From 1aaf5f340f47f8c17f9c681eee290717ae1b8605 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 23:12:41 +0000 Subject: [PATCH 1/7] add screenshot CI workflow Launches Minecraft with the mod on each push/PR, loads a singleplayer flat world via --quickPlaySingleplayer, takes a screenshot with scrot, and posts the image inline as a GitHub comment on the PR or commit. World is pre-generated by the Fabric server before the client runs. Screenshot is committed to a `screenshots` branch so the image URL renders inline without requiring artifact auth. https://claude.ai/code/session_01Lg4VQQXs3q3WW4aMoe3Xci --- .github/workflows/screenshot.yml | 276 +++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 .github/workflows/screenshot.yml diff --git a/.github/workflows/screenshot.yml b/.github/workflows/screenshot.yml new file mode 100644 index 0000000..c0414bd --- /dev/null +++ b/.github/workflows/screenshot.yml @@ -0,0 +1,276 @@ +name: screenshot test + +on: + pull_request: + push: + branches: [main, master] + +jobs: + screenshot: + runs-on: ubuntu-24.04 + permissions: + contents: write + pull-requests: write + issues: write + + steps: + - name: checkout repository + uses: actions/checkout@v6 + + - name: validate gradle wrapper + uses: gradle/actions/wrapper-validation@v6 + + - name: setup jdk + uses: actions/setup-java@v5 + with: + java-version: '25' + distribution: 'microsoft' + + - name: make gradle wrapper executable + run: chmod +x ./gradlew + + - name: build mod + run: ./gradlew build + + - name: install display tools + run: | + sudo apt-get update -qq + sudo apt-get install -y xvfb scrot fluxbox x11-utils + + - name: start virtual display + run: | + Xvfb :99 -screen 0 1920x1080x24 & + DISPLAY=:99 fluxbox &>/dev/null & + sleep 2 + echo "DISPLAY=:99" >> $GITHUB_ENV + + - name: prepare run directory + run: | + mkdir -p run/saves + + # Accept EULA so the server can generate a world + echo "eula=true" > run/eula.txt + + # Fast flat world, offline mode + cat > run/server.properties << 'EOF' + level-name=world + gamemode=creative + generate-structures=false + level-type=flat + spawn-protection=0 + view-distance=4 + simulation-distance=4 + online-mode=false + max-tick-time=60000 + EOF + + # Low-quality client settings so Minecraft loads faster in CI + cat > run/options.txt << 'EOF' + autoJump:false + renderDistance:4 + simulationDistance:4 + graphicsMode:0 + ao:false + bobView:false + guiScale:2 + fullscreen:false + EOF + + - name: generate world via server + run: | + ./gradlew runServer --args="--nogui" > /tmp/server.log 2>&1 & + SERVER_PID=$! + + echo "Waiting for server to finish world generation..." + TIMEOUT=180 + ELAPSED=0 + until grep -q "Done" /tmp/server.log 2>/dev/null || [ "$ELAPSED" -ge "$TIMEOUT" ]; do + sleep 3 + ELAPSED=$((ELAPSED + 3)) + done + + if grep -q "Done" /tmp/server.log 2>/dev/null; then + echo "World generated after ${ELAPSED}s" + else + echo "Server timed out — continuing anyway" + tail -30 /tmp/server.log + fi + + kill "$SERVER_PID" 2>/dev/null + wait "$SERVER_PID" 2>/dev/null || true + + # Copy server world to client saves + if [ -d "run/world" ]; then + cp -r run/world "run/saves/CI_World" + echo "Copied run/world -> run/saves/CI_World" + else + echo "WARNING: server world not found, client will show main menu" + ls run/ || true + fi + + - name: launch minecraft client + run: | + ./gradlew runClient \ + --args="--username CIBot --uuid 00000000-0000-0000-0000-000000000001 --quickPlaySingleplayer CI_World" \ + > /tmp/client.log 2>&1 & + CLIENT_PID=$! + echo "CLIENT_PID=$CLIENT_PID" >> "$GITHUB_ENV" + + # Verify it didn't crash immediately + sleep 5 + if ! kill -0 "$CLIENT_PID" 2>/dev/null; then + echo "ERROR: Minecraft crashed on launch" + tail -50 /tmp/client.log + else + echo "Client running (PID $CLIENT_PID), waiting 120s for world to load..." + sleep 120 + fi + + - name: take screenshot + if: always() + run: | + DISPLAY=:99 scrot /tmp/mc-screenshot.png + echo "Screenshot captured" + ls -lh /tmp/mc-screenshot.png + + - name: stop minecraft + if: always() + run: | + [ -n "$CLIENT_PID" ] && kill "$CLIENT_PID" 2>/dev/null || true + pkill -f 'net.minecraft' 2>/dev/null || true + + - name: upload screenshot artifact + if: always() + uses: actions/upload-artifact@v7 + with: + name: minecraft-screenshot + path: /tmp/mc-screenshot.png + if-no-files-found: warn + + - name: upload logs artifact + if: always() + uses: actions/upload-artifact@v7 + with: + name: minecraft-logs + path: | + /tmp/server.log + /tmp/client.log + run/logs/ + if-no-files-found: ignore + + - name: publish screenshot to screenshots branch + if: always() + run: | + if [ ! -f /tmp/mc-screenshot.png ]; then + echo "No screenshot file found, skipping upload" + exit 0 + fi + + FILENAME="screenshots/${{ github.sha }}.png" + COMMIT_MSG="Screenshot for ${{ github.sha }}" + + # Create the screenshots branch if it doesn't exist + BRANCH_EXISTS=$(curl -sf \ + -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/branches/screenshots" \ + | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('name',''))" 2>/dev/null || echo "") + + if [ -z "$BRANCH_EXISTS" ]; then + echo "Creating screenshots branch..." + HEAD_SHA=$(git rev-parse HEAD) + curl -sf -X POST \ + -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + -H "Content-Type: application/json" \ + "https://api.github.com/repos/${{ github.repository }}/git/refs" \ + -d "{\"ref\":\"refs/heads/screenshots\",\"sha\":\"${HEAD_SHA}\"}" > /dev/null \ + && echo "Branch created" || echo "Branch creation failed (may already exist)" + fi + + # Get current file SHA if it already exists (required for updates) + EXISTING_SHA=$(curl -sf \ + -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/contents/${FILENAME}?ref=screenshots" \ + | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('sha',''))" 2>/dev/null || echo "") + + # Build JSON body using Python to safely handle large base64 content + python3 - << PYEOF + import json, base64 + + with open("/tmp/mc-screenshot.png", "rb") as f: + content = base64.b64encode(f.read()).decode() + + body = { + "message": "$COMMIT_MSG", + "content": content, + "branch": "screenshots", + } + + existing_sha = "$EXISTING_SHA" + if existing_sha: + body["sha"] = existing_sha + + with open("/tmp/upload-body.json", "w") as f: + json.dump(body, f) + + print(f"Upload body written ({len(content)} chars of base64)") + PYEOF + + # Upload to GitHub Contents API + HTTP_STATUS=$(curl -sf -o /tmp/upload-response.json -w "%{http_code}" \ + -X PUT \ + -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + -H "Content-Type: application/json" \ + -d @/tmp/upload-body.json \ + "https://api.github.com/repos/${{ github.repository }}/contents/${FILENAME}" \ + || echo "000") + + if [ "$HTTP_STATUS" -ge 200 ] && [ "$HTTP_STATUS" -lt 300 ]; then + echo "Screenshot uploaded (HTTP $HTTP_STATUS)" + else + echo "Upload failed (HTTP $HTTP_STATUS)" + cat /tmp/upload-response.json || true + fi + + - name: post screenshot comment + if: always() + uses: actions/github-script@v7 + with: + script: | + const sha = context.sha; + const repo = `${context.repo.owner}/${context.repo.repo}`; + const runId = context.runId; + + const screenshotUrl = + `https://raw.githubusercontent.com/${repo}/screenshots/screenshots/${sha}.png`; + const runUrl = + `https://github.com/${repo}/actions/runs/${runId}`; + + const body = [ + '## Minecraft Screenshot', + '', + `![BetterHUD in Minecraft](${screenshotUrl})`, + '', + `> Commit \`${sha.slice(0, 7)}\` — [View CI Run & Artifacts](${runUrl})`, + ].join('\n'); + + try { + if (context.eventName === 'pull_request') { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body, + }); + console.log('Posted comment on PR #' + context.payload.pull_request.number); + } else { + await github.rest.repos.createCommitComment({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: sha, + body, + }); + console.log('Posted comment on commit ' + sha.slice(0, 7)); + } + } catch (err) { + core.warning('Failed to post comment: ' + err.message); + } From 5074a984c9dd38992071206354141ccb9d689041 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 23:18:37 +0000 Subject: [PATCH 2/7] fix: install Mesa software GL so Minecraft renders in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The screenshot was capturing only the bare Fluxbox desktop because Minecraft crashed silently — GitHub Actions runners have no GPU, so OpenGL calls fail without Mesa's software renderer. - Install libgl1-mesa-dri, libgles2-mesa, mesa-vulkan-drivers - Set LIBGL_ALWAYS_SOFTWARE=1 + GALLIUM_DRIVER=llvmpipe - Enable GLX extension on the Xvfb display - Install xdotool and wait for the actual Minecraft window to appear (via xdotool search --name) instead of blindly sleeping 120s - Only wait 30s after the window appears (world already loading) - Exit early with logs if the client crashes immediately https://claude.ai/code/session_01Lg4VQQXs3q3WW4aMoe3Xci --- .github/workflows/screenshot.yml | 33 ++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/.github/workflows/screenshot.yml b/.github/workflows/screenshot.yml index c0414bd..f6f322e 100644 --- a/.github/workflows/screenshot.yml +++ b/.github/workflows/screenshot.yml @@ -35,14 +35,20 @@ jobs: - name: install display tools run: | sudo apt-get update -qq - sudo apt-get install -y xvfb scrot fluxbox x11-utils + sudo apt-get install -y \ + xvfb scrot fluxbox x11-utils xdotool \ + libgl1-mesa-dri libgles2-mesa mesa-vulkan-drivers \ + libegl1-mesa libopengl0 - name: start virtual display run: | - Xvfb :99 -screen 0 1920x1080x24 & + Xvfb :99 -screen 0 1920x1080x24 +extension GLX +render -ac & DISPLAY=:99 fluxbox &>/dev/null & sleep 2 echo "DISPLAY=:99" >> $GITHUB_ENV + # Force Mesa software renderer — no GPU available in CI + echo "LIBGL_ALWAYS_SOFTWARE=1" >> $GITHUB_ENV + echo "GALLIUM_DRIVER=llvmpipe" >> $GITHUB_ENV - name: prepare run directory run: | @@ -117,13 +123,28 @@ jobs: echo "CLIENT_PID=$CLIENT_PID" >> "$GITHUB_ENV" # Verify it didn't crash immediately - sleep 5 + sleep 8 if ! kill -0 "$CLIENT_PID" 2>/dev/null; then echo "ERROR: Minecraft crashed on launch" - tail -50 /tmp/client.log + tail -80 /tmp/client.log + exit 1 + fi + + # Wait up to 3 minutes for a Minecraft window to actually appear + echo "Waiting for Minecraft window..." + TIMEOUT=180 + ELAPSED=0 + until xdotool search --name "Minecraft" 2>/dev/null | grep -q . || [ "$ELAPSED" -ge "$TIMEOUT" ]; do + sleep 3 + ELAPSED=$((ELAPSED + 3)) + done + + if xdotool search --name "Minecraft" 2>/dev/null | grep -q .; then + echo "Minecraft window appeared after ${ELAPSED}s — waiting 30s for world to finish loading" + sleep 30 else - echo "Client running (PID $CLIENT_PID), waiting 120s for world to load..." - sleep 120 + echo "WARNING: No Minecraft window detected after ${TIMEOUT}s — taking screenshot anyway" + tail -30 /tmp/client.log fi - name: take screenshot From d60cdd27b392689396a9205104ff8b5cd76e4bda Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 23:22:11 +0000 Subject: [PATCH 3/7] fix: add missing LWJGL/audio deps, remove hard exit on crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two causes of the immediate Minecraft crash in CI: 1. Missing LWJGL/GLFW X11 libraries — LWJGL 3 needs libxrandr2, libxcursor1, libxinerama1, libxi6, libxxf86vm1 in addition to the base Mesa packages. Any missing lib causes a SIGSEGV before the window ever opens. 2. OpenAL audio crash — without a sound device, LWJGL's audio init fails. Add libopenal1 and set ALSOFT_DRIVERS=null + SDL_AUDIODRIVER=dummy to suppress audio entirely on headless runners. Also pass all rendering/audio env vars directly on the runClient command line so they reach the forked JVM regardless of Gradle daemon caching, and remove the hard `exit 1` on early crash so the job stays green and still uploads a diagnostic screenshot + logs. https://claude.ai/code/session_01Lg4VQQXs3q3WW4aMoe3Xci --- .github/workflows/screenshot.yml | 62 +++++++++++++++++++------------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/.github/workflows/screenshot.yml b/.github/workflows/screenshot.yml index f6f322e..e5084b7 100644 --- a/.github/workflows/screenshot.yml +++ b/.github/workflows/screenshot.yml @@ -35,20 +35,27 @@ jobs: - name: install display tools run: | sudo apt-get update -qq + # Mesa software GL + full LWJGL/GLFW X11 + audio deps sudo apt-get install -y \ xvfb scrot fluxbox x11-utils xdotool \ libgl1-mesa-dri libgles2-mesa mesa-vulkan-drivers \ - libegl1-mesa libopengl0 + libegl1-mesa libopengl0 \ + libx11-6 libxext6 libxrender1 libxrandr2 \ + libxcursor1 libxinerama1 libxi6 libxxf86vm1 \ + libopenal1 pulseaudio-utils - name: start virtual display run: | Xvfb :99 -screen 0 1920x1080x24 +extension GLX +render -ac & DISPLAY=:99 fluxbox &>/dev/null & sleep 2 - echo "DISPLAY=:99" >> $GITHUB_ENV - # Force Mesa software renderer — no GPU available in CI + echo "DISPLAY=:99" >> $GITHUB_ENV + # Mesa software renderer — no GPU in CI echo "LIBGL_ALWAYS_SOFTWARE=1" >> $GITHUB_ENV echo "GALLIUM_DRIVER=llvmpipe" >> $GITHUB_ENV + # Disable audio so OpenAL doesn't crash LWJGL on a headless runner + echo "ALSOFT_DRIVERS=null" >> $GITHUB_ENV + echo "SDL_AUDIODRIVER=dummy" >> $GITHUB_ENV - name: prepare run directory run: | @@ -116,35 +123,42 @@ jobs: - name: launch minecraft client run: | + # Pass all rendering/audio env vars explicitly so they reach the + # forked JVM even if the Gradle daemon was started earlier + DISPLAY=:99 \ + LIBGL_ALWAYS_SOFTWARE=1 \ + GALLIUM_DRIVER=llvmpipe \ + ALSOFT_DRIVERS=null \ + SDL_AUDIODRIVER=dummy \ ./gradlew runClient \ --args="--username CIBot --uuid 00000000-0000-0000-0000-000000000001 --quickPlaySingleplayer CI_World" \ > /tmp/client.log 2>&1 & CLIENT_PID=$! echo "CLIENT_PID=$CLIENT_PID" >> "$GITHUB_ENV" - # Verify it didn't crash immediately - sleep 8 + # Give the JVM a moment, then check for early crash + sleep 10 if ! kill -0 "$CLIENT_PID" 2>/dev/null; then - echo "ERROR: Minecraft crashed on launch" - tail -80 /tmp/client.log - exit 1 - fi - - # Wait up to 3 minutes for a Minecraft window to actually appear - echo "Waiting for Minecraft window..." - TIMEOUT=180 - ELAPSED=0 - until xdotool search --name "Minecraft" 2>/dev/null | grep -q . || [ "$ELAPSED" -ge "$TIMEOUT" ]; do - sleep 3 - ELAPSED=$((ELAPSED + 3)) - done - - if xdotool search --name "Minecraft" 2>/dev/null | grep -q .; then - echo "Minecraft window appeared after ${ELAPSED}s — waiting 30s for world to finish loading" - sleep 30 + echo "WARNING: Minecraft process exited within 10s — showing last log lines:" + tail -100 /tmp/client.log + echo "Taking screenshot of whatever is on screen for diagnostics" else - echo "WARNING: No Minecraft window detected after ${TIMEOUT}s — taking screenshot anyway" - tail -30 /tmp/client.log + # Wait up to 3 minutes for a Minecraft window to actually appear + echo "Client running (PID $CLIENT_PID) — waiting for Minecraft window..." + TIMEOUT=180 + ELAPSED=0 + until xdotool search --name "Minecraft" 2>/dev/null | grep -q . || [ "$ELAPSED" -ge "$TIMEOUT" ]; do + sleep 3 + ELAPSED=$((ELAPSED + 3)) + done + + if xdotool search --name "Minecraft" 2>/dev/null | grep -q .; then + echo "Minecraft window appeared after ${ELAPSED}s — waiting 30s for world to load" + sleep 30 + else + echo "WARNING: No Minecraft window after ${TIMEOUT}s — taking diagnostic screenshot" + tail -30 /tmp/client.log + fi fi - name: take screenshot From 3dbc3013fd08baaa6d90733378a252423abda750 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 23:26:13 +0000 Subject: [PATCH 4/7] fix: kill with || true so set -e doesn't abort world generation step GitHub Actions runs each `run:` block under `set -e`. The bare `kill "$SERVER_PID"` in the generate-world step returned exit code 1 whenever the Gradle/server process had already exited naturally, which caused the entire step to fail. That in turn skipped the `launch minecraft client` step (no `if: always()`), so Minecraft never started and scrot captured only the bare Fluxbox desktop. - Add `|| true` to `kill "$SERVER_PID"` so the step never fails on process-already-gone errors - Add `if: always()` to the launch step so it runs even if world generation reports a failure https://claude.ai/code/session_01Lg4VQQXs3q3WW4aMoe3Xci --- .github/workflows/screenshot.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/screenshot.yml b/.github/workflows/screenshot.yml index e5084b7..0d95369 100644 --- a/.github/workflows/screenshot.yml +++ b/.github/workflows/screenshot.yml @@ -109,7 +109,7 @@ jobs: tail -30 /tmp/server.log fi - kill "$SERVER_PID" 2>/dev/null + kill "$SERVER_PID" 2>/dev/null || true wait "$SERVER_PID" 2>/dev/null || true # Copy server world to client saves @@ -122,6 +122,7 @@ jobs: fi - name: launch minecraft client + if: always() run: | # Pass all rendering/audio env vars explicitly so they reach the # forked JVM even if the Gradle daemon was started earlier From 9442c9b118fcd3855196b848621b1d7a1a775582 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 8 May 2026 00:01:48 +0000 Subject: [PATCH 5/7] fix: make screenshot job non-blocking and harden fragile steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three remaining failure modes under set -eo pipefail: 1. Job was blocking PRs on failure — add continue-on-error: true at the job level (screenshots are informational, not quality gates). 2. scrot exits non-zero when the X display has any issue, failing the take-screenshot step under set -e. The if: always() steps then run and post a comment with a broken image URL. Added explicit || {} so scrot failure is logged but never propagates as a step failure. 3. Python heredoc (< /tmp/server.log 2>&1 & SERVER_PID=$! @@ -165,9 +168,10 @@ jobs: - name: take screenshot if: always() run: | - DISPLAY=:99 scrot /tmp/mc-screenshot.png - echo "Screenshot captured" - ls -lh /tmp/mc-screenshot.png + # scrot exits non-zero if X has any issue — don't let that fail the job + DISPLAY=:99 scrot /tmp/mc-screenshot.png 2>&1 \ + || { echo "WARNING: scrot failed (exit $?), display may not be ready"; } + ls -lh /tmp/mc-screenshot.png 2>/dev/null || echo "No screenshot file produced" - name: stop minecraft if: always() @@ -228,28 +232,22 @@ jobs: "https://api.github.com/repos/${{ github.repository }}/contents/${FILENAME}?ref=screenshots" \ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('sha',''))" 2>/dev/null || echo "") - # Build JSON body using Python to safely handle large base64 content - python3 - << PYEOF - import json, base64 - - with open("/tmp/mc-screenshot.png", "rb") as f: - content = base64.b64encode(f.read()).decode() - - body = { - "message": "$COMMIT_MSG", - "content": content, - "branch": "screenshots", - } - - existing_sha = "$EXISTING_SHA" - if existing_sha: - body["sha"] = existing_sha - - with open("/tmp/upload-body.json", "w") as f: - json.dump(body, f) - - print(f"Upload body written ({len(content)} chars of base64)") - PYEOF + # Write Python helper with single-quoted heredoc (no shell substitution), + # then pass variables as argv so there are no quoting/expansion issues. + cat > /tmp/make_body.py << 'PYEOF' +import json, base64, sys +img_path, commit_msg, existing_sha = sys.argv[1], sys.argv[2], sys.argv[3] +with open(img_path, "rb") as f: + content = base64.b64encode(f.read()).decode() +body = {"message": commit_msg, "content": content, "branch": "screenshots"} +if existing_sha: + body["sha"] = existing_sha +with open("/tmp/upload-body.json", "w") as f: + json.dump(body, f) +print(f"Upload body: {len(content)} base64 chars") +PYEOF + python3 /tmp/make_body.py "/tmp/mc-screenshot.png" "$COMMIT_MSG" "$EXISTING_SHA" \ + || { echo "WARNING: failed to build upload body"; exit 0; } # Upload to GitHub Contents API HTTP_STATUS=$(curl -sf -o /tmp/upload-response.json -w "%{http_code}" \ From 29b34b0bcbf7b3eba520ec96a6bc046cc4efc5a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 8 May 2026 00:09:51 +0000 Subject: [PATCH 6/7] fix: indent Python heredoc content to satisfy YAML block scalar rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python script lines at column 0 inside a run: | block scalar caused the YAML parser to treat them as ending the block, making the entire workflow file unparseable — so the action never ran at all. YAML block scalars require every content line to be indented at least as deep as the block's own indentation (10 spaces here). GitHub Actions strips that minimum indentation before passing the script to bash, so the heredoc writes the Python file with correct zero-based indentation. https://claude.ai/code/session_01Lg4VQQXs3q3WW4aMoe3Xci --- .github/workflows/screenshot.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/screenshot.yml b/.github/workflows/screenshot.yml index 7dd79a6..688cfec 100644 --- a/.github/workflows/screenshot.yml +++ b/.github/workflows/screenshot.yml @@ -235,17 +235,17 @@ jobs: # Write Python helper with single-quoted heredoc (no shell substitution), # then pass variables as argv so there are no quoting/expansion issues. cat > /tmp/make_body.py << 'PYEOF' -import json, base64, sys -img_path, commit_msg, existing_sha = sys.argv[1], sys.argv[2], sys.argv[3] -with open(img_path, "rb") as f: - content = base64.b64encode(f.read()).decode() -body = {"message": commit_msg, "content": content, "branch": "screenshots"} -if existing_sha: - body["sha"] = existing_sha -with open("/tmp/upload-body.json", "w") as f: - json.dump(body, f) -print(f"Upload body: {len(content)} base64 chars") -PYEOF + import json, base64, sys + img_path, commit_msg, existing_sha = sys.argv[1], sys.argv[2], sys.argv[3] + with open(img_path, "rb") as f: + content = base64.b64encode(f.read()).decode() + body = {"message": commit_msg, "content": content, "branch": "screenshots"} + if existing_sha: + body["sha"] = existing_sha + with open("/tmp/upload-body.json", "w") as f: + json.dump(body, f) + print(f"Upload body: {len(content)} base64 chars") + PYEOF python3 /tmp/make_body.py "/tmp/mc-screenshot.png" "$COMMIT_MSG" "$EXISTING_SHA" \ || { echo "WARNING: failed to build upload body"; exit 0; } From 21c9245765a3409e62dccddad3a26fad1efce813 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 8 May 2026 00:24:18 +0000 Subject: [PATCH 7/7] fix: replace scrot with ImageMagick import for screenshot capture scrot was failing silently on the headless X11 display (exit non-zero, caught by || {}) resulting in no screenshot file and a broken image URL in the PR comment. ImageMagick's `import -window root` speaks the X protocol directly and is significantly more reliable on headless Xvfb displays. scrot is kept as a fallback. Also adds an xdpyinfo check upfront so a dead display produces a clear error rather than a silent no-op. https://claude.ai/code/session_01Lg4VQQXs3q3WW4aMoe3Xci --- .github/workflows/screenshot.yml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/screenshot.yml b/.github/workflows/screenshot.yml index 688cfec..fe5e138 100644 --- a/.github/workflows/screenshot.yml +++ b/.github/workflows/screenshot.yml @@ -38,7 +38,7 @@ jobs: sudo apt-get update -qq # Mesa software GL + full LWJGL/GLFW X11 + audio deps sudo apt-get install -y \ - xvfb scrot fluxbox x11-utils xdotool \ + xvfb scrot fluxbox x11-utils xdotool imagemagick \ libgl1-mesa-dri libgles2-mesa mesa-vulkan-drivers \ libegl1-mesa libopengl0 \ libx11-6 libxext6 libxrender1 libxrandr2 \ @@ -168,9 +168,19 @@ jobs: - name: take screenshot if: always() run: | - # scrot exits non-zero if X has any issue — don't let that fail the job - DISPLAY=:99 scrot /tmp/mc-screenshot.png 2>&1 \ - || { echo "WARNING: scrot failed (exit $?), display may not be ready"; } + # Verify the display is actually reachable before attempting capture + if ! DISPLAY=:99 xdpyinfo > /dev/null 2>&1; then + echo "ERROR: Display :99 is not available — no screenshot possible" + exit 0 + fi + echo "Display :99 is up" + + # ImageMagick import is more reliable than scrot on headless X11; + # fall back to scrot if import fails for any reason + DISPLAY=:99 import -window root /tmp/mc-screenshot.png 2>&1 \ + || DISPLAY=:99 scrot /tmp/mc-screenshot.png 2>&1 \ + || echo "WARNING: all screenshot methods failed" + ls -lh /tmp/mc-screenshot.png 2>/dev/null || echo "No screenshot file produced" - name: stop minecraft