Add a daily safety-net run to Gitea CI that skips already-tested commits

Add a schedule trigger (04:00 UTC daily) + workflow_dispatch to the Gitea CI, plus a
guard job: on push/PR it passes straight through, but on the daily run it asks the
Gitea API whether main's tip already carries a successful 'ci/headless' commit status
and, if so, skips the test job. The test job stamps that status when green, so a
commit already verified at push time is never re-tested — the nightly only spends a
runner on a tip whose push-time run was missed, skipped, or failed.

Also fix a cold-cache bug the nightly would have hit anyway: CI always starts without
a .godot cache, and a single 'godot --import' pass leaves .fbx/.glb-derived scenes
unresolved (the first pass imports base assets, the second resolves dependents), so
the headless tests couldn't load them. Import now runs twice.

Needs Gitea >= 1.21 (schedule) / >= 1.23 (the manual-run button); the auto-injected
GITEA_TOKEN needs statuses:write for the marker, granted via workflow permissions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KoYBYQgRyY1zDHXMsXY3V
This commit is contained in:
2026-09-06 01:22:10 +03:00
parent 4569d3f5d4
commit c5ef68e82a
+74 -4
View File
@@ -7,10 +7,23 @@ name: CI
# self-hosted runner usually can't reach the GitHub action registry (unless DEFAULT_ACTIONS_URL is set # 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 — # 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. # 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: on:
push: push:
pull_request: 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: env:
# The Godot build the team runs (godot --version -> 4.7.beta.custom_build.417cd33bf). Pin this to a # 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: "" GODOT_URL: ""
jobs: 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: test:
name: Lint & headless tests name: Lint & headless tests
runs-on: ubuntu-latest # must match a label your act_runner registered with runs-on: ubuntu-latest # must match a label your act_runner registered with
needs: guard
if: ${{ needs.guard.outputs.run == 'true' }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -46,11 +101,13 @@ jobs:
/tmp/venv/bin/pip install "gdtoolkit==4.*" /tmp/venv/bin/pip install "gdtoolkit==4.*"
echo "/tmp/venv/bin" >> "$GITHUB_PATH" echo "/tmp/venv/bin" >> "$GITHUB_PATH"
# First run imports assets and builds the .godot cache (gitignored) so headless tests can load # A cold .godot cache (CI always starts cold) needs two import passes: the first imports base
# the .glb/.png resources they instance. Import can exit non-zero on warnings. # 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 - name: Import resources
run: godot --headless --import run: |
continue-on-error: true godot --headless --import || true
godot --headless --import || true
- name: gdlint - name: gdlint
run: gdlint actors systems ui props levels tests run: gdlint actors systems ui props levels tests
@@ -60,6 +117,19 @@ jobs:
GODOT: godot GODOT: godot
run: bash run_tests.sh --no-gameplay 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 - name: Upload test output on failure
if: failure() if: failure()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4