@empire-builder-kit/nx 1.0.0-rc.15 → 1.0.0-rc.18
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/executors.json +10 -0
- package/package.json +3 -3
- package/src/cli/output.js +1 -1
- package/src/developer-agent/model.js +4 -4
- package/src/developer-agent/model.js.map +1 -1
- package/src/developer-agent/operation-registry.js +3 -1
- package/src/developer-agent/operation-registry.js.map +1 -1
- package/src/developer-agent/planning.js +7 -9
- package/src/developer-agent/planning.js.map +1 -1
- package/src/developer-agent/workspace-inspection.js +12 -0
- package/src/developer-agent/workspace-inspection.js.map +1 -1
- package/src/executors/deployment-plan/deployment-plan.js +5 -0
- package/src/executors/deployment-plan/deployment-plan.js.map +1 -1
- package/src/executors/foundation-preflight/foundation-preflight.d.ts +2 -0
- package/src/executors/foundation-preflight/foundation-preflight.js +6 -2
- package/src/executors/foundation-preflight/foundation-preflight.js.map +1 -1
- package/src/executors/foundation-preflight/schema.json +10 -0
- package/src/executors/foundation-profile-preflight/foundation-profile-preflight.d.ts +2 -0
- package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js +2 -1
- package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js.map +1 -1
- package/src/executors/foundation-profile-preflight/schema.json +19 -3
- package/src/executors/github-bootstrap/github-bootstrap.js +1 -0
- package/src/executors/github-bootstrap/github-bootstrap.js.map +1 -1
- package/src/executors/infrastructure-plan/infrastructure-plan.d.ts +229 -5
- package/src/executors/infrastructure-plan/infrastructure-plan.js +2298 -175
- package/src/executors/infrastructure-plan/infrastructure-plan.js.map +1 -1
- package/src/executors/infrastructure-plan/schema.json +16 -0
- package/src/executors/local-deploy/fixtures/local-deploy-state-machine.mjs +53 -0
- package/src/executors/local-deploy/local-approval.d.ts +23 -0
- package/src/executors/local-deploy/local-approval.js +43 -0
- package/src/executors/local-deploy/local-approval.js.map +1 -0
- package/src/executors/local-deploy/local-deploy.d.ts +69 -0
- package/src/executors/local-deploy/local-deploy.js +911 -0
- package/src/executors/local-deploy/local-deploy.js.map +1 -0
- package/src/executors/local-deploy/schema.json +42 -0
- package/src/executors/router-waf-rehearsal/router-waf-rehearsal.d.ts +3 -0
- package/src/executors/router-waf-rehearsal/router-waf-rehearsal.js +94 -0
- package/src/executors/router-waf-rehearsal/router-waf-rehearsal.js.map +1 -0
- package/src/executors/router-waf-rehearsal/schema.json +9 -0
- package/src/executors/secret-metadata/schema.json +5 -0
- package/src/executors/secret-metadata/secret-metadata.d.ts +12 -1
- package/src/executors/secret-metadata/secret-metadata.js +68 -16
- package/src/executors/secret-metadata/secret-metadata.js.map +1 -1
- package/src/executors/sst-lifecycle/logging-preflight.d.ts +5 -0
- package/src/executors/sst-lifecycle/logging-preflight.js +23 -13
- package/src/executors/sst-lifecycle/logging-preflight.js.map +1 -1
- package/src/executors/sst-lifecycle/run-sst-command.d.ts +8 -1
- package/src/executors/sst-lifecycle/run-sst-command.js +144 -12
- package/src/executors/sst-lifecycle/run-sst-command.js.map +1 -1
- package/src/executors/sst-lifecycle/schema.json +13 -1
- package/src/executors/sst-lifecycle/sst-environment-source.d.ts +25 -0
- package/src/executors/sst-lifecycle/sst-environment-source.js +213 -0
- package/src/executors/sst-lifecycle/sst-environment-source.js.map +1 -0
- package/src/foundation/index.d.ts +2 -0
- package/src/foundation/index.js +2 -0
- package/src/foundation/index.js.map +1 -1
- package/src/foundation/logging-preflight.js +1 -1
- package/src/foundation/logging-preflight.js.map +1 -1
- package/src/foundation/logging.d.ts +3 -1
- package/src/foundation/logging.js +1 -1
- package/src/foundation/logging.js.map +1 -1
- package/src/foundation/partial-state.d.ts +10 -0
- package/src/foundation/partial-state.js +21 -0
- package/src/foundation/partial-state.js.map +1 -0
- package/src/foundation/router-waf.d.ts +98 -0
- package/src/foundation/router-waf.js +401 -0
- package/src/foundation/router-waf.js.map +1 -0
- package/src/generators/function/function.js +2 -1
- package/src/generators/function/function.js.map +1 -1
- package/src/generators/preset/generator.js +23 -3
- package/src/generators/preset/generator.js.map +1 -1
- package/src/generators/preset/schema.d.ts +7 -0
- package/src/generators/preset/schema.json +47 -0
- package/src/generators/preset/workspace-files-ci-acceptance.js +176 -36
- package/src/generators/preset/workspace-files-ci-acceptance.js.map +1 -1
- package/src/generators/preset/workspace-files-developer-agent.d.ts +2 -0
- package/src/generators/preset/workspace-files-developer-agent.js +25 -11
- package/src/generators/preset/workspace-files-developer-agent.js.map +1 -1
- package/src/generators/preset/workspace-files-foundation-workload-roles.js +5 -2
- package/src/generators/preset/workspace-files-foundation-workload-roles.js.map +1 -1
- package/src/generators/preset/workspace-files-workload-role-baseline.js +254 -42
- package/src/generators/preset/workspace-files-workload-role-baseline.js.map +1 -1
- package/src/generators/preset/workspace-files.d.ts +6 -2
- package/src/generators/preset/workspace-files.js +821 -54
- package/src/generators/preset/workspace-files.js.map +1 -1
- package/src/generators/repository-policy/repository-policy.js +65 -3
- package/src/generators/repository-policy/repository-policy.js.map +1 -1
- package/src/generators/repository-policy/schema.d.ts +6 -0
- package/src/generators/repository-policy/schema.json +46 -0
- package/src/generators/slice/built-in-plan-api-files.js +2 -1
- package/src/generators/slice/built-in-plan-api-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js +18 -0
- package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-data-files.js +16 -5
- package/src/generators/slice/built-in-plan-data-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-data-retention-job-files.js +6 -3
- package/src/generators/slice/built-in-plan-data-retention-job-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-database-lifecycle-files.js +21 -7
- package/src/generators/slice/built-in-plan-database-lifecycle-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-foundation-files.js +2 -3
- package/src/generators/slice/built-in-plan-foundation-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-infrastructure-files.js +359 -27
- package/src/generators/slice/built-in-plan-infrastructure-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-infrastructure-safety-files.js +67 -11
- package/src/generators/slice/built-in-plan-infrastructure-safety-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-metadata-files.js +1 -0
- package/src/generators/slice/built-in-plan-metadata-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-operations-files.js +442 -5
- package/src/generators/slice/built-in-plan-operations-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-performance-files.d.ts +2 -1
- package/src/generators/slice/built-in-plan-performance-files.js +34 -47
- package/src/generators/slice/built-in-plan-performance-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-project-files.js +1 -0
- package/src/generators/slice/built-in-plan-project-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-release-files.js +119 -30
- package/src/generators/slice/built-in-plan-release-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-web-files.js +13 -0
- package/src/generators/slice/built-in-plan-web-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-workload-role-files.js +5 -2
- package/src/generators/slice/built-in-plan-workload-role-files.js.map +1 -1
- package/src/generators/slice/built-in-plan.d.ts +2 -1
- package/src/generators/slice/built-in-plan.js +8 -5
- package/src/generators/slice/built-in-plan.js.map +1 -1
- package/src/generators/slice/slice.js +11 -3
- package/src/generators/slice/slice.js.map +1 -1
- package/src/generators/upgrade/upgrade.js +35 -0
- package/src/generators/upgrade/upgrade.js.map +1 -1
- package/src/generators/workload/workload.js +2 -2
- package/src/generators/workload/workload.js.map +1 -1
- package/src/git-policy/github-bootstrap.d.ts +3 -1
- package/src/git-policy/github-bootstrap.js +22 -5
- package/src/git-policy/github-bootstrap.js.map +1 -1
- package/src/infrastructure-change/local-capability.d.ts +73 -0
- package/src/infrastructure-change/local-capability.js +114 -0
- package/src/infrastructure-change/local-capability.js.map +1 -0
- package/src/infrastructure-change/model.d.ts +25 -4
- package/src/infrastructure-change/model.js +1 -0
- package/src/infrastructure-change/model.js.map +1 -1
- package/src/infrastructure-change/normalize.d.ts +4 -0
- package/src/infrastructure-change/normalize.js +47 -7
- package/src/infrastructure-change/normalize.js.map +1 -1
- package/src/infrastructure-change/plan.d.ts +30 -3
- package/src/infrastructure-change/plan.js +323 -13
- package/src/infrastructure-change/plan.js.map +1 -1
- package/src/infrastructure-change/risk.d.ts +3 -1
- package/src/infrastructure-change/risk.js +96 -16
- package/src/infrastructure-change/risk.js.map +1 -1
- package/src/local-dev/private-state.d.ts +2 -0
- package/src/local-dev/private-state.js +3 -1
- package/src/local-dev/private-state.js.map +1 -1
- package/src/performance/cost-controls.d.ts +5 -3
- package/src/performance/cost-controls.js +30 -24
- package/src/performance/cost-controls.js.map +1 -1
- package/src/performance/model.d.ts +1 -1
- package/src/performance/model.js +0 -2
- package/src/performance/model.js.map +1 -1
- package/src/performance/test-fixtures.js +0 -2
- package/src/performance/test-fixtures.js.map +1 -1
- package/src/release/source-authority.d.ts +1 -0
- package/src/release/source-authority.js +136 -9
- package/src/release/source-authority.js.map +1 -1
- package/src/repository-policy/deployment-plan.d.ts +4 -1
- package/src/repository-policy/deployment-plan.js +8 -0
- package/src/repository-policy/deployment-plan.js.map +1 -1
- package/src/repository-policy/foundation-profile.js +1 -2
- package/src/repository-policy/foundation-profile.js.map +1 -1
- package/src/repository-policy/policy.d.ts +25 -1
- package/src/repository-policy/policy.js +145 -4
- package/src/repository-policy/policy.js.map +1 -1
- package/src/schemas/performance-policy-v1.json +0 -2
- package/src/schemas/repository-policy-v1.json +41 -0
- package/src/secrets/operator.js +22 -4
- package/src/secrets/operator.js.map +1 -1
- package/src/secrets/verification.js +9 -1
- package/src/secrets/verification.js.map +1 -1
- package/src/sst-environment-source-format.d.ts +3 -0
- package/src/sst-environment-source-format.js +179 -0
- package/src/sst-environment-source-format.js.map +1 -0
- package/src/supply-chain/blueprint-verification-authority.js +55 -3
- package/src/supply-chain/blueprint-verification-authority.js.map +1 -1
- package/src/supply-chain/generated-command.js +1 -0
- package/src/supply-chain/generated-command.js.map +1 -1
- package/src/supply-chain/generated-container-release-command.js +187 -38
- package/src/supply-chain/generated-container-release-command.js.map +1 -1
- package/src/supply-chain/generated-ecr-collector-command.js +12 -2
- package/src/supply-chain/generated-ecr-collector-command.js.map +1 -1
- package/src/supply-chain/generated-scanner-command.js +1 -0
- package/src/supply-chain/generated-scanner-command.js.map +1 -1
- package/src/supply-chain/sbom.d.ts +1 -1
- package/src/supply-chain/sbom.js +30 -5
- package/src/supply-chain/sbom.js.map +1 -1
- package/src/workload-roles/account-baseline.d.ts +11 -0
- package/src/workload-roles/account-baseline.js +253 -16
- package/src/workload-roles/account-baseline.js.map +1 -1
- package/src/workload-roles/authority.d.ts +3 -1
- package/src/workload-roles/authority.js +48 -2
- package/src/workload-roles/authority.js.map +1 -1
- package/src/workload-roles/aws-s3-state.js +27 -0
- package/src/workload-roles/aws-s3-state.js.map +1 -1
- package/src/workload-roles/boundary.d.ts +2 -1
- package/src/workload-roles/boundary.js +9 -2
- package/src/workload-roles/boundary.js.map +1 -1
- package/src/workload-roles/handler.js +52 -24
- package/src/workload-roles/handler.js.map +1 -1
- package/src/workload-roles/injection.js +1 -1
- package/src/workload-roles/injection.js.map +1 -1
- package/src/workload-roles/provisioner.js +12 -4
- package/src/workload-roles/provisioner.js.map +1 -1
- package/src/workload-roles/registration.d.ts +1 -0
- package/src/workload-roles/registration.js +5 -0
- package/src/workload-roles/registration.js.map +1 -1
- package/src/workload-roles/stage-boundary.js +1 -1
- package/src/workload-roles/stage-boundary.js.map +1 -1
- package/src/workload-roles/standard-slice.js +1 -0
- package/src/workload-roles/standard-slice.js.map +1 -1
|
@@ -61,11 +61,12 @@ exports.PLAN_PROVIDER_REFRESH_GLOBAL_READ_ACTIONS = [
|
|
|
61
61
|
'rds:DescribeDBProxies',
|
|
62
62
|
'rds:DescribeDBSubnetGroups',
|
|
63
63
|
];
|
|
64
|
-
function validatePlanProviderRefreshPolicy(value, region, account) {
|
|
64
|
+
function validatePlanProviderRefreshPolicy(value, region, account, stage) {
|
|
65
65
|
if (!Array.isArray(value) ||
|
|
66
66
|
value.length === 0 ||
|
|
67
67
|
!/^[a-z]{2}(?:-gov)?-[a-z]+-\d$/.test(region) ||
|
|
68
|
-
!/^\d{12}$/.test(account)
|
|
68
|
+
!/^\d{12}$/.test(account) ||
|
|
69
|
+
!['production', 'staging'].includes(stage)) {
|
|
69
70
|
throw new Error('Provider refresh policy must be a non-empty exact statement array for one account and region.');
|
|
70
71
|
}
|
|
71
72
|
const allowed = new Set(exports.PLAN_PROVIDER_REFRESH_READ_ACTIONS);
|
|
@@ -118,6 +119,14 @@ function validatePlanProviderRefreshPolicy(value, region, account) {
|
|
|
118
119
|
if (resources.some((resource) => {
|
|
119
120
|
const arn = resource.split(':');
|
|
120
121
|
const target = arn.slice(5).join(':');
|
|
122
|
+
const boundedPathWildcard = target.endsWith('/*') && !/[*?]/.test(target.slice(0, -2));
|
|
123
|
+
const boundedLambdaStagePrefix = arn[2] === 'lambda' &&
|
|
124
|
+
actions.every((action) => action.startsWith('lambda:') && !global.has(action)) &&
|
|
125
|
+
new RegExp('^function:(?:' +
|
|
126
|
+
stage +
|
|
127
|
+
'-|[A-Za-z0-9][A-Za-z0-9_-]*-' +
|
|
128
|
+
stage +
|
|
129
|
+
'-)[A-Za-z0-9_-]*\\*$').test(target);
|
|
121
130
|
return (arn[0] !== 'arn' ||
|
|
122
131
|
arn[1] !== 'aws' ||
|
|
123
132
|
!/^[a-z0-9-]+$/.test(arn[2] ?? '') ||
|
|
@@ -125,7 +134,8 @@ function validatePlanProviderRefreshPolicy(value, region, account) {
|
|
|
125
134
|
!['', account].includes(arn[4] ?? '') ||
|
|
126
135
|
!target ||
|
|
127
136
|
(/[*?]/.test(target) &&
|
|
128
|
-
|
|
137
|
+
!boundedPathWildcard &&
|
|
138
|
+
!boundedLambdaStagePrefix));
|
|
129
139
|
})) {
|
|
130
140
|
throw new Error(`Provider refresh statement ${index} is not bounded to exact targets in the selected account and region.`);
|
|
131
141
|
}
|
|
@@ -552,6 +562,7 @@ function planRoleAuditStep(branch, mode = 'produce') {
|
|
|
552
562
|
'AWS_PLAN_GENERATED_SECRET_ARNS_JSON',
|
|
553
563
|
new RegExp('^arn:aws:secretsmanager:' + process.env.AWS_REGION + ':' + account + ':secret:ebk/'),
|
|
554
564
|
);
|
|
565
|
+
const stateStage = process.env.EBK_PLAN_BRANCH === 'main' ? 'production' : 'staging';
|
|
555
566
|
let providerRefreshPolicy;
|
|
556
567
|
try {
|
|
557
568
|
providerRefreshPolicy = JSON.parse(
|
|
@@ -615,6 +626,23 @@ function planRoleAuditStep(branch, mode = 'produce') {
|
|
|
615
626
|
for (const resource of resources) {
|
|
616
627
|
const arn = resource.split(':');
|
|
617
628
|
const target = arn.slice(5).join(':');
|
|
629
|
+
const boundedPathWildcard =
|
|
630
|
+
target.endsWith('/*') &&
|
|
631
|
+
!/[*?]/.test(target.slice(0, -2));
|
|
632
|
+
const boundedLambdaStagePrefix =
|
|
633
|
+
arn[2] === 'lambda' &&
|
|
634
|
+
actions.every(
|
|
635
|
+
(action) =>
|
|
636
|
+
action.startsWith('lambda:') &&
|
|
637
|
+
!globalProviderReadActions.has(action),
|
|
638
|
+
) &&
|
|
639
|
+
new RegExp(
|
|
640
|
+
'^function:(?:' +
|
|
641
|
+
stateStage +
|
|
642
|
+
'-|[A-Za-z0-9][A-Za-z0-9_-]*-' +
|
|
643
|
+
stateStage +
|
|
644
|
+
'-)[A-Za-z0-9_-]*\\\\*$',
|
|
645
|
+
).test(target);
|
|
618
646
|
if (
|
|
619
647
|
arn[0] !== 'arn' ||
|
|
620
648
|
arn[1] !== 'aws' ||
|
|
@@ -623,7 +651,8 @@ function planRoleAuditStep(branch, mode = 'produce') {
|
|
|
623
651
|
!['', account].includes(arn[4] ?? '') ||
|
|
624
652
|
!target ||
|
|
625
653
|
(/[*?]/.test(target) &&
|
|
626
|
-
|
|
654
|
+
!boundedPathWildcard &&
|
|
655
|
+
!boundedLambdaStagePrefix)
|
|
627
656
|
) throw new Error('Provider refresh statement ' + index + ' has a target outside the selected account and region.');
|
|
628
657
|
}
|
|
629
658
|
}
|
|
@@ -638,7 +667,6 @@ function planRoleAuditStep(branch, mode = 'produce') {
|
|
|
638
667
|
).sort((left, right) =>
|
|
639
668
|
left.Sid < right.Sid ? -1 : left.Sid > right.Sid ? 1 : 0,
|
|
640
669
|
);
|
|
641
|
-
const stateStage = process.env.EBK_PLAN_BRANCH === 'main' ? 'production' : 'staging';
|
|
642
670
|
if (
|
|
643
671
|
stateKmsKeyArn &&
|
|
644
672
|
stateKmsKeyArn !==
|
|
@@ -709,6 +737,19 @@ function planRoleAuditStep(branch, mode = 'produce') {
|
|
|
709
737
|
],
|
|
710
738
|
Resource: '*',
|
|
711
739
|
},
|
|
740
|
+
{
|
|
741
|
+
Sid: 'ObserveStageSecretVersionIdentity',
|
|
742
|
+
Effect: 'Allow',
|
|
743
|
+
Action: 'secretsmanager:ListSecretVersionIds',
|
|
744
|
+
Resource:
|
|
745
|
+
'arn:aws:secretsmanager:' + process.env.AWS_REGION + ':' +
|
|
746
|
+
account + ':secret:*',
|
|
747
|
+
Condition: {
|
|
748
|
+
StringEquals: {
|
|
749
|
+
'aws:ResourceTag/ebk:stage': stateStage,
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
},
|
|
712
753
|
{
|
|
713
754
|
Sid: 'ValidatePlannedIamPolicies',
|
|
714
755
|
Effect: 'Allow',
|
|
@@ -969,6 +1010,7 @@ exports.WORKSPACE_PROJECT_MANAGED_JSON_POINTERS = [
|
|
|
969
1010
|
'/targets/blueprint-trust-check-protected',
|
|
970
1011
|
'/targets/developer-context',
|
|
971
1012
|
'/targets/deployment-plan',
|
|
1013
|
+
'/targets/deploy-local',
|
|
972
1014
|
'/targets/dev',
|
|
973
1015
|
'/targets/dev-doctor',
|
|
974
1016
|
'/targets/dev-setup',
|
|
@@ -989,6 +1031,10 @@ const FOUNDATION_SEEDED_PATHS = new Set([
|
|
|
989
1031
|
'docs/costs.md',
|
|
990
1032
|
'docs/runbooks.md',
|
|
991
1033
|
'docs/security.md',
|
|
1034
|
+
'performance/account-cost-policy.json',
|
|
1035
|
+
'security/router-waf-blocking-rehearsal.json',
|
|
1036
|
+
'security/router-waf-live-evidence.json',
|
|
1037
|
+
'security/router-waf.json',
|
|
992
1038
|
]);
|
|
993
1039
|
function createFoundationOwnership(files, frameworkVersion) {
|
|
994
1040
|
const paths = Object.keys(files)
|
|
@@ -1051,7 +1097,16 @@ function createWorkspaceOwnership(files, frameworkVersion) {
|
|
|
1051
1097
|
}
|
|
1052
1098
|
function renderWorkspaceFiles(options) {
|
|
1053
1099
|
const { frameworkVersion, name } = options;
|
|
1100
|
+
const deploymentProfile = options.deploymentProfile ?? 'starter';
|
|
1101
|
+
const initialRepositoryPolicy = (0, index_js_4.createIncubationRepositoryPolicy)(deploymentProfile);
|
|
1102
|
+
initialRepositoryPolicy.deploymentAccounts = options.deploymentAccounts ?? {
|
|
1103
|
+
development: null,
|
|
1104
|
+
production: null,
|
|
1105
|
+
staging: null,
|
|
1106
|
+
};
|
|
1107
|
+
initialRepositoryPolicy.deploymentRegion = options.deploymentRegion ?? null;
|
|
1054
1108
|
const developerAgentFilePlan = (0, workspace_files_developer_agent_js_1.renderDeveloperAgentWorkspaceFilePlan)({
|
|
1109
|
+
deploymentProfile,
|
|
1055
1110
|
frameworkVersion,
|
|
1056
1111
|
name,
|
|
1057
1112
|
});
|
|
@@ -1064,6 +1119,9 @@ function renderWorkspaceFiles(options) {
|
|
|
1064
1119
|
const uploadArtifactAction = (0, index_js_3.pinnedAction)(index_js_3.GITHUB_ACTION_PINS.uploadArtifact);
|
|
1065
1120
|
const protectedDevelopPromotion = "github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.ref == 'develop' && github.event.pull_request.base.ref == 'main'";
|
|
1066
1121
|
const protectedMainSync = "github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.ref == 'main' && github.event.pull_request.base.ref == 'develop'";
|
|
1122
|
+
const protectedPush = "github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')";
|
|
1123
|
+
const protectedDeploymentRouteJobName = 'protected deployment route';
|
|
1124
|
+
const stagingApplyJobPrefix = 'apply staging plan ';
|
|
1067
1125
|
const files = {
|
|
1068
1126
|
...(0, workspace_files_ci_acceptance_js_1.renderWorkspaceCiAcceptanceFiles)(),
|
|
1069
1127
|
'.node-version': `${tool_versions_js_1.WORKSPACE_NODE_VERSION}\n`,
|
|
@@ -1095,24 +1153,26 @@ env:
|
|
|
1095
1153
|
jobs:
|
|
1096
1154
|
acceptance:
|
|
1097
1155
|
name: accepted merge evidence
|
|
1098
|
-
if:
|
|
1156
|
+
if: ${protectedPush}
|
|
1099
1157
|
runs-on: ubuntu-latest
|
|
1100
1158
|
outputs:
|
|
1159
|
+
history_only: \${{ steps.resolve.outputs.history_only == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' }}
|
|
1101
1160
|
reused: \${{ steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' }}
|
|
1161
|
+
route: \${{ steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' && steps.resolve.outputs.route || '' }}
|
|
1102
1162
|
steps:
|
|
1103
1163
|
- name: Check out immutable merged candidate
|
|
1104
|
-
if:
|
|
1164
|
+
if: ${protectedPush}
|
|
1105
1165
|
uses: ${checkoutAction}
|
|
1106
1166
|
with:
|
|
1107
1167
|
fetch-depth: 0
|
|
1108
1168
|
ref: \${{ github.sha }}
|
|
1109
|
-
- name: Resolve exact protected-
|
|
1110
|
-
if:
|
|
1169
|
+
- name: Resolve exact protected-route acceptance
|
|
1170
|
+
if: ${protectedPush}
|
|
1111
1171
|
id: resolve
|
|
1112
1172
|
env:
|
|
1113
1173
|
GH_TOKEN: \${{ github.token }}
|
|
1114
1174
|
run: node tools/ci/workspace-acceptance.mjs resolve
|
|
1115
|
-
- name: Restore exact protected-
|
|
1175
|
+
- name: Restore exact protected-route acceptance
|
|
1116
1176
|
if: steps.resolve.outputs.reused == 'true'
|
|
1117
1177
|
id: restore
|
|
1118
1178
|
continue-on-error: true
|
|
@@ -1132,13 +1192,17 @@ jobs:
|
|
|
1132
1192
|
id: verify
|
|
1133
1193
|
continue-on-error: true
|
|
1134
1194
|
env:
|
|
1195
|
+
EBK_ACCEPTANCE_BASE: \${{ steps.resolve.outputs.base_commit }}
|
|
1135
1196
|
EBK_ACCEPTANCE_HEAD: \${{ steps.resolve.outputs.head_commit }}
|
|
1197
|
+
EBK_ACCEPTANCE_ROUTE: \${{ steps.resolve.outputs.route }}
|
|
1136
1198
|
EBK_ACCEPTANCE_RUN_ATTEMPT: \${{ steps.resolve.outputs.run_attempt }}
|
|
1137
1199
|
EBK_ACCEPTANCE_RUN_ID: \${{ steps.resolve.outputs.run_id }}
|
|
1138
1200
|
run: >-
|
|
1139
1201
|
node tools/ci/workspace-acceptance.mjs verify
|
|
1140
1202
|
.ebk/evidence/reused-acceptance/manifest.json
|
|
1141
1203
|
"$EBK_ACCEPTANCE_HEAD"
|
|
1204
|
+
"$EBK_ACCEPTANCE_BASE"
|
|
1205
|
+
"$EBK_ACCEPTANCE_ROUTE"
|
|
1142
1206
|
"$EBK_ACCEPTANCE_RUN_ID"
|
|
1143
1207
|
"$EBK_ACCEPTANCE_RUN_ATTEMPT"
|
|
1144
1208
|
|
|
@@ -1198,12 +1262,14 @@ jobs:
|
|
|
1198
1262
|
run: pnpm nx run-many -t verify --parallel=3
|
|
1199
1263
|
|
|
1200
1264
|
seal-acceptance:
|
|
1201
|
-
name: seal protected
|
|
1265
|
+
name: seal protected route acceptance
|
|
1202
1266
|
needs: [policy, verify]
|
|
1203
1267
|
if: >-
|
|
1204
1268
|
always() &&
|
|
1269
|
+
needs.policy.result == 'success' &&
|
|
1270
|
+
needs.verify.result == 'success' &&
|
|
1205
1271
|
github.event_name == 'pull_request' &&
|
|
1206
|
-
${protectedDevelopPromotion}
|
|
1272
|
+
(${protectedDevelopPromotion} || ${protectedMainSync})
|
|
1207
1273
|
runs-on: ubuntu-latest
|
|
1208
1274
|
steps:
|
|
1209
1275
|
- name: Check out immutable accepted candidate
|
|
@@ -1211,13 +1277,16 @@ jobs:
|
|
|
1211
1277
|
with:
|
|
1212
1278
|
fetch-depth: 0
|
|
1213
1279
|
ref: \${{ env.EBK_CI_CANDIDATE_SHA }}
|
|
1214
|
-
- name: Seal exact protected-
|
|
1280
|
+
- name: Seal exact protected-route acceptance
|
|
1281
|
+
id: seal
|
|
1215
1282
|
env:
|
|
1283
|
+
EBK_BASE_COMMIT: \${{ github.event.pull_request.base.sha }}
|
|
1216
1284
|
EBK_HEAD_REPOSITORY: \${{ github.event.pull_request.head.repo.full_name }}
|
|
1217
1285
|
run: >-
|
|
1218
1286
|
node tools/ci/workspace-acceptance.mjs seal
|
|
1219
1287
|
.ebk/evidence/ci/workspace-acceptance/manifest.json
|
|
1220
|
-
- name: Retain exact protected-
|
|
1288
|
+
- name: Retain exact protected-route acceptance
|
|
1289
|
+
if: steps.seal.outputs.sealed == 'true'
|
|
1221
1290
|
uses: ${uploadArtifactAction}
|
|
1222
1291
|
with:
|
|
1223
1292
|
name: workspace-acceptance-\${{ env.EBK_CI_CANDIDATE_SHA }}-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
@@ -1232,6 +1301,7 @@ jobs:
|
|
|
1232
1301
|
if: >-
|
|
1233
1302
|
always() &&
|
|
1234
1303
|
github.event_name == 'push' &&
|
|
1304
|
+
needs.acceptance.outputs.history_only != 'true' &&
|
|
1235
1305
|
(needs.acceptance.outputs.reused == 'true' ||
|
|
1236
1306
|
(needs.policy.result == 'success' && needs.verify.result == 'success'))
|
|
1237
1307
|
runs-on: ubuntu-latest
|
|
@@ -1298,7 +1368,97 @@ permissions:
|
|
|
1298
1368
|
contents: read
|
|
1299
1369
|
|
|
1300
1370
|
jobs:
|
|
1371
|
+
history-classification:
|
|
1372
|
+
name: classify protected history-only sync
|
|
1373
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
|
|
1374
|
+
runs-on: ubuntu-latest
|
|
1375
|
+
permissions:
|
|
1376
|
+
actions: read
|
|
1377
|
+
contents: read
|
|
1378
|
+
pull-requests: read
|
|
1379
|
+
outputs:
|
|
1380
|
+
history_only: \${{ steps.resolve.outputs.history_only == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' }}
|
|
1381
|
+
steps:
|
|
1382
|
+
- name: Check out immutable protected candidate
|
|
1383
|
+
uses: ${checkoutAction}
|
|
1384
|
+
with:
|
|
1385
|
+
fetch-depth: 0
|
|
1386
|
+
ref: \${{ github.sha }}
|
|
1387
|
+
- uses: ${setupNodeAction}
|
|
1388
|
+
with:
|
|
1389
|
+
node-version: ${tool_versions_js_1.WORKSPACE_NODE_VERSION}
|
|
1390
|
+
- name: Resolve exact protected-route acceptance
|
|
1391
|
+
id: resolve
|
|
1392
|
+
env:
|
|
1393
|
+
GH_TOKEN: \${{ github.token }}
|
|
1394
|
+
run: node tools/ci/workspace-acceptance.mjs resolve
|
|
1395
|
+
- name: Restore exact protected-route acceptance
|
|
1396
|
+
if: steps.resolve.outputs.reused == 'true'
|
|
1397
|
+
id: restore
|
|
1398
|
+
continue-on-error: true
|
|
1399
|
+
env:
|
|
1400
|
+
EBK_ACCEPTANCE_ARTIFACT_DIGEST: \${{ steps.resolve.outputs.artifact_digest }}
|
|
1401
|
+
EBK_ACCEPTANCE_ARTIFACT_ID: \${{ steps.resolve.outputs.artifact_id }}
|
|
1402
|
+
EBK_ACCEPTANCE_ARTIFACT_SIZE: \${{ steps.resolve.outputs.artifact_size }}
|
|
1403
|
+
GH_TOKEN: \${{ github.token }}
|
|
1404
|
+
run: >-
|
|
1405
|
+
node tools/ci/workspace-acceptance.mjs download
|
|
1406
|
+
.ebk/evidence/reused-history-acceptance
|
|
1407
|
+
"$EBK_ACCEPTANCE_ARTIFACT_ID"
|
|
1408
|
+
"$EBK_ACCEPTANCE_ARTIFACT_DIGEST"
|
|
1409
|
+
"$EBK_ACCEPTANCE_ARTIFACT_SIZE"
|
|
1410
|
+
- name: Verify exact history-only acceptance
|
|
1411
|
+
if: steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success'
|
|
1412
|
+
id: verify
|
|
1413
|
+
continue-on-error: true
|
|
1414
|
+
env:
|
|
1415
|
+
EBK_ACCEPTANCE_BASE: \${{ steps.resolve.outputs.base_commit }}
|
|
1416
|
+
EBK_ACCEPTANCE_HEAD: \${{ steps.resolve.outputs.head_commit }}
|
|
1417
|
+
EBK_ACCEPTANCE_ROUTE: \${{ steps.resolve.outputs.route }}
|
|
1418
|
+
EBK_ACCEPTANCE_RUN_ATTEMPT: \${{ steps.resolve.outputs.run_attempt }}
|
|
1419
|
+
EBK_ACCEPTANCE_RUN_ID: \${{ steps.resolve.outputs.run_id }}
|
|
1420
|
+
run: >-
|
|
1421
|
+
node tools/ci/workspace-acceptance.mjs verify
|
|
1422
|
+
.ebk/evidence/reused-history-acceptance/manifest.json
|
|
1423
|
+
"$EBK_ACCEPTANCE_HEAD"
|
|
1424
|
+
"$EBK_ACCEPTANCE_BASE"
|
|
1425
|
+
"$EBK_ACCEPTANCE_ROUTE"
|
|
1426
|
+
"$EBK_ACCEPTANCE_RUN_ID"
|
|
1427
|
+
"$EBK_ACCEPTANCE_RUN_ATTEMPT"
|
|
1428
|
+
|
|
1429
|
+
deployment-mode:
|
|
1430
|
+
name: classify deployment profile
|
|
1431
|
+
runs-on: ubuntu-latest
|
|
1432
|
+
outputs:
|
|
1433
|
+
profile: \${{ steps.profile.outputs.profile }}
|
|
1434
|
+
steps:
|
|
1435
|
+
- name: Check out immutable deployment policy
|
|
1436
|
+
uses: ${checkoutAction}
|
|
1437
|
+
with:
|
|
1438
|
+
ref: \${{ github.sha }}
|
|
1439
|
+
- id: profile
|
|
1440
|
+
name: Read exact committed deployment profile
|
|
1441
|
+
run: |
|
|
1442
|
+
node - <<'NODE'
|
|
1443
|
+
const { readFileSync } = require('node:fs');
|
|
1444
|
+
const policy = JSON.parse(readFileSync('ebk.repository-policy.json', 'utf8'));
|
|
1445
|
+
const profiles = new Set(['starter', 'production-isolated', 'fully-isolated']);
|
|
1446
|
+
if (!profiles.has(policy.deploymentProfile)) {
|
|
1447
|
+
throw new Error('Committed deployment profile is absent or invalid.');
|
|
1448
|
+
}
|
|
1449
|
+
require('node:fs').appendFileSync(
|
|
1450
|
+
process.env.GITHUB_OUTPUT,
|
|
1451
|
+
'profile=' + policy.deploymentProfile + '\\n',
|
|
1452
|
+
);
|
|
1453
|
+
NODE
|
|
1454
|
+
|
|
1301
1455
|
route:
|
|
1456
|
+
name: ${protectedDeploymentRouteJobName}
|
|
1457
|
+
needs: [history-classification, deployment-mode]
|
|
1458
|
+
if: >-
|
|
1459
|
+
always() &&
|
|
1460
|
+
needs.history-classification.outputs.history_only != 'true' &&
|
|
1461
|
+
needs.deployment-mode.result == 'success'
|
|
1302
1462
|
runs-on: ubuntu-latest
|
|
1303
1463
|
steps:
|
|
1304
1464
|
- name: Validate protected ref
|
|
@@ -1311,14 +1471,22 @@ jobs:
|
|
|
1311
1471
|
esac
|
|
1312
1472
|
|
|
1313
1473
|
plan-staging:
|
|
1314
|
-
needs: route
|
|
1315
|
-
if:
|
|
1474
|
+
needs: [route, deployment-mode]
|
|
1475
|
+
if: >-
|
|
1476
|
+
github.ref == 'refs/heads/develop' &&
|
|
1477
|
+
(github.event_name == 'workflow_dispatch' ||
|
|
1478
|
+
needs.deployment-mode.outputs.profile != 'starter')
|
|
1316
1479
|
runs-on: ubuntu-latest
|
|
1317
1480
|
outputs:
|
|
1481
|
+
deployment_profile: \${{ steps.plan.outputs.deployment_profile }}
|
|
1482
|
+
deployment_region: \${{ steps.plan.outputs.deployment_region }}
|
|
1483
|
+
development_account_id: \${{ steps.plan.outputs.development_account_id }}
|
|
1318
1484
|
foundation_project: \${{ steps.plan.outputs.foundation_project }}
|
|
1319
1485
|
has_slices: \${{ steps.plan.outputs.has_slices }}
|
|
1320
1486
|
ordered_projects: \${{ steps.plan.outputs.ordered_projects }}
|
|
1487
|
+
production_account_id: \${{ steps.plan.outputs.production_account_id }}
|
|
1321
1488
|
slice_projects: \${{ steps.plan.outputs.slice_projects }}
|
|
1489
|
+
staging_account_id: \${{ steps.plan.outputs.staging_account_id }}
|
|
1322
1490
|
steps:
|
|
1323
1491
|
- uses: ${checkoutAction}
|
|
1324
1492
|
with:
|
|
@@ -1529,16 +1697,21 @@ jobs:
|
|
|
1529
1697
|
path: .ebk/evidence/staging-release-source/**
|
|
1530
1698
|
|
|
1531
1699
|
plan-production:
|
|
1532
|
-
needs: route
|
|
1700
|
+
needs: [route, deployment-mode]
|
|
1533
1701
|
if: github.ref == 'refs/heads/main'
|
|
1534
1702
|
runs-on: ubuntu-latest
|
|
1535
1703
|
outputs:
|
|
1704
|
+
deployment_profile: \${{ steps.plan.outputs.deployment_profile }}
|
|
1705
|
+
deployment_region: \${{ steps.plan.outputs.deployment_region }}
|
|
1706
|
+
development_account_id: \${{ steps.plan.outputs.development_account_id }}
|
|
1536
1707
|
foundation_project: \${{ steps.plan.outputs.foundation_project }}
|
|
1537
1708
|
foundation_profile: \${{ steps.plan.outputs.foundation_profile }}
|
|
1538
1709
|
has_slices: \${{ steps.plan.outputs.has_slices }}
|
|
1539
1710
|
ordered_projects: \${{ steps.plan.outputs.ordered_projects }}
|
|
1540
1711
|
production_approval_mode: \${{ steps.plan.outputs.production_approval_mode }}
|
|
1712
|
+
production_account_id: \${{ steps.plan.outputs.production_account_id }}
|
|
1541
1713
|
slice_projects: \${{ steps.plan.outputs.slice_projects }}
|
|
1714
|
+
staging_account_id: \${{ steps.plan.outputs.staging_account_id }}
|
|
1542
1715
|
steps:
|
|
1543
1716
|
- uses: ${checkoutAction}
|
|
1544
1717
|
with:
|
|
@@ -1859,6 +2032,8 @@ jobs:
|
|
|
1859
2032
|
EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
|
|
1860
2033
|
EBK_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
|
|
1861
2034
|
EBK_STAGE: staging
|
|
2035
|
+
EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-staging.outputs.staging_account_id }}
|
|
2036
|
+
EBK_POLICY_REGION: \${{ needs.plan-staging.outputs.deployment_region }}
|
|
1862
2037
|
SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
|
|
1863
2038
|
steps:
|
|
1864
2039
|
- uses: ${checkoutAction}
|
|
@@ -1874,6 +2049,10 @@ jobs:
|
|
|
1874
2049
|
- run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
|
|
1875
2050
|
- name: Verify managed workspace integrity before AWS authority
|
|
1876
2051
|
run: pnpm nx run ${name}:workspace-health
|
|
2052
|
+
- name: Bind staging account to committed deployment topology
|
|
2053
|
+
run: |
|
|
2054
|
+
test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID"
|
|
2055
|
+
test "$AWS_REGION" = "$EBK_POLICY_REGION"
|
|
1877
2056
|
${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - uses: ${configureAwsAction}
|
|
1878
2057
|
with:
|
|
1879
2058
|
role-to-assume: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
|
@@ -1913,6 +2092,8 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - uses: ${c
|
|
|
1913
2092
|
EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
|
|
1914
2093
|
EBK_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
|
|
1915
2094
|
EBK_STAGE: staging
|
|
2095
|
+
EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-staging.outputs.staging_account_id }}
|
|
2096
|
+
EBK_POLICY_REGION: \${{ needs.plan-staging.outputs.deployment_region }}
|
|
1916
2097
|
SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
|
|
1917
2098
|
steps:
|
|
1918
2099
|
- uses: ${checkoutAction}
|
|
@@ -1928,6 +2109,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - uses: ${c
|
|
|
1928
2109
|
- run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
|
|
1929
2110
|
- name: Verify managed workspace integrity before AWS authority
|
|
1930
2111
|
run: pnpm nx run ${name}:workspace-health
|
|
2112
|
+
- name: Bind staging account to committed deployment topology
|
|
2113
|
+
run: |
|
|
2114
|
+
test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID"
|
|
2115
|
+
test "$AWS_REGION" = "$EBK_POLICY_REGION"
|
|
1931
2116
|
${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Restore both native platform records
|
|
1932
2117
|
uses: ${downloadArtifactAction}
|
|
1933
2118
|
with:
|
|
@@ -1969,6 +2154,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
|
|
|
1969
2154
|
AWS_DEPLOY_ROLE_ARN: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
|
1970
2155
|
AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
|
|
1971
2156
|
AWS_REGION: \${{ vars.AWS_REGION }}
|
|
2157
|
+
EBK_DEPLOYMENT_PROFILE: \${{ needs.plan-production.outputs.deployment_profile }}
|
|
2158
|
+
EBK_POLICY_PRODUCTION_ACCOUNT_ID: \${{ needs.plan-production.outputs.production_account_id }}
|
|
2159
|
+
EBK_POLICY_REGION: \${{ needs.plan-production.outputs.deployment_region }}
|
|
2160
|
+
EBK_POLICY_STAGING_ACCOUNT_ID: \${{ needs.plan-production.outputs.staging_account_id }}
|
|
1972
2161
|
EBK_CANDIDATE_RUN_ATTEMPT: \${{ needs.release-source-production.outputs.candidate_run_attempt }}
|
|
1973
2162
|
EBK_CANDIDATE_RUN_ID: \${{ needs.release-source-production.outputs.candidate_run_id }}
|
|
1974
2163
|
EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-production.outputs.source_commit }}
|
|
@@ -2069,11 +2258,13 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
|
|
|
2069
2258
|
cp "$source_sidecar" "$target"
|
|
2070
2259
|
done
|
|
2071
2260
|
- uses: ${configureAwsAction}
|
|
2261
|
+
if: env.EBK_DEPLOYMENT_PROFILE != 'starter'
|
|
2072
2262
|
with:
|
|
2073
2263
|
role-to-assume: \${{ secrets.AWS_STAGING_PROMOTION_PULL_ROLE_ARN }}
|
|
2074
2264
|
role-session-name: ebk-staging-container-pull-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
2075
2265
|
aws-region: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
|
|
2076
2266
|
- name: Authenticate exact staging source registry
|
|
2267
|
+
if: env.EBK_DEPLOYMENT_PROFILE != 'starter'
|
|
2077
2268
|
env:
|
|
2078
2269
|
EBK_STAGING_ACCOUNT_ID: \${{ vars.AWS_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID }}
|
|
2079
2270
|
EBK_STAGING_REGION: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
|
|
@@ -2092,10 +2283,27 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
|
|
|
2092
2283
|
role-to-assume: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
|
2093
2284
|
role-session-name: ebk-production-container-push-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
2094
2285
|
aws-region: \${{ vars.AWS_REGION }}
|
|
2286
|
+
- name: Bind production authority to committed deployment topology
|
|
2287
|
+
run: |
|
|
2288
|
+
test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_PRODUCTION_ACCOUNT_ID" ||
|
|
2289
|
+
{ echo 'Production environment account differs from committed deployment topology'; exit 1; }
|
|
2290
|
+
test "$AWS_REGION" = "$EBK_POLICY_REGION" ||
|
|
2291
|
+
{ echo 'Production environment Region differs from committed deployment topology'; exit 1; }
|
|
2292
|
+
- name: Authenticate shared-account staging source registry
|
|
2293
|
+
if: env.EBK_DEPLOYMENT_PROFILE == 'starter'
|
|
2294
|
+
run: |
|
|
2295
|
+
test "$EBK_POLICY_STAGING_ACCOUNT_ID" = "$EBK_POLICY_PRODUCTION_ACCOUNT_ID" ||
|
|
2296
|
+
{ echo 'Starter promotion requires one shared AWS account'; exit 1; }
|
|
2297
|
+
registry_suffix=.com
|
|
2298
|
+
case "$EBK_POLICY_REGION" in cn-*) registry_suffix=.com.cn ;; esac
|
|
2299
|
+
aws ecr get-login-password --region "$EBK_POLICY_REGION" |
|
|
2300
|
+
docker login --username AWS --password-stdin \
|
|
2301
|
+
"$EBK_POLICY_STAGING_ACCOUNT_ID.dkr.ecr.$EBK_POLICY_REGION.amazonaws$registry_suffix"
|
|
2095
2302
|
- name: Promote exact staging indexes without rebuilding
|
|
2096
2303
|
env:
|
|
2097
|
-
|
|
2098
|
-
|
|
2304
|
+
EBK_DEPLOYMENT_PROFILE: \${{ needs.plan-production.outputs.deployment_profile }}
|
|
2305
|
+
EBK_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID: \${{ needs.plan-production.outputs.deployment_profile == 'starter' && needs.plan-production.outputs.staging_account_id || vars.AWS_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID }}
|
|
2306
|
+
EBK_STAGING_PROMOTION_REGION: \${{ needs.plan-production.outputs.deployment_profile == 'starter' && needs.plan-production.outputs.deployment_region || vars.AWS_STAGING_PROMOTION_REGION }}
|
|
2099
2307
|
run: |
|
|
2100
2308
|
test "$(aws sts get-caller-identity --query Account --output text)" = "$AWS_EXPECTED_ACCOUNT_ID" ||
|
|
2101
2309
|
{ echo 'Production promotion role account differs from policy'; exit 1; }
|
|
@@ -2314,6 +2522,8 @@ ${planRoleAuditStep('main', 'verify')}
|
|
|
2314
2522
|
EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
|
|
2315
2523
|
EBK_SOURCE_COMMIT: \${{ github.sha }}
|
|
2316
2524
|
EBK_STAGE: staging
|
|
2525
|
+
EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-staging.outputs.staging_account_id }}
|
|
2526
|
+
EBK_POLICY_REGION: \${{ needs.plan-staging.outputs.deployment_region }}
|
|
2317
2527
|
FOUNDATION_PROJECT: \${{ needs.plan-staging.outputs.foundation_project }}
|
|
2318
2528
|
ORDERED_PROJECTS: \${{ needs.plan-staging.outputs.ordered_projects }}
|
|
2319
2529
|
SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
|
|
@@ -2421,7 +2631,7 @@ ${planRoleAuditStep('develop', 'verify')}
|
|
|
2421
2631
|
packages/*/.ebk/evidence/infrastructure-plan-command.json
|
|
2422
2632
|
|
|
2423
2633
|
deploy-staging:
|
|
2424
|
-
name:
|
|
2634
|
+
name: ${stagingApplyJobPrefix}\${{ needs.infrastructure-plan-staging.outputs.plan_digest || 'recovery-without-resource-plan' }}
|
|
2425
2635
|
needs: [plan-staging, release-source-staging, container-assemble-staging, infrastructure-plan-staging]
|
|
2426
2636
|
if: |
|
|
2427
2637
|
always() &&
|
|
@@ -2500,6 +2710,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
|
|
|
2500
2710
|
test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'staging AWS_DEPLOY_ROLE_ARN is required'; exit 1; }
|
|
2501
2711
|
test -n "$AWS_EXPECTED_ACCOUNT_ID" || { echo 'staging AWS_EXPECTED_ACCOUNT_ID is required'; exit 1; }
|
|
2502
2712
|
test -n "$AWS_REGION" || { echo 'staging AWS_REGION is required'; exit 1; }
|
|
2713
|
+
test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ||
|
|
2714
|
+
{ echo 'staging environment account differs from committed deployment topology'; exit 1; }
|
|
2715
|
+
test "$AWS_REGION" = "$EBK_POLICY_REGION" ||
|
|
2716
|
+
{ echo 'staging environment Region differs from committed deployment topology'; exit 1; }
|
|
2503
2717
|
test -n "$EBK_FLEET_ORIGIN" ||
|
|
2504
2718
|
{ echo 'staging environment EBK_FLEET_ORIGIN is required'; exit 1; }
|
|
2505
2719
|
test "$EBK_FLEET_ORIGIN" = "$EBK_PLANNED_FLEET_ORIGIN" ||
|
|
@@ -3136,6 +3350,8 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
|
|
|
3136
3350
|
EBK_APPROVED_MAIN_COMMIT: \${{ github.sha }}
|
|
3137
3351
|
EBK_SOURCE_COMMIT: \${{ github.sha }}
|
|
3138
3352
|
EBK_STAGE: production
|
|
3353
|
+
EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-production.outputs.production_account_id }}
|
|
3354
|
+
EBK_POLICY_REGION: \${{ needs.plan-production.outputs.deployment_region }}
|
|
3139
3355
|
EBK_VERIFIED_STAGING_RUN_ID: \${{ needs.release-source-production.outputs.verified_staging_run_id }}
|
|
3140
3356
|
FOUNDATION_PROJECT: \${{ needs.plan-production.outputs.foundation_project }}
|
|
3141
3357
|
ORDERED_PROJECTS: \${{ needs.plan-production.outputs.ordered_projects }}
|
|
@@ -3232,6 +3448,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
|
|
|
3232
3448
|
test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'production AWS_DEPLOY_ROLE_ARN is required'; exit 1; }
|
|
3233
3449
|
test -n "$AWS_EXPECTED_ACCOUNT_ID" || { echo 'production AWS_EXPECTED_ACCOUNT_ID is required'; exit 1; }
|
|
3234
3450
|
test -n "$AWS_REGION" || { echo 'production AWS_REGION is required'; exit 1; }
|
|
3451
|
+
test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ||
|
|
3452
|
+
{ echo 'production environment account differs from committed deployment topology'; exit 1; }
|
|
3453
|
+
test "$AWS_REGION" = "$EBK_POLICY_REGION" ||
|
|
3454
|
+
{ echo 'production environment Region differs from committed deployment topology'; exit 1; }
|
|
3235
3455
|
test -n "$EBK_FLEET_ORIGIN" ||
|
|
3236
3456
|
{ echo 'production environment EBK_FLEET_ORIGIN is required'; exit 1; }
|
|
3237
3457
|
test "$EBK_FLEET_ORIGIN" = "$EBK_PLANNED_FLEET_ORIGIN" ||
|
|
@@ -4898,13 +5118,51 @@ permissions:
|
|
|
4898
5118
|
contents: read
|
|
4899
5119
|
|
|
4900
5120
|
jobs:
|
|
5121
|
+
deployment-classification:
|
|
5122
|
+
name: classify exact staging deployment
|
|
5123
|
+
if: >-
|
|
5124
|
+
github.event_name == 'workflow_run' &&
|
|
5125
|
+
github.event.workflow_run.event == 'push' &&
|
|
5126
|
+
github.event.workflow_run.head_branch == 'develop' &&
|
|
5127
|
+
github.event.workflow_run.conclusion == 'success'
|
|
5128
|
+
permissions:
|
|
5129
|
+
actions: read
|
|
5130
|
+
contents: read
|
|
5131
|
+
outputs:
|
|
5132
|
+
deployed: \${{ steps.classify.outputs.deployed }}
|
|
5133
|
+
runs-on: ubuntu-latest
|
|
5134
|
+
steps:
|
|
5135
|
+
- id: classify
|
|
5136
|
+
name: Require one successful staging deployment job
|
|
5137
|
+
env:
|
|
5138
|
+
EBK_DEPLOYMENT_RUN_ID: \${{ github.event.workflow_run.id }}
|
|
5139
|
+
GH_TOKEN: \${{ github.token }}
|
|
5140
|
+
GH_REPO: \${{ github.repository }}
|
|
5141
|
+
run: |
|
|
5142
|
+
jobs="$(gh api \
|
|
5143
|
+
"/repos/$GH_REPO/actions/runs/$EBK_DEPLOYMENT_RUN_ID/jobs?filter=latest&per_page=100")"
|
|
5144
|
+
apply_count="$(printf '%s' "$jobs" | jq \
|
|
5145
|
+
'[.jobs[] | select(.name | startswith("${stagingApplyJobPrefix}")) | select(.conclusion == "success")] | length')"
|
|
5146
|
+
route_success_count="$(printf '%s' "$jobs" | jq \
|
|
5147
|
+
'[.jobs[] | select(.name == "${protectedDeploymentRouteJobName}" and .conclusion == "success")] | length')"
|
|
5148
|
+
route_skipped_count="$(printf '%s' "$jobs" | jq \
|
|
5149
|
+
'[.jobs[] | select(.name == "${protectedDeploymentRouteJobName}" and .conclusion == "skipped")] | length')"
|
|
5150
|
+
case "$apply_count:$route_success_count:$route_skipped_count" in
|
|
5151
|
+
1:1:0) echo 'deployed=true' >> "$GITHUB_OUTPUT" ;;
|
|
5152
|
+
0:0:1) echo 'deployed=false' >> "$GITHUB_OUTPUT" ;;
|
|
5153
|
+
*) echo 'Deploy route and staging application evidence disagree'; exit 1 ;;
|
|
5154
|
+
esac
|
|
5155
|
+
|
|
4901
5156
|
plan:
|
|
5157
|
+
needs: deployment-classification
|
|
4902
5158
|
if: >-
|
|
4903
|
-
(
|
|
5159
|
+
always() &&
|
|
5160
|
+
((github.event_name == 'workflow_run' &&
|
|
4904
5161
|
github.event.workflow_run.conclusion == 'success' &&
|
|
4905
5162
|
github.event.workflow_run.event == 'push' &&
|
|
4906
|
-
github.event.workflow_run.head_branch == 'develop'
|
|
4907
|
-
|
|
5163
|
+
github.event.workflow_run.head_branch == 'develop' &&
|
|
5164
|
+
needs.deployment-classification.outputs.deployed == 'true') ||
|
|
5165
|
+
github.event_name == 'workflow_dispatch')
|
|
4908
5166
|
outputs:
|
|
4909
5167
|
deployment_run_attempt: \${{ steps.identity.outputs.deployment_run_attempt }}
|
|
4910
5168
|
deployment_run_id: \${{ steps.identity.outputs.deployment_run_id }}
|
|
@@ -5397,14 +5655,14 @@ jobs:
|
|
|
5397
5655
|
test "$(node --version | cut -d. -f1)" = v22
|
|
5398
5656
|
- name: Run exact deployed smoke
|
|
5399
5657
|
working-directory: \${{ runner.temp }}/ebk-prepared-workspace
|
|
5400
|
-
run: node node_modules/nx/bin/nx.js run "$EBK_PROJECT:smoke-deployed"
|
|
5658
|
+
run: node node_modules/nx/dist/bin/nx.js run "$EBK_PROJECT:smoke-deployed"
|
|
5401
5659
|
- name: Verify reviewed frontend visual baseline
|
|
5402
5660
|
working-directory: \${{ runner.temp }}/ebk-prepared-workspace
|
|
5403
|
-
run: node node_modules/nx/bin/nx.js run "$EBK_PROJECT:frontend-visual"
|
|
5661
|
+
run: node node_modules/nx/dist/bin/nx.js run "$EBK_PROJECT:frontend-visual"
|
|
5404
5662
|
- name: Verify deployed frontend security headers
|
|
5405
5663
|
working-directory: \${{ runner.temp }}/ebk-prepared-workspace
|
|
5406
5664
|
run: |
|
|
5407
|
-
node node_modules/nx/bin/nx.js run \
|
|
5665
|
+
node node_modules/nx/dist/bin/nx.js run \
|
|
5408
5666
|
"$EBK_PROJECT:frontend-security-headers" -- \
|
|
5409
5667
|
--url="$EBK_SMOKE_URL"
|
|
5410
5668
|
- name: Bind exact alarm and data-integrity evidence
|
|
@@ -6851,11 +7109,11 @@ jobs:
|
|
|
6851
7109
|
if [ "$EBK_ACTION" = apply ]; then args+=("--apply"); fi
|
|
6852
7110
|
pnpm nx run "$EBK_PROJECT:secret-metadata" -- "\${args[@]}"
|
|
6853
7111
|
`,
|
|
6854
|
-
'.github/workflows/sync-back.yml': `name: Sync
|
|
7112
|
+
'.github/workflows/sync-back.yml': `name: Sync protected main history
|
|
6855
7113
|
|
|
6856
7114
|
on:
|
|
6857
7115
|
workflow_run:
|
|
6858
|
-
workflows: [
|
|
7116
|
+
workflows: [Verify]
|
|
6859
7117
|
types: [completed]
|
|
6860
7118
|
|
|
6861
7119
|
permissions:
|
|
@@ -6866,6 +7124,7 @@ jobs:
|
|
|
6866
7124
|
sync-back:
|
|
6867
7125
|
if: >-
|
|
6868
7126
|
github.event.workflow_run.conclusion == 'success' &&
|
|
7127
|
+
github.event.workflow_run.event == 'push' &&
|
|
6869
7128
|
github.event.workflow_run.head_branch == 'main'
|
|
6870
7129
|
runs-on: ubuntu-latest
|
|
6871
7130
|
steps:
|
|
@@ -6879,8 +7138,8 @@ jobs:
|
|
|
6879
7138
|
gh pr create \
|
|
6880
7139
|
--head main \
|
|
6881
7140
|
--base develop \
|
|
6882
|
-
--title 'chore: sync
|
|
6883
|
-
--body 'Managed EBK sync
|
|
7141
|
+
--title 'chore(git): sync protected main to develop' \
|
|
7142
|
+
--body 'Managed EBK source-history sync. Review and merge; this does not claim or waive independent staging or production deployment evidence, and this workflow never pushes or auto-merges.'
|
|
6884
7143
|
number=$(gh pr list --state open --head main --base develop --json number --jq '.[0].number')
|
|
6885
7144
|
fi
|
|
6886
7145
|
echo "Sync-back pull request #$number is ready for review."
|
|
@@ -7234,6 +7493,16 @@ use the direct installed \`.bin/ebk blueprint verification-review\` entry docume
|
|
|
7234
7493
|
\`docs/blueprints.md\`.
|
|
7235
7494
|
|
|
7236
7495
|
Workspace creation only writes source files. It does not create or change cloud resources.
|
|
7496
|
+
|
|
7497
|
+
New workspaces default to the one-account \`starter\` deployment profile. Configure the exact
|
|
7498
|
+
shared account and Region in \`ebk.repository-policy.json\`, authenticate through IAM Identity
|
|
7499
|
+
Center, add exact development and staging CIDRs to
|
|
7500
|
+
\`packages/foundation/security/router-waf.json\`, and use
|
|
7501
|
+
\`pnpm nx run ${name}:deploy-local -- --stage=development
|
|
7502
|
+
--profile=<profile> --expectedRoleArn=<role-arn>\`. Never invoke SST directly.
|
|
7503
|
+
For a brand-new stage with generated Product Slices, complete the documented workload-role
|
|
7504
|
+
Foundation publication and pass \`--phase=foundation-bootstrap\` once before publishing and
|
|
7505
|
+
deploying the complete stage.
|
|
7237
7506
|
`,
|
|
7238
7507
|
'docs/architecture.md': `# Workspace architecture
|
|
7239
7508
|
|
|
@@ -7250,6 +7519,11 @@ Foundation implementation files.
|
|
|
7250
7519
|
Canonical consumer stages use the matching \`development\`, \`staging\`, or \`production\`
|
|
7251
7520
|
Foundation. Framework-derived personal stages may use only the \`development\`
|
|
7252
7521
|
Foundation; generic preview stages are unsupported.
|
|
7522
|
+
|
|
7523
|
+
The committed deployment profile changes account and local/GitHub authority only. Every
|
|
7524
|
+
canonical stage always owns a separate Router/domain, Aurora cluster and migration history,
|
|
7525
|
+
Cognito user pool, event bus, secret namespace, IAM boundary, logs, queues, ECR namespace, and
|
|
7526
|
+
stage-qualified SSM discovery contract. Discovery rejects a record for another canonical stage.
|
|
7253
7527
|
`,
|
|
7254
7528
|
'docs/git-workflow.md': `# Protected Git workflow
|
|
7255
7529
|
|
|
@@ -7270,6 +7544,9 @@ GitHub rulesets, environments, and AWS IAM are the authorization boundaries; loc
|
|
|
7270
7544
|
only defense in depth.
|
|
7271
7545
|
|
|
7272
7546
|
The \`staging\` environment accepts only \`develop\`; \`production\` accepts only \`main\`.
|
|
7547
|
+
These workflows exist for every deployment profile. The staging workflow is optional for
|
|
7548
|
+
\`starter\`, whose governed local Nx target may deploy development and staging. Production is
|
|
7549
|
+
GitHub-only for every profile; \`fully-isolated\` also requires GitHub for staging.
|
|
7273
7550
|
The committed \`productionApproval.mode\` selects one of two production authorization
|
|
7274
7551
|
contracts:
|
|
7275
7552
|
|
|
@@ -7292,7 +7569,9 @@ contracts:
|
|
|
7292
7569
|
Both modes retain required checks, a non-author CODEOWNER approval, no ruleset bypass, the
|
|
7293
7570
|
literal production environment, and its branch-scoped credentials. The base \`staging\` and
|
|
7294
7571
|
\`production\` environments own deployment, smoke, and ECR evidence roles plus their
|
|
7295
|
-
variables;
|
|
7572
|
+
variables; isolated profiles also keep the staging-registry promotion pull role in production.
|
|
7573
|
+
Starter promotion authenticates once to the shared account and copies the exact staging digest
|
|
7574
|
+
into a distinct production ECR repository. Dedicated
|
|
7296
7575
|
\`infrastructure-plan-<stage>\` environments gate read-only role use. Separate
|
|
7297
7576
|
\`async-recovery-<stage>\`, \`performance-aggregate-<stage>\`,
|
|
7298
7577
|
\`performance-cost-<stage>\`, \`performance-load-<stage>\`, and
|
|
@@ -7441,18 +7720,35 @@ materializes, close and reopen the pull request as a human and then approve it.
|
|
|
7441
7720
|
`,
|
|
7442
7721
|
'docs/repository-operating-policy.md': `# Repository Operating Policy
|
|
7443
7722
|
|
|
7444
|
-
\`ebk.repository-policy.json\` is the reviewed source of truth for
|
|
7445
|
-
|
|
7723
|
+
\`ebk.repository-policy.json\` is the reviewed source of truth for deployment profile, exact
|
|
7724
|
+
stage account IDs, deployment Region, production access, production approval, per-Slice
|
|
7725
|
+
readiness, and the one shared production Foundation profile.
|
|
7446
7726
|
New workspaces start with production disabled and explicit Team-compatible
|
|
7447
7727
|
\`productionApproval.mode: "promotion-pr"\`. A missing approval mode or Slice entry fails
|
|
7448
7728
|
closed; no GitHub capability is allowed to choose a mode implicitly.
|
|
7449
7729
|
|
|
7730
|
+
Deployment profiles are proportional to operator maturity:
|
|
7731
|
+
|
|
7732
|
+
- \`starter\` (default) requires one shared account, permits local development and staging
|
|
7733
|
+
through the governed root Nx target, and reserves production for protected GitHub deployment;
|
|
7734
|
+
- \`production-isolated\` requires one development/staging account and a distinct production
|
|
7735
|
+
account; only production is GitHub-only; and
|
|
7736
|
+
- \`fully-isolated\` requires three distinct accounts; staging and production are GitHub-only.
|
|
7737
|
+
|
|
7738
|
+
Equal account IDs never imply shared stage resources. Router, Aurora, Cognito, events, secrets,
|
|
7739
|
+
IAM, logs, queues, resource names, and SSM discovery remain stage-specific. Existing prerelease
|
|
7740
|
+
repositories migrate explicitly to \`fully-isolated\`; upgrades never infer or weaken topology.
|
|
7741
|
+
That profile-only migration retains the former authority model but stays undeployable until exact
|
|
7742
|
+
development, staging, and production account IDs plus \`deploymentRegion\` are committed.
|
|
7743
|
+
|
|
7450
7744
|
The named repository modes are source-writing presets, not runtime state. Report a change
|
|
7451
7745
|
first and inspect the complete current/next policy:
|
|
7452
7746
|
|
|
7453
7747
|
\`\`\`bash
|
|
7454
7748
|
pnpm nx g @empire-builder-kit/nx:repository-policy \\
|
|
7455
|
-
--
|
|
7749
|
+
--deploymentProfile=starter --deploymentRegion=us-east-1 \\
|
|
7750
|
+
--developmentAccountId=111122223333 --stagingAccountId=111122223333 \\
|
|
7751
|
+
--productionAccountId=111122223333
|
|
7456
7752
|
\`\`\`
|
|
7457
7753
|
|
|
7458
7754
|
Apply the reviewed source change separately with \`--apply\`. This command never contacts AWS
|
|
@@ -7462,6 +7758,66 @@ Use \`--productionApprovalMode=promotion-pr|environment-reviewer\` for an explic
|
|
|
7462
7758
|
transition. This changes committed policy only; a separate bootstrap report/apply reconciles
|
|
7463
7759
|
remote GitHub settings.
|
|
7464
7760
|
|
|
7761
|
+
After applying exact topology, authenticate with temporary federated credentials for development
|
|
7762
|
+
or staging and invoke
|
|
7763
|
+
\`pnpm nx run ${name}:deploy-local -- --stage=<stage> --profile=<profile>
|
|
7764
|
+
--expectedRoleArn=<role-arn>\`. Production runs only through the protected main-branch GitHub
|
|
7765
|
+
workflow. Each admitted local invocation plans the complete current stage, displays the stage
|
|
7766
|
+
summary and first material project, and requires a real TTY response of
|
|
7767
|
+
\`approve:<action>:<stage>:<project>:<current-plan-digest>\`. A human or authorized agent may type
|
|
7768
|
+
that acknowledgement; it is active operator control, not a claim of independent human review.
|
|
7769
|
+
The target creates one private ten-minute capability for that project, rechecks the exact source,
|
|
7770
|
+
artifacts, account, Region, role, stage, policy, state, and lock without a second provider preview,
|
|
7771
|
+
consumes the capability once, and removes it on every exit path. Unattended scripts cannot approve.
|
|
7772
|
+
Every material deploy or refresh mutates at most the first material project and ends with a
|
|
7773
|
+
controlled non-success \`replan-required\` handoff, including the last project in the ordered stage.
|
|
7774
|
+
Rerun the complete stage, review the new current summary, and repeat until one invocation proves
|
|
7775
|
+
every project is semantically converged. Only that convergence invocation succeeds. A preview is
|
|
7776
|
+
recent review and audit evidence; it is not represented as a cryptographically replayable Pulumi
|
|
7777
|
+
transaction. A complete, unlocked deploy plan containing only provider reads with no changed
|
|
7778
|
+
properties, replacement, failure, or unmatched risk finding is retained as observation evidence
|
|
7779
|
+
and counts as semantically converged. The standalone project planner may still return its
|
|
7780
|
+
conservative non-success for that drift observation; the root local transaction accepts only a
|
|
7781
|
+
freshly emitted exact provider-read-only record from that child. Repeated reads coalesce
|
|
7782
|
+
only when their logical ID and resource type match exactly. Any stale or ambiguous
|
|
7783
|
+
observation fails closed; a plan with desired changes remains material and requires review. One
|
|
7784
|
+
narrower case stays reviewable instead of becoming
|
|
7785
|
+
drift: when an upstream Foundation apply changes only both external framework-version tag
|
|
7786
|
+
projections (plus SST's already-bounded name hydration), the retained
|
|
7787
|
+
external identity must match one exact non-replacing provider read in the same ordinary plan.
|
|
7788
|
+
Missing read correlation, owned resources, other tag or property changes, incomplete tag pairs,
|
|
7789
|
+
and provider failures remain drift and fail closed. The local target requires the exact SST v5 account bootstrap normally
|
|
7790
|
+
created by the pinned \`ebk dev up\` path, then creates and attests only missing exact
|
|
7791
|
+
per-project/canonical-stage state passphrases before its formal read-only plans. This passphrase
|
|
7792
|
+
bootstrap is the only persistent pre-acknowledgement write; it creates no deployment authority
|
|
7793
|
+
and is reported in bounded local evidence. Clean immutable source, immediate per-project plans,
|
|
7794
|
+
migration and retention preflights, and secret-free result
|
|
7795
|
+
evidence remain mandatory. Direct SST is forbidden.
|
|
7796
|
+
|
|
7797
|
+
After an interrupted apply, use the same target with \`--action=refresh\` and the exact
|
|
7798
|
+
phase, stage, profile, and role. It retains the same plan-summary review boundary, admits only
|
|
7799
|
+
bounded drift-detected refresh evidence, and never turns refresh into an implicit deploy. When
|
|
7800
|
+
the interrupted write leaves a recorded provider output unusable by the ordinary desired preview,
|
|
7801
|
+
the plan stops without mutation. The non-writing refresh preview separates provider refresh
|
|
7802
|
+
records from its later desired preview and cannot replace failed ordinary desired authority;
|
|
7803
|
+
repair the recorded dependency or use the documented
|
|
7804
|
+
operator recovery path before planning again.
|
|
7805
|
+
|
|
7806
|
+
A stage-wide refresh still plans and authorizes every selected project. When one exact retained
|
|
7807
|
+
project plan proves an absent SST state that is unlocked, complete, empty, and drift-free, the
|
|
7808
|
+
root target records that project as a refresh no-op instead of invoking SST against a nonexistent
|
|
7809
|
+
stage. It also retains the pinned provider's exact SSM \`ebk:framework-version\` \`tagsAll\`
|
|
7810
|
+
normalization as a semantically inert observation after the same bounded shape
|
|
7811
|
+
repeats following refresh. Value changes, other tags, security findings, deletes, replacements,
|
|
7812
|
+
sensitive properties, and every other resource type remain material. Locked, incomplete, failed,
|
|
7813
|
+
or otherwise drifted plans are never skipped.
|
|
7814
|
+
|
|
7815
|
+
For a stage with no workload-role account baseline, publish the fixed Foundation-only candidate
|
|
7816
|
+
and invoke this root target with \`--phase=foundation-bootstrap\`. After Foundation outputs exist,
|
|
7817
|
+
publish the complete \`full-stage\` candidate and invoke the target without \`--phase\` (or with
|
|
7818
|
+
\`--phase=full-stage\`). The bootstrap phase selects exactly Foundation; it is not an arbitrary
|
|
7819
|
+
project filter.
|
|
7820
|
+
|
|
7465
7821
|
Validation requires both an explicit production approval mode and an explicit Foundation
|
|
7466
7822
|
profile. It does not infer omitted values. Use \`promotion-pr\` for a private GitHub Team
|
|
7467
7823
|
repository. Use \`environment-reviewer\` only when the repository plan can enforce a separate
|
|
@@ -7475,10 +7831,16 @@ environment reviewer.
|
|
|
7475
7831
|
|
|
7476
7832
|
Protected production deploy and refresh validate the complete Foundation/Slice set before AWS
|
|
7477
7833
|
access. The Foundation is always ordered first. The set is derived from committed policy and
|
|
7478
|
-
cannot be narrowed with an ad hoc project argument.
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7834
|
+
cannot be narrowed with an ad hoc project argument. The retained stage preview remains review
|
|
7835
|
+
evidence. Immediately before each project mutation the workflow computes one fresh plan against
|
|
7836
|
+
current state and applies it without a second redundant preview. Source, artifacts, configuration,
|
|
7837
|
+
policy, environment, and protected identities remain exact. Provider-only read/discard observations
|
|
7838
|
+
may appear or disappear, but any new delete, replacement, import, stateful or security change, IAM
|
|
7839
|
+
expansion, protection reduction, or unclassified resource stops for renewed approval. After an
|
|
7840
|
+
upstream mutation, every downstream project therefore receives a fresh independent risk decision.
|
|
7841
|
+
|
|
7842
|
+
\`standard\` keeps Aurora warm at 2–16 ACU. \`cost-optimized\` keeps a smaller
|
|
7843
|
+
0.5–4 ACU warm floor for starter production traffic. Both keep managed NAT,
|
|
7482
7844
|
30-day backups, deletion protection, retained data, and the protected GitHub/OIDC/AWS path.
|
|
7483
7845
|
Changing policy affects only later lifecycle requests; it never deploys or removes resources.
|
|
7484
7846
|
Changing profile requires a separate reviewed Foundation deployment and retained operational
|
|
@@ -7549,7 +7911,7 @@ deploy or refresh before AWS access. Protected remove and unlock remain usable f
|
|
|
7549
7911
|
There is no local or silent bypass in this prerelease contract; any future waiver mechanism
|
|
7550
7912
|
must be committed, reviewable, and auditable.
|
|
7551
7913
|
`,
|
|
7552
|
-
'ebk.repository-policy.json': json(
|
|
7914
|
+
'ebk.repository-policy.json': json(initialRepositoryPolicy),
|
|
7553
7915
|
'nx.json': json({
|
|
7554
7916
|
$schema: './node_modules/nx/schemas/nx-schema.json',
|
|
7555
7917
|
defaultBase: 'develop',
|
|
@@ -7589,6 +7951,7 @@ must be committed, reviewable, and auditable.
|
|
|
7589
7951
|
inputs: ['default', '^production'],
|
|
7590
7952
|
},
|
|
7591
7953
|
},
|
|
7954
|
+
analytics: false,
|
|
7592
7955
|
neverConnectToCloud: true,
|
|
7593
7956
|
useDaemonProcess: false,
|
|
7594
7957
|
}),
|
|
@@ -7617,10 +7980,12 @@ must be committed, reviewable, and auditable.
|
|
|
7617
7980
|
},
|
|
7618
7981
|
devDependencies: {
|
|
7619
7982
|
'@aws-sdk/client-iam': TOOL_VERSIONS.awsSdk,
|
|
7983
|
+
'@aws-sdk/client-rds': TOOL_VERSIONS.awsSdk,
|
|
7620
7984
|
'@aws-sdk/client-s3': TOOL_VERSIONS.awsSdk,
|
|
7621
7985
|
'@aws-sdk/client-secrets-manager': TOOL_VERSIONS.awsSdk,
|
|
7622
7986
|
'@aws-sdk/client-ssm': TOOL_VERSIONS.awsSdk,
|
|
7623
7987
|
'@aws-sdk/client-sts': TOOL_VERSIONS.awsSdk,
|
|
7988
|
+
'@aws-sdk/credential-providers': TOOL_VERSIONS.awsSdk,
|
|
7624
7989
|
'@eslint/js': TOOL_VERSIONS.eslintJs,
|
|
7625
7990
|
'@empire-builder-kit/nx': frameworkVersion,
|
|
7626
7991
|
'@types/node': TOOL_VERSIONS.nodeTypes,
|
|
@@ -7704,6 +8069,13 @@ must be committed, reviewable, and auditable.
|
|
|
7704
8069
|
executor: 'nx:run-commands',
|
|
7705
8070
|
options: { command: 'node tools/install-ebk-hooks.mjs' },
|
|
7706
8071
|
},
|
|
8072
|
+
'deploy-local': {
|
|
8073
|
+
cache: false,
|
|
8074
|
+
executor: '@empire-builder-kit/nx:local-deploy',
|
|
8075
|
+
options: {
|
|
8076
|
+
stage: 'development',
|
|
8077
|
+
},
|
|
8078
|
+
},
|
|
7707
8079
|
'supply-chain-check': {
|
|
7708
8080
|
executor: 'nx:run-commands',
|
|
7709
8081
|
options: {
|
|
@@ -7772,6 +8144,10 @@ Workspace creation does not deploy this project. Review \`docs/costs.md\`,
|
|
|
7772
8144
|
|
|
7773
8145
|
Set \`EBK_ROOT_DOMAIN\` before deploying when Route 53 in the target account owns a public
|
|
7774
8146
|
domain. Without it, the shared Router uses its functional provider URL.
|
|
8147
|
+
|
|
8148
|
+
Before deploying development or staging, add at least one exact operator/probe CIDR to
|
|
8149
|
+
\`security/router-waf.json\`. Production is public by default behind the same managed and
|
|
8150
|
+
rate-limiting baseline; select its allowlist mode explicitly when required.
|
|
7775
8151
|
`,
|
|
7776
8152
|
'packages/foundation/contracts/foundation.json': json({
|
|
7777
8153
|
contractVersion: '1.0.0',
|
|
@@ -7831,8 +8207,8 @@ seeded decision record aligned with organization-specific budgets and alerts.
|
|
|
7831
8207
|
- Development uses EC2 NAT, a development bastion, and Aurora scale-to-zero.
|
|
7832
8208
|
- Staging uses EC2 NAT and Aurora scale-to-zero by default.
|
|
7833
8209
|
- Production always uses managed NAT and retains protected data. Its explicit Repository
|
|
7834
|
-
Operating Policy profile selects either the standard
|
|
7835
|
-
|
|
8210
|
+
Operating Policy profile selects either the standard 2-ACU Aurora floor or the
|
|
8211
|
+
cost-optimized 0.5-ACU warm floor.
|
|
7836
8212
|
- ECS, EventBridge, Cognito, SNS, SSM, and CloudWatch are primarily usage-priced; logs,
|
|
7837
8213
|
metrics, alarms, and retained data still require active cost review.
|
|
7838
8214
|
|
|
@@ -7850,8 +8226,69 @@ Use Nx for every supported lifecycle action:
|
|
|
7850
8226
|
- \`pnpm nx run foundation:unlock --stage=development\`
|
|
7851
8227
|
- \`pnpm nx run foundation:remove --stage=development\`
|
|
7852
8228
|
|
|
7853
|
-
Do not invoke SST lifecycle commands directly.
|
|
7854
|
-
|
|
8229
|
+
Do not invoke SST lifecycle commands directly. The root \`deploy-local\` target is the governed
|
|
8230
|
+
local path for stages admitted by the committed deployment profile. Its fixed
|
|
8231
|
+
\`foundation-bootstrap\` phase is the only local partial-stage path and exists solely for the
|
|
8232
|
+
documented first workload-role publication; isolated protected stages remain on the trusted
|
|
8233
|
+
GitHub path.
|
|
8234
|
+
|
|
8235
|
+
Every local invocation plans the complete selected stage but mutates at most one material
|
|
8236
|
+
project. Type the exact current \`approve:<action>:<stage>:<project>:<plan-digest>\` challenge in a
|
|
8237
|
+
real TTY after reviewing the displayed summary. A human or authorized agent may operate that TTY;
|
|
8238
|
+
unattended input is refused. The acknowledgement creates one ten-minute, one-use, private
|
|
8239
|
+
capability bound to source, artifacts, policy, environment, credentials, and current state. A
|
|
8240
|
+
completed deploy or refresh returns the controlled non-success \`replan-required\`; rerun the
|
|
8241
|
+
complete stage, review the new current summary, and continue one project at a time. Only a
|
|
8242
|
+
semantically converged complete stage reports success. No upstream mutation can authorize a
|
|
8243
|
+
downstream project. A complete, unlocked deploy plan containing only provider reads with no
|
|
8244
|
+
changed properties, replacement, failure, or unmatched risk finding is retained as observation
|
|
8245
|
+
evidence and counts as semantically converged. The standalone project planner may still return
|
|
8246
|
+
its conservative non-success for that drift observation; the root local transaction accepts only
|
|
8247
|
+
a freshly emitted exact provider-read-only record from that child. Repeated reads coalesce
|
|
8248
|
+
only when their logical ID and resource type match exactly. Any stale or ambiguous
|
|
8249
|
+
observation fails closed; a plan with desired changes remains material and requires review. A
|
|
8250
|
+
material downstream plan may also retain an upstream Foundation framework-version tag advance as
|
|
8251
|
+
observation only when both external tag
|
|
8252
|
+
projections, the retained identity, optional bounded SST name hydration, and one non-replacing
|
|
8253
|
+
provider read correlate exactly. Every other external property or uncorroborated read remains
|
|
8254
|
+
drift and blocks before the TTY challenge.
|
|
8255
|
+
|
|
8256
|
+
For an interrupted local apply, run the same root target with \`--action=refresh\`, review its
|
|
8257
|
+
current provider-refresh evidence, and type the displayed TTY challenge. Before mutation the
|
|
8258
|
+
executor writes and verifies a private state recovery point plus a secret-free digest/version
|
|
8259
|
+
manifest. The refresh preview validates its provider and later desired-event partitions; any
|
|
8260
|
+
ordinary preview failure or malformed post-refresh desired event remains blocked. Provider-only
|
|
8261
|
+
observation noise is retained without claiming a replayable preview. Because refresh changes
|
|
8262
|
+
recorded state rather than cloud resources, provider-output security and protection findings
|
|
8263
|
+
remain explicit review requirements under the drift decision. Refresh never continues into deploy
|
|
8264
|
+
or claims desired-state convergence: run the governed deploy target afterward and resolve every
|
|
8265
|
+
remaining source-versus-state change. Exclude the \`.private.json\` recovery point from archives.
|
|
8266
|
+
|
|
8267
|
+
The root refresh target includes every selected project in its exact reviewed plan set. It skips
|
|
8268
|
+
the SST child only when that project's retained plan proves an absent, unlocked, complete, empty,
|
|
8269
|
+
and drift-free state, or when the only retained difference is the pinned provider's exact repeated
|
|
8270
|
+
SSM \`ebk:framework-version\` \`tagsAll\` normalization. It records the project as an explicit refresh no-op
|
|
8271
|
+
in either case while retaining the observation. Value changes, other tags, security findings,
|
|
8272
|
+
deletes, replacements, sensitive
|
|
8273
|
+
properties, and every other resource type remain material; every other plan must execute or fail
|
|
8274
|
+
closed.
|
|
8275
|
+
|
|
8276
|
+
## Router WAF
|
|
8277
|
+
|
|
8278
|
+
Each canonical stage owns one global-scope Web ACL attached to that stage's Foundation Router.
|
|
8279
|
+
The committed \`security/router-waf.json\` policy requires AWS Common Rule Set, Known Bad
|
|
8280
|
+
Inputs, Amazon IP Reputation, an IP rate limit, CloudWatch metrics, and sampled requests.
|
|
8281
|
+
Development and staging fail synthesis until their allowlists contain explicit CIDRs.
|
|
8282
|
+
Production is public by default and may be changed to an explicit allowlist.
|
|
8283
|
+
|
|
8284
|
+
For tuning, set a nonproduction stage to \`count\`, deploy and inspect sampled requests, then
|
|
8285
|
+
return staging to \`block\` and run the live allow/deny, managed-rule, rate-limit, metric, and
|
|
8286
|
+
sampled-request probes. Write their bounded, secret-free result to
|
|
8287
|
+
\`security/router-waf-live-evidence.json\`, then run
|
|
8288
|
+
\`pnpm nx run foundation:router-waf-rehearsal\`. The executor seals the exact policy digest
|
|
8289
|
+
into \`router-waf-blocking-rehearsal.json\`. Production synthesis rejects a missing, stale, or
|
|
8290
|
+
count-mode rehearsal. Full request logging is opt-in, blocked-only, retention-bounded, and
|
|
8291
|
+
redacts query strings plus credential-bearing headers.
|
|
7855
8292
|
|
|
7856
8293
|
Foundation stages are canonical. \`ebk dev\` derives a Product Slice personal
|
|
7857
8294
|
stage from the operating-system account and maps that consumer to the
|
|
@@ -7886,6 +8323,12 @@ connects as the Slice's no-password IAM-authenticated migrator user.
|
|
|
7886
8323
|
stage-scoped SSM/secret path and least-privilege IAM.
|
|
7887
8324
|
- Public domains are configured explicitly through \`EBK_ROOT_DOMAIN\`; generated source never
|
|
7888
8325
|
contains credentials or customer domain assumptions.
|
|
8326
|
+
- Every stage's CloudFront Router has its own global Web ACL. The baseline always includes AWS
|
|
8327
|
+
Common Rule Set, Known Bad Inputs, Amazon IP Reputation, rate blocking, CloudWatch metrics,
|
|
8328
|
+
and sampled requests. Nonproduction is allowlisted by default. Production is public by
|
|
8329
|
+
default, but cannot deploy until the exact policy has passed a staging blocking rehearsal.
|
|
8330
|
+
- Full WAF request logging is disabled by default. Enabling it keeps blocked requests only,
|
|
8331
|
+
uses bounded retention, and redacts query strings, cookies, authorization, and API keys.
|
|
7889
8332
|
|
|
7890
8333
|
The managed \`contracts/frontend-security-headers.json\` file owns the transport
|
|
7891
8334
|
minimum: HTTPS in deployed environments, one-year HSTS there, and the common
|
|
@@ -7918,7 +8361,12 @@ declare const $app: {
|
|
|
7918
8361
|
import {
|
|
7919
8362
|
FOUNDATION_SSM_SCHEMA_VERSION,
|
|
7920
8363
|
STANDARD_FOUNDATION_CAPABILITIES,
|
|
8364
|
+
createRouterWafRules,
|
|
8365
|
+
createRouterWafStagePlan,
|
|
8366
|
+
foundationDatabaseEndpointHost,
|
|
7921
8367
|
foundationSsmPath,
|
|
8368
|
+
normalizeRouterWafPolicy,
|
|
8369
|
+
reconcileRouterWafBlockingRehearsal,
|
|
7922
8370
|
type FoundationSsmKey,
|
|
7923
8371
|
} from '@empire-builder-kit/nx/foundation';
|
|
7924
8372
|
import {
|
|
@@ -7928,6 +8376,9 @@ import {
|
|
|
7928
8376
|
} from '@empire-builder-kit/nx/repository-policy';
|
|
7929
8377
|
import { createFoundationAccountCostControlPlan } from '@empire-builder-kit/nx/performance';
|
|
7930
8378
|
import accountCostPolicy from '../performance/account-cost-policy.json';
|
|
8379
|
+
import routerWafBlockingRehearsal from '../security/router-waf-blocking-rehearsal.json';
|
|
8380
|
+
import routerWafLiveEvidence from '../security/router-waf-live-evidence.json';
|
|
8381
|
+
import routerWafPolicy from '../security/router-waf.json';
|
|
7931
8382
|
import type { FoundationWorkloadRolePreflight } from './workload-roles/preflight.js';
|
|
7932
8383
|
|
|
7933
8384
|
function identityDomainPrefix(stage: string, accountId: string): string {
|
|
@@ -7979,6 +8430,18 @@ export function foundation(
|
|
|
7979
8430
|
)
|
|
7980
8431
|
: undefined;
|
|
7981
8432
|
const natMode = productionConfiguration?.contract.nat ?? 'ec2';
|
|
8433
|
+
const normalizedRouterWafPolicy = normalizeRouterWafPolicy(routerWafPolicy);
|
|
8434
|
+
const wafPlan = createRouterWafStagePlan({
|
|
8435
|
+
policy: normalizedRouterWafPolicy,
|
|
8436
|
+
rehearsal: isProduction
|
|
8437
|
+
? reconcileRouterWafBlockingRehearsal({
|
|
8438
|
+
evidence: routerWafLiveEvidence,
|
|
8439
|
+
policy: normalizedRouterWafPolicy,
|
|
8440
|
+
receipt: routerWafBlockingRehearsal,
|
|
8441
|
+
})
|
|
8442
|
+
: null,
|
|
8443
|
+
stage: $app.stage,
|
|
8444
|
+
});
|
|
7982
8445
|
const natBinding = workloadRoles?.bindings.nat;
|
|
7983
8446
|
if (natMode === 'ec2' && natBinding) {
|
|
7984
8447
|
$transform(
|
|
@@ -7988,6 +8451,9 @@ export function foundation(
|
|
|
7988
8451
|
args.name = natBinding.instanceProfileName;
|
|
7989
8452
|
args.role = natBinding.roleName;
|
|
7990
8453
|
opts.import = natBinding.instanceProfileName;
|
|
8454
|
+
opts.ignoreChanges = [
|
|
8455
|
+
...new Set([...(opts.ignoreChanges ?? []), 'tags', 'tagsAll']),
|
|
8456
|
+
];
|
|
7991
8457
|
},
|
|
7992
8458
|
);
|
|
7993
8459
|
}
|
|
@@ -8009,8 +8475,83 @@ export function foundation(
|
|
|
8009
8475
|
aliases: ['*.' + rootDomain],
|
|
8010
8476
|
dns: sst.aws.dns(),
|
|
8011
8477
|
},
|
|
8012
|
-
|
|
8478
|
+
}
|
|
8013
8479
|
: {}),
|
|
8480
|
+
waf: {
|
|
8481
|
+
logging: wafPlan.logging,
|
|
8482
|
+
managedRules: {
|
|
8483
|
+
coreRuleSet: true,
|
|
8484
|
+
knownBadInputs: true,
|
|
8485
|
+
sqlInjection: false,
|
|
8486
|
+
},
|
|
8487
|
+
rateLimitPerIp: wafPlan.rateLimitPerIp,
|
|
8488
|
+
},
|
|
8489
|
+
transform: {
|
|
8490
|
+
cdn(args: any) {
|
|
8491
|
+
if (!args.webAclArn) {
|
|
8492
|
+
throw new Error(
|
|
8493
|
+
'Foundation Router must attach its exact global Web ACL.',
|
|
8494
|
+
);
|
|
8495
|
+
}
|
|
8496
|
+
},
|
|
8497
|
+
waf(args: any, opts: any) {
|
|
8498
|
+
if (args.scope !== 'CLOUDFRONT') {
|
|
8499
|
+
throw new Error(
|
|
8500
|
+
'Foundation Router Web ACL must use global CloudFront scope.',
|
|
8501
|
+
);
|
|
8502
|
+
}
|
|
8503
|
+
const ipSets: { ipv4Arn?: any; ipv6Arn?: any } = {};
|
|
8504
|
+
if (wafPlan.ipv4Cidrs.length > 0) {
|
|
8505
|
+
ipSets.ipv4Arn = new aws.wafv2.IpSet(
|
|
8506
|
+
'FoundationRouterIpv4Allowlist',
|
|
8507
|
+
{
|
|
8508
|
+
addresses: wafPlan.ipv4Cidrs,
|
|
8509
|
+
ipAddressVersion: 'IPV4',
|
|
8510
|
+
name: 'foundation-' + wafPlan.stage + '-router-ipv4-allowlist',
|
|
8511
|
+
scope: 'CLOUDFRONT',
|
|
8512
|
+
tags: {
|
|
8513
|
+
'ebk:policy-digest': wafPlan.policyDigest,
|
|
8514
|
+
'ebk:stage': wafPlan.stage,
|
|
8515
|
+
},
|
|
8516
|
+
},
|
|
8517
|
+
{ provider: opts.provider },
|
|
8518
|
+
).arn;
|
|
8519
|
+
}
|
|
8520
|
+
if (wafPlan.ipv6Cidrs.length > 0) {
|
|
8521
|
+
ipSets.ipv6Arn = new aws.wafv2.IpSet(
|
|
8522
|
+
'FoundationRouterIpv6Allowlist',
|
|
8523
|
+
{
|
|
8524
|
+
addresses: wafPlan.ipv6Cidrs,
|
|
8525
|
+
ipAddressVersion: 'IPV6',
|
|
8526
|
+
name: 'foundation-' + wafPlan.stage + '-router-ipv6-allowlist',
|
|
8527
|
+
scope: 'CLOUDFRONT',
|
|
8528
|
+
tags: {
|
|
8529
|
+
'ebk:policy-digest': wafPlan.policyDigest,
|
|
8530
|
+
'ebk:stage': wafPlan.stage,
|
|
8531
|
+
},
|
|
8532
|
+
},
|
|
8533
|
+
{ provider: opts.provider },
|
|
8534
|
+
).arn;
|
|
8535
|
+
}
|
|
8536
|
+
args.defaultAction = { allow: {} };
|
|
8537
|
+
args.rules = createRouterWafRules(wafPlan, ipSets);
|
|
8538
|
+
args.scope = 'CLOUDFRONT';
|
|
8539
|
+
args.tags = {
|
|
8540
|
+
...(args.tags ?? {}),
|
|
8541
|
+
'ebk:policy-digest': wafPlan.policyDigest,
|
|
8542
|
+
'ebk:stage': wafPlan.stage,
|
|
8543
|
+
};
|
|
8544
|
+
args.visibilityConfig = {
|
|
8545
|
+
cloudwatchMetricsEnabled: true,
|
|
8546
|
+
metricName:
|
|
8547
|
+
'Ebk' +
|
|
8548
|
+
wafPlan.stage[0].toUpperCase() +
|
|
8549
|
+
wafPlan.stage.slice(1) +
|
|
8550
|
+
'FoundationRouterWaf',
|
|
8551
|
+
sampledRequestsEnabled: true,
|
|
8552
|
+
};
|
|
8553
|
+
},
|
|
8554
|
+
},
|
|
8014
8555
|
});
|
|
8015
8556
|
const database = new sst.aws.Aurora('FoundationDatabase', {
|
|
8016
8557
|
// SQL bootstrap and migrations run inside the VPC. Keep the public HTTPS
|
|
@@ -8034,6 +8575,46 @@ export function foundation(
|
|
|
8034
8575
|
},
|
|
8035
8576
|
vpc,
|
|
8036
8577
|
});
|
|
8578
|
+
const databaseClusterEndpoint = database.nodes.cluster.endpoint.apply(
|
|
8579
|
+
foundationDatabaseEndpointHost,
|
|
8580
|
+
);
|
|
8581
|
+
const databaseReaderEndpoint = database.nodes.cluster.readerEndpoint.apply(
|
|
8582
|
+
foundationDatabaseEndpointHost,
|
|
8583
|
+
);
|
|
8584
|
+
// SST v4.17.1 creates a top-level LinkRef on the next tick and its Aurora
|
|
8585
|
+
// implementation unconditionally splits cluster endpoints. During a
|
|
8586
|
+
// governed refresh after an interrupted first deploy, those provider
|
|
8587
|
+
// outputs are legitimately absent. Preserve the normal link contract while
|
|
8588
|
+
// keeping that read-only recovery program total; a real deploy resolves the
|
|
8589
|
+
// exact endpoints before any discovery resource is created.
|
|
8590
|
+
database.getSSTLink = () => ({
|
|
8591
|
+
include: [
|
|
8592
|
+
sst.aws.permission({
|
|
8593
|
+
actions: ['secretsmanager:GetSecretValue'],
|
|
8594
|
+
resources: [database.secretArn],
|
|
8595
|
+
}),
|
|
8596
|
+
sst.aws.permission({
|
|
8597
|
+
actions: [
|
|
8598
|
+
'rds-data:BatchExecuteStatement',
|
|
8599
|
+
'rds-data:BeginTransaction',
|
|
8600
|
+
'rds-data:CommitTransaction',
|
|
8601
|
+
'rds-data:ExecuteStatement',
|
|
8602
|
+
'rds-data:RollbackTransaction',
|
|
8603
|
+
],
|
|
8604
|
+
resources: [database.clusterArn],
|
|
8605
|
+
}),
|
|
8606
|
+
],
|
|
8607
|
+
properties: {
|
|
8608
|
+
clusterArn: database.clusterArn,
|
|
8609
|
+
database: database.database,
|
|
8610
|
+
host: databaseClusterEndpoint,
|
|
8611
|
+
password: database.password,
|
|
8612
|
+
port: database.port,
|
|
8613
|
+
reader: databaseReaderEndpoint,
|
|
8614
|
+
secretArn: database.secretArn,
|
|
8615
|
+
username: database.username,
|
|
8616
|
+
},
|
|
8617
|
+
});
|
|
8037
8618
|
const foundationAccountId = aws.getCallerIdentityOutput({}).accountId;
|
|
8038
8619
|
const foundationPartition = aws.getPartitionOutput({});
|
|
8039
8620
|
const foundationRegion = aws.getRegionOutput({});
|
|
@@ -8096,6 +8677,7 @@ export function foundation(
|
|
|
8096
8677
|
},
|
|
8097
8678
|
],
|
|
8098
8679
|
resources: [alertTopic.arn],
|
|
8680
|
+
sid: 'AllowBudgetAlerts',
|
|
8099
8681
|
},
|
|
8100
8682
|
{
|
|
8101
8683
|
actions: ['sns:Publish'],
|
|
@@ -8109,6 +8691,7 @@ export function foundation(
|
|
|
8109
8691
|
type: 'Service',
|
|
8110
8692
|
}],
|
|
8111
8693
|
resources: [alertTopic.arn],
|
|
8694
|
+
sid: 'AllowCostAnomalyAlerts',
|
|
8112
8695
|
},
|
|
8113
8696
|
{
|
|
8114
8697
|
actions: ['sns:Publish'],
|
|
@@ -8131,6 +8714,7 @@ export function foundation(
|
|
|
8131
8714
|
type: 'Service',
|
|
8132
8715
|
}],
|
|
8133
8716
|
resources: [alertTopic.arn],
|
|
8717
|
+
sid: 'AllowRdsEventAlerts',
|
|
8134
8718
|
},
|
|
8135
8719
|
{
|
|
8136
8720
|
actions: ['sns:Publish'],
|
|
@@ -8155,6 +8739,7 @@ export function foundation(
|
|
|
8155
8739
|
},
|
|
8156
8740
|
],
|
|
8157
8741
|
resources: [alertTopic.arn],
|
|
8742
|
+
sid: 'AllowCloudWatchAlarmAlerts',
|
|
8158
8743
|
},
|
|
8159
8744
|
],
|
|
8160
8745
|
}).json,
|
|
@@ -8183,6 +8768,41 @@ export function foundation(
|
|
|
8183
8768
|
{ dependsOn: [alertTopicPolicy] },
|
|
8184
8769
|
)
|
|
8185
8770
|
: undefined;
|
|
8771
|
+
const accountCostAnomalyMonitor = isProduction
|
|
8772
|
+
? new aws.costexplorer.AnomalyMonitor(
|
|
8773
|
+
'FoundationAccountCostAnomalyMonitor',
|
|
8774
|
+
{
|
|
8775
|
+
monitorDimension: 'SERVICE',
|
|
8776
|
+
monitorType: 'DIMENSIONAL',
|
|
8777
|
+
name: '${name}-account-service-cost-anomalies',
|
|
8778
|
+
tags: {
|
|
8779
|
+
'ebk:cost-policy-digest': accountCostControl.policyDigest,
|
|
8780
|
+
'ebk:cost-scope': '${name}/account/production',
|
|
8781
|
+
'ebk:owner': accountCostControl.owner,
|
|
8782
|
+
},
|
|
8783
|
+
},
|
|
8784
|
+
)
|
|
8785
|
+
: undefined;
|
|
8786
|
+
const accountCostAnomalySubscription =
|
|
8787
|
+
accountCostControl.anomaly && accountCostAnomalyMonitor
|
|
8788
|
+
? new aws.costexplorer.AnomalySubscription(
|
|
8789
|
+
'FoundationAccountCostAnomalySubscription',
|
|
8790
|
+
{
|
|
8791
|
+
frequency: accountCostControl.anomaly.subscription.frequency,
|
|
8792
|
+
monitorArnLists: [accountCostAnomalyMonitor.arn],
|
|
8793
|
+
name: accountCostControl.anomaly.subscription.name,
|
|
8794
|
+
subscribers: [{ address: alertTopic.arn, type: 'SNS' }],
|
|
8795
|
+
tags: {
|
|
8796
|
+
'ebk:cost-policy-digest': accountCostControl.policyDigest,
|
|
8797
|
+
'ebk:cost-scope': '${name}/account/production',
|
|
8798
|
+
'ebk:owner': accountCostControl.owner,
|
|
8799
|
+
},
|
|
8800
|
+
thresholdExpression:
|
|
8801
|
+
accountCostControl.anomaly.subscription.thresholdExpression,
|
|
8802
|
+
},
|
|
8803
|
+
{ dependsOn: [accountCostAnomalyMonitor, alertTopicPolicy] },
|
|
8804
|
+
)
|
|
8805
|
+
: undefined;
|
|
8186
8806
|
const databaseEvents = isProduction
|
|
8187
8807
|
? new aws.rds.EventSubscription(
|
|
8188
8808
|
'FoundationDatabaseEvents',
|
|
@@ -8285,12 +8905,10 @@ export function foundation(
|
|
|
8285
8905
|
clusterId: cluster.id,
|
|
8286
8906
|
contractVersion: '1.0.0',
|
|
8287
8907
|
databaseClusterArn: database.clusterArn,
|
|
8288
|
-
databaseClusterEndpoint
|
|
8289
|
-
(endpoint: string) => endpoint.split(':')[0],
|
|
8290
|
-
),
|
|
8908
|
+
databaseClusterEndpoint,
|
|
8291
8909
|
databaseClusterId: database.id,
|
|
8292
8910
|
databaseClusterResourceId: database.nodes.cluster.clusterResourceId,
|
|
8293
|
-
databaseHost:
|
|
8911
|
+
databaseHost: databaseClusterEndpoint,
|
|
8294
8912
|
databaseMasterSecretArn: database.secretArn,
|
|
8295
8913
|
databasePort: database.port.apply(String),
|
|
8296
8914
|
eventBusArn: eventBus.arn,
|
|
@@ -8312,6 +8930,7 @@ export function foundation(
|
|
|
8312
8930
|
capabilities: STANDARD_FOUNDATION_CAPABILITIES,
|
|
8313
8931
|
clusterId: cluster.id,
|
|
8314
8932
|
contractVersion: '1.0.0',
|
|
8933
|
+
costAnomalySubscriptionArn: accountCostAnomalySubscription?.arn,
|
|
8315
8934
|
databaseCapacityAlarmName: databaseCapacityAlarm.name,
|
|
8316
8935
|
dashboardName: dashboard.dashboardName,
|
|
8317
8936
|
databaseClusterId: database.id,
|
|
@@ -8343,13 +8962,49 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
|
|
|
8343
8962
|
actualNotificationThresholdPercent: 80,
|
|
8344
8963
|
forecastNotificationThresholdPercent: 100,
|
|
8345
8964
|
kind: 'ebk-foundation-account-cost-policy',
|
|
8346
|
-
monthlyMaximumUsd: 1_000,
|
|
8965
|
+
monthlyMaximumUsd: deploymentProfile === 'starter' ? 250 : 1_000,
|
|
8347
8966
|
owner: 'replace-with-owner',
|
|
8348
8967
|
reviewAt: null,
|
|
8349
8968
|
schemaVersion: 1,
|
|
8350
8969
|
status: 'example',
|
|
8351
8970
|
workspace: name,
|
|
8352
8971
|
}),
|
|
8972
|
+
'packages/foundation/security/router-waf.json': json({
|
|
8973
|
+
kind: 'ebk-router-waf-policy',
|
|
8974
|
+
logging: {
|
|
8975
|
+
enabled: false,
|
|
8976
|
+
retention: '1 week',
|
|
8977
|
+
},
|
|
8978
|
+
rateLimitPerIp: 2_000,
|
|
8979
|
+
schemaVersion: 1,
|
|
8980
|
+
stages: {
|
|
8981
|
+
development: {
|
|
8982
|
+
access: 'allowlist',
|
|
8983
|
+
allowedCidrs: [],
|
|
8984
|
+
mode: 'block',
|
|
8985
|
+
},
|
|
8986
|
+
production: {
|
|
8987
|
+
access: 'public',
|
|
8988
|
+
allowedCidrs: [],
|
|
8989
|
+
mode: 'block',
|
|
8990
|
+
},
|
|
8991
|
+
staging: {
|
|
8992
|
+
access: 'allowlist',
|
|
8993
|
+
allowedCidrs: [],
|
|
8994
|
+
mode: 'block',
|
|
8995
|
+
},
|
|
8996
|
+
},
|
|
8997
|
+
}),
|
|
8998
|
+
'packages/foundation/security/router-waf-blocking-rehearsal.json': json({
|
|
8999
|
+
kind: 'ebk-router-waf-blocking-rehearsal',
|
|
9000
|
+
schemaVersion: 1,
|
|
9001
|
+
status: 'pending',
|
|
9002
|
+
}),
|
|
9003
|
+
'packages/foundation/security/router-waf-live-evidence.json': json({
|
|
9004
|
+
kind: 'ebk-router-waf-live-evidence',
|
|
9005
|
+
schemaVersion: 1,
|
|
9006
|
+
status: 'pending',
|
|
9007
|
+
}),
|
|
8353
9008
|
'packages/foundation/configuration/declarations.json': json({
|
|
8354
9009
|
project: 'foundation',
|
|
8355
9010
|
schemaVersion: 1,
|
|
@@ -8382,29 +9037,96 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
|
|
|
8382
9037
|
version: '4.17.1',
|
|
8383
9038
|
},
|
|
8384
9039
|
policy: {
|
|
9040
|
+
adoptedImports: [
|
|
9041
|
+
{
|
|
9042
|
+
authority: 'workload-role-account-baseline',
|
|
9043
|
+
logicalId: 'FoundationVpcNatInstanceProfile',
|
|
9044
|
+
privateIdentifierTemplate: 'ebk-{stage}-foundation-nat-profile',
|
|
9045
|
+
recoveryRunbookRef: 'docs/workload-role-account-baseline.md',
|
|
9046
|
+
removalBehavior: 'retain',
|
|
9047
|
+
resourceType: 'aws:iam/instanceProfile:InstanceProfile',
|
|
9048
|
+
},
|
|
9049
|
+
],
|
|
8385
9050
|
additiveLowRiskTypes: [
|
|
8386
9051
|
'aws:cloudwatch/dashboard:Dashboard',
|
|
8387
9052
|
'aws:cloudwatch/logGroup:LogGroup',
|
|
8388
9053
|
'aws:cloudwatch/metricAlarm:MetricAlarm',
|
|
8389
9054
|
],
|
|
9055
|
+
additiveReviewedTypes: [
|
|
9056
|
+
'aws:cloudwatch/eventArchive:EventArchive',
|
|
9057
|
+
'aws:cloudwatch/eventBus:EventBus',
|
|
9058
|
+
'aws:cognito/userPoolDomain:UserPoolDomain',
|
|
9059
|
+
'aws:ec2/defaultSecurityGroup:DefaultSecurityGroup',
|
|
9060
|
+
'aws:ec2/internetGateway:InternetGateway',
|
|
9061
|
+
'aws:ec2/routeTable:RouteTable',
|
|
9062
|
+
'aws:ec2/routeTableAssociation:RouteTableAssociation',
|
|
9063
|
+
'aws:ec2/securityGroup:SecurityGroup',
|
|
9064
|
+
'aws:ec2/subnet:Subnet',
|
|
9065
|
+
'aws:ec2/vpc:Vpc',
|
|
9066
|
+
'aws:ecs/cluster:Cluster',
|
|
9067
|
+
'aws:ecs/clusterCapacityProviders:ClusterCapacityProviders',
|
|
9068
|
+
'aws:rds/clusterParameterGroup:ClusterParameterGroup',
|
|
9069
|
+
'aws:rds/eventSubscription:EventSubscription',
|
|
9070
|
+
'aws:rds/parameterGroup:ParameterGroup',
|
|
9071
|
+
'aws:rds/subnetGroup:SubnetGroup',
|
|
9072
|
+
'aws:servicediscovery/privateDnsNamespace:PrivateDnsNamespace',
|
|
9073
|
+
'aws:sns/topic:Topic',
|
|
9074
|
+
'aws:sns/topicPolicy:TopicPolicy',
|
|
9075
|
+
'aws:wafv2/webAclLoggingConfiguration:WebAclLoggingConfiguration',
|
|
9076
|
+
'pulumi-nodejs:dynamic:Resource',
|
|
9077
|
+
'pulumi:providers:aws',
|
|
9078
|
+
'pulumi:providers:pulumi-nodejs',
|
|
9079
|
+
'pulumi:providers:random',
|
|
9080
|
+
'pulumi:pulumi:Stack',
|
|
9081
|
+
'sst:aws:Aurora',
|
|
9082
|
+
'sst:aws:Bus',
|
|
9083
|
+
'sst:aws:CDN',
|
|
9084
|
+
'sst:aws:Cluster',
|
|
9085
|
+
'sst:aws:CognitoUserPool',
|
|
9086
|
+
'sst:aws:Router',
|
|
9087
|
+
'sst:aws:SnsTopic',
|
|
9088
|
+
'sst:aws:Vpc',
|
|
9089
|
+
'sst:sst:LinkRef',
|
|
9090
|
+
'sst:sst:Version',
|
|
9091
|
+
],
|
|
8390
9092
|
costOrQuotaTypes: [
|
|
8391
9093
|
'aws:apigatewayv2/api:Api',
|
|
8392
9094
|
'aws:budgets/budget:Budget',
|
|
9095
|
+
'aws:cloudfront/cachePolicy:CachePolicy',
|
|
8393
9096
|
'aws:cloudfront/distribution:Distribution',
|
|
9097
|
+
'aws:cloudfront/function:Function',
|
|
9098
|
+
'aws:cloudfront/keyValueStore:KeyValueStore',
|
|
9099
|
+
'aws:costexplorer/anomalyMonitor:AnomalyMonitor',
|
|
9100
|
+
'aws:costexplorer/anomalySubscription:AnomalySubscription',
|
|
8394
9101
|
'aws:cognito/userPool:UserPool',
|
|
9102
|
+
'aws:ec2/eip:Eip',
|
|
9103
|
+
'aws:ec2/eipAssociation:EipAssociation',
|
|
9104
|
+
'aws:ec2/instance:Instance',
|
|
8395
9105
|
'aws:ec2/natGateway:NatGateway',
|
|
8396
9106
|
'aws:rds/cluster:Cluster',
|
|
9107
|
+
'aws:rds/clusterInstance:ClusterInstance',
|
|
9108
|
+
'aws:wafv2/ipSet:IpSet',
|
|
9109
|
+
'aws:wafv2/webAcl:WebAcl',
|
|
8397
9110
|
],
|
|
8398
9111
|
failClosedOnUnknown: true,
|
|
8399
9112
|
protectedPlanAuthority: 'deploy-role-residual',
|
|
8400
9113
|
standingPolicyRiskClasses: ['no-change', 'additive-low-risk'],
|
|
8401
9114
|
statefulTypes: [
|
|
9115
|
+
'aws:cloudwatch/eventArchive:EventArchive',
|
|
9116
|
+
'aws:cloudwatch/eventBus:EventBus',
|
|
8402
9117
|
'aws:cognito/userPool:UserPool',
|
|
8403
9118
|
'aws:dynamodb/table:Table',
|
|
8404
9119
|
'aws:events/eventBus:EventBus',
|
|
8405
9120
|
'aws:kms/key:Key',
|
|
8406
9121
|
'aws:rds/cluster:Cluster',
|
|
8407
9122
|
'aws:s3/bucket:Bucket',
|
|
9123
|
+
'aws:secretsmanager/secret:Secret',
|
|
9124
|
+
'aws:secretsmanager/secretVersion:SecretVersion',
|
|
9125
|
+
'aws:sns/topic:Topic',
|
|
9126
|
+
'aws:ssm/parameter:Parameter',
|
|
9127
|
+
'aws:wafv2/ipSet:IpSet',
|
|
9128
|
+
'aws:wafv2/webAcl:WebAcl',
|
|
9129
|
+
'random:index/randomPassword:RandomPassword',
|
|
8408
9130
|
],
|
|
8409
9131
|
},
|
|
8410
9132
|
project: 'foundation',
|
|
@@ -8502,6 +9224,13 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
|
|
|
8502
9224
|
executor: '@empire-builder-kit/nx:foundation-profile-preflight',
|
|
8503
9225
|
options: { stage: 'development' },
|
|
8504
9226
|
},
|
|
9227
|
+
'router-waf-rehearsal': {
|
|
9228
|
+
cache: false,
|
|
9229
|
+
executor: '@empire-builder-kit/nx:router-waf-rehearsal',
|
|
9230
|
+
outputs: [
|
|
9231
|
+
`{projectRoot}/security/router-waf-blocking-rehearsal.json`,
|
|
9232
|
+
],
|
|
9233
|
+
},
|
|
8505
9234
|
typecheck: {
|
|
8506
9235
|
executor: 'nx:run-commands',
|
|
8507
9236
|
options: {
|
|
@@ -8529,6 +9258,11 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
|
|
|
8529
9258
|
}),
|
|
8530
9259
|
'packages/foundation/scripts/verify-foundation.mjs': `import { readFileSync } from 'node:fs';
|
|
8531
9260
|
|
|
9261
|
+
import {
|
|
9262
|
+
normalizeRouterWafPolicy,
|
|
9263
|
+
reconcileRouterWafBlockingRehearsal,
|
|
9264
|
+
} from '@empire-builder-kit/nx/foundation';
|
|
9265
|
+
|
|
8532
9266
|
const descriptor = JSON.parse(
|
|
8533
9267
|
readFileSync(new URL('../contracts/foundation.json', import.meta.url), 'utf8'),
|
|
8534
9268
|
);
|
|
@@ -8544,6 +9278,33 @@ const frontendSecurityHeaders = JSON.parse(
|
|
|
8544
9278
|
'utf8',
|
|
8545
9279
|
),
|
|
8546
9280
|
);
|
|
9281
|
+
const routerWafPolicy = normalizeRouterWafPolicy(
|
|
9282
|
+
JSON.parse(
|
|
9283
|
+
readFileSync(
|
|
9284
|
+
new URL('../security/router-waf.json', import.meta.url),
|
|
9285
|
+
'utf8',
|
|
9286
|
+
),
|
|
9287
|
+
),
|
|
9288
|
+
);
|
|
9289
|
+
reconcileRouterWafBlockingRehearsal({
|
|
9290
|
+
evidence: JSON.parse(
|
|
9291
|
+
readFileSync(
|
|
9292
|
+
new URL('../security/router-waf-live-evidence.json', import.meta.url),
|
|
9293
|
+
'utf8',
|
|
9294
|
+
),
|
|
9295
|
+
),
|
|
9296
|
+
policy: routerWafPolicy,
|
|
9297
|
+
receipt:
|
|
9298
|
+
JSON.parse(
|
|
9299
|
+
readFileSync(
|
|
9300
|
+
new URL(
|
|
9301
|
+
'../security/router-waf-blocking-rehearsal.json',
|
|
9302
|
+
import.meta.url,
|
|
9303
|
+
),
|
|
9304
|
+
'utf8',
|
|
9305
|
+
),
|
|
9306
|
+
),
|
|
9307
|
+
});
|
|
8547
9308
|
const expectedCapabilities = [
|
|
8548
9309
|
'network',
|
|
8549
9310
|
'routing',
|
|
@@ -8559,6 +9320,13 @@ const expectedCapabilities = [
|
|
|
8559
9320
|
if (descriptor.contractVersion !== '1.0.0') {
|
|
8560
9321
|
throw new Error('Foundation contractVersion must be 1.0.0.');
|
|
8561
9322
|
}
|
|
9323
|
+
if (
|
|
9324
|
+
routerWafPolicy.stages.development.access !== 'allowlist' ||
|
|
9325
|
+
routerWafPolicy.stages.staging.access !== 'allowlist' ||
|
|
9326
|
+
routerWafPolicy.stages.production.mode !== 'block'
|
|
9327
|
+
) {
|
|
9328
|
+
throw new Error('Foundation Router WAF stage defaults are invalid.');
|
|
9329
|
+
}
|
|
8562
9330
|
if (JSON.stringify(descriptor.capabilities) !== JSON.stringify(expectedCapabilities)) {
|
|
8563
9331
|
throw new Error('Foundation capability descriptor does not match the v1 standard.');
|
|
8564
9332
|
}
|
|
@@ -8567,7 +9335,7 @@ if (
|
|
|
8567
9335
|
costs.aurora?.engineVersion !== '${index_js_4.FOUNDATION_AURORA_ENGINE_VERSION}' ||
|
|
8568
9336
|
costs.productionProfileSource !== 'ebk.repository-policy.json' ||
|
|
8569
9337
|
costs.productionProfiles?.standard?.aurora?.min !== '2 ACU' ||
|
|
8570
|
-
costs.productionProfiles?.['cost-optimized']?.aurora?.min !== '0 ACU' ||
|
|
9338
|
+
costs.productionProfiles?.['cost-optimized']?.aurora?.min !== '0.5 ACU' ||
|
|
8571
9339
|
!costs.stages?.development ||
|
|
8572
9340
|
!costs.stages?.staging ||
|
|
8573
9341
|
costs.stages?.production?.dataRetention !== 'retained-and-protected'
|
|
@@ -8623,16 +9391,15 @@ export default $config({
|
|
|
8623
9391
|
providers: {
|
|
8624
9392
|
aws: {
|
|
8625
9393
|
defaultTags: {
|
|
9394
|
+
// SST contributes sst:app and sst:stage. Keep the provider baseline
|
|
9395
|
+
// at eight tags so generated S3 asset objects remain within AWS's
|
|
9396
|
+
// hard ten-tag object limit. Richer performance tags stay explicit.
|
|
8626
9397
|
tags: {
|
|
8627
9398
|
'ebk:cost-allocation': 'foundation',
|
|
8628
9399
|
'ebk:cost-scope': '${name}/foundation/' + stage,
|
|
8629
9400
|
'ebk:framework-version': '${frameworkVersion}',
|
|
8630
9401
|
'ebk:managed-by': 'empire-builder-kit',
|
|
8631
9402
|
'ebk:owner': 'foundation',
|
|
8632
|
-
'ebk:production-profile':
|
|
8633
|
-
process.env.EBK_PRODUCTION_FOUNDATION_PROFILE ?? 'standard',
|
|
8634
|
-
'ebk:repository':
|
|
8635
|
-
process.env.GITHUB_REPOSITORY ?? '${name}',
|
|
8636
9403
|
'ebk:slice': 'foundation',
|
|
8637
9404
|
'ebk:stage': stage,
|
|
8638
9405
|
'ebk:workspace': '${name}',
|