Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 <server-id>`.
</details>

<details>
<summary>Enabling Package Alias</summary>

### 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
```
</details>

## JFrog Job Summary

Workflows using this GitHub action will output a summary of some of the key commands that were performed using JFrog CLI.
Expand Down
7 changes: 7 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
1 change: 1 addition & 0 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
64 changes: 64 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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';
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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((<any>error).message);
} finally {
Expand Down
63 changes: 60 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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';
Expand All @@ -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<void> {
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.');
}
}
103 changes: 103 additions & 0 deletions test/main.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -423,3 +424,105 @@ describe('getJfrogCliConfigArgs', () => {
expect(configString).not.toContain('--username test-user');
});
});

describe('setupPackageAliasIfRequested', () => {
const myCore: jest.Mocked<typeof core> = core as any;
const myExec: jest.Mocked<typeof exec> = 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();
});
});
Loading