@rtrentjones/greenlight 0.2.10 → 0.2.12

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.
Files changed (2) hide show
  1. package/dist/bin.js +148 -27
  2. package/package.json +4 -4
package/dist/bin.js CHANGED
@@ -414,7 +414,7 @@ function tokensForTool(tool) {
414
414
  }
415
415
 
416
416
  // src/version.ts
417
- var MODULE_REF = "v0.2.10";
417
+ var MODULE_REF = "v0.2.12";
418
418
  var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
419
419
  function moduleSource(module, ref = MODULE_REF) {
420
420
  return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
@@ -1099,10 +1099,17 @@ function vendorDeps(vendorDir) {
1099
1099
  }
1100
1100
  return out;
1101
1101
  }
1102
- function starterVerifyConfig(lane) {
1103
- const spec = lane === "mcp" ? "{ mode: 'mcp', expectTools: [] }" : "{ mode: 'api', checks: [{ path: '/', status: 200 }] }";
1102
+ function starterVerifyConfig(lane, target) {
1103
+ const spec = lane === "mcp" ? "mode: 'mcp', expectTools: []" : "mode: 'api', checks: [{ path: '/', status: 200 }]";
1104
+ const logHint = target === "vercel" ? 'vercel logs "$GREENLIGHT_VERIFY_URL" --token "$VERCEL_API_TOKEN" 2>&1 | head -40 || true' : 'curl -sS -i "$GREENLIGHT_VERIFY_URL" 2>&1 | head -30 || true';
1104
1105
  return `// Greenlight verify spec \u2014 edit to assert this tool's real contract.
1105
- export default ${spec};
1106
+ export default {
1107
+ ${spec},
1108
+ // Telemetry-into-verify: a shell command run ONLY when this report FAILS; its last ~50 lines
1109
+ // attach to the report so the agent/CI sees the "why" in-loop. $GREENLIGHT_VERIFY_URL is the
1110
+ // failing URL (no hard-coding). Best-effort (never fails the gate). Uncomment + adjust:
1111
+ // logsOnFailure: '${logHint}',
1112
+ };
1106
1113
  `;
1107
1114
  }
1108
1115
  function infraTf(name, domain, lane, target, data, envs, slug) {
@@ -1294,26 +1301,8 @@ jobs:
1294
1301
  -F client_payload[sha]=\${{ github.sha }}
1295
1302
  `;
1296
1303
  }
1297
- function deployListenerYml(name, toolRepo) {
1298
- return `name: greenlight-deploy-${name}
1299
-
1300
- # Option B: ${toolRepo} fires repository_dispatch(deploy-${name}) after pushing a new image.
1301
- on:
1302
- repository_dispatch:
1303
- types: [deploy-${name}]
1304
- workflow_dispatch:
1305
-
1306
- permissions:
1307
- contents: read
1308
-
1309
- jobs:
1310
- deploy:
1311
- runs-on: ubuntu-latest
1312
- steps:
1313
- - uses: actions/checkout@v4
1314
- - uses: jdx/mise-action@v2
1315
- - run: pnpm install --frozen-lockfile
1316
- - run: pip install --quiet oci-cli
1304
+ function ociDeployAndVerifySteps(name) {
1305
+ return ` - run: pip install --quiet oci-cli
1317
1306
  - name: Deploy (resolve instance OCID by name -> restart -> re-pull GHCR image)
1318
1307
  env:
1319
1308
  # The OCI CLI reuses the SAME TF_VAR_OCI_* secrets the apply uses \u2014 one secret set.
@@ -1346,7 +1335,33 @@ jobs:
1346
1335
  # The deploy "succeeds" only if the NEW image is actually serving. verify has a built-in
1347
1336
  # readiness wait (re-pull + container start). A failure here fails the job \u2192 the status
1348
1337
  # posted back is red. oci is verify-gated direct-to-prod (no cheap standing beta on free A1).
1349
- run: pnpm exec greenlight verify ${name} --env prod
1338
+ run: pnpm exec greenlight verify ${name} --env prod`;
1339
+ }
1340
+ function deployListenerYml(name, toolRepo) {
1341
+ return `name: greenlight-deploy-${name}
1342
+
1343
+ # Option B: ${toolRepo} fires repository_dispatch(deploy-${name}) after pushing a new image.
1344
+ on:
1345
+ repository_dispatch:
1346
+ types: [deploy-${name}]
1347
+ workflow_dispatch:
1348
+
1349
+ permissions:
1350
+ contents: read
1351
+
1352
+ # Share the self-heal workflow's group so a deploy and a remediation never run at the same time.
1353
+ concurrency:
1354
+ group: deploy-${name}
1355
+ cancel-in-progress: false
1356
+
1357
+ jobs:
1358
+ deploy:
1359
+ runs-on: ubuntu-latest
1360
+ steps:
1361
+ - uses: actions/checkout@v4
1362
+ - uses: jdx/mise-action@v2
1363
+ - run: pnpm install --frozen-lockfile
1364
+ ${ociDeployAndVerifySteps(name)}
1350
1365
  - name: Report status back to ${toolRepo}
1351
1366
  if: \${{ always() && github.event.client_payload.sha != '' }}
1352
1367
  env:
@@ -1360,6 +1375,65 @@ jobs:
1360
1375
  -f description="\${{ job.status }}"
1361
1376
  `;
1362
1377
  }
1378
+ function remediateYml(name) {
1379
+ return `name: greenlight-remediate-${name}
1380
+
1381
+ # Auto-heal: the keepalive Worker dispatches remediate-${name} when ${name} (oci) is unreachable.
1382
+ on:
1383
+ repository_dispatch:
1384
+ types: [remediate-${name}]
1385
+ workflow_dispatch:
1386
+
1387
+ permissions:
1388
+ contents: read
1389
+ issues: write
1390
+
1391
+ # Same group as greenlight-deploy-${name}: a self-heal never overlaps a deploy or another heal, and
1392
+ # re-applying an already-healthy instance is idempotent (no diff) \u2014 anti-flap with no extra state.
1393
+ concurrency:
1394
+ group: deploy-${name}
1395
+ cancel-in-progress: false
1396
+
1397
+ jobs:
1398
+ remediate:
1399
+ runs-on: ubuntu-latest
1400
+ steps:
1401
+ - uses: actions/checkout@v4
1402
+ - uses: jdx/mise-action@v2
1403
+ - run: pnpm install --frozen-lockfile
1404
+ - uses: hashicorp/setup-terraform@v3
1405
+ with:
1406
+ terraform_version: '~1.10'
1407
+ terraform_wrapper: false
1408
+ - name: Re-apply the instance (recreate it if OCI idle-reclaimed the Always-Free box)
1409
+ env:
1410
+ TF_TOKEN_app_terraform_io: \${{ secrets.TF_API_TOKEN }} # HCP state backend auth
1411
+ TF_VAR_oci_tenancy_ocid: \${{ secrets.TF_VAR_OCI_TENANCY_OCID }}
1412
+ TF_VAR_oci_user_ocid: \${{ secrets.TF_VAR_OCI_USER_OCID }}
1413
+ TF_VAR_oci_fingerprint: \${{ secrets.TF_VAR_OCI_FINGERPRINT }}
1414
+ TF_VAR_oci_private_key: \${{ secrets.TF_VAR_OCI_PRIVATE_KEY }}
1415
+ TF_VAR_oci_region: \${{ secrets.TF_VAR_OCI_REGION }}
1416
+ TF_VAR_oci_compartment_id: \${{ secrets.TF_VAR_OCI_COMPARTMENT_ID }}
1417
+ run: |
1418
+ if [ -z "$TF_TOKEN_app_terraform_io" ]; then
1419
+ echo "::warning::no TF_API_TOKEN \u2014 skipping re-apply; will still attempt a restart below"
1420
+ exit 0
1421
+ fi
1422
+ terraform -chdir=infra init -input=false
1423
+ # -target pulls in the instance's deps (the ${name}_network module) automatically.
1424
+ terraform -chdir=infra apply -input=false -auto-approve -target=module.${name}_instance
1425
+ ${ociDeployAndVerifySteps(name)}
1426
+ - name: Escalate if the self-heal failed
1427
+ if: \${{ failure() }}
1428
+ env:
1429
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
1430
+ run: |
1431
+ gh issue create --repo \${{ github.repository }} \\
1432
+ --title "remediate-${name}: self-heal FAILED" \\
1433
+ --body "Automatic remediation for ${name} (reason: \${{ github.event.client_payload.reason }}) did not bring prod back. Manual attention needed." \\
1434
+ --label keepalive || true
1435
+ `;
1436
+ }
1363
1437
  function verifyWorkflowYml(name) {
1364
1438
  return `name: greenlight-verify
1365
1439
 
@@ -1405,12 +1479,18 @@ function nextVerifyConfig(name) {
1405
1479
  // Unit tests belong in this repo's PR CI; to also gate the deploy on them, add
1406
1480
  // { mode: 'test', command: 'pnpm test' } + a tolerant deps-install step in greenlight-verify.yml.
1407
1481
  const bypass = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
1482
+ // Telemetry-into-verify: on a FAILED report, fetch the Vercel deployment's runtime logs and attach
1483
+ // the tail to the report (best-effort, never fails the gate). $GREENLIGHT_VERIFY_URL is the exact
1484
+ // failing deployment URL (injected \u2014 no hard-coding); needs VERCEL_API_TOKEN in CI.
1485
+ const logsOnFailure =
1486
+ 'vercel logs "$GREENLIGHT_VERIFY_URL" --token "$VERCEL_API_TOKEN" 2>&1 | head -40 || true';
1408
1487
  const api = bypass
1409
1488
  ? {
1410
1489
  mode: 'api',
1411
1490
  checks: [{ path: '/', status: 200, requestHeaders: { 'x-vercel-protection-bypass': bypass } }],
1491
+ logsOnFailure,
1412
1492
  }
1413
- : { mode: 'api', checks: [{ path: '/', status: 401 }] };
1493
+ : { mode: 'api', checks: [{ path: '/', status: 401 }], logsOnFailure };
1414
1494
  const agentWeb = process.env.ANTHROPIC_API_KEY
1415
1495
  ? [
1416
1496
  {
@@ -1512,7 +1592,7 @@ async function adoptWrapper(ctx) {
1512
1592
  if (target !== "vercel") {
1513
1593
  writeIfAbsent(
1514
1594
  join2(cwd, `verify/${name}.config.ts`),
1515
- starterVerifyConfig(lane),
1595
+ starterVerifyConfig(lane, target),
1516
1596
  `verify/${name}.config.ts`
1517
1597
  );
1518
1598
  }
@@ -1529,6 +1609,11 @@ async function adoptWrapper(ctx) {
1529
1609
  deployListenerYml(name, slug),
1530
1610
  `.github/workflows/greenlight-deploy-${name}.yml (wrapper deploy listener)`
1531
1611
  );
1612
+ writeIfAbsent(
1613
+ join2(cwd, `.github/workflows/greenlight-remediate-${name}.yml`),
1614
+ remediateYml(name),
1615
+ `.github/workflows/greenlight-remediate-${name}.yml (wrapper self-heal listener)`
1616
+ );
1532
1617
  writeIfAbsent(
1533
1618
  join2(dest, ".github/workflows/greenlight-build.yml"),
1534
1619
  containerBuildYml(name, wrapperSlug),
@@ -2120,6 +2205,7 @@ import { resolve as resolve10 } from "path";
2120
2205
  import { setTimeout as sleep } from "timers/promises";
2121
2206
 
2122
2207
  // src/commands/verify.ts
2208
+ import { spawnSync } from "child_process";
2123
2209
  import { resolve as resolve9 } from "path";
2124
2210
  function defaultSpec(lane) {
2125
2211
  switch (lane) {
@@ -2139,6 +2225,39 @@ function printReport(report) {
2139
2225
  }
2140
2226
  console.log(`
2141
2227
  ${report.pass ? "\u2714 PASS" : "\u2718 FAIL"}`);
2228
+ if (!report.pass && report.logs) {
2229
+ console.log(`
2230
+ --- recent logs (${report.mode}) ---
2231
+ ${report.logs}
2232
+ --- end logs ---`);
2233
+ }
2234
+ }
2235
+ var LOG_TAIL_LINES = 50;
2236
+ function attachFailureLogs(reports, specs, toolDir) {
2237
+ reports.forEach((report, i) => {
2238
+ if (report.pass) return;
2239
+ const cmd = specs[i]?.logsOnFailure;
2240
+ if (!cmd) {
2241
+ report.logs = "(no logsOnFailure configured for this spec)";
2242
+ return;
2243
+ }
2244
+ try {
2245
+ const res = spawnSync(cmd, {
2246
+ shell: true,
2247
+ cwd: toolDir,
2248
+ timeout: 3e4,
2249
+ encoding: "utf8",
2250
+ maxBuffer: 10 * 1024 * 1024,
2251
+ // Let the command target the exact failing URL without hard-coding it.
2252
+ env: { ...process.env, GREENLIGHT_VERIFY_URL: report.url }
2253
+ });
2254
+ const out = `${res.stdout ?? ""}${res.stderr ?? ""}`.trimEnd();
2255
+ const tail = out.split("\n").slice(-LOG_TAIL_LINES).join("\n");
2256
+ report.logs = tail || `(logsOnFailure produced no output${res.error ? `: ${res.error.message}` : ""})`;
2257
+ } catch (e) {
2258
+ report.logs = `(log fetch failed: ${e instanceof Error ? e.message : String(e)})`;
2259
+ }
2260
+ });
2142
2261
  }
2143
2262
  function flag6(args, name) {
2144
2263
  const i = args.indexOf(name);
@@ -2157,6 +2276,7 @@ async function verifyCommand(args) {
2157
2276
  reachableTimeoutMs: waitMs,
2158
2277
  toolDir: process.cwd()
2159
2278
  });
2279
+ attachFailureLogs(reports2, specs2, process.cwd());
2160
2280
  for (const report of reports2) printReport(report);
2161
2281
  const pass2 = allPass(reports2);
2162
2282
  if (reports2.length > 1)
@@ -2194,6 +2314,7 @@ ${pass2 ? "\u2714 ALL PASS" : "\u2718 FAIL"} (${reports2.length} specs)`);
2194
2314
  }
2195
2315
  const toolDir = resolve9(process.cwd(), entry.dir ?? ".");
2196
2316
  const reports = await verifyAll(url, specs, { reachableTimeoutMs, toolDir });
2317
+ attachFailureLogs(reports, specs, toolDir);
2197
2318
  for (const report of reports) printReport(report);
2198
2319
  const pass = allPass(reports);
2199
2320
  if (reports.length > 1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtrentjones/greenlight",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "Greenlight CLI — setup and lifecycle for the harness.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,10 +31,10 @@
31
31
  "@anthropic-ai/sdk": "^0.69.0"
32
32
  },
33
33
  "devDependencies": {
34
- "@rtrentjones/greenlight-loop": "0.2.4",
34
+ "@rtrentjones/greenlight-adapters": "0.2.4",
35
35
  "@rtrentjones/greenlight-shared": "0.2.4",
36
- "@rtrentjones/greenlight-verify": "0.2.4",
37
- "@rtrentjones/greenlight-adapters": "0.2.4"
36
+ "@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",