Repository navigation
Release v1.2.0 requested by @F1xGOD #4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| run-name: >- | |
| ${{ github.event_name == 'release' | |
| && format('Guard accidental publication of {0} by @{1}', github.event.release.tag_name, github.actor) | |
| || format('Release {0} requested by @{1}', inputs.tag, github.actor) }} | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: Existing signed version tag with a draft GitHub Release | |
| required: true | |
| type: string | |
| release: | |
| types: [published] | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: release-${{ inputs.tag }} | |
| cancel-in-progress: false | |
| jobs: | |
| guard-accidental-publish: | |
| name: Return manually published release to draft | |
| if: github.event_name == 'release' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: write | |
| steps: | |
| # GitHub does not recursively emit workflow runs for events created with | |
| # GITHUB_TOKEN. The guarded Release workflow publishes with that token; | |
| # a UI, CLI, PAT, or other manual publication does emit this event. | |
| - name: Re-draft release published outside the guarded workflow | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_ID: ${{ github.event.release.id }} | |
| RELEASE_TAG: ${{ github.event.release.tag_name }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ "$GITHUB_REPOSITORY" == 'YuWan-030/TrueUUID' ]] | |
| [[ "$RELEASE_ID" =~ ^[1-9][0-9]*$ ]] | |
| [[ "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] | |
| release_json=$(gh api "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}") | |
| jq -e --arg tag "$RELEASE_TAG" \ | |
| '.tag_name == $tag and .draft == false' <<<"$release_json" >/dev/null | |
| updated=$(gh api --method PATCH \ | |
| "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" \ | |
| -F draft=true \ | |
| -F prerelease=false \ | |
| -f make_latest=false) | |
| jq -e --arg tag "$RELEASE_TAG" \ | |
| '.tag_name == $tag and .draft == true' <<<"$updated" >/dev/null | |
| { | |
| echo '## Accidental publication blocked' | |
| echo | |
| echo "@${GITHUB_ACTOR} published ${RELEASE_TAG} outside the guarded Release workflow." | |
| echo 'The release was immediately returned to draft. Run the Release workflow from `main` instead.' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| echo "::error title=Release returned to draft::${RELEASE_TAG} was published outside the guarded workflow by @${GITHUB_ACTOR}." | |
| exit 1 | |
| metadata: | |
| name: Validate draft release | |
| if: github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| # Draft releases are deliberately omitted from the releases API for | |
| # read-only identities. This job only reads and freezes draft metadata, but | |
| # its job token must have push-equivalent contents access to see the draft. | |
| permissions: | |
| contents: write | |
| outputs: | |
| approved: ${{ steps.release.outputs.approved }} | |
| release_id: ${{ steps.release.outputs.release_id }} | |
| version: ${{ steps.release.outputs.version }} | |
| tag: ${{ steps.release.outputs.tag }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ inputs.tag }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - id: release | |
| name: Validate tag, changelog, and approvals | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_TAG: ${{ inputs.tag }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ "$GITHUB_REPOSITORY" == 'YuWan-030/TrueUUID' ]] | |
| [[ "$GITHUB_REF" == 'refs/heads/main' ]] | |
| [[ "$RELEASE_TAG" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]] | |
| version="${BASH_REMATCH[1]}" | |
| [[ "$(sed -n 's/^mod_version=//p' gradle.properties)" == "$version" ]] | |
| [[ "$(git cat-file -t "refs/tags/${RELEASE_TAG}")" == tag ]] | |
| tag_object=$(git rev-parse "refs/tags/${RELEASE_TAG}") | |
| [[ "$(gh api "repos/${GITHUB_REPOSITORY}/git/tags/${tag_object}" --jq '.verification.verified')" == true ]] | |
| git fetch --no-tags origin refs/heads/main:refs/remotes/origin/main | |
| tag_commit=$(git rev-list -n 1 "$RELEASE_TAG") | |
| git merge-base --is-ancestor "$tag_commit" refs/remotes/origin/main | |
| ./scripts/release/validate-targets.sh | |
| approved=$(jq -c ' | |
| def loader_name: | |
| if . == "forge" then "Forge" | |
| elif . == "fabric" then "Fabric" | |
| elif . == "neoforge" then "NeoForge" | |
| else error("unsupported loader") | |
| end; | |
| {include: [.targets[] | select(.release == true) | | |
| {target: .id, | |
| loader_name: (.loader | loader_name), | |
| game_version: .game_version}]}' \ | |
| release/targets.json) | |
| jq -e '.include | length > 0' <<<"$approved" >/dev/null | |
| release_json=$(gh api --paginate \ | |
| "repos/${GITHUB_REPOSITORY}/releases?per_page=100" | | |
| jq -sce --arg tag "$RELEASE_TAG" ' | |
| add | | |
| [.[] | select(.tag_name == $tag)] as $matches | | |
| if ($matches | length) == 1 then $matches[0] | |
| elif ($matches | length) == 0 then error("release not found") | |
| else error("release tag is ambiguous") | |
| end') | |
| jq -e --arg tag "$RELEASE_TAG" \ | |
| '.tag_name == $tag and .draft == true and .prerelease == false and | |
| (.body | length > 0)' <<<"$release_json" >/dev/null | |
| jq -j '.body' <<<"$release_json" > release-changelog.md | |
| ./scripts/release/validate-release-config.sh "$version" release-changelog.md | |
| expected_changelog="docs/development/release-changelog-${version}.md" | |
| [[ -f "$expected_changelog" ]] | |
| if ! cmp --silent "$expected_changelog" release-changelog.md; then | |
| echo "The draft body must exactly match ${expected_changelog}." >&2 | |
| exit 73 | |
| fi | |
| echo "Release ${RELEASE_TAG} requested by @${GITHUB_ACTOR}." | |
| echo "approved=$approved" >> "$GITHUB_OUTPUT" | |
| echo "release_id=$(jq -r '.id' <<<"$release_json")" >> "$GITHUB_OUTPUT" | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| echo "tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT" | |
| - name: Freeze the draft GitHub changelog | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: frozen-changelog | |
| path: release-changelog.md | |
| if-no-files-found: error | |
| overwrite: true | |
| retention-days: 14 | |
| full-self-test: | |
| name: Full release self-test | |
| needs: [metadata, publishing-access] | |
| uses: ./.github/workflows/self-test.yml | |
| with: | |
| ref: ${{ needs.metadata.outputs.tag }} | |
| permissions: | |
| contents: read | |
| publishing-access: | |
| name: Identify publisher and validate credentials | |
| needs: metadata | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| environment: release | |
| permissions: | |
| contents: write | |
| outputs: | |
| modrinth_username: ${{ steps.distribution.outputs.modrinth_username }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ needs.metadata.outputs.tag }} | |
| persist-credentials: false | |
| - name: Validate GitHub Release write access | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_ID: ${{ needs.metadata.outputs.release_id }} | |
| RELEASE_TAG: ${{ needs.metadata.outputs.tag }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| release_json=$(gh api "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}") | |
| jq -e --arg tag "$RELEASE_TAG" \ | |
| '.tag_name == $tag and .draft == true and .prerelease == false' \ | |
| <<<"$release_json" >/dev/null | |
| jq '{tag_name, target_commitish, name, body, | |
| draft: true, prerelease: false}' \ | |
| <<<"$release_json" > github-release-write-probe.json | |
| updated=$(gh api --method PATCH \ | |
| "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" \ | |
| --input github-release-write-probe.json) | |
| jq -e --arg tag "$RELEASE_TAG" \ | |
| '.tag_name == $tag and .draft == true and .prerelease == false' \ | |
| <<<"$updated" >/dev/null | |
| echo "Publishing requested by @${GITHUB_ACTOR}; GitHub Release writes use the repository-scoped github-actions[bot] token." | |
| - id: distribution | |
| name: Identify Modrinth publisher and validate distribution access | |
| env: | |
| CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} | |
| MODRINTH_PROJECT_ID: ${{ vars.MODRINTH_PROJECT_ID || secrets.MODRINTH_PROJECT_ID }} | |
| MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| curseforge_project_id=$(jq -r '.curseforge_project_id' release/targets.json) | |
| ./scripts/release/validate-publishing-access.sh "$curseforge_project_id" | |
| attach: | |
| name: Attach approved artifacts | |
| needs: [metadata, full-self-test] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| environment: release | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ needs.metadata.outputs.tag }} | |
| persist-credentials: false | |
| - name: Download all tested artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: release-* | |
| path: tested-artifacts | |
| - name: Collect only release-approved JARs | |
| env: | |
| VERSION: ${{ needs.metadata.outputs.version }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir release-assets | |
| while IFS= read -r target_id; do | |
| target=$(./scripts/release/validate-targets.sh --approved "$target_id") | |
| expected=$(jq -r --arg version "$VERSION" \ | |
| '.artifact | gsub("%VERSION%"; $version)' <<<"$target") | |
| source_dir="tested-artifacts/release-${target_id}" | |
| (cd "$source_dir" && sha256sum --check SHA256SUMS) | |
| source_jar="${source_dir}/$(basename "$expected")" | |
| [[ -f "$source_jar" ]] | |
| cp "$source_jar" release-assets/ | |
| done < <(jq -r '.targets[] | select(.release == true) | .id' release/targets.json) | |
| (cd release-assets && sha256sum -- *.jar > SHA256SUMS) | |
| - name: Attach tested assets to the GitHub Release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_ID: ${{ needs.metadata.outputs.release_id }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ "$RELEASE_ID" =~ ^[1-9][0-9]*$ ]] | |
| for asset in release-assets/*; do | |
| name=$(basename "$asset") | |
| [[ "$name" =~ ^(SHA256SUMS|trueuuid-[0-9]+\.[0-9]+\.[0-9]+-(forge|fabric|neoforge)-[0-9]+\.[0-9]+\.[0-9]+\.jar)$ ]] | |
| while IFS= read -r asset_id; do | |
| [[ "$asset_id" =~ ^[1-9][0-9]*$ ]] | |
| gh api --method DELETE \ | |
| "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" | |
| done < <(gh api \ | |
| "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?per_page=100" \ | |
| --jq ".[] | select(.name == \"${name}\") | .id") | |
| curl --fail --silent --show-error \ | |
| --proto '=https' \ | |
| --tlsv1.2 \ | |
| --connect-timeout 10 \ | |
| --max-time 300 \ | |
| --request POST \ | |
| --header "Authorization: Bearer ${GH_TOKEN}" \ | |
| --header 'Accept: application/vnd.github+json' \ | |
| --header 'X-GitHub-Api-Version: 2022-11-28' \ | |
| --header 'Content-Type: application/octet-stream' \ | |
| --data-binary "@${asset}" \ | |
| "https://uploads.github.com/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${name}" \ | |
| >/dev/null | |
| done | |
| publish-external: | |
| name: Publish ${{ matrix.loader_name }} ${{ matrix.game_version }} (Modrinth @${{ needs.publishing-access.outputs.modrinth_username }}) | |
| needs: [metadata, publishing-access, attach] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| environment: release | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJSON(needs.metadata.outputs.approved) }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ needs.metadata.outputs.tag }} | |
| persist-credentials: false | |
| - name: Download tested target artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: release-${{ matrix.target }} | |
| path: tested-artifact | |
| - name: Download the frozen GitHub changelog | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: frozen-changelog | |
| path: frozen-changelog | |
| - name: Verify artifact and publishing metadata | |
| env: | |
| TARGET_ID: ${{ matrix.target }} | |
| VERSION: ${{ needs.metadata.outputs.version }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| target=$(./scripts/release/validate-targets.sh --approved "$TARGET_ID") | |
| expected=$(jq -r --arg version "$VERSION" \ | |
| '.artifact | gsub("%VERSION%"; $version)' <<<"$target") | |
| loader=$(jq -r '.loader' <<<"$target") | |
| game_version=$(jq -r '.game_version' <<<"$target") | |
| curseforge_project_id=$(jq -r '.curseforge_project_id' release/targets.json) | |
| (cd tested-artifact && sha256sum --check SHA256SUMS) | |
| tested_jar="tested-artifact/$(basename "$expected")" | |
| [[ -f "$tested_jar" ]] | |
| [[ "$curseforge_project_id" =~ ^[1-9][0-9]*$ ]] | |
| mkdir -p "$(dirname "$expected")" | |
| cp "$tested_jar" "$expected" | |
| ./scripts/release/validate-changelog.sh frozen-changelog/release-changelog.md | |
| { | |
| echo "RELEASE_JAR=$expected" | |
| echo "RELEASE_LOADER=$loader" | |
| echo "RELEASE_GAME_VERSION=$game_version" | |
| echo "CURSEFORGE_PROJECT_ID=$curseforge_project_id" | |
| } >> "$GITHUB_ENV" | |
| - name: Publish to Modrinth | |
| env: | |
| MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} | |
| MODRINTH_PROJECT_ID: ${{ vars.MODRINTH_PROJECT_ID || secrets.MODRINTH_PROJECT_ID }} | |
| TARGET_ID: ${{ matrix.target }} | |
| VERSION: ${{ needs.metadata.outputs.version }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ -n "$MODRINTH_TOKEN" ]] | |
| [[ "$MODRINTH_PROJECT_ID" =~ ^[0-9A-Za-z]{8}$ ]] | |
| ./scripts/release/publish-modrinth.sh \ | |
| "$TARGET_ID" "$VERSION" frozen-changelog/release-changelog.md | |
| - id: curseforge-preflight | |
| name: Check for an existing CurseForge artifact | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| state=$(./scripts/release/check-curseforge-artifact.sh \ | |
| "$CURSEFORGE_PROJECT_ID" "$RELEASE_JAR") | |
| case "$state" in | |
| missing) echo "upload=true" >> "$GITHUB_OUTPUT" ;; | |
| identical) | |
| echo "CurseForge already has the identical artifact." | |
| echo "upload=false" >> "$GITHUB_OUTPUT" | |
| ;; | |
| *) | |
| echo "Unexpected CurseForge preflight result: $state" >&2 | |
| exit 69 | |
| ;; | |
| esac | |
| - name: Publish to CurseForge | |
| if: steps.curseforge-preflight.outputs.upload == 'true' | |
| uses: Kir-Antipov/mc-publish@52307b03863581dec6b652b83e597aec02ebb075 # v3.3.1 | |
| with: | |
| curseforge-id: ${{ env.CURSEFORGE_PROJECT_ID }} | |
| curseforge-token: ${{ secrets.CURSEFORGE_TOKEN }} | |
| files: ${{ env.RELEASE_JAR }} | |
| name: TrueUUID ${{ needs.metadata.outputs.version }} for ${{ matrix.loader_name }} ${{ matrix.game_version }} | |
| version: ${{ needs.metadata.outputs.version }}+${{ matrix.target }} | |
| version-type: release | |
| changelog-file: frozen-changelog/release-changelog.md | |
| loaders: ${{ env.RELEASE_LOADER }} | |
| game-versions: ${{ env.RELEASE_GAME_VERSION }} | |
| retry-attempts: 1 | |
| fail-mode: fail | |
| publish-github: | |
| name: Publish GitHub Release | |
| needs: [metadata, publish-external] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| environment: release | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Download the frozen GitHub changelog | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: frozen-changelog | |
| path: frozen-changelog | |
| - name: Publish only after every external upload succeeds | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_ID: ${{ needs.metadata.outputs.release_id }} | |
| RELEASE_TAG: ${{ needs.metadata.outputs.tag }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ "$RELEASE_ID" =~ ^[1-9][0-9]*$ ]] | |
| release_json=$(gh api \ | |
| "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}") | |
| jq -e --arg tag "$RELEASE_TAG" \ | |
| '.tag_name == $tag and .draft == true and .prerelease == false' \ | |
| <<<"$release_json" >/dev/null | |
| jq -j '.body' <<<"$release_json" > current-changelog.md | |
| if ! cmp --silent frozen-changelog/release-changelog.md current-changelog.md; then | |
| echo "The draft changelog changed while the release was running." >&2 | |
| exit 73 | |
| fi | |
| updated=$(gh api --method PATCH \ | |
| "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" \ | |
| -F draft=false \ | |
| -F prerelease=false \ | |
| -f make_latest=true) | |
| jq -e --arg tag "$RELEASE_TAG" \ | |
| '.tag_name == $tag and .draft == false and .prerelease == false' \ | |
| <<<"$updated" >/dev/null |