Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-ids-refused.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion docs/src/app/docs/commands/branch/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
17 changes: 17 additions & 0 deletions src/commands/branch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);

Expand Down
20 changes: 14 additions & 6 deletions src/integrations/github.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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();
});

Expand Down
6 changes: 3 additions & 3 deletions src/integrations/github.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
10 changes: 7 additions & 3 deletions src/integrations/jira.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
3 changes: 2 additions & 1 deletion src/integrations/jira.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { gitGetConfigValue } from "../lib/git.js";
import { InvalidValueError } from "../lib/prompt.js";
import { sanitizeBranchName } from "../lib/utils.js";

interface JiraCredentials {
Expand Down Expand Up @@ -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;
Expand Down
Loading