Summary
The visual-test scheduled workflow has been failing on every run since it was introduced in #977 (Aug 3 2026). The failure is not in the game/render code, screenshot pipeline, or PPM/PNG conversion. The "Run menu screenshot capture" step (and every downstream screenshot step) is skipped because an earlier CI housekeeping step exits with a non‑zero status, so build-output.log and screenshot.png are never produced. This is exactly the artifact gap the diagnosis prompt expects to read.
Observed failure
I queried the GitHub API for the last several runs of this workflow. Every run has the same shape:
| # |
Step |
State |
| 1–7 |
checkout, devenv, weston, opencode cache, prompt loads |
success |
| 8 |
Ensure visual-test label exists |
failure (≈2s) |
| 9 |
Setup Lavapipe Vulkan |
skipped |
| 10 |
Run menu screenshot capture |
skipped |
| 11–16 |
post-screenshot bookkeeping |
success / skipped |
| 17 |
Run opencode visual verification |
skipped |
| 18 |
Run opencode failure diagnosis |
in‑progress (this agent) |
Recent runs confirming this:
Because step 10 never runs, the workspace contains weston.log but no build-output.log and no screenshot.png, which is why the diagnosis prompt's expected artifact is missing in this checkout.
Root cause
File: .github/workflows/visual-test.yml, lines 55–69.
The workflow inlines gh label list / gh label create for two labels (visual-test and run-visual-test) using GH_TOKEN: ${{ secrets.OPENCODE_PAT }}. As written, any failure of either gh invocation (auth, jq, race where the label was just created by another run, etc.) makes the step exit non‑zero and cascade‑skip the entire screenshot pipeline.
Other opencode workflows in this repo already solved this by routing the same logic through a small helper that degrades gracefully:
.github/workflows/opencode-audit.yml:51-54
.github/workflows/opencode-test-writer.yml
Both invoke scripts/ensure_label.sh, which:
if ! gh label list --json name --jq '.[].name' | grep -qxF "$label"; then
if ! gh label create "$label" --description "$description" --color "$color"; then
printf '::warning::Failed to create label %s\n' "$label" >&2
fi
fi
i.e. the create failure is downgraded to a GitHub Actions warning, so the workflow keeps going. visual-test.yml does not use this helper and does not downgrade the failure, which is the exact behavior the API results show.
I confirmed against the live repo that visual-test and run-visual-test already exist on OpenStaticFish/ZigCraft (color E06C75), so the create branch is at minimum racy — and the helper resolves the race by no‑op‑ing on "already exists" without a non‑zero exit.
Suggested fix
Replace the inlined block in .github/workflows/visual-test.yml with two calls to the existing helper, mirroring opencode-audit.yml:
- name: Ensure visual-test label exists
run: |
bash scripts/ensure_label.sh "visual-test" \
"Issues from automated visual regression tests" "E06C75"
bash scripts/ensure_label.sh "run-visual-test" \
"Run deterministic visual regression workflow on a PR" "E06C75"
env:
GH_TOKEN: ${{ secrets.OPENCODE_PAT }}
Optionally tighten the existing helper to use --force on the create (idempotent) or to swallow the "already exists" error explicitly, so the helper is safe to call from any future workflow.
Secondary observations (not the cause)
While reading the prompt/code pair, two documentation drifts are worth flagging in the same issue so future diagnoses aren't chasing ghosts:
-
.github/prompts/visual-test-diagnose.md still describes the workflow as running with -Dscreenshot-path=screenshot.ppm and "PPM is then converted to PNG". The current workflow runs -Dscreenshot-path=screenshot.png directly (.github/workflows/visual-test.yml:81), and modules/engine-graphics/src/vulkan/screenshot.zig only implements PNG / JPEG / GIF / WebP encoders (detectScreenshotFormat at screenshot.zig:262-268, writeImage at screenshot.zig:245-260). If anyone re‑adds a PPM path, they need an encoder first; otherwise they will hit screenshot: unsupported image path '…ppm' (use .png, .jpg, .jpeg, .gif, or .webp) from screenshot.zig:40 and exit ScreenshotCaptureFailed from src/game/app.zig:588.
-
The screenshot capture is gated on screenshot_settle_frames >= 30 (src/game/app.zig:556), independent of ZIGCRAFT_SMOKE_FRAMES. With ZIGCRAFT_SMOKE_FRAMES=5 the menu screenshot still requires ~30 consecutive settled frames before captureFrame fires — worth knowing if anyone tunes ZIGCRAFT_SMOKE_FRAMES and expects the capture to happen on frame 5.
Where to look
.github/workflows/visual-test.yml – failing step at lines 55‑69
scripts/ensure_label.sh – existing helper that should be reused
.github/workflows/opencode-audit.yml:51-54 and opencode-test-writer.yml – reference patterns
Workflow run
https://github.com/OpenStaticFish/ZigCraft/actions/runs/31462404266
Summary
The
visual-testscheduled workflow has been failing on every run since it was introduced in #977 (Aug 3 2026). The failure is not in the game/render code, screenshot pipeline, or PPM/PNG conversion. The "Run menu screenshot capture" step (and every downstream screenshot step) is skipped because an earlier CI housekeeping step exits with a non‑zero status, sobuild-output.logandscreenshot.pngare never produced. This is exactly the artifact gap the diagnosis prompt expects to read.Observed failure
I queried the GitHub API for the last several runs of this workflow. Every run has the same shape:
Recent runs confirming this:
Because step 10 never runs, the workspace contains
weston.logbut nobuild-output.logand noscreenshot.png, which is why the diagnosis prompt's expected artifact is missing in this checkout.Root cause
File:
.github/workflows/visual-test.yml, lines 55–69.The workflow inlines
gh label list / gh label createfor two labels (visual-testandrun-visual-test) usingGH_TOKEN: ${{ secrets.OPENCODE_PAT }}. As written, any failure of eitherghinvocation (auth, jq, race where the label was just created by another run, etc.) makes the step exit non‑zero and cascade‑skip the entire screenshot pipeline.Other opencode workflows in this repo already solved this by routing the same logic through a small helper that degrades gracefully:
.github/workflows/opencode-audit.yml:51-54.github/workflows/opencode-test-writer.ymlBoth invoke
scripts/ensure_label.sh, which:i.e. the create failure is downgraded to a GitHub Actions warning, so the workflow keeps going.
visual-test.ymldoes not use this helper and does not downgrade the failure, which is the exact behavior the API results show.I confirmed against the live repo that
visual-testandrun-visual-testalready exist onOpenStaticFish/ZigCraft(colorE06C75), so the create branch is at minimum racy — and the helper resolves the race by no‑op‑ing on "already exists" without a non‑zero exit.Suggested fix
Replace the inlined block in
.github/workflows/visual-test.ymlwith two calls to the existing helper, mirroringopencode-audit.yml:Optionally tighten the existing helper to use
--forceon the create (idempotent) or to swallow the "already exists" error explicitly, so the helper is safe to call from any future workflow.Secondary observations (not the cause)
While reading the prompt/code pair, two documentation drifts are worth flagging in the same issue so future diagnoses aren't chasing ghosts:
.github/prompts/visual-test-diagnose.mdstill describes the workflow as running with-Dscreenshot-path=screenshot.ppmand "PPM is then converted to PNG". The current workflow runs-Dscreenshot-path=screenshot.pngdirectly (.github/workflows/visual-test.yml:81), andmodules/engine-graphics/src/vulkan/screenshot.zigonly implements PNG / JPEG / GIF / WebP encoders (detectScreenshotFormatatscreenshot.zig:262-268,writeImageatscreenshot.zig:245-260). If anyone re‑adds a PPM path, they need an encoder first; otherwise they will hitscreenshot: unsupported image path '…ppm' (use .png, .jpg, .jpeg, .gif, or .webp)fromscreenshot.zig:40and exitScreenshotCaptureFailedfromsrc/game/app.zig:588.The screenshot capture is gated on
screenshot_settle_frames >= 30(src/game/app.zig:556), independent ofZIGCRAFT_SMOKE_FRAMES. WithZIGCRAFT_SMOKE_FRAMES=5the menu screenshot still requires ~30 consecutive settled frames beforecaptureFramefires — worth knowing if anyone tunesZIGCRAFT_SMOKE_FRAMESand expects the capture to happen on frame 5.Where to look
.github/workflows/visual-test.yml– failing step at lines 55‑69scripts/ensure_label.sh– existing helper that should be reused.github/workflows/opencode-audit.yml:51-54andopencode-test-writer.yml– reference patternsWorkflow run
https://github.com/OpenStaticFish/ZigCraft/actions/runs/31462404266