CI / shell (shellcheck + syntax) (push) Successful in 9s
CI / python 3.11 (push) Successful in 13s
CI / python 3.13 (push) Successful in 16s
CI / python 3.12 (push) Successful in 16s
TrueNAS compatibility / compat (push) Failing after 6s
Release / release (push) Successful in 16s
Two bugs, both of which would have failed silently on the first scheduled run: - permissions were while the step pushes a branch and opens a PR. It would have died with a 403 and I would have had a bot that never worked. - it opened the PR on GITHUB, which is a one-way MIRROR. A PR merged there would be clobbered by the next fleet-repos mirror push from Gitea. A bot opening PRs against a mirror is a bot doing nothing, slowly. Now: contents+pull-requests write, and the PR is opened on Gitea (canonical) via its API. One long-lived PR, force-pushed in place -- a daily PR is the same mistake as a daily comment, wearing a hat.
175 lines
7.4 KiB
Python
175 lines
7.4 KiB
Python
"""Guards on the CI workflows themselves.
|
|
|
|
The workflows run on TWO forges -- Gitea (canonical) and GitHub (mirror), because
|
|
Gitea reads .github/workflows too -- and they hold tokens. A mistake here is not a
|
|
failed build, it is a bug report nobody files or a command nobody meant to run.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
|
|
import pytest
|
|
|
|
ROOT = os.path.join(os.path.dirname(__file__), "..")
|
|
WORKFLOWS = os.path.join(ROOT, ".github", "workflows")
|
|
|
|
|
|
def workflow_files():
|
|
return [
|
|
os.path.join(WORKFLOWS, f)
|
|
for f in sorted(os.listdir(WORKFLOWS))
|
|
if f.endswith((".yml", ".yaml"))
|
|
]
|
|
|
|
|
|
def run_bodies(path):
|
|
"""Every `run:` block's text, with its line number."""
|
|
with open(path, encoding="utf-8") as fh:
|
|
lines = fh.readlines()
|
|
|
|
out = []
|
|
i = 0
|
|
while i < len(lines):
|
|
m = re.match(r"^(\s*)run:\s*\|", lines[i])
|
|
if not m:
|
|
i += 1
|
|
continue
|
|
indent = len(m.group(1))
|
|
start = i + 1
|
|
body = []
|
|
i += 1
|
|
while i < len(lines):
|
|
line = lines[i]
|
|
if line.strip() and (len(line) - len(line.lstrip())) <= indent:
|
|
break
|
|
body.append(line)
|
|
i += 1
|
|
out.append((start + 1, "".join(body)))
|
|
return out
|
|
|
|
|
|
class TestNoExpressionInterpolationIntoShell:
|
|
"""`${{ ... }}` inside a `run:` body is spliced into the SCRIPT TEXT.
|
|
|
|
This is not theoretical. `echo "${{ steps.report.outputs.body }}"` in the compat
|
|
workflow pasted the report -- which is full of backticks -- straight into bash,
|
|
which promptly ran `create-snapshot`, `def` and `async` as commands. And because
|
|
that report is built from iX's middleware source, anything landing in their tree
|
|
would have executed on our runner.
|
|
|
|
The rule: files for data, `env:` for scalars. `env:` is safe because the runner
|
|
sets the variable rather than pasting it into the script.
|
|
"""
|
|
|
|
@pytest.mark.parametrize("path", workflow_files(), ids=os.path.basename)
|
|
def test_no_github_expression_in_a_run_body(self, path):
|
|
offenders = []
|
|
for lineno, body in run_bodies(path):
|
|
for m in re.finditer(r"\$\{\{[^}]*\}\}", body):
|
|
offenders.append(f"{os.path.basename(path)}:~{lineno}: {m.group(0)}")
|
|
assert not offenders, (
|
|
"GitHub/Gitea expressions interpolate into the shell script text, so "
|
|
"backticks and $() in the value EXECUTE. Pass data via a file, or a "
|
|
"scalar via `env:`.\n " + "\n ".join(offenders)
|
|
)
|
|
|
|
|
|
class TestBothForges:
|
|
"""Gitea is canonical; GitHub is a mirror. Both run these files."""
|
|
|
|
def test_release_publishes_on_each_forge_exactly_once(self):
|
|
with open(os.path.join(WORKFLOWS, "release.yml"), encoding="utf-8") as fh:
|
|
src = fh.read()
|
|
# One step gated ON github.com, one gated OFF it. Without the pair, a release
|
|
# either double-publishes or silently never publishes on the canonical host.
|
|
assert "if: ${{ contains(github.server_url, 'github.com') }}" in src
|
|
assert "if: ${{ !contains(github.server_url, 'github.com') }}" in src
|
|
|
|
def test_compat_files_its_report_through_ONE_implementation(self):
|
|
# It used to be two near-identical shell steps, one per forge. Two copies of
|
|
# "find the issue, decide whether to comment, post it" is two chances to drift,
|
|
# and the Gitea one duplicated an issue for real.
|
|
with open(os.path.join(WORKFLOWS, "compat.yml"), encoding="utf-8") as fh:
|
|
src = fh.read()
|
|
assert "tools/compat_publish.py" in src
|
|
assert "file a bug report (GitHub)" not in src
|
|
assert "file a bug report (Gitea)" not in src
|
|
|
|
|
|
class TestTheBotDoesNotSpam:
|
|
"""It left 11 identical 3,000-character comments on one issue in a single day.
|
|
|
|
A bot that repeats itself daily gets muted — and then the next REAL finding is
|
|
scrolled past, which defeats the entire reason for building it.
|
|
"""
|
|
|
|
def publisher(self):
|
|
with open(os.path.join(ROOT, "tools", "compat_publish.py"), encoding="utf-8") as fh:
|
|
return fh.read()
|
|
|
|
def test_it_compares_a_fingerprint_before_saying_anything(self):
|
|
src = self.publisher()
|
|
assert "extract_fingerprint" in src
|
|
assert "staying quiet" in src
|
|
|
|
def test_the_body_is_edited_in_place_not_appended_to(self):
|
|
src = self.publisher()
|
|
assert '"PATCH"' in src, "the issue body must be updated, not commented onto"
|
|
|
|
def test_it_closes_the_issue_when_everything_is_fixed(self):
|
|
src = self.publisher()
|
|
assert '"state": "closed"' in src
|
|
|
|
def test_the_matrix_refresh_opens_a_PR_rather_than_pushing_to_main(self):
|
|
# An unattended push to main from CI is exactly what the release barrier exists
|
|
# to prevent: a bot that can move main can move it somewhere nobody looked.
|
|
#
|
|
# Checked against CODE, not comments — the step's own commentary explains what
|
|
# it replaced, and that mention must not read as the thing itself.
|
|
with open(os.path.join(WORKFLOWS, "compat.yml"), encoding="utf-8") as fh:
|
|
src = fh.read()
|
|
code = "\n".join(
|
|
ln for ln in src.splitlines() if not ln.lstrip().startswith("#")
|
|
)
|
|
assert "/pulls" in code, "the matrix refresh must open a PR"
|
|
assert "HEAD:main" not in code, "CI still pushes straight to main"
|
|
|
|
def test_the_matrix_PR_targets_the_CANONICAL_forge_not_the_mirror(self):
|
|
# GitHub is a one-way mirror: a PR merged there would be silently clobbered by
|
|
# the next `fleet-repos mirror` push from Gitea. A bot opening PRs against a
|
|
# mirror is a bot doing nothing, slowly.
|
|
with open(os.path.join(WORKFLOWS, "compat.yml"), encoding="utf-8") as fh:
|
|
src = fh.read()
|
|
i = src.index("refresh the README matrix")
|
|
step = src[i:i + 400]
|
|
assert "!contains(github.server_url, 'github.com')" in step, (
|
|
"the matrix PR must be opened on Gitea (canonical), not GitHub (mirror)"
|
|
)
|
|
|
|
def test_the_workflow_has_the_permissions_its_steps_actually_need(self):
|
|
# It shipped with `contents: read` while the step pushed a branch and opened a
|
|
# PR — it would have died with a 403 on the first scheduled run, and I would
|
|
# have had a bot that silently never worked.
|
|
with open(os.path.join(WORKFLOWS, "compat.yml"), encoding="utf-8") as fh:
|
|
src = fh.read()
|
|
perms = src[src.index("permissions:"):src.index("jobs:")]
|
|
assert "contents: write" in perms, "pushing a branch needs contents: write"
|
|
assert "pull-requests: write" in perms, "opening a PR needs pull-requests: write"
|
|
assert "issues: write" in perms
|
|
|
|
|
|
class TestCompatCannotSilentlyPass:
|
|
def test_the_exit_code_is_captured_not_swallowed(self):
|
|
# Actions runs `bash -e`: `cmd > out` followed by `echo $?` never reaches the
|
|
# echo, so the "a shipped release is broken" signal would be lost and the job
|
|
# would go green while users were broken.
|
|
with open(os.path.join(WORKFLOWS, "compat.yml"), encoding="utf-8") as fh:
|
|
src = fh.read()
|
|
assert "|| rc=$?" in src
|
|
assert "shipped_broken=$rc" in src
|
|
|
|
def test_a_broken_shipped_release_fails_the_job(self):
|
|
with open(os.path.join(WORKFLOWS, "compat.yml"), encoding="utf-8") as fh:
|
|
src = fh.read()
|
|
assert "steps.check.outputs.shipped_broken != '0'" in src
|