@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.
- package/assets/skills/provider-github/SKILL.md +3 -2
- package/assets/skills/provider-vercel/SKILL.md +16 -0
- package/dist/bin.js +128 -17
- package/dist/{chunk-XBDQJVAX.js → chunk-ADS6BJJ5.js} +4 -1
- package/dist/{chunk-LM6M3DIV.js → chunk-VONSDNH4.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/{mcp-KU7WKB5K.js → mcp-3L6HJ6BH.js} +1 -1
- package/package.json +3 -3
|
@@ -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
|
-
- **`
|
|
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-
|
|
9
|
-
import "./chunk-
|
|
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,
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
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(
|
|
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(
|
|
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-
|
|
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-
|
|
6
|
-
import "./chunk-
|
|
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";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rtrentjones/greenlight",
|
|
3
|
-
"version": "0.2.
|
|
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",
|