Build Binaries #113
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: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 | |
| 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.3.14' | |
| - name: Report Bun compiler version | |
| run: bun --version | |
| - name: Setup Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e | |
| 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: release-assets-${{ env.RELEASE_TAG }} | |
| path: release-assets/* | |
| if-no-files-found: error | |
| retention-days: 14 | |
| stage-github-release: | |
| runs-on: ubuntu-latest | |
| needs: build | |
| 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@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 | |
| 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 |