@@ -43,6 +43,122 @@ tasks {
4343 named(" clean" ) {
4444 delete(" .sandbox" )
4545 }
46+
47+ val validateReleaseCredentials by registering {
48+ group = " publishing"
49+ description = " Pre-flight credential checks required for release"
50+ doLast {
51+ val signingKey = project.findProperty(" signingKey" ) as String?
52+ val signingPassword = project.findProperty(" signingPassword" ) as String?
53+ val ossrhUser = project.findProperty(" ossrhUsername" ) as String?
54+ val ossrhPass = project.findProperty(" ossrhPassword" ) as String?
55+ val chocoKey = System .getenv(" CHOCOLATEY_API_KEY" )
56+ val onWindows = System .getProperty(" os.name" ).lowercase().contains(" windows" )
57+
58+ val labels = mutableListOf<String >()
59+ val results = mutableListOf<Boolean >()
60+ fun check (label : String , value : String? , rejectDummy : Boolean = true) {
61+ labels.add(label)
62+ results.add(! value.isNullOrBlank() && (! rejectDummy || value != " dummy" ))
63+ }
64+
65+ val ghOk = ProcessBuilder (" gh" , " auth" , " status" )
66+ .redirectErrorStream(true ).start().waitFor() == 0
67+
68+ val expectedTag = " v${project.version} "
69+ val gitTagProc = ProcessBuilder (" git" , " tag" , " --points-at" , " HEAD" )
70+ .redirectErrorStream(true ).start()
71+ val tagOk = gitTagProc.inputStream.bufferedReader().readLines()
72+ .any { it.trim() == expectedTag }
73+ .also { gitTagProc.waitFor() }
74+
75+ check(" signingKey" , signingKey)
76+ check(" signingPassword" , signingPassword, rejectDummy = false )
77+ check(" ossrhUsername" , ossrhUser)
78+ check(" ossrhPassword" , ossrhPass)
79+ if (onWindows) check(" CHOCOLATEY_API_KEY (env var)" , chocoKey, rejectDummy = false )
80+
81+ println (" \n 🔑 Credential check:" )
82+ labels.zip(results).forEach { (label, ok) -> println (" ${if (ok) " ✅" else " ❌" } $label " ) }
83+ if (! onWindows) println (" ⏭️ CHOCOLATEY_API_KEY (skipped — Chocolatey publish requires Windows)" )
84+ println (" ${if (ghOk) " ✅" else " ❌" } GitHub CLI (gh auth status)" )
85+ println (" ${if (tagOk) " ✅" else " ❌" } git tag $expectedTag on HEAD" )
86+ println ()
87+
88+ val failures = labels.zip(results).filter { ! it.second }.map { it.first } +
89+ listOfNotNull(
90+ if (! ghOk) " GitHub CLI (gh auth status)" else null ,
91+ if (! tagOk) " git tag $expectedTag not found on HEAD — run: git tag $expectedTag " else null
92+ )
93+ if (failures.isNotEmpty()) {
94+ throw GradleException (
95+ " Release pre-flight failed — missing or invalid credentials:\n " +
96+ failures.joinToString(" \n " ) { " ❌ $it " } +
97+ " \n\n See release.md → Prerequisites for setup instructions."
98+ )
99+ }
100+ println (" ✅ All credentials present. Starting release...\n " )
101+ }
102+ }
103+
104+ val createGitHubRelease by registering(Exec ::class ) {
105+ group = " publishing"
106+ description = " Creates a GitHub release and attaches the setup JAR"
107+ val version = project.version.toString()
108+ commandLine(
109+ " gh" , " release" , " create" , " v$version " ,
110+ " --title" , " Robocode $version " ,
111+ " --notes" , " See [versions.md](https://github.com/robo-code/robocode/blob/main/versions.md) for release notes." ,
112+ " build/robocode-$version -setup.jar"
113+ )
114+ }
115+
116+ register(" release" ) {
117+ group = " publishing"
118+ description = " Full release: pre-flight checks, build, Maven Central, Chocolatey (Windows), GitHub Release"
119+ dependsOn(validateReleaseCredentials)
120+ dependsOn(" build" )
121+ dependsOn(createGitHubRelease)
122+ // Per-subproject publish tasks, closeAndRelease, and ordering are wired in projectsEvaluated below
123+ }
124+ }
125+
126+ // Use projectsEvaluated (runs after ALL afterEvaluate callbacks, including the nexus plugin's)
127+ // so that per-subproject publishAllPublicationsToSonatypeRepository tasks are already registered.
128+ gradle.projectsEvaluated {
129+ val releaseTask = tasks.named(" release" )
130+ val validateCreds = tasks.named(" validateReleaseCredentials" )
131+ tasks.named(" build" ) { mustRunAfter(validateCreds) }
132+
133+ val publishTasks = subprojects
134+ .mapNotNull { it.tasks.findByName(" publishAllPublicationsToSonatypeRepository" ) }
135+
136+ publishTasks.forEach { publishTask ->
137+ releaseTask.configure { dependsOn(publishTask) }
138+ publishTask.mustRunAfter(" build" )
139+ }
140+
141+ tasks.findByName(" closeAndReleaseSonatypeStagingRepository" )?.let { closeTask ->
142+ releaseTask.configure { dependsOn(closeTask) }
143+ closeTask.mustRunAfter(* publishTasks.toTypedArray())
144+ }
145+
146+ val onWindows = System .getProperty(" os.name" ).lowercase().contains(" windows" )
147+ val chocoKey = System .getenv(" CHOCOLATEY_API_KEY" )
148+ val closeTask = tasks.findByName(" closeAndReleaseSonatypeStagingRepository" )
149+ val ghRelease = tasks.named(" createGitHubRelease" )
150+
151+ var lastBeforeGhRelease: Any = closeTask ? : " build"
152+
153+ if (onWindows && ! chocoKey.isNullOrBlank()) {
154+ project(" :robocode.installer" ).tasks.findByName(" chocoPush" )?.let { chocoPush ->
155+ releaseTask.configure { dependsOn(chocoPush) }
156+ if (closeTask != null ) chocoPush.mustRunAfter(closeTask)
157+ lastBeforeGhRelease = chocoPush
158+ }
159+ }
160+
161+ ghRelease.configure { mustRunAfter(lastBeforeGhRelease) }
46162}
47163
48164nexusPublishing {
0 commit comments