rastack 0.0.38 → 0.0.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/deploy/config.d.ts +18 -0
- package/dist/deploy/config.js +49 -0
- package/dist/deploy/io.d.ts +24 -1
- package/dist/deploy/io.js +74 -1
- package/dist/rastack-bootstrap.d.ts +24 -0
- package/dist/rastack-bootstrap.js +222 -0
- package/dist/rastack-init.d.ts +1 -1
- package/dist/rastack-init.js +70 -20
- package/dist/rastack.d.ts +1 -0
- package/dist/rastack.js +7 -0
- package/package.json +1 -1
- package/src/deploy/config.ts +56 -0
- package/src/deploy/io.ts +74 -1
- package/src/rastack-bootstrap.ts +299 -0
- package/src/rastack-init.ts +95 -27
- package/src/rastack.ts +7 -0
- package/test/deploy.spec.ts +53 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [0.0.40](https://github.com/theserverkid/reactapistack/compare/v0.0.39...v0.0.40) (2026-07-12)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* `rastack init` now wires the GitHub→Lambda deploy variables ([fa35b97](https://github.com/theserverkid/reactapistack/commit/fa35b9744c2149ff7a9c663ffecdd2d6ad1edec6))
|
|
11
|
+
|
|
12
|
+
### [0.0.39](https://github.com/theserverkid/reactapistack/compare/v0.0.38...v0.0.39) (2026-07-11)
|
|
13
|
+
|
|
5
14
|
### [0.0.38](https://github.com/theserverkid/reactapistack/compare/v0.0.37...v0.0.38) (2026-07-11)
|
|
6
15
|
|
|
7
16
|
### [0.0.37](https://github.com/theserverkid/reactapistack/compare/v0.0.36...v0.0.37) (2026-07-11)
|
package/dist/deploy/config.d.ts
CHANGED
|
@@ -91,6 +91,14 @@ export declare function normalizeConfig(partial?: PartialDeployConfig): DeployCo
|
|
|
91
91
|
* config that passes here also passes CloudFormation's own validation.
|
|
92
92
|
*/
|
|
93
93
|
export declare function validateConfig(cfg: DeployConfig): string[];
|
|
94
|
+
/**
|
|
95
|
+
* Return the subset of problems that block standing the **tier-0 bootstrap
|
|
96
|
+
* stack** up. The bootstrap stack (bootstrap.yaml) is account+region scoped — it
|
|
97
|
+
* only needs the name prefix, the region, and the GitHub coordinates that anchor
|
|
98
|
+
* the OIDC trust — so `rastack bootstrap` validates just those, not the per-app
|
|
99
|
+
* fields (`app`, namespace, Lambda build) that `validateConfig` also requires.
|
|
100
|
+
*/
|
|
101
|
+
export declare function validateBootstrapConfig(cfg: DeployConfig): string[];
|
|
94
102
|
/** Deep-merge a patch over a base config (used by `deploy`/`init` reconfigure). */
|
|
95
103
|
export declare function mergeConfig(base: DeployConfig, patch: PartialDeployConfig): DeployConfig;
|
|
96
104
|
/**
|
|
@@ -177,6 +185,16 @@ export interface RepoVariable {
|
|
|
177
185
|
* Console.
|
|
178
186
|
*/
|
|
179
187
|
export declare function repoVariables(cfg: DeployConfig): RepoVariable[];
|
|
188
|
+
/**
|
|
189
|
+
* The argv (after `gh`) that sets one GitHub repo **Variable** on the config's
|
|
190
|
+
* repo. `init` runs this for each resolved `repoVariables` entry so the
|
|
191
|
+
* `deploy-backend.yml` guard (`if: vars.AWS_DEPLOY_ROLE_ARN != ''`) passes and
|
|
192
|
+
* the very first push actually ships — instead of the variables being a manual
|
|
193
|
+
* Console step the user has to remember. Uses `gh` (already authenticated to the
|
|
194
|
+
* user's GitHub) rather than an API token; a pure argv so it is inspectable and
|
|
195
|
+
* shell-free (`execFileSync`).
|
|
196
|
+
*/
|
|
197
|
+
export declare function ghVariableSetArgs(cfg: DeployConfig, name: string, value: string): string[];
|
|
180
198
|
/**
|
|
181
199
|
* The `RASTACK_*` environment the Lambda reads (docs/serverless-deploy.md §1).
|
|
182
200
|
* The IaC sets these on the function; surfaced here for `rastack deploy` to
|
package/dist/deploy/config.js
CHANGED
|
@@ -21,6 +21,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
21
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;
|
|
22
22
|
exports.normalizeConfig = normalizeConfig;
|
|
23
23
|
exports.validateConfig = validateConfig;
|
|
24
|
+
exports.validateBootstrapConfig = validateBootstrapConfig;
|
|
24
25
|
exports.mergeConfig = mergeConfig;
|
|
25
26
|
exports.parseGitHubRemote = parseGitHubRemote;
|
|
26
27
|
exports.cognitoAdvancedSecuritySupported = cognitoAdvancedSecuritySupported;
|
|
@@ -32,6 +33,7 @@ exports.bootstrapStackParameters = bootstrapStackParameters;
|
|
|
32
33
|
exports.bootstrapStackArgs = bootstrapStackArgs;
|
|
33
34
|
exports.bootstrapStackCommand = bootstrapStackCommand;
|
|
34
35
|
exports.repoVariables = repoVariables;
|
|
36
|
+
exports.ghVariableSetArgs = ghVariableSetArgs;
|
|
35
37
|
exports.lambdaEnv = lambdaEnv;
|
|
36
38
|
exports.DEFAULT_LAMBDA = {
|
|
37
39
|
bin: "bootstrap",
|
|
@@ -121,6 +123,33 @@ function validateConfig(cfg) {
|
|
|
121
123
|
errors.push("lambda.bin is required.");
|
|
122
124
|
return errors;
|
|
123
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Return the subset of problems that block standing the **tier-0 bootstrap
|
|
128
|
+
* stack** up. The bootstrap stack (bootstrap.yaml) is account+region scoped — it
|
|
129
|
+
* only needs the name prefix, the region, and the GitHub coordinates that anchor
|
|
130
|
+
* the OIDC trust — so `rastack bootstrap` validates just those, not the per-app
|
|
131
|
+
* fields (`app`, namespace, Lambda build) that `validateConfig` also requires.
|
|
132
|
+
*/
|
|
133
|
+
function validateBootstrapConfig(cfg) {
|
|
134
|
+
const errors = [];
|
|
135
|
+
if (!SLUG.test(cfg.namePrefix))
|
|
136
|
+
errors.push(`namePrefix "${cfg.namePrefix}" must match ${SLUG}.`);
|
|
137
|
+
if (!cfg.region)
|
|
138
|
+
errors.push("region is required (e.g. `af-south-1`).");
|
|
139
|
+
else if (!REGION.test(cfg.region))
|
|
140
|
+
errors.push(`region "${cfg.region}" does not look like an AWS region (e.g. af-south-1).`);
|
|
141
|
+
if (!cfg.github.owner)
|
|
142
|
+
errors.push("github.owner is required.");
|
|
143
|
+
else if (!GH_NAME.test(cfg.github.owner))
|
|
144
|
+
errors.push(`github.owner "${cfg.github.owner}" is not a valid GitHub name.`);
|
|
145
|
+
if (!cfg.github.repo)
|
|
146
|
+
errors.push("github.repo is required.");
|
|
147
|
+
else if (!GH_NAME.test(cfg.github.repo))
|
|
148
|
+
errors.push(`github.repo "${cfg.github.repo}" is not a valid GitHub name.`);
|
|
149
|
+
if (!cfg.github.branch)
|
|
150
|
+
errors.push("github.branch is required.");
|
|
151
|
+
return errors;
|
|
152
|
+
}
|
|
124
153
|
/** Deep-merge a patch over a base config (used by `deploy`/`init` reconfigure). */
|
|
125
154
|
function mergeConfig(base, patch) {
|
|
126
155
|
return normalizeConfig({
|
|
@@ -317,6 +346,26 @@ function repoVariables(cfg) {
|
|
|
317
346
|
{ name: "AWS_REGION", value: cfg.region, from: "Region stack output" },
|
|
318
347
|
];
|
|
319
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* The argv (after `gh`) that sets one GitHub repo **Variable** on the config's
|
|
351
|
+
* repo. `init` runs this for each resolved `repoVariables` entry so the
|
|
352
|
+
* `deploy-backend.yml` guard (`if: vars.AWS_DEPLOY_ROLE_ARN != ''`) passes and
|
|
353
|
+
* the very first push actually ships — instead of the variables being a manual
|
|
354
|
+
* Console step the user has to remember. Uses `gh` (already authenticated to the
|
|
355
|
+
* user's GitHub) rather than an API token; a pure argv so it is inspectable and
|
|
356
|
+
* shell-free (`execFileSync`).
|
|
357
|
+
*/
|
|
358
|
+
function ghVariableSetArgs(cfg, name, value) {
|
|
359
|
+
return [
|
|
360
|
+
"variable",
|
|
361
|
+
"set",
|
|
362
|
+
name,
|
|
363
|
+
"--repo",
|
|
364
|
+
`${cfg.github.owner}/${cfg.github.repo}`,
|
|
365
|
+
"--body",
|
|
366
|
+
value,
|
|
367
|
+
];
|
|
368
|
+
}
|
|
320
369
|
/**
|
|
321
370
|
* The `RASTACK_*` environment the Lambda reads (docs/serverless-deploy.md §1).
|
|
322
371
|
* The IaC sets these on the function; surfaced here for `rastack deploy` to
|
package/dist/deploy/io.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare function saveConfig(cfg: DeployConfig, cwd?: string): string;
|
|
|
17
17
|
*/
|
|
18
18
|
export declare function gitRemoteUrl(cwd?: string): string | null;
|
|
19
19
|
/** True when we can prompt the user (a TTY, and not explicitly opted out). */
|
|
20
|
-
export declare function canPrompt(nonInteractive
|
|
20
|
+
export declare function canPrompt(nonInteractive?: boolean): boolean;
|
|
21
21
|
/** Capture `aws <args>` stdout, or null when the CLI is absent / the call fails. */
|
|
22
22
|
export declare function awsCapture(args: string[], cwd?: string): string | null;
|
|
23
23
|
/** Run `aws <args>` streaming output; throws on a non-zero exit. */
|
|
@@ -37,6 +37,29 @@ export declare function stackOutput(stackName: string, key: string, region: stri
|
|
|
37
37
|
* on every `init`. Returns false when the placeholder file is missing.
|
|
38
38
|
*/
|
|
39
39
|
export declare function uploadPlaceholder(artifactsBucket: string, region: string, cwd?: string): boolean;
|
|
40
|
+
/** True when the GitHub CLI is on PATH. */
|
|
41
|
+
export declare function ghAvailable(): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* True when `gh` already holds valid credentials for github.com. `gh auth
|
|
44
|
+
* status` exits non-zero when logged out, so a thrown call means "not connected".
|
|
45
|
+
*/
|
|
46
|
+
export declare function ghAuthenticated(cwd?: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Connect `gh` to GitHub interactively (`gh auth login`) as part of `init`, so
|
|
49
|
+
* the repo Variables can be set right after. Streamed to the terminal — the
|
|
50
|
+
* browser/device-code flow needs the TTY — and left to `gh`'s own prompts so the
|
|
51
|
+
* user picks how to authenticate (browser or a pasted token). Returns true when
|
|
52
|
+
* the login exits 0.
|
|
53
|
+
*/
|
|
54
|
+
export declare function ghAuthLogin(cwd?: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Set one GitHub repo Variable via `gh variable set`. Returns true on success,
|
|
57
|
+
* false when `gh` is missing, unauthenticated, or the call otherwise fails —
|
|
58
|
+
* the caller falls back to printing the value for the user to wire by hand.
|
|
59
|
+
* `execFileSync` (no shell) so the ARN/region values are never interpolated
|
|
60
|
+
* into a command line.
|
|
61
|
+
*/
|
|
62
|
+
export declare function setRepoVariable(args: string[], cwd?: string): boolean;
|
|
40
63
|
/**
|
|
41
64
|
* Ask a single question with a default, echoed as `label [default]: `. Returns
|
|
42
65
|
* the trimmed answer, or the default on an empty line.
|
package/dist/deploy/io.js
CHANGED
|
@@ -48,6 +48,10 @@ exports.awsRun = awsRun;
|
|
|
48
48
|
exports.awsAvailable = awsAvailable;
|
|
49
49
|
exports.stackOutput = stackOutput;
|
|
50
50
|
exports.uploadPlaceholder = uploadPlaceholder;
|
|
51
|
+
exports.ghAvailable = ghAvailable;
|
|
52
|
+
exports.ghAuthenticated = ghAuthenticated;
|
|
53
|
+
exports.ghAuthLogin = ghAuthLogin;
|
|
54
|
+
exports.setRepoVariable = setRepoVariable;
|
|
51
55
|
exports.ask = ask;
|
|
52
56
|
exports.withPrompt = withPrompt;
|
|
53
57
|
const child_process_1 = require("child_process");
|
|
@@ -98,7 +102,7 @@ function gitRemoteUrl(cwd = process.cwd()) {
|
|
|
98
102
|
}
|
|
99
103
|
}
|
|
100
104
|
/** True when we can prompt the user (a TTY, and not explicitly opted out). */
|
|
101
|
-
function canPrompt(nonInteractive) {
|
|
105
|
+
function canPrompt(nonInteractive = false) {
|
|
102
106
|
return !nonInteractive && Boolean(process.stdin.isTTY);
|
|
103
107
|
}
|
|
104
108
|
// ── AWS CLI boundary ─────────────────────────────────────────────────────────
|
|
@@ -175,6 +179,75 @@ function uploadPlaceholder(artifactsBucket, region, cwd = process.cwd()) {
|
|
|
175
179
|
]);
|
|
176
180
|
return true;
|
|
177
181
|
}
|
|
182
|
+
// ── GitHub CLI boundary ──────────────────────────────────────────────────────
|
|
183
|
+
// After the stack is up `init` wires the two repo Variables CI reads
|
|
184
|
+
// (AWS_DEPLOY_ROLE_ARN / AWS_REGION) through the developer's own `gh` — already
|
|
185
|
+
// authenticated to their GitHub — so the pipeline fires on the first push
|
|
186
|
+
// instead of leaving the wiring as a manual Console step. Best-effort: absent or
|
|
187
|
+
// unauthenticated `gh` degrades to the printed instructions.
|
|
188
|
+
/** True when the GitHub CLI is on PATH. */
|
|
189
|
+
function ghAvailable() {
|
|
190
|
+
try {
|
|
191
|
+
(0, child_process_1.execFileSync)("gh", ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* True when `gh` already holds valid credentials for github.com. `gh auth
|
|
200
|
+
* status` exits non-zero when logged out, so a thrown call means "not connected".
|
|
201
|
+
*/
|
|
202
|
+
function ghAuthenticated(cwd = process.cwd()) {
|
|
203
|
+
try {
|
|
204
|
+
(0, child_process_1.execFileSync)("gh", ["auth", "status", "--hostname", "github.com"], {
|
|
205
|
+
cwd,
|
|
206
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
207
|
+
});
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Connect `gh` to GitHub interactively (`gh auth login`) as part of `init`, so
|
|
216
|
+
* the repo Variables can be set right after. Streamed to the terminal — the
|
|
217
|
+
* browser/device-code flow needs the TTY — and left to `gh`'s own prompts so the
|
|
218
|
+
* user picks how to authenticate (browser or a pasted token). Returns true when
|
|
219
|
+
* the login exits 0.
|
|
220
|
+
*/
|
|
221
|
+
function ghAuthLogin(cwd = process.cwd()) {
|
|
222
|
+
try {
|
|
223
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
224
|
+
(0, child_process_1.execFileSync)("gh", ["auth", "login", "--hostname", "github.com"], {
|
|
225
|
+
cwd,
|
|
226
|
+
stdio: "inherit",
|
|
227
|
+
});
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Set one GitHub repo Variable via `gh variable set`. Returns true on success,
|
|
236
|
+
* false when `gh` is missing, unauthenticated, or the call otherwise fails —
|
|
237
|
+
* the caller falls back to printing the value for the user to wire by hand.
|
|
238
|
+
* `execFileSync` (no shell) so the ARN/region values are never interpolated
|
|
239
|
+
* into a command line.
|
|
240
|
+
*/
|
|
241
|
+
function setRepoVariable(args, cwd = process.cwd()) {
|
|
242
|
+
try {
|
|
243
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
244
|
+
(0, child_process_1.execFileSync)("gh", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
178
251
|
/**
|
|
179
252
|
* Ask a single question with a default, echoed as `label [default]: `. Returns
|
|
180
253
|
* the trimmed answer, or the default on an empty line.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `rastack bootstrap` — stand the **tier-0 bootstrap stack** up for an AWS
|
|
4
|
+
* account. This is the once-per-account+region step that prepares an account to
|
|
5
|
+
* host React API Stack backends, decoupled from any single app.
|
|
6
|
+
*
|
|
7
|
+
* rastack bootstrap [--owner <o>] [--repo <r>] [--branch <b>]
|
|
8
|
+
* [--region <r>] [--prefix <p>]
|
|
9
|
+
* [--reuse-oidc] [--print] [--yes]
|
|
10
|
+
*
|
|
11
|
+
* It deploys `deploy/cloudformation/bootstrap.yaml`, which creates:
|
|
12
|
+
* • the S3 buckets that hold the Lambda placeholder + hosted templates, and
|
|
13
|
+
* • the GitHub OIDC provider + trust that lets CI assume a deploy role.
|
|
14
|
+
* It then uploads the Lambda placeholder bundle to the artifacts bucket, so the
|
|
15
|
+
* account is immediately ready for per-app deploys (`rastack init`).
|
|
16
|
+
*
|
|
17
|
+
* `rastack init` still creates this stack on demand if it is missing, so
|
|
18
|
+
* `bootstrap` is the explicit, app-independent way to do the same thing up front
|
|
19
|
+
* (e.g. an account admin preparing an account before app teams run `init`).
|
|
20
|
+
* `--reuse-oidc` skips creating the GitHub OIDC provider (AWS allows only one per
|
|
21
|
+
* URL per account); `--print` shows the exact single-line `aws cloudformation
|
|
22
|
+
* deploy` command without touching the cloud.
|
|
23
|
+
*/
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* `rastack bootstrap` — stand the **tier-0 bootstrap stack** up for an AWS
|
|
5
|
+
* account. This is the once-per-account+region step that prepares an account to
|
|
6
|
+
* host React API Stack backends, decoupled from any single app.
|
|
7
|
+
*
|
|
8
|
+
* rastack bootstrap [--owner <o>] [--repo <r>] [--branch <b>]
|
|
9
|
+
* [--region <r>] [--prefix <p>]
|
|
10
|
+
* [--reuse-oidc] [--print] [--yes]
|
|
11
|
+
*
|
|
12
|
+
* It deploys `deploy/cloudformation/bootstrap.yaml`, which creates:
|
|
13
|
+
* • the S3 buckets that hold the Lambda placeholder + hosted templates, and
|
|
14
|
+
* • the GitHub OIDC provider + trust that lets CI assume a deploy role.
|
|
15
|
+
* It then uploads the Lambda placeholder bundle to the artifacts bucket, so the
|
|
16
|
+
* account is immediately ready for per-app deploys (`rastack init`).
|
|
17
|
+
*
|
|
18
|
+
* `rastack init` still creates this stack on demand if it is missing, so
|
|
19
|
+
* `bootstrap` is the explicit, app-independent way to do the same thing up front
|
|
20
|
+
* (e.g. an account admin preparing an account before app teams run `init`).
|
|
21
|
+
* `--reuse-oidc` skips creating the GitHub OIDC provider (AWS allows only one per
|
|
22
|
+
* URL per account); `--print` shows the exact single-line `aws cloudformation
|
|
23
|
+
* deploy` command without touching the cloud.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
const config_1 = require("./deploy/config");
|
|
27
|
+
const io_1 = require("./deploy/io");
|
|
28
|
+
function parseFlags(argv) {
|
|
29
|
+
const patch = { github: {} };
|
|
30
|
+
let reuseOidc = false;
|
|
31
|
+
let print = false;
|
|
32
|
+
let nonInteractive = false;
|
|
33
|
+
for (let i = 0; i < argv.length; i++) {
|
|
34
|
+
const a = argv[i];
|
|
35
|
+
const next = () => argv[++i] ?? "";
|
|
36
|
+
switch (a) {
|
|
37
|
+
case "--owner":
|
|
38
|
+
patch.github.owner = next();
|
|
39
|
+
break;
|
|
40
|
+
case "--repo":
|
|
41
|
+
patch.github.repo = next();
|
|
42
|
+
break;
|
|
43
|
+
case "--branch":
|
|
44
|
+
patch.github.branch = next();
|
|
45
|
+
break;
|
|
46
|
+
case "--region":
|
|
47
|
+
patch.region = next();
|
|
48
|
+
break;
|
|
49
|
+
case "--prefix":
|
|
50
|
+
patch.namePrefix = next();
|
|
51
|
+
break;
|
|
52
|
+
case "--reuse-oidc":
|
|
53
|
+
reuseOidc = true;
|
|
54
|
+
break;
|
|
55
|
+
case "--print":
|
|
56
|
+
case "--dry-run":
|
|
57
|
+
print = true;
|
|
58
|
+
break;
|
|
59
|
+
case "--yes":
|
|
60
|
+
case "-y":
|
|
61
|
+
case "--non-interactive":
|
|
62
|
+
nonInteractive = true;
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
if (a.startsWith("-")) {
|
|
66
|
+
console.error(`Unknown flag: ${a}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { patch, reuseOidc, print, nonInteractive };
|
|
72
|
+
}
|
|
73
|
+
/** Seed a config: existing file → flags → git remote → defaults. */
|
|
74
|
+
function seedConfig(flags) {
|
|
75
|
+
const existing = (0, io_1.loadConfig)();
|
|
76
|
+
let cfg = existing
|
|
77
|
+
? (0, config_1.mergeConfig)(existing, flags.patch)
|
|
78
|
+
: (0, config_1.normalizeConfig)(flags.patch);
|
|
79
|
+
// Default the GitHub coordinates from `origin` when not already known — they
|
|
80
|
+
// anchor the OIDC trust the bootstrap stack sets up.
|
|
81
|
+
if (!cfg.github.owner || !cfg.github.repo) {
|
|
82
|
+
const url = (0, io_1.gitRemoteUrl)();
|
|
83
|
+
const remote = url ? (0, config_1.parseGitHubRemote)(url) : null;
|
|
84
|
+
if (remote) {
|
|
85
|
+
cfg = (0, config_1.mergeConfig)(cfg, {
|
|
86
|
+
github: {
|
|
87
|
+
owner: cfg.github.owner || remote.owner,
|
|
88
|
+
repo: cfg.github.repo || remote.repo,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return cfg;
|
|
94
|
+
}
|
|
95
|
+
async function promptMissing(cfg) {
|
|
96
|
+
return (0, io_1.withPrompt)(async (rl) => {
|
|
97
|
+
const owner = await (0, io_1.ask)(rl, "GitHub owner", cfg.github.owner);
|
|
98
|
+
const repo = await (0, io_1.ask)(rl, "GitHub repo", cfg.github.repo);
|
|
99
|
+
const branch = await (0, io_1.ask)(rl, "Deploy branch", cfg.github.branch);
|
|
100
|
+
const region = await (0, io_1.ask)(rl, "AWS region", cfg.region);
|
|
101
|
+
const prefix = await (0, io_1.ask)(rl, "Name prefix", cfg.namePrefix);
|
|
102
|
+
return (0, config_1.mergeConfig)(cfg, {
|
|
103
|
+
region,
|
|
104
|
+
namePrefix: prefix,
|
|
105
|
+
github: { owner, repo, branch },
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** Yes/no confirmation with a default of "no" — gates the cloud-mutating step. */
|
|
110
|
+
async function confirm(question) {
|
|
111
|
+
return (0, io_1.withPrompt)(async (rl) => {
|
|
112
|
+
const answer = await (0, io_1.ask)(rl, question, "no");
|
|
113
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/** Print the exact command `bootstrap` would run, without touching the cloud. */
|
|
117
|
+
function printPlan(cfg, reuseOidc) {
|
|
118
|
+
console.log(`\n Bootstrap plan (${(0, config_1.bootstrapStackName)(cfg)} @ ${cfg.region})\n`);
|
|
119
|
+
console.log(" ── Tier-0 bootstrap stack (bootstrap.yaml) ──");
|
|
120
|
+
console.log("\n " + (0, config_1.bootstrapStackCommand)(cfg, reuseOidc) + "\n");
|
|
121
|
+
console.log(" Then `rastack init` (or `rastack bootstrap` again) resolves this stack's\n" +
|
|
122
|
+
" ArtifactsBucket to create app backends. Nothing here is app-specific.\n");
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Deploy the bootstrap stack, retrying once against an account that already has
|
|
126
|
+
* a GitHub OIDC provider (AWS allows only one per URL) when the caller did not
|
|
127
|
+
* already ask to reuse it.
|
|
128
|
+
*/
|
|
129
|
+
function deployBootstrap(cfg, reuseOidc) {
|
|
130
|
+
try {
|
|
131
|
+
(0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg, reuseOidc));
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
if (reuseOidc) {
|
|
135
|
+
console.error(`\n ✗ bootstrap stack deploy failed.\n ${err.message}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
// The most common cause is an account that already has a GitHub OIDC
|
|
139
|
+
// provider. Retry telling the template to reuse it instead of creating a
|
|
140
|
+
// second (which CloudFormation rejects).
|
|
141
|
+
console.log(" • retrying bootstrap, reusing the account's existing GitHub OIDC provider");
|
|
142
|
+
try {
|
|
143
|
+
(0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg, true));
|
|
144
|
+
}
|
|
145
|
+
catch (retryErr) {
|
|
146
|
+
console.error(`\n ✗ bootstrap stack deploy failed.\n ${retryErr.message}`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Stand the tier-0 bootstrap stack up and upload the Lambda placeholder. */
|
|
152
|
+
function standUp(cfg, reuseOidc) {
|
|
153
|
+
if (!(0, io_1.awsAvailable)()) {
|
|
154
|
+
console.error(" ✗ the AWS CLI is not on PATH. Install it and configure credentials,\n" +
|
|
155
|
+
" or re-run with --print to see the command.");
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
const stack = (0, config_1.bootstrapStackName)(cfg);
|
|
159
|
+
const existing = (0, io_1.stackOutput)(stack, config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
|
|
160
|
+
if (existing) {
|
|
161
|
+
console.log(`\n▸ Bootstrap stack ${stack} already exists — reconciling (idempotent).`);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
console.log(`\n▸ Creating bootstrap stack ${stack} (@ ${cfg.region})`);
|
|
165
|
+
}
|
|
166
|
+
deployBootstrap(cfg, reuseOidc);
|
|
167
|
+
const artifactsBucket = (0, io_1.stackOutput)(stack, config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
|
|
168
|
+
const templatesBucket = (0, io_1.stackOutput)(stack, config_1.BOOTSTRAP_TEMPLATES_OUTPUT, cfg.region);
|
|
169
|
+
if (!artifactsBucket) {
|
|
170
|
+
console.error(" ✗ bootstrap stack deployed but its ArtifactsBucket output could not be read.");
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
console.log(` ✓ artifacts bucket: ${artifactsBucket}`);
|
|
174
|
+
if (templatesBucket)
|
|
175
|
+
console.log(` ✓ templates bucket: ${templatesBucket}`);
|
|
176
|
+
// Upload the placeholder the app.yaml function is later created from, so the
|
|
177
|
+
// account is ready for `rastack init` without a second manual step.
|
|
178
|
+
console.log("\n▸ Uploading the Lambda placeholder bundle");
|
|
179
|
+
if (!(0, io_1.uploadPlaceholder)(artifactsBucket, cfg.region)) {
|
|
180
|
+
console.error(" ✗ deploy/cloudformation/placeholder/bootstrap not found — the placeholder\n" +
|
|
181
|
+
" was not uploaded. `rastack init` will upload it on the first app deploy.");
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
console.log(" ✓ placeholder uploaded");
|
|
185
|
+
}
|
|
186
|
+
console.log(`\n ✓ account bootstrapped (${stack} @ ${cfg.region}).\n` +
|
|
187
|
+
` Run \`rastack init\` in an app repo to deploy a backend against it.\n`);
|
|
188
|
+
}
|
|
189
|
+
async function main(argv) {
|
|
190
|
+
const flags = parseFlags(argv);
|
|
191
|
+
let cfg = seedConfig(flags);
|
|
192
|
+
if ((0, io_1.canPrompt)(flags.nonInteractive) && !flags.print) {
|
|
193
|
+
console.log("\n rastack bootstrap — stand the tier-0 AWS account stack up\n");
|
|
194
|
+
cfg = await promptMissing(cfg);
|
|
195
|
+
}
|
|
196
|
+
const errors = (0, config_1.validateBootstrapConfig)(cfg);
|
|
197
|
+
if (errors.length) {
|
|
198
|
+
for (const e of errors)
|
|
199
|
+
console.error(` ✗ ${e}`);
|
|
200
|
+
console.error(`\n ${errors.length} problem(s). Pass the missing values as flags` +
|
|
201
|
+
` (--owner, --repo, --branch, --region, --prefix).`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
if (flags.print) {
|
|
205
|
+
printPlan(cfg, flags.reuseOidc);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if ((0, io_1.canPrompt)(flags.nonInteractive)) {
|
|
209
|
+
const ok = await confirm(`Create/update bootstrap stack ${(0, config_1.bootstrapStackName)(cfg)} in AWS ${cfg.region} now? [y/N]`);
|
|
210
|
+
if (!ok) {
|
|
211
|
+
console.log("\n Skipped. Re-run to deploy, or `rastack bootstrap --print` for the command.\n");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
standUp(cfg, flags.reuseOidc);
|
|
216
|
+
}
|
|
217
|
+
if (require.main === module) {
|
|
218
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
219
|
+
console.error(` ✗ ${err.message}`);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
});
|
|
222
|
+
}
|
package/dist/rastack-init.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* rastack init [--app <slug>] [--owner <o>] [--repo <r>] [--branch <b>]
|
|
7
7
|
* [--region <r>] [--prefix <p>] [--namespace <n>]
|
|
8
|
-
* [--force] [--no-workflows] [--no-deploy] [--print]
|
|
8
|
+
* [--force] [--no-workflows] [--no-deploy] [--print]
|
|
9
9
|
*
|
|
10
10
|
* This is the one command that takes you from nothing to deployed. It:
|
|
11
11
|
* 1. writes `.rastack/deploy.json` + `.github/workflows/{ci,deploy-backend}.yml`
|