@rtrentjones/greenlight 0.2.14 → 0.2.16
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/assets/skills/deploy-verify-promote/SKILL.md +55 -36
- package/dist/bin.js +256 -48
- package/dist/{chunk-JRCATCRY.js → chunk-6USV5AQV.js} +18 -4
- package/dist/{chunk-ADS6BJJ5.js → chunk-HX7VA25D.js} +14 -0
- package/dist/chunk-N3IKUCSF.js +122 -0
- package/dist/index.js +3 -3
- package/dist/{mcp-3L6HJ6BH.js → mcp-FFLOX4YP.js} +1 -1
- package/dist/{playwright-CGTTHGIL.js → playwright-SGRK3I4I.js} +1 -1
- package/package.json +3 -3
- package/dist/chunk-WFZTRXBF.js +0 -61
|
@@ -1,53 +1,72 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: deploy-verify-promote
|
|
3
|
-
description:
|
|
3
|
+
description: The one model for delivering a feature to ANY Greenlight tool — local-gate (preview) → add it to the verify loop → ship (gated on the tool's own tests) → verify prod. Same shape for blog (workers), web apps (vercel), and MCP servers (oci); only the matrix cells vary. Use when changing a Greenlight tool or the blog and you want it shipped with objective confidence, or when asked to deploy/verify/promote.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# deploy-verify-promote
|
|
6
|
+
# deploy-verify-promote — one loop for every tool
|
|
7
7
|
|
|
8
|
-
The execution discipline for
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
with objective confidence rather than vibes.
|
|
8
|
+
The execution discipline for delivering a feature to **any** Greenlight tool. The verify harness and
|
|
9
|
+
promote guard are the **same code CI runs**, so passing locally means passing in CI. This is what
|
|
10
|
+
lets a change (or a long autonomous string of changes) ship with objective confidence, not vibes.
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
**One shape, every tool** — only the matrix cells differ (never the steps):
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
```
|
|
15
|
+
branch → change → LOCAL GATE (greenlight preview) → ADD IT TO THE VERIFY LOOP (the tool's
|
|
16
|
+
verify.config) → SHIP (push; CI gates on the tool's own tests) → DEPLOY → VERIFY PROD
|
|
17
|
+
```
|
|
16
18
|
|
|
17
|
-
##
|
|
19
|
+
## Input
|
|
20
|
+
- `<name>` — a manifest entry: `blog`, or a tool from `greenlight.config.ts`.
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|---|---|---|
|
|
21
|
-
| tool | `https://<name>.<domain>` | `https://beta.<name>.<domain>` |
|
|
22
|
-
| blog (apex) | `https://<domain>` | `https://beta.<domain>` |
|
|
23
|
-
| mcp connect | *(tool url)* `+ /mcp` | same `+ /mcp` |
|
|
22
|
+
## The matrix (what varies by lane×target — look up your tool, then follow the one procedure)
|
|
24
23
|
|
|
25
|
-
`preview`
|
|
26
|
-
|
|
24
|
+
| lane×target | local gate (`preview`) | ship trigger | beta? / promote? | deploy | verify mode | verify config lives | code repo |
|
|
25
|
+
|---|---|---|---|---|---|---|---|
|
|
26
|
+
| astro/workers (blog) | build + `pnpm preview` | push `develop`/`main` | **yes / yes** | wrangler | api(+playwright) | `<dir>/verify.config.ts` | same repo |
|
|
27
|
+
| next/vercel (web app) | `preview` descriptor / build | git push (Vercel) | **yes (preview) / yes** | Vercel git-integration | api/agent-web/test | tool repo `verify/<name>.config.ts` | cross-repo (submodule) |
|
|
28
|
+
| mcp/oci (MCP server) | `preview` descriptor (docker `/mcp`) | push → build → dispatch | **no / no** (direct-to-prod) | restart instance | mcp(+eval) | wrapper `verify/<name>.config.ts` | cross-repo (submodule) |
|
|
29
|
+
| mcp/workers (dev) | `pnpm start` `/mcp` | push | yes / yes | wrangler | mcp | `<dir>/verify.config.ts` | same repo |
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
Two axes cause all the variation:
|
|
32
|
+
- **Standing beta + promote** (web: a cheap preview/beta exists → verify beta, then gated FF
|
|
33
|
+
`develop→main`) vs **direct-to-prod, verify-gated** (oci: no beta on the free tier → the
|
|
34
|
+
**local gate + the ship-gate are your pre-prod safety**; deploy restarts prod, then verify prod).
|
|
35
|
+
- **Same-repo** vs **cross-repo adopted** (the tool's code is a `tools/<name>` submodule; its infra
|
|
36
|
+
+ verify config live in the **wrapper**; you edit both and **bump the submodule pointer**).
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
2. **Make the change.**
|
|
32
|
-
3. **Preview** — push; the target's git integration produces a preview deploy. Verify it.
|
|
33
|
-
- Local/CI: `runLoop` (build → deploy → verify) from `@rtrentjones/greenlight-loop`.
|
|
34
|
-
- Standalone / local server: `pnpm greenlight verify <name> --url <preview-or-localhost-url>`.
|
|
35
|
-
4. **Beta** — merge to `develop` → beta deploy. `pnpm greenlight verify <name> --env beta`.
|
|
36
|
-
Mode is chosen by lane: `api`/`playwright` for web, `mcp` for MCP servers.
|
|
37
|
-
5. **Promote** — `pnpm greenlight promote <name>`. Checks the fast-forward guard
|
|
38
|
-
(`develop → main`). If it refuses (diverged `main`), reconcile and retry — never force-push.
|
|
39
|
-
6. **Prod** — after promote, `pnpm greenlight verify <name> --env prod`.
|
|
38
|
+
## Procedure (identical for every tool)
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
1. **Branch** in the tool's code repo — `git checkout -b <type>/<slug>` (`feat/new-tool`, `fix/auth`).
|
|
41
|
+
2. **Make the change** (+ the tool's own tests — they become the ship-gate in step 4).
|
|
42
|
+
3. **LOCAL GATE** — `pnpm greenlight preview <name>`: spins the tool up locally (matching its prod
|
|
43
|
+
contract) and runs the verify harness against it. Green here = your pre-prod signal (essential for
|
|
44
|
+
direct-to-prod oci, which has no beta). `preview` sets `GREENLIGHT_PREVIEW=1` so a config can pick
|
|
45
|
+
a local-appropriate spec (e.g. skip an auth-rejection a local no-auth server can't satisfy).
|
|
46
|
+
4. **ADD IT TO THE VERIFY LOOP** — edit the tool's `verify.config.ts` (location per the matrix) so
|
|
47
|
+
the new capability is asserted: add the tool to `expectTools` (mcp; `exactTools: true` makes a
|
|
48
|
+
forgotten entry fail the gate), a `check`/`renders`/`suite` (web), or an `eval` case (quality).
|
|
49
|
+
5. **SHIP** — push. CI **gates on the tool's own tests** before anything deploys (container build
|
|
50
|
+
`needs: [test]`; vercel via `deployment_status`; workers via deploy→verify). A broken change never
|
|
51
|
+
reaches prod.
|
|
52
|
+
6. **DEPLOY + VERIFY PROD** —
|
|
53
|
+
- **web (beta+promote):** merge to `develop` → beta; `greenlight verify <name> --env beta`; then
|
|
54
|
+
`greenlight promote <name>` (gated FF `develop→main` — never force-push); `verify --env prod`.
|
|
55
|
+
- **oci (direct-to-prod):** the push builds → dispatches → the wrapper restarts the instance →
|
|
56
|
+
`greenlight verify <name> --env prod` runs automatically as the deploy gate.
|
|
57
|
+
7. **Watch** — `pnpm greenlight status <name>` shows the last build/deploy/verify run across repos.
|
|
42
58
|
|
|
43
|
-
|
|
44
|
-
|
|
59
|
+
## Rules
|
|
60
|
+
- `verify` exits non-zero if any check fails; the report lists each. **Never promote/ship a tool
|
|
61
|
+
whose verify is failing.**
|
|
62
|
+
- **Always run the local gate (step 3) before pushing a direct-to-prod (oci) tool** — there is no
|
|
63
|
+
beta to catch a bad image; the deploy restarts prod.
|
|
64
|
+
- Cross-repo: commit the tool change in its submodule AND bump the submodule pointer in the wrapper;
|
|
65
|
+
the verify config + infra are the **wrapper's** to edit.
|
|
45
66
|
- Connect URL for MCP tools is the tool URL + `/mcp`; `verify` handles this by lane.
|
|
46
|
-
-
|
|
47
|
-
and promote guard are stable now.
|
|
67
|
+
- `greenlight doctor` flags any tool drifting from this model (no verify spec, no local preview gate).
|
|
48
68
|
|
|
49
69
|
## Cross-repo note
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
deps; the per-repo parameters come from that repo's `greenlight.config.ts`.
|
|
70
|
+
In adopted/standalone repos (BAMCP, ejected tools) this skill is delivered by the **Greenlight
|
|
71
|
+
Claude Code plugin** and the mechanics by the `@rtrentjones/greenlight*` npm deps; the per-repo
|
|
72
|
+
parameters come from that repo's (or the wrapper's) `greenlight.config.ts`.
|
package/dist/bin.js
CHANGED
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
loadConfig,
|
|
6
6
|
resolveUrl,
|
|
7
7
|
verifyAll
|
|
8
|
-
} from "./chunk-
|
|
9
|
-
import "./chunk-
|
|
10
|
-
import "./chunk-
|
|
8
|
+
} from "./chunk-6USV5AQV.js";
|
|
9
|
+
import "./chunk-HX7VA25D.js";
|
|
10
|
+
import "./chunk-N3IKUCSF.js";
|
|
11
11
|
import "./chunk-KP3Y6WRU.js";
|
|
12
12
|
import "./chunk-UXHHLEYO.js";
|
|
13
13
|
import "./chunk-6N7MD6FR.js";
|
|
@@ -49,6 +49,15 @@ function serializeTool(t) {
|
|
|
49
49
|
if (t.dir !== void 0) parts.push(`dir: ${q(t.dir)}`);
|
|
50
50
|
if (t.adopted) parts.push("adopted: true");
|
|
51
51
|
if (t.external) parts.push("external: true");
|
|
52
|
+
if (t.port !== void 0) parts.push(`port: ${t.port}`);
|
|
53
|
+
if (t.preview) {
|
|
54
|
+
const pv = t.preview;
|
|
55
|
+
const pvParts = [`command: ${q(pv.command)}`];
|
|
56
|
+
if (pv.teardown !== void 0) pvParts.push(`teardown: ${q(pv.teardown)}`);
|
|
57
|
+
if (pv.port !== void 0) pvParts.push(`port: ${pv.port}`);
|
|
58
|
+
if (pv.path !== void 0) pvParts.push(`path: ${q(pv.path)}`);
|
|
59
|
+
parts.push(`preview: { ${pvParts.join(", ")} }`);
|
|
60
|
+
}
|
|
52
61
|
return ` { ${parts.join(", ")} },`;
|
|
53
62
|
}
|
|
54
63
|
function serializeConfig(c) {
|
|
@@ -93,7 +102,8 @@ function addTool(config, t) {
|
|
|
93
102
|
...t.dir !== void 0 ? { dir: t.dir } : {},
|
|
94
103
|
...t.adopted ? { adopted: true } : {},
|
|
95
104
|
...t.external ? { external: true } : {},
|
|
96
|
-
...t.port !== void 0 ? { port: t.port } : {}
|
|
105
|
+
...t.port !== void 0 ? { port: t.port } : {},
|
|
106
|
+
...t.preview ? { preview: t.preview } : {}
|
|
97
107
|
}
|
|
98
108
|
]
|
|
99
109
|
};
|
|
@@ -118,7 +128,8 @@ function upsertTool(config, t) {
|
|
|
118
128
|
...t.dir !== void 0 ? { dir: t.dir } : {},
|
|
119
129
|
...t.adopted ? { adopted: true } : {},
|
|
120
130
|
...t.external ? { external: true } : {},
|
|
121
|
-
...t.port !== void 0 ? { port: t.port } : {}
|
|
131
|
+
...t.port !== void 0 ? { port: t.port } : {},
|
|
132
|
+
...t.preview ? { preview: t.preview } : {}
|
|
122
133
|
};
|
|
123
134
|
const tools = config.tools.some((x) => x.name === t.name) ? config.tools.map((x) => x.name === t.name ? entry : x) : [...config.tools, entry];
|
|
124
135
|
const result = ConfigSchema.safeParse({ ...config, tools });
|
|
@@ -173,7 +184,9 @@ function resolveEntry(config, name) {
|
|
|
173
184
|
target: tool.target,
|
|
174
185
|
data: tool.data,
|
|
175
186
|
dir: tool.dir ?? `tools/${tool.name}`,
|
|
176
|
-
external: tool.external
|
|
187
|
+
external: tool.external,
|
|
188
|
+
port: tool.port,
|
|
189
|
+
preview: tool.preview
|
|
177
190
|
};
|
|
178
191
|
}
|
|
179
192
|
var VERIFY_MODES = /* @__PURE__ */ new Set(["api", "mcp", "playwright", "test", "agent-web", "eval"]);
|
|
@@ -413,7 +426,7 @@ function tokensForTool(tool) {
|
|
|
413
426
|
}
|
|
414
427
|
|
|
415
428
|
// src/version.ts
|
|
416
|
-
var MODULE_REF = "v0.2.
|
|
429
|
+
var MODULE_REF = "v0.2.16";
|
|
417
430
|
var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
|
|
418
431
|
function moduleSource(module, ref = MODULE_REF) {
|
|
419
432
|
return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
|
|
@@ -679,11 +692,15 @@ function mergeMcpServers(existing, add) {
|
|
|
679
692
|
// src/commands/agent.ts
|
|
680
693
|
var CLAUDE_BLOCK = `## Greenlight loop (deploy \u2192 verify \u2192 promote)
|
|
681
694
|
|
|
682
|
-
This repo uses Greenlight.
|
|
683
|
-
|
|
695
|
+
This repo uses Greenlight. Deliver every change through the ONE model (same shape for web + MCP
|
|
696
|
+
tools \u2014 the deploy-verify-promote skill has the lane\xD7target matrix):
|
|
697
|
+
branch \u2192 change \u2192 \`greenlight preview <name>\` (local gate) \u2192 add it to the tool's verify.config \u2192
|
|
698
|
+
push (CI gates on the tool's own tests) \u2192 deploy \u2192 \`greenlight verify <name> --env prod\`.
|
|
699
|
+
Web tools also get beta + \`greenlight promote\`; oci is direct-to-prod (the local gate is the
|
|
700
|
+
pre-prod safety). \`greenlight status <name>\` shows the run chain; \`greenlight doctor\` flags drift.
|
|
684
701
|
|
|
685
702
|
Agentic kit:
|
|
686
|
-
- Skill: \`.claude/skills/deploy-verify-promote/SKILL.md\` (the
|
|
703
|
+
- Skill: \`.claude/skills/deploy-verify-promote/SKILL.md\` (the one model + the matrix).
|
|
687
704
|
- MCP servers: \`.mcp.json\` recommends the relevant providers \u2014 run \`/mcp\` to authenticate.
|
|
688
705
|
Vercel is OAuth; Supabase needs \`SUPABASE_ACCESS_TOKEN\` (+ \`SUPABASE_PROJECT_REF\`) in your env.
|
|
689
706
|
- Best-practice skills (one-time, user scope):
|
|
@@ -1104,12 +1121,36 @@ function vendorDeps(vendorDir) {
|
|
|
1104
1121
|
}
|
|
1105
1122
|
return out;
|
|
1106
1123
|
}
|
|
1107
|
-
function starterVerifyConfig(lane,
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1124
|
+
function starterVerifyConfig(lane, _target) {
|
|
1125
|
+
const logHint = 'curl -sS -i "$GREENLIGHT_VERIFY_URL" 2>&1 | head -30 || true';
|
|
1126
|
+
if (lane === "mcp") {
|
|
1127
|
+
return `// Greenlight verify spec \u2014 edit to assert this tool's real contract.
|
|
1128
|
+
// \`greenlight preview\` sets GREENLIGHT_PREVIEW=1 (local run, usually no auth); prod runs the full gate.
|
|
1129
|
+
const preview = process.env.GREENLIGHT_PREVIEW === '1';
|
|
1130
|
+
|
|
1131
|
+
export default [
|
|
1132
|
+
{
|
|
1133
|
+
mode: 'mcp',
|
|
1134
|
+
// List the tools this server exposes. exactTools makes the gate FAIL if a tool is added in code
|
|
1135
|
+
// but not here (or removed) \u2014 so "added to the verify loop" is enforced, not optional.
|
|
1136
|
+
expectTools: [],
|
|
1137
|
+
exactTools: true,
|
|
1138
|
+
// Auth-gated server? Require an unauthenticated request to be rejected \u2014 but not under preview
|
|
1139
|
+
// (a local no-auth server can't satisfy it). Add headers:{ Authorization: \`Bearer \${process.env.X}\` }
|
|
1140
|
+
// to run authenticated tools/list against prod.
|
|
1141
|
+
// requireAuthRejection: !preview,
|
|
1142
|
+
// logsOnFailure: '${logHint}',
|
|
1143
|
+
},
|
|
1144
|
+
// Quality (optional): LLM-judged tool output. Runs only with ANTHROPIC_API_KEY (degrades to a
|
|
1145
|
+
// failing check otherwise). Add cases:
|
|
1146
|
+
// { mode: 'eval', cases: [{ name: 'sensible', tool: 'your_tool', rubric: 'what good looks like' }] },
|
|
1147
|
+
];
|
|
1148
|
+
`;
|
|
1149
|
+
}
|
|
1110
1150
|
return `// Greenlight verify spec \u2014 edit to assert this tool's real contract.
|
|
1111
1151
|
export default {
|
|
1112
|
-
|
|
1152
|
+
mode: 'api',
|
|
1153
|
+
checks: [{ path: '/', status: 200 }],
|
|
1113
1154
|
// Telemetry-into-verify: a shell command run ONLY when this report FAILS; its last ~50 lines
|
|
1114
1155
|
// attach to the report so the agent/CI sees the "why" in-loop. $GREENLIGHT_VERIFY_URL is the
|
|
1115
1156
|
// failing URL (no hard-coding). Best-effort (never fails the gate). Uncomment + adjust:
|
|
@@ -1251,12 +1292,13 @@ var MISE_TOML = `# Toolchain, managed by mise. \`mise install\` to set up.
|
|
|
1251
1292
|
node = "24"
|
|
1252
1293
|
pnpm = "10.12.1"
|
|
1253
1294
|
`;
|
|
1254
|
-
function containerBuildYml(name, wrapperRepo) {
|
|
1295
|
+
function containerBuildYml(name, wrapperRepo, testCommand = "make install && make test") {
|
|
1255
1296
|
return `name: greenlight-build
|
|
1256
1297
|
|
|
1257
|
-
# Provider-agnostic: build the container -> push to GHCR -> notify the wrapper to deploy.
|
|
1258
|
-
# The
|
|
1259
|
-
# (a fine-grained PAT with "Contents: write"
|
|
1298
|
+
# Provider-agnostic: TEST -> build the container -> push to GHCR -> notify the wrapper to deploy.
|
|
1299
|
+
# The ship is GATED on the tool's own tests (build \`needs: [test]\`). The wrapper owns the OCI infra
|
|
1300
|
+
# + creds; this repo only needs GREENLIGHT_DISPATCH_TOKEN (a fine-grained PAT with "Contents: write"
|
|
1301
|
+
# on ${wrapperRepo}) to fire the dispatch.
|
|
1260
1302
|
on:
|
|
1261
1303
|
push:
|
|
1262
1304
|
branches: [main]
|
|
@@ -1271,7 +1313,16 @@ concurrency:
|
|
|
1271
1313
|
cancel-in-progress: true
|
|
1272
1314
|
|
|
1273
1315
|
jobs:
|
|
1316
|
+
test:
|
|
1317
|
+
# Ship-gate: the tool's own tests must pass before anything is built or deployed. Customize the
|
|
1318
|
+
# command + add the toolchain setup your tests need (setup-node / setup-python / mise) here.
|
|
1319
|
+
runs-on: ubuntu-latest
|
|
1320
|
+
steps:
|
|
1321
|
+
- uses: actions/checkout@v4
|
|
1322
|
+
- run: ${testCommand}
|
|
1323
|
+
|
|
1274
1324
|
build:
|
|
1325
|
+
needs: [test]
|
|
1275
1326
|
# Native arm64 runner \u2014 builds the arm64 image directly (no QEMU emulation, much faster).
|
|
1276
1327
|
runs-on: ubuntu-24.04-arm
|
|
1277
1328
|
steps:
|
|
@@ -1584,7 +1635,18 @@ async function adoptWrapper(ctx) {
|
|
|
1584
1635
|
envs,
|
|
1585
1636
|
dir: toolRel,
|
|
1586
1637
|
external: true,
|
|
1587
|
-
adopted: true
|
|
1638
|
+
adopted: true,
|
|
1639
|
+
// oci has no built-in local serve — scaffold a `preview` descriptor so the uniform local gate
|
|
1640
|
+
// (`greenlight preview <name>`) works. Default: a docker `preview` profile matching the prod
|
|
1641
|
+
// transport (the tool adds that profile to its compose). Edit the command/port/path to fit.
|
|
1642
|
+
...target === "oci" ? {
|
|
1643
|
+
preview: {
|
|
1644
|
+
command: "docker compose --profile preview up",
|
|
1645
|
+
teardown: "docker compose --profile preview down -v",
|
|
1646
|
+
port: 8e3,
|
|
1647
|
+
path: lane === "mcp" ? "/mcp" : ""
|
|
1648
|
+
}
|
|
1649
|
+
} : {}
|
|
1588
1650
|
});
|
|
1589
1651
|
writeFileSync3(regPath, serializeConfig(nextReg));
|
|
1590
1652
|
console.log(
|
|
@@ -1892,6 +1954,24 @@ import { join as join4 } from "path";
|
|
|
1892
1954
|
function dirCheck(label, dir) {
|
|
1893
1955
|
return existsSync7(dir) ? { name: `${label}: directory`, status: "ok" } : { name: `${label}: directory`, status: "fail", detail: `missing ${dir}` };
|
|
1894
1956
|
}
|
|
1957
|
+
function conformanceChecks(t, root) {
|
|
1958
|
+
const out = [];
|
|
1959
|
+
const specRel = t.external ? `verify/${t.name}.config.ts` : join4(t.dir ?? join4("tools", t.name), "verify.config.ts");
|
|
1960
|
+
const hasSpec = existsSync7(join4(root, specRel));
|
|
1961
|
+
out.push({
|
|
1962
|
+
name: `${t.name}: in the verify loop`,
|
|
1963
|
+
status: hasSpec ? "ok" : "warn",
|
|
1964
|
+
detail: hasSpec ? specRel : `no ${specRel} \u2014 verify falls back to the lane default`
|
|
1965
|
+
});
|
|
1966
|
+
const builtIn = !t.external && t.target === "workers";
|
|
1967
|
+
const gateable = Boolean(t.preview) || builtIn;
|
|
1968
|
+
out.push({
|
|
1969
|
+
name: `${t.name}: local preview gate`,
|
|
1970
|
+
status: gateable ? "ok" : "warn",
|
|
1971
|
+
detail: gateable ? void 0 : `no built-in serve for ${t.external ? "an external " : ""}${t.target} tool \u2014 add preview:{ command, \u2026 } so \`greenlight preview ${t.name}\` works`
|
|
1972
|
+
});
|
|
1973
|
+
return out;
|
|
1974
|
+
}
|
|
1895
1975
|
function runDoctor(config, root) {
|
|
1896
1976
|
const checks = [];
|
|
1897
1977
|
if (config.blog) checks.push(dirCheck("blog", join4(root, "apps/blog")));
|
|
@@ -1904,18 +1984,10 @@ function runDoctor(config, root) {
|
|
|
1904
1984
|
mcp: t.lane === "mcp"
|
|
1905
1985
|
});
|
|
1906
1986
|
checks.push({ name: `${t.name}: external (registry)`, status: "ok", detail: url });
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
const dir = join4(root, t.dir ?? join4("tools", t.name));
|
|
1910
|
-
checks.push(dirCheck(t.name, dir));
|
|
1911
|
-
if (t.lane === "mcp") {
|
|
1912
|
-
const vc = join4(dir, "verify.config.ts");
|
|
1913
|
-
checks.push({
|
|
1914
|
-
name: `${t.name}: verify.config.ts`,
|
|
1915
|
-
status: existsSync7(vc) ? "ok" : "warn",
|
|
1916
|
-
detail: existsSync7(vc) ? void 0 : "missing \u2014 verify will use the lane default"
|
|
1917
|
-
});
|
|
1987
|
+
} else {
|
|
1988
|
+
checks.push(dirCheck(t.name, join4(root, t.dir ?? join4("tools", t.name))));
|
|
1918
1989
|
}
|
|
1990
|
+
checks.push(...conformanceChecks(t, root));
|
|
1919
1991
|
}
|
|
1920
1992
|
const needsKeepalive = config.tools.filter((t) => t.data === "supabase" || t.target === "oci");
|
|
1921
1993
|
checks.push({
|
|
@@ -2356,18 +2428,57 @@ async function waitForServer(url, timeoutMs = 3e4) {
|
|
|
2356
2428
|
}
|
|
2357
2429
|
return false;
|
|
2358
2430
|
}
|
|
2359
|
-
async function
|
|
2360
|
-
const
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
const
|
|
2367
|
-
|
|
2368
|
-
|
|
2431
|
+
async function loadSpecs(entry) {
|
|
2432
|
+
const loaded = (entry.external && entry.name ? await loadExternalVerifySpec(entry.name) : await loadVerifySpec(entry.dir)) ?? defaultSpec(entry.lane);
|
|
2433
|
+
return Array.isArray(loaded) ? loaded : [loaded];
|
|
2434
|
+
}
|
|
2435
|
+
async function verifyLocal(entry, url) {
|
|
2436
|
+
process.env.GREENLIGHT_PREVIEW = "1";
|
|
2437
|
+
process.env.GREENLIGHT_VERIFY_URL = url;
|
|
2438
|
+
const specs = await loadSpecs(entry);
|
|
2439
|
+
const toolDir = resolve10(process.cwd(), entry.dir ?? ".");
|
|
2440
|
+
const reports = await verifyAll(url, specs, { toolDir });
|
|
2441
|
+
for (const report of reports) printReport(report);
|
|
2442
|
+
return allPass(reports);
|
|
2443
|
+
}
|
|
2444
|
+
async function previewViaDescriptor(entry, name, portOverride) {
|
|
2445
|
+
const pv = entry.preview;
|
|
2446
|
+
const lane = servePlan(entry.lane);
|
|
2447
|
+
const port = portOverride ?? pv.port ?? entry.port ?? lane.port;
|
|
2448
|
+
const path = pv.path ?? lane.path;
|
|
2449
|
+
const url = `http://localhost:${port}${path}`;
|
|
2450
|
+
const toolDir = resolve10(process.cwd(), entry.dir ?? ".");
|
|
2451
|
+
console.log(`preview ${name}: ${pv.command} (\u2192 ${url})`);
|
|
2452
|
+
const child = spawn(pv.command, {
|
|
2453
|
+
cwd: toolDir,
|
|
2454
|
+
shell: true,
|
|
2455
|
+
stdio: "inherit",
|
|
2456
|
+
detached: true,
|
|
2457
|
+
env: { ...process.env, PORT: String(port), GREENLIGHT_PREVIEW: "1" }
|
|
2458
|
+
});
|
|
2459
|
+
try {
|
|
2460
|
+
if (!await waitForServer(url, 12e4)) {
|
|
2461
|
+
throw new Error(`preview server did not become reachable at ${url} (check: ${pv.command})`);
|
|
2462
|
+
}
|
|
2463
|
+
return await verifyLocal(entry, url);
|
|
2464
|
+
} finally {
|
|
2465
|
+
if (pv.teardown) {
|
|
2466
|
+
try {
|
|
2467
|
+
execFileSync4(pv.teardown, { cwd: toolDir, shell: true, stdio: "inherit" });
|
|
2468
|
+
} catch {
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
if (child.pid) {
|
|
2472
|
+
try {
|
|
2473
|
+
process.kill(-child.pid, "SIGTERM");
|
|
2474
|
+
} catch {
|
|
2475
|
+
child.kill("SIGTERM");
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2369
2478
|
}
|
|
2370
|
-
|
|
2479
|
+
}
|
|
2480
|
+
async function previewViaBuiltIn(entry, name, portOverride) {
|
|
2481
|
+
const plan = servePlan(entry.lane, portOverride);
|
|
2371
2482
|
if (plan.build) {
|
|
2372
2483
|
console.log(`build ${name} (${entry.dir})`);
|
|
2373
2484
|
execFileSync4("pnpm", ["-C", entry.dir, "run", "build"], { stdio: "inherit" });
|
|
@@ -2380,7 +2491,6 @@ async function previewCommand(args) {
|
|
|
2380
2491
|
stdio: "ignore",
|
|
2381
2492
|
detached: true
|
|
2382
2493
|
});
|
|
2383
|
-
let pass = false;
|
|
2384
2494
|
try {
|
|
2385
2495
|
const base = `http://localhost:${plan.port}`;
|
|
2386
2496
|
if (!await waitForServer(base)) {
|
|
@@ -2388,12 +2498,7 @@ async function previewCommand(args) {
|
|
|
2388
2498
|
`server did not start on :${plan.port} (check the tool's ${plan.script} script)`
|
|
2389
2499
|
);
|
|
2390
2500
|
}
|
|
2391
|
-
|
|
2392
|
-
const specs = Array.isArray(loaded) ? loaded : [loaded];
|
|
2393
|
-
const toolDir = resolve10(process.cwd(), entry.dir ?? ".");
|
|
2394
|
-
const reports = await verifyAll(base + plan.path, specs, { toolDir });
|
|
2395
|
-
for (const report of reports) printReport(report);
|
|
2396
|
-
pass = allPass(reports);
|
|
2501
|
+
return await verifyLocal(entry, base + plan.path);
|
|
2397
2502
|
} finally {
|
|
2398
2503
|
if (child.pid) {
|
|
2399
2504
|
try {
|
|
@@ -2403,6 +2508,26 @@ async function previewCommand(args) {
|
|
|
2403
2508
|
}
|
|
2404
2509
|
}
|
|
2405
2510
|
}
|
|
2511
|
+
}
|
|
2512
|
+
async function previewCommand(args) {
|
|
2513
|
+
const name = args[0];
|
|
2514
|
+
if (!name || name.startsWith("-")) {
|
|
2515
|
+
throw new Error("usage: greenlight preview <name> [--port <n>]");
|
|
2516
|
+
}
|
|
2517
|
+
const portArg = flag7(args, "--port");
|
|
2518
|
+
const port = portArg ? Number(portArg) : void 0;
|
|
2519
|
+
const { config } = await loadManifest();
|
|
2520
|
+
const entry = resolveEntry(config, name);
|
|
2521
|
+
let pass;
|
|
2522
|
+
if (entry.preview) {
|
|
2523
|
+
pass = await previewViaDescriptor(entry, name, port);
|
|
2524
|
+
} else if (entry.external) {
|
|
2525
|
+
throw new Error(
|
|
2526
|
+
`"${name}" is external and has no preview descriptor \u2014 add preview:{ command, \u2026 } to its manifest entry (e.g. a docker command), or preview it from its own repo`
|
|
2527
|
+
);
|
|
2528
|
+
} else {
|
|
2529
|
+
pass = await previewViaBuiltIn(entry, name, port);
|
|
2530
|
+
}
|
|
2406
2531
|
process.exit(pass ? 0 : 1);
|
|
2407
2532
|
}
|
|
2408
2533
|
|
|
@@ -2482,6 +2607,86 @@ async function promoteCommand(args) {
|
|
|
2482
2607
|
process.exit(result.promoted ? 0 : 1);
|
|
2483
2608
|
}
|
|
2484
2609
|
|
|
2610
|
+
// src/commands/status.ts
|
|
2611
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
2612
|
+
function repoSlug(dir) {
|
|
2613
|
+
try {
|
|
2614
|
+
const url = execFileSync6("git", ["-C", dir, "remote", "get-url", "origin"], {
|
|
2615
|
+
encoding: "utf8"
|
|
2616
|
+
}).trim();
|
|
2617
|
+
const m = url.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
2618
|
+
return m?.[1] ?? null;
|
|
2619
|
+
} catch {
|
|
2620
|
+
return null;
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
function workflowsFor(entry, name, wrapper, toolRepo) {
|
|
2624
|
+
if (!entry.external) {
|
|
2625
|
+
return [{ repo: wrapper, workflow: "deploy.yml", label: "deploy + verify" }];
|
|
2626
|
+
}
|
|
2627
|
+
if (entry.target === "oci") {
|
|
2628
|
+
return [
|
|
2629
|
+
{
|
|
2630
|
+
repo: toolRepo,
|
|
2631
|
+
workflow: "greenlight-build.yml",
|
|
2632
|
+
label: "build (test \u2192 image \u2192 dispatch)"
|
|
2633
|
+
},
|
|
2634
|
+
{ repo: wrapper, workflow: `greenlight-deploy-${name}.yml`, label: "deploy + verify (prod)" },
|
|
2635
|
+
{ repo: wrapper, workflow: `greenlight-remediate-${name}.yml`, label: "self-heal" }
|
|
2636
|
+
];
|
|
2637
|
+
}
|
|
2638
|
+
if (entry.target === "vercel") {
|
|
2639
|
+
return [
|
|
2640
|
+
{ repo: toolRepo, workflow: "greenlight-verify.yml", label: "verify (deployment_status)" }
|
|
2641
|
+
];
|
|
2642
|
+
}
|
|
2643
|
+
return [{ repo: toolRepo, workflow: "deploy.yml", label: "deploy + verify" }];
|
|
2644
|
+
}
|
|
2645
|
+
function lastRun(repo, workflow) {
|
|
2646
|
+
try {
|
|
2647
|
+
const out = execFileSync6(
|
|
2648
|
+
"gh",
|
|
2649
|
+
[
|
|
2650
|
+
"run",
|
|
2651
|
+
"list",
|
|
2652
|
+
"--repo",
|
|
2653
|
+
repo,
|
|
2654
|
+
"--workflow",
|
|
2655
|
+
workflow,
|
|
2656
|
+
"--limit",
|
|
2657
|
+
"1",
|
|
2658
|
+
"--json",
|
|
2659
|
+
"status,conclusion,displayTitle,url"
|
|
2660
|
+
],
|
|
2661
|
+
{ encoding: "utf8" }
|
|
2662
|
+
);
|
|
2663
|
+
const runs = JSON.parse(out);
|
|
2664
|
+
const r = runs[0];
|
|
2665
|
+
if (!r) return "no runs";
|
|
2666
|
+
const state = r.status === "completed" ? r.conclusion : r.status;
|
|
2667
|
+
const icon = state === "success" ? "\u2714" : state === "failure" ? "\u2718" : "\xB7";
|
|
2668
|
+
return `${icon} ${state} ${r.displayTitle}
|
|
2669
|
+
${r.url}`;
|
|
2670
|
+
} catch (e) {
|
|
2671
|
+
return `(gh unavailable \u2014 ${e instanceof Error ? e.message.split("\n")[0] : "error"})`;
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
async function statusCommand(args) {
|
|
2675
|
+
const name = args[0];
|
|
2676
|
+
if (!name || name.startsWith("-")) throw new Error("usage: greenlight status <name>");
|
|
2677
|
+
const { config } = await loadManifest();
|
|
2678
|
+
const entry = resolveEntry(config, name);
|
|
2679
|
+
const wrapper = repoSlug(process.cwd()) ?? "(this repo)";
|
|
2680
|
+
const toolRepo = entry.external ? repoSlug(entry.dir) ?? "(tool repo)" : wrapper;
|
|
2681
|
+
console.log(`status: ${name} (${entry.lane}/${entry.target})
|
|
2682
|
+
`);
|
|
2683
|
+
for (const w of workflowsFor(entry, name, wrapper, toolRepo)) {
|
|
2684
|
+
console.log(` ${w.label} [${w.repo} \xB7 ${w.workflow}]`);
|
|
2685
|
+
console.log(` ${lastRun(w.repo, w.workflow)}
|
|
2686
|
+
`);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2485
2690
|
// src/bin.ts
|
|
2486
2691
|
var HELP = `greenlight <command>
|
|
2487
2692
|
|
|
@@ -2492,6 +2697,7 @@ var HELP = `greenlight <command>
|
|
|
2492
2697
|
preview <name> [--port <n>] build + serve locally + verify (one command)
|
|
2493
2698
|
verify <name> [--env <env> | --url <url>] run the verify harness against the URL
|
|
2494
2699
|
promote <name> [--perform] [--push] gated develop -> main fast-forward
|
|
2700
|
+
status <name> last ship/deploy/verify run for a tool (via gh)
|
|
2495
2701
|
secrets gather <name> [--repo o/r] [--env e] guided, link-first token prompts -> GitHub secrets (no disk/logs)
|
|
2496
2702
|
secrets sync [--repo o/r] [--env <env>] push .greenlight/secrets.env -> GitHub Actions secrets
|
|
2497
2703
|
agent sync write the loop skill + CLAUDE.md block into this repo
|
|
@@ -2523,6 +2729,8 @@ async function main() {
|
|
|
2523
2729
|
return verifyCommand(args);
|
|
2524
2730
|
case "promote":
|
|
2525
2731
|
return promoteCommand(args);
|
|
2732
|
+
case "status":
|
|
2733
|
+
return statusCommand(args);
|
|
2526
2734
|
case "secrets":
|
|
2527
2735
|
return secretsCommand(args);
|
|
2528
2736
|
case "agent":
|
|
@@ -42,7 +42,21 @@ var ToolSchema = z.object({
|
|
|
42
42
|
dir: z.string().optional(),
|
|
43
43
|
// The tool's code lives in another repo — this entry is a registry pointer only,
|
|
44
44
|
// not built/deployed here (greenlight-v1.md §15.5 poly-repo).
|
|
45
|
-
external: z.boolean().default(false)
|
|
45
|
+
external: z.boolean().default(false),
|
|
46
|
+
// How `greenlight preview` spins the tool up LOCALLY for the pre-deploy gate. Optional — node
|
|
47
|
+
// lanes (astro/next/mcp→workers) use the built-in build+serve path. Set it for targets with no
|
|
48
|
+
// built-in serve (e.g. oci: a docker command that matches the prod transport). The harness polls
|
|
49
|
+
// the local URL (http://localhost:<port><path>), verifies, then runs `teardown`.
|
|
50
|
+
preview: z.object({
|
|
51
|
+
command: z.string(),
|
|
52
|
+
// spin up locally in the background (e.g. a `docker compose … up`)
|
|
53
|
+
teardown: z.string().optional(),
|
|
54
|
+
// tear down afterwards (e.g. `docker compose … down`)
|
|
55
|
+
port: z.number().int().positive().optional(),
|
|
56
|
+
// local port (default: tool.port ?? lane default)
|
|
57
|
+
path: z.string().optional()
|
|
58
|
+
// connect path (default: lane default, e.g. `/mcp`)
|
|
59
|
+
}).optional()
|
|
46
60
|
}).superRefine((tool, ctx) => {
|
|
47
61
|
const rule = MATRIX[tool.lane];
|
|
48
62
|
if (!rule.targets.includes(tool.target)) {
|
|
@@ -229,12 +243,12 @@ async function verify(baseUrl, spec, opts) {
|
|
|
229
243
|
case "api":
|
|
230
244
|
return verifyApi(baseUrl, spec);
|
|
231
245
|
case "mcp": {
|
|
232
|
-
const { verifyMcp: verifyMcp2 } = await import("./mcp-
|
|
246
|
+
const { verifyMcp: verifyMcp2 } = await import("./mcp-FFLOX4YP.js");
|
|
233
247
|
return verifyMcp2(baseUrl, spec);
|
|
234
248
|
}
|
|
235
249
|
case "playwright": {
|
|
236
|
-
const { verifyPlaywright: verifyPlaywright2 } = await import("./playwright-
|
|
237
|
-
return verifyPlaywright2(baseUrl, spec);
|
|
250
|
+
const { verifyPlaywright: verifyPlaywright2 } = await import("./playwright-SGRK3I4I.js");
|
|
251
|
+
return verifyPlaywright2(baseUrl, spec, opts?.toolDir ?? process.cwd());
|
|
238
252
|
}
|
|
239
253
|
case "test": {
|
|
240
254
|
const { verifyTest: verifyTest2 } = await import("./test-7GMOU7I5.js");
|
|
@@ -32,6 +32,20 @@ async function verifyMcp(baseUrl, spec) {
|
|
|
32
32
|
detail: has ? void 0 : `got [${names.join(", ")}]`
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
+
if (spec.exactTools) {
|
|
36
|
+
const expected = new Set(spec.expectTools);
|
|
37
|
+
const extra = names.filter((n) => !expected.has(n));
|
|
38
|
+
const missing = spec.expectTools.filter((t) => !names.includes(t));
|
|
39
|
+
const drift = [
|
|
40
|
+
extra.length ? `unexpected: [${extra.join(", ")}]` : "",
|
|
41
|
+
missing.length ? `missing: [${missing.join(", ")}]` : ""
|
|
42
|
+
].filter(Boolean).join("; ");
|
|
43
|
+
checks.push({
|
|
44
|
+
name: "tools/list matches expectTools exactly",
|
|
45
|
+
pass: drift.length === 0,
|
|
46
|
+
detail: drift || void 0
|
|
47
|
+
});
|
|
48
|
+
}
|
|
35
49
|
} catch (e) {
|
|
36
50
|
checks.push({ name: "tools/list", pass: false, detail: msg(e) });
|
|
37
51
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
msg,
|
|
3
|
+
report
|
|
4
|
+
} from "./chunk-QFKE5JKC.js";
|
|
5
|
+
|
|
6
|
+
// ../packages/verify/src/playwright.ts
|
|
7
|
+
import { spawnSync } from "child_process";
|
|
8
|
+
async function verifyPlaywright(baseUrl, spec, toolDir = process.cwd()) {
|
|
9
|
+
const checks = [];
|
|
10
|
+
if (spec.suite) checks.push(runSuite(baseUrl, spec.suite, toolDir));
|
|
11
|
+
if (spec.renders?.length) checks.push(...await runRenders(baseUrl, spec.renders));
|
|
12
|
+
if (checks.length === 0) {
|
|
13
|
+
checks.push({
|
|
14
|
+
name: "playwright spec",
|
|
15
|
+
pass: false,
|
|
16
|
+
detail: "nothing to run \u2014 set `renders` and/or `suite`"
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return report("playwright", baseUrl, checks);
|
|
20
|
+
}
|
|
21
|
+
function summarize(output) {
|
|
22
|
+
const lines = output.split("\n");
|
|
23
|
+
const hit = lines.find(
|
|
24
|
+
(l) => /\d+\s+(passed|failed|flaky|skipped)|Tests?\s+\d+\s+(passed|failed)|Tests:/.test(l)
|
|
25
|
+
);
|
|
26
|
+
if (hit) return hit.trim();
|
|
27
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
28
|
+
const l = lines[i];
|
|
29
|
+
if (l?.trim()) return l.trim();
|
|
30
|
+
}
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
function runSuite(baseUrl, suite, toolDir) {
|
|
34
|
+
const command = suite.command ?? "pnpm exec playwright test";
|
|
35
|
+
const cwd = suite.cwd ?? toolDir;
|
|
36
|
+
try {
|
|
37
|
+
const res = spawnSync(command, {
|
|
38
|
+
cwd,
|
|
39
|
+
shell: true,
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
timeout: suite.timeoutMs ?? 6e5,
|
|
42
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
43
|
+
env: {
|
|
44
|
+
...process.env,
|
|
45
|
+
// The deployed URL the suite must target. PLAYWRIGHT_BASE_URL is what a stock
|
|
46
|
+
// playwright.config reads for `use.baseURL`; GREENLIGHT_VERIFY_URL mirrors the rest of the
|
|
47
|
+
// harness so one convention works everywhere.
|
|
48
|
+
PLAYWRIGHT_BASE_URL: baseUrl,
|
|
49
|
+
GREENLIGHT_VERIFY_URL: baseUrl,
|
|
50
|
+
...suite.env
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
if (res.error) {
|
|
54
|
+
return { name: command, pass: false, detail: msg(res.error) };
|
|
55
|
+
}
|
|
56
|
+
const out = `${res.stdout ?? ""}${res.stderr ?? ""}`;
|
|
57
|
+
const summary = summarize(out);
|
|
58
|
+
const pass = res.status === 0;
|
|
59
|
+
return {
|
|
60
|
+
name: `suite: ${command}`,
|
|
61
|
+
pass,
|
|
62
|
+
detail: pass ? summary : `exit ${res.status ?? "signal"}${summary ? ` \u2014 ${summary}` : ""}`
|
|
63
|
+
};
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return { name: command, pass: false, detail: msg(e) };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function runRenders(baseUrl, renders) {
|
|
69
|
+
let chromium;
|
|
70
|
+
try {
|
|
71
|
+
({ chromium } = await import("playwright"));
|
|
72
|
+
} catch {
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
name: "playwright available",
|
|
76
|
+
pass: false,
|
|
77
|
+
detail: "playwright not installed \u2014 run `pnpm add playwright && pnpm exec playwright install chromium`"
|
|
78
|
+
}
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
82
|
+
const checks = [];
|
|
83
|
+
let browser;
|
|
84
|
+
try {
|
|
85
|
+
browser = await chromium.launch();
|
|
86
|
+
} catch (e) {
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
name: "launch browser",
|
|
90
|
+
pass: false,
|
|
91
|
+
detail: `${msg(e)} (try \`playwright install chromium\`)`
|
|
92
|
+
}
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
for (const path of renders) {
|
|
97
|
+
const page = await browser.newPage();
|
|
98
|
+
try {
|
|
99
|
+
const res = await page.goto(base + path, { waitUntil: "domcontentloaded" });
|
|
100
|
+
const ok = res?.ok() ?? false;
|
|
101
|
+
const aria = await page.locator("body").ariaSnapshot();
|
|
102
|
+
const nonEmpty = aria.trim().length > 0;
|
|
103
|
+
checks.push({
|
|
104
|
+
name: `renders ${path}`,
|
|
105
|
+
pass: ok && nonEmpty,
|
|
106
|
+
detail: !ok ? `status ${res?.status() ?? "none"}` : nonEmpty ? void 0 : "empty accessibility tree"
|
|
107
|
+
});
|
|
108
|
+
} catch (e) {
|
|
109
|
+
checks.push({ name: `renders ${path}`, pass: false, detail: msg(e) });
|
|
110
|
+
} finally {
|
|
111
|
+
await page.close();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
await browser.close();
|
|
116
|
+
}
|
|
117
|
+
return checks;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export {
|
|
121
|
+
verifyPlaywright
|
|
122
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,9 @@ import {
|
|
|
2
2
|
defineConfig,
|
|
3
3
|
defineVerify,
|
|
4
4
|
loadConfig
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-6USV5AQV.js";
|
|
6
|
+
import "./chunk-HX7VA25D.js";
|
|
7
|
+
import "./chunk-N3IKUCSF.js";
|
|
8
8
|
import "./chunk-KP3Y6WRU.js";
|
|
9
9
|
import "./chunk-UXHHLEYO.js";
|
|
10
10
|
import "./chunk-6N7MD6FR.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rtrentjones/greenlight",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"description": "Greenlight CLI — setup and lifecycle for the harness.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@rtrentjones/greenlight-adapters": "0.2.4",
|
|
35
|
-
"@rtrentjones/greenlight-
|
|
35
|
+
"@rtrentjones/greenlight-shared": "0.2.4",
|
|
36
36
|
"@rtrentjones/greenlight-verify": "0.2.4",
|
|
37
|
-
"@rtrentjones/greenlight-
|
|
37
|
+
"@rtrentjones/greenlight-loop": "0.2.4"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "node scripts/copy-assets.mjs && tsup",
|
package/dist/chunk-WFZTRXBF.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
msg,
|
|
3
|
-
report
|
|
4
|
-
} from "./chunk-QFKE5JKC.js";
|
|
5
|
-
|
|
6
|
-
// ../packages/verify/src/playwright.ts
|
|
7
|
-
async function verifyPlaywright(baseUrl, spec) {
|
|
8
|
-
let chromium;
|
|
9
|
-
try {
|
|
10
|
-
({ chromium } = await import("playwright"));
|
|
11
|
-
} catch {
|
|
12
|
-
return report("playwright", baseUrl, [
|
|
13
|
-
{
|
|
14
|
-
name: "playwright available",
|
|
15
|
-
pass: false,
|
|
16
|
-
detail: "playwright not installed \u2014 run `pnpm add playwright && pnpm exec playwright install chromium`"
|
|
17
|
-
}
|
|
18
|
-
]);
|
|
19
|
-
}
|
|
20
|
-
const base = baseUrl.replace(/\/+$/, "");
|
|
21
|
-
const checks = [];
|
|
22
|
-
let browser;
|
|
23
|
-
try {
|
|
24
|
-
browser = await chromium.launch();
|
|
25
|
-
} catch (e) {
|
|
26
|
-
return report("playwright", baseUrl, [
|
|
27
|
-
{
|
|
28
|
-
name: "launch browser",
|
|
29
|
-
pass: false,
|
|
30
|
-
detail: `${msg(e)} (try \`playwright install chromium\`)`
|
|
31
|
-
}
|
|
32
|
-
]);
|
|
33
|
-
}
|
|
34
|
-
try {
|
|
35
|
-
for (const path of spec.renders) {
|
|
36
|
-
const page = await browser.newPage();
|
|
37
|
-
try {
|
|
38
|
-
const res = await page.goto(base + path, { waitUntil: "domcontentloaded" });
|
|
39
|
-
const ok = res?.ok() ?? false;
|
|
40
|
-
const aria = await page.locator("body").ariaSnapshot();
|
|
41
|
-
const nonEmpty = aria.trim().length > 0;
|
|
42
|
-
checks.push({
|
|
43
|
-
name: `renders ${path}`,
|
|
44
|
-
pass: ok && nonEmpty,
|
|
45
|
-
detail: !ok ? `status ${res?.status() ?? "none"}` : nonEmpty ? void 0 : "empty accessibility tree"
|
|
46
|
-
});
|
|
47
|
-
} catch (e) {
|
|
48
|
-
checks.push({ name: `renders ${path}`, pass: false, detail: msg(e) });
|
|
49
|
-
} finally {
|
|
50
|
-
await page.close();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
} finally {
|
|
54
|
-
await browser.close();
|
|
55
|
-
}
|
|
56
|
-
return report("playwright", baseUrl, checks);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export {
|
|
60
|
-
verifyPlaywright
|
|
61
|
-
};
|