1- import * as fetch from "node-fetch" ;
21import { pathExists , mkdtemp , createWriteStream , remove } from "fs-extra" ;
32import { tmpdir } from "os" ;
43import { delimiter , dirname , join } from "path" ;
54import * as semver from "semver" ;
6- import { URL } from "url" ;
75import { ExtensionContext , Event } from "vscode" ;
86import { DistributionConfig } from "../config" ;
97import { extLogger } from "../common/logging/vscode" ;
@@ -27,8 +25,8 @@ import {
2725} from "../common/logging" ;
2826import { unzipToDirectoryConcurrently } from "../common/unzip-concurrently" ;
2927import { 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