DIAG gameplay-baseline (TMNT-RP entity bugs) #553
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: Build MiSTer ARM | |
| run-name: ${{ inputs.reason || github.event.head_commit.message || github.ref_name }} | |
| on: | |
| workflow_dispatch: # manual "Run workflow" button (build any branch on demand) | |
| inputs: | |
| reason: | |
| description: 'Build label (shown as the run title in the Actions tab)' | |
| required: false | |
| type: string | |
| push: | |
| branches: [main] | |
| tags: ['v*'] | |
| # Harness tooling is not compiled into the ARM binary, so a tools/** | |
| # push has nothing to rebuild. Without this filter it still triggers a | |
| # full QEMU build AND a commit-back, which re-ships the binary to every | |
| # update_all user for no functional change: measured 2026-08-03, two | |
| # harness-only pushes each moved the shipped hash. (The bytes that | |
| # differ are the 20-byte GNU build-id note, not code -- the real fix is | |
| # -Wl,--build-id=none at link time; this filter avoids the trigger.) | |
| paths-ignore: | |
| - 'tools/**' | |
| # Markdown cannot change the ARM binary, so a docs edit must not rebuild | |
| # and re-commit it -- that re-ships ~2.2 MB to every update_all user for | |
| # nothing. Demonstrated 2026-08-16: a docs/dev/** push triggered a full | |
| # rebuild. Shipped docs still propagate -- dispatch_db.yml and | |
| # refresh_release.yml both trigger on docs/** and are unaffected by this. | |
| - 'docs/**' | |
| pull_request: | |
| branches: [main] | |
| jobs: | |
| build-arm: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # 🛑 THE GATES RUN HERE, BEFORE THE BUILD, so a failure stops the SHIP. | |
| # | |
| # These same checks also run in diff_harness.yml -- but that is a separate | |
| # workflow with no `needs:` or `workflow_run:` relationship to this one, so | |
| # the two fire in PARALLEL on the same push. A commit that broke the .inp | |
| # parser would still reach the commit-back and the DB dispatch, shipping to | |
| # every update_all user, while the harness went red minutes later. Running | |
| # them again here costs a couple of minutes of a runner and converts that | |
| # race into a gate. | |
| # | |
| # Kept in diff_harness.yml as well: it additionally builds the headless | |
| # engine and runs the corpus scan, and it fires on pushes this workflow's | |
| # paths-ignore filter deliberately skips. | |
| # 🛑 SHIP-ONLY GATE. Pushing to main IS the ship, and the standing rule is | |
| # that a new RBF and a new ARM binary go together. CI builds only the ARM | |
| # half, so without this a source push that moved the wire contract would | |
| # auto-ship a binary against whatever bitstream is in _Other/ -- and the | |
| # replay-slot picker adds a NEW DDR3 word, so under the old RBF that offset | |
| # is dead space holding the previous core's leftovers. | |
| # | |
| # Restricted to main deliberately: a workflow_dispatch validation build on | |
| # a branch SHOULD still run while the RBF is being recompiled. It is the | |
| # ship that must wait, not the verification. | |
| # The gate below reads per-file commit times, which a depth-1 checkout | |
| # cannot answer -- every tracked file reports the shallow boundary, so the | |
| # rbf and the RTL tie and the comparison can never fail. It reported a | |
| # green PASS in that state until 2026-08-13. | |
| # | |
| # blob:none, NOT fetch-depth: 0. This repo's history is ~206 MB, almost | |
| # all of it committed rbf and ARM-binary blobs; a blobless unshallow gets | |
| # every commit and tree while leaving those on the server -- measured | |
| # 36 MB locally, and GitHub honours the filter (a file:// remote does not, | |
| # so a local repro of this prints "filtering not recognized" and is a | |
| # worst case, not a representative one). | |
| # | |
| # || true: an already-complete clone makes --unshallow an error, and the | |
| # checker refuses on its own if this step fails to do its job. | |
| - name: Deepen history for the RBF/ARM contract gate | |
| if: github.ref == 'refs/heads/main' | |
| run: git fetch --unshallow --filter=blob:none || true | |
| - name: Gate - RBF and ARM binary ship together (main only) | |
| if: github.ref == 'refs/heads/main' | |
| run: python3 tools/harness/test_rbf_arm_contract.py | |
| - name: Gate - CI staleness guard | |
| run: python3 -m pip install --quiet pyyaml && python3 tools/harness/test_ci_staleness_guard.py | |
| - name: Gate - pause-menu mode parity (replay desync) | |
| run: python3 tools/harness/menu_mode_parity_check.py | |
| - name: Gate - save-payload extractor (untrusted input) | |
| run: python3 tools/harness/test_snap_extract.py | |
| - name: Gate - script-save grammar (a payload may carry a PROGRAM) | |
| run: python3 tools/harness/test_snap_script.py | |
| - name: Gate - probe refuses an unidentified take carrying a payload | |
| run: python3 tools/harness/test_probe_novouch.py | |
| - name: Gate - writer/reader agreement | |
| run: python3 tools/harness/test_writer_reader_agree.py | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@v3 | |
| with: | |
| platforms: arm | |
| - name: Build in ARM container | |
| run: | | |
| chmod +x .github/scripts/build_mister_arm.sh | |
| docker run --rm --platform linux/arm/v7 \ | |
| -e MREC_BUILD_ID=${{ github.sha }} \ | |
| -v ${{ github.workspace }}:/build \ | |
| -w /tmp \ | |
| arm32v7/debian:bullseye-slim \ | |
| bash /build/.github/scripts/build_mister_arm.sh | |
| - name: Commit fresh ARM binary back to repo | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| env: | |
| # For the staleness guard's `gh api` ancestry query. Read-only use of | |
| # the repo's own token; the DB/release dispatches below still use the | |
| # cross-repo PAT, which GITHUB_TOKEN cannot replace. | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| mkdir -p games/OpenBOR /tmp/ci-binary | |
| # Stage the freshly built binary OUTSIDE the work tree so the | |
| # re-apply loop below can `git reset --hard` without losing it. | |
| cp OpenBOR /tmp/ci-binary/OpenBOR_7533 | |
| cp OpenBOR games/OpenBOR/OpenBOR_7533 | |
| chmod +x games/OpenBOR/OpenBOR_7533 | |
| git config user.email "ci@misterorganize" | |
| git config user.name "MiSTer Organize CI" | |
| # Diagnostic-marker gate (added 2026-05-25 per feedback_no_diagnostic_binaries_in_db.md): | |
| # Universal hybrid-core rule. If any source path that contributes to the final | |
| # binary carries TEMPORARY / SUB-PROFILE / DIAG instrumentation, the rebuilt | |
| # binary is for research only and MUST NOT land on main (where DB rebuild would | |
| # pick up its hash and ship it via db.json to end users). Binary still uploads | |
| # as workflow artifact below for manual deploy testing. | |
| # Scope is .github/scripts/ ENTIRE, not apply_patches.py alone: | |
| # patch_sdl_dummy.py injects the DDR3 mmap, the keepalive thread and the | |
| # frame writer into SDL's dummy driver and is linked into the shipped | |
| # binary, but was outside the gate -- the same omission that let a | |
| # 60-line PPM frame-capture diagnostic through in 2026-05-22. | |
| # | |
| # And no 2>/dev/null: grep exits 2 on a missing path, which `if` reads | |
| # exactly like "no markers found", so a rename would turn this gate into | |
| # a silent pass. Assert the paths exist, then let grep speak. | |
| for p in .github/scripts patches src; do | |
| [ -e "$p" ] || { echo "::error::diagnostic gate cannot scan '$p' (missing) — refusing to commit blind"; exit 1; } | |
| done | |
| if grep -rqE "TEMPORARY PROFILE|TEMPORARY DIAG|TEMPORARY SUB-PROFILE|REVERT AFTER MEASURED" .github/scripts patches src; then | |
| echo "[CI gate] Diagnostic markers detected in .github/scripts/ / patches/ / src/ — SKIPPING binary commit-back." | |
| echo "[CI gate] Binary built successfully and uploads as workflow artifact for manual deploy." | |
| echo "[CI gate] Will commit binary back automatically once diagnostic markers are removed." | |
| exit 0 | |
| fi | |
| # Commit-back with RE-APPLY-ON-REJECT (parity with MiSTer_PICO-8, | |
| # universal hybrid-core rule per feedback_ci_commitback_reapply_on_reject.md). | |
| # A bare `git push` here loses any race with another push to main -- | |
| # the step then goes red AND the built binary is lost. On rejection we | |
| # fetch the new tip, re-apply THIS RUN'S binary onto it, and re-commit. | |
| # Never `git pull --rebase`: replaying a binary commit onto a tip that | |
| # already carries a different binary is an unresolvable merge conflict. | |
| PUSH_OK=0 | |
| NOTHING_TO_DO=0 | |
| STALE=0 | |
| for i in 1 2 3 4 5; do | |
| git fetch origin main | |
| git reset --hard origin/main | |
| # --- Staleness guard -------------------------------------------- | |
| # The re-apply above is right that a build must never be LOST, but | |
| # it never asks whether the tip already carries a NEWER binary. Two | |
| # builds overlap whenever pushes land close together, and they do | |
| # NOT finish in start order. Measured 2026-08-04 on this repo: the | |
| # 0ea791c build ran 00:46->01:15 while the d4815ef build ran | |
| # 00:52->01:07, so the OLDER source finished LAST, re-applied its | |
| # binary over the newer one, and main shipped a stale (unstripped) | |
| # binary until an unrelated build happened to heal it. | |
| # | |
| # Ancestry is asked of the API rather than the local clone: | |
| # actions/checkout is depth-1, so `git merge-base` has no history to | |
| # reason over. compare/{base}...{head} reports "ahead" when head is | |
| # ahead of base. | |
| # | |
| # FAILS OPEN on every uncertainty -- no marker commit, unparseable | |
| # message, API error, unrelated histories. The guard may only skip a | |
| # push it is certain is stale; when unsure it must defer to the old | |
| # behaviour, because wrongly skipping loses a build silently. | |
| TIP_SRC=$(gh api "repos/${{ github.repository }}/commits?path=games/OpenBOR/OpenBOR_7533&per_page=1" \ | |
| --jq '.[0].commit.message' 2>/dev/null \ | |
| | sed -n '1s/^CI: rebuild ARM binary for \([0-9a-f]\{40\}\)$/\1/p') | |
| if [ -n "$TIP_SRC" ] && [ "$TIP_SRC" != "${{ github.sha }}" ]; then | |
| REL=$(gh api "repos/${{ github.repository }}/compare/${{ github.sha }}...$TIP_SRC" \ | |
| --jq '.status' 2>/dev/null || echo unknown) | |
| if [ "$REL" = "ahead" ]; then | |
| echo "[staleness guard] main already carries a binary built from $TIP_SRC," | |
| echo " which is AHEAD of this run's source ${{ github.sha }}." | |
| echo " This build is stale -- refusing to overwrite a newer binary." | |
| echo " It is still uploaded as a workflow artifact if you need it." | |
| STALE=1 | |
| break | |
| fi | |
| echo "[staleness guard] tip binary source $TIP_SRC is '$REL' vs this run -- proceeding." | |
| fi | |
| cp /tmp/ci-binary/OpenBOR_7533 games/OpenBOR/OpenBOR_7533 | |
| chmod +x games/OpenBOR/OpenBOR_7533 | |
| if git diff --quiet games/OpenBOR/OpenBOR_7533; then | |
| echo "ARM binary unchanged after sync - nothing to commit." | |
| NOTHING_TO_DO=1 | |
| break | |
| fi | |
| git add games/OpenBOR/OpenBOR_7533 | |
| git commit -m "CI: rebuild ARM binary for ${{ github.sha }}" | |
| if git push; then | |
| echo "Push succeeded on attempt $i." | |
| PUSH_OK=1 | |
| break | |
| fi | |
| echo "Push rejected (attempt $i) - re-applying the fresh binary onto the new tip..." | |
| sleep 2 | |
| done | |
| if [ "$PUSH_OK" != "1" ] && [ "$NOTHING_TO_DO" != "1" ] && [ "$STALE" != "1" ]; then | |
| echo "::error::Binary commit-back failed after 5 attempts. The binary is still available as a workflow artifact; deploy manually or re-run." | |
| exit 1 | |
| fi | |
| if [ "$PUSH_OK" = "1" ]; then | |
| # Trigger MiSTer_Frontier DB rebuild so db.json reflects the new | |
| # binary hash promptly (prevents update_all hash-mismatch failures | |
| # in the window between binary push and the daily DB cron). | |
| # Requires MISTER_FRONTIER_DISPATCH_TOKEN secret (classic PAT, | |
| # workflow scope, no expiration -- threat model justifies this: | |
| # token only triggers workflow_dispatch on MiSTer_Frontier). | |
| # If unset, just warns (daily cron handles propagation slower). | |
| if [ -n "${{ secrets.MISTER_FRONTIER_DISPATCH_TOKEN }}" ]; then | |
| echo "Triggering MiSTer_Frontier DB rebuild..." | |
| GH_TOKEN="${{ secrets.MISTER_FRONTIER_DISPATCH_TOKEN }}" \ | |
| gh workflow run "Build Custom Database" -R MiSTerOrganize/MiSTer_Frontier \ | |
| || echo "WARN: DB dispatch failed (token may be expired)" | |
| # Refresh the rolling 'latest' GitHub Release so its zip asset | |
| # matches the just-committed binary. The commit-back push above | |
| # used GITHUB_TOKEN, which does NOT re-trigger refresh_release's | |
| # `on: push`, so we dispatch it explicitly (same PAT as the DB | |
| # rebuild). Manual RBF/handler/README pushes hit its on:push path | |
| # trigger directly. Content-gated -> a redundant fire is a no-op. | |
| echo "Triggering rolling 'latest' release refresh..." | |
| GH_TOKEN="${{ secrets.MISTER_FRONTIER_DISPATCH_TOKEN }}" \ | |
| gh workflow run refresh_release.yml -R ${{ github.repository }} \ | |
| || echo "WARN: refresh_release dispatch failed" | |
| else | |
| echo "WARN: MISTER_FRONTIER_DISPATCH_TOKEN not set -- skipping DB auto-dispatch." | |
| echo " Add this PAT secret to close the db.json staleness window." | |
| fi | |
| fi | |
| - name: Upload binary artifact | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: OpenBOR_7533-arm-binary | |
| path: OpenBOR |