ci(release-pr): open release PRs on push to main - #81
Conversation
git-cliff bumps only for feat, fix and breaking changes, so most merges to main propose nothing and the run ends quietly. A manual dispatch still fails loudly, where silence would read as success. Two guards come with the trigger. Merging a release PR is itself a push, and the tag does not exist until release-tag.yml clears its environment approval, so a run is skipped for a release-prep head commit and refused while any prepared version is still untagged. Separately, release-tag.yml derives the tag it mints from the release/* branch name, so a version that moves between runs leaves a stale PR that would tag the wrong commit; the new run closes it. Resolution moves into its own job so that nothing-to-release shows as a skipped job rather than a green one that did nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #81 +/- ##
==========================================
+ Coverage 47.93% 47.98% +0.05%
==========================================
Files 31 31
Lines 1863 1863
==========================================
+ Hits 893 894 +1
+ Misses 970 969 -1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
PR Summary by QodoOpen release PRs automatically on pushes to main
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
Code Review by Qodo
1.
|
There was a problem hiding this comment.
🟡 Changes recommended
The workflow introduces a likely-invalid inputs.version reference on push runs and a cleanup step that can close unrelated release/* PRs unless further constrained.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR updates the release automation to open release preparation PRs automatically on pushes to main, while keeping manual dispatch semantics for “nothing to release,” and documents the new behavior in CONTRIBUTING.
Changes:
- Adds a
pushtrigger onmainand splits the workflow into a read-onlyresolvejob and a gatedrelease-prjob. - Implements guards for “nothing releasable,” “release PR just merged,” and “prepared-but-untagged release in flight,” with different behavior for push vs dispatch.
- Adds a post-create cleanup step to close superseded
release/*PRs, and updates release documentation accordingly.
File summaries
| File | Description |
|---|---|
CONTRIBUTING.md |
Documents the new auto-opening release PR behavior and the new guards/cleanup semantics. |
.github/workflows/release-pr.yml |
Adds push trigger, splits into resolve + release-pr, and closes superseded release PRs after creating a new one. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The pending-release lookup appended `|| true` to `git log`, turning any real failure into an empty `prep` value. That reads as "no release in flight", so the untagged-release guard below is skipped and a second PR is prepared for the release already on its way out. Finding nothing already exits 0 and prints nothing, so the suppression could never have been load-bearing for the no-match case -- it only ever caught genuine errors. Dropping it lets `set -euo pipefail` stop the step. Reported by Qodo on #81. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The superseded-release cleanup selected PRs by `headRefName` alone, so any open PR against main whose branch starts with `release/` was in scope -- including one from a fork. The next release this workflow prepared would close that contributor's PR as superseded, comment that it was replaced, and point `--delete-branch` at their branch. release-tag.yml already treats a release branch as trusted only when its head repo is this repo. Filtering the listing on `isCrossRepository` puts the cleanup on that same footing. Reported by Qodo and Copilot on #81. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 99708db |
|
[Review-Convergence] Round 1: active
Round 1 dispositions
|
There was a problem hiding this comment.
🟢 Approval recommended
The workflow changes are well-guarded for push vs dispatch behavior and safely constrain cleanup to same-repo release/* PRs.
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 0 new
- Review effort level: Lite
|
[Review-Convergence] Round 2: converged
Clean signal evidence
Codex never engaged with this PR, so it was not eligible for re-request; quorum was met from the two engaged families. This runner does not merge, approve, or resolve human threads. Merge policy is yours to decide. |
Opens the release PR automatically instead of waiting for someone to run the workflow by hand. This is the one thing release-please would have added over what this repo already does; adopting it outright would have cost the GPG-signed annotated tag, the
releaseenvironment gate, and thecliff.tomlchangelog format.What changed
.github/workflows/release-pr.ymlgainspush: branches: [main]alongside the existing dispatch, split into two jobs.resolve(read-only permissions) decides whether there is anything to do:::notice::, job endschore(release): prepare …::notice::, job endsrelease-prruns only whenresolvesays so. It is unchanged except for a new final step that closes superseded same-reporelease/*PRs.CONTRIBUTING.mdis updated to match: the release flow now starts by itself on a releasable merge, and the two guards below are written down where someone running a release will find them.Why each guard exists
feat,fixand breaking changes, sochore,docsandbuildmerges — the weekly hook autoupdate and Dependabot among them — propose nothing. That has to end quietly on a push. A dispatch is a human asking for a release, where silence would read as success, so that path still fails loudly.main, and the tag does not exist at that moment —release-tag.ymlis waiting on thereleaseenvironment approval. Unguarded, the merge would immediately propose a second PR for the release just merged.fixlanding while Release Tag waits would get a duplicate PR for a version already on its way out. Refusing while any prepared version is untagged covers it. This deliberately also latches when an approval is rejected; the way out is the manual tag-push fallback already documented in CONTRIBUTING, which both publishes that release and clears the check. The lookup behind that guard is deliberately unsuppressed — a failedgit logmust stop the step rather than read as "nothing prepared" and wave the release through (18b887e).release-tag.ymlderives the tag it mints from that name — deliberately, since a same-reporelease/*branch is not attacker-controlled the way a title or body is. So the branch cannot be made stable, and a version that moves between runs (afeaton top of a pending patch) leaves a stale PR whose merge path would tag the wrong version. The new run closes it. Cleanup is scoped to same-repo branches for the same reasonrelease-tag.ymltrusts only those: a fork PR whose head happens to start withrelease/is not part of this release process and must not be closed by it (99708db).What this does not change
No new path to a tag or a published artifact. A release still takes the same two human actions: merging the release PR, then approving the Release Tag run in the protected
releaseenvironment. The release PR is also outsidebot-automerge.yml's author allowlist (dependabot[bot],pre-commit-ci[bot]), so it will not merge itself — #72's checks showbot-automerge SKIPPED.Test plan
Both shell blocks were extracted from the YAML and executed, rather than reasoned about.
resolve, 9 scenarios — push and dispatch × releasable and not; explicit and malformedversioninput; and an in-flight release simulated in a scratch clone (an untaggedchore(release): prepare v9.9.9with afixon top), including that the guard clears once the tag appears. All as specified: push exits 0 withrelease=false, dispatch exits 1.gh— supersession, nothing to close, empty list, and a failinggh pr list, which exits non-zero rather than reading as "nothing superseded". That fail-closed shape follows the convention already written intobot-automerge.yml.actionlintclean — it caught an SC2129 and yamllint caught a >100-char line during drafting; both fixed.make checkgreen (8 unit suites, 3 integration, pre-commit across all files).Two fixes landed after the first review round, so their evidence is separate from the extracted-block runs above:
make checkgreen before each of 18b887e and 99708db;actionlintandcheck-github-workflowsclean on both.git log -1 --grepwith no matching commit already exits 0 and prints nothing, so the removed|| truecould only ever have masked a real failure.gh pr listpayload: of a same-reporelease/*PR, two forkrelease/*PRs and a same-repo non-release PR, only the first survives thejqselection.Note on merging
The first push to
mainafter this lands will openrelease/v0.1.2, because #79's fix is sitting unreleased. That is the feature working, not a surprise.🤖 Generated with Claude Code
Closes #82 (review-convergence bulletin)