create-svc 0.1.39 → 0.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-svc",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "description": "Local microservice bootstrap CLI for Cloud Run and Workers services with Neon-backed data.",
5
5
  "module": "index.ts",
6
6
  "type": "module",
@@ -70,6 +70,8 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
70
70
  expect(serviceConfig).toContain('"api_key_secret_name": "dns-api-temporal-api-key"');
71
71
  expect(serviceConfig).toContain('"project_mode": "create_new"');
72
72
  expect(serviceConfig).toContain('"quota_project_id": "anmho-infra-prod"');
73
+ expect(serviceConfig).toContain('"artifact_repository": "cloud-run"');
74
+ expect(serviceConfig).not.toContain("cloudbuild.googleapis.com");
73
75
  expect(serviceConfig).toContain('"jwks_url": "https://auth.anmho.com/api/auth/jwks"');
74
76
  expect(serviceConfig).toContain('"git": {');
75
77
  expect(serviceConfig).toContain('"repository": "dns-api"');
@@ -0,0 +1,90 @@
1
+ export type DeployArgs = {
2
+ build: "local" | "cloudbuild";
3
+ ci: boolean;
4
+ destroy: boolean;
5
+ environment: "main" | "preview" | "personal";
6
+ name?: string;
7
+ };
8
+
9
+ export function parseDeployArgs(argv: string[]): DeployArgs {
10
+ const parsed: DeployArgs = {
11
+ build: parseBuildStrategy(process.env.SERVICE_BUILD_STRATEGY || process.env.SERVICE_BUILD),
12
+ ci: false,
13
+ destroy: false,
14
+ environment: "main",
15
+ };
16
+
17
+ for (let i = 0; i < argv.length; i += 1) {
18
+ const token = argv[i];
19
+ if (!token) {
20
+ continue;
21
+ }
22
+
23
+ const next = argv[i + 1];
24
+ const readValue = () => {
25
+ if (!next || next.startsWith("-")) {
26
+ throw new Error(`Missing value for ${token}`);
27
+ }
28
+ i += 1;
29
+ return next;
30
+ };
31
+
32
+ if (token === "--ci") {
33
+ parsed.ci = true;
34
+ continue;
35
+ }
36
+
37
+ if (token === "--destroy") {
38
+ parsed.destroy = true;
39
+ continue;
40
+ }
41
+
42
+ if (token === "--build") {
43
+ parsed.build = parseBuildStrategy(readValue());
44
+ continue;
45
+ }
46
+
47
+ if (token.startsWith("--build=")) {
48
+ parsed.build = parseBuildStrategy(token.slice("--build=".length));
49
+ continue;
50
+ }
51
+
52
+ if (token === "--cloud-build") {
53
+ parsed.build = "cloudbuild";
54
+ continue;
55
+ }
56
+
57
+ if (token === "--environment") {
58
+ parsed.environment = readValue() as DeployArgs["environment"];
59
+ continue;
60
+ }
61
+
62
+ if (token.startsWith("--environment=")) {
63
+ parsed.environment = token.slice("--environment=".length) as DeployArgs["environment"];
64
+ continue;
65
+ }
66
+
67
+ if (token === "--name") {
68
+ parsed.name = readValue();
69
+ continue;
70
+ }
71
+
72
+ if (token.startsWith("--name=")) {
73
+ parsed.name = token.slice("--name=".length);
74
+ continue;
75
+ }
76
+ }
77
+
78
+ return parsed;
79
+ }
80
+
81
+ function parseBuildStrategy(value: string | undefined): DeployArgs["build"] {
82
+ if (!value || value === "local") {
83
+ return "local";
84
+ }
85
+ if (value === "cloudbuild" || value === "cloud-build") {
86
+ return "cloudbuild";
87
+ }
88
+ throw new Error(`Unknown build strategy: ${value}`);
89
+ }
90
+
@@ -4,6 +4,7 @@ import { deleteBranch, ensureBranch, ensureDatabase, getConnectionUri, listBranc
4
4
  import {
5
5
  addSecretVersion,
6
6
  deleteService,
7
+ dockerStreaming,
7
8
  ensureArtifactRepository,
8
9
  ensureProductionDomainMapping,
9
10
  ensureSecretAccessor,
@@ -74,9 +75,18 @@ export async function deploy(args = Bun.argv.slice(2), deployOptions: DeployOpti
74
75
  });
75
76
  }
76
77
  const image = imageUrl();
77
- await runStep("Building container image in Cloud Build", () =>
78
- gcloudStreaming(["builds", "submit", "--project", config.project.id, "--region", config.region, "--tag", image])
79
- );
78
+ if (options.build === "cloudbuild") {
79
+ await runStep("Building container image in Cloud Build", () =>
80
+ gcloudStreaming(["builds", "submit", "--project", config.project.id, "--region", config.region, "--tag", image])
81
+ );
82
+ } else {
83
+ requireCommand("docker");
84
+ await runStep("Authenticating Docker to Artifact Registry", () =>
85
+ gcloud(["auth", "configure-docker", `${config.region}-docker.pkg.dev`, "--quiet"])
86
+ );
87
+ await runStep("Building container image locally", () => dockerStreaming(["build", "-t", image, "."]));
88
+ await runStep("Pushing container image to Artifact Registry", () => dockerStreaming(["push", image]));
89
+ }
80
90
 
81
91
  const renderedManifestPath = await runStep("Rendering Cloud Run manifest", () => writeRenderedManifest(image, target));
82
92
 
@@ -0,0 +1,29 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { parseDeployArgs } from "./deploy-args";
3
+
4
+ const originalBuild = process.env.SERVICE_BUILD;
5
+ const originalBuildStrategy = process.env.SERVICE_BUILD_STRATEGY;
6
+
7
+ afterEach(() => {
8
+ process.env.SERVICE_BUILD = originalBuild;
9
+ process.env.SERVICE_BUILD_STRATEGY = originalBuildStrategy;
10
+ });
11
+
12
+ test("parseDeployArgs defaults to local image builds", () => {
13
+ delete process.env.SERVICE_BUILD;
14
+ delete process.env.SERVICE_BUILD_STRATEGY;
15
+
16
+ expect(parseDeployArgs([]).build).toBe("local");
17
+ });
18
+
19
+ test("parseDeployArgs accepts explicit Cloud Build fallback", () => {
20
+ expect(parseDeployArgs(["--build", "cloudbuild"]).build).toBe("cloudbuild");
21
+ expect(parseDeployArgs(["--build=cloud-build"]).build).toBe("cloudbuild");
22
+ expect(parseDeployArgs(["--cloud-build"]).build).toBe("cloudbuild");
23
+ });
24
+
25
+ test("parseDeployArgs accepts build strategy from env", () => {
26
+ process.env.SERVICE_BUILD_STRATEGY = "cloudbuild";
27
+
28
+ expect(parseDeployArgs([]).build).toBe("cloudbuild");
29
+ });
@@ -2,6 +2,7 @@ import { intro, log, outro, spinner } from "@clack/prompts";
2
2
  import { join } from "node:path";
3
3
  import { config } from "./config";
4
4
  import { serviceRoot } from "../runtime";
5
+ import { parseDeployArgs, type DeployArgs } from "./deploy-args";
5
6
 
6
7
  type CommandOptions = {
7
8
  allowFailure?: boolean;
@@ -9,13 +10,6 @@ type CommandOptions = {
9
10
  env?: Record<string, string | undefined>;
10
11
  };
11
12
 
12
- type DeployArgs = {
13
- ci: boolean;
14
- destroy: boolean;
15
- environment: "main" | "preview" | "personal";
16
- name?: string;
17
- };
18
-
19
13
  type CleanupArgs = {
20
14
  destroyProject: boolean;
21
15
  force: boolean;
@@ -125,6 +119,10 @@ export async function gcloudStreaming(args: string[], options: CommandOptions =
125
119
  return runStreaming("gcloud", normalized, options);
126
120
  }
127
121
 
122
+ export async function dockerStreaming(args: string[], options: CommandOptions = {}) {
123
+ return runStreaming("docker", args, options);
124
+ }
125
+
128
126
  export async function runStreaming(command: string, args: string[], options: CommandOptions = {}): Promise<CommandResult> {
129
127
  const child = Bun.spawn([command, ...args], {
130
128
  cwd: process.cwd(),
@@ -454,61 +452,7 @@ export function imageUrl(tag = imageTag()) {
454
452
  return `${artifactImageBase()}:${tag}`;
455
453
  }
456
454
 
457
- export function parseDeployArgs(argv: string[]): DeployArgs {
458
- const parsed: DeployArgs = {
459
- ci: false,
460
- destroy: false,
461
- environment: "main",
462
- };
463
-
464
- for (let i = 0; i < argv.length; i += 1) {
465
- const token = argv[i];
466
- if (!token) {
467
- continue;
468
- }
469
-
470
- const next = argv[i + 1];
471
- const readValue = () => {
472
- if (!next || next.startsWith("-")) {
473
- throw new Error(`Missing value for ${token}`);
474
- }
475
- i += 1;
476
- return next;
477
- };
478
-
479
- if (token === "--ci") {
480
- parsed.ci = true;
481
- continue;
482
- }
483
-
484
- if (token === "--destroy") {
485
- parsed.destroy = true;
486
- continue;
487
- }
488
-
489
- if (token === "--environment") {
490
- parsed.environment = readValue() as DeployArgs["environment"];
491
- continue;
492
- }
493
-
494
- if (token.startsWith("--environment=")) {
495
- parsed.environment = token.slice("--environment=".length) as DeployArgs["environment"];
496
- continue;
497
- }
498
-
499
- if (token === "--name") {
500
- parsed.name = readValue();
501
- continue;
502
- }
503
-
504
- if (token.startsWith("--name=")) {
505
- parsed.name = token.slice("--name=".length);
506
- continue;
507
- }
508
- }
509
-
510
- return parsed;
511
- }
455
+ export { parseDeployArgs };
512
456
 
513
457
  export function parseCleanupArgs(argv: string[]): CleanupArgs {
514
458
  const parsed: CleanupArgs = {
@@ -66,10 +66,14 @@ Create, deploy, and destroy use:
66
66
 
67
67
  - known-good CLIs first, especially `gcloud`
68
68
  - `gcloud`
69
+ - Docker for local image builds and Artifact Registry pushes
69
70
  - `NEON_API_KEY`, or a working Vault login via `VAULT_ADDR` plus `VAULT_TOKEN`, `VAULT_TOKEN_FILE`, or `~/.vault-token`
70
71
  - the repo-aware `service` CLI from this package
71
72
 
72
73
  Local provisioning intentionally prefers known-good CLIs over SDKs for Google Cloud operations.
74
+ Cloud Run deploys build and push the container image locally by default. Use
75
+ `service deploy --build cloudbuild` only when you explicitly want Google Cloud
76
+ Build to build the image remotely.
73
77
 
74
78
  Authenticate `gcloud` on the machine before running provisioning commands:
75
79
 
@@ -97,7 +97,6 @@
97
97
  "service_account": "{{RUNTIME_SERVICE_ACCOUNT}}",
98
98
  "required_apis": [
99
99
  "run.googleapis.com",
100
- "cloudbuild.googleapis.com",
101
100
  "artifactregistry.googleapis.com",
102
101
  "iam.googleapis.com",
103
102
  "iamcredentials.googleapis.com",