chore: align release manifest source commit with refreshed surface ma… #236
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| concurrency: | |
| # Include SHA so a retag at a different commit gets its own concurrency | |
| # group and won't get blocked behind a stuck zombie queued run. | |
| group: release-${{ github.ref }}-${{ github.sha }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| id-token: write | |
| env: | |
| CARGO_TERM_COLOR: always | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| jobs: | |
| # All unit-level coverage (Linux, macOS, Windows cargo, Windows bash e2e). | |
| # Defined as a reusable workflow so tests.yml shares the exact same | |
| # workload. Release mode (strict=true) makes EVERY job blocking — including | |
| # the Windows jobs that are non-blocking at PR time. If a Windows-only | |
| # regression slipped past PR-time CI, this gate catches it before publish. | |
| unit: | |
| name: Unit | |
| uses: ./.github/workflows/_unit-suite.yml | |
| # The subc-core fetch needs the CK_CI_APP_* secrets (see tests.yml). | |
| secrets: inherit | |
| with: | |
| strict: true | |
| # Run the full E2E matrix (Linux Docker + Pi RPC + Windows native + | |
| # macOS native) at release time. Reuses the same workflow tests.yml runs | |
| # at PR time — single source of truth, so PR-time and release-time e2e | |
| # can never drift. Runs IN PARALLEL with unit (they share no artifacts); | |
| # every publishing job still `needs:` BOTH, so a regression in either | |
| # blocks the release exactly as before. Release e2e keeps the default | |
| # `release` cargo profile: the shipped fat-LTO binary shape is tested. | |
| e2e: | |
| name: E2E | |
| uses: ./.github/workflows/_e2e-suite.yml | |
| publish-crates: | |
| name: Publish to crates.io | |
| runs-on: ubuntu-latest | |
| needs: [unit, e2e] | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - uses: dtolnay/rust-toolchain@stable | |
| # Sync Cargo.toml version to match the tag. Without this, `cargo publish` | |
| # tries to publish whatever version is in HEAD's Cargo.toml — which is | |
| # last-released-version on tag pushes (the tag does NOT carry a separate | |
| # version commit; release.sh commits the version bump locally, but if | |
| # someone retags HEAD without running release.sh, Cargo.toml stays | |
| # stale). Mirrors the same step in publish-npm-platforms. | |
| - name: Sync versions from tag | |
| run: node scripts/version-sync.mjs --from-tag | |
| - name: Publish to crates.io | |
| # Allow re-runs of the same tag: treat "already exists" as success | |
| # ONLY if the version we're publishing matches the tag. Otherwise the | |
| # fallback silently masks the version-mismatch bug (we try to publish | |
| # vN, it already exists, we say success even though we wanted vN+1). | |
| # | |
| # `agent-file-tools` depends on `aft-tokenizer` by path; cargo refuses | |
| # to publish a crate whose path-only dep has no `version =`. Both | |
| # crates therefore declare versions (synced by version-sync.mjs) and | |
| # we publish `aft-tokenizer` first so `agent-file-tools` can resolve | |
| # the published version on crates.io. The `--no-verify` flag on the | |
| # tokenizer publish skips the build step (saves ~30s for a leaf crate | |
| # with no consumers in workspace until `agent-file-tools` resolves it | |
| # from crates.io after publish settles). | |
| run: | | |
| TAG_VERSION="${GITHUB_REF_NAME#v}" | |
| CARGO_VERSION=$(grep '^version' crates/aft/Cargo.toml | head -1 | sed -E 's/version = "([^"]+)"/\1/') | |
| if [ "$TAG_VERSION" != "$CARGO_VERSION" ]; then | |
| echo "::error::Tag $GITHUB_REF_NAME wants $TAG_VERSION but Cargo.toml has $CARGO_VERSION after version-sync. version-sync.mjs broken?" | |
| exit 1 | |
| fi | |
| # CRITICAL: use `true` (not `exit 0`) in the success branch of the | |
| # || fallback. `exit 0` in `||` terminates the WHOLE script, so the | |
| # second `cargo publish` is skipped silently when the first crate | |
| # is already published — which is exactly the retag-recovery case | |
| # this fallback exists for. (Repro: v0.28.1 retag — aft-tokenizer | |
| # already existed, agent-file-tools never published.) | |
| cargo publish --package aft-tokenizer \ | |
| || { ec=$?; cargo publish --package aft-tokenizer --dry-run 2>&1 | grep -q "already exists" && true || exit $ec; } | |
| # Wait briefly for the new tokenizer version to become visible on | |
| # crates.io's sparse index before publishing `agent-file-tools` — | |
| # otherwise the verify step in the second publish can race against | |
| # index propagation and fail with "no matching package found". | |
| sleep 30 | |
| cargo publish --package agent-file-tools \ | |
| || { ec=$?; cargo publish --package agent-file-tools --dry-run 2>&1 | grep -q "already exists" && true || exit $ec; } | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} | |
| # --------------------------------------------------------------------------- | |
| # Binary builds: NO `needs:` — they start immediately, in parallel with the | |
| # test stages. They are pure compute with no side effects (artifacts are | |
| # only uploaded to the run); every job that PUBLISHES anything still gates | |
| # on unit+e2e (publish-crates directly, github-release/publish-npm through | |
| # publish-crates), so a test regression blocks the release exactly as | |
| # before. A red run wastes some build minutes — accepted for cutting the | |
| # release critical path roughly in half. | |
| # --------------------------------------------------------------------------- | |
| build-darwin-arm64: | |
| name: Build macOS ARM64 | |
| runs-on: macos-26 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: aarch64-apple-darwin | |
| # cache-bin:false is what makes rust-cache safe on macOS (see | |
| # _unit-suite.yml unit-macos). | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| cache-bin: false | |
| cache-on-failure: true | |
| # Safety net: if someone retags HEAD without running release.sh, | |
| # Cargo.toml.version stays stale. Bake the tag's version into the | |
| # binary before cargo build so `aft --version` matches what the | |
| # plugin expects. | |
| - name: Sync versions from tag | |
| run: node scripts/version-sync.mjs --from-tag | |
| - name: Build | |
| run: cargo build --release --target aarch64-apple-darwin | |
| - name: Strip binary | |
| run: strip target/aarch64-apple-darwin/release/aft | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: darwin-arm64 | |
| path: target/aarch64-apple-darwin/release/aft | |
| if-no-files-found: error | |
| build-darwin-x64: | |
| name: Build macOS x64 | |
| runs-on: macos-26 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: x86_64-apple-darwin | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| cache-bin: false | |
| cache-on-failure: true | |
| - name: Sync versions from tag | |
| run: node scripts/version-sync.mjs --from-tag | |
| - name: Build | |
| run: cargo build --release --target x86_64-apple-darwin | |
| - name: Strip binary | |
| run: strip target/x86_64-apple-darwin/release/aft | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: darwin-x64 | |
| path: target/x86_64-apple-darwin/release/aft | |
| if-no-files-found: error | |
| build-linux-arm64: | |
| name: Build Linux ARM64 | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: aarch64-unknown-linux-gnu | |
| - name: Sync versions from tag | |
| run: node scripts/version-sync.mjs --from-tag | |
| # Prebuilt binary via binstall — `cargo install cross` compiled it from | |
| # source for 2-3 minutes on every release. | |
| - name: Install cross | |
| uses: taiki-e/install-action@v2 | |
| with: | |
| tool: cross@0.2.5 | |
| # Use gnu target (not musl) so dlopen works for ONNX Runtime loading. | |
| # musl produces static binaries where dlopen is a stub that always fails. | |
| - name: Build | |
| run: cross build --release --target aarch64-unknown-linux-gnu | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: linux-arm64 | |
| path: target/aarch64-unknown-linux-gnu/release/aft | |
| if-no-files-found: error | |
| build-linux-x64: | |
| name: Build Linux x64 | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - uses: dtolnay/rust-toolchain@stable | |
| - name: Sync versions from tag | |
| run: node scripts/version-sync.mjs --from-tag | |
| - name: Install cross | |
| uses: taiki-e/install-action@v2 | |
| with: | |
| tool: cross@0.2.5 | |
| # Use gnu target (not musl) so dlopen works for ONNX Runtime loading. | |
| # musl produces static binaries where dlopen is a stub that always fails. | |
| - name: Build | |
| run: cross build --release --target x86_64-unknown-linux-gnu | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: linux-x64 | |
| path: target/x86_64-unknown-linux-gnu/release/aft | |
| if-no-files-found: error | |
| build-win32-x64: | |
| name: Build Windows x64 | |
| runs-on: windows-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: x86_64-pc-windows-msvc | |
| - name: Sync versions from tag | |
| run: node scripts/version-sync.mjs --from-tag | |
| shell: pwsh | |
| - name: Build | |
| run: cargo build --release --target x86_64-pc-windows-msvc | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: win32-x64 | |
| path: target/x86_64-pc-windows-msvc/release/aft.exe | |
| if-no-files-found: error | |
| build-win32-arm64: | |
| name: Build Windows ARM64 | |
| # v0.28: native ARM64 binary. Host is windows-latest (x64) which | |
| # cross-compiles to aarch64-pc-windows-msvc via the MSVC ARM64 cross | |
| # toolchain bundled in the Visual Studio Build Tools on the runner. | |
| runs-on: windows-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: aarch64-pc-windows-msvc | |
| - name: Sync versions from tag | |
| run: node scripts/version-sync.mjs --from-tag | |
| shell: pwsh | |
| - name: Build | |
| run: cargo build --release --target aarch64-pc-windows-msvc | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: win32-arm64 | |
| path: target/aarch64-pc-windows-msvc/release/aft.exe | |
| if-no-files-found: error | |
| # npm publish runs AFTER github-release so that checksums.sha256 is already | |
| # available on the GitHub release page when ensureBinary() runs during | |
| # @cortexkit/aft-opencode@latest install. | |
| publish-npm: | |
| name: Publish to npm | |
| runs-on: ubuntu-latest | |
| needs: | |
| - build-darwin-arm64 | |
| - build-darwin-x64 | |
| - build-linux-arm64 | |
| - build-linux-x64 | |
| - build-win32-arm64 | |
| - build-win32-x64 | |
| - github-release | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| # The v0.49 release gate proves the surface-manifest source commit is | |
| # an ancestor of the candidate; a shallow (depth-1) clone makes | |
| # merge-base fail for every historical commit and the gate | |
| # fail-closed on a healthy release. | |
| fetch-depth: 0 | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: "24" | |
| registry-url: "https://registry.npmjs.org" | |
| - name: Ensure latest npm (for trusted publishing) | |
| run: npm install -g npm@latest | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Download darwin-arm64 binary | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: darwin-arm64 | |
| path: packages/npm/darwin-arm64/bin | |
| - name: Download darwin-x64 binary | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: darwin-x64 | |
| path: packages/npm/darwin-x64/bin | |
| - name: Download linux-arm64 binary | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: linux-arm64 | |
| path: packages/npm/linux-arm64/bin | |
| - name: Download linux-x64 binary | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: linux-x64 | |
| path: packages/npm/linux-x64/bin | |
| - name: Download win32-x64 binary | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: win32-x64 | |
| path: packages/npm/win32-x64/bin | |
| - name: Download win32-arm64 binary | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: win32-arm64 | |
| path: packages/npm/win32-arm64/bin | |
| - name: Set binary permissions | |
| run: | | |
| chmod +x packages/npm/darwin-arm64/bin/aft | |
| chmod +x packages/npm/darwin-x64/bin/aft | |
| chmod +x packages/npm/linux-arm64/bin/aft | |
| chmod +x packages/npm/linux-x64/bin/aft | |
| - name: Sync versions from tag | |
| run: node scripts/version-sync.mjs --from-tag | |
| - name: Validate packages | |
| run: node scripts/validate-packages.mjs | |
| - name: Install workspace dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Build aft-bridge | |
| run: bun run build | |
| working-directory: packages/aft-bridge | |
| - name: Build OpenCode plugin | |
| run: bun run build | |
| working-directory: packages/opencode-plugin | |
| - name: Build Pi plugin | |
| run: bun run build | |
| working-directory: packages/pi-plugin | |
| - name: Build CLI | |
| run: bun run build | |
| working-directory: packages/aft-cli | |
| - name: Fetch final v0.48.1 activation baseline | |
| run: git fetch --force origin refs/tags/v0.48.1:refs/tags/v0.48.1 | |
| - name: Verify v0.49 artifact and release gates | |
| run: node scripts/release-gate-v049.mjs --candidate --evidence docs/v0.49-release-evidence.json --require-platform-artifacts | |
| # Uses npm Trusted Publishing (OIDC) — configured per-package on npmjs.com | |
| - name: Publish platform packages | |
| run: | | |
| set -euo pipefail | |
| publish_or_skip() { | |
| local pkg_dir="$1" | |
| local pkg_name | |
| local version | |
| pkg_name=$(node -p "require('./${pkg_dir}/package.json').name") | |
| version=$(node -p "require('./${pkg_dir}/package.json').version") | |
| if npm view "${pkg_name}@${version}" version >/dev/null 2>&1; then | |
| echo "::notice::${pkg_name}@${version} already published; skipping" | |
| return 0 | |
| fi | |
| npm publish --access public --provenance "$pkg_dir" | |
| } | |
| for pkg in darwin-arm64 darwin-x64 linux-arm64 linux-x64 win32-arm64 win32-x64; do | |
| publish_or_skip "packages/npm/$pkg" | |
| done | |
| # aft-bridge MUST publish before the plugins, because @cortexkit/aft-opencode | |
| # and @cortexkit/aft-pi depend on it. Plugins publish from local builds, so | |
| # there's no install-time race here, but consumers installing @latest will | |
| # need aft-bridge resolvable on npm. | |
| # | |
| # Tolerate `already published` so a re-run of the same tag (or the | |
| # initial bootstrap publish that claims the package name with a | |
| # token before Trusted Publishing is configured) doesn't fail the | |
| # whole release. Mirrors the crates.io publish step above. | |
| - name: Publish @cortexkit/aft-bridge | |
| run: | | |
| set -euo pipefail | |
| PKG_NAME=$(node -p "require('./package.json').name") | |
| VERSION=$(node -p "require('./package.json').version") | |
| if npm view "${PKG_NAME}@${VERSION}" version >/dev/null 2>&1; then | |
| echo "::notice::${PKG_NAME}@${VERSION} already published; skipping" | |
| exit 0 | |
| fi | |
| npm publish --access public --provenance | |
| working-directory: packages/aft-bridge | |
| - name: Publish @cortexkit/aft-opencode | |
| run: | | |
| set -euo pipefail | |
| PKG_NAME=$(node -p "require('./package.json').name") | |
| VERSION=$(node -p "require('./package.json').version") | |
| if npm view "${PKG_NAME}@${VERSION}" version >/dev/null 2>&1; then | |
| echo "::notice::${PKG_NAME}@${VERSION} already published; skipping" | |
| exit 0 | |
| fi | |
| npm publish --access public --provenance | |
| working-directory: packages/opencode-plugin | |
| - name: Publish @cortexkit/aft-pi | |
| run: | | |
| set -euo pipefail | |
| PKG_NAME=$(node -p "require('./package.json').name") | |
| VERSION=$(node -p "require('./package.json').version") | |
| if npm view "${PKG_NAME}@${VERSION}" version >/dev/null 2>&1; then | |
| echo "::notice::${PKG_NAME}@${VERSION} already published; skipping" | |
| exit 0 | |
| fi | |
| npm publish --access public --provenance | |
| working-directory: packages/pi-plugin | |
| - name: Publish @cortexkit/aft (unified CLI) | |
| run: | | |
| set -euo pipefail | |
| PKG_NAME=$(node -p "require('./package.json').name") | |
| VERSION=$(node -p "require('./package.json').version") | |
| if npm view "${PKG_NAME}@${VERSION}" version >/dev/null 2>&1; then | |
| echo "::notice::${PKG_NAME}@${VERSION} already published; skipping" | |
| exit 0 | |
| fi | |
| npm publish --access public --provenance | |
| working-directory: packages/aft-cli | |
| # GitHub release must complete before npm publish so that checksums.sha256 | |
| # is available when freshly-installed @cortexkit/aft-opencode@latest triggers | |
| # ensureBinary(). crates.io publish is also gated so that a failed crate | |
| # publish blocks the release — no point shipping binaries if the Rust crate | |
| # isn't available yet. | |
| github-release: | |
| name: Create GitHub Release | |
| runs-on: ubuntu-latest | |
| needs: | |
| - build-darwin-arm64 | |
| - build-darwin-x64 | |
| - build-linux-arm64 | |
| - build-linux-x64 | |
| - build-win32-arm64 | |
| - build-win32-x64 | |
| - publish-crates | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| - name: Prepare release assets | |
| run: | | |
| mkdir -p release-assets | |
| cp artifacts/darwin-arm64/aft release-assets/aft-darwin-arm64 | |
| cp artifacts/darwin-x64/aft release-assets/aft-darwin-x64 | |
| cp artifacts/linux-arm64/aft release-assets/aft-linux-arm64 | |
| cp artifacts/linux-x64/aft release-assets/aft-linux-x64 | |
| cp artifacts/win32-arm64/aft.exe release-assets/aft-win32-arm64.exe | |
| cp artifacts/win32-x64/aft.exe release-assets/aft-win32-x64.exe | |
| chmod +x release-assets/aft-* | |
| - name: Generate checksums | |
| run: | | |
| cd release-assets | |
| sha256sum aft-* > checksums.sha256 | |
| cat checksums.sha256 | |
| - name: Verify curated release notes exist | |
| run: | | |
| notes_file=".alfonso/release-notes/${GITHUB_REF_NAME}.md" | |
| if [ ! -f "$notes_file" ]; then | |
| echo "::error::Curated release notes missing: $notes_file" | |
| echo "Draft them under .alfonso/release-notes/ before tagging the release." | |
| exit 1 | |
| fi | |
| echo "Using $notes_file as the release body." | |
| wc -l "$notes_file" | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| body_path: .alfonso/release-notes/${{ github.ref_name }}.md | |
| files: release-assets/* | |
| # Announce on Discord after the GitHub release is created. | |
| # | |
| # Why this lives here (not in discord-release.yml as `on: release: published`): | |
| # GitHub Actions intentionally suppresses event-triggered workflow cascades | |
| # from `GITHUB_TOKEN`-authored events. `softprops/action-gh-release@v2` uses | |
| # the default `GITHUB_TOKEN`, so the `release: published` event it emits does | |
| # not trigger downstream workflows. Inlining the Discord post here is the | |
| # cleanest fix (vs rotating PATs for release creation). | |
| # | |
| # `continue-on-error: true` keeps Discord failures from rolling back a | |
| # successful release — if the webhook is down, the announcement can be | |
| # retried via `gh workflow run discord-release.yml ...`. | |
| discord-announce: | |
| name: Announce on Discord | |
| runs-on: ubuntu-latest | |
| # MUST wait for BOTH github-release AND publish-npm. Without `publish-npm` | |
| # the announcement can fire while npm packages are still uploading, | |
| # leaving users who click the release link unable to install the | |
| # versions Discord just announced. | |
| needs: | |
| - github-release | |
| - publish-npm | |
| if: success() | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Read curated release notes | |
| id: notes | |
| run: | | |
| notes_file=".alfonso/release-notes/${GITHUB_REF_NAME}.md" | |
| if [ ! -f "$notes_file" ]; then | |
| echo "::error::Curated release notes missing: $notes_file" | |
| exit 1 | |
| fi | |
| # Use a random delimiter to guard against any string in the notes | |
| # accidentally matching the heredoc terminator. Also ensure the file | |
| # ends with a newline so `cat`'s output doesn't glue onto the EOF | |
| # marker (v0.26.3 release notes had no trailing newline, which | |
| # produced "Matching delimiter not found" and skipped the Discord | |
| # announcement entirely). | |
| eof="EOF_$(openssl rand -hex 12)" | |
| { | |
| printf 'body<<%s\n' "$eof" | |
| cat "$notes_file" | |
| # Guarantee a newline before the closing delimiter, even if the | |
| # file itself doesn't end with one. | |
| printf '\n%s\n' "$eof" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Post to Discord | |
| uses: SethCohen/github-releases-to-discord@v1 | |
| with: | |
| webhook_url: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }} | |
| # Cortexkit teal — decimal value of #2C4A7C. | |
| color: "2902140" | |
| username: "AFT Releases" | |
| # Reduce h1/h2 headings to fit Discord embed limits comfortably. | |
| reduce_headings: true | |
| # Keep PR/issue links — useful context in Discord. | |
| remove_github_reference_links: false | |
| footer_title: "Changelog" | |
| footer_timestamp: true | |
| release_name: ${{ github.ref_name }} | |
| release_body: ${{ steps.notes.outputs.body }} | |
| release_html_url: https://github.com/${{ github.repository }}/releases/tag/${{ github.ref_name }} |