applyPreflight function

StalenessAssessment applyPreflight({
  1. required ChangePlan plan,
  2. required String stagingRoot,
  3. required String canonicalDestination,
  4. ApplyFileSystem fileSystem = const ApplyFileSystem(),
  5. ApplyObservations? observations,
  6. List<String> unrelatedChangedPaths = const [],
})

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;
}