diff --git a/gradle/release.gradle b/gradle/release.gradle index 7335d72..fb0b114 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -4,27 +4,56 @@ import java.security.MessageDigest // Publish only a checksum-verified bundle built by the release workflow. def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') def releaseEnabled = providers.gradleProperty('release_enabled').getOrElse('false') == 'true' +def verifyPreparedBundle = { File prepared -> + List filenames = releaseFiles.get().collect { it.toString() } + Set expected = (filenames + ['SHA256SUMS', 'CHANGELOG.md']) as Set + Set actual = prepared.listFiles()?.findAll { it.isFile() }*.name as Set + if (actual != expected) throw new GradleException('The prepared bundle has missing or unexpected files') + def lines = new File(prepared, 'SHA256SUMS').readLines('UTF-8') + if (lines.size() != 3) throw new GradleException('The prepared bundle needs exactly three checksums') + Set checked = [] as Set + lines.each { line -> + def match = line =~ /^([0-9A-Fa-f]{64}) ([A-Za-z0-9._-]+)$/ + if (!match.matches() || !filenames.contains(match[0][2]) || !checked.add(match[0][2])) + throw new GradleException('Invalid or repeated prepared checksum') + String hash = MessageDigest.getInstance('SHA-256').digest( + Files.readAllBytes(new File(prepared, match[0][2]).toPath())).encodeHex().toString() + if (!hash.equalsIgnoreCase(match[0][1])) throw new GradleException('Prepared artifact checksum mismatch') + } +} tasks.register('verifyPreparedReleaseArtifacts') { group = 'verification' doLast { if (!preparedReleaseDir.isPresent()) throw new GradleException('preparedReleaseDir is required') - File prepared = file(preparedReleaseDir.get()) - Set expected = (releaseFiles.get() + ['SHA256SUMS', 'CHANGELOG.md']) as Set - Set actual = prepared.listFiles()?.findAll { it.isFile() }*.name as Set - if (actual != expected) throw new GradleException('The prepared bundle has missing or unexpected files') - def lines = new File(prepared, 'SHA256SUMS').readLines('UTF-8') - if (lines.size() != 3) throw new GradleException('The prepared bundle needs exactly three checksums') - Set checked = [] as Set - lines.each { line -> - def match = line =~ /^([0-9A-Fa-f]{64}) ([A-Za-z0-9._-]+)$/ - if (!match.matches() || !releaseFiles.get().contains(match[0][2]) || !checked.add(match[0][2])) - throw new GradleException('Invalid or repeated prepared checksum') - String hash = MessageDigest.getInstance('SHA-256').digest( - Files.readAllBytes(new File(prepared, match[0][2]).toPath())).encodeHex().toString() - if (!hash.equalsIgnoreCase(match[0][1])) throw new GradleException('Prepared artifact checksum mismatch') + verifyPreparedBundle(file(preparedReleaseDir.get())) + } +} +tasks.register('verifyReleaseBundleContract') { + group = 'verification' + dependsOn tasks.named('writeReleaseChecksums') + doLast { + File bundle = layout.buildDirectory.dir('release-bundle-test').get().asFile + List filenames = releaseFiles.get().collect { it.toString() } + copy { + from layout.buildDirectory.dir('libs') + include filenames + into bundle + } + copy { from 'CHANGELOG.md', layout.buildDirectory.file('release/SHA256SUMS'); into bundle } + verifyPreparedBundle(bundle) + // The same validator must reject a changed jar, even if its name is right. + copy { from new File(bundle, filenames[1]); into bundle; rename { filenames[0] } } + boolean rejected = false + try { + verifyPreparedBundle(bundle) + } catch (GradleException expected) { + if (!expected.message.contains('Prepared artifact checksum mismatch')) throw expected + rejected = true } + if (!rejected) throw new GradleException('Changed release bundle was accepted') } } +tasks.named('check') { dependsOn 'verifyReleaseBundleContract' } def mavenUploadUrl = providers.environmentVariable('MAVEN_UPLOAD_URL') publishing { publications { diff --git a/src/test/java/zone/moddev/mc/skysbuildingpieces/PublicationContractTest.java b/src/test/java/zone/moddev/mc/skysbuildingpieces/PublicationContractTest.java index 9771b86..8a636e5 100644 --- a/src/test/java/zone/moddev/mc/skysbuildingpieces/PublicationContractTest.java +++ b/src/test/java/zone/moddev/mc/skysbuildingpieces/PublicationContractTest.java @@ -9,8 +9,13 @@ class PublicationContractTest { @Test void publicationNeedsThisModsMetadataAndExplicitApproval() throws Exception { String properties = text("gradle.properties"); - assertTrue(properties.contains("release_enabled=false")); - assertTrue(properties.contains("curseforge_project_id=\n")); + java.util.Properties metadata = new java.util.Properties(); + metadata.load(new java.io.StringReader(properties)); + String projectId = metadata.getProperty("curseforge_project_id", ""); + assertTrue(metadata.getProperty("release_enabled").matches("true|false")); + if (!projectId.matches("[1-9][0-9]*")) + assertEquals("false", metadata.getProperty("release_enabled")); + assertNotEquals("1677588", projectId); assertTrue(properties.contains("release_repository=MinecraftModDevelopmentMods/SkysBuildingPieces")); String workflow = text(".github/workflows/deploy-release.yml"); assertTrue(workflow.contains("confirm_live_publication")); @@ -30,6 +35,8 @@ class PublicationContractTest { assertTrue(release.contains("lines.size() != 3")); assertTrue(release.contains("!checked.add")); assertTrue(release.contains("verifyPreparedReleaseArtifacts")); + assertTrue(release.contains("collect { it.toString() }")); + assertTrue(release.contains("verifyReleaseBundleContract")); String ci = text(".github/workflows/ci.yml"); assertTrue(ci.contains(" push:")); assertTrue(ci.contains(" pull_request:"));