From 447ec3156e526b7901f0e2c70998b2420d9a293e Mon Sep 17 00:00:00 2001 From: Chris Lee Date: Tue, 12 May 2026 09:20:18 -0700 Subject: [PATCH 1/2] feat: expand INFO-level runtime logging for diagnostics Adds startup banner, per-artifact Discovered/Completed lines, per-version In sync / Synced events, and a Sync complete terminator. Demotes per-asset upload logs and crawler index reads to DEBUG so the INFO stream stays scannable. Volume now scales with artifacts + synced versions rather than artifacts x assets x versions. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../mavensync/MavenHttpClient.kt | 4 +- .../mavensync/MavenRepositoryCrawler.kt | 2 +- .../mavensync/MavenSyncEngine.kt | 39 +++++- .../cloudshiftdev/mavensync/MavenSyncMain.kt | 7 +- .../io/cloudshiftdev/mavensync/SyncMetrics.kt | 2 +- .../mavensync/MavenSyncIntegrationTest.kt | 115 +++++++++--------- 6 files changed, 101 insertions(+), 68 deletions(-) diff --git a/src/main/kotlin/io/cloudshiftdev/mavensync/MavenHttpClient.kt b/src/main/kotlin/io/cloudshiftdev/mavensync/MavenHttpClient.kt index df6bd5b..85657eb 100644 --- a/src/main/kotlin/io/cloudshiftdev/mavensync/MavenHttpClient.kt +++ b/src/main/kotlin/io/cloudshiftdev/mavensync/MavenHttpClient.kt @@ -37,13 +37,13 @@ internal class MavenHttpClient( setBody(LocalFileContent(file)) } val size = file.length() - logger.info { "Uploaded $url: status=${resp.status} size=$size" } + logger.debug { "Uploaded $url: status=${resp.status} size=$size" } return size } internal suspend fun upload(url: Url, content: String) { val resp = httpClient.put(url) { setBody(content) } - logger.info { "Uploaded $url: ${resp.status}" } + logger.debug { "Uploaded $url: ${resp.status}" } } internal suspend fun parseDirectoryListing(url: Url): List { diff --git a/src/main/kotlin/io/cloudshiftdev/mavensync/MavenRepositoryCrawler.kt b/src/main/kotlin/io/cloudshiftdev/mavensync/MavenRepositoryCrawler.kt index a8badae..a512af8 100644 --- a/src/main/kotlin/io/cloudshiftdev/mavensync/MavenRepositoryCrawler.kt +++ b/src/main/kotlin/io/cloudshiftdev/mavensync/MavenRepositoryCrawler.kt @@ -33,7 +33,7 @@ internal class MavenRepositoryCrawler( url: Url, crawlDelay: Duration, ) { - logger.info { "Reading index: $url" } + logger.debug { "Reading index: $url" } val childLinks = httpClient.parseDirectoryListing(url) logger.debug { "Found ${childLinks.size} links in $url" } diff --git a/src/main/kotlin/io/cloudshiftdev/mavensync/MavenSyncEngine.kt b/src/main/kotlin/io/cloudshiftdev/mavensync/MavenSyncEngine.kt index 3bc262e..bdc13b5 100644 --- a/src/main/kotlin/io/cloudshiftdev/mavensync/MavenSyncEngine.kt +++ b/src/main/kotlin/io/cloudshiftdev/mavensync/MavenSyncEngine.kt @@ -39,6 +39,13 @@ internal class MavenSyncEngine( metrics.recordInSync(metadata.group, metadata.artifact, inSyncVersions) val missingVersions = sourceVersions - targetVersions + logger.info { + "Discovered ${metadata.group}:${metadata.artifact} — ${sourceVersions.size} versions" + + " in source, ${inSyncVersions.size} in sync, ${missingVersions.size} to sync" + } + inSyncVersions.forEach { version -> + logger.info { "In sync ${metadata.group}:${metadata.artifact}:${version.value}" } + } if (missingVersions.isEmpty()) { logger.debug { "No missing versions for ${metadata.group}:${metadata.artifact}" } return @@ -46,12 +53,24 @@ internal class MavenSyncEngine( logger.info { "Syncing missing versions for ${metadata.group}:${metadata.artifact}: $missingVersions" } + var synced = 0 + var failed = 0 missingVersions .map { Coordinates(metadata.group, metadata.artifact, it) } - .forEach { coordinates -> syncVersion(coordinates) } + .forEach { coordinates -> + when (syncVersion(coordinates)) { + VersionOutcome.Synced -> synced++ + VersionOutcome.Failed -> failed++ + VersionOutcome.NoAssets -> Unit + } + } + logger.info { + "Completed ${metadata.group}:${metadata.artifact} —" + + " inSync=${inSyncVersions.size} synced=$synced failed=$failed" + } } - private suspend fun syncVersion(coordinates: Coordinates) { + private suspend fun syncVersion(coordinates: Coordinates): VersionOutcome { val mark = TimeSource.Monotonic.markNow() try { val assets = @@ -61,25 +80,37 @@ internal class MavenSyncEngine( options.transferSignatures, ) - if (assets.isEmpty()) return + if (assets.isEmpty()) return VersionOutcome.NoAssets var bytes = 0L assets.forEach { asset -> bytes += source.copyAsset(asset, target) } target.releaseVersion(coordinates) + val duration = mark.elapsedNow() metrics.recordSynced( coordinates = coordinates, assetCount = assets.size, bytes = bytes, - duration = mark.elapsedNow(), + duration = duration, ) + logger.info { + "Synced $coordinates — ${assets.size} assets, ${formatBytes(bytes)} in $duration" + } delay(options.downloadDelay) + return VersionOutcome.Synced } catch (e: CancellationException) { throw e } catch (e: Exception) { val msg = e.message ?: e.toString() logger.error(e) { "Failed to sync $coordinates: $msg" } metrics.recordFailure(coordinates, msg, mark.elapsedNow()) + return VersionOutcome.Failed } } + + private enum class VersionOutcome { + Synced, + Failed, + NoAssets, + } } diff --git a/src/main/kotlin/io/cloudshiftdev/mavensync/MavenSyncMain.kt b/src/main/kotlin/io/cloudshiftdev/mavensync/MavenSyncMain.kt index 4fd569b..cd466c1 100644 --- a/src/main/kotlin/io/cloudshiftdev/mavensync/MavenSyncMain.kt +++ b/src/main/kotlin/io/cloudshiftdev/mavensync/MavenSyncMain.kt @@ -25,7 +25,11 @@ public suspend fun main(args: Array) { val config = loadConfiguration(args) - logger.info { "Effective configuration: $config" } + logger.info { + "Starting maven-sync: source=${config.source.url} target=${config.target.url} " + + "paths=${config.source.paths} concurrency=${config.artifactConcurrency}" + } + logger.debug { "Effective configuration: $config" } val metrics = SyncMetrics() try { @@ -34,6 +38,7 @@ public suspend fun main(args: Array) { MavenSyncEngine(source, target, config.toSyncOptions(), metrics).sync() } } + logger.info { "Sync complete" } } finally { val report = metrics.snapshot() logger.info { "\n" + report.renderDetailed() } diff --git a/src/main/kotlin/io/cloudshiftdev/mavensync/SyncMetrics.kt b/src/main/kotlin/io/cloudshiftdev/mavensync/SyncMetrics.kt index b3c110b..89c18b6 100644 --- a/src/main/kotlin/io/cloudshiftdev/mavensync/SyncMetrics.kt +++ b/src/main/kotlin/io/cloudshiftdev/mavensync/SyncMetrics.kt @@ -141,7 +141,7 @@ internal data class SyncReport(val totalDuration: Duration, val artifacts: List< } } -private fun formatBytes(bytes: Long): String { +internal fun formatBytes(bytes: Long): String { if (bytes < 1024) return "$bytes B" val units = listOf("KB", "MB", "GB", "TB") var value = bytes.toDouble() / 1024.0 diff --git a/src/test/kotlin/io/cloudshiftdev/mavensync/MavenSyncIntegrationTest.kt b/src/test/kotlin/io/cloudshiftdev/mavensync/MavenSyncIntegrationTest.kt index 2ea4fa5..a1bc62b 100644 --- a/src/test/kotlin/io/cloudshiftdev/mavensync/MavenSyncIntegrationTest.kt +++ b/src/test/kotlin/io/cloudshiftdev/mavensync/MavenSyncIntegrationTest.kt @@ -18,9 +18,9 @@ import java.util.TreeMap /** * Source-side integration test. Drives a real `MavenHttpRepository` against a user-supplied repo - * URL (and optional credentials) and runs everything the sync engine does *up to but excluding* - * the upload to the target: crawl, parse, target-diff (against an empty fake), and per-version - * asset listing. + * URL (and optional credentials) and runs everything the sync engine does *up to but excluding* the + * upload to the target: crawl, parse, target-diff (against an empty fake), and per-version asset + * listing. * * Gated on the `MAVEN_SYNC_IT_CONFIG` env var (path to a JSON file using the standard [SyncConfig] * schema). When unset, the test is reported as disabled and no network calls are made. @@ -33,40 +33,40 @@ class MavenSyncIntegrationTest : val configPath = System.getenv("MAVEN_SYNC_IT_CONFIG") val enabled = !configPath.isNullOrBlank() - test("discovers artifacts, versions, and assets from a real source repo") - .config(enabled = enabled) { - val config = loadIntegrationConfig(File(configPath!!)) - val options = config.toSyncOptions() - - config.source.toMavenHttpRepository().use { source -> - val target = FakeMavenHttpRepository("integration-test-target") - val report = DiscoveryReport() - - source.crawl(options.paths, options.crawlDelay).collect { metadata -> - val targetMetadata = - target.queryArtifactMetadata(metadata.group, metadata.artifact) - val missingVersions = - metadata.artifactVersions.toSet() - - targetMetadata.artifactVersions.toSet() - missingVersions.forEach { version -> - val coordinates = Coordinates(metadata.group, metadata.artifact, version) - val assets = - source.listArtifactVersionAssets( - coordinates, - options.transferChecksums, - options.transferSignatures, - ) - report.add(coordinates, assets) - } + test("discovers artifacts, versions, and assets from a real source repo").config( + enabled = enabled + ) { + val config = loadIntegrationConfig(File(configPath!!)) + val options = config.toSyncOptions() + + config.source.toMavenHttpRepository().use { source -> + val target = FakeMavenHttpRepository("integration-test-target") + val report = DiscoveryReport() + + source.crawl(options.paths, options.crawlDelay).collect { metadata -> + val targetMetadata = + target.queryArtifactMetadata(metadata.group, metadata.artifact) + val missingVersions = + metadata.artifactVersions.toSet() - targetMetadata.artifactVersions.toSet() + missingVersions.forEach { version -> + val coordinates = Coordinates(metadata.group, metadata.artifact, version) + val assets = + source.listArtifactVersionAssets( + coordinates, + options.transferChecksums, + options.transferSignatures, + ) + report.add(coordinates, assets) } + } - target.copyCalls.shouldBeEmpty() - target.uploadCalls.shouldBeEmpty() - target.releaseCalls.shouldBeEmpty() + target.copyCalls.shouldBeEmpty() + target.uploadCalls.shouldBeEmpty() + target.releaseCalls.shouldBeEmpty() - val reportPath = writeReport(report) - println( - """ + val reportPath = writeReport(report) + println( + """ |Maven-sync integration test discovery report: | source: ${config.source.url} | paths: ${options.paths} @@ -75,37 +75,35 @@ class MavenSyncIntegrationTest : | assets: ${report.assetCount} | report: $reportPath """ - .trimMargin() - ) + .trimMargin() + ) - withClue( - "No artifacts discovered — check source URL, credentials, and paths." - ) { - report.artifacts.keys.shouldNotBeEmpty() + withClue("No artifacts discovered — check source URL, credentials, and paths.") { + report.artifacts.keys.shouldNotBeEmpty() + } + report.artifacts.forEach { (artifactKey, versions) -> + withClue("Artifact $artifactKey has no versions") { + versions.keys.shouldNotBeEmpty() } - report.artifacts.forEach { (artifactKey, versions) -> - withClue("Artifact $artifactKey has no versions") { - versions.keys.shouldNotBeEmpty() - } - versions.forEach { (version, assets) -> - withClue("Artifact $artifactKey version $version has no assets") { - assets.shouldNotBeEmpty() - } + versions.forEach { (version, assets) -> + withClue("Artifact $artifactKey version $version has no assets") { + assets.shouldNotBeEmpty() } } + } - val expectedPath = System.getenv("MAVEN_SYNC_IT_EXPECTED") - if (!expectedPath.isNullOrBlank()) { - val expected = File(expectedPath).readText().trim() - withClue( - "Discovery report differs from snapshot at $expectedPath " + - "(live report: $reportPath)" - ) { - report.toJson().trim() shouldBe expected - } + val expectedPath = System.getenv("MAVEN_SYNC_IT_EXPECTED") + if (!expectedPath.isNullOrBlank()) { + val expected = File(expectedPath).readText().trim() + withClue( + "Discovery report differs from snapshot at $expectedPath " + + "(live report: $reportPath)" + ) { + report.toJson().trim() shouldBe expected } } } + } }) private fun loadIntegrationConfig(file: File): SyncConfig = @@ -179,8 +177,7 @@ private class DiscoveryReport { '\n' -> sb.append("\\n") '\r' -> sb.append("\\r") '\t' -> sb.append("\\t") - else -> - if (c.code < 0x20) sb.append("\\u%04x".format(c.code)) else sb.append(c) + else -> if (c.code < 0x20) sb.append("\\u%04x".format(c.code)) else sb.append(c) } } sb.append("\"") From 6e512f8f4bf761df4dc1d0c8f61fa3ff4d7ea6c9 Mon Sep 17 00:00:00 2001 From: Chris Lee Date: Tue, 12 May 2026 09:25:10 -0700 Subject: [PATCH 2/2] build: drop removed SonatypeHost reference from mavenPublishing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vanniktech-publish 0.36.0 (pulled in via dependabot #43) removed SonatypeHost — central.sonatype.com is the only target now. Switch to the named-argument form so the build configures again. Co-Authored-By: Claude Opus 4.7 (1M context) --- build.gradle.kts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index d15aef0..1adc636 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,5 @@ import com.ncorti.ktfmt.gradle.tasks.KtfmtCheckTask import com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask -import com.vanniktech.maven.publish.SonatypeHost import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestLogEvent @@ -57,7 +56,7 @@ kotlin { } mavenPublishing { - publishToMavenCentral(SonatypeHost.S01, true) + publishToMavenCentral(automaticRelease = true) signAllPublications() pom {