diff --git a/.changeset/calm-ids-refused.md b/.changeset/calm-ids-refused.md new file mode 100644 index 0000000..f78ac18 --- /dev/null +++ b/.changeset/calm-ids-refused.md @@ -0,0 +1,5 @@ +--- +"@northguild/worktree": patch +--- + +`worktree branch` now reports a malformed `--github` issue id or `--jira` issue key as `invalid_value` and exits 2, like any other refused value, instead of exiting 1 with `failed`. An issue that does not exist, or a failed lookup, still exits 1. diff --git a/docs/src/app/docs/commands/branch/page.mdx b/docs/src/app/docs/commands/branch/page.mdx index 193d16c..c3f18fe 100644 --- a/docs/src/app/docs/commands/branch/page.mdx +++ b/docs/src/app/docs/commands/branch/page.mdx @@ -276,7 +276,7 @@ describing it goes to stderr as usual: | `code` | Means | Exit | | --- | --- | --- | | `missing_value` | a value with no default was not given; `details` names it and the flag | `2` | -| `invalid_value` | a value or flag combination was refused: a bad branch name, an empty brief | `2` | +| `invalid_value` | a value or flag combination was refused: a bad branch name, an empty brief, a malformed `--github` or `--jira` issue id | `2` | | `not_found` | something named does not exist, such as the `--source` branch | `2` | | `timeout` | a call that is bounded did not answer in time | `1` | | `failed` | anything else | `1` | diff --git a/src/commands/branch.test.ts b/src/commands/branch.test.ts index bd203e2..abb9916 100644 --- a/src/commands/branch.test.ts +++ b/src/commands/branch.test.ts @@ -1780,6 +1780,23 @@ describe("branch command", () => { expect(mockGitCreateWorktreeCalls()).toBe(0); }); + it.each([ + ["--github abc", { github: "abc" }], + ["a malformed --jira key", { jira: "not-a-key" }], + ])("gives %s as invalid_value without creating a worktree", async (_label, flags) => { + // Earlier tests leave these stubbed; this one needs the real validation. + vi.spyOn(githubIntegration, "fetchGitHubIssue").mockRestore(); + vi.spyOn(jiraIntegration, "getJiraBranchNameFromIssue").mockRestore(); + + await runJson({}, flags); + + expect(output.document()).toMatchObject({ + error: { code: "invalid_value" }, + }); + expect(process.exitCode).toBe(2); + expect(mockGitCreateWorktreeCalls()).toBe(0); + }); + it("gives a missing source branch as not_found", async () => { vi.spyOn(git, "gitGetRemoteBranches").mockResolvedValue(["origin/main"]); diff --git a/src/integrations/github.test.ts b/src/integrations/github.test.ts index d0fdf94..dbb853c 100644 --- a/src/integrations/github.test.ts +++ b/src/integrations/github.test.ts @@ -180,9 +180,13 @@ describe("GitHub integration", () => { }); it("throws for invalid issue ids", async () => { - await expect(fetchGitHubIssue("abc")).rejects.toThrow( - 'GitHub: Invalid issue id "abc".', - ); + const refusal = fetchGitHubIssue("abc"); + + await expect(refusal).rejects.toThrow('GitHub: Invalid issue id "abc".'); + await expect(refusal).rejects.toMatchObject({ + code: "invalid_value", + oclif: { exit: 2 }, + }); }); it("throws when the GitHub API responds with an error", async () => { @@ -421,9 +425,13 @@ describe("GitHub integration", () => { const fetchSpy = vi.fn(); vi.stubGlobal("fetch", fetchSpy); - await expect(assignGitHubIssue("abc")).rejects.toThrow( - 'GitHub: Invalid issue id "abc".', - ); + const refusal = assignGitHubIssue("abc"); + + await expect(refusal).rejects.toThrow('GitHub: Invalid issue id "abc".'); + await expect(refusal).rejects.toMatchObject({ + code: "invalid_value", + oclif: { exit: 2 }, + }); expect(fetchSpy).not.toHaveBeenCalled(); }); diff --git a/src/integrations/github.ts b/src/integrations/github.ts index 09e2285..abe5dc3 100644 --- a/src/integrations/github.ts +++ b/src/integrations/github.ts @@ -1,6 +1,6 @@ import { commandExists, run } from "../lib/cli.js"; import { gitGetConfigValue, gitSetConfigValue } from "../lib/git.js"; -import { askInput, assertCanPrompt } from "../lib/prompt.js"; +import { askInput, assertCanPrompt, InvalidValueError } from "../lib/prompt.js"; interface GitHubRepository { owner: string; @@ -245,7 +245,7 @@ export async function fetchGitHubIssue(issueId: number | string) { const normalizedIssueId = String(issueId).trim(); if (!/^\d+$/.test(normalizedIssueId)) { - throw new Error(`GitHub: Invalid issue id "${issueId}".`); + throw new InvalidValueError(`GitHub: Invalid issue id "${issueId}".`); } const { owner, name } = await getCurrentGitHubRepository(); @@ -330,7 +330,7 @@ export async function assignGitHubIssue(issueId: number | string) { const normalizedIssueId = String(issueId).trim(); if (!/^\d+$/.test(normalizedIssueId)) { - throw new Error(`GitHub: Invalid issue id "${issueId}".`); + throw new InvalidValueError(`GitHub: Invalid issue id "${issueId}".`); } const { owner, name } = await getCurrentGitHubRepository(); diff --git a/src/integrations/jira.test.ts b/src/integrations/jira.test.ts index d31cbef..7a9688c 100644 --- a/src/integrations/jira.test.ts +++ b/src/integrations/jira.test.ts @@ -28,9 +28,13 @@ describe("Jira integration", () => { }); it("throws for invalid issue ids", async () => { - await expect(fetchJiraIssue("invalid")).rejects.toThrow( - 'Jira: Invalid issue id "invalid".', - ); + const refusal = fetchJiraIssue("invalid"); + + await expect(refusal).rejects.toThrow('Jira: Invalid issue id "invalid".'); + await expect(refusal).rejects.toMatchObject({ + code: "invalid_value", + oclif: { exit: 2 }, + }); }); it("fetches issue info using jira.host, jira.email and jira.apiToken", async () => { diff --git a/src/integrations/jira.ts b/src/integrations/jira.ts index 0e50de3..a117e6c 100644 --- a/src/integrations/jira.ts +++ b/src/integrations/jira.ts @@ -1,4 +1,5 @@ import { gitGetConfigValue } from "../lib/git.js"; +import { InvalidValueError } from "../lib/prompt.js"; import { sanitizeBranchName } from "../lib/utils.js"; interface JiraCredentials { @@ -53,7 +54,7 @@ function getIssueKey(issueId: string): string { const normalizedIssueId = issueId.trim().toUpperCase(); if (!/^[A-Z][A-Z0-9]*-\d+$/.test(normalizedIssueId)) { - throw new Error(`Jira: Invalid issue id "${issueId}".`); + throw new InvalidValueError(`Jira: Invalid issue id "${issueId}".`); } return normalizedIssueId;