26384fe9f1
Lint workflows / actionlint (push) Failing after 34s
Root cause of the recurring 'Illegal option -o pipefail' failures (ed49b8dresolve-versions;b7197e8promote-base-latest, run 418): docker-publish.yml had no workflow-level default shell, so Gitea's sh/dash default applied and every bash-syntax step had to individually remember 'shell: bash'. - docker-publish.yml: add 'defaults: run: shell: bash' — fixes the whole class; all pre-existing dash steps are POSIX so bash runs them unchanged. - lint.yml: new workflow, runs on every push/PR (not just release tags): * scripts/check-workflow-shell.sh — Gitea-accurate guard: fails if any run: step doesn't resolve to bash. Catches the omit-shell+bash-syntax case that actionlint MISSES (actionlint models GitHub, where the default shell is bash, so a shell-less step is assumed bash). * actionlint + shellcheck — catches explicit 'shell: sh' + bash syntax (SC3040) and general workflow errors. Verified locally: guard + actionlint pass current workflows; guard fails a synthetic omit-shell+pipefail workflow; shellcheck clean.
66 lines
2.7 KiB
Bash
Executable File
66 lines
2.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Gitea-accurate guard against the recurring "bash syntax under the default
|
|
# sh/dash shell" footgun (ed49b8d resolve-versions; b7197e8 promote-base-latest,
|
|
# run 418).
|
|
#
|
|
# WHY A CUSTOM CHECK AND NOT JUST actionlint:
|
|
# actionlint models *GitHub* Actions, whose default `run` shell is bash. It
|
|
# therefore assumes a step that omits `shell:` runs under bash, and does NOT
|
|
# flag `set -o pipefail` there. Gitea Actions' default is `sh` (dash), so the
|
|
# exact bug we hit (omit `shell:`, use bash syntax) is invisible to actionlint.
|
|
# actionlint only fires when a step *explicitly* declares `shell: sh`.
|
|
#
|
|
# THE INVARIANT THIS ENFORCES:
|
|
# Every `run:` step in every .gitea/workflows/*.yml must resolve to an
|
|
# effective shell of `bash` — via the step's own `shell:`, a job-level
|
|
# `defaults.run.shell`, or a workflow-level `defaults.run.shell`. Any step
|
|
# that would fall through to Gitea's `sh` default is a FAILURE, because a
|
|
# future author adding bash syntax to it fails silently in CI.
|
|
#
|
|
# Pair this with actionlint (which catches explicit `shell: sh` + bash syntax,
|
|
# expression errors, and much else). Together they cover the class on Gitea.
|
|
set -euo pipefail
|
|
|
|
WF_DIR="${1:-.gitea/workflows}"
|
|
|
|
python3 - "$WF_DIR" <<'PY'
|
|
import sys, glob, os
|
|
try:
|
|
import yaml
|
|
except ImportError:
|
|
sys.stderr.write("ERROR: python3 yaml module missing (apt install python3-yaml)\n")
|
|
sys.exit(2)
|
|
|
|
wf_dir = sys.argv[1]
|
|
files = sorted(glob.glob(os.path.join(wf_dir, "*.yml")) + glob.glob(os.path.join(wf_dir, "*.yaml")))
|
|
if not files:
|
|
sys.stderr.write(f"ERROR: no workflow files under {wf_dir}\n")
|
|
sys.exit(2)
|
|
|
|
problems = []
|
|
for f in files:
|
|
with open(f) as fh:
|
|
doc = yaml.safe_load(fh) or {}
|
|
wf_shell = (((doc.get("defaults") or {}).get("run") or {}).get("shell"))
|
|
jobs = doc.get("jobs") or {}
|
|
for jname, job in jobs.items():
|
|
job = job or {}
|
|
job_shell = (((job.get("defaults") or {}).get("run") or {}).get("shell"))
|
|
steps = job.get("steps") or []
|
|
for i, step in enumerate(steps):
|
|
step = step or {}
|
|
if "run" not in step:
|
|
continue # `uses:` steps have no shell
|
|
eff = step.get("shell") or job_shell or wf_shell or "sh" # Gitea default = sh
|
|
if eff != "bash":
|
|
name = step.get("name") or f"step[{i}]"
|
|
problems.append(f"{f}: job '{jname}' / '{name}': effective shell = '{eff}' (Gitea default is sh; declare shell: bash or a bash default)")
|
|
|
|
if problems:
|
|
sys.stderr.write("Workflow shell guard FAILED — bash default not guaranteed:\n")
|
|
for p in problems:
|
|
sys.stderr.write(f" - {p}\n")
|
|
sys.exit(1)
|
|
print(f"Workflow shell guard OK — all run: steps in {len(files)} workflow file(s) resolve to bash.")
|
|
PY
|