Better parsing of github extension source uris (#8736)

This commit is contained in:
Jacob MacDonald
2025-09-18 12:52:19 -07:00
committed by GitHub
parent db5b49b2ca
commit 899b6f72cb
2 changed files with 71 additions and 11 deletions
@@ -9,6 +9,7 @@ import {
checkForExtensionUpdate, checkForExtensionUpdate,
cloneFromGit, cloneFromGit,
findReleaseAsset, findReleaseAsset,
parseGitHubRepoForReleases,
} from './github.js'; } from './github.js';
import { simpleGit, type SimpleGit } from 'simple-git'; import { simpleGit, type SimpleGit } from 'simple-git';
import { ExtensionUpdateState } from '../../ui/state/extensions.js'; import { ExtensionUpdateState } from '../../ui/state/extensions.js';
@@ -231,4 +232,55 @@ describe('git extension helpers', () => {
expect(result).toBeUndefined(); expect(result).toBeUndefined();
}); });
}); });
describe('parseGitHubRepoForReleases', () => {
it('should parse owner and repo from a full GitHub URL', () => {
const source = 'https://github.com/owner/repo.git';
const { owner, repo } = parseGitHubRepoForReleases(source);
expect(owner).toBe('owner');
expect(repo).toBe('repo');
});
it('should parse owner and repo from a full GitHub UR without .git', () => {
const source = 'https://github.com/owner/repo';
const { owner, repo } = parseGitHubRepoForReleases(source);
expect(owner).toBe('owner');
expect(repo).toBe('repo');
});
it('should fail on a GitHub SSH URL', () => {
const source = 'git@github.com:owner/repo.git';
expect(() => parseGitHubRepoForReleases(source)).toThrow(
'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.',
);
});
it('should parse owner and repo from a shorthand string', () => {
const source = 'owner/repo';
const { owner, repo } = parseGitHubRepoForReleases(source);
expect(owner).toBe('owner');
expect(repo).toBe('repo');
});
it('should handle .git suffix in repo name', () => {
const source = 'owner/repo.git';
const { owner, repo } = parseGitHubRepoForReleases(source);
expect(owner).toBe('owner');
expect(repo).toBe('repo');
});
it('should throw error for invalid source format', () => {
const source = 'invalid-format';
expect(() => parseGitHubRepoForReleases(source)).toThrow(
'Invalid GitHub repository source: invalid-format. Expected "owner/repo" or a github repo uri.',
);
});
it('should throw error for source with too many parts', () => {
const source = 'https://github.com/owner/repo/extra';
expect(() => parseGitHubRepoForReleases(source)).toThrow(
'Invalid GitHub repository source: https://github.com/owner/repo/extra. Expected "owner/repo" or a github repo uri.',
);
});
});
}); });
+19 -11
View File
@@ -74,20 +74,28 @@ export async function cloneFromGit(
} }
} }
function parseGitHubRepo(source: string): { owner: string; repo: string } { export function parseGitHubRepoForReleases(source: string): {
// The source should be "owner/repo" or a full GitHub URL. owner: string;
const parts = source.split('/'); repo: string;
if (!source.includes('://') && parts.length !== 2) { } {
// Default to a github repo path, so `source` can be just an org/repo
const parsedUrl = URL.parse(source, 'https://github.com');
// The pathname should be "/owner/repo".
const parts = parsedUrl?.pathname.substring(1).split('/');
if (parts?.length !== 2) {
throw new Error( throw new Error(
`Invalid GitHub repository source: ${source}. Expected "owner/repo".`, `Invalid GitHub repository source: ${source}. Expected "owner/repo" or a github repo uri.`,
); );
} }
const owner = parts.at(-2); const owner = parts[0];
const repo = parts.at(-1)?.replace('.git', ''); const repo = parts[1].replace('.git', '');
if (!owner || !repo) { if (owner.startsWith('git@github.com')) {
throw new Error(`Invalid GitHub repository source: ${source}`); throw new Error(
`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.`,
);
} }
return { owner, repo }; return { owner, repo };
} }
@@ -155,7 +163,7 @@ export async function checkForExtensionUpdate(
if (!source) { if (!source) {
return ExtensionUpdateState.ERROR; return ExtensionUpdateState.ERROR;
} }
const { owner, repo } = parseGitHubRepo(source); const { owner, repo } = parseGitHubRepoForReleases(source);
const releaseData = await fetchFromGithub( const releaseData = await fetchFromGithub(
owner, owner,
@@ -180,7 +188,7 @@ export async function downloadFromGitHubRelease(
destination: string, destination: string,
): Promise<string> { ): Promise<string> {
const { source, ref } = installMetadata; const { source, ref } = installMetadata;
const { owner, repo } = parseGitHubRepo(source); const { owner, repo } = parseGitHubRepoForReleases(source);
try { try {
const releaseData = await fetchFromGithub(owner, repo, ref); const releaseData = await fetchFromGithub(owner, repo, ref);