diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b50ab6e40..74084cb9b 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.112021-sources.jar build/libs/Mineralogy-6.0.1.112021-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.12.2-test-reports-${{ github.sha }} + name: Mineralogy-1.12.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..180648fee --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,55 @@ +name: CodeQL + +on: + push: + branches: + - master-1.12 + - 'feature/**' + pull_request: + branches: + - master-1.12 + schedule: + - cron: '43 7 * * 4' + +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/deploy-release.yml b/.github/workflows/deploy-release.yml index 9eaccce55..1ec0cb9ba 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -1,23 +1,16 @@ -name: Deploy Mineralogy release +name: Release Mineralogy + +run-name: Release Mineralogy ${{ inputs.release_version }} (CurseForge ${{ inputs.curseforge_release_level }}) on: workflow_dispatch: inputs: - mode: - description: Validate the selected ref, verify release-secret access, or publish after all gates pass. - required: true - default: validate-only - type: choice - options: - - validate-only - - verify-secrets - - publish - release_ref: - description: Commit, branch, or tag to validate. Publication requires the exact release tag. + release_version: + description: Four-component Mineralogy version; the target branch is derived from its fourth component. required: true type: string - curseforge_channel: - description: CurseForge release level; beta and alpha also create GitHub prereleases. + curseforge_release_level: + description: CurseForge release level only; this does not change the GitHub Release type. required: true default: release type: choice @@ -25,18 +18,18 @@ on: - release - beta - alpha - confirm_version: - description: Publication confirmation; enter the version declared by the selected release tag. - required: false - default: '' - type: string + confirm_live_publication: + description: Confirm publication to Maven, CurseForge, and GitHub after the build passes. + required: true + default: false + type: boolean permissions: actions: read contents: read concurrency: - group: mineralogy-release-${{ inputs.release_ref }} + group: mineralogy-release-${{ inputs.release_version }} cancel-in-progress: false jobs: @@ -45,84 +38,193 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 outputs: + target_branch: ${{ steps.route.outputs.target_branch }} release_sha: ${{ steps.validate.outputs.release_sha }} release_version: ${{ steps.validate.outputs.release_version }} release_tag: ${{ steps.validate.outputs.release_tag }} minecraft_version: ${{ steps.validate.outputs.minecraft_version }} + loader_name: ${{ steps.validate.outputs.loader_name }} + loader_display: ${{ steps.validate.outputs.loader_display }} + java_version: ${{ steps.validate.outputs.java_version }} + java_toolchain_version: ${{ steps.validate.outputs.java_toolchain_version }} + gradle_java_version: ${{ steps.validate.outputs.gradle_java_version }} + curseforge_project_id: ${{ steps.validate.outputs.curseforge_project_id }} main_jar: ${{ steps.validate.outputs.main_jar }} sources_jar: ${{ steps.validate.outputs.sources_jar }} javadoc_jar: ${{ steps.validate.outputs.javadoc_jar }} steps: - - name: Check out selected release ref + - name: Resolve target branch from version + id: route + env: + GH_TOKEN: ${{ github.token }} + RELEASE_VERSION: ${{ inputs.release_version }} + run: | + set -euo pipefail + + if [[ "$GITHUB_REPOSITORY" != "MinecraftModDevelopmentMods/MinecraftMineralogy" ]]; then + echo "Releases can only run in MinecraftModDevelopmentMods/MinecraftMineralogy" >&2 + exit 1 + fi + if [[ ! "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.([0-9]+)$ ]]; then + echo "release_version must contain four numeric components" >&2 + exit 1 + fi + + target_suffix="${BASH_REMATCH[1]}" + if (( ${#target_suffix} < 6 )); then + echo "Invalid target suffix $target_suffix" >&2 + exit 1 + fi + + loader_code="${target_suffix: -1}" + target_digits="${target_suffix::-1}" + mc_patch_padded="${target_digits: -2}" + without_patch="${target_digits::-2}" + mc_minor_padded="${without_patch: -2}" + mc_major="${without_patch::-2}" + + if [[ -z "$mc_major" || ! "$mc_major" =~ ^[0-9]+$ ]]; then + echo "Invalid Minecraft major version in target suffix $target_suffix" >&2 + exit 1 + fi + mc_minor="$((10#$mc_minor_padded))" + mc_patch="$((10#$mc_patch_padded))" + case "$loader_code" in + 1) loader_suffix= ;; + 2) loader_suffix=-neo ;; + *) echo "Unknown loader code $loader_code in target suffix $target_suffix" >&2; exit 1 ;; + esac + + candidates=( + "master-$mc_major.$mc_minor.$mc_patch$loader_suffix" + "master-$mc_major.$mc_minor$loader_suffix" + ) + target_branch= + for candidate in "${candidates[@]}"; do + if gh api "repos/$GITHUB_REPOSITORY/branches/$candidate" >/dev/null 2>&1; then + target_branch="$candidate" + break + fi + done + if [[ -z "$target_branch" ]]; then + echo "No MMD release branch matches target suffix $target_suffix" >&2 + printf 'Checked branch %s\n' "${candidates[@]}" >&2 + exit 1 + fi + + echo "target_branch=$target_branch" >> "$GITHUB_OUTPUT" + echo "Resolved $RELEASE_VERSION to $target_branch" + + - name: Check out derived release branch uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: - ref: ${{ inputs.release_ref }} + ref: ${{ steps.route.outputs.target_branch }} fetch-depth: 0 - - name: Validate version, tag, and prior CI + - name: Validate version, target, tag, and prior CI id: validate env: GH_TOKEN: ${{ github.token }} - MODE: ${{ inputs.mode }} - RELEASE_REF: ${{ inputs.release_ref }} - CONFIRM_VERSION: ${{ inputs.confirm_version }} + REQUESTED_VERSION: ${{ inputs.release_version }} run: | set -euo pipefail - actual_version="$(sed -n 's/^mod_version=//p' gradle.properties)" - minecraft_version="$(sed -n 's/^mc_version=//p' gradle.properties)" - case "$minecraft_version" in - 1.10.2) target_suffix=110021 ;; - 1.12.2) target_suffix=112021 ;; - *) - echo "Selected ref declares unsupported release target mc_version=$minecraft_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)" + java_version="$(value java_version)" + java_toolchain_version="$(value java_toolchain_version)" + gradle_java_version="$(value gradle_java_version)" + curseforge_project_id="$(value curseforge_project_id)" + + for required in release_version minecraft_version loader_name loader_code \ + java_version gradle_java_version curseforge_project_id; do + if [[ -z "${!required}" ]]; then + echo "Selected ref is missing required release metadata: $required" >&2 exit 1 - ;; - esac - if [[ ! "$actual_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.${target_suffix}$ ]]; then - echo "Selected ref declares invalid Minecraft $minecraft_version Forge mod_version=$actual_version" >&2 - exit 1 - fi - - release_sha="$(git rev-parse HEAD)" - release_tag="$actual_version" - echo "release_sha=$release_sha" >> "$GITHUB_OUTPUT" - echo "release_version=$actual_version" >> "$GITHUB_OUTPUT" - echo "release_tag=$release_tag" >> "$GITHUB_OUTPUT" - echo "minecraft_version=$minecraft_version" >> "$GITHUB_OUTPUT" - echo "main_jar=Mineralogy-$actual_version.jar" >> "$GITHUB_OUTPUT" - echo "sources_jar=Mineralogy-$actual_version-sources.jar" >> "$GITHUB_OUTPUT" - echo "javadoc_jar=Mineralogy-$actual_version-javadoc.jar" >> "$GITHUB_OUTPUT" + fi + done + java_toolchain_version="${java_toolchain_version:-$java_version}" - if [[ "$MODE" != "publish" ]]; then - exit 0 + 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 - - if [[ "$RELEASE_REF" != "$release_tag" ]]; then - echo "Publication requires release_ref=$release_tag" >&2 + 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 - if [[ "$CONFIRM_VERSION" != "$actual_version" ]]; then - echo "confirm_version must equal $actual_version" >&2 + case "$loader_name:$loader_code" in + forge:1) loader_display=Forge ;; + neoforge:2) loader_display=NeoForge ;; + *) echo "Invalid loader metadata $loader_name/$loader_code" >&2; exit 1 ;; + esac + if [[ ! "$java_version" =~ ^[0-9]+$ || ! "$gradle_java_version" =~ ^[0-9]+$ ]]; then + echo "Invalid Java metadata $java_version/$gradle_java_version" >&2 exit 1 fi - if ! git show-ref --verify --quiet "refs/tags/$release_tag"; then - echo "The exact release tag does not exist" >&2 + if [[ "$curseforge_project_id" != "240974" ]]; then + echo "Unexpected Mineralogy CurseForge project $curseforge_project_id" >&2 exit 1 fi - tag_sha="$(git rev-list -n 1 "refs/tags/$release_tag")" - if [[ "$tag_sha" != "$release_sha" ]]; then - echo "The release tag does not resolve to the checked-out commit" >&2 + 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}" + if [[ ! "$release_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.${target_suffix}$ ]]; then + echo "mod_version $release_version does not match $minecraft_version $loader_display target $target_suffix" >&2 + exit 1 + fi + if [[ "$REQUESTED_VERSION" != "$release_version" ]]; then + echo "Requested version $REQUESTED_VERSION does not match ${{ steps.route.outputs.target_branch }} mod_version $release_version" >&2 exit 1 fi + release_sha="$(git rev-parse HEAD)" + release_tag="$release_version" + echo "release_sha=$release_sha" >> "$GITHUB_OUTPUT" + echo "release_version=$release_version" >> "$GITHUB_OUTPUT" + echo "release_tag=$release_tag" >> "$GITHUB_OUTPUT" + echo "minecraft_version=$minecraft_version" >> "$GITHUB_OUTPUT" + echo "loader_name=$loader_name" >> "$GITHUB_OUTPUT" + echo "loader_display=$loader_display" >> "$GITHUB_OUTPUT" + echo "java_version=$java_version" >> "$GITHUB_OUTPUT" + echo "java_toolchain_version=$java_toolchain_version" >> "$GITHUB_OUTPUT" + echo "gradle_java_version=$gradle_java_version" >> "$GITHUB_OUTPUT" + echo "curseforge_project_id=$curseforge_project_id" >> "$GITHUB_OUTPUT" + echo "main_jar=Mineralogy-$release_version.jar" >> "$GITHUB_OUTPUT" + echo "sources_jar=Mineralogy-$release_version-sources.jar" >> "$GITHUB_OUTPUT" + echo "javadoc_jar=Mineralogy-$release_version-javadoc.jar" >> "$GITHUB_OUTPUT" + successful_ci="$(gh api \ "repos/$GITHUB_REPOSITORY/commits/$release_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 Build, test, and audit check" >&2 + echo "The selected commit has no successful Build, test, and audit check" >&2 + exit 1 + fi + + if tag_json="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$release_tag" 2>/dev/null)"; then + tag_sha="$(jq -r '.object.sha' <<<"$tag_json")" + if [[ "$tag_sha" != "$release_sha" ]]; then + echo "Existing tag $release_tag resolves to $tag_sha instead of $release_sha" >&2 + exit 1 + fi + if gh api "repos/$GITHUB_REPOSITORY/releases/tags/$release_tag" >/dev/null 2>&1; then + echo "Version $release_version already has a GitHub Release for exact tag $release_tag" >&2 + exit 1 + fi + fi + + legacy_release_tag="$minecraft_version-$release_version" + if [[ "$legacy_release_tag" != "$release_tag" ]] \ + && gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$legacy_release_tag" >/dev/null 2>&1; then + echo "Version $release_version was already published under historical tag $legacy_release_tag" >&2 exit 1 fi @@ -130,10 +232,8 @@ jobs: name: Build immutable release artifact needs: preflight runs-on: ubuntu-latest - timeout-minutes: 45 + timeout-minutes: 60 env: - RELEASE_VERSION: ${{ needs.preflight.outputs.release_version }} - RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }} MAIN_JAR: ${{ needs.preflight.outputs.main_jar }} SOURCES_JAR: ${{ needs.preflight.outputs.sources_jar }} JAVADOC_JAR: ${{ needs.preflight.outputs.javadoc_jar }} @@ -146,25 +246,34 @@ jobs: with: ref: ${{ needs.preflight.outputs.release_sha }} - - name: Install Java 8 toolchain + - name: Install target Java toolchain uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: '8' + java-version: ${{ needs.preflight.outputs.java_toolchain_version }} - - name: Install Java 17 for Gradle + - name: Install Java for Gradle uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: - distribution: microsoft - java-version: '17' + distribution: temurin + java-version: ${{ needs.preflight.outputs.gradle_java_version }} - name: Set up Gradle - uses: gradle/actions/setup-gradle@4733eaac7c1b0da527e4206b7671e0061de1ce37 # v6 + uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 + + - 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 once run: | chmod +x ./gradlew - ./gradlew clean check build javadoc verifyReleaseDependencies verifyReleaseArtifacts writeReleaseChecksums --no-daemon --stacktrace + ./gradlew clean check build javadoc verifyReleaseDependencies verifyReleaseArtifacts writeReleaseChecksums \ + -PorespawnVerificationRepository="${{ steps.orespawn.outputs.repository }}" \ + --no-daemon --stacktrace - name: Stage the immutable release artifact id: artifact @@ -178,7 +287,7 @@ jobs: cp build/release/SHA256SUMS "$bundle/" cp CHANGELOG.txt "$bundle/" (cd "$bundle" && sha256sum --check SHA256SUMS) - echo "name=Mineralogy-${{ needs.preflight.outputs.minecraft_version }}-${{ needs.preflight.outputs.release_sha }}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT" + echo "name=Mineralogy-${{ needs.preflight.outputs.minecraft_version }}-${{ needs.preflight.outputs.loader_name }}-${{ needs.preflight.outputs.release_sha }}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT" - name: Upload immutable release artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 @@ -188,38 +297,50 @@ jobs: retention-days: 90 path: ${{ runner.temp }}/release-bundle/* - release_approval: - name: Approve live publication - if: inputs.mode == 'publish' + - name: Summarize the prepared release candidate + env: + RELEASE_BRANCH: ${{ needs.preflight.outputs.target_branch }} + RELEASE_LEVEL: ${{ inputs.curseforge_release_level }} + RELEASE_SHA: ${{ needs.preflight.outputs.release_sha }} + RELEASE_VERSION: ${{ needs.preflight.outputs.release_version }} + run: | + { + echo "## Prepared release candidate" + echo + echo "- Version: \`$RELEASE_VERSION\`" + echo "- Target branch: \`$RELEASE_BRANCH\`" + echo "- Commit: \`$RELEASE_SHA\`" + echo "- CurseForge release level: \`$RELEASE_LEVEL\`" + echo "- GitHub Release type: ordinary release" + echo + echo "### Public artifact checksums" + echo '```text' + cat build/release/SHA256SUMS + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + release_confirmation: + name: Confirm ${{ inputs.release_version }} live publication needs: - preflight - build runs-on: ubuntu-latest timeout-minutes: 10 - environment: - name: release - env: - RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }} steps: - - name: Record approval gate - run: echo "Release environment approval granted for $RELEASE_TAG" - - verify_secrets: - name: Verify release-secret access - if: inputs.mode == 'verify-secrets' - needs: build - runs-on: ubuntu-latest - timeout-minutes: 5 - - steps: - - name: Confirm all required release secrets are available + - name: Confirm publication and release-secret access env: + CONFIRM_LIVE_PUBLICATION: ${{ inputs.confirm_live_publication }} 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 + if [[ "$CONFIRM_LIVE_PUBLICATION" != "true" ]]; then + echo "Confirm live publication in the workflow form before releasing ${{ needs.preflight.outputs.release_tag }}" >&2 + exit 1 + fi + echo "Live publication confirmed for ${{ needs.preflight.outputs.release_tag }}" missing=() for name in CURSEFORGE_TOKEN MAVEN_UPLOAD_URL MAVEN_UPLOAD_USERNAME MAVEN_UPLOAD_PASSWORD; do if [[ -z "${!name:-}" ]]; then @@ -230,15 +351,55 @@ jobs: printf 'Required release secret is unavailable: %s\n' "${missing[@]}" >&2 exit 1 fi - echo "All four required release secrets are available to this repository workflow." + echo "All four required release secrets are available." + + create_release_tag: + name: Create validated release tag + needs: + - preflight + - build + - release_confirmation + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + actions: read + contents: write + + steps: + - name: Create or verify the exact tag + env: + GH_TOKEN: ${{ github.token }} + RELEASE_SHA: ${{ needs.preflight.outputs.release_sha }} + RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }} + run: | + set -euo pipefail + + if tag_json="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG" 2>/dev/null)"; then + tag_sha="$(jq -r '.object.sha' <<<"$tag_json")" + if [[ "$tag_sha" != "$RELEASE_SHA" ]]; then + echo "Existing tag $RELEASE_TAG resolves to $tag_sha instead of $RELEASE_SHA" >&2 + exit 1 + fi + echo "Existing tag $RELEASE_TAG already points to $RELEASE_SHA" + else + gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \ + -f ref="refs/tags/$RELEASE_TAG" \ + -f sha="$RELEASE_SHA" >/dev/null + echo "Created tag $RELEASE_TAG at $RELEASE_SHA" + fi + + created_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG" --jq '.object.sha')" + if [[ "$created_sha" != "$RELEASE_SHA" ]]; then + echo "Tag verification returned $created_sha instead of $RELEASE_SHA" >&2 + exit 1 + fi publish_maven: name: Publish Maven - if: inputs.mode == 'publish' needs: - preflight - build - - release_approval + - create_release_tag runs-on: ubuntu-latest timeout-minutes: 30 @@ -248,20 +409,20 @@ jobs: with: ref: ${{ needs.preflight.outputs.release_sha }} - - name: Install Java 8 toolchain + - name: Install target Java toolchain uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: '8' + java-version: ${{ needs.preflight.outputs.java_toolchain_version }} - - name: Install Java 17 for Gradle + - name: Install Java for Gradle uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: - distribution: microsoft - java-version: '17' + distribution: temurin + java-version: ${{ needs.preflight.outputs.gradle_java_version }} - name: Set up Gradle - uses: gradle/actions/setup-gradle@4733eaac7c1b0da527e4206b7671e0061de1ce37 # v6 + uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 - name: Download immutable release artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 @@ -269,6 +430,13 @@ jobs: name: ${{ needs.build.outputs.artifact_name }} path: ${{ runner.temp }}/release-input + - 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: Verify artifact and Maven credentials env: MAVEN_UPLOAD_URL: ${{ secrets.MAVEN_UPLOAD_URL }} @@ -295,11 +463,11 @@ jobs: chmod +x ./gradlew ./gradlew publishMavenJavaPublicationToReleaseRepository \ -PpreparedReleaseDir="$RUNNER_TEMP/release-input" \ + -PorespawnVerificationRepository="${{ steps.orespawn.outputs.repository }}" \ --no-daemon --stacktrace publish_curseforge: name: Publish CurseForge - if: inputs.mode == 'publish' needs: - preflight - build @@ -329,16 +497,16 @@ jobs: uses: Kira-NT/mc-publish@52307b03863581dec6b652b83e597aec02ebb075 # v3.3.1 with: curseforge-token: ${{ secrets.CURSEFORGE_TOKEN }} - curseforge-id: 240974 - name: Mineralogy ${{ needs.preflight.outputs.release_version }} for Minecraft ${{ needs.preflight.outputs.minecraft_version }} + curseforge-id: ${{ needs.preflight.outputs.curseforge_project_id }} + name: Mineralogy ${{ needs.preflight.outputs.release_version }} for Minecraft ${{ needs.preflight.outputs.minecraft_version }} (${{ needs.preflight.outputs.loader_display }}) version: ${{ needs.preflight.outputs.release_version }} - version-type: ${{ inputs.curseforge_channel }} + version-type: ${{ inputs.curseforge_release_level }} changelog-file: ${{ runner.temp }}/release-input/CHANGELOG.txt - loaders: forge + loaders: ${{ needs.preflight.outputs.loader_name }} game-versions: ${{ needs.preflight.outputs.minecraft_version }} game-version-filter: none environment: client | server - java: Java 8 + java: Java ${{ needs.preflight.outputs.java_version }} curseforge-dependencies: 245586(required) fail-mode: fail files: | @@ -348,7 +516,6 @@ jobs: publish_github: name: Publish GitHub Release - if: inputs.mode == 'publish' needs: - preflight - build @@ -356,6 +523,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 permissions: + actions: read contents: write steps: @@ -375,10 +543,10 @@ jobs: github-tag: ${{ needs.preflight.outputs.release_tag }} github-commitish: ${{ needs.preflight.outputs.release_sha }} github-draft: false - github-prerelease: ${{ inputs.curseforge_channel != 'release' }} - name: Mineralogy ${{ needs.preflight.outputs.release_version }} for Minecraft ${{ needs.preflight.outputs.minecraft_version }} + github-prerelease: false + name: Mineralogy ${{ needs.preflight.outputs.release_version }} for Minecraft ${{ needs.preflight.outputs.minecraft_version }} (${{ needs.preflight.outputs.loader_display }}) version: ${{ needs.preflight.outputs.release_version }} - version-type: ${{ inputs.curseforge_channel }} + version-type: release changelog-file: ${{ runner.temp }}/release-input/CHANGELOG.txt fail-mode: fail files: | diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml index ca8859230..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: - - '*.*.*.112021' + - '*.*.*.*' 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]+\.112021$ ]]; then - echo "mod_version must use the four-part 1.12.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="$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.12 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..cc6223df5 --- /dev/null +++ b/.github/workflows/validate-gradle-build.yml @@ -0,0 +1,23 @@ +name: Validate Gradle Wrapper + +on: + push: + branches: + - master-1.12 + - 'feature/**' + pull_request: + branches: + - master-1.12 + +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 e11d4acbb..a7b7ce6f7 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.12)](https://github.com/MinecraftModDevelopmentMods/MinecraftMineralogy/actions/workflows/ci.yml?query=branch%3Amaster-1.12) + # Mineralogy 6 for Minecraft 1.12.2 Mineralogy adds real-world rock families, matching construction blocks, @@ -30,4 +35,4 @@ explicitly selects a different engine. [CurseForge](https://www.curseforge.com/minecraft/mc-mods/minecraft-mineralogy) · [Source and issues](https://github.com/MinecraftModDevelopmentMods/MinecraftMineralogy) · -[MMD Discord](https://discord.mcmoddev.com) +[MMD Discord](https://discord.moddev.zone) diff --git a/build.gradle b/build.gradle index 811aa0438..d70848e5a 100644 --- a/build.gradle +++ b/build.gradle @@ -164,8 +164,8 @@ repositories { maven fg.forgeMaven maven fg.minecraftLibsMaven - // Optional exact-coordinate mirror for offline/local release verification. - // CI and ordinary builds do not set this property and resolve CurseMaven. + // Optional exact-coordinate mirror populated by CI from the official + // CurseForge file endpoint. Ordinary developer builds resolve CurseMaven. if (providers.gradleProperty('orespawnVerificationRepository').isPresent()) { maven { name = 'OreSpawnReleaseVerificationMirror' @@ -192,13 +192,15 @@ repositories { name = 'MinecraftLibraries' url = 'https://libraries.minecraft.net/' } - maven { - name = 'CurseMaven' - url = 'https://www.cursemaven.com' - metadataSources { - artifact() + if (!providers.gradleProperty('orespawnVerificationRepository').isPresent()) { + maven { + name = 'CurseMaven' + url = 'https://www.cursemaven.com' + metadataSources { + artifact() + } + content { includeGroup('curse.maven') } } - content { includeGroup('curse.maven') } } maven { name = 'ModDevZone' @@ -367,6 +369,15 @@ tasks.register('verifyReleaseConfiguration') { if (project.mc_version != '1.12.2' || project.forge_version != '14.23.5.2859') { 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.7.112021' || project.orespawn_curse_project_id != '245586' || project.orespawn_curse_file_id != '8685379' diff --git a/gradle.properties b/gradle.properties index 158209316..359944187 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,6 +10,15 @@ mod_group=zone.moddev.mc # Major.Minor.Bug.Target; 112021 means Minecraft 1.12.2 on Forge. mod_version=6.0.1.112021 +# Release metadata consumed by the generic dispatcher. +minecraft_version=1.12.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.12.2 #Forge version to use (Required) 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/VersionContractTest.java b/src/test/java/zone/moddev/mc/mineralogy/VersionContractTest.java index be9670e18..f615b86f6 100644 --- a/src/test/java/zone/moddev/mc/mineralogy/VersionContractTest.java +++ b/src/test/java/zone/moddev/mc/mineralogy/VersionContractTest.java @@ -23,6 +23,13 @@ public void minecraft1122ForgeUsesTheDeterministicFourComponentVersion() throws String minecraftVersion = properties.getProperty("mc_version"); String modVersion = properties.getProperty("mod_version"); assertEquals("1.12.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("112021", targetFor(minecraftVersion, 1)); assertEquals("6.0.1.112021", modVersion); assertEquals(modVersion, Mineralogy.VERSION); diff --git a/src/test/java/zone/moddev/mc/mineralogy/WorkflowContractTest.java b/src/test/java/zone/moddev/mc/mineralogy/WorkflowContractTest.java index 3ec3dfd55..28a9e381b 100644 --- a/src/test/java/zone/moddev/mc/mineralogy/WorkflowContractTest.java +++ b/src/test/java/zone/moddev/mc/mineralogy/WorkflowContractTest.java @@ -1,5 +1,6 @@ package zone.moddev.mc.mineralogy; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -11,39 +12,111 @@ public class WorkflowContractTest { @Test - public void branchCiUsesTheGuarded112BuildAndExactCheckName() throws Exception { + public void branchCiUsesTheGuardedBuildAndModernPinnedChecks() 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.12")); assertTrue(ci.contains("name: Build, test, and audit")); assertTrue(ci.contains("clean check build javadoc verifyReleaseDependencies verifyReleaseArtifacts")); - assertTrue(ci.contains("Mineralogy-6.0.1.112021.jar")); - assertTrue(ci.contains("Mineralogy-6.0.1.112021-sources.jar")); - assertTrue(ci.contains("Mineralogy-6.0.1.112021-javadoc.jar")); + 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 tagStarterAcceptsOnlyTheExact112TargetTagAfterGreenCi() throws Exception { + public void tagValidatorIsGenericReadOnlyAndNeverStartsASecondRelease() throws Exception { String starter = read(".github/workflows/release-on-tag.yml"); - assertTrue(starter.contains("'*.*.*.112021'")); - assertTrue(starter.contains("\\.112021$")); - assertTrue(starter.contains("expected_tag=\"$release_version\"")); - assertFalse(starter.contains("expected_tag=\"1.12.2-$release_version\"")); + assertTrue(starter.contains("'*.*.*.*'")); + assertTrue(starter.contains("loader_name:$loader_code")); assertTrue(starter.contains("Build, test, and audit")); - assertTrue(starter.contains("--ref master-1.12")); + assertTrue(starter.contains("Release tag must equal mod_version")); + assertTrue(starter.contains("confirm live publication: checked")); + assertFalse(starter.contains("gh workflow run")); + assertFalse(starter.contains("actions: write")); + } + + @Test + public void defaultDispatcherUsesVersionOnlyRoutingAndCurseForgeOnlyChannels() throws Exception { + String deploy = read(".github/workflows/deploy-release.yml"); + assertTrue(deploy.contains("release_version:")); + assertTrue(deploy.contains("curseforge_release_level:")); + assertTrue(deploy.contains("confirm_live_publication:")); + assertFalse(deploy.contains(" mode:")); + assertFalse(deploy.contains(" release_ref:")); + assertFalse(deploy.contains(" confirm_version:")); + assertFalse(deploy.contains("environment:\n name: release")); + + assertTrue(deploy.contains("target_suffix=\"${BASH_REMATCH[1]}\"")); + assertTrue(deploy.contains("loader_code=\"${target_suffix: -1}\"")); + assertTrue(deploy.contains("\"master-$mc_major.$mc_minor.$mc_patch$loader_suffix\"")); + assertTrue(deploy.contains("\"master-$mc_major.$mc_minor$loader_suffix\"")); + assertTrue(deploy.contains("forge:1) loader_display=Forge")); + assertTrue(deploy.contains("neoforge:2) loader_display=NeoForge")); + assertTrue(deploy.contains("Unexpected Mineralogy CurseForge project")); + + assertEquals("master-1.10.2", fullBranchFor("110021")); + assertEquals("master-1.12.2", fullBranchFor("112021")); + assertEquals("master-1.13.2", fullBranchFor("113021")); } @Test - public void defaultDispatcherRetains110MaintenanceAndAdds112() throws Exception { + public void dispatcherProtectsPublishedVersionsAndUsesOneOrderedBundle() throws Exception { String deploy = read(".github/workflows/deploy-release.yml"); - assertTrue(deploy.contains("1.10.2) target_suffix=110021")); - assertTrue(deploy.contains("1.12.2) target_suffix=112021")); - assertTrue(deploy.contains("release_tag=\"$actual_version\"")); - assertFalse(deploy.contains("release_tag=\"$minecraft_version-$actual_version\"")); - assertTrue(deploy.contains("minecraft_version: ${{ steps.validate.outputs.minecraft_version }}")); - assertTrue(deploy.contains("game-versions: ${{ needs.preflight.outputs.minecraft_version }}")); + assertTrue(deploy.contains("legacy_release_tag=\"$minecraft_version-$release_version\"")); + assertTrue(deploy.contains("was already published under historical tag")); + assertTrue(deploy.contains("Existing tag $release_tag resolves to")); + assertTrue(deploy.contains("already has a GitHub Release for exact tag")); + assertTrue(deploy.contains("Create validated release tag")); + assertTrue(deploy.contains("SHA256SUMS")); + assertTrue(deploy.contains("curseforge-dependencies: 245586(required)")); + assertTrue(deploy.contains("version-type: ${{ inputs.curseforge_release_level }}")); + assertTrue(deploy.contains("github-prerelease: false")); + assertTrue(deploy.contains("version-type: release")); + assertEquals(2, countOccurrences(deploy, "bash gradle/stage-orespawn-release.sh")); + assertTrue(deploy.contains("-PorespawnVerificationRepository=\"${{ steps.orespawn.outputs.repository }}\"")); + + int maven = deploy.indexOf(" publish_maven:"); + int curseForge = deploy.indexOf(" publish_curseforge:"); + int github = deploy.indexOf(" publish_github:"); + assertTrue(maven > 0 && curseForge > maven && github > curseForge); + assertTrue(deploy.substring(curseForge, github).contains("- publish_maven")); + assertTrue(deploy.substring(github).contains("- publish_curseforge")); + } + + private static String fullBranchFor(String suffix) { + String loaderCode = suffix.substring(suffix.length() - 1); + if (!"1".equals(loaderCode) && !"2".equals(loaderCode)) { + throw new IllegalArgumentException("Unknown loader code " + loaderCode); + } + String digits = suffix.substring(0, suffix.length() - 1); + String patch = Integer.toString(Integer.parseInt(digits.substring(digits.length() - 2))); + digits = digits.substring(0, digits.length() - 2); + String minor = Integer.toString(Integer.parseInt(digits.substring(digits.length() - 2))); + String major = digits.substring(0, digits.length() - 2); + return "master-" + major + "." + minor + "." + patch + ("2".equals(loaderCode) ? "-neo" : ""); + } + + private static int countOccurrences(String text, String token) { + int count = 0; + int offset = 0; + while ((offset = text.indexOf(token, offset)) >= 0) { + count++; + offset += token.length(); + } + return count; } private static String read(String path) throws Exception { - return new String(Files.readAllBytes(new File(path).toPath()), StandardCharsets.UTF_8); + return new String(Files.readAllBytes(new File(path).toPath()), StandardCharsets.UTF_8) + .replace("\r\n", "\n").replace('\r', '\n'); } } diff --git a/src/test/java/zone/moddev/mc/mineralogy/blocks/MaterialDropContractTest.java b/src/test/java/zone/moddev/mc/mineralogy/blocks/MaterialDropContractTest.java index 4fbebef38..7c592f050 100644 --- a/src/test/java/zone/moddev/mc/mineralogy/blocks/MaterialDropContractTest.java +++ b/src/test/java/zone/moddev/mc/mineralogy/blocks/MaterialDropContractTest.java @@ -106,6 +106,6 @@ private static String mineralogySource() throws Exception { private static String source(String path) throws Exception { return new String(Files.readAllBytes(new File(path).toPath()), - StandardCharsets.UTF_8); + StandardCharsets.UTF_8).replace("\r\n", "\n").replace('\r', '\n'); } }