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/dist/deploy/config.js
CHANGED
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
* thin IO wrappers that read/write the file and shell out.
|
|
19
19
|
*/
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.APP_STACK_OUTPUTS = exports.BOOTSTRAP_TEMPLATES_OUTPUT = exports.BOOTSTRAP_ARTIFACTS_OUTPUT = exports.BOOTSTRAP_TEMPLATE_FILE = exports.APP_TEMPLATE_FILE = exports.REGIONS_WITHOUT_COGNITO_ASF = exports.DEFAULTS = exports.DEFAULT_LAMBDA = void 0;
|
|
21
|
+
exports.APP_STACK_OUTPUTS = exports.BOOTSTRAP_TEMPLATES_OUTPUT = exports.BOOTSTRAP_ARTIFACTS_OUTPUT = exports.BOOTSTRAP_TEMPLATE_FILE = exports.APP_TEMPLATE_FILE = exports.REGIONS_WITHOUT_COGNITO_ASF = exports.DEFAULTS = exports.DEFAULT_LAMBDA = exports.PROD_GH_ENVIRONMENT = exports.DEV_DEFAULTS = exports.ENV_NAMES = void 0;
|
|
22
|
+
exports.envAppName = envAppName;
|
|
23
|
+
exports.resolveEnvironment = resolveEnvironment;
|
|
24
|
+
exports.resolveEnvironments = resolveEnvironments;
|
|
25
|
+
exports.cognitoRuntimeConfig = cognitoRuntimeConfig;
|
|
22
26
|
exports.normalizeConfig = normalizeConfig;
|
|
23
27
|
exports.validateConfig = validateConfig;
|
|
24
28
|
exports.validateBootstrapConfig = validateBootstrapConfig;
|
|
@@ -26,15 +30,83 @@ exports.mergeConfig = mergeConfig;
|
|
|
26
30
|
exports.parseGitHubRemote = parseGitHubRemote;
|
|
27
31
|
exports.cognitoAdvancedSecuritySupported = cognitoAdvancedSecuritySupported;
|
|
28
32
|
exports.stackParameters = stackParameters;
|
|
33
|
+
exports.stackParametersForEnv = stackParametersForEnv;
|
|
29
34
|
exports.deployStackArgs = deployStackArgs;
|
|
30
35
|
exports.deployStackCommand = deployStackCommand;
|
|
36
|
+
exports.deployStackArgsForEnv = deployStackArgsForEnv;
|
|
37
|
+
exports.deployStackCommandForEnv = deployStackCommandForEnv;
|
|
31
38
|
exports.bootstrapStackName = bootstrapStackName;
|
|
32
39
|
exports.bootstrapStackParameters = bootstrapStackParameters;
|
|
33
40
|
exports.bootstrapStackArgs = bootstrapStackArgs;
|
|
34
41
|
exports.bootstrapStackCommand = bootstrapStackCommand;
|
|
35
42
|
exports.repoVariables = repoVariables;
|
|
43
|
+
exports.repoVariablesForEnv = repoVariablesForEnv;
|
|
36
44
|
exports.ghVariableSetArgs = ghVariableSetArgs;
|
|
45
|
+
exports.ghEnvironmentCreateArgs = ghEnvironmentCreateArgs;
|
|
37
46
|
exports.lambdaEnv = lambdaEnv;
|
|
47
|
+
exports.ENV_NAMES = ["prod", "dev"];
|
|
48
|
+
/** Defaults for the dev environment when `environments.dev` is unset. */
|
|
49
|
+
exports.DEV_DEFAULTS = {
|
|
50
|
+
branch: "development",
|
|
51
|
+
ghEnvironment: "development",
|
|
52
|
+
namespace: "development",
|
|
53
|
+
};
|
|
54
|
+
/** The GitHub Actions Environment name the prod deploy uses. */
|
|
55
|
+
exports.PROD_GH_ENVIRONMENT = "production";
|
|
56
|
+
/**
|
|
57
|
+
* The app.yaml `AppName` for an environment — the infra namespace. Dev glues a
|
|
58
|
+
* `dev` suffix on so its stack's buckets/roles/pools/SSM are physically distinct
|
|
59
|
+
* from prod's without a hyphen (Cognito `AppName`/`IdentityPoolName` forbid one).
|
|
60
|
+
*/
|
|
61
|
+
function envAppName(cfg, env) {
|
|
62
|
+
return env === "dev" ? `${cfg.app}dev` : cfg.app;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve one environment to its full definition: the prod environment inherits
|
|
66
|
+
* the top-level branch/stack/namespace/outputs, the dev environment defaults to
|
|
67
|
+
* the `development` branch and a `-dev` stack, and anything under
|
|
68
|
+
* `cfg.environments[env]` overrides those.
|
|
69
|
+
*/
|
|
70
|
+
function resolveEnvironment(cfg, env) {
|
|
71
|
+
const o = cfg.environments?.[env] ?? {};
|
|
72
|
+
if (env === "prod") {
|
|
73
|
+
return {
|
|
74
|
+
name: "prod",
|
|
75
|
+
branch: o.branch ?? cfg.github.branch,
|
|
76
|
+
ghEnvironment: o.ghEnvironment ?? exports.PROD_GH_ENVIRONMENT,
|
|
77
|
+
appName: cfg.app,
|
|
78
|
+
stackName: o.stackName ?? cfg.stackName,
|
|
79
|
+
namespace: o.namespace ?? cfg.namespace,
|
|
80
|
+
proxyUpstream: o.proxyUpstream ?? "",
|
|
81
|
+
outputs: o.outputs ?? cfg.outputs,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
name: "dev",
|
|
86
|
+
branch: o.branch ?? exports.DEV_DEFAULTS.branch,
|
|
87
|
+
ghEnvironment: o.ghEnvironment ?? exports.DEV_DEFAULTS.ghEnvironment,
|
|
88
|
+
appName: envAppName(cfg, "dev"),
|
|
89
|
+
stackName: o.stackName ?? (cfg.stackName ? `${cfg.stackName}-dev` : ""),
|
|
90
|
+
namespace: o.namespace ?? exports.DEV_DEFAULTS.namespace,
|
|
91
|
+
proxyUpstream: o.proxyUpstream ?? "",
|
|
92
|
+
outputs: o.outputs,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** Both environments, prod first, fully resolved. */
|
|
96
|
+
function resolveEnvironments(cfg) {
|
|
97
|
+
return exports.ENV_NAMES.map((e) => resolveEnvironment(cfg, e));
|
|
98
|
+
}
|
|
99
|
+
function cognitoRuntimeConfig(cfg, env) {
|
|
100
|
+
const o = resolveEnvironment(cfg, env).outputs;
|
|
101
|
+
if (!o?.userPoolClientId || !o?.hostedUiUrl)
|
|
102
|
+
return null;
|
|
103
|
+
return {
|
|
104
|
+
userPoolId: o.userPoolId,
|
|
105
|
+
clientId: o.userPoolClientId,
|
|
106
|
+
hostedUiUrl: o.hostedUiUrl,
|
|
107
|
+
region: cfg.region,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
38
110
|
exports.DEFAULT_LAMBDA = {
|
|
39
111
|
bin: "bootstrap",
|
|
40
112
|
arch: "arm64",
|
|
@@ -82,8 +154,36 @@ function normalizeConfig(partial = {}) {
|
|
|
82
154
|
artifactsBucket: partial.artifactsBucket?.trim() || undefined,
|
|
83
155
|
tokenUse: partial.tokenUse ?? exports.DEFAULTS.tokenUse,
|
|
84
156
|
outputs: partial.outputs ? { ...partial.outputs } : undefined,
|
|
157
|
+
environments: normalizeEnvironments(partial.environments),
|
|
85
158
|
};
|
|
86
159
|
}
|
|
160
|
+
/** Copy through per-environment overrides, dropping an empty map to undefined. */
|
|
161
|
+
function normalizeEnvironments(envs) {
|
|
162
|
+
if (!envs)
|
|
163
|
+
return undefined;
|
|
164
|
+
const out = {};
|
|
165
|
+
for (const name of exports.ENV_NAMES) {
|
|
166
|
+
const o = envs[name];
|
|
167
|
+
if (!o)
|
|
168
|
+
continue;
|
|
169
|
+
const entry = {};
|
|
170
|
+
if (o.branch?.trim())
|
|
171
|
+
entry.branch = o.branch.trim();
|
|
172
|
+
if (o.ghEnvironment?.trim())
|
|
173
|
+
entry.ghEnvironment = o.ghEnvironment.trim();
|
|
174
|
+
if (o.stackName?.trim())
|
|
175
|
+
entry.stackName = o.stackName.trim();
|
|
176
|
+
if (o.namespace?.trim())
|
|
177
|
+
entry.namespace = o.namespace.trim();
|
|
178
|
+
if (o.proxyUpstream?.trim())
|
|
179
|
+
entry.proxyUpstream = o.proxyUpstream.trim();
|
|
180
|
+
if (o.outputs)
|
|
181
|
+
entry.outputs = { ...o.outputs };
|
|
182
|
+
if (Object.keys(entry).length)
|
|
183
|
+
out[name] = entry;
|
|
184
|
+
}
|
|
185
|
+
return Object.keys(out).length ? out : undefined;
|
|
186
|
+
}
|
|
87
187
|
const SLUG = /^[a-z0-9]{1,20}$/;
|
|
88
188
|
const REGION = /^[a-z]{2}-[a-z]+-\d+$/;
|
|
89
189
|
const GH_NAME = /^[A-Za-z0-9_.-]+$/;
|
|
@@ -99,6 +199,12 @@ function validateConfig(cfg) {
|
|
|
99
199
|
errors.push("app is required (a short slug, e.g. `airline`).");
|
|
100
200
|
else if (!SLUG.test(cfg.app))
|
|
101
201
|
errors.push(`app "${cfg.app}" must match ${SLUG} (lowercase letters/digits, 1–20).`);
|
|
202
|
+
// The dev environment appends "dev" to the app slug for its infra namespace
|
|
203
|
+
// (buckets/roles/pools/SSM). That derived name must still be a valid slug, so
|
|
204
|
+
// the app slug is effectively capped at 17 chars once a dev env is in play.
|
|
205
|
+
else if (!SLUG.test(envAppName(cfg, "dev")))
|
|
206
|
+
errors.push(`app "${cfg.app}" is too long: the dev environment uses "${envAppName(cfg, "dev")}" ` +
|
|
207
|
+
`for its infra names, which must still match ${SLUG} (so app ≤ 17 chars).`);
|
|
102
208
|
if (!SLUG.test(cfg.namePrefix))
|
|
103
209
|
errors.push(`namePrefix "${cfg.namePrefix}" must match ${SLUG}.`);
|
|
104
210
|
if (!cfg.region)
|
|
@@ -158,8 +264,27 @@ function mergeConfig(base, patch) {
|
|
|
158
264
|
github: { ...base.github, ...patch.github },
|
|
159
265
|
lambda: { ...base.lambda, ...patch.lambda },
|
|
160
266
|
outputs: { ...base.outputs, ...patch.outputs },
|
|
267
|
+
environments: mergeEnvironments(base.environments, patch.environments),
|
|
161
268
|
});
|
|
162
269
|
}
|
|
270
|
+
/** Deep-merge per-environment overrides (per env, and per env's outputs). */
|
|
271
|
+
function mergeEnvironments(base, patch) {
|
|
272
|
+
if (!base && !patch)
|
|
273
|
+
return undefined;
|
|
274
|
+
const out = {};
|
|
275
|
+
for (const name of exports.ENV_NAMES) {
|
|
276
|
+
const b = base?.[name];
|
|
277
|
+
const p = patch?.[name];
|
|
278
|
+
if (!b && !p)
|
|
279
|
+
continue;
|
|
280
|
+
out[name] = {
|
|
281
|
+
...b,
|
|
282
|
+
...p,
|
|
283
|
+
outputs: b?.outputs || p?.outputs ? { ...b?.outputs, ...p?.outputs } : undefined,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
return Object.keys(out).length ? out : undefined;
|
|
287
|
+
}
|
|
163
288
|
/**
|
|
164
289
|
* Recover `{ owner, repo }` from a GitHub remote URL — both the SSH
|
|
165
290
|
* (`git@github.com:owner/repo.git`) and HTTPS
|
|
@@ -232,6 +357,36 @@ function stackParameters(cfg) {
|
|
|
232
357
|
params.push({ key: "AdvancedSecurity", value: "OFF" });
|
|
233
358
|
return params;
|
|
234
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* The app.yaml `--parameter-overrides` for a **specific environment** (dev or
|
|
362
|
+
* prod). Same as `stackParameters` but the AppName/branch/namespace come from the
|
|
363
|
+
* resolved environment, and it also passes `GitHubEnvironment` so the stack's
|
|
364
|
+
* deploy-role trust is scoped to the GitHub Actions Environment (the OIDC subject
|
|
365
|
+
* for an environment-gated job is `…:environment:<env>`, not `…:ref:…`). Prod's
|
|
366
|
+
* AppName is the bare slug, so prod's resource names are byte-for-byte what the
|
|
367
|
+
* pre-environment template produced.
|
|
368
|
+
*/
|
|
369
|
+
function stackParametersForEnv(cfg, env) {
|
|
370
|
+
const e = resolveEnvironment(cfg, env);
|
|
371
|
+
const params = [
|
|
372
|
+
{ key: "AppName", value: e.appName },
|
|
373
|
+
{ key: "NamePrefix", value: cfg.namePrefix },
|
|
374
|
+
{ key: "GitHubOwner", value: cfg.github.owner },
|
|
375
|
+
{ key: "GitHubRepo", value: cfg.github.repo },
|
|
376
|
+
{ key: "GitHubBranch", value: e.branch },
|
|
377
|
+
{ key: "GitHubEnvironment", value: e.ghEnvironment },
|
|
378
|
+
{ key: "IcebergNamespace", value: e.namespace },
|
|
379
|
+
];
|
|
380
|
+
if (cfg.artifactsBucket)
|
|
381
|
+
params.push({ key: "ArtifactsBucket", value: cfg.artifactsBucket });
|
|
382
|
+
// The external-API proxy is provisioned only when an upstream is configured
|
|
383
|
+
// (typically dev → a UAT/prod backend). Empty leaves the route/authorizer out.
|
|
384
|
+
if (e.proxyUpstream)
|
|
385
|
+
params.push({ key: "ExternalApiUpstream", value: e.proxyUpstream });
|
|
386
|
+
if (!cognitoAdvancedSecuritySupported(cfg.region))
|
|
387
|
+
params.push({ key: "AdvancedSecurity", value: "OFF" });
|
|
388
|
+
return params;
|
|
389
|
+
}
|
|
235
390
|
/**
|
|
236
391
|
* The canonical repo-relative CloudFormation template paths. These are the paths
|
|
237
392
|
* in the *framework* repo (theserverkid/reactapistack) and the default the argv
|
|
@@ -296,6 +451,14 @@ function deployStackArgs(cfg, templateFile = exports.APP_TEMPLATE_FILE) {
|
|
|
296
451
|
function deployStackCommand(cfg, templateFile = exports.APP_TEMPLATE_FILE) {
|
|
297
452
|
return awsCommand(deployStackArgs(cfg, templateFile));
|
|
298
453
|
}
|
|
454
|
+
/** The `aws cloudformation deploy` argv for one environment's stack (dev/prod). */
|
|
455
|
+
function deployStackArgsForEnv(cfg, env, templateFile = exports.APP_TEMPLATE_FILE) {
|
|
456
|
+
return cfnDeployArgs(templateFile, resolveEnvironment(cfg, env).stackName, cfg.region, stackParametersForEnv(cfg, env));
|
|
457
|
+
}
|
|
458
|
+
/** The single-line `aws cloudformation deploy` command for one environment. */
|
|
459
|
+
function deployStackCommandForEnv(cfg, env, templateFile = exports.APP_TEMPLATE_FILE) {
|
|
460
|
+
return awsCommand(deployStackArgsForEnv(cfg, env, templateFile));
|
|
461
|
+
}
|
|
299
462
|
/**
|
|
300
463
|
* The tier-0 bootstrap stack (bootstrap.yaml) — the S3 buckets that hold the
|
|
301
464
|
* Lambda placeholder + hosted templates, and the GitHub OIDC provider. One per
|
|
@@ -346,12 +509,16 @@ exports.APP_STACK_OUTPUTS = [
|
|
|
346
509
|
{ key: "UserPoolClientId", field: "userPoolClientId" },
|
|
347
510
|
{ key: "JwksUrl", field: "jwksUrl" },
|
|
348
511
|
{ key: "WarehouseBucketName", field: "warehouseBucket" },
|
|
512
|
+
{ key: "UserPoolDomain", field: "userPoolDomain" },
|
|
513
|
+
{ key: "HostedUiUrl", field: "hostedUiUrl" },
|
|
514
|
+
{ key: "ProxyUrl", field: "proxyUrl" },
|
|
349
515
|
];
|
|
350
516
|
/**
|
|
351
517
|
* The GitHub repo **Variables** CI reads — `AWS_DEPLOY_ROLE_ARN` and
|
|
352
518
|
* `AWS_REGION` (see docs/serverless-deploy.md §3). Values come from the stack
|
|
353
519
|
* Outputs when already resolved, else an empty string to fill in from the
|
|
354
|
-
* Console.
|
|
520
|
+
* Console. This is the legacy single-environment view (the prod stack); the
|
|
521
|
+
* dev/prod split wires them per GitHub Environment via `repoVariablesForEnv`.
|
|
355
522
|
*/
|
|
356
523
|
function repoVariables(cfg) {
|
|
357
524
|
return [
|
|
@@ -363,6 +530,29 @@ function repoVariables(cfg) {
|
|
|
363
530
|
{ name: "AWS_REGION", value: cfg.region, from: "Region stack output" },
|
|
364
531
|
];
|
|
365
532
|
}
|
|
533
|
+
/**
|
|
534
|
+
* The same two Variables, scoped to one environment's GitHub Actions
|
|
535
|
+
* Environment. The dev deploy reads `development`'s copies, the prod deploy
|
|
536
|
+
* `production`'s — so the two never cross-assume each other's role. Values come
|
|
537
|
+
* from the environment's own stack Outputs.
|
|
538
|
+
*/
|
|
539
|
+
function repoVariablesForEnv(cfg, env) {
|
|
540
|
+
const e = resolveEnvironment(cfg, env);
|
|
541
|
+
return [
|
|
542
|
+
{
|
|
543
|
+
name: "AWS_DEPLOY_ROLE_ARN",
|
|
544
|
+
value: e.outputs?.deployRoleArn ?? "",
|
|
545
|
+
from: "DeployRoleArn stack output",
|
|
546
|
+
ghEnvironment: e.ghEnvironment,
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
name: "AWS_REGION",
|
|
550
|
+
value: cfg.region,
|
|
551
|
+
from: "Region stack output",
|
|
552
|
+
ghEnvironment: e.ghEnvironment,
|
|
553
|
+
},
|
|
554
|
+
];
|
|
555
|
+
}
|
|
366
556
|
/**
|
|
367
557
|
* The argv (after `gh`) that sets one GitHub repo **Variable** on the config's
|
|
368
558
|
* repo. `init` runs this for each resolved `repoVariables` entry so the
|
|
@@ -372,15 +562,35 @@ function repoVariables(cfg) {
|
|
|
372
562
|
* user's GitHub) rather than an API token; a pure argv so it is inspectable and
|
|
373
563
|
* shell-free (`execFileSync`).
|
|
374
564
|
*/
|
|
375
|
-
function ghVariableSetArgs(cfg, name, value) {
|
|
376
|
-
|
|
565
|
+
function ghVariableSetArgs(cfg, name, value, ghEnvironment) {
|
|
566
|
+
const args = [
|
|
377
567
|
"variable",
|
|
378
568
|
"set",
|
|
379
569
|
name,
|
|
380
570
|
"--repo",
|
|
381
571
|
`${cfg.github.owner}/${cfg.github.repo}`,
|
|
382
|
-
|
|
383
|
-
|
|
572
|
+
];
|
|
573
|
+
// Scope the Variable to a GitHub Actions Environment when one is given, so the
|
|
574
|
+
// dev and prod deploys each read their own role/region.
|
|
575
|
+
if (ghEnvironment)
|
|
576
|
+
args.push("--env", ghEnvironment);
|
|
577
|
+
args.push("--body", value);
|
|
578
|
+
return args;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* The argv (after `gh`) that creates (idempotently) a GitHub Actions
|
|
582
|
+
* **Environment** on the config's repo — `gh api --method PUT
|
|
583
|
+
* repos/{owner}/{repo}/environments/{env}`. `init` runs this for `development`
|
|
584
|
+
* and `production` before wiring their scoped Variables, since a Variable can
|
|
585
|
+
* only be set on an environment that already exists. PUT is idempotent, so
|
|
586
|
+
* re-running `init` is safe.
|
|
587
|
+
*/
|
|
588
|
+
function ghEnvironmentCreateArgs(cfg, ghEnvironment) {
|
|
589
|
+
return [
|
|
590
|
+
"api",
|
|
591
|
+
"--method",
|
|
592
|
+
"PUT",
|
|
593
|
+
`repos/${cfg.github.owner}/${cfg.github.repo}/environments/${ghEnvironment}`,
|
|
384
594
|
];
|
|
385
595
|
}
|
|
386
596
|
/**
|
package/dist/deploy/io.d.ts
CHANGED
|
@@ -22,6 +22,27 @@ export declare function configPath(cwd?: string): string;
|
|
|
22
22
|
* repo itself still works. Returns null when no candidate exists.
|
|
23
23
|
*/
|
|
24
24
|
export declare function resolveDeployAsset(relPath: string, cwd?: string): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Base URL the framework's CloudFormation templates are published at. The same
|
|
27
|
+
* site that hosts the installer (`reactapistack.com/setup.sh`) serves the base
|
|
28
|
+
* templates, so a CLI whose bundled copy is missing (a partial install, or a
|
|
29
|
+
* globally-linked dev build) can still fetch them.
|
|
30
|
+
*/
|
|
31
|
+
export declare const TEMPLATE_BASE_URL = "https://reactapistack.com/cloudformation";
|
|
32
|
+
/**
|
|
33
|
+
* Download a published CloudFormation template to a temp file, returning its
|
|
34
|
+
* path — or null on any failure (offline, 404, write error). Used as the
|
|
35
|
+
* fallback when the template is not bundled with the install.
|
|
36
|
+
*/
|
|
37
|
+
export declare function downloadTemplate(name: string, base?: string): Promise<string | null>;
|
|
38
|
+
/**
|
|
39
|
+
* Resolve a CloudFormation template to a local file path for `aws cloudformation
|
|
40
|
+
* deploy` (which accepts only `--template-file`). Prefers the copy bundled with
|
|
41
|
+
* the `rastack` install; when that is missing, downloads it from the React API
|
|
42
|
+
* Stack website. Returns null only when the template is neither bundled nor
|
|
43
|
+
* fetchable.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveTemplateFile(name: string, cwd?: string): Promise<string | null>;
|
|
25
46
|
/** Read + normalize the deploy config, or null when it does not exist yet. */
|
|
26
47
|
export declare function loadConfig(cwd?: string): DeployConfig | null;
|
|
27
48
|
/** Write the deploy config, creating `.rastack/` if needed. */
|
|
@@ -39,6 +60,14 @@ export declare function awsCapture(args: string[], cwd?: string): string | null;
|
|
|
39
60
|
export declare function awsRun(args: string[], cwd?: string): void;
|
|
40
61
|
/** True when the AWS CLI is on PATH. */
|
|
41
62
|
export declare function awsAvailable(): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* The AWS account id the CLI's active credentials resolve to, or null when the
|
|
65
|
+
* CLI has no usable credentials (not logged in, an expired SSO session, or no
|
|
66
|
+
* configured profile). `init`/`bootstrap` call this to fail fast with an
|
|
67
|
+
* actionable "log in" message before attempting a stack deploy, instead of
|
|
68
|
+
* letting the first `cloudformation deploy` fail with a cryptic AWS error.
|
|
69
|
+
*/
|
|
70
|
+
export declare function awsIdentity(region?: string): string | null;
|
|
42
71
|
/**
|
|
43
72
|
* A single Output value of a CloudFormation stack, or null when the stack does
|
|
44
73
|
* not exist or the key is unset. `describe-stacks` prints `None` for a missing
|
|
@@ -52,6 +81,27 @@ export declare function stackOutput(stackName: string, key: string, region: stri
|
|
|
52
81
|
* on every `init`. Returns false when the placeholder file is missing.
|
|
53
82
|
*/
|
|
54
83
|
export declare function uploadPlaceholder(artifactsBucket: string, region: string, cwd?: string): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Deploy the tier-0 bootstrap stack, retrying once against an account that
|
|
86
|
+
* already has a GitHub OIDC provider — AWS allows only one provider per URL, so
|
|
87
|
+
* a second create is rejected and the retry tells the template to reuse the
|
|
88
|
+
* existing one. Throws on failure (after the retry). Shared by `rastack init`
|
|
89
|
+
* and `rastack bootstrap` so both stand the stack up identically.
|
|
90
|
+
*/
|
|
91
|
+
export declare function deployBootstrap(cfg: DeployConfig, reuseOidc: boolean, template: string): void;
|
|
92
|
+
/**
|
|
93
|
+
* Ensure the tier-0 bootstrap stack exists for this account+region, **creating
|
|
94
|
+
* it when missing**, and return its artifacts/templates buckets. This is why
|
|
95
|
+
* `rastack init` is a single command that stands up *everything* — the
|
|
96
|
+
* account-level bootstrap stack and the per-app backend — with no separate
|
|
97
|
+
* `rastack bootstrap` step. Idempotent: an existing stack is reconciled in
|
|
98
|
+
* place. Throws with an actionable message when the template cannot be resolved,
|
|
99
|
+
* the deploy fails, or the buckets can't be read back.
|
|
100
|
+
*/
|
|
101
|
+
export declare function ensureBootstrapStack(cfg: DeployConfig, reuseOidc?: boolean): Promise<{
|
|
102
|
+
artifactsBucket: string;
|
|
103
|
+
templatesBucket: string | null;
|
|
104
|
+
}>;
|
|
55
105
|
/** True when the GitHub CLI is on PATH. */
|
|
56
106
|
export declare function ghAvailable(): boolean;
|
|
57
107
|
/**
|
|
@@ -75,6 +125,13 @@ export declare function ghAuthLogin(cwd?: string): boolean;
|
|
|
75
125
|
* into a command line.
|
|
76
126
|
*/
|
|
77
127
|
export declare function setRepoVariable(args: string[], cwd?: string): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Create (idempotently, via `gh api --method PUT`) a GitHub Actions Environment
|
|
130
|
+
* so its scoped Variables can be set. Returns true on success. Best-effort — a
|
|
131
|
+
* missing/unauthenticated `gh` or an API error degrades to the printed
|
|
132
|
+
* instructions, exactly like `setRepoVariable`.
|
|
133
|
+
*/
|
|
134
|
+
export declare function ghEnvironmentCreate(args: string[], cwd?: string): boolean;
|
|
78
135
|
/**
|
|
79
136
|
* Ask a single question with a default, echoed as `label [default]: `. Returns
|
|
80
137
|
* the trimmed answer, or the default on an empty line.
|
package/dist/deploy/io.js
CHANGED
|
@@ -38,8 +38,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
};
|
|
39
39
|
})();
|
|
40
40
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.TEMPLATE_BASE_URL = void 0;
|
|
41
42
|
exports.configPath = configPath;
|
|
42
43
|
exports.resolveDeployAsset = resolveDeployAsset;
|
|
44
|
+
exports.downloadTemplate = downloadTemplate;
|
|
45
|
+
exports.resolveTemplateFile = resolveTemplateFile;
|
|
43
46
|
exports.loadConfig = loadConfig;
|
|
44
47
|
exports.saveConfig = saveConfig;
|
|
45
48
|
exports.gitRemoteUrl = gitRemoteUrl;
|
|
@@ -47,16 +50,21 @@ exports.canPrompt = canPrompt;
|
|
|
47
50
|
exports.awsCapture = awsCapture;
|
|
48
51
|
exports.awsRun = awsRun;
|
|
49
52
|
exports.awsAvailable = awsAvailable;
|
|
53
|
+
exports.awsIdentity = awsIdentity;
|
|
50
54
|
exports.stackOutput = stackOutput;
|
|
51
55
|
exports.uploadPlaceholder = uploadPlaceholder;
|
|
56
|
+
exports.deployBootstrap = deployBootstrap;
|
|
57
|
+
exports.ensureBootstrapStack = ensureBootstrapStack;
|
|
52
58
|
exports.ghAvailable = ghAvailable;
|
|
53
59
|
exports.ghAuthenticated = ghAuthenticated;
|
|
54
60
|
exports.ghAuthLogin = ghAuthLogin;
|
|
55
61
|
exports.setRepoVariable = setRepoVariable;
|
|
62
|
+
exports.ghEnvironmentCreate = ghEnvironmentCreate;
|
|
56
63
|
exports.ask = ask;
|
|
57
64
|
exports.withPrompt = withPrompt;
|
|
58
65
|
const child_process_1 = require("child_process");
|
|
59
66
|
const fs = __importStar(require("fs"));
|
|
67
|
+
const https = __importStar(require("https"));
|
|
60
68
|
const os = __importStar(require("os"));
|
|
61
69
|
const path = __importStar(require("path"));
|
|
62
70
|
const readline = __importStar(require("readline"));
|
|
@@ -96,6 +104,54 @@ function resolveDeployAsset(relPath, cwd = process.cwd()) {
|
|
|
96
104
|
}
|
|
97
105
|
return null;
|
|
98
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Base URL the framework's CloudFormation templates are published at. The same
|
|
109
|
+
* site that hosts the installer (`reactapistack.com/setup.sh`) serves the base
|
|
110
|
+
* templates, so a CLI whose bundled copy is missing (a partial install, or a
|
|
111
|
+
* globally-linked dev build) can still fetch them.
|
|
112
|
+
*/
|
|
113
|
+
exports.TEMPLATE_BASE_URL = "https://reactapistack.com/cloudformation";
|
|
114
|
+
/**
|
|
115
|
+
* Download a published CloudFormation template to a temp file, returning its
|
|
116
|
+
* path — or null on any failure (offline, 404, write error). Used as the
|
|
117
|
+
* fallback when the template is not bundled with the install.
|
|
118
|
+
*/
|
|
119
|
+
function downloadTemplate(name, base = exports.TEMPLATE_BASE_URL) {
|
|
120
|
+
const url = `${base}/${name}`;
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
https
|
|
123
|
+
.get(url, (res) => {
|
|
124
|
+
if (res.statusCode !== 200) {
|
|
125
|
+
res.resume();
|
|
126
|
+
resolve(null);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const chunks = [];
|
|
130
|
+
res.on("data", (c) => chunks.push(c));
|
|
131
|
+
res.on("end", () => {
|
|
132
|
+
try {
|
|
133
|
+
const dest = path.join(os.tmpdir(), `rastack-${path.basename(name)}`);
|
|
134
|
+
fs.writeFileSync(dest, Buffer.concat(chunks));
|
|
135
|
+
resolve(dest);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
resolve(null);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
})
|
|
142
|
+
.on("error", () => resolve(null));
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Resolve a CloudFormation template to a local file path for `aws cloudformation
|
|
147
|
+
* deploy` (which accepts only `--template-file`). Prefers the copy bundled with
|
|
148
|
+
* the `rastack` install; when that is missing, downloads it from the React API
|
|
149
|
+
* Stack website. Returns null only when the template is neither bundled nor
|
|
150
|
+
* fetchable.
|
|
151
|
+
*/
|
|
152
|
+
async function resolveTemplateFile(name, cwd = process.cwd()) {
|
|
153
|
+
return resolveDeployAsset(name, cwd) ?? (await downloadTemplate(name));
|
|
154
|
+
}
|
|
99
155
|
/** Read + normalize the deploy config, or null when it does not exist yet. */
|
|
100
156
|
function loadConfig(cwd = process.cwd()) {
|
|
101
157
|
const file = configPath(cwd);
|
|
@@ -162,6 +218,26 @@ function awsRun(args, cwd = process.cwd()) {
|
|
|
162
218
|
function awsAvailable() {
|
|
163
219
|
return awsCapture(["--version"]) !== null;
|
|
164
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* The AWS account id the CLI's active credentials resolve to, or null when the
|
|
223
|
+
* CLI has no usable credentials (not logged in, an expired SSO session, or no
|
|
224
|
+
* configured profile). `init`/`bootstrap` call this to fail fast with an
|
|
225
|
+
* actionable "log in" message before attempting a stack deploy, instead of
|
|
226
|
+
* letting the first `cloudformation deploy` fail with a cryptic AWS error.
|
|
227
|
+
*/
|
|
228
|
+
function awsIdentity(region) {
|
|
229
|
+
const args = [
|
|
230
|
+
"sts",
|
|
231
|
+
"get-caller-identity",
|
|
232
|
+
"--query",
|
|
233
|
+
"Account",
|
|
234
|
+
"--output",
|
|
235
|
+
"text",
|
|
236
|
+
];
|
|
237
|
+
if (region)
|
|
238
|
+
args.push("--region", region);
|
|
239
|
+
return awsCapture(args);
|
|
240
|
+
}
|
|
165
241
|
/**
|
|
166
242
|
* A single Output value of a CloudFormation stack, or null when the stack does
|
|
167
243
|
* not exist or the key is unset. `describe-stacks` prints `None` for a missing
|
|
@@ -209,6 +285,55 @@ function uploadPlaceholder(artifactsBucket, region, cwd = process.cwd()) {
|
|
|
209
285
|
]);
|
|
210
286
|
return true;
|
|
211
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Deploy the tier-0 bootstrap stack, retrying once against an account that
|
|
290
|
+
* already has a GitHub OIDC provider — AWS allows only one provider per URL, so
|
|
291
|
+
* a second create is rejected and the retry tells the template to reuse the
|
|
292
|
+
* existing one. Throws on failure (after the retry). Shared by `rastack init`
|
|
293
|
+
* and `rastack bootstrap` so both stand the stack up identically.
|
|
294
|
+
*/
|
|
295
|
+
function deployBootstrap(cfg, reuseOidc, template) {
|
|
296
|
+
try {
|
|
297
|
+
awsRun((0, config_1.bootstrapStackArgs)(cfg, reuseOidc, template));
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
if (reuseOidc)
|
|
301
|
+
throw err;
|
|
302
|
+
console.log(" • retrying bootstrap, reusing the account's existing GitHub OIDC provider");
|
|
303
|
+
awsRun((0, config_1.bootstrapStackArgs)(cfg, true, template));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Ensure the tier-0 bootstrap stack exists for this account+region, **creating
|
|
308
|
+
* it when missing**, and return its artifacts/templates buckets. This is why
|
|
309
|
+
* `rastack init` is a single command that stands up *everything* — the
|
|
310
|
+
* account-level bootstrap stack and the per-app backend — with no separate
|
|
311
|
+
* `rastack bootstrap` step. Idempotent: an existing stack is reconciled in
|
|
312
|
+
* place. Throws with an actionable message when the template cannot be resolved,
|
|
313
|
+
* the deploy fails, or the buckets can't be read back.
|
|
314
|
+
*/
|
|
315
|
+
async function ensureBootstrapStack(cfg, reuseOidc = false) {
|
|
316
|
+
const stack = (0, config_1.bootstrapStackName)(cfg);
|
|
317
|
+
const existing = stackOutput(stack, config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
|
|
318
|
+
if (existing) {
|
|
319
|
+
console.log(` ✓ bootstrap stack ${stack} already exists — reconciling`);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
console.log(`\n▸ Creating the tier-0 bootstrap stack ${stack} (@ ${cfg.region})`);
|
|
323
|
+
}
|
|
324
|
+
const template = await resolveTemplateFile("bootstrap.yaml");
|
|
325
|
+
if (!template) {
|
|
326
|
+
throw new Error("bootstrap.yaml template not found in the rastack package and could not be\n" +
|
|
327
|
+
" downloaded from reactapistack.com. Reinstall rastack (`npm i -g rastack`).");
|
|
328
|
+
}
|
|
329
|
+
deployBootstrap(cfg, reuseOidc, template);
|
|
330
|
+
const artifactsBucket = stackOutput(stack, config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
|
|
331
|
+
const templatesBucket = stackOutput(stack, config_1.BOOTSTRAP_TEMPLATES_OUTPUT, cfg.region);
|
|
332
|
+
if (!artifactsBucket) {
|
|
333
|
+
throw new Error(`bootstrap stack ${stack} deployed but its ArtifactsBucket output could not be read.`);
|
|
334
|
+
}
|
|
335
|
+
return { artifactsBucket, templatesBucket };
|
|
336
|
+
}
|
|
212
337
|
// ── GitHub CLI boundary ──────────────────────────────────────────────────────
|
|
213
338
|
// After the stack is up `init` wires the two repo Variables CI reads
|
|
214
339
|
// (AWS_DEPLOY_ROLE_ARN / AWS_REGION) through the developer's own `gh` — already
|
|
@@ -278,6 +403,22 @@ function setRepoVariable(args, cwd = process.cwd()) {
|
|
|
278
403
|
return false;
|
|
279
404
|
}
|
|
280
405
|
}
|
|
406
|
+
/**
|
|
407
|
+
* Create (idempotently, via `gh api --method PUT`) a GitHub Actions Environment
|
|
408
|
+
* so its scoped Variables can be set. Returns true on success. Best-effort — a
|
|
409
|
+
* missing/unauthenticated `gh` or an API error degrades to the printed
|
|
410
|
+
* instructions, exactly like `setRepoVariable`.
|
|
411
|
+
*/
|
|
412
|
+
function ghEnvironmentCreate(args, cwd = process.cwd()) {
|
|
413
|
+
try {
|
|
414
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
415
|
+
(0, child_process_1.execFileSync)("gh", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
281
422
|
/**
|
|
282
423
|
* Ask a single question with a default, echoed as `label [default]: `. Returns
|
|
283
424
|
* the trimmed answer, or the default on an empty line.
|
|
@@ -34,6 +34,36 @@ export declare function renderConfigFile(cfg: DeployConfig): string;
|
|
|
34
34
|
export declare const CONFIG_PATH = ".rastack/deploy.json";
|
|
35
35
|
export declare const CI_WORKFLOW_PATH = ".github/workflows/ci.yml";
|
|
36
36
|
export declare const DEPLOY_WORKFLOW_PATH = ".github/workflows/deploy-backend.yml";
|
|
37
|
+
export declare const ENV_PATH = ".env";
|
|
38
|
+
export declare const GITIGNORE_PATH = ".gitignore";
|
|
39
|
+
/**
|
|
40
|
+
* The generated `.env` — the app's **local** environment. Every value is
|
|
41
|
+
* non-secret (client ids, pool ids, public URLs from the stack Outputs), but the
|
|
42
|
+
* file is git-ignored by convention (`gitignoreWith`) so a developer can add
|
|
43
|
+
* their own secrets beside these without ever committing them. Values are blank
|
|
44
|
+
* until the matching stack is deployed; `rastack init` rewrites the file with the
|
|
45
|
+
* resolved Outputs after standing the backends up.
|
|
46
|
+
*
|
|
47
|
+
* Keys are `RASTACK_<ENV>_*` (a neutral prefix). For a browser bundle, re-export
|
|
48
|
+
* the ones you need under your bundler's public prefix (EXPO_PUBLIC_/VITE_/…);
|
|
49
|
+
* `rastack dev` also serves the dev pool config live at `/__rastack/cognito.json`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function renderEnvFile(cfg: DeployConfig): string;
|
|
52
|
+
/**
|
|
53
|
+
* Upsert the generated `RASTACK_*` keys from `renderEnvFile(cfg)` into an
|
|
54
|
+
* existing `.env`, preserving every other line (a developer's own secrets,
|
|
55
|
+
* comments, extra keys). A matching `RASTACK_*=` line is updated in place;
|
|
56
|
+
* generated keys not yet present are appended. When there is no existing file,
|
|
57
|
+
* this is exactly `renderEnvFile(cfg)`. Pure — the IO wrapper reads/writes.
|
|
58
|
+
*/
|
|
59
|
+
export declare function applyEnvValues(existing: string, cfg: DeployConfig): string;
|
|
60
|
+
/**
|
|
61
|
+
* Return `.gitignore` content with a `.env` entry present (idempotent). Appends
|
|
62
|
+
* under a labelled block when missing, leaves the file untouched when `.env` is
|
|
63
|
+
* already ignored — so `rastack init` never duplicates the entry or clobbers a
|
|
64
|
+
* developer's existing ignore rules.
|
|
65
|
+
*/
|
|
66
|
+
export declare function gitignoreWith(existing: string, entry?: string): string;
|
|
37
67
|
export interface ScaffoldOptions {
|
|
38
68
|
/** Omit the GitHub Actions workflows (config only). */
|
|
39
69
|
noWorkflows?: boolean;
|