create-svc 0.1.8 → 0.1.9
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 +22 -12
- package/package.json +1 -1
- package/src/cli.test.ts +1 -1
- package/src/cli.ts +1 -1
- package/src/vault.test.ts +33 -0
- package/src/vault.ts +21 -2
- package/templates/shared/.env.example +2 -2
- package/templates/shared/README.md +20 -14
- package/templates/shared/scripts/cloudrun/neon.ts +19 -2
package/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# create-svc
|
|
2
2
|
|
|
3
|
-
`create-svc` is a Bun-authored scaffold CLI for generating
|
|
3
|
+
`create-svc` is a Bun-authored scaffold CLI for generating Cloud Run services with:
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
5
|
+
- `go + chi`
|
|
6
|
+
- `go + connectrpc`
|
|
7
|
+
- `bun + hono`
|
|
8
|
+
- `bun + connectrpc`
|
|
9
|
+
- a real `service.yaml` manifest
|
|
10
|
+
- shared Cloud Run bootstrap, deploy, and cleanup automation
|
|
11
|
+
- Neon-backed main, preview, and personal environments
|
|
10
12
|
|
|
11
13
|
## Usage
|
|
12
14
|
|
|
@@ -15,14 +17,22 @@ bun install
|
|
|
15
17
|
bun run index.ts my-service
|
|
16
18
|
```
|
|
17
19
|
|
|
18
|
-
The
|
|
20
|
+
The generator discovers:
|
|
21
|
+
|
|
22
|
+
- accessible GCP projects
|
|
23
|
+
- open billing accounts
|
|
24
|
+
- Neon defaults from `NEON_API_KEY`, or Vault via `VAULT_ADDR` plus `VAULT_TOKEN`, `VAULT_TOKEN_FILE`, or `~/.vault-token`
|
|
25
|
+
|
|
26
|
+
Generated repos are `Makefile`-first. The shared Cloud Run control plane is exposed as a local CLI bin and invoked by `make`.
|
|
19
27
|
|
|
20
28
|
```bash
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
make dev
|
|
30
|
+
make gen
|
|
31
|
+
make lint
|
|
32
|
+
make test
|
|
33
|
+
make bootstrap
|
|
34
|
+
make deploy
|
|
35
|
+
make cleanup
|
|
26
36
|
```
|
|
27
37
|
|
|
28
38
|
## Development
|
package/package.json
CHANGED
package/src/cli.test.ts
CHANGED
|
@@ -19,7 +19,7 @@ test("assertDiscoveryReady requires Neon discovery to succeed", () => {
|
|
|
19
19
|
neonError: "Vault secret resolution requires VAULT_ADDR, VAULT_TOKEN, and a secret path",
|
|
20
20
|
})
|
|
21
21
|
).toThrow(
|
|
22
|
-
"Neon discovery is required before scaffolding. Set NEON_API_KEY, or use Vault by providing VAULT_ADDR and VAULT_TOKEN. Optional overrides: VAULT_SECRET_MOUNT, VAULT_NEON_API_KEY_PATH, VAULT_NEON_API_KEY_FIELD."
|
|
22
|
+
"Neon discovery is required before scaffolding. Set NEON_API_KEY, or use Vault by providing VAULT_ADDR and either VAULT_TOKEN, VAULT_TOKEN_FILE, or ~/.vault-token. Optional overrides: VAULT_SECRET_MOUNT, VAULT_NEON_API_KEY_PATH, VAULT_NEON_API_KEY_FIELD."
|
|
23
23
|
);
|
|
24
24
|
});
|
|
25
25
|
|
package/src/cli.ts
CHANGED
|
@@ -550,7 +550,7 @@ function formatNeonDiscoveryRequirement(reason: string) {
|
|
|
550
550
|
if (reason.includes("Vault secret resolution requires")) {
|
|
551
551
|
return [
|
|
552
552
|
"Neon discovery is required before scaffolding.",
|
|
553
|
-
"Set NEON_API_KEY, or use Vault by providing VAULT_ADDR and VAULT_TOKEN.",
|
|
553
|
+
"Set NEON_API_KEY, or use Vault by providing VAULT_ADDR and either VAULT_TOKEN, VAULT_TOKEN_FILE, or ~/.vault-token.",
|
|
554
554
|
"Optional overrides: VAULT_SECRET_MOUNT, VAULT_NEON_API_KEY_PATH, VAULT_NEON_API_KEY_FIELD.",
|
|
555
555
|
].join(" ");
|
|
556
556
|
}
|
package/src/vault.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterEach, expect, mock, test } from "bun:test";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
2
3
|
import { readVaultSecret, resolveNeonApiKey } from "./vault";
|
|
3
4
|
|
|
4
5
|
const originalEnv = { ...process.env };
|
|
@@ -40,3 +41,35 @@ test("readVaultSecret reads KV v2 secret data using existing vault login env", a
|
|
|
40
41
|
})
|
|
41
42
|
).resolves.toBe("vault-token");
|
|
42
43
|
});
|
|
44
|
+
|
|
45
|
+
test("readVaultSecret falls back to ~/.vault-token", async () => {
|
|
46
|
+
const home = "/tmp/create-svc-vault-home";
|
|
47
|
+
process.env.HOME = home;
|
|
48
|
+
process.env.VAULT_ADDR = "https://vault.example.com";
|
|
49
|
+
delete process.env.VAULT_TOKEN;
|
|
50
|
+
|
|
51
|
+
await mkdir(home, { recursive: true });
|
|
52
|
+
await Bun.write(`${home}/.vault-token`, "token-from-file\n");
|
|
53
|
+
|
|
54
|
+
const fetchMock = mock(async () => {
|
|
55
|
+
return new Response(
|
|
56
|
+
JSON.stringify({
|
|
57
|
+
data: {
|
|
58
|
+
data: {
|
|
59
|
+
value: "vault-token",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
{ status: 200 }
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
globalThis.fetch = fetchMock as typeof fetch;
|
|
68
|
+
|
|
69
|
+
await expect(
|
|
70
|
+
readVaultSecret({
|
|
71
|
+
path: "provider/neon-api-key",
|
|
72
|
+
field: "value",
|
|
73
|
+
})
|
|
74
|
+
).resolves.toBe("vault-token");
|
|
75
|
+
});
|
package/src/vault.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
1
4
|
const DEFAULT_VAULT_SECRET_MOUNT = "secret";
|
|
2
5
|
const DEFAULT_NEON_API_KEY_PATH = "provider/neon-api-key";
|
|
3
6
|
const DEFAULT_NEON_API_KEY_FIELD = "value";
|
|
@@ -24,13 +27,13 @@ export async function resolveNeonApiKey() {
|
|
|
24
27
|
|
|
25
28
|
export async function readVaultSecret(options: VaultSecretOptions = {}) {
|
|
26
29
|
const addr = options.addr ?? process.env.VAULT_ADDR?.trim() ?? "";
|
|
27
|
-
const token = options.token ??
|
|
30
|
+
const token = options.token ?? (await resolveVaultToken());
|
|
28
31
|
const mount = options.mount ?? process.env.VAULT_SECRET_MOUNT?.trim() ?? DEFAULT_VAULT_SECRET_MOUNT;
|
|
29
32
|
const path = options.path?.trim() ?? "";
|
|
30
33
|
const field = options.field?.trim() ?? "value";
|
|
31
34
|
|
|
32
35
|
if (!addr || !token || !path) {
|
|
33
|
-
throw new Error("Vault secret resolution requires VAULT_ADDR,
|
|
36
|
+
throw new Error("Vault secret resolution requires VAULT_ADDR, a Vault token, and a secret path");
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
const normalizedAddr = addr.replace(/\/+$/g, "");
|
|
@@ -61,3 +64,19 @@ export async function readVaultSecret(options: VaultSecretOptions = {}) {
|
|
|
61
64
|
|
|
62
65
|
return value;
|
|
63
66
|
}
|
|
67
|
+
|
|
68
|
+
async function resolveVaultToken() {
|
|
69
|
+
const direct = process.env.VAULT_TOKEN?.trim();
|
|
70
|
+
if (direct) {
|
|
71
|
+
return direct;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const tokenFile = process.env.VAULT_TOKEN_FILE?.trim() || join(homedir(), ".vault-token");
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const value = (await Bun.file(tokenFile).text()).trim();
|
|
78
|
+
return value;
|
|
79
|
+
} catch {
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -6,5 +6,5 @@ VAULT_SECRET_MOUNT=secret
|
|
|
6
6
|
VAULT_NEON_API_KEY_PATH=provider/neon-api-key
|
|
7
7
|
VAULT_NEON_API_KEY_FIELD=value
|
|
8
8
|
|
|
9
|
-
# Do not commit VAULT_TOKEN. Prefer `vault login
|
|
10
|
-
|
|
9
|
+
# Do not commit VAULT_TOKEN. Prefer `vault login`; the CLI will also use
|
|
10
|
+
# VAULT_TOKEN_FILE or ~/.vault-token when available.
|
|
@@ -5,7 +5,7 @@ Generated by `create-svc`.
|
|
|
5
5
|
This scaffold targets `{{RUNTIME}} + {{FRAMEWORK}}` on Cloud Run with:
|
|
6
6
|
|
|
7
7
|
- one generated `service.yaml` manifest
|
|
8
|
-
-
|
|
8
|
+
- a local `svc-cloudrun` CLI for bootstrap, deploy, and cleanup
|
|
9
9
|
- GitHub Actions for CI, `main` deploys, PR previews, and personal environments
|
|
10
10
|
- GCP project bootstrap with billing and quota-project-aware `gcloud` calls
|
|
11
11
|
- Neon main, preview, and personal branch provisioning
|
|
@@ -13,25 +13,28 @@ This scaffold targets `{{RUNTIME}} + {{FRAMEWORK}}` on Cloud Run with:
|
|
|
13
13
|
## Commands
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
make dev
|
|
17
|
+
make gen
|
|
18
|
+
make lint
|
|
19
|
+
make test
|
|
20
|
+
make bootstrap
|
|
21
|
+
make deploy
|
|
22
|
+
make deploy ARGS="--environment personal --name <slug>"
|
|
23
|
+
make deploy ARGS="--destroy --environment personal --name <slug>"
|
|
24
|
+
make cleanup
|
|
25
|
+
make cleanup ARGS="--repo --project"
|
|
24
26
|
```
|
|
25
27
|
|
|
26
28
|
## Configuration
|
|
27
29
|
|
|
28
30
|
The generated Cloud Run config lives in [scripts/cloudrun/config.ts](scripts/cloudrun/config.ts).
|
|
29
31
|
|
|
30
|
-
Bootstrap and
|
|
32
|
+
Bootstrap, deploy, and cleanup use:
|
|
31
33
|
|
|
32
34
|
- `gcloud`
|
|
33
35
|
- `gh`
|
|
34
|
-
- `NEON_API_KEY`, or a working Vault login via `VAULT_ADDR`
|
|
36
|
+
- `NEON_API_KEY`, or a working Vault login via `VAULT_ADDR` plus `VAULT_TOKEN`, `VAULT_TOKEN_FILE`, or `~/.vault-token`
|
|
37
|
+
- the local repo CLI via `npx --no-install svc-cloudrun ...`
|
|
35
38
|
|
|
36
39
|
## Environment setup
|
|
37
40
|
|
|
@@ -43,14 +46,17 @@ cp .env.example .env.local
|
|
|
43
46
|
|
|
44
47
|
Then edit `.env.local` with your Vault address and secret path overrides.
|
|
45
48
|
|
|
46
|
-
For the token itself, prefer a
|
|
49
|
+
For the token itself, prefer a normal Vault login flow:
|
|
47
50
|
|
|
48
51
|
```bash
|
|
49
52
|
vault login
|
|
50
|
-
export VAULT_TOKEN="$(vault print token)"
|
|
51
53
|
```
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
The scaffold will use, in order:
|
|
56
|
+
|
|
57
|
+
1. `VAULT_TOKEN`
|
|
58
|
+
2. `VAULT_TOKEN_FILE`
|
|
59
|
+
3. `~/.vault-token`
|
|
54
60
|
|
|
55
61
|
That keeps stable settings in the repo and keeps the token out of `~/.zshrc`.
|
|
56
62
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createApiClient } from "@neondatabase/api-client";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
2
4
|
import { config } from "./config";
|
|
3
5
|
|
|
4
6
|
type NeonBranch = {
|
|
@@ -13,13 +15,13 @@ async function resolveNeonApiKey() {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
const addr = process.env.VAULT_ADDR?.trim() ?? "";
|
|
16
|
-
const token =
|
|
18
|
+
const token = await resolveVaultToken();
|
|
17
19
|
const mount = process.env.VAULT_SECRET_MOUNT?.trim() ?? "secret";
|
|
18
20
|
const path = process.env.VAULT_NEON_API_KEY_PATH?.trim() ?? "provider/neon-api-key";
|
|
19
21
|
const field = process.env.VAULT_NEON_API_KEY_FIELD?.trim() ?? "value";
|
|
20
22
|
|
|
21
23
|
if (!addr || !token) {
|
|
22
|
-
throw new Error("NEON_API_KEY is required for Neon provisioning, or set VAULT_ADDR
|
|
24
|
+
throw new Error("NEON_API_KEY is required for Neon provisioning, or set VAULT_ADDR with VAULT_TOKEN, VAULT_TOKEN_FILE, or ~/.vault-token");
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const normalizedAddr = addr.replace(/\/+$/g, "");
|
|
@@ -49,6 +51,21 @@ async function resolveNeonApiKey() {
|
|
|
49
51
|
return apiKey;
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
async function resolveVaultToken() {
|
|
55
|
+
const direct = process.env.VAULT_TOKEN?.trim();
|
|
56
|
+
if (direct) {
|
|
57
|
+
return direct;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const tokenFile = process.env.VAULT_TOKEN_FILE?.trim() || join(homedir(), ".vault-token");
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
return (await Bun.file(tokenFile).text()).trim();
|
|
64
|
+
} catch {
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
async function neonClient() {
|
|
53
70
|
const apiKey = await resolveNeonApiKey();
|
|
54
71
|
return createApiClient({ apiKey });
|