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
|
@@ -3,11 +3,31 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { config } from "./config";
|
|
5
5
|
|
|
6
|
+
type NeonProject = {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
6
11
|
type NeonBranch = {
|
|
7
12
|
id: string;
|
|
8
13
|
name: string;
|
|
9
14
|
};
|
|
10
15
|
|
|
16
|
+
type NeonDatabase = {
|
|
17
|
+
name: string;
|
|
18
|
+
ownerName: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type ResolvedNeonConfig = {
|
|
22
|
+
projectId: string;
|
|
23
|
+
baseBranchId: string;
|
|
24
|
+
baseBranchName: string;
|
|
25
|
+
databaseName: string;
|
|
26
|
+
roleName: string;
|
|
27
|
+
previewBranchPrefix: string;
|
|
28
|
+
personalBranchPrefix: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
11
31
|
async function resolveNeonApiKey() {
|
|
12
32
|
const direct = process.env.NEON_API_KEY?.trim();
|
|
13
33
|
if (direct) {
|
|
@@ -17,8 +37,8 @@ async function resolveNeonApiKey() {
|
|
|
17
37
|
const addr = process.env.VAULT_ADDR?.trim() ?? "";
|
|
18
38
|
const token = await resolveVaultToken();
|
|
19
39
|
const mount = process.env.VAULT_SECRET_MOUNT?.trim() ?? "secret";
|
|
20
|
-
const path = process.env.VAULT_NEON_API_KEY_PATH?.trim() ?? "
|
|
21
|
-
const field = process.env.VAULT_NEON_API_KEY_FIELD?.trim() ?? "
|
|
40
|
+
const path = process.env.VAULT_NEON_API_KEY_PATH?.trim() ?? "prod/providers/neon";
|
|
41
|
+
const field = process.env.VAULT_NEON_API_KEY_FIELD?.trim() ?? "api_key";
|
|
22
42
|
|
|
23
43
|
if (!addr || !token) {
|
|
24
44
|
throw new Error("NEON_API_KEY is required for Neon provisioning, or set VAULT_ADDR with VAULT_TOKEN, VAULT_TOKEN_FILE, or ~/.vault-token");
|
|
@@ -57,7 +77,7 @@ async function resolveVaultToken() {
|
|
|
57
77
|
return direct;
|
|
58
78
|
}
|
|
59
79
|
|
|
60
|
-
const tokenFile = process.env.VAULT_TOKEN_FILE?.trim() || join(homedir(), ".vault-token");
|
|
80
|
+
const tokenFile = process.env.VAULT_TOKEN_FILE?.trim() || join(process.env.HOME?.trim() || homedir(), ".vault-token");
|
|
61
81
|
|
|
62
82
|
try {
|
|
63
83
|
return (await Bun.file(tokenFile).text()).trim();
|
|
@@ -71,15 +91,80 @@ async function neonClient() {
|
|
|
71
91
|
return createApiClient({ apiKey });
|
|
72
92
|
}
|
|
73
93
|
|
|
94
|
+
export async function listProjects() {
|
|
95
|
+
const payload = await (await neonClient()).listProjects({ limit: 100 });
|
|
96
|
+
const projects = ((payload.data as { projects?: Array<{ id?: string; name?: string }> } | undefined)?.projects ?? []);
|
|
97
|
+
return projects
|
|
98
|
+
.map((project: { id?: string; name?: string }) => ({
|
|
99
|
+
id: project.id ?? "",
|
|
100
|
+
name: project.name ?? project.id ?? "",
|
|
101
|
+
}))
|
|
102
|
+
.filter((project: NeonProject): project is NeonProject => Boolean(project.id))
|
|
103
|
+
.sort((left: NeonProject, right: NeonProject) => left.name.localeCompare(right.name));
|
|
104
|
+
}
|
|
105
|
+
|
|
74
106
|
export async function listBranches(projectId: string) {
|
|
75
107
|
const payload = await (await neonClient()).listProjectBranches({ projectId });
|
|
76
|
-
|
|
77
|
-
|
|
108
|
+
const branches = ((payload.data as { branches?: Array<{ id?: string; name?: string }> } | undefined)?.branches ?? []);
|
|
109
|
+
return branches
|
|
110
|
+
.map((branch: { id?: string; name?: string }) => ({
|
|
78
111
|
id: branch.id ?? "",
|
|
79
112
|
name: branch.name ?? branch.id ?? "",
|
|
80
113
|
}))
|
|
81
|
-
.filter((branch): branch is NeonBranch => Boolean(branch.id))
|
|
82
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
114
|
+
.filter((branch: NeonBranch): branch is NeonBranch => Boolean(branch.id))
|
|
115
|
+
.sort((left: NeonBranch, right: NeonBranch) => left.name.localeCompare(right.name));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function listDatabases(projectId: string, branchId: string) {
|
|
119
|
+
const payload = await (await neonClient()).listProjectBranchDatabases(projectId, branchId);
|
|
120
|
+
const databases = ((payload.data as { databases?: Array<{ name?: string; owner_name?: string }> } | undefined)?.databases ?? []);
|
|
121
|
+
return databases
|
|
122
|
+
.map((database: { name?: string; owner_name?: string }) => ({
|
|
123
|
+
name: database.name ?? "",
|
|
124
|
+
ownerName: database.owner_name ?? "",
|
|
125
|
+
}))
|
|
126
|
+
.filter((database: NeonDatabase): database is NeonDatabase => Boolean(database.name))
|
|
127
|
+
.sort((left: NeonDatabase, right: NeonDatabase) => left.name.localeCompare(right.name));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function resolveNeonConfig(): Promise<ResolvedNeonConfig> {
|
|
131
|
+
const configuredProjectId = config.neon.projectId.trim();
|
|
132
|
+
const configuredBaseBranchId = config.neon.baseBranchId.trim();
|
|
133
|
+
const configuredBaseBranchName = config.neon.baseBranchName.trim() || "main";
|
|
134
|
+
|
|
135
|
+
if (configuredProjectId && configuredBaseBranchId) {
|
|
136
|
+
return {
|
|
137
|
+
projectId: configuredProjectId,
|
|
138
|
+
baseBranchId: configuredBaseBranchId,
|
|
139
|
+
baseBranchName: configuredBaseBranchName,
|
|
140
|
+
databaseName: config.neon.databaseName,
|
|
141
|
+
roleName: config.neon.roleName,
|
|
142
|
+
previewBranchPrefix: config.neon.previewBranchPrefix,
|
|
143
|
+
personalBranchPrefix: config.neon.personalBranchPrefix,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const projects = await listProjects();
|
|
148
|
+
const project = projects[0];
|
|
149
|
+
if (!project) {
|
|
150
|
+
throw new Error(`No Neon projects are available for ${config.serviceName}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const branches = await listBranches(project.id);
|
|
154
|
+
const branch = branches.find((candidate) => candidate.name === configuredBaseBranchName) ?? branches[0];
|
|
155
|
+
if (!branch) {
|
|
156
|
+
throw new Error(`No Neon branches are available in project ${project.id}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
projectId: project.id,
|
|
161
|
+
baseBranchId: branch.id,
|
|
162
|
+
baseBranchName: branch.name,
|
|
163
|
+
databaseName: config.neon.databaseName,
|
|
164
|
+
roleName: config.neon.roleName,
|
|
165
|
+
previewBranchPrefix: config.neon.previewBranchPrefix,
|
|
166
|
+
personalBranchPrefix: config.neon.personalBranchPrefix,
|
|
167
|
+
};
|
|
83
168
|
}
|
|
84
169
|
|
|
85
170
|
export async function ensureDatabase(projectId: string, branchId: string, databaseName: string) {
|
|
@@ -98,11 +183,13 @@ export async function ensureDatabase(projectId: string, branchId: string, databa
|
|
|
98
183
|
await client.createProjectBranchDatabase(projectId, branchId, {
|
|
99
184
|
database: {
|
|
100
185
|
name: databaseName,
|
|
186
|
+
owner_name: config.neon.roleName,
|
|
101
187
|
},
|
|
102
188
|
});
|
|
103
189
|
}
|
|
104
190
|
|
|
105
191
|
export async function deleteDatabase(projectId: string, branchId: string, databaseName: string) {
|
|
192
|
+
await assertDatabaseOwned(projectId, branchId, databaseName);
|
|
106
193
|
try {
|
|
107
194
|
await (await neonClient()).deleteProjectBranchDatabase(projectId, branchId, databaseName);
|
|
108
195
|
} catch (error) {
|
|
@@ -127,12 +214,12 @@ export async function ensureBranch(projectId: string, branchName: string, parent
|
|
|
127
214
|
},
|
|
128
215
|
endpoints: [
|
|
129
216
|
{
|
|
130
|
-
type: "read_write",
|
|
217
|
+
type: "read_write" as never,
|
|
131
218
|
},
|
|
132
219
|
],
|
|
133
220
|
});
|
|
134
221
|
|
|
135
|
-
const branch = payload.branch;
|
|
222
|
+
const branch = (payload.data as { branch?: { id?: string; name?: string } } | undefined)?.branch;
|
|
136
223
|
if (!branch?.id) {
|
|
137
224
|
throw new Error(`Neon did not return a branch for ${branchName}`);
|
|
138
225
|
}
|
|
@@ -144,6 +231,11 @@ export async function ensureBranch(projectId: string, branchName: string, parent
|
|
|
144
231
|
}
|
|
145
232
|
|
|
146
233
|
export async function deleteBranch(projectId: string, branchId: string) {
|
|
234
|
+
const branch = (await listBranches(projectId)).find((candidate) => candidate.id === branchId);
|
|
235
|
+
if (!branch) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
assertDisposableBranchName(branch.name);
|
|
147
239
|
try {
|
|
148
240
|
await (await neonClient()).deleteProjectBranch(projectId, branchId);
|
|
149
241
|
} catch (error) {
|
|
@@ -155,15 +247,37 @@ export async function deleteBranch(projectId: string, branchId: string) {
|
|
|
155
247
|
}
|
|
156
248
|
}
|
|
157
249
|
|
|
250
|
+
async function assertDatabaseOwned(projectId: string, branchId: string, databaseName: string) {
|
|
251
|
+
if (databaseName !== config.neon.databaseName) {
|
|
252
|
+
throw new Error(`Refusing to delete Neon database ${databaseName}; expected ${config.neon.databaseName}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const database = (await listDatabases(projectId, branchId)).find((candidate) => candidate.name === databaseName);
|
|
256
|
+
if (!database) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (database.ownerName && database.ownerName !== config.neon.roleName) {
|
|
261
|
+
throw new Error(`Refusing to delete Neon database ${databaseName}; owner is ${database.ownerName}, expected ${config.neon.roleName}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function assertDisposableBranchName(branchName: string) {
|
|
266
|
+
if (branchName.startsWith(`${config.neon.previewBranchPrefix}-`) || branchName.startsWith(`${config.neon.personalBranchPrefix}-`)) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
throw new Error(`Refusing to delete Neon branch ${branchName}; it is not owned by ${config.serviceName}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
158
272
|
export async function getConnectionUri(projectId: string, branchId: string, databaseName: string, roleName: string) {
|
|
159
273
|
const payload = await (await neonClient()).getConnectionUri({
|
|
160
274
|
projectId,
|
|
161
|
-
branchId,
|
|
162
|
-
databaseName,
|
|
163
|
-
roleName,
|
|
275
|
+
branch_id: branchId,
|
|
276
|
+
database_name: databaseName,
|
|
277
|
+
role_name: roleName,
|
|
164
278
|
});
|
|
165
279
|
|
|
166
|
-
const uri = payload.uri;
|
|
280
|
+
const uri = (payload.data as { uri?: string } | undefined)?.uri;
|
|
167
281
|
if (!uri) {
|
|
168
282
|
throw new Error(`Neon did not return a connection URI for ${databaseName} in ${config.serviceName}`);
|
|
169
283
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readLocalEnv } from "./local-env";
|
|
2
|
+
import { ensureLocalPostgres } from "./local-docker";
|
|
3
|
+
|
|
4
|
+
const command = Bun.argv.slice(2);
|
|
5
|
+
|
|
6
|
+
if (command.length === 0) {
|
|
7
|
+
throw new Error("Usage: bun run ./scripts/dev.ts <command...>");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
await ensureLocalPostgres();
|
|
11
|
+
|
|
12
|
+
const child = Bun.spawn(command, {
|
|
13
|
+
stdin: "inherit",
|
|
14
|
+
stdout: "inherit",
|
|
15
|
+
stderr: "inherit",
|
|
16
|
+
env: {
|
|
17
|
+
...Bun.env,
|
|
18
|
+
...(await readLocalEnv()),
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
process.exit(await child.exited);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export async function ensureLocalPostgres() {
|
|
2
|
+
await ensureDockerRunning();
|
|
3
|
+
await run(["docker", "compose", "up", "-d"], { label: "start local postgres" });
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
async function ensureDockerRunning() {
|
|
7
|
+
if (await dockerInfo()) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
await openDocker();
|
|
12
|
+
const deadline = Date.now() + 120_000;
|
|
13
|
+
|
|
14
|
+
while (Date.now() < deadline) {
|
|
15
|
+
if (await dockerInfo()) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
await Bun.sleep(2_000);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
throw new Error("Docker did not become ready within 120 seconds. Open Docker Desktop and retry.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function dockerInfo() {
|
|
25
|
+
const result = await Bun.spawn(["docker", "info"], {
|
|
26
|
+
stdout: "ignore",
|
|
27
|
+
stderr: "ignore",
|
|
28
|
+
}).exited;
|
|
29
|
+
return result === 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function openDocker() {
|
|
33
|
+
if (process.platform === "darwin") {
|
|
34
|
+
await run(["open", "-a", "Docker"], { label: "open Docker Desktop" });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (process.platform === "win32") {
|
|
39
|
+
await run(["powershell.exe", "-NoProfile", "-Command", "Start-Process 'Docker Desktop'"], {
|
|
40
|
+
label: "open Docker Desktop",
|
|
41
|
+
optional: true,
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await run(["systemctl", "--user", "start", "docker-desktop"], {
|
|
47
|
+
label: "open Docker Desktop",
|
|
48
|
+
optional: true,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function run(command: string[], options: { label: string; optional?: boolean }) {
|
|
53
|
+
const result = await Bun.spawn(command, {
|
|
54
|
+
stdin: "ignore",
|
|
55
|
+
stdout: "inherit",
|
|
56
|
+
stderr: "inherit",
|
|
57
|
+
env: Bun.env,
|
|
58
|
+
}).exited;
|
|
59
|
+
|
|
60
|
+
if (result !== 0 && !options.optional) {
|
|
61
|
+
throw new Error(`${options.label} failed with exit code ${result}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
export async function readLocalEnv() {
|
|
5
|
+
if (!existsSync(".env.local")) {
|
|
6
|
+
return {};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const values: Record<string, string> = {};
|
|
10
|
+
const text = await readFile(".env.local", "utf8");
|
|
11
|
+
for (const line of text.split(/\r?\n/)) {
|
|
12
|
+
const trimmed = line.trim();
|
|
13
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const separator = trimmed.indexOf("=");
|
|
18
|
+
if (separator === -1) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const key = trimmed.slice(0, separator).trim();
|
|
23
|
+
const rawValue = trimmed.slice(separator + 1).trim();
|
|
24
|
+
values[key] = rawValue.replace(/^['"]|['"]$/g, "");
|
|
25
|
+
}
|
|
26
|
+
return values;
|
|
27
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { SQL } from "bun";
|
|
4
|
+
|
|
5
|
+
const databaseUrl = Bun.env.DATABASE_URL?.trim();
|
|
6
|
+
if (!databaseUrl) {
|
|
7
|
+
throw new Error("DATABASE_URL is required");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const stage = normalizeStage(Bun.env.SERVICE_STAGE || Bun.env.APP_ENV || Bun.env.NODE_ENV || "local");
|
|
11
|
+
if (stage === "prod" && Bun.env.SEED_PROD !== "true") {
|
|
12
|
+
console.log("Skipping production seed data. Set SEED_PROD=true to apply production seeds.");
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const sql = new SQL(databaseUrl);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const entries = seedEntries(stage);
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
await sql`
|
|
22
|
+
insert into waitlist_entries (id, email, name, company, source, status)
|
|
23
|
+
values (${entry.id}, ${entry.email}, ${entry.name}, ${entry.company}, ${entry.source}, ${entry.status})
|
|
24
|
+
on conflict (email) do update set
|
|
25
|
+
name = excluded.name,
|
|
26
|
+
company = excluded.company,
|
|
27
|
+
source = excluded.source,
|
|
28
|
+
updated_at = now()
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
await sql`
|
|
32
|
+
insert into waitlist_triggers (id, type, entry_id, status, payload_json)
|
|
33
|
+
values (${`${entry.id}-trigger`}, ${"seed"}, ${entry.id}, ${"queued"}, ${JSON.stringify({ stage, email: entry.email })})
|
|
34
|
+
on conflict (id) do nothing
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(`Seeded ${entries.length} waitlist entr${entries.length === 1 ? "y" : "ies"} for ${stage}.`);
|
|
39
|
+
} finally {
|
|
40
|
+
await sql.close();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeStage(value: string) {
|
|
44
|
+
const normalized = value.trim().toLowerCase();
|
|
45
|
+
if (normalized === "production" || normalized === "main") {
|
|
46
|
+
return "prod";
|
|
47
|
+
}
|
|
48
|
+
if (normalized === "development") {
|
|
49
|
+
return "local";
|
|
50
|
+
}
|
|
51
|
+
return normalized || "local";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function seedEntries(stage: string) {
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
id: `seed-${stage}-founder`,
|
|
58
|
+
email: `founder+${stage}@example.com`,
|
|
59
|
+
name: "Founder Example",
|
|
60
|
+
company: "Example Co",
|
|
61
|
+
source: `seed:${stage}`,
|
|
62
|
+
status: "joined",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: `seed-${stage}-operator`,
|
|
66
|
+
email: `operator+${stage}@example.com`,
|
|
67
|
+
name: "Operator Example",
|
|
68
|
+
company: "Example Co",
|
|
69
|
+
source: `seed:${stage}`,
|
|
70
|
+
status: "joined",
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SQL } from "bun";
|
|
2
|
+
import { readLocalEnv } from "./local-env";
|
|
3
|
+
|
|
4
|
+
const env = {
|
|
5
|
+
...Bun.env,
|
|
6
|
+
...(await readLocalEnv()),
|
|
7
|
+
};
|
|
8
|
+
const databaseUrl = env.DATABASE_URL?.trim();
|
|
9
|
+
|
|
10
|
+
if (!databaseUrl) {
|
|
11
|
+
throw new Error("DATABASE_URL is required");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const client = new SQL(databaseUrl);
|
|
15
|
+
const deadline = Date.now() + 45_000;
|
|
16
|
+
let lastError: unknown;
|
|
17
|
+
|
|
18
|
+
while (Date.now() < deadline) {
|
|
19
|
+
try {
|
|
20
|
+
await client.unsafe("select 1");
|
|
21
|
+
process.exit(0);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
lastError = error;
|
|
24
|
+
await Bun.sleep(1_000);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw new Error(`Timed out waiting for Postgres: ${formatError(lastError)}`);
|
|
29
|
+
|
|
30
|
+
function formatError(error: unknown) {
|
|
31
|
+
return error instanceof Error ? error.message : String(error ?? "unknown error");
|
|
32
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
service_id: "{{SERVICE_ID}}",
|
|
3
|
+
target: "{{TARGET}}",
|
|
4
|
+
runtime: "{{RUNTIME}}",
|
|
5
|
+
framework: "{{FRAMEWORK}}",
|
|
6
|
+
stage_default: "prod",
|
|
7
|
+
dns: {
|
|
8
|
+
hostname: "{{API_HOSTNAME}}",
|
|
9
|
+
base_domain: "{{API_BASE_DOMAIN}}",
|
|
10
|
+
},
|
|
11
|
+
ownership: {
|
|
12
|
+
managed_by: "create-service",
|
|
13
|
+
service_id: "{{SERVICE_ID}}",
|
|
14
|
+
},
|
|
15
|
+
auth: {
|
|
16
|
+
issuer: "{{AUTH_ISSUER}}",
|
|
17
|
+
token_endpoint: "https://auth.anmho.com/api/auth/oauth2/token",
|
|
18
|
+
jwks_url: "https://auth.anmho.com/api/auth/jwks",
|
|
19
|
+
resource_server: {
|
|
20
|
+
id: "{{SERVICE_ID}}",
|
|
21
|
+
audience: "api://{{SERVICE_ID}}",
|
|
22
|
+
default_scopes: ["{{SERVICE_ID}}:read", "{{SERVICE_ID}}:write"],
|
|
23
|
+
},
|
|
24
|
+
client: {
|
|
25
|
+
app_id: "{{SERVICE_ID}}",
|
|
26
|
+
identity: "server",
|
|
27
|
+
vault_path_prefix: "prod/apps/{{SERVICE_ID}}/server/oauth-clients",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
temporal: {
|
|
31
|
+
enabled: false,
|
|
32
|
+
address: "localhost:7233",
|
|
33
|
+
namespace: "default",
|
|
34
|
+
task_queue: "{{SERVICE_ID}}",
|
|
35
|
+
api_key_secret_name: "{{SERVICE_ID}}-temporal-api-key",
|
|
36
|
+
},
|
|
37
|
+
providers: {
|
|
38
|
+
vault: {
|
|
39
|
+
mount: "secret",
|
|
40
|
+
neon_path: "prod/providers/neon",
|
|
41
|
+
grafana_path: "prod/providers/grafana",
|
|
42
|
+
clerk_m2m_path: "prod/providers/clerk-m2m",
|
|
43
|
+
temporal_path: "prod/providers/temporal",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
buf: {
|
|
47
|
+
module: "buf.build/anmho/{{SERVICE_ID}}",
|
|
48
|
+
},
|
|
49
|
+
cloudrun: {
|
|
50
|
+
project_id: "{{PROJECT_ID}}",
|
|
51
|
+
region: "{{REGION}}",
|
|
52
|
+
service_account: "{{RUNTIME_SERVICE_ACCOUNT}}",
|
|
53
|
+
},
|
|
54
|
+
workers: {
|
|
55
|
+
script_name: "{{SERVICE_ID}}",
|
|
56
|
+
hyperdrive_binding: "HYPERDRIVE",
|
|
57
|
+
cron: "*/15 * * * *",
|
|
58
|
+
},
|
|
59
|
+
} as const;
|
|
@@ -2,10 +2,17 @@ apiVersion: serving.knative.dev/v1
|
|
|
2
2
|
kind: Service
|
|
3
3
|
metadata:
|
|
4
4
|
name: ${SERVICE_NAME}
|
|
5
|
+
labels:
|
|
6
|
+
managed_by: create-service
|
|
7
|
+
service_id: ${SERVICE_ID}
|
|
5
8
|
annotations:
|
|
6
9
|
run.googleapis.com/ingress: all
|
|
7
10
|
spec:
|
|
8
11
|
template:
|
|
12
|
+
metadata:
|
|
13
|
+
labels:
|
|
14
|
+
managed_by: create-service
|
|
15
|
+
service_id: ${SERVICE_ID}
|
|
9
16
|
spec:
|
|
10
17
|
serviceAccountName: ${RUNTIME_SERVICE_ACCOUNT}
|
|
11
18
|
containers:
|
|
@@ -17,12 +24,28 @@ spec:
|
|
|
17
24
|
value: ${SERVICE_RUNTIME}
|
|
18
25
|
- name: APP_FRAMEWORK
|
|
19
26
|
value: ${SERVICE_FRAMEWORK}
|
|
27
|
+
- name: TEMPORAL_ENABLED
|
|
28
|
+
value: "${TEMPORAL_ENABLED}"
|
|
29
|
+
- name: TEMPORAL_ADDRESS
|
|
30
|
+
value: "${TEMPORAL_ADDRESS}"
|
|
31
|
+
- name: TEMPORAL_NAMESPACE
|
|
32
|
+
value: "${TEMPORAL_NAMESPACE}"
|
|
33
|
+
- name: TEMPORAL_TASK_QUEUE
|
|
34
|
+
value: "${TEMPORAL_TASK_QUEUE}"
|
|
35
|
+
${TEMPORAL_API_KEY_ENV}
|
|
20
36
|
- name: DATABASE_URL
|
|
21
37
|
valueFrom:
|
|
22
38
|
secretKeyRef:
|
|
23
39
|
name: ${DATABASE_URL_SECRET}
|
|
24
40
|
key: latest
|
|
41
|
+
- name: AUTH_ENABLED
|
|
42
|
+
value: "true"
|
|
43
|
+
- name: AUTH_ISSUER
|
|
44
|
+
value: ${AUTH_ISSUER}
|
|
45
|
+
- name: AUTH_AUDIENCE
|
|
46
|
+
value: ${AUTH_AUDIENCE}
|
|
47
|
+
- name: AUTH_JWKS_URL
|
|
48
|
+
value: ${AUTH_JWKS_URL}
|
|
25
49
|
traffic:
|
|
26
50
|
- latestRevision: true
|
|
27
51
|
percent: 100
|
|
28
|
-
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
- run: bun install
|
|
16
|
+
- run: bun lint
|
|
17
|
+
- run: bun test
|
|
18
|
+
- if: ${{ vars.GCX_ENABLED == 'true' && github.ref == 'refs/heads/main' }}
|
|
19
|
+
run: bun run dashboards
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Deploy
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
deploy:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: oven-sh/setup-bun@v2
|
|
14
|
+
- run: bun install
|
|
15
|
+
- run: bun run deploy
|
|
16
|
+
env:
|
|
17
|
+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
18
|
+
- if: ${{ vars.GCX_ENABLED == 'true' }}
|
|
19
|
+
run: bun run dashboards
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
.PHONY: dev migrate gen lint test create deploy dashboards auth destroy
|
|
2
|
+
|
|
3
|
+
SERVICE := npx --no-install service
|
|
4
|
+
|
|
5
|
+
dev:
|
|
6
|
+
bun run dev
|
|
7
|
+
|
|
8
|
+
migrate:
|
|
9
|
+
$(SERVICE) migrate
|
|
10
|
+
|
|
11
|
+
gen:
|
|
12
|
+
@echo "no generated code for workers"
|
|
13
|
+
|
|
14
|
+
lint:
|
|
15
|
+
bunx tsc --noEmit
|
|
16
|
+
|
|
17
|
+
test:
|
|
18
|
+
bun test
|
|
19
|
+
|
|
20
|
+
create:
|
|
21
|
+
$(SERVICE) create
|
|
22
|
+
|
|
23
|
+
deploy:
|
|
24
|
+
$(SERVICE) deploy $(ARGS)
|
|
25
|
+
|
|
26
|
+
dashboards:
|
|
27
|
+
$(SERVICE) dashboards
|
|
28
|
+
|
|
29
|
+
auth:
|
|
30
|
+
$(SERVICE) auth $(ARGS)
|
|
31
|
+
|
|
32
|
+
destroy:
|
|
33
|
+
$(SERVICE) destroy $(ARGS)
|