create-svc 0.1.13 → 0.1.15
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 +19 -6
- package/package.json +1 -3
- package/src/cli.test.ts +31 -2
- package/src/cli.ts +498 -159
- package/src/gcp.ts +35 -44
- package/src/git-bootstrap.ts +14 -11
- package/src/naming.test.ts +1 -1
- package/src/naming.ts +1 -1
- package/src/post-scaffold.test.ts +17 -1
- package/src/post-scaffold.ts +24 -3
- package/src/scaffold.test.ts +12 -5
- package/src/service.test.ts +12 -1
- package/src/service.ts +26 -0
- package/templates/shared/scripts/authctl.ts +17 -1
- package/templates/shared/scripts/cloudrun/cli.ts +21 -2
- package/templates/targets/workers/scripts/workers/cli.ts +20 -2
- package/templates/variants/go-chi/package.json +9 -1
- package/templates/variants/go-connectrpc/package.json +9 -1
package/src/gcp.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { CloudBillingClient } from "@google-cloud/billing";
|
|
2
|
-
import { ProjectsClient } from "@google-cloud/resource-manager";
|
|
3
|
-
|
|
4
1
|
export type GcpProject = {
|
|
5
2
|
projectId: string;
|
|
6
3
|
name: string;
|
|
@@ -20,58 +17,29 @@ export type GcpApi = {
|
|
|
20
17
|
attachBillingAccount(projectId: string, billingAccountName: string): Promise<void>;
|
|
21
18
|
};
|
|
22
19
|
|
|
23
|
-
export function createGcpApi(
|
|
24
|
-
projectsClient = new ProjectsClient(),
|
|
25
|
-
billingClient = new CloudBillingClient()
|
|
26
|
-
): GcpApi {
|
|
20
|
+
export function createGcpApi(): GcpApi {
|
|
27
21
|
return {
|
|
28
22
|
async listProjects() {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
name: project.displayName ?? project.projectId ?? "",
|
|
34
|
-
lifecycleState: `${project.state ?? ""}`,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return projects
|
|
39
|
-
.filter((project) => project.projectId && project.lifecycleState !== "DELETE_REQUESTED")
|
|
40
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
23
|
+
return parseJson<GcpProject[]>(
|
|
24
|
+
runGcloud(["projects", "list", "--format=json(projectId,name,lifecycleState)"]).stdout,
|
|
25
|
+
"GCP project discovery"
|
|
26
|
+
);
|
|
41
27
|
},
|
|
42
28
|
|
|
43
29
|
async listBillingAccounts() {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
displayName: account.displayName ?? account.name ?? "",
|
|
49
|
-
open: Boolean(account.open),
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return accounts
|
|
54
|
-
.filter((account) => account.name && account.open)
|
|
55
|
-
.sort((left, right) => left.displayName.localeCompare(right.displayName));
|
|
30
|
+
return parseJson<BillingAccount[]>(
|
|
31
|
+
runGcloud(["billing", "accounts", "list", "--format=json(name,displayName,open)"]).stdout,
|
|
32
|
+
"billing account discovery"
|
|
33
|
+
);
|
|
56
34
|
},
|
|
57
35
|
|
|
58
36
|
async createProject(projectId: string, name: string) {
|
|
59
|
-
|
|
60
|
-
project: {
|
|
61
|
-
projectId,
|
|
62
|
-
displayName: name,
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
await operation.promise();
|
|
37
|
+
runGcloud(["projects", "create", projectId, "--name", name]);
|
|
66
38
|
},
|
|
67
39
|
|
|
68
40
|
async attachBillingAccount(projectId: string, billingAccountName: string) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
projectBillingInfo: {
|
|
72
|
-
billingAccountName,
|
|
73
|
-
},
|
|
74
|
-
});
|
|
41
|
+
const account = billingAccountName.replace(/^billingAccounts\//, "");
|
|
42
|
+
runGcloud(["billing", "projects", "link", projectId, "--billing-account", account]);
|
|
75
43
|
},
|
|
76
44
|
};
|
|
77
45
|
}
|
|
@@ -95,3 +63,26 @@ export async function createProject(projectId: string, name: string, api = creat
|
|
|
95
63
|
export async function attachBillingAccount(projectId: string, billingAccountName: string, api = createGcpApi()) {
|
|
96
64
|
await api.attachBillingAccount(projectId, billingAccountName);
|
|
97
65
|
}
|
|
66
|
+
|
|
67
|
+
function runGcloud(args: string[]) {
|
|
68
|
+
const result = Bun.spawnSync(["gcloud", ...args], {
|
|
69
|
+
stdout: "pipe",
|
|
70
|
+
stderr: "pipe",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (result.exitCode !== 0) {
|
|
74
|
+
throw new Error(result.stderr.toString().trim() || `gcloud ${args.join(" ")} failed`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
stdout: result.stdout.toString(),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseJson<T>(value: string, label: string): T {
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(value) as T;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new Error(`Unable to parse ${label} output: ${(error as Error).message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/git-bootstrap.ts
CHANGED
|
@@ -30,19 +30,21 @@ export async function bootstrapGitHubRepository(targetDir: string, config: GitBo
|
|
|
30
30
|
return { status: "skipped-existing-worktree", root: existingRoot };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
run(["git", "--version"], targetDir, "git is required to initialize the generated repository");
|
|
34
|
-
run(["gh", "--version"], targetDir, "GitHub CLI `gh` is required to create the generated repository");
|
|
35
|
-
run(["gh", "auth", "status"], targetDir, "Authenticate GitHub CLI with `gh auth login` before creating the repository");
|
|
33
|
+
run(["git", "--version"], targetDir, "git is required to initialize the generated repository", { quiet: true });
|
|
34
|
+
run(["gh", "--version"], targetDir, "GitHub CLI `gh` is required to create the generated repository", { quiet: true });
|
|
35
|
+
run(["gh", "auth", "status"], targetDir, "Authenticate GitHub CLI with `gh auth login` before creating the repository", { quiet: true });
|
|
36
36
|
|
|
37
|
-
run(["git", "init", "-b", "main"], targetDir);
|
|
37
|
+
run(["git", "init", "-b", "main", "--quiet"], targetDir);
|
|
38
38
|
run(["git", "add", "."], targetDir);
|
|
39
39
|
|
|
40
40
|
if (hasStagedChanges(targetDir)) {
|
|
41
|
-
run(["git", "commit", "-m", "Initial commit"], targetDir);
|
|
41
|
+
run(["git", "commit", "--quiet", "-m", "Initial commit"], targetDir);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
const repository = `${config.owner}/${config.repository}`;
|
|
45
|
-
run(["gh", "repo", "create", repository, "--private", "--source", ".", "--remote", "origin", "--push"], targetDir
|
|
45
|
+
run(["gh", "repo", "create", repository, "--private", "--source", ".", "--remote", "origin", "--push"], targetDir, undefined, {
|
|
46
|
+
quiet: true,
|
|
47
|
+
});
|
|
46
48
|
|
|
47
49
|
return {
|
|
48
50
|
status: "created",
|
|
@@ -55,8 +57,8 @@ export function commitAndPushGeneratedArtifacts(targetDir: string, message: stri
|
|
|
55
57
|
if (!hasStagedChanges(targetDir)) {
|
|
56
58
|
return { committed: false };
|
|
57
59
|
}
|
|
58
|
-
run(["git", "commit", "-m", message], targetDir);
|
|
59
|
-
run(["git", "push"], targetDir);
|
|
60
|
+
run(["git", "commit", "--quiet", "-m", message], targetDir);
|
|
61
|
+
run(["git", "push", "--quiet"], targetDir, undefined, { quiet: true });
|
|
60
62
|
return { committed: true };
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -94,17 +96,18 @@ function hasStagedChanges(cwd: string) {
|
|
|
94
96
|
return result.exitCode === 1;
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
function run(command: string[], cwd: string, message?: string) {
|
|
99
|
+
function run(command: string[], cwd: string, message?: string, options: { quiet?: boolean } = {}) {
|
|
98
100
|
const result = Bun.spawnSync(command, {
|
|
99
101
|
cwd,
|
|
100
102
|
stdin: "inherit",
|
|
101
|
-
stdout: "inherit",
|
|
103
|
+
stdout: options.quiet ? "pipe" : "inherit",
|
|
102
104
|
stderr: "pipe",
|
|
103
105
|
});
|
|
104
106
|
if (result.exitCode === 0) {
|
|
105
107
|
return;
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
const output = result.stdout?.toString().trim() ?? "";
|
|
108
111
|
const detail = result.stderr.toString().trim();
|
|
109
|
-
throw new Error([message, `Command failed: ${command.join(" ")}`, detail].filter(Boolean).join("\n"));
|
|
112
|
+
throw new Error([message, `Command failed: ${command.join(" ")}`, output, detail].filter(Boolean).join("\n"));
|
|
110
113
|
}
|
package/src/naming.test.ts
CHANGED
|
@@ -11,7 +11,7 @@ test("deriveDefaults uses the service name for project, repo, and database namin
|
|
|
11
11
|
neonDatabaseName: "edge_api",
|
|
12
12
|
localDatabasePort: deriveLocalPostgresPort("edge-api"),
|
|
13
13
|
apiHostname: "api.edge-api.anmho.com",
|
|
14
|
-
modulePath: "
|
|
14
|
+
modulePath: "github.com/anmho/edge-api",
|
|
15
15
|
});
|
|
16
16
|
});
|
|
17
17
|
|
package/src/naming.ts
CHANGED
|
@@ -94,7 +94,7 @@ export function deriveDefaults(serviceName: string) {
|
|
|
94
94
|
neonDatabaseName: compactDatabaseName(normalizedServiceName),
|
|
95
95
|
localDatabasePort: deriveLocalPostgresPort(normalizedServiceName),
|
|
96
96
|
apiHostname: `api.${normalizedServiceName}.anmho.com`,
|
|
97
|
-
modulePath: `
|
|
97
|
+
modulePath: `github.com/anmho/${normalizedServiceName}`,
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { buildPostScaffoldCommands } from "./post-scaffold";
|
|
2
|
+
import { buildDeploymentVerificationCommands, buildPostScaffoldCommands } from "./post-scaffold";
|
|
3
3
|
|
|
4
4
|
describe("buildPostScaffoldCommands", () => {
|
|
5
5
|
test("runs create and deploy for HTTP services", () => {
|
|
@@ -17,3 +17,19 @@ describe("buildPostScaffoldCommands", () => {
|
|
|
17
17
|
]);
|
|
18
18
|
});
|
|
19
19
|
});
|
|
20
|
+
|
|
21
|
+
describe("buildDeploymentVerificationCommands", () => {
|
|
22
|
+
test("uses curl health checks for HTTP services", () => {
|
|
23
|
+
expect(buildDeploymentVerificationCommands({ apiHostname: "api.launch.anmho.com", framework: "hono", runtime: "bun" })).toEqual([
|
|
24
|
+
{ command: "curl", args: ["--fail", "--show-error", "--silent", "https://api.launch.anmho.com/healthz"] },
|
|
25
|
+
{ command: "curl", args: ["--fail", "--show-error", "--silent", "https://api.launch.anmho.com/readyz"] },
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("uses grpcurl for Go ConnectRPC services", () => {
|
|
30
|
+
expect(buildDeploymentVerificationCommands({ apiHostname: "api.launch.anmho.com", framework: "connectrpc", runtime: "go" })).toContainEqual({
|
|
31
|
+
command: "grpcurl",
|
|
32
|
+
args: ["api.launch.anmho.com:443", "list"],
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
package/src/post-scaffold.ts
CHANGED
|
@@ -4,6 +4,7 @@ type CommandOptions = {
|
|
|
4
4
|
cwd: string;
|
|
5
5
|
allowFailure?: boolean;
|
|
6
6
|
input?: string;
|
|
7
|
+
quiet?: boolean;
|
|
7
8
|
};
|
|
8
9
|
|
|
9
10
|
type CommandResult = {
|
|
@@ -26,12 +27,32 @@ export async function runPostScaffoldFlow(config: ScaffoldConfig, cwd: string) {
|
|
|
26
27
|
for (const command of buildPostScaffoldCommands(config)) {
|
|
27
28
|
run(command.command, command.args, { cwd });
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
+
for (const command of buildDeploymentVerificationCommands(config)) {
|
|
31
|
+
run(command.command, command.args, { cwd, quiet: true });
|
|
32
|
+
}
|
|
33
|
+
return { message: "Dependencies installed, service created, service deployed, and production health verified" };
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
return { message: "Backend package generated" };
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
export function buildDeploymentVerificationCommands(
|
|
40
|
+
config: Pick<ScaffoldConfig, "apiHostname" | "framework" | "runtime">
|
|
41
|
+
): PostScaffoldCommand[] {
|
|
42
|
+
const origin = `https://${config.apiHostname}`;
|
|
43
|
+
return [
|
|
44
|
+
{ command: "curl", args: ["--fail", "--show-error", "--silent", `${origin}/healthz`] },
|
|
45
|
+
{ command: "curl", args: ["--fail", "--show-error", "--silent", `${origin}/readyz`] },
|
|
46
|
+
...(config.framework === "connectrpc"
|
|
47
|
+
? [
|
|
48
|
+
config.runtime === "go"
|
|
49
|
+
? { command: "grpcurl", args: [`${config.apiHostname}:443`, "list"] }
|
|
50
|
+
: { command: "curl", args: ["--fail", "--show-error", "--silent", `${origin}/debug/connectrpc`] },
|
|
51
|
+
]
|
|
52
|
+
: []),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
35
56
|
export function buildPostScaffoldCommands(config: Pick<ScaffoldConfig, "framework">): PostScaffoldCommand[] {
|
|
36
57
|
return [
|
|
37
58
|
...(config.framework === "connectrpc" ? [{ command: "bun", args: ["run", "service", "--", "sdk", "build"] }] : []),
|
|
@@ -56,8 +77,8 @@ function run(command: string, args: string[], options: CommandOptions): CommandR
|
|
|
56
77
|
cwd: options.cwd,
|
|
57
78
|
env: process.env,
|
|
58
79
|
stdin: options.input === undefined ? undefined : encoder.encode(options.input),
|
|
59
|
-
stdout: options.allowFailure ? "pipe" : "inherit",
|
|
60
|
-
stderr: options.allowFailure ? "pipe" : "inherit",
|
|
80
|
+
stdout: options.allowFailure || options.quiet ? "pipe" : "inherit",
|
|
81
|
+
stderr: options.allowFailure || options.quiet ? "pipe" : "inherit",
|
|
61
82
|
});
|
|
62
83
|
|
|
63
84
|
const stdout = result.stdout ? decoder.decode(result.stdout).trim() : "";
|
package/src/scaffold.test.ts
CHANGED
|
@@ -9,7 +9,7 @@ function baseConfig(overrides: Partial<ScaffoldConfig> = {}): ScaffoldConfig {
|
|
|
9
9
|
return {
|
|
10
10
|
directory: "svc",
|
|
11
11
|
serviceName: "dns-api",
|
|
12
|
-
modulePath: "
|
|
12
|
+
modulePath: "github.com/anmho/dns-api",
|
|
13
13
|
target: "cloudrun",
|
|
14
14
|
runtime: "bun",
|
|
15
15
|
framework: "hono",
|
|
@@ -156,11 +156,17 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
|
|
|
156
156
|
|
|
157
157
|
if (variant.runtime === "go") {
|
|
158
158
|
const goMod = await Bun.file(join(generatedRoot, "go.mod")).text();
|
|
159
|
-
|
|
160
|
-
expect(goMod).
|
|
159
|
+
const packageJson = await Bun.file(join(generatedRoot, "package.json")).text();
|
|
160
|
+
expect(goMod).toContain("module github.com/anmho/dns-api");
|
|
161
|
+
expect(goMod).not.toContain("module example.com/dns-api");
|
|
162
|
+
expect(packageJson).toContain('"dev": "make dev"');
|
|
163
|
+
expect(packageJson).toContain('"migrate": "make migrate"');
|
|
164
|
+
expect(packageJson).toContain('"create": "bun run ./scripts/cloudrun/cli.ts create"');
|
|
165
|
+
expect(packageJson).toContain('"deploy": "bun run ./scripts/cloudrun/cli.ts deploy"');
|
|
166
|
+
expect(packageJson).toContain('"destroy": "bun run ./scripts/cloudrun/cli.ts destroy"');
|
|
161
167
|
|
|
162
168
|
const mainGo = await Bun.file(join(generatedRoot, "cmd", "server", "main.go")).text();
|
|
163
|
-
expect(mainGo).toContain("
|
|
169
|
+
expect(mainGo).toContain("github.com/anmho/dns-api");
|
|
164
170
|
if (variant.framework === "connectrpc") {
|
|
165
171
|
expect(goMod).toContain("connectrpc.com/connect");
|
|
166
172
|
expect(mainGo).toContain("NewWaitlistService");
|
|
@@ -202,7 +208,8 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
|
|
|
202
208
|
expect(packageJson).toContain('"auth": "bun run ./scripts/cloudrun/cli.ts auth"');
|
|
203
209
|
expect(packageJson).toContain('"destroy": "bun run ./scripts/cloudrun/cli.ts destroy"');
|
|
204
210
|
const serviceCli = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "cli.ts")).text();
|
|
205
|
-
expect(serviceCli).toContain("service <
|
|
211
|
+
expect(serviceCli).toContain("service <command> [args]");
|
|
212
|
+
expect(serviceCli).toContain("Provision auth, database, migrations, and first deploy");
|
|
206
213
|
expect(serviceCli).toContain("assertServiceNameAvailable(config.serviceName)");
|
|
207
214
|
expect(serviceCli).toContain("ensureAuthResourceServer");
|
|
208
215
|
expect(serviceCli).toContain('["resources", "push", "--path", "./grafana"]');
|
package/src/service.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
-
import { findGeneratedServiceRoot, normalizeScaffoldArgs } from "./service";
|
|
5
|
+
import { findGeneratedServiceRoot, generatedDependenciesInstalled, normalizeScaffoldArgs } from "./service";
|
|
6
6
|
|
|
7
7
|
test("normalizeScaffoldArgs treats service create as the scaffold command outside a service repo", () => {
|
|
8
8
|
expect(normalizeScaffoldArgs(["create", "launch-api", "--yes"])).toEqual(["launch-api", "--yes"]);
|
|
@@ -28,3 +28,14 @@ test("findGeneratedServiceRoot detects generated service context from nested dir
|
|
|
28
28
|
expect(findGeneratedServiceRoot(nested)).toBe(serviceRoot);
|
|
29
29
|
expect(findGeneratedServiceRoot(root)).toBeUndefined();
|
|
30
30
|
});
|
|
31
|
+
|
|
32
|
+
test("generatedDependenciesInstalled requires node_modules when package.json exists", async () => {
|
|
33
|
+
const root = await mkdtemp(join(tmpdir(), "create-svc-generated-deps-"));
|
|
34
|
+
expect(generatedDependenciesInstalled(root)).toBeTrue();
|
|
35
|
+
|
|
36
|
+
await writeFile(join(root, "package.json"), "{}");
|
|
37
|
+
expect(generatedDependenciesInstalled(root)).toBeFalse();
|
|
38
|
+
|
|
39
|
+
await mkdir(join(root, "node_modules"));
|
|
40
|
+
expect(generatedDependenciesInstalled(root)).toBeTrue();
|
|
41
|
+
});
|
package/src/service.ts
CHANGED
|
@@ -48,6 +48,8 @@ function isGeneratedServiceRoot(path: string) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function delegateToGeneratedService(serviceRoot: string, argv: string[]) {
|
|
51
|
+
ensureGeneratedDependencies(serviceRoot);
|
|
52
|
+
|
|
51
53
|
const cliPath = existsSync(join(serviceRoot, "scripts", "cloudrun", "cli.ts"))
|
|
52
54
|
? "./scripts/cloudrun/cli.ts"
|
|
53
55
|
: "./scripts/workers/cli.ts";
|
|
@@ -63,3 +65,27 @@ function delegateToGeneratedService(serviceRoot: string, argv: string[]) {
|
|
|
63
65
|
process.exit(result.exitCode || 1);
|
|
64
66
|
}
|
|
65
67
|
}
|
|
68
|
+
|
|
69
|
+
export function generatedDependenciesInstalled(serviceRoot: string) {
|
|
70
|
+
return !existsSync(join(serviceRoot, "package.json")) || existsSync(join(serviceRoot, "node_modules"));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ensureGeneratedDependencies(serviceRoot: string) {
|
|
74
|
+
if (generatedDependenciesInstalled(serviceRoot)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const result = Bun.spawnSync(["bun", "install", "--silent"], {
|
|
79
|
+
cwd: serviceRoot,
|
|
80
|
+
env: process.env,
|
|
81
|
+
stdin: "inherit",
|
|
82
|
+
stdout: "pipe",
|
|
83
|
+
stderr: "pipe",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!result.success) {
|
|
87
|
+
const output = [result.stdout.toString().trim(), result.stderr.toString().trim()].filter(Boolean).join("\n");
|
|
88
|
+
console.error(["Failed to install generated service dependencies with bun install --silent", output].filter(Boolean).join("\n"));
|
|
89
|
+
process.exit(result.exitCode || 1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -216,7 +216,7 @@ function authctl(args: string[], options: { allowFailure?: boolean; quiet?: bool
|
|
|
216
216
|
};
|
|
217
217
|
|
|
218
218
|
if (!output.success && !options.allowFailure) {
|
|
219
|
-
throw new Error(
|
|
219
|
+
throw new Error(formatAuthctlFailure(args, output));
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
if (output.stdout && !options.quiet) {
|
|
@@ -226,6 +226,22 @@ function authctl(args: string[], options: { allowFailure?: boolean; quiet?: bool
|
|
|
226
226
|
return output;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
function formatAuthctlFailure(args: string[], output: CommandResult) {
|
|
230
|
+
const detail = output.stderr || output.stdout;
|
|
231
|
+
if (detail.includes("status_code\":401") || detail.includes("Forbidden. You don't have permission")) {
|
|
232
|
+
return [
|
|
233
|
+
`authctl ${args.join(" ")} failed with exit code ${output.exitCode}`,
|
|
234
|
+
"authctl reached the auth internal API, but Cloudflare Access rejected the request.",
|
|
235
|
+
"Export the authctl Cloudflare Access service token before running service create:",
|
|
236
|
+
' export AUTH_INTERNAL_BASE_URL="$(vault kv get -mount=secret -field=AUTH_INTERNAL_BASE_URL prod/apps/auth/authctl/cloudflare-access)"',
|
|
237
|
+
' export CLOUDFLARE_ACCESS_SERVICE_TOKEN_CLIENT_ID="$(vault kv get -mount=secret -field=CLOUDFLARE_ACCESS_SERVICE_TOKEN_CLIENT_ID prod/apps/auth/authctl/cloudflare-access)"',
|
|
238
|
+
' export CLOUDFLARE_ACCESS_SERVICE_TOKEN_CLIENT_SECRET="$(vault kv get -mount=secret -field=CLOUDFLARE_ACCESS_SERVICE_TOKEN_CLIENT_SECRET prod/apps/auth/authctl/cloudflare-access)"',
|
|
239
|
+
].join("\n");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return `authctl ${args.join(" ")} failed with exit code ${output.exitCode}\n${detail}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
229
245
|
function authctlPath() {
|
|
230
246
|
return existsSync("./node_modules/.bin/authctl") ? "./node_modules/.bin/authctl" : Bun.which("authctl");
|
|
231
247
|
}
|
|
@@ -27,7 +27,7 @@ async function main(argv = Bun.argv.slice(2)) {
|
|
|
27
27
|
const [command, ...rest] = argv;
|
|
28
28
|
|
|
29
29
|
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
30
|
-
console.log(
|
|
30
|
+
console.log(formatHelp());
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -92,7 +92,26 @@ async function main(argv = Bun.argv.slice(2)) {
|
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
throw new Error(
|
|
95
|
+
throw new Error(`Unknown command: ${command}\n\n${formatHelp()}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatHelp() {
|
|
99
|
+
return [
|
|
100
|
+
"Usage:",
|
|
101
|
+
" service <command> [args]",
|
|
102
|
+
"",
|
|
103
|
+
"Commands:",
|
|
104
|
+
" create Provision auth, database, migrations, and first deploy",
|
|
105
|
+
" deploy Deploy the current service",
|
|
106
|
+
" migrate Apply database migrations",
|
|
107
|
+
" seed Run the seed script when configured",
|
|
108
|
+
" doctor Check local tools and cloud access",
|
|
109
|
+
" auth Manage auth resource server and clients",
|
|
110
|
+
" sdk Build or publish generated SDK artifacts",
|
|
111
|
+
" dns Repair or inspect DNS mappings",
|
|
112
|
+
" dashboards Publish Grafana resources",
|
|
113
|
+
" destroy Remove service-managed cloud resources",
|
|
114
|
+
].join("\n");
|
|
96
115
|
}
|
|
97
116
|
|
|
98
117
|
function runLanguageTask(task: "migrate", env?: Record<string, string | undefined>) {
|
|
@@ -18,7 +18,7 @@ async function main(argv = Bun.argv.slice(2)) {
|
|
|
18
18
|
const [command, ...rest] = argv;
|
|
19
19
|
|
|
20
20
|
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
21
|
-
console.log(
|
|
21
|
+
console.log(formatHelp());
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -87,7 +87,25 @@ async function main(argv = Bun.argv.slice(2)) {
|
|
|
87
87
|
throw new Error("SDK commands are only available for ConnectRPC services");
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
throw new Error(
|
|
90
|
+
throw new Error(`Unknown command: ${command}\n\n${formatHelp()}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatHelp() {
|
|
94
|
+
return [
|
|
95
|
+
"Usage:",
|
|
96
|
+
" service <command> [args]",
|
|
97
|
+
"",
|
|
98
|
+
"Commands:",
|
|
99
|
+
" create Provision auth, database, Hyperdrive, and first deploy",
|
|
100
|
+
" deploy Deploy the Worker",
|
|
101
|
+
" migrate Apply database schema",
|
|
102
|
+
" seed Report seed status",
|
|
103
|
+
" doctor Check local tools and cloud access",
|
|
104
|
+
" auth Manage auth resource server and clients",
|
|
105
|
+
" dns Show Workers custom-domain configuration",
|
|
106
|
+
" dashboards Publish Grafana resources",
|
|
107
|
+
" destroy Remove service-managed Worker resources",
|
|
108
|
+
].join("\n");
|
|
91
109
|
}
|
|
92
110
|
|
|
93
111
|
function run(command: string, args: string[], options: { allowFailure?: boolean; capture?: boolean } = {}) {
|
|
@@ -6,9 +6,17 @@
|
|
|
6
6
|
"service": "./scripts/cloudrun/cli.ts"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
+
"dev": "make dev",
|
|
9
10
|
"service": "bun run ./scripts/cloudrun/cli.ts",
|
|
11
|
+
"migrate": "make migrate",
|
|
12
|
+
"gen": "make gen",
|
|
13
|
+
"lint": "make lint",
|
|
14
|
+
"test": "make test",
|
|
15
|
+
"create": "bun run ./scripts/cloudrun/cli.ts create",
|
|
16
|
+
"deploy": "bun run ./scripts/cloudrun/cli.ts deploy",
|
|
10
17
|
"auth": "bun run ./scripts/cloudrun/cli.ts auth",
|
|
11
|
-
"dashboards": "bun run ./scripts/cloudrun/cli.ts dashboards"
|
|
18
|
+
"dashboards": "bun run ./scripts/cloudrun/cli.ts dashboards",
|
|
19
|
+
"destroy": "bun run ./scripts/cloudrun/cli.ts destroy"
|
|
12
20
|
},
|
|
13
21
|
"dependencies": {
|
|
14
22
|
"@anmho/authctl": "0.1.1",
|
|
@@ -6,9 +6,17 @@
|
|
|
6
6
|
"service": "./scripts/cloudrun/cli.ts"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
+
"dev": "make dev",
|
|
9
10
|
"service": "bun run ./scripts/cloudrun/cli.ts",
|
|
11
|
+
"migrate": "make migrate",
|
|
12
|
+
"gen": "make gen",
|
|
13
|
+
"lint": "make lint",
|
|
14
|
+
"test": "make test",
|
|
15
|
+
"create": "bun run ./scripts/cloudrun/cli.ts create",
|
|
16
|
+
"deploy": "bun run ./scripts/cloudrun/cli.ts deploy",
|
|
10
17
|
"auth": "bun run ./scripts/cloudrun/cli.ts auth",
|
|
11
|
-
"dashboards": "bun run ./scripts/cloudrun/cli.ts dashboards"
|
|
18
|
+
"dashboards": "bun run ./scripts/cloudrun/cli.ts dashboards",
|
|
19
|
+
"destroy": "bun run ./scripts/cloudrun/cli.ts destroy"
|
|
12
20
|
},
|
|
13
21
|
"dependencies": {
|
|
14
22
|
"@anmho/authctl": "0.1.1",
|