Skip to content

Commit 6d1cf08

Browse files
Pull out ReleasesApiConsumer to its own file
1 parent bc7c956 commit 6d1cf08

4 files changed

Lines changed: 444 additions & 439 deletions

File tree

extensions/ql-vscode/src/codeql-cli/distribution.ts

Lines changed: 2 additions & 223 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import * as fetch from "node-fetch";
21
import { pathExists, mkdtemp, createWriteStream, remove } from "fs-extra";
32
import { tmpdir } from "os";
43
import { delimiter, dirname, join } from "path";
54
import * as semver from "semver";
6-
import { URL } from "url";
75
import { ExtensionContext, Event } from "vscode";
86
import { DistributionConfig } from "../config";
97
import { extLogger } from "../common/logging/vscode";
@@ -27,8 +25,8 @@ import {
2725
} from "../common/logging";
2826
import { unzipToDirectoryConcurrently } from "../common/unzip-concurrently";
2927
import { reportUnzipProgress } from "../common/vscode/unzip-progress";
30-
import { Release, ReleaseAsset } from "./release";
31-
import { GithubRateLimitedError, GithubApiError } from "./github-api-error";
28+
import { Release } from "./release";
29+
import { ReleasesApiConsumer } from "./releases-api-consumer";
3230

3331
/**
3432
* distribution.ts
@@ -590,173 +588,6 @@ class ExtensionSpecificDistributionManager {
590588
private static readonly _codeQlExtractedFolderName = "codeql";
591589
}
592590

593-
export class ReleasesApiConsumer {
594-
constructor(
595-
ownerName: string,
596-
repoName: string,
597-
personalAccessToken?: string,
598-
) {
599-
// Specify version of the GitHub API
600-
this._defaultHeaders["accept"] = "application/vnd.github.v3+json";
601-
602-
if (personalAccessToken) {
603-
this._defaultHeaders["authorization"] = `token ${personalAccessToken}`;
604-
}
605-
606-
this._ownerName = ownerName;
607-
this._repoName = repoName;
608-
}
609-
610-
public async getLatestRelease(
611-
versionRange: semver.Range | undefined,
612-
orderBySemver = true,
613-
includePrerelease = false,
614-
additionalCompatibilityCheck?: (release: GithubRelease) => boolean,
615-
): Promise<Release> {
616-
const apiPath = `/repos/${this._ownerName}/${this._repoName}/releases`;
617-
const allReleases: GithubRelease[] = await (
618-
await this.makeApiCall(apiPath)
619-
).json();
620-
const compatibleReleases = allReleases.filter((release) => {
621-
if (release.prerelease && !includePrerelease) {
622-
return false;
623-
}
624-
625-
if (versionRange !== undefined) {
626-
const version = semver.parse(release.tag_name);
627-
if (
628-
version === null ||
629-
!semver.satisfies(version, versionRange, { includePrerelease })
630-
) {
631-
return false;
632-
}
633-
}
634-
635-
return (
636-
!additionalCompatibilityCheck || additionalCompatibilityCheck(release)
637-
);
638-
});
639-
// Tag names must all be parsable to semvers due to the previous filtering step.
640-
const latestRelease = compatibleReleases.sort((a, b) => {
641-
const versionComparison = orderBySemver
642-
? semver.compare(semver.parse(b.tag_name)!, semver.parse(a.tag_name)!)
643-
: b.id - a.id;
644-
if (versionComparison !== 0) {
645-
return versionComparison;
646-
}
647-
return b.created_at.localeCompare(a.created_at, "en-US");
648-
})[0];
649-
if (latestRelease === undefined) {
650-
throw new Error(
651-
"No compatible CodeQL CLI releases were found. " +
652-
"Please check that the CodeQL extension is up to date.",
653-
);
654-
}
655-
const assets: ReleaseAsset[] = latestRelease.assets.map((asset) => {
656-
return {
657-
id: asset.id,
658-
name: asset.name,
659-
size: asset.size,
660-
};
661-
});
662-
663-
return {
664-
assets,
665-
createdAt: latestRelease.created_at,
666-
id: latestRelease.id,
667-
name: latestRelease.name,
668-
};
669-
}
670-
671-
public async streamBinaryContentOfAsset(
672-
asset: ReleaseAsset,
673-
): Promise<fetch.Response> {
674-
const apiPath = `/repos/${this._ownerName}/${this._repoName}/releases/assets/${asset.id}`;
675-
676-
return await this.makeApiCall(apiPath, {
677-
accept: "application/octet-stream",
678-
});
679-
}
680-
681-
protected async makeApiCall(
682-
apiPath: string,
683-
additionalHeaders: { [key: string]: string } = {},
684-
): Promise<fetch.Response> {
685-
const response = await this.makeRawRequest(
686-
ReleasesApiConsumer._apiBase + apiPath,
687-
Object.assign({}, this._defaultHeaders, additionalHeaders),
688-
);
689-
690-
if (!response.ok) {
691-
// Check for rate limiting
692-
const rateLimitResetValue = response.headers.get("X-RateLimit-Reset");
693-
if (response.status === 403 && rateLimitResetValue) {
694-
const secondsToMillisecondsFactor = 1000;
695-
const rateLimitResetDate = new Date(
696-
parseInt(rateLimitResetValue, 10) * secondsToMillisecondsFactor,
697-
);
698-
throw new GithubRateLimitedError(
699-
response.status,
700-
await response.text(),
701-
rateLimitResetDate,
702-
);
703-
}
704-
throw new GithubApiError(response.status, await response.text());
705-
}
706-
return response;
707-
}
708-
709-
private async makeRawRequest(
710-
requestUrl: string,
711-
headers: { [key: string]: string },
712-
redirectCount = 0,
713-
): Promise<fetch.Response> {
714-
const response = await fetch.default(requestUrl, {
715-
headers,
716-
redirect: "manual",
717-
});
718-
719-
const redirectUrl = response.headers.get("location");
720-
if (
721-
isRedirectStatusCode(response.status) &&
722-
redirectUrl &&
723-
redirectCount < ReleasesApiConsumer._maxRedirects
724-
) {
725-
const parsedRedirectUrl = new URL(redirectUrl);
726-
if (parsedRedirectUrl.protocol !== "https:") {
727-
throw new Error("Encountered a non-https redirect, rejecting");
728-
}
729-
if (parsedRedirectUrl.host !== "api.github.com") {
730-
// Remove authorization header if we are redirected outside of the GitHub API.
731-
//
732-
// This is necessary to stream release assets since AWS fails if more than one auth
733-
// mechanism is provided.
734-
delete headers["authorization"];
735-
}
736-
return await this.makeRawRequest(redirectUrl, headers, redirectCount + 1);
737-
}
738-
739-
return response;
740-
}
741-
742-
private readonly _defaultHeaders: { [key: string]: string } = {};
743-
private readonly _ownerName: string;
744-
private readonly _repoName: string;
745-
746-
private static readonly _apiBase = "https://api.github.com";
747-
private static readonly _maxRedirects = 20;
748-
}
749-
750-
function isRedirectStatusCode(statusCode: number): boolean {
751-
return (
752-
statusCode === 301 ||
753-
statusCode === 302 ||
754-
statusCode === 303 ||
755-
statusCode === 307 ||
756-
statusCode === 308
757-
);
758-
}
759-
760591
/*
761592
* Types and helper functions relating to those types.
762593
*/
@@ -907,55 +738,3 @@ function warnDeprecatedLauncher() {
907738
`Please use "${codeQlLauncherName()}" instead. It is recommended to update to the latest CodeQL binaries.`,
908739
);
909740
}
910-
911-
/**
912-
* The json returned from github for a release.
913-
*/
914-
export interface GithubRelease {
915-
assets: GithubReleaseAsset[];
916-
917-
/**
918-
* The creation date of the release on GitHub, in ISO 8601 format.
919-
*/
920-
created_at: string;
921-
922-
/**
923-
* The id associated with the release on GitHub.
924-
*/
925-
id: number;
926-
927-
/**
928-
* The name associated with the release on GitHub.
929-
*/
930-
name: string;
931-
932-
/**
933-
* Whether the release is a prerelease.
934-
*/
935-
prerelease: boolean;
936-
937-
/**
938-
* The tag name. This should be the version.
939-
*/
940-
tag_name: string;
941-
}
942-
943-
/**
944-
* The json returned by github for an asset in a release.
945-
*/
946-
export interface GithubReleaseAsset {
947-
/**
948-
* The id associated with the asset on GitHub.
949-
*/
950-
id: number;
951-
952-
/**
953-
* The name associated with the asset on GitHub.
954-
*/
955-
name: string;
956-
957-
/**
958-
* The size of the asset in bytes.
959-
*/
960-
size: number;
961-
}

0 commit comments

Comments
 (0)