Skip to content

Commit b5ff065

Browse files
authored
Always populate snapshotVersions manually (#193)
1 parent 7d280be commit b5ff065

5 files changed

Lines changed: 155 additions & 61 deletions

File tree

nmcp/src/main/kotlin/nmcp/internal/task/layout.kt

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package nmcp.internal.task
22

3+
import org.gradle.internal.declarativedsl.parsing.parse
4+
5+
/**
6+
* See https://maven.apache.org/repositories/layout.html
7+
*/
38
internal data class Gav(
49
val groupId: String,
510
val artifactId: String,
6-
val version: String,
11+
val baseVersion: String,
712
) {
813
companion object {
914
fun from(gavPath: String): Gav {
@@ -33,11 +38,95 @@ internal data class Gav(
3338
}
3439
}
3540

36-
internal fun String.replaceBuildNumber(artifactId: String, snapshotVersion: String, newBuildNumber: Int): String {
37-
// module1-0.0.3-20250623.104441-1.jar.asc
38-
val versionWithoutSnapshot = snapshotVersion.replace("-SNAPSHOT","")
39-
return replace(Regex("(${artifactId}-$versionWithoutSnapshot-[0-9]{8}\\.[0-9]{6}-)[0-9]+(.*)")) {
40-
"${it.groupValues[1]}$newBuildNumber${it.groupValues[2]}"
41+
private fun String.classifierAndExtension(index: Int): Pair<String?, String> {
42+
check(this.isNotEmpty()) {
43+
"Nmcp: Premature end of artifact name, cannot find extension"
44+
}
45+
val name = this
46+
val dashOrDot = index
47+
val dot: Int
48+
val classifier = if (name.get(dashOrDot) == '-') {
49+
dot = name.indexOfFirst { it == '.' }
50+
check(dashOrDot >= 0) {
51+
"Nmcp: No extension found in '$this'"
52+
}
53+
name.substring(dashOrDot + 1, dot)
54+
} else {
55+
check(name.get(dashOrDot) == '.') {
56+
"Nmcp: No extension found in '$this'"
57+
}
58+
dot = dashOrDot
59+
null
60+
}
61+
62+
return classifier to name.substring(dot + 1)
63+
}
64+
65+
internal data class Artifact(
66+
val artifactId: String,
67+
val version: String,
68+
val classifier: String?,
69+
val extension: String,
70+
) {
71+
constructor(artifactId: String, version: String, pair: Pair<String?, String>) : this(
72+
artifactId,
73+
version,
74+
pair.first,
75+
pair.second,
76+
)
77+
78+
fun fileName(): String {
79+
return buildString {
80+
append(artifactId)
81+
append('-')
82+
append(version)
83+
if (classifier != null) {
84+
append('-')
85+
append(classifier)
86+
}
87+
append('.')
88+
append(extension)
89+
}
90+
}
91+
92+
companion object {
93+
fun from(name: String, artifactId: String, baseVersion: String): Artifact {
94+
return if (!baseVersion.endsWith("-SNAPSHOT")) {
95+
/**
96+
* SNAPSHOT
97+
*/
98+
check(name.startsWith("$artifactId-$baseVersion")) {
99+
"Nmcp: '$this' does not start with '$artifactId-$baseVersion' and is not a SNAPSHOT"
100+
}
101+
102+
val dashOrDot = "$artifactId-$baseVersion".length
103+
104+
Artifact(
105+
artifactId, baseVersion, name.classifierAndExtension(dashOrDot),
106+
)
107+
} else {
108+
/**
109+
* SNAPSHOT, name should look like "module1-0.0.3-20250623.104441-1.jar.asc"
110+
*/
111+
val versionWithoutSnapshot = baseVersion.removeSuffix("-SNAPSHOT")
112+
check(name.startsWith("$artifactId-$versionWithoutSnapshot")) {
113+
"Nmcp: '$this' is a SNAPSHOT and should start with '$artifactId-$versionWithoutSnapshot'"
114+
}
115+
val regex = Regex("(-[0-9]{8}\\.[0-9]{6}-[0-9]+)(.*)")
116+
val matchResult = regex.matchAt(name, "$artifactId-$versionWithoutSnapshot".length)
117+
check(matchResult != null) {
118+
"Nmcp: '$this' doesn't match ${regex.pattern}"
119+
}
120+
121+
val ce = matchResult.groupValues.get(2)
122+
123+
Artifact(
124+
artifactId,
125+
"$baseVersion${matchResult.groupValues.get(1)}",
126+
ce.classifierAndExtension(0),
127+
)
128+
}
129+
}
41130
}
42131
}
43132

nmcp/src/main/kotlin/nmcp/internal/task/nmcpFindDeploymentName.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ internal fun nmcpFindDeploymentName(inputFiles: GInputFiles, outputFile: GOutput
2424

2525
val groups = gavs.map { it.groupId }.distinct()
2626
val artifacts = gavs.map { it.artifactId }.distinct()
27-
val versions = gavs.map { it.version }.distinct()
27+
val versions = gavs.map { it.baseVersion }.distinct()
2828

2929
val deploymentName = buildString {
3030
append(groups.toDisplayName())

nmcp/src/main/kotlin/nmcp/transport/publishFileByFile.kt

Lines changed: 56 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import gratatouille.tasks.FileWithPath
44
import gratatouille.tasks.GInputFiles
55
import java.security.MessageDigest
66
import java.time.Instant
7+
import java.time.ZoneOffset
78
import kotlinx.coroutines.Dispatchers
89
import kotlinx.coroutines.joinAll
910
import kotlinx.coroutines.launch
@@ -14,7 +15,7 @@ import kotlinx.serialization.encodeToString
1415
import nmcp.internal.task.ArtifactMetadata
1516
import nmcp.internal.task.Gav
1617
import nmcp.internal.task.VersionMetadata
17-
import nmcp.internal.task.replaceBuildNumber
18+
import nmcp.internal.task.Artifact
1819
import nmcp.internal.task.xml
1920
import okio.ByteString.Companion.toByteString
2021

@@ -37,7 +38,7 @@ fun publishFileByFile(
3738
.map { it.normalizedPath.substringBeforeLast('/') }
3839
.distinct()
3940

40-
val lastUpdated = timestampNow()
41+
val lastUpdated = Instant.now()
4142

4243
runBlocking {
4344
withContext(Dispatchers.IO.limitedParallelism(parallelism)) {
@@ -53,11 +54,11 @@ fun publishFileByFile(
5354
private fun publishGav(
5455
gavPath: String,
5556
allFiles: List<FileWithPath>,
56-
lastUpdated: String,
57+
lastUpdated: Instant,
5758
transport: Transport,
5859
) {
5960
val gav = Gav.from(gavPath)
60-
val version = gav.version
61+
val version = gav.baseVersion
6162
val gavFiles = allFiles.filter { it.normalizedPath.startsWith(gavPath) }
6263

6364
/**
@@ -69,58 +70,65 @@ private fun publishGav(
6970
* - update the [version metadata](https://maven.apache.org/repositories/metadata.html).
7071
* - patch the file names to include the new build number.
7172
*
72-
* See https://s01.oss.sonatype.org/content/repositories/snapshots/com/apollographql/apollo/apollo-api-jvm/maven-metadata.xml for an example.
73-
*
7473
* For snapshots, it's not 100% clear who owns the metadata as the repository might expire some snapshot and therefore need to rewrite the
7574
* metadata to keep things consistent. This means there are 2 possibly concurrent writers to maven-metadata.xml: the repository and the
7675
* publisher. Hopefully, it's not too much of a problem in practice.
7776
*
7877
* See https://github.com/gradle/gradle/blob/d1ee068b1ee7f62ffcbb549352469307781af72e/platforms/software/maven/src/main/java/org/gradle/api/publish/maven/internal/publisher/MavenRemotePublisher.java#L70.
78+
*
7979
*/
8080
val versionMetadataPath = "$gavPath/maven-metadata.xml"
81-
val localVersionMetadataFile = gavFiles.firstOrNull {
82-
it.normalizedPath == versionMetadataPath
83-
}
84-
val localVersionMetadata = if (localVersionMetadataFile != null) {
85-
xml.decodeFromString<VersionMetadata>(localVersionMetadataFile.file.readText())
86-
} else {
87-
VersionMetadata(
88-
groupId = gav.groupId,
89-
artifactId = gav.artifactId,
90-
version = gav.version,
91-
versioning = VersionMetadata.Versioning(
92-
snapshot = VersionMetadata.Snapshot(timestamp = lastUpdated, buildNumber = 1),
93-
lastUpdated = lastUpdated,
94-
snapshotVersions = emptyList(),
95-
),
96-
)
97-
}
98-
9981
val remoteVersionMetadata = transport.get(versionMetadataPath)
100-
10182
val buildNumber = if (remoteVersionMetadata == null) {
10283
1
10384
} else {
10485
xml.decodeFromString<VersionMetadata>(remoteVersionMetadata.use { it.readUtf8() }).versioning.snapshot.buildNumber + 1
10586
}
10687

107-
val newVersionMetadata = localVersionMetadata.copy(
108-
versioning = localVersionMetadata.versioning.copy(
109-
snapshot = localVersionMetadata.versioning.snapshot.copy(buildNumber = buildNumber),
110-
),
111-
)
88+
val renamedFiles = mutableListOf<FileWithPath>()
89+
val snapshotVersions = mutableListOf<VersionMetadata.SnapshotVersion>()
11290

113-
val renamedFiles = gavFiles.mapNotNull {
91+
gavFiles.forEach {
11492
if (it.file.name.startsWith("maven-metadata.xml")) {
115-
return@mapNotNull null
93+
return@forEach
94+
}
95+
val artifact = Artifact.from(it.file.name, gav.artifactId, gav.baseVersion)
96+
val newVersion = "${gav.baseVersion.removeSuffix("-SNAPSHOT")}-${lastUpdated.asTimestamp(true)}-$buildNumber"
97+
val newArtifact = artifact.copy(version = newVersion)
98+
val newName = newArtifact.fileName()
99+
renamedFiles.add(FileWithPath(it.file, "$gavPath/${newName}"))
100+
101+
if (newArtifact.extension.substringAfterLast('.') !in checksums) {
102+
snapshotVersions.add(
103+
VersionMetadata.SnapshotVersion(
104+
classifier = newArtifact.classifier,
105+
extension = newArtifact.extension,
106+
value = newArtifact.version,
107+
updated = lastUpdated.asTimestamp(false),
108+
),
109+
)
116110
}
117-
val newName = it.file.name.replaceBuildNumber(gav.artifactId, gav.version, buildNumber)
118-
FileWithPath(it.file, "$gavPath/$newName")
119111
}
120112

113+
val versionMetadata =
114+
VersionMetadata(
115+
groupId = gav.groupId,
116+
artifactId = gav.artifactId,
117+
version = gav.baseVersion,
118+
versioning = VersionMetadata.Versioning(
119+
snapshot = VersionMetadata.Snapshot(
120+
timestamp = lastUpdated.asTimestamp(true),
121+
buildNumber = buildNumber,
122+
),
123+
lastUpdated = lastUpdated.asTimestamp(false),
124+
snapshotVersions = snapshotVersions,
125+
),
126+
)
127+
128+
121129
transport.uploadFiles(renamedFiles)
122130

123-
val bytes = encodeToXml(newVersionMetadata).toByteArray()
131+
val bytes = encodeToXml(versionMetadata).toByteArray()
124132
transport.put(versionMetadataPath, bytes)
125133
setOf("md5", "sha1", "sha256", "sha512").forEach {
126134
transport.put("$versionMetadataPath.$it", bytes.digest(it.uppercase()))
@@ -135,7 +143,7 @@ private fun publishGav(
135143
/**
136144
* Update the [artifact metadata](https://maven.apache.org/repositories/metadata.html).
137145
*
138-
* See https://repo1.maven.org/maven2/com/apollographql/apollo/apollo-api-jvm/maven-metadata.xml for an example.
146+
* See https://repo1.maven.org/maven2/com/apollographql/apollo/apollo-api-jvm/maven-metadata.xml for an example of artifact metadata.
139147
*/
140148
val index = gavPath.lastIndexOf('/')
141149
check(index != -1) {
@@ -149,10 +157,10 @@ private fun publishGav(
149157
groupId = gav.groupId,
150158
artifactId = gav.artifactId,
151159
versioning = ArtifactMetadata.Versioning(
152-
latest = gav.version,
153-
release = gav.version,
160+
latest = gav.baseVersion,
161+
release = gav.baseVersion,
154162
versions = emptyList(),
155-
lastUpdated = lastUpdated,
163+
lastUpdated = lastUpdated.asTimestamp(false),
156164
),
157165
)
158166
} else {
@@ -171,8 +179,8 @@ private fun publishGav(
171179
* See https://github.com/gradle/gradle/blob/cb0c615fb8e3690971bb7f89ad80f58943360624/platforms/software/maven/src/main/java/org/gradle/api/publish/maven/internal/publisher/AbstractMavenPublisher.java#L116.
172180
*/
173181
val versions = existingVersions.toMutableList()
174-
if (!versions.none { it == gav.version }) {
175-
versions.add(gav.version)
182+
if (!versions.none { it == gav.baseVersion }) {
183+
versions.add(gav.baseVersion)
176184
}
177185
val newArtifactMetadata = localArtifactMetadata.copy(
178186
versioning = localArtifactMetadata.versioning.copy(
@@ -182,7 +190,7 @@ private fun publishGav(
182190

183191
val bytes = encodeToXml(newArtifactMetadata).toByteArray()
184192
transport.put(artifactMetadataPath, bytes)
185-
setOf("md5", "sha1", "sha256", "sha512").forEach {
193+
checksums.forEach {
186194
transport.put("$artifactMetadataPath.$it", bytes.digest(it.uppercase()))
187195
}
188196
}
@@ -194,11 +202,12 @@ private fun Transport.uploadFiles(filesWithPath: List<FileWithPath>) {
194202
}
195203
}
196204

197-
internal fun timestampNow(): String {
198-
val now = Instant.now().atZone(java.time.ZoneOffset.UTC)
205+
internal fun Instant.asTimestamp(withDot: Boolean): String {
206+
val now = this.atZone(ZoneOffset.UTC)
199207

208+
val dot = if (withDot) "." else ""
200209
return String.format(
201-
"%04d%02d%02d%02d%02d%02d",
210+
"%04d%02d%02d${dot}%02d%02d%02d",
202211
now.year,
203212
now.monthValue,
204213
now.dayOfMonth,
@@ -208,6 +217,8 @@ internal fun timestampNow(): String {
208217
)
209218
}
210219

220+
internal val checksums = setOf("md5", "sha1", "sha256", "sha512")
221+
211222
/**
212223
* Helper function to add the `<?xml...` preamble as I haven't found how to do it with xmlutils
213224
*/

nmcp/src/test/kotlin/LayoutTest.kt

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import kotlin.test.Test
22
import kotlin.test.assertEquals
33
import kotlin.test.assertFails
44
import nmcp.internal.task.Gav
5-
import nmcp.internal.task.replaceBuildNumber
65

76
class LayoutTest {
87
@Test
@@ -20,10 +19,4 @@ class LayoutTest {
2019
assertFails { Gav.from("com//1.0.0") }
2120
assertFails { Gav.from("//") }
2221
}
23-
24-
@Test
25-
fun replaceBuildNumber() {
26-
val fileName = "module1-0.0.3-20250623.104441-1.jar.asc"
27-
assertEquals("module1-0.0.3-20250623.104441-42.jar.asc", fileName.replaceBuildNumber("module1", "0.0.3-SNAPSHOT", 42))
28-
}
2922
}

nmcp/src/test/kotlin/MetadataTest.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import java.io.File
2+
import java.time.Instant
23
import kotlin.test.Test
34
import kotlin.test.assertEquals
45
import kotlin.test.assertNull
@@ -8,7 +9,7 @@ import nmcp.internal.task.ArtifactMetadata
89
import nmcp.internal.task.VersionMetadata
910
import nmcp.transport.encodeToXml
1011
import nmcp.internal.task.xml
11-
import nmcp.transport.timestampNow
12+
import nmcp.transport.asTimestamp
1213

1314
class MetadataTest {
1415

@@ -217,7 +218,7 @@ class MetadataTest {
217218
fun timestampTest() {
218219
// Something that looks like 20250618175334
219220
assertTrue(
220-
Regex("""\d{4}\d{2}\d{2}\d{6}""").matches(timestampNow())
221+
Regex("""\d{4}\d{2}\d{2}\d{6}""").matches(Instant.now().asTimestamp(false))
221222
)
222223
}
223224
}

0 commit comments

Comments
 (0)