create-svc 0.1.8 → 0.1.10
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/README.md +142 -13
- package/package.json +9 -4
- package/src/cli.test.ts +29 -8
- package/src/cli.ts +103 -70
- package/src/naming.test.ts +4 -2
- package/src/naming.ts +9 -1
- package/src/neon.ts +10 -8
- package/src/post-scaffold.ts +7 -28
- package/src/profiles.ts +28 -0
- package/src/scaffold.test.ts +126 -15
- package/src/scaffold.ts +94 -23
- package/src/vault.test.ts +62 -5
- package/src/vault.ts +24 -4
- package/templates/shared/README.md +143 -26
- package/templates/shared/docker-compose.yml +19 -0
- package/templates/shared/scripts/cloudrun/bootstrap.ts +15 -42
- package/templates/shared/scripts/cloudrun/cleanup.ts +17 -31
- package/templates/shared/scripts/cloudrun/config.ts +14 -19
- package/templates/shared/scripts/cloudrun/deploy.ts +19 -10
- package/templates/shared/scripts/cloudrun/integrations.ts +111 -0
- package/templates/shared/scripts/cloudrun/lib.ts +88 -112
- package/templates/shared/scripts/cloudrun/neon.ts +100 -14
- package/templates/shared/service.yaml +44 -1
- package/templates/variants/bun-connectrpc/Dockerfile +1 -0
- package/templates/variants/bun-connectrpc/Makefile +4 -1
- package/templates/variants/bun-connectrpc/gen/protos/chat/v1/chat_pb.ts +1078 -0
- package/templates/variants/bun-connectrpc/migrations/0000_init.sql +63 -0
- package/templates/variants/bun-connectrpc/package.json +17 -0
- package/templates/variants/bun-connectrpc/protos/chat/v1/chat.proto +228 -0
- package/templates/variants/bun-connectrpc/scripts/codegen.ts +31 -1
- package/templates/variants/bun-connectrpc/scripts/migrate.ts +46 -0
- package/templates/variants/bun-connectrpc/src/chat/service.ts +384 -0
- package/templates/variants/bun-connectrpc/src/chat/types.ts +142 -0
- package/templates/variants/bun-connectrpc/src/db/client.ts +15 -0
- package/templates/variants/bun-connectrpc/src/db/repository.ts +479 -0
- package/templates/variants/bun-connectrpc/src/db/schema.ts +75 -0
- package/templates/variants/bun-connectrpc/src/index.ts +294 -22
- package/templates/variants/bun-connectrpc/src/storage.ts +72 -0
- package/templates/variants/bun-connectrpc/src/webhooks.ts +35 -0
- package/templates/variants/bun-connectrpc/test/app.test.ts +14 -13
- package/templates/variants/bun-connectrpc/test/list-messages.integration.test.ts +182 -0
- package/templates/variants/bun-connectrpc/tsconfig.json +2 -1
- package/templates/variants/bun-hono/Makefile +4 -1
- package/templates/variants/bun-hono/migrations/0000_init.sql +63 -0
- package/templates/variants/bun-hono/package.json +13 -0
- package/templates/variants/bun-hono/scripts/migrate.ts +46 -0
- package/templates/variants/bun-hono/src/chat/service.ts +384 -0
- package/templates/variants/bun-hono/src/chat/types.ts +142 -0
- package/templates/variants/bun-hono/src/db/client.ts +15 -0
- package/templates/variants/bun-hono/src/db/repository.ts +479 -0
- package/templates/variants/bun-hono/src/db/schema.ts +75 -0
- package/templates/variants/bun-hono/src/index.ts +254 -8
- package/templates/variants/bun-hono/src/storage.ts +72 -0
- package/templates/variants/bun-hono/src/webhooks.ts +35 -0
- package/templates/variants/bun-hono/test/app.test.ts +60 -6
- package/templates/variants/bun-hono/test/list-messages.integration.test.ts +256 -0
- package/templates/variants/bun-hono/tsconfig.json +1 -0
- package/templates/variants/go-chi/Makefile +6 -2
- package/templates/variants/go-chi/buf.gen.yaml +2 -0
- package/templates/variants/go-chi/cmd/migrate/main.go +101 -0
- package/templates/variants/go-chi/cmd/server/main.go +16 -15
- package/templates/variants/go-chi/go.mod +3 -0
- package/templates/variants/go-chi/internal/app/service.go +763 -71
- package/templates/variants/go-chi/internal/config/config.go +22 -7
- package/templates/variants/go-chi/internal/httpapi/list_messages_integration_test.go +298 -0
- package/templates/variants/go-chi/internal/httpapi/routes.go +245 -43
- package/templates/variants/go-chi/migrations/0000_init.sql +63 -0
- package/templates/variants/go-chi/protos/chat/v1/chat.proto +219 -0
- package/templates/variants/go-chi/test/go.test.ts +4 -1
- package/templates/variants/go-connectrpc/Makefile +6 -2
- package/templates/variants/go-connectrpc/buf.gen.yaml +2 -0
- package/templates/variants/go-connectrpc/cmd/migrate/main.go +101 -0
- package/templates/variants/go-connectrpc/cmd/server/main.go +35 -11
- package/templates/variants/go-connectrpc/gen/chat/v1/chat.pb.go +2512 -0
- package/templates/variants/go-connectrpc/gen/chat/v1/chatv1connect/chat.connect.go +571 -0
- package/templates/variants/go-connectrpc/go.mod +4 -0
- package/templates/variants/go-connectrpc/internal/app/service.go +763 -71
- package/templates/variants/go-connectrpc/internal/config/config.go +22 -7
- package/templates/variants/go-connectrpc/internal/connectapi/handler.go +254 -42
- package/templates/variants/go-connectrpc/internal/connectapi/list_messages_integration_test.go +216 -0
- package/templates/variants/go-connectrpc/internal/httpapi/routes.go +41 -56
- package/templates/variants/go-connectrpc/migrations/0000_init.sql +63 -0
- package/templates/variants/go-connectrpc/protos/chat/v1/chat.proto +232 -0
- package/templates/shared/.env.example +0 -10
- package/templates/variants/go-chi/gen/dns/v1/dns.pb.go +0 -623
- package/templates/variants/go-chi/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
- package/templates/variants/go-chi/internal/connectapi/handler.go +0 -79
- package/templates/variants/go-chi/protos/dns/v1/dns.proto +0 -58
- package/templates/variants/go-connectrpc/gen/dns/v1/dns.pb.go +0 -623
- package/templates/variants/go-connectrpc/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
- package/templates/variants/go-connectrpc/protos/dns/v1/dns.proto +0 -58
|
@@ -15,10 +15,9 @@ type DeployArgs = {
|
|
|
15
15
|
|
|
16
16
|
type CleanupArgs = {
|
|
17
17
|
destroyProject: boolean;
|
|
18
|
-
destroyRepo: boolean;
|
|
19
18
|
};
|
|
20
19
|
|
|
21
|
-
type DeploymentTarget = {
|
|
20
|
+
export type DeploymentTarget = {
|
|
22
21
|
environment: "main" | "preview" | "personal";
|
|
23
22
|
serviceName: string;
|
|
24
23
|
branchName: string;
|
|
@@ -33,6 +32,7 @@ type CommandResult = {
|
|
|
33
32
|
};
|
|
34
33
|
|
|
35
34
|
const decoder = new TextDecoder();
|
|
35
|
+
const encoder = new TextEncoder();
|
|
36
36
|
|
|
37
37
|
export class CommandError extends Error {
|
|
38
38
|
command: string;
|
|
@@ -58,11 +58,27 @@ export function requireCommand(name: string) {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export function requireGcloudAuth() {
|
|
62
|
+
const activeAccount = gcloud(["auth", "list", "--filter=status:ACTIVE", "--format=value(account)"], {
|
|
63
|
+
allowFailure: true,
|
|
64
|
+
}).stdout.trim();
|
|
65
|
+
|
|
66
|
+
if (!activeAccount) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
[
|
|
69
|
+
"gcloud is installed but no active Google Cloud account is available.",
|
|
70
|
+
"Run `gcloud auth login` on this machine before using bootstrap, deploy, or cleanup.",
|
|
71
|
+
"If you also rely on Application Default Credentials for other tooling, run `gcloud auth application-default login` as well.",
|
|
72
|
+
].join(" ")
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
61
77
|
export function run(command: string, args: string[], options: CommandOptions = {}): CommandResult {
|
|
62
78
|
const result = Bun.spawnSync([command, ...args], {
|
|
63
79
|
cwd: process.cwd(),
|
|
64
80
|
env: process.env,
|
|
65
|
-
stdin: options.input,
|
|
81
|
+
stdin: options.input === undefined ? undefined : encoder.encode(options.input),
|
|
66
82
|
stdout: "pipe",
|
|
67
83
|
stderr: "pipe",
|
|
68
84
|
});
|
|
@@ -89,10 +105,6 @@ export function gcloud(args: string[], options: CommandOptions = {}) {
|
|
|
89
105
|
return run("gcloud", normalized, options);
|
|
90
106
|
}
|
|
91
107
|
|
|
92
|
-
export function gh(args: string[], options: CommandOptions = {}) {
|
|
93
|
-
return run("gh", args, options);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
108
|
export async function runStep<T>(label: string, task: () => Promise<T> | T) {
|
|
97
109
|
const indicator = spinner();
|
|
98
110
|
indicator.start(label);
|
|
@@ -228,116 +240,26 @@ export function ensureArtifactRepository() {
|
|
|
228
240
|
]);
|
|
229
241
|
}
|
|
230
242
|
|
|
231
|
-
export function
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export function workloadIdentityPoolResource() {
|
|
236
|
-
return `projects/${projectNumber()}/locations/global/workloadIdentityPools/${config.workloadIdentityPoolId}`;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export function workloadIdentityProviderResource() {
|
|
240
|
-
return `${workloadIdentityPoolResource()}/providers/${config.workloadIdentityProviderId}`;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export function ensureWorkloadIdentityPool() {
|
|
244
|
-
if (
|
|
245
|
-
gcloud(["iam", "workload-identity-pools", "describe", config.workloadIdentityPoolId, "--project", config.project.id, "--location", "global"], {
|
|
246
|
-
allowFailure: true,
|
|
247
|
-
}).success
|
|
248
|
-
) {
|
|
243
|
+
export function ensureStorageBucket() {
|
|
244
|
+
if (gcloud(["storage", "buckets", "describe", `gs://${config.storage.attachmentBucket}`, "--project", config.project.id], { allowFailure: true }).success) {
|
|
249
245
|
return;
|
|
250
246
|
}
|
|
251
247
|
|
|
252
248
|
gcloud([
|
|
253
|
-
"
|
|
254
|
-
"
|
|
249
|
+
"storage",
|
|
250
|
+
"buckets",
|
|
255
251
|
"create",
|
|
256
|
-
config.
|
|
252
|
+
`gs://${config.storage.attachmentBucket}`,
|
|
257
253
|
"--project",
|
|
258
254
|
config.project.id,
|
|
259
255
|
"--location",
|
|
260
|
-
|
|
261
|
-
"--
|
|
262
|
-
"GitHub Actions",
|
|
263
|
-
]);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export function ensureWorkloadIdentityProvider() {
|
|
267
|
-
if (
|
|
268
|
-
gcloud(
|
|
269
|
-
[
|
|
270
|
-
"iam",
|
|
271
|
-
"workload-identity-pools",
|
|
272
|
-
"providers",
|
|
273
|
-
"describe",
|
|
274
|
-
config.workloadIdentityProviderId,
|
|
275
|
-
"--project",
|
|
276
|
-
config.project.id,
|
|
277
|
-
"--location",
|
|
278
|
-
"global",
|
|
279
|
-
"--workload-identity-pool",
|
|
280
|
-
config.workloadIdentityPoolId,
|
|
281
|
-
],
|
|
282
|
-
{ allowFailure: true }
|
|
283
|
-
).success
|
|
284
|
-
) {
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
gcloud([
|
|
289
|
-
"iam",
|
|
290
|
-
"workload-identity-pools",
|
|
291
|
-
"providers",
|
|
292
|
-
"create-oidc",
|
|
293
|
-
config.workloadIdentityProviderId,
|
|
294
|
-
"--project",
|
|
295
|
-
config.project.id,
|
|
296
|
-
"--location",
|
|
297
|
-
"global",
|
|
298
|
-
"--workload-identity-pool",
|
|
299
|
-
config.workloadIdentityPoolId,
|
|
300
|
-
"--display-name",
|
|
301
|
-
`${config.serviceName} GitHub`,
|
|
302
|
-
"--issuer-uri",
|
|
303
|
-
"https://token.actions.githubusercontent.com",
|
|
304
|
-
"--attribute-mapping",
|
|
305
|
-
"google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner",
|
|
306
|
-
"--attribute-condition",
|
|
307
|
-
`assertion.repository=='${config.github.repo}'`,
|
|
256
|
+
config.region,
|
|
257
|
+
"--uniform-bucket-level-access",
|
|
308
258
|
]);
|
|
309
259
|
}
|
|
310
260
|
|
|
311
|
-
export function
|
|
312
|
-
gcloud(
|
|
313
|
-
[
|
|
314
|
-
"iam",
|
|
315
|
-
"workload-identity-pools",
|
|
316
|
-
"providers",
|
|
317
|
-
"delete",
|
|
318
|
-
config.workloadIdentityProviderId,
|
|
319
|
-
"--project",
|
|
320
|
-
config.project.id,
|
|
321
|
-
"--location",
|
|
322
|
-
"global",
|
|
323
|
-
"--workload-identity-pool",
|
|
324
|
-
config.workloadIdentityPoolId,
|
|
325
|
-
"--quiet",
|
|
326
|
-
],
|
|
327
|
-
{ allowFailure: true }
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
export function setGithubVariable(name: string, value: string) {
|
|
332
|
-
gh(["variable", "set", name, "--repo", config.github.repo, "--body", value]);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
export function deleteGithubVariable(name: string) {
|
|
336
|
-
gh(["variable", "delete", name, "--repo", config.github.repo], { allowFailure: true });
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
export function deleteGithubRepository() {
|
|
340
|
-
gh(["repo", "delete", config.github.repo, "--yes"]);
|
|
261
|
+
export function projectNumber() {
|
|
262
|
+
return gcloud(["projects", "describe", config.project.id, "--format=value(projectNumber)"]).stdout;
|
|
341
263
|
}
|
|
342
264
|
|
|
343
265
|
export function imageTag() {
|
|
@@ -408,7 +330,6 @@ export function parseDeployArgs(argv: string[]): DeployArgs {
|
|
|
408
330
|
export function parseCleanupArgs(argv: string[]): CleanupArgs {
|
|
409
331
|
const parsed: CleanupArgs = {
|
|
410
332
|
destroyProject: false,
|
|
411
|
-
destroyRepo: false,
|
|
412
333
|
};
|
|
413
334
|
|
|
414
335
|
for (const token of argv) {
|
|
@@ -416,11 +337,6 @@ export function parseCleanupArgs(argv: string[]): CleanupArgs {
|
|
|
416
337
|
parsed.destroyProject = true;
|
|
417
338
|
continue;
|
|
418
339
|
}
|
|
419
|
-
|
|
420
|
-
if (token === "--repo") {
|
|
421
|
-
parsed.destroyRepo = true;
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
340
|
}
|
|
425
341
|
|
|
426
342
|
return parsed;
|
|
@@ -458,6 +374,19 @@ export function resolveDeploymentTarget(environment: DeployArgs["environment"],
|
|
|
458
374
|
};
|
|
459
375
|
}
|
|
460
376
|
|
|
377
|
+
export function runtimeSecretNames(target: DeploymentTarget) {
|
|
378
|
+
return {
|
|
379
|
+
CLERK_SECRET_KEY: `${target.serviceName}-clerk-secret-key`,
|
|
380
|
+
CLERK_WEBHOOK_SECRET: `${target.serviceName}-clerk-webhook-secret`,
|
|
381
|
+
STRIPE_SECRET_KEY: `${target.serviceName}-stripe-secret-key`,
|
|
382
|
+
STRIPE_WEBHOOK_SECRET: `${target.serviceName}-stripe-webhook-secret`,
|
|
383
|
+
REVENUECAT_API_KEY: `${target.serviceName}-revenuecat-api-key`,
|
|
384
|
+
REVENUECAT_WEBHOOK_SECRET: `${target.serviceName}-revenuecat-webhook-secret`,
|
|
385
|
+
RESEND_API_KEY: `${target.serviceName}-resend-api-key`,
|
|
386
|
+
POSTHOG_API_KEY: `${target.serviceName}-posthog-api-key`,
|
|
387
|
+
} as const;
|
|
388
|
+
}
|
|
389
|
+
|
|
461
390
|
export async function renderManifest(image: string, target: DeploymentTarget) {
|
|
462
391
|
const template = await Bun.file(new URL("../../service.yaml", import.meta.url)).text();
|
|
463
392
|
const values = {
|
|
@@ -465,8 +394,11 @@ export async function renderManifest(image: string, target: DeploymentTarget) {
|
|
|
465
394
|
RUNTIME_SERVICE_ACCOUNT: config.runtimeServiceAccount,
|
|
466
395
|
IMAGE_URL: image,
|
|
467
396
|
DATABASE_URL_SECRET: target.databaseSecretName,
|
|
397
|
+
...runtimeSecretNames(target),
|
|
468
398
|
SERVICE_RUNTIME: config.runtime,
|
|
469
399
|
SERVICE_FRAMEWORK: config.framework,
|
|
400
|
+
ATTACHMENT_BUCKET: config.storage.attachmentBucket,
|
|
401
|
+
ATTACHMENT_PUBLIC_BASE_URL: config.storage.attachmentPublicBaseUrl,
|
|
470
402
|
};
|
|
471
403
|
|
|
472
404
|
return template.replace(/\$\{([A-Z0-9_]+)\}/g, (_, key: string) => {
|
|
@@ -491,6 +423,50 @@ export function serviceUrl(serviceName: string) {
|
|
|
491
423
|
).stdout;
|
|
492
424
|
}
|
|
493
425
|
|
|
426
|
+
export function serviceDomain(target: DeploymentTarget) {
|
|
427
|
+
if (target.environment === "main") {
|
|
428
|
+
return config.domain.hostname;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return `${target.serviceName}-${config.project.id}-${config.region}.a.run.app`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function serviceOrigin(target: DeploymentTarget) {
|
|
435
|
+
if (target.environment === "main") {
|
|
436
|
+
return `https://${config.domain.hostname}`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const url = serviceUrl(target.serviceName);
|
|
440
|
+
return url || `https://${serviceDomain(target)}`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function ensureProductionDomainMapping(serviceName: string) {
|
|
444
|
+
if (gcloud(["beta", "run", "domain-mappings", "describe", "--domain", config.domain.hostname, "--project", config.project.id], { allowFailure: true }).success) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
gcloud([
|
|
449
|
+
"beta",
|
|
450
|
+
"run",
|
|
451
|
+
"domain-mappings",
|
|
452
|
+
"create",
|
|
453
|
+
"--service",
|
|
454
|
+
serviceName,
|
|
455
|
+
"--domain",
|
|
456
|
+
config.domain.hostname,
|
|
457
|
+
"--project",
|
|
458
|
+
config.project.id,
|
|
459
|
+
"--region",
|
|
460
|
+
config.region,
|
|
461
|
+
]);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function deleteProductionDomainMapping() {
|
|
465
|
+
gcloud(["beta", "run", "domain-mappings", "delete", "--domain", config.domain.hostname, "--project", config.project.id, "--quiet"], {
|
|
466
|
+
allowFailure: true,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
494
470
|
export function listCloudRunServices() {
|
|
495
471
|
return gcloud(["run", "services", "list", "--project", config.project.id, "--region", config.region, "--format=value(metadata.name)"]).stdout
|
|
496
472
|
.split("\n")
|
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
import { createApiClient } from "@neondatabase/api-client";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
2
4
|
import { config } from "./config";
|
|
3
5
|
|
|
6
|
+
type NeonProject = {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
4
11
|
type NeonBranch = {
|
|
5
12
|
id: string;
|
|
6
13
|
name: string;
|
|
7
14
|
};
|
|
8
15
|
|
|
16
|
+
type ResolvedNeonConfig = {
|
|
17
|
+
projectId: string;
|
|
18
|
+
baseBranchId: string;
|
|
19
|
+
baseBranchName: string;
|
|
20
|
+
databaseName: string;
|
|
21
|
+
roleName: string;
|
|
22
|
+
previewBranchPrefix: string;
|
|
23
|
+
personalBranchPrefix: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
9
26
|
async function resolveNeonApiKey() {
|
|
10
27
|
const direct = process.env.NEON_API_KEY?.trim();
|
|
11
28
|
if (direct) {
|
|
@@ -13,13 +30,13 @@ async function resolveNeonApiKey() {
|
|
|
13
30
|
}
|
|
14
31
|
|
|
15
32
|
const addr = process.env.VAULT_ADDR?.trim() ?? "";
|
|
16
|
-
const token =
|
|
33
|
+
const token = await resolveVaultToken();
|
|
17
34
|
const mount = process.env.VAULT_SECRET_MOUNT?.trim() ?? "secret";
|
|
18
|
-
const path = process.env.VAULT_NEON_API_KEY_PATH?.trim() ?? "
|
|
19
|
-
const field = process.env.VAULT_NEON_API_KEY_FIELD?.trim() ?? "
|
|
35
|
+
const path = process.env.VAULT_NEON_API_KEY_PATH?.trim() ?? "prod/providers/neon";
|
|
36
|
+
const field = process.env.VAULT_NEON_API_KEY_FIELD?.trim() ?? "api_key";
|
|
20
37
|
|
|
21
38
|
if (!addr || !token) {
|
|
22
|
-
throw new Error("NEON_API_KEY is required for Neon provisioning, or set VAULT_ADDR
|
|
39
|
+
throw new Error("NEON_API_KEY is required for Neon provisioning, or set VAULT_ADDR with VAULT_TOKEN, VAULT_TOKEN_FILE, or ~/.vault-token");
|
|
23
40
|
}
|
|
24
41
|
|
|
25
42
|
const normalizedAddr = addr.replace(/\/+$/g, "");
|
|
@@ -49,20 +66,88 @@ async function resolveNeonApiKey() {
|
|
|
49
66
|
return apiKey;
|
|
50
67
|
}
|
|
51
68
|
|
|
69
|
+
async function resolveVaultToken() {
|
|
70
|
+
const direct = process.env.VAULT_TOKEN?.trim();
|
|
71
|
+
if (direct) {
|
|
72
|
+
return direct;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const tokenFile = process.env.VAULT_TOKEN_FILE?.trim() || join(process.env.HOME?.trim() || homedir(), ".vault-token");
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
return (await Bun.file(tokenFile).text()).trim();
|
|
79
|
+
} catch {
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
52
84
|
async function neonClient() {
|
|
53
85
|
const apiKey = await resolveNeonApiKey();
|
|
54
86
|
return createApiClient({ apiKey });
|
|
55
87
|
}
|
|
56
88
|
|
|
89
|
+
export async function listProjects() {
|
|
90
|
+
const payload = await (await neonClient()).listProjects({ limit: 100 });
|
|
91
|
+
const projects = ((payload.data as { projects?: Array<{ id?: string; name?: string }> } | undefined)?.projects ?? []);
|
|
92
|
+
return projects
|
|
93
|
+
.map((project: { id?: string; name?: string }) => ({
|
|
94
|
+
id: project.id ?? "",
|
|
95
|
+
name: project.name ?? project.id ?? "",
|
|
96
|
+
}))
|
|
97
|
+
.filter((project: NeonProject): project is NeonProject => Boolean(project.id))
|
|
98
|
+
.sort((left: NeonProject, right: NeonProject) => left.name.localeCompare(right.name));
|
|
99
|
+
}
|
|
100
|
+
|
|
57
101
|
export async function listBranches(projectId: string) {
|
|
58
102
|
const payload = await (await neonClient()).listProjectBranches({ projectId });
|
|
59
|
-
|
|
60
|
-
|
|
103
|
+
const branches = ((payload.data as { branches?: Array<{ id?: string; name?: string }> } | undefined)?.branches ?? []);
|
|
104
|
+
return branches
|
|
105
|
+
.map((branch: { id?: string; name?: string }) => ({
|
|
61
106
|
id: branch.id ?? "",
|
|
62
107
|
name: branch.name ?? branch.id ?? "",
|
|
63
108
|
}))
|
|
64
|
-
.filter((branch): branch is NeonBranch => Boolean(branch.id))
|
|
65
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
109
|
+
.filter((branch: NeonBranch): branch is NeonBranch => Boolean(branch.id))
|
|
110
|
+
.sort((left: NeonBranch, right: NeonBranch) => left.name.localeCompare(right.name));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function resolveNeonConfig(): Promise<ResolvedNeonConfig> {
|
|
114
|
+
const configuredProjectId = config.neon.projectId.trim();
|
|
115
|
+
const configuredBaseBranchId = config.neon.baseBranchId.trim();
|
|
116
|
+
const configuredBaseBranchName = config.neon.baseBranchName.trim() || "main";
|
|
117
|
+
|
|
118
|
+
if (configuredProjectId && configuredBaseBranchId) {
|
|
119
|
+
return {
|
|
120
|
+
projectId: configuredProjectId,
|
|
121
|
+
baseBranchId: configuredBaseBranchId,
|
|
122
|
+
baseBranchName: configuredBaseBranchName,
|
|
123
|
+
databaseName: config.neon.databaseName,
|
|
124
|
+
roleName: config.neon.roleName,
|
|
125
|
+
previewBranchPrefix: config.neon.previewBranchPrefix,
|
|
126
|
+
personalBranchPrefix: config.neon.personalBranchPrefix,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const projects = await listProjects();
|
|
131
|
+
const project = projects[0];
|
|
132
|
+
if (!project) {
|
|
133
|
+
throw new Error(`No Neon projects are available for ${config.serviceName}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const branches = await listBranches(project.id);
|
|
137
|
+
const branch = branches.find((candidate) => candidate.name === configuredBaseBranchName) ?? branches[0];
|
|
138
|
+
if (!branch) {
|
|
139
|
+
throw new Error(`No Neon branches are available in project ${project.id}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
projectId: project.id,
|
|
144
|
+
baseBranchId: branch.id,
|
|
145
|
+
baseBranchName: branch.name,
|
|
146
|
+
databaseName: config.neon.databaseName,
|
|
147
|
+
roleName: config.neon.roleName,
|
|
148
|
+
previewBranchPrefix: config.neon.previewBranchPrefix,
|
|
149
|
+
personalBranchPrefix: config.neon.personalBranchPrefix,
|
|
150
|
+
};
|
|
66
151
|
}
|
|
67
152
|
|
|
68
153
|
export async function ensureDatabase(projectId: string, branchId: string, databaseName: string) {
|
|
@@ -81,6 +166,7 @@ export async function ensureDatabase(projectId: string, branchId: string, databa
|
|
|
81
166
|
await client.createProjectBranchDatabase(projectId, branchId, {
|
|
82
167
|
database: {
|
|
83
168
|
name: databaseName,
|
|
169
|
+
owner_name: config.neon.roleName,
|
|
84
170
|
},
|
|
85
171
|
});
|
|
86
172
|
}
|
|
@@ -110,12 +196,12 @@ export async function ensureBranch(projectId: string, branchName: string, parent
|
|
|
110
196
|
},
|
|
111
197
|
endpoints: [
|
|
112
198
|
{
|
|
113
|
-
type: "read_write",
|
|
199
|
+
type: "read_write" as never,
|
|
114
200
|
},
|
|
115
201
|
],
|
|
116
202
|
});
|
|
117
203
|
|
|
118
|
-
const branch = payload.branch;
|
|
204
|
+
const branch = (payload.data as { branch?: { id?: string; name?: string } } | undefined)?.branch;
|
|
119
205
|
if (!branch?.id) {
|
|
120
206
|
throw new Error(`Neon did not return a branch for ${branchName}`);
|
|
121
207
|
}
|
|
@@ -141,12 +227,12 @@ export async function deleteBranch(projectId: string, branchId: string) {
|
|
|
141
227
|
export async function getConnectionUri(projectId: string, branchId: string, databaseName: string, roleName: string) {
|
|
142
228
|
const payload = await (await neonClient()).getConnectionUri({
|
|
143
229
|
projectId,
|
|
144
|
-
branchId,
|
|
145
|
-
databaseName,
|
|
146
|
-
roleName,
|
|
230
|
+
branch_id: branchId,
|
|
231
|
+
database_name: databaseName,
|
|
232
|
+
role_name: roleName,
|
|
147
233
|
});
|
|
148
234
|
|
|
149
|
-
const uri = payload.uri;
|
|
235
|
+
const uri = (payload.data as { uri?: string } | undefined)?.uri;
|
|
150
236
|
if (!uri) {
|
|
151
237
|
throw new Error(`Neon did not return a connection URI for ${databaseName} in ${config.serviceName}`);
|
|
152
238
|
}
|
|
@@ -22,7 +22,50 @@ spec:
|
|
|
22
22
|
secretKeyRef:
|
|
23
23
|
name: ${DATABASE_URL_SECRET}
|
|
24
24
|
key: latest
|
|
25
|
+
- name: CLERK_SECRET_KEY
|
|
26
|
+
valueFrom:
|
|
27
|
+
secretKeyRef:
|
|
28
|
+
name: ${CLERK_SECRET_KEY}
|
|
29
|
+
key: latest
|
|
30
|
+
- name: CLERK_WEBHOOK_SECRET
|
|
31
|
+
valueFrom:
|
|
32
|
+
secretKeyRef:
|
|
33
|
+
name: ${CLERK_WEBHOOK_SECRET}
|
|
34
|
+
key: latest
|
|
35
|
+
- name: STRIPE_SECRET_KEY
|
|
36
|
+
valueFrom:
|
|
37
|
+
secretKeyRef:
|
|
38
|
+
name: ${STRIPE_SECRET_KEY}
|
|
39
|
+
key: latest
|
|
40
|
+
- name: STRIPE_WEBHOOK_SECRET
|
|
41
|
+
valueFrom:
|
|
42
|
+
secretKeyRef:
|
|
43
|
+
name: ${STRIPE_WEBHOOK_SECRET}
|
|
44
|
+
key: latest
|
|
45
|
+
- name: REVENUECAT_API_KEY
|
|
46
|
+
valueFrom:
|
|
47
|
+
secretKeyRef:
|
|
48
|
+
name: ${REVENUECAT_API_KEY}
|
|
49
|
+
key: latest
|
|
50
|
+
- name: REVENUECAT_WEBHOOK_SECRET
|
|
51
|
+
valueFrom:
|
|
52
|
+
secretKeyRef:
|
|
53
|
+
name: ${REVENUECAT_WEBHOOK_SECRET}
|
|
54
|
+
key: latest
|
|
55
|
+
- name: RESEND_API_KEY
|
|
56
|
+
valueFrom:
|
|
57
|
+
secretKeyRef:
|
|
58
|
+
name: ${RESEND_API_KEY}
|
|
59
|
+
key: latest
|
|
60
|
+
- name: POSTHOG_API_KEY
|
|
61
|
+
valueFrom:
|
|
62
|
+
secretKeyRef:
|
|
63
|
+
name: ${POSTHOG_API_KEY}
|
|
64
|
+
key: latest
|
|
65
|
+
- name: ATTACHMENT_BUCKET
|
|
66
|
+
value: ${ATTACHMENT_BUCKET}
|
|
67
|
+
- name: ATTACHMENT_PUBLIC_BASE_URL
|
|
68
|
+
value: ${ATTACHMENT_PUBLIC_BASE_URL}
|
|
25
69
|
traffic:
|
|
26
70
|
- latestRevision: true
|
|
27
71
|
percent: 100
|
|
28
|
-
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
.PHONY: dev gen lint test bootstrap deploy cleanup
|
|
1
|
+
.PHONY: dev migrate gen lint test bootstrap deploy cleanup
|
|
2
2
|
|
|
3
3
|
CLOUDRUN := npx --no-install svc-cloudrun
|
|
4
4
|
|
|
5
5
|
dev:
|
|
6
6
|
bun run ./src/index.ts
|
|
7
7
|
|
|
8
|
+
migrate:
|
|
9
|
+
bun run ./scripts/migrate.ts
|
|
10
|
+
|
|
8
11
|
gen:
|
|
9
12
|
bun run ./scripts/codegen.ts
|
|
10
13
|
|