Skip to content

Merge remote-tracking branch 'origin/master-1.21-lts' into master-26-lts #102

Merge remote-tracking branch 'origin/master-1.21-lts' into master-26-lts

Merge remote-tracking branch 'origin/master-1.21-lts' into master-26-lts #102

Workflow file for this run

name: "CI"
on: [push, pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build
runs-on: ubuntu-latest
permissions:
contents: write
env:
# Lifted to the job so that a step's `if:` can test it. The `secrets` context is not in scope
# for a step-level `if`, and a step's own `env:` block is not in scope for its own `if`
# either -- both read as empty, and the step silently never runs.
RELEASES_DEPLOY_KEY: ${{ secrets.RELEASES_DEPLOY_KEY }}
steps:
- name: 'Checkout'
uses: actions/checkout@v4
- name: 'Setup Java'
uses: actions/setup-java@v4
with:
distribution: 'microsoft'
java-version: 25
- name: 'Setup Gradle'
uses: gradle/actions/setup-gradle@v4
with:
gradle-version: wrapper
cache-read-only: false
- name: 'Check that Fabric declares the access widener'
# A Fabric access widener has no runtime effect unless fabric.mod.json points at it, and
# nothing about the build says so: the mod loads, and the first sign is an
# IllegalAccessError from whichever widened field a caller reaches first.
run: |
AW=loader-common/src/main/resources/clientdevbridge.accesswidener
MOD_JSON=loader-fabric/src/main/resources/fabric.mod.json
if [[ -f "$AW" ]] && ! grep -q '"accessWidener"' "$MOD_JSON"; then
echo "::error file=$MOD_JSON::$AW exists but $MOD_JSON does not declare it."
echo '::error::Add: "accessWidener": "${mod_id}.accesswidener"'
exit 1
fi
- name: 'Mark tag as release'
if: startsWith(github.ref, 'refs/tags/')
run: echo "RELEASE=true" >> $GITHUB_ENV
- name: 'Build'
run: ./gradlew build --no-daemon
env:
MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
MAVEN_KEY: ${{ secrets.MAVEN_KEY }}
GITHUB_USER: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: 'Deploy as GitHub CI artifacts'
uses: actions/upload-artifact@v4
with:
if-no-files-found: error
name: jars
path: loader-*/build/libs/*
- name: 'Deploy to Maven'
if: startsWith(github.ref, 'refs/heads/master')
env:
MAVEN_URL: ${{ secrets.MAVEN_URL }}
MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
MAVEN_KEY: ${{ secrets.MAVEN_KEY }}
run: ./gradlew publish
- name: 'Publish to the releases Maven'
# A static Maven repository committed to CyclopsMC/ClientDevBridge-Releases and served by
# GitHub Pages. That is where clientdevbridge-cli resolves the mod from, and the reason it
# is not the CyclopsMC GitHub Packages Maven is that GitHub Packages needs a token even for
# public packages -- which every consumer would otherwise have to set up before they could
# launch a client.
#
# Authenticated with a deploy key rather than a personal access token. A credential of some
# kind is unavoidable: the automatic GITHUB_TOKEN is scoped to the repository running the
# workflow and cannot push to another one, whoever owns it. A deploy key is the smallest
# credential that does the job -- it grants write to exactly this one repository, belongs to
# no user account, and does not expire.
#
# The releases repository is checked out and published *into*, rather than staged fresh:
# Gradle merges each artifact's maven-metadata.xml with the one already there, and that
# file is what makes dynamic versions ('+') resolvable. Staging into an empty directory
# would silently rewrite the version history down to whatever this run built.
#
# The key is tested in the condition rather than in the script so that a fork, or this
# repository before the secret is configured, skips the step instead of failing the build.
if: startsWith(github.ref, 'refs/heads/master') && env.RELEASES_DEPLOY_KEY != ''
run: |
set -euo pipefail
KEY_FILE="$RUNNER_TEMP/releases_deploy_key"
install -m 600 /dev/null "$KEY_FILE"
printf '%s\n' "$RELEASES_DEPLOY_KEY" > "$KEY_FILE"
mkdir -p ~/.ssh
ssh-keyscan -t rsa,ecdsa,ed25519 github.com >> ~/.ssh/known_hosts 2>/dev/null
# IdentitiesOnly stops ssh from offering any other key the runner happens to have and
# being rejected for too many attempts before it reaches this one.
export GIT_SSH_COMMAND="ssh -i $KEY_FILE -o IdentitiesOnly=yes"
RELEASES_DIR="$RUNNER_TEMP/releases"
# Not a shallow clone: the retry below rebases, and rebasing across a shallow boundary is
# the kind of thing that works until the day it does not.
git clone git@github.com:CyclopsMC/ClientDevBridge-Releases.git "$RELEASES_DIR"
cd "$RELEASES_DIR"
# Read rather than named, so the releases repository can rename its default branch
# without this workflow knowing.
BRANCH="$(git symbolic-ref --short HEAD)"
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
PAGES_MAVEN_DIR="$RELEASES_DIR" "$GITHUB_WORKSPACE/gradlew" -p "$GITHUB_WORKSPACE" publish --no-daemon
if [[ -z "$(git status --porcelain)" ]]; then
echo "Nothing changed in the releases Maven."
exit 0
fi
git add -A
git commit -m "${{ github.repository }}@${GITHUB_SHA::7}: ${GITHUB_REF_NAME} build ${GITHUB_RUN_NUMBER}"
# Three branches publish into this one repository and can finish at the same time. They
# write disjoint paths -- every artifact id carries its Minecraft version -- so a rebase
# always applies cleanly; the retry is for the push race, not for a conflict.
for attempt in 1 2 3 4 5; do
if git push origin "HEAD:$BRANCH"; then
exit 0
fi
echo "Push rejected, rebasing and retrying ($attempt/5)"
sleep $(( attempt * 3 ))
git fetch origin "$BRANCH"
git rebase "origin/$BRANCH"
done
echo "::error::Could not push to the releases Maven after 5 attempts."
exit 1
- name: 'Attach the jars to a GitHub release'
# CI artifacts expire after 90 days and need a login to download; a release asset does
# neither. This is for humans and for pinning a known build -- Gradle still resolves the
# mod from the Maven, and nothing about the artifact names depends on which branch tagged
# them, since every jar carries its Minecraft version and loader.
if: startsWith(github.ref, 'refs/tags/')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${GITHUB_REF_NAME}"
shopt -s nullglob
jars=()
for jar in loader-*/build/libs/*.jar; do
case "$jar" in
*-sources.jar|*-javadoc.jar|*-deobf.jar) continue ;;
esac
jars+=("$jar")
done
if [[ ${#jars[@]} -eq 0 ]]; then
echo "::error::The build produced no jars to attach."
exit 1
fi
# Idempotent: re-running a tag's workflow replaces the assets rather than failing.
gh release view "$TAG" >/dev/null 2>&1 || gh release create "$TAG" \
--title "$TAG" \
--notes "Minecraft $(grep -oP '^minecraft_version=\K.*' gradle.properties). See CHANGELOG.md." \
--verify-tag
gh release upload "$TAG" "${jars[@]}" --clobber
e2e:
name: End to end (${{ matrix.loader }})
runs-on: ubuntu-latest
needs: build
strategy:
fail-fast: false
matrix:
loader: [neoforge, fabric]
steps:
- name: 'Checkout'
uses: actions/checkout@v4
- name: 'Setup Java'
uses: actions/setup-java@v4
with:
distribution: 'microsoft'
java-version: 25
- name: 'Setup Gradle'
uses: gradle/actions/setup-gradle@v4
with:
gradle-version: wrapper
- name: 'Setup Node'
uses: actions/setup-node@v4
with:
node-version: 20
# A real client on a virtual display: xvfb plus Mesa's llvmpipe software rasteriser.
# libgl1-mesa-dri is the one that matters -- without it the client dies during shader
# loading with no useful error.
- name: 'Install the virtual display and software OpenGL'
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq xvfb libgl1-mesa-dri mesa-utils libglu1-mesa
- name: 'Install the CLI'
run: |
git clone --depth 1 https://github.com/CyclopsMC/clientdevbridge-cli /tmp/cli
cd /tmp/cli && npm ci && npm run build && npm link
- name: 'Run the end-to-end suite'
run: ./scripts/e2e.sh ${{ matrix.loader }}
env:
# Present, the suite runs against a real Flopper checkout on the matching branch;
# absent, it falls back to the minimal fixture in e2e/consumer.
MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
MAVEN_KEY: ${{ secrets.MAVEN_KEY }}
GITHUB_USER: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: 'Upload screenshots and diffs'
if: always()
uses: actions/upload-artifact@v4
with:
# Every one of these paths is under `.clientdevbridge`, and upload-artifact skips
# dot-directories unless told otherwise. Without this it silently uploaded nothing and
# reported "No files were found" -- so on exactly the runs worth diagnosing, the game log
# that would explain the failure was thrown away.
include-hidden-files: true
# `warn` rather than `ignore`: a run that captures nothing should say so in the log,
# instead of looking the same as a run that had nothing to capture.
if-no-files-found: warn
name: e2e-${{ matrix.loader }}
path: |
e2e/*/.clientdevbridge/screenshots/**
e2e/*/.clientdevbridge/diffs/**
e2e/*/.clientdevbridge/gradle.log