@rtrentjones/greenlight 0.2.8 → 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.
|
@@ -42,6 +42,13 @@ wrapper deploy listener. `greenlight adopt … --target vercel` emits, into the
|
|
|
42
42
|
`greenlight verify --url <url> --spec <path>` is the **manifest-free** mode that makes this work
|
|
43
43
|
without carrying the wrapper's `greenlight.config.ts` into the tool repo.
|
|
44
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
|
+
|
|
45
52
|
## MCP
|
|
46
53
|
|
|
47
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}`;
|
|
@@ -1443,14 +1467,14 @@ jobs:
|
|
|
1443
1467
|
- uses: actions/setup-node@v4
|
|
1444
1468
|
with:
|
|
1445
1469
|
node-version: '24'
|
|
1446
|
-
-
|
|
1447
|
-
|
|
1448
|
-
|
|
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
|
|
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.
|
|
1452
1473
|
- name: Verify the deployment
|
|
1453
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 }}
|
|
1454
1478
|
ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
|
|
1455
1479
|
run: npx -y @rtrentjones/greenlight@latest verify --url "\${{ github.event.deployment_status.target_url }}" --spec verify/${name}.config.ts
|
|
1456
1480
|
`;
|
|
@@ -1458,10 +1482,20 @@ jobs:
|
|
|
1458
1482
|
function nextVerifyConfig(name) {
|
|
1459
1483
|
return `// Greenlight verify spec for ${name} (next/vercel) \u2014 run by .github/workflows/greenlight-verify.yml
|
|
1460
1484
|
// after Vercel deploys (deployment_status). An array combines modes (allPass):
|
|
1461
|
-
// - api: the
|
|
1462
|
-
//
|
|
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).
|
|
1463
1488
|
// - agent-web: an LLM drives the live UI; runs ONLY when ANTHROPIC_API_KEY is set (else omitted,
|
|
1464
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 }] };
|
|
1465
1499
|
const agentWeb = process.env.ANTHROPIC_API_KEY
|
|
1466
1500
|
? [
|
|
1467
1501
|
{
|
|
@@ -1477,11 +1511,7 @@ const agentWeb = process.env.ANTHROPIC_API_KEY
|
|
|
1477
1511
|
]
|
|
1478
1512
|
: [];
|
|
1479
1513
|
|
|
1480
|
-
export default [
|
|
1481
|
-
{ mode: 'api', checks: [{ path: '/', status: 200 }] },
|
|
1482
|
-
{ mode: 'test', command: 'npm test' },
|
|
1483
|
-
...agentWeb,
|
|
1484
|
-
];
|
|
1514
|
+
export default [api, ...agentWeb];
|
|
1485
1515
|
`;
|
|
1486
1516
|
}
|
|
1487
1517
|
function writeIfAbsent(path, contents, label) {
|
|
@@ -1514,8 +1544,8 @@ async function adoptCommand(args) {
|
|
|
1514
1544
|
"run adopt from your site repo (needs a real greenlight.config.ts; run `greenlight init` first)"
|
|
1515
1545
|
);
|
|
1516
1546
|
}
|
|
1517
|
-
if (
|
|
1518
|
-
throw new Error(
|
|
1547
|
+
if (name === "blog") {
|
|
1548
|
+
throw new Error('"blog" is the apex site, not an adopted tool');
|
|
1519
1549
|
}
|
|
1520
1550
|
const domain = flag3(args, "--domain") ?? reg.domain;
|
|
1521
1551
|
const ctx = { name, repoArg, lane, target, data, auth, envs, domain, reg, regPath };
|
|
@@ -1542,7 +1572,8 @@ async function adoptWrapper(ctx) {
|
|
|
1542
1572
|
} else {
|
|
1543
1573
|
console.log(`\xB7 ${toolRel} exists \u2014 skipping submodule add`);
|
|
1544
1574
|
}
|
|
1545
|
-
const
|
|
1575
|
+
const existed = reg.tools.some((x) => x.name === name);
|
|
1576
|
+
const nextReg = upsertTool(reg, {
|
|
1546
1577
|
name,
|
|
1547
1578
|
lane,
|
|
1548
1579
|
target,
|
|
@@ -1554,7 +1585,9 @@ async function adoptWrapper(ctx) {
|
|
|
1554
1585
|
adopted: true
|
|
1555
1586
|
});
|
|
1556
1587
|
writeFileSync4(regPath, serializeConfig(nextReg));
|
|
1557
|
-
console.log(
|
|
1588
|
+
console.log(
|
|
1589
|
+
`\u2714 ${existed ? "updated" : "registered"} "${name}" (external, dir ${toolRel}) in the wrapper manifest`
|
|
1590
|
+
);
|
|
1558
1591
|
const slug = parseRepo(repoArg) ?? parseRepo(safeGit(dest, ["remote", "get-url", "origin"])) ?? `OWNER/${name}`;
|
|
1559
1592
|
writeIfAbsent(
|
|
1560
1593
|
join2(cwd, `infra/${name}.tf`),
|
|
@@ -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rtrentjones/greenlight",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
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",
|
|
35
36
|
"@rtrentjones/greenlight-shared": "0.2.4",
|
|
36
|
-
"@rtrentjones/greenlight-verify": "0.2.4"
|
|
37
|
-
"@rtrentjones/greenlight-loop": "0.2.4"
|
|
37
|
+
"@rtrentjones/greenlight-verify": "0.2.4"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "node scripts/copy-assets.mjs && tsup",
|