@empire-builder-kit/nx 1.0.0-rc.30 → 1.0.0-rc.32

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 (310) hide show
  1. package/executors.json +8 -3
  2. package/package.json +14 -7
  3. package/src/aws/child-environment.d.ts +10 -0
  4. package/src/aws/child-environment.js +77 -0
  5. package/src/aws/child-environment.js.map +1 -0
  6. package/src/aws/identity.d.ts +13 -0
  7. package/src/aws/identity.js +41 -0
  8. package/src/aws/identity.js.map +1 -0
  9. package/src/aws-authority/index.d.ts +1 -0
  10. package/src/aws-authority/index.js +5 -0
  11. package/src/aws-authority/index.js.map +1 -0
  12. package/src/aws-authority/stage-authority.d.ts +240 -0
  13. package/src/aws-authority/stage-authority.js +2968 -0
  14. package/src/aws-authority/stage-authority.js.map +1 -0
  15. package/src/cli/main.d.ts +2 -0
  16. package/src/cli/main.js +11 -0
  17. package/src/cli/main.js.map +1 -1
  18. package/src/developer-agent/generator-planning.js +4 -4
  19. package/src/developer-agent/generator-planning.js.map +1 -1
  20. package/src/developer-agent/operation-registry.js +14 -6
  21. package/src/developer-agent/operation-registry.js.map +1 -1
  22. package/src/developer-agent/workspace-inspection.js +11 -2
  23. package/src/developer-agent/workspace-inspection.js.map +1 -1
  24. package/src/executors/deployment-plan/deployment-plan.js +1 -1
  25. package/src/executors/deployment-plan/deployment-plan.js.map +1 -1
  26. package/src/executors/foundation-preflight/foundation-preflight.d.ts +0 -1
  27. package/src/executors/foundation-preflight/foundation-preflight.js.map +1 -1
  28. package/src/executors/foundation-preflight/schema.json +0 -5
  29. package/src/executors/foundation-profile-preflight/foundation-profile-preflight.d.ts +12 -3
  30. package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js +95 -32
  31. package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js.map +1 -1
  32. package/src/executors/foundation-profile-preflight/production-database-safeguard.d.ts +64 -0
  33. package/src/executors/foundation-profile-preflight/production-database-safeguard.js +221 -0
  34. package/src/executors/foundation-profile-preflight/production-database-safeguard.js.map +1 -0
  35. package/src/executors/foundation-profile-preflight/schema.json +1 -6
  36. package/src/executors/foundation-remove-preflight/foundation-remove-preflight.d.ts +11 -1
  37. package/src/executors/foundation-remove-preflight/foundation-remove-preflight.js +44 -21
  38. package/src/executors/foundation-remove-preflight/foundation-remove-preflight.js.map +1 -1
  39. package/src/executors/foundation-remove-preflight/schema.json +6 -1
  40. package/src/executors/github-bootstrap/github-bootstrap.d.ts +1 -0
  41. package/src/executors/github-bootstrap/github-bootstrap.js +1 -0
  42. package/src/executors/github-bootstrap/github-bootstrap.js.map +1 -1
  43. package/src/executors/github-bootstrap/schema.json +7 -1
  44. package/src/executors/infrastructure-plan/infrastructure-plan.d.ts +27 -369
  45. package/src/executors/infrastructure-plan/infrastructure-plan.js +237 -5470
  46. package/src/executors/infrastructure-plan/infrastructure-plan.js.map +1 -1
  47. package/src/executors/infrastructure-plan/schema.json +8 -64
  48. package/src/executors/local-deploy/federated-credential-transport.d.ts +48 -0
  49. package/src/executors/local-deploy/federated-credential-transport.js +252 -0
  50. package/src/executors/local-deploy/federated-credential-transport.js.map +1 -0
  51. package/src/executors/local-deploy/local-approval.d.ts +4 -9
  52. package/src/executors/local-deploy/local-approval.js +3 -7
  53. package/src/executors/local-deploy/local-approval.js.map +1 -1
  54. package/src/executors/local-deploy/local-deploy.d.ts +60 -43
  55. package/src/executors/local-deploy/local-deploy.js +955 -804
  56. package/src/executors/local-deploy/local-deploy.js.map +1 -1
  57. package/src/executors/local-deploy/schema.json +3 -20
  58. package/src/executors/refresh/refresh.js +2 -2
  59. package/src/executors/refresh/refresh.js.map +1 -1
  60. package/src/executors/remove/remove.js +2 -2
  61. package/src/executors/remove/remove.js.map +1 -1
  62. package/src/executors/router-waf-rehearsal/router-waf-rehearsal.d.ts +27 -1
  63. package/src/executors/router-waf-rehearsal/router-waf-rehearsal.js +907 -22
  64. package/src/executors/router-waf-rehearsal/router-waf-rehearsal.js.map +1 -1
  65. package/src/executors/router-waf-rehearsal/schema.json +17 -2
  66. package/src/executors/secret-initialize/schema.json +42 -0
  67. package/src/executors/secret-initialize/secret-initialize.d.ts +32 -0
  68. package/src/executors/secret-initialize/secret-initialize.js +243 -0
  69. package/src/executors/secret-initialize/secret-initialize.js.map +1 -0
  70. package/src/executors/secret-metadata/secret-metadata.d.ts +4 -1
  71. package/src/executors/secret-metadata/secret-metadata.js +76 -29
  72. package/src/executors/secret-metadata/secret-metadata.js.map +1 -1
  73. package/src/executors/sst-lifecycle/generated-secret-retirement.d.ts +53 -0
  74. package/src/executors/sst-lifecycle/generated-secret-retirement.js +192 -0
  75. package/src/executors/sst-lifecycle/generated-secret-retirement.js.map +1 -0
  76. package/src/executors/sst-lifecycle/governed-native-operation.d.ts +46 -0
  77. package/src/executors/sst-lifecycle/governed-native-operation.js +153 -0
  78. package/src/executors/sst-lifecycle/governed-native-operation.js.map +1 -0
  79. package/src/executors/sst-lifecycle/run-sst-command.d.ts +27 -7
  80. package/src/executors/sst-lifecycle/run-sst-command.js +474 -89
  81. package/src/executors/sst-lifecycle/run-sst-command.js.map +1 -1
  82. package/src/executors/sst-lifecycle/schema.json +0 -5
  83. package/src/executors/sst-lifecycle/secret-preflight.d.ts +2 -0
  84. package/src/executors/sst-lifecycle/secret-preflight.js +13 -7
  85. package/src/executors/sst-lifecycle/secret-preflight.js.map +1 -1
  86. package/src/executors/unlock/unlock.js +2 -2
  87. package/src/executors/unlock/unlock.js.map +1 -1
  88. package/src/foundation/provision-logical-database.js +2 -0
  89. package/src/foundation/provision-logical-database.js.map +1 -1
  90. package/src/foundation/router-waf-rate-rehearsal.d.ts +97 -0
  91. package/src/foundation/router-waf-rate-rehearsal.js +210 -0
  92. package/src/foundation/router-waf-rate-rehearsal.js.map +1 -0
  93. package/src/foundation/router-waf.d.ts +59 -9
  94. package/src/foundation/router-waf.js +427 -32
  95. package/src/foundation/router-waf.js.map +1 -1
  96. package/src/foundation/stages.d.ts +1 -0
  97. package/src/foundation/stages.js +11 -0
  98. package/src/foundation/stages.js.map +1 -1
  99. package/src/generators/formatting.d.ts +8 -0
  100. package/src/generators/formatting.js +118 -0
  101. package/src/generators/formatting.js.map +1 -0
  102. package/src/generators/function/function.js +37 -17
  103. package/src/generators/function/function.js.map +1 -1
  104. package/src/generators/post-generation-install.js +2 -2
  105. package/src/generators/post-generation-install.js.map +1 -1
  106. package/src/generators/preset/generator.js +28 -8
  107. package/src/generators/preset/generator.js.map +1 -1
  108. package/src/generators/preset/schema.d.ts +3 -0
  109. package/src/generators/preset/schema.json +5 -5
  110. package/src/generators/preset/workspace-files-developer-agent.js +15 -7
  111. package/src/generators/preset/workspace-files-developer-agent.js.map +1 -1
  112. package/src/generators/preset/workspace-files-ecr-inspector.d.ts +18 -0
  113. package/src/generators/preset/workspace-files-ecr-inspector.js +298 -0
  114. package/src/generators/preset/workspace-files-ecr-inspector.js.map +1 -0
  115. package/src/generators/preset/workspace-files-performance-cost.d.ts +2 -2
  116. package/src/generators/preset/workspace-files-performance-cost.js +8 -8
  117. package/src/generators/preset/workspace-files-performance-cost.js.map +1 -1
  118. package/src/generators/preset/workspace-files-performance-load.d.ts +1 -1
  119. package/src/generators/preset/workspace-files-performance-load.js +29 -24
  120. package/src/generators/preset/workspace-files-performance-load.js.map +1 -1
  121. package/src/generators/preset/workspace-files-stage-authority.d.ts +6 -0
  122. package/src/generators/preset/workspace-files-stage-authority.js +479 -0
  123. package/src/generators/preset/workspace-files-stage-authority.js.map +1 -0
  124. package/src/generators/preset/workspace-files-thin-deploy.d.ts +40 -0
  125. package/src/generators/preset/workspace-files-thin-deploy.js +1500 -0
  126. package/src/generators/preset/workspace-files-thin-deploy.js.map +1 -0
  127. package/src/generators/preset/workspace-files-thin-docs.d.ts +5 -0
  128. package/src/generators/preset/workspace-files-thin-docs.js +240 -0
  129. package/src/generators/preset/workspace-files-thin-docs.js.map +1 -0
  130. package/src/generators/preset/workspace-files-thin-release.d.ts +14 -0
  131. package/src/generators/preset/workspace-files-thin-release.js +26 -0
  132. package/src/generators/preset/workspace-files-thin-release.js.map +1 -0
  133. package/src/generators/preset/workspace-files.d.ts +2 -1
  134. package/src/generators/preset/workspace-files.js +448 -680
  135. package/src/generators/preset/workspace-files.js.map +1 -1
  136. package/src/generators/repository-policy/repository-policy.js +149 -19
  137. package/src/generators/repository-policy/repository-policy.js.map +1 -1
  138. package/src/generators/repository-policy/schema.d.ts +2 -0
  139. package/src/generators/repository-policy/schema.json +11 -1
  140. package/src/generators/repository-policy/tree-policy.d.ts +3 -0
  141. package/src/generators/repository-policy/tree-policy.js +25 -0
  142. package/src/generators/repository-policy/tree-policy.js.map +1 -0
  143. package/src/generators/slice/built-in-plan-api-files.js +1 -0
  144. package/src/generators/slice/built-in-plan-api-files.js.map +1 -1
  145. package/src/generators/slice/built-in-plan-async-work-files.js +41 -0
  146. package/src/generators/slice/built-in-plan-async-work-files.js.map +1 -1
  147. package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js +6 -0
  148. package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js.map +1 -1
  149. package/src/generators/slice/built-in-plan-data-export-infrastructure-files.js +5 -31
  150. package/src/generators/slice/built-in-plan-data-export-infrastructure-files.js.map +1 -1
  151. package/src/generators/slice/built-in-plan-data-files.d.ts +1 -1
  152. package/src/generators/slice/built-in-plan-data-files.js +89 -30
  153. package/src/generators/slice/built-in-plan-data-files.js.map +1 -1
  154. package/src/generators/slice/built-in-plan-data-lifecycle-runtime-files.js +19 -0
  155. package/src/generators/slice/built-in-plan-data-lifecycle-runtime-files.js.map +1 -1
  156. package/src/generators/slice/built-in-plan-data-retention-launcher-files.js +21 -3
  157. package/src/generators/slice/built-in-plan-data-retention-launcher-files.js.map +1 -1
  158. package/src/generators/slice/built-in-plan-database-retirement-rehearsal-files.js +20 -1
  159. package/src/generators/slice/built-in-plan-database-retirement-rehearsal-files.js.map +1 -1
  160. package/src/generators/slice/built-in-plan-foundation-files.js +3 -5
  161. package/src/generators/slice/built-in-plan-foundation-files.js.map +1 -1
  162. package/src/generators/slice/built-in-plan-frontend-files.js +5 -1
  163. package/src/generators/slice/built-in-plan-frontend-files.js.map +1 -1
  164. package/src/generators/slice/built-in-plan-infrastructure-files.d.ts +1 -1
  165. package/src/generators/slice/built-in-plan-infrastructure-files.js +199 -188
  166. package/src/generators/slice/built-in-plan-infrastructure-files.js.map +1 -1
  167. package/src/generators/slice/built-in-plan-metadata-files.js +13 -0
  168. package/src/generators/slice/built-in-plan-metadata-files.js.map +1 -1
  169. package/src/generators/slice/built-in-plan-native-runtime-authority-files.d.ts +2 -0
  170. package/src/generators/slice/built-in-plan-native-runtime-authority-files.js +307 -0
  171. package/src/generators/slice/built-in-plan-native-runtime-authority-files.js.map +1 -0
  172. package/src/generators/slice/built-in-plan-operations-files.js +65 -51
  173. package/src/generators/slice/built-in-plan-operations-files.js.map +1 -1
  174. package/src/generators/slice/built-in-plan-performance-files.js +97 -19
  175. package/src/generators/slice/built-in-plan-performance-files.js.map +1 -1
  176. package/src/generators/slice/built-in-plan-project-files.js +9 -68
  177. package/src/generators/slice/built-in-plan-project-files.js.map +1 -1
  178. package/src/generators/slice/built-in-plan-secret-runtime-files.d.ts +1 -1
  179. package/src/generators/slice/built-in-plan-secret-runtime-files.js +87 -76
  180. package/src/generators/slice/built-in-plan-secret-runtime-files.js.map +1 -1
  181. package/src/generators/slice/built-in-plan-testing-files.js +94 -47
  182. package/src/generators/slice/built-in-plan-testing-files.js.map +1 -1
  183. package/src/generators/slice/built-in-plan-thin-release-files.d.ts +11 -0
  184. package/src/generators/slice/built-in-plan-thin-release-files.js +129 -0
  185. package/src/generators/slice/built-in-plan-thin-release-files.js.map +1 -0
  186. package/src/generators/slice/built-in-plan-verification-files.js +6 -2
  187. package/src/generators/slice/built-in-plan-verification-files.js.map +1 -1
  188. package/src/generators/slice/built-in-plan-web-files.js +4 -2
  189. package/src/generators/slice/built-in-plan-web-files.js.map +1 -1
  190. package/src/generators/slice/built-in-plan-workload-role-files.js +26 -4
  191. package/src/generators/slice/built-in-plan-workload-role-files.js.map +1 -1
  192. package/src/generators/slice/built-in-plan.d.ts +1 -0
  193. package/src/generators/slice/built-in-plan.js +28 -11
  194. package/src/generators/slice/built-in-plan.js.map +1 -1
  195. package/src/generators/slice/slice.js +30 -32
  196. package/src/generators/slice/slice.js.map +1 -1
  197. package/src/generators/upgrade/upgrade.d.ts +1 -1
  198. package/src/generators/upgrade/upgrade.js +200 -48
  199. package/src/generators/upgrade/upgrade.js.map +1 -1
  200. package/src/generators/workload/performance-registration.d.ts +1 -0
  201. package/src/generators/workload/performance-registration.js +9 -11
  202. package/src/generators/workload/performance-registration.js.map +1 -1
  203. package/src/generators/workload/service-performance-files.d.ts +1 -0
  204. package/src/generators/workload/service-performance-files.js +27 -7
  205. package/src/generators/workload/service-performance-files.js.map +1 -1
  206. package/src/generators/workload/workload.d.ts +1 -1
  207. package/src/generators/workload/workload.js +55 -28
  208. package/src/generators/workload/workload.js.map +1 -1
  209. package/src/git-policy/github-bootstrap.d.ts +28 -7
  210. package/src/git-policy/github-bootstrap.js +205 -163
  211. package/src/git-policy/github-bootstrap.js.map +1 -1
  212. package/src/index.d.ts +1 -0
  213. package/src/index.js +1 -0
  214. package/src/index.js.map +1 -1
  215. package/src/local-dev/private-state.d.ts +3 -0
  216. package/src/local-dev/private-state.js +17 -5
  217. package/src/local-dev/private-state.js.map +1 -1
  218. package/src/local-dev/tls.js +5 -1
  219. package/src/local-dev/tls.js.map +1 -1
  220. package/src/performance/cost-collection.d.ts +1 -1
  221. package/src/performance/cost-collection.js +4 -2
  222. package/src/performance/cost-collection.js.map +1 -1
  223. package/src/performance/cost-controls.d.ts +3 -1
  224. package/src/performance/cost-controls.js +9 -4
  225. package/src/performance/cost-controls.js.map +1 -1
  226. package/src/performance/evidence.d.ts +1 -1
  227. package/src/performance/evidence.js +5 -3
  228. package/src/performance/evidence.js.map +1 -1
  229. package/src/performance/load-safety.js +5 -3
  230. package/src/performance/load-safety.js.map +1 -1
  231. package/src/performance/model.d.ts +2 -2
  232. package/src/performance/model.js +4 -3
  233. package/src/performance/model.js.map +1 -1
  234. package/src/release/index.d.ts +0 -2
  235. package/src/release/index.js +0 -2
  236. package/src/release/index.js.map +1 -1
  237. package/src/release/source-authority.d.ts +4 -5
  238. package/src/release/source-authority.js +13 -123
  239. package/src/release/source-authority.js.map +1 -1
  240. package/src/repository-policy/aurora-preflight.d.ts +1 -1
  241. package/src/repository-policy/aurora-preflight.js +4 -1
  242. package/src/repository-policy/aurora-preflight.js.map +1 -1
  243. package/src/repository-policy/policy.d.ts +21 -2
  244. package/src/repository-policy/policy.js +290 -25
  245. package/src/repository-policy/policy.js.map +1 -1
  246. package/src/schemas/performance-policy-v1.json +3 -1
  247. package/src/schemas/repository-policy-v1.json +46 -2
  248. package/src/secrets/adapters.d.ts +7 -2
  249. package/src/secrets/adapters.js +34 -3
  250. package/src/secrets/adapters.js.map +1 -1
  251. package/src/secrets/aws-control-plane.d.ts +10 -1
  252. package/src/secrets/aws-control-plane.js +67 -0
  253. package/src/secrets/aws-control-plane.js.map +1 -1
  254. package/src/secrets/generated-aws-control-plane.d.ts +23 -0
  255. package/src/secrets/generated-aws-control-plane.js +259 -0
  256. package/src/secrets/generated-aws-control-plane.js.map +1 -0
  257. package/src/secrets/metadata.js +38 -10
  258. package/src/secrets/metadata.js.map +1 -1
  259. package/src/secrets/model.d.ts +8 -1
  260. package/src/secrets/model.js +24 -3
  261. package/src/secrets/model.js.map +1 -1
  262. package/src/secrets/operator.d.ts +89 -3
  263. package/src/secrets/operator.js +422 -9
  264. package/src/secrets/operator.js.map +1 -1
  265. package/src/secrets/registry.d.ts +2 -1
  266. package/src/secrets/registry.js +18 -1
  267. package/src/secrets/registry.js.map +1 -1
  268. package/src/secrets/workload-access.js +10 -2
  269. package/src/secrets/workload-access.js.map +1 -1
  270. package/src/supply-chain/action-pins.js +61 -7
  271. package/src/supply-chain/action-pins.js.map +1 -1
  272. package/src/supply-chain/build-output-bounds.d.ts +2 -0
  273. package/src/supply-chain/build-output-bounds.js +12 -0
  274. package/src/supply-chain/build-output-bounds.js.map +1 -0
  275. package/src/supply-chain/build-output.js +2 -4
  276. package/src/supply-chain/build-output.js.map +1 -1
  277. package/src/supply-chain/ecr-scanning.d.ts +5 -3
  278. package/src/supply-chain/ecr-scanning.js +11 -7
  279. package/src/supply-chain/ecr-scanning.js.map +1 -1
  280. package/src/supply-chain/ecs-deployed-image.d.ts +5 -3
  281. package/src/supply-chain/ecs-deployed-image.js +6 -4
  282. package/src/supply-chain/ecs-deployed-image.js.map +1 -1
  283. package/src/supply-chain/generated-container-release-command.js +849 -44
  284. package/src/supply-chain/generated-container-release-command.js.map +1 -1
  285. package/src/supply-chain/generated-ecr-collector-command.d.ts +0 -1
  286. package/src/supply-chain/generated-ecr-collector-command.js +188 -91
  287. package/src/supply-chain/generated-ecr-collector-command.js.map +1 -1
  288. package/src/supply-chain/generated-files.js +4 -3
  289. package/src/supply-chain/generated-files.js.map +1 -1
  290. package/src/supply-chain/index.d.ts +1 -1
  291. package/src/supply-chain/index.js +2 -1
  292. package/src/supply-chain/index.js.map +1 -1
  293. package/src/workload-roles/index.d.ts +10 -17
  294. package/src/workload-roles/index.js +5 -18
  295. package/src/workload-roles/index.js.map +1 -1
  296. package/src/workspace-deployment/identity.d.ts +3 -0
  297. package/src/workspace-deployment/identity.js +22 -0
  298. package/src/workspace-deployment/identity.js.map +1 -0
  299. package/src/workspace-deployment/index.d.ts +3 -0
  300. package/src/workspace-deployment/index.js +7 -0
  301. package/src/workspace-deployment/index.js.map +1 -0
  302. package/src/workspace-deployment/migration-evidence.d.ts +29 -0
  303. package/src/workspace-deployment/migration-evidence.js +83 -0
  304. package/src/workspace-deployment/migration-evidence.js.map +1 -0
  305. package/src/workspace-deployment/migration.d.ts +27 -0
  306. package/src/workspace-deployment/migration.js +109 -0
  307. package/src/workspace-deployment/migration.js.map +1 -0
  308. package/src/workspace-deployment/ownership.d.ts +81 -0
  309. package/src/workspace-deployment/ownership.js +381 -0
  310. package/src/workspace-deployment/ownership.js.map +1 -0
@@ -0,0 +1,1500 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.THIN_DEPLOY_WORKFLOW_PATH = void 0;
4
+ exports.renderThinDeployVerifyCaller = renderThinDeployVerifyCaller;
5
+ exports.exactStageDeployerGuardScript = exactStageDeployerGuardScript;
6
+ exports.canonicalStageAuthorityDerivationScript = canonicalStageAuthorityDerivationScript;
7
+ exports.exactStagingPromotionPullGuardScript = exactStagingPromotionPullGuardScript;
8
+ exports.stagingPromotionReceiptValidationScript = stagingPromotionReceiptValidationScript;
9
+ exports.restoredCandidateValidationScript = restoredCandidateValidationScript;
10
+ exports.stagingPromotionSealScript = stagingPromotionSealScript;
11
+ exports.postdeploySupplyChainBindingScript = postdeploySupplyChainBindingScript;
12
+ exports.renderThinDeployWorkflowFilePlan = renderThinDeployWorkflowFilePlan;
13
+ exports.THIN_DEPLOY_WORKFLOW_PATH = '.github/workflows/deploy.yml';
14
+ const EXACT_SEMVER = /^\d+\.\d+\.\d+$/;
15
+ const WORKSPACE_NAME = /^[a-z0-9][a-z0-9-]{0,62}$/;
16
+ const ACTION_PIN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+@[a-f0-9]{40}(?: # [^\r\n]+)?$/;
17
+ function assertOptions(options) {
18
+ if (!WORKSPACE_NAME.test(options.name)) {
19
+ throw new Error('Workspace name must be a bounded lowercase slug.');
20
+ }
21
+ for (const [name, value] of Object.entries(options.actionPins)) {
22
+ if (!ACTION_PIN.test(value)) {
23
+ throw new Error(`${name} must be an exact 40-character action pin.`);
24
+ }
25
+ }
26
+ for (const [name, value] of Object.entries(options.toolVersions)) {
27
+ if (!EXACT_SEMVER.test(value)) {
28
+ throw new Error(`${name} must be an exact semantic version.`);
29
+ }
30
+ }
31
+ }
32
+ function workflowCallTrigger(profile) {
33
+ return profile === 'starter'
34
+ ? ''
35
+ : ` workflow_call:
36
+ inputs:
37
+ source_branch:
38
+ required: true
39
+ type: string
40
+ source_commit:
41
+ required: true
42
+ type: string
43
+ source_ref:
44
+ required: true
45
+ type: string
46
+ verify_run_attempt:
47
+ required: true
48
+ type: string
49
+ verify_run_id:
50
+ required: true
51
+ type: string
52
+ `;
53
+ }
54
+ function renderThinDeployVerifyCaller(profile) {
55
+ if (profile === 'starter')
56
+ return '';
57
+ const protectedBranchCondition = "(github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main')";
58
+ return `
59
+
60
+ deploy:
61
+ name: deploy exact verified source
62
+ needs: [acceptance, release-candidate]
63
+ if: >-
64
+ always() &&
65
+ github.event_name == 'push' &&
66
+ ${protectedBranchCondition} &&
67
+ needs.acceptance.result == 'success' &&
68
+ needs.acceptance.outputs.history_only != 'true' &&
69
+ needs.release-candidate.result == 'success'
70
+ permissions:
71
+ actions: read
72
+ contents: read
73
+ id-token: write
74
+ uses: ./.github/workflows/deploy.yml
75
+ with:
76
+ source_branch: \${{ github.ref_name }}
77
+ source_commit: \${{ github.sha }}
78
+ source_ref: \${{ github.ref }}
79
+ verify_run_attempt: \${{ github.run_attempt }}
80
+ verify_run_id: \${{ github.run_id }}`;
81
+ }
82
+ function indentedScript(source, spaces) {
83
+ const prefix = ' '.repeat(spaces);
84
+ return source
85
+ .trim()
86
+ .split('\n')
87
+ .map((line) => `${prefix}${line}`)
88
+ .join('\n');
89
+ }
90
+ /** @internal Exported for executable source-contract tests. */
91
+ function exactStageDeployerGuardScript(operation, accountVariable) {
92
+ return `read -r caller_account caller_arn <<< "$(aws sts get-caller-identity --query '[Account,Arn]' --output text)"
93
+ [ "$caller_account" = "${accountVariable}" ] || {
94
+ echo '${operation} caller account differs from committed policy'; exit 1;
95
+ }
96
+ caller_partition="\${caller_arn#arn:}"
97
+ caller_partition="\${caller_partition%%:*}"
98
+ [ "$caller_partition" = "$AWS_PARTITION" ] || {
99
+ echo '${operation} caller partition differs from committed policy'; exit 1;
100
+ }
101
+ expected_role_arn="arn:$AWS_PARTITION:iam::${accountVariable}:role/ebk/$EBK_STAGE/deployment/ebk-$EBK_STAGE-deploy"
102
+ [ "$AWS_DEPLOY_ROLE_ARN" = "$expected_role_arn" ] || {
103
+ echo '${operation} configured role is not the canonical stage deployer'; exit 1;
104
+ }
105
+ expected_caller_prefix="arn:$AWS_PARTITION:sts::${accountVariable}:assumed-role/ebk-$EBK_STAGE-deploy/"
106
+ case "$caller_arn" in
107
+ "$expected_caller_prefix"?*) ;;
108
+ *) echo '${operation} caller is not the exact stage deployer'; exit 1 ;;
109
+ esac`;
110
+ }
111
+ /** @internal Shared verbatim by every generated protected workflow. */
112
+ function canonicalStageAuthorityDerivationScript() {
113
+ return `case "$EBK_STAGE" in
114
+ development) account_id="$EBK_DEVELOPMENT_ACCOUNT" ;;
115
+ staging) account_id="$EBK_STAGING_ACCOUNT" ;;
116
+ production) account_id="$EBK_PRODUCTION_ACCOUNT" ;;
117
+ *) exit 1 ;;
118
+ esac
119
+ case "$account_id" in *[!0-9]*|'') exit 1 ;; esac
120
+ [ "\${#account_id}" -eq 12 ]
121
+ case "$EBK_REGION" in
122
+ cn-*) partition=aws-cn; audience=sts.amazonaws.com.cn ;;
123
+ us-gov-*) partition=aws-us-gov; audience=sts.amazonaws.com ;;
124
+ us-iso-*|us-isob-*|eu-isoe-*|us-isof-*)
125
+ echo 'The committed Region belongs to an unsupported AWS partition.' >&2
126
+ exit 1
127
+ ;;
128
+ *) partition=aws; audience=sts.amazonaws.com ;;
129
+ esac
130
+ role_arn="arn:$partition:iam::$account_id:role/ebk/$EBK_STAGE/deployment/ebk-$EBK_STAGE-deploy"
131
+ echo "account_id=$account_id" >> "$GITHUB_OUTPUT"
132
+ echo "audience=$audience" >> "$GITHUB_OUTPUT"
133
+ echo "partition=$partition" >> "$GITHUB_OUTPUT"
134
+ echo "role_arn=$role_arn" >> "$GITHUB_OUTPUT"`;
135
+ }
136
+ /** @internal Exported for executable source-contract tests. */
137
+ function exactStagingPromotionPullGuardScript() {
138
+ return `read -r caller_account caller_arn <<< "$(aws sts get-caller-identity --query '[Account,Arn]' --output text)"
139
+ [ "$caller_account" = "$EBK_STAGING_ACCOUNT_ID" ] || {
140
+ echo 'Staging registry caller account differs from committed policy'; exit 1;
141
+ }
142
+ caller_partition="\${caller_arn#arn:}"
143
+ caller_partition="\${caller_partition%%:*}"
144
+ [ "$caller_partition" = "$AWS_PARTITION" ] || {
145
+ echo 'Staging registry caller partition differs from committed policy'; exit 1;
146
+ }
147
+ expected_role_arn="arn:$AWS_PARTITION:iam::$EBK_STAGING_ACCOUNT_ID:role/ebk/promotion/ebk-staging-promotion-pull"
148
+ [ "$AWS_STAGING_PROMOTION_PULL_ROLE_ARN" = "$expected_role_arn" ] || {
149
+ echo 'Staging registry configured role is not the canonical promotion pull role'; exit 1;
150
+ }
151
+ expected_caller_prefix="arn:$AWS_PARTITION:sts::$EBK_STAGING_ACCOUNT_ID:assumed-role/ebk-staging-promotion-pull/"
152
+ case "$caller_arn" in
153
+ "$expected_caller_prefix"?*) ;;
154
+ *) echo 'Staging registry caller is not the exact promotion pull role'; exit 1 ;;
155
+ esac`;
156
+ }
157
+ /** @internal Shared verbatim with the generated workflow and executable tests. */
158
+ function stagingPromotionReceiptValidationScript() {
159
+ return `import { createHash } from 'node:crypto';
160
+ import { appendFileSync, readFileSync } from 'node:fs';
161
+ import { join } from 'node:path';
162
+
163
+ const commit = /^[a-f0-9]{40,64}$/;
164
+ const integer = /^[1-9][0-9]*$/;
165
+ const digest = /^sha256:[a-f0-9]{64}$/;
166
+ const required = (name, pattern) => {
167
+ const value = process.env[name]?.trim() ?? '';
168
+ if (!pattern.test(value)) throw new Error(name + ' is absent or malformed.');
169
+ return value;
170
+ };
171
+ const root = process.env.EBK_STAGING_RECEIPT_ROOT?.trim() ?? '';
172
+ if (!root) throw new Error('Staging deployment receipt root is absent.');
173
+ const expectedHasSlices = required('EBK_EXPECTED_HAS_SLICES', /^(?:true|false)$/) === 'true';
174
+ const sourceCommit = required('EBK_STAGING_SOURCE_COMMIT', commit);
175
+ const expectedAccount = required('EBK_EXPECTED_STAGING_ACCOUNT_ID', /^[0-9]{12}$/);
176
+ const expectedProfile = required('EBK_EXPECTED_DEPLOYMENT_PROFILE', /^(?:production-isolated|fully-isolated)$/);
177
+ const expectedRegion = required('EBK_EXPECTED_DEPLOYMENT_REGION', /^[a-z]{2}(?:-gov)?-[a-z]+(?:-[a-z]+)*-[0-9]$/);
178
+ const deploymentRunId = required('EBK_STAGING_DEPLOYMENT_RUN_ID', integer);
179
+ const deploymentRunAttempt = required('EBK_STAGING_DEPLOYMENT_RUN_ATTEMPT', integer);
180
+ const receiptArtifactId = required('EBK_STAGING_RECEIPT_ARTIFACT_ID', integer);
181
+ const receiptArtifactDigest = required('EBK_STAGING_RECEIPT_ARTIFACT_DIGEST', digest);
182
+ const receiptBytes = readFileSync(join(root, 'latest.json'));
183
+ const promotionBytes = readFileSync(join(root, 'promotion.json'));
184
+ const receipt = JSON.parse(receiptBytes.toString('utf8'));
185
+ const promotion = JSON.parse(promotionBytes.toString('utf8'));
186
+ const receiptDigest = createHash('sha256').update(receiptBytes).digest('hex');
187
+ if (
188
+ receipt?.kind !== 'ebk-deployment-receipt' ||
189
+ receipt.schemaVersion !== 1 ||
190
+ receipt.outcome !== 'succeeded' ||
191
+ receipt.authority !== 'github' ||
192
+ receipt.stage !== 'staging' ||
193
+ receipt.account !== expectedAccount ||
194
+ receipt.profile !== expectedProfile ||
195
+ receipt.region !== expectedRegion ||
196
+ receipt.source?.clean !== true ||
197
+ receipt.source?.commit !== sourceCommit ||
198
+ promotion?.kind !== 'ebk-staging-deployment-promotion' ||
199
+ promotion.schemaVersion !== 1 ||
200
+ promotion.hasSlices !== expectedHasSlices ||
201
+ promotion.stage !== 'staging' ||
202
+ promotion.account !== expectedAccount ||
203
+ promotion.deploymentProfile !== expectedProfile ||
204
+ promotion.region !== expectedRegion ||
205
+ promotion.sourceCommit !== sourceCommit ||
206
+ promotion.deployment?.runId !== deploymentRunId ||
207
+ promotion.deployment?.runAttempt !== deploymentRunAttempt ||
208
+ promotion.deployment?.receiptDigest !== receiptDigest ||
209
+ (expectedHasSlices
210
+ ? !integer.test(promotion.candidate?.artifactId ?? '') ||
211
+ !digest.test(promotion.candidate?.artifactDigest ?? '') ||
212
+ !integer.test(promotion.candidate?.producerRunId ?? '') ||
213
+ !integer.test(promotion.candidate?.producerRunAttempt ?? '') ||
214
+ promotion.candidate?.sourceCommit !== sourceCommit ||
215
+ !/^[a-f0-9]{64}$/.test(promotion.candidate?.materialDigest ?? '') ||
216
+ !integer.test(promotion.container?.artifactId ?? '') ||
217
+ !digest.test(promotion.container?.artifactDigest ?? '')
218
+ : promotion.candidate !== null || promotion.container !== null)
219
+ ) {
220
+ throw new Error('Staging deployment receipt and candidate promotion binding disagree.');
221
+ }
222
+ const outputs = {
223
+ deployment_run_attempt: deploymentRunAttempt,
224
+ deployment_run_id: deploymentRunId,
225
+ receipt_artifact_digest: receiptArtifactDigest,
226
+ receipt_artifact_id: receiptArtifactId,
227
+ source_commit: sourceCommit,
228
+ };
229
+ if (expectedHasSlices) Object.assign(outputs, {
230
+ artifact_digest: promotion.candidate.artifactDigest,
231
+ artifact_id: promotion.candidate.artifactId,
232
+ candidate_material_digest: promotion.candidate.materialDigest,
233
+ container_artifact_digest: promotion.container.artifactDigest,
234
+ container_artifact_id: promotion.container.artifactId,
235
+ producer_run_attempt: promotion.candidate.producerRunAttempt,
236
+ producer_run_id: promotion.candidate.producerRunId,
237
+ });
238
+ for (const [name, value] of Object.entries(outputs)) appendFileSync(process.env.GITHUB_OUTPUT, name + '=' + value + '\\n');`;
239
+ }
240
+ /** @internal Shared verbatim with the generated workflow and executable tests. */
241
+ function restoredCandidateValidationScript() {
242
+ return `import { createHash } from 'node:crypto';
243
+ import { readFileSync, readdirSync } from 'node:fs';
244
+ import { join } from 'node:path';
245
+
246
+ const root = '.ebk/evidence/release-candidate/packages';
247
+ const sourceCommit = process.env.EBK_RELEASE_SOURCE_COMMIT?.trim() ?? '';
248
+ const producerRunId = process.env.EBK_CANDIDATE_PRODUCER_RUN_ID?.trim() ?? '';
249
+ const producerRunAttempt = process.env.EBK_CANDIDATE_PRODUCER_RUN_ATTEMPT?.trim() ?? '';
250
+ const expected = process.env.EBK_CANDIDATE_MATERIAL_DIGEST?.trim() ?? '';
251
+ if (!/^[a-f0-9]{40,64}$/.test(sourceCommit) || !/^[1-9][0-9]*$/.test(producerRunId) || !/^[1-9][0-9]*$/.test(producerRunAttempt) || !/^[a-f0-9]{64}$/.test(expected)) {
252
+ throw new Error('Restored candidate authority is absent or malformed.');
253
+ }
254
+ const projects = readdirSync(root, { withFileTypes: true })
255
+ .filter((entry) => entry.isDirectory())
256
+ .map((entry) => entry.name)
257
+ .sort();
258
+ if (projects.length === 0 || projects.some((project) => !/^[a-z0-9][a-z0-9-]{0,62}$/.test(project))) {
259
+ throw new Error('Restored candidate project inventory is absent or malformed.');
260
+ }
261
+ const entries = projects.map((project) => {
262
+ const bytes = readFileSync(join(root, project, 'candidate.json'));
263
+ const value = JSON.parse(bytes.toString('utf8'));
264
+ if (
265
+ value?.kind !== 'ebk-release-candidate' ||
266
+ value.schemaVersion !== 1 ||
267
+ value.slice !== project ||
268
+ value.sourceCommit !== sourceCommit ||
269
+ value.provenance?.runId !== producerRunId ||
270
+ value.provenance?.runAttempt !== producerRunAttempt
271
+ ) throw new Error('Restored candidate provenance is stale or cross-paired.');
272
+ return { manifestDigest: createHash('sha256').update(bytes).digest('hex'), project };
273
+ });
274
+ const observed = createHash('sha256').update(JSON.stringify(entries)).digest('hex');
275
+ if (observed !== expected) throw new Error('Restored candidate material digest differs from staging.');`;
276
+ }
277
+ /** @internal Shared verbatim with the generated workflow and executable tests. */
278
+ function stagingPromotionSealScript() {
279
+ return `import { createHash } from 'node:crypto';
280
+ import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
281
+ import { join } from 'node:path';
282
+
283
+ const required = (name, pattern) => {
284
+ const value = process.env[name]?.trim() ?? '';
285
+ if (!pattern.test(value)) throw new Error(name + ' is absent or malformed.');
286
+ return value;
287
+ };
288
+ const sourceCommit = required('EBK_RELEASE_SOURCE_COMMIT', /^[a-f0-9]{40,64}$/);
289
+ const hasSlices = required('EBK_HAS_SLICES', /^(?:true|false)$/) === 'true';
290
+ const account = required('EBK_POLICY_ACCOUNT_ID', /^[0-9]{12}$/);
291
+ const deploymentProfile = required('EBK_DEPLOYMENT_PROFILE', /^(?:production-isolated|fully-isolated)$/);
292
+ const region = required('EBK_POLICY_REGION', /^[a-z]{2}(?:-gov)?-[a-z]+(?:-[a-z]+)*-[0-9]$/);
293
+ const deploymentRunId = required('GITHUB_RUN_ID', /^[1-9][0-9]*$/);
294
+ const deploymentRunAttempt = required('GITHUB_RUN_ATTEMPT', /^[1-9][0-9]*$/);
295
+ const receiptPath = '.ebk/evidence/deployment/latest.json';
296
+ const receiptBytes = readFileSync(receiptPath);
297
+ const receipt = JSON.parse(receiptBytes.toString('utf8'));
298
+ if (receipt?.kind !== 'ebk-deployment-receipt' || receipt.schemaVersion !== 1 || receipt.outcome !== 'succeeded' || receipt.authority !== 'github' || receipt.stage !== 'staging' || receipt.account !== account || receipt.profile !== deploymentProfile || receipt.region !== region || receipt.source?.clean !== true || receipt.source?.commit !== sourceCommit) {
299
+ throw new Error('Successful staging deployment receipt is absent or stale.');
300
+ }
301
+ const candidateEnvironment = {
302
+ artifactDigest: process.env.EBK_CANDIDATE_ARTIFACT_DIGEST?.trim() ?? '',
303
+ artifactId: process.env.EBK_CANDIDATE_ARTIFACT_ID?.trim() ?? '',
304
+ producerRunAttempt: process.env.EBK_CANDIDATE_PRODUCER_RUN_ATTEMPT?.trim() ?? '',
305
+ producerRunId: process.env.EBK_CANDIDATE_PRODUCER_RUN_ID?.trim() ?? '',
306
+ };
307
+ const containerEnvironment = {
308
+ artifactDigest: process.env.EBK_CONTAINER_ARTIFACT_DIGEST?.trim() ?? '',
309
+ artifactId: process.env.EBK_CONTAINER_ARTIFACT_ID?.trim() ?? '',
310
+ };
311
+ let candidate = null;
312
+ let container = null;
313
+ if (hasSlices) {
314
+ const producerRunId = required('EBK_CANDIDATE_PRODUCER_RUN_ID', /^[1-9][0-9]*$/);
315
+ const producerRunAttempt = required('EBK_CANDIDATE_PRODUCER_RUN_ATTEMPT', /^[1-9][0-9]*$/);
316
+ const artifactId = required('EBK_CANDIDATE_ARTIFACT_ID', /^[1-9][0-9]*$/);
317
+ const artifactDigest = required('EBK_CANDIDATE_ARTIFACT_DIGEST', /^sha256:[a-f0-9]{64}$/);
318
+ const containerArtifactId = required('EBK_CONTAINER_ARTIFACT_ID', /^[1-9][0-9]*$/);
319
+ const containerArtifactDigest = required('EBK_CONTAINER_ARTIFACT_DIGEST', /^sha256:[a-f0-9]{64}$/);
320
+ const root = '.ebk/evidence/release-candidate/packages';
321
+ const projects = readdirSync(root, { withFileTypes: true })
322
+ .filter((entry) => entry.isDirectory())
323
+ .map((entry) => entry.name)
324
+ .sort();
325
+ if (projects.length === 0 || projects.some((project) => !/^[a-z0-9][a-z0-9-]{0,62}$/.test(project))) throw new Error('Staging candidate inventory is absent or malformed.');
326
+ const entries = projects.map((project) => {
327
+ const bytes = readFileSync(join(root, project, 'candidate.json'));
328
+ const value = JSON.parse(bytes.toString('utf8'));
329
+ if (value?.kind !== 'ebk-release-candidate' || value.schemaVersion !== 1 || value.slice !== project || value.sourceCommit !== sourceCommit || value.provenance?.runId !== producerRunId || value.provenance?.runAttempt !== producerRunAttempt) {
330
+ throw new Error('Staging candidate provenance is stale or cross-paired.');
331
+ }
332
+ return { manifestDigest: createHash('sha256').update(bytes).digest('hex'), project };
333
+ });
334
+ candidate = { artifactDigest, artifactId, materialDigest: createHash('sha256').update(JSON.stringify(entries)).digest('hex'), producerRunAttempt, producerRunId, sourceCommit };
335
+ container = { artifactDigest: containerArtifactDigest, artifactId: containerArtifactId };
336
+ } else if (Object.values(candidateEnvironment).some(Boolean) || Object.values(containerEnvironment).some(Boolean)) {
337
+ throw new Error('A staging promotion without Slices must not carry Slice release material.');
338
+ }
339
+ const promotion = {
340
+ account,
341
+ candidate,
342
+ container,
343
+ deploymentProfile,
344
+ deployment: { receiptDigest: createHash('sha256').update(receiptBytes).digest('hex'), runAttempt: deploymentRunAttempt, runId: deploymentRunId },
345
+ hasSlices,
346
+ kind: 'ebk-staging-deployment-promotion',
347
+ region,
348
+ schemaVersion: 1,
349
+ sourceCommit,
350
+ stage: 'staging',
351
+ };
352
+ mkdirSync('.ebk/evidence/deployment', { recursive: true });
353
+ writeFileSync('.ebk/evidence/deployment/promotion.json', JSON.stringify(promotion, null, 2) + '\\n', { mode: 0o600 });`;
354
+ }
355
+ /** @internal Shared verbatim with the generated workflow and executable tests. */
356
+ function postdeploySupplyChainBindingScript() {
357
+ return `import { createHash } from 'node:crypto';
358
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
359
+
360
+ const required = (name, pattern) => {
361
+ const value = process.env[name]?.trim() ?? '';
362
+ if (!pattern.test(value)) throw new Error(name + ' is absent or malformed.');
363
+ return value;
364
+ };
365
+ const canonical = (value) => {
366
+ if (value === null || typeof value !== 'object') return JSON.stringify(value);
367
+ if (Array.isArray(value)) return '[' + value.map(canonical).join(',') + ']';
368
+ return '{' + Object.entries(value)
369
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
370
+ .map(([key, child]) => JSON.stringify(key) + ':' + canonical(child))
371
+ .join(',') + '}';
372
+ };
373
+ const accountId = required('EBK_POLICY_ACCOUNT_ID', /^[0-9]{12}$/);
374
+ const deploymentCommit = required('EBK_SOURCE_COMMIT', /^[a-f0-9]{40,64}$/);
375
+ const profile = required('EBK_DEPLOYMENT_PROFILE', /^(?:starter|production-isolated|fully-isolated)$/);
376
+ const region = required('EBK_POLICY_REGION', /^[a-z]{2}(?:-gov)?-[a-z]+(?:-[a-z]+)*-[0-9]$/);
377
+ const releaseCandidateArtifactDigest = required('EBK_CANDIDATE_ARTIFACT_DIGEST', /^sha256:[a-f0-9]{64}$/);
378
+ const releaseCandidateArtifactId = required('EBK_CANDIDATE_ARTIFACT_ID', /^[1-9][0-9]*$/);
379
+ const releaseCandidateProducerRunAttempt = required('EBK_CANDIDATE_PRODUCER_RUN_ATTEMPT', /^[1-9][0-9]*$/);
380
+ const releaseCandidateProducerRunId = required('EBK_CANDIDATE_PRODUCER_RUN_ID', /^[1-9][0-9]*$/);
381
+ const releaseSourceCommit = required('EBK_RELEASE_SOURCE_COMMIT', /^[a-f0-9]{40,64}$/);
382
+ const runAttempt = required('GITHUB_RUN_ATTEMPT', /^[1-9][0-9]*$/);
383
+ const runId = required('GITHUB_RUN_ID', /^[1-9][0-9]*$/);
384
+ const stage = required('EBK_STAGE', /^(?:development|staging|production)$/);
385
+ const receipt = JSON.parse(readFileSync('.ebk/evidence/deployment/latest.json', 'utf8'));
386
+ if (
387
+ receipt?.kind !== 'ebk-deployment-receipt' ||
388
+ receipt.schemaVersion !== 1 ||
389
+ receipt.outcome !== 'succeeded' ||
390
+ receipt.authority !== 'github' ||
391
+ receipt.account !== accountId ||
392
+ receipt.profile !== profile ||
393
+ receipt.region !== region ||
394
+ receipt.stage !== stage ||
395
+ receipt.source?.clean !== true ||
396
+ receipt.source?.commit !== deploymentCommit
397
+ ) throw new Error('Successful deployment receipt is absent or cross-paired.');
398
+ const unsigned = {
399
+ accountId,
400
+ deploymentCommit,
401
+ kind: 'ebk-postdeploy-supply-chain-binding',
402
+ profile,
403
+ region,
404
+ releaseCandidateArtifactDigest,
405
+ releaseCandidateArtifactId,
406
+ releaseCandidateProducerRunAttempt,
407
+ releaseCandidateProducerRunId,
408
+ releaseSourceCommit,
409
+ runAttempt,
410
+ runId,
411
+ schemaVersion: 1,
412
+ stage,
413
+ };
414
+ const binding = {
415
+ ...unsigned,
416
+ materialDigest: createHash('sha256').update(canonical(unsigned)).digest('hex'),
417
+ };
418
+ mkdirSync('.ebk/evidence/deployment', { recursive: true, mode: 0o700 });
419
+ writeFileSync(
420
+ '.ebk/evidence/deployment/postdeploy-supply-chain.json',
421
+ JSON.stringify(binding, null, 2) + '\\n',
422
+ { flag: 'wx', mode: 0o600 },
423
+ );`;
424
+ }
425
+ function renderThinDeployWorkflow(options) {
426
+ const { actionPins, deploymentProfile, name, toolVersions } = options;
427
+ return `name: Deploy
428
+ run-name: Deploy \${{ inputs.source_commit || github.sha }}
429
+
430
+ on:
431
+ ${workflowCallTrigger(deploymentProfile)} workflow_dispatch:
432
+ inputs:
433
+ action:
434
+ description: Native lifecycle action
435
+ required: true
436
+ default: deploy
437
+ type: choice
438
+ options: [deploy, refresh, unlock, remove]
439
+ project:
440
+ description: Required registered project for unlock or remove recovery
441
+ required: false
442
+ type: string
443
+
444
+ concurrency:
445
+ group: protected-lifecycle-\${{ inputs.source_branch || github.ref_name }}
446
+ cancel-in-progress: false
447
+
448
+ permissions:
449
+ contents: read
450
+
451
+ env:
452
+ EBK_DEPLOYMENT_SOURCE_BRANCH: \${{ inputs.source_branch || github.ref_name }}
453
+ EBK_DEPLOYMENT_SOURCE_COMMIT: \${{ inputs.source_commit || github.sha }}
454
+ EBK_DEPLOYMENT_SOURCE_REF: \${{ inputs.source_ref || github.ref }}
455
+ EBK_EXPECTED_SST_VERSION: ${toolVersions.sst}
456
+
457
+ jobs:
458
+ prepare:
459
+ name: route protected native lifecycle
460
+ runs-on: ubuntu-latest
461
+ outputs:
462
+ action: \${{ steps.route.outputs.action }}
463
+ account_id: \${{ steps.account.outputs.account_id }}
464
+ has_slices: \${{ steps.policy.outputs.has_slices }}
465
+ ordered_projects: \${{ steps.policy.outputs.ordered_projects }}
466
+ oidc_audience: \${{ steps.account.outputs.audience }}
467
+ partition: \${{ steps.account.outputs.partition }}
468
+ profile: \${{ steps.policy.outputs.deployment_profile }}
469
+ project: \${{ steps.route.outputs.project }}
470
+ region: \${{ steps.policy.outputs.deployment_region }}
471
+ slice_projects: \${{ steps.policy.outputs.slice_projects }}
472
+ staging_account_id: \${{ steps.policy.outputs.staging_account_id }}
473
+ stage: \${{ steps.route.outputs.stage }}
474
+ stage_deploy_role_arn: \${{ steps.account.outputs.role_arn }}
475
+ steps:
476
+ - name: Verify exact protected branch tip before setup
477
+ env:
478
+ EBK_CONTEXT_REF: \${{ github.ref }}
479
+ EBK_CONTEXT_REF_NAME: \${{ github.ref_name }}
480
+ EBK_CONTEXT_RUN_ATTEMPT: \${{ github.run_attempt }}
481
+ EBK_CONTEXT_RUN_ID: \${{ github.run_id }}
482
+ EBK_CONTEXT_SHA: \${{ github.sha }}
483
+ EBK_INPUT_RUN_ATTEMPT: \${{ inputs.verify_run_attempt || '' }}
484
+ EBK_INPUT_RUN_ID: \${{ inputs.verify_run_id || '' }}
485
+ EBK_INPUT_SOURCE_BRANCH: \${{ inputs.source_branch || '' }}
486
+ EBK_INPUT_SOURCE_COMMIT: \${{ inputs.source_commit || '' }}
487
+ EBK_INPUT_SOURCE_REF: \${{ inputs.source_ref || '' }}
488
+ EBK_SOURCE_BRANCH: \${{ env.EBK_DEPLOYMENT_SOURCE_BRANCH }}
489
+ EBK_SOURCE_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
490
+ EBK_SOURCE_REF: \${{ env.EBK_DEPLOYMENT_SOURCE_REF }}
491
+ EBK_TRIGGER: \${{ github.event_name }}
492
+ GH_REPO: \${{ github.repository }}
493
+ GH_TOKEN: \${{ github.token }}
494
+ run: |
495
+ set -euo pipefail
496
+ case "$EBK_SOURCE_COMMIT" in ''|*[!0-9a-f]*) exit 1 ;; esac
497
+ { [ "\${#EBK_SOURCE_COMMIT}" -eq 40 ] || [ "\${#EBK_SOURCE_COMMIT}" -eq 64 ]; } || exit 1
498
+ case "$EBK_SOURCE_BRANCH:$EBK_SOURCE_REF" in
499
+ develop:refs/heads/develop|main:refs/heads/main) ;;
500
+ *) echo 'Protected lifecycle must bind one exact protected branch ref'; exit 1 ;;
501
+ esac
502
+ if [ -n "$EBK_INPUT_RUN_ID$EBK_INPUT_RUN_ATTEMPT$EBK_INPUT_SOURCE_BRANCH$EBK_INPUT_SOURCE_COMMIT$EBK_INPUT_SOURCE_REF" ]; then
503
+ [ "$EBK_TRIGGER" = push ] || { echo 'Automatic deployment requires one protected push Verify caller'; exit 1; }
504
+ [ -n "$EBK_INPUT_RUN_ID" ] && [ -n "$EBK_INPUT_RUN_ATTEMPT" ] &&
505
+ [ -n "$EBK_INPUT_SOURCE_BRANCH" ] && [ -n "$EBK_INPUT_SOURCE_COMMIT" ] && [ -n "$EBK_INPUT_SOURCE_REF" ] ||
506
+ { echo 'Automatic deployment caller binding is incomplete'; exit 1; }
507
+ [ "$EBK_INPUT_RUN_ID" = "$EBK_CONTEXT_RUN_ID" ] &&
508
+ [ "$EBK_INPUT_RUN_ATTEMPT" = "$EBK_CONTEXT_RUN_ATTEMPT" ] &&
509
+ [ "$EBK_INPUT_SOURCE_COMMIT" = "$EBK_CONTEXT_SHA" ] &&
510
+ [ "$EBK_INPUT_SOURCE_BRANCH" = "$EBK_CONTEXT_REF_NAME" ] &&
511
+ [ "$EBK_INPUT_SOURCE_REF" = "$EBK_CONTEXT_REF" ] ||
512
+ { echo 'Automatic deployment differs from its exact Verify caller'; exit 1; }
513
+ else
514
+ [ "$EBK_TRIGGER" = workflow_dispatch ] || { echo 'Manual deployment requires workflow dispatch'; exit 1; }
515
+ fi
516
+ branch_tip="$(gh api "/repos/$GH_REPO/git/ref/heads/$EBK_SOURCE_BRANCH" --jq '.object.sha')"
517
+ [ "$branch_tip" = "$EBK_SOURCE_COMMIT" ] || {
518
+ echo 'Deployment source is no longer the exact protected branch tip'; exit 1;
519
+ }
520
+ - name: Check out immutable source
521
+ uses: ${actionPins.checkout}
522
+ with:
523
+ fetch-depth: 0
524
+ persist-credentials: false
525
+ ref: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
526
+ - uses: ${actionPins.pnpmSetup}
527
+ - uses: ${actionPins.setupNode}
528
+ with:
529
+ node-version: ${toolVersions.node}
530
+ cache: pnpm
531
+ - run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
532
+ - id: route
533
+ name: Resolve protected stage and action
534
+ env:
535
+ EBK_DISPATCH_ACTION: \${{ inputs.action || '' }}
536
+ EBK_DISPATCH_PROJECT: \${{ inputs.project || '' }}
537
+ EBK_PROFILE: ${deploymentProfile}
538
+ EBK_SOURCE_BRANCH: \${{ env.EBK_DEPLOYMENT_SOURCE_BRANCH }}
539
+ EBK_SOURCE_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
540
+ EBK_TRIGGER: \${{ github.event_name }}
541
+ run: |
542
+ set -euo pipefail
543
+ action=deploy
544
+ project=
545
+ if [ "$EBK_TRIGGER" = workflow_dispatch ]; then
546
+ action="$EBK_DISPATCH_ACTION"
547
+ project="$EBK_DISPATCH_PROJECT"
548
+ fi
549
+ case "$EBK_SOURCE_COMMIT" in ''|*[!0-9a-f]*) exit 1 ;; esac
550
+ { [ "\${#EBK_SOURCE_COMMIT}" -eq 40 ] || [ "\${#EBK_SOURCE_COMMIT}" -eq 64 ]; } || exit 1
551
+ case "$action" in deploy|refresh|unlock|remove) ;; *) exit 1 ;; esac
552
+ case "$project" in ''|*[!A-Za-z0-9_-]*) [ -z "$project" ] || exit 1 ;; esac
553
+ case "$action" in
554
+ unlock|remove) [ -n "$project" ] || { echo 'unlock and remove require an explicit project'; exit 1; } ;;
555
+ deploy|refresh) [ -z "$project" ] || { echo 'deploy and refresh operate on the policy-derived project order'; exit 1; } ;;
556
+ esac
557
+ case "$EBK_SOURCE_BRANCH" in
558
+ main) stage=production ;;
559
+ develop)
560
+ if [ "$EBK_PROFILE" = starter ]; then
561
+ stage=development
562
+ else
563
+ stage=staging
564
+ fi
565
+ ;;
566
+ *) echo 'Protected lifecycle must run from develop or main'; exit 1 ;;
567
+ esac
568
+ echo "action=$action" >> "$GITHUB_OUTPUT"
569
+ echo "project=$project" >> "$GITHUB_OUTPUT"
570
+ echo "stage=$stage" >> "$GITHUB_OUTPUT"
571
+ - id: policy
572
+ name: Derive ordered projects from committed policy
573
+ env:
574
+ EBK_ACTION: \${{ steps.route.outputs.action }}
575
+ EBK_PROJECT: \${{ steps.route.outputs.project }}
576
+ EBK_STAGE: \${{ steps.route.outputs.stage }}
577
+ run: |
578
+ set -euo pipefail
579
+ args=("--action=$EBK_ACTION" "--stage=$EBK_STAGE")
580
+ if [ -n "$EBK_PROJECT" ]; then args+=("--project=$EBK_PROJECT"); fi
581
+ pnpm nx run ${name}:deployment-plan -- "\${args[@]}"
582
+ - name: Bind generated profile
583
+ env:
584
+ EBK_RENDERED_PROFILE: ${deploymentProfile}
585
+ EBK_REPOSITORY_PROFILE: \${{ steps.policy.outputs.deployment_profile }}
586
+ run: test "$EBK_RENDERED_PROFILE" = "$EBK_REPOSITORY_PROFILE"
587
+ - id: account
588
+ name: Derive canonical committed stage authority
589
+ env:
590
+ EBK_DEVELOPMENT_ACCOUNT: \${{ steps.policy.outputs.development_account_id }}
591
+ EBK_PRODUCTION_ACCOUNT: \${{ steps.policy.outputs.production_account_id }}
592
+ EBK_REGION: \${{ steps.policy.outputs.deployment_region }}
593
+ EBK_STAGE: \${{ steps.route.outputs.stage }}
594
+ EBK_STAGING_ACCOUNT: \${{ steps.policy.outputs.staging_account_id }}
595
+ run: |
596
+ ${indentedScript(`set -euo pipefail\n${canonicalStageAuthorityDerivationScript()}`, 10)}
597
+
598
+ immutable-artifacts:
599
+ name: prepare exact immutable release artifacts
600
+ needs: prepare
601
+ if: >-
602
+ needs.prepare.outputs.action == 'deploy' &&
603
+ (
604
+ needs.prepare.outputs.has_slices == 'true' ||
605
+ (needs.prepare.outputs.stage == 'production' && needs.prepare.outputs.profile != 'starter')
606
+ )
607
+ runs-on: ubuntu-latest
608
+ permissions:
609
+ actions: read
610
+ contents: read
611
+ outputs:
612
+ artifact_digest: \${{ steps.current.outputs.artifact_digest || steps.staging.outputs.artifact_digest }}
613
+ artifact_id: \${{ steps.current.outputs.artifact_id || steps.staging.outputs.artifact_id }}
614
+ candidate_material_digest: \${{ steps.staging.outputs.candidate_material_digest }}
615
+ container_artifact_digest: \${{ steps.staging.outputs.container_artifact_digest }}
616
+ container_artifact_id: \${{ steps.staging.outputs.container_artifact_id }}
617
+ staging_deployment_run_attempt: \${{ steps.staging.outputs.deployment_run_attempt }}
618
+ staging_deployment_run_id: \${{ steps.staging.outputs.deployment_run_id }}
619
+ producer_run_attempt: \${{ steps.current.outputs.producer_run_attempt || steps.staging.outputs.producer_run_attempt }}
620
+ producer_run_id: \${{ steps.current.outputs.producer_run_id || steps.staging.outputs.producer_run_id }}
621
+ source_commit: \${{ steps.current.outputs.source_commit || steps.staging.outputs.source_commit }}
622
+ steps:
623
+ - name: Check out immutable production ancestry
624
+ if: needs.prepare.outputs.stage == 'production' && needs.prepare.outputs.profile != 'starter'
625
+ uses: ${actionPins.checkout}
626
+ with:
627
+ fetch-depth: 0
628
+ persist-credentials: false
629
+ ref: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
630
+ - id: current
631
+ name: Select exact successful current-source CI candidate
632
+ if: >-
633
+ needs.prepare.outputs.has_slices == 'true' &&
634
+ (needs.prepare.outputs.stage != 'production' || needs.prepare.outputs.profile == 'starter')
635
+ env:
636
+ EBK_CALLER_RUN_ATTEMPT: \${{ inputs.verify_run_attempt || '' }}
637
+ EBK_CALLER_RUN_ID: \${{ inputs.verify_run_id || '' }}
638
+ EBK_SOURCE_BRANCH: \${{ env.EBK_DEPLOYMENT_SOURCE_BRANCH }}
639
+ EBK_SOURCE_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
640
+ GH_REPO: \${{ github.repository }}
641
+ GH_TOKEN: \${{ github.token }}
642
+ run: |
643
+ set -euo pipefail
644
+ if [ -n "$EBK_CALLER_RUN_ID" ]; then
645
+ [ -n "$EBK_CALLER_RUN_ATTEMPT" ] || exit 1
646
+ run_id="$EBK_CALLER_RUN_ID"
647
+ run_attempt="$EBK_CALLER_RUN_ATTEMPT"
648
+ else
649
+ runs="$(gh api "/repos/$GH_REPO/actions/workflows/ci.yml/runs?event=push&branch=$EBK_SOURCE_BRANCH&head_sha=$EBK_SOURCE_COMMIT&status=success&per_page=100")"
650
+ producer="$(printf '%s' "$runs" | jq -c --arg branch "$EBK_SOURCE_BRANCH" --arg source "$EBK_SOURCE_COMMIT" \
651
+ '[.workflow_runs[] | select(.head_sha == $source and .head_branch == $branch and .event == "push" and .conclusion == "success" and .path == ".github/workflows/ci.yml")] | if length == 1 then .[0] else empty end')"
652
+ [ -n "$producer" ] || { echo 'Exact successful CI candidate producer is absent or ambiguous'; exit 1; }
653
+ run_id="$(printf '%s' "$producer" | jq -r '.id // empty')"
654
+ run_attempt="$(printf '%s' "$producer" | jq -r '.run_attempt // empty')"
655
+ fi
656
+ artifacts="$(gh api "/repos/$GH_REPO/actions/runs/$run_id/artifacts?per_page=100")"
657
+ artifact="$(printf '%s' "$artifacts" | jq -c --arg source "$EBK_SOURCE_COMMIT" \
658
+ '[.artifacts[] | select(.name == "release-candidate-" + $source and .expired == false)] | if length == 1 then .[0] else empty end')"
659
+ [ -n "$artifact" ] || { echo 'Exact release candidate artifact is absent or ambiguous'; exit 1; }
660
+ artifact_id="$(printf '%s' "$artifact" | jq -r '.id // empty')"
661
+ artifact_digest="$(printf '%s' "$artifact" | jq -r '.digest // empty')"
662
+ case "$run_id:$run_attempt:$artifact_id" in *[!0-9:]*) exit 1 ;; esac
663
+ case "$artifact_digest" in sha256:????????????????????????????????????????????????????????????????) ;; *) exit 1 ;; esac
664
+ echo "artifact_digest=$artifact_digest" >> "$GITHUB_OUTPUT"
665
+ echo "artifact_id=$artifact_id" >> "$GITHUB_OUTPUT"
666
+ echo "producer_run_attempt=$run_attempt" >> "$GITHUB_OUTPUT"
667
+ echo "producer_run_id=$run_id" >> "$GITHUB_OUTPUT"
668
+ echo "source_commit=$EBK_SOURCE_COMMIT" >> "$GITHUB_OUTPUT"
669
+ - id: staging-receipt
670
+ name: Select exact successful staging deployment receipt
671
+ if: needs.prepare.outputs.stage == 'production' && needs.prepare.outputs.profile != 'starter'
672
+ env:
673
+ EBK_DEPLOYMENT_SOURCE_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
674
+ GH_REPO: \${{ github.repository }}
675
+ GH_TOKEN: \${{ github.token }}
676
+ run: |
677
+ set -euo pipefail
678
+ parents="$(git rev-list --parents -n 1 "$EBK_DEPLOYMENT_SOURCE_COMMIT")"
679
+ set -- $parents
680
+ [ "$#" -eq 3 ] || { echo 'Isolated production requires one exact two-parent promotion merge'; exit 1; }
681
+ [ "$1" = "$EBK_DEPLOYMENT_SOURCE_COMMIT" ] || exit 1
682
+ staging_source="$3"
683
+ git merge-base --is-ancestor "$staging_source" "$EBK_DEPLOYMENT_SOURCE_COMMIT" || {
684
+ echo 'Staging promotion source is not an ancestor of production'; exit 1;
685
+ }
686
+ [ "$(git rev-parse "$EBK_DEPLOYMENT_SOURCE_COMMIT^{tree}")" = "$(git rev-parse "$staging_source^{tree}")" ] || {
687
+ echo 'Production merge tree differs from the promoted staging source'; exit 1;
688
+ }
689
+ git show-ref --verify --quiet refs/remotes/origin/develop || {
690
+ echo 'Protected develop lineage is absent from the immutable checkout'; exit 1;
691
+ }
692
+ git rev-list --first-parent refs/remotes/origin/develop > "$RUNNER_TEMP/develop-first-parent"
693
+ grep -Fxq "$staging_source" "$RUNNER_TEMP/develop-first-parent" || {
694
+ echo 'Staging promotion source is absent from protected develop first-parent history'; exit 1;
695
+ }
696
+ runs="$(gh api "/repos/$GH_REPO/actions/workflows/ci.yml/runs?event=push&branch=develop&head_sha=$staging_source&status=success&per_page=100")"
697
+ producer="$(printf '%s' "$runs" | jq -c --arg source "$staging_source" \
698
+ '[.workflow_runs[] | select(.head_sha == $source and .head_branch == "develop" and .event == "push" and .conclusion == "success" and .path == ".github/workflows/ci.yml")] | if length == 1 then .[0] else empty end')"
699
+ [ -n "$producer" ] || { echo 'Exact successful staging Verify deployment is absent or ambiguous'; exit 1; }
700
+ run_id="$(printf '%s' "$producer" | jq -r '.id // empty')"
701
+ run_attempt="$(printf '%s' "$producer" | jq -r '.run_attempt // empty')"
702
+ artifacts="$(gh api "/repos/$GH_REPO/actions/runs/$run_id/artifacts?per_page=100")"
703
+ receipt_name="deployment-receipt-staging-$run_id-$run_attempt"
704
+ receipt="$(printf '%s' "$artifacts" | jq -c --arg name "$receipt_name" \
705
+ '[.artifacts[] | select(.name == $name and .expired == false)] | if length == 1 then .[0] else empty end')"
706
+ [ -n "$receipt" ] || { echo 'Exact successful staging deployment receipt is absent or ambiguous'; exit 1; }
707
+ artifact_id="$(printf '%s' "$receipt" | jq -r '.id // empty')"
708
+ artifact_digest="$(printf '%s' "$receipt" | jq -r '.digest // empty')"
709
+ case "$run_id:$run_attempt:$artifact_id" in *[!0-9:]*) exit 1 ;; esac
710
+ case "$artifact_digest" in sha256:????????????????????????????????????????????????????????????????) ;; *) exit 1 ;; esac
711
+ echo "artifact_digest=$artifact_digest" >> "$GITHUB_OUTPUT"
712
+ echo "artifact_id=$artifact_id" >> "$GITHUB_OUTPUT"
713
+ echo "run_attempt=$run_attempt" >> "$GITHUB_OUTPUT"
714
+ echo "run_id=$run_id" >> "$GITHUB_OUTPUT"
715
+ echo "source_commit=$staging_source" >> "$GITHUB_OUTPUT"
716
+ - name: Restore exact staging deployment receipt
717
+ if: needs.prepare.outputs.stage == 'production' && needs.prepare.outputs.profile != 'starter'
718
+ uses: ${actionPins.downloadArtifact}
719
+ with:
720
+ artifact-ids: \${{ steps.staging-receipt.outputs.artifact_id }}
721
+ github-token: \${{ github.token }}
722
+ path: \${{ runner.temp }}/staging-deployment-receipt
723
+ run-id: \${{ steps.staging-receipt.outputs.run_id }}
724
+ - id: staging
725
+ name: Bind exact staging deployment and release evidence
726
+ if: needs.prepare.outputs.stage == 'production' && needs.prepare.outputs.profile != 'starter'
727
+ env:
728
+ EBK_STAGING_DEPLOYMENT_RUN_ATTEMPT: \${{ steps.staging-receipt.outputs.run_attempt }}
729
+ EBK_STAGING_DEPLOYMENT_RUN_ID: \${{ steps.staging-receipt.outputs.run_id }}
730
+ EBK_EXPECTED_HAS_SLICES: \${{ needs.prepare.outputs.has_slices }}
731
+ EBK_EXPECTED_DEPLOYMENT_PROFILE: \${{ needs.prepare.outputs.profile }}
732
+ EBK_EXPECTED_DEPLOYMENT_REGION: \${{ needs.prepare.outputs.region }}
733
+ EBK_EXPECTED_STAGING_ACCOUNT_ID: \${{ needs.prepare.outputs.staging_account_id }}
734
+ EBK_STAGING_RECEIPT_ARTIFACT_DIGEST: \${{ steps.staging-receipt.outputs.artifact_digest }}
735
+ EBK_STAGING_RECEIPT_ARTIFACT_ID: \${{ steps.staging-receipt.outputs.artifact_id }}
736
+ EBK_STAGING_RECEIPT_ROOT: \${{ runner.temp }}/staging-deployment-receipt
737
+ EBK_STAGING_SOURCE_COMMIT: \${{ steps.staging-receipt.outputs.source_commit }}
738
+ GH_REPO: \${{ github.repository }}
739
+ GH_TOKEN: \${{ github.token }}
740
+ run: |
741
+ node --input-type=module <<'NODE'
742
+ ${indentedScript(stagingPromotionReceiptValidationScript(), 10)}
743
+ NODE
744
+ if [ "$EBK_EXPECTED_HAS_SLICES" = true ]; then
745
+ promotion="$EBK_STAGING_RECEIPT_ROOT/promotion.json"
746
+ candidate_run_id="$(jq -r '.candidate.producerRunId' "$promotion")"
747
+ candidate_run_attempt="$(jq -r '.candidate.producerRunAttempt' "$promotion")"
748
+ candidate_artifact_id="$(jq -r '.candidate.artifactId' "$promotion")"
749
+ candidate_artifact_digest="$(jq -r '.candidate.artifactDigest' "$promotion")"
750
+ container_artifact_id="$(jq -r '.container.artifactId' "$promotion")"
751
+ container_artifact_digest="$(jq -r '.container.artifactDigest' "$promotion")"
752
+ candidate_run="$(gh api "/repos/$GH_REPO/actions/runs/$candidate_run_id")"
753
+ printf '%s' "$candidate_run" | jq -e \
754
+ --arg source "$EBK_STAGING_SOURCE_COMMIT" \
755
+ --arg attempt "$candidate_run_attempt" \
756
+ '.head_sha == $source and .head_branch == "develop" and .event == "push" and .path == ".github/workflows/ci.yml" and .conclusion == "success" and (.run_attempt | tostring) == $attempt' >/dev/null
757
+ candidate_artifacts="$(gh api "/repos/$GH_REPO/actions/runs/$candidate_run_id/artifacts?per_page=100")"
758
+ printf '%s' "$candidate_artifacts" | jq -e \
759
+ --arg id "$candidate_artifact_id" \
760
+ --arg digest "$candidate_artifact_digest" \
761
+ --arg name "release-candidate-$EBK_STAGING_SOURCE_COMMIT" \
762
+ '[.artifacts[] | select((.id | tostring) == $id and .name == $name and .digest == $digest and .expired == false)] | length == 1' >/dev/null
763
+ staging_artifacts="$(gh api "/repos/$GH_REPO/actions/runs/$EBK_STAGING_DEPLOYMENT_RUN_ID/artifacts?per_page=100")"
764
+ printf '%s' "$staging_artifacts" | jq -e \
765
+ --arg id "$container_artifact_id" \
766
+ --arg digest "$container_artifact_digest" \
767
+ --arg name "container-release-staging-$EBK_STAGING_DEPLOYMENT_RUN_ID-$EBK_STAGING_DEPLOYMENT_RUN_ATTEMPT" \
768
+ '[.artifacts[] | select((.id | tostring) == $id and .name == $name and .digest == $digest and .expired == false)] | length == 1' >/dev/null
769
+ fi
770
+
771
+ container-platform:
772
+ name: native \${{ needs.prepare.outputs.stage }} OCI \${{ matrix.platform }}
773
+ needs: [prepare, immutable-artifacts]
774
+ if: >-
775
+ always() &&
776
+ needs.prepare.result == 'success' &&
777
+ needs.immutable-artifacts.result == 'success' &&
778
+ needs.prepare.outputs.action == 'deploy' &&
779
+ needs.prepare.outputs.has_slices == 'true' &&
780
+ (
781
+ needs.prepare.outputs.stage == 'staging' ||
782
+ (needs.prepare.outputs.stage == 'production' && needs.prepare.outputs.profile == 'starter')
783
+ )
784
+ strategy:
785
+ fail-fast: false
786
+ matrix:
787
+ include:
788
+ - architecture: amd64
789
+ platform: linux/amd64
790
+ runner: ubuntu-latest
791
+ target: container-release-amd64
792
+ - architecture: arm64
793
+ platform: linux/arm64
794
+ runner: ubuntu-24.04-arm
795
+ target: container-release-arm64
796
+ runs-on: \${{ matrix.runner }}
797
+ environment: \${{ needs.prepare.outputs.stage }}
798
+ permissions:
799
+ actions: read
800
+ contents: read
801
+ id-token: write
802
+ env:
803
+ AWS_DEPLOY_ROLE_ARN: \${{ needs.prepare.outputs.stage_deploy_role_arn }}
804
+ AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
805
+ AWS_PARTITION: \${{ needs.prepare.outputs.partition }}
806
+ AWS_REGION: \${{ vars.AWS_REGION }}
807
+ EBK_ACTION: deploy
808
+ EBK_DEPLOYMENT_PROFILE: \${{ needs.prepare.outputs.profile }}
809
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.prepare.outputs.account_id }}
810
+ EBK_POLICY_REGION: \${{ needs.prepare.outputs.region }}
811
+ EBK_RELEASE_SOURCE_COMMIT: \${{ needs.immutable-artifacts.outputs.source_commit }}
812
+ EBK_SOURCE_COMMIT: \${{ inputs.source_commit || github.sha }}
813
+ EBK_STAGE: \${{ needs.prepare.outputs.stage }}
814
+ EBK_SLICE_PROJECTS: \${{ needs.prepare.outputs.slice_projects }}
815
+ steps:
816
+ - name: Check out immutable container source
817
+ uses: ${actionPins.checkout}
818
+ env:
819
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
820
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
821
+ with:
822
+ fetch-depth: 0
823
+ persist-credentials: false
824
+ ref: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
825
+ - uses: ${actionPins.pnpmSetup}
826
+ env:
827
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
828
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
829
+ - uses: ${actionPins.setupNode}
830
+ env:
831
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
832
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
833
+ with:
834
+ node-version: ${toolVersions.node}
835
+ cache: pnpm
836
+ - env:
837
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
838
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
839
+ run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
840
+ - name: Verify managed workspace integrity before container authority
841
+ env:
842
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
843
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
844
+ run: pnpm nx run ${name}:workspace-health
845
+ - name: Restore exact immutable release candidate for container build
846
+ uses: ${actionPins.downloadArtifact}
847
+ env:
848
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
849
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
850
+ with:
851
+ artifact-ids: \${{ needs.immutable-artifacts.outputs.artifact_id }}
852
+ github-token: \${{ github.token }}
853
+ path: .ebk/evidence/release-candidate
854
+ run-id: \${{ needs.immutable-artifacts.outputs.producer_run_id }}
855
+ - name: Revalidate exact protected branch tip before OIDC
856
+ env:
857
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
858
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
859
+ EBK_SOURCE_BRANCH: \${{ env.EBK_DEPLOYMENT_SOURCE_BRANCH }}
860
+ EBK_SOURCE_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
861
+ GH_REPO: \${{ github.repository }}
862
+ GH_TOKEN: \${{ github.token }}
863
+ run: |
864
+ set -euo pipefail
865
+ [ "$(git rev-parse HEAD)" = "$EBK_SOURCE_COMMIT" ] || {
866
+ echo 'Deployment checkout differs from the exact protected source'; exit 1;
867
+ }
868
+ branch_tip="$(gh api "/repos/$GH_REPO/git/ref/heads/$EBK_SOURCE_BRANCH" --jq '.object.sha')"
869
+ [ "$branch_tip" = "$EBK_SOURCE_COMMIT" ] || {
870
+ echo 'Deployment source is no longer the exact protected branch tip'; exit 1;
871
+ }
872
+ - name: Assume the current stage deployment role for container build
873
+ uses: ${actionPins.configureAwsCredentials}
874
+ with:
875
+ audience: \${{ needs.prepare.outputs.oidc_audience }}
876
+ aws-region: \${{ vars.AWS_REGION }}
877
+ role-session-name: ebk-container-\${{ matrix.architecture }}-\${{ github.run_id }}-\${{ github.run_attempt }}
878
+ role-to-assume: \${{ env.AWS_DEPLOY_ROLE_ARN }}
879
+ - name: Verify exact container build authority
880
+ run: |
881
+ set -euo pipefail
882
+ [ "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ]
883
+ [ "$AWS_REGION" = "$EBK_POLICY_REGION" ]
884
+ [ "$(aws sts get-caller-identity --query Account --output text)" = "$EBK_POLICY_ACCOUNT_ID" ]
885
+ - name: Build and run exact native OCI artifacts
886
+ run: |
887
+ set -euo pipefail
888
+ ${indentedScript(exactStageDeployerGuardScript('Container build', '$EBK_POLICY_ACCOUNT_ID'), 10)}
889
+ IFS=',' read -r -a projects <<< "$EBK_SLICE_PROJECTS"
890
+ for project in "\${projects[@]}"; do
891
+ test -n "$project" || continue
892
+ pnpm nx run "$project:\${{ matrix.target }}"
893
+ done
894
+ - name: Retain native platform evidence
895
+ uses: ${actionPins.uploadArtifact}
896
+ with:
897
+ name: container-platform-\${{ needs.prepare.outputs.stage }}-\${{ matrix.architecture }}-\${{ github.run_id }}-\${{ github.run_attempt }}
898
+ if-no-files-found: error
899
+ include-hidden-files: true
900
+ retention-days: 30
901
+ path: .ebk/evidence/container-release/partials/\${{ needs.prepare.outputs.stage }}/*/\${{ matrix.architecture }}
902
+
903
+ container-assemble:
904
+ name: assemble \${{ needs.prepare.outputs.stage }} OCI indexes
905
+ needs: [prepare, immutable-artifacts, container-platform]
906
+ if: >-
907
+ always() &&
908
+ needs.prepare.result == 'success' &&
909
+ needs.immutable-artifacts.result == 'success' &&
910
+ needs.container-platform.result == 'success' &&
911
+ needs.prepare.outputs.action == 'deploy' &&
912
+ needs.prepare.outputs.has_slices == 'true'
913
+ runs-on: ubuntu-latest
914
+ environment: \${{ needs.prepare.outputs.stage }}
915
+ permissions:
916
+ actions: read
917
+ contents: read
918
+ id-token: write
919
+ outputs:
920
+ artifact_digest: \${{ steps.retain.outputs.artifact-digest }}
921
+ artifact_id: \${{ steps.retain.outputs.artifact-id }}
922
+ env:
923
+ AWS_DEPLOY_ROLE_ARN: \${{ needs.prepare.outputs.stage_deploy_role_arn }}
924
+ AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
925
+ AWS_PARTITION: \${{ needs.prepare.outputs.partition }}
926
+ AWS_REGION: \${{ vars.AWS_REGION }}
927
+ EBK_ACTION: deploy
928
+ EBK_DEPLOYMENT_PROFILE: \${{ needs.prepare.outputs.profile }}
929
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.prepare.outputs.account_id }}
930
+ EBK_POLICY_REGION: \${{ needs.prepare.outputs.region }}
931
+ EBK_RELEASE_SOURCE_COMMIT: \${{ needs.immutable-artifacts.outputs.source_commit }}
932
+ EBK_SOURCE_COMMIT: \${{ inputs.source_commit || github.sha }}
933
+ EBK_STAGE: \${{ needs.prepare.outputs.stage }}
934
+ EBK_SLICE_PROJECTS: \${{ needs.prepare.outputs.slice_projects }}
935
+ steps:
936
+ - name: Check out immutable container source
937
+ uses: ${actionPins.checkout}
938
+ env:
939
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
940
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
941
+ with:
942
+ fetch-depth: 0
943
+ persist-credentials: false
944
+ ref: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
945
+ - uses: ${actionPins.pnpmSetup}
946
+ env:
947
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
948
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
949
+ - uses: ${actionPins.setupNode}
950
+ env:
951
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
952
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
953
+ with:
954
+ node-version: ${toolVersions.node}
955
+ cache: pnpm
956
+ - env:
957
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
958
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
959
+ run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
960
+ - name: Verify managed workspace integrity before container authority
961
+ env:
962
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
963
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
964
+ run: pnpm nx run ${name}:workspace-health
965
+ - name: Restore exact immutable release candidate for container assembly
966
+ uses: ${actionPins.downloadArtifact}
967
+ env:
968
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
969
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
970
+ with:
971
+ artifact-ids: \${{ needs.immutable-artifacts.outputs.artifact_id }}
972
+ github-token: \${{ github.token }}
973
+ path: .ebk/evidence/release-candidate
974
+ run-id: \${{ needs.immutable-artifacts.outputs.producer_run_id }}
975
+ - name: Restore both native platform records
976
+ uses: ${actionPins.downloadArtifact}
977
+ env:
978
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
979
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
980
+ with:
981
+ pattern: container-platform-\${{ needs.prepare.outputs.stage }}-*-\${{ github.run_id }}-\${{ github.run_attempt }}
982
+ merge-multiple: true
983
+ path: .ebk/evidence/container-release/partials/\${{ needs.prepare.outputs.stage }}
984
+ - name: Revalidate exact protected branch tip before OIDC
985
+ env:
986
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
987
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
988
+ EBK_SOURCE_BRANCH: \${{ env.EBK_DEPLOYMENT_SOURCE_BRANCH }}
989
+ EBK_SOURCE_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
990
+ GH_REPO: \${{ github.repository }}
991
+ GH_TOKEN: \${{ github.token }}
992
+ run: |
993
+ set -euo pipefail
994
+ [ "$(git rev-parse HEAD)" = "$EBK_SOURCE_COMMIT" ] || {
995
+ echo 'Deployment checkout differs from the exact protected source'; exit 1;
996
+ }
997
+ branch_tip="$(gh api "/repos/$GH_REPO/git/ref/heads/$EBK_SOURCE_BRANCH" --jq '.object.sha')"
998
+ [ "$branch_tip" = "$EBK_SOURCE_COMMIT" ] || {
999
+ echo 'Deployment source is no longer the exact protected branch tip'; exit 1;
1000
+ }
1001
+ - name: Assume the current stage deployment role for container assembly
1002
+ uses: ${actionPins.configureAwsCredentials}
1003
+ with:
1004
+ audience: \${{ needs.prepare.outputs.oidc_audience }}
1005
+ aws-region: \${{ vars.AWS_REGION }}
1006
+ role-session-name: ebk-container-assemble-\${{ github.run_id }}-\${{ github.run_attempt }}
1007
+ role-to-assume: \${{ env.AWS_DEPLOY_ROLE_ARN }}
1008
+ - name: Verify exact container assembly authority
1009
+ run: |
1010
+ set -euo pipefail
1011
+ [ "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ]
1012
+ [ "$AWS_REGION" = "$EBK_POLICY_REGION" ]
1013
+ [ "$(aws sts get-caller-identity --query Account --output text)" = "$EBK_POLICY_ACCOUNT_ID" ]
1014
+ - name: Assemble exact dual-architecture indexes
1015
+ run: |
1016
+ set -euo pipefail
1017
+ ${indentedScript(exactStageDeployerGuardScript('Container assembly', '$EBK_POLICY_ACCOUNT_ID'), 10)}
1018
+ IFS=',' read -r -a projects <<< "$EBK_SLICE_PROJECTS"
1019
+ for project in "\${projects[@]}"; do
1020
+ test -n "$project" || continue
1021
+ pnpm nx run "$project:container-release-assemble"
1022
+ binding=".ebk/evidence/container-release/$EBK_STAGE/packages/$project/binding.json"
1023
+ sidecar=".ebk/evidence/container-release/$EBK_STAGE/packages/$project/release-bound-binding.json"
1024
+ test -f "$binding" && test ! -e "$sidecar"
1025
+ cp "$binding" "$sidecar"
1026
+ cmp "$binding" "$sidecar"
1027
+ done
1028
+ - id: retain
1029
+ name: Retain exact predeploy container bundle
1030
+ uses: ${actionPins.uploadArtifact}
1031
+ with:
1032
+ name: container-release-\${{ needs.prepare.outputs.stage }}-\${{ github.run_id }}-\${{ github.run_attempt }}
1033
+ if-no-files-found: error
1034
+ include-hidden-files: true
1035
+ retention-days: 30
1036
+ path: .ebk/evidence/container-release
1037
+
1038
+ container-promote-production:
1039
+ name: promote reviewed OCI indexes to production
1040
+ needs: [prepare, immutable-artifacts]
1041
+ if: >-
1042
+ always() &&
1043
+ needs.prepare.result == 'success' &&
1044
+ needs.immutable-artifacts.result == 'success' &&
1045
+ needs.prepare.outputs.action == 'deploy' &&
1046
+ needs.prepare.outputs.stage == 'production' &&
1047
+ needs.prepare.outputs.profile != 'starter' &&
1048
+ needs.prepare.outputs.has_slices == 'true'
1049
+ runs-on: ubuntu-latest
1050
+ environment: production
1051
+ permissions:
1052
+ actions: read
1053
+ contents: read
1054
+ id-token: write
1055
+ outputs:
1056
+ artifact_digest: \${{ steps.retain.outputs.artifact-digest }}
1057
+ artifact_id: \${{ steps.retain.outputs.artifact-id }}
1058
+ env:
1059
+ AWS_DEPLOY_ROLE_ARN: \${{ needs.prepare.outputs.stage_deploy_role_arn }}
1060
+ AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
1061
+ AWS_PARTITION: \${{ needs.prepare.outputs.partition }}
1062
+ AWS_REGION: \${{ vars.AWS_REGION }}
1063
+ EBK_ACTION: deploy
1064
+ EBK_DEPLOYMENT_PROFILE: \${{ needs.prepare.outputs.profile }}
1065
+ EBK_POLICY_PRODUCTION_ACCOUNT_ID: \${{ needs.prepare.outputs.account_id }}
1066
+ EBK_POLICY_REGION: \${{ needs.prepare.outputs.region }}
1067
+ EBK_POLICY_STAGING_ACCOUNT_ID: \${{ needs.prepare.outputs.staging_account_id }}
1068
+ EBK_RELEASE_SOURCE_COMMIT: \${{ needs.immutable-artifacts.outputs.source_commit }}
1069
+ EBK_SOURCE_COMMIT: \${{ inputs.source_commit || github.sha }}
1070
+ EBK_STAGE: production
1071
+ EBK_SLICE_PROJECTS: \${{ needs.prepare.outputs.slice_projects }}
1072
+ steps:
1073
+ - name: Check out immutable production source
1074
+ uses: ${actionPins.checkout}
1075
+ env:
1076
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1077
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1078
+ with:
1079
+ fetch-depth: 0
1080
+ persist-credentials: false
1081
+ ref: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
1082
+ - uses: ${actionPins.pnpmSetup}
1083
+ env:
1084
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1085
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1086
+ - uses: ${actionPins.setupNode}
1087
+ env:
1088
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1089
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1090
+ with:
1091
+ node-version: ${toolVersions.node}
1092
+ cache: pnpm
1093
+ - env:
1094
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1095
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1096
+ run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
1097
+ - name: Verify managed workspace integrity before container authority
1098
+ env:
1099
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1100
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1101
+ run: pnpm nx run ${name}:workspace-health
1102
+ - name: Restore exact immutable release candidate for promotion
1103
+ uses: ${actionPins.downloadArtifact}
1104
+ env:
1105
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1106
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1107
+ with:
1108
+ artifact-ids: \${{ needs.immutable-artifacts.outputs.artifact_id }}
1109
+ github-token: \${{ github.token }}
1110
+ path: .ebk/evidence/release-candidate
1111
+ run-id: \${{ needs.immutable-artifacts.outputs.producer_run_id }}
1112
+ - name: Restore exact staging container bundle
1113
+ uses: ${actionPins.downloadArtifact}
1114
+ env:
1115
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1116
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1117
+ with:
1118
+ artifact-ids: \${{ needs.immutable-artifacts.outputs.container_artifact_id }}
1119
+ github-token: \${{ github.token }}
1120
+ path: .ebk/evidence/container-release
1121
+ run-id: \${{ needs.immutable-artifacts.outputs.staging_deployment_run_id }}
1122
+ - name: Revalidate exact protected branch tip before OIDC
1123
+ env:
1124
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1125
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1126
+ EBK_SOURCE_BRANCH: \${{ env.EBK_DEPLOYMENT_SOURCE_BRANCH }}
1127
+ EBK_SOURCE_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
1128
+ GH_REPO: \${{ github.repository }}
1129
+ GH_TOKEN: \${{ github.token }}
1130
+ run: |
1131
+ set -euo pipefail
1132
+ [ "$(git rev-parse HEAD)" = "$EBK_SOURCE_COMMIT" ] || {
1133
+ echo 'Deployment checkout differs from the exact protected source'; exit 1;
1134
+ }
1135
+ branch_tip="$(gh api "/repos/$GH_REPO/git/ref/heads/$EBK_SOURCE_BRANCH" --jq '.object.sha')"
1136
+ [ "$branch_tip" = "$EBK_SOURCE_COMMIT" ] || {
1137
+ echo 'Deployment source is no longer the exact protected branch tip'; exit 1;
1138
+ }
1139
+ - name: Assume exact staging registry pull role
1140
+ uses: ${actionPins.configureAwsCredentials}
1141
+ with:
1142
+ audience: \${{ needs.prepare.outputs.oidc_audience }}
1143
+ aws-region: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
1144
+ role-session-name: ebk-staging-container-pull-\${{ github.run_id }}-\${{ github.run_attempt }}
1145
+ role-to-assume: \${{ secrets.AWS_STAGING_PROMOTION_PULL_ROLE_ARN }}
1146
+ - name: Authenticate exact staging source registry
1147
+ env:
1148
+ AWS_STAGING_PROMOTION_PULL_ROLE_ARN: \${{ secrets.AWS_STAGING_PROMOTION_PULL_ROLE_ARN }}
1149
+ EBK_STAGING_ACCOUNT_ID: \${{ vars.AWS_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID }}
1150
+ EBK_STAGING_REGION: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
1151
+ run: |
1152
+ set -euo pipefail
1153
+ [ "$EBK_STAGING_ACCOUNT_ID" = "$EBK_POLICY_STAGING_ACCOUNT_ID" ]
1154
+ [ "$EBK_STAGING_REGION" = "$EBK_POLICY_REGION" ]
1155
+ ${indentedScript(exactStagingPromotionPullGuardScript(), 10)}
1156
+ [ "$EBK_STAGING_ACCOUNT_ID" != "$AWS_EXPECTED_ACCOUNT_ID" ]
1157
+ registry_suffix=.com
1158
+ case "$EBK_STAGING_REGION" in cn-*) registry_suffix=.com.cn ;; esac
1159
+ aws ecr get-login-password --region "$EBK_STAGING_REGION" |
1160
+ docker login --username AWS --password-stdin \
1161
+ "$EBK_STAGING_ACCOUNT_ID.dkr.ecr.$EBK_STAGING_REGION.amazonaws$registry_suffix"
1162
+ - name: Revalidate exact protected branch tip before OIDC
1163
+ env:
1164
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1165
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1166
+ EBK_SOURCE_BRANCH: \${{ env.EBK_DEPLOYMENT_SOURCE_BRANCH }}
1167
+ EBK_SOURCE_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
1168
+ GH_REPO: \${{ github.repository }}
1169
+ GH_TOKEN: \${{ github.token }}
1170
+ run: |
1171
+ set -euo pipefail
1172
+ [ "$(git rev-parse HEAD)" = "$EBK_SOURCE_COMMIT" ] || {
1173
+ echo 'Deployment checkout differs from the exact protected source'; exit 1;
1174
+ }
1175
+ branch_tip="$(gh api "/repos/$GH_REPO/git/ref/heads/$EBK_SOURCE_BRANCH" --jq '.object.sha')"
1176
+ [ "$branch_tip" = "$EBK_SOURCE_COMMIT" ] || {
1177
+ echo 'Deployment source is no longer the exact protected branch tip'; exit 1;
1178
+ }
1179
+ - name: Assume the production deployment role for container promotion
1180
+ uses: ${actionPins.configureAwsCredentials}
1181
+ with:
1182
+ audience: \${{ needs.prepare.outputs.oidc_audience }}
1183
+ aws-region: \${{ vars.AWS_REGION }}
1184
+ role-session-name: ebk-production-container-push-\${{ github.run_id }}-\${{ github.run_attempt }}
1185
+ role-to-assume: \${{ env.AWS_DEPLOY_ROLE_ARN }}
1186
+ - name: Promote exact staging indexes without rebuilding
1187
+ env:
1188
+ EBK_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID }}
1189
+ EBK_STAGING_PROMOTION_REGION: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
1190
+ run: |
1191
+ set -euo pipefail
1192
+ [ "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_PRODUCTION_ACCOUNT_ID" ]
1193
+ [ "$AWS_REGION" = "$EBK_POLICY_REGION" ]
1194
+ ${indentedScript(exactStageDeployerGuardScript('Container promotion', '$AWS_EXPECTED_ACCOUNT_ID'), 10)}
1195
+ IFS=',' read -r -a projects <<< "$EBK_SLICE_PROJECTS"
1196
+ for project in "\${projects[@]}"; do
1197
+ test -n "$project" || continue
1198
+ export EBK_CONTAINER_PROMOTION_SOURCE_BINDING_PATH="../../.ebk/evidence/container-release/staging/packages/$project/binding.json"
1199
+ export EBK_CONTAINER_PROMOTION_SOURCE_SIDECAR_PATH="../../.ebk/evidence/container-release/staging/packages/$project/release-bound-binding.json"
1200
+ pnpm nx run "$project:container-release-promote"
1201
+ done
1202
+ - id: retain
1203
+ name: Retain production target container binding
1204
+ uses: ${actionPins.uploadArtifact}
1205
+ with:
1206
+ name: container-release-production-\${{ github.run_id }}-\${{ github.run_attempt }}
1207
+ if-no-files-found: error
1208
+ include-hidden-files: true
1209
+ retention-days: 90
1210
+ path: .ebk/evidence/container-release
1211
+
1212
+ execute:
1213
+ name: native \${{ needs.prepare.outputs.action }} \${{ needs.prepare.outputs.stage }}
1214
+ needs: [prepare, immutable-artifacts, container-assemble, container-promote-production]
1215
+ if: >-
1216
+ always() &&
1217
+ needs.prepare.result == 'success' &&
1218
+ (
1219
+ needs.prepare.outputs.action != 'deploy' ||
1220
+ (
1221
+ needs.prepare.outputs.has_slices != 'true' &&
1222
+ (needs.prepare.outputs.stage != 'production' || needs.prepare.outputs.profile == 'starter')
1223
+ ) || (
1224
+ needs.immutable-artifacts.result == 'success' &&
1225
+ (
1226
+ needs.prepare.outputs.has_slices != 'true' ||
1227
+ needs.prepare.outputs.stage == 'development' ||
1228
+ (
1229
+ (
1230
+ needs.prepare.outputs.stage == 'staging' ||
1231
+ (needs.prepare.outputs.stage == 'production' && needs.prepare.outputs.profile == 'starter')
1232
+ ) &&
1233
+ needs.container-assemble.result == 'success'
1234
+ ) ||
1235
+ (
1236
+ needs.prepare.outputs.stage == 'production' &&
1237
+ needs.prepare.outputs.profile != 'starter' &&
1238
+ needs.container-promote-production.result == 'success'
1239
+ )
1240
+ )
1241
+ )
1242
+ )
1243
+ runs-on: ubuntu-latest
1244
+ environment: \${{ needs.prepare.outputs.stage }}
1245
+ permissions:
1246
+ actions: read
1247
+ contents: read
1248
+ id-token: write
1249
+ env:
1250
+ AWS_DEPLOY_ROLE_ARN: \${{ needs.prepare.outputs.stage_deploy_role_arn }}
1251
+ AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
1252
+ AWS_PARTITION: \${{ needs.prepare.outputs.partition }}
1253
+ AWS_REGION: \${{ vars.AWS_REGION }}
1254
+ EBK_ACTION: \${{ needs.prepare.outputs.action }}
1255
+ EBK_ALLOW_PRODUCTION_REMOVE: \${{ vars.EBK_ALLOW_PRODUCTION_REMOVE }}
1256
+ EBK_CANDIDATE_ARTIFACT_DIGEST: \${{ needs.immutable-artifacts.outputs.artifact_digest }}
1257
+ EBK_CANDIDATE_ARTIFACT_ID: \${{ needs.immutable-artifacts.outputs.artifact_id }}
1258
+ EBK_CANDIDATE_MATERIAL_DIGEST: \${{ needs.immutable-artifacts.outputs.candidate_material_digest }}
1259
+ EBK_CANDIDATE_PRODUCER_RUN_ATTEMPT: \${{ needs.immutable-artifacts.outputs.producer_run_attempt }}
1260
+ EBK_CANDIDATE_PRODUCER_RUN_ID: \${{ needs.immutable-artifacts.outputs.producer_run_id }}
1261
+ EBK_DEPLOYMENT_PROFILE: \${{ needs.prepare.outputs.profile }}
1262
+ EBK_ORDERED_PROJECTS: \${{ needs.prepare.outputs.ordered_projects }}
1263
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.prepare.outputs.account_id }}
1264
+ EBK_POLICY_REGION: \${{ needs.prepare.outputs.region }}
1265
+ EBK_RELEASE_SOURCE_COMMIT: \${{ needs.immutable-artifacts.outputs.source_commit || inputs.source_commit || github.sha }}
1266
+ EBK_SLICE_PROJECTS: \${{ needs.prepare.outputs.slice_projects }}
1267
+ EBK_SOURCE_COMMIT: \${{ inputs.source_commit || github.sha }}
1268
+ EBK_STAGE: \${{ needs.prepare.outputs.stage }}
1269
+ steps:
1270
+ - name: Check out immutable deployment source
1271
+ uses: ${actionPins.checkout}
1272
+ env:
1273
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1274
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1275
+ with:
1276
+ fetch-depth: 0
1277
+ persist-credentials: false
1278
+ ref: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
1279
+ - uses: ${actionPins.pnpmSetup}
1280
+ env:
1281
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1282
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1283
+ - uses: ${actionPins.setupNode}
1284
+ env:
1285
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1286
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1287
+ with:
1288
+ node-version: ${toolVersions.node}
1289
+ cache: pnpm
1290
+ - env:
1291
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1292
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1293
+ run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
1294
+ - name: Verify managed workspace integrity before AWS authority
1295
+ env:
1296
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1297
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1298
+ run: pnpm nx run ${name}:workspace-health
1299
+ - name: Restore exact immutable release candidate
1300
+ if: needs.prepare.outputs.action == 'deploy' && needs.prepare.outputs.has_slices == 'true'
1301
+ uses: ${actionPins.downloadArtifact}
1302
+ env:
1303
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1304
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1305
+ with:
1306
+ artifact-ids: \${{ needs.immutable-artifacts.outputs.artifact_id }}
1307
+ github-token: \${{ github.token }}
1308
+ path: .ebk/evidence/release-candidate
1309
+ run-id: \${{ needs.immutable-artifacts.outputs.producer_run_id }}
1310
+ - name: Restore exact current-stage container binding
1311
+ if: >-
1312
+ needs.prepare.outputs.action == 'deploy' &&
1313
+ needs.prepare.outputs.has_slices == 'true' &&
1314
+ (
1315
+ needs.prepare.outputs.stage == 'staging' ||
1316
+ (needs.prepare.outputs.stage == 'production' && needs.prepare.outputs.profile == 'starter')
1317
+ )
1318
+ uses: ${actionPins.downloadArtifact}
1319
+ env:
1320
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1321
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1322
+ with:
1323
+ name: container-release-\${{ needs.prepare.outputs.stage }}-\${{ github.run_id }}-\${{ github.run_attempt }}
1324
+ path: .ebk/evidence/container-release
1325
+ - name: Restore exact promoted production container binding
1326
+ if: >-
1327
+ needs.prepare.outputs.action == 'deploy' &&
1328
+ needs.prepare.outputs.has_slices == 'true' &&
1329
+ needs.prepare.outputs.stage == 'production' &&
1330
+ needs.prepare.outputs.profile != 'starter'
1331
+ uses: ${actionPins.downloadArtifact}
1332
+ env:
1333
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1334
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1335
+ with:
1336
+ name: container-release-production-\${{ github.run_id }}-\${{ github.run_attempt }}
1337
+ path: .ebk/evidence/container-release
1338
+ - name: Verify exact predeploy container binding inventory
1339
+ if: >-
1340
+ needs.prepare.outputs.action == 'deploy' &&
1341
+ needs.prepare.outputs.has_slices == 'true' &&
1342
+ needs.prepare.outputs.stage != 'development'
1343
+ env:
1344
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1345
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1346
+ run: |
1347
+ set -euo pipefail
1348
+ IFS=',' read -r -a projects <<< "$EBK_SLICE_PROJECTS"
1349
+ for project in "\${projects[@]}"; do
1350
+ test -n "$project" || continue
1351
+ test -f ".ebk/evidence/container-release/$EBK_STAGE/packages/$project/binding.json" || {
1352
+ echo "Exact predeploy container binding is absent for $project"
1353
+ exit 1
1354
+ }
1355
+ done
1356
+ - name: Verify exact restored staging-tested release candidate
1357
+ if: env.EBK_ACTION == 'deploy' && env.EBK_STAGE == 'production' && needs.prepare.outputs.profile != 'starter' && needs.prepare.outputs.has_slices == 'true'
1358
+ env:
1359
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1360
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1361
+ run: |
1362
+ node --input-type=module <<'NODE'
1363
+ ${indentedScript(restoredCandidateValidationScript(), 10)}
1364
+ NODE
1365
+ - name: Authorize deployed Blueprint trust before protected mutation
1366
+ if: env.EBK_ACTION == 'deploy'
1367
+ env:
1368
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1369
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1370
+ EBK_PROTECTED_BLUEPRINT_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
1371
+ run: |
1372
+ set -euo pipefail
1373
+ case "$EBK_PROTECTED_BLUEPRINT_COMMIT" in
1374
+ ''|*[!0-9a-f]*) echo 'Protected Blueprint authority must be one full lowercase Git object ID'; exit 1 ;;
1375
+ esac
1376
+ test "\${#EBK_PROTECTED_BLUEPRINT_COMMIT}" -eq 40 ||
1377
+ test "\${#EBK_PROTECTED_BLUEPRINT_COMMIT}" -eq 64 ||
1378
+ { echo 'Protected Blueprint authority must be one full Git object ID'; exit 1; }
1379
+ test "$(git rev-parse HEAD)" = "$EBK_PROTECTED_BLUEPRINT_COMMIT" ||
1380
+ { echo 'Protected Blueprint authority differs from the exact workflow checkout'; exit 1; }
1381
+ case "$EBK_STAGE" in
1382
+ staging) protected_branch=develop ;;
1383
+ production) protected_branch=main ;;
1384
+ *) exit 1 ;;
1385
+ esac
1386
+ node tools/blueprint-trust-check.mjs \
1387
+ --operation=deploy \
1388
+ --protected-branch="$protected_branch" \
1389
+ --protected-ref="$EBK_PROTECTED_BLUEPRINT_COMMIT" \
1390
+ --output=.ebk/evidence/supply-chain/blueprint-trust.json
1391
+ - name: Produce subject-bound protected scanner evidence
1392
+ if: env.EBK_ACTION == 'deploy'
1393
+ env:
1394
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1395
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1396
+ run: pnpm nx run ${name}:supply-chain-scan
1397
+ - name: Enforce protected supply-chain policy before mutation
1398
+ if: env.EBK_ACTION == 'deploy'
1399
+ env:
1400
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1401
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1402
+ run: pnpm nx run ${name}:supply-chain-check-protected
1403
+ - name: Revalidate exact protected branch tip before OIDC
1404
+ env:
1405
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1406
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1407
+ EBK_SOURCE_BRANCH: \${{ env.EBK_DEPLOYMENT_SOURCE_BRANCH }}
1408
+ EBK_SOURCE_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
1409
+ GH_REPO: \${{ github.repository }}
1410
+ GH_TOKEN: \${{ github.token }}
1411
+ run: |
1412
+ set -euo pipefail
1413
+ [ "$(git rev-parse HEAD)" = "$EBK_SOURCE_COMMIT" ] || {
1414
+ echo 'Deployment checkout differs from the exact protected source'; exit 1;
1415
+ }
1416
+ branch_tip="$(gh api "/repos/$GH_REPO/git/ref/heads/$EBK_SOURCE_BRANCH" --jq '.object.sha')"
1417
+ [ "$branch_tip" = "$EBK_SOURCE_COMMIT" ] || {
1418
+ echo 'Deployment source is no longer the exact protected branch tip'; exit 1;
1419
+ }
1420
+ - name: Assume the one stage deployment role
1421
+ uses: ${actionPins.configureAwsCredentials}
1422
+ with:
1423
+ audience: \${{ needs.prepare.outputs.oidc_audience }}
1424
+ aws-region: \${{ vars.AWS_REGION }}
1425
+ role-session-name: ebk-native-\${{ github.run_id }}-\${{ github.run_attempt }}
1426
+ role-to-assume: \${{ env.AWS_DEPLOY_ROLE_ARN }}
1427
+ - name: Verify exact stage authority
1428
+ run: |
1429
+ set -euo pipefail
1430
+ [ "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ]
1431
+ [ "$AWS_REGION" = "$EBK_POLICY_REGION" ]
1432
+ ${indentedScript(exactStageDeployerGuardScript('Native lifecycle', '$EBK_POLICY_ACCOUNT_ID'), 10)}
1433
+ - name: Run ordered native lifecycle targets
1434
+ env:
1435
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1436
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1437
+ EBK_GITHUB_JOB_WORKFLOW_REF: \${{ job.workflow_ref }}
1438
+ run: |
1439
+ set -euo pipefail
1440
+ test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'Stage deployment role is absent'; exit 1; }
1441
+ if [ "$EBK_ACTION" = deploy ]; then
1442
+ pnpm nx run ${name}:deploy-local -- \
1443
+ --stage="$EBK_STAGE" \
1444
+ --expectedRoleArn="$AWS_DEPLOY_ROLE_ARN"
1445
+ else
1446
+ [ -n "$EBK_ORDERED_PROJECTS" ] || { echo 'Committed deployment order is empty'; exit 1; }
1447
+ IFS=',' read -r -a projects <<< "$EBK_ORDERED_PROJECTS"
1448
+ seen=,
1449
+ for project in "\${projects[@]}"; do
1450
+ case "$project" in ''|*[!A-Za-z0-9_-]*) exit 1 ;; esac
1451
+ case "$seen" in *,"$project",*) echo "Duplicate lifecycle project: $project"; exit 1 ;; esac
1452
+ seen="$seen$project,"
1453
+ pnpm nx run "$project:$EBK_ACTION" -- --stage="$EBK_STAGE" --region="$AWS_REGION" --expectedRoleArn="$AWS_DEPLOY_ROLE_ARN"
1454
+ done
1455
+ fi
1456
+ - name: Seal exact staging promotion receipt
1457
+ if: env.EBK_ACTION == 'deploy' && env.EBK_STAGE == 'staging' && needs.prepare.outputs.profile != 'starter'
1458
+ env:
1459
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1460
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1461
+ EBK_CONTAINER_ARTIFACT_DIGEST: \${{ needs.container-assemble.outputs.artifact_digest }}
1462
+ EBK_CONTAINER_ARTIFACT_ID: \${{ needs.container-assemble.outputs.artifact_id }}
1463
+ EBK_HAS_SLICES: \${{ needs.prepare.outputs.has_slices }}
1464
+ run: |
1465
+ node --input-type=module <<'NODE'
1466
+ ${indentedScript(stagingPromotionSealScript(), 10)}
1467
+ NODE
1468
+ - name: Seal exact postdeploy supply-chain binding
1469
+ if: env.EBK_ACTION == 'deploy' && env.EBK_STAGE != 'development' && needs.prepare.outputs.has_slices == 'true'
1470
+ env:
1471
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1472
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1473
+ run: |
1474
+ node --input-type=module <<'NODE'
1475
+ ${indentedScript(postdeploySupplyChainBindingScript(), 10)}
1476
+ NODE
1477
+ - name: Retain bounded deployment receipt
1478
+ if: always() && needs.prepare.outputs.action == 'deploy'
1479
+ uses: ${actionPins.uploadArtifact}
1480
+ with:
1481
+ if-no-files-found: warn
1482
+ name: deployment-receipt-\${{ needs.prepare.outputs.stage }}-\${{ github.run_id }}-\${{ github.run_attempt }}
1483
+ path: |
1484
+ .ebk/evidence/deployment/latest.json
1485
+ .ebk/evidence/deployment/postdeploy-supply-chain.json
1486
+ .ebk/evidence/deployment/promotion.json
1487
+ .ebk/evidence/container-release/**
1488
+ retention-days: 30
1489
+ `;
1490
+ }
1491
+ function renderThinDeployWorkflowFilePlan(options) {
1492
+ assertOptions(options);
1493
+ return {
1494
+ files: {
1495
+ [exports.THIN_DEPLOY_WORKFLOW_PATH]: renderThinDeployWorkflow(options),
1496
+ },
1497
+ managedPaths: [exports.THIN_DEPLOY_WORKFLOW_PATH],
1498
+ };
1499
+ }
1500
+ //# sourceMappingURL=workspace-files-thin-deploy.js.map