-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathcut-release.ps1
More file actions
1137 lines (1050 loc) · 56.1 KB
/
Copy pathcut-release.ps1
File metadata and controls
1137 lines (1050 loc) · 56.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<#
.SYNOPSIS
Cuts a GraphCompose release: bumps pom versions, updates the
showcase site links, regenerates the examples manifest, runs
verify, commits, tags, pushes — then flips the showcase links
back to the release branch so ongoing dev work stays linkable.
.DESCRIPTION
Modes:
Default — full release cut. Requires the release branch
(-Branch, default develop) clean and in sync
with origin. Performs all 10 steps end to end
with prompts before each remote action (push
the branch, push tag, merge to main).
-DryRun — print every step without executing it. Use
to preview what the release will do.
-SkipPush — perform local changes (bump, tag, commit)
but do NOT push. Useful for staging a
release locally before publishing.
-PostReleaseOnly — skip the release work entirely; open the next
development line. Bumps every train pom to the next
patch -SNAPSHOT (leaving the README/showcase install
snippets on the just-published release), runs
`mvnw validate` + VersionConsistencyGuardTest to
validate the bump, flips ShowcaseMetadata.GH_BASE back
to /blob/<branch>, re-runs ShowcaseSync, then commits +
pushes. Runs the same branch / clean-tree / origin-sync
preflight as a real cut. Idempotent: skips the bump when
the poms are already a -SNAPSHOT.
-SkipVerify — skip the mvnw verify gate. Only use when
you've just run verify yourself and don't
want to wait another minute.
-SkipShowcase — skip the showcase steps (GH_BASE flip +
ShowcaseSync regen) and leave the showcase
out of the release commit. Use for a pure
code release where no example render changed,
so you don't need fresh generated PDFs. The
version bump still updates web/index.html.
.EXAMPLE
pwsh ./scripts/cut-release.ps1 -Version 1.6.0
# full release of v1.6.0
.EXAMPLE
pwsh ./scripts/cut-release.ps1 -Version 1.6.0 -DryRun
# preview what would happen
.EXAMPLE
pwsh ./scripts/cut-release.ps1 -Version 2.0.0-rc.1 -Branch 2.0-dev -DryRun
# preview the 2.0 release-candidate cut from 2.0-dev
.EXAMPLE
pwsh ./scripts/cut-release.ps1 -PostReleaseOnly -Branch 2.0-dev
# post-release: flip showcase links back to /blob/2.0-dev
.NOTES
Author: Artem Demchyshyn
Pre-conditions:
- on the release branch (-Branch, default develop)
- working tree clean
- the release branch in sync with its origin
- tag v$Version doesn't already exist
Post-release reminder: after pushing the tag, merge the release
branch into main so GitHub Pages picks up the new docs, then run
this script with -PostReleaseOnly -Branch <branch> to open the next
-SNAPSHOT development line and flip the showcase links back.
#>
[CmdletBinding(DefaultParameterSetName='Release')]
param(
# The release branch to cut from / push to. Defaults to `develop` (the 1.9.x
# line); pass `-Branch 2.0-dev` to cut the 2.0 line. Common to both modes.
[string]$Branch = 'develop',
[Parameter(Mandatory=$true, ParameterSetName='Release')]
[string]$Version,
# Common to both modes so -PostReleaseOnly can also be previewed.
[switch]$DryRun,
[Parameter(ParameterSetName='Release')]
[switch]$SkipPush,
[Parameter(ParameterSetName='Release')]
[switch]$SkipVerify,
[Parameter(ParameterSetName='Release')]
[switch]$SkipShowcase,
[Parameter(Mandatory=$true, ParameterSetName='PostRelease')]
[switch]$PostReleaseOnly
)
$ErrorActionPreference = 'Stop'
$repoRoot = (Resolve-Path "$PSScriptRoot/..").Path
$showcaseMetadata = Join-Path $repoRoot 'examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java'
$mvnw = Join-Path $repoRoot 'mvnw.cmd'
function Step($n, $title) {
Write-Host ""
Write-Host "[$n] $title" -ForegroundColor Cyan
}
function Note($message) {
Write-Host " $message" -ForegroundColor DarkGray
}
function Run($command) {
if ($DryRun) {
Write-Host " [DRY RUN] $command" -ForegroundColor Yellow
} else {
Write-Host " > $command" -ForegroundColor DarkGray
Invoke-Expression $command
if ($LASTEXITCODE -ne 0) {
throw "Command failed (exit $LASTEXITCODE): $command"
}
}
}
function Assert-BranchPreflight($branch) {
# Shared safety gate for BOTH a full release cut and -PostReleaseOnly: the current
# branch is the target branch, the working tree is clean, and the local branch is
# in sync with origin. In -DryRun the gate is relaxed so the flow can be previewed
# from any branch (e.g. while iterating on this script); live runs fail loudly.
$currentBranch = (git rev-parse --abbrev-ref HEAD).Trim()
if ($DryRun) {
Note "branch: $currentBranch (gate relaxed for -DryRun)"
return
}
# 1. On the target branch?
if ($currentBranch -ne $branch) {
throw "Not on $branch branch (currently on $currentBranch). Switch to $branch first."
}
Note "branch: $branch OK"
# 2. Working tree clean? `git status --porcelain` reports STAGED (column 1),
# unstaged (column 2), and untracked entries — any output means not clean.
if (git status --porcelain) {
throw "Working tree has uncommitted or staged changes. Commit or stash first."
}
Note "working tree: clean (incl. staged) OK"
# 3. Local branch in sync with origin? A silently-failed fetch (network / auth)
# would compare against a STALE origin ref and wrongly report "in sync", so
# check every git exit code before trusting the comparison.
git fetch origin $branch --quiet
if ($LASTEXITCODE -ne 0) {
throw "Failed to fetch origin/$branch. Cannot verify branch synchronization."
}
# Capture, THEN check the exit code, THEN .Trim(): calling .Trim() on a failed
# rev-parse's $null output would throw a confusing null-method error and skip the
# message below.
$local = git rev-parse $branch
if ($LASTEXITCODE -ne 0) {
throw "Failed to resolve local $branch (git rev-parse)."
}
$remote = git rev-parse "origin/$branch"
if ($LASTEXITCODE -ne 0) {
throw "Failed to resolve origin/$branch (git rev-parse)."
}
if ($local.Trim() -ne $remote.Trim()) {
throw "Local $branch ($local) is not in sync with origin/$branch ($remote). Pull/push first."
}
Note "in sync with origin/$branch OK"
}
function Update-PomVersion($pomPath, $newVersion) {
if (-not (Test-Path $pomPath)) {
Note "skip (no file): $pomPath"
return
}
$content = Get-Content $pomPath -Raw
$changed = $false
# 1. The FIRST <version> in the file. For the library root and the
# aggregator this is the project's own <version>; for the reactor
# children (examples/, benchmarks/) it is the inherited <parent>
# <version>, which must track the aggregator. Either way it sits
# before any dependency entries, so a single-shot replace is safe.
$projectRegex = [regex]'<version>[\w\.\-]+</version>'
$projectNew = "<version>$newVersion</version>"
$afterProject = $projectRegex.Replace($content, $projectNew, 1)
if ($content -ne $afterProject) {
$content = $afterProject
$changed = $true
Note "bumped <version>: $pomPath -> $projectNew"
}
# 2. <graphcompose.version> property (if present). Subordinate POMs
# (examples/, benchmarks/) declare this property and depend on
# "io.github.demchaav:graphcompose:${graphcompose.version}". The
# property must track the project version so the published tag
# actually resolves on a fresh CI agent without a populated
# local m2. Bug surfaced in v1.6.0 release CI: the property
# stayed at 1.6.0-beta.1 while the project version flipped to
# 1.6.0 and CI failed at "Could not find artifact ...:1.6.0-beta.1".
$propertyRegex = [regex]'<graphcompose\.version>[\w\.\-]+</graphcompose\.version>'
$propertyNew = "<graphcompose.version>$newVersion</graphcompose.version>"
$afterProperty = $propertyRegex.Replace($content, $propertyNew, 1)
if ($content -ne $afterProperty) {
$content = $afterProperty
$changed = $true
Note "bumped <graphcompose.version>: $pomPath -> $propertyNew"
}
if (-not $changed) {
Note "no change: $pomPath (version already $newVersion?)"
return
}
if ($DryRun) {
Write-Host " [DRY RUN] Bump $pomPath -> $newVersion" -ForegroundColor Yellow
} else {
[System.IO.File]::WriteAllText($pomPath, $content)
}
}
function Get-NextSnapshotVersion($version) {
# A final release X.Y.Z opens the next patch development line X.Y.(Z+1)-SNAPSHOT.
# Pre-release versions (rc / beta / alpha) stay on their own cycle, so return
# $null and let the caller skip the post-release SNAPSHOT bump for them.
if ($version -match '^(\d+)\.(\d+)\.(\d+)$') {
return "$($Matches[1]).$($Matches[2]).$([int]$Matches[3] + 1)-SNAPSHOT"
}
return $null
}
function Test-ReadmeLatestStable($version) {
# True when the README 'Latest stable' prose block names $version. Used to verify
# the block AFTER Step 1 rewrote it — see Update-ReadmeReleaseStatus.
$readme = Get-Content (Join-Path $repoRoot 'README.md') -Raw
return $readme -match "\*\*Latest stable\*\*:\s*\[v$([regex]::Escape($version))\]"
}
function Update-ReadmeReleaseStatus($readmePath, $newVersion) {
# The README 'Release status' blockquote carries two halves:
#
# > 🟢 **Latest stable**: [vX.Y.Z](…/releases/tag/vX.Y.Z) — …
# > · 🟡 **In development**: vX.Y.(Z+1) on `develop` — …
#
# It used to be a maintainer pre-cut hand-edit, gated in Step 0. That forced a
# false state: to leave `main` correct after the tag, `develop` had to advertise
# an unpublished version as "latest stable" for the whole cycle, with a release
# link that 404s. The script owns the block now — Step 1 promotes the in-development
# half to latest-stable and opens the next patch line — so `develop` stays truthful
# between releases and the release commit still carries the right text to `main`.
# Only the version tokens are rewritten; the maintainer's prose is left alone.
if (-not (Test-Path $readmePath)) {
Note "skip (no file): $readmePath"
return
}
$content = Get-Content $readmePath -Raw
$next = Get-NextSnapshotVersion $newVersion
$nextVersion = if ($next) { $next -replace '-SNAPSHOT$', '' } else { $null }
$stable = [regex]'(?<=\*\*Latest stable\*\*:\s*\[v)[\w\.\-]+(?=\])'
$stableUrl = [regex]'(?<=/releases/tag/v)[\w\.\-]+(?=\))'
$inDev = [regex]'(?<=\*\*In development\*\*:\s*v)[\w\.\-]+'
$updated = $stable.Replace($content, $newVersion, 1)
$updated = $stableUrl.Replace($updated, $newVersion, 1)
if ($nextVersion) {
$updated = $inDev.Replace($updated, $nextVersion, 1)
}
if ($updated -eq $content) {
Note "README release-status block already names v$newVersion"
return
}
if ($DryRun) {
Note "[DRY RUN] README release status -> latest stable v$newVersion, in development v$nextVersion"
return
}
[System.IO.File]::WriteAllText($readmePath, $updated)
Note "README release status -> latest stable v$newVersion, in development v$nextVersion"
}
function Assert-ReleaseMetadata($version, $isFinalRelease) {
# Fast, build-free validation of the metadata cut-release itself rewrote in Steps 1-2.
# Runs at Step 2b — after those mutations, before commit/tag; VersionConsistencyGuardTest
# re-checks it in the verify gate. The maintainer-authored README 'Latest stable' block
# is validated earlier, in Step 0 (Test-ReadmeLatestStable). The CHANGELOG-date and
# install-snippet checks are FINAL-release only: a pre-release carries no dated CHANGELOG
# entry (it is cut against the upcoming final's notes) and deliberately keeps the Central
# snippets on the last stable version. The pom-version check applies to both — the poms
# move to the (pre-)release version either way.
$problems = @()
if ($isFinalRelease) {
# 1. CHANGELOG carries a DATED entry for this version (Step 2 flips Planned -> dated).
$changelog = Get-Content (Join-Path $repoRoot 'CHANGELOG.md') -Raw
if ($changelog -notmatch "(?m)^## v$([regex]::Escape($version))\s+[—-]\s+\d{4}-\d{2}-\d{2}\b") {
$problems += "CHANGELOG.md has no dated '## v$version - YYYY-MM-DD' entry (still 'Planned', or missing)."
}
# 2. README Maven install snippet advertises this version (Step 1 rewrites it; the
# full per-module + Gradle + showcase check is VersionConsistencyGuardTest in the
# verify gate — this is the fast representative check).
$readme = Get-Content (Join-Path $repoRoot 'README.md') -Raw
$snippet = [regex]::Match($readme, '<artifactId>graph-compose</artifactId>\s*<version>v?([\w\.\-]+)</version>')
if (-not $snippet.Success) {
$problems += "README.md has no graph-compose Maven install snippet (<artifactId>graph-compose</artifactId> ... <version>...</version>)."
} elseif ($snippet.Groups[1].Value -ne $version) {
$problems += "README.md install snippet is $($snippet.Groups[1].Value), expected $version."
}
# 2b. README release-status block names this version as latest stable (Step 1
# rewrites it; this is the post-mutation verification that replaced the old
# pre-cut hand-edit gate).
if (-not (Test-ReadmeLatestStable $version)) {
$problems += "README.md release-status block does not name v$version as 'Latest stable'."
}
}
# 3. Every train pom's own <version> is this version (belt-and-suspenders ahead
# of the verify-gate guard; fonts/emoji version independently and are skipped).
foreach ($pom in @('core/pom.xml', 'pom.xml', 'render-pdf/pom.xml', 'render-docx/pom.xml',
'render-pptx/pom.xml', 'templates/pom.xml', 'testing/pom.xml', 'wrapper/pom.xml', 'bundle/pom.xml')) {
$p = Join-Path $repoRoot $pom
if (Test-Path $p) {
$m = [regex]::Match((Get-Content $p -Raw), '<version>([\w\.\-]+)</version>')
if ($m.Success -and $m.Groups[1].Value -ne $version) {
$problems += "$pom <version> is $($m.Groups[1].Value), expected $version."
}
}
}
if ($problems.Count -gt 0) {
throw ("Release metadata validation failed:`n - " + ($problems -join "`n - "))
}
if ($isFinalRelease) {
Note "release metadata: CHANGELOG dated, install snippet + train poms consistent OK"
} else {
Note "release metadata (pre-release): train poms consistent OK (snippets/Latest-stable left on last stable)"
}
}
function Update-ReadmeInstallVersion($readmePath, $newVersion) {
if (-not (Test-Path $readmePath)) {
Note "skip (no file): $readmePath"
return
}
$content = Get-Content $readmePath -Raw
$tag = "v$newVersion"
$changed = $false
# The README Maven Central install snippets pin the published version
# (X.Y.Z, no `v` prefix). They must flip in the SAME commit the release
# 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")
# 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)
if ($content -ne $afterMaven) {
$content = $afterMaven
$changed = $true
Note "bumped README Maven Central snippet -> $newVersion"
} else {
$mavenLegacyRegex = [regex]'(?<=<artifactId>GraphCompose</artifactId>\s*<version>)v?[\w\.\-]+(?=</version>)'
$afterMavenLegacy = $mavenLegacyRegex.Replace($content, $tag, 1)
if ($content -ne $afterMavenLegacy) {
$content = $afterMavenLegacy
$changed = $true
Note "bumped README legacy JitPack Maven snippet -> $tag"
}
}
$gradleCentralRegex = [regex]'(?<=io\.github\.demchaav:graph-compose:)v?[\w\.\-]+(?=")'
$afterGradle = $gradleCentralRegex.Replace($content, $newVersion, 1)
if ($content -ne $afterGradle) {
$content = $afterGradle
$changed = $true
Note "bumped README Maven Central Gradle snippet -> $newVersion"
} else {
$gradleLegacyRegex = [regex]'(?<=:GraphCompose:)v?[\w\.\-]+(?=")'
$afterGradleLegacy = $gradleLegacyRegex.Replace($content, $tag, 1)
if ($content -ne $afterGradleLegacy) {
$content = $afterGradleLegacy
$changed = $true
Note "bumped README legacy JitPack Gradle snippet -> $tag"
}
}
if (-not $changed) {
Note "no change: README install snippets (already $tag?)"
return
}
if ($DryRun) {
Write-Host " [DRY RUN] README install snippets -> $tag" -ForegroundColor Yellow
} else {
[System.IO.File]::WriteAllText($readmePath, $content)
}
}
function Update-ModuleReadmeInstallVersion($readmePath, $newVersion) {
# Per-module READMEs carry copy-paste install snippets for THEIR OWN train
# artifact (Maven + Gradle). Bump every occurrence — unlike the root README
# there is no legacy-format fallback, and each file only ever references its
# own coordinate, so a blanket replace within the file is safe. Called for
# the train modules only; fonts/emoji READMEs pin their own independent
# versions and must never be touched by an engine cut.
if (-not (Test-Path $readmePath)) {
Note "skip (no file): $readmePath"
return
}
$content = Get-Content $readmePath -Raw
$changed = $false
$mavenRegex = [regex]'(?<=<artifactId>graph-compose[\w\-]*</artifactId>\s*<version>)v?[\w\.\-]+(?=</version>)'
$afterMaven = $mavenRegex.Replace($content, $newVersion)
if ($content -ne $afterMaven) {
$content = $afterMaven
$changed = $true
Note "bumped module README Maven snippet: $readmePath -> $newVersion"
}
$gradleRegex = [regex]'(?<=io\.github\.demchaav:graph-compose[\w\-]*:)v?[\w\.\-]+(?=")'
$afterGradle = $gradleRegex.Replace($content, $newVersion)
if ($content -ne $afterGradle) {
$content = $afterGradle
$changed = $true
Note "bumped module README Gradle snippet: $readmePath -> $newVersion"
}
if (-not $changed) {
Note "no change: $readmePath (version already $newVersion?)"
return
}
if ($DryRun) {
Write-Host " [DRY RUN] Bump $readmePath install snippets -> $newVersion" -ForegroundColor Yellow
} else {
[System.IO.File]::WriteAllText($readmePath, $content)
}
}
function Update-ReleaseSmokeDefaultVersion($repoRoot, $newVersion) {
# The consumer-smoke harness hard-codes the version it tests when none is
# passed. Left behind, a post-release run that accepts the prefilled default
# re-verifies the PREVIOUS release and reports green — the one failure mode a
# release gate must not have. Each pattern is anchored to its own construct so
# a bump cannot smear across unrelated version strings in the same file.
$targets = @(
@{ Path = 'scripts/release-smoke/run.sh'; Pattern = '(?<=^GC_VERSION=")[\w\.\-]+(?=")'; Label = 'run.sh default' },
@{ Path = 'scripts/release-smoke/run.sh'; Pattern = '(?<=tests gc\.version=)[\w\.\-]+'; Label = 'run.sh usage' },
@{ Path = 'scripts/release-smoke/run.ps1'; Pattern = "(?<=\[string\]\`$Version = ')[\w\.\-]+(?=')"; Label = 'run.ps1 default' },
@{ Path = 'scripts/release-smoke/run.ps1'; Pattern = '(?<=# isolated, tests )[\w\.\-]+'; Label = 'run.ps1 usage' },
@{ Path = '.github/workflows/release-smoke.yml'; Pattern = "(?<=^\s{8}default: ')[\w\.\-]+(?=')"; Label = 'workflow input default' },
@{ Path = 'scripts/release-smoke/README.md'; Pattern = '(?<=defaults to the current published release \(`)[\w\.\-]+(?=`\))'; Label = 'README prose' }
)
foreach ($target in $targets) {
$path = Join-Path $repoRoot $target.Path
if (-not (Test-Path $path)) {
Note "skip (no file): $($target.Path)"
continue
}
$content = Get-Content $path -Raw
$updated = [regex]::Replace($content, $target.Pattern, $newVersion, 'Multiline')
if ($content -eq $updated) {
Note "no change: $($target.Path) [$($target.Label)] (already $newVersion?)"
continue
}
if ($DryRun) {
Write-Host " [DRY RUN] Bump $($target.Path) [$($target.Label)] -> $newVersion" -ForegroundColor Yellow
} else {
[System.IO.File]::WriteAllText($path, $updated)
Note "bumped release-smoke $($target.Label): $($target.Path) -> $newVersion"
}
}
}
function Update-IndexHtmlVersion($indexHtmlPath, $newVersion) {
if (-not (Test-Path $indexHtmlPath)) {
Note "skip (no file): $indexHtmlPath"
return
}
$content = Get-Content $indexHtmlPath -Raw
$tag = "v$newVersion"
$changed = $false
# The GitHub Pages showcase (web/index.html) hardcodes the version in
# several spots that do NOT inherit from the pom — they previously sat at
# v1.6.1 while the library shipped v1.6.4. VersionConsistencyGuardTest
# fails the verify gate if any lags, so flip them all in lockstep with the
# README + poms. The Maven Central format coordinates use bare semver
# ($newVersion), the hero badge keeps the v-prefix ($tag), and the
# downloadUrl points at the Central artefact page. Lookbehind/lookahead
# so only the version token is rewritten.
$replacements = @(
@{ Regex = [regex]'(?<="softwareVersion": ")v?[\w\.\-]+(?=")'; Value = $newVersion; Label = 'JSON-LD softwareVersion' },
@{ Regex = [regex]'(?<=https://central\.sonatype\.com/artifact/io\.github\.demchaav/graph-compose/)v?[\w\.\-]+(?=")'; Value = $newVersion; Label = 'Central downloadUrl' },
@{ Regex = [regex]'(?<=Java · )v?[\w\.\-]+(?= · MIT)'; Value = $tag; Label = 'hero badge' },
@{ Regex = [regex]'(?<=<artifactId>graph-compose</artifactId>\s*<version>)v?[\w\.\-]+(?=</version>)'; Value = $newVersion; Label = 'Maven Central snippet' },
@{ Regex = [regex]"(?<=io\.github\.demchaav:graph-compose:)v?[\w\.\-]+(?=')"; Value = $newVersion; Label = 'Gradle Central snippet' }
)
foreach ($r in $replacements) {
$after = $r.Regex.Replace($content, $r.Value, 1)
if ($content -ne $after) {
$content = $after
$changed = $true
Note "bumped index.html $($r.Label) -> $($r.Value)"
}
}
if (-not $changed) {
Note "no change: web/index.html version (already $tag?)"
return
}
if ($DryRun) {
Write-Host " [DRY RUN] web/index.html version -> $tag" -ForegroundColor Yellow
} else {
[System.IO.File]::WriteAllText($indexHtmlPath, $content)
}
}
function Update-ShowcaseGhBase($newRef) {
if (-not (Test-Path $showcaseMetadata)) {
throw "ShowcaseMetadata.java not found: $showcaseMetadata"
}
$content = Get-Content $showcaseMetadata -Raw
$regex = [regex]'private static final String GH_BASE = "https://github\.com/DemchaAV/GraphCompose/blob/[^"]+";'
$newLine = "private static final String GH_BASE = `"https://github.com/DemchaAV/GraphCompose/blob/$newRef`";"
$newContent = $regex.Replace($content, $newLine, 1)
if ($content -eq $newContent) {
Note "no change: ShowcaseMetadata GH_BASE (already $newRef)"
return $false
}
if ($DryRun) {
Write-Host " [DRY RUN] ShowcaseMetadata GH_BASE -> /blob/$newRef" -ForegroundColor Yellow
} else {
[System.IO.File]::WriteAllText($showcaseMetadata, $newContent)
Note "GH_BASE -> /blob/$newRef"
}
return $true
}
function Run-ShowcaseSync {
# Quote the -D argument: PowerShell's call operator drops the leading
# '-D' on the way to mvnw.cmd, so Maven sees ".mainClass=..." as a
# lifecycle phase. Wrapping the whole token in quotes preserves it
# as a single literal argument.
$execProp = '"-Dexec.mainClass=com.demcha.examples.support.ShowcaseSync"'
$generateProp = '"-Dexec.mainClass=com.demcha.examples.GenerateAllExamples"'
# The examples module depends on these bumped SNAPSHOT siblings (not on
# Central); each must be installed before exec:java can resolve them. Keep
# this list in lockstep with examples/pom.xml — a module the examples depend
# on but that is missing here fails Step 4 with "Could not find artifact
# …:<new version>", because the just-bumped version exists nowhere yet.
# ReleaseScriptInstallListGuardTest fails the build if the two drift apart.
# render-pptx must follow render-pdf: it depends on it at compile scope.
$exampleSnapshotSiblings = @('render-pdf/pom.xml', 'wrapper/pom.xml', 'render-docx/pom.xml',
'render-pptx/pom.xml', 'templates/pom.xml', 'testing/pom.xml')
if ($DryRun) {
Write-Host " [DRY RUN] $mvnw -B -ntp -DskipTests install -pl :graph-compose-core" -ForegroundColor Yellow
foreach ($modulePom in $exampleSnapshotSiblings) {
Write-Host " [DRY RUN] $mvnw -B -ntp -DskipTests install -f $modulePom" -ForegroundColor Yellow
}
Write-Host " [DRY RUN] $mvnw -B -ntp -f examples/pom.xml -DskipTests clean compile exec:java $generateProp" -ForegroundColor Yellow
Write-Host " [DRY RUN] $mvnw -B -ntp -f examples/pom.xml -DskipTests compile exec:java $execProp" -ForegroundColor Yellow
return
}
Push-Location $repoRoot
try {
# ShowcaseSync runs from the examples module, which depends on the
# engine plus the bumped SNAPSHOT siblings render-docx and
# graph-compose-testing. After Step 1 bumps the poms to the new release
# version, those artifacts are not yet in the local m2 cache — only the
# previous release is — so exec:java fails dependency resolution with
# "Could not find artifact ...:jar:<new-version>". Install the engine and
# each sibling first so the examples module can resolve them. Bug
# surfaced during v1.6.5 cut: Step 4 aborted with exit 1; we had to
# install by hand and resume manually.
Write-Host " > $mvnw -B -ntp -DskipTests install -pl :graph-compose-core" -ForegroundColor DarkGray
& $mvnw -B -ntp -DskipTests install -pl :graph-compose-core 2>&1 | ForEach-Object {
if ($_ -match 'BUILD SUCCESS|BUILD FAILURE|ERROR') {
Write-Host " $_" -ForegroundColor DarkGray
}
}
if ($LASTEXITCODE -ne 0) {
throw "Install root artifact failed (exit $LASTEXITCODE)"
}
foreach ($modulePom in $exampleSnapshotSiblings) {
Write-Host " > $mvnw -B -ntp -DskipTests install -f $modulePom" -ForegroundColor DarkGray
& $mvnw -B -ntp -DskipTests install -f $modulePom 2>&1 | ForEach-Object {
if ($_ -match 'BUILD SUCCESS|BUILD FAILURE|ERROR') {
Write-Host " $_" -ForegroundColor DarkGray
}
}
if ($LASTEXITCODE -ne 0) {
throw "Install $modulePom failed (exit $LASTEXITCODE)"
}
}
# Regenerate the catalogue before syncing it. ShowcaseSync only COPIES what is
# already in examples/target/generated-pdfs, so without this the published site
# is whatever happens to be sitting in the maintainer's target directory from
# earlier commands — and on a clean checkout there is nothing there at all.
# `clean` is the point: it drops artifacts left by tests or by examples that
# have since been renamed or deleted, so the published tree is a function of
# the tag rather than of shell history. This runs after Step 1 bumped the poms
# and Step 3 rewrote GH_BASE, so version-stamped renders carry the new version.
Write-Host " > $mvnw -B -ntp -f examples/pom.xml -DskipTests clean compile exec:java $generateProp" -ForegroundColor DarkGray
& $mvnw -B -ntp -f examples/pom.xml -DskipTests clean compile exec:java $generateProp 2>&1 | ForEach-Object {
if ($_ -match 'BUILD SUCCESS|BUILD FAILURE|ERROR') {
Write-Host " $_" -ForegroundColor DarkGray
}
}
if ($LASTEXITCODE -ne 0) {
throw "GenerateAllExamples failed (exit $LASTEXITCODE)"
}
# `compile` before exec:java is REQUIRED: Step 3 rewrote ShowcaseMetadata.GH_BASE
# to /blob/<tag>, and exec:java runs the COMPILED class. Without recompiling it here,
# ShowcaseSync would emit examples.json with the previous release's "View Code" links
# (the drift that shipped a 2.0 site still linking /blob/v1.9.0). Mirrors
# Render-ReadmeBanner, which recompiles for the same filtered-source reason.
& $mvnw -B -ntp -f examples/pom.xml -DskipTests compile exec:java $execProp 2>&1 | ForEach-Object {
if ($_ -match 'Synced|Wrote manifest|BUILD SUCCESS|BUILD FAILURE|ERROR') {
Write-Host " $_" -ForegroundColor DarkGray
}
}
if ($LASTEXITCODE -ne 0) {
throw "ShowcaseSync failed (exit $LASTEXITCODE)"
}
} finally {
Pop-Location
}
}
function Render-ReadmeBanner {
# Re-renders assets/readme/repository_showcase_render.png — the 2.0 module-first
# hero (EngineDeckV2Example.renderBannerImage) — so the hero's version pill
# carries the just-bumped ${project.version} (read from the filtered
# banner.properties). The `compile` is REQUIRED: banner.properties is filtered
# at examples-compile time, so the examples module must be recompiled AFTER the
# Step-1 version bump — otherwise the banner would carry the previous release
# version. Runs after Run-ShowcaseSync, which already installed the bumped root
# artifact into the local m2 cache so the examples module resolves it.
Write-Host " > Re-render the version-stamped README hero banner" -ForegroundColor Cyan
$banner = Join-Path $repoRoot 'assets/readme/repository_showcase_render.png'
$execProp = '"-Dexec.mainClass=com.demcha.examples.support.ReadmeBannerV2Renderer"'
$execArgs = "`"-Dexec.args=$banner`""
if ($DryRun) {
Write-Host " [DRY RUN] $mvnw -f examples/pom.xml -DskipTests compile exec:java $execProp $execArgs" -ForegroundColor Yellow
return
}
Push-Location $repoRoot
try {
& $mvnw -B -ntp -f examples/pom.xml -DskipTests compile exec:java $execProp $execArgs 2>&1 | ForEach-Object {
if ($_ -match 'Rendered README banner|BUILD SUCCESS|BUILD FAILURE|ERROR') {
Write-Host " $_" -ForegroundColor DarkGray
}
}
if ($LASTEXITCODE -ne 0) {
throw "README banner render failed (exit $LASTEXITCODE)"
}
} finally {
Pop-Location
}
Note "banner: assets/readme/repository_showcase_render.png re-rendered"
}
# ============================================================
# Mode: -PostReleaseOnly
# ============================================================
if ($PostReleaseOnly) {
Push-Location $repoRoot
try {
Step 0 "Pre-flight checks"
# Same branch / clean-tree / origin-sync gate as a real cut: -PostReleaseOnly
# also commits and pushes to $Branch, so it must not run from the wrong branch,
# over a dirty tree, or out of sync with origin.
Assert-BranchPreflight $Branch
# The released version is whatever the train poms currently carry (a cut leaves
# them at the release version). Open the next patch development line from it. The
# 2.0 module layout keeps the engine version in core/pom.xml; the legacy 1.x tree
# has no core/ and retired post-release bumping, so read defensively and skip the
# bump when it is absent (the showcase flip below still runs).
$nextSnapshot = $null
$corePom = Join-Path $repoRoot 'core/pom.xml'
if (Test-Path $corePom) {
$currentVersion = [regex]::Match((Get-Content $corePom -Raw), '<version>([\w\.\-]+)</version>').Groups[1].Value
$nextSnapshot = Get-NextSnapshotVersion $currentVersion
}
Step 1 "Switch ShowcaseMetadata GH_BASE back to /blob/$Branch"
$showcaseChanged = Update-ShowcaseGhBase $Branch
if ($showcaseChanged -or $DryRun) {
Step 2 "Regenerate web/examples.json with $Branch links"
Run-ShowcaseSync
} else {
Note "GH_BASE already points to $Branch."
}
# Post-release version bump: move the train off the release version onto the
# next patch -SNAPSHOT so develop builds are distinguishable from the release
# AND the japicmp gate (baseline = the release) actually compares (it short-
# circuits when the working version equals the baseline). The README / showcase
# INSTALL SNIPPETS are deliberately NOT touched here: they keep advertising the
# version actually on Maven Central, which VersionConsistencyGuardTest requires
# during a -SNAPSHOT cycle. cut-release rewrites the snippets to the new version
# at the NEXT release commit. Idempotent: if the poms are already on a -SNAPSHOT,
# Get-NextSnapshotVersion returns $null and the bump is skipped.
$bumpedPoms = @()
if ($nextSnapshot) {
Step 3 "Open the next development line: bump train poms to $nextSnapshot"
foreach ($pom in @('core/pom.xml', 'pom.xml', 'examples/pom.xml', 'benchmarks/pom.xml',
'qa/pom.xml', 'coverage/pom.xml', 'render-pdf/pom.xml', 'render-docx/pom.xml',
'render-pptx/pom.xml', 'templates/pom.xml', 'testing/pom.xml', 'wrapper/pom.xml', 'bundle/pom.xml')) {
if (Test-Path (Join-Path $repoRoot $pom)) {
Update-PomVersion (Join-Path $repoRoot $pom) $nextSnapshot
$bumpedPoms += $pom
}
}
} else {
Step 3 "Skipped SNAPSHOT bump (no core/pom.xml, or the current version is not a final X.Y.Z release)"
}
# Validate the bump BEFORE committing or pushing: a reactor `validate` resolves
# every module at the new SNAPSHOT (catches an inconsistent bump / unresolvable
# parent), and VersionConsistencyGuardTest confirms the poms stay in lockstep and
# the install snippets still advertise the published release (the -SNAPSHOT-cycle
# rule). Only runs when poms were actually bumped.
if ($bumpedPoms.Count -gt 0) {
Step "3b" "Validate the bump (mvnw validate + VersionConsistencyGuardTest)"
# Pass args as SPLATTED arrays, never inline: the call operator `&` re-tokenizes
# an inline unquoted arg with a dot (e.g. -Djacoco.skip=true splits into
# `-Djacoco` and `.skip=true`), which Maven then reads as a bogus phase. Array
# elements are passed literally (same pattern the japicmp gate uses).
$validateArgs = @('-B', '-ntp', 'validate')
$guardArgs = @('-B', '-ntp', 'test', '-Dtest=VersionConsistencyGuardTest', '-pl', ':graph-compose-core', '-Djacoco.skip=true')
if ($DryRun) {
Write-Host " [DRY RUN] $mvnw $($validateArgs -join ' ')" -ForegroundColor Yellow
Write-Host " [DRY RUN] $mvnw $($guardArgs -join ' ')" -ForegroundColor Yellow
} else {
& $mvnw @validateArgs 2>&1 | ForEach-Object {
if ($_ -match 'BUILD SUCCESS|BUILD FAILURE|ERROR') { Write-Host " $_" -ForegroundColor DarkGray }
}
if ($LASTEXITCODE -ne 0) { throw "mvnw validate failed after the SNAPSHOT bump." }
& $mvnw @guardArgs 2>&1 | ForEach-Object {
if ($_ -match 'Tests run:|BUILD SUCCESS|BUILD FAILURE|ERROR') { Write-Host " $_" -ForegroundColor DarkGray }
}
if ($LASTEXITCODE -ne 0) { throw "VersionConsistencyGuardTest failed after the SNAPSHOT bump." }
Note "bump validated: reactor resolves + version guard green OK"
}
}
# Commit whatever changed: the bumped poms and/or the restored showcase files.
$filesToCommit = @()
if ($showcaseChanged -or $DryRun) { $filesToCommit += @($showcaseMetadata, 'web/examples.json') }
$filesToCommit += $bumpedPoms
if ($filesToCommit.Count -gt 0) {
$parts = @()
if ($bumpedPoms.Count -gt 0) { $parts += "open $nextSnapshot" }
if ($showcaseChanged -or $DryRun) { $parts += "restore /blob/$Branch showcase links" }
$msg = "chore(release): " + ($parts -join ' + ')
Step 4 "Commit"
if ($DryRun) {
Write-Host " [DRY RUN] git add $($filesToCommit -join ' ')" -ForegroundColor Yellow
Write-Host " [DRY RUN] git commit -m `"$msg`"" -ForegroundColor Yellow
} else {
git add @filesToCommit
git commit -m $msg
Note "commit: $msg"
}
Step 5 "Push $Branch"
if ($DryRun) {
Write-Host " [DRY RUN] git push origin $Branch" -ForegroundColor Yellow
} else {
git push origin $Branch
}
} else {
Note "Nothing to do (showcase already on /blob/$Branch and version already a SNAPSHOT)."
}
} finally {
Pop-Location
}
Write-Host ""
Write-Host "Done. $Branch is on the next development line with linkable View Code buttons." -ForegroundColor Green
return
}
# ============================================================
# Mode: full release cut
# ============================================================
Push-Location $repoRoot
try {
$tag = "v$Version"
# Only X.Y.Z (final) and X.Y.Z-{rc|alpha|beta}.N (pre-release) are supported. Reject
# anything else up front, so an unrecognised suffix (e.g. 2.1.0-preview.1) cannot
# silently fall into the pre-release path.
if ($Version -notmatch '^\d+\.\d+\.\d+(-(rc|alpha|beta)\.\d+)?$') {
throw "Unsupported version '$Version'. Use X.Y.Z (final) or X.Y.Z-{rc|alpha|beta}.N (pre-release)."
}
# A FINAL release is X.Y.Z with no suffix. Pre-releases (X.Y.Z-rc.N / -alpha / -beta)
# ship only to the GitHub Release pre-release surface — publish.yml skips them for
# Maven Central — so a pre-release cut must NOT touch the README 'Latest stable' block
# or the Central install snippets: they keep advertising the last stable, on-Central
# version. The poms still move to the pre-release version (the tag builds at it).
$isFinalRelease = $Version -match '^\d+\.\d+\.\d+$'
if (-not $isFinalRelease) {
Note "pre-release ($Version): README 'Latest stable' + Central install snippets stay on the last stable version"
}
Step 0 "Pre-flight checks"
# Branch / clean-tree / origin-sync gate (shared with -PostReleaseOnly).
Assert-BranchPreflight $Branch
# Tag doesn't already exist — locally or on origin?
$existingTag = git tag -l $tag
if ($existingTag) {
throw "Tag $tag already exists locally. Bump version or delete the tag."
}
# Query origin with ls-remote, NOT `git fetch refs/tags/...`: fetch exits 128 for a
# legitimately-absent tag (the normal pre-cut case), so its exit code cannot tell
# "tag free" from "network broke". ls-remote returns empty for an absent tag and
# fails only on a real network/auth error, so a broken connection can no longer be
# mistaken for "tag is free".
$remoteTag = git ls-remote --tags origin "refs/tags/$tag"
if ($LASTEXITCODE -ne 0) {
throw "Failed to query origin for tag $tag (git ls-remote). Cannot verify the tag is free."
}
if ($remoteTag) {
throw "Tag $tag exists on origin. Bump version or delete the remote tag."
}
Note ("tag {0}: available OK" -f $tag)
# The README release-status block is rewritten by Step 1 (Update-ReadmeReleaseStatus)
# and verified after the mutation by Assert-ReleaseMetadata, so there is nothing to
# demand of it here. What Step 0 still checks is that the block EXISTS in the shape
# the rewrite expects — a renamed or reflowed blockquote would otherwise be skipped
# in silence and ship `main` advertising the previous release, the v1.6.9 failure.
if ($isFinalRelease) {
$readmeRaw = Get-Content (Join-Path $repoRoot 'README.md') -Raw
if ($readmeRaw -notmatch '\*\*Latest stable\*\*:\s*\[v[\w\.\-]+\]') {
throw "README has no '**Latest stable**: [vX.Y.Z](...)' release-status line for the cut to rewrite."
}
Note "README release-status block present OK"
}
if ($isFinalRelease) {
$step1Title = "Bump versions to $Version (poms + README install snippets)"
} else {
$step1Title = "Bump versions to $Version (poms only; snippets stay on last stable)"
}
Step 1 $step1Title
# All ENGINE-LINE version sites must move together or
# VersionConsistencyGuardTest fails the verify gate below: the standalone
# the engine core/pom.xml (the published artifact), the root reactor
# aggregator (pom.xml), the examples/benchmarks children whose inherited
# <parent> version tracks the aggregator, and the standalone bundle
# aggregate (graph-compose-bundle).
# NOTE: graph-compose-fonts is deliberately absent — it carries an
# INDEPENDENT version line (currently 1.0.0) and ships on its own fonts-v*
# tag, so an engine release must never rewrite it. Its first <version> is
# 1.0.0, not the engine version, so even a stray reactor bump would skip it.
Update-PomVersion (Join-Path $repoRoot 'core/pom.xml') $Version
Update-PomVersion (Join-Path $repoRoot 'pom.xml') $Version
Update-PomVersion (Join-Path $repoRoot 'examples/pom.xml') $Version
Update-PomVersion (Join-Path $repoRoot 'benchmarks/pom.xml') $Version
# graph-compose-qa / graph-compose-coverage are aggregator children too. They
# inherit their version, but the inherited <parent><version> is a literal that
# must track the root: skipping them leaves graph-compose-build unresolvable in a
# clean reactor build (no local m2 cache), which breaks the release.yml /
# publish.yml verify on the tag even though a warm local build passes.
Update-PomVersion (Join-Path $repoRoot 'qa/pom.xml') $Version
Update-PomVersion (Join-Path $repoRoot 'coverage/pom.xml') $Version
# render-pdf / render-docx / render-pptx track the engine line (lockstep): each
# <version> bumps here and its graph-compose-core dep is ${project.version}
# (follows automatically).
Update-PomVersion (Join-Path $repoRoot 'render-pdf/pom.xml') $Version
Update-PomVersion (Join-Path $repoRoot 'render-docx/pom.xml') $Version
Update-PomVersion (Join-Path $repoRoot 'render-pptx/pom.xml') $Version
# graph-compose-templates tracks the engine line (lockstep): its <version> bumps
# here and its graph-compose-core dep is ${project.version} (follows automatically).
Update-PomVersion (Join-Path $repoRoot 'templates/pom.xml') $Version
# graph-compose-testing tracks the engine line (lockstep): its <version>
# bumps here and its graph-compose dep is ${project.version} (follows
# automatically).
Update-PomVersion (Join-Path $repoRoot 'testing/pom.xml') $Version
# graph-compose (the graph-compose compat wrapper (wrapper/)) tracks the engine line lockstep;
# its graph-compose-core dep is ${project.version} (follows automatically).
Update-PomVersion (Join-Path $repoRoot 'wrapper/pom.xml') $Version
# Bundle tracks the engine line: its project <version> bumps here; its
# graph-compose dep is ${project.version} (follows automatically) and its
# graph-compose-fonts dep is ${graphcompose.fonts.version} (stays pinned —
# the bump regex does not touch the $-prefixed property reference).
Update-PomVersion (Join-Path $repoRoot 'bundle/pom.xml') $Version
# Maven Central install snippets — FINAL releases only. A pre-release never lands on
# Central (publish.yml skips hyphenated tags), so rewriting these to the pre-release
# version would advertise a coordinate that 404s for any user who copies it; leave
# them on the last stable, published version. VersionConsistencyGuardTest accepts the
# latest published release for a non-final (SNAPSHOT or pre-release) working version.
if ($isFinalRelease) {
Update-ReadmeInstallVersion (Join-Path $repoRoot 'README.md') $Version
Update-ReadmeReleaseStatus (Join-Path $repoRoot 'README.md') $Version
# Per-module README install snippets (train modules only — fonts/emoji pin
# their own independent versions).
foreach ($moduleReadme in @('core/README.md', 'render-pdf/README.md', 'render-docx/README.md',
'render-pptx/README.md', 'templates/README.md', 'testing/README.md',
'wrapper/README.md', 'bundle/README.md')) {
Update-ModuleReadmeInstallVersion (Join-Path $repoRoot $moduleReadme) $Version
}
Update-IndexHtmlVersion (Join-Path $repoRoot 'web/index.html') $Version
# The smoke harness's default version must follow the release, or the
# post-release run silently re-verifies the previous one.
Update-ReleaseSmokeDefaultVersion $repoRoot $Version
} else {
Note "pre-release: skipped README / module-README / web install-snippet bumps (stay on last stable)"
}
# The Next.js site/ and the docs->site/public mirror were retired when the static
# showcase moved to web/ (deployed directly via .github/workflows/deploy-web.yml).
# Only web/ is version-bumped now.
Step 2 "Update CHANGELOG date for v$Version"
$changelog = Join-Path $repoRoot 'CHANGELOG.md'
if (Test-Path $changelog) {
$today = (Get-Date -Format 'yyyy-MM-dd')
$content = Get-Content $changelog -Raw
$regex = [regex]"## v$([regex]::Escape($Version)) — Planned"
$newHeader = "## v$Version — $today"
$newContent = $regex.Replace($content, $newHeader, 1)
if ($content -ne $newContent) {
if ($DryRun) {
Write-Host " [DRY RUN] CHANGELOG.md: 'v$Version — Planned' -> 'v$Version — $today'" -ForegroundColor Yellow
} else {
[System.IO.File]::WriteAllText($changelog, $newContent)
Note "CHANGELOG: v$Version — $today"
}
} else {
Note "CHANGELOG: no '## v$Version — Planned' header found. Skipping (already dated?)."
}
}
Step "2b" "Validate release metadata (fast pre-tag gate)"
# Runs in milliseconds, before the showcase regen / verify / commit / tag, so a
# misconfigured release fails immediately. Validates the metadata cut-release just
# mutated in Steps 1-2 (CHANGELOG date, install snippet, train pom versions). In
# -DryRun those mutations were only previewed (files still at the pre-cut state), so
# the assertion is deferred to the real cut. (The README 'Latest stable' block was
# already validated in Step 0, before any mutation.)
if ($DryRun) {
Note "[DRY RUN] train pom versions (+ CHANGELOG date & install snippet for a final release) are validated on the real cut"
} else {
Assert-ReleaseMetadata $Version $isFinalRelease
}
if (-not $SkipShowcase) {
Step 3 "Switch ShowcaseMetadata GH_BASE to /blob/$tag"
Update-ShowcaseGhBase $tag | Out-Null
Step 4 "Regenerate web/examples.json with $tag links"
Run-ShowcaseSync
Render-ReadmeBanner
} else {
Step 3 "Skipped showcase GH_BASE flip + regen + banner (-SkipShowcase)"
}
if (-not $SkipVerify) {
Step 5 "Run mvnw clean verify (sanity check)"
# The whole train ships on the tag, so verify the whole reactor. The 2.0
# core/ layout builds every module from the root; the older 1.x layout
# scopes to the engine at the root (-pl .). Detect by core/pom.xml.
$verifyArgs = if (Test-Path (Join-Path $repoRoot 'core/pom.xml')) {
@('-B','-ntp','clean','verify')
} else {
@('-B','-ntp','clean','verify','-pl','.')
}
if ($DryRun) {
Write-Host " [DRY RUN] $mvnw $($verifyArgs -join ' ')" -ForegroundColor Yellow
} else {
& $mvnw @verifyArgs 2>&1 | ForEach-Object {
if ($_ -match 'Tests run:|BUILD SUCCESS|BUILD FAILURE|ERROR') {
Write-Host " $_" -ForegroundColor DarkGray
}
}
if ($LASTEXITCODE -ne 0) {
throw "mvnw verify failed."
}
Note "mvnw clean verify: green"