runAll method
- StepConfirm? confirm,
- StepListener? onStep,
- StepFailureHandler? onFailure,
- bool interactive = true,
- bool failFast = true,
Run every applicable sub-step in order and return an aggregate report.
Steps are skipped when the confirm callback returns false, when the
project configuration disables the relevant feature (e.g. Cloud Run
off), or when a soft prerequisite is unmet (Spark plan + Cloud Run).
Fail-fast contract: when failFast is true (the default for the
wizard) and a step fails, the orchestrator stops and asks the
onFailure handler what to do. The handler can FailureAction.retry
the same step, FailureAction.skip it and continue, or
FailureAction.abort the whole run. If onFailure is null, the
orchestrator behaves as if every failure returned FailureAction.abort.
When failFast is false (the legacy / non-interactive CLI mode),
failed steps are recorded and the run continues so the user can see
the full picture.
Implementation
Future<OrchestratorReport> runAll({
StepConfirm? confirm,
StepListener? onStep,
StepFailureHandler? onFailure,
bool interactive = true,
bool failFast = true,
}) async {
if (config.firebaseProjectId == null || config.firebaseProjectId!.isEmpty) {
return OrchestratorReport(
results: <SetupStepResult>[
SetupStepResult.failed(
WizardSubStep.firebaseLogin,
message: 'No Firebase project ID configured',
fixHint: 'Set FIREBASE_PROJECT_ID in config/setup_config.env',
),
],
aborted: true,
);
}
final List<SetupStepResult> results = <SetupStepResult>[];
String? releaseUrl;
String? betaUrl;
BlazeStatus? blazeStatus;
String? storageBucketName;
String? firestoreRegion;
bool aborted = false;
/// Run a single step with optional confirm gate, onStep listener,
/// and (when failFast is true) retry/skip/abort handling on failure.
/// Returns the final outcome.
Future<SetupStepResult> runStep(
WizardSubStep step,
Future<SetupStepResult> Function() body, {
bool defaultRun = true,
}) async {
// Once aborted, every subsequent call returns a "skipped due to abort"
// marker without executing or prompting.
if (aborted) {
final SetupStepResult abortMarker = SetupStepResult.skipped(
step,
message: 'Run aborted by user — fix and re-run.',
fixHint: _fixHintFor(step),
);
results.add(abortMarker);
if (onStep != null) {
await onStep(abortMarker);
}
return abortMarker;
}
bool shouldRun = defaultRun;
if (confirm != null) {
shouldRun = await confirm(step);
}
if (!shouldRun) {
final SetupStepResult skipped = SetupStepResult.skipped(
step,
message: 'User declined this step',
fixHint: _fixHintFor(step),
);
results.add(skipped);
if (onStep != null) {
await onStep(skipped);
}
return skipped;
}
// Execute (with retry loop when fail-fast is on).
int attempt = 1;
SetupStepResult outcome;
while (true) {
outcome = await body();
if (!outcome.failed) {
break;
}
if (!failFast) {
// Legacy mode: keep going regardless.
break;
}
// failFast=true → ask handler what to do.
final FailureAction action = onFailure != null
? await onFailure(outcome, attempt: attempt)
: FailureAction.abort;
if (action == FailureAction.retry) {
attempt++;
continue;
}
if (action == FailureAction.skip) {
outcome = SetupStepResult.skipped(
step,
message: outcome.message.isEmpty
? 'User skipped after failure'
: 'Skipped after failure: ${outcome.message}',
fixHint: outcome.fixHint,
);
break;
}
// abort
aborted = true;
break;
}
results.add(outcome);
if (onStep != null) {
await onStep(outcome);
}
return outcome;
}
// ── Step 5.1 – 5.3: authentication + billing ───────────────────────────
await runStep(WizardSubStep.firebaseLogin, runFirebaseLogin);
if (aborted) {
return _buildReport(
results,
releaseUrl,
betaUrl,
blazeStatus,
storageBucketName,
firestoreRegion,
aborted,
);
}
if (config.setupCloudRun || config.createServer) {
await runStep(WizardSubStep.gcloudLogin, runGcloudLogin);
if (aborted) {
return _buildReport(
results,
releaseUrl,
betaUrl,
blazeStatus,
storageBucketName,
firestoreRegion,
aborted,
);
}
}
if (config.requireBlaze || config.setupCloudRun || config.createServer) {
final SetupStepResult r = await runStep(
WizardSubStep.billingCheck,
() => runBillingCheck(interactive: interactive),
);
blazeStatus = r.success ? BlazeStatus.enabled : BlazeStatus.notEnabled;
if (aborted) {
return _buildReport(
results,
releaseUrl,
betaUrl,
blazeStatus,
storageBucketName,
firestoreRegion,
aborted,
);
}
}
// ── Step 5.4: enable required Firebase APIs upfront ────────────────────
// We do this BEFORE anything that hits firestore / storage / firebase
// management endpoints so those calls don't fail with SERVICE_DISABLED.
await runStep(
WizardSubStep.enableFirebaseApis,
() => runEnableFirebaseApis(interactive: interactive),
);
if (aborted) {
return _buildReport(
results,
releaseUrl,
betaUrl,
blazeStatus,
storageBucketName,
firestoreRegion,
aborted,
);
}
// ── Step 5.5: client wiring (FlutterFire OR Jaspr JS SDK) ──────────────
await runStep(WizardSubStep.configureClient, runConfigureClient);
if (aborted) {
return _buildReport(
results,
releaseUrl,
betaUrl,
blazeStatus,
storageBucketName,
firestoreRegion,
aborted,
);
}
// ── Step 5.6 – 5.7: bootstrap Firestore + Storage ──────────────────────
if (config.initializeFirestore) {
final SetupStepResult r = await runStep(
WizardSubStep.initFirestore,
runFirestoreInit,
);
if (r.success) {
firestoreRegion = config.firestoreRegion;
}
if (aborted) {
return _buildReport(
results,
releaseUrl,
betaUrl,
blazeStatus,
storageBucketName,
firestoreRegion,
aborted,
);
}
}
if (config.initializeStorage) {
final SetupStepResult r = await runStep(
WizardSubStep.initStorage,
runStorageInit,
);
if (r.success) {
storageBucketName = '${config.firebaseProjectId}.appspot.com';
}
if (aborted) {
return _buildReport(
results,
releaseUrl,
betaUrl,
blazeStatus,
storageBucketName,
firestoreRegion,
aborted,
);
}
}
// ── Step 5.8: auth providers (always console hand-off) ─────────────────
if (config.enableEmailAuth || config.enableGoogleAuth) {
await runStep(
WizardSubStep.enableAuthProviders,
() => runEnableAuthProviders(interactive: interactive),
);
if (aborted) {
return _buildReport(
results,
releaseUrl,
betaUrl,
blazeStatus,
storageBucketName,
firestoreRegion,
aborted,
);
}
}
// ── Step 5.9: rules deploy (Firestore + Storage) ───────────────────────
await runStep(WizardSubStep.deployFirestoreRules, runDeployFirestoreRules);
if (aborted) {
return _buildReport(
results,
releaseUrl,
betaUrl,
blazeStatus,
storageBucketName,
firestoreRegion,
aborted,
);
}
await runStep(WizardSubStep.deployStorageRules, runDeployStorageRules);
if (aborted) {
return _buildReport(
results,
releaseUrl,
betaUrl,
blazeStatus,
storageBucketName,
firestoreRegion,
aborted,
);
}
// ── Step 5.10 – 5.13: web build + hosting deploys (skip when no web) ───
final bool webEnabled = SetupGuidance.supportsWebHosting(config);
if (webEnabled) {
final SetupStepResult build = await runStep(
WizardSubStep.buildWeb,
runBuildWeb,
);
if (build.success) {
if (config.deployHostingRelease || config.deployHostingBeta) {
await runStep(WizardSubStep.hostingInit, runHostingInit);
if (aborted) {
return _buildReport(
results,
releaseUrl,
betaUrl,
blazeStatus,
storageBucketName,
firestoreRegion,
aborted,
);
}
}
if (config.deployHostingRelease) {
final SetupStepResult r = await runStep(
WizardSubStep.deployHostingRelease,
runDeployHostingRelease,
);
if (r.success) {
releaseUrl = SetupGuidance.releaseHostingUrl(
config.firebaseProjectId!,
);
}
if (aborted) {
return _buildReport(
results,
releaseUrl,
betaUrl,
blazeStatus,
storageBucketName,
firestoreRegion,
aborted,
);
}
}
if (config.deployHostingBeta) {
final SetupStepResult r = await runStep(
WizardSubStep.deployHostingBeta,
runDeployHostingBeta,
);
if (r.success) {
betaUrl = SetupGuidance.betaHostingUrl(config.firebaseProjectId!);
}
if (aborted) {
return _buildReport(
results,
releaseUrl,
betaUrl,
blazeStatus,
storageBucketName,
firestoreRegion,
aborted,
);
}
}
} else {
// build failed → record skip results for the dependent hosting steps
if (config.deployHostingRelease) {
results.add(
SetupStepResult.skipped(
WizardSubStep.deployHostingRelease,
message: 'Web build failed — hosting deploy skipped',
fixHint: _fixHintFor(WizardSubStep.deployHostingRelease),
),
);
}
if (config.deployHostingBeta) {
results.add(
SetupStepResult.skipped(
WizardSubStep.deployHostingBeta,
message: 'Web build failed — beta hosting deploy skipped',
fixHint: _fixHintFor(WizardSubStep.deployHostingBeta),
),
);
}
}
}
// ── Step 6.x: server / Cloud Run + cleanup (Blaze required) ────────────
final bool runServerSteps = config.setupCloudRun || config.createServer;
final bool blazeOk =
blazeStatus == BlazeStatus.enabled || blazeStatus == null;
if (runServerSteps && blazeOk) {
await runStep(WizardSubStep.enableServerApis, runEnableServerApis);
if (!aborted) {
await runStep(
WizardSubStep.ensureArtifactRegistryRepo,
runEnsureArtifactRegistryRepo,
);
}
if (!aborted && config.setupArtifactCleanup) {
await runStep(
WizardSubStep.applyArtifactCleanupPolicy,
runApplyArtifactCleanupPolicy,
);
}
if (!aborted && config.cloudRunKeepRevisions > 0) {
await runStep(
WizardSubStep.capCloudRunRevisions,
runCapCloudRunRevisions,
);
}
} else if (runServerSteps && !blazeOk) {
// Record explicit skips so the post-run summary can list them.
results.add(
SetupStepResult.skipped(
WizardSubStep.enableServerApis,
message: 'Project is on Spark — Cloud Run is unavailable.',
fixHint: _fixHintFor(WizardSubStep.enableServerApis),
),
);
results.add(
SetupStepResult.skipped(
WizardSubStep.ensureArtifactRegistryRepo,
message: 'Spark plan — Artifact Registry cleanup unavailable.',
fixHint: _fixHintFor(WizardSubStep.ensureArtifactRegistryRepo),
),
);
results.add(
SetupStepResult.skipped(
WizardSubStep.applyArtifactCleanupPolicy,
message: 'Spark plan — cleanup policy not applied.',
fixHint: _fixHintFor(WizardSubStep.applyArtifactCleanupPolicy),
),
);
results.add(
SetupStepResult.skipped(
WizardSubStep.capCloudRunRevisions,
message: 'Spark plan — Cloud Run revision cap not applied.',
fixHint: _fixHintFor(WizardSubStep.capCloudRunRevisions),
),
);
}
return _buildReport(
results,
releaseUrl,
betaUrl,
blazeStatus,
storageBucketName,
firestoreRegion,
aborted,
);
}