Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,15 @@ Concrete forms, cheapest first:
(lint + typecheck + test) that CI calls verbatim — the check bundle is defined
once, run identically locally and on the shared branch. Green `verify` locally ⇒
green CI. Run it before declaring any change done. (Golden templates:
`dotfiles/templates/ci/`.)
`fleet/templates/ci/`.)

**Check `dotfiles/SHARED.md` before building anything cross-cutting** — AI
**Check `fleet/SHARED.md` before building anything cross-cutting** — AI
calls, form fill, rate limiting, email, logging, health routes, CI sweeps. If a
package there already owns it, install it; do not write a second one. Measured
2026-08-16: `auto-merge-sweep.sh` exists in 22 repos in 8 different versions,
and rate limiting has 14 implementations (orangecat alone has 4, and its ADR to
unify them has been "Proposed" since January while the count doubled). The
duplication count is a ratchet — `scripts/ci/shared-inventory.sh --check` — and
duplication count is a ratchet — `fleet: scripts/ci/shared-inventory.sh --check` — and
it may fall or hold but never rise. If a copy really is justified, raise the
baseline in the same PR so a human sees the decision instead of inheriting it.

Expand Down
99 changes: 27 additions & 72 deletions .github/workflows/auto-merge-sweep.yml
Original file line number Diff line number Diff line change
@@ -1,101 +1,56 @@
# The auto-merge sweep, once, for the whole fleet.
# FORWARDING SHIM — the sweep lives in bitbaum/fleet now (moved 2026-08-28).
#
# Reusable (`workflow_call`). A repo adopts it with ~10 lines instead of a
# ~400-line copy that will drift — and 22 copies did drift, into 8 versions,
# each repo's outage fix reaching only itself. See scripts/ci/auto-merge-sweep.sh.
#
# The caller keeps its own triggers (workflow_run / schedule) because those are
# genuinely per-repo: the CI workflow's NAME differs, and `workflow_run` must
# name it exactly.
name: Auto-merge sweep (reusable)
# Sixteen repos call this file as a reusable workflow. This shim keeps every
# one of them working unchanged while they migrate to
# uses: bitbaum/fleet/.github/workflows/auto-merge-sweep.yml@main
# at their leisure. NO LOGIC LIVES HERE — logic in a shim is drift, which is
# the disease this whole arrangement exists to cure.

name: Auto-merge sweep (moved to bitbaum/fleet)

on:
workflow_call:
inputs:
base_branch:
description: 'Branch PRs merge into (main for most, master for some).'
type: string
default: main
ci_workflow:
description: 'CI workflow file whose green run gates a merge.'
type: string
default: ci.yml
rearm_workflows:
description: >
SPACE-separated workflows to dispatch after a merge, e.g.
"ci.yml deploy.yml". The script word-splits this value, so a
comma-separated list becomes ONE bogus token, every dispatch fails,
and the only symptom is that nothing deploys.

Load-bearing: a push made with GITHUB_TOKEN triggers NO workflows, so
without a working re-arm, merges land and silently never ship — and
the sweep still exits 0, so the automation looks healthy throughout.
type: string
default: ''
deploy_workflow:
description: >
Workflow that ships the base branch, e.g. deploy.yml. EMPTY DISABLES
the reconciler, and empty is the default.

Set it when merges must reach production: a push made with
GITHUB_TOKEN emits no workflow_run event — and neither does a run that
GITHUB_TOKEN dispatched — so nothing downstream wakes on an automated
merge. Rather than trust a trigger, the sweep compares the base tip
against the last successful deploy and closes the gap, so a deploy
that never fired or fired and failed is retried next sweep instead of
leaving a commit merged-but-not-live.
type: string
default: ''
max_run_attempts:
description: 'How many times to re-run a run that produced no verdict.'
type: string
default: '3'
ref:
description: 'Pin the sweep version (tag/sha of catomean/dotfiles).'
description: >
Pin the sweep version. Historic callers pass tags/shas of
bitbaum/dotfiles; the default 'master' (dotfiles' branch) is mapped
to fleet's 'main' below. A pinned sha of dotfiles will NOT resolve
in fleet — repin against bitbaum/fleet when migrating.
type: string
default: master
secrets:
token:
description: >
Token for merging. GITHUB_TOKEN works for most repos but CANNOT merge
a PR that touches .github/workflows — pass a PAT where that matters.
required: false

permissions:
contents: write # merge the PR
pull-requests: write # read PR state, delete the branch
actions: write # dispatch the re-arm workflows
# Only load-bearing on a PRIVATE repo, which is exactly why it went missing
# for so long: the sweep reads statusCheckRollup to decide whether a PR is
# green, a public repo answers that with no explicit scope, and the copied
# file worked in ~20 public repos. The first private repo to run it
# (ivy-portal) failed every sweep with "Resource not accessible by
# integration" and merged nothing, ever — indistinguishable from having
# nothing to merge.
contents: write
pull-requests: write
actions: write
checks: read
statuses: read

jobs:
sweep:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
# Check out the SWEEP, not the caller. The script talks to the caller repo
# entirely through the API (GH_REPO), so it never needs that checkout —
# and not taking one keeps a repo's own contents from influencing how its
# PRs get merged.
- uses: actions/checkout@v7
with:
repository: catomean/dotfiles
ref: ${{ inputs.ref }}

- name: Sweep
env:
GH_TOKEN: ${{ secrets.token || github.token }}
GH_REPO: ${{ github.repository }}
BASE_BRANCH: ${{ inputs.base_branch }}
CI_WORKFLOW: ${{ inputs.ci_workflow }}
REARM_WORKFLOWS: ${{ inputs.rearm_workflows }}
DEPLOY_WORKFLOW: ${{ inputs.deploy_workflow }}
MAX_RUN_ATTEMPTS: ${{ inputs.max_run_attempts }}
run: bash scripts/ci/auto-merge-sweep.sh
forward:
uses: bitbaum/fleet/.github/workflows/auto-merge-sweep.yml@main
with:
base_branch: ${{ inputs.base_branch }}
ci_workflow: ${{ inputs.ci_workflow }}
rearm_workflows: ${{ inputs.rearm_workflows }}
deploy_workflow: ${{ inputs.deploy_workflow }}
max_run_attempts: ${{ inputs.max_run_attempts }}
ref: ${{ inputs.ref == 'master' && 'main' || inputs.ref }}
secrets:
token: ${{ secrets.token }}
28 changes: 12 additions & 16 deletions .github/workflows/auto-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,16 @@ concurrency:
cancel-in-progress: false

jobs:
# The sweep script moved to bitbaum/fleet with the rest of the fleet
# automation (2026-08-28); this repo calls it like every other repo does.
sweep:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

- name: Merge every green, ready PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
BASE_BRANCH: ${{ github.event.repository.default_branch }}
CI_WORKFLOW: ci.yml
# Everything that runs on push and therefore would NOT fire after a
# merge made with the default GITHUB_TOKEN. Keep this in sync when a
# push-triggered workflow is added.
REARM_WORKFLOWS: ci.yml
run: bash scripts/ci/auto-merge-sweep.sh
uses: bitbaum/fleet/.github/workflows/auto-merge-sweep.yml@main
with:
base_branch: master
ci_workflow: ci.yml
# Everything that runs on push and therefore would NOT fire after a
# merge made with the default GITHUB_TOKEN. Keep this in sync when a
# push-triggered workflow is added.
rearm_workflows: ci.yml
secrets:
token: ${{ secrets.GITHUB_TOKEN }}
108 changes: 13 additions & 95 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# The template repo defends every other repo's branch. Until now it defended
# none of its own — no .github/ at all — and it drifted: it still handed out
# actions/checkout@v4 and setup-node@v4 sixteen days after the fleet moved to
# v7, so every fresh copy re-introduced the old versions. A source of truth
# that nothing checks is just a file.
# The environment repo's own CI. The fleet-automation test suites moved to
# bitbaum/fleet with the scripts they test (2026-08-28); what remains here is
# the environment, so what CI checks is the environment.
name: CI

on:
Expand All @@ -19,7 +17,7 @@ concurrency:
jobs:
verify:
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 5
steps:
- uses: actions/checkout@v7

Expand All @@ -35,94 +33,14 @@ jobs:
done < <(find . -name '*.sh' -not -path './.git/*')
echo "shell syntax: ok ($found script(s) checked)"

# Every CI template must be valid YAML — a broken template is copied
# into a repo and fails there, far from here.
- name: Templates are valid YAML
# Every symlink install.sh would create must point at a file that exists
# in this repo — a dangling link is a broken shell on a new machine.
- name: install.sh targets exist
run: |
set -euo pipefail
found=0
for f in templates/ci/*.yml; do
[ -e "$f" ] || continue
found=$((found + 1))
python3 -c "import yaml,sys; yaml.safe_load(open(sys.argv[1]))" "$f"
done
[ "$found" -gt 0 ] || { echo "no CI templates found — did they move?" >&2; exit 1; }
echo "templates parse: ok ($found template(s))"

# The template's whole premise is that "verified" is defined ONCE, in
# package.json `verify`, and CI calls it verbatim. If a template stops
# doing that, the premise is gone and nobody would notice.
- name: Templates still call the verify SSOT
run: |
set -euo pipefail
for f in templates/ci/ci-npm.yml templates/ci/ci-pnpm.yml; do
grep -q 'run verify' "$f" || {
echo "$f no longer calls the verify SSOT — the template's core promise" >&2
exit 1
}
done
echo "verify SSOT: present in both templates"

# The fleet audit enforces the template's premise across every repo, so it
# is the last thing that may quietly stop working. Its wiring rules are
# tested BOTH ways: that each still bites against a violating fixture, and
# that conforming shapes are not flagged — a checker that cries wolf gets
# ignored, which is the same end state as no checker.
#
# The rules sit in verify-predicates.sh so they are testable at all: the
# audit is remote-only by design, and a rule that can only be exercised by
# a live API call is a rule nobody re-tests after editing its regex.
- name: The verify-contract rules can still go red
run: bash scripts/ci/test-verify-predicates.sh

# The duplication ratchet's entire value is that it CAN go red. A ratchet
# that silently passes while duplication rises certifies the thing it was
# built to stop. Its counting half needs the API; its deciding half is
# pure text and is tested here against fixtures, with no network.
- name: The duplication ratchet can still go red
run: bash scripts/ci/test-shared-inventory.sh

# The stranded-work guard replaces a check that failed by being ignorable:
# git-health reported orangecat's 118 dirty files every day for ten days
# and changed nothing. This one keys on AGE and stays silent when healthy,
# so both failure modes are gates — it must go red on aged work, and it
# must stay quiet on a fresh tree, or it gets muted and is then absent.
- name: The stranded-work guard can go red, and stays quiet when it should
run: bash scripts/fleet/test-stranded-work.sh

# This one DELETES CHECKOUTS, so its only interesting failure is a false
# positive. Every refusal in the predicate is a case that would otherwise
# have destroyed the single existing copy of some work, and each is pinned
# separately — a combined "unsafe" fixture is exactly what hides one guard
# silently inverting.
- name: The worktree GC refuses everything it should
run: bash scripts/fleet/test-gc-merged-worktrees.sh

# The sweep decides what ships in every repo that calls it, so it is the
# last script here that should be untested — and until now it was. These
# run the REAL script against a fake `gh`, exercising shipped control flow
# rather than a description of it. Ported from evig, the only repo that
# had them, when its copy of the script was centralised.
- name: The auto-merge sweep behaves
run: bash scripts/ci/test-auto-merge-sweep.sh

# This detector reports on repos nobody is watching, so its own failure
# mode is silence: a clean report from a broken audit is worse than no
# audit, because it prints a ✓. Both sides are pinned — the real AOZ
# regression is still caught, corrected code stays quiet — plus the two
# false positives the first live run produced (xAI's `grok-3-mini` filed
# under Groq, and a computed `${...}` id read as a pin). No network, no
# key, no checkout, so it runs here as well as in the daily sweep.
- name: The model-pin audit still detects, and still stays quiet
run: node scripts/ci/test-model-pin-audit.mjs

# Drift guard. The fleet is on v7; templates handing out v4 is exactly
# how this repo fell behind the repos it governs.
- name: No stale action versions
run: |
set -euo pipefail
if grep -rnE 'actions/(checkout|setup-node)@v[1-6]\b' templates/; then
echo "stale action version in a template — the fleet is on v7" >&2
exit 1
fi
echo "action versions: no stale pins"
missing=0
while IFS= read -r target; do
[ -e "$target" ] || { echo "install.sh links '$target', which does not exist" >&2; missing=1; }
done < <(grep -oP '^\s*link\s+\K\S+' install.sh || true)
[ "$missing" -eq 0 ]
echo "install targets: ok"
Loading