Don't always fall back on a git clone when installing extensions (#11229)

This commit is contained in:
Jacob MacDonald
2025-10-15 14:29:16 -07:00
committed by GitHub
parent 1fc3fc0a22
commit 8c1656bf56
4 changed files with 314 additions and 46 deletions

View File

@@ -79,16 +79,22 @@ export async function cloneFromGit(
export function parseGitHubRepoForReleases(source: string): {
owner: string;
repo: string;
} {
} | null {
// Default to a github repo path, so `source` can be just an org/repo
const parsedUrl = URL.parse(source, 'https://github.com');
if (!parsedUrl) {
throw new Error(`Invalid repo URL: ${source}`);
}
// The pathname should be "/owner/repo".
const parts = parsedUrl?.pathname
.substring(1)
.split('/')
// Remove the empty segments, fixes trailing slashes
.filter((part) => part !== '');
if (parts?.length !== 2 || parsedUrl?.host !== 'github.com') {
if (parsedUrl?.host !== 'github.com') {
return null;
}
if (parts?.length !== 2) {
throw new Error(
`Invalid GitHub repository source: ${source}. Expected "owner/repo" or a github repo uri.`,
);
@@ -110,7 +116,7 @@ export async function fetchReleaseFromGithub(
repo: string,
ref?: string,
allowPreRelease?: boolean,
): Promise<GithubReleaseData> {
): Promise<GithubReleaseData | null> {
if (ref) {
return await fetchJson(
`https://api.github.com/repos/${owner}/${repo}/releases/tags/${ref}`,
@@ -120,9 +126,14 @@ export async function fetchReleaseFromGithub(
if (!allowPreRelease) {
// Grab the release that is tagged as the "latest", github does not allow
// this to be a pre-release so we can blindly grab it.
return await fetchJson(
`https://api.github.com/repos/${owner}/${repo}/releases/latest`,
);
try {
return await fetchJson(
`https://api.github.com/repos/${owner}/${repo}/releases/latest`,
);
} catch (_) {
// This can fail if there is no release marked latest. In that case
// we want to just try the pre-release logic below.
}
}
// If pre-releases are allowed, we just grab the most recent release.
@@ -130,7 +141,7 @@ export async function fetchReleaseFromGithub(
`https://api.github.com/repos/${owner}/${repo}/releases?per_page=1`,
);
if (releases.length === 0) {
throw new Error('No releases found');
return null;
}
return releases[0];
}
@@ -206,7 +217,14 @@ export async function checkForExtensionUpdate(
console.error(`No "source" provided for extension.`);
return ExtensionUpdateState.ERROR;
}
const { owner, repo } = parseGitHubRepoForReleases(source);
const repoInfo = parseGitHubRepoForReleases(source);
if (!repoInfo) {
console.error(
`Source is not a valid GitHub repository for release checks: ${source}`,
);
return ExtensionUpdateState.ERROR;
}
const { owner, repo } = repoInfo;
const releaseData = await fetchReleaseFromGithub(
owner,
@@ -214,6 +232,9 @@ export async function checkForExtensionUpdate(
installMetadata.ref,
installMetadata.allowPreRelease,
);
if (!releaseData) {
return ExtensionUpdateState.ERROR;
}
if (releaseData.tag_name !== releaseTag) {
return ExtensionUpdateState.UPDATE_AVAILABLE;
}
@@ -226,28 +247,57 @@ export async function checkForExtensionUpdate(
return ExtensionUpdateState.ERROR;
}
}
export interface GitHubDownloadResult {
tagName: string;
tagName?: string;
type: 'git' | 'github-release';
success: boolean;
failureReason?:
| 'failed to fetch release data'
| 'no release data'
| 'no release asset found'
| 'failed to download asset'
| 'failed to extract asset'
| 'unknown';
errorMessage?: string;
}
export async function downloadFromGitHubRelease(
installMetadata: ExtensionInstallMetadata,
destination: string,
): Promise<GitHubDownloadResult> {
const { source, ref, allowPreRelease: preRelease } = installMetadata;
const { owner, repo } = parseGitHubRepoForReleases(source);
let releaseData: GithubReleaseData | null = null;
try {
const releaseData = await fetchReleaseFromGithub(
owner,
repo,
ref,
preRelease,
);
if (!releaseData) {
throw new Error(
`No release data found for ${owner}/${repo} at tag ${ref}`,
);
const parts = parseGitHubRepoForReleases(source);
if (!parts) {
return {
failureReason: 'no release data',
success: false,
type: 'github-release',
errorMessage: `Not a github repo: ${source}`,
};
}
const { owner, repo } = parts;
try {
releaseData = await fetchReleaseFromGithub(owner, repo, ref, preRelease);
if (!releaseData) {
return {
failureReason: 'no release data',
success: false,
type: 'github-release',
errorMessage: `No release data found for ${owner}/${repo} at tag ${ref}`,
};
}
} catch (error) {
return {
failureReason: 'failed to fetch release data',
success: false,
type: 'github-release',
errorMessage: `Failed to fetch release data for ${owner}/${repo} at tag ${ref}: ${getErrorMessage(error)}`,
};
}
const asset = findReleaseAsset(releaseData.assets);
@@ -266,9 +316,13 @@ export async function downloadFromGitHubRelease(
}
}
if (!archiveUrl) {
throw new Error(
`No assets found for release with tag ${releaseData.tag_name}`,
);
return {
failureReason: 'no release asset found',
success: false,
type: 'github-release',
tagName: releaseData.tag_name,
errorMessage: `No assets found for release with tag ${releaseData.tag_name}`,
};
}
let downloadedAssetPath = path.join(
destination,
@@ -280,9 +334,29 @@ export async function downloadFromGitHubRelease(
downloadedAssetPath += '.zip';
}
await downloadFile(archiveUrl, downloadedAssetPath);
try {
await downloadFile(archiveUrl, downloadedAssetPath);
} catch (error) {
return {
failureReason: 'failed to download asset',
success: false,
type: 'github-release',
tagName: releaseData.tag_name,
errorMessage: `Failed to download asset from ${archiveUrl}: ${getErrorMessage(error)}`,
};
}
await extractFile(downloadedAssetPath, destination);
try {
await extractFile(downloadedAssetPath, destination);
} catch (error) {
return {
failureReason: 'failed to extract asset',
success: false,
type: 'github-release',
tagName: releaseData.tag_name,
errorMessage: `Failed to extract asset from ${downloadedAssetPath}: ${getErrorMessage(error)}`,
};
}
// For regular github releases, the repository is put inside of a top level
// directory. In this case we should see exactly two file in the destination
@@ -316,11 +390,16 @@ export async function downloadFromGitHubRelease(
return {
tagName: releaseData.tag_name,
type: 'github-release',
success: true,
};
} catch (error) {
throw new Error(
`Failed to download release from ${installMetadata.source}: ${getErrorMessage(error)}`,
);
return {
failureReason: 'unknown',
success: false,
type: 'github-release',
tagName: releaseData?.tag_name,
errorMessage: `Failed to download release from ${installMetadata.source}: ${getErrorMessage(error)}`,
};
}
}