@intentius/chant-lexicon-aws 0.44.13 → 0.45.0
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/dist/agentcore/trace-fetch.d.ts +4 -1
- package/dist/agentcore/trace-fetch.d.ts.map +1 -1
- package/dist/api/read-client.d.ts +31 -1
- package/dist/api/read-client.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/cloud-executor.d.ts +9 -0
- package/dist/components/cloud-executor.d.ts.map +1 -1
- package/dist/composites/agentcore-agent.d.ts +33 -19
- package/dist/composites/agentcore-agent.d.ts.map +1 -1
- package/dist/composites/index.d.ts +1 -1
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/deep-observe.d.ts +21 -0
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/op/activities/aws-apply.d.ts +14 -4
- package/dist/op/activities/aws-apply.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/properties.d.ts +4 -3
- package/dist/properties.d.ts.map +1 -1
- package/dist/spec/fetch.d.ts +12 -1
- package/dist/spec/fetch.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/agentcore/trace-fetch.test.ts +17 -0
- package/src/agentcore/trace-fetch.ts +7 -2
- package/src/api/read-client.test.ts +87 -0
- package/src/api/read-client.ts +57 -1
- package/src/codegen/docs-links.test.ts +44 -30
- package/src/codegen/docs.ts +2 -1034
- package/src/components/capability-plugin.ts +5 -2
- package/src/components/cloud-executor.test.ts +29 -1
- package/src/components/cloud-executor.ts +23 -5
- package/src/composites/agentcore-agent.test.ts +32 -12
- package/src/composites/agentcore-agent.ts +43 -23
- package/src/composites/index.ts +1 -1
- package/src/composites/microvm-app.test.ts +2 -2
- package/src/deep-observe.test.ts +94 -0
- package/src/deep-observe.ts +58 -2
- package/src/import/roundtrip-fixtures.test.ts +1 -1
- package/src/index.ts +3 -1
- package/src/lifecycle-integration.test.ts +89 -0
- package/src/op/activities/aws-apply.test.ts +40 -4
- package/src/op/activities/aws-apply.ts +23 -7
- package/src/plugin.ts +34 -35
- package/src/properties.test.ts +5 -5
- package/src/properties.ts +6 -5
- package/src/serializer.test.ts +73 -33
- package/src/spec/fetch.test.ts +40 -0
- package/src/spec/fetch.ts +24 -3
package/src/serializer.test.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { AWS } from "./pseudo";
|
|
|
8
8
|
import { nestedStack, NestedStackOutputRef } from "./nested-stack";
|
|
9
9
|
import { stackOutput } from "@intentius/chant/stack-output";
|
|
10
10
|
import { createResource } from "@intentius/chant/runtime";
|
|
11
|
+
import { resolveAttrRefs } from "@intentius/chant/discovery/resolve";
|
|
11
12
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
12
13
|
import type { BuildResult } from "@intentius/chant/build";
|
|
13
14
|
import { Parameter } from "./parameter";
|
|
@@ -41,7 +42,7 @@ describe("awsSerializer", () => {
|
|
|
41
42
|
describe("awsSerializer.serialize", () => {
|
|
42
43
|
test("produces valid CF template structure", () => {
|
|
43
44
|
const entities = new Map<string, Declarable>();
|
|
44
|
-
const output = awsSerializer.serialize(entities);
|
|
45
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
45
46
|
const template = JSON.parse(output);
|
|
46
47
|
|
|
47
48
|
expect(template.AWSTemplateFormatVersion).toBe("2010-09-09");
|
|
@@ -50,7 +51,7 @@ describe("awsSerializer.serialize", () => {
|
|
|
50
51
|
|
|
51
52
|
test("serializes empty entities", () => {
|
|
52
53
|
const entities = new Map<string, Declarable>();
|
|
53
|
-
const output = awsSerializer.serialize(entities);
|
|
54
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
54
55
|
const template = JSON.parse(output);
|
|
55
56
|
|
|
56
57
|
expect(template.Resources).toEqual({});
|
|
@@ -61,7 +62,7 @@ describe("awsSerializer.serialize", () => {
|
|
|
61
62
|
const entities = new Map<string, Declarable>();
|
|
62
63
|
entities.set("MyBucket", new MockBucket({ BucketName: "my-bucket" }));
|
|
63
64
|
|
|
64
|
-
const output = awsSerializer.serialize(entities);
|
|
65
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
65
66
|
const template = JSON.parse(output);
|
|
66
67
|
|
|
67
68
|
expect(template.Resources.MyBucket).toBeDefined();
|
|
@@ -74,7 +75,7 @@ describe("awsSerializer.serialize", () => {
|
|
|
74
75
|
entities.set("MyBucket", new MockBucket({ BucketName: "my-bucket" }));
|
|
75
76
|
entities.set("rotationTransform", templateTransform("AWS::SecretsManager-2020-07-23"));
|
|
76
77
|
|
|
77
|
-
const template = JSON.parse(awsSerializer.serialize(entities));
|
|
78
|
+
const template = JSON.parse(awsSerializer.serialize(entities) as string);
|
|
78
79
|
|
|
79
80
|
expect(template.Transform).toBe("AWS::SecretsManager-2020-07-23");
|
|
80
81
|
expect(template.Resources.rotationTransform).toBeUndefined();
|
|
@@ -87,7 +88,7 @@ describe("awsSerializer.serialize", () => {
|
|
|
87
88
|
entities.set("t2", templateTransform("AWS::LanguageExtensions"));
|
|
88
89
|
entities.set("t3", templateTransform("AWS::SecretsManager-2020-07-23")); // dup
|
|
89
90
|
|
|
90
|
-
const template = JSON.parse(awsSerializer.serialize(entities));
|
|
91
|
+
const template = JSON.parse(awsSerializer.serialize(entities) as string);
|
|
91
92
|
|
|
92
93
|
expect(template.Transform).toEqual(["AWS::SecretsManager-2020-07-23", "AWS::LanguageExtensions"]);
|
|
93
94
|
});
|
|
@@ -96,7 +97,7 @@ describe("awsSerializer.serialize", () => {
|
|
|
96
97
|
const entities = new Map<string, Declarable>();
|
|
97
98
|
entities.set("MyBucket", new MockBucket({ BucketName: "my-bucket" }));
|
|
98
99
|
|
|
99
|
-
const template = JSON.parse(awsSerializer.serialize(entities));
|
|
100
|
+
const template = JSON.parse(awsSerializer.serialize(entities) as string);
|
|
100
101
|
|
|
101
102
|
expect(template.Transform).toBeUndefined();
|
|
102
103
|
});
|
|
@@ -108,7 +109,7 @@ describe("awsSerializer.serialize", () => {
|
|
|
108
109
|
defaultValue: "dev",
|
|
109
110
|
}));
|
|
110
111
|
|
|
111
|
-
const output = awsSerializer.serialize(entities);
|
|
112
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
112
113
|
const template = JSON.parse(output);
|
|
113
114
|
|
|
114
115
|
expect(template.Parameters).toBeDefined();
|
|
@@ -124,7 +125,7 @@ describe("awsSerializer.serialize", () => {
|
|
|
124
125
|
VersioningConfiguration: { Status: "Enabled" },
|
|
125
126
|
}));
|
|
126
127
|
|
|
127
|
-
const output = awsSerializer.serialize(entities);
|
|
128
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
128
129
|
const template = JSON.parse(output);
|
|
129
130
|
|
|
130
131
|
expect(template.Resources.MyBucket.Properties.VersioningConfiguration).toEqual({
|
|
@@ -136,7 +137,7 @@ describe("awsSerializer.serialize", () => {
|
|
|
136
137
|
const entities = new Map<string, Declarable>();
|
|
137
138
|
entities.set("MyBucket", new MockBucket({ BucketName: "test" }));
|
|
138
139
|
|
|
139
|
-
const output = awsSerializer.serialize(entities);
|
|
140
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
140
141
|
const template = JSON.parse(output);
|
|
141
142
|
|
|
142
143
|
expect(template.Resources.MyBucket.Properties.BucketName).toBeDefined();
|
|
@@ -147,7 +148,7 @@ describe("awsSerializer.serialize", () => {
|
|
|
147
148
|
entities.set("DataBucket", new MockBucket({ BucketName: "data-bucket" }));
|
|
148
149
|
entities.set("LogsBucket", new MockBucket({ BucketName: "logs-bucket" }));
|
|
149
150
|
|
|
150
|
-
const output = awsSerializer.serialize(entities);
|
|
151
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
151
152
|
const template = JSON.parse(output);
|
|
152
153
|
|
|
153
154
|
expect(Object.keys(template.Resources)).toHaveLength(2);
|
|
@@ -160,7 +161,7 @@ describe("awsSerializer.serialize", () => {
|
|
|
160
161
|
entities.set("Env", new Parameter("String"));
|
|
161
162
|
entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
|
|
162
163
|
|
|
163
|
-
const output = awsSerializer.serialize(entities);
|
|
164
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
164
165
|
const template = JSON.parse(output);
|
|
165
166
|
|
|
166
167
|
expect(template.Parameters).toBeDefined();
|
|
@@ -173,7 +174,7 @@ describe("awsSerializer.serialize", () => {
|
|
|
173
174
|
const entities = new Map<string, Declarable>();
|
|
174
175
|
entities.set("MyBucket", new MockBucket({})); // No properties set
|
|
175
176
|
|
|
176
|
-
const output = awsSerializer.serialize(entities);
|
|
177
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
177
178
|
const template = JSON.parse(output);
|
|
178
179
|
|
|
179
180
|
// Should have no Properties key or empty properties
|
|
@@ -210,7 +211,7 @@ describe("property-kind Declarables", () => {
|
|
|
210
211
|
entities.set("DataEncryption", encryption);
|
|
211
212
|
entities.set("MyBucket", bucket);
|
|
212
213
|
|
|
213
|
-
const output = awsSerializer.serialize(entities);
|
|
214
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
214
215
|
const template = JSON.parse(output);
|
|
215
216
|
|
|
216
217
|
// Encryption should be inlined, not a Ref
|
|
@@ -232,7 +233,7 @@ describe("property-kind Declarables", () => {
|
|
|
232
233
|
entities.set("DataEncryption", encryption);
|
|
233
234
|
entities.set("MyBucket", new MockBucket({ BucketName: "my-bucket" }));
|
|
234
235
|
|
|
235
|
-
const output = awsSerializer.serialize(entities);
|
|
236
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
236
237
|
const template = JSON.parse(output);
|
|
237
238
|
|
|
238
239
|
expect(template.Resources.DataEncryption).toBeUndefined();
|
|
@@ -257,7 +258,7 @@ describe("property-kind Declarables", () => {
|
|
|
257
258
|
entities.set("SourceBucket", sourceBucket);
|
|
258
259
|
entities.set("Config", new MockConfig(sourceBucket));
|
|
259
260
|
|
|
260
|
-
const output = awsSerializer.serialize(entities);
|
|
261
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
261
262
|
const template = JSON.parse(output);
|
|
262
263
|
|
|
263
264
|
expect(template.Resources.Config.Properties.Bucket).toEqual({ Ref: "SourceBucket" });
|
|
@@ -268,7 +269,7 @@ describe("intrinsic serialization", () => {
|
|
|
268
269
|
test("handles AttrRef in properties", () => {
|
|
269
270
|
const source = new MockBucket({ BucketName: "source" });
|
|
270
271
|
// Set the logical name on the AttrRef before using it
|
|
271
|
-
(source.arn as
|
|
272
|
+
(source.arn as unknown as { _setLogicalName(n: string): void })._setLogicalName("SourceBucket");
|
|
272
273
|
|
|
273
274
|
class MockReplication implements Declarable {
|
|
274
275
|
readonly [DECLARABLE_MARKER] = true as const;
|
|
@@ -285,27 +286,61 @@ describe("intrinsic serialization", () => {
|
|
|
285
286
|
entities.set("SourceBucket", source);
|
|
286
287
|
entities.set("Replication", new MockReplication(source.arn));
|
|
287
288
|
|
|
288
|
-
const output = awsSerializer.serialize(entities);
|
|
289
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
289
290
|
const template = JSON.parse(output);
|
|
290
291
|
|
|
291
292
|
expect(template.Resources.Replication.Properties.SourceArn).toEqual({
|
|
292
293
|
"Fn::GetAtt": ["SourceBucket", "Arn"],
|
|
293
294
|
});
|
|
294
295
|
});
|
|
296
|
+
|
|
297
|
+
// chant #1535 — the issue's example: a sibling attr-ref inside a nested
|
|
298
|
+
// policy document. The serializer walks it to Fn::GetAtt at any depth; the
|
|
299
|
+
// `Principal: {}` the issue saw came from the fold path dropping the ref
|
|
300
|
+
// before the serializer ever ran (see core's fold.ts). Pinned here so the
|
|
301
|
+
// aws side of that contract stays covered.
|
|
302
|
+
test("handles AttrRef nested inside a policy document (#1535)", () => {
|
|
303
|
+
const OIDCProvider = createResource("AWS::IAM::OIDCProvider", "aws", { Arn: "Arn" });
|
|
304
|
+
const Role = createResource("AWS::IAM::Role", "aws", { Arn: "Arn", RoleId: "RoleId" });
|
|
305
|
+
const provider = new OIDCProvider({ Url: "https://token.actions.githubusercontent.com" });
|
|
306
|
+
const role = new Role({
|
|
307
|
+
AssumeRolePolicyDocument: {
|
|
308
|
+
Version: "2012-10-17",
|
|
309
|
+
Statement: [
|
|
310
|
+
{
|
|
311
|
+
Effect: "Allow",
|
|
312
|
+
Principal: { Federated: provider.Arn },
|
|
313
|
+
Action: "sts:AssumeRoleWithWebIdentity",
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
const entities = new Map<string, Declarable>([
|
|
319
|
+
["Provider", provider],
|
|
320
|
+
["Role", role],
|
|
321
|
+
]);
|
|
322
|
+
resolveAttrRefs(entities);
|
|
323
|
+
|
|
324
|
+
const template = JSON.parse(awsSerializer.serialize(entities) as string);
|
|
325
|
+
|
|
326
|
+
expect(template.Resources.Role.Properties.AssumeRolePolicyDocument.Statement[0].Principal).toEqual({
|
|
327
|
+
Federated: { "Fn::GetAtt": ["Provider", "Arn"] },
|
|
328
|
+
});
|
|
329
|
+
});
|
|
295
330
|
});
|
|
296
331
|
|
|
297
332
|
describe("LexiconOutput serialization", () => {
|
|
298
333
|
test("resolves AttrRef nested in a Join output to Fn::GetAtt, not the __attrRef envelope (chant#935)", () => {
|
|
299
334
|
const s1 = new MockBucket({ BucketName: "subnet-1" });
|
|
300
335
|
const s2 = new MockBucket({ BucketName: "subnet-2" });
|
|
301
|
-
(s1.arn as
|
|
302
|
-
(s2.arn as
|
|
336
|
+
(s1.arn as unknown as { _setLogicalName(n: string): void })._setLogicalName("Subnet1");
|
|
337
|
+
(s2.arn as unknown as { _setLogicalName(n: string): void })._setLogicalName("Subnet2");
|
|
303
338
|
const joined = new LexiconOutput(Join(":", [s1.arn, s2.arn]), "SubnetIds");
|
|
304
339
|
joined._setSourceEntity("Subnet1");
|
|
305
340
|
const entities = new Map<string, Declarable>();
|
|
306
341
|
entities.set("Subnet1", s1);
|
|
307
342
|
entities.set("Subnet2", s2);
|
|
308
|
-
const template = JSON.parse(awsSerializer.serialize(entities, [joined]));
|
|
343
|
+
const template = JSON.parse(awsSerializer.serialize(entities, [joined]) as string);
|
|
309
344
|
const value = JSON.stringify(template.Outputs.SubnetIds.Value);
|
|
310
345
|
expect(value).not.toContain("__attrRef");
|
|
311
346
|
expect(value).toContain("Fn::GetAtt");
|
|
@@ -320,7 +355,7 @@ describe("LexiconOutput serialization", () => {
|
|
|
320
355
|
const entities = new Map<string, Declarable>();
|
|
321
356
|
entities.set("dataBucket", bucket);
|
|
322
357
|
|
|
323
|
-
const result = awsSerializer.serialize(entities, [lexiconOutput]);
|
|
358
|
+
const result = awsSerializer.serialize(entities, [lexiconOutput]) as string;
|
|
324
359
|
const template = JSON.parse(result);
|
|
325
360
|
|
|
326
361
|
expect(template.Outputs).toBeDefined();
|
|
@@ -342,7 +377,7 @@ describe("LexiconOutput serialization", () => {
|
|
|
342
377
|
entities.set("dataBucket", dataBucket);
|
|
343
378
|
entities.set("logsBucket", logsBucket);
|
|
344
379
|
|
|
345
|
-
const result = awsSerializer.serialize(entities, [dataOutput, logsOutput]);
|
|
380
|
+
const result = awsSerializer.serialize(entities, [dataOutput, logsOutput]) as string;
|
|
346
381
|
const template = JSON.parse(result);
|
|
347
382
|
|
|
348
383
|
expect(template.Outputs).toBeDefined();
|
|
@@ -376,7 +411,7 @@ describe("LexiconOutput serialization", () => {
|
|
|
376
411
|
const entities = new Map<string, Declarable>();
|
|
377
412
|
entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
|
|
378
413
|
|
|
379
|
-
const result = awsSerializer.serialize(entities);
|
|
414
|
+
const result = awsSerializer.serialize(entities) as string;
|
|
380
415
|
const template = JSON.parse(result);
|
|
381
416
|
|
|
382
417
|
expect(template.Outputs).toBeUndefined();
|
|
@@ -458,7 +493,12 @@ function mockChildBuildResult(childTemplate: object): BuildResult {
|
|
|
458
493
|
entities: new Map(),
|
|
459
494
|
warnings: [],
|
|
460
495
|
errors: [],
|
|
461
|
-
manifest: {
|
|
496
|
+
manifest: {
|
|
497
|
+
lexicons: ["aws"],
|
|
498
|
+
outputs: {},
|
|
499
|
+
deployOrder: ["aws"],
|
|
500
|
+
stackGraph: { nodes: ["aws"], edges: [], order: ["aws"], waves: [["aws"]], cycles: [] },
|
|
501
|
+
},
|
|
462
502
|
sourceFileCount: 1,
|
|
463
503
|
};
|
|
464
504
|
}
|
|
@@ -623,7 +663,7 @@ class MockResourceWithAttrs implements Declarable {
|
|
|
623
663
|
readonly lexicon = "aws";
|
|
624
664
|
readonly entityType: string;
|
|
625
665
|
readonly props: Record<string, unknown>;
|
|
626
|
-
readonly attributes
|
|
666
|
+
readonly attributes!: Record<string, unknown>;
|
|
627
667
|
|
|
628
668
|
constructor(
|
|
629
669
|
type: string,
|
|
@@ -639,7 +679,7 @@ class MockResourceWithAttrs implements Declarable {
|
|
|
639
679
|
describe("resource-level CF attributes", () => {
|
|
640
680
|
function serialize(...entries: [string, Declarable][]) {
|
|
641
681
|
const entities = new Map<string, Declarable>(entries);
|
|
642
|
-
const output = awsSerializer.serialize(entities);
|
|
682
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
643
683
|
return JSON.parse(output as string);
|
|
644
684
|
}
|
|
645
685
|
|
|
@@ -839,7 +879,7 @@ describe("default tags serialization", () => {
|
|
|
839
879
|
entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
|
|
840
880
|
entities.set("tags", defaultTags([{ Key: "Env", Value: "prod" }]) as unknown as Declarable);
|
|
841
881
|
|
|
842
|
-
const output = awsSerializer.serialize(entities);
|
|
882
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
843
883
|
const template = JSON.parse(output as string);
|
|
844
884
|
|
|
845
885
|
expect(template.Resources.tags).toBeUndefined();
|
|
@@ -851,7 +891,7 @@ describe("default tags serialization", () => {
|
|
|
851
891
|
entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
|
|
852
892
|
entities.set("tags", defaultTags([{ Key: "Env", Value: "prod" }]) as unknown as Declarable);
|
|
853
893
|
|
|
854
|
-
const output = awsSerializer.serialize(entities);
|
|
894
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
855
895
|
const template = JSON.parse(output as string);
|
|
856
896
|
|
|
857
897
|
expect(template.Resources.MyBucket.Properties.Tags).toEqual([
|
|
@@ -868,7 +908,7 @@ describe("default tags serialization", () => {
|
|
|
868
908
|
}));
|
|
869
909
|
entities.set("tags", defaultTags([{ Key: "Env", Value: "prod" }]) as unknown as Declarable);
|
|
870
910
|
|
|
871
|
-
const output = awsSerializer.serialize(entities);
|
|
911
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
872
912
|
const template = JSON.parse(output as string);
|
|
873
913
|
|
|
874
914
|
expect(template.Resources.Perm.Properties.Tags).toBeUndefined();
|
|
@@ -885,7 +925,7 @@ describe("default tags serialization", () => {
|
|
|
885
925
|
{ Key: "Team", Value: "platform" },
|
|
886
926
|
]) as unknown as Declarable);
|
|
887
927
|
|
|
888
|
-
const output = awsSerializer.serialize(entities);
|
|
928
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
889
929
|
const template = JSON.parse(output as string);
|
|
890
930
|
|
|
891
931
|
const tags = template.Resources.MyBucket.Properties.Tags;
|
|
@@ -904,7 +944,7 @@ describe("default tags serialization", () => {
|
|
|
904
944
|
{ Key: "Stack", Value: Sub`${AWS.StackName}` },
|
|
905
945
|
]) as unknown as Declarable);
|
|
906
946
|
|
|
907
|
-
const output = awsSerializer.serialize(entities);
|
|
947
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
908
948
|
const template = JSON.parse(output as string);
|
|
909
949
|
|
|
910
950
|
const tags = template.Resources.MyBucket.Properties.Tags;
|
|
@@ -922,7 +962,7 @@ describe("default tags serialization", () => {
|
|
|
922
962
|
{ Key: "Environment", Value: env },
|
|
923
963
|
]) as unknown as Declarable);
|
|
924
964
|
|
|
925
|
-
const output = awsSerializer.serialize(entities);
|
|
965
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
926
966
|
const template = JSON.parse(output as string);
|
|
927
967
|
|
|
928
968
|
const tags = template.Resources.MyBucket.Properties.Tags;
|
|
@@ -935,7 +975,7 @@ describe("default tags serialization", () => {
|
|
|
935
975
|
const entities = new Map<string, Declarable>();
|
|
936
976
|
entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
|
|
937
977
|
|
|
938
|
-
const output = awsSerializer.serialize(entities);
|
|
978
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
939
979
|
const template = JSON.parse(output as string);
|
|
940
980
|
|
|
941
981
|
expect(template.Resources.MyBucket.Properties.Tags).toBeUndefined();
|
package/src/spec/fetch.test.ts
CHANGED
|
@@ -125,3 +125,43 @@ describe("loadPinnedSchemas (#1511)", () => {
|
|
|
125
125
|
);
|
|
126
126
|
});
|
|
127
127
|
});
|
|
128
|
+
|
|
129
|
+
// #1481 — the live fallback is the nondeterminism that made `chant` and
|
|
130
|
+
// `publish` disagree about the same commit. Under the release gate it is a
|
|
131
|
+
// refusal, and the refusal fires before any request to the live archive.
|
|
132
|
+
describe("fetchSchemaZip under CHANT_RELEASE_GATE (#1481)", () => {
|
|
133
|
+
test("refuses to fall back to the live archive when the pinned asset is unavailable", async () => {
|
|
134
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-spec-pin-"));
|
|
135
|
+
try {
|
|
136
|
+
await expect(
|
|
137
|
+
fetchSchemaZip(false, {
|
|
138
|
+
env: { CHANT_RELEASE_GATE: "1" },
|
|
139
|
+
download: async () => undefined,
|
|
140
|
+
pinCacheDir: dir,
|
|
141
|
+
}),
|
|
142
|
+
).rejects.toThrow(/CHANT_RELEASE_GATE=1 and the pinned spec asset could not be loaded/);
|
|
143
|
+
} finally {
|
|
144
|
+
rmSync(dir, { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("the gate goes through the pinned loader, never the live path", async () => {
|
|
149
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-spec-pin-"));
|
|
150
|
+
try {
|
|
151
|
+
const { zipSync } = await import("fflate");
|
|
152
|
+
const { AWS_SPEC_PIN } = await import("./pin");
|
|
153
|
+
// A zip that digests to the committed pin cannot be forged here, so use
|
|
154
|
+
// the verified-cache path: a cached asset that does NOT digest to the
|
|
155
|
+
// pin is refused by the loader, which is only reachable on the pinned
|
|
156
|
+
// path. The live path would have returned whatever upstream serves.
|
|
157
|
+
const bogus = Buffer.from(zipSync({ "x.json": Buffer.from(JSON.stringify({ typeName: "AWS::X::Y" })) }));
|
|
158
|
+
mkdirSync(dir, { recursive: true });
|
|
159
|
+
writeFileSync(join(dir, pinAssetName(AWS_SPEC_PIN)), bogus);
|
|
160
|
+
await expect(
|
|
161
|
+
fetchSchemaZip(false, { env: { CHANT_RELEASE_GATE: "1" }, pinCacheDir: dir }),
|
|
162
|
+
).rejects.toThrow(/extracts to/);
|
|
163
|
+
} finally {
|
|
164
|
+
rmSync(dir, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
package/src/spec/fetch.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
|
4
4
|
import { execFile } from "child_process";
|
|
5
5
|
import { promisify } from "util";
|
|
6
6
|
import { fetchWithCache, extractFromZip, clearCacheFile } from "@intentius/chant/codegen/fetch";
|
|
7
|
+
import { RELEASE_GATE_ENV } from "@intentius/chant/codegen/validate";
|
|
7
8
|
import { ACCEPT_ENV, AWS_SPEC_PIN, specContentDigest, type SpecPin } from "./pin";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -203,12 +204,32 @@ async function uploadPinAsset(zipData: Buffer, digest: string): Promise<void> {
|
|
|
203
204
|
* `force`, the accept flow (`CHANT_ACCEPT_AWS_SPEC=1`, which must sample
|
|
204
205
|
* upstream — and bypasses the 24h cache for the same reason), and when the
|
|
205
206
|
* asset is unreachable. The live path keeps its 24h local cache.
|
|
207
|
+
*
|
|
208
|
+
* chant #1481 — the fallback is what made two workflows disagree about one
|
|
209
|
+
* commit. The live archive is served from CloudFront and two fetches seconds
|
|
210
|
+
* apart can get different variants; under the release gate, building from
|
|
211
|
+
* it would ship a surface nobody reviewed on a retry lottery, so the gate
|
|
212
|
+
* refuses instead. Anywhere else the fallback stays, but says so, because a
|
|
213
|
+
* silent fallback is indistinguishable from the pinned path in a CI log.
|
|
206
214
|
*/
|
|
207
|
-
export async function fetchSchemaZip(
|
|
208
|
-
|
|
215
|
+
export async function fetchSchemaZip(
|
|
216
|
+
force = false,
|
|
217
|
+
options: { env?: NodeJS.ProcessEnv; download?: PinAssetDownloader; pinCacheDir?: string } = {},
|
|
218
|
+
): Promise<Map<string, Buffer>> {
|
|
219
|
+
const env = options.env ?? process.env;
|
|
220
|
+
const accepting = !!env[ACCEPT_ENV];
|
|
209
221
|
if (!force && !accepting) {
|
|
210
|
-
const pinned = await loadPinnedSchemas();
|
|
222
|
+
const pinned = await loadPinnedSchemas({ download: options.download, cacheDir: options.pinCacheDir });
|
|
211
223
|
if (pinned) return pinned;
|
|
224
|
+
if (env[RELEASE_GATE_ENV] === "1") {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`${RELEASE_GATE_ENV}=1 and the pinned spec asset could not be loaded (${pinAssetUrl()}) — ` +
|
|
227
|
+
`refusing to build a release from the live CloudFormation archive, which is not the content ` +
|
|
228
|
+
`the pin was reviewed against. Check the asset exists on the ${SPEC_PIN_RELEASE_TAG} release ` +
|
|
229
|
+
`(the accept flow uploads it: ${ACCEPT_ENV}=1 npm run generate).`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
console.error(`pinned spec asset unavailable (${pinAssetUrl()}); falling back to the live CloudFormation archive`);
|
|
212
233
|
}
|
|
213
234
|
const zipData = await fetchWithCache(
|
|
214
235
|
{ url: SCHEMA_ZIP_URL, cacheFile: CACHE_FILE },
|