diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 9518980..5c5241f 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -7,10 +7,23 @@ name: CI # self-hosted runner usually can't reach the GitHub action registry (unless DEFAULT_ACTIONS_URL is set # to proxy github.com). Only actions/checkout and actions/upload-artifact remain as external actions — # both ship with Gitea's default proxy; if yours is offline, mirror them into your instance. +# +# It also runs once a day (schedule) as a safety net over main's tip. The `guard` job skips that daily +# run when the commit already passed — so an idle main isn't re-tested, but a commit whose push-time +# run was missed/skipped still gets verified. schedule needs Gitea >= 1.21; workflow_dispatch (the +# manual "Run workflow" button) needs >= 1.23; from 1.23 the cron is evaluated in UTC. on: push: pull_request: + workflow_dispatch: + schedule: + - cron: "0 4 * * *" # 04:00 UTC daily + +# statuses:write lets the test job stamp a `ci/headless` commit status the guard reads back. +permissions: + contents: read + statuses: write env: # The Godot build the team runs (godot --version -> 4.7.beta.custom_build.417cd33bf). Pin this to a @@ -21,9 +34,51 @@ env: GODOT_URL: "" jobs: + # On push/PR this passes straight through. On the daily schedule it asks the API whether main's tip + # already carries a successful `ci/headless` status (stamped by an earlier run) and, if so, tells the + # test job to skip — that's the "don't re-test a commit that already ran" rule. + guard: + name: Decide whether to run + runs-on: ubuntu-latest + outputs: + run: ${{ steps.decide.outputs.run }} + steps: + - name: Check for an existing pass on this commit + id: decide + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + set -u + if [ "${{ github.event_name }}" != "schedule" ]; then + echo "event=${{ github.event_name }} -> run" + echo "run=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + api="${{ github.api_url }}"; [ -n "$api" ] || api="${{ github.server_url }}/api/v1" + curl -sfL -H "Authorization: token ${GITEA_TOKEN}" \ + "$api/repos/${{ github.repository }}/commits/${{ github.sha }}/statuses?limit=100" \ + -o /tmp/statuses.json || echo '[]' > /tmp/statuses.json + python3 - <<'PY' + import json, os + try: + data = json.load(open("/tmp/statuses.json")) + except Exception: + data = [] + items = data.get("statuses", data) if isinstance(data, dict) else data + done = any( + s.get("context") == "ci/headless" and (s.get("state") or s.get("status")) == "success" + for s in (items or []) + ) + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write("run=%s\n" % ("false" if done else "true")) + print("commit already passed ci/headless -> skip" if done else "no prior pass -> run") + PY + test: name: Lint & headless tests runs-on: ubuntu-latest # must match a label your act_runner registered with + needs: guard + if: ${{ needs.guard.outputs.run == 'true' }} steps: - name: Checkout uses: actions/checkout@v4 @@ -46,11 +101,13 @@ jobs: /tmp/venv/bin/pip install "gdtoolkit==4.*" echo "/tmp/venv/bin" >> "$GITHUB_PATH" - # First run imports assets and builds the .godot cache (gitignored) so headless tests can load - # the .glb/.png resources they instance. Import can exit non-zero on warnings. + # A cold .godot cache (CI always starts cold) needs two import passes: the first imports base + # assets, the second resolves scenes that depend on them (the .fbx/.glb -> .scn imports). One pass + # leaves those unresolved and the headless tests fail to load them. Warnings exit non-zero. - name: Import resources - run: godot --headless --import - continue-on-error: true + run: | + godot --headless --import || true + godot --headless --import || true - name: gdlint run: gdlint actors systems ui props levels tests @@ -60,6 +117,19 @@ jobs: GODOT: godot run: bash run_tests.sh --no-gameplay + # Stamp the commit so the daily guard recognizes it as already tested. Best-effort: a fork PR whose + # token can't write statuses just skips this (the build still passes). + - name: Mark commit as tested + if: success() + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + api="${{ github.api_url }}"; [ -n "$api" ] || api="${{ github.server_url }}/api/v1" + curl -sfL -X POST -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" \ + "$api/repos/${{ github.repository }}/statuses/${{ github.sha }}" \ + -d '{"context":"ci/headless","state":"success","description":"headless suite green"}' \ + >/dev/null || echo "could not post commit status (token lacks statuses:write?)" + - name: Upload test output on failure if: failure() uses: actions/upload-artifact@v4