create-svc 0.1.23 → 0.1.25
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 +3 -1
- package/src/post-scaffold.test.ts +10 -10
- package/src/post-scaffold.ts +4 -9
- package/src/scaffold.test.ts +46 -101
- package/src/scaffold.ts +10 -3
- package/{templates/shared/scripts → src/service-runtime}/authctl.ts +3 -3
- package/{templates/shared/scripts → src/service-runtime}/cloudrun/cleanup.ts +1 -1
- package/{templates/shared/scripts → src/service-runtime}/cloudrun/cli.ts +1 -1
- package/src/service-runtime/cloudrun/config.ts +55 -0
- package/src/service-runtime/runtime.ts +8 -0
- package/{templates/targets/workers/scripts → src/service-runtime}/workers/cli.ts +7 -6
- package/src/service.test.ts +0 -2
- package/src/service.ts +13 -19
- package/templates/shared/README.md +1 -1
- package/templates/shared/service.config.ts +31 -0
- package/templates/targets/workers/Makefile +1 -1
- package/templates/targets/workers/package.json +8 -11
- package/templates/variants/bun-connectrpc/Makefile +1 -1
- package/templates/variants/bun-connectrpc/package.json +7 -10
- package/templates/variants/bun-hono/Makefile +1 -1
- package/templates/variants/bun-hono/package.json +7 -10
- package/templates/variants/go-chi/Dockerfile +1 -1
- package/templates/variants/go-chi/Makefile +1 -1
- package/templates/variants/go-chi/go.mod +30 -1
- package/templates/variants/go-chi/go.sum +146 -0
- package/templates/variants/go-chi/package.json +7 -10
- package/templates/variants/go-connectrpc/Dockerfile +1 -1
- package/templates/variants/go-connectrpc/Makefile +1 -1
- package/templates/variants/go-connectrpc/go.mod +30 -2
- package/templates/variants/go-connectrpc/go.sum +150 -0
- package/templates/variants/go-connectrpc/package.json +7 -10
- package/templates/shared/scripts/cloudrun/config.ts +0 -62
- /package/{templates/shared/scripts → src/service-runtime}/cloudrun/bootstrap.ts +0 -0
- /package/{templates/shared/scripts → src/service-runtime}/cloudrun/deploy.ts +0 -0
- /package/{templates/shared/scripts → src/service-runtime}/cloudrun/lib.ts +0 -0
- /package/{templates/shared/scripts → src/service-runtime}/cloudrun/neon.ts +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-svc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
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",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
],
|
|
44
44
|
"packageManager": "bun@1.3.2",
|
|
45
45
|
"devDependencies": {
|
|
46
|
+
"@types/pg": "^8.16.0",
|
|
46
47
|
"@types/bun": "latest"
|
|
47
48
|
},
|
|
48
49
|
"peerDependencies": {
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
"dependencies": {
|
|
52
53
|
"@clack/prompts": "^1.4.0",
|
|
53
54
|
"@neondatabase/api-client": "^2.7.1",
|
|
55
|
+
"pg": "^8.16.3",
|
|
54
56
|
"picocolors": "^1.1.1"
|
|
55
57
|
}
|
|
56
58
|
}
|
|
@@ -4,23 +4,23 @@ import { buildDeploymentVerificationCommands, buildPostScaffoldCommands } from "
|
|
|
4
4
|
describe("buildPostScaffoldCommands", () => {
|
|
5
5
|
test("runs create and deploy for HTTP services", () => {
|
|
6
6
|
expect(buildPostScaffoldCommands({ framework: "hono" })).toEqual([
|
|
7
|
-
{ command: "
|
|
8
|
-
{ command: "
|
|
7
|
+
{ command: "service", args: ["create"] },
|
|
8
|
+
{ command: "service", args: ["deploy"] },
|
|
9
9
|
]);
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
test("builds SDK artifacts before create and deploy for ConnectRPC services", () => {
|
|
13
13
|
expect(buildPostScaffoldCommands({ framework: "connectrpc" })).toEqual([
|
|
14
|
-
{ command: "
|
|
15
|
-
{ command: "
|
|
16
|
-
{ command: "
|
|
14
|
+
{ command: "service", args: ["sdk", "build"] },
|
|
15
|
+
{ command: "service", args: ["create"] },
|
|
16
|
+
{ command: "service", args: ["deploy"] },
|
|
17
17
|
]);
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
test("uses the workers service CLI for workers services", () => {
|
|
21
21
|
expect(buildPostScaffoldCommands({ target: "workers", framework: "hono" })).toEqual([
|
|
22
|
-
{ command: "
|
|
23
|
-
{ command: "
|
|
22
|
+
{ command: "service", args: ["create"] },
|
|
23
|
+
{ command: "service", args: ["deploy"] },
|
|
24
24
|
]);
|
|
25
25
|
});
|
|
26
26
|
});
|
|
@@ -34,7 +34,7 @@ describe("buildDeploymentVerificationCommands", () => {
|
|
|
34
34
|
command: "sh",
|
|
35
35
|
args: [
|
|
36
36
|
"-c",
|
|
37
|
-
'TOKEN="$(
|
|
37
|
+
'TOKEN="$(service auth token)" && curl --fail --show-error --silent -H "Authorization: Bearer $TOKEN" "https://api.launch.anmho.com/v1/admin/waitlist?limit=1"',
|
|
38
38
|
],
|
|
39
39
|
},
|
|
40
40
|
]);
|
|
@@ -64,7 +64,7 @@ describe("buildDeploymentVerificationCommands", () => {
|
|
|
64
64
|
command: "sh",
|
|
65
65
|
args: [
|
|
66
66
|
"-c",
|
|
67
|
-
'TOKEN="$(
|
|
67
|
+
'TOKEN="$(service auth token)" && grpcurl -H "Authorization: Bearer $TOKEN" -d \'{"limit":1}\' -proto protos/waitlist/v1/waitlist.proto "api.launch.anmho.com:443" waitlist.v1.WaitlistService/ListWaitlistEntries',
|
|
68
68
|
],
|
|
69
69
|
});
|
|
70
70
|
});
|
|
@@ -81,7 +81,7 @@ describe("buildDeploymentVerificationCommands", () => {
|
|
|
81
81
|
command: "sh",
|
|
82
82
|
args: [
|
|
83
83
|
"-c",
|
|
84
|
-
'TOKEN="$(
|
|
84
|
+
'TOKEN="$(service auth token)" && curl --fail --show-error --silent -H "Authorization: Bearer $TOKEN" "https://api.launch.anmho.com/v1/admin/waitlist?limit=1"',
|
|
85
85
|
],
|
|
86
86
|
});
|
|
87
87
|
});
|
package/src/post-scaffold.ts
CHANGED
|
@@ -59,7 +59,7 @@ export function buildDeploymentVerificationCommands(
|
|
|
59
59
|
Partial<Pick<ScaffoldConfig, "target" | "serviceName" | "gcpProject" | "region">>
|
|
60
60
|
): PostScaffoldCommand[] {
|
|
61
61
|
const origin = verificationOrigin(config);
|
|
62
|
-
const tokenCommand =
|
|
62
|
+
const tokenCommand = 'TOKEN="$(service auth token)"';
|
|
63
63
|
return [
|
|
64
64
|
shellVerificationCommand(`curl --fail --show-error --silent "${origin}/"`),
|
|
65
65
|
shellVerificationCommand(`curl --fail --show-error --silent "${origin}/readyz"`),
|
|
@@ -148,18 +148,13 @@ function verificationHost(
|
|
|
148
148
|
export function buildPostScaffoldCommands(
|
|
149
149
|
config: Pick<ScaffoldConfig, "framework"> & Partial<Pick<ScaffoldConfig, "target">>
|
|
150
150
|
): PostScaffoldCommand[] {
|
|
151
|
-
const serviceCli = serviceCliPath(config);
|
|
152
151
|
return [
|
|
153
|
-
...(config.target !== "workers" && config.framework === "connectrpc" ? [{ command: "
|
|
154
|
-
{ command: "
|
|
155
|
-
{ command: "
|
|
152
|
+
...(config.target !== "workers" && config.framework === "connectrpc" ? [{ command: "service", args: ["sdk", "build"] }] : []),
|
|
153
|
+
{ command: "service", args: ["create"] },
|
|
154
|
+
{ command: "service", args: ["deploy"] },
|
|
156
155
|
];
|
|
157
156
|
}
|
|
158
157
|
|
|
159
|
-
function serviceCliPath(config: Partial<Pick<ScaffoldConfig, "target">>) {
|
|
160
|
-
return config.target === "workers" ? "./scripts/workers/cli.ts" : "./scripts/cloudrun/cli.ts";
|
|
161
|
-
}
|
|
162
|
-
|
|
163
158
|
function installProjectDependencies(cwd: string) {
|
|
164
159
|
requireCommand("bun");
|
|
165
160
|
run("bun", ["install"], { cwd });
|
package/src/scaffold.test.ts
CHANGED
|
@@ -54,63 +54,31 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
|
|
|
54
54
|
})
|
|
55
55
|
);
|
|
56
56
|
|
|
57
|
-
const configScript = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "config.ts")).text();
|
|
58
57
|
const serviceConfig = await Bun.file(join(generatedRoot, "service.config.ts")).text();
|
|
59
58
|
expect(serviceConfig).toContain('service_id: "dns-api"');
|
|
60
59
|
expect(serviceConfig).toContain('target: "cloudrun"');
|
|
60
|
+
expect(serviceConfig).toContain('profile: "microservice"');
|
|
61
|
+
expect(serviceConfig).toContain('domain: "waitlist"');
|
|
62
|
+
expect(serviceConfig).toContain('kind: "microservice"');
|
|
63
|
+
expect(serviceConfig).toContain(`runtime: "${variant.runtime}"`);
|
|
64
|
+
expect(serviceConfig).toContain(`framework: "${variant.framework}"`);
|
|
61
65
|
expect(serviceConfig).toContain('module: "buf.build/anmho/dns-api"');
|
|
62
66
|
expect(serviceConfig).toContain('cloudflare_vault_path: "prod/providers/cloudflare"');
|
|
63
67
|
expect(serviceConfig).toContain('issuer: "https://auth.anmho.com/api/auth"');
|
|
64
68
|
expect(serviceConfig).toContain('audience: "api://dns-api"');
|
|
65
69
|
expect(serviceConfig).toContain('vault_path_prefix: "prod/apps/dns-api/server/oauth-clients"');
|
|
66
70
|
expect(serviceConfig).toContain('api_key_secret_name: "dns-api-temporal-api-key"');
|
|
67
|
-
expect(
|
|
68
|
-
expect(
|
|
69
|
-
expect(
|
|
70
|
-
expect(
|
|
71
|
-
expect(
|
|
72
|
-
expect(
|
|
73
|
-
expect(
|
|
74
|
-
expect(
|
|
75
|
-
expect(
|
|
76
|
-
expect(
|
|
77
|
-
expect(configScript).toContain('apiKeySecretName: "dns-api-temporal-api-key"');
|
|
78
|
-
expect(configScript).toContain('projectId: ""');
|
|
79
|
-
expect(configScript).toContain('baseBranchId: ""');
|
|
80
|
-
expect(configScript).toContain('baseBranchName: "main"');
|
|
81
|
-
expect(configScript).toContain('previewBranchPrefix: "dns-api-pr"');
|
|
82
|
-
expect(configScript).toContain('hostname: "api.dns-api.anmho.com"');
|
|
83
|
-
expect(configScript).toContain('cloudflareVaultPath: "prod/providers/cloudflare"');
|
|
84
|
-
expect(configScript).not.toContain("github:");
|
|
85
|
-
expect(configScript).not.toContain("attachmentBucket");
|
|
86
|
-
|
|
87
|
-
const deployScript = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "lib.ts")).text();
|
|
88
|
-
expect(deployScript).toContain('--billing-project", config.project.quotaProjectId');
|
|
89
|
-
expect(deployScript).toContain('projectMode === "use_existing"');
|
|
90
|
-
expect(deployScript).toContain("serviceDomain");
|
|
91
|
-
expect(deployScript).toContain("ensureProductionDomainMapping");
|
|
92
|
-
expect(deployScript).toContain("ensureCloudflareDnsRecord");
|
|
93
|
-
expect(deployScript).toContain("gcloudWithRetry");
|
|
94
|
-
expect(deployScript).toContain("CLOUDFLARE_API_TOKEN");
|
|
95
|
-
expect(deployScript).toContain('"domain-mappings",');
|
|
96
|
-
expect(deployScript).toContain('"--region",');
|
|
97
|
-
expect(deployScript).toContain("assertProductionDomainAvailable");
|
|
98
|
-
expect(deployScript).toContain("assertServiceNameAvailable");
|
|
99
|
-
expect(deployScript).not.toContain("ensureStorageBucket");
|
|
100
|
-
|
|
71
|
+
expect(serviceConfig).toContain('project_mode: "create_new"');
|
|
72
|
+
expect(serviceConfig).toContain('quota_project_id: "anmho-infra-prod"');
|
|
73
|
+
expect(serviceConfig).toContain('jwks_url: "https://auth.anmho.com/api/auth/jwks"');
|
|
74
|
+
expect(serviceConfig).toContain('project_id: ""');
|
|
75
|
+
expect(serviceConfig).toContain('base_branch_id: ""');
|
|
76
|
+
expect(serviceConfig).toContain('base_branch_name: "main"');
|
|
77
|
+
expect(serviceConfig).toContain('preview_branch_prefix: "dns-api-pr"');
|
|
78
|
+
expect(serviceConfig).toContain('hostname: "api.dns-api.anmho.com"');
|
|
79
|
+
expect(serviceConfig).not.toContain("github:");
|
|
80
|
+
expect(serviceConfig).not.toContain("attachmentBucket");
|
|
101
81
|
expect(await Bun.file(join(generatedRoot, "scripts", "cloudrun", "integrations.ts")).exists()).toBeFalse();
|
|
102
|
-
const destroyScript = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "cleanup.ts")).text();
|
|
103
|
-
expect(destroyScript).toContain("assertOwnedResource");
|
|
104
|
-
expect(destroyScript).toContain("Planning resources to destroy");
|
|
105
|
-
expect(destroyScript).toContain("Resources selected for destroy");
|
|
106
|
-
expect(destroyScript).toContain("Destroy cannot continue until resource discovery succeeds");
|
|
107
|
-
expect(destroyScript).toContain("deleteAuthResourceServer");
|
|
108
|
-
expect(destroyScript).toContain("deleteGrafanaResources");
|
|
109
|
-
expect(destroyScript).toContain('gcx", ["resources", "delete"');
|
|
110
|
-
expect(destroyScript).toContain("config.temporal.apiKeySecretName");
|
|
111
|
-
const neonScript = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "neon.ts")).text();
|
|
112
|
-
expect(neonScript).toContain("assertDatabaseOwned");
|
|
113
|
-
expect(neonScript).toContain("assertDisposableBranchName");
|
|
114
82
|
const seedScript = await Bun.file(join(generatedRoot, "scripts", "seed.ts")).text();
|
|
115
83
|
expect(seedScript).toContain("SEED_PROD=true");
|
|
116
84
|
expect(seedScript).toContain("waitlist_entries");
|
|
@@ -168,14 +136,19 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
|
|
|
168
136
|
|
|
169
137
|
if (variant.runtime === "go") {
|
|
170
138
|
const goMod = await Bun.file(join(generatedRoot, "go.mod")).text();
|
|
139
|
+
const goSumExists = await Bun.file(join(generatedRoot, "go.sum")).exists();
|
|
171
140
|
const packageJson = await Bun.file(join(generatedRoot, "package.json")).text();
|
|
172
141
|
expect(goMod).toContain("module github.com/anmho/dns-api");
|
|
173
142
|
expect(goMod).not.toContain("module example.com/dns-api");
|
|
143
|
+
expect(goSumExists).toBeTrue();
|
|
144
|
+
const dockerfile = await Bun.file(join(generatedRoot, "Dockerfile")).text();
|
|
145
|
+
expect(dockerfile).toContain("COPY go.mod go.sum ./");
|
|
174
146
|
expect(packageJson).toContain('"dev": "make dev"');
|
|
175
|
-
expect(packageJson).toContain('"
|
|
176
|
-
expect(packageJson).toContain('"
|
|
177
|
-
expect(packageJson).toContain('"
|
|
178
|
-
expect(packageJson).toContain('"
|
|
147
|
+
expect(packageJson).toContain('"service": "service"');
|
|
148
|
+
expect(packageJson).toContain('"migrate": "service migrate"');
|
|
149
|
+
expect(packageJson).toContain('"create": "service create"');
|
|
150
|
+
expect(packageJson).toContain('"deploy": "service deploy"');
|
|
151
|
+
expect(packageJson).toContain('"destroy": "service destroy"');
|
|
179
152
|
|
|
180
153
|
const mainGo = await Bun.file(join(generatedRoot, "cmd", "server", "main.go")).text();
|
|
181
154
|
expect(mainGo).toContain("github.com/anmho/dns-api");
|
|
@@ -211,45 +184,26 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
|
|
|
211
184
|
const packageJson = await Bun.file(join(generatedRoot, "package.json")).text();
|
|
212
185
|
expect(packageJson).toContain('"@anmho/authctl": "0.1.1"');
|
|
213
186
|
expect(packageJson).toContain("@temporalio/worker");
|
|
214
|
-
expect(packageJson).toContain('"service": "./scripts/cloudrun/cli.ts"');
|
|
215
187
|
expect(packageJson).toContain('"dev": "bun run ./scripts/dev.ts bun run ./src/index.ts"');
|
|
216
188
|
expect(packageJson).toContain('"gen": "bun run ./scripts/codegen.ts"');
|
|
217
|
-
expect(packageJson).toContain('"
|
|
218
|
-
expect(packageJson).toContain('"
|
|
219
|
-
expect(packageJson).toContain('"
|
|
220
|
-
expect(packageJson).toContain('"
|
|
221
|
-
expect(packageJson).toContain('"
|
|
222
|
-
|
|
223
|
-
expect(
|
|
224
|
-
expect(
|
|
225
|
-
expect(
|
|
226
|
-
|
|
227
|
-
expect(
|
|
228
|
-
expect(
|
|
229
|
-
expect(
|
|
230
|
-
const cloudrunLib = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "lib.ts")).text();
|
|
231
|
-
expect(cloudrunLib).toContain("resolveTemporalRuntimeConfig");
|
|
232
|
-
expect(cloudrunLib).toContain("TEMPORAL_API_KEY_ENV");
|
|
233
|
-
expect(cloudrunLib).toContain("value === undefined");
|
|
234
|
-
|
|
235
|
-
const authctlScript = await Bun.file(join(generatedRoot, "scripts", "authctl.ts")).text();
|
|
236
|
-
expect(authctlScript).toContain("authctl");
|
|
237
|
-
expect(authctlScript).toContain("resource-servers");
|
|
238
|
-
expect(authctlScript).toContain("clients");
|
|
239
|
-
expect(authctlScript).toContain("defaultClientTargetArgs");
|
|
240
|
-
expect(authctlScript).toContain("ensureAuthClient");
|
|
241
|
-
expect(authctlScript).toContain("mintAuthToken");
|
|
242
|
-
expect(authctlScript).toContain("clientVaultPath");
|
|
243
|
-
expect(authctlScript).toContain("deleteAuthResourceServer");
|
|
244
|
-
expect(authctlScript).toContain("readAuthctlAccessVaultField");
|
|
245
|
-
expect(authctlScript).toContain("prod/apps/auth/authctl/cloudflare-access");
|
|
246
|
-
expect(authctlScript).toContain('existsSync("./node_modules/.bin/authctl") ? "./node_modules/.bin/authctl" : Bun.which("authctl")');
|
|
247
|
-
expect(authctlScript).not.toContain('defaultAuthResourceServerArgs(), "--yes", "--json"');
|
|
189
|
+
expect(packageJson).toContain('"service": "service"');
|
|
190
|
+
expect(packageJson).toContain('"migrate": "service migrate"');
|
|
191
|
+
expect(packageJson).toContain('"create": "service create"');
|
|
192
|
+
expect(packageJson).toContain('"deploy": "service deploy"');
|
|
193
|
+
expect(packageJson).toContain('"dashboards": "service dashboards"');
|
|
194
|
+
expect(packageJson).toContain('"auth": "service auth"');
|
|
195
|
+
expect(packageJson).toContain('"destroy": "service destroy"');
|
|
196
|
+
expect(await Bun.file(join(generatedRoot, "scripts", "cloudrun", "cli.ts")).exists()).toBeFalse();
|
|
197
|
+
expect(await Bun.file(join(generatedRoot, "scripts", "authctl.ts")).exists()).toBeFalse();
|
|
198
|
+
const serviceConfig = await Bun.file(join(generatedRoot, "service.config.ts")).text();
|
|
199
|
+
expect(serviceConfig).toContain('service_id: "dns-api"');
|
|
200
|
+
expect(serviceConfig).toContain('project_id: "anmho-dns-api"');
|
|
201
|
+
expect(serviceConfig).toContain('database_name: "dns_api"');
|
|
248
202
|
const authScript = await Bun.file(join(generatedRoot, "src", "auth.ts")).text();
|
|
249
203
|
expect(authScript).toContain('"Ed25519"');
|
|
250
204
|
|
|
251
205
|
const makefile = await Bun.file(join(generatedRoot, "Makefile")).text();
|
|
252
|
-
expect(makefile).toContain("
|
|
206
|
+
expect(makefile).toContain("SERVICE := service");
|
|
253
207
|
expect(makefile).toContain("dashboards:");
|
|
254
208
|
expect(makefile).toContain("auth:");
|
|
255
209
|
expect(makefile).toContain("bun run dev");
|
|
@@ -346,9 +300,9 @@ test("scaffolds the workers target with wrangler lifecycle commands", async () =
|
|
|
346
300
|
|
|
347
301
|
const packageJson = await Bun.file(join(generatedRoot, "package.json")).text();
|
|
348
302
|
expect(packageJson).toContain('"@anmho/authctl": "0.1.1"');
|
|
349
|
-
expect(packageJson).toContain('"service": "./scripts/workers/cli.ts"');
|
|
350
303
|
expect(packageJson).toContain('"dev": "wrangler dev"');
|
|
351
|
-
expect(packageJson).toContain('"
|
|
304
|
+
expect(packageJson).toContain('"service": "service"');
|
|
305
|
+
expect(packageJson).toContain('"auth": "service auth"');
|
|
352
306
|
expect(packageJson).toContain('"wrangler"');
|
|
353
307
|
expect(packageJson).toContain('"pg"');
|
|
354
308
|
|
|
@@ -370,28 +324,19 @@ test("scaffolds the workers target with wrangler lifecycle commands", async () =
|
|
|
370
324
|
expect(readme).toContain("Cloudflare Workers");
|
|
371
325
|
expect(readme).toContain("Hyperdrive binding for Neon-backed Postgres persistence");
|
|
372
326
|
expect(readme).not.toContain("Cloud Run");
|
|
373
|
-
const
|
|
374
|
-
expect(
|
|
375
|
-
expect(
|
|
376
|
-
expect(
|
|
377
|
-
expect(workerCli).toContain("ensureAuthClient");
|
|
378
|
-
expect(workerCli).toContain("auth token");
|
|
379
|
-
expect(workerCli).toContain("Workers database schema applied");
|
|
380
|
-
expect(workerCli).toContain("create table if not exists waitlist_entries");
|
|
381
|
-
expect(workerCli).toContain("DATABASE_URL or NEON_API_KEY is required to provision the Hyperdrive binding");
|
|
382
|
-
expect(workerCli).toContain("createProjectBranchDatabase");
|
|
383
|
-
expect(workerCli).toContain("deleteNeonDatabase");
|
|
384
|
-
expect(workerCli).toContain("deleteGrafanaResources");
|
|
385
|
-
expect(workerCli).toContain("hyperdrive\", \"delete");
|
|
327
|
+
const serviceConfig = await Bun.file(join(generatedRoot, "service.config.ts")).text();
|
|
328
|
+
expect(serviceConfig).toContain('target: "workers"');
|
|
329
|
+
expect(serviceConfig).toContain('hostname: "api.dns-api.anmho.com"');
|
|
330
|
+
expect(serviceConfig).toContain('database_name: "dns_api"');
|
|
386
331
|
const makefile = await Bun.file(join(generatedRoot, "Makefile")).text();
|
|
387
332
|
expect(makefile).toContain('no generated code for workers');
|
|
388
333
|
expect(makefile).toContain("auth:");
|
|
389
334
|
expect(makefile).not.toContain("scripts/codegen.ts");
|
|
390
335
|
|
|
391
|
-
expect(await Bun.file(join(generatedRoot, "scripts", "authctl.ts")).exists()).
|
|
336
|
+
expect(await Bun.file(join(generatedRoot, "scripts", "authctl.ts")).exists()).toBeFalse();
|
|
392
337
|
expect(await Bun.file(join(generatedRoot, "src", "auth.ts")).exists()).toBeTrue();
|
|
393
338
|
expect(await Bun.file(join(generatedRoot, "src", "storage.ts")).exists()).toBeTrue();
|
|
394
|
-
expect(await Bun.file(join(generatedRoot, "scripts", "workers", "cli.ts")).exists()).
|
|
339
|
+
expect(await Bun.file(join(generatedRoot, "scripts", "workers", "cli.ts")).exists()).toBeFalse();
|
|
395
340
|
expect(await Bun.file(join(generatedRoot, "scripts", "cloudrun", "cli.ts")).exists()).toBeFalse();
|
|
396
341
|
expect(await Bun.file(join(generatedRoot, "scripts", "dev.ts")).exists()).toBeFalse();
|
|
397
342
|
expect(await Bun.file(join(generatedRoot, "scripts", "ensure-local-db.ts")).exists()).toBeFalse();
|
package/src/scaffold.ts
CHANGED
|
@@ -77,6 +77,14 @@ export async function scaffoldProject(config: ScaffoldConfig) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
function shouldSkipForTarget(target: DeployTarget, templateKind: "shared" | "variant" | "target", relativePath: string) {
|
|
80
|
+
if (
|
|
81
|
+
relativePath === "scripts/authctl.ts" ||
|
|
82
|
+
relativePath.startsWith("scripts/cloudrun/") ||
|
|
83
|
+
relativePath.startsWith("scripts/workers/")
|
|
84
|
+
) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
80
88
|
if (target === "workers") {
|
|
81
89
|
if (templateKind === "target") {
|
|
82
90
|
return false;
|
|
@@ -94,8 +102,7 @@ function shouldSkipForTarget(target: DeployTarget, templateKind: "shared" | "var
|
|
|
94
102
|
relativePath === "scripts/local-docker.ts" ||
|
|
95
103
|
relativePath === "scripts/local-env.ts" ||
|
|
96
104
|
relativePath === "scripts/seed.ts" ||
|
|
97
|
-
relativePath === "scripts/wait-for-db.ts"
|
|
98
|
-
relativePath.startsWith("scripts/cloudrun/")
|
|
105
|
+
relativePath === "scripts/wait-for-db.ts"
|
|
99
106
|
);
|
|
100
107
|
}
|
|
101
108
|
|
|
@@ -110,7 +117,7 @@ function shouldSkipForTarget(target: DeployTarget, templateKind: "shared" | "var
|
|
|
110
117
|
);
|
|
111
118
|
}
|
|
112
119
|
|
|
113
|
-
return relativePath
|
|
120
|
+
return relativePath === "wrangler.toml";
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
async function ensureTargetDirectory(targetDir: string) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import serviceConfig from "../service.config";
|
|
2
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { serviceConfig } from "./runtime";
|
|
3
3
|
|
|
4
4
|
type CommandResult = {
|
|
5
5
|
success: boolean;
|
|
@@ -47,7 +47,7 @@ export function defaultAuthResourceServerArgs() {
|
|
|
47
47
|
auth.resource_server.audience,
|
|
48
48
|
"--stage",
|
|
49
49
|
serviceConfig.stage_default,
|
|
50
|
-
...auth.resource_server.default_scopes.flatMap((scope) => ["--scope", scope]),
|
|
50
|
+
...(auth.resource_server.default_scopes as string[]).flatMap((scope: string) => ["--scope", scope]),
|
|
51
51
|
];
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -244,7 +244,7 @@ function defaultClientTargetArgs(rest: string[]) {
|
|
|
244
244
|
const hasScope = hasFlag(rest, "--scope");
|
|
245
245
|
return [
|
|
246
246
|
...(hasResourceServer ? [] : ["--resource-server", serviceConfig.auth.resource_server.id]),
|
|
247
|
-
...(hasScope ? [] : serviceConfig.auth.resource_server.default_scopes.flatMap((scope) => ["--scope", scope])),
|
|
247
|
+
...(hasScope ? [] : (serviceConfig.auth.resource_server.default_scopes as string[]).flatMap((scope: string) => ["--scope", scope])),
|
|
248
248
|
];
|
|
249
249
|
}
|
|
250
250
|
|
|
@@ -151,7 +151,7 @@ function planProductionDomainMapping(plan: DestroyPlan) {
|
|
|
151
151
|
return;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
const routeName = mapping.spec?.routeName;
|
|
154
|
+
const routeName = mapping.spec?.routeName ?? "";
|
|
155
155
|
if (routeName !== config.serviceName) {
|
|
156
156
|
plan.blockers.push(`${config.domain.hostname} maps to ${routeName || "an unknown service"}; refusing to delete ambiguous DNS mapping`);
|
|
157
157
|
return;
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
serviceOrigin,
|
|
24
24
|
} from "./lib";
|
|
25
25
|
|
|
26
|
-
async function main(argv = Bun.argv.slice(2)) {
|
|
26
|
+
export async function main(argv = Bun.argv.slice(2)) {
|
|
27
27
|
const [command, ...rest] = argv;
|
|
28
28
|
|
|
29
29
|
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { serviceConfig } from "../runtime";
|
|
2
|
+
|
|
3
|
+
const cloudrun = serviceConfig.cloudrun;
|
|
4
|
+
const dns = serviceConfig.dns;
|
|
5
|
+
const neon = serviceConfig.neon;
|
|
6
|
+
|
|
7
|
+
export const config = {
|
|
8
|
+
serviceName: serviceConfig.service_id,
|
|
9
|
+
profile: serviceConfig.profile,
|
|
10
|
+
example: serviceConfig.example,
|
|
11
|
+
runtime: serviceConfig.runtime,
|
|
12
|
+
framework: serviceConfig.framework,
|
|
13
|
+
region: cloudrun.region,
|
|
14
|
+
artifactRepository: cloudrun.artifact_repository,
|
|
15
|
+
runtimeServiceAccount: cloudrun.service_account,
|
|
16
|
+
project: {
|
|
17
|
+
mode: cloudrun.project_mode,
|
|
18
|
+
id: cloudrun.project_id,
|
|
19
|
+
name: cloudrun.project_name,
|
|
20
|
+
createIfMissing: cloudrun.create_if_missing,
|
|
21
|
+
billingAccount: cloudrun.billing_account,
|
|
22
|
+
quotaProjectId: cloudrun.quota_project_id,
|
|
23
|
+
},
|
|
24
|
+
domain: {
|
|
25
|
+
hostname: dns.hostname,
|
|
26
|
+
baseDomain: dns.base_domain,
|
|
27
|
+
cloudflareApiBaseUrl: dns.cloudflare_api_base_url,
|
|
28
|
+
cloudflareVaultPath: dns.cloudflare_vault_path,
|
|
29
|
+
cloudflareVaultField: dns.cloudflare_vault_field,
|
|
30
|
+
},
|
|
31
|
+
auth: {
|
|
32
|
+
issuer: serviceConfig.auth.issuer,
|
|
33
|
+
audience: serviceConfig.auth.resource_server.audience,
|
|
34
|
+
jwksUrl: serviceConfig.auth.jwks_url,
|
|
35
|
+
},
|
|
36
|
+
temporal: {
|
|
37
|
+
enabled: serviceConfig.temporal.enabled,
|
|
38
|
+
address: serviceConfig.temporal.address,
|
|
39
|
+
namespace: serviceConfig.temporal.namespace,
|
|
40
|
+
taskQueue: serviceConfig.temporal.task_queue,
|
|
41
|
+
apiKeySecretName: serviceConfig.temporal.api_key_secret_name,
|
|
42
|
+
},
|
|
43
|
+
neon: {
|
|
44
|
+
projectId: neon.project_id,
|
|
45
|
+
baseBranchId: neon.base_branch_id,
|
|
46
|
+
baseBranchName: neon.base_branch_name,
|
|
47
|
+
databaseName: neon.database_name,
|
|
48
|
+
roleName: neon.role_name,
|
|
49
|
+
previewBranchPrefix: neon.preview_branch_prefix,
|
|
50
|
+
personalBranchPrefix: neon.personal_branch_prefix,
|
|
51
|
+
},
|
|
52
|
+
requiredApis: cloudrun.required_apis,
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
export type DeployEnvironment = "main" | "preview" | "personal";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
|
|
4
|
+
export const serviceRoot = process.env.CREATE_SVC_SERVICE_ROOT?.trim() || process.cwd();
|
|
5
|
+
|
|
6
|
+
export const serviceConfig = (
|
|
7
|
+
await import(pathToFileURL(join(serviceRoot, "service.config.ts")).href)
|
|
8
|
+
).default;
|
|
@@ -4,17 +4,18 @@ import { confirm, intro, isCancel, log, outro } from "@clack/prompts";
|
|
|
4
4
|
import { createApiClient } from "@neondatabase/api-client";
|
|
5
5
|
import { Client } from "pg";
|
|
6
6
|
import { ensureAuthClient, ensureAuthResourceServer, runAuthCommand, runAuthDoctor } from "../authctl";
|
|
7
|
+
import { serviceConfig } from "../runtime";
|
|
7
8
|
|
|
8
9
|
const config = {
|
|
9
|
-
serviceName:
|
|
10
|
-
hostname:
|
|
11
|
-
neonDatabaseName:
|
|
12
|
-
neonRoleName:
|
|
10
|
+
serviceName: serviceConfig.service_id,
|
|
11
|
+
hostname: serviceConfig.dns.hostname,
|
|
12
|
+
neonDatabaseName: serviceConfig.neon.database_name,
|
|
13
|
+
neonRoleName: serviceConfig.neon.role_name,
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
type DoctorStatus = "pass" | "warn" | "fail";
|
|
16
17
|
|
|
17
|
-
async function main(argv = Bun.argv.slice(2)) {
|
|
18
|
+
export async function main(argv = Bun.argv.slice(2)) {
|
|
18
19
|
const [command, ...rest] = argv;
|
|
19
20
|
|
|
20
21
|
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
@@ -255,7 +256,7 @@ async function deleteNeonDatabase() {
|
|
|
255
256
|
|
|
256
257
|
const payload = await neon.getProjectBranchDatabase(projectId, branchId, config.neonDatabaseName);
|
|
257
258
|
const database = (payload.data as { database?: { name?: string; owner_name?: string } } | undefined)?.database;
|
|
258
|
-
if (database
|
|
259
|
+
if (!database || database.name !== config.neonDatabaseName || (database.owner_name && database.owner_name !== config.neonRoleName)) {
|
|
259
260
|
throw new Error(`Refusing to delete Neon database ${database?.name ?? config.neonDatabaseName}; ownership metadata does not match`);
|
|
260
261
|
}
|
|
261
262
|
|
package/src/service.test.ts
CHANGED
|
@@ -20,10 +20,8 @@ test("findGeneratedServiceRoot detects generated service context from nested dir
|
|
|
20
20
|
const root = await mkdtemp(join(tmpdir(), "create-svc-service-root-"));
|
|
21
21
|
const serviceRoot = join(root, "generated-api");
|
|
22
22
|
const nested = join(serviceRoot, "src", "waitlist");
|
|
23
|
-
await mkdir(join(serviceRoot, "scripts", "cloudrun"), { recursive: true });
|
|
24
23
|
await mkdir(nested, { recursive: true });
|
|
25
24
|
await writeFile(join(serviceRoot, "service.config.ts"), "export default {}");
|
|
26
|
-
await writeFile(join(serviceRoot, "scripts", "cloudrun", "cli.ts"), "");
|
|
27
25
|
|
|
28
26
|
expect(findGeneratedServiceRoot(nested)).toBe(serviceRoot);
|
|
29
27
|
expect(findGeneratedServiceRoot(root)).toBeUndefined();
|
package/src/service.ts
CHANGED
|
@@ -7,7 +7,7 @@ const SCAFFOLD_COMMANDS = new Set(["create", "new", "init"]);
|
|
|
7
7
|
export async function runServiceCommand(argv: string[], cwd = process.cwd()) {
|
|
8
8
|
const serviceRoot = findGeneratedServiceRoot(cwd);
|
|
9
9
|
if (serviceRoot) {
|
|
10
|
-
delegateToGeneratedService(serviceRoot, argv);
|
|
10
|
+
await delegateToGeneratedService(serviceRoot, argv);
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -41,29 +41,23 @@ export function findGeneratedServiceRoot(start: string): string | undefined {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function isGeneratedServiceRoot(path: string) {
|
|
44
|
-
return (
|
|
45
|
-
existsSync(join(path, "service.config.ts")) &&
|
|
46
|
-
(existsSync(join(path, "scripts", "cloudrun", "cli.ts")) || existsSync(join(path, "scripts", "workers", "cli.ts")))
|
|
47
|
-
);
|
|
44
|
+
return existsSync(join(path, "service.config.ts"));
|
|
48
45
|
}
|
|
49
46
|
|
|
50
|
-
function delegateToGeneratedService(serviceRoot: string, argv: string[]) {
|
|
47
|
+
async function delegateToGeneratedService(serviceRoot: string, argv: string[]) {
|
|
51
48
|
ensureGeneratedDependencies(serviceRoot);
|
|
49
|
+
process.chdir(serviceRoot);
|
|
50
|
+
process.env.CREATE_SVC_SERVICE_ROOT = serviceRoot;
|
|
52
51
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
env: process.env,
|
|
59
|
-
stdin: "inherit",
|
|
60
|
-
stdout: "inherit",
|
|
61
|
-
stderr: "inherit",
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
if (!result.success) {
|
|
65
|
-
process.exit(result.exitCode || 1);
|
|
52
|
+
const serviceConfig = (await import(`${serviceRoot}/service.config.ts`)).default;
|
|
53
|
+
if (serviceConfig.target === "workers") {
|
|
54
|
+
const { main } = await import("./service-runtime/workers/cli");
|
|
55
|
+
await main(argv);
|
|
56
|
+
return;
|
|
66
57
|
}
|
|
58
|
+
|
|
59
|
+
const { main } = await import("./service-runtime/cloudrun/cli");
|
|
60
|
+
await main(argv);
|
|
67
61
|
}
|
|
68
62
|
|
|
69
63
|
export function generatedDependenciesInstalled(serviceRoot: string) {
|
|
@@ -59,7 +59,7 @@ No cloud credentials are required for local HTTP development after Docker and Po
|
|
|
59
59
|
|
|
60
60
|
## Remote provisioning
|
|
61
61
|
|
|
62
|
-
The generated
|
|
62
|
+
The generated service config lives in [service.config.ts](service.config.ts).
|
|
63
63
|
|
|
64
64
|
Create, deploy, and destroy use:
|
|
65
65
|
|
|
@@ -3,7 +3,13 @@ export default {
|
|
|
3
3
|
target: "{{TARGET}}",
|
|
4
4
|
runtime: "{{RUNTIME}}",
|
|
5
5
|
framework: "{{FRAMEWORK}}",
|
|
6
|
+
profile: "{{PROFILE}}",
|
|
6
7
|
stage_default: "prod",
|
|
8
|
+
example: {
|
|
9
|
+
kind: "{{EXAMPLE_KIND}}",
|
|
10
|
+
domain: "{{EXAMPLE_DOMAIN}}",
|
|
11
|
+
label: "{{EXAMPLE_LABEL}}",
|
|
12
|
+
},
|
|
7
13
|
dns: {
|
|
8
14
|
hostname: "{{API_HOSTNAME}}",
|
|
9
15
|
base_domain: "{{API_BASE_DOMAIN}}",
|
|
@@ -47,13 +53,38 @@ export default {
|
|
|
47
53
|
temporal_path: "prod/providers/temporal",
|
|
48
54
|
},
|
|
49
55
|
},
|
|
56
|
+
neon: {
|
|
57
|
+
project_id: "{{NEON_PROJECT_ID}}",
|
|
58
|
+
base_branch_id: "{{NEON_BASE_BRANCH_ID}}",
|
|
59
|
+
base_branch_name: "{{NEON_BASE_BRANCH_NAME}}",
|
|
60
|
+
database_name: "{{NEON_DATABASE_NAME}}",
|
|
61
|
+
role_name: "{{NEON_ROLE_NAME}}",
|
|
62
|
+
preview_branch_prefix: "{{NEON_PREVIEW_BRANCH_PREFIX}}",
|
|
63
|
+
personal_branch_prefix: "{{NEON_PERSONAL_BRANCH_PREFIX}}",
|
|
64
|
+
},
|
|
50
65
|
buf: {
|
|
51
66
|
module: "buf.build/anmho/{{SERVICE_ID}}",
|
|
52
67
|
},
|
|
53
68
|
cloudrun: {
|
|
54
69
|
project_id: "{{PROJECT_ID}}",
|
|
70
|
+
project_name: "{{PROJECT_NAME}}",
|
|
71
|
+
project_mode: "{{GCP_PROJECT_MODE}}",
|
|
72
|
+
create_if_missing: {{PROJECT_CREATE_IF_MISSING}},
|
|
73
|
+
billing_account: "{{BILLING_ACCOUNT}}",
|
|
74
|
+
quota_project_id: "{{QUOTA_PROJECT_ID}}",
|
|
55
75
|
region: "{{REGION}}",
|
|
76
|
+
artifact_repository: "cloud-run",
|
|
56
77
|
service_account: "{{RUNTIME_SERVICE_ACCOUNT}}",
|
|
78
|
+
required_apis: [
|
|
79
|
+
"run.googleapis.com",
|
|
80
|
+
"cloudbuild.googleapis.com",
|
|
81
|
+
"artifactregistry.googleapis.com",
|
|
82
|
+
"iam.googleapis.com",
|
|
83
|
+
"iamcredentials.googleapis.com",
|
|
84
|
+
"secretmanager.googleapis.com",
|
|
85
|
+
"serviceusage.googleapis.com",
|
|
86
|
+
"sts.googleapis.com",
|
|
87
|
+
],
|
|
57
88
|
},
|
|
58
89
|
workers: {
|
|
59
90
|
script_name: "{{SERVICE_ID}}",
|