@intentius/chant 0.124.44 → 0.124.46

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.
Files changed (249) hide show
  1. package/dist/capabilities/aws.paths +148 -0
  2. package/dist/capabilities/base-mappings.json +122 -0
  3. package/dist/capabilities/catalog.d.ts +89 -0
  4. package/dist/capabilities/catalog.d.ts.map +1 -0
  5. package/dist/capabilities/catalog.js +204 -0
  6. package/dist/capabilities/language.d.ts +103 -0
  7. package/dist/capabilities/language.d.ts.map +1 -0
  8. package/dist/capabilities/language.js +335 -0
  9. package/dist/capabilities/matcher.d.ts +65 -0
  10. package/dist/capabilities/matcher.d.ts.map +1 -0
  11. package/dist/capabilities/matcher.js +299 -0
  12. package/dist/capabilities/view.d.ts +207 -0
  13. package/dist/capabilities/view.d.ts.map +1 -0
  14. package/dist/capabilities/view.js +484 -0
  15. package/dist/ci-identity.d.ts +183 -12
  16. package/dist/ci-identity.d.ts.map +1 -1
  17. package/dist/ci-identity.js +362 -53
  18. package/dist/cli/commands/lint.d.ts.map +1 -1
  19. package/dist/cli/commands/lint.js +1 -0
  20. package/dist/cli/handlers/build.d.ts.map +1 -1
  21. package/dist/cli/handlers/build.js +17 -1
  22. package/dist/cli/handlers/declared-graph.d.ts +44 -0
  23. package/dist/cli/handlers/declared-graph.d.ts.map +1 -0
  24. package/dist/cli/handlers/declared-graph.js +87 -0
  25. package/dist/cli/handlers/graph.d.ts +25 -0
  26. package/dist/cli/handlers/graph.d.ts.map +1 -1
  27. package/dist/cli/handlers/graph.js +93 -87
  28. package/dist/cli/handlers/pr-env.d.ts +8 -2
  29. package/dist/cli/handlers/pr-env.d.ts.map +1 -1
  30. package/dist/cli/handlers/pr-env.js +127 -225
  31. package/dist/cli/handlers/pr.d.ts +3 -0
  32. package/dist/cli/handlers/pr.d.ts.map +1 -1
  33. package/dist/cli/handlers/pr.js +1 -0
  34. package/dist/cli/handlers/search-count.d.ts +89 -0
  35. package/dist/cli/handlers/search-count.d.ts.map +1 -0
  36. package/dist/cli/handlers/search-count.js +132 -0
  37. package/dist/cli/handlers/search.d.ts +358 -6
  38. package/dist/cli/handlers/search.d.ts.map +1 -1
  39. package/dist/cli/handlers/search.js +1065 -155
  40. package/dist/cli/main.d.ts.map +1 -1
  41. package/dist/cli/main.js +23 -3
  42. package/dist/cli/mcp/server.d.ts.map +1 -1
  43. package/dist/cli/mcp/server.js +3 -0
  44. package/dist/cli/mcp/tools/capabilities.d.ts +7 -0
  45. package/dist/cli/mcp/tools/capabilities.d.ts.map +1 -0
  46. package/dist/cli/mcp/tools/capabilities.js +27 -0
  47. package/dist/cli/mcp/workspace-tools.d.ts +3 -1
  48. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  49. package/dist/cli/mcp/workspace-tools.js +47 -2
  50. package/dist/cli/parse-args.d.ts.map +1 -1
  51. package/dist/cli/parse-args.js +64 -17
  52. package/dist/cli/registry.d.ts +22 -0
  53. package/dist/cli/registry.d.ts.map +1 -1
  54. package/dist/components/cli-support.d.ts +2 -0
  55. package/dist/components/cli-support.d.ts.map +1 -1
  56. package/dist/components/cli-support.js +4 -2
  57. package/dist/components/fan-out-support.d.ts +3 -0
  58. package/dist/components/fan-out-support.d.ts.map +1 -1
  59. package/dist/components/fan-out-support.js +1 -1
  60. package/dist/components/pr-env-ports.d.ts +29 -0
  61. package/dist/components/pr-env-ports.d.ts.map +1 -0
  62. package/dist/components/pr-env-ports.js +77 -0
  63. package/dist/components/pr-env-run.d.ts +254 -0
  64. package/dist/components/pr-env-run.d.ts.map +1 -0
  65. package/dist/components/pr-env-run.js +376 -0
  66. package/dist/components/pr-environment.d.ts +91 -15
  67. package/dist/components/pr-environment.d.ts.map +1 -1
  68. package/dist/components/pr-environment.js +95 -9
  69. package/dist/components/pr-pipeline.d.ts +10 -1
  70. package/dist/components/pr-pipeline.d.ts.map +1 -1
  71. package/dist/config-environments.d.ts +92 -0
  72. package/dist/config-environments.d.ts.map +1 -0
  73. package/dist/config-environments.js +102 -0
  74. package/dist/config.d.ts +24 -84
  75. package/dist/config.d.ts.map +1 -1
  76. package/dist/config.js +9 -96
  77. package/dist/graph-detail.d.ts.map +1 -1
  78. package/dist/graph-detail.js +15 -3
  79. package/dist/graph-effective.d.ts +33 -2
  80. package/dist/graph-effective.d.ts.map +1 -1
  81. package/dist/graph-effective.js +215 -68
  82. package/dist/graph-ir.d.ts +23 -0
  83. package/dist/graph-ir.d.ts.map +1 -1
  84. package/dist/graph-ir.js +130 -0
  85. package/dist/graph-sub-template.d.ts +11 -0
  86. package/dist/graph-sub-template.d.ts.map +1 -0
  87. package/dist/graph-sub-template.js +19 -0
  88. package/dist/lexicon.d.ts +9 -0
  89. package/dist/lexicon.d.ts.map +1 -1
  90. package/dist/lifecycle/observe.d.ts +28 -0
  91. package/dist/lifecycle/observe.d.ts.map +1 -1
  92. package/dist/lifecycle/observe.js +23 -1
  93. package/dist/lint/rule.d.ts +6 -0
  94. package/dist/lint/rule.d.ts.map +1 -1
  95. package/dist/live-endpoint.d.ts +1 -1
  96. package/dist/live-endpoint.d.ts.map +1 -1
  97. package/dist/live-endpoint.js +1 -1
  98. package/dist/observation.d.ts +48 -1
  99. package/dist/observation.d.ts.map +1 -1
  100. package/dist/observation.js +21 -5
  101. package/dist/op/activities/activity-contracts.d.ts +4 -4
  102. package/dist/op/op-waves.d.ts +17 -0
  103. package/dist/op/op-waves.d.ts.map +1 -1
  104. package/dist/op/op-waves.js +23 -1
  105. package/dist/pr-forge.d.ts +15 -1
  106. package/dist/pr-forge.d.ts.map +1 -1
  107. package/dist/pr-forge.js +28 -16
  108. package/dist/pr-requires.d.ts +5 -1
  109. package/dist/pr-requires.d.ts.map +1 -1
  110. package/dist/wave-runtime.mjs +251 -250
  111. package/dist/workspace/capabilities-contract.d.ts +66 -0
  112. package/dist/workspace/capabilities-contract.d.ts.map +1 -0
  113. package/dist/workspace/capabilities-contract.js +107 -0
  114. package/dist/workspace/capabilities.schema.json +229 -0
  115. package/dist/workspace/ci-cloud-identity.d.ts +3 -1
  116. package/dist/workspace/ci-cloud-identity.d.ts.map +1 -1
  117. package/dist/workspace/ci-cloud-identity.js +16 -1
  118. package/dist/workspace/ci-run.d.ts +35 -8
  119. package/dist/workspace/ci-run.d.ts.map +1 -1
  120. package/dist/workspace/ci-run.js +95 -21
  121. package/dist/workspace/ci-secrets.d.ts.map +1 -1
  122. package/dist/workspace/ci-secrets.js +4 -3
  123. package/dist/workspace/compose-graph.d.ts +9 -0
  124. package/dist/workspace/compose-graph.d.ts.map +1 -1
  125. package/dist/workspace/compose-graph.js +3 -1
  126. package/dist/workspace/conformance/index.d.ts +1 -1
  127. package/dist/workspace/conformance/index.d.ts.map +1 -1
  128. package/dist/workspace/conformance/index.js +39 -2
  129. package/dist/workspace/declaration.d.ts +10 -0
  130. package/dist/workspace/declaration.d.ts.map +1 -1
  131. package/dist/workspace/declaration.js +17 -1
  132. package/dist/workspace/declaration.schema.json +36 -1
  133. package/dist/workspace/graph-cache.d.ts +8 -0
  134. package/dist/workspace/graph-cache.d.ts.map +1 -1
  135. package/dist/workspace/graph-cache.js +27 -3
  136. package/dist/workspace/graph-cli.d.ts +39 -2
  137. package/dist/workspace/graph-cli.d.ts.map +1 -1
  138. package/dist/workspace/graph-cli.js +66 -24
  139. package/dist/workspace/graph.schema.json +148 -0
  140. package/dist/workspace/kind-readers.d.ts +3 -0
  141. package/dist/workspace/kind-readers.d.ts.map +1 -1
  142. package/dist/workspace/kind-readers.js +7 -1
  143. package/dist/workspace/member-commands.d.ts +10 -1
  144. package/dist/workspace/member-commands.d.ts.map +1 -1
  145. package/dist/workspace/member-commands.js +17 -1
  146. package/dist/workspace/nested-graph.d.ts +2 -0
  147. package/dist/workspace/nested-graph.d.ts.map +1 -1
  148. package/dist/workspace/nested-graph.js +32 -1
  149. package/dist/workspace/reason-codes.d.ts +10 -0
  150. package/dist/workspace/reason-codes.d.ts.map +1 -1
  151. package/dist/workspace/reason-codes.js +12 -0
  152. package/dist/workspace/search-cli.d.ts +204 -0
  153. package/dist/workspace/search-cli.d.ts.map +1 -0
  154. package/dist/workspace/search-cli.js +466 -0
  155. package/dist/workspace/search-contract.d.ts +56 -0
  156. package/dist/workspace/search-contract.d.ts.map +1 -0
  157. package/dist/workspace/search-contract.js +70 -0
  158. package/dist/workspace/search.schema.json +822 -0
  159. package/package.json +1 -1
  160. package/src/__snapshots__/okf.test.ts.snap +10 -0
  161. package/src/capabilities/aws.paths +148 -0
  162. package/src/capabilities/base-mappings.json +122 -0
  163. package/src/capabilities/catalog.ts +230 -0
  164. package/src/capabilities/language.test.ts +191 -0
  165. package/src/capabilities/language.ts +373 -0
  166. package/src/capabilities/matcher.ts +315 -0
  167. package/src/capabilities/view-compose.test.ts +76 -0
  168. package/src/capabilities/view.ts +631 -0
  169. package/src/ci-identity-targets.test.ts +275 -0
  170. package/src/ci-identity.ts +394 -61
  171. package/src/cli/commands/lint.ts +1 -0
  172. package/src/cli/handlers/build.ts +15 -1
  173. package/src/cli/handlers/declared-graph.ts +99 -0
  174. package/src/cli/handlers/graph.test.ts +94 -3
  175. package/src/cli/handlers/graph.ts +123 -96
  176. package/src/cli/handlers/pr-env.ts +135 -227
  177. package/src/cli/handlers/pr.ts +4 -0
  178. package/src/cli/handlers/search-build-roots.test.ts +144 -0
  179. package/src/cli/handlers/search-count.test.ts +191 -0
  180. package/src/cli/handlers/search-count.ts +165 -0
  181. package/src/cli/handlers/search-json.test.ts +333 -0
  182. package/src/cli/handlers/search-live.test.ts +387 -0
  183. package/src/cli/handlers/search-paths.test.ts +235 -0
  184. package/src/cli/handlers/search.ts +1312 -166
  185. package/src/cli/main.test.ts +28 -3
  186. package/src/cli/main.ts +23 -3
  187. package/src/cli/mcp/server.test.ts +3 -3
  188. package/src/cli/mcp/server.ts +3 -0
  189. package/src/cli/mcp/tools/capabilities.ts +32 -0
  190. package/src/cli/mcp/workspace-tools.e2e.test.ts +21 -1
  191. package/src/cli/mcp/workspace-tools.ts +47 -2
  192. package/src/cli/parse-args.ts +52 -13
  193. package/src/cli/registry.ts +22 -0
  194. package/src/components/cli-support.ts +6 -1
  195. package/src/components/fan-out-support.ts +4 -1
  196. package/src/components/pr-env-ports.ts +89 -0
  197. package/src/components/pr-env-run-bundle.test.ts +54 -0
  198. package/src/components/pr-env-run.test.ts +290 -0
  199. package/src/components/pr-env-run.ts +563 -0
  200. package/src/components/pr-environment.ts +161 -13
  201. package/src/components/pr-pipeline.ts +10 -1
  202. package/src/config-environments.ts +133 -0
  203. package/src/config.ts +37 -124
  204. package/src/graph-detail.ts +15 -3
  205. package/src/graph-effective.ts +226 -75
  206. package/src/graph-ir.test.ts +163 -0
  207. package/src/graph-ir.ts +147 -0
  208. package/src/graph-sub-template.ts +22 -0
  209. package/src/lexicon.ts +10 -0
  210. package/src/lifecycle/observe.test.ts +27 -1
  211. package/src/lifecycle/observe.ts +43 -1
  212. package/src/lint/rule.ts +6 -0
  213. package/src/live-endpoint.ts +1 -1
  214. package/src/observation.test.ts +25 -0
  215. package/src/observation.ts +69 -5
  216. package/src/op/op-waves.ts +38 -1
  217. package/src/pr-forge-bundle.test.ts +1 -1
  218. package/src/pr-forge.ts +44 -17
  219. package/src/pr-requires.test.ts +43 -6
  220. package/src/pr-requires.ts +5 -1
  221. package/src/workspace/capabilities-contract.test.ts +88 -0
  222. package/src/workspace/capabilities-contract.ts +132 -0
  223. package/src/workspace/capabilities.schema.json +229 -0
  224. package/src/workspace/ci-cloud-identity.test.ts +20 -0
  225. package/src/workspace/ci-cloud-identity.ts +14 -1
  226. package/src/workspace/ci-run.test.ts +74 -1
  227. package/src/workspace/ci-run.ts +122 -29
  228. package/src/workspace/ci-secrets.test.ts +1 -1
  229. package/src/workspace/ci-secrets.ts +4 -3
  230. package/src/workspace/compose-graph.ts +10 -1
  231. package/src/workspace/conformance/conformance.e2e.test.ts +14 -1
  232. package/src/workspace/conformance/index.ts +31 -2
  233. package/src/workspace/declaration.schema.json +36 -1
  234. package/src/workspace/declaration.test.ts +7 -0
  235. package/src/workspace/declaration.ts +24 -2
  236. package/src/workspace/graph-cache.ts +27 -3
  237. package/src/workspace/graph-cli.ts +96 -24
  238. package/src/workspace/graph.schema.json +148 -0
  239. package/src/workspace/kind-readers.e2e.test.ts +28 -0
  240. package/src/workspace/kind-readers.test.ts +1 -1
  241. package/src/workspace/kind-readers.ts +7 -1
  242. package/src/workspace/member-commands.ts +22 -1
  243. package/src/workspace/nested-graph.ts +32 -1
  244. package/src/workspace/reason-codes.test.ts +7 -0
  245. package/src/workspace/reason-codes.ts +12 -0
  246. package/src/workspace/search-cli.e2e.test.ts +305 -0
  247. package/src/workspace/search-cli.ts +591 -0
  248. package/src/workspace/search-contract.ts +95 -0
  249. package/src/workspace/search.schema.json +822 -0
@@ -0,0 +1,275 @@
1
+ /**
2
+ * #4332: per-target cloud identities. A rule with `targets` covers only the
3
+ * targets inside a member those globs match. The plan trade holds every
4
+ * plan identity a member's targets take, the trade leaves each identity's
5
+ * credentials in the CHANT_CLOUD_TARGETS file, targetCloudEnv hands a
6
+ * target its own, and cloudIdentityGroups splits an apply into one job per
7
+ * apply identity. The trade tests run the generated `node -e` line against
8
+ * the local emulator (./__fixtures__/oidc-emulator.ts). Test values only.
9
+ */
10
+
11
+ import { execFile } from "node:child_process";
12
+ import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+ import { promisify } from "node:util";
16
+ import { build } from "esbuild";
17
+ // @ts-ignore: picomatch has no types declaration
18
+ import picomatch from "picomatch";
19
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
20
+ import {
21
+ CLOUD_TARGETS_ENV,
22
+ cloudIdentityGroups,
23
+ cloudIdentityProblems,
24
+ cloudIdentityWave,
25
+ nameGlobMatch,
26
+ oneApplyIdentityProblem,
27
+ readTargetsFile,
28
+ resolveCloudIdentity,
29
+ resolveTargetCloudIdentity,
30
+ targetCloudEnv,
31
+ targetGlobMatch,
32
+ TRADE_SPEC_ENV,
33
+ tradeIdentities,
34
+ tradeLine,
35
+ type CloudIdentityDeclaration,
36
+ type CloudTradeSpec,
37
+ } from "./ci-identity";
38
+ import { assertOpWavesSpec, type OpWavesSpec } from "./op/op-waves";
39
+ import { emulatedCredentials, EMULATED_JWT_PREFIX, startOidcEmulator, type OidcEmulator } from "./__fixtures__/oidc-emulator";
40
+ import { REPO } from "./workspace/__fixtures__/contract-repo";
41
+
42
+ const run = promisify(execFile);
43
+
44
+ const role = (n: string) => `arn:aws:iam::111111111111:role/${n}`;
45
+
46
+ /** terragucci's shape: prod and staging roots plan and apply as their environment's roles, the rest as the member's own. */
47
+ const DECL: CloudIdentityDeclaration = {
48
+ aws: {
49
+ region: "eu-west-1",
50
+ rules: [
51
+ { environments: ["prod"], members: ["infra"], targets: ["envs/prod/**"], plan: role("prod-plan"), apply: role("prod-apply") },
52
+ { environments: ["prod"], members: ["infra"], targets: ["envs/staging/**"], name: "staging", plan: role("staging-plan"), apply: role("staging-apply") },
53
+ { environments: ["prod"], plan: role("own-plan"), apply: role("own-apply") },
54
+ { targets: ["unreachable/**"], plan: role("late-plan"), apply: role("late-apply") },
55
+ ],
56
+ },
57
+ gcp: {
58
+ provider: "projects/123/locations/global/workloadIdentityPools/ci/providers/forge",
59
+ rules: [{ targets: ["envs/prod/**"], plan: "prod-plan@p.iam.gserviceaccount.com", apply: "prod-apply@p.iam.gserviceaccount.com" }],
60
+ },
61
+ };
62
+
63
+ const ROOTS = ["envs/prod/net", "envs/staging/net", "shared/dns", "envs/prod/app"];
64
+
65
+ describe("which identity a target takes (#4332)", () => {
66
+ test("the first rule that covers the environment, the member and the target wins, per cloud", () => {
67
+ const prod = resolveTargetCloudIdentity(DECL, "prod", "infra", "envs/prod/net")!;
68
+ expect(prod.plan.aws).toEqual({ role: role("prod-plan"), region: "eu-west-1", audience: "sts.amazonaws.com" });
69
+ expect(prod.apply.gcp!.serviceAccount).toBe("prod-apply@p.iam.gserviceaccount.com");
70
+ const staging = resolveTargetCloudIdentity(DECL, "prod", "infra", "envs/staging/net")!;
71
+ expect(staging.apply.aws!.role).toBe(role("staging-apply"));
72
+ expect(staging.apply.gcp).toBeUndefined();
73
+ expect(resolveTargetCloudIdentity(DECL, "prod", "infra", "shared/dns")!.plan.aws!.role).toBe(role("own-plan"));
74
+ });
75
+
76
+ test("a rule with targets covers no target of another member, and a rule after one without targets is never reached", () => {
77
+ expect(resolveTargetCloudIdentity(DECL, "prod", "web", "envs/prod/net")!.plan.aws!.role).toBe(role("own-plan"));
78
+ expect(resolveTargetCloudIdentity(DECL, "prod", "infra", "unreachable/x")!.plan.aws!.role).toBe(role("own-plan"));
79
+ expect(resolveTargetCloudIdentity(DECL, "dev", "infra", "unreachable/x")!.plan.aws!.role).toBe(role("late-plan"));
80
+ expect(resolveTargetCloudIdentity({ aws: DECL.aws }, "dev", "infra", "shared/dns")).toBeUndefined();
81
+ });
82
+
83
+ test("the pipeline's trade names its own identity and, before it, the targets that take another", () => {
84
+ const p = resolveCloudIdentity(DECL, "prod", "infra")!;
85
+ expect(p.plan.aws).toEqual({
86
+ role: role("own-plan"),
87
+ targets: [
88
+ { targets: ["envs/prod/**"], role: role("prod-plan") },
89
+ { targets: ["envs/staging/**"], role: role("staging-plan") },
90
+ ],
91
+ region: "eu-west-1",
92
+ audience: "sts.amazonaws.com",
93
+ });
94
+ // GCP has no rule for the member as a whole: targets alone.
95
+ expect(p.plan.gcp).toMatchObject({ targets: [{ targets: ["envs/prod/**"], serviceAccount: "prod-plan@p.iam.gserviceaccount.com" }] });
96
+ expect(p.plan.gcp!.serviceAccount).toBeUndefined();
97
+ expect(tradeIdentities(p.plan)).toEqual([role("own-plan"), role("prod-plan"), role("staging-plan"), "prod-plan@p.iam.gserviceaccount.com"]);
98
+ // The plan trade names no apply identity.
99
+ for (const id of tradeIdentities(p.apply)) expect(JSON.stringify(p.plan)).not.toContain(id);
100
+ // Without targets rules, as #4296 resolved it.
101
+ expect(resolveCloudIdentity(DECL, "prod", "web")!.plan.aws).toEqual({ role: role("own-plan"), region: "eu-west-1", audience: "sts.amazonaws.com" });
102
+ });
103
+
104
+ test("a plan identity that any rule applies as is still refused, targets or not", () => {
105
+ expect(cloudIdentityProblems(DECL)).toEqual([]);
106
+ const crossed = cloudIdentityProblems({ aws: { rules: [{ targets: ["a/**"], plan: "x", apply: "y" }, { plan: "y", apply: "z" }] } });
107
+ expect(crossed).toEqual([{ pointer: "/ci/identity/cloud/aws/rules/1", message: expect.stringContaining("plans as y, which another rule applies as") }]);
108
+ });
109
+ });
110
+
111
+ describe("one apply job per identity (#4332)", () => {
112
+ test("targets that take the same identities in every cloud are one group, named from the rule, default last", () => {
113
+ const groups = cloudIdentityGroups(DECL, "prod", "infra", ROOTS);
114
+ expect(groups.map((g) => [g.name, g.targets])).toEqual([
115
+ ["envs-prod", ["envs/prod/net", "envs/prod/app"]],
116
+ ["staging", ["envs/staging/net"]],
117
+ ["default", ["shared/dns"]],
118
+ ]);
119
+ expect(tradeIdentities(groups[0]!.identity!.apply)).toEqual([role("prod-apply"), "prod-apply@p.iam.gserviceaccount.com"]);
120
+ expect(tradeIdentities(groups[2]!.identity!.apply)).toEqual([role("own-apply")]);
121
+ });
122
+
123
+ test("two rules whose names collide get -2, and targets no rule covers form a group with no identity", () => {
124
+ const decl: CloudIdentityDeclaration = { aws: { rules: [{ targets: ["a/*"], name: "x", plan: "p1", apply: "a1" }, { targets: ["b/*"], name: "x", plan: "p2", apply: "a2" }] } };
125
+ expect(cloudIdentityGroups(decl, "prod", undefined, ["a/1", "b/1", "c/1"])).toEqual([
126
+ expect.objectContaining({ name: "x", targets: ["a/1"] }),
127
+ expect.objectContaining({ name: "x-2", targets: ["b/1"] }),
128
+ { name: "default", targets: ["c/1"] },
129
+ ]);
130
+ });
131
+
132
+ test("an Op wave's deciding job trades every plan identity and each group's job its apply identities alone", () => {
133
+ const wave = cloudIdentityWave(DECL, "prod", "infra", ROOTS);
134
+ expect(wave.cloud).toEqual(resolveCloudIdentity(DECL, "prod", "infra")!.plan);
135
+ expect(wave.jobs!.map((j) => [j.name, j.runs, tradeIdentities(j.cloud!)])).toEqual([
136
+ ["envs-prod", ["envs/prod/net", "envs/prod/app"], [role("prod-apply"), "prod-apply@p.iam.gserviceaccount.com"]],
137
+ ["staging", ["envs/staging/net"], [role("staging-apply")]],
138
+ ["default", ["shared/dns"], [role("own-apply")]],
139
+ ]);
140
+ const spec: OpWavesSpec = { name: "tf-apply", op: "tf-apply", plan: ["plan", "{target}", "{plan}"], apply: ["apply", "{target}"], waves: [{ name: "wave-1", runs: ROOTS.map((target) => ({ target })), ...wave }] } as OpWavesSpec;
141
+ expect(() => assertOpWavesSpec(spec)).not.toThrow();
142
+ });
143
+
144
+ test("with one group the wave's one job trades its apply identities and has no jobs", () => {
145
+ expect(cloudIdentityWave(DECL, "prod", "infra", ["envs/prod/net"])).toEqual({ cloud: resolveTargetCloudIdentity(DECL, "prod", "infra", "envs/prod/net")!.apply });
146
+ expect(cloudIdentityWave(undefined, "prod", "infra", ["x"])).toEqual({});
147
+ });
148
+
149
+ test("an Op wave's cloud that names no identity is refused", () => {
150
+ const spec = { name: "w", op: "w", plan: ["p", "{plan}"], apply: ["a"], waves: [{ name: "one", runs: [{ target: "t" }], cloud: { aws: { region: "eu-west-1", audience: "sts.amazonaws.com" } } }] } as unknown as OpWavesSpec;
151
+ expect(() => assertOpWavesSpec(spec)).toThrow(/cloud\.aws must give an audience and a role/);
152
+ expect(() => assertOpWavesSpec({ ...spec, waves: [{ ...spec.waves[0]!, cloud: { k8s: {} } as never }] })).toThrow(/k8s is not a cloud/);
153
+ });
154
+
155
+ test("one job may hold one apply identity per cloud", () => {
156
+ expect(oneApplyIdentityProblem(resolveCloudIdentity(DECL, "prod", "web")!.apply)).toBeUndefined();
157
+ expect(oneApplyIdentityProblem(resolveCloudIdentity(DECL, "prod", "infra")!.apply)).toMatch(/3 aws apply identities/);
158
+ });
159
+ });
160
+
161
+ describe("the glob grammars (#4332)", () => {
162
+ test("member and environment globs read as picomatch reads the ciMemberGlobs grammar", () => {
163
+ const names = ["api", "web", "prod", "prod-eu", "staging-eu", "staging", "a1", "b2", "app-x"];
164
+ const globs = ["api", "prod-*", "staging-*", "*", "a?", "[ab][0-9]", "{api,web}", "app-{x,y}", "!prod*", "{a,b}{1,2}", "*-eu"];
165
+ for (const g of globs) for (const n of names) expect([g, n, nameGlobMatch(g, n)]).toEqual([g, n, (picomatch(g) as (s: string) => boolean)(n)]);
166
+ expect(nameGlobMatch("[!a]*", "web")).toBe(true);
167
+ expect(nameGlobMatch("[!a]*", "api")).toBe(false);
168
+ });
169
+
170
+ test("target globs read as the terraform lexicon's unit globs", () => {
171
+ expect(targetGlobMatch("envs/prod/**", "envs/prod/net")).toBe(true);
172
+ expect(targetGlobMatch("envs/prod/**", "envs/prod")).toBe(false);
173
+ expect(targetGlobMatch("envs/*/net", "envs/prod/net")).toBe(true);
174
+ expect(targetGlobMatch("envs/*", "envs/prod/net")).toBe(false);
175
+ expect(targetGlobMatch("a/**/b", "a/b")).toBe(true);
176
+ expect(targetGlobMatch("a/?", "a/b")).toBe(true);
177
+ expect(targetGlobMatch("envs/{a,b}", "envs/a")).toBe(false);
178
+ });
179
+ });
180
+
181
+ const dirs: string[] = [];
182
+ let emu: OidcEmulator;
183
+ beforeAll(async () => {
184
+ emu = await startOidcEmulator();
185
+ });
186
+ afterAll(async () => {
187
+ await emu.close();
188
+ for (const d of dirs) rmSync(d, { recursive: true, force: true });
189
+ });
190
+
191
+ const BASE = { PATH: process.env.PATH ?? "/usr/bin:/bin", HOME: process.env.HOME ?? "/tmp" };
192
+
193
+ /** Run the trade line as a GitHub step does, with RUNNER_TEMP its own directory. */
194
+ async function trade(spec: CloudTradeSpec): Promise<{ out: string; stderr: string; stdout: string; temp: string }> {
195
+ const temp = mkdtempSync(join(tmpdir(), "chant-trade-targets-"));
196
+ dirs.push(temp);
197
+ const file = join(temp, "github-env");
198
+ writeFileSync(file, "");
199
+ const r = await run("sh", ["-c", tradeLine(file, "github-env")], { env: { ...BASE, ...emu.requestEnv, RUNNER_TEMP: temp, [TRADE_SPEC_ENV]: JSON.stringify(emu.pointAt(spec)) } });
200
+ return { out: readFileSync(file, "utf-8"), stderr: r.stderr, stdout: r.stdout, temp };
201
+ }
202
+
203
+ const envOf = (lines: string): Record<string, string> => Object.fromEntries(lines.split("\n").filter(Boolean).map((l) => [l.slice(0, l.indexOf("=")), l.slice(l.indexOf("=") + 1)]));
204
+
205
+ describe("the plan trade with targets, run against the emulator (#4332)", () => {
206
+ test("it trades one token per cloud for every plan identity, keeps the job's own in GITHUB_ENV and each in the targets file", async () => {
207
+ const before = { minted: emu.log.minted.length, assumed: emu.log.assumed.length, impersonated: emu.log.impersonated.length };
208
+ const plan = resolveCloudIdentity(DECL, "prod", "infra")!.plan;
209
+ const r = await trade(plan);
210
+ expect(r.stderr).not.toContain("failed");
211
+ // One minted token per cloud, however many identities it is traded for.
212
+ expect(emu.log.minted.slice(before.minted)).toEqual(["sts.amazonaws.com", "https://iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/ci/providers/forge"]);
213
+ expect(emu.log.assumed.slice(before.assumed).map((a) => a.role)).toEqual([role("own-plan"), role("prod-plan"), role("staging-plan")]);
214
+ expect(emu.log.impersonated.slice(before.impersonated)).toEqual(["prod-plan@p.iam.gserviceaccount.com"]);
215
+ const env = envOf(r.out);
216
+ expect(env.AWS_ACCESS_KEY_ID).toBe(emulatedCredentials(role("own-plan")).accessKeyId);
217
+ // GCP names no identity of the job's own, so the job's environment has none.
218
+ expect(env.GOOGLE_OAUTH_ACCESS_TOKEN).toBeUndefined();
219
+ const path = env[CLOUD_TARGETS_ENV]!;
220
+ expect(path.startsWith(r.temp)).toBe(true);
221
+ expect(statSync(path).mode & 0o777).toBe(0o600);
222
+ const file = readTargetsFile(path);
223
+ expect(file.clouds.aws!.default).toBe(role("own-plan"));
224
+ expect(Object.keys(file.clouds.aws!.vars)).toEqual([role("own-plan"), role("prod-plan"), role("staging-plan")]);
225
+ // Every secret is masked, and the minted token is written nowhere.
226
+ expect(r.stdout).toContain(`::add-mask::${emulatedCredentials(role("staging-plan")).token}`);
227
+ expect(r.out + readFileSync(path, "utf-8") + r.stdout + r.stderr).not.toContain(EMULATED_JWT_PREFIX);
228
+ // No apply identity was asked for.
229
+ for (const id of tradeIdentities(resolveCloudIdentity(DECL, "prod", "infra")!.apply)) expect(emu.log.assumed.map((a) => a.role)).not.toContain(id);
230
+
231
+ // targetCloudEnv hands each target its own identity, as terragucci's rootRoleEnv did.
232
+ const job = { ...env, PATH: "/bin", AWS_ROLE_ARN: undefined };
233
+ const prod = targetCloudEnv(job, "envs/prod/net");
234
+ expect(prod.AWS_ACCESS_KEY_ID).toBe(emulatedCredentials(role("prod-plan")).accessKeyId);
235
+ expect(prod.AWS_SESSION_TOKEN).toBe(emulatedCredentials(role("prod-plan")).token);
236
+ expect(prod.AWS_REGION).toBe("eu-west-1");
237
+ expect(prod.GOOGLE_OAUTH_ACCESS_TOKEN).toBe(`ya29.${emulatedCredentials("prod-plan@p.iam.gserviceaccount.com").token}`);
238
+ expect(prod.PATH).toBe("/bin");
239
+ expect(targetCloudEnv(job, "envs/staging/net").AWS_ACCESS_KEY_ID).toBe(emulatedCredentials(role("staging-plan")).accessKeyId);
240
+ const shared = targetCloudEnv(job, "shared/dns");
241
+ expect(shared.AWS_ACCESS_KEY_ID).toBe(emulatedCredentials(role("own-plan")).accessKeyId);
242
+ expect(shared.GOOGLE_OAUTH_ACCESS_TOKEN).toBeUndefined();
243
+ });
244
+
245
+ test("an apply group's trade names one identity per cloud and leaves no targets file", async () => {
246
+ const before = emu.log.assumed.length;
247
+ const group = cloudIdentityGroups(DECL, "prod", "infra", ROOTS)[1]!;
248
+ const r = await trade(group.identity!.apply);
249
+ expect(emu.log.assumed.slice(before).map((a) => a.role)).toEqual([role("staging-apply")]);
250
+ expect(envOf(r.out)[CLOUD_TARGETS_ENV]).toBeUndefined();
251
+ expect(targetCloudEnv(envOf(r.out), "envs/staging/net")).toEqual(envOf(r.out));
252
+ });
253
+
254
+ test("a targets file that is not one is refused by name", () => {
255
+ expect(() => targetCloudEnv({ [CLOUD_TARGETS_ENV]: "/nowhere.json" }, "x", () => "{}")).toThrow(/not a version 1 targets file/);
256
+ expect(() => targetCloudEnv({ [CLOUD_TARGETS_ENV]: "/nowhere.json" }, "x", () => "nope")).toThrow(/cannot be read/);
257
+ });
258
+ });
259
+
260
+ describe("the ci-identity bundle (#4332)", () => {
261
+ test("holds ci-identity.ts alone, with no glob library, and stays small", async () => {
262
+ const result = await build({
263
+ entryPoints: [join(REPO, "packages", "core", "src", "ci-identity.ts")],
264
+ bundle: true,
265
+ platform: "node",
266
+ format: "esm",
267
+ write: false,
268
+ metafile: true,
269
+ outdir: "out",
270
+ logLevel: "silent",
271
+ });
272
+ expect(Object.keys(result.metafile.inputs).map((i) => i.replace(/^.*?packages\//, "packages/"))).toEqual(["packages/core/src/ci-identity.ts"]);
273
+ expect(result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0)).toBeLessThan(24 * 1024);
274
+ });
275
+ });