create-svc 0.1.10 → 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.
Files changed (168) hide show
  1. package/README.md +46 -43
  2. package/bin/create-service.mjs +2 -0
  3. package/package.json +12 -9
  4. package/src/cli.test.ts +28 -10
  5. package/src/cli.ts +195 -30
  6. package/src/git-bootstrap.test.ts +40 -0
  7. package/src/git-bootstrap.ts +110 -0
  8. package/src/naming.test.ts +1 -0
  9. package/src/naming.ts +23 -0
  10. package/src/post-scaffold.test.ts +19 -0
  11. package/src/post-scaffold.ts +17 -4
  12. package/src/profiles.ts +2 -5
  13. package/src/scaffold.test.ts +231 -40
  14. package/src/scaffold.ts +84 -29
  15. package/src/vault.test.ts +61 -1
  16. package/src/vault.ts +77 -15
  17. package/templates/shared/.github/workflows/ci.yml +2 -1
  18. package/templates/shared/.github/workflows/deploy.yml +2 -0
  19. package/templates/shared/README.md +124 -47
  20. package/templates/shared/grafana/alerts.yaml +54 -0
  21. package/templates/shared/grafana/waitlist-dashboard.json +63 -0
  22. package/templates/shared/scripts/authctl.ts +231 -0
  23. package/templates/shared/scripts/cloudrun/bootstrap.ts +14 -5
  24. package/templates/shared/scripts/cloudrun/cleanup.ts +64 -4
  25. package/templates/shared/scripts/cloudrun/cli.ts +324 -7
  26. package/templates/shared/scripts/cloudrun/config.ts +11 -4
  27. package/templates/shared/scripts/cloudrun/deploy.ts +0 -4
  28. package/templates/shared/scripts/cloudrun/lib.ts +174 -41
  29. package/templates/shared/scripts/cloudrun/neon.ts +45 -0
  30. package/templates/shared/scripts/dev.ts +22 -0
  31. package/templates/shared/scripts/ensure-local-db.ts +3 -0
  32. package/templates/shared/scripts/local-docker.ts +63 -0
  33. package/templates/shared/scripts/local-env.ts +27 -0
  34. package/templates/shared/scripts/seed.ts +73 -0
  35. package/templates/shared/scripts/wait-for-db.ts +32 -0
  36. package/templates/shared/service.config.ts +59 -0
  37. package/templates/shared/service.yaml +24 -44
  38. package/templates/targets/workers/.github/workflows/ci.yml +19 -0
  39. package/templates/targets/workers/.github/workflows/deploy.yml +19 -0
  40. package/templates/targets/workers/Makefile +33 -0
  41. package/templates/targets/workers/README.md +75 -0
  42. package/templates/targets/workers/package.json +35 -0
  43. package/templates/targets/workers/scripts/workers/cli.ts +397 -0
  44. package/templates/targets/workers/src/auth.ts +178 -0
  45. package/templates/targets/workers/src/index.ts +198 -0
  46. package/templates/targets/workers/src/storage.ts +370 -0
  47. package/templates/targets/workers/test/app.test.ts +108 -0
  48. package/templates/targets/workers/tsconfig.json +11 -0
  49. package/templates/targets/workers/wrangler.toml +24 -0
  50. package/templates/variants/bun-connectrpc/Makefile +14 -8
  51. package/templates/variants/bun-connectrpc/gen/protos/waitlist/v1/waitlist_pb.ts +424 -0
  52. package/templates/variants/bun-connectrpc/migrations/0000_init.sql +12 -55
  53. package/templates/variants/bun-connectrpc/package.json +12 -5
  54. package/templates/variants/bun-connectrpc/protos/waitlist/v1/waitlist.proto +91 -0
  55. package/templates/variants/bun-connectrpc/scripts/codegen.ts +1 -1
  56. package/templates/variants/bun-connectrpc/scripts/migrate.ts +4 -1
  57. package/templates/variants/bun-connectrpc/src/auth.ts +200 -0
  58. package/templates/variants/bun-connectrpc/src/db/repository.ts +67 -420
  59. package/templates/variants/bun-connectrpc/src/db/schema.ts +15 -64
  60. package/templates/variants/bun-connectrpc/src/index.ts +76 -176
  61. package/templates/variants/bun-connectrpc/src/temporal/activities.ts +14 -0
  62. package/templates/variants/bun-connectrpc/src/temporal/worker.ts +38 -0
  63. package/templates/variants/bun-connectrpc/src/temporal/workflows.ts +10 -0
  64. package/templates/variants/bun-connectrpc/src/waitlist/service.ts +172 -0
  65. package/templates/variants/bun-connectrpc/src/waitlist/types.ts +45 -0
  66. package/templates/variants/bun-connectrpc/test/app.test.ts +4 -4
  67. package/templates/variants/bun-connectrpc/test/waitlist.integration.test.ts +71 -0
  68. package/templates/variants/bun-hono/Makefile +14 -8
  69. package/templates/variants/bun-hono/migrations/0000_init.sql +12 -55
  70. package/templates/variants/bun-hono/package.json +12 -5
  71. package/templates/variants/bun-hono/scripts/migrate.ts +4 -1
  72. package/templates/variants/bun-hono/src/auth.ts +181 -0
  73. package/templates/variants/bun-hono/src/db/repository.ts +68 -421
  74. package/templates/variants/bun-hono/src/db/schema.ts +15 -64
  75. package/templates/variants/bun-hono/src/index.ts +65 -180
  76. package/templates/variants/bun-hono/src/temporal/activities.ts +14 -0
  77. package/templates/variants/bun-hono/src/temporal/worker.ts +38 -0
  78. package/templates/variants/bun-hono/src/temporal/workflows.ts +10 -0
  79. package/templates/variants/bun-hono/src/waitlist/service.ts +166 -0
  80. package/templates/variants/bun-hono/src/waitlist/types.ts +50 -0
  81. package/templates/variants/bun-hono/test/app.test.ts +72 -41
  82. package/templates/variants/bun-hono/test/waitlist.integration.test.ts +102 -0
  83. package/templates/variants/go-chi/Makefile +27 -11
  84. package/templates/variants/go-chi/atlas.hcl +8 -0
  85. package/templates/variants/go-chi/cmd/server/main.go +21 -10
  86. package/templates/variants/go-chi/go.mod +1 -3
  87. package/templates/variants/go-chi/internal/app/service.go +202 -685
  88. package/templates/variants/go-chi/internal/auth/middleware.go +289 -0
  89. package/templates/variants/go-chi/internal/auth/middleware_test.go +38 -0
  90. package/templates/variants/go-chi/internal/config/config.go +27 -11
  91. package/templates/variants/go-chi/internal/httpapi/routes.go +78 -157
  92. package/templates/variants/go-chi/internal/httpapi/waitlist_integration_test.go +199 -0
  93. package/templates/variants/go-chi/internal/temporal/activities.go +27 -0
  94. package/templates/variants/go-chi/internal/temporal/worker.go +42 -0
  95. package/templates/variants/go-chi/internal/temporal/workflows.go +18 -0
  96. package/templates/variants/go-chi/migrations/0000_init.sql +12 -55
  97. package/templates/variants/go-chi/migrations/atlas.sum +2 -0
  98. package/templates/variants/go-chi/package.json +7 -1
  99. package/templates/variants/go-connectrpc/Makefile +26 -9
  100. package/templates/variants/go-connectrpc/atlas.hcl +8 -0
  101. package/templates/variants/go-connectrpc/buf.gen.yaml +2 -2
  102. package/templates/variants/go-connectrpc/cmd/server/main.go +23 -12
  103. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlist.pb.go +960 -0
  104. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlistv1connect/waitlist.connect.go +283 -0
  105. package/templates/variants/go-connectrpc/go.mod +1 -1
  106. package/templates/variants/go-connectrpc/internal/app/service.go +202 -685
  107. package/templates/variants/go-connectrpc/internal/auth/middleware.go +289 -0
  108. package/templates/variants/go-connectrpc/internal/auth/middleware_test.go +38 -0
  109. package/templates/variants/go-connectrpc/internal/config/config.go +27 -11
  110. package/templates/variants/go-connectrpc/internal/connectapi/handler.go +78 -201
  111. package/templates/variants/go-connectrpc/internal/connectapi/waitlist_integration_test.go +122 -0
  112. package/templates/variants/go-connectrpc/internal/httpapi/routes.go +147 -9
  113. package/templates/variants/go-connectrpc/internal/temporal/activities.go +27 -0
  114. package/templates/variants/go-connectrpc/internal/temporal/worker.go +42 -0
  115. package/templates/variants/go-connectrpc/internal/temporal/workflows.go +18 -0
  116. package/templates/variants/go-connectrpc/migrations/0000_init.sql +12 -55
  117. package/templates/variants/go-connectrpc/migrations/atlas.sum +2 -0
  118. package/templates/variants/go-connectrpc/package.json +7 -1
  119. package/templates/variants/go-connectrpc/protos/waitlist/v1/waitlist.proto +93 -0
  120. package/templates/root/.github/workflows/buf-publish.yml +0 -19
  121. package/templates/root/.github/workflows/ci.yml +0 -26
  122. package/templates/root/.github/workflows/deploy.yml +0 -22
  123. package/templates/root/Dockerfile +0 -23
  124. package/templates/root/README.md +0 -69
  125. package/templates/root/buf.gen.yaml +0 -10
  126. package/templates/root/buf.yaml +0 -9
  127. package/templates/root/cmd/server/main.go +0 -44
  128. package/templates/root/gen/dns/v1/dns.pb.go +0 -623
  129. package/templates/root/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
  130. package/templates/root/go.mod +0 -10
  131. package/templates/root/internal/app/service.go +0 -152
  132. package/templates/root/internal/app/token_source.go +0 -50
  133. package/templates/root/internal/cloudflare/client.go +0 -160
  134. package/templates/root/internal/config/config.go +0 -55
  135. package/templates/root/internal/connectapi/handler.go +0 -79
  136. package/templates/root/internal/httpapi/routes.go +0 -93
  137. package/templates/root/internal/vault/client.go +0 -148
  138. package/templates/root/package.json +0 -12
  139. package/templates/root/protos/dns/v1/dns.proto +0 -58
  140. package/templates/root/scripts/cloudrun/bootstrap.ts +0 -65
  141. package/templates/root/scripts/cloudrun/config.ts +0 -50
  142. package/templates/root/scripts/cloudrun/deploy.ts +0 -41
  143. package/templates/root/scripts/cloudrun/lib.ts +0 -244
  144. package/templates/root/service.yaml +0 -50
  145. package/templates/root/test/go.test.ts +0 -19
  146. package/templates/shared/scripts/cloudrun/integrations.ts +0 -111
  147. package/templates/variants/bun-connectrpc/gen/protos/chat/v1/chat_pb.ts +0 -1078
  148. package/templates/variants/bun-connectrpc/protos/chat/v1/chat.proto +0 -228
  149. package/templates/variants/bun-connectrpc/src/chat/service.ts +0 -384
  150. package/templates/variants/bun-connectrpc/src/chat/types.ts +0 -142
  151. package/templates/variants/bun-connectrpc/src/storage.ts +0 -72
  152. package/templates/variants/bun-connectrpc/src/webhooks.ts +0 -35
  153. package/templates/variants/bun-connectrpc/test/list-messages.integration.test.ts +0 -182
  154. package/templates/variants/bun-hono/src/chat/service.ts +0 -384
  155. package/templates/variants/bun-hono/src/chat/types.ts +0 -142
  156. package/templates/variants/bun-hono/src/storage.ts +0 -72
  157. package/templates/variants/bun-hono/src/webhooks.ts +0 -35
  158. package/templates/variants/bun-hono/test/list-messages.integration.test.ts +0 -256
  159. package/templates/variants/go-chi/buf.gen.yaml +0 -12
  160. package/templates/variants/go-chi/buf.yaml +0 -9
  161. package/templates/variants/go-chi/cmd/migrate/main.go +0 -101
  162. package/templates/variants/go-chi/internal/httpapi/list_messages_integration_test.go +0 -298
  163. package/templates/variants/go-chi/protos/chat/v1/chat.proto +0 -219
  164. package/templates/variants/go-connectrpc/cmd/migrate/main.go +0 -101
  165. package/templates/variants/go-connectrpc/gen/chat/v1/chat.pb.go +0 -2512
  166. package/templates/variants/go-connectrpc/gen/chat/v1/chatv1connect/chat.connect.go +0 -571
  167. package/templates/variants/go-connectrpc/internal/connectapi/list_messages_integration_test.go +0 -216
  168. package/templates/variants/go-connectrpc/protos/chat/v1/chat.proto +0 -232
@@ -0,0 +1,231 @@
1
+ import serviceConfig from "../service.config";
2
+ import { existsSync } from "node:fs";
3
+
4
+ type CommandResult = {
5
+ success: boolean;
6
+ stdout: string;
7
+ stderr: string;
8
+ exitCode: number;
9
+ };
10
+
11
+ const decoder = new TextDecoder();
12
+
13
+ export type AuthDoctorResult = {
14
+ hasAuthctl: boolean;
15
+ hasResourceServerCommands: boolean;
16
+ detail: string;
17
+ resourceServerCommand?: ResourceServerCommand;
18
+ };
19
+
20
+ type ResourceServerCommand = {
21
+ subject: string;
22
+ mutationAction?: "upsert" | "create";
23
+ actions: string[];
24
+ };
25
+
26
+ type ResourceServerMutationCommand = ResourceServerCommand & {
27
+ mutationAction: "upsert" | "create";
28
+ };
29
+
30
+ export function defaultAuthResourceServerArgs() {
31
+ const auth = serviceConfig.auth;
32
+ return [
33
+ "--resource-server",
34
+ auth.resource_server.id,
35
+ "--audience",
36
+ auth.resource_server.audience,
37
+ "--stage",
38
+ serviceConfig.stage_default,
39
+ ...auth.resource_server.default_scopes.flatMap((scope) => ["--scope", scope]),
40
+ ];
41
+ }
42
+
43
+ export function runAuthCommand(args: string[]) {
44
+ const [subject, action, ...rest] = args;
45
+
46
+ if (!subject || subject === "doctor") {
47
+ const result = runAuthDoctor();
48
+ if (!result.hasAuthctl) {
49
+ throw new Error(result.detail);
50
+ }
51
+ return result.detail;
52
+ }
53
+
54
+ if (subject === "resource-server" || subject === "resource-servers") {
55
+ const command = resolveResourceServerCommand();
56
+ if (!command) {
57
+ throw new Error(
58
+ "authctl is installed but does not expose resource-server commands; install @anmho/authctl@0.1.1 or newer before managing auth resource servers"
59
+ );
60
+ }
61
+ if (action === "get" || action === "list") {
62
+ if (!command.actions.includes(action)) {
63
+ throw new Error(`authctl ${command.subject} does not expose ${action}`);
64
+ }
65
+ authctl([command.subject, action, ...rest]);
66
+ return `Auth resource server ${action} finished`;
67
+ }
68
+ const mutation = ensureResourceServerCommandAvailable();
69
+ const subcommand = action ?? mutation.mutationAction;
70
+ if (!mutation.mutationAction || (subcommand !== mutation.mutationAction && !(subcommand === "upsert" && mutation.mutationAction === "create"))) {
71
+ throw new Error(`Usage: service auth resource-server [${mutation.mutationAction}] [authctl args]`);
72
+ }
73
+ authctl([mutation.subject, mutation.mutationAction, ...defaultAuthResourceServerArgs(), "--json", ...rest]);
74
+ return `Auth resource server ready: ${serviceConfig.auth.resource_server.id}`;
75
+ }
76
+
77
+ if (subject === "client" || subject === "clients") {
78
+ return runClientCommand(action, rest);
79
+ }
80
+
81
+ throw new Error("Usage: service auth <doctor|resource-server|client> [args]");
82
+ }
83
+
84
+ export function ensureAuthResourceServer() {
85
+ const command = ensureResourceServerCommandAvailable();
86
+ authctl([command.subject, command.mutationAction, ...defaultAuthResourceServerArgs(), "--json"]);
87
+ return `Auth resource server ready: ${serviceConfig.auth.resource_server.audience}`;
88
+ }
89
+
90
+ export function runAuthDoctor(): AuthDoctorResult {
91
+ if (!authctlPath()) {
92
+ return {
93
+ hasAuthctl: false,
94
+ hasResourceServerCommands: false,
95
+ detail: "authctl is not installed; run bun install in this generated service or link @anmho/authctl before service create",
96
+ };
97
+ }
98
+
99
+ const doctor = authctl(["doctor", "--json"], { allowFailure: true, quiet: true });
100
+ const resourceServerCommand = resolveResourceServerCommand();
101
+ const hasResourceServerCommands = Boolean(resourceServerCommand?.mutationAction);
102
+
103
+ if (!doctor.success) {
104
+ return {
105
+ hasAuthctl: true,
106
+ hasResourceServerCommands,
107
+ resourceServerCommand,
108
+ detail: `authctl doctor failed: ${doctor.stderr || doctor.stdout}`,
109
+ };
110
+ }
111
+
112
+ if (!hasResourceServerCommands) {
113
+ return {
114
+ hasAuthctl: true,
115
+ hasResourceServerCommands: false,
116
+ resourceServerCommand,
117
+ detail:
118
+ "authctl is installed but does not expose resource-server upsert/create; install @anmho/authctl@0.1.1 or newer before service create",
119
+ };
120
+ }
121
+
122
+ return {
123
+ hasAuthctl: true,
124
+ hasResourceServerCommands: true,
125
+ resourceServerCommand,
126
+ detail: `authctl ready for ${serviceConfig.auth.resource_server.id}`,
127
+ };
128
+ }
129
+
130
+ function runClientCommand(action = "", rest: string[]) {
131
+ if (action === "create") {
132
+ authctl([
133
+ "clients",
134
+ "create",
135
+ "--client-app",
136
+ serviceConfig.auth.client.app_id,
137
+ "--client-identity",
138
+ serviceConfig.auth.client.identity,
139
+ ...defaultClientTargetArgs(rest),
140
+ "--stage",
141
+ serviceConfig.stage_default,
142
+ "--yes",
143
+ "--json",
144
+ ...rest,
145
+ ]);
146
+ return "Auth client created";
147
+ }
148
+
149
+ if (["list", "get", "rotate", "revoke"].includes(action)) {
150
+ authctl(["clients", action, ...rest]);
151
+ return `Auth client ${action} finished`;
152
+ }
153
+
154
+ throw new Error("Usage: service auth client <create|list|get|rotate|revoke> [args]");
155
+ }
156
+
157
+ function defaultClientTargetArgs(rest: string[]) {
158
+ const hasResourceServer = hasFlag(rest, "--resource-server");
159
+ const hasScope = hasFlag(rest, "--scope");
160
+ return [
161
+ ...(hasResourceServer ? [] : ["--resource-server", serviceConfig.auth.resource_server.id]),
162
+ ...(hasScope ? [] : serviceConfig.auth.resource_server.default_scopes.flatMap((scope) => ["--scope", scope])),
163
+ ];
164
+ }
165
+
166
+ function hasFlag(args: string[], name: string) {
167
+ return args.some((arg) => arg === name || arg.startsWith(`${name}=`));
168
+ }
169
+
170
+ function ensureResourceServerCommandAvailable(): ResourceServerMutationCommand {
171
+ const doctor = runAuthDoctor();
172
+ if (!doctor.hasAuthctl || !doctor.hasResourceServerCommands) {
173
+ throw new Error(doctor.detail);
174
+ }
175
+ if (!doctor.resourceServerCommand?.mutationAction) {
176
+ throw new Error("authctl resource-server command discovery failed");
177
+ }
178
+ return doctor.resourceServerCommand as ResourceServerMutationCommand;
179
+ }
180
+
181
+ function resolveResourceServerCommand(): ResourceServerCommand | undefined {
182
+ for (const subject of ["resource-servers", "resource-server", "resources"]) {
183
+ const help = authctl([subject, "--help"], { allowFailure: true, quiet: true });
184
+ const output = `${help.stdout}\n${help.stderr}`;
185
+ if (!help.success || !output.includes(subject)) {
186
+ continue;
187
+ }
188
+ const actions = ["upsert", "create", "get", "list"].filter((candidate) => output.includes(candidate));
189
+ const mutationAction = actions.includes("upsert") ? "upsert" : actions.includes("create") ? "create" : undefined;
190
+ if (actions.length > 0) {
191
+ return { subject, mutationAction, actions };
192
+ }
193
+ }
194
+ return undefined;
195
+ }
196
+
197
+ function authctl(args: string[], options: { allowFailure?: boolean; quiet?: boolean } = {}): CommandResult {
198
+ const command = authctlPath();
199
+ if (!command) {
200
+ throw new Error("authctl is not installed; run bun install in this generated service or link @anmho/authctl");
201
+ }
202
+
203
+ const result = Bun.spawnSync([command, ...args], {
204
+ cwd: process.cwd(),
205
+ env: process.env,
206
+ stdin: "inherit",
207
+ stdout: "pipe",
208
+ stderr: "pipe",
209
+ });
210
+
211
+ const output = {
212
+ success: result.success,
213
+ stdout: result.stdout ? decoder.decode(result.stdout).trim() : "",
214
+ stderr: result.stderr ? decoder.decode(result.stderr).trim() : "",
215
+ exitCode: result.exitCode,
216
+ };
217
+
218
+ if (!output.success && !options.allowFailure) {
219
+ throw new Error(`authctl ${args.join(" ")} failed with exit code ${output.exitCode}\n${output.stderr || output.stdout}`);
220
+ }
221
+
222
+ if (output.stdout && !options.quiet) {
223
+ console.log(output.stdout);
224
+ }
225
+
226
+ return output;
227
+ }
228
+
229
+ function authctlPath() {
230
+ return existsSync("./node_modules/.bin/authctl") ? "./node_modules/.bin/authctl" : Bun.which("authctl");
231
+ }
@@ -1,5 +1,4 @@
1
1
  import { config } from "./config";
2
- import { publishProviderRuntimeSecrets } from "./integrations";
3
2
  import { ensureDatabase, getConnectionUri, resolveNeonConfig } from "./neon";
4
3
  import {
5
4
  addSecretVersion,
@@ -9,11 +8,11 @@ import {
9
8
  ensureProjectRole,
10
9
  ensureSecretAccessor,
11
10
  ensureServiceAccount,
12
- ensureStorageBucket,
13
11
  gcloud,
14
12
  requireCommand,
15
13
  requireGcloudAuth,
16
14
  resolveDeploymentTarget,
15
+ resolveTemporalRuntimeConfig,
17
16
  runMain,
18
17
  runStep,
19
18
  } from "./lib";
@@ -31,8 +30,6 @@ export async function bootstrap() {
31
30
  });
32
31
 
33
32
  await runStep("Ensuring Artifact Registry repository", () => ensureArtifactRepository());
34
- await runStep("Ensuring attachment storage bucket", () => ensureStorageBucket());
35
-
36
33
  await runStep("Granting project roles", () => {
37
34
  ensureProjectRole(`serviceAccount:${config.runtimeServiceAccount}`, "roles/secretmanager.secretAccessor");
38
35
  });
@@ -53,7 +50,19 @@ export async function bootstrap() {
53
50
  ensureSecretAccessor(target.databaseSecretName, `serviceAccount:${config.runtimeServiceAccount}`);
54
51
  });
55
52
 
56
- await runStep("Publishing provider runtime secrets", () => publishProviderRuntimeSecrets(target));
53
+ await runStep("Publishing Temporal secrets", () => publishTemporalSecrets());
54
+ }
55
+
56
+ function publishTemporalSecrets() {
57
+ const temporal = resolveTemporalRuntimeConfig();
58
+ const apiKey = process.env.TEMPORAL_API_KEY?.trim();
59
+ if (!apiKey || !temporal.apiKeySecretName) {
60
+ return "No Temporal API key configured";
61
+ }
62
+
63
+ addSecretVersion(temporal.apiKeySecretName, apiKey);
64
+ ensureSecretAccessor(temporal.apiKeySecretName, `serviceAccount:${config.runtimeServiceAccount}`);
65
+ return temporal.apiKeySecretName;
57
66
  }
58
67
 
59
68
  if (import.meta.main) {
@@ -1,17 +1,22 @@
1
- import { log } from "@clack/prompts";
1
+ import { confirm, isCancel, log } from "@clack/prompts";
2
2
  import { config } from "./config";
3
3
  import { deleteBranch, deleteDatabase, listBranches, resolveNeonConfig } from "./neon";
4
4
  import {
5
+ assertOwnedResource,
5
6
  deleteProject,
6
7
  deleteProductionDomainMapping,
7
8
  deleteSecret,
8
9
  deleteService,
9
10
  deleteServiceAccount,
11
+ describeCloudRunService,
12
+ describeProductionDomainMapping,
13
+ describeSecret,
10
14
  listCloudRunServices,
11
15
  listSecrets,
12
16
  parseCleanupArgs,
13
17
  requireCommand,
14
18
  requireGcloudAuth,
19
+ run,
15
20
  runMain,
16
21
  runStep,
17
22
  } from "./lib";
@@ -21,7 +26,12 @@ function matchesServiceResource(name: string) {
21
26
  }
22
27
 
23
28
  function matchesSecretResource(name: string) {
24
- return name === `${config.serviceName}-database-url` || name.startsWith(`${config.serviceName}-pr-`) || name.startsWith(`${config.serviceName}-dev-`);
29
+ return (
30
+ name === `${config.serviceName}-database-url` ||
31
+ name === config.temporal.apiKeySecretName ||
32
+ name.startsWith(`${config.serviceName}-pr-`) ||
33
+ name.startsWith(`${config.serviceName}-dev-`)
34
+ );
25
35
  }
26
36
 
27
37
  export async function cleanup(args = Bun.argv.slice(2)) {
@@ -29,13 +39,16 @@ export async function cleanup(args = Bun.argv.slice(2)) {
29
39
  requireGcloudAuth();
30
40
 
31
41
  const options = parseCleanupArgs(args);
42
+ await requireDestroyConfirmation(options.force);
32
43
 
44
+ await runStep(`Verifying production domain mapping ${config.domain.hostname}`, () => assertProductionDomainMappingOwned());
33
45
  await runStep(`Deleting production domain mapping ${config.domain.hostname}`, () => deleteProductionDomainMapping());
34
46
 
35
47
  const services = await runStep("Finding Cloud Run services", () => listCloudRunServices());
36
48
  const serviceNames = services.filter(matchesServiceResource);
37
49
  await runStep("Deleting Cloud Run services", () => {
38
50
  for (const serviceName of serviceNames) {
51
+ assertOwnedResource(`Cloud Run service ${serviceName}`, describeCloudRunService(serviceName));
39
52
  deleteService(serviceName);
40
53
  }
41
54
  });
@@ -44,6 +57,7 @@ export async function cleanup(args = Bun.argv.slice(2)) {
44
57
  const secretNames = secrets.filter(matchesSecretResource);
45
58
  await runStep("Deleting service secrets", () => {
46
59
  for (const secretName of secretNames) {
60
+ assertOwnedResource(`Secret ${secretName}`, describeSecret(secretName));
47
61
  deleteSecret(secretName);
48
62
  }
49
63
  });
@@ -68,6 +82,8 @@ export async function cleanup(args = Bun.argv.slice(2)) {
68
82
  log.step(error instanceof Error ? error.message : String(error));
69
83
  }
70
84
 
85
+ await runStep("Deleting Grafana resources", async () => deleteGrafanaResources());
86
+
71
87
  await runStep("Deleting service-specific identity resources", () => {
72
88
  deleteServiceAccount(config.runtimeServiceAccount);
73
89
  });
@@ -78,9 +94,53 @@ export async function cleanup(args = Bun.argv.slice(2)) {
78
94
  }
79
95
 
80
96
  log.step(`Production API hostname released: ${config.domain.hostname}`);
81
- return `Cleanup finished for ${config.serviceName}`;
97
+ return `Destroy finished for ${config.serviceName}`;
98
+ }
99
+
100
+ async function deleteGrafanaResources() {
101
+ if (!(await Bun.file("./grafana").exists())) {
102
+ return "No grafana directory configured";
103
+ }
104
+ if (!Bun.which("gcx")) {
105
+ return "gcx is not installed; Grafana resources were not deleted";
106
+ }
107
+
108
+ run("gcx", ["resources", "delete", "--path", "./grafana", "--yes", "--on-error", "ignore"]);
109
+ return "Grafana resources deleted from local manifests";
110
+ }
111
+
112
+ function assertProductionDomainMappingOwned() {
113
+ const mapping = describeProductionDomainMapping();
114
+ if (!mapping) {
115
+ return;
116
+ }
117
+
118
+ const routeName = mapping.spec?.routeName;
119
+ if (routeName !== config.serviceName) {
120
+ throw new Error(`${config.domain.hostname} maps to ${routeName || "an unknown service"}; refusing to delete ambiguous DNS mapping`);
121
+ }
122
+
123
+ assertOwnedResource(`Cloud Run service ${routeName}`, describeCloudRunService(routeName));
124
+ }
125
+
126
+ async function requireDestroyConfirmation(force: boolean) {
127
+ if (force) {
128
+ return;
129
+ }
130
+
131
+ if (!process.stdin.isTTY) {
132
+ throw new Error("service destroy requires --force when running non-interactively");
133
+ }
134
+
135
+ const answer = await confirm({
136
+ message: `Destroy resources owned by ${config.serviceName}?`,
137
+ initialValue: false,
138
+ });
139
+ if (isCancel(answer) || !answer) {
140
+ throw new Error("Destroy cancelled");
141
+ }
82
142
  }
83
143
 
84
144
  if (import.meta.main) {
85
- await runMain("Cleanup", () => cleanup(Bun.argv.slice(2)));
145
+ await runMain("Destroy", () => cleanup(Bun.argv.slice(2)));
86
146
  }