diff --git a/README.md b/README.md index 51250637f..04ede3532 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ - [Setting the JFrog Project Key](#setting-the-jfrog-project-key) - [Downloading JFrog CLI from Artifactory](#downloading-jfrog-cli-from-artifactory) - [Custom Server ID and Multi-Configuration](#custom-server-id-and-multi-configuration) + - [Enabling Package Alias](#enabling-package-alias) - [JFrog Job Summary](#jfrog-job-summary) - [Code Scanning Alerts](#code-scanning-alerts) - [Automatic Evidence Collection](#automatic-evidence-collection) @@ -354,6 +355,25 @@ You may also use multiple configurations in the same workflow by providing a cus Alternating between configurations can be done by providing the `--server-id` option to JFrog CLI commands or by setting a default server using `jf c use `. +
+ Enabling Package Alias + +### Enabling Package Alias + +When enabled, the action runs `jf package-alias install` after setting up JFrog CLI and appends the alias bin directory to `GITHUB_PATH`. Subsequent steps will transparently intercept package manager commands such as `mvn`, `npm`, `go`, etc., so they use JFrog CLI without changing your workflow scripts. + +You can optionally provide `package-alias-tools` as a comma-separated list to pass specific package managers to `jf package-alias install --packages`. + +This feature requires JFrog CLI version **2.93.0** or above. If the requested version is older, or if `jf package-alias install` fails for another reason, the action logs a warning and does not fail the job; subsequent steps will not use package aliases. + +```yml +- uses: jfrog/setup-jfrog-cli@v4 + with: + enable-package-alias: true + package-alias-tools: npm,mvn,go +``` +
+ ## JFrog Job Summary Workflows using this GitHub action will output a summary of some of the key commands that were performed using JFrog CLI. diff --git a/action.yml b/action.yml index 32eea042f..ec7f90009 100644 --- a/action.yml +++ b/action.yml @@ -36,6 +36,13 @@ inputs: ghe_base_url: description: 'Alias for ghe-base-url' required: false + enable-package-alias: + description: "If true, after setting up JFrog CLI the action runs 'jf package-alias install' and appends the alias directory to GITHUB_PATH so that mvn, npm, go, etc. are transparently intercepted in subsequent steps." + default: "false" + required: false + package-alias-tools: + description: "Comma-separated package manager tools to pass to 'jf package-alias install --packages'." + required: false outputs: oidc-token: description: "JFrog OIDC token generated by the Setup JFrog CLI when setting oidc-provider-name." diff --git a/lib/main.js b/lib/main.js index 9b908ac15..9b7d3065e 100644 --- a/lib/main.js +++ b/lib/main.js @@ -42,6 +42,7 @@ function main() { let jfrogCredentials = utils_1.Utils.collectJfrogCredentialsFromEnvVars(); yield utils_1.Utils.getAndAddCliToPath(jfrogCredentials); yield utils_1.Utils.configJFrogServers(jfrogCredentials); + yield utils_1.Utils.setupPackageAliasIfRequested(); } catch (error) { core.setFailed(error.message); diff --git a/lib/utils.js b/lib/utils.js index 1f9b70440..68004282e 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -454,6 +454,64 @@ class Utils { } return; } + /** + * Returns the package-alias bin directory used by `jf package-alias install`. + * Linux/macOS: $HOME/.jfrog/package-alias/bin + * Windows: %USERPROFILE%\.jfrog\package-alias\bin + */ + static getPackageAliasBinDir() { + const home = process.env.HOME || process.env.USERPROFILE || ''; + return (0, path_1.join)(home, '.jfrog', 'package-alias', 'bin'); + } + /** + * If enable-package-alias is true and GITHUB_PATH is set, runs `jf package-alias install` + * and appends the alias bin directory to GITHUB_PATH so subsequent steps intercept mvn, npm, go, etc. + * On failure (e.g. older CLI without package-alias), logs a warning and does not fail the job. + */ + static setupPackageAliasIfRequested() { + return __awaiter(this, void 0, void 0, function* () { + if (!core.getBooleanInput(Utils.ENABLE_PACKAGE_ALIAS)) { + return; + } + const githubPath = process.env.GITHUB_PATH; + if (!githubPath) { + core.warning('enable-package-alias is true but GITHUB_PATH is not set (not running in GitHub Actions?). Skipping package-alias setup.'); + return; + } + const version = core.getInput(Utils.CLI_VERSION_ARG); + if (version !== Utils.LATEST_CLI_VERSION && !(0, semver_1.gte)(version, this.MIN_CLI_VERSION_PACKAGE_ALIAS)) { + core.warning('Package aliasing requires JFrog CLI ' + + this.MIN_CLI_VERSION_PACKAGE_ALIAS + + ' or above; requested version is ' + + version + + '. ' + + 'Skipping package-alias setup; subsequent steps will not use package aliases.'); + return; + } + const packageAliasTools = core + .getInput(Utils.PACKAGE_ALIAS_TOOLS) + .split(',') + .map((tool) => tool.trim()) + .filter((tool) => !!tool) + .join(','); + const packageAliasInstallArgs = ['package-alias', 'install']; + if (packageAliasTools) { + packageAliasInstallArgs.push('--packages', packageAliasTools); + } + const exitCode = yield (0, exec_1.exec)('jf', packageAliasInstallArgs, { ignoreReturnCode: true }); + if (exitCode !== core.ExitCode.Success) { + core.warning('jf package-alias install failed (exit code ' + + exitCode + + '). ' + + "Package Aliasing requires JFrog CLI version that supports 'jf package-alias'. " + + 'Skipping; subsequent steps will not use package aliases.'); + return; + } + const aliasBinDir = Utils.getPackageAliasBinDir(); + (0, fs_1.appendFileSync)(githubPath, aliasBinDir + '\n'); + core.info('Package aliases installed and "' + aliasBinDir + '" appended to GITHUB_PATH.'); + }); + } } exports.Utils = Utils; // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -497,3 +555,9 @@ Utils.CUSTOM_SERVER_ID = 'custom-server-id'; // GHES baseUrl support Utils.GHE_BASE_URL_INPUT = 'ghe-base-url'; Utils.GHE_BASE_URL_ALIAS_INPUT = 'ghe_base_url'; +// Enable Package Alias so mvn, npm, go etc. are intercepted in subsequent steps +Utils.ENABLE_PACKAGE_ALIAS = 'enable-package-alias'; +// Comma-separated package managers to include in package alias install +Utils.PACKAGE_ALIAS_TOOLS = 'package-alias-tools'; +// Minimum JFrog CLI version that supports jf package-alias +Utils.MIN_CLI_VERSION_PACKAGE_ALIAS = '2.93.0'; diff --git a/package-lock.json b/package-lock.json index 2652fa3a9..d263397d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "@jfrog/setup-jfrog-cli", - "version": "4.6.0", + "version": "4.9.1", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/src/main.ts b/src/main.ts index 24ff93361..8aeb6dd86 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,6 +9,7 @@ async function main() { let jfrogCredentials: JfrogCredentials = Utils.collectJfrogCredentialsFromEnvVars(); await Utils.getAndAddCliToPath(jfrogCredentials); await Utils.configJFrogServers(jfrogCredentials); + await Utils.setupPackageAliasIfRequested(); } catch (error) { core.setFailed((error).message); } finally { diff --git a/src/utils.ts b/src/utils.ts index ef4168309..f70a4ee32 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,7 +7,7 @@ import { chmodSync } from 'fs'; import { arch, platform } from 'os'; import { join } from 'path'; -import { lt } from 'semver'; +import { gte, lt } from 'semver'; import { DownloadDetails, JfrogCredentials } from './types'; import { OidcUtils } from './oidc-utils'; @@ -57,6 +57,11 @@ export class Utils { // GHES baseUrl support public static readonly GHE_BASE_URL_INPUT: string = 'ghe-base-url'; public static readonly GHE_BASE_URL_ALIAS_INPUT: string = 'ghe_base_url'; + // Enable Package Alias so mvn, npm, go etc. are intercepted in subsequent steps + public static readonly ENABLE_PACKAGE_ALIAS: string = 'enable-package-alias'; + + // Minimum JFrog CLI version that supports jf package-alias + private static readonly MIN_CLI_VERSION_PACKAGE_ALIAS: string = '2.93.0'; /** * Gathers JFrog's credentials from environment variables and delivers them in a JfrogCredentials structure @@ -480,8 +485,8 @@ export class Utils { if (!jfrogCredentials.jfrogUrl) { throw new Error( `'download-repository' input provided, but no JFrog environment details found. ` + - `Hint - Ensure that the JFrog connection details environment variables are set: ` + - `either a Config Token with a JF_ENV_ prefix or separate env config (JF_URL, JF_USER, JF_PASSWORD, JF_ACCESS_TOKEN)`, + `Hint - Ensure that the JFrog connection details environment variables are set: ` + + `either a Config Token with a JF_ENV_ prefix or separate env config (JF_URL, JF_USER, JF_PASSWORD, JF_ACCESS_TOKEN)`, ); } serverObj.artifactoryUrl = jfrogCredentials.jfrogUrl.replace(/\/$/, '') + '/artifactory'; @@ -506,4 +511,56 @@ export class Utils { } return; } + + /** + * Returns the package-alias bin directory used by `jf package-alias install`. + * Linux/macOS: $HOME/.jfrog/package-alias/bin + * Windows: %USERPROFILE%\.jfrog\package-alias\bin + */ + public static getPackageAliasBinDir(): string { + const home = process.env.HOME || process.env.USERPROFILE || ''; + return join(home, '.jfrog', 'package-alias', 'bin'); + } + + /** + * If enable-package-alias is true and GITHUB_PATH is set, runs `jf package-alias install` + * and adds the alias bin directory to PATH via core.addPath so subsequent steps intercept mvn, npm, go, etc. + * On failure (e.g. older CLI without package-alias), logs a warning and does not fail the job. + */ + public static async setupPackageAliasIfRequested(): Promise { + if (!core.getBooleanInput(Utils.ENABLE_PACKAGE_ALIAS)) { + return; + } + const githubPath = process.env.GITHUB_PATH; + if (!githubPath) { + core.warning('enable-package-alias is true but GITHUB_PATH is not set (not running in GitHub Actions?). Skipping package-alias setup.'); + return; + } + const version: string = core.getInput(Utils.CLI_VERSION_ARG); + if (version !== Utils.LATEST_CLI_VERSION && !gte(version, this.MIN_CLI_VERSION_PACKAGE_ALIAS)) { + core.warning( + 'Package aliasing requires JFrog CLI ' + + this.MIN_CLI_VERSION_PACKAGE_ALIAS + + ' or above; requested version is ' + + version + + '. ' + + 'Skipping package-alias setup; subsequent steps will not use package aliases.', + ); + return; + } + const exitCode = await exec('jf', ['package-alias', 'install'], { ignoreReturnCode: true }); + if (exitCode !== core.ExitCode.Success) { + core.warning( + 'jf package-alias install failed (exit code ' + + exitCode + + '). ' + + "Package Aliasing requires JFrog CLI version that supports 'jf package-alias'. " + + 'Skipping; subsequent steps will not use package aliases.', + ); + return; + } + const aliasBinDir = Utils.getPackageAliasBinDir(); + core.addPath(aliasBinDir); + core.info('Package aliases installed and "' + aliasBinDir + '" added to PATH.'); + } } diff --git a/test/main.spec.ts b/test/main.spec.ts index 86880b3a2..875efc69a 100644 --- a/test/main.spec.ts +++ b/test/main.spec.ts @@ -1,6 +1,7 @@ import * as os from 'os'; import * as core from '@actions/core'; import * as exec from '@actions/exec'; +import { existsSync, unlinkSync } from 'fs'; import { Utils } from '../src/utils'; import { DownloadDetails, JfrogCredentials } from '../src/types'; @@ -423,3 +424,105 @@ describe('getJfrogCliConfigArgs', () => { expect(configString).not.toContain('--username test-user'); }); }); + +describe('setupPackageAliasIfRequested', () => { + const myCore: jest.Mocked = core as any; + const myExec: jest.Mocked = exec as any; + let githubPath: string; + + beforeEach(() => { + jest.clearAllMocks(); + githubPath = `.github-path-${Date.now()}`; + process.env.GITHUB_PATH = githubPath; + }); + + afterEach(() => { + delete process.env.GITHUB_PATH; + if (existsSync(githubPath)) { + unlinkSync(githubPath); + } + jest.restoreAllMocks(); + }); + + it('should run package-alias install without --packages when package-alias-tools is empty', async () => { + jest.spyOn(core, 'getBooleanInput').mockReturnValue(true); + jest.spyOn(core, 'getInput').mockImplementation((name: string) => { + if (name === Utils.CLI_VERSION_ARG) { + return Utils.LATEST_CLI_VERSION; + } + if (name === Utils.PACKAGE_ALIAS_TOOLS) { + return ''; + } + return ''; + }); + myExec.exec.mockResolvedValue(0); + + await Utils.setupPackageAliasIfRequested(); + + expect(myExec.exec).toHaveBeenCalledWith('jf', ['package-alias', 'install'], { ignoreReturnCode: true }); + }); + + it('should pass normalized tools as --packages when package-alias-tools is provided', async () => { + jest.spyOn(core, 'getBooleanInput').mockReturnValue(true); + jest.spyOn(core, 'getInput').mockImplementation((name: string) => { + if (name === Utils.CLI_VERSION_ARG) { + return Utils.LATEST_CLI_VERSION; + } + if (name === Utils.PACKAGE_ALIAS_TOOLS) { + return 'npm, mvn, ,go '; + } + return ''; + }); + myExec.exec.mockResolvedValue(0); + + await Utils.setupPackageAliasIfRequested(); + + expect(myExec.exec).toHaveBeenCalledWith('jf', ['package-alias', 'install', '--packages', 'npm,mvn,go'], { ignoreReturnCode: true }); + }); + + it('should ignore whitespace-only package-alias-tools segments', async () => { + jest.spyOn(core, 'getBooleanInput').mockReturnValue(true); + jest.spyOn(core, 'getInput').mockImplementation((name: string) => { + if (name === Utils.CLI_VERSION_ARG) { + return Utils.LATEST_CLI_VERSION; + } + if (name === Utils.PACKAGE_ALIAS_TOOLS) { + return ' , , '; + } + return ''; + }); + myExec.exec.mockResolvedValue(0); + + await Utils.setupPackageAliasIfRequested(); + + expect(myExec.exec).toHaveBeenCalledWith('jf', ['package-alias', 'install'], { ignoreReturnCode: true }); + }); + + it('should skip package alias setup when feature is disabled', async () => { + jest.spyOn(core, 'getBooleanInput').mockReturnValue(false); + myExec.exec.mockResolvedValue(0); + + await Utils.setupPackageAliasIfRequested(); + + expect(myExec.exec).not.toHaveBeenCalled(); + }); + + it('should preserve skip behavior for unsupported CLI versions', async () => { + jest.spyOn(core, 'getBooleanInput').mockReturnValue(true); + jest.spyOn(core, 'getInput').mockImplementation((name: string) => { + if (name === Utils.CLI_VERSION_ARG) { + return '2.92.0'; + } + if (name === Utils.PACKAGE_ALIAS_TOOLS) { + return 'npm,mvn'; + } + return ''; + }); + myExec.exec.mockResolvedValue(0); + + await Utils.setupPackageAliasIfRequested(); + + expect(myCore.warning).toHaveBeenCalled(); + expect(myExec.exec).not.toHaveBeenCalled(); + }); +});