@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-
|
|
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.
|
|
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 (
|
|
1455
|
-
throw new Error(
|
|
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
|
|
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(
|
|
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
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
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(
|
|
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