Skip to content

Spec: Split release-plz into separate pr / release jobs (v0.10.3)

  • Repository: gitlab.com/phpboyscout/cicd
  • Component touched: release-plz
  • Release: patch — fix: → batched into v0.10.3

Problem

The release-plz component runs both release-plz subcommands as two script lines in a single job:

script:
  - release-plz release-pr --forge gitlab --git-token "$RELEASE_PLZ_TOKEN_RUNTIME"
  - release-plz release    --forge gitlab --git-token "$RELEASE_PLZ_TOKEN_RUNTIME"

release-plz release-pr mutates the working tree when an open Release MR exists. In release_plz_core's release_pr command it stashes, then reset_branch runs:

git checkout <release-mr-branch>
git reset --hard HEAD~<n>
git fetch <original-branch>
git rebase <original-branch>

After it returns, the working tree is checked out on the Release MR branch, not the default branch. The second line, release-plz release, then runs cargo metadata against that mutated tree. It reads the pre-bump crate versions, concludes is_published(crate@<old>) is true for every crate, logs <name> <version>: already published, and exits 0 having published nothing — no crates.io upload, no v{version} tag, no GitLab release, and so no downstream cargo-dist tag pipeline.

Observed failure

On phpboyscout/rust-tool-base, Release MR !34 (unified bump to 0.5.2) was merged to main. The push pipeline's release-plz job:

  1. ran release-pr, which opened a fresh (empty) follow-up Release MR !35 — proving the tree was mutated onto a release branch;
  2. ran release on that mutated tree, logged every crate as "already published", and published nothing.

0.5.2 reached main's Cargo.toml but never reached crates.io. A local release-plz release --dry-run on a clean checkout of the same commit correctly reported all crates as publishable — confirming the mutated working tree, not the commit, is the cause.

Decision

Run the two subcommands as two separate jobs, each with its own clean checkout of $CI_COMMIT_SHA:

  • release-plz:pr — runs release-plz release-pr. May mutate its own working tree; that tree is discarded when the job ends.
  • release-plz:release — runs release-plz release on a pristine checkout, so cargo metadata reports the merged, version-bumped tree and the publish fires.

Both inherit a hidden .release-plz-base job (extends:) carrying the shared before_script (binstall bootstrap, branch re-attach, authenticated origin), rules:, variables:, and dependencies: []. This is the documented hidden-job + extends: pattern, and matches upstream's release-plz GitLab component, which is itself split into distinct pr / release jobs for exactly this reason.

Why not cd/git checkout - between the two lines

Restoring the branch after release-pr would un-mutate the tree, but release-plz also leaves a stash and a moved HEAD; reconstructing the exact pre-state in shell is brittle and re-couples to release-plz internals. Separate jobs get a guaranteed-clean tree from GitLab's own checkout, with no knowledge of what release-pr did.

Scope & non-goals

  • In scope: the job structure of templates/release-plz.yml and the matching tests/release-plz/.gitlab-ci.yml self-test override. No input shape changes — every existing input keeps its name, type, and default.
  • Behaviour change: the component now emits two jobs named release-plz:pr and release-plz:release instead of one job named release-plz. A consumer that referenced the old job name (e.g. in needs: or an override) must update to the new names. No rust-tool- base consumer does; the only override is this repo's own self-test.
  • Non-goals: changing tokens, forge handling, the extra_before_ script hook, or the bootstrap. Not touching any other component.

Testing

tests/release-plz/.gitlab-ci.yml includes the component against a repo with no Cargo.toml, so each release-plz invocation exits non-zero with "not a Cargo workspace". The self-test override applies allow_failure: { exit_codes: [1, 2, 101, 128] } to both new jobs; any other exit (broken image, bad interpolation, missing binary) still fails the self-test. The self-test:release-plz trigger in the root pipeline runs on any change to templates/release-plz.yml or tests/release-plz/**.

The real publish path is verified downstream: after this ships in v0.10.3, rust-tool-base bumps its pin and re-runs the main pipeline, which must produce the crates.io uploads, the per-crate tags, and the GitLab releases that v0.5.2 missed.