From 058dc406a64dd420e92c1b691da86f570dde891c Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 21:43:42 +0200 Subject: [PATCH 01/22] Enable parallel forks for test and itest tasks Set maxParallelForks to half the available cores for utest and itest. Slowtest and integration test variants intentionally keep the global default of 1 since they share data files and call Main.main() per test. --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index de6aeb033e5..e9fc26b845a 100644 --- a/build.gradle +++ b/build.gradle @@ -733,9 +733,12 @@ tasks.register("testCoverage", JacocoReport) { executionData.from = fileTree(dir: layout.buildDirectory.dir("jacoco")).include("*.exec") } +def parallelTestForks = Math.max(1, Runtime.runtime.availableProcessors().intdiv(2) as int) + tasks.named("test", Test) { exclude 'test/pcgen/testsupport/**' useJUnitPlatform() + maxParallelForks = parallelTestForks } tasks.register("itest", Test) { @@ -744,6 +747,7 @@ tasks.register("itest", Test) { testClassesDirs = sourceSets.itest.output.classesDirs classpath = sourceSets.itest.runtimeClasspath systemProperties['jar.path'] = jar.getArchiveFile().get().getAsFile() + maxParallelForks = parallelTestForks } tasks.register("slowtest", Test) { From c286e17b69aee6964aa5da956988eb21b1d426fe Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 21:46:15 +0200 Subject: [PATCH 02/22] Refactor build.gradle: update converterJar task and improve parallel test forks calculation Signed-off-by: Vest --- build.gradle | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index e9fc26b845a..bba9f8473b7 100644 --- a/build.gradle +++ b/build.gradle @@ -449,7 +449,9 @@ if (Os.isFamily(Os.FAMILY_MAC)) { } } -// Create the Data Convertor JAR +// Create the Data Convertor JAR. +// This is a thin jar containing only a manifest that points at pcgen.jar +// (and the rest of the runtime classpath) via Class-Path. tasks.register("converterJar", Jar) { dependsOn jar manifest { @@ -460,12 +462,6 @@ tasks.register("converterJar", Jar) { } archiveClassifier.set('batch-convert') - from { - configurations.runtimeClasspath.collect { it.directory ? it : zipTree(it) } - } - - from sourceSets.main.output - include 'none' } tasks.named("assemble") { @@ -733,7 +729,8 @@ tasks.register("testCoverage", JacocoReport) { executionData.from = fileTree(dir: layout.buildDirectory.dir("jacoco")).include("*.exec") } -def parallelTestForks = Math.max(1, Runtime.runtime.availableProcessors().intdiv(2) as int) +// https://docs.gradle.org/current/userguide/performance.html#a_run_tests_in_parallel +def parallelTestForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 tasks.named("test", Test) { exclude 'test/pcgen/testsupport/**' From f333bda2df31f9c5aefe5cbc396a1d2fb3f4c2e6 Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 21:47:36 +0200 Subject: [PATCH 03/22] Remove commented-out repository declarations Drop the dead Ivy repo for PCGen Base/Formula sourceforge jars (we now get those from project dependencies) and the freehep maven repo, which was already disabled and never re-enabled. --- build.gradle | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/build.gradle b/build.gradle index bba9f8473b7..164b1cfdffb 100644 --- a/build.gradle +++ b/build.gradle @@ -96,31 +96,12 @@ repositories { // Maven Central as a default mavenCentral() - // Use Ant Ivy Plugin to reference our own hosted repos - // for PCGen Base and PCGen Formula JARs - //ivy { - // TODO Enable HTTPS (e.g., add letsencrypt) for that location - //url "http://pcgen.sourceforge.net/mvnrepo" - //allowInsecureProtocol = true - //patternLayout { - //artifact "[organisation]/jars/[artifact]-[revision].[ext]" - //} - //} ivy { // TODO Enable HTTPS (e.g., add letsencrypt) for that location name = 'fileRepo' url = 'http://pc-gen.org/librepo/' allowInsecureProtocol = true } - // Use Maven plugin to reference freehep (https://java.freehep.org/) artifact repository - // TODO Which libs do we pull from here? - /* - maven { - name = 'free' - url = 'https://java.freehep.org/maven2' - } - - */ maven { name = 'local' url = 'installers' From 7dbfb9b6a8a671a950645f2da6d98931b6b9f94d Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 21:48:00 +0200 Subject: [PATCH 04/22] Remove unused localOnly property and stale version comment The localOnly property is not referenced from any Gradle script, source, or properties file. The commented-out 6.09.06-SNAPSHOT version is obsolete history. --- gradle.properties | 2 -- 1 file changed, 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 517decc5f7d..31b1c1dc2c3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,4 @@ -#version=6.09.06-SNAPSHOT version=6.09.08.RC1 -localOnly=false org.gradle.jvmargs=-Xmx4096m org.gradle.configuration-cache=true From fb08352cf94c04d191dc4509b7706639089dc396 Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 21:52:53 +0200 Subject: [PATCH 05/22] Remove calls to undefined commitFile() in release tasks The updateVersionRelease and updateVersionToNext tasks called a commitFile() helper that was never defined anywhere in the build, so they would have failed at runtime. Drop the broken calls and leave a comment that the version change should be committed manually. --- code/gradle/release.gradle | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/code/gradle/release.gradle b/code/gradle/release.gradle index 77b7f76aca1..0a2ea5e4117 100644 --- a/code/gradle/release.gradle +++ b/code/gradle/release.gradle @@ -364,11 +364,10 @@ tasks.register("pcgenRelease") { // Installer is placed in outputDir } -// Update the PCGen version to indicate a release, then commit the new version +// Update the PCGen version to indicate a release. Commit the change manually. tasks.register("updateVersionRelease") { doLast { unSnapshotVersion() - commitFile(file("gradle.properties"), "Update version for release ${project.version}") } } @@ -377,10 +376,9 @@ tasks.register("pcgenReleaseOfficial") { description = "Release a new official version of PCGen." } -// Update the PCGen version for development of the next release, then commit the new version +// Update the PCGen version for development of the next release. Commit the change manually. tasks.register("updateVersionToNext") { doLast { updateVersion() - commitFile(file("gradle.properties"), "Update version for dev work ${project.version}") } } From ebd844dfc1c420cbeb1bb06753ec3d10bc29738a Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 21:53:35 +0200 Subject: [PATCH 06/22] Remove dead testZip task and unused jreImage copySpec testZip was registered but never depended on by any other task or workflow, and jreImage was defined but never applied via copySpec.with. Both are leftovers from the pre-jpackage distribution layout. --- code/gradle/distribution.gradle | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/code/gradle/distribution.gradle b/code/gradle/distribution.gradle index 4327effce6f..022cfca2351 100644 --- a/code/gradle/distribution.gradle +++ b/code/gradle/distribution.gradle @@ -75,12 +75,6 @@ ext { include 'characters/*.pcg' } } - - jreImage = copySpec { - from('.') { - include 'jdks/jdk_windows_x64/**' - } - } } application { @@ -119,19 +113,6 @@ tasks.named("installDist") { } } -tasks.register("testZip", Zip) { - dependsOn converterJar, copyToLibs, copyToOutput - archiveClassifier.set('test') - into(zipRootFolder) { - with dataDistsImage - with docsDistsImage - with programDistsImage - with libsDistsImage - with autobuildSpec - with charactersImage - } -} - tasks.register("dataZip", Zip) { dependsOn copyMasterSheets archiveClassifier.set('data') From be48763530f681939c255f108d2ae444ba6d9e61 Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 21:55:00 +0200 Subject: [PATCH 07/22] Remove dead NSIS installer scaffolding from release.gradle The NSIS-based Windows installer was retired in favor of jpackage, but the supporting copySpecs (baseLibs, lib32, lib64, pdfLibs, basePlugins, gmgenPlugins, nonPdfOutput, pdfOutput, baseData, optionalData), the nsisBaseFolder/nsisOptionFolder paths, the installerVerNum derivation, and the genDataList task that wrote installers/win-installer/data.nsh were left behind. None of them are referenced anywhere outside release.gradle. Drop them along with the now-unused FixCrLfFilter and DefaultCopySpec imports. --- code/gradle/release.gradle | 233 ------------------------------------- 1 file changed, 233 deletions(-) diff --git a/code/gradle/release.gradle b/code/gradle/release.gradle index 0a2ea5e4117..8b1079210d6 100644 --- a/code/gradle/release.gradle +++ b/code/gradle/release.gradle @@ -22,245 +22,13 @@ apply from: "code/gradle/releaseUtils.groovy" apply plugin: 'java' -import org.apache.tools.ant.filters.FixCrLfFilter import org.apache.tools.ant.taskdefs.condition.Os -import org.gradle.api.internal.file.copy.DefaultCopySpec ext { // Work out the path to the release notes for our current version. plainVerNum = version.replaceAll('-SNAPSHOT', '') shortVerNum = plainVerNum.replaceAll(/\./, '') releaseNotes = "${projectDir}/installers/release-notes/pcgen-release-notes-${shortVerNum}.html" - - nsisBaseFolder = layout.buildDirectory.dir("nsis/PCGen_${shortVerNum}_base") - nsisOptionFolder = layout.buildDirectory.dir("nsis/PCGen_${shortVerNum}_opt") - - // specifications for what to copy to the NSIS base and optional folders - baseLibs = copySpec { - from(layout.buildDirectory.dir("libs")) { - exclude 'pcgen-*.jar' - exclude 'fop*.jar' - exclude 'batik*.jar' - } - into ('/libs') - } as DefaultCopySpec - - lib32 = copySpec { - from(layout.buildDirectory.dir("image/pcgen-windows-x32/lib")) { - exclude 'fop*.jar' - exclude 'batik*.jar' - } - into('/lib32') - } as DefaultCopySpec - - lib64 = copySpec { - from(layout.buildDirectory.dir("image/pcgen-windows-x64/lib")) { - exclude 'fop*.jar' - exclude 'batik*.jar' - exclude 'javafx*.jar' - } - into('/lib64') - } as DefaultCopySpec - - pdfLibs = copySpec { - from(layout.buildDirectory.dir("libs")) { - include 'fop*.jar' - include 'batik*.jar' - } - into ('/plugin/pdf/libs') - } as DefaultCopySpec - - basePlugins = copySpec { - from(layout.projectDirectory.dir("plugins")) { - include '*plugins/**' - include 'Random Names/**' - include 'DoomsdayBook.jar' - } - into ('/plugins') - } as DefaultCopySpec - - gmgenPlugins = copySpec { - from(layout.projectDirectory.dir("plugins")) { - exclude '*plugins/**' - exclude 'Random Names/**' - exclude 'DoomsdayBook.jar' - } - into ('/plugin/gmgen/plugins') - } as DefaultCopySpec - - nonPdfOutput = copySpec { - from(layout.projectDirectory.dir("outputsheets")) { - exclude '**/pdf/**' - } - into ('/outputsheets') - } as DefaultCopySpec - - pdfOutput = copySpec { - from(layout.projectDirectory.dir("outputsheets")) { - include '**/pdf/**' - } - into ('/plugin/pdf/outputsheets') - } as DefaultCopySpec - - baseData = copySpec { - from(layout.projectDirectory.dir("data")) { - include 'customsources/**' - include 'homebrew/**' - } - into ('/data') - } as DefaultCopySpec - - optionalData = copySpec { - from(layout.projectDirectory.dir("data")) { - exclude 'customsources/**' - exclude 'homebrew/**' - } - into ('/data') - } as DefaultCopySpec - - List result = plainVerNum.replaceAll('-DEV', '').tokenize('.') - installerVerNum = result[0].toInteger() + "." + result[1].toInteger() + "." + result[2].take(2).toInteger() + ".0" -} - -// Generate the publisher/data list for the Windows installer script -tasks.register("genDataList") { - - notCompatibleWithConfigurationCache("File manipulation involved in genDataList") - - inputs.dir layout.projectDirectory.dir("data") - - def dataNsh = layout.projectDirectory.file("installers/win-installer/includes/data.nsh") - outputs.file dataNsh - - // TODO: find a way to move it to ext section. - // The list of publishers - add an entry here to correct a reported missing publisher - def publisherMap = ["12_to_midnight" : "12 to Midnight", - "4_winds_fantasy_gaming" : "4 Winds Fantasy Gaming", - "alderac_entertainment_group": "Alderac Entertainment Group", - "alderac_ent_group" : "Alderac Entertainment Group", - "alea_publishing_group" : "Alea Publishing Group", - "alluria_publishing" : "Alluria Publishing", - "ascension_games" : "Ascension Games", - "atlas_games" : "Atlas Games", - "auran_d20" : "Auran d20", - "avalanche_press" : "Avalanche Press", - "badaxe_games" : "Bad Axe Games", - "bards_and_sages" : "Bards and Sages", - "bastion_press" : "Bastion Press", - "battlefield_press" : "Battlefield Press", - "behemoth3" : "Behemoth3", - "big_finger_games" : "Big Finger Games", - "bloodstone_press" : "Bloodstone Press", - "blue_devil_games" : "Blue Devil Games", - "broken_ruler_games" : "Broken Ruler Games", - "crafty_games" : "Crafty Games", - "creativemountaingames" : "Creative Mountain Games", - "distant_horizons_games" : "Distant Horizons Games", - "doghouse_rules" : "Dog House Rules", - "dragonwing_games" : "DragonWing Games", - "dreamscarred_press" : "Dreamscarred Press", - "en_publishing" : "EN Publishing", - "everyman_gaming" : "Everyman Gaming", - "fantasy_community_council" : "Fantasy Community Council", - "fantasy_flight_games" : "Fantasy Flight Games", - "gallantry_productions" : "Gallantry Productions", - "goodman_games" : "Goodman Games", - "green_ronin" : "Green Ronin", - "kobold_press" : "Kobold Press", - "legendary_games" : "Legendary Games", - "lions_den_press" : "Lions Den Press", - "lone_wanderer_entertainment": "Lone Wanderer Entertainment", - "malhavoc_press" : "Malhavoc Press", - "minotaur_games" : "Minotaur Games", - "mongoose" : "Mongoose", - "mongoose_publishing" : "Mongoose Publishing", - "msrd" : "MSRD", - "mythic_dream_studios" : "Mythic Dreams Studios", - "necromancer_games" : "Necromancer Games", - "nitehawk_interactive" : "Nitehawk Interactive Games", - "pandahead" : "Pandahead", - "paradigm_concepts" : "Paradigm Concepts Inc", - "paizo" : "Paizo Publishing", - "parents_basement_games" : "Parents Basement Games", - "pcgen" : "PCGen OGL", - "pfsrd_publishing" : "Open Game Network (d20pfsrd,", - "pinnacle_entertainment" : "Pinnacle Entertainment", - "reality_deviant" : "Reality Deviant Publications", - "rite" : "Rite Publishing", - "rite_publishing" : "Rite Publishing", - "rpg_objects" : "RPG Objects", - "sagaborn" : "Sagaborn", - "secular_games" : "Secular Games", - "silven_publishing" : "Silven Publishing", - "silverthorne_games" : "Silverthorne Games", - "skirmisher_publishing" : "Skirmisher Publishing LLC", - "sovereign_press" : "Sovereign Press", - "srd" : "SRD", - "srd35" : "SRD35", - "st_cooley_publishing" : "S T Cooley Publishing", - "storm_bunny_studios" : "Storm Bunny Studios", - "super_genius_games" : "Super Genius Games", - "sword_and_sorcery_studios" : "Sword and Sorcery Studios", - "swords_edge_publishing" : "Swords Edge Publishing", - "the_game_mechanics" : "The Game Mechanics Inc", - "vigilance_press" : "Vigilance Press", - "wizards_of_the_coast" : "Wizards of the Coast", - "lpj_design" : "Louis Porter Jr. Design", - "tripod_machine" : "Tripod Machine", - "total_party_kill_games" : "Total Party Kill Games", - "starfinder" : "Starfinder"] - - doLast { - project.delete(dataNsh) - def out = new StringBuilder() - - // List of folder names to ignore. - def ignoreList = ["homebrew", "zen_test", "customsources", "_universal", "_images", "publisher_logos"] - - // List of sections - def sectionsIn = [msrd: "3 6", pcgen: "3 4 5", srd: "3 4", srd35: "3 5", necromancergames: "3 5", paizo: "3"] - - layout.projectDirectory.dir("data").asFile.eachDir { - if (ignoreList.contains(it.name)) - { - return - } - out.append("SubSection \"$it.name\"\n") - def dir = it.name - def subFolders = 0 - it.eachDir { - if (ignoreList.contains(it.name)) - { - return - } - def pubname = publisherMap.getOrDefault(it.name, it.name) - def sectionIn = sectionsIn.getOrDefault(it.name, "") - - out.append("""\tSection "$pubname" -\t\tSectionIn 1 2 ${sectionIn} -\t\tSetOutPath "\$INSTDIR\\\${APPDIR}\\data\\${dir}\\${it.name}" -\t\tFile /r "\${SrcDir}\\PCGen_\${SIMPVER}_opt\\data\\${dir}\\${it.name}\\*.*" -\tSectionEnd - -""") - subFolders++ - } - if (subFolders == 0){ - def pubname = publisherMap.getOrDefault(it.name, it.name) - out.append("""\tSection "$pubname" -\t\tSectionIn 1 2 -\t\tSetOutPath "\$INSTDIR\\\${APPDIR}\\data\\${dir}" -\t\tFile /r "\${SrcDir}\\PCGen_\${SIMPVER}_opt\\data\\${dir}\\*.*" -\tSectionEnd - -""") - } - out.append("SubSectionEnd\n\n") - } - - println("Writing data to: ${outputs.files.singleFile}") - outputs.files.singleFile.text = out - } } tasks.register("sourcesJar", Jar) { @@ -351,7 +119,6 @@ tasks.register("checksum") { } } -// genDataList removed as it causes release errors, 'genDataList' tasks.register("prepareRelease") { dependsOn build description = """Prepare PCGen for release. This will update and commit the version, \ From 16c091f0419997d8d4c5b100368e89197089ff0e Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 22:14:08 +0200 Subject: [PATCH 08/22] Remove dead programDistsImage launch4j copySpec programDistsImage copied build/launch4j/pcgen.exe, but the launch4j plugin was removed in favor of jpackage and that directory is never produced. The copySpec contributed zero files to programZip and installDist, so drop both the copySpec and its applications. --- code/gradle/distribution.gradle | 8 -------- 1 file changed, 8 deletions(-) diff --git a/code/gradle/distribution.gradle b/code/gradle/distribution.gradle index 022cfca2351..204706e0f4a 100644 --- a/code/gradle/distribution.gradle +++ b/code/gradle/distribution.gradle @@ -40,12 +40,6 @@ ext { } } - programDistsImage = copySpec { - from(layout.buildDirectory.dir("launch4j")) { - include 'pcgen.exe' - } - } - programScriptImage = copySpec { from('.') { include 'pcgen_low_mem.bat' @@ -87,7 +81,6 @@ application { applicationDistribution.with(autobuildSpec) applicationDistribution.with(charactersImage) applicationDistribution.with(programScriptImage) - applicationDistribution.with(programDistsImage) } tasks.named("run") { @@ -134,7 +127,6 @@ tasks.register("programZip", Zip) { dependsOn converterJar archiveClassifier.set('program') into(zipRootFolder) { - with programDistsImage with programScriptImage with autobuildSpec } From 11f3455c97a2c9a97f9d24e833e722ae74dd414f Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 22:16:39 +0200 Subject: [PATCH 09/22] Scope TestFX/Monocle JVM args to the test task The TestFX/Monocle-specific flags (testfx.* sysprops, prism.order=sw, the four --add-exports/--add-opens for monocle, prism.verbose) were applied to every Test task via configureEach, even though only the unit test source set (code/src/utest) contains TestFX-based tests. itest and slowtest don't use TestFX and don't need them. Move the TestFX-specific args onto the test task itself. Keep the JavaFX module loading args (--module-path, --add-modules, native access) on configureEach because the production code still requires the JavaFX modules to be on the path for class loading. Also drop -Dprism.verbose=true so test output stays quieter. --- build.gradle | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/build.gradle b/build.gradle index 164b1cfdffb..5b10f83122d 100644 --- a/build.gradle +++ b/build.gradle @@ -717,6 +717,18 @@ tasks.named("test", Test) { exclude 'test/pcgen/testsupport/**' useJUnitPlatform() maxParallelForks = parallelTestForks + // TestFX/Monocle setup — only the unit test source set has TestFX-based tests. + jvmArgs += [ + '-Dtestfx.robot=glass', + '-Dtestfx.headless=true', + '-Dprism.order=sw', + // Workaround for TestFX modularity issue. https://github.com/TestFX/TestFX/issues/638 + '--add-exports', 'javafx.graphics/com.sun.javafx.application=ALL-UNNAMED', + // Required for Monocle. + '--add-exports', 'javafx.graphics/com.sun.javafx.util=ALL-UNNAMED', + '--add-exports', 'javafx.base/com.sun.javafx.logging=ALL-UNNAMED', + '--add-opens', 'javafx.graphics/com.sun.glass.ui=ALL-UNNAMED', + ] } tasks.register("itest", Test) { @@ -808,22 +820,12 @@ allprojects { } jvmArgs += [ '-Djava.awt.headless=true', - '-Dtestfx.robot=glass', - '-Dtestfx.headless=true', - '-Dprism.order=sw', - '-Dprism.verbose=true', '-Djavafx.macosx.embedded=true', '-Djava.security.manager=disallow', "--module-path", layout.projectDirectory.dir("mods/lib"), "--add-modules", "javafx.controls,javafx.web,javafx.swing,javafx.fxml,javafx.graphics", - // Needed to fix issue with testfx test crashing due to issue with modularity of whole program. Slightly related issue. https://github.com/TestFX/TestFX/issues/638 - '--add-exports', 'javafx.graphics/com.sun.javafx.application=ALL-UNNAMED', - // Next two needed to support monocle testing - '--add-exports', 'javafx.graphics/com.sun.javafx.util=ALL-UNNAMED', - '--add-exports', 'javafx.base/com.sun.javafx.logging=ALL-UNNAMED', - '--add-opens', 'javafx.graphics/com.sun.glass.ui=ALL-UNNAMED', '--enable-native-access=javafx.graphics', ] } From ff21be6cb47bdd237a43aae3c983fb02293e0236 Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 22:18:35 +0200 Subject: [PATCH 10/22] Promote testcommon to a dedicated source set code/src/testcommon was listed as a srcDir in three source sets (test, itest, slowtest), so its 22 files were compiled three times. Extract it into its own testcommon source set; have test/itest/slowtest consume its compiled output via classpath instead. Wire testcommonImplementation/RuntimeOnly/CompileOnly to extend the matching test configurations so testcommon picks up the same JUnit and test-helper dependencies. --- build.gradle | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 5b10f83122d..9a4b9467346 100644 --- a/build.gradle +++ b/build.gradle @@ -127,13 +127,21 @@ sourceSets { srcDirs = ['code/src/resources'] } } - // Running regular tests - test { + // Shared test utilities — compiled once, consumed by test/itest/slowtest. + testcommon { java { - srcDirs = ['code/src/utest', 'code/src/testcommon'] + srcDirs = ['code/src/testcommon'] } compileClasspath += sourceSets.main.output runtimeClasspath += sourceSets.main.output + } + // Running regular tests + test { + java { + srcDirs = ['code/src/utest'] + } + compileClasspath += sourceSets.main.output + sourceSets.testcommon.output + runtimeClasspath += sourceSets.main.output + sourceSets.testcommon.output resources { srcDirs = ['code/src/testResources'] } @@ -141,7 +149,7 @@ sourceSets { // Integration Tests itest { java { - srcDirs = ['code/src/itest', 'code/src/testcommon'] + srcDirs = ['code/src/itest'] } resources { srcDirs = ['code/src/testResources'] @@ -152,7 +160,7 @@ sourceSets { // Running Slow Tests slowtest { java { - srcDirs = ['code/src/test', 'code/src/testcommon'] + srcDirs = ['code/src/test'] } resources { srcDirs = ['code/src/testResources'] @@ -162,6 +170,13 @@ sourceSets { } } +// Make the testcommon source set use the same dependency configurations as test. +configurations { + testcommonImplementation.extendsFrom testImplementation + testcommonRuntimeOnly.extendsFrom testRuntimeOnly + testcommonCompileOnly.extendsFrom testCompileOnly +} + /* Copy 'master' outputsheets into different genre folders */ tasks.register("copyMasterSheets", Copy) { def filesToCopy = copySpec { From a411ef3999898c62bd160893013f8ed45e6d1961 Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 22:21:19 +0200 Subject: [PATCH 11/22] Use JUnit BOM and align platform/jupiter versions Previously each junit-platform/junit-jupiter coordinate listed its own version, with junit-platform-runner pinned to a stale 1.14.4 while the others moved to 6.0.3. junit-platform-runner is for legacy @RunWith(JUnitPlatform.class) interop and isn't used anywhere, so drop it. Adopt org.junit:junit-bom:6.0.3 to source the version once for all junit-platform / junit-jupiter modules. About 870 tests still use the JUnit 4 API. They were compiling before because junit-platform-runner pulled junit:junit transitively. Add an explicit junit:junit:4.13.2 declaration plus junit-vintage-engine so those tests keep compiling and running on the JUnit Platform. --- build.gradle | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 9a4b9467346..c6e56178d26 100644 --- a/build.gradle +++ b/build.gradle @@ -254,11 +254,14 @@ dependencies { compileOnly 'com.yuvimasory:orange-extensions:1.3.0' compileOnly 'com.github.spotbugs:spotbugs-annotations:4.9.8' - testImplementation 'org.junit.platform:junit-platform-runner:1.14.4' - testImplementation 'org.junit.platform:junit-platform-launcher:6.0.3' - testImplementation 'org.junit.jupiter:junit-jupiter-api:6.0.3' - testImplementation 'org.junit.jupiter:junit-jupiter-params:6.0.3' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:6.0.3' + testImplementation platform('org.junit:junit-bom:6.0.3') + testImplementation 'org.junit.platform:junit-platform-launcher' + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testImplementation 'org.junit.jupiter:junit-jupiter-params' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' + // JUnit 4 is still used by ~870 legacy tests; pulled in via vintage engine + junit jar. + testImplementation 'junit:junit:4.13.2' + testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' testImplementation 'org.hamcrest:hamcrest:3.0' testImplementation 'org.testfx:testfx-junit5:4.0.18' From 42e80cce7b19beb99a57b2071c1530f50eeff9ed Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 22:25:29 +0200 Subject: [PATCH 12/22] Loop-generate per-game inttest tasks The five per-game inttest variants (sfinttest, pfinttest, rsrdinttest, srdinttest, msrdinttest) were copy-pasted with only the include pattern differing. Replace them with a map-driven loop. Task names are stable (CI references pfinttest), and the resolved configuration is identical. Pre-existing: the include pattern uses a "slowtest/" path prefix that doesn't exist in compiled classes, so these tasks have always reported NO-SOURCE. Behavior is preserved here; fixing the pattern is a separate change. --- build.gradle | 55 ++++++++++++++++------------------------------------ 1 file changed, 17 insertions(+), 38 deletions(-) diff --git a/build.gradle b/build.gradle index c6e56178d26..cc397503894 100644 --- a/build.gradle +++ b/build.gradle @@ -783,44 +783,23 @@ tasks.register("inttest", Test) { include 'slowtest/pcgen/inttest/**/*Test.class' } -tasks.register("sfinttest", Test) { - dependsOn jar - testClassesDirs = sourceSets.slowtest.output.classesDirs - classpath = sourceSets.slowtest.runtimeClasspath - forkEvery = 1 - include 'slowtest/pcgen/inttest/game_starfinder/*Test.class' -} - -tasks.register("pfinttest", Test) { - dependsOn jar - testClassesDirs = sourceSets.slowtest.output.classesDirs - classpath = sourceSets.slowtest.runtimeClasspath - forkEvery = 1 - include 'slowtest/pcgen/inttest/game_pathfinder/*Test.class' -} - -tasks.register("rsrdinttest", Test) { - dependsOn jar - testClassesDirs = sourceSets.slowtest.output.classesDirs - classpath = sourceSets.slowtest.runtimeClasspath - forkEvery = 1 - include 'slowtest/pcgen/inttest/game_35e/*Test.class' -} - -tasks.register("srdinttest", Test) { - dependsOn jar - testClassesDirs = sourceSets.slowtest.output.classesDirs - classpath = sourceSets.slowtest.runtimeClasspath - forkEvery = 1 - include 'slowtest/pcgen/inttest/game_3e/*Test.class' -} - -tasks.register("msrdinttest", Test) { - dependsOn jar - testClassesDirs = sourceSets.slowtest.output.classesDirs - classpath = sourceSets.slowtest.runtimeClasspath - forkEvery = 1 - include 'slowtest/pcgen/inttest/game_modern/*Test.class' +// Per-game-mode inttest variants. Each runs only the inttest classes for one +// game directory under code/src/test/pcgen/inttest. CI references these names +// (e.g. pfinttest) so the task names are stable. +[ + sfinttest : 'game_starfinder', + pfinttest : 'game_pathfinder', + rsrdinttest: 'game_35e', + srdinttest : 'game_3e', + msrdinttest: 'game_modern', +].each { taskName, gameDir -> + tasks.register(taskName, Test) { + dependsOn jar + testClassesDirs = sourceSets.slowtest.output.classesDirs + classpath = sourceSets.slowtest.runtimeClasspath + forkEvery = 1 + include "slowtest/pcgen/inttest/${gameDir}/*Test.class" + } } allprojects { From 0e7739bd8f31ff33de3099e6d7187c4343ae13b2 Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 22:29:34 +0200 Subject: [PATCH 13/22] Bump CodeQL action versions to v3 and checkout to v4 github/codeql-action v2 has been deprecated; runs emit warnings and will fail once GitHub retires the v2 endpoints. v3 is a drop-in upgrade for init, autobuild, and analyze. actions/checkout is bumped to v4 to match the other workflows in this repo. --- .github/workflows/codeql-analysis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 034648eff4b..e38727632b4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,11 +39,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -54,7 +54,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -68,4 +68,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 2a36fd4db2562cc204b8205701d7cb6db10479f3 Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 22:29:49 +0200 Subject: [PATCH 14/22] Align release workflow cache restore-keys with primary key The primary cache key was prefixed with \${{ runner.os }} (e.g. Linux, macOS, Windows) while the restore-keys used \${{ matrix.os }} (e.g. ubuntu-latest, macos-latest). Restore-keys must be a prefix of the primary key to ever match, so the fallback restore was dead code and every run cold-started build/jre and build/libs. --- .github/workflows/gradle-release-manual.yml | 4 ++-- .github/workflows/gradle-release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gradle-release-manual.yml b/.github/workflows/gradle-release-manual.yml index 3688e8f0553..cd54144e5e9 100644 --- a/.github/workflows/gradle-release-manual.yml +++ b/.github/workflows/gradle-release-manual.yml @@ -175,8 +175,8 @@ jobs: ${{ github.workspace }}/build/libs key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | - ${{ matrix.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - ${{ matrix.os }}-gradle + ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + ${{ runner.os }}-gradle - name: Build the image if: success() diff --git a/.github/workflows/gradle-release.yml b/.github/workflows/gradle-release.yml index dd9c6081f9b..4900103257c 100644 --- a/.github/workflows/gradle-release.yml +++ b/.github/workflows/gradle-release.yml @@ -145,8 +145,8 @@ jobs: ${{ github.workspace }}/build/libs key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | - ${{ matrix.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - ${{ matrix.os }}-gradle + ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + ${{ runner.os }}-gradle - name: Build the image if: success() From 7ae46bf27d0f9865001ca24aa39cc3ca945b5b91 Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 22:30:08 +0200 Subject: [PATCH 15/22] Rename tag-triggered release workflow to disambiguate from manual Both gradle-release.yml and gradle-release-manual.yml declared the same workflow name "Create Release with Manual Tag", which made them indistinguishable in the Actions UI. The manual one creates a tag on demand; this one fires when a tag is pushed (or dispatched against an existing tag). Renaming this one to reflect that. --- .github/workflows/gradle-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle-release.yml b/.github/workflows/gradle-release.yml index 4900103257c..2f3a91b728b 100644 --- a/.github/workflows/gradle-release.yml +++ b/.github/workflows/gradle-release.yml @@ -1,4 +1,4 @@ -name: Create Release with Manual Tag +name: Create Release on Tag Push on: push: From f270985a576f6a98b1de5987062fae8d8b9573ff Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 22:30:54 +0200 Subject: [PATCH 16/22] Drop redundant 'test' from PR test workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ./gradlew build already runs the test task via the standard Java lifecycle (build → check → test). The follow-up step then re-invoked test alongside itest/datatest/slowtest. The re-invocation is a no-op under up-to-date checks, but it still spins up a fresh Gradle daemon step and clutters the run summary. Drop it and rename the step to reflect what it actually adds. --- .github/workflows/gradle-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle-test.yml b/.github/workflows/gradle-test.yml index 4034f333f3b..667ba6865a9 100644 --- a/.github/workflows/gradle-test.yml +++ b/.github/workflows/gradle-test.yml @@ -53,8 +53,8 @@ jobs: - name: Build with Gradle Wrapper run: ./gradlew build - - name: Run tests - run: ./gradlew test itest datatest slowtest + - name: Run integration, data, and slow tests + run: ./gradlew itest datatest slowtest - name: Publish Test Results uses: EnricoMi/publish-unit-test-result-action@v2 From 70bc74df659b77104375333b317baef360b96f09 Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 22:31:08 +0200 Subject: [PATCH 17/22] Drop default and harmful Gradle cache flags from PR workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cache-disabled: false and cache-read-only: false are setup-gradle's defaults. cache-overwrite-existing: true is actively harmful for PR workflows: it forces every PR run to overwrite the shared Linux-runner Gradle cache, so concurrent PRs trash each other's warm caches. The intended behavior on PRs is to read the cache populated by master and write back only if the run is on the default branch — which is exactly what setup-gradle does by default. --- .github/workflows/gradle-test.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/gradle-test.yml b/.github/workflows/gradle-test.yml index 667ba6865a9..31ad82de230 100644 --- a/.github/workflows/gradle-test.yml +++ b/.github/workflows/gradle-test.yml @@ -35,10 +35,6 @@ jobs: # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - with: - cache-disabled: false - cache-read-only: false - cache-overwrite-existing: true - uses: actions/cache@v4 with: From 4970fcb472fb35bc0f491db17e59435dde72b76e Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 22:32:33 +0200 Subject: [PATCH 18/22] Split release pipeline into stages and drop redundant tasks The compound was: ./gradlew clean build copyToOutput test compileSlowtest \ datatest pfinttest allReports buildDist prepareRelease pcgenRelease Several entries were already in the dependency graph of pcgenRelease: - pcgenRelease dependsOn prepareRelease, assembleArtifacts, checksum - prepareRelease dependsOn build - assembleArtifacts dependsOn build, jlinkZip, sourcesJar - sourcesJar dependsOn classes, copyToOutput, distTar, distZip, startScripts - build (Java lifecycle) dependsOn check -> test So build, test, copyToOutput, and prepareRelease were all redundant when pcgenRelease was invoked at the end of the same command. The fact that they ran first only meant Gradle short-circuited later invocations under up-to-date checks, which still costs daemon time and obscures the failure point if any single step fails. The split makes each stage's purpose explicit and gives the GitHub Actions UI a real timeline of where time is being spent. --- .github/workflows/gradle-release-manual.yml | 17 ++++++++++++++--- .github/workflows/gradle-release.yml | 17 ++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/.github/workflows/gradle-release-manual.yml b/.github/workflows/gradle-release-manual.yml index cd54144e5e9..6ddb2a0583e 100644 --- a/.github/workflows/gradle-release-manual.yml +++ b/.github/workflows/gradle-release-manual.yml @@ -178,9 +178,20 @@ jobs: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} ${{ runner.os }}-gradle - - name: Build the image - if: success() - run: ./gradlew clean build copyToOutput test compileSlowtest datatest pfinttest allReports buildDist prepareRelease pcgenRelease + - name: Compile and run unit tests + run: ./gradlew clean build + + - name: Run data and integration tests + run: ./gradlew compileSlowtest datatest pfinttest + + - name: Run static analysis reports + run: ./gradlew allReports + + - name: Build distribution zips + run: ./gradlew buildDist + + - name: Build release artifacts + run: ./gradlew pcgenRelease - name: Upload zip based release assets for all platforms uses: actions/upload-artifact@v4 diff --git a/.github/workflows/gradle-release.yml b/.github/workflows/gradle-release.yml index 2f3a91b728b..7a1000f66c0 100644 --- a/.github/workflows/gradle-release.yml +++ b/.github/workflows/gradle-release.yml @@ -148,9 +148,20 @@ jobs: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} ${{ runner.os }}-gradle - - name: Build the image - if: success() - run: ./gradlew clean build copyToOutput test compileSlowtest datatest pfinttest allReports buildDist prepareRelease pcgenRelease + - name: Compile and run unit tests + run: ./gradlew clean build + + - name: Run data and integration tests + run: ./gradlew compileSlowtest datatest pfinttest + + - name: Run static analysis reports + run: ./gradlew allReports + + - name: Build distribution zips + run: ./gradlew buildDist + + - name: Build release artifacts + run: ./gradlew pcgenRelease - name: Upload zip based release assets for all platforms uses: actions/upload-artifact@v4 From 7d67a1a4634609565fc91000736fec6de535c350 Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 22:33:24 +0200 Subject: [PATCH 19/22] Collapse triple-overlapping CI cache to setup-gradle only Each workflow stacked three caches: 1. actions/setup-java cache: gradle -> caches ~/.gradle/{caches,wrapper} 2. gradle/actions/setup-gradle@v4 -> caches ~/.gradle/{caches,wrapper,configuration-cache} 3. actions/cache@v4 for build/jre + build/libs Layers 1 and 2 cover the same paths; setup-gradle is the canonical choice and adds Gradle-aware features (configuration cache, dep reports, write-once-on-default-branch semantics). Layer 3 caches *build outputs* across runs, which: - is unsafe (stale jars from a different commit can leak in) - is unused (build/jre is no longer produced; the 'jre' task is a no-op aggregator that just dependsOn downloadJavaFXMods) - duplicates Gradle's own up-to-date checks for build/libs Drop layers 1 and 3, keep setup-gradle. Release workflows keep their cache-overwrite-existing: true since they run from tagged commits on master and should refresh the shared cache. --- .github/workflows/gradle-release-manual.yml | 22 ++------------------- .github/workflows/gradle-release.yml | 22 ++------------------- .github/workflows/gradle-test.yml | 20 +------------------ 3 files changed, 5 insertions(+), 59 deletions(-) diff --git a/.github/workflows/gradle-release-manual.yml b/.github/workflows/gradle-release-manual.yml index 6ddb2a0583e..80c84886fa5 100644 --- a/.github/workflows/gradle-release-manual.yml +++ b/.github/workflows/gradle-release-manual.yml @@ -150,16 +150,8 @@ jobs: with: java-version: '25' distribution: 'temurin' - cache: gradle - cache-dependency-path: | - build.gradle - code/gradle/autobuild.gradle - code/gradle/distribution.gradle - code/gradle/release.gradle - code/gradle/reporting.gradle - code/gradle/plugins.gradle - - # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies. + + # setup-gradle@v4 is the canonical Gradle cache for GitHub Actions. # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 @@ -168,16 +160,6 @@ jobs: cache-read-only: false cache-overwrite-existing: true - - uses: actions/cache@v4 - with: - path: | - ${{ github.workspace }}/build/jre - ${{ github.workspace }}/build/libs - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - ${{ runner.os }}-gradle - - name: Compile and run unit tests run: ./gradlew clean build diff --git a/.github/workflows/gradle-release.yml b/.github/workflows/gradle-release.yml index 7a1000f66c0..dcb151355da 100644 --- a/.github/workflows/gradle-release.yml +++ b/.github/workflows/gradle-release.yml @@ -120,16 +120,8 @@ jobs: with: java-version: '25' distribution: 'temurin' - cache: gradle - cache-dependency-path: | - build.gradle - code/gradle/autobuild.gradle - code/gradle/distribution.gradle - code/gradle/release.gradle - code/gradle/reporting.gradle - code/gradle/plugins.gradle - - # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies. + + # setup-gradle@v4 is the canonical Gradle cache for GitHub Actions. # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 @@ -138,16 +130,6 @@ jobs: cache-read-only: false cache-overwrite-existing: true - - uses: actions/cache@v4 - with: - path: | - ${{ github.workspace }}/build/jre - ${{ github.workspace }}/build/libs - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - ${{ runner.os }}-gradle - - name: Compile and run unit tests run: ./gradlew clean build diff --git a/.github/workflows/gradle-test.yml b/.github/workflows/gradle-test.yml index 31ad82de230..acc144d5617 100644 --- a/.github/workflows/gradle-test.yml +++ b/.github/workflows/gradle-test.yml @@ -22,30 +22,12 @@ jobs: with: java-version: '25' distribution: 'temurin' - cache: gradle - cache-dependency-path: | - build.gradle - code/gradle/autobuild.gradle - code/gradle/distribution.gradle - code/gradle/release.gradle - code/gradle/reporting.gradle - code/gradle/plugins.gradle - # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies. + # setup-gradle@v4 is the canonical Gradle cache for GitHub Actions. # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - - uses: actions/cache@v4 - with: - path: | - ${{ github.workspace }}/build/jre - ${{ github.workspace }}/build/libs - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - ${{ runner.os }}-gradle - - name: Build with Gradle Wrapper run: ./gradlew build From 75d675497d446e3c9d7c5381e9a30ce383d15517 Mon Sep 17 00:00:00 2001 From: Vest Date: Sun, 17 May 2026 22:34:20 +0200 Subject: [PATCH 20/22] Attach release artifacts to GitHub Release Both release workflows produced artifacts in build/release/ but only uploaded a subset of them as workflow artifacts (and only for macOS), leaving the actual GitHub Release empty. The original intent was sketched out in commented-out softprops/action-gh-release blocks at the bottom of gradle-release.yml. This wires the release.gradle output (sources jar, image zips, jpackage installers) directly to the GitHub Release using the same tag the create_release job used. softprops/action-gh-release is idempotent against an existing release with the same tag_name, so each matrix OS appends its own platform-specific artifacts. Workflow artifacts are kept too, so debugging a failed release run still works without having to publish. --- .github/workflows/gradle-release-manual.yml | 24 +++++------ .github/workflows/gradle-release.yml | 45 +++++---------------- 2 files changed, 18 insertions(+), 51 deletions(-) diff --git a/.github/workflows/gradle-release-manual.yml b/.github/workflows/gradle-release-manual.yml index 80c84886fa5..b641f8d7a23 100644 --- a/.github/workflows/gradle-release-manual.yml +++ b/.github/workflows/gradle-release-manual.yml @@ -175,22 +175,16 @@ jobs: - name: Build release artifacts run: ./gradlew pcgenRelease - - name: Upload zip based release assets for all platforms + - name: Upload release artifacts as workflow artifacts uses: actions/upload-artifact@v4 with: - name: ${{ matrix.os }}-zip - path: ${{ github.workspace }}/build/release/image-*.zip + name: ${{ matrix.release_suffix }}-release + path: ${{ github.workspace }}/build/release/* + if-no-files-found: error - - name: Upload DMG release asset for macos - uses: actions/upload-artifact@v4 - if: matrix.os == 'macos-latest' - with: - name: ${{ matrix.os }}-dmg - path: ${{ github.workspace }}/build/release/*.dmg - - - name: Upload PKG release asset for macos - uses: actions/upload-artifact@v4 - if: matrix.os == 'macos-latest' + - name: Attach release artifacts to GitHub Release + uses: softprops/action-gh-release@v2 with: - name: ${{ matrix.os }}-pkg - path: ${{ github.workspace }}/build/release/*.pkg + tag_name: ${{ inputs.tag_name }} + files: ${{ github.workspace }}/build/release/* + fail_on_unmatched_files: true diff --git a/.github/workflows/gradle-release.yml b/.github/workflows/gradle-release.yml index dcb151355da..af86f675508 100644 --- a/.github/workflows/gradle-release.yml +++ b/.github/workflows/gradle-release.yml @@ -145,43 +145,16 @@ jobs: - name: Build release artifacts run: ./gradlew pcgenRelease - - name: Upload zip based release assets for all platforms + - name: Upload release artifacts as workflow artifacts uses: actions/upload-artifact@v4 - if: matrix.os == 'macos-latest' with: - name: ${{ matrix.os }}-zip - path: ${{ github.workspace }}/build/release/image-*.zip + name: ${{ matrix.release_suffix }}-release + path: ${{ github.workspace }}/build/release/* + if-no-files-found: error - - name: Upload DMG release asset for macos - uses: actions/upload-artifact@v4 - if: matrix.os == 'macos-latest' - with: - name: ${{ matrix.os }}-dmg - path: ${{ github.workspace }}/build/release/*.dmg - - - name: Upload PKG release asset for macos - uses: actions/upload-artifact@v4 - if: matrix.os == 'macos-latest' + - name: Attach release artifacts to GitHub Release + uses: softprops/action-gh-release@v2 with: - name: ${{ matrix.os }}-pkg - path: ${{ github.workspace }}/build/release/*.pkg - -# - name: Upload release assets for ubuntu -# uses: actions/upload-artifact@v4 -# if: matrix.os == 'ubuntu-latest' -# with: -# name: ${{ matrix.os }} -# path: ${{ github.workspace }}/build/jpackage/*.deb -# -# - name: Upload release assets for windows -# uses: actions/upload-artifact@v4 -# if: matrix.os == 'windows-latest' -# with: -# name: ${{ matrix.os }} -# path: ${{ github.workspace }}/build/jpackage/*.msi -# -# - name: Release - ${{ matrix.os }} -# uses: softprops/action-gh-release@v2 -# with: -# tag_name: ${{ needs.create_release.outputs.tag-name }} -# files: ${{ github.workspace }}/build/release/pcgen-*.* + tag_name: ${{ inputs.tag_name || github.ref_name }} + files: ${{ github.workspace }}/build/release/* + fail_on_unmatched_files: true From ecf9724b424fc6f71442aaadb3a19b9b0404d756 Mon Sep 17 00:00:00 2001 From: Vest Date: Mon, 18 May 2026 07:31:19 +0200 Subject: [PATCH 21/22] Update README to clarify test suite commands and remove redundancy Signed-off-by: Vest --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 86677ddba73..668778cdcc9 100644 --- a/README.md +++ b/README.md @@ -193,9 +193,12 @@ substitute `./gradlew` for `gradle` on the command line. ### Run Full Test Suite Do this primarily __before__ pull requests. -This is almost exactly the command Travis runs to verify, if it fails locally you will fail the build, and your PR will not be merged. +This mirrors what GitHub Actions runs to verify a PR; if it fails locally your CI build will also fail and your PR will not be merged. - ./gradlew clean build copyToOutput test compileSlowtest datatest pfinttest allReports buildDist + ./gradlew build + ./gradlew itest datatest slowtest + +`build` already runs the unit `test` task via the standard Java lifecycle, so it is not repeated. The second command runs the integration, data, and slow test suites. ### Clean All Build Files ./gradlew clean From 2bef5d176c17f1ef43477f486f846b7c102f0216 Mon Sep 17 00:00:00 2001 From: Vest Date: Mon, 18 May 2026 10:10:08 +0200 Subject: [PATCH 22/22] Cache downloaded JDK and JavaFX archives in release workflows The release pipeline downloads ~1.5 GB of cross-platform JDK and JavaFX jmod archives on every CI run (5 platforms, on each of 4 OS runners). The downloads are pinned by javaVersion in gradle.properties and the URL templates in build.gradle, so they are perfectly cacheable between runs. Add an actions/cache@v4 step keyed on the hash of those two files, so the cache invalidates exactly when the JDK or JavaFX version changes and never serves stale archives otherwise. Only the archive files are cached (.tar.gz / .zip) -- extracted JDK directories are 5x larger and Gradle's extractJdk_* tasks have proper up-to-date checks, so we let extraction run locally each time. Scoped to the two release workflows; the PR test workflow does not run jpackage and never populates jdks/. --- .github/workflows/gradle-release-manual.yml | 17 +++++++++++++++++ .github/workflows/gradle-release.yml | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/.github/workflows/gradle-release-manual.yml b/.github/workflows/gradle-release-manual.yml index b641f8d7a23..c9effb7c8a6 100644 --- a/.github/workflows/gradle-release-manual.yml +++ b/.github/workflows/gradle-release-manual.yml @@ -160,6 +160,23 @@ jobs: cache-read-only: false cache-overwrite-existing: true + # Cache cross-platform JDK and JavaFX jmod archives downloaded by + # downloadJdk_* / downloadJavaFXMods_* tasks. These are pinned by + # javaVersion (gradle.properties) and the URL templates (build.gradle), + # so keying on those file hashes invalidates correctly when we bump + # the JDK or JavaFX version. We cache only the archives (.tar.gz / .zip) + # and let Gradle re-extract them each run via its own up-to-date checks. + - name: Cache downloaded JDK and JavaFX archives + uses: actions/cache@v4 + with: + path: | + jdks/jdk_*.tar.gz + jdks/jdk_*.zip + jdks/jfx_jmods_*.zip + key: jdks-${{ hashFiles('gradle.properties', 'build.gradle') }} + restore-keys: | + jdks- + - name: Compile and run unit tests run: ./gradlew clean build diff --git a/.github/workflows/gradle-release.yml b/.github/workflows/gradle-release.yml index af86f675508..704e4656018 100644 --- a/.github/workflows/gradle-release.yml +++ b/.github/workflows/gradle-release.yml @@ -130,6 +130,23 @@ jobs: cache-read-only: false cache-overwrite-existing: true + # Cache cross-platform JDK and JavaFX jmod archives downloaded by + # downloadJdk_* / downloadJavaFXMods_* tasks. These are pinned by + # javaVersion (gradle.properties) and the URL templates (build.gradle), + # so keying on those file hashes invalidates correctly when we bump + # the JDK or JavaFX version. We cache only the archives (.tar.gz / .zip) + # and let Gradle re-extract them each run via its own up-to-date checks. + - name: Cache downloaded JDK and JavaFX archives + uses: actions/cache@v4 + with: + path: | + jdks/jdk_*.tar.gz + jdks/jdk_*.zip + jdks/jfx_jmods_*.zip + key: jdks-${{ hashFiles('gradle.properties', 'build.gradle') }} + restore-keys: | + jdks- + - name: Compile and run unit tests run: ./gradlew clean build