rastack 0.0.35 → 0.0.37
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 +4 -0
- package/dist/deploy/config.d.ts +33 -5
- package/dist/deploy/config.js +98 -24
- package/dist/deploy/index.d.ts +1 -0
- package/dist/deploy/index.js +1 -0
- package/dist/deploy/io.d.ts +19 -0
- package/dist/deploy/io.js +81 -0
- package/dist/deploy/zip.d.ts +24 -0
- package/dist/deploy/zip.js +95 -0
- package/dist/rastack-ci.js +1 -1
- package/dist/rastack-init.d.ts +18 -7
- package/dist/rastack-init.js +187 -18
- package/dist/rastack-update.d.ts +18 -0
- package/dist/rastack-update.js +139 -0
- package/dist/rastack.d.ts +3 -2
- package/dist/rastack.js +16 -8
- package/dist/update.d.ts +48 -0
- package/dist/update.js +110 -0
- package/package.json +1 -1
- package/src/deploy/config.ts +126 -22
- package/src/deploy/index.ts +1 -0
- package/src/deploy/io.ts +88 -1
- package/src/deploy/zip.ts +109 -0
- package/src/rastack-ci.ts +1 -1
- package/src/rastack-init.ts +271 -21
- package/src/rastack-update.ts +124 -0
- package/src/rastack.ts +16 -8
- package/src/update.ts +114 -0
- package/test/deploy.spec.ts +100 -12
- package/test/update.spec.ts +95 -0
- package/dist/rastack-deploy.d.ts +0 -20
- package/dist/rastack-deploy.js +0 -196
- package/src/rastack-deploy.ts +0 -257
package/src/rastack-init.ts
CHANGED
|
@@ -1,34 +1,60 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* `rastack init` —
|
|
4
|
+
* `rastack init` — scaffold a repo's deploy config + GitHub Actions AND stand the
|
|
5
|
+
* serverless backend up.
|
|
5
6
|
*
|
|
6
7
|
* rastack init [--app <slug>] [--owner <o>] [--repo <r>] [--branch <b>]
|
|
7
|
-
* [--region <r>] [--
|
|
8
|
+
* [--region <r>] [--prefix <p>] [--namespace <n>]
|
|
9
|
+
* [--force] [--no-workflows] [--no-deploy] [--print] [--yes]
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* This is the one command that takes you from nothing to deployed. It:
|
|
12
|
+
* 1. writes `.rastack/deploy.json` + `.github/workflows/{ci,deploy-backend}.yml`
|
|
13
|
+
* (GitHub coordinates default from the `origin` remote; missing fields are
|
|
14
|
+
* prompted for on a TTY);
|
|
15
|
+
* 2. resolves the tier-0 bootstrap stack's artifacts bucket — and CREATES the
|
|
16
|
+
* bootstrap stack if it does not exist yet;
|
|
17
|
+
* 3. uploads the Lambda placeholder bundle to that bucket;
|
|
18
|
+
* 4. deploys the per-app backend (app.yaml) with the resolved ArtifactsBucket;
|
|
19
|
+
* 5. reads the stack Outputs back into the config and prints the two GitHub repo
|
|
20
|
+
* Variables CI needs.
|
|
21
|
+
*
|
|
22
|
+
* Thereafter the recurring code deploy is CI's job — `rastack ci deploy`, which
|
|
23
|
+
* `rastack deploy` also runs. `--no-deploy` scaffolds only; `--print` shows the
|
|
24
|
+
* exact single-line `aws cloudformation deploy` commands without running them.
|
|
14
25
|
*/
|
|
15
26
|
|
|
16
27
|
import * as fs from "fs";
|
|
17
28
|
import * as path from "path";
|
|
18
29
|
import {
|
|
30
|
+
APP_STACK_OUTPUTS,
|
|
31
|
+
BOOTSTRAP_ARTIFACTS_OUTPUT,
|
|
32
|
+
BOOTSTRAP_TEMPLATES_OUTPUT,
|
|
19
33
|
DeployConfig,
|
|
34
|
+
DeployOutputs,
|
|
20
35
|
PartialDeployConfig,
|
|
36
|
+
bootstrapStackArgs,
|
|
37
|
+
bootstrapStackCommand,
|
|
38
|
+
bootstrapStackName,
|
|
39
|
+
deployStackArgs,
|
|
40
|
+
deployStackCommand,
|
|
21
41
|
mergeConfig,
|
|
22
42
|
normalizeConfig,
|
|
23
43
|
parseGitHubRemote,
|
|
44
|
+
repoVariables,
|
|
24
45
|
validateConfig,
|
|
25
46
|
} from "./deploy/config";
|
|
26
47
|
import { scaffoldFiles } from "./deploy/workflows";
|
|
27
48
|
import {
|
|
28
49
|
ask,
|
|
50
|
+
awsAvailable,
|
|
51
|
+
awsRun,
|
|
29
52
|
canPrompt,
|
|
30
53
|
gitRemoteUrl,
|
|
31
54
|
loadConfig,
|
|
55
|
+
saveConfig,
|
|
56
|
+
stackOutput,
|
|
57
|
+
uploadPlaceholder,
|
|
32
58
|
withPrompt,
|
|
33
59
|
} from "./deploy/io";
|
|
34
60
|
|
|
@@ -36,6 +62,8 @@ interface Flags {
|
|
|
36
62
|
patch: PartialDeployConfig;
|
|
37
63
|
force: boolean;
|
|
38
64
|
noWorkflows: boolean;
|
|
65
|
+
noDeploy: boolean;
|
|
66
|
+
print: boolean;
|
|
39
67
|
nonInteractive: boolean;
|
|
40
68
|
}
|
|
41
69
|
|
|
@@ -43,6 +71,8 @@ function parseFlags(argv: string[]): Flags {
|
|
|
43
71
|
const patch: PartialDeployConfig = { github: {} };
|
|
44
72
|
let force = false;
|
|
45
73
|
let noWorkflows = false;
|
|
74
|
+
let noDeploy = false;
|
|
75
|
+
let print = false;
|
|
46
76
|
let nonInteractive = false;
|
|
47
77
|
for (let i = 0; i < argv.length; i++) {
|
|
48
78
|
const a = argv[i];
|
|
@@ -66,12 +96,22 @@ function parseFlags(argv: string[]): Flags {
|
|
|
66
96
|
case "--prefix":
|
|
67
97
|
patch.namePrefix = next();
|
|
68
98
|
break;
|
|
99
|
+
case "--namespace":
|
|
100
|
+
patch.namespace = next();
|
|
101
|
+
break;
|
|
69
102
|
case "--force":
|
|
70
103
|
force = true;
|
|
71
104
|
break;
|
|
72
105
|
case "--no-workflows":
|
|
73
106
|
noWorkflows = true;
|
|
74
107
|
break;
|
|
108
|
+
case "--no-deploy":
|
|
109
|
+
noDeploy = true;
|
|
110
|
+
break;
|
|
111
|
+
case "--print":
|
|
112
|
+
case "--dry-run":
|
|
113
|
+
print = true;
|
|
114
|
+
break;
|
|
75
115
|
case "--yes":
|
|
76
116
|
case "-y":
|
|
77
117
|
case "--non-interactive":
|
|
@@ -84,7 +124,7 @@ function parseFlags(argv: string[]): Flags {
|
|
|
84
124
|
}
|
|
85
125
|
}
|
|
86
126
|
}
|
|
87
|
-
return { patch, force, noWorkflows, nonInteractive };
|
|
127
|
+
return { patch, force, noWorkflows, noDeploy, print, nonInteractive };
|
|
88
128
|
}
|
|
89
129
|
|
|
90
130
|
/** Seed a config: existing file → flags → git remote → defaults. */
|
|
@@ -128,31 +168,220 @@ async function promptMissing(cfg: DeployConfig): Promise<DeployConfig> {
|
|
|
128
168
|
});
|
|
129
169
|
}
|
|
130
170
|
|
|
171
|
+
/** Yes/no confirmation with a default of "no" — gates the cloud-mutating steps. */
|
|
172
|
+
async function confirm(question: string): Promise<boolean> {
|
|
173
|
+
return withPrompt(async (rl) => {
|
|
174
|
+
const answer = await ask(rl, question, "no");
|
|
175
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Pad `Key Value` rows into an aligned block. */
|
|
180
|
+
function table(rows: { key: string; value: string }[]): string {
|
|
181
|
+
const width = Math.max(0, ...rows.map((r) => r.key.length));
|
|
182
|
+
return rows.map((r) => ` ${r.key.padEnd(width)} ${r.value}`).join("\n");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Read the app.yaml stack Outputs into a `DeployOutputs`, skipping any that are
|
|
187
|
+
* not resolvable yet (best-effort — a freshly created stack has them all).
|
|
188
|
+
*/
|
|
189
|
+
function readAppOutputs(cfg: DeployConfig): DeployOutputs {
|
|
190
|
+
const outputs: DeployOutputs = {};
|
|
191
|
+
for (const { key, field } of APP_STACK_OUTPUTS) {
|
|
192
|
+
const value = stackOutput(cfg.stackName, key, cfg.region);
|
|
193
|
+
if (value) outputs[field] = value;
|
|
194
|
+
}
|
|
195
|
+
return outputs;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Print the exact commands `init` would run, without touching the cloud
|
|
200
|
+
* (`--print`). The artifacts bucket is read from the bootstrap stack when the
|
|
201
|
+
* AWS CLI is available, else shown as a placeholder.
|
|
202
|
+
*/
|
|
203
|
+
function printPlan(cfg: DeployConfig): void {
|
|
204
|
+
const bucket = awsAvailable()
|
|
205
|
+
? stackOutput(
|
|
206
|
+
bootstrapStackName(cfg),
|
|
207
|
+
BOOTSTRAP_ARTIFACTS_OUTPUT,
|
|
208
|
+
cfg.region,
|
|
209
|
+
)
|
|
210
|
+
: null;
|
|
211
|
+
const withBucket = mergeConfig(cfg, {
|
|
212
|
+
artifactsBucket: bucket ?? "<ArtifactsBucketName from bootstrap stack>",
|
|
213
|
+
});
|
|
214
|
+
console.log(`\n Deployment plan (${cfg.stackName} @ ${cfg.region})\n`);
|
|
215
|
+
console.log(
|
|
216
|
+
` ── 1. Bootstrap stack (${bootstrapStackName(cfg)}) — run only if it does not exist ──`,
|
|
217
|
+
);
|
|
218
|
+
console.log("\n " + bootstrapStackCommand(cfg) + "\n");
|
|
219
|
+
console.log(" ── 2. Per-app backend (app.yaml) ──");
|
|
220
|
+
console.log("\n " + deployStackCommand(withBucket) + "\n");
|
|
221
|
+
console.log(
|
|
222
|
+
" ── 3. Wire these GitHub repo Variables from the stack Outputs ──",
|
|
223
|
+
);
|
|
224
|
+
console.log(
|
|
225
|
+
table(
|
|
226
|
+
repoVariables(cfg).map((v) => ({
|
|
227
|
+
key: v.name,
|
|
228
|
+
value: v.value || `<${v.from}>`,
|
|
229
|
+
})),
|
|
230
|
+
) + "\n",
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Stand the infrastructure up: ensure the bootstrap stack + artifacts bucket,
|
|
236
|
+
* upload the Lambda placeholder, deploy app.yaml, and read the Outputs back into
|
|
237
|
+
* the config. Returns the config enriched with the resolved buckets + outputs.
|
|
238
|
+
*/
|
|
239
|
+
async function standUp(cfg: DeployConfig): Promise<DeployConfig> {
|
|
240
|
+
if (!awsAvailable()) {
|
|
241
|
+
console.error(
|
|
242
|
+
" ✗ the AWS CLI is not on PATH. Install it and configure credentials,\n" +
|
|
243
|
+
" or re-run with --print to see the commands, or --no-deploy to skip.",
|
|
244
|
+
);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 1. Resolve the artifacts bucket; bootstrap the tier-0 stack if it is absent.
|
|
249
|
+
let artifactsBucket = stackOutput(
|
|
250
|
+
bootstrapStackName(cfg),
|
|
251
|
+
BOOTSTRAP_ARTIFACTS_OUTPUT,
|
|
252
|
+
cfg.region,
|
|
253
|
+
);
|
|
254
|
+
let templatesBucket = stackOutput(
|
|
255
|
+
bootstrapStackName(cfg),
|
|
256
|
+
BOOTSTRAP_TEMPLATES_OUTPUT,
|
|
257
|
+
cfg.region,
|
|
258
|
+
);
|
|
259
|
+
if (!artifactsBucket) {
|
|
260
|
+
console.log(
|
|
261
|
+
`\n▸ Bootstrap stack ${bootstrapStackName(cfg)} not found — creating it (once per account+region).`,
|
|
262
|
+
);
|
|
263
|
+
deployBootstrap(cfg);
|
|
264
|
+
artifactsBucket = stackOutput(
|
|
265
|
+
bootstrapStackName(cfg),
|
|
266
|
+
BOOTSTRAP_ARTIFACTS_OUTPUT,
|
|
267
|
+
cfg.region,
|
|
268
|
+
);
|
|
269
|
+
templatesBucket = stackOutput(
|
|
270
|
+
bootstrapStackName(cfg),
|
|
271
|
+
BOOTSTRAP_TEMPLATES_OUTPUT,
|
|
272
|
+
cfg.region,
|
|
273
|
+
);
|
|
274
|
+
if (!artifactsBucket) {
|
|
275
|
+
console.error(
|
|
276
|
+
" ✗ bootstrap stack created but its ArtifactsBucket output could not be read.",
|
|
277
|
+
);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
console.log(` ✓ artifacts bucket: ${artifactsBucket}`);
|
|
282
|
+
|
|
283
|
+
// 2. Upload the placeholder the function is created from.
|
|
284
|
+
console.log("\n▸ Uploading the Lambda placeholder bundle");
|
|
285
|
+
if (!uploadPlaceholder(artifactsBucket, cfg.region)) {
|
|
286
|
+
console.error(
|
|
287
|
+
" ✗ deploy/cloudformation/placeholder/bootstrap not found — cannot create the function.",
|
|
288
|
+
);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 3. Deploy the per-app backend with the resolved bucket wired in.
|
|
293
|
+
cfg = mergeConfig(cfg, {
|
|
294
|
+
artifactsBucket,
|
|
295
|
+
templatesBucket: templatesBucket ?? undefined,
|
|
296
|
+
});
|
|
297
|
+
console.log(`\n▸ Deploying ${cfg.stackName}`);
|
|
298
|
+
try {
|
|
299
|
+
awsRun(deployStackArgs(cfg));
|
|
300
|
+
} catch (err) {
|
|
301
|
+
console.error(
|
|
302
|
+
`\n ✗ aws cloudformation deploy failed for ${cfg.stackName}.\n ${(err as Error).message}`,
|
|
303
|
+
);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 4. Read the Outputs back into the config so CI has them without a live lookup.
|
|
308
|
+
const outputs = readAppOutputs(cfg);
|
|
309
|
+
cfg = mergeConfig(cfg, { outputs });
|
|
310
|
+
saveConfig(cfg);
|
|
311
|
+
return cfg;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Deploy the bootstrap stack, retrying once against an existing OIDC provider. */
|
|
315
|
+
function deployBootstrap(cfg: DeployConfig): void {
|
|
316
|
+
try {
|
|
317
|
+
awsRun(bootstrapStackArgs(cfg));
|
|
318
|
+
} catch {
|
|
319
|
+
// The most common cause is an account that already has a GitHub OIDC
|
|
320
|
+
// provider (only one is allowed per URL). Retry telling the template to
|
|
321
|
+
// reuse it instead of creating a second.
|
|
322
|
+
console.log(
|
|
323
|
+
" • retrying bootstrap, reusing the account's existing GitHub OIDC provider",
|
|
324
|
+
);
|
|
325
|
+
try {
|
|
326
|
+
awsRun(bootstrapStackArgs(cfg, true));
|
|
327
|
+
} catch (err) {
|
|
328
|
+
console.error(
|
|
329
|
+
`\n ✗ bootstrap stack deploy failed.\n ${(err as Error).message}`,
|
|
330
|
+
);
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function printSummary(cfg: DeployConfig): void {
|
|
337
|
+
console.log(
|
|
338
|
+
`\n ✓ ${cfg.app} is deployed (${cfg.stackName} @ ${cfg.region})`,
|
|
339
|
+
);
|
|
340
|
+
if (cfg.outputs?.apiUrl) console.log(` API: ${cfg.outputs.apiUrl}`);
|
|
341
|
+
console.log(
|
|
342
|
+
"\n Wire these GitHub repo Variables so CI can ship code (Settings → Variables):",
|
|
343
|
+
);
|
|
344
|
+
console.log(
|
|
345
|
+
table(
|
|
346
|
+
repoVariables(cfg).map((v) => ({
|
|
347
|
+
key: v.name,
|
|
348
|
+
value: v.value || `<${v.from}>`,
|
|
349
|
+
})),
|
|
350
|
+
),
|
|
351
|
+
);
|
|
352
|
+
console.log(
|
|
353
|
+
`\n Then every push to ${cfg.github.branch} ships code via \`rastack ci deploy\`` +
|
|
354
|
+
` (also runnable locally as \`rastack deploy\`).\n`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
131
358
|
async function main(argv: string[]): Promise<void> {
|
|
132
359
|
const flags = parseFlags(argv);
|
|
133
360
|
let cfg = seedConfig(flags);
|
|
134
361
|
|
|
135
362
|
if (canPrompt(flags.nonInteractive)) {
|
|
136
363
|
console.log(
|
|
137
|
-
"\n rastack init — scaffold the deploy config +
|
|
364
|
+
"\n rastack init — scaffold the deploy config + stand the backend up\n",
|
|
138
365
|
);
|
|
139
366
|
cfg = await promptMissing(cfg);
|
|
140
367
|
}
|
|
141
368
|
|
|
142
369
|
const errors = validateConfig(cfg);
|
|
143
|
-
//
|
|
144
|
-
//
|
|
370
|
+
// Any deploy needs a complete config; a still-incomplete one is fatal unless we
|
|
371
|
+
// are only scaffolding files.
|
|
145
372
|
if (errors.length) {
|
|
146
|
-
const
|
|
373
|
+
const fatal = !flags.noDeploy || flags.nonInteractive;
|
|
374
|
+
const label = fatal ? "✗" : "⚠";
|
|
147
375
|
for (const e of errors) console.error(` ${label} ${e}`);
|
|
148
|
-
if (
|
|
376
|
+
if (fatal) {
|
|
149
377
|
console.error(
|
|
150
|
-
`\n ${errors.length} problem(s). Pass the missing values as flags.`,
|
|
378
|
+
`\n ${errors.length} problem(s). Pass the missing values as flags, or --no-deploy to scaffold only.`,
|
|
151
379
|
);
|
|
152
380
|
process.exit(1);
|
|
153
381
|
}
|
|
154
382
|
}
|
|
155
383
|
|
|
384
|
+
// 1. Scaffold the config + workflows.
|
|
156
385
|
const files = scaffoldFiles(cfg, { noWorkflows: flags.noWorkflows });
|
|
157
386
|
const written: string[] = [];
|
|
158
387
|
const skipped: string[] = [];
|
|
@@ -166,17 +395,38 @@ async function main(argv: string[]): Promise<void> {
|
|
|
166
395
|
fs.writeFileSync(abs, f.content);
|
|
167
396
|
written.push(f.path);
|
|
168
397
|
}
|
|
169
|
-
|
|
170
398
|
console.log("");
|
|
171
399
|
for (const w of written) console.log(` ✓ wrote ${w}`);
|
|
172
400
|
for (const s of skipped)
|
|
173
401
|
console.log(` • skipped ${s} (exists — pass --force to overwrite)`);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
402
|
+
|
|
403
|
+
// 2. Deploy (unless opted out).
|
|
404
|
+
if (flags.print) {
|
|
405
|
+
printPlan(cfg);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (flags.noDeploy) {
|
|
409
|
+
console.log(
|
|
410
|
+
`\n Config scaffolded. Run \`rastack init\` again (without --no-deploy) to deploy,\n` +
|
|
411
|
+
` or \`rastack init --print\` to see the exact commands.\n`,
|
|
412
|
+
);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (canPrompt(flags.nonInteractive)) {
|
|
417
|
+
const ok = await confirm(
|
|
418
|
+
`Deploy ${cfg.stackName} to AWS ${cfg.region} now? (creates the bootstrap stack if needed) [y/N]`,
|
|
419
|
+
);
|
|
420
|
+
if (!ok) {
|
|
421
|
+
console.log(
|
|
422
|
+
"\n Skipped the deploy. Re-run to deploy, or `rastack init --print` for the commands.\n",
|
|
423
|
+
);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
cfg = await standUp(cfg);
|
|
429
|
+
printSummary(cfg);
|
|
180
430
|
}
|
|
181
431
|
|
|
182
432
|
if (require.main === module) {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `rastack update` — update the CLI in place.
|
|
5
|
+
*
|
|
6
|
+
* rastack update [<version>] [--version <v>] [--check] [--dry-run]
|
|
7
|
+
*
|
|
8
|
+
* (no args) update to the latest published version
|
|
9
|
+
* <version> pin a target, e.g. `rastack update 0.0.40`
|
|
10
|
+
* --check report current vs. latest, install nothing
|
|
11
|
+
* --dry-run print the npm command that would run, don't run it
|
|
12
|
+
*
|
|
13
|
+
* `rastack` is a global npm package installed into a self-contained per-user
|
|
14
|
+
* prefix (`~/.rastack`, via the hosted installer). Updating re-runs
|
|
15
|
+
* `npm install -g rastack@<target>` against that SAME prefix — derived from where
|
|
16
|
+
* the running CLI lives — so it never scatters a second copy into npm's default
|
|
17
|
+
* global location. Pure decisions live in `update.ts`; this is the IO wrapper.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { execFileSync } from "child_process";
|
|
21
|
+
import { readFileSync } from "fs";
|
|
22
|
+
import * as path from "path";
|
|
23
|
+
import {
|
|
24
|
+
globalPrefixFromPackageDir,
|
|
25
|
+
npmInstallArgs,
|
|
26
|
+
parseUpdateFlags,
|
|
27
|
+
updateAvailable,
|
|
28
|
+
} from "./update";
|
|
29
|
+
|
|
30
|
+
/** The installed rastack package directory (…/rastack), one up from dist/. */
|
|
31
|
+
const PKG_DIR = path.join(__dirname, "..");
|
|
32
|
+
const PKG_JSON = path.join(PKG_DIR, "package.json");
|
|
33
|
+
|
|
34
|
+
/** Read the version from a package.json on disk, or "unknown" if unreadable. */
|
|
35
|
+
function readVersion(file: string): string {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(readFileSync(file, "utf-8")).version ?? "unknown";
|
|
38
|
+
} catch {
|
|
39
|
+
return "unknown";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Capture `npm <args>` stdout, or null when npm is absent / the call fails. */
|
|
44
|
+
function npmCapture(args: string[]): string | null {
|
|
45
|
+
try {
|
|
46
|
+
return execFileSync("npm", args, {
|
|
47
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
48
|
+
})
|
|
49
|
+
.toString()
|
|
50
|
+
.trim();
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function main(argv: string[]): void {
|
|
57
|
+
const flags = parseUpdateFlags(argv);
|
|
58
|
+
const current = readVersion(PKG_JSON);
|
|
59
|
+
const prefix = globalPrefixFromPackageDir(PKG_DIR);
|
|
60
|
+
|
|
61
|
+
// The npm prefix must land the update where this CLI actually lives.
|
|
62
|
+
const env = { ...process.env };
|
|
63
|
+
if (prefix) env.npm_config_prefix = prefix;
|
|
64
|
+
|
|
65
|
+
if (flags.check) {
|
|
66
|
+
const latest = npmCapture(["view", "rastack@latest", "version"]);
|
|
67
|
+
if (!latest) {
|
|
68
|
+
console.error(
|
|
69
|
+
" ✗ could not reach the npm registry to check for updates.",
|
|
70
|
+
);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
console.log(` rastack ${current} (installed) — latest is ${latest}`);
|
|
74
|
+
if (updateAvailable(current, latest)) {
|
|
75
|
+
console.log(` ▸ run \`rastack update\` to upgrade to ${latest}.`);
|
|
76
|
+
} else {
|
|
77
|
+
console.log(" ✓ you are on the latest version.");
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const args = npmInstallArgs(flags.version);
|
|
83
|
+
if (flags.dryRun) {
|
|
84
|
+
const pfx = prefix ? `npm_config_prefix=${prefix} ` : "";
|
|
85
|
+
console.log(`\n ${pfx}npm ${args.join(" ")}\n`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!prefix) {
|
|
90
|
+
// Running from a source checkout (not a global install) — nothing to self-update.
|
|
91
|
+
console.error(
|
|
92
|
+
" ✗ rastack is not running from a global install, so `update` does not apply.\n" +
|
|
93
|
+
" Reinstall with the hosted installer, or `npm install -g rastack` to manage it.",
|
|
94
|
+
);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log(
|
|
99
|
+
`\n▸ Updating rastack (${current} → ${flags.version}) in ${prefix}\n`,
|
|
100
|
+
);
|
|
101
|
+
try {
|
|
102
|
+
// Re-runs the developer's own npm to update this global package — no
|
|
103
|
+
// untrusted input, and execFileSync (no shell) avoids any injection.
|
|
104
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
105
|
+
execFileSync("npm", args, { stdio: "inherit", env });
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error(
|
|
108
|
+
`\n ✗ npm failed to update rastack. Re-run for detail:\n npm install -g ${npmInstallArgs(flags.version)[2]}\n ${(err as Error).message}`,
|
|
109
|
+
);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Re-read the (now replaced) package.json to report where we landed.
|
|
114
|
+
const updated = readVersion(PKG_JSON);
|
|
115
|
+
if (updated === current) {
|
|
116
|
+
console.log(`\n ✓ rastack is up to date (${updated}).\n`);
|
|
117
|
+
} else {
|
|
118
|
+
console.log(`\n ✓ rastack updated ${current} → ${updated}.\n`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (require.main === module) {
|
|
123
|
+
main(process.argv.slice(2));
|
|
124
|
+
}
|
package/src/rastack.ts
CHANGED
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
* rastack wasm Compile the API to WebAssembly (in-browser local dev)
|
|
17
17
|
* rastack scan [files...] [--fail-on-pii] Scan CSVs for PII and schema info
|
|
18
18
|
* rastack seed [--env dev] [--source gs://] Load CSVs into Iceberg namespace
|
|
19
|
-
* rastack init [--app slug] [--force]
|
|
20
|
-
* rastack deploy [--
|
|
19
|
+
* rastack init [--app slug] [--force] Scaffold config + GitHub Actions AND stand the backend up
|
|
20
|
+
* rastack deploy [--plan] Ship code to the Lambda (alias for `rastack ci deploy`)
|
|
21
21
|
* rastack ci <check|build|deploy> [--plan] Run the CI pipeline (keeps Actions a one-liner)
|
|
22
|
+
* rastack update [<version>] [--check] Update the CLI in place (self-update)
|
|
22
23
|
*/
|
|
23
24
|
|
|
24
25
|
import { execSync, spawnSync } from "child_process";
|
|
@@ -118,17 +119,23 @@ switch (command) {
|
|
|
118
119
|
run("seed.js");
|
|
119
120
|
break;
|
|
120
121
|
case "init":
|
|
121
|
-
//
|
|
122
|
+
// Scaffold config + GitHub Actions and stand the serverless backend up.
|
|
122
123
|
run("rastack-init.js", rest);
|
|
123
124
|
break;
|
|
124
125
|
case "deploy":
|
|
125
|
-
//
|
|
126
|
-
|
|
126
|
+
// Ship code to the Lambda — an alias for the CI code deploy. Infrastructure
|
|
127
|
+
// is stood up once by `rastack init`; `deploy` only ever ships code.
|
|
128
|
+
run("rastack-ci.js", ["deploy", ...rest]);
|
|
127
129
|
break;
|
|
128
130
|
case "ci":
|
|
129
131
|
// Run a CI pipeline phase (check | build | deploy).
|
|
130
132
|
run("rastack-ci.js", rest);
|
|
131
133
|
break;
|
|
134
|
+
case "update":
|
|
135
|
+
case "upgrade":
|
|
136
|
+
// Self-update the globally-installed CLI in place.
|
|
137
|
+
run("rastack-update.js", rest);
|
|
138
|
+
break;
|
|
132
139
|
case "generate":
|
|
133
140
|
case "gen":
|
|
134
141
|
// Proxy to existing rastack codegen pipeline
|
|
@@ -147,9 +154,10 @@ switch (command) {
|
|
|
147
154
|
` rastack wasm Compile the API to WebAssembly (browser local dev)\n` +
|
|
148
155
|
` rastack scan [files...] [--fail-on-pii]\n` +
|
|
149
156
|
` rastack seed [--env dev|staging] [--source gs://bucket/ | ./myapp-dev/]\n` +
|
|
150
|
-
` rastack init [--app slug] [--owner o] [--repo r] [--
|
|
151
|
-
` rastack deploy [--
|
|
152
|
-
` rastack ci <check|build|deploy> [--plan] Run the CI pipeline (cleaner Actions)\n
|
|
157
|
+
` rastack init [--app slug] [--owner o] [--repo r] [--print] [--no-deploy] Scaffold + stand the backend up\n` +
|
|
158
|
+
` rastack deploy [--plan] Ship code to the Lambda (alias for ci deploy)\n` +
|
|
159
|
+
` rastack ci <check|build|deploy> [--plan] Run the CI pipeline (cleaner Actions)\n` +
|
|
160
|
+
` rastack update [<version>] [--check] [--dry-run] Update the CLI in place (self-update)\n`,
|
|
153
161
|
);
|
|
154
162
|
process.exit(1);
|
|
155
163
|
}
|
package/src/update.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure core of `rastack update` — the CLI's self-updater.
|
|
3
|
+
*
|
|
4
|
+
* `rastack` is installed with `npm install -g rastack` into a self-contained
|
|
5
|
+
* per-user prefix (`~/.rastack`, wired by web/public/setup.{sh,ps1}), so updating
|
|
6
|
+
* is just re-running that global install against the SAME prefix the running CLI
|
|
7
|
+
* lives in — then reporting the version change. Everything decidable without IO
|
|
8
|
+
* lives here (flag parsing, the npm argv, deriving the prefix from the install
|
|
9
|
+
* path, semver comparison) so it is unit-tested without npm or the network;
|
|
10
|
+
* `rastack-update.ts` is the thin wrapper that shells out.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface UpdateFlags {
|
|
14
|
+
/** `latest`, or a pinned semver like `0.0.40`. */
|
|
15
|
+
version: string;
|
|
16
|
+
/** Only report current vs. latest — do not install. */
|
|
17
|
+
check: boolean;
|
|
18
|
+
/** Print the npm command that would run, without running it. */
|
|
19
|
+
dryRun: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse `rastack update` args. Accepts `--check`, `--dry-run`/`--print`, and a
|
|
24
|
+
* version either as `--version <v>` or as a bare positional (`rastack update
|
|
25
|
+
* 0.0.40`). Unknown values default to `latest`.
|
|
26
|
+
*/
|
|
27
|
+
export function parseUpdateFlags(argv: string[]): UpdateFlags {
|
|
28
|
+
let version = "latest";
|
|
29
|
+
let check = false;
|
|
30
|
+
let dryRun = false;
|
|
31
|
+
for (let i = 0; i < argv.length; i++) {
|
|
32
|
+
const a = argv[i];
|
|
33
|
+
switch (a) {
|
|
34
|
+
case "--check":
|
|
35
|
+
check = true;
|
|
36
|
+
break;
|
|
37
|
+
case "--dry-run":
|
|
38
|
+
case "--print":
|
|
39
|
+
dryRun = true;
|
|
40
|
+
break;
|
|
41
|
+
case "--version":
|
|
42
|
+
case "--to":
|
|
43
|
+
version = argv[++i] ?? "latest";
|
|
44
|
+
break;
|
|
45
|
+
default:
|
|
46
|
+
// A bare, non-flag token is taken as the target version.
|
|
47
|
+
if (!a.startsWith("-")) version = a;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { version, check, dryRun };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The npm package spec to install for a target version. */
|
|
54
|
+
export function installSpec(version: string): string {
|
|
55
|
+
return !version || version === "latest" ? "rastack" : `rastack@${version}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The `npm <args>` (after the `npm` binary) that installs the target globally. */
|
|
59
|
+
export function npmInstallArgs(version: string): string[] {
|
|
60
|
+
return [
|
|
61
|
+
"install",
|
|
62
|
+
"-g",
|
|
63
|
+
installSpec(version),
|
|
64
|
+
"--no-fund",
|
|
65
|
+
"--no-audit",
|
|
66
|
+
"--loglevel=error",
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Derive the npm global prefix from the installed rastack package directory, so
|
|
72
|
+
* the update lands in the SAME location the running CLI was installed to (not
|
|
73
|
+
* npm's default global prefix, which may differ from the per-user `~/.rastack`).
|
|
74
|
+
*
|
|
75
|
+
* Global installs put the package at `<prefix>/lib/node_modules/rastack` (POSIX)
|
|
76
|
+
* or `<prefix>/node_modules/rastack` (Windows). Returns null when rastack is not
|
|
77
|
+
* under a `node_modules` at all — e.g. running from a source checkout — where
|
|
78
|
+
* self-update does not apply.
|
|
79
|
+
*/
|
|
80
|
+
export function globalPrefixFromPackageDir(pkgDir: string): string | null {
|
|
81
|
+
const norm = pkgDir.replace(/\\/g, "/");
|
|
82
|
+
// Length-preserving normalization, so indices map back onto the original.
|
|
83
|
+
const posix = norm.lastIndexOf("/lib/node_modules/");
|
|
84
|
+
if (posix !== -1) return pkgDir.slice(0, posix);
|
|
85
|
+
const win = norm.lastIndexOf("/node_modules/");
|
|
86
|
+
if (win !== -1) return pkgDir.slice(0, win);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Compare two dotted versions numerically (major.minor.patch; any pre-release
|
|
92
|
+
* suffix is ignored). Returns -1 if a < b, 1 if a > b, 0 if equal — enough to
|
|
93
|
+
* tell `--check` whether an update is available.
|
|
94
|
+
*/
|
|
95
|
+
export function compareSemver(a: string, b: string): number {
|
|
96
|
+
const parts = (v: string) =>
|
|
97
|
+
v
|
|
98
|
+
.replace(/^v/, "")
|
|
99
|
+
.split("-")[0]
|
|
100
|
+
.split(".")
|
|
101
|
+
.map((n) => parseInt(n, 10) || 0);
|
|
102
|
+
const pa = parts(a);
|
|
103
|
+
const pb = parts(b);
|
|
104
|
+
for (let i = 0; i < 3; i++) {
|
|
105
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
106
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
107
|
+
}
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** True when `latest` is a strictly newer version than `current`. */
|
|
112
|
+
export function updateAvailable(current: string, latest: string): boolean {
|
|
113
|
+
return compareSemver(current, latest) < 0;
|
|
114
|
+
}
|