@rtrentjones/greenlight 0.2.5 → 0.2.7
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 +81 -0
- package/assets/skills/provider-cloudflare/SKILL.md +3 -1
- package/assets/skills/provider-github/SKILL.md +14 -0
- package/assets/skills/provider-oci/SKILL.md +4 -3
- package/dist/bin.js +71 -28
- package/dist/{chunk-XBDQJVAX.js → chunk-ADS6BJJ5.js} +4 -1
- package/dist/{chunk-KFKYLGFX.js → chunk-VONSDNH4.js} +5 -1
- package/dist/index.js +2 -2
- package/dist/{mcp-KU7WKB5K.js → mcp-3L6HJ6BH.js} +1 -1
- package/package.json +3 -3
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# @rtrentjones/greenlight
|
|
2
|
+
|
|
3
|
+
The Greenlight CLI — setup and lifecycle for the [Greenlight](https://github.com/RTrentJones/greenlight)
|
|
4
|
+
harness. Greenlight is a **clone-and-own** baseline that turns a domain + API tokens into a live
|
|
5
|
+
personal site plus a self-verifying agentic deploy loop, with plug-and-play subdomain tools (web apps
|
|
6
|
+
or MCP servers). It is provider-agnostic and free-tier-first, and it **edits declarative
|
|
7
|
+
infrastructure-as-code — your CI/CD applies it**. It is not a hosted PaaS.
|
|
8
|
+
|
|
9
|
+
This is the **single published package**: the CLI, with the framework libraries
|
|
10
|
+
(`shared`/`verify`/`adapters`/`loop`) bundled in. The Terraform modules are distributed as git tags
|
|
11
|
+
(pinned in lockstep with this package's version); the skills ship as a Claude Code plugin.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @rtrentjones/greenlight # or npm i / yarn add
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
A personal site is a **thin consumer** that depends on this package and owns only its manifest
|
|
20
|
+
(`greenlight.config.ts`) + content. Update with `pnpm update @rtrentjones/greenlight` — no framework
|
|
21
|
+
code to merge.
|
|
22
|
+
|
|
23
|
+
Optional peer features lazy-load and degrade to a failing check if absent (never a crash):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pnpm add -D playwright @anthropic-ai/sdk # only for verify modes agent-web / eval
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## CLI
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
greenlight <command>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Command | What it does |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `init --domain <d>` | scaffold the manifest + secrets store |
|
|
38
|
+
| `add <name> --lane <l> --target <t> [--data --auth --envs]` | **infra editor**: manifest entry → emit `infra/<name>.tf` + gather/verify tokens + wire the kit (never applies) |
|
|
39
|
+
| `adopt <name> --repo <url\|path> --lane --target` | onboard an existing tool repo (submodule wrapper, or `--standalone`) |
|
|
40
|
+
| `secrets gather <name> [--repo o/r] [--oci-config <path>]` | guided, link-first token prompts straight to GitHub secrets (no disk, no logs) |
|
|
41
|
+
| `secrets sync [--repo o/r] [--env <env>]` | push `.greenlight/secrets.env` → GitHub Actions secrets |
|
|
42
|
+
| `agent sync` | materialize the agent loop kit (skill + `.mcp.json` + CLAUDE block) into a repo |
|
|
43
|
+
| `preview <name>` | build + serve locally + verify in one command |
|
|
44
|
+
| `verify <name> --env <beta\|prod>` (or `--url`) | run the shared verify harness |
|
|
45
|
+
| `promote <name>` | gated `develop → main` fast-forward (after beta verify) |
|
|
46
|
+
| `deploy <name>` | target deploy hook (e.g. OCI restart = re-pull) |
|
|
47
|
+
| `doctor` / `config` | health checks / load + validate + print the manifest |
|
|
48
|
+
|
|
49
|
+
## The loop
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
greenlight add notes --lane mcp --target oci # one entry → Terraform + tokens + kit
|
|
53
|
+
greenlight verify notes --env beta # api | mcp | playwright | test | agent-web | eval
|
|
54
|
+
greenlight promote notes # gated develop → main, after beta verify passes
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The `verify` gate is the same code CI **and** the agent run — so changes ship with objective
|
|
58
|
+
confidence, not vibes.
|
|
59
|
+
|
|
60
|
+
## Programmatic API
|
|
61
|
+
|
|
62
|
+
Typed helpers for your `greenlight.config.ts` and `verify.config.ts`:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { defineConfig, defineVerify } from '@rtrentjones/greenlight';
|
|
66
|
+
|
|
67
|
+
export default defineConfig({
|
|
68
|
+
domain: 'you.dev',
|
|
69
|
+
tools: { notes: { lane: 'mcp', target: 'oci', data: 'none', auth: 'bearer' } },
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Also exported: `loadConfig`, and the `GreenlightConfig` / `VerifySpec` types.
|
|
74
|
+
|
|
75
|
+
## Links
|
|
76
|
+
|
|
77
|
+
- **Repo + full docs:** <https://github.com/RTrentJones/greenlight>
|
|
78
|
+
- **Architecture:** [docs/architecture.md](https://github.com/RTrentJones/greenlight/blob/main/docs/architecture.md)
|
|
79
|
+
- **Spec:** [greenlight-v1.md](https://github.com/RTrentJones/greenlight/blob/main/greenlight-v1.md)
|
|
80
|
+
|
|
81
|
+
MIT
|
|
@@ -12,10 +12,12 @@ blog and throwaway MCP dev targets.
|
|
|
12
12
|
|
|
13
13
|
## Token — `CLOUDFLARE_API_TOKEN`
|
|
14
14
|
|
|
15
|
-
One token,
|
|
15
|
+
One token, these scopes (a missing scope took down a live apply more than once):
|
|
16
16
|
- **Account · Workers Scripts · Edit** — deploy the keepalive worker / workers-target tools.
|
|
17
17
|
- **Zone · DNS · Edit** — the subdomain CNAMEs.
|
|
18
18
|
- **Account · Account Settings · Read** — read account id.
|
|
19
|
+
- **Account · Cloudflare Tunnel · Edit** — only if a tool uses `target: oci` (the cloudflared
|
|
20
|
+
tunnel). Without it, the tunnel resource fails with **403 Forbidden** on `cfd_tunnel` at apply.
|
|
19
21
|
|
|
20
22
|
Create at dash → My Profile → API Tokens → Custom Token. Store in `.greenlight/secrets.env`
|
|
21
23
|
(gitignored) and push to GitHub Actions with `greenlight secrets sync`. `greenlight add`
|
|
@@ -18,6 +18,20 @@ workflows. The `develop → main` flow is standardized (PR → preview, `develop
|
|
|
18
18
|
use a fine-grained **PAT** with the minimal scopes (e.g. `Secrets: write`, `Administration`
|
|
19
19
|
for protection). Prefer **GitHub OIDC → cloud** over long-lived cloud tokens where supported.
|
|
20
20
|
|
|
21
|
+
### Poly-repo deploy loop — the two option-B PATs
|
|
22
|
+
|
|
23
|
+
An adopted tool (submodule) and its wrapper hand off via two fine-grained PATs (`secrets gather`
|
|
24
|
+
pushes each to the right repo; see docs/provider-tokens.md):
|
|
25
|
+
|
|
26
|
+
- **`GREENLIGHT_DISPATCH_TOKEN`** — on the **tool** repo, scoped **Contents: write** on the
|
|
27
|
+
**wrapper** → the tool's build fires `repository_dispatch` so the wrapper deploys.
|
|
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.
|
|
31
|
+
|
|
32
|
+
Provider creds (OCI/Cloudflare/…) live **only in the wrapper**; the tool repo holds just the
|
|
33
|
+
dispatch PAT (its build pushes to GHCR with the built-in `github.token`).
|
|
34
|
+
|
|
21
35
|
## Secrets sync
|
|
22
36
|
|
|
23
37
|
`greenlight secrets sync [--repo o/r] [--env <env>]` pushes `.greenlight/secrets.env` to the
|
|
@@ -37,9 +37,10 @@ The `oci` provider (auth below) is added to `infra/main.tf`.
|
|
|
37
37
|
`greenlight secrets gather <tool> --repo <o/r>` pushes the OCI creds straight to GitHub secrets
|
|
38
38
|
(hidden prompts, no disk/logs). **The only manual OCI inputs are the API-key auth values** —
|
|
39
39
|
`TF_VAR_OCI_TENANCY_OCID`, `TF_VAR_OCI_USER_OCID`, `TF_VAR_OCI_FINGERPRINT`, `TF_VAR_OCI_PRIVATE_KEY`
|
|
40
|
-
(PEM), `TF_VAR_OCI_REGION
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
(PEM), `TF_VAR_OCI_REGION`. `TF_VAR_OCI_COMPARTMENT_ID` is **optional** (blank → the tenancy/root
|
|
41
|
+
compartment). Auth is API-key request signing — no bearer, so no fetch-verify. The container
|
|
42
|
+
instance OCID is **not** a manual input — the deploy workflow resolves it at deploy time from OCI
|
|
43
|
+
by the instance's display name (= the tool name), so it's abstracted from the developer.
|
|
43
44
|
|
|
44
45
|
**The VCN, subnet, and availability domain are NOT manual** — they're Terraform: the `oci-network`
|
|
45
46
|
module creates the VCN + a public (egress-only) subnet, and the container-instance module looks 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";
|
|
@@ -92,7 +92,8 @@ function addTool(config, t) {
|
|
|
92
92
|
envs: t.envs ?? ["beta", "prod"],
|
|
93
93
|
...t.dir !== void 0 ? { dir: t.dir } : {},
|
|
94
94
|
...t.adopted ? { adopted: true } : {},
|
|
95
|
-
...t.external ? { external: true } : {}
|
|
95
|
+
...t.external ? { external: true } : {},
|
|
96
|
+
...t.port !== void 0 ? { port: t.port } : {}
|
|
96
97
|
}
|
|
97
98
|
]
|
|
98
99
|
};
|
|
@@ -185,16 +186,17 @@ var PACKS = [
|
|
|
185
186
|
always: true,
|
|
186
187
|
// the zone/DNS provider + Workers (keepalive) for every Greenlight setup
|
|
187
188
|
appliesTo: () => true,
|
|
188
|
-
guide: "docs/provider-tokens.md \u2014 CLOUDFLARE_API_TOKEN (Workers Scripts:Edit + Zone DNS:Edit)",
|
|
189
|
+
guide: "docs/provider-tokens.md \u2014 CLOUDFLARE_API_TOKEN (Workers Scripts:Edit + Zone DNS:Edit + Cloudflare Tunnel:Edit for oci tools)",
|
|
189
190
|
setupUrl: "https://dash.cloudflare.com/profile/api-tokens",
|
|
190
191
|
tokens: [
|
|
191
192
|
{
|
|
192
193
|
envVar: "CLOUDFLARE_API_TOKEN",
|
|
193
|
-
label: "API token \u2014 Workers Scripts:Edit + Zone DNS:Edit",
|
|
194
|
+
label: "API token \u2014 Workers Scripts:Edit + Zone DNS:Edit (+ Cloudflare Tunnel:Edit for oci)",
|
|
194
195
|
scopes: [
|
|
195
196
|
"Account \xB7 Workers Scripts \xB7 Edit",
|
|
196
197
|
"Zone \xB7 DNS \xB7 Edit",
|
|
197
|
-
"Account \xB7 Account Settings \xB7 Read"
|
|
198
|
+
"Account \xB7 Account Settings \xB7 Read",
|
|
199
|
+
"Account \xB7 Cloudflare Tunnel \xB7 Edit (only if a tool uses target: oci)"
|
|
198
200
|
],
|
|
199
201
|
verify: async (t) => {
|
|
200
202
|
const r = await fetch("https://api.cloudflare.com/client/v4/user/tokens/verify", {
|
|
@@ -349,9 +351,12 @@ var PACKS = [
|
|
|
349
351
|
setupUrl: "https://github.com/settings/personal-access-tokens/new"
|
|
350
352
|
},
|
|
351
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.
|
|
352
356
|
envVar: "GREENLIGHT_STATUS_TOKEN",
|
|
353
|
-
label: "GitHub PAT,
|
|
357
|
+
label: "GitHub PAT, Commit statuses:write on the TOOL (WRAPPER posts deploy status back)",
|
|
354
358
|
optional: true,
|
|
359
|
+
perTool: true,
|
|
355
360
|
setupUrl: "https://github.com/settings/personal-access-tokens/new"
|
|
356
361
|
}
|
|
357
362
|
],
|
|
@@ -385,7 +390,7 @@ function tokensForTool(tool) {
|
|
|
385
390
|
}
|
|
386
391
|
|
|
387
392
|
// src/version.ts
|
|
388
|
-
var MODULE_REF = "v0.2.
|
|
393
|
+
var MODULE_REF = "v0.2.7";
|
|
389
394
|
var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
|
|
390
395
|
function moduleSource(module, ref = MODULE_REF) {
|
|
391
396
|
return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
|
|
@@ -395,6 +400,7 @@ function moduleSource(module, ref = MODULE_REF) {
|
|
|
395
400
|
var hcl = (s) => s.replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
396
401
|
function emitToolTf(opts) {
|
|
397
402
|
const { name, domain, lane, target, data, envs, ref = MODULE_REF } = opts;
|
|
403
|
+
const port = opts.port ?? 8e3;
|
|
398
404
|
const slug = opts.slug ?? `OWNER/${name}`;
|
|
399
405
|
const useSupabase = data === "supabase";
|
|
400
406
|
const useVercel = target === "vercel";
|
|
@@ -467,7 +473,7 @@ variable "${name}_vercel_project_id" {
|
|
|
467
473
|
}
|
|
468
474
|
if (useOci) {
|
|
469
475
|
blocks.push(`# OCI Container Instance (Always-Free Ampere A1) running the tool's GHCR image + a cloudflared
|
|
470
|
-
# sidecar; the tunnel routes ${name}.${domain} \u2192 the container at localhost
|
|
476
|
+
# sidecar; the tunnel routes ${name}.${domain} \u2192 the container at localhost:${port}. The tool's OWN
|
|
471
477
|
# CI builds + pushes the image (provider-agnostic); deploy = restart the instance (re-pull).
|
|
472
478
|
# beta would be a second instance + tunnel route \u2014 mind the free 2-OCPU / 12-GB A1 cap.
|
|
473
479
|
module "${name}_tunnel" {
|
|
@@ -476,7 +482,7 @@ module "${name}_tunnel" {
|
|
|
476
482
|
account_id = var.cloudflare_account_id
|
|
477
483
|
name = "${name}-tunnel"
|
|
478
484
|
ingress = [
|
|
479
|
-
{ hostname = "${name}.${domain}", service = "http://localhost
|
|
485
|
+
{ hostname = "${name}.${domain}", service = "http://localhost:${port}" },
|
|
480
486
|
]
|
|
481
487
|
}
|
|
482
488
|
|
|
@@ -560,11 +566,13 @@ function emitWrapperMainTf(opts) {
|
|
|
560
566
|
if (need.has("supabase")) providerBlocks.push('provider "supabase" {}');
|
|
561
567
|
if (need.has("oci")) {
|
|
562
568
|
providerBlocks.push(`provider "oci" {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
569
|
+
# trimspace guards against a trailing newline/space in a pasted secret (a malformed region
|
|
570
|
+
# makes the identity endpoint hostname fail to resolve \u2014 "no such host" \u2014 at plan time).
|
|
571
|
+
tenancy_ocid = trimspace(var.oci_tenancy_ocid)
|
|
572
|
+
user_ocid = trimspace(var.oci_user_ocid)
|
|
573
|
+
fingerprint = trimspace(var.oci_fingerprint)
|
|
566
574
|
private_key = var.oci_private_key
|
|
567
|
-
region = var.oci_region
|
|
575
|
+
region = trimspace(var.oci_region)
|
|
568
576
|
}`);
|
|
569
577
|
}
|
|
570
578
|
const vars = ['variable "cloudflare_zone_id" { type = string }'];
|
|
@@ -792,7 +800,8 @@ async function gatherSecrets(name, repo, env, prefill) {
|
|
|
792
800
|
for (const pack of packs) {
|
|
793
801
|
console.log(`\u2500\u2500 ${pack.name}${pack.setupUrl ? ` \u2192 ${pack.setupUrl}` : ""}`);
|
|
794
802
|
for (const tok of pack.tokens) {
|
|
795
|
-
const
|
|
803
|
+
const suffix = `_${name.toUpperCase().replace(/-/g, "_")}`;
|
|
804
|
+
const key = tok.envVar.toUpperCase() + (tok.perTool ? suffix : "");
|
|
796
805
|
if (key === "GITHUB_TOKEN") {
|
|
797
806
|
console.log(" \xB7 GITHUB_TOKEN \u2014 provided automatically by Actions; skipping");
|
|
798
807
|
continue;
|
|
@@ -1039,7 +1048,7 @@ async function addCommand(args) {
|
|
|
1039
1048
|
const name = args[0];
|
|
1040
1049
|
if (!name || name.startsWith("-")) {
|
|
1041
1050
|
throw new Error(
|
|
1042
|
-
"usage: greenlight add <name> --lane <lane> --target <target> [--data <d>] [--auth <a>] [--envs beta,prod]"
|
|
1051
|
+
"usage: greenlight add <name> --lane <lane> --target <target> [--data <d>] [--auth <a>] [--envs beta,prod] [--port 8000]"
|
|
1043
1052
|
);
|
|
1044
1053
|
}
|
|
1045
1054
|
const lane = flag2(args, "--lane");
|
|
@@ -1049,13 +1058,15 @@ async function addCommand(args) {
|
|
|
1049
1058
|
if (path.endsWith(".example.ts")) {
|
|
1050
1059
|
throw new Error("no greenlight.config.ts \u2014 run `greenlight init` first");
|
|
1051
1060
|
}
|
|
1061
|
+
const portFlag = flag2(args, "--port");
|
|
1052
1062
|
const next = addTool(config, {
|
|
1053
1063
|
name,
|
|
1054
1064
|
lane,
|
|
1055
1065
|
target,
|
|
1056
1066
|
data: flag2(args, "--data"),
|
|
1057
1067
|
auth: flag2(args, "--auth"),
|
|
1058
|
-
envs: flag2(args, "--envs")?.split(",")
|
|
1068
|
+
envs: flag2(args, "--envs")?.split(","),
|
|
1069
|
+
port: portFlag ? Number(portFlag) : void 0
|
|
1059
1070
|
});
|
|
1060
1071
|
const entry = next.tools.find((t) => t.name === name);
|
|
1061
1072
|
const data = entry?.data ?? "none";
|
|
@@ -1094,7 +1105,10 @@ async function addCommand(args) {
|
|
|
1094
1105
|
if (existsSync6(toolTf)) {
|
|
1095
1106
|
console.log(`\xB7 infra/${name}.tf exists \u2014 left as-is`);
|
|
1096
1107
|
} else {
|
|
1097
|
-
writeFileSync3(
|
|
1108
|
+
writeFileSync3(
|
|
1109
|
+
toolTf,
|
|
1110
|
+
emitToolTf({ name, domain: config.domain, lane, target, data, envs, port: entry?.port })
|
|
1111
|
+
);
|
|
1098
1112
|
console.log(`\u2714 wrote infra/${name}.tf (modules: ${providers.join(", ")})`);
|
|
1099
1113
|
}
|
|
1100
1114
|
if (!args.includes("--no-tokens")) {
|
|
@@ -1307,13 +1321,13 @@ concurrency:
|
|
|
1307
1321
|
|
|
1308
1322
|
jobs:
|
|
1309
1323
|
build:
|
|
1310
|
-
|
|
1324
|
+
# Native arm64 runner \u2014 builds the arm64 image directly (no QEMU emulation, much faster).
|
|
1325
|
+
runs-on: ubuntu-24.04-arm
|
|
1311
1326
|
steps:
|
|
1312
1327
|
- uses: actions/checkout@v4
|
|
1313
1328
|
- name: Resolve image ref (GHCR namespaces are lowercase)
|
|
1314
1329
|
id: img
|
|
1315
|
-
run: echo "
|
|
1316
|
-
- uses: docker/setup-qemu-action@v3
|
|
1330
|
+
run: echo "base=ghcr.io/\${GITHUB_REPOSITORY_OWNER,,}/${name}" >> "$GITHUB_OUTPUT"
|
|
1317
1331
|
- uses: docker/setup-buildx-action@v3
|
|
1318
1332
|
- uses: docker/login-action@v3
|
|
1319
1333
|
with:
|
|
@@ -1325,7 +1339,12 @@ jobs:
|
|
|
1325
1339
|
context: .
|
|
1326
1340
|
platforms: linux/arm64
|
|
1327
1341
|
push: true
|
|
1328
|
-
|
|
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
|
|
1329
1348
|
- name: Notify wrapper to deploy
|
|
1330
1349
|
env:
|
|
1331
1350
|
GH_TOKEN: \${{ secrets.GREENLIGHT_DISPATCH_TOKEN }}
|
|
@@ -1356,7 +1375,7 @@ jobs:
|
|
|
1356
1375
|
- uses: jdx/mise-action@v2
|
|
1357
1376
|
- run: pnpm install --frozen-lockfile
|
|
1358
1377
|
- run: pip install --quiet oci-cli
|
|
1359
|
-
- name: Deploy (
|
|
1378
|
+
- name: Deploy (resolve instance OCID by name -> restart -> re-pull GHCR image)
|
|
1360
1379
|
env:
|
|
1361
1380
|
# The OCI CLI reuses the SAME TF_VAR_OCI_* secrets the apply uses \u2014 one secret set.
|
|
1362
1381
|
OCI_CLI_TENANCY: \${{ secrets.TF_VAR_OCI_TENANCY_OCID }}
|
|
@@ -1364,12 +1383,36 @@ jobs:
|
|
|
1364
1383
|
OCI_CLI_FINGERPRINT: \${{ secrets.TF_VAR_OCI_FINGERPRINT }}
|
|
1365
1384
|
OCI_CLI_KEY_CONTENT: \${{ secrets.TF_VAR_OCI_PRIVATE_KEY }}
|
|
1366
1385
|
OCI_CLI_REGION: \${{ secrets.TF_VAR_OCI_REGION }}
|
|
1367
|
-
|
|
1368
|
-
run:
|
|
1386
|
+
OCI_COMPARTMENT_ID: \${{ secrets.TF_VAR_OCI_COMPARTMENT_ID }}
|
|
1387
|
+
run: |
|
|
1388
|
+
# The instance OCID is abstracted from the developer: resolve it from OCI by the
|
|
1389
|
+
# instance's display name (= the tool name, set by the oci-container-instance module).
|
|
1390
|
+
# No manually-fetched/stored OCID secret. Compartment falls back to the tenancy (root).
|
|
1391
|
+
set -o pipefail
|
|
1392
|
+
COMPARTMENT="\${OCI_COMPARTMENT_ID:-$OCI_CLI_TENANCY}"
|
|
1393
|
+
echo "Resolving '${name}' (region=$OCI_CLI_REGION)\u2026"
|
|
1394
|
+
oci container-instances container-instance list \\
|
|
1395
|
+
--compartment-id "$COMPARTMENT" --display-name ${name} --all > list.json \\
|
|
1396
|
+
|| { echo "::error::oci list failed (auth/region/compartment) \u2014 see output above"; exit 1; }
|
|
1397
|
+
# OCIDs are identifiers, not secrets (tenancy/compartment are masked by Actions).
|
|
1398
|
+
echo "Matches:"; jq -r '(.data.items // .data // [])[]? | " \\(.["lifecycle-state"] // "?") \\(.id)"' list.json
|
|
1399
|
+
OCID=$(jq -r '[(.data.items // .data // [])[]? | select((.["lifecycle-state"] // "")=="ACTIVE") | .id][0] // ""' list.json)
|
|
1400
|
+
if [ -z "$OCID" ]; then
|
|
1401
|
+
echo "::error::no ACTIVE container instance named '${name}' in compartment $COMPARTMENT"
|
|
1402
|
+
exit 1
|
|
1403
|
+
fi
|
|
1404
|
+
echo "Resolved ${name} instance: $OCID"
|
|
1405
|
+
OCI_CONTAINER_INSTANCE_OCID="$OCID" pnpm exec greenlight deploy ${name} --env prod
|
|
1406
|
+
- name: Verify prod (gate the signal on real health, not just the restart)
|
|
1407
|
+
# The deploy "succeeds" only if the NEW image is actually serving. verify has a built-in
|
|
1408
|
+
# readiness wait (re-pull + container start). A failure here fails the job \u2192 the status
|
|
1409
|
+
# posted back is red. oci is verify-gated direct-to-prod (no cheap standing beta on free A1).
|
|
1410
|
+
run: pnpm exec greenlight verify ${name} --env prod
|
|
1369
1411
|
- name: Report status back to ${toolRepo}
|
|
1370
1412
|
if: \${{ always() && github.event.client_payload.sha != '' }}
|
|
1371
1413
|
env:
|
|
1372
|
-
|
|
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, "_")} }}
|
|
1373
1416
|
run: |
|
|
1374
1417
|
[ -z "$GH_TOKEN" ] && exit 0
|
|
1375
1418
|
gh api repos/${toolRepo}/statuses/\${{ github.event.client_payload.sha }} \\
|
|
@@ -1488,7 +1531,7 @@ Next:
|
|
|
1488
1531
|
git commit && git push # CI (infra.yml) applies. Tool's CI builds; wrapper deploys.${target === "oci" ? `
|
|
1489
1532
|
Secrets (guided): greenlight secrets gather ${name} --repo <wrapper> # TF_VAR_OCI_* + GREENLIGHT_STATUS_TOKEN
|
|
1490
1533
|
greenlight secrets gather ${name} --repo ${slug} # GREENLIGHT_DISPATCH_TOKEN
|
|
1491
|
-
|
|
1534
|
+
The instance OCID is auto-resolved by the deploy workflow (by display name) \u2014 nothing to set.` : ""}`);
|
|
1492
1535
|
}
|
|
1493
1536
|
async function adoptStandalone(ctx) {
|
|
1494
1537
|
const { name, repoArg, lane, target, data, auth, envs, domain, reg, regPath } = ctx;
|
|
@@ -1710,7 +1753,7 @@ async function deployCommand(args) {
|
|
|
1710
1753
|
}
|
|
1711
1754
|
const { config } = await loadManifest();
|
|
1712
1755
|
const entry = resolveEntry(config, name);
|
|
1713
|
-
if (entry.external) {
|
|
1756
|
+
if (entry.external && entry.target !== "oci") {
|
|
1714
1757
|
throw new Error(`"${name}" is external (registry pointer) \u2014 deploy it from its own repo`);
|
|
1715
1758
|
}
|
|
1716
1759
|
const adapter = createAdapter(entry.target, { domain: config.domain, name: entry.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 });
|
|
@@ -33,6 +33,10 @@ var ToolSchema = z.object({
|
|
|
33
33
|
access: AccessEnum.default("public"),
|
|
34
34
|
envs: z.array(EnvEnum).nonempty("a tool needs at least one env"),
|
|
35
35
|
adopted: z.boolean().default(false),
|
|
36
|
+
// The port the container listens on (target: oci). The tunnel routes to localhost:<port>;
|
|
37
|
+
// defaults to 8000 (the mcp/FastMCP convention). Set it for a lane:docker tool on a different
|
|
38
|
+
// port so the oci modules stay generic. Ignored by non-oci targets.
|
|
39
|
+
port: z.number().int().positive().optional(),
|
|
36
40
|
// Directory the tool builds/deploys from. Defaults to tools/<name>; a standalone
|
|
37
41
|
// (poly-repo) tool sets '.' (the repo root).
|
|
38
42
|
dir: z.string().optional(),
|
|
@@ -225,7 +229,7 @@ async function verify(baseUrl, spec, opts) {
|
|
|
225
229
|
case "api":
|
|
226
230
|
return verifyApi(baseUrl, spec);
|
|
227
231
|
case "mcp": {
|
|
228
|
-
const { verifyMcp: verifyMcp2 } = await import("./mcp-
|
|
232
|
+
const { verifyMcp: verifyMcp2 } = await import("./mcp-3L6HJ6BH.js");
|
|
229
233
|
return verifyMcp2(baseUrl, spec);
|
|
230
234
|
}
|
|
231
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.7",
|
|
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",
|