create-svc 0.1.84 → 0.1.86

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.84",
3
+ "version": "0.1.86",
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",
@@ -71,6 +71,7 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
71
71
  expect(serviceConfig).toContain('"project_mode": "create_new"');
72
72
  expect(serviceConfig).toContain('"quota_project_id": "anmho-infra-prod"');
73
73
  expect(serviceConfig).toContain('"artifact_repository": "cloud-run"');
74
+ expect(serviceConfig).toContain('"worker_min_instances": 0');
74
75
  expect(serviceConfig).not.toContain("cloudbuild.googleapis.com");
75
76
  expect(serviceConfig).toContain('"jwks_url": "https://auth.anmho.com/api/auth/jwks"');
76
77
  expect(serviceConfig).toContain('"git": {');
@@ -97,6 +98,7 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
97
98
  expect(manifest).toContain("${AUTH_AUDIENCE}");
98
99
  expect(manifest).toContain("managed_by: create-service");
99
100
  expect(manifest).toContain("service_id: ${SERVICE_ID}");
101
+ expect(manifest).toContain('autoscaling.knative.dev/minScale: "${SERVICE_MIN_SCALE}"');
100
102
  expect(manifest).not.toContain("CLERK_SECRET_KEY");
101
103
  expect(manifest).not.toContain("STRIPE_SECRET_KEY");
102
104
  expect(manifest).not.toContain("REVENUECAT_API_KEY");
@@ -111,6 +113,8 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
111
113
 
112
114
  const dockerCompose = await Bun.file(join(generatedRoot, "docker-compose.yml")).text();
113
115
  expect(dockerCompose).toContain('image: postgres:16-alpine');
116
+ expect(dockerCompose).toContain("image: temporalio/auto-setup:");
117
+ expect(dockerCompose).toContain("127.0.0.1:7233:7233");
114
118
  expect(dockerCompose).toContain(`127.0.0.1:${localPort}:5432`);
115
119
 
116
120
  const envExample = await Bun.file(join(generatedRoot, ".env.example")).text();
@@ -335,7 +339,7 @@ test("scaffolds a backend package cleanly into a nested monorepo-style directory
335
339
  expect(readme).toContain("`microservice` profile");
336
340
  expect(readme).toContain("api.dns-api.anmho.com");
337
341
  expect(readme).toContain("open Docker Desktop if needed");
338
- expect(readme).toContain("local Postgres service in `docker-compose.yml`");
342
+ expect(readme).toContain("local Postgres and Temporal services in `docker-compose.yml`");
339
343
  expect(readme).toContain("gcloud auth login");
340
344
  expect(readme).toContain("known-good CLIs");
341
345
  expect(readme).toContain("service create");
@@ -0,0 +1,12 @@
1
+ export type AuthctlCommand = {
2
+ path: string;
3
+ runWithBun: boolean;
4
+ };
5
+
6
+ export function authctlSpawnArgs(command: AuthctlCommand, args: string[]) {
7
+ return command.runWithBun ? [bunExecutable(), command.path, ...args] : [command.path, ...args];
8
+ }
9
+
10
+ function bunExecutable() {
11
+ return Bun.which("bun") ?? process.execPath;
12
+ }
@@ -0,0 +1,16 @@
1
+ import { expect, test } from "bun:test";
2
+ import { authctlSpawnArgs } from "./authctl-command";
3
+
4
+ test("authctlSpawnArgs runs repo-local authctl through bun", () => {
5
+ const args = authctlSpawnArgs({ path: "./node_modules/.bin/authctl", runWithBun: true }, ["doctor", "--json"]);
6
+
7
+ expect(args[0]).toEndWith("bun");
8
+ expect(args.slice(1)).toEqual(["./node_modules/.bin/authctl", "doctor", "--json"]);
9
+ });
10
+
11
+ test("authctlSpawnArgs runs global authctl directly", () => {
12
+ expect(authctlSpawnArgs({ path: "/usr/local/bin/authctl", runWithBun: false }, ["version"])).toEqual([
13
+ "/usr/local/bin/authctl",
14
+ "version",
15
+ ]);
16
+ });
@@ -1,4 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
+ import { authctlSpawnArgs, type AuthctlCommand } from "./authctl-command";
2
3
  import { serviceConfig } from "./runtime";
3
4
 
4
5
  type CommandResult = {
@@ -9,6 +10,7 @@ type CommandResult = {
9
10
  };
10
11
 
11
12
  const decoder = new TextDecoder();
13
+ const localAuthctlPath = "./node_modules/.bin/authctl";
12
14
 
13
15
  export type AuthDoctorResult = {
14
16
  hasAuthctl: boolean;
@@ -418,12 +420,12 @@ function resolveResourceServerCommand(): ResourceServerCommand | undefined {
418
420
  }
419
421
 
420
422
  function authctl(args: string[], options: { allowFailure?: boolean; quiet?: boolean } = {}): CommandResult {
421
- const command = authctlPath();
423
+ const command = authctlCommand();
422
424
  if (!command) {
423
425
  throw new Error("authctl is not installed; run bun install in this generated service or link @anmho/authctl");
424
426
  }
425
427
 
426
- const result = Bun.spawnSync([command, ...args], {
428
+ const result = Bun.spawnSync(authctlSpawnArgs(command, args), {
427
429
  cwd: process.cwd(),
428
430
  env: authctlEnvironment(),
429
431
  stdin: "inherit",
@@ -464,8 +466,24 @@ function formatAuthctlFailure(args: string[], output: CommandResult) {
464
466
  return `authctl ${args.join(" ")} failed with exit code ${output.exitCode}\n${detail}`;
465
467
  }
466
468
 
469
+ function authctlCommand(): AuthctlCommand | undefined {
470
+ if (existsSync(localAuthctlPath)) {
471
+ return {
472
+ path: localAuthctlPath,
473
+ runWithBun: true,
474
+ };
475
+ }
476
+ const global = Bun.which("authctl");
477
+ return global
478
+ ? {
479
+ path: global,
480
+ runWithBun: false,
481
+ }
482
+ : undefined;
483
+ }
484
+
467
485
  function authctlPath() {
468
- return existsSync("./node_modules/.bin/authctl") ? "./node_modules/.bin/authctl" : Bun.which("authctl");
486
+ return authctlCommand()?.path;
469
487
  }
470
488
 
471
489
  function authctlEnvironment() {
@@ -14,6 +14,7 @@ export const config = {
14
14
  region: cloudrun.region,
15
15
  artifactRepository: cloudrun.artifact_repository,
16
16
  runtimeServiceAccount: cloudrun.service_account,
17
+ workerMinInstances: Number(cloudrun.worker_min_instances ?? 0),
17
18
  project: {
18
19
  mode: cloudrun.project_mode,
19
20
  id: cloudrun.project_id,
@@ -518,6 +518,7 @@ export async function renderManifest(image: string, target: DeploymentTarget, pr
518
518
  SERVICE_ID: config.serviceName,
519
519
  SERVICE_ROLE: process,
520
520
  SERVICE_INGRESS: process === "worker" ? "internal" : "all",
521
+ SERVICE_MIN_SCALE: process === "worker" ? String(config.workerMinInstances) : "0",
521
522
  CONTAINER_COMMAND: renderContainerCommand(process),
522
523
  RUNTIME_SERVICE_ACCOUNT: config.runtimeServiceAccount,
523
524
  IMAGE_URL: image,
@@ -43,7 +43,7 @@ console to create and deploy.
43
43
 
44
44
  ## Local development
45
45
 
46
- The scaffold writes a ready-to-use `.env.local` and includes a local Postgres service in `docker-compose.yml`.
46
+ The scaffold writes a ready-to-use `.env.local` and includes local Postgres and Temporal services in `docker-compose.yml`.
47
47
 
48
48
  First local run:
49
49
 
@@ -55,8 +55,8 @@ First local run:
55
55
  Local runtime uses:
56
56
 
57
57
  - `DATABASE_URL` from `.env.local`, pointed at Docker Compose Postgres
58
- - `{{COMMAND_MIGRATE}}` and `{{COMMAND_DEV}}`, which open Docker Desktop if needed, wait for Docker readiness, and start Docker Compose Postgres
59
- - `TEMPORAL_ENABLED=true` by default with `localhost:7233` and `default`; set `TEMPORAL_ENABLED=false` when you are not running a local Temporal server
58
+ - `{{COMMAND_MIGRATE}}` and `{{COMMAND_DEV}}`, which open Docker Desktop if needed, wait for Docker readiness, and start Docker Compose Postgres and Temporal
59
+ - `TEMPORAL_ENABLED=true` by default with `localhost:7233` and `default`; `{{COMMAND_DEV}}` waits for the local Temporal container before starting the API and worker
60
60
 
61
61
  No cloud credentials are required for local HTTP development after Docker and Postgres are running.
62
62
 
@@ -78,7 +78,7 @@ creates a trigger, the API service starts `waitlistFollowUpWorkflow` /
78
78
  `WaitlistFollowUpWorkflow` asynchronously on the service task queue. The API
79
79
  request only waits for the trigger record; workflow completion happens through
80
80
  Temporal and is polled by the worker service.
81
- Local `{{COMMAND_DEV}}` starts the API process and the worker process together.
81
+ Local `{{COMMAND_DEV}}` starts the API process and the worker process together after Docker Compose starts the local Temporal server.
82
82
 
83
83
  Production and preview deploys render `TEMPORAL_ENABLED=true` into the Cloud Run
84
84
  manifest unless you override it. For Temporal Cloud, replace the local defaults
@@ -88,6 +88,11 @@ with real connection settings before the worker can run:
88
88
  - `TEMPORAL_NAMESPACE`
89
89
  - any TLS certificate/key or API-key settings used by your worker code
90
90
 
91
+ Cloud Run worker min instances are controlled by
92
+ `cloudrun.worker_min_instances` in `service.jsonc`. The generated default is
93
+ `0`; set it to `1` for production services that must keep a Temporal poller
94
+ warm for scheduled workflows.
95
+
91
96
  If a service does not need Temporal, opt out with:
92
97
 
93
98
  ```bash
@@ -15,5 +15,21 @@ services:
15
15
  timeout: 5s
16
16
  retries: 10
17
17
 
18
+ temporal:
19
+ image: temporalio/auto-setup:1.28.1
20
+ depends_on:
21
+ postgres:
22
+ condition: service_healthy
23
+ environment:
24
+ DB: postgres12
25
+ DB_PORT: 5432
26
+ POSTGRES_USER: {{LOCAL_DATABASE_USER}}
27
+ POSTGRES_PWD: {{LOCAL_DATABASE_PASSWORD}}
28
+ POSTGRES_SEEDS: postgres
29
+ DBNAME: temporal
30
+ VISIBILITY_DBNAME: temporal_visibility
31
+ ports:
32
+ - "127.0.0.1:7233:7233"
33
+
18
34
  volumes:
19
35
  postgres-data:
@@ -16,6 +16,9 @@ const env = {
16
16
  if (env.DATABASE_URL && !env.CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE) {
17
17
  env.CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE = env.DATABASE_URL;
18
18
  }
19
+ if (temporalEnabled(env)) {
20
+ await waitForTemporal(env.TEMPORAL_ADDRESS || "localhost:7233");
21
+ }
19
22
 
20
23
  const api = Bun.spawn(apiCommand, {
21
24
  stdin: "inherit",
@@ -57,3 +60,36 @@ function parseCommands(argv: string[]) {
57
60
  const workerCommand = argv.slice(separator + 1);
58
61
  return { apiCommand, workerCommand: workerCommand.length > 0 ? workerCommand : undefined };
59
62
  }
63
+
64
+ function temporalEnabled(env: Record<string, string | undefined>) {
65
+ return (env.TEMPORAL_ENABLED ?? "true").trim().toLowerCase() !== "false";
66
+ }
67
+
68
+ async function waitForTemporal(address: string) {
69
+ const { host, port } = parseTemporalAddress(address);
70
+ const deadline = Date.now() + 120_000;
71
+
72
+ while (Date.now() < deadline) {
73
+ const exitCode = await Bun.spawn(["nc", "-z", host, String(port)], {
74
+ stdin: "ignore",
75
+ stdout: "ignore",
76
+ stderr: "ignore",
77
+ }).exited;
78
+ if (exitCode === 0) {
79
+ return;
80
+ }
81
+ await Bun.sleep(2_000);
82
+ }
83
+
84
+ throw new Error(`Temporal did not become ready at ${host}:${port} within 120 seconds`);
85
+ }
86
+
87
+ function parseTemporalAddress(address: string) {
88
+ const trimmed = address.trim();
89
+ const withoutScheme = trimmed.includes("://") ? new URL(trimmed).host : trimmed;
90
+ const [host = "localhost", port = "7233"] = withoutScheme.split(":");
91
+ return {
92
+ host: host || "localhost",
93
+ port: Number(port || "7233"),
94
+ };
95
+ }
@@ -1,6 +1,6 @@
1
1
  export async function ensureLocalPostgres() {
2
2
  await ensureDockerRunning();
3
- await run(["docker", "compose", "up", "-d"], { label: "start local postgres" });
3
+ await run(["docker", "compose", "up", "-d"], { label: "start local services" });
4
4
  }
5
5
 
6
6
  async function ensureDockerRunning() {
@@ -107,6 +107,7 @@
107
107
  "region": "{{REGION}}",
108
108
  "artifact_repository": "cloud-run",
109
109
  "service_account": "{{RUNTIME_SERVICE_ACCOUNT}}",
110
+ "worker_min_instances": 0,
110
111
  "required_apis": [
111
112
  "run.googleapis.com",
112
113
  "artifactregistry.googleapis.com",
@@ -15,6 +15,8 @@ spec:
15
15
  managed_by: create-service
16
16
  service_id: ${SERVICE_ID}
17
17
  service_role: ${SERVICE_ROLE}
18
+ annotations:
19
+ autoscaling.knative.dev/minScale: "${SERVICE_MIN_SCALE}"
18
20
  spec:
19
21
  serviceAccountName: ${RUNTIME_SERVICE_ACCOUNT}
20
22
  containers: