From 7482141007ef9507a48eb44708b0fc61d1b16b30 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 23 Sep 2026 18:24:17 -0700 Subject: [PATCH] Disable Chromatic in CI Delete the Chromatic workflow and stop tend's ci-fix watching it. Argos now runs alone, so its workflow carries the trigger, draft, and fork gate reasoning that used to live in chromatic.yml. Docs that described Chromatic as running in CI now name Argos. The Storybook integration, the `chromatic` dependency, and the repo-level CHROMATIC_PROJECT_TOKEN stay for now; the secret stays listed in tend's secrets.allowed while it exists. Co-Authored-By: Claude Opus 5.5 (1M context) --- .claude/skills/running-tend/SKILL.md | 6 -- .config/tend.yaml | 1 - .github/renovate.json | 2 +- .github/workflows/argos.yml | 38 +++++++--- .github/workflows/chromatic.yml | 92 ----------------------- .github/workflows/tend-ci-fix.yaml | 2 +- AGENTS.md | 4 +- TESTING_AND_MODIFICATION_GUIDE.md | 4 +- docs/specs/security-ci.md | 2 +- docs/specs/theme.rationale.md | 2 +- docs/stories/pairing.mdx | 2 +- lib/.storybook/main.ts | 2 +- vscode-ext/test/webview-boot.smoketest.ts | 2 +- 13 files changed, 40 insertions(+), 119 deletions(-) delete mode 100644 .github/workflows/chromatic.yml diff --git a/.claude/skills/running-tend/SKILL.md b/.claude/skills/running-tend/SKILL.md index c65325c7b..445458bf1 100644 --- a/.claude/skills/running-tend/SKILL.md +++ b/.claude/skills/running-tend/SKILL.md @@ -9,12 +9,6 @@ No project-specific tend preferences yet beyond the notes below. Add guidance he When asking permission to file an issue upstream (e.g. at `max-sixty/tend`), do **not** include the standing-exception offer ("I can treat this target as file-directly going forward"). nedtwigg wants to keep approving each cross-repo issue individually — keep asking each time, and skip the offer. ([diffplug/dormouse#168](https://github.com/diffplug/dormouse/issues/168#issuecomment-4836133002)) -## The Chromatic `UI Tests` check is human-gated — don't wait it out in a gated-approval poll - -After approving a visually-changing PR the approval is gated, so the CI Monitoring poll runs to dismiss-on-red. One status context — **`UI Tests`** (Chromatic, `target_url` → `chromatic.com/build...`) — stays `PENDING` for the entire poll because Chromatic holds it open until a maintainer accepts or rejects the visual diffs in the Chromatic UI; it does **not** auto-terminalize in-session. Its sibling **`Storybook Publish`** (also Chromatic) *does* terminalize normally, so this applies only to `UI Tests`. - -When `UI Tests` is the only non-terminal check and every automated check is green (Build & Test, Visual Regression Tests, verify, Standalone Smoketest, Cloudflare Pages, Storybook Publish), treat it as human-gated: stop polling, confirm nothing flipped to FAILURE, and keep the approval standing — don't wait out the poll cap. Polling it to the cap wastes ~9–17 job-minutes per visually-changing PR with no added signal. (Observed on #203, #289, #317.) - ## A restart starts clean — don't carry a superseded PR's findings forward Long-running work here is often closed and reopened as a fresh PR ("Supersedes #N"), and that restart is deliberate — nedtwigg: *"When I start over, I usually **want** to start over. The original conversation grew too unfocused and out of hand."* So review the successor on its own terms: don't fetch the predecessor's bot comments and reviews in order to re-raise findings from them, and don't treat a finding dropped that way as a gap in the review machinery. Carrying the closed thread's context forward is the thing the restart was for. diff --git a/.config/tend.yaml b/.config/tend.yaml index 589cd6ba1..6d81014ee 100644 --- a/.config/tend.yaml +++ b/.config/tend.yaml @@ -10,5 +10,4 @@ workflows: ci-fix: watched_workflows: - CI - - Chromatic - Argos diff --git a/.github/renovate.json b/.github/renovate.json index 6ff1e9aeb..b011c9ae2 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -10,7 +10,7 @@ "Security fixes must not wait for the Monday window. Everything else here is batched to one day a week, which is right for routine bumps and wrong for a known-vulnerable dependency, so this overrides the schedule only.", "`minimumReleaseAge` is set here EXPLICITLY, and must stay that way. Renovate\u0027s own default for this block is `minimumReleaseAge: null`, force-applied before lookup — so omitting it does not inherit the cooldown from packageRules, it DROPS the cooldown entirely. Stating it is the only way to keep it.", "Keeping it is the deliberate choice: the cooldown guards the opposite threat — a compromised release that gets yanked within a day — and a reviewer reading a Renovate diff cannot detect a supply-chain compromise the way the ecosystem\u0027s own yank process can. Nothing here auto-merges, and the Dependabot alert already makes the vulnerability visible the moment it is published, so what the cooldown costs is a day before the remediation PR appears, not a day before anyone knows.", - "`draftPR` is set here EXPLICITLY for the same reason: the root sets it true so routine dependency PRs do not spend Chromatic snapshots until someone marks them ready, and this block inherits PR-stage options from the root — `labels` below is one. A draft security PR would put a manual state flip in front of the remediation, and tend-review cannot approve a draft, so it would arrive with no approval signal. Snapshot cost of exempting it is nil; security PRs are rare.", + "`draftPR` is set here EXPLICITLY for the same reason: the root sets it true so routine dependency PRs do not spend Argos snapshots until someone marks them ready, and this block inherits PR-stage options from the root — `labels` below is one. A draft security PR would put a manual state flip in front of the remediation, and tend-review cannot approve a draft, so it would arrive with no approval signal. Snapshot cost of exempting it is nil; security PRs are rare.", "See docs/specs/security-supply-chain.md, Cooldown and alerts." ], "enabled": true, diff --git a/.github/workflows/argos.yml b/.github/workflows/argos.yml index be9ca9f49..e9abe6329 100644 --- a/.github/workflows/argos.yml +++ b/.github/workflows/argos.yml @@ -1,10 +1,8 @@ name: Argos -# Runs beside Chromatic while Argos is evaluated as its replacement: same -# stories, and the same triggers and draft and fork gates as `chromatic.yml`, -# whose comments give the reasons. Argos renders in this job -# (Playwright, via the Storybook Vitest addon) rather than in its own cloud, so -# a play-function exception fails the run here instead of passing silently. +# Argos renders in this job (Playwright, via the Storybook Vitest addon) rather +# than in its own cloud, so a play-function exception fails the run here instead +# of passing silently. on: push: branches: @@ -12,7 +10,18 @@ on: paths: - 'lib/**' - 'docs/stories/**' + # The lockfile is the root's, so an in-range bump of a rendering dependency + # reaches the stories while matching neither glob above. Here it refreshes + # the baseline against the commit that moved the dependency, so the pixel + # change is attributed there rather than surfacing inside the next + # unrelated `lib/**` PR and being accepted as that PR's. Not on + # `pull_request`, where every dependency PR would bill a full run. - 'pnpm-lock.yaml' + # `lib/.storybook/main.ts` aliases these three to source, putting them in the + # story dependency graph exactly as `lib/src` is: `Wall` reaches all three, + # and `PocketWall.stories.tsx` imports `remote-lib-common` directly. Push-only + # like the lockfile; they nearly always change alongside `lib/**`, so the + # marginal build count is close to zero. - 'dor/**' - 'remote-lib-common/**' - 'dor-lib-common/**' @@ -21,12 +30,16 @@ on: paths: - 'lib/**' - 'docs/stories/**' + # Manual rebaseline. No glob catches every rendering input, and a commit editing + # these globs — this file — matches none of them, so without a dispatch the only + # way to refresh main's baseline is to wait for an unrelated qualifying commit. workflow_dispatch: permissions: contents: read concurrency: + # One in-flight build per PR, so a superseded push stops burning runner time. # Keyed by SHA on push so main never cancels: those builds are the baselines. group: argos-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} @@ -34,13 +47,20 @@ concurrency: jobs: argos: name: Argos Visual Tests + # A draft PR does not spend snapshots: marking it ready is the deliberate + # trigger. A skipped job reports success, but GitHub refuses to merge a draft at + # all, so for that clause the draft state is itself the gate. + # Fork PRs are skipped because secrets are withheld from them, leaving the upload + # without `ARGOS_TOKEN`. That clause has no such backstop — if this check is ever + # made required, a fork PR touching `lib/**` merges with it green rather than + # blocking as it does today. if: >- github.event_name != 'pull_request' || (github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository) runs-on: ubuntu-latest - # Unlike Chromatic's, this job does the rendering, and `--with-deps` runs an - # unbounded apt-get; without a cap a stalled mirror holds the runner 6h. + # This job does the rendering, and `--with-deps` runs an unbounded apt-get; + # without a cap a stalled mirror holds the runner 6h. timeout-minutes: 30 steps: - name: Checkout @@ -70,8 +90,8 @@ jobs: env: ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }} - # Publish even when a story fails capture, as Chromatic does, so one throwing - # `play` on main does not freeze the standing Storybook link. + # Publish even when a story fails capture, so one throwing `play` on main + # does not freeze the standing Storybook link. - name: Build Storybook if: ${{ !cancelled() }} run: pnpm build-storybook diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml deleted file mode 100644 index 999b468ee..000000000 --- a/.github/workflows/chromatic.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: Chromatic - -on: - push: - branches: - - main - paths: - - 'lib/**' - - 'docs/stories/**' - # The lockfile is the root's, so an in-range bump of a rendering dependency - # reaches the stories while matching neither glob above. Here it refreshes - # the baseline against the commit that moved the dependency, so the pixel - # change is attributed there rather than surfacing inside the next - # unrelated `lib/**` PR and being accepted as that PR's. Deliberately not - # on `pull_request`: a dependency change is global, so TurboSnap cannot - # narrow it and every dependency PR would bill a full run. - - 'pnpm-lock.yaml' - # `lib/.storybook/main.ts` aliases these three to source, putting them in the - # story dependency graph exactly as `lib/src` is: `Wall` reaches all three, - # and `PocketWall.stories.tsx` imports `remote-lib-common` directly. Push-only - # for the reason above; they nearly always change alongside `lib/**`, so the - # marginal build count is close to zero. - - 'dor/**' - - 'remote-lib-common/**' - - 'dor-lib-common/**' - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - paths: - - 'lib/**' - - 'docs/stories/**' - # Manual rebaseline. No glob catches every rendering input, and a commit editing - # these globs — this file — matches none of them, so without a dispatch the only - # way to refresh main's baseline is to wait for an unrelated qualifying commit. - workflow_dispatch: - -permissions: - contents: read - -concurrency: - # One in-flight build per PR, so a superseded push stops burning runner time. - # Whether it also stops the snapshot bill depends on the CLI cancelling the build - # server-side after upload, which is unverified. Keyed by SHA on push so main - # never cancels — those builds establish the baselines every PR diffs against. - group: chromatic-${{ github.event.pull_request.number || github.sha }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -jobs: - chromatic: - name: Visual Regression Tests - # A draft PR does not spend snapshots: marking it ready is the deliberate - # trigger. A skipped job reports success, but GitHub refuses to merge a draft at - # all, so for that clause the draft state is itself the gate. - # Fork PRs are skipped because secrets are withheld from them: the token would be - # empty and the action would fail red having captured nothing. That clause has no - # such backstop — if this check is ever made required, a fork PR touching `lib/**` - # merges with it green rather than blocking as it does today. - if: >- - github.event_name != 'pull_request' - || (github.event.pull_request.draft == false - && github.event.pull_request.head.repo.full_name == github.repository) - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - - - name: Setup Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version-file: package.json - - - name: Setup pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - - - name: Install dependencies - # Frozen, like every other workflow: a bare `pnpm install` resolves - # afresh, so the lockfile stops pinning what actually runs — in a job - # that holds CHROMATIC_PROJECT_TOKEN. Not `minimumReleaseAge`, which is - # applied during resolution and so binds a fresh resolve too; the - # lockfile pin is the whole of what freezing buys. - run: pnpm install --frozen-lockfile - working-directory: lib - - - name: Run Chromatic - uses: chromaui/action@bb3b582719a93c1828c5e520e62992b2937d7889 # v18.9.4 - with: - projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} - workingDir: lib - buildScriptName: build-storybook - exitZeroOnChanges: true - onlyChanged: true diff --git a/.github/workflows/tend-ci-fix.yaml b/.github/workflows/tend-ci-fix.yaml index cb559e4da..d7663a2d0 100644 --- a/.github/workflows/tend-ci-fix.yaml +++ b/.github/workflows/tend-ci-fix.yaml @@ -10,7 +10,7 @@ name: tend-ci-fix on: workflow_run: - workflows: ["CI", "Chromatic", "Argos"] + workflows: ["CI", "Argos"] types: [completed] branches: ["main"] diff --git a/AGENTS.md b/AGENTS.md index 185c98750..dd55a8559 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,8 +13,8 @@ pnpm build # build lib, vscode extension, Pocket, and website The harness opens its browser pane and prints its URL and browser command (`docs/specs/standalone.md` → "Standalone browser-dev harness"). -**Open every PR as a draft.** Chromatic and Argos bill per snapshot and skip drafts, so -marking a PR ready for review is what spends them. +**Open every PR as a draft.** Argos bills per snapshot and skips drafts, so marking a +PR ready for review is what spends snapshots. ## Worktrees diff --git a/TESTING_AND_MODIFICATION_GUIDE.md b/TESTING_AND_MODIFICATION_GUIDE.md index 48226bf60..139b7a5b6 100644 --- a/TESTING_AND_MODIFICATION_GUIDE.md +++ b/TESTING_AND_MODIFICATION_GUIDE.md @@ -53,8 +53,8 @@ needed. If a PR is squash-merged by mistake, PR N+1 will show PR N's changes again until `main` is merged into its branch (that merge resolves cleanly, since both sides carry identical content). The repo's default is a merge commit; keep it. -Keep every PR a draft until it is actually up for review (Chromatic and Argos -bill on ready-for-review). +Keep every PR a draft until it is actually up for review (Argos bills on +ready-for-review). ## 2. Setup in a worktree diff --git a/docs/specs/security-ci.md b/docs/specs/security-ci.md index 78f0463f1..d8ef68b33 100644 --- a/docs/specs/security-ci.md +++ b/docs/specs/security-ci.md @@ -35,7 +35,7 @@ This repository runs the [tend](https://github.com/max-sixty/tend) agent harness **The notifications poll widens its input.** `tend-notifications.yaml` alone takes its subjects from the bot account's own unread feed rather than from an event payload or a fixed scheduled sweep, and its inlined pre-check `PUT /repos/diffplug/dormouse/subscription -F subscribed=true` runs every `*/15` cycle to keep that feed wide. `dormouse-bot` watches all repository activity; its prompt decides whether to respond (rationale). What still bounds the bot on an undispatched thread is `author_association` tiering and the admin gate on `main`. **Never expect unwatching by hand to stick**: the PUT is idempotent and repeated every cycle, so the lever is `tend-notifications.yaml`, not the Unwatch button. -**Reachable repo-level secrets.** `CHROMATIC_PROJECT_TOKEN` and `ARGOS_TOKEN` are reachable by any workflow the bot can author: `.github/workflows/chromatic.yml` and `.github/workflows/argos.yml` are `pull_request`-triggered, and GitHub environment policies cannot distinguish a bot from a human contributor at the ref level. **Accepted, with rotation as the mitigation** — each token is scoped to a single project and easy to rotate. `OVSX_PAT` and `VSCE_PAT` are protected: they live only in the `vscode-extension-publish` environment, whose deployment-branch-policy admits only `v*` tags, and tag creation is admin-only. +**Reachable repo-level secrets.** `CHROMATIC_PROJECT_TOKEN` and `ARGOS_TOKEN` are reachable by any workflow the bot can author: `.github/workflows/argos.yml` is `pull_request`-triggered, and GitHub environment policies cannot distinguish a bot from a human contributor at the ref level. **Accepted, with rotation as the mitigation** — each token is scoped to a single project and easy to rotate. `OVSX_PAT` and `VSCE_PAT` are protected: they live only in the `vscode-extension-publish` environment, whose deployment-branch-policy admits only `v*` tags, and tag creation is admin-only. **Inert secret plumbing.** Every generated `tend-*.yaml` passes `anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}` to `max-sixty/tend/claude`; no such secret exists at repo or org level, so it resolves to the empty string and the harness authenticates with `CLAUDE_CODE_OAUTH_TOKEN` instead. **Never create an `ANTHROPIC_API_KEY` secret**: the input cannot be deleted locally, so the `FAIL IF` below is what makes adding one a deliberate, documented expansion of the bot's reach (rationale). diff --git a/docs/specs/theme.rationale.md b/docs/specs/theme.rationale.md index 173b4a702..39f9a3106 100644 --- a/docs/specs/theme.rationale.md +++ b/docs/specs/theme.rationale.md @@ -44,7 +44,7 @@ showed the stored theme. **Why the picker row's `X` keeps a gap from the select target.** The two paths above do not recover symmetrically: `Remove` leaves the extension row on screen to re-install, while the `X` means re-finding the extension through an OpenVSX search. The gap prices in the harder undo. -**Why a story, not only a unit test, pins the short-viewport cap.** A unit test can stub the trigger and menu rectangles to pin side selection and resize recomputation, including a visual viewport shrinking while the layout viewport stays fixed, but it cannot prove that the real list yields height while the footer survives. `lib/src/components/design.test.ts` pins the viewport inset, `lib/src/components/use-anchored-menu.test.tsx` the geometry, and the Chromatic story the rendered result. +**Why a story, not only a unit test, pins the short-viewport cap.** A unit test can stub the trigger and menu rectangles to pin side selection and resize recomputation, including a visual viewport shrinking while the layout viewport stays fixed, but it cannot prove that the real list yields height while the footer survives. `lib/src/components/design.test.ts` pins the viewport inset, `lib/src/components/use-anchored-menu.test.tsx` the geometry, and the Argos-snapshotted story the rendered result. **Why the colour tokens are their own file.** The website compiles the library from source, so its Tailwind root has to scan `lib/src` or none of the library's utilities are emitted there — which is how the picker once lost its width, cap, and stacking on the docs pages, silently and with no build error. Scanning alone was not enough: the colour utilities resolve against an `@theme` the website did not declare, so the picker, the OpenVSX store, and the theme debugger each rendered with no surface, border, or text colour, and the picker carried a private inline stylesheet to compensate. Importing the whole of `theme.css` was not an option either — its `@theme` retunes `--text-xs`/`--text-sm` for a dense terminal UI, which would shrink type across the marketing site. Splitting the colour half out lets a host take the tokens without the app, and the inline stylesheet was deleted. diff --git a/docs/stories/pairing.mdx b/docs/stories/pairing.mdx index 486ad3102..d4e6d079b 100644 --- a/docs/stories/pairing.mdx +++ b/docs/stories/pairing.mdx @@ -17,7 +17,7 @@ you meet it: stand up a Relay, build a Burrow that is allowed to reach it, enrol the laptop, scan its code with the phone, approve the pairing, connect. The screens below are the real components, rendered from the same stories -Chromatic watches. They are static — nothing here is wired to a live Relay. +Argos watches. They are static — nothing here is wired to a live Relay. One of them carries `autoplay` because a docs page does not run a story's `play` by default, and its state — a painted terminal — only exists after it runs. diff --git a/lib/.storybook/main.ts b/lib/.storybook/main.ts index 09afe9b75..0ee90894e 100644 --- a/lib/.storybook/main.ts +++ b/lib/.storybook/main.ts @@ -46,7 +46,7 @@ const config: StorybookConfig = { dor: path.resolve(here, '..', '..', 'dor', 'src'), // Same reason: `Wall` → `RemotePairingModalHost` pulls in the remote host // modules, which import `remote-lib-common`. Its package `exports` point - // at a `dist` the Storybook/Chromatic job never builds, so alias the bare + // at a `dist` the Storybook/Argos job never builds, so alias the bare // specifier to source too. 'remote-lib-common': path.resolve(here, '..', '..', 'remote-lib-common', 'src'), // And `Wall` → `useDorControl` → `connect-port` imports diff --git a/vscode-ext/test/webview-boot.smoketest.ts b/vscode-ext/test/webview-boot.smoketest.ts index dcc54a5d5..cfd73c65b 100644 --- a/vscode-ext/test/webview-boot.smoketest.ts +++ b/vscode-ext/test/webview-boot.smoketest.ts @@ -22,7 +22,7 @@ import { launchChromium } from './launch-chromium'; * substitutes for it. jsdom is not an option: it does not enforce CSP at all. * * Deliberately shallow. It asserts the app mounts and the policy blocked - * nothing — not what the UI looks like, which is Storybook's and Chromatic's + * nothing — not what the UI looks like, which is Storybook's and Argos's * job. */