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

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 (111) hide show
  1. package/package.json +3 -3
  2. package/src/aws-authority/stage-authority.js +4 -0
  3. package/src/aws-authority/stage-authority.js.map +1 -1
  4. package/src/developer-agent/model.d.ts +1 -1
  5. package/src/developer-agent/operation-registry.js +25 -3
  6. package/src/developer-agent/operation-registry.js.map +1 -1
  7. package/src/developer-agent/planning.js +9 -4
  8. package/src/developer-agent/planning.js.map +1 -1
  9. package/src/developer-agent/validation.js +5 -1
  10. package/src/developer-agent/validation.js.map +1 -1
  11. package/src/executors/deployment-plan/deployment-plan.js +3 -0
  12. package/src/executors/deployment-plan/deployment-plan.js.map +1 -1
  13. package/src/executors/foundation-profile-preflight/production-database-safeguard.d.ts +1 -0
  14. package/src/executors/foundation-profile-preflight/production-database-safeguard.js +4 -2
  15. package/src/executors/foundation-profile-preflight/production-database-safeguard.js.map +1 -1
  16. package/src/executors/github-bootstrap/github-bootstrap.js +1 -0
  17. package/src/executors/github-bootstrap/github-bootstrap.js.map +1 -1
  18. package/src/executors/local-deploy/local-deploy.d.ts +21 -3
  19. package/src/executors/local-deploy/local-deploy.js +201 -1
  20. package/src/executors/local-deploy/local-deploy.js.map +1 -1
  21. package/src/executors/local-deploy/schema.json +21 -0
  22. package/src/executors/router-waf-rehearsal/router-waf-rehearsal.d.ts +13 -0
  23. package/src/executors/router-waf-rehearsal/router-waf-rehearsal.js +323 -137
  24. package/src/executors/router-waf-rehearsal/router-waf-rehearsal.js.map +1 -1
  25. package/src/executors/router-waf-rehearsal/schema.json +5 -0
  26. package/src/executors/secret-initialize/secret-initialize.d.ts +2 -0
  27. package/src/executors/secret-initialize/secret-initialize.js +57 -3
  28. package/src/executors/secret-initialize/secret-initialize.js.map +1 -1
  29. package/src/executors/sst-lifecycle/run-sst-command.d.ts +2 -0
  30. package/src/executors/sst-lifecycle/run-sst-command.js +4 -0
  31. package/src/executors/sst-lifecycle/run-sst-command.js.map +1 -1
  32. package/src/foundation/index.d.ts +1 -0
  33. package/src/foundation/index.js +8 -0
  34. package/src/foundation/index.js.map +1 -1
  35. package/src/foundation/private-smoke-access.d.ts +23 -0
  36. package/src/foundation/private-smoke-access.js +127 -0
  37. package/src/foundation/private-smoke-access.js.map +1 -0
  38. package/src/foundation/private-smoke-admission.d.ts +43 -0
  39. package/src/foundation/private-smoke-admission.js +158 -0
  40. package/src/foundation/private-smoke-admission.js.map +1 -0
  41. package/src/foundation/private-smoke-control-plane.d.ts +18 -0
  42. package/src/foundation/private-smoke-control-plane.js +173 -0
  43. package/src/foundation/private-smoke-control-plane.js.map +1 -0
  44. package/src/generators/preset/dependency-overrides.d.ts +8 -0
  45. package/src/generators/preset/dependency-overrides.js +55 -0
  46. package/src/generators/preset/dependency-overrides.js.map +1 -0
  47. package/src/generators/preset/tool-versions.d.ts +2 -0
  48. package/src/generators/preset/tool-versions.js +3 -1
  49. package/src/generators/preset/tool-versions.js.map +1 -1
  50. package/src/generators/preset/workspace-files-developer-agent.js +19 -6
  51. package/src/generators/preset/workspace-files-developer-agent.js.map +1 -1
  52. package/src/generators/preset/workspace-files-thin-deploy.d.ts +16 -1
  53. package/src/generators/preset/workspace-files-thin-deploy.js +946 -26
  54. package/src/generators/preset/workspace-files-thin-deploy.js.map +1 -1
  55. package/src/generators/preset/workspace-files-thin-docs.d.ts +2 -0
  56. package/src/generators/preset/workspace-files-thin-docs.js +66 -6
  57. package/src/generators/preset/workspace-files-thin-docs.js.map +1 -1
  58. package/src/generators/preset/workspace-files.d.ts +8 -1
  59. package/src/generators/preset/workspace-files.js +97 -21
  60. package/src/generators/preset/workspace-files.js.map +1 -1
  61. package/src/generators/repository-policy/repository-policy.js +1 -0
  62. package/src/generators/repository-policy/repository-policy.js.map +1 -1
  63. package/src/generators/repository-policy/schema.d.ts +2 -0
  64. package/src/generators/repository-policy/schema.json +27 -7
  65. package/src/generators/slice/built-in-plan-background-work-files.d.ts +2 -0
  66. package/src/generators/slice/built-in-plan-background-work-files.js +89 -0
  67. package/src/generators/slice/built-in-plan-background-work-files.js.map +1 -0
  68. package/src/generators/slice/built-in-plan-database-lifecycle-files.js +20 -4
  69. package/src/generators/slice/built-in-plan-database-lifecycle-files.js.map +1 -1
  70. package/src/generators/slice/built-in-plan-infrastructure-files.js +22 -9
  71. package/src/generators/slice/built-in-plan-infrastructure-files.js.map +1 -1
  72. package/src/generators/slice/built-in-plan-operations-files.js +8 -10
  73. package/src/generators/slice/built-in-plan-operations-files.js.map +1 -1
  74. package/src/generators/slice/built-in-plan.js +3 -0
  75. package/src/generators/slice/built-in-plan.js.map +1 -1
  76. package/src/generators/upgrade/upgrade.d.ts +12 -0
  77. package/src/generators/upgrade/upgrade.js +84 -9
  78. package/src/generators/upgrade/upgrade.js.map +1 -1
  79. package/src/generators/workload/workload.js +1 -1
  80. package/src/git-policy/action-pins.d.ts +5 -0
  81. package/src/git-policy/action-pins.js +5 -0
  82. package/src/git-policy/action-pins.js.map +1 -1
  83. package/src/git-policy/github-bootstrap.d.ts +10 -2
  84. package/src/git-policy/github-bootstrap.js +129 -34
  85. package/src/git-policy/github-bootstrap.js.map +1 -1
  86. package/src/git-policy/routing.d.ts +5 -1
  87. package/src/git-policy/routing.js +50 -0
  88. package/src/git-policy/routing.js.map +1 -1
  89. package/src/performance/model.d.ts +1 -1
  90. package/src/repository-policy/capacity-migration.d.ts +3 -0
  91. package/src/repository-policy/capacity-migration.js +21 -0
  92. package/src/repository-policy/capacity-migration.js.map +1 -0
  93. package/src/repository-policy/foundation-profile.js +10 -0
  94. package/src/repository-policy/foundation-profile.js.map +1 -1
  95. package/src/repository-policy/policy.d.ts +13 -2
  96. package/src/repository-policy/policy.js +47 -2
  97. package/src/repository-policy/policy.js.map +1 -1
  98. package/src/schemas/repository-policy-v1.json +56 -17
  99. package/src/supply-chain/generated-container-release-command.js +281 -74
  100. package/src/supply-chain/generated-container-release-command.js.map +1 -1
  101. package/src/supply-chain/generated-files.js +21 -11
  102. package/src/supply-chain/generated-files.js.map +1 -1
  103. package/src/supply-chain/generated-scanner-command.js +1 -1
  104. package/src/supply-chain/scanner.js +3 -0
  105. package/src/supply-chain/scanner.js.map +1 -1
  106. package/src/workspace-deployment/affected-lifecycle.d.ts +53 -0
  107. package/src/workspace-deployment/affected-lifecycle.js +84 -0
  108. package/src/workspace-deployment/affected-lifecycle.js.map +1 -0
  109. package/src/workspace-deployment/index.d.ts +1 -0
  110. package/src/workspace-deployment/index.js +1 -0
  111. package/src/workspace-deployment/index.js.map +1 -1
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.THIN_DEPLOY_WORKFLOW_PATH = void 0;
3
+ exports.EBK_RECORD_MAIN_TOKEN_SECRET = exports.THIN_DEPLOY_WORKFLOW_PATH = void 0;
4
4
  exports.renderThinDeployVerifyCaller = renderThinDeployVerifyCaller;
5
+ exports.protectedBrowserInstallScript = protectedBrowserInstallScript;
5
6
  exports.exactStageDeployerGuardScript = exactStageDeployerGuardScript;
6
7
  exports.canonicalStageAuthorityDerivationScript = canonicalStageAuthorityDerivationScript;
7
8
  exports.exactStagingPromotionPullGuardScript = exactStagingPromotionPullGuardScript;
@@ -10,11 +11,19 @@ exports.restoredCandidateValidationScript = restoredCandidateValidationScript;
10
11
  exports.stagingPromotionSealScript = stagingPromotionSealScript;
11
12
  exports.postdeploySupplyChainBindingScript = postdeploySupplyChainBindingScript;
12
13
  exports.renderThinDeployWorkflowFilePlan = renderThinDeployWorkflowFilePlan;
14
+ const tool_versions_js_1 = require("./tool-versions.js");
13
15
  exports.THIN_DEPLOY_WORKFLOW_PATH = '.github/workflows/deploy.yml';
16
+ /** The production environment secret that lets record-main fast-forward main. */
17
+ exports.EBK_RECORD_MAIN_TOKEN_SECRET = 'EBK_RECORD_MAIN_TOKEN';
14
18
  const EXACT_SEMVER = /^\d+\.\d+\.\d+$/;
15
19
  const WORKSPACE_NAME = /^[a-z0-9][a-z0-9-]{0,62}$/;
16
20
  const ACTION_PIN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+@[a-f0-9]{40}(?: # [^\r\n]+)?$/;
17
21
  function assertOptions(options) {
22
+ if (options.productionApprovalMode === 'trunk-promotion' &&
23
+ (options.deploymentProfile === 'starter' ||
24
+ options.deliveryLane === 'trunk')) {
25
+ throw new Error('trunk-promotion requires an isolated deployment profile on the protected pair.');
26
+ }
18
27
  if (!WORKSPACE_NAME.test(options.name)) {
19
28
  throw new Error('Workspace name must be a bounded lowercase slug.');
20
29
  }
@@ -29,8 +38,8 @@ function assertOptions(options) {
29
38
  }
30
39
  }
31
40
  }
32
- function workflowCallTrigger(profile) {
33
- return profile === 'starter'
41
+ function workflowCallTrigger(profile, deliveryLane = 'protected-pair') {
42
+ return profile === 'starter' && deliveryLane !== 'trunk'
34
43
  ? ''
35
44
  : ` workflow_call:
36
45
  inputs:
@@ -51,10 +60,18 @@ function workflowCallTrigger(profile) {
51
60
  type: string
52
61
  `;
53
62
  }
54
- function renderThinDeployVerifyCaller(profile) {
55
- if (profile === 'starter')
63
+ function renderThinDeployVerifyCaller(profile, deliveryLane = 'protected-pair', productionApprovalMode = 'promotion-pr') {
64
+ const trunk = deliveryLane === 'trunk';
65
+ if (profile === 'starter' && !trunk)
56
66
  return '';
57
- const protectedBranchCondition = "(github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main')";
67
+ // Trunk lane: every merged main push is a production delivery.
68
+ // Trunk promotion: production deploys from the develop run itself and
69
+ // record-main fast-forwards main afterwards, so a main push only verifies.
70
+ const protectedBranchCondition = trunk
71
+ ? "github.ref == 'refs/heads/main'"
72
+ : productionApprovalMode === 'trunk-promotion'
73
+ ? "github.ref == 'refs/heads/develop'"
74
+ : "(github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main')";
58
75
  return `
59
76
 
60
77
  deploy:
@@ -66,7 +83,8 @@ function renderThinDeployVerifyCaller(profile) {
66
83
  ${protectedBranchCondition} &&
67
84
  needs.acceptance.result == 'success' &&
68
85
  needs.acceptance.outputs.history_only != 'true' &&
69
- needs.release-candidate.result == 'success'
86
+ (needs.release-candidate.result == 'success' ||
87
+ (github.ref == 'refs/heads/main' && needs.release-candidate.result == 'skipped'))
70
88
  permissions:
71
89
  actions: read
72
90
  contents: read
@@ -77,7 +95,11 @@ function renderThinDeployVerifyCaller(profile) {
77
95
  source_commit: \${{ github.sha }}
78
96
  source_ref: \${{ github.ref }}
79
97
  verify_run_attempt: \${{ github.run_attempt }}
80
- verify_run_id: \${{ github.run_id }}`;
98
+ verify_run_id: \${{ github.run_id }}
99
+ # The reusable workflow reads the production environment secret
100
+ # AWS_STAGING_PROMOTION_PULL_ROLE_ARN, which only reaches a called
101
+ # workflow when the caller inherits secrets (EBK-INT-2026-075).
102
+ secrets: inherit`;
81
103
  }
82
104
  function indentedScript(source, spaces) {
83
105
  const prefix = ' '.repeat(spaces);
@@ -87,6 +109,22 @@ function indentedScript(source, spaces) {
87
109
  .map((line) => `${prefix}${line}`)
88
110
  .join('\n');
89
111
  }
112
+ /** @internal Run exactly this generated script in the protected-runner fixture. */
113
+ function protectedBrowserInstallScript() {
114
+ return `set -euo pipefail
115
+ if [ -z "$EBK_SLICE_PROJECTS" ]; then exit 0; fi
116
+ case "$EBK_SLICE_PROJECTS" in ,*|*,|*,,*) echo 'Invalid smoke project inventory'; exit 1 ;; esac
117
+ IFS=',' read -r -a projects <<< "$EBK_SLICE_PROJECTS"
118
+ seen=,
119
+ for project in "\${projects[@]}"; do
120
+ case "$project" in ''|*[!A-Za-z0-9_-]*) echo 'Invalid smoke project'; exit 1 ;; esac
121
+ case "$seen" in *,"$project",*) echo 'Duplicate smoke project'; exit 1 ;; esac
122
+ seen="$seen$project,"
123
+ done
124
+ for project in "\${projects[@]}"; do
125
+ pnpm --dir "packages/$project" exec playwright install --with-deps chromium
126
+ done`;
127
+ }
90
128
  /** @internal Exported for executable source-contract tests. */
91
129
  function exactStageDeployerGuardScript(operation, accountVariable) {
92
130
  return `read -r caller_account caller_arn <<< "$(aws sts get-caller-identity --query '[Account,Arn]' --output text)"
@@ -316,6 +354,13 @@ if (hasSlices) {
316
354
  const artifactId = required('EBK_CANDIDATE_ARTIFACT_ID', /^[1-9][0-9]*$/);
317
355
  const artifactDigest = required('EBK_CANDIDATE_ARTIFACT_DIGEST', /^sha256:[a-f0-9]{64}$/);
318
356
  const containerArtifactId = required('EBK_CONTAINER_ARTIFACT_ID', /^[1-9][0-9]*$/);
357
+ // actions/upload-artifact emits the bare SHA-256 hex in artifact-digest while
358
+ // the artifacts API and the promotion compare the sha256:<hex> registry form
359
+ // (EBK-INT-2026-073). Normalize before the exact validation.
360
+ const containerArtifactDigestRaw = process.env.EBK_CONTAINER_ARTIFACT_DIGEST?.trim() ?? '';
361
+ process.env.EBK_CONTAINER_ARTIFACT_DIGEST = /^[a-f0-9]{64}$/.test(containerArtifactDigestRaw)
362
+ ? 'sha256:' + containerArtifactDigestRaw
363
+ : containerArtifactDigestRaw;
319
364
  const containerArtifactDigest = required('EBK_CONTAINER_ARTIFACT_DIGEST', /^sha256:[a-f0-9]{64}$/);
320
365
  const root = '.ebk/evidence/release-candidate/packages';
321
366
  const projects = readdirSync(root, { withFileTypes: true })
@@ -424,22 +469,35 @@ writeFileSync(
424
469
  }
425
470
  function renderThinDeployWorkflow(options) {
426
471
  const { actionPins, deploymentProfile, name, toolVersions } = options;
472
+ const trunkPromotion = options.productionApprovalMode === 'trunk-promotion';
427
473
  return `name: Deploy
428
474
  run-name: Deploy \${{ inputs.source_commit || github.sha }}
429
475
 
430
476
  on:
431
- ${workflowCallTrigger(deploymentProfile)} workflow_dispatch:
477
+ ${workflowCallTrigger(deploymentProfile, options.deliveryLane)} workflow_dispatch:
432
478
  inputs:
433
479
  action:
434
480
  description: Native lifecycle action
435
481
  required: true
436
482
  default: deploy
437
483
  type: choice
438
- options: [deploy, refresh, unlock, remove]
484
+ options: [deploy, refresh, unlock, remove${deploymentProfile === 'starter' ? '' : ', rehearse'}]
439
485
  project:
440
486
  description: Required registered project for unlock or remove recovery
441
487
  required: false
442
- type: string
488
+ type: string${deploymentProfile === 'starter'
489
+ ? ''
490
+ : `
491
+ probe_url:
492
+ description: rehearse only; exact HTTPS Router health URL in /<kebab-slice>/api/health form for the staging blocking rehearsal
493
+ required: false
494
+ type: string`}
495
+ lifecycle:
496
+ description: affected skips Slices unchanged since the newest successful deployment; full deploys everything
497
+ required: false
498
+ default: affected
499
+ type: choice
500
+ options: [affected, full]
443
501
 
444
502
  concurrency:
445
503
  group: protected-lifecycle-\${{ inputs.source_branch || github.ref_name }}
@@ -460,6 +518,7 @@ jobs:
460
518
  runs-on: ubuntu-latest
461
519
  outputs:
462
520
  action: \${{ steps.route.outputs.action }}
521
+ probe_url: \${{ steps.route.outputs.probe_url }}
463
522
  account_id: \${{ steps.account.outputs.account_id }}
464
523
  has_slices: \${{ steps.policy.outputs.has_slices }}
465
524
  ordered_projects: \${{ steps.policy.outputs.ordered_projects }}
@@ -469,9 +528,14 @@ jobs:
469
528
  project: \${{ steps.route.outputs.project }}
470
529
  region: \${{ steps.policy.outputs.deployment_region }}
471
530
  slice_projects: \${{ steps.policy.outputs.slice_projects }}
531
+ slice_matrix: \${{ steps.policy.outputs.slice_matrix }}
472
532
  staging_account_id: \${{ steps.policy.outputs.staging_account_id }}
473
533
  stage: \${{ steps.route.outputs.stage }}
474
- stage_deploy_role_arn: \${{ steps.account.outputs.role_arn }}
534
+ stage_deploy_role_arn: \${{ steps.account.outputs.role_arn }}${trunkPromotion
535
+ ? `
536
+ production_account_id: \${{ steps.production-account.outputs.account_id }}
537
+ production_deploy_role_arn: \${{ steps.production-account.outputs.role_arn }}`
538
+ : ''}
475
539
  steps:
476
540
  - name: Verify exact protected branch tip before setup
477
541
  env:
@@ -533,6 +597,7 @@ jobs:
533
597
  name: Resolve protected stage and action
534
598
  env:
535
599
  EBK_DISPATCH_ACTION: \${{ inputs.action || '' }}
600
+ EBK_DISPATCH_PROBE_URL: \${{ inputs.probe_url || '' }}
536
601
  EBK_DISPATCH_PROJECT: \${{ inputs.project || '' }}
537
602
  EBK_PROFILE: ${deploymentProfile}
538
603
  EBK_SOURCE_BRANCH: \${{ env.EBK_DEPLOYMENT_SOURCE_BRANCH }}
@@ -548,12 +613,25 @@ jobs:
548
613
  fi
549
614
  case "$EBK_SOURCE_COMMIT" in ''|*[!0-9a-f]*) exit 1 ;; esac
550
615
  { [ "\${#EBK_SOURCE_COMMIT}" -eq 40 ] || [ "\${#EBK_SOURCE_COMMIT}" -eq 64 ]; } || exit 1
551
- case "$action" in deploy|refresh|unlock|remove) ;; *) exit 1 ;; esac
616
+ case "$action" in deploy|refresh|unlock|remove|rehearse) ;; *) exit 1 ;; esac
552
617
  case "$project" in ''|*[!A-Za-z0-9_-]*) [ -z "$project" ] || exit 1 ;; esac
553
618
  case "$action" in
554
619
  unlock|remove) [ -n "$project" ] || { echo 'unlock and remove require an explicit project'; exit 1; } ;;
555
620
  deploy|refresh) [ -z "$project" ] || { echo 'deploy and refresh operate on the policy-derived project order'; exit 1; } ;;
621
+ rehearse)
622
+ [ -z "$project" ] || { echo 'rehearse targets the foundation Router WAF and takes no project'; exit 1; }
623
+ [ "$EBK_PROFILE" != starter ] || { echo 'starter rehearses development locally; rehearse is for isolated staging'; exit 1; }
624
+ [ "$EBK_SOURCE_BRANCH" = develop ] || { echo 'rehearse runs from develop against staging'; exit 1; }
625
+ case "$EBK_DISPATCH_PROBE_URL" in
626
+ https://*/*/api/health) ;;
627
+ *) echo 'rehearse requires probe_url in https://<router>/<kebab-slice>/api/health form'; exit 1 ;;
628
+ esac
629
+ ;;
556
630
  esac
631
+ # The committed deployment plan knows deploy, refresh, unlock, and
632
+ # remove; a rehearsal derives the same project inventory as deploy.
633
+ plan_action="$action"
634
+ [ "$action" != rehearse ] || plan_action=deploy
557
635
  case "$EBK_SOURCE_BRANCH" in
558
636
  main) stage=production ;;
559
637
  develop)
@@ -566,12 +644,14 @@ jobs:
566
644
  *) echo 'Protected lifecycle must run from develop or main'; exit 1 ;;
567
645
  esac
568
646
  echo "action=$action" >> "$GITHUB_OUTPUT"
647
+ echo "plan_action=$plan_action" >> "$GITHUB_OUTPUT"
648
+ echo "probe_url=$EBK_DISPATCH_PROBE_URL" >> "$GITHUB_OUTPUT"
569
649
  echo "project=$project" >> "$GITHUB_OUTPUT"
570
650
  echo "stage=$stage" >> "$GITHUB_OUTPUT"
571
651
  - id: policy
572
652
  name: Derive ordered projects from committed policy
573
653
  env:
574
- EBK_ACTION: \${{ steps.route.outputs.action }}
654
+ EBK_ACTION: \${{ steps.route.outputs.plan_action }}
575
655
  EBK_PROJECT: \${{ steps.route.outputs.project }}
576
656
  EBK_STAGE: \${{ steps.route.outputs.stage }}
577
657
  run: |
@@ -593,7 +673,22 @@ jobs:
593
673
  EBK_STAGE: \${{ steps.route.outputs.stage }}
594
674
  EBK_STAGING_ACCOUNT: \${{ steps.policy.outputs.staging_account_id }}
595
675
  run: |
596
- ${indentedScript(`set -euo pipefail\n${canonicalStageAuthorityDerivationScript()}`, 10)}
676
+ ${indentedScript(`set -euo pipefail\n${canonicalStageAuthorityDerivationScript()}`, 10)}${trunkPromotion
677
+ ? `
678
+ # Trunk promotion deploys production from the develop run, so the
679
+ # production authority is derived here beside the staging authority.
680
+ - id: production-account
681
+ name: Derive canonical committed production authority for trunk promotion
682
+ if: steps.route.outputs.stage == 'staging'
683
+ env:
684
+ EBK_DEVELOPMENT_ACCOUNT: \${{ steps.policy.outputs.development_account_id }}
685
+ EBK_PRODUCTION_ACCOUNT: \${{ steps.policy.outputs.production_account_id }}
686
+ EBK_REGION: \${{ steps.policy.outputs.deployment_region }}
687
+ EBK_STAGE: production
688
+ EBK_STAGING_ACCOUNT: \${{ steps.policy.outputs.staging_account_id }}
689
+ run: |
690
+ ${indentedScript(`set -euo pipefail\n${canonicalStageAuthorityDerivationScript()}`, 10)}`
691
+ : ''}
597
692
 
598
693
  immutable-artifacts:
599
694
  name: prepare exact immutable release artifacts
@@ -616,6 +711,11 @@ ${indentedScript(`set -euo pipefail\n${canonicalStageAuthorityDerivationScript()
616
711
  container_artifact_id: \${{ steps.staging.outputs.container_artifact_id }}
617
712
  staging_deployment_run_attempt: \${{ steps.staging.outputs.deployment_run_attempt }}
618
713
  staging_deployment_run_id: \${{ steps.staging.outputs.deployment_run_id }}
714
+ baseline_artifact_digest: \${{ steps.baseline.outputs.artifact_digest }}
715
+ baseline_artifact_id: \${{ steps.baseline.outputs.artifact_id }}
716
+ baseline_commit: \${{ steps.baseline.outputs.commit }}
717
+ baseline_found: \${{ steps.baseline.outputs.found }}
718
+ baseline_run_id: \${{ steps.baseline.outputs.run_id }}
619
719
  producer_run_attempt: \${{ steps.current.outputs.producer_run_attempt || steps.staging.outputs.producer_run_attempt }}
620
720
  producer_run_id: \${{ steps.current.outputs.producer_run_id || steps.staging.outputs.producer_run_id }}
621
721
  source_commit: \${{ steps.current.outputs.source_commit || steps.staging.outputs.source_commit }}
@@ -666,6 +766,46 @@ ${indentedScript(`set -euo pipefail\n${canonicalStageAuthorityDerivationScript()
666
766
  echo "producer_run_attempt=$run_attempt" >> "$GITHUB_OUTPUT"
667
767
  echo "producer_run_id=$run_id" >> "$GITHUB_OUTPUT"
668
768
  echo "source_commit=$EBK_SOURCE_COMMIT" >> "$GITHUB_OUTPUT"
769
+ - id: baseline
770
+ name: Select the newest successful deployment receipt on this branch
771
+ if: >-
772
+ needs.prepare.outputs.has_slices == 'true' &&
773
+ (needs.prepare.outputs.stage != 'production' || needs.prepare.outputs.profile == 'starter')
774
+ env:
775
+ EBK_SOURCE_BRANCH: \${{ env.EBK_DEPLOYMENT_SOURCE_BRANCH }}
776
+ EBK_SOURCE_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
777
+ EBK_STAGE: \${{ needs.prepare.outputs.stage }}
778
+ GH_REPO: \${{ github.repository }}
779
+ GH_TOKEN: \${{ github.token }}
780
+ run: |
781
+ set -euo pipefail
782
+ # A baseline lets unchanged OCI artifacts reuse their retained evidence.
783
+ # Declining is always safe: without one, every artifact is rebuilt.
784
+ echo "found=false" >> "$GITHUB_OUTPUT"
785
+ runs="$(gh api "/repos/$GH_REPO/actions/workflows/ci.yml/runs?event=push&branch=$EBK_SOURCE_BRANCH&status=success&per_page=50")"
786
+ for candidate in $(printf '%s' "$runs" | jq -r --arg branch "$EBK_SOURCE_BRANCH" \\
787
+ '.workflow_runs | sort_by(.run_number) | reverse | .[] | select(.head_branch == $branch and .event == "push" and .conclusion == "success" and .path == ".github/workflows/ci.yml") | "\\(.id):\\(.run_attempt):\\(.head_sha)"'); do
788
+ IFS=':' read -r run_id run_attempt head_sha <<< "$candidate"
789
+ case "$run_id:$run_attempt" in *[!0-9:]*) continue ;; esac
790
+ case "$head_sha" in ????????????????????????????????????????) ;; *) continue ;; esac
791
+ [ "$head_sha" != "$EBK_SOURCE_COMMIT" ] || continue
792
+ git merge-base --is-ancestor "$head_sha" "$EBK_SOURCE_COMMIT" 2>/dev/null || continue
793
+ artifacts="$(gh api "/repos/$GH_REPO/actions/runs/$run_id/artifacts?per_page=100")"
794
+ receipt="$(printf '%s' "$artifacts" | jq -c --arg name "deployment-receipt-$EBK_STAGE-$run_id-$run_attempt" \\
795
+ '[.artifacts[] | select(.name == $name and .expired == false)] | if length == 1 then .[0] else empty end')"
796
+ [ -n "$receipt" ] || continue
797
+ artifact_id="$(printf '%s' "$receipt" | jq -r '.id // empty')"
798
+ artifact_digest="$(printf '%s' "$receipt" | jq -r '.digest // empty')"
799
+ case "$artifact_id" in ''|*[!0-9]*) continue ;; esac
800
+ case "$artifact_digest" in sha256:????????????????????????????????????????????????????????????????) ;; *) continue ;; esac
801
+ echo "artifact_digest=$artifact_digest" >> "$GITHUB_OUTPUT"
802
+ echo "artifact_id=$artifact_id" >> "$GITHUB_OUTPUT"
803
+ echo "commit=$head_sha" >> "$GITHUB_OUTPUT"
804
+ echo "found=true" >> "$GITHUB_OUTPUT"
805
+ echo "run_id=$run_id" >> "$GITHUB_OUTPUT"
806
+ echo "Baseline deployment receipt: run $run_id attempt $run_attempt at $head_sha"
807
+ break
808
+ done
669
809
  - id: staging-receipt
670
810
  name: Select exact successful staging deployment receipt
671
811
  if: needs.prepare.outputs.stage == 'production' && needs.prepare.outputs.profile != 'starter'
@@ -733,7 +873,9 @@ ${indentedScript(`set -euo pipefail\n${canonicalStageAuthorityDerivationScript()
733
873
  EBK_EXPECTED_STAGING_ACCOUNT_ID: \${{ needs.prepare.outputs.staging_account_id }}
734
874
  EBK_STAGING_RECEIPT_ARTIFACT_DIGEST: \${{ steps.staging-receipt.outputs.artifact_digest }}
735
875
  EBK_STAGING_RECEIPT_ARTIFACT_ID: \${{ steps.staging-receipt.outputs.artifact_id }}
736
- EBK_STAGING_RECEIPT_ROOT: \${{ runner.temp }}/staging-deployment-receipt
876
+ # The receipt artifact keeps .ebk/evidence as its root, so latest.json
877
+ # and promotion.json live under deployment/ (EBK-INT-2026-074).
878
+ EBK_STAGING_RECEIPT_ROOT: \${{ runner.temp }}/staging-deployment-receipt/deployment
737
879
  EBK_STAGING_SOURCE_COMMIT: \${{ steps.staging-receipt.outputs.source_commit }}
738
880
  GH_REPO: \${{ github.repository }}
739
881
  GH_TOKEN: \${{ github.token }}
@@ -769,7 +911,7 @@ ${indentedScript(stagingPromotionReceiptValidationScript(), 10)}
769
911
  fi
770
912
 
771
913
  container-platform:
772
- name: native \${{ needs.prepare.outputs.stage }} OCI \${{ matrix.platform }}
914
+ name: native \${{ needs.prepare.outputs.stage }} OCI \${{ matrix.project.project }} \${{ matrix.platform }}
773
915
  needs: [prepare, immutable-artifacts]
774
916
  if: >-
775
917
  always() &&
@@ -783,7 +925,10 @@ ${indentedScript(stagingPromotionReceiptValidationScript(), 10)}
783
925
  )
784
926
  strategy:
785
927
  fail-fast: false
928
+ # One job per Slice and architecture: Slices build in parallel instead of
929
+ # looping on one runner per architecture.
786
930
  matrix:
931
+ project: \${{ fromJson(needs.prepare.outputs.slice_matrix) }}
787
932
  include:
788
933
  - architecture: amd64
789
934
  platform: linux/amd64
@@ -812,6 +957,7 @@ ${indentedScript(stagingPromotionReceiptValidationScript(), 10)}
812
957
  EBK_SOURCE_COMMIT: \${{ inputs.source_commit || github.sha }}
813
958
  EBK_STAGE: \${{ needs.prepare.outputs.stage }}
814
959
  EBK_SLICE_PROJECTS: \${{ needs.prepare.outputs.slice_projects }}
960
+ EBK_SLICE_PROJECT: \${{ matrix.project.project }}
815
961
  steps:
816
962
  - name: Check out immutable container source
817
963
  uses: ${actionPins.checkout}
@@ -852,6 +998,17 @@ ${indentedScript(stagingPromotionReceiptValidationScript(), 10)}
852
998
  github-token: \${{ github.token }}
853
999
  path: .ebk/evidence/release-candidate
854
1000
  run-id: \${{ needs.immutable-artifacts.outputs.producer_run_id }}
1001
+ - name: Restore the baseline deployment receipt for unchanged artifacts
1002
+ if: needs.immutable-artifacts.outputs.baseline_found == 'true'
1003
+ uses: ${actionPins.downloadArtifact}
1004
+ env:
1005
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1006
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1007
+ with:
1008
+ artifact-ids: \${{ needs.immutable-artifacts.outputs.baseline_artifact_id }}
1009
+ github-token: \${{ github.token }}
1010
+ path: \${{ runner.temp }}/ebk-deployment-baseline
1011
+ run-id: \${{ needs.immutable-artifacts.outputs.baseline_run_id }}
855
1012
  - name: Revalidate exact protected branch tip before OIDC
856
1013
  env:
857
1014
  ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
@@ -882,22 +1039,48 @@ ${indentedScript(stagingPromotionReceiptValidationScript(), 10)}
882
1039
  [ "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ]
883
1040
  [ "$AWS_REGION" = "$EBK_POLICY_REGION" ]
884
1041
  [ "$(aws sts get-caller-identity --query Account --output text)" = "$EBK_POLICY_ACCOUNT_ID" ]
1042
+ - name: Restore the container layer and release tool caches
1043
+ uses: ${actionPins.cache}
1044
+ env:
1045
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1046
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1047
+ with:
1048
+ path: |
1049
+ \${{ runner.temp }}/ebk-buildx-cache
1050
+ \${{ runner.temp }}/ebk-release-tools
1051
+ key: ebk-container-\${{ needs.prepare.outputs.stage }}-\${{ matrix.project.project }}-\${{ matrix.architecture }}-\${{ hashFiles('pnpm-lock.yaml') }}-\${{ github.run_id }}
1052
+ restore-keys: |
1053
+ ebk-container-\${{ needs.prepare.outputs.stage }}-\${{ matrix.project.project }}-\${{ matrix.architecture }}-\${{ hashFiles('pnpm-lock.yaml') }}-
1054
+ ebk-container-\${{ needs.prepare.outputs.stage }}-\${{ matrix.project.project }}-\${{ matrix.architecture }}-
885
1055
  - name: Build and run exact native OCI artifacts
1056
+ env:
1057
+ EBK_BUILDX_CACHE_DIR: \${{ runner.temp }}/ebk-buildx-cache
1058
+ EBK_RELEASE_TOOL_CACHE_DIR: \${{ runner.temp }}/ebk-release-tools
1059
+ EBK_CONTAINER_BASELINE_COMMIT: \${{ needs.immutable-artifacts.outputs.baseline_commit }}
1060
+ EBK_CONTAINER_BASELINE_ROOT: \${{ needs.immutable-artifacts.outputs.baseline_found == 'true' && format('{0}/ebk-deployment-baseline', runner.temp) || '' }}
886
1061
  run: |
887
1062
  set -euo pipefail
888
1063
  ${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
1064
+ case "$EBK_SLICE_PROJECT" in
1065
+ ''|-*|*-|*[!a-z0-9-]*) echo 'Container build Slice project must be an exact normalized Slice name'; exit 1 ;;
1066
+ esac
1067
+ case ",$EBK_SLICE_PROJECTS," in
1068
+ *",$EBK_SLICE_PROJECT,"*) ;;
1069
+ *) echo 'Container build Slice project is not in the committed deployment plan'; exit 1 ;;
1070
+ esac
1071
+ pnpm nx run "$EBK_SLICE_PROJECT:\${{ matrix.target }}"
894
1072
  - name: Retain native platform evidence
895
1073
  uses: ${actionPins.uploadArtifact}
896
1074
  with:
897
- name: container-platform-\${{ needs.prepare.outputs.stage }}-\${{ matrix.architecture }}-\${{ github.run_id }}-\${{ github.run_attempt }}
1075
+ name: container-platform-\${{ needs.prepare.outputs.stage }}-\${{ matrix.project.project }}-\${{ matrix.architecture }}-\${{ github.run_id }}-\${{ github.run_attempt }}
898
1076
  if-no-files-found: error
899
1077
  include-hidden-files: true
900
1078
  retention-days: 30
1079
+ # The glob keeps partials/<stage> as the artifact root: the assemble
1080
+ # job merges every platform artifact into that directory and the
1081
+ # container release tool reads <project>/<architecture>/ beneath it.
1082
+ # A literal <project>/<architecture> path would make the architecture
1083
+ # directory the root and flatten the merged restore.
901
1084
  path: .ebk/evidence/container-release/partials/\${{ needs.prepare.outputs.stage }}/*/\${{ matrix.architecture }}
902
1085
 
903
1086
  container-assemble:
@@ -1195,8 +1378,10 @@ ${indentedScript(exactStageDeployerGuardScript('Container promotion', '$AWS_EXPE
1195
1378
  IFS=',' read -r -a projects <<< "$EBK_SLICE_PROJECTS"
1196
1379
  for project in "\${projects[@]}"; do
1197
1380
  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"
1381
+ # The container release tool resolves these against the repository
1382
+ # root, so they stay repository-relative (EBK-INT-2026-077).
1383
+ export EBK_CONTAINER_PROMOTION_SOURCE_BINDING_PATH=".ebk/evidence/container-release/staging/packages/$project/binding.json"
1384
+ export EBK_CONTAINER_PROMOTION_SOURCE_SIDECAR_PATH=".ebk/evidence/container-release/staging/packages/$project/release-bound-binding.json"
1200
1385
  pnpm nx run "$project:container-release-promote"
1201
1386
  done
1202
1387
  - id: retain
@@ -1215,6 +1400,7 @@ ${indentedScript(exactStageDeployerGuardScript('Container promotion', '$AWS_EXPE
1215
1400
  if: >-
1216
1401
  always() &&
1217
1402
  needs.prepare.result == 'success' &&
1403
+ needs.prepare.outputs.action != 'rehearse' &&
1218
1404
  (
1219
1405
  needs.prepare.outputs.action != 'deploy' ||
1220
1406
  (
@@ -1266,6 +1452,9 @@ ${indentedScript(exactStageDeployerGuardScript('Container promotion', '$AWS_EXPE
1266
1452
  EBK_SLICE_PROJECTS: \${{ needs.prepare.outputs.slice_projects }}
1267
1453
  EBK_SOURCE_COMMIT: \${{ inputs.source_commit || github.sha }}
1268
1454
  EBK_STAGE: \${{ needs.prepare.outputs.stage }}
1455
+ EBK_LIFECYCLE_SCOPE: \${{ inputs.lifecycle || 'affected' }}
1456
+ EBK_BASELINE_FOUND: \${{ needs.immutable-artifacts.outputs.baseline_found }}
1457
+ EBK_BASELINE_RUN_ID: \${{ needs.immutable-artifacts.outputs.baseline_run_id }}
1269
1458
  steps:
1270
1459
  - name: Check out immutable deployment source
1271
1460
  uses: ${actionPins.checkout}
@@ -1307,6 +1496,17 @@ ${indentedScript(exactStageDeployerGuardScript('Container promotion', '$AWS_EXPE
1307
1496
  github-token: \${{ github.token }}
1308
1497
  path: .ebk/evidence/release-candidate
1309
1498
  run-id: \${{ needs.immutable-artifacts.outputs.producer_run_id }}
1499
+ - name: Restore the baseline deployment receipt for unchanged Slices
1500
+ if: needs.prepare.outputs.action == 'deploy' && needs.immutable-artifacts.outputs.baseline_found == 'true'
1501
+ uses: ${actionPins.downloadArtifact}
1502
+ env:
1503
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1504
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1505
+ with:
1506
+ artifact-ids: \${{ needs.immutable-artifacts.outputs.baseline_artifact_id }}
1507
+ github-token: \${{ github.token }}
1508
+ path: \${{ runner.temp }}/ebk-deployment-baseline
1509
+ run-id: \${{ needs.immutable-artifacts.outputs.baseline_run_id }}
1310
1510
  - name: Restore exact current-stage container binding
1311
1511
  if: >-
1312
1512
  needs.prepare.outputs.action == 'deploy' &&
@@ -1400,6 +1600,23 @@ ${indentedScript(restoredCandidateValidationScript(), 10)}
1400
1600
  ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1401
1601
  ACTIONS_ID_TOKEN_REQUEST_URL: ''
1402
1602
  run: pnpm nx run ${name}:supply-chain-check-protected
1603
+ - name: Install pinned uv for generated Python workloads
1604
+ if: env.EBK_ACTION == 'deploy'
1605
+ env:
1606
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1607
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1608
+ run: |
1609
+ set -euo pipefail
1610
+ python3 -m pip install --user --disable-pip-version-check 'uv==${tool_versions_js_1.WORKSPACE_UV_VERSION}'
1611
+ echo "$HOME/.local/bin" >> "$GITHUB_PATH"
1612
+ "$HOME/.local/bin/uv" --version
1613
+ - name: Install pinned Playwright Chromium for deployed smoke proofs
1614
+ if: env.EBK_ACTION == 'deploy' && needs.prepare.outputs.has_slices == 'true'
1615
+ env:
1616
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1617
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1618
+ run: |
1619
+ ${indentedScript(protectedBrowserInstallScript(), 10)}
1403
1620
  - name: Revalidate exact protected branch tip before OIDC
1404
1621
  env:
1405
1622
  ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
@@ -1431,17 +1648,31 @@ ${indentedScript(restoredCandidateValidationScript(), 10)}
1431
1648
  [ "$AWS_REGION" = "$EBK_POLICY_REGION" ]
1432
1649
  ${indentedScript(exactStageDeployerGuardScript('Native lifecycle', '$EBK_POLICY_ACCOUNT_ID'), 10)}
1433
1650
  - name: Run ordered native lifecycle targets
1651
+ id: native-lifecycle
1434
1652
  env:
1435
1653
  ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1436
1654
  ACTIONS_ID_TOKEN_REQUEST_URL: ''
1437
1655
  EBK_GITHUB_JOB_WORKFLOW_REF: \${{ job.workflow_ref }}
1656
+ GITHUB_TOKEN: \${{ github.token }}
1438
1657
  run: |
1439
1658
  set -euo pipefail
1440
1659
  test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'Stage deployment role is absent'; exit 1; }
1441
1660
  if [ "$EBK_ACTION" = deploy ]; then
1661
+ case "$EBK_LIFECYCLE_SCOPE" in affected|full) ;; *) echo 'Lifecycle scope must be affected or full'; exit 1 ;; esac
1662
+ baseline_arguments=()
1663
+ if [ "$EBK_LIFECYCLE_SCOPE" = affected ] && [ "$EBK_BASELINE_FOUND" = true ]; then
1664
+ case "$EBK_BASELINE_RUN_ID" in ''|*[!0-9]*) echo 'Baseline run id is malformed'; exit 1 ;; esac
1665
+ baseline_arguments=(
1666
+ --baselineReceipt="$RUNNER_TEMP/ebk-deployment-baseline/deployment/latest.json"
1667
+ --baselineRunId="$EBK_BASELINE_RUN_ID"
1668
+ --baselineRunAttempt=1
1669
+ )
1670
+ fi
1442
1671
  pnpm nx run ${name}:deploy-local -- \
1443
1672
  --stage="$EBK_STAGE" \
1444
- --expectedRoleArn="$AWS_DEPLOY_ROLE_ARN"
1673
+ --expectedRoleArn="$AWS_DEPLOY_ROLE_ARN" \
1674
+ --lifecycleScope="$EBK_LIFECYCLE_SCOPE" \
1675
+ "\${baseline_arguments[@]}"
1445
1676
  else
1446
1677
  [ -n "$EBK_ORDERED_PROJECTS" ] || { echo 'Committed deployment order is empty'; exit 1; }
1447
1678
  IFS=',' read -r -a projects <<< "$EBK_ORDERED_PROJECTS"
@@ -1453,6 +1684,29 @@ ${indentedScript(exactStageDeployerGuardScript('Native lifecycle', '$EBK_POLICY_
1453
1684
  pnpm nx run "$project:$EBK_ACTION" -- --stage="$EBK_STAGE" --region="$AWS_REGION" --expectedRoleArn="$AWS_DEPLOY_ROLE_ARN"
1454
1685
  done
1455
1686
  fi
1687
+ - name: Prove temporary smoke admission removed
1688
+ if: always() && env.EBK_ACTION == 'deploy' && steps.native-lifecycle.outcome != 'skipped'
1689
+ env:
1690
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1691
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1692
+ EBK_GITHUB_JOB_WORKFLOW_REF: \${{ job.workflow_ref }}
1693
+ GITHUB_TOKEN: \${{ github.token }}
1694
+ run: |
1695
+ set -euo pipefail
1696
+ ${indentedScript(exactStageDeployerGuardScript('Smoke access cleanup', '$EBK_POLICY_ACCOUNT_ID'), 10)}
1697
+ [ "$(git rev-parse HEAD)" = "$EBK_SOURCE_COMMIT" ]
1698
+ git diff --quiet HEAD
1699
+ node --input-type=module <<'NODE'
1700
+ import { cleanupGithubPrivateSmoke } from '@empire-builder-kit/nx/foundation';
1701
+ await cleanupGithubPrivateSmoke({
1702
+ account: process.env.EBK_POLICY_ACCOUNT_ID,
1703
+ deploymentProfile: process.env.EBK_DEPLOYMENT_PROFILE,
1704
+ root: process.cwd(),
1705
+ source: process.env.EBK_SOURCE_COMMIT,
1706
+ stage: process.env.EBK_STAGE,
1707
+ workspace: '${name}',
1708
+ });
1709
+ NODE
1456
1710
  - name: Seal exact staging promotion receipt
1457
1711
  if: env.EBK_ACTION == 'deploy' && env.EBK_STAGE == 'staging' && needs.prepare.outputs.profile != 'starter'
1458
1712
  env:
@@ -1486,6 +1740,672 @@ ${indentedScript(postdeploySupplyChainBindingScript(), 10)}
1486
1740
  .ebk/evidence/deployment/promotion.json
1487
1741
  .ebk/evidence/container-release/**
1488
1742
  retention-days: 30
1743
+ ${deploymentProfile === 'starter' ? '' : renderRehearseJob(options)}${trunkPromotion ? renderTrunkPromotionJobs(options) : ''}`;
1744
+ }
1745
+ /**
1746
+ * Managed Router WAF blocking rehearsal (EBK-INT-2026-079). The protected
1747
+ * dispatch runs the foundation rehearsal target under the staging deployment
1748
+ * role with seedProbes, so the runner performs the operator probes itself
1749
+ * inside the private-smoke allowlist admission, proves the admission removed,
1750
+ * and opens the pull request that carries the sealed evidence and receipt
1751
+ * back to develop through the ordinary review path.
1752
+ */
1753
+ function renderRehearseJob(options) {
1754
+ const { actionPins, name, toolVersions } = options;
1755
+ return `
1756
+ rehearse:
1757
+ name: rehearse staging Router WAF blocking
1758
+ needs: prepare
1759
+ if: >-
1760
+ needs.prepare.result == 'success' &&
1761
+ needs.prepare.outputs.action == 'rehearse' &&
1762
+ needs.prepare.outputs.stage == 'staging' &&
1763
+ needs.prepare.outputs.profile != 'starter'
1764
+ runs-on: ubuntu-latest
1765
+ environment: \${{ needs.prepare.outputs.stage }}
1766
+ permissions:
1767
+ actions: read
1768
+ contents: write
1769
+ id-token: write
1770
+ pull-requests: write
1771
+ env:
1772
+ AWS_DEPLOY_ROLE_ARN: \${{ needs.prepare.outputs.stage_deploy_role_arn }}
1773
+ AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
1774
+ AWS_PARTITION: \${{ needs.prepare.outputs.partition }}
1775
+ AWS_REGION: \${{ vars.AWS_REGION }}
1776
+ EBK_ACTION: rehearse
1777
+ EBK_DEPLOYMENT_PROFILE: \${{ needs.prepare.outputs.profile }}
1778
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.prepare.outputs.account_id }}
1779
+ EBK_POLICY_REGION: \${{ needs.prepare.outputs.region }}
1780
+ EBK_PROBE_URL: \${{ needs.prepare.outputs.probe_url }}
1781
+ EBK_SOURCE_COMMIT: \${{ inputs.source_commit || github.sha }}
1782
+ EBK_STAGE: \${{ needs.prepare.outputs.stage }}
1783
+ steps:
1784
+ - name: Check out immutable deployment source
1785
+ uses: ${actionPins.checkout}
1786
+ env:
1787
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1788
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1789
+ with:
1790
+ fetch-depth: 0
1791
+ persist-credentials: false
1792
+ ref: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
1793
+ - uses: ${actionPins.pnpmSetup}
1794
+ env:
1795
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1796
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1797
+ - uses: ${actionPins.setupNode}
1798
+ env:
1799
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1800
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1801
+ with:
1802
+ node-version: ${toolVersions.node}
1803
+ cache: pnpm
1804
+ - env:
1805
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1806
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1807
+ run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
1808
+ - name: Verify managed workspace integrity before AWS authority
1809
+ env:
1810
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1811
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1812
+ run: pnpm nx run ${name}:workspace-health
1813
+ - name: Require the exact pending rehearsal seeds
1814
+ env:
1815
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1816
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1817
+ run: |
1818
+ set -euo pipefail
1819
+ for seed in router-waf-live-evidence router-waf-blocking-rehearsal; do
1820
+ [ "$(jq -r '.status // empty' "packages/foundation/security/$seed.json")" = pending ] || {
1821
+ echo "packages/foundation/security/$seed.json must be reset to its pending seed before another rehearsal"; exit 1;
1822
+ }
1823
+ done
1824
+ - name: Revalidate exact protected branch tip before OIDC
1825
+ env:
1826
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1827
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1828
+ EBK_SOURCE_BRANCH: \${{ env.EBK_DEPLOYMENT_SOURCE_BRANCH }}
1829
+ EBK_SOURCE_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
1830
+ GH_REPO: \${{ github.repository }}
1831
+ GH_TOKEN: \${{ github.token }}
1832
+ run: |
1833
+ set -euo pipefail
1834
+ [ "$(git rev-parse HEAD)" = "$EBK_SOURCE_COMMIT" ] || {
1835
+ echo 'Deployment checkout differs from the exact protected source'; exit 1;
1836
+ }
1837
+ branch_tip="$(gh api "/repos/$GH_REPO/git/ref/heads/$EBK_SOURCE_BRANCH" --jq '.object.sha')"
1838
+ [ "$branch_tip" = "$EBK_SOURCE_COMMIT" ] || {
1839
+ echo 'Deployment source is no longer the exact protected branch tip'; exit 1;
1840
+ }
1841
+ - name: Assume the one stage deployment role
1842
+ uses: ${actionPins.configureAwsCredentials}
1843
+ with:
1844
+ audience: \${{ needs.prepare.outputs.oidc_audience }}
1845
+ aws-region: \${{ vars.AWS_REGION }}
1846
+ role-session-name: ebk-rehearse-\${{ github.run_id }}-\${{ github.run_attempt }}
1847
+ role-to-assume: \${{ env.AWS_DEPLOY_ROLE_ARN }}
1848
+ - name: Verify exact stage authority
1849
+ run: |
1850
+ set -euo pipefail
1851
+ [ "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ]
1852
+ [ "$AWS_REGION" = "$EBK_POLICY_REGION" ]
1853
+ ${indentedScript(exactStageDeployerGuardScript('Router WAF rehearsal', '$EBK_POLICY_ACCOUNT_ID'), 10)}
1854
+ - name: Rehearse Router WAF blocking with runner-seeded probes
1855
+ id: rehearsal
1856
+ env:
1857
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1858
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1859
+ EBK_GITHUB_JOB_WORKFLOW_REF: \${{ job.workflow_ref }}
1860
+ GITHUB_TOKEN: \${{ github.token }}
1861
+ run: |
1862
+ set -euo pipefail
1863
+ test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'Stage deployment role is absent'; exit 1; }
1864
+ pnpm nx run foundation:router-waf-rehearsal -- \\
1865
+ --probeUrl="$EBK_PROBE_URL" \\
1866
+ --seedProbes=true
1867
+ - name: Prove temporary smoke admission removed
1868
+ if: always() && steps.rehearsal.outcome != 'skipped'
1869
+ env:
1870
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1871
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1872
+ EBK_GITHUB_JOB_WORKFLOW_REF: \${{ job.workflow_ref }}
1873
+ GITHUB_TOKEN: \${{ github.token }}
1874
+ run: |
1875
+ set -euo pipefail
1876
+ ${indentedScript(exactStageDeployerGuardScript('Smoke access cleanup', '$EBK_POLICY_ACCOUNT_ID'), 10)}
1877
+ [ "$(git rev-parse HEAD)" = "$EBK_SOURCE_COMMIT" ]
1878
+ node --input-type=module <<'NODE'
1879
+ import { cleanupGithubPrivateSmoke } from '@empire-builder-kit/nx/foundation';
1880
+ await cleanupGithubPrivateSmoke({
1881
+ account: process.env.EBK_POLICY_ACCOUNT_ID,
1882
+ deploymentProfile: process.env.EBK_DEPLOYMENT_PROFILE,
1883
+ root: process.cwd(),
1884
+ source: process.env.EBK_SOURCE_COMMIT,
1885
+ stage: process.env.EBK_STAGE,
1886
+ workspace: '${name}',
1887
+ });
1888
+ NODE
1889
+ - name: Retain the sealed rehearsal evidence
1890
+ uses: ${actionPins.uploadArtifact}
1891
+ with:
1892
+ if-no-files-found: error
1893
+ name: router-waf-rehearsal-\${{ github.run_id }}-\${{ github.run_attempt }}
1894
+ path: |
1895
+ packages/foundation/security/router-waf-live-evidence.json
1896
+ packages/foundation/security/router-waf-blocking-rehearsal.json
1897
+ retention-days: 90
1898
+ - name: Open the rehearsal receipt pull request
1899
+ env:
1900
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1901
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1902
+ EBK_RUN_ATTEMPT: \${{ github.run_attempt }}
1903
+ EBK_RUN_ID: \${{ github.run_id }}
1904
+ GH_REPO: \${{ github.repository }}
1905
+ GH_TOKEN: \${{ github.token }}
1906
+ run: |
1907
+ set -euo pipefail
1908
+ case "$EBK_RUN_ID:$EBK_RUN_ATTEMPT" in *[!0-9:]*|:*|*:) exit 1 ;; esac
1909
+ branch="ebk/router-waf-rehearsal-$EBK_RUN_ID-$EBK_RUN_ATTEMPT"
1910
+ git diff --quiet -- packages/foundation/security && { echo 'The rehearsal sealed nothing new'; exit 1; }
1911
+ git config user.name 'ebk-router-waf-rehearsal[bot]'
1912
+ git config user.email 'ebk-router-waf-rehearsal[bot]@users.noreply.github.com'
1913
+ git checkout -q -b "$branch"
1914
+ git add packages/foundation/security/router-waf-live-evidence.json packages/foundation/security/router-waf-blocking-rehearsal.json
1915
+ git commit -q -m "chore(security): seal the staging Router WAF blocking rehearsal (run $EBK_RUN_ID)"
1916
+ git -c http.https://github.com/.extraheader="AUTHORIZATION: bearer $GH_TOKEN" push -q origin "HEAD:refs/heads/$branch"
1917
+ gh pr create \\
1918
+ --head "$branch" \\
1919
+ --base develop \\
1920
+ --title 'chore(security): seal the staging Router WAF blocking rehearsal' \\
1921
+ --body "Managed rehearsal run $EBK_RUN_ID attempt $EBK_RUN_ATTEMPT sealed the staging Router WAF blocking rehearsal for the committed policy digest. Merge to unblock the production Router WAF gate; the receipt binds the exact account, Region, and policy digest."
1922
+ `;
1923
+ }
1924
+ /**
1925
+ * Trunk promotion: after a successful staging lifecycle on develop, the same
1926
+ * run binds its own staging receipt, promotes the reviewed OCI indexes into
1927
+ * production, deploys production, and fast-forwards main to the deployed
1928
+ * commit. Every job carries the production environment so environment
1929
+ * reviewers can still gate it, and every step reuses the promotion-pr scripts.
1930
+ */
1931
+ function renderTrunkPromotionJobs(options) {
1932
+ const { actionPins, name, toolVersions } = options;
1933
+ const checkoutAndInstall = (integrityStep) => ` - name: Check out immutable deployment source
1934
+ uses: ${actionPins.checkout}
1935
+ env:
1936
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1937
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1938
+ with:
1939
+ fetch-depth: 0
1940
+ persist-credentials: false
1941
+ ref: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
1942
+ - uses: ${actionPins.pnpmSetup}
1943
+ env:
1944
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1945
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1946
+ - uses: ${actionPins.setupNode}
1947
+ env:
1948
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1949
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1950
+ with:
1951
+ node-version: ${toolVersions.node}
1952
+ cache: pnpm
1953
+ - env:
1954
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1955
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1956
+ run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
1957
+ - name: ${integrityStep}
1958
+ env:
1959
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1960
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1961
+ run: pnpm nx run ${name}:workspace-health`;
1962
+ const revalidateTip = ` - name: Revalidate exact protected branch tip before OIDC
1963
+ env:
1964
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
1965
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
1966
+ EBK_SOURCE_BRANCH: \${{ env.EBK_DEPLOYMENT_SOURCE_BRANCH }}
1967
+ EBK_SOURCE_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
1968
+ GH_REPO: \${{ github.repository }}
1969
+ GH_TOKEN: \${{ github.token }}
1970
+ run: |
1971
+ set -euo pipefail
1972
+ [ "$(git rev-parse HEAD)" = "$EBK_SOURCE_COMMIT" ] || {
1973
+ echo 'Deployment checkout differs from the exact protected source'; exit 1;
1974
+ }
1975
+ branch_tip="$(gh api "/repos/$GH_REPO/git/ref/heads/$EBK_SOURCE_BRANCH" --jq '.object.sha')"
1976
+ [ "$branch_tip" = "$EBK_SOURCE_COMMIT" ] || {
1977
+ echo 'Deployment source is no longer the exact protected branch tip'; exit 1;
1978
+ }`;
1979
+ const trunkPromotionGate = ` needs.prepare.outputs.action == 'deploy' &&
1980
+ needs.prepare.outputs.stage == 'staging' &&
1981
+ needs.prepare.outputs.profile != 'starter' &&
1982
+ needs.prepare.outputs.has_slices == 'true'`;
1983
+ return `
1984
+ promote-production:
1985
+ name: promote the staging-tested candidate to production
1986
+ needs: [prepare, immutable-artifacts, container-assemble, execute]
1987
+ if: >-
1988
+ always() &&
1989
+ needs.prepare.result == 'success' &&
1990
+ needs.immutable-artifacts.result == 'success' &&
1991
+ needs.container-assemble.result == 'success' &&
1992
+ needs.execute.result == 'success' &&
1993
+ ${trunkPromotionGate}
1994
+ runs-on: ubuntu-latest
1995
+ environment: production
1996
+ permissions:
1997
+ actions: read
1998
+ contents: read
1999
+ id-token: write
2000
+ outputs:
2001
+ artifact_digest: \${{ steps.retain.outputs.artifact-digest }}
2002
+ artifact_id: \${{ steps.retain.outputs.artifact-id }}
2003
+ candidate_artifact_digest: \${{ steps.staging.outputs.artifact_digest }}
2004
+ candidate_artifact_id: \${{ steps.staging.outputs.artifact_id }}
2005
+ candidate_material_digest: \${{ steps.staging.outputs.candidate_material_digest }}
2006
+ candidate_producer_run_attempt: \${{ steps.staging.outputs.producer_run_attempt }}
2007
+ candidate_producer_run_id: \${{ steps.staging.outputs.producer_run_id }}
2008
+ env:
2009
+ AWS_DEPLOY_ROLE_ARN: \${{ needs.prepare.outputs.production_deploy_role_arn }}
2010
+ AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
2011
+ AWS_PARTITION: \${{ needs.prepare.outputs.partition }}
2012
+ AWS_REGION: \${{ vars.AWS_REGION }}
2013
+ EBK_ACTION: deploy
2014
+ EBK_DEPLOYMENT_PROFILE: \${{ needs.prepare.outputs.profile }}
2015
+ EBK_POLICY_PRODUCTION_ACCOUNT_ID: \${{ needs.prepare.outputs.production_account_id }}
2016
+ EBK_POLICY_REGION: \${{ needs.prepare.outputs.region }}
2017
+ EBK_POLICY_STAGING_ACCOUNT_ID: \${{ needs.prepare.outputs.staging_account_id }}
2018
+ EBK_RELEASE_SOURCE_COMMIT: \${{ needs.immutable-artifacts.outputs.source_commit }}
2019
+ EBK_SOURCE_COMMIT: \${{ inputs.source_commit || github.sha }}
2020
+ EBK_STAGE: production
2021
+ EBK_SLICE_PROJECTS: \${{ needs.prepare.outputs.slice_projects }}
2022
+ steps:
2023
+ ${checkoutAndInstall('Verify managed workspace integrity before container authority')}
2024
+ - id: staging-receipt
2025
+ name: Select this run's staging deployment receipt
2026
+ env:
2027
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
2028
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
2029
+ EBK_DEPLOYMENT_RUN_ATTEMPT: \${{ github.run_attempt }}
2030
+ EBK_DEPLOYMENT_RUN_ID: \${{ github.run_id }}
2031
+ GH_REPO: \${{ github.repository }}
2032
+ GH_TOKEN: \${{ github.token }}
2033
+ run: |
2034
+ set -euo pipefail
2035
+ case "$EBK_DEPLOYMENT_RUN_ID:$EBK_DEPLOYMENT_RUN_ATTEMPT" in *[!0-9:]*|:*|*:) exit 1 ;; esac
2036
+ artifacts="$(gh api "/repos/$GH_REPO/actions/runs/$EBK_DEPLOYMENT_RUN_ID/artifacts?per_page=100")"
2037
+ receipt_name="deployment-receipt-staging-$EBK_DEPLOYMENT_RUN_ID-$EBK_DEPLOYMENT_RUN_ATTEMPT"
2038
+ receipt="$(printf '%s' "$artifacts" | jq -c --arg name "$receipt_name" \\
2039
+ '[.artifacts[] | select(.name == $name and .expired == false)] | if length == 1 then .[0] else empty end')"
2040
+ [ -n "$receipt" ] || { echo 'This run retained no exact staging deployment receipt'; exit 1; }
2041
+ artifact_id="$(printf '%s' "$receipt" | jq -r '.id // empty')"
2042
+ artifact_digest="$(printf '%s' "$receipt" | jq -r '.digest // empty')"
2043
+ case "$artifact_id" in ''|*[!0-9]*) exit 1 ;; esac
2044
+ case "$artifact_digest" in sha256:????????????????????????????????????????????????????????????????) ;; *) exit 1 ;; esac
2045
+ echo "artifact_digest=$artifact_digest" >> "$GITHUB_OUTPUT"
2046
+ echo "artifact_id=$artifact_id" >> "$GITHUB_OUTPUT"
2047
+ - name: Restore this run's staging deployment receipt
2048
+ uses: ${actionPins.downloadArtifact}
2049
+ env:
2050
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
2051
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
2052
+ with:
2053
+ artifact-ids: \${{ steps.staging-receipt.outputs.artifact_id }}
2054
+ github-token: \${{ github.token }}
2055
+ path: \${{ runner.temp }}/staging-deployment-receipt
2056
+ - id: staging
2057
+ name: Bind exact staging deployment and release evidence
2058
+ env:
2059
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
2060
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
2061
+ EBK_STAGING_DEPLOYMENT_RUN_ATTEMPT: \${{ github.run_attempt }}
2062
+ EBK_STAGING_DEPLOYMENT_RUN_ID: \${{ github.run_id }}
2063
+ EBK_EXPECTED_HAS_SLICES: \${{ needs.prepare.outputs.has_slices }}
2064
+ EBK_EXPECTED_DEPLOYMENT_PROFILE: \${{ needs.prepare.outputs.profile }}
2065
+ EBK_EXPECTED_DEPLOYMENT_REGION: \${{ needs.prepare.outputs.region }}
2066
+ EBK_EXPECTED_STAGING_ACCOUNT_ID: \${{ needs.prepare.outputs.staging_account_id }}
2067
+ EBK_STAGING_RECEIPT_ARTIFACT_DIGEST: \${{ steps.staging-receipt.outputs.artifact_digest }}
2068
+ EBK_STAGING_RECEIPT_ARTIFACT_ID: \${{ steps.staging-receipt.outputs.artifact_id }}
2069
+ # The receipt artifact keeps .ebk/evidence as its root (EBK-INT-2026-074).
2070
+ EBK_STAGING_RECEIPT_ROOT: \${{ runner.temp }}/staging-deployment-receipt/deployment
2071
+ EBK_STAGING_SOURCE_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
2072
+ GH_REPO: \${{ github.repository }}
2073
+ GH_TOKEN: \${{ github.token }}
2074
+ run: |
2075
+ node --input-type=module <<'NODE'
2076
+ ${indentedScript(stagingPromotionReceiptValidationScript(), 10)}
2077
+ NODE
2078
+ promotion="$EBK_STAGING_RECEIPT_ROOT/promotion.json"
2079
+ candidate_run_id="$(jq -r '.candidate.producerRunId' "$promotion")"
2080
+ candidate_run_attempt="$(jq -r '.candidate.producerRunAttempt' "$promotion")"
2081
+ candidate_artifact_id="$(jq -r '.candidate.artifactId' "$promotion")"
2082
+ candidate_artifact_digest="$(jq -r '.candidate.artifactDigest' "$promotion")"
2083
+ candidate_run="$(gh api "/repos/$GH_REPO/actions/runs/$candidate_run_id")"
2084
+ printf '%s' "$candidate_run" | jq -e \\
2085
+ --arg source "$EBK_STAGING_SOURCE_COMMIT" \\
2086
+ --arg attempt "$candidate_run_attempt" \\
2087
+ '.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
2088
+ candidate_artifacts="$(gh api "/repos/$GH_REPO/actions/runs/$candidate_run_id/artifacts?per_page=100")"
2089
+ printf '%s' "$candidate_artifacts" | jq -e \\
2090
+ --arg id "$candidate_artifact_id" \\
2091
+ --arg digest "$candidate_artifact_digest" \\
2092
+ --arg name "release-candidate-$EBK_STAGING_SOURCE_COMMIT" \\
2093
+ '[.artifacts[] | select((.id | tostring) == $id and .name == $name and .digest == $digest and .expired == false)] | length == 1' >/dev/null
2094
+ - name: Restore exact immutable release candidate for promotion
2095
+ uses: ${actionPins.downloadArtifact}
2096
+ env:
2097
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
2098
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
2099
+ with:
2100
+ artifact-ids: \${{ steps.staging.outputs.artifact_id }}
2101
+ github-token: \${{ github.token }}
2102
+ path: .ebk/evidence/release-candidate
2103
+ run-id: \${{ steps.staging.outputs.producer_run_id }}
2104
+ - name: Restore this run's staging container bundle
2105
+ uses: ${actionPins.downloadArtifact}
2106
+ env:
2107
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
2108
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
2109
+ with:
2110
+ artifact-ids: \${{ steps.staging.outputs.container_artifact_id }}
2111
+ github-token: \${{ github.token }}
2112
+ path: .ebk/evidence/container-release
2113
+ ${revalidateTip}
2114
+ - name: Assume exact staging registry pull role
2115
+ uses: ${actionPins.configureAwsCredentials}
2116
+ with:
2117
+ audience: \${{ needs.prepare.outputs.oidc_audience }}
2118
+ aws-region: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
2119
+ role-session-name: ebk-staging-container-pull-\${{ github.run_id }}-\${{ github.run_attempt }}
2120
+ role-to-assume: \${{ secrets.AWS_STAGING_PROMOTION_PULL_ROLE_ARN }}
2121
+ - name: Authenticate exact staging source registry
2122
+ env:
2123
+ AWS_STAGING_PROMOTION_PULL_ROLE_ARN: \${{ secrets.AWS_STAGING_PROMOTION_PULL_ROLE_ARN }}
2124
+ EBK_STAGING_ACCOUNT_ID: \${{ vars.AWS_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID }}
2125
+ EBK_STAGING_REGION: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
2126
+ run: |
2127
+ set -euo pipefail
2128
+ [ "$EBK_STAGING_ACCOUNT_ID" = "$EBK_POLICY_STAGING_ACCOUNT_ID" ]
2129
+ [ "$EBK_STAGING_REGION" = "$EBK_POLICY_REGION" ]
2130
+ ${indentedScript(exactStagingPromotionPullGuardScript(), 10)}
2131
+ [ "$EBK_STAGING_ACCOUNT_ID" != "$AWS_EXPECTED_ACCOUNT_ID" ]
2132
+ registry_suffix=.com
2133
+ case "$EBK_STAGING_REGION" in cn-*) registry_suffix=.com.cn ;; esac
2134
+ aws ecr get-login-password --region "$EBK_STAGING_REGION" |
2135
+ docker login --username AWS --password-stdin \\
2136
+ "$EBK_STAGING_ACCOUNT_ID.dkr.ecr.$EBK_STAGING_REGION.amazonaws$registry_suffix"
2137
+ ${revalidateTip}
2138
+ - name: Assume the production deployment role for container promotion
2139
+ uses: ${actionPins.configureAwsCredentials}
2140
+ with:
2141
+ audience: \${{ needs.prepare.outputs.oidc_audience }}
2142
+ aws-region: \${{ vars.AWS_REGION }}
2143
+ role-session-name: ebk-production-container-push-\${{ github.run_id }}-\${{ github.run_attempt }}
2144
+ role-to-assume: \${{ env.AWS_DEPLOY_ROLE_ARN }}
2145
+ - name: Promote exact staging indexes without rebuilding
2146
+ env:
2147
+ EBK_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID }}
2148
+ EBK_STAGING_PROMOTION_REGION: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
2149
+ run: |
2150
+ set -euo pipefail
2151
+ [ "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_PRODUCTION_ACCOUNT_ID" ]
2152
+ [ "$AWS_REGION" = "$EBK_POLICY_REGION" ]
2153
+ ${indentedScript(exactStageDeployerGuardScript('Container promotion', '$AWS_EXPECTED_ACCOUNT_ID'), 10)}
2154
+ IFS=',' read -r -a projects <<< "$EBK_SLICE_PROJECTS"
2155
+ for project in "\${projects[@]}"; do
2156
+ test -n "$project" || continue
2157
+ # Repository-relative for the container release tool (EBK-INT-2026-077).
2158
+ export EBK_CONTAINER_PROMOTION_SOURCE_BINDING_PATH=".ebk/evidence/container-release/staging/packages/$project/binding.json"
2159
+ export EBK_CONTAINER_PROMOTION_SOURCE_SIDECAR_PATH=".ebk/evidence/container-release/staging/packages/$project/release-bound-binding.json"
2160
+ pnpm nx run "$project:container-release-promote"
2161
+ done
2162
+ - id: retain
2163
+ name: Retain production target container binding
2164
+ uses: ${actionPins.uploadArtifact}
2165
+ with:
2166
+ name: container-release-production-\${{ github.run_id }}-\${{ github.run_attempt }}
2167
+ if-no-files-found: error
2168
+ include-hidden-files: true
2169
+ retention-days: 90
2170
+ path: .ebk/evidence/container-release
2171
+
2172
+ execute-production:
2173
+ name: native deploy production
2174
+ needs: [prepare, immutable-artifacts, promote-production]
2175
+ if: >-
2176
+ always() &&
2177
+ needs.prepare.result == 'success' &&
2178
+ needs.immutable-artifacts.result == 'success' &&
2179
+ needs.promote-production.result == 'success' &&
2180
+ ${trunkPromotionGate}
2181
+ runs-on: ubuntu-latest
2182
+ environment: production
2183
+ permissions:
2184
+ actions: read
2185
+ contents: read
2186
+ id-token: write
2187
+ env:
2188
+ AWS_DEPLOY_ROLE_ARN: \${{ needs.prepare.outputs.production_deploy_role_arn }}
2189
+ AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
2190
+ AWS_PARTITION: \${{ needs.prepare.outputs.partition }}
2191
+ AWS_REGION: \${{ vars.AWS_REGION }}
2192
+ EBK_ACTION: deploy
2193
+ EBK_ALLOW_PRODUCTION_REMOVE: \${{ vars.EBK_ALLOW_PRODUCTION_REMOVE }}
2194
+ EBK_CANDIDATE_ARTIFACT_DIGEST: \${{ needs.promote-production.outputs.candidate_artifact_digest }}
2195
+ EBK_CANDIDATE_ARTIFACT_ID: \${{ needs.promote-production.outputs.candidate_artifact_id }}
2196
+ EBK_CANDIDATE_MATERIAL_DIGEST: \${{ needs.promote-production.outputs.candidate_material_digest }}
2197
+ EBK_CANDIDATE_PRODUCER_RUN_ATTEMPT: \${{ needs.promote-production.outputs.candidate_producer_run_attempt }}
2198
+ EBK_CANDIDATE_PRODUCER_RUN_ID: \${{ needs.promote-production.outputs.candidate_producer_run_id }}
2199
+ EBK_DEPLOYMENT_PROFILE: \${{ needs.prepare.outputs.profile }}
2200
+ EBK_ORDERED_PROJECTS: \${{ needs.prepare.outputs.ordered_projects }}
2201
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.prepare.outputs.production_account_id }}
2202
+ EBK_POLICY_REGION: \${{ needs.prepare.outputs.region }}
2203
+ EBK_RELEASE_SOURCE_COMMIT: \${{ needs.immutable-artifacts.outputs.source_commit || inputs.source_commit || github.sha }}
2204
+ EBK_SLICE_PROJECTS: \${{ needs.prepare.outputs.slice_projects }}
2205
+ EBK_SOURCE_COMMIT: \${{ inputs.source_commit || github.sha }}
2206
+ EBK_STAGE: production
2207
+ # Production has no same-branch baseline receipt in this run; the
2208
+ # full lifecycle keeps its evidence complete.
2209
+ EBK_LIFECYCLE_SCOPE: full
2210
+ EBK_BASELINE_FOUND: 'false'
2211
+ EBK_BASELINE_RUN_ID: ''
2212
+ steps:
2213
+ ${checkoutAndInstall('Verify managed workspace integrity before AWS authority')}
2214
+ - name: Restore exact immutable release candidate
2215
+ uses: ${actionPins.downloadArtifact}
2216
+ env:
2217
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
2218
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
2219
+ with:
2220
+ artifact-ids: \${{ needs.promote-production.outputs.candidate_artifact_id }}
2221
+ github-token: \${{ github.token }}
2222
+ path: .ebk/evidence/release-candidate
2223
+ run-id: \${{ needs.promote-production.outputs.candidate_producer_run_id }}
2224
+ - name: Restore exact promoted production container binding
2225
+ uses: ${actionPins.downloadArtifact}
2226
+ env:
2227
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
2228
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
2229
+ with:
2230
+ name: container-release-production-\${{ github.run_id }}-\${{ github.run_attempt }}
2231
+ path: .ebk/evidence/container-release
2232
+ - name: Verify exact predeploy container binding inventory
2233
+ env:
2234
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
2235
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
2236
+ run: |
2237
+ set -euo pipefail
2238
+ IFS=',' read -r -a projects <<< "$EBK_SLICE_PROJECTS"
2239
+ for project in "\${projects[@]}"; do
2240
+ test -n "$project" || continue
2241
+ test -f ".ebk/evidence/container-release/$EBK_STAGE/packages/$project/binding.json" || {
2242
+ echo "Exact predeploy container binding is absent for $project"
2243
+ exit 1
2244
+ }
2245
+ done
2246
+ - name: Verify exact restored staging-tested release candidate
2247
+ env:
2248
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
2249
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
2250
+ run: |
2251
+ node --input-type=module <<'NODE'
2252
+ ${indentedScript(restoredCandidateValidationScript(), 10)}
2253
+ NODE
2254
+ - name: Authorize deployed Blueprint trust before protected mutation
2255
+ env:
2256
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
2257
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
2258
+ EBK_PROTECTED_BLUEPRINT_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
2259
+ run: |
2260
+ set -euo pipefail
2261
+ case "$EBK_PROTECTED_BLUEPRINT_COMMIT" in
2262
+ ''|*[!0-9a-f]*) echo 'Protected Blueprint authority must be one full lowercase Git object ID'; exit 1 ;;
2263
+ esac
2264
+ test "\${#EBK_PROTECTED_BLUEPRINT_COMMIT}" -eq 40 ||
2265
+ test "\${#EBK_PROTECTED_BLUEPRINT_COMMIT}" -eq 64 ||
2266
+ { echo 'Protected Blueprint authority must be one full Git object ID'; exit 1; }
2267
+ test "$(git rev-parse HEAD)" = "$EBK_PROTECTED_BLUEPRINT_COMMIT" ||
2268
+ { echo 'Protected Blueprint authority differs from the exact workflow checkout'; exit 1; }
2269
+ node tools/blueprint-trust-check.mjs \\
2270
+ --operation=deploy \\
2271
+ --protected-branch=main \\
2272
+ --protected-ref="$EBK_PROTECTED_BLUEPRINT_COMMIT" \\
2273
+ --output=.ebk/evidence/supply-chain/blueprint-trust.json
2274
+ - name: Produce subject-bound protected scanner evidence
2275
+ env:
2276
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
2277
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
2278
+ run: pnpm nx run ${name}:supply-chain-scan
2279
+ - name: Enforce protected supply-chain policy before mutation
2280
+ env:
2281
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
2282
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
2283
+ run: pnpm nx run ${name}:supply-chain-check-protected
2284
+ - name: Install pinned uv for generated Python workloads
2285
+ env:
2286
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
2287
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
2288
+ run: |
2289
+ set -euo pipefail
2290
+ python3 -m pip install --user --disable-pip-version-check 'uv==${tool_versions_js_1.WORKSPACE_UV_VERSION}'
2291
+ echo "$HOME/.local/bin" >> "$GITHUB_PATH"
2292
+ "$HOME/.local/bin/uv" --version
2293
+ - name: Install pinned Playwright Chromium for deployed smoke proofs
2294
+ env:
2295
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
2296
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
2297
+ run: |
2298
+ ${indentedScript(protectedBrowserInstallScript(), 10)}
2299
+ ${revalidateTip}
2300
+ - name: Assume the one stage deployment role
2301
+ uses: ${actionPins.configureAwsCredentials}
2302
+ with:
2303
+ audience: \${{ needs.prepare.outputs.oidc_audience }}
2304
+ aws-region: \${{ vars.AWS_REGION }}
2305
+ role-session-name: ebk-native-\${{ github.run_id }}-\${{ github.run_attempt }}
2306
+ role-to-assume: \${{ env.AWS_DEPLOY_ROLE_ARN }}
2307
+ - name: Verify exact stage authority
2308
+ run: |
2309
+ set -euo pipefail
2310
+ [ "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ]
2311
+ [ "$AWS_REGION" = "$EBK_POLICY_REGION" ]
2312
+ ${indentedScript(exactStageDeployerGuardScript('Native lifecycle', '$EBK_POLICY_ACCOUNT_ID'), 10)}
2313
+ - name: Run ordered native lifecycle targets
2314
+ id: native-lifecycle
2315
+ env:
2316
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
2317
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
2318
+ EBK_GITHUB_JOB_WORKFLOW_REF: \${{ job.workflow_ref }}
2319
+ GITHUB_TOKEN: \${{ github.token }}
2320
+ run: |
2321
+ set -euo pipefail
2322
+ test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'Stage deployment role is absent'; exit 1; }
2323
+ pnpm nx run ${name}:deploy-local -- \\
2324
+ --stage="$EBK_STAGE" \\
2325
+ --expectedRoleArn="$AWS_DEPLOY_ROLE_ARN" \\
2326
+ --lifecycleScope="$EBK_LIFECYCLE_SCOPE"
2327
+ - name: Prove temporary smoke admission removed
2328
+ if: always() && steps.native-lifecycle.outcome != 'skipped'
2329
+ env:
2330
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
2331
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
2332
+ EBK_GITHUB_JOB_WORKFLOW_REF: \${{ job.workflow_ref }}
2333
+ GITHUB_TOKEN: \${{ github.token }}
2334
+ run: |
2335
+ set -euo pipefail
2336
+ ${indentedScript(exactStageDeployerGuardScript('Smoke access cleanup', '$EBK_POLICY_ACCOUNT_ID'), 10)}
2337
+ [ "$(git rev-parse HEAD)" = "$EBK_SOURCE_COMMIT" ]
2338
+ git diff --quiet HEAD
2339
+ node --input-type=module <<'NODE'
2340
+ import { cleanupGithubPrivateSmoke } from '@empire-builder-kit/nx/foundation';
2341
+ await cleanupGithubPrivateSmoke({
2342
+ account: process.env.EBK_POLICY_ACCOUNT_ID,
2343
+ deploymentProfile: process.env.EBK_DEPLOYMENT_PROFILE,
2344
+ root: process.cwd(),
2345
+ source: process.env.EBK_SOURCE_COMMIT,
2346
+ stage: process.env.EBK_STAGE,
2347
+ workspace: '${name}',
2348
+ });
2349
+ NODE
2350
+ - name: Seal exact postdeploy supply-chain binding
2351
+ env:
2352
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
2353
+ ACTIONS_ID_TOKEN_REQUEST_URL: ''
2354
+ run: |
2355
+ node --input-type=module <<'NODE'
2356
+ ${indentedScript(postdeploySupplyChainBindingScript(), 10)}
2357
+ NODE
2358
+ - name: Retain bounded deployment receipt
2359
+ if: always()
2360
+ uses: ${actionPins.uploadArtifact}
2361
+ with:
2362
+ if-no-files-found: warn
2363
+ name: deployment-receipt-production-\${{ github.run_id }}-\${{ github.run_attempt }}
2364
+ path: |
2365
+ .ebk/evidence/deployment/latest.json
2366
+ .ebk/evidence/deployment/postdeploy-supply-chain.json
2367
+ .ebk/evidence/deployment/promotion.json
2368
+ .ebk/evidence/container-release/**
2369
+ retention-days: 30
2370
+
2371
+ record-main:
2372
+ name: record the deployed commit on main
2373
+ needs: [prepare, execute-production]
2374
+ if: >-
2375
+ always() &&
2376
+ needs.prepare.result == 'success' &&
2377
+ needs.execute-production.result == 'success' &&
2378
+ ${trunkPromotionGate}
2379
+ runs-on: ubuntu-latest
2380
+ environment: production
2381
+ permissions:
2382
+ contents: write
2383
+ steps:
2384
+ - name: Fast-forward protected main to the deployed commit
2385
+ env:
2386
+ EBK_RECORD_MAIN_TOKEN: \${{ secrets.${exports.EBK_RECORD_MAIN_TOKEN_SECRET} }}
2387
+ EBK_SOURCE_BRANCH: \${{ env.EBK_DEPLOYMENT_SOURCE_BRANCH }}
2388
+ EBK_SOURCE_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
2389
+ GH_REPO: \${{ github.repository }}
2390
+ run: |
2391
+ set -euo pipefail
2392
+ test -n "$EBK_RECORD_MAIN_TOKEN" || { echo '${exports.EBK_RECORD_MAIN_TOKEN_SECRET} is absent from the production environment'; exit 1; }
2393
+ [ "$EBK_SOURCE_BRANCH" = develop ] || { echo 'Trunk promotion records main only from develop'; exit 1; }
2394
+ case "$EBK_SOURCE_COMMIT" in ''|*[!0-9a-f]*) exit 1 ;; esac
2395
+ export GH_TOKEN="$EBK_RECORD_MAIN_TOKEN"
2396
+ develop_tip="$(gh api "/repos/$GH_REPO/git/ref/heads/develop" --jq '.object.sha')"
2397
+ [ "$develop_tip" = "$EBK_SOURCE_COMMIT" ] || { echo 'Deployed commit is no longer the develop tip; leaving main unchanged'; exit 1; }
2398
+ main_tip="$(gh api "/repos/$GH_REPO/git/ref/heads/main" --jq '.object.sha')"
2399
+ if [ "$main_tip" = "$EBK_SOURCE_COMMIT" ]; then
2400
+ echo 'main already records the deployed commit'
2401
+ exit 0
2402
+ fi
2403
+ # GitHub refuses a non-fast-forward reference update unless force is
2404
+ # requested; it is never requested here.
2405
+ gh api -X PATCH "/repos/$GH_REPO/git/refs/heads/main" -f sha="$EBK_SOURCE_COMMIT" -F force=false >/dev/null
2406
+ recorded="$(gh api "/repos/$GH_REPO/git/ref/heads/main" --jq '.object.sha')"
2407
+ [ "$recorded" = "$EBK_SOURCE_COMMIT" ] || { echo 'main did not record the deployed commit'; exit 1; }
2408
+ echo "main now records $EBK_SOURCE_COMMIT (previously $main_tip)"
1489
2409
  `;
1490
2410
  }
1491
2411
  function renderThinDeployWorkflowFilePlan(options) {