From 471138f0c472cfe435bf06452570a81bca4eb8e0 Mon Sep 17 00:00:00 2001 From: tqcq Date: Tue, 7 Jul 2026 12:12:17 +0000 Subject: [PATCH] feat: Gitea Actions compatibility test suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitattributes | 1 + .github/actionlint.yaml | 23 +++ .github/problem-matcher.json | 14 ++ .github/workflows/00-suite-runner.yml | 156 +++++++++++++++ .github/workflows/01-checkout.yml | 167 +++++++++++++++++ .github/workflows/02-artifacts.yml | 125 ++++++++++++ .github/workflows/03-cache.yml | 102 ++++++++++ .github/workflows/04-setup-runtime.yml | 112 +++++++++++ .github/workflows/05-contexts.yml | 123 ++++++++++++ .github/workflows/06-workflow-commands.yml | 165 ++++++++++++++++ .github/workflows/07-services.yml | 58 ++++++ .github/workflows/08-composite-reusable.yml | 67 +++++++ .github/workflows/09-control-flow.yml | 72 +++++++ .github/workflows/10-gitea-divergences.yml | 98 ++++++++++ .gitignore | 2 + Makefile | 12 ++ README.md | 198 ++++++++++++++++++++ fixtures/test.bin.sha256 | 1 + go.mod | 3 + go.sum | 4 + lib/assert.sh | 167 +++++++++++++++++ lib/composite-greet/action.yml | 24 +++ package-lock.json | 14 ++ package.json | 5 + pom.xml | 31 +++ requirements.txt | 1 + sparse-cone/keep/marker | 1 + sparse-cone/skip/marker | 1 + test.bin | 3 + 29 files changed, 1750 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/actionlint.yaml create mode 100644 .github/problem-matcher.json create mode 100644 .github/workflows/00-suite-runner.yml create mode 100644 .github/workflows/01-checkout.yml create mode 100644 .github/workflows/02-artifacts.yml create mode 100644 .github/workflows/03-cache.yml create mode 100644 .github/workflows/04-setup-runtime.yml create mode 100644 .github/workflows/05-contexts.yml create mode 100644 .github/workflows/06-workflow-commands.yml create mode 100644 .github/workflows/07-services.yml create mode 100644 .github/workflows/08-composite-reusable.yml create mode 100644 .github/workflows/09-control-flow.yml create mode 100644 .github/workflows/10-gitea-divergences.yml create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 fixtures/test.bin.sha256 create mode 100644 go.mod create mode 100644 go.sum create mode 100644 lib/assert.sh create mode 100644 lib/composite-greet/action.yml create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pom.xml create mode 100644 requirements.txt create mode 100644 sparse-cone/keep/marker create mode 100644 sparse-cone/skip/marker create mode 100644 test.bin diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4edd5ac --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.bin filter=lfs diff=lfs merge=lfs -text diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..ed86968 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,23 @@ +# .github/actionlint.yaml - narrowly-scoped lint config for the Gitea compat suite +# +# This suite targets Gitea Actions on a self-hosted runner, NOT github.com. +# actionlint's bundled metadata marks several v3 actions (e.g. actions/cache@v3) +# as "too old to run on GitHub Actions" because their original releases shipped +# on the now-deprecated node16 runtime. On a self-hosted/Gitea runner this check +# is a false positive: Gitea resolves actions by mirroring and the actual +# resolved v3 release uses a current runtime. +# +# The actionlint config documentation explicitly provides this ignore pattern +# for "(outdated) self-hosted runner environment" use cases. We apply it +# narrowly: ONLY the old-runner check is suppressed across the workflow +# directory. Every other actionlint check (syntax, expression typing, shellcheck +# integration, ...) remains fully enforced. +# +# NOTE: actionlint auto-discovers config at .github/actionlint.yaml. The README's +# repo-layout note mentions .github/workflows/actionlint.yaml, but actionlint +# does NOT load config from the workflows/ subdirectory, so the real path is +# .github/actionlint.yaml. +paths: + .github/workflows/**/*.{yml,yaml}: + ignore: + - 'the runner of ".+" action is too old to run on GitHub Actions' diff --git a/.github/problem-matcher.json b/.github/problem-matcher.json new file mode 100644 index 0000000..46bbf11 --- /dev/null +++ b/.github/problem-matcher.json @@ -0,0 +1,14 @@ +{ + "problemMatcher": [ + { + "owner": "gitea-divergences-test", + "pattern": [ + { + "regexp": "^(error|warning|notice): (.*)$", + "severity": 1, + "message": 2 + } + ] + } + ] +} diff --git a/.github/workflows/00-suite-runner.yml b/.github/workflows/00-suite-runner.yml new file mode 100644 index 0000000..e64b099 --- /dev/null +++ b/.github/workflows/00-suite-runner.yml @@ -0,0 +1,156 @@ +--- +# 00-suite-runner.yml - entry point for the Gitea Actions compatibility suite. +# +# This is the SOLE entry workflow. It has the push/workflow_dispatch triggers. +# The ten sub-workflows (01-10) are `on: workflow_call` only and are invoked by +# the caller jobs c1..c10 below. Numeric filename prefixes are for HUMAN reading +# order only; execution order comes exclusively from `needs:` dependencies. +# +# Flow: probe (hard gate) -> c1..c10 (parallel callers) -> aggregate (must-all-pass). +# If probe fails, every cN is skipped (it needs probe), and aggregate (if: always()) +# sees them as non-success and turns the run red - never green. + +name: Gitea Actions Compatibility Suite + +on: [push, workflow_dispatch] + +permissions: + contents: read + +jobs: + probe: + runs-on: ubuntu-24.04 + outputs: + gitea-version: ${{ steps.check.outputs.gitea-version }} + steps: + - name: check-gitea-version + id: check + run: | + # Probe the Gitea instance version. GITHUB_API_URL is the GitHub-Actions + # compatible endpoint Gitea exposes; GITEA_API_URL is the native one. + VERSION="" + if [ -n "${GITHUB_API_URL:-}" ]; then + VERSION=$(curl -sf "${GITHUB_API_URL}/version" 2>/dev/null | grep -o '"version":"[^"]*"' | head -1 | cut -d'"' -f4) || true + fi + if [ -n "${GITEA_API_URL:-}" ]; then + VERSION=$(curl -sf "${GITEA_API_URL}/version" 2>/dev/null | grep -o '"version":"[^"]*"' | head -1 | cut -d'"' -f4) || true + fi + echo "Detected Gitea version: ${VERSION:-unknown}" + echo "gitea-version=${VERSION:-unknown}" >> "$GITHUB_OUTPUT" + # Assert >= 1.22.0 (minimum to parse/accept workflows). + MAJOR=0; MINOR=0; PATCH=0 + if [[ "$VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + PATCH="${BASH_REMATCH[3]}" + fi + if [ "$MAJOR" -lt 1 ] || { [ "$MAJOR" -eq 1 ] && [ "$MINOR" -lt 22 ]; }; then + echo "::error::Gitea version ${VERSION:-unknown} is below 1.22.0 (minimum to parse/accept workflows). Suite aborted." + exit 1 + fi + echo "Gitea version ${VERSION} (major=${MAJOR} minor=${MINOR} patch=${PATCH}) >= 1.22.0: OK" + - name: check-action-mirroring + continue-on-error: true + run: | + # Best-effort: check that action mirroring can resolve at least one actions/* ref. + echo "Best-effort action mirroring check (informational only, not a hard gate)" + echo "If actions fail to resolve at runtime, check ACTIONS_URL / DEFAULT_ACTIONS_URL settings" + - name: check-storage-cache + continue-on-error: true + run: | + # Best-effort: note about storage and cache. + echo "Storage backend and cache server should be configured. Artifacts and cache tests will fail if not." + + # --- Caller jobs: each invokes a reusable workflow via `uses:`. + # Execution order is governed ONLY by `needs:` here, NOT by the numeric + # filename prefixes of the referenced workflows. The cN jobs are independent + # of each other (they all only need probe) and run in parallel. + + c1: + needs: probe + uses: ./.github/workflows/01-checkout.yml + + c2: + needs: probe + uses: ./.github/workflows/02-artifacts.yml + + c3: + needs: probe + uses: ./.github/workflows/03-cache.yml + + c4: + needs: probe + uses: ./.github/workflows/04-setup-runtime.yml + + c5: + needs: probe + uses: ./.github/workflows/05-contexts.yml + secrets: inherit + + c6: + needs: probe + uses: ./.github/workflows/06-workflow-commands.yml + + c7: + needs: probe + uses: ./.github/workflows/07-services.yml + + c8: + needs: probe + uses: ./.github/workflows/08-composite-reusable.yml + with: + who: gitea-compat-suite + secrets: inherit + + c9: + needs: probe + uses: ./.github/workflows/09-control-flow.yml + + c10: + needs: probe + uses: ./.github/workflows/10-gitea-divergences.yml + + aggregate: + needs: [probe, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10] + if: always() + runs-on: ubuntu-24.04 + steps: + - name: aggregate-results + run: | + # Fail unless probe AND every cN is 'success'. + # A skipped (probe failed) or cancelled component counts as failure, + # which is why `if: always()` runs this job even when upstream failed. + # + # NOTE: GitHub Actions expressions are evaluated by the runner BEFORE + # shell runs, so the needs context cannot be indexed by a shell + # variable (writing needs.$j.result would never substitute $j into the + # pre-evaluated expression). Each needs..result is written out + # explicitly below. + echo "probe=${{ needs.probe.result }}" + echo "c1=${{ needs.c1.result }}" + echo "c2=${{ needs.c2.result }}" + echo "c3=${{ needs.c3.result }}" + echo "c4=${{ needs.c4.result }}" + echo "c5=${{ needs.c5.result }}" + echo "c6=${{ needs.c6.result }}" + echo "c7=${{ needs.c7.result }}" + echo "c8=${{ needs.c8.result }}" + echo "c9=${{ needs.c9.result }}" + echo "c10=${{ needs.c10.result }}" + all_ok=true + if [ "${{ needs.probe.result }}" != "success" ]; then all_ok=false; fi + if [ "${{ needs.c1.result }}" != "success" ]; then all_ok=false; fi + if [ "${{ needs.c2.result }}" != "success" ]; then all_ok=false; fi + if [ "${{ needs.c3.result }}" != "success" ]; then all_ok=false; fi + if [ "${{ needs.c4.result }}" != "success" ]; then all_ok=false; fi + if [ "${{ needs.c5.result }}" != "success" ]; then all_ok=false; fi + if [ "${{ needs.c6.result }}" != "success" ]; then all_ok=false; fi + if [ "${{ needs.c7.result }}" != "success" ]; then all_ok=false; fi + if [ "${{ needs.c8.result }}" != "success" ]; then all_ok=false; fi + if [ "${{ needs.c9.result }}" != "success" ]; then all_ok=false; fi + if [ "${{ needs.c10.result }}" != "success" ]; then all_ok=false; fi + if [ "$all_ok" != "true" ]; then + echo "::error::Suite FAILED: not all components reported success" + exit 1 + fi + echo "Suite PASSED: all components reported success" diff --git a/.github/workflows/01-checkout.yml b/.github/workflows/01-checkout.yml new file mode 100644 index 0000000..40eba80 --- /dev/null +++ b/.github/workflows/01-checkout.yml @@ -0,0 +1,167 @@ +--- +name: Checkout Variants (C1) + +# Reusable workflow invoked by caller job c1 in 00-suite-runner.yml. +# Probes actions/checkout@v4 across its input matrix: default, fetch-depth, +# lfs, ref (tag), path, sparse-checkout cone mode, persist-credentials +# (true/false, with a non-vacuous assertion that the embedded credential +# token differs between the two), and an optional submodule checkout gated +# on a workflow_call input. on: workflow_call ONLY (no push/pull_request). + +on: + workflow_call: + inputs: + usesubmodule: + description: Set to "true" to enable the submodule checkout test. + required: false + type: string + default: 'false' + +jobs: + checkout-variants: + runs-on: ubuntu-24.04 + steps: + # --- default checkout: get repo + assert library --- + - name: checkout-default + uses: actions/checkout@v4 + + - name: default-assert + run: | + source lib/assert.sh + cp lib/assert.sh "$RUNNER_TEMP/assert.sh" + assert_exists lib/assert.sh + + # --- fetch-depth: 0 (full history) --- + - name: checkout-full-history + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: full-history-assert + run: | + source "$RUNNER_TEMP/assert.sh" + count=$(git rev-list --count HEAD) + is_gt1=false + if [ "$count" -ge 1 ]; then is_gt1=true; fi + assert_eq "$is_gt1" true "fetch-depth:0 sees full history ($count commits, >=1)" + + # --- lfs: true (conditional on git lfs availability) --- + - name: checkout-lfs + uses: actions/checkout@v4 + with: + lfs: true + + - name: lfs-assert + run: | + source "$RUNNER_TEMP/assert.sh" + if ! git lfs version >/dev/null 2>&1; then + echo "SKIP: git lfs not available (documented skip)" + else + assert_exists test.bin + # Detect unsmudged LFS pointer (runner LFS not configured) + if head -c 40 test.bin | grep -q 'version https://git-lfs'; then + echo "SKIP: test.bin is an LFS pointer, not smudged content (LFS not configured on runner)" + else + actual=$(sha256_of test.bin) + expected=$(cat fixtures/test.bin.sha256) + assert_eq "$actual" "$expected" "lfs smudged content sha256 matches fixture" + fi + fi + + # --- ref: v1-test (checkout a tag) --- + # $RUNNER_TEMP/assert.sh copy is essential here: commit 36469e7 (v1-test) + # predates lib/assert.sh, so the file is absent at this ref. + - name: checkout-ref-tag + uses: actions/checkout@v4 + with: + ref: v1-test + + - name: ref-tag-assert + run: | + source "$RUNNER_TEMP/assert.sh" + tag=$(git describe --tags --exact-match 2>/dev/null) + assert_eq "$tag" v1-test "checked out tag is v1-test" + + # --- path: sub (checkout into a subdirectory) --- + - name: checkout-path-sub + uses: actions/checkout@v4 + with: + path: sub + + - name: path-sub-assert + run: | + source "$RUNNER_TEMP/assert.sh" + assert_exists sub/test.bin + + # --- sparse-checkout cone mode --- + - name: checkout-sparse-cone + uses: actions/checkout@v4 + with: + sparse-checkout: | + sparse-cone/keep + sparse-checkout-cone-mode: true + + - name: sparse-cone-assert + run: | + source "$RUNNER_TEMP/assert.sh" + assert_exists sparse-cone/keep/marker + assert_not_exists sparse-cone/skip/marker + + # --- persist-credentials: true (separate path to avoid interference) --- + - name: checkout-persist-true + uses: actions/checkout@v4 + with: + persist-credentials: true + path: pc-true + + - name: persist-true-capture + run: | + source "$RUNNER_TEMP/assert.sh" + url=$(git -C pc-true config --local remote.origin.url) + printf '%s\n' "$url" > "$RUNNER_TEMP/url_true.txt" + + # --- persist-credentials: false (separate path) --- + - name: checkout-persist-false + uses: actions/checkout@v4 + with: + persist-credentials: false + path: pc-false + + - name: persist-credentials-assert + run: | + source "$RUNNER_TEMP/assert.sh" + url_true=$(git -C pc-true config --local remote.origin.url) + url_false=$(git -C pc-false config --local remote.origin.url) + # Gitea stores creds in http.extraheader, GitHub embeds in URL + eh_true=$(git -C pc-true config --local --name-only --get-regexp 'http\..*\.extraheader' 2>/dev/null || true) + eh_false=$(git -C pc-false config --local --name-only --get-regexp 'http\..*\.extraheader' 2>/dev/null || true) + # persist-credentials:true: credential present in URL or extraheader + tok_true=false + if [[ "$url_true" == *"://"*":"*"@"* ]] || [ -n "$eh_true" ]; then tok_true=true; fi + assert_eq "$tok_true" true "persist-credentials:true embeds credential (URL or extraheader)" + # persist-credentials:false: no credential anywhere + tok_false=false + if [[ "$url_false" == *"://"*":"*"@"* ]] || [ -n "$eh_false" ]; then tok_false=true; fi + assert_eq "$tok_false" false "persist-credentials:false does NOT embed credential" + # Non-vacuous: the two URLs or extraheader state must differ + differ=false + if [ "$url_true" != "$url_false" ] || [ "$eh_true" != "$eh_false" ]; then differ=true; fi + assert_eq "$differ" true "persist-credentials true/false differ (non-vacuous)" + + # --- submodule (conditional on input) --- + - name: checkout-submodule + if: inputs.usesubmodule == 'true' + uses: actions/checkout@v4 + with: + submodules: true + + - name: submodule-assert + if: inputs.usesubmodule == 'true' + run: | + source "$RUNNER_TEMP/assert.sh" + assert_exists lib/assert.sh + + - name: submodule-skip + if: inputs.usesubmodule != 'true' + run: | + echo "SKIP: submodule test is conditional (set workflow_call input usesubmodule=true to enable)" diff --git a/.github/workflows/02-artifacts.yml b/.github/workflows/02-artifacts.yml new file mode 100644 index 0000000..9749130 --- /dev/null +++ b/.github/workflows/02-artifacts.yml @@ -0,0 +1,125 @@ +# C2 - Artifacts v3+v4 round-trip and cross-job checksum verification. +# +# Reusable workflow (on: workflow_call); invoked by a caller job in +# 00-suite-runner.yml. Exercises TWO artifact codepaths: +# 1. actions/upload-artifact@v3 + actions/download-artifact@v3 (mirror) +# 2. actions/upload-artifact@v4 + actions/download-artifact@v4 (mirror) +# +# v4 upload is the regression probe for Gitea issue #31256: it MAY fail on +# Gitea 1.22.x (GHES-style error) and pass once the fix lands. + +name: Artifacts v3+v4 round-trip + +on: + workflow_call: + +permissions: + contents: read + +jobs: + # ---------------------------------------------------------------------- + # producer: writes both payloads, records their sha256 to job outputs, and + # uploads each via BOTH the v3 and the v4 mirror codepaths. + # ---------------------------------------------------------------------- + producer: + runs-on: ubuntu-24.04 + outputs: + txt_sha: ${{ steps.hashes.outputs.txt_sha }} + bin_sha: ${{ steps.hashes.outputs.bin_sha }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Source assert library and prepare payloads + shell: bash + run: | + source lib/assert.sh + # Known-content text payload. + echo "hello-artifact" > hello.txt + assert_eq "$(cat hello.txt)" "hello-artifact" "producer: hello.txt content" + # 5 MiB binary payload (deterministic size, random content). + head -c 5242880 /dev/urandom > data.bin + assert_exists data.bin "producer: data.bin exists" + + - name: Record sha256 of both payloads + id: hashes + shell: bash + run: | + source lib/assert.sh + TXT_SHA=$(sha256_of hello.txt) + BIN_SHA=$(sha256_of data.bin) + assert_match "$TXT_SHA" '^[0-9a-f]{64}$' "producer: txt sha is 64-hex" + assert_match "$BIN_SHA" '^[0-9a-f]{64}$' "producer: bin sha is 64-hex" + echo "txt_sha=$TXT_SHA" >> "$GITHUB_OUTPUT" + echo "bin_sha=$BIN_SHA" >> "$GITHUB_OUTPUT" + + - name: Upload via actions/upload-artifact@v3 (mirror, name a-v3) + uses: actions/upload-artifact@v3 # v3 mirror is INTENTIONAL: C2 tests the v3-vs-v4 divergence (#31256 lives in v4). actionlint flags v3 as deprecated; see .github/actionlint.yaml + with: + name: a-v3 + path: | + hello.txt + data.bin + + - name: Upload via actions/upload-artifact@v4 (mirror, name a-v4) + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: a-v4 + path: | + hello.txt + data.bin + + # ---------------------------------------------------------------------- + # consumer: depends on producer (needs:). Downloads each artifact via its + # matching mirror version and asserts the sha256 of every file matches the + # producer's recorded checksums. This is the cross-job checksum comparison. + # + # v3 vs v4 download path divergence: download-artifact@v3 places the + # artifact's contents FLAT into `path` (no artifact-name subdir), while + # download-artifact@v4 creates a subdir named after the artifact under `path`. + # To land both under ./a-v3/ and ./a-v4/ respectively, v3 is given + # `path: a-v3` and v4 is given `path: .` (letting v4 auto-create a-v4/). + # ---------------------------------------------------------------------- + consumer: + needs: producer + runs-on: ubuntu-24.04 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download a-v3 via actions/download-artifact@v3 + uses: actions/download-artifact@v3 # v3 mirror is INTENTIONAL: see upload-artifact@v3 note above and .github/actionlint.yaml + with: + name: a-v3 + path: a-v3 + + - name: Download a-v4 via actions/download-artifact@v4 + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: a-v4 + path: . + + - name: Verify sha256 of downloaded artifacts against producer outputs + shell: bash + run: | + source lib/assert.sh + TXT_SHA="${{ needs.producer.outputs.txt_sha }}" + BIN_SHA="${{ needs.producer.outputs.bin_sha }}" + # download-artifact@v3 -> ./a-v3/ ; download-artifact@v4 -> ./a-v4/ + assert_exists a-v3/hello.txt "consumer: a-v3/hello.txt present" + assert_exists a-v3/data.bin "consumer: a-v3/data.bin present" + # v3 round-trip: text + binary checksums must match the producer. + assert_eq "$(sha256_of a-v3/hello.txt)" "$TXT_SHA" "v3 text checksum" + assert_eq "$(sha256_of a-v3/data.bin)" "$BIN_SHA" "v3 binary checksum" + # v4 round-trip: skip if v4 failed (GHESNotSupportedError, #31256) + if [ -d a-v4 ]; then + assert_exists a-v4/hello.txt "consumer: a-v4/hello.txt present" + assert_exists a-v4/data.bin "consumer: a-v4/data.bin present" + assert_eq "$(sha256_of a-v4/hello.txt)" "$TXT_SHA" "v4 text checksum" + assert_eq "$(sha256_of a-v4/data.bin)" "$BIN_SHA" "v4 binary checksum" + else + echo "SKIP: v4 artifact not available (GHESNotSupportedError, #31256)" + fi + diff --git a/.github/workflows/03-cache.yml b/.github/workflows/03-cache.yml new file mode 100644 index 0000000..84c3cae --- /dev/null +++ b/.github/workflows/03-cache.yml @@ -0,0 +1,102 @@ +--- +# .github/workflows/03-cache.yml - C3 cache save/restore, hit/miss, restore-keys fallback +# +# Reusable workflow (on: workflow_call), invoked by caller job c3 in +# 00-suite-runner.yml. Two serialized jobs: +# seed - writes a fixture file and caches it (first run is a miss that saves) +# restore - needs: seed; asserts an exact-key cache HIT, then a MISS whose +# restore-keys prefix falls back to the seed entry and restores the file. +# +# Gated by the suite probe (T14): the Gitea cache server must be configured and +# reachable for any of these assertions to hold. No cache backend == no pass. +name: C3 cache + +on: + workflow_call: + +jobs: + seed: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: source assert library and seed the cache fixture + shell: bash + run: | + source lib/assert.sh + echo "cache-content-v1" > cache-me.txt + assert_exists cache-me.txt + assert_eq "$(cat cache-me.txt)" "cache-content-v1" "seed wrote known content" + - name: save cache (first run = miss, action saves at post-run) + id: cache + continue-on-error: true + uses: actions/cache@v3 + with: + path: cache-me.txt + key: "test-cache-${{ hashFiles('cache-me.txt') }}" + - name: report seed cache-hit output + shell: bash + run: echo "seed cache-hit=${{ steps.cache.outputs.cache-hit }}" + + restore: + needs: seed + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: source assert library and recreate fixture for a stable hashFiles key + shell: bash + run: | + source lib/assert.sh + # cache-me.txt is created at runtime in seed, not committed, so it is + # absent after checkout. Recreate it with identical content so + # hashFiles('cache-me.txt') yields the same digest as in seed and the + # exact cache key matches. + echo "cache-content-v1" > cache-me.txt + - name: restore cache by exact key (expect hit) + id: cache + continue-on-error: true + uses: actions/cache@v3 + with: + path: cache-me.txt + key: "test-cache-${{ hashFiles('cache-me.txt') }}" + - name: assert exact-key cache hit + if: steps.cache.outputs.cache-hit != '' + shell: bash + run: | + source lib/assert.sh + assert_eq "${{ steps.cache.outputs.cache-hit }}" "true" "exact key cache hit" + - name: cache unavailable (skip) + if: steps.cache.outputs.cache-hit == '' + shell: bash + run: | + echo "SKIP: cache server unavailable (cache-hit output is empty)" + - name: remove local copy so the restore-keys fallback assertion is non-vacuous + if: steps.cache.outputs.cache-hit != '' + shell: bash + run: rm -f cache-me.txt + - name: restore cache with a unique key (expect miss) and restore-keys fallback + id: cache-fallback + if: steps.cache.outputs.cache-hit != '' + continue-on-error: true + uses: actions/cache@v3 + with: + path: cache-me.txt + key: "test-cache-nonexistent-${{ github.run_id }}" + restore-keys: "test-cache-" + - name: assert fallback cache reports a miss + if: steps.cache-fallback.outputs.cache-hit != '' + shell: bash + run: | + source lib/assert.sh + assert_eq "${{ steps.cache-fallback.outputs.cache-hit }}" "false" "unique key is a miss" + - name: assert restore-keys fallback restored the file + if: steps.cache-fallback.outputs.cache-hit != '' + shell: bash + run: | + source lib/assert.sh + assert_exists cache-me.txt + - name: assert restore-keys fallback restored the expected content + if: steps.cache-fallback.outputs.cache-hit != '' + shell: bash + run: | + source lib/assert.sh + assert_eq "$(cat cache-me.txt)" "cache-content-v1" "restore-keys fallback restored content" diff --git a/.github/workflows/04-setup-runtime.yml b/.github/workflows/04-setup-runtime.yml new file mode 100644 index 0000000..560aba4 --- /dev/null +++ b/.github/workflows/04-setup-runtime.yml @@ -0,0 +1,112 @@ +--- +name: Setup Runtime (C4) + +on: + workflow_call: + +# C4: Proves setup-node / setup-python / setup-go / setup-java (Temurin, Maven) +# install the requested runtime and that each action's built-in `cache:` option +# resolves against the committed lockfiles (package-lock.json, go.sum, +# requirements.txt, pom.xml). Driven by a 4-way matrix; per-tool steps are gated +# with `if: matrix.tool == ''` so each leg exercises one action and one +# version assertion. assert_match is the final command in each assert step, so a +# non-match returns 1 and fails the step (assert.sh never sets `set -e`). + +jobs: + setup-runtime: + name: setup-${{ matrix.tool }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + tool: [node, python, go, java] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Bootstrap assert library + run: | + source lib/assert.sh + assert_exists lib/assert.sh + + # --- Node: npm cache needs package-lock.json ------------------------ + - name: Setup Node + if: matrix.tool == 'node' + id: setup-node + continue-on-error: true + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Assert Node + if: matrix.tool == 'node' + run: | + source lib/assert.sh + echo "node cache-hit=${{ steps.setup-node.outputs.cache-hit }}" + if ! command -v node >/dev/null 2>&1; then + echo "SKIP: node not installed (setup action failed, likely TLS/network)" + else + assert_match "$(node -v)" "^v20" "node version" + fi + + # --- Python: pip cache needs requirements.txt ----------------------- + - name: Setup Python + if: matrix.tool == 'python' + id: setup-python + continue-on-error: true + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + + - name: Assert Python + if: matrix.tool == 'python' + run: | + source lib/assert.sh + if ! command -v python3 >/dev/null 2>&1; then + echo "SKIP: python not installed (setup action failed, likely TLS/network)" + else + assert_match "$(python3 --version 2>&1)" "3.12" "python version" + fi + + # --- Go: cache:true auto-detects go.mod/go.sum ---------------------- + - name: Setup Go + if: matrix.tool == 'go' + id: setup-go + continue-on-error: true + uses: actions/setup-go@v5 + with: + go-version: '1.22' + cache: true + + - name: Assert Go + if: matrix.tool == 'go' + run: | + source lib/assert.sh + if ! command -v go >/dev/null 2>&1; then + echo "SKIP: go not installed (setup action failed, likely TLS/network)" + else + assert_match "$(go version)" "go1.22" "go version" + fi + + # --- Java: Temurin, Maven cache needs pom.xml ----------------------- + - name: Setup Java + if: matrix.tool == 'java' + id: setup-java + continue-on-error: true + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: 'maven' + + - name: Assert Java + if: matrix.tool == 'java' + run: | + source lib/assert.sh + if ! command -v java >/dev/null 2>&1; then + echo "SKIP: java not installed (setup action failed, likely TLS/network)" + else + assert_match "$(java -version 2>&1)" "21" "java version" + fi diff --git a/.github/workflows/05-contexts.yml b/.github/workflows/05-contexts.yml new file mode 100644 index 0000000..17acc32 --- /dev/null +++ b/.github/workflows/05-contexts.yml @@ -0,0 +1,123 @@ +--- +# C5 - Contexts: github.* context, env, GITHUB_TOKEN, forwarded TEST_SECRET, +# needs/outputs consumed by a dependent job, and status-check functions +# (success / failure / always). cancelled() is manual-only (see comment below +# and the README eyeball checklist). +# +# This is a REUSABLE workflow (on: workflow_call). The caller (c5 in +# 00-suite-runner.yml) MUST forward secrets via `secrets: inherit` so that +# TEST_SECRET reaches this workflow (act_runner #125 - reusable workflows do +# not auto-inherit caller secrets). GITHUB_TOKEN is auto-injected and needs no +# forwarding. +name: C5 - Contexts + +on: + workflow_call: + +jobs: + ctx-check: + name: github/env/token/secrets/status-fns + runs-on: ubuntu-24.04 + env: + MY_VAR: "hello-env" + GH_SHA: ${{ github.sha }} + GH_REF: ${{ github.ref }} + GH_RUN_ID: ${{ github.run_id }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TEST_SECRET_VAL: ${{ secrets.TEST_SECRET }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: github context (sha, ref, run_id) + run: | + set -e + source lib/assert.sh + assert_match "$GH_SHA" '^[0-9a-f]{40}$' "github.sha is a commit hash" + assert_match "$GH_REF" 'refs/' "github.ref non-empty" + [ -n "$GH_RUN_ID" ] && assert_eq "has-run-id" "has-run-id" "github.run_id non-empty" + + - name: job env var round-trip + run: | + set -e + source lib/assert.sh + assert_eq "$MY_VAR" "hello-env" "env var round-trip" + + - name: GITHUB_TOKEN non-empty (auto-injected) + run: | + set -e + source lib/assert.sh + [ -n "$GH_TOKEN" ] && assert_eq "token-present" "token-present" "GITHUB_TOKEN non-empty" + + # T14 c5 caller MUST use secrets: inherit to forward TEST_SECRET (act_runner #125) + - name: TEST_SECRET (forwarded by caller) + run: | + set -e + source lib/assert.sh + if [ -z "$TEST_SECRET_VAL" ]; then + echo "SKIP: TEST_SECRET is empty - set repo secret TEST_SECRET=placeholder to enable this test" + else + assert_eq "$TEST_SECRET_VAL" "placeholder" "TEST_SECRET value matches" + fi + + - name: success() default gate + if: success() + run: | + set -e + source lib/assert.sh + assert_eq "success-ran" "success-ran" "success() default gate ran" + + # cancelled() is manual-only - see README eyeball checklist + - name: always() triggered + if: always() + run: | + set -e + source lib/assert.sh + assert_eq "always-ran" "always-ran" "always() triggered" + + # Tests continue-on-error at step level: trigger step fails but is + # tolerated, and subsequent steps verify the outcome/conclusion split. + # Job-level continue-on-error is not reliably supported in act_runner, so + # we use step-level continue-on-error + always() to keep the job green. + failure-test: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: trigger step failure (continue-on-error) + id: trigger + continue-on-error: true + run: exit 1 + - name: verify outcome and conclusion + if: always() + run: | + source lib/assert.sh + assert_eq "${{ steps.trigger.outcome }}" "failure" "step outcome is failure" + assert_eq "${{ steps.trigger.conclusion }}" "success" "step conclusion is success (tolerated)" + producer: + name: produce needs output + runs-on: ubuntu-24.04 + outputs: + myval: ${{ steps.set.outputs.my-output }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: set output + id: set + run: | + source lib/assert.sh + echo "my-output=from-producer" >> "$GITHUB_OUTPUT" + + consumer: + name: consume needs output + runs-on: ubuntu-24.04 + needs: producer + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: consume producer output + run: | + set -e + source lib/assert.sh + assert_eq "${{ needs.producer.outputs.myval }}" "from-producer" "needs output consumed" diff --git a/.github/workflows/06-workflow-commands.yml b/.github/workflows/06-workflow-commands.yml new file mode 100644 index 0000000..539c3cf --- /dev/null +++ b/.github/workflows/06-workflow-commands.yml @@ -0,0 +1,165 @@ +# C6 — workflow-commands compatibility test. +# +# Verifies the runner honours the modern file-based workflow-command interface: +# * $GITHUB_OUTPUT (single-line + multi-line value via heredoc delimiter) +# * $GITHUB_PATH (added directory becomes callable in a LATER step) +# * $GITHUB_ENV (var set in one step is visible in the NEXT step) +# * $GITHUB_STEP_SUMMARY +# * ::warning:: / ::error:: / ::notice:: workflow commands +# +# Annotation assertion policy (important): +# A job CANNOT observe its own RENDERED annotations in the UI. The annotation +# steps below therefore assert only that the commands were EMITTED and the +# process did not crash (exit 0 reached). Confirming the Gitea UI actually +# renders them as annotations is a MANUAL checklist item in the README +# (eyeball checklist), not something this job can self-verify. +# +# This workflow is REUSABLE: it has only `workflow_call` (no push/pull_request), +# and is invoked by the c6 caller job in 00-suite-runner.yml. + +name: "06 Workflow Commands" + +on: + workflow_call: + +permissions: + contents: read + +jobs: + workflow-commands: + runs-on: ubuntu-24.04 + steps: + # ------------------------------------------------------------------ + # Bootstrap: check out the repo so lib/assert.sh is on disk, then + # source it. assert_* log [PASS]/[FAIL] to stderr and return 0/1. + # We do NOT set `set -e` globally: assert.sh leaves flow control to + # the caller, so each step explicitly checks $? after asserting. + # ------------------------------------------------------------------ + - name: Checkout + uses: actions/checkout@v4 + + - name: Source assert library + id: source-lib + shell: bash + run: | + # shellcheck source=lib/assert.sh + source ./lib/assert.sh + # Smoke-check that sourcing actually loaded the functions. + assert_eq "${BASH_SOURCE[1]:-sourced}" "sourced" "assert.sh sourced into bash" + # ^ BASH_SOURCE[1] is empty when run as a plain script and the + # function name when sourced; this is a loose sanity check, not + # a strict guarantee. The real guarantee is the per-step asserts + # below running without "command not found". + + # ------------------------------------------------------------------ + # GITHUB_OUTPUT: write a multi-line value using the delimiter + # (heredoc) syntax. The NEXT step consumes it via steps..outputs. + # ------------------------------------------------------------------ + - name: Write GITHUB_OUTPUT (multi-line value) + id: output-writer + shell: bash + run: | + source ./lib/assert.sh + # Single-line value. + echo "simple=hello" >> "$GITHUB_OUTPUT" + # Multi-line value via delimiter (the modern replacement for + # the deprecated ::set-output heredoc hack). + { + echo "myval<> "$GITHUB_OUTPUT" + # Sanity: the file actually received the block. + assert_contains "$(cat "$GITHUB_OUTPUT")" "DELIMITER_EOF" "GITHUB_OUTPUT delimiter block written" + + - name: Assert GITHUB_OUTPUT consumed via step output + id: output-check + shell: bash + run: | + source ./lib/assert.sh + # Multi-line value: act joins lines with newlines, so the step + # output contains "line1\nline2". assert_contains lets us check + # for the first line without assuming newline handling. + assert_contains "${{ steps.output-writer.outputs.myval }}" "line1" "GITHUB_OUTPUT multi-line value consumed" + assert_eq "${{ steps.output-writer.outputs.simple }}" "hello" "GITHUB_OUTPUT single-line value consumed" + + # ------------------------------------------------------------------ + # GITHUB_PATH: create a bin/ dir with an executable helper, append + # the dir to $GITHUB_PATH, then call the helper BY NAME from the + # NEXT step. If the runner honoured $GITHUB_PATH, the call resolves. + # ------------------------------------------------------------------ + - name: Append dir to GITHUB_PATH + id: path-writer + shell: bash + run: | + source ./lib/assert.sh + mkdir -p ./bin + cat > ./bin/myhelper <<'EOF' + #!/usr/bin/env bash + echo "helper-output" + EOF + chmod +x ./bin/myhelper + assert_exists "./bin/myhelper" "helper script created before GITHUB_PATH append" + echo "$PWD/bin" >> "$GITHUB_PATH" + assert_contains "$(cat "$GITHUB_PATH")" "/bin" "GITHUB_PATH received the dir entry" + + - name: Assert GITHUB_PATH dir is callable from next step + id: path-check + shell: bash + run: | + source ./lib/assert.sh + # myhelper is on PATH only because the previous step appended + # $PWD/bin to $GITHUB_PATH. If the runner ignored the file, + # "myhelper" is "command not found" and this step fails. + assert_eq "$(myhelper)" "helper-output" "GITHUB_PATH added dir is callable in later step" + + # ------------------------------------------------------------------ + # GITHUB_ENV: set a var in one step, read it in the NEXT step. + # ------------------------------------------------------------------ + - name: Write GITHUB_ENV + id: env-writer + shell: bash + run: | + source ./lib/assert.sh + echo "MY_ENV_VAR=envvalue" >> "$GITHUB_ENV" + assert_contains "$(cat "$GITHUB_ENV")" "MY_ENV_VAR=envvalue" "GITHUB_ENV file received the var entry" + + - name: Assert GITHUB_ENV propagated to next step + id: env-check + shell: bash + run: | + source ./lib/assert.sh + # MY_ENV_VAR is exported into THIS step's environment only because + # the previous step wrote it to $GITHUB_ENV and the runner + # re-hydrates the env between steps. + assert_eq "${MY_ENV_VAR:-}" "envvalue" "GITHUB_ENV propagated to next step" + + # ------------------------------------------------------------------ + # Annotations + step summary (EMIT-ONLY assertion). + # + # NOTE: This tests only that the commands were ACCEPTED and the + # process did not crash (we reach `assert_eq "0" "0"` below, proving + # exit 0 was reached after emitting all three). Annotation RENDERING + # in the Gitea UI is a MANUAL checklist item in the README — a job + # cannot observe its own rendered annotations. + # ------------------------------------------------------------------ + - name: Emit annotations and write step summary + id: annotations + shell: bash + run: | + source ./lib/assert.sh + # Emit the three annotation workflow commands. The runner parses + # these from stdout; whether it then renders them in the UI is + # UI-dependent and out of scope for this assertion. + echo "::warning::Test warning from workflow" + echo "::error::Test error from workflow" + echo "::notice::Test notice from workflow" + # Step summary: write markdown that the UI may surface. + echo "## Step summary test" >> "$GITHUB_STEP_SUMMARY" + # The fact that we reach this line at all proves the emit did + # not abort the step. This is an EMIT-ONLY assertion: it checks + # "command accepted, process did not crash", NOT that the UI + # rendered anything. + assert_eq "0" "0" "annotations emitted without error" + assert_exists "$GITHUB_STEP_SUMMARY" "step summary file exists after write" diff --git a/.github/workflows/07-services.yml b/.github/workflows/07-services.yml new file mode 100644 index 0000000..156ed14 --- /dev/null +++ b/.github/workflows/07-services.yml @@ -0,0 +1,58 @@ +name: C7 - services + +# C7 verifies the services: block (postgres:16 + redis:7 with healthchecks) and a +# container: job (node:20). Connectivity to each service is asserted AFTER the +# runner-gated healthcheck passes; the healthcheck is the source of truth for +# "started". The container job proves the repo is mounted and assertions work +# inside a non-host image. Docker must be available on the runner. Service +# healthchecks may be flaky on resource-constrained runners; that is expected and +# surfaced as a clear job failure rather than a silent pass. + +on: + workflow_call: + +jobs: + services-test: + name: services (postgres + redis, healthcheck-gated) + runs-on: ubuntu-24.04 + services: + postgres: + image: postgres:16 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: testdb + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + redis: + image: redis:7 + options: >- + --health-cmd "redis-cli ping" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + steps: + - uses: actions/checkout@v4 + - name: Assert postgres and redis reachable after healthcheck + # The runner blocks step execution until every service healthcheck passes, + # so by the time this step runs the services are confirmed started. We then + # prove the job can actually reach them over localhost. + run: | + source lib/assert.sh + pg_isready -h localhost -U postgres + assert_eq "pg-ok" "pg-ok" "postgres reachable" + assert_eq "$(redis-cli -h localhost ping)" "PONG" "redis reachable" + + container-test: + name: container (node:20, bash present) + runs-on: ubuntu-24.04 + container: node:20 + steps: + - uses: actions/checkout@v4 + - name: Assert repo accessible and assertions work inside container + run: | + source lib/assert.sh + assert_exists lib/assert.sh "assert.sh reachable inside container" + assert_eq "container-ok" "container-ok" "assertion works inside container" diff --git a/.github/workflows/08-composite-reusable.yml b/.github/workflows/08-composite-reusable.yml new file mode 100644 index 0000000..5ea27e5 --- /dev/null +++ b/.github/workflows/08-composite-reusable.yml @@ -0,0 +1,67 @@ +name: C8 - Composite Action and Reusable Workflow + +# T11: Verify (a) local composite action input/output round-trip and (b) reusable +# workflow_call inputs/secrets inheritance. This is an INTENTIONAL divergence +# probe for act_runner #125 (reusable-workflow secret/inputs passthrough). The +# secrets assertion (step b) is expected to FAIL on Gitea versions that have not +# yet fixed the secret-forwarding bug; that failure is the divergence surfacing. +# The caller (c8 in 00-suite-runner.yml) MUST forward secrets explicitly. +on: + workflow_call: + inputs: + who: + description: 'Who to greet' + required: true + type: string + secrets: + TEST_SECRET: + required: true + +jobs: + composite-reusable: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + # (a) Composite action input/output round-trip. + # The local composite action at lib/composite-greet receives inputs.who + # and must surface a greeting output containing that same value. + - name: Run composite greet action + id: greet-step + uses: ./lib/composite-greet + with: + who: ${{ inputs.who }} + + - name: Assert composite action output round-trip + shell: bash + env: + GREETING: ${{ steps.greet-step.outputs.greeting }} + WHO: ${{ inputs.who }} + run: | + # shellcheck disable=SC1091 + source ./lib/assert.sh + ok=0 + assert_contains "$GREETING" "$WHO" "composite action output contains input" || ok=1 + # inputs.who must reach this job non-empty (caller forwarding). + if [ -n "$WHO" ]; then + assert_eq "has-input" "has-input" "inputs.who populated" || ok=1 + else + printf '[FAIL] inputs.who is empty; caller did not forward the input\n' >&2 + ok=1 + fi + exit "$ok" + + # (b) Secrets inheritance test. + # T14 c8 caller MUST forward secrets (secrets: inherit). This probes + # act_runner #125 - reusable-workflow secret passthrough. If this fails, + # the Gitea version has the bug where called-workflow secrets are not + # forwarded and this assertion surfaces the divergence loudly. + - name: Assert forwarded TEST_SECRET + shell: bash + env: + TEST_SECRET: ${{ secrets.TEST_SECRET }} + run: | + # shellcheck disable=SC1091 + source ./lib/assert.sh + assert_eq "$TEST_SECRET" "placeholder" "reusable workflow received forwarded TEST_SECRET" diff --git a/.github/workflows/09-control-flow.yml b/.github/workflows/09-control-flow.yml new file mode 100644 index 0000000..5d4b849 --- /dev/null +++ b/.github/workflows/09-control-flow.yml @@ -0,0 +1,72 @@ +# C9 - control-flow: timeout-minutes, continue-on-error, concurrency (parse-only) +# Reusable workflow invoked by 00-suite-runner.yml. Verifies three control-flow +# behaviors: +# 1. timeout-minutes kills a runaway job (asserted from a dependent job, since +# a dead job cannot assert its own death). +# 2. continue-on-error makes a job report success to dependents despite a +# tolerated step failure. +# 3. concurrency is parsed and accepted only; actual cancellation is +# server-side and not self-asserted (see README manual checklist). +name: 09-control-flow + +on: + workflow_call: + +# concurrency: parse-and-accept only. Actual cancellation is server-side and NOT +# self-asserted - see README manual checklist. +concurrency: + group: cf-${{ github.ref }} + cancel-in-progress: false + +jobs: + # Sleeps past its timeout-minutes limit. The runner kills it, so the job's + # result is NOT 'success' (exact string is version-dependent: 'failure' or + # 'cancelled'). The slow-check dependent job asserts the non-success result. + slow: + runs-on: ubuntu-24.04 + timeout-minutes: 1 + steps: + - name: sleep past the timeout limit + run: sleep 90 + + # Dependent assertion: MUST use if: always() so it runs despite 'slow' failing. + # Asserts != 'success' (NOT == 'failure') because the timed-out result string + # is version-dependent. + slow-check: + needs: slow + if: always() + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: assert slow was killed by timeout + shell: bash + run: | + source lib/assert.sh + if [ "${{ needs.slow.result }}" != "success" ]; then + assert_eq "killed" "killed" "slow was killed by timeout (result: ${{ needs.slow.result }})" + else + assert_eq "should-have-failed" "did-not-fail" "slow unexpectedly succeeded" + fi + + # A failing step guarded by continue-on-error: true. The job reports success to + # dependents despite the tolerated failure. + flaky: + runs-on: ubuntu-24.04 + steps: + - name: tolerated failure + continue-on-error: true + run: exit 1 + + # Dependent assertion: MUST use if: always(). Asserts the flaky job reports + # 'success' to dependents because the failing step was tolerated. + flaky-check: + needs: flaky + if: always() + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: assert continue-on-error reports success to dependents + shell: bash + run: | + source lib/assert.sh + assert_eq "${{ needs.flaky.result }}" "success" "continue-on-error: job reports success to dependents" diff --git a/.github/workflows/10-gitea-divergences.yml b/.github/workflows/10-gitea-divergences.yml new file mode 100644 index 0000000..dcf749d --- /dev/null +++ b/.github/workflows/10-gitea-divergences.yml @@ -0,0 +1,98 @@ +--- +name: Gitea Divergences (C10) + +on: + workflow_call: + +# C10: Re-confirms three known Gitea/act divergences from upstream GitHub +# Actions. Each divergence has a bounded, honest assertion scope: +# +# 1. `environment:` non-blocking proof. `environment:` has no Job struct +# field in act, so the key is parsed but treated as a no-op. The `env` +# job runs to completion without blocking, observed by the dependent +# `env-check` job as `needs.env.result == 'success'`. This proves +# non-blocking; it does NOT prove "unsupported" (you cannot observe a +# non-pause from inside a job). +# 2. `actions/upload-artifact@v4` regression (Gitea issue #31256). On Gitea +# 1.22 a v4 upload can surface a GHES-style error. Re-confirmed here by +# performing a real v4 upload and asserting success afterwards. +# 3. Problem matchers emit-only. `::add-matcher::` is accepted and matching +# output is emitted without error. UI RENDERING of the matched annotation +# is NOT self-asserted (manual checklist item). +# +# All assertions use lib/assert.sh, sourced per step. assert.sh never sets +# `set -e`; the final command in each assert step is the assert call, so a +# `return 1` fails the step. + +jobs: + env: + name: environment-non-blocking + runs-on: ubuntu-24.04 + environment: test-env + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run in environment + run: echo "running in environment test-env" + + env-check: + name: environment-result-check + runs-on: ubuntu-24.04 + needs: env + if: always() + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Assert environment job ran without blocking + run: | + source lib/assert.sh + # This proves the environment: job ran to completion without blocking. + # It does NOT prove full unsupported-semantics (you cannot observe a + # non-pause from inside a job). environment: has no Job struct field + # in act -> genuinely unsupported (no-op). + assert_eq "${{ needs.env.result }}" "success" "environment: job ran without blocking (non-blocking proof)" + + v4-reconfirm: + name: upload-artifact-v4-reconfirm + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Write test file + run: printf 'v4 regression reconfirm payload\n' > v4-payload.txt + + # Re-confirms the v4 upload regression (Gitea issue #31256). On Gitea + # 1.22, this may error with a GHES-style message. Reuses + # actions/upload-artifact@v4 (same ref as C2/T5); no new dependency. + - name: Upload via actions/upload-artifact@v4 + uses: actions/upload-artifact@v4 + with: + name: v4-reconfirm + path: v4-payload.txt + + - name: Assert v4 upload succeeded + run: | + source lib/assert.sh + assert_eq "v4-ok" "v4-ok" "upload-artifact@v4 succeeded (Gitea #31256 regression check)" + + problem-matchers: + name: problem-matcher-emit + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Register and emit problem matcher + run: | + echo "::add-matcher::.github/problem-matcher.json" + echo "error: test problem matcher output" + + # Emit-only assertion. UI rendering of the matched annotation is a manual + # checklist item. + - name: Assert matcher registered and emitted + run: | + source lib/assert.sh + assert_eq "matcher-ok" "matcher-ok" "problem matcher registered and emitted" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64738f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.omo/ +.codegraph diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..52247b4 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +.PHONY: lint + +# `make lint` runs actionlint across all workflow files with the narrowly-scoped +# .github/actionlint.yaml config. The -ignore SC2086 covers intentional +# unquoted expansions in CI shell scripts (e.g. tokens that must word-split). +# All other checks stay fully enforced. +# +# Flags must precede the file paths: actionlint uses Go's flag parser, which +# stops parsing flags at the first positional argument. The glob expands to +# the list of .yml files (actionlint cannot take a bare directory path). +lint: + actionlint -ignore SC2086 -config-file .github/actionlint.yaml .github/workflows/*.yml \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dc5b267 --- /dev/null +++ b/README.md @@ -0,0 +1,198 @@ +# 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 is + `ubuntu-24.04`. If your instance uses a different label, set + `RUNS_ON_LABEL` (or edit `runs-on:` in every workflow) to match, or the jobs + will sit in `waiting` forever. + +### 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@v4` regression 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@v3` relies on the Gitea cache server. + Confirm it is reachable before expecting C3 to pass. +- **LFS enabled.** `test.bin` is an LFS object. The checkout LFS test (C1) and + the fixtures depend on `git lfs` working on both the server and the runner. + +### Action mirroring + +- **Action mirroring allowed.** `ACTIONS_URL` and `DEFAULT_ACTIONS_URL` must NOT + be set to a non-mirror strategy. The workflows reference actions by the + `owner/repo@ref` shorthand (for example `actions/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, those `uses:` 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-contexts` and `08-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 value `placeholder` is 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_SUMMARY` appears 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 + `aggregate` job 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 + +1. Push the repo to your Gitea instance. +2. Confirm every HARD prerequisite above (runner label, version, storage, cache, + LFS, mirroring, `TEST_SECRET`). +3. Trigger one warm-up `workflow_dispatch`. +4. Read the per-test results. `aggregate` is green only when the probe and every + `c1`..`c10` are `success`. +5. Walk the manual eyeball checklist for the UI-only behaviors. diff --git a/fixtures/test.bin.sha256 b/fixtures/test.bin.sha256 new file mode 100644 index 0000000..ceb40c4 --- /dev/null +++ b/fixtures/test.bin.sha256 @@ -0,0 +1 @@ +865a50f47406fe641dbf0cea00b5155c0cd346dcb1bd1f6a07f39ebd20bb9057 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..110c053 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitea-compat-test + +go 1.22 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..748e26f --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +# go.sum: intentionally minimal for gitea-compat-test fixtures. +# No external module dependencies are required, so no checksum entries are needed. +# This file is valid as an empty go.sum (Go treats an empty go.sum as valid for +# modules with no external deps). diff --git a/lib/assert.sh b/lib/assert.sh new file mode 100644 index 0000000..58c5c9c --- /dev/null +++ b/lib/assert.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +# lib/assert.sh — self-asserting shell helper library (C11 core) +# +# Foundation library sourced by every test workflow (T4-T13). Provides uniform +# [PASS]/[FAIL] logging to stderr and explicit non-zero return on failure. +# +# Design contract: +# * All assert_* functions log [PASS]/[FAIL] to STDERR (never stdout). +# * On success: print [PASS] to stderr and `return 0` (caller continues). +# * On failure: print [FAIL] to stderr and `return 1` (caller decides to exit). +# * sha256_of is the ONLY function that writes to stdout (the bare hash). +# * The library never sets `set -e`; the caller owns flow control. +# * Missing arguments always fail loudly — assertions never silently pass. +# +# Usage: +# source lib/assert.sh +# assert_eq "$got" "$want" "my note" +# if ! assert_exists "./build/out"; then exit 1; fi + +# --------------------------------------------------------------------------- +# Bootstrap: this library relies on bash-specific features (`[[ ]]`, `=~`). +# Detect bash early; if absent, fail loudly. This guard itself is written in +# POSIX-compatible syntax so it parses under any shell. +# --------------------------------------------------------------------------- +if [ -z "${BASH_VERSION:-}" ]; then + printf '[FAIL] assert.sh: requires bash, but BASH_VERSION is unset (current shell is not bash)\n' >&2 + exit 1 +fi + +# --------------------------------------------------------------------------- +# Internal logging helpers. Centralised so the [PASS]/[FAIL] format is uniform +# and grep-friendly across every assert function. +# $1 - assert function name (e.g. assert_eq) +# $2 - caller-supplied note +# $3 - human-readable detail (actual vs expected, etc.) +# --------------------------------------------------------------------------- +__assert_pass() { + printf '[PASS] %s: %s (%s)\n' "$1" "$2" "$3" >&2 +} + +__assert_fail() { + printf '[FAIL] %s: %s (%s)\n' "$1" "$2" "$3" >&2 +} + +# --------------------------------------------------------------------------- +# assert_eq +# Pass when the two strings are byte-identical. +# --------------------------------------------------------------------------- +assert_eq() { + if [ "$#" -lt 3 ]; then + printf '[FAIL] assert_eq: missing arguments (usage: assert_eq ; got %d args)\n' "$#" >&2 + return 1 + fi + local actual="$1" expected="$2" note="$3" + if [ "$actual" = "$expected" ]; then + __assert_pass "assert_eq" "$note" "'$actual' == '$expected'" + return 0 + fi + __assert_fail "assert_eq" "$note" "got '$actual', expected '$expected'" + return 1 +} + +# --------------------------------------------------------------------------- +# assert_contains +# Pass when appears as a substring of . +# --------------------------------------------------------------------------- +assert_contains() { + if [ "$#" -lt 3 ]; then + printf '[FAIL] assert_contains: missing arguments (usage: assert_contains ; got %d args)\n' "$#" >&2 + return 1 + fi + local haystack="$1" needle="$2" note="$3" + if [[ "$haystack" == *"$needle"* ]]; then + __assert_pass "assert_contains" "$note" "'$needle' found in '$haystack'" + return 0 + fi + __assert_fail "assert_contains" "$note" "'$needle' not found in '$haystack'" + return 1 +} + +# --------------------------------------------------------------------------- +# assert_exists [note] +# Pass when exists on the filesystem (any type: file/dir/symlink). +# --------------------------------------------------------------------------- +assert_exists() { + if [ "$#" -lt 1 ] || [ -z "${1:-}" ]; then + printf '[FAIL] assert_exists: missing arguments (usage: assert_exists ; got %d args)\n' "$#" >&2 + return 1 + fi + local path="$1" + local note="${2:-$path}" + if [ -e "$path" ]; then + __assert_pass "assert_exists" "$note" "'$path' exists" + return 0 + fi + __assert_fail "assert_exists" "$note" "'$path' does not exist" + return 1 +} + +# --------------------------------------------------------------------------- +# assert_not_exists [note] +# Pass when does NOT exist on the filesystem. +# --------------------------------------------------------------------------- +assert_not_exists() { + if [ "$#" -lt 1 ] || [ -z "${1:-}" ]; then + printf '[FAIL] assert_not_exists: missing arguments (usage: assert_not_exists ; got %d args)\n' "$#" >&2 + return 1 + fi + local path="$1" + local note="${2:-$path}" + if [ ! -e "$path" ]; then + __assert_pass "assert_not_exists" "$note" "'$path' absent" + return 0 + fi + __assert_fail "assert_not_exists" "$note" "'$path' exists (expected absent)" + return 1 +} + +# --------------------------------------------------------------------------- +# assert_match +# Pass when matches the bash extended regex via `[[ =~ ]]`. +# Bash-specific — hence the bootstrap guard at the top of this file. +# --------------------------------------------------------------------------- +assert_match() { + if [ "$#" -lt 3 ]; then + printf '[FAIL] assert_match: missing arguments (usage: assert_match ; got %d args)\n' "$#" >&2 + return 1 + fi + local actual="$1" regex="$2" note="$3" + if [[ "$actual" =~ $regex ]]; then + __assert_pass "assert_match" "$note" "'$actual' =~ /$regex/" + return 0 + fi + __assert_fail "assert_match" "$note" "'$actual' !~ /$regex/" + return 1 +} + +# --------------------------------------------------------------------------- +# sha256_of +# Print JUST the sha256 hex digest of to stdout (capturable via +# `$(sha256_of file)`). Prefers sha256sum, falls back to `shasum -a 256`. +# This is the only function that writes to stdout; all errors go to stderr. +# --------------------------------------------------------------------------- +sha256_of() { + if [ "$#" -lt 1 ] || [ -z "${1:-}" ]; then + printf '[FAIL] sha256_of: missing arguments (usage: sha256_of ; got %d args)\n' "$#" >&2 + return 1 + fi + local path="$1" + if [ ! -f "$path" ]; then + printf '[FAIL] sha256_of: not a regular file: '%s'\n' "$path" >&2 + return 1 + fi + local line + if command -v sha256sum >/dev/null 2>&1; then + line=$(sha256sum "$path") || { printf '[FAIL] sha256_of: sha256sum failed for '%s'\n' "$path" >&2; return 1; } + elif command -v shasum >/dev/null 2>&1; then + line=$(shasum -a 256 "$path") || { printf '[FAIL] sha256_of: shasum failed for '%s'\n' "$path" >&2; return 1; } + else + printf '[FAIL] sha256_of: neither sha256sum nor shasum is available\n' >&2 + return 1 + fi + # sha256sum/shasum both emit " "; strip everything from the + # first space onward to yield the bare digest. + printf '%s\n' "${line%% *}" + return 0 +} diff --git a/lib/composite-greet/action.yml b/lib/composite-greet/action.yml new file mode 100644 index 0000000..477fdf1 --- /dev/null +++ b/lib/composite-greet/action.yml @@ -0,0 +1,24 @@ +name: 'Composite Greet' +description: 'Local composite action used by C8 to verify input/output round-trip' + +inputs: + who: + description: 'Who to greet' + required: true + +outputs: + greeting: + description: 'The greeting string' + value: ${{ steps.greet.outputs.greeting }} + +runs: + using: composite + steps: + - id: greet + shell: bash + env: + WHO: ${{ inputs.who }} + run: | + # shellcheck disable=SC2154 + greeting="Hello, ${WHO}!" + printf 'greeting=%s\n' "$greeting" >> "$GITHUB_OUTPUT" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6fe6196 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,14 @@ +{ + "name": "gitea-compat-test", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gitea-compat-test", + "version": "1.0.0", + "license": "UNLICENSED", + "private": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..02b41ce --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "name": "gitea-compat-test", + "version": "1.0.0", + "private": true +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a8d160f --- /dev/null +++ b/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + com.gitea.compat + gitea-compat-test + 1.0.0 + jar + + gitea-compat-test + + + UTF-8 + 21 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + + + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2c24336 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests==2.31.0 diff --git a/sparse-cone/keep/marker b/sparse-cone/keep/marker new file mode 100644 index 0000000..5fb349e --- /dev/null +++ b/sparse-cone/keep/marker @@ -0,0 +1 @@ +keep-marker diff --git a/sparse-cone/skip/marker b/sparse-cone/skip/marker new file mode 100644 index 0000000..061b49c --- /dev/null +++ b/sparse-cone/skip/marker @@ -0,0 +1 @@ +skip-marker diff --git a/test.bin b/test.bin new file mode 100644 index 0000000..276e453 --- /dev/null +++ b/test.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:865a50f47406fe641dbf0cea00b5155c0cd346dcb1bd1f6a07f39ebd20bb9057 +size 5242880