Merge remote-tracking branch 'origin/master-1.21-lts' into master-26-lts #102
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: "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 |