Repository navigation
feat(release): add Cut Pre-release Tag workflow (#2677) - #3253
Conversation
Pre-release tags (vX.Y.Z-alpha.N / -beta.N / -rc.N) were cut by a manual `git push origin HEAD:refs/tags/<tag>`. That leaves the immutable-tag work in #2677 only partly enforceable: a repo ruleset cannot restrict `v*` tag *creation* to CI while still letting maintainers hand-push pre-releases (fnmatch has no way to match "v* except v*-rc.*"), so "do not hand-cut stable tags" stays a matter of discipline backed only by an in-workflow guard. Add cut-prerelease.yaml — a workflow_dispatch that becomes the sole entry point for creating a pre-release tag: - Generates the CozyStack CI app token (same app as promote-rc.yaml) and pushes the tag as the app, so tags.yaml fires (a tag pushed with the default GITHUB_TOKEN would not trigger it) and the push-event base_ref is populated (tags.yaml's Get-base-branch step needs it). - Validates ^v\d+\.\d+\.\d+-(alpha|beta|rc)\.\d+$ and rejects stable vX.Y.Z — stable tags are cut write-once by the promote flow at a PR merge commit. - Enforces dispatch from main or the matching release-X.Y branch, and refuses to move an existing tag (write-once). - Pushes HEAD:refs/tags/<tag> from the branch tip — the exact form docs/release.md prescribes — on ubuntu-latest (no build runner needed). With every pre-release cut by CI, repo admins can then enable a "Restrict creations" tag ruleset on `v*` with the CI app as the sole bypass actor, turning tag-creation discipline into hard enforcement. Also update tags.yaml's two human-facing error messages and docs/release.md (the RC and patch flows, the workflow list, and a new repo-side tag-ruleset enforcement note) to point at the workflow instead of a manual push. Co-Authored-By: Claude Opus 4.8 <[email protected]> Signed-off-by: Myasnikov Daniil <[email protected]>
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request centralizes the creation of pre-release tags into a dedicated GitHub Actions workflow. By moving this responsibility to a CI-driven process, the repository can now enforce strict tag-creation rules, ensuring that all pre-release and stable tags are generated exclusively by the CI app. This change improves security and consistency in the release process while providing a clear path for enabling repository-wide tag protection. Highlights
New Features🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Ignored Files
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThis PR adds a manual workflow for cutting pre-release tags, updates release documentation and workflow messages to reference it, and pins several workflow actions to fixed commit SHAs. ChangesPre-release Tag Cutting
Workflow Action Pinning
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request updates the release documentation in docs/release.md to reflect the transition from manual tag pushing to using the cut-prerelease.yaml workflow for cutting pre-releases. It details the updated five-workflow CI release process and outlines repo-side enforcement using GitHub tag-protection rulesets to guarantee tag immutability and creation control. I have no feedback to provide on these changes.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/tags.yaml:
- Around line 104-108: The stable-tag rejection message points users to the
wrong workflow path. Update the error text in the hand-pushed stable tag check
to reference the actual stable-tag flow that finalizes `vX.Y.Z` at the merge
commit, not `promote-rc.yaml`; keep the guidance about cutting a pre-release and
promoting it, but make the stable-tag origin explicit using the
`steps.check_release` / `steps.tag` branch in the `Reject hand-pushed stable
tags` step.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 43bedaa6-e8af-4ef0-a3e9-2b9426e9dd4d
📒 Files selected for processing (3)
.github/workflows/cut-prerelease.yaml.github/workflows/tags.yamldocs/release.md
The zizmor "Audit workflows" gate (added in #3223) is red on main: five actions/* references in the release workflows from #3017 are pinned to a floating tag instead of a commit SHA (blanket unpinned-uses policy, High severity): - nightly.yaml (x3) actions/checkout@v4 - promote-rc.yaml actions/checkout@v4 - retention.yaml actions/create-github-app-token@v1 Pin each to the SHA the rest of the repo already standardizes on: - actions/checkout -> 34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - actions/create-github-app-token -> d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 `zizmor --offline .github/workflows/` now reports no findings. Co-Authored-By: Claude Opus 4.8 <[email protected]> Signed-off-by: Myasnikov Daniil <[email protected]>
VerdictLGTM with non-blocking notes Clean, defensively-written CI/docs PR. It adds What was verified (checked, not assumed)
Findings1. [MINOR]
|
Non-blocking review notes from #3253: - Require write access to dispatch. workflow_dispatch already needs write access to trigger, but add an explicit getCollaboratorPermissionLevel gate (admin/maintain/write) as auditable defense-in-depth that fails closed. - Constrain a `main` dispatch to a new minor's pre-release (patch Z == 0). A patch-line pre-release built from main content would produce a wrong-tree release-X.Y.Z-rc.N staging branch; it must be cut from its release-X.Y line. - Guard against a stale checkout tip. workflow_dispatch pins the dispatch-time SHA, not the push-time branch tip (the gap includes the concurrency-queue wait); if the branch advanced, the push would tag a stale commit and yield an empty base_ref, failing tags.yaml after burning the write-once name. Compare local HEAD to the remote branch tip and fail with a re-run hint instead. - Distinguish ls-remote "tag absent" (exit 2) from a transport/auth failure (any other non-zero), so an infra blip cannot read as "free to create". - Shallow checkout (fetch-depth: 1, no fetch-tags) instead of a full-history clone to push one ref; drop the redundant local tag pre-check. - Run pure-input validation (tag + dispatch branch) before minting the token or checking out, so a bad dispatch fails in seconds. actionlint + zizmor clean; run blocks bash -n checked. Co-Authored-By: Claude Opus 4.8 <[email protected]> Signed-off-by: Myasnikov Daniil <[email protected]>
|
Thanks IvanHunters — thorough review, and the finding-3 catch is a good one. Addressed all six plus a write-access gate (pushed as
Separately added a write-access gate: a The runtime path (app-token push, |
IvanHunters
left a comment
There was a problem hiding this comment.
Re-review — all six findings addressed
Verified the fix(ci): address review findings commit against the working tree. Every point from the previous review is resolved, and I re-checked each fix rather than taking it on faith.
| # | Finding | Fix | Verified |
|---|---|---|---|
| 1 | main accepted any version line |
parse now emits the patch digit; a main dispatch is rejected unless Z == 0 |
✅ |
| 2 | Stale-tip race burned the write-once name | Before push, compares local HEAD to git ls-remote origin refs/heads/<branch> and fails "advanced since dispatch, re-run" |
✅ fail-closed |
| 3 | PR-body rationale wrong | Header comment + PR description corrected (rulesets express stable-only via include+exclude; the workflow's real value is CI-locking pre-release creation) | ✅ |
| 4 | ls-remote conflated exit 128 and 2 |
rc handling: 0 = exists, 2 = absent, any other = transport error → refuse |
✅ |
| 5 | Full-history clone | fetch-depth: 1, dropped fetch-tags, removed the redundant local tag pre-check |
✅ |
| 6 | Validation after token mint | Tag parse + dispatch-branch validation now run first; token + checkout only after | ✅ |
Both new run: blocks pass bash -n; tags.yaml and docs/release.md from the original commit are untouched.
On the new write-access gate
Sound defense-in-depth, fail-closed. Two non-blocking notes:
- [NIT]
allowed = ['admin', 'maintain', 'write']— the'maintain'entry is inert. Per the GitHub API,getCollaboratorPermissionLevel's top-levelpermissionfield returns only the legacy base rolesadmin/write/read/none(maintain → write, triage → read). Behavior is still correct (maintainers pass aswrite, triage/read/none are denied); the string just never matches and reads as if it distinguishes a role the field can't return. - [Runtime, already disclosed] The gate depends on the app-token installation being able to read collaborator permission. If it can't, the API call throws and the step fails closed — for every dispatcher, not just unauthorized ones — until the permission is granted. That's the safe failure direction, and the PR already flags the runtime path as unproven until the first real dispatch; worth confirming on that first run.
- [NIT] The stale-tip guard's
git ls-remote … | cut -f1masksls-remote's exit code (noset -o pipefail), so a transport blip surfaces as "Could not resolve the tip" rather than a distinct transport error — slightly inconsistent with the deliberate exit-code distinction added just above for the write-once check. Fail-closed either way.
Verdict
LGTM. All prior findings are correctly resolved with no new defects; what remains is one inert list entry and one disclosed runtime dependency of the new gate, none blocking. The core mechanism still follows the repo's documented convention, and the honest "unproven until first dispatch" caveat stands. Nice, tight follow-up.
Cosmetic follow-ups from the re-review (all fail-closed, no behavior change): - Write-access gate: drop the inert 'maintain' from the allowed list. getCollaboratorPermissionLevel's `permission` field returns only the legacy roles admin/write/read/none (maintain collapses to write), so admin+write is the full "can push" set; 'maintain' never matched. - Stale-tip guard: resolve the remote tip without a pipe masking ls-remote's exit code, mirroring the write-once query — a transport failure now reports a distinct error instead of "branch not found". - tags.yaml: the hand-pushed-stable rejection message now says stable tags are finalized by the promote flow at the merge commit (pull-requests-release.yaml), not "come from promote-rc.yaml" — promote-rc opens the PR; finalize cuts the tag. Co-Authored-By: Claude Opus 4.8 <[email protected]> Signed-off-by: Myasnikov Daniil <[email protected]>
|
IvanHunters thanks again for the re-review. Addressed the two remaining nits:
The push dismissed your approval — mind re-approving when you get a chance? Only those two nits changed since your LGTM. CI is re-running. |
IvanHunters
left a comment
There was a problem hiding this comment.
Re-approve — polish commit verified
Checked 4a2e4396 fix(ci): polish cut-prerelease per re-review nits against the tree. Both remaining nits are resolved correctly, and there's a welcome bonus:
'maintain'inert entry — dropped;allowedis now['admin', 'write'], with a comment stating why (the legacypermissionfield collapses maintain→write, triage→read). Matches the GitHub API behavior exactly.- stale-tip guard exit-code masking — the remote-tip resolution now captures
git ls-remote's exit code (set +e/rc=$?) and fails distinctly onrc != 0(transport/auth) before thecut+ empty-output ("no such branch") check. Consistent with the write-once query above it; a network blip can no longer read as "branch not found". - Bonus:
tags.yaml's "Reject hand-pushed stable tags" message now reads "Stable tags are finalized by the promote flow at the merge commit" instead of "come from promote-rc.yaml" — the pre-existing wording nit from the original review.
Both run: blocks pass bash -n; diff scope is unchanged (only the expected workflow + docs files). No regressions, no new defects — the changes are strictly a subset of improvements over the state I already approved.
LGTM. Nothing left open across all three review passes. Ship it.
What this PR does
Adds
cut-prerelease.yaml— aworkflow_dispatchthat becomes the sole entry point for creating a pre-release tag (vX.Y.Z-alpha.N/-beta.N/-rc.N), replacing the manualgit push origin HEAD:refs/tags/<tag>.Why
#2677 makes stable tags immutable, but tag creation was still only convention-enforced for pre-releases. Repo rulesets can already restrict stable
vX.Y.Zcreation to CI — a "Restrict creations" ruleset targetingv*with an exclude pattern forv*-*covers stable tags without any new workflow. What that approach cannot cover is pre-releases:-rc/-alpha/-betatags were hand-pushed, so they can't be locked to CI while they still have a human entry point.This workflow closes that gap by making CI the sole creator of pre-release tags too. With it in place, an admin can extend "Restrict creations" (CI app as sole bypass) to all of
v*— pre-release and stable alike — instead of only the stable subset.(Correction: an earlier version of this description claimed a ruleset couldn't express "stable-only" creation. It can, via include + exclude patterns — thanks IvanHunters. The workflow's real value is extending creation-control to pre-release tags, plus the audit trail, single entry point, guaranteed
base_ref, and app-identity push that makestags.yamlfire.)How it works
promote-rc.yaml) and pushes the tag as the app — sotags.yamlfires (a tag pushed with the defaultGITHUB_TOKENwould not) and the push event'sbase_refis populated (tags.yaml's Get base branch step requires it).^v\d+\.\d+\.\d+-(alpha|beta|rc)\.\d+$and rejects stablevX.Y.Z— stable tags are cut write-once by the promote flow at a PR merge commit, never here.workflow_dispatchalready requires it; the explicit check is auditable defense-in-depth that fails closed).main(new minor'svX.Y.0-*only) or the matchingrelease-X.Ybranch; write-once (refuses an existing tag) with a stale-tip guard so a branch that advanced since dispatch fails cleanly instead of burning the tag name.HEAD:refs/tags/<tag>from the branch tip — the exact formdocs/release.mdprescribes. Runs onubuntu-latest(no build runner needed).Also updates
tags.yaml's two human-facing error messages anddocs/release.md(the RC and patch flows, the workflow list, and a new repo-side tag-ruleset enforcement note) to point at the workflow instead of a manual push. Completes SHA-pinning on the touched release workflows (nightly.yaml,promote-rc.yaml,retention.yaml) — the zizmor "Audit workflows" gate was red onmainfor those.Rollout ordering
The two
v*tag rulesets have a hard order (now documented indocs/release.md):v*tag.Validation
actionlint— clean.act -l— job graph resolves (cut/workflow_dispatch).zizmor --offline— no findings across the whole workflows dir (SHA-pinned actions, least-privilegecontents: read).bash -nsyntax-checked.base_refpopulation, thegetCollaboratorPermissionLevelgate) needs a live run with the CI secrets — the same "orchestration is unproven until it runs" caveat feat(release): immutable tags & rc→stable promotion (#2677) #3017 carried.Review follow-up
Addressed the non-blocking findings from IvanHunters' review (commit
fix(ci): address review findings):maindispatch now requires patchZ == 0(a new minor's pre-release), so a patch-line rc can't be cut frommaincontent.base_reffailure.ls-remotenow distinguishes "tag absent" (exit 2) from a transport/auth failure.fetch-depth: 1, nofetch-tags); dropped the redundant local tag pre-check.Plus a write-access gate on the dispatcher, requested separately.
Part of #2677.
Release note
🤖 Generated with Claude Code
Summary by CodeRabbit
v*tags.