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

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