playwright-e2e-mcp 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,12 @@
1
1
  # playwright-e2e-mcp
2
2
 
3
- [![CI](https://github.com/trajectiq-ai/E2E/actions/workflows/ci.yml/badge.svg)](https://github.com/trajectiq-ai/E2E/actions/workflows/ci.yml) [![release](https://img.shields.io/github/v/release/trajectiq-ai/E2E)](https://github.com/trajectiq-ai/E2E/releases) [![MCP Registry](https://img.shields.io/badge/MCP%20Registry-io.github.trajectiq--ai%2FE2E-2563eb)](https://registry.modelcontextprotocol.io/)
3
+ [![CI](https://github.com/xsignon-secure/E2E/actions/workflows/ci.yml/badge.svg)](https://github.com/xsignon-secure/E2E/actions/workflows/ci.yml) [![release](https://img.shields.io/github/v/release/xsignon-secure/E2E)](https://github.com/xsignon-secure/E2E/releases) [![MCP Registry](https://img.shields.io/badge/MCP%20Registry-io.github.xsignon--secure%2FE2E-2563eb)](https://registry.modelcontextprotocol.io/)
4
4
  [![M8ven Score](https://m8ven.ai/badge/mcp/trajectiq-ai/e2e)](https://m8ven.ai/mcp/trajectiq-ai/e2e?s=readme)
5
5
 
6
+ [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=playwright-e2e&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsInBsYXl3cmlnaHQtZTJlLW1jcCJdfQ==)
7
+ [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_server-0098FF?logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright-e2e%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522playwright-e2e-mcp%2522%255D%257D)
8
+ [![Add to Claude Desktop](https://img.shields.io/badge/Claude_Desktop-.mcpb_bundle-D97757)](https://github.com/xsignon-secure/E2E/releases/latest)
9
+
6
10
  An [MCP](https://modelcontextprotocol.io) server that **diagnoses and fixes failing Playwright
7
11
  tests**. It is the layer that sits next to a browser-driving MCP: Playwright MCP gives an agent
8
12
  hands in the browser; this server gives it a test runner, a failure analyst and a verified fixer.
@@ -26,9 +30,13 @@ What it does that a browser-driving MCP or a raw `npx playwright test` does not:
26
30
  (`getByRole('button', { name: 'Proceed to checkout' })`), re-runs the test and reverts the
27
31
  edit if it is still red. When nothing on the page plausibly matches, it says the element is
28
32
  missing and does not patch the test to hide a real bug.
29
- - **Flakiness across runs.** Every run is recorded locally; `analyze-history` separates
30
- tests that flip (flaky) from ones that started failing (regressed) or always fail (broken),
31
- and groups failures that share one cause.
33
+ - **Flakiness with evidence.** `diagnose-flaky` repeats failing tests in one process and
34
+ gives a verdict per test; with `isolate: true` it re-runs them alone to tell a shared-state
35
+ bug that only bites in parallel from a race inside the test. Every run is recorded locally;
36
+ `analyze-history` separates tests that flip (flaky) from ones that started failing
37
+ (regressed) or always fail (broken), and groups failures that share one cause.
38
+ - **In CI too.** A [GitHub Action and `ci` command](#ci-github-action-and-ci-command) post the
39
+ same diagnosis, flaky verdict and fix diffs to the job summary and the PR.
32
40
  - **Pages behind a login.** Every page tool accepts a Playwright `storageState`, extra
33
41
  headers, and click/fill steps to reach a modal or a later step.
34
42
  - **Small on the wire.** In the [benchmark](bench/RESULTS.md), diagnosing and fixing one
@@ -48,27 +56,47 @@ work side by side in the same client.
48
56
  `https://playwright-e2e-mcp.vercel.app/api/mcp` returns for a client that sends the
49
57
  bearer token (without one, only `list-tests` and `get-failure` are listed):
50
58
 
51
- ![Token-protected endpoint: initialize handshake and the tool list](https://raw.githubusercontent.com/trajectiq-ai/E2E/main/docs/demo-endpoint.png)
59
+ ![Token-protected endpoint: initialize handshake and the tool list](https://raw.githubusercontent.com/xsignon-secure/E2E/main/docs/demo-endpoint.png)
52
60
 
53
61
  **A real test run** — `run-test` served over stdio by `npx -y playwright-e2e-mcp`
54
62
  against the bundled `examples/sample-test.spec.ts` (actual output, unedited):
55
63
 
56
- ![run-test result: 4 passed, 0 failed, 5.1s](https://raw.githubusercontent.com/trajectiq-ai/E2E/main/docs/demo-run-test.png)
64
+ ![run-test result: 4 passed, 0 failed, 5.1s](https://raw.githubusercontent.com/xsignon-secure/E2E/main/docs/demo-run-test.png)
57
65
 
58
66
  Images are rendered with `node scripts/gen-demo-images.mjs`: the run-test card is real
59
67
  captured output; the endpoint card is an illustration of the authenticated listing.
60
68
 
69
+ > If this saves you a debugging session, a ⭐ on the repo helps other Playwright users find it.
70
+
61
71
  ## Install
62
72
 
63
73
  Works with Claude Desktop, Claude Code, Cursor, Windsurf, Codex, Gemini CLI, Freebuff
64
74
  and every other MCP client — pick whichever route fits:
65
75
 
76
+ **Let your agent do it.** Paste this into Claude Code, Cursor, Codex, Gemini CLI or any
77
+ other coding agent:
78
+
79
+ > Install the playwright-e2e-mcp server and skill by following
80
+ > https://raw.githubusercontent.com/xsignon-secure/E2E/main/llms-install.md
81
+
82
+ or run the installer yourself from the project root. It merges the server into each
83
+ client's project config it finds (Claude Code, Cursor, VS Code, Gemini CLI, Codex) and
84
+ installs the agent skill, leaving your other servers alone:
85
+
86
+ ```bash
87
+ npx -y playwright-e2e-mcp init # --client all | cursor,vscode … · --dry-run
88
+ ```
89
+
66
90
  | Route | How |
67
91
  | --- | --- |
92
+ | **One command, any project** | `npx -y playwright-e2e-mcp init` |
93
+ | **Claude Code plugin (server + skill)** | `/plugin marketplace add xsignon-secure/E2E` then `/plugin install playwright-e2e-mcp@xsignon-secure` |
94
+ | **Gemini CLI extension (server + skill)** | `gemini extensions install https://github.com/xsignon-secure/E2E` |
95
+ | **Skill only, any agent** | `npx skills add xsignon-secure/E2E` |
68
96
  | **npm (canonical, fastest)** | `npx -y playwright-e2e-mcp` |
69
- | **MCP Registry** (registry-aware clients discover it automatically) | `io.github.trajectiq-ai/E2E` — [listing](https://registry.modelcontextprotocol.io/) |
70
- | **Any client, no npm account needed** | `npx -y github:trajectiq-ai/E2E#v0.2.2` (pin a release tag) |
71
- | **Claude Desktop, zero Node setup** | double-click the [`.mcpb` extension](https://github.com/trajectiq-ai/E2E/releases) |
97
+ | **MCP Registry** (registry-aware clients discover it automatically) | `io.github.xsignon-secure/E2E` — [listing](https://registry.modelcontextprotocol.io/) |
98
+ | **Any client, no npm account needed** | `npx -y github:xsignon-secure/E2E#v0.3.1` (pin a release tag) |
99
+ | **Claude Desktop, zero Node setup** | double-click the [`.mcpb` extension](https://github.com/xsignon-secure/E2E/releases) |
72
100
  | **Remote-only clients (ChatGPT connectors)** | `https://playwright-e2e-mcp.vercel.app/api/mcp` |
73
101
 
74
102
  One line for the common clients:
@@ -78,6 +106,9 @@ claude mcp add playwright-e2e -- npx -y playwright-e2e-mcp
78
106
  code --add-mcp '{"name":"playwright-e2e","command":"npx","args":["-y","playwright-e2e-mcp"]}'
79
107
  ```
80
108
 
109
+ Only need the fix loop? `--tools=core` (or `PW_MCP_TOOLS=core`) exposes six tools and
110
+ halves the tool-definition context; see [Configuration](#configuration).
111
+
81
112
  Details and per-client config: [Installation](#installation) ·
82
113
  [MCP client configuration](#mcp-client-configuration).
83
114
 
@@ -283,14 +314,21 @@ and write a red-highlighted diff image for review.
283
314
  | Argument | Type | Description |
284
315
  | --- | --- | --- |
285
316
  | `testFiles` | string[] | Tests to diagnose (`file:line` supported). Defaults to the tests that failed in the most recent run |
286
- | `runs` | number | Times to run them, `2–10` (default `3`) |
317
+ | `runs` | number | Repetitions, `2–10` (default `3`) |
318
+ | `isolate` | boolean | Also tell parallelism bugs from flakiness: repeat fully parallel (`workers` default `4`), then repeat the failing tests with one worker |
319
+ | `separateProcesses` | boolean | Start a fresh Playwright process per repetition instead of one `--repeat-each` process (slower; for flakes that depend on global setup or a cold start) |
287
320
  | `browser` / `headed` / `workers` / `config` | — | As with `run-test` |
288
- | `timeoutMs` | number | Hard wall-clock limit **per run** (default `120000`) |
321
+ | `timeoutMs` | number | Hard wall-clock limit **per repetition** (default `120000`); one `--repeat-each` process gets this × `runs` |
289
322
  | `projectRoot` | string | Project directory |
290
323
 
291
- Every run executes with `--retries=0` and auto-retry disabled, so each result is
292
- honest evidence. The response contains a per-run table (status, duration, first
293
- failure), the count of **distinct normalized error signatures**, and one of:
324
+ All repetitions run in **one Playwright process** with `--repeat-each` and
325
+ `--retries=0`, so each attempt is honest evidence and the browser and workers
326
+ start once instead of once per run (about 25% faster on the integration fixture,
327
+ more for suites with expensive setup). A small extra reporter records every
328
+ attempt, because Playwright's JSON report keeps only one result per test.
329
+
330
+ The verdict is **per test**: a table of passes, failures, duration spread and
331
+ first error for each test, plus the verdict block for the worst one:
294
332
 
295
333
  - **❌ CONSISTENTLY FAILING** — failed every run (same error → reproducible bug,
296
334
  different errors → still broken, just noisy). Fix it; it is not flaky.
@@ -299,7 +337,17 @@ failure), the count of **distinct normalized error signatures**, and one of:
299
337
  instability).
300
338
  - **✅ NOT REPRODUCING** — passed every re-run; the original failure was one-off.
301
339
 
302
- The last run is stored, so `get-failure` can analyze it immediately afterwards.
340
+ With `isolate: true` a second table compares each failing test **in parallel**
341
+ and **alone**:
342
+
343
+ | Reading | Meaning |
344
+ | --- | --- |
345
+ | 🔀 fails only in parallel | Passes every run alone: shared state between tests (same account, DB rows, files, ports). Give each worker its own data or run the file serially |
346
+ | ⏱️ flaky even alone | The race is inside the test or the app: missing `await`, asserting before the UI settles |
347
+ | ❌ fails alone too | Not a parallelism problem; a real bug |
348
+
349
+ Each repetition is recorded in the run history, and the last failing run is
350
+ stored, so `analyze-history` and `get-failure` work right afterwards.
303
351
 
304
352
  ### `suggest-fix`
305
353
 
@@ -367,11 +415,11 @@ Requirements:
367
415
 
368
416
  **No npm account needed** — install straight from GitHub (the `prepare` script
369
417
  builds `dist/` automatically on install). Pin a release tag: an unpinned
370
- `github:trajectiq-ai/E2E` runs whatever is on the default branch at that moment.
418
+ `github:xsignon-secure/E2E` runs whatever is on the default branch at that moment.
371
419
 
372
420
  ```bash
373
- npx -y github:trajectiq-ai/E2E#v0.2.2
374
- npm install -D github:trajectiq-ai/E2E#v0.2.2 @playwright/test # or as a project dependency
421
+ npx -y github:xsignon-secure/E2E#v0.3.1
422
+ npm install -D github:xsignon-secure/E2E#v0.3.1 @playwright/test # or as a project dependency
375
423
  npx playwright install chromium
376
424
  ```
377
425
 
@@ -379,11 +427,11 @@ Or grab the packaged tarball from the repo's **GitHub Releases** page and instal
379
427
  it locally:
380
428
 
381
429
  ```bash
382
- npm install -D https://github.com/trajectiq-ai/E2E/releases/download/v0.2.2/playwright-e2e-mcp-0.2.2.tgz
430
+ npm install -D https://github.com/xsignon-secure/E2E/releases/download/v0.3.1/playwright-e2e-mcp-0.3.1.tgz
383
431
  ```
384
432
 
385
433
  Listed in the **official [MCP Registry](https://registry.modelcontextprotocol.io/)** as
386
- `io.github.trajectiq-ai/E2E` — registry-aware clients discover it there, and every
434
+ `io.github.xsignon-secure/E2E` — registry-aware clients discover it there, and every
387
435
  `v*` release tag republishes the entry from CI via [`server.json`](server.json).
388
436
 
389
437
  ### MCP client configuration
@@ -395,7 +443,7 @@ Listed in the **official [MCP Registry](https://registry.modelcontextprotocol.io
395
443
  "mcpServers": {
396
444
  "playwright-e2e": {
397
445
  "command": "npx",
398
- "args": ["-y", "github:trajectiq-ai/E2E#v0.2.2"],
446
+ "args": ["-y", "github:xsignon-secure/E2E#v0.3.1"],
399
447
  "env": { "PW_MCP_PROJECT_ROOT": "/absolute/path/to/your/project" }
400
448
  }
401
449
  }
@@ -409,8 +457,8 @@ when the client launches it somewhere else (e.g. your home directory).
409
457
  **Codex / VS Code / Copilot CLIs:**
410
458
 
411
459
  ```bash
412
- codex mcp add playwright-e2e -- npx -y github:trajectiq-ai/E2E#v0.2.2
413
- code --add-mcp '{"name":"playwright-e2e","command":"npx","args":["-y","github:trajectiq-ai/E2E#v0.2.2"]}'
460
+ codex mcp add playwright-e2e -- npx -y github:xsignon-secure/E2E#v0.3.1
461
+ code --add-mcp '{"name":"playwright-e2e","command":"npx","args":["-y","github:xsignon-secure/E2E#v0.3.1"]}'
414
462
  ```
415
463
 
416
464
  Codex's defaults fight this server: the first launch clones the repo and runs
@@ -421,13 +469,13 @@ Codex's defaults fight this server: the first launch clones the repo and runs
421
469
  ```toml
422
470
  [mcp_servers.playwright-e2e]
423
471
  command = "npx"
424
- args = ["-y", "github:trajectiq-ai/E2E#v0.2.2"]
472
+ args = ["-y", "github:xsignon-secure/E2E#v0.3.1"]
425
473
  startup_timeout_sec = 60
426
474
  tool_timeout_sec = 600
427
475
  ```
428
476
 
429
477
  **Claude Desktop (one-click):** download and double-click the `.mcpb` Desktop
430
- Extension attached to the [latest release](https://github.com/trajectiq-ai/E2E/releases) —
478
+ Extension attached to the [latest release](https://github.com/xsignon-secure/E2E/releases) —
431
479
  the bundle ships its own dependencies, so no Node setup is required. On install it
432
480
  asks you to pick your **project root** (required, no default: choose the project
433
481
  folder, not your home directory) and wires it
@@ -436,7 +484,7 @@ into `PW_MCP_PROJECT_ROOT`, so the tools point at a real project from the first
436
484
  **Claude Code:**
437
485
 
438
486
  ```bash
439
- claude mcp add playwright-e2e -- npx -y github:trajectiq-ai/E2E#v0.2.2
487
+ claude mcp add playwright-e2e -- npx -y github:xsignon-secure/E2E#v0.3.1
440
488
  ```
441
489
 
442
490
  **Gemini CLI / Qwen Code:** paste the `mcpServers` block above into
@@ -545,11 +593,60 @@ Redeploy after a change: merge to `main`. Vercel's Git integration deploys
545
593
  every push, so there is no token to manage and no CLI step — watch the
546
594
  `Vercel` commit status for the deployment result.
547
595
 
596
+ ## CI: GitHub Action and `ci` command
597
+
598
+ The same diagnosis runs without an agent. `playwright-e2e-mcp ci` runs the
599
+ suite (auto-retry once, so flaky tests are reported as flaky, not failed),
600
+ optionally drafts `suggest-fix` diffs and a flaky verdict for the failures, and
601
+ writes one Markdown report to stdout, to `--output`, and to the GitHub job
602
+ summary when `GITHUB_STEP_SUMMARY` is set. Exit code `0` passed, `1` tests
603
+ failed, `2` the run itself failed.
604
+
605
+ ```bash
606
+ npx -y playwright-e2e-mcp ci --flaky-runs 3 --suggest 2 --output report.md
607
+ npx -y playwright-e2e-mcp ci --help
608
+ ```
609
+
610
+ As a GitHub Action, it adds the report to the job summary and keeps one PR
611
+ comment up to date when the run fails:
612
+
613
+ ```yaml
614
+ permissions:
615
+ contents: read
616
+ pull-requests: write # only for the PR comment
617
+ steps:
618
+ - uses: actions/checkout@v6
619
+ - uses: actions/setup-node@v6
620
+ with: { node-version: 24 }
621
+ - run: npm ci && npx playwright install --with-deps chromium
622
+ - uses: xsignon-secure/E2E@v0.3.1
623
+ with:
624
+ flaky-runs: 3 # 0 skips the flaky verdict
625
+ suggest: 2 # suggest-fix diffs for the first 2 failures
626
+ # args: --browser chromium tests/checkout
627
+ ```
628
+
629
+ Inputs: `project-root`, `args`, `flaky-runs`, `suggest`, `comment`,
630
+ `fail-on-failure`, `version`, `github-token`. Outputs: `exit-code`, `report`.
631
+
632
+ ## Docker
633
+
634
+ ```bash
635
+ docker build -t playwright-e2e-mcp .
636
+ docker run -i --rm -v "$PWD:/work" playwright-e2e-mcp # stdio MCP server
637
+ docker run --rm -v "$PWD:/work" playwright-e2e-mcp ci # CI report
638
+ ```
639
+
640
+ The image is based on `mcr.microsoft.com/playwright:v1.63.0-noble`, so its
641
+ browsers match Playwright 1.63. Mount your project at `/work`; its own
642
+ `node_modules` provide `@playwright/test`.
643
+
548
644
  ## Configuration
549
645
 
550
646
  | Environment variable | Default | Purpose |
551
647
  | --- | --- | --- |
552
648
  | `PW_MCP_PROJECT_ROOT` | server cwd | Default project root for every tool |
649
+ | `PW_MCP_TOOLS` | all | Which tools to expose, as tool names and/or profiles, comma separated (same as the `--tools=` flag). Profiles: `core` (run, status, failure, fix, flaky, list), `triage` (run, status, failure, flaky, history, list), `inspect` (page, selector, visual), `author` (list, page, selector, generate, run). All eleven tool definitions cost about 5.2k tokens of context per request; `core` costs about 2.4k |
553
650
  | `PW_MCP_STORAGE_STATE` | — | Default `storageState` file (inside the project) for the page tools |
554
651
  | `PW_MCP_HISTORY` | on | `0` stops recording runs to `.playwright-e2e-mcp/history.jsonl` |
555
652
  | `PW_MCP_ALLOWED_ROOTS` | — | Extra directories a caller may pass as `projectRoot` (`:`-separated, `;` on Windows). Anything outside these and the default root is rejected |
@@ -686,6 +783,9 @@ npm run e2e # build + e2e/run.mjs: live MCP ↔ Playwright integration suite
686
783
  npm run mcpb # build + bundle dist/ into playwright-e2e-mcp-<version>.mcpb (+ .sha256)
687
784
  npm run mcpb:smoke # ...then install that bundle in a temp dir, launch it and assert the handshake
688
785
  npm run mcpb:sign # ...then sign it with MCPB_CERT/MCPB_KEY → <version>.signed.mcpb
786
+ npm run skill:zip # just the archive: dist-skill/playwright-e2e-mcp.zip
787
+ npm run skill:package # the uploadable folder: ../agensi-skill-<version>/ (skill copy, README, zip)
788
+ npm run skill:check # build, then fail if a packaged copy has drifted from skill/
689
789
  ```
690
790
 
691
791
  Tests cover the report parser (sample Playwright JSON, trace attachments), path utils
@@ -695,11 +795,15 @@ helpers, the **trace reader** (synthetic trace.zip: error, failed action, DOM sn
695
795
  `*.network` failed-request parsing, console error/warning events), the **image diff**
696
796
  (PNG round-trip, regions, color shift, dimension changes), the **change analyzer**
697
797
  (selector extraction, git + mtime paths), the **flaky verdict logic**
698
- (failure signatures, CONSISTENTLY FAILING / FLAKY / NOT REPRODUCING / NO TESTS RAN),
798
+ (failure signatures, CONSISTENTLY FAILING / FLAKY / NOT REPRODUCING / NO TESTS RAN,
799
+ per-test grouping of repeat attempts and the parallel-vs-alone reading), the `ci`
800
+ argument parser and tool profiles,
699
801
  the **HTTP bridge** (token, restricted mode, Host allowlist), the **security
700
802
  regressions** (sandbox escapes, argument smuggling, symlink writes, code injection,
701
803
  SSRF ranges, env scrubbing, decode limits), the `.mcpb` manifest, the `.mcpb` zip
702
- extractor, and the MCP config files.
804
+ extractor, the **skill package** (byte-identical rebuilds, a CRLF checkout hashing the
805
+ same, refusal of junk/binary/symlinks, and drift detection between `skill/` and a
806
+ packaged copy), and the MCP config files.
703
807
 
704
808
  ### Integration suite (`npm run e2e`)
705
809
 
@@ -719,7 +823,10 @@ that only appear end-to-end:
719
823
  - `get-failure` trace diagnostics: DOM at failure, the 404 network request,
720
824
  the `console.error` message, diagnosis and next steps,
721
825
  - auto-retry turning a first-run failure into `PASSED (1 flaky)`,
722
- - `diagnose-flaky` verdict **FLAKY** (2 of 3 runs) with retries disabled,
826
+ - `diagnose-flaky` verdict **FLAKY** (2 of 3 runs) with retries disabled, in one
827
+ `--repeat-each` process, and `isolate` reading a shared-slot test as "fails only in parallel",
828
+ - `--tools=` profiles trimming `tools/list`, and the `ci` command's exit code, report,
829
+ `--output` file and GitHub job summary,
723
830
  - `generate-e2e-test` writing its scaffold, plus error paths
724
831
  (missing test path, unknown tool, unreachable server).
725
832
 
@@ -753,24 +860,137 @@ but not third-party identity. A certificate issued by a public CA, or an
753
860
  internal one installed on your machines, is what makes hosts report it as
754
861
  signed.
755
862
 
863
+ ### Assembling a release
864
+
865
+ ```bash
866
+ npm run mcpb && npm pack # the bundle, then the npm tarball
867
+ npm run release:assemble -- --tag v0.2.3 # sign, checksum and write the notes
868
+ npm run release:publish -- --tag v0.2.3 --assets dist-release/assets \
869
+ --title-file dist-release/RELEASE_TITLE.txt \
870
+ --body-file dist-release/RELEASE_BODY.md --download dist-release/downloaded
871
+ npm run release:verify -- v0.2.3 --dir dist-release/downloaded
872
+ npm run release:publish -- --tag v0.2.3 --publish
873
+ ```
874
+
875
+ Pushing a `v*` tag runs exactly that sequence in
876
+ `.github/workflows/release.yml`. The release used to be assembled by hand, and
877
+ the notes were written from what those commands printed: every hash in them was
878
+ a copy, and nothing compared the bundle hash printed there with the bundle that
879
+ shipped.
880
+
881
+ `scripts/assemble-release.mjs` stages the whole release into
882
+ `dist-release/assets/`: the bundle and its sidecar, the **signed** bundle and the
883
+ signing certificate when a key is configured, the npm tarball, the skill package
884
+ built by the same writer as `npm run skill:zip`, and `SHA256SUMS.txt` covering
885
+ every file it staged. It empties that directory first, so a file left over from
886
+ an earlier run cannot be hashed and published as if this run had produced it.
887
+
888
+ The notes are half written, half generated. `release-notes/<tag>.md` holds the
889
+ prose — its H1 becomes the release title and has to name the tag — and the script
890
+ appends the asset table, the reproduction recipe carrying the real bundle hash,
891
+ the signer and fingerprint, and the commit list. The prose is held to the same
892
+ rule: a 64-character hex string in it has to be a hash this release publishes,
893
+ and writing an `### Assets` section of your own fails the assembly rather than
894
+ shipping a second, staler table. See [release-notes/README.md](release-notes/README.md).
895
+
896
+ `scripts/publish-release.mjs` fills the release **as a draft**, uploads the
897
+ assets, downloads every one of them back and compares it byte for byte, and only
898
+ `--publish` makes it public — after `release:verify` has re-derived the release
899
+ from the tag. Re-running is safe: a draft this workflow assembled is refreshed in
900
+ place, while a release it did not assemble, or one already published, is refused
901
+ by name instead of overwritten.
902
+
903
+ Signing is optional and its absence is stated rather than hidden: without the
904
+ `MCPB_KEY` and `MCPB_CERT` repository secrets the release is assembled and
905
+ published **unsigned**, with a notice in the run and a line saying so in the
906
+ notes. Add both — the private key stays out of the repository and out of the logs:
907
+
908
+ ```bash
909
+ gh secret set MCPB_KEY --repo xsignon-secure/E2E < ~/signing/key.pem
910
+ gh secret set MCPB_CERT --repo xsignon-secure/E2E < ~/signing/cert.pem
911
+ ```
912
+
913
+ Creating the release and uploading its assets needs `contents: write`, which the
914
+ job requests for itself; if it is refused, the run names the setting to change.
915
+
756
916
  ### Verifying a published release
757
917
 
758
918
  ```bash
759
- git checkout v0.2.2 && npm ci && npm run mcpb
760
- npm run release:verify -- v0.2.2
919
+ git checkout v0.3.1 && npm ci && npm run mcpb
920
+ npm run release:verify -- v0.3.1
761
921
  ```
762
922
 
763
923
  The release notes promise that the tag rebuilds the published bundle byte for
764
924
  byte. `scripts/verify-release.mjs` checks that promise from a checkout of the
765
925
  tag: the published bundle against your fresh build, every checksum the release
766
926
  publishes (and that nothing shippable is missing from `SHA256SUMS.txt`), a
767
- **signed** bundle against the bytes its signature wraps, and that the published
768
- archive still extracts with that version's manifest.
769
- `.github/workflows/verify-release.yml` runs it on every published release and on
770
- demand for any past tag. The `.tgz` is checked for download integrity only: npm
927
+ **signed** bundle against the bytes its signature wraps, the **skill package**
928
+ against a rebuild from the same tag, and that the published archive still
929
+ extracts with that version's manifest. The skill package is required of a tag
930
+ whose tree carries the publishing workflow; older tags predate it, so there it
931
+ is checked when present and skipped when not, like the signing certificate.
932
+ `.github/workflows/verify-release.yml` runs it on demand for any past tag, and by
933
+ itself for a release published by hand. A release assembled by
934
+ `.github/workflows/release.yml` runs the same check on its own assets *before*
935
+ it is published — GitHub does not raise `release: published` for a release created
936
+ with the repository's own token, so the check is a step there rather than a
937
+ trigger. The `.tgz` is checked for download integrity only: npm
771
938
  records the packer's line endings, so a tarball packed locally on another OS is
772
939
  legitimately different.
773
940
 
941
+ ### Packaging the agent skill
942
+
943
+ ```bash
944
+ npm run skill:package # ../agensi-skill-<version>/, ready to upload
945
+ npm run skill:zip # only the archive: dist-skill/playwright-e2e-mcp.zip
946
+ npm run skill:check # build, then compare every copy
947
+ npm run skill:check -- ../agensi-skill-0.3.0
948
+ ```
949
+
950
+ `skill/` is an [Agent Skill](https://agentskills.io/specification) — the
951
+ instructions an agent follows to drive this server — and the marketplace gets
952
+ the same Markdown as a zip with `playwright-e2e-mcp/` at its root.
953
+ `scripts/package-skill.mjs` produces that zip, so the uploaded copy is built
954
+ rather than assembled by hand, and it refuses what the marketplace rejects: a
955
+ missing `SKILL.md`, a non-text file, a symlink, `Thumbs.db`.
956
+
957
+ `skill:package` writes the whole folder a release would otherwise assemble by
958
+ hand — the skill copy, `skill/README.md` and the archive — to
959
+ `../agensi-skill-<version>` (or `--out <dir>`), then verifies it with the same
960
+ comparison `check` uses, so a successful exit means the folder really is the
961
+ skill tree. It is idempotent: a folder that already matches is left untouched.
962
+ A folder that has drifted is refused instead of overwritten, because it may
963
+ already be published — bump `version` for a new package, or pass `--force` to
964
+ replace that one.
965
+
966
+ The archive comes from the same deterministic writer as the `.mcpb`, so the
967
+ same tree always hashes the same, and text payloads are LF-normalised first:
968
+ a CRLF checkout builds the bytes an LF one does. CI runs `skill:check`, generates
969
+ a package folder, and uploads the built zip as an artifact
970
+ (`.github/workflows/ci.yml`).
971
+
972
+ **Every release publishes the package too.** Assembling a release builds it from
973
+ the release tag and attaches it as `<name>-<version>-skill.zip`, and the
974
+ `.github/workflows/release.yml` run downloads that asset back off the draft and
975
+ byte-compares it with the build before anything is published. The marketplace
976
+ upload is that asset, not a folder someone assembled — a deterministic archive
977
+ means "the same build as the tagged release" is an exact statement, and
978
+ `release:verify` checks it again against a rebuild from the tag.
979
+ `.github/workflows/publish-skill-package.yml` stays for the repair path: run it
980
+ by hand to attach or rebuild the package for a release that is missing it.
981
+
982
+ `skill:check` is the other half of that, and the part that matters when a
983
+ package has already been uploaded. It builds twice (the bytes must agree),
984
+ reads the archive back and compares its contents with `skill/` file by file,
985
+ then compares a packaged copy — `../agensi-skill-<version>` by default, or a
986
+ path you pass: the package folder, the bare `playwright-e2e-mcp/` folder, or
987
+ the zip on its own. Comparisons are by content, never by archive bytes, because
988
+ a zip written by a platform `zip` tool carries real timestamps that say nothing
989
+ about whether the Markdown inside it is current. Any file that differs, is
990
+ missing or is extra is named on stderr and the command exits 1 — the repository
991
+ copy and the uploaded copy are the same document, so an edit to either one
992
+ should not be invisible.
993
+
774
994
  ### Try the example
775
995
 
776
996
  With Playwright installed in your project:
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `playwright-e2e-mcp ci`: the same diagnosis the MCP tools give an agent,
3
+ * as a plain command for CI. It runs the suite (auto-retry once, so flaky
4
+ * tests are reported as flaky rather than failed), optionally repeats the
5
+ * failures through diagnose-flaky and drafts suggest-fix diffs, and writes
6
+ * one Markdown report: to stdout, to --output, and to the GitHub job
7
+ * summary when GITHUB_STEP_SUMMARY is set.
8
+ *
9
+ * Exit codes: 0 all passed (flaky allowed), 1 tests failed, 2 the run
10
+ * itself failed (no Playwright, config error, timeout) or bad arguments.
11
+ */
12
+ export declare const CI_USAGE: string;
13
+ export interface CiOptions {
14
+ projectRoot?: string;
15
+ config?: string;
16
+ browser?: 'chromium' | 'firefox' | 'webkit';
17
+ grep?: string;
18
+ workers?: number;
19
+ timeoutMs: number;
20
+ retry: boolean;
21
+ flakyRuns?: number;
22
+ suggest: number;
23
+ output?: string;
24
+ testFiles: string[];
25
+ }
26
+ /** Parse `ci` arguments (everything after the subcommand). Throws on bad input. */
27
+ export declare function parseCiArgs(argv: string[]): CiOptions;
28
+ /** Run the CI flow; returns the exit code. */
29
+ export declare function runCi(argv: string[]): Promise<number>;
package/dist/cli/ci.js ADDED
@@ -0,0 +1,145 @@
1
+ /**
2
+ * `playwright-e2e-mcp ci`: the same diagnosis the MCP tools give an agent,
3
+ * as a plain command for CI. It runs the suite (auto-retry once, so flaky
4
+ * tests are reported as flaky rather than failed), optionally repeats the
5
+ * failures through diagnose-flaky and drafts suggest-fix diffs, and writes
6
+ * one Markdown report: to stdout, to --output, and to the GitHub job
7
+ * summary when GITHUB_STEP_SUMMARY is set.
8
+ *
9
+ * Exit codes: 0 all passed (flaky allowed), 1 tests failed, 2 the run
10
+ * itself failed (no Playwright, config error, timeout) or bad arguments.
11
+ */
12
+ import { appendFile, writeFile } from 'node:fs/promises';
13
+ import { parseArgs } from 'node:util';
14
+ import { createLogger } from '../utils/logger.js';
15
+ import { createToolStore } from '../tools/shared.js';
16
+ import { runTestTool } from '../tools/run-test.js';
17
+ import { diagnoseFlakyTool } from '../tools/diagnose-flaky.js';
18
+ import { suggestFixTool } from '../tools/suggest-fix.js';
19
+ export const CI_USAGE = [
20
+ 'Usage: playwright-e2e-mcp ci [options] [test files…]',
21
+ '',
22
+ 'Runs the Playwright suite and writes a Markdown diagnosis (stdout, --output,',
23
+ 'and $GITHUB_STEP_SUMMARY when set). Exit 0 = passed, 1 = tests failed, 2 = run error.',
24
+ '',
25
+ ' --project-root <dir> project directory (default: PW_MCP_PROJECT_ROOT or cwd)',
26
+ ' --config <path|n> playwright.config path or 1-based index',
27
+ ' --browser <name> chromium | firefox | webkit',
28
+ ' --grep <regex> only tests whose title matches',
29
+ ' --workers <n> Playwright workers',
30
+ ' --timeout <ms> wall-clock limit for the run (default 600000)',
31
+ ' --no-retry do not auto-retry failures once',
32
+ ' --flaky-runs <n> repeat failing tests n times (2-10) for a flaky verdict',
33
+ ' --suggest <n> draft suggest-fix diffs for the first n failures (max 5)',
34
+ ' --output <file> also write the report to this file (for a PR comment)',
35
+ '',
36
+ ].join('\n');
37
+ function intOption(value, name, min, max) {
38
+ if (value === undefined)
39
+ return undefined;
40
+ const n = Number(value);
41
+ if (!Number.isInteger(n) || n < min || n > max)
42
+ throw new Error(`--${name} must be an integer from ${min} to ${max}`);
43
+ return n;
44
+ }
45
+ /** Parse `ci` arguments (everything after the subcommand). Throws on bad input. */
46
+ export function parseCiArgs(argv) {
47
+ const { values, positionals } = parseArgs({
48
+ args: argv,
49
+ allowPositionals: true,
50
+ strict: true,
51
+ options: {
52
+ 'project-root': { type: 'string' },
53
+ config: { type: 'string' },
54
+ browser: { type: 'string' },
55
+ grep: { type: 'string' },
56
+ workers: { type: 'string' },
57
+ timeout: { type: 'string' },
58
+ 'no-retry': { type: 'boolean' },
59
+ 'flaky-runs': { type: 'string' },
60
+ suggest: { type: 'string' },
61
+ output: { type: 'string' },
62
+ },
63
+ });
64
+ const browser = values.browser;
65
+ if (browser !== undefined && browser !== 'chromium' && browser !== 'firefox' && browser !== 'webkit') {
66
+ throw new Error('--browser must be chromium, firefox or webkit');
67
+ }
68
+ return {
69
+ projectRoot: values['project-root'],
70
+ config: values.config,
71
+ browser,
72
+ grep: values.grep,
73
+ workers: intOption(values.workers, 'workers', 1, 64),
74
+ timeoutMs: intOption(values.timeout, 'timeout', 1_000, 3_600_000) ?? 600_000,
75
+ retry: values['no-retry'] !== true,
76
+ flakyRuns: intOption(values['flaky-runs'], 'flaky-runs', 2, 10),
77
+ suggest: intOption(values.suggest, 'suggest', 0, 5) ?? 0,
78
+ output: values.output,
79
+ testFiles: positionals,
80
+ };
81
+ }
82
+ function text(response) {
83
+ return response.content.map((block) => block.text).join('\n');
84
+ }
85
+ /** Run the CI flow; returns the exit code. */
86
+ export async function runCi(argv) {
87
+ let options;
88
+ try {
89
+ options = parseCiArgs(argv);
90
+ }
91
+ catch (err) {
92
+ process.stderr.write(`${err.message}\n\n${CI_USAGE}`);
93
+ return 2;
94
+ }
95
+ const projectRoot = options.projectRoot ?? process.env.PW_MCP_PROJECT_ROOT ?? process.cwd();
96
+ const ctx = {
97
+ // Info logs would bury the report in CI output; LOG_LEVEL still wins.
98
+ logger: createLogger({
99
+ level: process.env.LOG_LEVEL || process.env.MCP_LOG_LEVEL ? undefined : 'warn',
100
+ bindings: { name: 'playwright-e2e-mcp', mode: 'ci' },
101
+ }),
102
+ store: createToolStore(),
103
+ projectRoot,
104
+ };
105
+ const sections = [];
106
+ const run = await runTestTool.handler({
107
+ testFiles: options.testFiles.length > 0 ? options.testFiles : undefined,
108
+ grep: options.grep,
109
+ browser: options.browser,
110
+ workers: options.workers,
111
+ timeoutMs: options.timeoutMs,
112
+ retryOnFailure: options.retry,
113
+ config: options.config,
114
+ }, ctx);
115
+ sections.push(text(run));
116
+ const result = ctx.store.lastRun?.result;
117
+ const failures = result?.failures ?? [];
118
+ let code = result === undefined || run.isError ? 2 : result.ok ? 0 : failures.length > 0 ? 1 : 2;
119
+ if (code === 1 && options.suggest > 0) {
120
+ // Before diagnose-flaky, which replaces the last run suggest-fix indexes into.
121
+ const count = Math.min(options.suggest, ctx.store.lastRun?.result.failures.length ?? 0);
122
+ for (let index = 1; index <= count; index += 1) {
123
+ const fix = await suggestFixTool.handler({ index, config: options.config }, ctx);
124
+ sections.push(text(fix).replace(/^## /m, '### '));
125
+ }
126
+ }
127
+ if (code === 1 && options.flakyRuns) {
128
+ const flaky = await diagnoseFlakyTool.handler({ runs: options.flakyRuns, config: options.config, browser: options.browser }, ctx);
129
+ sections.push(text(flaky).replace(/^## /m, '### '));
130
+ }
131
+ const report = `${sections.join('\n\n---\n\n')}\n\n<sub>Generated by [playwright-e2e-mcp](https://github.com/xsignon-secure/E2E) \`ci\`.</sub>\n`;
132
+ process.stdout.write(report);
133
+ try {
134
+ if (options.output)
135
+ await writeFile(options.output, report, 'utf8');
136
+ if (process.env.GITHUB_STEP_SUMMARY)
137
+ await appendFile(process.env.GITHUB_STEP_SUMMARY, report, 'utf8');
138
+ }
139
+ catch (err) {
140
+ process.stderr.write(`could not write the report: ${err.message}\n`);
141
+ code = Math.max(code, 2);
142
+ }
143
+ return code;
144
+ }
145
+ //# sourceMappingURL=ci.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ci.js","sourceRoot":"","sources":["../../src/cli/ci.ts"],"names":[],"mappings":"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"}