From 4dc22c7500f2c23700e948b558513b4f30ff840f Mon Sep 17 00:00:00 2001 From: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com> Date: Sat, 16 May 2026 16:12:53 -0400 Subject: [PATCH 1/3] ci: publish Sprout Agent Bundle (sprout-acp + sprout-agent + sprout-dev-mcp) to GitHub Releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Linux release bundle for external services that need to embed a Sprout agent end-to-end (e.g. sprout-backend-blox). What's in the bundle: - sprout-acp — ACP harness that bridges Sprout events to an agent - sprout-agent — ACP-compliant agent (spawns MCP, calls LLMs) - sprout-dev-mcp — Developer MCP server (multicall: rg, tree, sprout, git-credential-nostr, git-sign-nostr) Workflow `.github/workflows/sprout-agent-bundle.yml`: - Targets x86_64-unknown-linux-musl and aarch64-unknown-linux-musl - Uses the same `cross@0.2.5` toolchain already pinned in ci.yml - Triggers: push to main → updates rolling `sprout-agent-bundle-latest` release tag `sprout-agent-bundle-v*` → versioned release workflow_dispatch → manual build (optional publish) - Uses `gh release` (matches release.yml style), not a new third-party action - Uploads asset + .sha256 sidecar per target Script `scripts/build-agent-bundle.sh` is the single source of truth for packaging (CI calls it directly). It: - Builds with `cross` when cross-compiling, plain `cargo` on host - Strips binaries (best-effort) - Emits `bundle.json` with {name, version, git_sha, target, binaries: [{name, sha256, size}]} for downstream consumers - Produces a deterministic-ish tarball (sorted, uid=0/gid=0) with a README and bundle.json - Supports SKIP_BUILD=1 for re-packaging without rebuilding (used in local verification and useful for fast iteration) Replaces `scripts/build-agent-release.sh`, which only packaged sprout-agent + sprout-dev-mcp. Not in this PR (intentional, per discussion): - sprout-backend-blox is not yet switched over to consume the bundle - macOS/Windows bundles - sprout-mcp-server, git-credential-nostr, git-sign-nostr as separate binaries (the dev-mcp multicall covers them) Verified locally on aarch64-apple-darwin with SKIP_BUILD=1: tarball contains the 3 binaries + README.md + bundle.json, sha256 sidecar matches, and bundle.json parses cleanly under jq. Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com> --- .github/workflows/sprout-agent-bundle.yml | 198 +++++++++++++++++++++ .gitignore | 1 + scripts/build-agent-bundle.sh | 207 ++++++++++++++++++++++ scripts/build-agent-release.sh | 97 ---------- 4 files changed, 406 insertions(+), 97 deletions(-) create mode 100644 .github/workflows/sprout-agent-bundle.yml create mode 100755 scripts/build-agent-bundle.sh delete mode 100755 scripts/build-agent-release.sh diff --git a/.github/workflows/sprout-agent-bundle.yml b/.github/workflows/sprout-agent-bundle.yml new file mode 100644 index 000000000..9a6c43846 --- /dev/null +++ b/.github/workflows/sprout-agent-bundle.yml @@ -0,0 +1,198 @@ +name: Sprout Agent Bundle + +# Builds and publishes the Sprout Agent Bundle — a Linux tarball containing +# the three binaries an external service (e.g. sprout-backend-blox) needs to +# run a Sprout agent end-to-end: +# +# sprout-acp ACP harness that bridges Sprout events to the LLM agent +# sprout-agent ACP-compliant agent (spawns MCP, calls LLMs) +# sprout-dev-mcp Developer MCP server (multicall: rg, tree, sprout, +# git-credential-nostr, git-sign-nostr) +# +# Targets: x86_64-unknown-linux-musl and aarch64-unknown-linux-musl (static +# musl so the tarball runs on any modern Linux without libc surprises). +# +# Triggers: +# - push to main → updates rolling `sprout-agent-bundle-latest` release +# - tag `sprout-agent-bundle-v*` → versioned release +# - workflow_dispatch → manual canary build (no release publish) + +on: + push: + branches: [main] + tags: ["sprout-agent-bundle-v*"] + workflow_dispatch: + inputs: + publish: + description: "Publish to the rolling release (otherwise artifacts only)" + type: boolean + default: false + +permissions: + contents: read + +jobs: + build: + name: Build (${{ matrix.target }}) + runs-on: ubuntu-latest + timeout-minutes: 45 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + target: + - x86_64-unknown-linux-musl + - aarch64-unknown-linux-musl + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1 + + - name: Install cross + uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2 + with: + tool: cross@0.2.5 + + - name: Resolve version + id: ver + run: | + set -euo pipefail + WORKSPACE_VERSION=$(cargo metadata --no-deps --format-version=1 \ + | jq -r '.workspace_default_members[0] as $m | .packages[] | select(.id==$m) | .version') + if [[ -z "$WORKSPACE_VERSION" || "$WORKSPACE_VERSION" == "null" ]]; then + # Fallback: read sprout-acp's version directly. + WORKSPACE_VERSION=$(cargo metadata --no-deps --format-version=1 \ + | jq -r '.packages[] | select(.name=="sprout-acp") | .version') + fi + + REF="${GITHUB_REF#refs/tags/}" + if [[ "$GITHUB_REF" == refs/tags/sprout-agent-bundle-v* ]]; then + VERSION="${REF#sprout-agent-bundle-v}" + CHANNEL="tag" + else + SHORT_SHA="${GITHUB_SHA::7}" + VERSION="${WORKSPACE_VERSION}+git.${SHORT_SHA}" + CHANNEL="rolling" + fi + { + echo "workspace_version=$WORKSPACE_VERSION" + echo "version=$VERSION" + echo "channel=$CHANNEL" + } >> "$GITHUB_OUTPUT" + echo "Resolved version=$VERSION channel=$CHANNEL" + + - name: Build binaries + env: + TARGET: ${{ matrix.target }} + run: | + cross build --release --target "$TARGET" \ + -p sprout-acp \ + -p sprout-agent \ + -p sprout-dev-mcp + + - name: Package bundle + id: pkg + env: + TARGET: ${{ matrix.target }} + VERSION: ${{ steps.ver.outputs.version }} + GIT_SHA: ${{ github.sha }} + run: | + set -euo pipefail + ./scripts/build-agent-bundle.sh "$VERSION" "$TARGET" + # Capture the produced archive path for the upload step. + ARCHIVE="dist/sprout-agent-bundle-${VERSION}-${TARGET}.tar.gz" + test -f "$ARCHIVE" + echo "archive=$ARCHIVE" >> "$GITHUB_OUTPUT" + echo "archive_name=$(basename "$ARCHIVE")" >> "$GITHUB_OUTPUT" + + - name: Upload workflow artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: sprout-agent-bundle-${{ matrix.target }} + path: | + ${{ steps.pkg.outputs.archive }} + ${{ steps.pkg.outputs.archive }}.sha256 + if-no-files-found: error + retention-days: 30 + + publish: + name: Publish rolling release + needs: build + if: | + github.event_name == 'push' && github.ref == 'refs/heads/main' + || (github.event_name == 'workflow_dispatch' && inputs.publish) + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Download all bundle artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + path: dist + pattern: sprout-agent-bundle-* + merge-multiple: true + + - name: List assets + run: ls -lh dist/ + + - name: Update rolling release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + TAG="sprout-agent-bundle-latest" + TITLE="Sprout Agent Bundle (rolling)" + NOTES="Rolling Linux build of the Sprout Agent Bundle (sprout-acp + sprout-agent + sprout-dev-mcp), tracking \`main\` (\`${GITHUB_SHA}\`)." + + # Create the release if it doesn't exist; otherwise reuse it. + if ! gh release view "$TAG" >/dev/null 2>&1; then + gh release create "$TAG" \ + --prerelease \ + --title "$TITLE" \ + --notes "$NOTES" + else + gh release edit "$TAG" --prerelease --title "$TITLE" --notes "$NOTES" + fi + gh release upload "$TAG" dist/* --clobber + + publish-tag: + name: Publish tagged release + needs: build + if: startsWith(github.ref, 'refs/tags/sprout-agent-bundle-v') + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Download all bundle artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + path: dist + pattern: sprout-agent-bundle-* + merge-multiple: true + + - name: Resolve tag version + id: ver + run: | + REF="${GITHUB_REF#refs/tags/}" + VERSION="${REF#sprout-agent-bundle-v}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=$REF" >> "$GITHUB_OUTPUT" + + - name: Create tagged release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.ver.outputs.tag }} + VERSION: ${{ steps.ver.outputs.version }} + run: | + set -euo pipefail + gh release create "$TAG" \ + --title "Sprout Agent Bundle v${VERSION}" \ + --notes "Sprout Agent Bundle v${VERSION} — Linux builds of sprout-acp + sprout-agent + sprout-dev-mcp." \ + dist/* diff --git a/.gitignore b/.gitignore index 18c8f7d5f..b5ab3b4f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Build artifacts /target/ +/dist/ # Environment files (may contain secrets) .env diff --git a/scripts/build-agent-bundle.sh b/scripts/build-agent-bundle.sh new file mode 100755 index 000000000..d7096eb0d --- /dev/null +++ b/scripts/build-agent-bundle.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash +# Build the Sprout Agent Bundle — a tarball containing the three binaries +# needed to run a Sprout agent end-to-end: +# +# sprout-acp ACP harness +# sprout-agent ACP-compliant agent (spawns MCP, calls LLMs) +# sprout-dev-mcp Developer MCP server (multicall: rg/tree/sprout/git-*) +# +# Usage: +# ./scripts/build-agent-bundle.sh [version] [target] +# +# Environment overrides: +# TARGET cross-compile target (defaults to host) +# USE_CROSS=1 use `cross` instead of `cargo` for the build +# +# Output: +# dist/sprout-agent-bundle--.tar.gz +# dist/sprout-agent-bundle--.tar.gz.sha256 +# +# The tarball contains: +# sprout-acp +# sprout-agent +# sprout-dev-mcp +# README.md +# bundle.json { version, git_sha, target, binaries: [{name, sha256, size}] } + +set -euo pipefail + +VERSION="${1:-${VERSION:-0.0.0-dev}}" +HOST_TARGET="$(rustc -vV | sed -n 's|host: ||p')" +TARGET="${2:-${TARGET:-$HOST_TARGET}}" +DIST_DIR="${DIST_DIR:-dist}" + +# Resolve git SHA (best effort — works in CI checkout and local clones). +if GIT_SHA="$(git rev-parse HEAD 2>/dev/null)"; then + : +else + GIT_SHA="unknown" +fi + +BINARIES=(sprout-acp sprout-agent sprout-dev-mcp) + +echo "==> Building Sprout Agent Bundle v${VERSION} for ${TARGET}" +echo " git_sha=${GIT_SHA}" +echo " binaries=${BINARIES[*]}" + +# Pick build driver. `cross` is required for cross-compilation in CI; +# for host builds we use plain `cargo` so contributors don't need Docker. +if [[ "${USE_CROSS:-0}" == "1" ]] || [[ "$TARGET" != "$HOST_TARGET" ]]; then + if ! command -v cross >/dev/null 2>&1; then + echo "error: cross-compiling to $TARGET requires \`cross\` (install: cargo install cross --version 0.2.5)" >&2 + exit 1 + fi + BUILDER=(cross build --release --target "$TARGET") + BIN_DIR="target/${TARGET}/release" +else + BUILDER=(cargo build --release) + BIN_DIR="target/release" +fi + +PKG_ARGS=() +for bin in "${BINARIES[@]}"; do + PKG_ARGS+=(-p "$bin") +done + +if [[ "${SKIP_BUILD:-0}" == "1" ]]; then + echo " (SKIP_BUILD=1 set — expecting prebuilt binaries in ${BIN_DIR}/)" +else + "${BUILDER[@]}" "${PKG_ARGS[@]}" +fi + +# Verify all binaries exist. +for bin in "${BINARIES[@]}"; do + if [[ ! -f "${BIN_DIR}/${bin}" ]]; then + echo "error: ${BIN_DIR}/${bin} not found after build" >&2 + exit 1 + fi +done + +# Stage into a tempdir. +mkdir -p "${DIST_DIR}" +STAGING="$(mktemp -d)" +trap 'rm -rf "${STAGING}"' EXIT + +# sha256 helper: prefer sha256sum (linux), fall back to shasum -a 256 (macos). +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + shasum -a 256 "$1" | awk '{print $1}' + fi +} + +# Copy + strip binaries, collect manifest entries. +MANIFEST_ENTRIES=() +for bin in "${BINARIES[@]}"; do + cp "${BIN_DIR}/${bin}" "${STAGING}/${bin}" + chmod 0755 "${STAGING}/${bin}" + # Best-effort strip. `cross` images include the cross-target strip; on host + # we use the system strip. Skip silently if unavailable (e.g. cross-arch + # local builds on macOS). + if command -v strip >/dev/null 2>&1; then + strip "${STAGING}/${bin}" 2>/dev/null || true + fi + sha="$(sha256_of "${STAGING}/${bin}")" + size="$(wc -c < "${STAGING}/${bin}" | tr -d ' ')" + MANIFEST_ENTRIES+=("{\"name\":\"${bin}\",\"sha256\":\"${sha}\",\"size\":${size}}") +done + +# bundle.json — machine-readable manifest. +ENTRIES_JSON="$(IFS=,; echo "${MANIFEST_ENTRIES[*]}")" +cat > "${STAGING}/bundle.json" < "${STAGING}/README.md" <<'EOF' +# Sprout Agent Bundle + +Linux build of the three binaries needed to run a Sprout agent end-to-end: + +- `sprout-acp` — ACP harness that bridges Sprout channel events to an + ACP-compliant agent over stdio. +- `sprout-agent` — ACP-compliant agent (spawns MCP servers, calls LLMs). +- `sprout-dev-mcp` — Developer MCP server (shell, str_replace, todo) and + multicall entrypoint for `rg`, `tree`, `sprout`, `git-credential-nostr`, + `git-sign-nostr`. + +See `bundle.json` for binary SHA-256s, sizes, and the source git SHA. + +## Install + +```bash +tar -xzf sprout-agent-bundle-*.tar.gz -C /opt/sprout-agent +export PATH="/opt/sprout-agent:$PATH" +``` + +## Configure + +```bash +# Agent provider +export SPROUT_AGENT_PROVIDER=anthropic # or openai +export ANTHROPIC_API_KEY=sk-... +export ANTHROPIC_MODEL=claude-sonnet-4-20250514 + +# Nostr identity (shared by sprout-acp, git auth, signing, and sprout CLI) +export NOSTR_PRIVATE_KEY=nsec1... +export SPROUT_PRIVATE_KEY="$NOSTR_PRIVATE_KEY" +export SPROUT_RELAY_URL=https://your-relay.example.com +``` + +## Git Integration + +When `NOSTR_PRIVATE_KEY` is set, `sprout-dev-mcp` automatically configures +git to use nostr-based credential auth and commit signing for all shell +commands. This is ephemeral (session-scoped via `GIT_CONFIG_*` env vars) — +your persistent git config is never modified. + +The nostr credential helper is additive: it silently declines non-Sprout +remotes so git falls through to your system credential helpers for GitHub, +GitLab, etc. `NOSTR_PRIVATE_KEY` is written to a 0600 keyfile and removed +from the process environment — shell commands cannot read it from env. + +## Multicall Binary + +`sprout-dev-mcp` is a multicall binary. When symlinked/invoked as: + +- `rg` — ripgrep-compatible search +- `tree` — directory tree with line counts +- `sprout` — Sprout relay CLI +- `git-credential-nostr` — NIP-98 git credential helper +- `git-sign-nostr` — NIP-GS git commit/tag signing + +…it dispatches to the corresponding subcommand. The installer is free to +symlink these names next to `sprout-dev-mcp` on the PATH. +EOF + +# Tar. +ARCHIVE_NAME="sprout-agent-bundle-${VERSION}-${TARGET}.tar.gz" +ARCHIVE_PATH="${DIST_DIR}/${ARCHIVE_NAME}" + +# Deterministic-ish tar: sorted entries, no owner/group info. +tar \ + --sort=name \ + --owner=0 --group=0 --numeric-owner \ + -czf "${ARCHIVE_PATH}" \ + -C "${STAGING}" \ + . 2>/dev/null || \ +tar -czf "${ARCHIVE_PATH}" -C "${STAGING}" . # fallback for BSD tar (macOS) + +# Sidecar checksum. +sha256_of "${ARCHIVE_PATH}" > "${ARCHIVE_PATH}.sha256" +# Pretty-print the form ` ` like sha256sum -c expects. +echo "$(cat "${ARCHIVE_PATH}.sha256") ${ARCHIVE_NAME}" > "${ARCHIVE_PATH}.sha256" + +echo "" +echo "==> Built: ${ARCHIVE_PATH}" +ls -lh "${ARCHIVE_PATH}" "${ARCHIVE_PATH}.sha256" +echo "" +echo "==> bundle.json:" +sed 's/^/ /' "${STAGING}/bundle.json" diff --git a/scripts/build-agent-release.sh b/scripts/build-agent-release.sh deleted file mode 100755 index ab1547bd5..000000000 --- a/scripts/build-agent-release.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Build a release tarball containing sprout-agent + sprout-dev-mcp. -# Usage: ./scripts/build-agent-release.sh [version] -# TARGET=aarch64-unknown-linux-musl ./scripts/build-agent-release.sh 0.1.0 -# Output: dist/sprout-agent-v-.tar.gz - -VERSION="${1:-0.1.0}" -HOST_TARGET="$(rustc -vV | sed -n 's|host: ||p')" -TARGET="${TARGET:-$HOST_TARGET}" -DIST_DIR="dist" - -echo "Building sprout-agent release v${VERSION} for ${TARGET}..." - -# Build release binaries — use --target only when cross-compiling. -if [[ "$TARGET" == "$HOST_TARGET" ]]; then - cargo build --release -p sprout-agent -p sprout-dev-mcp - BIN_DIR="target/release" -else - cargo build --release --target "$TARGET" -p sprout-agent -p sprout-dev-mcp - BIN_DIR="target/${TARGET}/release" -fi - -# Verify binaries exist -for bin in sprout-agent sprout-dev-mcp; do - if [[ ! -f "${BIN_DIR}/${bin}" ]]; then - echo "error: ${BIN_DIR}/${bin} not found" >&2 - exit 1 - fi -done - -# Package -mkdir -p "${DIST_DIR}" -ARCHIVE_NAME="sprout-agent-v${VERSION}-${TARGET}.tar.gz" -STAGING=$(mktemp -d) -trap 'rm -rf "${STAGING}"' EXIT - -cp "${BIN_DIR}/sprout-agent" "${STAGING}/" -cp "${BIN_DIR}/sprout-dev-mcp" "${STAGING}/" - -cat > "${STAGING}/README.md" << 'EOF' -# Sprout Agent - -Minimal ACP agent + developer MCP toolchain. - -## Contents - -- `sprout-agent` — ACP-compliant agent (spawns MCP servers, calls LLMs) -- `sprout-dev-mcp` — Developer MCP server (shell, str_replace, todo, rg, tree, - sprout CLI, git-credential-nostr, git-sign-nostr) - -## Quick Start - -```bash -# Place both binaries on your PATH -export PATH="/path/to/this/dir:$PATH" - -# Set required env vars -export SPROUT_AGENT_PROVIDER=anthropic # or openai -export ANTHROPIC_API_KEY=sk-... -export ANTHROPIC_MODEL=claude-sonnet-4-20250514 - -# Nostr identity (same key for git auth, signing, and relay CLI) -export NOSTR_PRIVATE_KEY=nsec1... -export SPROUT_PRIVATE_KEY=$NOSTR_PRIVATE_KEY -export SPROUT_RELAY_URL=https://your-relay.example.com -``` - -## Git Integration - -When `NOSTR_PRIVATE_KEY` is set, the dev-mcp automatically configures git to -use nostr-based credential auth and commit signing for all shell commands. -This is ephemeral (session-scoped via `GIT_CONFIG_*` env vars) — your -persistent git config is never modified. - -The nostr credential helper is additive: it silently declines non-Sprout -remotes so git falls through to your system credential helpers for GitHub, -GitLab, etc. `NOSTR_PRIVATE_KEY` is written to a 0600 keyfile and removed -from the process environment — shell commands cannot read it from env. - -Set `SPROUT_PRIVATE_KEY` to the same key for the `sprout` relay CLI. - -## Multicall Binary - -`sprout-dev-mcp` is a multicall binary. When symlinked/invoked as: -- `rg` — ripgrep-compatible search -- `tree` — directory tree with line counts -- `sprout` — Sprout relay CLI -- `git-credential-nostr` — NIP-98 git credential helper -- `git-sign-nostr` — NIP-GS git commit/tag signing -EOF - -tar -czf "${DIST_DIR}/${ARCHIVE_NAME}" -C "${STAGING}" . - -echo "Built: ${DIST_DIR}/${ARCHIVE_NAME}" -ls -lh "${DIST_DIR}/${ARCHIVE_NAME}" From 866739b83014d8ad4bfd61ded5214b0394f357e0 Mon Sep 17 00:00:00 2001 From: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com> Date: Sat, 16 May 2026 16:42:33 -0400 Subject: [PATCH 2/3] =?UTF-8?q?ci(sprout-agent-bundle):=20address=20review?= =?UTF-8?q?=20=E2=80=94=20force-move=20rolling=20tag,=20stable=20rolling?= =?UTF-8?q?=20asset=20names,=20single-source=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Max's review on #603. Rolling release semantics: - Force-move the `sprout-agent-bundle-latest` git tag to the current `main` SHA before touching the release. `gh release edit` updates release metadata but does not move the underlying tag, so without this the tag would stick to whatever SHA first created the release. Uses `gh api PATCH .../git/refs/tags/...` (with force=true) and falls back to `POST .../git/refs` for the initial-create case. - Also pass `--target ${SHA}` to `gh release create/edit` so the release's "target commitish" tracks the build SHA. Stale-asset cleanup on the rolling release: - Rolling builds now use stable, version-less asset filenames (`sprout-agent-bundle-.tar.gz`). `--clobber` overwrites them in place on every push, so the release no longer accumulates SHA-named tarballs. - Tagged releases keep the versioned filename (`sprout-agent-bundle--.tar.gz`) for traceability. - The git SHA + version always live inside `bundle.json` regardless of the asset filename, so consumers/debugging never lose provenance. - `scripts/build-agent-bundle.sh` gains an `ARCHIVE_BASENAME` env var override (documented in the header) so the workflow can pick the right filename per channel; default unchanged. Single source of truth for the build: - Removed the workflow's explicit `cross build` step. The script already auto-selects `cross` when target != host, so the previous setup was building everything twice. Verified locally with `SKIP_BUILD=1`: - Rolling-style basename → `sprout-agent-bundle-.tar.gz` with sha sidecar `OK` under `sha256sum -c`. - Tagged-style basename → `sprout-agent-bundle--.tar.gz`. - Both tarballs contain the 3 binaries + README + bundle.json with matching shas/sizes. - actionlint + shellcheck clean. Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com> --- .github/workflows/sprout-agent-bundle.yml | 55 ++++++++++++++++------- scripts/build-agent-bundle.sh | 15 +++++-- 2 files changed, 51 insertions(+), 19 deletions(-) diff --git a/.github/workflows/sprout-agent-bundle.yml b/.github/workflows/sprout-agent-bundle.yml index 9a6c43846..f78114175 100644 --- a/.github/workflows/sprout-agent-bundle.yml +++ b/.github/workflows/sprout-agent-bundle.yml @@ -82,29 +82,30 @@ jobs: } >> "$GITHUB_OUTPUT" echo "Resolved version=$VERSION channel=$CHANNEL" - - name: Build binaries - env: - TARGET: ${{ matrix.target }} - run: | - cross build --release --target "$TARGET" \ - -p sprout-acp \ - -p sprout-agent \ - -p sprout-dev-mcp - - - name: Package bundle + - name: Build & package bundle id: pkg env: TARGET: ${{ matrix.target }} VERSION: ${{ steps.ver.outputs.version }} + CHANNEL: ${{ steps.ver.outputs.channel }} GIT_SHA: ${{ github.sha }} run: | set -euo pipefail - ./scripts/build-agent-bundle.sh "$VERSION" "$TARGET" - # Capture the produced archive path for the upload step. - ARCHIVE="dist/sprout-agent-bundle-${VERSION}-${TARGET}.tar.gz" + # Rolling releases use stable, version-less filenames so the + # asset overwrites cleanly on every push to main. Tagged + # releases keep the version in the filename for traceability. + # The git SHA + version always live inside bundle.json. + if [[ "$CHANNEL" == "tag" ]]; then + ARCHIVE_BASENAME="sprout-agent-bundle-${VERSION}-${TARGET}" + else + ARCHIVE_BASENAME="sprout-agent-bundle-${TARGET}" + fi + ARCHIVE_BASENAME="$ARCHIVE_BASENAME" \ + ./scripts/build-agent-bundle.sh "$VERSION" "$TARGET" + ARCHIVE="dist/${ARCHIVE_BASENAME}.tar.gz" test -f "$ARCHIVE" echo "archive=$ARCHIVE" >> "$GITHUB_OUTPUT" - echo "archive_name=$(basename "$ARCHIVE")" >> "$GITHUB_OUTPUT" + echo "archive_name=${ARCHIVE_BASENAME}.tar.gz" >> "$GITHUB_OUTPUT" - name: Upload workflow artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 @@ -142,21 +143,43 @@ jobs: - name: Update rolling release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + SHA: ${{ github.sha }} run: | set -euo pipefail TAG="sprout-agent-bundle-latest" TITLE="Sprout Agent Bundle (rolling)" - NOTES="Rolling Linux build of the Sprout Agent Bundle (sprout-acp + sprout-agent + sprout-dev-mcp), tracking \`main\` (\`${GITHUB_SHA}\`)." + NOTES="Rolling Linux build of the Sprout Agent Bundle (sprout-acp + sprout-agent + sprout-dev-mcp), tracking \`main\` (\`${SHA}\`)." + + # Force-move the underlying git tag to the current SHA before + # we touch the release. `gh release edit` updates release + # metadata but does *not* move the tag, so without this the + # tag would stick to whatever SHA first created the release. + if gh api "repos/${REPO}/git/refs/tags/${TAG}" >/dev/null 2>&1; then + gh api -X PATCH "repos/${REPO}/git/refs/tags/${TAG}" \ + -f sha="${SHA}" -F force=true >/dev/null + else + gh api -X POST "repos/${REPO}/git/refs" \ + -f ref="refs/tags/${TAG}" -f sha="${SHA}" >/dev/null + fi # Create the release if it doesn't exist; otherwise reuse it. if ! gh release view "$TAG" >/dev/null 2>&1; then gh release create "$TAG" \ --prerelease \ + --target "${SHA}" \ --title "$TITLE" \ --notes "$NOTES" else - gh release edit "$TAG" --prerelease --title "$TITLE" --notes "$NOTES" + gh release edit "$TAG" \ + --prerelease \ + --target "${SHA}" \ + --title "$TITLE" \ + --notes "$NOTES" fi + # Asset filenames are stable for rolling builds (no version + # in filename — see the package step), so --clobber overwrites + # them in place. No stale-asset accumulation. gh release upload "$TAG" dist/* --clobber publish-tag: diff --git a/scripts/build-agent-bundle.sh b/scripts/build-agent-bundle.sh index d7096eb0d..45f7e20bb 100755 --- a/scripts/build-agent-bundle.sh +++ b/scripts/build-agent-bundle.sh @@ -12,10 +12,18 @@ # Environment overrides: # TARGET cross-compile target (defaults to host) # USE_CROSS=1 use `cross` instead of `cargo` for the build +# SKIP_BUILD=1 skip the cargo/cross build (use prebuilt binaries +# already present in target/[/]release) +# ARCHIVE_BASENAME override the archive basename (sans .tar.gz). Useful +# for rolling releases where the asset filename should +# be stable across builds (e.g. `sprout-agent-bundle- +# `). Defaults to +# `sprout-agent-bundle--`. +# DIST_DIR output directory (default: dist) # # Output: -# dist/sprout-agent-bundle--.tar.gz -# dist/sprout-agent-bundle--.tar.gz.sha256 +# ${DIST_DIR}/${ARCHIVE_BASENAME}.tar.gz +# ${DIST_DIR}/${ARCHIVE_BASENAME}.tar.gz.sha256 # # The tarball contains: # sprout-acp @@ -182,7 +190,8 @@ symlink these names next to `sprout-dev-mcp` on the PATH. EOF # Tar. -ARCHIVE_NAME="sprout-agent-bundle-${VERSION}-${TARGET}.tar.gz" +ARCHIVE_BASENAME="${ARCHIVE_BASENAME:-sprout-agent-bundle-${VERSION}-${TARGET}}" +ARCHIVE_NAME="${ARCHIVE_BASENAME}.tar.gz" ARCHIVE_PATH="${DIST_DIR}/${ARCHIVE_NAME}" # Deterministic-ish tar: sorted entries, no owner/group info. From 59a28d059322355c2f23ff18b782943619a5f90c Mon Sep 17 00:00:00 2001 From: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com> Date: Sat, 16 May 2026 16:54:56 -0400 Subject: [PATCH 3/3] ci(sprout-agent-bundle): pin action comments to exact tags (zizmor hygiene) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the zizmor PR review thread on #603: the SHA-pinned action comments should match the exact upstream tag, not a broader floating tag, so the scanner doesn't flag a metadata mismatch. - taiki-e/install-action: `# v2` → `# v2.75.18` (the SHA's exact tag) - actions/upload-artifact: `# v4` → `# v4.6.2` - actions/download-artifact: `# v4` → `# v4.3.0` Pinned SHAs are unchanged — no behavior change, just correct metadata. `actions/checkout` and `cashapp/activate-hermit` already match (their SHAs are the literal `v6` / `v1` commits). Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com> --- .github/workflows/sprout-agent-bundle.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sprout-agent-bundle.yml b/.github/workflows/sprout-agent-bundle.yml index f78114175..437124113 100644 --- a/.github/workflows/sprout-agent-bundle.yml +++ b/.github/workflows/sprout-agent-bundle.yml @@ -50,7 +50,7 @@ jobs: - uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1 - name: Install cross - uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2 + uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 with: tool: cross@0.2.5 @@ -108,7 +108,7 @@ jobs: echo "archive_name=${ARCHIVE_BASENAME}.tar.gz" >> "$GITHUB_OUTPUT" - name: Upload workflow artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: sprout-agent-bundle-${{ matrix.target }} path: | @@ -131,7 +131,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Download all bundle artifacts - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: path: dist pattern: sprout-agent-bundle-* @@ -194,7 +194,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Download all bundle artifacts - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: path: dist pattern: sprout-agent-bundle-*