rastack 0.0.62 → 0.0.64

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/admin/AdminApp.tsx +56 -3
  3. package/admin/index.tsx +8 -2
  4. package/admin/ui/icons.tsx +9 -0
  5. package/admin/ui/styles.ts +3 -0
  6. package/admin/useAdmin.ts +41 -4
  7. package/auth/provider.ts +71 -8
  8. package/dist/admin.js +17 -14
  9. package/dist/compile/analyze.js +11 -1
  10. package/dist/define/index.d.ts +8 -1
  11. package/dist/define/index.js +2 -2
  12. package/dist/define/manifest.js +7 -1
  13. package/dist/deploy/ci.d.ts +10 -3
  14. package/dist/deploy/ci.js +22 -11
  15. package/dist/deploy/config.d.ts +155 -5
  16. package/dist/deploy/config.js +216 -6
  17. package/dist/deploy/io.d.ts +57 -0
  18. package/dist/deploy/io.js +141 -0
  19. package/dist/deploy/workflows.d.ts +30 -0
  20. package/dist/deploy/workflows.js +117 -12
  21. package/dist/rastack-bootstrap.js +18 -47
  22. package/dist/rastack-ci.js +19 -1
  23. package/dist/rastack-dev.js +48 -2
  24. package/dist/rastack-init.d.ts +6 -3
  25. package/dist/rastack-init.js +231 -92
  26. package/dist/rastack.js +1 -0
  27. package/dist/templates/cloudformation/app.yaml +133 -2
  28. package/dist/wasm/rastack_wasm.js +1 -1
  29. package/dist/wasm/rastack_wasm_bg.wasm +0 -0
  30. package/package.json +1 -1
  31. package/provider/provider.tsx +23 -0
  32. package/provider/types.ts +9 -0
  33. package/src/compile/analyze.ts +11 -1
  34. package/src/define/index.ts +5 -1
  35. package/src/define/manifest.ts +7 -1
  36. package/src/deploy/ci.ts +50 -16
  37. package/src/deploy/config.ts +340 -7
  38. package/src/deploy/io.ts +179 -2
  39. package/src/deploy/workflows.ts +122 -12
  40. package/src/rastack-bootstrap.ts +24 -69
  41. package/src/rastack-ci.ts +20 -2
  42. package/src/rastack-dev.ts +61 -6
  43. package/src/rastack-init.ts +283 -115
  44. package/src/rastack.ts +1 -0
@@ -12,14 +12,17 @@
12
12
  * 1. writes `.rastack/deploy.json` + `.github/workflows/{ci,deploy-backend}.yml`
13
13
  * (GitHub coordinates default from the `origin` remote; missing fields are
14
14
  * prompted for on a TTY);
15
- * 2. resolves the tier-0 bootstrap stack's artifacts bucket — erroring with a
16
- * pointer to `rastack bootstrap` when that account+region stack is missing
17
- * (init is per-app and never creates account-level infrastructure);
15
+ * 2. verifies the AWS CLI is logged in, then ensures the tier-0 bootstrap stack
16
+ * exists — creating it (the S3 template/placeholder buckets + GitHub OIDC
17
+ * provider) when missing, reconciling it when it already does;
18
18
  * 3. uploads the Lambda placeholder bundle to that bucket;
19
19
  * 4. deploys the per-app backend (app.yaml) with the resolved ArtifactsBucket;
20
20
  * 5. reads the stack Outputs back into the config and prints the two GitHub repo
21
21
  * Variables CI needs.
22
22
  *
23
+ * The CloudFormation templates ship inside the `rastack` package; a partial
24
+ * install falls back to downloading them from reactapistack.com.
25
+ *
23
26
  * Thereafter the recurring code deploy is CI's job — `rastack ci deploy`, which
24
27
  * `rastack deploy` also runs. `--no-deploy` scaffolds only; `--print` shows the
25
28
  * exact single-line `aws cloudformation deploy` commands without running them.
@@ -63,12 +66,23 @@ const path = __importStar(require("path"));
63
66
  const config_1 = require("./deploy/config");
64
67
  const workflows_1 = require("./deploy/workflows");
65
68
  const io_1 = require("./deploy/io");
69
+ function parseEnvFlag(value) {
70
+ const v = value.trim().toLowerCase();
71
+ if (v === "all" || v === "")
72
+ return [...config_1.ENV_NAMES];
73
+ if (config_1.ENV_NAMES.includes(v))
74
+ return [v];
75
+ console.error(`Unknown --env: expected dev | prod | all.`);
76
+ process.exit(1);
77
+ }
66
78
  function parseFlags(argv) {
67
- const patch = { github: {} };
79
+ const patch = { github: {}, environments: {} };
80
+ let envs = [...config_1.ENV_NAMES];
68
81
  let force = false;
69
82
  let noWorkflows = false;
70
83
  let noDeploy = false;
71
84
  let print = false;
85
+ const devPatch = () => { var _a; return ((_a = patch.environments).dev ?? (_a.dev = {})); };
72
86
  for (let i = 0; i < argv.length; i++) {
73
87
  const a = argv[i];
74
88
  const next = () => argv[++i] ?? "";
@@ -85,6 +99,17 @@ function parseFlags(argv) {
85
99
  case "--branch":
86
100
  patch.github.branch = next();
87
101
  break;
102
+ case "--dev-branch":
103
+ devPatch().branch = next();
104
+ break;
105
+ case "--dev-proxy":
106
+ case "--dev-proxy-upstream":
107
+ // The dev external-API proxy upstream (a UAT/prod backend URL).
108
+ devPatch().proxyUpstream = next();
109
+ break;
110
+ case "--env":
111
+ envs = parseEnvFlag(next());
112
+ break;
88
113
  case "--region":
89
114
  patch.region = next();
90
115
  break;
@@ -114,7 +139,10 @@ function parseFlags(argv) {
114
139
  }
115
140
  }
116
141
  }
117
- return { patch, force, noWorkflows, noDeploy, print };
142
+ // Drop the scratch environments container when nothing was set on it.
143
+ if (patch.environments && !patch.environments.dev)
144
+ delete patch.environments;
145
+ return { patch, envs, force, noWorkflows, noDeploy, print };
118
146
  }
119
147
  /** Seed a config: existing file → flags → git remote → defaults. */
120
148
  function seedConfig(flags) {
@@ -151,6 +179,35 @@ async function promptMissing(cfg) {
151
179
  });
152
180
  });
153
181
  }
182
+ /**
183
+ * Ensure `.env` is git-ignored and (re)write the generated `.env` with the
184
+ * current config, preserving any secrets a developer already added. Returns the
185
+ * repo-relative paths touched, for the written/skipped summary.
186
+ */
187
+ function writeEnvAndGitignore(cfg) {
188
+ const touched = [];
189
+ // 1. git-ignore `.env` (append-merge; never duplicate or clobber).
190
+ const giAbs = path.join(process.cwd(), workflows_1.GITIGNORE_PATH);
191
+ const existingGi = fs.existsSync(giAbs)
192
+ ? fs.readFileSync(giAbs, "utf-8")
193
+ : "";
194
+ const nextGi = (0, workflows_1.gitignoreWith)(existingGi, workflows_1.ENV_PATH);
195
+ if (nextGi !== existingGi) {
196
+ fs.writeFileSync(giAbs, nextGi);
197
+ touched.push(workflows_1.GITIGNORE_PATH);
198
+ }
199
+ // 2. Upsert the generated RASTACK_* values into `.env` (secrets preserved).
200
+ const envAbs = path.join(process.cwd(), workflows_1.ENV_PATH);
201
+ const existingEnv = fs.existsSync(envAbs)
202
+ ? fs.readFileSync(envAbs, "utf-8")
203
+ : "";
204
+ const nextEnv = (0, workflows_1.applyEnvValues)(existingEnv, cfg);
205
+ if (nextEnv !== existingEnv) {
206
+ fs.writeFileSync(envAbs, nextEnv);
207
+ touched.push(workflows_1.ENV_PATH);
208
+ }
209
+ return touched;
210
+ }
154
211
  /** Yes/no confirmation with a default of "no" — gates the cloud-mutating steps. */
155
212
  async function confirm(question) {
156
213
  return (0, io_1.withPrompt)(async (rl) => {
@@ -164,104 +221,137 @@ function table(rows) {
164
221
  return rows.map((r) => ` ${r.key.padEnd(width)} ${r.value}`).join("\n");
165
222
  }
166
223
  /**
167
- * Read the app.yaml stack Outputs into a `DeployOutputs`, skipping any that are
168
- * not resolvable yet (best-effort — a freshly created stack has them all).
224
+ * Read one environment's app.yaml stack Outputs into a `DeployOutputs`, skipping
225
+ * any not resolvable yet (best-effort — a freshly created stack has them all).
169
226
  */
170
- function readAppOutputs(cfg) {
227
+ function readEnvOutputs(cfg, env) {
228
+ const stackName = (0, config_1.resolveEnvironment)(cfg, env).stackName;
171
229
  const outputs = {};
172
230
  for (const { key, field } of config_1.APP_STACK_OUTPUTS) {
173
- const value = (0, io_1.stackOutput)(cfg.stackName, key, cfg.region);
231
+ const value = (0, io_1.stackOutput)(stackName, key, cfg.region);
174
232
  if (value)
175
233
  outputs[field] = value;
176
234
  }
177
235
  return outputs;
178
236
  }
237
+ /** Merge an environment's resolved outputs back into the config in the right slot. */
238
+ function withEnvOutputs(cfg, env, outputs) {
239
+ return env === "prod"
240
+ ? (0, config_1.mergeConfig)(cfg, { outputs })
241
+ : (0, config_1.mergeConfig)(cfg, { environments: { dev: { outputs } } });
242
+ }
243
+ /** Human label for an environment ("dev" / "prod") plus its branch. */
244
+ function envLabel(cfg, env) {
245
+ const e = (0, config_1.resolveEnvironment)(cfg, env);
246
+ return `${env} (${e.branch} → ${e.ghEnvironment})`;
247
+ }
179
248
  /**
180
249
  * Print the exact commands `init` would run, without touching the cloud
181
- * (`--print`). The artifacts bucket is read from the bootstrap stack when the
182
- * AWS CLI is available, else shown as a placeholder.
250
+ * (`--print`). init stands up *both* stacks, so both `aws cloudformation deploy`
251
+ * commands are shown: the tier-0 bootstrap stack (created only when missing) and
252
+ * the per-app backend. The artifacts bucket is read from an existing bootstrap
253
+ * stack when the AWS CLI is available, else shown as a placeholder.
183
254
  */
184
- function printPlan(cfg) {
255
+ function printPlan(cfg, envs) {
185
256
  const bucket = (0, io_1.awsAvailable)()
186
257
  ? (0, io_1.stackOutput)((0, config_1.bootstrapStackName)(cfg), config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region)
187
258
  : null;
188
259
  const withBucket = (0, config_1.mergeConfig)(cfg, {
189
260
  artifactsBucket: bucket ?? "<ArtifactsBucketName from bootstrap stack>",
190
261
  });
262
+ const bootstrapTemplate = (0, io_1.resolveDeployAsset)("bootstrap.yaml") ?? undefined;
191
263
  const appTemplate = (0, io_1.resolveDeployAsset)("app.yaml") ?? undefined;
192
- console.log(`\n Deployment plan (${cfg.stackName} @ ${cfg.region})\n`);
193
- console.log(` ── Prerequisite: the tier-0 bootstrap stack (${(0, config_1.bootstrapStackName)(cfg)}) must exist ──`);
194
- 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) — what init deploys ──");
196
- console.log("\n " + (0, config_1.deployStackCommand)(withBucket, appTemplate) + "\n");
197
- console.log(" ── 2. Wire these GitHub repo Variables from the stack Outputs ──");
198
- console.log(table((0, config_1.repoVariables)(cfg).map((v) => ({
199
- key: v.name,
200
- value: v.value || `<${v.from}>`,
201
- }))) + "\n");
264
+ console.log(`\n Deployment plan (${envs.join(" + ")} @ ${cfg.region})\n`);
265
+ console.log(` ── 1. Tier-0 bootstrap stack (${(0, config_1.bootstrapStackName)(cfg)}) — created only when missing ──`);
266
+ console.log("\n " + (0, config_1.bootstrapStackCommand)(cfg, false, bootstrapTemplate) + "\n");
267
+ console.log(" ── 2. Per-app backend (app.yaml) — one stack per environment ──");
268
+ for (const env of envs) {
269
+ const e = (0, config_1.resolveEnvironment)(withBucket, env);
270
+ console.log(`\n # ${envLabel(withBucket, env)} — stack ${e.stackName}`);
271
+ console.log(" " + (0, config_1.deployStackCommandForEnv)(withBucket, env, appTemplate));
272
+ }
273
+ console.log("\n ── 2. Wire each GitHub Environment's Variables from its stack Outputs ──");
274
+ for (const env of envs) {
275
+ const e = (0, config_1.resolveEnvironment)(cfg, env);
276
+ console.log(`\n # GitHub Environment: ${e.ghEnvironment}`);
277
+ console.log(table((0, config_1.repoVariablesForEnv)(cfg, env).map((v) => ({
278
+ key: v.name,
279
+ value: v.value || `<${v.from}>`,
280
+ }))));
281
+ }
282
+ console.log("");
202
283
  }
203
284
  /**
204
- * Stand the infrastructure up: ensure the bootstrap stack + artifacts bucket,
205
- * upload the Lambda placeholder, deploy app.yaml, and read the Outputs back into
206
- * the config. Returns the config enriched with the resolved buckets + outputs.
285
+ * Stand the infrastructure up: verify the AWS CLI is logged in, ensure the
286
+ * tier-0 bootstrap stack (creating it when missing), upload the Lambda
287
+ * placeholder, deploy app.yaml, and read the Outputs back into the config.
288
+ * `init` is the single command that stands up *everything* — there is no
289
+ * separate manual bootstrap step. Returns the config enriched with the resolved
290
+ * buckets + outputs.
207
291
  */
208
- async function standUp(cfg, interactive) {
292
+ async function standUp(cfg, envs, interactive) {
209
293
  if (!(0, io_1.awsAvailable)()) {
210
294
  console.error(" ✗ the AWS CLI is not on PATH. Install it and configure credentials,\n" +
211
295
  " or re-run with --print to see the commands, or --no-deploy to skip.");
212
296
  process.exit(1);
213
297
  }
214
- // 1. Resolve the artifacts bucket from the tier-0 bootstrap stack. That stack
215
- // is an account+region once-off — the S3 buckets that host the templates +
216
- // Lambda placeholder, and the GitHub OIDC provider. `init` is a per-app
217
- // command and deliberately does NOT create it: standing up account-level
218
- // infrastructure is `rastack bootstrap`'s job (run once by an account admin),
219
- // so a missing bootstrap stack is a clear, actionable error, not an implicit
220
- // account mutation buried inside an app deploy.
221
- const artifactsBucket = (0, io_1.stackOutput)((0, config_1.bootstrapStackName)(cfg), config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
222
- const templatesBucket = (0, io_1.stackOutput)((0, config_1.bootstrapStackName)(cfg), config_1.BOOTSTRAP_TEMPLATES_OUTPUT, cfg.region);
223
- if (!artifactsBucket) {
224
- console.error(`\n ✗ Bootstrap stack ${(0, config_1.bootstrapStackName)(cfg)} not found in ${cfg.region}.\n` +
225
- " It is the once-per-account+region step that hosts the CloudFormation\n" +
226
- " templates and the Lambda placeholder every app deploy reads. Create it\n" +
227
- " first (an account admin does this once), then re-run `rastack init`:\n\n" +
228
- ` rastack bootstrap --region ${cfg.region}\n`);
298
+ // Verify the AWS CLI is actually logged in before creating any stacks, so a
299
+ // missing/expired credential fails fast with an actionable message rather than
300
+ // a cryptic `cloudformation deploy` error partway through.
301
+ const account = (0, io_1.awsIdentity)(cfg.region);
302
+ if (!account) {
303
+ console.error(" ✗ the AWS CLI has no usable credentials — it is not logged in.\n" +
304
+ " Configure credentials, then re-run `rastack init`:\n" +
305
+ " aws configure # static access key / secret\n" +
306
+ " aws sso login # or an existing SSO session\n" +
307
+ " Or re-run with --print to see the commands, or --no-deploy to skip.");
229
308
  process.exit(1);
230
309
  }
310
+ console.log(` ✓ AWS account ${account} (${cfg.region})`);
311
+ // 1. Ensure the tier-0 bootstrap stack exists — the S3 buckets that host the
312
+ // templates + Lambda placeholder and the GitHub OIDC provider. init creates
313
+ // it when missing (idempotent for an existing one), so a single command
314
+ // takes an account+repo from nothing to a deployed backend.
315
+ const { artifactsBucket, templatesBucket } = await (0, io_1.ensureBootstrapStack)(cfg).catch((err) => {
316
+ console.error(`\n ✗ ${err.message}`);
317
+ process.exit(1);
318
+ });
231
319
  console.log(` ✓ artifacts bucket: ${artifactsBucket}`);
232
- // 2. Upload the placeholder the function is created from.
320
+ // 2. Upload the placeholder the function is created from (shared by all envs).
233
321
  console.log("\n▸ Uploading the Lambda placeholder bundle");
234
322
  if (!(0, io_1.uploadPlaceholder)(artifactsBucket, cfg.region)) {
235
323
  console.error(" ✗ Lambda placeholder bundle not found in the rastack package — cannot create the function.");
236
324
  process.exit(1);
237
325
  }
238
- // 3. Deploy the per-app backend with the resolved bucket wired in.
239
326
  cfg = (0, config_1.mergeConfig)(cfg, {
240
327
  artifactsBucket,
241
328
  templatesBucket: templatesBucket ?? undefined,
242
329
  });
243
- const appTemplate = (0, io_1.resolveDeployAsset)("app.yaml");
330
+ const appTemplate = await (0, io_1.resolveTemplateFile)("app.yaml");
244
331
  if (!appTemplate) {
245
- console.error(" ✗ app.yaml template not found in the rastack package. Reinstall rastack\n" +
246
- " (`npm i -g rastack`) or run from the framework repo.");
332
+ console.error(" ✗ app.yaml template not found in the rastack package and could not be\n" +
333
+ " downloaded from reactapistack.com. Reinstall rastack (`npm i -g rastack`).");
247
334
  process.exit(1);
248
335
  }
249
- console.log(`\n▸ Deploying ${cfg.stackName}`);
250
- try {
251
- (0, io_1.awsRun)((0, config_1.deployStackArgs)(cfg, appTemplate));
252
- }
253
- catch (err) {
254
- console.error(`\n ✗ aws cloudformation deploy failed for ${cfg.stackName}.\n ${err.message}`);
255
- process.exit(1);
336
+ // 3. Deploy one stack per environment — each with its own Cognito pool,
337
+ // warehouse, Lambda and API — reading the Outputs back into the config.
338
+ for (const env of envs) {
339
+ const stackName = (0, config_1.resolveEnvironment)(cfg, env).stackName;
340
+ console.log(`\n▸ Deploying ${envLabel(cfg, env)} — ${stackName}`);
341
+ try {
342
+ (0, io_1.awsRun)((0, config_1.deployStackArgsForEnv)(cfg, env, appTemplate));
343
+ }
344
+ catch (err) {
345
+ console.error(`\n ✗ aws cloudformation deploy failed for ${stackName}.\n ${err.message}`);
346
+ process.exit(1);
347
+ }
348
+ cfg = withEnvOutputs(cfg, env, readEnvOutputs(cfg, env));
349
+ (0, io_1.saveConfig)(cfg);
256
350
  }
257
- // 4. Read the Outputs back into the config so CI has them without a live lookup.
258
- const outputs = readAppOutputs(cfg);
259
- cfg = (0, config_1.mergeConfig)(cfg, { outputs });
260
- (0, io_1.saveConfig)(cfg);
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);
351
+ // 4. Connect the GitHub CLI and wire each environment's scoped Variables, so
352
+ // the FIRST push actually ships. Without this the deploy-backend workflow's
353
+ // `if: vars.AWS_DEPLOY_ROLE_ARN != ''` guard silently skips every run.
354
+ wireEnvironments(cfg, envs, interactive);
265
355
  return cfg;
266
356
  }
267
357
  /**
@@ -275,58 +365,97 @@ async function standUp(cfg, interactive) {
275
365
  * (and isn't) done.
276
366
  */
277
367
  const wiredVariables = new Set();
278
- function wireRepoVariables(cfg, interactive) {
279
- const vars = (0, config_1.repoVariables)(cfg).filter((v) => v.value);
280
- if (!vars.length)
368
+ /** Key a wired Variable by its GitHub Environment so dev/prod don't collide. */
369
+ function wiredKey(ghEnvironment, name) {
370
+ return `${ghEnvironment ?? ""}:${name}`;
371
+ }
372
+ /**
373
+ * Create each environment's GitHub Actions Environment and wire its scoped
374
+ * `AWS_DEPLOY_ROLE_ARN` / `AWS_REGION` Variables via `gh`, so the GitHub →
375
+ * Lambda pipeline is live for both dev and prod the moment `init` finishes.
376
+ */
377
+ function wireEnvironments(cfg, envs, interactive) {
378
+ const anyValue = envs.some((env) => (0, config_1.repoVariablesForEnv)(cfg, env).some((v) => v.value));
379
+ if (!anyValue)
281
380
  return;
282
381
  if (!(0, io_1.ghAvailable)()) {
283
- console.log("\n▸ GitHub CLI (`gh`) not found — skipping repo Variable wiring.\n" +
382
+ console.log("\n▸ GitHub CLI (`gh`) not found — skipping environment + Variable wiring.\n" +
284
383
  " Install `gh` (https://cli.github.com) and re-run, or wire the\n" +
285
384
  " Variables below by hand so pushes deploy.");
286
385
  return;
287
386
  }
288
- // Connect `gh` to GitHub if it isn't already, so it can set the Variables.
387
+ // Connect `gh` to GitHub if it isn't already, so it can create environments.
289
388
  if (!(0, io_1.ghAuthenticated)()) {
290
389
  if (interactive) {
291
390
  console.log("\n▸ Connecting the GitHub CLI to your account (`gh auth login`)");
292
391
  (0, io_1.ghAuthLogin)();
293
392
  }
294
393
  if (!(0, io_1.ghAuthenticated)()) {
295
- console.log("\n▸ GitHub CLI is not connected — skipping repo Variable wiring.\n" +
394
+ console.log("\n▸ GitHub CLI is not connected — skipping environment wiring.\n" +
296
395
  " Run `gh auth login`, then wire the Variables below by hand.");
297
396
  return;
298
397
  }
299
398
  }
300
- console.log("\n▸ Wiring the GitHub repo Variables CI reads (via `gh`)");
301
- for (const v of vars) {
302
- const ok = (0, io_1.setRepoVariable)((0, config_1.ghVariableSetArgs)(cfg, v.name, v.value));
303
- if (ok) {
304
- wiredVariables.add(v.name);
305
- console.log(` ✓ set ${v.name}`);
399
+ console.log("\n▸ Creating GitHub Environments + wiring their Variables (via `gh`)");
400
+ for (const env of envs) {
401
+ const e = (0, config_1.resolveEnvironment)(cfg, env);
402
+ // Create the Environment first — a Variable can only be scoped to one that
403
+ // already exists. Idempotent (PUT), so re-running init is safe.
404
+ if (!(0, io_1.ghEnvironmentCreate)((0, config_1.ghEnvironmentCreateArgs)(cfg, e.ghEnvironment))) {
405
+ console.log(` • could not create GitHub Environment ${e.ghEnvironment} — create it by hand`);
406
+ continue;
306
407
  }
307
- else {
308
- console.log(` • could not set ${v.name} — wire it by hand (below)`);
408
+ for (const v of (0, config_1.repoVariablesForEnv)(cfg, env).filter((x) => x.value)) {
409
+ const ok = (0, io_1.setRepoVariable)((0, config_1.ghVariableSetArgs)(cfg, v.name, v.value, v.ghEnvironment));
410
+ if (ok) {
411
+ wiredVariables.add(wiredKey(v.ghEnvironment, v.name));
412
+ console.log(` ✓ set ${e.ghEnvironment}/${v.name}`);
413
+ }
414
+ else {
415
+ console.log(` • could not set ${e.ghEnvironment}/${v.name} — wire it by hand (below)`);
416
+ }
309
417
  }
310
418
  }
311
419
  }
312
- function printSummary(cfg) {
313
- console.log(`\n ✓ ${cfg.app} is deployed (${cfg.stackName} @ ${cfg.region})`);
314
- if (cfg.outputs?.apiUrl)
315
- console.log(` API: ${cfg.outputs.apiUrl}`);
420
+ function printSummary(cfg, envs) {
421
+ console.log(`\n ✓ ${cfg.app} is deployed @ ${cfg.region}`);
422
+ for (const env of envs) {
423
+ const e = (0, config_1.resolveEnvironment)(cfg, env);
424
+ console.log(`\n ${env} — ${e.stackName}`);
425
+ if (e.outputs?.apiUrl)
426
+ console.log(` API: ${e.outputs.apiUrl}`);
427
+ if (e.outputs?.hostedUiUrl)
428
+ console.log(` Hosted UI: ${e.outputs.hostedUiUrl}`);
429
+ if (e.outputs?.userPoolId)
430
+ console.log(` User Pool: ${e.outputs.userPoolId}`);
431
+ if (e.outputs?.proxyUrl)
432
+ console.log(` Proxy: ${e.outputs.proxyUrl}`);
433
+ }
316
434
  // Only the Variables `init` could NOT wire itself remain a manual step.
317
- const pending = (0, config_1.repoVariables)(cfg).filter((v) => !wiredVariables.has(v.name));
435
+ const pending = [];
436
+ for (const env of envs) {
437
+ const e = (0, config_1.resolveEnvironment)(cfg, env);
438
+ for (const v of (0, config_1.repoVariablesForEnv)(cfg, env)) {
439
+ if (!wiredVariables.has(wiredKey(v.ghEnvironment, v.name)))
440
+ pending.push({
441
+ key: `${e.ghEnvironment}/${v.name}`,
442
+ value: v.value || `<${v.from}>`,
443
+ });
444
+ }
445
+ }
318
446
  if (pending.length) {
319
- console.log("\n Wire these GitHub repo Variables so CI can ship code (Settings → Variables):");
320
- console.log(table(pending.map((v) => ({
321
- key: v.name,
322
- value: v.value || `<${v.from}>`,
323
- }))));
447
+ console.log("\n Wire these per-environment GitHub Variables so CI can ship code\n" +
448
+ " (Settings → Environments → <env> → Variables):");
449
+ console.log(table(pending));
324
450
  }
325
451
  else {
326
- console.log("\n ✓ wired the GitHub repo Variables CI reads (AWS_DEPLOY_ROLE_ARN, AWS_REGION).");
452
+ console.log("\n ✓ wired every GitHub Environment's Variables (AWS_DEPLOY_ROLE_ARN, AWS_REGION).");
327
453
  }
328
- console.log(`\n Every push to ${cfg.github.branch} now ships code via \`rastack ci deploy\`` +
329
- ` (also runnable locally as \`rastack deploy\`).\n`);
454
+ const dev = (0, config_1.resolveEnvironment)(cfg, "dev");
455
+ const prod = (0, config_1.resolveEnvironment)(cfg, "prod");
456
+ console.log(`\n Pushes now ship code via \`rastack ci deploy\`:\n` +
457
+ ` ${prod.branch} → prod (${prod.ghEnvironment}) ${dev.branch} → dev (${dev.ghEnvironment})\n` +
458
+ ` Run one locally with \`rastack deploy --env dev\` / \`--env prod\`.\n`);
330
459
  }
331
460
  async function main(argv) {
332
461
  const flags = parseFlags(argv);
@@ -362,6 +491,10 @@ async function main(argv) {
362
491
  fs.writeFileSync(abs, f.content);
363
492
  written.push(f.path);
364
493
  }
494
+ // The `.env` (local app environment) + its `.gitignore` entry are standard —
495
+ // generated on every init, secrets preserved, refreshed with Outputs post-deploy.
496
+ for (const p of writeEnvAndGitignore(cfg))
497
+ written.push(p);
365
498
  console.log("");
366
499
  for (const w of written)
367
500
  console.log(` ✓ wrote ${w}`);
@@ -369,7 +502,7 @@ async function main(argv) {
369
502
  console.log(` • skipped ${s} (exists — pass --force to overwrite)`);
370
503
  // 2. Deploy (unless opted out).
371
504
  if (flags.print) {
372
- printPlan(cfg);
505
+ printPlan(cfg, flags.envs);
373
506
  return;
374
507
  }
375
508
  if (flags.noDeploy) {
@@ -378,14 +511,20 @@ async function main(argv) {
378
511
  return;
379
512
  }
380
513
  if ((0, io_1.canPrompt)()) {
381
- const ok = await confirm(`Deploy ${cfg.stackName} to AWS ${cfg.region} now? (requires the bootstrap stack to exist) [y/N]`);
514
+ const stacks = flags.envs
515
+ .map((env) => (0, config_1.resolveEnvironment)(cfg, env).stackName)
516
+ .join(" + ");
517
+ const ok = await confirm(`Deploy ${stacks} to AWS ${cfg.region} now? (creates the bootstrap stack if missing) [y/N]`);
382
518
  if (!ok) {
383
519
  console.log("\n Skipped the deploy. Re-run to deploy, or `rastack init --print` for the commands.\n");
384
520
  return;
385
521
  }
386
522
  }
387
- cfg = await standUp(cfg, (0, io_1.canPrompt)());
388
- printSummary(cfg);
523
+ cfg = await standUp(cfg, flags.envs, (0, io_1.canPrompt)());
524
+ // Refresh `.env` with the resolved stack Outputs (API URLs, pool/client ids).
525
+ for (const p of writeEnvAndGitignore(cfg))
526
+ console.log(` ✓ updated ${p}`);
527
+ printSummary(cfg, flags.envs);
389
528
  }
390
529
  if (require.main === module) {
391
530
  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":