@empire-builder-kit/nx 1.0.0-rc.33 → 1.0.0-rc.35
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/executors/deployment-plan/deployment-plan.js +3 -0
- package/src/executors/deployment-plan/deployment-plan.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 +12 -3
- package/src/executors/local-deploy/local-deploy.js +156 -0
- 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/generators/preset/workspace-files-thin-deploy.d.ts +14 -1
- package/src/generators/preset/workspace-files-thin-deploy.js +897 -28
- 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 +51 -6
- package/src/generators/preset/workspace-files-thin-docs.js.map +1 -1
- package/src/generators/preset/workspace-files.d.ts +7 -1
- package/src/generators/preset/workspace-files.js +80 -18
- 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 +26 -6
- package/src/generators/slice/built-in-plan-infrastructure-files.js +12 -0
- package/src/generators/slice/built-in-plan-infrastructure-files.js.map +1 -1
- package/src/generators/upgrade/upgrade.d.ts +12 -0
- package/src/generators/upgrade/upgrade.js +68 -0
- package/src/generators/upgrade/upgrade.js.map +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/repository-policy/policy.d.ts +12 -1
- package/src/repository-policy/policy.js +42 -2
- package/src/repository-policy/policy.js.map +1 -1
- package/src/schemas/repository-policy-v1.json +55 -16
- 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 +10 -11
- package/src/supply-chain/generated-files.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,6 +1,6 @@
|
|
|
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
5
|
exports.protectedBrowserInstallScript = protectedBrowserInstallScript;
|
|
6
6
|
exports.exactStageDeployerGuardScript = exactStageDeployerGuardScript;
|
|
@@ -13,10 +13,17 @@ exports.postdeploySupplyChainBindingScript = postdeploySupplyChainBindingScript;
|
|
|
13
13
|
exports.renderThinDeployWorkflowFilePlan = renderThinDeployWorkflowFilePlan;
|
|
14
14
|
const tool_versions_js_1 = require("./tool-versions.js");
|
|
15
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';
|
|
16
18
|
const EXACT_SEMVER = /^\d+\.\d+\.\d+$/;
|
|
17
19
|
const WORKSPACE_NAME = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
18
20
|
const ACTION_PIN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+@[a-f0-9]{40}(?: # [^\r\n]+)?$/;
|
|
19
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
|
+
}
|
|
20
27
|
if (!WORKSPACE_NAME.test(options.name)) {
|
|
21
28
|
throw new Error('Workspace name must be a bounded lowercase slug.');
|
|
22
29
|
}
|
|
@@ -31,8 +38,8 @@ function assertOptions(options) {
|
|
|
31
38
|
}
|
|
32
39
|
}
|
|
33
40
|
}
|
|
34
|
-
function workflowCallTrigger(profile) {
|
|
35
|
-
return profile === 'starter'
|
|
41
|
+
function workflowCallTrigger(profile, deliveryLane = 'protected-pair') {
|
|
42
|
+
return profile === 'starter' && deliveryLane !== 'trunk'
|
|
36
43
|
? ''
|
|
37
44
|
: ` workflow_call:
|
|
38
45
|
inputs:
|
|
@@ -53,10 +60,23 @@ function workflowCallTrigger(profile) {
|
|
|
53
60
|
type: string
|
|
54
61
|
`;
|
|
55
62
|
}
|
|
56
|
-
function renderThinDeployVerifyCaller(profile) {
|
|
57
|
-
|
|
63
|
+
function renderThinDeployVerifyCaller(profile, deliveryLane = 'protected-pair', productionApprovalMode = 'promotion-pr') {
|
|
64
|
+
const trunk = deliveryLane === 'trunk';
|
|
65
|
+
if (profile === 'starter' && !trunk)
|
|
58
66
|
return '';
|
|
59
|
-
|
|
67
|
+
// GitHub validates every nested deploy.yml job against this caller's
|
|
68
|
+
// permissions on every event, so the ceiling must admit the managed
|
|
69
|
+
// rehearsal, which pushes its sealed receipt branch and opens the pull
|
|
70
|
+
// request with the workflow token (isolated profiles only).
|
|
71
|
+
const nestedWrite = profile !== 'starter';
|
|
72
|
+
// Trunk lane: every merged main push is a production delivery.
|
|
73
|
+
// Trunk promotion: production deploys from the develop run itself and
|
|
74
|
+
// record-main fast-forwards main afterwards, so a main push only verifies.
|
|
75
|
+
const protectedBranchCondition = trunk
|
|
76
|
+
? "github.ref == 'refs/heads/main'"
|
|
77
|
+
: productionApprovalMode === 'trunk-promotion'
|
|
78
|
+
? "github.ref == 'refs/heads/develop'"
|
|
79
|
+
: "(github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main')";
|
|
60
80
|
return `
|
|
61
81
|
|
|
62
82
|
deploy:
|
|
@@ -68,18 +88,26 @@ function renderThinDeployVerifyCaller(profile) {
|
|
|
68
88
|
${protectedBranchCondition} &&
|
|
69
89
|
needs.acceptance.result == 'success' &&
|
|
70
90
|
needs.acceptance.outputs.history_only != 'true' &&
|
|
71
|
-
needs.release-candidate.result == 'success'
|
|
91
|
+
(needs.release-candidate.result == 'success' ||
|
|
92
|
+
(github.ref == 'refs/heads/main' && needs.release-candidate.result == 'skipped'))
|
|
93
|
+
# Ceiling for the nested deploy.yml jobs; each nested job declares its
|
|
94
|
+
# own least-privilege set. Isolated profiles admit the rehearse job's
|
|
95
|
+
# receipt push and pull request (EBK-INT-2026-080).
|
|
72
96
|
permissions:
|
|
73
97
|
actions: read
|
|
74
|
-
contents: read
|
|
75
|
-
id-token: write
|
|
98
|
+
contents: ${nestedWrite ? 'write' : 'read'}
|
|
99
|
+
id-token: write${nestedWrite ? '\n pull-requests: write' : ''}
|
|
76
100
|
uses: ./.github/workflows/deploy.yml
|
|
77
101
|
with:
|
|
78
102
|
source_branch: \${{ github.ref_name }}
|
|
79
103
|
source_commit: \${{ github.sha }}
|
|
80
104
|
source_ref: \${{ github.ref }}
|
|
81
105
|
verify_run_attempt: \${{ github.run_attempt }}
|
|
82
|
-
verify_run_id: \${{ github.run_id }}
|
|
106
|
+
verify_run_id: \${{ github.run_id }}
|
|
107
|
+
# The reusable workflow reads the production environment secret
|
|
108
|
+
# AWS_STAGING_PROMOTION_PULL_ROLE_ARN, which only reaches a called
|
|
109
|
+
# workflow when the caller inherits secrets (EBK-INT-2026-075).
|
|
110
|
+
secrets: inherit`;
|
|
83
111
|
}
|
|
84
112
|
function indentedScript(source, spaces) {
|
|
85
113
|
const prefix = ' '.repeat(spaces);
|
|
@@ -334,6 +362,13 @@ if (hasSlices) {
|
|
|
334
362
|
const artifactId = required('EBK_CANDIDATE_ARTIFACT_ID', /^[1-9][0-9]*$/);
|
|
335
363
|
const artifactDigest = required('EBK_CANDIDATE_ARTIFACT_DIGEST', /^sha256:[a-f0-9]{64}$/);
|
|
336
364
|
const containerArtifactId = required('EBK_CONTAINER_ARTIFACT_ID', /^[1-9][0-9]*$/);
|
|
365
|
+
// actions/upload-artifact emits the bare SHA-256 hex in artifact-digest while
|
|
366
|
+
// the artifacts API and the promotion compare the sha256:<hex> registry form
|
|
367
|
+
// (EBK-INT-2026-073). Normalize before the exact validation.
|
|
368
|
+
const containerArtifactDigestRaw = process.env.EBK_CONTAINER_ARTIFACT_DIGEST?.trim() ?? '';
|
|
369
|
+
process.env.EBK_CONTAINER_ARTIFACT_DIGEST = /^[a-f0-9]{64}$/.test(containerArtifactDigestRaw)
|
|
370
|
+
? 'sha256:' + containerArtifactDigestRaw
|
|
371
|
+
: containerArtifactDigestRaw;
|
|
337
372
|
const containerArtifactDigest = required('EBK_CONTAINER_ARTIFACT_DIGEST', /^sha256:[a-f0-9]{64}$/);
|
|
338
373
|
const root = '.ebk/evidence/release-candidate/packages';
|
|
339
374
|
const projects = readdirSync(root, { withFileTypes: true })
|
|
@@ -442,22 +477,35 @@ writeFileSync(
|
|
|
442
477
|
}
|
|
443
478
|
function renderThinDeployWorkflow(options) {
|
|
444
479
|
const { actionPins, deploymentProfile, name, toolVersions } = options;
|
|
480
|
+
const trunkPromotion = options.productionApprovalMode === 'trunk-promotion';
|
|
445
481
|
return `name: Deploy
|
|
446
482
|
run-name: Deploy \${{ inputs.source_commit || github.sha }}
|
|
447
483
|
|
|
448
484
|
on:
|
|
449
|
-
${workflowCallTrigger(deploymentProfile)} workflow_dispatch:
|
|
485
|
+
${workflowCallTrigger(deploymentProfile, options.deliveryLane)} workflow_dispatch:
|
|
450
486
|
inputs:
|
|
451
487
|
action:
|
|
452
488
|
description: Native lifecycle action
|
|
453
489
|
required: true
|
|
454
490
|
default: deploy
|
|
455
491
|
type: choice
|
|
456
|
-
options: [deploy, refresh, unlock, remove]
|
|
492
|
+
options: [deploy, refresh, unlock, remove${deploymentProfile === 'starter' ? '' : ', rehearse'}]
|
|
457
493
|
project:
|
|
458
494
|
description: Required registered project for unlock or remove recovery
|
|
459
495
|
required: false
|
|
460
|
-
type: string
|
|
496
|
+
type: string${deploymentProfile === 'starter'
|
|
497
|
+
? ''
|
|
498
|
+
: `
|
|
499
|
+
probe_url:
|
|
500
|
+
description: rehearse only; exact HTTPS Router health URL in /<kebab-slice>/api/health form for the staging blocking rehearsal
|
|
501
|
+
required: false
|
|
502
|
+
type: string`}
|
|
503
|
+
lifecycle:
|
|
504
|
+
description: affected skips Slices unchanged since the newest successful deployment; full deploys everything
|
|
505
|
+
required: false
|
|
506
|
+
default: affected
|
|
507
|
+
type: choice
|
|
508
|
+
options: [affected, full]
|
|
461
509
|
|
|
462
510
|
concurrency:
|
|
463
511
|
group: protected-lifecycle-\${{ inputs.source_branch || github.ref_name }}
|
|
@@ -478,6 +526,7 @@ jobs:
|
|
|
478
526
|
runs-on: ubuntu-latest
|
|
479
527
|
outputs:
|
|
480
528
|
action: \${{ steps.route.outputs.action }}
|
|
529
|
+
probe_url: \${{ steps.route.outputs.probe_url }}
|
|
481
530
|
account_id: \${{ steps.account.outputs.account_id }}
|
|
482
531
|
has_slices: \${{ steps.policy.outputs.has_slices }}
|
|
483
532
|
ordered_projects: \${{ steps.policy.outputs.ordered_projects }}
|
|
@@ -487,9 +536,14 @@ jobs:
|
|
|
487
536
|
project: \${{ steps.route.outputs.project }}
|
|
488
537
|
region: \${{ steps.policy.outputs.deployment_region }}
|
|
489
538
|
slice_projects: \${{ steps.policy.outputs.slice_projects }}
|
|
539
|
+
slice_matrix: \${{ steps.policy.outputs.slice_matrix }}
|
|
490
540
|
staging_account_id: \${{ steps.policy.outputs.staging_account_id }}
|
|
491
541
|
stage: \${{ steps.route.outputs.stage }}
|
|
492
|
-
stage_deploy_role_arn: \${{ steps.account.outputs.role_arn }}
|
|
542
|
+
stage_deploy_role_arn: \${{ steps.account.outputs.role_arn }}${trunkPromotion
|
|
543
|
+
? `
|
|
544
|
+
production_account_id: \${{ steps.production-account.outputs.account_id }}
|
|
545
|
+
production_deploy_role_arn: \${{ steps.production-account.outputs.role_arn }}`
|
|
546
|
+
: ''}
|
|
493
547
|
steps:
|
|
494
548
|
- name: Verify exact protected branch tip before setup
|
|
495
549
|
env:
|
|
@@ -551,6 +605,7 @@ jobs:
|
|
|
551
605
|
name: Resolve protected stage and action
|
|
552
606
|
env:
|
|
553
607
|
EBK_DISPATCH_ACTION: \${{ inputs.action || '' }}
|
|
608
|
+
EBK_DISPATCH_PROBE_URL: \${{ inputs.probe_url || '' }}
|
|
554
609
|
EBK_DISPATCH_PROJECT: \${{ inputs.project || '' }}
|
|
555
610
|
EBK_PROFILE: ${deploymentProfile}
|
|
556
611
|
EBK_SOURCE_BRANCH: \${{ env.EBK_DEPLOYMENT_SOURCE_BRANCH }}
|
|
@@ -566,12 +621,25 @@ jobs:
|
|
|
566
621
|
fi
|
|
567
622
|
case "$EBK_SOURCE_COMMIT" in ''|*[!0-9a-f]*) exit 1 ;; esac
|
|
568
623
|
{ [ "\${#EBK_SOURCE_COMMIT}" -eq 40 ] || [ "\${#EBK_SOURCE_COMMIT}" -eq 64 ]; } || exit 1
|
|
569
|
-
case "$action" in deploy|refresh|unlock|remove) ;; *) exit 1 ;; esac
|
|
624
|
+
case "$action" in deploy|refresh|unlock|remove|rehearse) ;; *) exit 1 ;; esac
|
|
570
625
|
case "$project" in ''|*[!A-Za-z0-9_-]*) [ -z "$project" ] || exit 1 ;; esac
|
|
571
626
|
case "$action" in
|
|
572
627
|
unlock|remove) [ -n "$project" ] || { echo 'unlock and remove require an explicit project'; exit 1; } ;;
|
|
573
628
|
deploy|refresh) [ -z "$project" ] || { echo 'deploy and refresh operate on the policy-derived project order'; exit 1; } ;;
|
|
629
|
+
rehearse)
|
|
630
|
+
[ -z "$project" ] || { echo 'rehearse targets the foundation Router WAF and takes no project'; exit 1; }
|
|
631
|
+
[ "$EBK_PROFILE" != starter ] || { echo 'starter rehearses development locally; rehearse is for isolated staging'; exit 1; }
|
|
632
|
+
[ "$EBK_SOURCE_BRANCH" = develop ] || { echo 'rehearse runs from develop against staging'; exit 1; }
|
|
633
|
+
case "$EBK_DISPATCH_PROBE_URL" in
|
|
634
|
+
https://*/*/api/health) ;;
|
|
635
|
+
*) echo 'rehearse requires probe_url in https://<router>/<kebab-slice>/api/health form'; exit 1 ;;
|
|
636
|
+
esac
|
|
637
|
+
;;
|
|
574
638
|
esac
|
|
639
|
+
# The committed deployment plan knows deploy, refresh, unlock, and
|
|
640
|
+
# remove; a rehearsal derives the same project inventory as deploy.
|
|
641
|
+
plan_action="$action"
|
|
642
|
+
[ "$action" != rehearse ] || plan_action=deploy
|
|
575
643
|
case "$EBK_SOURCE_BRANCH" in
|
|
576
644
|
main) stage=production ;;
|
|
577
645
|
develop)
|
|
@@ -584,12 +652,14 @@ jobs:
|
|
|
584
652
|
*) echo 'Protected lifecycle must run from develop or main'; exit 1 ;;
|
|
585
653
|
esac
|
|
586
654
|
echo "action=$action" >> "$GITHUB_OUTPUT"
|
|
655
|
+
echo "plan_action=$plan_action" >> "$GITHUB_OUTPUT"
|
|
656
|
+
echo "probe_url=$EBK_DISPATCH_PROBE_URL" >> "$GITHUB_OUTPUT"
|
|
587
657
|
echo "project=$project" >> "$GITHUB_OUTPUT"
|
|
588
658
|
echo "stage=$stage" >> "$GITHUB_OUTPUT"
|
|
589
659
|
- id: policy
|
|
590
660
|
name: Derive ordered projects from committed policy
|
|
591
661
|
env:
|
|
592
|
-
EBK_ACTION: \${{ steps.route.outputs.
|
|
662
|
+
EBK_ACTION: \${{ steps.route.outputs.plan_action }}
|
|
593
663
|
EBK_PROJECT: \${{ steps.route.outputs.project }}
|
|
594
664
|
EBK_STAGE: \${{ steps.route.outputs.stage }}
|
|
595
665
|
run: |
|
|
@@ -611,7 +681,22 @@ jobs:
|
|
|
611
681
|
EBK_STAGE: \${{ steps.route.outputs.stage }}
|
|
612
682
|
EBK_STAGING_ACCOUNT: \${{ steps.policy.outputs.staging_account_id }}
|
|
613
683
|
run: |
|
|
614
|
-
${indentedScript(`set -euo pipefail\n${canonicalStageAuthorityDerivationScript()}`, 10)}
|
|
684
|
+
${indentedScript(`set -euo pipefail\n${canonicalStageAuthorityDerivationScript()}`, 10)}${trunkPromotion
|
|
685
|
+
? `
|
|
686
|
+
# Trunk promotion deploys production from the develop run, so the
|
|
687
|
+
# production authority is derived here beside the staging authority.
|
|
688
|
+
- id: production-account
|
|
689
|
+
name: Derive canonical committed production authority for trunk promotion
|
|
690
|
+
if: steps.route.outputs.stage == 'staging'
|
|
691
|
+
env:
|
|
692
|
+
EBK_DEVELOPMENT_ACCOUNT: \${{ steps.policy.outputs.development_account_id }}
|
|
693
|
+
EBK_PRODUCTION_ACCOUNT: \${{ steps.policy.outputs.production_account_id }}
|
|
694
|
+
EBK_REGION: \${{ steps.policy.outputs.deployment_region }}
|
|
695
|
+
EBK_STAGE: production
|
|
696
|
+
EBK_STAGING_ACCOUNT: \${{ steps.policy.outputs.staging_account_id }}
|
|
697
|
+
run: |
|
|
698
|
+
${indentedScript(`set -euo pipefail\n${canonicalStageAuthorityDerivationScript()}`, 10)}`
|
|
699
|
+
: ''}
|
|
615
700
|
|
|
616
701
|
immutable-artifacts:
|
|
617
702
|
name: prepare exact immutable release artifacts
|
|
@@ -634,6 +719,11 @@ ${indentedScript(`set -euo pipefail\n${canonicalStageAuthorityDerivationScript()
|
|
|
634
719
|
container_artifact_id: \${{ steps.staging.outputs.container_artifact_id }}
|
|
635
720
|
staging_deployment_run_attempt: \${{ steps.staging.outputs.deployment_run_attempt }}
|
|
636
721
|
staging_deployment_run_id: \${{ steps.staging.outputs.deployment_run_id }}
|
|
722
|
+
baseline_artifact_digest: \${{ steps.baseline.outputs.artifact_digest }}
|
|
723
|
+
baseline_artifact_id: \${{ steps.baseline.outputs.artifact_id }}
|
|
724
|
+
baseline_commit: \${{ steps.baseline.outputs.commit }}
|
|
725
|
+
baseline_found: \${{ steps.baseline.outputs.found }}
|
|
726
|
+
baseline_run_id: \${{ steps.baseline.outputs.run_id }}
|
|
637
727
|
producer_run_attempt: \${{ steps.current.outputs.producer_run_attempt || steps.staging.outputs.producer_run_attempt }}
|
|
638
728
|
producer_run_id: \${{ steps.current.outputs.producer_run_id || steps.staging.outputs.producer_run_id }}
|
|
639
729
|
source_commit: \${{ steps.current.outputs.source_commit || steps.staging.outputs.source_commit }}
|
|
@@ -684,6 +774,46 @@ ${indentedScript(`set -euo pipefail\n${canonicalStageAuthorityDerivationScript()
|
|
|
684
774
|
echo "producer_run_attempt=$run_attempt" >> "$GITHUB_OUTPUT"
|
|
685
775
|
echo "producer_run_id=$run_id" >> "$GITHUB_OUTPUT"
|
|
686
776
|
echo "source_commit=$EBK_SOURCE_COMMIT" >> "$GITHUB_OUTPUT"
|
|
777
|
+
- id: baseline
|
|
778
|
+
name: Select the newest successful deployment receipt on this branch
|
|
779
|
+
if: >-
|
|
780
|
+
needs.prepare.outputs.has_slices == 'true' &&
|
|
781
|
+
(needs.prepare.outputs.stage != 'production' || needs.prepare.outputs.profile == 'starter')
|
|
782
|
+
env:
|
|
783
|
+
EBK_SOURCE_BRANCH: \${{ env.EBK_DEPLOYMENT_SOURCE_BRANCH }}
|
|
784
|
+
EBK_SOURCE_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
|
|
785
|
+
EBK_STAGE: \${{ needs.prepare.outputs.stage }}
|
|
786
|
+
GH_REPO: \${{ github.repository }}
|
|
787
|
+
GH_TOKEN: \${{ github.token }}
|
|
788
|
+
run: |
|
|
789
|
+
set -euo pipefail
|
|
790
|
+
# A baseline lets unchanged OCI artifacts reuse their retained evidence.
|
|
791
|
+
# Declining is always safe: without one, every artifact is rebuilt.
|
|
792
|
+
echo "found=false" >> "$GITHUB_OUTPUT"
|
|
793
|
+
runs="$(gh api "/repos/$GH_REPO/actions/workflows/ci.yml/runs?event=push&branch=$EBK_SOURCE_BRANCH&status=success&per_page=50")"
|
|
794
|
+
for candidate in $(printf '%s' "$runs" | jq -r --arg branch "$EBK_SOURCE_BRANCH" \\
|
|
795
|
+
'.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
|
|
796
|
+
IFS=':' read -r run_id run_attempt head_sha <<< "$candidate"
|
|
797
|
+
case "$run_id:$run_attempt" in *[!0-9:]*) continue ;; esac
|
|
798
|
+
case "$head_sha" in ????????????????????????????????????????) ;; *) continue ;; esac
|
|
799
|
+
[ "$head_sha" != "$EBK_SOURCE_COMMIT" ] || continue
|
|
800
|
+
git merge-base --is-ancestor "$head_sha" "$EBK_SOURCE_COMMIT" 2>/dev/null || continue
|
|
801
|
+
artifacts="$(gh api "/repos/$GH_REPO/actions/runs/$run_id/artifacts?per_page=100")"
|
|
802
|
+
receipt="$(printf '%s' "$artifacts" | jq -c --arg name "deployment-receipt-$EBK_STAGE-$run_id-$run_attempt" \\
|
|
803
|
+
'[.artifacts[] | select(.name == $name and .expired == false)] | if length == 1 then .[0] else empty end')"
|
|
804
|
+
[ -n "$receipt" ] || continue
|
|
805
|
+
artifact_id="$(printf '%s' "$receipt" | jq -r '.id // empty')"
|
|
806
|
+
artifact_digest="$(printf '%s' "$receipt" | jq -r '.digest // empty')"
|
|
807
|
+
case "$artifact_id" in ''|*[!0-9]*) continue ;; esac
|
|
808
|
+
case "$artifact_digest" in sha256:????????????????????????????????????????????????????????????????) ;; *) continue ;; esac
|
|
809
|
+
echo "artifact_digest=$artifact_digest" >> "$GITHUB_OUTPUT"
|
|
810
|
+
echo "artifact_id=$artifact_id" >> "$GITHUB_OUTPUT"
|
|
811
|
+
echo "commit=$head_sha" >> "$GITHUB_OUTPUT"
|
|
812
|
+
echo "found=true" >> "$GITHUB_OUTPUT"
|
|
813
|
+
echo "run_id=$run_id" >> "$GITHUB_OUTPUT"
|
|
814
|
+
echo "Baseline deployment receipt: run $run_id attempt $run_attempt at $head_sha"
|
|
815
|
+
break
|
|
816
|
+
done
|
|
687
817
|
- id: staging-receipt
|
|
688
818
|
name: Select exact successful staging deployment receipt
|
|
689
819
|
if: needs.prepare.outputs.stage == 'production' && needs.prepare.outputs.profile != 'starter'
|
|
@@ -751,7 +881,9 @@ ${indentedScript(`set -euo pipefail\n${canonicalStageAuthorityDerivationScript()
|
|
|
751
881
|
EBK_EXPECTED_STAGING_ACCOUNT_ID: \${{ needs.prepare.outputs.staging_account_id }}
|
|
752
882
|
EBK_STAGING_RECEIPT_ARTIFACT_DIGEST: \${{ steps.staging-receipt.outputs.artifact_digest }}
|
|
753
883
|
EBK_STAGING_RECEIPT_ARTIFACT_ID: \${{ steps.staging-receipt.outputs.artifact_id }}
|
|
754
|
-
|
|
884
|
+
# The receipt artifact keeps .ebk/evidence as its root, so latest.json
|
|
885
|
+
# and promotion.json live under deployment/ (EBK-INT-2026-074).
|
|
886
|
+
EBK_STAGING_RECEIPT_ROOT: \${{ runner.temp }}/staging-deployment-receipt/deployment
|
|
755
887
|
EBK_STAGING_SOURCE_COMMIT: \${{ steps.staging-receipt.outputs.source_commit }}
|
|
756
888
|
GH_REPO: \${{ github.repository }}
|
|
757
889
|
GH_TOKEN: \${{ github.token }}
|
|
@@ -787,7 +919,7 @@ ${indentedScript(stagingPromotionReceiptValidationScript(), 10)}
|
|
|
787
919
|
fi
|
|
788
920
|
|
|
789
921
|
container-platform:
|
|
790
|
-
name: native \${{ needs.prepare.outputs.stage }} OCI \${{ matrix.platform }}
|
|
922
|
+
name: native \${{ needs.prepare.outputs.stage }} OCI \${{ matrix.project.project }} \${{ matrix.platform }}
|
|
791
923
|
needs: [prepare, immutable-artifacts]
|
|
792
924
|
if: >-
|
|
793
925
|
always() &&
|
|
@@ -801,7 +933,10 @@ ${indentedScript(stagingPromotionReceiptValidationScript(), 10)}
|
|
|
801
933
|
)
|
|
802
934
|
strategy:
|
|
803
935
|
fail-fast: false
|
|
936
|
+
# One job per Slice and architecture: Slices build in parallel instead of
|
|
937
|
+
# looping on one runner per architecture.
|
|
804
938
|
matrix:
|
|
939
|
+
project: \${{ fromJson(needs.prepare.outputs.slice_matrix) }}
|
|
805
940
|
include:
|
|
806
941
|
- architecture: amd64
|
|
807
942
|
platform: linux/amd64
|
|
@@ -830,6 +965,7 @@ ${indentedScript(stagingPromotionReceiptValidationScript(), 10)}
|
|
|
830
965
|
EBK_SOURCE_COMMIT: \${{ inputs.source_commit || github.sha }}
|
|
831
966
|
EBK_STAGE: \${{ needs.prepare.outputs.stage }}
|
|
832
967
|
EBK_SLICE_PROJECTS: \${{ needs.prepare.outputs.slice_projects }}
|
|
968
|
+
EBK_SLICE_PROJECT: \${{ matrix.project.project }}
|
|
833
969
|
steps:
|
|
834
970
|
- name: Check out immutable container source
|
|
835
971
|
uses: ${actionPins.checkout}
|
|
@@ -870,6 +1006,17 @@ ${indentedScript(stagingPromotionReceiptValidationScript(), 10)}
|
|
|
870
1006
|
github-token: \${{ github.token }}
|
|
871
1007
|
path: .ebk/evidence/release-candidate
|
|
872
1008
|
run-id: \${{ needs.immutable-artifacts.outputs.producer_run_id }}
|
|
1009
|
+
- name: Restore the baseline deployment receipt for unchanged artifacts
|
|
1010
|
+
if: needs.immutable-artifacts.outputs.baseline_found == 'true'
|
|
1011
|
+
uses: ${actionPins.downloadArtifact}
|
|
1012
|
+
env:
|
|
1013
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
1014
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
1015
|
+
with:
|
|
1016
|
+
artifact-ids: \${{ needs.immutable-artifacts.outputs.baseline_artifact_id }}
|
|
1017
|
+
github-token: \${{ github.token }}
|
|
1018
|
+
path: \${{ runner.temp }}/ebk-deployment-baseline
|
|
1019
|
+
run-id: \${{ needs.immutable-artifacts.outputs.baseline_run_id }}
|
|
873
1020
|
- name: Revalidate exact protected branch tip before OIDC
|
|
874
1021
|
env:
|
|
875
1022
|
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
@@ -900,22 +1047,48 @@ ${indentedScript(stagingPromotionReceiptValidationScript(), 10)}
|
|
|
900
1047
|
[ "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ]
|
|
901
1048
|
[ "$AWS_REGION" = "$EBK_POLICY_REGION" ]
|
|
902
1049
|
[ "$(aws sts get-caller-identity --query Account --output text)" = "$EBK_POLICY_ACCOUNT_ID" ]
|
|
1050
|
+
- name: Restore the container layer and release tool caches
|
|
1051
|
+
uses: ${actionPins.cache}
|
|
1052
|
+
env:
|
|
1053
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
1054
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
1055
|
+
with:
|
|
1056
|
+
path: |
|
|
1057
|
+
\${{ runner.temp }}/ebk-buildx-cache
|
|
1058
|
+
\${{ runner.temp }}/ebk-release-tools
|
|
1059
|
+
key: ebk-container-\${{ needs.prepare.outputs.stage }}-\${{ matrix.project.project }}-\${{ matrix.architecture }}-\${{ hashFiles('pnpm-lock.yaml') }}-\${{ github.run_id }}
|
|
1060
|
+
restore-keys: |
|
|
1061
|
+
ebk-container-\${{ needs.prepare.outputs.stage }}-\${{ matrix.project.project }}-\${{ matrix.architecture }}-\${{ hashFiles('pnpm-lock.yaml') }}-
|
|
1062
|
+
ebk-container-\${{ needs.prepare.outputs.stage }}-\${{ matrix.project.project }}-\${{ matrix.architecture }}-
|
|
903
1063
|
- name: Build and run exact native OCI artifacts
|
|
1064
|
+
env:
|
|
1065
|
+
EBK_BUILDX_CACHE_DIR: \${{ runner.temp }}/ebk-buildx-cache
|
|
1066
|
+
EBK_RELEASE_TOOL_CACHE_DIR: \${{ runner.temp }}/ebk-release-tools
|
|
1067
|
+
EBK_CONTAINER_BASELINE_COMMIT: \${{ needs.immutable-artifacts.outputs.baseline_commit }}
|
|
1068
|
+
EBK_CONTAINER_BASELINE_ROOT: \${{ needs.immutable-artifacts.outputs.baseline_found == 'true' && format('{0}/ebk-deployment-baseline', runner.temp) || '' }}
|
|
904
1069
|
run: |
|
|
905
1070
|
set -euo pipefail
|
|
906
1071
|
${indentedScript(exactStageDeployerGuardScript('Container build', '$EBK_POLICY_ACCOUNT_ID'), 10)}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1072
|
+
case "$EBK_SLICE_PROJECT" in
|
|
1073
|
+
''|-*|*-|*[!a-z0-9-]*) echo 'Container build Slice project must be an exact normalized Slice name'; exit 1 ;;
|
|
1074
|
+
esac
|
|
1075
|
+
case ",$EBK_SLICE_PROJECTS," in
|
|
1076
|
+
*",$EBK_SLICE_PROJECT,"*) ;;
|
|
1077
|
+
*) echo 'Container build Slice project is not in the committed deployment plan'; exit 1 ;;
|
|
1078
|
+
esac
|
|
1079
|
+
pnpm nx run "$EBK_SLICE_PROJECT:\${{ matrix.target }}"
|
|
912
1080
|
- name: Retain native platform evidence
|
|
913
1081
|
uses: ${actionPins.uploadArtifact}
|
|
914
1082
|
with:
|
|
915
|
-
name: container-platform-\${{ needs.prepare.outputs.stage }}-\${{ matrix.architecture }}-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
1083
|
+
name: container-platform-\${{ needs.prepare.outputs.stage }}-\${{ matrix.project.project }}-\${{ matrix.architecture }}-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
916
1084
|
if-no-files-found: error
|
|
917
1085
|
include-hidden-files: true
|
|
918
1086
|
retention-days: 30
|
|
1087
|
+
# The glob keeps partials/<stage> as the artifact root: the assemble
|
|
1088
|
+
# job merges every platform artifact into that directory and the
|
|
1089
|
+
# container release tool reads <project>/<architecture>/ beneath it.
|
|
1090
|
+
# A literal <project>/<architecture> path would make the architecture
|
|
1091
|
+
# directory the root and flatten the merged restore.
|
|
919
1092
|
path: .ebk/evidence/container-release/partials/\${{ needs.prepare.outputs.stage }}/*/\${{ matrix.architecture }}
|
|
920
1093
|
|
|
921
1094
|
container-assemble:
|
|
@@ -1213,8 +1386,10 @@ ${indentedScript(exactStageDeployerGuardScript('Container promotion', '$AWS_EXPE
|
|
|
1213
1386
|
IFS=',' read -r -a projects <<< "$EBK_SLICE_PROJECTS"
|
|
1214
1387
|
for project in "\${projects[@]}"; do
|
|
1215
1388
|
test -n "$project" || continue
|
|
1216
|
-
|
|
1217
|
-
|
|
1389
|
+
# The container release tool resolves these against the repository
|
|
1390
|
+
# root, so they stay repository-relative (EBK-INT-2026-077).
|
|
1391
|
+
export EBK_CONTAINER_PROMOTION_SOURCE_BINDING_PATH=".ebk/evidence/container-release/staging/packages/$project/binding.json"
|
|
1392
|
+
export EBK_CONTAINER_PROMOTION_SOURCE_SIDECAR_PATH=".ebk/evidence/container-release/staging/packages/$project/release-bound-binding.json"
|
|
1218
1393
|
pnpm nx run "$project:container-release-promote"
|
|
1219
1394
|
done
|
|
1220
1395
|
- id: retain
|
|
@@ -1233,6 +1408,7 @@ ${indentedScript(exactStageDeployerGuardScript('Container promotion', '$AWS_EXPE
|
|
|
1233
1408
|
if: >-
|
|
1234
1409
|
always() &&
|
|
1235
1410
|
needs.prepare.result == 'success' &&
|
|
1411
|
+
needs.prepare.outputs.action != 'rehearse' &&
|
|
1236
1412
|
(
|
|
1237
1413
|
needs.prepare.outputs.action != 'deploy' ||
|
|
1238
1414
|
(
|
|
@@ -1284,6 +1460,9 @@ ${indentedScript(exactStageDeployerGuardScript('Container promotion', '$AWS_EXPE
|
|
|
1284
1460
|
EBK_SLICE_PROJECTS: \${{ needs.prepare.outputs.slice_projects }}
|
|
1285
1461
|
EBK_SOURCE_COMMIT: \${{ inputs.source_commit || github.sha }}
|
|
1286
1462
|
EBK_STAGE: \${{ needs.prepare.outputs.stage }}
|
|
1463
|
+
EBK_LIFECYCLE_SCOPE: \${{ inputs.lifecycle || 'affected' }}
|
|
1464
|
+
EBK_BASELINE_FOUND: \${{ needs.immutable-artifacts.outputs.baseline_found }}
|
|
1465
|
+
EBK_BASELINE_RUN_ID: \${{ needs.immutable-artifacts.outputs.baseline_run_id }}
|
|
1287
1466
|
steps:
|
|
1288
1467
|
- name: Check out immutable deployment source
|
|
1289
1468
|
uses: ${actionPins.checkout}
|
|
@@ -1325,6 +1504,17 @@ ${indentedScript(exactStageDeployerGuardScript('Container promotion', '$AWS_EXPE
|
|
|
1325
1504
|
github-token: \${{ github.token }}
|
|
1326
1505
|
path: .ebk/evidence/release-candidate
|
|
1327
1506
|
run-id: \${{ needs.immutable-artifacts.outputs.producer_run_id }}
|
|
1507
|
+
- name: Restore the baseline deployment receipt for unchanged Slices
|
|
1508
|
+
if: needs.prepare.outputs.action == 'deploy' && needs.immutable-artifacts.outputs.baseline_found == 'true'
|
|
1509
|
+
uses: ${actionPins.downloadArtifact}
|
|
1510
|
+
env:
|
|
1511
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
1512
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
1513
|
+
with:
|
|
1514
|
+
artifact-ids: \${{ needs.immutable-artifacts.outputs.baseline_artifact_id }}
|
|
1515
|
+
github-token: \${{ github.token }}
|
|
1516
|
+
path: \${{ runner.temp }}/ebk-deployment-baseline
|
|
1517
|
+
run-id: \${{ needs.immutable-artifacts.outputs.baseline_run_id }}
|
|
1328
1518
|
- name: Restore exact current-stage container binding
|
|
1329
1519
|
if: >-
|
|
1330
1520
|
needs.prepare.outputs.action == 'deploy' &&
|
|
@@ -1476,9 +1666,21 @@ ${indentedScript(exactStageDeployerGuardScript('Native lifecycle', '$EBK_POLICY_
|
|
|
1476
1666
|
set -euo pipefail
|
|
1477
1667
|
test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'Stage deployment role is absent'; exit 1; }
|
|
1478
1668
|
if [ "$EBK_ACTION" = deploy ]; then
|
|
1669
|
+
case "$EBK_LIFECYCLE_SCOPE" in affected|full) ;; *) echo 'Lifecycle scope must be affected or full'; exit 1 ;; esac
|
|
1670
|
+
baseline_arguments=()
|
|
1671
|
+
if [ "$EBK_LIFECYCLE_SCOPE" = affected ] && [ "$EBK_BASELINE_FOUND" = true ]; then
|
|
1672
|
+
case "$EBK_BASELINE_RUN_ID" in ''|*[!0-9]*) echo 'Baseline run id is malformed'; exit 1 ;; esac
|
|
1673
|
+
baseline_arguments=(
|
|
1674
|
+
--baselineReceipt="$RUNNER_TEMP/ebk-deployment-baseline/deployment/latest.json"
|
|
1675
|
+
--baselineRunId="$EBK_BASELINE_RUN_ID"
|
|
1676
|
+
--baselineRunAttempt=1
|
|
1677
|
+
)
|
|
1678
|
+
fi
|
|
1479
1679
|
pnpm nx run ${name}:deploy-local -- \
|
|
1480
1680
|
--stage="$EBK_STAGE" \
|
|
1481
|
-
--expectedRoleArn="$AWS_DEPLOY_ROLE_ARN"
|
|
1681
|
+
--expectedRoleArn="$AWS_DEPLOY_ROLE_ARN" \
|
|
1682
|
+
--lifecycleScope="$EBK_LIFECYCLE_SCOPE" \
|
|
1683
|
+
"\${baseline_arguments[@]}"
|
|
1482
1684
|
else
|
|
1483
1685
|
[ -n "$EBK_ORDERED_PROJECTS" ] || { echo 'Committed deployment order is empty'; exit 1; }
|
|
1484
1686
|
IFS=',' read -r -a projects <<< "$EBK_ORDERED_PROJECTS"
|
|
@@ -1546,6 +1748,673 @@ ${indentedScript(postdeploySupplyChainBindingScript(), 10)}
|
|
|
1546
1748
|
.ebk/evidence/deployment/promotion.json
|
|
1547
1749
|
.ebk/evidence/container-release/**
|
|
1548
1750
|
retention-days: 30
|
|
1751
|
+
${deploymentProfile === 'starter' ? '' : renderRehearseJob(options)}${trunkPromotion ? renderTrunkPromotionJobs(options) : ''}`;
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Managed Router WAF blocking rehearsal (EBK-INT-2026-079). The protected
|
|
1755
|
+
* dispatch runs the foundation rehearsal target under the staging deployment
|
|
1756
|
+
* role with seedProbes, so the runner performs the operator probes itself
|
|
1757
|
+
* inside the private-smoke allowlist admission, proves the admission removed,
|
|
1758
|
+
* and opens the pull request that carries the sealed evidence and receipt
|
|
1759
|
+
* back to develop through the ordinary review path.
|
|
1760
|
+
*/
|
|
1761
|
+
function renderRehearseJob(options) {
|
|
1762
|
+
const { actionPins, name, toolVersions } = options;
|
|
1763
|
+
return `
|
|
1764
|
+
rehearse:
|
|
1765
|
+
name: rehearse staging Router WAF blocking
|
|
1766
|
+
needs: prepare
|
|
1767
|
+
if: >-
|
|
1768
|
+
needs.prepare.result == 'success' &&
|
|
1769
|
+
needs.prepare.outputs.action == 'rehearse' &&
|
|
1770
|
+
needs.prepare.outputs.stage == 'staging' &&
|
|
1771
|
+
needs.prepare.outputs.profile != 'starter'
|
|
1772
|
+
runs-on: ubuntu-latest
|
|
1773
|
+
environment: \${{ needs.prepare.outputs.stage }}
|
|
1774
|
+
permissions:
|
|
1775
|
+
actions: read
|
|
1776
|
+
contents: write
|
|
1777
|
+
id-token: write
|
|
1778
|
+
pull-requests: write
|
|
1779
|
+
env:
|
|
1780
|
+
AWS_DEPLOY_ROLE_ARN: \${{ needs.prepare.outputs.stage_deploy_role_arn }}
|
|
1781
|
+
AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
|
|
1782
|
+
AWS_PARTITION: \${{ needs.prepare.outputs.partition }}
|
|
1783
|
+
AWS_REGION: \${{ vars.AWS_REGION }}
|
|
1784
|
+
EBK_ACTION: rehearse
|
|
1785
|
+
EBK_DEPLOYMENT_PROFILE: \${{ needs.prepare.outputs.profile }}
|
|
1786
|
+
EBK_POLICY_ACCOUNT_ID: \${{ needs.prepare.outputs.account_id }}
|
|
1787
|
+
EBK_POLICY_REGION: \${{ needs.prepare.outputs.region }}
|
|
1788
|
+
EBK_PROBE_URL: \${{ needs.prepare.outputs.probe_url }}
|
|
1789
|
+
EBK_SOURCE_COMMIT: \${{ inputs.source_commit || github.sha }}
|
|
1790
|
+
EBK_STAGE: \${{ needs.prepare.outputs.stage }}
|
|
1791
|
+
steps:
|
|
1792
|
+
- name: Check out immutable deployment source
|
|
1793
|
+
uses: ${actionPins.checkout}
|
|
1794
|
+
env:
|
|
1795
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
1796
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
1797
|
+
with:
|
|
1798
|
+
fetch-depth: 0
|
|
1799
|
+
persist-credentials: false
|
|
1800
|
+
ref: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
|
|
1801
|
+
- uses: ${actionPins.pnpmSetup}
|
|
1802
|
+
env:
|
|
1803
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
1804
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
1805
|
+
- uses: ${actionPins.setupNode}
|
|
1806
|
+
env:
|
|
1807
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
1808
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
1809
|
+
with:
|
|
1810
|
+
node-version: ${toolVersions.node}
|
|
1811
|
+
cache: pnpm
|
|
1812
|
+
- env:
|
|
1813
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
1814
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
1815
|
+
run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
|
|
1816
|
+
- name: Verify managed workspace integrity before AWS authority
|
|
1817
|
+
env:
|
|
1818
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
1819
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
1820
|
+
run: pnpm nx run ${name}:workspace-health
|
|
1821
|
+
- name: Require the exact pending rehearsal seeds
|
|
1822
|
+
env:
|
|
1823
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
1824
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
1825
|
+
run: |
|
|
1826
|
+
set -euo pipefail
|
|
1827
|
+
for seed in router-waf-live-evidence router-waf-blocking-rehearsal; do
|
|
1828
|
+
[ "$(jq -r '.status // empty' "packages/foundation/security/$seed.json")" = pending ] || {
|
|
1829
|
+
echo "packages/foundation/security/$seed.json must be reset to its pending seed before another rehearsal"; exit 1;
|
|
1830
|
+
}
|
|
1831
|
+
done
|
|
1832
|
+
- name: Revalidate exact protected branch tip before OIDC
|
|
1833
|
+
env:
|
|
1834
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
1835
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
1836
|
+
EBK_SOURCE_BRANCH: \${{ env.EBK_DEPLOYMENT_SOURCE_BRANCH }}
|
|
1837
|
+
EBK_SOURCE_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
|
|
1838
|
+
GH_REPO: \${{ github.repository }}
|
|
1839
|
+
GH_TOKEN: \${{ github.token }}
|
|
1840
|
+
run: |
|
|
1841
|
+
set -euo pipefail
|
|
1842
|
+
[ "$(git rev-parse HEAD)" = "$EBK_SOURCE_COMMIT" ] || {
|
|
1843
|
+
echo 'Deployment checkout differs from the exact protected source'; exit 1;
|
|
1844
|
+
}
|
|
1845
|
+
branch_tip="$(gh api "/repos/$GH_REPO/git/ref/heads/$EBK_SOURCE_BRANCH" --jq '.object.sha')"
|
|
1846
|
+
[ "$branch_tip" = "$EBK_SOURCE_COMMIT" ] || {
|
|
1847
|
+
echo 'Deployment source is no longer the exact protected branch tip'; exit 1;
|
|
1848
|
+
}
|
|
1849
|
+
- name: Assume the one stage deployment role
|
|
1850
|
+
uses: ${actionPins.configureAwsCredentials}
|
|
1851
|
+
with:
|
|
1852
|
+
audience: \${{ needs.prepare.outputs.oidc_audience }}
|
|
1853
|
+
aws-region: \${{ vars.AWS_REGION }}
|
|
1854
|
+
role-session-name: ebk-rehearse-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
1855
|
+
role-to-assume: \${{ env.AWS_DEPLOY_ROLE_ARN }}
|
|
1856
|
+
- name: Verify exact stage authority
|
|
1857
|
+
run: |
|
|
1858
|
+
set -euo pipefail
|
|
1859
|
+
[ "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ]
|
|
1860
|
+
[ "$AWS_REGION" = "$EBK_POLICY_REGION" ]
|
|
1861
|
+
${indentedScript(exactStageDeployerGuardScript('Router WAF rehearsal', '$EBK_POLICY_ACCOUNT_ID'), 10)}
|
|
1862
|
+
- name: Rehearse Router WAF blocking with runner-seeded probes
|
|
1863
|
+
id: rehearsal
|
|
1864
|
+
env:
|
|
1865
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
1866
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
1867
|
+
EBK_GITHUB_JOB_WORKFLOW_REF: \${{ job.workflow_ref }}
|
|
1868
|
+
GITHUB_TOKEN: \${{ github.token }}
|
|
1869
|
+
run: |
|
|
1870
|
+
set -euo pipefail
|
|
1871
|
+
test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'Stage deployment role is absent'; exit 1; }
|
|
1872
|
+
pnpm nx run foundation:router-waf-rehearsal -- \\
|
|
1873
|
+
--probeUrl="$EBK_PROBE_URL" \\
|
|
1874
|
+
--seedProbes=true
|
|
1875
|
+
- name: Prove temporary smoke admission removed
|
|
1876
|
+
if: always() && steps.rehearsal.outcome != 'skipped'
|
|
1877
|
+
env:
|
|
1878
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
1879
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
1880
|
+
EBK_GITHUB_JOB_WORKFLOW_REF: \${{ job.workflow_ref }}
|
|
1881
|
+
GITHUB_TOKEN: \${{ github.token }}
|
|
1882
|
+
run: |
|
|
1883
|
+
set -euo pipefail
|
|
1884
|
+
${indentedScript(exactStageDeployerGuardScript('Smoke access cleanup', '$EBK_POLICY_ACCOUNT_ID'), 10)}
|
|
1885
|
+
[ "$(git rev-parse HEAD)" = "$EBK_SOURCE_COMMIT" ]
|
|
1886
|
+
node --input-type=module <<'NODE'
|
|
1887
|
+
import { cleanupGithubPrivateSmoke } from '@empire-builder-kit/nx/foundation';
|
|
1888
|
+
await cleanupGithubPrivateSmoke({
|
|
1889
|
+
account: process.env.EBK_POLICY_ACCOUNT_ID,
|
|
1890
|
+
deploymentProfile: process.env.EBK_DEPLOYMENT_PROFILE,
|
|
1891
|
+
root: process.cwd(),
|
|
1892
|
+
source: process.env.EBK_SOURCE_COMMIT,
|
|
1893
|
+
stage: process.env.EBK_STAGE,
|
|
1894
|
+
workspace: '${name}',
|
|
1895
|
+
});
|
|
1896
|
+
NODE
|
|
1897
|
+
- name: Retain the sealed rehearsal evidence
|
|
1898
|
+
uses: ${actionPins.uploadArtifact}
|
|
1899
|
+
with:
|
|
1900
|
+
if-no-files-found: error
|
|
1901
|
+
name: router-waf-rehearsal-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
1902
|
+
path: |
|
|
1903
|
+
packages/foundation/security/router-waf-live-evidence.json
|
|
1904
|
+
packages/foundation/security/router-waf-blocking-rehearsal.json
|
|
1905
|
+
retention-days: 90
|
|
1906
|
+
- name: Open the rehearsal receipt pull request
|
|
1907
|
+
env:
|
|
1908
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
1909
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
1910
|
+
EBK_RUN_ATTEMPT: \${{ github.run_attempt }}
|
|
1911
|
+
EBK_RUN_ID: \${{ github.run_id }}
|
|
1912
|
+
GH_REPO: \${{ github.repository }}
|
|
1913
|
+
GH_TOKEN: \${{ github.token }}
|
|
1914
|
+
run: |
|
|
1915
|
+
set -euo pipefail
|
|
1916
|
+
case "$EBK_RUN_ID:$EBK_RUN_ATTEMPT" in *[!0-9:]*|:*|*:) exit 1 ;; esac
|
|
1917
|
+
branch="ebk/router-waf-rehearsal-$EBK_RUN_ID-$EBK_RUN_ATTEMPT"
|
|
1918
|
+
git diff --quiet -- packages/foundation/security && { echo 'The rehearsal sealed nothing new'; exit 1; }
|
|
1919
|
+
git config user.name 'ebk-router-waf-rehearsal[bot]'
|
|
1920
|
+
git config user.email 'ebk-router-waf-rehearsal[bot]@users.noreply.github.com'
|
|
1921
|
+
git checkout -q -b "$branch"
|
|
1922
|
+
git add packages/foundation/security/router-waf-live-evidence.json packages/foundation/security/router-waf-blocking-rehearsal.json
|
|
1923
|
+
git commit -q -m "chore(security): seal the staging Router WAF blocking rehearsal (run $EBK_RUN_ID)"
|
|
1924
|
+
git -c http.https://github.com/.extraheader="AUTHORIZATION: bearer $GH_TOKEN" push -q origin "HEAD:refs/heads/$branch"
|
|
1925
|
+
gh pr create \\
|
|
1926
|
+
--head "$branch" \\
|
|
1927
|
+
--base develop \\
|
|
1928
|
+
--title 'chore(security): seal the staging Router WAF blocking rehearsal' \\
|
|
1929
|
+
--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."
|
|
1930
|
+
`;
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Trunk promotion: after a successful staging lifecycle on develop, the same
|
|
1934
|
+
* run binds its own staging receipt, promotes the reviewed OCI indexes into
|
|
1935
|
+
* production, deploys production, and fast-forwards main to the deployed
|
|
1936
|
+
* commit. Every job carries the production environment so environment
|
|
1937
|
+
* reviewers can still gate it, and every step reuses the promotion-pr scripts.
|
|
1938
|
+
*/
|
|
1939
|
+
function renderTrunkPromotionJobs(options) {
|
|
1940
|
+
const { actionPins, name, toolVersions } = options;
|
|
1941
|
+
const checkoutAndInstall = (integrityStep) => ` - name: Check out immutable deployment source
|
|
1942
|
+
uses: ${actionPins.checkout}
|
|
1943
|
+
env:
|
|
1944
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
1945
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
1946
|
+
with:
|
|
1947
|
+
fetch-depth: 0
|
|
1948
|
+
persist-credentials: false
|
|
1949
|
+
ref: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
|
|
1950
|
+
- uses: ${actionPins.pnpmSetup}
|
|
1951
|
+
env:
|
|
1952
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
1953
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
1954
|
+
- uses: ${actionPins.setupNode}
|
|
1955
|
+
env:
|
|
1956
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
1957
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
1958
|
+
with:
|
|
1959
|
+
node-version: ${toolVersions.node}
|
|
1960
|
+
cache: pnpm
|
|
1961
|
+
- env:
|
|
1962
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
1963
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
1964
|
+
run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
|
|
1965
|
+
- name: ${integrityStep}
|
|
1966
|
+
env:
|
|
1967
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
1968
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
1969
|
+
run: pnpm nx run ${name}:workspace-health`;
|
|
1970
|
+
const revalidateTip = ` - name: Revalidate exact protected branch tip before OIDC
|
|
1971
|
+
env:
|
|
1972
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
1973
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
1974
|
+
EBK_SOURCE_BRANCH: \${{ env.EBK_DEPLOYMENT_SOURCE_BRANCH }}
|
|
1975
|
+
EBK_SOURCE_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
|
|
1976
|
+
GH_REPO: \${{ github.repository }}
|
|
1977
|
+
GH_TOKEN: \${{ github.token }}
|
|
1978
|
+
run: |
|
|
1979
|
+
set -euo pipefail
|
|
1980
|
+
[ "$(git rev-parse HEAD)" = "$EBK_SOURCE_COMMIT" ] || {
|
|
1981
|
+
echo 'Deployment checkout differs from the exact protected source'; exit 1;
|
|
1982
|
+
}
|
|
1983
|
+
branch_tip="$(gh api "/repos/$GH_REPO/git/ref/heads/$EBK_SOURCE_BRANCH" --jq '.object.sha')"
|
|
1984
|
+
[ "$branch_tip" = "$EBK_SOURCE_COMMIT" ] || {
|
|
1985
|
+
echo 'Deployment source is no longer the exact protected branch tip'; exit 1;
|
|
1986
|
+
}`;
|
|
1987
|
+
const trunkPromotionGate = ` needs.prepare.outputs.action == 'deploy' &&
|
|
1988
|
+
needs.prepare.outputs.stage == 'staging' &&
|
|
1989
|
+
needs.prepare.outputs.profile != 'starter' &&
|
|
1990
|
+
needs.prepare.outputs.has_slices == 'true'`;
|
|
1991
|
+
return `
|
|
1992
|
+
promote-production:
|
|
1993
|
+
name: promote the staging-tested candidate to production
|
|
1994
|
+
needs: [prepare, immutable-artifacts, container-assemble, execute]
|
|
1995
|
+
if: >-
|
|
1996
|
+
always() &&
|
|
1997
|
+
needs.prepare.result == 'success' &&
|
|
1998
|
+
needs.immutable-artifacts.result == 'success' &&
|
|
1999
|
+
needs.container-assemble.result == 'success' &&
|
|
2000
|
+
needs.execute.result == 'success' &&
|
|
2001
|
+
${trunkPromotionGate}
|
|
2002
|
+
runs-on: ubuntu-latest
|
|
2003
|
+
environment: production
|
|
2004
|
+
permissions:
|
|
2005
|
+
actions: read
|
|
2006
|
+
contents: read
|
|
2007
|
+
id-token: write
|
|
2008
|
+
outputs:
|
|
2009
|
+
artifact_digest: \${{ steps.retain.outputs.artifact-digest }}
|
|
2010
|
+
artifact_id: \${{ steps.retain.outputs.artifact-id }}
|
|
2011
|
+
candidate_artifact_digest: \${{ steps.staging.outputs.artifact_digest }}
|
|
2012
|
+
candidate_artifact_id: \${{ steps.staging.outputs.artifact_id }}
|
|
2013
|
+
candidate_material_digest: \${{ steps.staging.outputs.candidate_material_digest }}
|
|
2014
|
+
candidate_producer_run_attempt: \${{ steps.staging.outputs.producer_run_attempt }}
|
|
2015
|
+
candidate_producer_run_id: \${{ steps.staging.outputs.producer_run_id }}
|
|
2016
|
+
env:
|
|
2017
|
+
AWS_DEPLOY_ROLE_ARN: \${{ needs.prepare.outputs.production_deploy_role_arn }}
|
|
2018
|
+
AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
|
|
2019
|
+
AWS_PARTITION: \${{ needs.prepare.outputs.partition }}
|
|
2020
|
+
AWS_REGION: \${{ vars.AWS_REGION }}
|
|
2021
|
+
EBK_ACTION: deploy
|
|
2022
|
+
EBK_DEPLOYMENT_PROFILE: \${{ needs.prepare.outputs.profile }}
|
|
2023
|
+
EBK_POLICY_PRODUCTION_ACCOUNT_ID: \${{ needs.prepare.outputs.production_account_id }}
|
|
2024
|
+
EBK_POLICY_REGION: \${{ needs.prepare.outputs.region }}
|
|
2025
|
+
EBK_POLICY_STAGING_ACCOUNT_ID: \${{ needs.prepare.outputs.staging_account_id }}
|
|
2026
|
+
EBK_RELEASE_SOURCE_COMMIT: \${{ needs.immutable-artifacts.outputs.source_commit }}
|
|
2027
|
+
EBK_SOURCE_COMMIT: \${{ inputs.source_commit || github.sha }}
|
|
2028
|
+
EBK_STAGE: production
|
|
2029
|
+
EBK_SLICE_PROJECTS: \${{ needs.prepare.outputs.slice_projects }}
|
|
2030
|
+
steps:
|
|
2031
|
+
${checkoutAndInstall('Verify managed workspace integrity before container authority')}
|
|
2032
|
+
- id: staging-receipt
|
|
2033
|
+
name: Select this run's staging deployment receipt
|
|
2034
|
+
env:
|
|
2035
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
2036
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
2037
|
+
EBK_DEPLOYMENT_RUN_ATTEMPT: \${{ github.run_attempt }}
|
|
2038
|
+
EBK_DEPLOYMENT_RUN_ID: \${{ github.run_id }}
|
|
2039
|
+
GH_REPO: \${{ github.repository }}
|
|
2040
|
+
GH_TOKEN: \${{ github.token }}
|
|
2041
|
+
run: |
|
|
2042
|
+
set -euo pipefail
|
|
2043
|
+
case "$EBK_DEPLOYMENT_RUN_ID:$EBK_DEPLOYMENT_RUN_ATTEMPT" in *[!0-9:]*|:*|*:) exit 1 ;; esac
|
|
2044
|
+
artifacts="$(gh api "/repos/$GH_REPO/actions/runs/$EBK_DEPLOYMENT_RUN_ID/artifacts?per_page=100")"
|
|
2045
|
+
receipt_name="deployment-receipt-staging-$EBK_DEPLOYMENT_RUN_ID-$EBK_DEPLOYMENT_RUN_ATTEMPT"
|
|
2046
|
+
receipt="$(printf '%s' "$artifacts" | jq -c --arg name "$receipt_name" \\
|
|
2047
|
+
'[.artifacts[] | select(.name == $name and .expired == false)] | if length == 1 then .[0] else empty end')"
|
|
2048
|
+
[ -n "$receipt" ] || { echo 'This run retained no exact staging deployment receipt'; exit 1; }
|
|
2049
|
+
artifact_id="$(printf '%s' "$receipt" | jq -r '.id // empty')"
|
|
2050
|
+
artifact_digest="$(printf '%s' "$receipt" | jq -r '.digest // empty')"
|
|
2051
|
+
case "$artifact_id" in ''|*[!0-9]*) exit 1 ;; esac
|
|
2052
|
+
case "$artifact_digest" in sha256:????????????????????????????????????????????????????????????????) ;; *) exit 1 ;; esac
|
|
2053
|
+
echo "artifact_digest=$artifact_digest" >> "$GITHUB_OUTPUT"
|
|
2054
|
+
echo "artifact_id=$artifact_id" >> "$GITHUB_OUTPUT"
|
|
2055
|
+
- name: Restore this run's staging deployment receipt
|
|
2056
|
+
uses: ${actionPins.downloadArtifact}
|
|
2057
|
+
env:
|
|
2058
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
2059
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
2060
|
+
with:
|
|
2061
|
+
artifact-ids: \${{ steps.staging-receipt.outputs.artifact_id }}
|
|
2062
|
+
github-token: \${{ github.token }}
|
|
2063
|
+
path: \${{ runner.temp }}/staging-deployment-receipt
|
|
2064
|
+
- id: staging
|
|
2065
|
+
name: Bind exact staging deployment and release evidence
|
|
2066
|
+
env:
|
|
2067
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
2068
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
2069
|
+
EBK_STAGING_DEPLOYMENT_RUN_ATTEMPT: \${{ github.run_attempt }}
|
|
2070
|
+
EBK_STAGING_DEPLOYMENT_RUN_ID: \${{ github.run_id }}
|
|
2071
|
+
EBK_EXPECTED_HAS_SLICES: \${{ needs.prepare.outputs.has_slices }}
|
|
2072
|
+
EBK_EXPECTED_DEPLOYMENT_PROFILE: \${{ needs.prepare.outputs.profile }}
|
|
2073
|
+
EBK_EXPECTED_DEPLOYMENT_REGION: \${{ needs.prepare.outputs.region }}
|
|
2074
|
+
EBK_EXPECTED_STAGING_ACCOUNT_ID: \${{ needs.prepare.outputs.staging_account_id }}
|
|
2075
|
+
EBK_STAGING_RECEIPT_ARTIFACT_DIGEST: \${{ steps.staging-receipt.outputs.artifact_digest }}
|
|
2076
|
+
EBK_STAGING_RECEIPT_ARTIFACT_ID: \${{ steps.staging-receipt.outputs.artifact_id }}
|
|
2077
|
+
# The receipt artifact keeps .ebk/evidence as its root (EBK-INT-2026-074).
|
|
2078
|
+
EBK_STAGING_RECEIPT_ROOT: \${{ runner.temp }}/staging-deployment-receipt/deployment
|
|
2079
|
+
EBK_STAGING_SOURCE_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
|
|
2080
|
+
GH_REPO: \${{ github.repository }}
|
|
2081
|
+
GH_TOKEN: \${{ github.token }}
|
|
2082
|
+
run: |
|
|
2083
|
+
node --input-type=module <<'NODE'
|
|
2084
|
+
${indentedScript(stagingPromotionReceiptValidationScript(), 10)}
|
|
2085
|
+
NODE
|
|
2086
|
+
promotion="$EBK_STAGING_RECEIPT_ROOT/promotion.json"
|
|
2087
|
+
candidate_run_id="$(jq -r '.candidate.producerRunId' "$promotion")"
|
|
2088
|
+
candidate_run_attempt="$(jq -r '.candidate.producerRunAttempt' "$promotion")"
|
|
2089
|
+
candidate_artifact_id="$(jq -r '.candidate.artifactId' "$promotion")"
|
|
2090
|
+
candidate_artifact_digest="$(jq -r '.candidate.artifactDigest' "$promotion")"
|
|
2091
|
+
candidate_run="$(gh api "/repos/$GH_REPO/actions/runs/$candidate_run_id")"
|
|
2092
|
+
printf '%s' "$candidate_run" | jq -e \\
|
|
2093
|
+
--arg source "$EBK_STAGING_SOURCE_COMMIT" \\
|
|
2094
|
+
--arg attempt "$candidate_run_attempt" \\
|
|
2095
|
+
'.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
|
|
2096
|
+
candidate_artifacts="$(gh api "/repos/$GH_REPO/actions/runs/$candidate_run_id/artifacts?per_page=100")"
|
|
2097
|
+
printf '%s' "$candidate_artifacts" | jq -e \\
|
|
2098
|
+
--arg id "$candidate_artifact_id" \\
|
|
2099
|
+
--arg digest "$candidate_artifact_digest" \\
|
|
2100
|
+
--arg name "release-candidate-$EBK_STAGING_SOURCE_COMMIT" \\
|
|
2101
|
+
'[.artifacts[] | select((.id | tostring) == $id and .name == $name and .digest == $digest and .expired == false)] | length == 1' >/dev/null
|
|
2102
|
+
- name: Restore exact immutable release candidate for promotion
|
|
2103
|
+
uses: ${actionPins.downloadArtifact}
|
|
2104
|
+
env:
|
|
2105
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
2106
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
2107
|
+
with:
|
|
2108
|
+
artifact-ids: \${{ steps.staging.outputs.artifact_id }}
|
|
2109
|
+
github-token: \${{ github.token }}
|
|
2110
|
+
path: .ebk/evidence/release-candidate
|
|
2111
|
+
run-id: \${{ steps.staging.outputs.producer_run_id }}
|
|
2112
|
+
- name: Restore this run's staging container bundle
|
|
2113
|
+
uses: ${actionPins.downloadArtifact}
|
|
2114
|
+
env:
|
|
2115
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
2116
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
2117
|
+
with:
|
|
2118
|
+
artifact-ids: \${{ steps.staging.outputs.container_artifact_id }}
|
|
2119
|
+
github-token: \${{ github.token }}
|
|
2120
|
+
path: .ebk/evidence/container-release
|
|
2121
|
+
${revalidateTip}
|
|
2122
|
+
- name: Assume exact staging registry pull role
|
|
2123
|
+
uses: ${actionPins.configureAwsCredentials}
|
|
2124
|
+
with:
|
|
2125
|
+
audience: \${{ needs.prepare.outputs.oidc_audience }}
|
|
2126
|
+
aws-region: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
|
|
2127
|
+
role-session-name: ebk-staging-container-pull-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
2128
|
+
role-to-assume: \${{ secrets.AWS_STAGING_PROMOTION_PULL_ROLE_ARN }}
|
|
2129
|
+
- name: Authenticate exact staging source registry
|
|
2130
|
+
env:
|
|
2131
|
+
AWS_STAGING_PROMOTION_PULL_ROLE_ARN: \${{ secrets.AWS_STAGING_PROMOTION_PULL_ROLE_ARN }}
|
|
2132
|
+
EBK_STAGING_ACCOUNT_ID: \${{ vars.AWS_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID }}
|
|
2133
|
+
EBK_STAGING_REGION: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
|
|
2134
|
+
run: |
|
|
2135
|
+
set -euo pipefail
|
|
2136
|
+
[ "$EBK_STAGING_ACCOUNT_ID" = "$EBK_POLICY_STAGING_ACCOUNT_ID" ]
|
|
2137
|
+
[ "$EBK_STAGING_REGION" = "$EBK_POLICY_REGION" ]
|
|
2138
|
+
${indentedScript(exactStagingPromotionPullGuardScript(), 10)}
|
|
2139
|
+
[ "$EBK_STAGING_ACCOUNT_ID" != "$AWS_EXPECTED_ACCOUNT_ID" ]
|
|
2140
|
+
registry_suffix=.com
|
|
2141
|
+
case "$EBK_STAGING_REGION" in cn-*) registry_suffix=.com.cn ;; esac
|
|
2142
|
+
aws ecr get-login-password --region "$EBK_STAGING_REGION" |
|
|
2143
|
+
docker login --username AWS --password-stdin \\
|
|
2144
|
+
"$EBK_STAGING_ACCOUNT_ID.dkr.ecr.$EBK_STAGING_REGION.amazonaws$registry_suffix"
|
|
2145
|
+
${revalidateTip}
|
|
2146
|
+
- name: Assume the production deployment role for container promotion
|
|
2147
|
+
uses: ${actionPins.configureAwsCredentials}
|
|
2148
|
+
with:
|
|
2149
|
+
audience: \${{ needs.prepare.outputs.oidc_audience }}
|
|
2150
|
+
aws-region: \${{ vars.AWS_REGION }}
|
|
2151
|
+
role-session-name: ebk-production-container-push-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
2152
|
+
role-to-assume: \${{ env.AWS_DEPLOY_ROLE_ARN }}
|
|
2153
|
+
- name: Promote exact staging indexes without rebuilding
|
|
2154
|
+
env:
|
|
2155
|
+
EBK_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID }}
|
|
2156
|
+
EBK_STAGING_PROMOTION_REGION: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
|
|
2157
|
+
run: |
|
|
2158
|
+
set -euo pipefail
|
|
2159
|
+
[ "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_PRODUCTION_ACCOUNT_ID" ]
|
|
2160
|
+
[ "$AWS_REGION" = "$EBK_POLICY_REGION" ]
|
|
2161
|
+
${indentedScript(exactStageDeployerGuardScript('Container promotion', '$AWS_EXPECTED_ACCOUNT_ID'), 10)}
|
|
2162
|
+
IFS=',' read -r -a projects <<< "$EBK_SLICE_PROJECTS"
|
|
2163
|
+
for project in "\${projects[@]}"; do
|
|
2164
|
+
test -n "$project" || continue
|
|
2165
|
+
# Repository-relative for the container release tool (EBK-INT-2026-077).
|
|
2166
|
+
export EBK_CONTAINER_PROMOTION_SOURCE_BINDING_PATH=".ebk/evidence/container-release/staging/packages/$project/binding.json"
|
|
2167
|
+
export EBK_CONTAINER_PROMOTION_SOURCE_SIDECAR_PATH=".ebk/evidence/container-release/staging/packages/$project/release-bound-binding.json"
|
|
2168
|
+
pnpm nx run "$project:container-release-promote"
|
|
2169
|
+
done
|
|
2170
|
+
- id: retain
|
|
2171
|
+
name: Retain production target container binding
|
|
2172
|
+
uses: ${actionPins.uploadArtifact}
|
|
2173
|
+
with:
|
|
2174
|
+
name: container-release-production-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
2175
|
+
if-no-files-found: error
|
|
2176
|
+
include-hidden-files: true
|
|
2177
|
+
retention-days: 90
|
|
2178
|
+
path: .ebk/evidence/container-release
|
|
2179
|
+
|
|
2180
|
+
execute-production:
|
|
2181
|
+
name: native deploy production
|
|
2182
|
+
needs: [prepare, immutable-artifacts, promote-production]
|
|
2183
|
+
if: >-
|
|
2184
|
+
always() &&
|
|
2185
|
+
needs.prepare.result == 'success' &&
|
|
2186
|
+
needs.immutable-artifacts.result == 'success' &&
|
|
2187
|
+
needs.promote-production.result == 'success' &&
|
|
2188
|
+
${trunkPromotionGate}
|
|
2189
|
+
runs-on: ubuntu-latest
|
|
2190
|
+
environment: production
|
|
2191
|
+
permissions:
|
|
2192
|
+
actions: read
|
|
2193
|
+
contents: read
|
|
2194
|
+
id-token: write
|
|
2195
|
+
env:
|
|
2196
|
+
AWS_DEPLOY_ROLE_ARN: \${{ needs.prepare.outputs.production_deploy_role_arn }}
|
|
2197
|
+
AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
|
|
2198
|
+
AWS_PARTITION: \${{ needs.prepare.outputs.partition }}
|
|
2199
|
+
AWS_REGION: \${{ vars.AWS_REGION }}
|
|
2200
|
+
EBK_ACTION: deploy
|
|
2201
|
+
EBK_ALLOW_PRODUCTION_REMOVE: \${{ vars.EBK_ALLOW_PRODUCTION_REMOVE }}
|
|
2202
|
+
EBK_CANDIDATE_ARTIFACT_DIGEST: \${{ needs.promote-production.outputs.candidate_artifact_digest }}
|
|
2203
|
+
EBK_CANDIDATE_ARTIFACT_ID: \${{ needs.promote-production.outputs.candidate_artifact_id }}
|
|
2204
|
+
EBK_CANDIDATE_MATERIAL_DIGEST: \${{ needs.promote-production.outputs.candidate_material_digest }}
|
|
2205
|
+
EBK_CANDIDATE_PRODUCER_RUN_ATTEMPT: \${{ needs.promote-production.outputs.candidate_producer_run_attempt }}
|
|
2206
|
+
EBK_CANDIDATE_PRODUCER_RUN_ID: \${{ needs.promote-production.outputs.candidate_producer_run_id }}
|
|
2207
|
+
EBK_DEPLOYMENT_PROFILE: \${{ needs.prepare.outputs.profile }}
|
|
2208
|
+
EBK_ORDERED_PROJECTS: \${{ needs.prepare.outputs.ordered_projects }}
|
|
2209
|
+
EBK_POLICY_ACCOUNT_ID: \${{ needs.prepare.outputs.production_account_id }}
|
|
2210
|
+
EBK_POLICY_REGION: \${{ needs.prepare.outputs.region }}
|
|
2211
|
+
EBK_RELEASE_SOURCE_COMMIT: \${{ needs.immutable-artifacts.outputs.source_commit || inputs.source_commit || github.sha }}
|
|
2212
|
+
EBK_SLICE_PROJECTS: \${{ needs.prepare.outputs.slice_projects }}
|
|
2213
|
+
EBK_SOURCE_COMMIT: \${{ inputs.source_commit || github.sha }}
|
|
2214
|
+
EBK_STAGE: production
|
|
2215
|
+
# Production has no same-branch baseline receipt in this run; the
|
|
2216
|
+
# full lifecycle keeps its evidence complete.
|
|
2217
|
+
EBK_LIFECYCLE_SCOPE: full
|
|
2218
|
+
EBK_BASELINE_FOUND: 'false'
|
|
2219
|
+
EBK_BASELINE_RUN_ID: ''
|
|
2220
|
+
steps:
|
|
2221
|
+
${checkoutAndInstall('Verify managed workspace integrity before AWS authority')}
|
|
2222
|
+
- name: Restore exact immutable release candidate
|
|
2223
|
+
uses: ${actionPins.downloadArtifact}
|
|
2224
|
+
env:
|
|
2225
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
2226
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
2227
|
+
with:
|
|
2228
|
+
artifact-ids: \${{ needs.promote-production.outputs.candidate_artifact_id }}
|
|
2229
|
+
github-token: \${{ github.token }}
|
|
2230
|
+
path: .ebk/evidence/release-candidate
|
|
2231
|
+
run-id: \${{ needs.promote-production.outputs.candidate_producer_run_id }}
|
|
2232
|
+
- name: Restore exact promoted production container binding
|
|
2233
|
+
uses: ${actionPins.downloadArtifact}
|
|
2234
|
+
env:
|
|
2235
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
2236
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
2237
|
+
with:
|
|
2238
|
+
name: container-release-production-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
2239
|
+
path: .ebk/evidence/container-release
|
|
2240
|
+
- name: Verify exact predeploy container binding inventory
|
|
2241
|
+
env:
|
|
2242
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
2243
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
2244
|
+
run: |
|
|
2245
|
+
set -euo pipefail
|
|
2246
|
+
IFS=',' read -r -a projects <<< "$EBK_SLICE_PROJECTS"
|
|
2247
|
+
for project in "\${projects[@]}"; do
|
|
2248
|
+
test -n "$project" || continue
|
|
2249
|
+
test -f ".ebk/evidence/container-release/$EBK_STAGE/packages/$project/binding.json" || {
|
|
2250
|
+
echo "Exact predeploy container binding is absent for $project"
|
|
2251
|
+
exit 1
|
|
2252
|
+
}
|
|
2253
|
+
done
|
|
2254
|
+
- name: Verify exact restored staging-tested release candidate
|
|
2255
|
+
env:
|
|
2256
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
2257
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
2258
|
+
run: |
|
|
2259
|
+
node --input-type=module <<'NODE'
|
|
2260
|
+
${indentedScript(restoredCandidateValidationScript(), 10)}
|
|
2261
|
+
NODE
|
|
2262
|
+
- name: Authorize deployed Blueprint trust before protected mutation
|
|
2263
|
+
env:
|
|
2264
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
2265
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
2266
|
+
EBK_PROTECTED_BLUEPRINT_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
|
|
2267
|
+
run: |
|
|
2268
|
+
set -euo pipefail
|
|
2269
|
+
case "$EBK_PROTECTED_BLUEPRINT_COMMIT" in
|
|
2270
|
+
''|*[!0-9a-f]*) echo 'Protected Blueprint authority must be one full lowercase Git object ID'; exit 1 ;;
|
|
2271
|
+
esac
|
|
2272
|
+
test "\${#EBK_PROTECTED_BLUEPRINT_COMMIT}" -eq 40 ||
|
|
2273
|
+
test "\${#EBK_PROTECTED_BLUEPRINT_COMMIT}" -eq 64 ||
|
|
2274
|
+
{ echo 'Protected Blueprint authority must be one full Git object ID'; exit 1; }
|
|
2275
|
+
test "$(git rev-parse HEAD)" = "$EBK_PROTECTED_BLUEPRINT_COMMIT" ||
|
|
2276
|
+
{ echo 'Protected Blueprint authority differs from the exact workflow checkout'; exit 1; }
|
|
2277
|
+
node tools/blueprint-trust-check.mjs \\
|
|
2278
|
+
--operation=deploy \\
|
|
2279
|
+
--protected-branch=main \\
|
|
2280
|
+
--protected-ref="$EBK_PROTECTED_BLUEPRINT_COMMIT" \\
|
|
2281
|
+
--output=.ebk/evidence/supply-chain/blueprint-trust.json
|
|
2282
|
+
- name: Produce subject-bound protected scanner evidence
|
|
2283
|
+
env:
|
|
2284
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
2285
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
2286
|
+
run: pnpm nx run ${name}:supply-chain-scan
|
|
2287
|
+
- name: Enforce protected supply-chain policy before mutation
|
|
2288
|
+
env:
|
|
2289
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
2290
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
2291
|
+
run: pnpm nx run ${name}:supply-chain-check-protected
|
|
2292
|
+
- name: Install pinned uv for generated Python workloads
|
|
2293
|
+
env:
|
|
2294
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
2295
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
2296
|
+
run: |
|
|
2297
|
+
set -euo pipefail
|
|
2298
|
+
python3 -m pip install --user --disable-pip-version-check 'uv==${tool_versions_js_1.WORKSPACE_UV_VERSION}'
|
|
2299
|
+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
|
2300
|
+
"$HOME/.local/bin/uv" --version
|
|
2301
|
+
- name: Install pinned Playwright Chromium for deployed smoke proofs
|
|
2302
|
+
env:
|
|
2303
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
2304
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
2305
|
+
run: |
|
|
2306
|
+
${indentedScript(protectedBrowserInstallScript(), 10)}
|
|
2307
|
+
${revalidateTip}
|
|
2308
|
+
- name: Assume the one stage deployment role
|
|
2309
|
+
uses: ${actionPins.configureAwsCredentials}
|
|
2310
|
+
with:
|
|
2311
|
+
audience: \${{ needs.prepare.outputs.oidc_audience }}
|
|
2312
|
+
aws-region: \${{ vars.AWS_REGION }}
|
|
2313
|
+
role-session-name: ebk-native-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
2314
|
+
role-to-assume: \${{ env.AWS_DEPLOY_ROLE_ARN }}
|
|
2315
|
+
- name: Verify exact stage authority
|
|
2316
|
+
run: |
|
|
2317
|
+
set -euo pipefail
|
|
2318
|
+
[ "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ]
|
|
2319
|
+
[ "$AWS_REGION" = "$EBK_POLICY_REGION" ]
|
|
2320
|
+
${indentedScript(exactStageDeployerGuardScript('Native lifecycle', '$EBK_POLICY_ACCOUNT_ID'), 10)}
|
|
2321
|
+
- name: Run ordered native lifecycle targets
|
|
2322
|
+
id: native-lifecycle
|
|
2323
|
+
env:
|
|
2324
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
2325
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
2326
|
+
EBK_GITHUB_JOB_WORKFLOW_REF: \${{ job.workflow_ref }}
|
|
2327
|
+
GITHUB_TOKEN: \${{ github.token }}
|
|
2328
|
+
run: |
|
|
2329
|
+
set -euo pipefail
|
|
2330
|
+
test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'Stage deployment role is absent'; exit 1; }
|
|
2331
|
+
pnpm nx run ${name}:deploy-local -- \\
|
|
2332
|
+
--stage="$EBK_STAGE" \\
|
|
2333
|
+
--expectedRoleArn="$AWS_DEPLOY_ROLE_ARN" \\
|
|
2334
|
+
--lifecycleScope="$EBK_LIFECYCLE_SCOPE"
|
|
2335
|
+
- name: Prove temporary smoke admission removed
|
|
2336
|
+
if: always() && steps.native-lifecycle.outcome != 'skipped'
|
|
2337
|
+
env:
|
|
2338
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
2339
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
2340
|
+
EBK_GITHUB_JOB_WORKFLOW_REF: \${{ job.workflow_ref }}
|
|
2341
|
+
GITHUB_TOKEN: \${{ github.token }}
|
|
2342
|
+
run: |
|
|
2343
|
+
set -euo pipefail
|
|
2344
|
+
${indentedScript(exactStageDeployerGuardScript('Smoke access cleanup', '$EBK_POLICY_ACCOUNT_ID'), 10)}
|
|
2345
|
+
[ "$(git rev-parse HEAD)" = "$EBK_SOURCE_COMMIT" ]
|
|
2346
|
+
git diff --quiet HEAD
|
|
2347
|
+
node --input-type=module <<'NODE'
|
|
2348
|
+
import { cleanupGithubPrivateSmoke } from '@empire-builder-kit/nx/foundation';
|
|
2349
|
+
await cleanupGithubPrivateSmoke({
|
|
2350
|
+
account: process.env.EBK_POLICY_ACCOUNT_ID,
|
|
2351
|
+
deploymentProfile: process.env.EBK_DEPLOYMENT_PROFILE,
|
|
2352
|
+
root: process.cwd(),
|
|
2353
|
+
source: process.env.EBK_SOURCE_COMMIT,
|
|
2354
|
+
stage: process.env.EBK_STAGE,
|
|
2355
|
+
workspace: '${name}',
|
|
2356
|
+
});
|
|
2357
|
+
NODE
|
|
2358
|
+
- name: Seal exact postdeploy supply-chain binding
|
|
2359
|
+
env:
|
|
2360
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
2361
|
+
ACTIONS_ID_TOKEN_REQUEST_URL: ''
|
|
2362
|
+
run: |
|
|
2363
|
+
node --input-type=module <<'NODE'
|
|
2364
|
+
${indentedScript(postdeploySupplyChainBindingScript(), 10)}
|
|
2365
|
+
NODE
|
|
2366
|
+
- name: Retain bounded deployment receipt
|
|
2367
|
+
if: always()
|
|
2368
|
+
uses: ${actionPins.uploadArtifact}
|
|
2369
|
+
with:
|
|
2370
|
+
if-no-files-found: warn
|
|
2371
|
+
name: deployment-receipt-production-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
2372
|
+
path: |
|
|
2373
|
+
.ebk/evidence/deployment/latest.json
|
|
2374
|
+
.ebk/evidence/deployment/postdeploy-supply-chain.json
|
|
2375
|
+
.ebk/evidence/deployment/promotion.json
|
|
2376
|
+
.ebk/evidence/container-release/**
|
|
2377
|
+
retention-days: 30
|
|
2378
|
+
|
|
2379
|
+
record-main:
|
|
2380
|
+
name: record the deployed commit on main
|
|
2381
|
+
needs: [prepare, execute-production]
|
|
2382
|
+
if: >-
|
|
2383
|
+
always() &&
|
|
2384
|
+
needs.prepare.result == 'success' &&
|
|
2385
|
+
needs.execute-production.result == 'success' &&
|
|
2386
|
+
${trunkPromotionGate}
|
|
2387
|
+
runs-on: ubuntu-latest
|
|
2388
|
+
environment: production
|
|
2389
|
+
# The fast-forward uses the scoped environment token, not the workflow token.
|
|
2390
|
+
permissions:
|
|
2391
|
+
contents: read
|
|
2392
|
+
steps:
|
|
2393
|
+
- name: Fast-forward protected main to the deployed commit
|
|
2394
|
+
env:
|
|
2395
|
+
EBK_RECORD_MAIN_TOKEN: \${{ secrets.${exports.EBK_RECORD_MAIN_TOKEN_SECRET} }}
|
|
2396
|
+
EBK_SOURCE_BRANCH: \${{ env.EBK_DEPLOYMENT_SOURCE_BRANCH }}
|
|
2397
|
+
EBK_SOURCE_COMMIT: \${{ env.EBK_DEPLOYMENT_SOURCE_COMMIT }}
|
|
2398
|
+
GH_REPO: \${{ github.repository }}
|
|
2399
|
+
run: |
|
|
2400
|
+
set -euo pipefail
|
|
2401
|
+
test -n "$EBK_RECORD_MAIN_TOKEN" || { echo '${exports.EBK_RECORD_MAIN_TOKEN_SECRET} is absent from the production environment'; exit 1; }
|
|
2402
|
+
[ "$EBK_SOURCE_BRANCH" = develop ] || { echo 'Trunk promotion records main only from develop'; exit 1; }
|
|
2403
|
+
case "$EBK_SOURCE_COMMIT" in ''|*[!0-9a-f]*) exit 1 ;; esac
|
|
2404
|
+
export GH_TOKEN="$EBK_RECORD_MAIN_TOKEN"
|
|
2405
|
+
develop_tip="$(gh api "/repos/$GH_REPO/git/ref/heads/develop" --jq '.object.sha')"
|
|
2406
|
+
[ "$develop_tip" = "$EBK_SOURCE_COMMIT" ] || { echo 'Deployed commit is no longer the develop tip; leaving main unchanged'; exit 1; }
|
|
2407
|
+
main_tip="$(gh api "/repos/$GH_REPO/git/ref/heads/main" --jq '.object.sha')"
|
|
2408
|
+
if [ "$main_tip" = "$EBK_SOURCE_COMMIT" ]; then
|
|
2409
|
+
echo 'main already records the deployed commit'
|
|
2410
|
+
exit 0
|
|
2411
|
+
fi
|
|
2412
|
+
# GitHub refuses a non-fast-forward reference update unless force is
|
|
2413
|
+
# requested; it is never requested here.
|
|
2414
|
+
gh api -X PATCH "/repos/$GH_REPO/git/refs/heads/main" -f sha="$EBK_SOURCE_COMMIT" -F force=false >/dev/null
|
|
2415
|
+
recorded="$(gh api "/repos/$GH_REPO/git/ref/heads/main" --jq '.object.sha')"
|
|
2416
|
+
[ "$recorded" = "$EBK_SOURCE_COMMIT" ] || { echo 'main did not record the deployed commit'; exit 1; }
|
|
2417
|
+
echo "main now records $EBK_SOURCE_COMMIT (previously $main_tip)"
|
|
1549
2418
|
`;
|
|
1550
2419
|
}
|
|
1551
2420
|
function renderThinDeployWorkflowFilePlan(options) {
|