@rtrentjones/greenlight 0.2.20 → 0.2.22
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/README.md +6 -3
- package/assets/skills/provider-hcp/SKILL.md +1 -1
- package/dist/bin.js +59 -13
- package/dist/{chunk-PSNO7F4Q.js → chunk-GO2RVNOP.js} +13 -3
- package/dist/index.js +1 -1
- package/package.json +5 -5
- package/templates/_template-mcp/README.md +1 -1
- package/templates/_template-next/README.md +1 -1
package/README.md
CHANGED
|
@@ -48,7 +48,8 @@ greenlight <command>
|
|
|
48
48
|
| `verify <name> --env <beta\|prod>` (or `--url`) | run the shared verify harness |
|
|
49
49
|
| `promote <name>` | gated `develop → main` fast-forward (after beta verify) |
|
|
50
50
|
| `deploy <name>` | target deploy hook (e.g. OCI restart = re-pull) |
|
|
51
|
-
| `
|
|
51
|
+
| `status <name>` | the ship → deploy → verify run chain across repos |
|
|
52
|
+
| `doctor` / `config` | health checks (incl. token-scoping conformance) / load + validate + print the manifest |
|
|
52
53
|
|
|
53
54
|
## The loop
|
|
54
55
|
|
|
@@ -70,7 +71,8 @@ import { defineConfig, defineVerify } from '@rtrentjones/greenlight';
|
|
|
70
71
|
|
|
71
72
|
export default defineConfig({
|
|
72
73
|
domain: 'you.dev',
|
|
73
|
-
|
|
74
|
+
alerts: { sink: 'github-issue' },
|
|
75
|
+
tools: [{ name: 'notes', lane: 'mcp', target: 'oci', data: 'none', auth: 'bearer', envs: ['prod'] }],
|
|
74
76
|
});
|
|
75
77
|
```
|
|
76
78
|
|
|
@@ -80,6 +82,7 @@ Also exported: `loadConfig`, and the `GreenlightConfig` / `VerifySpec` types.
|
|
|
80
82
|
|
|
81
83
|
- **Repo + full docs:** <https://github.com/RTrentJones/greenlight>
|
|
82
84
|
- **Architecture:** [docs/architecture.md](https://github.com/RTrentJones/greenlight/blob/main/docs/architecture.md)
|
|
83
|
-
- **Spec:** [greenlight-
|
|
85
|
+
- **Spec:** [greenlight-v2.md](https://github.com/RTrentJones/greenlight/blob/main/greenlight-v2.md)
|
|
86
|
+
- **Add a provider type:** [docs/adding-a-provider.md](https://github.com/RTrentJones/greenlight/blob/main/docs/adding-a-provider.md)
|
|
84
87
|
|
|
85
88
|
MIT
|
|
@@ -42,5 +42,5 @@ Migrate local → HCP with a plain `terraform init` (answer `yes` to copy state)
|
|
|
42
42
|
plan -out → apply. This is the deploy half — the CLI only edits the `.tf`; CI applies.
|
|
43
43
|
|
|
44
44
|
## Alternatives
|
|
45
|
-
See `docs/terraform-state
|
|
45
|
+
See `docs/terraform-state.md` for the full backend chooser (HCP no-CC · OCI S3-compat ·
|
|
46
46
|
R2 card-required · AWS · local).
|
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-GO2RVNOP.js";
|
|
9
9
|
import "./chunk-HX7VA25D.js";
|
|
10
10
|
import "./chunk-N3IKUCSF.js";
|
|
11
11
|
import "./chunk-KP3Y6WRU.js";
|
|
@@ -322,7 +322,7 @@ var PACKS = [
|
|
|
322
322
|
always: true,
|
|
323
323
|
// remote state backs every wrapper's infra
|
|
324
324
|
appliesTo: () => true,
|
|
325
|
-
guide: "docs/terraform-state
|
|
325
|
+
guide: "docs/terraform-state.md \u2014 HCP Terraform free tier (no credit card)",
|
|
326
326
|
setupUrl: "https://app.terraform.io/app/settings/tokens",
|
|
327
327
|
tokens: [
|
|
328
328
|
{
|
|
@@ -443,7 +443,7 @@ function tokensForTool(tool) {
|
|
|
443
443
|
}
|
|
444
444
|
|
|
445
445
|
// src/version.ts
|
|
446
|
-
var MODULE_REF = "v0.2.
|
|
446
|
+
var MODULE_REF = "v0.2.22";
|
|
447
447
|
var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
|
|
448
448
|
function moduleSource(module, ref = MODULE_REF) {
|
|
449
449
|
return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
|
|
@@ -677,7 +677,7 @@ locals {
|
|
|
677
677
|
` : "";
|
|
678
678
|
return `# Wrapper infra (singleton): providers + remote-state backend + shared variables.
|
|
679
679
|
# \`greenlight add\` appends per-tool module blocks as infra/<name>.tf. Apply is CI/CD's job
|
|
680
|
-
# (infra.yml). Fill in the HCP backend below before the first apply (docs/terraform-state
|
|
680
|
+
# (infra.yml). Fill in the HCP backend below before the first apply (docs/terraform-state.md).
|
|
681
681
|
|
|
682
682
|
terraform {
|
|
683
683
|
required_version = ">= 1.7"
|
|
@@ -2332,7 +2332,7 @@ async function initCommand(args) {
|
|
|
2332
2332
|
Next:
|
|
2333
2333
|
1. greenlight add <name> --lane <lane> --target <target> # scaffold a tool, emit infra, and
|
|
2334
2334
|
# gather THAT tool's keys \u2192 GitHub${pushed ? "" : "\n (run `greenlight secrets sync` if base tokens were not pushed)"}
|
|
2335
|
-
2. set the HCP backend (cloud{} org + workspace) in infra/main.tf # docs/terraform-state
|
|
2335
|
+
2. set the HCP backend (cloud{} org + workspace) in infra/main.tf # docs/terraform-state.md
|
|
2336
2336
|
3. commit + push \u2192 CI (.github/workflows/infra.yml) runs \`terraform apply\`
|
|
2337
2337
|
4. greenlight verify <name> --env prod | greenlight doctor`);
|
|
2338
2338
|
}
|
|
@@ -2348,7 +2348,13 @@ import { resolve as resolve9 } from "path";
|
|
|
2348
2348
|
function defaultSpec(lane) {
|
|
2349
2349
|
switch (lane) {
|
|
2350
2350
|
case "astro":
|
|
2351
|
-
return {
|
|
2351
|
+
return {
|
|
2352
|
+
mode: "api",
|
|
2353
|
+
checks: [{ path: "/", status: 200 }],
|
|
2354
|
+
noBrokenInternalLinks: true,
|
|
2355
|
+
settleRetries: 8,
|
|
2356
|
+
settleMs: 5e3
|
|
2357
|
+
};
|
|
2352
2358
|
case "next":
|
|
2353
2359
|
return { mode: "api", checks: [{ path: "/", status: 200 }] };
|
|
2354
2360
|
case "mcp":
|
|
@@ -2601,8 +2607,21 @@ function git(repoDir, args) {
|
|
|
2601
2607
|
function gitOut(repoDir, args) {
|
|
2602
2608
|
return execFileSync5("git", args, { cwd: repoDir, encoding: "utf8" }).trim();
|
|
2603
2609
|
}
|
|
2610
|
+
function tryRev(repoDir, ref) {
|
|
2611
|
+
try {
|
|
2612
|
+
return gitOut(repoDir, ["rev-parse", "--verify", "--quiet", ref]);
|
|
2613
|
+
} catch {
|
|
2614
|
+
return null;
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
function fetchRefs(repoDir, branches) {
|
|
2618
|
+
try {
|
|
2619
|
+
git(repoDir, ["fetch", "--no-tags", "origin", ...branches]);
|
|
2620
|
+
} catch {
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2604
2623
|
function resolveRef(repoDir, branch) {
|
|
2605
|
-
for (const ref of [
|
|
2624
|
+
for (const ref of [`origin/${branch}`, branch]) {
|
|
2606
2625
|
try {
|
|
2607
2626
|
git(repoDir, ["rev-parse", "--verify", "--quiet", ref]);
|
|
2608
2627
|
return ref;
|
|
@@ -2611,19 +2630,35 @@ function resolveRef(repoDir, branch) {
|
|
|
2611
2630
|
}
|
|
2612
2631
|
return null;
|
|
2613
2632
|
}
|
|
2633
|
+
function staleLocalWarnings(repoDir, branches) {
|
|
2634
|
+
const warnings = [];
|
|
2635
|
+
for (const branch of branches) {
|
|
2636
|
+
const local = tryRev(repoDir, branch);
|
|
2637
|
+
const origin = tryRev(repoDir, `origin/${branch}`);
|
|
2638
|
+
if (local && origin && local !== origin) {
|
|
2639
|
+
warnings.push(
|
|
2640
|
+
`local "${branch}" (${local.slice(0, 7)}) differs from origin/${branch} (${origin.slice(0, 7)}) \u2014 promoting the origin (verified) state. Sync with \`git fetch && git branch -f ${branch} origin/${branch}\`.`
|
|
2641
|
+
);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
return warnings;
|
|
2645
|
+
}
|
|
2614
2646
|
function canPromote(repoDir, from = "develop", to = "main") {
|
|
2647
|
+
fetchRefs(repoDir, [from, to]);
|
|
2615
2648
|
const fromRef = resolveRef(repoDir, from);
|
|
2616
2649
|
const toRef = resolveRef(repoDir, to);
|
|
2617
2650
|
if (!fromRef || !toRef) {
|
|
2618
2651
|
return { canPromote: false, reason: `branch "${from}" or "${to}" not found in ${repoDir}` };
|
|
2619
2652
|
}
|
|
2653
|
+
const warnings = staleLocalWarnings(repoDir, [from, to]);
|
|
2620
2654
|
try {
|
|
2621
2655
|
git(repoDir, ["merge-base", "--is-ancestor", toRef, fromRef]);
|
|
2622
|
-
return { canPromote: true, reason: `"${to}" can fast-forward to "${from}"
|
|
2656
|
+
return { canPromote: true, reason: `"${to}" can fast-forward to "${from}"`, warnings };
|
|
2623
2657
|
} catch {
|
|
2624
2658
|
return {
|
|
2625
2659
|
canPromote: false,
|
|
2626
|
-
reason: `"${to}" has diverged from "${from}" \u2014 fast-forward refused. Reconcile first (rebase "${from}" onto "${to}", or merge "${to}" into "${from}") before promoting
|
|
2660
|
+
reason: `"${to}" has diverged from "${from}" \u2014 fast-forward refused. Reconcile first (rebase "${from}" onto "${to}", or merge "${to}" into "${from}") before promoting.`,
|
|
2661
|
+
warnings
|
|
2627
2662
|
};
|
|
2628
2663
|
}
|
|
2629
2664
|
}
|
|
@@ -2631,7 +2666,10 @@ function promote(repoDir, opts = {}) {
|
|
|
2631
2666
|
const from = opts.from ?? "develop";
|
|
2632
2667
|
const to = opts.to ?? "main";
|
|
2633
2668
|
const check = canPromote(repoDir, from, to);
|
|
2634
|
-
if (!check.canPromote)
|
|
2669
|
+
if (!check.canPromote) {
|
|
2670
|
+
return { promoted: false, from, to, reason: check.reason, warnings: check.warnings };
|
|
2671
|
+
}
|
|
2672
|
+
const warnings = check.warnings;
|
|
2635
2673
|
const fromRef = resolveRef(repoDir, from);
|
|
2636
2674
|
const fromCommit = gitOut(repoDir, ["rev-parse", fromRef]);
|
|
2637
2675
|
const current = gitOut(repoDir, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
@@ -2643,14 +2681,20 @@ function promote(repoDir, opts = {}) {
|
|
|
2643
2681
|
} catch {
|
|
2644
2682
|
}
|
|
2645
2683
|
}
|
|
2646
|
-
return {
|
|
2684
|
+
return {
|
|
2685
|
+
promoted: true,
|
|
2686
|
+
from,
|
|
2687
|
+
to,
|
|
2688
|
+
reason: `"${to}" fast-forwarded to "${from}" and pushed`,
|
|
2689
|
+
warnings
|
|
2690
|
+
};
|
|
2647
2691
|
}
|
|
2648
2692
|
if (current === to) {
|
|
2649
2693
|
git(repoDir, ["merge", "--ff-only", fromRef]);
|
|
2650
2694
|
} else {
|
|
2651
2695
|
git(repoDir, ["update-ref", `refs/heads/${to}`, fromCommit]);
|
|
2652
2696
|
}
|
|
2653
|
-
return { promoted: true, from, to, reason: `"${to}" fast-forwarded to "${from}"
|
|
2697
|
+
return { promoted: true, from, to, reason: `"${to}" fast-forwarded to "${from}"`, warnings };
|
|
2654
2698
|
}
|
|
2655
2699
|
|
|
2656
2700
|
// src/commands/promote.ts
|
|
@@ -2660,11 +2704,13 @@ async function promoteCommand(args) {
|
|
|
2660
2704
|
const cwd = process.cwd();
|
|
2661
2705
|
if (!perform) {
|
|
2662
2706
|
const check = canPromote(cwd);
|
|
2707
|
+
for (const w of check.warnings ?? []) console.warn(`\u26A0 ${w}`);
|
|
2663
2708
|
console.log(`${check.canPromote ? "\u2714" : "\u2718"} ${check.reason}`);
|
|
2664
2709
|
if (check.canPromote) console.log("\nEligible. Re-run with --perform (and --push) to promote.");
|
|
2665
2710
|
process.exit(check.canPromote ? 0 : 1);
|
|
2666
2711
|
}
|
|
2667
2712
|
const result = promote(cwd, { push });
|
|
2713
|
+
for (const w of result.warnings ?? []) console.warn(`\u26A0 ${w}`);
|
|
2668
2714
|
console.log(`${result.promoted ? "\u2714" : "\u2718"} ${result.reason}`);
|
|
2669
2715
|
process.exit(result.promoted ? 0 : 1);
|
|
2670
2716
|
}
|
|
@@ -2767,7 +2813,7 @@ var HELP = `greenlight <command>
|
|
|
2767
2813
|
doctor manifest + repo consistency checks
|
|
2768
2814
|
help show this message
|
|
2769
2815
|
|
|
2770
|
-
Real cloud deploys need the target's creds (e.g. CLOUDFLARE_API_TOKEN); see greenlight-v1.md \xA716.`;
|
|
2816
|
+
Real cloud deploys need the target's creds (e.g. CLOUDFLARE_API_TOKEN); see docs/archive/greenlight-v1.md \xA716.`;
|
|
2771
2817
|
async function main() {
|
|
2772
2818
|
const [cmd, ...args] = process.argv.slice(2);
|
|
2773
2819
|
switch (cmd) {
|
|
@@ -41,7 +41,7 @@ var ToolSchema = z.object({
|
|
|
41
41
|
// (poly-repo) tool sets '.' (the repo root).
|
|
42
42
|
dir: z.string().optional(),
|
|
43
43
|
// The tool's code lives in another repo — this entry is a registry pointer only,
|
|
44
|
-
// not built/deployed here (greenlight-v1.md §15.5 poly-repo).
|
|
44
|
+
// not built/deployed here (docs/archive/greenlight-v1.md §15.5 poly-repo).
|
|
45
45
|
external: z.boolean().default(false),
|
|
46
46
|
// How `greenlight preview` spins the tool up LOCALLY for the pre-deploy gate. Optional — node
|
|
47
47
|
// lanes (astro/next/mcp→workers) use the built-in build+serve path. Set it for targets with no
|
|
@@ -206,8 +206,7 @@ async function checkInternalLinks(base, max = 25) {
|
|
|
206
206
|
return { name: "no broken internal links", pass: false, detail: msg(e) };
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
|
-
async function
|
|
210
|
-
const base = trimSlash(baseUrl);
|
|
209
|
+
async function runChecks(base, spec) {
|
|
211
210
|
const checks = [];
|
|
212
211
|
for (const c of spec.checks ?? []) checks.push(await checkRoute(base, c));
|
|
213
212
|
if (spec.rssValid) {
|
|
@@ -226,6 +225,17 @@ async function verifyApi(baseUrl, spec) {
|
|
|
226
225
|
);
|
|
227
226
|
}
|
|
228
227
|
if (spec.noBrokenInternalLinks) checks.push(await checkInternalLinks(base));
|
|
228
|
+
return checks;
|
|
229
|
+
}
|
|
230
|
+
async function verifyApi(baseUrl, spec) {
|
|
231
|
+
const base = trimSlash(baseUrl);
|
|
232
|
+
const retries = Math.max(0, spec.settleRetries ?? 0);
|
|
233
|
+
const delayMs = spec.settleMs ?? 5e3;
|
|
234
|
+
let checks = await runChecks(base, spec);
|
|
235
|
+
for (let i = 0; i < retries && !checks.every((c) => c.pass); i++) {
|
|
236
|
+
if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
237
|
+
checks = await runChecks(base, spec);
|
|
238
|
+
}
|
|
229
239
|
return report("api", baseUrl, checks);
|
|
230
240
|
}
|
|
231
241
|
|
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.22",
|
|
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-adapters": "0.2.
|
|
35
|
-
"@rtrentjones/greenlight-
|
|
36
|
-
"@rtrentjones/greenlight-
|
|
37
|
-
"@rtrentjones/greenlight-
|
|
34
|
+
"@rtrentjones/greenlight-adapters": "0.2.22",
|
|
35
|
+
"@rtrentjones/greenlight-verify": "0.2.22",
|
|
36
|
+
"@rtrentjones/greenlight-loop": "0.2.22",
|
|
37
|
+
"@rtrentjones/greenlight-shared": "0.2.22"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "node scripts/copy-assets.mjs && tsup",
|
|
@@ -25,4 +25,4 @@ export default { mode: 'mcp', expectTools: ['<tool>'], call: { name: '<tool>' }
|
|
|
25
25
|
|
|
26
26
|
## Auth
|
|
27
27
|
|
|
28
|
-
`auth: none` only for public read-only servers. Mutating/private servers default to `bearer`/`oauth` (greenlight-v1.md §6/§14).
|
|
28
|
+
`auth: none` only for public read-only servers. Mutating/private servers default to `bearer`/`oauth` (docs/archive/greenlight-v1.md §6/§14).
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
Lane template for **Next.js on Vercel** with Supabase — verify mode `api + playwright`.
|
|
4
4
|
|
|
5
|
-
> **Phase 0:** placeholder only. Real template content arrives when the `next` lane is exercised (HeistMind migration, **Phase 9** — greenlight-v1.md §16). Materialized into a tool by `greenlight add` / `greenlight adopt`.
|
|
5
|
+
> **Phase 0:** placeholder only. Real template content arrives when the `next` lane is exercised (HeistMind migration, **Phase 9** — docs/archive/greenlight-v1.md §16). Materialized into a tool by `greenlight add` / `greenlight adopt`.
|