Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 25 additions & 28 deletions .github/scripts/generate-matrices.sh
Original file line number Diff line number Diff line change
@@ -1,44 +1,41 @@
#!/usr/bin/env bash
# Derives the CI job matrices from supported-versions.json (the single source
# of truth for supported Minecraft versions) and writes them to GITHUB_OUTPUT:
# - build: one entry per build variant -> { mc, java }
# - launch: one entry per launchable version -> { mc, java, pregen_world }
# - publish: one entry per build variant -> { mc, java, game_versions, range }
# pregen_world marks versions whose Fabric API has no client gametest module;
# their launch test needs a pre-generated world (see generate-test-world.sh).
# game_versions is the newline-separated list of Minecraft versions the
# variant's jar covers (fed to mc-publish), range is its human-readable form
# for the release name (e.g. "1.21-1.21.5").
set -euo pipefail

JSON=supported-versions.json
releases="$(curl -sSf https://piston-meta.mojang.com/mc/game/version_manifest_v2.json \
| jq -c '[.versions[] | select(.type == "release") | .id] | reverse')"

java_for_variant() {
grep -oP '^java_version=\K.+' "versions/$1/gradle.properties"
expand_mc_dep() {
jq -nc --argjson releases "$releases" --arg dep "$1" '
def key: split(".") | map(tonumber);
[$releases[] | . as $id | select(
all($dep | split(" ")[] | capture("(?<op>>=|<=|<|>)(?<ver>.+)");
if .op == ">=" then ($id | key) >= (.ver | key)
elif .op == "<=" then ($id | key) <= (.ver | key)
elif .op == ">" then ($id | key) > (.ver | key)
else ($id | key) < (.ver | key) end))]'
}

launch="[]"
while IFS=$'\t' read -r mc variant gametest; do
java="$(java_for_variant "$variant")"
pregen=$([ "$gametest" = "false" ] && echo true || echo false)
launch="$(jq -c --arg mc "$mc" --arg java "$java" --argjson pregen "$pregen" \
'. + [{mc: $mc, java: $java, pregen_world: $pregen}]' <<<"$launch")"
done < <(jq -r 'to_entries[] | [.key, .value.variant, (.value.clientGametest | tostring)] | @tsv' "$JSON")
prop() {
grep -oP "^$2=\K.+" "versions/$1/gradle.properties"
}

build="[]"
launch="[]"
publish="[]"
while read -r variant; do
java="$(java_for_variant "$variant")"
for variant in $(ls versions | sort -V); do
java="$(prop "$variant" java_version)"
mc_versions="$(expand_mc_dep "$(prop "$variant" mc_dep)")"

build="$(jq -c --arg mc "$variant" --arg java "$java" \
'. + [{mc: $mc, java: $java}]' <<<"$build")"
game_versions="$(jq -r --arg v "$variant" \
'[to_entries[] | select(.value.variant == $v) | .key] | join("\n")' "$JSON")"
range="$(jq -r --arg v "$variant" \
'[to_entries[] | select(.value.variant == $v) | .key]
| if length == 1 then .[0] else "\(first)-\(last)" end' "$JSON")"
launch="$(jq -c --argjson mcs "$mc_versions" --arg java "$java" \
'. + [$mcs[] | {mc: ., java: $java}]' <<<"$launch")"

game_versions="$(jq -r 'join("\n")' <<<"$mc_versions")"
range="$(jq -r 'if length == 1 then .[0] else "\(first)-\(last)" end' <<<"$mc_versions")"
publish="$(jq -c --arg mc "$variant" --arg java "$java" --arg gv "$game_versions" --arg range "$range" \
'. + [{mc: $mc, java: $java, game_versions: $gv, range: $range}]' <<<"$publish")"
done < <(jq -r '[.[].variant] | reduce .[] as $v ([]; if index($v) then . else . + [$v] end) | .[]' "$JSON")
done

echo "build=$build" >> "$GITHUB_OUTPUT"
echo "launch=$launch" >> "$GITHUB_OUTPUT"
Expand Down
13 changes: 1 addition & 12 deletions .github/scripts/generate-test-world.sh
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
#!/usr/bin/env bash
# Generates a vanilla singleplayer world save for the given Minecraft version
# by briefly running that version's dedicated server, and places it where the
# launch test expects it (launchtest/run/saves/ci-world).
#
# Only needed for versions whose Fabric API has no client gametest module
# (before 1.21.4): their launch test cannot create a world in-game, so the
# client auto-joins this save via --quickPlaySingleplayer instead. Using the
# exact same Minecraft version to generate the world avoids any world-upgrade
# prompts when the client opens it.
set -euo pipefail

MC_VERSION="${1:?usage: generate-test-world.sh <minecraft-version>}"
DEST="$(pwd)/launchtest/run/saves/ci-world"
DEST="$(pwd)/src/launchtest/run/saves/ci-world"

WORK="$(mktemp -d)"
cd "$WORK"
Expand All @@ -22,8 +13,6 @@ SERVER_URL="$(curl -sSf "$MANIFEST_URL" | jq -r '.downloads.server.url')"
curl -sSfo server.jar "$SERVER_URL"

echo "eula=true" > eula.txt
# A flat world keeps generation fast and renders instantly on the CI software
# renderer. The default gamemode (survival) matches the gametest-based runs.
cat > server.properties <<'EOF'
level-name=ci-world
level-type=minecraft\:flat
Expand Down
23 changes: 4 additions & 19 deletions .github/scripts/publish-screenshots.sh
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
#!/usr/bin/env bash
# Collects the launch-test screenshots uploaded as artifacts by every matrix
# job of the current workflow run, normalizes their names, and commits them to
# the ci-screenshots branch so they can be embedded (as raw.githubusercontent
# URLs) in a PR comment and in the job summary.
#
# Leaves the normalized screenshots in screenshots-publish/ for the comment
# step: <sha>/mc-<version>-survival.png and <sha>/mc-<version>-title.png.
#
# Required env: GITHUB_TOKEN, GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_SHA
set -euo pipefail

api() {
curl -sSf -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/vnd.github+json" "$@"
}

# --- Download every launch-screenshots-mc* artifact of this run -------------
mkdir -p artifact-zips shots
api "https://api.github.com/repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts?per_page=100" > artifacts.json

Expand All @@ -28,18 +18,15 @@ while IFS=: read -r id name; do
unzip -oq "artifact-zips/$id.zip" -d "shots/$mc"
done < <(jq -r '.artifacts[] | select(.name | startswith("launch-screenshots-mc")) | "\(.id):\(.name)"' artifacts.json)

# --- Normalize names (gametest screenshots carry a timestamp prefix) --------
PUBLISH="screenshots-publish/$GITHUB_SHA"
mkdir -p "$PUBLISH"

for dir in shots/*/; do
mc="$(basename "$dir")"
for kind in survival-world title-screen; do
src="$(find "$dir" -name "*betterhud-$kind.png" -print -quit)"
if [ -n "$src" ]; then
cp "$src" "$PUBLISH/mc-$mc-${kind%%-*}.png"
fi
done
src="$(find "$dir" -name "betterhud-survival-world.png" -print -quit)"
if [ -n "$src" ]; then
cp "$src" "$PUBLISH/mc-$mc-survival.png"
fi
done

count="$(find "$PUBLISH" -name '*.png' | wc -l)"
Expand All @@ -49,7 +36,6 @@ if [ "$count" -eq 0 ]; then
exit 0
fi

# --- Commit to the ci-screenshots branch ------------------------------------
REMOTE="https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
if ! git clone --quiet --depth 1 --branch ci-screenshots "$REMOTE" ci-screenshots-branch 2>/dev/null; then
mkdir -p ci-screenshots-branch
Expand All @@ -68,7 +54,6 @@ fi
git -c user.name="github-actions[bot]" -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \
commit --quiet -m "CI screenshots for $GITHUB_SHA"

# Retry the push in case a concurrent run updated the branch first.
for _ in 1 2 3; do
if git push --quiet origin ci-screenshots; then
echo "Published screenshots to ci-screenshots/$GITHUB_SHA"
Expand Down
50 changes: 13 additions & 37 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,6 @@
# Build the mod for every Stonecutter variant, then launch-test the built jars
# on EVERY supported Minecraft version concurrently: each launch job boots a
# real production Fabric client under XVFB with the covering variant's jar and
# version-matched runtime mods, enters a survival singleplayer world with the
# HUD active, screenshots it, and fails if anything crashes on the way.
# The screenshot report job embeds all screenshots in a PR comment (via the
# ci-screenshots branch) and in the job summary, and the modrinth bundle job
# packages the release jars with their per-jar Minecraft version lists.
#
# Both job matrices are generated from supported-versions.json — add a version
# there (see README) and every job picks it up.

name: build
on: push

# contents: write lets the screenshot report push to the ci-screenshots branch,
# pull-requests: write lets it comment on the associated pull request.
permissions:
contents: write
pull-requests: write
Expand All @@ -30,7 +16,7 @@ jobs:
- name: checkout repository
uses: actions/checkout@v6

- name: generate matrices from supported-versions.json
- name: generate matrices
id: gen
run: bash .github/scripts/generate-matrices.sh

Expand Down Expand Up @@ -67,14 +53,6 @@ jobs:
name: betterhud-mc${{ matrix.mc }}
path: versions/${{ matrix.mc }}/build/libs/*.jar

# One job per supported Minecraft version (not per build variant): the
# 1.21 variant jar is launch-tested on 1.21-1.21.5, the 1.21.6 variant jar
# on 1.21.6-1.21.11, and so on.
#
# pregen_world marks versions whose Fabric API has no client gametest module
# (before 1.21.4): for those a vanilla world save is generated first with
# that version's dedicated server, and the client auto-joins it instead of
# creating one through the gametest API.
launch-test:
name: launch mc${{ matrix.mc }}
runs-on: ubuntu-24.04
Expand All @@ -101,34 +79,31 @@ jobs:
run: chmod +x ./gradlew

- name: install xvfb
# Provides a virtual display so the Minecraft client can launch headlessly.
run: sudo apt-get update && sudo apt-get install -y xvfb

- name: generate singleplayer test world
if: ${{ matrix.pregen_world }}
run: bash .github/scripts/generate-test-world.sh ${{ matrix.mc }}

- name: launch minecraft ${{ matrix.mc }} with mod
run: ./gradlew :launchtest:runProductionClientGameTest -PtestMcVersion=${{ matrix.mc }} --stacktrace
run: ./gradlew :launchtest:runLaunchTest -PtestMcVersion=${{ matrix.mc }} --stacktrace

- name: check survival world screenshot was taken
run: test -n "$(find launchtest/run/screenshots -name '*betterhud-survival-world.png' -print -quit 2>/dev/null)"
run: test -n "$(find src/launchtest/run/screenshots -name 'betterhud-survival-world.png' -print -quit 2>/dev/null)"

- name: upload screenshots
# Consumed by the screenshot-report job below.
if: always()
uses: actions/upload-artifact@v7
with:
name: launch-screenshots-mc${{ matrix.mc }}
path: launchtest/run/screenshots/
path: src/launchtest/run/screenshots/
if-no-files-found: ignore

- name: upload game logs
if: always()
uses: actions/upload-artifact@v7
with:
name: launch-logs-mc${{ matrix.mc }}
path: launchtest/run/logs/
path: src/launchtest/run/logs/
if-no-files-found: ignore

screenshot-report:
Expand All @@ -151,10 +126,16 @@ jobs:
script: |
const fs = require('fs');

const versions = Object.keys(JSON.parse(fs.readFileSync('supported-versions.json', 'utf8')));
const dir = `screenshots-publish/${context.sha}`;
const base = `https://raw.githubusercontent.com/${context.repo.owner}/${context.repo.repo}/ci-screenshots/${context.sha}`;

const versions = fs.existsSync(dir)
? fs.readdirSync(dir)
.filter(f => f.startsWith('mc-') && f.endsWith('-survival.png'))
.map(f => f.slice(3, -13))
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
: [];

const cells = versions.map(v => {
const file = `mc-${v}-survival.png`;
const ok = fs.existsSync(`${dir}/${file}`);
Expand All @@ -170,12 +151,11 @@ jobs:
const marker = '<!-- betterhud-launch-screenshots -->';
const body = [
marker,
`The game launched and entered a survival singleplayer world with the HUD active, on every supported Minecraft version, for \`${context.sha.slice(0, 7)}\`. A ❌ means that version's launch test did not produce a screenshot — check its job. Title-screen screenshots and game logs are in the run artifacts.`,
`# BetterHUD Launch Screenshots`,
'',
`<table>\n${rows}</table>`,
].join('\n');

// Always show the grid in the job summary, even without a PR.
await core.summary.addRaw(body).write();

const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
Expand All @@ -189,7 +169,6 @@ jobs:
return;
}

// Keep a single comment up to date instead of posting one per push.
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: pr.number, per_page: 100,
Expand All @@ -207,9 +186,6 @@ jobs:
});
}

# Builds every variant and bundles the release jars together with UPLOAD.md,
# which lists exactly which Minecraft versions to select for each jar when
# uploading to Modrinth. Download the "modrinth-upload" artifact and follow it.
modrinth-bundle:
name: modrinth bundle
runs-on: ubuntu-24.04
Expand Down
28 changes: 1 addition & 27 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,3 @@
# Publishes the mod to Modrinth and CurseForge on demand: bump `mod_version`
# in gradle.properties, push, then run this workflow from the Actions tab.
# It builds every Stonecutter variant and uploads each release jar as its own
# version on both platforms, selecting exactly the Minecraft versions that jar
# covers — driven by supported-versions.json, the same single source of truth
# the build and launch-test matrices use.
#
# Required repository secrets (Settings -> Secrets and variables -> Actions):
# - MODRINTH_TOKEN: Modrinth PAT with the "Create versions" scope
# (https://modrinth.com/settings/pats)
# - CURSEFORGE_TOKEN: CurseForge API token
# (https://legacy.curseforge.com/account/api-tokens)
#
# This workflow only builds; correctness on every supported Minecraft version
# is already covered by the launch tests that run on every push (build.yml),
# so make sure the commit you release from is green there first.

name: publish
on:
workflow_dispatch:
Expand Down Expand Up @@ -45,7 +28,6 @@ on:
permissions:
contents: read

# Never let two release runs interleave their uploads.
concurrency:
group: publish
cancel-in-progress: false
Expand All @@ -61,7 +43,7 @@ jobs:
- name: checkout repository
uses: actions/checkout@v6

- name: generate matrices from supported-versions.json
- name: generate matrices
id: gen
run: bash .github/scripts/generate-matrices.sh

Expand All @@ -73,9 +55,6 @@ jobs:
echo "Releasing BetterHUD $version"

- name: check the required tokens are configured
# mc-publish silently skips a platform whose token is empty, which
# would make a run "succeed" while publishing nothing — fail early
# instead if a selected platform has no token.
env:
PLATFORMS: ${{ inputs.platforms }}
MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }}
Expand All @@ -93,9 +72,6 @@ jobs:
runs-on: ubuntu-24.04
needs: matrices
strategy:
# Publish one variant at a time, oldest first (supported-versions.json
# order), so the newest-Minecraft version ends up as the most recently
# uploaded — and a failure stops before uploading the rest.
max-parallel: 1
fail-fast: true
matrix:
Expand All @@ -122,8 +98,6 @@ jobs:
- name: publish to modrinth and curseforge
uses: Kir-Antipov/mc-publish@v3.3
with:
# An empty token makes mc-publish skip that platform, which is how
# the "platforms" input narrows the run to a single site.
modrinth-id: betterhudfabric
modrinth-token: ${{ inputs.platforms != 'curseforge' && secrets.MODRINTH_TOKEN || '' }}
curseforge-id: 1375095
Expand Down
Loading