Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 91 additions & 2 deletions .github/workflows/release-script-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 (@(
' <artifactId>graph-compose</artifactId>',
' <version>1.2.3</version>',
'dependencies { implementation("io.github.demchaav:graph-compose:1.2.3") }',
' <artifactId>graph-compose-testing</artifactId>',
' <version>1.2.2</version>',
'dependencies { testImplementation("io.github.demchaav:graph-compose-testing:1.2.2") }',
' <artifactId>graph-compose-fonts</artifactId>',
' <version>1.1.0</version>',
'dependencies { implementation("io.github.demchaav:graph-compose-emoji:1.0.0") }'
) -join "`n")
$expected = @(
' <artifactId>graph-compose</artifactId>',
' <version>9.9.9</version>',
'dependencies { implementation("io.github.demchaav:graph-compose:9.9.9") }',
' <artifactId>graph-compose-testing</artifactId>',
' <version>9.9.9</version>',
'dependencies { testImplementation("io.github.demchaav:graph-compose-testing:9.9.9") }',
' <artifactId>graph-compose-fonts</artifactId>',
' <version>1.1.0</version>',
'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 '<artifactId>(graph-compose[\w-]*)</artifactId>') {
$artifact = $Matches[1]
if ($artifact -notin $companions) {
$window = $new[$i..([Math]::Min($i + 3, $new.Count - 1))] -join "`n"
$version = [regex]::Match($window, '<version>([^<]+)</version>').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: |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ Add the testing artifact at test scope:
<dependency>
<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose-testing</artifactId>
<version>2.3.0</version>
<version>2.4.0</version>
<scope>test</scope>
</dependency>
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,17 @@ void theOpenChangelogEntryNamesTheVersionUnderDevelopment() throws Exception {
.isNull();
}

/**
* The root README's install snippets advertise the version a reader can resolve.
*
* <p>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/}.</p>
*/
@Test
void readmeInstallSnippetsMatchTheProjectVersion() throws Exception {
Set<String> targets = acceptableTargets();
Expand All @@ -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();
}

/**
Expand Down Expand Up @@ -670,15 +685,7 @@ void companionReadmeInstallSnippetsMatchTheirPomVersions() throws Exception {
void documentationInstallSnippetsMatchTheProjectVersion() throws Exception {
Set<String> trainTargets = acceptableTargets();
List<DocCoordinate> coordinates = documentationCoordinates();
List<String> drift = new ArrayList<>();

for (DocCoordinate coordinate : coordinates) {
Set<String> 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<String> drift = driftIn(coordinates, trainTargets);

assertThat(coordinates)
.describedAs("no versioned install coordinate found under docs/ — this guard would cover nothing")
Expand Down Expand Up @@ -750,17 +757,40 @@ private static List<DocCoordinate> 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<DocCoordinate> coordinatesIn(String page, String text) {
List<DocCoordinate> 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<String> driftIn(List<DocCoordinate> coordinates, Set<String> trainTargets) throws Exception {
List<String> drift = new ArrayList<>();
for (DocCoordinate coordinate : coordinates) {
Set<String> 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<Set<String>> docPageLists(String script) {
List<Set<String>> lists = new ArrayList<>();
Expand Down
2 changes: 1 addition & 1 deletion docs/contributing/release-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <X.Y.Z>` 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 `<X.Y.Z>` 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 `<X.Y.Z>` 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<X.Y.Z> — Planned` to `## v<X.Y.Z> — <today-ISO>`.
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.)

Expand Down
20 changes: 13 additions & 7 deletions scripts/cut-release.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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: <artifactId>graphcompose</artifactId><version>X.Y.Z</version>
# Gradle: implementation("io.github.demchaav:graphcompose:X.Y.Z")
# never earlier). Every train coordinate the page names carries it, in both
# forms:
# Maven: <artifactId>graph-compose[-module]</artifactId><version>X.Y.Z</version>
# 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
# (<artifactId>GraphCompose</artifactId> / GraphCompose:vX.Y.Z) so
# the script still works if a future change re-introduces a JitPack
# snippet for documentation purposes.
$mavenCentralRegex = [regex]'(?<=<artifactId>graph-compose</artifactId>\s*<version>)v?[\w\.\-]+(?=</version>)'
$afterMaven = $mavenCentralRegex.Replace($content, $newVersion, 1)
$mavenCentralRegex = [regex]'(?<=<artifactId>graph-compose(?!-fonts|-emoji)[\w\-]*</artifactId>\s*<version>)v?[\w\.\-]+(?=</version>)'
$afterMaven = $mavenCentralRegex.Replace($content, $newVersion)
if ($content -ne $afterMaven) {
$content = $afterMaven
$changed = $true
Expand All @@ -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
Expand Down
Loading