rastack 0.0.58 → 0.0.60
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 +16 -0
- package/README.md +190 -0
- package/dist/deploy/ci.js +2 -2
- package/dist/deploy/io.js +2 -2
- package/dist/dev/harness.d.ts +1 -1
- package/dist/dev/harness.js +1 -2
- package/dist/external/cache.d.ts +28 -0
- package/dist/external/cache.js +35 -0
- package/dist/external/envelope.d.ts +35 -0
- package/dist/external/envelope.js +48 -0
- package/dist/external/graphql.d.ts +25 -0
- package/dist/external/graphql.js +78 -0
- package/dist/external/handler.d.ts +52 -0
- package/dist/external/handler.js +139 -0
- package/dist/external/index.d.ts +19 -0
- package/dist/external/index.js +38 -0
- package/dist/external/lambda.d.ts +49 -0
- package/dist/external/lambda.js +86 -0
- package/dist/external/rest.d.ts +17 -0
- package/dist/external/rest.js +46 -0
- package/dist/external/route.d.ts +12 -0
- package/dist/external/route.js +33 -0
- package/dist/external/service.d.ts +50 -0
- package/dist/external/service.js +59 -0
- package/dist/external/types.d.ts +107 -0
- package/dist/external/types.js +22 -0
- package/dist/external/util.d.ts +16 -0
- package/dist/external/util.js +58 -0
- package/dist/rastack-dev.js +16 -1
- package/dist/rastack-wasm-build.d.ts +1 -1
- package/dist/rastack-wasm-build.js +6 -6
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/external.ts +10 -0
- package/hooks/query/delete.ts +12 -11
- package/hooks/query/fetch.ts +6 -8
- package/hooks/query/interfaces.ts +1 -4
- package/hooks/query/list.ts +16 -9
- package/hooks/query/update.ts +8 -6
- package/package.json +24 -2
- package/scripts/copy-templates.js +1 -1
- package/scripts/copy-wasm.js +1 -1
- package/scripts/dev-install.mjs +63 -0
- package/src/deploy/ci.ts +2 -2
- package/src/deploy/io.ts +2 -2
- package/src/dev/harness.ts +1 -2
- package/src/external/cache.ts +53 -0
- package/src/external/envelope.ts +78 -0
- package/src/external/graphql.ts +98 -0
- package/src/external/handler.ts +207 -0
- package/src/external/index.ts +25 -0
- package/src/external/lambda.ts +108 -0
- package/src/external/rest.ts +60 -0
- package/src/external/route.ts +33 -0
- package/src/external/service.ts +86 -0
- package/src/external/types.ts +121 -0
- package/src/external/util.ts +57 -0
- package/src/rastack-dev.ts +17 -1
- package/src/rastack-wasm-build.ts +6 -6
- package/jest.config.cjs +0 -12
- package/test/auth.spec.ts +0 -207
- package/test/cache.spec.ts +0 -263
- package/test/cognito.spec.ts +0 -281
- package/test/compile.spec.ts +0 -240
- package/test/components.spec.ts +0 -524
- package/test/csv-schema.spec.ts +0 -143
- package/test/deploy.spec.ts +0 -571
- package/test/dev.spec.ts +0 -414
- package/test/entities.spec.ts +0 -597
- package/test/import.spec.ts +0 -241
- package/test/plugin.spec.ts +0 -315
- package/test/runtime-manifest.spec.ts +0 -309
- package/test/schema-entities.spec.ts +0 -688
- package/test/tokens.spec.ts +0 -302
- package/test/transitions.spec.ts +0 -372
- package/test/typed-hooks.spec.ts +0 -412
- package/test/update.spec.ts +0 -152
- package/test/validate.spec.ts +0 -319
- package/tsconfig.json +0 -27
- package/wasm/package.json +0 -4
- package/wasm/rastack_wasm.d.ts +0 -100
- package/wasm/rastack_wasm.js +0 -745
- package/wasm/rastack_wasm_bg.wasm +0 -0
- package/wasm/rastack_wasm_bg.wasm.d.ts +0 -28
package/test/deploy.spec.ts
DELETED
|
@@ -1,571 +0,0 @@
|
|
|
1
|
-
import yaml from "js-yaml";
|
|
2
|
-
import {
|
|
3
|
-
DeployConfig,
|
|
4
|
-
buildSteps,
|
|
5
|
-
checkSteps,
|
|
6
|
-
ciPlan,
|
|
7
|
-
deployStackArgs,
|
|
8
|
-
deployStackCommand,
|
|
9
|
-
deploySteps,
|
|
10
|
-
bootstrapStackArgs,
|
|
11
|
-
bootstrapStackCommand,
|
|
12
|
-
bootstrapStackName,
|
|
13
|
-
bootstrapStackParameters,
|
|
14
|
-
cognitoAdvancedSecuritySupported,
|
|
15
|
-
APP_STACK_OUTPUTS,
|
|
16
|
-
ghVariableSetArgs,
|
|
17
|
-
lambdaEnv,
|
|
18
|
-
mergeConfig,
|
|
19
|
-
normalizeConfig,
|
|
20
|
-
parseGitHubRemote,
|
|
21
|
-
repoVariables,
|
|
22
|
-
scaffoldFiles,
|
|
23
|
-
stackParameters,
|
|
24
|
-
validateConfig,
|
|
25
|
-
validateBootstrapConfig,
|
|
26
|
-
zipStore,
|
|
27
|
-
renderCiWorkflow,
|
|
28
|
-
renderDeployWorkflow,
|
|
29
|
-
CI_WORKFLOW_PATH,
|
|
30
|
-
CONFIG_PATH,
|
|
31
|
-
DEPLOY_WORKFLOW_PATH,
|
|
32
|
-
} from "../src/deploy";
|
|
33
|
-
|
|
34
|
-
/** A complete, valid config to build variations off. */
|
|
35
|
-
function base(overrides: Partial<DeployConfig> = {}): DeployConfig {
|
|
36
|
-
return normalizeConfig({
|
|
37
|
-
app: "airline",
|
|
38
|
-
github: {
|
|
39
|
-
owner: "theserverkid",
|
|
40
|
-
repo: "reactapistack-demo",
|
|
41
|
-
branch: "master",
|
|
42
|
-
},
|
|
43
|
-
...overrides,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
describe("deploy config — normalize", () => {
|
|
48
|
-
it("fills defaults and derives the stack name from prefix + app", () => {
|
|
49
|
-
const cfg = base();
|
|
50
|
-
expect(cfg.namePrefix).toBe("rastack");
|
|
51
|
-
expect(cfg.region).toBe("af-south-1");
|
|
52
|
-
expect(cfg.stackName).toBe("rastack-airline");
|
|
53
|
-
expect(cfg.namespace).toBe("production");
|
|
54
|
-
expect(cfg.lambda).toEqual({
|
|
55
|
-
bin: "bootstrap",
|
|
56
|
-
arch: "arm64",
|
|
57
|
-
features: "lambda",
|
|
58
|
-
});
|
|
59
|
-
expect(cfg.resourcesDir).toBe("resources");
|
|
60
|
-
expect(cfg.outDir).toBe(".rastack");
|
|
61
|
-
expect(cfg.tokenUse).toBe("access");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("honours an explicit stack name and prefix", () => {
|
|
65
|
-
const cfg = base({ namePrefix: "acme", stackName: "custom-stack" });
|
|
66
|
-
expect(cfg.stackName).toBe("custom-stack");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("re-derives the stack name off a custom prefix when none is given", () => {
|
|
70
|
-
const cfg = normalizeConfig({
|
|
71
|
-
app: "airline",
|
|
72
|
-
namePrefix: "acme",
|
|
73
|
-
github: { owner: "o", repo: "r" },
|
|
74
|
-
});
|
|
75
|
-
expect(cfg.stackName).toBe("acme-airline");
|
|
76
|
-
expect(cfg.github.branch).toBe("master"); // branch default
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("drops empty optional buckets to undefined", () => {
|
|
80
|
-
const cfg = normalizeConfig({
|
|
81
|
-
app: "a",
|
|
82
|
-
templatesBucket: " ",
|
|
83
|
-
github: {},
|
|
84
|
-
});
|
|
85
|
-
expect(cfg.templatesBucket).toBeUndefined();
|
|
86
|
-
expect(cfg.artifactsBucket).toBeUndefined();
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
describe("deploy config — merge", () => {
|
|
91
|
-
it("deep-merges nested github/lambda without dropping siblings", () => {
|
|
92
|
-
const cfg = mergeConfig(base(), {
|
|
93
|
-
github: { branch: "dev" },
|
|
94
|
-
lambda: { arch: "x86_64" },
|
|
95
|
-
});
|
|
96
|
-
expect(cfg.github.owner).toBe("theserverkid");
|
|
97
|
-
expect(cfg.github.branch).toBe("dev");
|
|
98
|
-
expect(cfg.lambda.arch).toBe("x86_64");
|
|
99
|
-
expect(cfg.lambda.bin).toBe("bootstrap");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("merges stack outputs", () => {
|
|
103
|
-
const cfg = mergeConfig(base(), { outputs: { functionName: "fn-1" } });
|
|
104
|
-
const cfg2 = mergeConfig(cfg, { outputs: { apiUrl: "https://x" } });
|
|
105
|
-
expect(cfg2.outputs).toEqual({ functionName: "fn-1", apiUrl: "https://x" });
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe("deploy config — validate", () => {
|
|
110
|
-
it("passes a well-formed config", () => {
|
|
111
|
-
expect(validateConfig(base())).toEqual([]);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("rejects a bad app slug (Cognito [a-z0-9] rule)", () => {
|
|
115
|
-
expect(validateConfig(base({ app: "My-App" })).join("\n")).toMatch(
|
|
116
|
-
/app "My-App"/,
|
|
117
|
-
);
|
|
118
|
-
expect(validateConfig(base({ app: "" })).join("\n")).toMatch(
|
|
119
|
-
/app is required/,
|
|
120
|
-
);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("rejects a malformed region", () => {
|
|
124
|
-
expect(validateConfig(base({ region: "not-a-region" })).join()).toMatch(
|
|
125
|
-
/region/,
|
|
126
|
-
);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("requires github owner + repo", () => {
|
|
130
|
-
const errs = validateConfig(normalizeConfig({ app: "a", github: {} }));
|
|
131
|
-
expect(errs.join("\n")).toMatch(/github.owner is required/);
|
|
132
|
-
expect(errs.join("\n")).toMatch(/github.repo is required/);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("rejects an unknown lambda arch", () => {
|
|
136
|
-
const cfg = {
|
|
137
|
-
...base(),
|
|
138
|
-
lambda: { ...base().lambda, arch: "riscv" as never },
|
|
139
|
-
};
|
|
140
|
-
expect(validateConfig(cfg).join()).toMatch(/lambda.arch/);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe("parseGitHubRemote", () => {
|
|
145
|
-
it("parses ssh, https, and https+.git forms", () => {
|
|
146
|
-
expect(
|
|
147
|
-
parseGitHubRemote("git@github.com:theserverkid/reactapistack.git"),
|
|
148
|
-
).toEqual({
|
|
149
|
-
owner: "theserverkid",
|
|
150
|
-
repo: "reactapistack",
|
|
151
|
-
});
|
|
152
|
-
expect(
|
|
153
|
-
parseGitHubRemote("https://github.com/theserverkid/reactapistack"),
|
|
154
|
-
).toEqual({
|
|
155
|
-
owner: "theserverkid",
|
|
156
|
-
repo: "reactapistack",
|
|
157
|
-
});
|
|
158
|
-
expect(
|
|
159
|
-
parseGitHubRemote("https://github.com/theserverkid/reactapistack.git/"),
|
|
160
|
-
).toEqual({
|
|
161
|
-
owner: "theserverkid",
|
|
162
|
-
repo: "reactapistack",
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("returns null for non-GitHub remotes", () => {
|
|
167
|
-
expect(parseGitHubRemote("git@gitlab.com:o/r.git")).toBeNull();
|
|
168
|
-
expect(parseGitHubRemote("not a url")).toBeNull();
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
describe("stack parameters + launch command", () => {
|
|
173
|
-
it("emits the app.yaml parameters in template order", () => {
|
|
174
|
-
// A region WITH Cognito advanced security, so AdvancedSecurity is left off.
|
|
175
|
-
const params = stackParameters(
|
|
176
|
-
base({ region: "us-east-1", artifactsBucket: "art-bucket" }),
|
|
177
|
-
);
|
|
178
|
-
expect(params.map((p) => p.key)).toEqual([
|
|
179
|
-
"AppName",
|
|
180
|
-
"NamePrefix",
|
|
181
|
-
"GitHubOwner",
|
|
182
|
-
"GitHubRepo",
|
|
183
|
-
"GitHubBranch",
|
|
184
|
-
"IcebergNamespace",
|
|
185
|
-
"ArtifactsBucket",
|
|
186
|
-
]);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("omits ArtifactsBucket when unset (it has no template default)", () => {
|
|
190
|
-
expect(
|
|
191
|
-
stackParameters(base({ region: "us-east-1" })).map((p) => p.key),
|
|
192
|
-
).not.toContain("ArtifactsBucket");
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("forces AdvancedSecurity=OFF only in regions lacking Cognito ASF", () => {
|
|
196
|
-
// af-south-1 (the repo default) has no threat protection — a User Pool with
|
|
197
|
-
// a UserPoolAddOns block is undeployable, so the override must be emitted.
|
|
198
|
-
expect(cognitoAdvancedSecuritySupported("af-south-1")).toBe(false);
|
|
199
|
-
expect(cognitoAdvancedSecuritySupported("us-east-1")).toBe(true);
|
|
200
|
-
|
|
201
|
-
const capeTown = stackParameters(base({ region: "af-south-1" }));
|
|
202
|
-
expect(capeTown.find((p) => p.key === "AdvancedSecurity")?.value).toBe(
|
|
203
|
-
"OFF",
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
const virginia = stackParameters(base({ region: "us-east-1" }));
|
|
207
|
-
expect(virginia.map((p) => p.key)).not.toContain("AdvancedSecurity");
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("always launches from the in-repo --template-file (deploy takes no URL)", () => {
|
|
211
|
-
// `aws cloudformation deploy` accepts ONLY `--template-file`; `--template-url`
|
|
212
|
-
// is exclusive to create-stack/update-stack and makes deploy reject the
|
|
213
|
-
// command. Even with a known templates bucket, the local file is used.
|
|
214
|
-
const hosted = deployStackCommand(
|
|
215
|
-
base({ templatesBucket: "tpl", artifactsBucket: "art" }),
|
|
216
|
-
);
|
|
217
|
-
expect(hosted).toContain("--template-file deploy/cloudformation/app.yaml");
|
|
218
|
-
expect(hosted).not.toContain("--template-url");
|
|
219
|
-
expect(hosted).toContain("--stack-name rastack-airline");
|
|
220
|
-
expect(hosted).toContain("ArtifactsBucket=art");
|
|
221
|
-
|
|
222
|
-
const local = deployStackCommand(base());
|
|
223
|
-
expect(local).toContain("--template-file deploy/cloudformation/app.yaml");
|
|
224
|
-
expect(local).not.toContain("--template-url");
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it("emits ONE line (PowerShell-safe — no backslash continuations)", () => {
|
|
228
|
-
// A `\`-continued command is bash-only; pasted into PowerShell each `--flag`
|
|
229
|
-
// becomes its own statement and errors. Keep it to a single line.
|
|
230
|
-
const cmd = deployStackCommand(base({ artifactsBucket: "art" }));
|
|
231
|
-
expect(cmd).not.toContain("\\");
|
|
232
|
-
expect(cmd.split("\n")).toHaveLength(1);
|
|
233
|
-
expect(cmd.startsWith("aws cloudformation deploy ")).toBe(true);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it("deployStackArgs is the argv the printed command joins", () => {
|
|
237
|
-
const cfg = base({ artifactsBucket: "art" });
|
|
238
|
-
expect(`aws ${deployStackArgs(cfg).join(" ")}`).toBe(
|
|
239
|
-
deployStackCommand(cfg),
|
|
240
|
-
);
|
|
241
|
-
expect(deployStackArgs(cfg).slice(0, 2)).toEqual([
|
|
242
|
-
"cloudformation",
|
|
243
|
-
"deploy",
|
|
244
|
-
]);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it("accepts a template-file override (the package-resolved path)", () => {
|
|
248
|
-
// `init` runs in a consumer repo that carries no framework IaC, so it passes
|
|
249
|
-
// the path the template was resolved to inside the `rastack` package.
|
|
250
|
-
const cfg = base({ artifactsBucket: "art" });
|
|
251
|
-
const abs = "/opt/rastack/dist/templates/cloudformation/app.yaml";
|
|
252
|
-
expect(deployStackArgs(cfg, abs)).toContain(abs);
|
|
253
|
-
expect(deployStackCommand(cfg, abs)).toContain(`--template-file ${abs}`);
|
|
254
|
-
// Default is unchanged when no override is given.
|
|
255
|
-
expect(deployStackCommand(cfg)).toContain(
|
|
256
|
-
"--template-file deploy/cloudformation/app.yaml",
|
|
257
|
-
);
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
describe("bootstrap stack", () => {
|
|
262
|
-
it("derives the bootstrap stack name from the prefix", () => {
|
|
263
|
-
expect(bootstrapStackName(base())).toBe("rastack-bootstrap");
|
|
264
|
-
expect(bootstrapStackName(base({ namePrefix: "acme" }))).toBe(
|
|
265
|
-
"acme-bootstrap",
|
|
266
|
-
);
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it("passes the publisher repo coordinates as parameters", () => {
|
|
270
|
-
const params = bootstrapStackParameters(base());
|
|
271
|
-
const map = Object.fromEntries(params.map((p) => [p.key, p.value]));
|
|
272
|
-
expect(map.NamePrefix).toBe("rastack");
|
|
273
|
-
expect(map.GitHubOwner).toBe("theserverkid");
|
|
274
|
-
expect(map.PublisherRepo).toBe("reactapistack-demo");
|
|
275
|
-
expect(map.PublisherBranch).toBe("master");
|
|
276
|
-
expect(map.CreateOIDCProvider).toBeUndefined();
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it("adds CreateOIDCProvider=false when reusing an existing provider", () => {
|
|
280
|
-
const params = bootstrapStackParameters(base(), true);
|
|
281
|
-
expect(params.find((p) => p.key === "CreateOIDCProvider")!.value).toBe(
|
|
282
|
-
"false",
|
|
283
|
-
);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it("builds a single-line command from the bootstrap template", () => {
|
|
287
|
-
const cmd = bootstrapStackCommand(base());
|
|
288
|
-
expect(cmd).not.toContain("\\");
|
|
289
|
-
expect(cmd.split("\n")).toHaveLength(1);
|
|
290
|
-
expect(cmd).toContain(
|
|
291
|
-
"--template-file deploy/cloudformation/bootstrap.yaml",
|
|
292
|
-
);
|
|
293
|
-
expect(cmd).toContain("--stack-name rastack-bootstrap");
|
|
294
|
-
expect(`aws ${bootstrapStackArgs(base()).join(" ")}`).toBe(cmd);
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it("accepts a template-file override (the package-resolved path)", () => {
|
|
298
|
-
const abs = "/opt/rastack/dist/templates/cloudformation/bootstrap.yaml";
|
|
299
|
-
expect(bootstrapStackArgs(base(), false, abs)).toContain(abs);
|
|
300
|
-
expect(bootstrapStackCommand(base(), true, abs)).toContain(
|
|
301
|
-
`--template-file ${abs}`,
|
|
302
|
-
);
|
|
303
|
-
// Reuse-OIDC + override compose without disturbing the default path.
|
|
304
|
-
expect(bootstrapStackArgs(base(), true, abs)).toContain(
|
|
305
|
-
"CreateOIDCProvider=false",
|
|
306
|
-
);
|
|
307
|
-
expect(bootstrapStackCommand(base())).toContain(
|
|
308
|
-
"--template-file deploy/cloudformation/bootstrap.yaml",
|
|
309
|
-
);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("validates only the account-level fields, not the per-app ones", () => {
|
|
313
|
-
// A config with no `app` is fine for bootstrap — it never touches app.yaml.
|
|
314
|
-
const cfg = normalizeConfig({
|
|
315
|
-
github: { owner: "theserverkid", repo: "reactapistack-demo" },
|
|
316
|
-
});
|
|
317
|
-
expect(validateConfig(cfg)).toContain(
|
|
318
|
-
"app is required (a short slug, e.g. `airline`).",
|
|
319
|
-
);
|
|
320
|
-
expect(validateBootstrapConfig(cfg)).toEqual([]);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
it("still flags a bad prefix, region, or GitHub coordinates", () => {
|
|
324
|
-
expect(
|
|
325
|
-
validateBootstrapConfig(base({ namePrefix: "Bad_Prefix" })),
|
|
326
|
-
).toContainEqual(expect.stringContaining("namePrefix"));
|
|
327
|
-
expect(
|
|
328
|
-
validateBootstrapConfig(base({ region: "not-a-region" })),
|
|
329
|
-
).toContainEqual(expect.stringContaining("region"));
|
|
330
|
-
expect(
|
|
331
|
-
validateBootstrapConfig(
|
|
332
|
-
normalizeConfig({ app: "airline", github: { repo: "demo" } }),
|
|
333
|
-
),
|
|
334
|
-
).toContain("github.owner is required.");
|
|
335
|
-
});
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
describe("app stack outputs map", () => {
|
|
339
|
-
it("maps each stack Output key to a DeployOutputs field", () => {
|
|
340
|
-
const keys = APP_STACK_OUTPUTS.map((o) => o.key);
|
|
341
|
-
expect(keys).toContain("FunctionName");
|
|
342
|
-
expect(keys).toContain("ApiUrl");
|
|
343
|
-
expect(keys).toContain("DeployRoleArn");
|
|
344
|
-
expect(APP_STACK_OUTPUTS.find((o) => o.key === "FunctionName")!.field).toBe(
|
|
345
|
-
"functionName",
|
|
346
|
-
);
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
describe("zipStore — placeholder packaging", () => {
|
|
351
|
-
it("produces a valid, deterministic STORE archive", () => {
|
|
352
|
-
const data = Buffer.from("#!/bin/sh\necho placeholder\n");
|
|
353
|
-
const a = zipStore([{ name: "bootstrap", data, mode: 0o755 }]);
|
|
354
|
-
const b = zipStore([{ name: "bootstrap", data, mode: 0o755 }]);
|
|
355
|
-
// Deterministic (fixed DOS timestamp) → idempotent uploads.
|
|
356
|
-
expect(a.equals(b)).toBe(true);
|
|
357
|
-
// Local file header + End-of-central-directory signatures.
|
|
358
|
-
expect(a.readUInt32LE(0)).toBe(0x04034b50);
|
|
359
|
-
expect(a.readUInt32LE(a.length - 22)).toBe(0x06054b50);
|
|
360
|
-
// One entry recorded in the EOCD.
|
|
361
|
-
expect(a.readUInt16LE(a.length - 12)).toBe(1);
|
|
362
|
-
// STORE method → the file bytes appear verbatim after the header + name.
|
|
363
|
-
expect(a.includes(data)).toBe(true);
|
|
364
|
-
});
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
describe("repo variables + lambda env", () => {
|
|
368
|
-
it("resolves repo Variables from stack outputs when present", () => {
|
|
369
|
-
const cfg = base({ outputs: { deployRoleArn: "arn:aws:iam::1:role/x" } });
|
|
370
|
-
const vars = repoVariables(cfg);
|
|
371
|
-
expect(vars.find((v) => v.name === "AWS_DEPLOY_ROLE_ARN")!.value).toBe(
|
|
372
|
-
"arn:aws:iam::1:role/x",
|
|
373
|
-
);
|
|
374
|
-
expect(vars.find((v) => v.name === "AWS_REGION")!.value).toBe("af-south-1");
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it("builds the `gh variable set` argv init wires the pipeline with", () => {
|
|
378
|
-
const cfg = base({ outputs: { deployRoleArn: "arn:aws:iam::1:role/x" } });
|
|
379
|
-
const roleVar = repoVariables(cfg).find(
|
|
380
|
-
(v) => v.name === "AWS_DEPLOY_ROLE_ARN",
|
|
381
|
-
)!;
|
|
382
|
-
expect(ghVariableSetArgs(cfg, roleVar.name, roleVar.value)).toEqual([
|
|
383
|
-
"variable",
|
|
384
|
-
"set",
|
|
385
|
-
"AWS_DEPLOY_ROLE_ARN",
|
|
386
|
-
"--repo",
|
|
387
|
-
"theserverkid/reactapistack-demo",
|
|
388
|
-
"--body",
|
|
389
|
-
"arn:aws:iam::1:role/x",
|
|
390
|
-
]);
|
|
391
|
-
// Region variable targets the same repo with the region value.
|
|
392
|
-
expect(ghVariableSetArgs(cfg, "AWS_REGION", cfg.region)).toEqual([
|
|
393
|
-
"variable",
|
|
394
|
-
"set",
|
|
395
|
-
"AWS_REGION",
|
|
396
|
-
"--repo",
|
|
397
|
-
"theserverkid/reactapistack-demo",
|
|
398
|
-
"--body",
|
|
399
|
-
"af-south-1",
|
|
400
|
-
]);
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
it("fills the RASTACK_* env, using outputs where available", () => {
|
|
404
|
-
const cfg = base({
|
|
405
|
-
outputs: {
|
|
406
|
-
warehouseBucket: "wh-bucket",
|
|
407
|
-
userPoolId: "eu_x",
|
|
408
|
-
userPoolClientId: "cli",
|
|
409
|
-
},
|
|
410
|
-
});
|
|
411
|
-
const env = Object.fromEntries(lambdaEnv(cfg).map((e) => [e.key, e.value]));
|
|
412
|
-
expect(env.RASTACK_WAREHOUSE).toBe("s3://wh-bucket/warehouse");
|
|
413
|
-
expect(env.RASTACK_ISSUER).toBe(
|
|
414
|
-
"https://cognito-idp.af-south-1.amazonaws.com/eu_x",
|
|
415
|
-
);
|
|
416
|
-
expect(env.RASTACK_AUDIENCE).toBe("cli");
|
|
417
|
-
expect(env.RASTACK_TOKEN_USE).toBe("access");
|
|
418
|
-
});
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
describe("ci plan — check", () => {
|
|
422
|
-
it("validates then compiles in-process, honouring the resources dir + bin", () => {
|
|
423
|
-
const steps = checkSteps(base({ resourcesDir: "res" }), "npx rastack");
|
|
424
|
-
expect(steps.map((s) => s.run)).toEqual([
|
|
425
|
-
"npx rastack check res",
|
|
426
|
-
"compile res .rastack (in-process)",
|
|
427
|
-
]);
|
|
428
|
-
// The compile step runs in-process (no shell command), so the CLI can drop
|
|
429
|
-
// the standalone `compile` command entirely.
|
|
430
|
-
expect(steps[1].action).toBe("compile");
|
|
431
|
-
expect(ciPlan("check", base(), { bin: "rastack" })).toEqual(
|
|
432
|
-
checkSteps(base(), "rastack"),
|
|
433
|
-
);
|
|
434
|
-
});
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
describe("ci plan — build", () => {
|
|
438
|
-
it("uses --arm64 and bakes the JWKS from the stack output when unresolved", () => {
|
|
439
|
-
const steps = buildSteps(base());
|
|
440
|
-
const cross = steps.find((s) => s.name.startsWith("Cross-build"))!;
|
|
441
|
-
expect(cross.run).toContain("--arm64");
|
|
442
|
-
expect(cross.cwd).toBe("rust");
|
|
443
|
-
const bake = steps.find((s) => s.name.startsWith("Bake"))!;
|
|
444
|
-
expect(bake.run).toContain("describe-stacks");
|
|
445
|
-
expect(bake.run).toContain("OutputKey=='JwksUrl'");
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
it("uses --x86-64 for an x86 build and a literal JWKS when wired", () => {
|
|
449
|
-
const cfg = base({
|
|
450
|
-
lambda: { ...base().lambda, arch: "x86_64" },
|
|
451
|
-
outputs: { jwksUrl: "https://jwks" },
|
|
452
|
-
});
|
|
453
|
-
const steps = buildSteps(cfg);
|
|
454
|
-
expect(steps.find((s) => s.name.startsWith("Cross-build"))!.run).toContain(
|
|
455
|
-
"--x86-64",
|
|
456
|
-
);
|
|
457
|
-
const bake = steps.find((s) => s.name.startsWith("Bake"))!;
|
|
458
|
-
expect(bake.run).toContain('"https://jwks"');
|
|
459
|
-
expect(bake.run).not.toContain("describe-stacks");
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
it("stages the compiled manifest + openapi next to the binary", () => {
|
|
463
|
-
const steps = buildSteps(base());
|
|
464
|
-
const zip = steps.find((s) => s.name.startsWith("Assemble"))!;
|
|
465
|
-
expect(zip.run).toContain("rust/target/lambda/bootstrap/bootstrap.zip");
|
|
466
|
-
expect(zip.run).toContain("schema.rastack.json");
|
|
467
|
-
expect(zip.run).toContain("openapi.json");
|
|
468
|
-
expect(zip.run).toContain("jwks.json");
|
|
469
|
-
});
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
describe("ci plan — deploy", () => {
|
|
473
|
-
it("builds then ships, resolving the function name live when unwired", () => {
|
|
474
|
-
const steps = deploySteps(base());
|
|
475
|
-
const ship = steps.find((s) => s.name.startsWith("Ship"))!;
|
|
476
|
-
expect(ship.run).toContain("aws lambda update-function-code");
|
|
477
|
-
expect(ship.run).toContain("OutputKey=='FunctionName'");
|
|
478
|
-
expect(ship.run).toContain(
|
|
479
|
-
"fileb://rust/target/lambda/bootstrap/bootstrap.zip",
|
|
480
|
-
);
|
|
481
|
-
// build steps are included
|
|
482
|
-
expect(steps.some((s) => s.name.startsWith("Cross-build"))).toBe(true);
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
it("skips the build with skipBuild and uses the wired function name", () => {
|
|
486
|
-
const cfg = base({ outputs: { functionName: "airline-api" } });
|
|
487
|
-
const steps = deploySteps(cfg, { skipBuild: true });
|
|
488
|
-
expect(steps.some((s) => s.name.startsWith("Cross-build"))).toBe(false);
|
|
489
|
-
const ship = steps.find((s) => s.name.startsWith("Ship"))!;
|
|
490
|
-
expect(ship.run).toContain("--function-name airline-api");
|
|
491
|
-
expect(ship.run).not.toContain("describe-stacks");
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
it("ciPlan dispatches each phase", () => {
|
|
495
|
-
expect(ciPlan("build", base())).toEqual(buildSteps(base()));
|
|
496
|
-
expect(ciPlan("deploy", base())).toEqual(deploySteps(base()));
|
|
497
|
-
});
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
describe("workflow templates", () => {
|
|
501
|
-
it("renders valid YAML that pins the deploy branch", () => {
|
|
502
|
-
const ci = renderCiWorkflow(
|
|
503
|
-
base({ github: { owner: "o", repo: "r", branch: "main" } }),
|
|
504
|
-
);
|
|
505
|
-
const doc = yaml.load(ci) as any;
|
|
506
|
-
expect(doc.name).toBe("CI");
|
|
507
|
-
// `on:` parses to the boolean-ish key `true` in YAML 1.1 — assert via the raw text.
|
|
508
|
-
expect(ci).toContain("branches: [main]");
|
|
509
|
-
expect(ci).toContain("run: rastack ci check");
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
it("deploy workflow keeps the GitHub expressions intact and runs one line", () => {
|
|
513
|
-
const wf = renderDeployWorkflow(base());
|
|
514
|
-
const doc = yaml.load(wf) as any;
|
|
515
|
-
expect(doc.permissions["id-token"]).toBe("write");
|
|
516
|
-
expect(wf).toContain("${{ vars.AWS_DEPLOY_ROLE_ARN }}");
|
|
517
|
-
expect(wf).toContain("aws-actions/configure-aws-credentials@v4");
|
|
518
|
-
expect(wf).toContain("run: rastack ci deploy");
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
it("names the deploy job after the target architecture", () => {
|
|
522
|
-
expect(renderDeployWorkflow(base())).toContain("(arm64)");
|
|
523
|
-
expect(
|
|
524
|
-
renderDeployWorkflow(
|
|
525
|
-
base({ lambda: { ...base().lambda, arch: "x86_64" } }),
|
|
526
|
-
),
|
|
527
|
-
).toContain("(x86_64)");
|
|
528
|
-
});
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
describe("scaffold", () => {
|
|
532
|
-
it("writes the config + both workflows by default", () => {
|
|
533
|
-
const files = scaffoldFiles(base());
|
|
534
|
-
expect(files.map((f) => f.path).sort()).toEqual(
|
|
535
|
-
[CONFIG_PATH, CI_WORKFLOW_PATH, DEPLOY_WORKFLOW_PATH].sort(),
|
|
536
|
-
);
|
|
537
|
-
// the config file round-trips through normalize
|
|
538
|
-
const cfgFile = files.find((f) => f.path === CONFIG_PATH)!;
|
|
539
|
-
expect(normalizeConfig(JSON.parse(cfgFile.content))).toEqual(base());
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
it("omits the workflows with noWorkflows", () => {
|
|
543
|
-
const files = scaffoldFiles(base(), { noWorkflows: true });
|
|
544
|
-
expect(files.map((f) => f.path)).toEqual([CONFIG_PATH]);
|
|
545
|
-
});
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
describe("resolveDeployAsset — bundled template resolution", () => {
|
|
549
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
550
|
-
const { resolveDeployAsset } = require("../src/deploy/io");
|
|
551
|
-
const path = require("path");
|
|
552
|
-
|
|
553
|
-
it("resolves the framework templates to an existing file", () => {
|
|
554
|
-
for (const name of ["app.yaml", "bootstrap.yaml"]) {
|
|
555
|
-
const resolved = resolveDeployAsset(name);
|
|
556
|
-
expect(resolved).not.toBeNull();
|
|
557
|
-
expect(path.isAbsolute(resolved)).toBe(true);
|
|
558
|
-
expect(resolved.endsWith(name)).toBe(true);
|
|
559
|
-
}
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
it("resolves the Lambda placeholder bundle", () => {
|
|
563
|
-
const resolved = resolveDeployAsset(path.join("placeholder", "bootstrap"));
|
|
564
|
-
expect(resolved).not.toBeNull();
|
|
565
|
-
expect(resolved.endsWith(path.join("placeholder", "bootstrap"))).toBe(true);
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
it("returns null for an asset that does not exist", () => {
|
|
569
|
-
expect(resolveDeployAsset("does-not-exist.yaml")).toBeNull();
|
|
570
|
-
});
|
|
571
|
-
});
|