ci: reconcile dev after hotfix-staging releases#2383
Conversation
The release-semantic.yaml `ff_target` flow skipped its dev reconcile when the promotion source was a hotfix-staging branch, on the (wrong) assumption that patch-id-dedup would collapse the duplicates at the next dev→main promotion. The `chore(release):` commit and its tag have no patch-id equivalent on dev, so they never entered dev's ancestry. The lineage invariant "main is an ancestor of dev after reconcile" silently broke, and the next RC cut on dev (e.g. v1.6.0-rc.1) computed its version floor and changelog against the wrong prior tag — the 1.5.2 hotfix was invisible. Fix the reconcile by rebasing dev onto the new main after a hotfix release. `git rebase` automatically drops dev's originals of the cherry-picked fixes (patch-id match) and replays any unique dev work on top, keeping linear history. Push is `--force-with-lease`; lease failure surfaces a remediation block instead of clobbering concurrent dev pushes. Rebase conflicts fail the job with a recovery script. Add an `Auto-rebase open PRs` workflow that runs on every push to dev (including the rebase-forward push from this fix) and rebases open PRs in-place. Drafts and PRs labeled `no-auto-rebase` are skipped; forks without "Allow edits by maintainers" are listed in the summary; conflicting PRs get a `needs-rebase` label and a comment. Document both flows and the one sanctioned force-push in CLAUDE.md. The new workflow reuses the existing RELEASE_PAT secret rather than introducing a separate token. RELEASE_PAT needs `repo` + `workflow` scopes (classic PAT): `repo` for protected-branch push and PR-head push via maintainer-edit; `workflow` so rebased PRs that include .github/workflows/ changes can be force-pushed without GitHub's "refusing to allow a Personal Access Token to create or update workflow" rejection.
Switch the auto-rebase-PRs workflow from the hand-rolled bash loop to
peter-evans/rebase@v3. The original choice mixed up Option 1 (off-the-
shelf action) and Option 2 (custom workflow with conflict labeling and
fork-permission reporting) from the design discussion — Option 1 was
the intent. Drops:
- ~150 lines of custom shell that iterated PRs, ran rebase in a
worktree per PR, force-pushed with --force-with-lease, applied
`needs-rebase` labels, posted PR comments on conflict.
- The `no-auto-rebase` label exclusion is preserved via the action's
`exclude-labels` input.
- Drafts are still excluded (`exclude-drafts: true`).
- Single-PR mode is preserved by resolving `pr_number` to the
`<owner>:<branch>` head spec the action accepts.
Behavior trade-offs (intentional with the simpler path):
- Conflict-skipped PRs no longer get an auto-applied `needs-rebase`
label or an explanatory comment — the action just skips them. The
workflow summary documents this bucket so reviewers know what's
happening.
- Fork PRs without "Allow edits by maintainers" are silently skipped
(same as before, just not enumerated in the summary).
If conflict-handling visibility becomes important, layer it on as a
follow-up step that compares `rebased-count` vs open-PR count, or
re-introduce the custom loop. CLAUDE.md updated to reflect the new
(simpler) behavior.
Token remains RELEASE_PAT for the same scope reasons — GITHUB_TOKEN
can't push to fork PR head branches and can't force-push commits that
touch .github/workflows/ without the `workflow` scope.
Two valid findings from CodeRabbit on PR #21: 1. auto-rebase-prs.yaml: validate manual `pr_number` is OPEN and targets `dev` before rebasing. Without the guard, a wrong number could rebase and force-push a PR that targets a hotfix-staging branch (or any other base), silently corrupting it. The validation now exits cleanly with a `::warning::` if state != OPEN or baseRefName != dev, and an `if:` guard on the rebase step prevents the empty-head value from falling through to the all-PRs default path when validation rejected the input. 2. release-semantic.yaml: rebase-reconcile push lease failure was returning `exit 0`, so the workflow appeared green while dev remained unreconciled. That weakens the lineage invariant and makes follow-up easy to miss (the next RC on dev would compute against the wrong floor — same class of bug that caused the v1.5.2 hotfix to be invisible). Changed to `exit 1` with an `::error::` so the run goes red and forces a human to read the remediation block. Applied the same `exit 1` treatment to the dev-source FF-reconcile path on line 181 (not CodeRabbit-flagged because it's pre-existing code, but the same logical issue and the same invariant break).
Apply the new concise-comments directive to the workflows added in this PR. Removes ~40 lines of block comments that paraphrased the adjacent code or repeated input/step descriptions. Behavior unchanged.
Apply the present-tense-comments directive. Reframe two comments that
named past state ("pr_number was given but validation rejected it",
"v1.5.2-style miss") to describe the present invariant the code
protects. Behavior unchanged.
Empty commit to trigger CodeQL on this PR. The default-setup CodeQL workflow only fires on synchronize events, not reopened, so a push is required to gate the PR on the new code scan. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR implements automated dev reconciliation for the release promotion workflow. It adds a new auto-rebase workflow for open PRs, updates the release workflow to rebase dev onto main after hotfix promotion, and documents the branch lineage invariants governing these reconciliation behaviors. ChangesRelease promotion and dev reconciliation
Sequence Diagram(s)sequenceDiagram
participant Push as Push to dev
participant Workflow as auto-rebase-prs
participant Validate as Validate PR state
participant Rebase as peter-evans/rebase
participant Summary as Job Summary
Push->>Workflow: trigger (or workflow_dispatch)
Workflow->>Validate: resolve PR head spec
alt single PR mode
Validate->>Validate: check PR is OPEN & base=dev
Validate->>Rebase: pass owner:branch if valid
else all PRs mode
Validate-->>Rebase: skip validation
end
Rebase->>Rebase: rebase onto dev, exclude drafts/no-auto-rebase
Rebase->>Summary: report rebase count & PR details
sequenceDiagram
participant Release as Release trigger
participant Promote as Promote to main
participant FFCheck as Fast-forward check
participant Rebase as Rebase dev
participant Push as Force-with-lease push
participant Summary as Job Summary
Release->>Promote: create release commit
Promote->>FFCheck: can dev FF onto main?
alt FF possible
FFCheck->>Summary: record FF reconcile
else FF not possible
FFCheck->>Rebase: start rebase
Rebase->>Rebase: rebase dev onto main
Rebase->>Push: attempt force-with-lease
alt push succeeds
Push->>Summary: record rebase-forward reconcile
else push-lease rejected
Push->>Summary: record conflict & failure guidance
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 actionlint (1.7.12).github/workflows/release-semantic.yamlcould not read ".github/workflows/release-semantic.yaml": open .github/workflows/release-semantic.yaml: no such file or directory .github/workflows/auto-rebase-prs.yamlcould not read ".github/workflows/auto-rebase-prs.yaml": open .github/workflows/auto-rebase-prs.yaml: no such file or directory 🔧 zizmor (1.25.2).github/workflows/release-semantic.yamlINFO zizmor: 🌈 zizmor v1.25.2 .github/workflows/auto-rebase-prs.yamlINFO zizmor: 🌈 zizmor v1.25.2 🔧 YAMLlint (1.38.0).github/workflows/auto-rebase-prs.yaml[Errno 2] No such file or directory: '.github/workflows/auto-rebase-prs.yaml' .github/workflows/release-semantic.yaml[Errno 2] No such file or directory: '.github/workflows/release-semantic.yaml' 🔧 Checkov (3.2.529).github/workflows/auto-rebase-prs.yaml2026-05-20 10:56:03,412 [MainThread ] [ERROR] Template file not found: .github/workflows/auto-rebase-prs.yaml ... [truncated 9233 characters] ... ocess file .github/workflows/auto-rebase-prs.yaml .github/workflows/release-semantic.yaml2026-05-20 10:56:03,410 [MainThread ] [ERROR] Template file not found: .github/workflows/release-semantic.yaml ... [truncated 9250 characters] ... ess file .github/workflows/release-semantic.yaml 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.
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/release-semantic.yaml:
- Line 178: The current GitHub Actions conditional uses
steps.semantic.outputs.new_release_published to gate the hotfix reconcile, which
prevents reconcile when semantic-release emits no release; update the if
expression to remove the new_release_published check so the job runs whenever
inputs.ff_target is set and steps.validate.outputs.source is not 'dev' or empty
(i.e., keep inputs.ff_target != '' && steps.validate.outputs.source != 'dev' &&
steps.validate.outputs.source != '' and drop
steps.semantic.outputs.new_release_published == 'true'), referencing the
existing inputs.ff_target, steps.semantic.outputs.new_release_published, and
steps.validate.outputs.source symbols to locate and modify the condition.
🪄 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 Plus
Run ID: 3200c561-6f75-4985-be11-e25f892f247c
📒 Files selected for processing (3)
.claude/CLAUDE.md.github/workflows/auto-rebase-prs.yaml.github/workflows/release-semantic.yaml
Summary
Background — why this matters
When v1.5.2 was released via the hotfix-staging path, dev never absorbed the release commit. The next RC cut on dev (v1.6.0-rc.1) computed its version floor and changelog against the prior tag reachable from dev — v1.5.1 — not v1.5.2. The hotfix was invisible to semantic-release.
The old workflow's "Note dev reconcile skipped (hotfix-staging source)" step claimed patch-id-dedup would handle this at the next dev→main promotion. That assumption is wrong: the chore(release): commit has no patch-id equivalent on dev, so it never lands there. The lineage invariant breaks and stays broken.
How the fix works
Secrets / token requirements
RELEASE_PAT is reused for the new workflow (no new secret introduced). Required scopes (classic PAT):
Cherrypicked from Alandtses fork, necessary for ci.
Summary by CodeRabbit