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