applyPreflight function
StalenessAssessment
applyPreflight({
- required ChangePlan plan,
- required String stagingRoot,
- required String canonicalDestination,
- ApplyFileSystem fileSystem = const ApplyFileSystem(),
- ApplyObservations? observations,
Implementation
StalenessAssessment applyPreflight({
required ChangePlan plan,
required String stagingRoot,
required String canonicalDestination,
ApplyFileSystem fileSystem = const ApplyFileSystem(),
ApplyObservations? observations,
List<String> unrelatedChangedPaths = const [],
}) {
if (plan.state.isTerminal) {
throw ApplyException(
'Plan state ${plan.state.name} is terminal and forbids Apply.',
exitCode: ExitCodes.planNotFoundOrStateForbids,
);
}
if (plan.state != PlanState.reviewed && plan.state != PlanState.approved) {
throw ApplyException(
'Plan state ${plan.state.name} forbids Apply.',
exitCode: ExitCodes.planNotFoundOrStateForbids,
);
}
final approval = verifyApproval(plan);
if (approval != ApprovalVerification.valid) {
throw ApplyException(
'Approval is absent or invalid: ${approval.name}.',
exitCode: ExitCodes.approvalRequiredAndAbsent,
);
}
if (!fileSystem.destinationWritable(canonicalDestination)) {
throw const ApplyException(
'Destination is not writable.',
exitCode: ExitCodes.planStaleOrStagingIntegrityFailure,
);
}
final currentNormalizedHashes = <String, String?>{};
for (final operation in _reviewableOperations(plan)) {
final expected = plan.reviewedSourceHashes[operation.logicalPath];
if (expected == null || expected != operation.contentHash) {
throw ApplyException(
'Reviewed hash metadata disagrees for ${operation.logicalPath}.',
exitCode: ExitCodes.planStaleOrStagingIntegrityFailure,
);
}
final stagedPath = applyPath(stagingRoot, operation.logicalPath);
if (!fileSystem.fileExists(stagedPath)) {
currentNormalizedHashes[operation.logicalPath] = null;
throw ApplyException(
'Missing staged artifact: ${operation.logicalPath}.',
exitCode: ExitCodes.planStaleOrStagingIntegrityFailure,
);
} else {
final bytes = fileSystem.readBytes(stagedPath);
final normalized = hashBytes(bytes);
currentNormalizedHashes[operation.logicalPath] = switch (normalized) {
SuccessfulHash(:final normalizedHash) => normalizedHash,
_ => null,
};
final expectedNormalized =
plan.reviewedNormalizedHashes[operation.logicalPath];
if ((expectedNormalized != null &&
currentNormalizedHashes[operation.logicalPath] !=
expectedNormalized) ||
(expectedNormalized == null && computeRawHash(bytes) != expected)) {
throw ApplyException(
'Changed staged artifact: ${operation.logicalPath}.',
exitCode: ExitCodes.planStaleOrStagingIntegrityFailure,
);
}
}
if (fileSystem.fileExists(
applyPath(canonicalDestination, operation.logicalPath),
)) {
throw ApplyException(
'Conflicting target file: ${operation.logicalPath}.',
exitCode: ExitCodes.planStaleOrStagingIntegrityFailure,
);
}
}
final targetPreconditions = {
'destinationExists': fileSystem.directoryExists(canonicalDestination),
'destinationEmpty': fileSystem.directoryEmpty(canonicalDestination),
};
if (!_mapsEqual(
plan.approvalRecord!.targetPreconditions,
targetPreconditions,
)) {
throw const ApplyException(
'Target preconditions changed after approval.',
exitCode: ExitCodes.planStaleOrStagingIntegrityFailure,
);
}
final assessment = observations == null
? StalenessAssessment(
blockingReasons: const [],
warnings: [
for (final path in unrelatedChangedPaths)
'Unrelated changed path: $path.',
],
)
: evaluateStaleness(
StalenessInputs(
recordedManifestHash: plan.manifestHash,
recordedNormalizedHashes: plan.reviewedNormalizedHashes,
recordedBlueprintHash: plan.blueprintHash,
recordedRecipeVersion: plan.recipeVersion,
recordedEngineVersion: plan.engineVersion,
recordedRelevantPubspecHashes: const {},
currentManifestHash: observations.manifestHash,
currentNormalizedHashes: currentNormalizedHashes,
currentBlueprintHash: observations.blueprintHash,
currentRecipeVersion: observations.recipeVersion,
currentEngineVersion: observations.engineVersion,
currentRelevantPubspecHashes: const {},
unrelatedChangedPaths: unrelatedChangedPaths,
),
);
if (assessment.isStale) {
throw ApplyException(
assessment.blockingReasons.join(' '),
exitCode: ExitCodes.planStaleOrStagingIntegrityFailure,
);
}
return assessment;
}