@rtrentjones/greenlight 0.2.4 → 0.2.5
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-oci/SKILL.md +25 -10
- package/dist/bin.js +228 -46
- package/package.json +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: provider-oci
|
|
3
|
-
description: How Oracle Cloud (OCI) works in a Greenlight setup — the `target: oci` runtime for stateful MCP servers (BAMCP) on a free-tier Ampere A1 Container Instance, the provider-agnostic build-via-GitHub→GHCR model, Greenlight-owned compute + tunnel Terraform, the OCI token CLI, deploy = restart, and the
|
|
3
|
+
description: How Oracle Cloud (OCI) works in a Greenlight setup — the `target: oci` runtime for stateful MCP servers (BAMCP) on a free-tier Ampere A1 Container Instance, the provider-agnostic build-via-GitHub→GHCR model, Greenlight-owned compute + tunnel Terraform, the OCI token CLI, deploy = restart, and staying on the free tier (no PAYG; recover-on-alert). Use when wiring/debugging an oci-target tool.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# provider-oci
|
|
@@ -34,11 +34,25 @@ The `oci` provider (auth below) is added to `infra/main.tf`.
|
|
|
34
34
|
|
|
35
35
|
## OCI token CLI
|
|
36
36
|
|
|
37
|
-
`greenlight
|
|
38
|
-
**
|
|
39
|
-
`
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
`greenlight secrets gather <tool> --repo <o/r>` pushes the OCI creds straight to GitHub secrets
|
|
38
|
+
(hidden prompts, no disk/logs). **The only manual OCI inputs are the API-key auth values** —
|
|
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` — plus `OCI_CONTAINER_INSTANCE_OCID` (the Terraform output, set after the
|
|
41
|
+
first apply, for deploy). `TF_VAR_OCI_COMPARTMENT_ID` is **optional** (blank → the tenancy/root
|
|
42
|
+
compartment). Auth is API-key request signing — no bearer, so no fetch-verify.
|
|
43
|
+
|
|
44
|
+
**The VCN, subnet, and availability domain are NOT manual** — they're Terraform: the `oci-network`
|
|
45
|
+
module creates the VCN + a public (egress-only) subnet, and the container-instance module looks the
|
|
46
|
+
AD up via an `oci_identity_availability_domains` data source. So the bootstrap is just "create one
|
|
47
|
+
API key" — Terraform can't create the credential it needs to authenticate, but it owns everything
|
|
48
|
+
after that. (Out-of-A1-capacity in one AD? set `availability_domain` on the instance module to pin
|
|
49
|
+
another — the only time you touch it.)
|
|
50
|
+
|
|
51
|
+
**Shortcut — feed the API-key config preview directly.** After *Add API key*, OCI shows a
|
|
52
|
+
"Configuration file preview" (the `[DEFAULT]` block) and you download the `.pem`. Pass both:
|
|
53
|
+
`greenlight secrets gather <tool> --repo <o/r> --oci-config ~/path/config [--oci-key ~/path/key.pem]`
|
|
54
|
+
— it auto-fills the 5 auth secrets (incl. the multi-line PEM, read from the file so it's never
|
|
55
|
+
pasted) and only prompts for the 3 placement values + the option-B deploy PATs.
|
|
42
56
|
|
|
43
57
|
## Deploy = restart (re-pull)
|
|
44
58
|
|
|
@@ -46,11 +60,12 @@ The `oci` provider (auth below) is added to `infra/main.tf`.
|
|
|
46
60
|
--container-instance-id <OCID>` — the instance re-pulls the latest GHCR image. The tool's CI
|
|
47
61
|
builds; an event trigger (the chosen deploy option) fires the restart. The adapter does NOT build.
|
|
48
62
|
|
|
49
|
-
##
|
|
63
|
+
## Idle-reclaim — stay free, recover on alert
|
|
50
64
|
|
|
51
|
-
OCI
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
OCI Always-Free can reclaim idle compute. We **stay on the free tier** and accept that: the
|
|
66
|
+
instance runs restart-policy ALWAYS, keepalive health-checks it, and if it's ever stopped/reclaimed
|
|
67
|
+
the alert fires and a **re-apply / redeploy** restores it. **PAYG is NOT used** — it's an optional
|
|
68
|
+
last resort (see `docs/oci-payg-runbook.md`) only if reclaim ever becomes a recurring problem.
|
|
54
69
|
|
|
55
70
|
## Verify
|
|
56
71
|
The tool is typically an **MCP server**: verify with `mode: mcp`, connect at `<name>.<domain>/mcp`
|
package/dist/bin.js
CHANGED
|
@@ -132,6 +132,7 @@ function resolveEntry(config, name) {
|
|
|
132
132
|
name: void 0,
|
|
133
133
|
lane: config.blog.lane,
|
|
134
134
|
target: config.blog.target,
|
|
135
|
+
data: config.blog.data,
|
|
135
136
|
dir: "apps/blog",
|
|
136
137
|
external: false
|
|
137
138
|
};
|
|
@@ -145,6 +146,7 @@ function resolveEntry(config, name) {
|
|
|
145
146
|
name: tool.name,
|
|
146
147
|
lane: tool.lane,
|
|
147
148
|
target: tool.target,
|
|
149
|
+
data: tool.data,
|
|
148
150
|
dir: tool.dir ?? `tools/${tool.name}`,
|
|
149
151
|
external: tool.external
|
|
150
152
|
};
|
|
@@ -184,6 +186,7 @@ var PACKS = [
|
|
|
184
186
|
// the zone/DNS provider + Workers (keepalive) for every Greenlight setup
|
|
185
187
|
appliesTo: () => true,
|
|
186
188
|
guide: "docs/provider-tokens.md \u2014 CLOUDFLARE_API_TOKEN (Workers Scripts:Edit + Zone DNS:Edit)",
|
|
189
|
+
setupUrl: "https://dash.cloudflare.com/profile/api-tokens",
|
|
187
190
|
tokens: [
|
|
188
191
|
{
|
|
189
192
|
envVar: "CLOUDFLARE_API_TOKEN",
|
|
@@ -214,6 +217,7 @@ var PACKS = [
|
|
|
214
217
|
name: "Vercel",
|
|
215
218
|
appliesTo: (t) => t.target === "vercel",
|
|
216
219
|
guide: "docs/provider-tokens.md \u2014 VERCEL_API_TOKEN (team-scoped)",
|
|
220
|
+
setupUrl: "https://vercel.com/account/settings/tokens",
|
|
217
221
|
tokens: [
|
|
218
222
|
{
|
|
219
223
|
envVar: "VERCEL_API_TOKEN",
|
|
@@ -235,6 +239,7 @@ var PACKS = [
|
|
|
235
239
|
name: "Supabase",
|
|
236
240
|
appliesTo: (t) => t.data === "supabase",
|
|
237
241
|
guide: "docs/provider-tokens.md \u2014 SUPABASE_ACCESS_TOKEN (Management API)",
|
|
242
|
+
setupUrl: "https://supabase.com/dashboard/account/tokens",
|
|
238
243
|
tokens: [
|
|
239
244
|
{
|
|
240
245
|
envVar: "SUPABASE_ACCESS_TOKEN",
|
|
@@ -269,6 +274,7 @@ var PACKS = [
|
|
|
269
274
|
// remote state backs every wrapper's infra
|
|
270
275
|
appliesTo: () => true,
|
|
271
276
|
guide: "docs/terraform-state-r2.md \u2014 HCP Terraform free tier (no credit card)",
|
|
277
|
+
setupUrl: "https://app.terraform.io/app/settings/tokens",
|
|
272
278
|
tokens: [
|
|
273
279
|
{
|
|
274
280
|
envVar: "TF_API_TOKEN",
|
|
@@ -290,6 +296,7 @@ var PACKS = [
|
|
|
290
296
|
// secrets sync + repo/branch infra
|
|
291
297
|
appliesTo: () => true,
|
|
292
298
|
guide: "docs/provider-tokens.md \u2014 GitHub (gh auth, or a fine-grained PAT)",
|
|
299
|
+
setupUrl: "https://github.com/settings/personal-access-tokens/new",
|
|
293
300
|
tokens: [
|
|
294
301
|
{
|
|
295
302
|
envVar: "GITHUB_TOKEN",
|
|
@@ -305,7 +312,8 @@ var PACKS = [
|
|
|
305
312
|
id: "oci",
|
|
306
313
|
name: "Oracle Cloud (OCI)",
|
|
307
314
|
appliesTo: (t) => t.target === "oci",
|
|
308
|
-
guide: "docs/oci-payg-runbook.md \u2014 Always-Free A1 Container Instance + tunnel (PAYG
|
|
315
|
+
guide: "docs/oci-payg-runbook.md \u2014 Always-Free A1 Container Instance + tunnel (no PAYG)",
|
|
316
|
+
setupUrl: "https://cloud.oracle.com \u2014 Profile \u2192 User settings \u2192 Tokens and keys \u2192 Add API key",
|
|
309
317
|
tokens: [
|
|
310
318
|
// OCI provider auth = API-key request signing (no bearer → no cheap fetch verify). These
|
|
311
319
|
// flow to the `oci` Terraform provider as TF_VAR_oci_* (the wrapper apply uses them).
|
|
@@ -318,16 +326,12 @@ var PACKS = [
|
|
|
318
326
|
optional: true
|
|
319
327
|
},
|
|
320
328
|
{ envVar: "TF_VAR_oci_region", label: "OCI region, e.g. us-ashburn-1", optional: true },
|
|
321
|
-
//
|
|
322
|
-
|
|
329
|
+
// Compartment is the ONLY placement input, and it's optional — blank → the tenancy (root)
|
|
330
|
+
// compartment. The VCN/subnet are created by the oci-network module and the availability
|
|
331
|
+
// domain is looked up by a data source, so neither is a manual secret anymore.
|
|
323
332
|
{
|
|
324
|
-
envVar: "
|
|
325
|
-
label: "OCI
|
|
326
|
-
optional: true
|
|
327
|
-
},
|
|
328
|
-
{
|
|
329
|
-
envVar: "TF_VAR_oci_subnet_id",
|
|
330
|
-
label: "OCI subnet OCID (public, for egress)",
|
|
333
|
+
envVar: "TF_VAR_oci_compartment_id",
|
|
334
|
+
label: "OCI compartment OCID (optional \u2014 blank uses the tenancy/root compartment)",
|
|
331
335
|
optional: true
|
|
332
336
|
},
|
|
333
337
|
// Deploy (restart the instance → re-pull). Set from the Terraform output.
|
|
@@ -335,11 +339,25 @@ var PACKS = [
|
|
|
335
339
|
envVar: "OCI_CONTAINER_INSTANCE_OCID",
|
|
336
340
|
label: "container instance OCID (TF output) \u2014 `greenlight deploy` restarts it",
|
|
337
341
|
optional: true
|
|
342
|
+
},
|
|
343
|
+
// Option-B event-driven deploy (GitHub PATs). dispatch → set on the TOOL repo;
|
|
344
|
+
// status → set on the WRAPPER repo. Skip the one that doesn't match `--repo`.
|
|
345
|
+
{
|
|
346
|
+
envVar: "GREENLIGHT_DISPATCH_TOKEN",
|
|
347
|
+
label: "GitHub PAT, Contents:write on the WRAPPER (TOOL repo fires the deploy dispatch)",
|
|
348
|
+
optional: true,
|
|
349
|
+
setupUrl: "https://github.com/settings/personal-access-tokens/new"
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
envVar: "GREENLIGHT_STATUS_TOKEN",
|
|
353
|
+
label: "GitHub PAT, Commits:write on the TOOL (WRAPPER posts deploy status back)",
|
|
354
|
+
optional: true,
|
|
355
|
+
setupUrl: "https://github.com/settings/personal-access-tokens/new"
|
|
338
356
|
}
|
|
339
357
|
],
|
|
340
358
|
skill: "provider-oci",
|
|
341
|
-
|
|
342
|
-
|
|
359
|
+
// DNS + tunnel + network (VCN/subnet) + compute; deploy = restart.
|
|
360
|
+
tfModules: ["tool", "tunnel", "oci-network", "oci-container-instance"]
|
|
343
361
|
}
|
|
344
362
|
];
|
|
345
363
|
function packsForTool(tool) {
|
|
@@ -367,7 +385,7 @@ function tokensForTool(tool) {
|
|
|
367
385
|
}
|
|
368
386
|
|
|
369
387
|
// src/version.ts
|
|
370
|
-
var MODULE_REF = "v0.2.
|
|
388
|
+
var MODULE_REF = "v0.2.5";
|
|
371
389
|
var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
|
|
372
390
|
function moduleSource(module, ref = MODULE_REF) {
|
|
373
391
|
return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
|
|
@@ -384,13 +402,7 @@ function emitToolTf(opts) {
|
|
|
384
402
|
const envList = envs.map((e) => `"${e}"`).join(", ");
|
|
385
403
|
const blocks = [];
|
|
386
404
|
const assumes = ["var.cloudflare_zone_id"];
|
|
387
|
-
if (useOci)
|
|
388
|
-
assumes.push(
|
|
389
|
-
"var.cloudflare_account_id",
|
|
390
|
-
"var.oci_compartment_id",
|
|
391
|
-
"var.oci_availability_domain",
|
|
392
|
-
"var.oci_subnet_id"
|
|
393
|
-
);
|
|
405
|
+
if (useOci) assumes.push("var.cloudflare_account_id", "local.oci_compartment_id");
|
|
394
406
|
if (useSupabase) assumes.push("var.supabase_organization_id", "var.supabase_database_password");
|
|
395
407
|
const ghcrOwner = (slug.split("/")[0] ?? "owner").toLowerCase();
|
|
396
408
|
blocks.push(
|
|
@@ -468,15 +480,23 @@ module "${name}_tunnel" {
|
|
|
468
480
|
]
|
|
469
481
|
}
|
|
470
482
|
|
|
483
|
+
# Network is IaC too \u2014 VCN + public subnet (egress only). No hand-clicking in the OCI console.
|
|
484
|
+
module "${name}_network" {
|
|
485
|
+
source = "${moduleSource("oci-network", ref)}"
|
|
486
|
+
|
|
487
|
+
name = "${name}"
|
|
488
|
+
compartment_id = local.oci_compartment_id
|
|
489
|
+
}
|
|
490
|
+
|
|
471
491
|
module "${name}_instance" {
|
|
472
492
|
source = "${moduleSource("oci-container-instance", ref)}"
|
|
473
493
|
|
|
474
|
-
name
|
|
475
|
-
compartment_id
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
494
|
+
name = "${name}"
|
|
495
|
+
compartment_id = local.oci_compartment_id
|
|
496
|
+
subnet_id = module.${name}_network.subnet_id
|
|
497
|
+
image_url = var.${name}_image
|
|
498
|
+
tunnel_token = module.${name}_tunnel.token
|
|
499
|
+
# availability_domain is auto-picked (first AD in the compartment); set it to pin a specific AD.
|
|
480
500
|
|
|
481
501
|
# Tool runtime env \u2014 fill in (e.g. PORT/listen settings, auth). The container must listen on 8000.
|
|
482
502
|
environment = {}
|
|
@@ -561,10 +581,16 @@ function emitWrapperMainTf(opts) {
|
|
|
561
581
|
vars.push('variable "oci_fingerprint" { type = string }');
|
|
562
582
|
vars.push('variable "oci_private_key" {\n type = string\n sensitive = true\n}');
|
|
563
583
|
vars.push('variable "oci_region" { type = string }');
|
|
564
|
-
vars.push(
|
|
565
|
-
|
|
566
|
-
|
|
584
|
+
vars.push(
|
|
585
|
+
'variable "oci_compartment_id" {\n type = string\n default = "" # blank \u2192 tenancy (root) compartment\n}'
|
|
586
|
+
);
|
|
567
587
|
}
|
|
588
|
+
const localsBlock = need.has("oci") ? `
|
|
589
|
+
locals {
|
|
590
|
+
# Compartment for all OCI tools \u2014 blank var.oci_compartment_id falls back to the tenancy (root).
|
|
591
|
+
oci_compartment_id = var.oci_compartment_id != "" ? var.oci_compartment_id : var.oci_tenancy_ocid
|
|
592
|
+
}
|
|
593
|
+
` : "";
|
|
568
594
|
return `# Wrapper infra (singleton): providers + remote-state backend + shared variables.
|
|
569
595
|
# \`greenlight add\` appends per-tool module blocks as infra/<name>.tf. Apply is CI/CD's job
|
|
570
596
|
# (infra.yml). Fill in the HCP backend below before the first apply (docs/terraform-state-r2.md).
|
|
@@ -585,7 +611,7 @@ ${req.join("\n")}
|
|
|
585
611
|
${providerBlocks.join("\n")}
|
|
586
612
|
|
|
587
613
|
${vars.join("\n")}
|
|
588
|
-
`;
|
|
614
|
+
${localsBlock}`;
|
|
589
615
|
}
|
|
590
616
|
function providersForTool(tool) {
|
|
591
617
|
const ids = new Set(packsForTool(tool).map((p) => p.id));
|
|
@@ -599,12 +625,13 @@ function providersForTool(tool) {
|
|
|
599
625
|
// src/tokens.ts
|
|
600
626
|
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
601
627
|
import { resolve as resolve4 } from "path";
|
|
602
|
-
import { createInterface } from "readline/promises";
|
|
628
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
603
629
|
|
|
604
630
|
// src/commands/secrets.ts
|
|
605
631
|
import { execFileSync } from "child_process";
|
|
606
632
|
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
607
633
|
import { resolve as resolve3 } from "path";
|
|
634
|
+
import { createInterface } from "readline";
|
|
608
635
|
function parseSecretsEnv(text) {
|
|
609
636
|
const out = [];
|
|
610
637
|
for (const raw of text.split("\n")) {
|
|
@@ -616,6 +643,36 @@ function parseSecretsEnv(text) {
|
|
|
616
643
|
}
|
|
617
644
|
return out;
|
|
618
645
|
}
|
|
646
|
+
function parseOciConfig(text) {
|
|
647
|
+
const out = {};
|
|
648
|
+
for (const raw of text.split("\n")) {
|
|
649
|
+
const line = raw.trim();
|
|
650
|
+
if (line === "" || line.startsWith("#") || line.startsWith("[")) continue;
|
|
651
|
+
const eq = line.indexOf("=");
|
|
652
|
+
if (eq <= 0) continue;
|
|
653
|
+
const key = line.slice(0, eq).trim().toLowerCase();
|
|
654
|
+
if (!(key in out)) out[key] = line.slice(eq + 1).trim();
|
|
655
|
+
}
|
|
656
|
+
return out;
|
|
657
|
+
}
|
|
658
|
+
function ociPrefill(configPath, keyPath) {
|
|
659
|
+
const cfg = parseOciConfig(readFileSync(configPath, "utf8"));
|
|
660
|
+
const map = /* @__PURE__ */ new Map();
|
|
661
|
+
const set = (k, v) => {
|
|
662
|
+
if (v) map.set(k, v);
|
|
663
|
+
};
|
|
664
|
+
set("TF_VAR_OCI_USER_OCID", cfg.user);
|
|
665
|
+
set("TF_VAR_OCI_FINGERPRINT", cfg.fingerprint);
|
|
666
|
+
set("TF_VAR_OCI_TENANCY_OCID", cfg.tenancy);
|
|
667
|
+
set("TF_VAR_OCI_REGION", cfg.region);
|
|
668
|
+
const pem = keyPath ?? cfg.key_file;
|
|
669
|
+
if (pem && existsSync3(pem)) {
|
|
670
|
+
map.set("TF_VAR_OCI_PRIVATE_KEY", readFileSync(pem, "utf8"));
|
|
671
|
+
} else if (pem) {
|
|
672
|
+
console.log(` ! PEM not found at ${pem} \u2014 set TF_VAR_OCI_PRIVATE_KEY manually (--oci-key)`);
|
|
673
|
+
}
|
|
674
|
+
return map;
|
|
675
|
+
}
|
|
619
676
|
function parseRepo(remoteUrl) {
|
|
620
677
|
const m = remoteUrl.trim().match(/github\.com[/:]([^/]+)\/(.+?)(?:\.git)?$/);
|
|
621
678
|
return m ? `${m[1]}/${m[2]}` : null;
|
|
@@ -669,12 +726,135 @@ function syncSecrets(opts) {
|
|
|
669
726
|
}
|
|
670
727
|
return { repo, count: entries.length };
|
|
671
728
|
}
|
|
729
|
+
function hiddenPrompter() {
|
|
730
|
+
const tty = Boolean(process.stdin.isTTY);
|
|
731
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: tty });
|
|
732
|
+
if (tty) rl._writeToOutput = () => {
|
|
733
|
+
};
|
|
734
|
+
return {
|
|
735
|
+
ask: (query) => new Promise((resolve11) => {
|
|
736
|
+
process.stdout.write(query);
|
|
737
|
+
rl.question("", (val) => {
|
|
738
|
+
process.stdout.write("\n");
|
|
739
|
+
resolve11(val.trim());
|
|
740
|
+
});
|
|
741
|
+
}),
|
|
742
|
+
close: () => rl.close()
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
function listGitHubSecrets(repo, env) {
|
|
746
|
+
const ghArgs = ["secret", "list", "--repo", repo, "--json", "name"];
|
|
747
|
+
if (env) ghArgs.push("--env", env);
|
|
748
|
+
try {
|
|
749
|
+
const out = execFileSync("gh", ghArgs, {
|
|
750
|
+
encoding: "utf8",
|
|
751
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
752
|
+
// don't leak gh's stderr into the guided flow
|
|
753
|
+
});
|
|
754
|
+
const parsed = JSON.parse(out);
|
|
755
|
+
return new Set(parsed.map((s) => s.name));
|
|
756
|
+
} catch {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function setGitHubSecret(repo, env, key, value) {
|
|
761
|
+
const ghArgs = ["secret", "set", key, "--repo", repo];
|
|
762
|
+
if (env) ghArgs.push("--env", env);
|
|
763
|
+
try {
|
|
764
|
+
execFileSync("gh", ghArgs, { input: value });
|
|
765
|
+
} catch (e) {
|
|
766
|
+
const err = e;
|
|
767
|
+
if (err.code === "ENOENT") {
|
|
768
|
+
throw new Error("the GitHub CLI `gh` is required \u2014 install it and run `gh auth login`");
|
|
769
|
+
}
|
|
770
|
+
const detail = err.stderr?.toString().trim();
|
|
771
|
+
throw new Error(`failed to set ${key}${detail ? `: ${detail}` : " (check `gh auth status`)"}`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
async function gatherSecrets(name, repo, env, prefill) {
|
|
775
|
+
const { config } = await loadManifest();
|
|
776
|
+
const entry = resolveEntry(config, name);
|
|
777
|
+
const packs = packsForTool({ target: entry.target, data: entry.data });
|
|
778
|
+
const dest = env ? `env "${env}" of ${repo}` : repo;
|
|
779
|
+
const existing = listGitHubSecrets(repo, env);
|
|
780
|
+
console.log(`Gathering secrets for "${name}" \u2192 GitHub ${dest}`);
|
|
781
|
+
console.log(
|
|
782
|
+
"Paste each value (hidden); Enter to skip. Values go straight to GitHub \u2014 never to disk."
|
|
783
|
+
);
|
|
784
|
+
console.log(
|
|
785
|
+
`[already set] = a value exists (paste to override, Enter to keep) \xB7 [not set] = new.${existing ? "" : " (could not read existing secrets \u2014 annotations omitted)"}`
|
|
786
|
+
);
|
|
787
|
+
if (prefill?.size) console.log(`Auto-filling ${prefill.size} value(s) from the OCI config.`);
|
|
788
|
+
console.log("");
|
|
789
|
+
const prompt = hiddenPrompter();
|
|
790
|
+
let pushed = 0;
|
|
791
|
+
try {
|
|
792
|
+
for (const pack of packs) {
|
|
793
|
+
console.log(`\u2500\u2500 ${pack.name}${pack.setupUrl ? ` \u2192 ${pack.setupUrl}` : ""}`);
|
|
794
|
+
for (const tok of pack.tokens) {
|
|
795
|
+
const key = tok.envVar.toUpperCase();
|
|
796
|
+
if (key === "GITHUB_TOKEN") {
|
|
797
|
+
console.log(" \xB7 GITHUB_TOKEN \u2014 provided automatically by Actions; skipping");
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
const pre = prefill?.get(key);
|
|
801
|
+
if (pre) {
|
|
802
|
+
setGitHubSecret(repo, env, key, pre);
|
|
803
|
+
console.log(` \u2714 ${existing?.has(key) ? "overrode" : "pushed"} ${key} \u2190 OCI config`);
|
|
804
|
+
pushed++;
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
if (tok.scopes?.length) console.log(` scopes: ${tok.scopes.join(", ")}`);
|
|
808
|
+
if (tok.setupUrl) console.log(` link: ${tok.setupUrl}`);
|
|
809
|
+
const state = existing ? existing.has(key) ? " [already set]" : " [not set]" : "";
|
|
810
|
+
const value = await prompt.ask(` ${key} \u2014 ${tok.label}${state}
|
|
811
|
+
value: `);
|
|
812
|
+
if (!value) {
|
|
813
|
+
console.log(existing?.has(key) ? " \xB7 kept existing" : " \xB7 skipped");
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
if (tok.verify) {
|
|
817
|
+
const check = await tok.verify(value, {}).catch((e) => ({ ok: false, detail: e instanceof Error ? e.message : String(e) }));
|
|
818
|
+
if (!check.ok) {
|
|
819
|
+
console.log(
|
|
820
|
+
` \u2716 verify failed${check.detail ? ` (${check.detail})` : ""} \u2014 not pushed`
|
|
821
|
+
);
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
console.log(" \u2714 verified");
|
|
825
|
+
}
|
|
826
|
+
setGitHubSecret(repo, env, key, value);
|
|
827
|
+
const verb = existing?.has(key) ? "overrode" : "pushed";
|
|
828
|
+
console.log(` \u2714 ${verb} ${key} \u2192 ${repo}`);
|
|
829
|
+
pushed++;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
} finally {
|
|
833
|
+
prompt.close();
|
|
834
|
+
}
|
|
835
|
+
console.log(`
|
|
836
|
+
${pushed} secret(s) pushed to ${repo}. (None written to disk.)`);
|
|
837
|
+
}
|
|
672
838
|
async function secretsCommand(args) {
|
|
673
|
-
|
|
839
|
+
const sub = args[0];
|
|
840
|
+
if (sub === "gather") {
|
|
841
|
+
const name = args[1];
|
|
842
|
+
if (!name || name.startsWith("-")) {
|
|
843
|
+
throw new Error("usage: greenlight secrets gather <name> [--repo owner/repo] [--env <env>]");
|
|
844
|
+
}
|
|
845
|
+
const repo = flag(args, "--repo") ?? detectRepo(process.cwd());
|
|
846
|
+
if (!repo) throw new Error("could not determine the repo \u2014 pass --repo owner/repo");
|
|
847
|
+
const ociConfig2 = flag(args, "--oci-config");
|
|
848
|
+
const ociKey = flag(args, "--oci-key");
|
|
849
|
+
const prefill = ociConfig2 ? ociPrefill(resolve3(process.cwd(), ociConfig2), ociKey && resolve3(process.cwd(), ociKey)) : void 0;
|
|
850
|
+
await gatherSecrets(name, repo, flag(args, "--env"), prefill);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (sub !== "sync") {
|
|
674
854
|
console.log(
|
|
675
|
-
"usage
|
|
855
|
+
"usage:\n greenlight secrets sync [--repo owner/repo] [--env <env>] # push .greenlight/secrets.env\n greenlight secrets gather <name> [--repo owner/repo] [--env <env>] # guided, link-first, straight to GitHub (no disk/logs)\n [--oci-config <path>] [--oci-key <path>] # auto-fill OCI auth from the API-key config preview + .pem"
|
|
676
856
|
);
|
|
677
|
-
process.exit(
|
|
857
|
+
process.exit(sub ? 1 : 0);
|
|
678
858
|
}
|
|
679
859
|
const { count } = syncSecrets({
|
|
680
860
|
cwd: process.cwd(),
|
|
@@ -724,7 +904,7 @@ async function ensureTokensForTool(cwd, tool, opts = {}) {
|
|
|
724
904
|
const interactive = Boolean(process.stdin.isTTY);
|
|
725
905
|
const env = presentEnv(cwd);
|
|
726
906
|
const results = [];
|
|
727
|
-
const rl = interactive ?
|
|
907
|
+
const rl = interactive ? createInterface2({ input: process.stdin, output: process.stdout }) : null;
|
|
728
908
|
try {
|
|
729
909
|
for (const spec of tokensForTool(tool)) {
|
|
730
910
|
let value = env[spec.envVar];
|
|
@@ -1157,7 +1337,6 @@ jobs:
|
|
|
1157
1337
|
`;
|
|
1158
1338
|
}
|
|
1159
1339
|
function deployListenerYml(name, toolRepo) {
|
|
1160
|
-
const SECRET = `${name.toUpperCase().replace(/-/g, "_")}_OCI_CONTAINER_INSTANCE_OCID`;
|
|
1161
1340
|
return `name: greenlight-deploy-${name}
|
|
1162
1341
|
|
|
1163
1342
|
# Option B: ${toolRepo} fires repository_dispatch(deploy-${name}) after pushing a new image.
|
|
@@ -1179,12 +1358,13 @@ jobs:
|
|
|
1179
1358
|
- run: pip install --quiet oci-cli
|
|
1180
1359
|
- name: Deploy (restart container instance -> re-pull GHCR image)
|
|
1181
1360
|
env:
|
|
1182
|
-
|
|
1183
|
-
OCI_CLI_TENANCY: \${{ secrets.
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1361
|
+
# The OCI CLI reuses the SAME TF_VAR_OCI_* secrets the apply uses \u2014 one secret set.
|
|
1362
|
+
OCI_CLI_TENANCY: \${{ secrets.TF_VAR_OCI_TENANCY_OCID }}
|
|
1363
|
+
OCI_CLI_USER: \${{ secrets.TF_VAR_OCI_USER_OCID }}
|
|
1364
|
+
OCI_CLI_FINGERPRINT: \${{ secrets.TF_VAR_OCI_FINGERPRINT }}
|
|
1365
|
+
OCI_CLI_KEY_CONTENT: \${{ secrets.TF_VAR_OCI_PRIVATE_KEY }}
|
|
1366
|
+
OCI_CLI_REGION: \${{ secrets.TF_VAR_OCI_REGION }}
|
|
1367
|
+
OCI_CONTAINER_INSTANCE_OCID: \${{ secrets.OCI_CONTAINER_INSTANCE_OCID }}
|
|
1188
1368
|
run: pnpm exec greenlight deploy ${name} --env prod
|
|
1189
1369
|
- name: Report status back to ${toolRepo}
|
|
1190
1370
|
if: \${{ always() && github.event.client_payload.sha != '' }}
|
|
@@ -1306,8 +1486,9 @@ Next:
|
|
|
1306
1486
|
(in the wrapper) review infra/${name}.tf, then commit the submodule + infra + listener:
|
|
1307
1487
|
git add .gitmodules ${toolRel} infra/${name}.tf verify/${name}.config.ts greenlight.config.ts .github
|
|
1308
1488
|
git commit && git push # CI (infra.yml) applies. Tool's CI builds; wrapper deploys.${target === "oci" ? `
|
|
1309
|
-
Secrets:
|
|
1310
|
-
|
|
1489
|
+
Secrets (guided): greenlight secrets gather ${name} --repo <wrapper> # TF_VAR_OCI_* + GREENLIGHT_STATUS_TOKEN
|
|
1490
|
+
greenlight secrets gather ${name} --repo ${slug} # GREENLIGHT_DISPATCH_TOKEN
|
|
1491
|
+
After apply, set OCI_CONTAINER_INSTANCE_OCID (the TF output) in the wrapper.` : ""}`);
|
|
1311
1492
|
}
|
|
1312
1493
|
async function adoptStandalone(ctx) {
|
|
1313
1494
|
const { name, repoArg, lane, target, data, auth, envs, domain, reg, regPath } = ctx;
|
|
@@ -1613,7 +1794,7 @@ ${failed === 0 ? "\u2714 no failures" : `\u2718 ${failed} failure(s)`}`);
|
|
|
1613
1794
|
// src/commands/init.ts
|
|
1614
1795
|
import { existsSync as existsSync9, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
1615
1796
|
import { resolve as resolve8 } from "path";
|
|
1616
|
-
import { createInterface as
|
|
1797
|
+
import { createInterface as createInterface3 } from "readline/promises";
|
|
1617
1798
|
function flag5(args, name) {
|
|
1618
1799
|
const i = args.indexOf(name);
|
|
1619
1800
|
return i >= 0 ? args[i + 1] : void 0;
|
|
@@ -1630,7 +1811,7 @@ async function initCommand(args) {
|
|
|
1630
1811
|
let domain = flag5(args, "--domain");
|
|
1631
1812
|
if (!domain) {
|
|
1632
1813
|
if (!process.stdin.isTTY) throw new Error("init needs --domain <domain> (no TTY for prompts)");
|
|
1633
|
-
const rl =
|
|
1814
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
1634
1815
|
domain = (await rl.question("Domain (e.g. example.dev): ")).trim();
|
|
1635
1816
|
rl.close();
|
|
1636
1817
|
}
|
|
@@ -1901,6 +2082,7 @@ var HELP = `greenlight <command>
|
|
|
1901
2082
|
preview <name> [--port <n>] build + serve locally + verify (one command)
|
|
1902
2083
|
verify <name> [--env <env> | --url <url>] run the verify harness against the URL
|
|
1903
2084
|
promote <name> [--perform] [--push] gated develop -> main fast-forward
|
|
2085
|
+
secrets gather <name> [--repo o/r] [--env e] guided, link-first token prompts -> GitHub secrets (no disk/logs)
|
|
1904
2086
|
secrets sync [--repo o/r] [--env <env>] push .greenlight/secrets.env -> GitHub Actions secrets
|
|
1905
2087
|
agent sync write the loop skill + CLAUDE.md block into this repo
|
|
1906
2088
|
adopt <name> --repo <path> --lane --target onboard an existing tool repo as a thin consumer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rtrentjones/greenlight",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
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",
|