Skip to content

Commit 9371137

Browse files
CopilotByron
andcommitted
Add detection for stacked PRs across forks error, and also expired credentials
Co-authored-by: Byron <63622+Byron@users.noreply.github.com>
1 parent 74440a1 commit 9371137

File tree

3 files changed

+132
-2
lines changed

3 files changed

+132
-2
lines changed

apps/desktop/src/lib/error/knownErrors.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ export enum Code {
88
ProjectMissing = 'errors.projects.missing',
99
SecretKeychainNotFound = 'errors.secret.keychain_notfound',
1010
MissingLoginKeychain = 'errors.secret.missing_login_keychain',
11-
GitHubTokenExpired = 'errors.github.expired_token'
11+
GitHubTokenExpired = 'errors.github.expired_token',
12+
GitHubStackedPrFork = 'errors.github.stacked_pr_fork'
1213
}
1314

1415
export const KNOWN_ERRORS: Record<string, string> = {
@@ -34,5 +35,10 @@ With \`seahorse\` or equivalent, create a \`Login\` password store, right click
3435
`,
3536
[Code.GitHubTokenExpired]: `
3637
Your GitHub token appears expired, please check your settings!
38+
`,
39+
[Code.GitHubStackedPrFork]: `
40+
Stacked pull requests across forks are not supported by GitHub.
41+
42+
The base branch you specified doesn't exist in your fork. When creating a stacked PR, the base branch must exist in the same repository.
3743
`
3844
};

apps/desktop/src/lib/forge/github/ghQuery.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,26 @@ export async function ghQuery<
8686
: 'GitHub API error';
8787

8888
const message = isErrorlike(err) ? err.message : String(err);
89-
const code = message.startsWith('Not Found -') ? Code.GitHubTokenExpired : undefined;
89+
90+
// Check for stacked PR across forks error (base field invalid)
91+
let code: string | undefined;
92+
if (isErrorlike(err) && 'response' in err) {
93+
const response = (err as any).response;
94+
if (response?.data?.errors instanceof Array) {
95+
const hasInvalidBaseError = response.data.errors.some(
96+
(error: any) =>
97+
error.resource === 'PullRequest' && error.field === 'base' && error.code === 'invalid'
98+
);
99+
if (hasInvalidBaseError) {
100+
code = Code.GitHubStackedPrFork;
101+
}
102+
}
103+
}
104+
105+
// Check for expired token
106+
if (!code && message.startsWith('Not Found -')) {
107+
code = Code.GitHubTokenExpired;
108+
}
90109

91110
return { error: { name: title, message, code } };
92111
}

apps/desktop/src/lib/forge/github/githubPrService.test.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Code } from '$lib/error/knownErrors';
12
import { GitHub } from '$lib/forge/github/github';
23
import { setupMockGitHubApi } from '$lib/testing/mockGitHubApi.svelte';
34
import { type RestEndpointMethodTypes } from '@octokit/rest';
@@ -36,4 +37,108 @@ describe('GitHubPrService', () => {
3637
const pr = await service?.fetch(123);
3738
expect(pr?.title).equal(title);
3839
});
40+
41+
test('should detect stacked PR across forks error', async () => {
42+
const mockError = {
43+
message: 'Validation Failed',
44+
response: {
45+
data: {
46+
message: 'Validation Failed',
47+
errors: [
48+
{
49+
resource: 'PullRequest',
50+
field: 'base',
51+
code: 'invalid'
52+
}
53+
]
54+
}
55+
}
56+
};
57+
58+
vi.spyOn(octokit.pulls, 'create').mockRejectedValue(mockError);
59+
60+
try {
61+
await service?.createPr({
62+
title: 'Test PR',
63+
body: 'Test body',
64+
draft: false,
65+
baseBranchName: 'feature-branch',
66+
upstreamName: 'my-branch'
67+
});
68+
expect.fail('Should have thrown an error');
69+
} catch (err: any) {
70+
expect(err.code).toBe(Code.GitHubStackedPrFork);
71+
}
72+
});
73+
74+
test('should not detect stacked PR error for other validation errors', async () => {
75+
const mockError = {
76+
message: 'Validation Failed',
77+
response: {
78+
data: {
79+
message: 'Validation Failed',
80+
errors: [
81+
{
82+
resource: 'PullRequest',
83+
field: 'title',
84+
code: 'missing'
85+
}
86+
]
87+
}
88+
}
89+
};
90+
91+
vi.spyOn(octokit.pulls, 'create').mockRejectedValue(mockError);
92+
93+
try {
94+
await service?.createPr({
95+
title: 'Test PR',
96+
body: 'Test body',
97+
draft: false,
98+
baseBranchName: 'feature-branch',
99+
upstreamName: 'my-branch'
100+
});
101+
expect.fail('Should have thrown an error');
102+
} catch (err: any) {
103+
expect(err.code).not.toBe(Code.GitHubStackedPrFork);
104+
}
105+
});
106+
107+
test('should detect stacked PR error among multiple validation errors', async () => {
108+
const mockError = {
109+
message: 'Validation Failed',
110+
response: {
111+
data: {
112+
message: 'Validation Failed',
113+
errors: [
114+
{
115+
resource: 'Issue',
116+
field: 'title',
117+
code: 'missing'
118+
},
119+
{
120+
resource: 'PullRequest',
121+
field: 'base',
122+
code: 'invalid'
123+
}
124+
]
125+
}
126+
}
127+
};
128+
129+
vi.spyOn(octokit.pulls, 'create').mockRejectedValue(mockError);
130+
131+
try {
132+
await service?.createPr({
133+
title: 'Test PR',
134+
body: 'Test body',
135+
draft: false,
136+
baseBranchName: 'feature-branch',
137+
upstreamName: 'my-branch'
138+
});
139+
expect.fail('Should have thrown an error');
140+
} catch (err: any) {
141+
expect(err.code).toBe(Code.GitHubStackedPrFork);
142+
}
143+
});
39144
});

0 commit comments

Comments
 (0)