@empire-builder-kit/nx 1.0.0-rc.33 → 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/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 +886 -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 +48 -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,18 @@ 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
|
+
// 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')";
|
|
60
75
|
return `
|
|
61
76
|
|
|
62
77
|
deploy:
|
|
@@ -68,7 +83,8 @@ function renderThinDeployVerifyCaller(profile) {
|
|
|
68
83
|
${protectedBranchCondition} &&
|
|
69
84
|
needs.acceptance.result == 'success' &&
|
|
70
85
|
needs.acceptance.outputs.history_only != 'true' &&
|
|
71
|
-
needs.release-candidate.result == 'success'
|
|
86
|
+
(needs.release-candidate.result == 'success' ||
|
|
87
|
+
(github.ref == 'refs/heads/main' && needs.release-candidate.result == 'skipped'))
|
|
72
88
|
permissions:
|
|
73
89
|
actions: read
|
|
74
90
|
contents: read
|
|
@@ -79,7 +95,11 @@ function renderThinDeployVerifyCaller(profile) {
|
|
|
79
95
|
source_commit: \${{ github.sha }}
|
|
80
96
|
source_ref: \${{ github.ref }}
|
|
81
97
|
verify_run_attempt: \${{ github.run_attempt }}
|
|
82
|
-
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`;
|
|
83
103
|
}
|
|
84
104
|
function indentedScript(source, spaces) {
|
|
85
105
|
const prefix = ' '.repeat(spaces);
|
|
@@ -334,6 +354,13 @@ if (hasSlices) {
|
|
|
334
354
|
const artifactId = required('EBK_CANDIDATE_ARTIFACT_ID', /^[1-9][0-9]*$/);
|
|
335
355
|
const artifactDigest = required('EBK_CANDIDATE_ARTIFACT_DIGEST', /^sha256:[a-f0-9]{64}$/);
|
|
336
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;
|
|
337
364
|
const containerArtifactDigest = required('EBK_CONTAINER_ARTIFACT_DIGEST', /^sha256:[a-f0-9]{64}$/);
|
|
338
365
|
const root = '.ebk/evidence/release-candidate/packages';
|
|
339
366
|
const projects = readdirSync(root, { withFileTypes: true })
|
|
@@ -442,22 +469,35 @@ writeFileSync(
|
|
|
442
469
|
}
|
|
443
470
|
function renderThinDeployWorkflow(options) {
|
|
444
471
|
const { actionPins, deploymentProfile, name, toolVersions } = options;
|
|
472
|
+
const trunkPromotion = options.productionApprovalMode === 'trunk-promotion';
|
|
445
473
|
return `name: Deploy
|
|
446
474
|
run-name: Deploy \${{ inputs.source_commit || github.sha }}
|
|
447
475
|
|
|
448
476
|
on:
|
|
449
|
-
${workflowCallTrigger(deploymentProfile)} workflow_dispatch:
|
|
477
|
+
${workflowCallTrigger(deploymentProfile, options.deliveryLane)} workflow_dispatch:
|
|
450
478
|
inputs:
|
|
451
479
|
action:
|
|
452
480
|
description: Native lifecycle action
|
|
453
481
|
required: true
|
|
454
482
|
default: deploy
|
|
455
483
|
type: choice
|
|
456
|
-
options: [deploy, refresh, unlock, remove]
|
|
484
|
+
options: [deploy, refresh, unlock, remove${deploymentProfile === 'starter' ? '' : ', rehearse'}]
|
|
457
485
|
project:
|
|
458
486
|
description: Required registered project for unlock or remove recovery
|
|
459
487
|
required: false
|
|
460
|
-
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]
|
|
461
501
|
|
|
462
502
|
concurrency:
|
|
463
503
|
group: protected-lifecycle-\${{ inputs.source_branch || github.ref_name }}
|
|
@@ -478,6 +518,7 @@ jobs:
|
|
|
478
518
|
runs-on: ubuntu-latest
|
|
479
519
|
outputs:
|
|
480
520
|
action: \${{ steps.route.outputs.action }}
|
|
521
|
+
probe_url: \${{ steps.route.outputs.probe_url }}
|
|
481
522
|
account_id: \${{ steps.account.outputs.account_id }}
|
|
482
523
|
has_slices: \${{ steps.policy.outputs.has_slices }}
|
|
483
524
|
ordered_projects: \${{ steps.policy.outputs.ordered_projects }}
|
|
@@ -487,9 +528,14 @@ jobs:
|
|
|
487
528
|
project: \${{ steps.route.outputs.project }}
|
|
488
529
|
region: \${{ steps.policy.outputs.deployment_region }}
|
|
489
530
|
slice_projects: \${{ steps.policy.outputs.slice_projects }}
|
|
531
|
+
slice_matrix: \${{ steps.policy.outputs.slice_matrix }}
|
|
490
532
|
staging_account_id: \${{ steps.policy.outputs.staging_account_id }}
|
|
491
533
|
stage: \${{ steps.route.outputs.stage }}
|
|
492
|
-
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
|
+
: ''}
|
|
493
539
|
steps:
|
|
494
540
|
- name: Verify exact protected branch tip before setup
|
|
495
541
|
env:
|
|
@@ -551,6 +597,7 @@ jobs:
|
|
|
551
597
|
name: Resolve protected stage and action
|
|
552
598
|
env:
|
|
553
599
|
EBK_DISPATCH_ACTION: \${{ inputs.action || '' }}
|
|
600
|
+
EBK_DISPATCH_PROBE_URL: \${{ inputs.probe_url || '' }}
|
|
554
601
|
EBK_DISPATCH_PROJECT: \${{ inputs.project || '' }}
|
|
555
602
|
EBK_PROFILE: ${deploymentProfile}
|
|
556
603
|
EBK_SOURCE_BRANCH: \${{ env.EBK_DEPLOYMENT_SOURCE_BRANCH }}
|
|
@@ -566,12 +613,25 @@ jobs:
|
|
|
566
613
|
fi
|
|
567
614
|
case "$EBK_SOURCE_COMMIT" in ''|*[!0-9a-f]*) exit 1 ;; esac
|
|
568
615
|
{ [ "\${#EBK_SOURCE_COMMIT}" -eq 40 ] || [ "\${#EBK_SOURCE_COMMIT}" -eq 64 ]; } || exit 1
|
|
569
|
-
case "$action" in deploy|refresh|unlock|remove) ;; *) exit 1 ;; esac
|
|
616
|
+
case "$action" in deploy|refresh|unlock|remove|rehearse) ;; *) exit 1 ;; esac
|
|
570
617
|
case "$project" in ''|*[!A-Za-z0-9_-]*) [ -z "$project" ] || exit 1 ;; esac
|
|
571
618
|
case "$action" in
|
|
572
619
|
unlock|remove) [ -n "$project" ] || { echo 'unlock and remove require an explicit project'; exit 1; } ;;
|
|
573
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
|
+
;;
|
|
574
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
|
|
575
635
|
case "$EBK_SOURCE_BRANCH" in
|
|
576
636
|
main) stage=production ;;
|
|
577
637
|
develop)
|
|
@@ -584,12 +644,14 @@ jobs:
|
|
|
584
644
|
*) echo 'Protected lifecycle must run from develop or main'; exit 1 ;;
|
|
585
645
|
esac
|
|
586
646
|
echo "action=$action" >> "$GITHUB_OUTPUT"
|
|
647
|
+
echo "plan_action=$plan_action" >> "$GITHUB_OUTPUT"
|
|
648
|
+
echo "probe_url=$EBK_DISPATCH_PROBE_URL" >> "$GITHUB_OUTPUT"
|
|
587
649
|
echo "project=$project" >> "$GITHUB_OUTPUT"
|
|
588
650
|
echo "stage=$stage" >> "$GITHUB_OUTPUT"
|
|
589
651
|
- id: policy
|
|
590
652
|
name: Derive ordered projects from committed policy
|
|
591
653
|
env:
|
|
592
|
-
EBK_ACTION: \${{ steps.route.outputs.
|
|
654
|
+
EBK_ACTION: \${{ steps.route.outputs.plan_action }}
|
|
593
655
|
EBK_PROJECT: \${{ steps.route.outputs.project }}
|
|
594
656
|
EBK_STAGE: \${{ steps.route.outputs.stage }}
|
|
595
657
|
run: |
|
|
@@ -611,7 +673,22 @@ jobs:
|
|
|
611
673
|
EBK_STAGE: \${{ steps.route.outputs.stage }}
|
|
612
674
|
EBK_STAGING_ACCOUNT: \${{ steps.policy.outputs.staging_account_id }}
|
|
613
675
|
run: |
|
|
614
|
-
${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
|
+
: ''}
|
|
615
692
|
|
|
616
693
|
immutable-artifacts:
|
|
617
694
|
name: prepare exact immutable release artifacts
|
|
@@ -634,6 +711,11 @@ ${indentedScript(`set -euo pipefail\n${canonicalStageAuthorityDerivationScript()
|
|
|
634
711
|
container_artifact_id: \${{ steps.staging.outputs.container_artifact_id }}
|
|
635
712
|
staging_deployment_run_attempt: \${{ steps.staging.outputs.deployment_run_attempt }}
|
|
636
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 }}
|
|
637
719
|
producer_run_attempt: \${{ steps.current.outputs.producer_run_attempt || steps.staging.outputs.producer_run_attempt }}
|
|
638
720
|
producer_run_id: \${{ steps.current.outputs.producer_run_id || steps.staging.outputs.producer_run_id }}
|
|
639
721
|
source_commit: \${{ steps.current.outputs.source_commit || steps.staging.outputs.source_commit }}
|
|
@@ -684,6 +766,46 @@ ${indentedScript(`set -euo pipefail\n${canonicalStageAuthorityDerivationScript()
|
|
|
684
766
|
echo "producer_run_attempt=$run_attempt" >> "$GITHUB_OUTPUT"
|
|
685
767
|
echo "producer_run_id=$run_id" >> "$GITHUB_OUTPUT"
|
|
686
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
|
|
687
809
|
- id: staging-receipt
|
|
688
810
|
name: Select exact successful staging deployment receipt
|
|
689
811
|
if: needs.prepare.outputs.stage == 'production' && needs.prepare.outputs.profile != 'starter'
|
|
@@ -751,7 +873,9 @@ ${indentedScript(`set -euo pipefail\n${canonicalStageAuthorityDerivationScript()
|
|
|
751
873
|
EBK_EXPECTED_STAGING_ACCOUNT_ID: \${{ needs.prepare.outputs.staging_account_id }}
|
|
752
874
|
EBK_STAGING_RECEIPT_ARTIFACT_DIGEST: \${{ steps.staging-receipt.outputs.artifact_digest }}
|
|
753
875
|
EBK_STAGING_RECEIPT_ARTIFACT_ID: \${{ steps.staging-receipt.outputs.artifact_id }}
|
|
754
|
-
|
|
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
|
|
755
879
|
EBK_STAGING_SOURCE_COMMIT: \${{ steps.staging-receipt.outputs.source_commit }}
|
|
756
880
|
GH_REPO: \${{ github.repository }}
|
|
757
881
|
GH_TOKEN: \${{ github.token }}
|
|
@@ -787,7 +911,7 @@ ${indentedScript(stagingPromotionReceiptValidationScript(), 10)}
|
|
|
787
911
|
fi
|
|
788
912
|
|
|
789
913
|
container-platform:
|
|
790
|
-
name: native \${{ needs.prepare.outputs.stage }} OCI \${{ matrix.platform }}
|
|
914
|
+
name: native \${{ needs.prepare.outputs.stage }} OCI \${{ matrix.project.project }} \${{ matrix.platform }}
|
|
791
915
|
needs: [prepare, immutable-artifacts]
|
|
792
916
|
if: >-
|
|
793
917
|
always() &&
|
|
@@ -801,7 +925,10 @@ ${indentedScript(stagingPromotionReceiptValidationScript(), 10)}
|
|
|
801
925
|
)
|
|
802
926
|
strategy:
|
|
803
927
|
fail-fast: false
|
|
928
|
+
# One job per Slice and architecture: Slices build in parallel instead of
|
|
929
|
+
# looping on one runner per architecture.
|
|
804
930
|
matrix:
|
|
931
|
+
project: \${{ fromJson(needs.prepare.outputs.slice_matrix) }}
|
|
805
932
|
include:
|
|
806
933
|
- architecture: amd64
|
|
807
934
|
platform: linux/amd64
|
|
@@ -830,6 +957,7 @@ ${indentedScript(stagingPromotionReceiptValidationScript(), 10)}
|
|
|
830
957
|
EBK_SOURCE_COMMIT: \${{ inputs.source_commit || github.sha }}
|
|
831
958
|
EBK_STAGE: \${{ needs.prepare.outputs.stage }}
|
|
832
959
|
EBK_SLICE_PROJECTS: \${{ needs.prepare.outputs.slice_projects }}
|
|
960
|
+
EBK_SLICE_PROJECT: \${{ matrix.project.project }}
|
|
833
961
|
steps:
|
|
834
962
|
- name: Check out immutable container source
|
|
835
963
|
uses: ${actionPins.checkout}
|
|
@@ -870,6 +998,17 @@ ${indentedScript(stagingPromotionReceiptValidationScript(), 10)}
|
|
|
870
998
|
github-token: \${{ github.token }}
|
|
871
999
|
path: .ebk/evidence/release-candidate
|
|
872
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 }}
|
|
873
1012
|
- name: Revalidate exact protected branch tip before OIDC
|
|
874
1013
|
env:
|
|
875
1014
|
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ''
|
|
@@ -900,22 +1039,48 @@ ${indentedScript(stagingPromotionReceiptValidationScript(), 10)}
|
|
|
900
1039
|
[ "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ]
|
|
901
1040
|
[ "$AWS_REGION" = "$EBK_POLICY_REGION" ]
|
|
902
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 }}-
|
|
903
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) || '' }}
|
|
904
1061
|
run: |
|
|
905
1062
|
set -euo pipefail
|
|
906
1063
|
${indentedScript(exactStageDeployerGuardScript('Container build', '$EBK_POLICY_ACCOUNT_ID'), 10)}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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 }}"
|
|
912
1072
|
- name: Retain native platform evidence
|
|
913
1073
|
uses: ${actionPins.uploadArtifact}
|
|
914
1074
|
with:
|
|
915
|
-
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 }}
|
|
916
1076
|
if-no-files-found: error
|
|
917
1077
|
include-hidden-files: true
|
|
918
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.
|
|
919
1084
|
path: .ebk/evidence/container-release/partials/\${{ needs.prepare.outputs.stage }}/*/\${{ matrix.architecture }}
|
|
920
1085
|
|
|
921
1086
|
container-assemble:
|
|
@@ -1213,8 +1378,10 @@ ${indentedScript(exactStageDeployerGuardScript('Container promotion', '$AWS_EXPE
|
|
|
1213
1378
|
IFS=',' read -r -a projects <<< "$EBK_SLICE_PROJECTS"
|
|
1214
1379
|
for project in "\${projects[@]}"; do
|
|
1215
1380
|
test -n "$project" || continue
|
|
1216
|
-
|
|
1217
|
-
|
|
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"
|
|
1218
1385
|
pnpm nx run "$project:container-release-promote"
|
|
1219
1386
|
done
|
|
1220
1387
|
- id: retain
|
|
@@ -1233,6 +1400,7 @@ ${indentedScript(exactStageDeployerGuardScript('Container promotion', '$AWS_EXPE
|
|
|
1233
1400
|
if: >-
|
|
1234
1401
|
always() &&
|
|
1235
1402
|
needs.prepare.result == 'success' &&
|
|
1403
|
+
needs.prepare.outputs.action != 'rehearse' &&
|
|
1236
1404
|
(
|
|
1237
1405
|
needs.prepare.outputs.action != 'deploy' ||
|
|
1238
1406
|
(
|
|
@@ -1284,6 +1452,9 @@ ${indentedScript(exactStageDeployerGuardScript('Container promotion', '$AWS_EXPE
|
|
|
1284
1452
|
EBK_SLICE_PROJECTS: \${{ needs.prepare.outputs.slice_projects }}
|
|
1285
1453
|
EBK_SOURCE_COMMIT: \${{ inputs.source_commit || github.sha }}
|
|
1286
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 }}
|
|
1287
1458
|
steps:
|
|
1288
1459
|
- name: Check out immutable deployment source
|
|
1289
1460
|
uses: ${actionPins.checkout}
|
|
@@ -1325,6 +1496,17 @@ ${indentedScript(exactStageDeployerGuardScript('Container promotion', '$AWS_EXPE
|
|
|
1325
1496
|
github-token: \${{ github.token }}
|
|
1326
1497
|
path: .ebk/evidence/release-candidate
|
|
1327
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 }}
|
|
1328
1510
|
- name: Restore exact current-stage container binding
|
|
1329
1511
|
if: >-
|
|
1330
1512
|
needs.prepare.outputs.action == 'deploy' &&
|
|
@@ -1476,9 +1658,21 @@ ${indentedScript(exactStageDeployerGuardScript('Native lifecycle', '$EBK_POLICY_
|
|
|
1476
1658
|
set -euo pipefail
|
|
1477
1659
|
test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'Stage deployment role is absent'; exit 1; }
|
|
1478
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
|
|
1479
1671
|
pnpm nx run ${name}:deploy-local -- \
|
|
1480
1672
|
--stage="$EBK_STAGE" \
|
|
1481
|
-
--expectedRoleArn="$AWS_DEPLOY_ROLE_ARN"
|
|
1673
|
+
--expectedRoleArn="$AWS_DEPLOY_ROLE_ARN" \
|
|
1674
|
+
--lifecycleScope="$EBK_LIFECYCLE_SCOPE" \
|
|
1675
|
+
"\${baseline_arguments[@]}"
|
|
1482
1676
|
else
|
|
1483
1677
|
[ -n "$EBK_ORDERED_PROJECTS" ] || { echo 'Committed deployment order is empty'; exit 1; }
|
|
1484
1678
|
IFS=',' read -r -a projects <<< "$EBK_ORDERED_PROJECTS"
|
|
@@ -1546,6 +1740,672 @@ ${indentedScript(postdeploySupplyChainBindingScript(), 10)}
|
|
|
1546
1740
|
.ebk/evidence/deployment/promotion.json
|
|
1547
1741
|
.ebk/evidence/container-release/**
|
|
1548
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)"
|
|
1549
2409
|
`;
|
|
1550
2410
|
}
|
|
1551
2411
|
function renderThinDeployWorkflowFilePlan(options) {
|