@msn-control/liftoff 0.9.8 → 0.10.0
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/DEVELOPER.md +142 -0
- package/README.md +26 -18
- package/assets/governance/single-maintainer-gitflow/policy.md +62 -13
- package/dist/args.js +21 -0
- package/dist/args.js.map +1 -1
- package/dist/artifact-lifecycle.d.ts +1 -1
- package/dist/artifact-lifecycle.js +5 -0
- package/dist/artifact-lifecycle.js.map +1 -1
- package/dist/commands.js +355 -13
- package/dist/commands.js.map +1 -1
- package/dist/file-system.js +100 -13
- package/dist/file-system.js.map +1 -1
- package/dist/governance-activation/activation-state.d.ts +32 -0
- package/dist/governance-activation/activation-state.js +165 -0
- package/dist/governance-activation/activation-state.js.map +1 -0
- package/dist/governance-activation/approvals.d.ts +188 -0
- package/dist/governance-activation/approvals.js +508 -0
- package/dist/governance-activation/approvals.js.map +1 -0
- package/dist/governance-activation/canonical-json.d.ts +3 -0
- package/dist/governance-activation/canonical-json.js +36 -0
- package/dist/governance-activation/canonical-json.js.map +1 -0
- package/dist/governance-activation/commands.d.ts +10 -0
- package/dist/governance-activation/commands.js +1023 -0
- package/dist/governance-activation/commands.js.map +1 -0
- package/dist/governance-activation/compatibility.d.ts +68 -0
- package/dist/governance-activation/compatibility.js +447 -0
- package/dist/governance-activation/compatibility.js.map +1 -0
- package/dist/governance-activation/credentials.d.ts +160 -0
- package/dist/governance-activation/credentials.js +403 -0
- package/dist/governance-activation/credentials.js.map +1 -0
- package/dist/governance-activation/doctor.d.ts +10 -0
- package/dist/governance-activation/doctor.js +379 -0
- package/dist/governance-activation/doctor.js.map +1 -0
- package/dist/governance-activation/evidence.d.ts +49 -0
- package/dist/governance-activation/evidence.js +246 -0
- package/dist/governance-activation/evidence.js.map +1 -0
- package/dist/governance-activation/graph.d.ts +397 -0
- package/dist/governance-activation/graph.js +441 -0
- package/dist/governance-activation/graph.js.map +1 -0
- package/dist/governance-activation/identity.d.ts +35 -0
- package/dist/governance-activation/identity.js +89 -0
- package/dist/governance-activation/identity.js.map +1 -0
- package/dist/governance-activation/index.d.ts +18 -0
- package/dist/governance-activation/index.js +19 -0
- package/dist/governance-activation/index.js.map +1 -0
- package/dist/governance-activation/migration.d.ts +44 -0
- package/dist/governance-activation/migration.js +268 -0
- package/dist/governance-activation/migration.js.map +1 -0
- package/dist/governance-activation/readiness.d.ts +23 -0
- package/dist/governance-activation/readiness.js +158 -0
- package/dist/governance-activation/readiness.js.map +1 -0
- package/dist/governance-activation/reconciliation.d.ts +11 -0
- package/dist/governance-activation/reconciliation.js +58 -0
- package/dist/governance-activation/reconciliation.js.map +1 -0
- package/dist/governance-activation/release-integrity.d.ts +11 -0
- package/dist/governance-activation/release-integrity.js +49 -0
- package/dist/governance-activation/release-integrity.js.map +1 -0
- package/dist/governance-activation/seed-lifecycle.d.ts +116 -0
- package/dist/governance-activation/seed-lifecycle.js +609 -0
- package/dist/governance-activation/seed-lifecycle.js.map +1 -0
- package/dist/governance-activation/source-of-truth.d.ts +164 -0
- package/dist/governance-activation/source-of-truth.js +986 -0
- package/dist/governance-activation/source-of-truth.js.map +1 -0
- package/dist/governance-activation/task-projection.d.ts +21 -0
- package/dist/governance-activation/task-projection.js +75 -0
- package/dist/governance-activation/task-projection.js.map +1 -0
- package/dist/governance-activation/transitions.d.ts +163 -0
- package/dist/governance-activation/transitions.js +2128 -0
- package/dist/governance-activation/transitions.js.map +1 -0
- package/dist/governance-activation/type-contract.d.ts +1 -0
- package/dist/governance-activation/type-contract.js +27 -0
- package/dist/governance-activation/type-contract.js.map +1 -0
- package/dist/governance-activation/types.d.ts +390 -0
- package/dist/governance-activation/types.js +108 -0
- package/dist/governance-activation/types.js.map +1 -0
- package/dist/governance-activation/validators.d.ts +17 -0
- package/dist/governance-activation/validators.js +1305 -0
- package/dist/governance-activation/validators.js.map +1 -0
- package/dist/init-filesystem.js +5 -1
- package/dist/init-filesystem.js.map +1 -1
- package/dist/planner.js +3 -3
- package/dist/planner.js.map +1 -1
- package/dist/power-apps-templates.js +14 -1
- package/dist/power-apps-templates.js.map +1 -1
- package/dist/process-runner.d.ts +3 -0
- package/dist/process-runner.js +68 -10
- package/dist/process-runner.js.map +1 -1
- package/dist/repository-governance.d.ts +13 -3
- package/dist/repository-governance.js +385 -52
- package/dist/repository-governance.js.map +1 -1
- package/dist/telemetry/contract.d.ts +1 -1
- package/dist/telemetry/contract.js +6 -0
- package/dist/telemetry/contract.js.map +1 -1
- package/dist/templates.js +320 -25
- package/dist/templates.js.map +1 -1
- package/dist/types.d.ts +14 -6
- package/docs/cli-reference.md +60 -12
- package/docs/configuration-and-manifests.md +40 -16
- package/docs/existing-repositories.md +22 -11
- package/docs/getting-started.md +46 -10
- package/docs/prerequisites.md +5 -0
- package/docs/project-structure.md +34 -5
- package/docs/repository-governance.md +200 -203
- package/docs/safety-and-consent.md +29 -2
- package/docs/troubleshooting.md +74 -4
- package/docs/workloads.md +1 -1
- package/package.json +2 -1
package/dist/commands.js
CHANGED
|
@@ -26,12 +26,19 @@ import { lookupStableRelease } from './stable-release.js';
|
|
|
26
26
|
import { supportedStack } from './supported-stack.js';
|
|
27
27
|
import { buildArtifacts, buildManifest, partitionGeneratedArtifacts } from './templates.js';
|
|
28
28
|
import { PresentationSession } from './terminal.js';
|
|
29
|
+
import { governanceCommand } from './governance-activation/commands.js';
|
|
30
|
+
import { loadActivationState as loadCurrentActivationState } from './governance-activation/activation-state.js';
|
|
31
|
+
import { planHistoricalActivationStateMigration, updateFailureInjectionEnv } from './governance-activation/migration.js';
|
|
32
|
+
import { currentActivationIdentity } from './governance-activation/graph.js';
|
|
33
|
+
import { governanceChangeMetadataFileName, reconcileActiveGovernanceChange, validateGovernanceChangeMetadata } from './governance-activation/source-of-truth.js';
|
|
34
|
+
import { governanceDoctorChecks } from './governance-activation/doctor.js';
|
|
29
35
|
import { liftoffVersion } from './version.js';
|
|
30
36
|
import { blockingReadinessFailures, detectHostEnvironment, installRequirement, probeWorkstation, selectLiftoffRuntimeRequirements, selectWorkstationRequirements } from './workstation.js';
|
|
31
37
|
export async function runCommand(parsed, context) {
|
|
32
38
|
const helpRequested = parsed.command !== undefined && readBooleanFlag(parsed.flags, 'help') === true;
|
|
33
39
|
const jsonMode = !helpRequested && (parsed.command === 'doctor' ||
|
|
34
40
|
parsed.command === 'update' ||
|
|
41
|
+
parsed.command === 'governance' ||
|
|
35
42
|
parsed.command === 'upgrade' ||
|
|
36
43
|
parsed.command === 'validate') &&
|
|
37
44
|
readBooleanFlag(parsed.flags, 'json') === true;
|
|
@@ -81,6 +88,8 @@ export async function runCommand(parsed, context) {
|
|
|
81
88
|
return await migrateCommand(parsed, executionContext);
|
|
82
89
|
case 'doctor':
|
|
83
90
|
return await doctorCommand(parsed, executionContext);
|
|
91
|
+
case 'governance':
|
|
92
|
+
return await governanceCommand(parsed, executionContext);
|
|
84
93
|
case 'dev':
|
|
85
94
|
return helperCommand(parsed, executionContext, 'docker compose');
|
|
86
95
|
case 'infra':
|
|
@@ -239,9 +248,11 @@ async function initCommand(parsed, context) {
|
|
|
239
248
|
label: 'Repository governance',
|
|
240
249
|
value: plan.governanceProfile.id === 'none'
|
|
241
250
|
? 'Disabled'
|
|
242
|
-
: '
|
|
251
|
+
: 'Deterministic setup generated; run /liftoff-setup next'
|
|
243
252
|
}
|
|
244
|
-
],
|
|
253
|
+
], plan.governanceProfile.id === 'none'
|
|
254
|
+
? `liftoff validate ${JSON.stringify(target.root)}`
|
|
255
|
+
: '/liftoff-setup');
|
|
245
256
|
return 0;
|
|
246
257
|
}
|
|
247
258
|
finally {
|
|
@@ -718,7 +729,11 @@ async function executeMigration(parsed, context, sourceRoot) {
|
|
|
718
729
|
await assertSafeInitTarget(target, parentDir);
|
|
719
730
|
await assertNewOrEmptyDirectory(targetRoot);
|
|
720
731
|
presentation.stage('Check workstation readiness');
|
|
721
|
-
const
|
|
732
|
+
const readinessParent = process.env.LIFTOFF_STAGING_ROOT
|
|
733
|
+
? path.resolve(process.env.LIFTOFF_STAGING_ROOT)
|
|
734
|
+
: os.tmpdir();
|
|
735
|
+
await mkdir(readinessParent, { recursive: true });
|
|
736
|
+
const readinessRoot = await mkdtemp(path.join(readinessParent, 'liftoff-migrate-readiness-'));
|
|
722
737
|
const readiness = await (async () => {
|
|
723
738
|
try {
|
|
724
739
|
return await ensureWorkstationReady(plan, options, context, runner, presentation, prompter, `liftoff migrate ${JSON.stringify(sourceRoot)}`, readinessRoot);
|
|
@@ -1098,9 +1113,10 @@ function selectUpdatePreconditions(snapshots, entries, mutations, additionalPath
|
|
|
1098
1113
|
}
|
|
1099
1114
|
return snapshots.filter((snapshot) => requiredKeys.has(updateSnapshotKey(snapshot.pathParts)));
|
|
1100
1115
|
}
|
|
1101
|
-
function assertAuthorizedUpdateMutations(mutations, entries, provisioningPlans) {
|
|
1116
|
+
function assertAuthorizedUpdateMutations(mutations, entries, provisioningPlans, additionalAuthorizedPaths = []) {
|
|
1102
1117
|
const authorized = new Set([
|
|
1103
|
-
updateSnapshotKey(['liftoff.manifest.json'])
|
|
1118
|
+
updateSnapshotKey(['liftoff.manifest.json']),
|
|
1119
|
+
...additionalAuthorizedPaths.map((pathParts) => updateSnapshotKey(pathParts))
|
|
1104
1120
|
]);
|
|
1105
1121
|
for (const entry of entries) {
|
|
1106
1122
|
if (entry.rendered && entry.rendered.lifecycle !== 'managed-core') {
|
|
@@ -1127,6 +1143,232 @@ function assertAuthorizedUpdateMutations(mutations, entries, provisioningPlans)
|
|
|
1127
1143
|
}
|
|
1128
1144
|
}
|
|
1129
1145
|
}
|
|
1146
|
+
function identityFieldChanges(from, to = currentActivationIdentity) {
|
|
1147
|
+
return Object.keys(to)
|
|
1148
|
+
.filter((field) => from[field] !== undefined && from[field] !== to[field])
|
|
1149
|
+
.map((field) => ({
|
|
1150
|
+
field: `activationIdentity.${field}`,
|
|
1151
|
+
from: from[field],
|
|
1152
|
+
to: to[field]
|
|
1153
|
+
}));
|
|
1154
|
+
}
|
|
1155
|
+
function manifestChanges(manifest, plannedManifest) {
|
|
1156
|
+
const changes = [];
|
|
1157
|
+
if (manifest.artifactVersion !== plannedManifest.artifactVersion) {
|
|
1158
|
+
changes.push({
|
|
1159
|
+
field: 'artifactVersion',
|
|
1160
|
+
from: manifest.artifactVersion,
|
|
1161
|
+
to: plannedManifest.artifactVersion
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
if (manifest.liftoffVersion !== plannedManifest.liftoffVersion) {
|
|
1165
|
+
changes.push({
|
|
1166
|
+
field: 'liftoffVersion',
|
|
1167
|
+
from: manifest.liftoffVersion,
|
|
1168
|
+
to: plannedManifest.liftoffVersion
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
if (manifest.governance.profile !== 'none' &&
|
|
1172
|
+
manifest.governance.profile !== 'unspecified' &&
|
|
1173
|
+
plannedManifest.governance.profile !== 'none' &&
|
|
1174
|
+
plannedManifest.governance.profile !== 'unspecified') {
|
|
1175
|
+
if (manifest.governance.policyVersion !== plannedManifest.governance.policyVersion) {
|
|
1176
|
+
changes.push({
|
|
1177
|
+
field: 'governance.policyVersion',
|
|
1178
|
+
from: manifest.governance.policyVersion,
|
|
1179
|
+
to: plannedManifest.governance.policyVersion
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
const fromIdentity = manifest.governance.activationIdentity;
|
|
1183
|
+
const toIdentity = plannedManifest.governance.activationIdentity;
|
|
1184
|
+
if (toIdentity) {
|
|
1185
|
+
if (!fromIdentity) {
|
|
1186
|
+
changes.push({
|
|
1187
|
+
field: 'governance.activationIdentity',
|
|
1188
|
+
from: null,
|
|
1189
|
+
to: toIdentity
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
else {
|
|
1193
|
+
changes.push(...identityFieldChanges(fromIdentity, toIdentity));
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return changes;
|
|
1198
|
+
}
|
|
1199
|
+
function stateMigrationReconciliation(stateMigration) {
|
|
1200
|
+
if (stateMigration.status === 'blocked') {
|
|
1201
|
+
return {
|
|
1202
|
+
status: 'blocked',
|
|
1203
|
+
changedIdentityFields: [],
|
|
1204
|
+
phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
|
|
1205
|
+
issues: stateMigration.report.issues,
|
|
1206
|
+
remedy: 'Preserve governance/activation-state.json byte-for-byte and provide an explicit versioned import mapping; checkboxes, filenames, and prose cannot become evidence.'
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
if (stateMigration.status === 'migrate') {
|
|
1210
|
+
return {
|
|
1211
|
+
status: 'reconciliation-required',
|
|
1212
|
+
changedIdentityFields: identityFieldChanges(stateMigration.report.fromIdentity, stateMigration.report.toIdentity),
|
|
1213
|
+
phaseImpact: {
|
|
1214
|
+
preservedPhaseIds: stateMigration.report.preservedPhaseIds,
|
|
1215
|
+
invalidPhaseIds: stateMigration.report.invalidPhaseIds
|
|
1216
|
+
},
|
|
1217
|
+
issues: [
|
|
1218
|
+
`Historical activation state will be migrated with the managed update transaction; evidence bytes are preserved and ${stateMigration.report.reconciliationPath} records the explicit graph mapping.`
|
|
1219
|
+
],
|
|
1220
|
+
remedy: 'Run governance status/verify after update and acknowledge the reconciliation record before executing affected phases.'
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
return {
|
|
1224
|
+
status: 'not-required',
|
|
1225
|
+
changedIdentityFields: [],
|
|
1226
|
+
phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
|
|
1227
|
+
issues: []
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
function activeChangePathParts(stateKind, changeId) {
|
|
1231
|
+
return stateKind === 'openspec'
|
|
1232
|
+
? ['openspec', 'changes', changeId]
|
|
1233
|
+
: ['specs', changeId];
|
|
1234
|
+
}
|
|
1235
|
+
async function activeChangeReconciliationReport(projectRoot) {
|
|
1236
|
+
let loaded;
|
|
1237
|
+
try {
|
|
1238
|
+
loaded = await loadCurrentActivationState(projectRoot);
|
|
1239
|
+
}
|
|
1240
|
+
catch {
|
|
1241
|
+
return {
|
|
1242
|
+
status: 'not-required',
|
|
1243
|
+
changedIdentityFields: [],
|
|
1244
|
+
phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
|
|
1245
|
+
issues: []
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
const activeChange = loaded?.state.activeChange;
|
|
1249
|
+
if (!activeChange) {
|
|
1250
|
+
return {
|
|
1251
|
+
status: 'not-required',
|
|
1252
|
+
changedIdentityFields: [],
|
|
1253
|
+
phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
|
|
1254
|
+
issues: []
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
const metadataPathParts = [
|
|
1258
|
+
...activeChangePathParts(activeChange.kind, activeChange.id),
|
|
1259
|
+
governanceChangeMetadataFileName
|
|
1260
|
+
];
|
|
1261
|
+
const bytes = await readProjectFile(projectRoot, metadataPathParts);
|
|
1262
|
+
if (bytes === undefined) {
|
|
1263
|
+
return {
|
|
1264
|
+
status: 'blocked',
|
|
1265
|
+
changedIdentityFields: [],
|
|
1266
|
+
phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
|
|
1267
|
+
issues: [
|
|
1268
|
+
`${metadataPathParts.join('/')} is missing for active change ${activeChange.id}; no update mode can infer it from tasks or prose.`
|
|
1269
|
+
],
|
|
1270
|
+
remedy: 'Restore schema-valid governance metadata or record an explicit supersession before continuing setup.'
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
let parsed;
|
|
1274
|
+
try {
|
|
1275
|
+
parsed = JSON.parse(bytes.toString('utf8'));
|
|
1276
|
+
}
|
|
1277
|
+
catch (error) {
|
|
1278
|
+
return {
|
|
1279
|
+
status: 'blocked',
|
|
1280
|
+
changedIdentityFields: [],
|
|
1281
|
+
phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
|
|
1282
|
+
issues: [`Unable to parse ${metadataPathParts.join('/')}: ${error instanceof Error ? error.message : String(error)}`],
|
|
1283
|
+
remedy: 'Restore schema-valid governance metadata; no update mode can infer active governance from checkboxes, filenames, or prose.'
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
let reconciliation;
|
|
1287
|
+
try {
|
|
1288
|
+
const metadata = validateGovernanceChangeMetadata(parsed);
|
|
1289
|
+
reconciliation = reconcileActiveGovernanceChange({ metadata, evidence: [] });
|
|
1290
|
+
}
|
|
1291
|
+
catch (error) {
|
|
1292
|
+
return {
|
|
1293
|
+
status: 'blocked',
|
|
1294
|
+
changedIdentityFields: [],
|
|
1295
|
+
phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
|
|
1296
|
+
issues: [`Invalid ${metadataPathParts.join('/')}: ${error instanceof Error ? error.message : String(error)}`],
|
|
1297
|
+
remedy: 'Restore schema-valid governance metadata; no update mode can infer active governance from checkboxes, filenames, or prose.'
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
if (reconciliation.status === 'not-required') {
|
|
1301
|
+
return {
|
|
1302
|
+
status: 'not-required',
|
|
1303
|
+
changedIdentityFields: [],
|
|
1304
|
+
phaseImpact: {
|
|
1305
|
+
preservedPhaseIds: reconciliation.preservedPhaseIds,
|
|
1306
|
+
invalidPhaseIds: reconciliation.invalidPhaseIds
|
|
1307
|
+
},
|
|
1308
|
+
issues: []
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
if (reconciliation.status === 'blocked') {
|
|
1312
|
+
return {
|
|
1313
|
+
status: 'blocked',
|
|
1314
|
+
changedIdentityFields: [],
|
|
1315
|
+
phaseImpact: {
|
|
1316
|
+
preservedPhaseIds: reconciliation.preservedPhaseIds,
|
|
1317
|
+
invalidPhaseIds: reconciliation.invalidPhaseIds
|
|
1318
|
+
},
|
|
1319
|
+
issues: reconciliation.issues,
|
|
1320
|
+
remedy: 'Upgrade Liftoff or add an explicit compatibility mapping before executing affected governance phases.'
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
return {
|
|
1324
|
+
status: 'reconciliation-required',
|
|
1325
|
+
changedIdentityFields: identityFieldChanges(reconciliation.fromIdentity, reconciliation.toIdentity),
|
|
1326
|
+
phaseImpact: {
|
|
1327
|
+
preservedPhaseIds: reconciliation.preservedPhaseIds,
|
|
1328
|
+
invalidPhaseIds: reconciliation.invalidPhaseIds
|
|
1329
|
+
},
|
|
1330
|
+
issues: reconciliation.issues,
|
|
1331
|
+
remedy: 'Managed definitions and manifest may be updated, but governance status/verify will block affected execution until the active change acknowledges the installed identity.'
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
function combineReconciliationReports(reports) {
|
|
1335
|
+
if (reports.some((report) => report.status === 'blocked')) {
|
|
1336
|
+
return {
|
|
1337
|
+
status: 'blocked',
|
|
1338
|
+
changedIdentityFields: reports.flatMap((report) => report.changedIdentityFields),
|
|
1339
|
+
phaseImpact: {
|
|
1340
|
+
preservedPhaseIds: [],
|
|
1341
|
+
invalidPhaseIds: [...new Set(reports.flatMap((report) => report.phaseImpact.invalidPhaseIds))]
|
|
1342
|
+
},
|
|
1343
|
+
issues: reports.flatMap((report) => report.issues),
|
|
1344
|
+
remedy: reports.find((report) => report.remedy)?.remedy
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
if (reports.some((report) => report.status === 'reconciliation-required')) {
|
|
1348
|
+
return {
|
|
1349
|
+
status: 'reconciliation-required',
|
|
1350
|
+
changedIdentityFields: reports.flatMap((report) => report.changedIdentityFields),
|
|
1351
|
+
phaseImpact: {
|
|
1352
|
+
preservedPhaseIds: [...new Set(reports.flatMap((report) => report.phaseImpact.preservedPhaseIds))],
|
|
1353
|
+
invalidPhaseIds: [...new Set(reports.flatMap((report) => report.phaseImpact.invalidPhaseIds))]
|
|
1354
|
+
},
|
|
1355
|
+
issues: reports.flatMap((report) => report.issues),
|
|
1356
|
+
remedy: reports.find((report) => report.remedy)?.remedy
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
return {
|
|
1360
|
+
status: 'not-required',
|
|
1361
|
+
changedIdentityFields: [],
|
|
1362
|
+
phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
|
|
1363
|
+
issues: []
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
function maybeInjectUpdateFailure(env, stage) {
|
|
1367
|
+
const requested = env?.[updateFailureInjectionEnv] ?? process.env[updateFailureInjectionEnv];
|
|
1368
|
+
if (requested === stage) {
|
|
1369
|
+
throw new Error(`Injected managed-update failure at ${stage}.`);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1130
1372
|
async function updateCommand(parsed, context) {
|
|
1131
1373
|
const { presentation } = context;
|
|
1132
1374
|
const check = readBooleanFlag(parsed.flags, 'check') ?? false;
|
|
@@ -1224,18 +1466,52 @@ async function updateCommand(parsed, context) {
|
|
|
1224
1466
|
: await captureUpdateSnapshots(projectRoot, manifest, scopedRender, initialUpdateSnapshots);
|
|
1225
1467
|
const entries = await reconcileProject(manifest, render, projectRoot);
|
|
1226
1468
|
const summary = summarizeEntries(entries);
|
|
1227
|
-
const ownershipMigrationPending = manifest.artifactVersion !==
|
|
1469
|
+
const ownershipMigrationPending = manifest.artifactVersion !== 7 ||
|
|
1228
1470
|
manifestHadFilteredLegacyNonDurableOwnership(manifest);
|
|
1229
1471
|
const plannedManifest = buildManifest(renderPlan, render, {
|
|
1230
1472
|
frameworkState: manifest.framework.state,
|
|
1231
1473
|
projectArtifacts: manifest.projectArtifacts
|
|
1232
1474
|
});
|
|
1475
|
+
const plannedManifestChanges = manifestChanges(manifest, plannedManifest);
|
|
1476
|
+
const stateMigration = await planHistoricalActivationStateMigration(projectRoot);
|
|
1477
|
+
const reconciliation = combineReconciliationReports([
|
|
1478
|
+
stateMigrationReconciliation(stateMigration),
|
|
1479
|
+
await activeChangeReconciliationReport(projectRoot)
|
|
1480
|
+
]);
|
|
1481
|
+
if (reconciliation.status === 'blocked') {
|
|
1482
|
+
const blockedReport = {
|
|
1483
|
+
schemaVersion: 2,
|
|
1484
|
+
mode: check ? 'check' : 'apply',
|
|
1485
|
+
scope: 'managed-core',
|
|
1486
|
+
status: 'blocked',
|
|
1487
|
+
cliVersion: liftoffVersion,
|
|
1488
|
+
projectVersion: manifest.liftoffVersion,
|
|
1489
|
+
entries: [],
|
|
1490
|
+
provisioning: [],
|
|
1491
|
+
ownershipMigrationPending: false,
|
|
1492
|
+
manifestChanges: plannedManifestChanges,
|
|
1493
|
+
activationStateMigration: stateMigration.report,
|
|
1494
|
+
reconciliation,
|
|
1495
|
+
summary
|
|
1496
|
+
};
|
|
1497
|
+
if (jsonMode) {
|
|
1498
|
+
presentation.rawStdout(`${JSON.stringify(blockedReport, null, 2)}\n`);
|
|
1499
|
+
}
|
|
1500
|
+
else {
|
|
1501
|
+
presentation.status('error', 'Managed update blocked', reconciliation.issues.join('; '));
|
|
1502
|
+
if (reconciliation.remedy) {
|
|
1503
|
+
presentation.remedy(reconciliation.remedy);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
return 1;
|
|
1507
|
+
}
|
|
1233
1508
|
const workloadIntentChanged = !sameWorkloadIntent(plannedManifest.project.workload, manifest.project.workload);
|
|
1234
1509
|
const manifestRewritePending = ownershipMigrationPending ||
|
|
1235
1510
|
workloadIntentChanged;
|
|
1236
1511
|
const drift = hasDrift(entries) ||
|
|
1237
1512
|
manifestRewritePending ||
|
|
1238
|
-
provisioningPlans.length > 0
|
|
1513
|
+
provisioningPlans.length > 0 ||
|
|
1514
|
+
stateMigration.status === 'migrate';
|
|
1239
1515
|
const visible = entries.filter((entry) => entry.status !== 'unchanged' || entry.refreshHash);
|
|
1240
1516
|
if (!drift) {
|
|
1241
1517
|
if (jsonMode) {
|
|
@@ -1243,22 +1519,36 @@ async function updateCommand(parsed, context) {
|
|
|
1243
1519
|
schemaVersion: 2,
|
|
1244
1520
|
mode: 'check',
|
|
1245
1521
|
scope: 'managed-core',
|
|
1522
|
+
status: reconciliation.status === 'reconciliation-required' ? 'reconciliation-required' : 'current',
|
|
1246
1523
|
cliVersion: liftoffVersion,
|
|
1247
1524
|
projectVersion: manifest.liftoffVersion,
|
|
1248
1525
|
entries: [],
|
|
1249
1526
|
provisioning: [],
|
|
1250
1527
|
ownershipMigrationPending,
|
|
1528
|
+
manifestChanges: plannedManifestChanges,
|
|
1529
|
+
activationIdentity: manifest.governance.profile !== 'none' && manifest.governance.profile !== 'unspecified'
|
|
1530
|
+
? manifest.governance.activationIdentity ?? null
|
|
1531
|
+
: null,
|
|
1532
|
+
activationStateMigration: stateMigration.report,
|
|
1533
|
+
reconciliation,
|
|
1251
1534
|
summary
|
|
1252
1535
|
} : {
|
|
1253
1536
|
schemaVersion: 2,
|
|
1254
1537
|
mode: 'apply',
|
|
1255
1538
|
scope: 'managed-core',
|
|
1539
|
+
status: reconciliation.status === 'reconciliation-required' ? 'reconciliation-required' : 'current',
|
|
1256
1540
|
cliVersion: liftoffVersion,
|
|
1257
1541
|
projectVersion: manifest.liftoffVersion,
|
|
1258
1542
|
written: [],
|
|
1259
1543
|
skipped: [],
|
|
1260
1544
|
provisioning: [],
|
|
1261
1545
|
ownershipMigrationPending,
|
|
1546
|
+
manifestChanges: plannedManifestChanges,
|
|
1547
|
+
activationIdentity: manifest.governance.profile !== 'none' && manifest.governance.profile !== 'unspecified'
|
|
1548
|
+
? manifest.governance.activationIdentity ?? null
|
|
1549
|
+
: null,
|
|
1550
|
+
activationStateMigration: stateMigration.report,
|
|
1551
|
+
reconciliation,
|
|
1262
1552
|
summary
|
|
1263
1553
|
}, null, 2)}\n`);
|
|
1264
1554
|
return 0;
|
|
@@ -1287,6 +1577,12 @@ async function updateCommand(parsed, context) {
|
|
|
1287
1577
|
})),
|
|
1288
1578
|
provisioning: provisioningJson(provisioningPlans),
|
|
1289
1579
|
ownershipMigrationPending,
|
|
1580
|
+
manifestChanges: plannedManifestChanges,
|
|
1581
|
+
activationIdentity: plannedManifest.governance.profile !== 'none' && plannedManifest.governance.profile !== 'unspecified'
|
|
1582
|
+
? plannedManifest.governance.activationIdentity ?? null
|
|
1583
|
+
: null,
|
|
1584
|
+
activationStateMigration: stateMigration.report,
|
|
1585
|
+
reconciliation,
|
|
1290
1586
|
summary
|
|
1291
1587
|
}, null, 2)}\n`);
|
|
1292
1588
|
return 2;
|
|
@@ -1308,7 +1604,7 @@ async function updateCommand(parsed, context) {
|
|
|
1308
1604
|
}
|
|
1309
1605
|
const manifestMaintenance = [
|
|
1310
1606
|
ownershipMigrationPending
|
|
1311
|
-
? 'release legacy project artifacts from Liftoff update authority in manifest schema
|
|
1607
|
+
? 'release legacy project artifacts from Liftoff update authority in manifest schema v7; no production file will be written'
|
|
1312
1608
|
: undefined,
|
|
1313
1609
|
workloadIntentChanged
|
|
1314
1610
|
? 'record the requested project configuration intent after safe provisioning'
|
|
@@ -1317,6 +1613,18 @@ async function updateCommand(parsed, context) {
|
|
|
1317
1613
|
if (manifestMaintenance.length > 0) {
|
|
1318
1614
|
presentation.bullets('Manifest maintenance', manifestMaintenance);
|
|
1319
1615
|
}
|
|
1616
|
+
if (stateMigration.status === 'migrate') {
|
|
1617
|
+
presentation.bullets('Activation-state migration', [
|
|
1618
|
+
`${stateMigration.report.path}: explicit compatibility mapping preserves evidence bytes and stages ${stateMigration.report.reconciliationPath}`
|
|
1619
|
+
]);
|
|
1620
|
+
}
|
|
1621
|
+
if (reconciliation.status === 'reconciliation-required') {
|
|
1622
|
+
presentation.bullets('Reconciliation required after update', [
|
|
1623
|
+
...reconciliation.changedIdentityFields.map((field) => `${field.field}: ${JSON.stringify(field.from)} -> ${JSON.stringify(field.to)}`),
|
|
1624
|
+
`invalid phases: ${reconciliation.phaseImpact.invalidPhaseIds.join(', ') || 'none'}`,
|
|
1625
|
+
reconciliation.remedy ?? 'Run governance status/verify before executing affected phases.'
|
|
1626
|
+
]);
|
|
1627
|
+
}
|
|
1320
1628
|
const toWrite = summary.new +
|
|
1321
1629
|
summary.missing +
|
|
1322
1630
|
summary.upgrade +
|
|
@@ -1325,7 +1633,8 @@ async function updateCommand(parsed, context) {
|
|
|
1325
1633
|
provisioningPlans.reduce((count, group) => count + (group.blocked
|
|
1326
1634
|
? 0
|
|
1327
1635
|
: group.entries.filter((entry) => entry.status === 'create').length), 0) +
|
|
1328
|
-
(manifestRewritePending ? 1 : 0)
|
|
1636
|
+
(manifestRewritePending ? 1 : 0) +
|
|
1637
|
+
stateMigration.mutations.length;
|
|
1329
1638
|
presentation.status('warning', 'Liftoff core maintenance available', `${toWrite} to write, ${summary.conflict} core conflict(s), ${summary.orphan} core orphan(s), ${summary.unchanged} core unchanged`);
|
|
1330
1639
|
if (toWrite > 0) {
|
|
1331
1640
|
presentation.command('liftoff update');
|
|
@@ -1347,6 +1656,7 @@ async function updateCommand(parsed, context) {
|
|
|
1347
1656
|
}
|
|
1348
1657
|
presentation.stage('Apply safe Liftoff core changes', projectRoot);
|
|
1349
1658
|
await preflightUpdate(projectRoot, entries, force);
|
|
1659
|
+
maybeInjectUpdateFailure(context.env, 'after-preflight');
|
|
1350
1660
|
const written = [];
|
|
1351
1661
|
const skipped = [];
|
|
1352
1662
|
const mutations = [];
|
|
@@ -1414,6 +1724,9 @@ async function updateCommand(parsed, context) {
|
|
|
1414
1724
|
provisioned.push(entry);
|
|
1415
1725
|
}
|
|
1416
1726
|
}
|
|
1727
|
+
for (const mutation of stateMigration.mutations) {
|
|
1728
|
+
mutations.push(mutation);
|
|
1729
|
+
}
|
|
1417
1730
|
const oldByName = new Map(manifest.managedArtifacts.map((artifact) => [artifact.logicalName, artifact]));
|
|
1418
1731
|
const skippedByName = new Map(skipped.map((entry) => [entry.logicalName, entry]));
|
|
1419
1732
|
const hasUnrecordedGovernanceConflict = skipped.some((entry) => entry.status === 'conflict' &&
|
|
@@ -1462,22 +1775,40 @@ async function updateCommand(parsed, context) {
|
|
|
1462
1775
|
pathParts: ['liftoff.manifest.json'],
|
|
1463
1776
|
content: `${JSON.stringify(nextManifest, null, 2)}\n`
|
|
1464
1777
|
});
|
|
1465
|
-
assertAuthorizedUpdateMutations(mutations, entries, provisioningPlans);
|
|
1778
|
+
assertAuthorizedUpdateMutations(mutations, entries, provisioningPlans, stateMigration.mutations.map((mutation) => mutation.pathParts));
|
|
1466
1779
|
await applyProjectFileTransaction(projectRoot, mutations, {
|
|
1467
|
-
|
|
1780
|
+
onBeforeMutation: async (mutation, index) => {
|
|
1781
|
+
maybeInjectUpdateFailure(context.env, `before-mutation:${index}`);
|
|
1782
|
+
maybeInjectUpdateFailure(context.env, `before-path:${mutation.pathParts.join('/')}`);
|
|
1783
|
+
},
|
|
1784
|
+
preconditions: [
|
|
1785
|
+
...selectUpdatePreconditions(updateSnapshots, entries, mutations, provisioned.map((entry) => entry.rendered.pathParts)),
|
|
1786
|
+
...stateMigration.preconditions
|
|
1787
|
+
]
|
|
1468
1788
|
});
|
|
1469
1789
|
if (jsonMode) {
|
|
1470
1790
|
presentation.rawStdout(`${JSON.stringify({
|
|
1471
1791
|
schemaVersion: 2,
|
|
1472
1792
|
mode: 'apply',
|
|
1473
1793
|
scope: 'managed-core',
|
|
1474
|
-
status: blockedProvisioning.length > 0
|
|
1794
|
+
status: blockedProvisioning.length > 0
|
|
1795
|
+
? 'partial'
|
|
1796
|
+
: reconciliation.status === 'reconciliation-required'
|
|
1797
|
+
? 'reconciliation-required'
|
|
1798
|
+
: 'applied',
|
|
1475
1799
|
cliVersion: liftoffVersion,
|
|
1476
1800
|
projectVersion: manifest.liftoffVersion,
|
|
1477
1801
|
written: written.map((entry) => manifestDisplayPath(entry.pathParts)),
|
|
1802
|
+
stateWritten: stateMigration.mutations.map((mutation) => manifestDisplayPath(mutation.pathParts)),
|
|
1478
1803
|
skipped: skipped.map((entry) => ({ path: manifestDisplayPath(entry.pathParts), reason: entry.reason })),
|
|
1479
1804
|
provisioning: provisioningJson(provisioningPlans),
|
|
1480
1805
|
ownershipMigrationPending,
|
|
1806
|
+
manifestChanges: manifestChanges(manifest, nextManifest),
|
|
1807
|
+
activationIdentity: nextManifest.governance.profile !== 'none' && nextManifest.governance.profile !== 'unspecified'
|
|
1808
|
+
? nextManifest.governance.activationIdentity ?? null
|
|
1809
|
+
: null,
|
|
1810
|
+
activationStateMigration: stateMigration.report,
|
|
1811
|
+
reconciliation,
|
|
1481
1812
|
summary
|
|
1482
1813
|
}, null, 2)}\n`);
|
|
1483
1814
|
return 0;
|
|
@@ -1485,6 +1816,9 @@ async function updateCommand(parsed, context) {
|
|
|
1485
1816
|
if (written.length > 0) {
|
|
1486
1817
|
presentation.bullets('Applied Liftoff core changes', written.map((entry) => `wrote ${entryDisplay(entry)}`));
|
|
1487
1818
|
}
|
|
1819
|
+
if (stateMigration.status === 'migrate') {
|
|
1820
|
+
presentation.bullets('Migrated activation state', stateMigration.mutations.map((mutation) => `wrote ${manifestDisplayPath(mutation.pathParts)}; evidence bytes preserved`));
|
|
1821
|
+
}
|
|
1488
1822
|
if (provisioned.length > 0) {
|
|
1489
1823
|
presentation.bullets('Provisioned project components', provisioned.map((entry) => `${entry.status === 'create' ? 'wrote' : 'adopted'} ${manifestDisplayPath(entry.rendered.pathParts)}`));
|
|
1490
1824
|
}
|
|
@@ -1500,6 +1834,13 @@ async function updateCommand(parsed, context) {
|
|
|
1500
1834
|
if (orphans.length > 0) {
|
|
1501
1835
|
presentation.bullets('Orphaned Liftoff core artifacts', orphans.map((entry) => `orphan ${entryDisplay(entry)} ${entry.reason}`));
|
|
1502
1836
|
}
|
|
1837
|
+
if (reconciliation.status === 'reconciliation-required') {
|
|
1838
|
+
presentation.bullets('Reconciliation required', [
|
|
1839
|
+
...reconciliation.changedIdentityFields.map((field) => `${field.field}: ${JSON.stringify(field.from)} -> ${JSON.stringify(field.to)}`),
|
|
1840
|
+
`invalid phases: ${reconciliation.phaseImpact.invalidPhaseIds.join(', ') || 'none'}`,
|
|
1841
|
+
reconciliation.remedy ?? 'Run governance status/verify before executing affected phases.'
|
|
1842
|
+
]);
|
|
1843
|
+
}
|
|
1503
1844
|
presentation.completion('Updated project', [
|
|
1504
1845
|
`${written.length} core written`,
|
|
1505
1846
|
`${provisioned.length} project provisioned`,
|
|
@@ -1961,6 +2302,7 @@ async function projectLayer(projectRoot, manifest) {
|
|
|
1961
2302
|
detail,
|
|
1962
2303
|
...(remedy ? { remedy } : {})
|
|
1963
2304
|
});
|
|
2305
|
+
checks.push(...await governanceDoctorChecks(projectRoot, manifest));
|
|
1964
2306
|
}
|
|
1965
2307
|
checks.push(...await frameworkDoctorChecks(projectRoot, manifest));
|
|
1966
2308
|
if (manifest.project.workload.kind === 'power-apps-code-app') {
|
|
@@ -1985,7 +2327,7 @@ async function projectLayer(projectRoot, manifest) {
|
|
|
1985
2327
|
const provisioningPlans = await inspectProvisioningGroups(projectRoot, render, requestedProvisioningGroups(manifest, plan));
|
|
1986
2328
|
const driftCount = entries.filter((entry) => entry.status !== 'unchanged' || entry.refreshHash).length +
|
|
1987
2329
|
provisioningPlans.length +
|
|
1988
|
-
(manifest.artifactVersion ===
|
|
2330
|
+
(manifest.artifactVersion === 7 ? 0 : 1);
|
|
1989
2331
|
if (driftCount > 0) {
|
|
1990
2332
|
checks.push({
|
|
1991
2333
|
label: 'managed core',
|