create-svc 0.1.63 → 0.1.64

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.63",
3
+ "version": "0.1.64",
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",
@@ -79,19 +79,18 @@ export async function prepareGcpProject() {
79
79
 
80
80
  function publishTemporalSecrets() {
81
81
  const temporal = resolveTemporalRuntimeConfig();
82
- const apiKey = process.env.TEMPORAL_API_KEY?.trim();
83
- if (!apiKey || !temporal.apiKeySecretName) {
82
+ if (!temporal.apiKey || !temporal.apiKeySecretName) {
84
83
  return "No Temporal API key configured";
85
84
  }
86
85
 
87
- addSecretVersion(temporal.apiKeySecretName, apiKey);
86
+ addSecretVersion(temporal.apiKeySecretName, temporal.apiKey);
88
87
  ensureSecretAccessor(temporal.apiKeySecretName, `serviceAccount:${config.runtimeServiceAccount}`);
89
88
  return temporal.apiKeySecretName;
90
89
  }
91
90
 
92
91
  function shouldPublishTemporalSecrets() {
93
92
  const temporal = resolveTemporalRuntimeConfig();
94
- return Boolean(process.env.TEMPORAL_API_KEY?.trim() && temporal.apiKeySecretName);
93
+ return Boolean(temporal.enabled && temporal.apiKey && temporal.apiKeySecretName);
95
94
  }
96
95
 
97
96
  if (import.meta.main) {
@@ -3,6 +3,7 @@ import { serviceConfig } from "../runtime";
3
3
  const cloudrun = serviceConfig.cloudrun;
4
4
  const dns = serviceConfig.dns;
5
5
  const neon = serviceConfig.neon;
6
+ const vault = serviceConfig.providers?.vault ?? {};
6
7
 
7
8
  export const config = {
8
9
  serviceName: serviceConfig.service_id,
@@ -39,6 +40,8 @@ export const config = {
39
40
  namespace: serviceConfig.temporal.namespace,
40
41
  taskQueue: serviceConfig.temporal.task_queue,
41
42
  apiKeySecretName: serviceConfig.temporal.api_key_secret_name,
43
+ vaultMount: vault.mount || "secret",
44
+ vaultPath: vault.temporal_path || "prod/providers/temporal",
42
45
  },
43
46
  neon: {
44
47
  projectId: neon.project_id,
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { config } from "./config";
4
4
  import { serviceRoot } from "../runtime";
5
5
  import { localDockerBuildArgs, parseDeployArgs, type DeployArgs } from "./deploy-args";
6
+ import { resolveTemporalRuntimeConfigValues } from "./temporal-config";
6
7
 
7
8
  type CommandOptions = {
8
9
  allowFailure?: boolean;
@@ -551,24 +552,40 @@ export async function renderManifest(image: string, target: DeploymentTarget, pr
551
552
  }
552
553
 
553
554
  export function resolveTemporalRuntimeConfig() {
554
- const enabledOverride = process.env.TEMPORAL_ENABLED?.trim();
555
- const address = process.env.TEMPORAL_ADDRESS?.trim() || config.temporal.address;
556
- const namespace = process.env.TEMPORAL_NAMESPACE?.trim() || config.temporal.namespace;
557
- const taskQueue = process.env.TEMPORAL_TASK_QUEUE?.trim() || config.temporal.taskQueue;
558
- const apiKeySecretName = process.env.TEMPORAL_API_KEY_SECRET?.trim() || (process.env.TEMPORAL_API_KEY?.trim() ? config.temporal.apiKeySecretName : "");
559
- const enabled = enabledOverride
560
- ? ["1", "true", "yes", "on"].includes(enabledOverride.toLowerCase())
561
- : config.temporal.enabled;
555
+ return resolveTemporalRuntimeConfigValues(config.temporal, process.env, readTemporalProviderFields);
556
+ }
562
557
 
558
+ function readTemporalProviderFields(mount: string, path: string) {
563
559
  return {
564
- enabled,
565
- address,
566
- namespace,
567
- taskQueue,
568
- apiKeySecretName,
560
+ address: readVaultField(mount, path, ["TEMPORAL_ADDRESS", "address"]),
561
+ namespace: readVaultField(mount, path, ["TEMPORAL_NAMESPACE", "namespace"]),
562
+ apiKey: readVaultField(mount, path, ["TEMPORAL_API_KEY", "api_key"]),
569
563
  };
570
564
  }
571
565
 
566
+ function readVaultField(mount: string, path: string, fields: string[]) {
567
+ const vault = Bun.which("vault");
568
+ if (!vault || !path) {
569
+ return "";
570
+ }
571
+
572
+ for (const field of fields) {
573
+ const result = Bun.spawnSync([vault, "kv", "get", `-mount=${mount}`, `-field=${field}`, path], {
574
+ cwd: process.cwd(),
575
+ env: process.env,
576
+ stdout: "pipe",
577
+ stderr: "pipe",
578
+ });
579
+ if (result.success && result.stdout) {
580
+ const value = decoder.decode(result.stdout).trim();
581
+ if (value) {
582
+ return value;
583
+ }
584
+ }
585
+ }
586
+ return "";
587
+ }
588
+
572
589
  export async function writeRenderedManifest(image: string, target: DeploymentTarget) {
573
590
  const rendered = await renderManifest(image, target);
574
591
  const path = new URL("../../.cloudrun.rendered.yaml", import.meta.url);
@@ -0,0 +1,66 @@
1
+ import { expect, test } from "bun:test";
2
+ import { resolveTemporalRuntimeConfigValues } from "./temporal-config";
3
+
4
+ const baseConfig = {
5
+ enabled: true,
6
+ address: "localhost:7233",
7
+ namespace: "default",
8
+ taskQueue: "orders",
9
+ apiKeySecretName: "orders-temporal-api-key",
10
+ vaultMount: "secret",
11
+ vaultPath: "prod/providers/temporal",
12
+ };
13
+
14
+ test("resolveTemporalRuntimeConfigValues reads production Temporal config from Vault fields", () => {
15
+ const resolved = resolveTemporalRuntimeConfigValues(baseConfig, {}, () => ({
16
+ address: "temporal.example.tmprl.cloud:7233",
17
+ namespace: "anmho.prod",
18
+ apiKey: "secret-key",
19
+ }));
20
+
21
+ expect(resolved).toEqual({
22
+ enabled: true,
23
+ address: "temporal.example.tmprl.cloud:7233",
24
+ namespace: "anmho.prod",
25
+ taskQueue: "orders",
26
+ apiKeySecretName: "orders-temporal-api-key",
27
+ apiKey: "secret-key",
28
+ });
29
+ });
30
+
31
+ test("resolveTemporalRuntimeConfigValues prefers explicit environment overrides", () => {
32
+ const resolved = resolveTemporalRuntimeConfigValues(
33
+ baseConfig,
34
+ {
35
+ TEMPORAL_ADDRESS: "env.temporal:7233",
36
+ TEMPORAL_NAMESPACE: "env.namespace",
37
+ TEMPORAL_TASK_QUEUE: "env-task-queue",
38
+ TEMPORAL_API_KEY: "env-key",
39
+ TEMPORAL_API_KEY_SECRET: "env-secret-name",
40
+ },
41
+ () => ({
42
+ address: "vault.temporal:7233",
43
+ namespace: "vault.namespace",
44
+ apiKey: "vault-key",
45
+ })
46
+ );
47
+
48
+ expect(resolved.address).toBe("env.temporal:7233");
49
+ expect(resolved.namespace).toBe("env.namespace");
50
+ expect(resolved.taskQueue).toBe("env-task-queue");
51
+ expect(resolved.apiKey).toBe("env-key");
52
+ expect(resolved.apiKeySecretName).toBe("env-secret-name");
53
+ });
54
+
55
+ test("resolveTemporalRuntimeConfigValues fails clearly when enabled Temporal resolves to localhost", () => {
56
+ expect(() => resolveTemporalRuntimeConfigValues(baseConfig, {}, () => ({}))).toThrow(
57
+ "Temporal is enabled for this Cloud Run service, but the resolved Temporal address is local"
58
+ );
59
+ });
60
+
61
+ test("resolveTemporalRuntimeConfigValues allows explicit Temporal disable", () => {
62
+ const resolved = resolveTemporalRuntimeConfigValues(baseConfig, { TEMPORAL_ENABLED: "false" }, () => ({}));
63
+
64
+ expect(resolved.enabled).toBeFalse();
65
+ expect(resolved.apiKeySecretName).toBe("");
66
+ });
@@ -0,0 +1,84 @@
1
+ type TemporalConfigInput = {
2
+ enabled: boolean;
3
+ address: string;
4
+ namespace: string;
5
+ taskQueue: string;
6
+ apiKeySecretName: string;
7
+ vaultMount: string;
8
+ vaultPath: string;
9
+ };
10
+
11
+ type TemporalProviderFields = {
12
+ address?: string;
13
+ namespace?: string;
14
+ apiKey?: string;
15
+ };
16
+
17
+ export type TemporalRuntimeConfig = {
18
+ enabled: boolean;
19
+ address: string;
20
+ namespace: string;
21
+ taskQueue: string;
22
+ apiKeySecretName: string;
23
+ apiKey: string;
24
+ };
25
+
26
+ export function resolveTemporalRuntimeConfigValues(
27
+ config: TemporalConfigInput,
28
+ env: Record<string, string | undefined>,
29
+ readProviderFields: (mount: string, path: string) => TemporalProviderFields
30
+ ): TemporalRuntimeConfig {
31
+ const enabledOverride = env.TEMPORAL_ENABLED?.trim();
32
+ const enabled = enabledOverride ? isTruthy(enabledOverride) : config.enabled;
33
+ const taskQueue = env.TEMPORAL_TASK_QUEUE?.trim() || config.taskQueue;
34
+
35
+ if (!enabled) {
36
+ return {
37
+ enabled: false,
38
+ address: env.TEMPORAL_ADDRESS?.trim() || config.address,
39
+ namespace: env.TEMPORAL_NAMESPACE?.trim() || config.namespace,
40
+ taskQueue,
41
+ apiKeySecretName: "",
42
+ apiKey: "",
43
+ };
44
+ }
45
+
46
+ const provider = readProviderFields(config.vaultMount, config.vaultPath);
47
+ const address = env.TEMPORAL_ADDRESS?.trim() || provider.address || config.address;
48
+ const namespace = env.TEMPORAL_NAMESPACE?.trim() || provider.namespace || config.namespace;
49
+ const apiKey = env.TEMPORAL_API_KEY?.trim() || provider.apiKey || "";
50
+ const apiKeySecretName = env.TEMPORAL_API_KEY_SECRET?.trim() || (apiKey ? config.apiKeySecretName : "");
51
+
52
+ if (isLocalTemporalAddress(address)) {
53
+ throw new Error(
54
+ [
55
+ "Temporal is enabled for this Cloud Run service, but the resolved Temporal address is local.",
56
+ `Set TEMPORAL_ADDRESS, TEMPORAL_NAMESPACE, and TEMPORAL_API_KEY, or populate Vault at ${config.vaultMount}/${config.vaultPath}`,
57
+ "with TEMPORAL_ADDRESS, TEMPORAL_NAMESPACE, and TEMPORAL_API_KEY before running service create or service deploy.",
58
+ "Set TEMPORAL_ENABLED=false only for services that should deploy without Temporal.",
59
+ ].join(" ")
60
+ );
61
+ }
62
+
63
+ if (!namespace) {
64
+ throw new Error(`Temporal is enabled but TEMPORAL_NAMESPACE is missing; set it in env or Vault at ${config.vaultMount}/${config.vaultPath}`);
65
+ }
66
+
67
+ return {
68
+ enabled,
69
+ address,
70
+ namespace,
71
+ taskQueue,
72
+ apiKeySecretName,
73
+ apiKey,
74
+ };
75
+ }
76
+
77
+ function isTruthy(value: string) {
78
+ return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
79
+ }
80
+
81
+ function isLocalTemporalAddress(address: string) {
82
+ const value = address.trim().toLowerCase();
83
+ return value === "" || value.startsWith("localhost:") || value.startsWith("127.0.0.1:") || value.startsWith("[::1]:");
84
+ }