@empire-builder-kit/nx 1.0.0-beta.4 → 1.0.0-beta.6
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/README.md +13 -4
- package/executors.json +20 -0
- package/generators.json +5 -0
- package/package.json +15 -1
- package/src/executors/deployment-plan/deployment-plan.d.ts +9 -0
- package/src/executors/deployment-plan/deployment-plan.js +50 -0
- package/src/executors/deployment-plan/deployment-plan.js.map +1 -0
- package/src/executors/deployment-plan/schema.json +24 -0
- package/src/executors/foundation-preflight/foundation-preflight.js +22 -0
- package/src/executors/foundation-preflight/foundation-preflight.js.map +1 -1
- package/src/executors/foundation-profile-preflight/foundation-profile-preflight.d.ts +8 -0
- package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js +41 -0
- package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js.map +1 -0
- package/src/executors/foundation-profile-preflight/schema.json +21 -0
- package/src/executors/github-bootstrap/github-bootstrap.d.ts +8 -0
- package/src/executors/github-bootstrap/github-bootstrap.js +27 -0
- package/src/executors/github-bootstrap/github-bootstrap.js.map +1 -0
- package/src/executors/github-bootstrap/schema.json +25 -0
- package/src/executors/sst-lifecycle/run-sst-command.d.ts +2 -0
- package/src/executors/sst-lifecycle/run-sst-command.js +58 -5
- package/src/executors/sst-lifecycle/run-sst-command.js.map +1 -1
- package/src/executors/work-markers/schema.json +14 -0
- package/src/executors/work-markers/work-markers.d.ts +6 -0
- package/src/executors/work-markers/work-markers.js +20 -0
- package/src/executors/work-markers/work-markers.js.map +1 -0
- package/src/generators/preset/workspace-files.d.ts +1 -1
- package/src/generators/preset/workspace-files.js +702 -38
- package/src/generators/preset/workspace-files.js.map +1 -1
- package/src/generators/repository-policy/repository-policy.d.ts +4 -0
- package/src/generators/repository-policy/repository-policy.js +55 -0
- package/src/generators/repository-policy/repository-policy.js.map +1 -0
- package/src/generators/repository-policy/schema.d.ts +16 -0
- package/src/generators/repository-policy/schema.json +41 -0
- package/src/generators/slice/slice.js +22 -0
- package/src/generators/slice/slice.js.map +1 -1
- package/src/git-policy/action-pins.d.ts +28 -0
- package/src/git-policy/action-pins.js +35 -0
- package/src/git-policy/action-pins.js.map +1 -0
- package/src/git-policy/github-bootstrap.d.ts +20 -0
- package/src/git-policy/github-bootstrap.js +676 -0
- package/src/git-policy/github-bootstrap.js.map +1 -0
- package/src/git-policy/index.d.ts +3 -0
- package/src/git-policy/index.js +7 -0
- package/src/git-policy/index.js.map +1 -0
- package/src/git-policy/routing.d.ts +15 -0
- package/src/git-policy/routing.js +77 -0
- package/src/git-policy/routing.js.map +1 -0
- package/src/index.d.ts +3 -0
- package/src/index.js +3 -0
- package/src/index.js.map +1 -1
- package/src/repository-policy/aurora-preflight.d.ts +15 -0
- package/src/repository-policy/aurora-preflight.js +45 -0
- package/src/repository-policy/aurora-preflight.js.map +1 -0
- package/src/repository-policy/deployment-plan.d.ts +24 -0
- package/src/repository-policy/deployment-plan.js +124 -0
- package/src/repository-policy/deployment-plan.js.map +1 -0
- package/src/repository-policy/foundation-profile.d.ts +20 -0
- package/src/repository-policy/foundation-profile.js +45 -0
- package/src/repository-policy/foundation-profile.js.map +1 -0
- package/src/repository-policy/index.d.ts +4 -0
- package/src/repository-policy/index.js +8 -0
- package/src/repository-policy/index.js.map +1 -0
- package/src/repository-policy/policy.d.ts +53 -0
- package/src/repository-policy/policy.js +250 -0
- package/src/repository-policy/policy.js.map +1 -0
- package/src/schemas/repository-policy-v1.json +42 -0
- package/src/work-markers/index.d.ts +1 -0
- package/src/work-markers/index.js +5 -0
- package/src/work-markers/index.js.map +1 -0
- package/src/work-markers/work-markers.d.ts +39 -0
- package/src/work-markers/work-markers.js +359 -0
- package/src/work-markers/work-markers.js.map +1 -0
|
@@ -5,6 +5,8 @@ exports.renderWorkspaceFiles = renderWorkspaceFiles;
|
|
|
5
5
|
exports.renderWorkspaceRootFilePlan = renderWorkspaceRootFilePlan;
|
|
6
6
|
exports.renderFoundationFilePlan = renderFoundationFilePlan;
|
|
7
7
|
const index_js_1 = require("../../ownership/index.js");
|
|
8
|
+
const index_js_2 = require("../../git-policy/index.js");
|
|
9
|
+
const index_js_3 = require("../../repository-policy/index.js");
|
|
8
10
|
const TOOL_VERSIONS = {
|
|
9
11
|
eslint: '^9.8.0',
|
|
10
12
|
eslintJs: '^9.8.0',
|
|
@@ -25,8 +27,15 @@ const FOUNDATION_OWNERSHIP_PATH = `${FOUNDATION_ROOT}.ebk/ownership.json`;
|
|
|
25
27
|
const FOUNDATION_TOOL_OWNED_PATHS = new Set(['sst-env.d.ts']);
|
|
26
28
|
const WORKSPACE_OWNERSHIP_PATH = '.ebk/ownership.json';
|
|
27
29
|
const WORKSPACE_SEEDED_PATHS = new Set([
|
|
30
|
+
'.github/CODEOWNERS',
|
|
31
|
+
'AGENTS.md',
|
|
32
|
+
'CLAUDE.md',
|
|
28
33
|
'README.md',
|
|
29
34
|
'docs/architecture.md',
|
|
35
|
+
'docs/git-workflow.md',
|
|
36
|
+
'docs/repository-operating-policy.md',
|
|
37
|
+
'docs/work-markers.md',
|
|
38
|
+
'ebk.repository-policy.json',
|
|
30
39
|
'package.json',
|
|
31
40
|
]);
|
|
32
41
|
exports.WORKSPACE_PROJECT_MANAGED_JSON_POINTERS = [
|
|
@@ -34,10 +43,14 @@ exports.WORKSPACE_PROJECT_MANAGED_JSON_POINTERS = [
|
|
|
34
43
|
'/$schema',
|
|
35
44
|
'/projectType',
|
|
36
45
|
'/tags',
|
|
46
|
+
'/targets/deployment-plan',
|
|
37
47
|
'/targets/dev',
|
|
38
48
|
'/targets/dev-doctor',
|
|
39
49
|
'/targets/dev-setup',
|
|
50
|
+
'/targets/github-bootstrap',
|
|
51
|
+
'/targets/install-git-hooks',
|
|
40
52
|
'/targets/verify',
|
|
53
|
+
'/targets/work-markers',
|
|
41
54
|
'/targets/workspace-health',
|
|
42
55
|
];
|
|
43
56
|
const FOUNDATION_SEEDED_PATHS = new Set([
|
|
@@ -101,13 +114,24 @@ function createWorkspaceOwnership(files, frameworkVersion) {
|
|
|
101
114
|
}
|
|
102
115
|
function renderWorkspaceFiles(options) {
|
|
103
116
|
const { frameworkVersion, name } = options;
|
|
117
|
+
const checkoutAction = (0, index_js_2.pinnedAction)(index_js_2.GITHUB_ACTION_PINS.checkout);
|
|
118
|
+
const configureAwsAction = (0, index_js_2.pinnedAction)(index_js_2.GITHUB_ACTION_PINS.configureAwsCredentials);
|
|
119
|
+
const pnpmAction = (0, index_js_2.pinnedAction)(index_js_2.GITHUB_ACTION_PINS.pnpmSetup);
|
|
120
|
+
const setupNodeAction = (0, index_js_2.pinnedAction)(index_js_2.GITHUB_ACTION_PINS.setupNode);
|
|
104
121
|
const files = {
|
|
122
|
+
'.github/CODEOWNERS': `# Replace this placeholder with the GitHub user or team that owns protected source.
|
|
123
|
+
# GitHub bootstrap remains blocked until a non-placeholder catch-all owner is committed.
|
|
124
|
+
* @REPLACE_WITH_GITHUB_CODE_OWNER
|
|
125
|
+
`,
|
|
126
|
+
'.github/actions-pins.json': json(index_js_2.GITHUB_ACTION_PINS),
|
|
105
127
|
'.github/workflows/ci.yml': `name: Verify
|
|
106
128
|
|
|
107
129
|
on:
|
|
108
130
|
pull_request:
|
|
131
|
+
branches: [develop, main]
|
|
109
132
|
push:
|
|
110
|
-
branches: [main]
|
|
133
|
+
branches: [develop, main]
|
|
134
|
+
workflow_dispatch:
|
|
111
135
|
|
|
112
136
|
concurrency:
|
|
113
137
|
group: verify-\${{ github.ref }}
|
|
@@ -117,12 +141,29 @@ permissions:
|
|
|
117
141
|
contents: read
|
|
118
142
|
|
|
119
143
|
jobs:
|
|
144
|
+
policy:
|
|
145
|
+
name: policy
|
|
146
|
+
runs-on: ubuntu-latest
|
|
147
|
+
steps:
|
|
148
|
+
- uses: ${checkoutAction}
|
|
149
|
+
with:
|
|
150
|
+
fetch-depth: 0
|
|
151
|
+
ref: \${{ github.event.pull_request.head.sha || github.sha }}
|
|
152
|
+
- uses: ${pnpmAction}
|
|
153
|
+
- uses: ${setupNodeAction}
|
|
154
|
+
with:
|
|
155
|
+
node-version: 22
|
|
156
|
+
cache: pnpm
|
|
157
|
+
- run: pnpm install --frozen-lockfile
|
|
158
|
+
- run: node tools/ebk-git-policy.mjs
|
|
159
|
+
|
|
120
160
|
verify:
|
|
161
|
+
name: verify
|
|
121
162
|
runs-on: ubuntu-latest
|
|
122
163
|
steps:
|
|
123
|
-
- uses:
|
|
124
|
-
- uses:
|
|
125
|
-
- uses:
|
|
164
|
+
- uses: ${checkoutAction}
|
|
165
|
+
- uses: ${pnpmAction}
|
|
166
|
+
- uses: ${setupNodeAction}
|
|
126
167
|
with:
|
|
127
168
|
node-version: 22
|
|
128
169
|
cache: pnpm
|
|
@@ -133,49 +174,289 @@ jobs:
|
|
|
133
174
|
'.github/workflows/deploy.yml': `name: Deploy
|
|
134
175
|
|
|
135
176
|
on:
|
|
177
|
+
push:
|
|
178
|
+
branches: [develop, main]
|
|
136
179
|
workflow_dispatch:
|
|
137
180
|
inputs:
|
|
138
|
-
stage:
|
|
139
|
-
description: Deployment stage
|
|
140
|
-
required: true
|
|
141
|
-
default: development
|
|
142
|
-
type: choice
|
|
143
|
-
options: [development, staging, production]
|
|
144
181
|
action:
|
|
145
182
|
description: Lifecycle action
|
|
146
183
|
required: true
|
|
147
184
|
default: deploy
|
|
148
185
|
type: choice
|
|
149
186
|
options: [deploy, refresh, unlock, remove]
|
|
187
|
+
project:
|
|
188
|
+
description: Required registered project for explicit unlock or remove
|
|
189
|
+
required: false
|
|
190
|
+
type: string
|
|
150
191
|
|
|
151
192
|
concurrency:
|
|
152
|
-
group: deploy-\${{
|
|
193
|
+
group: deploy-\${{ github.ref }}
|
|
153
194
|
cancel-in-progress: false
|
|
154
195
|
|
|
155
196
|
permissions:
|
|
156
197
|
contents: read
|
|
157
|
-
id-token: write
|
|
158
198
|
|
|
159
199
|
jobs:
|
|
160
|
-
|
|
200
|
+
route:
|
|
201
|
+
runs-on: ubuntu-latest
|
|
202
|
+
steps:
|
|
203
|
+
- name: Validate protected ref
|
|
204
|
+
env:
|
|
205
|
+
EBK_PROTECTED_REF: \${{ github.ref }}
|
|
206
|
+
run: |
|
|
207
|
+
case "$EBK_PROTECTED_REF" in
|
|
208
|
+
refs/heads/develop|refs/heads/main) ;;
|
|
209
|
+
*) echo 'Protected lifecycle must be dispatched from develop or main'; exit 1 ;;
|
|
210
|
+
esac
|
|
211
|
+
|
|
212
|
+
plan-staging:
|
|
213
|
+
needs: route
|
|
214
|
+
if: github.ref == 'refs/heads/develop'
|
|
215
|
+
runs-on: ubuntu-latest
|
|
216
|
+
outputs:
|
|
217
|
+
foundation_project: \${{ steps.plan.outputs.foundation_project }}
|
|
218
|
+
has_slices: \${{ steps.plan.outputs.has_slices }}
|
|
219
|
+
ordered_projects: \${{ steps.plan.outputs.ordered_projects }}
|
|
220
|
+
slice_projects: \${{ steps.plan.outputs.slice_projects }}
|
|
221
|
+
steps:
|
|
222
|
+
- uses: ${checkoutAction}
|
|
223
|
+
- uses: ${pnpmAction}
|
|
224
|
+
- uses: ${setupNodeAction}
|
|
225
|
+
with:
|
|
226
|
+
node-version: 22
|
|
227
|
+
cache: pnpm
|
|
228
|
+
- run: pnpm install --frozen-lockfile
|
|
229
|
+
- id: plan
|
|
230
|
+
env:
|
|
231
|
+
EBK_ACTION: \${{ github.event_name == 'push' && 'deploy' || inputs.action }}
|
|
232
|
+
EBK_PROJECT: \${{ inputs.project || '' }}
|
|
233
|
+
run: |
|
|
234
|
+
args=("--action=$EBK_ACTION" "--stage=staging")
|
|
235
|
+
if [ -n "$EBK_PROJECT" ]; then args+=("--project=$EBK_PROJECT"); fi
|
|
236
|
+
pnpm nx run ${name}:deployment-plan -- "\${args[@]}"
|
|
237
|
+
|
|
238
|
+
plan-production:
|
|
239
|
+
needs: route
|
|
161
240
|
if: github.ref == 'refs/heads/main'
|
|
162
|
-
|
|
241
|
+
runs-on: ubuntu-latest
|
|
242
|
+
outputs:
|
|
243
|
+
foundation_project: \${{ steps.plan.outputs.foundation_project }}
|
|
244
|
+
foundation_profile: \${{ steps.plan.outputs.foundation_profile }}
|
|
245
|
+
has_slices: \${{ steps.plan.outputs.has_slices }}
|
|
246
|
+
ordered_projects: \${{ steps.plan.outputs.ordered_projects }}
|
|
247
|
+
slice_projects: \${{ steps.plan.outputs.slice_projects }}
|
|
248
|
+
steps:
|
|
249
|
+
- uses: ${checkoutAction}
|
|
250
|
+
- uses: ${pnpmAction}
|
|
251
|
+
- uses: ${setupNodeAction}
|
|
252
|
+
with:
|
|
253
|
+
node-version: 22
|
|
254
|
+
cache: pnpm
|
|
255
|
+
- run: pnpm install --frozen-lockfile
|
|
256
|
+
- id: plan
|
|
257
|
+
env:
|
|
258
|
+
EBK_ACTION: \${{ github.event_name == 'push' && 'deploy' || inputs.action }}
|
|
259
|
+
EBK_PROJECT: \${{ inputs.project || '' }}
|
|
260
|
+
run: |
|
|
261
|
+
args=("--action=$EBK_ACTION" "--stage=production")
|
|
262
|
+
if [ -n "$EBK_PROJECT" ]; then args+=("--project=$EBK_PROJECT"); fi
|
|
263
|
+
pnpm nx run ${name}:deployment-plan -- "\${args[@]}"
|
|
264
|
+
|
|
265
|
+
deploy-staging:
|
|
266
|
+
name: deploy staging
|
|
267
|
+
needs: plan-staging
|
|
268
|
+
environment: staging
|
|
269
|
+
permissions:
|
|
270
|
+
contents: read
|
|
271
|
+
id-token: write
|
|
272
|
+
env:
|
|
273
|
+
AWS_DEPLOY_ROLE_ARN: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
|
274
|
+
AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
|
|
275
|
+
AWS_REGION: \${{ vars.AWS_REGION }}
|
|
276
|
+
EBK_ACTION: \${{ github.event_name == 'push' && 'deploy' || inputs.action }}
|
|
277
|
+
EBK_STAGE: staging
|
|
278
|
+
FOUNDATION_PROJECT: \${{ needs.plan-staging.outputs.foundation_project }}
|
|
279
|
+
ORDERED_PROJECTS: \${{ needs.plan-staging.outputs.ordered_projects }}
|
|
280
|
+
SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
|
|
281
|
+
runs-on: ubuntu-latest
|
|
282
|
+
steps:
|
|
283
|
+
- uses: ${checkoutAction}
|
|
284
|
+
- name: Validate environment configuration
|
|
285
|
+
run: |
|
|
286
|
+
test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'staging AWS_DEPLOY_ROLE_ARN is required'; exit 1; }
|
|
287
|
+
test -n "$AWS_EXPECTED_ACCOUNT_ID" || { echo 'staging AWS_EXPECTED_ACCOUNT_ID is required'; exit 1; }
|
|
288
|
+
test -n "$AWS_REGION" || { echo 'staging AWS_REGION is required'; exit 1; }
|
|
289
|
+
case "$AWS_DEPLOY_ROLE_ARN" in
|
|
290
|
+
arn:aws:iam::$AWS_EXPECTED_ACCOUNT_ID:role/*) ;;
|
|
291
|
+
*) echo 'Deployment role does not belong to the expected staging account'; exit 1 ;;
|
|
292
|
+
esac
|
|
293
|
+
- uses: ${configureAwsAction}
|
|
294
|
+
with:
|
|
295
|
+
role-to-assume: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
|
296
|
+
role-session-name: ebk-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
297
|
+
aws-region: \${{ vars.AWS_REGION }}
|
|
298
|
+
- name: Verify assumed account
|
|
299
|
+
run: test "$(aws sts get-caller-identity --query Account --output text)" = "$AWS_EXPECTED_ACCOUNT_ID"
|
|
300
|
+
- uses: ${pnpmAction}
|
|
301
|
+
- uses: ${setupNodeAction}
|
|
302
|
+
with:
|
|
303
|
+
node-version: 22
|
|
304
|
+
cache: pnpm
|
|
305
|
+
- run: pnpm install --frozen-lockfile
|
|
306
|
+
- name: Run ordered lifecycle
|
|
307
|
+
run: |
|
|
308
|
+
if [ -n "$FOUNDATION_PROJECT" ]; then
|
|
309
|
+
pnpm nx run "$FOUNDATION_PROJECT:$EBK_ACTION" --stage="$EBK_STAGE"
|
|
310
|
+
fi
|
|
311
|
+
if [ -n "$SLICE_PROJECTS" ]; then
|
|
312
|
+
pnpm nx run-many -t "$EBK_ACTION" --projects="$SLICE_PROJECTS" --stage="$EBK_STAGE" --parallel=1
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
deploy-production:
|
|
316
|
+
name: deploy production
|
|
317
|
+
needs: plan-production
|
|
318
|
+
environment: production
|
|
319
|
+
permissions:
|
|
320
|
+
contents: read
|
|
321
|
+
id-token: write
|
|
163
322
|
env:
|
|
323
|
+
AWS_DEPLOY_ROLE_ARN: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
|
324
|
+
AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
|
|
325
|
+
AWS_REGION: \${{ vars.AWS_REGION }}
|
|
326
|
+
EBK_ACTION: \${{ github.event_name == 'push' && 'deploy' || inputs.action }}
|
|
164
327
|
EBK_ALLOW_PRODUCTION_REMOVE: \${{ vars.EBK_ALLOW_PRODUCTION_REMOVE || 'false' }}
|
|
328
|
+
EBK_STAGE: production
|
|
329
|
+
FOUNDATION_PROJECT: \${{ needs.plan-production.outputs.foundation_project }}
|
|
330
|
+
ORDERED_PROJECTS: \${{ needs.plan-production.outputs.ordered_projects }}
|
|
331
|
+
SLICE_PROJECTS: \${{ needs.plan-production.outputs.slice_projects }}
|
|
165
332
|
runs-on: ubuntu-latest
|
|
166
333
|
steps:
|
|
167
|
-
- uses:
|
|
168
|
-
-
|
|
334
|
+
- uses: ${checkoutAction}
|
|
335
|
+
- name: Validate environment configuration
|
|
336
|
+
run: |
|
|
337
|
+
test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'production AWS_DEPLOY_ROLE_ARN is required'; exit 1; }
|
|
338
|
+
test -n "$AWS_EXPECTED_ACCOUNT_ID" || { echo 'production AWS_EXPECTED_ACCOUNT_ID is required'; exit 1; }
|
|
339
|
+
test -n "$AWS_REGION" || { echo 'production AWS_REGION is required'; exit 1; }
|
|
340
|
+
case "$AWS_DEPLOY_ROLE_ARN" in
|
|
341
|
+
arn:aws:iam::$AWS_EXPECTED_ACCOUNT_ID:role/*) ;;
|
|
342
|
+
*) echo 'Deployment role does not belong to the expected production account'; exit 1 ;;
|
|
343
|
+
esac
|
|
344
|
+
- uses: ${configureAwsAction}
|
|
169
345
|
with:
|
|
170
346
|
role-to-assume: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
-
|
|
347
|
+
role-session-name: ebk-\${{ github.run_id }}-\${{ github.run_attempt }}
|
|
348
|
+
aws-region: \${{ vars.AWS_REGION }}
|
|
349
|
+
- name: Verify assumed account
|
|
350
|
+
run: test "$(aws sts get-caller-identity --query Account --output text)" = "$AWS_EXPECTED_ACCOUNT_ID"
|
|
351
|
+
- uses: ${pnpmAction}
|
|
352
|
+
- uses: ${setupNodeAction}
|
|
174
353
|
with:
|
|
175
354
|
node-version: 22
|
|
176
355
|
cache: pnpm
|
|
177
356
|
- run: pnpm install --frozen-lockfile
|
|
178
|
-
-
|
|
357
|
+
- name: Run ordered lifecycle
|
|
358
|
+
run: |
|
|
359
|
+
if [ -n "$FOUNDATION_PROJECT" ]; then
|
|
360
|
+
pnpm nx run "$FOUNDATION_PROJECT:$EBK_ACTION" --stage="$EBK_STAGE"
|
|
361
|
+
fi
|
|
362
|
+
if [ -n "$SLICE_PROJECTS" ]; then
|
|
363
|
+
pnpm nx run-many -t "$EBK_ACTION" --projects="$SLICE_PROJECTS" --stage="$EBK_STAGE" --parallel=1
|
|
364
|
+
fi
|
|
365
|
+
`,
|
|
366
|
+
'.github/workflows/sync-back.yml': `name: Sync production history
|
|
367
|
+
|
|
368
|
+
on:
|
|
369
|
+
workflow_run:
|
|
370
|
+
workflows: [Deploy]
|
|
371
|
+
types: [completed]
|
|
372
|
+
|
|
373
|
+
permissions:
|
|
374
|
+
contents: read
|
|
375
|
+
pull-requests: write
|
|
376
|
+
|
|
377
|
+
jobs:
|
|
378
|
+
sync-back:
|
|
379
|
+
if: >-
|
|
380
|
+
github.event.workflow_run.conclusion == 'success' &&
|
|
381
|
+
github.event.workflow_run.head_branch == 'main'
|
|
382
|
+
runs-on: ubuntu-latest
|
|
383
|
+
steps:
|
|
384
|
+
- name: Open or locate main-to-develop pull request
|
|
385
|
+
env:
|
|
386
|
+
GH_TOKEN: \${{ github.token }}
|
|
387
|
+
GH_REPO: \${{ github.repository }}
|
|
388
|
+
run: |
|
|
389
|
+
number=$(gh pr list --state open --head main --base develop --json number --jq '.[0].number // empty')
|
|
390
|
+
if [ -z "$number" ]; then
|
|
391
|
+
gh pr create \
|
|
392
|
+
--head main \
|
|
393
|
+
--base develop \
|
|
394
|
+
--title 'chore: sync production history to develop' \
|
|
395
|
+
--body 'Managed EBK sync-back. Review and merge; this workflow never pushes or auto-merges.'
|
|
396
|
+
number=$(gh pr list --state open --head main --base develop --json number --jq '.[0].number')
|
|
397
|
+
fi
|
|
398
|
+
echo "Sync-back pull request #$number is ready for review."
|
|
399
|
+
|
|
400
|
+
head_sha=$(gh pr view "$number" --json headRefOid --jq '.headRefOid')
|
|
401
|
+
for attempt in 1 2 3 4 5 6; do
|
|
402
|
+
runs=$(gh api \
|
|
403
|
+
-H 'Accept: application/vnd.github+json' \
|
|
404
|
+
"/repos/$GH_REPO/actions/workflows/ci.yml/runs?event=pull_request&head_sha=$head_sha" \
|
|
405
|
+
--jq '.total_count')
|
|
406
|
+
if [ "$runs" -gt 0 ]; then
|
|
407
|
+
echo 'Pull-request verification run materialized; approve it if GitHub marks it approval-required.'
|
|
408
|
+
exit 0
|
|
409
|
+
fi
|
|
410
|
+
sleep 5
|
|
411
|
+
done
|
|
412
|
+
echo 'No pull-request verification run materialized. Close and reopen the sync pull request as a human, then approve its workflows.'
|
|
413
|
+
exit 1
|
|
414
|
+
`,
|
|
415
|
+
'tools/ebk-pre-push.mjs': `import { createInterface } from 'node:readline';
|
|
416
|
+
|
|
417
|
+
const protectedRefs = new Set(['refs/heads/develop', 'refs/heads/main']);
|
|
418
|
+
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
419
|
+
let rejected = false;
|
|
420
|
+
|
|
421
|
+
for await (const line of lines) {
|
|
422
|
+
const [, , remoteRef] = line.trim().split(/\\s+/);
|
|
423
|
+
if (!protectedRefs.has(remoteRef)) continue;
|
|
424
|
+
rejected = true;
|
|
425
|
+
const branch = remoteRef.slice('refs/heads/'.length);
|
|
426
|
+
const route = branch === 'main'
|
|
427
|
+
? 'develop -> main promotion'
|
|
428
|
+
: 'feature -> develop or main -> develop sync-back';
|
|
429
|
+
console.error('Direct push to ' + branch + ' rejected. Use a reviewed ' + route + ' pull request.');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (rejected) process.exitCode = 1;
|
|
433
|
+
`,
|
|
434
|
+
'tools/install-ebk-hooks.mjs': `import { chmodSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
435
|
+
import { spawnSync } from 'node:child_process';
|
|
436
|
+
|
|
437
|
+
const marker = '# Empire Builder Kit managed pre-push hook';
|
|
438
|
+
const resolved = spawnSync('git', ['rev-parse', '--git-path', 'hooks/pre-push'], {
|
|
439
|
+
encoding: 'utf8',
|
|
440
|
+
shell: false,
|
|
441
|
+
});
|
|
442
|
+
if (resolved.status !== 0 || !resolved.stdout.trim()) {
|
|
443
|
+
console.error(resolved.stderr.trim() || 'Unable to resolve the repository-local Git hook path.');
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const hookPath = resolved.stdout.trim();
|
|
448
|
+
if (existsSync(hookPath) && !readFileSync(hookPath, 'utf8').includes(marker)) {
|
|
449
|
+
console.error('A non-EBK pre-push hook already exists at ' + hookPath + '; preserve or compose it manually.');
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
writeFileSync(
|
|
454
|
+
hookPath,
|
|
455
|
+
'#!/bin/sh\\n' + marker + '\\nnode tools/ebk-pre-push.mjs\\n',
|
|
456
|
+
'utf8',
|
|
457
|
+
);
|
|
458
|
+
chmodSync(hookPath, 0o755);
|
|
459
|
+
console.log('Installed the repository-local EBK pre-push hook at ' + hookPath + '.');
|
|
179
460
|
`,
|
|
180
461
|
'.gitignore': `.nx/cache
|
|
181
462
|
.nx/workspace-data
|
|
@@ -200,6 +481,36 @@ dist
|
|
|
200
481
|
pnpm-lock.yaml
|
|
201
482
|
`,
|
|
202
483
|
'.prettierrc.json': json({ singleQuote: true }),
|
|
484
|
+
'AGENTS.md': `# Coding agent instructions
|
|
485
|
+
|
|
486
|
+
Use \`pnpm nx\` for workspace tasks and read the repository runbooks before changing
|
|
487
|
+
infrastructure or protected deployment policy. Preserve the ownership boundary recorded in
|
|
488
|
+
\`.ebk/ownership.json\`: managed files belong to EBK, seeded files are generated starting
|
|
489
|
+
points that adopters may change, and unrecorded files are adopter-owned.
|
|
490
|
+
|
|
491
|
+
## Work markers
|
|
492
|
+
|
|
493
|
+
Run \`pnpm nx run ${name}:work-markers\` when beginning or handing off implementation work.
|
|
494
|
+
Search results are durable pointers to custom or nondeterministic work that the deterministic
|
|
495
|
+
generator cannot complete safely. Read the marker's self-contained description and its
|
|
496
|
+
\`GH:<number>\` or \`PLAN:<phase>/<slug>\` reference before acting.
|
|
497
|
+
|
|
498
|
+
Blueprint and repository upgrades may also add a marker to an editable surface when preserved
|
|
499
|
+
custom code must be reconciled with the new contract. Treat that marker as incomplete upgrade
|
|
500
|
+
work: adapt the custom code, run the relevant verification, retain evidence, and then remove it.
|
|
501
|
+
|
|
502
|
+
Generators and agents may add markers only to seeded or adopter-owned source files. Never add
|
|
503
|
+
one to a managed file or edit managed output to resolve one. Complete the referenced work,
|
|
504
|
+
preserve relevant tests and evidence, then remove the marker. \`EBK-SECURITY\` and
|
|
505
|
+
\`EBK-BLOCKER\` prevent staging and production deploy or refresh; remove and unlock recovery
|
|
506
|
+
remain available. See \`docs/work-markers.md\` for the fixed vocabulary and syntax.
|
|
507
|
+
`,
|
|
508
|
+
'CLAUDE.md': `# Claude workspace guidance
|
|
509
|
+
|
|
510
|
+
Read and follow \`AGENTS.md\`, including its generated-file ownership rules and work-marker
|
|
511
|
+
workflow. Before implementing or handing off work, run
|
|
512
|
+
\`pnpm nx run ${name}:work-markers\` and inspect every reported reference.
|
|
513
|
+
`,
|
|
203
514
|
'eslint.config.mjs': `import eslint from '@eslint/js';
|
|
204
515
|
import globals from 'globals';
|
|
205
516
|
import tseslint from 'typescript-eslint';
|
|
@@ -223,6 +534,8 @@ This workspace was created with Empire Builder Kit v1.
|
|
|
223
534
|
1. Review \`docs/architecture.md\`.
|
|
224
535
|
2. Review the generated Foundation before deploying it.
|
|
225
536
|
3. Add product slices through the Empire Builder Kit Nx plugin.
|
|
537
|
+
4. Review \`docs/repository-operating-policy.md\` and \`docs/git-workflow.md\` before enabling production.
|
|
538
|
+
5. Read \`AGENTS.md\` and \`docs/work-markers.md\` before assigning generated customization work to agents.
|
|
226
539
|
|
|
227
540
|
For one-origin fleet development, first run \`pnpm nx run ${name}:dev-setup\`, inspect the
|
|
228
541
|
generated root certificate, then explicitly approve \`--trust --portless\`. Diagnose without
|
|
@@ -246,9 +559,150 @@ Foundation implementation files.
|
|
|
246
559
|
Canonical consumer stages use the matching \`development\`, \`staging\`, or \`production\`
|
|
247
560
|
Foundation. Personal and preview consumer stages may use only the \`development\` Foundation.
|
|
248
561
|
`,
|
|
562
|
+
'docs/git-workflow.md': `# Protected Git workflow
|
|
563
|
+
|
|
564
|
+
Ordinary work starts from \`develop\` and returns through a reviewed pull request to
|
|
565
|
+
\`develop\`. A production promotion is exactly \`develop\` to \`main\`. After production,
|
|
566
|
+
review and merge the managed \`main\` to \`develop\` sync-back pull request before ordinary
|
|
567
|
+
work resumes.
|
|
568
|
+
|
|
569
|
+
Both protected branches require the stable \`policy\` and \`verify\` checks, one approving
|
|
570
|
+
review including an approval from the committed catch-all CODEOWNER, dismissed stale reviews,
|
|
571
|
+
resolved conversations, and merge commits. Replace the placeholder in \`.github/CODEOWNERS\`
|
|
572
|
+
with a GitHub user or team that has repository access before bootstrap. This prevents an
|
|
573
|
+
approval from \`github-actions[bot]\` from satisfying the independent code-owner gate even
|
|
574
|
+
though Actions must be allowed to create and approve the managed sync-back pull request.
|
|
575
|
+
Direct updates, force pushes, deletion, squash merging, and rebase merging are prohibited.
|
|
576
|
+
GitHub rulesets, environments, and AWS IAM are the authorization boundaries; local hooks are
|
|
577
|
+
only defense in depth.
|
|
578
|
+
|
|
579
|
+
The \`staging\` environment accepts only \`develop\`; \`production\` accepts only \`main\`
|
|
580
|
+
and requires an independent approval when configured. Each environment owns its own
|
|
581
|
+
\`AWS_DEPLOY_ROLE_ARN\`, \`AWS_EXPECTED_ACCOUNT_ID\`, and \`AWS_REGION\`; production also
|
|
582
|
+
defines \`EBK_ALLOW_PRODUCTION_REMOVE\`, normally as \`false\`. Never add repository- or
|
|
583
|
+
organization-visible fallbacks for these protected values. Keep repository default workflow
|
|
584
|
+
permissions read-only; the generated jobs request only the additional permissions they need.
|
|
585
|
+
|
|
586
|
+
Run the GitHub bootstrap in report mode first. Source upgrades never alter remote GitHub
|
|
587
|
+
settings. Apply a reviewed report only after \`develop\` exists and the candidate's exact
|
|
588
|
+
\`policy\` and \`verify\` check contexts have run.
|
|
589
|
+
|
|
590
|
+
Set \`GH_REPO=<owner>/<repository>\` and a suitable \`GH_TOKEN\`, then run
|
|
591
|
+
\`pnpm nx run ${name}:github-bootstrap\`. Review the deterministic report before rerunning
|
|
592
|
+
with \`--apply\`. The command never creates branches, credentials, secrets, or AWS resources.
|
|
593
|
+
|
|
594
|
+
Install the optional defense-in-depth pre-push hook explicitly with
|
|
595
|
+
\`pnpm nx run ${name}:install-git-hooks\`. It refuses to overwrite an unrelated existing hook.
|
|
596
|
+
|
|
597
|
+
The sync-back workflow uses only \`GITHUB_TOKEN\` and never pushes or auto-merges. GitHub may
|
|
598
|
+
place its pull-request workflows in an approval-required state. Approve them; if no run
|
|
599
|
+
materializes, close and reopen the pull request as a human and then approve it.
|
|
600
|
+
`,
|
|
601
|
+
'docs/repository-operating-policy.md': `# Repository Operating Policy
|
|
602
|
+
|
|
603
|
+
\`ebk.repository-policy.json\` is the reviewed source of truth for production access,
|
|
604
|
+
per-Slice readiness, and the one shared production Foundation profile. New workspaces start
|
|
605
|
+
with production disabled. A missing Slice entry is always blocked.
|
|
606
|
+
|
|
607
|
+
The named repository modes are source-writing presets, not runtime state. Report a change
|
|
608
|
+
first and inspect the complete current/next policy:
|
|
609
|
+
|
|
610
|
+
\`\`\`bash
|
|
611
|
+
pnpm nx g @empire-builder-kit/nx:repository-policy \\
|
|
612
|
+
--preset=controlled-production --slices=billing
|
|
613
|
+
\`\`\`
|
|
614
|
+
|
|
615
|
+
Apply the reviewed source change separately with \`--apply\`. This command never contacts AWS
|
|
616
|
+
or GitHub. Use \`--status=blocked|pilot|ready\` for explicit Slice transitions and
|
|
617
|
+
\`--profile=cost-optimized|standard\` for an explicit shared Foundation decision.
|
|
618
|
+
|
|
619
|
+
- \`blocked\` prohibits production create, update, and migration.
|
|
620
|
+
- \`pilot\` permits protected evidence gathering without claiming normal latency or maturity.
|
|
621
|
+
- \`ready\` records an adopter decision after its evidence and review; it is not inferred.
|
|
622
|
+
- removal and unlock remain explicit protected recovery paths so disabled policy cannot strand
|
|
623
|
+
resources.
|
|
624
|
+
|
|
625
|
+
Protected production deploy and refresh validate the complete Foundation/Slice set before AWS
|
|
626
|
+
access. The Foundation is always ordered first. The set is derived from committed policy and
|
|
627
|
+
cannot be narrowed with an ad hoc project argument.
|
|
628
|
+
|
|
629
|
+
\`standard\` keeps Aurora warm at 2–16 ACU. \`cost-optimized\` is a prerelease candidate at
|
|
630
|
+
0–4 ACU with a five-minute auto-pause and potential cold-resume latency. Both keep managed NAT,
|
|
631
|
+
30-day backups, deletion protection, retained data, and the protected GitHub/OIDC/AWS path.
|
|
632
|
+
Changing policy affects only later lifecycle requests; it never deploys or removes resources.
|
|
633
|
+
Changing profile requires a separate reviewed Foundation deployment and retained operational
|
|
634
|
+
evidence.
|
|
635
|
+
|
|
636
|
+
\`EBK_PRODUCTION_FOUNDATION_PROFILE\` is reserved managed executor input. The lifecycle
|
|
637
|
+
executor derives it from validated committed policy immediately before SST and removes any
|
|
638
|
+
ambient shell value. Do not set it as a local, repository, organization, or environment
|
|
639
|
+
variable. Incubation keeps the last configured profile, or \`standard\` for legacy/default
|
|
640
|
+
policy, solely so protected remove and unlock can still render existing production resources;
|
|
641
|
+
production deploy remains disabled.
|
|
642
|
+
`,
|
|
643
|
+
'docs/work-markers.md': `# Agent work markers
|
|
644
|
+
|
|
645
|
+
EBK work markers let deterministic generators identify places where an agent or human must
|
|
646
|
+
make a contextual product decision. They are searchable pointers to durable work, not a
|
|
647
|
+
replacement for the referenced issue or plan.
|
|
648
|
+
|
|
649
|
+
## Syntax and vocabulary
|
|
650
|
+
|
|
651
|
+
Use one full-line source comment with a self-contained description. Examples include
|
|
652
|
+
\`// EBK-TODO(GH:123): Replace the sample authorization decision with the approved policy.\`
|
|
653
|
+
and
|
|
654
|
+
\`# EBK-ASSUMPTION(PLAN:1/operator-proof): Confirm this timeout during the operator pilot.\`
|
|
655
|
+
|
|
656
|
+
The fixed vocabulary is:
|
|
657
|
+
|
|
658
|
+
- \`EBK-TODO\` — known implementation work;
|
|
659
|
+
- \`EBK-ASSUMPTION\` — a decision that needs validation;
|
|
660
|
+
- \`EBK-QUESTION\` — an unresolved design or product question;
|
|
661
|
+
- \`EBK-SECURITY\` — security work that blocks protected promotion; and
|
|
662
|
+
- \`EBK-BLOCKER\` — any other work that blocks protected promotion.
|
|
663
|
+
|
|
664
|
+
Vocabulary names used in comment prose are inert. The scanner treats only a kind immediately
|
|
665
|
+
followed by \`(\` or \`:\` as a marker attempt, so malformed marker-shaped comments still fail
|
|
666
|
+
validation.
|
|
667
|
+
|
|
668
|
+
Until Work Orchestration imports these markers, references must be an existing GitHub issue
|
|
669
|
+
(\`GH:<positive-number>\`) or an append-only roadmap key
|
|
670
|
+
(\`PLAN:<phase>/<slug>\`). The description must remain useful without opening the reference.
|
|
671
|
+
The scanner validates syntax and ownership now; it intentionally does not claim that a
|
|
672
|
+
reference exists. Phase 2 owns reference import, retained old-to-new ID mapping, and later
|
|
673
|
+
orphan checks. Do not invent temporary Work Orchestration numbers.
|
|
674
|
+
|
|
675
|
+
## Generator and agent workflow
|
|
676
|
+
|
|
677
|
+
Generators may emit a marker only into a \`seeded\` customization surface. They must never put
|
|
678
|
+
one in a \`managed\` file: managed output stays deterministic, and a needed change there is a
|
|
679
|
+
framework defect to report outside the generated file. Adopter-owned files may also contain
|
|
680
|
+
markers. Framework upgrades preserve seeded source and its unresolved markers.
|
|
681
|
+
|
|
682
|
+
Blueprint and repository update tooling may create a deterministic reconciliation marker in a
|
|
683
|
+
seeded or adopter-owned file when a preserved customization needs human or agent judgment. The
|
|
684
|
+
update must preserve the custom code and identify the changed contract in the description; a
|
|
685
|
+
successful update command alone does not resolve that marker. Reconcile the code, run the
|
|
686
|
+
relevant checks, retain evidence at the referenced work item, and only then remove the marker.
|
|
687
|
+
Automatic insertion requires an explicit publisher migration or other deterministic anchor;
|
|
688
|
+
the generic upgrade command does not guess where to edit preserved custom code.
|
|
689
|
+
|
|
690
|
+
At the start and handoff of agent work, run:
|
|
691
|
+
|
|
692
|
+
\`pnpm nx run ${name}:work-markers\`
|
|
693
|
+
|
|
694
|
+
The same ordinary validation runs through workspace health and CI. Complete the referenced
|
|
695
|
+
custom work and remove the marker; do not merely delete it while leaving the requirement
|
|
696
|
+
unresolved. Open \`TODO\`, \`ASSUMPTION\`, and \`QUESTION\` markers are reported but do not block
|
|
697
|
+
deployment by themselves. Open \`SECURITY\` and \`BLOCKER\` markers fail staging and production
|
|
698
|
+
deploy or refresh before AWS access. Protected remove and unlock remain usable for recovery.
|
|
699
|
+
There is no local or silent bypass in this prerelease contract; any future waiver mechanism
|
|
700
|
+
must be committed, reviewable, and auditable.
|
|
701
|
+
`,
|
|
702
|
+
'ebk.repository-policy.json': json((0, index_js_3.createIncubationRepositoryPolicy)()),
|
|
249
703
|
'nx.json': json({
|
|
250
704
|
$schema: './node_modules/nx/schemas/nx-schema.json',
|
|
251
|
-
defaultBase: '
|
|
705
|
+
defaultBase: 'develop',
|
|
252
706
|
namedInputs: {
|
|
253
707
|
default: ['{projectRoot}/**/*', 'sharedGlobals'],
|
|
254
708
|
production: [
|
|
@@ -324,6 +778,13 @@ Foundation. Personal and preview consumer stages may use only the \`development\
|
|
|
324
778
|
projectType: 'application',
|
|
325
779
|
tags: ['scope:workspace', 'layer:governance'],
|
|
326
780
|
targets: {
|
|
781
|
+
'deployment-plan': {
|
|
782
|
+
executor: '@empire-builder-kit/nx:deployment-plan',
|
|
783
|
+
options: {
|
|
784
|
+
action: 'deploy',
|
|
785
|
+
stage: 'staging',
|
|
786
|
+
},
|
|
787
|
+
},
|
|
327
788
|
dev: {
|
|
328
789
|
continuous: true,
|
|
329
790
|
executor: '@empire-builder-kit/nx:fleet',
|
|
@@ -339,6 +800,18 @@ Foundation. Personal and preview consumer stages may use only the \`development\
|
|
|
339
800
|
executor: '@empire-builder-kit/nx:dev-setup',
|
|
340
801
|
options: {},
|
|
341
802
|
},
|
|
803
|
+
'github-bootstrap': {
|
|
804
|
+
executor: '@empire-builder-kit/nx:github-bootstrap',
|
|
805
|
+
options: {},
|
|
806
|
+
},
|
|
807
|
+
'install-git-hooks': {
|
|
808
|
+
executor: 'nx:run-commands',
|
|
809
|
+
options: { command: 'node tools/install-ebk-hooks.mjs' },
|
|
810
|
+
},
|
|
811
|
+
'work-markers': {
|
|
812
|
+
executor: '@empire-builder-kit/nx:work-markers',
|
|
813
|
+
options: {},
|
|
814
|
+
},
|
|
342
815
|
verify: {
|
|
343
816
|
executor: 'nx:run-commands',
|
|
344
817
|
options: { command: 'node tools/workspace-health.mjs' },
|
|
@@ -380,7 +853,13 @@ domain. Without it, the shared Router uses its functional provider URL.
|
|
|
380
853
|
],
|
|
381
854
|
}),
|
|
382
855
|
'packages/foundation/contracts/costs.json': json({
|
|
383
|
-
schemaVersion:
|
|
856
|
+
schemaVersion: 2,
|
|
857
|
+
aurora: {
|
|
858
|
+
engine: index_js_3.FOUNDATION_AURORA_ENGINE_FAMILY,
|
|
859
|
+
engineVersion: index_js_3.FOUNDATION_AURORA_ENGINE_VERSION,
|
|
860
|
+
},
|
|
861
|
+
productionProfiles: index_js_3.PRODUCTION_FOUNDATION_PROFILE_CONTRACTS,
|
|
862
|
+
productionProfileSource: 'ebk.repository-policy.json',
|
|
384
863
|
stages: {
|
|
385
864
|
development: {
|
|
386
865
|
aurora: 'scale-to-zero',
|
|
@@ -393,7 +872,7 @@ domain. Without it, the shared Router uses its functional provider URL.
|
|
|
393
872
|
nat: 'ec2',
|
|
394
873
|
},
|
|
395
874
|
production: {
|
|
396
|
-
aurora: '
|
|
875
|
+
aurora: 'repository-policy',
|
|
397
876
|
dataRetention: 'retained-and-protected',
|
|
398
877
|
nat: 'managed',
|
|
399
878
|
},
|
|
@@ -415,7 +894,9 @@ seeded decision record aligned with organization-specific budgets and alerts.
|
|
|
415
894
|
|
|
416
895
|
- Development uses EC2 NAT, a development bastion, and Aurora scale-to-zero.
|
|
417
896
|
- Staging uses EC2 NAT and Aurora scale-to-zero by default.
|
|
418
|
-
- Production uses managed NAT and
|
|
897
|
+
- Production always uses managed NAT and retains protected data. Its explicit Repository
|
|
898
|
+
Operating Policy profile selects either the standard always-warm Aurora floor or the
|
|
899
|
+
prerelease cost-optimized auto-pause candidate.
|
|
419
900
|
- ECS, EventBridge, Cognito, SNS, SSM, and CloudWatch are primarily usage-priced; logs,
|
|
420
901
|
metrics, alarms, and retained data still require active cost review.
|
|
421
902
|
|
|
@@ -474,6 +955,11 @@ declare const $app: {
|
|
|
474
955
|
foundationSsmPath,
|
|
475
956
|
type FoundationSsmKey,
|
|
476
957
|
} from '@empire-builder-kit/nx/foundation';
|
|
958
|
+
import {
|
|
959
|
+
FOUNDATION_AURORA_ENGINE,
|
|
960
|
+
FOUNDATION_AURORA_ENGINE_VERSION,
|
|
961
|
+
productionFoundationProfileContract,
|
|
962
|
+
} from '@empire-builder-kit/nx/repository-policy';
|
|
477
963
|
|
|
478
964
|
function publishFoundation(values: Record<FoundationSsmKey, any>) {
|
|
479
965
|
return Object.fromEntries(
|
|
@@ -492,11 +978,16 @@ export function foundation() {
|
|
|
492
978
|
const isProduction = $app.stage === 'production';
|
|
493
979
|
const isStaging = $app.stage === 'staging';
|
|
494
980
|
const rootDomain = process.env.EBK_ROOT_DOMAIN?.trim();
|
|
981
|
+
const productionConfiguration = isProduction
|
|
982
|
+
? productionFoundationProfileContract(
|
|
983
|
+
process.env.EBK_PRODUCTION_FOUNDATION_PROFILE,
|
|
984
|
+
)
|
|
985
|
+
: undefined;
|
|
495
986
|
|
|
496
987
|
const vpc = new sst.aws.Vpc('FoundationVpc', {
|
|
497
988
|
az: 2,
|
|
498
989
|
bastion: $app.stage === 'development',
|
|
499
|
-
nat:
|
|
990
|
+
nat: productionConfiguration?.contract.nat ?? 'ec2',
|
|
500
991
|
});
|
|
501
992
|
const cluster = new sst.aws.Cluster('FoundationCluster', { vpc });
|
|
502
993
|
const router = new sst.aws.Router('FoundationRouter', {
|
|
@@ -512,16 +1003,19 @@ export function foundation() {
|
|
|
512
1003
|
});
|
|
513
1004
|
const database = new sst.aws.Aurora('FoundationDatabase', {
|
|
514
1005
|
dataApi: true,
|
|
515
|
-
engine:
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
1006
|
+
engine: FOUNDATION_AURORA_ENGINE,
|
|
1007
|
+
version: FOUNDATION_AURORA_ENGINE_VERSION,
|
|
1008
|
+
scaling:
|
|
1009
|
+
productionConfiguration?.contract.aurora ??
|
|
1010
|
+
(isStaging
|
|
519
1011
|
? { min: '0 ACU', max: '4 ACU', pauseAfter: '5 minutes' }
|
|
520
|
-
: { min: '0 ACU', max: '2 ACU', pauseAfter: '5 minutes' },
|
|
1012
|
+
: { min: '0 ACU', max: '2 ACU', pauseAfter: '5 minutes' }),
|
|
521
1013
|
transform: {
|
|
522
1014
|
cluster: {
|
|
523
|
-
backupRetentionPeriod:
|
|
524
|
-
|
|
1015
|
+
backupRetentionPeriod:
|
|
1016
|
+
productionConfiguration?.contract.backupRetentionDays ?? 7,
|
|
1017
|
+
deletionProtection:
|
|
1018
|
+
productionConfiguration?.contract.deletionProtection ?? false,
|
|
525
1019
|
},
|
|
526
1020
|
},
|
|
527
1021
|
vpc,
|
|
@@ -531,9 +1025,39 @@ export function foundation() {
|
|
|
531
1025
|
});
|
|
532
1026
|
const eventBus = new sst.aws.Bus('FoundationEventBus');
|
|
533
1027
|
const alertTopic = new sst.aws.SnsTopic('FoundationAlerts');
|
|
1028
|
+
const alertTopicPolicy = new aws.sns.TopicPolicy(
|
|
1029
|
+
'FoundationAlertTopicPolicy',
|
|
1030
|
+
{
|
|
1031
|
+
arn: alertTopic.arn,
|
|
1032
|
+
policy: aws.iam.getPolicyDocumentOutput({
|
|
1033
|
+
statements: [
|
|
1034
|
+
{
|
|
1035
|
+
actions: ['sns:Publish'],
|
|
1036
|
+
principals: [
|
|
1037
|
+
{ identifiers: ['events.rds.amazonaws.com'], type: 'Service' },
|
|
1038
|
+
],
|
|
1039
|
+
resources: [alertTopic.arn],
|
|
1040
|
+
},
|
|
1041
|
+
],
|
|
1042
|
+
}).json,
|
|
1043
|
+
},
|
|
1044
|
+
);
|
|
1045
|
+
const databaseInstanceId = database.nodes[0].identifier;
|
|
1046
|
+
const databaseEvents = isProduction
|
|
1047
|
+
? new aws.rds.EventSubscription(
|
|
1048
|
+
'FoundationDatabaseEvents',
|
|
1049
|
+
{
|
|
1050
|
+
eventCategories: ['failure', 'notification', 'serverless'],
|
|
1051
|
+
snsTopic: alertTopic.arn,
|
|
1052
|
+
sourceIds: database.nodes.map((node: any) => node.identifier),
|
|
1053
|
+
sourceType: 'db-instance',
|
|
1054
|
+
},
|
|
1055
|
+
{ dependsOn: [alertTopicPolicy] },
|
|
1056
|
+
)
|
|
1057
|
+
: undefined;
|
|
534
1058
|
const dashboard = new aws.cloudwatch.Dashboard('FoundationDashboard', {
|
|
535
1059
|
dashboardName: 'ebk-' + $app.stage + '-foundation',
|
|
536
|
-
dashboardBody: JSON.stringify({
|
|
1060
|
+
dashboardBody: databaseInstanceId.apply((identifier: string) => JSON.stringify({
|
|
537
1061
|
widgets: [
|
|
538
1062
|
{
|
|
539
1063
|
type: 'text',
|
|
@@ -542,12 +1066,64 @@ export function foundation() {
|
|
|
542
1066
|
width: 24,
|
|
543
1067
|
height: 3,
|
|
544
1068
|
properties: {
|
|
545
|
-
markdown:
|
|
1069
|
+
markdown:
|
|
1070
|
+
'# Empire Builder Kit Foundation\\nStage: ' +
|
|
1071
|
+
$app.stage +
|
|
1072
|
+
'\\nProduction profile: ' +
|
|
1073
|
+
(productionConfiguration?.profile ?? 'not-applicable'),
|
|
1074
|
+
},
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
type: 'metric',
|
|
1078
|
+
x: 0,
|
|
1079
|
+
y: 3,
|
|
1080
|
+
width: 12,
|
|
1081
|
+
height: 6,
|
|
1082
|
+
properties: {
|
|
1083
|
+
title: 'Aurora capacity and pause state',
|
|
1084
|
+
period: 60,
|
|
1085
|
+
stat: 'Minimum',
|
|
1086
|
+
metrics: [
|
|
1087
|
+
['AWS/RDS', 'ServerlessDatabaseCapacity', 'DBInstanceIdentifier', identifier],
|
|
1088
|
+
['.', 'ACUUtilization', '.', '.'],
|
|
1089
|
+
],
|
|
1090
|
+
},
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
type: 'metric',
|
|
1094
|
+
x: 12,
|
|
1095
|
+
y: 3,
|
|
1096
|
+
width: 12,
|
|
1097
|
+
height: 6,
|
|
1098
|
+
properties: {
|
|
1099
|
+
title: 'Aurora connections',
|
|
1100
|
+
period: 60,
|
|
1101
|
+
stat: 'Maximum',
|
|
1102
|
+
metrics: [
|
|
1103
|
+
['AWS/RDS', 'DatabaseConnections', 'DBInstanceIdentifier', identifier],
|
|
1104
|
+
],
|
|
546
1105
|
},
|
|
547
1106
|
},
|
|
548
1107
|
],
|
|
549
|
-
}),
|
|
1108
|
+
})),
|
|
550
1109
|
});
|
|
1110
|
+
const databaseCapacityAlarm = new aws.cloudwatch.MetricAlarm(
|
|
1111
|
+
'FoundationDatabaseCapacityAlarm',
|
|
1112
|
+
{
|
|
1113
|
+
alarmActions: [alertTopic.arn],
|
|
1114
|
+
alarmDescription:
|
|
1115
|
+
'Aurora Serverless capacity utilization stayed above 90 percent.',
|
|
1116
|
+
comparisonOperator: 'GreaterThanThreshold',
|
|
1117
|
+
dimensions: { DBInstanceIdentifier: databaseInstanceId },
|
|
1118
|
+
evaluationPeriods: 5,
|
|
1119
|
+
metricName: 'ACUUtilization',
|
|
1120
|
+
namespace: 'AWS/RDS',
|
|
1121
|
+
period: 60,
|
|
1122
|
+
statistic: 'Average',
|
|
1123
|
+
threshold: 90,
|
|
1124
|
+
treatMissingData: 'notBreaching',
|
|
1125
|
+
},
|
|
1126
|
+
);
|
|
551
1127
|
|
|
552
1128
|
if (new Set(['development', 'staging', 'production']).has($app.stage)) {
|
|
553
1129
|
new aws.cloudwatch.EventArchive('FoundationEventArchive', {
|
|
@@ -581,11 +1157,14 @@ export function foundation() {
|
|
|
581
1157
|
capabilities: STANDARD_FOUNDATION_CAPABILITIES,
|
|
582
1158
|
clusterId: cluster.id,
|
|
583
1159
|
contractVersion: '1.0.0',
|
|
1160
|
+
databaseCapacityAlarmName: databaseCapacityAlarm.name,
|
|
584
1161
|
dashboardName: dashboard.dashboardName,
|
|
585
1162
|
databaseClusterId: database.id,
|
|
1163
|
+
databaseEventSubscriptionId: databaseEvents?.id,
|
|
586
1164
|
eventBusName: eventBus.name,
|
|
587
1165
|
identityUserPoolId: userPool.id,
|
|
588
1166
|
name: 'foundation',
|
|
1167
|
+
productionFoundationProfile: productionConfiguration?.profile,
|
|
589
1168
|
rootDomain,
|
|
590
1169
|
routerUrl: router.url,
|
|
591
1170
|
vpcId: vpc.id,
|
|
@@ -613,7 +1192,10 @@ export function foundation() {
|
|
|
613
1192
|
targets: {
|
|
614
1193
|
deploy: {
|
|
615
1194
|
executor: '@empire-builder-kit/nx:deploy',
|
|
616
|
-
dependsOn: [
|
|
1195
|
+
dependsOn: [
|
|
1196
|
+
'verify',
|
|
1197
|
+
{ target: 'profile-preflight', params: 'forward' },
|
|
1198
|
+
],
|
|
617
1199
|
options: {
|
|
618
1200
|
cwd: 'packages/foundation',
|
|
619
1201
|
stage: 'development',
|
|
@@ -638,6 +1220,7 @@ export function foundation() {
|
|
|
638
1220
|
},
|
|
639
1221
|
refresh: {
|
|
640
1222
|
executor: '@empire-builder-kit/nx:refresh',
|
|
1223
|
+
dependsOn: [{ target: 'profile-preflight', params: 'forward' }],
|
|
641
1224
|
options: {
|
|
642
1225
|
cwd: 'packages/foundation',
|
|
643
1226
|
stage: 'development',
|
|
@@ -657,6 +1240,10 @@ export function foundation() {
|
|
|
657
1240
|
executor: '@empire-builder-kit/nx:foundation-remove-preflight',
|
|
658
1241
|
options: { stage: 'development' },
|
|
659
1242
|
},
|
|
1243
|
+
'profile-preflight': {
|
|
1244
|
+
executor: '@empire-builder-kit/nx:foundation-profile-preflight',
|
|
1245
|
+
options: { stage: 'development' },
|
|
1246
|
+
},
|
|
660
1247
|
typecheck: {
|
|
661
1248
|
executor: 'nx:run-commands',
|
|
662
1249
|
options: {
|
|
@@ -709,7 +1296,11 @@ if (JSON.stringify(descriptor.capabilities) !== JSON.stringify(expectedCapabilit
|
|
|
709
1296
|
throw new Error('Foundation capability descriptor does not match the v1 standard.');
|
|
710
1297
|
}
|
|
711
1298
|
if (
|
|
712
|
-
costs.schemaVersion !==
|
|
1299
|
+
costs.schemaVersion !== 2 ||
|
|
1300
|
+
costs.aurora?.engineVersion !== '${index_js_3.FOUNDATION_AURORA_ENGINE_VERSION}' ||
|
|
1301
|
+
costs.productionProfileSource !== 'ebk.repository-policy.json' ||
|
|
1302
|
+
costs.productionProfiles?.standard?.aurora?.min !== '2 ACU' ||
|
|
1303
|
+
costs.productionProfiles?.['cost-optimized']?.aurora?.min !== '0 ACU' ||
|
|
713
1304
|
!costs.stages?.development ||
|
|
714
1305
|
!costs.stages?.staging ||
|
|
715
1306
|
costs.stages?.production?.dataRetention !== 'retained-and-protected'
|
|
@@ -792,7 +1383,69 @@ export default $config({
|
|
|
792
1383
|
files: [],
|
|
793
1384
|
references: [],
|
|
794
1385
|
}),
|
|
795
|
-
'tools/
|
|
1386
|
+
'tools/ebk-git-policy.mjs': `import { evaluateGitPolicy } from '@empire-builder-kit/nx/git-policy';
|
|
1387
|
+
import { spawnSync } from 'node:child_process';
|
|
1388
|
+
|
|
1389
|
+
const git = (...args) => {
|
|
1390
|
+
const result = spawnSync('git', args, { encoding: 'utf8', shell: false });
|
|
1391
|
+
if (result.status !== 0) {
|
|
1392
|
+
throw new Error('git ' + args.join(' ') + ' failed: ' + (result.stderr || result.stdout).trim());
|
|
1393
|
+
}
|
|
1394
|
+
return result.stdout.trim();
|
|
1395
|
+
};
|
|
1396
|
+
const isAncestor = (ancestor, descendant) =>
|
|
1397
|
+
spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
|
|
1398
|
+
encoding: 'utf8',
|
|
1399
|
+
shell: false,
|
|
1400
|
+
}).status === 0;
|
|
1401
|
+
|
|
1402
|
+
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
1403
|
+
if (!['pull_request', 'push', 'workflow_dispatch'].includes(eventName)) {
|
|
1404
|
+
throw new Error('Unsupported or missing GITHUB_EVENT_NAME; Git policy fails closed.');
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
const request = { eventName, ref: process.env.GITHUB_REF };
|
|
1408
|
+
if (eventName === 'pull_request' || eventName === 'push') {
|
|
1409
|
+
git(
|
|
1410
|
+
'fetch',
|
|
1411
|
+
'--no-tags',
|
|
1412
|
+
'origin',
|
|
1413
|
+
'main:refs/remotes/origin/main',
|
|
1414
|
+
'develop:refs/remotes/origin/develop',
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
if (eventName === 'pull_request') {
|
|
1418
|
+
const baseRef = process.env.GITHUB_BASE_REF;
|
|
1419
|
+
const headRef = process.env.GITHUB_HEAD_REF;
|
|
1420
|
+
if (!baseRef || !headRef) {
|
|
1421
|
+
throw new Error('Pull-request branch metadata is missing.');
|
|
1422
|
+
}
|
|
1423
|
+
Object.assign(request, {
|
|
1424
|
+
baseIsAncestorOfHead: isAncestor('refs/remotes/origin/' + baseRef, 'HEAD'),
|
|
1425
|
+
baseRef,
|
|
1426
|
+
headRef,
|
|
1427
|
+
mainIsAncestorOfDevelop: isAncestor(
|
|
1428
|
+
'refs/remotes/origin/main',
|
|
1429
|
+
'refs/remotes/origin/develop',
|
|
1430
|
+
),
|
|
1431
|
+
});
|
|
1432
|
+
} else if (eventName === 'push') {
|
|
1433
|
+
Object.assign(request, {
|
|
1434
|
+
baseIsAncestorOfHead: isAncestor('refs/remotes/origin/develop', 'HEAD'),
|
|
1435
|
+
mainIsAncestorOfDevelop: isAncestor(
|
|
1436
|
+
'refs/remotes/origin/main',
|
|
1437
|
+
'refs/remotes/origin/develop',
|
|
1438
|
+
),
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
const decision = evaluateGitPolicy(request);
|
|
1443
|
+
process.stdout.write(JSON.stringify(decision, null, 2) + '\\n');
|
|
1444
|
+
if (!decision.allowed) process.exitCode = 1;
|
|
1445
|
+
`,
|
|
1446
|
+
'tools/workspace-health.mjs': `import { validateRepositoryOperatingPolicy } from '@empire-builder-kit/nx/repository-policy';
|
|
1447
|
+
import { assertWorkMarkerReport, formatWorkMarkerReport, scanWorkMarkers } from '@empire-builder-kit/nx/work-markers';
|
|
1448
|
+
import { createHash } from 'node:crypto';
|
|
796
1449
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
797
1450
|
import { join } from 'node:path';
|
|
798
1451
|
|
|
@@ -853,6 +1506,14 @@ if (
|
|
|
853
1506
|
validateOwnership('.', workspace, 'Workspace');
|
|
854
1507
|
validateOwnership('packages/foundation', foundation, 'Foundation');
|
|
855
1508
|
|
|
1509
|
+
const policy = validateRepositoryOperatingPolicy(readJson('ebk.repository-policy.json'));
|
|
1510
|
+
if (!policy.valid) {
|
|
1511
|
+
throw new Error(
|
|
1512
|
+
'Repository Operating Policy is invalid:\\n' +
|
|
1513
|
+
policy.issues.map(({ message, path }) => '- ' + path + ': ' + message).join('\\n'),
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
856
1517
|
for (const name of readdirSync('packages')) {
|
|
857
1518
|
if (name === 'foundation') continue;
|
|
858
1519
|
const ownershipPath = join('packages', name, '.ebk/ownership.json');
|
|
@@ -863,6 +1524,9 @@ for (const name of readdirSync('packages')) {
|
|
|
863
1524
|
}
|
|
864
1525
|
validateOwnership(join('packages', name), ownership, 'Product Slice ' + name);
|
|
865
1526
|
}
|
|
1527
|
+
const markerReport = scanWorkMarkers(process.cwd());
|
|
1528
|
+
process.stdout.write(formatWorkMarkerReport(markerReport) + '\\n');
|
|
1529
|
+
assertWorkMarkerReport(markerReport);
|
|
866
1530
|
process.stdout.write('Empire Builder Kit workspace source health is green.\\n');
|
|
867
1531
|
`,
|
|
868
1532
|
};
|