Skip to content

Releasing a new version

This is the release checklist. Work through the phases in order; each one ends with a single command or a single decision.

The shape of a release. Day-to-day work accumulates on dev. A release closes out the CHANGELOG.md [Unreleased] section, merges dev into main through a pull request, and pushes a vX.Y.Z tag on main. The tag — nothing else — triggers release.yml, which publishes the GitHub Release and updates Homebrew, Scoop and the Ubuntu PPA. See ADR-0024 for the branch model and ADR-0020 for the pipeline mechanics and the list of required repository secrets.

scripts/release.sh does the mechanical parts. It never merges, never tags, and never pushes — those stay in your hands.


Phase 0 — Decide

Three judgement calls, none of which a script can make for you.

Pick the version number. Read the [Unreleased] section and apply semantic versioning (ADR-0001): a breaking change to a public API — jpipe-operators above all — is a MAJOR bump, new behaviour is MINOR, fixes alone are PATCH. What pom.xml currently says is not an input to this decision; Phase 1 sets the pom to whatever you choose here.

Review dependencies (ADR-0007) — a required step, not optional housekeeping:

mvn versions:display-dependency-updates
mvn versions:display-plugin-updates

Review the Ubuntu series matrix (ADR-0023). Compare the distro: list in .github/workflows/release.yml against the policy: every LTS released in or after 2024 that is still in standard support, plus the next LTS once its series opens on Launchpad, plus every interim release inside its nine-month window. Add a series when it enters support; drop one in the release that follows its EOL.


Phase 1 — Prepare, on dev

git switch dev && git pull --ff-only
scripts/release.sh prepare X.Y.Z

This sets every pom to X.Y.Z-SNAPSHOT, renames ## [Unreleased] to ## [X.Y.Z] — <today>, opens a fresh empty [Unreleased] above it, adds the [X.Y.Z] compare link at the bottom of the changelog, runs mvn verify, and commits. Add --dry-run to see the diff without writing anything.

Setting the pom here is what makes the version safe. The pipeline compares the tag against the pom version and fails after the tag has been pushed, so the pom must say X.Y.Z-SNAPSHOT before you tag vX.Y.Z — whatever dev happened to be bumped to earlier is irrelevant once prepare has run.

Then push. A release chore does not need a pull request (ADR-0024, amended):

git push

Phase 2 — Release pull request

scripts/release.sh preflight X.Y.Z

Preflight re-runs every check release.yml performs, while the tag still does not exist: clean tree, branch in sync, pom version against the tag base, tag not already taken, changelog closed out, mvn verify green. It also reports whether HEAD is on main yet — expect a warning here, since the merge has not happened.

Open a pull request from dev to main titled Release vX.Y.Z, and merge it once the checks are green. This is the one merge in the process that must go through a pull request: main is release-only, and every commit on it is a published version.


Phase 3 — Tag, on main

git switch main && git pull --ff-only
scripts/release.sh preflight X.Y.Z    # now expect "HEAD is on main"
git tag vX.Y.Z
git push origin vX.Y.Z

Pushing the tag is what starts the release. release.yml verifies the tag is an ancestor of main, re-checks it against pom.xml, stamps the version into the fat JAR manifest, builds once, and publishes the GitHub Release with three assets. The Homebrew, Scoop and PPA jobs then run in parallel off that release.

A tag containing a hyphen (v2.4.0-rc1) is published as a GitHub pre-release and every channel job is skipped (ADR-0025). A release candidate is cut from an already-prepared version — do not run prepare for it.

Watch the run: https://github.com/jpipe-mcscert/jpipe-compiler/actions


Phase 4 — Verify

Package indices take up to about thirty minutes to settle.

# macOS
brew tap jpipe-mcscert/mcscert
brew install jpipe && jpipe doctor

# Ubuntu
sudo add-apt-repository ppa:mcscert/ppa
sudo apt update && sudo apt install jpipe && jpipe doctor
# Windows
scoop bucket add mcscert https://github.com/jpipe-mcscert/scoop-mcscert
scoop install mcscert/jpipe
jpipe doctor

jpipe doctor should report the version you just tagged. The Windows check is manual and required — it is the one channel CI cannot smoke-test (ADR-0025).


Phase 5 — Merge back and reopen development

git switch dev && git pull --ff-only
git merge origin/main
scripts/release.sh post-release <next>-SNAPSHOT   # e.g. 2.4.1-SNAPSHOT
git push

Pass the next version literally — <next> is a placeholder, and the script rejects anything that is not a valid Maven version. The next patch is the low-surprise default: since Phase 1 sets the release version explicitly, an over-bump here is harmless, but it is what confused the 2.3.1 release.

Merging main back into dev keeps the release commit shared between the two branches; post-release refuses to run until you have.


Troubleshooting

The tag is pushed and the pipeline failed on the version check. The tag is not on the release yet, so it is safe to withdraw:

git push --delete origin vX.Y.Z
git tag -d vX.Y.Z

Fix the pom on dev (scripts/release.sh prepare X.Y.Z), merge to main again, and re-tag. Running preflight in Phase 3 is what prevents this.

The release published but a channel job failed. By design, a missing credential or a broken index fails one channel, not the release — the GitHub Release and its assets are already live. Fix the cause and re-run the failed job from the Actions run page; the channel jobs consume release assets rather than rebuilding, so re-running is safe and produces identical bytecode.

prepare says [Unreleased] is empty. Nothing has been merged into dev since the last release, or the entries were never written. The changelog is the release notes — write them before releasing, not after.

post-release says main is not merged back. Run git merge origin/main on dev first. Skipping this leaves the release commit on main only, and the next release's devmain merge becomes a conflict.