rastack 0.0.38 → 0.0.40
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/CHANGELOG.md +9 -0
- package/dist/deploy/config.d.ts +18 -0
- package/dist/deploy/config.js +49 -0
- package/dist/deploy/io.d.ts +24 -1
- package/dist/deploy/io.js +74 -1
- package/dist/rastack-bootstrap.d.ts +24 -0
- package/dist/rastack-bootstrap.js +222 -0
- package/dist/rastack-init.d.ts +1 -1
- package/dist/rastack-init.js +70 -20
- package/dist/rastack.d.ts +1 -0
- package/dist/rastack.js +7 -0
- package/package.json +1 -1
- package/src/deploy/config.ts +56 -0
- package/src/deploy/io.ts +74 -1
- package/src/rastack-bootstrap.ts +299 -0
- package/src/rastack-init.ts +95 -27
- package/src/rastack.ts +7 -0
- package/test/deploy.spec.ts +53 -0
package/src/rastack-init.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* rastack init [--app <slug>] [--owner <o>] [--repo <r>] [--branch <b>]
|
|
8
8
|
* [--region <r>] [--prefix <p>] [--namespace <n>]
|
|
9
|
-
* [--force] [--no-workflows] [--no-deploy] [--print]
|
|
9
|
+
* [--force] [--no-workflows] [--no-deploy] [--print]
|
|
10
10
|
*
|
|
11
11
|
* This is the one command that takes you from nothing to deployed. It:
|
|
12
12
|
* 1. writes `.rastack/deploy.json` + `.github/workflows/{ci,deploy-backend}.yml`
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
bootstrapStackName,
|
|
39
39
|
deployStackArgs,
|
|
40
40
|
deployStackCommand,
|
|
41
|
+
ghVariableSetArgs,
|
|
41
42
|
mergeConfig,
|
|
42
43
|
normalizeConfig,
|
|
43
44
|
parseGitHubRemote,
|
|
@@ -50,9 +51,13 @@ import {
|
|
|
50
51
|
awsAvailable,
|
|
51
52
|
awsRun,
|
|
52
53
|
canPrompt,
|
|
54
|
+
ghAuthLogin,
|
|
55
|
+
ghAuthenticated,
|
|
56
|
+
ghAvailable,
|
|
53
57
|
gitRemoteUrl,
|
|
54
58
|
loadConfig,
|
|
55
59
|
saveConfig,
|
|
60
|
+
setRepoVariable,
|
|
56
61
|
stackOutput,
|
|
57
62
|
uploadPlaceholder,
|
|
58
63
|
withPrompt,
|
|
@@ -64,7 +69,6 @@ interface Flags {
|
|
|
64
69
|
noWorkflows: boolean;
|
|
65
70
|
noDeploy: boolean;
|
|
66
71
|
print: boolean;
|
|
67
|
-
nonInteractive: boolean;
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
function parseFlags(argv: string[]): Flags {
|
|
@@ -73,7 +77,6 @@ function parseFlags(argv: string[]): Flags {
|
|
|
73
77
|
let noWorkflows = false;
|
|
74
78
|
let noDeploy = false;
|
|
75
79
|
let print = false;
|
|
76
|
-
let nonInteractive = false;
|
|
77
80
|
for (let i = 0; i < argv.length; i++) {
|
|
78
81
|
const a = argv[i];
|
|
79
82
|
const next = () => argv[++i] ?? "";
|
|
@@ -112,11 +115,6 @@ function parseFlags(argv: string[]): Flags {
|
|
|
112
115
|
case "--dry-run":
|
|
113
116
|
print = true;
|
|
114
117
|
break;
|
|
115
|
-
case "--yes":
|
|
116
|
-
case "-y":
|
|
117
|
-
case "--non-interactive":
|
|
118
|
-
nonInteractive = true;
|
|
119
|
-
break;
|
|
120
118
|
default:
|
|
121
119
|
if (a.startsWith("-")) {
|
|
122
120
|
console.error(`Unknown flag: ${a}`);
|
|
@@ -124,7 +122,7 @@ function parseFlags(argv: string[]): Flags {
|
|
|
124
122
|
}
|
|
125
123
|
}
|
|
126
124
|
}
|
|
127
|
-
return { patch, force, noWorkflows, noDeploy, print
|
|
125
|
+
return { patch, force, noWorkflows, noDeploy, print };
|
|
128
126
|
}
|
|
129
127
|
|
|
130
128
|
/** Seed a config: existing file → flags → git remote → defaults. */
|
|
@@ -236,7 +234,10 @@ function printPlan(cfg: DeployConfig): void {
|
|
|
236
234
|
* upload the Lambda placeholder, deploy app.yaml, and read the Outputs back into
|
|
237
235
|
* the config. Returns the config enriched with the resolved buckets + outputs.
|
|
238
236
|
*/
|
|
239
|
-
async function standUp(
|
|
237
|
+
async function standUp(
|
|
238
|
+
cfg: DeployConfig,
|
|
239
|
+
interactive: boolean,
|
|
240
|
+
): Promise<DeployConfig> {
|
|
240
241
|
if (!awsAvailable()) {
|
|
241
242
|
console.error(
|
|
242
243
|
" ✗ the AWS CLI is not on PATH. Install it and configure credentials,\n" +
|
|
@@ -308,9 +309,67 @@ async function standUp(cfg: DeployConfig): Promise<DeployConfig> {
|
|
|
308
309
|
const outputs = readAppOutputs(cfg);
|
|
309
310
|
cfg = mergeConfig(cfg, { outputs });
|
|
310
311
|
saveConfig(cfg);
|
|
312
|
+
|
|
313
|
+
// 5. Connect the GitHub CLI and wire the repo Variables CI reads, so the FIRST
|
|
314
|
+
// push actually ships. Without this the deploy-backend workflow's `if:
|
|
315
|
+
// vars.AWS_DEPLOY_ROLE_ARN != ''` guard silently skips every run.
|
|
316
|
+
wireRepoVariables(cfg, interactive);
|
|
311
317
|
return cfg;
|
|
312
318
|
}
|
|
313
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Set the `AWS_DEPLOY_ROLE_ARN` / `AWS_REGION` repo Variables via `gh` so the
|
|
322
|
+
* GitHub → Lambda pipeline is live the moment `init` finishes. Connects `gh` to
|
|
323
|
+
* GitHub first — running `gh auth login` when it is installed but not yet
|
|
324
|
+
* authenticated (on a TTY) — so a fresh machine goes from nothing to a working
|
|
325
|
+
* pipeline in one command. Best-effort throughout: no `gh`, a declined login, or
|
|
326
|
+
* a value the stack Outputs did not yield falls back to the printed instructions
|
|
327
|
+
* in `printSummary`. Records the names it wired so the summary reports what is
|
|
328
|
+
* (and isn't) done.
|
|
329
|
+
*/
|
|
330
|
+
const wiredVariables = new Set<string>();
|
|
331
|
+
function wireRepoVariables(cfg: DeployConfig, interactive: boolean): void {
|
|
332
|
+
const vars = repoVariables(cfg).filter((v) => v.value);
|
|
333
|
+
if (!vars.length) return;
|
|
334
|
+
|
|
335
|
+
if (!ghAvailable()) {
|
|
336
|
+
console.log(
|
|
337
|
+
"\n▸ GitHub CLI (`gh`) not found — skipping repo Variable wiring.\n" +
|
|
338
|
+
" Install `gh` (https://cli.github.com) and re-run, or wire the\n" +
|
|
339
|
+
" Variables below by hand so pushes deploy.",
|
|
340
|
+
);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Connect `gh` to GitHub if it isn't already, so it can set the Variables.
|
|
345
|
+
if (!ghAuthenticated()) {
|
|
346
|
+
if (interactive) {
|
|
347
|
+
console.log(
|
|
348
|
+
"\n▸ Connecting the GitHub CLI to your account (`gh auth login`)",
|
|
349
|
+
);
|
|
350
|
+
ghAuthLogin();
|
|
351
|
+
}
|
|
352
|
+
if (!ghAuthenticated()) {
|
|
353
|
+
console.log(
|
|
354
|
+
"\n▸ GitHub CLI is not connected — skipping repo Variable wiring.\n" +
|
|
355
|
+
" Run `gh auth login`, then wire the Variables below by hand.",
|
|
356
|
+
);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
console.log("\n▸ Wiring the GitHub repo Variables CI reads (via `gh`)");
|
|
362
|
+
for (const v of vars) {
|
|
363
|
+
const ok = setRepoVariable(ghVariableSetArgs(cfg, v.name, v.value));
|
|
364
|
+
if (ok) {
|
|
365
|
+
wiredVariables.add(v.name);
|
|
366
|
+
console.log(` ✓ set ${v.name}`);
|
|
367
|
+
} else {
|
|
368
|
+
console.log(` • could not set ${v.name} — wire it by hand (below)`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
314
373
|
/** Deploy the bootstrap stack, retrying once against an existing OIDC provider. */
|
|
315
374
|
function deployBootstrap(cfg: DeployConfig): void {
|
|
316
375
|
try {
|
|
@@ -338,19 +397,28 @@ function printSummary(cfg: DeployConfig): void {
|
|
|
338
397
|
`\n ✓ ${cfg.app} is deployed (${cfg.stackName} @ ${cfg.region})`,
|
|
339
398
|
);
|
|
340
399
|
if (cfg.outputs?.apiUrl) console.log(` API: ${cfg.outputs.apiUrl}`);
|
|
400
|
+
|
|
401
|
+
// Only the Variables `init` could NOT wire itself remain a manual step.
|
|
402
|
+
const pending = repoVariables(cfg).filter((v) => !wiredVariables.has(v.name));
|
|
403
|
+
if (pending.length) {
|
|
404
|
+
console.log(
|
|
405
|
+
"\n Wire these GitHub repo Variables so CI can ship code (Settings → Variables):",
|
|
406
|
+
);
|
|
407
|
+
console.log(
|
|
408
|
+
table(
|
|
409
|
+
pending.map((v) => ({
|
|
410
|
+
key: v.name,
|
|
411
|
+
value: v.value || `<${v.from}>`,
|
|
412
|
+
})),
|
|
413
|
+
),
|
|
414
|
+
);
|
|
415
|
+
} else {
|
|
416
|
+
console.log(
|
|
417
|
+
"\n ✓ wired the GitHub repo Variables CI reads (AWS_DEPLOY_ROLE_ARN, AWS_REGION).",
|
|
418
|
+
);
|
|
419
|
+
}
|
|
341
420
|
console.log(
|
|
342
|
-
|
|
343
|
-
);
|
|
344
|
-
console.log(
|
|
345
|
-
table(
|
|
346
|
-
repoVariables(cfg).map((v) => ({
|
|
347
|
-
key: v.name,
|
|
348
|
-
value: v.value || `<${v.from}>`,
|
|
349
|
-
})),
|
|
350
|
-
),
|
|
351
|
-
);
|
|
352
|
-
console.log(
|
|
353
|
-
`\n Then every push to ${cfg.github.branch} ships code via \`rastack ci deploy\`` +
|
|
421
|
+
`\n Every push to ${cfg.github.branch} now ships code via \`rastack ci deploy\`` +
|
|
354
422
|
` (also runnable locally as \`rastack deploy\`).\n`,
|
|
355
423
|
);
|
|
356
424
|
}
|
|
@@ -359,7 +427,7 @@ async function main(argv: string[]): Promise<void> {
|
|
|
359
427
|
const flags = parseFlags(argv);
|
|
360
428
|
let cfg = seedConfig(flags);
|
|
361
429
|
|
|
362
|
-
if (canPrompt(
|
|
430
|
+
if (canPrompt()) {
|
|
363
431
|
console.log(
|
|
364
432
|
"\n rastack init — scaffold the deploy config + stand the backend up\n",
|
|
365
433
|
);
|
|
@@ -368,9 +436,9 @@ async function main(argv: string[]): Promise<void> {
|
|
|
368
436
|
|
|
369
437
|
const errors = validateConfig(cfg);
|
|
370
438
|
// Any deploy needs a complete config; a still-incomplete one is fatal unless we
|
|
371
|
-
// are only scaffolding files.
|
|
439
|
+
// are only scaffolding files, and always fatal off a TTY (no prompt to fix it).
|
|
372
440
|
if (errors.length) {
|
|
373
|
-
const fatal = !flags.noDeploy ||
|
|
441
|
+
const fatal = !flags.noDeploy || !canPrompt();
|
|
374
442
|
const label = fatal ? "✗" : "⚠";
|
|
375
443
|
for (const e of errors) console.error(` ${label} ${e}`);
|
|
376
444
|
if (fatal) {
|
|
@@ -413,7 +481,7 @@ async function main(argv: string[]): Promise<void> {
|
|
|
413
481
|
return;
|
|
414
482
|
}
|
|
415
483
|
|
|
416
|
-
if (canPrompt(
|
|
484
|
+
if (canPrompt()) {
|
|
417
485
|
const ok = await confirm(
|
|
418
486
|
`Deploy ${cfg.stackName} to AWS ${cfg.region} now? (creates the bootstrap stack if needed) [y/N]`,
|
|
419
487
|
);
|
|
@@ -425,7 +493,7 @@ async function main(argv: string[]): Promise<void> {
|
|
|
425
493
|
}
|
|
426
494
|
}
|
|
427
495
|
|
|
428
|
-
cfg = await standUp(cfg);
|
|
496
|
+
cfg = await standUp(cfg, canPrompt());
|
|
429
497
|
printSummary(cfg);
|
|
430
498
|
}
|
|
431
499
|
|
package/src/rastack.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* rastack wasm Compile the API to WebAssembly (in-browser local dev)
|
|
17
17
|
* rastack scan [files...] [--fail-on-pii] Scan CSVs for PII and schema info
|
|
18
18
|
* rastack seed [--env dev] [--source gs://] Load CSVs into Iceberg namespace
|
|
19
|
+
* rastack bootstrap [--owner o] [--repo r] Stand the tier-0 AWS account stack up (once per account+region)
|
|
19
20
|
* rastack init [--app slug] [--force] Scaffold config + GitHub Actions AND stand the backend up
|
|
20
21
|
* rastack deploy [--plan] Ship code to the Lambda (alias for `rastack ci deploy`)
|
|
21
22
|
* rastack ci <check|build|deploy> [--plan] Run the CI pipeline (keeps Actions a one-liner)
|
|
@@ -118,6 +119,11 @@ switch (command) {
|
|
|
118
119
|
case "seed":
|
|
119
120
|
run("seed.js");
|
|
120
121
|
break;
|
|
122
|
+
case "bootstrap":
|
|
123
|
+
// Stand the tier-0 bootstrap stack up (buckets + GitHub OIDC) for an AWS
|
|
124
|
+
// account — the once-per-account+region step, decoupled from any app.
|
|
125
|
+
run("rastack-bootstrap.js", rest);
|
|
126
|
+
break;
|
|
121
127
|
case "init":
|
|
122
128
|
// Scaffold config + GitHub Actions and stand the serverless backend up.
|
|
123
129
|
run("rastack-init.js", rest);
|
|
@@ -154,6 +160,7 @@ switch (command) {
|
|
|
154
160
|
` rastack wasm Compile the API to WebAssembly (browser local dev)\n` +
|
|
155
161
|
` rastack scan [files...] [--fail-on-pii]\n` +
|
|
156
162
|
` rastack seed [--env dev|staging] [--source gs://bucket/ | ./myapp-dev/]\n` +
|
|
163
|
+
` rastack bootstrap [--owner o] [--repo r] [--region r] [--prefix p] [--reuse-oidc] [--print] Stand the AWS account stack up\n` +
|
|
157
164
|
` rastack init [--app slug] [--owner o] [--repo r] [--print] [--no-deploy] Scaffold + stand the backend up\n` +
|
|
158
165
|
` rastack deploy [--plan] Ship code to the Lambda (alias for ci deploy)\n` +
|
|
159
166
|
` rastack ci <check|build|deploy> [--plan] Run the CI pipeline (cleaner Actions)\n` +
|
package/test/deploy.spec.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
bootstrapStackParameters,
|
|
14
14
|
cognitoAdvancedSecuritySupported,
|
|
15
15
|
APP_STACK_OUTPUTS,
|
|
16
|
+
ghVariableSetArgs,
|
|
16
17
|
lambdaEnv,
|
|
17
18
|
mergeConfig,
|
|
18
19
|
normalizeConfig,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
scaffoldFiles,
|
|
22
23
|
stackParameters,
|
|
23
24
|
validateConfig,
|
|
25
|
+
validateBootstrapConfig,
|
|
24
26
|
zipStore,
|
|
25
27
|
renderCiWorkflow,
|
|
26
28
|
renderDeployWorkflow,
|
|
@@ -278,6 +280,31 @@ describe("bootstrap stack", () => {
|
|
|
278
280
|
expect(cmd).toContain("--stack-name rastack-bootstrap");
|
|
279
281
|
expect(`aws ${bootstrapStackArgs(base()).join(" ")}`).toBe(cmd);
|
|
280
282
|
});
|
|
283
|
+
|
|
284
|
+
it("validates only the account-level fields, not the per-app ones", () => {
|
|
285
|
+
// A config with no `app` is fine for bootstrap — it never touches app.yaml.
|
|
286
|
+
const cfg = normalizeConfig({
|
|
287
|
+
github: { owner: "theserverkid", repo: "reactapistack-demo" },
|
|
288
|
+
});
|
|
289
|
+
expect(validateConfig(cfg)).toContain(
|
|
290
|
+
"app is required (a short slug, e.g. `airline`).",
|
|
291
|
+
);
|
|
292
|
+
expect(validateBootstrapConfig(cfg)).toEqual([]);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("still flags a bad prefix, region, or GitHub coordinates", () => {
|
|
296
|
+
expect(
|
|
297
|
+
validateBootstrapConfig(base({ namePrefix: "Bad_Prefix" })),
|
|
298
|
+
).toContainEqual(expect.stringContaining("namePrefix"));
|
|
299
|
+
expect(
|
|
300
|
+
validateBootstrapConfig(base({ region: "not-a-region" })),
|
|
301
|
+
).toContainEqual(expect.stringContaining("region"));
|
|
302
|
+
expect(
|
|
303
|
+
validateBootstrapConfig(
|
|
304
|
+
normalizeConfig({ app: "airline", github: { repo: "demo" } }),
|
|
305
|
+
),
|
|
306
|
+
).toContain("github.owner is required.");
|
|
307
|
+
});
|
|
281
308
|
});
|
|
282
309
|
|
|
283
310
|
describe("app stack outputs map", () => {
|
|
@@ -319,6 +346,32 @@ describe("repo variables + lambda env", () => {
|
|
|
319
346
|
expect(vars.find((v) => v.name === "AWS_REGION")!.value).toBe("af-south-1");
|
|
320
347
|
});
|
|
321
348
|
|
|
349
|
+
it("builds the `gh variable set` argv init wires the pipeline with", () => {
|
|
350
|
+
const cfg = base({ outputs: { deployRoleArn: "arn:aws:iam::1:role/x" } });
|
|
351
|
+
const roleVar = repoVariables(cfg).find(
|
|
352
|
+
(v) => v.name === "AWS_DEPLOY_ROLE_ARN",
|
|
353
|
+
)!;
|
|
354
|
+
expect(ghVariableSetArgs(cfg, roleVar.name, roleVar.value)).toEqual([
|
|
355
|
+
"variable",
|
|
356
|
+
"set",
|
|
357
|
+
"AWS_DEPLOY_ROLE_ARN",
|
|
358
|
+
"--repo",
|
|
359
|
+
"theserverkid/reactapistack-demo",
|
|
360
|
+
"--body",
|
|
361
|
+
"arn:aws:iam::1:role/x",
|
|
362
|
+
]);
|
|
363
|
+
// Region variable targets the same repo with the region value.
|
|
364
|
+
expect(ghVariableSetArgs(cfg, "AWS_REGION", cfg.region)).toEqual([
|
|
365
|
+
"variable",
|
|
366
|
+
"set",
|
|
367
|
+
"AWS_REGION",
|
|
368
|
+
"--repo",
|
|
369
|
+
"theserverkid/reactapistack-demo",
|
|
370
|
+
"--body",
|
|
371
|
+
"af-south-1",
|
|
372
|
+
]);
|
|
373
|
+
});
|
|
374
|
+
|
|
322
375
|
it("fills the RASTACK_* env, using outputs where available", () => {
|
|
323
376
|
const cfg = base({
|
|
324
377
|
outputs: {
|