1414#
1515# The check is deliberately syntax-only. Validating the schema would need the
1616# full Actions grammar; the failure mode that actually happened is a file that
17- # does not parse, and that costs three lines to rule out forever.
17+ # does not parse, and that costs a few lines to rule out forever.
1818set -e
1919
2020REPO=" $( cd " $( dirname " ${BASH_SOURCE[0]} " ) /../.." && pwd) "
2121DIR=" $REPO /.github/workflows"
2222[ -d " $DIR " ] || { echo " no workflows directory at $DIR " ; exit 1; }
2323
24- count=$( find " $DIR " -maxdepth 1 -name ' *.yml' -o -maxdepth 1 - name ' *.yaml' | wc -l)
24+ count=$( find " $DIR " -maxdepth 1 \( -name ' *.yml' -o -name ' *.yaml' \) | wc -l)
2525[ " $count " -gt 0 ] || { echo " no workflow files found under $DIR " ; exit 1; }
2626
27- python3 - " $DIR " << 'PY '
28- import pathlib, sys, yaml
27+ python3 - " $DIR " << 'PYEOF '
28+ import pathlib, re, sys
29+
30+ # PyYAML is not everywhere — the macOS runner has none, and a test that
31+ # hard-fails on a missing dev dependency is a test that gets deleted. So: full
32+ # parse where it exists, targeted lint where it does not, and SAY WHICH RAN. A
33+ # fallback that is quietly weaker than the check it replaces is how a green
34+ # stops meaning anything.
35+ try:
36+ import yaml
37+ HAVE_YAML = True
38+ except ImportError:
39+ HAVE_YAML = False
40+
41+ # The exact failure this test exists for: a scalar that opens with a quote and
42+ # carries more content after the closing one —
43+ # run: "$BENCH" --list
44+ # YAML reads that as a quoted scalar followed by garbage and refuses the file.
45+ TRAILING_AFTER_QUOTED = re.compile(r'^\s*[\w.-]+:\s*"[^"]*"\s*\S')
2946
3047bad = []
3148files = sorted(p for p in pathlib.Path(sys.argv[1]).iterdir()
3249 if p.suffix in (".yml", ".yaml"))
3350for p in files:
51+ text = p.read_text()
52+ for n, line in enumerate(text.splitlines(), 1):
53+ if TRAILING_AFTER_QUOTED.match(line):
54+ bad.append(f"{p.name}:{n}: content after a quoted scalar: {line.strip()}")
55+ if not HAVE_YAML:
56+ continue
3457 try:
35- doc = yaml.safe_load(p.read_text() )
58+ doc = yaml.safe_load(text )
3659 except Exception as e:
3760 bad.append(f"{p.name}: {e}")
3861 continue
39- # A workflow with no `jobs` parses but can never do anything — the same
40- # class of silent nothing, so it is reported the same way.
62+ # A workflow with no `jobs` parses but can never do anything — the same class
63+ # of silent nothing, so it is reported the same way.
4164 if not isinstance(doc, dict) or not doc.get("jobs"):
4265 bad.append(f"{p.name}: parsed, but declares no jobs")
4366
@@ -46,7 +69,9 @@ if bad:
4669 for b in bad:
4770 print(" " + b)
4871 sys.exit(1)
49- print(f"{len(files)} workflow files parse and declare jobs")
50- PY
72+
73+ mode = "parsed" if HAVE_YAML else "linted (no PyYAML here — quoted-scalar check only)"
74+ print(f"{len(files)} workflow files {mode}")
75+ PYEOF
5176
5277echo " workflow syntax OK"
0 commit comments