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
63 changes: 54 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,53 @@ rail) → `v1.5.1` (drop the `gh`-based author re-check that skipped every real
(`allowed_bots: claude[bot]`, so the bot-opened round 1 actually reviews) → `v1.5.3` (always-latest
resilient Claude Code self-install in the three agent reusables) → `v1.5.4` (`dependabot-validate`:
npm-install fallback for lockfile-less repos + `actions/checkout` v7) → `v1.5.5` (claude-code-action
1.0.161 → 1.0.168 in the agent reusables) → `v1.6.0` → `v1.7.0` → **`v1.8.0`** (all below).
`v1.3.0` was never tagged.
1.0.161 → 1.0.168 in the agent reusables) → `v1.6.0` → `v1.7.0` → `v1.8.0` → `v1.9.0` → `v1.10.0` →
**`v1.11.0`** (all below). `v1.3.0` was never tagged.

### `v1.11.0` (`90f0d06`, 2026-08-02)

Waved to all 21 repin targets on 2026-08-02; fleet uniform, 108 pins, zero stale.

- **`bonsai-status-sync.yml` conversion completed.** The 190-line per-repo copy became a 67-line
caller stub — the status machine, actor gate, linkage logic and cascade caveat now live in one
central file. The reusable itself landed one tag earlier (see `v1.10.0`), so the conversion spans
the two tags: a new reusable's stub cannot be pinned until the tag containing it exists.
- **kit `claude.yml` + `shopify-tool-smoke.yml`:** `DRIVER_AGENTS_REF` → `4d63371`. The previous pin
`0bbb125` predated `graphql_guard.py`, so every fleet runner executed `admin-graphql.sh` with no
fail-closed allowlist and the Driver Engineering scope grant was the only control on destructive
mutations.
- **Shopify operator tripwire** appended to `claude.yml`'s static `--append-system-prompt`, pairing
with that wrapper. The blockquote is copied verbatim from driver-agents
`docs/agent-instructions-shopify.md` (canonical — edit there first); a non-canonical kit-side
lead-in precedes it, un-scoping the block from the conduct rules above and telling the model how to
report a trip on a rail that cannot set a job exit code.
- **this repo's own CI:** `lint.yml` gained a tokenization guard asserting `claude_args` holds
exactly four single quotes and the system prompt contains no apostrophe or `$`. One apostrophe
typed into canonical upstream silently *truncates* the prompt — `shell-quote` does not throw, every
flag still parses, and the wave would copy the truncated prompt fleet-wide green.
- **Piloted before the wave:** `vars.BONSAI_URL` proven to resolve against the caller, so the
per-repo tunnel override survives the conversion. See
[`docs/fleet-operations.md`](docs/fleet-operations.md).

### `v1.10.0` (`b394c6d`, 2026-08-02)

- **New sixth reusable: `.github/workflows/bonsai-status-sync.yml`.** Its `jobs:` body is
byte-identical to the old per-repo copy except one added comment. Deliberately shipped without its
caller stub — see `v1.11.0`.
- **`lint.yml` placeholder-pin guard:** fails the build on any kit stub still carrying an all-zero
pin, so a stub that cannot resolve can never reach `main`.
- Never waved on its own; superseded by `v1.11.0` two commits later.

### `v1.9.0` (`a54c91e`, 2026-08-01, kit-only)

- **Store app secrets renamed** `DRIVER_AGENTS_SCOPES_CLIENT_ID/_SECRET` →
`DRIVER_ENGINEERING_APP_CLIENT_ID/_SECRET`, tied to the per-org "Driver Engineering" app that
replaced "Driver Agents Scopes" (retired 2026-08-01). Waved to all 21 targets; Avara's smoke test
green on the new names, old-name secrets deleted.
- **Reusables unchanged.** Note this release **never got its kit repin commit** — `templates/` sat at
`v1.8.0`'s SHA while the deployed fleet was waved to `a54c91e`, leaving the fleet a release *ahead*
of the kit templates until `v1.11.0` closed it. That gap is invisible to
`tools/fleet-pin-audit.sh`; see [`docs/fleet-operations.md`](docs/fleet-operations.md).

### `v1.8.0` (2026-07-31, kit-only)

Expand Down Expand Up @@ -140,13 +185,13 @@ npm-install fallback for lockfile-less repos + `actions/checkout` v7) → `v1.5.
wave script rewrites `templates/github/` → `.github/workflows/`, so assert the rewritten diff
touches no destination path twice before applying — otherwise the reusable can land in a client
repo *as* the workflow, where it is `workflow_call`-only, fires on nothing, and looks green.
- **This wave only — the five pre-existing stubs' pin hunks will not apply.** They patch from
`80c35fe` (`v1.8.0`), but every deployed stub holds `a54c91e` (`v1.9.0`): `v1.9.0` shipped without
a kit repin commit while the 2026-08-01 wave repinned the fleet anyway, so **no kit revision has
ever carried `a54c91e` in a pin line** and no diff base produces a matching `-` line. `git apply`
rejects all five on target #1. Apply the kit diff restricted to
`templates/github/bonsai-status-sync.yml` (a whole-file replacement) and let the wave script's
existing `sed` repin handle the other five pin lines.
- **Sed pin lines; never `git apply` them.** A pin hunk patches from whatever SHA the kit held,
which is not necessarily what the fleet holds — at v1.11.0 the kit diff patched from `80c35fe`
(v1.8.0) while every deployed stub held `a54c91e` (v1.9.0), a SHA no kit revision had ever
carried in a pin line, so no diff base produced a matching `-` line and `git apply` would have
rejected all five files on target #1.
- Wave mechanics, the guards worth keeping, and what the pin audit cannot see:
[`docs/fleet-operations.md`](docs/fleet-operations.md).

**Template pins are manual.** `.github/dependabot.yml` uses `directory: "/"`, which only scans
`.github/workflows/` — nothing will ever bump an action pin inside `templates/`. Check
Expand Down
153 changes: 153 additions & 0 deletions docs/fleet-operations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Fleet operations — waves, pilots, and what the audit cannot see

How kit changes actually reach the fleet, and the traps that have bitten. The **authoritative
release sequence is [`README.md`](../README.md) → *Release + repin order*** — this document does not
restate it. What lives here is the operational knowledge around it: how a wave is executed, what a
pilot can and cannot prove, and where the drift detector is blind.

Written 2026-08-02 from the v1.7.0 → v1.11.0 waves.

---

## The fleet

**21 repo@branch pairs**, and the split matters because two different numbers are correct depending
on the question:

| Set | Size | What it is |
|---|---|---|
| **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. |
| **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. |
| **Difference** | **3** | `Team-Laird@develop`, `The-Gathery@develop`, `driver-bonsai-mcp@main` — stub rails only, neither full workflow. They still need the pin repin. |

Palmers contributes **8** of the 18 (one per country branch: `main`, `-au`, `-ca`, `-in`, `-ma`,
`-me`, `-sa`, `-uk`); the other 10 are single-branch repos including Avara.

**Avara is the only provisioned store repo** — the only pair carrying `shopify-tool-smoke.yml`, and
the only one whose `claude.yml` has a non-empty `SHOPIFY_STORE_NAME`.

---

## Waves are direct pushes, not PRs

Decided at the v1.7.0 wave (2026-07-31) and used for every wave since. A mechanical,
centrally-reviewed kit change is pushed **straight to each branch with `[skip ci]` in the commit
message**, rather than opening 21 PRs.

Why:

- **Zero review runs.** 21 PRs would each fire `pr-first-review` and burn quota on a change that was
already reviewed centrally.
- **Zero theme deploys.** Recon found `develop`/`staging` deploy workflows on ~10 fleet branches that
a bare push *would* have fired. `[skip ci]` suppresses them.
- Branch protection does not enforce for admins (`enforce_admins: false` fleet-wide), so the push
lands as Maria without a review round-trip.

**Reserve PR waves for changes that genuinely want per-repo review.** A kit change that is
byte-identical everywhere does not.

`[skip ci]` suppresses workflow triggers but **not** GitHub's own "Dependabot Updates" scheduler —
seeing one of those fire after a wave is expected and benign.

### Execution shape

One atomic commit per branch via the Git Data API (blobs → tree → commit → ref patch), not one
commit per file. Per target:

1. `claude.yml` ← kit version, with the repo's own `SHOPIFY_STORE_NAME` restored.
2. `bonsai-status-sync.yml` ← kit stub, **whole-file replacement**.
3. The other five stubs ← **sed the pin line only**, so any per-repo edit survives.
4. `shopify-tool-smoke.yml` (Avara only) ← kit version, store handle restored.
5. `actionlint` every file about to be written, then commit `[skip ci]` and patch the ref.

Guards worth keeping in any wave script: assert no destination path is written twice, assert the
store handle survived, assert no stale pin remains, and dry-run the whole fleet before writing
anything.

---

## Three traps

**1. Same basename in both halves of the diff.** When a full workflow becomes a stub, the kit diff
carries `templates/github/<name>.yml` *and* `.github/workflows/<name>.yml`. The wave rewrites
`templates/github/` → `.github/workflows/`, so both collapse onto one destination. Apply them
blindly and the *reusable* can land in a client repo **as** the workflow — where it is
`workflow_call`-only, fires on nothing, and looks green. Assert no destination is touched twice.

**2. Pin hunks patch from a base the fleet was never on.** At v1.11.0 the kit diff patched from
`80c35fe` (v1.8.0) while every deployed stub held `a54c91e` (v1.9.0) — because v1.9.0 shipped
without a kit repin commit even though the wave repinned the fleet. No kit revision had *ever*
carried `a54c91e` in a pin line, so no diff base produced a matching `-` line and `git apply` would
have rejected all five files on target #1. **Sed the pin; don't patch it.**

**3. Per-repo state that must survive.** `SHOPIFY_STORE_NAME` in `claude.yml` and
`shopify-tool-smoke.yml`, and any Dependabot-bumped action pins. Surveyed at v1.11.0: the fleet's
`claude.yml` copies were byte-identical to the kit except Avara's store handle, and there was no
Dependabot drift — but survey, don't assume.

---

## What the pin audit cannot see

`tools/fleet-pin-audit.sh` greps only
`DriverDigital/workflows/.github/workflows/<name>@<sha>` and compares the SHA to the latest tag.
Three consequences:

- **A file with no `uses:` line is invisible.** An unconverted 190-line copy has none, so the audit
cannot tell a repo that was skipped by a stub conversion from one that never carried the file.
- **Content is never compared.** `DRIVER_AGENTS_REF` is a raw SHA in an `env:` block, and the
system-prompt text is just text. A fleet running kit content from no tag reports clean.
- **The reference itself can drift.** The audit compares against the latest *tag*, never against
`templates/`. When those disagree the audit reports uniform while real drift sits in the source of
truth — which is exactly how the v1.9.0 gap went unnoticed for a day.

This is why the release order requires the tag to contain what gets waved, and why a
`DRIVER_AGENTS_REF` bump must re-run the canonical parity check by hand.

---

## Piloting a cross-repo reusable

The v1.11.0 pilot proved `vars.BONSAI_URL` resolves against the **caller**, so a per-repo tunnel
override still works after conversion. Two things made it harder than expected, both worth knowing
before designing the next one.

**The `issues` leg is not pilotable.** `bonsai-status-sync`'s issues gate greps the issue body for
`@claude`, and `claude.yml`'s issues gate does the same — deliberately mirrored. Any issue that
trips the status flip also wakes a real implementer run on a client repo. Use the PR leg.

**`closingIssuesReferences` only populates for PRs targeting the default branch.** A PR into a
scratch base dodges the theme-deploy workflows (they filter on `branches: [staging, dev-staging]`)
but resolves `uuid=<none>`, so the run never reaches the `curl` and passes green having tested
nothing. If the assertion needs the network call, the PR must target the default branch.

**Split the legs by what each can actually prove.** Leg 1 on a private consumer
(`foundrae-blackridge@staging`) proves a private repo resolves the public cross-repo reusable and
reads the caller's event payload — that is the visibility question. Variable resolution is
repo-agnostic, so leg 2 belongs wherever it is cheapest: `vite-plugin-shopify-clean` is public,
single-branch, and has no Shopify store attached, so nothing but node tests fire.

**Assert on the log line, not the colour.** Setting `BONSAI_URL` to a bogus host and checking for a
red run is not sufficient — a wrong-way resolution falls back to the hardcoded default and *also*
fails. The discriminator is which host the log names:

```
BONSAI_URL: https://pilot-bogus-host.invalid
curl: (6) Could not resolve host: pilot-bogus-host.invalid
```

Clean up afterwards: delete the variable, close the issue and PR, delete the scratch branches. Leave
the installed stub — the wave covers it anyway.

---

## Branch protection

`enforce_admins` is `false` fleet-wide, which is what makes direct-push waves work. Two live kit
branches have **no protection at all** — `studio-sulzer@main` and `Team-Laird@develop` (404 on the
protection endpoint). Every other kit branch is protected. The kit's onboarding steps assume a
human-approver rule exists, so on those two a bot signal alone could satisfy a merge.

On this repo, `main` has `required_status_checks` with `strict: true` but empty `contexts` — so
`lint.yml` reports red without being able to block. The context string to add is **`actionlint`**
(the job id at `.github/workflows/lint.yml:28`; the workflow-level `name:` is not part of it).
23 changes: 19 additions & 4 deletions docs/reusable-conversion-scope.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Scope: convert `claude.yml` + `bonsai-status-sync.yml` into reusable workflows

**Status:** `bonsai-status-sync` half IN PROGRESS (Phase 1 done); `claude.yml` half TABLED. **Written:** 2026-07-31 against `main` @ `9b70acf` (tag `v1.6.0` = `0a3934f`).
**Status:** `bonsai-status-sync` half **COMPLETE** (shipped `v1.11.0`, waved 2026-08-02); `claude.yml` half TABLED. **Written:** 2026-07-31 against `main` @ `9b70acf` (tag `v1.6.0` = `0a3934f`).
**Refreshed:** 2026-08-02 against `main` @ `a54c91e` (tag `v1.9.0`). Three releases landed underneath the
original draft — v1.7.0 (Slack alerting), v1.8.0 (audit context + artifact leg), v1.9.0 (store-secret rename).
Every `file:line` citation below was re-verified again on 2026-08-02 against the **v1.11.0** release branch
Expand Down Expand Up @@ -345,10 +345,10 @@ so in the PR body. Merge on review of the diff alone; validate after merge.
| Phase | Work | Est. | Status |
|---|---|---|---|
| 1 | Convert `bonsai-status-sync.yml` + stub + docs + lint | 3h | **done 2026-08-02** |
| 2 | Pilot it (only the `pull_request` leg is testable pre-merge — see *Sequencing*) | 2h | **next** |
| 3 | Fleet wave for `bonsai-status-sync` — 18 repo@branch pairs across 11 repos | 3–4h | after Phase 2 |
| 2 | Pilot it (only the `pull_request` leg is testable pre-merge — see *Sequencing*) | 2h | **done 2026-08-02** — passed |
| 3 | Fleet wave for `bonsai-status-sync` — 18 repo@branch pairs across 11 repos | 3–4h | **done 2026-08-02** — waved 21 targets |
| 5 | Tag + repin the kit stubs (README's mandatory 3-step release order) | 1h | **done 2026-08-02** — tag `v1.11.0` + PRs #26/#27 |
| | **Approved subtotal** | **9–10h** | Phases 1 + 5 done → **pilot + wave left** |
| | **Approved subtotal** | **9–10h** | **COMPLETE 2026-08-02** — shipped as `v1.11.0` |
| 0 | Spike: go/no-go on OIDC-in-reusable | 3–4h | **tabled** |
| 4 | Convert `claude.yml` — move the 477 lines **faithfully** | 7–9h | **tabled** |
| 6 | Pilot `claude.yml` with the four assertions incl. pin-vs-HEAD | 4–6h | **tabled** |
Expand Down Expand Up @@ -461,6 +461,21 @@ the per-repo escape hatch being the untested path.
**Assertion:** on the pilot repo, set `BONSAI_URL` as a repository variable to a deliberately bogus host and
confirm the run goes **red**. Thirty seconds, and it converts the assumption into evidence.

> **RESOLVED 2026-08-02 — the assumption held.** Run log on `vite-plugin-shopify-clean`:
> `BONSAI_URL: https://pilot-bogus-host.invalid` then `curl: (6) Could not resolve host`, exit 6.
> The variable resolves against the **caller**, so the per-repo tunnel override survives the
> conversion. Leg 1 on `foundrae-blackridge@staging` separately proved a *private* consumer resolves
> the public cross-repo reusable and reads the caller's event payload
> (`event=pull_request action=opened draft=false -> status=Internal Review`).
>
> Two design corrections worth carrying into the `claude.yml` pilot, both recorded in
> [`fleet-operations.md`](fleet-operations.md): the `issues` leg is **not pilotable** (its `@claude`
> grep is mirrored by `claude.yml`'s gate, so tripping it wakes a real implementer on a client repo),
> and `closingIssuesReferences` only populates for PRs targeting the **default** branch — so a
> scratch-base PR dodges the theme deploy but resolves `uuid=<none>` and passes green having tested
> nothing. Assert on which host the log names, not on the run colour: a wrong-way resolution falls
> back to the hardcoded default and fails too.

**Phase 5 moved up.** It was written as "tag + repin" after the `claude.yml` conversion, but the
`bonsai-status-sync` half needs its own tag and repin to be usable at all — the new stub ships with a
placeholder pin. Do it as part of the Phase 3 wave, not after it.
Expand Down