Skip to content
Merged
11 changes: 9 additions & 2 deletions .github/workflows/auto-build-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ ubuntu, windows, macos ]
# macOS omitted: local Artifactory bootstrap on GitHub-hosted macOS is broken
# (JGC-413; same skip as jfrog-cli). Per-step skips still provision a runner.
os: [ ubuntu, windows ]
cli-version: [ "latest", "2.66.0" ]
runs-on: ${{ matrix.os }}-latest
steps:
Expand All @@ -31,8 +33,13 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}

# This repo is a Node action and has no go.mod. install-go-with-cache@main
# reads go.mod when go-version is unset (#28), which fails Setup Go immediately.
# local-rt-setup (used by install-local-artifactory) requires Go 1.24+.
- name: Setup Go with cache
uses: jfrog/.github/actions/install-go-with-cache@main
with:
go-version: "1.24"

- name: Install local Artifactory
uses: jfrog/.github/actions/install-local-artifactory@main
Expand Down Expand Up @@ -84,4 +91,4 @@ jobs:
- name: Add npm modules to local build-info
run: |
jf npm-config --repo-resolve npm-remote
jf npm install
jf npm install
2 changes: 2 additions & 0 deletions .github/workflows/frogbot-scan-repository.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ jobs:
steps:
- name: Setup Go with cache
uses: jfrog/.github/actions/install-go-with-cache@main
with:
go-version: "1.24"

- uses: jfrog/frogbot@v2
env:
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@ It is also possible to set the latest JFrog CLI version by adding the _version_
version: latest
```

With `download-repository`, prefer a concrete `version: X.Y.Z` over `latest`. See [Downloading JFrog CLI from Artifactory](#downloading-jfrog-cli-from-artifactory).

| Important: Only JFrog CLI versions 1.46.4 or above are supported. |
|-------------------------------------------------------------------|

Expand Down Expand Up @@ -417,10 +419,17 @@ In this example, each job builds and publishes a different service from the mono

If your agent has no Internet access, you can configure the workflow to download JFrog CLI from a [remote repository](https://www.jfrog.com/confluence/display/JFROG/Remote+Repositories) in your JFrog Artifactory, which is configured to proxy the official download URL.

> [!NOTE]
> With `download-repository`, prefer a concrete `version: X.Y.Z` over `latest`.
>
> `latest` is not resolved to a version number. It becomes the literal path segment `[RELEASE]` in the download URL (`v2/[RELEASE]/jfrog-cli-.../jfrog`), and a generic repository serves that path like any other. If the repository has **Store Artifacts Locally** enabled (the default), the binary returned for that path is cached under it, so later runs can keep receiving that same binary instead of a newer CLI. A concrete version avoids this, because every version has its own immutable path.
>
> Jobs that can reach the internet and want the newest CLI: omit `download-repository`.

Here's how you do this:

1. Create a remote repository in Artifactory. Name the repository jfrog-cli-remote and set its URL to https://releases.jfrog.io/artifactory/jfrog-cli/
2. Set _download-repository_ input to jfrog-cli-remote:
2. Set _download-repository_ input to jfrog-cli-remote and pin `version`:

```yml
- uses: jfrog/setup-jfrog-cli@v4
Expand All @@ -430,6 +439,7 @@ Here's how you do this:
JF_ACCESS_TOKEN: ${{ secrets.JF_ACCESS_TOKEN }}

with:
version: X.Y.Z
download-repository: jfrog-cli-remote
```
</details>
Expand Down
4 changes: 2 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ description: "Install and configure JFrog CLI."
author: "JFrog"
inputs:
version:
description: "JFrog CLI Version"
description: "JFrog CLI Version. A concrete X.Y.Z is recommended when download-repository points to a remote repository that stores artifacts locally, since latest is requested as the v2/[RELEASE] path and can be served from cache."
default: "2.91.0"
required: false
download-repository:
description: "Remote repository in Artifactory pointing to 'https://releases.jfrog.io/artifactory/jfrog-cli'. Use this parameter in case you don't have an Internet access."
description: "Remote repository in Artifactory pointing to 'https://releases.jfrog.io/artifactory/jfrog-cli'. Use this parameter in case you don't have an Internet access. When the repository stores artifacts locally, prefer a concrete version over latest: latest is requested as the literal v2/[RELEASE] path, and the binary cached under that path can keep being served on later runs."
required: false
oidc-provider-name:
description: "Provider Name's value that was set in OpenId Connect integration in the JFrog platform."
Expand Down
6 changes: 4 additions & 2 deletions lib/cleanup.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkConnectionToArtifactory = checkConnectionToArtifactory;
const core = __importStar(require("@actions/core"));
const fs_1 = require("fs");
const path_1 = require("path");
const utils_1 = require("./utils");
const job_summary_1 = require("./job-summary");
const evidence_collection_1 = require("./evidence-collection");
Expand Down Expand Up @@ -137,8 +139,8 @@ function collectAndPublishBuildInfoIfNeeded() {
// 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');
const gitDir = require('path').join(workingDirectory, '.git');
if (require('fs').existsSync(gitDir)) {
const gitDir = (0, path_1.join)(workingDirectory, '.git');
if ((0, fs_1.existsSync)(gitDir)) {
yield utils_1.Utils.runCli(['rt', 'build-add-git'], { cwd: workingDirectory });
}
else {
Expand Down
19 changes: 16 additions & 3 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class Utils {
let version = core.getInput(Utils.CLI_VERSION_ARG);
let cliRemote = core.getInput(Utils.CLI_REMOTE_ARG);
const isLatestVer = version === Utils.LATEST_CLI_VERSION;
Utils.logIfLatestDownloadedFromRemote(version, cliRemote);
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);
}
Expand Down Expand Up @@ -168,6 +169,16 @@ class Utils {
}
return `${artifactoryUrl}/${downloadDetails.repository}/v${major}/${version}/${architecture}/${fileName}`;
}
/**
* Log when latest is resolved through an Artifactory repository.
* [RELEASE] is part of the artifact path, so a remote repository caches it like any other file
* and keeps serving the first version it resolved. Pin a concrete version instead.
*/
static logIfLatestDownloadedFromRemote(version, cliRemote) {
if (cliRemote && version === Utils.LATEST_CLI_VERSION) {
core.info(Utils.LATEST_FROM_REMOTE_INFO);
}
}
// Get Config Tokens created on your local machine using JFrog CLI.
// The Tokens configured with JF_ENV_ environment variables.
static getConfigTokens() {
Expand Down Expand Up @@ -470,7 +481,7 @@ class Utils {
}
/**
* 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.
* 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.
*/
static setupPackageAliasIfRequested() {
Expand Down Expand Up @@ -513,8 +524,8 @@ class Utils {
return;
}
const aliasBinDir = Utils.getPackageAliasBinDir();
(0, fs_1.appendFileSync)(githubPath, aliasBinDir + '\n');
core.info('Package aliases installed and "' + aliasBinDir + '" appended to GITHUB_PATH.');
core.addPath(aliasBinDir);
Comment thread
sverdlov93 marked this conversation as resolved.
core.info('Package aliases installed and "' + aliasBinDir + '" added to PATH.');
});
}
}
Expand All @@ -534,6 +545,8 @@ 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]';
// Logged when version=latest is downloaded through a remote repository, which caches the literal [RELEASE] path
Utils.LATEST_FROM_REMOTE_INFO = 'download-repository is set with version=latest, so the CLI is requested from the literal path v2/[RELEASE] instead of a version number. If that repository stores artifacts locally, the binary cached under this path can keep being served on later runs. Use a concrete version to download from a per-version path, or omit download-repository if the runner can reach releases.jfrog.io.';
// 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
Expand Down
6 changes: 4 additions & 2 deletions src/cleanup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import * as core from '@actions/core';
import { existsSync } from 'fs';
import { join } from 'path';
import { Utils } from './utils';
import { JobSummary } from './job-summary';
import { collectEvidences } from './evidence-collection';
Expand Down Expand Up @@ -105,8 +107,8 @@ async function collectAndPublishBuildInfoIfNeeded() {
// 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');
const gitDir: string = require('path').join(workingDirectory, '.git');
if (require('fs').existsSync(gitDir)) {
const gitDir: string = join(workingDirectory, '.git');
if (existsSync(gitDir)) {
await Utils.runCli(['rt', 'build-add-git'], { cwd: workingDirectory });
} else {
core.info('No .git directory found. Skipping Git information collection.');
Expand Down
1 change: 0 additions & 1 deletion src/evidence-collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as core from '@actions/core';
import { Utils } from './utils';
import { HttpClient, HttpClientResponse } from '@actions/http-client';
import { OutgoingHttpHeaders } from 'http';
import { JfrogCredentials } from './types';
import { promises as fs } from 'fs';
import * as path from 'path';

Expand Down
3 changes: 0 additions & 3 deletions src/job-summary.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import * as core from '@actions/core';
import { gte } from 'semver';
import { Octokit } from '@octokit/core';
import { OctokitResponse } from '@octokit/types/dist-types/OctokitResponse';
import * as github from '@actions/github';
import { promisify } from 'util';
import { gzip } from 'zlib';
import path from 'path';
import { existsSync, promises as fs } from 'fs';
import { HttpClient, HttpClientResponse } from '@actions/http-client';
import { OutgoingHttpHeaders } from 'http';
import { tmpdir } from 'os';
import { Utils } from './utils';

Expand Down
15 changes: 15 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export class Utils {
public 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]';
// Logged when version=latest is downloaded through a remote repository, which caches the literal [RELEASE] path
public static readonly LATEST_FROM_REMOTE_INFO: string =
'download-repository is set with version=latest, so the CLI is requested from the literal path v2/[RELEASE] instead of a version number. If that repository stores artifacts locally, the binary cached under this path can keep being served on later runs. Use a concrete version to download from a per-version path, or omit download-repository if the runner can reach releases.jfrog.io.';
// 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
Expand Down Expand Up @@ -108,6 +111,7 @@ export class Utils {
let version: string = core.getInput(Utils.CLI_VERSION_ARG);
let cliRemote: string = core.getInput(Utils.CLI_REMOTE_ARG);
const isLatestVer: boolean = version === Utils.LATEST_CLI_VERSION;
Utils.logIfLatestDownloadedFromRemote(version, cliRemote);

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);
Expand Down Expand Up @@ -199,6 +203,17 @@ export class Utils {
return `${artifactoryUrl}/${downloadDetails.repository}/v${major}/${version}/${architecture}/${fileName}`;
}

/**
* Log when latest is resolved through an Artifactory repository.
* [RELEASE] is part of the artifact path, so a remote repository caches it like any other file
* and keeps serving the first version it resolved. Pin a concrete version instead.
*/
public static logIfLatestDownloadedFromRemote(version: string, cliRemote: string): void {
if (cliRemote && version === Utils.LATEST_CLI_VERSION) {
core.info(Utils.LATEST_FROM_REMOTE_INFO);
}
}

// Get Config Tokens created on your local machine using JFrog CLI.
// The Tokens configured with JF_ENV_ environment variables.
public static getConfigTokens(): Set<string> {
Expand Down
135 changes: 135 additions & 0 deletions test/main.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as os from 'os';
import * as path from 'path';
import * as core from '@actions/core';
import * as exec from '@actions/exec';
import * as toolCache from '@actions/tool-cache';
import { existsSync, unlinkSync } from 'fs';

import { Utils } from '../src/utils';
Expand All @@ -10,6 +11,7 @@ import { DownloadDetails, JfrogCredentials } from '../src/types';
jest.mock('os');
jest.mock('@actions/exec');
jest.mock('@actions/core');
jest.mock('@actions/tool-cache');

const DEFAULT_CLI_URL: string = 'https://releases.jfrog.io/artifactory/jfrog-cli/';
const CUSTOM_CLI_URL: string = 'http://127.0.0.1:8081/artifactory/jfrog-cli-remote/';
Expand Down Expand Up @@ -259,6 +261,139 @@ describe('JFrog CLI V2 URL Tests', () => {
});
});

describe('JFrog CLI latest URL uses literal [RELEASE]', () => {
const myOs: jest.Mocked<typeof os> = os as any;

beforeEach(() => {
myOs.platform.mockImplementation(() => 'linux');
myOs.arch.mockImplementation(() => 'amd64');
});

test('Public download URL keeps [RELEASE] instead of a version number', () => {
const cliUrl: string = Utils.getCliUrl(Utils.LATEST_CLI_VERSION, 'jfrog', Utils.DEFAULT_DOWNLOAD_DETAILS);
expect(cliUrl).toBe(DEFAULT_CLI_URL + 'v2/[RELEASE]/jfrog-cli-linux-amd64/jfrog');
expect(cliUrl).not.toMatch(/v2\/\d+\.\d+\.\d+\//);
});

test('download-repository URL still uses the literal [RELEASE] path', () => {
process.env.JF_ENV_LOCAL = V2_CONFIG;
const cliUrl: string = Utils.getCliUrl(
Utils.LATEST_CLI_VERSION,
'jfrog',
Utils.extractDownloadDetails('jfrog-cli-remote', {} as JfrogCredentials),
);
expect(cliUrl).toBe(CUSTOM_CLI_URL + 'v2/[RELEASE]/jfrog-cli-linux-amd64/jfrog');
expect(cliUrl).toContain('/v2/[RELEASE]/');
expect(cliUrl).not.toMatch(/v2\/\d+\.\d+\.\d+\//);
});
});

describe('logIfLatestDownloadedFromRemote', () => {
beforeEach(() => {
(core.info as jest.Mock).mockClear();
});

test('Logs info when latest is downloaded from an Artifactory repository', () => {
Utils.logIfLatestDownloadedFromRemote(Utils.LATEST_CLI_VERSION, 'jfrog-cli-remote');
expect(core.info).toHaveBeenCalledWith(Utils.LATEST_FROM_REMOTE_INFO);
});

test('Does not log when version is pinned', () => {
Utils.logIfLatestDownloadedFromRemote('2.91.0', 'jfrog-cli-remote');
expect(core.info).not.toHaveBeenCalled();
});

test('Does not log when download-repository is unset', () => {
Utils.logIfLatestDownloadedFromRemote(Utils.LATEST_CLI_VERSION, '');
expect(core.info).not.toHaveBeenCalled();
});

test('Does not log when version is Latest with different casing', () => {
Utils.logIfLatestDownloadedFromRemote('Latest', 'jfrog-cli-remote');
expect(core.info).not.toHaveBeenCalled();
});
});

describe('getAndAddCliToPath latest + download-repository', () => {
const myCore: jest.Mocked<typeof core> = core as any;
const myOs: jest.Mocked<typeof os> = os as any;
const myToolCache: jest.Mocked<typeof toolCache> = toolCache as any;

beforeEach(() => {
myCore.info.mockClear();
myOs.platform.mockImplementation(() => 'linux');
myOs.arch.mockImplementation(() => 'amd64');
myToolCache.downloadTool.mockResolvedValue('/tmp/downloaded-jf');
jest.spyOn(Utils, 'cacheAndAddPath').mockResolvedValue();
});

afterEach(() => {
jest.spyOn(Utils, 'cacheAndAddPath').mockRestore();
});

test('logs and downloads the literal [RELEASE] URL when latest and download-repository are set', async () => {
myCore.getInput.mockImplementation((name: string) => {
if (name === Utils.CLI_VERSION_ARG) {
return Utils.LATEST_CLI_VERSION;
}
if (name === Utils.CLI_REMOTE_ARG) {
return 'jfrog-cli-remote';
}
return '';
});
process.env.JF_ENV_LOCAL = V2_CONFIG;

await Utils.getAndAddCliToPath({} as JfrogCredentials);

expect(myCore.info).toHaveBeenCalledWith(Utils.LATEST_FROM_REMOTE_INFO);
expect(myToolCache.downloadTool).toHaveBeenCalledWith(
CUSTOM_CLI_URL + 'v2/[RELEASE]/jfrog-cli-linux-amd64/jfrog',
undefined,
'Basic YWRtaW46cGFzc3dvcmQ=',
);
});

test('does not log when version is latest but download-repository is empty', async () => {
myCore.getInput.mockImplementation((name: string) => {
if (name === Utils.CLI_VERSION_ARG) {
return Utils.LATEST_CLI_VERSION;
}
if (name === Utils.CLI_REMOTE_ARG) {
return '';
}
return '';
});

await Utils.getAndAddCliToPath({} as JfrogCredentials);

expect(myCore.info).not.toHaveBeenCalledWith(Utils.LATEST_FROM_REMOTE_INFO);
expect(myToolCache.downloadTool).toHaveBeenCalledWith(DEFAULT_CLI_URL + 'v2/[RELEASE]/jfrog-cli-linux-amd64/jfrog', undefined, undefined);
});

test('does not log when version is pinned and download-repository is set', async () => {
myCore.getInput.mockImplementation((name: string) => {
if (name === Utils.CLI_VERSION_ARG) {
return '2.91.0';
}
if (name === Utils.CLI_REMOTE_ARG) {
return 'jfrog-cli-remote';
}
return '';
});
process.env.JF_ENV_LOCAL = V2_CONFIG;
myToolCache.find.mockReturnValue('');

await Utils.getAndAddCliToPath({} as JfrogCredentials);

expect(myCore.info).not.toHaveBeenCalledWith(Utils.LATEST_FROM_REMOTE_INFO);
expect(myToolCache.downloadTool).toHaveBeenCalledWith(
CUSTOM_CLI_URL + 'v2/2.91.0/jfrog-cli-linux-amd64/jfrog',
undefined,
'Basic YWRtaW46cGFzc3dvcmQ=',
);
});
});

test('Extract download details Tests', () => {
for (let config of [V1_CONFIG, V2_CONFIG]) {
process.env.JF_ENV_LOCAL = config;
Expand Down
Loading