Skip to content

feat: auto-merge release pull requests once a day - #50

Merged
rodrigoscna merged 1 commit into
mainfrom
ci/daily-release-automerge
Aug 27, 2026
Merged

feat: auto-merge release pull requests once a day#50
rodrigoscna merged 1 commit into
mainfrom
ci/daily-release-automerge

Conversation

@rodrigoscna

Copy link
Copy Markdown
Contributor

Summary

Batches releases to one a day instead of one per merge, by keeping Release Please proposing on every push while only a new 09:00 UTC scheduled run turns auto-merge on. Before this change, every merge to main ended up cutting a version: the push run opened or updated the release pull request and enabled auto-merge on it, so it merged as soon as its checks passed. The release pull request now stays open through the day, current with main, and merges once in the morning.

Getting there required changing how the shared workflow finds those pull requests — see Solution, because the obvious version of this merges nothing at all.

Problem

Releases are involuntary. _release-please.yaml turns auto-merge on at the end of every run, and a push to main starts a run, so any merged pull request cuts a version a few minutes later. A day of five merges is five releases, each carrying one change.

That is noise for consumers, who pin exact tags and get a Renovate bump per release. It also produces the race release-please.yaml already documents: five auto-merges landing inside a minute started five overlapping runs, and one died on Label does not exist.

Solution

The push trigger stays, because it is what keeps the release pull request's changelog and version current and pending work visible all day. A new schedule: entry at 09:00 UTC — 06:00 America/Sao_Paulo, the timezone Renovate runs on — is the only trigger that passes auto-merge: true.

flowchart TD
    A[Push to main] --> B[Propose releases]
    B --> C([Release pull request open and current, not merging])
    D[Schedule, 09:00 UTC] --> E[Propose releases]
    E --> F{Open pull request labelled<br/>autorelease: pending?}
    F -- no --> G([Nothing to merge, run green])
    F -- yes --> H[gh pr merge --auto --squash]
    H --> I([Merges once checks pass; that push cuts the tag])
    J[workflow_dispatch] --> K[Propose releases]
    K --> L([Release pull request re-proposed, not merging])
Loading

Auto-merge now finds the pull requests by label, not by the action's prs output. That is the part worth reviewing, because gating the scheduled run on prs would have merged nothing, ever, with no failing run to say so.

  • prs carries only what a run created or updated.
  • When release-please recomputes a release and gets the same answer, it logs remained the same and drops that pull request.
  • The action then sets no prs output at all, rather than an empty array.
  • That is the scheduled run's ordinary case: the last push of the day already wrote the pull request the 09:00 run finds unchanged.

The autorelease: pending label is on the pull request regardless of which run last touched it. --state open is load-bearing rather than tidy — a merged release pull request keeps that label until a later run tags it, so filtering by label alone would turn up one with nothing left to merge.

Two consequences to weigh:

  • The auto-merge input's meaning widened, from the release pull requests a run opens or updates to the open ones however they got there. Not breaking under this repository's definition, since no consumer must edit anything: a caller left on the push-only shape keeps auto-merging its release pull request, and now also does so on runs where release-please rewrote nothing.
  • !inputs.dry-run became load-bearing. It used to be backed up by prs being empty whenever the pull request was skipped. A label lookup finds the standing release pull request either way, so that condition is now the only thing keeping a dry run from merging a real release.

This repository's own caller takes the new shape in the same commit, because it calls _release-please.yaml by path rather than by tag. kanso-ui, unplugin-style-dictionary and home-assistant-applications pin a tag and follow in their own pull requests once this releases as v3.2.0, each bumping its pin and adding the schedule in one commit.

Review Guide

Release Please              (.github/workflows/release-please.yaml)     ← caller: triggers, concurrency, secrets
   │
   └── Release Please       (.github/workflows/_release-please.yaml)    ← reusable: token, propose, auto-merge
                                                                            ↑ the only behavioural change

README.md                                                              ← consumer-facing: the block to copy
AGENTS.md                                                              ← the trap, so the lookup is not simplified back

Focus areas

  • .github/workflows/_release-please.yaml:140-173 — the rewritten auto-merge step. The if: shed two clauses and the step gained a lookup that reaches GitHub rather than reading a step output, so it is the one place a mistake merges something it should not.

Design Decisions

Decision Context
Evaluate github.event_name in the caller, not inside the shared workflow The shared workflow can read the caller's event, so it could have gated auto-merge on schedule itself. That would impose one release policy on all four consumers at once and make auto-merge mean something other than its name. Keeping the expression in the caller leaves the input mechanical and the event unambiguous.
workflow_dispatch proposes without merging Dispatch is the documented lever for re-proposing a release pull request that has to be discarded, not a release-now button. One rule — auto-merge happens on the scheduled run and nowhere else — is easier to reason about than a dispatch input, and releasing sooner is still one click on the pull request.
Keep proposing on push rather than moving the whole run to the schedule Running release-please only daily would leave main carrying merged work with nothing proposed for it between runs. Proposing on push costs nothing and keeps the pending release visible.

Blast Radius

Not behind a feature flag. Merging this changes how this repository releases itself immediately, since its caller runs _release-please.yaml straight from main.

The three tag-pinning consumers are unaffected until they bump to v3.2.0, and unaffected in behaviour even then until they add the schedule — their default auto-merge: true keeps working.

Watch for the first 09:00 UTC run in Actions, and for a release pull request sitting open longer than a day, which is what a schedule that never fired would look like.

Rollback Plan

Revert this pull request via GitHub's Revert button. Nothing is migrated and no tag is moved. If a release pull request is left standing at the time, enable auto-merge on it by hand or merge it directly.

Test plan

Verifiable by agent before merging

  • npm ci, npm run format and npm run lint clean under the Node in .tool-versions (24.20.0)

    > prettier --check .
    Checking formatting...
    All matched files use Prettier code style!
    
  • actionlint 1.7.12 clean over all workflows

    $ actionlint; echo "actionlint exit: $?"
    actionlint exit: 0
    
  • The label lookup returns cleanly against all four repositories, and handles the empty case

    $ gh pr list --repo kanso-labs/github-actions --label 'autorelease: pending' --state open --limit 100 --json number
    []
    
  • The label lifecycle confirms --state open is required — a merged release pull request carries autorelease: tagged, and both labels exist on the consumers

    $ gh pr view 49 --repo kanso-labs/github-actions --json number,state,labels
    {"labels":["autorelease: tagged"],"number":49,"state":"MERGED","title":"chore(main): release 3.1.2"}
    
    $ gh label list --repo kanso-labs/kanso-ui --json name --jq '.[].name' | grep autorelease
    autorelease: pending
    autorelease: tagged
    
  • Canary the gate through a real caller: point a branch in a consumer at _release-please.yaml@ci/daily-release-automerge with the schedule change, dispatch it, and confirm the run proposes the release pull request and skips Enable auto-merge on the open release pull requests. Dry run release-please in test.yaml cannot cover this — it passes no secrets, so CLIENT_ID == '' and the step is skipped for the wrong reason.

Cannot be verified before merge

  • The 09:00 UTC run fires and enables auto-merge on the standing release pull request — a schedule: trigger cannot be dispatched, and GitHub only registers one once the workflow is on the default branch.
  • A push run's log shows the auto-merge step skipped — requires a real push to main after merge.

Auto-merge now finds the release pull requests by their
`autorelease: pending` label instead of the action's `prs` output, which
carries only what a run wrote. That output is unset on a run that finds
the release pull request already correct, which is the ordinary case for
a caller proposing on every push and merging on a schedule.

This repository's own caller takes that shape: it proposes on push and
passes `auto-merge` only on a new 09:00 UTC schedule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant