Files
vscode-codeql/extensions/ql-vscode/src/distribution.ts
Andrew Eisenberg 540124478b Refactor: Move commandRunner to its own module
Also, extract related functions and types. There are no behavioral
changes in this commit. Only refactorings.
2021-01-19 12:51:12 -08:00

829 lines
28 KiB
TypeScript

import * as fetch from 'node-fetch';
import * as fs from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import * as semver from 'semver';
import * as unzipper from 'unzipper';
import * as url from 'url';
import { ExtensionContext, Event } from 'vscode';
import { DistributionConfig } from './config';
import {
InvocationRateLimiter,
InvocationRateLimiterResultKind,
showAndLogErrorMessage,
showAndLogWarningMessage
} from './helpers';
import { logger } from './logging';
import { getCodeQlCliVersion } from './cli-version';
import { ProgressCallback, reportStreamProgress } from './commandRunner';
/**
* distribution.ts
* ------------
*
* Management of CodeQL CLI binaries.
*/
/**
* Default value for the owner name of the extension-managed distribution on GitHub.
*
* We set the default here rather than as a default config value so that this default is invoked
* upon blanking the setting.
*/
const DEFAULT_DISTRIBUTION_OWNER_NAME = 'github';
/**
* Default value for the repository name of the extension-managed distribution on GitHub.
*
* We set the default here rather than as a default config value so that this default is invoked
* upon blanking the setting.
*/
const DEFAULT_DISTRIBUTION_REPOSITORY_NAME = 'codeql-cli-binaries';
/**
* Range of versions of the CLI that are compatible with the extension.
*
* This applies to both extension-managed and CLI distributions.
*/
export const DEFAULT_DISTRIBUTION_VERSION_RANGE: semver.Range = new semver.Range('2.x');
export interface DistributionProvider {
getCodeQlPathWithoutVersionCheck(): Promise<string | undefined>;
onDidChangeDistribution?: Event<void>;
getDistribution(): Promise<FindDistributionResult>;
}
export class DistributionManager implements DistributionProvider {
/**
* Get the name of the codeql cli installation we prefer to install, based on our current platform.
*/
public static getRequiredAssetName(): string {
switch (os.platform()) {
case 'linux':
return 'codeql-linux64.zip';
case 'darwin':
return 'codeql-osx64.zip';
case 'win32':
return 'codeql-win64.zip';
default:
return 'codeql.zip';
}
}
constructor(
public readonly config: DistributionConfig,
private readonly versionRange: semver.Range,
extensionContext: ExtensionContext
) {
this._onDidChangeDistribution = config.onDidChangeConfiguration;
this.extensionSpecificDistributionManager =
new ExtensionSpecificDistributionManager(config, versionRange, extensionContext);
this.updateCheckRateLimiter = new InvocationRateLimiter(
extensionContext,
'extensionSpecificDistributionUpdateCheck',
() => this.extensionSpecificDistributionManager.checkForUpdatesToDistribution()
);
}
/**
* Look up a CodeQL launcher binary.
*/
public async getDistribution(): Promise<FindDistributionResult> {
const distribution = await this.getDistributionWithoutVersionCheck();
if (distribution === undefined) {
return {
kind: FindDistributionResultKind.NoDistribution,
};
}
const version = await getCodeQlCliVersion(distribution.codeQlPath, logger);
if (version === undefined) {
return {
distribution,
kind: FindDistributionResultKind.UnknownCompatibilityDistribution,
};
}
/**
* Specifies whether prerelease versions of the CodeQL CLI should be accepted.
*
* Suppose a user sets the includePrerelease config option, obtains a prerelease, then decides
* they no longer want a prerelease, so unsets the includePrerelease config option.
* Unsetting the includePrerelease config option should trigger an update check, and this
* update check should present them an update that returns them back to a non-prerelease
* version.
*
* Therefore, we adopt the following:
*
* - If the user is managing their own CLI, they can use a prerelease without specifying the
* includePrerelease option.
* - If the user is using an extension-managed CLI, then prereleases are only accepted when the
* includePrerelease config option is set.
*/
const includePrerelease = distribution.kind !== DistributionKind.ExtensionManaged || this.config.includePrerelease;
if (!semver.satisfies(version, this.versionRange, { includePrerelease })) {
return {
distribution,
kind: FindDistributionResultKind.IncompatibleDistribution,
version,
};
}
return {
distribution,
kind: FindDistributionResultKind.CompatibleDistribution,
version
};
}
public async hasDistribution(): Promise<boolean> {
const result = await this.getDistribution();
return result.kind !== FindDistributionResultKind.NoDistribution;
}
public async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
const distribution = await this.getDistributionWithoutVersionCheck();
return distribution?.codeQlPath;
}
/**
* Returns the path to a possibly-compatible CodeQL launcher binary, or undefined if a binary not be found.
*/
async getDistributionWithoutVersionCheck(): Promise<Distribution | undefined> {
// Check config setting, then extension specific distribution, then PATH.
if (this.config.customCodeQlPath) {
if (!await fs.pathExists(this.config.customCodeQlPath)) {
showAndLogErrorMessage(`The CodeQL executable path is specified as "${this.config.customCodeQlPath}" ` +
'by a configuration setting, but a CodeQL executable could not be found at that path. Please check ' +
'that a CodeQL executable exists at the specified path or remove the setting.');
return undefined;
}
// emit a warning if using a deprecated launcher and a non-deprecated launcher exists
if (
deprecatedCodeQlLauncherName() &&
this.config.customCodeQlPath.endsWith(deprecatedCodeQlLauncherName()!) &&
await this.hasNewLauncherName()
) {
warnDeprecatedLauncher();
}
return {
codeQlPath: this.config.customCodeQlPath,
kind: DistributionKind.CustomPathConfig
};
}
const extensionSpecificCodeQlPath = await this.extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
if (extensionSpecificCodeQlPath !== undefined) {
return {
codeQlPath: extensionSpecificCodeQlPath,
kind: DistributionKind.ExtensionManaged
};
}
if (process.env.PATH) {
for (const searchDirectory of process.env.PATH.split(path.delimiter)) {
const expectedLauncherPath = await getExecutableFromDirectory(searchDirectory);
if (expectedLauncherPath) {
return {
codeQlPath: expectedLauncherPath,
kind: DistributionKind.PathEnvironmentVariable
};
}
}
logger.log('INFO: Could not find CodeQL on path.');
}
return undefined;
}
/**
* Check for updates to the extension-managed distribution. If one has not already been installed,
* this will return an update available result with the latest available release.
*
* Returns a failed promise if an unexpected error occurs during installation.
*/
public async checkForUpdatesToExtensionManagedDistribution(
minSecondsSinceLastUpdateCheck: number): Promise<DistributionUpdateCheckResult> {
const distribution = await this.getDistributionWithoutVersionCheck();
const extensionManagedCodeQlPath = await this.extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
if (distribution?.codeQlPath !== extensionManagedCodeQlPath) {
// A distribution is present but it isn't managed by the extension.
return createInvalidLocationResult();
}
const updateCheckResult = await this.updateCheckRateLimiter.invokeFunctionIfIntervalElapsed(minSecondsSinceLastUpdateCheck);
switch (updateCheckResult.kind) {
case InvocationRateLimiterResultKind.Invoked:
return updateCheckResult.result;
case InvocationRateLimiterResultKind.RateLimited:
return createAlreadyCheckedRecentlyResult();
}
}
/**
* Installs a release of the extension-managed distribution.
*
* Returns a failed promise if an unexpected error occurs during installation.
*/
public installExtensionManagedDistributionRelease(release: Release,
progressCallback?: ProgressCallback): Promise<void> {
return this.extensionSpecificDistributionManager!.installDistributionRelease(release, progressCallback);
}
public get onDidChangeDistribution(): Event<void> | undefined {
return this._onDidChangeDistribution;
}
/**
* @return true if the non-deprecated launcher name exists on the file system
* in the same directory as the specified launcher only if using an external
* installation. False otherwise.
*/
private async hasNewLauncherName(): Promise<boolean> {
if (!this.config.customCodeQlPath) {
// not managed externally
return false;
}
const dir = path.dirname(this.config.customCodeQlPath);
const newLaunderPath = path.join(dir, codeQlLauncherName());
return await fs.pathExists(newLaunderPath);
}
private readonly extensionSpecificDistributionManager: ExtensionSpecificDistributionManager;
private readonly updateCheckRateLimiter: InvocationRateLimiter<DistributionUpdateCheckResult>;
private readonly _onDidChangeDistribution: Event<void> | undefined;
}
class ExtensionSpecificDistributionManager {
constructor(
private readonly config: DistributionConfig,
private readonly versionRange: semver.Range,
private readonly extensionContext: ExtensionContext
) {
/**/
}
public async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
if (this.getInstalledRelease() !== undefined) {
// An extension specific distribution has been installed.
const expectedLauncherPath = await getExecutableFromDirectory(this.getDistributionRootPath(), true);
if (expectedLauncherPath) {
return expectedLauncherPath;
}
try {
await this.removeDistribution();
} catch (e) {
logger.log('WARNING: Tried to remove corrupted CodeQL CLI at ' +
`${this.getDistributionStoragePath()} but encountered an error: ${e}.`);
}
}
return undefined;
}
/**
* Check for updates to the extension-managed distribution. If one has not already been installed,
* this will return an update available result with the latest available release.
*
* Returns a failed promise if an unexpected error occurs during installation.
*/
public async checkForUpdatesToDistribution(): Promise<DistributionUpdateCheckResult> {
const codeQlPath = await this.getCodeQlPathWithoutVersionCheck();
const extensionSpecificRelease = this.getInstalledRelease();
const latestRelease = await this.getLatestRelease();
if (
extensionSpecificRelease !== undefined &&
codeQlPath !== undefined &&
latestRelease.id === extensionSpecificRelease.id
) {
return createAlreadyUpToDateResult();
}
return createUpdateAvailableResult(latestRelease);
}
/**
* Installs a release of the extension-managed distribution.
*
* Returns a failed promise if an unexpected error occurs during installation.
*/
public async installDistributionRelease(release: Release,
progressCallback?: ProgressCallback): Promise<void> {
await this.downloadDistribution(release, progressCallback);
// Store the installed release within the global extension state.
this.storeInstalledRelease(release);
}
private async downloadDistribution(release: Release,
progressCallback?: ProgressCallback): Promise<void> {
try {
await this.removeDistribution();
} catch (e) {
logger.log(`Tried to clean up old version of CLI at ${this.getDistributionStoragePath()} ` +
`but encountered an error: ${e}.`);
}
// Filter assets to the unique one that we require.
const requiredAssetName = DistributionManager.getRequiredAssetName();
const assets = release.assets.filter(asset => asset.name === requiredAssetName);
if (assets.length === 0) {
throw new Error(`Invariant violation: chose a release to install that didn't have ${requiredAssetName}`);
}
if (assets.length > 1) {
logger.log('WARNING: chose a release with more than one asset to install, found ' +
assets.map(asset => asset.name).join(', '));
}
const assetStream = await this.createReleasesApiConsumer().streamBinaryContentOfAsset(assets[0]);
const tmpDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-codeql'));
try {
const archivePath = path.join(tmpDirectory, 'distributionDownload.zip');
const archiveFile = fs.createWriteStream(archivePath);
const contentLength = assetStream.headers.get('content-length');
const totalNumBytes = contentLength ? parseInt(contentLength, 10) : undefined;
reportStreamProgress(assetStream.body, 'Downloading CodeQL CLI…', totalNumBytes, progressCallback);
await new Promise((resolve, reject) =>
assetStream.body.pipe(archiveFile)
.on('finish', resolve)
.on('error', reject)
);
await this.bumpDistributionFolderIndex();
logger.log(`Extracting CodeQL CLI to ${this.getDistributionStoragePath()}`);
await extractZipArchive(archivePath, this.getDistributionStoragePath());
} finally {
await fs.remove(tmpDirectory);
}
}
/**
* Remove the extension-managed distribution.
*
* This should not be called for a distribution that is currently in use, as remove may fail.
*/
private async removeDistribution(): Promise<void> {
this.storeInstalledRelease(undefined);
if (await fs.pathExists(this.getDistributionStoragePath())) {
await fs.remove(this.getDistributionStoragePath());
}
}
private async getLatestRelease(): Promise<Release> {
const requiredAssetName = DistributionManager.getRequiredAssetName();
logger.log(`Searching for latest release including ${requiredAssetName}.`);
return this.createReleasesApiConsumer().getLatestRelease(
this.versionRange,
this.config.includePrerelease,
release => {
const matchingAssets = release.assets.filter(asset => asset.name === requiredAssetName);
if (matchingAssets.length === 0) {
// For example, this could be a release with no platform-specific assets.
logger.log(`INFO: Ignoring a release with no assets named ${requiredAssetName}`);
return false;
}
if (matchingAssets.length > 1) {
logger.log(`WARNING: Ignoring a release with more than one asset named ${requiredAssetName}`);
return false;
}
return true;
}
);
}
private createReleasesApiConsumer(): ReleasesApiConsumer {
const ownerName = this.config.ownerName ? this.config.ownerName : DEFAULT_DISTRIBUTION_OWNER_NAME;
const repositoryName = this.config.repositoryName ? this.config.repositoryName : DEFAULT_DISTRIBUTION_REPOSITORY_NAME;
return new ReleasesApiConsumer(ownerName, repositoryName, this.config.personalAccessToken);
}
private async bumpDistributionFolderIndex(): Promise<void> {
const index = this.extensionContext.globalState.get(
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, 0);
await this.extensionContext.globalState.update(
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, index + 1);
}
private getDistributionStoragePath(): string {
// Use an empty string for the initial distribution for backwards compatibility.
const distributionFolderIndex = this.extensionContext.globalState.get(
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, 0) || '';
return path.join(this.extensionContext.globalStoragePath,
ExtensionSpecificDistributionManager._currentDistributionFolderBaseName + distributionFolderIndex);
}
private getDistributionRootPath(): string {
return path.join(this.getDistributionStoragePath(),
ExtensionSpecificDistributionManager._codeQlExtractedFolderName);
}
private getInstalledRelease(): Release | undefined {
return this.extensionContext.globalState.get(ExtensionSpecificDistributionManager._installedReleaseStateKey);
}
private async storeInstalledRelease(release: Release | undefined): Promise<void> {
await this.extensionContext.globalState.update(ExtensionSpecificDistributionManager._installedReleaseStateKey, release);
}
private static readonly _currentDistributionFolderBaseName = 'distribution';
private static readonly _currentDistributionFolderIndexStateKey = 'distributionFolderIndex';
private static readonly _installedReleaseStateKey = 'distributionRelease';
private static readonly _codeQlExtractedFolderName = 'codeql';
}
export class ReleasesApiConsumer {
constructor(ownerName: string, repoName: string, personalAccessToken?: string) {
// Specify version of the GitHub API
this._defaultHeaders['accept'] = 'application/vnd.github.v3+json';
if (personalAccessToken) {
this._defaultHeaders['authorization'] = `token ${personalAccessToken}`;
}
this._ownerName = ownerName;
this._repoName = repoName;
}
public async getLatestRelease(versionRange: semver.Range, includePrerelease = false, additionalCompatibilityCheck?: (release: GithubRelease) => boolean): Promise<Release> {
const apiPath = `/repos/${this._ownerName}/${this._repoName}/releases`;
const allReleases: GithubRelease[] = await (await this.makeApiCall(apiPath)).json();
const compatibleReleases = allReleases.filter(release => {
if (release.prerelease && !includePrerelease) {
return false;
}
const version = semver.parse(release.tag_name);
if (version === null || !semver.satisfies(version, versionRange, { includePrerelease })) {
return false;
}
return !additionalCompatibilityCheck || additionalCompatibilityCheck(release);
});
// Tag names must all be parsable to semvers due to the previous filtering step.
const latestRelease = compatibleReleases.sort((a, b) => {
const versionComparison = semver.compare(semver.parse(b.tag_name)!, semver.parse(a.tag_name)!);
if (versionComparison !== 0) {
return versionComparison;
}
return b.created_at.localeCompare(a.created_at, 'en-US');
})[0];
if (latestRelease === undefined) {
throw new Error('No compatible CodeQL CLI releases were found. ' +
'Please check that the CodeQL extension is up to date.');
}
const assets: ReleaseAsset[] = latestRelease.assets.map(asset => {
return {
id: asset.id,
name: asset.name,
size: asset.size
};
});
return {
assets,
createdAt: latestRelease.created_at,
id: latestRelease.id,
name: latestRelease.name
};
}
public async streamBinaryContentOfAsset(asset: ReleaseAsset): Promise<fetch.Response> {
const apiPath = `/repos/${this._ownerName}/${this._repoName}/releases/assets/${asset.id}`;
return await this.makeApiCall(apiPath, {
'accept': 'application/octet-stream'
});
}
protected async makeApiCall(apiPath: string, additionalHeaders: { [key: string]: string } = {}): Promise<fetch.Response> {
const response = await this.makeRawRequest(ReleasesApiConsumer._apiBase + apiPath,
Object.assign({}, this._defaultHeaders, additionalHeaders));
if (!response.ok) {
// Check for rate limiting
const rateLimitResetValue = response.headers.get('X-RateLimit-Reset');
if (response.status === 403 && rateLimitResetValue) {
const secondsToMillisecondsFactor = 1000;
const rateLimitResetDate = new Date(parseInt(rateLimitResetValue, 10) * secondsToMillisecondsFactor);
throw new GithubRateLimitedError(response.status, await response.text(), rateLimitResetDate);
}
throw new GithubApiError(response.status, await response.text());
}
return response;
}
private async makeRawRequest(
requestUrl: string,
headers: { [key: string]: string },
redirectCount = 0): Promise<fetch.Response> {
const response = await fetch.default(requestUrl, {
headers,
redirect: 'manual'
});
const redirectUrl = response.headers.get('location');
if (isRedirectStatusCode(response.status) && redirectUrl && redirectCount < ReleasesApiConsumer._maxRedirects) {
const parsedRedirectUrl = url.parse(redirectUrl);
if (parsedRedirectUrl.protocol != 'https:') {
throw new Error('Encountered a non-https redirect, rejecting');
}
if (parsedRedirectUrl.host != 'api.github.com') {
// Remove authorization header if we are redirected outside of the GitHub API.
//
// This is necessary to stream release assets since AWS fails if more than one auth
// mechanism is provided.
delete headers['authorization'];
}
return await this.makeRawRequest(redirectUrl, headers, redirectCount + 1);
}
return response;
}
private readonly _defaultHeaders: { [key: string]: string } = {};
private readonly _ownerName: string;
private readonly _repoName: string;
private static readonly _apiBase = 'https://api.github.com';
private static readonly _maxRedirects = 20;
}
export async function extractZipArchive(archivePath: string, outPath: string): Promise<void> {
const archive = await unzipper.Open.file(archivePath);
await archive.extract({
concurrency: 4,
path: outPath
});
// Set file permissions for extracted files
await Promise.all(archive.files.map(async file => {
// Only change file permissions if within outPath (path.join normalises the path)
const extractedPath = path.join(outPath, file.path);
if (extractedPath.indexOf(outPath) !== 0 || !(await fs.pathExists(extractedPath))) {
return Promise.resolve();
}
return fs.chmod(extractedPath, file.externalFileAttributes >>> 16);
}));
}
export function codeQlLauncherName(): string {
return (os.platform() === 'win32') ? 'codeql.exe' : 'codeql';
}
function deprecatedCodeQlLauncherName(): string | undefined {
return (os.platform() === 'win32') ? 'codeql.cmd' : undefined;
}
function isRedirectStatusCode(statusCode: number): boolean {
return statusCode === 301 || statusCode === 302 || statusCode === 303 || statusCode === 307 || statusCode === 308;
}
/*
* Types and helper functions relating to those types.
*/
export enum DistributionKind {
CustomPathConfig,
ExtensionManaged,
PathEnvironmentVariable
}
export interface Distribution {
codeQlPath: string;
kind: DistributionKind;
}
export enum FindDistributionResultKind {
CompatibleDistribution,
UnknownCompatibilityDistribution,
IncompatibleDistribution,
NoDistribution
}
export type FindDistributionResult =
| CompatibleDistributionResult
| UnknownCompatibilityDistributionResult
| IncompatibleDistributionResult
| NoDistributionResult;
/**
* A result representing a distribution of the CodeQL CLI that may or may not be compatible with
* the extension.
*/
interface DistributionResult {
distribution: Distribution;
kind: FindDistributionResultKind;
}
interface CompatibleDistributionResult extends DistributionResult {
kind: FindDistributionResultKind.CompatibleDistribution;
version: semver.SemVer;
}
interface UnknownCompatibilityDistributionResult extends DistributionResult {
kind: FindDistributionResultKind.UnknownCompatibilityDistribution;
}
interface IncompatibleDistributionResult extends DistributionResult {
kind: FindDistributionResultKind.IncompatibleDistribution;
version: semver.SemVer;
}
interface NoDistributionResult {
kind: FindDistributionResultKind.NoDistribution;
}
export enum DistributionUpdateCheckResultKind {
AlreadyCheckedRecentlyResult,
AlreadyUpToDate,
InvalidLocation,
UpdateAvailable
}
type DistributionUpdateCheckResult =
| AlreadyCheckedRecentlyResult
| AlreadyUpToDateResult
| InvalidLocationResult
| UpdateAvailableResult;
export interface AlreadyCheckedRecentlyResult {
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult;
}
export interface AlreadyUpToDateResult {
kind: DistributionUpdateCheckResultKind.AlreadyUpToDate;
}
/**
* The distribution could not be installed or updated because it is not managed by the extension.
*/
export interface InvalidLocationResult {
kind: DistributionUpdateCheckResultKind.InvalidLocation;
}
export interface UpdateAvailableResult {
kind: DistributionUpdateCheckResultKind.UpdateAvailable;
updatedRelease: Release;
}
function createAlreadyCheckedRecentlyResult(): AlreadyCheckedRecentlyResult {
return {
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult
};
}
function createAlreadyUpToDateResult(): AlreadyUpToDateResult {
return {
kind: DistributionUpdateCheckResultKind.AlreadyUpToDate
};
}
function createInvalidLocationResult(): InvalidLocationResult {
return {
kind: DistributionUpdateCheckResultKind.InvalidLocation
};
}
function createUpdateAvailableResult(updatedRelease: Release): UpdateAvailableResult {
return {
kind: DistributionUpdateCheckResultKind.UpdateAvailable,
updatedRelease
};
}
// Exported for testing
export async function getExecutableFromDirectory(directory: string, warnWhenNotFound = false): Promise<string | undefined> {
const expectedLauncherPath = path.join(directory, codeQlLauncherName());
const deprecatedLauncherName = deprecatedCodeQlLauncherName();
const alternateExpectedLauncherPath = deprecatedLauncherName ? path.join(directory, deprecatedLauncherName) : undefined;
if (await fs.pathExists(expectedLauncherPath)) {
return expectedLauncherPath;
} else if (alternateExpectedLauncherPath && (await fs.pathExists(alternateExpectedLauncherPath))) {
warnDeprecatedLauncher();
return alternateExpectedLauncherPath;
}
if (warnWhenNotFound) {
logger.log(`WARNING: Expected to find a CodeQL CLI executable at ${expectedLauncherPath} but one was not found. ` +
'Will try PATH.');
}
return undefined;
}
function warnDeprecatedLauncher() {
showAndLogWarningMessage(
`The "${deprecatedCodeQlLauncherName()!}" launcher has been deprecated and will be removed in a future version. ` +
`Please use "${codeQlLauncherName()}" instead. It is recommended to update to the latest CodeQL binaries.`
);
}
/**
* A release on GitHub.
*/
export interface Release {
assets: ReleaseAsset[];
/**
* The creation date of the release on GitHub.
*/
createdAt: string;
/**
* The id associated with the release on GitHub.
*/
id: number;
/**
* The name associated with the release on GitHub.
*/
name: string;
}
/**
* An asset corresponding to a release on GitHub.
*/
export interface ReleaseAsset {
/**
* The id associated with the asset on GitHub.
*/
id: number;
/**
* The name associated with the asset on GitHub.
*/
name: string;
/**
* The size of the asset in bytes.
*/
size: number;
}
/**
* The json returned from github for a release.
*/
export interface GithubRelease {
assets: GithubReleaseAsset[];
/**
* The creation date of the release on GitHub, in ISO 8601 format.
*/
created_at: string;
/**
* The id associated with the release on GitHub.
*/
id: number;
/**
* The name associated with the release on GitHub.
*/
name: string;
/**
* Whether the release is a prerelease.
*/
prerelease: boolean;
/**
* The tag name. This should be the version.
*/
tag_name: string;
}
/**
* The json returned by github for an asset in a release.
*/
export interface GithubReleaseAsset {
/**
* The id associated with the asset on GitHub.
*/
id: number;
/**
* The name associated with the asset on GitHub.
*/
name: string;
/**
* The size of the asset in bytes.
*/
size: number;
}
export class GithubApiError extends Error {
constructor(public status: number, public body: string) {
super(`API call failed with status code ${status}, body: ${body}`);
}
}
export class GithubRateLimitedError extends GithubApiError {
constructor(public status: number, public body: string, public rateLimitResetDate: Date) {
super(status, body);
}
}