Skip to content

Release v1.2.0 requested by @F1xGOD #4

Release v1.2.0 requested by @F1xGOD

Release v1.2.0 requested by @F1xGOD #4

Workflow file for this run

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