diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c75e8c..d1ad4f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,3 +59,17 @@ jobs: -Dorg.gradle.java.installations.auto-detect=false \ -Dorg.gradle.java.installations.auto-download=false sha256sum build/libs/*.jar + + - name: Stage the audited release bundle + run: | + mkdir -p build/release-bundle + cp build/libs/*.jar build/release/SHA256SUMS CHANGELOG.txt build/release-bundle/ + (cd build/release-bundle && sha256sum --check SHA256SUMS) + + - name: Upload the audited release bundle + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: BaseGems-1.12.2-${{ github.sha }} + path: build/release-bundle/* + if-no-files-found: error + retention-days: 90 diff --git a/.github/workflows/deploy-release-112.yml b/.github/workflows/deploy-release-112.yml new file mode 100644 index 0000000..9ba96ee --- /dev/null +++ b/.github/workflows/deploy-release-112.yml @@ -0,0 +1,245 @@ +name: Release Base Gems 1.12.2 +run-name: Base Gems ${{ inputs.release_version }} (${{ inputs.confirm_live_publication && 'publish' || 'verify' }}) + +on: + workflow_dispatch: + inputs: + release_version: + description: Base Gems version for Minecraft 1.12.2 + required: true + type: string + tested_jar_sha256: + description: SHA-256 of the game jar that passed manual testing + required: true + type: string + curseforge_release_level: + description: CurseForge release level + required: true + default: release + type: choice + options: [release, beta, alpha] + confirm_live_publication: + description: Publish the verified bundle to Maven, CurseForge and GitHub + required: true + default: false + type: boolean + +permissions: + actions: read + contents: read + +concurrency: + group: basegems-release-1.12.2 + cancel-in-progress: false + +jobs: + preflight: + name: Verify the merged release bundle + if: github.repository == 'MinecraftModDevelopmentMods/BaseGems' && github.ref == 'refs/heads/master-1.12' + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + sha: ${{ steps.candidate.outputs.sha }} + run: ${{ steps.candidate.outputs.run }} + steps: + - name: Locate successful CI for the current branch tip + id: candidate + env: + GH_TOKEN: ${{ github.token }} + RELEASE_VERSION: ${{ inputs.release_version }} + TESTED_JAR_SHA256: ${{ inputs.tested_jar_sha256 }} + run: | + set -euo pipefail + [[ "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.112021$ ]] + [[ "$TESTED_JAR_SHA256" =~ ^[[:xdigit:]]{64}$ ]] + sha="$(gh api "repos/$GITHUB_REPOSITORY/branches/master-1.12" --jq .commit.sha)" + [[ "$sha" == "$GITHUB_SHA" ]] + props="$(gh api "repos/$GITHUB_REPOSITORY/contents/gradle.properties?ref=$sha" --jq .content | base64 --decode)" + grep -Fxq "mod_version=$RELEASE_VERSION" <<<"$props" + grep -Fxq 'minecraft_version=1.12.2' <<<"$props" + run="$(gh api --method GET "repos/$GITHUB_REPOSITORY/actions/workflows/ci.yml/runs" \ + -f branch=master-1.12 -f head_sha="$sha" -f event=push -f status=success \ + --jq '.workflow_runs[0].id // empty')" + [[ -n "$run" ]] + echo "sha=$sha" >> "$GITHUB_OUTPUT" + echo "run=$run" >> "$GITHUB_OUTPUT" + + - name: Download the audited CI bundle + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: BaseGems-1.12.2-${{ steps.candidate.outputs.sha }} + path: ${{ runner.temp }}/release-input + run-id: ${{ steps.candidate.outputs.run }} + github-token: ${{ github.token }} + + - name: Check all three jars and the manual test match + working-directory: ${{ runner.temp }}/release-input + env: + RELEASE_VERSION: ${{ inputs.release_version }} + TESTED_JAR_SHA256: ${{ inputs.tested_jar_sha256 }} + run: | + set -euo pipefail + diff -u SHA256SUMS <(sha256sum "BaseGems-$RELEASE_VERSION"{,-sources,-javadoc}.jar) + printf '%s BaseGems-%s.jar\n' "${TESTED_JAR_SHA256,,}" "$RELEASE_VERSION" | sha256sum --check --strict + grep -Fxq "$RELEASE_VERSION for Minecraft 1.12.2" CHANGELOG.txt + sha256sum --check SHA256SUMS >> "$GITHUB_STEP_SUMMARY" + + release_confirmation: + name: Verify publication credentials + needs: preflight + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Check credential access without printing credentials + env: + CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} + MAVEN_UPLOAD_URL: ${{ secrets.MAVEN_UPLOAD_URL }} + MAVEN_UPLOAD_USERNAME: ${{ secrets.MAVEN_UPLOAD_USERNAME }} + MAVEN_UPLOAD_PASSWORD: ${{ secrets.MAVEN_UPLOAD_PASSWORD }} + run: | + set -euo pipefail + for name in CURSEFORGE_TOKEN MAVEN_UPLOAD_URL MAVEN_UPLOAD_USERNAME MAVEN_UPLOAD_PASSWORD; do + if [[ -z "${!name:-}" ]]; then + echo "Required release secret is unavailable: $name" >&2 + exit 1 + fi + done + [[ "$MAVEN_UPLOAD_URL" == https://* ]] + curl --fail --silent --show-error --max-time 30 \ + -H "X-Api-Token: $CURSEFORGE_TOKEN" https://minecraft.curseforge.com/api/game/versions >/dev/null + + create_release_tag: + name: Create or verify the release tag + if: inputs.confirm_live_publication + needs: [preflight, release_confirmation] + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: write + steps: + - name: Bind the tag to the verified merged commit + env: + GH_TOKEN: ${{ github.token }} + RELEASE_SHA: ${{ needs.preflight.outputs.sha }} + RELEASE_VERSION: ${{ inputs.release_version }} + run: | + set -euo pipefail + [[ "$(gh api "repos/$GITHUB_REPOSITORY/branches/master-1.12" --jq .commit.sha)" == "$RELEASE_SHA" ]] + if ! gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_VERSION" >/dev/null 2>&1; then + gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \ + -f ref="refs/tags/$RELEASE_VERSION" -f sha="$RELEASE_SHA" >/dev/null + fi + [[ "$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_VERSION" --jq .object.sha)" == "$RELEASE_SHA" ]] + + publish_maven: + name: Publish Maven + needs: [preflight, create_release_tag] + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - name: Check out the tagged publication definition + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ needs.preflight.outputs.sha }} + - name: Install Java 25 for Mavenizer + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + - name: Install Java 8 for Forge + 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: temurin + java-version: '17.0.1+12' + - name: Download the audited CI bundle + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: BaseGems-1.12.2-${{ needs.preflight.outputs.sha }} + path: ${{ runner.temp }}/release-input + run-id: ${{ needs.preflight.outputs.run }} + github-token: ${{ github.token }} + - name: Publish the prepared artifacts + env: + MAVEN_UPLOAD_URL: ${{ secrets.MAVEN_UPLOAD_URL }} + MAVEN_UPLOAD_USERNAME: ${{ secrets.MAVEN_UPLOAD_USERNAME }} + MAVEN_UPLOAD_PASSWORD: ${{ secrets.MAVEN_UPLOAD_PASSWORD }} + run: | + set -euo pipefail + python3 gradle/stage-legacy-dependencies.py .gradle/legacy-dependencies + ./gradlew publishMavenJavaPublicationToReleaseRepository \ + -PpreparedReleaseDir="$RUNNER_TEMP/release-input" --no-daemon --stacktrace --max-workers=2 \ + "-Dorg.gradle.java.installations.paths=$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" \ + -Dorg.gradle.java.installations.auto-detect=false \ + -Dorg.gradle.java.installations.auto-download=false + + publish_curseforge: + name: Publish CurseForge + needs: [preflight, publish_maven] + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Download the audited CI bundle + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: BaseGems-1.12.2-${{ needs.preflight.outputs.sha }} + path: ${{ runner.temp }}/release-input + run-id: ${{ needs.preflight.outputs.run }} + github-token: ${{ github.token }} + - name: Verify the bundle + run: (cd "$RUNNER_TEMP/release-input" && sha256sum --check SHA256SUMS) + - name: Publish Base Gems on CurseForge + uses: Kira-NT/mc-publish@52307b03863581dec6b652b83e597aec02ebb075 # v3.3.1 + with: + curseforge-token: ${{ secrets.CURSEFORGE_TOKEN }} + curseforge-id: '1793939' + name: Base Gems ${{ inputs.release_version }} for Minecraft 1.12.2 (Forge) + version: ${{ inputs.release_version }} + version-type: ${{ inputs.curseforge_release_level }} + changelog-file: ${{ runner.temp }}/release-input/CHANGELOG.txt + loaders: forge + game-versions: '1.12.2' + game-version-filter: none + environment: client | server + java: Java 8 + curseforge-dependencies: | + 261744(required) + 245586(required) + 240967(optional) + fail-mode: fail + files: | + ${{ runner.temp }}/release-input/BaseGems-${{ inputs.release_version }}.jar + ${{ runner.temp }}/release-input/BaseGems-${{ inputs.release_version }}-sources.jar + ${{ runner.temp }}/release-input/BaseGems-${{ inputs.release_version }}-javadoc.jar + + publish_github: + name: Publish GitHub Release + needs: [preflight, publish_curseforge] + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + actions: read + contents: write + steps: + - name: Download the audited CI bundle + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: BaseGems-1.12.2-${{ needs.preflight.outputs.sha }} + path: ${{ runner.temp }}/release-input + run-id: ${{ needs.preflight.outputs.run }} + github-token: ${{ github.token }} + - name: Publish the same artifacts on GitHub + working-directory: ${{ runner.temp }}/release-input + env: + GH_TOKEN: ${{ github.token }} + RELEASE_VERSION: ${{ inputs.release_version }} + run: | + set -euo pipefail + sha256sum --check SHA256SUMS + gh release create "$RELEASE_VERSION" --repo "$GITHUB_REPOSITORY" --verify-tag \ + --title "Base Gems $RELEASE_VERSION for Minecraft 1.12.2 (Forge)" \ + --notes-file CHANGELOG.txt "BaseGems-$RELEASE_VERSION"{,-sources,-javadoc}.jar SHA256SUMS diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4debee7..1e3a30b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -5,6 +5,9 @@ XP, smith trades and both Fantasy Level settings. Use Java 8, Forge 14.23.5.2859 or later for 1.12.2, MMDLib 1.0.0 RC3 and MMD OreSpawn 4.1.6.112021. Base Metals is optional on this version. +This is the first published 1.12.2 build. It also works alongside Base Metals +2.6.3.112021. Install MMDLib and OreSpawn on both the client and server. + - Pick up gems and their crafting materials to learn the recipes in the recipe book. It only teaches recipes allowed by your gem and Fantasy Level settings. diff --git a/README.md b/README.md index ea0a1a6..1792ae3 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ jars in the `mods` folder on both the client and server. or a compatible 4.x update. You can play with just those three mods. If you also use Base Metals, this -build was checked with [Base Metals 2.6.2.112021](https://www.curseforge.com/minecraft/mc-mods/base-metals/files/9532211). +build is checked with [Base Metals 2.6.3.112021](https://www.curseforge.com/minecraft/mc-mods/base-metals/files/9541608). ## Mining and trading diff --git a/build.gradle b/build.gradle index 0c2480b..ae9ee28 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,7 @@ plugins { id 'java' id 'eclipse' id 'idea' + id 'maven-publish' id 'net.minecraftforge.renamer' version '1.1.5' id 'net.minecraftforge.gradle' version '7.0.34' } @@ -150,6 +151,7 @@ tasks.named('jar', Jar) { tasks.named(name, Jar) { filteringCharset = 'UTF-8' filesMatching(textPatterns, normalizeText) + filesMatching('package-list', normalizeText) } } @@ -167,4 +169,5 @@ ext.baseGemsBuild = [developmentOutput: developmentOutput].asImmutable() apply from: 'gradle/ore-models.gradle' apply from: 'gradle/recipe-advancements.gradle' apply from: 'gradle/release/artifacts.gradle' +apply from: 'gradle/release/publishing.gradle' apply from: 'gradle/ide/eclipse.gradle' diff --git a/docs/BUILDING.md b/docs/BUILDING.md index ad0657f..252e02e 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -24,3 +24,14 @@ Use a fresh test world when checking the OreSpawn defaults. In Low Fantasy, try mining and trading a gem, then check that storage blocks are still craftable and equipment recipes have gone. See [ore generation](ORE_GENERATION.md) for the placement rules and config overrides. + +## Publishing + +After the release PR is merged and its push CI passes, run **Release Base Gems +1.12.2** on `master-1.12`. Enter the version and the SHA-256 of the game jar +that passed manual testing. Leave live publication off for a verification run. + +The workflow checks the merged CI bundle against that hash, then publishes +the same three jars to Maven, CurseForge and GitHub. Maven credentials and the +CurseForge token come from repository or organization secrets. The 1.11.2 +dispatcher remains separate. diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index a0d1742..18c18c9 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -2,7 +2,7 @@ import groovy.json.JsonSlurper import java.security.MessageDigest def specs = new JsonSlurper().parse(file('gradle/dependencies.json')) -def mapped = specs.collectEntries { [(it.id): renamer.dependency(it.coordinate)] } +def mapped = specs.findAll { !it.compatibility }.collectEntries { [(it.id): renamer.dependency(it.coordinate)] } configurations { legacyModArtifacts { canBeConsumed = false; transitive = false } @@ -14,7 +14,7 @@ dependencies { compileOnly 'org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209' specs.each { spec -> if (spec.runtime) implementation mapped[spec.id] - else compileOnly mapped[spec.id] + else if (!spec.compatibility) compileOnly mapped[spec.id] legacyModArtifacts spec.coordinate } } diff --git a/gradle/dependencies.json b/gradle/dependencies.json index 45e9a1a..3d4d90e 100644 --- a/gradle/dependencies.json +++ b/gradle/dependencies.json @@ -1,4 +1,12 @@ [ + { + "id": "basemetals", + "coordinate": "curse.maven:base-metals-240967:9541608", + "repository": "https://www.cursemaven.com", + "sha256": "327AB27CDBADA30D850F5DA6DE89F566242A2ED9AC16F345FEF8E1DAC07A0ACE", + "runtime": false, + "compatibility": true + }, { "id": "mmdlib", "coordinate": "curse.maven:mmdlib-261744:9414723", diff --git a/gradle/release/artifacts.gradle b/gradle/release/artifacts.gradle index be899ad..58bbf03 100644 --- a/gradle/release/artifacts.gradle +++ b/gradle/release/artifacts.gradle @@ -85,7 +85,8 @@ def auditJars = { File directory -> if (!entry.directory && !(date.toString() in ['1980-01-01T00:00', '1980-02-01T00:00'])) { throw new GradleException("Volatile archive timestamp: ${entry.name}") } - if (!entry.directory && path ==~ /.*\.(java|html|css|js|json|lang|mcmeta|md|txt|xml|cfg|info|properties)/) { + if (!entry.directory && (path == 'package-list' + || path ==~ /.*\.(java|html|css|js|json|lang|mcmeta|md|txt|xml|cfg|info|properties)/)) { def decoder = java.nio.charset.StandardCharsets.UTF_8.newDecoder() .onMalformedInput(java.nio.charset.CodingErrorAction.REPORT) .onUnmappableCharacter(java.nio.charset.CodingErrorAction.REPORT) @@ -151,3 +152,5 @@ tasks.register('writeReleaseChecksums') { } } tasks.named('check') { dependsOn tasks.named('verifyReleaseConfiguration') } + +ext.baseGemsRelease = [jar: releaseJar, audit: auditJars].asImmutable() diff --git a/gradle/release/publishing.gradle b/gradle/release/publishing.gradle new file mode 100644 index 0000000..88bbaac --- /dev/null +++ b/gradle/release/publishing.gradle @@ -0,0 +1,89 @@ +import java.security.MessageDigest + +def prepared = providers.gradleProperty('preparedReleaseDir') +def releaseFiles = ['', '-sources', '-javadoc'].collect { "BaseGems-${project.version}${it}.jar".toString() } + +publishing { + publications { + mavenJava(MavenPublication) { + groupId = project.group.toString() + artifactId = 'BaseGems' + version = project.version.toString() + if (prepared.isPresent()) { + artifact(file("${prepared.get()}/${releaseFiles[0]}")) + artifact(file("${prepared.get()}/${releaseFiles[1]}")) { classifier = 'sources' } + artifact(file("${prepared.get()}/${releaseFiles[2]}")) { classifier = 'javadoc' } + } else { + artifact(project.ext.baseGemsRelease.jar) + artifact(tasks.named('sourcesJar')) + artifact(tasks.named('javadocJar')) + } + pom { + name = 'Base Gems' + description = 'Gemstones to mine, trade and craft with in Minecraft.' + url = 'https://github.com/MinecraftModDevelopmentMods/BaseGems' + licenses { + license { + name = 'GNU Lesser General Public License, Version 2.1' + url = 'https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt' + } + } + scm { + connection = 'scm:git:https://github.com/MinecraftModDevelopmentMods/BaseGems.git' + url = 'https://github.com/MinecraftModDevelopmentMods/BaseGems' + } + } + } + } + repositories { + maven { + name = 'release' + url = uri(providers.environmentVariable('MAVEN_UPLOAD_URL') + .orElse('https://invalid.invalid/missing-maven-upload-url').get()) + credentials { + username = providers.environmentVariable('MAVEN_UPLOAD_USERNAME').orNull ?: '' + password = providers.environmentVariable('MAVEN_UPLOAD_PASSWORD').orNull ?: '' + } + } + if (providers.gradleProperty('mavenVerificationRepository').isPresent()) { + maven { + name = 'verification' + url = uri(providers.gradleProperty('mavenVerificationRepository').get()) + } + } + } +} + +tasks.register('verifyPreparedReleaseArtifacts') { + doLast { + if (!prepared.isPresent()) throw new GradleException('preparedReleaseDir is required.') + File directory = file(prepared.get()) + def checksums = new File(directory, 'SHA256SUMS').readLines('UTF-8').collectEntries { line -> + def parts = line.split(' ', 2) + if (parts.length != 2 || !(parts[0] ==~ /[a-fA-F0-9]{64}/) + || !releaseFiles.contains(parts[1])) throw new GradleException('Invalid release checksum entry.') + [(parts[1]): parts[0]] + } + if (checksums.size() != releaseFiles.size()) throw new GradleException('Incomplete release bundle.') + releaseFiles.each { name -> + File jar = new File(directory, name) + String actual = MessageDigest.getInstance('SHA-256').digest(jar.bytes).encodeHex().toString() + if (!actual.equalsIgnoreCase(checksums[name])) throw new GradleException("Checksum mismatch: ${name}") + } + project.ext.baseGemsRelease.audit(directory) + } +} + +tasks.withType(PublishToMavenRepository).configureEach { + dependsOn prepared.isPresent() ? tasks.named('verifyPreparedReleaseArtifacts') : tasks.named('verifyReleaseArtifacts') + if (repository.name == 'release') { + doFirst { + for (String name : ['MAVEN_UPLOAD_URL', 'MAVEN_UPLOAD_USERNAME', 'MAVEN_UPLOAD_PASSWORD']) { + if (!providers.environmentVariable(name).orNull) throw new GradleException("Missing ${name}") + } + if (!repository.url.toString().startsWith('https://')) { + throw new GradleException('Release publication requires an HTTPS Maven repository.') + } + } + } +}