|
| 1 | +# Fleet operations — waves, pilots, and what the audit cannot see |
| 2 | + |
| 3 | +How kit changes actually reach the fleet, and the traps that have bitten. The **authoritative |
| 4 | +release sequence is [`README.md`](../README.md) → *Release + repin order*** — this document does not |
| 5 | +restate it. What lives here is the operational knowledge around it: how a wave is executed, what a |
| 6 | +pilot can and cannot prove, and where the drift detector is blind. |
| 7 | + |
| 8 | +Written 2026-08-02 from the v1.7.0 → v1.11.0 waves. |
| 9 | + |
| 10 | +--- |
| 11 | + |
| 12 | +## The fleet |
| 13 | + |
| 14 | +**21 repo@branch pairs**, and the split matters because two different numbers are correct depending |
| 15 | +on the question: |
| 16 | + |
| 17 | +| Set | Size | What it is | |
| 18 | +|---|---|---| |
| 19 | +| **Repin-wave targets** | **21** | Every pair carrying any kit caller stub. What `tools/fleet-pin-audit.sh` enumerates, and what a pin-only wave must cover — miss one and `--stale` never reads clean. | |
| 20 | +| **Full-kit targets** | **18** | Pairs carrying `claude.yml` *and* `bonsai-status-sync.yml`. Verified branch-by-branch across all 618 org branches: zero rows where one is present without the other, so a wave touching one can touch both. | |
| 21 | +| **Difference** | **3** | `Team-Laird@develop`, `The-Gathery@develop`, `driver-bonsai-mcp@main` — stub rails only, neither full workflow. They still need the pin repin. | |
| 22 | + |
| 23 | +Palmers contributes **8** of the 18 (one per country branch: `main`, `-au`, `-ca`, `-in`, `-ma`, |
| 24 | +`-me`, `-sa`, `-uk`); the other 10 are single-branch repos including Avara. |
| 25 | + |
| 26 | +**Avara is the only provisioned store repo** — the only pair carrying `shopify-tool-smoke.yml`, and |
| 27 | +the only one whose `claude.yml` has a non-empty `SHOPIFY_STORE_NAME`. |
| 28 | + |
| 29 | +--- |
| 30 | + |
| 31 | +## Waves are direct pushes, not PRs |
| 32 | + |
| 33 | +Decided at the v1.7.0 wave (2026-07-31) and used for every wave since. A mechanical, |
| 34 | +centrally-reviewed kit change is pushed **straight to each branch with `[skip ci]` in the commit |
| 35 | +message**, rather than opening 21 PRs. |
| 36 | + |
| 37 | +Why: |
| 38 | + |
| 39 | +- **Zero review runs.** 21 PRs would each fire `pr-first-review` and burn quota on a change that was |
| 40 | + already reviewed centrally. |
| 41 | +- **Zero theme deploys.** Recon found `develop`/`staging` deploy workflows on ~10 fleet branches that |
| 42 | + a bare push *would* have fired. `[skip ci]` suppresses them. |
| 43 | +- Branch protection does not enforce for admins (`enforce_admins: false` fleet-wide), so the push |
| 44 | + lands as Maria without a review round-trip. |
| 45 | + |
| 46 | +**Reserve PR waves for changes that genuinely want per-repo review.** A kit change that is |
| 47 | +byte-identical everywhere does not. |
| 48 | + |
| 49 | +`[skip ci]` suppresses workflow triggers but **not** GitHub's own "Dependabot Updates" scheduler — |
| 50 | +seeing one of those fire after a wave is expected and benign. |
| 51 | + |
| 52 | +### Execution shape |
| 53 | + |
| 54 | +One atomic commit per branch via the Git Data API (blobs → tree → commit → ref patch), not one |
| 55 | +commit per file. Per target: |
| 56 | + |
| 57 | +1. `claude.yml` ← kit version, with the repo's own `SHOPIFY_STORE_NAME` restored. |
| 58 | +2. `bonsai-status-sync.yml` ← kit stub, **whole-file replacement**. |
| 59 | +3. The other five stubs ← **sed the pin line only**, so any per-repo edit survives. |
| 60 | +4. `shopify-tool-smoke.yml` (Avara only) ← kit version, store handle restored. |
| 61 | +5. `actionlint` every file about to be written, then commit `[skip ci]` and patch the ref. |
| 62 | + |
| 63 | +Guards worth keeping in any wave script: assert no destination path is written twice, assert the |
| 64 | +store handle survived, assert no stale pin remains, and dry-run the whole fleet before writing |
| 65 | +anything. |
| 66 | + |
| 67 | +--- |
| 68 | + |
| 69 | +## Three traps |
| 70 | + |
| 71 | +**1. Same basename in both halves of the diff.** When a full workflow becomes a stub, the kit diff |
| 72 | +carries `templates/github/<name>.yml` *and* `.github/workflows/<name>.yml`. The wave rewrites |
| 73 | +`templates/github/` → `.github/workflows/`, so both collapse onto one destination. Apply them |
| 74 | +blindly and the *reusable* can land in a client repo **as** the workflow — where it is |
| 75 | +`workflow_call`-only, fires on nothing, and looks green. Assert no destination is touched twice. |
| 76 | + |
| 77 | +**2. Pin hunks patch from a base the fleet was never on.** At v1.11.0 the kit diff patched from |
| 78 | +`80c35fe` (v1.8.0) while every deployed stub held `a54c91e` (v1.9.0) — because v1.9.0 shipped |
| 79 | +without a kit repin commit even though the wave repinned the fleet. No kit revision had *ever* |
| 80 | +carried `a54c91e` in a pin line, so no diff base produced a matching `-` line and `git apply` would |
| 81 | +have rejected all five files on target #1. **Sed the pin; don't patch it.** |
| 82 | + |
| 83 | +**3. Per-repo state that must survive.** `SHOPIFY_STORE_NAME` in `claude.yml` and |
| 84 | +`shopify-tool-smoke.yml`, and any Dependabot-bumped action pins. Surveyed at v1.11.0: the fleet's |
| 85 | +`claude.yml` copies were byte-identical to the kit except Avara's store handle, and there was no |
| 86 | +Dependabot drift — but survey, don't assume. |
| 87 | + |
| 88 | +--- |
| 89 | + |
| 90 | +## What the pin audit cannot see |
| 91 | + |
| 92 | +`tools/fleet-pin-audit.sh` greps only |
| 93 | +`DriverDigital/workflows/.github/workflows/<name>@<sha>` and compares the SHA to the latest tag. |
| 94 | +Three consequences: |
| 95 | + |
| 96 | +- **A file with no `uses:` line is invisible.** An unconverted 190-line copy has none, so the audit |
| 97 | + cannot tell a repo that was skipped by a stub conversion from one that never carried the file. |
| 98 | +- **Content is never compared.** `DRIVER_AGENTS_REF` is a raw SHA in an `env:` block, and the |
| 99 | + system-prompt text is just text. A fleet running kit content from no tag reports clean. |
| 100 | +- **The reference itself can drift.** The audit compares against the latest *tag*, never against |
| 101 | + `templates/`. When those disagree the audit reports uniform while real drift sits in the source of |
| 102 | + truth — which is exactly how the v1.9.0 gap went unnoticed for a day. |
| 103 | + |
| 104 | +This is why the release order requires the tag to contain what gets waved, and why a |
| 105 | +`DRIVER_AGENTS_REF` bump must re-run the canonical parity check by hand. |
| 106 | + |
| 107 | +--- |
| 108 | + |
| 109 | +## Piloting a cross-repo reusable |
| 110 | + |
| 111 | +The v1.11.0 pilot proved `vars.BONSAI_URL` resolves against the **caller**, so a per-repo tunnel |
| 112 | +override still works after conversion. Two things made it harder than expected, both worth knowing |
| 113 | +before designing the next one. |
| 114 | + |
| 115 | +**The `issues` leg is not pilotable.** `bonsai-status-sync`'s issues gate greps the issue body for |
| 116 | +`@claude`, and `claude.yml`'s issues gate does the same — deliberately mirrored. Any issue that |
| 117 | +trips the status flip also wakes a real implementer run on a client repo. Use the PR leg. |
| 118 | + |
| 119 | +**`closingIssuesReferences` only populates for PRs targeting the default branch.** A PR into a |
| 120 | +scratch base dodges the theme-deploy workflows (they filter on `branches: [staging, dev-staging]`) |
| 121 | +but resolves `uuid=<none>`, so the run never reaches the `curl` and passes green having tested |
| 122 | +nothing. If the assertion needs the network call, the PR must target the default branch. |
| 123 | + |
| 124 | +**Split the legs by what each can actually prove.** Leg 1 on a private consumer |
| 125 | +(`foundrae-blackridge@staging`) proves a private repo resolves the public cross-repo reusable and |
| 126 | +reads the caller's event payload — that is the visibility question. Variable resolution is |
| 127 | +repo-agnostic, so leg 2 belongs wherever it is cheapest: `vite-plugin-shopify-clean` is public, |
| 128 | +single-branch, and has no Shopify store attached, so nothing but node tests fire. |
| 129 | + |
| 130 | +**Assert on the log line, not the colour.** Setting `BONSAI_URL` to a bogus host and checking for a |
| 131 | +red run is not sufficient — a wrong-way resolution falls back to the hardcoded default and *also* |
| 132 | +fails. The discriminator is which host the log names: |
| 133 | + |
| 134 | +``` |
| 135 | +BONSAI_URL: https://pilot-bogus-host.invalid |
| 136 | +curl: (6) Could not resolve host: pilot-bogus-host.invalid |
| 137 | +``` |
| 138 | + |
| 139 | +Clean up afterwards: delete the variable, close the issue and PR, delete the scratch branches. Leave |
| 140 | +the installed stub — the wave covers it anyway. |
| 141 | + |
| 142 | +--- |
| 143 | + |
| 144 | +## Branch protection |
| 145 | + |
| 146 | +`enforce_admins` is `false` fleet-wide, which is what makes direct-push waves work. Two live kit |
| 147 | +branches have **no protection at all** — `studio-sulzer@main` and `Team-Laird@develop` (404 on the |
| 148 | +protection endpoint). Every other kit branch is protected. The kit's onboarding steps assume a |
| 149 | +human-approver rule exists, so on those two a bot signal alone could satisfy a merge. |
| 150 | + |
| 151 | +On this repo, `main` has `required_status_checks` with `strict: true` but empty `contexts` — so |
| 152 | +`lint.yml` reports red without being able to block. The context string to add is **`actionlint`** |
| 153 | +(the job id at `.github/workflows/lint.yml:28`; the workflow-level `name:` is not part of it). |
0 commit comments