Skip to content

fix: route nested singular test directories (#10660) #142

fix: route nested singular test directories (#10660)

fix: route nested singular test directories (#10660) #142

Workflow file for this run

# Post-merge guard for `main`, plus the weekly full-tree audit debt sweep.
#
# (File name is historical — this started life as the audit-only debt sweep and
# grew the post-merge lint/test gates. Kept as `audit-debt.yml` so the file's
# git history stays intact.)
#
# ── Why the post-merge gates exist (#6807) ──────────────────────────────────
# Two pipelines guard this repo, and until now BOTH were changed-scope:
#
# * ci.yml (PRs) — `differential-gating: true`, scoped to the PR diff.
# * release.yml — gate-lint/gate-test pass `--changed-since <before>`, so
# they only re-check what that one push touched.
#
# Nothing ever ran the whole suite against the integrated `main`. A change whose
# blast radius lands OUTSIDE its own diff is invisible to both: the PR never
# runs the affected tests, and the release only runs them if some later merge
# happens to touch those same files.
#
# That is exactly how 2026-07-27 went. A POSIX-sh portability bug merged green,
# main went red in `crates/homeboy-lab-runner/src/workspace/tests/prune.rs`, and
# nobody found out until merge #10425 happened to touch prune.rs and dragged it
# into the release's changed scope. Release runs 30272803452 and 30271131168
# both died at `Release Quality Policy` → `Test`. Cost: 188 commits
# undeliverable, two orphaned tags, ~16 hours of stranded work.
#
# The release is the WORST place to discover this: it runs 16-35 minutes and a
# failure there can strand a tag. So the gates below run the release-blocking
# command set (RELEASE_BLOCKING_COMMANDS = `review lint,review test`) against
# `main` on every merge, at FULL scope.
#
# Full scope is not an extra flag — homeboy-action's `scope: auto` already
# resolves to `mode=full` on push events. The release narrows itself by
# explicitly passing `--changed-since`; we simply do not pass it. That makes
# this gate a strict superset of the release's blocking scope, which is the
# point: anything that would fail the release is caught here first, and so is
# the cross-scope breakage the release would have missed.
#
# ── Why the weekly sweep exists ─────────────────────────────────────────────
# The PR pipeline runs a `--profile=pr --changed-since` audit: fast, scoped to
# the diff, and it only runs the cheap RootOnly detector families. The
# whole-tree discovery detectors (duplication, dead code, constant/command
# bypass, god files, …) are deliberately OFF in the PR profile — they need the
# entire codebase to work and would re-report repo-wide debt on every unrelated
# PR.
#
# Those discovery detectors are the "code factory" roadmap: each finding kind
# becomes one deduplicated tracking issue (via homeboy-action's auto-issue
# filing), and the issue closes when that fix kind gets automated. The sweep
# runs weekly, decoupled from releases so a full audit never taxes the release
# path, and files/refreshes one issue per finding kind.
name: Main Guard
on:
push:
branches: [main]
schedule:
# Mondays 07:00 UTC — once a week is plenty for slow-moving structural debt.
- cron: '0 7 * * 1'
workflow_dispatch:
inputs:
profile:
description: 'Audit profile to run (full = all discovery detectors).'
required: false
default: 'full'
# Latest merge wins. This gate answers exactly one question — "is main green
# RIGHT NOW?" — and that answer is a pure function of the current tip, so a
# superseded run's verdict is worthless. It also produces no artifacts, mutates
# nothing, and blocks nothing downstream, so cancelling it is free.
#
# This is deliberately the OPPOSITE of release.yml's `cancel-in-progress: false`.
# The release queues because it may be mid-tag/mid-publish and cancelling it
# strands a tag. Nothing here can strand anything.
#
# It is also a capacity requirement, not just a nicety: at ~40 merges/day
# against a 16-35 minute suite, queueing every merge would serialise into
# 10-23 hours of backlog and the gate would end up reporting on hours-old main
# — strictly worse than reporting on the current tip.
#
# Trade-off, stated plainly: when a burst of merges collapses into one run, the
# failure is attributed to the newest merge rather than the one that actually
# broke it. The failure summary prints the compare range so the candidate set is
# still bounded (see "Attribute failure to this merge" below).
#
# Push and sweep get separate groups so a merge train never cancels the weekly
# audit sweep (and vice versa).
concurrency:
group: main-guard-${{ github.event_name == 'push' && 'post-merge' || 'sweep' }}
cancel-in-progress: true
permissions:
contents: read
issues: write
# Shared-artifact handoff between gate-build and the gates.
actions: read
jobs:
# ── Build once, share with all three post-merge gates ──
# Mirrors release.yml's gate-build → artifact → `binary-path` handoff. Before
# this, the audit gate compiled homeboy by itself via `cargo run`; adding two
# more gates that each did their own build would have tripled compile cost on
# every merge. One release build now feeds all three.
gate-build:
name: Build
if: github.event_name == 'push'
runs-on: ubuntu-latest
# Least privilege: this job compiles and uploads, it never files issues.
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
# Deliberately the same cache key as release.yml's gate-build: both run
# `cargo build --release` on `main`, so whichever lands first warms the
# cache for the other. A key drift in release.yml only costs a cache miss.
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-release-gate-${{ hashFiles('Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-release-gate-
- name: Build homeboy
run: cargo build --release --locked
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: homeboy-binary
path: target/release/homeboy
retention-days: 1
# ── Full-tree audit gate ──
#
# #10557: this job used to invoke `.homeboy-bin/homeboy review audit` directly.
# That skipped homeboy-action's `Install extension` step, so no extension
# declared `provides.file_extensions`, the audit corpus came back EMPTY, and
# the gate reported `files_scanned: 0, files_skipped: 1817, findings: [],
# passed: true` — green in 7 seconds on an 1800-file repository. The sibling
# lint/test gates below were unaffected because they already go through the
# action, which is why the workflow as a whole looked healthy.
#
# Two changes close it:
#
# 1. Route through Extra-Chill/homeboy-action, exactly like the lint/test
# gates and the weekly sweep, so the extension is installed and the audit
# has something to fingerprint.
# 2. `review audit` itself now hard-errors on an empty corpus
# (engine.rs, "audit.corpus"), so the 7-second green run is impossible
# for EVERY consumer, not just this workflow. The `Assert the audit
# actually scanned` step below is the belt to that braces: it is
# unconditionally blocking and fails closed.
#
# ── Why the findings verdict is reporting-only, for now ──
#
# Turning the findings verdict on today turns `main` red immediately, for debt
# nobody has triaged and that this PR did not create:
#
# * 37 findings on `main` are already unbaselined (`drift_increased: true`,
# 28 resolved, net +9) — accumulated since the 2026-07-27 baseline.
# * ~293 core_boundary_leak findings across 38 files become visible for the
# first time now that the source-policy corpus is fixed (#10558): 182
# index files (`mod.rs`/`lib.rs`/`main.rs`) plus 83 files sitting alone in
# a directory were unscannable by ANY source policy.
#
# Baselining that set requires running the FIXED binary, which only exists
# after this merges. So the findings verdict lands reporting-only and the
# follow-up issue below flips it — with a regenerated baseline — as its own
# reviewed change. `continue-on-error` here is a dated, tracked state, not a
# permanent one; `tests/audit_debt_workflow_test.rs` requires the follow-up
# issue reference to sit next to it so it cannot quietly become permanent.
#
# FOLLOW-UP: #10569 — regenerate the audit baseline on main and remove
# `continue-on-error` from the audit step below.
full-audit-gate:
name: Full-tree audit gate
if: github.event_name == 'push'
needs: gate-build
runs-on: ubuntu-latest
# Pure pass/fail gate — deliberately cannot file issues, unlike the sweep.
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Download homeboy binary
uses: actions/download-artifact@v4
with:
name: homeboy-binary
path: .homeboy-bin
- name: Verify homeboy binary present
run: |
if [ ! -f .homeboy-bin/homeboy ]; then
echo "::error::Build artifact missing from upstream Build job: .homeboy-bin/homeboy was not produced/uploaded by gate-build. This is a CI artifact-handoff problem, not a code finding on main. Re-run the failed Build job or investigate the homeboy-binary upload step." >&2
exit 1
fi
chmod +x .homeboy-bin/homeboy
# Reporting-only until #10569 baselines the pre-existing set. The action
# owns the `Install extension` step whose absence caused the 0-file scan.
- uses: Extra-Chill/homeboy-action@v2
id: audit
continue-on-error: true
with:
binary-path: .homeboy-bin/homeboy
commands: review audit
expected-commands: review audit,review lint,review test
# Full profile enables the whole-tree discovery detectors
# (duplication, dead code, source policies, core boundary leaks) that
# `--profile=pr` deliberately omits. No `--changed-since`: this gate
# exists to see the integrated tree.
args: --profile=full
# Pure pass/fail gate — the weekly sweep files the tracking issues.
# (No `autofix` input: homeboy-action@v2 has none, so passing one is
# dead configuration. This gate mutates nothing regardless.)
auto-issue: 'false'
# BLOCKING. #10557's defect was a gate that reported success while
# measuring nothing, so the one property this job enforces unconditionally
# is that the audit had a corpus at all.
#
# The assertion lives in .github/assert-audit-corpus.sh, not inline here,
# because an inline `run:` block can only ever be checked by a YAML
# substring test — and a substring test is exactly what let this step ship
# broken twice (wrong result filename, then a wrong jq path bolted on top
# of it). The script is executed against recorded fixtures by
# tests/audit_debt_workflow_test.rs, including one fixture per historical
# defect, so the assertion is verified by running it rather than by
# reading it.
#
# `files_scanned` is the CONVENTION corpus (index files and single-file
# directories excluded — see #10558). It is a lower bound on what the
# audit actually read, which is exactly what a "did this run at all"
# assertion wants. `review audit` itself already errors when the wider
# source-policy corpus is empty, so the two checks bracket the failure.
- name: Assert the audit actually scanned the tree
run: bash .github/assert-audit-corpus.sh
- name: Report the (non-blocking) audit findings verdict
if: steps.audit.outcome == 'failure'
run: |
set -euo pipefail
result="${HOMEBOY_OUTPUT_DIR:-}/review-audit.json"
echo "::warning::Full-profile audit reported findings. This verdict is REPORTING-ONLY until #10569 regenerates the baseline; the corpus assertion above is the blocking part. Expand the audit group in this job's log for the finding list."
# Put the size of the debt in the job summary. A warning annotation
# nobody counts is how a reporting-only verdict quietly becomes
# permanent; a number that has to be looked at every merge is not.
{
echo "### Full-tree audit: reporting-only verdict (#10569)"
echo
if [ -s "${result}" ]; then
jq -r '"- corpus: \(.data.summary.files_scanned) file(s)",
"- total findings: \(.data.findings | length)",
"- new since baseline: \(.data.baseline_comparison.new_items | length)",
"- resolved since baseline: \(.data.baseline_comparison.resolved_fingerprints | length)"' \
"${result}" 2>/dev/null \
|| echo "- could not read \`${result}\`"
else
echo "- no audit result at \`${result}\` (see the corpus assertion step)"
fi
} >> "${GITHUB_STEP_SUMMARY}"
# ── Release-blocking suite, at full scope, on the merged result ──
# `review lint` + `review test` is exactly release.yml's RELEASE_BLOCKING_COMMANDS
# (default 'review lint,review test'). Split into two jobs, and `review test`
# carries `--skip-lint`, both matching release.yml's gate-lint / gate-test
# split so lint work is not duplicated.
#
# The ONE deliberate difference from the release gates: no `--changed-since`.
# The release passes `--changed-since ${{ github.event.before }}`; omitting it
# lets `scope: auto` resolve to full scope on push. Matching the release's
# narrowing here would reproduce the blind spot instead of closing it.
#
# No `differential-gating` (it only applies to pull_request events anyway) and
# no `autofix` input — homeboy-action@v2 has no such input, so there is no
# autofix/auto-push path to disable. This gate never mutates the repo.
full-lint-gate:
name: Full-suite lint gate
if: github.event_name == 'push'
needs: gate-build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Download homeboy binary
uses: actions/download-artifact@v4
with:
name: homeboy-binary
path: .homeboy-bin
- name: Verify homeboy binary present
run: |
if [ ! -f .homeboy-bin/homeboy ]; then
echo "::error::Build artifact missing from upstream Build job: .homeboy-bin/homeboy was not produced/uploaded by gate-build. This is a CI artifact-handoff problem, not a lint finding on main. Re-run the failed Build job or investigate the homeboy-binary upload step." >&2
exit 1
fi
chmod +x .homeboy-bin/homeboy
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v3
continue-on-error: true
with:
app-id: ${{ secrets.HOMEBOY_APP_ID }}
private-key: ${{ secrets.HOMEBOY_APP_PRIVATE_KEY }}
- uses: Extra-Chill/homeboy-action@v2
with:
binary-path: .homeboy-bin/homeboy
commands: review lint
expected-commands: review audit,review lint,review test
# Durable notification. The failing check is the primary signal, but
# nobody watches main's commit list; the issue survives the run.
auto-issue: 'true'
app-token: ${{ steps.app-token.outputs.token || '' }}
- name: Attribute failure to this merge
if: failure()
env:
MERGE_SHA: ${{ github.sha }}
MERGE_BEFORE: ${{ github.event.before }}
MERGE_SUBJECT: ${{ github.event.head_commit.message }}
MERGE_AUTHOR: ${{ github.event.head_commit.author.name }}
GATE: lint
run: bash .github/report-red-main.sh
full-test-gate:
name: Full-suite test gate
if: github.event_name == 'push'
needs: gate-build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Download homeboy binary
uses: actions/download-artifact@v4
with:
name: homeboy-binary
path: .homeboy-bin
- name: Verify homeboy binary present
run: |
if [ ! -f .homeboy-bin/homeboy ]; then
echo "::error::Build artifact missing from upstream Build job: .homeboy-bin/homeboy was not produced/uploaded by gate-build. This is a CI artifact-handoff problem, not a test finding on main. Re-run the failed Build job or investigate the homeboy-binary upload step." >&2
exit 1
fi
chmod +x .homeboy-bin/homeboy
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v3
continue-on-error: true
with:
app-id: ${{ secrets.HOMEBOY_APP_ID }}
private-key: ${{ secrets.HOMEBOY_APP_PRIVATE_KEY }}
- uses: Extra-Chill/homeboy-action@v2
with:
binary-path: .homeboy-bin/homeboy
commands: review test
expected-commands: review audit,review lint,review test
# Matches release.yml gate-test: lint is its own job above.
args: --skip-lint
auto-issue: 'true'
app-token: ${{ steps.app-token.outputs.token || '' }}
- name: Attribute failure to this merge
if: failure()
env:
MERGE_SHA: ${{ github.sha }}
MERGE_BEFORE: ${{ github.event.before }}
MERGE_SUBJECT: ${{ github.event.head_commit.message }}
MERGE_AUTHOR: ${{ github.event.head_commit.author.name }}
GATE: test
run: bash .github/report-red-main.sh
full-audit:
name: Full-tree audit → tracking issues
if: github.event_name != 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Check out Homeboy Action
uses: actions/checkout@v6
with:
repository: Extra-Chill/homeboy-action
ref: v2
path: .homeboy-action
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v3
continue-on-error: true
with:
app-id: ${{ secrets.HOMEBOY_APP_ID }}
private-key: ${{ secrets.HOMEBOY_APP_PRIVATE_KEY }}
- uses: ./.homeboy-action
with:
source: .
component: homeboy
commands: review audit
# Full profile enables the whole-tree discovery detectors. No
# --changed-since: this is a deliberate repo-wide sweep.
args: --profile=${{ github.event.inputs.profile || 'full' }}
# File one deduplicated tracking issue per finding kind. The action
# auto-enables this on non-PR events, but set it explicitly so the
# intent of this workflow is unambiguous.
auto-issue: 'true'
# Advisory sweep — never pushes. Findings become the roadmap; fixes
# land through their own reviewed PRs. (There is no `autofix` input on
# homeboy-action@v2, so the `autofix: 'false'` that used to sit here
# was dead configuration asserting a property it did not enforce.)
app-token: ${{ steps.app-token.outputs.token || '' }}