Files
nolimit-discounts/ci/workflows-in-sync.sh
T
kristianwindandClaude Opus 5 bd058566f5 Parse the workflows on Gitea too, and keep the compat matrix off its runner
Both found by the subscriptions session in its copy of this kit, and both
reproduce here. They share the shape everything else in ci/ has had: the
failure is invisible on GitHub, which is where anyone would be looking.

1. ci/workflows-in-sync.sh never parsed the workflow files on Gitea, and
   printed "Workflows: in sync." anyway. It skipped with a note when PyYAML was
   missing — and PyYAML is missing in exactly one place. Measured here rather
   than taken on report: `import yaml` on gitea/runner-images:ubuntu-latest
   raises ModuleNotFoundError. So the check was switched off on the single
   forge where an unparseable workflow produces no run at all, no failed run
   and no error to click on, which looks exactly like a runner that has not
   picked the job up.

   ci_python_yaml in ci/lib.sh now falls back to a container the way ci_php
   already did — alpine + py3-yaml, no network install, one apk call. There is
   no skip path left: workflows that cannot be parsed are a failure, not a
   note. Both routes verified to give byte-identical output.

2. compat.yml ran its five-cell matrix on Gitea for every PR touching ci/ or
   tests/. The `schedule` was gated to GitHub for exactly this reason and
   `pull_request` was forgotten. The subscriptions repo measured six jobs dying
   together with exit code 137 — SIGKILL, runner out of memory, two seconds in,
   without one error message in the log. Latent here only because this repo
   merges through GitHub and never opens a Gitea PR; one Gitea PR touching ci/
   would have found it.

New ci/check-workflows.py enforces both, called through ci_python_yaml. It
parses every workflow and fails when a Gitea job with more than one matrix cell
is not gated, and it is fatal when it parsed no file at all.

🔴 Its first version asked whether the `if:` CONTAINED the server_url test, and
passed the very condition it exists to catch: `!= 'schedule' || server_url ==
...` contains the gate while letting pull_request through. Substring matching is
adjacent to the signal rather than the signal. It now compares the whole
normalised condition against an allowlist, and was checked both ways in one run
— red against the unfixed compat.yml naming the job and the cell count, green
after.

Three self-test assertions added, 29 total: an unparseable workflow fails, an
ungated multi-cell matrix fails, and parsing zero files fails.

Two notes from building it, both the same trap this kit keeps setting. The
container path could not find check-workflows.py because ci_in_container tars
`git ls-files` and the file was untracked. And the ungated-matrix fixture used
| as a sed delimiter while the gate itself contains ||, so sed silently changed
nothing; the assertion reported "could not stage the fixture" instead of
passing, which is the only reason it was noticed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 00:37:30 +02:00

89 lines
3.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# Guard the trap in .gitea/workflows.
#
# Gitea uses .gitea/workflows IF IT EXISTS, and falls back to .github/workflows
# only when it does not. It never merges them. So a workflow that lives only in
# .github/ simply does not run on Gitea — no error, no run to click on. It
# looks exactly like a runner that has not picked the job up, so you wait, and
# waiting never fixes it.
#
# Workflows that must behave identically on both forges are therefore kept
# byte-identical in both roots, and this script fails when they drift or when
# one appears in only one root without a stated reason.
set -euo pipefail
cd "$(dirname "$0")/.."
. ci/lib.sh
# Workflows that are deliberately single-forge, and why.
declare -A only_on=(
["auto-merge.yml"]="github: it drives the GitHub merge API. Gitea merges the owner's green PRs itself."
["release.yml"]="both, but different: each publishes to the forge it runs on, and Gitea is where the update feed lives."
)
fail=0
for root in .github .gitea; do
[ -d "$root/workflows" ] || { echo "$root/workflows is missing."; fail=1; }
done
for f in .github/workflows/*.yml .gitea/workflows/*.yml; do
[ -e "$f" ] || continue
name=$(basename "$f")
gh=".github/workflows/$name"
gt=".gitea/workflows/$name"
case "${only_on[$name]:-}" in
github:*)
[ -f "$gt" ] && { echo "$name is marked GitHub-only but exists in .gitea/workflows too."; fail=1; }
continue
;;
both,*|"both, "*)
# Present in both, intentionally different. Both copies must exist.
{ [ -f "$gh" ] && [ -f "$gt" ]; } \
|| { echo "$name is expected in both roots, but one copy is missing."; fail=1; }
continue
;;
esac
if [ ! -f "$gh" ] || [ ! -f "$gt" ]; then
echo "$name exists in only one workflow root and is not listed in ci/workflows-in-sync.sh."
echo " A workflow missing from .gitea/workflows does not run on Gitea, silently."
fail=1
continue
fi
if ! cmp -s "$gh" "$gt"; then
echo "$name has drifted between the two roots:"
diff -u "$gh" "$gt" || true
fail=1
fi
done
# Measured on Gitea 1.27.2: a workflow-level `concurrency:` block produced no
# run at all — not a failed one, not a cancelled one. It may have been fixed
# since, but the cost of keeping the rule is nil and the cost of rediscovering
# it is an afternoon of blaming the runner.
if grep -rn '^concurrency:' .gitea/workflows/ 2>/dev/null; then
echo "A workflow-level concurrency: block can stop a Gitea workflow from running at all."
fail=1
fi
# A workflow that does not parse produces no run — the same symptom as every
# other trap in this file. ci.yml cannot catch its own syntax error this way,
# but release.yml and compat.yml can be caught here rather than on the tag.
#
# 🔴 This used to skip with a note when PyYAML was missing, and still printed
# "Workflows: in sync." at the end. PyYAML is missing in exactly one place —
# the GITEA RUNNER (measured: `import yaml` raises ModuleNotFoundError on
# gitea/runner-images:ubuntu-latest) — which is the one forge where an
# unparseable workflow produces no run and no error to click on. So the check
# was switched off precisely where it was needed, and said "in sync" anyway.
#
# ci_python_yaml falls back to a container, so there is no skip path left: if
# the workflows cannot be parsed, that is a failure, not a note.
ci_python_yaml ci/check-workflows.py .github/workflows/*.yml .gitea/workflows/*.yml \
|| fail=1
[ "$fail" = 0 ] || exit 1
echo "Workflows: in sync."