@empire-builder-kit/nx 1.0.0-beta.5 → 1.0.0-beta.7

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.
Files changed (74) hide show
  1. package/README.md +13 -4
  2. package/executors.json +20 -0
  3. package/generators.json +5 -0
  4. package/package.json +15 -1
  5. package/src/executors/deployment-plan/deployment-plan.d.ts +9 -0
  6. package/src/executors/deployment-plan/deployment-plan.js +50 -0
  7. package/src/executors/deployment-plan/deployment-plan.js.map +1 -0
  8. package/src/executors/deployment-plan/schema.json +24 -0
  9. package/src/executors/foundation-preflight/foundation-preflight.js +22 -0
  10. package/src/executors/foundation-preflight/foundation-preflight.js.map +1 -1
  11. package/src/executors/foundation-profile-preflight/foundation-profile-preflight.d.ts +8 -0
  12. package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js +41 -0
  13. package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js.map +1 -0
  14. package/src/executors/foundation-profile-preflight/schema.json +21 -0
  15. package/src/executors/github-bootstrap/github-bootstrap.d.ts +8 -0
  16. package/src/executors/github-bootstrap/github-bootstrap.js +30 -0
  17. package/src/executors/github-bootstrap/github-bootstrap.js.map +1 -0
  18. package/src/executors/github-bootstrap/schema.json +25 -0
  19. package/src/executors/sst-lifecycle/run-sst-command.d.ts +2 -0
  20. package/src/executors/sst-lifecycle/run-sst-command.js +58 -5
  21. package/src/executors/sst-lifecycle/run-sst-command.js.map +1 -1
  22. package/src/executors/work-markers/schema.json +14 -0
  23. package/src/executors/work-markers/work-markers.d.ts +6 -0
  24. package/src/executors/work-markers/work-markers.js +20 -0
  25. package/src/executors/work-markers/work-markers.js.map +1 -0
  26. package/src/generators/preset/workspace-files.d.ts +1 -1
  27. package/src/generators/preset/workspace-files.js +739 -38
  28. package/src/generators/preset/workspace-files.js.map +1 -1
  29. package/src/generators/repository-policy/repository-policy.d.ts +4 -0
  30. package/src/generators/repository-policy/repository-policy.js +107 -0
  31. package/src/generators/repository-policy/repository-policy.js.map +1 -0
  32. package/src/generators/repository-policy/schema.d.ts +18 -0
  33. package/src/generators/repository-policy/schema.json +46 -0
  34. package/src/generators/slice/slice.js +22 -0
  35. package/src/generators/slice/slice.js.map +1 -1
  36. package/src/generators/upgrade/upgrade.js +16 -0
  37. package/src/generators/upgrade/upgrade.js.map +1 -1
  38. package/src/git-policy/action-pins.d.ts +28 -0
  39. package/src/git-policy/action-pins.js +35 -0
  40. package/src/git-policy/action-pins.js.map +1 -0
  41. package/src/git-policy/github-bootstrap.d.ts +23 -0
  42. package/src/git-policy/github-bootstrap.js +759 -0
  43. package/src/git-policy/github-bootstrap.js.map +1 -0
  44. package/src/git-policy/index.d.ts +3 -0
  45. package/src/git-policy/index.js +7 -0
  46. package/src/git-policy/index.js.map +1 -0
  47. package/src/git-policy/routing.d.ts +15 -0
  48. package/src/git-policy/routing.js +77 -0
  49. package/src/git-policy/routing.js.map +1 -0
  50. package/src/index.d.ts +3 -0
  51. package/src/index.js +3 -0
  52. package/src/index.js.map +1 -1
  53. package/src/repository-policy/aurora-preflight.d.ts +15 -0
  54. package/src/repository-policy/aurora-preflight.js +45 -0
  55. package/src/repository-policy/aurora-preflight.js.map +1 -0
  56. package/src/repository-policy/deployment-plan.d.ts +24 -0
  57. package/src/repository-policy/deployment-plan.js +124 -0
  58. package/src/repository-policy/deployment-plan.js.map +1 -0
  59. package/src/repository-policy/foundation-profile.d.ts +20 -0
  60. package/src/repository-policy/foundation-profile.js +45 -0
  61. package/src/repository-policy/foundation-profile.js.map +1 -0
  62. package/src/repository-policy/index.d.ts +4 -0
  63. package/src/repository-policy/index.js +8 -0
  64. package/src/repository-policy/index.js.map +1 -0
  65. package/src/repository-policy/policy.d.ts +59 -0
  66. package/src/repository-policy/policy.js +274 -0
  67. package/src/repository-policy/policy.js.map +1 -0
  68. package/src/schemas/repository-policy-v1.json +58 -0
  69. package/src/work-markers/index.d.ts +1 -0
  70. package/src/work-markers/index.js +5 -0
  71. package/src/work-markers/index.js.map +1 -0
  72. package/src/work-markers/work-markers.d.ts +39 -0
  73. package/src/work-markers/work-markers.js +359 -0
  74. 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: actions/checkout@v6
124
- - uses: pnpm/action-setup@v5
125
- - uses: actions/setup-node@v6
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-\${{ inputs.stage }}
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
- deploy:
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
- environment: \${{ inputs.stage }}
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: actions/checkout@v6
168
- - uses: aws-actions/configure-aws-credentials@v6
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
- aws-region: \${{ vars.AWS_REGION || 'us-east-1' }}
172
- - uses: pnpm/action-setup@v5
173
- - uses: actions/setup-node@v6
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
- - run: pnpm nx run-many -t \${{ inputs.action }} --stage=\${{ inputs.stage }} --parallel=1
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,187 @@ 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 and someone other than the
571
+ most recent pusher, dismissed stale reviews, resolved conversations, and merge commits. Replace
572
+ the placeholder in \`.github/CODEOWNERS\` with a GitHub user or team that has repository access
573
+ before bootstrap. An approval from \`github-actions[bot]\` cannot satisfy the independent
574
+ code-owner gate; the Actions repository setting is needed to create the managed sync-back pull
575
+ request, not to approve it.
576
+ Direct updates, force pushes, deletion, squash merging, and rebase merging are prohibited.
577
+ GitHub rulesets, environments, and AWS IAM are the authorization boundaries; local hooks are
578
+ only defense in depth.
579
+
580
+ The \`staging\` environment accepts only \`develop\`; \`production\` accepts only \`main\`.
581
+ The committed \`productionApproval.mode\` selects one of two production authorization
582
+ contracts:
583
+
584
+ - \`promotion-pr\` is compatible with private repositories on GitHub Team. The required
585
+ non-author CODEOWNER approval on the exact \`develop\` to \`main\` promotion pull request
586
+ is the production authorization. Merging that pull request starts production deployment;
587
+ there is no second EBK-managed environment approval or wait timer. The approver may also be
588
+ the human whose merge triggers deployment. Roll back through a reviewed revert on the
589
+ protected branches and let the resulting \`main\` push redeploy. A repository write actor who
590
+ can run Actions may also manually dispatch \`deploy\`, \`refresh\`, or \`unlock\` against the
591
+ already approved \`main\` tip without another human gate. That residual cannot introduce
592
+ unreviewed source; production \`remove\` remains separately variable-gated. Restrict write and
593
+ Actions access, audit dispatches, or select \`environment-reviewer\` when every production
594
+ action needs separate approval.
595
+ - \`environment-reviewer\` adds a separate production environment reviewer with self-review
596
+ prevention. A private repository needs GitHub Enterprise capability for this mode. Bootstrap
597
+ inspects that capability and fails closed on an unsupported plan; it never silently changes
598
+ the committed mode.
599
+
600
+ Both modes retain required checks, a non-author CODEOWNER approval, no ruleset bypass, the
601
+ literal production environment, and its branch-scoped credentials. Each environment owns its own
602
+ \`AWS_DEPLOY_ROLE_ARN\`, \`AWS_EXPECTED_ACCOUNT_ID\`, and \`AWS_REGION\`; production also
603
+ defines \`EBK_ALLOW_PRODUCTION_REMOVE\`, normally as \`false\`. Never add repository- or
604
+ organization-visible fallbacks for these protected values. Keep repository default workflow
605
+ permissions read-only; the generated jobs request only the additional permissions they need.
606
+
607
+ Run the GitHub bootstrap in report mode first. Source upgrades never alter remote GitHub
608
+ settings. Apply a reviewed report only after \`develop\` exists and the candidate's exact
609
+ \`policy\` and \`verify\` check contexts have run.
610
+
611
+ Set \`GH_REPO=<owner>/<repository>\` and a suitable \`GH_TOKEN\`, then run
612
+ \`pnpm nx run ${name}:github-bootstrap\`. Review the deterministic report before rerunning
613
+ with \`--apply\`. The command never creates branches, credentials, secrets, or AWS resources.
614
+ Supply \`--productionReviewerIds=<id,...>\` only when committed policy selects
615
+ \`environment-reviewer\`; it is rejected in \`promotion-pr\` mode.
616
+
617
+ Install the optional defense-in-depth pre-push hook explicitly with
618
+ \`pnpm nx run ${name}:install-git-hooks\`. It refuses to overwrite an unrelated existing hook.
619
+
620
+ The sync-back workflow uses only \`GITHUB_TOKEN\` and never pushes or auto-merges. GitHub may
621
+ place its pull-request workflows in an approval-required state. Approve them; if no run
622
+ materializes, close and reopen the pull request as a human and then approve it.
623
+ `,
624
+ 'docs/repository-operating-policy.md': `# Repository Operating Policy
625
+
626
+ \`ebk.repository-policy.json\` is the reviewed source of truth for production access,
627
+ production approval, per-Slice readiness, and the one shared production Foundation profile.
628
+ New workspaces start with production disabled and explicit Team-compatible
629
+ \`productionApproval.mode: "promotion-pr"\`. A missing approval mode or Slice entry fails
630
+ closed; no GitHub capability is allowed to choose a mode implicitly.
631
+
632
+ The named repository modes are source-writing presets, not runtime state. Report a change
633
+ first and inspect the complete current/next policy:
634
+
635
+ \`\`\`bash
636
+ pnpm nx g @empire-builder-kit/nx:repository-policy \\
637
+ --preset=controlled-production --slices=billing
638
+ \`\`\`
639
+
640
+ Apply the reviewed source change separately with \`--apply\`. This command never contacts AWS
641
+ or GitHub. Use \`--status=blocked|pilot|ready\` for explicit Slice transitions and
642
+ \`--profile=cost-optimized|standard\` for an explicit shared Foundation decision.
643
+ Use \`--productionApprovalMode=promotion-pr|environment-reviewer\` for an explicit approval
644
+ transition. This changes committed policy only; a separate bootstrap report/apply reconciles
645
+ remote GitHub settings.
646
+
647
+ Policies created before beta.7 do not contain \`productionApproval\`. After applying the
648
+ workspace source upgrade, report the explicit amendment with
649
+ \`pnpm nx g @empire-builder-kit/nx:repository-policy --productionApprovalMode=promotion-pr\`,
650
+ review it, and rerun with \`--apply\` before health or bootstrap. This narrowly accepts a policy
651
+ whose sole defect is the missing member; every other defect still fails closed. Validation
652
+ provides no legacy default. Use \`promotion-pr\` for a private GitHub Team repository. Use
653
+ \`environment-reviewer\` only when the repository plan can enforce a separate environment
654
+ reviewer.
655
+
656
+ - \`blocked\` prohibits production create, update, and migration.
657
+ - \`pilot\` permits protected evidence gathering without claiming normal latency or maturity.
658
+ - \`ready\` records an adopter decision after its evidence and review; it is not inferred.
659
+ - removal and unlock remain explicit protected recovery paths so disabled policy cannot strand
660
+ resources.
661
+
662
+ Protected production deploy and refresh validate the complete Foundation/Slice set before AWS
663
+ access. The Foundation is always ordered first. The set is derived from committed policy and
664
+ cannot be narrowed with an ad hoc project argument.
665
+
666
+ \`standard\` keeps Aurora warm at 2–16 ACU. \`cost-optimized\` is a prerelease candidate at
667
+ 0–4 ACU with a five-minute auto-pause and potential cold-resume latency. Both keep managed NAT,
668
+ 30-day backups, deletion protection, retained data, and the protected GitHub/OIDC/AWS path.
669
+ Changing policy affects only later lifecycle requests; it never deploys or removes resources.
670
+ Changing profile requires a separate reviewed Foundation deployment and retained operational
671
+ evidence.
672
+
673
+ \`EBK_PRODUCTION_FOUNDATION_PROFILE\` is reserved managed executor input. The lifecycle
674
+ executor derives it from validated committed policy immediately before SST and removes any
675
+ ambient shell value. Do not set it as a local, repository, organization, or environment
676
+ variable. Incubation keeps the last configured profile, or \`standard\` for legacy/default
677
+ policy, solely so protected remove and unlock can still render existing production resources;
678
+ production deploy remains disabled.
679
+ `,
680
+ 'docs/work-markers.md': `# Agent work markers
681
+
682
+ EBK work markers let deterministic generators identify places where an agent or human must
683
+ make a contextual product decision. They are searchable pointers to durable work, not a
684
+ replacement for the referenced issue or plan.
685
+
686
+ ## Syntax and vocabulary
687
+
688
+ Use one full-line source comment with a self-contained description. Examples include
689
+ \`// EBK-TODO(GH:123): Replace the sample authorization decision with the approved policy.\`
690
+ and
691
+ \`# EBK-ASSUMPTION(PLAN:1/operator-proof): Confirm this timeout during the operator pilot.\`
692
+
693
+ The fixed vocabulary is:
694
+
695
+ - \`EBK-TODO\` — known implementation work;
696
+ - \`EBK-ASSUMPTION\` — a decision that needs validation;
697
+ - \`EBK-QUESTION\` — an unresolved design or product question;
698
+ - \`EBK-SECURITY\` — security work that blocks protected promotion; and
699
+ - \`EBK-BLOCKER\` — any other work that blocks protected promotion.
700
+
701
+ Vocabulary names used in comment prose are inert. The scanner treats only a kind immediately
702
+ followed by \`(\` or \`:\` as a marker attempt, so malformed marker-shaped comments still fail
703
+ validation.
704
+
705
+ Until Work Orchestration imports these markers, references must be an existing GitHub issue
706
+ (\`GH:<positive-number>\`) or an append-only roadmap key
707
+ (\`PLAN:<phase>/<slug>\`). The description must remain useful without opening the reference.
708
+ The scanner validates syntax and ownership now; it intentionally does not claim that a
709
+ reference exists. Phase 2 owns reference import, retained old-to-new ID mapping, and later
710
+ orphan checks. Do not invent temporary Work Orchestration numbers.
711
+
712
+ ## Generator and agent workflow
713
+
714
+ Generators may emit a marker only into a \`seeded\` customization surface. They must never put
715
+ one in a \`managed\` file: managed output stays deterministic, and a needed change there is a
716
+ framework defect to report outside the generated file. Adopter-owned files may also contain
717
+ markers. Framework upgrades preserve seeded source and its unresolved markers.
718
+
719
+ Blueprint and repository update tooling may create a deterministic reconciliation marker in a
720
+ seeded or adopter-owned file when a preserved customization needs human or agent judgment. The
721
+ update must preserve the custom code and identify the changed contract in the description; a
722
+ successful update command alone does not resolve that marker. Reconcile the code, run the
723
+ relevant checks, retain evidence at the referenced work item, and only then remove the marker.
724
+ Automatic insertion requires an explicit publisher migration or other deterministic anchor;
725
+ the generic upgrade command does not guess where to edit preserved custom code.
726
+
727
+ At the start and handoff of agent work, run:
728
+
729
+ \`pnpm nx run ${name}:work-markers\`
730
+
731
+ The same ordinary validation runs through workspace health and CI. Complete the referenced
732
+ custom work and remove the marker; do not merely delete it while leaving the requirement
733
+ unresolved. Open \`TODO\`, \`ASSUMPTION\`, and \`QUESTION\` markers are reported but do not block
734
+ deployment by themselves. Open \`SECURITY\` and \`BLOCKER\` markers fail staging and production
735
+ deploy or refresh before AWS access. Protected remove and unlock remain usable for recovery.
736
+ There is no local or silent bypass in this prerelease contract; any future waiver mechanism
737
+ must be committed, reviewable, and auditable.
738
+ `,
739
+ 'ebk.repository-policy.json': json((0, index_js_3.createIncubationRepositoryPolicy)()),
249
740
  'nx.json': json({
250
741
  $schema: './node_modules/nx/schemas/nx-schema.json',
251
- defaultBase: 'main',
742
+ defaultBase: 'develop',
252
743
  namedInputs: {
253
744
  default: ['{projectRoot}/**/*', 'sharedGlobals'],
254
745
  production: [
@@ -324,6 +815,13 @@ Foundation. Personal and preview consumer stages may use only the \`development\
324
815
  projectType: 'application',
325
816
  tags: ['scope:workspace', 'layer:governance'],
326
817
  targets: {
818
+ 'deployment-plan': {
819
+ executor: '@empire-builder-kit/nx:deployment-plan',
820
+ options: {
821
+ action: 'deploy',
822
+ stage: 'staging',
823
+ },
824
+ },
327
825
  dev: {
328
826
  continuous: true,
329
827
  executor: '@empire-builder-kit/nx:fleet',
@@ -339,6 +837,18 @@ Foundation. Personal and preview consumer stages may use only the \`development\
339
837
  executor: '@empire-builder-kit/nx:dev-setup',
340
838
  options: {},
341
839
  },
840
+ 'github-bootstrap': {
841
+ executor: '@empire-builder-kit/nx:github-bootstrap',
842
+ options: {},
843
+ },
844
+ 'install-git-hooks': {
845
+ executor: 'nx:run-commands',
846
+ options: { command: 'node tools/install-ebk-hooks.mjs' },
847
+ },
848
+ 'work-markers': {
849
+ executor: '@empire-builder-kit/nx:work-markers',
850
+ options: {},
851
+ },
342
852
  verify: {
343
853
  executor: 'nx:run-commands',
344
854
  options: { command: 'node tools/workspace-health.mjs' },
@@ -380,7 +890,13 @@ domain. Without it, the shared Router uses its functional provider URL.
380
890
  ],
381
891
  }),
382
892
  'packages/foundation/contracts/costs.json': json({
383
- schemaVersion: 1,
893
+ schemaVersion: 2,
894
+ aurora: {
895
+ engine: index_js_3.FOUNDATION_AURORA_ENGINE_FAMILY,
896
+ engineVersion: index_js_3.FOUNDATION_AURORA_ENGINE_VERSION,
897
+ },
898
+ productionProfiles: index_js_3.PRODUCTION_FOUNDATION_PROFILE_CONTRACTS,
899
+ productionProfileSource: 'ebk.repository-policy.json',
384
900
  stages: {
385
901
  development: {
386
902
  aurora: 'scale-to-zero',
@@ -393,7 +909,7 @@ domain. Without it, the shared Router uses its functional provider URL.
393
909
  nat: 'ec2',
394
910
  },
395
911
  production: {
396
- aurora: 'always-warm',
912
+ aurora: 'repository-policy',
397
913
  dataRetention: 'retained-and-protected',
398
914
  nat: 'managed',
399
915
  },
@@ -415,7 +931,9 @@ seeded decision record aligned with organization-specific budgets and alerts.
415
931
 
416
932
  - Development uses EC2 NAT, a development bastion, and Aurora scale-to-zero.
417
933
  - Staging uses EC2 NAT and Aurora scale-to-zero by default.
418
- - Production uses managed NAT and an always-warm Aurora baseline.
934
+ - Production always uses managed NAT and retains protected data. Its explicit Repository
935
+ Operating Policy profile selects either the standard always-warm Aurora floor or the
936
+ prerelease cost-optimized auto-pause candidate.
419
937
  - ECS, EventBridge, Cognito, SNS, SSM, and CloudWatch are primarily usage-priced; logs,
420
938
  metrics, alarms, and retained data still require active cost review.
421
939
 
@@ -474,6 +992,11 @@ declare const $app: {
474
992
  foundationSsmPath,
475
993
  type FoundationSsmKey,
476
994
  } from '@empire-builder-kit/nx/foundation';
995
+ import {
996
+ FOUNDATION_AURORA_ENGINE,
997
+ FOUNDATION_AURORA_ENGINE_VERSION,
998
+ productionFoundationProfileContract,
999
+ } from '@empire-builder-kit/nx/repository-policy';
477
1000
 
478
1001
  function publishFoundation(values: Record<FoundationSsmKey, any>) {
479
1002
  return Object.fromEntries(
@@ -492,11 +1015,16 @@ export function foundation() {
492
1015
  const isProduction = $app.stage === 'production';
493
1016
  const isStaging = $app.stage === 'staging';
494
1017
  const rootDomain = process.env.EBK_ROOT_DOMAIN?.trim();
1018
+ const productionConfiguration = isProduction
1019
+ ? productionFoundationProfileContract(
1020
+ process.env.EBK_PRODUCTION_FOUNDATION_PROFILE,
1021
+ )
1022
+ : undefined;
495
1023
 
496
1024
  const vpc = new sst.aws.Vpc('FoundationVpc', {
497
1025
  az: 2,
498
1026
  bastion: $app.stage === 'development',
499
- nat: isProduction ? 'managed' : 'ec2',
1027
+ nat: productionConfiguration?.contract.nat ?? 'ec2',
500
1028
  });
501
1029
  const cluster = new sst.aws.Cluster('FoundationCluster', { vpc });
502
1030
  const router = new sst.aws.Router('FoundationRouter', {
@@ -512,16 +1040,19 @@ export function foundation() {
512
1040
  });
513
1041
  const database = new sst.aws.Aurora('FoundationDatabase', {
514
1042
  dataApi: true,
515
- engine: 'postgres',
516
- scaling: isProduction
517
- ? { min: '2 ACU', max: '16 ACU' }
518
- : isStaging
1043
+ engine: FOUNDATION_AURORA_ENGINE,
1044
+ version: FOUNDATION_AURORA_ENGINE_VERSION,
1045
+ scaling:
1046
+ productionConfiguration?.contract.aurora ??
1047
+ (isStaging
519
1048
  ? { min: '0 ACU', max: '4 ACU', pauseAfter: '5 minutes' }
520
- : { min: '0 ACU', max: '2 ACU', pauseAfter: '5 minutes' },
1049
+ : { min: '0 ACU', max: '2 ACU', pauseAfter: '5 minutes' }),
521
1050
  transform: {
522
1051
  cluster: {
523
- backupRetentionPeriod: isProduction ? 30 : 7,
524
- deletionProtection: isProduction,
1052
+ backupRetentionPeriod:
1053
+ productionConfiguration?.contract.backupRetentionDays ?? 7,
1054
+ deletionProtection:
1055
+ productionConfiguration?.contract.deletionProtection ?? false,
525
1056
  },
526
1057
  },
527
1058
  vpc,
@@ -531,9 +1062,39 @@ export function foundation() {
531
1062
  });
532
1063
  const eventBus = new sst.aws.Bus('FoundationEventBus');
533
1064
  const alertTopic = new sst.aws.SnsTopic('FoundationAlerts');
1065
+ const alertTopicPolicy = new aws.sns.TopicPolicy(
1066
+ 'FoundationAlertTopicPolicy',
1067
+ {
1068
+ arn: alertTopic.arn,
1069
+ policy: aws.iam.getPolicyDocumentOutput({
1070
+ statements: [
1071
+ {
1072
+ actions: ['sns:Publish'],
1073
+ principals: [
1074
+ { identifiers: ['events.rds.amazonaws.com'], type: 'Service' },
1075
+ ],
1076
+ resources: [alertTopic.arn],
1077
+ },
1078
+ ],
1079
+ }).json,
1080
+ },
1081
+ );
1082
+ const databaseInstanceId = database.nodes[0].identifier;
1083
+ const databaseEvents = isProduction
1084
+ ? new aws.rds.EventSubscription(
1085
+ 'FoundationDatabaseEvents',
1086
+ {
1087
+ eventCategories: ['failure', 'notification', 'serverless'],
1088
+ snsTopic: alertTopic.arn,
1089
+ sourceIds: database.nodes.map((node: any) => node.identifier),
1090
+ sourceType: 'db-instance',
1091
+ },
1092
+ { dependsOn: [alertTopicPolicy] },
1093
+ )
1094
+ : undefined;
534
1095
  const dashboard = new aws.cloudwatch.Dashboard('FoundationDashboard', {
535
1096
  dashboardName: 'ebk-' + $app.stage + '-foundation',
536
- dashboardBody: JSON.stringify({
1097
+ dashboardBody: databaseInstanceId.apply((identifier: string) => JSON.stringify({
537
1098
  widgets: [
538
1099
  {
539
1100
  type: 'text',
@@ -542,12 +1103,64 @@ export function foundation() {
542
1103
  width: 24,
543
1104
  height: 3,
544
1105
  properties: {
545
- markdown: '# Empire Builder Kit Foundation\\nStage: ' + $app.stage,
1106
+ markdown:
1107
+ '# Empire Builder Kit Foundation\\nStage: ' +
1108
+ $app.stage +
1109
+ '\\nProduction profile: ' +
1110
+ (productionConfiguration?.profile ?? 'not-applicable'),
1111
+ },
1112
+ },
1113
+ {
1114
+ type: 'metric',
1115
+ x: 0,
1116
+ y: 3,
1117
+ width: 12,
1118
+ height: 6,
1119
+ properties: {
1120
+ title: 'Aurora capacity and pause state',
1121
+ period: 60,
1122
+ stat: 'Minimum',
1123
+ metrics: [
1124
+ ['AWS/RDS', 'ServerlessDatabaseCapacity', 'DBInstanceIdentifier', identifier],
1125
+ ['.', 'ACUUtilization', '.', '.'],
1126
+ ],
1127
+ },
1128
+ },
1129
+ {
1130
+ type: 'metric',
1131
+ x: 12,
1132
+ y: 3,
1133
+ width: 12,
1134
+ height: 6,
1135
+ properties: {
1136
+ title: 'Aurora connections',
1137
+ period: 60,
1138
+ stat: 'Maximum',
1139
+ metrics: [
1140
+ ['AWS/RDS', 'DatabaseConnections', 'DBInstanceIdentifier', identifier],
1141
+ ],
546
1142
  },
547
1143
  },
548
1144
  ],
549
- }),
1145
+ })),
550
1146
  });
1147
+ const databaseCapacityAlarm = new aws.cloudwatch.MetricAlarm(
1148
+ 'FoundationDatabaseCapacityAlarm',
1149
+ {
1150
+ alarmActions: [alertTopic.arn],
1151
+ alarmDescription:
1152
+ 'Aurora Serverless capacity utilization stayed above 90 percent.',
1153
+ comparisonOperator: 'GreaterThanThreshold',
1154
+ dimensions: { DBInstanceIdentifier: databaseInstanceId },
1155
+ evaluationPeriods: 5,
1156
+ metricName: 'ACUUtilization',
1157
+ namespace: 'AWS/RDS',
1158
+ period: 60,
1159
+ statistic: 'Average',
1160
+ threshold: 90,
1161
+ treatMissingData: 'notBreaching',
1162
+ },
1163
+ );
551
1164
 
552
1165
  if (new Set(['development', 'staging', 'production']).has($app.stage)) {
553
1166
  new aws.cloudwatch.EventArchive('FoundationEventArchive', {
@@ -581,11 +1194,14 @@ export function foundation() {
581
1194
  capabilities: STANDARD_FOUNDATION_CAPABILITIES,
582
1195
  clusterId: cluster.id,
583
1196
  contractVersion: '1.0.0',
1197
+ databaseCapacityAlarmName: databaseCapacityAlarm.name,
584
1198
  dashboardName: dashboard.dashboardName,
585
1199
  databaseClusterId: database.id,
1200
+ databaseEventSubscriptionId: databaseEvents?.id,
586
1201
  eventBusName: eventBus.name,
587
1202
  identityUserPoolId: userPool.id,
588
1203
  name: 'foundation',
1204
+ productionFoundationProfile: productionConfiguration?.profile,
589
1205
  rootDomain,
590
1206
  routerUrl: router.url,
591
1207
  vpcId: vpc.id,
@@ -613,7 +1229,10 @@ export function foundation() {
613
1229
  targets: {
614
1230
  deploy: {
615
1231
  executor: '@empire-builder-kit/nx:deploy',
616
- dependsOn: ['verify'],
1232
+ dependsOn: [
1233
+ 'verify',
1234
+ { target: 'profile-preflight', params: 'forward' },
1235
+ ],
617
1236
  options: {
618
1237
  cwd: 'packages/foundation',
619
1238
  stage: 'development',
@@ -638,6 +1257,7 @@ export function foundation() {
638
1257
  },
639
1258
  refresh: {
640
1259
  executor: '@empire-builder-kit/nx:refresh',
1260
+ dependsOn: [{ target: 'profile-preflight', params: 'forward' }],
641
1261
  options: {
642
1262
  cwd: 'packages/foundation',
643
1263
  stage: 'development',
@@ -657,6 +1277,10 @@ export function foundation() {
657
1277
  executor: '@empire-builder-kit/nx:foundation-remove-preflight',
658
1278
  options: { stage: 'development' },
659
1279
  },
1280
+ 'profile-preflight': {
1281
+ executor: '@empire-builder-kit/nx:foundation-profile-preflight',
1282
+ options: { stage: 'development' },
1283
+ },
660
1284
  typecheck: {
661
1285
  executor: 'nx:run-commands',
662
1286
  options: {
@@ -709,7 +1333,11 @@ if (JSON.stringify(descriptor.capabilities) !== JSON.stringify(expectedCapabilit
709
1333
  throw new Error('Foundation capability descriptor does not match the v1 standard.');
710
1334
  }
711
1335
  if (
712
- costs.schemaVersion !== 1 ||
1336
+ costs.schemaVersion !== 2 ||
1337
+ costs.aurora?.engineVersion !== '${index_js_3.FOUNDATION_AURORA_ENGINE_VERSION}' ||
1338
+ costs.productionProfileSource !== 'ebk.repository-policy.json' ||
1339
+ costs.productionProfiles?.standard?.aurora?.min !== '2 ACU' ||
1340
+ costs.productionProfiles?.['cost-optimized']?.aurora?.min !== '0 ACU' ||
713
1341
  !costs.stages?.development ||
714
1342
  !costs.stages?.staging ||
715
1343
  costs.stages?.production?.dataRetention !== 'retained-and-protected'
@@ -792,7 +1420,69 @@ export default $config({
792
1420
  files: [],
793
1421
  references: [],
794
1422
  }),
795
- 'tools/workspace-health.mjs': `import { createHash } from 'node:crypto';
1423
+ 'tools/ebk-git-policy.mjs': `import { evaluateGitPolicy } from '@empire-builder-kit/nx/git-policy';
1424
+ import { spawnSync } from 'node:child_process';
1425
+
1426
+ const git = (...args) => {
1427
+ const result = spawnSync('git', args, { encoding: 'utf8', shell: false });
1428
+ if (result.status !== 0) {
1429
+ throw new Error('git ' + args.join(' ') + ' failed: ' + (result.stderr || result.stdout).trim());
1430
+ }
1431
+ return result.stdout.trim();
1432
+ };
1433
+ const isAncestor = (ancestor, descendant) =>
1434
+ spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
1435
+ encoding: 'utf8',
1436
+ shell: false,
1437
+ }).status === 0;
1438
+
1439
+ const eventName = process.env.GITHUB_EVENT_NAME;
1440
+ if (!['pull_request', 'push', 'workflow_dispatch'].includes(eventName)) {
1441
+ throw new Error('Unsupported or missing GITHUB_EVENT_NAME; Git policy fails closed.');
1442
+ }
1443
+
1444
+ const request = { eventName, ref: process.env.GITHUB_REF };
1445
+ if (eventName === 'pull_request' || eventName === 'push') {
1446
+ git(
1447
+ 'fetch',
1448
+ '--no-tags',
1449
+ 'origin',
1450
+ 'main:refs/remotes/origin/main',
1451
+ 'develop:refs/remotes/origin/develop',
1452
+ );
1453
+ }
1454
+ if (eventName === 'pull_request') {
1455
+ const baseRef = process.env.GITHUB_BASE_REF;
1456
+ const headRef = process.env.GITHUB_HEAD_REF;
1457
+ if (!baseRef || !headRef) {
1458
+ throw new Error('Pull-request branch metadata is missing.');
1459
+ }
1460
+ Object.assign(request, {
1461
+ baseIsAncestorOfHead: isAncestor('refs/remotes/origin/' + baseRef, 'HEAD'),
1462
+ baseRef,
1463
+ headRef,
1464
+ mainIsAncestorOfDevelop: isAncestor(
1465
+ 'refs/remotes/origin/main',
1466
+ 'refs/remotes/origin/develop',
1467
+ ),
1468
+ });
1469
+ } else if (eventName === 'push') {
1470
+ Object.assign(request, {
1471
+ baseIsAncestorOfHead: isAncestor('refs/remotes/origin/develop', 'HEAD'),
1472
+ mainIsAncestorOfDevelop: isAncestor(
1473
+ 'refs/remotes/origin/main',
1474
+ 'refs/remotes/origin/develop',
1475
+ ),
1476
+ });
1477
+ }
1478
+
1479
+ const decision = evaluateGitPolicy(request);
1480
+ process.stdout.write(JSON.stringify(decision, null, 2) + '\\n');
1481
+ if (!decision.allowed) process.exitCode = 1;
1482
+ `,
1483
+ 'tools/workspace-health.mjs': `import { validateRepositoryOperatingPolicy } from '@empire-builder-kit/nx/repository-policy';
1484
+ import { assertWorkMarkerReport, formatWorkMarkerReport, scanWorkMarkers } from '@empire-builder-kit/nx/work-markers';
1485
+ import { createHash } from 'node:crypto';
796
1486
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
797
1487
  import { join } from 'node:path';
798
1488
 
@@ -853,6 +1543,14 @@ if (
853
1543
  validateOwnership('.', workspace, 'Workspace');
854
1544
  validateOwnership('packages/foundation', foundation, 'Foundation');
855
1545
 
1546
+ const policy = validateRepositoryOperatingPolicy(readJson('ebk.repository-policy.json'));
1547
+ if (!policy.valid) {
1548
+ throw new Error(
1549
+ 'Repository Operating Policy is invalid:\\n' +
1550
+ policy.issues.map(({ message, path }) => '- ' + path + ': ' + message).join('\\n'),
1551
+ );
1552
+ }
1553
+
856
1554
  for (const name of readdirSync('packages')) {
857
1555
  if (name === 'foundation') continue;
858
1556
  const ownershipPath = join('packages', name, '.ebk/ownership.json');
@@ -863,6 +1561,9 @@ for (const name of readdirSync('packages')) {
863
1561
  }
864
1562
  validateOwnership(join('packages', name), ownership, 'Product Slice ' + name);
865
1563
  }
1564
+ const markerReport = scanWorkMarkers(process.cwd());
1565
+ process.stdout.write(formatWorkMarkerReport(markerReport) + '\\n');
1566
+ assertWorkMarkerReport(markerReport);
866
1567
  process.stdout.write('Empire Builder Kit workspace source health is green.\\n');
867
1568
  `,
868
1569
  };