Skip to content
Closed
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
320 changes: 320 additions & 0 deletions .github/workflows/screenshot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
name: screenshot test

on:
pull_request:
push:
branches: [main, master]

jobs:
screenshot:
runs-on: ubuntu-24.04
continue-on-error: true # screenshots are informational, never block a PR
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
# Mesa software GL + full LWJGL/GLFW X11 + audio deps
sudo apt-get install -y \
xvfb scrot fluxbox x11-utils xdotool imagemagick \
libgl1-mesa-dri libgles2-mesa mesa-vulkan-drivers \
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
# 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: |
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
continue-on-error: true
run: |
set +e # process management shouldn't abort the step
./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 || true
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
if: always()
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"

# Give the JVM a moment, then check for early crash
sleep 10
if ! kill -0 "$CLIENT_PID" 2>/dev/null; then
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
# 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
if: always()
run: |
# 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
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 "")

# 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}" \
-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);
}
Loading