|
| 1 | +# Workflow Convergence Campaign — 2026-05-26 |
| 2 | + |
| 3 | +> Generated by: estate-wide audit of drifting per-repo workflow templates |
| 4 | +> Scope: 5 candidate templates ranked by drift × deployments × feature-variance |
| 5 | +> Outcome: 5 reusable-workflow PRs filed in this repo; classifier tooling shipped; nested-path methodology gotcha documented for future campaigns |
| 6 | +
|
| 7 | +## Summary |
| 8 | + |
| 9 | +This campaign extracted 5 drifting per-repo workflow templates into reusable workflows hosted in this repo, plus shipped the classifier tooling and a Git-Tree-walk helper that planned the wrapper sweeps and validated the deployment counts. |
| 10 | + |
| 11 | +The campaign also surfaced a **methodology gotcha** that affects every future drift survey in this estate: `gh api /search/code` queries undercount workflow files in three compounding ways. After the campaign-meta-doc filed its first cut, the helper at [#204](https://github.com/hyperpolymath/standards/pull/204) walked the Git Tree API for all 5 templates and revised the count tables — top-level deployments were undercounted by 1–35% depending on template, and nested copies were undercounted by 100%+ for several templates. See [Corrected estate counts](#corrected-estate-counts) for the helper-validated tables. |
| 12 | + |
| 13 | +## Filed PRs |
| 14 | + |
| 15 | +Deploy counts here are the **helper-validated top-level counts** (from `list-workflow-paths.sh` walking each repo's Git Tree). The initial path-filtered survey numbers — which are listed in the PR bodies themselves — are 1–35% lower depending on template. |
| 16 | + |
| 17 | +| PR | Template | Top-level (helper) | Top-level (PR body) | Reusable LOC | Top SHA share | |
| 18 | +|---|---|---:|---:|---:|---| |
| 19 | +| [#187](https://github.com/hyperpolymath/standards/pull/187) | `mirror-reusable.yml` | 293 | 289 | 165 | 76% | |
| 20 | +| [#190](https://github.com/hyperpolymath/standards/pull/190) | `secret-scanner-reusable.yml` | 299 | 281 | 159 | 69% (across top 4 SHAs) | |
| 21 | +| [#192](https://github.com/hyperpolymath/standards/pull/192) | `codeql-reusable.yml` | 280 | 263 | 96 | 83% single-language | |
| 22 | +| [#193](https://github.com/hyperpolymath/standards/pull/193) | `hypatia-scan-reusable.yml` | 344 | 255 | 459 | 83.5% top-5 | |
| 23 | +| [#194](https://github.com/hyperpolymath/standards/pull/194) | sweep-classifier scripts | — | — | — | tooling | |
| 24 | +| [#199](https://github.com/hyperpolymath/standards/pull/199) | this campaign meta-doc | — | — | — | docs | |
| 25 | +| [#204](https://github.com/hyperpolymath/standards/pull/204) | `list-workflow-paths.sh` helper + classifier ingestion | — | — | — | tooling | |
| 26 | +| [#205](https://github.com/hyperpolymath/standards/pull/205) | `scorecard-reusable.yml` | 278 | 258 | 87 | 38.8% | |
| 27 | + |
| 28 | +All PRs have auto-merge enabled (per the [auto-merge-on-all-PRs standing policy](https://github.com/hyperpolymath/.github)). The wrapper sweep does NOT fire automatically — each template is owner-gated post-merge. |
| 29 | + |
| 30 | +## Convergence set status |
| 31 | + |
| 32 | +The 5-candidate convergence set is **fully filed** (#187 mirror / #190 secret-scanner / #192 codeql / #193 hypatia-scan / #205 scorecard). #194 (classifiers), #199 (this doc), and #204 (helper + nested-path classifier ingestion) close out the supporting infrastructure. |
| 33 | + |
| 34 | +## Ranking methodology |
| 35 | + |
| 36 | +For each candidate, three signals were combined: |
| 37 | + |
| 38 | +1. **Drift** = (unique blob SHAs / total deployments). Lower = more homogeneous. |
| 39 | +2. **Deployments** = number of repos carrying the template. Higher = more leverage. |
| 40 | +3. **Feature variance** = whether the SHAs differ in job-set / step-set / language matrix (real customization) or only in SPDX-header / action-pin / whitespace (mechanical lag). |
| 41 | + |
| 42 | +A reusable is viable when (2) is high AND (3) is low. (1) tells you how much wrapper-sweep work the rollout needs. |
| 43 | + |
| 44 | +| Template | Deploys | Drift | Feature variance | Reusable viable | |
| 45 | +|---|---|---|---|---| |
| 46 | +| `hypatia-scan.yml` | 255 | 11.8% | ZERO | ✅ (filed #193) | |
| 47 | +| `scorecard.yml` | 258 | 17.8% | ZERO | ✅ (recommended #195) | |
| 48 | +| `secret-scanner.yml` | 281 | 19% | LOW (job-set homogeneous; force-propagates `shell-secrets` guardrail) | ✅ (filed #190) | |
| 49 | +| `mirror.yml` | 289 | 24% | LOW (job-set homogeneous; same 7 forges across top SHAs) | ✅ (filed #187) | |
| 50 | +| `codeql.yml` | 263 | 26% | MEDIUM (language matrix variance — needed `language` + `build-mode` inputs) | ✅ (filed #192) | |
| 51 | + |
| 52 | +## The nested-path methodology gotcha |
| 53 | + |
| 54 | +### Layer 1: path-prefix filter excludes nested workflows |
| 55 | + |
| 56 | +`gh api /search/code` with `path:.github/workflows` matches the path **PREFIX**, so: |
| 57 | +- `.github/workflows/codeql.yml` ✓ matches |
| 58 | +- `developer-ecosystem/asdf-augmenters/.github/workflows/codeql.yml` ✗ does NOT match |
| 59 | + |
| 60 | +Removing the `path:` filter and running `filename:codeql.yml org:hyperpolymath` exposes nested copies inside monorepos like `developer-ecosystem`, `ssg-collection`, `ambientops`, `standards`, `julia-ecosystem`, `asdf-tool-plugins`, etc. |
| 61 | + |
| 62 | +### Layer 2: even the broad query is org-scope-truncated |
| 63 | + |
| 64 | +GitHub Code Search's org-scoped result set is capped well below the true file count. For `codeql.yml`: |
| 65 | + |
| 66 | +| Monorepo | Broad query saw | Per-repo query (truth) | Missed | |
| 67 | +|---|---:|---:|---:| |
| 68 | +| developer-ecosystem | 41 | 170 | 129 | |
| 69 | +| asdf-tool-plugins | 11 | 111 | 100 | |
| 70 | +| ssg-collection | 39 | 68 | 29 | |
| 71 | +| standards | 26 | 39 | 13 | |
| 72 | +| ambientops | 12 | 29 | 17 | |
| 73 | +| julia-ecosystem | 11 | 20 | 9 | |
| 74 | +| **Top-6 total** | **140** | **437** | **297** | |
| 75 | + |
| 76 | +Broad-query undercount factor: **~2.6×** inside heavily-nested monorepos. Per-repo queries (`gh api repos/<org>/<repo>/git/trees/HEAD?recursive=1 --jq '.tree[] | select(.path | endswith("/<file>.yml"))'`) are the only reliable source of truth for monorepo-nested workflow files. |
| 77 | + |
| 78 | +**Layer 2 also affects path-filtered queries** (a finding from the helper re-survey). `path:.github/workflows filename:hypatia-scan.yml` returned 255 results, but the Git-Tree walk found 344 top-level files — a 35% gap on the most-deployed template. The path filter is less severely truncated than the broad query, but both are sub-truth. Only direct Git-Tree enumeration is reliable. |
| 79 | + |
| 80 | +### Layer 3: nested workflows are inert |
| 81 | + |
| 82 | +**GitHub Actions only runs workflows from the repo-root `.github/workflows/` directory.** Workflows under any other path (e.g. `a2ml/bindings/deno/.github/workflows/secret-scanner.yml` inside the `standards` repo) are **never triggered**. |
| 83 | + |
| 84 | +This means nested copies are one of: |
| 85 | + |
| 86 | +1. **Vendored templates** — maintained as a snapshot of what a sub-package's own workflow would be if it were extracted into its own GitHub repo. Read-only intent; not load-bearing for the sub-package's CI. |
| 87 | +2. **Stale leftover** — surviving artifacts of a previous monorepo merger; pure dead code. |
| 88 | + |
| 89 | +**Implication:** the security-gap argument that motivated the secret-scanner reusable (forcing `shell-secrets` propagation post-Cloudflare-leak) does NOT extend to nested copies. The ~282 nested `secret-scanner.yml` files do not represent missing-guardrail attack surface — they don't run. The wrapper sweep on nested copies is **single-source-of-truth cleanup**, not security-hardening. |
| 90 | + |
| 91 | +### Corrected estate counts |
| 92 | + |
| 93 | +After [#204](https://github.com/hyperpolymath/standards/pull/204) shipped `list-workflow-paths.sh`, all 5 templates were re-enumerated by walking each repo's Git Tree API directly. The results invalidated **both** the broad-query and path-filtered survey numbers — even top-level counts were undercounted across the board. |
| 94 | + |
| 95 | +#### Helper-validated counts (Git-Tree walk, all 5 templates) |
| 96 | + |
| 97 | +| Template | Top-level | Nested | Total | Unique blob SHAs (all) | |
| 98 | +|---|---:|---:|---:|---:| |
| 99 | +| `hypatia-scan.yml` | **344** | 603 | **947** | 32 | |
| 100 | +| `mirror.yml` | **293** | 335 | **628** | 120 | |
| 101 | +| `secret-scanner.yml` | **299** | 292 | **591** | 83 | |
| 102 | +| `codeql.yml` | **280** | 646 | **926** | 175 | |
| 103 | +| `scorecard.yml` | **278** | 626 | **904** | 114 | |
| 104 | + |
| 105 | +#### Top-level-only drift |
| 106 | + |
| 107 | +The drift % when nested copies are excluded is the figure relevant for the *executing* surface (since nested workflows are inert, per Layer 3): |
| 108 | + |
| 109 | +| Template | Top-level | Unique blob SHAs (top-level only) | Drift | |
| 110 | +|---|---:|---:|---:| |
| 111 | +| `hypatia-scan.yml` | 344 | **3** | **0.9%** | |
| 112 | +| `secret-scanner.yml` | 299 | 54 | 18.1% | |
| 113 | +| `scorecard.yml` | 278 | 46 | 16.5% | |
| 114 | +| `mirror.yml` | 293 | 75 | 25.6% | |
| 115 | +| `codeql.yml` | 280 | 75 | 26.8% | |
| 116 | + |
| 117 | +**hypatia-scan top-level is byte-near-identical across 344 sites** — only 3 unique blob SHAs total. The reusable wrapper sweep for hypatia-scan is therefore essentially mechanical with near-zero per-repo variance, much tighter than the 11.8% drift the PR body reports (which was the drift across top-level + nested). |
| 118 | + |
| 119 | +#### Initial-survey undercount summary |
| 120 | + |
| 121 | +| Template | PR-body top-level | Helper top-level | Top-level undercount | Original nested estimate | Helper nested | |
| 122 | +|---|---:|---:|---:|---:|---:| |
| 123 | +| `hypatia-scan.yml` | 255 | 344 | **+89 (35%)** | 449 | 603 | |
| 124 | +| `secret-scanner.yml` | 281 | 299 | +18 (6%) | 282 | 292 | |
| 125 | +| `codeql.yml` | 263 | 280 | +17 (6%) | ~518 | 646 | |
| 126 | +| `scorecard.yml` | 258 | 278 | +20 (8%) | 626 | 626 | |
| 127 | +| `mirror.yml` | 289 | 293 | +4 (1%) | 133 | 335 | |
| 128 | + |
| 129 | +The `path:.github/workflows`-filtered query under-reports top-level counts at the rate above, and the broad query under-reports nested counts even more severely (most extremely for mirror: 133 reported vs 335 true). Some of the gap is creation-of-new-repos between survey-and-helper runs, but most is search-index truncation. |
| 130 | + |
| 131 | +#### LOC retirement (top-level only — the executing surface) |
| 132 | + |
| 133 | +| Template | Top-level | × Canonical LOC | LOC retired | |
| 134 | +|---|---:|---:|---:| |
| 135 | +| `hypatia-scan.yml` | 344 | 416 | **~143,000** | |
| 136 | +| `mirror.yml` | 293 | 145 | ~42,500 | |
| 137 | +| `codeql.yml` | 280 | ~150 | ~42,000 | |
| 138 | +| `secret-scanner.yml` | 299 | ~120 | ~36,000 | |
| 139 | +| `scorecard.yml` | 278 | 41 | ~11,400 | |
| 140 | +| **Total (5 reusables, top-level)** | | | **~275,000** | |
| 141 | + |
| 142 | +Including nested copies (single-source-of-truth cleanup, not the executing surface): **~732,000 LOC** estate-wide eligible for wrapper replacement. |
| 143 | + |
| 144 | +## Classifier tooling pattern (`scripts/sweep-classifiers/`) |
| 145 | + |
| 146 | +Each template has a `classify-<template>.sh` script (committed in [#194](https://github.com/hyperpolymath/standards/pull/194)) that: |
| 147 | + |
| 148 | +1. Reads paginated `gh api /search/code` JSON into a `(repo, sha)` table. |
| 149 | +2. Fetches each unique blob ONCE (cached in `$BLOBS_DIR`) — bandwidth-efficient. |
| 150 | +3. Classifies each blob by job-set / line-count / language-matrix / SPDX-header lag. |
| 151 | +4. Emits a per-repo TSV with `<repo> <class> <evidence>` for sweep batching. |
| 152 | + |
| 153 | +Pattern: the classifier output drives a 3-wave sweep (mechanical bulk → propagation-lag bulk → per-repo review). Wave-1 PRs are auto-generated wrappers pinned to the merged-reusable HEAD SHA. |
| 154 | + |
| 155 | +**Outstanding tooling gap** (filed as follow-up): classifiers currently emit one row per repo, not per `(repo, path)` tuple. For monorepo-nested copies, the classifiers need to ingest the per-repo recursive-tree listing and emit one wrapper site per nested copy. This is a small change to each script's input loop. |
| 156 | + |
| 157 | +## Per-template specifics |
| 158 | + |
| 159 | +### `mirror.yml` (PR #187) |
| 160 | + |
| 161 | +7 forge jobs (gitlab, bitbucket, codeberg, sourcehut, disroot, gitea, radicle), each gated on `vars.<FORGE>_MIRROR_ENABLED == 'true'`. Only input: `runs-on`. Caller MUST use `secrets: inherit` (jobs reference forge-specific secrets). Wrapper sweep is mostly mechanical; the 22 NEEDS_REVIEW cases in the classifier output are slim 2-3-forge variants (older repos pre-7-forge expansion) or NONE-stub placeholders. |
| 162 | + |
| 163 | +### `secret-scanner.yml` (PR #190) |
| 164 | + |
| 165 | +4 jobs: `trufflehog`, `gitleaks`, `rust-secrets`, `shell-secrets`. Self-conditioning — `rust-secrets` exits early on no `Cargo.toml`, `shell-secrets` no-ops without `.sh`/`.bash`. **Force-propagates** the `shell-secrets` job (post-Cloudflare-leak guardrail) to the estate, where it currently exists in only 1 of 281 top-level deployments (this repo itself). The 282 nested copies do NOT close additional security gaps (Layer 3 caveat) but DO consolidate the source of truth. |
| 166 | + |
| 167 | +The full-pagination data corrected an initial 100-repo sample's "55% drift" reading to a true **19% drift** with 100% job-set homogeneity. The parallel-session ranking that said "skip secret-scanner" was based on the sample drift figure and missed the homogeneity finding. |
| 168 | + |
| 169 | +### `codeql.yml` (PR #192) |
| 170 | + |
| 171 | +Two inputs: `language` (default `javascript-typescript`) and `build-mode` (default `none`). Single-language single-job design — multi-language wrappers invoke the reusable per-language. Language matrix distribution across 263 top-level deployments: |
| 172 | + |
| 173 | +- 84.8% `javascript-typescript`-only |
| 174 | +- 8.4% `actions`-only |
| 175 | +- ~3.4% multi-language |
| 176 | +- ~3.4% NONE-matrix (needs per-repo review) |
| 177 | + |
| 178 | +The campaign's most complex reusable — but still ~93% mechanical sweep. |
| 179 | + |
| 180 | +### `hypatia-scan.yml` (PR #193) |
| 181 | + |
| 182 | +Byte-identical to the canonical 416-line `scan` job. Single `runs-on` input. Caller MUST grant `security-events: write` + `pull-requests: write` permissions AND use `secrets: inherit`. The 416-line canonical × 704 total estate sites = ~280k LOC retired (the largest leverage of the 5 campaigns). 30 unique blob SHAs / 11.8% drift / 100% single-`scan`-job homogeneity. All drift is pure propagation lag (older slimmer versions of the same body). |
| 183 | + |
| 184 | +### `scorecard.yml` (recommended #195) |
| 185 | + |
| 186 | +258 deployments, 17.8% drift, **100% mechanical drift** — every variant has one `analysis` job with `checkout → ossf/scorecard-action → upload-sarif`. Same single `scorecard-action` SHA pin across all variants; differences are SPDX header (`PMPL-1.0` / `PMPL-1.0-or-later` / `MPL-2.0`), `upload-sarif` SHA pin, `permissions: read-all` vs `contents: read`, and presence/absence of the estate concurrency-guardrail block. 41-line canonical → ~8k LOC retired top-level only. Zero nested copies, making it the cleanest sweep candidate. |
| 187 | + |
| 188 | +## Reconciliation with the parallel-session ranking |
| 189 | + |
| 190 | +A parallel session ([[project_foundational_workflow_survey_2026_05_26]] in agent memory) ranked the 5 candidates and concluded "pursue hypatia-scan + mirror; skip codeql / secret-scanner / scorecard." The recommendation set differs from this campaign's outcome on three templates: |
| 191 | + |
| 192 | +| Template | Parallel call | This campaign | Reason for divergence | |
| 193 | +|---|---|---|---| |
| 194 | +| `secret-scanner.yml` | SKIP | PURSUE | Parallel based on 55% sample drift; full pagination → 19% drift, 100% homogeneity. The skip call missed the shell-secrets propagation gap. | |
| 195 | +| `codeql.yml` | SKIP | PURSUE | Real (but contained) language-matrix variance — 85% are single-language `javascript-typescript`, making the 2-input reusable dead-simple for the dominant case. | |
| 196 | +| `scorecard.yml` | SKIP | PURSUE | Same shape as secret-scanner mistake: 18% sample drift looks like real customization but is ALL mechanical (action-SHA churn + SPDX header drift). | |
| 197 | + |
| 198 | +The agreement between sessions was strong on `hypatia-scan.yml` (both: pursue, HIGH homogeneity) and `mirror.yml` (both: pursue, ~76% top-SHA dominance). The divergence comes from the parallel session's reliance on sample-drift estimates without full pagination, where my campaign full-paginated and blob-fetched each candidate. |
| 199 | + |
| 200 | +## Standing follow-ups |
| 201 | + |
| 202 | +1. **Owner-review on the 8 PRs** (#187/#190/#192/#193/#194/#199/#204/#205) — auto-merge enabled; CI-green will auto-land. |
| 203 | +2. **Per-repo recursive-tree classifier inputs** — DONE in #204. Classifiers now accept the helper's TSV output (`<repo>\t<path>\t<blob-sha>`) and emit one row per `(repo, path)` tuple. Backward-compatible with legacy JSONL. |
| 204 | +3. **Wrapper sweep firing** is owner-gated per template. The 82-PR rust-ci sweep is the upper bound the user has signalled comfort with; mirror's 293-PR sweep would need explicit per-batch sign-off. With the helper-validated counts, sweeps now have ground-truth target lists. |
| 205 | +4. **Nested-copy disposition decision** — for each monorepo (developer-ecosystem, ssg-collection, ambientops, julia-ecosystem, asdf-tool-plugins, etc.), are the nested workflows vendored templates (keep, sync to reusable) or stale leftover (delete)? This is a per-monorepo judgement call; the campaign defers it. |
| 206 | +5. **Re-run helper periodically** — the Git-Tree-walk truth shifts as new repos land. Suggest a quarterly re-run of `list-workflow-paths.sh` against each of the 5 templates to keep this doc's count tables fresh. |
| 207 | + |
| 208 | +## Cross-references |
| 209 | + |
| 210 | +- Prior pattern: PR [#168 language-policy convergence](https://github.com/hyperpolymath/standards/pull/168) — first instance of "subsume legacy workflow into governance-reusable" pattern; merged 2026-05-26. |
| 211 | +- Prior pattern: PR [#174 rust-ci + elixir-ci reusables](https://github.com/hyperpolymath/standards/pull/174) — extended the pattern; 82-PR wrapper sweep filed downstream (`project_rust_ci_wrapper_sweep_2026_05_26`). |
| 212 | +- Estate-wide language policy: [`.claude/CLAUDE.md`](../../.claude/CLAUDE.md) — Python/V-lang/ATS2/ReScript/TypeScript bans enforced by `governance-reusable.yml`. |
| 213 | +- Security context: the `shell-secrets` job force-propagation in #190 is direct mitigation for the [unrotated Cloudflare API token leak](#) (memory `project_cloudflare_token_leak_2026_05_21`). |
0 commit comments