@empire-builder-kit/nx 1.0.0-beta.10
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/LICENSE +201 -0
- package/NOTICE +5 -0
- package/README.md +34 -0
- package/executors.json +74 -0
- package/generators.json +40 -0
- package/migrations/standard-pre-v1-to-v1.md +11 -0
- package/package.json +80 -0
- package/src/blueprints/execution.d.ts +31 -0
- package/src/blueprints/execution.js +166 -0
- package/src/blueprints/execution.js.map +1 -0
- package/src/blueprints/index.d.ts +5 -0
- package/src/blueprints/index.js +9 -0
- package/src/blueprints/index.js.map +1 -0
- package/src/blueprints/inputs.d.ts +43 -0
- package/src/blueprints/inputs.js +103 -0
- package/src/blueprints/inputs.js.map +1 -0
- package/src/blueprints/manifest.d.ts +281 -0
- package/src/blueprints/manifest.js +441 -0
- package/src/blueprints/manifest.js.map +1 -0
- package/src/blueprints/resolver.d.ts +28 -0
- package/src/blueprints/resolver.js +218 -0
- package/src/blueprints/resolver.js.map +1 -0
- package/src/blueprints/transitions.d.ts +20 -0
- package/src/blueprints/transitions.js +61 -0
- package/src/blueprints/transitions.js.map +1 -0
- package/src/executors/deploy/deploy.d.ts +4 -0
- package/src/executors/deploy/deploy.js +6 -0
- package/src/executors/deploy/deploy.js.map +1 -0
- 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/dev/dev.d.ts +4 -0
- package/src/executors/dev/dev.js +6 -0
- package/src/executors/dev/dev.js.map +1 -0
- package/src/executors/dev-doctor/dev-doctor.d.ts +31 -0
- package/src/executors/dev-doctor/dev-doctor.js +260 -0
- package/src/executors/dev-doctor/dev-doctor.js.map +1 -0
- package/src/executors/dev-doctor/schema.json +17 -0
- package/src/executors/dev-setup/dev-setup.d.ts +11 -0
- package/src/executors/dev-setup/dev-setup.js +454 -0
- package/src/executors/dev-setup/dev-setup.js.map +1 -0
- package/src/executors/dev-setup/schema.json +16 -0
- package/src/executors/fleet/fleet.d.ts +62 -0
- package/src/executors/fleet/fleet.js +613 -0
- package/src/executors/fleet/fleet.js.map +1 -0
- package/src/executors/fleet/schema.json +37 -0
- package/src/executors/foundation-preflight/foundation-preflight.d.ts +15 -0
- package/src/executors/foundation-preflight/foundation-preflight.js +116 -0
- package/src/executors/foundation-preflight/foundation-preflight.js.map +1 -0
- package/src/executors/foundation-preflight/schema.json +31 -0
- 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/foundation-remove-preflight/foundation-remove-preflight.d.ts +13 -0
- package/src/executors/foundation-remove-preflight/foundation-remove-preflight.js +69 -0
- package/src/executors/foundation-remove-preflight/foundation-remove-preflight.js.map +1 -0
- package/src/executors/foundation-remove-preflight/schema.json +17 -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/refresh/refresh.d.ts +4 -0
- package/src/executors/refresh/refresh.js +6 -0
- package/src/executors/refresh/refresh.js.map +1 -0
- package/src/executors/remove/remove.d.ts +4 -0
- package/src/executors/remove/remove.js +6 -0
- package/src/executors/remove/remove.js.map +1 -0
- package/src/executors/sst-lifecycle/run-sst-command.d.ts +31 -0
- package/src/executors/sst-lifecycle/run-sst-command.js +226 -0
- package/src/executors/sst-lifecycle/run-sst-command.js.map +1 -0
- package/src/executors/sst-lifecycle/schema.json +45 -0
- package/src/executors/unlock/unlock.d.ts +4 -0
- package/src/executors/unlock/unlock.js +6 -0
- package/src/executors/unlock/unlock.js.map +1 -0
- 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/foundation/aurora-logical-database.d.ts +76 -0
- package/src/foundation/aurora-logical-database.js +419 -0
- package/src/foundation/aurora-logical-database.js.map +1 -0
- package/src/foundation/discovery.d.ts +55 -0
- package/src/foundation/discovery.js +55 -0
- package/src/foundation/discovery.js.map +1 -0
- package/src/foundation/index.d.ts +6 -0
- package/src/foundation/index.js +10 -0
- package/src/foundation/index.js.map +1 -0
- package/src/foundation/local-database-contract.d.ts +30 -0
- package/src/foundation/local-database-contract.js +75 -0
- package/src/foundation/local-database-contract.js.map +1 -0
- package/src/foundation/preflight.d.ts +31 -0
- package/src/foundation/preflight.js +114 -0
- package/src/foundation/preflight.js.map +1 -0
- package/src/foundation/provision-database-bindings.d.ts +30 -0
- package/src/foundation/provision-database-bindings.js +113 -0
- package/src/foundation/provision-database-bindings.js.map +1 -0
- package/src/foundation/provision-logical-database.d.ts +26 -0
- package/src/foundation/provision-logical-database.js +163 -0
- package/src/foundation/provision-logical-database.js.map +1 -0
- package/src/foundation/requirements.d.ts +37 -0
- package/src/foundation/requirements.js +141 -0
- package/src/foundation/requirements.js.map +1 -0
- package/src/foundation/stages.d.ts +14 -0
- package/src/foundation/stages.js +51 -0
- package/src/foundation/stages.js.map +1 -0
- package/src/generators/function/function.d.ts +13 -0
- package/src/generators/function/function.js +71 -0
- package/src/generators/function/function.js.map +1 -0
- package/src/generators/function/schema.d.ts +5 -0
- package/src/generators/function/schema.json +20 -0
- package/src/generators/job/job.d.ts +4 -0
- package/src/generators/job/job.js +5 -0
- package/src/generators/job/job.js.map +1 -0
- package/src/generators/preset/generator.d.ts +5 -0
- package/src/generators/preset/generator.js +41 -0
- package/src/generators/preset/generator.js.map +1 -0
- package/src/generators/preset/schema.d.ts +4 -0
- package/src/generators/preset/schema.json +22 -0
- package/src/generators/preset/workspace-files.d.ts +18 -0
- package/src/generators/preset/workspace-files.js +1605 -0
- package/src/generators/preset/workspace-files.js.map +1 -0
- 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/service/service.d.ts +4 -0
- package/src/generators/service/service.js +5 -0
- package/src/generators/service/service.js.map +1 -0
- package/src/generators/slice/schema.d.ts +7 -0
- package/src/generators/slice/schema.json +38 -0
- package/src/generators/slice/slice.d.ts +26 -0
- package/src/generators/slice/slice.js +1698 -0
- package/src/generators/slice/slice.js.map +1 -0
- package/src/generators/upgrade/schema.d.ts +8 -0
- package/src/generators/upgrade/schema.json +31 -0
- package/src/generators/upgrade/upgrade.d.ts +27 -0
- package/src/generators/upgrade/upgrade.js +682 -0
- package/src/generators/upgrade/upgrade.js.map +1 -0
- package/src/generators/workload/schema.d.ts +6 -0
- package/src/generators/workload/schema.json +21 -0
- package/src/generators/workload/workload.d.ts +16 -0
- package/src/generators/workload/workload.js +157 -0
- package/src/generators/workload/workload.js.map +1 -0
- 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 +8 -0
- package/src/index.js +12 -0
- package/src/index.js.map +1 -0
- package/src/local-dev/contracts.d.ts +31 -0
- package/src/local-dev/contracts.js +126 -0
- package/src/local-dev/contracts.js.map +1 -0
- package/src/local-dev/effects.d.ts +16 -0
- package/src/local-dev/effects.js +63 -0
- package/src/local-dev/effects.js.map +1 -0
- package/src/local-dev/gateway-daemon.d.ts +4 -0
- package/src/local-dev/gateway-daemon.js +39 -0
- package/src/local-dev/gateway-daemon.js.map +1 -0
- package/src/local-dev/gateway.d.ts +60 -0
- package/src/local-dev/gateway.js +368 -0
- package/src/local-dev/gateway.js.map +1 -0
- package/src/local-dev/identity.d.ts +16 -0
- package/src/local-dev/identity.js +101 -0
- package/src/local-dev/identity.js.map +1 -0
- package/src/local-dev/index.d.ts +7 -0
- package/src/local-dev/index.js +11 -0
- package/src/local-dev/index.js.map +1 -0
- package/src/local-dev/portless.d.ts +15 -0
- package/src/local-dev/portless.js +115 -0
- package/src/local-dev/portless.js.map +1 -0
- package/src/local-dev/ports.d.ts +10 -0
- package/src/local-dev/ports.js +39 -0
- package/src/local-dev/ports.js.map +1 -0
- package/src/local-dev/proxy.d.ts +37 -0
- package/src/local-dev/proxy.js +271 -0
- package/src/local-dev/proxy.js.map +1 -0
- package/src/local-dev/tls.d.ts +24 -0
- package/src/local-dev/tls.js +294 -0
- package/src/local-dev/tls.js.map +1 -0
- package/src/ownership/composition.d.ts +16 -0
- package/src/ownership/composition.js +125 -0
- package/src/ownership/composition.js.map +1 -0
- package/src/ownership/foundation-record.d.ts +66 -0
- package/src/ownership/foundation-record.js +108 -0
- package/src/ownership/foundation-record.js.map +1 -0
- package/src/ownership/hash.d.ts +2 -0
- package/src/ownership/hash.js +10 -0
- package/src/ownership/hash.js.map +1 -0
- package/src/ownership/index.d.ts +6 -0
- package/src/ownership/index.js +10 -0
- package/src/ownership/index.js.map +1 -0
- package/src/ownership/json-ownership.d.ts +13 -0
- package/src/ownership/json-ownership.js +147 -0
- package/src/ownership/json-ownership.js.map +1 -0
- package/src/ownership/record.d.ts +186 -0
- package/src/ownership/record.js +336 -0
- package/src/ownership/record.js.map +1 -0
- package/src/ownership/workspace-record.d.ts +66 -0
- package/src/ownership/workspace-record.js +108 -0
- package/src/ownership/workspace-record.js.map +1 -0
- 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/blueprint-input-schema-v1.json +23 -0
- package/src/schemas/blueprint-v1.json +186 -0
- package/src/schemas/foundation-ownership-v2.json +42 -0
- package/src/schemas/foundation-requirement-v1.json +32 -0
- package/src/schemas/generated-ownership-v2.json +117 -0
- package/src/schemas/repository-policy-v1.json +58 -0
- package/src/schemas/workspace-ownership-v2.json +42 -0
- package/src/validation.d.ts +14 -0
- package/src/validation.js +14 -0
- package/src/validation.js.map +1 -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
|
@@ -0,0 +1,1605 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WORKSPACE_PACKAGE_MANAGER_VERSION = exports.WORKSPACE_PROJECT_MANAGED_JSON_POINTERS = void 0;
|
|
4
|
+
exports.renderWorkspaceFiles = renderWorkspaceFiles;
|
|
5
|
+
exports.renderWorkspaceRootFilePlan = renderWorkspaceRootFilePlan;
|
|
6
|
+
exports.renderFoundationFilePlan = renderFoundationFilePlan;
|
|
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");
|
|
10
|
+
const TOOL_VERSIONS = {
|
|
11
|
+
eslint: '^9.8.0',
|
|
12
|
+
eslintJs: '^9.8.0',
|
|
13
|
+
globals: '^16.0.0',
|
|
14
|
+
nodeTypes: '25.5.0',
|
|
15
|
+
nx: '23.1.0',
|
|
16
|
+
pnpm: '10.30.1',
|
|
17
|
+
prettier: '^3.8.2',
|
|
18
|
+
sst: '4.11.0',
|
|
19
|
+
typescript: '~5.9.2',
|
|
20
|
+
typescriptEslint: '^8.40.0',
|
|
21
|
+
};
|
|
22
|
+
function json(value) {
|
|
23
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
24
|
+
}
|
|
25
|
+
const FOUNDATION_ROOT = 'packages/foundation/';
|
|
26
|
+
const FOUNDATION_OWNERSHIP_PATH = `${FOUNDATION_ROOT}.ebk/ownership.json`;
|
|
27
|
+
const FOUNDATION_TOOL_OWNED_PATHS = new Set(['sst-env.d.ts']);
|
|
28
|
+
const WORKSPACE_OWNERSHIP_PATH = '.ebk/ownership.json';
|
|
29
|
+
const WORKSPACE_SEEDED_PATHS = new Set([
|
|
30
|
+
'.github/CODEOWNERS',
|
|
31
|
+
'AGENTS.md',
|
|
32
|
+
'CLAUDE.md',
|
|
33
|
+
'README.md',
|
|
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',
|
|
39
|
+
'package.json',
|
|
40
|
+
]);
|
|
41
|
+
exports.WORKSPACE_PROJECT_MANAGED_JSON_POINTERS = [
|
|
42
|
+
'/name',
|
|
43
|
+
'/$schema',
|
|
44
|
+
'/projectType',
|
|
45
|
+
'/tags',
|
|
46
|
+
'/targets/deployment-plan',
|
|
47
|
+
'/targets/dev',
|
|
48
|
+
'/targets/dev-doctor',
|
|
49
|
+
'/targets/dev-setup',
|
|
50
|
+
'/targets/github-bootstrap',
|
|
51
|
+
'/targets/install-git-hooks',
|
|
52
|
+
'/targets/verify',
|
|
53
|
+
'/targets/work-markers',
|
|
54
|
+
'/targets/workspace-health',
|
|
55
|
+
];
|
|
56
|
+
const FOUNDATION_SEEDED_PATHS = new Set([
|
|
57
|
+
'README.md',
|
|
58
|
+
'docs/costs.md',
|
|
59
|
+
'docs/runbooks.md',
|
|
60
|
+
'docs/security.md',
|
|
61
|
+
]);
|
|
62
|
+
function createFoundationOwnership(files, frameworkVersion) {
|
|
63
|
+
const paths = Object.keys(files)
|
|
64
|
+
.filter((path) => path.startsWith(FOUNDATION_ROOT) &&
|
|
65
|
+
path !== FOUNDATION_OWNERSHIP_PATH &&
|
|
66
|
+
!FOUNDATION_TOOL_OWNED_PATHS.has(path.slice(FOUNDATION_ROOT.length)))
|
|
67
|
+
.map((path) => {
|
|
68
|
+
const relativePath = path.slice(FOUNDATION_ROOT.length);
|
|
69
|
+
return {
|
|
70
|
+
mode: FOUNDATION_SEEDED_PATHS.has(relativePath)
|
|
71
|
+
? 'seeded'
|
|
72
|
+
: 'managed',
|
|
73
|
+
origin: 'framework',
|
|
74
|
+
path: relativePath,
|
|
75
|
+
sha256: (0, index_js_1.hashGeneratedContent)(files[path] ?? ''),
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
paths.sort((left, right) => left.path.localeCompare(right.path));
|
|
79
|
+
return {
|
|
80
|
+
frameworkVersion,
|
|
81
|
+
kind: 'foundation',
|
|
82
|
+
paths,
|
|
83
|
+
schemaVersion: index_js_1.FOUNDATION_OWNERSHIP_SCHEMA_VERSION,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function createWorkspaceOwnership(files, frameworkVersion) {
|
|
87
|
+
const paths = Object.keys(files)
|
|
88
|
+
.filter((path) => !path.startsWith(FOUNDATION_ROOT) && path !== WORKSPACE_OWNERSHIP_PATH)
|
|
89
|
+
.map((path) => {
|
|
90
|
+
const ownership = {
|
|
91
|
+
...(path === 'project.json'
|
|
92
|
+
? {
|
|
93
|
+
managedJsonPointers: [...exports.WORKSPACE_PROJECT_MANAGED_JSON_POINTERS],
|
|
94
|
+
}
|
|
95
|
+
: {}),
|
|
96
|
+
mode: WORKSPACE_SEEDED_PATHS.has(path)
|
|
97
|
+
? 'seeded'
|
|
98
|
+
: 'managed',
|
|
99
|
+
origin: 'framework',
|
|
100
|
+
path,
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
...ownership,
|
|
104
|
+
sha256: (0, index_js_1.hashOwnedContent)(files[path] ?? '', ownership),
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
paths.sort((left, right) => left.path.localeCompare(right.path));
|
|
108
|
+
return {
|
|
109
|
+
frameworkVersion,
|
|
110
|
+
kind: 'workspace',
|
|
111
|
+
paths,
|
|
112
|
+
schemaVersion: index_js_1.WORKSPACE_OWNERSHIP_SCHEMA_VERSION,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function renderWorkspaceFiles(options) {
|
|
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);
|
|
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),
|
|
127
|
+
'.github/workflows/ci.yml': `name: Verify
|
|
128
|
+
|
|
129
|
+
on:
|
|
130
|
+
pull_request:
|
|
131
|
+
branches: [develop, main]
|
|
132
|
+
push:
|
|
133
|
+
branches: [develop, main]
|
|
134
|
+
workflow_dispatch:
|
|
135
|
+
|
|
136
|
+
concurrency:
|
|
137
|
+
group: verify-\${{ github.ref }}
|
|
138
|
+
cancel-in-progress: true
|
|
139
|
+
|
|
140
|
+
permissions:
|
|
141
|
+
contents: read
|
|
142
|
+
|
|
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
|
+
|
|
160
|
+
verify:
|
|
161
|
+
name: verify
|
|
162
|
+
runs-on: ubuntu-latest
|
|
163
|
+
steps:
|
|
164
|
+
- uses: ${checkoutAction}
|
|
165
|
+
- uses: ${pnpmAction}
|
|
166
|
+
- uses: ${setupNodeAction}
|
|
167
|
+
with:
|
|
168
|
+
node-version: 22
|
|
169
|
+
cache: pnpm
|
|
170
|
+
- run: pnpm install --frozen-lockfile
|
|
171
|
+
- run: pnpm nx format:check
|
|
172
|
+
- run: pnpm nx run-many -t verify --parallel=3
|
|
173
|
+
`,
|
|
174
|
+
'.github/workflows/deploy.yml': `name: Deploy
|
|
175
|
+
|
|
176
|
+
on:
|
|
177
|
+
push:
|
|
178
|
+
branches: [develop, main]
|
|
179
|
+
workflow_dispatch:
|
|
180
|
+
inputs:
|
|
181
|
+
action:
|
|
182
|
+
description: Lifecycle action
|
|
183
|
+
required: true
|
|
184
|
+
default: deploy
|
|
185
|
+
type: choice
|
|
186
|
+
options: [deploy, refresh, unlock, remove]
|
|
187
|
+
project:
|
|
188
|
+
description: Required registered project for explicit unlock or remove
|
|
189
|
+
required: false
|
|
190
|
+
type: string
|
|
191
|
+
|
|
192
|
+
concurrency:
|
|
193
|
+
group: deploy-\${{ github.ref }}
|
|
194
|
+
cancel-in-progress: false
|
|
195
|
+
|
|
196
|
+
permissions:
|
|
197
|
+
contents: read
|
|
198
|
+
|
|
199
|
+
jobs:
|
|
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
|
|
240
|
+
if: github.ref == 'refs/heads/main'
|
|
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
|
|
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 }}
|
|
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 }}
|
|
332
|
+
runs-on: ubuntu-latest
|
|
333
|
+
steps:
|
|
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}
|
|
345
|
+
with:
|
|
346
|
+
role-to-assume: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
|
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}
|
|
353
|
+
with:
|
|
354
|
+
node-version: 22
|
|
355
|
+
cache: pnpm
|
|
356
|
+
- run: pnpm install --frozen-lockfile
|
|
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 + '.');
|
|
460
|
+
`,
|
|
461
|
+
'.gitignore': `.nx/cache
|
|
462
|
+
.nx/workspace-data
|
|
463
|
+
.ebk/cache
|
|
464
|
+
.next
|
|
465
|
+
.open-next
|
|
466
|
+
dist
|
|
467
|
+
*.tsbuildinfo
|
|
468
|
+
node_modules
|
|
469
|
+
.sst
|
|
470
|
+
.env
|
|
471
|
+
.env.*
|
|
472
|
+
!.env.example
|
|
473
|
+
`,
|
|
474
|
+
'.prettierignore': `node_modules
|
|
475
|
+
dist
|
|
476
|
+
.nx
|
|
477
|
+
.next
|
|
478
|
+
.open-next
|
|
479
|
+
.sst
|
|
480
|
+
**/next-env.d.ts
|
|
481
|
+
pnpm-lock.yaml
|
|
482
|
+
`,
|
|
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
|
+
`,
|
|
514
|
+
'eslint.config.mjs': `import eslint from '@eslint/js';
|
|
515
|
+
import globals from 'globals';
|
|
516
|
+
import tseslint from 'typescript-eslint';
|
|
517
|
+
|
|
518
|
+
export default tseslint.config(
|
|
519
|
+
{ ignores: ['**/.next/**', '**/.open-next/**', '**/.sst/**', '**/dist/**', '**/node_modules/**'] },
|
|
520
|
+
eslint.configs.recommended,
|
|
521
|
+
...tseslint.configs.recommended,
|
|
522
|
+
{
|
|
523
|
+
languageOptions: { globals: globals.node },
|
|
524
|
+
rules: { '@typescript-eslint/no-explicit-any': 'off' },
|
|
525
|
+
},
|
|
526
|
+
);
|
|
527
|
+
`,
|
|
528
|
+
'README.md': `# ${name}
|
|
529
|
+
|
|
530
|
+
This workspace was created with Empire Builder Kit v1.
|
|
531
|
+
|
|
532
|
+
## Start here
|
|
533
|
+
|
|
534
|
+
1. Review \`docs/architecture.md\`.
|
|
535
|
+
2. Review the generated Foundation before deploying it.
|
|
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.
|
|
539
|
+
|
|
540
|
+
For one-origin fleet development, first run \`pnpm nx run ${name}:dev-setup\`, inspect the
|
|
541
|
+
generated root certificate, then explicitly approve \`--trust\`. The supported v1 origin uses
|
|
542
|
+
the complete explicit-port HTTPS URL reported at startup; optional \`--portless\` setup is
|
|
543
|
+
experimental. Diagnose without changing trust or forwarding rules through
|
|
544
|
+
\`pnpm nx run ${name}:dev-doctor --stage=<personal-stage>\`.
|
|
545
|
+
\`dev-doctor\` trust and connectivity status requires a real exact-host TLS
|
|
546
|
+
handshake; optional portless access reports ready only after a live port-443 probe. Then complete
|
|
547
|
+
\`aws sso login --profile <bounded-development-profile>\`, then start selected Slices with
|
|
548
|
+
\`pnpm dev --slices=billing,accounts --profile=<bounded-development-profile>\`. Fleet reads
|
|
549
|
+
the region from that profile, propagates the AWS context to child Slices, and uses SST
|
|
550
|
+
\`mono\` mode so agents and non-cursor-addressable terminals receive ordinary logs.
|
|
551
|
+
|
|
552
|
+
Workspace creation only writes source files. It does not create or change cloud resources.
|
|
553
|
+
`,
|
|
554
|
+
'docs/architecture.md': `# Workspace architecture
|
|
555
|
+
|
|
556
|
+
This workspace uses three public Empire Builder Kit packages:
|
|
557
|
+
|
|
558
|
+
- \`@empire-builder-kit/create\` creates the workspace source tree.
|
|
559
|
+
- \`@empire-builder-kit/nx\` owns generators and infrastructure lifecycle executors.
|
|
560
|
+
- \`@empire-builder-kit/runtime\` owns application-facing runtime contracts.
|
|
561
|
+
|
|
562
|
+
The Standard Foundation lives at \`packages/foundation\`. Product slices live beside it under
|
|
563
|
+
\`packages/<slice-name>\` and consume versioned Foundation contracts instead of importing
|
|
564
|
+
Foundation implementation files.
|
|
565
|
+
|
|
566
|
+
Canonical consumer stages use the matching \`development\`, \`staging\`, or \`production\`
|
|
567
|
+
Foundation. Personal and preview consumer stages may use only the \`development\` Foundation.
|
|
568
|
+
`,
|
|
569
|
+
'docs/git-workflow.md': `# Protected Git workflow
|
|
570
|
+
|
|
571
|
+
Ordinary work starts from \`develop\` and returns through a reviewed pull request to
|
|
572
|
+
\`develop\`. A production promotion is exactly \`develop\` to \`main\`. After production,
|
|
573
|
+
review and merge the managed \`main\` to \`develop\` sync-back pull request before ordinary
|
|
574
|
+
work resumes.
|
|
575
|
+
|
|
576
|
+
Both protected branches require the stable \`policy\` and \`verify\` checks, one approving
|
|
577
|
+
review including an approval from the committed catch-all CODEOWNER and someone other than the
|
|
578
|
+
most recent pusher, dismissed stale reviews, resolved conversations, and merge commits. Replace
|
|
579
|
+
the placeholder in \`.github/CODEOWNERS\` with a GitHub user or team that has repository access
|
|
580
|
+
before bootstrap. An approval from \`github-actions[bot]\` cannot satisfy the independent
|
|
581
|
+
code-owner gate; the Actions repository setting is needed to create the managed sync-back pull
|
|
582
|
+
request, not to approve it.
|
|
583
|
+
Direct updates, force pushes, deletion, squash merging, and rebase merging are prohibited.
|
|
584
|
+
GitHub rulesets, environments, and AWS IAM are the authorization boundaries; local hooks are
|
|
585
|
+
only defense in depth.
|
|
586
|
+
|
|
587
|
+
The \`staging\` environment accepts only \`develop\`; \`production\` accepts only \`main\`.
|
|
588
|
+
The committed \`productionApproval.mode\` selects one of two production authorization
|
|
589
|
+
contracts:
|
|
590
|
+
|
|
591
|
+
- \`promotion-pr\` is compatible with private repositories on GitHub Team. The required
|
|
592
|
+
non-author CODEOWNER approval on the exact \`develop\` to \`main\` promotion pull request
|
|
593
|
+
is the production authorization. Merging that pull request starts production deployment;
|
|
594
|
+
there is no second EBK-managed environment approval or wait timer. The approver may also be
|
|
595
|
+
the human whose merge triggers deployment. Roll back through a reviewed revert on the
|
|
596
|
+
protected branches and let the resulting \`main\` push redeploy. A repository write actor who
|
|
597
|
+
can run Actions may also manually dispatch \`deploy\`, \`refresh\`, or \`unlock\` against the
|
|
598
|
+
already approved \`main\` tip without another human gate. That residual cannot introduce
|
|
599
|
+
unreviewed source; production \`remove\` remains separately variable-gated. Restrict write and
|
|
600
|
+
Actions access, audit dispatches, or select \`environment-reviewer\` when every production
|
|
601
|
+
action needs separate approval.
|
|
602
|
+
- \`environment-reviewer\` adds a separate production environment reviewer with self-review
|
|
603
|
+
prevention. A private repository needs GitHub Enterprise capability for this mode. Bootstrap
|
|
604
|
+
inspects that capability and fails closed on an unsupported plan; it never silently changes
|
|
605
|
+
the committed mode.
|
|
606
|
+
|
|
607
|
+
Both modes retain required checks, a non-author CODEOWNER approval, no ruleset bypass, the
|
|
608
|
+
literal production environment, and its branch-scoped credentials. Each environment owns its own
|
|
609
|
+
\`AWS_DEPLOY_ROLE_ARN\`, \`AWS_EXPECTED_ACCOUNT_ID\`, and \`AWS_REGION\`; production also
|
|
610
|
+
defines \`EBK_ALLOW_PRODUCTION_REMOVE\`, normally as \`false\`. Never add repository- or
|
|
611
|
+
organization-visible fallbacks for these protected values. Keep repository default workflow
|
|
612
|
+
permissions read-only; the generated jobs request only the additional permissions they need.
|
|
613
|
+
|
|
614
|
+
Run the GitHub bootstrap in report mode first. Source upgrades never alter remote GitHub
|
|
615
|
+
settings. Apply a reviewed report only after \`develop\` exists and the candidate's exact
|
|
616
|
+
\`policy\` and \`verify\` check contexts have run.
|
|
617
|
+
|
|
618
|
+
Set \`GH_REPO=<owner>/<repository>\` and a suitable \`GH_TOKEN\`, then run
|
|
619
|
+
\`pnpm nx run ${name}:github-bootstrap\`. Review the deterministic report before rerunning
|
|
620
|
+
with \`--apply\`. The command never creates branches, credentials, secrets, or AWS resources.
|
|
621
|
+
Supply \`--productionReviewerIds=<id,...>\` only when committed policy selects
|
|
622
|
+
\`environment-reviewer\`; it is rejected in \`promotion-pr\` mode.
|
|
623
|
+
|
|
624
|
+
Install the optional defense-in-depth pre-push hook explicitly with
|
|
625
|
+
\`pnpm nx run ${name}:install-git-hooks\`. It refuses to overwrite an unrelated existing hook.
|
|
626
|
+
|
|
627
|
+
The sync-back workflow uses only \`GITHUB_TOKEN\` and never pushes or auto-merges. GitHub may
|
|
628
|
+
place its pull-request workflows in an approval-required state. Approve them; if no run
|
|
629
|
+
materializes, close and reopen the pull request as a human and then approve it.
|
|
630
|
+
`,
|
|
631
|
+
'docs/repository-operating-policy.md': `# Repository Operating Policy
|
|
632
|
+
|
|
633
|
+
\`ebk.repository-policy.json\` is the reviewed source of truth for production access,
|
|
634
|
+
production approval, per-Slice readiness, and the one shared production Foundation profile.
|
|
635
|
+
New workspaces start with production disabled and explicit Team-compatible
|
|
636
|
+
\`productionApproval.mode: "promotion-pr"\`. A missing approval mode or Slice entry fails
|
|
637
|
+
closed; no GitHub capability is allowed to choose a mode implicitly.
|
|
638
|
+
|
|
639
|
+
The named repository modes are source-writing presets, not runtime state. Report a change
|
|
640
|
+
first and inspect the complete current/next policy:
|
|
641
|
+
|
|
642
|
+
\`\`\`bash
|
|
643
|
+
pnpm nx g @empire-builder-kit/nx:repository-policy \\
|
|
644
|
+
--preset=controlled-production --slices=billing
|
|
645
|
+
\`\`\`
|
|
646
|
+
|
|
647
|
+
Apply the reviewed source change separately with \`--apply\`. This command never contacts AWS
|
|
648
|
+
or GitHub. Use \`--status=blocked|pilot|ready\` for explicit Slice transitions and
|
|
649
|
+
\`--profile=cost-optimized|standard\` for an explicit shared Foundation decision.
|
|
650
|
+
Use \`--productionApprovalMode=promotion-pr|environment-reviewer\` for an explicit approval
|
|
651
|
+
transition. This changes committed policy only; a separate bootstrap report/apply reconciles
|
|
652
|
+
remote GitHub settings.
|
|
653
|
+
|
|
654
|
+
Policies created before beta.7 do not contain \`productionApproval\`. After applying the
|
|
655
|
+
workspace source upgrade, report the explicit amendment with
|
|
656
|
+
\`pnpm nx g @empire-builder-kit/nx:repository-policy --productionApprovalMode=promotion-pr\`,
|
|
657
|
+
review it, and rerun with \`--apply\` before health or bootstrap. This narrowly accepts a policy
|
|
658
|
+
whose sole defect is the missing member; every other defect still fails closed. Validation
|
|
659
|
+
provides no legacy default. Use \`promotion-pr\` for a private GitHub Team repository. Use
|
|
660
|
+
\`environment-reviewer\` only when the repository plan can enforce a separate environment
|
|
661
|
+
reviewer.
|
|
662
|
+
|
|
663
|
+
- \`blocked\` prohibits production create, update, and migration.
|
|
664
|
+
- \`pilot\` permits protected evidence gathering without claiming normal latency or maturity.
|
|
665
|
+
- \`ready\` records an adopter decision after its evidence and review; it is not inferred.
|
|
666
|
+
- removal and unlock remain explicit protected recovery paths so disabled policy cannot strand
|
|
667
|
+
resources.
|
|
668
|
+
|
|
669
|
+
Protected production deploy and refresh validate the complete Foundation/Slice set before AWS
|
|
670
|
+
access. The Foundation is always ordered first. The set is derived from committed policy and
|
|
671
|
+
cannot be narrowed with an ad hoc project argument.
|
|
672
|
+
|
|
673
|
+
\`standard\` keeps Aurora warm at 2–16 ACU. \`cost-optimized\` is a prerelease candidate at
|
|
674
|
+
0–4 ACU with a five-minute auto-pause and potential cold-resume latency. Both keep managed NAT,
|
|
675
|
+
30-day backups, deletion protection, retained data, and the protected GitHub/OIDC/AWS path.
|
|
676
|
+
Changing policy affects only later lifecycle requests; it never deploys or removes resources.
|
|
677
|
+
Changing profile requires a separate reviewed Foundation deployment and retained operational
|
|
678
|
+
evidence.
|
|
679
|
+
|
|
680
|
+
\`EBK_PRODUCTION_FOUNDATION_PROFILE\` is reserved managed executor input. The lifecycle
|
|
681
|
+
executor derives it from validated committed policy immediately before SST and removes any
|
|
682
|
+
ambient shell value. Do not set it as a local, repository, organization, or environment
|
|
683
|
+
variable. Incubation keeps the last configured profile, or \`standard\` for legacy/default
|
|
684
|
+
policy, solely so protected remove and unlock can still render existing production resources;
|
|
685
|
+
production deploy remains disabled.
|
|
686
|
+
`,
|
|
687
|
+
'docs/work-markers.md': `# Agent work markers
|
|
688
|
+
|
|
689
|
+
EBK work markers let deterministic generators identify places where an agent or human must
|
|
690
|
+
make a contextual product decision. They are searchable pointers to durable work, not a
|
|
691
|
+
replacement for the referenced issue or plan.
|
|
692
|
+
|
|
693
|
+
## Syntax and vocabulary
|
|
694
|
+
|
|
695
|
+
Use one full-line source comment with a self-contained description. Examples include
|
|
696
|
+
\`// EBK-TODO(GH:123): Replace the sample authorization decision with the approved policy.\`
|
|
697
|
+
and
|
|
698
|
+
\`# EBK-ASSUMPTION(PLAN:1/operator-proof): Confirm this timeout during the operator pilot.\`
|
|
699
|
+
|
|
700
|
+
The fixed vocabulary is:
|
|
701
|
+
|
|
702
|
+
- \`EBK-TODO\` — known implementation work;
|
|
703
|
+
- \`EBK-ASSUMPTION\` — a decision that needs validation;
|
|
704
|
+
- \`EBK-QUESTION\` — an unresolved design or product question;
|
|
705
|
+
- \`EBK-SECURITY\` — security work that blocks protected promotion; and
|
|
706
|
+
- \`EBK-BLOCKER\` — any other work that blocks protected promotion.
|
|
707
|
+
|
|
708
|
+
Vocabulary names used in comment prose are inert. The scanner treats only a kind immediately
|
|
709
|
+
followed by \`(\` or \`:\` as a marker attempt, so malformed marker-shaped comments still fail
|
|
710
|
+
validation.
|
|
711
|
+
|
|
712
|
+
Until Work Orchestration imports these markers, references must be an existing GitHub issue
|
|
713
|
+
(\`GH:<positive-number>\`) or an append-only roadmap key
|
|
714
|
+
(\`PLAN:<phase>/<slug>\`). The description must remain useful without opening the reference.
|
|
715
|
+
The scanner validates syntax and ownership now; it intentionally does not claim that a
|
|
716
|
+
reference exists. Phase 2 owns reference import, retained old-to-new ID mapping, and later
|
|
717
|
+
orphan checks. Do not invent temporary Work Orchestration numbers.
|
|
718
|
+
|
|
719
|
+
## Generator and agent workflow
|
|
720
|
+
|
|
721
|
+
Generators may emit a marker only into a \`seeded\` customization surface. They must never put
|
|
722
|
+
one in a \`managed\` file: managed output stays deterministic, and a needed change there is a
|
|
723
|
+
framework defect to report outside the generated file. Adopter-owned files may also contain
|
|
724
|
+
markers. Framework upgrades preserve seeded source and its unresolved markers.
|
|
725
|
+
|
|
726
|
+
Blueprint and repository update tooling may create a deterministic reconciliation marker in a
|
|
727
|
+
seeded or adopter-owned file when a preserved customization needs human or agent judgment. The
|
|
728
|
+
update must preserve the custom code and identify the changed contract in the description; a
|
|
729
|
+
successful update command alone does not resolve that marker. Reconcile the code, run the
|
|
730
|
+
relevant checks, retain evidence at the referenced work item, and only then remove the marker.
|
|
731
|
+
Automatic insertion requires an explicit publisher migration or other deterministic anchor;
|
|
732
|
+
the generic upgrade command does not guess where to edit preserved custom code.
|
|
733
|
+
|
|
734
|
+
At the start and handoff of agent work, run:
|
|
735
|
+
|
|
736
|
+
\`pnpm nx run ${name}:work-markers\`
|
|
737
|
+
|
|
738
|
+
The same ordinary validation runs through workspace health and CI. Complete the referenced
|
|
739
|
+
custom work and remove the marker; do not merely delete it while leaving the requirement
|
|
740
|
+
unresolved. Open \`TODO\`, \`ASSUMPTION\`, and \`QUESTION\` markers are reported but do not block
|
|
741
|
+
deployment by themselves. Open \`SECURITY\` and \`BLOCKER\` markers fail staging and production
|
|
742
|
+
deploy or refresh before AWS access. Protected remove and unlock remain usable for recovery.
|
|
743
|
+
There is no local or silent bypass in this prerelease contract; any future waiver mechanism
|
|
744
|
+
must be committed, reviewable, and auditable.
|
|
745
|
+
`,
|
|
746
|
+
'ebk.repository-policy.json': json((0, index_js_3.createIncubationRepositoryPolicy)()),
|
|
747
|
+
'nx.json': json({
|
|
748
|
+
$schema: './node_modules/nx/schemas/nx-schema.json',
|
|
749
|
+
defaultBase: 'develop',
|
|
750
|
+
namedInputs: {
|
|
751
|
+
default: ['{projectRoot}/**/*', 'sharedGlobals'],
|
|
752
|
+
production: [
|
|
753
|
+
'default',
|
|
754
|
+
'!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)',
|
|
755
|
+
'!{projectRoot}/tsconfig.spec.json',
|
|
756
|
+
],
|
|
757
|
+
sharedGlobals: [],
|
|
758
|
+
},
|
|
759
|
+
targetDefaults: {
|
|
760
|
+
build: {
|
|
761
|
+
cache: true,
|
|
762
|
+
dependsOn: ['^build'],
|
|
763
|
+
inputs: ['production', '^production'],
|
|
764
|
+
},
|
|
765
|
+
lint: {
|
|
766
|
+
cache: true,
|
|
767
|
+
inputs: ['default', '^production'],
|
|
768
|
+
},
|
|
769
|
+
test: {
|
|
770
|
+
cache: true,
|
|
771
|
+
dependsOn: ['^build'],
|
|
772
|
+
inputs: ['default', '^production'],
|
|
773
|
+
},
|
|
774
|
+
typecheck: {
|
|
775
|
+
cache: true,
|
|
776
|
+
dependsOn: ['^build'],
|
|
777
|
+
inputs: ['default', '^production'],
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
neverConnectToCloud: true,
|
|
781
|
+
}),
|
|
782
|
+
'package.json': json({
|
|
783
|
+
name,
|
|
784
|
+
version: '0.0.0',
|
|
785
|
+
private: true,
|
|
786
|
+
packageManager: `pnpm@${TOOL_VERSIONS.pnpm}`,
|
|
787
|
+
engines: {
|
|
788
|
+
node: '>=22.12.0',
|
|
789
|
+
pnpm: '>=10.0.0',
|
|
790
|
+
},
|
|
791
|
+
scripts: {
|
|
792
|
+
build: `nx run-many -t build --exclude=${name}`,
|
|
793
|
+
format: 'nx format:write',
|
|
794
|
+
'format:check': 'nx format:check',
|
|
795
|
+
graph: 'nx graph',
|
|
796
|
+
health: `nx run ${name}:workspace-health`,
|
|
797
|
+
lint: `nx run-many -t lint --exclude=${name}`,
|
|
798
|
+
test: `nx run-many -t test --exclude=${name}`,
|
|
799
|
+
typecheck: `nx run-many -t typecheck --exclude=${name}`,
|
|
800
|
+
verify: `nx run ${name}:workspace-health && nx run-many -t verify --exclude=${name}`,
|
|
801
|
+
dev: `nx run ${name}:dev`,
|
|
802
|
+
},
|
|
803
|
+
dependencies: {
|
|
804
|
+
'@empire-builder-kit/runtime': frameworkVersion,
|
|
805
|
+
},
|
|
806
|
+
devDependencies: {
|
|
807
|
+
'@eslint/js': TOOL_VERSIONS.eslintJs,
|
|
808
|
+
'@empire-builder-kit/nx': frameworkVersion,
|
|
809
|
+
'@types/node': TOOL_VERSIONS.nodeTypes,
|
|
810
|
+
eslint: TOOL_VERSIONS.eslint,
|
|
811
|
+
globals: TOOL_VERSIONS.globals,
|
|
812
|
+
nx: TOOL_VERSIONS.nx,
|
|
813
|
+
prettier: TOOL_VERSIONS.prettier,
|
|
814
|
+
sst: TOOL_VERSIONS.sst,
|
|
815
|
+
typescript: TOOL_VERSIONS.typescript,
|
|
816
|
+
'typescript-eslint': TOOL_VERSIONS.typescriptEslint,
|
|
817
|
+
},
|
|
818
|
+
}),
|
|
819
|
+
'project.json': json({
|
|
820
|
+
name,
|
|
821
|
+
$schema: './node_modules/nx/schemas/project-schema.json',
|
|
822
|
+
projectType: 'application',
|
|
823
|
+
tags: ['scope:workspace', 'layer:governance'],
|
|
824
|
+
targets: {
|
|
825
|
+
'deployment-plan': {
|
|
826
|
+
executor: '@empire-builder-kit/nx:deployment-plan',
|
|
827
|
+
options: {
|
|
828
|
+
action: 'deploy',
|
|
829
|
+
stage: 'staging',
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
dev: {
|
|
833
|
+
continuous: true,
|
|
834
|
+
executor: '@empire-builder-kit/nx:fleet',
|
|
835
|
+
options: {
|
|
836
|
+
stage: 'personal',
|
|
837
|
+
},
|
|
838
|
+
},
|
|
839
|
+
'dev-doctor': {
|
|
840
|
+
executor: '@empire-builder-kit/nx:dev-doctor',
|
|
841
|
+
options: {},
|
|
842
|
+
},
|
|
843
|
+
'dev-setup': {
|
|
844
|
+
executor: '@empire-builder-kit/nx:dev-setup',
|
|
845
|
+
options: {},
|
|
846
|
+
},
|
|
847
|
+
'github-bootstrap': {
|
|
848
|
+
executor: '@empire-builder-kit/nx:github-bootstrap',
|
|
849
|
+
options: {},
|
|
850
|
+
},
|
|
851
|
+
'install-git-hooks': {
|
|
852
|
+
executor: 'nx:run-commands',
|
|
853
|
+
options: { command: 'node tools/install-ebk-hooks.mjs' },
|
|
854
|
+
},
|
|
855
|
+
'work-markers': {
|
|
856
|
+
executor: '@empire-builder-kit/nx:work-markers',
|
|
857
|
+
options: {},
|
|
858
|
+
},
|
|
859
|
+
verify: {
|
|
860
|
+
executor: 'nx:run-commands',
|
|
861
|
+
options: { command: 'node tools/workspace-health.mjs' },
|
|
862
|
+
},
|
|
863
|
+
'workspace-health': {
|
|
864
|
+
executor: 'nx:run-commands',
|
|
865
|
+
options: { command: 'node tools/workspace-health.mjs' },
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
}),
|
|
869
|
+
'packages/foundation/README.md': `# Standard Foundation
|
|
870
|
+
|
|
871
|
+
This project owns the workspace-wide AWS platform for each canonical \`development\`,
|
|
872
|
+
\`staging\`, or \`production\` stage. It is a separate SST state boundary from product slices.
|
|
873
|
+
Never deploy it at a personal or preview stage; those consumers map to \`development\`.
|
|
874
|
+
|
|
875
|
+
It provides the v1 network, routing, container compute, Postgres, identity, events,
|
|
876
|
+
observability, configuration, and deployment capabilities. Product slices consume its
|
|
877
|
+
versioned contract; they do not import files from \`infra/\`.
|
|
878
|
+
|
|
879
|
+
Workspace creation does not deploy this project. Review \`docs/costs.md\`,
|
|
880
|
+
\`docs/security.md\`, and \`docs/runbooks.md\` before the first deployment.
|
|
881
|
+
|
|
882
|
+
Set \`EBK_ROOT_DOMAIN\` before deploying when Route 53 in the target account owns a public
|
|
883
|
+
domain. Without it, the shared Router uses its functional provider URL.
|
|
884
|
+
`,
|
|
885
|
+
'packages/foundation/contracts/foundation.json': json({
|
|
886
|
+
contractVersion: '1.0.0',
|
|
887
|
+
capabilities: [
|
|
888
|
+
'network',
|
|
889
|
+
'routing',
|
|
890
|
+
'container-compute',
|
|
891
|
+
'postgres',
|
|
892
|
+
'identity',
|
|
893
|
+
'events',
|
|
894
|
+
'observability',
|
|
895
|
+
'configuration',
|
|
896
|
+
'deployment',
|
|
897
|
+
],
|
|
898
|
+
}),
|
|
899
|
+
'packages/foundation/contracts/costs.json': json({
|
|
900
|
+
schemaVersion: 2,
|
|
901
|
+
aurora: {
|
|
902
|
+
engine: index_js_3.FOUNDATION_AURORA_ENGINE_FAMILY,
|
|
903
|
+
engineVersion: index_js_3.FOUNDATION_AURORA_ENGINE_VERSION,
|
|
904
|
+
},
|
|
905
|
+
productionProfiles: index_js_3.PRODUCTION_FOUNDATION_PROFILE_CONTRACTS,
|
|
906
|
+
productionProfileSource: 'ebk.repository-policy.json',
|
|
907
|
+
stages: {
|
|
908
|
+
development: {
|
|
909
|
+
aurora: 'scale-to-zero',
|
|
910
|
+
dataRetention: 'retained',
|
|
911
|
+
nat: 'ec2',
|
|
912
|
+
},
|
|
913
|
+
staging: {
|
|
914
|
+
aurora: 'scale-to-zero',
|
|
915
|
+
dataRetention: 'retained',
|
|
916
|
+
nat: 'ec2',
|
|
917
|
+
},
|
|
918
|
+
production: {
|
|
919
|
+
aurora: 'repository-policy',
|
|
920
|
+
dataRetention: 'retained-and-protected',
|
|
921
|
+
nat: 'managed',
|
|
922
|
+
},
|
|
923
|
+
},
|
|
924
|
+
usagePriced: [
|
|
925
|
+
'cloudwatch',
|
|
926
|
+
'cognito',
|
|
927
|
+
'ecs-workloads',
|
|
928
|
+
'eventbridge',
|
|
929
|
+
'sns',
|
|
930
|
+
'ssm',
|
|
931
|
+
],
|
|
932
|
+
}),
|
|
933
|
+
'packages/foundation/docs/costs.md': `# Foundation cost contract
|
|
934
|
+
|
|
935
|
+
The VPC NAT path and Aurora capacity are the main fixed-cost decisions.
|
|
936
|
+
The machine-readable stage inventory is \`../contracts/costs.json\`; keep this
|
|
937
|
+
seeded decision record aligned with organization-specific budgets and alerts.
|
|
938
|
+
|
|
939
|
+
- Development uses EC2 NAT, a development bastion, and Aurora scale-to-zero.
|
|
940
|
+
- Staging uses EC2 NAT and Aurora scale-to-zero by default.
|
|
941
|
+
- Production always uses managed NAT and retains protected data. Its explicit Repository
|
|
942
|
+
Operating Policy profile selects either the standard always-warm Aurora floor or the
|
|
943
|
+
prerelease cost-optimized auto-pause candidate.
|
|
944
|
+
- ECS, EventBridge, Cognito, SNS, SSM, and CloudWatch are primarily usage-priced; logs,
|
|
945
|
+
metrics, alarms, and retained data still require active cost review.
|
|
946
|
+
|
|
947
|
+
Run \`foundation:remove\` only for disposable stages. Production is retained and protected by
|
|
948
|
+
the SST configuration.
|
|
949
|
+
`,
|
|
950
|
+
'packages/foundation/docs/runbooks.md': `# Foundation runbooks
|
|
951
|
+
|
|
952
|
+
## Deployment
|
|
953
|
+
|
|
954
|
+
Use Nx for every supported lifecycle action:
|
|
955
|
+
|
|
956
|
+
- \`pnpm nx run foundation:deploy --stage=development\`
|
|
957
|
+
- \`pnpm nx run foundation:refresh --stage=development\`
|
|
958
|
+
- \`pnpm nx run foundation:unlock --stage=development\`
|
|
959
|
+
- \`pnpm nx run foundation:remove --stage=development\`
|
|
960
|
+
|
|
961
|
+
Do not invoke SST lifecycle commands directly. Production deployment belongs in the trusted
|
|
962
|
+
CI path.
|
|
963
|
+
|
|
964
|
+
Foundation stages are canonical. Run a Product Slice with \`--stage=<personal-name>\` to map
|
|
965
|
+
that consumer to the development Foundation. The slice preflight reports both stages; it
|
|
966
|
+
does not deploy a personal Foundation. Full mapped personal-stage deploys fail closed.
|
|
967
|
+
|
|
968
|
+
## Database restore
|
|
969
|
+
|
|
970
|
+
Production Aurora retains 30 days of automated backups; non-production retains 7 days.
|
|
971
|
+
Restore into a new cluster, validate application migrations and data integrity, then cut over
|
|
972
|
+
consumers deliberately. Never expose the Foundation master credential to a product slice.
|
|
973
|
+
`,
|
|
974
|
+
'packages/foundation/docs/security.md': `# Foundation security contract
|
|
975
|
+
|
|
976
|
+
- Production resources are retained and protected.
|
|
977
|
+
- The shared Aurora master credential is Foundation-internal.
|
|
978
|
+
- Product slices receive scoped logical-database credentials through the provisioning contract.
|
|
979
|
+
- Cognito owns authentication; product slices own authorization policy and app clients.
|
|
980
|
+
- Lambda and server-side web workloads use resource linking. Containers use the documented
|
|
981
|
+
stage-scoped SSM/secret path and least-privilege IAM.
|
|
982
|
+
- Public domains are configured explicitly through \`EBK_ROOT_DOMAIN\`; generated source never
|
|
983
|
+
contains credentials or customer domain assumptions.
|
|
984
|
+
`,
|
|
985
|
+
'packages/foundation/ebk-globals.d.ts': `// SST and Pulumi inject these globals while evaluating sst.config.ts.
|
|
986
|
+
declare const sst: any;
|
|
987
|
+
declare const aws: any;
|
|
988
|
+
declare const $config: any;
|
|
989
|
+
declare const $app: {
|
|
990
|
+
name: string;
|
|
991
|
+
stage: string;
|
|
992
|
+
removal: 'remove' | 'retain' | 'retain-all';
|
|
993
|
+
protect: boolean;
|
|
994
|
+
};
|
|
995
|
+
`,
|
|
996
|
+
'packages/foundation/infra/foundation.ts': `import {
|
|
997
|
+
FOUNDATION_SSM_SCHEMA_VERSION,
|
|
998
|
+
STANDARD_FOUNDATION_CAPABILITIES,
|
|
999
|
+
foundationSsmPath,
|
|
1000
|
+
type FoundationSsmKey,
|
|
1001
|
+
} from '@empire-builder-kit/nx/foundation';
|
|
1002
|
+
import {
|
|
1003
|
+
FOUNDATION_AURORA_ENGINE,
|
|
1004
|
+
FOUNDATION_AURORA_ENGINE_VERSION,
|
|
1005
|
+
productionFoundationProfileContract,
|
|
1006
|
+
} from '@empire-builder-kit/nx/repository-policy';
|
|
1007
|
+
|
|
1008
|
+
function publishFoundation(values: Record<FoundationSsmKey, any>) {
|
|
1009
|
+
return Object.fromEntries(
|
|
1010
|
+
Object.entries(values).map(([key, value]) => [
|
|
1011
|
+
key,
|
|
1012
|
+
new aws.ssm.Parameter('FoundationSsm_' + key, {
|
|
1013
|
+
name: foundationSsmPath($app.stage, key as FoundationSsmKey),
|
|
1014
|
+
type: 'String',
|
|
1015
|
+
value,
|
|
1016
|
+
}),
|
|
1017
|
+
]),
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
export function foundation() {
|
|
1022
|
+
const isProduction = $app.stage === 'production';
|
|
1023
|
+
const isStaging = $app.stage === 'staging';
|
|
1024
|
+
const rootDomain = process.env.EBK_ROOT_DOMAIN?.trim();
|
|
1025
|
+
const productionConfiguration = isProduction
|
|
1026
|
+
? productionFoundationProfileContract(
|
|
1027
|
+
process.env.EBK_PRODUCTION_FOUNDATION_PROFILE,
|
|
1028
|
+
)
|
|
1029
|
+
: undefined;
|
|
1030
|
+
|
|
1031
|
+
const vpc = new sst.aws.Vpc('FoundationVpc', {
|
|
1032
|
+
az: 2,
|
|
1033
|
+
bastion: $app.stage === 'development',
|
|
1034
|
+
nat: productionConfiguration?.contract.nat ?? 'ec2',
|
|
1035
|
+
});
|
|
1036
|
+
const cluster = new sst.aws.Cluster('FoundationCluster', { vpc });
|
|
1037
|
+
const router = new sst.aws.Router('FoundationRouter', {
|
|
1038
|
+
...(rootDomain
|
|
1039
|
+
? {
|
|
1040
|
+
domain: {
|
|
1041
|
+
name: rootDomain,
|
|
1042
|
+
aliases: ['*.' + rootDomain],
|
|
1043
|
+
dns: sst.aws.dns(),
|
|
1044
|
+
},
|
|
1045
|
+
}
|
|
1046
|
+
: {}),
|
|
1047
|
+
});
|
|
1048
|
+
const database = new sst.aws.Aurora('FoundationDatabase', {
|
|
1049
|
+
dataApi: true,
|
|
1050
|
+
engine: FOUNDATION_AURORA_ENGINE,
|
|
1051
|
+
version: FOUNDATION_AURORA_ENGINE_VERSION,
|
|
1052
|
+
scaling:
|
|
1053
|
+
productionConfiguration?.contract.aurora ??
|
|
1054
|
+
(isStaging
|
|
1055
|
+
? { min: '0 ACU', max: '4 ACU', pauseAfter: '5 minutes' }
|
|
1056
|
+
: { min: '0 ACU', max: '2 ACU', pauseAfter: '5 minutes' }),
|
|
1057
|
+
transform: {
|
|
1058
|
+
cluster: {
|
|
1059
|
+
backupRetentionPeriod:
|
|
1060
|
+
productionConfiguration?.contract.backupRetentionDays ?? 7,
|
|
1061
|
+
deletionProtection:
|
|
1062
|
+
productionConfiguration?.contract.deletionProtection ?? false,
|
|
1063
|
+
},
|
|
1064
|
+
},
|
|
1065
|
+
vpc,
|
|
1066
|
+
});
|
|
1067
|
+
const userPool = new sst.aws.CognitoUserPool('FoundationIdentity', {
|
|
1068
|
+
usernames: ['email'],
|
|
1069
|
+
});
|
|
1070
|
+
const eventBus = new sst.aws.Bus('FoundationEventBus');
|
|
1071
|
+
const alertTopic = new sst.aws.SnsTopic('FoundationAlerts');
|
|
1072
|
+
const alertTopicPolicy = new aws.sns.TopicPolicy(
|
|
1073
|
+
'FoundationAlertTopicPolicy',
|
|
1074
|
+
{
|
|
1075
|
+
arn: alertTopic.arn,
|
|
1076
|
+
policy: aws.iam.getPolicyDocumentOutput({
|
|
1077
|
+
statements: [
|
|
1078
|
+
{
|
|
1079
|
+
actions: ['sns:Publish'],
|
|
1080
|
+
principals: [
|
|
1081
|
+
{ identifiers: ['events.rds.amazonaws.com'], type: 'Service' },
|
|
1082
|
+
],
|
|
1083
|
+
resources: [alertTopic.arn],
|
|
1084
|
+
},
|
|
1085
|
+
],
|
|
1086
|
+
}).json,
|
|
1087
|
+
},
|
|
1088
|
+
);
|
|
1089
|
+
const databaseInstanceId = database.nodes.instance.identifier;
|
|
1090
|
+
const databaseEvents = isProduction
|
|
1091
|
+
? new aws.rds.EventSubscription(
|
|
1092
|
+
'FoundationDatabaseEvents',
|
|
1093
|
+
{
|
|
1094
|
+
eventCategories: ['failure', 'notification', 'serverless'],
|
|
1095
|
+
snsTopic: alertTopic.arn,
|
|
1096
|
+
sourceIds: [databaseInstanceId],
|
|
1097
|
+
sourceType: 'db-instance',
|
|
1098
|
+
},
|
|
1099
|
+
{ dependsOn: [alertTopicPolicy] },
|
|
1100
|
+
)
|
|
1101
|
+
: undefined;
|
|
1102
|
+
const dashboard = new aws.cloudwatch.Dashboard('FoundationDashboard', {
|
|
1103
|
+
dashboardName: 'ebk-' + $app.stage + '-foundation',
|
|
1104
|
+
dashboardBody: databaseInstanceId.apply((identifier: string) => JSON.stringify({
|
|
1105
|
+
widgets: [
|
|
1106
|
+
{
|
|
1107
|
+
type: 'text',
|
|
1108
|
+
x: 0,
|
|
1109
|
+
y: 0,
|
|
1110
|
+
width: 24,
|
|
1111
|
+
height: 3,
|
|
1112
|
+
properties: {
|
|
1113
|
+
markdown:
|
|
1114
|
+
'# Empire Builder Kit Foundation\\nStage: ' +
|
|
1115
|
+
$app.stage +
|
|
1116
|
+
'\\nProduction profile: ' +
|
|
1117
|
+
(productionConfiguration?.profile ?? 'not-applicable'),
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
type: 'metric',
|
|
1122
|
+
x: 0,
|
|
1123
|
+
y: 3,
|
|
1124
|
+
width: 12,
|
|
1125
|
+
height: 6,
|
|
1126
|
+
properties: {
|
|
1127
|
+
title: 'Aurora capacity and pause state',
|
|
1128
|
+
region: process.env.AWS_REGION ?? 'us-east-1',
|
|
1129
|
+
period: 60,
|
|
1130
|
+
stat: 'Minimum',
|
|
1131
|
+
metrics: [
|
|
1132
|
+
['AWS/RDS', 'ServerlessDatabaseCapacity', 'DBInstanceIdentifier', identifier],
|
|
1133
|
+
['.', 'ACUUtilization', '.', '.'],
|
|
1134
|
+
],
|
|
1135
|
+
},
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
type: 'metric',
|
|
1139
|
+
x: 12,
|
|
1140
|
+
y: 3,
|
|
1141
|
+
width: 12,
|
|
1142
|
+
height: 6,
|
|
1143
|
+
properties: {
|
|
1144
|
+
title: 'Aurora connections',
|
|
1145
|
+
region: process.env.AWS_REGION ?? 'us-east-1',
|
|
1146
|
+
period: 60,
|
|
1147
|
+
stat: 'Maximum',
|
|
1148
|
+
metrics: [
|
|
1149
|
+
['AWS/RDS', 'DatabaseConnections', 'DBInstanceIdentifier', identifier],
|
|
1150
|
+
],
|
|
1151
|
+
},
|
|
1152
|
+
},
|
|
1153
|
+
],
|
|
1154
|
+
})),
|
|
1155
|
+
});
|
|
1156
|
+
const databaseCapacityAlarm = new aws.cloudwatch.MetricAlarm(
|
|
1157
|
+
'FoundationDatabaseCapacityAlarm',
|
|
1158
|
+
{
|
|
1159
|
+
alarmActions: [alertTopic.arn],
|
|
1160
|
+
alarmDescription:
|
|
1161
|
+
'Aurora Serverless capacity utilization stayed above 90 percent.',
|
|
1162
|
+
comparisonOperator: 'GreaterThanThreshold',
|
|
1163
|
+
dimensions: { DBInstanceIdentifier: databaseInstanceId },
|
|
1164
|
+
evaluationPeriods: 5,
|
|
1165
|
+
metricName: 'ACUUtilization',
|
|
1166
|
+
namespace: 'AWS/RDS',
|
|
1167
|
+
period: 60,
|
|
1168
|
+
statistic: 'Average',
|
|
1169
|
+
threshold: 90,
|
|
1170
|
+
treatMissingData: 'notBreaching',
|
|
1171
|
+
},
|
|
1172
|
+
);
|
|
1173
|
+
|
|
1174
|
+
if (new Set(['development', 'staging', 'production']).has($app.stage)) {
|
|
1175
|
+
new aws.cloudwatch.EventArchive('FoundationEventArchive', {
|
|
1176
|
+
eventSourceArn: eventBus.arn,
|
|
1177
|
+
retentionDays: 30,
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
publishFoundation({
|
|
1182
|
+
alertTopicArn: alertTopic.arn,
|
|
1183
|
+
capabilities: STANDARD_FOUNDATION_CAPABILITIES.join(','),
|
|
1184
|
+
clusterId: cluster.id,
|
|
1185
|
+
contractVersion: '1.0.0',
|
|
1186
|
+
databaseClusterArn: database.clusterArn,
|
|
1187
|
+
databaseClusterId: database.id,
|
|
1188
|
+
databaseMasterSecretArn: database.secretArn,
|
|
1189
|
+
eventBusArn: eventBus.arn,
|
|
1190
|
+
eventBusName: eventBus.name,
|
|
1191
|
+
hasPublicDomain: rootDomain ? 'true' : 'false',
|
|
1192
|
+
identityUserPoolArn: userPool.arn,
|
|
1193
|
+
identityUserPoolId: userPool.id,
|
|
1194
|
+
rootDomain: rootDomain ?? 'none',
|
|
1195
|
+
routerDistributionId: router.distributionID,
|
|
1196
|
+
routerUrl: router.url,
|
|
1197
|
+
schemaVersion: String(FOUNDATION_SSM_SCHEMA_VERSION),
|
|
1198
|
+
vpcId: vpc.id,
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
return {
|
|
1202
|
+
alertTopicArn: alertTopic.arn,
|
|
1203
|
+
capabilities: STANDARD_FOUNDATION_CAPABILITIES,
|
|
1204
|
+
clusterId: cluster.id,
|
|
1205
|
+
contractVersion: '1.0.0',
|
|
1206
|
+
databaseCapacityAlarmName: databaseCapacityAlarm.name,
|
|
1207
|
+
dashboardName: dashboard.dashboardName,
|
|
1208
|
+
databaseClusterId: database.id,
|
|
1209
|
+
databaseEventSubscriptionId: databaseEvents?.id,
|
|
1210
|
+
eventBusName: eventBus.name,
|
|
1211
|
+
identityUserPoolId: userPool.id,
|
|
1212
|
+
name: 'foundation',
|
|
1213
|
+
productionFoundationProfile: productionConfiguration?.profile,
|
|
1214
|
+
rootDomain,
|
|
1215
|
+
routerUrl: router.url,
|
|
1216
|
+
vpcId: vpc.id,
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
`,
|
|
1220
|
+
'packages/foundation/infra/index.ts': `export { foundation as app } from './foundation.js';
|
|
1221
|
+
`,
|
|
1222
|
+
'packages/foundation/package.json': json({
|
|
1223
|
+
name: `@${name}/foundation`,
|
|
1224
|
+
version: '0.0.0',
|
|
1225
|
+
private: true,
|
|
1226
|
+
}),
|
|
1227
|
+
'packages/foundation/project.json': json({
|
|
1228
|
+
name: 'foundation',
|
|
1229
|
+
$schema: '../../node_modules/nx/schemas/project-schema.json',
|
|
1230
|
+
projectType: 'application',
|
|
1231
|
+
sourceRoot: 'packages/foundation/infra',
|
|
1232
|
+
tags: [
|
|
1233
|
+
'scope:foundation',
|
|
1234
|
+
'type:sst',
|
|
1235
|
+
'layer:platform',
|
|
1236
|
+
'visibility:internal',
|
|
1237
|
+
],
|
|
1238
|
+
targets: {
|
|
1239
|
+
deploy: {
|
|
1240
|
+
executor: '@empire-builder-kit/nx:deploy',
|
|
1241
|
+
dependsOn: [
|
|
1242
|
+
'verify',
|
|
1243
|
+
{ target: 'profile-preflight', params: 'forward' },
|
|
1244
|
+
],
|
|
1245
|
+
options: {
|
|
1246
|
+
cwd: 'packages/foundation',
|
|
1247
|
+
stage: 'development',
|
|
1248
|
+
stageMode: 'foundation',
|
|
1249
|
+
},
|
|
1250
|
+
},
|
|
1251
|
+
dev: {
|
|
1252
|
+
executor: '@empire-builder-kit/nx:dev',
|
|
1253
|
+
dependsOn: ['verify'],
|
|
1254
|
+
options: {
|
|
1255
|
+
cwd: 'packages/foundation',
|
|
1256
|
+
stage: 'development',
|
|
1257
|
+
stageMode: 'foundation',
|
|
1258
|
+
},
|
|
1259
|
+
},
|
|
1260
|
+
lint: {
|
|
1261
|
+
executor: 'nx:run-commands',
|
|
1262
|
+
options: {
|
|
1263
|
+
command: 'eslint .',
|
|
1264
|
+
cwd: 'packages/foundation',
|
|
1265
|
+
},
|
|
1266
|
+
},
|
|
1267
|
+
refresh: {
|
|
1268
|
+
executor: '@empire-builder-kit/nx:refresh',
|
|
1269
|
+
dependsOn: [{ target: 'profile-preflight', params: 'forward' }],
|
|
1270
|
+
options: {
|
|
1271
|
+
cwd: 'packages/foundation',
|
|
1272
|
+
stage: 'development',
|
|
1273
|
+
stageMode: 'foundation',
|
|
1274
|
+
},
|
|
1275
|
+
},
|
|
1276
|
+
remove: {
|
|
1277
|
+
executor: '@empire-builder-kit/nx:remove',
|
|
1278
|
+
dependsOn: [{ target: 'remove-preflight', params: 'forward' }],
|
|
1279
|
+
options: {
|
|
1280
|
+
cwd: 'packages/foundation',
|
|
1281
|
+
stage: 'development',
|
|
1282
|
+
stageMode: 'foundation',
|
|
1283
|
+
},
|
|
1284
|
+
},
|
|
1285
|
+
'remove-preflight': {
|
|
1286
|
+
executor: '@empire-builder-kit/nx:foundation-remove-preflight',
|
|
1287
|
+
options: { stage: 'development' },
|
|
1288
|
+
},
|
|
1289
|
+
'profile-preflight': {
|
|
1290
|
+
executor: '@empire-builder-kit/nx:foundation-profile-preflight',
|
|
1291
|
+
options: { stage: 'development' },
|
|
1292
|
+
},
|
|
1293
|
+
typecheck: {
|
|
1294
|
+
executor: 'nx:run-commands',
|
|
1295
|
+
options: {
|
|
1296
|
+
command: 'tsc --noEmit -p tsconfig.json',
|
|
1297
|
+
cwd: 'packages/foundation',
|
|
1298
|
+
},
|
|
1299
|
+
},
|
|
1300
|
+
unlock: {
|
|
1301
|
+
executor: '@empire-builder-kit/nx:unlock',
|
|
1302
|
+
options: {
|
|
1303
|
+
cwd: 'packages/foundation',
|
|
1304
|
+
stage: 'development',
|
|
1305
|
+
stageMode: 'foundation',
|
|
1306
|
+
},
|
|
1307
|
+
},
|
|
1308
|
+
verify: {
|
|
1309
|
+
executor: 'nx:run-commands',
|
|
1310
|
+
dependsOn: ['lint', 'typecheck'],
|
|
1311
|
+
options: {
|
|
1312
|
+
command: 'node scripts/verify-foundation.mjs',
|
|
1313
|
+
cwd: 'packages/foundation',
|
|
1314
|
+
},
|
|
1315
|
+
},
|
|
1316
|
+
},
|
|
1317
|
+
}),
|
|
1318
|
+
'packages/foundation/scripts/verify-foundation.mjs': `import { readFileSync } from 'node:fs';
|
|
1319
|
+
|
|
1320
|
+
const descriptor = JSON.parse(
|
|
1321
|
+
readFileSync(new URL('../contracts/foundation.json', import.meta.url), 'utf8'),
|
|
1322
|
+
);
|
|
1323
|
+
const costs = JSON.parse(
|
|
1324
|
+
readFileSync(new URL('../contracts/costs.json', import.meta.url), 'utf8'),
|
|
1325
|
+
);
|
|
1326
|
+
const expectedCapabilities = [
|
|
1327
|
+
'network',
|
|
1328
|
+
'routing',
|
|
1329
|
+
'container-compute',
|
|
1330
|
+
'postgres',
|
|
1331
|
+
'identity',
|
|
1332
|
+
'events',
|
|
1333
|
+
'observability',
|
|
1334
|
+
'configuration',
|
|
1335
|
+
'deployment',
|
|
1336
|
+
];
|
|
1337
|
+
|
|
1338
|
+
if (descriptor.contractVersion !== '1.0.0') {
|
|
1339
|
+
throw new Error('Foundation contractVersion must be 1.0.0.');
|
|
1340
|
+
}
|
|
1341
|
+
if (JSON.stringify(descriptor.capabilities) !== JSON.stringify(expectedCapabilities)) {
|
|
1342
|
+
throw new Error('Foundation capability descriptor does not match the v1 standard.');
|
|
1343
|
+
}
|
|
1344
|
+
if (
|
|
1345
|
+
costs.schemaVersion !== 2 ||
|
|
1346
|
+
costs.aurora?.engineVersion !== '${index_js_3.FOUNDATION_AURORA_ENGINE_VERSION}' ||
|
|
1347
|
+
costs.productionProfileSource !== 'ebk.repository-policy.json' ||
|
|
1348
|
+
costs.productionProfiles?.standard?.aurora?.min !== '2 ACU' ||
|
|
1349
|
+
costs.productionProfiles?.['cost-optimized']?.aurora?.min !== '0 ACU' ||
|
|
1350
|
+
!costs.stages?.development ||
|
|
1351
|
+
!costs.stages?.staging ||
|
|
1352
|
+
costs.stages?.production?.dataRetention !== 'retained-and-protected'
|
|
1353
|
+
) {
|
|
1354
|
+
throw new Error('Foundation cost manifest does not match the v1 stage contract.');
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
process.stdout.write('Foundation source contract verified.\\n');
|
|
1358
|
+
`,
|
|
1359
|
+
'packages/foundation/sst-env.d.ts': `// Bootstrap placeholder. SST overwrites this file after install, dev, or deploy.
|
|
1360
|
+
export {};
|
|
1361
|
+
`,
|
|
1362
|
+
'packages/foundation/sst.config.ts': `// SST/Pulumi ambient globals are declared in ebk-globals.d.ts.
|
|
1363
|
+
export default $config({
|
|
1364
|
+
app(input: { stage?: string } | undefined) {
|
|
1365
|
+
const stage = input?.stage ?? 'development';
|
|
1366
|
+
if (!['development', 'staging', 'production'].includes(stage)) {
|
|
1367
|
+
throw new Error(
|
|
1368
|
+
'Foundation stage ' + stage + ' is not canonical. Use development, staging, or production.',
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
const production = stage === 'production';
|
|
1372
|
+
return {
|
|
1373
|
+
name: 'foundation',
|
|
1374
|
+
removal: production ? 'retain' : 'remove',
|
|
1375
|
+
protect: production,
|
|
1376
|
+
home: 'aws',
|
|
1377
|
+
providers: {
|
|
1378
|
+
aws: {
|
|
1379
|
+
region: process.env.AWS_REGION ?? 'us-east-1',
|
|
1380
|
+
},
|
|
1381
|
+
},
|
|
1382
|
+
};
|
|
1383
|
+
},
|
|
1384
|
+
async run() {
|
|
1385
|
+
const { app } = await import('./infra/index.js');
|
|
1386
|
+
return app();
|
|
1387
|
+
},
|
|
1388
|
+
});
|
|
1389
|
+
`,
|
|
1390
|
+
'packages/foundation/tsconfig.json': json({
|
|
1391
|
+
extends: '../../tsconfig.base.json',
|
|
1392
|
+
compilerOptions: { types: ['node'] },
|
|
1393
|
+
include: [
|
|
1394
|
+
'infra/**/*.ts',
|
|
1395
|
+
'sst.config.ts',
|
|
1396
|
+
'sst-env.d.ts',
|
|
1397
|
+
'ebk-globals.d.ts',
|
|
1398
|
+
],
|
|
1399
|
+
exclude: ['.sst', 'dist', 'node_modules'],
|
|
1400
|
+
}),
|
|
1401
|
+
'pnpm-workspace.yaml': `packages:
|
|
1402
|
+
- 'packages/*'
|
|
1403
|
+
- 'packages/*/apps/*'
|
|
1404
|
+
- 'packages/*/jobs/*'
|
|
1405
|
+
- 'packages/*/libs/*'
|
|
1406
|
+
- 'packages/*/services/*'
|
|
1407
|
+
`,
|
|
1408
|
+
'tsconfig.base.json': json({
|
|
1409
|
+
compilerOptions: {
|
|
1410
|
+
composite: true,
|
|
1411
|
+
declarationMap: true,
|
|
1412
|
+
forceConsistentCasingInFileNames: true,
|
|
1413
|
+
isolatedModules: true,
|
|
1414
|
+
lib: ['es2022'],
|
|
1415
|
+
module: 'nodenext',
|
|
1416
|
+
moduleResolution: 'nodenext',
|
|
1417
|
+
noEmitOnError: true,
|
|
1418
|
+
noFallthroughCasesInSwitch: true,
|
|
1419
|
+
noImplicitOverride: true,
|
|
1420
|
+
noImplicitReturns: true,
|
|
1421
|
+
noUnusedLocals: true,
|
|
1422
|
+
skipLibCheck: true,
|
|
1423
|
+
strict: true,
|
|
1424
|
+
target: 'es2022',
|
|
1425
|
+
},
|
|
1426
|
+
}),
|
|
1427
|
+
'tsconfig.json': json({
|
|
1428
|
+
extends: './tsconfig.base.json',
|
|
1429
|
+
files: [],
|
|
1430
|
+
references: [],
|
|
1431
|
+
}),
|
|
1432
|
+
'tools/ebk-git-policy.mjs': `import { evaluateGitPolicy } from '@empire-builder-kit/nx/git-policy';
|
|
1433
|
+
import { spawnSync } from 'node:child_process';
|
|
1434
|
+
|
|
1435
|
+
const git = (...args) => {
|
|
1436
|
+
const result = spawnSync('git', args, { encoding: 'utf8', shell: false });
|
|
1437
|
+
if (result.status !== 0) {
|
|
1438
|
+
throw new Error('git ' + args.join(' ') + ' failed: ' + (result.stderr || result.stdout).trim());
|
|
1439
|
+
}
|
|
1440
|
+
return result.stdout.trim();
|
|
1441
|
+
};
|
|
1442
|
+
const isAncestor = (ancestor, descendant) =>
|
|
1443
|
+
spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
|
|
1444
|
+
encoding: 'utf8',
|
|
1445
|
+
shell: false,
|
|
1446
|
+
}).status === 0;
|
|
1447
|
+
|
|
1448
|
+
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
1449
|
+
if (!['pull_request', 'push', 'workflow_dispatch'].includes(eventName)) {
|
|
1450
|
+
throw new Error('Unsupported or missing GITHUB_EVENT_NAME; Git policy fails closed.');
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
const request = { eventName, ref: process.env.GITHUB_REF };
|
|
1454
|
+
if (eventName === 'pull_request' || eventName === 'push') {
|
|
1455
|
+
git(
|
|
1456
|
+
'fetch',
|
|
1457
|
+
'--no-tags',
|
|
1458
|
+
'origin',
|
|
1459
|
+
'main:refs/remotes/origin/main',
|
|
1460
|
+
'develop:refs/remotes/origin/develop',
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
if (eventName === 'pull_request') {
|
|
1464
|
+
const baseRef = process.env.GITHUB_BASE_REF;
|
|
1465
|
+
const headRef = process.env.GITHUB_HEAD_REF;
|
|
1466
|
+
if (!baseRef || !headRef) {
|
|
1467
|
+
throw new Error('Pull-request branch metadata is missing.');
|
|
1468
|
+
}
|
|
1469
|
+
Object.assign(request, {
|
|
1470
|
+
baseIsAncestorOfHead: isAncestor('refs/remotes/origin/' + baseRef, 'HEAD'),
|
|
1471
|
+
baseRef,
|
|
1472
|
+
headRef,
|
|
1473
|
+
mainIsAncestorOfDevelop: isAncestor(
|
|
1474
|
+
'refs/remotes/origin/main',
|
|
1475
|
+
'refs/remotes/origin/develop',
|
|
1476
|
+
),
|
|
1477
|
+
});
|
|
1478
|
+
} else if (eventName === 'push') {
|
|
1479
|
+
Object.assign(request, {
|
|
1480
|
+
baseIsAncestorOfHead: isAncestor('refs/remotes/origin/develop', 'HEAD'),
|
|
1481
|
+
mainIsAncestorOfDevelop: isAncestor(
|
|
1482
|
+
'refs/remotes/origin/main',
|
|
1483
|
+
'refs/remotes/origin/develop',
|
|
1484
|
+
),
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
const decision = evaluateGitPolicy(request);
|
|
1489
|
+
process.stdout.write(JSON.stringify(decision, null, 2) + '\\n');
|
|
1490
|
+
if (!decision.allowed) process.exitCode = 1;
|
|
1491
|
+
`,
|
|
1492
|
+
'tools/workspace-health.mjs': `import { validateRepositoryOperatingPolicy } from '@empire-builder-kit/nx/repository-policy';
|
|
1493
|
+
import { assertWorkMarkerReport, formatWorkMarkerReport, scanWorkMarkers } from '@empire-builder-kit/nx/work-markers';
|
|
1494
|
+
import { createHash } from 'node:crypto';
|
|
1495
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
1496
|
+
import { join } from 'node:path';
|
|
1497
|
+
|
|
1498
|
+
const parseJson = (contents, label) => {
|
|
1499
|
+
try {
|
|
1500
|
+
return JSON.parse(contents);
|
|
1501
|
+
} catch (error) {
|
|
1502
|
+
throw new Error(label + ' is not valid JSON: ' + (error instanceof Error ? error.message : String(error)));
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
const readJson = (path) => parseJson(readFileSync(path, 'utf8'), path);
|
|
1506
|
+
const sha256 = (contents) => createHash('sha256').update(contents, 'utf8').digest('hex');
|
|
1507
|
+
const stableValue = (value) => {
|
|
1508
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
1509
|
+
if (!value || typeof value !== 'object') return value;
|
|
1510
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
|
|
1511
|
+
};
|
|
1512
|
+
const pointerValue = (document, pointer) => {
|
|
1513
|
+
let current = document;
|
|
1514
|
+
for (const segment of pointer.slice(1).split('/').map((part) => part.replaceAll('~1', '/').replaceAll('~0', '~'))) {
|
|
1515
|
+
if (!current || typeof current !== 'object' || !Object.hasOwn(current, segment)) {
|
|
1516
|
+
return { exists: false };
|
|
1517
|
+
}
|
|
1518
|
+
current = current[segment];
|
|
1519
|
+
}
|
|
1520
|
+
return { exists: true, value: current };
|
|
1521
|
+
};
|
|
1522
|
+
const ownedSha256 = (contents, entry) => {
|
|
1523
|
+
if (!entry.managedJsonPointers) return sha256(contents);
|
|
1524
|
+
const document = parseJson(contents, 'Managed JSON file ' + entry.path);
|
|
1525
|
+
const projection = [...entry.managedJsonPointers].sort().map((pointer) => {
|
|
1526
|
+
const resolved = pointerValue(document, pointer);
|
|
1527
|
+
return { exists: resolved.exists, pointer, ...(resolved.exists ? { value: resolved.value } : {}) };
|
|
1528
|
+
});
|
|
1529
|
+
return sha256(JSON.stringify(stableValue(projection)));
|
|
1530
|
+
};
|
|
1531
|
+
const validateOwnership = (root, record, label) => {
|
|
1532
|
+
for (const entry of record.paths ?? []) {
|
|
1533
|
+
if (entry.mode !== 'managed') continue;
|
|
1534
|
+
const path = join(root, entry.path);
|
|
1535
|
+
if (!existsSync(path)) throw new Error(label + ' managed path is missing: ' + entry.path);
|
|
1536
|
+
if (ownedSha256(readFileSync(path, 'utf8'), entry) !== entry.sha256) {
|
|
1537
|
+
throw new Error(label + ' managed path has drifted: ' + entry.path);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
};
|
|
1541
|
+
|
|
1542
|
+
const workspace = readJson('.ebk/ownership.json');
|
|
1543
|
+
const foundation = readJson('packages/foundation/.ebk/ownership.json');
|
|
1544
|
+
if (
|
|
1545
|
+
workspace.schemaVersion !== ${index_js_1.WORKSPACE_OWNERSHIP_SCHEMA_VERSION} ||
|
|
1546
|
+
workspace.kind !== 'workspace' ||
|
|
1547
|
+
foundation.schemaVersion !== ${index_js_1.FOUNDATION_OWNERSHIP_SCHEMA_VERSION} ||
|
|
1548
|
+
foundation.kind !== 'foundation'
|
|
1549
|
+
) {
|
|
1550
|
+
throw new Error('EBK workspace or Foundation ownership metadata is incompatible.');
|
|
1551
|
+
}
|
|
1552
|
+
validateOwnership('.', workspace, 'Workspace');
|
|
1553
|
+
validateOwnership('packages/foundation', foundation, 'Foundation');
|
|
1554
|
+
|
|
1555
|
+
const policy = validateRepositoryOperatingPolicy(readJson('ebk.repository-policy.json'));
|
|
1556
|
+
if (!policy.valid) {
|
|
1557
|
+
throw new Error(
|
|
1558
|
+
'Repository Operating Policy is invalid:\\n' +
|
|
1559
|
+
policy.issues.map(({ message, path }) => '- ' + path + ': ' + message).join('\\n'),
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
for (const name of readdirSync('packages')) {
|
|
1564
|
+
if (name === 'foundation') continue;
|
|
1565
|
+
const ownershipPath = join('packages', name, '.ebk/ownership.json');
|
|
1566
|
+
if (!existsSync(ownershipPath)) continue;
|
|
1567
|
+
const ownership = readJson(ownershipPath);
|
|
1568
|
+
if (ownership.schemaVersion !== ${index_js_1.GENERATED_OWNERSHIP_SCHEMA_VERSION}) {
|
|
1569
|
+
throw new Error('Product Slice ' + name + ' ownership metadata is incompatible.');
|
|
1570
|
+
}
|
|
1571
|
+
validateOwnership(join('packages', name), ownership, 'Product Slice ' + name);
|
|
1572
|
+
}
|
|
1573
|
+
const markerReport = scanWorkMarkers(process.cwd());
|
|
1574
|
+
process.stdout.write(formatWorkMarkerReport(markerReport) + '\\n');
|
|
1575
|
+
assertWorkMarkerReport(markerReport);
|
|
1576
|
+
process.stdout.write('Empire Builder Kit workspace source health is green.\\n');
|
|
1577
|
+
`,
|
|
1578
|
+
};
|
|
1579
|
+
files[FOUNDATION_OWNERSHIP_PATH] = json(createFoundationOwnership(files, frameworkVersion));
|
|
1580
|
+
files[WORKSPACE_OWNERSHIP_PATH] = json(createWorkspaceOwnership(files, frameworkVersion));
|
|
1581
|
+
return files;
|
|
1582
|
+
}
|
|
1583
|
+
function renderWorkspaceRootFilePlan(options) {
|
|
1584
|
+
const workspaceFiles = renderWorkspaceFiles(options);
|
|
1585
|
+
const files = Object.fromEntries(Object.entries(workspaceFiles).filter(([path]) => !path.startsWith(FOUNDATION_ROOT)));
|
|
1586
|
+
return {
|
|
1587
|
+
files,
|
|
1588
|
+
ownership: JSON.parse(workspaceFiles[WORKSPACE_OWNERSHIP_PATH]),
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
function renderFoundationFilePlan(options) {
|
|
1592
|
+
const workspaceFiles = renderWorkspaceFiles(options);
|
|
1593
|
+
const files = Object.fromEntries(Object.entries(workspaceFiles)
|
|
1594
|
+
.filter(([path]) => path.startsWith(FOUNDATION_ROOT))
|
|
1595
|
+
.map(([path, contents]) => [
|
|
1596
|
+
path.slice(FOUNDATION_ROOT.length),
|
|
1597
|
+
contents,
|
|
1598
|
+
]));
|
|
1599
|
+
return {
|
|
1600
|
+
files,
|
|
1601
|
+
ownership: JSON.parse(workspaceFiles[FOUNDATION_OWNERSHIP_PATH]),
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
exports.WORKSPACE_PACKAGE_MANAGER_VERSION = TOOL_VERSIONS.pnpm;
|
|
1605
|
+
//# sourceMappingURL=workspace-files.js.map
|