ci(release): derive the version from commit subjects, and add a manual backdoor #17

Merged
whitlocktech merged 1 commits from feature/release-per-merge into main 2026-08-19 18:07:57 +00:00
Member

What & why

A bundle used to cost a second pull request whose entire content was a number. The workflow published only when a merge to main left module.json at a version with no release yet, so an ordinary merge released nothing.

Measured rather than argued: v0.3.0 (2026-08-12) is the only release this repo has ever cut, while the nine Teams phases and the cutover landed on main in the week since. Not one of them touched that line, so not one produced a bundle.

This adopts the engine link and installer already run:

Since the newest v* tag Result
feat!: / BREAKING CHANGE major
feat: minor
fix: / perf: patch
none of those no release
no tag at all releases what module.json declares

The number that ships is the tag, and CI writes it into the module.json inside the bundle — with an assertion that the rewrite happened, because a bundle carrying the wrong version installs under a number that is not the one it was released as.

module.json's version is kept as a floor, not deleted. A version above the newest tag still releases at that version, so the declared model survives as the special case it always was, and is still how a coreApi bump overrules the subjects.

The backdoor (workflow_dispatch) covers what the rules cannot reach — a module.json change worth shipping with no releasable code behind it. Leave version blank to bump the newest tag by bump, or name an exact version. Where the log and the input disagree the larger wins: a button pressed on a log full of feat: would otherwise publish its patch default over a minor's worth of work.

Two things carried over from link in the same pass:

  • a tag pushed without a release behind it is now recovered instead of making that state permanent (every later run would see the tag and stand down);
  • the changelog moved into the plan step, so the assemble step clears $OUT rather than dist/ — which now holds the changelog the publish step reads back.

The workflow still never writes to a branch, which was always the half of the original design that mattered under branch protection.

Not in scope, raised separately: the bundle's for d in … include list is the shape the kit's acceptance run found silently drops any newly added server/ directory. The Integration kit's template was fixed; this repo was not. Left alone deliberately to keep this diff about cadence.

On this repo's main today

==> release=true version=0.4.0 bump=minor declared=0.3.0 last_tag=v0.3.0

…with a changelog listing the nine Teams feat:s and two fix:es.

How it was tested

The plan step was extracted from the YAML and run against purpose-built git fixtures, plus this repo's real main. Both the derived path and the backdoor were exercised, with curl stubbed to return each HTTP code the recovery branch distinguishes:

Case Result
feat: since tag 0.4.0, release
fix: only 0.3.1, release
feat!: 1.0.0, release
nothing releasable, release exists (200) stands down
nothing releasable, tag unpublished (404) recovers, reuse_tag=true, tag step skipped
lookup fails (000) ::error::, exit 1 — refuses to guess
declared 0.5.0 above tag 0.3.0, chore only 0.5.0 (the floor)
declared 0.5.0, breaking in log 1.0.0 (the larger wins)
declared below the tag (stale) ignored
declared 0.10.0 vs tag 0.9.0 0.10.0 (sort -V, not a string compare)
dispatch, blank version, no code 0.3.1
dispatch, bump=minor, no code 0.4.0
dispatch, exact 1.2.3 1.2.3
dispatch, exact version already released stands down
dispatch patch (default) on a feat: log 0.4.0, not 0.3.1
push event with the inputs absent unaffected

The bundle's module.json rewrite and its version assertion were run directly against this repo's manifest: the rewrite passes, an un-rewritten module.json fails with bundle declares 0.3.0, but this is release 0.4.0, and a missing client/dist/entry.js still fails as before. YAML parses; 9 steps in the expected order.

jq is not on the dev box, so the two jq lines were verified by reading only — the runner has it and the workflow already depended on it before this change.

Checklist

  • I have read CONTRIBUTING.md.
  • The change builds and existing tests/checks pass locally.
  • I have added or updated tests/docs where it makes sense.
  • My commits are reasonably scoped with clear messages.

AI-assisted contributions (required)

  • No AI tools were used to produce this contribution.
  • AI tools were used. Tool(s): Claude Code (Opus 5). I have reviewed and understand
    every change, and take responsibility for it. AI-authored commits are
    marked with a Co-Authored-By / Assisted-By trailer.

License

  • I agree that my contribution is licensed under this project's license
    (GNU GPL v3.0 or later), and I have the right to contribute it.

Pairs with RunicGateway/docs#171 (MODULE_SYSTEM.md §2.7.1 and settled decision 19) and RunicGateway/Integration-kit#8 (the template and chapter 2). Merge the docs one alongside this.

## What & why **A bundle used to cost a second pull request whose entire content was a number.** The workflow published only when a merge to `main` left `module.json` at a version with no release yet, so an ordinary merge released nothing. Measured rather than argued: **`v0.3.0` (2026-08-12) is the only release this repo has ever cut**, while the nine Teams phases and the cutover landed on `main` in the week since. Not one of them touched that line, so not one produced a bundle. This adopts the engine `link` and `installer` already run: | Since the newest `v*` tag | Result | | --- | --- | | `feat!:` / `BREAKING CHANGE` | major | | `feat:` | minor | | `fix:` / `perf:` | patch | | none of those | no release | | no tag at all | releases what `module.json` declares | **The number that ships is the tag**, and CI writes it into the `module.json` inside the bundle — with an assertion that the rewrite happened, because a bundle carrying the wrong version installs under a number that is not the one it was released as. **`module.json`'s version is kept as a floor, not deleted.** A version above the newest tag still releases at that version, so the declared model survives as the special case it always was, and is still how a `coreApi` bump overrules the subjects. **The backdoor** (`workflow_dispatch`) covers what the rules cannot reach — a `module.json` change worth shipping with no releasable code behind it. Leave `version` blank to bump the newest tag by `bump`, or name an exact version. Where the log and the input disagree the **larger** wins: a button pressed on a log full of `feat:` would otherwise publish its `patch` default over a minor's worth of work. Two things carried over from `link` in the same pass: - **a tag pushed without a release behind it is now recovered** instead of making that state permanent (every later run would see the tag and stand down); - **the changelog moved into the plan step**, so the assemble step clears `$OUT` rather than `dist/` — which now holds the changelog the publish step reads back. The workflow still **never writes to a branch**, which was always the half of the original design that mattered under branch protection. **Not in scope, raised separately:** the bundle's `for d in …` include list is the shape the kit's acceptance run found silently drops any newly added `server/` directory. The Integration kit's template was fixed; this repo was not. Left alone deliberately to keep this diff about cadence. ### On this repo's `main` today ``` ==> release=true version=0.4.0 bump=minor declared=0.3.0 last_tag=v0.3.0 ``` …with a changelog listing the nine Teams `feat:`s and two `fix:`es. ## How it was tested The plan step was extracted from the YAML and run against purpose-built git fixtures, plus this repo's real `main`. Both the derived path and the backdoor were exercised, with `curl` stubbed to return each HTTP code the recovery branch distinguishes: | Case | Result | | --- | --- | | `feat:` since tag | `0.4.0`, release | | `fix:` only | `0.3.1`, release | | `feat!:` | `1.0.0`, release | | nothing releasable, release exists (200) | stands down | | nothing releasable, tag unpublished (404) | recovers, `reuse_tag=true`, tag step skipped | | lookup fails (000) | `::error::`, exit 1 — refuses to guess | | declared `0.5.0` above tag `0.3.0`, chore only | `0.5.0` (the floor) | | declared `0.5.0`, breaking in log | `1.0.0` (the larger wins) | | declared below the tag (stale) | ignored | | declared `0.10.0` vs tag `0.9.0` | `0.10.0` (`sort -V`, not a string compare) | | dispatch, blank version, no code | `0.3.1` | | dispatch, `bump=minor`, no code | `0.4.0` | | dispatch, exact `1.2.3` | `1.2.3` | | dispatch, exact version already released | stands down | | dispatch `patch` (default) on a `feat:` log | `0.4.0`, not `0.3.1` | | push event with the inputs absent | unaffected | The bundle's `module.json` rewrite and its version assertion were run directly against this repo's manifest: the rewrite passes, an un-rewritten `module.json` fails with `bundle declares 0.3.0, but this is release 0.4.0`, and a missing `client/dist/entry.js` still fails as before. YAML parses; 9 steps in the expected order. `jq` is not on the dev box, so the two `jq` lines were verified by reading only — the runner has it and the workflow already depended on it before this change. ## Checklist - [x] I have read [CONTRIBUTING.md](CONTRIBUTING.md). - [x] The change builds and existing tests/checks pass locally. - [x] I have added or updated tests/docs where it makes sense. - [x] My commits are reasonably scoped with clear messages. ## AI-assisted contributions (required) - [ ] No AI tools were used to produce this contribution. - [x] AI tools were used. Tool(s): `Claude Code (Opus 5)`. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a `Co-Authored-By` / `Assisted-By` trailer. ## License - [x] I agree that my contribution is licensed under this project's license (**GNU GPL v3.0 or later**), and I have the right to contribute it. --- Pairs with **RunicGateway/docs#171** (MODULE_SYSTEM.md §2.7.1 and settled decision 19) and **RunicGateway/Integration-kit#8** (the template and chapter 2). Merge the docs one alongside this.
wtclaude added 1 commit 2026-08-19 18:02:58 +00:00
ci(release): derive the version from commit subjects, and add a manual backdoor
All checks were successful
PR Checks / client-build (pull_request) Successful in 18s
PR Checks / server-tests (pull_request) Successful in 20s
PR Checks / frozen-manifest (pull_request) Successful in 56s
8ec21086b5
A bundle used to cost a second pull request whose entire content was a number.
The workflow published only when a merge to `main` left `module.json` at a
version with no release yet, so a merge that did not touch that line released
nothing. Measured rather than argued: v0.3.0 (2026-08-12) is the only release
this repo has ever cut, while the nine Teams phases and the cutover landed on
`main` in the week since.

Adopt the engine `link` and `installer` already run - feat!/BREAKING CHANGE ->
major, feat -> minor, fix|perf -> patch, nothing releasable -> no release. The
number that ships is the tag, and CI writes it into the `module.json` inside the
bundle, with an assertion that the rewrite happened: a bundle carrying the wrong
version would install under a number that is not the one it was released as.

`module.json`'s version is kept as a floor rather than deleted - a version above
the newest tag still releases at that version - so the declared model survives as
the special case it always was, and is still how a `coreApi` bump overrules the
subjects. `workflow_dispatch` covers what the rules cannot reach: leave `version`
blank to bump the newest tag by `bump`, or name an exact version. Where the log
and the input disagree the larger bump wins, because a button pressed on a log
full of `feat:` would otherwise publish its `patch` default over a minor's worth
of work.

Two things carried over from `link` at the same time: a tag pushed without a
release behind it is now recovered rather than making that state permanent, and
the changelog moved into the plan step (so the assemble step clears `$OUT`, not
`dist/`, which now holds it).

On this repo's `main` the engine computes v0.4.0. The workflow still never
writes to a branch.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit 16cfbe194d into main 2026-08-19 18:07:57 +00:00
whitlocktech deleted branch feature/release-per-merge 2026-08-19 18:07:58 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/Module-uo#17
No description provided.