@andwhy/e2e-pipeline 0.12.1 → 0.13.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/dist/bin/init.js
CHANGED
|
@@ -117,8 +117,10 @@ const run = async () => {
|
|
|
117
117
|
" deployment protection",
|
|
118
118
|
" SLACK_WEBHOOK_URL — auto-heal notifications",
|
|
119
119
|
"(ANTHROPIC_API_KEY is an org-level secret inherited by all repos —",
|
|
120
|
-
"nothing to add
|
|
121
|
-
"
|
|
120
|
+
"nothing to add; if the heal agent fails to authenticate, a repo",
|
|
121
|
+
"secret of the same name overrides it.) The auto-heal workflow stays",
|
|
122
|
+
"inert until a Checkly webhook alert channel dispatches to it — the",
|
|
123
|
+
"skill walks through that.",
|
|
122
124
|
].join("\n"));
|
|
123
125
|
}
|
|
124
126
|
console.log("Next steps:");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andwhy/e2e-pipeline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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",
|
|
@@ -92,8 +92,13 @@ The scaffold writes templates, not a working setup. Walk these with the user:
|
|
|
92
92
|
value from 1Password ("e2e Slack webhook"). Same value in every repo;
|
|
93
93
|
do not walk the user through creating a Slack app or webhook — routing
|
|
94
94
|
to the project channel happens via a Slack rule in Phase 5.
|
|
95
|
-
- `ANTHROPIC_API_KEY` needs **no action**: it is an
|
|
96
|
-
secret inherited by every repo
|
|
95
|
+
- `ANTHROPIC_API_KEY` usually needs **no action**: it is an
|
|
96
|
+
organization-level secret inherited by every repo — do not walk the
|
|
97
|
+
user through adding it up front. But it can fail to reach a repo
|
|
98
|
+
(org-secret scoping, or a stale stored value): if a heal run's agent
|
|
99
|
+
step fails to authenticate, have the user add a repository-level
|
|
100
|
+
`ANTHROPIC_API_KEY` — a repo secret with the same name overrides the
|
|
101
|
+
organization one.
|
|
97
102
|
5. **Existing tests under `tests/`** (unit, a11y — the scaffold lists them as
|
|
98
103
|
"outside tests/e2e"): leave `testDir` at `./tests/e2e` and give that suite
|
|
99
104
|
its own Playwright project with its own `testDir` if it should run under
|
|
@@ -181,7 +186,9 @@ overwritten):
|
|
|
181
186
|
`CHECKLY_API_KEY` and `CHECKLY_ACCOUNT_ID` for the deploy and auto-heal,
|
|
182
187
|
`VERCEL_AUTOMATION_BYPASS_SECRET` for the PR run through deployment
|
|
183
188
|
protection, `SLACK_WEBHOOK_URL` for auto-heal notifications
|
|
184
|
-
(`ANTHROPIC_API_KEY` is inherited from the organization — nothing to add
|
|
189
|
+
(`ANTHROPIC_API_KEY` is inherited from the organization — nothing to add
|
|
190
|
+
unless a heal run fails to authenticate; then a repo-level secret of the
|
|
191
|
+
same name overrides it, see Phase 0).
|
|
185
192
|
If the scaffold just created the workflows, say so explicitly and list the
|
|
186
193
|
secrets — a merge without them fails CI and looks like a pipeline bug.
|
|
187
194
|
|
|
@@ -201,7 +208,9 @@ Set up once per project:
|
|
|
201
208
|
|
|
202
209
|
1. Confirm the `SLACK_WEBHOOK_URL` repository secret from Phase 0 exists —
|
|
203
210
|
the workflow escalates nothing without it. (`ANTHROPIC_API_KEY` comes
|
|
204
|
-
from the organization automatically
|
|
211
|
+
from the organization automatically — but if the dry run in step 6 ends
|
|
212
|
+
agent-failed with an authentication error, override it with a repo-level
|
|
213
|
+
secret of the same name, per Phase 0.)
|
|
205
214
|
2. The shared `GH_DISPATCH_TOKEN` secret exists in Checkly and covers this
|
|
206
215
|
repo. This is **one-time organization setup, not yours to perform**: the
|
|
207
216
|
token (`checkly-heal-dispatch`, a fine-grained PAT) is created once and
|
|
@@ -43,6 +43,12 @@ negotiable:
|
|
|
43
43
|
workflow reads your outcome from it after you finish, and destroying it
|
|
44
44
|
turns your entire run into an unexplained failure. No `rm -rf`, no
|
|
45
45
|
`git clean`. Only add files to it.
|
|
46
|
+
6. **Runtime is metered — stop the moment the classification is certain.**
|
|
47
|
+
You run under a hard 35-minute step timeout, and every minute is paid CI
|
|
48
|
+
and API time. Run the narrowest thing that answers the current question,
|
|
49
|
+
never the full suite, and never re-run a test whose answer you already
|
|
50
|
+
have. If the deadline nears, write an honest partial outcome instead of
|
|
51
|
+
pushing on — a run killed by the timeout reports nothing.
|
|
46
52
|
|
|
47
53
|
## Procedure
|
|
48
54
|
|
|
@@ -60,17 +66,20 @@ run "wasn't real".
|
|
|
60
66
|
|
|
61
67
|
### 2. Reproduce
|
|
62
68
|
|
|
63
|
-
|
|
69
|
+
Identify the narrowest reproduction: the failing spec and test named by
|
|
70
|
+
`result.json` when present; without a result file, the spec files belonging
|
|
71
|
+
to the failing check. Run that once against production, stopping at the
|
|
72
|
+
first failure:
|
|
64
73
|
|
|
65
74
|
```bash
|
|
66
|
-
BASE_URL=<production-url> pnpm playwright test <failing-spec> --project=Chrome
|
|
75
|
+
BASE_URL=<production-url> pnpm playwright test <failing-spec> --project=Chrome --max-failures=1
|
|
67
76
|
```
|
|
68
77
|
|
|
69
78
|
- **Fails** → continue to triage.
|
|
70
|
-
- **Passes** →
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
- **Passes** → classification `not-reproducible`. Write the outcome and stop
|
|
80
|
+
immediately — no repeat runs, no wider investigation. Checkly saw it fail
|
|
81
|
+
and it passes now, so the blip was transient; a genuinely unstable test
|
|
82
|
+
will alert again, and that repetition is the signal a human acts on.
|
|
74
83
|
|
|
75
84
|
### 3. Triage — before any edit
|
|
76
85
|
|
|
@@ -84,9 +93,10 @@ the live page:
|
|
|
84
93
|
behavior hides an outage.
|
|
85
94
|
- **The test is flaky.** The outcome works, but the test fails
|
|
86
95
|
intermittently — races, missing waits for dynamic content, animation
|
|
87
|
-
timing, order-dependent assertions.
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
timing, order-dependent assertions. When the trace alone doesn't settle
|
|
97
|
+
it, repeating the one failing test is the only permitted repeat run.
|
|
98
|
+
Classification `flaky` — proceed to step 4 and fix the root cause per the
|
|
99
|
+
robustness rules; blanket timeout increases are not a fix.
|
|
90
100
|
- **The test is stale.** Production changed legitimately (new markup, changed
|
|
91
101
|
copy, moved element) and the test's selectors or assertions no longer
|
|
92
102
|
describe it, while the actual user outcome still works. Classification
|
|
@@ -113,10 +123,11 @@ that make the test robust.
|
|
|
113
123
|
|
|
114
124
|
### 5. Verify
|
|
115
125
|
|
|
116
|
-
- **Test-only fixes:** re-run the spec against production until green —
|
|
117
|
-
flaky fix, prove stability with `--repeat-each=5
|
|
118
|
-
|
|
119
|
-
|
|
126
|
+
- **Test-only fixes:** re-run the fixed spec against production until green —
|
|
127
|
+
for a flaky fix, prove stability with `--repeat-each=5` on that one spec,
|
|
128
|
+
not a single pass. Do not run the full suite (override 6): the heal PR gets
|
|
129
|
+
its own `pr-playwright-tests` preview run, and that is where collateral
|
|
130
|
+
damage surfaces. Validate the fixed spec on Checkly's infrastructure:
|
|
120
131
|
|
|
121
132
|
```bash
|
|
122
133
|
pnpm checkly pw-test --env BASE_URL="<production-url>" -- <failing-spec>
|
|
@@ -11,7 +11,9 @@ name: Checkly auto-heal
|
|
|
11
11
|
# commit, PR, and Slack are workflow steps.
|
|
12
12
|
# Needs repo secrets CHECKLY_API_KEY, CHECKLY_ACCOUNT_ID, SLACK_WEBHOOK_URL
|
|
13
13
|
# (the org-shared e2e-channel webhook; values in 1Password); ANTHROPIC_API_KEY
|
|
14
|
-
# is inherited from the organization
|
|
14
|
+
# is inherited from the organization (a repo secret of the same name
|
|
15
|
+
# overrides it if the org value fails to authenticate). Slack messages
|
|
16
|
+
# carry an [e2e:<repo>]
|
|
15
17
|
# tag so Slack Workflow Builder rules route them from the shared channel to
|
|
16
18
|
# project channels. Setup docs: the playwright-test-pipeline skill walks
|
|
17
19
|
# through the Checkly webhook channel and the Slack rule.
|
|
@@ -165,36 +167,55 @@ jobs:
|
|
|
165
167
|
npm install -g @anthropic-ai/claude-code@2
|
|
166
168
|
# Transcript lives OUTSIDE .checkly-heal so it survives whatever the
|
|
167
169
|
# agent does to its working directory, and outside the repo tree so
|
|
168
|
-
# the diff guard never counts it.
|
|
170
|
+
# the diff guard never counts it. stream-json writes each event as it
|
|
171
|
+
# happens — a run killed by the timeout still leaves the full history
|
|
172
|
+
# (plain json buffers everything and a kill leaves an empty file).
|
|
169
173
|
claude -p "$(cat .github/prompts/checkly-heal.md)" \
|
|
170
174
|
--model claude-opus-5 \
|
|
171
175
|
--allowedTools "Bash,Read,Edit,Write,Glob,Grep" \
|
|
172
176
|
--permission-mode dontAsk \
|
|
173
177
|
--max-turns 150 \
|
|
174
|
-
--output-format json \
|
|
175
|
-
> /tmp/checkly-heal-agent-output.json || echo "claude exited non-zero: $?"
|
|
178
|
+
--output-format stream-json --verbose \
|
|
179
|
+
> /tmp/checkly-heal-agent-output.json || echo "claude exited non-zero: $?" | tee -a "$GITHUB_STEP_SUMMARY"
|
|
180
|
+
# Diagnostics go to the job log AND the summary: the log is readable
|
|
181
|
+
# over the API, the summary only in the browser.
|
|
176
182
|
{
|
|
177
183
|
echo "### Agent step diagnostics"
|
|
178
184
|
echo '```'
|
|
179
185
|
ls -la .checkly-heal 2>&1 || echo ".checkly-heal missing"
|
|
180
186
|
tail -c 2000 /tmp/checkly-heal-agent-output.json 2>/dev/null || echo "no agent output"
|
|
181
187
|
echo '```'
|
|
182
|
-
}
|
|
188
|
+
} | tee -a "$GITHUB_STEP_SUMMARY"
|
|
183
189
|
|
|
184
190
|
# The guard is deterministic on purpose: the PR path requires an outcome
|
|
185
191
|
# file claiming a fix AND a diff clear of protected paths. App-source
|
|
186
192
|
# changes (test hooks) are allowed but derived from the diff, not the
|
|
187
193
|
# agent's claim, and flagged downstream. Everything else — protected
|
|
188
194
|
# paths, missing outcome, other classifications — escalates.
|
|
195
|
+
# always(): a killed agent step (timeout, cancel) must still produce a
|
|
196
|
+
# classification, or Slack reports an unexplained failure.
|
|
189
197
|
- name: Read the outcome and guard the diff
|
|
190
|
-
if: steps.guard.outputs.mode == 'heal'
|
|
198
|
+
if: always() && steps.guard.outputs.mode == 'heal'
|
|
191
199
|
id: outcome
|
|
200
|
+
env:
|
|
201
|
+
AGENT_CONCLUSION: ${{ steps.agent.conclusion }}
|
|
192
202
|
run: |
|
|
193
203
|
classification="agent-failed"
|
|
194
204
|
summary="The heal agent did not produce an outcome."
|
|
195
|
-
if [ -f .checkly-heal/outcome.json ] && jq -e . .checkly-heal/outcome.json > /dev/null 2>&1; then
|
|
205
|
+
if [ "$AGENT_CONCLUSION" = "success" ] && [ -f .checkly-heal/outcome.json ] && jq -e . .checkly-heal/outcome.json > /dev/null 2>&1; then
|
|
196
206
|
classification=$(jq -r '.classification // "agent-failed"' .checkly-heal/outcome.json)
|
|
197
207
|
summary=$(jq -r '.summary // ""' .checkly-heal/outcome.json)
|
|
208
|
+
else
|
|
209
|
+
# No trustworthy outcome — say WHY, so the Slack message diagnoses
|
|
210
|
+
# itself. An outcome written before a kill is unverified: escalate
|
|
211
|
+
# rather than open a PR for it.
|
|
212
|
+
if [ "$AGENT_CONCLUSION" = "skipped" ]; then
|
|
213
|
+
summary="The heal agent never ran — a setup step failed before it."
|
|
214
|
+
elif [ "$AGENT_CONCLUSION" != "success" ]; then
|
|
215
|
+
summary="The heal agent was killed before finishing (step conclusion: ${AGENT_CONCLUSION:-unknown} — usually the 35-minute timeout)."
|
|
216
|
+
fi
|
|
217
|
+
agent_error=$(jq -r 'select(.is_error == true) | .result // empty' /tmp/checkly-heal-agent-output.json 2>/dev/null | head -c 300)
|
|
218
|
+
[ -n "$agent_error" ] && summary="$summary Agent error: $agent_error"
|
|
198
219
|
fi
|
|
199
220
|
|
|
200
221
|
files=$(git status --porcelain | awk '{print $NF}' | grep -v "^\.checkly-heal/" || true)
|
|
@@ -291,28 +312,48 @@ jobs:
|
|
|
291
312
|
run: |
|
|
292
313
|
case "${MODE}:${CLASSIFICATION}" in
|
|
293
314
|
repeat:*)
|
|
294
|
-
|
|
315
|
+
title=":rotating_light: *\`$CHECK_NAME\` is failing again*"
|
|
316
|
+
body="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." ;;
|
|
295
317
|
*:healed)
|
|
296
|
-
|
|
318
|
+
title=":wrench: *\`$CHECK_NAME\` failed — fix ready for review*"
|
|
319
|
+
body="$SUMMARY" ;;
|
|
297
320
|
*:flaky)
|
|
298
|
-
|
|
321
|
+
title=":wrench: *\`$CHECK_NAME\` was flaky — test hardened, fix ready for review*"
|
|
322
|
+
body="$SUMMARY" ;;
|
|
299
323
|
*:regression)
|
|
300
|
-
|
|
324
|
+
title=":rotating_light: *\`$CHECK_NAME\` failed — production regression*"
|
|
325
|
+
body="The test is correct; the site is broken. $SUMMARY" ;;
|
|
301
326
|
*:needs-app-change)
|
|
302
|
-
|
|
327
|
+
title=":warning: *\`$CHECK_NAME\` failed — fix needs an app code change*"
|
|
328
|
+
body="$SUMMARY" ;;
|
|
303
329
|
*:not-reproducible)
|
|
304
330
|
# Transient blip, nothing to act on — summary only, no ping.
|
|
305
331
|
echo "Not reproducible — no Slack notification." >> "$GITHUB_STEP_SUMMARY"
|
|
306
332
|
exit 0 ;;
|
|
307
333
|
*)
|
|
308
|
-
|
|
334
|
+
title=":rotating_light: *\`$CHECK_NAME\` failed and the auto-heal run did not finish cleanly*"
|
|
335
|
+
body="${SUMMARY:-Check the run logs.} This needs a developer." ;;
|
|
309
336
|
esac
|
|
310
|
-
|
|
311
|
-
#
|
|
312
|
-
#
|
|
313
|
-
|
|
314
|
-
[ "$
|
|
315
|
-
|
|
337
|
+
|
|
338
|
+
# Repeat mode skips the agent, so there is no outcome classification;
|
|
339
|
+
# label it as what it is rather than defaulting to agent-failed.
|
|
340
|
+
label="${CLASSIFICATION:-agent-failed}"
|
|
341
|
+
[ "$MODE" = "repeat" ] && label="repeat-offender"
|
|
342
|
+
|
|
343
|
+
# Failing test titles, best effort from the Checkly result's embedded
|
|
344
|
+
# Playwright report — the recursive search tolerates schema changes,
|
|
345
|
+
# and an empty result simply omits the field.
|
|
346
|
+
failed_tests=""
|
|
347
|
+
if [ -f .checkly-heal/result.json ]; then
|
|
348
|
+
failed_tests=$(jq -r '[.. | objects | select((.status? == "failed") or (.outcome? == "unexpected")) | .title? // empty] | unique | .[0:3] | join(", ")' .checkly-heal/result.json 2>/dev/null || true)
|
|
349
|
+
fi
|
|
350
|
+
|
|
351
|
+
# Plain-text fallback: what notifications, search, and the Slack
|
|
352
|
+
# routing rule see — the Workflow Builder rule matches the [e2e:<repo>]
|
|
353
|
+
# tag here and forwards this text, so it stays a complete message.
|
|
354
|
+
repo="${GITHUB_REPOSITORY##*/}"
|
|
355
|
+
text="[e2e:$repo] $title — $body"
|
|
356
|
+
[ -n "$PR_URL" ] && text="$text\nPR: $PR_URL"
|
|
316
357
|
text="$text\nRun: $RUN_URL"
|
|
317
358
|
|
|
318
359
|
if [ -z "$SLACK_WEBHOOK_URL" ]; then
|
|
@@ -320,7 +361,51 @@ jobs:
|
|
|
320
361
|
printf '%b\n' "$text" >> "$GITHUB_STEP_SUMMARY"
|
|
321
362
|
exit 0
|
|
322
363
|
fi
|
|
323
|
-
|
|
364
|
+
|
|
365
|
+
# Block Kit body: title, classification/repo fields, failing tests
|
|
366
|
+
# when known, the diagnosis, and link buttons assembled per outcome —
|
|
367
|
+
# a button never renders without its target.
|
|
368
|
+
jq -n \
|
|
369
|
+
--arg text "$(printf '%b' "$text")" \
|
|
370
|
+
--arg title "$title" \
|
|
371
|
+
--arg body "$body" \
|
|
372
|
+
--arg classification "$label" \
|
|
373
|
+
--arg repo "$repo" \
|
|
374
|
+
--arg failed "$failed_tests" \
|
|
375
|
+
--arg app_change "$APP_CHANGE" \
|
|
376
|
+
--arg pr "$PR_URL" \
|
|
377
|
+
--arg run "$RUN_URL" \
|
|
378
|
+
--arg result "$RESULT_LINK" \
|
|
379
|
+
--arg rerun "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/workflows/checkly-heal.yml" \
|
|
380
|
+
'
|
|
381
|
+
def btn($label; $url): {type: "button", text: {type: "plain_text", text: $label}, url: $url};
|
|
382
|
+
{
|
|
383
|
+
text: $text,
|
|
384
|
+
blocks: (
|
|
385
|
+
[
|
|
386
|
+
{type: "section", text: {type: "mrkdwn", text: $title}},
|
|
387
|
+
{type: "section", fields: ([
|
|
388
|
+
{type: "mrkdwn", text: ("*Classification*\n" + $classification)},
|
|
389
|
+
{type: "mrkdwn", text: ("*Repository*\n" + $repo)}
|
|
390
|
+
] + (if $failed != "" then
|
|
391
|
+
[{type: "mrkdwn", text: ("*Failed test*\n" + $failed)}]
|
|
392
|
+
else [] end))}
|
|
393
|
+
]
|
|
394
|
+
+ (if $body != "" then
|
|
395
|
+
[{type: "section", text: {type: "mrkdwn", text: $body}}]
|
|
396
|
+
else [] end)
|
|
397
|
+
+ (if $app_change == "true" then
|
|
398
|
+
[{type: "context", elements: [{type: "mrkdwn", text: "Includes app-side test hooks — the PR'\''s preview run validates them."}]}]
|
|
399
|
+
else [] end)
|
|
400
|
+
+ [{type: "actions", elements: (
|
|
401
|
+
(if $pr != "" then [btn("Review PR"; $pr)] else [] end)
|
|
402
|
+
+ [btn("View run"; $run)]
|
|
403
|
+
+ (if $result != "" then [btn("Checkly result"; $result)] else [] end)
|
|
404
|
+
+ (if $classification == "agent-failed" then [btn("Re-run heal"; $rerun)] else [] end)
|
|
405
|
+
)}]
|
|
406
|
+
+ [{type: "context", elements: [{type: "mrkdwn", text: ("[e2e:" + $repo + "]")}]}]
|
|
407
|
+
)
|
|
408
|
+
}' \
|
|
324
409
|
| curl -sf --max-time 15 -X POST -H "Content-Type: application/json" -d @- "$SLACK_WEBHOOK_URL" \
|
|
325
410
|
|| echo "Slack notification failed — see summary." >> "$GITHUB_STEP_SUMMARY"
|
|
326
411
|
|
|
@@ -332,5 +417,8 @@ jobs:
|
|
|
332
417
|
path: |
|
|
333
418
|
.checkly-heal/
|
|
334
419
|
/tmp/checkly-heal-agent-output.json
|
|
420
|
+
# .checkly-heal is a dot-directory; without this it is silently
|
|
421
|
+
# excluded and the debug artifact loses context/outcome/result.
|
|
422
|
+
include-hidden-files: true
|
|
335
423
|
if-no-files-found: ignore
|
|
336
424
|
retention-days: 14
|