runHook static method

Future<HookRunResult> runHook({
  1. required String hookName,
  2. required HooksConfigModel hooksConfig,
  3. String? configPath,
  4. bool exitOnFailure = true,
})

Executes all steps of the hook named hookName.

Called both manually via frx hooks run <name> and automatically when git triggers the hook script.

Returns a HookRunResult and exits with code 1 if the hook fails and stopOnFailure is true — this is what causes git to abort the commit.

Implementation

static Future<HookRunResult> runHook({
  required String hookName,
  required HooksConfigModel hooksConfig,
  String? configPath,
  bool exitOnFailure = true,
}) async {
  final hook = hooksConfig.hooks[hookName];

  if (hook == null) {
    print('❌ Hook "$hookName" is not configured in config.yaml.');
    print('   Available hooks: ${hooksConfig.hooks.keys.join(', ')}');
    if (exitOnFailure) exit(1);
    return HookRunResult(
      hookName: hookName,
      success: false,
      totalDuration: Duration.zero,
    );
  }

  if (!hook.enabled) {
    print('ℹ️  Hook "$hookName" is disabled (enabled: false). Skipping.');
    return HookRunResult(
      hookName: hookName,
      success: true,
      totalDuration: Duration.zero,
    );
  }

  final totalStopwatch = Stopwatch()..start();

  print('');
  _printHookBanner(hookName, hook);

  final stepResults = <HookStepResult>[];
  bool overallSuccess = true;

  // ── Option A: delegate to a named FRX pipeline ───────────────────────────
  if (hook.hasPipeline) {
    print('   🔗 Delegating to FRX pipeline: "${hook.runPipeline}"');
    print('');
    try {
      await FlutterReleaseXHelpers.executePipeline(
          pipelineName: hook.runPipeline);
    } catch (e) {
      print('❌ Pipeline "${hook.runPipeline}" failed: $e');
      overallSuccess = false;
      if (hook.stopOnFailure && exitOnFailure) {
        _printSummary(hookName, stepResults, overallSuccess,
            totalStopwatch.elapsed, hook.runPipeline);
        exit(1);
      }
    }

    totalStopwatch.stop();
    _printSummary(hookName, stepResults, overallSuccess,
        totalStopwatch.elapsed, hook.runPipeline);

    return HookRunResult(
      hookName: hookName,
      success: overallSuccess,
      stepResults: stepResults,
      totalDuration: totalStopwatch.elapsed,
      pipelineRan: hook.runPipeline,
    );
  }

  // ── Option B: run inline steps ────────────────────────────────────────────
  if (!hook.hasSteps) {
    print(
        '⚠️  Hook "$hookName" has no steps and no run_pipeline. Nothing to run.');
    totalStopwatch.stop();
    return HookRunResult(
      hookName: hookName,
      success: true,
      totalDuration: totalStopwatch.elapsed,
    );
  }

  for (int i = 0; i < hook.steps.length; i++) {
    final step = hook.steps[i];
    final stepStopwatch = Stopwatch()..start();

    print('   [${i + 1}/${hook.steps.length}] ▶  ${step.name}');
    if (step.description != null) {
      print('         ${step.description}');
    }

    // Validate working directory
    if (step.workingDirectory != null) {
      if (!Directory(step.workingDirectory!).existsSync()) {
        print(
            '   ❌ working_directory "${step.workingDirectory}" does not exist. Aborting step.');
        stepStopwatch.stop();
        final res = HookStepResult(
          stepName: step.name,
          passed: false,
          duration: stepStopwatch.elapsed,
          note: 'working_directory not found',
        );
        stepResults.add(res);
        if (!step.allowFailure && hook.stopOnFailure) {
          overallSuccess = false;
          if (exitOnFailure) {
            _printSummary(
                hookName, stepResults, false, totalStopwatch.elapsed, null);
            exit(1);
          }
        }
        continue;
      }
    }

    final result = await FlutterReleaseXHelpers.executeCommand(
      step.command,
      env: step.env,
      workingDirectory: step.workingDirectory,
      timeoutSeconds: step.timeout,
    );

    stepStopwatch.stop();
    final elapsed = stepStopwatch.elapsed;

    if (result.exitCode == 0) {
      print('   ✅ ${step.name} — passed (${_formatDuration(elapsed)})');
      stepResults.add(HookStepResult(
        stepName: step.name,
        passed: true,
        duration: elapsed,
      ));
    } else if (step.allowFailure) {
      print('   ⚠️  ${step.name} — failed (allow_failure, continuing)');
      stepResults.add(HookStepResult(
        stepName: step.name,
        passed: false,
        warned: true,
        duration: elapsed,
        note: 'allowed failure',
      ));
    } else {
      print(
          '   ❌ ${step.name} — FAILED (exit code ${result.exitCode}) (${_formatDuration(elapsed)})');
      overallSuccess = false;
      stepResults.add(HookStepResult(
        stepName: step.name,
        passed: false,
        duration: elapsed,
      ));
      if (hook.stopOnFailure) {
        _printSummary(
            hookName, stepResults, false, totalStopwatch.elapsed, null);
        if (exitOnFailure) exit(1);
        break;
      }
    }
  }

  totalStopwatch.stop();
  _printSummary(
      hookName, stepResults, overallSuccess, totalStopwatch.elapsed, null);

  if (!overallSuccess && exitOnFailure) exit(1);

  return HookRunResult(
    hookName: hookName,
    success: overallSuccess,
    stepResults: stepResults,
    totalDuration: totalStopwatch.elapsed,
  );
}