Skip to content

Commit c7f3186

Browse files
jakemac53chrstnb
authored andcommitted
Better parsing of github extension source uris (#8736)
1 parent b8363e3 commit c7f3186

File tree

2 files changed

+71
-11
lines changed

2 files changed

+71
-11
lines changed

packages/cli/src/config/extensions/github.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
checkForExtensionUpdate,
1010
cloneFromGit,
1111
findReleaseAsset,
12+
parseGitHubRepoForReleases,
1213
} from './github.js';
1314
import { simpleGit, type SimpleGit } from 'simple-git';
1415
import { ExtensionUpdateState } from '../../ui/state/extensions.js';
@@ -231,4 +232,55 @@ describe('git extension helpers', () => {
231232
expect(result).toBeUndefined();
232233
});
233234
});
235+
236+
describe('parseGitHubRepoForReleases', () => {
237+
it('should parse owner and repo from a full GitHub URL', () => {
238+
const source = 'https://github.com/owner/repo.git';
239+
const { owner, repo } = parseGitHubRepoForReleases(source);
240+
expect(owner).toBe('owner');
241+
expect(repo).toBe('repo');
242+
});
243+
244+
it('should parse owner and repo from a full GitHub UR without .git', () => {
245+
const source = 'https://github.com/owner/repo';
246+
const { owner, repo } = parseGitHubRepoForReleases(source);
247+
expect(owner).toBe('owner');
248+
expect(repo).toBe('repo');
249+
});
250+
251+
it('should fail on a GitHub SSH URL', () => {
252+
const source = 'git@github.com:owner/repo.git';
253+
expect(() => parseGitHubRepoForReleases(source)).toThrow(
254+
'GitHub release-based extensions are not supported for SSH. You must use an HTTPS URI with a personal access token to download releases from private repositories. You can set your personal access token in the GITHUB_TOKEN environment variable and install the extension via SSH.',
255+
);
256+
});
257+
258+
it('should parse owner and repo from a shorthand string', () => {
259+
const source = 'owner/repo';
260+
const { owner, repo } = parseGitHubRepoForReleases(source);
261+
expect(owner).toBe('owner');
262+
expect(repo).toBe('repo');
263+
});
264+
265+
it('should handle .git suffix in repo name', () => {
266+
const source = 'owner/repo.git';
267+
const { owner, repo } = parseGitHubRepoForReleases(source);
268+
expect(owner).toBe('owner');
269+
expect(repo).toBe('repo');
270+
});
271+
272+
it('should throw error for invalid source format', () => {
273+
const source = 'invalid-format';
274+
expect(() => parseGitHubRepoForReleases(source)).toThrow(
275+
'Invalid GitHub repository source: invalid-format. Expected "owner/repo" or a github repo uri.',
276+
);
277+
});
278+
279+
it('should throw error for source with too many parts', () => {
280+
const source = 'https://github.com/owner/repo/extra';
281+
expect(() => parseGitHubRepoForReleases(source)).toThrow(
282+
'Invalid GitHub repository source: https://github.com/owner/repo/extra. Expected "owner/repo" or a github repo uri.',
283+
);
284+
});
285+
});
234286
});

packages/cli/src/config/extensions/github.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,20 +74,28 @@ export async function cloneFromGit(
7474
}
7575
}
7676

77-
function parseGitHubRepo(source: string): { owner: string; repo: string } {
78-
// The source should be "owner/repo" or a full GitHub URL.
79-
const parts = source.split('/');
80-
if (!source.includes('://') && parts.length !== 2) {
77+
export function parseGitHubRepoForReleases(source: string): {
78+
owner: string;
79+
repo: string;
80+
} {
81+
// Default to a github repo path, so `source` can be just an org/repo
82+
const parsedUrl = URL.parse(source, 'https://github.com');
83+
// The pathname should be "/owner/repo".
84+
const parts = parsedUrl?.pathname.substring(1).split('/');
85+
if (parts?.length !== 2) {
8186
throw new Error(
82-
`Invalid GitHub repository source: ${source}. Expected "owner/repo".`,
87+
`Invalid GitHub repository source: ${source}. Expected "owner/repo" or a github repo uri.`,
8388
);
8489
}
85-
const owner = parts.at(-2);
86-
const repo = parts.at(-1)?.replace('.git', '');
90+
const owner = parts[0];
91+
const repo = parts[1].replace('.git', '');
8792

88-
if (!owner || !repo) {
89-
throw new Error(`Invalid GitHub repository source: ${source}`);
93+
if (owner.startsWith('git@github.com')) {
94+
throw new Error(
95+
`GitHub release-based extensions are not supported for SSH. You must use an HTTPS URI with a personal access token to download releases from private repositories. You can set your personal access token in the GITHUB_TOKEN environment variable and install the extension via SSH.`,
96+
);
9097
}
98+
9199
return { owner, repo };
92100
}
93101

@@ -155,7 +163,7 @@ export async function checkForExtensionUpdate(
155163
if (!source) {
156164
return ExtensionUpdateState.ERROR;
157165
}
158-
const { owner, repo } = parseGitHubRepo(source);
166+
const { owner, repo } = parseGitHubRepoForReleases(source);
159167

160168
const releaseData = await fetchFromGithub(
161169
owner,
@@ -180,7 +188,7 @@ export async function downloadFromGitHubRelease(
180188
destination: string,
181189
): Promise<string> {
182190
const { source, ref } = installMetadata;
183-
const { owner, repo } = parseGitHubRepo(source);
191+
const { owner, repo } = parseGitHubRepoForReleases(source);
184192

185193
try {
186194
const releaseData = await fetchFromGithub(owner, repo, ref);

0 commit comments

Comments
 (0)