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.
- package/CHANGELOG.md +14 -0
- package/admin/AdminApp.tsx +56 -3
- package/admin/index.tsx +8 -2
- package/admin/ui/icons.tsx +9 -0
- package/admin/ui/styles.ts +3 -0
- package/admin/useAdmin.ts +41 -4
- package/auth/provider.ts +71 -8
- package/dist/admin.js +17 -14
- package/dist/compile/analyze.js +11 -1
- package/dist/define/index.d.ts +8 -1
- package/dist/define/index.js +2 -2
- package/dist/define/manifest.js +7 -1
- 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 +57 -0
- package/dist/deploy/io.js +141 -0
- package/dist/deploy/workflows.d.ts +30 -0
- package/dist/deploy/workflows.js +117 -12
- package/dist/rastack-bootstrap.js +18 -47
- package/dist/rastack-ci.js +19 -1
- package/dist/rastack-dev.js +48 -2
- package/dist/rastack-init.d.ts +6 -3
- package/dist/rastack-init.js +231 -92
- package/dist/rastack.js +1 -0
- package/dist/templates/cloudformation/app.yaml +133 -2
- package/dist/wasm/rastack_wasm.js +1 -1
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/package.json +1 -1
- package/provider/provider.tsx +23 -0
- package/provider/types.ts +9 -0
- package/src/compile/analyze.ts +11 -1
- package/src/define/index.ts +5 -1
- package/src/define/manifest.ts +7 -1
- package/src/deploy/ci.ts +50 -16
- package/src/deploy/config.ts +340 -7
- package/src/deploy/io.ts +179 -2
- package/src/deploy/workflows.ts +122 -12
- package/src/rastack-bootstrap.ts +24 -69
- package/src/rastack-ci.ts +20 -2
- package/src/rastack-dev.ts +61 -6
- package/src/rastack-init.ts +283 -115
- package/src/rastack.ts +1 -0
package/src/rastack-init.ts
CHANGED
|
@@ -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.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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.
|
|
@@ -30,32 +33,46 @@ import * as path from "path";
|
|
|
30
33
|
import {
|
|
31
34
|
APP_STACK_OUTPUTS,
|
|
32
35
|
BOOTSTRAP_ARTIFACTS_OUTPUT,
|
|
33
|
-
BOOTSTRAP_TEMPLATES_OUTPUT,
|
|
34
36
|
DeployConfig,
|
|
35
37
|
DeployOutputs,
|
|
38
|
+
EnvName,
|
|
39
|
+
ENV_NAMES,
|
|
36
40
|
PartialDeployConfig,
|
|
41
|
+
bootstrapStackCommand,
|
|
37
42
|
bootstrapStackName,
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
deployStackCommandForEnv,
|
|
44
|
+
deployStackArgsForEnv,
|
|
45
|
+
ghEnvironmentCreateArgs,
|
|
40
46
|
ghVariableSetArgs,
|
|
41
47
|
mergeConfig,
|
|
42
48
|
normalizeConfig,
|
|
43
49
|
parseGitHubRemote,
|
|
44
|
-
|
|
50
|
+
repoVariablesForEnv,
|
|
51
|
+
resolveEnvironment,
|
|
45
52
|
validateConfig,
|
|
46
53
|
} from "./deploy/config";
|
|
47
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
ENV_PATH,
|
|
56
|
+
GITIGNORE_PATH,
|
|
57
|
+
applyEnvValues,
|
|
58
|
+
gitignoreWith,
|
|
59
|
+
scaffoldFiles,
|
|
60
|
+
} from "./deploy/workflows";
|
|
48
61
|
import {
|
|
49
62
|
ask,
|
|
50
63
|
awsAvailable,
|
|
64
|
+
awsIdentity,
|
|
51
65
|
awsRun,
|
|
52
66
|
canPrompt,
|
|
67
|
+
ensureBootstrapStack,
|
|
53
68
|
ghAuthLogin,
|
|
54
69
|
ghAuthenticated,
|
|
55
70
|
ghAvailable,
|
|
71
|
+
ghEnvironmentCreate,
|
|
56
72
|
gitRemoteUrl,
|
|
57
73
|
loadConfig,
|
|
58
74
|
resolveDeployAsset,
|
|
75
|
+
resolveTemplateFile,
|
|
59
76
|
saveConfig,
|
|
60
77
|
setRepoVariable,
|
|
61
78
|
stackOutput,
|
|
@@ -65,18 +82,30 @@ import {
|
|
|
65
82
|
|
|
66
83
|
interface Flags {
|
|
67
84
|
patch: PartialDeployConfig;
|
|
85
|
+
/** Which environments to stand up (default: both). */
|
|
86
|
+
envs: EnvName[];
|
|
68
87
|
force: boolean;
|
|
69
88
|
noWorkflows: boolean;
|
|
70
89
|
noDeploy: boolean;
|
|
71
90
|
print: boolean;
|
|
72
91
|
}
|
|
73
92
|
|
|
93
|
+
function parseEnvFlag(value: string): EnvName[] {
|
|
94
|
+
const v = value.trim().toLowerCase();
|
|
95
|
+
if (v === "all" || v === "") return [...ENV_NAMES];
|
|
96
|
+
if ((ENV_NAMES as readonly string[]).includes(v)) return [v as EnvName];
|
|
97
|
+
console.error(`Unknown --env: expected dev | prod | all.`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
74
101
|
function parseFlags(argv: string[]): Flags {
|
|
75
|
-
const patch: PartialDeployConfig = { github: {} };
|
|
102
|
+
const patch: PartialDeployConfig = { github: {}, environments: {} };
|
|
103
|
+
let envs: EnvName[] = [...ENV_NAMES];
|
|
76
104
|
let force = false;
|
|
77
105
|
let noWorkflows = false;
|
|
78
106
|
let noDeploy = false;
|
|
79
107
|
let print = false;
|
|
108
|
+
const devPatch = () => (patch.environments!.dev ??= {});
|
|
80
109
|
for (let i = 0; i < argv.length; i++) {
|
|
81
110
|
const a = argv[i];
|
|
82
111
|
const next = () => argv[++i] ?? "";
|
|
@@ -93,6 +122,17 @@ function parseFlags(argv: string[]): Flags {
|
|
|
93
122
|
case "--branch":
|
|
94
123
|
patch.github!.branch = next();
|
|
95
124
|
break;
|
|
125
|
+
case "--dev-branch":
|
|
126
|
+
devPatch().branch = next();
|
|
127
|
+
break;
|
|
128
|
+
case "--dev-proxy":
|
|
129
|
+
case "--dev-proxy-upstream":
|
|
130
|
+
// The dev external-API proxy upstream (a UAT/prod backend URL).
|
|
131
|
+
devPatch().proxyUpstream = next();
|
|
132
|
+
break;
|
|
133
|
+
case "--env":
|
|
134
|
+
envs = parseEnvFlag(next());
|
|
135
|
+
break;
|
|
96
136
|
case "--region":
|
|
97
137
|
patch.region = next();
|
|
98
138
|
break;
|
|
@@ -122,7 +162,9 @@ function parseFlags(argv: string[]): Flags {
|
|
|
122
162
|
}
|
|
123
163
|
}
|
|
124
164
|
}
|
|
125
|
-
|
|
165
|
+
// Drop the scratch environments container when nothing was set on it.
|
|
166
|
+
if (patch.environments && !patch.environments.dev) delete patch.environments;
|
|
167
|
+
return { patch, envs, force, noWorkflows, noDeploy, print };
|
|
126
168
|
}
|
|
127
169
|
|
|
128
170
|
/** Seed a config: existing file → flags → git remote → defaults. */
|
|
@@ -166,6 +208,38 @@ async function promptMissing(cfg: DeployConfig): Promise<DeployConfig> {
|
|
|
166
208
|
});
|
|
167
209
|
}
|
|
168
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Ensure `.env` is git-ignored and (re)write the generated `.env` with the
|
|
213
|
+
* current config, preserving any secrets a developer already added. Returns the
|
|
214
|
+
* repo-relative paths touched, for the written/skipped summary.
|
|
215
|
+
*/
|
|
216
|
+
function writeEnvAndGitignore(cfg: DeployConfig): string[] {
|
|
217
|
+
const touched: string[] = [];
|
|
218
|
+
|
|
219
|
+
// 1. git-ignore `.env` (append-merge; never duplicate or clobber).
|
|
220
|
+
const giAbs = path.join(process.cwd(), GITIGNORE_PATH);
|
|
221
|
+
const existingGi = fs.existsSync(giAbs)
|
|
222
|
+
? fs.readFileSync(giAbs, "utf-8")
|
|
223
|
+
: "";
|
|
224
|
+
const nextGi = gitignoreWith(existingGi, ENV_PATH);
|
|
225
|
+
if (nextGi !== existingGi) {
|
|
226
|
+
fs.writeFileSync(giAbs, nextGi);
|
|
227
|
+
touched.push(GITIGNORE_PATH);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 2. Upsert the generated RASTACK_* values into `.env` (secrets preserved).
|
|
231
|
+
const envAbs = path.join(process.cwd(), ENV_PATH);
|
|
232
|
+
const existingEnv = fs.existsSync(envAbs)
|
|
233
|
+
? fs.readFileSync(envAbs, "utf-8")
|
|
234
|
+
: "";
|
|
235
|
+
const nextEnv = applyEnvValues(existingEnv, cfg);
|
|
236
|
+
if (nextEnv !== existingEnv) {
|
|
237
|
+
fs.writeFileSync(envAbs, nextEnv);
|
|
238
|
+
touched.push(ENV_PATH);
|
|
239
|
+
}
|
|
240
|
+
return touched;
|
|
241
|
+
}
|
|
242
|
+
|
|
169
243
|
/** Yes/no confirmation with a default of "no" — gates the cloud-mutating steps. */
|
|
170
244
|
async function confirm(question: string): Promise<boolean> {
|
|
171
245
|
return withPrompt(async (rl) => {
|
|
@@ -181,24 +255,44 @@ function table(rows: { key: string; value: string }[]): string {
|
|
|
181
255
|
}
|
|
182
256
|
|
|
183
257
|
/**
|
|
184
|
-
* Read
|
|
185
|
-
* not resolvable yet (best-effort — a freshly created stack has them all).
|
|
258
|
+
* Read one environment's app.yaml stack Outputs into a `DeployOutputs`, skipping
|
|
259
|
+
* any not resolvable yet (best-effort — a freshly created stack has them all).
|
|
186
260
|
*/
|
|
187
|
-
function
|
|
261
|
+
function readEnvOutputs(cfg: DeployConfig, env: EnvName): DeployOutputs {
|
|
262
|
+
const stackName = resolveEnvironment(cfg, env).stackName;
|
|
188
263
|
const outputs: DeployOutputs = {};
|
|
189
264
|
for (const { key, field } of APP_STACK_OUTPUTS) {
|
|
190
|
-
const value = stackOutput(
|
|
265
|
+
const value = stackOutput(stackName, key, cfg.region);
|
|
191
266
|
if (value) outputs[field] = value;
|
|
192
267
|
}
|
|
193
268
|
return outputs;
|
|
194
269
|
}
|
|
195
270
|
|
|
271
|
+
/** Merge an environment's resolved outputs back into the config in the right slot. */
|
|
272
|
+
function withEnvOutputs(
|
|
273
|
+
cfg: DeployConfig,
|
|
274
|
+
env: EnvName,
|
|
275
|
+
outputs: DeployOutputs,
|
|
276
|
+
): DeployConfig {
|
|
277
|
+
return env === "prod"
|
|
278
|
+
? mergeConfig(cfg, { outputs })
|
|
279
|
+
: mergeConfig(cfg, { environments: { dev: { outputs } } });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Human label for an environment ("dev" / "prod") plus its branch. */
|
|
283
|
+
function envLabel(cfg: DeployConfig, env: EnvName): string {
|
|
284
|
+
const e = resolveEnvironment(cfg, env);
|
|
285
|
+
return `${env} (${e.branch} → ${e.ghEnvironment})`;
|
|
286
|
+
}
|
|
287
|
+
|
|
196
288
|
/**
|
|
197
289
|
* Print the exact commands `init` would run, without touching the cloud
|
|
198
|
-
* (`--print`).
|
|
199
|
-
*
|
|
290
|
+
* (`--print`). init stands up *both* stacks, so both `aws cloudformation deploy`
|
|
291
|
+
* commands are shown: the tier-0 bootstrap stack (created only when missing) and
|
|
292
|
+
* the per-app backend. The artifacts bucket is read from an existing bootstrap
|
|
293
|
+
* stack when the AWS CLI is available, else shown as a placeholder.
|
|
200
294
|
*/
|
|
201
|
-
function printPlan(cfg: DeployConfig): void {
|
|
295
|
+
function printPlan(cfg: DeployConfig, envs: EnvName[]): void {
|
|
202
296
|
const bucket = awsAvailable()
|
|
203
297
|
? stackOutput(
|
|
204
298
|
bootstrapStackName(cfg),
|
|
@@ -209,36 +303,50 @@ function printPlan(cfg: DeployConfig): void {
|
|
|
209
303
|
const withBucket = mergeConfig(cfg, {
|
|
210
304
|
artifactsBucket: bucket ?? "<ArtifactsBucketName from bootstrap stack>",
|
|
211
305
|
});
|
|
306
|
+
const bootstrapTemplate = resolveDeployAsset("bootstrap.yaml") ?? undefined;
|
|
212
307
|
const appTemplate = resolveDeployAsset("app.yaml") ?? undefined;
|
|
213
|
-
console.log(`\n Deployment plan (${
|
|
214
|
-
console.log(
|
|
215
|
-
` ── Prerequisite: the tier-0 bootstrap stack (${bootstrapStackName(cfg)}) must exist ──`,
|
|
216
|
-
);
|
|
308
|
+
console.log(`\n Deployment plan (${envs.join(" + ")} @ ${cfg.region})\n`);
|
|
217
309
|
console.log(
|
|
218
|
-
`
|
|
310
|
+
` ── 1. Tier-0 bootstrap stack (${bootstrapStackName(cfg)}) — created only when missing ──`,
|
|
219
311
|
);
|
|
220
|
-
console.log("
|
|
221
|
-
console.log("\n " + deployStackCommand(withBucket, appTemplate) + "\n");
|
|
312
|
+
console.log("\n " + bootstrapStackCommand(cfg, false, bootstrapTemplate) + "\n");
|
|
222
313
|
console.log(
|
|
223
|
-
" ── 2.
|
|
314
|
+
" ── 2. Per-app backend (app.yaml) — one stack per environment ──",
|
|
224
315
|
);
|
|
316
|
+
for (const env of envs) {
|
|
317
|
+
const e = resolveEnvironment(withBucket, env);
|
|
318
|
+
console.log(`\n # ${envLabel(withBucket, env)} — stack ${e.stackName}`);
|
|
319
|
+
console.log(" " + deployStackCommandForEnv(withBucket, env, appTemplate));
|
|
320
|
+
}
|
|
225
321
|
console.log(
|
|
226
|
-
|
|
227
|
-
repoVariables(cfg).map((v) => ({
|
|
228
|
-
key: v.name,
|
|
229
|
-
value: v.value || `<${v.from}>`,
|
|
230
|
-
})),
|
|
231
|
-
) + "\n",
|
|
322
|
+
"\n ── 2. Wire each GitHub Environment's Variables from its stack Outputs ──",
|
|
232
323
|
);
|
|
324
|
+
for (const env of envs) {
|
|
325
|
+
const e = resolveEnvironment(cfg, env);
|
|
326
|
+
console.log(`\n # GitHub Environment: ${e.ghEnvironment}`);
|
|
327
|
+
console.log(
|
|
328
|
+
table(
|
|
329
|
+
repoVariablesForEnv(cfg, env).map((v) => ({
|
|
330
|
+
key: v.name,
|
|
331
|
+
value: v.value || `<${v.from}>`,
|
|
332
|
+
})),
|
|
333
|
+
),
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
console.log("");
|
|
233
337
|
}
|
|
234
338
|
|
|
235
339
|
/**
|
|
236
|
-
* Stand the infrastructure up:
|
|
237
|
-
*
|
|
238
|
-
*
|
|
340
|
+
* Stand the infrastructure up: verify the AWS CLI is logged in, ensure the
|
|
341
|
+
* tier-0 bootstrap stack (creating it when missing), upload the Lambda
|
|
342
|
+
* placeholder, deploy app.yaml, and read the Outputs back into the config.
|
|
343
|
+
* `init` is the single command that stands up *everything* — there is no
|
|
344
|
+
* separate manual bootstrap step. Returns the config enriched with the resolved
|
|
345
|
+
* buckets + outputs.
|
|
239
346
|
*/
|
|
240
347
|
async function standUp(
|
|
241
348
|
cfg: DeployConfig,
|
|
349
|
+
envs: EnvName[],
|
|
242
350
|
interactive: boolean,
|
|
243
351
|
): Promise<DeployConfig> {
|
|
244
352
|
if (!awsAvailable()) {
|
|
@@ -249,36 +357,35 @@ async function standUp(
|
|
|
249
357
|
process.exit(1);
|
|
250
358
|
}
|
|
251
359
|
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// so a missing bootstrap stack is a clear, actionable error, not an implicit
|
|
258
|
-
// account mutation buried inside an app deploy.
|
|
259
|
-
const artifactsBucket = stackOutput(
|
|
260
|
-
bootstrapStackName(cfg),
|
|
261
|
-
BOOTSTRAP_ARTIFACTS_OUTPUT,
|
|
262
|
-
cfg.region,
|
|
263
|
-
);
|
|
264
|
-
const templatesBucket = stackOutput(
|
|
265
|
-
bootstrapStackName(cfg),
|
|
266
|
-
BOOTSTRAP_TEMPLATES_OUTPUT,
|
|
267
|
-
cfg.region,
|
|
268
|
-
);
|
|
269
|
-
if (!artifactsBucket) {
|
|
360
|
+
// Verify the AWS CLI is actually logged in before creating any stacks, so a
|
|
361
|
+
// missing/expired credential fails fast with an actionable message rather than
|
|
362
|
+
// a cryptic `cloudformation deploy` error partway through.
|
|
363
|
+
const account = awsIdentity(cfg.region);
|
|
364
|
+
if (!account) {
|
|
270
365
|
console.error(
|
|
271
|
-
|
|
272
|
-
"
|
|
273
|
-
"
|
|
274
|
-
"
|
|
275
|
-
|
|
366
|
+
" ✗ the AWS CLI has no usable credentials — it is not logged in.\n" +
|
|
367
|
+
" Configure credentials, then re-run `rastack init`:\n" +
|
|
368
|
+
" aws configure # static access key / secret\n" +
|
|
369
|
+
" aws sso login # or an existing SSO session\n" +
|
|
370
|
+
" Or re-run with --print to see the commands, or --no-deploy to skip.",
|
|
276
371
|
);
|
|
277
372
|
process.exit(1);
|
|
278
373
|
}
|
|
374
|
+
console.log(` ✓ AWS account ${account} (${cfg.region})`);
|
|
375
|
+
|
|
376
|
+
// 1. Ensure the tier-0 bootstrap stack exists — the S3 buckets that host the
|
|
377
|
+
// templates + Lambda placeholder and the GitHub OIDC provider. init creates
|
|
378
|
+
// it when missing (idempotent for an existing one), so a single command
|
|
379
|
+
// takes an account+repo from nothing to a deployed backend.
|
|
380
|
+
const { artifactsBucket, templatesBucket } = await ensureBootstrapStack(
|
|
381
|
+
cfg,
|
|
382
|
+
).catch((err: Error) => {
|
|
383
|
+
console.error(`\n ✗ ${err.message}`);
|
|
384
|
+
process.exit(1);
|
|
385
|
+
});
|
|
279
386
|
console.log(` ✓ artifacts bucket: ${artifactsBucket}`);
|
|
280
387
|
|
|
281
|
-
// 2. Upload the placeholder the function is created from.
|
|
388
|
+
// 2. Upload the placeholder the function is created from (shared by all envs).
|
|
282
389
|
console.log("\n▸ Uploading the Lambda placeholder bundle");
|
|
283
390
|
if (!uploadPlaceholder(artifactsBucket, cfg.region)) {
|
|
284
391
|
console.error(
|
|
@@ -287,38 +394,40 @@ async function standUp(
|
|
|
287
394
|
process.exit(1);
|
|
288
395
|
}
|
|
289
396
|
|
|
290
|
-
// 3. Deploy the per-app backend with the resolved bucket wired in.
|
|
291
397
|
cfg = mergeConfig(cfg, {
|
|
292
398
|
artifactsBucket,
|
|
293
399
|
templatesBucket: templatesBucket ?? undefined,
|
|
294
400
|
});
|
|
295
|
-
const appTemplate =
|
|
401
|
+
const appTemplate = await resolveTemplateFile("app.yaml");
|
|
296
402
|
if (!appTemplate) {
|
|
297
403
|
console.error(
|
|
298
|
-
" ✗ app.yaml template not found in the rastack package
|
|
299
|
-
" (`npm i -g rastack`)
|
|
300
|
-
);
|
|
301
|
-
process.exit(1);
|
|
302
|
-
}
|
|
303
|
-
console.log(`\n▸ Deploying ${cfg.stackName}`);
|
|
304
|
-
try {
|
|
305
|
-
awsRun(deployStackArgs(cfg, appTemplate));
|
|
306
|
-
} catch (err) {
|
|
307
|
-
console.error(
|
|
308
|
-
`\n ✗ aws cloudformation deploy failed for ${cfg.stackName}.\n ${(err as Error).message}`,
|
|
404
|
+
" ✗ app.yaml template not found in the rastack package and could not be\n" +
|
|
405
|
+
" downloaded from reactapistack.com. Reinstall rastack (`npm i -g rastack`).",
|
|
309
406
|
);
|
|
310
407
|
process.exit(1);
|
|
311
408
|
}
|
|
312
409
|
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
410
|
+
// 3. Deploy one stack per environment — each with its own Cognito pool,
|
|
411
|
+
// warehouse, Lambda and API — reading the Outputs back into the config.
|
|
412
|
+
for (const env of envs) {
|
|
413
|
+
const stackName = resolveEnvironment(cfg, env).stackName;
|
|
414
|
+
console.log(`\n▸ Deploying ${envLabel(cfg, env)} — ${stackName}`);
|
|
415
|
+
try {
|
|
416
|
+
awsRun(deployStackArgsForEnv(cfg, env, appTemplate));
|
|
417
|
+
} catch (err) {
|
|
418
|
+
console.error(
|
|
419
|
+
`\n ✗ aws cloudformation deploy failed for ${stackName}.\n ${(err as Error).message}`,
|
|
420
|
+
);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
cfg = withEnvOutputs(cfg, env, readEnvOutputs(cfg, env));
|
|
424
|
+
saveConfig(cfg);
|
|
425
|
+
}
|
|
317
426
|
|
|
318
|
-
//
|
|
319
|
-
// push actually ships. Without this the deploy-backend workflow's
|
|
320
|
-
// vars.AWS_DEPLOY_ROLE_ARN != ''` guard silently skips every run.
|
|
321
|
-
|
|
427
|
+
// 4. Connect the GitHub CLI and wire each environment's scoped Variables, so
|
|
428
|
+
// the FIRST push actually ships. Without this the deploy-backend workflow's
|
|
429
|
+
// `if: vars.AWS_DEPLOY_ROLE_ARN != ''` guard silently skips every run.
|
|
430
|
+
wireEnvironments(cfg, envs, interactive);
|
|
322
431
|
return cfg;
|
|
323
432
|
}
|
|
324
433
|
|
|
@@ -333,20 +442,37 @@ async function standUp(
|
|
|
333
442
|
* (and isn't) done.
|
|
334
443
|
*/
|
|
335
444
|
const wiredVariables = new Set<string>();
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
445
|
+
|
|
446
|
+
/** Key a wired Variable by its GitHub Environment so dev/prod don't collide. */
|
|
447
|
+
function wiredKey(ghEnvironment: string | undefined, name: string): string {
|
|
448
|
+
return `${ghEnvironment ?? ""}:${name}`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Create each environment's GitHub Actions Environment and wire its scoped
|
|
453
|
+
* `AWS_DEPLOY_ROLE_ARN` / `AWS_REGION` Variables via `gh`, so the GitHub →
|
|
454
|
+
* Lambda pipeline is live for both dev and prod the moment `init` finishes.
|
|
455
|
+
*/
|
|
456
|
+
function wireEnvironments(
|
|
457
|
+
cfg: DeployConfig,
|
|
458
|
+
envs: EnvName[],
|
|
459
|
+
interactive: boolean,
|
|
460
|
+
): void {
|
|
461
|
+
const anyValue = envs.some((env) =>
|
|
462
|
+
repoVariablesForEnv(cfg, env).some((v) => v.value),
|
|
463
|
+
);
|
|
464
|
+
if (!anyValue) return;
|
|
339
465
|
|
|
340
466
|
if (!ghAvailable()) {
|
|
341
467
|
console.log(
|
|
342
|
-
"\n▸ GitHub CLI (`gh`) not found — skipping
|
|
468
|
+
"\n▸ GitHub CLI (`gh`) not found — skipping environment + Variable wiring.\n" +
|
|
343
469
|
" Install `gh` (https://cli.github.com) and re-run, or wire the\n" +
|
|
344
470
|
" Variables below by hand so pushes deploy.",
|
|
345
471
|
);
|
|
346
472
|
return;
|
|
347
473
|
}
|
|
348
474
|
|
|
349
|
-
// Connect `gh` to GitHub if it isn't already, so it can
|
|
475
|
+
// Connect `gh` to GitHub if it isn't already, so it can create environments.
|
|
350
476
|
if (!ghAuthenticated()) {
|
|
351
477
|
if (interactive) {
|
|
352
478
|
console.log(
|
|
@@ -356,53 +482,86 @@ function wireRepoVariables(cfg: DeployConfig, interactive: boolean): void {
|
|
|
356
482
|
}
|
|
357
483
|
if (!ghAuthenticated()) {
|
|
358
484
|
console.log(
|
|
359
|
-
"\n▸ GitHub CLI is not connected — skipping
|
|
485
|
+
"\n▸ GitHub CLI is not connected — skipping environment wiring.\n" +
|
|
360
486
|
" Run `gh auth login`, then wire the Variables below by hand.",
|
|
361
487
|
);
|
|
362
488
|
return;
|
|
363
489
|
}
|
|
364
490
|
}
|
|
365
491
|
|
|
366
|
-
console.log(
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
492
|
+
console.log(
|
|
493
|
+
"\n▸ Creating GitHub Environments + wiring their Variables (via `gh`)",
|
|
494
|
+
);
|
|
495
|
+
for (const env of envs) {
|
|
496
|
+
const e = resolveEnvironment(cfg, env);
|
|
497
|
+
// Create the Environment first — a Variable can only be scoped to one that
|
|
498
|
+
// already exists. Idempotent (PUT), so re-running init is safe.
|
|
499
|
+
if (!ghEnvironmentCreate(ghEnvironmentCreateArgs(cfg, e.ghEnvironment))) {
|
|
500
|
+
console.log(
|
|
501
|
+
` • could not create GitHub Environment ${e.ghEnvironment} — create it by hand`,
|
|
502
|
+
);
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
for (const v of repoVariablesForEnv(cfg, env).filter((x) => x.value)) {
|
|
506
|
+
const ok = setRepoVariable(
|
|
507
|
+
ghVariableSetArgs(cfg, v.name, v.value, v.ghEnvironment),
|
|
508
|
+
);
|
|
509
|
+
if (ok) {
|
|
510
|
+
wiredVariables.add(wiredKey(v.ghEnvironment, v.name));
|
|
511
|
+
console.log(` ✓ set ${e.ghEnvironment}/${v.name}`);
|
|
512
|
+
} else {
|
|
513
|
+
console.log(
|
|
514
|
+
` • could not set ${e.ghEnvironment}/${v.name} — wire it by hand (below)`,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
374
517
|
}
|
|
375
518
|
}
|
|
376
519
|
}
|
|
377
520
|
|
|
378
|
-
function printSummary(cfg: DeployConfig): void {
|
|
379
|
-
console.log(
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
521
|
+
function printSummary(cfg: DeployConfig, envs: EnvName[]): void {
|
|
522
|
+
console.log(`\n ✓ ${cfg.app} is deployed @ ${cfg.region}`);
|
|
523
|
+
for (const env of envs) {
|
|
524
|
+
const e = resolveEnvironment(cfg, env);
|
|
525
|
+
console.log(`\n ${env} — ${e.stackName}`);
|
|
526
|
+
if (e.outputs?.apiUrl) console.log(` API: ${e.outputs.apiUrl}`);
|
|
527
|
+
if (e.outputs?.hostedUiUrl)
|
|
528
|
+
console.log(` Hosted UI: ${e.outputs.hostedUiUrl}`);
|
|
529
|
+
if (e.outputs?.userPoolId)
|
|
530
|
+
console.log(` User Pool: ${e.outputs.userPoolId}`);
|
|
531
|
+
if (e.outputs?.proxyUrl)
|
|
532
|
+
console.log(` Proxy: ${e.outputs.proxyUrl}`);
|
|
533
|
+
}
|
|
383
534
|
|
|
384
535
|
// Only the Variables `init` could NOT wire itself remain a manual step.
|
|
385
|
-
const pending
|
|
536
|
+
const pending: { key: string; value: string }[] = [];
|
|
537
|
+
for (const env of envs) {
|
|
538
|
+
const e = resolveEnvironment(cfg, env);
|
|
539
|
+
for (const v of repoVariablesForEnv(cfg, env)) {
|
|
540
|
+
if (!wiredVariables.has(wiredKey(v.ghEnvironment, v.name)))
|
|
541
|
+
pending.push({
|
|
542
|
+
key: `${e.ghEnvironment}/${v.name}`,
|
|
543
|
+
value: v.value || `<${v.from}>`,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
386
547
|
if (pending.length) {
|
|
387
548
|
console.log(
|
|
388
|
-
"\n Wire these GitHub
|
|
389
|
-
|
|
390
|
-
console.log(
|
|
391
|
-
table(
|
|
392
|
-
pending.map((v) => ({
|
|
393
|
-
key: v.name,
|
|
394
|
-
value: v.value || `<${v.from}>`,
|
|
395
|
-
})),
|
|
396
|
-
),
|
|
549
|
+
"\n Wire these per-environment GitHub Variables so CI can ship code\n" +
|
|
550
|
+
" (Settings → Environments → <env> → Variables):",
|
|
397
551
|
);
|
|
552
|
+
console.log(table(pending));
|
|
398
553
|
} else {
|
|
399
554
|
console.log(
|
|
400
|
-
"\n ✓ wired
|
|
555
|
+
"\n ✓ wired every GitHub Environment's Variables (AWS_DEPLOY_ROLE_ARN, AWS_REGION).",
|
|
401
556
|
);
|
|
402
557
|
}
|
|
558
|
+
|
|
559
|
+
const dev = resolveEnvironment(cfg, "dev");
|
|
560
|
+
const prod = resolveEnvironment(cfg, "prod");
|
|
403
561
|
console.log(
|
|
404
|
-
`\n
|
|
405
|
-
`
|
|
562
|
+
`\n Pushes now ship code via \`rastack ci deploy\`:\n` +
|
|
563
|
+
` ${prod.branch} → prod (${prod.ghEnvironment}) ${dev.branch} → dev (${dev.ghEnvironment})\n` +
|
|
564
|
+
` Run one locally with \`rastack deploy --env dev\` / \`--env prod\`.\n`,
|
|
406
565
|
);
|
|
407
566
|
}
|
|
408
567
|
|
|
@@ -446,6 +605,10 @@ async function main(argv: string[]): Promise<void> {
|
|
|
446
605
|
fs.writeFileSync(abs, f.content);
|
|
447
606
|
written.push(f.path);
|
|
448
607
|
}
|
|
608
|
+
// The `.env` (local app environment) + its `.gitignore` entry are standard —
|
|
609
|
+
// generated on every init, secrets preserved, refreshed with Outputs post-deploy.
|
|
610
|
+
for (const p of writeEnvAndGitignore(cfg)) written.push(p);
|
|
611
|
+
|
|
449
612
|
console.log("");
|
|
450
613
|
for (const w of written) console.log(` ✓ wrote ${w}`);
|
|
451
614
|
for (const s of skipped)
|
|
@@ -453,7 +616,7 @@ async function main(argv: string[]): Promise<void> {
|
|
|
453
616
|
|
|
454
617
|
// 2. Deploy (unless opted out).
|
|
455
618
|
if (flags.print) {
|
|
456
|
-
printPlan(cfg);
|
|
619
|
+
printPlan(cfg, flags.envs);
|
|
457
620
|
return;
|
|
458
621
|
}
|
|
459
622
|
if (flags.noDeploy) {
|
|
@@ -465,8 +628,11 @@ async function main(argv: string[]): Promise<void> {
|
|
|
465
628
|
}
|
|
466
629
|
|
|
467
630
|
if (canPrompt()) {
|
|
631
|
+
const stacks = flags.envs
|
|
632
|
+
.map((env) => resolveEnvironment(cfg, env).stackName)
|
|
633
|
+
.join(" + ");
|
|
468
634
|
const ok = await confirm(
|
|
469
|
-
`Deploy ${
|
|
635
|
+
`Deploy ${stacks} to AWS ${cfg.region} now? (creates the bootstrap stack if missing) [y/N]`,
|
|
470
636
|
);
|
|
471
637
|
if (!ok) {
|
|
472
638
|
console.log(
|
|
@@ -476,8 +642,10 @@ async function main(argv: string[]): Promise<void> {
|
|
|
476
642
|
}
|
|
477
643
|
}
|
|
478
644
|
|
|
479
|
-
cfg = await standUp(cfg, canPrompt());
|
|
480
|
-
|
|
645
|
+
cfg = await standUp(cfg, flags.envs, canPrompt());
|
|
646
|
+
// Refresh `.env` with the resolved stack Outputs (API URLs, pool/client ids).
|
|
647
|
+
for (const p of writeEnvAndGitignore(cfg)) console.log(` ✓ updated ${p}`);
|
|
648
|
+
printSummary(cfg, flags.envs);
|
|
481
649
|
}
|
|
482
650
|
|
|
483
651
|
if (require.main === module) {
|
package/src/rastack.ts
CHANGED
|
@@ -137,6 +137,7 @@ switch (command) {
|
|
|
137
137
|
case "deploy":
|
|
138
138
|
// Ship code to the Lambda — an alias for the CI code deploy. Infrastructure
|
|
139
139
|
// is stood up once by `rastack init`; `deploy` only ever ships code.
|
|
140
|
+
// `--env dev|prod` picks which environment's function to update.
|
|
140
141
|
run("rastack-ci.js", ["deploy", ...rest]);
|
|
141
142
|
break;
|
|
142
143
|
case "ci":
|