ci: isolate and verify Homebrew bottle publication - #881
Conversation
|
CI triage: the failed staging gate did not execute this workflow change. The focused workflow evidence remains the portable-Ruby YAML parse, |
ebfe621 to
fb2499e
Compare
|
Rebased the unchanged cache-isolation patch onto merged ABI 18 main at The prior red The stable patch ID is unchanged before/after rebase: |
|
Final devil/security review at |
|
Publisher trust follow-up at exact head Independent review found that the privileged workflows still executed mutable Checkout and Upload Artifact tags while the structured trust test treated those strings as pinned. This head preserves the exact currently resolved action commits while removing tag mutability:
All six checkout steps and the diagnostic upload now use those full SHAs. The common trust contract additionally rejects every non-local Focused validation through |
|
Publication snapshot follow-up at exact head Review found that validating the names The structured trust test closes this wiring: plan outputs, the commit-resolution script, and both build checkout refs are exact contracts; mutation back to an arbitrary or branch ref is rejected. This applies equally to read-only branch dry runs and write publication, producing one coherent source snapshot per dispatch. Focused validation through |
|
Closed-world trust correction at exact head Dynamic mutation proved that the prior checker accepted an appended static token-exfiltration step in the publisher and a static unreviewed push step in maintenance rollback. Those steps used no GitHub expression and changed no action list, so the earlier selective checks did not see them. The checker now binds canonical, recursively key-sorted JSON digests for every step mapping in the publisher plan, publisher build, and maintenance rollback. It also exact-checks top-level keys, workflow names, concurrency, complete New self-mutations require rejection of:
The previously accepted static backdoors now fail closed. Through |
Run formula builds, GHCR upload, anonymous verification, and tap finalization on separate runners. Carry only bounded, validated bottle data across those trust boundaries and require anonymous byte-for-byte readback before publication.
|
Superseded by #936, which retains this PR as patch-equivalent purpose commits in the consolidated Homebrew publisher batch and reruns the combined validation on current main. |
Purpose
Make Kandelo Homebrew bottle publication safe to activate by ensuring Formula and package build code never shares a runner with GHCR or tap-write credentials.
This is platform-owned publisher work in
Automattic/kandelo. It changes no tap Formula, package recipe, kernel ABI, syscall, or host ABI, and it must not be auto-merged.Root Cause
The earlier reusable workflow executed Formula Ruby, upstream build scripts, GHCR upload, browser validation, sidecar generation, and tap publication on one runner. A Formula build can modify the workspace,
GITHUB_ENV,GITHUB_PATH, HOME state, helper scripts, or background processes. Removing tokens from the build step environment did not prevent that state from reaching a later credentialed step.The old path also trusted raw bottle JSON across the boundary, reused locally built bytes as publication evidence after upload, accepted browser skips caused by missing ABI assets, left ORAS auth in shared state, and could persist raw stderr in tap failure reports.
Change
The publisher now resolves immutable Kandelo/tap commits once, then runs every
(formula, arch)entry through four fresh runners:build-and-testhas only read authority. It runs reviewed Homebrew plus Formula/package code and emits exactly three bounded data files: a gzip bottle, raw bottle JSON, and a strict manifest.upload-bottlehaspackages: writebut no tap-write authority. It revalidates the handoff before exposingGITHUB_TOKEN, uploads through an ephemeral ORAS registry config, revalidates a strict receipt, and emits only that receipt.verify-bottleis read-only. It reconstructs minimal canonical bottle JSON, preserves Homebrew relocation-cellar semantics separately from Kandelo's install cellar, fetches the complete ABI runtime graph, and uses anonymous GHCR readback as runtime evidence for write publications. Dry runs use the exact local bottle. The strict Chromium smoke fails on missing URLs, Vite overlays, skips, flaky results, or anything other than one pass.finalize-taphascontents: writebut no package-write authority. It validates the complete publication payload as inert data before checking out with push credentials. The validator rejects extras, symlinks, oversized files, identity drift, Formula changes outsidebottle do, duplicate selected tags, sibling-tag drift, and root/digest/cellar mismatches. Failed attempts use a separate clean tap checkout and record bounded stage outcomes without raw stderr.Additional controls:
repository_dispatchcallers and both repositories atmain;Paired Tap Caller
Automattic/kandelo-homebrew#22 provides the protected
Publish Kandelo bottlesand dry-run callers. Its current head isa7d084da0c2ecd3a526881ce0c4508f8ee6e39b6; its trust check is green.Do not merge or enable that privileged caller until #885 supplies lossless under-lock peer/sibling composition and refreshed Formula-source drift rejection. New GHCR packages are private by default; changing each package to public is a separate explicit approval boundary because anonymous readback must succeed before tap finalization.
Validation
Run through
scripts/dev-shell.shon headbca9393a3:bash scripts/test-homebrew-publish-workflow.sh: passed, including bounded handoffs, control-character rejection, gzip-only enforcement, receipt binding, duplicate/sibling Formula-tag attacks, last-green failure behavior, explicit push refs, and adversarial workflow mutations;ruby scripts/check-homebrew-publish-workflow-trust.rb: passed;actionlinton both reusable Homebrew workflows andstaging-build.yml: passed;npx vitest run host/test/homebrew-public-bottle-verifier.test.ts: 3 passed;b31c5b52e72da1686d8d95cdfe04883e400a273d4cc3d7e15eda95ba5a57183d, 104143 bytes;git diff --check: passed.The complete write publication was not run locally because it requires GitHub-hosted package/tap credentials and the activation gates above. Hosted CI is the evidence for the full runner graph. No ABI bump or snapshot update is required.