fix: add grouped command help docs guard
This commit is contained in:
parent
41ad14cc81
commit
50ceed86bf
@ -29,9 +29,9 @@ crabbox run --id blue-lobster -- pnpm test
|
||||
Subcommands:
|
||||
|
||||
```text
|
||||
hydrate --id <lease-id-or-slug> [--repo owner/name] [--workflow <file|name|id>] [--ref <ref>] [--wait-timeout 20m] [--keep-alive-minutes 90] [--reclaim] [--timing-json] [-f key=value]
|
||||
hydrate --id <lease-id-or-slug> [--repo owner/name] [--workflow <file|name|id>] [--ref <ref>] [--wait-timeout 20m] [--keep-alive-minutes 90] [--reclaim] [--timing-json] [-f key=value] [--field key=value]
|
||||
register --id <lease-id-or-slug> [--repo owner/name] [--name <runner-name>] [--labels <csv>] [--version latest] [--ephemeral=true] [--reclaim]
|
||||
dispatch [--repo owner/name] [--workflow <file|name|id>] [--ref <ref>] [-f key=value]
|
||||
dispatch [--repo owner/name] [--workflow <file|name|id>] [--ref <ref>] [-f key=value] [--field key=value]
|
||||
```
|
||||
|
||||
Hydrate/register validate the local repo claim before touching the lease. Use `--reclaim` when intentionally moving a lease to the current repo.
|
||||
@ -54,7 +54,7 @@ actions:
|
||||
|
||||
Workflow jobs should target the dynamic label printed by registration, for example `crabbox-cbx-123`, plus any static labels configured for the project.
|
||||
When `actions.job` is set and the workflow declares `crabbox_job`, Crabbox sends it and verifies that the ready marker came from that job. Older workflows can omit both.
|
||||
Use `actions.fields` for repository-specific workflow inputs that should be sent on every hydration. CLI `-f key=value` values override matching configured fields for that dispatch.
|
||||
Use `actions.fields` for repository-specific workflow inputs that should be sent on every hydration. CLI `-f key=value` / `--field key=value` values override matching configured fields for that dispatch.
|
||||
|
||||
## Hydration Flow
|
||||
|
||||
|
||||
@ -29,7 +29,12 @@ func (r GitHubRepo) Slug() string {
|
||||
|
||||
func (a App) actions(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return exit(2, "usage: crabbox actions hydrate|register|dispatch")
|
||||
a.printActionsHelp()
|
||||
return exit(2, "missing actions subcommand")
|
||||
}
|
||||
if wantsHelp(args) {
|
||||
a.printActionsHelp()
|
||||
return nil
|
||||
}
|
||||
switch args[0] {
|
||||
case "hydrate":
|
||||
@ -43,6 +48,47 @@ func (a App) actions(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (a App) printActionsHelp() {
|
||||
fmt.Fprintln(a.Stdout, `Usage:
|
||||
crabbox actions hydrate --id <lease-id-or-slug> [flags]
|
||||
crabbox actions register --id <lease-id-or-slug> [flags]
|
||||
crabbox actions dispatch [flags]
|
||||
|
||||
Subcommands:
|
||||
hydrate Register a runner, dispatch the hydrate workflow, wait for readiness
|
||||
register Register an existing Linux lease as a GitHub Actions runner
|
||||
dispatch Dispatch the configured GitHub Actions workflow
|
||||
|
||||
Hydrate Flags:
|
||||
--id <lease-id-or-slug>
|
||||
--repo <owner/name>
|
||||
--workflow <file|name|id>
|
||||
--ref <ref>
|
||||
--wait-timeout <duration>
|
||||
--keep-alive-minutes <n>
|
||||
--reclaim
|
||||
--timing-json
|
||||
-f, --field <key=value>
|
||||
|
||||
Register Flags:
|
||||
--id <lease-id-or-slug>
|
||||
--repo <owner/name>
|
||||
--name <runner-name>
|
||||
--labels <csv>
|
||||
--version <version|latest>
|
||||
--ephemeral
|
||||
--reclaim
|
||||
|
||||
Dispatch Flags:
|
||||
--repo <owner/name>
|
||||
--workflow <file|name|id>
|
||||
--ref <ref>
|
||||
-f, --field <key=value>
|
||||
|
||||
Docs:
|
||||
docs/commands/actions.md`)
|
||||
}
|
||||
|
||||
func (a App) actionsHydrate(ctx context.Context, args []string) error {
|
||||
started := time.Now()
|
||||
fs := newFlagSet("actions hydrate", a.Stderr)
|
||||
|
||||
@ -8,7 +8,12 @@ import (
|
||||
|
||||
func (a App) admin(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return exit(2, "usage: crabbox admin leases|release|delete")
|
||||
a.printAdminHelp()
|
||||
return exit(2, "missing admin subcommand")
|
||||
}
|
||||
if wantsHelp(args) {
|
||||
a.printAdminHelp()
|
||||
return nil
|
||||
}
|
||||
switch args[0] {
|
||||
case "leases":
|
||||
@ -22,6 +27,26 @@ func (a App) admin(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (a App) printAdminHelp() {
|
||||
fmt.Fprintln(a.Stdout, `Usage:
|
||||
crabbox admin leases [flags]
|
||||
crabbox admin release <lease-id-or-slug> [flags]
|
||||
crabbox admin delete <lease-id-or-slug> --force [flags]
|
||||
|
||||
Subcommands:
|
||||
leases List coordinator lease records
|
||||
release Mark a lease released, optionally deleting the backing server
|
||||
delete Delete the backing server and mark the lease released
|
||||
|
||||
Flags:
|
||||
leases: --state <state> --owner <email> --org <name> --limit <n> --json
|
||||
release: --id <lease-id-or-slug> --delete --json
|
||||
delete: --id <lease-id-or-slug> --force --json
|
||||
|
||||
Docs:
|
||||
docs/commands/admin.md`)
|
||||
}
|
||||
|
||||
func (a App) adminLeases(ctx context.Context, args []string) error {
|
||||
fs := newFlagSet("admin leases", a.Stderr)
|
||||
state := fs.String("state", "", "filter by state")
|
||||
|
||||
@ -246,3 +246,10 @@ func parseFlags(fs *flag.FlagSet, args []string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func wantsHelp(args []string) bool {
|
||||
if len(args) == 0 {
|
||||
return false
|
||||
}
|
||||
return args[0] == "-h" || args[0] == "--help" || args[0] == "help"
|
||||
}
|
||||
|
||||
@ -16,7 +16,12 @@ type cacheEntry struct {
|
||||
|
||||
func (a App) cache(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return exit(2, "usage: crabbox cache list|stats|purge|warm")
|
||||
a.printCacheHelp()
|
||||
return exit(2, "missing cache subcommand")
|
||||
}
|
||||
if wantsHelp(args) {
|
||||
a.printCacheHelp()
|
||||
return nil
|
||||
}
|
||||
switch args[0] {
|
||||
case "list", "stats":
|
||||
@ -30,6 +35,27 @@ func (a App) cache(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (a App) printCacheHelp() {
|
||||
fmt.Fprintln(a.Stdout, `Usage:
|
||||
crabbox cache stats --id <lease-id-or-slug> [flags]
|
||||
crabbox cache list --id <lease-id-or-slug> [flags]
|
||||
crabbox cache purge --id <lease-id-or-slug> --kind <kind> --force [flags]
|
||||
crabbox cache warm --id <lease-id-or-slug> -- <command...>
|
||||
|
||||
Subcommands:
|
||||
list, stats Show remote cache usage
|
||||
purge Remove selected cache content
|
||||
warm Run a command that populates caches
|
||||
|
||||
Flags:
|
||||
stats/list: --id <lease-id-or-slug> --reclaim --json
|
||||
purge: --id <lease-id-or-slug> --kind pnpm|npm|docker|git|all --force --reclaim
|
||||
warm: --id <lease-id-or-slug> --reclaim
|
||||
|
||||
Docs:
|
||||
docs/commands/cache.md`)
|
||||
}
|
||||
|
||||
func (a App) cacheStats(ctx context.Context, args []string) error {
|
||||
fs := newFlagSet("cache stats", a.Stderr)
|
||||
id := fs.String("id", "", "lease id or slug")
|
||||
|
||||
@ -11,7 +11,12 @@ import (
|
||||
|
||||
func (a App) config(_ context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return exit(2, "usage: crabbox config show|path|set-broker")
|
||||
a.printConfigHelp()
|
||||
return exit(2, "missing config subcommand")
|
||||
}
|
||||
if wantsHelp(args) {
|
||||
a.printConfigHelp()
|
||||
return nil
|
||||
}
|
||||
switch args[0] {
|
||||
case "path":
|
||||
@ -30,6 +35,25 @@ func (a App) config(_ context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (a App) printConfigHelp() {
|
||||
fmt.Fprintln(a.Stdout, `Usage:
|
||||
crabbox config path
|
||||
crabbox config show [--json]
|
||||
crabbox config set-broker --url <url> [flags]
|
||||
|
||||
Subcommands:
|
||||
path Print the user config path
|
||||
show Print merged config without secret values
|
||||
set-broker Store broker URL and optional tokens in user config
|
||||
|
||||
Flags:
|
||||
show: --json
|
||||
set-broker: --url <url> --provider hetzner|aws --token-stdin --admin-token-stdin
|
||||
|
||||
Docs:
|
||||
docs/commands/config.md`)
|
||||
}
|
||||
|
||||
func (a App) configShow(args []string) error {
|
||||
fs := newFlagSet("config show", a.Stderr)
|
||||
jsonOut := fs.Bool("json", false, "print JSON")
|
||||
|
||||
@ -9,7 +9,12 @@ import (
|
||||
|
||||
func (a App) desktop(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return exit(2, "usage: crabbox desktop launch --id <lease-id-or-slug> [--browser] [--url <url>] -- <command...>")
|
||||
a.printDesktopHelp()
|
||||
return exit(2, "missing desktop subcommand")
|
||||
}
|
||||
if wantsHelp(args) {
|
||||
a.printDesktopHelp()
|
||||
return nil
|
||||
}
|
||||
switch args[0] {
|
||||
case "launch":
|
||||
@ -19,6 +24,31 @@ func (a App) desktop(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (a App) printDesktopHelp() {
|
||||
fmt.Fprintln(a.Stdout, `Usage:
|
||||
crabbox desktop launch --id <lease-id-or-slug> [flags] -- <command...>
|
||||
crabbox desktop launch --id <lease-id-or-slug> --browser [--url <url>]
|
||||
|
||||
Subcommands:
|
||||
launch Start an app inside a desktop lease
|
||||
|
||||
Flags:
|
||||
--id <lease-id-or-slug>
|
||||
--provider hetzner|aws|ssh
|
||||
--target linux|macos|windows
|
||||
--windows-mode normal|wsl2
|
||||
--static-host <host>
|
||||
--static-user <user>
|
||||
--static-port <port>
|
||||
--static-work-root <path>
|
||||
--browser
|
||||
--url <url>
|
||||
--reclaim
|
||||
|
||||
Docs:
|
||||
docs/commands/desktop.md`)
|
||||
}
|
||||
|
||||
func (a App) desktopLaunch(ctx context.Context, args []string) error {
|
||||
defaults := defaultConfig()
|
||||
fs := newFlagSet("desktop launch", a.Stderr)
|
||||
|
||||
@ -81,6 +81,22 @@ func TestSubcommandHelpExitsZero(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupedCommandHelpExitsZero(t *testing.T) {
|
||||
for _, command := range []string{"actions", "admin", "cache", "config", "desktop", "pool", "machine"} {
|
||||
t.Run(command, func(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
app := App{Stdout: &stdout, Stderr: &bytes.Buffer{}}
|
||||
err := app.Run(context.Background(), []string{command, "--help"})
|
||||
if err != nil {
|
||||
t.Fatalf("%s --help error=%v, want nil", command, err)
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "Usage:") {
|
||||
t.Fatalf("%s --help output missing usage: %s", command, stdout.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpSubcommandRoutesToCommandHelp(t *testing.T) {
|
||||
var stderr bytes.Buffer
|
||||
app := App{Stdout: &bytes.Buffer{}, Stderr: &stderr}
|
||||
|
||||
@ -9,6 +9,14 @@ import (
|
||||
)
|
||||
|
||||
func (a App) pool(ctx context.Context, args []string) error {
|
||||
if wantsHelp(args) {
|
||||
fmt.Fprintln(a.Stdout, `Usage:
|
||||
crabbox pool list [--json]
|
||||
|
||||
Alias:
|
||||
crabbox pool list is an alias for crabbox list`)
|
||||
return nil
|
||||
}
|
||||
if len(args) == 0 || args[0] != "list" {
|
||||
return exit(2, "usage: crabbox pool list [--json]")
|
||||
}
|
||||
@ -141,7 +149,20 @@ func coordinatorMachineOrphanField(labels map[string]string, activeLeaseIDs map[
|
||||
|
||||
func (a App) machine(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return exit(2, "usage: crabbox machine cleanup [--dry-run]")
|
||||
fmt.Fprintln(a.Stdout, `Usage:
|
||||
crabbox machine cleanup [--dry-run]
|
||||
|
||||
Alias:
|
||||
crabbox machine cleanup is an alias for crabbox cleanup`)
|
||||
return exit(2, "missing machine subcommand")
|
||||
}
|
||||
if wantsHelp(args) {
|
||||
fmt.Fprintln(a.Stdout, `Usage:
|
||||
crabbox machine cleanup [--dry-run]
|
||||
|
||||
Alias:
|
||||
crabbox machine cleanup is an alias for crabbox cleanup`)
|
||||
return nil
|
||||
}
|
||||
switch args[0] {
|
||||
case "cleanup":
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const root = process.cwd();
|
||||
@ -10,6 +12,7 @@ const commands = readHelpCommands();
|
||||
const commandNames = commands.map((command) => command.name);
|
||||
const commandSet = new Set(commandNames);
|
||||
const indexEntries = readCommandIndex();
|
||||
const cliPath = buildCLIForHelp();
|
||||
|
||||
for (const command of commands) {
|
||||
const file = path.join(commandDocsDir, `${command.name}.md`);
|
||||
@ -21,6 +24,12 @@ for (const command of commands) {
|
||||
if (firstLine !== `# ${command.name}`) {
|
||||
failures.push(`${rel(file)} should start with "# ${command.name}"`);
|
||||
}
|
||||
const help = runCommandHelp(command.name);
|
||||
if (help.status !== 0) {
|
||||
failures.push(`crabbox ${command.name} --help should exit 0, got ${help.status}: ${help.output.trim()}`);
|
||||
} else if (!help.output.includes("Usage:") && !help.output.includes("Usage of ")) {
|
||||
failures.push(`crabbox ${command.name} --help should print usage text`);
|
||||
}
|
||||
}
|
||||
|
||||
const seenIndexEntries = new Set();
|
||||
@ -93,6 +102,40 @@ function readHelpCommands() {
|
||||
return commands;
|
||||
}
|
||||
|
||||
function runCommandHelp(command) {
|
||||
if (!cliPath) {
|
||||
return {
|
||||
status: 1,
|
||||
output: "failed to build crabbox CLI for help checks",
|
||||
};
|
||||
}
|
||||
const result = spawnSync(cliPath, [command, "--help"], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
});
|
||||
return {
|
||||
status: result.status ?? 1,
|
||||
output: `${result.stdout ?? ""}${result.stderr ?? ""}`,
|
||||
};
|
||||
}
|
||||
|
||||
function buildCLIForHelp() {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "crabbox-docs-"));
|
||||
const binary = path.join(dir, process.platform === "win32" ? "crabbox.exe" : "crabbox");
|
||||
process.on("exit", () => {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
const result = spawnSync("go", ["build", "-o", binary, "./cmd/crabbox"], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
failures.push(`go build ./cmd/crabbox failed: ${`${result.stdout ?? ""}${result.stderr ?? ""}`.trim()}`);
|
||||
return "";
|
||||
}
|
||||
return binary;
|
||||
}
|
||||
|
||||
function readCommandIndex() {
|
||||
const readmePath = path.join(commandDocsDir, "README.md");
|
||||
const readme = fs.readFileSync(readmePath, "utf8");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user