create-svc 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -1
- package/src/cli.ts +328 -108
- package/src/gcp.test.ts +71 -0
- package/src/gcp.ts +97 -0
- package/src/naming.test.ts +37 -0
- package/src/naming.ts +103 -0
- package/src/neon.test.ts +48 -0
- package/src/neon.ts +76 -0
- package/src/post-scaffold.ts +77 -0
- package/src/scaffold.test.ts +66 -31
- package/src/scaffold.ts +60 -55
- package/templates/shared/.github/workflows/ci.yml +22 -0
- package/templates/shared/.github/workflows/deploy.yml +30 -0
- package/templates/shared/.github/workflows/personal.yml +41 -0
- package/templates/shared/.github/workflows/preview-cleanup.yml +25 -0
- package/templates/shared/.github/workflows/preview.yml +29 -0
- package/templates/shared/README.md +37 -0
- package/templates/shared/scripts/cloudrun/bootstrap.ts +76 -0
- package/templates/shared/scripts/cloudrun/config.ts +57 -0
- package/templates/shared/scripts/cloudrun/deploy.ts +82 -0
- package/templates/shared/scripts/cloudrun/lib.ts +380 -0
- package/templates/shared/scripts/cloudrun/neon.ts +104 -0
- package/templates/shared/service.yaml +28 -0
- package/templates/variants/bun-connectrpc/Dockerfile +13 -0
- package/templates/variants/bun-connectrpc/package.json +20 -0
- package/templates/variants/bun-connectrpc/scripts/codegen.ts +1 -0
- package/templates/variants/bun-connectrpc/src/index.ts +32 -0
- package/templates/variants/bun-connectrpc/test/app.test.ts +17 -0
- package/templates/variants/bun-connectrpc/tsconfig.json +10 -0
- package/templates/variants/bun-hono/Dockerfile +13 -0
- package/templates/variants/bun-hono/package.json +21 -0
- package/templates/variants/bun-hono/scripts/codegen.ts +1 -0
- package/templates/variants/bun-hono/src/index.ts +24 -0
- package/templates/variants/bun-hono/test/app.test.ts +12 -0
- package/templates/variants/bun-hono/tsconfig.json +10 -0
- package/templates/variants/go-chi/Dockerfile +23 -0
- package/templates/variants/go-chi/buf.gen.yaml +10 -0
- package/templates/variants/go-chi/buf.yaml +9 -0
- package/templates/variants/go-chi/cmd/server/main.go +52 -0
- package/templates/variants/go-chi/gen/dns/v1/dns.pb.go +623 -0
- package/templates/variants/go-chi/gen/dns/v1/dnsv1connect/dns.connect.go +192 -0
- package/templates/variants/go-chi/go.mod +10 -0
- package/templates/variants/go-chi/internal/app/service.go +109 -0
- package/templates/variants/go-chi/internal/app/token_source.go +50 -0
- package/templates/variants/go-chi/internal/cloudflare/client.go +160 -0
- package/templates/variants/go-chi/internal/config/config.go +23 -0
- package/templates/variants/go-chi/internal/connectapi/handler.go +79 -0
- package/templates/variants/go-chi/internal/httpapi/routes.go +93 -0
- package/templates/variants/go-chi/internal/vault/client.go +148 -0
- package/templates/variants/go-chi/package.json +16 -0
- package/templates/variants/go-chi/protos/dns/v1/dns.proto +58 -0
- package/templates/variants/go-chi/test/go.test.ts +19 -0
- package/templates/variants/go-connectrpc/Dockerfile +23 -0
- package/templates/variants/go-connectrpc/buf.gen.yaml +10 -0
- package/templates/variants/go-connectrpc/buf.yaml +9 -0
- package/templates/variants/go-connectrpc/cmd/server/main.go +51 -0
- package/templates/variants/go-connectrpc/gen/dns/v1/dns.pb.go +623 -0
- package/templates/variants/go-connectrpc/gen/dns/v1/dnsv1connect/dns.connect.go +192 -0
- package/templates/variants/go-connectrpc/go.mod +10 -0
- package/templates/variants/go-connectrpc/internal/app/service.go +109 -0
- package/templates/variants/go-connectrpc/internal/app/token_source.go +50 -0
- package/templates/variants/go-connectrpc/internal/cloudflare/client.go +160 -0
- package/templates/variants/go-connectrpc/internal/config/config.go +23 -0
- package/templates/variants/go-connectrpc/internal/connectapi/handler.go +79 -0
- package/templates/variants/go-connectrpc/internal/httpapi/routes.go +93 -0
- package/templates/variants/go-connectrpc/internal/vault/client.go +148 -0
- package/templates/variants/go-connectrpc/package.json +16 -0
- package/templates/variants/go-connectrpc/protos/dns/v1/dns.proto +58 -0
- 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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
25
|
-
const
|
|
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
|
|
28
|
-
const
|
|
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
|
-
|
|
34
|
-
|
|
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 [
|
|
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.
|
|
74
|
-
const deployerServiceAccount = `${serviceAccountBase}-deployer@${config.
|
|
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:
|
|
83
|
-
PROJECT_ID: config.
|
|
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:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
}
|