@rtrentjones/greenlight 0.2.6 → 0.2.8

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.
@@ -25,8 +25,9 @@ pushes each to the right repo; see docs/provider-tokens.md):
25
25
 
26
26
  - **`GREENLIGHT_DISPATCH_TOKEN`** — on the **tool** repo, scoped **Contents: write** on the
27
27
  **wrapper** → the tool's build fires `repository_dispatch` so the wrapper deploys.
28
- - **`GREENLIGHT_STATUS_TOKEN`** — on the **wrapper** repo, scoped **Commit statuses: write** on the
29
- **tool** → the wrapper posts deploy/verify status back to the tool's commit.
28
+ - **`GREENLIGHT_STATUS_TOKEN_<TOOL>`** — on the **wrapper** repo, scoped **Commit statuses: write**
29
+ on the **tool** → the wrapper posts deploy/verify status back to the tool's commit. **Per-tool
30
+ suffix** (e.g. `…_BAMCP`) because it lives on the shared wrapper alongside other tools' tokens.
30
31
 
31
32
  Provider creds (OCI/Cloudflare/…) live **only in the wrapper**; the tool repo holds just the
32
33
  dispatch PAT (its build pushes to GHCR with the built-in `github.token`).
@@ -26,6 +26,22 @@ 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
+
29
45
  ## MCP
30
46
 
31
47
  `.mcp.json` wires `vercel` (hosted, OAuth, read-only). Run `/mcp` and authenticate in the
package/dist/bin.js CHANGED
@@ -5,8 +5,8 @@ import {
5
5
  loadConfig,
6
6
  resolveUrl,
7
7
  verifyAll
8
- } from "./chunk-LM6M3DIV.js";
9
- import "./chunk-XBDQJVAX.js";
8
+ } from "./chunk-VONSDNH4.js";
9
+ import "./chunk-ADS6BJJ5.js";
10
10
  import "./chunk-WFZTRXBF.js";
11
11
  import "./chunk-KP3Y6WRU.js";
12
12
  import "./chunk-UXHHLEYO.js";
@@ -351,9 +351,12 @@ var PACKS = [
351
351
  setupUrl: "https://github.com/settings/personal-access-tokens/new"
352
352
  },
353
353
  {
354
+ // Stored on the shared wrapper, scoped to THIS tool's repo → per-tool name
355
+ // (GREENLIGHT_STATUS_TOKEN_<TOOL>) so multiple tools' status tokens don't collide.
354
356
  envVar: "GREENLIGHT_STATUS_TOKEN",
355
- label: "GitHub PAT, Commits:write on the TOOL (WRAPPER posts deploy status back)",
357
+ label: "GitHub PAT, Commit statuses:write on the TOOL (WRAPPER posts deploy status back)",
356
358
  optional: true,
359
+ perTool: true,
357
360
  setupUrl: "https://github.com/settings/personal-access-tokens/new"
358
361
  }
359
362
  ],
@@ -387,7 +390,7 @@ function tokensForTool(tool) {
387
390
  }
388
391
 
389
392
  // src/version.ts
390
- var MODULE_REF = "v0.2.6";
393
+ var MODULE_REF = "v0.2.8";
391
394
  var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
392
395
  function moduleSource(module, ref = MODULE_REF) {
393
396
  return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
@@ -797,7 +800,8 @@ async function gatherSecrets(name, repo, env, prefill) {
797
800
  for (const pack of packs) {
798
801
  console.log(`\u2500\u2500 ${pack.name}${pack.setupUrl ? ` \u2192 ${pack.setupUrl}` : ""}`);
799
802
  for (const tok of pack.tokens) {
800
- const key = tok.envVar.toUpperCase();
803
+ const suffix = `_${name.toUpperCase().replace(/-/g, "_")}`;
804
+ const key = tok.envVar.toUpperCase() + (tok.perTool ? suffix : "");
801
805
  if (key === "GITHUB_TOKEN") {
802
806
  console.log(" \xB7 GITHUB_TOKEN \u2014 provided automatically by Actions; skipping");
803
807
  continue;
@@ -1317,13 +1321,13 @@ concurrency:
1317
1321
 
1318
1322
  jobs:
1319
1323
  build:
1320
- runs-on: ubuntu-latest
1324
+ # Native arm64 runner \u2014 builds the arm64 image directly (no QEMU emulation, much faster).
1325
+ runs-on: ubuntu-24.04-arm
1321
1326
  steps:
1322
1327
  - uses: actions/checkout@v4
1323
1328
  - name: Resolve image ref (GHCR namespaces are lowercase)
1324
1329
  id: img
1325
- run: echo "ref=ghcr.io/\${GITHUB_REPOSITORY_OWNER,,}/${name}:prod" >> "$GITHUB_OUTPUT"
1326
- - uses: docker/setup-qemu-action@v3
1330
+ run: echo "base=ghcr.io/\${GITHUB_REPOSITORY_OWNER,,}/${name}" >> "$GITHUB_OUTPUT"
1327
1331
  - uses: docker/setup-buildx-action@v3
1328
1332
  - uses: docker/login-action@v3
1329
1333
  with:
@@ -1335,7 +1339,12 @@ jobs:
1335
1339
  context: .
1336
1340
  platforms: linux/arm64
1337
1341
  push: true
1338
- tags: \${{ steps.img.outputs.ref }}
1342
+ # :prod is the moving deploy tag; :<sha> is immutable (rollback + deploy-identity).
1343
+ tags: |
1344
+ \${{ steps.img.outputs.base }}:prod
1345
+ \${{ steps.img.outputs.base }}:\${{ github.sha }}
1346
+ cache-from: type=gha
1347
+ cache-to: type=gha,mode=max
1339
1348
  - name: Notify wrapper to deploy
1340
1349
  env:
1341
1350
  GH_TOKEN: \${{ secrets.GREENLIGHT_DISPATCH_TOKEN }}
@@ -1402,7 +1411,8 @@ jobs:
1402
1411
  - name: Report status back to ${toolRepo}
1403
1412
  if: \${{ always() && github.event.client_payload.sha != '' }}
1404
1413
  env:
1405
- GH_TOKEN: \${{ secrets.GREENLIGHT_STATUS_TOKEN }}
1414
+ # Per-tool name: the status PAT lives on the shared wrapper, scoped to this tool's repo.
1415
+ GH_TOKEN: \${{ secrets.GREENLIGHT_STATUS_TOKEN_${name.toUpperCase().replace(/-/g, "_")} }}
1406
1416
  run: |
1407
1417
  [ -z "$GH_TOKEN" ] && exit 0
1408
1418
  gh api repos/${toolRepo}/statuses/\${{ github.event.client_payload.sha }} \\
@@ -1411,6 +1421,69 @@ jobs:
1411
1421
  -f description="\${{ job.status }}"
1412
1422
  `;
1413
1423
  }
1424
+ function verifyWorkflowYml(name) {
1425
+ return `name: greenlight-verify
1426
+
1427
+ # Vercel deploys ${name} on push and posts a deployment_status to GitHub; we verify the exact
1428
+ # deployed URL. ANTHROPIC_API_KEY (optional) enables the agent-web scenarios \u2014 absent \u2192 omitted
1429
+ # (see verify/${name}.config.ts), so the gate stays green on api + test alone.
1430
+ on:
1431
+ deployment_status:
1432
+
1433
+ permissions:
1434
+ contents: read
1435
+ statuses: write
1436
+
1437
+ jobs:
1438
+ verify:
1439
+ if: \${{ github.event.deployment_status.state == 'success' }}
1440
+ runs-on: ubuntu-latest
1441
+ steps:
1442
+ - uses: actions/checkout@v4
1443
+ - uses: actions/setup-node@v4
1444
+ with:
1445
+ node-version: '24'
1446
+ - name: Install deps (for test-mode)
1447
+ run: |
1448
+ corepack enable || true
1449
+ if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile;
1450
+ elif [ -f yarn.lock ]; then yarn install --frozen-lockfile;
1451
+ else npm ci; fi
1452
+ - name: Verify the deployment
1453
+ env:
1454
+ ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
1455
+ run: npx -y @rtrentjones/greenlight@latest verify --url "\${{ github.event.deployment_status.target_url }}" --spec verify/${name}.config.ts
1456
+ `;
1457
+ }
1458
+ function nextVerifyConfig(name) {
1459
+ return `// Greenlight verify spec for ${name} (next/vercel) \u2014 run by .github/workflows/greenlight-verify.yml
1460
+ // after Vercel deploys (deployment_status). An array combines modes (allPass):
1461
+ // - api: the deployed URL serves (200).
1462
+ // - test: this tool's own suite \u2014 set the real command for your package manager.
1463
+ // - agent-web: an LLM drives the live UI; runs ONLY when ANTHROPIC_API_KEY is set (else omitted,
1464
+ // so the gate stays green). Replace the scenario with real user tasks + assertions.
1465
+ const agentWeb = process.env.ANTHROPIC_API_KEY
1466
+ ? [
1467
+ {
1468
+ mode: 'agent-web',
1469
+ scenarios: [
1470
+ {
1471
+ name: 'home renders',
1472
+ task: 'Open the home page and confirm the app loads without an error screen.',
1473
+ asserts: [{ selector: 'body' }],
1474
+ },
1475
+ ],
1476
+ },
1477
+ ]
1478
+ : [];
1479
+
1480
+ export default [
1481
+ { mode: 'api', checks: [{ path: '/', status: 200 }] },
1482
+ { mode: 'test', command: 'npm test' },
1483
+ ...agentWeb,
1484
+ ];
1485
+ `;
1486
+ }
1414
1487
  function writeIfAbsent(path, contents, label) {
1415
1488
  if (existsSync7(path)) {
1416
1489
  console.log(`\xB7 ${label} exists \u2014 left as-is`);
@@ -1488,11 +1561,13 @@ async function adoptWrapper(ctx) {
1488
1561
  emitToolTf({ name, domain, lane, target, data, envs, slug, external: true }),
1489
1562
  `infra/${name}.tf`
1490
1563
  );
1491
- writeIfAbsent(
1492
- join2(cwd, `verify/${name}.config.ts`),
1493
- starterVerifyConfig(lane),
1494
- `verify/${name}.config.ts`
1495
- );
1564
+ if (target !== "vercel") {
1565
+ writeIfAbsent(
1566
+ join2(cwd, `verify/${name}.config.ts`),
1567
+ starterVerifyConfig(lane),
1568
+ `verify/${name}.config.ts`
1569
+ );
1570
+ }
1496
1571
  const providers = providersForTool({ target, data });
1497
1572
  if (existsSync7(join2(cwd, "infra/main.tf")) && providers.some((p) => p !== "cloudflare" && p !== "github")) {
1498
1573
  console.log(`\xB7 ensure infra/main.tf declares provider(s): ${providers.join(", ")}`);
@@ -1512,6 +1587,18 @@ async function adoptWrapper(ctx) {
1512
1587
  `${toolRel}/.github/workflows/greenlight-build.yml (provider-agnostic build \u2192 GHCR \u2192 dispatch)`
1513
1588
  );
1514
1589
  }
1590
+ if (target === "vercel") {
1591
+ writeIfAbsent(
1592
+ join2(dest, `verify/${name}.config.ts`),
1593
+ nextVerifyConfig(name),
1594
+ `${toolRel}/verify/${name}.config.ts (tool-CI verify spec)`
1595
+ );
1596
+ writeIfAbsent(
1597
+ join2(dest, ".github/workflows/greenlight-verify.yml"),
1598
+ verifyWorkflowYml(name),
1599
+ `${toolRel}/.github/workflows/greenlight-verify.yml (verify on Vercel deployment_status)`
1600
+ );
1601
+ }
1515
1602
  console.log(`
1516
1603
  Next:
1517
1604
  (in the tool repo) commit the Greenlight kit + build workflow so they travel with the submodule:
@@ -1521,7 +1608,10 @@ Next:
1521
1608
  git commit && git push # CI (infra.yml) applies. Tool's CI builds; wrapper deploys.${target === "oci" ? `
1522
1609
  Secrets (guided): greenlight secrets gather ${name} --repo <wrapper> # TF_VAR_OCI_* + GREENLIGHT_STATUS_TOKEN
1523
1610
  greenlight secrets gather ${name} --repo ${slug} # GREENLIGHT_DISPATCH_TOKEN
1524
- The instance OCID is auto-resolved by the deploy workflow (by display name) \u2014 nothing to set.` : ""}`);
1611
+ The instance OCID is auto-resolved by the deploy workflow (by display name) \u2014 nothing to set.` : target === "vercel" ? `
1612
+ Deploy is Vercel's git integration (no wrapper deploy). The tool's greenlight-verify.yml verifies
1613
+ each deployment (deployment_status). Optional: add ANTHROPIC_API_KEY to ${slug} to enable the
1614
+ agent-web scenarios in verify/${name}.config.ts (absent \u2192 api + test gate alone).` : ""}`);
1525
1615
  }
1526
1616
  async function adoptStandalone(ctx) {
1527
1617
  const { name, repoArg, lane, target, data, auth, envs, domain, reg, regPath } = ctx;
@@ -1926,9 +2016,30 @@ function flag6(args, name) {
1926
2016
  return i >= 0 ? args[i + 1] : void 0;
1927
2017
  }
1928
2018
  async function verifyCommand(args) {
2019
+ const specPath = flag6(args, "--spec");
2020
+ if (specPath) {
2021
+ const url2 = flag6(args, "--url");
2022
+ if (!url2) throw new Error("verify --spec needs --url <deployed-url>");
2023
+ const loaded2 = await loadVerifySpecAt(specPath);
2024
+ if (!loaded2) throw new Error(`no verify spec at ${specPath}`);
2025
+ const specs2 = Array.isArray(loaded2) ? loaded2 : [loaded2];
2026
+ const waitMs = (flag6(args, "--wait") !== void 0 ? Number(flag6(args, "--wait")) : 0) * 1e3;
2027
+ const reports2 = await verifyAll(url2, specs2, {
2028
+ reachableTimeoutMs: waitMs,
2029
+ toolDir: process.cwd()
2030
+ });
2031
+ for (const report of reports2) printReport(report);
2032
+ const pass2 = allPass(reports2);
2033
+ if (reports2.length > 1)
2034
+ console.log(`
2035
+ ${pass2 ? "\u2714 ALL PASS" : "\u2718 FAIL"} (${reports2.length} specs)`);
2036
+ process.exit(pass2 ? 0 : 1);
2037
+ }
1929
2038
  const name = args[0];
1930
2039
  if (!name || name.startsWith("-")) {
1931
- throw new Error("usage: greenlight verify <name> [--env <beta|prod> | --url <url>]");
2040
+ throw new Error(
2041
+ "usage: greenlight verify <name> [--env <beta|prod> | --url <url>] | verify --url <url> --spec <path>"
2042
+ );
1932
2043
  }
1933
2044
  const { config } = await loadManifest();
1934
2045
  const entry = resolveEntry(config, name);
@@ -9,7 +9,10 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
9
9
  async function verifyMcp(baseUrl, spec) {
10
10
  const checks = [];
11
11
  const client = new Client({ name: "greenlight-verify", version: "0.0.0" });
12
- const transport = new StreamableHTTPClientTransport(new URL(baseUrl));
12
+ const transport = new StreamableHTTPClientTransport(
13
+ new URL(baseUrl),
14
+ spec.headers ? { requestInit: { headers: spec.headers } } : void 0
15
+ );
13
16
  try {
14
17
  await client.connect(transport);
15
18
  checks.push({ name: "initialize handshake", pass: true });
@@ -229,7 +229,7 @@ async function verify(baseUrl, spec, opts) {
229
229
  case "api":
230
230
  return verifyApi(baseUrl, spec);
231
231
  case "mcp": {
232
- const { verifyMcp: verifyMcp2 } = await import("./mcp-KU7WKB5K.js");
232
+ const { verifyMcp: verifyMcp2 } = await import("./mcp-3L6HJ6BH.js");
233
233
  return verifyMcp2(baseUrl, spec);
234
234
  }
235
235
  case "playwright": {
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@ import {
2
2
  defineConfig,
3
3
  defineVerify,
4
4
  loadConfig
5
- } from "./chunk-LM6M3DIV.js";
6
- import "./chunk-XBDQJVAX.js";
5
+ } from "./chunk-VONSDNH4.js";
6
+ import "./chunk-ADS6BJJ5.js";
7
7
  import "./chunk-WFZTRXBF.js";
8
8
  import "./chunk-KP3Y6WRU.js";
9
9
  import "./chunk-UXHHLEYO.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  verifyMcp
3
- } from "./chunk-XBDQJVAX.js";
3
+ } from "./chunk-ADS6BJJ5.js";
4
4
  import "./chunk-QFKE5JKC.js";
5
5
  export {
6
6
  verifyMcp
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtrentjones/greenlight",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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-loop": "0.2.4",
36
35
  "@rtrentjones/greenlight-shared": "0.2.4",
37
- "@rtrentjones/greenlight-verify": "0.2.4"
36
+ "@rtrentjones/greenlight-verify": "0.2.4",
37
+ "@rtrentjones/greenlight-loop": "0.2.4"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "node scripts/copy-assets.mjs && tsup",