create-svc 0.1.53 → 0.1.55
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 +6 -0
- package/package.json +1 -1
- package/src/git-bootstrap.test.ts +49 -1
- package/src/git-bootstrap.ts +13 -1
- package/src/scaffold.test.ts +38 -8
- package/src/scaffold.ts +28 -1
- package/src/service-runtime/cloudrun/cli.ts +10 -0
- package/src/service-runtime/cloudrun/config.ts +3 -0
- package/src/service-runtime/cloudrun/lib.ts +0 -3
- package/src/service-runtime/cloudrun/observability.ts +18 -0
- package/templates/shared/.env.example +40 -0
- package/templates/shared/.github/workflows/deploy.yml +2 -1
- package/templates/shared/.github/workflows/preview.yml +5 -3
- package/templates/shared/README.md +37 -0
- package/templates/shared/_gitignore +13 -0
- package/templates/shared/service.jsonc +8 -0
- package/templates/targets/workers/.github/workflows/deploy.yml +1 -0
- package/templates/variants/bun-connectrpc/Makefile +4 -1
- package/templates/variants/bun-connectrpc/migrations/0000_init.sql +10 -0
- package/templates/variants/bun-connectrpc/package.json +1 -0
- package/templates/variants/bun-connectrpc/src/db/repository.ts +52 -3
- package/templates/variants/bun-connectrpc/src/db/schema.ts +13 -0
- package/templates/variants/bun-connectrpc/src/index.ts +47 -4
- package/templates/variants/bun-connectrpc/src/waitlist/service.ts +22 -0
- package/templates/variants/bun-connectrpc/src/waitlist/types.ts +16 -0
- package/templates/variants/bun-hono/Makefile +4 -1
- package/templates/variants/bun-hono/migrations/0000_init.sql +10 -0
- package/templates/variants/bun-hono/package.json +1 -0
- package/templates/variants/bun-hono/src/db/repository.ts +52 -3
- package/templates/variants/bun-hono/src/db/schema.ts +13 -0
- package/templates/variants/bun-hono/src/index.ts +34 -8
- package/templates/variants/bun-hono/src/waitlist/service.ts +22 -0
- package/templates/variants/bun-hono/src/waitlist/types.ts +16 -0
- package/templates/variants/bun-hono/test/app.test.ts +13 -0
- package/templates/variants/go-chi/Dockerfile +0 -1
- package/templates/variants/go-chi/Makefile +4 -1
- package/templates/variants/go-chi/internal/app/service.go +96 -0
- package/templates/variants/go-chi/internal/httpapi/routes.go +56 -7
- package/templates/variants/go-chi/migrations/0000_init.sql +10 -0
- package/templates/variants/go-chi/migrations/atlas.sum +2 -2
- package/templates/variants/go-connectrpc/Makefile +4 -1
- package/templates/variants/go-connectrpc/internal/app/service.go +96 -0
- package/templates/variants/go-connectrpc/internal/httpapi/routes.go +56 -7
- package/templates/variants/go-connectrpc/migrations/0000_init.sql +10 -0
- package/templates/variants/go-connectrpc/migrations/atlas.sum +2 -2
package/README.md
CHANGED
|
@@ -115,6 +115,7 @@ bun run lint
|
|
|
115
115
|
bun run test
|
|
116
116
|
service create
|
|
117
117
|
service deploy
|
|
118
|
+
service observability-bootstrap
|
|
118
119
|
service dev down
|
|
119
120
|
service destroy
|
|
120
121
|
```
|
|
@@ -129,6 +130,7 @@ make lint
|
|
|
129
130
|
make test
|
|
130
131
|
service create
|
|
131
132
|
service deploy
|
|
133
|
+
service observability-bootstrap
|
|
132
134
|
service dev down
|
|
133
135
|
service destroy
|
|
134
136
|
```
|
|
@@ -136,6 +138,10 @@ service destroy
|
|
|
136
138
|
Language-specific tasks such as local running, linting, formatting, testing, and building stay in package scripts or Make targets. Service lifecycle operations are exposed through the generated `service` CLI.
|
|
137
139
|
`service destroy --force` also stops local dev and runs Docker Compose cleanup for generated Cloud Run services.
|
|
138
140
|
|
|
141
|
+
`service observability-bootstrap` enables the Google Cloud Logging, Monitoring,
|
|
142
|
+
and Trace APIs for the generated GCP project. It does not create dashboards,
|
|
143
|
+
alerts, log-based metrics, or SLOs; those stay explicit follow-up work.
|
|
144
|
+
|
|
139
145
|
After `service create` has provisioned auth, the generated repo can mint a
|
|
140
146
|
client-credentials bearer token for smoke checks:
|
|
141
147
|
|
package/package.json
CHANGED
|
@@ -2,7 +2,13 @@ import { expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdir, mkdtemp, realpath, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
buildGitBootstrapConfig,
|
|
7
|
+
commitAndPushGeneratedArtifacts,
|
|
8
|
+
findExistingGitWorktree,
|
|
9
|
+
manualGitHubDeleteCommand,
|
|
10
|
+
markGitHubRepositoryDeleteOnDestroy,
|
|
11
|
+
} from "./git-bootstrap";
|
|
6
12
|
|
|
7
13
|
test("buildGitBootstrapConfig defaults to anmho private repo creation", () => {
|
|
8
14
|
expect(buildGitBootstrapConfig("launch-api", undefined)).toEqual({
|
|
@@ -41,6 +47,36 @@ test("markGitHubRepositoryDeleteOnDestroy records generated repo ownership", asy
|
|
|
41
47
|
expect(await Bun.file(join(root, "service.jsonc")).text()).toContain('"delete_on_destroy": true');
|
|
42
48
|
});
|
|
43
49
|
|
|
50
|
+
test("commitAndPushGeneratedArtifacts excludes local dependencies and dev runtime files", async () => {
|
|
51
|
+
const root = await mkdtemp(join(tmpdir(), "create-svc-git-"));
|
|
52
|
+
const remote = await mkdtemp(join(tmpdir(), "create-svc-remote-"));
|
|
53
|
+
run(["git", "init", "--bare"], remote);
|
|
54
|
+
run(["git", "init", "-b", "main"], root);
|
|
55
|
+
run(["git", "config", "user.name", "create-svc test"], root);
|
|
56
|
+
run(["git", "config", "user.email", "create-svc@example.invalid"], root);
|
|
57
|
+
run(["git", "remote", "add", "origin", remote], root);
|
|
58
|
+
await writeFile(join(root, "README.md"), "# generated\n");
|
|
59
|
+
run(["git", "add", "."], root);
|
|
60
|
+
run(["git", "commit", "-m", "Initial commit"], root);
|
|
61
|
+
run(["git", "push", "-u", "origin", "main"], root);
|
|
62
|
+
|
|
63
|
+
await mkdir(join(root, "node_modules", "large-package"), { recursive: true });
|
|
64
|
+
await mkdir(join(root, ".service"), { recursive: true });
|
|
65
|
+
await mkdir(join(root, ".wrangler", "tmp"), { recursive: true });
|
|
66
|
+
await writeFile(join(root, "node_modules", "large-package", "artifact.bin"), "large");
|
|
67
|
+
await writeFile(join(root, ".service", "local-dev.log"), "log");
|
|
68
|
+
await writeFile(join(root, ".service", "local-dev.pid"), "123");
|
|
69
|
+
await writeFile(join(root, ".wrangler", "tmp", "bundle.js"), "bundle");
|
|
70
|
+
await writeFile(join(root, "service.jsonc"), '{ "deployed": true }\n');
|
|
71
|
+
|
|
72
|
+
expect(commitAndPushGeneratedArtifacts(root, "Record generated deployment artifacts")).toEqual({ committed: true });
|
|
73
|
+
|
|
74
|
+
expect(git(["ls-files"], root)).toContain("service.jsonc");
|
|
75
|
+
expect(git(["ls-files"], root)).not.toContain("node_modules");
|
|
76
|
+
expect(git(["ls-files"], root)).not.toContain(".service/local-dev.log");
|
|
77
|
+
expect(git(["ls-files"], root)).not.toContain(".wrangler");
|
|
78
|
+
});
|
|
79
|
+
|
|
44
80
|
function run(command: string[], cwd: string) {
|
|
45
81
|
const result = Bun.spawnSync(command, {
|
|
46
82
|
cwd,
|
|
@@ -51,3 +87,15 @@ function run(command: string[], cwd: string) {
|
|
|
51
87
|
throw new Error(result.stderr.toString());
|
|
52
88
|
}
|
|
53
89
|
}
|
|
90
|
+
|
|
91
|
+
function git(command: string[], cwd: string) {
|
|
92
|
+
const result = Bun.spawnSync(["git", ...command], {
|
|
93
|
+
cwd,
|
|
94
|
+
stdout: "pipe",
|
|
95
|
+
stderr: "pipe",
|
|
96
|
+
});
|
|
97
|
+
if (result.exitCode !== 0) {
|
|
98
|
+
throw new Error(result.stderr.toString());
|
|
99
|
+
}
|
|
100
|
+
return result.stdout.toString();
|
|
101
|
+
}
|
package/src/git-bootstrap.ts
CHANGED
|
@@ -58,7 +58,19 @@ export async function bootstrapGitHubRepository(targetDir: string, config: GitBo
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export function commitAndPushGeneratedArtifacts(targetDir: string, message: string) {
|
|
61
|
-
run(
|
|
61
|
+
run(
|
|
62
|
+
[
|
|
63
|
+
"git",
|
|
64
|
+
"add",
|
|
65
|
+
"--all",
|
|
66
|
+
".",
|
|
67
|
+
":!node_modules",
|
|
68
|
+
":!.service/*.log",
|
|
69
|
+
":!.service/*.pid",
|
|
70
|
+
":!.wrangler",
|
|
71
|
+
],
|
|
72
|
+
targetDir
|
|
73
|
+
);
|
|
62
74
|
if (!hasStagedChanges(targetDir)) {
|
|
63
75
|
return { committed: false };
|
|
64
76
|
}
|
package/src/scaffold.test.ts
CHANGED
|
@@ -105,6 +105,8 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
|
|
|
105
105
|
|
|
106
106
|
const gitignore = await Bun.file(join(generatedRoot, ".gitignore")).text();
|
|
107
107
|
expect(gitignore).toContain("node_modules");
|
|
108
|
+
expect(gitignore).toContain(".service/*.log");
|
|
109
|
+
expect(gitignore).toContain(".wrangler");
|
|
108
110
|
expect(await Bun.file(join(generatedRoot, "website", "package.json")).exists()).toBeFalse();
|
|
109
111
|
|
|
110
112
|
const dockerCompose = await Bun.file(join(generatedRoot, "docker-compose.yml")).text();
|
|
@@ -133,12 +135,25 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
|
|
|
133
135
|
expect(localEnv).toContain("VAULT_CLOUDFLARE_API_TOKEN_PATH=prod/providers/cloudflare");
|
|
134
136
|
expect(localEnv).not.toContain("ATTACHMENT_PUBLIC_BASE_URL=");
|
|
135
137
|
|
|
136
|
-
const ciWorkflow = await Bun.file(join(generatedRoot, ".github", "workflows", "ci.yml")).text();
|
|
137
|
-
expect(ciWorkflow).toContain("bun run dashboards");
|
|
138
|
-
expect(ciWorkflow).toContain("GCX_ENABLED");
|
|
139
138
|
expect(await Bun.file(join(generatedRoot, "grafana", "waitlist-dashboard.json")).exists()).toBeTrue();
|
|
140
139
|
expect(await Bun.file(join(generatedRoot, "grafana", "alerts.yaml")).exists()).toBeTrue();
|
|
141
140
|
|
|
141
|
+
const previewWorkflow = await Bun.file(join(generatedRoot, ".github", "workflows", "preview.yml")).text();
|
|
142
|
+
expect(previewWorkflow).toContain("push:");
|
|
143
|
+
expect(previewWorkflow).toContain("branches-ignore:");
|
|
144
|
+
expect(previewWorkflow).toContain("- main");
|
|
145
|
+
expect(previewWorkflow).toContain("github.ref_name");
|
|
146
|
+
expect(previewWorkflow).toContain("service deploy --ci --environment preview --name");
|
|
147
|
+
expect(previewWorkflow).toContain("NEON_API_KEY");
|
|
148
|
+
|
|
149
|
+
const deployWorkflow = await Bun.file(join(generatedRoot, ".github", "workflows", "deploy.yml")).text();
|
|
150
|
+
expect(deployWorkflow).toContain("branches:");
|
|
151
|
+
expect(deployWorkflow).toContain("- main");
|
|
152
|
+
expect(deployWorkflow).toContain("bun install -g create-svc@latest");
|
|
153
|
+
expect(deployWorkflow).toContain("service deploy --ci");
|
|
154
|
+
expect(deployWorkflow).toContain("bun run dashboards");
|
|
155
|
+
expect(deployWorkflow).toContain("GCX_ENABLED");
|
|
156
|
+
|
|
142
157
|
if (variant.runtime === "go") {
|
|
143
158
|
const goMod = await Bun.file(join(generatedRoot, "go.mod")).text();
|
|
144
159
|
const goSumExists = await Bun.file(join(generatedRoot, "go.sum")).exists();
|
|
@@ -148,6 +163,11 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
|
|
|
148
163
|
expect(goSumExists).toBeTrue();
|
|
149
164
|
const dockerfile = await Bun.file(join(generatedRoot, "Dockerfile")).text();
|
|
150
165
|
expect(dockerfile).toContain("COPY go.mod go.sum ./");
|
|
166
|
+
if (variant.framework === "chi") {
|
|
167
|
+
expect(dockerfile).not.toContain("COPY gen ./gen");
|
|
168
|
+
} else {
|
|
169
|
+
expect(dockerfile).toContain("COPY gen ./gen");
|
|
170
|
+
}
|
|
151
171
|
expect(packageJson).toContain('"dev": "make dev"');
|
|
152
172
|
expect(packageJson).toContain('"service": "service"');
|
|
153
173
|
expect(packageJson).toContain('"migrate": "service migrate"');
|
|
@@ -198,6 +218,7 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
|
|
|
198
218
|
expect(packageJson).toContain('"create": "service create"');
|
|
199
219
|
expect(packageJson).toContain('"deploy": "service deploy"');
|
|
200
220
|
expect(packageJson).toContain('"dashboards": "service dashboards"');
|
|
221
|
+
expect(packageJson).toContain('"observability-bootstrap": "service observability-bootstrap"');
|
|
201
222
|
expect(packageJson).toContain('"auth": "service auth"');
|
|
202
223
|
expect(packageJson).toContain('"destroy": "service destroy"');
|
|
203
224
|
expect(await Bun.file(join(generatedRoot, "scripts", "cloudrun", "cli.ts")).exists()).toBeFalse();
|
|
@@ -206,12 +227,15 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
|
|
|
206
227
|
expect(serviceConfig).toContain('"service_id": "dns-api"');
|
|
207
228
|
expect(serviceConfig).toContain('"project_id": "anmho-dns-api"');
|
|
208
229
|
expect(serviceConfig).toContain('"database_name": "dns_api"');
|
|
230
|
+
expect(serviceConfig).toContain('"observability"');
|
|
231
|
+
expect(serviceConfig).toContain("logging.googleapis.com");
|
|
209
232
|
const authScript = await Bun.file(join(generatedRoot, "src", "auth.ts")).text();
|
|
210
233
|
expect(authScript).toContain('"Ed25519"');
|
|
211
234
|
|
|
212
235
|
const makefile = await Bun.file(join(generatedRoot, "Makefile")).text();
|
|
213
236
|
expect(makefile).toContain("SERVICE := service");
|
|
214
237
|
expect(makefile).toContain("dashboards:");
|
|
238
|
+
expect(makefile).toContain("observability-bootstrap:");
|
|
215
239
|
expect(makefile).toContain("auth:");
|
|
216
240
|
expect(makefile).toContain("bun run dev");
|
|
217
241
|
const devScript = await Bun.file(join(generatedRoot, "scripts", "dev.ts")).text();
|
|
@@ -249,8 +273,6 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
|
|
|
249
273
|
expect(readme).toContain("service auth resource-server");
|
|
250
274
|
}
|
|
251
275
|
|
|
252
|
-
const deployWorkflow = await Bun.file(join(generatedRoot, ".github", "workflows", "deploy.yml")).text();
|
|
253
|
-
expect(deployWorkflow).toContain("bun run dashboards");
|
|
254
276
|
}
|
|
255
277
|
}, 30000);
|
|
256
278
|
|
|
@@ -275,14 +297,20 @@ test("scaffolds a backend package cleanly into a nested monorepo-style directory
|
|
|
275
297
|
expect(readme).toContain("known-good CLIs");
|
|
276
298
|
expect(readme).toContain("service create");
|
|
277
299
|
expect(readme).toContain("service deploy");
|
|
300
|
+
expect(readme).toContain("Google observability bootstrap");
|
|
301
|
+
expect(readme).toContain("Google Cloud Logging, Monitoring, and Trace APIs");
|
|
278
302
|
expect(readme).toContain("one-command production create");
|
|
279
303
|
expect(readme).toContain("waitlist/launch service");
|
|
280
304
|
expect(readme).toContain("Terraform is optional");
|
|
281
305
|
expect(readme).toContain("waitlist/launch service");
|
|
282
306
|
expect(readme).not.toContain("Neon main, preview, and personal branch provisioning");
|
|
283
|
-
|
|
284
|
-
expect(
|
|
285
|
-
expect(readme).
|
|
307
|
+
expect(readme).toContain("GitHub Actions deployment");
|
|
308
|
+
expect(readme).toContain(".github/workflows/preview.yml");
|
|
309
|
+
expect(readme).toContain(".github/workflows/deploy.yml");
|
|
310
|
+
expect(readme).toContain("pushes to non-main branches");
|
|
311
|
+
expect(readme).toContain("GCP_WIF_PROVIDER");
|
|
312
|
+
expect(readme).toContain("GCP_DEPLOYER_SERVICE_ACCOUNT");
|
|
313
|
+
expect(readme).toContain("NEON_API_KEY");
|
|
286
314
|
|
|
287
315
|
const packageJson = await Bun.file(join(generatedRoot, "package.json")).text();
|
|
288
316
|
expect(packageJson).toContain('"hono"');
|
|
@@ -290,6 +318,8 @@ test("scaffolds a backend package cleanly into a nested monorepo-style directory
|
|
|
290
318
|
const entrypoint = await Bun.file(join(generatedRoot, "src", "index.ts")).text();
|
|
291
319
|
expect(entrypoint).toContain("/v1/waitlist");
|
|
292
320
|
expect(entrypoint).toContain("/v1/admin/waitlist");
|
|
321
|
+
expect(await Bun.file(join(generatedRoot, ".github", "workflows", "preview.yml")).exists()).toBeTrue();
|
|
322
|
+
expect(await Bun.file(join(generatedRoot, ".github", "workflows", "deploy.yml")).exists()).toBeTrue();
|
|
293
323
|
}, 15000);
|
|
294
324
|
|
|
295
325
|
test("scaffolds the workers target with wrangler lifecycle commands", async () => {
|
package/src/scaffold.ts
CHANGED
|
@@ -12,6 +12,11 @@ import {
|
|
|
12
12
|
import { exampleForProfile, type Profile } from "./profiles";
|
|
13
13
|
import type { GitBootstrapConfig } from "./git-bootstrap";
|
|
14
14
|
|
|
15
|
+
const GENERATED_GITHUB_ACTION_WORKFLOWS = new Set([
|
|
16
|
+
".github/workflows/preview.yml",
|
|
17
|
+
".github/workflows/deploy.yml",
|
|
18
|
+
]);
|
|
19
|
+
|
|
15
20
|
export type ScaffoldConfig = {
|
|
16
21
|
directory: string;
|
|
17
22
|
serviceName: string;
|
|
@@ -64,7 +69,7 @@ export async function scaffoldProject(config: ScaffoldConfig) {
|
|
|
64
69
|
continue;
|
|
65
70
|
}
|
|
66
71
|
const sourcePath = join(template.root, relativePath);
|
|
67
|
-
const destinationPath = join(targetDir, relativePath);
|
|
72
|
+
const destinationPath = join(targetDir, templateDestinationPath(relativePath));
|
|
68
73
|
const raw = await Bun.file(sourcePath).text();
|
|
69
74
|
const rendered = renderTemplate(raw, replacements);
|
|
70
75
|
|
|
@@ -76,6 +81,10 @@ export async function scaffoldProject(config: ScaffoldConfig) {
|
|
|
76
81
|
await writeLocalEnvFile(targetDir, replacements);
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
function templateDestinationPath(relativePath: string) {
|
|
85
|
+
return relativePath === "_gitignore" ? ".gitignore" : relativePath;
|
|
86
|
+
}
|
|
87
|
+
|
|
79
88
|
function shouldSkipForTarget(target: DeployTarget, templateKind: "shared" | "variant" | "target", relativePath: string) {
|
|
80
89
|
if (
|
|
81
90
|
relativePath === "scripts/authctl.ts" ||
|
|
@@ -153,12 +162,23 @@ async function collectTemplateFiles(root: string, relative = ""): Promise<string
|
|
|
153
162
|
files.push(...(await collectTemplateFiles(root, nextRelative)));
|
|
154
163
|
continue;
|
|
155
164
|
}
|
|
165
|
+
if (shouldSkipTemplateFile(nextRelative)) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
156
168
|
files.push(nextRelative);
|
|
157
169
|
}
|
|
158
170
|
|
|
159
171
|
return files.sort();
|
|
160
172
|
}
|
|
161
173
|
|
|
174
|
+
function shouldSkipTemplateFile(relativePath: string) {
|
|
175
|
+
if (!relativePath.startsWith(".github/")) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return !GENERATED_GITHUB_ACTION_WORKFLOWS.has(relativePath);
|
|
180
|
+
}
|
|
181
|
+
|
|
162
182
|
function buildReplacements(config: ScaffoldConfig) {
|
|
163
183
|
const example = exampleForProfile(config.profile);
|
|
164
184
|
const serviceAccountBase = compactIdentifier(config.serviceName, 21);
|
|
@@ -219,6 +239,13 @@ function buildReplacements(config: ScaffoldConfig) {
|
|
|
219
239
|
COMMAND_DEV_DOWN: "service dev down",
|
|
220
240
|
COMMAND_BOOTSTRAP: "service create",
|
|
221
241
|
COMMAND_DEPLOY: "service deploy",
|
|
242
|
+
COMMAND_OBSERVABILITY_BOOTSTRAP:
|
|
243
|
+
config.runtime === "bun" ? "bun run observability-bootstrap" : "make observability-bootstrap",
|
|
244
|
+
WORKFLOW_DEPLOY_MAIN_COMMAND: "service deploy --ci",
|
|
245
|
+
WORKFLOW_DEPLOY_PREVIEW_COMMAND:
|
|
246
|
+
"service deploy --ci --environment preview --name ${{ github.ref_name }}",
|
|
247
|
+
WORKFLOW_DEPLOY_MAIN_DOC_COMMAND: "service deploy --ci",
|
|
248
|
+
WORKFLOW_DEPLOY_PREVIEW_DOC_COMMAND: "service deploy --ci --environment preview --name <branch-name>",
|
|
222
249
|
COMMAND_AUTH_RESOURCE: "service auth resource-server",
|
|
223
250
|
COMMAND_AUTH_CLIENT: "service auth client create",
|
|
224
251
|
COMMAND_AUTH_TOKEN: "service auth token",
|
|
@@ -6,6 +6,7 @@ import { stopLocalDev } from "../local-dev";
|
|
|
6
6
|
import { bootstrap, prepareGcpProject } from "./bootstrap";
|
|
7
7
|
import { cleanup } from "./cleanup";
|
|
8
8
|
import { deploy } from "./deploy";
|
|
9
|
+
import { observabilityBootstrap } from "./observability";
|
|
9
10
|
import { config } from "./config";
|
|
10
11
|
import {
|
|
11
12
|
accessSecretVersion,
|
|
@@ -69,6 +70,14 @@ export async function main(argv = Bun.argv.slice(2)) {
|
|
|
69
70
|
return;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
if (command === "observability-bootstrap") {
|
|
74
|
+
await runMain("Google observability bootstrap", async () => {
|
|
75
|
+
await observabilityBootstrap();
|
|
76
|
+
return `Google observability bootstrap finished for ${config.serviceName}`;
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
72
81
|
if (command === "dev") {
|
|
73
82
|
if (rest[0] !== "down") {
|
|
74
83
|
throw new Error(`Unknown dev command: ${rest[0] || ""}\n\n${formatHelp()}`);
|
|
@@ -125,6 +134,7 @@ function formatHelp() {
|
|
|
125
134
|
" sdk Build or publish generated SDK artifacts",
|
|
126
135
|
" dns Repair or inspect DNS mappings",
|
|
127
136
|
" dev down Stop local dev and Docker Compose containers",
|
|
137
|
+
" observability-bootstrap Enable Google observability APIs",
|
|
128
138
|
" dashboards Publish Grafana resources",
|
|
129
139
|
" destroy Remove service-managed cloud resources",
|
|
130
140
|
].join("\n");
|
|
@@ -55,6 +55,9 @@ export const config = {
|
|
|
55
55
|
repository: serviceConfig.git?.repository || serviceConfig.service_id,
|
|
56
56
|
deleteOnDestroy: Boolean(serviceConfig.git?.delete_on_destroy),
|
|
57
57
|
},
|
|
58
|
+
observability: {
|
|
59
|
+
requiredApis: serviceConfig.observability?.required_apis ?? ["logging.googleapis.com", "monitoring.googleapis.com", "cloudtrace.googleapis.com"],
|
|
60
|
+
},
|
|
58
61
|
requiredApis: cloudrun.required_apis,
|
|
59
62
|
} as const;
|
|
60
63
|
|
|
@@ -605,7 +605,6 @@ export function ensureProductionDomainMapping(serviceName: string) {
|
|
|
605
605
|
}
|
|
606
606
|
|
|
607
607
|
const result = gcloud([
|
|
608
|
-
"beta",
|
|
609
608
|
"run",
|
|
610
609
|
"domain-mappings",
|
|
611
610
|
"create",
|
|
@@ -627,7 +626,6 @@ export function describeProductionDomainMapping():
|
|
|
627
626
|
| undefined {
|
|
628
627
|
const result = gcloud(
|
|
629
628
|
[
|
|
630
|
-
"beta",
|
|
631
629
|
"run",
|
|
632
630
|
"domain-mappings",
|
|
633
631
|
"describe",
|
|
@@ -680,7 +678,6 @@ export function deleteProductionDomainMapping() {
|
|
|
680
678
|
deleteCloudflareDnsRecord();
|
|
681
679
|
gcloud(
|
|
682
680
|
[
|
|
683
|
-
"beta",
|
|
684
681
|
"run",
|
|
685
682
|
"domain-mappings",
|
|
686
683
|
"delete",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { config } from "./config";
|
|
2
|
+
import { gcloud, requireCommand, requireGcloudAuth, runMain, runStep } from "./lib";
|
|
3
|
+
|
|
4
|
+
export async function observabilityBootstrap() {
|
|
5
|
+
requireCommand("gcloud");
|
|
6
|
+
requireGcloudAuth();
|
|
7
|
+
|
|
8
|
+
await runStep("Enabling Google observability APIs", () =>
|
|
9
|
+
gcloud(["services", "enable", ...config.observability.requiredApis, "--project", config.project.id])
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (import.meta.main) {
|
|
14
|
+
await runMain("Google observability bootstrap", async () => {
|
|
15
|
+
await observabilityBootstrap();
|
|
16
|
+
return `Google observability bootstrap finished for ${config.serviceName}`;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Local development defaults.
|
|
2
|
+
# The scaffold also writes a ready-to-use .env.local for Docker Compose Postgres.
|
|
3
|
+
|
|
4
|
+
DATABASE_URL=postgres://{{LOCAL_DATABASE_USER}}:{{LOCAL_DATABASE_PASSWORD}}@127.0.0.1:{{LOCAL_DATABASE_PORT}}/{{LOCAL_DATABASE_NAME}}?sslmode=disable
|
|
5
|
+
|
|
6
|
+
TEMPORAL_ENABLED=false
|
|
7
|
+
TEMPORAL_ADDRESS=localhost:7233
|
|
8
|
+
TEMPORAL_NAMESPACE=default
|
|
9
|
+
TEMPORAL_TASK_QUEUE={{SERVICE_ID}}
|
|
10
|
+
# Optional for Temporal Cloud. `service create` writes this to Secret Manager.
|
|
11
|
+
TEMPORAL_API_KEY=
|
|
12
|
+
|
|
13
|
+
AUTH_ENABLED=false
|
|
14
|
+
AUTH_ISSUER=https://auth.anmho.com
|
|
15
|
+
AUTH_AUDIENCE=api://{{SERVICE_ID}}
|
|
16
|
+
AUTH_JWKS_URL=https://auth.anmho.com/api/auth/jwks
|
|
17
|
+
|
|
18
|
+
# Production authctl operations such as `service create` and
|
|
19
|
+
# `service auth client create` load Cloudflare Access values from Vault by
|
|
20
|
+
# default. Direct env values still override Vault when set.
|
|
21
|
+
AUTH_INTERNAL_BASE_URL=https://auth.anmho.com/internal
|
|
22
|
+
CLOUDFLARE_ACCESS_SERVICE_TOKEN_CLIENT_ID=
|
|
23
|
+
CLOUDFLARE_ACCESS_SERVICE_TOKEN_CLIENT_SECRET=
|
|
24
|
+
|
|
25
|
+
# Remote bootstrap and deploy can use Neon admin credentials directly or through Vault.
|
|
26
|
+
# Do not commit VAULT_TOKEN. Prefer `vault login`; the CLI will also use
|
|
27
|
+
# VAULT_TOKEN_FILE or ~/.vault-token when available.
|
|
28
|
+
|
|
29
|
+
VAULT_ADDR=https://vault.example.com
|
|
30
|
+
VAULT_SECRET_MOUNT=secret
|
|
31
|
+
VAULT_AUTHCTL_ACCESS_PATH=prod/apps/auth/authctl/cloudflare-access
|
|
32
|
+
VAULT_AUTHCTL_ACCESS_BASE_URL_FIELD=AUTH_INTERNAL_BASE_URL
|
|
33
|
+
VAULT_AUTHCTL_ACCESS_CLIENT_ID_FIELD=CLOUDFLARE_ACCESS_SERVICE_TOKEN_CLIENT_ID
|
|
34
|
+
VAULT_AUTHCTL_ACCESS_CLIENT_SECRET_FIELD=CLOUDFLARE_ACCESS_SERVICE_TOKEN_CLIENT_SECRET
|
|
35
|
+
VAULT_NEON_API_KEY_PATH=prod/providers/neon
|
|
36
|
+
VAULT_NEON_API_KEY_FIELD=api_key
|
|
37
|
+
|
|
38
|
+
# Optional provider credentials can be read from Vault or environment by
|
|
39
|
+
# generated adapters you add later. The base waitlist service does not require
|
|
40
|
+
# provider secrets.
|
|
@@ -25,7 +25,8 @@ jobs:
|
|
|
25
25
|
service_account: ${{ vars.GCP_DEPLOYER_SERVICE_ACCOUNT }}
|
|
26
26
|
- uses: google-github-actions/setup-gcloud@v2
|
|
27
27
|
- run: bun install
|
|
28
|
-
- run: bun
|
|
28
|
+
- run: bun install -g create-svc@latest
|
|
29
|
+
- run: {{WORKFLOW_DEPLOY_MAIN_COMMAND}}
|
|
29
30
|
env:
|
|
30
31
|
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
|
|
31
32
|
- if: ${{ vars.GCX_ENABLED == 'true' }}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
name: Preview
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
push:
|
|
5
|
+
branches-ignore:
|
|
6
|
+
- main
|
|
6
7
|
|
|
7
8
|
permissions:
|
|
8
9
|
contents: read
|
|
@@ -24,6 +25,7 @@ jobs:
|
|
|
24
25
|
service_account: ${{ vars.GCP_DEPLOYER_SERVICE_ACCOUNT }}
|
|
25
26
|
- uses: google-github-actions/setup-gcloud@v2
|
|
26
27
|
- run: bun install
|
|
27
|
-
- run: bun
|
|
28
|
+
- run: bun install -g create-svc@latest
|
|
29
|
+
- run: {{WORKFLOW_DEPLOY_PREVIEW_COMMAND}}
|
|
28
30
|
env:
|
|
29
31
|
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
|
|
@@ -9,6 +9,7 @@ This `{{PROFILE}}` profile targets `{{RUNTIME}} + {{FRAMEWORK}}` on Cloud Run wi
|
|
|
9
9
|
- local Docker Compose Postgres for first-run development
|
|
10
10
|
- the `service` CLI for create, deploy, doctor, dashboards, and destroy
|
|
11
11
|
- shared GCP project deployment with quota-project-aware `gcloud` calls
|
|
12
|
+
- Google Cloud Observability API bootstrap through `service observability-bootstrap`
|
|
12
13
|
- Neon-backed remote database provisioning during create and deploy
|
|
13
14
|
- Better Auth client-credentials resource-server registration through `authctl`
|
|
14
15
|
- stage-aware waitlist data and trigger ingestion
|
|
@@ -77,6 +78,20 @@ Build to build the image remotely.
|
|
|
77
78
|
Local Docker builds target `linux/amd64` so images built on Apple Silicon run on
|
|
78
79
|
Cloud Run.
|
|
79
80
|
|
|
81
|
+
## Google observability bootstrap
|
|
82
|
+
|
|
83
|
+
Run this after the first production create when you want the project ready for
|
|
84
|
+
Google Cloud Observability follow-up work:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
{{COMMAND_OBSERVABILITY_BOOTSTRAP}}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The command authenticates through `gcloud`, targets the generated GCP project,
|
|
91
|
+
and enables the Google Cloud Logging, Monitoring, and Trace APIs. It does not
|
|
92
|
+
create dashboards, alerts, log-based metrics, or SLOs; those remain explicit
|
|
93
|
+
follow-up work.
|
|
94
|
+
|
|
80
95
|
Authenticate `gcloud` on the machine before running provisioning commands:
|
|
81
96
|
|
|
82
97
|
```bash
|
|
@@ -211,6 +226,28 @@ secrets only when you add a provider adapter. A generic adapter can honor:
|
|
|
211
226
|
|
|
212
227
|
- `WEBHOOK_<PROVIDER>_SECRET`
|
|
213
228
|
|
|
229
|
+
## GitHub Actions deployment
|
|
230
|
+
|
|
231
|
+
The scaffold emits a minimal deployment workflow slice for Cloud Run:
|
|
232
|
+
|
|
233
|
+
- [.github/workflows/preview.yml](.github/workflows/preview.yml) deploys a preview environment on pushes to non-main branches. It runs `{{WORKFLOW_DEPLOY_PREVIEW_DOC_COMMAND}}`.
|
|
234
|
+
- [.github/workflows/deploy.yml](.github/workflows/deploy.yml) deploys the main production environment on pushes to `main`. It runs `{{WORKFLOW_DEPLOY_MAIN_DOC_COMMAND}}`.
|
|
235
|
+
|
|
236
|
+
These workflows intentionally assume only GitHub OIDC, Google Cloud Workload Identity Federation, the generated `service` CLI, and Neon.
|
|
237
|
+
They do not use a long-lived Google service account key.
|
|
238
|
+
|
|
239
|
+
Before enabling the workflows, set these GitHub repository variables:
|
|
240
|
+
|
|
241
|
+
- `GCP_WIF_PROVIDER`: full Workload Identity Provider resource name that trusts this repository
|
|
242
|
+
- `GCP_DEPLOYER_SERVICE_ACCOUNT`: deployer service account email
|
|
243
|
+
|
|
244
|
+
Set this GitHub repository secret:
|
|
245
|
+
|
|
246
|
+
- `NEON_API_KEY`: Neon admin API key used to create preview branches and resolve the main database
|
|
247
|
+
|
|
248
|
+
The deployer service account needs enough access in the generated GCP project to run `service deploy`, including Cloud Run, Artifact Registry, Secret Manager, IAM service account usage, and storage operations for this service.
|
|
249
|
+
Run `{{COMMAND_BOOTSTRAP}}` once before relying on the production workflow for a fresh project.
|
|
250
|
+
|
|
214
251
|
## One-command production create
|
|
215
252
|
|
|
216
253
|
The one-command production create path is designed for a fresh standalone service.
|
|
@@ -85,6 +85,14 @@
|
|
|
85
85
|
"module": "buf.build/anmho/{{SERVICE_ID}}"
|
|
86
86
|
},
|
|
87
87
|
|
|
88
|
+
"observability": {
|
|
89
|
+
"required_apis": [
|
|
90
|
+
"logging.googleapis.com",
|
|
91
|
+
"monitoring.googleapis.com",
|
|
92
|
+
"cloudtrace.googleapis.com"
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
|
|
88
96
|
"cloudrun": {
|
|
89
97
|
"project_id": "{{PROJECT_ID}}",
|
|
90
98
|
"project_name": "{{PROJECT_NAME}}",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.PHONY: dev migrate gen lint test create deploy dashboards auth destroy
|
|
1
|
+
.PHONY: dev migrate gen lint test create deploy dashboards observability-bootstrap auth destroy
|
|
2
2
|
|
|
3
3
|
SERVICE := service
|
|
4
4
|
|
|
@@ -26,6 +26,9 @@ deploy:
|
|
|
26
26
|
dashboards:
|
|
27
27
|
$(SERVICE) dashboards
|
|
28
28
|
|
|
29
|
+
observability-bootstrap:
|
|
30
|
+
$(SERVICE) observability-bootstrap
|
|
31
|
+
|
|
29
32
|
auth:
|
|
30
33
|
$(SERVICE) auth $(ARGS)
|
|
31
34
|
|
|
@@ -18,3 +18,13 @@ create table if not exists waitlist_triggers (
|
|
|
18
18
|
created_at timestamptz not null default now(),
|
|
19
19
|
processed_at timestamptz
|
|
20
20
|
);
|
|
21
|
+
|
|
22
|
+
create table if not exists webhook_events (
|
|
23
|
+
id text primary key,
|
|
24
|
+
provider text not null,
|
|
25
|
+
external_event_id text not null,
|
|
26
|
+
payload_json text not null,
|
|
27
|
+
headers_json text not null,
|
|
28
|
+
received_at timestamptz not null default now(),
|
|
29
|
+
unique (provider, external_event_id)
|
|
30
|
+
);
|