create-svc 0.1.85 → 0.1.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-svc",
3
- "version": "0.1.85",
3
+ "version": "0.1.87",
4
4
  "description": "Local microservice bootstrap CLI for Cloud Run and Workers services with Neon-backed data.",
5
5
  "module": "index.ts",
6
6
  "type": "module",
@@ -113,6 +113,8 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
113
113
 
114
114
  const dockerCompose = await Bun.file(join(generatedRoot, "docker-compose.yml")).text();
115
115
  expect(dockerCompose).toContain('image: postgres:16-alpine');
116
+ expect(dockerCompose).toContain("image: temporalio/auto-setup:");
117
+ expect(dockerCompose).toContain("127.0.0.1:7233:7233");
116
118
  expect(dockerCompose).toContain(`127.0.0.1:${localPort}:5432`);
117
119
 
118
120
  const envExample = await Bun.file(join(generatedRoot, ".env.example")).text();
@@ -337,7 +339,7 @@ test("scaffolds a backend package cleanly into a nested monorepo-style directory
337
339
  expect(readme).toContain("`microservice` profile");
338
340
  expect(readme).toContain("api.dns-api.anmho.com");
339
341
  expect(readme).toContain("open Docker Desktop if needed");
340
- expect(readme).toContain("local Postgres service in `docker-compose.yml`");
342
+ expect(readme).toContain("local Postgres and Temporal services in `docker-compose.yml`");
341
343
  expect(readme).toContain("gcloud auth login");
342
344
  expect(readme).toContain("known-good CLIs");
343
345
  expect(readme).toContain("service create");
package/src/scaffold.ts CHANGED
@@ -244,6 +244,8 @@ function buildReplacements(config: ScaffoldConfig) {
244
244
  COMMAND_GEN: config.runtime === "bun" ? "bun run gen" : "make gen",
245
245
  COMMAND_LINT: config.runtime === "bun" ? "bun run lint" : "make lint",
246
246
  COMMAND_TEST: config.runtime === "bun" ? "bun run test" : "make test",
247
+ COMMAND_TEST_E2E_LOCAL: config.runtime === "bun" ? "bun run test:e2e:local" : "make test-e2e-local",
248
+ COMMAND_TEST_E2E_PROD: config.runtime === "bun" ? "bun run test:e2e:prod" : "make test-e2e-prod",
247
249
  COMMAND_DEV_DOWN: "service dev down",
248
250
  COMMAND_BOOTSTRAP: "service create",
249
251
  COMMAND_DEPLOY: "service deploy",
@@ -0,0 +1,12 @@
1
+ export type AuthctlCommand = {
2
+ path: string;
3
+ runWithBun: boolean;
4
+ };
5
+
6
+ export function authctlSpawnArgs(command: AuthctlCommand, args: string[]) {
7
+ return command.runWithBun ? [bunExecutable(), command.path, ...args] : [command.path, ...args];
8
+ }
9
+
10
+ function bunExecutable() {
11
+ return Bun.which("bun") ?? process.execPath;
12
+ }
@@ -0,0 +1,16 @@
1
+ import { expect, test } from "bun:test";
2
+ import { authctlSpawnArgs } from "./authctl-command";
3
+
4
+ test("authctlSpawnArgs runs repo-local authctl through bun", () => {
5
+ const args = authctlSpawnArgs({ path: "./node_modules/.bin/authctl", runWithBun: true }, ["doctor", "--json"]);
6
+
7
+ expect(args[0]).toEndWith("bun");
8
+ expect(args.slice(1)).toEqual(["./node_modules/.bin/authctl", "doctor", "--json"]);
9
+ });
10
+
11
+ test("authctlSpawnArgs runs global authctl directly", () => {
12
+ expect(authctlSpawnArgs({ path: "/usr/local/bin/authctl", runWithBun: false }, ["version"])).toEqual([
13
+ "/usr/local/bin/authctl",
14
+ "version",
15
+ ]);
16
+ });
@@ -1,4 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
+ import { authctlSpawnArgs, type AuthctlCommand } from "./authctl-command";
2
3
  import { serviceConfig } from "./runtime";
3
4
 
4
5
  type CommandResult = {
@@ -9,6 +10,7 @@ type CommandResult = {
9
10
  };
10
11
 
11
12
  const decoder = new TextDecoder();
13
+ const localAuthctlPath = "./node_modules/.bin/authctl";
12
14
 
13
15
  export type AuthDoctorResult = {
14
16
  hasAuthctl: boolean;
@@ -418,12 +420,12 @@ function resolveResourceServerCommand(): ResourceServerCommand | undefined {
418
420
  }
419
421
 
420
422
  function authctl(args: string[], options: { allowFailure?: boolean; quiet?: boolean } = {}): CommandResult {
421
- const command = authctlPath();
423
+ const command = authctlCommand();
422
424
  if (!command) {
423
425
  throw new Error("authctl is not installed; run bun install in this generated service or link @anmho/authctl");
424
426
  }
425
427
 
426
- const result = Bun.spawnSync([command, ...args], {
428
+ const result = Bun.spawnSync(authctlSpawnArgs(command, args), {
427
429
  cwd: process.cwd(),
428
430
  env: authctlEnvironment(),
429
431
  stdin: "inherit",
@@ -464,8 +466,24 @@ function formatAuthctlFailure(args: string[], output: CommandResult) {
464
466
  return `authctl ${args.join(" ")} failed with exit code ${output.exitCode}\n${detail}`;
465
467
  }
466
468
 
469
+ function authctlCommand(): AuthctlCommand | undefined {
470
+ if (existsSync(localAuthctlPath)) {
471
+ return {
472
+ path: localAuthctlPath,
473
+ runWithBun: true,
474
+ };
475
+ }
476
+ const global = Bun.which("authctl");
477
+ return global
478
+ ? {
479
+ path: global,
480
+ runWithBun: false,
481
+ }
482
+ : undefined;
483
+ }
484
+
467
485
  function authctlPath() {
468
- return existsSync("./node_modules/.bin/authctl") ? "./node_modules/.bin/authctl" : Bun.which("authctl");
486
+ return authctlCommand()?.path;
469
487
  }
470
488
 
471
489
  function authctlEnvironment() {
@@ -29,6 +29,8 @@ console to create and deploy.
29
29
  {{COMMAND_GEN}}
30
30
  {{COMMAND_LINT}}
31
31
  {{COMMAND_TEST}}
32
+ {{COMMAND_TEST_E2E_LOCAL}}
33
+ {{COMMAND_TEST_E2E_PROD}}
32
34
  {{COMMAND_BOOTSTRAP}}
33
35
  {{COMMAND_DEPLOY}}
34
36
  {{COMMAND_PROTECT_MAIN}}
@@ -43,7 +45,7 @@ console to create and deploy.
43
45
 
44
46
  ## Local development
45
47
 
46
- The scaffold writes a ready-to-use `.env.local` and includes a local Postgres service in `docker-compose.yml`.
48
+ The scaffold writes a ready-to-use `.env.local` and includes local Postgres and Temporal services in `docker-compose.yml`.
47
49
 
48
50
  First local run:
49
51
 
@@ -55,11 +57,26 @@ First local run:
55
57
  Local runtime uses:
56
58
 
57
59
  - `DATABASE_URL` from `.env.local`, pointed at Docker Compose Postgres
58
- - `{{COMMAND_MIGRATE}}` and `{{COMMAND_DEV}}`, which open Docker Desktop if needed, wait for Docker readiness, and start Docker Compose Postgres
59
- - `TEMPORAL_ENABLED=true` by default with `localhost:7233` and `default`; set `TEMPORAL_ENABLED=false` when you are not running a local Temporal server
60
+ - `{{COMMAND_MIGRATE}}` and `{{COMMAND_DEV}}`, which open Docker Desktop if needed, wait for Docker readiness, and start Docker Compose Postgres and Temporal
61
+ - `TEMPORAL_ENABLED=true` by default with `localhost:7233` and `default`; `{{COMMAND_DEV}}` waits for the local Temporal container before starting the API and worker
60
62
 
61
63
  No cloud credentials are required for local HTTP development after Docker and Postgres are running.
62
64
 
65
+ Run the local end-to-end test against the already-running local service:
66
+
67
+ ```bash
68
+ {{COMMAND_TEST_E2E_LOCAL}}
69
+ ```
70
+
71
+ The production end-to-end test exercises health and public webhook idempotency,
72
+ then requires Cloud Logging rows and Cloud Monitoring
73
+ `run.googleapis.com/container/instance_count` rows for the current Cloud Run API
74
+ and worker revisions:
75
+
76
+ ```bash
77
+ {{COMMAND_TEST_E2E_PROD}}
78
+ ```
79
+
63
80
  ## Temporal
64
81
 
65
82
  Temporal is enabled by default for generated services.
@@ -78,7 +95,7 @@ creates a trigger, the API service starts `waitlistFollowUpWorkflow` /
78
95
  `WaitlistFollowUpWorkflow` asynchronously on the service task queue. The API
79
96
  request only waits for the trigger record; workflow completion happens through
80
97
  Temporal and is polled by the worker service.
81
- Local `{{COMMAND_DEV}}` starts the API process and the worker process together.
98
+ Local `{{COMMAND_DEV}}` starts the API process and the worker process together after Docker Compose starts the local Temporal server.
82
99
 
83
100
  Production and preview deploys render `TEMPORAL_ENABLED=true` into the Cloud Run
84
101
  manifest unless you override it. For Temporal Cloud, replace the local defaults
@@ -15,5 +15,21 @@ services:
15
15
  timeout: 5s
16
16
  retries: 10
17
17
 
18
+ temporal:
19
+ image: temporalio/auto-setup:1.28.1
20
+ depends_on:
21
+ postgres:
22
+ condition: service_healthy
23
+ environment:
24
+ DB: postgres12
25
+ DB_PORT: 5432
26
+ POSTGRES_USER: {{LOCAL_DATABASE_USER}}
27
+ POSTGRES_PWD: {{LOCAL_DATABASE_PASSWORD}}
28
+ POSTGRES_SEEDS: postgres
29
+ DBNAME: temporal
30
+ VISIBILITY_DBNAME: temporal_visibility
31
+ ports:
32
+ - "127.0.0.1:7233:7233"
33
+
18
34
  volumes:
19
35
  postgres-data:
@@ -16,6 +16,9 @@ const env = {
16
16
  if (env.DATABASE_URL && !env.CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE) {
17
17
  env.CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE = env.DATABASE_URL;
18
18
  }
19
+ if (temporalEnabled(env)) {
20
+ await waitForTemporal(env.TEMPORAL_ADDRESS || "localhost:7233");
21
+ }
19
22
 
20
23
  const api = Bun.spawn(apiCommand, {
21
24
  stdin: "inherit",
@@ -57,3 +60,36 @@ function parseCommands(argv: string[]) {
57
60
  const workerCommand = argv.slice(separator + 1);
58
61
  return { apiCommand, workerCommand: workerCommand.length > 0 ? workerCommand : undefined };
59
62
  }
63
+
64
+ function temporalEnabled(env: Record<string, string | undefined>) {
65
+ return (env.TEMPORAL_ENABLED ?? "true").trim().toLowerCase() !== "false";
66
+ }
67
+
68
+ async function waitForTemporal(address: string) {
69
+ const { host, port } = parseTemporalAddress(address);
70
+ const deadline = Date.now() + 120_000;
71
+
72
+ while (Date.now() < deadline) {
73
+ const exitCode = await Bun.spawn(["nc", "-z", host, String(port)], {
74
+ stdin: "ignore",
75
+ stdout: "ignore",
76
+ stderr: "ignore",
77
+ }).exited;
78
+ if (exitCode === 0) {
79
+ return;
80
+ }
81
+ await Bun.sleep(2_000);
82
+ }
83
+
84
+ throw new Error(`Temporal did not become ready at ${host}:${port} within 120 seconds`);
85
+ }
86
+
87
+ function parseTemporalAddress(address: string) {
88
+ const trimmed = address.trim();
89
+ const withoutScheme = trimmed.includes("://") ? new URL(trimmed).host : trimmed;
90
+ const [host = "localhost", port = "7233"] = withoutScheme.split(":");
91
+ return {
92
+ host: host || "localhost",
93
+ port: Number(port || "7233"),
94
+ };
95
+ }
@@ -0,0 +1,275 @@
1
+ type Target = "local" | "prod";
2
+
3
+ export {};
4
+
5
+ type CommandOptions = {
6
+ allowFailure?: boolean;
7
+ };
8
+
9
+ type CloudRunService = {
10
+ status?: {
11
+ latestReadyRevisionName?: string;
12
+ };
13
+ };
14
+
15
+ type MonitoringSeries = {
16
+ resource?: {
17
+ labels?: Record<string, string>;
18
+ };
19
+ points?: Array<{
20
+ value?: {
21
+ int64Value?: string;
22
+ doubleValue?: number;
23
+ };
24
+ interval?: {
25
+ endTime?: string;
26
+ };
27
+ }>;
28
+ };
29
+
30
+ const args = parseArgs(Bun.argv.slice(2));
31
+ const serviceName = "{{SERVICE_NAME}}";
32
+ const projectId = "{{PROJECT_ID}}";
33
+ const region = "{{REGION}}";
34
+ const apiHostname = "{{API_HOSTNAME}}";
35
+ const baseUrl = args.url ?? (args.target === "prod" ? `https://${apiHostname}` : `http://127.0.0.1:${Bun.env.PORT || "3000"}`);
36
+ const proofId = `e2e-${Date.now()}-${Math.random().toString(16).slice(2)}`;
37
+
38
+ section(`${serviceName} ${args.target} e2e`);
39
+ detail("base_url", baseUrl);
40
+ detail("event_id", proofId);
41
+
42
+ await requestJSON(`${baseUrl}/healthz`, { expectStatus: 200 });
43
+
44
+ const webhookPayload = {
45
+ id: proofId,
46
+ source: "generated-e2e",
47
+ service: serviceName,
48
+ target: args.target,
49
+ };
50
+ const firstWebhook = await requestJSON(`${baseUrl}/webhooks/generated-e2e`, {
51
+ method: "POST",
52
+ headers: {
53
+ "content-type": "application/json",
54
+ "x-webhook-event-id": proofId,
55
+ },
56
+ body: JSON.stringify(webhookPayload),
57
+ expectStatus: 202,
58
+ });
59
+ detail("webhook_first", JSON.stringify(firstWebhook));
60
+
61
+ const secondWebhook = await requestJSON(`${baseUrl}/webhooks/generated-e2e`, {
62
+ method: "POST",
63
+ headers: {
64
+ "content-type": "application/json",
65
+ "x-webhook-event-id": proofId,
66
+ },
67
+ body: JSON.stringify(webhookPayload),
68
+ expectStatus: 200,
69
+ });
70
+ detail("webhook_duplicate", JSON.stringify(secondWebhook));
71
+ if (!isDuplicateWebhook(secondWebhook)) {
72
+ throw new Error("second webhook delivery did not report duplicate=true");
73
+ }
74
+
75
+ if (args.target === "prod") {
76
+ await Bun.sleep(5_000);
77
+ const state = await printCloudRunState();
78
+ await printCloudLogs(serviceName, state.apiRevision);
79
+ await printCloudLogs(`${serviceName}-worker`, state.workerRevision);
80
+ await printCloudMetrics(state);
81
+ }
82
+
83
+ section("e2e complete");
84
+
85
+ async function requestJSON(
86
+ url: string,
87
+ options: RequestInit & { expectStatus: number } = { expectStatus: 200 }
88
+ ) {
89
+ const response = await fetch(url, {
90
+ ...options,
91
+ signal: AbortSignal.timeout(15_000),
92
+ });
93
+ const text = await response.text();
94
+ if (response.status !== options.expectStatus) {
95
+ throw new Error(`${url} returned ${response.status}, expected ${options.expectStatus}: ${text}`);
96
+ }
97
+ if (!text) {
98
+ return {};
99
+ }
100
+ try {
101
+ return JSON.parse(text);
102
+ } catch {
103
+ return { text };
104
+ }
105
+ }
106
+
107
+ function isDuplicateWebhook(value: unknown) {
108
+ return Boolean(value && typeof value === "object" && "duplicate" in value && value.duplicate === true);
109
+ }
110
+
111
+ async function printCloudRunState() {
112
+ const [api, worker] = await Promise.all([
113
+ describeCloudRunService(serviceName),
114
+ describeCloudRunService(`${serviceName}-worker`),
115
+ ]);
116
+ const apiRevision = api.status?.latestReadyRevisionName ?? "";
117
+ const workerRevision = worker.status?.latestReadyRevisionName ?? "";
118
+ if (!apiRevision) {
119
+ throw new Error(`Cloud Run service ${serviceName} did not report latestReadyRevisionName`);
120
+ }
121
+ if (!workerRevision) {
122
+ throw new Error(`Cloud Run service ${serviceName}-worker did not report latestReadyRevisionName`);
123
+ }
124
+ detail("api_revision", apiRevision);
125
+ detail("worker_revision", workerRevision);
126
+ return { apiRevision, workerRevision };
127
+ }
128
+
129
+ async function describeCloudRunService(name: string): Promise<CloudRunService> {
130
+ const output = await command([
131
+ "gcloud",
132
+ "run",
133
+ "services",
134
+ "describe",
135
+ name,
136
+ "--project",
137
+ projectId,
138
+ "--region",
139
+ region,
140
+ "--format=json",
141
+ ]);
142
+ return JSON.parse(output || "{}") as CloudRunService;
143
+ }
144
+
145
+ async function printCloudLogs(name: string, revision: string) {
146
+ const filter = [
147
+ 'resource.type="cloud_run_revision"',
148
+ `resource.labels.service_name="${name}"`,
149
+ `resource.labels.revision_name="${revision}"`,
150
+ ].join(" AND ");
151
+ const output = await command([
152
+ "gcloud",
153
+ "logging",
154
+ "read",
155
+ filter,
156
+ "--project",
157
+ projectId,
158
+ "--limit=3",
159
+ "--format=json",
160
+ ]);
161
+ const rows = JSON.parse(output || "[]") as unknown[];
162
+ section(`cloud logs ${name}`);
163
+ if (rows.length === 0) {
164
+ throw new Error(`Cloud Logging did not return rows for ${name} revision ${revision}`);
165
+ }
166
+ console.log(JSON.stringify(rows, null, 2));
167
+ }
168
+
169
+ async function printCloudMetrics(expected: { apiRevision: string; workerRevision: string }) {
170
+ const expectedRevisions = new Set([expected.apiRevision, expected.workerRevision]);
171
+ for (let attempt = 1; attempt <= 6; attempt += 1) {
172
+ const rows = await readCloudMetrics();
173
+ const seenRevisions = new Set(rows.map((row) => row.revision).filter(Boolean));
174
+ section(`cloud metrics container/instance_count attempt ${attempt}`);
175
+ for (const row of rows) {
176
+ console.log(`${row.service}\t${row.revision}\t${row.value}\t${row.endTime}`);
177
+ }
178
+ const missing = [...expectedRevisions].filter((revision) => !seenRevisions.has(revision));
179
+ if (missing.length === 0) {
180
+ return;
181
+ }
182
+ if (attempt === 6) {
183
+ throw new Error(`Cloud Monitoring did not return current revision metrics: ${missing.join(", ")}`);
184
+ }
185
+ detail("metrics_waiting_for_revisions", missing.join(", "));
186
+ await Bun.sleep(20_000);
187
+ }
188
+ }
189
+
190
+ async function readCloudMetrics() {
191
+ const end = new Date().toISOString();
192
+ const start = new Date(Date.now() - 20 * 60_000).toISOString();
193
+ const accessToken = await command(["gcloud", "auth", "print-access-token"]);
194
+ const filter = `metric.type="run.googleapis.com/container/instance_count" AND resource.labels.service_name=starts_with("${serviceName}")`;
195
+ const url = new URL(`https://monitoring.googleapis.com/v3/projects/${projectId}/timeSeries`);
196
+ url.searchParams.set("filter", filter);
197
+ url.searchParams.set("interval.startTime", start);
198
+ url.searchParams.set("interval.endTime", end);
199
+ url.searchParams.set("aggregation.alignmentPeriod", "60s");
200
+ url.searchParams.set("aggregation.perSeriesAligner", "ALIGN_SUM");
201
+ url.searchParams.set("view", "FULL");
202
+ const response = await fetch(url, {
203
+ headers: { authorization: `Bearer ${accessToken}` },
204
+ signal: AbortSignal.timeout(15_000),
205
+ });
206
+ if (!response.ok) {
207
+ throw new Error(`Cloud Monitoring query failed: ${response.status} ${await response.text()}`);
208
+ }
209
+ const data = (await response.json()) as { timeSeries?: MonitoringSeries[] };
210
+ return ((data.timeSeries ?? []) as MonitoringSeries[]).map((series) => {
211
+ const labels = series.resource?.labels ?? {};
212
+ const point = series.points?.[0];
213
+ const value = point?.value?.int64Value ?? point?.value?.doubleValue ?? "";
214
+ return {
215
+ service: labels.service_name ?? "",
216
+ revision: labels.revision_name ?? "",
217
+ value,
218
+ endTime: point?.interval?.endTime ?? "",
219
+ };
220
+ });
221
+ }
222
+
223
+ async function command(commandArgs: string[], options: CommandOptions = {}) {
224
+ const process = Bun.spawn(commandArgs, {
225
+ stdin: "ignore",
226
+ stdout: "pipe",
227
+ stderr: "pipe",
228
+ env: Bun.env,
229
+ });
230
+ const [stdout, stderr, exitCode] = await Promise.all([
231
+ new Response(process.stdout).text(),
232
+ new Response(process.stderr).text(),
233
+ process.exited,
234
+ ]);
235
+ if (exitCode !== 0 && !options.allowFailure) {
236
+ throw new Error(`${commandArgs.join(" ")} failed with exit code ${exitCode}\n${stderr.trim()}`);
237
+ }
238
+ return stdout.trim();
239
+ }
240
+
241
+ function parseArgs(argv: string[]) {
242
+ let target: Target = "local";
243
+ let url = "";
244
+ for (let index = 0; index < argv.length; index += 1) {
245
+ const arg = argv[index];
246
+ if (arg === "--local") {
247
+ target = "local";
248
+ } else if (arg === "--prod") {
249
+ target = "prod";
250
+ } else if (arg === "--url") {
251
+ const value = argv[index + 1];
252
+ if (!value || value.startsWith("-")) {
253
+ throw new Error("Missing value for --url");
254
+ }
255
+ url = value;
256
+ index += 1;
257
+ } else if (arg.startsWith("--url=")) {
258
+ url = arg.slice("--url=".length);
259
+ } else if (arg === "--help" || arg === "-h") {
260
+ console.log("Usage: bun run ./scripts/e2e.ts [--local|--prod] [--url <origin>]");
261
+ process.exit(0);
262
+ } else {
263
+ throw new Error(`Unknown argument: ${arg}`);
264
+ }
265
+ }
266
+ return { target, url };
267
+ }
268
+
269
+ function section(title: string) {
270
+ console.log(`\n== ${title} ==`);
271
+ }
272
+
273
+ function detail(key: string, value: string) {
274
+ console.log(`${key}: ${value}`);
275
+ }
@@ -1,6 +1,6 @@
1
1
  export async function ensureLocalPostgres() {
2
2
  await ensureDockerRunning();
3
- await run(["docker", "compose", "up", "-d"], { label: "start local postgres" });
3
+ await run(["docker", "compose", "up", "-d"], { label: "start local services" });
4
4
  }
5
5
 
6
6
  async function ensureDockerRunning() {
@@ -1,4 +1,4 @@
1
- .PHONY: dev migrate gen lint test create deploy protect-main dashboards observability-bootstrap auth destroy
1
+ .PHONY: dev migrate gen lint test test-e2e test-e2e-local test-e2e-prod create deploy protect-main dashboards observability-bootstrap auth destroy
2
2
 
3
3
  SERVICE := service
4
4
 
@@ -17,6 +17,15 @@ lint:
17
17
  test:
18
18
  bun test
19
19
 
20
+ test-e2e:
21
+ bun run ./scripts/e2e.ts $(ARGS)
22
+
23
+ test-e2e-local:
24
+ bun run ./scripts/e2e.ts --local $(ARGS)
25
+
26
+ test-e2e-prod:
27
+ bun run ./scripts/e2e.ts --prod $(ARGS)
28
+
20
29
  create:
21
30
  $(SERVICE) create
22
31
 
@@ -9,6 +9,9 @@
9
9
  "gen": "bun run ./scripts/codegen.ts",
10
10
  "lint": "tsc --noEmit",
11
11
  "test": "bun test",
12
+ "test:e2e": "bun run ./scripts/e2e.ts",
13
+ "test:e2e:local": "bun run ./scripts/e2e.ts --local",
14
+ "test:e2e:prod": "bun run ./scripts/e2e.ts --prod",
12
15
  "create": "service create",
13
16
  "deploy": "service deploy",
14
17
  "protect-main": "service protect-main",
@@ -1,4 +1,4 @@
1
- .PHONY: dev migrate gen lint test create deploy protect-main dashboards observability-bootstrap auth destroy
1
+ .PHONY: dev migrate gen lint test test-e2e test-e2e-local test-e2e-prod create deploy protect-main dashboards observability-bootstrap auth destroy
2
2
 
3
3
  SERVICE := service
4
4
 
@@ -17,6 +17,15 @@ lint:
17
17
  test:
18
18
  bun test
19
19
 
20
+ test-e2e:
21
+ bun run ./scripts/e2e.ts $(ARGS)
22
+
23
+ test-e2e-local:
24
+ bun run ./scripts/e2e.ts --local $(ARGS)
25
+
26
+ test-e2e-prod:
27
+ bun run ./scripts/e2e.ts --prod $(ARGS)
28
+
20
29
  create:
21
30
  $(SERVICE) create
22
31
 
@@ -9,6 +9,9 @@
9
9
  "gen": "bun run ./scripts/codegen.ts",
10
10
  "lint": "tsc --noEmit",
11
11
  "test": "bun test",
12
+ "test:e2e": "bun run ./scripts/e2e.ts",
13
+ "test:e2e:local": "bun run ./scripts/e2e.ts --local",
14
+ "test:e2e:prod": "bun run ./scripts/e2e.ts --prod",
12
15
  "create": "service create",
13
16
  "deploy": "service deploy",
14
17
  "protect-main": "service protect-main",
@@ -1,4 +1,4 @@
1
- .PHONY: dev migrate migrate-lint gen lint test create deploy protect-main dashboards observability-bootstrap auth destroy
1
+ .PHONY: dev migrate migrate-lint gen lint test test-e2e test-e2e-local test-e2e-prod create deploy protect-main dashboards observability-bootstrap auth destroy
2
2
 
3
3
  SERVICE := service
4
4
  WITH_ENV := set -a; [ ! -f .env.local ] || . ./.env.local; set +a;
@@ -27,6 +27,15 @@ lint:
27
27
  test:
28
28
  bun test ./test
29
29
 
30
+ test-e2e:
31
+ bun run ./scripts/e2e.ts $(ARGS)
32
+
33
+ test-e2e-local:
34
+ bun run ./scripts/e2e.ts --local $(ARGS)
35
+
36
+ test-e2e-prod:
37
+ bun run ./scripts/e2e.ts --prod $(ARGS)
38
+
30
39
  create:
31
40
  $(SERVICE) create
32
41
 
@@ -9,6 +9,9 @@
9
9
  "gen": "make gen",
10
10
  "lint": "make lint",
11
11
  "test": "make test",
12
+ "test:e2e": "bun run ./scripts/e2e.ts",
13
+ "test:e2e:local": "bun run ./scripts/e2e.ts --local",
14
+ "test:e2e:prod": "bun run ./scripts/e2e.ts --prod",
12
15
  "create": "service create",
13
16
  "deploy": "service deploy",
14
17
  "protect-main": "service protect-main",
@@ -1,4 +1,4 @@
1
- .PHONY: dev migrate migrate-lint gen lint test create deploy protect-main dashboards observability-bootstrap auth destroy
1
+ .PHONY: dev migrate migrate-lint gen lint test test-e2e test-e2e-local test-e2e-prod create deploy protect-main dashboards observability-bootstrap auth destroy
2
2
 
3
3
  SERVICE := service
4
4
  WITH_ENV := set -a; [ ! -f .env.local ] || . ./.env.local; set +a;
@@ -28,6 +28,15 @@ lint:
28
28
  test:
29
29
  bun test ./test
30
30
 
31
+ test-e2e:
32
+ bun run ./scripts/e2e.ts $(ARGS)
33
+
34
+ test-e2e-local:
35
+ bun run ./scripts/e2e.ts --local $(ARGS)
36
+
37
+ test-e2e-prod:
38
+ bun run ./scripts/e2e.ts --prod $(ARGS)
39
+
31
40
  create:
32
41
  $(SERVICE) create
33
42
 
@@ -9,6 +9,9 @@
9
9
  "gen": "make gen",
10
10
  "lint": "make lint",
11
11
  "test": "make test",
12
+ "test:e2e": "bun run ./scripts/e2e.ts",
13
+ "test:e2e:local": "bun run ./scripts/e2e.ts --local",
14
+ "test:e2e:prod": "bun run ./scripts/e2e.ts --prod",
12
15
  "create": "service create",
13
16
  "deploy": "service deploy",
14
17
  "protect-main": "service protect-main",