feat: Gitea Actions compatibility test suite
Gitea Actions Compatibility Suite / aggregate (push) Failing after 0s
Gitea Actions Compatibility Suite / c8 (push) Failing after 1s
Gitea Actions Compatibility Suite / c9 (push) Failing after 0s
Gitea Actions Compatibility Suite / c10 (push) Failing after 0s
Gitea Actions Compatibility Suite / probe (push) Successful in 0s
Gitea Actions Compatibility Suite / c1 (push) Successful in 8s
Gitea Actions Compatibility Suite / c2 (push) Successful in 5s
Gitea Actions Compatibility Suite / c3 (push) Successful in 1m24s
Gitea Actions Compatibility Suite / c4 (push) Failing after 9s
Gitea Actions Compatibility Suite / c5 (push) Successful in 3s
Gitea Actions Compatibility Suite / c6 (push) Successful in 1s
Gitea Actions Compatibility Suite / c7 (push) Failing after 32s
Gitea Actions Compatibility Suite / aggregate (push) Failing after 0s
Gitea Actions Compatibility Suite / c8 (push) Failing after 1s
Gitea Actions Compatibility Suite / c9 (push) Failing after 0s
Gitea Actions Compatibility Suite / c10 (push) Failing after 0s
Gitea Actions Compatibility Suite / probe (push) Successful in 0s
Gitea Actions Compatibility Suite / c1 (push) Successful in 8s
Gitea Actions Compatibility Suite / c2 (push) Successful in 5s
Gitea Actions Compatibility Suite / c3 (push) Successful in 1m24s
Gitea Actions Compatibility Suite / c4 (push) Failing after 9s
Gitea Actions Compatibility Suite / c5 (push) Successful in 3s
Gitea Actions Compatibility Suite / c6 (push) Successful in 1s
Gitea Actions Compatibility Suite / c7 (push) Failing after 32s
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
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
*.bin filter=lfs diff=lfs merge=lfs -text
|
||||||
@@ -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'
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "gitea-divergences-test",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(error|warning|notice): (.*)$",
|
||||||
|
"severity": 1,
|
||||||
|
"message": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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.<job>.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"
|
||||||
@@ -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)"
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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"
|
||||||
@@ -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 == '<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
|
||||||
@@ -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"
|
||||||
@@ -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.<id>.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<<DELIMITER_EOF"
|
||||||
|
echo "line1"
|
||||||
|
echo "line2"
|
||||||
|
echo "DELIMITER_EOF"
|
||||||
|
} >> "$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"
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.omo/
|
||||||
|
.codegraph
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
865a50f47406fe641dbf0cea00b5155c0cd346dcb1bd1f6a07f39ebd20bb9057
|
||||||
@@ -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).
|
||||||
+167
@@ -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 <actual> <expected> <note>
|
||||||
|
# Pass when the two strings are byte-identical.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
assert_eq() {
|
||||||
|
if [ "$#" -lt 3 ]; then
|
||||||
|
printf '[FAIL] assert_eq: missing arguments (usage: assert_eq <actual> <expected> <note>; 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 <haystack> <needle> <note>
|
||||||
|
# Pass when <needle> appears as a substring of <haystack>.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
assert_contains() {
|
||||||
|
if [ "$#" -lt 3 ]; then
|
||||||
|
printf '[FAIL] assert_contains: missing arguments (usage: assert_contains <haystack> <needle> <note>; 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 <path> [note]
|
||||||
|
# Pass when <path> 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 <path>; 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 <path> [note]
|
||||||
|
# Pass when <path> 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 <path>; 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 <actual> <regex> <note>
|
||||||
|
# Pass when <actual> matches the bash extended regex <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 <actual> <regex> <note>; 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 <path>
|
||||||
|
# Print JUST the sha256 hex digest of <path> 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 <path>; 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 "<hash> <file>"; strip everything from the
|
||||||
|
# first space onward to yield the bare digest.
|
||||||
|
printf '%s\n' "${line%% *}"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
Generated
+14
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "gitea-compat-test",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.gitea.compat</groupId>
|
||||||
|
<artifactId>gitea-compat-test</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>gitea-compat-test</name>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<maven.compiler.release>21</maven.compiler.release>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
<configuration>
|
||||||
|
<release>21</release>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
requests==2.31.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
keep-marker
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
skip-marker
|
||||||
Reference in New Issue
Block a user