diff --git a/.github/workflows/release-script-check.yml b/.github/workflows/release-script-check.yml
index f329d5c15..171763e82 100644
--- a/.github/workflows/release-script-check.yml
+++ b/.github/workflows/release-script-check.yml
@@ -6,8 +6,8 @@ name: Release Script Check
# unnoticed and only surface mid-release. This runs the script's dry-runs — which
# exercise every code path except the actual git/Maven mutations — plus focused unit
# checks of the helpers a dry-run cannot reach on its own: the version arithmetic, the
-# README and roadmap promotions, the japicmp baseline move, and the knowledge-tooling
-# refusal. It creates no tag and touches no remote.
+# README install-snippet bump, the README and roadmap promotions, the japicmp baseline
+# move, and the knowledge-tooling refusal. It creates no tag and touches no remote.
on:
push:
@@ -274,6 +274,95 @@ jobs:
Write-Host "release-status: a staged story is promoted, a patch keeps its line's headline."
+ - name: Unit-check the README install-snippet bump
+ shell: pwsh
+ run: |
+ # Every train coordinate the root README names moves with a final cut, not only
+ # graph-compose: v2.4.0 shipped graph-compose-testing:2.3.0 beside
+ # graph-compose:2.4.0 because the bump matched the bare coordinate alone. The
+ # companions version on their own tags and must stay put. Lifted by AST like the
+ # checks around it, so the code under test is the code that ships.
+ $path = (Resolve-Path scripts/cut-release.ps1).Path
+ $ast = [System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$null, [ref]$null)
+ $fn = $ast.FindAll({
+ param($n)
+ $n -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $n.Name -eq 'Update-ReadmeInstallVersion'
+ }, $true) | Select-Object -First 1
+ if (-not $fn) { throw 'cut-release.ps1 no longer defines Update-ReadmeInstallVersion' }
+ $calls = $ast.FindAll({
+ param($n)
+ $n -is [System.Management.Automation.Language.CommandAst] -and $n.GetCommandName() -eq 'Update-ReadmeInstallVersion'
+ }, $true)
+ if ($calls.Count -eq 0) { throw 'nothing in cut-release.ps1 calls Update-ReadmeInstallVersion' }
+ Invoke-Expression $fn.Extent.Text
+ function Note($m) { Write-Host " $m" }
+ $DryRun = $false
+
+ # 1. A fixture carrying every shape, against its exact expected text. The test
+ # coordinate starts a release behind, as it did on the README that shipped.
+ $fixture = Join-Path ([IO.Path]::GetTempPath()) ("readme-" + [guid]::NewGuid().ToString() + ".md")
+ Set-Content -Path $fixture -NoNewline -Value (@(
+ ' graph-compose',
+ ' 1.2.3',
+ 'dependencies { implementation("io.github.demchaav:graph-compose:1.2.3") }',
+ ' graph-compose-testing',
+ ' 1.2.2',
+ 'dependencies { testImplementation("io.github.demchaav:graph-compose-testing:1.2.2") }',
+ ' graph-compose-fonts',
+ ' 1.1.0',
+ 'dependencies { implementation("io.github.demchaav:graph-compose-emoji:1.0.0") }'
+ ) -join "`n")
+ $expected = @(
+ ' graph-compose',
+ ' 9.9.9',
+ 'dependencies { implementation("io.github.demchaav:graph-compose:9.9.9") }',
+ ' graph-compose-testing',
+ ' 9.9.9',
+ 'dependencies { testImplementation("io.github.demchaav:graph-compose-testing:9.9.9") }',
+ ' graph-compose-fonts',
+ ' 1.1.0',
+ 'dependencies { implementation("io.github.demchaav:graph-compose-emoji:1.0.0") }'
+ ) -join "`n"
+ Update-ReadmeInstallVersion $fixture '9.9.9'
+ $after = Get-Content $fixture -Raw
+ if ($after -ne $expected) { throw "the fixture did not bump to its expected text:`n$after" }
+ Update-ReadmeInstallVersion $fixture '9.9.9'
+ if ((Get-Content $fixture -Raw) -ne $expected) { throw 'a second bump changed the fixture' }
+
+ # 2. The real README: nothing left behind, and nothing but version tokens moved.
+ $copy = Join-Path ([IO.Path]::GetTempPath()) ("readme-real-" + [guid]::NewGuid().ToString() + ".md")
+ Copy-Item README.md $copy
+ Update-ReadmeInstallVersion $copy '9.9.9'
+ $old = @(Get-Content README.md)
+ $new = @(Get-Content $copy)
+ if ($old.Count -ne $new.Count) { throw 'the bump changed the README line count' }
+ $companions = @('graph-compose-fonts', 'graph-compose-emoji')
+ $checked = 0
+ for ($i = 0; $i -lt $new.Count; $i++) {
+ if ($old[$i] -ne $new[$i] -and $new[$i] -notmatch '9\.9\.9') {
+ throw "README line $($i + 1) changed without taking the new version: $($new[$i])"
+ }
+ if ($new[$i] -match '(graph-compose[\w-]*)') {
+ $artifact = $Matches[1]
+ if ($artifact -notin $companions) {
+ $window = $new[$i..([Math]::Min($i + 3, $new.Count - 1))] -join "`n"
+ $version = [regex]::Match($window, '([^<]+)').Groups[1].Value
+ if ($version -ne '9.9.9') { throw "README $artifact Maven snippet reads '$version' after the bump" }
+ $checked++
+ }
+ }
+ foreach ($gradle in [regex]::Matches($new[$i], 'io\.github\.demchaav:(graph-compose[\w-]*):([^"'')\s]+)')) {
+ if ($gradle.Groups[1].Value -in $companions) { continue }
+ if ($gradle.Groups[2].Value -ne '9.9.9') {
+ throw "README $($gradle.Groups[1].Value) Gradle snippet reads '$($gradle.Groups[2].Value)' after the bump"
+ }
+ $checked++
+ }
+ }
+ if ($checked -lt 2) { throw "the README should carry at least the graph-compose Maven and Gradle snippets; checked $checked" }
+
+ Write-Host "README install snippets: every train coordinate moves, companions stay, idempotent; real README $checked coordinates checked."
+
- name: Unit-check the roadmap promotion (refusals and the real rewrite)
shell: pwsh
run: |
diff --git a/README.md b/README.md
index e49e19abf..f36c3252c 100644
--- a/README.md
+++ b/README.md
@@ -194,7 +194,7 @@ Add the testing artifact at test scope:
io.github.demchaav
graph-compose-testing
- 2.3.0
+ 2.4.0
test
```
diff --git a/core/src/test/java/com/demcha/documentation/VersionConsistencyGuardTest.java b/core/src/test/java/com/demcha/documentation/VersionConsistencyGuardTest.java
index a2a1fefb6..2737f5fc6 100644
--- a/core/src/test/java/com/demcha/documentation/VersionConsistencyGuardTest.java
+++ b/core/src/test/java/com/demcha/documentation/VersionConsistencyGuardTest.java
@@ -555,6 +555,17 @@ void theOpenChangelogEntryNamesTheVersionUnderDevelopment() throws Exception {
.isNull();
}
+ /**
+ * The root README's install snippets advertise the version a reader can resolve.
+ *
+ *
The {@code graph-compose} Maven and Gradle snippets have to be there, and so does
+ * every other GraphCompose coordinate the page tells a reader to add. v2.4.0 shipped a
+ * README whose {@code graph-compose-testing} snippet still read 2.3.0 beside
+ * {@code graph-compose} 2.4.0: this check and the release script's bump both matched
+ * the bare {@code graph-compose} coordinate alone, and the train is only a
+ * tested-compatible set when every module carries the same version. A companion
+ * coordinate (fonts, emoji) is held to its own pom, as it is under {@code docs/}.
+ */
@Test
void readmeInstallSnippetsMatchTheProjectVersion() throws Exception {
Set targets = acceptableTargets();
@@ -569,6 +580,10 @@ void readmeInstallSnippetsMatchTheProjectVersion() throws Exception {
assertThat(gradleSnippetVersion)
.describedAs("README Gradle install snippet must reference the latest published release, or the release version in a release commit (one of %s)", targets)
.isIn(targets);
+ assertThat(driftIn(coordinatesIn("README.md", readme), targets))
+ .describedAs("every GraphCompose install coordinate in README.md must advertise the version a "
+ + "reader can resolve beside graph-compose, not only graph-compose itself")
+ .isEmpty();
}
/**
@@ -670,15 +685,7 @@ void companionReadmeInstallSnippetsMatchTheirPomVersions() throws Exception {
void documentationInstallSnippetsMatchTheProjectVersion() throws Exception {
Set trainTargets = acceptableTargets();
List coordinates = documentationCoordinates();
- List drift = new ArrayList<>();
-
- for (DocCoordinate coordinate : coordinates) {
- Set expected = expectedVersionsFor(coordinate.artifact(), trainTargets);
- if (!expected.contains(coordinate.version())) {
- drift.add("%s:%d %s advertises %s, expected one of %s".formatted(
- coordinate.page(), coordinate.line(), coordinate.artifact(), coordinate.version(), expected));
- }
- }
+ List drift = driftIn(coordinates, trainTargets);
assertThat(coordinates)
.describedAs("no versioned install coordinate found under docs/ — this guard would cover nothing")
@@ -750,17 +757,40 @@ private static List documentationCoordinates() throws IOException
for (Path page : pages) {
String text = Files.readString(page);
String name = PROJECT_ROOT.relativize(page).toString().replace('\\', '/');
- for (Pattern pattern : List.of(DOCS_MAVEN_COORDINATE, DOCS_GRADLE_COORDINATE)) {
- Matcher coordinate = pattern.matcher(text);
- while (coordinate.find()) {
- found.add(new DocCoordinate(name, lineOf(text, coordinate.start()),
- coordinate.group(1), coordinate.group(2).trim()));
- }
+ found.addAll(coordinatesIn(name, text));
+ }
+ return found;
+ }
+
+ /** Every versioned GraphCompose coordinate in one page, Maven form first, then Gradle. */
+ private static List coordinatesIn(String page, String text) {
+ List found = new ArrayList<>();
+ for (Pattern pattern : List.of(DOCS_MAVEN_COORDINATE, DOCS_GRADLE_COORDINATE)) {
+ Matcher coordinate = pattern.matcher(text);
+ while (coordinate.find()) {
+ found.add(new DocCoordinate(page, lineOf(text, coordinate.start()),
+ coordinate.group(1), coordinate.group(2).trim()));
}
}
return found;
}
+ /**
+ * The coordinates that advertise a version a reader cannot resolve beside the rest of
+ * the train: a train artifact off {@code trainTargets}, or a companion off its own pom.
+ */
+ private List driftIn(List coordinates, Set trainTargets) throws Exception {
+ List drift = new ArrayList<>();
+ for (DocCoordinate coordinate : coordinates) {
+ Set expected = expectedVersionsFor(coordinate.artifact(), trainTargets);
+ if (!expected.contains(coordinate.version())) {
+ drift.add("%s:%d %s advertises %s, expected one of %s".formatted(
+ coordinate.page(), coordinate.line(), coordinate.artifact(), coordinate.version(), expected));
+ }
+ }
+ return drift;
+ }
+
/** The {@code $docPage} lists in {@code cut-release.ps1}, in source order: bump first, staging second. */
private static List> docPageLists(String script) {
List> lists = new ArrayList<>();
diff --git a/docs/contributing/release-process.md b/docs/contributing/release-process.md
index c560c0846..fc78bf748 100644
--- a/docs/contributing/release-process.md
+++ b/docs/contributing/release-process.md
@@ -75,7 +75,7 @@ The script's Step 1–4 mutates these. The agent only confirms the *current stat
Running `pwsh ./scripts/cut-release.ps1 -Version ` performs:
1. **Pre-flight** — re-checks all of A above (branch, clean tree, in-sync, no existing tag), and that the knowledge tooling this cut will need is installed (see *Knowledge tooling is a pre-flight condition* below).
-2. **Bump versions** to `` across all 13 train poms (§0.D), **and** the install snippets — the root `README.md` plus every per-module README, which a separate pass (`Update-ModuleReadmeInstallVersion`) rewrites because their coordinates carry an artifact suffix the root regex does not match. All in one pass, so `VersionConsistencyGuardTest` stays green at Step 5. (`fonts/pom.xml` and `emoji/pom.xml` are left alone — they version independently; see §2.D.)
+2. **Bump versions** to `` across all 13 train poms (§0.D), **and** the install snippets — every train coordinate in the root `README.md` (`graph-compose` and any module the page tells a reader to add beside it, such as `graph-compose-testing`), then every per-module README and the listed documentation pages through a separate pass (`Update-ModuleReadmeInstallVersion`). Both passes skip `graph-compose-fonts` and `graph-compose-emoji`. All in the same step, so `VersionConsistencyGuardTest` stays green at Step 5. (`fonts/pom.xml` and `emoji/pom.xml` are left alone — they version independently; see §2.D.)
3. **Date the CHANGELOG** — flips `## v — Planned` to `## v — `.
3b. **Validate release metadata** — a fast, build-free pre-tag gate: the CHANGELOG is dated for the target, the README `Latest stable` prose block names the target, the ROADMAP `Current stable` section names the target, the README install snippet reads the target, and every published-train pom carries the target version. Fails immediately (before showcase / verify / commit / tag) if any is stale. These are post-mutation checks: Step 2 rewrites both prose blocks (`Update-ReadmeReleaseStatus`, `Update-RoadmapCurrentStable`), and this step confirms the rewrite landed rather than trusting it. (The full per-module + Gradle + showcase snippet consistency is separately enforced by `VersionConsistencyGuardTest` in the Step-5 verify, which also holds the ROADMAP section to a published version — so a section left behind would fail the cut *after* the poms had already moved. That is why the script owns it.)
diff --git a/scripts/cut-release.ps1 b/scripts/cut-release.ps1
index cda2b523f..1c13fdafd 100644
--- a/scripts/cut-release.ps1
+++ b/scripts/cut-release.ps1
@@ -754,16 +754,22 @@ function Update-ReadmeInstallVersion($readmePath, $newVersion) {
# tag is cut from, so a new user who copy-pastes the README resolves
# the version this release actually publishes (Phase 2.3 of the
# release skill: README version flips at release-execution time,
- # never earlier). Two snippets carry it:
- # Maven: graphcomposeX.Y.Z
- # Gradle: implementation("io.github.demchaav:graphcompose:X.Y.Z")
+ # never earlier). Every train coordinate the page names carries it, in both
+ # forms:
+ # Maven: graph-compose[-module]X.Y.Z
+ # Gradle: implementation("io.github.demchaav:graph-compose[-module]:X.Y.Z")
+ # That is graph-compose itself and any module the README tells a reader to add
+ # beside it — graph-compose-testing at test scope, for one. Matching the bare
+ # coordinate alone left that snippet a release behind on v2.4.0. As in
+ # Update-ModuleReadmeInstallVersion, graph-compose-fonts and graph-compose-emoji
+ # are excluded: they ship on their own fonts-v* / emoji-v* tags.
# Lookbehind/lookahead so only the version token is rewritten. A
# secondary fallback handles the legacy JitPack format
# (GraphCompose / GraphCompose:vX.Y.Z) so
# the script still works if a future change re-introduces a JitPack
# snippet for documentation purposes.
- $mavenCentralRegex = [regex]'(?<=graph-compose\s*)v?[\w\.\-]+(?=)'
- $afterMaven = $mavenCentralRegex.Replace($content, $newVersion, 1)
+ $mavenCentralRegex = [regex]'(?<=graph-compose(?!-fonts|-emoji)[\w\-]*\s*)v?[\w\.\-]+(?=)'
+ $afterMaven = $mavenCentralRegex.Replace($content, $newVersion)
if ($content -ne $afterMaven) {
$content = $afterMaven
$changed = $true
@@ -778,8 +784,8 @@ function Update-ReadmeInstallVersion($readmePath, $newVersion) {
}
}
- $gradleCentralRegex = [regex]'(?<=io\.github\.demchaav:graph-compose:)v?[\w\.\-]+(?=")'
- $afterGradle = $gradleCentralRegex.Replace($content, $newVersion, 1)
+ $gradleCentralRegex = [regex]'(?<=io\.github\.demchaav:graph-compose(?!-fonts|-emoji)[\w\-]*:)v?[\w\.\-]+(?=")'
+ $afterGradle = $gradleCentralRegex.Replace($content, $newVersion)
if ($content -ne $afterGradle) {
$content = $afterGradle
$changed = $true