@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 +6 -2
- package/dist/bin/init.js +10 -2
- package/dist/utils/copy.js +26 -0
- package/dist/utils/install.js +16 -4
- package/package.json +1 -1
- package/templates/claude/partials/_robustness-core.md +23 -2
- package/templates/claude/rules/playwright-workflow.md +17 -1
- package/templates/claude/skills/playwright-test-pipeline/SKILL.md +84 -20
- package/templates/github/prompts/checkly-heal.md +141 -0
- package/templates/github/workflows/checkly-heal.yml +318 -0
- package/templates/github/workflows/pr-playwright-tests.yml +3 -0
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
|
|
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
|
-
|
|
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
|
|
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";
|
package/dist/utils/copy.js
CHANGED
|
@@ -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/dist/utils/install.js
CHANGED
|
@@ -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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
179
|
-
failing monitor only shows in the Checkly dashboard
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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 \
|