rastack 0.0.62 → 0.0.63
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 +7 -0
- package/auth/provider.ts +71 -8
- package/dist/admin.js +9 -9
- package/dist/deploy/ci.d.ts +10 -3
- package/dist/deploy/ci.js +22 -11
- package/dist/deploy/config.d.ts +155 -5
- package/dist/deploy/config.js +216 -6
- package/dist/deploy/io.d.ts +7 -0
- package/dist/deploy/io.js +17 -0
- package/dist/deploy/workflows.d.ts +30 -0
- package/dist/deploy/workflows.js +117 -12
- package/dist/rastack-ci.js +19 -1
- package/dist/rastack-dev.js +48 -2
- package/dist/rastack-init.js +190 -64
- package/dist/rastack.js +1 -0
- package/dist/templates/cloudformation/app.yaml +133 -2
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/package.json +1 -1
- package/src/deploy/ci.ts +50 -16
- package/src/deploy/config.ts +340 -7
- package/src/deploy/io.ts +17 -1
- package/src/deploy/workflows.ts +122 -12
- package/src/rastack-ci.ts +20 -2
- package/src/rastack-dev.ts +61 -6
- package/src/rastack-init.ts +235 -76
- package/src/rastack.ts +1 -0
package/dist/rastack-init.js
CHANGED
|
@@ -63,12 +63,23 @@ const path = __importStar(require("path"));
|
|
|
63
63
|
const config_1 = require("./deploy/config");
|
|
64
64
|
const workflows_1 = require("./deploy/workflows");
|
|
65
65
|
const io_1 = require("./deploy/io");
|
|
66
|
+
function parseEnvFlag(value) {
|
|
67
|
+
const v = value.trim().toLowerCase();
|
|
68
|
+
if (v === "all" || v === "")
|
|
69
|
+
return [...config_1.ENV_NAMES];
|
|
70
|
+
if (config_1.ENV_NAMES.includes(v))
|
|
71
|
+
return [v];
|
|
72
|
+
console.error(`Unknown --env: expected dev | prod | all.`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
66
75
|
function parseFlags(argv) {
|
|
67
|
-
const patch = { github: {} };
|
|
76
|
+
const patch = { github: {}, environments: {} };
|
|
77
|
+
let envs = [...config_1.ENV_NAMES];
|
|
68
78
|
let force = false;
|
|
69
79
|
let noWorkflows = false;
|
|
70
80
|
let noDeploy = false;
|
|
71
81
|
let print = false;
|
|
82
|
+
const devPatch = () => { var _a; return ((_a = patch.environments).dev ?? (_a.dev = {})); };
|
|
72
83
|
for (let i = 0; i < argv.length; i++) {
|
|
73
84
|
const a = argv[i];
|
|
74
85
|
const next = () => argv[++i] ?? "";
|
|
@@ -85,6 +96,17 @@ function parseFlags(argv) {
|
|
|
85
96
|
case "--branch":
|
|
86
97
|
patch.github.branch = next();
|
|
87
98
|
break;
|
|
99
|
+
case "--dev-branch":
|
|
100
|
+
devPatch().branch = next();
|
|
101
|
+
break;
|
|
102
|
+
case "--dev-proxy":
|
|
103
|
+
case "--dev-proxy-upstream":
|
|
104
|
+
// The dev external-API proxy upstream (a UAT/prod backend URL).
|
|
105
|
+
devPatch().proxyUpstream = next();
|
|
106
|
+
break;
|
|
107
|
+
case "--env":
|
|
108
|
+
envs = parseEnvFlag(next());
|
|
109
|
+
break;
|
|
88
110
|
case "--region":
|
|
89
111
|
patch.region = next();
|
|
90
112
|
break;
|
|
@@ -114,7 +136,10 @@ function parseFlags(argv) {
|
|
|
114
136
|
}
|
|
115
137
|
}
|
|
116
138
|
}
|
|
117
|
-
|
|
139
|
+
// Drop the scratch environments container when nothing was set on it.
|
|
140
|
+
if (patch.environments && !patch.environments.dev)
|
|
141
|
+
delete patch.environments;
|
|
142
|
+
return { patch, envs, force, noWorkflows, noDeploy, print };
|
|
118
143
|
}
|
|
119
144
|
/** Seed a config: existing file → flags → git remote → defaults. */
|
|
120
145
|
function seedConfig(flags) {
|
|
@@ -151,6 +176,35 @@ async function promptMissing(cfg) {
|
|
|
151
176
|
});
|
|
152
177
|
});
|
|
153
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Ensure `.env` is git-ignored and (re)write the generated `.env` with the
|
|
181
|
+
* current config, preserving any secrets a developer already added. Returns the
|
|
182
|
+
* repo-relative paths touched, for the written/skipped summary.
|
|
183
|
+
*/
|
|
184
|
+
function writeEnvAndGitignore(cfg) {
|
|
185
|
+
const touched = [];
|
|
186
|
+
// 1. git-ignore `.env` (append-merge; never duplicate or clobber).
|
|
187
|
+
const giAbs = path.join(process.cwd(), workflows_1.GITIGNORE_PATH);
|
|
188
|
+
const existingGi = fs.existsSync(giAbs)
|
|
189
|
+
? fs.readFileSync(giAbs, "utf-8")
|
|
190
|
+
: "";
|
|
191
|
+
const nextGi = (0, workflows_1.gitignoreWith)(existingGi, workflows_1.ENV_PATH);
|
|
192
|
+
if (nextGi !== existingGi) {
|
|
193
|
+
fs.writeFileSync(giAbs, nextGi);
|
|
194
|
+
touched.push(workflows_1.GITIGNORE_PATH);
|
|
195
|
+
}
|
|
196
|
+
// 2. Upsert the generated RASTACK_* values into `.env` (secrets preserved).
|
|
197
|
+
const envAbs = path.join(process.cwd(), workflows_1.ENV_PATH);
|
|
198
|
+
const existingEnv = fs.existsSync(envAbs)
|
|
199
|
+
? fs.readFileSync(envAbs, "utf-8")
|
|
200
|
+
: "";
|
|
201
|
+
const nextEnv = (0, workflows_1.applyEnvValues)(existingEnv, cfg);
|
|
202
|
+
if (nextEnv !== existingEnv) {
|
|
203
|
+
fs.writeFileSync(envAbs, nextEnv);
|
|
204
|
+
touched.push(workflows_1.ENV_PATH);
|
|
205
|
+
}
|
|
206
|
+
return touched;
|
|
207
|
+
}
|
|
154
208
|
/** Yes/no confirmation with a default of "no" — gates the cloud-mutating steps. */
|
|
155
209
|
async function confirm(question) {
|
|
156
210
|
return (0, io_1.withPrompt)(async (rl) => {
|
|
@@ -164,24 +218,36 @@ function table(rows) {
|
|
|
164
218
|
return rows.map((r) => ` ${r.key.padEnd(width)} ${r.value}`).join("\n");
|
|
165
219
|
}
|
|
166
220
|
/**
|
|
167
|
-
* Read
|
|
168
|
-
* not resolvable yet (best-effort — a freshly created stack has them all).
|
|
221
|
+
* Read one environment's app.yaml stack Outputs into a `DeployOutputs`, skipping
|
|
222
|
+
* any not resolvable yet (best-effort — a freshly created stack has them all).
|
|
169
223
|
*/
|
|
170
|
-
function
|
|
224
|
+
function readEnvOutputs(cfg, env) {
|
|
225
|
+
const stackName = (0, config_1.resolveEnvironment)(cfg, env).stackName;
|
|
171
226
|
const outputs = {};
|
|
172
227
|
for (const { key, field } of config_1.APP_STACK_OUTPUTS) {
|
|
173
|
-
const value = (0, io_1.stackOutput)(
|
|
228
|
+
const value = (0, io_1.stackOutput)(stackName, key, cfg.region);
|
|
174
229
|
if (value)
|
|
175
230
|
outputs[field] = value;
|
|
176
231
|
}
|
|
177
232
|
return outputs;
|
|
178
233
|
}
|
|
234
|
+
/** Merge an environment's resolved outputs back into the config in the right slot. */
|
|
235
|
+
function withEnvOutputs(cfg, env, outputs) {
|
|
236
|
+
return env === "prod"
|
|
237
|
+
? (0, config_1.mergeConfig)(cfg, { outputs })
|
|
238
|
+
: (0, config_1.mergeConfig)(cfg, { environments: { dev: { outputs } } });
|
|
239
|
+
}
|
|
240
|
+
/** Human label for an environment ("dev" / "prod") plus its branch. */
|
|
241
|
+
function envLabel(cfg, env) {
|
|
242
|
+
const e = (0, config_1.resolveEnvironment)(cfg, env);
|
|
243
|
+
return `${env} (${e.branch} → ${e.ghEnvironment})`;
|
|
244
|
+
}
|
|
179
245
|
/**
|
|
180
246
|
* Print the exact commands `init` would run, without touching the cloud
|
|
181
247
|
* (`--print`). The artifacts bucket is read from the bootstrap stack when the
|
|
182
248
|
* AWS CLI is available, else shown as a placeholder.
|
|
183
249
|
*/
|
|
184
|
-
function printPlan(cfg) {
|
|
250
|
+
function printPlan(cfg, envs) {
|
|
185
251
|
const bucket = (0, io_1.awsAvailable)()
|
|
186
252
|
? (0, io_1.stackOutput)((0, config_1.bootstrapStackName)(cfg), config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region)
|
|
187
253
|
: null;
|
|
@@ -189,23 +255,32 @@ function printPlan(cfg) {
|
|
|
189
255
|
artifactsBucket: bucket ?? "<ArtifactsBucketName from bootstrap stack>",
|
|
190
256
|
});
|
|
191
257
|
const appTemplate = (0, io_1.resolveDeployAsset)("app.yaml") ?? undefined;
|
|
192
|
-
console.log(`\n Deployment plan (${
|
|
258
|
+
console.log(`\n Deployment plan (${envs.join(" + ")} @ ${cfg.region})\n`);
|
|
193
259
|
console.log(` ── Prerequisite: the tier-0 bootstrap stack (${(0, config_1.bootstrapStackName)(cfg)}) must exist ──`);
|
|
194
260
|
console.log(` Create it once per account+region (init does not): rastack bootstrap --region ${cfg.region}\n`);
|
|
195
|
-
console.log(" ── 1. Per-app backend (app.yaml) —
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
261
|
+
console.log(" ── 1. Per-app backend (app.yaml) — one stack per environment ──");
|
|
262
|
+
for (const env of envs) {
|
|
263
|
+
const e = (0, config_1.resolveEnvironment)(withBucket, env);
|
|
264
|
+
console.log(`\n # ${envLabel(withBucket, env)} — stack ${e.stackName}`);
|
|
265
|
+
console.log(" " + (0, config_1.deployStackCommandForEnv)(withBucket, env, appTemplate));
|
|
266
|
+
}
|
|
267
|
+
console.log("\n ── 2. Wire each GitHub Environment's Variables from its stack Outputs ──");
|
|
268
|
+
for (const env of envs) {
|
|
269
|
+
const e = (0, config_1.resolveEnvironment)(cfg, env);
|
|
270
|
+
console.log(`\n # GitHub Environment: ${e.ghEnvironment}`);
|
|
271
|
+
console.log(table((0, config_1.repoVariablesForEnv)(cfg, env).map((v) => ({
|
|
272
|
+
key: v.name,
|
|
273
|
+
value: v.value || `<${v.from}>`,
|
|
274
|
+
}))));
|
|
275
|
+
}
|
|
276
|
+
console.log("");
|
|
202
277
|
}
|
|
203
278
|
/**
|
|
204
279
|
* Stand the infrastructure up: ensure the bootstrap stack + artifacts bucket,
|
|
205
280
|
* upload the Lambda placeholder, deploy app.yaml, and read the Outputs back into
|
|
206
281
|
* the config. Returns the config enriched with the resolved buckets + outputs.
|
|
207
282
|
*/
|
|
208
|
-
async function standUp(cfg, interactive) {
|
|
283
|
+
async function standUp(cfg, envs, interactive) {
|
|
209
284
|
if (!(0, io_1.awsAvailable)()) {
|
|
210
285
|
console.error(" ✗ the AWS CLI is not on PATH. Install it and configure credentials,\n" +
|
|
211
286
|
" or re-run with --print to see the commands, or --no-deploy to skip.");
|
|
@@ -229,13 +304,12 @@ async function standUp(cfg, interactive) {
|
|
|
229
304
|
process.exit(1);
|
|
230
305
|
}
|
|
231
306
|
console.log(` ✓ artifacts bucket: ${artifactsBucket}`);
|
|
232
|
-
// 2. Upload the placeholder the function is created from.
|
|
307
|
+
// 2. Upload the placeholder the function is created from (shared by all envs).
|
|
233
308
|
console.log("\n▸ Uploading the Lambda placeholder bundle");
|
|
234
309
|
if (!(0, io_1.uploadPlaceholder)(artifactsBucket, cfg.region)) {
|
|
235
310
|
console.error(" ✗ Lambda placeholder bundle not found in the rastack package — cannot create the function.");
|
|
236
311
|
process.exit(1);
|
|
237
312
|
}
|
|
238
|
-
// 3. Deploy the per-app backend with the resolved bucket wired in.
|
|
239
313
|
cfg = (0, config_1.mergeConfig)(cfg, {
|
|
240
314
|
artifactsBucket,
|
|
241
315
|
templatesBucket: templatesBucket ?? undefined,
|
|
@@ -246,22 +320,25 @@ async function standUp(cfg, interactive) {
|
|
|
246
320
|
" (`npm i -g rastack`) or run from the framework repo.");
|
|
247
321
|
process.exit(1);
|
|
248
322
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
323
|
+
// 3. Deploy one stack per environment — each with its own Cognito pool,
|
|
324
|
+
// warehouse, Lambda and API — reading the Outputs back into the config.
|
|
325
|
+
for (const env of envs) {
|
|
326
|
+
const stackName = (0, config_1.resolveEnvironment)(cfg, env).stackName;
|
|
327
|
+
console.log(`\n▸ Deploying ${envLabel(cfg, env)} — ${stackName}`);
|
|
328
|
+
try {
|
|
329
|
+
(0, io_1.awsRun)((0, config_1.deployStackArgsForEnv)(cfg, env, appTemplate));
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
console.error(`\n ✗ aws cloudformation deploy failed for ${stackName}.\n ${err.message}`);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
cfg = withEnvOutputs(cfg, env, readEnvOutputs(cfg, env));
|
|
336
|
+
(0, io_1.saveConfig)(cfg);
|
|
256
337
|
}
|
|
257
|
-
// 4.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
(
|
|
261
|
-
// 5. Connect the GitHub CLI and wire the repo Variables CI reads, so the FIRST
|
|
262
|
-
// push actually ships. Without this the deploy-backend workflow's `if:
|
|
263
|
-
// vars.AWS_DEPLOY_ROLE_ARN != ''` guard silently skips every run.
|
|
264
|
-
wireRepoVariables(cfg, interactive);
|
|
338
|
+
// 4. Connect the GitHub CLI and wire each environment's scoped Variables, so
|
|
339
|
+
// the FIRST push actually ships. Without this the deploy-backend workflow's
|
|
340
|
+
// `if: vars.AWS_DEPLOY_ROLE_ARN != ''` guard silently skips every run.
|
|
341
|
+
wireEnvironments(cfg, envs, interactive);
|
|
265
342
|
return cfg;
|
|
266
343
|
}
|
|
267
344
|
/**
|
|
@@ -275,58 +352,97 @@ async function standUp(cfg, interactive) {
|
|
|
275
352
|
* (and isn't) done.
|
|
276
353
|
*/
|
|
277
354
|
const wiredVariables = new Set();
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
355
|
+
/** Key a wired Variable by its GitHub Environment so dev/prod don't collide. */
|
|
356
|
+
function wiredKey(ghEnvironment, name) {
|
|
357
|
+
return `${ghEnvironment ?? ""}:${name}`;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Create each environment's GitHub Actions Environment and wire its scoped
|
|
361
|
+
* `AWS_DEPLOY_ROLE_ARN` / `AWS_REGION` Variables via `gh`, so the GitHub →
|
|
362
|
+
* Lambda pipeline is live for both dev and prod the moment `init` finishes.
|
|
363
|
+
*/
|
|
364
|
+
function wireEnvironments(cfg, envs, interactive) {
|
|
365
|
+
const anyValue = envs.some((env) => (0, config_1.repoVariablesForEnv)(cfg, env).some((v) => v.value));
|
|
366
|
+
if (!anyValue)
|
|
281
367
|
return;
|
|
282
368
|
if (!(0, io_1.ghAvailable)()) {
|
|
283
|
-
console.log("\n▸ GitHub CLI (`gh`) not found — skipping
|
|
369
|
+
console.log("\n▸ GitHub CLI (`gh`) not found — skipping environment + Variable wiring.\n" +
|
|
284
370
|
" Install `gh` (https://cli.github.com) and re-run, or wire the\n" +
|
|
285
371
|
" Variables below by hand so pushes deploy.");
|
|
286
372
|
return;
|
|
287
373
|
}
|
|
288
|
-
// Connect `gh` to GitHub if it isn't already, so it can
|
|
374
|
+
// Connect `gh` to GitHub if it isn't already, so it can create environments.
|
|
289
375
|
if (!(0, io_1.ghAuthenticated)()) {
|
|
290
376
|
if (interactive) {
|
|
291
377
|
console.log("\n▸ Connecting the GitHub CLI to your account (`gh auth login`)");
|
|
292
378
|
(0, io_1.ghAuthLogin)();
|
|
293
379
|
}
|
|
294
380
|
if (!(0, io_1.ghAuthenticated)()) {
|
|
295
|
-
console.log("\n▸ GitHub CLI is not connected — skipping
|
|
381
|
+
console.log("\n▸ GitHub CLI is not connected — skipping environment wiring.\n" +
|
|
296
382
|
" Run `gh auth login`, then wire the Variables below by hand.");
|
|
297
383
|
return;
|
|
298
384
|
}
|
|
299
385
|
}
|
|
300
|
-
console.log("\n▸
|
|
301
|
-
for (const
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
386
|
+
console.log("\n▸ Creating GitHub Environments + wiring their Variables (via `gh`)");
|
|
387
|
+
for (const env of envs) {
|
|
388
|
+
const e = (0, config_1.resolveEnvironment)(cfg, env);
|
|
389
|
+
// Create the Environment first — a Variable can only be scoped to one that
|
|
390
|
+
// already exists. Idempotent (PUT), so re-running init is safe.
|
|
391
|
+
if (!(0, io_1.ghEnvironmentCreate)((0, config_1.ghEnvironmentCreateArgs)(cfg, e.ghEnvironment))) {
|
|
392
|
+
console.log(` • could not create GitHub Environment ${e.ghEnvironment} — create it by hand`);
|
|
393
|
+
continue;
|
|
306
394
|
}
|
|
307
|
-
|
|
308
|
-
|
|
395
|
+
for (const v of (0, config_1.repoVariablesForEnv)(cfg, env).filter((x) => x.value)) {
|
|
396
|
+
const ok = (0, io_1.setRepoVariable)((0, config_1.ghVariableSetArgs)(cfg, v.name, v.value, v.ghEnvironment));
|
|
397
|
+
if (ok) {
|
|
398
|
+
wiredVariables.add(wiredKey(v.ghEnvironment, v.name));
|
|
399
|
+
console.log(` ✓ set ${e.ghEnvironment}/${v.name}`);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
console.log(` • could not set ${e.ghEnvironment}/${v.name} — wire it by hand (below)`);
|
|
403
|
+
}
|
|
309
404
|
}
|
|
310
405
|
}
|
|
311
406
|
}
|
|
312
|
-
function printSummary(cfg) {
|
|
313
|
-
console.log(`\n ✓ ${cfg.app} is deployed
|
|
314
|
-
|
|
315
|
-
|
|
407
|
+
function printSummary(cfg, envs) {
|
|
408
|
+
console.log(`\n ✓ ${cfg.app} is deployed @ ${cfg.region}`);
|
|
409
|
+
for (const env of envs) {
|
|
410
|
+
const e = (0, config_1.resolveEnvironment)(cfg, env);
|
|
411
|
+
console.log(`\n ${env} — ${e.stackName}`);
|
|
412
|
+
if (e.outputs?.apiUrl)
|
|
413
|
+
console.log(` API: ${e.outputs.apiUrl}`);
|
|
414
|
+
if (e.outputs?.hostedUiUrl)
|
|
415
|
+
console.log(` Hosted UI: ${e.outputs.hostedUiUrl}`);
|
|
416
|
+
if (e.outputs?.userPoolId)
|
|
417
|
+
console.log(` User Pool: ${e.outputs.userPoolId}`);
|
|
418
|
+
if (e.outputs?.proxyUrl)
|
|
419
|
+
console.log(` Proxy: ${e.outputs.proxyUrl}`);
|
|
420
|
+
}
|
|
316
421
|
// Only the Variables `init` could NOT wire itself remain a manual step.
|
|
317
|
-
const pending =
|
|
422
|
+
const pending = [];
|
|
423
|
+
for (const env of envs) {
|
|
424
|
+
const e = (0, config_1.resolveEnvironment)(cfg, env);
|
|
425
|
+
for (const v of (0, config_1.repoVariablesForEnv)(cfg, env)) {
|
|
426
|
+
if (!wiredVariables.has(wiredKey(v.ghEnvironment, v.name)))
|
|
427
|
+
pending.push({
|
|
428
|
+
key: `${e.ghEnvironment}/${v.name}`,
|
|
429
|
+
value: v.value || `<${v.from}>`,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
318
433
|
if (pending.length) {
|
|
319
|
-
console.log("\n Wire these GitHub
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
value: v.value || `<${v.from}>`,
|
|
323
|
-
}))));
|
|
434
|
+
console.log("\n Wire these per-environment GitHub Variables so CI can ship code\n" +
|
|
435
|
+
" (Settings → Environments → <env> → Variables):");
|
|
436
|
+
console.log(table(pending));
|
|
324
437
|
}
|
|
325
438
|
else {
|
|
326
|
-
console.log("\n ✓ wired
|
|
439
|
+
console.log("\n ✓ wired every GitHub Environment's Variables (AWS_DEPLOY_ROLE_ARN, AWS_REGION).");
|
|
327
440
|
}
|
|
328
|
-
|
|
329
|
-
|
|
441
|
+
const dev = (0, config_1.resolveEnvironment)(cfg, "dev");
|
|
442
|
+
const prod = (0, config_1.resolveEnvironment)(cfg, "prod");
|
|
443
|
+
console.log(`\n Pushes now ship code via \`rastack ci deploy\`:\n` +
|
|
444
|
+
` ${prod.branch} → prod (${prod.ghEnvironment}) ${dev.branch} → dev (${dev.ghEnvironment})\n` +
|
|
445
|
+
` Run one locally with \`rastack deploy --env dev\` / \`--env prod\`.\n`);
|
|
330
446
|
}
|
|
331
447
|
async function main(argv) {
|
|
332
448
|
const flags = parseFlags(argv);
|
|
@@ -362,6 +478,10 @@ async function main(argv) {
|
|
|
362
478
|
fs.writeFileSync(abs, f.content);
|
|
363
479
|
written.push(f.path);
|
|
364
480
|
}
|
|
481
|
+
// The `.env` (local app environment) + its `.gitignore` entry are standard —
|
|
482
|
+
// generated on every init, secrets preserved, refreshed with Outputs post-deploy.
|
|
483
|
+
for (const p of writeEnvAndGitignore(cfg))
|
|
484
|
+
written.push(p);
|
|
365
485
|
console.log("");
|
|
366
486
|
for (const w of written)
|
|
367
487
|
console.log(` ✓ wrote ${w}`);
|
|
@@ -369,7 +489,7 @@ async function main(argv) {
|
|
|
369
489
|
console.log(` • skipped ${s} (exists — pass --force to overwrite)`);
|
|
370
490
|
// 2. Deploy (unless opted out).
|
|
371
491
|
if (flags.print) {
|
|
372
|
-
printPlan(cfg);
|
|
492
|
+
printPlan(cfg, flags.envs);
|
|
373
493
|
return;
|
|
374
494
|
}
|
|
375
495
|
if (flags.noDeploy) {
|
|
@@ -378,14 +498,20 @@ async function main(argv) {
|
|
|
378
498
|
return;
|
|
379
499
|
}
|
|
380
500
|
if ((0, io_1.canPrompt)()) {
|
|
381
|
-
const
|
|
501
|
+
const stacks = flags.envs
|
|
502
|
+
.map((env) => (0, config_1.resolveEnvironment)(cfg, env).stackName)
|
|
503
|
+
.join(" + ");
|
|
504
|
+
const ok = await confirm(`Deploy ${stacks} to AWS ${cfg.region} now? (requires the bootstrap stack to exist) [y/N]`);
|
|
382
505
|
if (!ok) {
|
|
383
506
|
console.log("\n Skipped the deploy. Re-run to deploy, or `rastack init --print` for the commands.\n");
|
|
384
507
|
return;
|
|
385
508
|
}
|
|
386
509
|
}
|
|
387
|
-
cfg = await standUp(cfg, (0, io_1.canPrompt)());
|
|
388
|
-
|
|
510
|
+
cfg = await standUp(cfg, flags.envs, (0, io_1.canPrompt)());
|
|
511
|
+
// Refresh `.env` with the resolved stack Outputs (API URLs, pool/client ids).
|
|
512
|
+
for (const p of writeEnvAndGitignore(cfg))
|
|
513
|
+
console.log(` ✓ updated ${p}`);
|
|
514
|
+
printSummary(cfg, flags.envs);
|
|
389
515
|
}
|
|
390
516
|
if (require.main === module) {
|
|
391
517
|
main(process.argv.slice(2)).catch((err) => {
|
package/dist/rastack.js
CHANGED
|
@@ -130,6 +130,7 @@ switch (command) {
|
|
|
130
130
|
case "deploy":
|
|
131
131
|
// Ship code to the Lambda — an alias for the CI code deploy. Infrastructure
|
|
132
132
|
// is stood up once by `rastack init`; `deploy` only ever ships code.
|
|
133
|
+
// `--env dev|prod` picks which environment's function to update.
|
|
133
134
|
run("rastack-ci.js", ["deploy", ...rest]);
|
|
134
135
|
break;
|
|
135
136
|
case "ci":
|
|
@@ -34,7 +34,44 @@ Parameters:
|
|
|
34
34
|
GitHubBranch:
|
|
35
35
|
Type: String
|
|
36
36
|
Default: master
|
|
37
|
-
Description: Branch of GitHubRepo allowed to deploy.
|
|
37
|
+
Description: Branch of GitHubRepo allowed to deploy (used when GitHubEnvironment is blank).
|
|
38
|
+
|
|
39
|
+
GitHubEnvironment:
|
|
40
|
+
Type: String
|
|
41
|
+
Default: ""
|
|
42
|
+
Description: >
|
|
43
|
+
GitHub Actions **Environment** (e.g. development / production) this stack's
|
|
44
|
+
deploy role trusts. When set, the OIDC trust is scoped to
|
|
45
|
+
`…:environment:<name>` — the subject a job that declares `environment:` gets
|
|
46
|
+
— instead of a branch ref, so the dev and prod deploys can share a branch
|
|
47
|
+
set yet only assume their own environment's role. Blank keeps the legacy
|
|
48
|
+
branch-ref trust (`…:ref:refs/heads/<GitHubBranch>`). `rastack init` sets it
|
|
49
|
+
to development / production for the dev / prod stacks.
|
|
50
|
+
|
|
51
|
+
CallbackUrls:
|
|
52
|
+
Type: CommaDelimitedList
|
|
53
|
+
Default: "http://localhost:4321"
|
|
54
|
+
Description: >
|
|
55
|
+
Allowed OAuth redirect (callback) URLs for the Hosted UI app client. The
|
|
56
|
+
local `rastack dev` origin is the default so a developer can sign in against
|
|
57
|
+
this environment's real Cognito pool while developing; add your deployed
|
|
58
|
+
frontend origin(s) too.
|
|
59
|
+
|
|
60
|
+
LogoutUrls:
|
|
61
|
+
Type: CommaDelimitedList
|
|
62
|
+
Default: "http://localhost:4321"
|
|
63
|
+
Description: Allowed sign-out redirect URLs for the Hosted UI app client.
|
|
64
|
+
|
|
65
|
+
ExternalApiUpstream:
|
|
66
|
+
Type: String
|
|
67
|
+
Default: ""
|
|
68
|
+
Description: >
|
|
69
|
+
Base URL (no trailing slash) the **external-API proxy** forwards to —
|
|
70
|
+
typically a UAT or prod backend. When set, the stack adds a
|
|
71
|
+
Cognito-authorized `ANY /proxy/{proxy+}` route to the HTTP API that forwards
|
|
72
|
+
to `<upstream>/{proxy}`, so a locally-running app (signed in against this
|
|
73
|
+
pool) reaches external APIs only through the proxy, never directly. Blank
|
|
74
|
+
omits the proxy route + authorizer entirely.
|
|
38
75
|
|
|
39
76
|
ArtifactsBucket:
|
|
40
77
|
Type: String
|
|
@@ -87,6 +124,10 @@ Conditions:
|
|
|
87
124
|
DeriveOIDCArn: !Equals [!Ref OIDCProviderArn, ""]
|
|
88
125
|
ShouldTrailDataEvents: !Equals [!Ref EnableDataEventTrail, "true"]
|
|
89
126
|
UseAdvancedSecurity: !Not [!Equals [!Ref AdvancedSecurity, "OFF"]]
|
|
127
|
+
# Scope the deploy-role trust to a GitHub Actions Environment when one is given.
|
|
128
|
+
TrustEnvironment: !Not [!Equals [!Ref GitHubEnvironment, ""]]
|
|
129
|
+
# Provision the external-API proxy route only when an upstream is configured.
|
|
130
|
+
HasProxy: !Not [!Equals [!Ref ExternalApiUpstream, ""]]
|
|
90
131
|
|
|
91
132
|
Resources:
|
|
92
133
|
|
|
@@ -290,6 +331,20 @@ Resources:
|
|
|
290
331
|
- ALLOW_USER_SRP_AUTH
|
|
291
332
|
- ALLOW_REFRESH_TOKEN_AUTH
|
|
292
333
|
PreventUserExistenceErrors: ENABLED
|
|
334
|
+
# Hosted UI (OAuth 2.0 Authorization Code + PKCE) — the flow rastack/auth's
|
|
335
|
+
# <CognitoAuthProvider> drives. Enabling it here lets a locally-running app
|
|
336
|
+
# (`rastack dev`) sign in against this environment's real Cognito pool.
|
|
337
|
+
SupportedIdentityProviders:
|
|
338
|
+
- COGNITO
|
|
339
|
+
AllowedOAuthFlowsUserPoolClient: true
|
|
340
|
+
AllowedOAuthFlows:
|
|
341
|
+
- code
|
|
342
|
+
AllowedOAuthScopes:
|
|
343
|
+
- openid
|
|
344
|
+
- email
|
|
345
|
+
- profile
|
|
346
|
+
CallbackURLs: !Ref CallbackUrls
|
|
347
|
+
LogoutURLs: !Ref LogoutUrls
|
|
293
348
|
TokenValidityUnits:
|
|
294
349
|
AccessToken: minutes
|
|
295
350
|
IdToken: minutes
|
|
@@ -299,6 +354,15 @@ Resources:
|
|
|
299
354
|
RefreshTokenValidity: 30
|
|
300
355
|
EnableTokenRevocation: true
|
|
301
356
|
|
|
357
|
+
# Hosted UI domain — the browser sign-in surface. Prefix is globally unique via
|
|
358
|
+
# the account id. Both the deployed frontend and local `rastack dev` redirect
|
|
359
|
+
# here to authenticate against this environment's pool.
|
|
360
|
+
UserPoolDomain:
|
|
361
|
+
Type: AWS::Cognito::UserPoolDomain
|
|
362
|
+
Properties:
|
|
363
|
+
Domain: !Sub "${NamePrefix}-${AppName}-${AWS::AccountId}"
|
|
364
|
+
UserPoolId: !Ref UserPool
|
|
365
|
+
|
|
302
366
|
IdentityPool:
|
|
303
367
|
Type: AWS::Cognito::IdentityPool
|
|
304
368
|
Properties:
|
|
@@ -494,6 +558,49 @@ Resources:
|
|
|
494
558
|
Principal: apigateway.amazonaws.com
|
|
495
559
|
SourceArn: !Sub "arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${HttpApi}/*/*"
|
|
496
560
|
|
|
561
|
+
# -- External-API proxy (optional) --------------------------------------------
|
|
562
|
+
# When ExternalApiUpstream is set, a Cognito-authorized `ANY /proxy/{proxy+}`
|
|
563
|
+
# route forwards to `<upstream>/{proxy}`. HTTP API prefers the more specific
|
|
564
|
+
# route, so `/proxy/*` goes here and everything else still hits the Rust Lambda
|
|
565
|
+
# via `$default`. The JWT authorizer means only callers holding a valid token
|
|
566
|
+
# from THIS environment's pool can use the proxy — so a local dev app signs in
|
|
567
|
+
# against the dev pool, then reaches UAT/prod externals only through here.
|
|
568
|
+
ProxyJwtAuthorizer:
|
|
569
|
+
Type: AWS::ApiGatewayV2::Authorizer
|
|
570
|
+
Condition: HasProxy
|
|
571
|
+
Properties:
|
|
572
|
+
ApiId: !Ref HttpApi
|
|
573
|
+
AuthorizerType: JWT
|
|
574
|
+
Name: !Sub "${NamePrefix}-${AppName}-cognito"
|
|
575
|
+
IdentitySource:
|
|
576
|
+
- "$request.header.Authorization"
|
|
577
|
+
JwtConfiguration:
|
|
578
|
+
Audience:
|
|
579
|
+
- !Ref UserPoolClient
|
|
580
|
+
Issuer: !Sub "https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}"
|
|
581
|
+
|
|
582
|
+
ProxyIntegration:
|
|
583
|
+
Type: AWS::ApiGatewayV2::Integration
|
|
584
|
+
Condition: HasProxy
|
|
585
|
+
Properties:
|
|
586
|
+
ApiId: !Ref HttpApi
|
|
587
|
+
IntegrationType: HTTP_PROXY
|
|
588
|
+
IntegrationMethod: ANY
|
|
589
|
+
# `{proxy}` is the greedy path variable captured by the route; the caller's
|
|
590
|
+
# Authorization header is forwarded so the upstream can authorize too.
|
|
591
|
+
IntegrationUri: !Sub "${ExternalApiUpstream}/{proxy}"
|
|
592
|
+
PayloadFormatVersion: "1.0"
|
|
593
|
+
|
|
594
|
+
ProxyRoute:
|
|
595
|
+
Type: AWS::ApiGatewayV2::Route
|
|
596
|
+
Condition: HasProxy
|
|
597
|
+
Properties:
|
|
598
|
+
ApiId: !Ref HttpApi
|
|
599
|
+
RouteKey: "ANY /proxy/{proxy+}"
|
|
600
|
+
Target: !Sub "integrations/${ProxyIntegration}"
|
|
601
|
+
AuthorizationType: JWT
|
|
602
|
+
AuthorizerId: !Ref ProxyJwtAuthorizer
|
|
603
|
+
|
|
497
604
|
# -- Deploy role (GitHub OIDC) — code only ------------------------------------
|
|
498
605
|
# CI assumes this to ship compiled code. It can UpdateFunctionCode on THIS one
|
|
499
606
|
# function and read this app's SSM params — nothing else — with an explicit
|
|
@@ -518,7 +625,13 @@ Resources:
|
|
|
518
625
|
StringEquals:
|
|
519
626
|
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
520
627
|
StringLike:
|
|
521
|
-
|
|
628
|
+
# An environment-gated GitHub job's OIDC subject is
|
|
629
|
+
# `repo:O/R:environment:<env>`, not `…:ref:refs/heads/<branch>`,
|
|
630
|
+
# so the trust must match the environment when one is set.
|
|
631
|
+
token.actions.githubusercontent.com:sub: !If
|
|
632
|
+
- TrustEnvironment
|
|
633
|
+
- !Sub "repo:${GitHubOwner}/${GitHubRepo}:environment:${GitHubEnvironment}"
|
|
634
|
+
- !Sub "repo:${GitHubOwner}/${GitHubRepo}:ref:refs/heads/${GitHubBranch}"
|
|
522
635
|
Policies:
|
|
523
636
|
- PolicyName: update-function-code-only
|
|
524
637
|
PolicyDocument:
|
|
@@ -602,6 +715,14 @@ Resources:
|
|
|
602
715
|
Type: String
|
|
603
716
|
Value: !Sub "https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}/.well-known/jwks.json"
|
|
604
717
|
|
|
718
|
+
ParamHostedUiUrl:
|
|
719
|
+
Type: AWS::SSM::Parameter
|
|
720
|
+
DependsOn: UserPoolDomain
|
|
721
|
+
Properties:
|
|
722
|
+
Name: !Sub "/rastack/${AppName}/hosted-ui-url"
|
|
723
|
+
Type: String
|
|
724
|
+
Value: !Sub "https://${NamePrefix}-${AppName}-${AWS::AccountId}.auth.${AWS::Region}.amazoncognito.com"
|
|
725
|
+
|
|
605
726
|
Outputs:
|
|
606
727
|
ApiUrl:
|
|
607
728
|
Description: Base URL of the deployed API (set as the frontend's remote base).
|
|
@@ -627,9 +748,19 @@ Outputs:
|
|
|
627
748
|
JwksUrl:
|
|
628
749
|
Description: JWKS to bake into the Lambda bundle at build time.
|
|
629
750
|
Value: !Sub "https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}/.well-known/jwks.json"
|
|
751
|
+
UserPoolDomain:
|
|
752
|
+
Description: Cognito Hosted UI domain prefix (scheme-less).
|
|
753
|
+
Value: !Ref UserPoolDomain
|
|
754
|
+
HostedUiUrl:
|
|
755
|
+
Description: Cognito Hosted UI base URL — browser sign-in / `rastack dev` auth.
|
|
756
|
+
Value: !Sub "https://${NamePrefix}-${AppName}-${AWS::AccountId}.auth.${AWS::Region}.amazoncognito.com"
|
|
630
757
|
WarehouseBucketName:
|
|
631
758
|
Description: S3 Iceberg warehouse — the system of record.
|
|
632
759
|
Value: !Ref WarehouseBucket
|
|
633
760
|
AuditLogBucketName:
|
|
634
761
|
Description: S3 access logs + CloudTrail data events for the warehouse.
|
|
635
762
|
Value: !Ref AuditLogBucket
|
|
763
|
+
ProxyUrl:
|
|
764
|
+
Condition: HasProxy
|
|
765
|
+
Description: Base URL of the Cognito-authorized external-API proxy (when enabled).
|
|
766
|
+
Value: !Sub "https://${HttpApi}.execute-api.${AWS::Region}.amazonaws.com/proxy"
|
|
Binary file
|