Skip to content

Commit 92bebef

Browse files
Merge pull request #3188 from github/robertbrignull/releases-refactor
Move ReleasesApiConsumer to a separate file and do simple refactors
2 parents 4792994 + 1ce2b36 commit 92bebef

File tree

7 files changed

+493
-534
lines changed

7 files changed

+493
-534
lines changed

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

Lines changed: 11 additions & 317 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,6 +25,8 @@ import {
2725
} from "../common/logging";
2826
import { unzipToDirectoryConcurrently } from "../common/unzip-concurrently";
2927
import { reportUnzipProgress } from "../common/vscode/unzip-progress";
28+
import { Release } from "./distribution/release";
29+
import { ReleasesApiConsumer } from "./distribution/releases-api-consumer";
3030

3131
/**
3232
* distribution.ts
@@ -36,30 +36,14 @@ import { reportUnzipProgress } from "../common/vscode/unzip-progress";
3636
*/
3737

3838
/**
39-
* Default value for the owner name of the extension-managed distribution on GitHub.
40-
*
41-
* We set the default here rather than as a default config value so that this default is invoked
42-
* upon blanking the setting.
43-
*/
44-
const DEFAULT_DISTRIBUTION_OWNER_NAME = "github";
45-
46-
/**
47-
* Default value for the repository name of the extension-managed distribution on GitHub.
48-
*
49-
* We set the default here rather than as a default config value so that this default is invoked
50-
* upon blanking the setting.
39+
* Repository name with owner of the stable version of the extension-managed distribution on GitHub.
5140
*/
52-
const DEFAULT_DISTRIBUTION_REPOSITORY_NAME = "codeql-cli-binaries";
41+
const STABLE_DISTRIBUTION_REPOSITORY_NWO = "github/codeql-cli-binaries";
5342

5443
/**
55-
* Owner name of the nightly version of the extension-managed distribution on GitHub.
44+
* Repository name with owner of the nightly version of the extension-managed distribution on GitHub.
5645
*/
57-
const NIGHTLY_DISTRIBUTION_OWNER_NAME = "dsp-testing";
58-
59-
/**
60-
* Repository name of the nightly version of the extension-managed distribution on GitHub.
61-
*/
62-
const NIGHTLY_DISTRIBUTION_REPOSITORY_NAME = "codeql-cli-nightlies";
46+
const NIGHTLY_DISTRIBUTION_REPOSITORY_NWO = "dsp-testing/codeql-cli-nightlies";
6347

6448
/**
6549
* Range of versions of the CLI that are compatible with the extension.
@@ -505,32 +489,22 @@ class ExtensionSpecificDistributionManager {
505489

506490
private createReleasesApiConsumer(): ReleasesApiConsumer {
507491
return new ReleasesApiConsumer(
508-
this.distributionOwnerName,
509-
this.distributionRepositoryName,
492+
this.distributionRepositoryNwo,
510493
this.config.personalAccessToken,
511494
);
512495
}
513496

514-
private get distributionOwnerName(): string {
497+
private get distributionRepositoryNwo(): string {
515498
if (this.config.channel === "nightly") {
516-
return NIGHTLY_DISTRIBUTION_OWNER_NAME;
499+
return NIGHTLY_DISTRIBUTION_REPOSITORY_NWO;
517500
} else {
518-
return DEFAULT_DISTRIBUTION_OWNER_NAME;
519-
}
520-
}
521-
522-
private get distributionRepositoryName(): string {
523-
if (this.config.channel === "nightly") {
524-
return NIGHTLY_DISTRIBUTION_REPOSITORY_NAME;
525-
} else {
526-
return DEFAULT_DISTRIBUTION_REPOSITORY_NAME;
501+
return STABLE_DISTRIBUTION_REPOSITORY_NWO;
527502
}
528503
}
529504

530505
private get usingNightlyReleases(): boolean {
531506
return (
532-
this.distributionOwnerName === NIGHTLY_DISTRIBUTION_OWNER_NAME &&
533-
this.distributionRepositoryName === NIGHTLY_DISTRIBUTION_REPOSITORY_NAME
507+
this.distributionRepositoryNwo === NIGHTLY_DISTRIBUTION_REPOSITORY_NWO
534508
);
535509
}
536510

@@ -588,173 +562,6 @@ class ExtensionSpecificDistributionManager {
588562
private static readonly _codeQlExtractedFolderName = "codeql";
589563
}
590564

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

0 commit comments

Comments
 (0)