@empire-builder-kit/nx 1.0.0-rc.13 → 1.0.0-rc.17
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 +53 -8
- 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 +5 -1
- package/src/generators/preset/workspace-files.js +788 -49
- 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 +14 -2
- 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
|
@@ -709,6 +709,19 @@ function planRoleAuditStep(branch, mode = 'produce') {
|
|
|
709
709
|
],
|
|
710
710
|
Resource: '*',
|
|
711
711
|
},
|
|
712
|
+
{
|
|
713
|
+
Sid: 'ObserveStageSecretVersionIdentity',
|
|
714
|
+
Effect: 'Allow',
|
|
715
|
+
Action: 'secretsmanager:ListSecretVersionIds',
|
|
716
|
+
Resource:
|
|
717
|
+
'arn:aws:secretsmanager:' + process.env.AWS_REGION + ':' +
|
|
718
|
+
account + ':secret:*',
|
|
719
|
+
Condition: {
|
|
720
|
+
StringEquals: {
|
|
721
|
+
'aws:ResourceTag/ebk:stage': stateStage,
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
},
|
|
712
725
|
{
|
|
713
726
|
Sid: 'ValidatePlannedIamPolicies',
|
|
714
727
|
Effect: 'Allow',
|
|
@@ -969,6 +982,7 @@ exports.WORKSPACE_PROJECT_MANAGED_JSON_POINTERS = [
|
|
|
969
982
|
'/targets/blueprint-trust-check-protected',
|
|
970
983
|
'/targets/developer-context',
|
|
971
984
|
'/targets/deployment-plan',
|
|
985
|
+
'/targets/deploy-local',
|
|
972
986
|
'/targets/dev',
|
|
973
987
|
'/targets/dev-doctor',
|
|
974
988
|
'/targets/dev-setup',
|
|
@@ -989,6 +1003,10 @@ const FOUNDATION_SEEDED_PATHS = new Set([
|
|
|
989
1003
|
'docs/costs.md',
|
|
990
1004
|
'docs/runbooks.md',
|
|
991
1005
|
'docs/security.md',
|
|
1006
|
+
'performance/account-cost-policy.json',
|
|
1007
|
+
'security/router-waf-blocking-rehearsal.json',
|
|
1008
|
+
'security/router-waf-live-evidence.json',
|
|
1009
|
+
'security/router-waf.json',
|
|
992
1010
|
]);
|
|
993
1011
|
function createFoundationOwnership(files, frameworkVersion) {
|
|
994
1012
|
const paths = Object.keys(files)
|
|
@@ -1051,7 +1069,16 @@ function createWorkspaceOwnership(files, frameworkVersion) {
|
|
|
1051
1069
|
}
|
|
1052
1070
|
function renderWorkspaceFiles(options) {
|
|
1053
1071
|
const { frameworkVersion, name } = options;
|
|
1072
|
+
const deploymentProfile = options.deploymentProfile ?? 'starter';
|
|
1073
|
+
const initialRepositoryPolicy = (0, index_js_4.createIncubationRepositoryPolicy)(deploymentProfile);
|
|
1074
|
+
initialRepositoryPolicy.deploymentAccounts = options.deploymentAccounts ?? {
|
|
1075
|
+
development: null,
|
|
1076
|
+
production: null,
|
|
1077
|
+
staging: null,
|
|
1078
|
+
};
|
|
1079
|
+
initialRepositoryPolicy.deploymentRegion = options.deploymentRegion ?? null;
|
|
1054
1080
|
const developerAgentFilePlan = (0, workspace_files_developer_agent_js_1.renderDeveloperAgentWorkspaceFilePlan)({
|
|
1081
|
+
deploymentProfile,
|
|
1055
1082
|
frameworkVersion,
|
|
1056
1083
|
name,
|
|
1057
1084
|
});
|
|
@@ -1064,6 +1091,9 @@ function renderWorkspaceFiles(options) {
|
|
|
1064
1091
|
const uploadArtifactAction = (0, index_js_3.pinnedAction)(index_js_3.GITHUB_ACTION_PINS.uploadArtifact);
|
|
1065
1092
|
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
1093
|
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'";
|
|
1094
|
+
const protectedPush = "github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')";
|
|
1095
|
+
const protectedDeploymentRouteJobName = 'protected deployment route';
|
|
1096
|
+
const stagingApplyJobPrefix = 'apply staging plan ';
|
|
1067
1097
|
const files = {
|
|
1068
1098
|
...(0, workspace_files_ci_acceptance_js_1.renderWorkspaceCiAcceptanceFiles)(),
|
|
1069
1099
|
'.node-version': `${tool_versions_js_1.WORKSPACE_NODE_VERSION}\n`,
|
|
@@ -1095,24 +1125,26 @@ env:
|
|
|
1095
1125
|
jobs:
|
|
1096
1126
|
acceptance:
|
|
1097
1127
|
name: accepted merge evidence
|
|
1098
|
-
if:
|
|
1128
|
+
if: ${protectedPush}
|
|
1099
1129
|
runs-on: ubuntu-latest
|
|
1100
1130
|
outputs:
|
|
1131
|
+
history_only: \${{ steps.resolve.outputs.history_only == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' }}
|
|
1101
1132
|
reused: \${{ steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' }}
|
|
1133
|
+
route: \${{ steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' && steps.resolve.outputs.route || '' }}
|
|
1102
1134
|
steps:
|
|
1103
1135
|
- name: Check out immutable merged candidate
|
|
1104
|
-
if:
|
|
1136
|
+
if: ${protectedPush}
|
|
1105
1137
|
uses: ${checkoutAction}
|
|
1106
1138
|
with:
|
|
1107
1139
|
fetch-depth: 0
|
|
1108
1140
|
ref: \${{ github.sha }}
|
|
1109
|
-
- name: Resolve exact protected-
|
|
1110
|
-
if:
|
|
1141
|
+
- name: Resolve exact protected-route acceptance
|
|
1142
|
+
if: ${protectedPush}
|
|
1111
1143
|
id: resolve
|
|
1112
1144
|
env:
|
|
1113
1145
|
GH_TOKEN: \${{ github.token }}
|
|
1114
1146
|
run: node tools/ci/workspace-acceptance.mjs resolve
|
|
1115
|
-
- name: Restore exact protected-
|
|
1147
|
+
- name: Restore exact protected-route acceptance
|
|
1116
1148
|
if: steps.resolve.outputs.reused == 'true'
|
|
1117
1149
|
id: restore
|
|
1118
1150
|
continue-on-error: true
|
|
@@ -1132,13 +1164,17 @@ jobs:
|
|
|
1132
1164
|
id: verify
|
|
1133
1165
|
continue-on-error: true
|
|
1134
1166
|
env:
|
|
1167
|
+
EBK_ACCEPTANCE_BASE: \${{ steps.resolve.outputs.base_commit }}
|
|
1135
1168
|
EBK_ACCEPTANCE_HEAD: \${{ steps.resolve.outputs.head_commit }}
|
|
1169
|
+
EBK_ACCEPTANCE_ROUTE: \${{ steps.resolve.outputs.route }}
|
|
1136
1170
|
EBK_ACCEPTANCE_RUN_ATTEMPT: \${{ steps.resolve.outputs.run_attempt }}
|
|
1137
1171
|
EBK_ACCEPTANCE_RUN_ID: \${{ steps.resolve.outputs.run_id }}
|
|
1138
1172
|
run: >-
|
|
1139
1173
|
node tools/ci/workspace-acceptance.mjs verify
|
|
1140
1174
|
.ebk/evidence/reused-acceptance/manifest.json
|
|
1141
1175
|
"$EBK_ACCEPTANCE_HEAD"
|
|
1176
|
+
"$EBK_ACCEPTANCE_BASE"
|
|
1177
|
+
"$EBK_ACCEPTANCE_ROUTE"
|
|
1142
1178
|
"$EBK_ACCEPTANCE_RUN_ID"
|
|
1143
1179
|
"$EBK_ACCEPTANCE_RUN_ATTEMPT"
|
|
1144
1180
|
|
|
@@ -1198,12 +1234,14 @@ jobs:
|
|
|
1198
1234
|
run: pnpm nx run-many -t verify --parallel=3
|
|
1199
1235
|
|
|
1200
1236
|
seal-acceptance:
|
|
1201
|
-
name: seal protected
|
|
1237
|
+
name: seal protected route acceptance
|
|
1202
1238
|
needs: [policy, verify]
|
|
1203
1239
|
if: >-
|
|
1204
1240
|
always() &&
|
|
1241
|
+
needs.policy.result == 'success' &&
|
|
1242
|
+
needs.verify.result == 'success' &&
|
|
1205
1243
|
github.event_name == 'pull_request' &&
|
|
1206
|
-
${protectedDevelopPromotion}
|
|
1244
|
+
(${protectedDevelopPromotion} || ${protectedMainSync})
|
|
1207
1245
|
runs-on: ubuntu-latest
|
|
1208
1246
|
steps:
|
|
1209
1247
|
- name: Check out immutable accepted candidate
|
|
@@ -1211,13 +1249,16 @@ jobs:
|
|
|
1211
1249
|
with:
|
|
1212
1250
|
fetch-depth: 0
|
|
1213
1251
|
ref: \${{ env.EBK_CI_CANDIDATE_SHA }}
|
|
1214
|
-
- name: Seal exact protected-
|
|
1252
|
+
- name: Seal exact protected-route acceptance
|
|
1253
|
+
id: seal
|
|
1215
1254
|
env:
|
|
1255
|
+
EBK_BASE_COMMIT: \${{ github.event.pull_request.base.sha }}
|
|
1216
1256
|
EBK_HEAD_REPOSITORY: \${{ github.event.pull_request.head.repo.full_name }}
|
|
1217
1257
|
run: >-
|
|
1218
1258
|
node tools/ci/workspace-acceptance.mjs seal
|
|
1219
1259
|
.ebk/evidence/ci/workspace-acceptance/manifest.json
|
|
1220
|
-
- name: Retain exact protected-
|
|
1260
|
+
- name: Retain exact protected-route acceptance
|
|
1261
|
+
if: steps.seal.outputs.sealed == 'true'
|
|
1221
1262
|
uses: ${uploadArtifactAction}
|
|
1222
1263
|
with:
|
|
1223
1264
|
name: workspace-acceptance-\${{ env.EBK_CI_CANDIDATE_SHA }}-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
@@ -1232,6 +1273,7 @@ jobs:
|
|
|
1232
1273
|
if: >-
|
|
1233
1274
|
always() &&
|
|
1234
1275
|
github.event_name == 'push' &&
|
|
1276
|
+
needs.acceptance.outputs.history_only != 'true' &&
|
|
1235
1277
|
(needs.acceptance.outputs.reused == 'true' ||
|
|
1236
1278
|
(needs.policy.result == 'success' && needs.verify.result == 'success'))
|
|
1237
1279
|
runs-on: ubuntu-latest
|
|
@@ -1298,7 +1340,97 @@ permissions:
|
|
|
1298
1340
|
contents: read
|
|
1299
1341
|
|
|
1300
1342
|
jobs:
|
|
1343
|
+
history-classification:
|
|
1344
|
+
name: classify protected history-only sync
|
|
1345
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
|
|
1346
|
+
runs-on: ubuntu-latest
|
|
1347
|
+
permissions:
|
|
1348
|
+
actions: read
|
|
1349
|
+
contents: read
|
|
1350
|
+
pull-requests: read
|
|
1351
|
+
outputs:
|
|
1352
|
+
history_only: \${{ steps.resolve.outputs.history_only == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' }}
|
|
1353
|
+
steps:
|
|
1354
|
+
- name: Check out immutable protected candidate
|
|
1355
|
+
uses: ${checkoutAction}
|
|
1356
|
+
with:
|
|
1357
|
+
fetch-depth: 0
|
|
1358
|
+
ref: \${{ github.sha }}
|
|
1359
|
+
- uses: ${setupNodeAction}
|
|
1360
|
+
with:
|
|
1361
|
+
node-version: ${tool_versions_js_1.WORKSPACE_NODE_VERSION}
|
|
1362
|
+
- name: Resolve exact protected-route acceptance
|
|
1363
|
+
id: resolve
|
|
1364
|
+
env:
|
|
1365
|
+
GH_TOKEN: \${{ github.token }}
|
|
1366
|
+
run: node tools/ci/workspace-acceptance.mjs resolve
|
|
1367
|
+
- name: Restore exact protected-route acceptance
|
|
1368
|
+
if: steps.resolve.outputs.reused == 'true'
|
|
1369
|
+
id: restore
|
|
1370
|
+
continue-on-error: true
|
|
1371
|
+
env:
|
|
1372
|
+
EBK_ACCEPTANCE_ARTIFACT_DIGEST: \${{ steps.resolve.outputs.artifact_digest }}
|
|
1373
|
+
EBK_ACCEPTANCE_ARTIFACT_ID: \${{ steps.resolve.outputs.artifact_id }}
|
|
1374
|
+
EBK_ACCEPTANCE_ARTIFACT_SIZE: \${{ steps.resolve.outputs.artifact_size }}
|
|
1375
|
+
GH_TOKEN: \${{ github.token }}
|
|
1376
|
+
run: >-
|
|
1377
|
+
node tools/ci/workspace-acceptance.mjs download
|
|
1378
|
+
.ebk/evidence/reused-history-acceptance
|
|
1379
|
+
"$EBK_ACCEPTANCE_ARTIFACT_ID"
|
|
1380
|
+
"$EBK_ACCEPTANCE_ARTIFACT_DIGEST"
|
|
1381
|
+
"$EBK_ACCEPTANCE_ARTIFACT_SIZE"
|
|
1382
|
+
- name: Verify exact history-only acceptance
|
|
1383
|
+
if: steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success'
|
|
1384
|
+
id: verify
|
|
1385
|
+
continue-on-error: true
|
|
1386
|
+
env:
|
|
1387
|
+
EBK_ACCEPTANCE_BASE: \${{ steps.resolve.outputs.base_commit }}
|
|
1388
|
+
EBK_ACCEPTANCE_HEAD: \${{ steps.resolve.outputs.head_commit }}
|
|
1389
|
+
EBK_ACCEPTANCE_ROUTE: \${{ steps.resolve.outputs.route }}
|
|
1390
|
+
EBK_ACCEPTANCE_RUN_ATTEMPT: \${{ steps.resolve.outputs.run_attempt }}
|
|
1391
|
+
EBK_ACCEPTANCE_RUN_ID: \${{ steps.resolve.outputs.run_id }}
|
|
1392
|
+
run: >-
|
|
1393
|
+
node tools/ci/workspace-acceptance.mjs verify
|
|
1394
|
+
.ebk/evidence/reused-history-acceptance/manifest.json
|
|
1395
|
+
"$EBK_ACCEPTANCE_HEAD"
|
|
1396
|
+
"$EBK_ACCEPTANCE_BASE"
|
|
1397
|
+
"$EBK_ACCEPTANCE_ROUTE"
|
|
1398
|
+
"$EBK_ACCEPTANCE_RUN_ID"
|
|
1399
|
+
"$EBK_ACCEPTANCE_RUN_ATTEMPT"
|
|
1400
|
+
|
|
1401
|
+
deployment-mode:
|
|
1402
|
+
name: classify deployment profile
|
|
1403
|
+
runs-on: ubuntu-latest
|
|
1404
|
+
outputs:
|
|
1405
|
+
profile: \${{ steps.profile.outputs.profile }}
|
|
1406
|
+
steps:
|
|
1407
|
+
- name: Check out immutable deployment policy
|
|
1408
|
+
uses: ${checkoutAction}
|
|
1409
|
+
with:
|
|
1410
|
+
ref: \${{ github.sha }}
|
|
1411
|
+
- id: profile
|
|
1412
|
+
name: Read exact committed deployment profile
|
|
1413
|
+
run: |
|
|
1414
|
+
node - <<'NODE'
|
|
1415
|
+
const { readFileSync } = require('node:fs');
|
|
1416
|
+
const policy = JSON.parse(readFileSync('ebk.repository-policy.json', 'utf8'));
|
|
1417
|
+
const profiles = new Set(['starter', 'production-isolated', 'fully-isolated']);
|
|
1418
|
+
if (!profiles.has(policy.deploymentProfile)) {
|
|
1419
|
+
throw new Error('Committed deployment profile is absent or invalid.');
|
|
1420
|
+
}
|
|
1421
|
+
require('node:fs').appendFileSync(
|
|
1422
|
+
process.env.GITHUB_OUTPUT,
|
|
1423
|
+
'profile=' + policy.deploymentProfile + '\\n',
|
|
1424
|
+
);
|
|
1425
|
+
NODE
|
|
1426
|
+
|
|
1301
1427
|
route:
|
|
1428
|
+
name: ${protectedDeploymentRouteJobName}
|
|
1429
|
+
needs: [history-classification, deployment-mode]
|
|
1430
|
+
if: >-
|
|
1431
|
+
always() &&
|
|
1432
|
+
needs.history-classification.outputs.history_only != 'true' &&
|
|
1433
|
+
needs.deployment-mode.result == 'success'
|
|
1302
1434
|
runs-on: ubuntu-latest
|
|
1303
1435
|
steps:
|
|
1304
1436
|
- name: Validate protected ref
|
|
@@ -1311,14 +1443,22 @@ jobs:
|
|
|
1311
1443
|
esac
|
|
1312
1444
|
|
|
1313
1445
|
plan-staging:
|
|
1314
|
-
needs: route
|
|
1315
|
-
if:
|
|
1446
|
+
needs: [route, deployment-mode]
|
|
1447
|
+
if: >-
|
|
1448
|
+
github.ref == 'refs/heads/develop' &&
|
|
1449
|
+
(github.event_name == 'workflow_dispatch' ||
|
|
1450
|
+
needs.deployment-mode.outputs.profile != 'starter')
|
|
1316
1451
|
runs-on: ubuntu-latest
|
|
1317
1452
|
outputs:
|
|
1453
|
+
deployment_profile: \${{ steps.plan.outputs.deployment_profile }}
|
|
1454
|
+
deployment_region: \${{ steps.plan.outputs.deployment_region }}
|
|
1455
|
+
development_account_id: \${{ steps.plan.outputs.development_account_id }}
|
|
1318
1456
|
foundation_project: \${{ steps.plan.outputs.foundation_project }}
|
|
1319
1457
|
has_slices: \${{ steps.plan.outputs.has_slices }}
|
|
1320
1458
|
ordered_projects: \${{ steps.plan.outputs.ordered_projects }}
|
|
1459
|
+
production_account_id: \${{ steps.plan.outputs.production_account_id }}
|
|
1321
1460
|
slice_projects: \${{ steps.plan.outputs.slice_projects }}
|
|
1461
|
+
staging_account_id: \${{ steps.plan.outputs.staging_account_id }}
|
|
1322
1462
|
steps:
|
|
1323
1463
|
- uses: ${checkoutAction}
|
|
1324
1464
|
with:
|
|
@@ -1529,16 +1669,21 @@ jobs:
|
|
|
1529
1669
|
path: .ebk/evidence/staging-release-source/**
|
|
1530
1670
|
|
|
1531
1671
|
plan-production:
|
|
1532
|
-
needs: route
|
|
1672
|
+
needs: [route, deployment-mode]
|
|
1533
1673
|
if: github.ref == 'refs/heads/main'
|
|
1534
1674
|
runs-on: ubuntu-latest
|
|
1535
1675
|
outputs:
|
|
1676
|
+
deployment_profile: \${{ steps.plan.outputs.deployment_profile }}
|
|
1677
|
+
deployment_region: \${{ steps.plan.outputs.deployment_region }}
|
|
1678
|
+
development_account_id: \${{ steps.plan.outputs.development_account_id }}
|
|
1536
1679
|
foundation_project: \${{ steps.plan.outputs.foundation_project }}
|
|
1537
1680
|
foundation_profile: \${{ steps.plan.outputs.foundation_profile }}
|
|
1538
1681
|
has_slices: \${{ steps.plan.outputs.has_slices }}
|
|
1539
1682
|
ordered_projects: \${{ steps.plan.outputs.ordered_projects }}
|
|
1540
1683
|
production_approval_mode: \${{ steps.plan.outputs.production_approval_mode }}
|
|
1684
|
+
production_account_id: \${{ steps.plan.outputs.production_account_id }}
|
|
1541
1685
|
slice_projects: \${{ steps.plan.outputs.slice_projects }}
|
|
1686
|
+
staging_account_id: \${{ steps.plan.outputs.staging_account_id }}
|
|
1542
1687
|
steps:
|
|
1543
1688
|
- uses: ${checkoutAction}
|
|
1544
1689
|
with:
|
|
@@ -1859,6 +2004,8 @@ jobs:
|
|
|
1859
2004
|
EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
|
|
1860
2005
|
EBK_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
|
|
1861
2006
|
EBK_STAGE: staging
|
|
2007
|
+
EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-staging.outputs.staging_account_id }}
|
|
2008
|
+
EBK_POLICY_REGION: \${{ needs.plan-staging.outputs.deployment_region }}
|
|
1862
2009
|
SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
|
|
1863
2010
|
steps:
|
|
1864
2011
|
- uses: ${checkoutAction}
|
|
@@ -1874,6 +2021,10 @@ jobs:
|
|
|
1874
2021
|
- run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
|
|
1875
2022
|
- name: Verify managed workspace integrity before AWS authority
|
|
1876
2023
|
run: pnpm nx run ${name}:workspace-health
|
|
2024
|
+
- name: Bind staging account to committed deployment topology
|
|
2025
|
+
run: |
|
|
2026
|
+
test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID"
|
|
2027
|
+
test "$AWS_REGION" = "$EBK_POLICY_REGION"
|
|
1877
2028
|
${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - uses: ${configureAwsAction}
|
|
1878
2029
|
with:
|
|
1879
2030
|
role-to-assume: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
|
@@ -1913,6 +2064,8 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - uses: ${c
|
|
|
1913
2064
|
EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
|
|
1914
2065
|
EBK_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
|
|
1915
2066
|
EBK_STAGE: staging
|
|
2067
|
+
EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-staging.outputs.staging_account_id }}
|
|
2068
|
+
EBK_POLICY_REGION: \${{ needs.plan-staging.outputs.deployment_region }}
|
|
1916
2069
|
SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
|
|
1917
2070
|
steps:
|
|
1918
2071
|
- uses: ${checkoutAction}
|
|
@@ -1928,6 +2081,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - uses: ${c
|
|
|
1928
2081
|
- run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
|
|
1929
2082
|
- name: Verify managed workspace integrity before AWS authority
|
|
1930
2083
|
run: pnpm nx run ${name}:workspace-health
|
|
2084
|
+
- name: Bind staging account to committed deployment topology
|
|
2085
|
+
run: |
|
|
2086
|
+
test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID"
|
|
2087
|
+
test "$AWS_REGION" = "$EBK_POLICY_REGION"
|
|
1931
2088
|
${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Restore both native platform records
|
|
1932
2089
|
uses: ${downloadArtifactAction}
|
|
1933
2090
|
with:
|
|
@@ -1969,6 +2126,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
|
|
|
1969
2126
|
AWS_DEPLOY_ROLE_ARN: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
|
1970
2127
|
AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
|
|
1971
2128
|
AWS_REGION: \${{ vars.AWS_REGION }}
|
|
2129
|
+
EBK_DEPLOYMENT_PROFILE: \${{ needs.plan-production.outputs.deployment_profile }}
|
|
2130
|
+
EBK_POLICY_PRODUCTION_ACCOUNT_ID: \${{ needs.plan-production.outputs.production_account_id }}
|
|
2131
|
+
EBK_POLICY_REGION: \${{ needs.plan-production.outputs.deployment_region }}
|
|
2132
|
+
EBK_POLICY_STAGING_ACCOUNT_ID: \${{ needs.plan-production.outputs.staging_account_id }}
|
|
1972
2133
|
EBK_CANDIDATE_RUN_ATTEMPT: \${{ needs.release-source-production.outputs.candidate_run_attempt }}
|
|
1973
2134
|
EBK_CANDIDATE_RUN_ID: \${{ needs.release-source-production.outputs.candidate_run_id }}
|
|
1974
2135
|
EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-production.outputs.source_commit }}
|
|
@@ -2069,11 +2230,13 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
|
|
|
2069
2230
|
cp "$source_sidecar" "$target"
|
|
2070
2231
|
done
|
|
2071
2232
|
- uses: ${configureAwsAction}
|
|
2233
|
+
if: env.EBK_DEPLOYMENT_PROFILE != 'starter'
|
|
2072
2234
|
with:
|
|
2073
2235
|
role-to-assume: \${{ secrets.AWS_STAGING_PROMOTION_PULL_ROLE_ARN }}
|
|
2074
2236
|
role-session-name: ebk-staging-container-pull-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
2075
2237
|
aws-region: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
|
|
2076
2238
|
- name: Authenticate exact staging source registry
|
|
2239
|
+
if: env.EBK_DEPLOYMENT_PROFILE != 'starter'
|
|
2077
2240
|
env:
|
|
2078
2241
|
EBK_STAGING_ACCOUNT_ID: \${{ vars.AWS_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID }}
|
|
2079
2242
|
EBK_STAGING_REGION: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
|
|
@@ -2092,10 +2255,27 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
|
|
|
2092
2255
|
role-to-assume: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
|
2093
2256
|
role-session-name: ebk-production-container-push-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
2094
2257
|
aws-region: \${{ vars.AWS_REGION }}
|
|
2258
|
+
- name: Bind production authority to committed deployment topology
|
|
2259
|
+
run: |
|
|
2260
|
+
test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_PRODUCTION_ACCOUNT_ID" ||
|
|
2261
|
+
{ echo 'Production environment account differs from committed deployment topology'; exit 1; }
|
|
2262
|
+
test "$AWS_REGION" = "$EBK_POLICY_REGION" ||
|
|
2263
|
+
{ echo 'Production environment Region differs from committed deployment topology'; exit 1; }
|
|
2264
|
+
- name: Authenticate shared-account staging source registry
|
|
2265
|
+
if: env.EBK_DEPLOYMENT_PROFILE == 'starter'
|
|
2266
|
+
run: |
|
|
2267
|
+
test "$EBK_POLICY_STAGING_ACCOUNT_ID" = "$EBK_POLICY_PRODUCTION_ACCOUNT_ID" ||
|
|
2268
|
+
{ echo 'Starter promotion requires one shared AWS account'; exit 1; }
|
|
2269
|
+
registry_suffix=.com
|
|
2270
|
+
case "$EBK_POLICY_REGION" in cn-*) registry_suffix=.com.cn ;; esac
|
|
2271
|
+
aws ecr get-login-password --region "$EBK_POLICY_REGION" |
|
|
2272
|
+
docker login --username AWS --password-stdin \
|
|
2273
|
+
"$EBK_POLICY_STAGING_ACCOUNT_ID.dkr.ecr.$EBK_POLICY_REGION.amazonaws$registry_suffix"
|
|
2095
2274
|
- name: Promote exact staging indexes without rebuilding
|
|
2096
2275
|
env:
|
|
2097
|
-
|
|
2098
|
-
|
|
2276
|
+
EBK_DEPLOYMENT_PROFILE: \${{ needs.plan-production.outputs.deployment_profile }}
|
|
2277
|
+
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 }}
|
|
2278
|
+
EBK_STAGING_PROMOTION_REGION: \${{ needs.plan-production.outputs.deployment_profile == 'starter' && needs.plan-production.outputs.deployment_region || vars.AWS_STAGING_PROMOTION_REGION }}
|
|
2099
2279
|
run: |
|
|
2100
2280
|
test "$(aws sts get-caller-identity --query Account --output text)" = "$AWS_EXPECTED_ACCOUNT_ID" ||
|
|
2101
2281
|
{ echo 'Production promotion role account differs from policy'; exit 1; }
|
|
@@ -2314,6 +2494,8 @@ ${planRoleAuditStep('main', 'verify')}
|
|
|
2314
2494
|
EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
|
|
2315
2495
|
EBK_SOURCE_COMMIT: \${{ github.sha }}
|
|
2316
2496
|
EBK_STAGE: staging
|
|
2497
|
+
EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-staging.outputs.staging_account_id }}
|
|
2498
|
+
EBK_POLICY_REGION: \${{ needs.plan-staging.outputs.deployment_region }}
|
|
2317
2499
|
FOUNDATION_PROJECT: \${{ needs.plan-staging.outputs.foundation_project }}
|
|
2318
2500
|
ORDERED_PROJECTS: \${{ needs.plan-staging.outputs.ordered_projects }}
|
|
2319
2501
|
SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
|
|
@@ -2421,7 +2603,7 @@ ${planRoleAuditStep('develop', 'verify')}
|
|
|
2421
2603
|
packages/*/.ebk/evidence/infrastructure-plan-command.json
|
|
2422
2604
|
|
|
2423
2605
|
deploy-staging:
|
|
2424
|
-
name:
|
|
2606
|
+
name: ${stagingApplyJobPrefix}\${{ needs.infrastructure-plan-staging.outputs.plan_digest || 'recovery-without-resource-plan' }}
|
|
2425
2607
|
needs: [plan-staging, release-source-staging, container-assemble-staging, infrastructure-plan-staging]
|
|
2426
2608
|
if: |
|
|
2427
2609
|
always() &&
|
|
@@ -2500,6 +2682,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
|
|
|
2500
2682
|
test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'staging AWS_DEPLOY_ROLE_ARN is required'; exit 1; }
|
|
2501
2683
|
test -n "$AWS_EXPECTED_ACCOUNT_ID" || { echo 'staging AWS_EXPECTED_ACCOUNT_ID is required'; exit 1; }
|
|
2502
2684
|
test -n "$AWS_REGION" || { echo 'staging AWS_REGION is required'; exit 1; }
|
|
2685
|
+
test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ||
|
|
2686
|
+
{ echo 'staging environment account differs from committed deployment topology'; exit 1; }
|
|
2687
|
+
test "$AWS_REGION" = "$EBK_POLICY_REGION" ||
|
|
2688
|
+
{ echo 'staging environment Region differs from committed deployment topology'; exit 1; }
|
|
2503
2689
|
test -n "$EBK_FLEET_ORIGIN" ||
|
|
2504
2690
|
{ echo 'staging environment EBK_FLEET_ORIGIN is required'; exit 1; }
|
|
2505
2691
|
test "$EBK_FLEET_ORIGIN" = "$EBK_PLANNED_FLEET_ORIGIN" ||
|
|
@@ -3136,6 +3322,8 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
|
|
|
3136
3322
|
EBK_APPROVED_MAIN_COMMIT: \${{ github.sha }}
|
|
3137
3323
|
EBK_SOURCE_COMMIT: \${{ github.sha }}
|
|
3138
3324
|
EBK_STAGE: production
|
|
3325
|
+
EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-production.outputs.production_account_id }}
|
|
3326
|
+
EBK_POLICY_REGION: \${{ needs.plan-production.outputs.deployment_region }}
|
|
3139
3327
|
EBK_VERIFIED_STAGING_RUN_ID: \${{ needs.release-source-production.outputs.verified_staging_run_id }}
|
|
3140
3328
|
FOUNDATION_PROJECT: \${{ needs.plan-production.outputs.foundation_project }}
|
|
3141
3329
|
ORDERED_PROJECTS: \${{ needs.plan-production.outputs.ordered_projects }}
|
|
@@ -3232,6 +3420,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
|
|
|
3232
3420
|
test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'production AWS_DEPLOY_ROLE_ARN is required'; exit 1; }
|
|
3233
3421
|
test -n "$AWS_EXPECTED_ACCOUNT_ID" || { echo 'production AWS_EXPECTED_ACCOUNT_ID is required'; exit 1; }
|
|
3234
3422
|
test -n "$AWS_REGION" || { echo 'production AWS_REGION is required'; exit 1; }
|
|
3423
|
+
test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ||
|
|
3424
|
+
{ echo 'production environment account differs from committed deployment topology'; exit 1; }
|
|
3425
|
+
test "$AWS_REGION" = "$EBK_POLICY_REGION" ||
|
|
3426
|
+
{ echo 'production environment Region differs from committed deployment topology'; exit 1; }
|
|
3235
3427
|
test -n "$EBK_FLEET_ORIGIN" ||
|
|
3236
3428
|
{ echo 'production environment EBK_FLEET_ORIGIN is required'; exit 1; }
|
|
3237
3429
|
test "$EBK_FLEET_ORIGIN" = "$EBK_PLANNED_FLEET_ORIGIN" ||
|
|
@@ -4898,13 +5090,51 @@ permissions:
|
|
|
4898
5090
|
contents: read
|
|
4899
5091
|
|
|
4900
5092
|
jobs:
|
|
5093
|
+
deployment-classification:
|
|
5094
|
+
name: classify exact staging deployment
|
|
5095
|
+
if: >-
|
|
5096
|
+
github.event_name == 'workflow_run' &&
|
|
5097
|
+
github.event.workflow_run.event == 'push' &&
|
|
5098
|
+
github.event.workflow_run.head_branch == 'develop' &&
|
|
5099
|
+
github.event.workflow_run.conclusion == 'success'
|
|
5100
|
+
permissions:
|
|
5101
|
+
actions: read
|
|
5102
|
+
contents: read
|
|
5103
|
+
outputs:
|
|
5104
|
+
deployed: \${{ steps.classify.outputs.deployed }}
|
|
5105
|
+
runs-on: ubuntu-latest
|
|
5106
|
+
steps:
|
|
5107
|
+
- id: classify
|
|
5108
|
+
name: Require one successful staging deployment job
|
|
5109
|
+
env:
|
|
5110
|
+
EBK_DEPLOYMENT_RUN_ID: \${{ github.event.workflow_run.id }}
|
|
5111
|
+
GH_TOKEN: \${{ github.token }}
|
|
5112
|
+
GH_REPO: \${{ github.repository }}
|
|
5113
|
+
run: |
|
|
5114
|
+
jobs="$(gh api \
|
|
5115
|
+
"/repos/$GH_REPO/actions/runs/$EBK_DEPLOYMENT_RUN_ID/jobs?filter=latest&per_page=100")"
|
|
5116
|
+
apply_count="$(printf '%s' "$jobs" | jq \
|
|
5117
|
+
'[.jobs[] | select(.name | startswith("${stagingApplyJobPrefix}")) | select(.conclusion == "success")] | length')"
|
|
5118
|
+
route_success_count="$(printf '%s' "$jobs" | jq \
|
|
5119
|
+
'[.jobs[] | select(.name == "${protectedDeploymentRouteJobName}" and .conclusion == "success")] | length')"
|
|
5120
|
+
route_skipped_count="$(printf '%s' "$jobs" | jq \
|
|
5121
|
+
'[.jobs[] | select(.name == "${protectedDeploymentRouteJobName}" and .conclusion == "skipped")] | length')"
|
|
5122
|
+
case "$apply_count:$route_success_count:$route_skipped_count" in
|
|
5123
|
+
1:1:0) echo 'deployed=true' >> "$GITHUB_OUTPUT" ;;
|
|
5124
|
+
0:0:1) echo 'deployed=false' >> "$GITHUB_OUTPUT" ;;
|
|
5125
|
+
*) echo 'Deploy route and staging application evidence disagree'; exit 1 ;;
|
|
5126
|
+
esac
|
|
5127
|
+
|
|
4901
5128
|
plan:
|
|
5129
|
+
needs: deployment-classification
|
|
4902
5130
|
if: >-
|
|
4903
|
-
(
|
|
5131
|
+
always() &&
|
|
5132
|
+
((github.event_name == 'workflow_run' &&
|
|
4904
5133
|
github.event.workflow_run.conclusion == 'success' &&
|
|
4905
5134
|
github.event.workflow_run.event == 'push' &&
|
|
4906
|
-
github.event.workflow_run.head_branch == 'develop'
|
|
4907
|
-
|
|
5135
|
+
github.event.workflow_run.head_branch == 'develop' &&
|
|
5136
|
+
needs.deployment-classification.outputs.deployed == 'true') ||
|
|
5137
|
+
github.event_name == 'workflow_dispatch')
|
|
4908
5138
|
outputs:
|
|
4909
5139
|
deployment_run_attempt: \${{ steps.identity.outputs.deployment_run_attempt }}
|
|
4910
5140
|
deployment_run_id: \${{ steps.identity.outputs.deployment_run_id }}
|
|
@@ -5397,14 +5627,14 @@ jobs:
|
|
|
5397
5627
|
test "$(node --version | cut -d. -f1)" = v22
|
|
5398
5628
|
- name: Run exact deployed smoke
|
|
5399
5629
|
working-directory: \${{ runner.temp }}/ebk-prepared-workspace
|
|
5400
|
-
run: node node_modules/nx/bin/nx.js run "$EBK_PROJECT:smoke-deployed"
|
|
5630
|
+
run: node node_modules/nx/dist/bin/nx.js run "$EBK_PROJECT:smoke-deployed"
|
|
5401
5631
|
- name: Verify reviewed frontend visual baseline
|
|
5402
5632
|
working-directory: \${{ runner.temp }}/ebk-prepared-workspace
|
|
5403
|
-
run: node node_modules/nx/bin/nx.js run "$EBK_PROJECT:frontend-visual"
|
|
5633
|
+
run: node node_modules/nx/dist/bin/nx.js run "$EBK_PROJECT:frontend-visual"
|
|
5404
5634
|
- name: Verify deployed frontend security headers
|
|
5405
5635
|
working-directory: \${{ runner.temp }}/ebk-prepared-workspace
|
|
5406
5636
|
run: |
|
|
5407
|
-
node node_modules/nx/bin/nx.js run \
|
|
5637
|
+
node node_modules/nx/dist/bin/nx.js run \
|
|
5408
5638
|
"$EBK_PROJECT:frontend-security-headers" -- \
|
|
5409
5639
|
--url="$EBK_SMOKE_URL"
|
|
5410
5640
|
- name: Bind exact alarm and data-integrity evidence
|
|
@@ -6851,11 +7081,11 @@ jobs:
|
|
|
6851
7081
|
if [ "$EBK_ACTION" = apply ]; then args+=("--apply"); fi
|
|
6852
7082
|
pnpm nx run "$EBK_PROJECT:secret-metadata" -- "\${args[@]}"
|
|
6853
7083
|
`,
|
|
6854
|
-
'.github/workflows/sync-back.yml': `name: Sync
|
|
7084
|
+
'.github/workflows/sync-back.yml': `name: Sync protected main history
|
|
6855
7085
|
|
|
6856
7086
|
on:
|
|
6857
7087
|
workflow_run:
|
|
6858
|
-
workflows: [
|
|
7088
|
+
workflows: [Verify]
|
|
6859
7089
|
types: [completed]
|
|
6860
7090
|
|
|
6861
7091
|
permissions:
|
|
@@ -6866,6 +7096,7 @@ jobs:
|
|
|
6866
7096
|
sync-back:
|
|
6867
7097
|
if: >-
|
|
6868
7098
|
github.event.workflow_run.conclusion == 'success' &&
|
|
7099
|
+
github.event.workflow_run.event == 'push' &&
|
|
6869
7100
|
github.event.workflow_run.head_branch == 'main'
|
|
6870
7101
|
runs-on: ubuntu-latest
|
|
6871
7102
|
steps:
|
|
@@ -6879,8 +7110,8 @@ jobs:
|
|
|
6879
7110
|
gh pr create \
|
|
6880
7111
|
--head main \
|
|
6881
7112
|
--base develop \
|
|
6882
|
-
--title 'chore: sync
|
|
6883
|
-
--body 'Managed EBK sync
|
|
7113
|
+
--title 'chore(git): sync protected main to develop' \
|
|
7114
|
+
--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
7115
|
number=$(gh pr list --state open --head main --base develop --json number --jq '.[0].number')
|
|
6885
7116
|
fi
|
|
6886
7117
|
echo "Sync-back pull request #$number is ready for review."
|
|
@@ -7234,6 +7465,16 @@ use the direct installed \`.bin/ebk blueprint verification-review\` entry docume
|
|
|
7234
7465
|
\`docs/blueprints.md\`.
|
|
7235
7466
|
|
|
7236
7467
|
Workspace creation only writes source files. It does not create or change cloud resources.
|
|
7468
|
+
|
|
7469
|
+
New workspaces default to the one-account \`starter\` deployment profile. Configure the exact
|
|
7470
|
+
shared account and Region in \`ebk.repository-policy.json\`, authenticate through IAM Identity
|
|
7471
|
+
Center, add exact development and staging CIDRs to
|
|
7472
|
+
\`packages/foundation/security/router-waf.json\`, and use
|
|
7473
|
+
\`pnpm nx run ${name}:deploy-local -- --stage=development
|
|
7474
|
+
--profile=<profile> --expectedRoleArn=<role-arn>\`. Never invoke SST directly.
|
|
7475
|
+
For a brand-new stage with generated Product Slices, complete the documented workload-role
|
|
7476
|
+
Foundation publication and pass \`--phase=foundation-bootstrap\` once before publishing and
|
|
7477
|
+
deploying the complete stage.
|
|
7237
7478
|
`,
|
|
7238
7479
|
'docs/architecture.md': `# Workspace architecture
|
|
7239
7480
|
|
|
@@ -7250,6 +7491,11 @@ Foundation implementation files.
|
|
|
7250
7491
|
Canonical consumer stages use the matching \`development\`, \`staging\`, or \`production\`
|
|
7251
7492
|
Foundation. Framework-derived personal stages may use only the \`development\`
|
|
7252
7493
|
Foundation; generic preview stages are unsupported.
|
|
7494
|
+
|
|
7495
|
+
The committed deployment profile changes account and local/GitHub authority only. Every
|
|
7496
|
+
canonical stage always owns a separate Router/domain, Aurora cluster and migration history,
|
|
7497
|
+
Cognito user pool, event bus, secret namespace, IAM boundary, logs, queues, ECR namespace, and
|
|
7498
|
+
stage-qualified SSM discovery contract. Discovery rejects a record for another canonical stage.
|
|
7253
7499
|
`,
|
|
7254
7500
|
'docs/git-workflow.md': `# Protected Git workflow
|
|
7255
7501
|
|
|
@@ -7270,6 +7516,9 @@ GitHub rulesets, environments, and AWS IAM are the authorization boundaries; loc
|
|
|
7270
7516
|
only defense in depth.
|
|
7271
7517
|
|
|
7272
7518
|
The \`staging\` environment accepts only \`develop\`; \`production\` accepts only \`main\`.
|
|
7519
|
+
These workflows exist for every deployment profile. The staging workflow is optional for
|
|
7520
|
+
\`starter\`, whose governed local Nx target may deploy development and staging. Production is
|
|
7521
|
+
GitHub-only for every profile; \`fully-isolated\` also requires GitHub for staging.
|
|
7273
7522
|
The committed \`productionApproval.mode\` selects one of two production authorization
|
|
7274
7523
|
contracts:
|
|
7275
7524
|
|
|
@@ -7292,7 +7541,9 @@ contracts:
|
|
|
7292
7541
|
Both modes retain required checks, a non-author CODEOWNER approval, no ruleset bypass, the
|
|
7293
7542
|
literal production environment, and its branch-scoped credentials. The base \`staging\` and
|
|
7294
7543
|
\`production\` environments own deployment, smoke, and ECR evidence roles plus their
|
|
7295
|
-
variables;
|
|
7544
|
+
variables; isolated profiles also keep the staging-registry promotion pull role in production.
|
|
7545
|
+
Starter promotion authenticates once to the shared account and copies the exact staging digest
|
|
7546
|
+
into a distinct production ECR repository. Dedicated
|
|
7296
7547
|
\`infrastructure-plan-<stage>\` environments gate read-only role use. Separate
|
|
7297
7548
|
\`async-recovery-<stage>\`, \`performance-aggregate-<stage>\`,
|
|
7298
7549
|
\`performance-cost-<stage>\`, \`performance-load-<stage>\`, and
|
|
@@ -7441,18 +7692,35 @@ materializes, close and reopen the pull request as a human and then approve it.
|
|
|
7441
7692
|
`,
|
|
7442
7693
|
'docs/repository-operating-policy.md': `# Repository Operating Policy
|
|
7443
7694
|
|
|
7444
|
-
\`ebk.repository-policy.json\` is the reviewed source of truth for
|
|
7445
|
-
|
|
7695
|
+
\`ebk.repository-policy.json\` is the reviewed source of truth for deployment profile, exact
|
|
7696
|
+
stage account IDs, deployment Region, production access, production approval, per-Slice
|
|
7697
|
+
readiness, and the one shared production Foundation profile.
|
|
7446
7698
|
New workspaces start with production disabled and explicit Team-compatible
|
|
7447
7699
|
\`productionApproval.mode: "promotion-pr"\`. A missing approval mode or Slice entry fails
|
|
7448
7700
|
closed; no GitHub capability is allowed to choose a mode implicitly.
|
|
7449
7701
|
|
|
7702
|
+
Deployment profiles are proportional to operator maturity:
|
|
7703
|
+
|
|
7704
|
+
- \`starter\` (default) requires one shared account, permits local development and staging
|
|
7705
|
+
through the governed root Nx target, and reserves production for protected GitHub deployment;
|
|
7706
|
+
- \`production-isolated\` requires one development/staging account and a distinct production
|
|
7707
|
+
account; only production is GitHub-only; and
|
|
7708
|
+
- \`fully-isolated\` requires three distinct accounts; staging and production are GitHub-only.
|
|
7709
|
+
|
|
7710
|
+
Equal account IDs never imply shared stage resources. Router, Aurora, Cognito, events, secrets,
|
|
7711
|
+
IAM, logs, queues, resource names, and SSM discovery remain stage-specific. Existing prerelease
|
|
7712
|
+
repositories migrate explicitly to \`fully-isolated\`; upgrades never infer or weaken topology.
|
|
7713
|
+
That profile-only migration retains the former authority model but stays undeployable until exact
|
|
7714
|
+
development, staging, and production account IDs plus \`deploymentRegion\` are committed.
|
|
7715
|
+
|
|
7450
7716
|
The named repository modes are source-writing presets, not runtime state. Report a change
|
|
7451
7717
|
first and inspect the complete current/next policy:
|
|
7452
7718
|
|
|
7453
7719
|
\`\`\`bash
|
|
7454
7720
|
pnpm nx g @empire-builder-kit/nx:repository-policy \\
|
|
7455
|
-
--
|
|
7721
|
+
--deploymentProfile=starter --deploymentRegion=us-east-1 \\
|
|
7722
|
+
--developmentAccountId=111122223333 --stagingAccountId=111122223333 \\
|
|
7723
|
+
--productionAccountId=111122223333
|
|
7456
7724
|
\`\`\`
|
|
7457
7725
|
|
|
7458
7726
|
Apply the reviewed source change separately with \`--apply\`. This command never contacts AWS
|
|
@@ -7462,6 +7730,66 @@ Use \`--productionApprovalMode=promotion-pr|environment-reviewer\` for an explic
|
|
|
7462
7730
|
transition. This changes committed policy only; a separate bootstrap report/apply reconciles
|
|
7463
7731
|
remote GitHub settings.
|
|
7464
7732
|
|
|
7733
|
+
After applying exact topology, authenticate with temporary federated credentials for development
|
|
7734
|
+
or staging and invoke
|
|
7735
|
+
\`pnpm nx run ${name}:deploy-local -- --stage=<stage> --profile=<profile>
|
|
7736
|
+
--expectedRoleArn=<role-arn>\`. Production runs only through the protected main-branch GitHub
|
|
7737
|
+
workflow. Each admitted local invocation plans the complete current stage, displays the stage
|
|
7738
|
+
summary and first material project, and requires a real TTY response of
|
|
7739
|
+
\`approve:<action>:<stage>:<project>:<current-plan-digest>\`. A human or authorized agent may type
|
|
7740
|
+
that acknowledgement; it is active operator control, not a claim of independent human review.
|
|
7741
|
+
The target creates one private ten-minute capability for that project, rechecks the exact source,
|
|
7742
|
+
artifacts, account, Region, role, stage, policy, state, and lock without a second provider preview,
|
|
7743
|
+
consumes the capability once, and removes it on every exit path. Unattended scripts cannot approve.
|
|
7744
|
+
Every material deploy or refresh mutates at most the first material project and ends with a
|
|
7745
|
+
controlled non-success \`replan-required\` handoff, including the last project in the ordered stage.
|
|
7746
|
+
Rerun the complete stage, review the new current summary, and repeat until one invocation proves
|
|
7747
|
+
every project is semantically converged. Only that convergence invocation succeeds. A preview is
|
|
7748
|
+
recent review and audit evidence; it is not represented as a cryptographically replayable Pulumi
|
|
7749
|
+
transaction. A complete, unlocked deploy plan containing only provider reads with no changed
|
|
7750
|
+
properties, replacement, failure, or unmatched risk finding is retained as observation evidence
|
|
7751
|
+
and counts as semantically converged. The standalone project planner may still return its
|
|
7752
|
+
conservative non-success for that drift observation; the root local transaction accepts only a
|
|
7753
|
+
freshly emitted exact provider-read-only record from that child. Repeated reads coalesce
|
|
7754
|
+
only when their logical ID and resource type match exactly. Any stale or ambiguous
|
|
7755
|
+
observation fails closed; a plan with desired changes remains material and requires review. One
|
|
7756
|
+
narrower case stays reviewable instead of becoming
|
|
7757
|
+
drift: when an upstream Foundation apply changes only both external framework-version tag
|
|
7758
|
+
projections (plus SST's already-bounded name hydration), the retained
|
|
7759
|
+
external identity must match one exact non-replacing provider read in the same ordinary plan.
|
|
7760
|
+
Missing read correlation, owned resources, other tag or property changes, incomplete tag pairs,
|
|
7761
|
+
and provider failures remain drift and fail closed. The local target requires the exact SST v5 account bootstrap normally
|
|
7762
|
+
created by the pinned \`ebk dev up\` path, then creates and attests only missing exact
|
|
7763
|
+
per-project/canonical-stage state passphrases before its formal read-only plans. This passphrase
|
|
7764
|
+
bootstrap is the only persistent pre-acknowledgement write; it creates no deployment authority
|
|
7765
|
+
and is reported in bounded local evidence. Clean immutable source, immediate per-project plans,
|
|
7766
|
+
migration and retention preflights, and secret-free result
|
|
7767
|
+
evidence remain mandatory. Direct SST is forbidden.
|
|
7768
|
+
|
|
7769
|
+
After an interrupted apply, use the same target with \`--action=refresh\` and the exact
|
|
7770
|
+
phase, stage, profile, and role. It retains the same plan-summary review boundary, admits only
|
|
7771
|
+
bounded drift-detected refresh evidence, and never turns refresh into an implicit deploy. When
|
|
7772
|
+
the interrupted write leaves a recorded provider output unusable by the ordinary desired preview,
|
|
7773
|
+
the plan stops without mutation. The non-writing refresh preview separates provider refresh
|
|
7774
|
+
records from its later desired preview and cannot replace failed ordinary desired authority;
|
|
7775
|
+
repair the recorded dependency or use the documented
|
|
7776
|
+
operator recovery path before planning again.
|
|
7777
|
+
|
|
7778
|
+
A stage-wide refresh still plans and authorizes every selected project. When one exact retained
|
|
7779
|
+
project plan proves an absent SST state that is unlocked, complete, empty, and drift-free, the
|
|
7780
|
+
root target records that project as a refresh no-op instead of invoking SST against a nonexistent
|
|
7781
|
+
stage. It also retains the pinned provider's exact SSM \`ebk:framework-version\` \`tagsAll\`
|
|
7782
|
+
normalization as a semantically inert observation after the same bounded shape
|
|
7783
|
+
repeats following refresh. Value changes, other tags, security findings, deletes, replacements,
|
|
7784
|
+
sensitive properties, and every other resource type remain material. Locked, incomplete, failed,
|
|
7785
|
+
or otherwise drifted plans are never skipped.
|
|
7786
|
+
|
|
7787
|
+
For a stage with no workload-role account baseline, publish the fixed Foundation-only candidate
|
|
7788
|
+
and invoke this root target with \`--phase=foundation-bootstrap\`. After Foundation outputs exist,
|
|
7789
|
+
publish the complete \`full-stage\` candidate and invoke the target without \`--phase\` (or with
|
|
7790
|
+
\`--phase=full-stage\`). The bootstrap phase selects exactly Foundation; it is not an arbitrary
|
|
7791
|
+
project filter.
|
|
7792
|
+
|
|
7465
7793
|
Validation requires both an explicit production approval mode and an explicit Foundation
|
|
7466
7794
|
profile. It does not infer omitted values. Use \`promotion-pr\` for a private GitHub Team
|
|
7467
7795
|
repository. Use \`environment-reviewer\` only when the repository plan can enforce a separate
|
|
@@ -7475,10 +7803,16 @@ environment reviewer.
|
|
|
7475
7803
|
|
|
7476
7804
|
Protected production deploy and refresh validate the complete Foundation/Slice set before AWS
|
|
7477
7805
|
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
|
-
|
|
7806
|
+
cannot be narrowed with an ad hoc project argument. The retained stage preview remains review
|
|
7807
|
+
evidence. Immediately before each project mutation the workflow computes one fresh plan against
|
|
7808
|
+
current state and applies it without a second redundant preview. Source, artifacts, configuration,
|
|
7809
|
+
policy, environment, and protected identities remain exact. Provider-only read/discard observations
|
|
7810
|
+
may appear or disappear, but any new delete, replacement, import, stateful or security change, IAM
|
|
7811
|
+
expansion, protection reduction, or unclassified resource stops for renewed approval. After an
|
|
7812
|
+
upstream mutation, every downstream project therefore receives a fresh independent risk decision.
|
|
7813
|
+
|
|
7814
|
+
\`standard\` keeps Aurora warm at 2–16 ACU. \`cost-optimized\` keeps a smaller
|
|
7815
|
+
0.5–4 ACU warm floor for starter production traffic. Both keep managed NAT,
|
|
7482
7816
|
30-day backups, deletion protection, retained data, and the protected GitHub/OIDC/AWS path.
|
|
7483
7817
|
Changing policy affects only later lifecycle requests; it never deploys or removes resources.
|
|
7484
7818
|
Changing profile requires a separate reviewed Foundation deployment and retained operational
|
|
@@ -7549,7 +7883,7 @@ deploy or refresh before AWS access. Protected remove and unlock remain usable f
|
|
|
7549
7883
|
There is no local or silent bypass in this prerelease contract; any future waiver mechanism
|
|
7550
7884
|
must be committed, reviewable, and auditable.
|
|
7551
7885
|
`,
|
|
7552
|
-
'ebk.repository-policy.json': json(
|
|
7886
|
+
'ebk.repository-policy.json': json(initialRepositoryPolicy),
|
|
7553
7887
|
'nx.json': json({
|
|
7554
7888
|
$schema: './node_modules/nx/schemas/nx-schema.json',
|
|
7555
7889
|
defaultBase: 'develop',
|
|
@@ -7589,6 +7923,7 @@ must be committed, reviewable, and auditable.
|
|
|
7589
7923
|
inputs: ['default', '^production'],
|
|
7590
7924
|
},
|
|
7591
7925
|
},
|
|
7926
|
+
analytics: false,
|
|
7592
7927
|
neverConnectToCloud: true,
|
|
7593
7928
|
useDaemonProcess: false,
|
|
7594
7929
|
}),
|
|
@@ -7617,10 +7952,12 @@ must be committed, reviewable, and auditable.
|
|
|
7617
7952
|
},
|
|
7618
7953
|
devDependencies: {
|
|
7619
7954
|
'@aws-sdk/client-iam': TOOL_VERSIONS.awsSdk,
|
|
7955
|
+
'@aws-sdk/client-rds': TOOL_VERSIONS.awsSdk,
|
|
7620
7956
|
'@aws-sdk/client-s3': TOOL_VERSIONS.awsSdk,
|
|
7621
7957
|
'@aws-sdk/client-secrets-manager': TOOL_VERSIONS.awsSdk,
|
|
7622
7958
|
'@aws-sdk/client-ssm': TOOL_VERSIONS.awsSdk,
|
|
7623
7959
|
'@aws-sdk/client-sts': TOOL_VERSIONS.awsSdk,
|
|
7960
|
+
'@aws-sdk/credential-providers': TOOL_VERSIONS.awsSdk,
|
|
7624
7961
|
'@eslint/js': TOOL_VERSIONS.eslintJs,
|
|
7625
7962
|
'@empire-builder-kit/nx': frameworkVersion,
|
|
7626
7963
|
'@types/node': TOOL_VERSIONS.nodeTypes,
|
|
@@ -7704,6 +8041,13 @@ must be committed, reviewable, and auditable.
|
|
|
7704
8041
|
executor: 'nx:run-commands',
|
|
7705
8042
|
options: { command: 'node tools/install-ebk-hooks.mjs' },
|
|
7706
8043
|
},
|
|
8044
|
+
'deploy-local': {
|
|
8045
|
+
cache: false,
|
|
8046
|
+
executor: '@empire-builder-kit/nx:local-deploy',
|
|
8047
|
+
options: {
|
|
8048
|
+
stage: 'development',
|
|
8049
|
+
},
|
|
8050
|
+
},
|
|
7707
8051
|
'supply-chain-check': {
|
|
7708
8052
|
executor: 'nx:run-commands',
|
|
7709
8053
|
options: {
|
|
@@ -7772,6 +8116,10 @@ Workspace creation does not deploy this project. Review \`docs/costs.md\`,
|
|
|
7772
8116
|
|
|
7773
8117
|
Set \`EBK_ROOT_DOMAIN\` before deploying when Route 53 in the target account owns a public
|
|
7774
8118
|
domain. Without it, the shared Router uses its functional provider URL.
|
|
8119
|
+
|
|
8120
|
+
Before deploying development or staging, add at least one exact operator/probe CIDR to
|
|
8121
|
+
\`security/router-waf.json\`. Production is public by default behind the same managed and
|
|
8122
|
+
rate-limiting baseline; select its allowlist mode explicitly when required.
|
|
7775
8123
|
`,
|
|
7776
8124
|
'packages/foundation/contracts/foundation.json': json({
|
|
7777
8125
|
contractVersion: '1.0.0',
|
|
@@ -7831,8 +8179,8 @@ seeded decision record aligned with organization-specific budgets and alerts.
|
|
|
7831
8179
|
- Development uses EC2 NAT, a development bastion, and Aurora scale-to-zero.
|
|
7832
8180
|
- Staging uses EC2 NAT and Aurora scale-to-zero by default.
|
|
7833
8181
|
- Production always uses managed NAT and retains protected data. Its explicit Repository
|
|
7834
|
-
Operating Policy profile selects either the standard
|
|
7835
|
-
|
|
8182
|
+
Operating Policy profile selects either the standard 2-ACU Aurora floor or the
|
|
8183
|
+
cost-optimized 0.5-ACU warm floor.
|
|
7836
8184
|
- ECS, EventBridge, Cognito, SNS, SSM, and CloudWatch are primarily usage-priced; logs,
|
|
7837
8185
|
metrics, alarms, and retained data still require active cost review.
|
|
7838
8186
|
|
|
@@ -7850,8 +8198,69 @@ Use Nx for every supported lifecycle action:
|
|
|
7850
8198
|
- \`pnpm nx run foundation:unlock --stage=development\`
|
|
7851
8199
|
- \`pnpm nx run foundation:remove --stage=development\`
|
|
7852
8200
|
|
|
7853
|
-
Do not invoke SST lifecycle commands directly.
|
|
7854
|
-
|
|
8201
|
+
Do not invoke SST lifecycle commands directly. The root \`deploy-local\` target is the governed
|
|
8202
|
+
local path for stages admitted by the committed deployment profile. Its fixed
|
|
8203
|
+
\`foundation-bootstrap\` phase is the only local partial-stage path and exists solely for the
|
|
8204
|
+
documented first workload-role publication; isolated protected stages remain on the trusted
|
|
8205
|
+
GitHub path.
|
|
8206
|
+
|
|
8207
|
+
Every local invocation plans the complete selected stage but mutates at most one material
|
|
8208
|
+
project. Type the exact current \`approve:<action>:<stage>:<project>:<plan-digest>\` challenge in a
|
|
8209
|
+
real TTY after reviewing the displayed summary. A human or authorized agent may operate that TTY;
|
|
8210
|
+
unattended input is refused. The acknowledgement creates one ten-minute, one-use, private
|
|
8211
|
+
capability bound to source, artifacts, policy, environment, credentials, and current state. A
|
|
8212
|
+
completed deploy or refresh returns the controlled non-success \`replan-required\`; rerun the
|
|
8213
|
+
complete stage, review the new current summary, and continue one project at a time. Only a
|
|
8214
|
+
semantically converged complete stage reports success. No upstream mutation can authorize a
|
|
8215
|
+
downstream project. A complete, unlocked deploy plan containing only provider reads with no
|
|
8216
|
+
changed properties, replacement, failure, or unmatched risk finding is retained as observation
|
|
8217
|
+
evidence and counts as semantically converged. The standalone project planner may still return
|
|
8218
|
+
its conservative non-success for that drift observation; the root local transaction accepts only
|
|
8219
|
+
a freshly emitted exact provider-read-only record from that child. Repeated reads coalesce
|
|
8220
|
+
only when their logical ID and resource type match exactly. Any stale or ambiguous
|
|
8221
|
+
observation fails closed; a plan with desired changes remains material and requires review. A
|
|
8222
|
+
material downstream plan may also retain an upstream Foundation framework-version tag advance as
|
|
8223
|
+
observation only when both external tag
|
|
8224
|
+
projections, the retained identity, optional bounded SST name hydration, and one non-replacing
|
|
8225
|
+
provider read correlate exactly. Every other external property or uncorroborated read remains
|
|
8226
|
+
drift and blocks before the TTY challenge.
|
|
8227
|
+
|
|
8228
|
+
For an interrupted local apply, run the same root target with \`--action=refresh\`, review its
|
|
8229
|
+
current provider-refresh evidence, and type the displayed TTY challenge. Before mutation the
|
|
8230
|
+
executor writes and verifies a private state recovery point plus a secret-free digest/version
|
|
8231
|
+
manifest. The refresh preview validates its provider and later desired-event partitions; any
|
|
8232
|
+
ordinary preview failure or malformed post-refresh desired event remains blocked. Provider-only
|
|
8233
|
+
observation noise is retained without claiming a replayable preview. Because refresh changes
|
|
8234
|
+
recorded state rather than cloud resources, provider-output security and protection findings
|
|
8235
|
+
remain explicit review requirements under the drift decision. Refresh never continues into deploy
|
|
8236
|
+
or claims desired-state convergence: run the governed deploy target afterward and resolve every
|
|
8237
|
+
remaining source-versus-state change. Exclude the \`.private.json\` recovery point from archives.
|
|
8238
|
+
|
|
8239
|
+
The root refresh target includes every selected project in its exact reviewed plan set. It skips
|
|
8240
|
+
the SST child only when that project's retained plan proves an absent, unlocked, complete, empty,
|
|
8241
|
+
and drift-free state, or when the only retained difference is the pinned provider's exact repeated
|
|
8242
|
+
SSM \`ebk:framework-version\` \`tagsAll\` normalization. It records the project as an explicit refresh no-op
|
|
8243
|
+
in either case while retaining the observation. Value changes, other tags, security findings,
|
|
8244
|
+
deletes, replacements, sensitive
|
|
8245
|
+
properties, and every other resource type remain material; every other plan must execute or fail
|
|
8246
|
+
closed.
|
|
8247
|
+
|
|
8248
|
+
## Router WAF
|
|
8249
|
+
|
|
8250
|
+
Each canonical stage owns one global-scope Web ACL attached to that stage's Foundation Router.
|
|
8251
|
+
The committed \`security/router-waf.json\` policy requires AWS Common Rule Set, Known Bad
|
|
8252
|
+
Inputs, Amazon IP Reputation, an IP rate limit, CloudWatch metrics, and sampled requests.
|
|
8253
|
+
Development and staging fail synthesis until their allowlists contain explicit CIDRs.
|
|
8254
|
+
Production is public by default and may be changed to an explicit allowlist.
|
|
8255
|
+
|
|
8256
|
+
For tuning, set a nonproduction stage to \`count\`, deploy and inspect sampled requests, then
|
|
8257
|
+
return staging to \`block\` and run the live allow/deny, managed-rule, rate-limit, metric, and
|
|
8258
|
+
sampled-request probes. Write their bounded, secret-free result to
|
|
8259
|
+
\`security/router-waf-live-evidence.json\`, then run
|
|
8260
|
+
\`pnpm nx run foundation:router-waf-rehearsal\`. The executor seals the exact policy digest
|
|
8261
|
+
into \`router-waf-blocking-rehearsal.json\`. Production synthesis rejects a missing, stale, or
|
|
8262
|
+
count-mode rehearsal. Full request logging is opt-in, blocked-only, retention-bounded, and
|
|
8263
|
+
redacts query strings plus credential-bearing headers.
|
|
7855
8264
|
|
|
7856
8265
|
Foundation stages are canonical. \`ebk dev\` derives a Product Slice personal
|
|
7857
8266
|
stage from the operating-system account and maps that consumer to the
|
|
@@ -7886,6 +8295,12 @@ connects as the Slice's no-password IAM-authenticated migrator user.
|
|
|
7886
8295
|
stage-scoped SSM/secret path and least-privilege IAM.
|
|
7887
8296
|
- Public domains are configured explicitly through \`EBK_ROOT_DOMAIN\`; generated source never
|
|
7888
8297
|
contains credentials or customer domain assumptions.
|
|
8298
|
+
- Every stage's CloudFront Router has its own global Web ACL. The baseline always includes AWS
|
|
8299
|
+
Common Rule Set, Known Bad Inputs, Amazon IP Reputation, rate blocking, CloudWatch metrics,
|
|
8300
|
+
and sampled requests. Nonproduction is allowlisted by default. Production is public by
|
|
8301
|
+
default, but cannot deploy until the exact policy has passed a staging blocking rehearsal.
|
|
8302
|
+
- Full WAF request logging is disabled by default. Enabling it keeps blocked requests only,
|
|
8303
|
+
uses bounded retention, and redacts query strings, cookies, authorization, and API keys.
|
|
7889
8304
|
|
|
7890
8305
|
The managed \`contracts/frontend-security-headers.json\` file owns the transport
|
|
7891
8306
|
minimum: HTTPS in deployed environments, one-year HSTS there, and the common
|
|
@@ -7918,7 +8333,12 @@ declare const $app: {
|
|
|
7918
8333
|
import {
|
|
7919
8334
|
FOUNDATION_SSM_SCHEMA_VERSION,
|
|
7920
8335
|
STANDARD_FOUNDATION_CAPABILITIES,
|
|
8336
|
+
createRouterWafRules,
|
|
8337
|
+
createRouterWafStagePlan,
|
|
8338
|
+
foundationDatabaseEndpointHost,
|
|
7921
8339
|
foundationSsmPath,
|
|
8340
|
+
normalizeRouterWafPolicy,
|
|
8341
|
+
reconcileRouterWafBlockingRehearsal,
|
|
7922
8342
|
type FoundationSsmKey,
|
|
7923
8343
|
} from '@empire-builder-kit/nx/foundation';
|
|
7924
8344
|
import {
|
|
@@ -7928,6 +8348,9 @@ import {
|
|
|
7928
8348
|
} from '@empire-builder-kit/nx/repository-policy';
|
|
7929
8349
|
import { createFoundationAccountCostControlPlan } from '@empire-builder-kit/nx/performance';
|
|
7930
8350
|
import accountCostPolicy from '../performance/account-cost-policy.json';
|
|
8351
|
+
import routerWafBlockingRehearsal from '../security/router-waf-blocking-rehearsal.json';
|
|
8352
|
+
import routerWafLiveEvidence from '../security/router-waf-live-evidence.json';
|
|
8353
|
+
import routerWafPolicy from '../security/router-waf.json';
|
|
7931
8354
|
import type { FoundationWorkloadRolePreflight } from './workload-roles/preflight.js';
|
|
7932
8355
|
|
|
7933
8356
|
function identityDomainPrefix(stage: string, accountId: string): string {
|
|
@@ -7979,6 +8402,18 @@ export function foundation(
|
|
|
7979
8402
|
)
|
|
7980
8403
|
: undefined;
|
|
7981
8404
|
const natMode = productionConfiguration?.contract.nat ?? 'ec2';
|
|
8405
|
+
const normalizedRouterWafPolicy = normalizeRouterWafPolicy(routerWafPolicy);
|
|
8406
|
+
const wafPlan = createRouterWafStagePlan({
|
|
8407
|
+
policy: normalizedRouterWafPolicy,
|
|
8408
|
+
rehearsal: isProduction
|
|
8409
|
+
? reconcileRouterWafBlockingRehearsal({
|
|
8410
|
+
evidence: routerWafLiveEvidence,
|
|
8411
|
+
policy: normalizedRouterWafPolicy,
|
|
8412
|
+
receipt: routerWafBlockingRehearsal,
|
|
8413
|
+
})
|
|
8414
|
+
: null,
|
|
8415
|
+
stage: $app.stage,
|
|
8416
|
+
});
|
|
7982
8417
|
const natBinding = workloadRoles?.bindings.nat;
|
|
7983
8418
|
if (natMode === 'ec2' && natBinding) {
|
|
7984
8419
|
$transform(
|
|
@@ -7988,6 +8423,9 @@ export function foundation(
|
|
|
7988
8423
|
args.name = natBinding.instanceProfileName;
|
|
7989
8424
|
args.role = natBinding.roleName;
|
|
7990
8425
|
opts.import = natBinding.instanceProfileName;
|
|
8426
|
+
opts.ignoreChanges = [
|
|
8427
|
+
...new Set([...(opts.ignoreChanges ?? []), 'tags', 'tagsAll']),
|
|
8428
|
+
];
|
|
7991
8429
|
},
|
|
7992
8430
|
);
|
|
7993
8431
|
}
|
|
@@ -8009,8 +8447,83 @@ export function foundation(
|
|
|
8009
8447
|
aliases: ['*.' + rootDomain],
|
|
8010
8448
|
dns: sst.aws.dns(),
|
|
8011
8449
|
},
|
|
8012
|
-
|
|
8450
|
+
}
|
|
8013
8451
|
: {}),
|
|
8452
|
+
waf: {
|
|
8453
|
+
logging: wafPlan.logging,
|
|
8454
|
+
managedRules: {
|
|
8455
|
+
coreRuleSet: true,
|
|
8456
|
+
knownBadInputs: true,
|
|
8457
|
+
sqlInjection: false,
|
|
8458
|
+
},
|
|
8459
|
+
rateLimitPerIp: wafPlan.rateLimitPerIp,
|
|
8460
|
+
},
|
|
8461
|
+
transform: {
|
|
8462
|
+
cdn(args: any) {
|
|
8463
|
+
if (!args.webAclArn) {
|
|
8464
|
+
throw new Error(
|
|
8465
|
+
'Foundation Router must attach its exact global Web ACL.',
|
|
8466
|
+
);
|
|
8467
|
+
}
|
|
8468
|
+
},
|
|
8469
|
+
waf(args: any, opts: any) {
|
|
8470
|
+
if (args.scope !== 'CLOUDFRONT') {
|
|
8471
|
+
throw new Error(
|
|
8472
|
+
'Foundation Router Web ACL must use global CloudFront scope.',
|
|
8473
|
+
);
|
|
8474
|
+
}
|
|
8475
|
+
const ipSets: { ipv4Arn?: any; ipv6Arn?: any } = {};
|
|
8476
|
+
if (wafPlan.ipv4Cidrs.length > 0) {
|
|
8477
|
+
ipSets.ipv4Arn = new aws.wafv2.IpSet(
|
|
8478
|
+
'FoundationRouterIpv4Allowlist',
|
|
8479
|
+
{
|
|
8480
|
+
addresses: wafPlan.ipv4Cidrs,
|
|
8481
|
+
ipAddressVersion: 'IPV4',
|
|
8482
|
+
name: 'foundation-' + wafPlan.stage + '-router-ipv4-allowlist',
|
|
8483
|
+
scope: 'CLOUDFRONT',
|
|
8484
|
+
tags: {
|
|
8485
|
+
'ebk:policy-digest': wafPlan.policyDigest,
|
|
8486
|
+
'ebk:stage': wafPlan.stage,
|
|
8487
|
+
},
|
|
8488
|
+
},
|
|
8489
|
+
{ provider: opts.provider },
|
|
8490
|
+
).arn;
|
|
8491
|
+
}
|
|
8492
|
+
if (wafPlan.ipv6Cidrs.length > 0) {
|
|
8493
|
+
ipSets.ipv6Arn = new aws.wafv2.IpSet(
|
|
8494
|
+
'FoundationRouterIpv6Allowlist',
|
|
8495
|
+
{
|
|
8496
|
+
addresses: wafPlan.ipv6Cidrs,
|
|
8497
|
+
ipAddressVersion: 'IPV6',
|
|
8498
|
+
name: 'foundation-' + wafPlan.stage + '-router-ipv6-allowlist',
|
|
8499
|
+
scope: 'CLOUDFRONT',
|
|
8500
|
+
tags: {
|
|
8501
|
+
'ebk:policy-digest': wafPlan.policyDigest,
|
|
8502
|
+
'ebk:stage': wafPlan.stage,
|
|
8503
|
+
},
|
|
8504
|
+
},
|
|
8505
|
+
{ provider: opts.provider },
|
|
8506
|
+
).arn;
|
|
8507
|
+
}
|
|
8508
|
+
args.defaultAction = { allow: {} };
|
|
8509
|
+
args.rules = createRouterWafRules(wafPlan, ipSets);
|
|
8510
|
+
args.scope = 'CLOUDFRONT';
|
|
8511
|
+
args.tags = {
|
|
8512
|
+
...(args.tags ?? {}),
|
|
8513
|
+
'ebk:policy-digest': wafPlan.policyDigest,
|
|
8514
|
+
'ebk:stage': wafPlan.stage,
|
|
8515
|
+
};
|
|
8516
|
+
args.visibilityConfig = {
|
|
8517
|
+
cloudwatchMetricsEnabled: true,
|
|
8518
|
+
metricName:
|
|
8519
|
+
'Ebk' +
|
|
8520
|
+
wafPlan.stage[0].toUpperCase() +
|
|
8521
|
+
wafPlan.stage.slice(1) +
|
|
8522
|
+
'FoundationRouterWaf',
|
|
8523
|
+
sampledRequestsEnabled: true,
|
|
8524
|
+
};
|
|
8525
|
+
},
|
|
8526
|
+
},
|
|
8014
8527
|
});
|
|
8015
8528
|
const database = new sst.aws.Aurora('FoundationDatabase', {
|
|
8016
8529
|
// SQL bootstrap and migrations run inside the VPC. Keep the public HTTPS
|
|
@@ -8034,6 +8547,46 @@ export function foundation(
|
|
|
8034
8547
|
},
|
|
8035
8548
|
vpc,
|
|
8036
8549
|
});
|
|
8550
|
+
const databaseClusterEndpoint = database.nodes.cluster.endpoint.apply(
|
|
8551
|
+
foundationDatabaseEndpointHost,
|
|
8552
|
+
);
|
|
8553
|
+
const databaseReaderEndpoint = database.nodes.cluster.readerEndpoint.apply(
|
|
8554
|
+
foundationDatabaseEndpointHost,
|
|
8555
|
+
);
|
|
8556
|
+
// SST v4.17.1 creates a top-level LinkRef on the next tick and its Aurora
|
|
8557
|
+
// implementation unconditionally splits cluster endpoints. During a
|
|
8558
|
+
// governed refresh after an interrupted first deploy, those provider
|
|
8559
|
+
// outputs are legitimately absent. Preserve the normal link contract while
|
|
8560
|
+
// keeping that read-only recovery program total; a real deploy resolves the
|
|
8561
|
+
// exact endpoints before any discovery resource is created.
|
|
8562
|
+
database.getSSTLink = () => ({
|
|
8563
|
+
include: [
|
|
8564
|
+
sst.aws.permission({
|
|
8565
|
+
actions: ['secretsmanager:GetSecretValue'],
|
|
8566
|
+
resources: [database.secretArn],
|
|
8567
|
+
}),
|
|
8568
|
+
sst.aws.permission({
|
|
8569
|
+
actions: [
|
|
8570
|
+
'rds-data:BatchExecuteStatement',
|
|
8571
|
+
'rds-data:BeginTransaction',
|
|
8572
|
+
'rds-data:CommitTransaction',
|
|
8573
|
+
'rds-data:ExecuteStatement',
|
|
8574
|
+
'rds-data:RollbackTransaction',
|
|
8575
|
+
],
|
|
8576
|
+
resources: [database.clusterArn],
|
|
8577
|
+
}),
|
|
8578
|
+
],
|
|
8579
|
+
properties: {
|
|
8580
|
+
clusterArn: database.clusterArn,
|
|
8581
|
+
database: database.database,
|
|
8582
|
+
host: databaseClusterEndpoint,
|
|
8583
|
+
password: database.password,
|
|
8584
|
+
port: database.port,
|
|
8585
|
+
reader: databaseReaderEndpoint,
|
|
8586
|
+
secretArn: database.secretArn,
|
|
8587
|
+
username: database.username,
|
|
8588
|
+
},
|
|
8589
|
+
});
|
|
8037
8590
|
const foundationAccountId = aws.getCallerIdentityOutput({}).accountId;
|
|
8038
8591
|
const foundationPartition = aws.getPartitionOutput({});
|
|
8039
8592
|
const foundationRegion = aws.getRegionOutput({});
|
|
@@ -8096,6 +8649,7 @@ export function foundation(
|
|
|
8096
8649
|
},
|
|
8097
8650
|
],
|
|
8098
8651
|
resources: [alertTopic.arn],
|
|
8652
|
+
sid: 'AllowBudgetAlerts',
|
|
8099
8653
|
},
|
|
8100
8654
|
{
|
|
8101
8655
|
actions: ['sns:Publish'],
|
|
@@ -8109,6 +8663,7 @@ export function foundation(
|
|
|
8109
8663
|
type: 'Service',
|
|
8110
8664
|
}],
|
|
8111
8665
|
resources: [alertTopic.arn],
|
|
8666
|
+
sid: 'AllowCostAnomalyAlerts',
|
|
8112
8667
|
},
|
|
8113
8668
|
{
|
|
8114
8669
|
actions: ['sns:Publish'],
|
|
@@ -8131,6 +8686,7 @@ export function foundation(
|
|
|
8131
8686
|
type: 'Service',
|
|
8132
8687
|
}],
|
|
8133
8688
|
resources: [alertTopic.arn],
|
|
8689
|
+
sid: 'AllowRdsEventAlerts',
|
|
8134
8690
|
},
|
|
8135
8691
|
{
|
|
8136
8692
|
actions: ['sns:Publish'],
|
|
@@ -8155,6 +8711,7 @@ export function foundation(
|
|
|
8155
8711
|
},
|
|
8156
8712
|
],
|
|
8157
8713
|
resources: [alertTopic.arn],
|
|
8714
|
+
sid: 'AllowCloudWatchAlarmAlerts',
|
|
8158
8715
|
},
|
|
8159
8716
|
],
|
|
8160
8717
|
}).json,
|
|
@@ -8183,6 +8740,41 @@ export function foundation(
|
|
|
8183
8740
|
{ dependsOn: [alertTopicPolicy] },
|
|
8184
8741
|
)
|
|
8185
8742
|
: undefined;
|
|
8743
|
+
const accountCostAnomalyMonitor = isProduction
|
|
8744
|
+
? new aws.costexplorer.AnomalyMonitor(
|
|
8745
|
+
'FoundationAccountCostAnomalyMonitor',
|
|
8746
|
+
{
|
|
8747
|
+
monitorDimension: 'SERVICE',
|
|
8748
|
+
monitorType: 'DIMENSIONAL',
|
|
8749
|
+
name: '${name}-account-service-cost-anomalies',
|
|
8750
|
+
tags: {
|
|
8751
|
+
'ebk:cost-policy-digest': accountCostControl.policyDigest,
|
|
8752
|
+
'ebk:cost-scope': '${name}/account/production',
|
|
8753
|
+
'ebk:owner': accountCostControl.owner,
|
|
8754
|
+
},
|
|
8755
|
+
},
|
|
8756
|
+
)
|
|
8757
|
+
: undefined;
|
|
8758
|
+
const accountCostAnomalySubscription =
|
|
8759
|
+
accountCostControl.anomaly && accountCostAnomalyMonitor
|
|
8760
|
+
? new aws.costexplorer.AnomalySubscription(
|
|
8761
|
+
'FoundationAccountCostAnomalySubscription',
|
|
8762
|
+
{
|
|
8763
|
+
frequency: accountCostControl.anomaly.subscription.frequency,
|
|
8764
|
+
monitorArnLists: [accountCostAnomalyMonitor.arn],
|
|
8765
|
+
name: accountCostControl.anomaly.subscription.name,
|
|
8766
|
+
subscribers: [{ address: alertTopic.arn, type: 'SNS' }],
|
|
8767
|
+
tags: {
|
|
8768
|
+
'ebk:cost-policy-digest': accountCostControl.policyDigest,
|
|
8769
|
+
'ebk:cost-scope': '${name}/account/production',
|
|
8770
|
+
'ebk:owner': accountCostControl.owner,
|
|
8771
|
+
},
|
|
8772
|
+
thresholdExpression:
|
|
8773
|
+
accountCostControl.anomaly.subscription.thresholdExpression,
|
|
8774
|
+
},
|
|
8775
|
+
{ dependsOn: [accountCostAnomalyMonitor, alertTopicPolicy] },
|
|
8776
|
+
)
|
|
8777
|
+
: undefined;
|
|
8186
8778
|
const databaseEvents = isProduction
|
|
8187
8779
|
? new aws.rds.EventSubscription(
|
|
8188
8780
|
'FoundationDatabaseEvents',
|
|
@@ -8285,12 +8877,10 @@ export function foundation(
|
|
|
8285
8877
|
clusterId: cluster.id,
|
|
8286
8878
|
contractVersion: '1.0.0',
|
|
8287
8879
|
databaseClusterArn: database.clusterArn,
|
|
8288
|
-
databaseClusterEndpoint
|
|
8289
|
-
(endpoint: string) => endpoint.split(':')[0],
|
|
8290
|
-
),
|
|
8880
|
+
databaseClusterEndpoint,
|
|
8291
8881
|
databaseClusterId: database.id,
|
|
8292
8882
|
databaseClusterResourceId: database.nodes.cluster.clusterResourceId,
|
|
8293
|
-
databaseHost:
|
|
8883
|
+
databaseHost: databaseClusterEndpoint,
|
|
8294
8884
|
databaseMasterSecretArn: database.secretArn,
|
|
8295
8885
|
databasePort: database.port.apply(String),
|
|
8296
8886
|
eventBusArn: eventBus.arn,
|
|
@@ -8312,6 +8902,7 @@ export function foundation(
|
|
|
8312
8902
|
capabilities: STANDARD_FOUNDATION_CAPABILITIES,
|
|
8313
8903
|
clusterId: cluster.id,
|
|
8314
8904
|
contractVersion: '1.0.0',
|
|
8905
|
+
costAnomalySubscriptionArn: accountCostAnomalySubscription?.arn,
|
|
8315
8906
|
databaseCapacityAlarmName: databaseCapacityAlarm.name,
|
|
8316
8907
|
dashboardName: dashboard.dashboardName,
|
|
8317
8908
|
databaseClusterId: database.id,
|
|
@@ -8343,13 +8934,49 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
|
|
|
8343
8934
|
actualNotificationThresholdPercent: 80,
|
|
8344
8935
|
forecastNotificationThresholdPercent: 100,
|
|
8345
8936
|
kind: 'ebk-foundation-account-cost-policy',
|
|
8346
|
-
monthlyMaximumUsd: 1_000,
|
|
8937
|
+
monthlyMaximumUsd: deploymentProfile === 'starter' ? 250 : 1_000,
|
|
8347
8938
|
owner: 'replace-with-owner',
|
|
8348
8939
|
reviewAt: null,
|
|
8349
8940
|
schemaVersion: 1,
|
|
8350
8941
|
status: 'example',
|
|
8351
8942
|
workspace: name,
|
|
8352
8943
|
}),
|
|
8944
|
+
'packages/foundation/security/router-waf.json': json({
|
|
8945
|
+
kind: 'ebk-router-waf-policy',
|
|
8946
|
+
logging: {
|
|
8947
|
+
enabled: false,
|
|
8948
|
+
retention: '1 week',
|
|
8949
|
+
},
|
|
8950
|
+
rateLimitPerIp: 2_000,
|
|
8951
|
+
schemaVersion: 1,
|
|
8952
|
+
stages: {
|
|
8953
|
+
development: {
|
|
8954
|
+
access: 'allowlist',
|
|
8955
|
+
allowedCidrs: [],
|
|
8956
|
+
mode: 'block',
|
|
8957
|
+
},
|
|
8958
|
+
production: {
|
|
8959
|
+
access: 'public',
|
|
8960
|
+
allowedCidrs: [],
|
|
8961
|
+
mode: 'block',
|
|
8962
|
+
},
|
|
8963
|
+
staging: {
|
|
8964
|
+
access: 'allowlist',
|
|
8965
|
+
allowedCidrs: [],
|
|
8966
|
+
mode: 'block',
|
|
8967
|
+
},
|
|
8968
|
+
},
|
|
8969
|
+
}),
|
|
8970
|
+
'packages/foundation/security/router-waf-blocking-rehearsal.json': json({
|
|
8971
|
+
kind: 'ebk-router-waf-blocking-rehearsal',
|
|
8972
|
+
schemaVersion: 1,
|
|
8973
|
+
status: 'pending',
|
|
8974
|
+
}),
|
|
8975
|
+
'packages/foundation/security/router-waf-live-evidence.json': json({
|
|
8976
|
+
kind: 'ebk-router-waf-live-evidence',
|
|
8977
|
+
schemaVersion: 1,
|
|
8978
|
+
status: 'pending',
|
|
8979
|
+
}),
|
|
8353
8980
|
'packages/foundation/configuration/declarations.json': json({
|
|
8354
8981
|
project: 'foundation',
|
|
8355
8982
|
schemaVersion: 1,
|
|
@@ -8382,29 +9009,96 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
|
|
|
8382
9009
|
version: '4.17.1',
|
|
8383
9010
|
},
|
|
8384
9011
|
policy: {
|
|
9012
|
+
adoptedImports: [
|
|
9013
|
+
{
|
|
9014
|
+
authority: 'workload-role-account-baseline',
|
|
9015
|
+
logicalId: 'FoundationVpcNatInstanceProfile',
|
|
9016
|
+
privateIdentifierTemplate: 'ebk-{stage}-foundation-nat-profile',
|
|
9017
|
+
recoveryRunbookRef: 'docs/workload-role-account-baseline.md',
|
|
9018
|
+
removalBehavior: 'retain',
|
|
9019
|
+
resourceType: 'aws:iam/instanceProfile:InstanceProfile',
|
|
9020
|
+
},
|
|
9021
|
+
],
|
|
8385
9022
|
additiveLowRiskTypes: [
|
|
8386
9023
|
'aws:cloudwatch/dashboard:Dashboard',
|
|
8387
9024
|
'aws:cloudwatch/logGroup:LogGroup',
|
|
8388
9025
|
'aws:cloudwatch/metricAlarm:MetricAlarm',
|
|
8389
9026
|
],
|
|
9027
|
+
additiveReviewedTypes: [
|
|
9028
|
+
'aws:cloudwatch/eventArchive:EventArchive',
|
|
9029
|
+
'aws:cloudwatch/eventBus:EventBus',
|
|
9030
|
+
'aws:cognito/userPoolDomain:UserPoolDomain',
|
|
9031
|
+
'aws:ec2/defaultSecurityGroup:DefaultSecurityGroup',
|
|
9032
|
+
'aws:ec2/internetGateway:InternetGateway',
|
|
9033
|
+
'aws:ec2/routeTable:RouteTable',
|
|
9034
|
+
'aws:ec2/routeTableAssociation:RouteTableAssociation',
|
|
9035
|
+
'aws:ec2/securityGroup:SecurityGroup',
|
|
9036
|
+
'aws:ec2/subnet:Subnet',
|
|
9037
|
+
'aws:ec2/vpc:Vpc',
|
|
9038
|
+
'aws:ecs/cluster:Cluster',
|
|
9039
|
+
'aws:ecs/clusterCapacityProviders:ClusterCapacityProviders',
|
|
9040
|
+
'aws:rds/clusterParameterGroup:ClusterParameterGroup',
|
|
9041
|
+
'aws:rds/eventSubscription:EventSubscription',
|
|
9042
|
+
'aws:rds/parameterGroup:ParameterGroup',
|
|
9043
|
+
'aws:rds/subnetGroup:SubnetGroup',
|
|
9044
|
+
'aws:servicediscovery/privateDnsNamespace:PrivateDnsNamespace',
|
|
9045
|
+
'aws:sns/topic:Topic',
|
|
9046
|
+
'aws:sns/topicPolicy:TopicPolicy',
|
|
9047
|
+
'aws:wafv2/webAclLoggingConfiguration:WebAclLoggingConfiguration',
|
|
9048
|
+
'pulumi-nodejs:dynamic:Resource',
|
|
9049
|
+
'pulumi:providers:aws',
|
|
9050
|
+
'pulumi:providers:pulumi-nodejs',
|
|
9051
|
+
'pulumi:providers:random',
|
|
9052
|
+
'pulumi:pulumi:Stack',
|
|
9053
|
+
'sst:aws:Aurora',
|
|
9054
|
+
'sst:aws:Bus',
|
|
9055
|
+
'sst:aws:CDN',
|
|
9056
|
+
'sst:aws:Cluster',
|
|
9057
|
+
'sst:aws:CognitoUserPool',
|
|
9058
|
+
'sst:aws:Router',
|
|
9059
|
+
'sst:aws:SnsTopic',
|
|
9060
|
+
'sst:aws:Vpc',
|
|
9061
|
+
'sst:sst:LinkRef',
|
|
9062
|
+
'sst:sst:Version',
|
|
9063
|
+
],
|
|
8390
9064
|
costOrQuotaTypes: [
|
|
8391
9065
|
'aws:apigatewayv2/api:Api',
|
|
8392
9066
|
'aws:budgets/budget:Budget',
|
|
9067
|
+
'aws:cloudfront/cachePolicy:CachePolicy',
|
|
8393
9068
|
'aws:cloudfront/distribution:Distribution',
|
|
9069
|
+
'aws:cloudfront/function:Function',
|
|
9070
|
+
'aws:cloudfront/keyValueStore:KeyValueStore',
|
|
9071
|
+
'aws:costexplorer/anomalyMonitor:AnomalyMonitor',
|
|
9072
|
+
'aws:costexplorer/anomalySubscription:AnomalySubscription',
|
|
8394
9073
|
'aws:cognito/userPool:UserPool',
|
|
9074
|
+
'aws:ec2/eip:Eip',
|
|
9075
|
+
'aws:ec2/eipAssociation:EipAssociation',
|
|
9076
|
+
'aws:ec2/instance:Instance',
|
|
8395
9077
|
'aws:ec2/natGateway:NatGateway',
|
|
8396
9078
|
'aws:rds/cluster:Cluster',
|
|
9079
|
+
'aws:rds/clusterInstance:ClusterInstance',
|
|
9080
|
+
'aws:wafv2/ipSet:IpSet',
|
|
9081
|
+
'aws:wafv2/webAcl:WebAcl',
|
|
8397
9082
|
],
|
|
8398
9083
|
failClosedOnUnknown: true,
|
|
8399
9084
|
protectedPlanAuthority: 'deploy-role-residual',
|
|
8400
9085
|
standingPolicyRiskClasses: ['no-change', 'additive-low-risk'],
|
|
8401
9086
|
statefulTypes: [
|
|
9087
|
+
'aws:cloudwatch/eventArchive:EventArchive',
|
|
9088
|
+
'aws:cloudwatch/eventBus:EventBus',
|
|
8402
9089
|
'aws:cognito/userPool:UserPool',
|
|
8403
9090
|
'aws:dynamodb/table:Table',
|
|
8404
9091
|
'aws:events/eventBus:EventBus',
|
|
8405
9092
|
'aws:kms/key:Key',
|
|
8406
9093
|
'aws:rds/cluster:Cluster',
|
|
8407
9094
|
'aws:s3/bucket:Bucket',
|
|
9095
|
+
'aws:secretsmanager/secret:Secret',
|
|
9096
|
+
'aws:secretsmanager/secretVersion:SecretVersion',
|
|
9097
|
+
'aws:sns/topic:Topic',
|
|
9098
|
+
'aws:ssm/parameter:Parameter',
|
|
9099
|
+
'aws:wafv2/ipSet:IpSet',
|
|
9100
|
+
'aws:wafv2/webAcl:WebAcl',
|
|
9101
|
+
'random:index/randomPassword:RandomPassword',
|
|
8408
9102
|
],
|
|
8409
9103
|
},
|
|
8410
9104
|
project: 'foundation',
|
|
@@ -8502,6 +9196,13 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
|
|
|
8502
9196
|
executor: '@empire-builder-kit/nx:foundation-profile-preflight',
|
|
8503
9197
|
options: { stage: 'development' },
|
|
8504
9198
|
},
|
|
9199
|
+
'router-waf-rehearsal': {
|
|
9200
|
+
cache: false,
|
|
9201
|
+
executor: '@empire-builder-kit/nx:router-waf-rehearsal',
|
|
9202
|
+
outputs: [
|
|
9203
|
+
`{projectRoot}/security/router-waf-blocking-rehearsal.json`,
|
|
9204
|
+
],
|
|
9205
|
+
},
|
|
8505
9206
|
typecheck: {
|
|
8506
9207
|
executor: 'nx:run-commands',
|
|
8507
9208
|
options: {
|
|
@@ -8529,6 +9230,11 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
|
|
|
8529
9230
|
}),
|
|
8530
9231
|
'packages/foundation/scripts/verify-foundation.mjs': `import { readFileSync } from 'node:fs';
|
|
8531
9232
|
|
|
9233
|
+
import {
|
|
9234
|
+
normalizeRouterWafPolicy,
|
|
9235
|
+
reconcileRouterWafBlockingRehearsal,
|
|
9236
|
+
} from '@empire-builder-kit/nx/foundation';
|
|
9237
|
+
|
|
8532
9238
|
const descriptor = JSON.parse(
|
|
8533
9239
|
readFileSync(new URL('../contracts/foundation.json', import.meta.url), 'utf8'),
|
|
8534
9240
|
);
|
|
@@ -8544,6 +9250,33 @@ const frontendSecurityHeaders = JSON.parse(
|
|
|
8544
9250
|
'utf8',
|
|
8545
9251
|
),
|
|
8546
9252
|
);
|
|
9253
|
+
const routerWafPolicy = normalizeRouterWafPolicy(
|
|
9254
|
+
JSON.parse(
|
|
9255
|
+
readFileSync(
|
|
9256
|
+
new URL('../security/router-waf.json', import.meta.url),
|
|
9257
|
+
'utf8',
|
|
9258
|
+
),
|
|
9259
|
+
),
|
|
9260
|
+
);
|
|
9261
|
+
reconcileRouterWafBlockingRehearsal({
|
|
9262
|
+
evidence: JSON.parse(
|
|
9263
|
+
readFileSync(
|
|
9264
|
+
new URL('../security/router-waf-live-evidence.json', import.meta.url),
|
|
9265
|
+
'utf8',
|
|
9266
|
+
),
|
|
9267
|
+
),
|
|
9268
|
+
policy: routerWafPolicy,
|
|
9269
|
+
receipt:
|
|
9270
|
+
JSON.parse(
|
|
9271
|
+
readFileSync(
|
|
9272
|
+
new URL(
|
|
9273
|
+
'../security/router-waf-blocking-rehearsal.json',
|
|
9274
|
+
import.meta.url,
|
|
9275
|
+
),
|
|
9276
|
+
'utf8',
|
|
9277
|
+
),
|
|
9278
|
+
),
|
|
9279
|
+
});
|
|
8547
9280
|
const expectedCapabilities = [
|
|
8548
9281
|
'network',
|
|
8549
9282
|
'routing',
|
|
@@ -8559,6 +9292,13 @@ const expectedCapabilities = [
|
|
|
8559
9292
|
if (descriptor.contractVersion !== '1.0.0') {
|
|
8560
9293
|
throw new Error('Foundation contractVersion must be 1.0.0.');
|
|
8561
9294
|
}
|
|
9295
|
+
if (
|
|
9296
|
+
routerWafPolicy.stages.development.access !== 'allowlist' ||
|
|
9297
|
+
routerWafPolicy.stages.staging.access !== 'allowlist' ||
|
|
9298
|
+
routerWafPolicy.stages.production.mode !== 'block'
|
|
9299
|
+
) {
|
|
9300
|
+
throw new Error('Foundation Router WAF stage defaults are invalid.');
|
|
9301
|
+
}
|
|
8562
9302
|
if (JSON.stringify(descriptor.capabilities) !== JSON.stringify(expectedCapabilities)) {
|
|
8563
9303
|
throw new Error('Foundation capability descriptor does not match the v1 standard.');
|
|
8564
9304
|
}
|
|
@@ -8567,7 +9307,7 @@ if (
|
|
|
8567
9307
|
costs.aurora?.engineVersion !== '${index_js_4.FOUNDATION_AURORA_ENGINE_VERSION}' ||
|
|
8568
9308
|
costs.productionProfileSource !== 'ebk.repository-policy.json' ||
|
|
8569
9309
|
costs.productionProfiles?.standard?.aurora?.min !== '2 ACU' ||
|
|
8570
|
-
costs.productionProfiles?.['cost-optimized']?.aurora?.min !== '0 ACU' ||
|
|
9310
|
+
costs.productionProfiles?.['cost-optimized']?.aurora?.min !== '0.5 ACU' ||
|
|
8571
9311
|
!costs.stages?.development ||
|
|
8572
9312
|
!costs.stages?.staging ||
|
|
8573
9313
|
costs.stages?.production?.dataRetention !== 'retained-and-protected'
|
|
@@ -8623,16 +9363,15 @@ export default $config({
|
|
|
8623
9363
|
providers: {
|
|
8624
9364
|
aws: {
|
|
8625
9365
|
defaultTags: {
|
|
9366
|
+
// SST contributes sst:app and sst:stage. Keep the provider baseline
|
|
9367
|
+
// at eight tags so generated S3 asset objects remain within AWS's
|
|
9368
|
+
// hard ten-tag object limit. Richer performance tags stay explicit.
|
|
8626
9369
|
tags: {
|
|
8627
9370
|
'ebk:cost-allocation': 'foundation',
|
|
8628
9371
|
'ebk:cost-scope': '${name}/foundation/' + stage,
|
|
8629
9372
|
'ebk:framework-version': '${frameworkVersion}',
|
|
8630
9373
|
'ebk:managed-by': 'empire-builder-kit',
|
|
8631
9374
|
'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
9375
|
'ebk:slice': 'foundation',
|
|
8637
9376
|
'ebk:stage': stage,
|
|
8638
9377
|
'ebk:workspace': '${name}',
|