@empire-builder-kit/nx 1.0.0-rc.44 → 1.0.0-rc.46
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/package.json +3 -3
- package/src/blueprints/external-sandbox.d.ts +1 -1
- package/src/cli/main.d.ts +11 -5
- package/src/cli/main.js +88 -29
- package/src/container-images.d.ts +1 -1
- package/src/container-images.js +1 -1
- package/src/developer-agent/generator-planning.js +10 -3
- package/src/developer-agent/operation-registry.js +1 -1
- package/src/executors/deployment-plan/deployment-plan.js +5 -1
- package/src/executors/deployment-plan/schema.json +1 -1
- package/src/executors/fleet/fleet.js +4 -0
- package/src/executors/fleet/schema.json +1 -1
- package/src/executors/github-bootstrap/github-bootstrap.d.ts +3 -0
- package/src/executors/github-bootstrap/github-bootstrap.js +28 -0
- package/src/executors/github-bootstrap/schema.json +14 -0
- package/src/executors/local-deploy/federated-credential-transport.d.ts +4 -3
- package/src/executors/local-deploy/federated-credential-transport.js +15 -5
- package/src/foundation/cost-estimate.d.ts +8 -3
- package/src/foundation/cost-estimate.js +42 -19
- package/src/foundation/index.d.ts +1 -0
- package/src/foundation/index.js +1 -0
- package/src/foundation/local-private-smoke-access.js +6 -0
- package/src/foundation/logging.d.ts +3 -1
- package/src/foundation/logging.js +8 -1
- package/src/foundation/observability.d.ts +42 -0
- package/src/foundation/observability.js +181 -0
- package/src/foundation/preflight.js +1 -1
- package/src/generators/function/function.js +5 -2
- package/src/generators/observability-infrastructure.d.ts +2 -0
- package/src/generators/observability-infrastructure.js +31 -0
- package/src/generators/observability-policy.d.ts +4 -0
- package/src/generators/observability-policy.js +23 -0
- package/src/generators/preset/dependency-overrides.d.ts +17 -6
- package/src/generators/preset/dependency-overrides.js +17 -6
- package/src/generators/preset/tool-versions.d.ts +2 -2
- package/src/generators/preset/tool-versions.js +2 -2
- package/src/generators/preset/workspace-files-adopter-skills.js +28 -17
- package/src/generators/preset/workspace-files-agent-development.d.ts +9 -0
- package/src/generators/preset/workspace-files-agent-development.js +42 -0
- package/src/generators/preset/workspace-files-thin-deploy.js +17 -4
- package/src/generators/preset/workspace-files-thin-docs.js +10 -22
- package/src/generators/preset/workspace-files.d.ts +1 -0
- package/src/generators/preset/workspace-files.js +120 -35
- package/src/generators/slice/built-in-plan-alarm-identity.d.ts +173 -0
- package/src/generators/slice/built-in-plan-alarm-identity.js +202 -0
- package/src/generators/slice/built-in-plan-api-contract-files.js +136 -0
- package/src/generators/slice/built-in-plan-data-files.js +10 -3
- package/src/generators/slice/built-in-plan-database-lifecycle-files.js +1 -1
- package/src/generators/slice/built-in-plan-event-contract-literal.d.ts +6 -0
- package/src/generators/slice/built-in-plan-event-contract-literal.js +52 -0
- package/src/generators/slice/built-in-plan-infrastructure-files.js +304 -144
- package/src/generators/slice/built-in-plan-key-value-retention-infrastructure.js +2 -2
- package/src/generators/slice/built-in-plan-metadata-files.js +37 -2
- package/src/generators/slice/built-in-plan-operations-files.js +106 -6
- package/src/generators/slice/built-in-plan-project-files.js +1 -0
- package/src/generators/slice/built-in-plan-release-files.js +2 -44
- package/src/generators/slice/built-in-plan-reliability-files.js +222 -236
- package/src/generators/slice/built-in-plan-shared.d.ts +29 -3
- package/src/generators/slice/built-in-plan-shared.js +29 -2
- package/src/generators/slice/built-in-plan-verification-files.js +0 -1
- package/src/generators/slice/built-in-plan-web-files.js +22 -6
- package/src/generators/slice/built-in-plan.js +2 -2
- package/src/generators/slice/slice.js +2 -0
- package/src/generators/upgrade/upgrade.d.ts +4 -3
- package/src/generators/upgrade/upgrade.js +139 -29
- package/src/generators/workload/frozen-container.d.ts +1 -1
- package/src/generators/workload/reliability-registration.d.ts +1 -1
- package/src/generators/workload/reliability-registration.js +9 -9
- package/src/generators/workload/workload.d.ts +1 -1
- package/src/generators/workload/workload.js +32 -10
- package/src/git-policy/github-bootstrap.d.ts +18 -0
- package/src/git-policy/github-bootstrap.js +99 -6
- package/src/local-dev/dev-command-core.d.ts +24 -3
- package/src/local-dev/dev-command-core.js +124 -12
- package/src/local-dev/errors.d.ts +19 -1
- package/src/local-dev/errors.js +18 -1
- package/src/local-dev/fleet-client.d.ts +64 -6
- package/src/local-dev/fleet-client.js +189 -15
- package/src/local-dev/fleet-process.d.ts +47 -2
- package/src/local-dev/fleet-process.js +248 -31
- package/src/local-dev/fleet-runtime.d.ts +37 -0
- package/src/local-dev/fleet-runtime.js +127 -69
- package/src/local-dev/fleet-session.d.ts +1 -0
- package/src/local-dev/fleet-session.js +83 -7
- package/src/local-dev/fleet-state.d.ts +5 -0
- package/src/local-dev/fleet-state.js +11 -2
- package/src/local-dev/fleet-supervisor-daemon.js +1 -5
- package/src/local-dev/fleet-supervisor.d.ts +7 -0
- package/src/local-dev/fleet-supervisor.js +83 -12
- package/src/local-dev/slice-runner.d.ts +19 -1
- package/src/local-dev/slice-runner.js +290 -91
- package/src/repository-policy/deployment-plan.d.ts +1 -1
- package/src/repository-policy/deployment-plan.js +16 -1
- package/src/secrets/generated-aws-control-plane.d.ts +11 -1
- package/src/secrets/generated-aws-control-plane.js +92 -29
- package/src/secrets/operator.d.ts +3 -2
- package/src/secrets/operator.js +27 -5
- package/src/supply-chain/blueprint-verification-authority.js +24 -3
- package/src/supply-chain/generated-files.js +9 -0
- package/src/supply-chain/sbom.js +74 -12
|
@@ -11,7 +11,9 @@ exports.upgradeGenerator = upgradeGenerator;
|
|
|
11
11
|
const deterministic_order_js_1 = require("../../deterministic-order.js");
|
|
12
12
|
const capacity_migration_js_1 = require("../../repository-policy/capacity-migration.js");
|
|
13
13
|
const dependency_overrides_js_1 = require("../preset/dependency-overrides.js");
|
|
14
|
+
const observability_js_1 = require("../../foundation/observability.js");
|
|
14
15
|
const workspace_files_adopter_skills_js_1 = require("../preset/workspace-files-adopter-skills.js");
|
|
16
|
+
const observability_policy_js_1 = require("../observability-policy.js");
|
|
15
17
|
const sql_seeded_reconciliation_js_1 = require("./sql-seeded-reconciliation.js");
|
|
16
18
|
const devkit_1 = require("@nx/devkit");
|
|
17
19
|
const node_buffer_1 = require("node:buffer");
|
|
@@ -25,7 +27,8 @@ const index_js_2 = require("../../api-contract/index.js");
|
|
|
25
27
|
const index_js_3 = require("../../foundation/index.js");
|
|
26
28
|
const capability_plan_js_1 = require("../../foundation/capability-plan.js");
|
|
27
29
|
const persistence_contract_js_1 = require("../slice/persistence-contract.js");
|
|
28
|
-
const index_js_4 = require("../../
|
|
30
|
+
const index_js_4 = require("../../repository-policy/index.js");
|
|
31
|
+
const index_js_5 = require("../../ownership/index.js");
|
|
29
32
|
const function_js_1 = require("../function/function.js");
|
|
30
33
|
const logging_registry_js_1 = require("../logging-registry.js");
|
|
31
34
|
const feature_registry_js_1 = require("../feature-registry.js");
|
|
@@ -380,6 +383,15 @@ const SEEDED_RECONCILIATION = {
|
|
|
380
383
|
'journey-authority',
|
|
381
384
|
],
|
|
382
385
|
},
|
|
386
|
+
'reliability/metric-declarations.ts': {
|
|
387
|
+
category: 'reliability-metrics',
|
|
388
|
+
guidance: 'Review existing custom recordReliabilityOperation calls and declare each finite deployment-owned component/feature/operation tuple in reliability/metric-declarations.ts. Undeclared custom series are dropped by the managed recorder. Never derive declarations from request, user, resource, or correlation IDs. Preserve existing declarations, verify custom metric emission, retain evidence, and resolve the seeded reconciliation marker before protected promotion.',
|
|
389
|
+
implications: [
|
|
390
|
+
'custom-metric-series-allowlist',
|
|
391
|
+
'finite-deployment-owned-dimensions',
|
|
392
|
+
'undeclared-custom-series-are-dropped',
|
|
393
|
+
],
|
|
394
|
+
},
|
|
383
395
|
'secrets/declarations.ts': {
|
|
384
396
|
category: 'secret-contract',
|
|
385
397
|
implications: [
|
|
@@ -458,6 +470,89 @@ function appendSeededReconciliationMarker(tree, root, report) {
|
|
|
458
470
|
return;
|
|
459
471
|
tree.write(anchor, `${current.replace(/\s*$/, '')}\n\n${SEEDED_RECONCILIATION_MARKER}\n`);
|
|
460
472
|
}
|
|
473
|
+
function isJsonObject(value) {
|
|
474
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
475
|
+
}
|
|
476
|
+
function appendServiceTaskPresenceReconciliation(currentFiles, currentOwnership, targetOwnership, report) {
|
|
477
|
+
const services = currentOwnership.compositions
|
|
478
|
+
.filter(({ kind }) => kind === 'service')
|
|
479
|
+
.map(({ name }) => name)
|
|
480
|
+
.sort(deterministic_order_js_1.compareUtf16);
|
|
481
|
+
if (services.length === 0)
|
|
482
|
+
return;
|
|
483
|
+
const registryPath = 'operations/reliability.json';
|
|
484
|
+
const source = currentFiles.get(registryPath);
|
|
485
|
+
if (source === null || source === undefined)
|
|
486
|
+
return;
|
|
487
|
+
let operations;
|
|
488
|
+
try {
|
|
489
|
+
operations = JSON.parse(source);
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
throw new Error('Retained Service reliability registry must be valid JSON before upgrade.');
|
|
493
|
+
}
|
|
494
|
+
if (!isJsonObject(operations) ||
|
|
495
|
+
!Array.isArray(operations.deployedAlarmResources)) {
|
|
496
|
+
throw new Error('Retained Service reliability registry must expose deployedAlarmResources before upgrade.');
|
|
497
|
+
}
|
|
498
|
+
const resources = operations.deployedAlarmResources;
|
|
499
|
+
const append = (path, guidance) => {
|
|
500
|
+
const current = currentFiles.get(path);
|
|
501
|
+
const targetEntry = targetOwnership.paths.find((entry) => entry.path === path);
|
|
502
|
+
const currentEntry = currentOwnership.paths.find((entry) => entry.path === path);
|
|
503
|
+
if (current === null ||
|
|
504
|
+
current === undefined ||
|
|
505
|
+
targetEntry?.mode !== 'seeded')
|
|
506
|
+
return;
|
|
507
|
+
const existing = report.seededReconciliation.find((entry) => entry.path === path);
|
|
508
|
+
if (existing) {
|
|
509
|
+
existing.guidance = `${existing.guidance ? `${existing.guidance}\n\n` : ''}${guidance}`;
|
|
510
|
+
existing.implications = [
|
|
511
|
+
...new Set([...existing.implications, 'service-live-task-presence']),
|
|
512
|
+
].sort(deterministic_order_js_1.compareUtf16);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
report.seededReconciliation.push({
|
|
516
|
+
adopterContentSha256: (0, index_js_5.hashOwnedContent)(current, currentEntry ?? targetEntry),
|
|
517
|
+
category: 'reliability-operations',
|
|
518
|
+
currentFrameworkSha256: currentEntry?.sha256 ?? null,
|
|
519
|
+
guidance,
|
|
520
|
+
implications: ['alarm-registration', 'service-live-task-presence'],
|
|
521
|
+
markerReference: SEEDED_RECONCILIATION_MARKER_REFERENCE,
|
|
522
|
+
path,
|
|
523
|
+
requiredAction: 'review-and-reconcile-preserving-adopter-content',
|
|
524
|
+
targetFrameworkSha256: targetEntry.sha256,
|
|
525
|
+
});
|
|
526
|
+
};
|
|
527
|
+
for (const name of services) {
|
|
528
|
+
const id = `service-${name}-health`;
|
|
529
|
+
const legacy = resources.some((entry) => {
|
|
530
|
+
if (!isJsonObject(entry))
|
|
531
|
+
return false;
|
|
532
|
+
const expected = {
|
|
533
|
+
comparisonOperator: 'LessThanThreshold',
|
|
534
|
+
evaluationPeriods: 1,
|
|
535
|
+
id,
|
|
536
|
+
metricName: 'RunningTaskCount',
|
|
537
|
+
name: `${currentOwnership.slice}-{stage}-service-${name}-running`,
|
|
538
|
+
namespace: 'ECS/ContainerInsights',
|
|
539
|
+
period: 60,
|
|
540
|
+
signal: 'metric',
|
|
541
|
+
statistic: 'Sum',
|
|
542
|
+
threshold: 1,
|
|
543
|
+
treatMissingData: 'breaching',
|
|
544
|
+
};
|
|
545
|
+
return (Object.entries(expected).every(([key, value]) => entry[key] === value) &&
|
|
546
|
+
JSON.stringify(entry.dimensionNames) ===
|
|
547
|
+
JSON.stringify(['ClusterName', 'ServiceName']));
|
|
548
|
+
});
|
|
549
|
+
if (!legacy)
|
|
550
|
+
continue;
|
|
551
|
+
append(registryPath, `For recorded Service ${name}, reconcile only deployedAlarmResources[id=${id}]: namespace ECS/ContainerInsights -> AWS/ECS, metricName RunningTaskCount -> LiveTaskCount, statistic Sum -> Minimum. Keep its ID, physical name, actions, dimensions, threshold 1, LessThanThreshold, 60-second period, one evaluation period and breaching missing-data policy. In alarmRegistrations[id=${id}], set missingData to breaching and review businessSymptom, condition and both expected risk fields for task presence or missing telemetry, startup/emission-gap false positives, and activating/draining/replacement crash-loop false negatives. Preserve unrelated registrations and every adopter-owned field. Review services/${name}/RUNBOOK.md, run the declared reliability verification after authorized deployment, retain evidence, then resolve the existing seeded reconciliation marker.`);
|
|
552
|
+
append(`services/${name}/RUNBOOK.md`, `Review the retained Service ${name} runbook for AWS/ECS LiveTaskCount Minimum: ACTIVATING, RUNNING and DEACTIVATING task presence, not readiness. Missing telemetry breaches but does not prove zero tasks. Document startup/emission-gap false positives and activating/draining/replacement crash-loop false negatives, preserve custom recovery instructions, verify the critical journey and alarm recovery after an authorized roll-forward, retain evidence, then resolve the existing seeded reconciliation marker.`);
|
|
553
|
+
}
|
|
554
|
+
report.seededReconciliation.sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.path, right.path));
|
|
555
|
+
}
|
|
461
556
|
function currentContractConsumers(tree, root) {
|
|
462
557
|
const source = tree.read(node_path_1.posix.join(root, 'contracts/dependencies.json'), 'utf8');
|
|
463
558
|
if (source === null)
|
|
@@ -667,7 +762,7 @@ function preserveBuiltInDerivedApiArtifacts(tree, root, currentOwnership, target
|
|
|
667
762
|
entry?.mode === 'managed' &&
|
|
668
763
|
entry.origin === 'blueprint' &&
|
|
669
764
|
entry.managedJsonPointers === undefined &&
|
|
670
|
-
(0,
|
|
765
|
+
(0, index_js_5.hashOwnedContent)(current, entry) === entry.sha256);
|
|
671
766
|
if (!completeCleanPair)
|
|
672
767
|
return targetFiles;
|
|
673
768
|
const retained = { ...targetFiles };
|
|
@@ -821,16 +916,17 @@ function ownershipValidationMessage(label, issues) {
|
|
|
821
916
|
}
|
|
822
917
|
function requireUpgradePrerequisites(tree, target) {
|
|
823
918
|
const installedVersion = (0, generator_js_1.frameworkPackageVersion)();
|
|
824
|
-
const workspaceResult = (0,
|
|
919
|
+
const workspaceResult = (0, index_js_5.validateWorkspaceOwnershipRecord)(readJson(tree, '.ebk/ownership.json', 'Workspace ownership metadata'));
|
|
825
920
|
if (!workspaceResult.valid) {
|
|
826
921
|
throw new Error(ownershipValidationMessage('Workspace ownership metadata', workspaceResult.issues));
|
|
827
922
|
}
|
|
828
923
|
if (workspaceResult.value.frameworkVersion !== installedVersion) {
|
|
829
924
|
throw new Error(`Workspace tooling is recorded at ${workspaceResult.value.frameworkVersion}, but the installed @empire-builder-kit/nx version is ${installedVersion}. Report and apply the workspace upgrade before upgrading the ${target === 'foundation' ? 'Standard Foundation' : 'Product Slice'}.`);
|
|
830
925
|
}
|
|
926
|
+
(0, observability_policy_js_1.requireObservabilityPolicyFromTree)(tree);
|
|
831
927
|
if (target === 'foundation')
|
|
832
928
|
return;
|
|
833
|
-
const foundationResult = (0,
|
|
929
|
+
const foundationResult = (0, index_js_5.validateFoundationOwnershipRecord)(readJson(tree, 'packages/foundation/.ebk/ownership.json', 'Standard Foundation ownership metadata'));
|
|
834
930
|
if (!foundationResult.valid) {
|
|
835
931
|
throw new Error(ownershipValidationMessage('Standard Foundation ownership metadata', foundationResult.issues));
|
|
836
932
|
}
|
|
@@ -959,21 +1055,29 @@ function createUpgradeReport(currentFiles, currentOwnership, targetFiles, target
|
|
|
959
1055
|
path.startsWith('database/migrations/')) {
|
|
960
1056
|
report.incompatibleSeededMigrations.push(path);
|
|
961
1057
|
}
|
|
962
|
-
else if (current !== null &&
|
|
963
|
-
|
|
1058
|
+
else if (((current !== null && currentEntry?.sha256 !== targetSha256) ||
|
|
1059
|
+
(current === null &&
|
|
1060
|
+
path === 'reliability/metric-declarations.ts')) &&
|
|
964
1061
|
seededReconciliationDefinition(path) !== null) {
|
|
965
1062
|
const reconciliation = seededReconciliationDefinition(path);
|
|
966
1063
|
if (reconciliation === null) {
|
|
967
1064
|
throw new Error(`Missing seeded reconciliation definition for ${path}.`);
|
|
968
1065
|
}
|
|
969
1066
|
report.seededReconciliation.push({
|
|
970
|
-
adopterContentSha256:
|
|
1067
|
+
adopterContentSha256: current === null
|
|
1068
|
+
? null
|
|
1069
|
+
: (0, index_js_5.hashOwnedContent)(current, currentEntry ?? targetEntry),
|
|
971
1070
|
category: reconciliation.category,
|
|
972
1071
|
currentFrameworkSha256: currentEntry?.sha256 ?? null,
|
|
1072
|
+
...('guidance' in reconciliation
|
|
1073
|
+
? { guidance: reconciliation.guidance }
|
|
1074
|
+
: {}),
|
|
973
1075
|
implications: [...reconciliation.implications],
|
|
974
1076
|
markerReference: SEEDED_RECONCILIATION_MARKER_REFERENCE,
|
|
975
1077
|
path,
|
|
976
|
-
requiredAction:
|
|
1078
|
+
requiredAction: current === null
|
|
1079
|
+
? 'declare-existing-custom-metric-series-before-promotion'
|
|
1080
|
+
: 'review-and-reconcile-preserving-adopter-content',
|
|
977
1081
|
targetFrameworkSha256: targetSha256,
|
|
978
1082
|
});
|
|
979
1083
|
}
|
|
@@ -982,7 +1086,7 @@ function createUpgradeReport(currentFiles, currentOwnership, targetFiles, target
|
|
|
982
1086
|
report.missing.push(path);
|
|
983
1087
|
}
|
|
984
1088
|
else if (currentEntry !== undefined &&
|
|
985
|
-
(0,
|
|
1089
|
+
(0, index_js_5.hashOwnedContent)(current, currentEntry) !== currentEntry.sha256) {
|
|
986
1090
|
report.conflicting.push(path);
|
|
987
1091
|
}
|
|
988
1092
|
else if (current === target) {
|
|
@@ -1005,7 +1109,7 @@ function createUpgradeReport(currentFiles, currentOwnership, targetFiles, target
|
|
|
1005
1109
|
const current = currentFiles.get(path) ?? null;
|
|
1006
1110
|
if (current === null)
|
|
1007
1111
|
continue;
|
|
1008
|
-
if ((0,
|
|
1112
|
+
if ((0, index_js_5.hashOwnedContent)(current, entry) === sha256) {
|
|
1009
1113
|
report.obsolete.push(path);
|
|
1010
1114
|
}
|
|
1011
1115
|
else if (!report.conflicting.includes(path)) {
|
|
@@ -1060,7 +1164,7 @@ function mergeStructuredTargetFiles(currentFiles, currentOwnership, targetFiles,
|
|
|
1060
1164
|
if (!targetEntry.managedJsonPointers)
|
|
1061
1165
|
continue;
|
|
1062
1166
|
if (currentEntry?.managedJsonPointers) {
|
|
1063
|
-
const transition = (0,
|
|
1167
|
+
const transition = (0, index_js_5.analyzeManagedJsonPointerTransition)(current, target, currentEntry.managedJsonPointers, targetEntry.managedJsonPointers);
|
|
1064
1168
|
changes.claimedJsonMembers.push(...transition.claimed.map((pointer) => `${targetEntry.path}#${pointer}`));
|
|
1065
1169
|
changes.conflictingJsonMembers.push(...transition.conflictingClaims.map((pointer) => `${targetEntry.path}#${pointer}`));
|
|
1066
1170
|
changes.releasedJsonMembers.push(...transition.released.map((pointer) => `${targetEntry.path}#${pointer}`));
|
|
@@ -1069,7 +1173,7 @@ function mergeStructuredTargetFiles(currentFiles, currentOwnership, targetFiles,
|
|
|
1069
1173
|
continue;
|
|
1070
1174
|
}
|
|
1071
1175
|
}
|
|
1072
|
-
files.set(targetEntry.path, (0,
|
|
1176
|
+
files.set(targetEntry.path, (0, index_js_5.mergeManagedJsonContents)(current, target, targetEntry.managedJsonPointers));
|
|
1073
1177
|
}
|
|
1074
1178
|
return { files, ...changes };
|
|
1075
1179
|
}
|
|
@@ -1089,7 +1193,7 @@ async function formatMergedManagedJsonFiles(tree, root, merge, ownership) {
|
|
|
1089
1193
|
}));
|
|
1090
1194
|
return { ...merge, files };
|
|
1091
1195
|
}
|
|
1092
|
-
function renderCompositionUpgradePlan(ownership, workspace, runtimeVersion, serviceAdapterOwner) {
|
|
1196
|
+
function renderCompositionUpgradePlan(ownership, workspace, runtimeVersion, serviceAdapterOwner, deploymentProfile) {
|
|
1093
1197
|
const compositionPaths = ownership.paths.filter(({ origin }) => origin === 'composition');
|
|
1094
1198
|
const files = {};
|
|
1095
1199
|
const paths = new Map();
|
|
@@ -1107,7 +1211,7 @@ function renderCompositionUpgradePlan(ownership, workspace, runtimeVersion, serv
|
|
|
1107
1211
|
mode: entry.mode ?? 'managed',
|
|
1108
1212
|
origin: 'composition',
|
|
1109
1213
|
path: entry.path,
|
|
1110
|
-
sha256: (0,
|
|
1214
|
+
sha256: (0, index_js_5.hashOwnedContent)(contents, entry),
|
|
1111
1215
|
});
|
|
1112
1216
|
}
|
|
1113
1217
|
};
|
|
@@ -1118,7 +1222,7 @@ function renderCompositionUpgradePlan(ownership, workspace, runtimeVersion, serv
|
|
|
1118
1222
|
}
|
|
1119
1223
|
else {
|
|
1120
1224
|
const plan = (0, workload_js_1.createWorkloadFilePlan)(workspace, ownership.slice, composition.name, composition.kind, runtimeVersion, composition.feature, composition.kind === 'service' &&
|
|
1121
|
-
composition.name === serviceAdapterOwner);
|
|
1225
|
+
composition.name === serviceAdapterOwner, [(0, index_js_4.productionRehearsalStage)(deploymentProfile), 'production']);
|
|
1122
1226
|
addPlan(plan.files, plan.paths);
|
|
1123
1227
|
}
|
|
1124
1228
|
}
|
|
@@ -1251,7 +1355,7 @@ async function upgradeFoundation(tree, options, packageJson, output, foundationR
|
|
|
1251
1355
|
throw new Error('Standard Foundation upgrades do not accept Blueprint or input options.');
|
|
1252
1356
|
}
|
|
1253
1357
|
const root = 'packages/foundation';
|
|
1254
|
-
const ownershipResult = (0,
|
|
1358
|
+
const ownershipResult = (0, index_js_5.validateFoundationOwnershipRecord)(readJson(tree, node_path_1.posix.join(root, '.ebk/ownership.json'), 'Standard Foundation ownership metadata'));
|
|
1255
1359
|
if (!ownershipResult.valid) {
|
|
1256
1360
|
throw new Error(ownershipValidationMessage('Standard Foundation ownership metadata', ownershipResult.issues));
|
|
1257
1361
|
}
|
|
@@ -1271,7 +1375,7 @@ async function upgradeFoundation(tree, options, packageJson, output, foundationR
|
|
|
1271
1375
|
...renderedTarget,
|
|
1272
1376
|
files: await formatRenderedFiles(tree, renderedTarget.files, root),
|
|
1273
1377
|
};
|
|
1274
|
-
const targetValidation = (0,
|
|
1378
|
+
const targetValidation = (0, index_js_5.validateFoundationOwnershipRecord)(target.ownership);
|
|
1275
1379
|
if (!targetValidation.valid) {
|
|
1276
1380
|
throw new Error(`Target Standard Foundation ownership metadata is invalid:\n${targetValidation.issues
|
|
1277
1381
|
.map((issue) => `- ${issue.path}: ${issue.message}`)
|
|
@@ -1312,7 +1416,7 @@ async function upgradeFoundation(tree, options, packageJson, output, foundationR
|
|
|
1312
1416
|
}
|
|
1313
1417
|
tree.write(node_path_1.posix.join(root, path), contents);
|
|
1314
1418
|
}
|
|
1315
|
-
(0,
|
|
1419
|
+
(0, index_js_5.restampFoundationOwnership)(tree);
|
|
1316
1420
|
// Ownership bytes are part of the upgrade result, including agent previews
|
|
1317
1421
|
// that skip workspace formatting. Canonicalize before binding those bytes.
|
|
1318
1422
|
await formatOwnershipRecord(tree, node_path_1.posix.join(root, '.ebk/ownership.json'), 'Standard Foundation');
|
|
@@ -1339,7 +1443,7 @@ async function upgradeWorkspace(tree, options, packageJson, output) {
|
|
|
1339
1443
|
if (options.blueprint !== undefined || options.inputs !== undefined) {
|
|
1340
1444
|
throw new Error('Workspace upgrades do not accept Blueprint or input options.');
|
|
1341
1445
|
}
|
|
1342
|
-
const ownershipResult = (0,
|
|
1446
|
+
const ownershipResult = (0, index_js_5.validateWorkspaceOwnershipRecord)(readJson(tree, '.ebk/ownership.json', 'Workspace ownership metadata'));
|
|
1343
1447
|
if (!ownershipResult.valid) {
|
|
1344
1448
|
throw new Error(ownershipValidationMessage('Workspace ownership metadata', ownershipResult.issues));
|
|
1345
1449
|
}
|
|
@@ -1354,6 +1458,9 @@ async function upgradeWorkspace(tree, options, packageJson, output) {
|
|
|
1354
1458
|
dependencyOverrides: tree.exists(dependency_overrides_js_1.DEPENDENCY_OVERRIDES_PATH)
|
|
1355
1459
|
? readJson(tree, dependency_overrides_js_1.DEPENDENCY_OVERRIDES_PATH, 'Dependency overrides')
|
|
1356
1460
|
: undefined,
|
|
1461
|
+
observabilityPolicy: tree.exists(observability_js_1.OBSERVABILITY_POLICY_PATH)
|
|
1462
|
+
? readJson(tree, observability_js_1.OBSERVABILITY_POLICY_PATH, 'Observability policy')
|
|
1463
|
+
: (0, observability_js_1.defaultObservabilityPolicy)((0, index_js_4.activeDeploymentStages)(deployment.deploymentProfile ?? 'starter'), { preserveLegacy: true }),
|
|
1357
1464
|
frameworkVersion: (0, generator_js_1.frameworkPackageVersion)(),
|
|
1358
1465
|
name: packageJson.name,
|
|
1359
1466
|
});
|
|
@@ -1361,7 +1468,7 @@ async function upgradeWorkspace(tree, options, packageJson, output) {
|
|
|
1361
1468
|
...renderedTarget,
|
|
1362
1469
|
files: await formatRenderedFiles(tree, renderedTarget.files),
|
|
1363
1470
|
};
|
|
1364
|
-
const targetValidation = (0,
|
|
1471
|
+
const targetValidation = (0, index_js_5.validateWorkspaceOwnershipRecord)(target.ownership);
|
|
1365
1472
|
if (!targetValidation.valid) {
|
|
1366
1473
|
throw new Error('Target workspace ownership metadata is invalid.');
|
|
1367
1474
|
}
|
|
@@ -1417,7 +1524,7 @@ async function upgradeWorkspace(tree, options, packageJson, output) {
|
|
|
1417
1524
|
if (developerAgentInstructionAppend !== null) {
|
|
1418
1525
|
tree.write(developerAgentInstructionAppend.path, developerAgentInstructionAppend.contents);
|
|
1419
1526
|
}
|
|
1420
|
-
(0,
|
|
1527
|
+
(0, index_js_5.restampWorkspaceOwnership)(tree);
|
|
1421
1528
|
await formatOwnershipRecord(tree, '.ebk/ownership.json', 'Workspace');
|
|
1422
1529
|
}
|
|
1423
1530
|
async function executeUpgradeGenerator(tree, options, output = devkit_1.logger, executionOptions = {}) {
|
|
@@ -1445,7 +1552,7 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
|
|
|
1445
1552
|
if (recorded?.blueprint
|
|
1446
1553
|
?.name !== index_js_1.BUILTIN_BLUEPRINT_NAME)
|
|
1447
1554
|
continue;
|
|
1448
|
-
const parsed = (0,
|
|
1555
|
+
const parsed = (0, index_js_5.validateGeneratedOwnershipRecord)(recorded);
|
|
1449
1556
|
if (!parsed.valid)
|
|
1450
1557
|
throw new Error(ownershipValidationMessage('Product Slice ownership metadata', parsed.issues));
|
|
1451
1558
|
(0, persistence_contract_js_1.standardPersistenceContract)({
|
|
@@ -1462,7 +1569,7 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
|
|
|
1462
1569
|
}
|
|
1463
1570
|
requireUpgradePrerequisites(tree, 'product-slice');
|
|
1464
1571
|
const root = node_path_1.posix.join('packages', name);
|
|
1465
|
-
const ownershipResult = (0,
|
|
1572
|
+
const ownershipResult = (0, index_js_5.validateGeneratedOwnershipRecord)(readJson(tree, node_path_1.posix.join(root, '.ebk/ownership.json'), 'Product Slice ownership metadata'));
|
|
1466
1573
|
if (!ownershipResult.valid) {
|
|
1467
1574
|
throw new Error(ownershipValidationMessage('Product Slice ownership metadata', ownershipResult.issues));
|
|
1468
1575
|
}
|
|
@@ -1609,7 +1716,7 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
|
|
|
1609
1716
|
mode,
|
|
1610
1717
|
origin: 'blueprint',
|
|
1611
1718
|
path,
|
|
1612
|
-
sha256: (0,
|
|
1719
|
+
sha256: (0, index_js_5.hashOwnedContent)(contents, { managedJsonPointers }),
|
|
1613
1720
|
}));
|
|
1614
1721
|
paths.sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.path, right.path));
|
|
1615
1722
|
targetOwnership = {
|
|
@@ -1623,7 +1730,7 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
|
|
|
1623
1730
|
inputs: inputResult.value,
|
|
1624
1731
|
paths,
|
|
1625
1732
|
projects: generated.projects,
|
|
1626
|
-
schemaVersion:
|
|
1733
|
+
schemaVersion: index_js_5.GENERATED_OWNERSHIP_SCHEMA_VERSION,
|
|
1627
1734
|
slice: name,
|
|
1628
1735
|
verification: blueprint.manifest.verification,
|
|
1629
1736
|
};
|
|
@@ -1648,7 +1755,7 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
|
|
|
1648
1755
|
targetFiles = preserveBuiltInDerivedApiArtifacts(tree, root, currentOwnership, targetFiles);
|
|
1649
1756
|
}
|
|
1650
1757
|
const targetPathSet = new Set(targetOwnership.paths.map(({ path }) => path));
|
|
1651
|
-
const compositionPlan = renderCompositionUpgradePlan(currentOwnership, packageJson.name ?? '', packageJson.dependencies?.['@empire-builder-kit/runtime'] ?? '', serviceAdapterOwner);
|
|
1758
|
+
const compositionPlan = renderCompositionUpgradePlan(currentOwnership, packageJson.name ?? '', packageJson.dependencies?.['@empire-builder-kit/runtime'] ?? '', serviceAdapterOwner, (0, tree_policy_js_1.readRepositoryOperatingPolicyFromTree)(tree).deploymentProfile);
|
|
1652
1759
|
const compositionPaths = compositionPlan.paths.filter(({ path }) => !targetPathSet.has(path));
|
|
1653
1760
|
if (externalBlueprintDestinationPaths) {
|
|
1654
1761
|
(0, index_js_1.assertBlueprintDestinationPaths)(tree.root, root, [
|
|
@@ -1677,7 +1784,7 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
|
|
|
1677
1784
|
if (contents === undefined) {
|
|
1678
1785
|
throw new Error(`Target Blueprint did not render ${entry.path}.`);
|
|
1679
1786
|
}
|
|
1680
|
-
return { ...entry, sha256: (0,
|
|
1787
|
+
return { ...entry, sha256: (0, index_js_5.hashOwnedContent)(contents, entry) };
|
|
1681
1788
|
}),
|
|
1682
1789
|
projects: [
|
|
1683
1790
|
...new Set([...targetOwnership.projects, ...currentOwnership.projects]),
|
|
@@ -1687,7 +1794,7 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
|
|
|
1687
1794
|
...targetFiles,
|
|
1688
1795
|
'.ebk/ownership.json': json(targetOwnership),
|
|
1689
1796
|
};
|
|
1690
|
-
const targetValidation = (0,
|
|
1797
|
+
const targetValidation = (0, index_js_5.validateGeneratedOwnershipRecord)(targetOwnership);
|
|
1691
1798
|
if (!targetValidation.valid) {
|
|
1692
1799
|
throw new Error(`Target ownership metadata is invalid:\n${targetValidation.issues
|
|
1693
1800
|
.map((issue) => `- ${issue.path}: ${issue.message}`)
|
|
@@ -1721,6 +1828,9 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
|
|
|
1721
1828
|
mergedTarget.files.set(logging_registry_js_1.SLICE_LOGGING_REGISTRY_PATH, await (0, formatting_js_1.formatTreeContentsToFixedPoint)(loggingAppend, formatting, workspacePath));
|
|
1722
1829
|
}
|
|
1723
1830
|
const report = createUpgradeReport(currentFiles, currentOwnership, mergedTarget.files, targetOwnership, mergedTarget);
|
|
1831
|
+
if (blueprint.source === 'built-in') {
|
|
1832
|
+
appendServiceTaskPresenceReconciliation(currentFiles, currentOwnership, targetOwnership, report);
|
|
1833
|
+
}
|
|
1724
1834
|
if (loggingAppend !== null) {
|
|
1725
1835
|
report.preservedSeeded = report.preservedSeeded.filter((path) => path !== logging_registry_js_1.SLICE_LOGGING_REGISTRY_PATH);
|
|
1726
1836
|
report.appendedSeeded.push(logging_registry_js_1.SLICE_LOGGING_REGISTRY_PATH);
|
|
@@ -1774,7 +1884,7 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
|
|
|
1774
1884
|
await (0, formatting_js_1.formatTreeFileToFixedPoint)(tree, path);
|
|
1775
1885
|
}
|
|
1776
1886
|
}
|
|
1777
|
-
(0,
|
|
1887
|
+
(0, index_js_5.restampProductSliceOwnership)(tree, name, { preserveSeeded: true });
|
|
1778
1888
|
await formatOwnershipRecord(tree, node_path_1.posix.join(root, '.ebk/ownership.json'), 'Product Slice');
|
|
1779
1889
|
return options.skipInstall
|
|
1780
1890
|
? undefined
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const WORKLOAD_CONTAINER_NODE_IMAGE: "node:22-alpine@sha256:
|
|
1
|
+
export declare const WORKLOAD_CONTAINER_NODE_IMAGE: "node:22-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914";
|
|
2
2
|
export declare const WORKLOAD_CONTAINER_PNPM_VERSION: "11.20.0";
|
|
3
3
|
export declare const WORKLOAD_CONTAINER_PNPM_DESCRIPTOR: "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee";
|
|
4
4
|
export declare const WORKLOAD_CONTAINER_PNPM_STORE = "/workspace/.ebk/cache/pnpm-store";
|
|
@@ -3,7 +3,7 @@ export declare const WORKLOAD_RELIABILITY_REGISTRY_PATH: "operations/reliability
|
|
|
3
3
|
export interface WorkloadReliabilityPlan {
|
|
4
4
|
alarmId: string;
|
|
5
5
|
contents: string;
|
|
6
|
-
metricName: '
|
|
6
|
+
metricName: 'LiveTaskCount' | 'MatchedEvents';
|
|
7
7
|
path: string;
|
|
8
8
|
runbookRef: string;
|
|
9
9
|
}
|
|
@@ -61,21 +61,21 @@ function planWorkloadReliabilityRegistration(tree, input) {
|
|
|
61
61
|
...operations.alarmRegistrations,
|
|
62
62
|
(0, reliability_1.validateAlarmRegistrationMetadata)({
|
|
63
63
|
businessSymptom: input.kind === 'service'
|
|
64
|
-
? `${input.name} Service
|
|
64
|
+
? `${input.name} Service task presence is below one or telemetry is missing.`
|
|
65
65
|
: `${input.name} Job executions are failing.`,
|
|
66
66
|
condition: input.kind === 'service'
|
|
67
|
-
? 'The AWS
|
|
67
|
+
? 'The AWS/ECS LiveTaskCount minimum is below one or missing for the exact Service; it counts ACTIVATING, RUNNING, and DEACTIVATING tasks.'
|
|
68
68
|
: 'An AWS-native ECS task-state event reports an essential container exit for the exact Job task family.',
|
|
69
69
|
dependencyAlarmIds: [],
|
|
70
70
|
escalationRouteKey: `${input.slice}-on-call`,
|
|
71
71
|
expectedFalseNegativeRisk: input.kind === 'service'
|
|
72
|
-
? '
|
|
72
|
+
? 'Activating, draining, or repeatedly replaced tasks can keep LiveTaskCount positive while readiness or useful work fails.'
|
|
73
73
|
: 'ECS task-state events are durable but can be delayed before EventBridge reports a match.',
|
|
74
74
|
expectedFalsePositiveRisk: input.kind === 'service'
|
|
75
|
-
? '
|
|
75
|
+
? 'Metric emission gaps, startup, or lifecycle transitions can breach the missing-data policy while tasks remain present.'
|
|
76
76
|
: 'A retried Job attempt can fail before a later attempt succeeds.',
|
|
77
77
|
id: alarmId,
|
|
78
|
-
missingData: 'missing',
|
|
78
|
+
missingData: input.kind === 'service' ? 'breaching' : 'missing',
|
|
79
79
|
owner,
|
|
80
80
|
runbookRef: `runbook:${runbookRef}`,
|
|
81
81
|
severity: 'high',
|
|
@@ -90,15 +90,15 @@ function planWorkloadReliabilityRegistration(tree, input) {
|
|
|
90
90
|
dimensionNames: input.kind === 'service' ? ['ClusterName', 'ServiceName'] : ['RuleName'],
|
|
91
91
|
evaluationPeriods: 1,
|
|
92
92
|
id: alarmId,
|
|
93
|
-
metricName: input.kind === 'service' ? '
|
|
93
|
+
metricName: input.kind === 'service' ? 'LiveTaskCount' : 'MatchedEvents',
|
|
94
94
|
name: `${input.slice}-{stage}-${input.kind}-${input.name}-${input.kind === 'service' ? 'running' : 'hard-crash'}`,
|
|
95
|
-
namespace: input.kind === 'service' ? 'ECS
|
|
95
|
+
namespace: input.kind === 'service' ? 'AWS/ECS' : 'AWS/Events',
|
|
96
96
|
owner,
|
|
97
97
|
period: 60,
|
|
98
98
|
route: `${input.slice}-on-call`,
|
|
99
99
|
runbook: runbookRef,
|
|
100
100
|
signal: 'metric',
|
|
101
|
-
statistic: 'Sum',
|
|
101
|
+
statistic: input.kind === 'service' ? 'Minimum' : 'Sum',
|
|
102
102
|
threshold: 1,
|
|
103
103
|
treatMissingData: input.kind === 'service' ? 'breaching' : 'notBreaching',
|
|
104
104
|
};
|
|
@@ -142,7 +142,7 @@ function planWorkloadReliabilityRegistration(tree, input) {
|
|
|
142
142
|
return {
|
|
143
143
|
alarmId,
|
|
144
144
|
contents: `${JSON.stringify(operations, null, 2)}\n`,
|
|
145
|
-
metricName: input.kind === 'service' ? '
|
|
145
|
+
metricName: input.kind === 'service' ? 'LiveTaskCount' : 'MatchedEvents',
|
|
146
146
|
path,
|
|
147
147
|
runbookRef,
|
|
148
148
|
};
|
|
@@ -13,5 +13,5 @@ export interface WorkloadFilePlan {
|
|
|
13
13
|
}>;
|
|
14
14
|
project: string;
|
|
15
15
|
}
|
|
16
|
-
export declare function createWorkloadFilePlan(workspace: string, slice: string, workload: string, kind: WorkloadKind, runtimeVersion: string, feature: string, ownsProtectedLoadAdapter: boolean, protectedRuntimeStages
|
|
16
|
+
export declare function createWorkloadFilePlan(workspace: string, slice: string, workload: string, kind: WorkloadKind, runtimeVersion: string, feature: string, ownsProtectedLoadAdapter: boolean, protectedRuntimeStages: readonly ('development' | 'production' | 'staging')[]): WorkloadFilePlan;
|
|
17
17
|
export declare function workloadGenerator(tree: Tree, options: WorkloadGeneratorSchema, kind: WorkloadKind): Promise<GeneratorCallback | void>;
|
|
@@ -4,6 +4,8 @@ exports.SERVICE_WORKLOAD_PACKAGE_MANAGED_JSON_POINTERS = exports.WORKLOAD_PACKAG
|
|
|
4
4
|
exports.createWorkloadFilePlan = createWorkloadFilePlan;
|
|
5
5
|
exports.workloadGenerator = workloadGenerator;
|
|
6
6
|
const deterministic_order_js_1 = require("../../deterministic-order.js");
|
|
7
|
+
const observability_infrastructure_js_1 = require("../observability-infrastructure.js");
|
|
8
|
+
const observability_policy_js_1 = require("../observability-policy.js");
|
|
7
9
|
const node_path_1 = require("node:path");
|
|
8
10
|
const index_js_1 = require("../../ownership/index.js");
|
|
9
11
|
const capability_plan_js_1 = require("../../foundation/capability-plan.js");
|
|
@@ -137,7 +139,7 @@ exports.SERVICE_WORKLOAD_PACKAGE_MANAGED_JSON_POINTERS = [
|
|
|
137
139
|
'/dependencies/@aws-sdk~1client-dynamodb',
|
|
138
140
|
'/dependencies/sst',
|
|
139
141
|
];
|
|
140
|
-
function createWorkloadFilePlan(workspace, slice, workload, kind, runtimeVersion, feature, ownsProtectedLoadAdapter, protectedRuntimeStages
|
|
142
|
+
function createWorkloadFilePlan(workspace, slice, workload, kind, runtimeVersion, feature, ownsProtectedLoadAdapter, protectedRuntimeStages) {
|
|
141
143
|
const plural = kind === 'service' ? 'services' : 'jobs';
|
|
142
144
|
const relativeRoot = node_path_1.posix.join(plural, workload);
|
|
143
145
|
const className = `${pascal(slice)}${pascal(workload)}${pascal(kind)}`;
|
|
@@ -159,7 +161,7 @@ function createWorkloadFilePlan(workspace, slice, workload, kind, runtimeVersion
|
|
|
159
161
|
version: '0.0.0',
|
|
160
162
|
scripts: {
|
|
161
163
|
build: 'tsc -p tsconfig.json',
|
|
162
|
-
'docker-build':
|
|
164
|
+
'docker-build': 'docker build -f Dockerfile ' +
|
|
163
165
|
`-t ${slice}-${kind}-${workload}:local ../../../..`,
|
|
164
166
|
dev: `node --experimental-strip-types ${kind === 'service' ? 'src/main.ts' : 'src/run.ts'}`,
|
|
165
167
|
lint: 'eslint .',
|
|
@@ -210,7 +212,7 @@ function createWorkloadFilePlan(workspace, slice, workload, kind, runtimeVersion
|
|
|
210
212
|
[`${relativeRoot}/src/workload.spec.ts`]: kind === 'service'
|
|
211
213
|
? `import { describe, expect, it } from 'vitest';\nimport { healthResponse } from './main.js';\n\ndescribe('${workload} service', () => {\n it('reports healthy only on the health endpoint', () => {\n expect(healthResponse('/health')).toEqual({ body: { service: '${workload}', status: 'ok' }, status: 200 });\n expect(healthResponse('/missing').status).toBe(404);\n });\n});\n`
|
|
212
214
|
: `import { createLogger, type LogEntry } from '@empire-builder-kit/runtime/logging';\nimport { describe, expect, it } from 'vitest';\nimport { run } from './run.js';\n\ndescribe('${workload} job', () => {\n it('emits canonical start and completion records', async () => {\n const entries: LogEntry[] = [];\n const logger = createLogger({\n component: 'job-${workload}', feature: '${feature}', operation: 'run',\n slice: '${slice}', stage: 'test', workspace: '${workspace}',\n sink: (entry) => entries.push(entry as LogEntry),\n });\n await expect(run(logger)).resolves.toEqual({\n ok: true,\n providerDeliveryAssigned: false,\n });\n expect(entries.map(({ message, outcome }) => ({ message, outcome }))).toEqual([\n { message: '${workload} job started', outcome: undefined },\n { message: '${workload} job completed', outcome: 'success' },\n ]);\n });\n});\n`,
|
|
213
|
-
[`${relativeRoot}/infra.ts`]: `import { createManagedLogGroupContract } from '@empire-builder-kit/nx/foundation';\nimport { standardSliceExtensionTaskFamily } from '../../infra/workload-roles/effective-intent.js';\n\nexport function ${factoryName}(\n cluster: any,\n alertTopicArn: any,\n roles?: { executionRole: string; taskRole: string },\n links: any[] = [],\n configuration?: {\n applicationId: any;\n deployment: any;\n environmentId: any;\n profileId: any;\n },\n) {\n const contract = createManagedLogGroupContract({\n component: '${kind}-${workload}',\n slice: '${slice}',\n stage: $app.stage,\n workspace: '${workspace}',\n });\n const workload = new sst.aws.${resource}('${className}', {\n ...(roles ?? {}),\n cluster,\n environment: {\n EBK_STAGE: $app.stage,\n ...(configuration\n ? {\n EBK_APPCONFIG_APPLICATION_ID: configuration.applicationId,\n EBK_APPCONFIG_ENVIRONMENT_ID: configuration.environmentId,\n EBK_APPCONFIG_PROFILE_ID: configuration.profileId,\n }\n : {}),\n },\n link: links,\n ${kind === 'service' ? "image: { context: './services/" + workload + "', dockerfile: 'Dockerfile' },\n health: { command: ['CMD-SHELL', 'wget -qO- http://localhost:3000/health || exit 1'] },\n logging: { name: contract.name, retention: '1 month' },\n serviceRegistry: { port: 3000 }," : "image: { context: './jobs/" + workload + "', dockerfile: 'Dockerfile' },\n logging: { name: contract.name, retention: '1 month' },"}\n transform: {\n logGroup: {\n logGroupClass: contract.logGroupClass,\n tags: contract.tags,\n },\n taskDefinition: {\n family: standardSliceExtensionTaskFamily(\n $app.stage,\n '${workload}',\n '${kind}',\n ),\n },\n },\n }, configuration?.deployment\n ? { dependsOn: [configuration.deployment] }\n : {});\n const dataProtection = new aws.cloudwatch.LogDataProtectionPolicy(\n '${className}LogDataProtection',\n {\n logGroupName: contract.name,\n policyDocument: JSON.stringify(contract.dataProtectionPolicy),\n },\n { dependsOn: [workload.nodes.taskDefinition] },\n );\n const dataProtectionAlarm = new aws.cloudwatch.MetricAlarm(\n '${className}LogDataProtectionFindings',\n {\n alarmActions: [alertTopicArn],\n comparisonOperator: 'GreaterThanOrEqualToThreshold',\n dimensions: { LogGroupName: contract.name },\n evaluationPeriods: 1,\n metricName: 'LogEventsWithFindings',\n namespace: 'AWS/Logs',\n period: 300,\n statistic: 'Sum',\n threshold: 1,\n treatMissingData: 'notBreaching',\n },\n { dependsOn: [dataProtection] },\n );\n void dataProtectionAlarm;\n return workload;\n}\n`,
|
|
215
|
+
[`${relativeRoot}/infra.ts`]: `import { createManagedLogGroupContract } from '@empire-builder-kit/nx/foundation';\nimport { standardSliceExtensionTaskFamily } from '../../infra/workload-roles/effective-intent.js';\n\nexport function ${factoryName}(\n cluster: any,\n alertTopicArn: any,\n roles?: { executionRole: string; taskRole: string },\n links: any[] = [],\n configuration?: {\n applicationId: any;\n deployment: any;\n environmentId: any;\n profileId: any;\n },\n) {\n const contract = createManagedLogGroupContract({\n component: '${kind}-${workload}',\n slice: '${slice}',\n stage: $app.stage,\n workspace: '${workspace}',\n });\n const workload = new sst.aws.${resource}('${className}', {\n ...(roles ?? {}),\n cluster,\n environment: {\n EBK_STAGE: $app.stage,\n ...(configuration\n ? {\n EBK_APPCONFIG_APPLICATION_ID: configuration.applicationId,\n EBK_APPCONFIG_ENVIRONMENT_ID: configuration.environmentId,\n EBK_APPCONFIG_PROFILE_ID: configuration.profileId,\n }\n : {}),\n },\n link: links,\n ${kind === 'service' ? "image: { context: './services/" + workload + "', dockerfile: 'Dockerfile' },\n health: { command: ['CMD-SHELL', 'wget -qO- http://localhost:3000/health || exit 1'] },\n logging: { name: contract.name, retention: '1 month' },\n serviceRegistry: { port: 3000 }," : "image: { context: './jobs/" + workload + "', dockerfile: 'Dockerfile' },\n logging: { name: contract.name, retention: '1 month' },"}\n transform: {\n logGroup: {\n logGroupClass: contract.logGroupClass,\n tags: contract.tags,\n },\n taskDefinition: {\n family: standardSliceExtensionTaskFamily(\n $app.stage,\n '${workload}',\n '${kind}',\n ),\n },\n },\n }, configuration?.deployment\n ? { dependsOn: [configuration.deployment] }\n : {});\n${kind === 'service' ? ' if ($dev) return workload;\n' : ''} const dataProtection = new aws.cloudwatch.LogDataProtectionPolicy(\n '${className}LogDataProtection',\n {\n logGroupName: contract.name,\n policyDocument: JSON.stringify(contract.dataProtectionPolicy),\n },\n { dependsOn: [workload.nodes.taskDefinition] },\n );\n const dataProtectionAlarm = new aws.cloudwatch.MetricAlarm(\n '${className}LogDataProtectionFindings',\n {\n alarmActions: [alertTopicArn],\n comparisonOperator: 'GreaterThanOrEqualToThreshold',\n dimensions: { LogGroupName: contract.name },\n evaluationPeriods: 1,\n metricName: 'LogEventsWithFindings',\n namespace: 'AWS/Logs',\n period: 300,\n statistic: 'Sum',\n threshold: 1,\n treatMissingData: 'notBreaching',\n },\n { dependsOn: [dataProtection] },\n );\n void dataProtectionAlarm;\n return workload;\n}\n`,
|
|
214
216
|
};
|
|
215
217
|
if (kind === 'service') {
|
|
216
218
|
const serviceMainPath = `${relativeRoot}/src/main.ts`;
|
|
@@ -411,7 +413,7 @@ ${kind !== 'service' || ownsProtectedLoadAdapter
|
|
|
411
413
|
alarmDescription: JSON.stringify({
|
|
412
414
|
businessSymptom:
|
|
413
415
|
'${kind === 'service'
|
|
414
|
-
? `${workload} Service
|
|
416
|
+
? `${workload} Service task presence is below one or telemetry is missing.`
|
|
415
417
|
: `${workload} Job essential container exited.`}',
|
|
416
418
|
id: '${kind === 'service'
|
|
417
419
|
? `service-${workload}-health`
|
|
@@ -426,14 +428,14 @@ ${kind !== 'service' || ownsProtectedLoadAdapter
|
|
|
426
428
|
? 'LessThanThreshold'
|
|
427
429
|
: 'GreaterThanOrEqualToThreshold'}',
|
|
428
430
|
dimensions: ${kind === 'service'
|
|
429
|
-
? `{ ClusterName: cluster.name, ServiceName: workload.name }`
|
|
431
|
+
? `{ ClusterName: cluster.nodes.cluster.name, ServiceName: workload.nodes.service.name }`
|
|
430
432
|
: `{ RuleName: reliabilityTaskStopped.name }`},
|
|
431
433
|
evaluationPeriods: 1,
|
|
432
|
-
metricName: '${kind === 'service' ? '
|
|
434
|
+
metricName: '${kind === 'service' ? 'LiveTaskCount' : 'MatchedEvents'}',
|
|
433
435
|
name: '${slice}-' + $app.stage + '-${kind}-${workload}-${kind === 'service' ? 'running' : 'hard-crash'}',
|
|
434
|
-
namespace: '${kind === 'service' ? 'ECS
|
|
436
|
+
namespace: '${kind === 'service' ? 'AWS/ECS' : 'AWS/Events'}',
|
|
435
437
|
period: 60,
|
|
436
|
-
statistic: 'Sum',
|
|
438
|
+
statistic: '${kind === 'service' ? 'Minimum' : 'Sum'}',
|
|
437
439
|
tags: standardTags,
|
|
438
440
|
threshold: 1,
|
|
439
441
|
treatMissingData: '${kind === 'service' ? 'breaching' : 'notBreaching'}',
|
|
@@ -539,7 +541,8 @@ ${kind !== 'service' || ownsProtectedLoadAdapter
|
|
|
539
541
|
}
|
|
540
542
|
const dataProtection = new aws.cloudwatch.LogDataProtectionPolicy(`);
|
|
541
543
|
}
|
|
542
|
-
files[infrastructurePath] =
|
|
544
|
+
files[infrastructurePath] =
|
|
545
|
+
(0, observability_infrastructure_js_1.renderExtensionObservability)(infrastructureSource);
|
|
543
546
|
files[`${relativeRoot}/RUNBOOK.md`] = `# ${workload} ${kind} recovery
|
|
544
547
|
|
|
545
548
|
Confirm the exact Slice, stage, release, configuration, alarm, and workload
|
|
@@ -548,7 +551,25 @@ through the read-only diagnostic path. Do not restart, scale, retry, or replace
|
|
|
548
551
|
protected resources without the named approval.
|
|
549
552
|
|
|
550
553
|
${kind === 'service'
|
|
551
|
-
?
|
|
554
|
+
? `The native AWS/ECS LiveTaskCount alarm uses Minimum over 60 seconds and
|
|
555
|
+
counts tasks in ACTIVATING, RUNNING, and DEACTIVATING states. It breaches below
|
|
556
|
+
one or when telemetry is missing. ECS can emit no metrics when there are no
|
|
557
|
+
RUNNING tasks; missing telemetry is actionable but does not prove zero tasks.
|
|
558
|
+
|
|
559
|
+
Startup, emission gaps, and lifecycle transitions can cause false positives.
|
|
560
|
+
Activating, draining, or repeatedly replaced tasks can keep the count positive
|
|
561
|
+
while readiness or useful work fails. ALARM is not a diagnosis. OK does not
|
|
562
|
+
prove readiness, freedom from crash loops, or successful business traffic.
|
|
563
|
+
|
|
564
|
+
Inspect ECS events, task states and exit reasons, the existing container health
|
|
565
|
+
check, and the Slice API health and burn alarms. Do not infer that every Service
|
|
566
|
+
has a load-balancer readiness alarm. Prefer a verified roll-forward; verify
|
|
567
|
+
stable tasks, readiness, dependency protection, and the critical Service journey
|
|
568
|
+
after recovery, then confirm telemetry and alarm recovery.
|
|
569
|
+
|
|
570
|
+
LiveTaskCount adds no metric collection charge and needs no Container Insights
|
|
571
|
+
opt-in. Existing alarm, compute, log, and request charges still apply; this is
|
|
572
|
+
not a measured account-cost claim.`
|
|
552
573
|
: 'Preserve idempotency and checkpoint state; verify the business deadline and postconditions before retrying or declaring recovery.'}
|
|
553
574
|
`;
|
|
554
575
|
files[`${relativeRoot}/src/reliability.ts`] = `import {
|
|
@@ -1038,6 +1059,7 @@ async function executeWorkloadGenerator(tree, options, kind) {
|
|
|
1038
1059
|
const slice = name(options.slice, 'Product Slice name');
|
|
1039
1060
|
const workload = name(options.name, kind === 'service' ? 'Service name' : 'Job name');
|
|
1040
1061
|
const requestedFeature = name(options.feature, 'Feature');
|
|
1062
|
+
(0, observability_policy_js_1.requireObservabilityPolicyFromTree)(tree);
|
|
1041
1063
|
const plural = kind === 'service' ? 'services' : 'jobs';
|
|
1042
1064
|
const sliceRoot = node_path_1.posix.join('packages', slice);
|
|
1043
1065
|
const root = node_path_1.posix.join(sliceRoot, plural, workload);
|
|
@@ -72,5 +72,23 @@ export interface GitHubRepositoryIdentity {
|
|
|
72
72
|
repositoryId: string;
|
|
73
73
|
subjectPrefix: string;
|
|
74
74
|
}
|
|
75
|
+
export interface FrameworkNpmStableBootstrapOptions {
|
|
76
|
+
npmStable: true;
|
|
77
|
+
apiBaseUrl?: string;
|
|
78
|
+
apply?: boolean;
|
|
79
|
+
repository: string;
|
|
80
|
+
token: string;
|
|
81
|
+
npmStableReviewerIds: number[];
|
|
82
|
+
npmStablePreventSelfReview: boolean;
|
|
83
|
+
}
|
|
84
|
+
export interface FrameworkNpmStableBootstrapReport {
|
|
85
|
+
kind: 'framework-npm-stable';
|
|
86
|
+
repository: string;
|
|
87
|
+
ready: boolean;
|
|
88
|
+
blockers: string[];
|
|
89
|
+
changes: string[];
|
|
90
|
+
warnings: string[];
|
|
91
|
+
}
|
|
92
|
+
export declare function runGitHubBootstrap(options: FrameworkNpmStableBootstrapOptions): Promise<FrameworkNpmStableBootstrapReport>;
|
|
75
93
|
export declare function runGitHubBootstrap(options: GitHubBootstrapOptions): Promise<GitHubBootstrapReport>;
|
|
76
94
|
export {};
|