create-svc 0.1.7 → 0.1.9
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 +22 -12
- package/package.json +1 -1
- package/src/cli.test.ts +30 -1
- package/src/cli.ts +50 -16
- package/src/scaffold.test.ts +4 -1
- package/src/vault.test.ts +33 -0
- package/src/vault.ts +21 -2
- package/templates/shared/.env.example +2 -2
- package/templates/shared/README.md +20 -14
- package/templates/shared/scripts/cloudrun/bootstrap.ts +50 -35
- package/templates/shared/scripts/cloudrun/cleanup.ts +100 -0
- package/templates/shared/scripts/cloudrun/cli.ts +34 -0
- package/templates/shared/scripts/cloudrun/deploy.ts +49 -34
- package/templates/shared/scripts/cloudrun/lib.ts +156 -19
- package/templates/shared/scripts/cloudrun/neon.ts +31 -2
- package/templates/variants/bun-connectrpc/Makefile +24 -0
- package/templates/variants/bun-connectrpc/package.json +3 -7
- package/templates/variants/bun-hono/Makefile +24 -0
- package/templates/variants/bun-hono/package.json +3 -7
- package/templates/variants/go-chi/Makefile +25 -0
- package/templates/variants/go-chi/package.json +4 -8
- package/templates/variants/go-chi/test/go.test.ts +3 -9
- package/templates/variants/go-connectrpc/Makefile +25 -0
- package/templates/variants/go-connectrpc/package.json +4 -8
- package/templates/variants/go-connectrpc/test/go.test.ts +3 -9
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { bootstrap } from "./bootstrap";
|
|
2
1
|
import { config } from "./config";
|
|
2
|
+
import { bootstrap } from "./bootstrap";
|
|
3
3
|
import { deleteBranch, ensureBranch, ensureDatabase, getConnectionUri, listBranches } from "./neon";
|
|
4
4
|
import {
|
|
5
5
|
addSecretVersion,
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
parseDeployArgs,
|
|
12
12
|
requireCommand,
|
|
13
13
|
resolveDeploymentTarget,
|
|
14
|
+
runMain,
|
|
15
|
+
runStep,
|
|
14
16
|
serviceUrl,
|
|
15
17
|
writeRenderedManifest,
|
|
16
18
|
} from "./lib";
|
|
@@ -31,52 +33,65 @@ export async function deploy(args = Bun.argv.slice(2)) {
|
|
|
31
33
|
throw new Error("Refusing to destroy the main environment");
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
deleteService(target.serviceName);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
await runStep(`Deleting Cloud Run service ${target.serviceName}`, () => deleteService(target.serviceName));
|
|
37
|
+
await runStep(`Deleting Neon branch ${target.branchName}`, async () => {
|
|
38
|
+
const branches = await listBranches(config.neon.projectId);
|
|
39
|
+
const branch = branches.find((candidate) => candidate.name === target.branchName);
|
|
40
|
+
if (branch) {
|
|
41
|
+
await deleteBranch(config.neon.projectId, branch.id);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return `Destroyed ${target.serviceName}`;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
ensureArtifactRepository();
|
|
47
|
+
await runStep("Ensuring Artifact Registry repository", () => ensureArtifactRepository());
|
|
45
48
|
|
|
46
49
|
let branchId = config.neon.baseBranchId;
|
|
47
50
|
if (options.environment !== "main") {
|
|
48
|
-
const branch = await
|
|
51
|
+
const branch = await runStep(`Ensuring Neon branch ${target.branchName}`, () =>
|
|
52
|
+
ensureBranch(config.neon.projectId, target.branchName, config.neon.baseBranchId)
|
|
53
|
+
);
|
|
49
54
|
branchId = branch.id;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
await
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
await runStep("Publishing environment database secret", async () => {
|
|
58
|
+
await ensureDatabase(config.neon.projectId, branchId, config.neon.databaseName);
|
|
59
|
+
const connectionUri = await getConnectionUri(config.neon.projectId, branchId, config.neon.databaseName, config.neon.roleName);
|
|
60
|
+
addSecretVersion(target.databaseSecretName, connectionUri);
|
|
61
|
+
ensureSecretAccessor(target.databaseSecretName, `serviceAccount:${config.runtimeServiceAccount}`);
|
|
62
|
+
});
|
|
56
63
|
|
|
57
64
|
const image = imageUrl();
|
|
58
|
-
|
|
65
|
+
await runStep("Building container image", () =>
|
|
66
|
+
gcloud(["builds", "submit", "--project", config.project.id, "--region", config.region, "--tag", image])
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const renderedManifestPath = await runStep("Rendering Cloud Run manifest", () => writeRenderedManifest(image, target));
|
|
70
|
+
|
|
71
|
+
await runStep(`Deploying Cloud Run service ${target.serviceName}`, () =>
|
|
72
|
+
gcloud(["run", "services", "replace", renderedManifestPath.pathname, "--project", config.project.id, "--region", config.region])
|
|
73
|
+
);
|
|
59
74
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
await runStep("Granting public invoker access", () =>
|
|
76
|
+
gcloud([
|
|
77
|
+
"run",
|
|
78
|
+
"services",
|
|
79
|
+
"add-iam-policy-binding",
|
|
80
|
+
target.serviceName,
|
|
81
|
+
"--project",
|
|
82
|
+
config.project.id,
|
|
83
|
+
"--region",
|
|
84
|
+
config.region,
|
|
85
|
+
"--member",
|
|
86
|
+
"allUsers",
|
|
87
|
+
"--role",
|
|
88
|
+
"roles/run.invoker",
|
|
89
|
+
])
|
|
90
|
+
);
|
|
76
91
|
|
|
77
|
-
|
|
92
|
+
return serviceUrl(target.serviceName);
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
if (import.meta.main) {
|
|
81
|
-
await deploy();
|
|
96
|
+
await runMain("Deploy", () => deploy(Bun.argv.slice(2)));
|
|
82
97
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { intro, log, outro, spinner } from "@clack/prompts";
|
|
1
2
|
import { config } from "./config";
|
|
2
3
|
|
|
3
4
|
type CommandOptions = {
|
|
4
5
|
allowFailure?: boolean;
|
|
5
|
-
capture?: boolean;
|
|
6
6
|
input?: string;
|
|
7
7
|
};
|
|
8
8
|
|
|
@@ -13,6 +13,11 @@ type DeployArgs = {
|
|
|
13
13
|
name?: string;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
type CleanupArgs = {
|
|
17
|
+
destroyProject: boolean;
|
|
18
|
+
destroyRepo: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
16
21
|
type DeploymentTarget = {
|
|
17
22
|
environment: "main" | "preview" | "personal";
|
|
18
23
|
serviceName: string;
|
|
@@ -20,36 +25,60 @@ type DeploymentTarget = {
|
|
|
20
25
|
databaseSecretName: string;
|
|
21
26
|
};
|
|
22
27
|
|
|
28
|
+
type CommandResult = {
|
|
29
|
+
success: boolean;
|
|
30
|
+
stdout: string;
|
|
31
|
+
stderr: string;
|
|
32
|
+
exitCode: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
23
35
|
const decoder = new TextDecoder();
|
|
24
36
|
|
|
37
|
+
export class CommandError extends Error {
|
|
38
|
+
command: string;
|
|
39
|
+
args: string[];
|
|
40
|
+
stdout: string;
|
|
41
|
+
stderr: string;
|
|
42
|
+
exitCode: number;
|
|
43
|
+
|
|
44
|
+
constructor(command: string, args: string[], result: CommandResult) {
|
|
45
|
+
super(`command failed: ${command} ${args.join(" ")}`);
|
|
46
|
+
this.name = "CommandError";
|
|
47
|
+
this.command = command;
|
|
48
|
+
this.args = args;
|
|
49
|
+
this.stdout = result.stdout;
|
|
50
|
+
this.stderr = result.stderr;
|
|
51
|
+
this.exitCode = result.exitCode;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
25
55
|
export function requireCommand(name: string) {
|
|
26
56
|
if (!Bun.which(name)) {
|
|
27
57
|
throw new Error(`missing required command: ${name}`);
|
|
28
58
|
}
|
|
29
59
|
}
|
|
30
60
|
|
|
31
|
-
export function run(command: string, args: string[], options: CommandOptions = {}) {
|
|
61
|
+
export function run(command: string, args: string[], options: CommandOptions = {}): CommandResult {
|
|
32
62
|
const result = Bun.spawnSync([command, ...args], {
|
|
33
63
|
cwd: process.cwd(),
|
|
34
64
|
env: process.env,
|
|
35
65
|
stdin: options.input,
|
|
36
|
-
stdout:
|
|
37
|
-
stderr:
|
|
66
|
+
stdout: "pipe",
|
|
67
|
+
stderr: "pipe",
|
|
38
68
|
});
|
|
39
69
|
|
|
40
|
-
const
|
|
41
|
-
const stderr = result.stderr ? decoder.decode(result.stderr).trim() : "";
|
|
42
|
-
|
|
43
|
-
if (!result.success && !options.allowFailure) {
|
|
44
|
-
throw new Error([`command failed: ${command} ${args.join(" ")}`, stdout, stderr].filter(Boolean).join("\n"));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return {
|
|
70
|
+
const commandResult: CommandResult = {
|
|
48
71
|
success: result.success,
|
|
49
|
-
stdout,
|
|
50
|
-
stderr,
|
|
72
|
+
stdout: result.stdout ? decoder.decode(result.stdout).trim() : "",
|
|
73
|
+
stderr: result.stderr ? decoder.decode(result.stderr).trim() : "",
|
|
51
74
|
exitCode: result.exitCode,
|
|
52
75
|
};
|
|
76
|
+
|
|
77
|
+
if (!commandResult.success && !options.allowFailure) {
|
|
78
|
+
throw new CommandError(command, args, commandResult);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return commandResult;
|
|
53
82
|
}
|
|
54
83
|
|
|
55
84
|
export function gcloud(args: string[], options: CommandOptions = {}) {
|
|
@@ -64,6 +93,40 @@ export function gh(args: string[], options: CommandOptions = {}) {
|
|
|
64
93
|
return run("gh", args, options);
|
|
65
94
|
}
|
|
66
95
|
|
|
96
|
+
export async function runStep<T>(label: string, task: () => Promise<T> | T) {
|
|
97
|
+
const indicator = spinner();
|
|
98
|
+
indicator.start(label);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const result = await task();
|
|
102
|
+
indicator.stop(label);
|
|
103
|
+
return result;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
indicator.stop(`${label} failed`);
|
|
106
|
+
throw new Error(`${label} failed\n${formatError(error)}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function runMain(name: string, task: () => Promise<string | void>) {
|
|
111
|
+
intro(name);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const message = await task();
|
|
115
|
+
outro(message || "Done");
|
|
116
|
+
} catch (error) {
|
|
117
|
+
log.error(formatError(error));
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function formatError(error: unknown) {
|
|
123
|
+
if (error instanceof CommandError) {
|
|
124
|
+
return [error.message, error.stderr || error.stdout].filter(Boolean).join("\n");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return error instanceof Error ? error.message : String(error);
|
|
128
|
+
}
|
|
129
|
+
|
|
67
130
|
export function ensureProject() {
|
|
68
131
|
if (gcloud(["projects", "describe", config.project.id], { allowFailure: true }).success) {
|
|
69
132
|
return;
|
|
@@ -89,6 +152,10 @@ export function ensureServiceAccount(email: string) {
|
|
|
89
152
|
gcloud(["iam", "service-accounts", "create", accountId, "--project", config.project.id, "--display-name", accountId]);
|
|
90
153
|
}
|
|
91
154
|
|
|
155
|
+
export function deleteServiceAccount(email: string) {
|
|
156
|
+
gcloud(["iam", "service-accounts", "delete", email, "--project", config.project.id, "--quiet"], { allowFailure: true });
|
|
157
|
+
}
|
|
158
|
+
|
|
92
159
|
export function ensureProjectRole(member: string, role: string) {
|
|
93
160
|
gcloud(["projects", "add-iam-policy-binding", config.project.id, "--member", member, "--role", role]);
|
|
94
161
|
}
|
|
@@ -125,6 +192,18 @@ export function ensureSecretAccessor(secretName: string, member: string) {
|
|
|
125
192
|
gcloud(["secrets", "add-iam-policy-binding", secretName, "--project", config.project.id, "--member", member, "--role", "roles/secretmanager.secretAccessor"]);
|
|
126
193
|
}
|
|
127
194
|
|
|
195
|
+
export function listSecrets() {
|
|
196
|
+
return gcloud(["secrets", "list", "--project", config.project.id, "--format=value(name)"]).stdout
|
|
197
|
+
.split("\n")
|
|
198
|
+
.map((line) => line.trim())
|
|
199
|
+
.filter(Boolean)
|
|
200
|
+
.map((name) => name.split("/").pop() ?? name);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function deleteSecret(secretName: string) {
|
|
204
|
+
gcloud(["secrets", "delete", secretName, "--project", config.project.id, "--quiet"], { allowFailure: true });
|
|
205
|
+
}
|
|
206
|
+
|
|
128
207
|
export function ensureArtifactRepository() {
|
|
129
208
|
if (
|
|
130
209
|
gcloud(
|
|
@@ -150,7 +229,7 @@ export function ensureArtifactRepository() {
|
|
|
150
229
|
}
|
|
151
230
|
|
|
152
231
|
export function projectNumber() {
|
|
153
|
-
return gcloud(["projects", "describe", config.project.id, "--format=value(projectNumber)"]
|
|
232
|
+
return gcloud(["projects", "describe", config.project.id, "--format=value(projectNumber)"]).stdout;
|
|
154
233
|
}
|
|
155
234
|
|
|
156
235
|
export function workloadIdentityPoolResource() {
|
|
@@ -229,12 +308,40 @@ export function ensureWorkloadIdentityProvider() {
|
|
|
229
308
|
]);
|
|
230
309
|
}
|
|
231
310
|
|
|
311
|
+
export function deleteWorkloadIdentityProvider() {
|
|
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
|
+
|
|
232
331
|
export function setGithubVariable(name: string, value: string) {
|
|
233
332
|
gh(["variable", "set", name, "--repo", config.github.repo, "--body", value]);
|
|
234
333
|
}
|
|
235
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"]);
|
|
341
|
+
}
|
|
342
|
+
|
|
236
343
|
export function imageTag() {
|
|
237
|
-
const gitSha = run("git", ["rev-parse", "--short", "HEAD"], { allowFailure: true
|
|
344
|
+
const gitSha = run("git", ["rev-parse", "--short", "HEAD"], { allowFailure: true }).stdout;
|
|
238
345
|
return gitSha || `${Date.now()}`;
|
|
239
346
|
}
|
|
240
347
|
|
|
@@ -298,6 +405,27 @@ export function parseDeployArgs(argv: string[]): DeployArgs {
|
|
|
298
405
|
return parsed;
|
|
299
406
|
}
|
|
300
407
|
|
|
408
|
+
export function parseCleanupArgs(argv: string[]): CleanupArgs {
|
|
409
|
+
const parsed: CleanupArgs = {
|
|
410
|
+
destroyProject: false,
|
|
411
|
+
destroyRepo: false,
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
for (const token of argv) {
|
|
415
|
+
if (token === "--project") {
|
|
416
|
+
parsed.destroyProject = true;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (token === "--repo") {
|
|
421
|
+
parsed.destroyRepo = true;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return parsed;
|
|
427
|
+
}
|
|
428
|
+
|
|
301
429
|
export function resolveDeploymentTarget(environment: DeployArgs["environment"], rawName?: string): DeploymentTarget {
|
|
302
430
|
if (environment === "main") {
|
|
303
431
|
return {
|
|
@@ -359,17 +487,27 @@ export async function writeRenderedManifest(image: string, target: DeploymentTar
|
|
|
359
487
|
|
|
360
488
|
export function serviceUrl(serviceName: string) {
|
|
361
489
|
return gcloud(
|
|
362
|
-
["run", "services", "describe", serviceName, "--project", config.project.id, "--region", config.region, "--format=value(status.url)"]
|
|
363
|
-
{ capture: true }
|
|
490
|
+
["run", "services", "describe", serviceName, "--project", config.project.id, "--region", config.region, "--format=value(status.url)"]
|
|
364
491
|
).stdout;
|
|
365
492
|
}
|
|
366
493
|
|
|
494
|
+
export function listCloudRunServices() {
|
|
495
|
+
return gcloud(["run", "services", "list", "--project", config.project.id, "--region", config.region, "--format=value(metadata.name)"]).stdout
|
|
496
|
+
.split("\n")
|
|
497
|
+
.map((line) => line.trim())
|
|
498
|
+
.filter(Boolean);
|
|
499
|
+
}
|
|
500
|
+
|
|
367
501
|
export function deleteService(serviceName: string) {
|
|
368
502
|
gcloud(["run", "services", "delete", serviceName, "--project", config.project.id, "--region", config.region, "--quiet"], {
|
|
369
503
|
allowFailure: true,
|
|
370
504
|
});
|
|
371
505
|
}
|
|
372
506
|
|
|
507
|
+
export function deleteProject() {
|
|
508
|
+
gcloud(["projects", "delete", config.project.id, "--quiet"]);
|
|
509
|
+
}
|
|
510
|
+
|
|
373
511
|
function slugify(value: string) {
|
|
374
512
|
return value
|
|
375
513
|
.trim()
|
|
@@ -377,4 +515,3 @@ function slugify(value: string) {
|
|
|
377
515
|
.replace(/[^a-z0-9]+/g, "-")
|
|
378
516
|
.replace(/^-+|-+$/g, "");
|
|
379
517
|
}
|
|
380
|
-
|
|
@@ -1,4 +1,6 @@
|
|
|
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
|
|
|
4
6
|
type NeonBranch = {
|
|
@@ -13,13 +15,13 @@ async function resolveNeonApiKey() {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
const addr = process.env.VAULT_ADDR?.trim() ?? "";
|
|
16
|
-
const token =
|
|
18
|
+
const token = await resolveVaultToken();
|
|
17
19
|
const mount = process.env.VAULT_SECRET_MOUNT?.trim() ?? "secret";
|
|
18
20
|
const path = process.env.VAULT_NEON_API_KEY_PATH?.trim() ?? "provider/neon-api-key";
|
|
19
21
|
const field = process.env.VAULT_NEON_API_KEY_FIELD?.trim() ?? "value";
|
|
20
22
|
|
|
21
23
|
if (!addr || !token) {
|
|
22
|
-
throw new Error("NEON_API_KEY is required for Neon provisioning, or set VAULT_ADDR
|
|
24
|
+
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
25
|
}
|
|
24
26
|
|
|
25
27
|
const normalizedAddr = addr.replace(/\/+$/g, "");
|
|
@@ -49,6 +51,21 @@ async function resolveNeonApiKey() {
|
|
|
49
51
|
return apiKey;
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
async function resolveVaultToken() {
|
|
55
|
+
const direct = process.env.VAULT_TOKEN?.trim();
|
|
56
|
+
if (direct) {
|
|
57
|
+
return direct;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const tokenFile = process.env.VAULT_TOKEN_FILE?.trim() || join(homedir(), ".vault-token");
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
return (await Bun.file(tokenFile).text()).trim();
|
|
64
|
+
} catch {
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
async function neonClient() {
|
|
53
70
|
const apiKey = await resolveNeonApiKey();
|
|
54
71
|
return createApiClient({ apiKey });
|
|
@@ -85,6 +102,18 @@ export async function ensureDatabase(projectId: string, branchId: string, databa
|
|
|
85
102
|
});
|
|
86
103
|
}
|
|
87
104
|
|
|
105
|
+
export async function deleteDatabase(projectId: string, branchId: string, databaseName: string) {
|
|
106
|
+
try {
|
|
107
|
+
await (await neonClient()).deleteProjectBranchDatabase(projectId, branchId, databaseName);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const status = (error as { response?: { status?: number } })?.response?.status;
|
|
110
|
+
if (status === 404) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
88
117
|
export async function ensureBranch(projectId: string, branchName: string, parentId: string) {
|
|
89
118
|
const existing = (await listBranches(projectId)).find((branch) => branch.name === branchName);
|
|
90
119
|
if (existing) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
.PHONY: dev gen lint test bootstrap deploy cleanup
|
|
2
|
+
|
|
3
|
+
CLOUDRUN := npx --no-install svc-cloudrun
|
|
4
|
+
|
|
5
|
+
dev:
|
|
6
|
+
bun run ./src/index.ts
|
|
7
|
+
|
|
8
|
+
gen:
|
|
9
|
+
bun run ./scripts/codegen.ts
|
|
10
|
+
|
|
11
|
+
lint:
|
|
12
|
+
bunx tsc --noEmit
|
|
13
|
+
|
|
14
|
+
test:
|
|
15
|
+
bun test
|
|
16
|
+
|
|
17
|
+
bootstrap:
|
|
18
|
+
$(CLOUDRUN) bootstrap
|
|
19
|
+
|
|
20
|
+
deploy:
|
|
21
|
+
$(CLOUDRUN) deploy $(ARGS)
|
|
22
|
+
|
|
23
|
+
cleanup:
|
|
24
|
+
$(CLOUDRUN) cleanup $(ARGS)
|
|
@@ -2,15 +2,11 @@
|
|
|
2
2
|
"name": "{{SERVICE_NAME}}",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"gen": "bun run ./scripts/codegen.ts",
|
|
8
|
-
"lint": "bunx tsc --noEmit",
|
|
9
|
-
"test": "bun test",
|
|
10
|
-
"bootstrap": "bun run ./scripts/cloudrun/bootstrap.ts",
|
|
11
|
-
"deploy": "bun run ./scripts/cloudrun/deploy.ts"
|
|
5
|
+
"bin": {
|
|
6
|
+
"svc-cloudrun": "./scripts/cloudrun/cli.ts"
|
|
12
7
|
},
|
|
13
8
|
"dependencies": {
|
|
9
|
+
"@clack/prompts": "^1.2.0",
|
|
14
10
|
"@neondatabase/api-client": "^2.7.1"
|
|
15
11
|
},
|
|
16
12
|
"devDependencies": {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
.PHONY: dev gen lint test bootstrap deploy cleanup
|
|
2
|
+
|
|
3
|
+
CLOUDRUN := npx --no-install svc-cloudrun
|
|
4
|
+
|
|
5
|
+
dev:
|
|
6
|
+
bun run ./src/index.ts
|
|
7
|
+
|
|
8
|
+
gen:
|
|
9
|
+
bun run ./scripts/codegen.ts
|
|
10
|
+
|
|
11
|
+
lint:
|
|
12
|
+
bunx tsc --noEmit
|
|
13
|
+
|
|
14
|
+
test:
|
|
15
|
+
bun test
|
|
16
|
+
|
|
17
|
+
bootstrap:
|
|
18
|
+
$(CLOUDRUN) bootstrap
|
|
19
|
+
|
|
20
|
+
deploy:
|
|
21
|
+
$(CLOUDRUN) deploy $(ARGS)
|
|
22
|
+
|
|
23
|
+
cleanup:
|
|
24
|
+
$(CLOUDRUN) cleanup $(ARGS)
|
|
@@ -2,15 +2,11 @@
|
|
|
2
2
|
"name": "{{SERVICE_NAME}}",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"gen": "bun run ./scripts/codegen.ts",
|
|
8
|
-
"lint": "bunx tsc --noEmit",
|
|
9
|
-
"test": "bun test",
|
|
10
|
-
"bootstrap": "bun run ./scripts/cloudrun/bootstrap.ts",
|
|
11
|
-
"deploy": "bun run ./scripts/cloudrun/deploy.ts"
|
|
5
|
+
"bin": {
|
|
6
|
+
"svc-cloudrun": "./scripts/cloudrun/cli.ts"
|
|
12
7
|
},
|
|
13
8
|
"dependencies": {
|
|
9
|
+
"@clack/prompts": "^1.2.0",
|
|
14
10
|
"@neondatabase/api-client": "^2.7.1",
|
|
15
11
|
"hono": "^4.10.1"
|
|
16
12
|
},
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.PHONY: dev gen lint test bootstrap deploy cleanup
|
|
2
|
+
|
|
3
|
+
CLOUDRUN := npx --no-install svc-cloudrun
|
|
4
|
+
|
|
5
|
+
dev:
|
|
6
|
+
go run ./cmd/server
|
|
7
|
+
|
|
8
|
+
gen:
|
|
9
|
+
buf generate
|
|
10
|
+
|
|
11
|
+
lint:
|
|
12
|
+
go vet ./...
|
|
13
|
+
buf lint
|
|
14
|
+
|
|
15
|
+
test:
|
|
16
|
+
bun test ./test
|
|
17
|
+
|
|
18
|
+
bootstrap:
|
|
19
|
+
$(CLOUDRUN) bootstrap
|
|
20
|
+
|
|
21
|
+
deploy:
|
|
22
|
+
$(CLOUDRUN) deploy $(ARGS)
|
|
23
|
+
|
|
24
|
+
cleanup:
|
|
25
|
+
$(CLOUDRUN) cleanup $(ARGS)
|
|
@@ -2,15 +2,11 @@
|
|
|
2
2
|
"name": "{{SERVICE_NAME}}",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"svc-cloudrun": "./scripts/cloudrun/cli.ts"
|
|
7
|
+
},
|
|
5
8
|
"dependencies": {
|
|
9
|
+
"@clack/prompts": "^1.2.0",
|
|
6
10
|
"@neondatabase/api-client": "^2.7.1"
|
|
7
|
-
},
|
|
8
|
-
"scripts": {
|
|
9
|
-
"dev": "go run ./cmd/server",
|
|
10
|
-
"gen": "buf generate",
|
|
11
|
-
"lint": "go vet ./... && buf lint",
|
|
12
|
-
"test": "bun test ./test",
|
|
13
|
-
"bootstrap": "bun run ./scripts/cloudrun/bootstrap.ts",
|
|
14
|
-
"deploy": "bun run ./scripts/cloudrun/deploy.ts"
|
|
15
11
|
}
|
|
16
12
|
}
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
test(
|
|
6
|
-
"go test ./...",
|
|
7
|
-
{ timeout: 60_000 },
|
|
8
|
-
() => {
|
|
3
|
+
test("go test ./...", { timeout: 60_000 }, () => {
|
|
9
4
|
const result = Bun.spawnSync(["go", "test", "./..."], {
|
|
10
5
|
cwd: process.cwd(),
|
|
11
6
|
stdout: "pipe",
|
|
@@ -13,7 +8,6 @@ test(
|
|
|
13
8
|
env: process.env,
|
|
14
9
|
});
|
|
15
10
|
|
|
16
|
-
const output =
|
|
11
|
+
const output = `${new TextDecoder().decode(result.stdout)}${new TextDecoder().decode(result.stderr)}`.trim();
|
|
17
12
|
expect(result.exitCode, output || "go test ./... failed").toBe(0);
|
|
18
|
-
|
|
19
|
-
);
|
|
13
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.PHONY: dev gen lint test bootstrap deploy cleanup
|
|
2
|
+
|
|
3
|
+
CLOUDRUN := npx --no-install svc-cloudrun
|
|
4
|
+
|
|
5
|
+
dev:
|
|
6
|
+
go run ./cmd/server
|
|
7
|
+
|
|
8
|
+
gen:
|
|
9
|
+
buf generate
|
|
10
|
+
|
|
11
|
+
lint:
|
|
12
|
+
go vet ./...
|
|
13
|
+
buf lint
|
|
14
|
+
|
|
15
|
+
test:
|
|
16
|
+
bun test ./test
|
|
17
|
+
|
|
18
|
+
bootstrap:
|
|
19
|
+
$(CLOUDRUN) bootstrap
|
|
20
|
+
|
|
21
|
+
deploy:
|
|
22
|
+
$(CLOUDRUN) deploy $(ARGS)
|
|
23
|
+
|
|
24
|
+
cleanup:
|
|
25
|
+
$(CLOUDRUN) cleanup $(ARGS)
|
|
@@ -2,15 +2,11 @@
|
|
|
2
2
|
"name": "{{SERVICE_NAME}}",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"svc-cloudrun": "./scripts/cloudrun/cli.ts"
|
|
7
|
+
},
|
|
5
8
|
"dependencies": {
|
|
9
|
+
"@clack/prompts": "^1.2.0",
|
|
6
10
|
"@neondatabase/api-client": "^2.7.1"
|
|
7
|
-
},
|
|
8
|
-
"scripts": {
|
|
9
|
-
"dev": "go run ./cmd/server",
|
|
10
|
-
"gen": "buf generate",
|
|
11
|
-
"lint": "go vet ./... && buf lint",
|
|
12
|
-
"test": "bun test ./test",
|
|
13
|
-
"bootstrap": "bun run ./scripts/cloudrun/bootstrap.ts",
|
|
14
|
-
"deploy": "bun run ./scripts/cloudrun/deploy.ts"
|
|
15
11
|
}
|
|
16
12
|
}
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
test(
|
|
6
|
-
"go test ./...",
|
|
7
|
-
{ timeout: 60_000 },
|
|
8
|
-
() => {
|
|
3
|
+
test("go test ./...", { timeout: 60_000 }, () => {
|
|
9
4
|
const result = Bun.spawnSync(["go", "test", "./..."], {
|
|
10
5
|
cwd: process.cwd(),
|
|
11
6
|
stdout: "pipe",
|
|
@@ -13,7 +8,6 @@ test(
|
|
|
13
8
|
env: process.env,
|
|
14
9
|
});
|
|
15
10
|
|
|
16
|
-
const output =
|
|
11
|
+
const output = `${new TextDecoder().decode(result.stdout)}${new TextDecoder().decode(result.stderr)}`.trim();
|
|
17
12
|
expect(result.exitCode, output || "go test ./... failed").toBe(0);
|
|
18
|
-
|
|
19
|
-
);
|
|
13
|
+
});
|