Skip to content

DIAG gameplay-baseline (TMNT-RP entity bugs) #553

DIAG gameplay-baseline (TMNT-RP entity bugs)

DIAG gameplay-baseline (TMNT-RP entity bugs) #553

Workflow file for this run

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