Skip to content

ci: verify signed multi-channel releases before publication - #66

Merged
steipete merged 1 commit into
mainfrom
audit/release-pipeline-20261012
Oct 12, 2026
Merged

steipete merged 1 commit into
mainfrom
audit/release-pipeline-20261012

Conversation

@steipete

Copy link
Copy Markdown
Contributor

Build and verify each release channel from one immutable source. Signed tags require protected-main ancestry, green push CI, and dated changelog notes. macOS binaries are signed and notarized, then those same bytes are packaged in npm and independently verified on arm64 and Intel before publication. The obsolete separate npm promotion workflow is removed.

The release workflow defaults to a no-publish dispatch rehearsal. npm uses OIDC and retains its existing trusted-publisher binding; GitHub assets include checksums and exact notes, with Homebrew chained afterward. Existing versions are refused, and bounded metadata polling accommodates publish-time scans without a second publish attempt.

Validation: package tests and six-platform archive-to-npm smoke passed on a clean Linux runner; actionlint and independent Codex review passed. No release or version tag was created.

@clawsweeper

clawsweeper Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

@steipete
steipete requested review from a team and Patrick-Erichsen as code owners October 12, 2026 03:13
@clawsweeper clawsweeper Bot added P2 Normal priority bug or improvement with limited blast radius. merge-risk: 🚨 automation 🚨 Merging this PR could break CI, automerge, proof capture, label sync, or automation. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Oct 12, 2026
@clawsweeper

clawsweeper Bot commented Oct 12, 2026

Copy link
Copy Markdown

Codex review: needs real behavior proof before merge.

What this changes

This PR consolidates release publication and adds signed-tag checks, macOS signing and notarization, shared npm payloads, and native verification.

Example: Publish an authorized v0.2.1 release.

  • Before: GitHub archives and npm binaries are built separately without macOS signing verification.
  • After: The workflow packages the signed Darwin binaries into npm and compares their bytes on arm64 and Intel before publication.

Review scores

Measure Result What it means
Overall readiness 🧂 unranked krab (1/6) The release design has value, but credential exposure, incomplete final-effect authorization, and absent authority evidence prevent landing.
Proof confidence 🧂 unranked krab (1/6) Real behavior proof is necessary before merge. See Before merge.
Patch quality 🧂 unranked krab (1/6) Security review found an item that needs attention.

Product

Kind: Feature · Worth it: Yes
User problem: Release consumers need consistent, authenticated binaries across download and package channels.
Reason: The supplied maintainer-authored direction authorizes signing and consolidated publication, and the shared-payload design has clear supply-chain value.

Merge readiness

⛔ Blocked before merge - 7 items remain

Keep this PR open: the release hardening is absent from main, but signing credentials and publication still have actionable authorization gaps.

Priority: P2
Reviewed head: 152afda896b455f45b99f9edb7f590d6ee38caa9

Before merge

  • Add real behavior proof - Authority-chain proof required: supply redacted workflow logs showing an allowed signer reaching signing and the publication boundaries, a repository writer without that signing authority rejected before credential use, and deleted or reassigned tags rejected before npm and tap effects; include native arm64 and Intel signature verification and archive-to-npm byte comparison, since the reported Linux smoke supplies no evidence for those changed paths. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
  • Authorize the release before executing tag-supplied code (P1) - A repository writer without the allowed signing key can push a v-prefixed tag containing modified preflight or test code. Checkout selects that tag, executes its files before establishing authorization, and later provides the new signing and notarization secrets to a script from the same checkout. Replacing the guard lets that script consume or disclose the credentials without a valid signed release. The retrieved repository ruleset protects main from deletion and force pushes, not this tag execution path. Establish authorization using protected workflow code or an enforced release credential boundary before executing candidate scripts or releasing these secrets.
  • Revalidate the live tag before publishing to npm (P1) - If an authorized tag is deleted or retargeted while build and native verification run, the npm job still publishes its tarball because it receives only the tag name and performs no live tag-object or commit comparison. The check in the subsequent GitHub publication job is too late: npm publication is irreversible and the channels can diverge. Pass the pinned tag object and commit into npm and verify the current source reference immediately before npm publish.
  • Bind the Homebrew handoff to the verified release payload (P2) - The new all-channel verification claim does not reach Homebrew: this dispatch passes only a tag and filename template. The tap consequently uses legacy mode, downloads whatever occupies those release URLs, and calculates fresh hashes. Replacing an asset between GitHub publication and tap processing therefore installs a checksum for bytes this workflow never verified, while tag reassignment is not checked before the tap push. Use the tap's existing verified-hashes-v1 inputs with the four verified archive hashes and pinned source tag identity, including its required public-byte verification around the handoff.
  • Give each Homebrew retry a distinct request identifier (P2) - Rerunning the Homebrew job retains GITHUB_RUN_ID, so the request identifier is reused. Before the new downstream dispatch appears, the polling query can immediately find the previous failed tap run and gh run watch returns its failure instead of waiting for the new attempt. Include GITHUB_RUN_ATTEMPT in the identifier and match the intended dispatch exactly so a retry tracks its own result.
  • Resolve security concern: Signing credentials become available to candidate-controlled scripts - The newly introduced Developer ID and App Store Connect credentials are reusable authority; the signature guard runs from the same candidate tree it is intended to authorize.
  • Resolve security concern: Publication can outlive the checked release reference - npm lacks the later GitHub job's source-identity revalidation, and the Homebrew handoff discards verified identity and hashes before its downstream filesystem and push effects.

Findings

  • [P1] Authorize the release before executing tag-supplied code — .github/workflows/release.yml:33-38
  • [P1] Revalidate the live tag before publishing to npm — .github/workflows/release.yml:175-178
  • [P2] Bind the Homebrew handoff to the verified release payload — .github/workflows/homebrew-release.yml:40-46
  • [high] Signing credentials become available to candidate-controlled scripts — .github/workflows/release.yml:65
  • [medium] Publication can outlive the checked release reference — .github/workflows/release.yml:178

Provenance

  • Homebrew dispatch: original intent not found (83a2d90: History records release deployment work; git show could not read an older blob and the commit-to-PR lookup returned no associated PR explaining the original fallback choices.)

Tests

  • Missing end-to-end proof: No supplied workflow evidence demonstrates authorized signing, rejection of an unauthorized tag before credential use, or invalidation of withdrawn tag authority before publication.
Agent review details

How this fits together

The release automation takes a source reference and credentials, builds CLI packages, and publishes them to npm, GitHub Releases, and the Homebrew tap.

flowchart TD
  A[Source reference] --> B[Release authorization]
  B --> C[Build and sign binaries]
  C --> D[Package and verify payloads]
  D --> E[Publish npm package]
  E --> F[Publish GitHub assets]
  F --> G[Update Homebrew tap]
Loading

Technical review

Best possible solution:

Authorize releases from trusted workflow code and carry the verified tag identity and payload hashes through every publication channel.

Do we have a high-confidence way to reproduce the issue?

Source inspection establishes the credential and retry paths; no target code was executed, and no live exploit or release was attempted.

Is this the best way to solve the issue?

Building once and comparing signed payloads is a useful approach, but authorization must precede untrusted execution and remain valid at each final publication effect.

Full review comments:

  • [P1] Authorize the release before executing tag-supplied code — .github/workflows/release.yml:33-38
    A repository writer without the allowed signing key can push a v-prefixed tag containing modified preflight or test code. Checkout selects that tag, executes its files before establishing authorization, and later provides the new signing and notarization secrets to a script from the same checkout. Replacing the guard lets that script consume or disclose the credentials without a valid signed release. The retrieved repository ruleset protects main from deletion and force pushes, not this tag execution path. Establish authorization using protected workflow code or an enforced release credential boundary before executing candidate scripts or releasing these secrets.
    Confidence: 0.96
  • [P1] Revalidate the live tag before publishing to npm — .github/workflows/release.yml:175-178
    If an authorized tag is deleted or retargeted while build and native verification run, the npm job still publishes its tarball because it receives only the tag name and performs no live tag-object or commit comparison. The check in the subsequent GitHub publication job is too late: npm publication is irreversible and the channels can diverge. Pass the pinned tag object and commit into npm and verify the current source reference immediately before npm publish.
    Confidence: 0.98
  • [P2] Bind the Homebrew handoff to the verified release payload — .github/workflows/homebrew-release.yml:40-46
    The new all-channel verification claim does not reach Homebrew: this dispatch passes only a tag and filename template. The tap consequently uses legacy mode, downloads whatever occupies those release URLs, and calculates fresh hashes. Replacing an asset between GitHub publication and tap processing therefore installs a checksum for bytes this workflow never verified, while tag reassignment is not checked before the tap push. Use the tap's existing verified-hashes-v1 inputs with the four verified archive hashes and pinned source tag identity, including its required public-byte verification around the handoff.
    Confidence: 0.96
  • [P2] Give each Homebrew retry a distinct request identifier — .github/workflows/homebrew-release.yml:31
    Rerunning the Homebrew job retains GITHUB_RUN_ID, so the request identifier is reused. Before the new downstream dispatch appears, the polling query can immediately find the previous failed tap run and gh run watch returns its failure instead of waiting for the new attempt. Include GITHUB_RUN_ATTEMPT in the identifier and match the intended dispatch exactly so a retry tracks its own result.
    Confidence: 0.99

Overall correctness: patch is incorrect
Overall confidence: 0.96

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning medium; reviewed against 128e69644488.

Provenance checked

  • Separate npm promotion and existing-version handling changes intended behavior with a stated reason (50aa4b9: The previous workflow gated promotion and verified prepared artifacts; the current body explicitly chooses one release owner and refuses existing versions.)
  • Registry propagation polling keeps the original intent (ci: allow npm registry processing before verification #61: An observed successful publish remained invisible longer than the former polling window.)
  • Changelog-backed release notes changes intended behavior with a stated reason (chore(release): prepare v0.2.0 #60: GitHub publication followed npm and rendered the selected changelog with validated registry metadata; this PR deliberately switches to exact changelog notes and separately checks npm integrity.)

Testing

Proof path: unit tests only.

Security

Needs attention: The signing and publication chain does not consistently enforce release authority before credential use and final effects.

Evidence

Security concerns:

  • [high] Signing credentials become available to candidate-controlled scripts — .github/workflows/release.yml:65
    The newly introduced Developer ID and App Store Connect credentials are reusable authority; the signature guard runs from the same candidate tree it is intended to authorize.
    Confidence: 0.96
  • [medium] Publication can outlive the checked release reference — .github/workflows/release.yml:178
    npm lacks the later GitHub job's source-identity revalidation, and the Homebrew handoff discards verified identity and hashes before its downstream filesystem and push effects.
    Confidence: 0.98

What I checked:

  • Verified introduced change: The pinned main-to-head diff introduces the signing and verification pipeline; current main retains separate archive and npm builds. (.github/workflows/release.yml:23, 152afda896b4)
  • Signing trust boundary: The build checks out the triggering reference, executes its Python files, and subsequently supplies signing and notarization secrets to its shell script; there is no protected environment on this job. (.github/workflows/release.yml:33, 152afda896b4)
  • Repository protection: The retrieved active ruleset targets main and prevents deletion and non-fast-forward updates; the ruleset listing contains no tag protection establishing the proposed signed-release boundary.
  • Publication ordering: npm publication occurs at line 178; the live tag-object and commit comparisons occur only in the later GitHub publication job. (.github/workflows/release.yml:178, 152afda896b4)
  • Executed dependency contract: This PR dispatches this repository's update-formula workflow, making its handoff contract applicable: README lines 174–188 describe verified hashes and source identity, while legacy mode recomputes hashes from downloaded assets. (README.md:174, 9f817361b3c7)
  • Accepted publication tradeoffs: The maintainer-authored body and RELEASING.md explicitly retire separate npm promotion and refuse existing versions; these intentional choices are not findings. (RELEASING.md:12, 152afda896b4)

Likely related people:

  • Peter Steinberger: Raw commit 6432c40 adds scripts/build-npm-package.mjs:171 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: high; commits: 6432c40f2340; files: scripts/build-npm-package.mjs)
  • Patrick Erichsen: Raw commit f178c86 adds scripts/build-npm-package.mjs:29 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: high; commits: f178c861b898; files: scripts/build-npm-package.mjs)
  • Vincent Koc: Raw commit ca811e0 adds scripts/build-npm-package.mjs:105 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: high; commits: ca811e0e73ce; files: scripts/build-npm-package.mjs)
  • Jesse Merhi: Raw commit e63bacb adds scripts/build-npm-package.mjs:32 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: medium; commits: e63bacb73e8e; files: scripts/build-npm-package.mjs)

Labels

Label changes:

  • add P2: This changes release infrastructure without establishing an urgent current-main runtime regression.
  • add merge-risk: 🚨 security-boundary: The PR introduces signing credentials into tag-selected code and leaves publication authorization stale at npm.
  • add merge-risk: 🚨 automation: The changed Homebrew retry identification can select an earlier failed dispatch instead of the new attempt.
  • add rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🧂 unranked krab.
  • add status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Label justifications:

  • P2: This changes release infrastructure without establishing an urgent current-main runtime regression.
  • merge-risk: 🚨 security-boundary: The PR introduces signing credentials into tag-selected code and leaves publication authorization stale at npm.
  • merge-risk: 🚨 automation: The changed Homebrew retry identification can select an earlier failed dispatch instead of the new attempt.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🧂 unranked krab.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Rating scale

6/6 🦀 challenger crab · 5/6 🦞 diamond lobster · 4/6 🐚 platinum hermit · 3/6 🦐 gold shrimp · 2/6 🦪 silver shellfish · 1/6 🧂 unranked krab. Overall follows the weaker of proof and patch quality; ✨ marks media proof (a screenshot, video, or linked artifact) that directly shows the changed behavior.

Workflow

ClawSweeper edits this one comment on every review. Comment @clawsweeper re-review for a fresh review only. Repair and merge execution lanes are retired.

Reviewed October 11, 2026, 11:17 PM ET / October 12, 2026, 03:17 UTC.

@steipete
steipete merged commit 036679b into main Oct 12, 2026
10 checks passed
@steipete
steipete deleted the audit/release-pipeline-20261012 branch October 12, 2026 03:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 automation 🚨 Merging this PR could break CI, automerge, proof capture, label sync, or automation. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. P2 Normal priority bug or improvement with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant