ci: publish Sprout Agent Bundle (sprout-acp + sprout-agent + sprout-dev-mcp) to GitHub Releases (#603)

Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com>
This commit is contained in:
tlongwell-block
2026-05-16 17:04:00 -04:00
committed by GitHub
parent 3daf8d6ae1
commit 6ba46f21da
4 changed files with 438 additions and 97 deletions

View File

@@ -0,0 +1,221 @@
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.75.18
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 & 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
# 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=${ARCHIVE_BASENAME}.tar.gz" >> "$GITHUB_OUTPUT"
- name: Upload workflow artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
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.3.0
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 }}
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\` (\`${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 \
--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:
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.3.0
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/*

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
# Build artifacts
/target/
/dist/
# Environment files (may contain secrets)
.env

216
scripts/build-agent-bundle.sh Executable file
View File

@@ -0,0 +1,216 @@
#!/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
# SKIP_BUILD=1 skip the cargo/cross build (use prebuilt binaries
# already present in target/[<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-
# <target>`). Defaults to
# `sprout-agent-bundle-<version>-<target>`.
# DIST_DIR output directory (default: dist)
#
# Output:
# ${DIST_DIR}/${ARCHIVE_BASENAME}.tar.gz
# ${DIST_DIR}/${ARCHIVE_BASENAME}.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" <<JSON
{
"name": "sprout-agent-bundle",
"version": "${VERSION}",
"git_sha": "${GIT_SHA}",
"target": "${TARGET}",
"binaries": [${ENTRIES_JSON}]
}
JSON
# README — human-readable.
cat > "${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_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.
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 `<sha> <filename>` 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"

View File

@@ -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<version>-<target>.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}"