Release on merge: tag-release promotes Unreleased and tags it #416

Merged
robbertbos merged 1 commit from auto-release into main 2026-09-23 20:34:16 +00:00
Owner

A new tag-release workflow releases every push to main that has something under ## [Unreleased]. The rules are in docs/deployment.md > "Cutting a release".

Under Unreleased Route
entries release: promote to ## [vYYYY.M.D] (.N for a second release that day), commit as Waggle Release Bot, tag, push
<!-- release: hold --> or ### Note when rolling out stage: no release; publish-and-stage builds and deploys staging
nothing, or no Unreleased heading at all, with the newest ## [vX] heading not yet tagged tag: a release cut by hand in a PR gets its tag on merge
nothing else none
  • Staging only under a hold. publish-and-stage now builds and deploys staging only when the push is held.
  • Production path unchanged. release-and-deploy.yaml is untouched: the tag builds, releases and deploys production.
  • Only a PR lifts a hold. There is no manual trigger, and release-promote.sh refuses a section that still carries the marker. A failed run can be re-run from the Actions tab; it stops on its own if main has moved on.

Scripts

  • release-decide.sh picks the route. It reuses changelog-section.sh, validate-calver-tag.sh and the new newest-calver-tag.sh; assert-newest-calver-tag.sh now uses that last one too.
  • release-promote.sh rewrites the CHANGELOG (section, fresh Unreleased, both links). It renames unreleased.md only when it has content, and removes it when it is empty.
  • .forgejo/known_hosts pins the host key the push checks.

Tests. test-release-scripts.sh covers every route, the .N numbering, the hold refusal and the in-app notes. Everything passes in a clean container, and changelog-frozen.sh passes on the real history. Locally, the existing tag-based cases fail on this host only because tag.gpgSign is set globally.

Push identity

  • The push uses the RELEASE_PUSH_KEY deploy key. The automatic token cannot push to a protected main (forgejo#11159), and a tag it pushes starts no workflow. A deploy-key push counts as the repository owner's (routers/private/serv.go, v15), so the tag does start release-and-deploy.
  • Commits come from Waggle Release Bot <release-bot@waggle.rijksapp.nl>. That domain publishes a null MX, so no account can claim the address. The commits are unsigned; "Require signed commits" is off.

This PR lands under a hold, so merging it only stages. The first real release comes from the PR that removes the marker.

Open points

  • SSH from the runner. The host key (RSA, SHA256:vcXtpeDOqkTc5F466dN5PiTAAAM+J+vU1A6ejGkpX20) was read from mybee, trust on first use. Whether the runner reaches port 22 shows on the first real run.
  • Every release makes open PRs outdated. With "Block merge if pull request is outdated" on, each open PR then needs "Update branch" and a fresh CI run.
  • Branch protection is set by hand, not in this PR: required checks CI / * and test-build / *, "Enforce this rule for repository admins", "Block merge if pull request is outdated", and "Whitelist restricted push" with only deploy keys.
A new `tag-release` workflow releases every push to `main` that has something under `## [Unreleased]`. The rules are in `docs/deployment.md` > "Cutting a release". | Under Unreleased | Route | |---|---| | entries | **release**: promote to `## [vYYYY.M.D]` (`.N` for a second release that day), commit as Waggle Release Bot, tag, push | | `<!-- release: hold -->` or `### Note when rolling out` | **stage**: no release; `publish-and-stage` builds and deploys staging | | nothing, or no Unreleased heading at all, with the newest `## [vX]` heading not yet tagged | **tag**: a release cut by hand in a PR gets its tag on merge | | nothing else | none | - **Staging only under a hold.** `publish-and-stage` now builds and deploys staging only when the push is held. - **Production path unchanged.** `release-and-deploy.yaml` is untouched: the tag builds, releases and deploys production. - **Only a PR lifts a hold.** There is no manual trigger, and `release-promote.sh` refuses a section that still carries the marker. A failed run can be re-run from the Actions tab; it stops on its own if `main` has moved on. **Scripts** - `release-decide.sh` picks the route. It reuses `changelog-section.sh`, `validate-calver-tag.sh` and the new `newest-calver-tag.sh`; `assert-newest-calver-tag.sh` now uses that last one too. - `release-promote.sh` rewrites the CHANGELOG (section, fresh Unreleased, both links). It renames `unreleased.md` only when it has content, and removes it when it is empty. - `.forgejo/known_hosts` pins the host key the push checks. **Tests.** `test-release-scripts.sh` covers every route, the `.N` numbering, the hold refusal and the in-app notes. Everything passes in a clean container, and `changelog-frozen.sh` passes on the real history. Locally, the existing tag-based cases fail on this host only because `tag.gpgSign` is set globally. **Push identity** - The push uses the `RELEASE_PUSH_KEY` deploy key. The automatic token cannot push to a protected `main` (forgejo#11159), and a tag it pushes starts no workflow. A deploy-key push counts as the repository owner's (`routers/private/serv.go`, v15), so the tag does start `release-and-deploy`. - Commits come from `Waggle Release Bot <release-bot@waggle.rijksapp.nl>`. That domain publishes a null MX, so no account can claim the address. The commits are unsigned; "Require signed commits" is off. **This PR lands under a hold**, so merging it only stages. The first real release comes from the PR that removes the marker. **Open points** - **SSH from the runner.** The host key (RSA, `SHA256:vcXtpeDOqkTc5F466dN5PiTAAAM+J+vU1A6ejGkpX20`) was read from mybee, trust on first use. Whether the runner reaches port 22 shows on the first real run. - **Every release makes open PRs outdated.** With "Block merge if pull request is outdated" on, each open PR then needs "Update branch" and a fresh CI run. - **Branch protection is set by hand, not in this PR:** required checks `CI / *` and `test-build / *`, "Enforce this rule for repository admins", "Block merge if pull request is outdated", and "Whitelist restricted push" with only deploy keys.
Release on merge: promote Unreleased and tag from publish-and-stage
Some checks failed
CI / release-scripts (pull_request) Successful in 11s
security-scan / SBOM (trivy) (pull_request) Successful in 11s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / Python SAST (bandit) (pull_request) Successful in 22s
security-scan / JS SCA (npm audit) (pull_request) Successful in 35s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 38s
test-build / build (backend) (pull_request) Successful in 1m9s
test-build / build (frontend) (pull_request) Successful in 1m16s
test-build / build (pull_request) Successful in 0s
CI / pre-commit (pull_request) Successful in 2m0s
CI / frontend-test (pull_request) Failing after 3m43s
CI / e2e (pull_request) Has been cancelled
CI / backend-test (pull_request) Has been cancelled
23dc2ded55
A push to main is now judged from CHANGELOG.md by release-decide.sh.
Entries under Unreleased are promoted to a dated CalVer section
(release-promote.sh), committed as Waggle Release Bot, tagged and pushed
with a write deploy key; the tag runs release-and-deploy unchanged. A
'<!-- release: hold -->' marker or a "Note when rolling out" section
routes the push to staging instead, which is now the only time staging
builds. An empty Unreleased with an untagged newest section gets its
tag, so a release cut by hand in a PR still deploys on merge. A manual
dispatch ignores the hold.

The deploy key is needed because the automatic token can neither push
to protected main nor start a workflow with the tag it pushes; a
deploy-key push counts as the repository owner's. The push pins the
code.overheid.nl host key in .forgejo/known_hosts.

unreleased.md is renamed to the version only when it has content and
removed when empty. This PR lands under a hold, so its own merge only
stages.
robbertbos force-pushed auto-release from 23dc2ded55
Some checks failed
CI / release-scripts (pull_request) Successful in 11s
security-scan / SBOM (trivy) (pull_request) Successful in 11s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / Python SAST (bandit) (pull_request) Successful in 22s
security-scan / JS SCA (npm audit) (pull_request) Successful in 35s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 38s
test-build / build (backend) (pull_request) Successful in 1m9s
test-build / build (frontend) (pull_request) Successful in 1m16s
test-build / build (pull_request) Successful in 0s
CI / pre-commit (pull_request) Successful in 2m0s
CI / frontend-test (pull_request) Failing after 3m43s
CI / e2e (pull_request) Has been cancelled
CI / backend-test (pull_request) Has been cancelled
to 23e8b63c17
Some checks failed
CI / release-scripts (pull_request) Successful in 17s
security-scan / SBOM (trivy) (pull_request) Successful in 16s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 20s
security-scan / Python SAST (bandit) (pull_request) Successful in 33s
security-scan / JS SCA (npm audit) (pull_request) Successful in 38s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 52s
test-build / build (backend) (pull_request) Successful in 1m23s
test-build / build (frontend) (pull_request) Successful in 1m29s
test-build / build (pull_request) Successful in 0s
CI / pre-commit (pull_request) Successful in 1m36s
CI / frontend-test (pull_request) Successful in 1m37s
CI / backend-test (pull_request) Successful in 3m34s
CI / e2e (pull_request) Has been cancelled
2026-09-23 19:31:22 +00:00
Compare
robbertbos force-pushed auto-release from 23e8b63c17
Some checks failed
CI / release-scripts (pull_request) Successful in 17s
security-scan / SBOM (trivy) (pull_request) Successful in 16s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 20s
security-scan / Python SAST (bandit) (pull_request) Successful in 33s
security-scan / JS SCA (npm audit) (pull_request) Successful in 38s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 52s
test-build / build (backend) (pull_request) Successful in 1m23s
test-build / build (frontend) (pull_request) Successful in 1m29s
test-build / build (pull_request) Successful in 0s
CI / pre-commit (pull_request) Successful in 1m36s
CI / frontend-test (pull_request) Successful in 1m37s
CI / backend-test (pull_request) Successful in 3m34s
CI / e2e (pull_request) Has been cancelled
to ca397cf2d4
Some checks failed
CI / e2e (pull_request) Has been cancelled
security-scan / SBOM (trivy) (pull_request) Successful in 11s
CI / release-scripts (pull_request) Successful in 15s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 20s
security-scan / Python SAST (bandit) (pull_request) Successful in 25s
security-scan / JS SCA (npm audit) (pull_request) Successful in 37s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 43s
CI / pre-commit (pull_request) Successful in 1m7s
test-build / build (backend) (pull_request) Successful in 1m11s
CI / frontend-test (pull_request) Successful in 1m14s
test-build / build (frontend) (pull_request) Successful in 1m20s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Has been cancelled
2026-09-23 19:36:25 +00:00
Compare
robbertbos force-pushed auto-release from ca397cf2d4
Some checks failed
CI / e2e (pull_request) Has been cancelled
security-scan / SBOM (trivy) (pull_request) Successful in 11s
CI / release-scripts (pull_request) Successful in 15s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 20s
security-scan / Python SAST (bandit) (pull_request) Successful in 25s
security-scan / JS SCA (npm audit) (pull_request) Successful in 37s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 43s
CI / pre-commit (pull_request) Successful in 1m7s
test-build / build (backend) (pull_request) Successful in 1m11s
CI / frontend-test (pull_request) Successful in 1m14s
test-build / build (frontend) (pull_request) Successful in 1m20s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Has been cancelled
to cf0afbb1be
All checks were successful
CI / release-scripts (pull_request) Successful in 16s
security-scan / SBOM (trivy) (pull_request) Successful in 16s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 21s
security-scan / Python SAST (bandit) (pull_request) Successful in 25s
security-scan / JS SCA (npm audit) (pull_request) Successful in 38s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 46s
CI / frontend-test (pull_request) Successful in 1m10s
test-build / build (backend) (pull_request) Successful in 1m10s
test-build / build (frontend) (pull_request) Successful in 1m20s
test-build / build (pull_request) Successful in 0s
CI / pre-commit (pull_request) Successful in 2m5s
CI / backend-test (pull_request) Successful in 4m46s
CI / e2e (pull_request) Successful in 8m3s
2026-09-23 19:38:28 +00:00
Compare
robbertbos changed title from Release on merge: promote Unreleased and tag from publish-and-stage to Release on merge: cut-release promotes Unreleased and tags it 2026-09-23 19:38:42 +00:00
robbertbos force-pushed auto-release from cf0afbb1be
All checks were successful
CI / release-scripts (pull_request) Successful in 16s
security-scan / SBOM (trivy) (pull_request) Successful in 16s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 21s
security-scan / Python SAST (bandit) (pull_request) Successful in 25s
security-scan / JS SCA (npm audit) (pull_request) Successful in 38s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 46s
CI / frontend-test (pull_request) Successful in 1m10s
test-build / build (backend) (pull_request) Successful in 1m10s
test-build / build (frontend) (pull_request) Successful in 1m20s
test-build / build (pull_request) Successful in 0s
CI / pre-commit (pull_request) Successful in 2m5s
CI / backend-test (pull_request) Successful in 4m46s
CI / e2e (pull_request) Successful in 8m3s
to 622480904e
Some checks failed
security-scan / SBOM (trivy) (pull_request) Successful in 12s
CI / release-scripts (pull_request) Successful in 13s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 17s
security-scan / Python SAST (bandit) (pull_request) Successful in 24s
security-scan / JS SCA (npm audit) (pull_request) Successful in 34s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 42s
test-build / build (backend) (pull_request) Successful in 1m12s
test-build / build (frontend) (pull_request) Successful in 1m19s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m23s
CI / pre-commit (pull_request) Successful in 2m6s
CI / backend-test (pull_request) Successful in 3m16s
CI / e2e (pull_request) Has been cancelled
2026-09-23 19:52:19 +00:00
Compare
robbertbos force-pushed auto-release from 622480904e
Some checks failed
security-scan / SBOM (trivy) (pull_request) Successful in 12s
CI / release-scripts (pull_request) Successful in 13s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 17s
security-scan / Python SAST (bandit) (pull_request) Successful in 24s
security-scan / JS SCA (npm audit) (pull_request) Successful in 34s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 42s
test-build / build (backend) (pull_request) Successful in 1m12s
test-build / build (frontend) (pull_request) Successful in 1m19s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m23s
CI / pre-commit (pull_request) Successful in 2m6s
CI / backend-test (pull_request) Successful in 3m16s
CI / e2e (pull_request) Has been cancelled
to 7aef31357f
Some checks failed
CI / release-scripts (pull_request) Successful in 13s
security-scan / SBOM (trivy) (pull_request) Successful in 12s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / Python SAST (bandit) (pull_request) Successful in 23s
CI / frontend-test (pull_request) Failing after 23s
security-scan / JS SCA (npm audit) (pull_request) Successful in 35s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 42s
test-build / build (backend) (pull_request) Successful in 1m15s
CI / pre-commit (pull_request) Successful in 1m17s
test-build / build (frontend) (pull_request) Successful in 1m18s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 3m38s
CI / e2e (pull_request) Successful in 6m7s
2026-09-23 19:56:14 +00:00
Compare
robbertbos changed title from Release on merge: cut-release promotes Unreleased and tags it to Release on merge: tag-release promotes Unreleased and tags it 2026-09-23 19:56:50 +00:00
robbertbos force-pushed auto-release from 7aef31357f
Some checks failed
CI / release-scripts (pull_request) Successful in 13s
security-scan / SBOM (trivy) (pull_request) Successful in 12s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / Python SAST (bandit) (pull_request) Successful in 23s
CI / frontend-test (pull_request) Failing after 23s
security-scan / JS SCA (npm audit) (pull_request) Successful in 35s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 42s
test-build / build (backend) (pull_request) Successful in 1m15s
CI / pre-commit (pull_request) Successful in 1m17s
test-build / build (frontend) (pull_request) Successful in 1m18s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 3m38s
CI / e2e (pull_request) Successful in 6m7s
to 225727c08d
Some checks failed
security-scan / SBOM (trivy) (pull_request) Successful in 9s
CI / release-scripts (pull_request) Successful in 11s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / Python SAST (bandit) (pull_request) Successful in 21s
security-scan / JS SCA (npm audit) (pull_request) Successful in 35s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 39s
test-build / build (backend) (pull_request) Successful in 1m8s
test-build / build (frontend) (pull_request) Successful in 1m15s
test-build / build (pull_request) Successful in 0s
CI / pre-commit (pull_request) Successful in 1m57s
CI / frontend-test (pull_request) Successful in 4m22s
CI / e2e (pull_request) Successful in 15m20s
CI / backend-test (pull_request) Successful in 17m3s
tag-release / release (push) Successful in 9s
publish-and-stage / decide (push) Successful in 10s
CI / release-scripts (push) Successful in 15s
security-scan / SBOM (trivy) (push) Successful in 14s
security-scan / Filesystem scan (trivy fs) (push) Successful in 20s
security-scan / Python SAST (bandit) (push) Successful in 27s
security-scan / JS SCA (npm audit) (push) Successful in 38s
security-scan / Python SCA (pip-audit) (push) Successful in 46s
publish-and-stage / build (backend) (push) Successful in 1m42s
publish-and-stage / build (frontend) (push) Successful in 1m50s
publish-and-stage / build (push) Successful in 0s
CI / pre-commit (push) Successful in 2m3s
publish-and-stage / deploy-staging (push) Successful in 1m52s
CI / frontend-test (push) Successful in 4m4s
CI / backend-test (push) Has been cancelled
CI / e2e (push) Has been cancelled
2026-09-23 20:17:11 +00:00
Compare
robbertbos scheduled this pull request to auto merge when all checks succeed 2026-09-23 20:17:51 +00:00
robbertbos referenced this pull request from a commit 2026-09-23 20:27:40 +00:00
robbertbos deleted branch auto-release 2026-09-23 20:34:16 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
robbertbos/waggle!416
No description provided.