A self-validating test suite that proves Gitea's implementations of GitHub-style actions behave like GitHub's. Push to any Gitea instance with a registered runner — on push or manual dispatch, it runs 10 independent test workflows covering checkout, artifacts, caching, language setup, expression contexts, workflow commands, services, composite/reusable workflows, control flow, and known divergences. Components: - lib/assert.sh — self-asserting shell helper library (assert_eq, assert_contains, assert_exists, assert_not_exists, assert_match, sha256_of) with verified negative fail path - 10 reusable workflows (on: workflow_call): 01-checkout, 02-artifacts, 03-cache, 04-setup-runtime, 05-contexts, 06-workflow-commands, 07-services, 08-composite-reusable, 09-control-flow, 10-gitea-divergences - 00-suite-runner.yml — sole entry (on: push + workflow_dispatch): probe gate (Gitea >= 1.22.0) -> c1-c10 parallel callers (c5/c8 forward secrets: inherit for act_runner #125) -> aggregate (if: always, skipped-as-failure) - Fixtures: LFS 5MB binary, git tag v1-test, 4 language lockfiles (npm/go/pip/maven), sparse-cone layout - Makefile: make lint (actionlint static gate) - README: prerequisites, per-test expectations, correction table, manual eyeball checklist for UI-only behaviors
gitea-actions-compat-test
A self-asserting compatibility suite for Gitea Actions. Push this repo to a Gitea
instance with a registered runner, and 00-suite-runner.yml runs ten independent
test workflows that prove Gitea's implementation of GitHub-style actions behaves
like GitHub's. Where Gitea diverges from upstream docs, the relevant check fails
loudly with a clear message instead of passing silently.
This README is the human-facing guide. It tells you what to set up before you push, what each test verifies, where the docs disagree with the source code, and which behaviors cannot be checked from inside a job (those live in the manual eyeball checklist at the bottom).
HARD prerequisites
These are non-negotiable. The probe job in 00-suite-runner.yml fails fast and
turns the whole run red if any of them is missing.
Runner
- A runner registered with the label used by
runs-on. The default label isubuntu-24.04. If your instance uses a different label, setRUNS_ON_LABEL(or editruns-on:in every workflow) to match, or the jobs will sit inwaitingforever.
Gitea version
-
Gitea >= 1.22.0. This is the minimum to PARSE/accept these workflows. It is NOT a guarantee that every component passes. Several checks (notably the
upload-artifact@v4regression and reusable-workflow secret passthrough) may still fail on 1.22.x and only turn green on later point releases. See the per-test table below for which components need a newer version.To be explicit about the two thresholds:
Threshold Meaning 1.22.0 Minimum to PARSE/accept the workflow YAML. Below this, the runner rejects the suite outright. higher, version-dependent Minimum to PASS all components. Each component that needs more lists its version note in the per-test table.
Storage and caches
- Storage backend enabled. The artifact and cache actions need object storage (local, MinIO, S3-compatible). Without it, uploads and downloads error out.
- Cache server enabled.
actions/cache@v3relies on the Gitea cache server. Confirm it is reachable before expecting C3 to pass. - LFS enabled.
test.binis an LFS object. The checkout LFS test (C1) and the fixtures depend ongit lfsworking on both the server and the runner.
Action mirroring
- Action mirroring allowed.
ACTIONS_URLandDEFAULT_ACTIONS_URLmust NOT be set to a non-mirror strategy. The workflows reference actions by theowner/repo@refshorthand (for exampleactions/checkout@v4), which Gitea resolves by mirroring the upstream action on first use. If mirroring is off or pointed at a custom non-mirror source, thoseuses:refs 404 and the run fails at fetch time. The mirror fetch is itself a behavior under test.
Secrets
- A repo secret
TEST_SECRET=placeholder. Define it in the repo settings. The reusable workflows that read it (05-contextsand08-composite-reusable) receive it through explicit forwarding (secrets: inherit) in the caller jobs, because Gitea's act_runner does not auto-forward caller secrets into reusable workflows by default (see act_runner #125 in the correction table). The placeholder valueplaceholderis what the assertion checks for, so do not put anything else there unless you also update the assertion.
Warm-up: run workflow_dispatch once
After first setup, trigger the suite once via the Run workflow button
(workflow_dispatch) before drawing any conclusions from a push run. Action
mirroring is lazy. The first fetch of each actions/* ref can take a moment to
populate, and a cold run may show spurious fetch failures that vanish on the
second attempt. One warm-up dispatch lets the mirror settle.
Per-test expectations
The suite is split into ten reusable workflows (on: workflow_call), each
invoked by a caller job (c1 through c10) in 00-suite-runner.yml. Each
component reports its own pass/fail, and aggregate fails the whole run if any
component is not success (a skipped or cancelled component counts as
failure).
| # | Workflow | What it verifies | Expected result | Min-version notes |
|---|---|---|---|---|
| C1 | 01-checkout |
actions/checkout@v4 variants: default checkout, fetch-depth, lfs, ref to a tag, path, sparse-checkout cone, and persist-credentials (both true and false, with a non-vacuous assertion that the embedded credential token differs between the two). |
Pass on Gitea >= 1.22 with LFS enabled. The LFS sub-case is conditional on git lfs being present. |
1.22.0 to parse. LFS sub-case needs LFS enabled on both server and runner. |
| C2 | 02-artifacts |
Artifacts v3 and v4 round-trip, cross-job checksum comparison (producer writes, consumer verifies sha256). | v3 passes. v4 MAY fail on Gitea 1.22 (the GHES regression error). Passes on fixed versions. | v4 needs a point release after the #31256 fix. v3 works on 1.22. |
| C3 | 03-cache |
Cache save/restore: a seed job writes a file and caches it (first run is a miss that saves), a restore job asserts an exact-key hit, then a miss-with-restore-keys fallback that restores the file but reports cache-hit == 'false'. |
Pass if the cache server is configured and reachable. | Probe gates this; no extra version requirement beyond 1.22. |
| C4 | 04-setup-runtime |
setup-node, setup-python, setup-go, setup-java (Temurin, Maven) in a matrix, each asserting the installed version and exercising the action's built-in cache: option against the committed lockfiles. |
Pass if the lockfiles from T2 are present (package-lock.json, go.sum, requirements.txt, pom.xml). Java is Maven, not Gradle. |
1.22.0. Runtime downloads depend on upstream availability. |
| C5 | 05-contexts |
github.sha / github.ref / github.run_id, a job env: var round-tripping into a step, the auto-injected GITHUB_TOKEN, the forwarded TEST_SECRET (asserted == 'placeholder'), needs outputs consumed by a dependent job, and if: conditionals with success() / failure() / always(). |
Pass. TEST_SECRET must be forwarded by the caller; the assertion distinguishes "empty because caller did not forward" from "empty because the repo secret is unset". cancelled() is manual-only (see eyeball checklist). |
1.22.0 to parse. Secret forwarding depends on act_runner #125 (see correction table). |
| C6 | 06-workflow-commands |
GITHUB_OUTPUT (multi-line value, consumed via step output), GITHUB_PATH (added dir becomes callable), GITHUB_ENV (set in one step, read in the next), and ::warning:: / ::error:: / ::notice:: annotations plus $GITHUB_STEP_SUMMARY. |
Pass. The annotation assertion is emit-only: it checks the command was accepted and the process did not crash. Annotation RENDERING in the UI is a manual checklist item. | 1.22.0. Rendering is UI-dependent (manual). |
| C7 | 07-services |
postgres:16 and redis:7 services with healthchecks, connectivity asserted from the job after healthcheck passes, plus a container: node:20 job that checks out the repo and runs an assertion inside the container. |
Pass if Docker is available on the runner. May be flaky on resource-constrained runners. The service healthcheck is the source of truth for "started" vs "harness cannot reach it". | 1.22.0. Docker on the runner is required. |
| C8 | 08-composite-reusable |
A local composite action (lib/composite-greet) with input/output round-trip, and a reusable workflow that declares inputs and secrets on its workflow_call trigger and asserts ${{ inputs.who }} and the forwarded ${{ secrets.TEST_SECRET }} are populated. |
This is an intentional divergence probe for act_runner #125. May fail on buggy versions where reusable-workflow secrets are not forwarded. | Version-dependent. See the note below. |
| C9 | 09-control-flow |
timeout-minutes (a job that sleeps past the limit, asserted dead from a dependent if: always() job as needs.slow.result != 'success'), continue-on-error (a failing step tolerated, dependent job sees needs.flaky.result == 'success'), and concurrency as parse-and-accept only. |
Pass. The timeout result string (failure vs cancelled) is version-dependent and asserted as != 'success' to avoid the ambiguity. Concurrency cancellation is manual-only. |
1.22.0. Cancellation is server-side (manual). |
| C10 | 10-gitea-divergences |
environment: non-blocking proof (a job with environment: test-env runs to completion without blocking, asserted from a dependent job as needs.env.result == 'success'), the upload-artifact@v4 regression re-confirmed, and problem matchers emitted (emit-only). |
environment: is unsupported in act and treated as a no-op, so the job runs through. The v4 upload re-surfaces the #31256 regression on affected versions. Matcher rendering is manual-only. |
1.22.0 to parse. v4 needs the fix from #31256. |
Correction table: where docs and source disagree
GitHub's public docs describe the canonical runtime. Gitea's act implementation
sometimes diverges. The table below records the known divergences with source
citations and the covering test. Keep this bounded: at most 10 rows.
| topic | doc-claim | source-truth | citation | covering-test |
|---|---|---|---|---|
timeout-minutes |
Docs say it cancels a runaway job. | Source enforces it. job_executor.go around L370-380 applies the timeout and kills the job. |
act source (job_executor.go L370-380) |
C9 |
success / failure / cancelled / always |
Docs describe four status-check functions. | All four are implemented. interpreter.go around L621-640 defines them. |
act source (interpreter.go L621-640) |
C5 |
environment: |
Docs say it gates deployments behind protection rules. | Genuinely unsupported. There is no environment field on the Job struct in act, so the key is parsed but treated as a no-op. |
act source (Job struct) | C10 |
upload-artifact@v4 |
Docs frame v4 as a drop-in replacement for v3. | Not on Gitea 1.22. v4 triggers a GHES-style regression error. Reported fixed but may recur on certain configurations (see Gitea issue #36024 for a Nov 2025 recurrence on Docker-based runners). | Gitea issue #31256 | C2, C10 |
| Reusable-workflow secret inheritance | Docs say secrets auto-forward to called workflows. | They do not by default. You must pass secrets: inherit (or list them explicitly), otherwise the called workflow sees empty secrets. |
act_runner #125 | C5, C8 |
continue-on-error |
Docs say the job reports success to dependents despite a tolerated step failure. | Source honors this. workflow.go SetContinueOnError around L216-227 sets the result. |
act source (workflow.go L216-227) |
C9 |
| Problem matchers | Docs say registered matchers render annotations in the UI. | Registration is accepted, but rendering depends on the Gitea UI and cannot be observed from inside the job. | act source (matcher registration path) | C6, C10 |
concurrency: cancel-in-progress |
Docs say an in-flight run is cancelled when a newer run starts. | Enforcement is server-side and not observable from within the run. The suite parses and accepts the key only. | Gitea server-side enforcement | C9 |
reusable-workflow secret passthrough fix version
act_runner #125 is now CLOSED, fixed via act PR #41 ("Parse secret inputs in reusable workflows"). This suite still tests the behavior empirically in C8 (and the forwarding path in C5) rather than assuming a version, because the fix landed across several act and Gitea releases and older instances may still be affected. If C8 fails on your instance, the most likely cause is that you are running a version where the fix has not landed yet. Check the act_runner #125 thread for the current status rather than gating on a specific Gitea version number.
Manual eyeball checklist (UI-only behaviors)
A workflow cannot observe its own rendered UI. These behaviors are emitted where possible and left for a human to confirm in the Gitea web interface after a run.
- Annotation rendering. Open the run for
06-workflow-commands. Confirm the::warning::,::error::, and::notice::messages appear as rendered annotations on the relevant steps (not just as raw text in the log). - Step summary rendering. In the same run, confirm the content written to
$GITHUB_STEP_SUMMARYappears in the run's summary view. - Run cancellation. Trigger a run, then cancel it from the UI while it is
executing. Confirm the run transitions to a cancelled state and that the
aggregatejob reflects it as a failure (not green). - Deployment environment gate. Note that
environment:is treated as a no-op by act (see the correction table). There is no protection-rule gate to enforce, so this item is about confirming that absence rather than testing a gate. If you need real environment gating, that is a known gap.
Repository layout
.github/
actionlint.yaml narrowly-scoped lint config
problem-matcher.json problem matcher fixture
workflows/
00-suite-runner.yml entry: probe -> c1..c10 -> aggregate
01-checkout.yml C1
02-artifacts.yml C2
03-cache.yml C3
04-setup-runtime.yml C4
05-contexts.yml C5
06-workflow-commands.yml C6
07-services.yml C7
08-composite-reusable.yml C8
09-control-flow.yml C9
10-gitea-divergences.yml C10
lib/
assert.sh self-asserting shell helpers
composite-greet/action.yml local composite action (used by C8)
fixtures/ sha256 of the LFS object, etc.
sparse-cone/ layout for sparse-checkout tests
Makefile `make lint` runs actionlint
Numeric filename prefixes do NOT imply execution order. Ordering comes only from
needs:. The prefixes exist to give humans a stable reading order.
Running the suite
- Push the repo to your Gitea instance.
- Confirm every HARD prerequisite above (runner label, version, storage, cache,
LFS, mirroring,
TEST_SECRET). - Trigger one warm-up
workflow_dispatch. - Read the per-test results.
aggregateis green only when the probe and everyc1..c10aresuccess. - Walk the manual eyeball checklist for the UI-only behaviors.