From 3bf594ab9024ae07f09ccc534037930e655a2009 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 27 May 2026 00:20:56 +0100 Subject: [PATCH] feat(cicd-rules): close 3 doomed-CI detection gaps surfaced by 2026-05-27 audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 2026-05-27 estate doomed-CI audit produced concrete 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 change closes those gaps in `Hypatia.Rules.CicdRules`. (1) Bump `:irrelevant_jekyll` from `:low` to `:medium`. The existing rule correctly fires when neither `_config.yml` nor `Gemfile` is present (the Jekyll-build step is doomed), but at `:low` 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, not a triage candidate. 21 of 25 Jekyll-workflow repos in the estate hit this — bumping the severity makes the rule do real work. (2) New `:redundant_subsumed_standalone` (severity `:medium`). Fires for each of the 9 standalone workflow names whose logic is now provided by `hyperpolymath/standards/.github/workflows/governance-reusable.yml`, when `governance.yml` is also present in the repo. Authoritative subsumption list per the reusable's own header comment: 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 Audit found 107 instances across 88 repos. workflow-linter.yml alone appears in 80 repos. The standalones drift independently of the reusable's authoritative logic; deletion is the fix. (3) New `:irrelevant_rust_ci` (severity `:high`). 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 files-list extension scan). Cousin of `:missing_rust_ci` (which catches the inverse: Cargo.toml exists but no CI). Audit found 11 doomed cases; every one is a delete candidate. Validation: 8/8 standalone smoke assertions pass — verified that `:redundant_subsumed_standalone` is silent without `governance.yml`, `:irrelevant_rust_ci` is silent on workspace layouts with `.rs` files, and the Jekyll severity bump is reflected in `waste_patterns/0`. Refs: standards#219 (the cascading root-cause fix that triggered the audit), hypatia#348 (sibling rules for `workflow_sha`/`ref` anti-patterns). Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/rules/cicd_rules.ex | 61 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/lib/rules/cicd_rules.ex b/lib/rules/cicd_rules.ex index d50700d2..4f72211d 100644 --- a/lib/rules/cicd_rules.ex +++ b/lib/rules/cicd_rules.ex @@ -172,8 +172,8 @@ defmodule Hypatia.Rules.CicdRules do description: "ts-blocker.yml in repo with no TypeScript or JavaScript"}, %{id: :irrelevant_npm_blocker, severity: :low, auto_fixable: true, description: "npm-bun-blocker.yml in repo with no JS package ecosystem"}, - %{id: :irrelevant_jekyll, severity: :low, auto_fixable: true, - description: "Jekyll workflow in repo with no _config.yml or Gemfile"}, + %{id: :irrelevant_jekyll, severity: :medium, auto_fixable: true, + description: "Jekyll workflow in repo with no _config.yml or Gemfile (estate policy bans Jekyll; replacement is casket-ssg)"}, %{id: :irrelevant_guix_nix, severity: :low, auto_fixable: true, description: "guix-nix-policy.yml in repo with no Guix or Nix configuration"}, %{id: :irrelevant_wellknown, severity: :low, auto_fixable: true, @@ -187,6 +187,20 @@ defmodule Hypatia.Rules.CicdRules do %{id: :redundant_security_policy_wf, severity: :info, auto_fixable: true, description: "security-policy.yml checking for SECURITY.md that already exists"}, + # Standalone workflows subsumed by governance-reusable.yml (per the + # hyperpolymath/standards governance-reusable.yml header). When + # `governance.yml` is present in a repo, every name in + # @subsumed_standalones below is redundant — its logic runs via the + # reusable and the standalone copy drifts independently. + %{id: :redundant_subsumed_standalone, severity: :medium, auto_fixable: true, + description: "Standalone workflow whose logic is already exercised by governance.yml (calls governance-reusable.yml)"}, + + # rust-ci.yml exists but the repo has no Cargo.toml at root and no .rs + # files anywhere — guaranteed install/build failure. Cousin of + # :missing_rust_ci, which catches the inverse (Cargo.toml without CI). + %{id: :irrelevant_rust_ci, severity: :high, auto_fixable: true, + description: "rust-ci.yml in repo with no Cargo.toml at root and no .rs files (guaranteed install failure)"}, + # Missing language-appropriate CI %{id: :missing_julia_ci, severity: :high, auto_fixable: true, description: "Julia package without CI running Pkg.test()"}, @@ -271,6 +285,49 @@ defmodule Hypatia.Rules.CicdRules do end end) + # Subsumed standalones: when `governance.yml` is present in the repo, + # each of these standalone workflow files is redundant — its logic + # already runs via the standards governance-reusable.yml. Authoritative + # list per the reusable's own header comment in hyperpolymath/standards. + subsumed_standalones = [ + "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" + ] + + results = + if "governance.yml" in workflows do + Enum.reduce(subsumed_standalones, results, fn wf, acc -> + if wf in workflows do + [%{pattern: :redundant_subsumed_standalone, workflow: wf, auto_fixable: true} | acc] + else + acc + end + end) + else + results + end + + # rust-ci.yml without any Rust code in the repo. Mirrors + # :missing_rust_ci (which detects the inverse) but uses the same + # file-presence and *.rs walk heuristics that BaselineHealth uses. + has_rs_anywhere = + Map.get(repo_info, :rs_file_count, 0) > 0 or + Enum.any?(files, &String.ends_with?(&1, ".rs")) + + results = + if "rust-ci.yml" in workflows and "Cargo.toml" not in files and not has_rs_anywhere do + [%{pattern: :irrelevant_rust_ci, workflow: "rust-ci.yml", auto_fixable: true} | results] + else + results + end + # Missing language-appropriate CI has_ci = Enum.any?(workflows, fn w -> w in ["ci.yml", "CI.yml", "test.yml"] end)