diff --git a/.github/workflows/auto-build-publish-test.yml b/.github/workflows/auto-build-publish-test.yml index 9753a4cd9..2fd6ea549 100644 --- a/.github/workflows/auto-build-publish-test.yml +++ b/.github/workflows/auto-build-publish-test.yml @@ -13,11 +13,13 @@ concurrency: jobs: Auto-Build-Publish-Test: + name: Auto-Build-Publish-Test (${{ matrix.os }}) - (CLI ${{ matrix.cli-version }}) if: contains(github.event.pull_request.labels.*.name, 'safe to test') || github.event_name == 'push' strategy: fail-fast: false matrix: os: [ ubuntu, windows, macos ] + cli-version: [ "latest", "2.66.0" ] runs-on: ${{ matrix.os }}-latest steps: - name: Checkout Repository @@ -51,6 +53,8 @@ jobs: - name: Setup JFrog CLI id: setup-jfrog-cli uses: ./ + with: + version: ${{ matrix.cli-version }} env: JF_URL: http://localhost:8081/ JF_USER: admin diff --git a/lib/cleanup.js b/lib/cleanup.js index 18caf506c..5c64c53e0 100644 --- a/lib/cleanup.js +++ b/lib/cleanup.js @@ -36,31 +36,12 @@ const core = __importStar(require("@actions/core")); const utils_1 = require("./utils"); function cleanup() { return __awaiter(this, void 0, void 0, function* () { - if (!addCachedJfToPath()) { - core.error('Could not find JFrog CLI path in the step state. Skipping cleanup.'); + if (!utils_1.Utils.loadFromCache(core.getInput(utils_1.Utils.CLI_VERSION_ARG))) { + core.warning('Could not find JFrog CLI executable. Skipping cleanup.'); return; } - // Auto-publish build info if needed - try { - if (!core.getBooleanInput(utils_1.Utils.AUTO_BUILD_PUBLISH_DISABLE)) { - yield collectAndPublishBuildInfoIfNeeded(); - } - } - catch (error) { - core.warning('failed while attempting to publish build info: ' + error); - } - // Generate job summary - try { - if (!core.getBooleanInput(utils_1.Utils.JOB_SUMMARY_DISABLE)) { - core.startGroup('Generating Job Summary'); - yield utils_1.Utils.runCli(['generate-summary-markdown']); - yield utils_1.Utils.setMarkdownAsJobSummary(); - core.endGroup(); - } - } - catch (error) { - core.warning('failed while attempting to generate job summary: ' + error); - } + // Run post tasks related to Build Info (auto build publish, job summary) + yield buildInfoPostTasks(); // Cleanup JFrog CLI servers configuration try { core.startGroup('Cleanup JFrog CLI servers configuration'); @@ -74,36 +55,65 @@ function cleanup() { } }); } -function addCachedJfToPath() { - // Get the JFrog CLI path from step state. saveState/getState are methods to pass data between a step, and it's cleanup function. - const jfCliPath = core.getState(utils_1.Utils.JF_CLI_PATH_STATE); - if (!jfCliPath) { - // This means that the JFrog CLI was not installed in the first place, because there was a failure in the installation step. - return false; - } - core.addPath(jfCliPath); - return true; +/** + * Executes post tasks related to build information. + * + * This function performs several tasks after the main build process: + * 1. Checks if auto build publish and job summary are disabled. + * 2. Verifies connection to JFrog Artifactory. + * 3. Collects and publishes build information if needed. + * 4. Generates a job summary if required. + */ +function buildInfoPostTasks() { + return __awaiter(this, void 0, void 0, function* () { + const disableAutoBuildPublish = core.getBooleanInput(utils_1.Utils.AUTO_BUILD_PUBLISH_DISABLE); + const disableJobSummary = core.getBooleanInput(utils_1.Utils.JOB_SUMMARY_DISABLE) || !utils_1.Utils.isJobSummarySupported(); + if (disableAutoBuildPublish && disableJobSummary) { + core.info(`Both auto-build-publish and job-summary are disabled. Skipping Build Info post tasks.`); + return; + } + // Check connection to Artifactory before proceeding with build info post tasks + if (!(yield checkConnectionToArtifactory())) { + return; + } + // Auto-publish build info if needed + if (!disableAutoBuildPublish) { + yield collectAndPublishBuildInfoIfNeeded(); + } + else { + core.info('Auto build info publish is disabled. Skipping auto build info collection and publishing'); + } + // Generate job summary if not disabled and the JFrog CLI version supports it + if (!disableJobSummary) { + yield generateJobSummary(); + } + else { + core.info('Job summary is disabled. Skipping job summary generation'); + } + }); } function hasUnpublishedModules(workingDirectory) { return __awaiter(this, void 0, void 0, function* () { // Save the old value of the environment variable to revert it later const origValue = process.env[utils_1.Utils.JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR_ENV]; try { + core.startGroup('Check for unpublished modules'); // Avoid saving a command summary for this dry-run command core.exportVariable(utils_1.Utils.JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR_ENV, ''); // Running build-publish command with a dry-run flag to check if there are any unpublished modules, 'silent' to avoid polluting the logs - const responseStr = yield utils_1.Utils.runCliAndGetOutput(['rt', 'build-publish', '--dry-run'], { silent: true, cwd: workingDirectory }); + const responseStr = yield utils_1.Utils.runCliAndGetOutput(['rt', 'build-publish', '--dry-run'], { cwd: workingDirectory }); // Parse the JSON string to an object const response = JSON.parse(responseStr); // Check if the "modules" key exists and if it's an array with more than one item return response.modules != undefined && Array.isArray(response.modules) && response.modules.length > 0; } catch (error) { - core.error('Failed to parse JSON: ' + error); - return false; // Return false if parsing fails + core.warning('Failed to check if there are any unpublished modules: ' + error); + return false; } finally { core.exportVariable(utils_1.Utils.JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR_ENV, origValue); + core.endGroup(); } }); } @@ -121,14 +131,19 @@ function collectAndPublishBuildInfoIfNeeded() { yield utils_1.Utils.runCli(['rt', 'build-add-git'], { cwd: workingDirectory }); } catch (error) { - core.warning('failed while attempting to collect Git information: ' + error); + core.warning('Failed while attempting to collect Git information: ' + error); + } + finally { + core.endGroup(); + } + // Publish the build info to Artifactory + try { + core.startGroup('Publish the build info to JFrog Artifactory'); + yield utils_1.Utils.runCli(['rt', 'build-publish'], { cwd: workingDirectory }); } finally { core.endGroup(); } - core.startGroup('Publish the build info to JFrog Artifactory'); - yield utils_1.Utils.runCli(['rt', 'build-publish'], { cwd: workingDirectory }); - core.endGroup(); }); } function getWorkingDirectory() { @@ -138,4 +153,40 @@ function getWorkingDirectory() { } return workingDirectory; } +function checkConnectionToArtifactory() { + return __awaiter(this, void 0, void 0, function* () { + try { + core.startGroup('Checking connection to JFrog Artifactory'); + const pingResult = yield utils_1.Utils.runCliAndGetOutput(['rt', 'ping']); + if (pingResult !== 'OK') { + core.debug(`Ping result: ${pingResult}`); + core.warning('Could not connect to Artifactory. Skipping Build Info post tasks.'); + return false; + } + return true; + } + catch (error) { + core.warning(`An error occurred while trying to connect to Artifactory: ${error}. Skipping Build Info post tasks.`); + return false; + } + finally { + core.endGroup(); + } + }); +} +function generateJobSummary() { + return __awaiter(this, void 0, void 0, function* () { + try { + core.startGroup('Generating Job Summary'); + yield utils_1.Utils.runCli(['generate-summary-markdown']); + yield utils_1.Utils.setMarkdownAsJobSummary(); + } + catch (error) { + core.warning('Failed while attempting to generate job summary: ' + error); + } + finally { + core.endGroup(); + } + }); +} cleanup(); diff --git a/lib/utils.js b/lib/utils.js index 451ff0fa7..4f82aceec 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -200,48 +200,45 @@ class Utils { return __awaiter(this, void 0, void 0, function* () { let version = core.getInput(Utils.CLI_VERSION_ARG); let cliRemote = core.getInput(Utils.CLI_REMOTE_ARG); - let major = version.split('.')[0]; - if (version === Utils.LATEST_CLI_VERSION) { - version = Utils.LATEST_RELEASE_VERSION; - major = '2'; - } - else if ((0, semver_1.lt)(version, this.MIN_CLI_VERSION)) { + const isLatestVer = version === Utils.LATEST_CLI_VERSION; + if (!isLatestVer && (0, semver_1.lt)(version, this.MIN_CLI_VERSION)) { throw new Error('Requested to download JFrog CLI version ' + version + ' but must be at least ' + this.MIN_CLI_VERSION); } - let jfFileName = Utils.getJfExecutableName(); - let jfrogFileName = Utils.getJFrogExecutableName(); - if (this.loadFromCache(jfFileName, jfrogFileName, version)) { - // Download is not needed + if (!isLatestVer && this.loadFromCache(version)) { + core.info('Found JFrog CLI in cache. No need to download'); return; } // Download JFrog CLI let downloadDetails = Utils.extractDownloadDetails(cliRemote, jfrogCredentials); - let url = Utils.getCliUrl(major, version, jfrogFileName, downloadDetails); + let url = Utils.getCliUrl(version, Utils.getJFrogExecutableName(), downloadDetails); core.info('Downloading JFrog CLI from ' + url); let downloadedExecutable = yield toolCache.downloadTool(url, undefined, downloadDetails.auth); // Cache 'jf' and 'jfrog' executables - yield this.cacheAndAddPath(downloadedExecutable, version, jfFileName, jfrogFileName); + yield this.cacheAndAddPath(downloadedExecutable, version); }); } /** * Try to load the JFrog CLI executables from cache. * - * @param jfFileName - 'jf' or 'jf.exe' - * @param jfrogFileName - 'jfrog' or 'jfrog.exe' * @param version - JFrog CLI version * @returns true if the CLI executable was loaded from cache and added to path */ - static loadFromCache(jfFileName, jfrogFileName, version) { - if (version === Utils.LATEST_RELEASE_VERSION) { - return false; - } - let jfExecDir = toolCache.find(jfFileName, version); - let jfrogExecDir = toolCache.find(jfrogFileName, version); + static loadFromCache(version) { + const jfFileName = Utils.getJfExecutableName(); + const jfrogFileName = Utils.getJFrogExecutableName(); + if (version === Utils.LATEST_CLI_VERSION) { + // If the version is 'latest', we keep it on cache as 100.100.100 + version = Utils.LATEST_SEMVER; + } + const jfExecDir = toolCache.find(jfFileName, version); + const jfrogExecDir = toolCache.find(jfrogFileName, version); if (jfExecDir && jfrogExecDir) { core.addPath(jfExecDir); core.addPath(jfrogExecDir); - // Save the JF CLI path to use on cleanup. saveState/getState are methods to pass data between a step, and its cleanup function. - core.saveState(Utils.JF_CLI_PATH_STATE, jfExecDir); + if (!Utils.isWindows()) { + (0, fs_1.chmodSync)((0, path_1.join)(jfExecDir, jfFileName), 0o555); + (0, fs_1.chmodSync)((0, path_1.join)(jfrogExecDir, jfrogFileName), 0o555); + } return true; } return false; @@ -250,11 +247,15 @@ class Utils { * Add JFrog CLI executables to cache and to the system path. * @param downloadedExecutable - Path to the downloaded JFrog CLI executable * @param version - JFrog CLI version - * @param jfFileName - 'jf' or 'jf.exe' - * @param jfrogFileName - 'jfrog' or 'jfrog.exe' */ - static cacheAndAddPath(downloadedExecutable, version, jfFileName, jfrogFileName) { + static cacheAndAddPath(downloadedExecutable, version) { return __awaiter(this, void 0, void 0, function* () { + if (version === Utils.LATEST_CLI_VERSION) { + // If the version is 'latest', we keep it on cache as 100.100.100 as GitHub actions cache supports only semver versions + version = Utils.LATEST_SEMVER; + } + const jfFileName = Utils.getJfExecutableName(); + const jfrogFileName = Utils.getJFrogExecutableName(); let jfCacheDir = yield toolCache.cacheFile(downloadedExecutable, jfFileName, jfFileName, version); core.addPath(jfCacheDir); let jfrogCacheDir = yield toolCache.cacheFile(downloadedExecutable, jfrogFileName, jfrogFileName, version); @@ -263,13 +264,25 @@ class Utils { (0, fs_1.chmodSync)((0, path_1.join)(jfCacheDir, jfFileName), 0o555); (0, fs_1.chmodSync)((0, path_1.join)(jfrogCacheDir, jfrogFileName), 0o555); } - // Save the JF CLI path to use on cleanup. saveState/getState are methods to pass data between a step, and it's cleanup function. - core.saveState(Utils.JF_CLI_PATH_STATE, jfCacheDir); }); } - static getCliUrl(major, version, fileName, downloadDetails) { - let architecture = 'jfrog-cli-' + Utils.getArchitecture(); - let artifactoryUrl = downloadDetails.artifactoryUrl.replace(/\/$/, ''); + /** + * Get the JFrog CLI download URL. + * @param version - Requested version + * @param fileName - Executable file name + * @param downloadDetails - Source Artifactory details + */ + static getCliUrl(version, fileName, downloadDetails) { + const architecture = 'jfrog-cli-' + Utils.getArchitecture(); + const artifactoryUrl = downloadDetails.artifactoryUrl.replace(/\/$/, ''); + let major; + if (version === Utils.LATEST_CLI_VERSION) { + version = Utils.LATEST_RELEASE_VERSION; + major = '2'; + } + else { + major = version.split('.')[0]; + } return `${artifactoryUrl}/${downloadDetails.repository}/v${major}/${version}/${architecture}/${fileName}`; } // Get Config Tokens created on your local machine using JFrog CLI. @@ -411,11 +424,14 @@ class Utils { */ static runCliAndGetOutput(args, options) { return __awaiter(this, void 0, void 0, function* () { - let output = yield (0, exec_1.getExecOutput)('jf', args, options); + let output; + output = yield (0, exec_1.getExecOutput)('jf', args, Object.assign(Object.assign({}, options), { ignoreReturnCode: true })); if (output.exitCode !== core.ExitCode.Success) { - core.info(output.stdout); - core.info(output.stderr); - throw new Error('JFrog CLI exited with exit code ' + output.exitCode); + if (options === null || options === void 0 ? void 0 : options.silent) { + core.info(output.stdout); + core.info(output.stderr); + } + throw new Error(`JFrog CLI exited with exit code ${output.exitCode}`); } return output.stdout; }); @@ -467,6 +483,10 @@ class Utils { } return; } + static isJobSummarySupported() { + const version = core.getInput(Utils.CLI_VERSION_ARG); + return version === Utils.LATEST_CLI_VERSION || (0, semver_1.gt)(version, Utils.MIN_CLI_VERSION_JOB_SUMMARY); + } /** * Generates GitHub workflow unified Summary report. * This function runs as part of post-workflow cleanup function, @@ -621,14 +641,16 @@ Utils.MIN_CLI_VERSION = '1.46.4'; Utils.LATEST_CLI_VERSION = 'latest'; // The value in the download URL to set to get the latest version Utils.LATEST_RELEASE_VERSION = '[RELEASE]'; -// State name for saving JF CLI path to use on cleanup -Utils.JF_CLI_PATH_STATE = 'JF_CLI_PATH_STATE'; +// Placeholder CLI version to use to keep 'latest' in cache. +Utils.LATEST_SEMVER = '100.100.100'; // The default server id name for separate env config Utils.SETUP_JFROG_CLI_SERVER_ID = 'setup-jfrog-cli-server'; // Directory name which holds markdown files for the Workflow summary Utils.JOB_SUMMARY_DIR_NAME = 'jfrog-command-summary'; // JFrog CLI command summary output directory environment variable Utils.JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR_ENV = 'JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR'; +// Minimum JFrog CLI version supported for job summary command +Utils.MIN_CLI_VERSION_JOB_SUMMARY = '2.66.0'; // Inputs // Version input Utils.CLI_VERSION_ARG = 'version'; diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index f7aaab84c..280e17aca 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -1,6 +1,6 @@ { "name": "@jfrog/setup-jfrog-cli", - "version": "4.3.2", + "version": "4.3.3", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package-lock.json b/package-lock.json index 293044005..b7e4facbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jfrog/setup-jfrog-cli", - "version": "4.3.2", + "version": "4.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jfrog/setup-jfrog-cli", - "version": "4.3.2", + "version": "4.3.3", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 90ee79604..4c8437d61 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@jfrog/setup-jfrog-cli", - "version": "4.3.2", + "version": "4.3.3", "private": true, "description": "Setup JFrog CLI in GitHub Actions", "main": "lib/main.js", diff --git a/src/cleanup.ts b/src/cleanup.ts index 01be9fd53..0f1b86807 100644 --- a/src/cleanup.ts +++ b/src/cleanup.ts @@ -2,29 +2,13 @@ import * as core from '@actions/core'; import { Utils } from './utils'; async function cleanup() { - if (!addCachedJfToPath()) { - core.error('Could not find JFrog CLI path in the step state. Skipping cleanup.'); + if (!Utils.loadFromCache(core.getInput(Utils.CLI_VERSION_ARG))) { + core.warning('Could not find JFrog CLI executable. Skipping cleanup.'); return; } - // Auto-publish build info if needed - try { - if (!core.getBooleanInput(Utils.AUTO_BUILD_PUBLISH_DISABLE)) { - await collectAndPublishBuildInfoIfNeeded(); - } - } catch (error) { - core.warning('failed while attempting to publish build info: ' + error); - } - // Generate job summary - try { - if (!core.getBooleanInput(Utils.JOB_SUMMARY_DISABLE)) { - core.startGroup('Generating Job Summary'); - await Utils.runCli(['generate-summary-markdown']); - await Utils.setMarkdownAsJobSummary(); - core.endGroup(); - } - } catch (error) { - core.warning('failed while attempting to generate job summary: ' + error); - } + // Run post tasks related to Build Info (auto build publish, job summary) + await buildInfoPostTasks(); + // Cleanup JFrog CLI servers configuration try { core.startGroup('Cleanup JFrog CLI servers configuration'); @@ -36,15 +20,41 @@ async function cleanup() { } } -function addCachedJfToPath(): boolean { - // Get the JFrog CLI path from step state. saveState/getState are methods to pass data between a step, and it's cleanup function. - const jfCliPath: string = core.getState(Utils.JF_CLI_PATH_STATE); - if (!jfCliPath) { - // This means that the JFrog CLI was not installed in the first place, because there was a failure in the installation step. - return false; +/** + * Executes post tasks related to build information. + * + * This function performs several tasks after the main build process: + * 1. Checks if auto build publish and job summary are disabled. + * 2. Verifies connection to JFrog Artifactory. + * 3. Collects and publishes build information if needed. + * 4. Generates a job summary if required. + */ +async function buildInfoPostTasks() { + const disableAutoBuildPublish: boolean = core.getBooleanInput(Utils.AUTO_BUILD_PUBLISH_DISABLE); + const disableJobSummary: boolean = core.getBooleanInput(Utils.JOB_SUMMARY_DISABLE) || !Utils.isJobSummarySupported(); + if (disableAutoBuildPublish && disableJobSummary) { + core.info(`Both auto-build-publish and job-summary are disabled. Skipping Build Info post tasks.`); + return; + } + + // Check connection to Artifactory before proceeding with build info post tasks + if (!(await checkConnectionToArtifactory())) { + return; + } + + // Auto-publish build info if needed + if (!disableAutoBuildPublish) { + await collectAndPublishBuildInfoIfNeeded(); + } else { + core.info('Auto build info publish is disabled. Skipping auto build info collection and publishing'); + } + + // Generate job summary if not disabled and the JFrog CLI version supports it + if (!disableJobSummary) { + await generateJobSummary(); + } else { + core.info('Job summary is disabled. Skipping job summary generation'); } - core.addPath(jfCliPath); - return true; } interface BuildPublishResponse { @@ -55,21 +65,23 @@ async function hasUnpublishedModules(workingDirectory: string): Promise // Save the old value of the environment variable to revert it later const origValue: string | undefined = process.env[Utils.JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR_ENV]; try { + core.startGroup('Check for unpublished modules'); // Avoid saving a command summary for this dry-run command core.exportVariable(Utils.JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR_ENV, ''); // Running build-publish command with a dry-run flag to check if there are any unpublished modules, 'silent' to avoid polluting the logs - const responseStr: string = await Utils.runCliAndGetOutput(['rt', 'build-publish', '--dry-run'], { silent: true, cwd: workingDirectory }); + const responseStr: string = await Utils.runCliAndGetOutput(['rt', 'build-publish', '--dry-run'], { cwd: workingDirectory }); // Parse the JSON string to an object const response: BuildPublishResponse = JSON.parse(responseStr); // Check if the "modules" key exists and if it's an array with more than one item return response.modules != undefined && Array.isArray(response.modules) && response.modules.length > 0; } catch (error) { - core.error('Failed to parse JSON: ' + error); - return false; // Return false if parsing fails + core.warning('Failed to check if there are any unpublished modules: ' + error); + return false; } finally { core.exportVariable(Utils.JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR_ENV, origValue); + core.endGroup(); } } @@ -82,19 +94,22 @@ async function collectAndPublishBuildInfoIfNeeded() { // The flow here is to collect Git information before publishing the build info. // We allow this step to fail, and we don't want to fail the entire build publish if they do. - try { core.startGroup('Collect the Git information'); await Utils.runCli(['rt', 'build-add-git'], { cwd: workingDirectory }); } catch (error) { - core.warning('failed while attempting to collect Git information: ' + error); + core.warning('Failed while attempting to collect Git information: ' + error); } finally { core.endGroup(); } - core.startGroup('Publish the build info to JFrog Artifactory'); - await Utils.runCli(['rt', 'build-publish'], { cwd: workingDirectory }); - core.endGroup(); + // Publish the build info to Artifactory + try { + core.startGroup('Publish the build info to JFrog Artifactory'); + await Utils.runCli(['rt', 'build-publish'], { cwd: workingDirectory }); + } finally { + core.endGroup(); + } } function getWorkingDirectory(): string { @@ -105,4 +120,34 @@ function getWorkingDirectory(): string { return workingDirectory; } +async function checkConnectionToArtifactory(): Promise { + try { + core.startGroup('Checking connection to JFrog Artifactory'); + const pingResult: string = await Utils.runCliAndGetOutput(['rt', 'ping']); + if (pingResult !== 'OK') { + core.debug(`Ping result: ${pingResult}`); + core.warning('Could not connect to Artifactory. Skipping Build Info post tasks.'); + return false; + } + return true; + } catch (error) { + core.warning(`An error occurred while trying to connect to Artifactory: ${error}. Skipping Build Info post tasks.`); + return false; + } finally { + core.endGroup(); + } +} + +async function generateJobSummary() { + try { + core.startGroup('Generating Job Summary'); + await Utils.runCli(['generate-summary-markdown']); + await Utils.setMarkdownAsJobSummary(); + } catch (error) { + core.warning('Failed while attempting to generate job summary: ' + error); + } finally { + core.endGroup(); + } +} + cleanup(); diff --git a/src/utils.ts b/src/utils.ts index 849c43b15..96e68448c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,7 +7,7 @@ import { OutgoingHttpHeaders } from 'http'; import { arch, platform, tmpdir } from 'os'; import * as path from 'path'; import { join } from 'path'; -import { lt } from 'semver'; +import { gt, lt } from 'semver'; export class Utils { // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -26,18 +26,20 @@ export class Utils { private static readonly LATEST_CLI_VERSION: string = 'latest'; // The value in the download URL to set to get the latest version private static readonly LATEST_RELEASE_VERSION: string = '[RELEASE]'; - // State name for saving JF CLI path to use on cleanup - public static readonly JF_CLI_PATH_STATE: string = 'JF_CLI_PATH_STATE'; + // Placeholder CLI version to use to keep 'latest' in cache. + public static readonly LATEST_SEMVER: string = '100.100.100'; // The default server id name for separate env config public static readonly SETUP_JFROG_CLI_SERVER_ID: string = 'setup-jfrog-cli-server'; // Directory name which holds markdown files for the Workflow summary private static readonly JOB_SUMMARY_DIR_NAME: string = 'jfrog-command-summary'; // JFrog CLI command summary output directory environment variable public static readonly JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR_ENV: string = 'JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR'; + // Minimum JFrog CLI version supported for job summary command + private static readonly MIN_CLI_VERSION_JOB_SUMMARY: string = '2.66.0'; // Inputs // Version input - private static readonly CLI_VERSION_ARG: string = 'version'; + public static readonly CLI_VERSION_ARG: string = 'version'; // Download repository input private static readonly CLI_REMOTE_ARG: string = 'download-repository'; // OpenID Connect audience input @@ -223,50 +225,48 @@ export class Utils { public static async getAndAddCliToPath(jfrogCredentials: JfrogCredentials) { let version: string = core.getInput(Utils.CLI_VERSION_ARG); let cliRemote: string = core.getInput(Utils.CLI_REMOTE_ARG); - let major: string = version.split('.')[0]; - if (version === Utils.LATEST_CLI_VERSION) { - version = Utils.LATEST_RELEASE_VERSION; - major = '2'; - } else if (lt(version, this.MIN_CLI_VERSION)) { + const isLatestVer: boolean = version === Utils.LATEST_CLI_VERSION; + + if (!isLatestVer && lt(version, this.MIN_CLI_VERSION)) { throw new Error('Requested to download JFrog CLI version ' + version + ' but must be at least ' + this.MIN_CLI_VERSION); } - - let jfFileName: string = Utils.getJfExecutableName(); - let jfrogFileName: string = Utils.getJFrogExecutableName(); - if (this.loadFromCache(jfFileName, jfrogFileName, version)) { - // Download is not needed + if (!isLatestVer && this.loadFromCache(version)) { + core.info('Found JFrog CLI in cache. No need to download'); return; } - // Download JFrog CLI let downloadDetails: DownloadDetails = Utils.extractDownloadDetails(cliRemote, jfrogCredentials); - let url: string = Utils.getCliUrl(major, version, jfrogFileName, downloadDetails); + let url: string = Utils.getCliUrl(version, Utils.getJFrogExecutableName(), downloadDetails); core.info('Downloading JFrog CLI from ' + url); let downloadedExecutable: string = await toolCache.downloadTool(url, undefined, downloadDetails.auth); // Cache 'jf' and 'jfrog' executables - await this.cacheAndAddPath(downloadedExecutable, version, jfFileName, jfrogFileName); + await this.cacheAndAddPath(downloadedExecutable, version); } /** * Try to load the JFrog CLI executables from cache. * - * @param jfFileName - 'jf' or 'jf.exe' - * @param jfrogFileName - 'jfrog' or 'jfrog.exe' * @param version - JFrog CLI version * @returns true if the CLI executable was loaded from cache and added to path */ - private static loadFromCache(jfFileName: string, jfrogFileName: string, version: string): boolean { - if (version === Utils.LATEST_RELEASE_VERSION) { - return false; + public static loadFromCache(version: string): boolean { + const jfFileName: string = Utils.getJfExecutableName(); + const jfrogFileName: string = Utils.getJFrogExecutableName(); + if (version === Utils.LATEST_CLI_VERSION) { + // If the version is 'latest', we keep it on cache as 100.100.100 + version = Utils.LATEST_SEMVER; } - let jfExecDir: string = toolCache.find(jfFileName, version); - let jfrogExecDir: string = toolCache.find(jfrogFileName, version); + const jfExecDir: string = toolCache.find(jfFileName, version); + const jfrogExecDir: string = toolCache.find(jfrogFileName, version); if (jfExecDir && jfrogExecDir) { core.addPath(jfExecDir); core.addPath(jfrogExecDir); - // Save the JF CLI path to use on cleanup. saveState/getState are methods to pass data between a step, and its cleanup function. - core.saveState(Utils.JF_CLI_PATH_STATE, jfExecDir); + + if (!Utils.isWindows()) { + chmodSync(join(jfExecDir, jfFileName), 0o555); + chmodSync(join(jfrogExecDir, jfrogFileName), 0o555); + } return true; } return false; @@ -276,10 +276,14 @@ export class Utils { * Add JFrog CLI executables to cache and to the system path. * @param downloadedExecutable - Path to the downloaded JFrog CLI executable * @param version - JFrog CLI version - * @param jfFileName - 'jf' or 'jf.exe' - * @param jfrogFileName - 'jfrog' or 'jfrog.exe' */ - private static async cacheAndAddPath(downloadedExecutable: string, version: string, jfFileName: string, jfrogFileName: string) { + private static async cacheAndAddPath(downloadedExecutable: string, version: string) { + if (version === Utils.LATEST_CLI_VERSION) { + // If the version is 'latest', we keep it on cache as 100.100.100 as GitHub actions cache supports only semver versions + version = Utils.LATEST_SEMVER; + } + const jfFileName: string = Utils.getJfExecutableName(); + const jfrogFileName: string = Utils.getJFrogExecutableName(); let jfCacheDir: string = await toolCache.cacheFile(downloadedExecutable, jfFileName, jfFileName, version); core.addPath(jfCacheDir); @@ -290,16 +294,27 @@ export class Utils { chmodSync(join(jfCacheDir, jfFileName), 0o555); chmodSync(join(jfrogCacheDir, jfrogFileName), 0o555); } - - // Save the JF CLI path to use on cleanup. saveState/getState are methods to pass data between a step, and it's cleanup function. - core.saveState(Utils.JF_CLI_PATH_STATE, jfCacheDir); } - public static getCliUrl(major: string, version: string, fileName: string, downloadDetails: DownloadDetails): string { - let architecture: string = 'jfrog-cli-' + Utils.getArchitecture(); - let artifactoryUrl: string = downloadDetails.artifactoryUrl.replace(/\/$/, ''); + /** + * Get the JFrog CLI download URL. + * @param version - Requested version + * @param fileName - Executable file name + * @param downloadDetails - Source Artifactory details + */ + public static getCliUrl(version: string, fileName: string, downloadDetails: DownloadDetails): string { + const architecture: string = 'jfrog-cli-' + Utils.getArchitecture(); + const artifactoryUrl: string = downloadDetails.artifactoryUrl.replace(/\/$/, ''); + let major: string; + if (version === Utils.LATEST_CLI_VERSION) { + version = Utils.LATEST_RELEASE_VERSION; + major = '2'; + } else { + major = version.split('.')[0]; + } return `${artifactoryUrl}/${downloadDetails.repository}/v${major}/${version}/${architecture}/${fileName}`; } + // Get Config Tokens created on your local machine using JFrog CLI. // The Tokens configured with JF_ENV_ environment variables. public static getConfigTokens(): Set { @@ -455,11 +470,14 @@ export class Utils { * @throws An error if the JFrog CLI command exits with a non-success code. */ public static async runCliAndGetOutput(args: string[], options?: ExecOptions): Promise { - let output: ExecOutput = await getExecOutput('jf', args, options); + let output: ExecOutput; + output = await getExecOutput('jf', args, { ...options, ignoreReturnCode: true }); if (output.exitCode !== core.ExitCode.Success) { - core.info(output.stdout); - core.info(output.stderr); - throw new Error('JFrog CLI exited with exit code ' + output.exitCode); + if (options?.silent) { + core.info(output.stdout); + core.info(output.stderr); + } + throw new Error(`JFrog CLI exited with exit code ${output.exitCode}`); } return output.stdout; } @@ -516,6 +534,11 @@ export class Utils { return; } + public static isJobSummarySupported(): boolean { + const version: string = core.getInput(Utils.CLI_VERSION_ARG); + return version === Utils.LATEST_CLI_VERSION || gt(version, Utils.MIN_CLI_VERSION_JOB_SUMMARY); + } + /** * Generates GitHub workflow unified Summary report. * This function runs as part of post-workflow cleanup function, diff --git a/test/main.spec.ts b/test/main.spec.ts index 4464fd497..2b4cbdeab 100644 --- a/test/main.spec.ts +++ b/test/main.spec.ts @@ -168,11 +168,11 @@ describe('JFrog CLI V1 URL Tests', () => { test.each(cases)('CLI Url for %s-%s', (platform, arch, fileName, expectedUrl) => { myOs.platform.mockImplementation(() => platform); myOs.arch.mockImplementation(() => arch); - let cliUrl: string = Utils.getCliUrl('1', '1.2.3', fileName, Utils.DEFAULT_DOWNLOAD_DETAILS); + let cliUrl: string = Utils.getCliUrl('1.2.3', fileName, Utils.DEFAULT_DOWNLOAD_DETAILS); expect(cliUrl).toBe(DEFAULT_CLI_URL + expectedUrl); process.env.JF_ENV_LOCAL = V1_CONFIG; - cliUrl = Utils.getCliUrl('1', '1.2.3', fileName, Utils.extractDownloadDetails('jfrog-cli-remote', {} as JfrogCredentials)); + cliUrl = Utils.getCliUrl('1.2.3', fileName, Utils.extractDownloadDetails('jfrog-cli-remote', {} as JfrogCredentials)); expect(cliUrl).toBe(CUSTOM_CLI_URL + expectedUrl); }); }); @@ -193,11 +193,11 @@ describe('JFrog CLI V2 URL Tests', () => { myOs.platform.mockImplementation(() => platform); myOs.arch.mockImplementation(() => arch); - let cliUrl: string = Utils.getCliUrl('2', '2.3.4', fileName, Utils.extractDownloadDetails('', {} as JfrogCredentials)); + let cliUrl: string = Utils.getCliUrl('2.3.4', fileName, Utils.extractDownloadDetails('', {} as JfrogCredentials)); expect(cliUrl).toBe(DEFAULT_CLI_URL + expectedUrl); process.env.JF_ENV_LOCAL = V2_CONFIG; - cliUrl = Utils.getCliUrl('2', '2.3.4', fileName, Utils.extractDownloadDetails('jfrog-cli-remote', {} as JfrogCredentials)); + cliUrl = Utils.getCliUrl('2.3.4', fileName, Utils.extractDownloadDetails('jfrog-cli-remote', {} as JfrogCredentials)); expect(cliUrl).toBe(CUSTOM_CLI_URL + expectedUrl); }); });