diff --git a/AUDIT.md b/AUDIT.md index 4be7e4a1..1ca6626c 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -89,6 +89,17 @@ Run [`WORKFLOW.md`][workflow]'s methodology against the repo's **own** Actions: - **Secrets** - confirm each required secret exists (name only; values are not readable). Check the Actions store and, where the mechanism needs it (Docker Hub, codegen App), the Dependabot store too. +- **Dependabot ecosystem coverage** - for each ecosystem the repo's tree implies, `.github/dependabot.yml` must declare it: `github-actions` when `.github/workflows/` is present (its workflows reference actions) - otherwise those versions go stale and a stood-up merge-bot has no action-update PRs to auto-merge - and `devcontainers` when a `.devcontainer` is present. The mechanical check (`spec/audit.py`) asserts each implied ecosystem's **presence**; a tree-implied ecosystem declared nowhere is a **drift finding** (the file exists; its absence would instead be a file-presence letter). Then confirm **by inspection** that each declared ecosystem **dual-targets `main` + `develop`** per the [Branching Model][agents-branching-model] - the regex below cannot pair an ecosystem with its `target-branch`. Language ecosystems (`nuget`/`uv`/`npm`) are directory-scoped and audited by inspection too. + + ```sh + # Anchor to the line start (optional list dash) so a commented-out '# package-ecosystem:' is not counted. + decl=$(gh api "repos///contents/.github/dependabot.yml?ref=" --jq '.content' | base64 -d | grep -oE '^[[:space:]]*-?[[:space:]]*package-ecosystem:[[:space:]]*"?[a-z-]+' | grep -oE '[a-z-]+$' | sort -u) + has() { gh api "repos///contents/$1?ref=" >/dev/null 2>&1; } + has .github/workflows && { grep -qx github-actions <<<"$decl" && echo "github-actions: present" || echo "github-actions: MISSING (workflows present)"; } + has .devcontainer && { grep -qx devcontainers <<<"$decl" && echo "devcontainers: present" || echo "devcontainers: MISSING (.devcontainer present)"; } + # then read dependabot.yml and confirm each present ecosystem has both a main and a develop target-branch entry + ``` + ## 7. Verdict Model Per dimension, record `operational | not-operational | N/A`, each with a letter verdict and an intent verdict: @@ -125,6 +136,7 @@ The convergence model: the hub audits and the agent **applies** the fixes via ta [agents]: ./AGENTS.md +[agents-branching-model]: ./AGENTS.md#branching-model [audit-runner]: ./spec/audit.py [codestyle]: ./CODESTYLE.md [copilot-runbook]: ./.github/copilot-instructions.md diff --git a/spec/audit.py b/spec/audit.py index 03298047..3e544369 100644 --- a/spec/audit.py +++ b/spec/audit.py @@ -16,8 +16,10 @@ Usage: python3 spec/audit.py [RepoName ...] (default: every cataloged repo) """ +import base64 import json import pathlib +import re import subprocess import sys @@ -177,6 +179,28 @@ def audit_repo(entry, spec): for name in sorted(present - claimed_names): findings.append(("DRIFT", f"secrets: {name} in the {store} store is claimed by no applicable mechanism (stale?)")) + # --- Dependabot ecosystem coverage --- + # A repo's tree implies Dependabot ecosystems it must track: github-actions when it ships workflows + # (the action versions they reference otherwise go stale, and a merge-bot then has no PRs to auto-merge), + # devcontainers when it ships a .devcontainer. dependabot.yml is YAML (no stdlib parser), so scan the + # declared package-ecosystem values by regex - anchored to the line start so a commented-out entry + # (# package-ecosystem: ...) is not read as declared. This asserts an implied ecosystem's *presence* + # only; that each declared ecosystem dual-targets main+develop (the fleet norm) is verified by + # inspection, not here. Only runs when dependabot.yml exists; its absence is already a file-presence + # LETTER below. Language ecosystems (nuget/uv/npm) are directory-scoped and not yet cross-checked here. + db = gh(f"repos/{slug}/contents/.github/dependabot.yml?ref={ground}", ok404=True) + if db and db.get("content"): + declared = set(re.findall(r'^[ \t]*-?[ \t]*package-ecosystem:[ \t]*["\']?([\w-]+)', base64.b64decode(db["content"]).decode("utf-8", "replace"), re.M)) + implied = {} + workflows = gh(f"repos/{slug}/contents/.github/workflows?ref={ground}", ok404=True) + if isinstance(workflows, list) and any(e["name"].endswith((".yml", ".yaml")) for e in workflows): + implied["github-actions"] = ".github/workflows/ is present" + if gh(f"repos/{slug}/contents/.devcontainer?ref={ground}", ok404=True) is not None: + implied["devcontainers"] = ".devcontainer/ is present" + for eco, why in sorted(implied.items()): + if eco not in declared: + findings.append(("DRIFT", f"dependabot: {eco} ecosystem not declared though {why}; add it for both main and develop per the fleet norm")) + # --- File presence on the ground-truth branch --- seen_paths = set() for item in spec["files"]["baseline"]: diff --git a/spec/project-types.json b/spec/project-types.json index fbcbff34..f1f50679 100644 --- a/spec/project-types.json +++ b/spec/project-types.json @@ -123,7 +123,8 @@ "appliesTo": "*", "checks": [ { "id": "setup.secrets.present", "verdict": "letter", "assert": "Every requiredSecret for the repo's publish mechanisms is configured (per spec/secrets.json).", "intentRef": "repo-config/README.md" }, - { "id": "setup.secrets.noforbidden", "verdict": "letter", "assert": "No forbidden secret is present (e.g. a static NUGET_API_KEY on an OIDC repo).", "intentRef": "spec/secrets.json" } + { "id": "setup.secrets.noforbidden", "verdict": "letter", "assert": "No forbidden secret is present (e.g. a static NUGET_API_KEY on an OIDC repo).", "intentRef": "spec/secrets.json" }, + { "id": "setup.dependabot.ecosystems", "verdict": "intent", "assert": "For each ecosystem the repo's tree implies, .github/dependabot.yml declares it (dual-target main+develop per the fleet norm): github-actions when .github/workflows/ is present (its workflows reference actions, else their versions go stale and a stood-up merge-bot has no PRs to auto-merge), devcontainers when a .devcontainer is present. A missing implied ecosystem is a drift finding. Language ecosystems (nuget/uv/npm) are directory-scoped, audited by inspection.", "intentRef": "AGENTS.md#branching-model" } ] }, "linter-parity": {