@intentius/chant 0.124.39 → 0.124.41

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 (337) hide show
  1. package/dist/__fixtures__/oidc-emulator.d.ts +49 -0
  2. package/dist/__fixtures__/oidc-emulator.d.ts.map +1 -0
  3. package/dist/__fixtures__/oidc-emulator.js +89 -0
  4. package/dist/ci-handover.d.ts +117 -0
  5. package/dist/ci-handover.d.ts.map +1 -0
  6. package/dist/ci-handover.js +217 -0
  7. package/dist/ci-identity.d.ts +190 -0
  8. package/dist/ci-identity.d.ts.map +1 -0
  9. package/dist/ci-identity.js +219 -0
  10. package/dist/cli/component-endpoint.d.ts +20 -0
  11. package/dist/cli/component-endpoint.d.ts.map +1 -0
  12. package/dist/cli/component-endpoint.js +36 -0
  13. package/dist/cli/handlers/approve.d.ts +2 -1
  14. package/dist/cli/handlers/approve.d.ts.map +1 -1
  15. package/dist/cli/handlers/approve.js +20 -6
  16. package/dist/cli/handlers/build.d.ts.map +1 -1
  17. package/dist/cli/handlers/build.js +15 -1
  18. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  19. package/dist/cli/handlers/fan-out.js +26 -4
  20. package/dist/cli/handlers/gate-resume.d.ts +3 -0
  21. package/dist/cli/handlers/gate-resume.d.ts.map +1 -1
  22. package/dist/cli/handlers/gate-resume.js +15 -2
  23. package/dist/cli/handlers/lock.d.ts +8 -0
  24. package/dist/cli/handlers/lock.d.ts.map +1 -0
  25. package/dist/cli/handlers/lock.js +197 -0
  26. package/dist/cli/handlers/pr-env.d.ts +34 -0
  27. package/dist/cli/handlers/pr-env.d.ts.map +1 -0
  28. package/dist/cli/handlers/pr-env.js +436 -0
  29. package/dist/cli/handlers/pr.d.ts +74 -0
  30. package/dist/cli/handlers/pr.d.ts.map +1 -1
  31. package/dist/cli/handlers/pr.js +514 -58
  32. package/dist/cli/handlers/promote.d.ts.map +1 -1
  33. package/dist/cli/handlers/promote.js +16 -2
  34. package/dist/cli/handlers/query.d.ts +6 -0
  35. package/dist/cli/handlers/query.d.ts.map +1 -0
  36. package/dist/cli/handlers/query.js +80 -0
  37. package/dist/cli/handlers/run.d.ts +4 -1
  38. package/dist/cli/handlers/run.d.ts.map +1 -1
  39. package/dist/cli/handlers/run.js +17 -6
  40. package/dist/cli/main.d.ts.map +1 -1
  41. package/dist/cli/main.js +41 -2
  42. package/dist/cli/parse-args.d.ts.map +1 -1
  43. package/dist/cli/parse-args.js +31 -2
  44. package/dist/cli/registry.d.ts +12 -2
  45. package/dist/cli/registry.d.ts.map +1 -1
  46. package/dist/components/auto-release.d.ts +35 -3
  47. package/dist/components/auto-release.d.ts.map +1 -1
  48. package/dist/components/auto-release.js +29 -2
  49. package/dist/components/cli-support.d.ts +2 -0
  50. package/dist/components/cli-support.d.ts.map +1 -1
  51. package/dist/components/cli-support.js +2 -0
  52. package/dist/components/deploy-units.d.ts.map +1 -1
  53. package/dist/components/deploy-units.js +5 -0
  54. package/dist/components/fan-out-run.d.ts +28 -0
  55. package/dist/components/fan-out-run.d.ts.map +1 -1
  56. package/dist/components/fan-out-run.js +27 -3
  57. package/dist/components/pr-environment.d.ts +298 -0
  58. package/dist/components/pr-environment.d.ts.map +1 -0
  59. package/dist/components/pr-environment.js +406 -0
  60. package/dist/components/pr-hooks.d.ts +66 -0
  61. package/dist/components/pr-hooks.d.ts.map +1 -0
  62. package/dist/components/pr-hooks.js +119 -0
  63. package/dist/components/pr-pipeline.d.ts +72 -2
  64. package/dist/components/pr-pipeline.d.ts.map +1 -1
  65. package/dist/components/pr-pipeline.js +96 -5
  66. package/dist/components/pr-policy.d.ts +109 -0
  67. package/dist/components/pr-policy.d.ts.map +1 -0
  68. package/dist/components/pr-policy.js +150 -0
  69. package/dist/components/pr-run.d.ts.map +1 -1
  70. package/dist/components/pr-run.js +5 -3
  71. package/dist/components/promote.d.ts.map +1 -1
  72. package/dist/components/promote.js +39 -16
  73. package/dist/gate-decision.d.ts +15 -2
  74. package/dist/gate-decision.d.ts.map +1 -1
  75. package/dist/gate-decision.js +17 -2
  76. package/dist/gate-policy.d.ts +140 -0
  77. package/dist/gate-policy.d.ts.map +1 -0
  78. package/dist/gate-policy.js +242 -0
  79. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -1
  80. package/dist/lifecycle/__fixtures__/publish-fakes.js +6 -1
  81. package/dist/lifecycle/gate-checkpoint.d.ts +1 -1
  82. package/dist/lifecycle/gate-checkpoint.js +1 -1
  83. package/dist/{workspace/trust → lifecycle}/gate-seal.d.ts +1 -1
  84. package/dist/lifecycle/gate-seal.d.ts.map +1 -0
  85. package/dist/lifecycle/publish-declaration.d.ts.map +1 -1
  86. package/dist/lifecycle/publish-declaration.js +4 -3
  87. package/dist/lifecycle/publish-npm.d.ts +2 -2
  88. package/dist/lifecycle/publish-npm.d.ts.map +1 -1
  89. package/dist/lifecycle/publish-npm.js +31 -8
  90. package/dist/lifecycle/query.d.ts +68 -0
  91. package/dist/lifecycle/query.d.ts.map +1 -0
  92. package/dist/lifecycle/query.js +246 -0
  93. package/dist/lifecycle/release-ledger.d.ts +31 -0
  94. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  95. package/dist/lifecycle/release-ledger.js +19 -1
  96. package/dist/lifecycle/run-ledger.d.ts +13 -0
  97. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  98. package/dist/lifecycle/run-ledger.js +16 -0
  99. package/dist/lifecycle/status.d.ts +5 -1
  100. package/dist/lifecycle/status.d.ts.map +1 -1
  101. package/dist/lifecycle/status.js +10 -5
  102. package/dist/lifecycle/target-lock.d.ts +97 -0
  103. package/dist/lifecycle/target-lock.d.ts.map +1 -0
  104. package/dist/lifecycle/target-lock.js +286 -0
  105. package/dist/live-endpoint.d.ts +18 -0
  106. package/dist/live-endpoint.d.ts.map +1 -1
  107. package/dist/live-endpoint.js +27 -0
  108. package/dist/op/activities/activity-contracts.d.ts +30 -0
  109. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  110. package/dist/op/activities/activity-contracts.js +26 -0
  111. package/dist/op/activities/index.d.ts +2 -0
  112. package/dist/op/activities/index.d.ts.map +1 -1
  113. package/dist/op/activities/index.js +4 -0
  114. package/dist/op/activities/review-agent.d.ts +137 -0
  115. package/dist/op/activities/review-agent.d.ts.map +1 -0
  116. package/dist/op/activities/review-agent.js +266 -0
  117. package/dist/op/builders.d.ts +9 -0
  118. package/dist/op/builders.d.ts.map +1 -1
  119. package/dist/op/builders.js +11 -0
  120. package/dist/op/cron.d.ts +8 -0
  121. package/dist/op/cron.d.ts.map +1 -1
  122. package/dist/op/cron.js +51 -0
  123. package/dist/op/gate-approval.d.ts +20 -13
  124. package/dist/op/gate-approval.d.ts.map +1 -1
  125. package/dist/op/gate-approval.js +13 -18
  126. package/dist/op/gate-quorum.d.ts +131 -0
  127. package/dist/op/gate-quorum.d.ts.map +1 -0
  128. package/dist/op/gate-quorum.js +183 -0
  129. package/dist/op/gate-resume.d.ts +4 -2
  130. package/dist/op/gate-resume.d.ts.map +1 -1
  131. package/dist/op/gate-resume.js +5 -4
  132. package/dist/op/gate-review.js +1 -1
  133. package/dist/op/gate.d.ts +32 -4
  134. package/dist/op/gate.d.ts.map +1 -1
  135. package/dist/op/gate.js +40 -24
  136. package/dist/op/index.d.ts +6 -4
  137. package/dist/op/index.d.ts.map +1 -1
  138. package/dist/op/index.js +4 -3
  139. package/dist/op/local-executor.d.ts.map +1 -1
  140. package/dist/op/local-executor.js +9 -3
  141. package/dist/op/op-waves-run.d.ts +62 -1
  142. package/dist/op/op-waves-run.d.ts.map +1 -1
  143. package/dist/op/op-waves-run.js +190 -11
  144. package/dist/op/op-waves.d.ts +55 -3
  145. package/dist/op/op-waves.d.ts.map +1 -1
  146. package/dist/op/op-waves.js +63 -8
  147. package/dist/op/schedule-overdue.d.ts +57 -0
  148. package/dist/op/schedule-overdue.d.ts.map +1 -0
  149. package/dist/op/schedule-overdue.js +138 -0
  150. package/dist/override-gate.d.ts +2 -2
  151. package/dist/override-gate.d.ts.map +1 -1
  152. package/dist/override-gate.js +1 -1
  153. package/dist/pr-forge.d.ts +36 -0
  154. package/dist/pr-forge.d.ts.map +1 -1
  155. package/dist/pr-forge.js +82 -0
  156. package/dist/pr-loop.d.ts +90 -1
  157. package/dist/pr-loop.d.ts.map +1 -1
  158. package/dist/pr-loop.js +77 -3
  159. package/dist/pr-post.d.ts +72 -0
  160. package/dist/pr-post.d.ts.map +1 -0
  161. package/dist/pr-post.js +134 -0
  162. package/dist/wave-blocked.d.ts +79 -0
  163. package/dist/wave-blocked.d.ts.map +1 -0
  164. package/dist/wave-blocked.js +245 -0
  165. package/dist/wave-engine.d.ts +6 -0
  166. package/dist/wave-engine.d.ts.map +1 -1
  167. package/dist/wave-engine.js +2 -0
  168. package/dist/wave-runtime.mjs +239 -239
  169. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +1 -0
  170. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -1
  171. package/dist/workspace/__fixtures__/two-member-workspace.js +1 -1
  172. package/dist/workspace/ci-branches.d.ts +51 -0
  173. package/dist/workspace/ci-branches.d.ts.map +1 -0
  174. package/dist/workspace/ci-branches.js +69 -0
  175. package/dist/workspace/ci-cloud-identity.d.ts +11 -0
  176. package/dist/workspace/ci-cloud-identity.d.ts.map +1 -0
  177. package/dist/workspace/ci-cloud-identity.js +24 -0
  178. package/dist/workspace/ci-locks.d.ts +12 -0
  179. package/dist/workspace/ci-locks.d.ts.map +1 -0
  180. package/dist/workspace/ci-locks.js +6 -0
  181. package/dist/workspace/ci-run.d.ts +24 -1
  182. package/dist/workspace/ci-run.d.ts.map +1 -1
  183. package/dist/workspace/ci-run.js +85 -6
  184. package/dist/workspace/declaration.d.ts +63 -0
  185. package/dist/workspace/declaration.d.ts.map +1 -1
  186. package/dist/workspace/declaration.js +72 -1
  187. package/dist/workspace/declaration.schema.json +147 -0
  188. package/dist/workspace/identity-gate-rule.d.ts +1 -1
  189. package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
  190. package/dist/workspace/identity-gate-rule.js +1 -1
  191. package/dist/workspace/identity.d.ts +1 -1
  192. package/dist/workspace/identity.d.ts.map +1 -1
  193. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  194. package/dist/workspace/lineage-upgrade.js +8 -5
  195. package/dist/workspace/member-pipeline.d.ts +6 -0
  196. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  197. package/dist/workspace/member-pipeline.js +23 -0
  198. package/dist/workspace/pr-report.schema.json +73 -2
  199. package/dist/workspace/security.d.ts.map +1 -1
  200. package/dist/workspace/security.js +4 -0
  201. package/dist/workspace/status-gates.d.ts +3 -2
  202. package/dist/workspace/status-gates.d.ts.map +1 -1
  203. package/dist/workspace/status-gates.js +8 -7
  204. package/dist/workspace/status.d.ts +3 -1
  205. package/dist/workspace/status.d.ts.map +1 -1
  206. package/dist/workspace/status.js +7 -3
  207. package/dist/workspace/status.schema.json +1 -0
  208. package/dist/workspace/trust/seal.d.ts +2 -2
  209. package/dist/workspace/trust/seal.d.ts.map +1 -1
  210. package/dist/workspace/trust/seal.js +2 -2
  211. package/package.json +1 -1
  212. package/src/__fixtures__/oidc-emulator.ts +118 -0
  213. package/src/ci-handover.test.ts +209 -0
  214. package/src/ci-handover.ts +237 -0
  215. package/src/ci-identity.test.ts +201 -0
  216. package/src/ci-identity.ts +310 -0
  217. package/src/cli/component-endpoint.ts +43 -0
  218. package/src/cli/handlers/approve.ts +18 -5
  219. package/src/cli/handlers/build.ts +15 -1
  220. package/src/cli/handlers/fan-out.test.ts +10 -0
  221. package/src/cli/handlers/fan-out.ts +29 -4
  222. package/src/cli/handlers/gate-resume.ts +16 -2
  223. package/src/cli/handlers/lock.test.ts +98 -0
  224. package/src/cli/handlers/lock.ts +202 -0
  225. package/src/cli/handlers/operator.test.ts +3 -1
  226. package/src/cli/handlers/pr-endpoint.test.ts +169 -0
  227. package/src/cli/handlers/pr-env.ts +462 -0
  228. package/src/cli/handlers/pr-waves-blocked.test.ts +247 -0
  229. package/src/cli/handlers/pr.test.ts +184 -0
  230. package/src/cli/handlers/pr.ts +508 -64
  231. package/src/cli/handlers/promote.ts +25 -2
  232. package/src/cli/handlers/query.ts +78 -0
  233. package/src/cli/handlers/run.test.ts +2 -1
  234. package/src/cli/handlers/run.ts +16 -5
  235. package/src/cli/main.ts +41 -2
  236. package/src/cli/parse-args.ts +23 -2
  237. package/src/cli/registry.ts +12 -2
  238. package/src/components/auto-release-infra.test.ts +131 -0
  239. package/src/components/auto-release.test.ts +40 -1
  240. package/src/components/auto-release.ts +50 -4
  241. package/src/components/cli-support.ts +4 -0
  242. package/src/components/deploy-units.ts +5 -0
  243. package/src/components/fan-out-run.ts +39 -2
  244. package/src/components/fan-out-waves.test.ts +45 -1
  245. package/src/components/pr-environment.test.ts +249 -0
  246. package/src/components/pr-environment.ts +538 -0
  247. package/src/components/pr-hooks.test.ts +118 -0
  248. package/src/components/pr-hooks.ts +157 -0
  249. package/src/components/pr-pipeline.ts +130 -5
  250. package/src/components/pr-policy.test.ts +145 -0
  251. package/src/components/pr-policy.ts +211 -0
  252. package/src/components/pr-run.ts +5 -2
  253. package/src/components/promote.test.ts +39 -0
  254. package/src/components/promote.ts +38 -14
  255. package/src/gate-decision-bundle.test.ts +4 -3
  256. package/src/gate-decision.ts +39 -2
  257. package/src/gate-policy-bundle.test.ts +28 -0
  258. package/src/gate-policy.test.ts +162 -0
  259. package/src/gate-policy.ts +298 -0
  260. package/src/lifecycle/__fixtures__/publish-fakes.ts +6 -1
  261. package/src/lifecycle/gate-checkpoint.ts +1 -1
  262. package/src/{workspace/trust → lifecycle}/gate-seal.ts +1 -1
  263. package/src/lifecycle/publish-declaration.ts +4 -3
  264. package/src/lifecycle/publish-npm.ts +33 -8
  265. package/src/lifecycle/publish-targets.test.ts +36 -3
  266. package/src/lifecycle/query.test.ts +91 -0
  267. package/src/lifecycle/query.ts +315 -0
  268. package/src/lifecycle/release-ledger.ts +42 -1
  269. package/src/lifecycle/run-ledger.ts +18 -0
  270. package/src/lifecycle/status.test.ts +19 -0
  271. package/src/lifecycle/status.ts +14 -6
  272. package/src/lifecycle/target-lock.test.ts +195 -0
  273. package/src/lifecycle/target-lock.ts +347 -0
  274. package/src/live-endpoint.ts +31 -0
  275. package/src/op/activities/activity-contracts.ts +31 -0
  276. package/src/op/activities/index.ts +6 -0
  277. package/src/op/activities/review-agent.test.ts +240 -0
  278. package/src/op/activities/review-agent.ts +355 -0
  279. package/src/op/builders.ts +13 -0
  280. package/src/op/cron.ts +44 -0
  281. package/src/op/gate-alias.test.ts +2 -2
  282. package/src/op/gate-approval.test.ts +30 -9
  283. package/src/op/gate-approval.ts +23 -26
  284. package/src/op/gate-quorum.test.ts +139 -0
  285. package/src/op/gate-quorum.ts +248 -0
  286. package/src/op/gate-resume.ts +6 -3
  287. package/src/op/gate-review.ts +1 -1
  288. package/src/op/gate.ts +64 -23
  289. package/src/op/index.ts +13 -1
  290. package/src/op/local-executor.ts +9 -2
  291. package/src/op/op-waves-hook-targets.test.ts +126 -0
  292. package/src/op/op-waves-policy-hold.test.ts +66 -0
  293. package/src/op/op-waves-quorum.test.ts +101 -0
  294. package/src/op/op-waves-run.ts +233 -11
  295. package/src/op/op-waves.ts +83 -10
  296. package/src/op/schedule-overdue.test.ts +220 -0
  297. package/src/op/schedule-overdue.ts +161 -0
  298. package/src/override-gate-bundle.test.ts +1 -1
  299. package/src/override-gate.ts +2 -2
  300. package/src/pr-forge-bundle.test.ts +2 -2
  301. package/src/pr-forge.test.ts +33 -0
  302. package/src/pr-forge.ts +114 -0
  303. package/src/pr-loop-bundle.test.ts +2 -2
  304. package/src/pr-loop.test.ts +58 -0
  305. package/src/pr-loop.ts +146 -4
  306. package/src/pr-post.test.ts +141 -0
  307. package/src/pr-post.ts +159 -0
  308. package/src/wave-blocked-bundle.test.ts +25 -0
  309. package/src/wave-blocked.test.ts +146 -0
  310. package/src/wave-blocked.ts +271 -0
  311. package/src/wave-engine.ts +8 -0
  312. package/src/workspace/__fixtures__/two-member-workspace.ts +2 -2
  313. package/src/workspace/ci-branches.test.ts +62 -0
  314. package/src/workspace/ci-branches.ts +79 -0
  315. package/src/workspace/ci-cloud-identity.test.ts +53 -0
  316. package/src/workspace/ci-cloud-identity.ts +23 -0
  317. package/src/workspace/ci-locks.ts +16 -0
  318. package/src/workspace/ci-run-index.test.ts +2 -1
  319. package/src/workspace/ci-run.test.ts +133 -8
  320. package/src/workspace/ci-run.ts +92 -7
  321. package/src/workspace/declaration.schema.json +147 -0
  322. package/src/workspace/declaration.test.ts +76 -0
  323. package/src/workspace/declaration.ts +134 -2
  324. package/src/workspace/identity-gate-rule.ts +1 -1
  325. package/src/workspace/identity.ts +1 -1
  326. package/src/workspace/lineage-upgrade.ts +11 -8
  327. package/src/workspace/member-pipeline.test.ts +29 -0
  328. package/src/workspace/member-pipeline.ts +30 -0
  329. package/src/workspace/pr-report.schema.json +73 -2
  330. package/src/workspace/security.ts +4 -0
  331. package/src/workspace/status-gates.ts +9 -6
  332. package/src/workspace/status.schema.json +1 -0
  333. package/src/workspace/status.ts +11 -4
  334. package/src/workspace/trust/gate-seal.test.ts +1 -1
  335. package/src/workspace/trust/seal.ts +2 -2
  336. package/dist/workspace/trust/gate-seal.d.ts.map +0 -1
  337. /package/dist/{workspace/trust → lifecycle}/gate-seal.js +0 -0
@@ -0,0 +1,237 @@
1
+ /**
2
+ * The forge token handover (#4295): no step that runs a pull request's code
3
+ * holds a forge token.
4
+ *
5
+ * A pull request's plan runs its code: the synth of its components, the
6
+ * plans their capabilities run, its build and lint, and its hooks. A token
7
+ * that can post a note or set a status can also push, so the generated plan
8
+ * job uses the job's token for the checkout's fetches alone and drops it
9
+ * before anything else runs. The ordering is terragucci's `handover.ts`
10
+ * (INTENTIUS/terragucci e6cb431, packages/terragucci/src/handover.ts):
11
+ *
12
+ * 1. The checkout is a script ({@link checkoutScript}), not actions/checkout,
13
+ * whose post step runs at the job's end with the token as its input. The
14
+ * token reaches its fetches through the environment, never the git config.
15
+ * 2. Every later step starts again without the token variables
16
+ * ({@link dropForgeTokens}).
17
+ * 3. The plan step is the job's last. A step after it would start beside
18
+ * whatever the change's code left running, with what it wrote to
19
+ * GITHUB_ENV and GITHUB_PATH. So the plan step hands its report directory
20
+ * on as the job's outputs ({@link handoverTrap}) in place of an artifact
21
+ * upload.
22
+ * 4. A fresh job, `plan-note`, checks out the target branch's commit, takes
23
+ * the report back ({@link takeOverScript}), keeps it as the artifact, and
24
+ * posts the note and the status with the token (`chant components
25
+ * pr-post`, ./pr-post.ts). It reads the report as data.
26
+ *
27
+ * `chant ci run plan` keeps the same rule: no step of the plan job gets a
28
+ * forge token, and the runner posts from the report once the steps settle
29
+ * (../workspace/ci-run.ts).
30
+ */
31
+
32
+ import type { ForgeKind } from "./pr-forge";
33
+
34
+ /**
35
+ * The variables a runner, a job or a secret may hold a forge token in.
36
+ * A step that runs the change's code starts without them. The list is
37
+ * terragucci's `STEP_TOKEN_VARS` (render.ts:1787) with chant's own names
38
+ * (`FORGE_TOKEN_ENV` in ./pr-forge.ts) and GitLab's added.
39
+ */
40
+ export const FORGE_TOKEN_VARS = [
41
+ "CHANT_FORGE_TOKEN",
42
+ "CHANT_GITHUB_TOKEN",
43
+ "CHANT_FORGEJO_TOKEN",
44
+ "CHANT_GITLAB_TOKEN",
45
+ "CHANT_CHECKOUT_TOKEN",
46
+ "GITHUB_TOKEN",
47
+ "GH_TOKEN",
48
+ "FORGEJO_TOKEN",
49
+ "GITEA_TOKEN",
50
+ "GITLAB_TOKEN",
51
+ "CI_JOB_TOKEN",
52
+ "ACTIONS_RUNTIME_TOKEN",
53
+ ] as const;
54
+
55
+ /** `env` without {@link FORGE_TOKEN_VARS}. */
56
+ export function withoutForgeTokens<T extends Record<string, string | undefined>>(env: T): T {
57
+ const out = { ...env };
58
+ for (const name of FORGE_TOKEN_VARS) delete out[name];
59
+ return out;
60
+ }
61
+
62
+ /** Whether `env` holds a forge token under one of {@link FORGE_TOKEN_VARS}. */
63
+ export function holdsForgeToken(env: Record<string, string | undefined>): boolean {
64
+ return FORGE_TOKEN_VARS.some((name) => (env[name] ?? "") !== "");
65
+ }
66
+
67
+ /** The marker a step {@link dropForgeTokens} restarted carries. */
68
+ export const NO_FORGE_TOKEN_MARKER = "CHANT_NO_FORGE_TOKEN";
69
+
70
+ /**
71
+ * The first line of a step that runs the change's code: restart the step's
72
+ * shell once without the token variables in its process environment, or,
73
+ * when it cannot restart, unset them. Unsetting alone leaves the values in
74
+ * the shell's own initial environment, which a child reads on Linux from
75
+ * `/proc/<pid>/environ`; the restart replaces it. A forge runs a step's
76
+ * script as a file (`sh -e {0}` in a GitHub or Forgejo container job), so
77
+ * `$0` is that file. terragucci's `restartWithout` (render.ts:1826).
78
+ */
79
+ export function dropForgeTokens(shell: "sh" | "bash" = "sh", extra: readonly string[] = []): string {
80
+ const again = shell === "bash" ? 'bash --noprofile --norc -e -o pipefail "$0"' : 'sh -e "$0"';
81
+ const marker = NO_FORGE_TOKEN_MARKER;
82
+ // #4296: a plan job that traded its OIDC token drops the variables a token is requested with too.
83
+ const vars = [...FORGE_TOKEN_VARS, ...extra];
84
+ const can = `[ -z "\${${marker}:-}" ] && [ "\${0#*/}" != "$0" ] && [ -f "$0" ] && ! { [ "\${0##*/}" = sh ] || [ "\${0##*/}" = bash ] || [ "\${0##*/}" = dash ]; } && command -v env >/dev/null 2>&1 && command -v ${shell} >/dev/null 2>&1`;
85
+ return `if ${can}; then exec env ${vars.map((v) => `-u ${v}`).join(" ")} ${marker}=1 ${again}; fi; unset ${vars.join(" ")}`;
86
+ }
87
+
88
+ /** The line a GitLab plan job starts its script with: GitLab runs the script in one shell, so the unset holds for every line after it. */
89
+ export const GITLAB_DROP_FORGE_TOKENS = `unset ${FORGE_TOKEN_VARS.join(" ")}`;
90
+
91
+ /** The forges whose plan job hands its report on through job outputs. GitLab passes the artifact to the next job itself. */
92
+ export type HandoverForge = Exclude<ForgeKind, "gitlab">;
93
+
94
+ /**
95
+ * How many outputs the plan job hands its report on in, and the most
96
+ * characters each holds. Forgejo keeps each output's value to 1 MiB. GitHub
97
+ * keeps a job's outputs to 1 MB together, sized as UTF-16. terragucci's
98
+ * `HANDOVER_LIMITS` (handover.ts).
99
+ */
100
+ export const HANDOVER_LIMITS: Record<HandoverForge, { chunk: number; parts: number }> = {
101
+ forgejo: { chunk: 1_000_000, parts: 4 },
102
+ github: { chunk: 450_000, parts: 1 },
103
+ };
104
+
105
+ /** The outputs' names on a forge: report_0, report_1, ... */
106
+ export function handoverKeys(forge: HandoverForge): string[] {
107
+ return Array.from({ length: HANDOVER_LIMITS[forge].parts }, (_, i) => `report_${i}`);
108
+ }
109
+
110
+ /** The files the note job reads, which a report too large to hand on whole is cut to. */
111
+ export const HANDOVER_NOTE_FILES = ["pr-plan.json", "pr-note.md"] as const;
112
+
113
+ /**
114
+ * Packs the report directory (argv 1) into the step's output file (argv 2):
115
+ * every regular file, found without following a link, as gzipped JSON in
116
+ * base64, cut into the forge's chunks. A report too large is cut to
117
+ * {@link HANDOVER_NOTE_FILES}, and to none when even those are too large.
118
+ */
119
+ export function packJs(forge: HandoverForge): string {
120
+ const { chunk, parts } = HANDOVER_LIMITS[forge];
121
+ return [
122
+ 'const fs=require("fs"),p=require("path"),z=require("zlib");',
123
+ "const [dir,out]=process.argv.slice(1);",
124
+ `const CHUNK=${chunk},PARTS=${parts},NOTE=${JSON.stringify(HANDOVER_NOTE_FILES)};`,
125
+ "const all={};",
126
+ 'const walk=(d,r)=>{let n=[];try{n=fs.readdirSync(d).sort()}catch{return}for(const x of n){const f=p.join(d,x),k=r?r+"/"+x:x;let s;try{s=fs.lstatSync(f)}catch{continue}if(s.isDirectory())walk(f,k);else if(s.isFile())all[k]=fs.readFileSync(f).toString("base64")}};',
127
+ 'walk(dir,"");',
128
+ 'const pack=(o)=>z.gzipSync(JSON.stringify(o)).toString("base64");',
129
+ "let b=pack(all);",
130
+ "if(b.length>CHUNK*PARTS){b=pack(Object.fromEntries(Object.entries(all).filter(([k])=>NOTE.includes(k))));",
131
+ 'if(b.length>CHUNK*PARTS){b=pack({});console.error("chant: the plan report is too large to hand to the note job, even cut to the report and the note; it hands on none of it")}',
132
+ 'else console.error("chant: the plan report is too large to hand to the note job whole; it hands on pr-plan.json and pr-note.md")}',
133
+ 'let o="";for(let i=0;i*CHUNK<b.length;i++)o+="report_"+i+"="+b.slice(i*CHUNK,(i+1)*CHUNK)+"\\n";',
134
+ "fs.appendFileSync(out,o);",
135
+ ].join("");
136
+ }
137
+
138
+ /**
139
+ * Writes back, under the directory argv 1 names, the files the plan job
140
+ * handed on: stdin holds its outputs, one JSON string a line. A path is kept
141
+ * only when it is plain segments (no empty one, no `.` or `..`, no backslash
142
+ * or control character), so nothing lands outside the directory.
143
+ */
144
+ export const UNPACK_JS = [
145
+ 'const fs=require("fs"),p=require("path"),z=require("zlib");',
146
+ "const dir=process.argv[1];",
147
+ 'let b="";',
148
+ 'for(const l of fs.readFileSync(0,"utf8").split("\\n")){if(!l.trim())continue;let s="";try{s=JSON.parse(l)}catch{}if(typeof s==="string")b+=s}',
149
+ 'if(!b){console.log("chant: the plan job handed on no report");process.exit(0)}',
150
+ 'let files;try{files=JSON.parse(z.gunzipSync(Buffer.from(b,"base64"),{maxOutputLength:268435456}).toString("utf8"))}catch(e){console.log("chant: the report the plan job handed on does not read: "+e.message);process.exit(0)}',
151
+ 'if(!files||typeof files!=="object"||Array.isArray(files)){console.log("chant: the report the plan job handed on is not a set of files");process.exit(0)}',
152
+ "let n=0;",
153
+ 'for(const [k,v] of Object.entries(files)){const parts=k.split("/");',
154
+ 'if(typeof v!=="string"||k.length>1024||parts.some((x)=>!x||x==="."||x===".."||/[\\\\\\x00-\\x1f]/.test(x))){console.log("chant: left out "+JSON.stringify(k));continue}',
155
+ 'try{const f=p.join(dir,...parts);fs.mkdirSync(p.dirname(f),{recursive:true});fs.writeFileSync(f,Buffer.from(v,"base64"));n++}catch(e){console.log("chant: could not write "+JSON.stringify(k)+": "+e.message)}}',
156
+ 'console.log("chant: took "+n+" report files from the plan job")',
157
+ ].join("");
158
+
159
+ /**
160
+ * The lines of the plan step, after {@link dropForgeTokens}: when the step
161
+ * ends, however it ends, its report directory (`dir`, from the directory the
162
+ * step starts in) goes into the step's outputs.
163
+ */
164
+ export function handoverTrap(dir: string, forge: HandoverForge): string[] {
165
+ return [
166
+ `chant_report="$PWD/${dir}"`,
167
+ `chant_handover() { node -e '${packJs(forge)}' "$chant_report" "\${GITHUB_OUTPUT:-/dev/null}" || echo "chant: the plan report could not be handed to the note job" >&2; }`,
168
+ "trap chant_handover EXIT",
169
+ ];
170
+ }
171
+
172
+ /** A job's outputs that carry the report its step `step` handed on. */
173
+ export function handoverOutputs(step: string, forge: HandoverForge): Record<string, string> {
174
+ return Object.fromEntries(handoverKeys(forge).map((k) => [k, `\${{ steps.${step}.outputs.${k} }}`]));
175
+ }
176
+
177
+ /**
178
+ * The script of the step that takes the report job `job` handed on into
179
+ * `dir`. Each output comes in as toJSON's string, one line of a quoted
180
+ * heredoc whose end no JSON string can be, so the change's outputs stay data.
181
+ */
182
+ export function takeOverScript(job: string, dir: string, forge: HandoverForge): string {
183
+ return [
184
+ "set -u",
185
+ `node -e '${UNPACK_JS}' ${dir} <<'END_OF_REPORT'`,
186
+ ...handoverKeys(forge).map((k) => `\${{ toJSON(needs.${job}.outputs.${k}) }}`),
187
+ "END_OF_REPORT",
188
+ ].join("\n");
189
+ }
190
+
191
+ /** The variable the checkout script reads the job's token from. */
192
+ export const CHECKOUT_TOKEN_ENV = "CHANT_CHECKOUT_TOKEN";
193
+
194
+ /**
195
+ * Checks the run's commit out with git, as actions/checkout does without
196
+ * credentials kept: `GITHUB_SHA` under `GITHUB_REF`. With `history`, every
197
+ * branch and tag too, since the plan measures the change with git. The job's
198
+ * token ({@link CHECKOUT_TOKEN_ENV}) reaches the fetches alone, through the
199
+ * environment, never the git config. A workspace already checked out is
200
+ * cleaned first, as actions/checkout does. terragucci's `checkoutScript`
201
+ * (handover.ts). With `ledger`, the `chant/lifecycle` branch is fetched
202
+ * into a local branch too, while the token is there: the plan reads the
203
+ * gate ledger from it, and a later fetch without the token cannot reach a
204
+ * private repository (`requireLifecycleLedger` in ./lifecycle/git.ts reads
205
+ * the local branch when the fetch fails).
206
+ */
207
+ export function checkoutScript(history: boolean, ledger = false): string {
208
+ const fetchLine = history
209
+ ? `git -c protocol.version=2 fetch -q --prune --no-recurse-submodules origin '+refs/heads/*:refs/remotes/origin/*' '+refs/tags/*:refs/tags/*' "+$want:$target"`
210
+ : 'git -c protocol.version=2 fetch -q --no-tags --prune --no-recurse-submodules --depth=1 origin "+$want:$target"';
211
+ return [
212
+ "set -eu",
213
+ 'git config --global --add safe.directory "$PWD" 2>/dev/null || true',
214
+ "[ -d .git ] || git init -q .",
215
+ 'git remote get-url origin >/dev/null 2>&1 || git remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"',
216
+ `if [ -n "\${${CHECKOUT_TOKEN_ENV}:-}" ]; then`,
217
+ ` chant_auth="$(printf 'x-access-token:%s' "$${CHECKOUT_TOKEN_ENV}" | base64 | tr -d '\\n')"`,
218
+ ' export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.extraheader GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $chant_auth"',
219
+ " unset chant_auth",
220
+ "fi",
221
+ 'ref="$GITHUB_REF"',
222
+ 'want="$GITHUB_SHA"',
223
+ 'case "$ref" in',
224
+ ' refs/heads/*) target="refs/remotes/origin/${ref#refs/heads/}" ;;',
225
+ ' refs/tags/*) target="$ref" ;;',
226
+ ' refs/*) target="refs/remotes/${ref#refs/}" ;;',
227
+ ' *) target="refs/remotes/origin/chant-head" ;;',
228
+ "esac",
229
+ "if git rev-parse -q --verify HEAD >/dev/null; then git clean -ffdxq; fi",
230
+ fetchLine,
231
+ ...(ledger ? ["git -c protocol.version=2 fetch -q --no-tags origin '+refs/heads/chant/lifecycle:refs/heads/chant/lifecycle' 2>/dev/null || true"] : []),
232
+ 'case "$ref" in',
233
+ ' refs/heads/*) git checkout -q --force -B "${ref#refs/heads/}" "$target" ;;',
234
+ ' *) git checkout -q --force --detach "$target" ;;',
235
+ "esac",
236
+ ].join("\n");
237
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * #4296: the plan and apply cloud identities, and the OIDC trade. The trade
3
+ * tests run the generated `node -e` line with `sh`, as a forge step runs
4
+ * it, against a local emulator of the forge's token URL, AWS STS, Google's
5
+ * STS and IAM credentials, and Entra (./__fixtures__/oidc-emulator.ts).
6
+ * Test values only; no cloud is reached.
7
+ */
8
+
9
+ import { execFile } from "node:child_process";
10
+ import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { promisify } from "node:util";
14
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
15
+ import {
16
+ cloudIdentityProblems,
17
+ gitlabTradeIdTokens,
18
+ gitlabTradeLines,
19
+ idTokenVar,
20
+ OIDC_REQUEST_VARS,
21
+ resolveCloudIdentity,
22
+ TRADE_JS,
23
+ TRADE_SPEC_ENV,
24
+ tradeLine,
25
+ type CloudIdentityDeclaration,
26
+ type CloudTradeSpec,
27
+ } from "./ci-identity";
28
+ import { emulatedCredentials, EMULATED_JWT_PREFIX, startOidcEmulator, type OidcEmulator } from "./__fixtures__/oidc-emulator";
29
+ import { dropForgeTokens } from "./ci-handover";
30
+
31
+ const run = promisify(execFile);
32
+
33
+ const DECL: CloudIdentityDeclaration = {
34
+ aws: {
35
+ region: "eu-west-1",
36
+ rules: [
37
+ { environments: ["prod"], members: ["net"], plan: "arn:aws:iam::111111111111:role/net-prod-plan", apply: "arn:aws:iam::111111111111:role/net-prod-apply" },
38
+ { environments: ["prod"], plan: "arn:aws:iam::111111111111:role/prod-plan", apply: "arn:aws:iam::111111111111:role/prod-apply" },
39
+ { environments: ["staging-*"], plan: "arn:aws:iam::222222222222:role/staging-plan", apply: "arn:aws:iam::222222222222:role/staging-apply" },
40
+ ],
41
+ },
42
+ gcp: {
43
+ provider: "projects/123/locations/global/workloadIdentityPools/ci/providers/forge",
44
+ rules: [{ plan: "plan@test-project.iam.gserviceaccount.com", apply: "apply@test-project.iam.gserviceaccount.com" }],
45
+ },
46
+ azure: { tenantId: "00000000-0000-0000-0000-00000000tena", rules: [{ members: ["app"], plan: "plan-client-id", apply: "apply-client-id" }] },
47
+ };
48
+
49
+ describe("which identities a pipeline takes (#4296)", () => {
50
+ test("the first rule whose environments and members match wins, per cloud", () => {
51
+ const net = resolveCloudIdentity(DECL, "prod", "net")!;
52
+ expect(net.plan.aws!.role).toBe("arn:aws:iam::111111111111:role/net-prod-plan");
53
+ expect(net.apply.aws!.role).toBe("arn:aws:iam::111111111111:role/net-prod-apply");
54
+ expect(resolveCloudIdentity(DECL, "prod", "web")!.plan.aws!.role).toBe("arn:aws:iam::111111111111:role/prod-plan");
55
+ expect(resolveCloudIdentity(DECL, "staging-eu", "net")!.apply.aws!.role).toBe("arn:aws:iam::222222222222:role/staging-apply");
56
+ expect(net.plan.aws).toMatchObject({ region: "eu-west-1", audience: "sts.amazonaws.com" });
57
+ });
58
+
59
+ test("a cloud no rule covers is left out, and so is a pipeline no rule covers", () => {
60
+ expect(resolveCloudIdentity(DECL, "prod", "net")!.plan.azure).toBeUndefined();
61
+ expect(resolveCloudIdentity(DECL, "prod", "app")!.plan.azure).toMatchObject({ clientId: "plan-client-id", audience: "api://AzureADTokenExchange" });
62
+ expect(resolveCloudIdentity({ aws: DECL.aws }, "dev", "net")).toBeUndefined();
63
+ expect(resolveCloudIdentity(undefined, "prod")).toBeUndefined();
64
+ });
65
+
66
+ test("a rule with members never matches a pipeline outside a member", () => {
67
+ expect(resolveCloudIdentity(DECL, "prod")!.plan.aws!.role).toBe("arn:aws:iam::111111111111:role/prod-plan");
68
+ });
69
+
70
+ test("GCP's default audience names the provider", () => {
71
+ expect(resolveCloudIdentity(DECL, "prod")!.plan.gcp!.audience).toBe("https://iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/ci/providers/forge");
72
+ });
73
+
74
+ test("a rule may not plan as an identity it or another rule applies as", () => {
75
+ expect(cloudIdentityProblems(DECL)).toEqual([]);
76
+ const same = cloudIdentityProblems({ aws: { rules: [{ plan: "r", apply: "r" }] } });
77
+ expect(same).toEqual([{ pointer: "/ci/identity/cloud/aws/rules/0", message: expect.stringContaining("plans and applies as r") }]);
78
+ const crossed = cloudIdentityProblems({ gcp: { provider: "p", rules: [{ plan: "a", apply: "b" }, { plan: "b", apply: "c" }] } });
79
+ expect(crossed).toEqual([{ pointer: "/ci/identity/cloud/gcp/rules/1", message: expect.stringContaining("plans as b, which another rule applies as") }]);
80
+ });
81
+ });
82
+
83
+ describe("the trade script stays inside one shell word", () => {
84
+ test("it holds no single quote, so tradeLine can quote it", () => {
85
+ expect(TRADE_JS).not.toContain("'");
86
+ });
87
+ test("GitLab's lines trade, unset the token variables and the extra ones, then source the credentials", () => {
88
+ const [trade, unset, source] = gitlabTradeLines(["CHANT_FORGE_TOKEN"]);
89
+ expect(trade).toContain("node -e");
90
+ expect(unset).toBe(`unset CHANT_FORGE_TOKEN ${OIDC_REQUEST_VARS.join(" ")}`);
91
+ expect(source).toMatch(/^\. "\$\{TMPDIR:-\/tmp\}\/chant-cloud-\$CI_JOB_ID\.sh" && rm -f /);
92
+ expect(gitlabTradeIdTokens(resolveCloudIdentity(DECL, "prod", "app")!.plan)).toEqual({
93
+ CHANT_ID_TOKEN_AWS: { aud: "sts.amazonaws.com" },
94
+ CHANT_ID_TOKEN_GCP: { aud: "https://iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/ci/providers/forge" },
95
+ CHANT_ID_TOKEN_AZURE: { aud: "api://AzureADTokenExchange" },
96
+ });
97
+ });
98
+ });
99
+
100
+ const dirs: string[] = [];
101
+ let emu: OidcEmulator;
102
+ beforeAll(async () => {
103
+ emu = await startOidcEmulator();
104
+ });
105
+ afterAll(async () => {
106
+ await emu.close();
107
+ for (const d of dirs) rmSync(d, { recursive: true, force: true });
108
+ });
109
+
110
+ const BASE = { PATH: process.env.PATH ?? "/usr/bin:/bin", HOME: process.env.HOME ?? "/tmp" };
111
+
112
+ /** Run the trade line as a step does, with `spec` pointed at the emulator. */
113
+ async function trade(spec: CloudTradeSpec, format: "github-env" | "sh", env: Record<string, string> = emu.requestEnv): Promise<{ out: string; stdout: string; stderr: string; code: number; file: string }> {
114
+ const dir = mkdtempSync(join(tmpdir(), "chant-trade-"));
115
+ dirs.push(dir);
116
+ const file = join(dir, format === "sh" ? "cloud.sh" : "github-env");
117
+ if (format === "github-env") writeFileSync(file, "EARLIER=1\n");
118
+ try {
119
+ const r = await run("sh", ["-c", tradeLine(file, format)], { env: { ...BASE, ...env, [TRADE_SPEC_ENV]: JSON.stringify(emu.pointAt(spec)) } });
120
+ return { out: existsSync(file) ? readFileSync(file, "utf-8") : "", stdout: r.stdout, stderr: r.stderr, code: 0, file };
121
+ } catch (e) {
122
+ const err = e as { stdout: string; stderr: string; code: number };
123
+ return { out: existsSync(file) ? readFileSync(file, "utf-8") : "", stdout: err.stdout, stderr: err.stderr, code: err.code, file };
124
+ }
125
+ }
126
+
127
+ describe("the trade, run against the emulator (#4296)", () => {
128
+ const prodApp = () => resolveCloudIdentity(DECL, "prod", "app")!;
129
+
130
+ test("it trades one minted token per cloud for the plan identity alone, and writes the credentials to GITHUB_ENV", async () => {
131
+ const before = { minted: emu.log.minted.length, assumed: emu.log.assumed.length };
132
+ const r = await trade(prodApp().plan, "github-env");
133
+ expect(r.stderr).not.toContain("failed");
134
+ expect(r.code).toBe(0);
135
+ const aws = emulatedCredentials("arn:aws:iam::111111111111:role/prod-plan");
136
+ expect(r.out).toContain("EARLIER=1\n");
137
+ expect(r.out).toContain(`AWS_ACCESS_KEY_ID=${aws.accessKeyId}\n`);
138
+ expect(r.out).toContain(`AWS_SESSION_TOKEN=${aws.token}\n`);
139
+ expect(r.out).toContain("AWS_REGION=eu-west-1\n");
140
+ expect(r.out).toMatch(/^GOOGLE_OAUTH_ACCESS_TOKEN=ya29\.session-/m);
141
+ expect(r.out).toMatch(/^AZURE_ACCESS_TOKEN=azure\.session-/m);
142
+ expect(r.out).toContain("AZURE_CLIENT_ID=plan-client-id\n");
143
+ // The minted token itself is in no line the later steps get, and no apply identity was asked for.
144
+ expect(r.out).not.toContain(EMULATED_JWT_PREFIX);
145
+ expect(r.stdout + r.stderr).not.toContain(EMULATED_JWT_PREFIX);
146
+ expect(emu.log.minted.slice(before.minted)).toEqual(["sts.amazonaws.com", "https://iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/ci/providers/forge", "api://AzureADTokenExchange"]);
147
+ expect(emu.log.assumed.slice(before.assumed).map((a) => a.role)).toEqual(["arn:aws:iam::111111111111:role/prod-plan"]);
148
+ expect(emu.log.impersonated.at(-1)).toBe("plan@test-project.iam.gserviceaccount.com");
149
+ expect(emu.log.clients.at(-1)!.clientId).toBe("plan-client-id");
150
+ // GitHub masks each secret in every later step's log.
151
+ expect(r.stdout).toContain(`::add-mask::${aws.secret}`);
152
+ expect(r.stdout).toContain(`::add-mask::${aws.token}`);
153
+ });
154
+
155
+ test("the sh format writes export lines a GitLab script sources, mode 0600", async () => {
156
+ const r = await trade({ aws: prodApp().apply.aws }, "sh");
157
+ expect(r.code).toBe(0);
158
+ expect(statSync(r.file).mode & 0o777).toBe(0o600);
159
+ const probe = await run("sh", ["-c", `. "${r.file}"; echo "$AWS_ACCESS_KEY_ID"`], { env: BASE });
160
+ expect(probe.stdout.trim()).toBe(emulatedCredentials("arn:aws:iam::111111111111:role/prod-apply").accessKeyId);
161
+ });
162
+
163
+ test("on GitLab the token comes from the job's id_tokens variable, and no request URL is needed", async () => {
164
+ const before = emu.log.minted.length;
165
+ const jwt = `${EMULATED_JWT_PREFIX}.gitlab.1`;
166
+ const r = await trade({ aws: prodApp().plan.aws }, "sh", { [idTokenVar("aws")]: jwt });
167
+ expect(r.code).toBe(0);
168
+ expect(emu.log.minted.length).toBe(before);
169
+ expect(emu.log.assumed.at(-1)).toEqual({ role: "arn:aws:iam::111111111111:role/prod-plan", token: jwt });
170
+ expect(r.out).not.toContain(jwt);
171
+ });
172
+
173
+ test("a job that can request no token fails the step and writes nothing", async () => {
174
+ const r = await trade({ aws: prodApp().plan.aws }, "sh", {});
175
+ expect(r.code).not.toBe(0);
176
+ expect(r.stderr).toContain("the job can request no OIDC token for aws");
177
+ expect(r.out).toBe("");
178
+ });
179
+
180
+ test("an answer that would add a line of its own to GITHUB_ENV is refused", async () => {
181
+ const r = await trade({ aws: { ...prodApp().plan.aws!, region: "eu-west-1\nNODE_OPTIONS=--require=/tmp/x" } }, "github-env");
182
+ expect(r.code).not.toBe(0);
183
+ expect(r.out).toBe("EARLIER=1\n");
184
+ });
185
+ });
186
+
187
+ describe("after the trade, a step that runs the change's code cannot request a token (#4296)", () => {
188
+ test("the drop line with the OIDC request variables leaves none of them, and a request from the step fails", async () => {
189
+ const dir = mkdtempSync(join(tmpdir(), "chant-trade-drop-"));
190
+ dirs.push(dir);
191
+ const file = join(dir, "step.sh");
192
+ const ask = `node -e 'const u=process.env.ACTIONS_ID_TOKEN_REQUEST_URL,k=process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;if(!u||!k){console.log("no-request-vars");process.exit(0)}fetch(u+"&audience=sts.amazonaws.com",{headers:{authorization:"bearer "+k}}).then(r=>r.text()).then(t=>console.log("minted:"+t))'`;
193
+ writeFileSync(file, `${dropForgeTokens("sh", OIDC_REQUEST_VARS)}\nenv\n${ask}\n`);
194
+ const before = emu.log.minted.length;
195
+ const r = await run("sh", ["-e", file], { cwd: dir, env: { ...BASE, ...emu.requestEnv, [idTokenVar("aws")]: `${EMULATED_JWT_PREFIX}.x.y`, [TRADE_SPEC_ENV]: "{}" } });
196
+ expect(r.stdout).toContain("no-request-vars");
197
+ expect(r.stdout).not.toContain(EMULATED_JWT_PREFIX);
198
+ for (const v of OIDC_REQUEST_VARS) expect(r.stdout).not.toMatch(new RegExp(`^${v}=`, "m"));
199
+ expect(emu.log.minted.length).toBe(before);
200
+ });
201
+ });