mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
The workflow directory mixed entry points, reusable stages and scheduled jobs into one flat list sorted by topic, so the first question about any file - what starts it - was the one the name did not answer. Every workflow now carries one of three prefixes, derived from its on: block: cron- for a schedule trigger, call- for one only reachable through workflow_call, entry- for everything a repository event or a human starts. Scheduled workflows additionally carry an alarm clock as the first character of their name. Two lints keep it that way. One derives the expected prefix from the triggers and compares it against the file name; it flagged all 17 offenders before the rename and none after. The other resolves every .github/workflows/<file> string in the repository against the directory, which is what a rename silently breaks in Markdown links, Python constants and shell API URLs alike. Renaming pulled 69 pre-existing comments into the comment lint's changed-file scope. The vendored CodeQL template got the nocheck marker its scorecard sibling already carried, two workflow headers moved above name: where they count as file headers, and the remaining banners were deleted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
102 lines
3.4 KiB
Python
102 lines
3.4 KiB
Python
"""Resolve the explicit role subset declared in a pull-request body.
|
|
|
|
Activated by the ``🧩 Subset`` label: the entry-pull-request workflow only
|
|
runs this module when the label is present. It gives fork contributors the
|
|
selective-CI capability that ``entry-manual-steer.yml`` (``workflow_dispatch``)
|
|
grants maintainers, which forks cannot trigger.
|
|
|
|
It reads the PR body from ``$PR_BODY``, extracts the first fenced code block
|
|
whose YAML defines a ``roles:`` list, validates that every listed id is an
|
|
existing ``roles/<id>`` directory, and writes ``whitelist`` (space-separated)
|
|
to ``$GITHUB_OUTPUT`` -- the same output the diff-derived resolver
|
|
produces.
|
|
|
|
Unlike the fail-safe diff resolver (which widens to ``__ALL__`` on any
|
|
problem), the subset path is strict: invalid YAML, a missing/empty ``roles:``
|
|
list, or an unknown role id each abort with a clear message and a non-zero
|
|
exit, so a mistyped subset never silently deploys the wrong set. Role ids must
|
|
match ``^[a-z0-9-]+$``, which also blocks path traversal before any filesystem
|
|
lookup.
|
|
|
|
Invoked from scripts/github/resolve/pr_affected_roles.sh via
|
|
``python -m cli.meta.ci.subset_roles``.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import NoReturn
|
|
|
|
import yaml
|
|
|
|
from utils import PROJECT_ROOT
|
|
from utils.cache.yaml import load_yaml_str
|
|
|
|
ROLES_DIR = PROJECT_ROOT / "roles"
|
|
FENCE_RE = re.compile(r"```[^\n]*\n(.*?)```", re.DOTALL)
|
|
NAME_RE = re.compile(r"^[a-z0-9-]+$")
|
|
|
|
|
|
def fail(message: str) -> NoReturn:
|
|
print(f"Subset roles error: {message}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def extract_roles(body: str):
|
|
blocks = FENCE_RE.findall(body)
|
|
if not blocks:
|
|
fail(
|
|
"the '🧩 Subset' label is set but the PR body has no fenced "
|
|
"```yaml block. Add a '## Roles' section with a `roles:` list."
|
|
)
|
|
for block in blocks:
|
|
try:
|
|
data = load_yaml_str(block)
|
|
except yaml.YAMLError as exc:
|
|
fail(f"invalid YAML in the PR body roles block: {exc}")
|
|
if isinstance(data, dict) and "roles" in data:
|
|
return data["roles"]
|
|
fail(
|
|
"no `roles:` key found in any YAML block of the PR body. Declare the "
|
|
"subset as a `roles:` list under a '## Roles' section."
|
|
)
|
|
|
|
|
|
def main() -> None:
|
|
roles = extract_roles(os.environ.get("PR_BODY", ""))
|
|
if roles is None:
|
|
roles = []
|
|
if not isinstance(roles, list) or not all(isinstance(r, str) for r in roles):
|
|
fail("`roles:` must be a YAML list of role-id strings.")
|
|
|
|
roles = [r.strip() for r in roles if r and r.strip()]
|
|
if not roles:
|
|
fail(
|
|
"the '🧩 Subset' label is set but the `roles:` list is empty. "
|
|
"List at least one role id or remove the label."
|
|
)
|
|
|
|
invalid = sorted({r for r in roles if not NAME_RE.fullmatch(r)})
|
|
if invalid:
|
|
fail(
|
|
"invalid role id(s) (allowed: lowercase letters, digits, '-'): "
|
|
+ ", ".join(invalid)
|
|
)
|
|
|
|
unknown = sorted({r for r in roles if not (ROLES_DIR / r).is_dir()})
|
|
if unknown:
|
|
fail("unknown role id(s): " + ", ".join(unknown))
|
|
|
|
whitelist = " ".join(roles)
|
|
print(f"Subset roles resolved from PR body: {whitelist}")
|
|
|
|
github_output = os.environ.get("GITHUB_OUTPUT")
|
|
if github_output:
|
|
with Path(github_output).open("a", encoding="utf-8") as handle:
|
|
handle.write(f"whitelist={whitelist}\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|