@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.
- package/package.json +3 -3
- package/src/aws-authority/stage-authority.js +4 -0
- package/src/aws-authority/stage-authority.js.map +1 -1
- package/src/developer-agent/model.d.ts +1 -1
- package/src/developer-agent/operation-registry.js +25 -3
- package/src/developer-agent/operation-registry.js.map +1 -1
- package/src/developer-agent/planning.js +9 -4
- package/src/developer-agent/planning.js.map +1 -1
- package/src/developer-agent/validation.js +5 -1
- package/src/developer-agent/validation.js.map +1 -1
- package/src/executors/deployment-plan/deployment-plan.js +3 -0
- package/src/executors/deployment-plan/deployment-plan.js.map +1 -1
- package/src/executors/foundation-profile-preflight/production-database-safeguard.d.ts +1 -0
- package/src/executors/foundation-profile-preflight/production-database-safeguard.js +4 -2
- package/src/executors/foundation-profile-preflight/production-database-safeguard.js.map +1 -1
- package/src/executors/github-bootstrap/github-bootstrap.js +1 -0
- package/src/executors/github-bootstrap/github-bootstrap.js.map +1 -1
- package/src/executors/local-deploy/local-deploy.d.ts +21 -3
- package/src/executors/local-deploy/local-deploy.js +201 -1
- package/src/executors/local-deploy/local-deploy.js.map +1 -1
- package/src/executors/local-deploy/schema.json +21 -0
- package/src/executors/router-waf-rehearsal/router-waf-rehearsal.d.ts +13 -0
- package/src/executors/router-waf-rehearsal/router-waf-rehearsal.js +323 -137
- package/src/executors/router-waf-rehearsal/router-waf-rehearsal.js.map +1 -1
- package/src/executors/router-waf-rehearsal/schema.json +5 -0
- package/src/executors/secret-initialize/secret-initialize.d.ts +2 -0
- package/src/executors/secret-initialize/secret-initialize.js +57 -3
- package/src/executors/secret-initialize/secret-initialize.js.map +1 -1
- package/src/executors/sst-lifecycle/run-sst-command.d.ts +2 -0
- package/src/executors/sst-lifecycle/run-sst-command.js +4 -0
- package/src/executors/sst-lifecycle/run-sst-command.js.map +1 -1
- package/src/foundation/index.d.ts +1 -0
- package/src/foundation/index.js +8 -0
- package/src/foundation/index.js.map +1 -1
- package/src/foundation/private-smoke-access.d.ts +23 -0
- package/src/foundation/private-smoke-access.js +127 -0
- package/src/foundation/private-smoke-access.js.map +1 -0
- package/src/foundation/private-smoke-admission.d.ts +43 -0
- package/src/foundation/private-smoke-admission.js +158 -0
- package/src/foundation/private-smoke-admission.js.map +1 -0
- package/src/foundation/private-smoke-control-plane.d.ts +18 -0
- package/src/foundation/private-smoke-control-plane.js +173 -0
- package/src/foundation/private-smoke-control-plane.js.map +1 -0
- package/src/generators/preset/dependency-overrides.d.ts +8 -0
- package/src/generators/preset/dependency-overrides.js +55 -0
- package/src/generators/preset/dependency-overrides.js.map +1 -0
- package/src/generators/preset/tool-versions.d.ts +2 -0
- package/src/generators/preset/tool-versions.js +3 -1
- package/src/generators/preset/tool-versions.js.map +1 -1
- package/src/generators/preset/workspace-files-developer-agent.js +19 -6
- package/src/generators/preset/workspace-files-developer-agent.js.map +1 -1
- package/src/generators/preset/workspace-files-thin-deploy.d.ts +16 -1
- package/src/generators/preset/workspace-files-thin-deploy.js +946 -26
- package/src/generators/preset/workspace-files-thin-deploy.js.map +1 -1
- package/src/generators/preset/workspace-files-thin-docs.d.ts +2 -0
- package/src/generators/preset/workspace-files-thin-docs.js +66 -6
- package/src/generators/preset/workspace-files-thin-docs.js.map +1 -1
- package/src/generators/preset/workspace-files.d.ts +8 -1
- package/src/generators/preset/workspace-files.js +97 -21
- package/src/generators/preset/workspace-files.js.map +1 -1
- package/src/generators/repository-policy/repository-policy.js +1 -0
- package/src/generators/repository-policy/repository-policy.js.map +1 -1
- package/src/generators/repository-policy/schema.d.ts +2 -0
- package/src/generators/repository-policy/schema.json +27 -7
- package/src/generators/slice/built-in-plan-background-work-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-background-work-files.js +89 -0
- package/src/generators/slice/built-in-plan-background-work-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-database-lifecycle-files.js +20 -4
- package/src/generators/slice/built-in-plan-database-lifecycle-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-infrastructure-files.js +22 -9
- package/src/generators/slice/built-in-plan-infrastructure-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-operations-files.js +8 -10
- package/src/generators/slice/built-in-plan-operations-files.js.map +1 -1
- package/src/generators/slice/built-in-plan.js +3 -0
- package/src/generators/slice/built-in-plan.js.map +1 -1
- package/src/generators/upgrade/upgrade.d.ts +12 -0
- package/src/generators/upgrade/upgrade.js +84 -9
- package/src/generators/upgrade/upgrade.js.map +1 -1
- package/src/generators/workload/workload.js +1 -1
- package/src/git-policy/action-pins.d.ts +5 -0
- package/src/git-policy/action-pins.js +5 -0
- package/src/git-policy/action-pins.js.map +1 -1
- package/src/git-policy/github-bootstrap.d.ts +10 -2
- package/src/git-policy/github-bootstrap.js +129 -34
- package/src/git-policy/github-bootstrap.js.map +1 -1
- package/src/git-policy/routing.d.ts +5 -1
- package/src/git-policy/routing.js +50 -0
- package/src/git-policy/routing.js.map +1 -1
- package/src/performance/model.d.ts +1 -1
- package/src/repository-policy/capacity-migration.d.ts +3 -0
- package/src/repository-policy/capacity-migration.js +21 -0
- package/src/repository-policy/capacity-migration.js.map +1 -0
- package/src/repository-policy/foundation-profile.js +10 -0
- package/src/repository-policy/foundation-profile.js.map +1 -1
- package/src/repository-policy/policy.d.ts +13 -2
- package/src/repository-policy/policy.js +47 -2
- package/src/repository-policy/policy.js.map +1 -1
- package/src/schemas/repository-policy-v1.json +56 -17
- package/src/supply-chain/generated-container-release-command.js +281 -74
- package/src/supply-chain/generated-container-release-command.js.map +1 -1
- package/src/supply-chain/generated-files.js +21 -11
- package/src/supply-chain/generated-files.js.map +1 -1
- package/src/supply-chain/generated-scanner-command.js +1 -1
- package/src/supply-chain/scanner.js +3 -0
- package/src/supply-chain/scanner.js.map +1 -1
- package/src/workspace-deployment/affected-lifecycle.d.ts +53 -0
- package/src/workspace-deployment/affected-lifecycle.js +84 -0
- package/src/workspace-deployment/affected-lifecycle.js.map +1 -0
- package/src/workspace-deployment/index.d.ts +1 -0
- package/src/workspace-deployment/index.js +1 -0
- 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
|
-
|
|
63
|
+
function renderThinDeployVerifyCaller(profile, deliveryLane = 'protected-pair', productionApprovalMode = 'promotion-pr') {
|
|
64
|
+
const trunk = deliveryLane === 'trunk';
|
|
65
|
+
if (profile === 'starter' && !trunk)
|
|
56
66
|
return '';
|
|
57
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
-
|
|
1199
|
-
|
|
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) {
|