From ec2fa122df29fcb9db50bf177a32eda77a064c44 Mon Sep 17 00:00:00 2001 From: addielarue Date: Sat, 18 Jul 2026 21:01:51 +1000 Subject: [PATCH] feat(cli): add --ci-provider flag to setup-ci setup-ci already generated config for 5 CI providers internally but had no way to select one explicitly, and the --workflow option's baked-in default silently prevented the provider-based default path from ever being used from the CLI. Add a --ci-provider flag (validated against the 5 supported providers, with a clear error listing valid options), drop the hardcoded --workflow default so provider-based path selection actually reaches the CLI, and keep auto-detection as the fallback when the flag is omitted. Fixes #289 --- src/commands/init-ci.ts | 13 ++++++++++++- tests/init-ci.test.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/commands/init-ci.ts b/src/commands/init-ci.ts index 53e73b7..8e09f9f 100644 --- a/src/commands/init-ci.ts +++ b/src/commands/init-ci.ts @@ -51,6 +51,14 @@ function providerLabel(provider: string): string { return PROVIDER_LABELS[provider] ?? "CI Workflow"; } +const VALID_CI_PROVIDERS = Object.keys(CI_FILE_PATHS); + +function validateCiProvider(provider: string | undefined): void { + if (provider !== undefined && !VALID_CI_PROVIDERS.includes(provider)) { + throw new Error(`Invalid --ci-provider "${provider}". Valid options: ${VALID_CI_PROVIDERS.join(", ")}.`); + } +} + const DEFAULT_WORKFLOW_PATH = ".github/workflows/mcp-observatory.yml"; const DEFAULT_BADGE_PATH = "docs/mcp-observatory-badge.md"; const DEFAULT_TARGET_CONFIG_PATH = "mcp-observatory.target.json"; @@ -495,6 +503,7 @@ function doctorFixOptions(options: InitCiOptions, workflow: string | undefined): } export async function doctorSetupCi(options: InitCiOptions = {}): Promise { + validateCiProvider(options.ciProvider); const detectedProvider = options.ciProvider ?? detectCiProvider() ?? "github-actions"; const defaultPath = CI_FILE_PATHS[detectedProvider] ?? DEFAULT_WORKFLOW_PATH; const workflowPath = options.workflow ?? defaultPath; @@ -595,6 +604,7 @@ export async function doctorSetupCi(options: InitCiOptions = {}): Promise { + validateCiProvider(options.ciProvider); if (options.command && options.target) { throw new Error("Use either --command or --target, not both."); } @@ -644,7 +654,8 @@ function addInitCiOptions(command: Command): Command { return command .option("--command ", "MCP server command to test, for example: 'npx -y my-mcp-server'") .option("--target ", "Target config JSON path to use instead of a command.") - .option("--workflow ", "Workflow output path.", DEFAULT_WORKFLOW_PATH) + .option("--workflow ", "Workflow output path. Defaults to the path for the detected or selected CI provider.") + .option("--ci-provider ", `Override auto-detected CI provider (${VALID_CI_PROVIDERS.join(", ")}).`) .option("--badge", "Also write a README badge snippet.", false) .option("--badge-file ", "Badge snippet output path.", DEFAULT_BADGE_PATH) .option("--target-config [file]", "Also write an example target config and point the workflow at it.") diff --git a/tests/init-ci.test.ts b/tests/init-ci.test.ts index 4a5d785..b535e2c 100644 --- a/tests/init-ci.test.ts +++ b/tests/init-ci.test.ts @@ -287,4 +287,45 @@ describe("init-ci", () => { it("rejects target config generation when an existing target is supplied", async () => { await expect(initCi({ target: "./target.json", targetConfig: true })).rejects.toThrow("Use either --target or --target-config"); }); + + it("honors an explicit --ci-provider override instead of auto-detecting", async () => { + const dir = await tempDir(); + const workflow = path.join(dir, ".gitlab-ci.yml"); + + const result = await initCi({ + command: "npx -y @example/mcp-server", + workflow, + ciProvider: "gitlab-ci", + }); + + expect(result.workflowStatus).toBe("created"); + const workflowText = await readFile(workflow, "utf8"); + expect(workflowText).toContain("mcp-observatory:"); + expect(workflowText).toContain("npx @kryptosai/mcp-observatory test npx -y @example/mcp-server"); + }); + + it("rejects an invalid --ci-provider value and lists the valid options", async () => { + await expect(initCi({ command: "npx -y server", ciProvider: "jenkins" as never })).rejects.toThrow( + 'Invalid --ci-provider "jenkins". Valid options: github-actions, gitlab-ci, circleci, bitbucket-pipelines, azure-pipelines.', + ); + }); + + it("validates --ci-provider in doctor mode too", async () => { + await expect(doctorSetupCi({ ciProvider: "travis" as never })).rejects.toThrow('Invalid --ci-provider "travis"'); + }); + + it("derives the default workflow path from --ci-provider when --workflow is omitted", async () => { + const dir = await tempDir(); + const originalCwd = process.cwd(); + process.chdir(dir); + try { + const result = await initCi({ command: "npx -y @example/mcp-server", ciProvider: "gitlab-ci" }); + expect(result.workflowPath).toBe(".gitlab-ci.yml"); + expect(result.workflowStatus).toBe("created"); + const workflowText = await readFile(path.join(dir, ".gitlab-ci.yml"), "utf8"); + expect(workflowText).toContain("mcp-observatory:"); + } finally { + process.chdir(originalCwd); + } + }); });