Releasing 2.0
This is the maintainer runbook for the 2.0.0 release path (v2 plan §4.3, M4-23): how merging v2 into main is kept from publishing anything early, how that is rehearsed, how the 1.x maintenance branch is cut and released from (D40), and the order of the release day. The Release workflow itself, its signed tags, and its recovery paths are described in CONTRIBUTING.
Nothing is published from v2 before 2.0.0 (D5). v2 still carries the 1.x project version, so after the merge the project version on main cannot tell 2.0 contents from 1.x contents. The release line can.
The release line
Every branch declares its release line, the major version its contents belong to, in .github/release-line: one number, with # comments. v2 declares 2; the prepared main declares 1, and 1.x will inherit 1 when it is cut. The line must be the project version's major, or that major plus one while a branch prepares the next major, and a missing or malformed file fails every check that reads it. A branch containing bootui-agent, which exists only from 2.0, must declare at least 2, so a main-to-v2 sync that resolves a conflict on this file to 1 fails the integrity guard, the gate, and the Release workflow instead of letting the merged main publish 2.0 as the 1.x line. The first commit on a branch preparing a later major, such as v3, declares that major.
How each channel is guarded
| Channel | What decides | Before the merge | After the merge, before 2.0.0 | Once 2.0.0 is on Maven Central |
|---|---|---|---|---|
Documentation site (pages.yml) | release-line-gate.sh on every push and manual run | main (line 1) deploys | main (line 2) builds the site but skips the upload and the deploy | main deploys again; 1.x never deploys |
Docker Hub sample images (docker-publish.yml) | The same gate, in the workflow's first job | main publishes daily | Every job but prune is skipped | main publishes 2.x images; 1.x publishes none |
Maven Central (release.yml) | release-version-policy.sh with the release line, before any file changes, and again on the tagged contents before publication; only from main or the version's N.x branch | main releases 1.x only | main releases 2.0.0 only, and only when dispatched; 1.x releases 1.x patches only | main releases 2.x; 1.x releases 1.x patches, without redeploying the site |
JBang alias (jbang-catalog.json) | The Release workflow rewrites it | 1.x | Unchanged: the catalog is identical on main and v2 | 2.0.0 |
Installers (install.sh, install.ps1) and the CLI's update check | Maven Central's maven-metadata.xml | 1.x | 1.x | 2.x |
release-line-gate.sh reads the branch's release line, the tags on origin, and Maven Central. A branch may publish only when one tag of its line has its bootui-engine and Spring starter jars on Maven Central (2.0 no longer publishes the parent POM or bootui-core), and no tag of a newer major does. Requiring any tag, not the newest, keeps one failed or still-propagating patch from blocking every later deploy, and counting a newer major only once it is on Maven Central keeps a stray or failed tag from retiring the released line. An unreadable answer, from origin or from Maven Central, fails the run instead of deciding.
The Release workflow dispatches pages.yml at the release tag only after every artifact is available and only for the newest major, so the gate passes there, and it fails unless that run's deploy job actually deployed. Pull requests still build the site, without the gate.
The Release workflow also prepares a release only from main or from the version's maintenance branch (1.x for a 1.x version), and publishes a tag only when its commit is on one of them, so a signed tag pushed on v2 cannot publish before the merge.
check-release-integrity.sh pins all of this: the release-line arguments, the tagged-contents and release-branch checks, and the deploy confirmation in release.yml; the gate and the gated upload and deploy in pages.yml; the gate job ahead of every publishing job in docker-publish.yml, whose image jobs keep exactly their repository condition so no status function such as always() can run them after a skipped gate; and a valid .github/release-line. Workflows may not set the gate's test seams.
What workflow files cannot guard
A manual run executes the workflow file of the ref it is dispatched from, so refs created before the gate, such as the v1.* tags or an older v2 feature branch, carry ungated workflows forever. The GitHub environments are the only control over them:
maven-centralacceptsmainandv*tags, with required reviewers. It refuses manual runs fromv2and its feature branches. Add1.xwhen the branch is cut.github-pagesacceptsmainandv*tags. Right after the 2.0 site deploys, narrow the tag rule tov2.*, so an old tag's ungatedpages.ymlcannot replace it. Repeat at 3.0.docker-hubaccepts onlymain(done 2026-10-04). Without that policy, any ref whosedocker-publish.ymlpredates the gate, such as an olderv2feature branch or av1.*tag after 2.0.0, could pushlatestthrough a manual run. The daily schedule runs only onmain, so nothing legitimate lost access. A manual run from any other ref now fails at its firstdocker-hubjob, including aprune_dry_runpreview: run retention previews frommain. Keep this policy at 3.0.
Re-running a workflow run replays the workflow file of that run, and GitHub allows it for 30 days. Never re-run a Pages or Docker run that predates the gate on main; the preparation below has landed, but old runs remain a concern until they age out.
The rehearsal
.github/scripts/rehearse_v2_merge.py rehearses the merge on a candidate that is never published. It builds the merge commit of main and v2 with git merge-tree and git commit-tree, so no branch moves, and checks:
- every workflow that a push to
mainor a schedule runs, and that can publish, is behind the gate, and the Release workflow has no branch trigger; - on the candidate, the integrity guard passes, the gate decides not to publish the site or the images, against the real tags on origin and Maven Central, and the Release workflow's policy refuses the next 1.x patch and accepts only 2.0.0;
- the 1.x line before the merge still publishes, against the real tags and Maven Central;
- simulated futures, with synthetic tags and a local stand-in for Maven Central: a v2.0.0 tag without its artifacts publishes nothing, its artifacts publish the 2.x site, and the
1.xbranch then publishes no site, releases its next patch without redeploying the site, and can never release 2.0.0; - whether
mainis ready to become1.x, with its own integrity guard and release tests passing, whether the release sign-off explicitly approves 2.0.0 with complete candidate and maintainer metadata, and whether the GitHub environments are set as above (read only, withgh).
# Any day: a local, read-only rehearsal against origin/main and the current v2 commit
python3 .github/scripts/rehearse_v2_merge.py --v2 origin/v2
# Release day, only after the approved 1.x cut and administrator settings:
# also run the gated workflows on GitHub, and fail on any pending prerequisite
python3 .github/scripts/rehearse_v2_merge.py --v2 origin/v2 --live --release-day
--live pushes the candidate to a temporary rehearsal/v2-merge-* branch, dispatches pages.yml and docker-publish.yml from it, requires every publishing job to be skipped, and deletes the branch. No push trigger runs on that branch, and the github-pages and maven-central environments refuse it, so a broken gate fails rather than publishes; docker-hub refuses it too, so the Docker image jobs could not push even past a broken gate. As a second line, the rehearsal cancels a run as soon as any job past its gate is queued. It never pushes the temporary branch or dispatches either workflow while any prerequisite check is FAIL or PENDING, including incomplete or non-approved release sign-off.
A merge conflict fails the rehearsal: merge main into v2 first. Prerequisites that are not done yet are reported as PENDING; --release-day turns them into failures.
The read-only rehearsal also runs main's complete release-policy test suite. On macOS this can take several minutes: progress remains captured until the suite exits, so a short external timeout can look like a stalled test. An interrupted rehearsal is incomplete, not a passing preflight or proof of a failed assertion. If an external timeout stopped it, preserve the log, distinguish an actual assertion failure from the timeout, and rerun the same read-only command with enough time for the full suite. Do not skip the maintenance-line tests or weaken a guard to get past a timeout.
Showcase video
The homepage MP4 lives outside Git as bootui-showcase.mp4 on a media-only GitHub Release. Each branch selects that release in docs/.vuepress/showcase-video.json. main is the 2.0 line and selects showcase-video-v2-r4, with its matching poster, captions, and player copy. The earlier media releases (showcase-video, showcase-video-v2, showcase-video-v2-r2) remain hosted but are not selected. Keep all published media releases and assets intact. These media tags do not start with v, so they do not trigger the Maven Release workflow or count as a product version. A new product release does not require changing the showcase video.
Published media releases can be immutable. If an approved correction is needed, publish a new revisioned media release, such as showcase-video-v2-r2, and update the manifest after verifying its assets. Do not delete or move the previous media tag or overwrite immutable assets. A superseded render is not the final approved video.
Upload only the verified final render. Commit its matching JPEG poster and English WebVTT captions at docs/.vuepress/public/videos/bootui-showcase-poster.jpg and bootui-showcase.en.vtt on the corresponding documentation branch; do not pair the new narration with old captions. Pages downloads the selected MP4 during the site build, fails on a missing config or asset, and checks all three assets in the generated site.
Release sign-off format
The maintainer records the decision in the single Release sign-off section of the validation report. Each required table row appears exactly once:
| Field | Required format |
|---|---|
| Release candidate | A full lowercase 40-character commit SHA and its BootUI version: `<SHA>` (version `<major.minor.patch>`). Identify the final validated source, not a historical rerun build. |
| Decision | Exactly PENDING, HOLD, or APPROVE_RELEASE_2_0_0, with no prose or qualifiers in the cell. Only APPROVE_RELEASE_2_0_0 authorizes release. |
| Decision rationale | The maintainer's nonempty rationale, including accepted exceptions or reasons for holding release. |
| Maintainer | In the Role, Name, Date table: a nonempty, non-placeholder name and a valid, non-future calendar date in YYYY-MM-DD format. |
Empty or missing fields, duplicate rows, placeholders, invalid dates, and unresolved TODOs never authorize release. Free-form decisions such as "Do not release", a question, or a conditional approval also never authorize it; record the exact decision token and put its explanation in Decision rationale. Ordinary read-only rehearsal reports incomplete or non-approved sign-off as PENDING, not PASS; --release-day reports it as FAIL. This is validation of the recorded human sign-off, not cryptographic authentication or a substitute for maintainer review of the source and evidence. Agents must never fill in an approval, candidate, name, or date on the maintainer's behalf.
The 1.x maintenance branch
1.x is cut from main's last 1.x commit right before v2 merges. Until then main is the 1.x line and keeps receiving fixes; do not cut the branch early.
Before the cut: verify the prepared main
The per-major release machinery and release-line gates have landed on main. The following is the preparation record, not an outstanding instruction to backport them again. Before release day, verify that subsequent changes have kept these invariants; the read-only rehearsal checks them:
release-version-policy.sh,release-line-gate.sh,pages.yml, anddocker-publish.ymlare byte-identical onmainandv2. Preserve that equality through reviewed changes whenever a shared gate changes. The gate's sentinel isbootui-engine, not the retired 2.0bootui-corecoordinate, so the prepared 1.x line recognizes 2.0 on Maven Central.main'srelease.ymlhas the release-line arguments, the tagged-contents and release-branch checks, the deploy confirmation, and the newest-major documentation decision, keepingmain's own publication reactor and availability list, which have no agent modules; its integrity guard and release tests enforce that 1.x-specific machinery.main's.github/release-lineis1. When syncing it intov2, keep2onv2(the guard rejects1on a branch containingbootui-agent).1.xis listed in the push and pull-request branches ofbuild.ymlandjdk-compatibility.yml, so it gets the green build a release requires.
This preparation and a read-only rehearsal are not release authorization. Cutting 1.x, merging the release, dispatching live rehearsals or Release, signing, and publishing still require the maintainer's release-day decision; environment/ruleset changes require a repository administrator. Pending sign-off or environment permissions remain blockers, not waived checks.
Settings, by a repository administrator
- A ruleset for
1.xlikemain's: no force push or deletion, pull requests required, andgithub-actions[bot]allowed to push the release commit and itsv1.*tag. - The
maven-centralenvironment accepts the1.xbranch. - The
docker-hubenvironment already accepts onlymain(done 2026-10-04); nothing to change for1.x, which publishes no images.
Cutting the branch
On release day, after the last 1.x release from main, the sync into v2, and the read-only preflight, and only with explicit maintainer approval, freeze main and cut the branch. The administrator applies the settings above before the strict live rehearsal; that rehearsal requires maven-central to accept 1.x, so it cannot precede the cut and settings. Keep main frozen from preflight through the cut, strict rehearsal, and v2 merge:
git fetch origin
git show origin/main:.github/release-line # must declare 1
git push origin origin/main:refs/heads/1.x
The branch points at the frozen, preflighted main commit: no merge, no rebase, no new commit. Stop if main advances; do not move an already-cut maintenance branch to repair the sequence. Reconcile the source and repeat preflight before proceeding.
Releasing a 1.x patch
- Fix on
1.xthrough a pull request into1.x, and port the fix tomainwhen it applies to 2.x (main-to-v2syncs carry it into any later major's branch). - Cut
CHANGELOG.mdon1.xto## [1.N.P] - YYYY-MM-DDas its own commit, and wait for a green build. - Run Release from
1.xwith the next 1.x version. The policy accepts only the next patch or minor after the newest 1.x tag; the release line refuses 2.x versions. - After 2.0.0, the workflow skips the documentation redeploy, with a notice: the site follows the newest major. Between the merge and 2.0.0, 1.x is still the newest released major, so a 1.x patch still redeploys the 1.x site from its tag. A manual
pages.ymlrun from1.xis refused by thegithub-pagesenvironment, which accepts onlymainand tags.
Release day
- Review. Known limitations matches the shipped scope. Review the release sign-off evidence and accepted exceptions; a pending or hold decision is not release approval.
- Last 1.x release. Release any unreleased 1.x change from
mainas usual, and verify the preparedmainabove. - Sync. Merge
mainintov2with a merge commit, never a rebase;.github/release-linestays2. - Read-only preflight. Freeze
main, then runpython3 .github/scripts/rehearse_v2_merge.py --v2 origin/v2without--liveor--release-day. Resolve every failure. Missing1.xpermission and unsigned sign-off may still bePENDINGat this stage; record them as blockers, not waived checks. The maintainer identifies the final validated candidate and records its explicit approval, rationale, name, and date in the supported format before authorizing the next step. - Cut
1.xand apply its settings. With explicit maintainer approval, cut from the frozen, preflightedorigin/main, as above. A repository administrator applies the1.xruleset andmaven-centralbranch permission. Do not waive either protection to make the rehearsal pass;mainremains frozen. - Strict live rehearsal. Only after the cut and settings, with explicit maintainer authorization, run
python3 .github/scripts/rehearse_v2_merge.py --v2 origin/v2 --live --release-day. It must report no failure and nothing pending before the merge. If it fails, stop and fix the prerequisite; do not advancemain. - Merge
v2intomainwith a merge commit. Check that the Pages run of the merge push skipped its upload and deploy with "release line 2 has no release tag yet", and that the next Docker run skipped every job but prune. - Changelog. Cut
CHANGELOG.md's[Unreleased]to## [2.0.0] - YYYY-MM-DDonmain, as its own commit, and wait for a green build. - Release. With maintainer approval, run Release from
mainwith version2.0.0. Merge nothing intomainuntil the run is green: while Maven Central propagates, a push could find the gate's two artifacts before the others. The run refuses an imported key other than the pinnedRELEASE_KEY_FINGERPRINT(7B7C0BD038603E5A9F1476D0498BA5AC9BABBAF9), and checks every signature against it twice: after the verification build, before the tag (unlessskip_build), and on the Central bundle, before the upload. Normal preparation also stages the exact candidate bundle and smoke-tests its consumers before committing/tagging. Tag-entry andskip_buildruns do that from the immutable tagged checkout before uploading instead; a successful pre-tag smoke is not repeated after the tag. Publication, availability checks, published-consumer smoke and the site deployment all use the commit peeled from the verified signed tag. - After the release. Confirm that the site shows 2.0 (deployed from the v2.0.0 tag), and that
jbang bootui@jdubois/boot-uiand the installers resolve 2.0.0. Once 2.0.0 is available on Maven Central, explicitly run Publish Docker images (docker-publish.yml) frommain, withcleanup_only=false, rather than waiting for the daily schedule. Before Central availability, the gate must still skip the build and publication; never bypass it to test new images early. Require all six variants (JVM, AOT, CRaC, native, WebFlux, and Quarkus) on both architectures to pass the workflow's native-runner smoke tests, including readiness, BootUI API/UI, AOT advisor checks, and CRaC checkpoint/restore. Verify the published multi-arch manifests and exact tested digests, then pull and exercise the published images on available platforms; report any platform not additionally exercised rather than claiming local cross-architecture coverage. Narrow thegithub-pagestag rule tov2.*. After the first 1.x patch that follows, confirm that Maven Central'smaven-metadata.xmlforbootui-clistill lists a 2.x<release>, which the installers and the CLI's update check read.