name: CI # Gitea Actions is GitHub-Actions-compatible: drop this in .gitea/workflows/, enable Actions on the # repo ([actions] ENABLED=true in app.ini, Gitea >= 1.19) and register an act_runner — it then runs # on every push / PR. Godot and gdtoolkit are installed with plain shell steps (not the chickensoft # setup-godot action), because a 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 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 # PUBLISHED build from https://github.com/godotengine/godot-builds/releases. If you run a custom # build with no public binary, host the linux headless zip where the runner can reach it and set # GODOT_URL to that (it overrides the constructed URL below). GODOT_VERSION: "4.7-beta1" 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 - name: Install Godot ${{ env.GODOT_VERSION }} run: | set -eux url="${GODOT_URL:-https://github.com/godotengine/godot-builds/releases/download/${GODOT_VERSION}/Godot_v${GODOT_VERSION}_linux.x86_64.zip}" curl -fL "$url" -o /tmp/godot.zip unzip -o /tmp/godot.zip -d /tmp/godot install -m 755 /tmp/godot/Godot_v*_linux.x86_64 /usr/local/bin/godot godot --version - name: Install gdlint (gdtoolkit) # A venv sidesteps PEP-668 "externally managed" pip on recent runner images; putting its bin on # GITHUB_PATH lets run_tests.sh call bare `gdlint` in later steps. run: | set -eux python3 -m venv /tmp/venv /tmp/venv/bin/pip install "gdtoolkit==4.*" echo "/tmp/venv/bin" >> "$GITHUB_PATH" # 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 || true godot --headless --import || true - name: gdlint run: gdlint actors systems ui props levels tests - name: Headless tests env: 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 with: name: test-output path: tests/output/ if-no-files-found: ignore