diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a099fab9a..b81407939 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: build: name: Build, test, and audit runs-on: ubuntu-latest - timeout-minutes: 45 + timeout-minutes: 60 steps: - name: Check out source @@ -30,7 +30,7 @@ jobs: uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: '8' + java-version: '8.0.502+7' - name: Install Java 17 for Gradle uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 @@ -39,15 +39,24 @@ jobs: java-version: '17' - name: Set up Gradle - uses: gradle/actions/setup-gradle@4733eaac7c1b0da527e4206b7671e0061de1ce37 # v6 + uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 - name: Make the wrapper executable run: chmod +x ./gradlew + - name: Stage exact OreSpawn release dependency + id: orespawn + run: | + mirror="$RUNNER_TEMP/orespawn-mirror" + bash gradle/stage-orespawn-release.sh "$mirror" + echo "repository=$mirror" >> "$GITHUB_OUTPUT" + - name: Build, test, and audit release artifacts run: >- ./gradlew clean check build javadoc verifyReleaseDependencies verifyReleaseArtifacts - writeReleaseChecksums --no-daemon --stacktrace + writeReleaseChecksums verifyEclipseProductionClasspath + -PorespawnVerificationRepository=${{ steps.orespawn.outputs.repository }} + --no-daemon --stacktrace - name: Upload audited release candidate uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 @@ -60,15 +69,17 @@ jobs: build/libs/Mineralogy-6.0.1.110021-sources.jar build/libs/Mineralogy-6.0.1.110021-javadoc.jar build/release/SHA256SUMS + CHANGELOG.txt - - name: Upload test reports on failure + - name: Upload diagnostics on failure if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: Mineralogy-1.10.2-test-reports-${{ github.sha }} + name: Mineralogy-1.10.2-diagnostics-${{ github.sha }} if-no-files-found: ignore retention-days: 14 path: | - build/test-results/test/** - build/reports/tests/test/** - build/reports/problems/** + build/test-results/** + build/reports/** + build/*-run/logs/** + build/problems/** diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 000000000..cca770139 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,53 @@ +name: CodeQL + +on: + push: + branches: + - master-1.10.2 + - 'feature/**' + pull_request: + branches: + - master-1.10.2 + +permissions: + actions: read + contents: read + security-events: write + +jobs: + analyze: + name: Analyze Java + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Install Java 8 toolchain + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '8.0.502+7' + + - name: Install Java 17 for Gradle + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: microsoft + java-version: '17' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 + with: + languages: java-kotlin + + - name: Compile production code + run: | + chmod +x ./gradlew + ./gradlew clean classes --rerun-tasks --no-build-cache --no-daemon --stacktrace + + - name: Analyze + uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml index be443ccda..78326a6c9 100644 --- a/.github/workflows/release-on-tag.yml +++ b/.github/workflows/release-on-tag.yml @@ -1,21 +1,21 @@ -name: Start Mineralogy release from tag +name: Validate Mineralogy release tag on: push: tags: - - '1.10.2-*' + - '*.*.*.*' permissions: - actions: write + actions: read contents: read concurrency: - group: mineralogy-release-starter-${{ github.ref_name }} + group: mineralogy-release-tag-${{ github.ref_name }} cancel-in-progress: false jobs: - dispatch-release: - name: Validate tag and start guarded release + validate-release-tag: + name: Validate tag for manual release confirmation if: github.repository == 'MinecraftModDevelopmentMods/MinecraftMineralogy' runs-on: ubuntu-latest timeout-minutes: 10 @@ -26,22 +26,42 @@ jobs: with: fetch-depth: 0 - - name: Validate release tag and prior CI - id: validate + - name: Validate release tag, target metadata, and prior CI env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - release_version="$(sed -n 's/^mod_version=//p' gradle.properties)" - if [[ ! "$release_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.110021$ ]]; then - echo "mod_version must use the four-part 1.10.2 Forge target format; found $release_version" >&2 + value() { sed -n "s/^$1=//p" gradle.properties; } + release_version="$(value mod_version)" + minecraft_version="$(value minecraft_version)" + loader_name="$(value loader_name)" + loader_code="$(value loader_code)" + + IFS=. read -r mc_major mc_minor mc_patch extra <<<"$minecraft_version" + if [[ -n "${extra:-}" || -z "${mc_major:-}" || -z "${mc_minor:-}" ]]; then + echo "Invalid minecraft_version=$minecraft_version" >&2 + exit 1 + fi + mc_patch="${mc_patch:-0}" + if [[ ! "$mc_major" =~ ^[0-9]+$ || ! "$mc_minor" =~ ^[0-9]+$ || ! "$mc_patch" =~ ^[0-9]+$ ]]; then + echo "Invalid minecraft_version=$minecraft_version" >&2 exit 1 fi + case "$loader_name:$loader_code" in + forge:1|neoforge:2) ;; + *) echo "Invalid loader metadata $loader_name/$loader_code" >&2; exit 1 ;; + esac + printf -v minor_padded '%02d' "$((10#$mc_minor))" + printf -v patch_padded '%02d' "$((10#$mc_patch))" + target_suffix="${mc_major}${minor_padded}${patch_padded}${loader_code}" - expected_tag="1.10.2-$release_version" - if [[ "$GITHUB_REF_NAME" != "$expected_tag" ]]; then - echo "Release tag must be $expected_tag; found $GITHUB_REF_NAME" >&2 + if [[ ! "$release_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.${target_suffix}$ ]]; then + echo "mod_version $release_version does not match $minecraft_version $loader_name target $target_suffix" >&2 + exit 1 + fi + if [[ "$GITHUB_REF_NAME" != "$release_version" ]]; then + echo "Release tag must equal mod_version $release_version; found $GITHUB_REF_NAME" >&2 exit 1 fi @@ -49,21 +69,24 @@ jobs: "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/check-runs?per_page=100" \ --jq '[.check_runs[] | select(.name == "Build, test, and audit" and .conclusion == "success")] | length')" if [[ "$successful_ci" -lt 1 ]]; then - echo "The tagged commit has no successful Mineralogy 1.10 CI check" >&2 + echo "The tagged commit has no successful Build, test, and audit check" >&2 exit 1 fi - echo "version=$release_version" >> "$GITHUB_OUTPUT" - - - name: Start default-branch release workflow + - name: Record the manual publication interface env: - GH_TOKEN: ${{ github.token }} - RELEASE_VERSION: ${{ steps.validate.outputs.version }} + RELEASE_WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/workflows/deploy-release.yml run: | - gh workflow run deploy-release.yml \ - --repo "$GITHUB_REPOSITORY" \ - --ref master-1.12 \ - -f mode=publish \ - -f release_ref="$GITHUB_REF_NAME" \ - -f curseforge_channel=release \ - -f confirm_version="$RELEASE_VERSION" + { + echo "## Release tag validated" + echo + echo "Tag \`$GITHUB_REF_NAME\` matches the selected target and has a successful Build, test, and audit check." + echo + echo "If this tag was created outside the release workflow, nothing has been published." + echo + echo "To start or recover publication, open [Release Mineralogy]($RELEASE_WORKFLOW_URL), select **Run workflow**, and enter:" + echo + echo "- release version: \`$GITHUB_REF_NAME\`" + echo "- CurseForge release level: \`release\`, \`beta\`, or \`alpha\`" + echo "- confirm live publication: checked" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/validate-gradle-build.yml b/.github/workflows/validate-gradle-build.yml new file mode 100644 index 000000000..81f7cd4ee --- /dev/null +++ b/.github/workflows/validate-gradle-build.yml @@ -0,0 +1,23 @@ +name: Validate Gradle Wrapper + +on: + push: + branches: + - master-1.10.2 + - 'feature/**' + pull_request: + branches: + - master-1.10.2 + +permissions: + contents: read + +jobs: + validation: + name: Validation + runs-on: ubuntu-latest + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Validate wrapper integrity + uses: gradle/actions/wrapper-validation@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 diff --git a/README.md b/README.md index 860030485..f851e39ee 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +[![Discord](https://img.shields.io/badge/Discord-MMD-green.svg?style=flat&logo=Discord)](https://discord.moddev.zone) +[![CurseForge downloads](https://cf.way2muchnoise.eu/full_minecraft-mineralogy_downloads.svg)](https://www.curseforge.com/minecraft/mc-mods/minecraft-mineralogy) +[![Supported Minecraft versions](https://cf.way2muchnoise.eu/versions/Minecraft_minecraft-mineralogy_all.svg)](https://www.curseforge.com/minecraft/mc-mods/minecraft-mineralogy) +[![Build, test, and audit](https://github.com/MinecraftModDevelopmentMods/MinecraftMineralogy/actions/workflows/ci.yml/badge.svg?branch=master-1.10.2)](https://github.com/MinecraftModDevelopmentMods/MinecraftMineralogy/actions/workflows/ci.yml?query=branch%3Amaster-1.10.2) + # Mineralogy Mineralogy 6 brings real-world rock types, mineral ores, construction blocks, and crude oil to Minecraft 1.10.2. diff --git a/build.gradle b/build.gradle index 923306cf3..572ccf3be 100644 --- a/build.gradle +++ b/build.gradle @@ -40,7 +40,7 @@ if (versionParts[3] != expectedTargetVersion) { ext.functional_version = versionParts[0..2].join('.') ext.display_version = project.mod_version -ext.release_tag = "${project.mc_version}-${project.mod_version}" +ext.release_tag = project.mod_version java { toolchain.languageVersion = JavaLanguageVersion.of(8) @@ -120,6 +120,17 @@ repositories { maven fg.forgeMaven maven fg.minecraftLibsMaven + // Optional exact-coordinate mirror populated by CI from the official + // CurseForge file endpoint, avoiding transient CurseMaven throttling. + if (providers.gradleProperty('orespawnVerificationRepository').isPresent()) { + maven { + name = 'OreSpawnReleaseVerificationMirror' + url = uri(providers.gradleProperty('orespawnVerificationRepository').get()) + metadataSources { artifact() } + content { includeGroup('curse.maven') } + } + } + exclusiveContent { forRepository { maven { @@ -137,10 +148,13 @@ repositories { name = 'MinecraftLibraries' url = 'https://libraries.minecraft.net/' } - maven { - name = 'CurseMaven' - url = 'https://www.cursemaven.com' - content { includeGroup('curse.maven') } + if (!providers.gradleProperty('orespawnVerificationRepository').isPresent()) { + maven { + name = 'CurseMaven' + url = 'https://www.cursemaven.com' + metadataSources { artifact() } + content { includeGroup('curse.maven') } + } } maven { name = 'ModDevZone' @@ -280,9 +294,19 @@ tasks.register('verifyReleaseConfiguration') { if (project.mc_version != '1.10.2' || project.forge_version != '12.18.3.2511') { throw new GradleException('Unexpected Minecraft or Forge target') } + if (project.minecraft_version != project.mc_version + || project.loader_name != 'forge' + || project.loader_code != '1' + || project.java_version != '8' + || project.java_toolchain_version != '8.0.502+7' + || project.gradle_java_version != '17' + || project.curseforge_project_id != project.cf_project_id) { + throw new GradleException('Unexpected generic release dispatcher metadata') + } if (project.orespawn_version != '4.0.6.110021' || project.orespawn_curse_project_id != '245586' - || project.orespawn_curse_file_id != '8681675') { + || project.orespawn_curse_file_id != '8681675' + || project.orespawn_sha256 != '9FD6F2D6C5513A45C2B1CBE62F352A4F804FC7EE3EE30C44A6E5266BBF813E6D') { throw new GradleException('Unexpected released OreSpawn dependency') } if (project.cf_project_id != '240974' @@ -331,6 +355,13 @@ tasks.register('verifyReleaseDependencies') { + oreSpawnArtifacts.collect { it.moduleVersion.id }) } + String actualOreSpawnSha256 = MessageDigest.getInstance('SHA-256') + .digest(oreSpawnArtifacts.first().file.bytes).encodeHex().toString().toUpperCase() + if (actualOreSpawnSha256 != project.orespawn_sha256) { + throw new GradleException("Released OreSpawn SHA-256 is ${actualOreSpawnSha256}, expected " + + project.orespawn_sha256) + } + ZipFile oreSpawn = new ZipFile(oreSpawnArtifacts.first().file) try { def metadataEntry = oreSpawn.getEntry('mcmod.info') diff --git a/gradle.properties b/gradle.properties index 92c957a2f..786ca7e07 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,6 +10,15 @@ mod_group=zone.moddev.mc # Major.Minor.Bug.Target; 110021 means Minecraft 1.10.2 on Forge. mod_version=6.0.1.110021 +# Release metadata consumed by the generic dispatcher. +minecraft_version=1.10.2 +loader_name=forge +loader_code=1 +java_version=8 +java_toolchain_version=8.0.502+7 +gradle_java_version=17 +curseforge_project_id=240974 + #Minumum Minecraft Version Mod will be built with mc_version=1.10.2 #Forge version to use (Required) @@ -53,6 +62,7 @@ dep_has_ats=false orespawn_curse_project_id=245586 orespawn_curse_file_id=8681675 orespawn_version=4.0.6.110021 +orespawn_sha256=9FD6F2D6C5513A45C2B1CBE62F352A4F804FC7EE3EE30C44A6E5266BBF813E6D #File name of this mod's Access Transformers (If any) #at_file = _at.cfg diff --git a/gradle/stage-orespawn-release.sh b/gradle/stage-orespawn-release.sh new file mode 100644 index 000000000..c17c4a37e --- /dev/null +++ b/gradle/stage-orespawn-release.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +repository="${1:?Usage: stage-orespawn-release.sh }" +value() { sed -n "s/^$1=//p" gradle.properties; } + +project_id="$(value orespawn_curse_project_id)" +file_id="$(value orespawn_curse_file_id)" +expected_sha="$(value orespawn_sha256)" +for required in project_id file_id expected_sha; do + if [[ -z "${!required}" ]]; then + echo "Missing OreSpawn release metadata: $required" >&2 + exit 1 + fi +done + +artifact_dir="$repository/curse/maven/mmd-orespawn-$project_id/$file_id" +artifact="$artifact_dir/mmd-orespawn-$project_id-$file_id.jar" +pom="$artifact_dir/mmd-orespawn-$project_id-$file_id.pom" +mkdir -p "$artifact_dir" +curl --fail --location --silent --show-error \ + --retry 5 --retry-all-errors --retry-delay 5 \ + "https://www.curseforge.com/api/v1/mods/$project_id/files/$file_id/download" \ + --output "$artifact" + +actual_sha="$(sha256sum "$artifact" | awk '{print toupper($1)}')" +if [[ "$actual_sha" != "${expected_sha^^}" ]]; then + echo "OreSpawn file $file_id has SHA-256 $actual_sha, expected ${expected_sha^^}" >&2 + exit 1 +fi +printf '%s\n' \ + '' \ + '' \ + ' 4.0.0' \ + " curse.maven" \ + " mmd-orespawn-$project_id" \ + " $file_id" \ + '' > "$pom" +echo "Staged exact OreSpawn file $file_id in $repository" diff --git a/src/test/java/zone/moddev/mc/mineralogy/ContentPolicyTest.java b/src/test/java/zone/moddev/mc/mineralogy/ContentPolicyTest.java index 112e7a06f..f8582c25e 100644 --- a/src/test/java/zone/moddev/mc/mineralogy/ContentPolicyTest.java +++ b/src/test/java/zone/moddev/mc/mineralogy/ContentPolicyTest.java @@ -140,7 +140,7 @@ private static void assertPolicy(ContentPolicy policy, boolean drywalls, private static String mineralogySource() throws Exception { return new String(Files.readAllBytes(new File( "src/main/java/zone/moddev/mc/mineralogy/Mineralogy.java").toPath()), - StandardCharsets.UTF_8); + StandardCharsets.UTF_8).replace("\r\n", "\n").replace('\r', '\n'); } private static int countOccurrences(String value, String search) { diff --git a/src/test/java/zone/moddev/mc/mineralogy/OreDictionaryPolicyTest.java b/src/test/java/zone/moddev/mc/mineralogy/OreDictionaryPolicyTest.java index caa3a43ef..3f13ef307 100644 --- a/src/test/java/zone/moddev/mc/mineralogy/OreDictionaryPolicyTest.java +++ b/src/test/java/zone/moddev/mc/mineralogy/OreDictionaryPolicyTest.java @@ -92,7 +92,7 @@ public void newCompatibilityKeyKeepsTheExistingConfigNonRewriteGuard() throws Ex private static String mineralogySource() throws Exception { return new String(Files.readAllBytes(new File( "src/main/java/zone/moddev/mc/mineralogy/Mineralogy.java").toPath()), - StandardCharsets.UTF_8); + StandardCharsets.UTF_8).replace("\r\n", "\n").replace('\r', '\n'); } private static int countOccurrences(String value, String search) { diff --git a/src/test/java/zone/moddev/mc/mineralogy/RecipeContractTest.java b/src/test/java/zone/moddev/mc/mineralogy/RecipeContractTest.java index adb580c72..dd763ae20 100644 --- a/src/test/java/zone/moddev/mc/mineralogy/RecipeContractTest.java +++ b/src/test/java/zone/moddev/mc/mineralogy/RecipeContractTest.java @@ -350,7 +350,7 @@ public boolean canInteractWith(EntityPlayer playerIn) { private static String mineralogySource() throws Exception { return new String(Files.readAllBytes(new File( "src/main/java/zone/moddev/mc/mineralogy/Mineralogy.java").toPath()), - StandardCharsets.UTF_8); + StandardCharsets.UTF_8).replace("\r\n", "\n").replace('\r', '\n'); } private static int countOccurrences(String value, String search) { diff --git a/src/test/java/zone/moddev/mc/mineralogy/VersionContractTest.java b/src/test/java/zone/moddev/mc/mineralogy/VersionContractTest.java index 285db8dd0..007b3f746 100644 --- a/src/test/java/zone/moddev/mc/mineralogy/VersionContractTest.java +++ b/src/test/java/zone/moddev/mc/mineralogy/VersionContractTest.java @@ -24,6 +24,13 @@ public void minecraft1102ForgeUsesTheDeterministicFourComponentVersion() throws String minecraftVersion = properties.getProperty("mc_version"); String modVersion = properties.getProperty("mod_version"); assertEquals("1.10.2", minecraftVersion); + assertEquals(minecraftVersion, properties.getProperty("minecraft_version")); + assertEquals("forge", properties.getProperty("loader_name")); + assertEquals("1", properties.getProperty("loader_code")); + assertEquals("8", properties.getProperty("java_version")); + assertEquals("8.0.502+7", properties.getProperty("java_toolchain_version")); + assertEquals("17", properties.getProperty("gradle_java_version")); + assertEquals("240974", properties.getProperty("curseforge_project_id")); assertEquals("110021", targetFor(minecraftVersion, 1)); assertEquals("6.0.1.110021", modVersion); assertEquals(modVersion, Mineralogy.VERSION); @@ -38,7 +45,8 @@ public void gradlePublishesTheQualifiedVersionWithoutCiSuffixes() throws Excepti assertFalse(build.contains("Mineralogy-${project.mc_version}")); assertTrue(build.contains("artifact(releaseJar)")); assertTrue(build.contains("versionParts[3] != expectedTargetVersion")); - assertTrue(build.contains("ext.release_tag = \"${project.mc_version}-${project.mod_version}\"")); + assertTrue(build.contains("ext.release_tag = project.mod_version")); + assertFalse(build.contains("${project.mc_version}-${project.mod_version}")); assertFalse(build.contains("BUILD_NUMBER")); assertFalse(build.contains("TRAVIS_BUILD_NUMBER")); assertFalse(build.contains("CIRCLE_BUILD_NUM")); @@ -69,6 +77,8 @@ public void modernBuildAndReleaseMetadataKeepTheRequiredOreSpawnContract() throw assertEquals("245586", properties.getProperty("orespawn_curse_project_id")); assertEquals("8681675", properties.getProperty("orespawn_curse_file_id")); assertEquals("4.0.6.110021", properties.getProperty("orespawn_version")); + assertEquals("9FD6F2D6C5513A45C2B1CBE62F352A4F804FC7EE3EE30C44A6E5266BBF813E6D", + properties.getProperty("orespawn_sha256")); assertFalse(properties.containsKey("create_api_jar")); assertFalse(properties.containsKey("create_deobf_jar")); diff --git a/src/test/java/zone/moddev/mc/mineralogy/WorkflowContractTest.java b/src/test/java/zone/moddev/mc/mineralogy/WorkflowContractTest.java new file mode 100644 index 000000000..f56aec084 --- /dev/null +++ b/src/test/java/zone/moddev/mc/mineralogy/WorkflowContractTest.java @@ -0,0 +1,50 @@ +package zone.moddev.mc.mineralogy; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import org.junit.Test; + +public class WorkflowContractTest { + @Test + public void branchCiUsesTheAuditedBuildAndModernPinnedChecks() throws Exception { + String ci = read(".github/workflows/ci.yml"); + String codeql = read(".github/workflows/codeql-analysis.yml"); + String wrapper = read(".github/workflows/validate-gradle-build.yml"); + String staging = read("gradle/stage-orespawn-release.sh"); + + assertTrue(ci.contains("master-1.10.2")); + assertTrue(ci.contains("name: Build, test, and audit")); + assertTrue(ci.contains("clean check build javadoc verifyReleaseDependencies verifyReleaseArtifacts")); + assertTrue(ci.contains("verifyEclipseProductionClasspath")); + assertTrue(ci.contains("CHANGELOG.txt")); + assertTrue(ci.contains("-PorespawnVerificationRepository=${{ steps.orespawn.outputs.repository }}")); + assertTrue(staging.contains("https://www.curseforge.com/api/v1/mods/$project_id/files/$file_id/download")); + assertTrue(staging.contains("sha256sum")); + assertTrue(codeql.contains("github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28")); + assertTrue(codeql.contains("./gradlew clean classes")); + assertTrue(codeql.contains("--rerun-tasks --no-build-cache")); + assertTrue(wrapper.contains("gradle/actions/wrapper-validation@9c971963bec38e04b3d30dcc455b5382be2fdbfb")); + } + + @Test + public void tagValidatorUsesVersionMetadataAndNeverDispatchesPublication() throws Exception { + String tag = read(".github/workflows/release-on-tag.yml"); + assertTrue(tag.contains("'*.*.*.*'")); + assertTrue(tag.contains("loader_name:$loader_code")); + assertTrue(tag.contains("Build, test, and audit")); + assertTrue(tag.contains("Release tag must equal mod_version")); + assertTrue(tag.contains("confirm live publication: checked")); + assertFalse(tag.contains("gh workflow run")); + assertFalse(tag.contains("actions: write")); + } + + private static String read(String path) throws Exception { + return new String(Files.readAllBytes(new File(path).toPath()), StandardCharsets.UTF_8) + .replace("\r\n", "\n").replace('\r', '\n'); + } +}