@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.) The auto-heal workflow stays inert until a Checkly",
121
- "webhook alert channel dispatches to it — the skill walks through that.",
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.12.1",
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 organization-level
96
- secret inherited by every repo. Do not tell the user to add it.
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
- Run only the failing spec against production:
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** → run it again with `--repeat-each=3`. All green means the
71
- failure was transient on Checkly's side: classification `not-reproducible`,
72
- stop, change nothing. Intermittent failures mean the test is **flaky** —
73
- continue to triage with that hypothesis.
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. Classification `flaky` — proceed to
88
- step 4 and fix the root cause per the robustness rules; blanket timeout
89
- increases are not a fix.
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 — for a
117
- flaky fix, prove stability with `--repeat-each=5`, not a single pass. Then
118
- run the full e2e suite once the same way to catch collateral damage, and
119
- validate on Checkly's infrastructure:
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. Slack messages carry an [e2e:<repo>]
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: $?" >> "$GITHUB_STEP_SUMMARY"
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
- } >> "$GITHUB_STEP_SUMMARY"
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
- 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." ;;
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
- text=":wrench: *Checkly: \`$CHECK_NAME\` failed — fix ready for review.* $SUMMARY\nPR: $PR_URL" ;;
318
+ title=":wrench: *\`$CHECK_NAME\` failed — fix ready for review*"
319
+ body="$SUMMARY" ;;
297
320
  *:flaky)
298
- text=":wrench: *Checkly: \`$CHECK_NAME\` was flaky — test hardened, fix ready for review.* $SUMMARY\nPR: $PR_URL" ;;
321
+ title=":wrench: *\`$CHECK_NAME\` was flaky — test hardened, fix ready for review*"
322
+ body="$SUMMARY" ;;
299
323
  *:regression)
300
- text=":rotating_light: *Checkly: \`$CHECK_NAME\` failed — production regression.* The test is correct; the site is broken. $SUMMARY" ;;
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
- text=":warning: *Checkly: \`$CHECK_NAME\` failed — fix needs an app code change.* $SUMMARY" ;;
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
- 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." ;;
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
- # Routing tag: Slack Workflow Builder rules match the full bracketed
311
- # tag ([e2e:<repo>]) to fan messages out from the shared e2e channel
312
- # to project channels without substring collisions.
313
- text="[e2e:${GITHUB_REPOSITORY##*/}] $text"
314
- [ "$APP_CHANGE" = "true" ] && text="$text\nIncludes app-side test hooks — the PR's preview run validates them."
315
- [ -n "$RESULT_LINK" ] && text="$text\nCheckly result: $RESULT_LINK"
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
- jq -n --arg text "$(printf '%b' "$text")" '{text: $text}' \
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