Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 113 additions & 9 deletions .github/workflows/release-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ name: Release binaries
# the Git tag are frozen the moment a release is published. Assets are therefore
# attached to a draft and the draft is published last.

# Manual-only, deliberately: two of the four matrix legs need macOS runners,
# which are billed at a premium on GitHub-hosted infrastructure, so binaries
# are built when a human dispatches this workflow for a tag rather than on
# every tag push. (The npm release in release.yml stays automatic — it runs on
# a Linux runner.) Immutable releases force this to be all-or-nothing anyway:
# every asset must exist before the one-shot publish, so the darwin legs could
# not be deferred independently. If a self-hosted macOS runner is ever
# registered, point the darwin legs at it and a tag-push trigger can return.
on:
push:
tags:
- "v*.*.*"
workflow_dispatch:
inputs:
tag:
Expand Down Expand Up @@ -121,7 +126,9 @@ jobs:
include:
- runner: macos-14
target: darwin-arm64
- runner: macos-13
# macos-13 was the last plain Intel label but is retired; the -intel
# variants are the supported Intel images.
- runner: macos-15-intel
target: darwin-x64
- runner: ubuntu-24.04
target: linux-x64
Expand Down Expand Up @@ -324,7 +331,7 @@ jobs:
# A convenience index only. Each tarball carries its own provenance and
# SBOM attestation, so `gh attestation verify` — not this file — is the
# integrity mechanism.
shasum -a 256 *.tar.gz *.cdx.json > SHA256SUMS
shasum -a 256 -- *.tar.gz *.cdx.json > SHA256SUMS
cat SHA256SUMS

- name: Attach assets to a draft, then publish
Expand Down Expand Up @@ -364,9 +371,106 @@ jobs:
gh release edit "$TAG" --draft=false
echo "Published $TAG; assets and tag are now immutable."

- name: Formula checksums
formula:
name: Update Homebrew formula
needs: publish
runs-on: ubuntu-24.04
# Writes a branch and opens a PR; requires "Allow GitHub Actions to create
# and approve pull requests" in the repository's Actions settings.
#
# Trust model: this job holds a contents-write token, so it runs only
# committed repository scripts — no dependency install, no freshly built
# code. The generator reads the checksum manifest of the now-immutable
# release and the npm registry tarball, both fetched over TLS from their
# canonical origins.
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# main, not the tag: the formula PR targets main, and the generator
# script that runs here is the reviewed one on main.
ref: main
# The push below authenticates with this credential. Acceptable here
# only because the job executes no third-party or built code.
persist-credentials: true

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ env.NODE_VERSION }}
check-latest: false

- name: Fetch the release checksum manifest
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ inputs.tag || github.ref_name }}
run: |
set -euo pipefail
# From the published release, not the build artifacts: the release is
# immutable, so this is the manifest users can verify against.
gh release download "$TAG" --pattern SHA256SUMS --dir "${RUNNER_TEMP}"

- name: Wait for the npm tarball and record its checksum
id: npm
env:
TAG: ${{ inputs.tag || github.ref_name }}
run: |
set -euo pipefail
version="${TAG#v}"
# release.yml publishes the npm package in a parallel workflow, so the
# tarball may lag this job by a few minutes. Fail after ~10 minutes:
# a formula whose fallback URL 404s must not be proposed.
for _ in $(seq 1 30); do
if curl -fsSL -o "${RUNNER_TEMP}/acpx.tgz" \
"https://registry.npmjs.org/acpx/-/acpx-${version}.tgz"; then
sha="$(sha256sum "${RUNNER_TEMP}/acpx.tgz" | cut -d' ' -f1)"
echo "sha256=${sha}" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "npm tarball for ${version} not available yet; retrying in 20s."
sleep 20
done
echo "::error::acpx@${version} never appeared on the npm registry."
exit 1

- name: Regenerate the formula
env:
TAG: ${{ inputs.tag || github.ref_name }}
NPM_SHA256: ${{ steps.npm.outputs.sha256 }}
run: |
set -euo pipefail
# Paste into Formula/acpx.rb. A stale checksum makes `brew install`
# fail loudly instead of installing a wrong artifact.
grep 'tar\.gz$' assets/SHA256SUMS
node scripts/sea/generate-formula.mjs \
--version "${TAG#v}" \
--npm-sha256 "$NPM_SHA256" \
--sums "${RUNNER_TEMP}/SHA256SUMS"

- name: Open a pull request
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag || github.ref_name }}
run: |
set -euo pipefail
if git diff --quiet -- Formula/acpx.rb; then
echo "Formula already matches $TAG; nothing to propose."
exit 0
fi
branch="bot/homebrew-formula-${TAG}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b "$branch"
git add Formula/acpx.rb
git commit -m "chore(brew): point the formula at $TAG"
# Plain --force: a fresh clone has no remote-tracking ref for the bot
# branch, so --force-with-lease would reject every re-run. The branch
# is namespaced to this job and carries generated content only.
git push --force origin "$branch"
# A PR rather than a direct push: the formula names the bytes every
# brew user installs, so it goes through the same review path as any
# other change to main.
if ! gh pr view "$branch" >/dev/null 2>&1; then
gh pr create \
--title "chore(brew): point the formula at $TAG" \
--body "$(printf 'Regenerated by the formula job of release-binaries.yml from the %s release assets and npm tarball.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)' "$TAG")"
fi
56 changes: 36 additions & 20 deletions Formula/acpx.rb
Original file line number Diff line number Diff line change
@@ -1,43 +1,59 @@
# Generated by scripts/sea/generate-formula.mjs — do not edit by hand.
# The `formula` job in .github/workflows/release-binaries.yml regenerates
# this file for every release and opens a PR with the result.
class Acpx < Formula
desc "Headless CLI client for the Agent Client Protocol (ACP)"
homepage "https://github.com/artagon/acpx"
version "0.12.0"
license "MIT"

# Self-contained Node single-executable application: the bundle and a V8
# startup snapshot are injected into a Node binary, so there is no runtime
# dependency on a system Node install and startup is ~50ms versus ~77ms for
# the npm package.
# Two install paths, resolved per platform:
#
# Assets are built by .github/workflows/release-binaries.yml (`pnpm run sea`
# per target) and attached to the tagged release. Homebrew's own node is
# compiled without single-executable support and cannot build them, which is
# why this formula ships prebuilt binaries rather than building from source.
# Platforms with a published asset get a self-contained Node
# single-executable: the bundle and a V8 startup snapshot injected into an
# official Node binary, with no runtime dependency on a system Node and
# ~50ms startup versus ~77ms for the npm package. The snapshot is
# architecture-specific, so each asset is built on a native runner by
# release-binaries.yml; Homebrew's own node has SEA support compiled out
# and cannot build them from source.
#
# Every asset carries build-provenance and SBOM attestations, and releases are
# immutable, so the sha256 below pins bytes that cannot be replaced upstream.
# See docs/verifying-releases.md.
# Every other platform installs the npm package below with Homebrew's
# node — same code, ordinary module resolution instead of a snapshot.
#
# Only the platforms with a published asset are listed. Adding a url/sha256
# pair for a platform whose asset does not exist turns a clear "unsupported"
# message into a download failure, so new platforms are added by the release
# workflow, not by hand.
# Binary assets carry build-provenance and SBOM attestations, and releases
# are immutable, so each sha256 pins bytes that cannot be replaced
# upstream. See docs/verifying-releases.md.
url "https://registry.npmjs.org/acpx/-/acpx-0.12.0.tgz"
version "0.12.0"
sha256 "1dd271ad09a39071b8305bdcdf6acddaa31c8f35ecf063e782dc9b5da8e193d7"
license "MIT"

on_macos do
on_arm do
url "https://github.com/artagon/acpx/releases/download/v0.12.0/acpx-0.12.0-darwin-arm64.tar.gz"
sha256 "823fea276f249b73c9305f0b36299f0af8f8936966208e5b52ef73f6f97e2c58"
end
Comment on lines 29 to 33
on_intel do
depends_on "node"
end
end

on_linux do
depends_on "node"
end

def install
bin.install "acpx"
if (buildpath/"acpx").exist?
bin.install "acpx"
else
system "npm", "install", *std_npm_args
bin.install_symlink Dir["#{libexec}/bin/*"]
end
end

test do
assert_match version.to_s, shell_output("#{bin}/acpx --version")

# The binary must answer without a system Node on PATH — that is the
# property that justifies shipping a ~122MB single executable.
# On binary platforms this must answer without a system Node on PATH —
# that is the property that justifies shipping a ~122MB executable.
assert_match "Usage", shell_output("#{bin}/acpx --help")
end
end
Loading
Loading