Skip to content

Repo Health

Repo Health #2369

name: Repo Health
on:
pull_request:
branches: [dev]
types: [opened, synchronize, reopened]
# Unprivileged half of Repo Health. The github org blocks pull_request_target,
# so every job runs under `pull_request` with a read-only token and no secrets.
# - A pull_request run uses this file as it is in the PR merge commit. Every
# checkout below is the PR's base commit (`ref: base.sha`), so the scanners and
# checks are base-controlled only when the PR leaves this file unchanged. A PR
# that edits this file controls every job of its own run, including the result
# of the Bootstrap Protection check. The compensating control is code-owner
# review: .github/CODEOWNERS line 4 requests it on every PR (whether dev
# requires it is a branch rule; see .github/REPO_HEALTH_RUNBOOK.md).
# - The PR head is fetched only as Git data for diff/show and is never checked
# out or executed.
# - Bootstrap Protection and Diff Size Guard report as PR checks, as before.
# - Leakage, architectural and security results are uploaded as artifacts.
# squad-repo-health-report.yml (workflow_run, default branch) validates them,
# binds them to this PR head and posts the marker comments. It posts only when
# the run's head commit holds the same copy of this file as its merge base
# with dev (see .github/REPO_HEALTH_RUNBOOK.md).
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
# ─── Bootstrap Protection (BLOCKING) ────────────────────────────────
bootstrap-protection:
name: Bootstrap Protection
runs-on: ubuntu-latest
timeout-minutes: 3
if: github.actor != 'dependabot[bot]'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7
with:
ref: ${{ github.event.pull_request.base.sha }}
sparse-checkout: |
scripts/check-bootstrap-deps.mjs
sparse-checkout-cone-mode: false
- name: Fetch PR head (data only — not executed)
run: git fetch origin ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 #v6
with:
node-version: '22'
- name: Check bootstrap dependencies
id: bootstrap
run: |
set +e
OUTPUT=$(node scripts/check-bootstrap-deps.mjs --ref ${{ github.event.pull_request.head.sha }} 2>&1)
EXIT_CODE=$?
echo "$OUTPUT"
echo "result<<EOF" >> "$GITHUB_OUTPUT"
echo "$OUTPUT" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT"
exit $EXIT_CODE
# ─── Diff Size Guard (WARNING) ─────────────────────────────────────
diff-guard:
name: Diff Size Guard
runs-on: ubuntu-latest
timeout-minutes: 2
if: github.actor != 'dependabot[bot]'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7
with:
ref: ${{ github.event.pull_request.base.sha }}
fetch-depth: 0
- name: Fetch PR head (data only — not executed)
run: git fetch origin ${{ github.event.pull_request.head.sha }}
- name: Check for likely contamination
run: |
git fetch origin dev --quiet
FILE_COUNT=$(git diff --name-only origin/dev...${{ github.event.pull_request.head.sha }} | wc -l)
COMMIT_COUNT=$(git rev-list --count origin/dev..${{ github.event.pull_request.head.sha }})
# Heuristic: if a single-commit PR touches more than 30 files, it's likely contaminated
if [ "$COMMIT_COUNT" -le 2 ] && [ "$FILE_COUNT" -gt 30 ]; then
echo "::warning::⚠️ This PR has $COMMIT_COUNT commit(s) but touches $FILE_COUNT files."
echo "::warning::This may indicate branch contamination from broad staging (--all or .) on a stale branch."
echo "::warning::Please verify all changed files are intentional: git diff --name-only origin/dev...${{ github.event.pull_request.head.sha }}"
else
echo "✅ Diff looks proportional: $COMMIT_COUNT commit(s), $FILE_COUNT file(s)."
fi
# ─── Squad File Leakage (WARNING) ───────────────────────────────────
squad-leakage:
name: Squad File Leakage
runs-on: ubuntu-latest
timeout-minutes: 3
if: github.actor != 'dependabot[bot]'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7
with:
ref: ${{ github.event.pull_request.base.sha }}
fetch-depth: 0
- name: Fetch PR head (data only — not executed)
run: git fetch origin ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 #v6
with:
node-version: '22'
- name: Detect .squad/ leakage
run: |
git fetch origin dev --quiet
mkdir -p "$RUNNER_TEMP/repo-health"
node scripts/check-squad-leakage.mjs origin/dev ${{ github.event.pull_request.head.sha }} > "$RUNNER_TEMP/repo-health/output.txt"
cat "$RUNNER_TEMP/repo-health/output.txt"
# Scanner output is data for squad-repo-health-report.yml, never script source (#1770).
- name: Package result for Repo Health Report
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
jq -n --arg job leakage --argjson pr_number "$PR_NUMBER" --arg head_sha "$HEAD_SHA" \
--rawfile output "$RUNNER_TEMP/repo-health/output.txt" \
'{schema: "squad-repo-health-result/v1", job: $job, pr_number: $pr_number, head_sha: $head_sha, output: $output}' \
> "$RUNNER_TEMP/repo-health/leakage.json"
- name: Upload result for Repo Health Report
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 #v7
with:
name: repo-health-leakage
path: ${{ runner.temp }}/repo-health/leakage.json
if-no-files-found: error
retention-days: 1
overwrite: true
# ─── Architectural Review (INFORMATIONAL) ───────────────────────────
architectural-review:
name: Architectural Review — Structure & Design Rules
runs-on: ubuntu-latest
timeout-minutes: 5
if: github.actor != 'dependabot[bot]'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7
with:
ref: ${{ github.event.pull_request.base.sha }}
fetch-depth: 0
- name: Fetch PR head (data only — not executed)
run: git fetch origin ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 #v6
with:
node-version: '22'
- name: Run architectural review
run: |
git fetch origin dev --quiet
mkdir -p "$RUNNER_TEMP/repo-health"
node scripts/architectural-review.mjs origin/dev ${{ github.event.pull_request.head.sha }} > "$RUNNER_TEMP/repo-health/output.txt"
cat "$RUNNER_TEMP/repo-health/output.txt"
# Scanner output is data for squad-repo-health-report.yml, never script source (#1770).
- name: Package result for Repo Health Report
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
jq -n --arg job architectural --argjson pr_number "$PR_NUMBER" --arg head_sha "$HEAD_SHA" \
--rawfile output "$RUNNER_TEMP/repo-health/output.txt" \
'{schema: "squad-repo-health-result/v1", job: $job, pr_number: $pr_number, head_sha: $head_sha, output: $output}' \
> "$RUNNER_TEMP/repo-health/architectural.json"
- name: Upload result for Repo Health Report
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 #v7
with:
name: repo-health-architectural
path: ${{ runner.temp }}/repo-health/architectural.json
if-no-files-found: error
retention-days: 1
overwrite: true
# ─── Security Review (INFORMATIONAL) ────────────────────────────────
security-review:
name: Security Review — Permissions & Secrets
runs-on: ubuntu-latest
timeout-minutes: 5
if: github.actor != 'dependabot[bot]'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7
with:
ref: ${{ github.event.pull_request.base.sha }}
fetch-depth: 0
- name: Fetch PR head (data only — not executed)
run: git fetch origin ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 #v6
with:
node-version: '22'
- name: Run security review
run: |
git fetch origin dev --quiet
mkdir -p "$RUNNER_TEMP/repo-health"
node scripts/security-review.mjs origin/dev ${{ github.event.pull_request.head.sha }} > "$RUNNER_TEMP/repo-health/output.txt"
cat "$RUNNER_TEMP/repo-health/output.txt"
# Scanner output is data for squad-repo-health-report.yml, never script source (#1770).
- name: Package result for Repo Health Report
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
jq -n --arg job security --argjson pr_number "$PR_NUMBER" --arg head_sha "$HEAD_SHA" \
--rawfile output "$RUNNER_TEMP/repo-health/output.txt" \
'{schema: "squad-repo-health-result/v1", job: $job, pr_number: $pr_number, head_sha: $head_sha, output: $output}' \
> "$RUNNER_TEMP/repo-health/security.json"
- name: Upload result for Repo Health Report
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 #v7
with:
name: repo-health-security
path: ${{ runner.temp }}/repo-health/security.json
if-no-files-found: error
retention-days: 1
overwrite: true