Skip to content

Build Binaries

Build Binaries #186

Workflow file for this run

name: Build Binaries
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to build (e.g., v0.12.0)'
required: true
type: string
source_ref:
description: 'Source ref to build/publish (defaults to tag; use only for release recovery)'
required: false
type: string
dry_run:
description: 'Build and validate only: skip the GitHub Release upload and the npm publish dispatch'
required: false
type: boolean
default: false
permissions: {}
concurrency:
group: build-binaries-${{ github.event.inputs.tag || github.ref_name }}
cancel-in-progress: false
jobs:
# Keep the public GitHub Release publication last. Binary assets are staged in
# a draft release first; cleanup removes the draft if later publishing fails.
build:
runs-on: ubuntu-latest
permissions:
contents: read
env:
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
SOURCE_REF: ${{ github.event.inputs.source_ref || github.event.inputs.tag || github.ref_name }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.SOURCE_REF }}
persist-credentials: false
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
# Cross-compilation downloads target executables matching the running Bun
# release. Canary can advance before those target artifacts are published.
bun-version: '1.4.2'
- name: Report Bun compiler version
run: bun --version
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
package-manager-cache: false
- name: Build binaries
run: ./scripts/build-binaries.sh
- name: Prepare GitHub release payload
run: |
set -euo pipefail
mkdir -p release-assets
VERSION="${RELEASE_TAG}"
VERSION="${VERSION#v}" # Remove 'v' prefix
node scripts/release-notes.mjs extract --version "${VERSION}" --tag "${RELEASE_TAG}" --out release-assets/RELEASE_NOTES.md
node scripts/generate-coding-agent-install-lock.mjs --check
cp packages/coding-agent/install-lock/package.json release-assets/pi-coding-agent-install-package.json
cp packages/coding-agent/install-lock/package-lock.json release-assets/pi-coding-agent-install-package-lock.json
cd packages/coding-agent/binaries
binary_assets=(
pi-darwin-arm64.tar.gz
pi-darwin-x64.tar.gz
pi-linux-x64.tar.gz
pi-linux-arm64.tar.gz
pi-windows-x64.zip
pi-windows-arm64.zip
)
for asset in "${binary_assets[@]}"; do
test -f "${asset}"
done
cp "${binary_assets[@]}" "${GITHUB_WORKSPACE}/release-assets/"
cd "${GITHUB_WORKSPACE}/release-assets"
release_assets=(
pi-darwin-arm64.tar.gz
pi-darwin-x64.tar.gz
pi-linux-x64.tar.gz
pi-linux-arm64.tar.gz
pi-windows-x64.zip
pi-windows-arm64.zip
pi-coding-agent-install-package.json
pi-coding-agent-install-package-lock.json
)
sha256sum "${release_assets[@]}" > SHA256SUMS
- name: Upload GitHub release payload
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-assets-${{ env.RELEASE_TAG }}
path: release-assets/*
if-no-files-found: error
retention-days: 14
smoke-test-binaries:
runs-on: ${{ matrix.runner }}
needs: build
strategy:
fail-fast: false
matrix:
runner:
- ubuntu-latest
- macos-latest
- windows-latest
permissions:
actions: read
env:
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
steps:
- name: Download binary archives
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-assets-${{ env.RELEASE_TAG }}
path: release-assets
- name: Extract current-platform binary
id: binary
shell: bash
run: |
set -euo pipefail
runtime_platform="$(node -p '`${process.platform}-${process.arch}`')"
case "${runtime_platform}" in
darwin-arm64|darwin-x64|linux-arm64|linux-x64)
platform="${runtime_platform}"
archive="release-assets/pi-${platform}.tar.gz"
root="extracted/pi"
binary="${root}/pi"
;;
win32-arm64)
platform="windows-arm64"
archive="release-assets/pi-${platform}.zip"
root="extracted"
binary="${root}/pi.exe"
;;
win32-x64)
platform="windows-x64"
archive="release-assets/pi-${platform}.zip"
root="extracted"
binary="${root}/pi.exe"
;;
*)
echo "::error::Unsupported runner platform ${runtime_platform}"
exit 1
;;
esac
mkdir -p extracted
if [[ "${archive}" == *.zip ]]; then
export ARCHIVE_PATH="$(cygpath -w "${archive}")"
export DESTINATION_PATH="$(cygpath -w extracted)"
powershell.exe -NoProfile -NonInteractive -Command \
'Expand-Archive -LiteralPath $env:ARCHIVE_PATH -DestinationPath $env:DESTINATION_PATH -Force'
else
tar -xf "${archive}" -C extracted
fi
echo "root=${root}" >> "${GITHUB_OUTPUT}"
echo "binary=${binary}" >> "${GITHUB_OUTPUT}"
- name: Smoke-test binary
shell: bash
run: |
"${{ steps.binary.outputs.binary }}" --help
"${{ steps.binary.outputs.binary }}" --version
stage-github-release:
runs-on: ubuntu-latest
needs:
- build
- smoke-test-binaries
if: ${{ inputs.dry_run != true }}
permissions:
actions: read
contents: write
env:
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
steps:
- name: Download GitHub release payload
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-assets-${{ env.RELEASE_TAG }}
path: release-assets
- name: Validate GitHub release payload
run: |
set -euo pipefail
cd release-assets
expected_assets=(
pi-darwin-arm64.tar.gz
pi-darwin-x64.tar.gz
pi-linux-x64.tar.gz
pi-linux-arm64.tar.gz
pi-windows-x64.zip
pi-windows-arm64.zip
pi-coding-agent-install-package.json
pi-coding-agent-install-package-lock.json
SHA256SUMS
RELEASE_NOTES.md
)
for asset in "${expected_assets[@]}"; do
test -f "${asset}"
done
sha256sum -c SHA256SUMS
- name: Create draft GitHub Release and upload binaries
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
cd release-assets
release_assets=(
pi-darwin-arm64.tar.gz
pi-darwin-x64.tar.gz
pi-linux-x64.tar.gz
pi-linux-arm64.tar.gz
pi-windows-x64.zip
pi-windows-arm64.zip
pi-coding-agent-install-package.json
pi-coding-agent-install-package-lock.json
SHA256SUMS
)
existing_release="$(gh release view "${RELEASE_TAG}" --json isDraft --jq .isDraft 2>/dev/null || true)"
if [[ "${existing_release}" == "false" ]]; then
echo "::error::GitHub Release ${RELEASE_TAG} is already published. Refusing to mutate a public release."
exit 1
fi
if [[ "${existing_release}" == "true" ]]; then
gh release delete "${RELEASE_TAG}" --yes
fi
gh release create "${RELEASE_TAG}" \
--verify-tag \
--draft \
--title "${RELEASE_TAG}" \
--notes-file RELEASE_NOTES.md \
"${release_assets[@]}"
expected_asset_names="$(printf '%s\n' "${release_assets[@]}" | sort)"
actual_asset_names="$(gh release view "${RELEASE_TAG}" --json assets --jq '.assets[].name' | sort)"
if [[ "${actual_asset_names}" != "${expected_asset_names}" ]]; then
echo "::error::Draft GitHub Release asset set does not match expected files."
diff -u <(printf '%s\n' "${expected_asset_names}") <(printf '%s\n' "${actual_asset_names}") || true
exit 1
fi
# npm trusted publishing is bound to the publish-npm.yml workflow identity
# (workflow_ref), so this workflow must never publish directly. Dispatch
# publish-npm.yml in publish-only mode — the same mechanism used for manual
# release recovery — and gate the public GitHub Release on its result.
publish-npm:
runs-on: ubuntu-latest
needs: stage-github-release
if: ${{ inputs.dry_run != true }}
permissions:
actions: write
env:
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
steps:
- name: Dispatch publish-npm.yml
id: dispatch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
VERSION="${RELEASE_TAG#v}"
echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"
# One minute of lookback absorbs runner/GitHub clock skew when the
# await step matches the dispatched run by creation time.
echo "not_before=$(date -u -d '1 minute ago' +%Y-%m-%dT%H:%M:%SZ)" >> "${GITHUB_OUTPUT}"
gh workflow run publish-npm.yml -f version="${VERSION}" -f publish-only=true
- name: Await publish-npm.yml completion
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NOT_BEFORE: ${{ steps.dispatch.outputs.not_before }}
run: |
set -euo pipefail
# The dispatched run can take a few seconds to appear in the API.
run_id=""
for _ in $(seq 1 12); do
run_id="$(gh run list --workflow publish-npm.yml --event workflow_dispatch --limit 20 \
--json databaseId,createdAt \
--jq "[.[] | select(.createdAt >= \"${NOT_BEFORE}\")] | .[0].databaseId // empty")"
if [[ -n "${run_id}" ]]; then
break
fi
sleep 5
done
if [[ -z "${run_id}" ]]; then
echo "::error::Could not find the dispatched publish-npm.yml run."
exit 1
fi
echo "Watching publish-npm.yml run ${run_id}: https://github.com/${GH_REPO}/actions/runs/${run_id}"
if ! timeout 3600 gh run watch "${run_id}" --exit-status --interval 20; then
echo "::error::publish-npm.yml run ${run_id} failed or exceeded the 60 minute wait budget."
exit 1
fi
publish-github-release:
runs-on: ubuntu-latest
needs:
- stage-github-release
- publish-npm
permissions:
contents: write
env:
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
steps:
- name: Publish staged GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
existing_release="$(gh release view "${RELEASE_TAG}" --json isDraft --jq .isDraft 2>/dev/null || true)"
if [[ "${existing_release}" == "" ]]; then
echo "::error::Draft GitHub Release ${RELEASE_TAG} does not exist."
exit 1
fi
if [[ "${existing_release}" == "false" ]]; then
echo "::error::GitHub Release ${RELEASE_TAG} is already published."
exit 1
fi
gh release edit "${RELEASE_TAG}" --draft=false
cleanup-draft-github-release:
runs-on: ubuntu-latest
needs:
- build
- stage-github-release
- publish-npm
- publish-github-release
if: ${{ always() && needs.stage-github-release.result != 'skipped' && (needs.stage-github-release.result != 'success' || needs.publish-npm.result != 'success' || needs.publish-github-release.result != 'success') }}
permissions:
contents: write
env:
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
steps:
- name: Delete draft GitHub Release after failure
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
existing_release="$(gh release view "${RELEASE_TAG}" --json isDraft --jq .isDraft 2>/dev/null || true)"
if [[ "${existing_release}" == "true" ]]; then
gh release delete "${RELEASE_TAG}" --yes
fi