From 5967d05b4cf4350de23cf2623b3c3d30b5432f6f Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Mon, 21 Sep 2026 16:11:07 +0200 Subject: [PATCH 1/2] docs(release-process): the 0.x to 1.0 release, and the order for a coupled pair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two sections for a release shape the skill did not cover. Both come from taking nr_passkeys_be and nr_passkeys_fe to 1.0 on 2026-09-20/21. "The 0.x to 1.0 Release" names what changes besides the number: the semver promise belongs in the changelog and in the ecosystem's own state field (a TYPO3 extension can say 1.0.0 everywhere while ext_emconf still says beta); removals need migrated consumers; and every suite the repository ships has to be invoked by CI, with the grep that answers it mechanically. nr_passkeys_fe 1.0.0 shipped 19 end-to-end specifications that every file skipped and no workflow ran — the release notes had to admit it. "Releasing a Coupled Pair" gives the order: consumer migrates, dependency removes, consumer opens its constraint. nr_passkeys_be 1.0.0 removed two methods after a grep of its own repository only; the frontend called both at three sites, and the broken pair was unreachable by luck rather than by order. Assisted-by: claude-code:claude-opus-5 Agent-Session: https://claude.ai/code/session_01CfKQd7MhomydPqLFFoawMG Agent-Host: 0493f0 Signed-off-by: Sebastian Mendel --- skills/github-release/SKILL.md | 2 +- .../references/release-process.md | 69 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/skills/github-release/SKILL.md b/skills/github-release/SKILL.md index c6b9e91..57f8c29 100644 --- a/skills/github-release/SKILL.md +++ b/skills/github-release/SKILL.md @@ -71,7 +71,7 @@ Steps 8 and 9 collapse into that second command: it publishes and carries the no ## References -- `references/release-process.md` +- `references/release-process.md` — the flow; also the 0.x → 1.0 release (what a first stable promises, and the suites CI must actually invoke) and the order for a coupled pair - `references/ecosystem-detection.md` — version-file patterns - `references/immutable-releases.md` — immutable releases, tag burning - `references/supply-chain-security.md` — SLSA, Sigstore diff --git a/skills/github-release/references/release-process.md b/skills/github-release/references/release-process.md index 86d540e..bca3708 100644 --- a/skills/github-release/references/release-process.md +++ b/skills/github-release/references/release-process.md @@ -407,6 +407,45 @@ When releasing many repositories that share one reusable release workflow (e.g. 4. **Run the notes overhaul as a separate pass.** The Phase 5 description overhaul is the step most likely to run out of context in a combined loop — do the bump→merge→tag→publish pipeline for the whole batch first, then a second pass for narrative notes via `gh release edit --notes-file`. 5. **Handle "drift" repos.** If a repo's version file was already bumped ahead of its latest tag, the next tag is `max(natural_bump, current_version_in_file)` — use the pre-bumped value (unless that version was already published/burned, in which case bump higher). +## The 0.x → 1.0 Release: What Changes Besides the Number + +A patch or minor release ships changes. A first stable release ships a **promise**, and three +things that were optional until then become binding. It is also the release a project does once, +so nobody has the routine. + +**1. The promise itself, written where a consumer reads it.** From 1.0 on, an incompatible change +to a public class, method or setting needs a new major. Put that sentence in the changelog entry, +not only in a commit message, and set the state that carries it in the ecosystem's own metadata — +for a TYPO3 extension `'state' => 'stable'` in `ext_emconf.php`, which is a separate field from +the version and is easy to leave at `beta` while every version surface says 1.0.0. + +**2. Every deprecated API the release removes has its consumers migrated first.** See the next +section for the order; at 1.0 this is not a nicety, because a 1.0 is where the removals get +batched. + +**3. Every test suite the repository ships is invoked by CI.** A suite that exists but is never +run is not a gate, and a first stable release is where that gap becomes a claim about quality. +Enumerate them mechanically rather than from memory: + +```bash +# suites the repo declares +grep -o 'name="[^"]*"' Build/phpunit*.xml phpunit*.xml 2>/dev/null | sort -u +ls -d Tests/*/ 2>/dev/null + +# what CI actually invokes +grep -rhoE '(runTests\.sh -s [a-z:]+|phpunit[^|]*--testsuite [a-z]+|npx (playwright|vitest)[a-z ]*)' \ + .github/workflows/ | sort -u +``` + +A suite in the first list and not in the second is either wired up before the release or named in +the release notes as not running. `nr_passkeys_fe` 1.0.0 shipped 19 end-to-end specifications that +every file skipped with a blanket `test.skip()` and no workflow invoked; the release notes had to +say so, and the suite was built for real in 1.0.1. + +Two more surfaces that only matter at 1.0: the documentation version (a docs build pinned to +`main` keeps serving the pre-release manual), and a dependency range that still allows the 0.x of a +sibling package you are releasing in the same batch. + ## Releasing a Dependency: the Consumer's CI Races the Registry When the repo you just released is a **dependency of another repo you are also working on**, the consumer's PR has a window in which its CI is guaranteed to fail for a reason that has nothing to do with its code. @@ -451,6 +490,36 @@ composer show "netresearch/nr-vault" "$VERSION" >/dev/null 2>&1 && echo availabl Observed 2026-07-30: 31 red checks on a consumer PR, zero real defects — CI created 16:13:27, release published 16:22:59. + +## Releasing a Coupled Pair: the Consumer Migrates First + +Where two packages you maintain are coupled — a frontend extension importing the backend +extension's services, a library and its bundle — a release that **removes** something they share +has an order, and the wrong order ships a combination that cannot work. + +1. The consumer migrates to the new API and merges, while its constraint still allows the old + dependency version. +2. The dependency releases the removal. +3. The consumer opens its constraint to the new major and releases. + +Doing it the other way round — remove first, migrate afterwards — leaves a window in which the +published pair is broken for anyone who installs both at their newest version. + +Two checks before the removal, neither of which the dependency's own repository can answer: + +```bash +# the consumer's source, not the dependency's +grep -rn "removedMethodName" /path/to/consumer/Classes/ + +# who else declares the dependency +gh search code "netresearch/the-dependency" --owner netresearch --limit 50 +``` + +Measured in this fleet on 2026-09-20: `nr_passkeys_be` 1.0.0 removed two `RateLimiterService` +methods after a grep that covered only its own repository. `nr_passkeys_fe` called both, at three +sites. The broken pair was unreachable only because the consumer's `^0.12` constraint refused the +new major — luck, not order. + ## Prove an Unproven Pipeline With an `-rc` Tag First Before the first real tag on a pipeline that has not succeeded **in its current From 84a7d1e3702fbd4856aade526b422981f0db8d6f Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Tue, 22 Sep 2026 00:42:31 +0200 Subject: [PATCH 2/2] fix(release-process): say what the suite check is, and widen the consumer grep Two review findings, both valid. The two suite-listing commands do not produce comparable identifiers and miss a suite launched through a project script or a reusable workflow. They are now labelled a prompt rather than a comparison, with a second command that lists the reusable callers to follow, and the instruction to place each declared suite by hand. The consumer grep looked only in Classes/. A caller can sit in a template, in JavaScript, in a fixture or in configuration, so it is now a repository-wide git grep with vendor, .Build and node_modules excluded. Assisted-by: claude-code:claude-opus-5 Agent-Session: https://claude.ai/code/session_01CfKQd7MhomydPqLFFoawMG Agent-Host: 0493f0 Signed-off-by: Sebastian Mendel --- .../references/release-process.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/skills/github-release/references/release-process.md b/skills/github-release/references/release-process.md index bca3708..4d77f08 100644 --- a/skills/github-release/references/release-process.md +++ b/skills/github-release/references/release-process.md @@ -425,20 +425,24 @@ batched. **3. Every test suite the repository ships is invoked by CI.** A suite that exists but is never run is not a gate, and a first stable release is where that gap becomes a claim about quality. -Enumerate them mechanically rather than from memory: +List both sides and read them side by side. This is a **prompt, not a comparison**: the two +commands do not produce matching identifiers, and neither sees a suite launched through a +project script or a reusable workflow. It is here so the question gets asked at all. ```bash # suites the repo declares grep -o 'name="[^"]*"' Build/phpunit*.xml phpunit*.xml 2>/dev/null | sort -u ls -d Tests/*/ 2>/dev/null -# what CI actually invokes +# what CI invokes — plus the callers, which hide the command in another repository grep -rhoE '(runTests\.sh -s [a-z:]+|phpunit[^|]*--testsuite [a-z]+|npx (playwright|vitest)[a-z ]*)' \ .github/workflows/ | sort -u +grep -rhoE 'uses: [^ ]+\.ya?ml@' .github/workflows/ | sort -u # follow each one ``` -A suite in the first list and not in the second is either wired up before the release or named in -the release notes as not running. `nr_passkeys_fe` 1.0.0 shipped 19 end-to-end specifications that +For every suite in the first list, name where the second list runs it, and follow a reusable +workflow into its own repository rather than assuming its name covers the suite. A suite you +cannot place is either wired up before the release or named in the release notes as not running. `nr_passkeys_fe` 1.0.0 shipped 19 end-to-end specifications that every file skipped with a blanket `test.skip()` and no workflow invoked; the release notes had to say so, and the suite was built for real in 1.0.1. @@ -508,8 +512,9 @@ published pair is broken for anyone who installs both at their newest version. Two checks before the removal, neither of which the dependency's own repository can answer: ```bash -# the consumer's source, not the dependency's -grep -rn "removedMethodName" /path/to/consumer/Classes/ +# the consumer's whole tree, not just Classes/ — a caller can sit in a template, +# in JavaScript, in a fixture or in configuration +git -C /path/to/consumer grep -n "removedMethodName" -- ':!vendor' ':!.Build' ':!node_modules' # who else declares the dependency gh search code "netresearch/the-dependency" --owner netresearch --limit 50