ci: require human signoff before a major release - #387
Conversation
🦋 Changeset detectedLatest commit: 9e25007 The changes in this PR will be included in the next version bump. This PR includes changesets to release 0 packagesWhen changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
@jhampton @camrun91 — out of draft and green. Two different asks, so splitting them: @jhampton — the open question is on YPE-2523, not in this diff. The ticket specifies The one thing that would change this diff: I gate on "this PR added a @camrun91 — normal review welcome, particularly on whether the workflow fits how this repo runs CI. Worth knowing, since it is the interesting bit: the gate failed on its own PR when I first pushed it, and the reason was hidden because the script swallowed stderr. Still needs someone with admin: the gate only actually blocks once |
Did you mean to tag @cameronapak? |
cameronapak
left a comment
There was a problem hiding this comment.
Review
Jira: YPE-2523. Spec is the PR body.
Summary
Standards: 2 must-fix findings. Spec: 2 must-fix findings. Worst: the comment-triggered path executes PR-controlled install code with write-capable credentials.
For Agents: review basis
- CI line:
major-release-signoff - Greptile/bot: prior findings were independently verified.
- Event:
issue_comment - HEAD:
fb94946456e826775b6fca1d75aeb2f50d94d829
By Code Reviewer bot, sent on behalf of Cam.
|
suggestion: Prefer GitHub-native approvals over a custom comment-driven release gate. For Agents: simpler release controlA simpler approach would be to assign release maintainers as CODEOWNERS for That removes the The tradeoff is that CODEOWNERS requires approval for every release-bearing changeset rather than only majors, but it directly protects the stated goal with substantially less custom security-sensitive code. By Code Reviewer bot, sent on behalf of Cam. |
Yes, thank you. |
🚨 Breaking change detected — signoff required for
|
|
All three addressed in Privileged workflow executing PR codeSplit into three jobs. The one that runs PR code holds no write scopes; the one that can write statuses and comments never checks that code out.
Workflow-level is now That alone was not enough, though. A same-repo author could still edit
Modified changesets bypassing the gateConfirmed exactly as described, against real commits. Editing a pending changeset from
So the workflow posted "No breaking changeset added; signoff not required" while Changesets computed a real major. Now
Mutable action tagsAll three pinned to full SHAs with the tag in a trailing comment. One more, not in the original reviewA signoff was not bound to the head it was given on. Approve Also fixed: on a preview failure the job used to abort before reaching the step that posts a red status, so a previously-green status could outlive the signoff that earned it. The decision step now records why it cannot evaluate and the red-status and fail steps run under Known residuals
|
…nherit a signoff Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gnt93sVjs6ApXSzkDC2efU
| permissions: | ||
| pull-requests: write | ||
| statuses: write |
There was a problem hiding this comment.
When a same-repository PR modifies this workflow, its pull_request run executes the modified gate job with statuses: write, allowing the PR to publish a successful major-release-signoff status without a qualifying collaborator approval. This bypasses the independent human signoff required for a breaking release.
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/major-release-signoff.yml
Line: 173-175
Comment:
**PR workflow can forge signoff**
When a same-repository PR modifies this workflow, its `pull_request` run executes the modified gate job with `statuses: write`, allowing the PR to publish a successful `major-release-signoff` status without a qualifying collaborator approval. This bypasses the independent human signoff required for a breaking release.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| context: | ||
| # issue_comment fires for issues AND PRs across the repo. Skip non-PR | ||
| # issues so we don't waste a run when someone comments on a plain issue. | ||
| if: ${{ github.event_name != 'issue_comment' || github.event.issue.pull_request != null }} |
| pull-requests: read | ||
| outputs: | ||
| head_sha: ${{ steps.pr.outputs.head_sha }} | ||
| is_fork: ${{ steps.pr.outputs.is_fork }} |
| BASE=$(git merge-base main "$HEAD_SHA") | ||
| node scripts/preview-release.mjs --base "$BASE" --head "$HEAD_SHA" > preview.json | ||
| cat preview.json | ||
| # `introduced_major`, not `is_major`: a major already pending on main |
| ref: ${{ needs.context.outputs.head_sha }} | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Restore release tooling from the base branch |
| runs-on: ubuntu-latest | ||
| permissions: | ||
| pull-requests: write | ||
| statuses: write |
There was a problem hiding this comment.
I get this, but I'm not sure it's something we must guard against. That said, it could happen accidentally, so we need some kind of answer. How about we check the file modifications against the workflows folder and establish a rule that breaking change PRs cannot introduce or change CI behavior, i.e. That would need to be a separate PR. What do you think?
There was a problem hiding this comment.
Worth having, but one catch: on pull_request GitHub runs the workflow file from the PR head, so a PR could delete the new rule in the same commit. Any guard in this file is editable by the PR it guards.
What a PR cannot touch is on your side:
- make
major-release-signoffa required check - "Require review from Code Owners" + a
CODEOWNERSentry for.github/workflows/**
No CODEOWNERS in the repo today. Who should own that path?
Closes YPE-2523 — pending a scope correction, see below.
Ports the Swift SDK's major-release signoff gate (YPE-2521) to this repo. A PR that declares a breaking change cannot merge until a write-access collaborator other than the author confirms it, by name, with the exact version and a 🚀.
The ticket's stated mechanism does not exist here
YPE-2523 says "
semantic-release(current branch) detects a breaking change". This repo has no semantic-release: releases run on Changesets, and the bump level is declared in.changeset/*.mdfrontmatter rather than inferred from commit footers.That is not a new discovery.
docs/release-hardening-decisions.md(Decision 1, jhampton 2026-07-01) already records it:So the gate is the same; only the detector changes.
scripts/preview-release.mjskeeps the Swift name and output contract, but askschangeset statusinstead of@semantic-release/commit-analyzer.One deliberate difference from Swift
Swift gates on "this release is a major". Here that would be wrong. Changesets accumulates changesets until the Version Packages PR ships them, so a major pending on
mainwould make every unrelated PR report major and get blocked until the release went out.The script therefore reports two separate things, and only the second gates:
is_majorintroduced_majormajor(gates)Verified behaviour
Run against real commits on this branch, not reasoned about:
nextis_majorintroduced_majorpatchchangesetmajorchangesetThe last row is the one a straight port would get wrong.
Also confirmed: the three packages are a
fixedgroup in.changeset/config.json, so one major changeset bumps all three to the same3.0.0. That keeps Swift's single-version signoff format valid — worth knowing, because iffixedwere ever removed the signoff comment would become ambiguous about which version is approved. The script fails loudly rather than picking one if it sees more than one version.Carried over from Swift unchanged
The parts that are release-tool-agnostic and were the hard-won bits there:
deleted, so a cachedsuccesscannot outlive its evidence.>quote markers and whitespace, so GitHub's "Quote reply" matches.Not done, needs someone with admin
The workflow only blocks if
major-release-signoffis added as a required status check onmain. I cannot read branch protection (404 without admin). This is what actually made the Swift gate bite, so it should not be skipped.Scope
Asking on the ticket for the description to be corrected before this merges, since it currently specifies a mechanism this repo does not have.
Greptile Summary
This PR adds a Changesets-based major-release approval gate that binds collaborator signoff to the precise release version and full PR head SHA.
Confidence Score: 3/5
This PR is not safe to merge until the status-writing gate executes from a trusted workflow definition that the evaluated PR cannot modify.
A same-repository PR can alter the pull_request-triggered gate job while retaining its status-write token, allowing it to publish a successful major-release-signoff result without an independent collaborator's approval.
Files Needing Attention: .github/workflows/major-release-signoff.yml
Security Review
The privileged gate remains defined by the pull request it evaluates. A same-repository PR can modify the status-writing commands and clear its own required signoff check without independent approval.
How this was verified: The
pull_request-triggered workflow grants status-write access to its inline gate job, which directly publishes the protected status context.Important Files Changed
Sequence Diagram
sequenceDiagram participant A as PR author participant W as PR-defined workflow participant G as Privileged gate job participant S as GitHub status API A->>W: Open internal PR modifying gate commands W->>G: Run on pull_request Note over G: statuses: write G->>S: "Publish major-release-signoff = success" S-->>A: Required gate appears satisfiedPrompt To Fix All With AI
Reviews (5): Last reviewed commit: "fix(ci): match the full commit hash so a..." | Re-trigger Greptile