# Continuous build pipeline for Prime Agent (Rust). # # Builds the 4-target release matrix on every push to `main` and # uploads the tarballs as workflow artifacts (downloadable from the run page). # Does NOT create tags or releases — the org repo's release history stays # owned by the merged product's release workflow (release.yml). # No Rust toolchain needed on the consumer side: the kernel runtime sidecar # ships inside every tarball. # name: continuous on: push: branches: - main permissions: {} concurrency: group: continuous-main cancel-in-progress: false env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 jobs: # Superseded-push gate (TS #2567): with `cancel-in-progress: false`, every # push to main queues a full 4-target build, so a burst of merges # leaves older runs building artifacts for a commit that is no longer the # tip. The gate checks the live branch tip first: when the workflow commit # is superseded, the expensive build jobs skip (neutral exit) and the # newest push's run produces the artifacts. Fail-open: a failed tip lookup # builds as before. PR runs and tag-driven releases (release.yml) are # unaffected — this workflow only fires on branch pushes. staleness: # After `deny`: the tip lookup runs as late in the DAG as the builds # allow, so a push that lands while `deny` runs is still caught (the # check-then-build window shrinks to the build's own startup). needs: deny name: Superseded push check runs-on: ubuntu-latest permissions: contents: read outputs: superseded: ${{ steps.tip.outputs.superseded }} steps: - name: Compare the workflow commit with the live branch tip id: tip run: | set -eu # Git-over-HTTPS wants Basic (x-access-token), not Bearer: a Bearer # header is answered with "invalid credentials" EVEN on a public # repo (an offered-but-bad credential 401s instead of serving # anonymously), which would swallow every lookup and no-op the # gate. The Basic form is verified working; empty tip builds as # before (fail-open). basic="$(printf 'x-access-token:%s' "${{ github.token }}" | base64 | tr -d '\n')" tip="$(git -c "http.extraheader=AUTHORIZATION: basic ${basic}" ls-remote "https://github.com/${GITHUB_REPOSITORY}.git" "refs/heads/${GITHUB_REF_NAME}" | cut -f1 || true)" if [ -n "${tip}" ] && [ "${tip}" != "${GITHUB_SHA}" ]; then echo "superseded=true" >> "${GITHUB_OUTPUT}" echo "superseded: ${GITHUB_SHA} is no longer the tip of ${GITHUB_REF_NAME} (tip ${tip}); the build jobs skip" else echo "superseded=false" >> "${GITHUB_OUTPUT}" fi # The dependency audit gate (RELEASE_SECURITY pragmatic baseline): the # same advisories + licenses check as `make deny` (deny.toml), on every # tree the branch channel builds binaries from. Advisories that are # triaged-unmaintained carry an `ignore` entry with the reason and a # review date (docs/installer-ci-design.md §7); the gate fails on # anything new. SEAM: pin the advisory-DB snapshot (or vendor it) when # the channel goes public. deny: runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Dependency audit (cargo-deny advisories + licenses) uses: ./.github/actions/dependency-audit # The audit gates every build: an unaudited tree must not produce # downloadable branch-channel binaries. build-gnu: needs: [deny, staleness] if: needs.staleness.outputs.superseded != 'true' name: build-${{ matrix.target }} runs-on: ${{ matrix.runs-on }} # GLIBC 2.35 build baseline: artifacts built on the ubuntu-24.04 runner # image required GLIBC_2.39 and did not start on Ubuntu 22.04 (verified # 2026-09-24 on a deploy box). The host runner only supplies CPU; this # container pins the build glibc. The image is multi-arch, so aarch64 # stays a native build on the arm runner. container: # Pinned by digest (review finding: the mutable tag could drift the # release baseline under promote). Pin the multi-arch index so both # matrix arches resolve from one reviewed image; update the digest # only through reviewed changes. image: ubuntu:22.04@sha256:b8b6ee6aa931ecd9d0d952abc34dc0e5f7c6a30c6bb71b079fe399fde0329c02 # Container jobs run `run:` steps with the image's /bin/sh (dash on # ubuntu:22.04), which rejects `set -o pipefail`; pin the steps to bash. defaults: run: shell: bash permissions: contents: read timeout-minutes: 90 strategy: fail-fast: false matrix: include: - target: x86_64-unknown-linux-gnu runs-on: ubuntu-24.04 - target: aarch64-unknown-linux-gnu runs-on: ubuntu-24.04-arm env: # AppleDouble resource forks must not leak into the archive; the # assembler additionally enforces plain-file payloads. COPYFILE_DISABLE: "1" steps: # The base image is minimal: the checkout/toolchain/cache actions and # the build need these (git before checkout so clones do not fall # back to the REST-API download path). - name: Install build prerequisites (Ubuntu 22.04 container) run: | set -euo pipefail apt-get update apt-get install -y --no-install-recommends \ build-essential curl ca-certificates python3 git binutils - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false # The pinned SHA is the toolchain pin: this action version has no # `toolchain` input, and its parse step hard-codes 1.98.1. Change the # compiler by changing the SHA; a `toolchain:` input is ignored. - uses: dtolnay/rust-toolchain@ce678459e9fc7500d337468f904b95f1b5c10b5e with: targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: # The glibc235 marker scopes the cache to the Ubuntu 22.04 build # baseline: cargo fingerprints do not encode the host libc, so a # key without it could restore a target/ tree linked on the old # ubuntu-24.04 host and reuse a GLIBC_2.39 binary. First run on # the new key builds cold; the marker moves only with reviewed # baseline changes (same protocol as the image digest below). key: ${{ matrix.target }}-glibc235 # Only the shipped binary links here: the job packages `prime-agent` # alone, so pa-daemon's [[bin]] would be a second thin-LTO link with # no consumer (~26% of the build step, measured). - name: Build (release, locked) run: cargo build --release --locked --target ${{ matrix.target }} -j 4 -p pa-cli --bin prime-agent - name: Split the shipped binary + split-debug decoder (Option C posture) run: | set -euo pipefail version="$(sed -n 's/^version *= *"\([^"]*\)".*/\1/p' Cargo.toml | head -1)" [[ -n "${version}" ]] || { echo "could not read the workspace version"; exit 1; } python3 scripts/release/split_debug.py \ --binary "target/${{ matrix.target }}/release/prime-agent" \ --shipped "target/${{ matrix.target }}/dist/prime-agent" \ --out "target/${{ matrix.target }}/dist" \ --version "${version}" \ --target "${{ matrix.target }}" - name: "GLIBC baseline gate: no symbols above GLIBC_2.35 (Ubuntu 22.04)" run: | set -euo pipefail bin="target/${{ matrix.target }}/dist/prime-agent" syms="$(objdump -T "${bin}" | grep -o 'GLIBC_[0-9.]*' || true)" if [ -z "${syms}" ]; then echo "::error::could not inspect ${bin} with objdump (no GLIBC symbols found) - refusing to pass the gate without evidence" exit 1 fi max_glibc="$(printf '%s\n' "${syms}" | sort -Vu | tail -1)" echo "highest GLIBC symbol required: ${max_glibc}" top="$(printf '%s\nGLIBC_2.35\n' "${max_glibc}" | sort -Vu | tail -1)" if [ "${top}" != "GLIBC_2.35" ]; then echo "::error::binary requires ${max_glibc}, above the GLIBC_2.35 (Ubuntu 22.04) release baseline" exit 1 fi - name: Generate bundled catalog assets (fixture, offline-safe) run: | set -euo pipefail python3 scripts/release/bundle_catalog.py generate --fixture \ --out "target/${{ matrix.target }}/catalog-assets" - name: Assemble tarball + checksums + manifest (commit-stamped) run: | set -euo pipefail version="$(sed -n 's/^version *= *"\([^"]*\)".*/\1/p' Cargo.toml | head -1)" [[ -n "${version}" ]] || { echo "could not read the workspace version"; exit 1; } python3 scripts/release/assemble_artifacts.py \ --repo-root . \ --version "${version}" \ --target "${{ matrix.target }}" \ --sha "${GITHUB_SHA}" \ --binary "target/${{ matrix.target }}/dist/prime-agent" \ --decoder "target/${{ matrix.target }}/dist/prime-agent-${version}-$(case "${{ matrix.target }}" in x86_64*) echo linux-x64;; aarch64*) echo linux-arm64;; esac).debug.gz" \ --catalog-assets "target/${{ matrix.target }}/catalog-assets" \ --out-dir "target/${{ matrix.target }}/dist" cat "target/${{ matrix.target }}/dist/manifest.json" - name: Livecheck the assembled continuous artifact on its native runner run: | set -euo pipefail version="$(sed -n 's/^version *= *"\([^"]*\)".*/\1/p' Cargo.toml | head -1)" [[ -n "${version}" ]] || { echo "could not read the workspace version"; exit 1; } python3 scripts/release/verify_release.py \ --dist-dir "target/${{ matrix.target }}/dist" \ --version "${version}" \ --target "${{ matrix.target }}" \ --sha "${GITHUB_SHA}" - name: Upload artifacts uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: artifacts-${{ matrix.target }} path: | target/${{ matrix.target }}/dist/prime-agent-* target/${{ matrix.target }}/dist/SHA256SUMS target/${{ matrix.target }}/dist/manifest.json if-no-files-found: error build-darwin: needs: [deny, staleness] if: needs.staleness.outputs.superseded != 'true' name: build-${{ matrix.target }} runs-on: ${{ matrix.runs-on }} permissions: contents: read timeout-minutes: 90 strategy: fail-fast: false matrix: include: - target: aarch64-apple-darwin runs-on: macos-14 # The x86_64 leg cross-compiles on the Apple-silicon pool: rustc # and the Xcode linker build either darwin arch from an M-series # host, and the M-pool links this binary ~2.2x faster than the # macos-15-intel pool with far tighter variance (484 s vs 1046 s # same-commit link tail, 2026-09-30). The livecheck below runs # the assembled x64 artifact under Rosetta 2 — the macos-15 # arm64 image installs it, and install-rust.sh ships the x64 # build to an Apple-Silicon Mac's Rosetta shell, so the gate # still boots the binary on its delivery runtime. - target: x86_64-apple-darwin runs-on: macos-15 env: # AppleDouble resource forks must not leak into the archive; the # assembler additionally enforces plain-file payloads. COPYFILE_DISABLE: "1" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false # The pinned SHA is the toolchain pin: this action version has no # `toolchain` input, and its parse step hard-codes 1.98.1. Change the # compiler by changing the SHA; a `toolchain:` input is ignored. - uses: dtolnay/rust-toolchain@ce678459e9fc7500d337468f904b95f1b5c10b5e with: targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: key: ${{ matrix.target }} # Only the shipped binary links here: the job packages `prime-agent` # alone, so pa-daemon's [[bin]] would be a second thin-LTO link with # no consumer (~26% of the build step, measured). - name: Build (release, locked) run: cargo build --release --locked --target ${{ matrix.target }} -j 4 -p pa-cli --bin prime-agent - name: Generate bundled catalog assets (fixture, offline-safe) run: | set -euo pipefail python3 scripts/release/bundle_catalog.py generate --fixture \ --out "target/${{ matrix.target }}/catalog-assets" - name: Assemble tarball + checksums + manifest (commit-stamped) run: | set -euo pipefail version="$(sed -n 's/^version *= *"\([^"]*\)".*/\1/p' Cargo.toml | head -1)" [[ -n "${version}" ]] || { echo "could not read the workspace version"; exit 1; } python3 scripts/release/assemble_artifacts.py \ --repo-root . \ --version "${version}" \ --target "${{ matrix.target }}" \ --sha "${GITHUB_SHA}" \ --binary "target/${{ matrix.target }}/release/prime-agent" \ --catalog-assets "target/${{ matrix.target }}/catalog-assets" \ --out-dir "target/${{ matrix.target }}/dist" cat "target/${{ matrix.target }}/dist/manifest.json" # The livecheck extracts the tarball and runs the payload binary's # `--version` from a scratch cwd (verify_release.py). The aarch64 leg # executes it natively; the x86_64 leg, cross-compiled on the # Apple-silicon pool, executes it under Rosetta 2 — the runtime the # x64 payload runs on for an Apple-Silicon Mac customer, so the gate # still boots the artifact on its delivery runtime. - name: Livecheck the assembled continuous artifact on its build runner run: | set -euo pipefail version="$(sed -n 's/^version *= *"\([^"]*\)".*/\1/p' Cargo.toml | head -1)" [[ -n "${version}" ]] || { echo "could not read the workspace version"; exit 1; } python3 scripts/release/verify_release.py \ --dist-dir "target/${{ matrix.target }}/dist" \ --version "${version}" \ --target "${{ matrix.target }}" \ --sha "${GITHUB_SHA}" - name: Upload artifacts uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: artifacts-${{ matrix.target }} path: | target/${{ matrix.target }}/dist/prime-agent-* target/${{ matrix.target }}/dist/SHA256SUMS target/${{ matrix.target }}/dist/manifest.json if-no-files-found: error