@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.
- 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 +30 -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 +739 -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 +107 -0
- package/src/generators/repository-policy/repository-policy.js.map +1 -0
- package/src/generators/repository-policy/schema.d.ts +18 -0
- package/src/generators/repository-policy/schema.json +46 -0
- package/src/generators/slice/slice.js +22 -0
- package/src/generators/slice/slice.js.map +1 -1
- package/src/generators/upgrade/upgrade.js +16 -0
- package/src/generators/upgrade/upgrade.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 +23 -0
- package/src/git-policy/github-bootstrap.js +759 -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 +59 -0
- package/src/repository-policy/policy.js +274 -0
- package/src/repository-policy/policy.js.map +1 -0
- package/src/schemas/repository-policy-v1.json +58 -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,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: '
|
|
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:
|
|
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: '
|
|
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
|
|
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:
|
|
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:
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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:
|
|
524
|
-
|
|
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:
|
|
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: [
|
|
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 !==
|
|
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/
|
|
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
|
};
|