create-svc 0.1.9 → 0.1.11
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 +138 -16
- package/bin/create-service.mjs +2 -0
- package/package.json +19 -11
- package/src/cli.test.ts +46 -7
- package/src/cli.ts +282 -84
- package/src/git-bootstrap.test.ts +40 -0
- package/src/git-bootstrap.ts +110 -0
- package/src/naming.test.ts +5 -2
- package/src/naming.ts +32 -1
- package/src/neon.ts +10 -8
- package/src/post-scaffold.test.ts +19 -0
- package/src/post-scaffold.ts +18 -26
- package/src/profiles.ts +25 -0
- package/src/scaffold.test.ts +320 -18
- package/src/scaffold.ts +154 -28
- package/src/vault.test.ts +94 -10
- package/src/vault.ts +81 -18
- package/templates/shared/.github/workflows/ci.yml +2 -1
- package/templates/shared/.github/workflows/deploy.yml +2 -0
- package/templates/shared/README.md +217 -29
- package/templates/shared/docker-compose.yml +19 -0
- package/templates/shared/grafana/alerts.yaml +54 -0
- package/templates/shared/grafana/waitlist-dashboard.json +63 -0
- package/templates/shared/scripts/authctl.ts +231 -0
- package/templates/shared/scripts/cloudrun/bootstrap.ts +24 -42
- package/templates/shared/scripts/cloudrun/cleanup.ts +81 -35
- package/templates/shared/scripts/cloudrun/cli.ts +324 -7
- package/templates/shared/scripts/cloudrun/config.ts +21 -19
- package/templates/shared/scripts/cloudrun/deploy.ts +16 -11
- package/templates/shared/scripts/cloudrun/lib.ts +232 -123
- package/templates/shared/scripts/cloudrun/neon.ts +127 -13
- package/templates/shared/scripts/dev.ts +22 -0
- package/templates/shared/scripts/ensure-local-db.ts +3 -0
- package/templates/shared/scripts/local-docker.ts +63 -0
- package/templates/shared/scripts/local-env.ts +27 -0
- package/templates/shared/scripts/seed.ts +73 -0
- package/templates/shared/scripts/wait-for-db.ts +32 -0
- package/templates/shared/service.config.ts +59 -0
- package/templates/shared/service.yaml +24 -1
- package/templates/targets/workers/.github/workflows/ci.yml +19 -0
- package/templates/targets/workers/.github/workflows/deploy.yml +19 -0
- package/templates/targets/workers/Makefile +33 -0
- package/templates/targets/workers/README.md +75 -0
- package/templates/targets/workers/package.json +35 -0
- package/templates/targets/workers/scripts/workers/cli.ts +397 -0
- package/templates/targets/workers/src/auth.ts +178 -0
- package/templates/targets/workers/src/index.ts +198 -0
- package/templates/targets/workers/src/storage.ts +370 -0
- package/templates/targets/workers/test/app.test.ts +108 -0
- package/templates/targets/workers/tsconfig.json +11 -0
- package/templates/targets/workers/wrangler.toml +24 -0
- package/templates/variants/bun-connectrpc/Dockerfile +1 -0
- package/templates/variants/bun-connectrpc/Makefile +17 -8
- package/templates/variants/bun-connectrpc/gen/protos/waitlist/v1/waitlist_pb.ts +424 -0
- package/templates/variants/bun-connectrpc/migrations/0000_init.sql +20 -0
- package/templates/variants/bun-connectrpc/package.json +25 -1
- package/templates/variants/bun-connectrpc/protos/waitlist/v1/waitlist.proto +91 -0
- package/templates/variants/bun-connectrpc/scripts/codegen.ts +31 -1
- package/templates/variants/bun-connectrpc/scripts/migrate.ts +49 -0
- package/templates/variants/bun-connectrpc/src/auth.ts +200 -0
- package/templates/variants/bun-connectrpc/src/db/client.ts +15 -0
- package/templates/variants/bun-connectrpc/src/db/repository.ts +126 -0
- package/templates/variants/bun-connectrpc/src/db/schema.ts +26 -0
- package/templates/variants/bun-connectrpc/src/index.ts +194 -22
- package/templates/variants/bun-connectrpc/src/temporal/activities.ts +14 -0
- package/templates/variants/bun-connectrpc/src/temporal/worker.ts +38 -0
- package/templates/variants/bun-connectrpc/src/temporal/workflows.ts +10 -0
- package/templates/variants/bun-connectrpc/src/waitlist/service.ts +172 -0
- package/templates/variants/bun-connectrpc/src/waitlist/types.ts +45 -0
- package/templates/variants/bun-connectrpc/test/app.test.ts +14 -13
- package/templates/variants/bun-connectrpc/test/waitlist.integration.test.ts +71 -0
- package/templates/variants/bun-connectrpc/tsconfig.json +2 -1
- package/templates/variants/bun-hono/Makefile +17 -8
- package/templates/variants/bun-hono/migrations/0000_init.sql +20 -0
- package/templates/variants/bun-hono/package.json +21 -1
- package/templates/variants/bun-hono/scripts/migrate.ts +49 -0
- package/templates/variants/bun-hono/src/auth.ts +181 -0
- package/templates/variants/bun-hono/src/db/client.ts +15 -0
- package/templates/variants/bun-hono/src/db/repository.ts +126 -0
- package/templates/variants/bun-hono/src/db/schema.ts +26 -0
- package/templates/variants/bun-hono/src/index.ts +141 -10
- package/templates/variants/bun-hono/src/temporal/activities.ts +14 -0
- package/templates/variants/bun-hono/src/temporal/worker.ts +38 -0
- package/templates/variants/bun-hono/src/temporal/workflows.ts +10 -0
- package/templates/variants/bun-hono/src/waitlist/service.ts +166 -0
- package/templates/variants/bun-hono/src/waitlist/types.ts +50 -0
- package/templates/variants/bun-hono/test/app.test.ts +90 -5
- package/templates/variants/bun-hono/test/waitlist.integration.test.ts +102 -0
- package/templates/variants/bun-hono/tsconfig.json +1 -0
- package/templates/variants/go-chi/Makefile +30 -10
- package/templates/variants/go-chi/atlas.hcl +8 -0
- package/templates/variants/go-chi/cmd/server/main.go +25 -13
- package/templates/variants/go-chi/go.mod +3 -2
- package/templates/variants/go-chi/internal/app/service.go +279 -70
- package/templates/variants/go-chi/internal/auth/middleware.go +289 -0
- package/templates/variants/go-chi/internal/auth/middleware_test.go +38 -0
- package/templates/variants/go-chi/internal/config/config.go +38 -7
- package/templates/variants/go-chi/internal/httpapi/routes.go +170 -47
- package/templates/variants/go-chi/internal/httpapi/waitlist_integration_test.go +199 -0
- package/templates/variants/go-chi/internal/temporal/activities.go +27 -0
- package/templates/variants/go-chi/internal/temporal/worker.go +42 -0
- package/templates/variants/go-chi/internal/temporal/workflows.go +18 -0
- package/templates/variants/go-chi/migrations/0000_init.sql +20 -0
- package/templates/variants/go-chi/migrations/atlas.sum +2 -0
- package/templates/variants/go-chi/package.json +7 -1
- package/templates/variants/go-chi/test/go.test.ts +4 -1
- package/templates/variants/go-connectrpc/Makefile +29 -8
- package/templates/variants/go-connectrpc/atlas.hcl +8 -0
- package/templates/variants/go-connectrpc/buf.gen.yaml +2 -0
- package/templates/variants/go-connectrpc/cmd/server/main.go +44 -9
- package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlist.pb.go +960 -0
- package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlistv1connect/waitlist.connect.go +283 -0
- package/templates/variants/go-connectrpc/go.mod +4 -0
- package/templates/variants/go-connectrpc/internal/app/service.go +279 -70
- package/templates/variants/go-connectrpc/internal/auth/middleware.go +289 -0
- package/templates/variants/go-connectrpc/internal/auth/middleware_test.go +38 -0
- package/templates/variants/go-connectrpc/internal/config/config.go +38 -7
- package/templates/variants/go-connectrpc/internal/connectapi/handler.go +129 -40
- package/templates/variants/go-connectrpc/internal/connectapi/waitlist_integration_test.go +122 -0
- package/templates/variants/go-connectrpc/internal/httpapi/routes.go +170 -47
- package/templates/variants/go-connectrpc/internal/temporal/activities.go +27 -0
- package/templates/variants/go-connectrpc/internal/temporal/worker.go +42 -0
- package/templates/variants/go-connectrpc/internal/temporal/workflows.go +18 -0
- package/templates/variants/go-connectrpc/migrations/0000_init.sql +20 -0
- package/templates/variants/go-connectrpc/migrations/atlas.sum +2 -0
- package/templates/variants/go-connectrpc/package.json +7 -1
- package/templates/variants/go-connectrpc/protos/waitlist/v1/waitlist.proto +93 -0
- package/templates/root/.github/workflows/buf-publish.yml +0 -19
- package/templates/root/.github/workflows/ci.yml +0 -26
- package/templates/root/.github/workflows/deploy.yml +0 -22
- package/templates/root/Dockerfile +0 -23
- package/templates/root/README.md +0 -69
- package/templates/root/buf.gen.yaml +0 -10
- package/templates/root/buf.yaml +0 -9
- package/templates/root/cmd/server/main.go +0 -44
- package/templates/root/gen/dns/v1/dns.pb.go +0 -623
- package/templates/root/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
- package/templates/root/go.mod +0 -10
- package/templates/root/internal/app/service.go +0 -152
- package/templates/root/internal/app/token_source.go +0 -50
- package/templates/root/internal/cloudflare/client.go +0 -160
- package/templates/root/internal/config/config.go +0 -55
- package/templates/root/internal/connectapi/handler.go +0 -79
- package/templates/root/internal/httpapi/routes.go +0 -93
- package/templates/root/internal/vault/client.go +0 -148
- package/templates/root/package.json +0 -12
- package/templates/root/protos/dns/v1/dns.proto +0 -58
- package/templates/root/scripts/cloudrun/bootstrap.ts +0 -65
- package/templates/root/scripts/cloudrun/config.ts +0 -50
- package/templates/root/scripts/cloudrun/deploy.ts +0 -41
- package/templates/root/scripts/cloudrun/lib.ts +0 -244
- package/templates/root/service.yaml +0 -50
- package/templates/root/test/go.test.ts +0 -19
- package/templates/shared/.env.example +0 -10
- package/templates/variants/go-chi/buf.gen.yaml +0 -10
- package/templates/variants/go-chi/buf.yaml +0 -9
- 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
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
export const config = {
|
|
2
2
|
serviceName: "{{SERVICE_NAME}}",
|
|
3
|
+
profile: "{{PROFILE}}",
|
|
4
|
+
example: {
|
|
5
|
+
kind: "{{EXAMPLE_KIND}}",
|
|
6
|
+
domain: "{{EXAMPLE_DOMAIN}}",
|
|
7
|
+
label: "{{EXAMPLE_LABEL}}",
|
|
8
|
+
},
|
|
3
9
|
runtime: "{{RUNTIME}}",
|
|
4
10
|
framework: "{{FRAMEWORK}}",
|
|
5
11
|
region: "{{REGION}}",
|
|
6
12
|
artifactRepository: "cloud-run",
|
|
7
13
|
runtimeServiceAccount: "{{RUNTIME_SERVICE_ACCOUNT}}",
|
|
8
|
-
deployerServiceAccount: "{{DEPLOYER_SERVICE_ACCOUNT}}",
|
|
9
|
-
workloadIdentityPoolId: "{{WIF_POOL_ID}}",
|
|
10
|
-
workloadIdentityProviderId: "{{WIF_PROVIDER_ID}}",
|
|
11
14
|
project: {
|
|
12
15
|
mode: "{{GCP_PROJECT_MODE}}",
|
|
13
16
|
id: "{{PROJECT_ID}}",
|
|
@@ -16,10 +19,21 @@ export const config = {
|
|
|
16
19
|
billingAccount: "{{BILLING_ACCOUNT}}",
|
|
17
20
|
quotaProjectId: "{{QUOTA_PROJECT_ID}}",
|
|
18
21
|
},
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
domain: {
|
|
23
|
+
hostname: "{{API_HOSTNAME}}",
|
|
24
|
+
baseDomain: "{{API_BASE_DOMAIN}}",
|
|
25
|
+
},
|
|
26
|
+
auth: {
|
|
27
|
+
issuer: "https://auth.anmho.com/api/auth",
|
|
28
|
+
audience: "api://{{SERVICE_ID}}",
|
|
29
|
+
jwksUrl: "https://auth.anmho.com/api/auth/jwks",
|
|
30
|
+
},
|
|
31
|
+
temporal: {
|
|
32
|
+
enabled: false,
|
|
33
|
+
address: "localhost:7233",
|
|
34
|
+
namespace: "default",
|
|
35
|
+
taskQueue: "{{SERVICE_ID}}",
|
|
36
|
+
apiKeySecretName: "{{SERVICE_ID}}-temporal-api-key",
|
|
23
37
|
},
|
|
24
38
|
neon: {
|
|
25
39
|
projectId: "{{NEON_PROJECT_ID}}",
|
|
@@ -42,16 +56,4 @@ export const config = {
|
|
|
42
56
|
],
|
|
43
57
|
} as const;
|
|
44
58
|
|
|
45
|
-
export const githubVariables = {
|
|
46
|
-
GCP_PROJECT_ID: "{{PROJECT_ID}}",
|
|
47
|
-
GCP_REGION: "{{REGION}}",
|
|
48
|
-
CLOUD_RUN_SERVICE: "{{SERVICE_NAME}}",
|
|
49
|
-
CREATE_SVC_RUNTIME: "{{RUNTIME}}",
|
|
50
|
-
CREATE_SVC_FRAMEWORK: "{{FRAMEWORK}}",
|
|
51
|
-
NEON_PROJECT_ID: "{{NEON_PROJECT_ID}}",
|
|
52
|
-
NEON_BASE_BRANCH_ID: "{{NEON_BASE_BRANCH_ID}}",
|
|
53
|
-
NEON_DATABASE_NAME: "{{NEON_DATABASE_NAME}}",
|
|
54
|
-
} as const;
|
|
55
|
-
|
|
56
59
|
export type DeployEnvironment = "main" | "preview" | "personal";
|
|
57
|
-
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { config } from "./config";
|
|
2
2
|
import { bootstrap } from "./bootstrap";
|
|
3
|
-
import { deleteBranch, ensureBranch, ensureDatabase, getConnectionUri, listBranches } from "./neon";
|
|
3
|
+
import { deleteBranch, ensureBranch, ensureDatabase, getConnectionUri, listBranches, resolveNeonConfig } from "./neon";
|
|
4
4
|
import {
|
|
5
5
|
addSecretVersion,
|
|
6
6
|
deleteService,
|
|
7
7
|
ensureArtifactRepository,
|
|
8
|
+
ensureProductionDomainMapping,
|
|
8
9
|
ensureSecretAccessor,
|
|
9
10
|
gcloud,
|
|
10
11
|
imageUrl,
|
|
@@ -13,7 +14,7 @@ import {
|
|
|
13
14
|
resolveDeploymentTarget,
|
|
14
15
|
runMain,
|
|
15
16
|
runStep,
|
|
16
|
-
|
|
17
|
+
serviceOrigin,
|
|
17
18
|
writeRenderedManifest,
|
|
18
19
|
} from "./lib";
|
|
19
20
|
|
|
@@ -27,6 +28,7 @@ export async function deploy(args = Bun.argv.slice(2)) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const target = resolveDeploymentTarget(options.environment, options.name);
|
|
31
|
+
const neon = await runStep("Resolving Neon defaults", () => resolveNeonConfig());
|
|
30
32
|
|
|
31
33
|
if (options.destroy) {
|
|
32
34
|
if (options.environment === "main") {
|
|
@@ -35,10 +37,10 @@ export async function deploy(args = Bun.argv.slice(2)) {
|
|
|
35
37
|
|
|
36
38
|
await runStep(`Deleting Cloud Run service ${target.serviceName}`, () => deleteService(target.serviceName));
|
|
37
39
|
await runStep(`Deleting Neon branch ${target.branchName}`, async () => {
|
|
38
|
-
const branches = await listBranches(
|
|
39
|
-
const branch = branches.find((candidate) => candidate.name === target.branchName);
|
|
40
|
+
const branches = await listBranches(neon.projectId);
|
|
41
|
+
const branch = branches.find((candidate: { name: string }) => candidate.name === target.branchName);
|
|
40
42
|
if (branch) {
|
|
41
|
-
await deleteBranch(
|
|
43
|
+
await deleteBranch(neon.projectId, branch.id);
|
|
42
44
|
}
|
|
43
45
|
});
|
|
44
46
|
return `Destroyed ${target.serviceName}`;
|
|
@@ -46,21 +48,20 @@ export async function deploy(args = Bun.argv.slice(2)) {
|
|
|
46
48
|
|
|
47
49
|
await runStep("Ensuring Artifact Registry repository", () => ensureArtifactRepository());
|
|
48
50
|
|
|
49
|
-
let branchId =
|
|
51
|
+
let branchId: string = neon.baseBranchId;
|
|
50
52
|
if (options.environment !== "main") {
|
|
51
53
|
const branch = await runStep(`Ensuring Neon branch ${target.branchName}`, () =>
|
|
52
|
-
ensureBranch(
|
|
54
|
+
ensureBranch(neon.projectId, target.branchName, neon.baseBranchId)
|
|
53
55
|
);
|
|
54
56
|
branchId = branch.id;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
await runStep("Publishing environment database secret", async () => {
|
|
58
|
-
await ensureDatabase(
|
|
59
|
-
const connectionUri = await getConnectionUri(
|
|
60
|
+
await ensureDatabase(neon.projectId, branchId, neon.databaseName);
|
|
61
|
+
const connectionUri = await getConnectionUri(neon.projectId, branchId, neon.databaseName, neon.roleName);
|
|
60
62
|
addSecretVersion(target.databaseSecretName, connectionUri);
|
|
61
63
|
ensureSecretAccessor(target.databaseSecretName, `serviceAccount:${config.runtimeServiceAccount}`);
|
|
62
64
|
});
|
|
63
|
-
|
|
64
65
|
const image = imageUrl();
|
|
65
66
|
await runStep("Building container image", () =>
|
|
66
67
|
gcloud(["builds", "submit", "--project", config.project.id, "--region", config.region, "--tag", image])
|
|
@@ -89,7 +90,11 @@ export async function deploy(args = Bun.argv.slice(2)) {
|
|
|
89
90
|
])
|
|
90
91
|
);
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
if (target.environment === "main") {
|
|
94
|
+
await runStep(`Ensuring production domain mapping for ${config.domain.hostname}`, () => ensureProductionDomainMapping(target.serviceName));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return serviceOrigin(target);
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
if (import.meta.main) {
|
|
@@ -4,6 +4,7 @@ import { config } from "./config";
|
|
|
4
4
|
type CommandOptions = {
|
|
5
5
|
allowFailure?: boolean;
|
|
6
6
|
input?: string;
|
|
7
|
+
env?: Record<string, string | undefined>;
|
|
7
8
|
};
|
|
8
9
|
|
|
9
10
|
type DeployArgs = {
|
|
@@ -15,16 +16,23 @@ type DeployArgs = {
|
|
|
15
16
|
|
|
16
17
|
type CleanupArgs = {
|
|
17
18
|
destroyProject: boolean;
|
|
18
|
-
|
|
19
|
+
force: boolean;
|
|
19
20
|
};
|
|
20
21
|
|
|
21
|
-
type DeploymentTarget = {
|
|
22
|
+
export type DeploymentTarget = {
|
|
22
23
|
environment: "main" | "preview" | "personal";
|
|
23
24
|
serviceName: string;
|
|
24
25
|
branchName: string;
|
|
25
26
|
databaseSecretName: string;
|
|
26
27
|
};
|
|
27
28
|
|
|
29
|
+
type GcpResourceWithLabels = {
|
|
30
|
+
metadata?: {
|
|
31
|
+
labels?: Record<string, string>;
|
|
32
|
+
};
|
|
33
|
+
labels?: Record<string, string>;
|
|
34
|
+
};
|
|
35
|
+
|
|
28
36
|
type CommandResult = {
|
|
29
37
|
success: boolean;
|
|
30
38
|
stdout: string;
|
|
@@ -33,6 +41,7 @@ type CommandResult = {
|
|
|
33
41
|
};
|
|
34
42
|
|
|
35
43
|
const decoder = new TextDecoder();
|
|
44
|
+
const encoder = new TextEncoder();
|
|
36
45
|
|
|
37
46
|
export class CommandError extends Error {
|
|
38
47
|
command: string;
|
|
@@ -58,11 +67,27 @@ export function requireCommand(name: string) {
|
|
|
58
67
|
}
|
|
59
68
|
}
|
|
60
69
|
|
|
70
|
+
export function requireGcloudAuth() {
|
|
71
|
+
const activeAccount = gcloud(["auth", "list", "--filter=status:ACTIVE", "--format=value(account)"], {
|
|
72
|
+
allowFailure: true,
|
|
73
|
+
}).stdout.trim();
|
|
74
|
+
|
|
75
|
+
if (!activeAccount) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
[
|
|
78
|
+
"gcloud is installed but no active Google Cloud account is available.",
|
|
79
|
+
"Run `gcloud auth login` on this machine before using service create, deploy, doctor, dns, or destroy.",
|
|
80
|
+
"If you also rely on Application Default Credentials for other tooling, run `gcloud auth application-default login` as well.",
|
|
81
|
+
].join(" ")
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
61
86
|
export function run(command: string, args: string[], options: CommandOptions = {}): CommandResult {
|
|
62
87
|
const result = Bun.spawnSync([command, ...args], {
|
|
63
88
|
cwd: process.cwd(),
|
|
64
|
-
env: process.env,
|
|
65
|
-
stdin: options.input,
|
|
89
|
+
env: { ...process.env, ...options.env },
|
|
90
|
+
stdin: options.input === undefined ? undefined : encoder.encode(options.input),
|
|
66
91
|
stdout: "pipe",
|
|
67
92
|
stderr: "pipe",
|
|
68
93
|
});
|
|
@@ -89,10 +114,6 @@ export function gcloud(args: string[], options: CommandOptions = {}) {
|
|
|
89
114
|
return run("gcloud", normalized, options);
|
|
90
115
|
}
|
|
91
116
|
|
|
92
|
-
export function gh(args: string[], options: CommandOptions = {}) {
|
|
93
|
-
return run("gh", args, options);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
117
|
export async function runStep<T>(label: string, task: () => Promise<T> | T) {
|
|
97
118
|
const indicator = spinner();
|
|
98
119
|
indicator.start(label);
|
|
@@ -107,7 +128,7 @@ export async function runStep<T>(label: string, task: () => Promise<T> | T) {
|
|
|
107
128
|
}
|
|
108
129
|
}
|
|
109
130
|
|
|
110
|
-
export async function runMain(name: string, task: () => Promise<string | void>) {
|
|
131
|
+
export async function runMain(name: string, task: () => Promise<string | void> | string | void) {
|
|
111
132
|
intro(name);
|
|
112
133
|
|
|
113
134
|
try {
|
|
@@ -140,6 +161,9 @@ export function ensureProject() {
|
|
|
140
161
|
}
|
|
141
162
|
|
|
142
163
|
export function attachBilling() {
|
|
164
|
+
if (config.project.mode === "use_existing") {
|
|
165
|
+
return "Using existing project billing";
|
|
166
|
+
}
|
|
143
167
|
gcloud(["beta", "billing", "projects", "link", config.project.id, "--billing-account", config.project.billingAccount]);
|
|
144
168
|
}
|
|
145
169
|
|
|
@@ -180,7 +204,17 @@ export function ensureSecret(secretName: string) {
|
|
|
180
204
|
return;
|
|
181
205
|
}
|
|
182
206
|
|
|
183
|
-
gcloud([
|
|
207
|
+
gcloud([
|
|
208
|
+
"secrets",
|
|
209
|
+
"create",
|
|
210
|
+
secretName,
|
|
211
|
+
"--project",
|
|
212
|
+
config.project.id,
|
|
213
|
+
"--replication-policy",
|
|
214
|
+
"automatic",
|
|
215
|
+
"--labels",
|
|
216
|
+
ownershipLabelsArg(),
|
|
217
|
+
]);
|
|
184
218
|
}
|
|
185
219
|
|
|
186
220
|
export function addSecretVersion(secretName: string, value: string) {
|
|
@@ -188,6 +222,10 @@ export function addSecretVersion(secretName: string, value: string) {
|
|
|
188
222
|
gcloud(["secrets", "versions", "add", secretName, "--project", config.project.id, "--data-file=-"], { input: value });
|
|
189
223
|
}
|
|
190
224
|
|
|
225
|
+
export function accessSecretVersion(secretName: string) {
|
|
226
|
+
return gcloud(["secrets", "versions", "access", "latest", "--secret", secretName, "--project", config.project.id]).stdout;
|
|
227
|
+
}
|
|
228
|
+
|
|
191
229
|
export function ensureSecretAccessor(secretName: string, member: string) {
|
|
192
230
|
gcloud(["secrets", "add-iam-policy-binding", secretName, "--project", config.project.id, "--member", member, "--role", "roles/secretmanager.secretAccessor"]);
|
|
193
231
|
}
|
|
@@ -204,6 +242,11 @@ export function deleteSecret(secretName: string) {
|
|
|
204
242
|
gcloud(["secrets", "delete", secretName, "--project", config.project.id, "--quiet"], { allowFailure: true });
|
|
205
243
|
}
|
|
206
244
|
|
|
245
|
+
export function describeSecret(secretName: string): GcpResourceWithLabels | undefined {
|
|
246
|
+
const result = gcloud(["secrets", "describe", secretName, "--project", config.project.id, "--format=json"], { allowFailure: true });
|
|
247
|
+
return parseOptionalJson(result.stdout, result.success);
|
|
248
|
+
}
|
|
249
|
+
|
|
207
250
|
export function ensureArtifactRepository() {
|
|
208
251
|
if (
|
|
209
252
|
gcloud(
|
|
@@ -232,114 +275,6 @@ export function projectNumber() {
|
|
|
232
275
|
return gcloud(["projects", "describe", config.project.id, "--format=value(projectNumber)"]).stdout;
|
|
233
276
|
}
|
|
234
277
|
|
|
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
|
-
) {
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
gcloud([
|
|
253
|
-
"iam",
|
|
254
|
-
"workload-identity-pools",
|
|
255
|
-
"create",
|
|
256
|
-
config.workloadIdentityPoolId,
|
|
257
|
-
"--project",
|
|
258
|
-
config.project.id,
|
|
259
|
-
"--location",
|
|
260
|
-
"global",
|
|
261
|
-
"--display-name",
|
|
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}'`,
|
|
308
|
-
]);
|
|
309
|
-
}
|
|
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
|
-
|
|
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"]);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
278
|
export function imageTag() {
|
|
344
279
|
const gitSha = run("git", ["rev-parse", "--short", "HEAD"], { allowFailure: true }).stdout;
|
|
345
280
|
return gitSha || `${Date.now()}`;
|
|
@@ -408,7 +343,7 @@ export function parseDeployArgs(argv: string[]): DeployArgs {
|
|
|
408
343
|
export function parseCleanupArgs(argv: string[]): CleanupArgs {
|
|
409
344
|
const parsed: CleanupArgs = {
|
|
410
345
|
destroyProject: false,
|
|
411
|
-
|
|
346
|
+
force: false,
|
|
412
347
|
};
|
|
413
348
|
|
|
414
349
|
for (const token of argv) {
|
|
@@ -416,9 +351,8 @@ export function parseCleanupArgs(argv: string[]): CleanupArgs {
|
|
|
416
351
|
parsed.destroyProject = true;
|
|
417
352
|
continue;
|
|
418
353
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
parsed.destroyRepo = true;
|
|
354
|
+
if (token === "--force") {
|
|
355
|
+
parsed.force = true;
|
|
422
356
|
continue;
|
|
423
357
|
}
|
|
424
358
|
}
|
|
@@ -460,24 +394,61 @@ export function resolveDeploymentTarget(environment: DeployArgs["environment"],
|
|
|
460
394
|
|
|
461
395
|
export async function renderManifest(image: string, target: DeploymentTarget) {
|
|
462
396
|
const template = await Bun.file(new URL("../../service.yaml", import.meta.url)).text();
|
|
397
|
+
const temporal = resolveTemporalRuntimeConfig();
|
|
463
398
|
const values = {
|
|
464
399
|
SERVICE_NAME: target.serviceName,
|
|
400
|
+
SERVICE_ID: config.serviceName,
|
|
465
401
|
RUNTIME_SERVICE_ACCOUNT: config.runtimeServiceAccount,
|
|
466
402
|
IMAGE_URL: image,
|
|
467
403
|
DATABASE_URL_SECRET: target.databaseSecretName,
|
|
468
404
|
SERVICE_RUNTIME: config.runtime,
|
|
469
405
|
SERVICE_FRAMEWORK: config.framework,
|
|
406
|
+
TEMPORAL_ENABLED: String(temporal.enabled),
|
|
407
|
+
TEMPORAL_ADDRESS: temporal.address,
|
|
408
|
+
TEMPORAL_NAMESPACE: temporal.namespace,
|
|
409
|
+
TEMPORAL_TASK_QUEUE: temporal.taskQueue,
|
|
410
|
+
TEMPORAL_API_KEY_ENV: temporal.apiKeySecretName
|
|
411
|
+
? [
|
|
412
|
+
" - name: TEMPORAL_API_KEY",
|
|
413
|
+
" valueFrom:",
|
|
414
|
+
" secretKeyRef:",
|
|
415
|
+
` name: ${temporal.apiKeySecretName}`,
|
|
416
|
+
" key: latest",
|
|
417
|
+
].join("\n")
|
|
418
|
+
: "",
|
|
419
|
+
AUTH_ISSUER: config.auth.issuer,
|
|
420
|
+
AUTH_AUDIENCE: config.auth.audience,
|
|
421
|
+
AUTH_JWKS_URL: config.auth.jwksUrl,
|
|
470
422
|
};
|
|
471
423
|
|
|
472
424
|
return template.replace(/\$\{([A-Z0-9_]+)\}/g, (_, key: string) => {
|
|
473
425
|
const value = values[key as keyof typeof values];
|
|
474
|
-
if (
|
|
426
|
+
if (value === undefined) {
|
|
475
427
|
throw new Error(`missing manifest value for ${key}`);
|
|
476
428
|
}
|
|
477
429
|
return value;
|
|
478
430
|
});
|
|
479
431
|
}
|
|
480
432
|
|
|
433
|
+
export function resolveTemporalRuntimeConfig() {
|
|
434
|
+
const enabledOverride = process.env.TEMPORAL_ENABLED?.trim();
|
|
435
|
+
const address = process.env.TEMPORAL_ADDRESS?.trim() || config.temporal.address;
|
|
436
|
+
const namespace = process.env.TEMPORAL_NAMESPACE?.trim() || config.temporal.namespace;
|
|
437
|
+
const taskQueue = process.env.TEMPORAL_TASK_QUEUE?.trim() || config.temporal.taskQueue;
|
|
438
|
+
const apiKeySecretName = process.env.TEMPORAL_API_KEY_SECRET?.trim() || (process.env.TEMPORAL_API_KEY?.trim() ? config.temporal.apiKeySecretName : "");
|
|
439
|
+
const enabled = enabledOverride
|
|
440
|
+
? ["1", "true", "yes", "on"].includes(enabledOverride.toLowerCase())
|
|
441
|
+
: Boolean(process.env.TEMPORAL_ADDRESS?.trim() || process.env.TEMPORAL_API_KEY?.trim() || process.env.TEMPORAL_API_KEY_SECRET?.trim());
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
enabled,
|
|
445
|
+
address,
|
|
446
|
+
namespace,
|
|
447
|
+
taskQueue,
|
|
448
|
+
apiKeySecretName,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
481
452
|
export async function writeRenderedManifest(image: string, target: DeploymentTarget) {
|
|
482
453
|
const rendered = await renderManifest(image, target);
|
|
483
454
|
const path = new URL("../../.cloudrun.rendered.yaml", import.meta.url);
|
|
@@ -491,6 +462,109 @@ export function serviceUrl(serviceName: string) {
|
|
|
491
462
|
).stdout;
|
|
492
463
|
}
|
|
493
464
|
|
|
465
|
+
export function serviceDomain(target: DeploymentTarget) {
|
|
466
|
+
if (target.environment === "main") {
|
|
467
|
+
return config.domain.hostname;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return `${target.serviceName}-${config.project.id}-${config.region}.a.run.app`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function serviceOrigin(target: DeploymentTarget) {
|
|
474
|
+
if (target.environment === "main") {
|
|
475
|
+
return `https://${config.domain.hostname}`;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const url = serviceUrl(target.serviceName);
|
|
479
|
+
return url || `https://${serviceDomain(target)}`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export function ensureProductionDomainMapping(serviceName: string) {
|
|
483
|
+
const existing = describeProductionDomainMapping();
|
|
484
|
+
if (existing) {
|
|
485
|
+
const mappedService = existing.spec?.routeName ?? existing.status?.resourceRecords?.[0]?.rrdata;
|
|
486
|
+
if (!mappedService || mappedService === serviceName) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
throw new Error(`${config.domain.hostname} is already mapped to ${mappedService}; refusing to take it over`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
gcloud([
|
|
493
|
+
"beta",
|
|
494
|
+
"run",
|
|
495
|
+
"domain-mappings",
|
|
496
|
+
"create",
|
|
497
|
+
"--service",
|
|
498
|
+
serviceName,
|
|
499
|
+
"--domain",
|
|
500
|
+
config.domain.hostname,
|
|
501
|
+
"--project",
|
|
502
|
+
config.project.id,
|
|
503
|
+
"--region",
|
|
504
|
+
config.region,
|
|
505
|
+
]);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export function describeProductionDomainMapping():
|
|
509
|
+
| { spec?: { routeName?: string }; status?: { resourceRecords?: Array<{ rrdata?: string }> } }
|
|
510
|
+
| undefined {
|
|
511
|
+
const result = gcloud(
|
|
512
|
+
[
|
|
513
|
+
"beta",
|
|
514
|
+
"run",
|
|
515
|
+
"domain-mappings",
|
|
516
|
+
"describe",
|
|
517
|
+
"--domain",
|
|
518
|
+
config.domain.hostname,
|
|
519
|
+
"--project",
|
|
520
|
+
config.project.id,
|
|
521
|
+
"--region",
|
|
522
|
+
config.region,
|
|
523
|
+
"--format=json",
|
|
524
|
+
],
|
|
525
|
+
{ allowFailure: true }
|
|
526
|
+
);
|
|
527
|
+
if (!result.success || !result.stdout) {
|
|
528
|
+
return undefined;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
try {
|
|
532
|
+
return JSON.parse(result.stdout);
|
|
533
|
+
} catch {
|
|
534
|
+
throw new Error(`Unable to parse Cloud Run domain mapping for ${config.domain.hostname}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function assertProductionDomainAvailable(serviceName: string) {
|
|
539
|
+
const existing = describeProductionDomainMapping();
|
|
540
|
+
if (!existing) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const mappedService = existing.spec?.routeName;
|
|
545
|
+
if (mappedService && mappedService !== serviceName) {
|
|
546
|
+
throw new Error(`${config.domain.hostname} is already mapped to ${mappedService}; choose a different service_id before provisioning resources`);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
throw new Error(`${config.domain.hostname} already has a domain mapping; use service deploy to redeploy or service dns to repair it`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function assertServiceNameAvailable(serviceName: string) {
|
|
553
|
+
const result = gcloud(
|
|
554
|
+
["run", "services", "describe", serviceName, "--project", config.project.id, "--region", config.region, "--format=value(metadata.name)"],
|
|
555
|
+
{ allowFailure: true }
|
|
556
|
+
);
|
|
557
|
+
if (result.success) {
|
|
558
|
+
throw new Error(`${serviceName} already exists in Cloud Run; use service deploy to redeploy or service destroy to remove owned resources`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export function deleteProductionDomainMapping() {
|
|
563
|
+
gcloud(["beta", "run", "domain-mappings", "delete", "--domain", config.domain.hostname, "--project", config.project.id, "--quiet"], {
|
|
564
|
+
allowFailure: true,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
494
568
|
export function listCloudRunServices() {
|
|
495
569
|
return gcloud(["run", "services", "list", "--project", config.project.id, "--region", config.region, "--format=value(metadata.name)"]).stdout
|
|
496
570
|
.split("\n")
|
|
@@ -498,6 +572,14 @@ export function listCloudRunServices() {
|
|
|
498
572
|
.filter(Boolean);
|
|
499
573
|
}
|
|
500
574
|
|
|
575
|
+
export function describeCloudRunService(serviceName: string): GcpResourceWithLabels | undefined {
|
|
576
|
+
const result = gcloud(
|
|
577
|
+
["run", "services", "describe", serviceName, "--project", config.project.id, "--region", config.region, "--format=json"],
|
|
578
|
+
{ allowFailure: true }
|
|
579
|
+
);
|
|
580
|
+
return parseOptionalJson(result.stdout, result.success);
|
|
581
|
+
}
|
|
582
|
+
|
|
501
583
|
export function deleteService(serviceName: string) {
|
|
502
584
|
gcloud(["run", "services", "delete", serviceName, "--project", config.project.id, "--region", config.region, "--quiet"], {
|
|
503
585
|
allowFailure: true,
|
|
@@ -515,3 +597,30 @@ function slugify(value: string) {
|
|
|
515
597
|
.replace(/[^a-z0-9]+/g, "-")
|
|
516
598
|
.replace(/^-+|-+$/g, "");
|
|
517
599
|
}
|
|
600
|
+
|
|
601
|
+
export function assertOwnedResource(name: string, resource: GcpResourceWithLabels | undefined) {
|
|
602
|
+
if (!resource) {
|
|
603
|
+
throw new Error(`${name} does not exist`);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const labels = resource.metadata?.labels ?? resource.labels ?? {};
|
|
607
|
+
if (labels.managed_by !== "create-service" || labels.service_id !== config.serviceName) {
|
|
608
|
+
throw new Error(`${name} is missing ownership labels for service_id=${config.serviceName}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function ownershipLabelsArg() {
|
|
613
|
+
return `managed_by=create-service,service_id=${config.serviceName}`;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function parseOptionalJson<T>(stdout: string, success: boolean): T | undefined {
|
|
617
|
+
if (!success || !stdout) {
|
|
618
|
+
return undefined;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
try {
|
|
622
|
+
return JSON.parse(stdout) as T;
|
|
623
|
+
} catch {
|
|
624
|
+
throw new Error("Unable to parse gcloud JSON response");
|
|
625
|
+
}
|
|
626
|
+
}
|