Repository navigation
perf(coding-agent): memoize session entry lines so prunes rewrite only what changed #39872
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: CI | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - "packages/**" | |
| - "crates/**" | |
| - "scripts/**" | |
| - "bazel/**" | |
| - "MODULE.bazel" | |
| - "MODULE.bazel.lock" | |
| - "BUILD.bazel" | |
| - ".bazelrc" | |
| - ".bazelignore" | |
| - ".bazelversion" | |
| - "Cargo.toml" | |
| - "Cargo.lock" | |
| - "deny.toml" | |
| - "about.toml" | |
| - "LICENSE" | |
| - "THIRD-PARTY-NOTICES.txt" | |
| - "rust-toolchain.toml" | |
| - "rustfmt.toml" | |
| # A root dependency change (lockfile/catalog/patches) affects every | |
| # job; without these a lockfile-only push shipped untested — and a | |
| # release retagged onto such a commit never triggered its run. | |
| - "bun.lock" | |
| - "bunfig.toml" | |
| - "package.json" | |
| - "patches/**" | |
| - ".github/**" | |
| pull_request: | |
| branches: [main, omp2] | |
| paths: | |
| - "packages/**" | |
| - "crates/**" | |
| - "scripts/**" | |
| - "bazel/**" | |
| - "MODULE.bazel" | |
| - "MODULE.bazel.lock" | |
| - "BUILD.bazel" | |
| - ".bazelrc" | |
| - ".bazelignore" | |
| - ".bazelversion" | |
| - "Cargo.toml" | |
| - "Cargo.lock" | |
| - "deny.toml" | |
| - "about.toml" | |
| - "LICENSE" | |
| - "THIRD-PARTY-NOTICES.txt" | |
| - "rust-toolchain.toml" | |
| - "rustfmt.toml" | |
| - "bun.lock" | |
| - "bunfig.toml" | |
| - "package.json" | |
| - "patches/**" | |
| - ".github/**" | |
| workflow_dispatch: | |
| inputs: | |
| skip_npm: | |
| description: "Skip npm publish" | |
| type: boolean | |
| default: false | |
| # Release runs publish a `v*` tag pushed atomically with main HEAD; sharing | |
| # the cheap branch-wide `CI-refs/heads/main` group meant a later main push | |
| # silently cancelled the in-flight release and left the tag without a GitHub | |
| # Release or npm publish (#2564). Detect release runs at workflow-scheduling | |
| # time via the release-script commit subject (`chore: bump version to vX.Y.Z`), | |
| # via `v*` tag-ref dispatches, and via manual dispatches whose tag-on-HEAD | |
| # status is only known after checkout; scope them to a per-sha group with no | |
| # cancellation. Every other event keeps branch-wide cancellation for PR/main churn. | |
| concurrency: | |
| group: "${{ github.workflow }}-${{ (startsWith(github.event.head_commit.message, 'chore: bump version to ') || startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && format('release-{0}', github.sha) || github.ref }}" | |
| cancel-in-progress: "${{ !(startsWith(github.event.head_commit.message, 'chore: bump version to ') || startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') }}" | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| # native_addons builds the pair TS jobs need; native_addons_cross builds the | |
| # rest. Together they must equal //:natives-all (checked in the cross job), | |
| # so a filegroup addition cannot silently skip both. | |
| NATIVES_X64_TARGETS: natives-linux-x64-baseline natives-linux-x64-modern | |
| NATIVES_CROSS_TARGETS: natives-darwin-arm64 natives-darwin-x64-baseline natives-linux-arm64 natives-linux-musl-arm64 natives-linux-musl-x64-baseline natives-win32-arm64 natives-win32-x64-baseline | |
| # Per-test wall-clock ceiling (seconds) for scripts/ci-test-ts.ts. omp-kata | |
| # runners with reclaim_guest_freed_memory (infra/tune-kata-runtime.sh) run | |
| # the SQLite/subprocess-heavy coding-agent suites 1.5-2x slower, which put | |
| # several cases (model-registry refresh, session fork, credential bridge) | |
| # over the 30 s default; 120 s still catches a real hang. | |
| OMP_TEST_TIMEOUT: "120" | |
| permissions: | |
| contents: read | |
| jobs: | |
| # scripts/release.ts pushes the version-bump commit and its `v*` tag | |
| # atomically (`git push --atomic origin refs/heads/main:refs/heads/main | |
| # <sha>:refs/tags/v<version>`), so a release now arrives as a single `push` to | |
| # `refs/heads/main` — we no longer trigger on the tag ref at all (see | |
| # `on.push`). This one branch-push run is therefore authoritative: it runs the | |
| # full build AND, when HEAD carries a release tag, the release/publish jobs. | |
| # `release_metadata` resolves that tag once so downstream jobs switch on | |
| # `is-release` and address the tag by name — `github.ref` is | |
| # `refs/heads/main` here, not the tag. A `workflow_dispatch` from a `v*` tag | |
| # ref (or from a tagged main HEAD) is also treated as a release. | |
| release_metadata: | |
| name: Resolve release metadata | |
| # Hosted, not omp-kata: this ~20 s git-tag probe gates the whole release | |
| # chain, and queueing it behind the 4-runner kata pool adds dead minutes | |
| # to the release critical path. | |
| runs-on: ubuntu-22.04 | |
| outputs: | |
| is-release: ${{ steps.detect.outputs.is-release }} | |
| release-tag: ${{ steps.detect.outputs.release-tag }} | |
| # Canary tags publish to npm's canary dist-tag (derived script-side), mark | |
| # the GitHub release prerelease, and skip the stable-only brew tap. | |
| channel: ${{ steps.detect.outputs.channel }} | |
| steps: | |
| # Only a main-branch run needs tags fetched, so `git tag --points-at | |
| # HEAD` can see the freshly-pushed `v*`. A tag-ref dispatch reads the | |
| # tag straight from `github.ref_name`, and fetching `--tags` while | |
| # checkout uses an explicit tag refspec makes git refuse — so scope | |
| # fetch-tags to main refs. | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-tags: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Detect release tag at HEAD | |
| id: detect | |
| shell: bash | |
| run: | | |
| is_release=false | |
| release_tag="" | |
| channel=stable | |
| case "${{ github.ref }}" in | |
| refs/tags/v[0-9]*) | |
| release_tag="${{ github.ref_name }}" | |
| ;; | |
| refs/heads/main) | |
| if [ "${{ github.event_name }}" != "pull_request" ]; then | |
| release_tag=$(git tag --points-at HEAD | grep -E '^v[0-9]' | head -n1 || true) | |
| fi | |
| ;; | |
| esac | |
| if [ -n "$release_tag" ]; then | |
| echo "HEAD carries release tag $release_tag; this run builds and publishes the release." | |
| is_release=true | |
| fi | |
| if [[ "$release_tag" == *-canary.* ]]; then | |
| channel=canary | |
| fi | |
| { | |
| echo "is-release=$is_release" | |
| echo "release-tag=$release_tag" | |
| echo "channel=$channel" | |
| } >> "$GITHUB_OUTPUT" | |
| check: | |
| name: Lint, type check & web build | |
| runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/bun-install | |
| - name: Verify Rust license compliance (cargo-deny 0.20.2) | |
| run: | | |
| # Prebuilt release binary checked against digests pinned here: | |
| # `cargo install` compiled cargo-deny from source on every run | |
| # (~135 s), and a .sha256 fetched from the same mutable release | |
| # would prove transport integrity only. Bump ver and both | |
| # digests together. | |
| ver=0.20.2 | |
| case "$(uname -m)" in | |
| x86_64) arch=x86_64 sha=9f12ed4c49936e09b48bf862b595cde2fe64fcbd9d74dfacac6131ca824c8d5f ;; | |
| aarch64|arm64) arch=aarch64 sha=995c82be0defc7a025cae49a2aa2644ce8245c9a3318fc4103907c6a285e8c7d ;; | |
| *) echo "::error::unsupported arch $(uname -m)"; exit 1 ;; | |
| esac | |
| name="cargo-deny-$ver-$arch-unknown-linux-musl" | |
| curl -fsSL --retry 3 -o "$RUNNER_TEMP/$name.tar.gz" "https://github.com/EmbarkStudios/cargo-deny/releases/download/$ver/$name.tar.gz" | |
| echo "$sha $RUNNER_TEMP/$name.tar.gz" | sha256sum -c - | |
| tar -xzf "$RUNNER_TEMP/$name.tar.gz" -C "$RUNNER_TEMP" | |
| mkdir -p "$HOME/.cargo/bin" | |
| install -m 0755 "$RUNNER_TEMP/$name/cargo-deny" "$HOME/.cargo/bin/cargo-deny" | |
| # `--offline` needs every workspace crate already in the registry | |
| # cache. PR runs use a fresh GitHub-hosted runner (see runs-on | |
| # above) with no prior cargo work, so nothing populates the index | |
| # for our own graph and resolution fails. Non-PR events land on | |
| # omp-kata, whose Cargo cache is warm. Fetch first. | |
| cargo fetch --locked | |
| cargo deny --locked --offline check licenses sources | |
| - name: Type check workspace | |
| run: bun run ci:check:full | |
| - name: Build collab web | |
| run: bun run collab:web:build | |
| # MODULE.bazel.lock caches the crate_universe (@crates) extension result, | |
| # keyed by Cargo.toml/Cargo.lock/member-manifest hashes. While current, a | |
| # fresh bazel server skips the extension entirely; once stale, every bazel | |
| # job here re-runs `cargo-bazel splice` (~240 s each, CI run 36255186857). | |
| # Stale is slow, never wrong, so this job only makes drift visible — PRs | |
| # that touch Cargo files get told to run `bun run gen:bazel-lock`. The | |
| # check compares hashes only (no splice); on failure the job evaluates the | |
| # extension once and uploads the refreshed lockfile, so a contributor | |
| # without bazelisk can commit it as-is. | |
| bazel_lock: | |
| name: Check MODULE.bazel.lock freshness | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| - name: Check crate_universe entry | |
| run: bun scripts/gen-bazel-lock.ts --check | |
| - name: Refresh MODULE.bazel.lock | |
| if: failure() | |
| run: bun scripts/gen-bazel-lock.ts | |
| - name: Upload refreshed MODULE.bazel.lock | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: MODULE.bazel.lock | |
| path: MODULE.bazel.lock | |
| # Rust validation (tests, clippy, rustfmt) runs on non-PR events only. | |
| # Native-changing PRs are rare enough that they do not warrant PR-side | |
| # Rust validation or a PR-side addon build (see native_addons): Rust is | |
| # validated post-merge on main (kata remote cache, warm) and again at | |
| # release. A skipped required check still satisfies branch protection. | |
| rust_validate: | |
| name: Validate Rust workspace (bazel) | |
| if: github.event_name != 'pull_request' | |
| runs-on: omp-kata | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/bun-install | |
| - id: cache | |
| uses: ./.github/actions/bazel-cache | |
| - name: Rust tests | |
| # The ulimit guard runs in the step that launches the bazel server | |
| # (limits are per-process and the server persists across steps). | |
| run: | | |
| if [ "$(ulimit -Sn)" != unlimited ] && [ "$(ulimit -Sn)" -lt 65536 ]; then ulimit -Sn 65536 || true; fi | |
| bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" test //crates/... | |
| # Clippy scope mirrors `cargo clippy --workspace` (libraries only, no | |
| # test targets) plus the strict/default split: crates with | |
| # `[lints] workspace = true` get the workspace policy, except | |
| # brush-core (a vendored fork excluded from the Cargo task too). | |
| # pi-builtins allows every clippy group in its own manifest (ported | |
| # brush/uutils/jaq code) but is still held to zero rustc warnings; | |
| # Cargo honors that via `[lints]`, Bazel via the clippy-ported config. | |
| - name: Clippy (workspace lint policy on opted-in crates) | |
| run: | | |
| bazelisk query "kind('rust_library|rust_shared_library', //crates/pi-ast/... + //crates/pi-diff/... + //crates/pi-edit/... + //crates/pi-iso/... + //crates/pi-natives/... + //crates/pi-shell/... + //crates/pi-vcs/... + //crates/pi-voice/... + //crates/pi-walker/...)" \ | |
| | xargs bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" build --config=clippy-strict -- | |
| - name: Clippy (default lints elsewhere) | |
| run: | | |
| bazelisk query "kind('rust_library|rust_shared_library', //crates/... - (//crates/pi-ast/... + //crates/pi-diff/... + //crates/pi-edit/... + //crates/pi-iso/... + //crates/pi-natives/... + //crates/pi-shell/... + //crates/pi-vcs/... + //crates/pi-voice/... + //crates/pi-walker/...) - //crates/pi-builtins/... - //crates/vendor/brush-core/...)" \ | |
| | xargs bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" build --config=clippy -- | |
| - name: Clippy (manifest allows on ported pi-builtins) | |
| run: | | |
| bazelisk query "kind('rust_library|rust_shared_library', //crates/pi-builtins/...)" \ | |
| | xargs bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" build --config=clippy-ported -- | |
| - name: Rustfmt | |
| run: bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" build --config=rustfmt //crates/... | |
| # Provides the linux-x64 addon pair every downstream TS job installs. | |
| # | |
| # PRs never build. They restore the pair main built for the same native | |
| # sources from actions/cache (key = hash of every native build input: | |
| # toolchain and build settings plus crates/** and BUILD.bazel). With no | |
| # exact entry (the PR changes native inputs, or main has not built this | |
| # state yet) they take main's newest entry, and with no entry at all the | |
| # latest npm release. The workspace loader skips its version sentinel for | |
| # workspace loads, so any of these load under a newer checkout. Testing | |
| # against main's build instead of the last release keeps merged-but- | |
| # unreleased native fixes from failing every PR that did not touch Rust | |
| # (a merged grep fix plus its TS test failed 75 PR runs against the | |
| # 18.3.1 addon). Only main saves; see the save step for why. | |
| # | |
| # Main (kata, cluster remote cache) builds only the pair here so the TS | |
| # fan-out starts as soon as it links; native_addons_cross builds every | |
| # other target in parallel. Job name says "bazel" for continuity | |
| # with required checks. | |
| native_addons: | |
| name: Build native addons (bazel) | |
| runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }} | |
| # actions: write lets main prune superseded addon cache entries. | |
| permissions: | |
| contents: read | |
| actions: write | |
| outputs: | |
| # "<artifact-relative path>=<sha256>" pairs; kata consumers restore | |
| # these from the in-cluster stash (see native-artifacts). | |
| stash: ${{ steps.stage.outputs.stash }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Compute native addon cache key | |
| id: natives-key | |
| shell: bash | |
| env: | |
| # Must stay identical on every event: PRs look up the entry main | |
| # saved for the same inputs. | |
| NATIVE_INPUTS_HASH: ${{ hashFiles('Cargo.toml', 'Cargo.lock', 'MODULE.bazel', 'MODULE.bazel.lock', 'rust-toolchain.toml', 'rustfmt.toml', '.bazelrc', '.bazelignore', '.bazelversion', 'bazel/**', 'crates/**', 'BUILD.bazel') }} | |
| run: | | |
| echo "prefix=natives-linux-x64-v1-" >> "$GITHUB_OUTPUT" | |
| echo "key=natives-linux-x64-v1-$NATIVE_INPUTS_HASH" >> "$GITHUB_OUTPUT" | |
| # ---- PR path: main-built addons from actions/cache, else npm ---- | |
| # `ci-natives` is a plain workspace-relative path on purpose: the | |
| # cache version hashes the literal path list, so kata (save) and | |
| # hosted (restore) must spell it identically, and upload-artifact | |
| # skips dot-directories by default. | |
| - name: Restore main-built native addons | |
| id: natives-cache | |
| if: github.event_name == 'pull_request' | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: ci-natives | |
| key: ${{ steps.natives-key.outputs.key }} | |
| restore-keys: | | |
| ${{ steps.natives-key.outputs.prefix }} | |
| - name: Fetch release native addons (npm) | |
| if: github.event_name == 'pull_request' && steps.natives-cache.outputs.cache-matched-key == '' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| tarball="$(npm view @oh-my-pi/pi-natives-linux-x64@latest dist.tarball)" | |
| echo "Fetching $tarball" | |
| curl -fsSL --retry 3 "$tarball" | tar -xz -C "$RUNNER_TEMP" | |
| mkdir -p ci-natives/natives-linux-x64-baseline ci-natives/natives-linux-x64-modern | |
| cp "$RUNNER_TEMP/package/pi_natives.linux-x64-baseline.node" ci-natives/natives-linux-x64-baseline/ | |
| cp "$RUNNER_TEMP/package/pi_natives.linux-x64-modern.node" ci-natives/natives-linux-x64-modern/ | |
| # Make the addon source visible whenever it is not source-matched, | |
| # instead of leaving a reviewer to wonder which addons the tests ran. | |
| - name: Report native addon source | |
| if: github.event_name == 'pull_request' | |
| shell: bash | |
| env: | |
| EXACT: ${{ steps.natives-cache.outputs.cache-hit }} | |
| MATCHED: ${{ steps.natives-cache.outputs.cache-matched-key }} | |
| run: | | |
| if [ "$EXACT" = true ]; then | |
| echo "Using main's addons for this exact native source state ($MATCHED)." | |
| elif [ -n "$MATCHED" ]; then | |
| echo "::notice title=Native addons not source-matched::No main build matches this PR's native sources; testing against main's newest addon build ($MATCHED). Native changes are validated post-merge on main and at release." | |
| else | |
| echo "::notice title=Native addons from npm::No main-built addon cache entry is available; testing against the latest npm release addons." | |
| fi | |
| # ---- main path: bazel build of the linux-x64 pair ---- | |
| - id: cache | |
| if: github.event_name != 'pull_request' | |
| uses: ./.github/actions/bazel-cache | |
| - name: Build linux-x64 native addons | |
| if: github.event_name != 'pull_request' | |
| run: | | |
| set -eo pipefail | |
| if [ "$(ulimit -Sn)" != unlimited ] && [ "$(ulimit -Sn)" -lt 65536 ]; then ulimit -Sn 65536 || true; fi | |
| # The addon cdylib links each peak at several GiB of rustc RSS; | |
| # building several addons in one invocation runs them | |
| # concurrently, which OOMs the kata pod and takes the bazel | |
| # server with it (exit 37 "Server terminated abruptly", runs | |
| # 30556752623 / 30557524371). Build one addon per invocation — | |
| # analysis and cached actions are shared through the persistent | |
| # server, so only the heavy links serialize. | |
| : > "$RUNNER_TEMP/bazel-build.log" | |
| for target in $NATIVES_X64_TARGETS; do | |
| bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" build "//:$target" 2>&1 | tee -a "$RUNNER_TEMP/bazel-build.log" | |
| done | |
| for target in $NATIVES_X64_TARGETS; do | |
| mkdir -p "ci-natives/$target" | |
| cp -L bazel-bin/"$target"/*.node "ci-natives/$target/" | |
| done | |
| # Cache-hit visibility: a supposedly warm build that executes | |
| # thousands of actions is the failure mode that made CI slow — make | |
| # it visible in the run summary instead of discovering it weeks in. | |
| - name: Report bazel cache stats | |
| if: github.event_name != 'pull_request' | |
| shell: bash | |
| run: | | |
| # Bazel runs with --color=yes (config=ci); strip ANSI before | |
| # matching — escapes split "INFO:" from the process count in | |
| # the raw log. | |
| summary=$(sed -E 's/\x1b\[[0-9;]*m//g' "$RUNNER_TEMP/bazel-build.log" | grep -E '[0-9]+ processes:' | tail -1 || true) | |
| echo "::notice title=Bazel build summary::${summary:-no process summary found}" | |
| # ---- both paths ---- | |
| # A corrupt download or cache entry must fail here, not as a | |
| # confusing dlopen error in every downstream test shard. | |
| - name: Smoke native addons | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # NOTE: `bun -e 'require("./x.node")'` swallows dlopen failures | |
| # (exit 0 on a bogus addon); a script file enforces them in | |
| # both bun and node. Verified against a corrupt .node fixture. | |
| cat > "$RUNNER_TEMP/smoke-addons.js" <<'EOF' | |
| for (const f of process.argv.slice(2)) { | |
| const m = require(f); | |
| if (!m || Object.keys(m).length === 0) { | |
| console.error(`addon failed to load: ${f}`); | |
| process.exit(1); | |
| } | |
| } | |
| EOF | |
| loader=node | |
| if command -v bun >/dev/null 2>&1; then loader=bun; fi | |
| "$loader" "$RUNNER_TEMP/smoke-addons.js" \ | |
| "$PWD/ci-natives/natives-linux-x64-baseline/pi_natives.linux-x64-baseline.node" \ | |
| "$PWD/ci-natives/natives-linux-x64-modern/pi_natives.linux-x64-modern.node" | |
| # Kata test jobs download ~120 MB of artifact over the node's NAT | |
| # egress (44-68 s per job, on the critical path). Every kata pod | |
| # mounts the shared runner-cache PVC at /opt/bazel-repo-cache (the | |
| # xwin cache already lives there), so stash the pair content-addressed | |
| # beside it; the native-artifacts action restores and re-verifies it | |
| # and falls back to the artifact on any miss. Best-effort: a stash | |
| # failure only costs consumers the download. | |
| - name: Stage native addons for consumers | |
| id: stage | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| stash="" | |
| for f in ci-natives/natives-linux-x64-*/*.node; do | |
| sha="$(sha256sum "$f" | cut -d' ' -f1)" | |
| stash="$stash ${f#ci-natives/}=$sha" | |
| done | |
| echo "stash=${stash# }" >> "$GITHUB_OUTPUT" | |
| stash_dir=/opt/bazel-repo-cache/ci-natives | |
| if [ -z "${BAZEL_REMOTE_USER:-}" ] || [ ! -w /opt/bazel-repo-cache ]; then | |
| echo "No in-cluster stash on this runner; consumers download the artifact." | |
| exit 0 | |
| fi | |
| if ! mkdir -p "$stash_dir"; then | |
| echo "::warning title=Native addon stash::cannot create $stash_dir; consumers will download the artifact" | |
| exit 0 | |
| fi | |
| # Consumers of a run need its entries for minutes; two days | |
| # covers re-runs while bounding the PVC footprint. | |
| find "$stash_dir" -type f -mmin +2880 -delete 2>/dev/null || true | |
| for entry in ${stash# }; do | |
| rel="${entry%%=*}" | |
| sha="${entry##*=}" | |
| dest="$stash_dir/$sha.node" | |
| if [ -f "$dest" ]; then | |
| touch "$dest" | |
| continue | |
| fi | |
| tmp="$stash_dir/.$sha.$$.tmp" | |
| if cp "ci-natives/$rel" "$tmp" && mv -f "$tmp" "$dest"; then | |
| echo "stashed $rel -> $dest" | |
| else | |
| rm -f "$tmp" | |
| echo "::warning title=Native addon stash::could not stash $rel; consumers will download the artifact" | |
| fi | |
| done | |
| # Only main saves: an entry saved on any other ref is readable by | |
| # that ref alone yet counts against the shared 10 GB repo quota, | |
| # whose LRU eviction has already cost the darwin release archives. | |
| # Probe first so an existing entry is not re-uploaded, then keep | |
| # only the newest two entries so the footprint stays ~100 MB. | |
| # The three steps below run only on main (never exercised by PR or | |
| # branch runs) and are pure optimization for later PRs: a failure | |
| # must never fail native_addons and with it every test job. | |
| - name: Probe native addon cache entry | |
| id: natives-cache-probe | |
| if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' | |
| continue-on-error: true | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: ci-natives | |
| key: ${{ steps.natives-key.outputs.key }} | |
| lookup-only: true | |
| - name: Save native addon cache entry | |
| if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && steps.natives-cache-probe.outputs.cache-hit != 'true' | |
| continue-on-error: true | |
| uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: ci-natives | |
| key: ${{ steps.natives-key.outputs.key }} | |
| - name: Prune superseded native addon cache entries | |
| if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && steps.natives-cache-probe.outputs.cache-hit != 'true' | |
| continue-on-error: true | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PREFIX: ${{ steps.natives-key.outputs.prefix }} | |
| run: | | |
| # Best-effort: a failed prune leaves eviction to GitHub's LRU. | |
| gh cache list --repo "$GITHUB_REPOSITORY" --ref refs/heads/main --key "$PREFIX" \ | |
| --sort created_at --order desc --limit 100 --json id --jq '.[2:][].id' \ | |
| | while read -r id; do | |
| gh cache delete "$id" --repo "$GITHUB_REPOSITORY" || true | |
| done || true | |
| - name: Upload linux-x64 native addon artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: natives-linux-x64 | |
| path: ci-natives/natives-linux-x64-*/*.node | |
| if-no-files-found: error | |
| retention-days: 1 | |
| # The remaining targets (darwin, arm64, musl and win32 cross builds). Only | |
| # release jobs consume them, so they build beside the TS fan-out instead of | |
| # ahead of it: serially after the pair they held every test job back ~10 | |
| # min on native-changing main pushes. Non-PR only, like rust_validate; | |
| # the build also keeps the remote cache warm for release runs. | |
| native_addons_cross: | |
| name: Build native addons (bazel, cross targets) | |
| if: github.event_name != 'pull_request' | |
| runs-on: omp-kata | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - id: cache | |
| uses: ./.github/actions/bazel-cache | |
| - name: Build cross-target native addons | |
| run: | | |
| set -eo pipefail | |
| if [ "$(ulimit -Sn)" != unlimited ] && [ "$(ulimit -Sn)" -lt 65536 ]; then ulimit -Sn 65536 || true; fi | |
| # One addon per invocation, for the OOM reason in native_addons. | |
| : > "$RUNNER_TEMP/bazel-build.log" | |
| for target in $NATIVES_CROSS_TARGETS; do | |
| bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" build "//:$target" 2>&1 | tee -a "$RUNNER_TEMP/bazel-build.log" | |
| done | |
| # The two jobs' target lists must together cover //:natives-all | |
| # exactly; a target added to the filegroup but to neither list would | |
| # otherwise silently never build or ship. cquery, not query: the rc's | |
| # --config=ci is defined for build only, which cquery inherits and | |
| # query rejects; this also reuses the warm server. | |
| - name: Check addon target lists cover natives-all | |
| run: | | |
| set -euo pipefail | |
| read -r -a listed <<< "$NATIVES_X64_TARGETS $NATIVES_CROSS_TARGETS" | |
| expected="$(printf '%s\n' "${listed[@]}" | sort)" | |
| actual="$(bazelisk --bazelrc="${{ steps.cache.outputs.rc }}" cquery 'labels(srcs, //:natives-all)' 2>"$RUNNER_TEMP/query.err" | sed -E 's/ \(.*\)$//; s/.*://' | sort)" || { | |
| cat "$RUNNER_TEMP/query.err" | |
| exit 1 | |
| } | |
| if [ "$expected" != "$actual" ]; then | |
| echo "::error title=Native addon target lists::NATIVES_X64_TARGETS + NATIVES_CROSS_TARGETS must equal //:natives-all srcs" | |
| diff <(echo "$expected") <(echo "$actual") || true | |
| exit 1 | |
| fi | |
| - name: Verify Apple Foundation Models bridge | |
| run: | | |
| set -euo pipefail | |
| addon=bazel-bin/natives-darwin-arm64/pi_natives.darwin-arm64.node | |
| # The bridge is embedded as a separate dylib (module OmpAppleFm), | |
| # never linked: a linked Swift runtime crashes the addon on load | |
| # on older macOS. | |
| grep -aqF OmpAppleFm "$addon" || | |
| { echo "::error::Darwin arm64 addon was built without the Apple Foundation Models bridge"; exit 1; } | |
| dylibs="$(llvm-objdump --macho --dylibs-used "$addon")" | |
| if grep -E 'libswift|FoundationModels' <<< "$dylibs"; then | |
| echo "::error::Darwin arm64 addon links the Swift runtime or FoundationModels directly"; exit 1 | |
| fi | |
| - name: Report bazel cache stats | |
| shell: bash | |
| run: | | |
| summary=$(sed -E 's/\x1b\[[0-9;]*m//g' "$RUNNER_TEMP/bazel-build.log" | grep -E '[0-9]+ processes:' | tail -1 || true) | |
| echo "::notice title=Bazel build summary::${summary:-no process summary found}" | |
| # Everything except the linux-x64 pair, which native_addons ships as | |
| # its own small `natives-linux-x64` artifact. | |
| - name: Upload remaining native addon artifacts | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: native-addons | |
| path: | | |
| bazel-bin/natives-*/*.node | |
| !bazel-bin/natives-linux-x64-*/*.node | |
| if-no-files-found: error | |
| retention-days: 1 | |
| test_coding_agent_native: | |
| name: "Test coding-agent native/unit (TS) ${{ matrix.shard }}" | |
| runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }} | |
| needs: [native_addons] | |
| if: ${{ !cancelled() && needs.native_addons.result == 'success' }} | |
| timeout-minutes: 25 | |
| # The longest PR job, so it runs as three shards on hosted PR runners | |
| # (two left the slower shard ~25 s behind every other job). Main keeps | |
| # one: omp-kata caps at 8 runners and the fan-out already fills it. | |
| # Declared first among the test jobs: when the 8-runner pool is full | |
| # the longest bucket should not be the one left queued (probe run | |
| # 36238796966 queued it 1.9 min behind the shorter buckets). | |
| # No required check keys on this job name (branch protection and | |
| # rulesets are off for main/omp2), so the per-shard names are safe. | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: ${{ fromJSON(github.event_name == 'pull_request' && '["1/3","2/3","3/3"]' || '["1/1"]') }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/bun-install | |
| - uses: ./.github/actions/native-artifacts | |
| with: | |
| targets: linux-x64-baseline linux-x64-modern | |
| stash: ${{ needs.native_addons.outputs.stash }} | |
| - name: Test coding-agent native/unit bucket | |
| env: | |
| OMP_TEST_CONCURRENCY: "4" | |
| OMP_TEST_SHARD: ${{ matrix.shard }} | |
| run: bun run ci:test:coding-agent:native | |
| test_workspace: | |
| name: Test TS workspace fast | |
| runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }} | |
| needs: [native_addons] | |
| if: ${{ !cancelled() && needs.native_addons.result == 'success' }} | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/bun-install | |
| - uses: ./.github/actions/native-artifacts | |
| with: | |
| targets: linux-x64-baseline linux-x64-modern | |
| stash: ${{ needs.native_addons.outputs.stash }} | |
| - name: Test workspace packages and repo scripts (TS) | |
| env: | |
| OMP_TEST_CONCURRENCY: "4" | |
| run: | | |
| bun run ci:test:ts:workspace | |
| # Not `test:scripts`: scripts/install.test.ts fails on main | |
| # (its install.sh smoke-check executes a fake binary), so running | |
| # the whole group here would red this job on an unrelated break. | |
| bun test scripts/release.test.ts | |
| test_coding_agent_singleton: | |
| name: Test coding-agent singleton/global-state (TS) | |
| runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }} | |
| needs: [native_addons] | |
| if: ${{ !cancelled() && needs.native_addons.result == 'success' }} | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/bun-install | |
| - uses: ./.github/actions/native-artifacts | |
| with: | |
| targets: linux-x64-baseline linux-x64-modern | |
| stash: ${{ needs.native_addons.outputs.stash }} | |
| - name: Test coding-agent singleton/global-state bucket | |
| # Keep global Settings/env/fake-timer tests serial; native addon | |
| # artifacts are still available like every other coding-agent bucket. | |
| run: bun run ci:test:coding-agent:singleton | |
| test_ts_native: | |
| name: Test TS native/integration packages | |
| runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }} | |
| needs: [native_addons] | |
| if: ${{ !cancelled() && needs.native_addons.result == 'success' }} | |
| timeout-minutes: 25 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/bun-install | |
| - uses: ./.github/actions/native-artifacts | |
| with: | |
| targets: linux-x64-baseline linux-x64-modern | |
| stash: ${{ needs.native_addons.outputs.stash }} | |
| - name: Test native/TUI/browser-ish packages (TS) | |
| env: | |
| OMP_TEST_CONCURRENCY: "4" | |
| run: bun run ci:test:ts:native | |
| test_coding_agent_ui: | |
| name: Test coding-agent UI/TUI (TS) | |
| runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }} | |
| needs: [native_addons] | |
| if: ${{ !cancelled() && needs.native_addons.result == 'success' }} | |
| timeout-minutes: 25 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/bun-install | |
| - uses: ./.github/actions/native-artifacts | |
| with: | |
| targets: linux-x64-baseline linux-x64-modern | |
| stash: ${{ needs.native_addons.outputs.stash }} | |
| - name: Test coding-agent UI/TUI bucket | |
| env: | |
| OMP_TEST_CONCURRENCY: "2" | |
| run: bun run ci:test:coding-agent:ui | |
| test_coding_agent_runtime: | |
| name: Test coding-agent runtime/session (TS) | |
| runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }} | |
| needs: [native_addons] | |
| if: ${{ !cancelled() && needs.native_addons.result == 'success' }} | |
| timeout-minutes: 25 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/bun-install | |
| - uses: ./.github/actions/native-artifacts | |
| with: | |
| targets: linux-x64-baseline linux-x64-modern | |
| stash: ${{ needs.native_addons.outputs.stash }} | |
| - name: Test coding-agent runtime bucket | |
| # Runtime/session tests import native-backed barrels too; keep this | |
| # separate for concurrency, not as a native-free guardrail. | |
| env: | |
| OMP_TEST_CONCURRENCY: "4" | |
| run: bun run ci:test:coding-agent:runtime | |
| test_smoke: | |
| name: Test CLI smoke (TS) | |
| runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }} | |
| needs: [native_addons] | |
| if: ${{ !cancelled() && needs.native_addons.result == 'success' }} | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/bun-install | |
| - uses: ./.github/actions/native-artifacts | |
| with: | |
| targets: linux-x64-baseline linux-x64-modern | |
| stash: ${{ needs.native_addons.outputs.stash }} | |
| - name: CLI smoke test | |
| run: bun run ci:test:smoke | |
| install_methods: | |
| name: Install method smoke tests | |
| runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-22.04' || 'omp-kata' }} | |
| needs: [native_addons] | |
| if: ${{ !cancelled() && needs.native_addons.result == 'success' }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/bun-install | |
| - uses: ./.github/actions/native-artifacts | |
| with: | |
| targets: linux-x64-baseline linux-x64-modern | |
| stash: ${{ needs.native_addons.outputs.stash }} | |
| - name: Install method smoke tests | |
| env: | |
| OMP_INSTALL_TEST_SKIP_NATIVE_BUILD: "1" | |
| run: bun run ci:test:install-methods | |
| # Aggregates every validation job so publish-side release jobs gate on one | |
| # result. Binary BUILDS deliberately do not wait for this gate — they run | |
| # in parallel with the test fan-out and only publishing is held back. | |
| release_gate: | |
| name: Release validation gate | |
| if: ${{ needs.release_metadata.outputs.is-release == 'true' && !cancelled() && | |
| needs.rust_validate.result == 'success' && | |
| needs.native_addons.result == 'success' && | |
| needs.native_addons_cross.result == 'success' && | |
| needs.test_workspace.result == 'success' && | |
| needs.test_coding_agent_singleton.result == 'success' && | |
| needs.test_ts_native.result == 'success' && | |
| needs.test_coding_agent_ui.result == 'success' && | |
| needs.test_coding_agent_runtime.result == 'success' && | |
| needs.test_coding_agent_native.result == 'success' && | |
| needs.test_smoke.result == 'success' && needs.check.result == 'success' && | |
| needs.install_methods.result == 'success' }} | |
| needs: [release_metadata, check, rust_validate, native_addons, native_addons_cross, test_workspace, test_coding_agent_singleton, test_ts_native, test_coding_agent_ui, test_coding_agent_runtime, test_coding_agent_native, test_smoke, install_methods] | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/bun-install | |
| # Generated-config drift gate: bazel/clippy.bazelrc is maintained by | |
| # scripts/release.ts (like the lockfiles); a stale file only blocks | |
| # publishing, never ordinary CI. | |
| - name: Check generated clippy config | |
| run: bun scripts/gen-clippy-bazelrc.ts --check | |
| - run: echo "release validation green" | |
| # Builds (does not publish) every release binary on one Linux x64 image in | |
| # parallel with the test fan-out; the needs are the two native_addons jobs, | |
| # whose artifacts supply their addons. bun cross-compiles every target; | |
| # macOS binaries are Developer ID signed and notarized here too | |
| # (rcodesign). release_smoke then runs each binary on its own platform. | |
| # Publishing (npm leaves, GitHub release, build.stencil.so, core npm) is gated on | |
| # release_gate downstream. | |
| release_binary: | |
| name: "Release binary: ${{ matrix.target_id }}" | |
| if: ${{ needs.release_metadata.outputs.is-release == 'true' && !cancelled() && | |
| needs.native_addons.result == 'success' && | |
| needs.native_addons_cross.result == 'success' }} | |
| needs: [release_metadata, native_addons, native_addons_cross] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # Addons cross-built by Bazel (clang + ld64.lld against the | |
| # Command Line Tools SDK) in native_addons_cross. | |
| - { | |
| platform: darwin, | |
| target_id: darwin-x64, | |
| binary_path: packages/coding-agent/binaries/omp-darwin-x64, | |
| native_targets: darwin-x64-baseline, | |
| } | |
| - { | |
| platform: darwin, | |
| target_id: darwin-arm64, | |
| binary_path: packages/coding-agent/binaries/omp-darwin-arm64, | |
| native_targets: darwin-arm64, | |
| } | |
| - { | |
| platform: linux, | |
| target_id: linux-x64, | |
| binary_path: packages/coding-agent/binaries/omp-linux-x64, | |
| native_targets: linux-x64-baseline linux-x64-modern, | |
| } | |
| - { | |
| platform: linux, | |
| target_id: linux-musl-x64, | |
| binary_path: packages/coding-agent/binaries/omp-linux-musl-x64, | |
| native_targets: linux-musl-x64-baseline, | |
| } | |
| - { | |
| platform: linux, | |
| target_id: linux-arm64, | |
| binary_path: packages/coding-agent/binaries/omp-linux-arm64, | |
| native_targets: linux-arm64, | |
| } | |
| - { | |
| platform: linux, | |
| target_id: linux-musl-arm64, | |
| binary_path: packages/coding-agent/binaries/omp-linux-musl-arm64, | |
| native_targets: linux-musl-arm64, | |
| } | |
| - { | |
| platform: win32, | |
| target_id: win32-x64, | |
| binary_path: packages/coding-agent/binaries/omp-windows-x64.exe, | |
| native_targets: win32-x64-baseline, | |
| } | |
| # Addon cross-built by Bazel (clang-cl + xwin arm64 splat) in | |
| # native_addons_cross. | |
| - { | |
| platform: win32, | |
| target_id: win32-arm64, | |
| binary_path: packages/coding-agent/binaries/omp-windows-arm64.exe, | |
| native_targets: win32-arm64, | |
| } | |
| runs-on: ubuntu-22.04 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| - name: Restore bun store | |
| id: bun-cache | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} | |
| restore-keys: | | |
| bun-${{ runner.os }}- | |
| - run: bun install --frozen-lockfile | |
| - name: Install prebuilt native addon(s) | |
| uses: ./.github/actions/native-artifacts | |
| with: | |
| targets: ${{ matrix.native_targets }} | |
| - name: Build release binary | |
| env: | |
| RELEASE_TARGETS: ${{ matrix.target_id }} | |
| run: bun run ci:release:build-binaries | |
| - name: Sign macOS binary | |
| # Replaces bun's signature with a Developer ID + hardened-runtime one | |
| # carrying the required JIT, Apple Events, and library-validation | |
| # entitlements, then notarizes; ad hoc with the same entitlements | |
| # until the APPLE_* secrets are configured. | |
| if: matrix.platform == 'darwin' | |
| env: | |
| APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
| APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }} | |
| APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} | |
| run: bash scripts/ci-macos-sign.sh "${{ matrix.binary_path }}" | |
| - name: Upload release binary artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: omp-binary-${{ matrix.target_id }} | |
| path: ${{ matrix.binary_path }} | |
| # Main-only save, after the build so it is off the leg's critical | |
| # path: caches saved on a tag or branch are readable only by that ref | |
| # yet count against the shared 10 GB repo cache quota. | |
| - name: Save bun store | |
| if: github.ref == 'refs/heads/main' && steps.bun-cache.outputs.cache-hit != 'true' | |
| continue-on-error: true | |
| uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} | |
| # Every release binary is cross-built on Linux (release_binary); this runs | |
| # each one on its own platform before anything publishes: download the | |
| # artifact, then `--version` and `--smoke-test` in a throwaway home. macOS | |
| # legs first run Apple's own signature check, and launch under the final | |
| # signature (hardened runtime and entitlements included when signed). These | |
| # are the exact bytes release_github and release_build publish, so no job | |
| # re-verifies the published assets. | |
| release_smoke: | |
| name: "Smoke release binary: ${{ matrix.target_id }}" | |
| if: ${{ needs.release_metadata.outputs.is-release == 'true' && !cancelled() && | |
| needs.release_binary.result == 'success' }} | |
| needs: [release_metadata, release_binary] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { os: ubuntu-22.04, target_id: linux-x64, binary: omp-linux-x64 } | |
| - { os: ubuntu-24.04-arm, target_id: linux-arm64, binary: omp-linux-arm64 } | |
| # musl binaries need the musl loader, which glibc runners lack: | |
| # they run in an Alpine container of the runner's arch. | |
| - { os: ubuntu-22.04, target_id: linux-musl-x64, binary: omp-linux-musl-x64, alpine: true } | |
| - { os: ubuntu-24.04-arm, target_id: linux-musl-arm64, binary: omp-linux-musl-arm64, alpine: true } | |
| - { os: macos-15-intel, target_id: darwin-x64, binary: omp-darwin-x64 } | |
| - { os: macos-15, target_id: darwin-arm64, binary: omp-darwin-arm64 } | |
| - { os: windows-2025, target_id: win32-x64, binary: omp-windows-x64.exe } | |
| - { os: windows-11-arm, target_id: win32-arm64, binary: omp-windows-arm64.exe } | |
| runs-on: ${{ matrix.os }} | |
| permissions: | |
| contents: read | |
| env: | |
| MACOS_SIGNING: ${{ secrets.APPLE_CERTIFICATE_P12 != '' && secrets.APPLE_CERTIFICATE_PASSWORD != '' && secrets.APPLE_API_KEY_ID != '' && secrets.APPLE_API_ISSUER_ID != '' && secrets.APPLE_API_KEY != '' }} | |
| steps: | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: omp-binary-${{ matrix.target_id }} | |
| path: binaries | |
| - name: Smoke release binary | |
| if: runner.os != 'Windows' && !matrix.alpine | |
| env: | |
| BINARY: binaries/${{ matrix.binary }} | |
| run: | | |
| set -euo pipefail | |
| chmod +x "$BINARY" | |
| if [ "$RUNNER_OS" = macOS ]; then codesign --verify --strict --verbose=2 "$BINARY"; fi | |
| runtime_dir="$(mktemp -d)" | |
| HOME="$runtime_dir/home" XDG_DATA_HOME="$runtime_dir/xdg" "$BINARY" --version | |
| HOME="$runtime_dir/home" XDG_DATA_HOME="$runtime_dir/xdg" "$BINARY" --smoke-test | |
| # ci-macos-sign.sh signs ad hoc whenever its step lacks the APPLE_* | |
| # credentials, so an ad-hoc binary while the secrets exist means they | |
| # stopped reaching it. | |
| - name: Assert Developer ID signature | |
| if: runner.os == 'macOS' && env.MACOS_SIGNING == 'true' | |
| env: | |
| BINARY: binaries/${{ matrix.binary }} | |
| run: | | |
| info="$(codesign -dvvv "$BINARY" 2>&1)" | |
| echo "$info" | |
| if grep -qE "flags=.*adhoc|Signature=adhoc" <<< "$info"; then | |
| echo "::error::$BINARY is ad-hoc signed (Developer ID signing did not run)" | |
| exit 1 | |
| fi | |
| # Informational: a bare (unstapled) Mach-O relies on the online | |
| # notarization ticket lookup, so surface the Gatekeeper result | |
| # without gating the release on it. | |
| spctl -a -t exec -vv "$BINARY" || echo "spctl non-zero (expected for unstapled bare binary; ticket served online)" | |
| - name: Smoke release binary (Alpine) | |
| if: matrix.alpine | |
| env: | |
| BINARY: binaries/${{ matrix.binary }} | |
| run: | | |
| chmod +x "$BINARY" | |
| # Bun's musl-target binaries link libstdc++/libgcc dynamically; | |
| # Alpine users install them alongside the binary (same as bun itself). | |
| docker run --rm -v "$(realpath "$BINARY"):/usr/local/bin/omp:ro" alpine:3.22 sh -ec ' | |
| apk add --no-cache libstdc++ libgcc >/dev/null | |
| runtime_dir="$(mktemp -d)" | |
| HOME="$runtime_dir/home" XDG_DATA_HOME="$runtime_dir/xdg" omp --version | |
| HOME="$runtime_dir/home" XDG_DATA_HOME="$runtime_dir/xdg" omp --smoke-test | |
| ' | |
| - name: Smoke release binary (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| env: | |
| BINARY: binaries/${{ matrix.binary }} | |
| run: | | |
| $runtimeDir = Join-Path $env:RUNNER_TEMP "omp-smoke" | |
| $env:HOME = Join-Path $runtimeDir "home" | |
| $env:USERPROFILE = $env:HOME | |
| $env:LOCALAPPDATA = Join-Path $runtimeDir "local" | |
| $env:XDG_DATA_HOME = Join-Path $runtimeDir "xdg" | |
| New-Item -ItemType Directory -Force -Path $env:HOME, $env:LOCALAPPDATA, $env:XDG_DATA_HOME | Out-Null | |
| & $env:BINARY --version | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| & $env:BINARY --smoke-test | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| # Publishes the six @oh-my-pi/pi-natives-<tag> leaf packages once | |
| # validation passes and every binary built. Runs on one linux runner: leaf | |
| # publishing is platform-agnostic file packaging (gen-npm-packages reads | |
| # packages/natives/native), so no per-platform runners are needed. | |
| release_native_leaves: | |
| name: Publish native leaf packages | |
| if: ${{ needs.release_metadata.outputs.is-release == 'true' && !cancelled() && | |
| needs.release_gate.result == 'success' && | |
| needs.release_binary.result == 'success' && | |
| needs.release_smoke.result == 'success' && | |
| !inputs.skip_npm }} | |
| needs: [release_metadata, release_gate, release_binary, release_smoke] | |
| runs-on: ubuntu-22.04 | |
| # id-token lets npm mint the OIDC token for trusted publishing; the | |
| # NODE_AUTH_TOKEN below is the fallback for unconfigured packages. | |
| permissions: | |
| id-token: write | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: "24" | |
| registry-url: "https://registry.npmjs.org" | |
| # npm runs under Bun when invoked by the release script; npm 12 | |
| # requires a newer emulated Node version than Bun 1.3 provides. | |
| - name: Ensure npm supports trusted publishing | |
| run: npm install -g npm@11.17.0 | |
| - name: Restore bun store | |
| id: bun-cache | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} | |
| restore-keys: | | |
| bun-${{ runner.os }}- | |
| - run: bun install --frozen-lockfile | |
| # Main-only save: caches saved on a tag or branch are readable only by | |
| # that ref yet count against the shared 10 GB repo cache quota. | |
| - name: Save bun store | |
| if: github.ref == 'refs/heads/main' && steps.bun-cache.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} | |
| # Every leaf's addon from the native_addons artifacts (gnu targets | |
| # only — musl reuses the linux filenames and has no leaf package). | |
| - name: Install native addons | |
| uses: ./.github/actions/native-artifacts | |
| with: | |
| targets: linux-x64-baseline linux-x64-modern linux-arm64 darwin-x64-baseline darwin-arm64 win32-x64-baseline win32-arm64 | |
| - name: Publish native leaf packages | |
| env: | |
| # Fallback auth: setup-node wrote an .npmrc referencing | |
| # NODE_AUTH_TOKEN; npm uses it only when OIDC has no trusted | |
| # publisher for the package (or on a first publish). | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| # The six leaves are independent: each process generates its own | |
| # packages/natives/npm/<tag> dir and packs into a private temp dir, | |
| # and the legal payload is tracked in packages/natives, so nothing | |
| # shared is written. Serially they cost ~140 s on the release | |
| # critical path. Every leaf runs to completion so one failure does | |
| # not strand the others half-published; reruns skip published ones. | |
| run: | | |
| set -uo pipefail | |
| logs="$RUNNER_TEMP/native-leaf-logs" | |
| mkdir -p "$logs" | |
| tags=(linux-x64 linux-arm64 darwin-x64 darwin-arm64 win32-x64 win32-arm64) | |
| pids=() | |
| for tag in "${tags[@]}"; do | |
| bun run ci:release:publish-native-leaf "$tag" >"$logs/$tag.log" 2>&1 & | |
| pids+=("$!") | |
| done | |
| failed=() | |
| for i in "${!tags[@]}"; do | |
| tag="${tags[$i]}" | |
| if wait "${pids[$i]}"; then | |
| status=ok | |
| else | |
| status="failed (exit $?)" | |
| failed+=("$tag") | |
| fi | |
| echo "::group::$tag: $status" | |
| cat "$logs/$tag.log" | |
| echo "::endgroup::" | |
| done | |
| if [ "${#failed[@]}" -gt 0 ]; then | |
| echo "::error title=Native leaf publish::failed: ${failed[*]}" | |
| exit 1 | |
| fi | |
| release_github: | |
| name: Publish GitHub release | |
| if: ${{ needs.release_metadata.outputs.is-release == 'true' && !cancelled() && | |
| needs.release_gate.result == 'success' && | |
| needs.release_binary.result == 'success' && | |
| needs.release_smoke.result == 'success' }} | |
| needs: [release_metadata, release_gate, release_binary, release_smoke] | |
| runs-on: ubuntu-22.04 | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| - name: Generate release notes from CHANGELOGs | |
| env: | |
| # `gh release list` (used to find the latest published GitHub | |
| # Release tag below the target so silent-tag changelog sections | |
| # roll forward — #2596) requires GH_TOKEN in Actions. Without | |
| # it gh exits non-zero and the script would degrade to legacy | |
| # single-version notes, defeating the recovery. | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: bun scripts/ci-release-notes.ts ${{ needs.release_metadata.outputs.release-tag }} | |
| - name: Download release binaries | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: omp-binary-* | |
| path: packages/coding-agent/binaries | |
| merge-multiple: true | |
| # Dependency-free build (bun APIs + zip only), so no `bun install` | |
| # is needed on this runner. | |
| - name: Build browser relay artifacts | |
| run: bun run --cwd packages/browser-relay build | |
| # Generated after every other release asset is in place and before | |
| # the release is created, so SHA256SUMS.txt itself ships as an asset | |
| # and covers every other file uploaded alongside it. | |
| - name: Generate checksums | |
| run: | | |
| bun run ci:release:checksums SHA256SUMS.txt \ | |
| packages/coding-agent/binaries/omp-* \ | |
| packages/browser-relay/dist/omp-browser-relay-extension.zip \ | |
| LICENSE \ | |
| THIRD-PARTY-NOTICES.txt | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 | |
| with: | |
| tag_name: ${{ needs.release_metadata.outputs.release-tag }} | |
| prerelease: ${{ needs.release_metadata.outputs.channel == 'canary' }} | |
| files: | | |
| packages/coding-agent/binaries/omp-* | |
| packages/browser-relay/dist/omp-browser-relay-extension.zip | |
| LICENSE | |
| THIRD-PARTY-NOTICES.txt | |
| SHA256SUMS.txt | |
| body_path: release-notes.md | |
| generate_release_notes: true | |
| # Publishes the release binaries to build.stencil.so, which serves them | |
| # (with binary patches between releases) to `omp update`, the install | |
| # scripts' binary mode, and the Homebrew formula. Canary tags upload to the | |
| # canary channel (derived script-side). Not gated on skip_npm: binary | |
| # clients never read npm. | |
| release_build: | |
| name: Publish to build.stencil.so | |
| if: ${{ needs.release_metadata.outputs.is-release == 'true' && !cancelled() && | |
| needs.release_gate.result == 'success' && | |
| needs.release_binary.result == 'success' && | |
| needs.release_smoke.result == 'success' && | |
| needs.release_github.result == 'success' }} | |
| needs: [release_metadata, release_gate, release_binary, release_smoke, release_github] | |
| runs-on: ubuntu-22.04 | |
| # `id-token: write` lets the upload mint the GitHub OIDC token (audience | |
| # build.stencil.so) the service authenticates the publisher with. | |
| permissions: | |
| id-token: write | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| - name: Download release binaries | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: omp-binary-* | |
| path: packages/coding-agent/binaries | |
| merge-multiple: true | |
| # Dependency-free script (bun APIs + curl), so no `bun install`. | |
| - name: Publish to build.stencil.so | |
| run: bun scripts/ci-build-upload.ts "${{ needs.release_metadata.outputs.release-tag }}" packages/coding-agent/binaries/omp-* | |
| release_npm: | |
| name: Publish to npm | |
| if: ${{ needs.release_metadata.outputs.is-release == 'true' && !cancelled() && | |
| needs.release_binary.result == 'success' && | |
| needs.release_smoke.result == 'success' && | |
| needs.release_build.result == 'success' && | |
| needs.release_native_leaves.result == 'success' && | |
| !inputs.skip_npm }} | |
| # release_native_leaves must finish first: the core package lists the | |
| # leaf packages as lockstep-versioned optionalDependencies, so installs | |
| # resolve them the moment the core version is live. release_build too: | |
| # binary updates install what build.stencil.so published (update-cli.ts), | |
| # so the build service must lead npm. | |
| needs: [release_metadata, release_binary, release_smoke, release_build, release_native_leaves] | |
| runs-on: ubuntu-22.04 | |
| # `id-token: write` lets npm mint the GitHub OIDC token it exchanges for a | |
| # short-lived publish token (trusted publishing + provenance). When a | |
| # package has no matching trusted publisher configured, npm silently falls | |
| # back to NODE_AUTH_TOKEN below — which also covers first-ever publishes. | |
| permissions: | |
| id-token: write | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: "24" | |
| registry-url: "https://registry.npmjs.org" | |
| # npm runs under Bun when invoked by the release script; npm 12 | |
| # requires a newer emulated Node version than Bun 1.3 provides. | |
| - name: Ensure npm supports trusted publishing | |
| run: npm install -g npm@11.17.0 | |
| - name: Restore bun store | |
| id: bun-cache | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} | |
| restore-keys: | | |
| bun-${{ runner.os }}- | |
| - run: bun install --frozen-lockfile | |
| # Main-only save: caches saved on a tag or branch are readable only by | |
| # that ref yet count against the shared 10 GB repo cache quota. | |
| - name: Save bun store | |
| if: github.ref == 'refs/heads/main' && steps.bun-cache.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} | |
| # The prepack executes workspace code which loads the Linux x64 | |
| # addon, so install the native_addons job's artifact before publishing. | |
| - name: Install prebuilt native addons | |
| uses: ./.github/actions/native-artifacts | |
| with: | |
| targets: linux-x64-baseline linux-x64-modern | |
| - name: Publish to npm | |
| env: | |
| # Fallback auth: setup-node wrote an .npmrc referencing | |
| # NODE_AUTH_TOKEN; npm uses it only when OIDC has no trusted | |
| # publisher for the package (or on a first publish). | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: bun run ci:release:publish | |
| # Regenerate the Homebrew tap formula (can1357/homebrew-tap) from the build | |
| # release_build just published to build.stencil.so and push it; the formula | |
| # downloads from there. release_smoke already ran those binaries on their | |
| # own platforms. No-ops when HOMEBREW_TAP_DEPLOY_KEY is unset, so a release | |
| # never blocks on tap access. | |
| release_brew: | |
| name: Update Homebrew tap | |
| if: ${{ needs.release_metadata.outputs.is-release == 'true' && !cancelled() && | |
| needs.release_metadata.outputs.channel != 'canary' && | |
| needs.release_build.result == 'success' }} | |
| needs: [release_metadata, release_build] | |
| runs-on: ubuntu-22.04 | |
| env: | |
| HAS_TAP_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY != '' }} | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| if: env.HAS_TAP_KEY == 'true' | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| if: env.HAS_TAP_KEY == 'true' | |
| - name: Check out the Homebrew tap | |
| if: env.HAS_TAP_KEY == 'true' | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| repository: can1357/homebrew-tap | |
| ssh-key: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }} | |
| path: homebrew-tap | |
| - name: Regenerate and push the formula | |
| if: env.HAS_TAP_KEY == 'true' | |
| run: | | |
| bun scripts/ci-update-brew-formula.ts "${{ needs.release_metadata.outputs.release-tag }}" --out homebrew-tap/Formula/omp.rb | |
| cd homebrew-tap | |
| if git diff --quiet -- Formula/omp.rb; then | |
| echo "formula already up to date for ${{ needs.release_metadata.outputs.release-tag }}" | |
| exit 0 | |
| fi | |
| git -c user.name="github-actions[bot]" \ | |
| -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \ | |
| commit -m "omp ${{ needs.release_metadata.outputs.release-tag }}" -- Formula/omp.rb | |
| git push origin HEAD:main |