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 +1 -1
- package/src/scaffold.test.ts +5 -1
- package/src/service-runtime/authctl-command.ts +12 -0
- package/src/service-runtime/authctl.test.ts +16 -0
- package/src/service-runtime/authctl.ts +21 -3
- package/src/service-runtime/cloudrun/config.ts +1 -0
- package/src/service-runtime/cloudrun/lib.ts +1 -0
- package/templates/shared/README.md +9 -4
- package/templates/shared/docker-compose.yml +16 -0
- package/templates/shared/scripts/dev.ts +36 -0
- package/templates/shared/scripts/local-docker.ts +1 -1
- package/templates/shared/service.jsonc +1 -0
- package/templates/shared/service.yaml +2 -0
package/package.json
CHANGED
package/src/scaffold.test.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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`;
|
|
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
|
|
3
|
+
await run(["docker", "compose", "up", "-d"], { label: "start local services" });
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
async function ensureDockerRunning() {
|