deploy method

Future<JasprServerDeployResult> deploy({
  1. String repository = 'oracular',
  2. String region = 'us-central1',
})

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