@andwhy/e2e-pipeline 0.10.1 → 0.11.0

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,12 @@ 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 `ANTHROPIC_API_KEY` and
52
+ `SLACK_WEBHOOK_URL` (values in 1Password).
50
53
 
51
54
  E2E specs live under **`tests/e2e/`** so a repo can keep unit tests elsewhere under
52
55
  `tests/` without Playwright trying to run them. Seeds sit in the same tree and are
package/dist/bin/init.js CHANGED
@@ -107,9 +107,13 @@ const run = async () => {
107
107
  console.log([
108
108
  "\nGitHub workflows created. They need repository secrets before they run",
109
109
  "(values in 1Password):",
110
- " CHECKLY_API_KEY, CHECKLY_ACCOUNT_ID — deploy monitors on merge",
110
+ " CHECKLY_API_KEY, CHECKLY_ACCOUNT_ID — deploy monitors, auto-heal",
111
111
  " VERCEL_AUTOMATION_BYPASS_SECRET — PR tests through Vercel",
112
112
  " deployment protection",
113
+ " ANTHROPIC_API_KEY — Checkly auto-heal agent",
114
+ " SLACK_WEBHOOK_URL — auto-heal notifications",
115
+ "The auto-heal workflow stays inert until a Checkly webhook alert",
116
+ "channel dispatches to it — the skill walks through that setup.",
113
117
  ].join("\n"));
114
118
  }
115
119
  installDeps(projectRoot, fileFormat);
@@ -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
  };
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.0",
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",
@@ -201,3 +201,16 @@ If any tests fail:
201
201
  - **Plan files** are saved at `tests/e2e/<feature>/<name>.plan.md`.
202
202
  - **Test files** follow **Project test rules** §7–8 — typically `tests/e2e/<feature>/<group>.spec.js`
203
203
  or `.spec.ts` with one `describe` and multiple tests.
204
+
205
+ ## Checkly auto-heal (CI, not this session)
206
+
207
+ Failing deployed monitors are handled by `.github/workflows/checkly-heal.yml`:
208
+ it triages first (production regression → Slack alert; stale test → fix; flaky
209
+ test → harden), may add behavior-neutral test hooks (`data-testid`) to app
210
+ source when the robust fix needs them, and opens a `checkly-heal` PR a human
211
+ must review — app-side hooks are validated by the PR's own preview run. Its
212
+ agent instructions live in `.github/prompts/checkly-heal.md` (package-owned —
213
+ update via `init --force`, don't hand-edit). If a user asks about a
214
+ `checkly-heal` PR, that workflow authored it; reviewing one means verifying the
215
+ updated test still describes correct production behavior, not that it merely
216
+ 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,12 @@ 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
+ - `ANTHROPIC_API_KEY` — 1Password ("Anthropic"); powers the Checkly
92
+ auto-heal agent. Optional until Phase 5 wires the alert webhook, but
93
+ cheapest to add alongside the others.
94
+ - `SLACK_WEBHOOK_URL` — a Slack incoming webhook for the project's
95
+ notification channel; the auto-heal workflow posts its alerts there.
96
+ Ask which channel before creating one.
91
97
  5. **Existing tests under `tests/`** (unit, a11y — the scaffold lists them as
92
98
  "outside tests/e2e"): leave `testDir` at `./tests/e2e` and give that suite
93
99
  its own Playwright project with its own `testDir` if it should run under
@@ -148,7 +154,7 @@ until after merge.
148
154
 
149
155
  ## Phase 5 — Merge and deployment
150
156
 
151
- The scaffold ships two workflows (seeded once — existing ones are never
157
+ The scaffold ships three workflows (seeded once — existing ones are never
152
158
  overwritten):
153
159
 
154
160
  - `.github/workflows/pr-playwright-tests.yml` — runs the suite against the
@@ -161,13 +167,22 @@ overwritten):
161
167
  merge that adds tests for new `data-testid`s would go red until the release
162
168
  carrying them is live — with gated releases that gap can be days. A
163
169
  `workflow_dispatch` fallback allows manual runs.
170
+ - `.github/workflows/checkly-heal.yml` — a failing deployed monitor
171
+ dispatches here (via the webhook alert channel below). An agent triages
172
+ first: a production regression or anything needing a behavioral app change
173
+ alerts Slack immediately; a stale test gets fixed and a flaky test gets
174
+ hardened — including behavior-neutral test hooks (`data-testid`) in app
175
+ source when the robust fix needs them — and opened as a PR that a human
176
+ must review. Test-only fixes are verified against production and on
177
+ Checkly's infrastructure; app-side hooks are validated by the PR's own
178
+ preview run (pr-playwright-tests). Inert until the webhook exists.
164
179
 
165
180
  **Before the first merge, the repo needs secrets** (values in 1Password):
166
- `CHECKLY_API_KEY` and `CHECKLY_ACCOUNT_ID` for the deploy,
181
+ `CHECKLY_API_KEY` and `CHECKLY_ACCOUNT_ID` for the deploy and auto-heal,
167
182
  `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.
183
+ protection, `ANTHROPIC_API_KEY` and `SLACK_WEBHOOK_URL` for auto-heal. If
184
+ 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,63 @@ 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 `ANTHROPIC_API_KEY` and `SLACK_WEBHOOK_URL` repository
202
+ secrets from Phase 0 exist — the workflow escalates nothing without them.
203
+ 2. A GitHub token for Checkly to call `repository_dispatch`: fine-grained
204
+ PAT, scoped to this repo only, **Contents: read and write** permission,
205
+ the longest expiry policy allows — note the renewal date wherever the team
206
+ tracks credentials, an expired token silences the trigger. Store it in
207
+ Checkly as an environment variable (Checkly → Environment variables),
208
+ e.g. `GH_DISPATCH_TOKEN` — never in code or `checkly.config`.
209
+ 3. Checkly → Alert channels → "Add more channels" → **Webhook**:
210
+ - Method `POST`, URL
211
+ `https://api.github.com/repos/<org>/<repo>/dispatches`
212
+ - Headers: `Authorization: Bearer {{GH_DISPATCH_TOKEN}}` and
213
+ `Accept: application/vnd.github+json`
214
+ - Body (Checkly's editor lists the available variables — confirm the
215
+ names there):
216
+
217
+ ```json
218
+ {
219
+ "event_type": "checkly-alert",
220
+ "client_payload": {
221
+ "check_name": "{{CHECK_NAME}}",
222
+ "check_id": "{{CHECK_ID}}",
223
+ "result_id": "{{CHECK_RESULT_ID}}",
224
+ "result_link": "{{RESULT_LINK}}",
225
+ "alert_type": "{{ALERT_TYPE}}"
226
+ }
227
+ }
228
+ ```
229
+
230
+ - Notify on **failure**; enable **reminders** while a check stays failing
231
+ — a reminder re-fires the webhook, the workflow dedupes repeats, and a
232
+ lost first delivery gets retried by the next one. Recovery/degraded
233
+ notifications are ignored by the workflow either way.
234
+ 4. Subscriptions: **only the current check**, and **uncheck** "Automatically
235
+ subscribe newly created checks to this alert channel".
236
+ 5. **Do not also subscribe a Slack alert channel to these suites.** The
237
+ pipeline is the single alert path; a parallel raw Checkly alert double-pings
238
+ the channel and trains people to ignore one of them.
239
+ 6. Dry run: GitHub → Actions → "Checkly auto-heal" → Run workflow, with the
240
+ suite's check name. Confirm the run completes and the Slack message
241
+ arrives (a passing check should end as "not reproducible" — summary only,
242
+ no ping — so a real dry run is easiest right after any genuine failure).
243
+
244
+ What the user should expect afterwards: a failing monitor produces either a
245
+ `checkly-heal` PR plus a Slack note (stale or flaky test — **review
246
+ required, the PR is never merged automatically**; PRs carrying app-side test
247
+ hooks say so and point at their preview run for validation), or a Slack
248
+ escalation (regression, fix needs a behavioral app change, agent failed). A
249
+ transient blip that doesn't reproduce stays silent by design.
192
250
 
193
251
  ## Conventions to uphold
194
252
 
@@ -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,317 @@
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: ANTHROPIC_API_KEY, CHECKLY_API_KEY, CHECKLY_ACCOUNT_ID,
13
+ # SLACK_WEBHOOK_URL (values in 1Password). Setup docs: the
14
+ # playwright-test-pipeline skill walks through the Checkly webhook channel.
15
+ on:
16
+ repository_dispatch:
17
+ types: [checkly-alert]
18
+ workflow_dispatch:
19
+ inputs:
20
+ check_name:
21
+ description: "Failing check name (as in checkly.config)"
22
+ required: true
23
+ check_id:
24
+ description: "Checkly check ID (optional, enables result download)"
25
+ required: false
26
+ result_id:
27
+ description: "Checkly check result ID (optional)"
28
+ required: false
29
+
30
+ run-name: "Checkly auto-heal · ${{ github.event.client_payload.check_name || inputs.check_name }}"
31
+
32
+ permissions:
33
+ contents: write
34
+ pull-requests: write
35
+ issues: write
36
+
37
+ # One heal at a time; queued alerts (e.g. an outage failing several checks)
38
+ # run serially and the dedup below makes repeats cheap no-ops.
39
+ concurrency:
40
+ group: checkly-heal
41
+ cancel-in-progress: false
42
+
43
+ env:
44
+ # The e2e test root. Changes outside it (behavior-neutral test hooks in app
45
+ # source) are allowed but flagged on the PR and in Slack.
46
+ # Match your playwright.config testDir.
47
+ E2E_ROOT: tests/e2e
48
+ # Paths the agent must never change — anything here aborts and escalates.
49
+ # Extended regex against the changed-file list.
50
+ PROTECTED_PATHS: '^(\.github/|\.claude/|\.mcp\.json$|.*\.config\.(ts|mjs|js|cjs)$|package\.json$|pnpm-lock\.yaml$|package-lock\.json$|yarn\.lock$|\.env)'
51
+ # How long a merged heal PR blocks a second silent attempt on the same check.
52
+ REPEAT_WINDOW_DAYS: 7
53
+
54
+ jobs:
55
+ heal:
56
+ # Recovery/degraded notifications are not work; only failures are.
57
+ if: >-
58
+ github.event_name == 'workflow_dispatch' ||
59
+ contains(github.event.client_payload.alert_type || 'ALERT_FAILURE', 'FAILURE')
60
+ runs-on: ubuntu-latest
61
+ timeout-minutes: 45
62
+
63
+ steps:
64
+ - name: Check out the default branch
65
+ uses: actions/checkout@v5
66
+ with:
67
+ fetch-depth: 0
68
+
69
+ # Dedup and repeat-offender guard. An open heal PR for this check means
70
+ # the alert is already handled. A heal PR merged recently means the fix
71
+ # did not hold — a second silent attempt would mask instability, so that
72
+ # escalates to a human instead.
73
+ - name: Guard against duplicate and repeated heals
74
+ id: guard
75
+ env:
76
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
77
+ CHECK_NAME: ${{ github.event.client_payload.check_name || inputs.check_name }}
78
+ run: |
79
+ slug=$(printf '%s' "$CHECK_NAME" | tr -c 'a-zA-Z0-9' '-' | tr -s '-' | sed 's/^-//;s/-$//' | cut -c1-40 | tr '[:upper:]' '[:lower:]')
80
+ echo "slug=${slug:-check}" >> "$GITHUB_OUTPUT"
81
+
82
+ open=$(gh pr list --state open --label checkly-heal --json headRefName \
83
+ --jq "[.[] | select(.headRefName | startswith(\"checkly-heal/$slug\"))] | length")
84
+ if [ "$open" -gt 0 ]; then
85
+ echo "mode=skip" >> "$GITHUB_OUTPUT"
86
+ echo "A heal PR for this check is already open — nothing to do." >> "$GITHUB_STEP_SUMMARY"
87
+ exit 0
88
+ fi
89
+
90
+ cutoff=$(date -u -d "$REPEAT_WINDOW_DAYS days ago" +%Y-%m-%dT%H:%M:%SZ)
91
+ recent=$(gh pr list --state merged --label checkly-heal --json headRefName,mergedAt \
92
+ --jq "[.[] | select((.headRefName | startswith(\"checkly-heal/$slug\")) and .mergedAt > \"$cutoff\")] | length")
93
+ if [ "$recent" -gt 0 ]; then
94
+ echo "mode=repeat" >> "$GITHUB_OUTPUT"
95
+ exit 0
96
+ fi
97
+ echo "mode=heal" >> "$GITHUB_OUTPUT"
98
+
99
+ - name: Set up pnpm
100
+ if: steps.guard.outputs.mode == 'heal'
101
+ uses: pnpm/action-setup@v4
102
+
103
+ - name: Set up Node
104
+ if: steps.guard.outputs.mode == 'heal'
105
+ uses: actions/setup-node@v5
106
+ with:
107
+ node-version: 22
108
+ cache: pnpm
109
+
110
+ # --ignore-scripts: only the tests run here; project postinstall hooks
111
+ # are not needed and may want CMS tokens CI does not have.
112
+ - name: Install dependencies
113
+ if: steps.guard.outputs.mode == 'heal'
114
+ run: pnpm install --frozen-lockfile --ignore-scripts
115
+
116
+ - name: Install Playwright browsers
117
+ if: steps.guard.outputs.mode == 'heal'
118
+ run: pnpm playwright install --with-deps chromium
119
+
120
+ # Checkly already diagnosed the failure — error, failing step, trace.
121
+ # Best effort: the agent reproduces from scratch when this is missing.
122
+ - name: Fetch the Checkly result and write agent context
123
+ if: steps.guard.outputs.mode == 'heal'
124
+ env:
125
+ CHECKLY_API_KEY: ${{ secrets.CHECKLY_API_KEY }}
126
+ CHECKLY_ACCOUNT_ID: ${{ secrets.CHECKLY_ACCOUNT_ID }}
127
+ CHECK_NAME: ${{ github.event.client_payload.check_name || inputs.check_name }}
128
+ CHECK_ID: ${{ github.event.client_payload.check_id || inputs.check_id }}
129
+ RESULT_ID: ${{ github.event.client_payload.result_id || inputs.result_id }}
130
+ RESULT_LINK: ${{ github.event.client_payload.result_link }}
131
+ ALERT_TYPE: ${{ github.event.client_payload.alert_type }}
132
+ run: |
133
+ mkdir -p .checkly-heal
134
+ if [ -n "$CHECK_ID" ] && [ -n "$RESULT_ID" ]; then
135
+ curl -sf --max-time 30 \
136
+ -H "Authorization: Bearer $CHECKLY_API_KEY" \
137
+ -H "X-Checkly-Account: $CHECKLY_ACCOUNT_ID" \
138
+ "https://api.checklyhq.com/v1/check-results/$CHECK_ID/$RESULT_ID" \
139
+ -o .checkly-heal/result.json \
140
+ || echo "Checkly result fetch failed — agent reproduces from scratch."
141
+ fi
142
+ jq -n \
143
+ --arg check_name "$CHECK_NAME" \
144
+ --arg alert_type "${ALERT_TYPE:-ALERT_FAILURE}" \
145
+ --arg result_link "${RESULT_LINK:-}" \
146
+ --arg result_file "$([ -f .checkly-heal/result.json ] && echo .checkly-heal/result.json)" \
147
+ '{check_name: $check_name, alert_type: $alert_type, result_link: $result_link, result_file: $result_file}' \
148
+ > .checkly-heal/context.json
149
+
150
+ # The agent gets test-fixing tools and Checkly credentials (for
151
+ # pw-test verification) — deliberately no GitHub token: it cannot push,
152
+ # comment, or open PRs even if it tries.
153
+ - name: Run the triage-and-heal agent
154
+ if: steps.guard.outputs.mode == 'heal'
155
+ id: agent
156
+ timeout-minutes: 35
157
+ env:
158
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
159
+ CHECKLY_API_KEY: ${{ secrets.CHECKLY_API_KEY }}
160
+ CHECKLY_ACCOUNT_ID: ${{ secrets.CHECKLY_ACCOUNT_ID }}
161
+ run: |
162
+ npm install -g @anthropic-ai/claude-code@2
163
+ claude -p "$(cat .github/prompts/checkly-heal.md)" \
164
+ --model claude-opus-5 \
165
+ --allowedTools "Bash,Read,Edit,Write,Glob,Grep" \
166
+ --permission-mode dontAsk \
167
+ --max-turns 150 \
168
+ --output-format json \
169
+ > .checkly-heal/agent-output.json || true
170
+
171
+ # The guard is deterministic on purpose: the PR path requires an outcome
172
+ # file claiming a fix AND a diff clear of protected paths. App-source
173
+ # changes (test hooks) are allowed but derived from the diff, not the
174
+ # agent's claim, and flagged downstream. Everything else — protected
175
+ # paths, missing outcome, other classifications — escalates.
176
+ - name: Read the outcome and guard the diff
177
+ if: steps.guard.outputs.mode == 'heal'
178
+ id: outcome
179
+ run: |
180
+ classification="agent-failed"
181
+ summary="The heal agent did not produce an outcome."
182
+ if [ -f .checkly-heal/outcome.json ] && jq -e . .checkly-heal/outcome.json > /dev/null 2>&1; then
183
+ classification=$(jq -r '.classification // "agent-failed"' .checkly-heal/outcome.json)
184
+ summary=$(jq -r '.summary // ""' .checkly-heal/outcome.json)
185
+ fi
186
+
187
+ files=$(git status --porcelain | awk '{print $NF}' | grep -v "^\.checkly-heal/" || true)
188
+ protected=$(printf '%s' "$files" | grep -Ec "$PROTECTED_PATHS" || true)
189
+ changed=$(printf '%s' "$files" | grep -c . || true)
190
+ app_change=$(printf '%s' "$files" | grep -cv "^$E2E_ROOT/" || true)
191
+
192
+ if [ "$classification" = "healed" ] || [ "$classification" = "flaky" ]; then
193
+ if [ "$protected" -gt 0 ]; then
194
+ classification="agent-failed"
195
+ summary="Diff guard tripped: protected files (.github, .claude, configs, lockfiles) were changed. Nothing was pushed."
196
+ elif [ "$changed" -eq 0 ]; then
197
+ classification="agent-failed"
198
+ summary="The agent reported a fix but changed no files."
199
+ fi
200
+ fi
201
+
202
+ echo "app_change=$([ "$app_change" -gt 0 ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
203
+ echo "classification=$classification" >> "$GITHUB_OUTPUT"
204
+ {
205
+ echo "summary<<CHECKLY_HEAL_EOF"
206
+ echo "$summary"
207
+ echo "CHECKLY_HEAL_EOF"
208
+ } >> "$GITHUB_OUTPUT"
209
+ echo "## Checkly auto-heal: $classification" >> "$GITHUB_STEP_SUMMARY"
210
+ echo "$summary" >> "$GITHUB_STEP_SUMMARY"
211
+
212
+ - name: Open the heal PR for human review
213
+ if: >-
214
+ steps.guard.outputs.mode == 'heal' &&
215
+ (steps.outcome.outputs.classification == 'healed' ||
216
+ steps.outcome.outputs.classification == 'flaky')
217
+ id: pr
218
+ env:
219
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
220
+ CHECK_NAME: ${{ github.event.client_payload.check_name || inputs.check_name }}
221
+ RESULT_LINK: ${{ github.event.client_payload.result_link }}
222
+ SLUG: ${{ steps.guard.outputs.slug }}
223
+ CLASSIFICATION: ${{ steps.outcome.outputs.classification }}
224
+ APP_CHANGE: ${{ steps.outcome.outputs.app_change }}
225
+ run: |
226
+ branch="checkly-heal/$SLUG-${{ github.run_id }}"
227
+ git config user.name "github-actions[bot]"
228
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
229
+ git checkout -b "$branch"
230
+ git add -A -- ':(exclude).checkly-heal'
231
+ git commit -m "Heal Checkly check: $CHECK_NAME"
232
+ git push -u origin "$branch"
233
+
234
+ gh label create checkly-heal --force \
235
+ --description "Automated fix for a failing Checkly monitor" \
236
+ --color BFD4F2 || true
237
+
238
+ case "$CLASSIFICATION" in
239
+ flaky) kind="a flaky test (hardened, stability proven with repeated runs)" ;;
240
+ *) kind="a stale test" ;;
241
+ esac
242
+ {
243
+ echo "A Checkly monitor failed and the auto-heal agent classified it as $kind. **Human review required — do not merge unread.**"
244
+ echo ""
245
+ echo "- Check: \`$CHECK_NAME\`"
246
+ [ -n "$RESULT_LINK" ] && echo "- Failing result: $RESULT_LINK"
247
+ echo "- Heal run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
248
+ if [ "$APP_CHANGE" = "true" ]; then
249
+ echo ""
250
+ echo "> [!IMPORTANT]"
251
+ 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."
252
+ fi
253
+ echo ""
254
+ jq -r '.details // ""' .checkly-heal/outcome.json
255
+ } > pr-body.md
256
+
257
+ url=$(gh pr create \
258
+ --title "Heal Checkly check: $CHECK_NAME" \
259
+ --body-file pr-body.md \
260
+ --label checkly-heal)
261
+ echo "url=$url" >> "$GITHUB_OUTPUT"
262
+
263
+ # The single alert path: every outcome that needs eyes lands in Slack,
264
+ # written after triage. Runs on step failure too (always() + the guard
265
+ # conditions), so a crashed agent still alerts.
266
+ - name: Notify Slack
267
+ if: always() && steps.guard.outputs.mode != 'skip'
268
+ env:
269
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
270
+ CHECK_NAME: ${{ github.event.client_payload.check_name || inputs.check_name }}
271
+ RESULT_LINK: ${{ github.event.client_payload.result_link }}
272
+ MODE: ${{ steps.guard.outputs.mode }}
273
+ CLASSIFICATION: ${{ steps.outcome.outputs.classification }}
274
+ SUMMARY: ${{ steps.outcome.outputs.summary }}
275
+ APP_CHANGE: ${{ steps.outcome.outputs.app_change }}
276
+ PR_URL: ${{ steps.pr.outputs.url }}
277
+ RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
278
+ run: |
279
+ case "${MODE}:${CLASSIFICATION}" in
280
+ repeat:*)
281
+ 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." ;;
282
+ *:healed)
283
+ text=":wrench: *Checkly: \`$CHECK_NAME\` failed — fix ready for review.* $SUMMARY\nPR: $PR_URL" ;;
284
+ *:flaky)
285
+ text=":wrench: *Checkly: \`$CHECK_NAME\` was flaky — test hardened, fix ready for review.* $SUMMARY\nPR: $PR_URL" ;;
286
+ *:regression)
287
+ text=":rotating_light: *Checkly: \`$CHECK_NAME\` failed — production regression.* The test is correct; the site is broken. $SUMMARY" ;;
288
+ *:needs-app-change)
289
+ text=":warning: *Checkly: \`$CHECK_NAME\` failed — fix needs an app code change.* $SUMMARY" ;;
290
+ *:not-reproducible)
291
+ # Transient blip, nothing to act on — summary only, no ping.
292
+ echo "Not reproducible — no Slack notification." >> "$GITHUB_STEP_SUMMARY"
293
+ exit 0 ;;
294
+ *)
295
+ 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." ;;
296
+ esac
297
+ [ "$APP_CHANGE" = "true" ] && text="$text\nIncludes app-side test hooks — the PR's preview run validates them."
298
+ [ -n "$RESULT_LINK" ] && text="$text\nCheckly result: $RESULT_LINK"
299
+ text="$text\nRun: $RUN_URL"
300
+
301
+ if [ -z "$SLACK_WEBHOOK_URL" ]; then
302
+ echo "SLACK_WEBHOOK_URL is not set — notification skipped:" >> "$GITHUB_STEP_SUMMARY"
303
+ printf '%b\n' "$text" >> "$GITHUB_STEP_SUMMARY"
304
+ exit 0
305
+ fi
306
+ jq -n --arg text "$(printf '%b' "$text")" '{text: $text}' \
307
+ | curl -sf --max-time 15 -X POST -H "Content-Type: application/json" -d @- "$SLACK_WEBHOOK_URL" \
308
+ || echo "Slack notification failed — see summary." >> "$GITHUB_STEP_SUMMARY"
309
+
310
+ - name: Upload agent artifacts
311
+ if: always() && steps.guard.outputs.mode == 'heal'
312
+ uses: actions/upload-artifact@v4
313
+ with:
314
+ name: checkly-heal-${{ github.run_id }}
315
+ path: .checkly-heal/
316
+ if-no-files-found: ignore
317
+ retention-days: 14