create-svc 0.1.2 → 0.1.4

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.
Files changed (69) hide show
  1. package/package.json +6 -3
  2. package/src/cli.ts +328 -108
  3. package/src/gcp.test.ts +71 -0
  4. package/src/gcp.ts +97 -0
  5. package/src/naming.test.ts +37 -0
  6. package/src/naming.ts +103 -0
  7. package/src/neon.test.ts +48 -0
  8. package/src/neon.ts +76 -0
  9. package/src/post-scaffold.ts +77 -0
  10. package/src/scaffold.test.ts +66 -31
  11. package/src/scaffold.ts +60 -55
  12. package/templates/shared/.github/workflows/ci.yml +22 -0
  13. package/templates/shared/.github/workflows/deploy.yml +30 -0
  14. package/templates/shared/.github/workflows/personal.yml +41 -0
  15. package/templates/shared/.github/workflows/preview-cleanup.yml +25 -0
  16. package/templates/shared/.github/workflows/preview.yml +29 -0
  17. package/templates/shared/README.md +37 -0
  18. package/templates/shared/scripts/cloudrun/bootstrap.ts +76 -0
  19. package/templates/shared/scripts/cloudrun/config.ts +57 -0
  20. package/templates/shared/scripts/cloudrun/deploy.ts +82 -0
  21. package/templates/shared/scripts/cloudrun/lib.ts +380 -0
  22. package/templates/shared/scripts/cloudrun/neon.ts +104 -0
  23. package/templates/shared/service.yaml +28 -0
  24. package/templates/variants/bun-connectrpc/Dockerfile +13 -0
  25. package/templates/variants/bun-connectrpc/package.json +20 -0
  26. package/templates/variants/bun-connectrpc/scripts/codegen.ts +1 -0
  27. package/templates/variants/bun-connectrpc/src/index.ts +32 -0
  28. package/templates/variants/bun-connectrpc/test/app.test.ts +17 -0
  29. package/templates/variants/bun-connectrpc/tsconfig.json +10 -0
  30. package/templates/variants/bun-hono/Dockerfile +13 -0
  31. package/templates/variants/bun-hono/package.json +21 -0
  32. package/templates/variants/bun-hono/scripts/codegen.ts +1 -0
  33. package/templates/variants/bun-hono/src/index.ts +24 -0
  34. package/templates/variants/bun-hono/test/app.test.ts +12 -0
  35. package/templates/variants/bun-hono/tsconfig.json +10 -0
  36. package/templates/variants/go-chi/Dockerfile +23 -0
  37. package/templates/variants/go-chi/buf.gen.yaml +10 -0
  38. package/templates/variants/go-chi/buf.yaml +9 -0
  39. package/templates/variants/go-chi/cmd/server/main.go +52 -0
  40. package/templates/variants/go-chi/gen/dns/v1/dns.pb.go +623 -0
  41. package/templates/variants/go-chi/gen/dns/v1/dnsv1connect/dns.connect.go +192 -0
  42. package/templates/variants/go-chi/go.mod +10 -0
  43. package/templates/variants/go-chi/internal/app/service.go +109 -0
  44. package/templates/variants/go-chi/internal/app/token_source.go +50 -0
  45. package/templates/variants/go-chi/internal/cloudflare/client.go +160 -0
  46. package/templates/variants/go-chi/internal/config/config.go +23 -0
  47. package/templates/variants/go-chi/internal/connectapi/handler.go +79 -0
  48. package/templates/variants/go-chi/internal/httpapi/routes.go +93 -0
  49. package/templates/variants/go-chi/internal/vault/client.go +148 -0
  50. package/templates/variants/go-chi/package.json +16 -0
  51. package/templates/variants/go-chi/protos/dns/v1/dns.proto +58 -0
  52. package/templates/variants/go-chi/test/go.test.ts +19 -0
  53. package/templates/variants/go-connectrpc/Dockerfile +23 -0
  54. package/templates/variants/go-connectrpc/buf.gen.yaml +10 -0
  55. package/templates/variants/go-connectrpc/buf.yaml +9 -0
  56. package/templates/variants/go-connectrpc/cmd/server/main.go +51 -0
  57. package/templates/variants/go-connectrpc/gen/dns/v1/dns.pb.go +623 -0
  58. package/templates/variants/go-connectrpc/gen/dns/v1/dnsv1connect/dns.connect.go +192 -0
  59. package/templates/variants/go-connectrpc/go.mod +10 -0
  60. package/templates/variants/go-connectrpc/internal/app/service.go +109 -0
  61. package/templates/variants/go-connectrpc/internal/app/token_source.go +50 -0
  62. package/templates/variants/go-connectrpc/internal/cloudflare/client.go +160 -0
  63. package/templates/variants/go-connectrpc/internal/config/config.go +23 -0
  64. package/templates/variants/go-connectrpc/internal/connectapi/handler.go +79 -0
  65. package/templates/variants/go-connectrpc/internal/httpapi/routes.go +93 -0
  66. package/templates/variants/go-connectrpc/internal/vault/client.go +148 -0
  67. package/templates/variants/go-connectrpc/package.json +16 -0
  68. package/templates/variants/go-connectrpc/protos/dns/v1/dns.proto +58 -0
  69. package/templates/variants/go-connectrpc/test/go.test.ts +19 -0
package/src/scaffold.ts CHANGED
@@ -1,18 +1,31 @@
1
1
  import { mkdir, readdir } from "node:fs/promises";
2
2
  import { dirname, join, resolve } from "node:path";
3
+ import {
4
+ compactIdentifier,
5
+ type Framework,
6
+ type GcpProjectMode,
7
+ type Runtime,
8
+ } from "./naming";
3
9
 
4
10
  export type ScaffoldConfig = {
5
11
  directory: string;
6
12
  serviceName: string;
7
- modulePath: string;
8
- projectId: string;
13
+ runtime: Runtime;
14
+ framework: Framework;
9
15
  region: string;
16
+ gcpProjectMode: GcpProjectMode;
17
+ gcpProject: string;
18
+ gcpProjectName: string;
19
+ billingAccount: string;
20
+ quotaProjectId: string;
10
21
  githubRepo: string;
11
- vaultAddr: string;
12
- vaultSecretPath: string;
13
- vaultSecretKey: string;
14
- cloudflareZoneId: string;
15
- bufModule: string;
22
+ githubVisibility: "public" | "private";
23
+ createGithubRepo: boolean;
24
+ autoDeploy: boolean;
25
+ neonProjectId: string;
26
+ neonBaseBranchId: string;
27
+ neonBaseBranchName: string;
28
+ neonDatabaseName: string;
16
29
  generatorRoot: string;
17
30
  };
18
31
 
@@ -21,17 +34,21 @@ export async function scaffoldProject(config: ScaffoldConfig) {
21
34
  await ensureTargetDirectory(targetDir);
22
35
 
23
36
  const replacements = buildReplacements(config);
24
- const templateRoot = resolve(config.generatorRoot, "templates", "root");
25
- const files = await collectTemplateFiles(templateRoot);
37
+ const sharedTemplateRoot = resolve(config.generatorRoot, "templates", "shared");
38
+ const variantTemplateRoot = resolve(config.generatorRoot, "templates", "variants", `${config.runtime}-${config.framework}`);
26
39
 
27
- for (const relativePath of files) {
28
- const sourcePath = join(templateRoot, relativePath);
29
- const destinationPath = join(targetDir, relativePath);
30
- const raw = await Bun.file(sourcePath).text();
31
- const rendered = renderTemplate(raw, replacements);
40
+ for (const templateRoot of [sharedTemplateRoot, variantTemplateRoot]) {
41
+ const files = await collectTemplateFiles(templateRoot);
32
42
 
33
- await mkdir(dirname(destinationPath), { recursive: true });
34
- await Bun.write(destinationPath, rendered);
43
+ for (const relativePath of files) {
44
+ const sourcePath = join(templateRoot, relativePath);
45
+ const destinationPath = join(targetDir, relativePath);
46
+ const raw = await Bun.file(sourcePath).text();
47
+ const rendered = renderTemplate(raw, replacements);
48
+
49
+ await mkdir(dirname(destinationPath), { recursive: true });
50
+ await Bun.write(destinationPath, rendered);
51
+ }
35
52
  }
36
53
  }
37
54
 
@@ -68,31 +85,43 @@ async function collectTemplateFiles(root: string, relative = ""): Promise<string
68
85
  }
69
86
 
70
87
  function buildReplacements(config: ScaffoldConfig) {
71
- const [repoOwner = "anmho"] = config.githubRepo.split("/");
88
+ const [githubOwner = "anmho"] = config.githubRepo.split("/");
89
+ const modulePath = `github.com/${config.githubRepo}`;
72
90
  const serviceAccountBase = compactIdentifier(config.serviceName, 21);
73
- const runtimeServiceAccount = `${serviceAccountBase}-runtime@${config.projectId}.iam.gserviceaccount.com`;
74
- const deployerServiceAccount = `${serviceAccountBase}-deployer@${config.projectId}.iam.gserviceaccount.com`;
75
- const vaultRoleIdSecret = `${config.serviceName}-vault-role-id`;
76
- const vaultSecretIdSecret = `${config.serviceName}-vault-secret-id`;
91
+ const runtimeServiceAccount = `${serviceAccountBase}-runtime@${config.gcpProject}.iam.gserviceaccount.com`;
92
+ const deployerServiceAccount = `${serviceAccountBase}-deployer@${config.gcpProject}.iam.gserviceaccount.com`;
77
93
  const wifPoolId = "github";
78
94
  const wifProviderId = compactIdentifier(config.serviceName, 32);
95
+ const previewBranchPrefix = `${config.serviceName}-pr`;
96
+ const personalBranchPrefix = `${config.serviceName}-dev`;
79
97
 
80
98
  return {
81
99
  SERVICE_NAME: config.serviceName,
82
- MODULE_PATH: config.modulePath,
83
- PROJECT_ID: config.projectId,
100
+ MODULE_PATH: modulePath,
101
+ PROJECT_ID: config.gcpProject,
102
+ PROJECT_NAME: config.gcpProjectName,
84
103
  REGION: config.region,
104
+ GCP_PROJECT_MODE: config.gcpProjectMode,
105
+ PROJECT_CREATE_IF_MISSING: String(config.gcpProjectMode === "create_new"),
106
+ BILLING_ACCOUNT: config.billingAccount,
107
+ QUOTA_PROJECT_ID: config.quotaProjectId,
85
108
  GITHUB_REPO: config.githubRepo,
86
- GITHUB_OWNER: repoOwner,
87
- VAULT_ADDR: config.vaultAddr,
88
- VAULT_SECRET_PATH: config.vaultSecretPath,
89
- VAULT_SECRET_KEY: config.vaultSecretKey,
90
- CLOUDFLARE_ZONE_ID: config.cloudflareZoneId,
91
- BUF_MODULE: config.bufModule,
109
+ GITHUB_OWNER: githubOwner,
110
+ GITHUB_VISIBILITY: config.githubVisibility,
111
+ GITHUB_CREATE_IF_MISSING: String(config.createGithubRepo),
112
+ AUTO_DEPLOY: String(config.autoDeploy),
113
+ RUNTIME: config.runtime,
114
+ FRAMEWORK: config.framework,
115
+ CLOUD_RUN_SERVICE: config.serviceName,
116
+ NEON_PROJECT_ID: config.neonProjectId,
117
+ NEON_BASE_BRANCH_ID: config.neonBaseBranchId,
118
+ NEON_BASE_BRANCH_NAME: config.neonBaseBranchName,
119
+ NEON_DATABASE_NAME: config.neonDatabaseName,
120
+ NEON_ROLE_NAME: "neondb_owner",
121
+ NEON_PREVIEW_BRANCH_PREFIX: previewBranchPrefix,
122
+ NEON_PERSONAL_BRANCH_PREFIX: personalBranchPrefix,
92
123
  RUNTIME_SERVICE_ACCOUNT: runtimeServiceAccount,
93
124
  DEPLOYER_SERVICE_ACCOUNT: deployerServiceAccount,
94
- VAULT_ROLE_ID_SECRET: vaultRoleIdSecret,
95
- VAULT_SECRET_ID_SECRET: vaultSecretIdSecret,
96
125
  WIF_POOL_ID: wifPoolId,
97
126
  WIF_PROVIDER_ID: wifProviderId,
98
127
  };
@@ -107,27 +136,3 @@ function renderTemplate(input: string, replacements: Record<string, string>) {
107
136
  return replacement;
108
137
  });
109
138
  }
110
-
111
- function compactIdentifier(value: string, maxLength: number) {
112
- const normalized = value
113
- .toLowerCase()
114
- .replace(/[^a-z0-9-]+/g, "-")
115
- .replace(/^-+|-+$/g, "");
116
-
117
- if (normalized.length <= maxLength) {
118
- return normalized || "service";
119
- }
120
-
121
- const hash = shortHash(normalized);
122
- const head = normalized.slice(0, Math.max(1, maxLength - hash.length - 1)).replace(/-+$/g, "");
123
- return `${head}-${hash}`;
124
- }
125
-
126
- function shortHash(value: string) {
127
- let hash = 2166136261;
128
- for (let i = 0; i < value.length; i += 1) {
129
- hash ^= value.charCodeAt(i);
130
- hash = Math.imul(hash, 16777619);
131
- }
132
- return (hash >>> 0).toString(16).slice(0, 8);
133
- }
@@ -0,0 +1,22 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: oven-sh/setup-bun@v2
15
+ - if: ${{ '{{RUNTIME}}' == 'go' }}
16
+ uses: actions/setup-go@v5
17
+ with:
18
+ go-version: "1.25"
19
+ - run: bun install
20
+ - run: bun lint
21
+ - run: bun test
22
+
@@ -0,0 +1,30 @@
1
+ name: Deploy
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: read
10
+ id-token: write
11
+
12
+ jobs:
13
+ deploy:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: oven-sh/setup-bun@v2
18
+ - if: ${{ '{{RUNTIME}}' == 'go' }}
19
+ uses: actions/setup-go@v5
20
+ with:
21
+ go-version: "1.25"
22
+ - uses: google-github-actions/auth@v2
23
+ with:
24
+ workload_identity_provider: ${{ vars.GCP_WIF_PROVIDER }}
25
+ service_account: ${{ vars.GCP_DEPLOYER_SERVICE_ACCOUNT }}
26
+ - uses: google-github-actions/setup-gcloud@v2
27
+ - run: bun install
28
+ - run: bun run deploy -- --ci
29
+ env:
30
+ NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
@@ -0,0 +1,41 @@
1
+ name: Personal Environment
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ slug:
7
+ description: Personal environment slug
8
+ required: true
9
+ destroy:
10
+ description: Destroy the environment instead of deploying it
11
+ required: false
12
+ type: boolean
13
+
14
+ permissions:
15
+ contents: read
16
+ id-token: write
17
+
18
+ jobs:
19
+ deploy:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - uses: oven-sh/setup-bun@v2
24
+ - if: ${{ '{{RUNTIME}}' == 'go' }}
25
+ uses: actions/setup-go@v5
26
+ with:
27
+ go-version: "1.25"
28
+ - uses: google-github-actions/auth@v2
29
+ with:
30
+ workload_identity_provider: ${{ vars.GCP_WIF_PROVIDER }}
31
+ service_account: ${{ vars.GCP_DEPLOYER_SERVICE_ACCOUNT }}
32
+ - uses: google-github-actions/setup-gcloud@v2
33
+ - run: bun install
34
+ - run: |
35
+ if [ "${{ inputs.destroy }}" = "true" ]; then
36
+ bun run deploy -- --ci --destroy --environment personal --name "${{ inputs.slug }}"
37
+ else
38
+ bun run deploy -- --ci --environment personal --name "${{ inputs.slug }}"
39
+ fi
40
+ env:
41
+ NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
@@ -0,0 +1,25 @@
1
+ name: Preview Cleanup
2
+
3
+ on:
4
+ pull_request:
5
+ types: [closed]
6
+
7
+ permissions:
8
+ contents: read
9
+ id-token: write
10
+
11
+ jobs:
12
+ cleanup:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: oven-sh/setup-bun@v2
17
+ - uses: google-github-actions/auth@v2
18
+ with:
19
+ workload_identity_provider: ${{ vars.GCP_WIF_PROVIDER }}
20
+ service_account: ${{ vars.GCP_DEPLOYER_SERVICE_ACCOUNT }}
21
+ - uses: google-github-actions/setup-gcloud@v2
22
+ - run: bun install
23
+ - run: bun run deploy -- --ci --destroy --environment preview --name ${{ github.event.pull_request.number }}
24
+ env:
25
+ NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
@@ -0,0 +1,29 @@
1
+ name: Preview
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, reopened]
6
+
7
+ permissions:
8
+ contents: read
9
+ id-token: write
10
+
11
+ jobs:
12
+ deploy:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: oven-sh/setup-bun@v2
17
+ - if: ${{ '{{RUNTIME}}' == 'go' }}
18
+ uses: actions/setup-go@v5
19
+ with:
20
+ go-version: "1.25"
21
+ - uses: google-github-actions/auth@v2
22
+ with:
23
+ workload_identity_provider: ${{ vars.GCP_WIF_PROVIDER }}
24
+ service_account: ${{ vars.GCP_DEPLOYER_SERVICE_ACCOUNT }}
25
+ - uses: google-github-actions/setup-gcloud@v2
26
+ - run: bun install
27
+ - run: bun run deploy -- --ci --environment preview --name ${{ github.event.pull_request.number }}
28
+ env:
29
+ NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
@@ -0,0 +1,37 @@
1
+ # {{SERVICE_NAME}}
2
+
3
+ Generated by `create-svc`.
4
+
5
+ This scaffold targets `{{RUNTIME}} + {{FRAMEWORK}}` on Cloud Run with:
6
+
7
+ - one generated `service.yaml` manifest
8
+ - Bun-based `bootstrap` and `deploy` helpers
9
+ - GitHub Actions for CI, `main` deploys, PR previews, and personal environments
10
+ - GCP project bootstrap with billing and quota-project-aware `gcloud` calls
11
+ - Neon main, preview, and personal branch provisioning
12
+
13
+ ## Commands
14
+
15
+ ```bash
16
+ bun dev
17
+ bun gen
18
+ bun lint
19
+ bun test
20
+ bun run bootstrap
21
+ bun run deploy
22
+ bun run deploy -- --environment personal --name <slug>
23
+ bun run deploy -- --destroy --environment personal --name <slug>
24
+ ```
25
+
26
+ ## Configuration
27
+
28
+ The generated Cloud Run config lives in [scripts/cloudrun/config.ts](scripts/cloudrun/config.ts).
29
+
30
+ Bootstrap and deploy use:
31
+
32
+ - `gcloud`
33
+ - `gh`
34
+ - `NEON_API_KEY`
35
+
36
+ The generator only stores application-facing connection material in Secret Manager. Neon admin credentials stay local to bootstrap and deploy.
37
+
@@ -0,0 +1,76 @@
1
+ import { config, githubVariables } from "./config";
2
+ import { ensureDatabase, getConnectionUri } from "./neon";
3
+ import {
4
+ addSecretVersion,
5
+ attachBilling,
6
+ ensureArtifactRepository,
7
+ ensureProject,
8
+ ensureProjectRole,
9
+ ensureSecretAccessor,
10
+ ensureServiceAccount,
11
+ ensureServiceAccountRole,
12
+ ensureWorkloadIdentityPool,
13
+ ensureWorkloadIdentityProvider,
14
+ gcloud,
15
+ requireCommand,
16
+ resolveDeploymentTarget,
17
+ setGithubVariable,
18
+ workloadIdentityPoolResource,
19
+ workloadIdentityProviderResource,
20
+ } from "./lib";
21
+
22
+ export async function bootstrap() {
23
+ requireCommand("gcloud");
24
+ requireCommand("gh");
25
+
26
+ ensureProject();
27
+ attachBilling();
28
+ gcloud(["services", "enable", ...config.requiredApis, "--project", config.project.id]);
29
+
30
+ ensureServiceAccount(config.runtimeServiceAccount);
31
+ ensureServiceAccount(config.deployerServiceAccount);
32
+ ensureArtifactRepository();
33
+
34
+ ensureProjectRole(`serviceAccount:${config.deployerServiceAccount}`, "roles/run.admin");
35
+ ensureProjectRole(`serviceAccount:${config.deployerServiceAccount}`, "roles/cloudbuild.builds.editor");
36
+ ensureProjectRole(`serviceAccount:${config.deployerServiceAccount}`, "roles/artifactregistry.writer");
37
+ ensureProjectRole(`serviceAccount:${config.deployerServiceAccount}`, "roles/serviceusage.serviceUsageConsumer");
38
+ ensureProjectRole(`serviceAccount:${config.runtimeServiceAccount}`, "roles/secretmanager.secretAccessor");
39
+
40
+ ensureServiceAccountRole(config.runtimeServiceAccount, `serviceAccount:${config.deployerServiceAccount}`, "roles/iam.serviceAccountUser");
41
+
42
+ ensureWorkloadIdentityPool();
43
+ ensureWorkloadIdentityProvider();
44
+ ensureServiceAccountRole(
45
+ config.deployerServiceAccount,
46
+ `principalSet://iam.googleapis.com/${workloadIdentityPoolResource()}/attribute.repository/${config.github.repo}`,
47
+ "roles/iam.workloadIdentityUser"
48
+ );
49
+
50
+ if (!config.neon.projectId || !config.neon.baseBranchId) {
51
+ throw new Error("Neon project and base branch must be configured before bootstrap");
52
+ }
53
+
54
+ const target = resolveDeploymentTarget("main");
55
+ await ensureDatabase(config.neon.projectId, config.neon.baseBranchId, config.neon.databaseName);
56
+ const connectionUri = await getConnectionUri(
57
+ config.neon.projectId,
58
+ config.neon.baseBranchId,
59
+ config.neon.databaseName,
60
+ config.neon.roleName
61
+ );
62
+ addSecretVersion(target.databaseSecretName, connectionUri);
63
+ ensureSecretAccessor(target.databaseSecretName, `serviceAccount:${config.runtimeServiceAccount}`);
64
+
65
+ for (const [name, value] of Object.entries(githubVariables)) {
66
+ setGithubVariable(name, value);
67
+ }
68
+
69
+ setGithubVariable("GCP_WIF_PROVIDER", workloadIdentityProviderResource());
70
+ setGithubVariable("GCP_DEPLOYER_SERVICE_ACCOUNT", config.deployerServiceAccount);
71
+ }
72
+
73
+ if (import.meta.main) {
74
+ await bootstrap();
75
+ }
76
+
@@ -0,0 +1,57 @@
1
+ export const config = {
2
+ serviceName: "{{SERVICE_NAME}}",
3
+ runtime: "{{RUNTIME}}",
4
+ framework: "{{FRAMEWORK}}",
5
+ region: "{{REGION}}",
6
+ artifactRepository: "cloud-run",
7
+ runtimeServiceAccount: "{{RUNTIME_SERVICE_ACCOUNT}}",
8
+ deployerServiceAccount: "{{DEPLOYER_SERVICE_ACCOUNT}}",
9
+ workloadIdentityPoolId: "{{WIF_POOL_ID}}",
10
+ workloadIdentityProviderId: "{{WIF_PROVIDER_ID}}",
11
+ project: {
12
+ mode: "{{GCP_PROJECT_MODE}}",
13
+ id: "{{PROJECT_ID}}",
14
+ name: "{{PROJECT_NAME}}",
15
+ createIfMissing: {{PROJECT_CREATE_IF_MISSING}},
16
+ billingAccount: "{{BILLING_ACCOUNT}}",
17
+ quotaProjectId: "{{QUOTA_PROJECT_ID}}",
18
+ },
19
+ github: {
20
+ repo: "{{GITHUB_REPO}}",
21
+ visibility: "{{GITHUB_VISIBILITY}}",
22
+ createIfMissing: {{GITHUB_CREATE_IF_MISSING}},
23
+ },
24
+ neon: {
25
+ projectId: "{{NEON_PROJECT_ID}}",
26
+ baseBranchId: "{{NEON_BASE_BRANCH_ID}}",
27
+ baseBranchName: "{{NEON_BASE_BRANCH_NAME}}",
28
+ databaseName: "{{NEON_DATABASE_NAME}}",
29
+ roleName: "{{NEON_ROLE_NAME}}",
30
+ previewBranchPrefix: "{{NEON_PREVIEW_BRANCH_PREFIX}}",
31
+ personalBranchPrefix: "{{NEON_PERSONAL_BRANCH_PREFIX}}",
32
+ },
33
+ requiredApis: [
34
+ "run.googleapis.com",
35
+ "cloudbuild.googleapis.com",
36
+ "artifactregistry.googleapis.com",
37
+ "iam.googleapis.com",
38
+ "iamcredentials.googleapis.com",
39
+ "secretmanager.googleapis.com",
40
+ "serviceusage.googleapis.com",
41
+ "sts.googleapis.com",
42
+ ],
43
+ } as const;
44
+
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
+ export type DeployEnvironment = "main" | "preview" | "personal";
57
+
@@ -0,0 +1,82 @@
1
+ import { bootstrap } from "./bootstrap";
2
+ import { config } from "./config";
3
+ import { deleteBranch, ensureBranch, ensureDatabase, getConnectionUri, listBranches } from "./neon";
4
+ import {
5
+ addSecretVersion,
6
+ deleteService,
7
+ ensureArtifactRepository,
8
+ ensureSecretAccessor,
9
+ gcloud,
10
+ imageUrl,
11
+ parseDeployArgs,
12
+ requireCommand,
13
+ resolveDeploymentTarget,
14
+ serviceUrl,
15
+ writeRenderedManifest,
16
+ } from "./lib";
17
+
18
+ export async function deploy(args = Bun.argv.slice(2)) {
19
+ requireCommand("gcloud");
20
+ requireCommand("bun");
21
+
22
+ const options = parseDeployArgs(args);
23
+ if (!options.ci) {
24
+ await bootstrap();
25
+ }
26
+
27
+ const target = resolveDeploymentTarget(options.environment, options.name);
28
+
29
+ if (options.destroy) {
30
+ if (options.environment === "main") {
31
+ throw new Error("Refusing to destroy the main environment");
32
+ }
33
+
34
+ deleteService(target.serviceName);
35
+
36
+ const branches = await listBranches(config.neon.projectId);
37
+ const branch = branches.find((candidate) => candidate.name === target.branchName);
38
+ if (branch) {
39
+ await deleteBranch(config.neon.projectId, branch.id);
40
+ }
41
+ return;
42
+ }
43
+
44
+ ensureArtifactRepository();
45
+
46
+ let branchId = config.neon.baseBranchId;
47
+ if (options.environment !== "main") {
48
+ const branch = await ensureBranch(config.neon.projectId, target.branchName, config.neon.baseBranchId);
49
+ branchId = branch.id;
50
+ }
51
+
52
+ await ensureDatabase(config.neon.projectId, branchId, config.neon.databaseName);
53
+ const connectionUri = await getConnectionUri(config.neon.projectId, branchId, config.neon.databaseName, config.neon.roleName);
54
+ addSecretVersion(target.databaseSecretName, connectionUri);
55
+ ensureSecretAccessor(target.databaseSecretName, `serviceAccount:${config.runtimeServiceAccount}`);
56
+
57
+ const image = imageUrl();
58
+ gcloud(["builds", "submit", "--project", config.project.id, "--region", config.region, "--tag", image]);
59
+
60
+ const renderedManifestPath = await writeRenderedManifest(image, target);
61
+ gcloud(["run", "services", "replace", renderedManifestPath.pathname, "--project", config.project.id, "--region", config.region]);
62
+ gcloud([
63
+ "run",
64
+ "services",
65
+ "add-iam-policy-binding",
66
+ target.serviceName,
67
+ "--project",
68
+ config.project.id,
69
+ "--region",
70
+ config.region,
71
+ "--member",
72
+ "allUsers",
73
+ "--role",
74
+ "roles/run.invoker",
75
+ ]);
76
+
77
+ console.log(serviceUrl(target.serviceName));
78
+ }
79
+
80
+ if (import.meta.main) {
81
+ await deploy();
82
+ }