diff --git a/.github/scripts/retry.sh b/.github/scripts/retry.sh new file mode 100755 index 0000000..8cb5452 --- /dev/null +++ b/.github/scripts/retry.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Runs a command and retries it on failure, to ride out transient network +# flakes (interrupted maven downloads, brief mirror outages, and the like). +# +# retry.sh [args...] +# +# RETRY_ATTEMPTS (default 3) sets the total number of attempts; the pause +# between attempts grows with each failure. +set -uo pipefail + +attempts="${RETRY_ATTEMPTS:-3}" +code=0 +for ((i = 1; i <= attempts; i++)); do + "$@" + code=$? + [ "$code" -eq 0 ] && exit 0 + if ((i < attempts)); then + delay=$((i * 15)) + echo "::warning::Attempt ${i}/${attempts} of '$*' failed with exit code ${code}; retrying in ${delay}s" + sleep "$delay" + fi +done +echo "::error::All ${attempts} attempts of '$*' failed" +exit "$code" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 20256d5..8a626a6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,11 +55,17 @@ jobs: java-version: ${{ matrix.java }} distribution: 'microsoft' + - name: setup gradle with dependency caching + # Caches Gradle dependencies between runs (written on the default + # branch, read everywhere), so a flaky maven rarely has to be hit. + uses: gradle/actions/setup-gradle@v6 + - name: make gradle wrapper executable run: chmod +x ./gradlew - name: build :${{ matrix.mc }}:build - run: ./gradlew :${{ matrix.mc }}:build --stacktrace + # retry.sh reruns the build on transient failures (flaky maven downloads). + run: bash .github/scripts/retry.sh ./gradlew :${{ matrix.mc }}:build --stacktrace - name: upload jar uses: actions/upload-artifact@v7 @@ -97,6 +103,9 @@ jobs: java-version: ${{ matrix.java }} distribution: 'microsoft' + - name: setup gradle with dependency caching + uses: gradle/actions/setup-gradle@v6 + - name: make gradle wrapper executable run: chmod +x ./gradlew @@ -109,7 +118,11 @@ jobs: run: bash .github/scripts/generate-test-world.sh ${{ matrix.mc }} - name: launch minecraft ${{ matrix.mc }} with mod - run: ./gradlew :launchtest:runProductionClientGameTest -PtestMcVersion=${{ matrix.mc }} --stacktrace + # Two attempts (not the default three): a real game crash fails both, + # and a second full launch still fits the 40-minute job timeout. + env: + RETRY_ATTEMPTS: '2' + run: bash .github/scripts/retry.sh ./gradlew :launchtest:runProductionClientGameTest -PtestMcVersion=${{ matrix.mc }} --stacktrace - name: check survival world screenshot was taken run: test -n "$(find launchtest/run/screenshots -name '*betterhud-survival-world.png' -print -quit 2>/dev/null)" @@ -228,11 +241,14 @@ jobs: 25 distribution: 'microsoft' + - name: setup gradle with dependency caching + uses: gradle/actions/setup-gradle@v6 + - name: make gradle wrapper executable run: chmod +x ./gradlew - name: build all variants and write the upload guide - run: ./gradlew modrinthBundle --stacktrace "-Porg.gradle.java.installations.paths=$JAVA_HOME_21_X64,$JAVA_HOME_25_X64" + run: bash .github/scripts/retry.sh ./gradlew modrinthBundle --stacktrace "-Porg.gradle.java.installations.paths=$JAVA_HOME_21_X64,$JAVA_HOME_25_X64" - name: upload modrinth bundle uses: actions/upload-artifact@v7 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9ab1350..90a291f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -113,11 +113,16 @@ jobs: java-version: ${{ matrix.java }} distribution: 'microsoft' + - name: setup gradle with dependency caching + # Caches Gradle dependencies between runs; combined with retry.sh below + # this keeps transient maven download failures from killing a release. + uses: gradle/actions/setup-gradle@v6 + - name: make gradle wrapper executable run: chmod +x ./gradlew - name: build :${{ matrix.mc }}:build - run: ./gradlew :${{ matrix.mc }}:build --stacktrace + run: bash .github/scripts/retry.sh ./gradlew :${{ matrix.mc }}:build --stacktrace - name: publish to modrinth and curseforge uses: Kir-Antipov/mc-publish@v3.3 diff --git a/build.gradle b/build.gradle index 9972fe4..6ea0f99 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,10 @@ repositories { maven { url 'https://maven.terraformersmc.com/releases/' } } +// Adds the Modrinth maven and modmenuDependency(), a fallback for when the +// TerraformersMC maven is unreachable. +apply from: "${rootDir}/gradle/modrinth-fallback.gradle" + // MC 26+ uses fabric-rendering-v1's transitive deps but rejects ASM coming from // annotation processors. if (!isLegacy) { @@ -52,14 +56,14 @@ dependencies { modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" - modImplementation "com.terraformersmc:modmenu:${project.modmenu_version}" + modImplementation modmenuDependency(project.modmenu_version) modImplementation("me.shedaniel.cloth:cloth-config-fabric:${project.cloth_config_version}") { exclude(group: 'net.fabricmc.fabric-api') } } else { implementation "net.fabricmc:fabric-loader:${project.loader_version}" implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" - implementation "com.terraformersmc:modmenu:${project.modmenu_version}" + implementation modmenuDependency(project.modmenu_version) implementation("me.shedaniel.cloth:cloth-config-fabric:${project.cloth_config_version}") { exclude(group: 'net.fabricmc.fabric-api') } diff --git a/gradle/modrinth-fallback.gradle b/gradle/modrinth-fallback.gradle new file mode 100644 index 0000000..987dcd0 --- /dev/null +++ b/gradle/modrinth-fallback.gradle @@ -0,0 +1,41 @@ +// Fallback for the TerraformersMC maven, which intermittently fails downloads +// in CI ("Premature end of chunk coded message body"). The Modrinth maven +// serves the exact same Mod Menu release jars under maven.modrinth coordinates +// with identical version strings, so when the TerraformersMC coordinate cannot +// be resolved the build switches to the Modrinth mirror instead of failing. +// +// Apply after the project's own repositories block, then declare Mod Menu via +// modmenuDependency(""). + +repositories { + exclusiveContent { + forRepository { + maven { + name = 'Modrinth' + url = 'https://api.modrinth.com/maven' + } + } + // Only maven.modrinth artifacts come from here, and nowhere else — the + // other repositories are never queried for the fallback coordinates. + filter { + includeGroup 'maven.modrinth' + } + } +} + +ext.modmenuDependency = { String version -> + def primary = "com.terraformersmc:modmenu:${version}" + try { + // Probe the primary coordinate with a detached configuration. Release + // versions are cached forever, so once downloaded (or restored from the + // CI Gradle cache) this touches no network at all. + def probe = configurations.detachedConfiguration(dependencies.create(primary)) + probe.transitive = false + probe.resolve() + return primary + } catch (Exception e) { + logger.warn("Could not resolve ${primary} from the TerraformersMC maven " + + "(${e.message}); falling back to maven.modrinth:modmenu:${version}") + return "maven.modrinth:modmenu:${version}" + } +} diff --git a/launchtest/build.gradle b/launchtest/build.gradle index 49d52e8..8a33a52 100644 --- a/launchtest/build.gradle +++ b/launchtest/build.gradle @@ -68,6 +68,10 @@ repositories { maven { url 'https://maven.terraformersmc.com/releases/' } } +// Adds the Modrinth maven and modmenuDependency(), a fallback for when the +// TerraformersMC maven is unreachable. +apply from: "${rootDir}/gradle/modrinth-fallback.gradle" + // Matches the variant builds: MC 26+ rejects ASM coming from annotation processors. if (!isLegacy) { configurations.configureEach { @@ -107,7 +111,7 @@ dependencies { // mavens; transitive deps are excluded because each fat jar already nests // everything it needs via jar-in-jar. productionRuntimeMods("net.fabricmc.fabric-api:fabric-api:${entry.fabricApi}") { transitive = false } - productionRuntimeMods("com.terraformersmc:modmenu:${entry.modmenu}") { transitive = false } + productionRuntimeMods(modmenuDependency(entry.modmenu)) { transitive = false } productionRuntimeMods("me.shedaniel.cloth:cloth-config-fabric:${entry.cloth}") { transitive = false } if (entry.clientGametest) {