deploy method
End-to-end: build, push, and deploy the Jaspr server to Cloud Run.
Returns a JasprServerDeployResult describing the outcome. Never throws on shell failures — every step short-circuits with a descriptive JasprServerDeployResult.message so the CLI can render a clean summary.
Implementation
Future<JasprServerDeployResult> deploy({
String repository = 'oracular',
String region = 'us-central1',
}) async {
// ── Gate: only run when the project produces a Jaspr server image.
if (!config.hasJasprServer) {
return JasprServerDeployResult.failure(
'Render mode ${config.jasprRenderMode.displayName} does not '
'produce a Cloud Run image — nothing to deploy.',
);
}
final String? projectId = config.firebaseProjectId;
if (projectId == null || projectId.trim().isEmpty) {
return JasprServerDeployResult.failure(
'No Firebase / GCP project ID configured. Set '
'FIREBASE_PROJECT_ID in config/setup_config.env and retry.',
);
}
// The Jaspr host must exist on disk — without it there's nothing to
// build. The Dockerfile.jaspr check is delegated to
// BuildOrchestrator.buildJasprServerImage.
if (!Directory(_jasprAppPath).existsSync()) {
return JasprServerDeployResult.failure(
'Jaspr project not found at $_jasprAppPath. Re-scaffold with '
'`oracular create` to generate it.',
);
}
// ── 1. Preflight (active account / APIs / AR repo) ────────────────
if (!await _preflight.runAll(
projectId: projectId,
repository: repository,
region: region,
)) {
return JasprServerDeployResult.failure(
'Cloud Run preflight failed. See errors above.',
);
}
// ── 2. Build via BuildOrchestrator ────────────────────────────────
// Routes through the orchestrator so the `oracular build
// jaspr-server` CLI entry point and this deploy hit identical
// docker-build invocations. The orchestrator handles git-SHA
// tagging and Dockerfile.jaspr discovery.
info('Building Jaspr Cloud Run image for $serviceName...');
final BuildStepResult build = await _builder.buildJasprServerImage();
if (build.status != BuildStepStatus.success) {
return JasprServerDeployResult.failure(
'docker build failed: ${build.message}',
);
}
// ── 3. Push every tag we built (`:latest` + optional `:<sha>`) ────
// The orchestrator records the `:latest` tag in `outputPath` but the
// git-sha tag is implicit; we re-derive both here.
final String latestTag =
imageTag(projectId: projectId, region: region, repository: repository);
info('Pushing image to Artifact Registry...');
final ProcessResult? push = await _runner.runWithRetry(
'docker',
<String>['push', latestTag],
workingDirectory: _jasprAppPath,
operationName: 'docker push (Jaspr server)',
);
if (push == null || !push.success) {
return JasprServerDeployResult.failure(
'docker push failed. Verify the Artifact Registry repository '
'`$repository` exists in $region and that you are authenticated '
'(`gcloud auth configure-docker $region-docker.pkg.dev`).',
);
}
// Best-effort: push the git-sha tag if BuildOrchestrator added one.
// `docker push` with the per-image base tag pushes *that tag*, so we
// explicitly ask for the SHA-tagged variant when we can read git.
final String? gitSha = await _readGitSha(_jasprAppPath);
if (gitSha != null && gitSha.isNotEmpty) {
final String shaTag =
'$region-docker.pkg.dev/$projectId/$repository/$serviceName:$gitSha';
info('Pushing image SHA tag $shaTag...');
final ProcessResult? pushSha = await _runner.runWithRetry(
'docker',
<String>['push', shaTag],
workingDirectory: _jasprAppPath,
operationName: 'docker push (Jaspr server SHA tag)',
);
if (pushSha == null || !pushSha.success) {
// Non-fatal: the `:latest` push already succeeded.
warn('Could not push SHA tag $shaTag '
'(continuing — :latest is in place).');
}
}
// ── 4. gcloud run deploy ──────────────────────────────────────────
info('Deploying $serviceName to Cloud Run ($region)...');
final ProcessResult? deployResult = await _runner.runWithRetry(
'gcloud',
<String>[
'run',
'deploy',
serviceName,
'--image=$latestTag',
'--platform=managed',
'--region=$region',
'--project=$projectId',
'--allow-unauthenticated',
'--port=8080',
'--memory=512Mi',
'--cpu=1',
'--min-instances=0',
'--max-instances=10',
],
operationName: 'gcloud run deploy (Jaspr server)',
);
if (deployResult == null || !deployResult.success) {
return JasprServerDeployResult.failure(
'gcloud run deploy failed for service $serviceName.',
);
}
// Cloud Run service URLs embed a per-project hash; ask gcloud rather
// than guessing.
String url = 'https://$serviceName-$projectId.$region.run.app';
final ProcessResult describe = await _runner.run(
'gcloud',
<String>[
'run',
'services',
'describe',
serviceName,
'--region=$region',
'--project=$projectId',
'--format=value(status.url)',
],
);
final String describedUrl = describe.stdout.trim();
if (describe.success && describedUrl.isNotEmpty) {
url = describedUrl;
}
success('Jaspr server deployed to Cloud Run.');
info(' Service: $serviceName');
info(' URL: $url');
return JasprServerDeployResult(
success: true,
serviceUrl: url,
imageTag: latestTag,
serviceName: serviceName,
region: region,
);
}