@@ -10,7 +10,117 @@ const semver = require('semver');
1010const fileName = getCliExecutableName ( ) ;
1111const jfrogCliToolName = 'jf' ;
1212const cliPackage = 'jfrog-cli-' + getArchitecture ( ) ;
13- const defaultJfrogCliVersion = '2.85.0' ;
13+ const fallbackCliVersion = '2.89.0' ;
14+ let defaultJfrogCliVersion = null ;
15+
16+ /**
17+ * Executes an HTTP request with retry logic for 5xx errors.
18+ * @param {string } method - HTTP method (GET, POST, etc.)
19+ * @param {string } url - Request URL
20+ * @param {object } options - Request options (timeout, headers, etc.)
21+ * @param {number } maxRetries - Maximum number of retry attempts (default: 3)
22+ * @param {number } retryDelay - Delay between retries in ms (default: 1000)
23+ * @returns {object } Response object from syncRequest on success
24+ * @throws {Error } If all retries fail (network errors or 5xx responses)
25+ */
26+ function syncRequestWithRetry ( method , url , options = { } , maxRetries = 3 , retryDelay = 1000 ) {
27+ let errorToThrow = null ;
28+ let lastResponse = null ;
29+
30+ for ( let attempt = 1 ; attempt <= maxRetries ; attempt ++ ) {
31+ try {
32+ const response = syncRequest ( method , url , options ) ;
33+ // Retry on 5xx server errors
34+ if ( response ?. statusCode >= 500 && response ?. statusCode < 600 ) {
35+ console . warn ( `Attempt ${ attempt } /${ maxRetries } : Server error ${ response ?. statusCode } for ${ url } ` ) ;
36+ lastResponse = response ;
37+ } else {
38+ return response ;
39+ }
40+ } catch ( err ) {
41+ console . warn ( `Attempt ${ attempt } /${ maxRetries } : Request failed for ${ url } - ${ err ?. message } ` ) ;
42+ errorToThrow = err ;
43+ }
44+
45+ if ( attempt < maxRetries ) {
46+ // Blocking delay before retry
47+ const waitUntil = Date . now ( ) + retryDelay ;
48+ while ( Date . now ( ) < waitUntil ) { /* wait */ }
49+ }
50+ }
51+
52+ // All retries exhausted - throw error for both network failures and 5xx responses
53+ if ( errorToThrow ) {
54+ throw errorToThrow ;
55+ }
56+ if ( lastResponse ) {
57+ throw new Error ( `Server error ${ lastResponse . statusCode } after ${ maxRetries } retries for ${ url } ` ) ;
58+ }
59+ }
60+
61+ /**
62+ * Checks if the CLI binary exists on releases.jfrog.io for the given version.
63+ * @param {string } version - The CLI version to check
64+ * @returns {boolean } True if binary exists, false otherwise
65+ */
66+ function isCliBinaryAvailable ( version ) {
67+ const binaryUrl = `https://releases.jfrog.io/artifactory/jfrog-cli/v2-jf/${ version } /${ cliPackage } /${ fileName } ` ;
68+ try {
69+ console . log ( 'Verifying CLI binary availability at: ' + binaryUrl ) ;
70+ const res = syncRequestWithRetry ( 'HEAD' , binaryUrl , { timeout : 5000 } ) ;
71+ return res ?. statusCode === 200 ;
72+ } catch ( err ) {
73+ console . warn ( 'Failed to verify CLI binary availability: ' + err ?. message ) ;
74+ return false ;
75+ }
76+ }
77+
78+ /**
79+ * Fetches the latest available JFrog CLI version from GitHub releases with retry mechanism.
80+ * Validates that the binary is available on releases.jfrog.io before returning.
81+ * If the latest release binary isn't available, falls back to the previous release.
82+ * Called once during module initialization. Result is cached in defaultJfrogCliVersion.
83+ * @returns {string } The CLI version (e.g., '2.89.0') or fallback if fetch fails or no binary available
84+ */
85+ function fetchLatestCliVersion ( ) {
86+ try {
87+ console . log ( 'Fetching JFrog CLI releases from https://api.github.com/repos/jfrog/jfrog-cli/releases' ) ;
88+ const res = syncRequestWithRetry ( 'GET' , 'https://api.github.com/repos/jfrog/jfrog-cli/releases?per_page=3' , {
89+ headers : { 'User-Agent' : 'jfrog-azure-devops-extension' } ,
90+ timeout : 5000 ,
91+ } ) ;
92+ if ( res . statusCode === 200 ) {
93+ const releases = JSON . parse ( res . getBody ( 'utf8' ) ) ;
94+ console . log ( 'Fetched ' + releases ?. length ?? 0 + ' JFrog CLI releases' ) ;
95+
96+ if ( ! releases || releases . length === 0 ) {
97+ console . warn ( 'No JFrog CLI releases found, using fallback: ' + fallbackCliVersion ) ;
98+ return fallbackCliVersion ;
99+ }
100+
101+ // Try each release until we find one with an available binary
102+ for ( const release of releases ) {
103+ const version = release ?. name ;
104+ console . log ( 'Checking CLI version: ' + version ) ;
105+
106+ if ( version && isCliBinaryAvailable ( version ) ) {
107+ console . log ( 'CLI binary verified available for version: ' + version ) ;
108+ return version ;
109+ }
110+ console . warn ( 'CLI binary not yet available for version: ' + version ) ;
111+ }
112+ console . warn ( 'No CLI binaries available for last 3 releases, using fallback: ' + fallbackCliVersion ) ;
113+ return fallbackCliVersion ;
114+ }
115+ console . warn ( 'Unexpected status code: ' + res . statusCode + ', using fallback version: ' + fallbackCliVersion ) ;
116+ } catch ( err ) {
117+ console . warn ( 'Failed to fetch JFrog CLI releases, due to error: ' + err ?. message + ', using fallback: ' + fallbackCliVersion ) ;
118+ }
119+ return fallbackCliVersion ;
120+ }
121+
122+ // Fetch and cache the CLI version during module initialization
123+ defaultJfrogCliVersion = fetchLatestCliVersion ( ) ;
14124
15125/**
16126 * Safely constructs the JFrog tools directory path, handling potential issues with Agent.ToolsDirectory
@@ -69,6 +179,9 @@ const jfrogCliConfigUseCommand = 'c use';
69179let runTaskCbk = null ;
70180
71181module . exports = {
182+ syncRequestWithRetry : syncRequestWithRetry ,
183+ isCliBinaryAvailable : isCliBinaryAvailable ,
184+ fetchLatestCliVersion : fetchLatestCliVersion ,
72185 executeCliTask : executeCliTask ,
73186 executeCliCommand : executeCliCommand ,
74187 downloadCli : downloadCli ,
@@ -112,6 +225,7 @@ module.exports = {
112225 configureDefaultXrayServer : configureDefaultXrayServer ,
113226 minCustomCliVersion : minCustomCliVersion ,
114227 defaultJfrogCliVersion : defaultJfrogCliVersion ,
228+ fallbackCliVersion : fallbackCliVersion ,
115229 pipelineRequestedCliVersionEnv : pipelineRequestedCliVersionEnv ,
116230 taskSelectedCliVersionEnv : taskSelectedCliVersionEnv ,
117231 extractorsRemoteEnv : extractorsRemoteEnv ,
0 commit comments