mirror of
https://github.com/jkdansereau/goldeneye-pc-port.git
synced 2026-10-23 15:52:42 +02:00
* ci: route windows-build to self-hosted runner (G2); land ROM-gated sweep workflow (G1)
G2: windows-build now runs on the self-hosted box (corpyvt6agent01) for
push/workflow_dispatch, using local ccache instead of the actions/cache
round-trip and skipping msys2/setup-msys2's install once packages are
already present. pull_request always stays on windows-latest — fork PRs
must never reach a box that will hold a ROM.
G1: adds .github/workflows/selfhosted.yml, a dispatch-only workflow that
builds + runs the 21-level headless sweep (tools_pc/verify.sh sweep --json)
against a runner-cached ROM. Inert until manually dispatched.
Still pending (manual, physical steps on the box — not done by this
commit, see docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md rollout table):
- register corpyvt6agent01 as a runner against this repo
- confirm MSYS2 at C:\msys64 with the required mingw64 package set
- one-time ROM seed into {runner_home}/_rom-seed/
- first workflow_dispatch sweep + parity check against a local run
- flip G1 from workflow_dispatch to push:[main] once green
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* fix(selfhosted.yml): remove invalid concurrency.max-parallel key
Found by testing: dispatching the workflow failed with a hard parse
error (HTTP 422, "Unexpected value 'max-parallel'"). GitHub Actions'
workflow-level `concurrency` block only supports `group` and
`cancel-in-progress` — `max-parallel` is a job `strategy:` key for
matrix jobs, not a concurrency-block key. Serialization was already
achieved via group + cancel-in-progress:false; the stray key just
blocked every dispatch.
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* fix(selfhosted.yml): register the msys2 shell wrapper (setup-msys2 step)
Found by testing: the first fixed dispatch failed every msys2-shell step
with "msys2: command not found". Unlike ci.yml's windows-build, this
workflow used `shell: msys2 {0}` without ever running msys2/setup-msys2,
which is what registers that shell alias for the runner — MSYS2 being
pre-installed on the box doesn't help if Actions doesn't know the shell
exists. Adds the same setup-msys2 step ci.yml uses, with update:false
since the packages are expected already present (fast no-op per plan).
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* ci: stop reinstalling MSYS2 per run on the self-hosted box
The self-hosted paths (selfhosted.yml; ci.yml windows-build's push/
dispatch route) no longer call msys2/setup-msys2 or pacman -S at all.
MSYS2 + the mingw64 package set are now assumed hand-maintained on
corpyvt6agent01 (docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md), invoked by
calling its bash directly (`C:\msys64\usr\bin\bash.exe -leo pipefail
"{0}"`, the same invocation setup-msys2's generated wrapper uses) with
MSYSTEM=MINGW64 so /etc/profile still puts mingw64/bin on PATH.
Adds tools_pc/check-selfhosted-deps.sh: a fast, install-free check that
exits 0 immediately when the required packages (toolchain, cmake,
SDL2, zlib, python, ccache, zip, unzip) are present, or fails with the
exact `pacman -S --needed ...` line to run otherwise — so a missing
package on the box surfaces immediately with a clear fix, instead of
as a confusing failure deeper in the build.
ci.yml's windows-latest (PR) path is unchanged: ephemeral runners still
get a full msys2/setup-msys2 install every time, since there's nothing
to persist there.
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* fix: use msys2/setup-msys2 release:false instead of a raw bash-path shell
The previous attempt (invoking C:\msys64\usr\bin\bash.exe directly as a
custom `shell:` command) failed on the actual runner with "Second path
fragment must not be a drive or UNC name" — a real limitation, not a
typo; it failed identically in both files, including the one with a
static (non-templated) shell string, so it wasn't the ${{ }} expression
either.
Root-caused by reading msys2/setup-msys2's actual source: `release:
false` makes it reuse the existing C:\msys64 as-is (hardcoded path in
that branch, confirmed in main.js) instead of downloading+installing a
fresh copy, and — verified in the same source — omitting `update` and
`install` skips every pacman call entirely (just one `uname -a` + a
pacman.conf sed tweak, no network). This is the officially documented
mechanism for self-hosted runners with a pre-existing MSYS2 install
(see the action's README, "release" section).
ci.yml's windows-build now runs setup-msys2 unconditionally but with
release/update/install all keyed off github.event_name: full install on
windows-latest (PR, ephemeral), reuse-only on the self-hosted box
(push/dispatch). selfhosted.yml always uses release:false. Both then
run tools_pc/check-selfhosted-deps.sh to verify the box's hand-
maintained packages are actually present.
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* fix(selfhosted.yml): cp -r for the ROM sidecar directories
Found by testing with the ROM seed actually in place: pcmodels-ntsc-final
and pccg-ntsc-final are directories (not single files, unlike
ge007.ntsc-final.z64), so a plain `cp` failed with "omitting directory"
and left the seed step failing on every run even with the seed populated.
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* ci: drive MSYS2 MINGW64 directly via ci-shell.cmd, drop msys2 shell shim
The self-hosted runs were dying with 'msys2: command not found' because the
msys2/setup-msys2 action's shell shim was never registered on the box. Replace
the dependency on that shim with a small committed launcher (tools_pc/ci-shell.cmd)
that headlessly reproduces the local 'MSYS2 MINGW64' terminal:
- MSYSTEM=MINGW64 + /mingw64/bin on PATH
- NON-login bash, so it keeps the workspace cwd (a login shell cd's to $HOME
and would break every relative path in the workflow)
- -e -o pipefail for GitHub-style error propagation
Both workflows now set defaults.run.shell to C:\msys64\ci-shell.cmd {0}; a
bootstrap step (shell: cmd) refreshes the launcher from the repo into C:\msys64
each run. selfhosted.yml drops setup-msys2 entirely; ci.yml keeps it only for
the ephemeral windows-latest PR path (which installs MSYS2 at C:\msys64).
.gitattributes: force CRLF for *.cmd so cmd.exe parses the launcher correctly.
* Revert "ci: drive MSYS2 MINGW64 directly via ci-shell.cmd, drop msys2 shell shim"
This reverts commit a976b2589c.
* docs(D244): log self-hosted-runner link failure — undefined reference to objectiveGetStatus_WEAK
First real build attempt on the reinstalled corpyvt6agent01 box (run
34707798707) fails at link with an unresolved #pragma weak alias. A local
repro of the identical alias pattern links fine on current MSYS2
(gcc 16.2.0/binutils 2.47); the box reports gcc 16.1.0 — likely a stale
toolchain, consistent with rollout step 2's earlier package-hygiene gap.
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
* fix(verify.sh): surface run.log tail on NO-FRAMES instead of silently discarding it
sweep mode's per-level run.log (the game process's own stdout/stderr, where
an SDL/GL init failure would show up) was captured into a mktemp capdir but
unconditionally rm -rf'd with no diagnostic ever printed for the NO-FRAMES
(no-crash) case -- every level failed this way on the self-hosted runner's
first real sweep attempt with zero clue why.
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
* fix(selfhosted.yml): stop deleting the ROM seed dir -- cache save silently fails on this box
actions/cache/save has been failing to persist the ROM cache entry on
corpyvt6agent01 (WARNING annotation, tar.exe exit code 2) but doesn't fail
the step -- by design, a cache-save failure is non-fatal, so the workflow
carried on and the now-unconditional "Delete one-time seed dir" step wiped
the only copy of the ROM every single run. Every dispatch since has failed
at "Seed ROM cache" with an empty seed dir and an empty cache -- exactly
the user-reported "it wipes out the ROM data each run".
Fix: don't delete the seed dir. It's cheap disk (three files, a few hundred
MB) and the existing restore-then-seed-if-missing logic already treats it as
the fallback source whenever the cache comes back empty, so leaving it in
place is safe and makes the workflow tolerant of the cache-save bug until
that's root-caused separately.
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
* fix(verify.sh): sweep --json never actually produced a verdict array; fix NO-FRAMES diagnostic clipping it further
Two bugs, found together while chasing the self-hosted runner's first real
sweep attempt:
1. sweep mode's `out=$(verify_level ...); echo "$out" | head -1` always
discarded emit_verdict's second (--json) output line -- RESULTS_JSON was
declared for assembling a verdict array but never populated or printed,
so `sweep --json` has never produced anything but `[]` for the
workflow's "Extract JSON verdict" step. Now builds and prints a real
`[{...}, {...}]` array before the RC exit.
2. My own D244-session addition (printing a NO-FRAMES level's run.log tail)
wrote to stdout, which the same `out=$(...)/head -1` capture turned into
the *new* first line, clipping the real verdict summary. Moved to stderr,
which bypasses that capture.
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
* docs: regenerate findings-index.csv for D244
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
235 lines
10 KiB
YAML
235 lines
10 KiB
YAML
# Self-hosted runtime (ROM-gated)
|
|
#
|
|
# Runs the build + the 21-level headless sweep (tools_pc/verify.sh) on the
|
|
# self-hosted runner "corpyvt6agent01", the one machine that has a legal ROM.
|
|
# This is the job class ci.yml explicitly does NOT do: CI is build-only, no
|
|
# ROM, so runtime checks live here. Setup + rollout plan (gitignored, local):
|
|
# docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md
|
|
#
|
|
# Persistence model — everything lives under the runner's own paths, managed
|
|
# by the pipeline; no hand-maintained directories:
|
|
# - ccache -> actions/cache (stored by the runner under _work/_temp)
|
|
# - ROM -> actions/cache, key ge-rom-data-ntsc-v1. Bootstrap: copy the
|
|
# three files into {runner_home}/_rom-seed/ on the box; a run
|
|
# with an empty cache copies them in and attempts to save the
|
|
# cache entry. The seed dir is kept (NOT deleted) rather than
|
|
# treated as one-time -- actions/cache/save has been silently
|
|
# failing to persist on this box (warning, not a hard step
|
|
# failure; root cause open, see SELFHOSTED-RUNNER-PLAN.md), so
|
|
# the seed is the durable fallback every run re-copies from
|
|
# until that's fixed. Integrity is re-verified every run
|
|
# against the committed ge007.u.sha1 (repo-tracked US ROM hash).
|
|
# - MSYS2 -> machine-level install at C:\msys64 (verify.sh hardcodes the
|
|
# prefix), installed and its packages maintained BY HAND on
|
|
# the box. msys2/setup-msys2 still runs every job (it's what
|
|
# registers the `msys2 {0}` shell wrapper), but with
|
|
# `release: false` and update/install both omitted — verified
|
|
# against the action's source to do no pacman -Syu / pacman -S
|
|
# / network round-trip, just a fast reuse of the existing
|
|
# C:\msys64. Package presence is checked (not installed) by
|
|
# tools_pc/check-selfhosted-deps.sh, which fails fast with
|
|
# exact install instructions if something's missing.
|
|
#
|
|
# Trigger gating (non-negotiable):
|
|
# - workflow_dispatch ONLY for now. Flip on `push: branches: [main]` once
|
|
# the first manual runs are reliably green.
|
|
# - NEVER add `pull_request`: unreviewed fork code must not be routable to
|
|
# a runner that can see the ROM.
|
|
#
|
|
# Leak rules (docs/dev/notes/AGENT-VM-FORGEJO-PLAN.md checklist):
|
|
# - The ROM + sidecars live in the runner-managed cache only; nothing in
|
|
# this workflow uploads from data/.
|
|
# - Artifacts here are console logs + a JSON verdict only. Rendered frames
|
|
# (ppm/) are ROM-derived content and are NOT uploaded.
|
|
|
|
name: Self-hosted runtime
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
levels:
|
|
description: "Sweep subset, SWEEP_LEVELS format (blank = all 21)"
|
|
required: false
|
|
default: ""
|
|
type: string
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
# One ROM job at a time per ref; never cancel an in-flight sweep out from
|
|
# under itself (the sweep is serialized by its own lockdir + watchdog). Group
|
|
# + cancel-in-progress:false is what actually serializes runs — GitHub
|
|
# Actions' workflow-level `concurrency` has no `max-parallel` key (that only
|
|
# exists under a job's `strategy:` for matrix jobs); a stray one here is a
|
|
# hard parse error that blocks every dispatch.
|
|
concurrency:
|
|
group: selfhosted-${{ github.ref }}
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
ROM_CACHE_KEY: ge-rom-data-ntsc-v1
|
|
|
|
jobs:
|
|
build-and-sweep:
|
|
runs-on: [ self-hosted, windows ]
|
|
defaults:
|
|
run:
|
|
shell: msys2 {0}
|
|
env:
|
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
|
CCACHE_MAXSIZE: 500M
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
persist-credentials: false
|
|
|
|
# `release: false` reuses the box's own C:\msys64 as-is instead of
|
|
# downloading/installing MSYS2 fresh (verified against the action's
|
|
# source: with this set, and update/install both omitted, it does
|
|
# exactly one `uname -a` + a pacman.conf sed tweak — no pacman -Syu,
|
|
# no pacman -S, no network round-trip). This step still has to run
|
|
# every time — it's what registers the `msys2 {0}` shell wrapper
|
|
# above — but does no install/update work.
|
|
- uses: msys2/setup-msys2@v2
|
|
with:
|
|
msystem: MINGW64
|
|
release: false
|
|
|
|
# Packages themselves are hand-maintained on the box (never installed
|
|
# by this workflow) — fails fast with exact install instructions if
|
|
# one's missing, instead of a confusing failure deeper in the build.
|
|
- name: Check self-hosted dependencies
|
|
run: tools_pc/check-selfhosted-deps.sh
|
|
|
|
# --- ROM + sidecars: runner-managed cache, one-time seed --------------
|
|
- name: Restore ROM + sidecars (runner cache)
|
|
uses: actions/cache/restore@v4
|
|
with:
|
|
path: |
|
|
data/ge007.ntsc-final.z64
|
|
data/pcmodels-ntsc-final
|
|
data/pccg-ntsc-final
|
|
key: ${{ env.ROM_CACHE_KEY }}
|
|
|
|
- name: Check ROM store presence
|
|
id: romcheck
|
|
run: |
|
|
present=true
|
|
for f in ge007.ntsc-final.z64 pcmodels-ntsc-final pccg-ntsc-final; do
|
|
if [ ! -f "data/$f" ]; then echo "missing from cache: $f"; present=false; fi
|
|
done
|
|
echo "present=$present" >> "$GITHUB_OUTPUT"
|
|
|
|
# {runner_home} = two levels up from RUNNER_WORKSPACE ({home}/_work/{repo}).
|
|
# NOT deleted after the cache save below (see that step's comment) --
|
|
# kept as a durable fallback source until the cache save is proven to
|
|
# actually round-trip on this box.
|
|
- name: Seed ROM cache (from durable seed dir)
|
|
if: steps.romcheck.outputs.present == 'false'
|
|
run: |
|
|
seed="$(dirname "$(dirname "$RUNNER_WORKSPACE")")/_rom-seed"
|
|
[ -d "$seed" ] || {
|
|
echo "ROM cache is empty and no seed found at $seed." >&2
|
|
echo "One-time bootstrap: copy ge007.ntsc-final.z64, pcmodels-ntsc-final," >&2
|
|
echo "and pccg-ntsc-final into that directory on the runner, then re-run." >&2
|
|
exit 2
|
|
}
|
|
mkdir -p data
|
|
# -r: pcmodels-*/pccg-* are directories, not single files (found by
|
|
# testing — a plain cp failed with "omitting directory").
|
|
cp -r "$seed/ge007.ntsc-final.z64" "$seed/pcmodels-ntsc-final" \
|
|
"$seed/pccg-ntsc-final" data/
|
|
echo "seeded ROM store from $seed"
|
|
|
|
# Explicit save (restore alone never saves): persist the seeded store
|
|
# immediately, before the build/sweep can fail out from under it.
|
|
# KNOWN ISSUE (2026-09-12): this reliably logs a WARNING annotation
|
|
# ("Failed to save: ... tar.exe failed with exit code 2") on this box
|
|
# and never actually persists anything to the cache -- but
|
|
# actions/cache/save treats a save failure as non-fatal by design, so
|
|
# the step still reports success and the job carries on. Root cause not
|
|
# yet found (see docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md). Consequence:
|
|
# do NOT delete the seed dir after this step (previously done, and it
|
|
# meant a *silent* cache-save failure combined with the delete wiped
|
|
# the only copy of the ROM every run -- exactly what broke rollout
|
|
# step 5's repeated dispatches). The seed dir now stays in place
|
|
# indefinitely as a durable fallback; "Check ROM store presence" /
|
|
# "Seed ROM cache" above already re-copy from it whenever the cache
|
|
# comes back empty, so this is safe to leave as a no-op once the cache
|
|
# save is fixed for real.
|
|
- name: Save ROM cache
|
|
if: steps.romcheck.outputs.present == 'false'
|
|
uses: actions/cache/save@v4
|
|
with:
|
|
path: |
|
|
data/ge007.ntsc-final.z64
|
|
data/pcmodels-ntsc-final
|
|
data/pccg-ntsc-final
|
|
key: ${{ env.ROM_CACHE_KEY }}
|
|
|
|
- name: Verify ROM integrity (committed US sha1)
|
|
run: |
|
|
cd data
|
|
want="$(cut -d' ' -f1 ../ge007.u.sha1)"
|
|
got="$(sha1sum ge007.ntsc-final.z64 | cut -d' ' -f1)"
|
|
[ "$want" = "$got" ] || { echo "ROM hash mismatch: want $want got $got" >&2; exit 2; }
|
|
echo "ROM sha1 ok: $got"
|
|
|
|
# --- build -------------------------------------------------------------
|
|
# Same opt-in ccache CMakeLists.txt wires up (speed-ups plan Step 5);
|
|
# persisted via actions/cache like ci.yml's windows-build.
|
|
- name: Restore ccache
|
|
uses: actions/cache/restore@v4
|
|
with:
|
|
path: ${{ env.CCACHE_DIR }}
|
|
key: ccache-selfhosted-${{ hashFiles('CMakeLists.txt') }}-${{ github.sha }}
|
|
restore-keys: |
|
|
ccache-selfhosted-${{ hashFiles('CMakeLists.txt') }}-
|
|
ccache-selfhosted-
|
|
|
|
- name: Regenerate ROM-asset symbol file (US)
|
|
run: python scripts/gen_romassets.py u
|
|
|
|
- name: Build (ntsc-final)
|
|
run: ./build-pc.sh ntsc-final
|
|
|
|
# verify.sh: build-if-stale -> headless GE_PCDUMP per level -> crash
|
|
# symbolication -> pixcount -> framediff vs tools_pc/golden/. Serialized
|
|
# + watchdog + one retry by design (D117: not frame-deterministic).
|
|
- name: Level sweep (all 21, or dispatch subset)
|
|
run: |
|
|
[ -n "${{ inputs.levels }}" ] && export SWEEP_LEVELS='${{ inputs.levels }}'
|
|
tools_pc/verify.sh sweep --json > sweep.log 2>&1
|
|
rc=$?
|
|
cat sweep.log
|
|
exit $rc
|
|
|
|
# Pull the JSON verdict array out of the mixed stdout for easy review.
|
|
- name: Extract JSON verdict
|
|
if: always()
|
|
run: |
|
|
python -c "
|
|
import json, sys
|
|
s = open('sweep.log', encoding='utf-8', errors='replace').read()
|
|
i = s.rfind('[{')
|
|
out = '[]'
|
|
if i >= 0:
|
|
d, _ = json.JSONDecoder().raw_decode(s[i:])
|
|
out = json.dumps(d, indent=1)
|
|
open('sweep-verdict.json', 'w').write(out + '\n')
|
|
print(out)
|
|
"
|
|
|
|
# Console logs + verdict only — no frames, no ROM bytes (leak rules).
|
|
- name: Upload sweep report
|
|
if: always()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: selfhosted-sweep-${{ github.run_number }}
|
|
path: |
|
|
sweep.log
|
|
sweep-verdict.json
|
|
ge007.crash.log
|
|
if-no-files-found: ignore
|
|
retention-days: 14
|