rastack 0.0.42 → 0.0.43
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 +8 -0
- package/dist/rastack-init.d.ts +3 -2
- package/dist/rastack-init.js +23 -44
- package/dist/rastack-update.js +16 -8
- package/dist/update.d.ts +11 -0
- package/dist/update.js +14 -0
- package/package.json +1 -1
- package/src/rastack-init.ts +26 -62
- package/src/rastack-update.ts +22 -9
- package/src/update.ts +14 -0
- package/test/update.spec.ts +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
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.43](https://github.com/theserverkid/reactapistack/compare/v0.0.42...v0.0.43) (2026-07-12)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **init:** require the bootstrap stack instead of creating it ([085c59d](https://github.com/theserverkid/reactapistack/commit/085c59df4a61c5d28312edb7d0895c5b2f0442b5))
|
|
11
|
+
* **update:** silence Node DEP0190 when launching npm on Windows ([959fe61](https://github.com/theserverkid/reactapistack/commit/959fe6177ec4e6e0d634e2751a1099717f2bda3b))
|
|
12
|
+
|
|
5
13
|
### [0.0.42](https://github.com/theserverkid/reactapistack/compare/v0.0.41...v0.0.42) (2026-07-12)
|
|
6
14
|
|
|
7
15
|
|
package/dist/rastack-init.d.ts
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* 1. writes `.rastack/deploy.json` + `.github/workflows/{ci,deploy-backend}.yml`
|
|
12
12
|
* (GitHub coordinates default from the `origin` remote; missing fields are
|
|
13
13
|
* prompted for on a TTY);
|
|
14
|
-
* 2. resolves the tier-0 bootstrap stack's artifacts bucket —
|
|
15
|
-
* bootstrap
|
|
14
|
+
* 2. resolves the tier-0 bootstrap stack's artifacts bucket — erroring with a
|
|
15
|
+
* pointer to `rastack bootstrap` when that account+region stack is missing
|
|
16
|
+
* (init is per-app and never creates account-level infrastructure);
|
|
16
17
|
* 3. uploads the Lambda placeholder bundle to that bucket;
|
|
17
18
|
* 4. deploys the per-app backend (app.yaml) with the resolved ArtifactsBucket;
|
|
18
19
|
* 5. reads the stack Outputs back into the config and prints the two GitHub repo
|
package/dist/rastack-init.js
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* 1. writes `.rastack/deploy.json` + `.github/workflows/{ci,deploy-backend}.yml`
|
|
13
13
|
* (GitHub coordinates default from the `origin` remote; missing fields are
|
|
14
14
|
* prompted for on a TTY);
|
|
15
|
-
* 2. resolves the tier-0 bootstrap stack's artifacts bucket —
|
|
16
|
-
* bootstrap
|
|
15
|
+
* 2. resolves the tier-0 bootstrap stack's artifacts bucket — erroring with a
|
|
16
|
+
* pointer to `rastack bootstrap` when that account+region stack is missing
|
|
17
|
+
* (init is per-app and never creates account-level infrastructure);
|
|
17
18
|
* 3. uploads the Lambda placeholder bundle to that bucket;
|
|
18
19
|
* 4. deploys the per-app backend (app.yaml) with the resolved ArtifactsBucket;
|
|
19
20
|
* 5. reads the stack Outputs back into the config and prints the two GitHub repo
|
|
@@ -187,14 +188,13 @@ function printPlan(cfg) {
|
|
|
187
188
|
const withBucket = (0, config_1.mergeConfig)(cfg, {
|
|
188
189
|
artifactsBucket: bucket ?? "<ArtifactsBucketName from bootstrap stack>",
|
|
189
190
|
});
|
|
190
|
-
const bootstrapTemplate = (0, io_1.resolveDeployAsset)("bootstrap.yaml") ?? undefined;
|
|
191
191
|
const appTemplate = (0, io_1.resolveDeployAsset)("app.yaml") ?? undefined;
|
|
192
192
|
console.log(`\n Deployment plan (${cfg.stackName} @ ${cfg.region})\n`);
|
|
193
|
-
console.log(` ──
|
|
194
|
-
console.log(
|
|
195
|
-
console.log(" ──
|
|
193
|
+
console.log(` ── Prerequisite: the tier-0 bootstrap stack (${(0, config_1.bootstrapStackName)(cfg)}) must exist ──`);
|
|
194
|
+
console.log(` Create it once per account+region (init does not): rastack bootstrap --region ${cfg.region}\n`);
|
|
195
|
+
console.log(" ── 1. Per-app backend (app.yaml) — what init deploys ──");
|
|
196
196
|
console.log("\n " + (0, config_1.deployStackCommand)(withBucket, appTemplate) + "\n");
|
|
197
|
-
console.log(" ──
|
|
197
|
+
console.log(" ── 2. Wire these GitHub repo Variables from the stack Outputs ──");
|
|
198
198
|
console.log(table((0, config_1.repoVariables)(cfg).map((v) => ({
|
|
199
199
|
key: v.name,
|
|
200
200
|
value: v.value || `<${v.from}>`,
|
|
@@ -211,18 +211,22 @@ async function standUp(cfg, interactive) {
|
|
|
211
211
|
" or re-run with --print to see the commands, or --no-deploy to skip.");
|
|
212
212
|
process.exit(1);
|
|
213
213
|
}
|
|
214
|
-
// 1. Resolve the artifacts bucket
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
// 1. Resolve the artifacts bucket from the tier-0 bootstrap stack. That stack
|
|
215
|
+
// is an account+region once-off — the S3 buckets that host the templates +
|
|
216
|
+
// Lambda placeholder, and the GitHub OIDC provider. `init` is a per-app
|
|
217
|
+
// command and deliberately does NOT create it: standing up account-level
|
|
218
|
+
// infrastructure is `rastack bootstrap`'s job (run once by an account admin),
|
|
219
|
+
// so a missing bootstrap stack is a clear, actionable error, not an implicit
|
|
220
|
+
// account mutation buried inside an app deploy.
|
|
221
|
+
const artifactsBucket = (0, io_1.stackOutput)((0, config_1.bootstrapStackName)(cfg), config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
|
|
222
|
+
const templatesBucket = (0, io_1.stackOutput)((0, config_1.bootstrapStackName)(cfg), config_1.BOOTSTRAP_TEMPLATES_OUTPUT, cfg.region);
|
|
217
223
|
if (!artifactsBucket) {
|
|
218
|
-
console.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
process.exit(1);
|
|
225
|
-
}
|
|
224
|
+
console.error(`\n ✗ Bootstrap stack ${(0, config_1.bootstrapStackName)(cfg)} not found in ${cfg.region}.\n` +
|
|
225
|
+
" It is the once-per-account+region step that hosts the CloudFormation\n" +
|
|
226
|
+
" templates and the Lambda placeholder every app deploy reads. Create it\n" +
|
|
227
|
+
" first (an account admin does this once), then re-run `rastack init`:\n\n" +
|
|
228
|
+
` rastack bootstrap --region ${cfg.region}\n`);
|
|
229
|
+
process.exit(1);
|
|
226
230
|
}
|
|
227
231
|
console.log(` ✓ artifacts bucket: ${artifactsBucket}`);
|
|
228
232
|
// 2. Upload the placeholder the function is created from.
|
|
@@ -305,31 +309,6 @@ function wireRepoVariables(cfg, interactive) {
|
|
|
305
309
|
}
|
|
306
310
|
}
|
|
307
311
|
}
|
|
308
|
-
/** Deploy the bootstrap stack, retrying once against an existing OIDC provider. */
|
|
309
|
-
function deployBootstrap(cfg) {
|
|
310
|
-
const template = (0, io_1.resolveDeployAsset)("bootstrap.yaml");
|
|
311
|
-
if (!template) {
|
|
312
|
-
console.error(" ✗ bootstrap.yaml template not found in the rastack package. Reinstall\n" +
|
|
313
|
-
" rastack (`npm i -g rastack`) or run from the framework repo.");
|
|
314
|
-
process.exit(1);
|
|
315
|
-
}
|
|
316
|
-
try {
|
|
317
|
-
(0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg, false, template));
|
|
318
|
-
}
|
|
319
|
-
catch {
|
|
320
|
-
// The most common cause is an account that already has a GitHub OIDC
|
|
321
|
-
// provider (only one is allowed per URL). Retry telling the template to
|
|
322
|
-
// reuse it instead of creating a second.
|
|
323
|
-
console.log(" • retrying bootstrap, reusing the account's existing GitHub OIDC provider");
|
|
324
|
-
try {
|
|
325
|
-
(0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg, true, template));
|
|
326
|
-
}
|
|
327
|
-
catch (err) {
|
|
328
|
-
console.error(`\n ✗ bootstrap stack deploy failed.\n ${err.message}`);
|
|
329
|
-
process.exit(1);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
312
|
function printSummary(cfg) {
|
|
334
313
|
console.log(`\n ✓ ${cfg.app} is deployed (${cfg.stackName} @ ${cfg.region})`);
|
|
335
314
|
if (cfg.outputs?.apiUrl)
|
|
@@ -399,7 +378,7 @@ async function main(argv) {
|
|
|
399
378
|
return;
|
|
400
379
|
}
|
|
401
380
|
if ((0, io_1.canPrompt)()) {
|
|
402
|
-
const ok = await confirm(`Deploy ${cfg.stackName} to AWS ${cfg.region} now? (
|
|
381
|
+
const ok = await confirm(`Deploy ${cfg.stackName} to AWS ${cfg.region} now? (requires the bootstrap stack to exist) [y/N]`);
|
|
403
382
|
if (!ok) {
|
|
404
383
|
console.log("\n Skipped the deploy. Re-run to deploy, or `rastack init --print` for the commands.\n");
|
|
405
384
|
return;
|
package/dist/rastack-update.js
CHANGED
|
@@ -74,15 +74,24 @@ function readVersion(file) {
|
|
|
74
74
|
return "unknown";
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Run npm and return stdout, or throw on failure. Windows must launch the
|
|
79
|
+
* `npm.cmd` shim through a shell; we pass one command string to `execSync`
|
|
80
|
+
* rather than an args array with `shell: true` (Node 22+ DEP0190 — args would be
|
|
81
|
+
* concatenated unescaped). The only non-literal token, the version, is already
|
|
82
|
+
* sanitized (`sanitizeVersion`). POSIX keeps the shell-free `execFile`.
|
|
83
|
+
*/
|
|
84
|
+
function runNpm(args, opts) {
|
|
85
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
86
|
+
return NPM_SHELL
|
|
87
|
+
? (0, child_process_1.execSync)((0, update_1.npmShellCommand)(NPM, args), opts)
|
|
88
|
+
: (0, child_process_1.execFileSync)(NPM, args, opts);
|
|
89
|
+
}
|
|
77
90
|
/** Capture `npm <args>` stdout, or null when npm is absent / the call fails. */
|
|
78
91
|
function npmCapture(args) {
|
|
79
92
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
shell: NPM_SHELL,
|
|
83
|
-
})
|
|
84
|
-
.toString()
|
|
85
|
-
.trim();
|
|
93
|
+
const out = runNpm(args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
94
|
+
return (out ?? "").toString().trim();
|
|
86
95
|
}
|
|
87
96
|
catch {
|
|
88
97
|
return null;
|
|
@@ -129,8 +138,7 @@ function main(argv) {
|
|
|
129
138
|
// the `.cmd` shim is launched through a shell (EINVAL otherwise); the only
|
|
130
139
|
// caller-supplied token — the version — is sanitized in `parseUpdateFlags`,
|
|
131
140
|
// and every other arg is a fixed literal, so nothing injectable reaches it.
|
|
132
|
-
|
|
133
|
-
(0, child_process_1.execFileSync)(NPM, args, { stdio: "inherit", env, shell: NPM_SHELL });
|
|
141
|
+
runNpm(args, { stdio: "inherit", env });
|
|
134
142
|
}
|
|
135
143
|
catch (err) {
|
|
136
144
|
console.error(`\n ✗ npm failed to update rastack. Re-run for detail:\n npm install -g ${(0, update_1.npmInstallArgs)(flags.version)[2]}\n ${err.message}`);
|
package/dist/update.d.ts
CHANGED
|
@@ -52,6 +52,17 @@ export declare function npmBinary(platform?: string): string;
|
|
|
52
52
|
* shell means no injection surface).
|
|
53
53
|
*/
|
|
54
54
|
export declare function npmSpawnShell(platform?: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* The single command string used to launch npm on platforms that must go through
|
|
57
|
+
* a shell (Windows — see `npmSpawnShell`). Node 22+ deprecates passing an args
|
|
58
|
+
* *array* together with `shell: true` (DEP0190: the args are concatenated, not
|
|
59
|
+
* escaped), so the shell path builds one string and runs it via `execSync`
|
|
60
|
+
* instead. Joining with spaces is safe here because every token is a fixed
|
|
61
|
+
* literal except the version, which `sanitizeVersion` constrains to
|
|
62
|
+
* `[A-Za-z0-9._-]` (no shell metacharacters). POSIX never uses this — it keeps
|
|
63
|
+
* the shell-free, array-based `execFile`, which has no concatenation at all.
|
|
64
|
+
*/
|
|
65
|
+
export declare function npmShellCommand(binary: string, args: string[]): string;
|
|
55
66
|
/** The npm package spec to install for a target version. */
|
|
56
67
|
export declare function installSpec(version: string): string;
|
|
57
68
|
/** The `npm <args>` (after the `npm` binary) that installs the target globally. */
|
package/dist/update.js
CHANGED
|
@@ -15,6 +15,7 @@ exports.parseUpdateFlags = parseUpdateFlags;
|
|
|
15
15
|
exports.sanitizeVersion = sanitizeVersion;
|
|
16
16
|
exports.npmBinary = npmBinary;
|
|
17
17
|
exports.npmSpawnShell = npmSpawnShell;
|
|
18
|
+
exports.npmShellCommand = npmShellCommand;
|
|
18
19
|
exports.installSpec = installSpec;
|
|
19
20
|
exports.npmInstallArgs = npmInstallArgs;
|
|
20
21
|
exports.globalPrefixFromPackageDir = globalPrefixFromPackageDir;
|
|
@@ -88,6 +89,19 @@ function npmBinary(platform = process.platform) {
|
|
|
88
89
|
function npmSpawnShell(platform = process.platform) {
|
|
89
90
|
return platform === "win32";
|
|
90
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* The single command string used to launch npm on platforms that must go through
|
|
94
|
+
* a shell (Windows — see `npmSpawnShell`). Node 22+ deprecates passing an args
|
|
95
|
+
* *array* together with `shell: true` (DEP0190: the args are concatenated, not
|
|
96
|
+
* escaped), so the shell path builds one string and runs it via `execSync`
|
|
97
|
+
* instead. Joining with spaces is safe here because every token is a fixed
|
|
98
|
+
* literal except the version, which `sanitizeVersion` constrains to
|
|
99
|
+
* `[A-Za-z0-9._-]` (no shell metacharacters). POSIX never uses this — it keeps
|
|
100
|
+
* the shell-free, array-based `execFile`, which has no concatenation at all.
|
|
101
|
+
*/
|
|
102
|
+
function npmShellCommand(binary, args) {
|
|
103
|
+
return [binary, ...args].join(" ");
|
|
104
|
+
}
|
|
91
105
|
/** The npm package spec to install for a target version. */
|
|
92
106
|
function installSpec(version) {
|
|
93
107
|
return !version || version === "latest" ? "rastack" : `rastack@${version}`;
|
package/package.json
CHANGED
package/src/rastack-init.ts
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* 1. writes `.rastack/deploy.json` + `.github/workflows/{ci,deploy-backend}.yml`
|
|
13
13
|
* (GitHub coordinates default from the `origin` remote; missing fields are
|
|
14
14
|
* prompted for on a TTY);
|
|
15
|
-
* 2. resolves the tier-0 bootstrap stack's artifacts bucket —
|
|
16
|
-
* bootstrap
|
|
15
|
+
* 2. resolves the tier-0 bootstrap stack's artifacts bucket — erroring with a
|
|
16
|
+
* pointer to `rastack bootstrap` when that account+region stack is missing
|
|
17
|
+
* (init is per-app and never creates account-level infrastructure);
|
|
17
18
|
* 3. uploads the Lambda placeholder bundle to that bucket;
|
|
18
19
|
* 4. deploys the per-app backend (app.yaml) with the resolved ArtifactsBucket;
|
|
19
20
|
* 5. reads the stack Outputs back into the config and prints the two GitHub repo
|
|
@@ -33,8 +34,6 @@ import {
|
|
|
33
34
|
DeployConfig,
|
|
34
35
|
DeployOutputs,
|
|
35
36
|
PartialDeployConfig,
|
|
36
|
-
bootstrapStackArgs,
|
|
37
|
-
bootstrapStackCommand,
|
|
38
37
|
bootstrapStackName,
|
|
39
38
|
deployStackArgs,
|
|
40
39
|
deployStackCommand,
|
|
@@ -210,17 +209,18 @@ function printPlan(cfg: DeployConfig): void {
|
|
|
210
209
|
const withBucket = mergeConfig(cfg, {
|
|
211
210
|
artifactsBucket: bucket ?? "<ArtifactsBucketName from bootstrap stack>",
|
|
212
211
|
});
|
|
213
|
-
const bootstrapTemplate = resolveDeployAsset("bootstrap.yaml") ?? undefined;
|
|
214
212
|
const appTemplate = resolveDeployAsset("app.yaml") ?? undefined;
|
|
215
213
|
console.log(`\n Deployment plan (${cfg.stackName} @ ${cfg.region})\n`);
|
|
216
214
|
console.log(
|
|
217
|
-
` ──
|
|
215
|
+
` ── Prerequisite: the tier-0 bootstrap stack (${bootstrapStackName(cfg)}) must exist ──`,
|
|
218
216
|
);
|
|
219
|
-
console.log(
|
|
220
|
-
|
|
217
|
+
console.log(
|
|
218
|
+
` Create it once per account+region (init does not): rastack bootstrap --region ${cfg.region}\n`,
|
|
219
|
+
);
|
|
220
|
+
console.log(" ── 1. Per-app backend (app.yaml) — what init deploys ──");
|
|
221
221
|
console.log("\n " + deployStackCommand(withBucket, appTemplate) + "\n");
|
|
222
222
|
console.log(
|
|
223
|
-
" ──
|
|
223
|
+
" ── 2. Wire these GitHub repo Variables from the stack Outputs ──",
|
|
224
224
|
);
|
|
225
225
|
console.log(
|
|
226
226
|
table(
|
|
@@ -249,38 +249,32 @@ async function standUp(
|
|
|
249
249
|
process.exit(1);
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
// 1. Resolve the artifacts bucket
|
|
253
|
-
|
|
252
|
+
// 1. Resolve the artifacts bucket from the tier-0 bootstrap stack. That stack
|
|
253
|
+
// is an account+region once-off — the S3 buckets that host the templates +
|
|
254
|
+
// Lambda placeholder, and the GitHub OIDC provider. `init` is a per-app
|
|
255
|
+
// command and deliberately does NOT create it: standing up account-level
|
|
256
|
+
// infrastructure is `rastack bootstrap`'s job (run once by an account admin),
|
|
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(
|
|
254
260
|
bootstrapStackName(cfg),
|
|
255
261
|
BOOTSTRAP_ARTIFACTS_OUTPUT,
|
|
256
262
|
cfg.region,
|
|
257
263
|
);
|
|
258
|
-
|
|
264
|
+
const templatesBucket = stackOutput(
|
|
259
265
|
bootstrapStackName(cfg),
|
|
260
266
|
BOOTSTRAP_TEMPLATES_OUTPUT,
|
|
261
267
|
cfg.region,
|
|
262
268
|
);
|
|
263
269
|
if (!artifactsBucket) {
|
|
264
|
-
console.
|
|
265
|
-
`\n
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
BOOTSTRAP_ARTIFACTS_OUTPUT,
|
|
271
|
-
cfg.region,
|
|
272
|
-
);
|
|
273
|
-
templatesBucket = stackOutput(
|
|
274
|
-
bootstrapStackName(cfg),
|
|
275
|
-
BOOTSTRAP_TEMPLATES_OUTPUT,
|
|
276
|
-
cfg.region,
|
|
270
|
+
console.error(
|
|
271
|
+
`\n ✗ Bootstrap stack ${bootstrapStackName(cfg)} not found in ${cfg.region}.\n` +
|
|
272
|
+
" It is the once-per-account+region step that hosts the CloudFormation\n" +
|
|
273
|
+
" templates and the Lambda placeholder every app deploy reads. Create it\n" +
|
|
274
|
+
" first (an account admin does this once), then re-run `rastack init`:\n\n" +
|
|
275
|
+
` rastack bootstrap --region ${cfg.region}\n`,
|
|
277
276
|
);
|
|
278
|
-
|
|
279
|
-
console.error(
|
|
280
|
-
" ✗ bootstrap stack created but its ArtifactsBucket output could not be read.",
|
|
281
|
-
);
|
|
282
|
-
process.exit(1);
|
|
283
|
-
}
|
|
277
|
+
process.exit(1);
|
|
284
278
|
}
|
|
285
279
|
console.log(` ✓ artifacts bucket: ${artifactsBucket}`);
|
|
286
280
|
|
|
@@ -381,36 +375,6 @@ function wireRepoVariables(cfg: DeployConfig, interactive: boolean): void {
|
|
|
381
375
|
}
|
|
382
376
|
}
|
|
383
377
|
|
|
384
|
-
/** Deploy the bootstrap stack, retrying once against an existing OIDC provider. */
|
|
385
|
-
function deployBootstrap(cfg: DeployConfig): void {
|
|
386
|
-
const template = resolveDeployAsset("bootstrap.yaml");
|
|
387
|
-
if (!template) {
|
|
388
|
-
console.error(
|
|
389
|
-
" ✗ bootstrap.yaml template not found in the rastack package. Reinstall\n" +
|
|
390
|
-
" rastack (`npm i -g rastack`) or run from the framework repo.",
|
|
391
|
-
);
|
|
392
|
-
process.exit(1);
|
|
393
|
-
}
|
|
394
|
-
try {
|
|
395
|
-
awsRun(bootstrapStackArgs(cfg, false, template));
|
|
396
|
-
} catch {
|
|
397
|
-
// The most common cause is an account that already has a GitHub OIDC
|
|
398
|
-
// provider (only one is allowed per URL). Retry telling the template to
|
|
399
|
-
// reuse it instead of creating a second.
|
|
400
|
-
console.log(
|
|
401
|
-
" • retrying bootstrap, reusing the account's existing GitHub OIDC provider",
|
|
402
|
-
);
|
|
403
|
-
try {
|
|
404
|
-
awsRun(bootstrapStackArgs(cfg, true, template));
|
|
405
|
-
} catch (err) {
|
|
406
|
-
console.error(
|
|
407
|
-
`\n ✗ bootstrap stack deploy failed.\n ${(err as Error).message}`,
|
|
408
|
-
);
|
|
409
|
-
process.exit(1);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
378
|
function printSummary(cfg: DeployConfig): void {
|
|
415
379
|
console.log(
|
|
416
380
|
`\n ✓ ${cfg.app} is deployed (${cfg.stackName} @ ${cfg.region})`,
|
|
@@ -502,7 +466,7 @@ async function main(argv: string[]): Promise<void> {
|
|
|
502
466
|
|
|
503
467
|
if (canPrompt()) {
|
|
504
468
|
const ok = await confirm(
|
|
505
|
-
`Deploy ${cfg.stackName} to AWS ${cfg.region} now? (
|
|
469
|
+
`Deploy ${cfg.stackName} to AWS ${cfg.region} now? (requires the bootstrap stack to exist) [y/N]`,
|
|
506
470
|
);
|
|
507
471
|
if (!ok) {
|
|
508
472
|
console.log(
|
package/src/rastack-update.ts
CHANGED
|
@@ -17,13 +17,14 @@
|
|
|
17
17
|
* global location. Pure decisions live in `update.ts`; this is the IO wrapper.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { execFileSync } from "child_process";
|
|
20
|
+
import { execFileSync, execSync } from "child_process";
|
|
21
21
|
import { readFileSync } from "fs";
|
|
22
22
|
import * as path from "path";
|
|
23
23
|
import {
|
|
24
24
|
globalPrefixFromPackageDir,
|
|
25
25
|
npmBinary,
|
|
26
26
|
npmInstallArgs,
|
|
27
|
+
npmShellCommand,
|
|
27
28
|
npmSpawnShell,
|
|
28
29
|
parseUpdateFlags,
|
|
29
30
|
updateAvailable,
|
|
@@ -52,15 +53,28 @@ function readVersion(file: string): string {
|
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Run npm and return stdout, or throw on failure. Windows must launch the
|
|
58
|
+
* `npm.cmd` shim through a shell; we pass one command string to `execSync`
|
|
59
|
+
* rather than an args array with `shell: true` (Node 22+ DEP0190 — args would be
|
|
60
|
+
* concatenated unescaped). The only non-literal token, the version, is already
|
|
61
|
+
* sanitized (`sanitizeVersion`). POSIX keeps the shell-free `execFile`.
|
|
62
|
+
*/
|
|
63
|
+
function runNpm(
|
|
64
|
+
args: string[],
|
|
65
|
+
opts: { stdio: "inherit" | ["ignore", "pipe", "pipe"]; env?: NodeJS.ProcessEnv },
|
|
66
|
+
): Buffer | null {
|
|
67
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
68
|
+
return NPM_SHELL
|
|
69
|
+
? execSync(npmShellCommand(NPM, args), opts)
|
|
70
|
+
: execFileSync(NPM, args, opts);
|
|
71
|
+
}
|
|
72
|
+
|
|
55
73
|
/** Capture `npm <args>` stdout, or null when npm is absent / the call fails. */
|
|
56
74
|
function npmCapture(args: string[]): string | null {
|
|
57
75
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
shell: NPM_SHELL,
|
|
61
|
-
})
|
|
62
|
-
.toString()
|
|
63
|
-
.trim();
|
|
76
|
+
const out = runNpm(args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
77
|
+
return (out ?? "").toString().trim();
|
|
64
78
|
} catch {
|
|
65
79
|
return null;
|
|
66
80
|
}
|
|
@@ -116,8 +130,7 @@ function main(argv: string[]): void {
|
|
|
116
130
|
// the `.cmd` shim is launched through a shell (EINVAL otherwise); the only
|
|
117
131
|
// caller-supplied token — the version — is sanitized in `parseUpdateFlags`,
|
|
118
132
|
// and every other arg is a fixed literal, so nothing injectable reaches it.
|
|
119
|
-
|
|
120
|
-
execFileSync(NPM, args, { stdio: "inherit", env, shell: NPM_SHELL });
|
|
133
|
+
runNpm(args, { stdio: "inherit", env });
|
|
121
134
|
} catch (err) {
|
|
122
135
|
console.error(
|
|
123
136
|
`\n ✗ npm failed to update rastack. Re-run for detail:\n npm install -g ${npmInstallArgs(flags.version)[2]}\n ${(err as Error).message}`,
|
package/src/update.ts
CHANGED
|
@@ -90,6 +90,20 @@ export function npmSpawnShell(platform: string = process.platform): boolean {
|
|
|
90
90
|
return platform === "win32";
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* The single command string used to launch npm on platforms that must go through
|
|
95
|
+
* a shell (Windows — see `npmSpawnShell`). Node 22+ deprecates passing an args
|
|
96
|
+
* *array* together with `shell: true` (DEP0190: the args are concatenated, not
|
|
97
|
+
* escaped), so the shell path builds one string and runs it via `execSync`
|
|
98
|
+
* instead. Joining with spaces is safe here because every token is a fixed
|
|
99
|
+
* literal except the version, which `sanitizeVersion` constrains to
|
|
100
|
+
* `[A-Za-z0-9._-]` (no shell metacharacters). POSIX never uses this — it keeps
|
|
101
|
+
* the shell-free, array-based `execFile`, which has no concatenation at all.
|
|
102
|
+
*/
|
|
103
|
+
export function npmShellCommand(binary: string, args: string[]): string {
|
|
104
|
+
return [binary, ...args].join(" ");
|
|
105
|
+
}
|
|
106
|
+
|
|
93
107
|
/** The npm package spec to install for a target version. */
|
|
94
108
|
export function installSpec(version: string): string {
|
|
95
109
|
return !version || version === "latest" ? "rastack" : `rastack@${version}`;
|
package/test/update.spec.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
installSpec,
|
|
5
5
|
npmBinary,
|
|
6
6
|
npmInstallArgs,
|
|
7
|
+
npmShellCommand,
|
|
7
8
|
npmSpawnShell,
|
|
8
9
|
parseUpdateFlags,
|
|
9
10
|
sanitizeVersion,
|
|
@@ -67,6 +68,18 @@ describe("update — npm spec + args", () => {
|
|
|
67
68
|
expect(npmSpawnShell("linux")).toBe(false);
|
|
68
69
|
expect(npmSpawnShell("darwin")).toBe(false);
|
|
69
70
|
});
|
|
71
|
+
|
|
72
|
+
it("joins the shell command as one string (avoids the Node DEP0190 warning)", () => {
|
|
73
|
+
// The Windows path runs npm via `execSync(<string>)` rather than
|
|
74
|
+
// `execFileSync(npm, args, { shell: true })`, which Node 22+ deprecates
|
|
75
|
+
// (args concatenated, not escaped). The sanitized version keeps it safe.
|
|
76
|
+
expect(npmShellCommand("npm.cmd", npmInstallArgs("latest"))).toBe(
|
|
77
|
+
"npm.cmd install -g rastack --no-fund --no-audit --loglevel=error",
|
|
78
|
+
);
|
|
79
|
+
expect(npmShellCommand("npm.cmd", npmInstallArgs("0.0.42"))).toContain(
|
|
80
|
+
"rastack@0.0.42",
|
|
81
|
+
);
|
|
82
|
+
});
|
|
70
83
|
});
|
|
71
84
|
|
|
72
85
|
describe("update — version sanitization", () => {
|