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.
@@ -4,8 +4,14 @@ import {
4
4
  buildSteps,
5
5
  checkSteps,
6
6
  ciPlan,
7
+ deployStackArgs,
7
8
  deployStackCommand,
8
9
  deploySteps,
10
+ bootstrapStackArgs,
11
+ bootstrapStackCommand,
12
+ bootstrapStackName,
13
+ bootstrapStackParameters,
14
+ APP_STACK_OUTPUTS,
9
15
  lambdaEnv,
10
16
  mergeConfig,
11
17
  normalizeConfig,
@@ -13,8 +19,8 @@ import {
13
19
  repoVariables,
14
20
  scaffoldFiles,
15
21
  stackParameters,
16
- templateUrl,
17
22
  validateConfig,
23
+ zipStore,
18
24
  renderCiWorkflow,
19
25
  renderDeployWorkflow,
20
26
  CI_WORKFLOW_PATH,
@@ -180,25 +186,107 @@ describe("stack parameters + launch command", () => {
180
186
  );
181
187
  });
182
188
 
183
- it("builds the template URL only when the templates bucket is known", () => {
184
- expect(templateUrl(base())).toBeNull();
185
- expect(templateUrl(base({ templatesBucket: "tpl" }))).toBe(
186
- "https://tpl.s3.af-south-1.amazonaws.com/app.yaml",
187
- );
188
- });
189
-
190
- it("launches by URL when hosted, else from the in-repo template", () => {
189
+ it("always launches from the in-repo --template-file (deploy takes no URL)", () => {
190
+ // `aws cloudformation deploy` accepts ONLY `--template-file`; `--template-url`
191
+ // is exclusive to create-stack/update-stack and makes deploy reject the
192
+ // command. Even with a known templates bucket, the local file is used.
191
193
  const hosted = deployStackCommand(
192
194
  base({ templatesBucket: "tpl", artifactsBucket: "art" }),
193
195
  );
194
- expect(hosted).toContain(
195
- "--template-url https://tpl.s3.af-south-1.amazonaws.com/app.yaml",
196
- );
196
+ expect(hosted).toContain("--template-file deploy/cloudformation/app.yaml");
197
+ expect(hosted).not.toContain("--template-url");
197
198
  expect(hosted).toContain("--stack-name rastack-airline");
198
199
  expect(hosted).toContain("ArtifactsBucket=art");
199
200
 
200
201
  const local = deployStackCommand(base());
201
202
  expect(local).toContain("--template-file deploy/cloudformation/app.yaml");
203
+ expect(local).not.toContain("--template-url");
204
+ });
205
+
206
+ it("emits ONE line (PowerShell-safe — no backslash continuations)", () => {
207
+ // A `\`-continued command is bash-only; pasted into PowerShell each `--flag`
208
+ // becomes its own statement and errors. Keep it to a single line.
209
+ const cmd = deployStackCommand(base({ artifactsBucket: "art" }));
210
+ expect(cmd).not.toContain("\\");
211
+ expect(cmd.split("\n")).toHaveLength(1);
212
+ expect(cmd.startsWith("aws cloudformation deploy ")).toBe(true);
213
+ });
214
+
215
+ it("deployStackArgs is the argv the printed command joins", () => {
216
+ const cfg = base({ artifactsBucket: "art" });
217
+ expect(`aws ${deployStackArgs(cfg).join(" ")}`).toBe(
218
+ deployStackCommand(cfg),
219
+ );
220
+ expect(deployStackArgs(cfg).slice(0, 2)).toEqual([
221
+ "cloudformation",
222
+ "deploy",
223
+ ]);
224
+ });
225
+ });
226
+
227
+ describe("bootstrap stack", () => {
228
+ it("derives the bootstrap stack name from the prefix", () => {
229
+ expect(bootstrapStackName(base())).toBe("rastack-bootstrap");
230
+ expect(bootstrapStackName(base({ namePrefix: "acme" }))).toBe(
231
+ "acme-bootstrap",
232
+ );
233
+ });
234
+
235
+ it("passes the publisher repo coordinates as parameters", () => {
236
+ const params = bootstrapStackParameters(base());
237
+ const map = Object.fromEntries(params.map((p) => [p.key, p.value]));
238
+ expect(map.NamePrefix).toBe("rastack");
239
+ expect(map.GitHubOwner).toBe("theserverkid");
240
+ expect(map.PublisherRepo).toBe("reactapistack-demo");
241
+ expect(map.PublisherBranch).toBe("master");
242
+ expect(map.CreateOIDCProvider).toBeUndefined();
243
+ });
244
+
245
+ it("adds CreateOIDCProvider=false when reusing an existing provider", () => {
246
+ const params = bootstrapStackParameters(base(), true);
247
+ expect(params.find((p) => p.key === "CreateOIDCProvider")!.value).toBe(
248
+ "false",
249
+ );
250
+ });
251
+
252
+ it("builds a single-line command from the bootstrap template", () => {
253
+ const cmd = bootstrapStackCommand(base());
254
+ expect(cmd).not.toContain("\\");
255
+ expect(cmd.split("\n")).toHaveLength(1);
256
+ expect(cmd).toContain(
257
+ "--template-file deploy/cloudformation/bootstrap.yaml",
258
+ );
259
+ expect(cmd).toContain("--stack-name rastack-bootstrap");
260
+ expect(`aws ${bootstrapStackArgs(base()).join(" ")}`).toBe(cmd);
261
+ });
262
+ });
263
+
264
+ describe("app stack outputs map", () => {
265
+ it("maps each stack Output key to a DeployOutputs field", () => {
266
+ const keys = APP_STACK_OUTPUTS.map((o) => o.key);
267
+ expect(keys).toContain("FunctionName");
268
+ expect(keys).toContain("ApiUrl");
269
+ expect(keys).toContain("DeployRoleArn");
270
+ expect(APP_STACK_OUTPUTS.find((o) => o.key === "FunctionName")!.field).toBe(
271
+ "functionName",
272
+ );
273
+ });
274
+ });
275
+
276
+ describe("zipStore — placeholder packaging", () => {
277
+ it("produces a valid, deterministic STORE archive", () => {
278
+ const data = Buffer.from("#!/bin/sh\necho placeholder\n");
279
+ const a = zipStore([{ name: "bootstrap", data, mode: 0o755 }]);
280
+ const b = zipStore([{ name: "bootstrap", data, mode: 0o755 }]);
281
+ // Deterministic (fixed DOS timestamp) → idempotent uploads.
282
+ expect(a.equals(b)).toBe(true);
283
+ // Local file header + End-of-central-directory signatures.
284
+ expect(a.readUInt32LE(0)).toBe(0x04034b50);
285
+ expect(a.readUInt32LE(a.length - 22)).toBe(0x06054b50);
286
+ // One entry recorded in the EOCD.
287
+ expect(a.readUInt16LE(a.length - 12)).toBe(1);
288
+ // STORE method → the file bytes appear verbatim after the header + name.
289
+ expect(a.includes(data)).toBe(true);
202
290
  });
203
291
  });
204
292
 
@@ -0,0 +1,95 @@
1
+ import {
2
+ compareSemver,
3
+ globalPrefixFromPackageDir,
4
+ installSpec,
5
+ npmInstallArgs,
6
+ parseUpdateFlags,
7
+ updateAvailable,
8
+ } from "../src/update";
9
+
10
+ describe("update — flag parsing", () => {
11
+ it("defaults to a latest, non-check, non-dry-run update", () => {
12
+ expect(parseUpdateFlags([])).toEqual({
13
+ version: "latest",
14
+ check: false,
15
+ dryRun: false,
16
+ });
17
+ });
18
+
19
+ it("takes a bare positional as the target version", () => {
20
+ expect(parseUpdateFlags(["0.0.40"]).version).toBe("0.0.40");
21
+ });
22
+
23
+ it("accepts --version / --to and the check + dry-run flags", () => {
24
+ expect(parseUpdateFlags(["--version", "1.2.3"]).version).toBe("1.2.3");
25
+ expect(parseUpdateFlags(["--to", "1.2.3"]).version).toBe("1.2.3");
26
+ expect(parseUpdateFlags(["--check"]).check).toBe(true);
27
+ expect(parseUpdateFlags(["--dry-run"]).dryRun).toBe(true);
28
+ expect(parseUpdateFlags(["--print"]).dryRun).toBe(true);
29
+ });
30
+ });
31
+
32
+ describe("update — npm spec + args", () => {
33
+ it("installs bare `rastack` for latest, pinned otherwise", () => {
34
+ expect(installSpec("latest")).toBe("rastack");
35
+ expect(installSpec("")).toBe("rastack");
36
+ expect(installSpec("0.0.40")).toBe("rastack@0.0.40");
37
+ });
38
+
39
+ it("builds a quiet global install argv", () => {
40
+ expect(npmInstallArgs("latest")).toEqual([
41
+ "install",
42
+ "-g",
43
+ "rastack",
44
+ "--no-fund",
45
+ "--no-audit",
46
+ "--loglevel=error",
47
+ ]);
48
+ expect(npmInstallArgs("0.0.40")[2]).toBe("rastack@0.0.40");
49
+ });
50
+ });
51
+
52
+ describe("update — global prefix derivation", () => {
53
+ it("derives the per-user prefix from a POSIX global install", () => {
54
+ expect(
55
+ globalPrefixFromPackageDir("/home/me/.rastack/lib/node_modules/rastack"),
56
+ ).toBe("/home/me/.rastack");
57
+ });
58
+
59
+ it("derives the prefix from a Windows global install", () => {
60
+ expect(
61
+ globalPrefixFromPackageDir(
62
+ "C:\\Users\\me\\.rastack\\node_modules\\rastack",
63
+ ),
64
+ ).toBe("C:\\Users\\me\\.rastack");
65
+ });
66
+
67
+ it("prefers the lib/node_modules layout when both could match", () => {
68
+ // POSIX path also contains `/node_modules/` — the lib form must win so the
69
+ // prefix is the install root, not `<root>/lib`.
70
+ expect(
71
+ globalPrefixFromPackageDir("/opt/tools/lib/node_modules/rastack"),
72
+ ).toBe("/opt/tools");
73
+ });
74
+
75
+ it("returns null for a source checkout (not under node_modules)", () => {
76
+ expect(
77
+ globalPrefixFromPackageDir("/home/me/src/reactapistack/tools"),
78
+ ).toBeNull();
79
+ });
80
+ });
81
+
82
+ describe("update — semver comparison", () => {
83
+ it("orders versions numerically, ignoring a `v` prefix and pre-release", () => {
84
+ expect(compareSemver("0.0.9", "0.0.10")).toBe(-1);
85
+ expect(compareSemver("v1.2.0", "1.2.0")).toBe(0);
86
+ expect(compareSemver("2.0.0", "1.9.9")).toBe(1);
87
+ expect(compareSemver("1.0.0-beta", "1.0.0")).toBe(0);
88
+ });
89
+
90
+ it("reports whether an update is available", () => {
91
+ expect(updateAvailable("0.0.33", "0.0.40")).toBe(true);
92
+ expect(updateAvailable("0.0.40", "0.0.40")).toBe(false);
93
+ expect(updateAvailable("0.1.0", "0.0.40")).toBe(false);
94
+ });
95
+ });
@@ -1,20 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * `rastack deploy` — configure a repo's serverless deployment.
4
- *
5
- * rastack deploy [--app <slug>] [--region <r>] [--owner <o>] [--repo <r>]
6
- * [--branch <b>] [--namespace <n>] [--templates-bucket <b>]
7
- * [--artifacts-bucket <b>] [--arch arm64|x86_64]
8
- * [--print] [--run] [--yes]
9
- *
10
- * Writes/updates `.rastack/deploy.json` (creating it if `rastack init` has not
11
- * run) and prints exactly how to stand the app up: the one-time
12
- * `aws cloudformation deploy` that creates all infrastructure, the GitHub repo
13
- * Variables CI reads, and the Lambda's runtime env. Infra is created once by a
14
- * human — this command *configures* that step, it does not silently mutate the
15
- * cloud. `--run` executes the launch command for you; `--print` is
16
- * non-interactive (config as-is).
17
- *
18
- * The recurring code deploy is CI's job (`rastack ci deploy`), not this command.
19
- */
20
- export {};
@@ -1,196 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- /**
4
- * `rastack deploy` — configure a repo's serverless deployment.
5
- *
6
- * rastack deploy [--app <slug>] [--region <r>] [--owner <o>] [--repo <r>]
7
- * [--branch <b>] [--namespace <n>] [--templates-bucket <b>]
8
- * [--artifacts-bucket <b>] [--arch arm64|x86_64]
9
- * [--print] [--run] [--yes]
10
- *
11
- * Writes/updates `.rastack/deploy.json` (creating it if `rastack init` has not
12
- * run) and prints exactly how to stand the app up: the one-time
13
- * `aws cloudformation deploy` that creates all infrastructure, the GitHub repo
14
- * Variables CI reads, and the Lambda's runtime env. Infra is created once by a
15
- * human — this command *configures* that step, it does not silently mutate the
16
- * cloud. `--run` executes the launch command for you; `--print` is
17
- * non-interactive (config as-is).
18
- *
19
- * The recurring code deploy is CI's job (`rastack ci deploy`), not this command.
20
- */
21
- Object.defineProperty(exports, "__esModule", { value: true });
22
- const child_process_1 = require("child_process");
23
- const config_1 = require("./deploy/config");
24
- const io_1 = require("./deploy/io");
25
- function parseFlags(argv) {
26
- const patch = { github: {}, lambda: {} };
27
- let print = false;
28
- let run = false;
29
- let nonInteractive = false;
30
- for (let i = 0; i < argv.length; i++) {
31
- const a = argv[i];
32
- const next = () => argv[++i] ?? "";
33
- switch (a) {
34
- case "--app":
35
- patch.app = next();
36
- break;
37
- case "--region":
38
- patch.region = next();
39
- break;
40
- case "--owner":
41
- patch.github.owner = next();
42
- break;
43
- case "--repo":
44
- patch.github.repo = next();
45
- break;
46
- case "--branch":
47
- patch.github.branch = next();
48
- break;
49
- case "--prefix":
50
- patch.namePrefix = next();
51
- break;
52
- case "--namespace":
53
- patch.namespace = next();
54
- break;
55
- case "--stack-name":
56
- patch.stackName = next();
57
- break;
58
- case "--templates-bucket":
59
- patch.templatesBucket = next();
60
- break;
61
- case "--artifacts-bucket":
62
- patch.artifactsBucket = next();
63
- break;
64
- case "--arch":
65
- patch.lambda.arch = next();
66
- break;
67
- case "--print":
68
- print = true;
69
- nonInteractive = true;
70
- break;
71
- case "--run":
72
- run = true;
73
- break;
74
- case "--yes":
75
- case "-y":
76
- case "--non-interactive":
77
- nonInteractive = true;
78
- break;
79
- default:
80
- if (a.startsWith("-")) {
81
- console.error(`Unknown flag: ${a}`);
82
- process.exit(1);
83
- }
84
- }
85
- }
86
- return { patch, print, run, nonInteractive };
87
- }
88
- function seedConfig(patch) {
89
- const existing = (0, io_1.loadConfig)();
90
- return existing ? (0, config_1.mergeConfig)(existing, patch) : (0, config_1.normalizeConfig)(patch);
91
- }
92
- async function promptMissing(cfg) {
93
- return (0, io_1.withPrompt)(async (rl) => {
94
- const app = await (0, io_1.ask)(rl, "App slug", cfg.app);
95
- const owner = await (0, io_1.ask)(rl, "GitHub owner", cfg.github.owner);
96
- const repo = await (0, io_1.ask)(rl, "GitHub repo", cfg.github.repo);
97
- const branch = await (0, io_1.ask)(rl, "Deploy branch", cfg.github.branch);
98
- const region = await (0, io_1.ask)(rl, "AWS region", cfg.region);
99
- const namespace = await (0, io_1.ask)(rl, "Iceberg namespace", cfg.namespace);
100
- const artifactsBucket = await (0, io_1.ask)(rl, "Artifacts bucket (from bootstrap stack)", cfg.artifactsBucket ?? "");
101
- const templatesBucket = await (0, io_1.ask)(rl, "Templates bucket (from bootstrap stack, optional)", cfg.templatesBucket ?? "");
102
- return (0, config_1.mergeConfig)(cfg, {
103
- app,
104
- region,
105
- namespace,
106
- artifactsBucket,
107
- templatesBucket,
108
- github: { owner, repo, branch },
109
- });
110
- });
111
- }
112
- /** Pad a list of `Key=Value` rows into an aligned table. */
113
- function table(rows) {
114
- const width = Math.max(0, ...rows.map((r) => r.key.length));
115
- return rows.map((r) => ` ${r.key.padEnd(width)} ${r.value}`).join("\n");
116
- }
117
- function printSummary(cfg) {
118
- const src = (0, config_1.templateUrl)(cfg);
119
- console.log(`\n Deployment config (${cfg.stackName} @ ${cfg.region})\n`);
120
- console.log(" ── 1. Create the infrastructure (once, by a human) ──");
121
- if (!cfg.artifactsBucket) {
122
- console.log(" ⚠ artifactsBucket is unset — app.yaml requires it. Run the bootstrap\n" +
123
- " stack first, then set --artifacts-bucket (its ArtifactsBucketName output).");
124
- }
125
- if (!src) {
126
- console.log(" (launching from the in-repo template; set --templates-bucket to launch\n" +
127
- " the hosted app.yaml by URL instead)");
128
- }
129
- console.log("\n" + (0, config_1.deployStackCommand)(cfg) + "\n");
130
- console.log(" ── 2. Wire the GitHub repo Variables (from the stack Outputs) ──");
131
- console.log(table((0, config_1.repoVariables)(cfg).map((v) => ({
132
- key: v.name,
133
- value: v.value || `<${v.from}>`,
134
- }))));
135
- console.log("\n ── 3. The Lambda runtime env (set by the stack) ──");
136
- console.log(table((0, config_1.lambdaEnv)(cfg)));
137
- console.log(`\n ── 4. Then every push to ${cfg.github.branch} runs \`rastack ci deploy\`. ──\n`);
138
- }
139
- function runLaunch(cfg) {
140
- const params = (0, config_1.stackParameters)(cfg).map((p) => `${p.key}=${p.value}`);
141
- const src = (0, config_1.templateUrl)(cfg);
142
- const args = [
143
- "cloudformation",
144
- "deploy",
145
- ...(src
146
- ? ["--template-url", src]
147
- : ["--template-file", "deploy/cloudformation/app.yaml"]),
148
- "--stack-name",
149
- cfg.stackName,
150
- "--capabilities",
151
- "CAPABILITY_NAMED_IAM",
152
- "--region",
153
- cfg.region,
154
- "--parameter-overrides",
155
- ...params,
156
- ];
157
- console.log(`\n ▸ aws ${args.join(" ")}\n`);
158
- try {
159
- // Runs the developer's own AWS CLI with args built from their config — no
160
- // untrusted input, and `execFileSync` (no shell) avoids any injection.
161
- (0, child_process_1.execFileSync)("aws", args, { stdio: "inherit" });
162
- }
163
- catch (err) {
164
- console.error(`\n ✗ aws cloudformation deploy failed. Is the AWS CLI installed and are` +
165
- ` your credentials configured?\n ${err.message}`);
166
- process.exit(1);
167
- }
168
- }
169
- async function main(argv) {
170
- const flags = parseFlags(argv);
171
- let cfg = seedConfig(flags.patch);
172
- if ((0, io_1.canPrompt)(flags.nonInteractive)) {
173
- console.log("\n rastack deploy — configure the serverless deployment\n");
174
- cfg = await promptMissing(cfg);
175
- }
176
- const errors = (0, config_1.validateConfig)(cfg);
177
- if (errors.length) {
178
- for (const e of errors)
179
- console.error(` ✗ ${e}`);
180
- console.error(`\n ${errors.length} problem(s) — fix the config and re-run.`);
181
- process.exit(1);
182
- }
183
- if (!flags.print) {
184
- const file = (0, io_1.saveConfig)(cfg);
185
- console.log(` ✓ saved ${file}`);
186
- }
187
- printSummary(cfg);
188
- if (flags.run)
189
- runLaunch(cfg);
190
- }
191
- if (require.main === module) {
192
- main(process.argv.slice(2)).catch((err) => {
193
- console.error(` ✗ ${err.message}`);
194
- process.exit(1);
195
- });
196
- }