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:
+74
-4
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user