Skip to content

Commit abc286e

Browse files
committed
feat: add required-workflows drift check and bump VERSION to 1.10.0
Adds RequiredWorkflowsCheck to the drift checker. The check reads required_workflows lists from drift-checker.config.json per repo type (cursor-plugin, mcp-server) and emits an error finding for each absent workflow. Policy is additive across config tiers (globals -> type -> repo); extra workflows are never flagged; skip_checks suppresses the check entirely. New files: - scripts/drift_check/checks/required_workflows.py - tests/test_required_workflows.py (9 tests, all pass) Changed files: - types.py: adds required_workflows to RepoConfig, present_workflows to RepoSnapshot, and tier-union logic in DriftConfig.resolve() - snapshot.py: discovers .github/workflows/ filenames into present_workflows - cli.py: wires RequiredWorkflowsCheck into the check pipeline - checks/__init__.py: exports RequiredWorkflowsCheck - standards/drift-checker.config.json: adds required_workflows arrays for cursor-plugin and mcp-server types On first run the check correctly flags two real gaps: - steam-mcp: missing stale.yml (known from the prior manual audit) - Mobile-App-Developer-Tools: missing stale.yml (caught by the check; missed by the manual audit) Both are follow-up fix: PRs in their respective repos. They are not suppressed here; the check is working as intended. file=None findings render cleanly in both markdown and gh-summary renderers (both use `if f.file else "-"`). Signed-off-by: fOuttaMyPaint <tmhospitalitystrategies@gmail.com> Signed-off-by: fOuttaMyPaint <154358121+TMHSDigital@users.noreply.github.com>
1 parent 73affd8 commit abc286e

8 files changed

Lines changed: 312 additions & 6 deletions

File tree

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.9.5
1+
1.10.0
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
"""Registered checks live here.
22
33
Session A shipped ``version_signal``. Session B adds ``broken_refs``,
4-
``required_refs``, and ``stale_counts``. Phase 3 will add more via the
5-
same ``Check`` Protocol from ``types.py``.
4+
``required_refs``, and ``stale_counts``. ``required_workflows`` was added
5+
in v1.10.0.
66
"""
77
from .broken_refs import BrokenRefsCheck
88
from .required_refs import RequiredRefsCheck
9+
from .required_workflows import RequiredWorkflowsCheck
910
from .stale_counts import StaleCountsCheck
1011
from .version_signal import VersionSignalCheck
1112

1213
__all__ = [
1314
"VersionSignalCheck",
1415
"BrokenRefsCheck",
1516
"RequiredRefsCheck",
17+
"RequiredWorkflowsCheck",
1618
"StaleCountsCheck",
1719
]
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""Required-workflows check.
2+
3+
For each repo, compare the set of workflow filenames present under
4+
``.github/workflows/`` against the per-type ``required_workflows`` list
5+
resolved from ``standards/drift-checker.config.json``.
6+
7+
A workflow that is required but absent -> ``error``.
8+
Extra or unexpected workflows are never flagged; this check is presence-
9+
only and never emits "this workflow should not be here" findings.
10+
11+
Policy lives entirely in config (``types.<repo-type>.required_workflows``),
12+
merged via the additive-strictness tier logic in ``DriftConfig.resolve``.
13+
No workflow names are hardcoded here.
14+
15+
Edge cases:
16+
* ``repo_type == "unknown"`` -> silent; we cannot know what is required.
17+
* ``required_workflows`` empty (absent from config) -> silent; permissive
18+
by default, same posture as ``required-refs``.
19+
* ``skip_checks`` contains this check's name -> silent for that repo.
20+
* No per-file pragma support; suppression is via ``skip_checks`` in
21+
config, because the check operates at repo level (no file to annotate
22+
when the workflow is absent).
23+
"""
24+
from __future__ import annotations
25+
26+
from typing import Iterable, List
27+
28+
from ..types import Finding, RepoSnapshot
29+
30+
31+
NAME = "required-workflows"
32+
33+
34+
class RequiredWorkflowsCheck:
35+
name: str = NAME
36+
37+
def run(self, snapshot: RepoSnapshot) -> Iterable[Finding]:
38+
if NAME in snapshot.config.skip_checks:
39+
return ()
40+
41+
# Cannot determine requirements for unknown repo types.
42+
if snapshot.repo_type == "unknown":
43+
return ()
44+
45+
required = snapshot.config.required_workflows
46+
if not required:
47+
return ()
48+
49+
out: List[Finding] = []
50+
for workflow in sorted(required):
51+
if workflow not in snapshot.present_workflows:
52+
out.append(
53+
Finding(
54+
repo=snapshot.slug,
55+
file=None,
56+
check=NAME,
57+
severity="error",
58+
message=(
59+
f"required workflow '{workflow}' is absent"
60+
f" (required for {snapshot.repo_type} repos)"
61+
),
62+
suggested_fix=(
63+
f"add .github/workflows/{workflow} following"
64+
f" the scaffold template or ci-cd.md"
65+
),
66+
)
67+
)
68+
return out

scripts/drift_check/cli.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from drift_check.checks import ( # type: ignore
4242
BrokenRefsCheck,
4343
RequiredRefsCheck,
44+
RequiredWorkflowsCheck,
4445
StaleCountsCheck,
4546
VersionSignalCheck,
4647
)
@@ -63,6 +64,7 @@
6364
from .checks import (
6465
BrokenRefsCheck,
6566
RequiredRefsCheck,
67+
RequiredWorkflowsCheck,
6668
StaleCountsCheck,
6769
VersionSignalCheck,
6870
)
@@ -315,6 +317,7 @@ def _run_checks(snapshots: Sequence[RepoSnapshot]) -> List[Finding]:
315317
VersionSignalCheck(),
316318
BrokenRefsCheck(),
317319
RequiredRefsCheck(),
320+
RequiredWorkflowsCheck(),
318321
StaleCountsCheck(),
319322
)
320323
for snap in snapshots:

scripts/drift_check/snapshot.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"skills",
5050
"rules",
5151
".cursor-plugin",
52+
".github/workflows", # workflow presence for the required-workflows check
5253
)
5354

5455

@@ -91,6 +92,19 @@ def _detect_repo_type(repo_path: Path) -> RepoType:
9192
return "unknown"
9293

9394

95+
def _collect_workflow_names(repo_path: Path) -> frozenset[str]:
96+
"""Return the set of workflow filenames (not paths) present under
97+
``.github/workflows/``. Only ``.yml`` and ``.yaml`` files count.
98+
Missing directory -> empty frozenset (not an error)."""
99+
wf_dir = repo_path / ".github" / "workflows"
100+
if not wf_dir.is_dir():
101+
return frozenset()
102+
return frozenset(
103+
p.name for p in wf_dir.iterdir()
104+
if p.is_file() and p.suffix.lower() in (".yml", ".yaml")
105+
)
106+
107+
94108
def _collect_paths(repo_path: Path) -> list[Path]:
95109
out: list[Path] = []
96110
for name in ("AGENTS.md", "CLAUDE.md"):
@@ -159,6 +173,7 @@ def _build_snapshot_from_path(
159173
config=config.resolve(slug, repo_type),
160174
meta_standards=meta_standards,
161175
meta_required_refs=meta_required_refs,
176+
present_workflows=_collect_workflow_names(repo_path),
162177
)
163178

164179

scripts/drift_check/types.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ class RepoConfig:
9494
repo_type: RepoType
9595
skip_checks: frozenset[str] = frozenset()
9696
signal_policy: str = "same-major-minor"
97+
# Additive-strictness: tiers ADD requirements, never relax them.
98+
# This is the opposite safety direction from skip_checks (which accumulates
99+
# permissiveness). Do not change to a last-tier-wins override - that would
100+
# let a repo-tier entry silently zero out the type-tier requirements.
101+
required_workflows: frozenset[str] = frozenset()
97102

98103

99104
@dataclass(frozen=True)
@@ -109,8 +114,16 @@ class DriftConfig:
109114

110115
def resolve(self, slug: str, repo_type: RepoType) -> RepoConfig:
111116
"""Merge globals -> type -> repo. Later layers override scalars and
112-
extend ``skip_checks``."""
117+
extend accumulating sets (``skip_checks``, ``required_workflows``).
118+
119+
Note on required_workflows: the merge is additive-strictness only.
120+
Tiers can ADD required workflows but cannot remove them. This is the
121+
opposite safety direction from skip_checks (which accumulates
122+
permissiveness). A repo tier that lists extra workflows makes the repo
123+
stricter, never more lenient.
124+
"""
113125
skip: set[str] = set()
126+
required_wf: set[str] = set()
114127
signal_policy = str(self.globals.get("signal_policy", "same-major-minor"))
115128

116129
for tier in (self.globals, self.types.get(repo_type, {}), self.repos.get(slug, {})):
@@ -121,12 +134,16 @@ def resolve(self, slug: str, repo_type: RepoType) -> RepoConfig:
121134
skip.update(str(x) for x in tier_skips)
122135
if "signal_policy" in tier:
123136
signal_policy = str(tier["signal_policy"])
137+
wf_list = tier.get("required_workflows", [])
138+
if isinstance(wf_list, list):
139+
required_wf.update(str(x) for x in wf_list)
124140

125141
return RepoConfig(
126142
slug=slug,
127143
repo_type=repo_type,
128144
skip_checks=frozenset(skip),
129145
signal_policy=signal_policy,
146+
required_workflows=frozenset(required_wf),
130147
)
131148

132149

@@ -145,6 +162,10 @@ class RepoSnapshot:
145162
# this in remote mode via sparse-checkout.
146163
meta_standards: frozenset[str] = frozenset()
147164
meta_required_refs: Mapping[str, Mapping[str, Sequence[str]]] = field(default_factory=dict)
165+
# Filenames (not paths) of workflow files present under .github/workflows/.
166+
# Populated at snapshot time; the required-workflows check reads this.
167+
# Default frozenset() so existing snapshot constructions remain valid.
168+
present_workflows: frozenset[str] = frozenset()
148169

149170

150171
@dataclass(frozen=True)

standards/drift-checker.config.json

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,21 @@
66
},
77
"types": {
88
"cursor-plugin": {
9-
"skip_checks": []
9+
"skip_checks": [],
10+
"required_workflows": [
11+
"validate.yml",
12+
"release.yml",
13+
"stale.yml",
14+
"drift-check.yml"
15+
]
1016
},
1117
"mcp-server": {
12-
"skip_checks": ["required-refs"]
18+
"skip_checks": ["required-refs"],
19+
"required_workflows": [
20+
"drift-check.yml",
21+
"stale.yml",
22+
"publish.yml"
23+
]
1324
}
1425
},
1526
"repos": {

0 commit comments

Comments
 (0)