@andwhy/e2e-pipeline 0.10.1 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,6 +35,7 @@ Re-running `init --force` in an existing project is the way to pick up a new ver
35
35
  - `.claude/skills/playwright-test-pipeline/` — guided walkthrough skill; triggers when someone asks to create or fix tests and escorts them through setup check, Plan → Generate → Heal, preview validation, and deployment
36
36
  - `.mcp.json` — Playwright MCP server config (project root)
37
37
  - `playwright.mcp.config.{ts,mjs}` — config the MCP server loads: extends `playwright.config` and clears `testIgnore`, so the planner/generator can execute seeds that normal runs ignore
38
+ - `.github/prompts/checkly-heal.md` — instructions for the auto-heal agent the `checkly-heal` workflow runs
38
39
 
39
40
  **Seeded once** — starting points your project then owns. Written only when absent and never overwritten, `--force` included:
40
41
 
@@ -43,10 +44,13 @@ Re-running `init --force` in an existing project is the way to pick up a new ver
43
44
  - `tests/e2e/seed.spec.{ts,js}` — seed file for Playwright MCP `setup_page`
44
45
  - `tests/e2e/helpers/preview-consent-setup.{ts,js}` — `prepareVercelPreviewPage` (Vercel deployment-protection bypass); projects extend it with consent logic
45
46
  - `.github/workflows/pr-playwright-tests.yml` — runs the suite against the PR's Vercel preview and comments the result (plus `.github/scripts/playwright-pr-comment.mjs`)
46
- - `.github/workflows/checkly-deploy.yml` — deploys monitors on merge to `main`
47
+ - `.github/workflows/checkly-deploy.yml` — deploys monitors when a production deployment succeeds
48
+ - `.github/workflows/checkly-heal.yml` — a failing deployed monitor triggers a triage-and-heal agent: production regressions escalate to Slack immediately; stale tests get fixed and flaky tests hardened — including behavior-neutral test hooks (`data-testid`) in app source when needed — and opened as a human-review PR, whose own preview run validates the app-side hooks. Inert until a Checkly webhook alert channel dispatches to it — the skill walks through that setup.
47
49
 
48
50
  The workflows need repository secrets: `CHECKLY_API_KEY`, `CHECKLY_ACCOUNT_ID`,
49
- and `VERCEL_AUTOMATION_BYPASS_SECRET` (values in 1Password).
51
+ `VERCEL_AUTOMATION_BYPASS_SECRET`, and for auto-heal `SLACK_WEBHOOK_URL`
52
+ (values in 1Password). `ANTHROPIC_API_KEY` is an organization secret inherited
53
+ by all repos — nothing to add.
50
54
 
51
55
  E2E specs live under **`tests/e2e/`** so a repo can keep unit tests elsewhere under
52
56
  `tests/` without Playwright trying to run them. Seeds sit in the same tree and are
package/dist/bin/init.js CHANGED
@@ -79,6 +79,11 @@ const run = async () => {
79
79
  console.log(` ${path.relative(projectRoot, f)}`);
80
80
  }
81
81
  }
82
+ // Dependencies install before the formatting pass: prettier is resolved
83
+ // from the project's node_modules, which a fresh clone doesn't have yet.
84
+ // Formatting after the install means scaffolded files pick up the project's
85
+ // style even on a first run in a bare checkout.
86
+ installDeps(projectRoot, fileFormat);
82
87
  const formatted = await formatFiles(projectRoot, [
83
88
  ...result.created,
84
89
  ...result.regenerated,
@@ -107,12 +112,15 @@ const run = async () => {
107
112
  console.log([
108
113
  "\nGitHub workflows created. They need repository secrets before they run",
109
114
  "(values in 1Password):",
110
- " CHECKLY_API_KEY, CHECKLY_ACCOUNT_ID — deploy monitors on merge",
115
+ " CHECKLY_API_KEY, CHECKLY_ACCOUNT_ID — deploy monitors, auto-heal",
111
116
  " VERCEL_AUTOMATION_BYPASS_SECRET — PR tests through Vercel",
112
117
  " deployment protection",
118
+ " SLACK_WEBHOOK_URL — auto-heal notifications",
119
+ "(ANTHROPIC_API_KEY is an org-level secret inherited by all repos —",
120
+ "nothing to add.) The auto-heal workflow stays inert until a Checkly",
121
+ "webhook alert channel dispatches to it — the skill walks through that.",
113
122
  ].join("\n"));
114
123
  }
115
- installDeps(projectRoot, fileFormat);
116
124
  console.log("Next steps:");
117
125
  let step = 1;
118
126
  const ext = fileFormat === "mjs" ? "mjs" : "ts";
@@ -77,6 +77,9 @@ const GITIGNORE_LINES = [
77
77
  // Session artifacts (traces, snapshots) the Playwright MCP server writes
78
78
  // while the planner/generator drive the browser.
79
79
  ".playwright-mcp/",
80
+ // Working directory of the Checkly auto-heal agent (context, outcome,
81
+ // downloaded results) — CI artifacts, never committed.
82
+ ".checkly-heal/",
80
83
  ];
81
84
  // Lines earlier scaffold versions added and this one must take back: seeds
82
85
  // were once regenerable root-level scaffolding, but since 0.5.0 the planner
@@ -228,6 +231,10 @@ export const scaffold = async (templatesRoot, projectRoot, { force = false, file
228
231
  path.join("workflows", "pr-playwright-tests.yml"),
229
232
  path.join(".github", "workflows", "pr-playwright-tests.yml"),
230
233
  ],
234
+ [
235
+ path.join("workflows", "checkly-heal.yml"),
236
+ path.join(".github", "workflows", "checkly-heal.yml"),
237
+ ],
231
238
  [
232
239
  path.join("scripts", "playwright-pr-comment.mjs"),
233
240
  path.join(".github", "scripts", "playwright-pr-comment.mjs"),
@@ -244,6 +251,25 @@ export const scaffold = async (templatesRoot, projectRoot, { force = false, file
244
251
  result.created.push(destPath);
245
252
  }
246
253
  }
254
+ // The heal prompt is package-owned like the agent definitions: it carries
255
+ // agent behavior, not project config, so --force updates it.
256
+ const githubOwnedMappings = [
257
+ [
258
+ path.join("prompts", "checkly-heal.md"),
259
+ path.join(".github", "prompts", "checkly-heal.md"),
260
+ ],
261
+ ];
262
+ for (const [src, dest] of githubOwnedMappings) {
263
+ const destPath = path.join(projectRoot, dest);
264
+ if (!force && (await fs.pathExists(destPath))) {
265
+ result.skipped.push(destPath);
266
+ }
267
+ else {
268
+ await fs.ensureDir(path.dirname(destPath));
269
+ await fs.copy(path.join(githubSrc, src), destPath);
270
+ result.created.push(destPath);
271
+ }
272
+ }
247
273
  await appendGitignore(projectRoot, result);
248
274
  return result;
249
275
  };
@@ -47,8 +47,20 @@ export const installDeps = (projectRoot, fileFormat = "ts") => {
47
47
  console.log(` ${cmd}\n`);
48
48
  execSync(cmd, { cwd: projectRoot, stdio: "inherit" });
49
49
  console.log("\nInstalling Chromium browser…\n");
50
- execSync("npx playwright install chromium", {
51
- cwd: projectRoot,
52
- stdio: "inherit",
53
- });
50
+ // Non-fatal: restricted networks (CI sandboxes, proxied agents) block the
51
+ // browser CDN, and a failed download must not abort the install — the
52
+ // scaffolded files are already written and CI installs its own browsers.
53
+ try {
54
+ execSync("npx playwright install chromium", {
55
+ cwd: projectRoot,
56
+ stdio: "inherit",
57
+ });
58
+ }
59
+ catch {
60
+ console.warn([
61
+ "\nChromium download failed (restricted network?). The scaffold is",
62
+ "complete — run `npx playwright install chromium` yourself before",
63
+ "local test runs.",
64
+ ].join("\n"));
65
+ }
54
66
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andwhy/e2e-pipeline",
3
- "version": "0.10.1",
3
+ "version": "0.11.1",
4
4
  "description": "Scaffolds the full e2e testing pipeline: agent-assisted Playwright tests, CI runs against Vercel previews, and Checkly monitors that follow production deploys",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -59,11 +59,32 @@
59
59
  await expect(page).not.toHaveURL(initialUrl, { timeout: 10_000 });
60
60
  ```
61
61
 
62
- - **Skip unsupported domains in `beforeEach`.** If a domain has no entry in a spec's locale
63
- config, skip all tests up front rather than letting them fail:
62
+ - **Skip unsupported domains in `beforeEach` — but never previews.** If a domain has no
63
+ entry in a spec's locale config, skip all tests up front rather than letting them fail:
64
64
 
65
65
  ```ts
66
66
  test.beforeEach(async ({ page, baseURL }) => {
67
67
  test.skip(!getLocaleConfig(baseURL!), "No config for this domain");
68
68
  });
69
69
  ```
70
+
71
+ A preview deployment's hostname (`*.vercel.app`) is never in the locale map, so a plain
72
+ exact-match lookup silently skips the entire suite on every PR run and in
73
+ `checkly pw-test` — green checks that tested nothing. The lookup must resolve preview
74
+ hosts to the config of the production domain the preview serves (ask the user which one
75
+ if it isn't obvious; `PREVIEW_LOCALE_HOST` overrides it at runtime):
76
+
77
+ ```ts
78
+ const getLocaleConfig = (baseURL: string) => {
79
+ const hostname = new URL(baseURL).hostname.replace(/^www\./, "");
80
+ const config = LOCALE_CONFIG[hostname];
81
+ if (config) return config;
82
+ // Previews serve <primary domain>'s content; without this every preview run skips.
83
+ if (hostname.endsWith(".vercel.app"))
84
+ return LOCALE_CONFIG[process.env.PREVIEW_LOCALE_HOST ?? "<primary domain>"];
85
+ return undefined;
86
+ };
87
+ ```
88
+
89
+ Only a genuinely unknown non-preview host skips. Single-domain suites without a locale
90
+ map are unaffected — they have no lookup to fall through.
@@ -36,7 +36,10 @@ These rules apply to **all** agents (generator, healer, and orchestrator). Inclu
36
36
  project consent logic; do not reinvent it. The helper file extension must match rule 7.
37
37
 
38
38
  4. Never hardcode a single locale's URL pattern. Use a locale config map keyed
39
- by bare hostname and resolve at runtime.
39
+ by bare hostname and resolve at runtime. Preview hosts (`*.vercel.app`) are
40
+ never in the map and must fall back to the primary production domain's
41
+ config (overridable via `PREVIEW_LOCALE_HOST`) — an exact-match-only lookup
42
+ skips the whole suite on every PR run.
40
43
 
41
44
  5. After all tests pass locally, verify with
42
45
  `BASE_URL=<dev-url> pnpm playwright test` via the terminal tool. If failures
@@ -201,3 +204,16 @@ If any tests fail:
201
204
  - **Plan files** are saved at `tests/e2e/<feature>/<name>.plan.md`.
202
205
  - **Test files** follow **Project test rules** §7–8 — typically `tests/e2e/<feature>/<group>.spec.js`
203
206
  or `.spec.ts` with one `describe` and multiple tests.
207
+
208
+ ## Checkly auto-heal (CI, not this session)
209
+
210
+ Failing deployed monitors are handled by `.github/workflows/checkly-heal.yml`:
211
+ it triages first (production regression → Slack alert; stale test → fix; flaky
212
+ test → harden), may add behavior-neutral test hooks (`data-testid`) to app
213
+ source when the robust fix needs them, and opens a `checkly-heal` PR a human
214
+ must review — app-side hooks are validated by the PR's own preview run. Its
215
+ agent instructions live in `.github/prompts/checkly-heal.md` (package-owned —
216
+ update via `init --force`, don't hand-edit). If a user asks about a
217
+ `checkly-heal` PR, that workflow authored it; reviewing one means verifying the
218
+ updated test still describes correct production behavior, not that it merely
219
+ passes.
@@ -76,7 +76,7 @@ The scaffold writes templates, not a working setup. Walk these with the user:
76
76
  page, and pointing it at an empty or unreachable app produces a plan that
77
77
  asserts nothing useful.
78
78
  4. **Repository secrets for the workflows**: if the scaffold just created
79
- `.github/workflows/`, tell the user now — not at merge time — to add three
79
+ `.github/workflows/`, tell the user now — not at merge time — to add the
80
80
  repository secrets (GitHub → Settings → Secrets and variables → Actions).
81
81
  You cannot add them; a person must. Without them the first PR push and the
82
82
  first merge fail CI in ways that look like pipeline bugs.
@@ -88,6 +88,11 @@ The scaffold writes templates, not a working setup. Walk these with the user:
88
88
  - `VERCEL_AUTOMATION_BYPASS_SECRET` — Vercel → project → Settings →
89
89
  Deployment Protection → Protection Bypass for Automation. Per project —
90
90
  never copied from another repo.
91
+ - `SLACK_WEBHOOK_URL` — a Slack incoming webhook for the project's
92
+ notification channel; the auto-heal workflow posts its alerts there.
93
+ Ask which channel before creating one.
94
+ - `ANTHROPIC_API_KEY` needs **no action**: it is an organization-level
95
+ secret inherited by every repo. Do not tell the user to add it.
91
96
  5. **Existing tests under `tests/`** (unit, a11y — the scaffold lists them as
92
97
  "outside tests/e2e"): leave `testDir` at `./tests/e2e` and give that suite
93
98
  its own Playwright project with its own `testDir` if it should run under
@@ -148,7 +153,7 @@ until after merge.
148
153
 
149
154
  ## Phase 5 — Merge and deployment
150
155
 
151
- The scaffold ships two workflows (seeded once — existing ones are never
156
+ The scaffold ships three workflows (seeded once — existing ones are never
152
157
  overwritten):
153
158
 
154
159
  - `.github/workflows/pr-playwright-tests.yml` — runs the suite against the
@@ -161,13 +166,23 @@ overwritten):
161
166
  merge that adds tests for new `data-testid`s would go red until the release
162
167
  carrying them is live — with gated releases that gap can be days. A
163
168
  `workflow_dispatch` fallback allows manual runs.
169
+ - `.github/workflows/checkly-heal.yml` — a failing deployed monitor
170
+ dispatches here (via the webhook alert channel below). An agent triages
171
+ first: a production regression or anything needing a behavioral app change
172
+ alerts Slack immediately; a stale test gets fixed and a flaky test gets
173
+ hardened — including behavior-neutral test hooks (`data-testid`) in app
174
+ source when the robust fix needs them — and opened as a PR that a human
175
+ must review. Test-only fixes are verified against production and on
176
+ Checkly's infrastructure; app-side hooks are validated by the PR's own
177
+ preview run (pr-playwright-tests). Inert until the webhook exists.
164
178
 
165
179
  **Before the first merge, the repo needs secrets** (values in 1Password):
166
- `CHECKLY_API_KEY` and `CHECKLY_ACCOUNT_ID` for the deploy,
180
+ `CHECKLY_API_KEY` and `CHECKLY_ACCOUNT_ID` for the deploy and auto-heal,
167
181
  `VERCEL_AUTOMATION_BYPASS_SECRET` for the PR run through deployment
168
- protection. If the scaffold just created the workflows, say so explicitly and
169
- list the three secrets — a merge without them fails CI and looks like a
170
- pipeline bug.
182
+ protection, `SLACK_WEBHOOK_URL` for auto-heal notifications
183
+ (`ANTHROPIC_API_KEY` is inherited from the organization — nothing to add).
184
+ If the scaffold just created the workflows, say so explicitly and list the
185
+ secrets — a merge without them fails CI and looks like a pipeline bug.
171
186
 
172
187
  After merge, monitors update once the production deployment goes live — the
173
188
  user verifies in the Checkly dashboard (https://app.checklyhq.com/); nothing
@@ -175,20 +190,69 @@ to run by hand. Manual fallback if a repo deleted the deploy workflow:
175
190
  `pnpm checkly test --record`, then `pnpm checkly deploy` (requires the
176
191
  Phase 4 login).
177
192
 
178
- **Alert notifications are not automatic — walk the user through it.** A
179
- failing monitor only shows in the Checkly dashboard until an alert channel
180
- exists, so nobody gets notified. In Checkly:
181
-
182
- 1. Alert channels → "Add more channels" → select **Slack**.
183
- 2. Channel configuration: the project's Slack notification channel
184
- (`#…`). A dedicated test-notification channel may exist by now — ask
185
- which channel to use rather than assuming.
186
- 3. Verify the subscriptions: **only the current check** subscribed, and
187
- **uncheck** "Automatically subscribe newly created checks to this alert
188
- channel".
189
- 4. Test the alert channel (button) and confirm the notification arrived in
190
- Slack.
191
- 5. "Create alert Slack channel".
193
+ **Alert notifications are not automatic — walk the user through the
194
+ auto-heal webhook.** A failing monitor only shows in the Checkly dashboard
195
+ until an alert channel exists. The alert channel for these suites is a
196
+ **webhook into the auto-heal workflow**, not a direct Slack channel: the
197
+ team's Slack message is written by the pipeline *after* triage (regression
198
+ vs. stale test), so it carries a diagnosis and, for stale tests, the fix PR.
199
+ Set up once per project:
200
+
201
+ 1. Confirm the `SLACK_WEBHOOK_URL` repository secret from Phase 0 exists —
202
+ the workflow escalates nothing without it. (`ANTHROPIC_API_KEY` comes
203
+ from the organization automatically.)
204
+ 2. The shared `GH_DISPATCH_TOKEN` secret exists in Checkly and covers this
205
+ repo. This is **one-time organization setup, not yours to perform**: the
206
+ token (`checkly-heal-dispatch`, a fine-grained PAT) is created once and
207
+ maintained by its owner, named in 1Password — see "One-time organization
208
+ setup" in the usage guide
209
+ (https://app.notion.com/p/32df837b627080748719c0f94a3aeaaf). Your only
210
+ per-project concern: if this repo is not yet on the token's repository
211
+ list, the webhook channel's test (step 3) fails with a 404 — then ask
212
+ the token owner to widen it (a one-minute edit on their side; the token
213
+ value doesn't change). Do not walk the user through creating a PAT or a
214
+ Checkly secret.
215
+ 3. Checkly → Alert channels → "Add more channels" → **Webhook**:
216
+ - Method `POST`, URL
217
+ `https://api.github.com/repos/<org>/<repo>/dispatches`
218
+ - Headers: `Authorization: Bearer {{GH_DISPATCH_TOKEN}}` and
219
+ `Accept: application/vnd.github+json`
220
+ - Body (Checkly's editor lists the available variables — confirm the
221
+ names there):
222
+
223
+ ```json
224
+ {
225
+ "event_type": "checkly-alert",
226
+ "client_payload": {
227
+ "check_name": "{{CHECK_NAME}}",
228
+ "check_id": "{{CHECK_ID}}",
229
+ "result_id": "{{CHECK_RESULT_ID}}",
230
+ "result_link": "{{RESULT_LINK}}",
231
+ "alert_type": "{{ALERT_TYPE}}"
232
+ }
233
+ }
234
+ ```
235
+
236
+ - Notify on **failure**; enable **reminders** while a check stays failing
237
+ — a reminder re-fires the webhook, the workflow dedupes repeats, and a
238
+ lost first delivery gets retried by the next one. Recovery/degraded
239
+ notifications are ignored by the workflow either way.
240
+ 4. Subscriptions: **only the current check**, and **uncheck** "Automatically
241
+ subscribe newly created checks to this alert channel".
242
+ 5. **Do not also subscribe a Slack alert channel to these suites.** The
243
+ pipeline is the single alert path; a parallel raw Checkly alert double-pings
244
+ the channel and trains people to ignore one of them.
245
+ 6. Dry run: GitHub → Actions → "Checkly auto-heal" → Run workflow, with the
246
+ suite's check name. Confirm the run completes and the Slack message
247
+ arrives (a passing check should end as "not reproducible" — summary only,
248
+ no ping — so a real dry run is easiest right after any genuine failure).
249
+
250
+ What the user should expect afterwards: a failing monitor produces either a
251
+ `checkly-heal` PR plus a Slack note (stale or flaky test — **review
252
+ required, the PR is never merged automatically**; PRs carrying app-side test
253
+ hooks say so and point at their preview run for validation), or a Slack
254
+ escalation (regression, fix needs a behavioral app change, agent failed). A
255
+ transient blip that doesn't reproduce stays silent by design.
192
256
 
193
257
  ## Conventions to uphold
194
258
 
@@ -0,0 +1,141 @@
1
+ # Checkly monitor auto-heal
2
+
3
+ You are running unattended in CI because a deployed Checkly monitor failed
4
+ against production. Your job is to triage the failure first, and only then —
5
+ when the test is stale or flaky rather than the site broken — fix it. A
6
+ workflow wraps this run: it commits, opens the pull request, and notifies the
7
+ team. You never push, never open PRs, never send notifications.
8
+
9
+ ## Inputs
10
+
11
+ - `.checkly-heal/context.json` — the failing check's name, the alert type, a
12
+ link to the Checkly result, and the paths of any downloaded artifacts.
13
+ - `.checkly-heal/result.json` — the raw Checkly check result when the API call
14
+ succeeded: error message, failing test and step, stack trace. May be absent;
15
+ proceed without it if so.
16
+ - `.claude/agents/playwright-test-healer.md` — debugging practice and
17
+ robustness rules. Follow its methodology. The overrides below take
18
+ precedence over anything it says.
19
+
20
+ ## Overrides for this run
21
+
22
+ These invert or forbid parts of the normal healer behavior. They are not
23
+ negotiable:
24
+
25
+ 1. **`BASE_URL` pins to production, not localhost.** The monitor asserts
26
+ against production, so production is the reproduction and verification
27
+ target. Resolve the URL from `checkly.config.{ts,mjs}`: find the
28
+ `playwrightChecks` entry whose `name` (or `logicalId`) matches the failing
29
+ check name from the context, and use its `BASE_URL` environment variable.
30
+ 2. **Never `test.fixme()`, never skip, disable, or delete a test.** A skipped
31
+ test is a silently disabled production monitor. If you cannot fix it,
32
+ classify accordingly and stop.
33
+ 3. **App source changes are limited to behavior-neutral test hooks.** You may
34
+ add or restore attributes that exist for tests — `data-testid`, `aria-*`
35
+ where genuinely correct — on components the failing test targets. You may
36
+ not change logic, markup structure, styling, content, dependencies, or
37
+ configuration. If only a behavioral app change can fix the failure, that is
38
+ the `needs-app-change` classification, not something you attempt. Never
39
+ touch `.github/`, `.claude/`, `package.json`, lockfiles, or any
40
+ `*.config.*` file.
41
+ 4. **Never run git commit, push, or any GitHub CLI/API command.**
42
+
43
+ ## Procedure
44
+
45
+ ### 1. Orient
46
+
47
+ Read the context and result files. Identify the failing spec file, test, and
48
+ step. Read the spec. Resolve the production `BASE_URL` (override 1).
49
+
50
+ ### 2. Reproduce
51
+
52
+ Run only the failing spec against production:
53
+
54
+ ```bash
55
+ BASE_URL=<production-url> pnpm playwright test <failing-spec> --project=Chrome
56
+ ```
57
+
58
+ - **Fails** → continue to triage.
59
+ - **Passes** → run it again with `--repeat-each=3`. All green means the
60
+ failure was transient on Checkly's side: classification `not-reproducible`,
61
+ stop, change nothing. Intermittent failures mean the test is **flaky** —
62
+ continue to triage with that hypothesis.
63
+
64
+ ### 3. Triage — before any edit
65
+
66
+ Decide which of these is true, using the failure output, a fresh trace, and
67
+ the live page:
68
+
69
+ - **The site is broken.** The user-visible outcome the test asserts genuinely
70
+ does not work on production — a purchase flow erroring, a page failing to
71
+ render, content missing that should exist. Classification `regression`.
72
+ Stop immediately. Do not touch the test: adjusting a test to accept broken
73
+ behavior hides an outage.
74
+ - **The test is flaky.** The outcome works, but the test fails
75
+ intermittently — races, missing waits for dynamic content, animation
76
+ timing, order-dependent assertions. Classification `flaky` — proceed to
77
+ step 4 and fix the root cause per the robustness rules; blanket timeout
78
+ increases are not a fix.
79
+ - **The test is stale.** Production changed legitimately (new markup, changed
80
+ copy, moved element) and the test's selectors or assertions no longer
81
+ describe it, while the actual user outcome still works. Classification
82
+ `healed` — proceed to step 4.
83
+ - **The fix needs a behavioral app change.** The failure is rooted in app
84
+ code beyond a test hook — or in a pending release that will resolve it.
85
+ Classification `needs-app-change`. Stop and say precisely what a developer
86
+ must change, or which release resolves it.
87
+
88
+ When genuinely uncertain between regression and any fixable case, choose
89
+ `regression`. A false alarm costs a developer minutes; a masked outage costs
90
+ much more.
91
+
92
+ ### 4. Heal (stale or flaky)
93
+
94
+ Fix the test following the healer methodology and robustness rules: prefer
95
+ resilient locators, fix root causes over timeouts, keep edits minimal.
96
+
97
+ When the robust fix needs a stable hook the app no longer (or never) provides,
98
+ add the minimal behavior-neutral attribute to the component (override 3) and
99
+ target it from the test — do not fall back to a brittle text or CSS selector
100
+ just to avoid touching app source. Keep such changes to the fewest attributes
101
+ that make the test robust.
102
+
103
+ ### 5. Verify
104
+
105
+ - **Test-only fixes:** re-run the spec against production until green — for a
106
+ flaky fix, prove stability with `--repeat-each=5`, not a single pass. Then
107
+ run the full e2e suite once the same way to catch collateral damage, and
108
+ validate on Checkly's infrastructure:
109
+
110
+ ```bash
111
+ pnpm checkly pw-test --env BASE_URL="<production-url>" -- <failing-spec>
112
+ ```
113
+
114
+ Credentials are in the environment. If cloud verification cannot run, say
115
+ so in the outcome details rather than claiming it.
116
+ - **Fixes that add app-side test hooks:** production does not carry your new
117
+ attributes yet, so a full production run cannot pass — that is expected.
118
+ Verify everything verifiable (the spec compiles, unrelated tests still pass
119
+ against production, the selector logic matches the component you edited)
120
+ and state exactly what remains for the preview run: the pull request the
121
+ workflow opens gets its own Vercel preview deployment, and the repo's
122
+ `pr-playwright-tests` workflow runs the full suite against it — that run is
123
+ the authoritative validation for app-side hooks. Say this in the details so
124
+ the reviewer knows to check it.
125
+
126
+ ### 6. Write the outcome — required on every path
127
+
128
+ Write `.checkly-heal/outcome.json` before finishing, whatever happened:
129
+
130
+ ```json
131
+ {
132
+ "classification": "healed | flaky | regression | needs-app-change | not-reproducible",
133
+ "summary": "One sentence for the Slack notification.",
134
+ "details": "Markdown for the PR body or alert: what failed, the root cause, what changed — or exactly what a developer needs to do. For app-side hooks: which components gained which attributes, and that the PR's preview run validates them."
135
+ }
136
+ ```
137
+
138
+ For `healed` and `flaky`, the details become the PR description a human
139
+ reviews — include what broke, why, what you changed, and how it was verified.
140
+ If you finish without writing this file, the workflow treats the run as failed
141
+ and escalates to a developer, so an honest partial outcome always beats none.
@@ -0,0 +1,318 @@
1
+ name: Checkly auto-heal
2
+
3
+ # A failing Checkly monitor dispatches here (webhook alert channel →
4
+ # repository_dispatch). The agent triages first: a stale or flaky test gets
5
+ # fixed and opened as a human-review PR; a real production regression, a fix
6
+ # needing behavioral app code, or any agent failure escalates to Slack
7
+ # instead. Fixes may include behavior-neutral test hooks in app source
8
+ # (data-testid) — the PR's own preview run (pr-playwright-tests.yml)
9
+ # validates those; a diff guard aborts changes to protected paths
10
+ # (.github, .claude, configs, lockfiles). The agent never pushes: branch,
11
+ # commit, PR, and Slack are workflow steps.
12
+ # Needs repo secrets CHECKLY_API_KEY, CHECKLY_ACCOUNT_ID, SLACK_WEBHOOK_URL
13
+ # (values in 1Password); ANTHROPIC_API_KEY is inherited from the organization.
14
+ # Setup docs: the playwright-test-pipeline skill walks through the Checkly
15
+ # webhook channel.
16
+ on:
17
+ repository_dispatch:
18
+ types: [checkly-alert]
19
+ workflow_dispatch:
20
+ inputs:
21
+ check_name:
22
+ description: "Failing check name (as in checkly.config)"
23
+ required: true
24
+ check_id:
25
+ description: "Checkly check ID (optional, enables result download)"
26
+ required: false
27
+ result_id:
28
+ description: "Checkly check result ID (optional)"
29
+ required: false
30
+
31
+ run-name: "Checkly auto-heal · ${{ github.event.client_payload.check_name || inputs.check_name }}"
32
+
33
+ permissions:
34
+ contents: write
35
+ pull-requests: write
36
+ issues: write
37
+
38
+ # One heal at a time; queued alerts (e.g. an outage failing several checks)
39
+ # run serially and the dedup below makes repeats cheap no-ops.
40
+ concurrency:
41
+ group: checkly-heal
42
+ cancel-in-progress: false
43
+
44
+ env:
45
+ # The e2e test root. Changes outside it (behavior-neutral test hooks in app
46
+ # source) are allowed but flagged on the PR and in Slack.
47
+ # Match your playwright.config testDir.
48
+ E2E_ROOT: tests/e2e
49
+ # Paths the agent must never change — anything here aborts and escalates.
50
+ # Extended regex against the changed-file list.
51
+ PROTECTED_PATHS: '^(\.github/|\.claude/|\.mcp\.json$|.*\.config\.(ts|mjs|js|cjs)$|package\.json$|pnpm-lock\.yaml$|package-lock\.json$|yarn\.lock$|\.env)'
52
+ # How long a merged heal PR blocks a second silent attempt on the same check.
53
+ REPEAT_WINDOW_DAYS: 7
54
+
55
+ jobs:
56
+ heal:
57
+ # Recovery/degraded notifications are not work; only failures are.
58
+ if: >-
59
+ github.event_name == 'workflow_dispatch' ||
60
+ contains(github.event.client_payload.alert_type || 'ALERT_FAILURE', 'FAILURE')
61
+ runs-on: ubuntu-latest
62
+ timeout-minutes: 45
63
+
64
+ steps:
65
+ - name: Check out the default branch
66
+ uses: actions/checkout@v5
67
+ with:
68
+ fetch-depth: 0
69
+
70
+ # Dedup and repeat-offender guard. An open heal PR for this check means
71
+ # the alert is already handled. A heal PR merged recently means the fix
72
+ # did not hold — a second silent attempt would mask instability, so that
73
+ # escalates to a human instead.
74
+ - name: Guard against duplicate and repeated heals
75
+ id: guard
76
+ env:
77
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
78
+ CHECK_NAME: ${{ github.event.client_payload.check_name || inputs.check_name }}
79
+ run: |
80
+ slug=$(printf '%s' "$CHECK_NAME" | tr -c 'a-zA-Z0-9' '-' | tr -s '-' | sed 's/^-//;s/-$//' | cut -c1-40 | tr '[:upper:]' '[:lower:]')
81
+ echo "slug=${slug:-check}" >> "$GITHUB_OUTPUT"
82
+
83
+ open=$(gh pr list --state open --label checkly-heal --json headRefName \
84
+ --jq "[.[] | select(.headRefName | startswith(\"checkly-heal/$slug\"))] | length")
85
+ if [ "$open" -gt 0 ]; then
86
+ echo "mode=skip" >> "$GITHUB_OUTPUT"
87
+ echo "A heal PR for this check is already open — nothing to do." >> "$GITHUB_STEP_SUMMARY"
88
+ exit 0
89
+ fi
90
+
91
+ cutoff=$(date -u -d "$REPEAT_WINDOW_DAYS days ago" +%Y-%m-%dT%H:%M:%SZ)
92
+ recent=$(gh pr list --state merged --label checkly-heal --json headRefName,mergedAt \
93
+ --jq "[.[] | select((.headRefName | startswith(\"checkly-heal/$slug\")) and .mergedAt > \"$cutoff\")] | length")
94
+ if [ "$recent" -gt 0 ]; then
95
+ echo "mode=repeat" >> "$GITHUB_OUTPUT"
96
+ exit 0
97
+ fi
98
+ echo "mode=heal" >> "$GITHUB_OUTPUT"
99
+
100
+ - name: Set up pnpm
101
+ if: steps.guard.outputs.mode == 'heal'
102
+ uses: pnpm/action-setup@v4
103
+
104
+ - name: Set up Node
105
+ if: steps.guard.outputs.mode == 'heal'
106
+ uses: actions/setup-node@v5
107
+ with:
108
+ node-version: 22
109
+ cache: pnpm
110
+
111
+ # --ignore-scripts: only the tests run here; project postinstall hooks
112
+ # are not needed and may want CMS tokens CI does not have.
113
+ - name: Install dependencies
114
+ if: steps.guard.outputs.mode == 'heal'
115
+ run: pnpm install --frozen-lockfile --ignore-scripts
116
+
117
+ - name: Install Playwright browsers
118
+ if: steps.guard.outputs.mode == 'heal'
119
+ run: pnpm playwright install --with-deps chromium
120
+
121
+ # Checkly already diagnosed the failure — error, failing step, trace.
122
+ # Best effort: the agent reproduces from scratch when this is missing.
123
+ - name: Fetch the Checkly result and write agent context
124
+ if: steps.guard.outputs.mode == 'heal'
125
+ env:
126
+ CHECKLY_API_KEY: ${{ secrets.CHECKLY_API_KEY }}
127
+ CHECKLY_ACCOUNT_ID: ${{ secrets.CHECKLY_ACCOUNT_ID }}
128
+ CHECK_NAME: ${{ github.event.client_payload.check_name || inputs.check_name }}
129
+ CHECK_ID: ${{ github.event.client_payload.check_id || inputs.check_id }}
130
+ RESULT_ID: ${{ github.event.client_payload.result_id || inputs.result_id }}
131
+ RESULT_LINK: ${{ github.event.client_payload.result_link }}
132
+ ALERT_TYPE: ${{ github.event.client_payload.alert_type }}
133
+ run: |
134
+ mkdir -p .checkly-heal
135
+ if [ -n "$CHECK_ID" ] && [ -n "$RESULT_ID" ]; then
136
+ curl -sf --max-time 30 \
137
+ -H "Authorization: Bearer $CHECKLY_API_KEY" \
138
+ -H "X-Checkly-Account: $CHECKLY_ACCOUNT_ID" \
139
+ "https://api.checklyhq.com/v1/check-results/$CHECK_ID/$RESULT_ID" \
140
+ -o .checkly-heal/result.json \
141
+ || echo "Checkly result fetch failed — agent reproduces from scratch."
142
+ fi
143
+ jq -n \
144
+ --arg check_name "$CHECK_NAME" \
145
+ --arg alert_type "${ALERT_TYPE:-ALERT_FAILURE}" \
146
+ --arg result_link "${RESULT_LINK:-}" \
147
+ --arg result_file "$([ -f .checkly-heal/result.json ] && echo .checkly-heal/result.json)" \
148
+ '{check_name: $check_name, alert_type: $alert_type, result_link: $result_link, result_file: $result_file}' \
149
+ > .checkly-heal/context.json
150
+
151
+ # The agent gets test-fixing tools and Checkly credentials (for
152
+ # pw-test verification) — deliberately no GitHub token: it cannot push,
153
+ # comment, or open PRs even if it tries.
154
+ - name: Run the triage-and-heal agent
155
+ if: steps.guard.outputs.mode == 'heal'
156
+ id: agent
157
+ timeout-minutes: 35
158
+ env:
159
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
160
+ CHECKLY_API_KEY: ${{ secrets.CHECKLY_API_KEY }}
161
+ CHECKLY_ACCOUNT_ID: ${{ secrets.CHECKLY_ACCOUNT_ID }}
162
+ run: |
163
+ npm install -g @anthropic-ai/claude-code@2
164
+ claude -p "$(cat .github/prompts/checkly-heal.md)" \
165
+ --model claude-opus-5 \
166
+ --allowedTools "Bash,Read,Edit,Write,Glob,Grep" \
167
+ --permission-mode dontAsk \
168
+ --max-turns 150 \
169
+ --output-format json \
170
+ > .checkly-heal/agent-output.json || true
171
+
172
+ # The guard is deterministic on purpose: the PR path requires an outcome
173
+ # file claiming a fix AND a diff clear of protected paths. App-source
174
+ # changes (test hooks) are allowed but derived from the diff, not the
175
+ # agent's claim, and flagged downstream. Everything else — protected
176
+ # paths, missing outcome, other classifications — escalates.
177
+ - name: Read the outcome and guard the diff
178
+ if: steps.guard.outputs.mode == 'heal'
179
+ id: outcome
180
+ run: |
181
+ classification="agent-failed"
182
+ summary="The heal agent did not produce an outcome."
183
+ if [ -f .checkly-heal/outcome.json ] && jq -e . .checkly-heal/outcome.json > /dev/null 2>&1; then
184
+ classification=$(jq -r '.classification // "agent-failed"' .checkly-heal/outcome.json)
185
+ summary=$(jq -r '.summary // ""' .checkly-heal/outcome.json)
186
+ fi
187
+
188
+ files=$(git status --porcelain | awk '{print $NF}' | grep -v "^\.checkly-heal/" || true)
189
+ protected=$(printf '%s' "$files" | grep -Ec "$PROTECTED_PATHS" || true)
190
+ changed=$(printf '%s' "$files" | grep -c . || true)
191
+ app_change=$(printf '%s' "$files" | grep -cv "^$E2E_ROOT/" || true)
192
+
193
+ if [ "$classification" = "healed" ] || [ "$classification" = "flaky" ]; then
194
+ if [ "$protected" -gt 0 ]; then
195
+ classification="agent-failed"
196
+ summary="Diff guard tripped: protected files (.github, .claude, configs, lockfiles) were changed. Nothing was pushed."
197
+ elif [ "$changed" -eq 0 ]; then
198
+ classification="agent-failed"
199
+ summary="The agent reported a fix but changed no files."
200
+ fi
201
+ fi
202
+
203
+ echo "app_change=$([ "$app_change" -gt 0 ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
204
+ echo "classification=$classification" >> "$GITHUB_OUTPUT"
205
+ {
206
+ echo "summary<<CHECKLY_HEAL_EOF"
207
+ echo "$summary"
208
+ echo "CHECKLY_HEAL_EOF"
209
+ } >> "$GITHUB_OUTPUT"
210
+ echo "## Checkly auto-heal: $classification" >> "$GITHUB_STEP_SUMMARY"
211
+ echo "$summary" >> "$GITHUB_STEP_SUMMARY"
212
+
213
+ - name: Open the heal PR for human review
214
+ if: >-
215
+ steps.guard.outputs.mode == 'heal' &&
216
+ (steps.outcome.outputs.classification == 'healed' ||
217
+ steps.outcome.outputs.classification == 'flaky')
218
+ id: pr
219
+ env:
220
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
221
+ CHECK_NAME: ${{ github.event.client_payload.check_name || inputs.check_name }}
222
+ RESULT_LINK: ${{ github.event.client_payload.result_link }}
223
+ SLUG: ${{ steps.guard.outputs.slug }}
224
+ CLASSIFICATION: ${{ steps.outcome.outputs.classification }}
225
+ APP_CHANGE: ${{ steps.outcome.outputs.app_change }}
226
+ run: |
227
+ branch="checkly-heal/$SLUG-${{ github.run_id }}"
228
+ git config user.name "github-actions[bot]"
229
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
230
+ git checkout -b "$branch"
231
+ git add -A -- ':(exclude).checkly-heal'
232
+ git commit -m "Heal Checkly check: $CHECK_NAME"
233
+ git push -u origin "$branch"
234
+
235
+ gh label create checkly-heal --force \
236
+ --description "Automated fix for a failing Checkly monitor" \
237
+ --color BFD4F2 || true
238
+
239
+ case "$CLASSIFICATION" in
240
+ flaky) kind="a flaky test (hardened, stability proven with repeated runs)" ;;
241
+ *) kind="a stale test" ;;
242
+ esac
243
+ {
244
+ echo "A Checkly monitor failed and the auto-heal agent classified it as $kind. **Human review required — do not merge unread.**"
245
+ echo ""
246
+ echo "- Check: \`$CHECK_NAME\`"
247
+ [ -n "$RESULT_LINK" ] && echo "- Failing result: $RESULT_LINK"
248
+ echo "- Heal run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
249
+ if [ "$APP_CHANGE" = "true" ]; then
250
+ echo ""
251
+ echo "> [!IMPORTANT]"
252
+ echo "> This fix includes app-source changes (behavior-neutral test hooks such as \`data-testid\`). Production does not carry them yet, so the authoritative validation is **this PR's own preview run** (pr-playwright-tests) — check it before merging."
253
+ fi
254
+ echo ""
255
+ jq -r '.details // ""' .checkly-heal/outcome.json
256
+ } > pr-body.md
257
+
258
+ url=$(gh pr create \
259
+ --title "Heal Checkly check: $CHECK_NAME" \
260
+ --body-file pr-body.md \
261
+ --label checkly-heal)
262
+ echo "url=$url" >> "$GITHUB_OUTPUT"
263
+
264
+ # The single alert path: every outcome that needs eyes lands in Slack,
265
+ # written after triage. Runs on step failure too (always() + the guard
266
+ # conditions), so a crashed agent still alerts.
267
+ - name: Notify Slack
268
+ if: always() && steps.guard.outputs.mode != 'skip'
269
+ env:
270
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
271
+ CHECK_NAME: ${{ github.event.client_payload.check_name || inputs.check_name }}
272
+ RESULT_LINK: ${{ github.event.client_payload.result_link }}
273
+ MODE: ${{ steps.guard.outputs.mode }}
274
+ CLASSIFICATION: ${{ steps.outcome.outputs.classification }}
275
+ SUMMARY: ${{ steps.outcome.outputs.summary }}
276
+ APP_CHANGE: ${{ steps.outcome.outputs.app_change }}
277
+ PR_URL: ${{ steps.pr.outputs.url }}
278
+ RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
279
+ run: |
280
+ case "${MODE}:${CLASSIFICATION}" in
281
+ repeat:*)
282
+ text=":rotating_light: *Checkly: \`$CHECK_NAME\` is failing again* — a heal PR for this check merged within the last $REPEAT_WINDOW_DAYS days, so no second automatic fix was attempted. This needs a developer." ;;
283
+ *:healed)
284
+ text=":wrench: *Checkly: \`$CHECK_NAME\` failed — fix ready for review.* $SUMMARY\nPR: $PR_URL" ;;
285
+ *:flaky)
286
+ text=":wrench: *Checkly: \`$CHECK_NAME\` was flaky — test hardened, fix ready for review.* $SUMMARY\nPR: $PR_URL" ;;
287
+ *:regression)
288
+ text=":rotating_light: *Checkly: \`$CHECK_NAME\` failed — production regression.* The test is correct; the site is broken. $SUMMARY" ;;
289
+ *:needs-app-change)
290
+ text=":warning: *Checkly: \`$CHECK_NAME\` failed — fix needs an app code change.* $SUMMARY" ;;
291
+ *:not-reproducible)
292
+ # Transient blip, nothing to act on — summary only, no ping.
293
+ echo "Not reproducible — no Slack notification." >> "$GITHUB_STEP_SUMMARY"
294
+ exit 0 ;;
295
+ *)
296
+ text=":rotating_light: *Checkly: \`$CHECK_NAME\` failed and the auto-heal run did not finish cleanly.* ${SUMMARY:-Check the run logs.} This needs a developer." ;;
297
+ esac
298
+ [ "$APP_CHANGE" = "true" ] && text="$text\nIncludes app-side test hooks — the PR's preview run validates them."
299
+ [ -n "$RESULT_LINK" ] && text="$text\nCheckly result: $RESULT_LINK"
300
+ text="$text\nRun: $RUN_URL"
301
+
302
+ if [ -z "$SLACK_WEBHOOK_URL" ]; then
303
+ echo "SLACK_WEBHOOK_URL is not set — notification skipped:" >> "$GITHUB_STEP_SUMMARY"
304
+ printf '%b\n' "$text" >> "$GITHUB_STEP_SUMMARY"
305
+ exit 0
306
+ fi
307
+ jq -n --arg text "$(printf '%b' "$text")" '{text: $text}' \
308
+ | curl -sf --max-time 15 -X POST -H "Content-Type: application/json" -d @- "$SLACK_WEBHOOK_URL" \
309
+ || echo "Slack notification failed — see summary." >> "$GITHUB_STEP_SUMMARY"
310
+
311
+ - name: Upload agent artifacts
312
+ if: always() && steps.guard.outputs.mode == 'heal'
313
+ uses: actions/upload-artifact@v4
314
+ with:
315
+ name: checkly-heal-${{ github.run_id }}
316
+ path: .checkly-heal/
317
+ if-no-files-found: ignore
318
+ retention-days: 14
@@ -92,6 +92,9 @@ jobs:
92
92
  env:
93
93
  BASE_URL: ${{ github.event.deployment_status.target_url }}
94
94
  VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
95
+ # Which production domain's locale config preview hosts resolve to
96
+ # (repo variable, optional — specs default to the primary domain).
97
+ PREVIEW_LOCALE_HOST: ${{ vars.PREVIEW_LOCALE_HOST }}
95
98
  PLAYWRIGHT_JSON_OUTPUT_NAME: playwright-results.json
96
99
  run: |
97
100
  pnpm playwright test \