@andwhy/e2e-pipeline 0.13.2 → 0.14.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 +40 -10
- package/dist/bin/init.js +29 -5
- package/dist/utils/copy.d.ts +15 -1
- package/dist/utils/copy.js +152 -91
- package/package.json +7 -3
- package/plugin/.claude-plugin/plugin.json +18 -0
- package/plugin/agents/playwright-test-generator.md +273 -0
- package/plugin/agents/playwright-test-healer.md +159 -0
- package/plugin/agents/playwright-test-planner.md +135 -0
- package/plugin/hooks/hooks.json +16 -0
- package/plugin/hooks/session-context.md +9 -0
- package/plugin/hooks/session-start.mjs +49 -0
- package/plugin/skills/playwright-test-pipeline/SKILL.md +547 -0
- package/templates/github/prompts/checkly-heal.md +10 -3
- package/templates/github/workflows/checkly-deploy.yml +19 -52
- package/templates/github/workflows/checkly-heal.yml +20 -419
- package/templates/github/workflows/pr-playwright-tests.yml +17 -143
- package/templates/claude/rules/playwright-workflow.md +0 -219
- package/templates/claude/skills/playwright-test-pipeline/SKILL.md +0 -286
package/README.md
CHANGED
|
@@ -8,7 +8,9 @@ Scaffolds the full e2e testing pipeline into any project: agent-assisted Playwri
|
|
|
8
8
|
pnpm dlx @andwhy/e2e-pipeline init
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
The CLI
|
|
11
|
+
The CLI writes the config templates and workflows into your project and enables the `e2e-pipeline` **Claude Code plugin** for the repo — that plugin carries the agent definitions, the orchestration rules, and the guided skill, and Claude Code keeps it updated on its own. The install is purely additive — it never creates or overwrites `CLAUDE.md`.
|
|
12
|
+
|
|
13
|
+
The plugin lives in [`plugin/`](plugin/) in this repository, listed by the marketplace manifest at [`.claude-plugin/marketplace.json`](.claude-plugin/marketplace.json). It loads on developer machines (terminal, VS Code, desktop app); a claude.ai/code cloud session does not load repo-enabled plugins.
|
|
12
14
|
|
|
13
15
|
**Setup and workflow docs:** [E2E Tests in Notion](https://app.notion.com/p/31ff837b6270805d8922c4a7c7beb6b0) (why-agency internal). These are maintained in one place rather than scaffolded into each project, so a workflow change is published once.
|
|
14
16
|
|
|
@@ -21,21 +23,24 @@ pnpm dlx @andwhy/e2e-pipeline init [--format ts|mjs] [--force] [--reset-config]
|
|
|
21
23
|
| Flag | Description |
|
|
22
24
|
| ---------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
23
25
|
| `--format` | Config file format (`ts` or `mjs`). Use `mjs` if TS causes conflicts. Prompted if omitted. |
|
|
24
|
-
| `--force` | Update package-owned files
|
|
26
|
+
| `--force` | Update package-owned files and remove pre-plugin copies from `.claude/`. Never touches your configs, workflows, `CLAUDE.md`, or anything else you own. |
|
|
25
27
|
| `--reset-config` | Write a fresh `*.new.{ts,mjs}` next to each config so you can diff it. Your originals are left alone. |
|
|
28
|
+
| `--update-workflows` | Replace the three GitHub workflows with the current callers of the reusable workflows, and remove the prompt and script copies only pre-0.14.0 workflows read. The one way the scaffold overwrites a workflow. |
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
The agents, skill and rules update themselves through the plugin; the workflows through the reusable workflows they call. `init --force` is only needed for the package-owned files below, and `init --force --update-workflows` once to migrate a repo scaffolded before 0.14.0.
|
|
28
31
|
|
|
29
32
|
## What gets created
|
|
30
33
|
|
|
34
|
+
**From the plugin** — nothing in the repo but two entries in `.claude/settings.json` (commit it):
|
|
35
|
+
|
|
36
|
+
- `playwright-test-planner`, `playwright-test-generator`, `playwright-test-healer` agents
|
|
37
|
+
- `playwright-test-pipeline` skill — guided walkthrough; triggers when someone asks to create or fix tests and escorts them through setup check, Plan → Generate → Heal, preview validation, and deployment. It also holds the project test rules and the agent prompt templates.
|
|
38
|
+
- A SessionStart hook with the always-on delegation rules (delegate to the agents, never hand-write spec bodies), active only in repos that carry the pipeline
|
|
39
|
+
|
|
31
40
|
**Package-owned** — regenerated by `--force`, so don't edit these by hand:
|
|
32
41
|
|
|
33
|
-
- `.claude/agents/` — Planner, Generator, Healer agent definitions (3 agents, each defines its own MCP server)
|
|
34
|
-
- `.claude/rules/playwright-workflow.md` — orchestration rules for the Plan → Generate → Heal pipeline, auto-loaded into the main session alongside any existing `CLAUDE.md`
|
|
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
42
|
- `.mcp.json` — Playwright MCP server config (project root)
|
|
37
43
|
- `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
|
|
39
44
|
|
|
40
45
|
**Seeded once** — starting points your project then owns. Written only when absent and never overwritten, `--force` included:
|
|
41
46
|
|
|
@@ -43,10 +48,12 @@ Re-running `init --force` in an existing project is the way to pick up a new ver
|
|
|
43
48
|
- `checkly.config.{ts,mjs}` — Checkly config template with placeholders
|
|
44
49
|
- `tests/e2e/seed.spec.{ts,js}` — seed file for Playwright MCP `setup_page`
|
|
45
50
|
- `tests/e2e/helpers/preview-consent-setup.{ts,js}` — `prepareVercelPreviewPage` (Vercel deployment-protection bypass); projects extend it with consent logic
|
|
46
|
-
- `.github/workflows/pr-playwright-tests.yml` — runs the suite against the PR's Vercel preview and comments the result
|
|
51
|
+
- `.github/workflows/pr-playwright-tests.yml` — runs the suite against the PR's Vercel preview and comments the result
|
|
47
52
|
- `.github/workflows/checkly-deploy.yml` — deploys monitors when a production deployment succeeds
|
|
48
53
|
- `.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.
|
|
49
54
|
|
|
55
|
+
The three workflow files are callers of a few lines each. The work runs in this repository's `.github/workflows/reusable-*.yml`, referenced at `@main`, so a merge here updates every consumer's CI without a PR there. The heal prompt (`templates/github/prompts/checkly-heal.md`) and the PR comment script (`templates/github/scripts/playwright-pr-comment.mjs`) are read from the published npm package inside those workflows. One-time admin setting on this repository so other repos may call its workflows: Settings → Actions → General → Access → "Accessible from repositories in the organization".
|
|
56
|
+
|
|
50
57
|
The workflows need repository secrets: `CHECKLY_API_KEY`, `CHECKLY_ACCOUNT_ID`,
|
|
51
58
|
`VERCEL_AUTOMATION_BYPASS_SECRET`, and for auto-heal `SLACK_WEBHOOK_URL`
|
|
52
59
|
(values in 1Password). `ANTHROPIC_API_KEY` is an organization secret inherited
|
|
@@ -57,8 +64,8 @@ E2E specs live under **`tests/e2e/`** so a repo can keep unit tests elsewhere un
|
|
|
57
64
|
kept out of test discovery by `testIgnore` rather than by living outside it.
|
|
58
65
|
|
|
59
66
|
The agents treat this as a default, not a rule: if a repo already has specs
|
|
60
|
-
somewhere else, they write there instead. See rule 6
|
|
61
|
-
|
|
67
|
+
somewhere else, they write there instead. See rule 6 of the project test
|
|
68
|
+
rules in the `playwright-test-pipeline` skill.
|
|
62
69
|
|
|
63
70
|
`.gitignore` gains Playwright/test output and seed entries; existing lines are left in place.
|
|
64
71
|
|
|
@@ -77,6 +84,29 @@ These agents extend the vanilla Playwright test agents with:
|
|
|
77
84
|
- **Locale-awareness**: no hardcoded domains or locale-specific URL patterns
|
|
78
85
|
- **Optional draft input**: Planner can validate a user-provided journey draft against the live page
|
|
79
86
|
|
|
87
|
+
## The plugin
|
|
88
|
+
|
|
89
|
+
`plugin/` is a Claude Code plugin in the standard layout: `agents/`,
|
|
90
|
+
`skills/playwright-test-pipeline/`, `hooks/`, and `.claude-plugin/plugin.json`.
|
|
91
|
+
`agents/` is generated — `npm run build:plugin` inlines
|
|
92
|
+
`templates/claude/partials/_robustness-core.md` into the agent sources under
|
|
93
|
+
`templates/claude/agents/` and syncs the manifest version from `package.json`.
|
|
94
|
+
The output is committed because a marketplace is a git clone; CI fails a pull
|
|
95
|
+
request whose output is stale (`npm run check:plugin`).
|
|
96
|
+
|
|
97
|
+
To try a branch before it is released, in any consumer repo:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
/plugin marketplace add why-agency/why-e2e-pipeline#<branch>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
or start Claude Code with `--plugin-dir ../why-e2e-pipeline/plugin`.
|
|
104
|
+
|
|
105
|
+
Auto-heal in CI does not load the plugin (a headless run with an API key has
|
|
106
|
+
no plugin sources): the workflow copies `plugin/agents/playwright-test-healer.md`
|
|
107
|
+
out of the published package instead, which is why `plugin/` ships in the npm
|
|
108
|
+
package too.
|
|
109
|
+
|
|
80
110
|
## Documentation
|
|
81
111
|
|
|
82
112
|
The Installation and Usage guides live in `docs/notion/` and are the source of
|
package/dist/bin/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { findSpecsOutsideE2e, scaffold, } from "../utils/copy.js";
|
|
4
|
+
import { findSpecsOutsideE2e, PLUGIN_ID, scaffold, } from "../utils/copy.js";
|
|
5
5
|
import { formatFiles } from "../utils/format.js";
|
|
6
6
|
import { installDeps } from "../utils/install.js";
|
|
7
7
|
import { askFileFormat } from "../utils/prompt.js";
|
|
@@ -13,6 +13,7 @@ const parseArgs = (argv) => {
|
|
|
13
13
|
let format;
|
|
14
14
|
let force = false;
|
|
15
15
|
let resetConfig = false;
|
|
16
|
+
let updateWorkflows = false;
|
|
16
17
|
for (let i = 2; i < argv.length; i++) {
|
|
17
18
|
const arg = argv[i];
|
|
18
19
|
if (arg === "init")
|
|
@@ -23,6 +24,9 @@ const parseArgs = (argv) => {
|
|
|
23
24
|
else if (arg === "--reset-config") {
|
|
24
25
|
resetConfig = true;
|
|
25
26
|
}
|
|
27
|
+
else if (arg === "--update-workflows") {
|
|
28
|
+
updateWorkflows = true;
|
|
29
|
+
}
|
|
26
30
|
else if (arg === "--format" && argv[i + 1]) {
|
|
27
31
|
const val = argv[++i];
|
|
28
32
|
if (val === "ts" || val === "mjs") {
|
|
@@ -44,10 +48,10 @@ const parseArgs = (argv) => {
|
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
|
-
return { format, force, resetConfig };
|
|
51
|
+
return { format, force, resetConfig, updateWorkflows };
|
|
48
52
|
};
|
|
49
53
|
const run = async () => {
|
|
50
|
-
const { format: formatArg, force, resetConfig } = parseArgs(process.argv);
|
|
54
|
+
const { format: formatArg, force, resetConfig, updateWorkflows, } = parseArgs(process.argv);
|
|
51
55
|
const fileFormat = formatArg ?? (await askFileFormat());
|
|
52
56
|
const projectRoot = process.cwd();
|
|
53
57
|
const templatesRoot = path.resolve(__dirname, "..", "..", "templates");
|
|
@@ -56,6 +60,7 @@ const run = async () => {
|
|
|
56
60
|
force,
|
|
57
61
|
fileFormat,
|
|
58
62
|
resetConfig,
|
|
63
|
+
updateWorkflows,
|
|
59
64
|
});
|
|
60
65
|
if (result.created.length > 0) {
|
|
61
66
|
console.log("Created:");
|
|
@@ -79,6 +84,12 @@ const run = async () => {
|
|
|
79
84
|
console.log(` ${path.relative(projectRoot, f)}`);
|
|
80
85
|
}
|
|
81
86
|
}
|
|
87
|
+
if (result.removed.length > 0) {
|
|
88
|
+
console.log(`\nRemoved copies the ${PLUGIN_ID} plugin or the reusable workflows provide now:`);
|
|
89
|
+
for (const f of result.removed) {
|
|
90
|
+
console.log(` ${path.relative(projectRoot, f)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
82
93
|
// Dependencies install before the formatting pass: prettier is resolved
|
|
83
94
|
// from the project's node_modules, which a fresh clone doesn't have yet.
|
|
84
95
|
// Formatting after the install means scaffolded files pick up the project's
|
|
@@ -123,13 +134,26 @@ const run = async () => {
|
|
|
123
134
|
"skill walks through that.",
|
|
124
135
|
].join("\n"));
|
|
125
136
|
}
|
|
126
|
-
|
|
137
|
+
if (result.warnings.length > 0) {
|
|
138
|
+
console.log("\nNeeds your attention:");
|
|
139
|
+
for (const w of result.warnings) {
|
|
140
|
+
console.log(` - ${w}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
console.log("\nNext steps:");
|
|
127
144
|
let step = 1;
|
|
128
145
|
const ext = fileFormat === "mjs" ? "mjs" : "ts";
|
|
129
146
|
console.log(` ${step++}. Edit checkly.config.${ext} with your project name and check suites`);
|
|
130
147
|
console.log(` ${step++}. Run: npx checkly login`);
|
|
148
|
+
console.log(` ${step++}. Restart Claude Code in this repo: the ${PLUGIN_ID} plugin (agents, skill, rules) installs at session start`);
|
|
131
149
|
console.log(` ${step++}. Setup and workflow docs: ${DOCS_URL}`);
|
|
132
|
-
console.log(
|
|
150
|
+
console.log([
|
|
151
|
+
"",
|
|
152
|
+
`The Playwright agents, the playwright-test-pipeline skill and the workflow rules come from the ${PLUGIN_ID}`,
|
|
153
|
+
"Claude Code plugin, enabled for this repo in .claude/settings.json (commit it). It updates itself",
|
|
154
|
+
"on every pipeline release — no more init --force for those. Developer machines only: a claude.ai/code",
|
|
155
|
+
"cloud session does not load a repo's plugin settings.",
|
|
156
|
+
].join("\n"));
|
|
133
157
|
console.log();
|
|
134
158
|
};
|
|
135
159
|
run().catch((err) => {
|
package/dist/utils/copy.d.ts
CHANGED
|
@@ -2,7 +2,14 @@ interface CopyResult {
|
|
|
2
2
|
created: string[];
|
|
3
3
|
skipped: string[];
|
|
4
4
|
regenerated: string[];
|
|
5
|
+
/** Pre-plugin copies of package files deleted from the project (--force). */
|
|
6
|
+
removed: string[];
|
|
7
|
+
/** Things the scaffold cannot fix itself and the user must act on. */
|
|
8
|
+
warnings: string[];
|
|
5
9
|
}
|
|
10
|
+
export declare const MARKETPLACE_NAME = "andwhy";
|
|
11
|
+
export declare const PLUGIN_NAME = "e2e-pipeline";
|
|
12
|
+
export declare const PLUGIN_ID = "e2e-pipeline@andwhy";
|
|
6
13
|
/**
|
|
7
14
|
* Specs already living under tests/ but outside tests/e2e — a unit or a11y
|
|
8
15
|
* suite that predates the scaffold. They need calling out at install time:
|
|
@@ -18,6 +25,13 @@ export interface ScaffoldOptions {
|
|
|
18
25
|
fileFormat?: FileFormat;
|
|
19
26
|
/** Write a fresh copy of each config alongside the existing one, as `.new`. */
|
|
20
27
|
resetConfig?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Replace the three GitHub workflows with the current callers of the
|
|
30
|
+
* pipeline's reusable workflows, and remove the prompt and script copies
|
|
31
|
+
* the old workflows read. The one way the scaffold ever overwrites a
|
|
32
|
+
* workflow.
|
|
33
|
+
*/
|
|
34
|
+
updateWorkflows?: boolean;
|
|
21
35
|
}
|
|
22
|
-
export declare const scaffold: (templatesRoot: string, projectRoot: string, { force, fileFormat, resetConfig }?: ScaffoldOptions) => Promise<CopyResult>;
|
|
36
|
+
export declare const scaffold: (templatesRoot: string, projectRoot: string, { force, fileFormat, resetConfig, updateWorkflows, }?: ScaffoldOptions) => Promise<CopyResult>;
|
|
23
37
|
export {};
|
package/dist/utils/copy.js
CHANGED
|
@@ -1,48 +1,153 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "fs-extra";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
// The agents, the skill and the always-on rules ship as the `e2e-pipeline`
|
|
4
|
+
// Claude Code plugin (plugin/ in this repository) rather than as copies in
|
|
5
|
+
// each project. The project only carries the two settings entries that
|
|
6
|
+
// register the marketplace and enable the plugin; Claude Code installs and
|
|
7
|
+
// auto-updates it from there, so a pipeline release no longer needs a PR in
|
|
8
|
+
// every consumer repo.
|
|
9
|
+
export const MARKETPLACE_NAME = "andwhy";
|
|
10
|
+
export const PLUGIN_NAME = "e2e-pipeline";
|
|
11
|
+
export const PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
12
|
+
const MARKETPLACE_SOURCE = {
|
|
13
|
+
source: "github",
|
|
14
|
+
repo: "why-agency/why-e2e-pipeline",
|
|
15
|
+
};
|
|
16
|
+
// Merges the plugin entries into .claude/settings.json (the committed project
|
|
17
|
+
// settings, not settings.local.json). Only the two keys the plugin needs are
|
|
18
|
+
// touched; everything else in the file is kept as is. Idempotent, so it runs
|
|
19
|
+
// on every init — an existing project picks the plugin up on its next
|
|
20
|
+
// `init --force` without a separate step.
|
|
21
|
+
const writeClaudeSettings = async (projectRoot, result) => {
|
|
22
|
+
const settingsPath = path.join(projectRoot, ".claude", "settings.json");
|
|
23
|
+
let settings = {};
|
|
24
|
+
if (await fs.pathExists(settingsPath)) {
|
|
25
|
+
try {
|
|
26
|
+
settings = await fs.readJson(settingsPath);
|
|
11
27
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
else {
|
|
17
|
-
await fs.ensureDir(path.dirname(destPath));
|
|
18
|
-
await fs.copy(srcPath, destPath);
|
|
19
|
-
result.created.push(destPath);
|
|
20
|
-
}
|
|
28
|
+
catch {
|
|
29
|
+
result.warnings.push(`.claude/settings.json is not valid JSON, so the ${PLUGIN_ID} plugin entry was not written. ` +
|
|
30
|
+
`Fix the file and re-run, or add the entry by hand (see the installation docs).`);
|
|
31
|
+
return;
|
|
21
32
|
}
|
|
22
33
|
}
|
|
34
|
+
const marketplaces = settings.extraKnownMarketplaces ??
|
|
35
|
+
{};
|
|
36
|
+
const enabled = settings.enabledPlugins ?? {};
|
|
37
|
+
const wantMarketplace = { source: MARKETPLACE_SOURCE, autoUpdate: true };
|
|
38
|
+
const upToDate = JSON.stringify(marketplaces[MARKETPLACE_NAME]) ===
|
|
39
|
+
JSON.stringify(wantMarketplace) && enabled[PLUGIN_ID] === true;
|
|
40
|
+
if (upToDate) {
|
|
41
|
+
result.skipped.push(settingsPath);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
settings.extraKnownMarketplaces = {
|
|
45
|
+
...marketplaces,
|
|
46
|
+
[MARKETPLACE_NAME]: wantMarketplace,
|
|
47
|
+
};
|
|
48
|
+
settings.enabledPlugins = { ...enabled, [PLUGIN_ID]: true };
|
|
49
|
+
await fs.ensureDir(path.dirname(settingsPath));
|
|
50
|
+
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
51
|
+
result.created.push(settingsPath);
|
|
23
52
|
};
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
53
|
+
// What earlier versions copied into .claude/ and the plugin now provides. A
|
|
54
|
+
// copy left in place shadows or duplicates the plugin's agents and skill, so
|
|
55
|
+
// --force deletes them (they were package-owned and overwritten wholesale by
|
|
56
|
+
// --force before, so nothing hand-made is lost). Without --force they are
|
|
57
|
+
// reported, not touched.
|
|
58
|
+
const STALE_CLAUDE_FILES = [
|
|
59
|
+
path.join(".claude", "agents", "playwright-test-planner.md"),
|
|
60
|
+
path.join(".claude", "agents", "playwright-test-generator.md"),
|
|
61
|
+
path.join(".claude", "agents", "playwright-test-healer.md"),
|
|
62
|
+
path.join(".claude", "rules", "playwright-workflow.md"),
|
|
63
|
+
path.join(".claude", "skills", "playwright-test-pipeline"),
|
|
64
|
+
];
|
|
65
|
+
const removeStaleClaudeFiles = async (projectRoot, force, result) => {
|
|
66
|
+
const present = [];
|
|
67
|
+
for (const rel of STALE_CLAUDE_FILES) {
|
|
68
|
+
if (await fs.pathExists(path.join(projectRoot, rel)))
|
|
69
|
+
present.push(rel);
|
|
70
|
+
}
|
|
71
|
+
if (present.length === 0)
|
|
72
|
+
return;
|
|
73
|
+
if (!force) {
|
|
74
|
+
result.warnings.push(`Pre-plugin copies still in the repo: ${present.join(", ")}. ` +
|
|
75
|
+
`They shadow the ${PLUGIN_ID} plugin's agents and skill — run init --force to remove them.`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
for (const rel of present) {
|
|
79
|
+
const abs = path.join(projectRoot, rel);
|
|
80
|
+
await fs.remove(abs);
|
|
81
|
+
result.removed.push(abs);
|
|
82
|
+
// Leave no empty package-created directory behind; a project's own
|
|
83
|
+
// agents/rules/skills keep the directory alive.
|
|
84
|
+
const parent = path.dirname(abs);
|
|
85
|
+
if ((await fs.readdir(parent)).length === 0)
|
|
86
|
+
await fs.remove(parent);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
// Since 0.14.0 the three workflows in a project are short callers of the
|
|
90
|
+
// reusable workflows in the pipeline repository, so a workflow change ships
|
|
91
|
+
// on merge without a PR per repo. Workflows are seeded once and never
|
|
92
|
+
// overwritten by --force (repos customize them), so a repo scaffolded
|
|
93
|
+
// earlier keeps its full-length copies until --update-workflows replaces
|
|
94
|
+
// them. The heal prompt and the PR comment script those copies read from
|
|
95
|
+
// the repo are served from the published package to the callers' reusable
|
|
96
|
+
// workflows, so they are removed together with the old workflows.
|
|
97
|
+
const REUSABLE_MARKER = "uses: why-agency/why-e2e-pipeline/";
|
|
98
|
+
const WORKFLOW_FILES = [
|
|
99
|
+
"pr-playwright-tests.yml",
|
|
100
|
+
"checkly-deploy.yml",
|
|
101
|
+
"checkly-heal.yml",
|
|
102
|
+
];
|
|
103
|
+
const WORKFLOW_SIDE_FILES = [
|
|
104
|
+
path.join(".github", "prompts", "checkly-heal.md"),
|
|
105
|
+
path.join(".github", "scripts", "playwright-pr-comment.mjs"),
|
|
106
|
+
];
|
|
107
|
+
const isCaller = async (workflowPath) => (await fs.readFile(workflowPath, "utf-8")).includes(REUSABLE_MARKER);
|
|
108
|
+
const writeWorkflows = async (githubSrc, projectRoot, updateWorkflows, result) => {
|
|
109
|
+
const stale = [];
|
|
110
|
+
for (const name of WORKFLOW_FILES) {
|
|
111
|
+
const srcPath = path.join(githubSrc, "workflows", name);
|
|
112
|
+
const destPath = path.join(projectRoot, ".github", "workflows", name);
|
|
113
|
+
const exists = await fs.pathExists(destPath);
|
|
114
|
+
if (exists && !updateWorkflows) {
|
|
37
115
|
result.skipped.push(destPath);
|
|
116
|
+
if (!(await isCaller(destPath)))
|
|
117
|
+
stale.push(`.github/workflows/${name}`);
|
|
38
118
|
continue;
|
|
39
119
|
}
|
|
40
|
-
|
|
41
|
-
|
|
120
|
+
if (exists && (await isCaller(destPath))) {
|
|
121
|
+
// Already a caller; --update-workflows refreshes it like a
|
|
122
|
+
// package-owned file.
|
|
123
|
+
const same = (await fs.readFile(srcPath, "utf-8")) ===
|
|
124
|
+
(await fs.readFile(destPath, "utf-8"));
|
|
125
|
+
if (same) {
|
|
126
|
+
result.skipped.push(destPath);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
42
130
|
await fs.ensureDir(path.dirname(destPath));
|
|
43
|
-
await fs.
|
|
131
|
+
await fs.copy(srcPath, destPath);
|
|
44
132
|
result.created.push(destPath);
|
|
45
133
|
}
|
|
134
|
+
if (stale.length > 0) {
|
|
135
|
+
result.warnings.push(`Workflows from before 0.14.0: ${stale.join(", ")}. They still work, but changes to them ` +
|
|
136
|
+
`need a PR in this repo. Run init --update-workflows to replace them with callers of the ` +
|
|
137
|
+
`pipeline's reusable workflows (review the diff: repo customizations become \`with:\` inputs).`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Every workflow is a caller now, so the copied prompt and script are dead.
|
|
141
|
+
for (const rel of WORKFLOW_SIDE_FILES) {
|
|
142
|
+
const abs = path.join(projectRoot, rel);
|
|
143
|
+
if (!(await fs.pathExists(abs)))
|
|
144
|
+
continue;
|
|
145
|
+
await fs.remove(abs);
|
|
146
|
+
result.removed.push(abs);
|
|
147
|
+
const parent = path.dirname(abs);
|
|
148
|
+
if ((await fs.readdir(parent)).length === 0)
|
|
149
|
+
await fs.remove(parent);
|
|
150
|
+
}
|
|
46
151
|
};
|
|
47
152
|
const readProjectName = async (projectRoot) => {
|
|
48
153
|
const pkgPath = path.join(projectRoot, "package.json");
|
|
@@ -149,19 +254,22 @@ export const findSpecsOutsideE2e = async (projectRoot) => {
|
|
|
149
254
|
await walk(testsRoot);
|
|
150
255
|
return [...found].sort();
|
|
151
256
|
};
|
|
152
|
-
export const scaffold = async (templatesRoot, projectRoot, { force = false, fileFormat = "ts", resetConfig = false } = {}) => {
|
|
153
|
-
const result = {
|
|
257
|
+
export const scaffold = async (templatesRoot, projectRoot, { force = false, fileFormat = "ts", resetConfig = false, updateWorkflows = false, } = {}) => {
|
|
258
|
+
const result = {
|
|
259
|
+
created: [],
|
|
260
|
+
skipped: [],
|
|
261
|
+
regenerated: [],
|
|
262
|
+
removed: [],
|
|
263
|
+
warnings: [],
|
|
264
|
+
};
|
|
154
265
|
const configExt = fileFormat === "mjs" ? "mjs" : "ts";
|
|
155
266
|
const seedExt = fileFormat === "mjs" ? "js" : "ts";
|
|
156
267
|
// The MCP server gets its own config: it must discover seeds that the main
|
|
157
268
|
// config's testIgnore hides from local, CI, and Checkly runs. Package-owned,
|
|
158
269
|
// like .mcp.json which points at it.
|
|
159
270
|
const mcpConfigPath = `./playwright.mcp.config.${configExt}`;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
await copyAgentsDir(path.join(claudeSrc, "agents"), path.join(projectRoot, ".claude", "agents"), robustnessCore, force, result);
|
|
163
|
-
await copyTemplateDir(path.join(claudeSrc, "rules"), path.join(projectRoot, ".claude", "rules"), force, result);
|
|
164
|
-
await copyTemplateDir(path.join(claudeSrc, "skills"), path.join(projectRoot, ".claude", "skills"), force, result);
|
|
271
|
+
await writeClaudeSettings(projectRoot, result);
|
|
272
|
+
await removeStaleClaudeFiles(projectRoot, force, result);
|
|
165
273
|
await writeMcpConfig(path.join(projectRoot, ".mcp.json"), mcpConfigPath, force, result);
|
|
166
274
|
{
|
|
167
275
|
const src = path.join(templatesRoot, "shared", `playwright.mcp.config.template.${configExt}`);
|
|
@@ -219,57 +327,10 @@ export const scaffold = async (templatesRoot, projectRoot, { force = false, file
|
|
|
219
327
|
}
|
|
220
328
|
// GitHub workflows: seeded once, like the configs. Repos customize their
|
|
221
329
|
// workflows (path filters, retry posture), and --force must never clobber
|
|
222
|
-
// that
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
[
|
|
227
|
-
path.join("workflows", "checkly-deploy.yml"),
|
|
228
|
-
path.join(".github", "workflows", "checkly-deploy.yml"),
|
|
229
|
-
],
|
|
230
|
-
[
|
|
231
|
-
path.join("workflows", "pr-playwright-tests.yml"),
|
|
232
|
-
path.join(".github", "workflows", "pr-playwright-tests.yml"),
|
|
233
|
-
],
|
|
234
|
-
[
|
|
235
|
-
path.join("workflows", "checkly-heal.yml"),
|
|
236
|
-
path.join(".github", "workflows", "checkly-heal.yml"),
|
|
237
|
-
],
|
|
238
|
-
[
|
|
239
|
-
path.join("scripts", "playwright-pr-comment.mjs"),
|
|
240
|
-
path.join(".github", "scripts", "playwright-pr-comment.mjs"),
|
|
241
|
-
],
|
|
242
|
-
];
|
|
243
|
-
for (const [src, dest] of githubMappings) {
|
|
244
|
-
const destPath = path.join(projectRoot, dest);
|
|
245
|
-
if (await fs.pathExists(destPath)) {
|
|
246
|
-
result.skipped.push(destPath);
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
await fs.ensureDir(path.dirname(destPath));
|
|
250
|
-
await fs.copy(path.join(githubSrc, src), destPath);
|
|
251
|
-
result.created.push(destPath);
|
|
252
|
-
}
|
|
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
|
-
}
|
|
330
|
+
// that; --update-workflows is the explicit opt-in. They need repository
|
|
331
|
+
// secrets the scaffold cannot create — the CLI says which below, the skill
|
|
332
|
+
// walks the user through adding them.
|
|
333
|
+
await writeWorkflows(path.join(templatesRoot, "github"), projectRoot, updateWorkflows, result);
|
|
273
334
|
await appendGitignore(projectRoot, result);
|
|
274
335
|
return result;
|
|
275
336
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andwhy/e2e-pipeline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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",
|
|
@@ -13,14 +13,18 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"dist",
|
|
16
|
-
"templates"
|
|
16
|
+
"templates",
|
|
17
|
+
"plugin"
|
|
17
18
|
],
|
|
18
19
|
"publishConfig": {
|
|
19
20
|
"access": "public"
|
|
20
21
|
},
|
|
21
22
|
"scripts": {
|
|
22
23
|
"build": "tsc",
|
|
23
|
-
"
|
|
24
|
+
"build:plugin": "node scripts/build-plugin.mjs",
|
|
25
|
+
"check:plugin": "node scripts/build-plugin.mjs --check",
|
|
26
|
+
"prepublishOnly": "npm run build && npm run check:plugin",
|
|
27
|
+
"skill:zip": "node scripts/zip-skill.mjs"
|
|
24
28
|
},
|
|
25
29
|
"dependencies": {
|
|
26
30
|
"fs-extra": "^11.3.0",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "e2e-pipeline",
|
|
3
|
+
"displayName": "&why e2e pipeline",
|
|
4
|
+
"version": "0.14.1",
|
|
5
|
+
"description": "Agent-assisted Playwright e2e tests: planner, generator and healer agents plus the playwright-test-pipeline skill that drives Plan → Generate → Heal, Checkly preview validation and deployment.",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "&why",
|
|
8
|
+
"url": "https://why.de"
|
|
9
|
+
},
|
|
10
|
+
"repository": "https://github.com/why-agency/why-e2e-pipeline",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"playwright",
|
|
14
|
+
"e2e",
|
|
15
|
+
"testing",
|
|
16
|
+
"checkly"
|
|
17
|
+
]
|
|
18
|
+
}
|