@rtrentjones/greenlight 0.2.7 → 0.2.9

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.
@@ -26,6 +26,29 @@ Manages the **existing** project (nothing to import — it configures by id):
26
26
 
27
27
  The DNS CNAME is the **cloudflare** `tool` module, unproxied (`proxied = false`) → `cname.vercel-dns.com`.
28
28
 
29
+ ## The verify loop — tool-CI on `deployment_status`
30
+
31
+ Because Vercel deploys (not the wrapper), the verify gate runs in the **tool repo's own CI**, not a
32
+ wrapper deploy listener. `greenlight adopt … --target vercel` emits, into the tool repo:
33
+ - **`.github/workflows/greenlight-verify.yml`** — triggers on GitHub's **`deployment_status`** event
34
+ (Vercel posts a deployment + `target_url`); on `state == success` it runs
35
+ `npx @rtrentjones/greenlight verify --url <target_url> --spec verify/<name>.config.ts`. The result
36
+ is a check on the commit — no wrapper round-trip, no dispatch/status PATs (Vercel owns deploy + URL
37
+ + its own statuses).
38
+ - **`verify/<name>.config.ts`** — a verifyAll array: `api` (deployed URL 200) + `test` (the tool's
39
+ suite) + `agent-web` (LLM drives the live UI), where agent-web is **config-gated on
40
+ `ANTHROPIC_API_KEY`** (omitted when unset → the gate stays green on api + test alone).
41
+
42
+ `greenlight verify --url <url> --spec <path>` is the **manifest-free** mode that makes this work
43
+ without carrying the wrapper's `greenlight.config.ts` into the tool repo.
44
+
45
+ **Deployment Protection gotcha:** `deployment_status.target_url` is the `*.vercel.app` *deployment*
46
+ URL, which Vercel **Deployment Protection** gates (→ **401**) even though the public custom domain
47
+ is 200. To verify the real app, create a **Protection Bypass for Automation** secret (Vercel →
48
+ project → Settings → Deployment Protection) and set it as `VERCEL_AUTOMATION_BYPASS_SECRET` on the
49
+ tool repo — the api check sends it as `x-vercel-protection-bypass` and asserts 200. Without it the
50
+ generated spec asserts **401** (the deployment is served + protected), so the gate stays green.
51
+
29
52
  ## MCP
30
53
 
31
54
  `.mcp.json` wires `vercel` (hosted, OAuth, read-only). Run `/mcp` and authenticate in the
package/dist/bin.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  loadConfig,
6
6
  resolveUrl,
7
7
  verifyAll
8
- } from "./chunk-VONSDNH4.js";
8
+ } from "./chunk-JRCATCRY.js";
9
9
  import "./chunk-ADS6BJJ5.js";
10
10
  import "./chunk-WFZTRXBF.js";
11
11
  import "./chunk-KP3Y6WRU.js";
@@ -105,6 +105,30 @@ function addTool(config, t) {
105
105
  }
106
106
  return result.data;
107
107
  }
108
+ function upsertTool(config, t) {
109
+ if (t.name === "blog") throw new Error('"blog" is a reserved name');
110
+ const entry = {
111
+ name: t.name,
112
+ lane: t.lane,
113
+ target: t.target,
114
+ data: t.data ?? "none",
115
+ auth: t.auth ?? "none",
116
+ access: t.access ?? "public",
117
+ envs: t.envs ?? ["beta", "prod"],
118
+ ...t.dir !== void 0 ? { dir: t.dir } : {},
119
+ ...t.adopted ? { adopted: true } : {},
120
+ ...t.external ? { external: true } : {},
121
+ ...t.port !== void 0 ? { port: t.port } : {}
122
+ };
123
+ const tools = config.tools.some((x) => x.name === t.name) ? config.tools.map((x) => x.name === t.name ? entry : x) : [...config.tools, entry];
124
+ const result = ConfigSchema.safeParse({ ...config, tools });
125
+ if (!result.success) {
126
+ throw new Error(
127
+ result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")
128
+ );
129
+ }
130
+ return result.data;
131
+ }
108
132
 
109
133
  // src/manifest.ts
110
134
  import { existsSync as existsSync2 } from "fs";
@@ -390,7 +414,7 @@ function tokensForTool(tool) {
390
414
  }
391
415
 
392
416
  // src/version.ts
393
- var MODULE_REF = "v0.2.7";
417
+ var MODULE_REF = "v0.2.9";
394
418
  var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
395
419
  function moduleSource(module, ref = MODULE_REF) {
396
420
  return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
@@ -1421,6 +1445,75 @@ jobs:
1421
1445
  -f description="\${{ job.status }}"
1422
1446
  `;
1423
1447
  }
1448
+ function verifyWorkflowYml(name) {
1449
+ return `name: greenlight-verify
1450
+
1451
+ # Vercel deploys ${name} on push and posts a deployment_status to GitHub; we verify the exact
1452
+ # deployed URL. ANTHROPIC_API_KEY (optional) enables the agent-web scenarios \u2014 absent \u2192 omitted
1453
+ # (see verify/${name}.config.ts), so the gate stays green on api + test alone.
1454
+ on:
1455
+ deployment_status:
1456
+
1457
+ permissions:
1458
+ contents: read
1459
+ statuses: write
1460
+
1461
+ jobs:
1462
+ verify:
1463
+ if: \${{ github.event.deployment_status.state == 'success' }}
1464
+ runs-on: ubuntu-latest
1465
+ steps:
1466
+ - uses: actions/checkout@v4
1467
+ - uses: actions/setup-node@v4
1468
+ with:
1469
+ node-version: '24'
1470
+ # agent-web needs browsers: add \`- run: npx -y playwright install --with-deps chromium\` when
1471
+ # you set ANTHROPIC_API_KEY. test-mode needs the tool's deps: add a tolerant install step
1472
+ # (\`pnpm install --no-frozen-lockfile\`) \u2014 but unit tests usually belong in the tool's PR CI.
1473
+ - name: Verify the deployment
1474
+ env:
1475
+ # Bypass Vercel Deployment Protection on the deployment URL (Vercel \u2192 project \u2192 Deployment
1476
+ # Protection \u2192 Protection Bypass for Automation). Without it the gate asserts 401 (served).
1477
+ VERCEL_AUTOMATION_BYPASS_SECRET: \${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
1478
+ ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
1479
+ run: npx -y @rtrentjones/greenlight@latest verify --url "\${{ github.event.deployment_status.target_url }}" --spec verify/${name}.config.ts
1480
+ `;
1481
+ }
1482
+ function nextVerifyConfig(name) {
1483
+ return `// Greenlight verify spec for ${name} (next/vercel) \u2014 run by .github/workflows/greenlight-verify.yml
1484
+ // after Vercel deploys (deployment_status). An array combines modes (allPass):
1485
+ // - api: deployment_status' target_url is the *.vercel.app deployment URL, which Vercel Deployment
1486
+ // Protection gates (401). With VERCEL_AUTOMATION_BYPASS_SECRET set we send the bypass header and
1487
+ // assert 200 (the real app); without it we assert 401 (the deployment is served + protected).
1488
+ // - agent-web: an LLM drives the live UI; runs ONLY when ANTHROPIC_API_KEY is set (else omitted,
1489
+ // so the gate stays green). Replace the scenario with real user tasks + assertions.
1490
+ // Unit tests belong in this repo's PR CI; to also gate the deploy on them, add
1491
+ // { mode: 'test', command: 'pnpm test' } + a tolerant deps-install step in greenlight-verify.yml.
1492
+ const bypass = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
1493
+ const api = bypass
1494
+ ? {
1495
+ mode: 'api',
1496
+ checks: [{ path: '/', status: 200, requestHeaders: { 'x-vercel-protection-bypass': bypass } }],
1497
+ }
1498
+ : { mode: 'api', checks: [{ path: '/', status: 401 }] };
1499
+ const agentWeb = process.env.ANTHROPIC_API_KEY
1500
+ ? [
1501
+ {
1502
+ mode: 'agent-web',
1503
+ scenarios: [
1504
+ {
1505
+ name: 'home renders',
1506
+ task: 'Open the home page and confirm the app loads without an error screen.',
1507
+ asserts: [{ selector: 'body' }],
1508
+ },
1509
+ ],
1510
+ },
1511
+ ]
1512
+ : [];
1513
+
1514
+ export default [api, ...agentWeb];
1515
+ `;
1516
+ }
1424
1517
  function writeIfAbsent(path, contents, label) {
1425
1518
  if (existsSync7(path)) {
1426
1519
  console.log(`\xB7 ${label} exists \u2014 left as-is`);
@@ -1451,8 +1544,8 @@ async function adoptCommand(args) {
1451
1544
  "run adopt from your site repo (needs a real greenlight.config.ts; run `greenlight init` first)"
1452
1545
  );
1453
1546
  }
1454
- if (reg.tools.some((t) => t.name === name) || name === "blog") {
1455
- throw new Error(`"${name}" already in the registry`);
1547
+ if (name === "blog") {
1548
+ throw new Error('"blog" is the apex site, not an adopted tool');
1456
1549
  }
1457
1550
  const domain = flag3(args, "--domain") ?? reg.domain;
1458
1551
  const ctx = { name, repoArg, lane, target, data, auth, envs, domain, reg, regPath };
@@ -1479,7 +1572,8 @@ async function adoptWrapper(ctx) {
1479
1572
  } else {
1480
1573
  console.log(`\xB7 ${toolRel} exists \u2014 skipping submodule add`);
1481
1574
  }
1482
- const nextReg = addTool(reg, {
1575
+ const existed = reg.tools.some((x) => x.name === name);
1576
+ const nextReg = upsertTool(reg, {
1483
1577
  name,
1484
1578
  lane,
1485
1579
  target,
@@ -1491,18 +1585,22 @@ async function adoptWrapper(ctx) {
1491
1585
  adopted: true
1492
1586
  });
1493
1587
  writeFileSync4(regPath, serializeConfig(nextReg));
1494
- console.log(`\u2714 registered "${name}" (external, dir ${toolRel}) in the wrapper manifest`);
1588
+ console.log(
1589
+ `\u2714 ${existed ? "updated" : "registered"} "${name}" (external, dir ${toolRel}) in the wrapper manifest`
1590
+ );
1495
1591
  const slug = parseRepo(repoArg) ?? parseRepo(safeGit(dest, ["remote", "get-url", "origin"])) ?? `OWNER/${name}`;
1496
1592
  writeIfAbsent(
1497
1593
  join2(cwd, `infra/${name}.tf`),
1498
1594
  emitToolTf({ name, domain, lane, target, data, envs, slug, external: true }),
1499
1595
  `infra/${name}.tf`
1500
1596
  );
1501
- writeIfAbsent(
1502
- join2(cwd, `verify/${name}.config.ts`),
1503
- starterVerifyConfig(lane),
1504
- `verify/${name}.config.ts`
1505
- );
1597
+ if (target !== "vercel") {
1598
+ writeIfAbsent(
1599
+ join2(cwd, `verify/${name}.config.ts`),
1600
+ starterVerifyConfig(lane),
1601
+ `verify/${name}.config.ts`
1602
+ );
1603
+ }
1506
1604
  const providers = providersForTool({ target, data });
1507
1605
  if (existsSync7(join2(cwd, "infra/main.tf")) && providers.some((p) => p !== "cloudflare" && p !== "github")) {
1508
1606
  console.log(`\xB7 ensure infra/main.tf declares provider(s): ${providers.join(", ")}`);
@@ -1522,6 +1620,18 @@ async function adoptWrapper(ctx) {
1522
1620
  `${toolRel}/.github/workflows/greenlight-build.yml (provider-agnostic build \u2192 GHCR \u2192 dispatch)`
1523
1621
  );
1524
1622
  }
1623
+ if (target === "vercel") {
1624
+ writeIfAbsent(
1625
+ join2(dest, `verify/${name}.config.ts`),
1626
+ nextVerifyConfig(name),
1627
+ `${toolRel}/verify/${name}.config.ts (tool-CI verify spec)`
1628
+ );
1629
+ writeIfAbsent(
1630
+ join2(dest, ".github/workflows/greenlight-verify.yml"),
1631
+ verifyWorkflowYml(name),
1632
+ `${toolRel}/.github/workflows/greenlight-verify.yml (verify on Vercel deployment_status)`
1633
+ );
1634
+ }
1525
1635
  console.log(`
1526
1636
  Next:
1527
1637
  (in the tool repo) commit the Greenlight kit + build workflow so they travel with the submodule:
@@ -1531,7 +1641,10 @@ Next:
1531
1641
  git commit && git push # CI (infra.yml) applies. Tool's CI builds; wrapper deploys.${target === "oci" ? `
1532
1642
  Secrets (guided): greenlight secrets gather ${name} --repo <wrapper> # TF_VAR_OCI_* + GREENLIGHT_STATUS_TOKEN
1533
1643
  greenlight secrets gather ${name} --repo ${slug} # GREENLIGHT_DISPATCH_TOKEN
1534
- The instance OCID is auto-resolved by the deploy workflow (by display name) \u2014 nothing to set.` : ""}`);
1644
+ The instance OCID is auto-resolved by the deploy workflow (by display name) \u2014 nothing to set.` : target === "vercel" ? `
1645
+ Deploy is Vercel's git integration (no wrapper deploy). The tool's greenlight-verify.yml verifies
1646
+ each deployment (deployment_status). Optional: add ANTHROPIC_API_KEY to ${slug} to enable the
1647
+ agent-web scenarios in verify/${name}.config.ts (absent \u2192 api + test gate alone).` : ""}`);
1535
1648
  }
1536
1649
  async function adoptStandalone(ctx) {
1537
1650
  const { name, repoArg, lane, target, data, auth, envs, domain, reg, regPath } = ctx;
@@ -1936,9 +2049,30 @@ function flag6(args, name) {
1936
2049
  return i >= 0 ? args[i + 1] : void 0;
1937
2050
  }
1938
2051
  async function verifyCommand(args) {
2052
+ const specPath = flag6(args, "--spec");
2053
+ if (specPath) {
2054
+ const url2 = flag6(args, "--url");
2055
+ if (!url2) throw new Error("verify --spec needs --url <deployed-url>");
2056
+ const loaded2 = await loadVerifySpecAt(specPath);
2057
+ if (!loaded2) throw new Error(`no verify spec at ${specPath}`);
2058
+ const specs2 = Array.isArray(loaded2) ? loaded2 : [loaded2];
2059
+ const waitMs = (flag6(args, "--wait") !== void 0 ? Number(flag6(args, "--wait")) : 0) * 1e3;
2060
+ const reports2 = await verifyAll(url2, specs2, {
2061
+ reachableTimeoutMs: waitMs,
2062
+ toolDir: process.cwd()
2063
+ });
2064
+ for (const report of reports2) printReport(report);
2065
+ const pass2 = allPass(reports2);
2066
+ if (reports2.length > 1)
2067
+ console.log(`
2068
+ ${pass2 ? "\u2714 ALL PASS" : "\u2718 FAIL"} (${reports2.length} specs)`);
2069
+ process.exit(pass2 ? 0 : 1);
2070
+ }
1939
2071
  const name = args[0];
1940
2072
  if (!name || name.startsWith("-")) {
1941
- throw new Error("usage: greenlight verify <name> [--env <beta|prod> | --url <url>]");
2073
+ throw new Error(
2074
+ "usage: greenlight verify <name> [--env <beta|prod> | --url <url>] | verify --url <url> --spec <path>"
2075
+ );
1942
2076
  }
1943
2077
  const { config } = await loadManifest();
1944
2078
  const entry = resolveEntry(config, name);
@@ -116,7 +116,7 @@ var trimSlash = (s) => s.replace(/\/+$/, "");
116
116
  async function checkRoute(base, c) {
117
117
  const name = `GET ${c.path}`;
118
118
  try {
119
- const res = await fetch(base + c.path, { redirect: "manual" });
119
+ const res = await fetch(base + c.path, { redirect: "manual", headers: c.requestHeaders });
120
120
  const reasons = [];
121
121
  if (c.status !== void 0 && res.status !== c.status) {
122
122
  reasons.push(`status ${res.status} != ${c.status}`);
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  defineConfig,
3
3
  defineVerify,
4
4
  loadConfig
5
- } from "./chunk-VONSDNH4.js";
5
+ } from "./chunk-JRCATCRY.js";
6
6
  import "./chunk-ADS6BJJ5.js";
7
7
  import "./chunk-WFZTRXBF.js";
8
8
  import "./chunk-KP3Y6WRU.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtrentjones/greenlight",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Greenlight CLI — setup and lifecycle for the harness.",
5
5
  "license": "MIT",
6
6
  "repository": {