Skip to content

Commit dcc2ffe

Browse files
feat(cicd-rules): close 3 doomed-CI detection gaps surfaced by 2026-05-27 audit (#350)
## Summary The 2026-05-27 estate doomed-CI audit produced deletion PRs for 5 pilot repos (`road-skate`, `vext`, `quandledb`, `absolute-zero`, `Axiom.jl`). Three of the underlying anti-patterns had no Hypatia coverage, or had coverage too weak to drive action. This PR closes those gaps in \`Hypatia.Rules.CicdRules\`. | Track | Before | After | |---|---|---| | A1 — Jekyll on non-Pages | \`:irrelevant_jekyll\` at \`:low\` (ignored) | bumped to \`:medium\` (acted on) | | C — Subsumed standalone beside \`governance.yml\` | none (just a comment) | new \`:redundant_subsumed_standalone\` at \`:medium\` | | D — \`rust-ci.yml\` on no-Rust repo | only the inverse (\`:missing_rust_ci\`) | new \`:irrelevant_rust_ci\` at \`:high\` | ## Why (1) **Severity bump for \`:irrelevant_jekyll\`** — the existing detection (no \`_config.yml\` AND no \`Gemfile\`) is accurate, but \`:low\` meant it never made it onto any actionable backlog. Estate language policy bans Jekyll outright in favour of \`hyperpolymath/casket-ssg\`, so when the rule fires the finding is unambiguously a delete candidate. 21 of 25 Jekyll-workflow repos in the estate fire this rule. (2) **\`:redundant_subsumed_standalone\`** — fires for any of the 9 standalone workflow names whose logic is provided by \`hyperpolymath/standards/.github/workflows/governance-reusable.yml\` when \`governance.yml\` is also present: \`\`\` workflow-linter.yml, language-policy.yml, quality.yml, security-policy.yml, guix-nix-policy.yml, npm-bun-blocker.yml, ts-blocker.yml, rsr-antipattern.yml, wellknown-enforcement.yml \`\`\` Authoritative subsumption list per the reusable's own header. Audit found 107 instances across 88 repos. \`workflow-linter.yml\` alone is in 80 repos. (3) **\`:irrelevant_rust_ci\`** — fires when \`rust-ci.yml\` exists but the repo has no \`Cargo.toml\` at root AND no \`*.rs\` files anywhere (workspace check via \`rs_file_count\` or extension scan). Cousin of \`:missing_rust_ci\` (which catches the inverse). Audit found 11 doomed cases. ## Validation 8/8 standalone smoke assertions pass: - \`:redundant_subsumed_standalone\` fires on \`workflow-linter.yml + governance.yml\` - \`:redundant_subsumed_standalone\` fires on \`quality.yml + governance.yml\` - \`:redundant_subsumed_standalone\` count matches subsumed names present - silent without \`governance.yml\` (so vintage repos still on standalones don't get pinged) - \`:irrelevant_rust_ci\` fires when no \`Cargo.toml\` + no \`.rs\` - silent when \`Cargo.toml\` present - silent on workspace layouts (\`rs_file_count > 0\`) - \`:irrelevant_jekyll\` severity is now \`:medium\` ## Related - \`hyperpolymath/standards#219\` — the root-cause fix for the governance-reusable \`workflow_sha\` bug that triggered the audit. - \`#348\` — sibling rules for the \`workflow_sha\` / caller-context-\`ref\` anti-patterns (one PR over from this one in time). - Pilot deletion PRs validated against the existing rules: road-skate#1 (MERGED), vext#11, quandledb#16, absolute-zero#57, Axiom.jl#29. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d79da94 commit dcc2ffe

1 file changed

Lines changed: 59 additions & 2 deletions

File tree

lib/rules/cicd_rules.ex

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,8 @@ defmodule Hypatia.Rules.CicdRules do
172172
description: "ts-blocker.yml in repo with no TypeScript or JavaScript"},
173173
%{id: :irrelevant_npm_blocker, severity: :low, auto_fixable: true,
174174
description: "npm-bun-blocker.yml in repo with no JS package ecosystem"},
175-
%{id: :irrelevant_jekyll, severity: :low, auto_fixable: true,
176-
description: "Jekyll workflow in repo with no _config.yml or Gemfile"},
175+
%{id: :irrelevant_jekyll, severity: :medium, auto_fixable: true,
176+
description: "Jekyll workflow in repo with no _config.yml or Gemfile (estate policy bans Jekyll; replacement is casket-ssg)"},
177177
%{id: :irrelevant_guix_nix, severity: :low, auto_fixable: true,
178178
description: "guix-nix-policy.yml in repo with no Guix or Nix configuration"},
179179
%{id: :irrelevant_wellknown, severity: :low, auto_fixable: true,
@@ -187,6 +187,20 @@ defmodule Hypatia.Rules.CicdRules do
187187
%{id: :redundant_security_policy_wf, severity: :info, auto_fixable: true,
188188
description: "security-policy.yml checking for SECURITY.md that already exists"},
189189

190+
# Standalone workflows subsumed by governance-reusable.yml (per the
191+
# hyperpolymath/standards governance-reusable.yml header). When
192+
# `governance.yml` is present in a repo, every name in
193+
# @subsumed_standalones below is redundant — its logic runs via the
194+
# reusable and the standalone copy drifts independently.
195+
%{id: :redundant_subsumed_standalone, severity: :medium, auto_fixable: true,
196+
description: "Standalone workflow whose logic is already exercised by governance.yml (calls governance-reusable.yml)"},
197+
198+
# rust-ci.yml exists but the repo has no Cargo.toml at root and no .rs
199+
# files anywhere — guaranteed install/build failure. Cousin of
200+
# :missing_rust_ci, which catches the inverse (Cargo.toml without CI).
201+
%{id: :irrelevant_rust_ci, severity: :high, auto_fixable: true,
202+
description: "rust-ci.yml in repo with no Cargo.toml at root and no .rs files (guaranteed install failure)"},
203+
190204
# Missing language-appropriate CI
191205
%{id: :missing_julia_ci, severity: :high, auto_fixable: true,
192206
description: "Julia package without CI running Pkg.test()"},
@@ -271,6 +285,49 @@ defmodule Hypatia.Rules.CicdRules do
271285
end
272286
end)
273287

288+
# Subsumed standalones: when `governance.yml` is present in the repo,
289+
# each of these standalone workflow files is redundant — its logic
290+
# already runs via the standards governance-reusable.yml. Authoritative
291+
# list per the reusable's own header comment in hyperpolymath/standards.
292+
subsumed_standalones = [
293+
"workflow-linter.yml",
294+
"language-policy.yml",
295+
"quality.yml",
296+
"security-policy.yml",
297+
"guix-nix-policy.yml",
298+
"npm-bun-blocker.yml",
299+
"ts-blocker.yml",
300+
"rsr-antipattern.yml",
301+
"wellknown-enforcement.yml"
302+
]
303+
304+
results =
305+
if "governance.yml" in workflows do
306+
Enum.reduce(subsumed_standalones, results, fn wf, acc ->
307+
if wf in workflows do
308+
[%{pattern: :redundant_subsumed_standalone, workflow: wf, auto_fixable: true} | acc]
309+
else
310+
acc
311+
end
312+
end)
313+
else
314+
results
315+
end
316+
317+
# rust-ci.yml without any Rust code in the repo. Mirrors
318+
# :missing_rust_ci (which detects the inverse) but uses the same
319+
# file-presence and *.rs walk heuristics that BaselineHealth uses.
320+
has_rs_anywhere =
321+
Map.get(repo_info, :rs_file_count, 0) > 0 or
322+
Enum.any?(files, &String.ends_with?(&1, ".rs"))
323+
324+
results =
325+
if "rust-ci.yml" in workflows and "Cargo.toml" not in files and not has_rs_anywhere do
326+
[%{pattern: :irrelevant_rust_ci, workflow: "rust-ci.yml", auto_fixable: true} | results]
327+
else
328+
results
329+
end
330+
274331
# Missing language-appropriate CI
275332
has_ci = Enum.any?(workflows, fn w -> w in ["ci.yml", "CI.yml", "test.yml"] end)
276333

0 commit comments

Comments
 (0)