Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
84 changes: 79 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- [Automatic installation from release.jfrog.io](#automatic-installation-from-releasejfrogio)
- [Automatic installation from Artifactory](#automatic-installation-from-artifactory)
- [Manual installation](#manual-installation)
- [CLI Installation Behavior](#cli-installation-behavior)
- [Using JFrog CLI in your pipeline jobs](#using-jfrog-cli-in-your-pipeline-jobs)
- [Setting the build name and build number](#setting-the-build-name-and-the-build-number)
- [Using multiple JFrog Platform instances](#using-multiple-jfrog-platform-instances)
Expand Down Expand Up @@ -89,6 +90,42 @@ as shown in the below screenshot.

<img src="images/readme/manual-installation.png" width="30%">

### CLI Installation Behavior

The plugin uses an intelligent installation strategy that handles various scenarios gracefully:

#### Fresh Installation
When JFrog CLI is not yet installed on an agent:
- The plugin acquires an installation lock to coordinate parallel pipeline steps
- If another step is already installing, subsequent steps wait (up to 5 minutes)
- This ensures reliable installation even with parallel pipeline execution

#### Upgrade Behavior
When an existing JFrog CLI installation is detected:
- The plugin checks if an upgrade is available (via SHA256 comparison)
- If an upgrade is needed and the binary is **not in use**, it's replaced with the new version
- If the binary is **currently in use** by another process (common on Windows with parallel steps):
- The upgrade is gracefully skipped
- The existing CLI version is used for the current build
- A warning is logged: *"Upgrade skipped. Using existing version. Upgrade will be attempted in next build."*
- The next build will automatically attempt the upgrade again

This behavior ensures:
- **Build reliability**: Builds don't fail due to file locking conflicts on Windows
- **Automatic recovery**: Upgrades are deferred, not lost
- **Parallel step safety**: Multiple parallel steps can safely use the CLI without conflicts

#### Windows Considerations
On Windows agents, file locking can occur when:
- Multiple parallel pipeline steps attempt to install/upgrade the CLI simultaneously
- Antivirus software scans the binary during installation
- The CLI is actively being executed by another step

The plugin handles these scenarios gracefully by:
1. Using file-based locking for installation coordination
2. Detecting Windows file locking errors
3. Falling back to existing versions when upgrades can't proceed

## Using JFrog CLI in your pipeline jobs

To have your pipeline jobs run JFrog CLI commands, add the following to your pipeline script.
Expand Down Expand Up @@ -224,20 +261,34 @@ echo "JFrog CLI version output: $version"

The Jenkins JFrog Plugin also supports Freestyle jobs through the "Run JFrog CLI" build step. This allows you to execute JFrog CLI commands without using Pipeline syntax.

### Prerequisites
### Setting up the Build Environment (Recommended for Freestyle jobs)

To make JFrog CLI available to **all** build steps in your Freestyle job (including shell scripts), use the Build Environment wrapper:

1. In your Freestyle job configuration, scroll to the **Build Environment** section
2. Check **"Set up JFrog CLI environment"**
3. Select your JFrog CLI installation from the dropdown

This sets the `JFROG_BINARY_PATH` environment variable for the entire build, making the `jf` command available to:
- All "Run JFrog CLI" build steps (without needing to select an installation in each step)
- Shell/Batch build steps
- Any other build steps that use the environment

Before using the Freestyle job support, make sure you have completed the plugin setup:
**Benefits:**
- Configure the CLI installation once, use it everywhere
- Shell scripts can use `$JFROG_BINARY_PATH/jf` or add it to PATH
- Consistent environment across all build steps

1. **Configure JFrog CLI as a tool** (see [Configuring JFrog CLI as a Tool](#configuring-jfrog-cli-as-a-tool))
2. **Configure your JFrog Platform instance** (see [Installing and configuring the plugin](#installing-and-configuring-the-plugin))
> **Note:** This Build Environment option is only available for Freestyle jobs, not Matrix jobs. Matrix jobs run across multiple nodes with potentially different environments. For Matrix jobs, use individual "Run JFrog CLI" build steps with an automatic installation configured (from releases.jfrog.io or Artifactory), which will download the CLI to each node as needed.

### Adding the build step

1. Create a new Freestyle job or open an existing one
2. In the Build section, click "Add build step"
3. Select "Run JFrog CLI" from the dropdown menu
4. **Select JFrog CLI Installation** from the dropdown (choose the installation you configured in Global Tool Configuration)
- If you leave it as "(Use JFrog CLI from system PATH)", it will try to use `jf` from your system PATH
- **Recommended:** Select a configured installation (e.g., one set up with automatic download from releases.jfrog.io)
- If you select "(Use pre-installed JFrog CLI from system PATH)", the `jf` command must already be manually installed and available in the PATH on your Jenkins agent. This option will fail if JFrog CLI is not pre-installed.
5. Enter your JFrog CLI command in the "JFrog CLI Command" field (must start with `jf` or `jfrog`)

### Example Freestyle job configuration
Expand Down Expand Up @@ -281,10 +332,33 @@ jfrog docker push my-image:latest
jfrog mvn clean install
```

### Publishing Build Info

The plugin automatically collects build information when you run JFrog CLI commands like `jf rt upload`, `jf mvn`, etc. To publish this collected build info to Artifactory, you have two options:

#### Option 1: Post-build Action (Recommended)

Add the **"Publish JFrog Build Info"** post-build action to automatically publish build info after your build completes:

1. In your Freestyle job configuration, scroll to **Post-build Actions**
2. Click **"Add post-build action"** and select **"Publish JFrog Build Info"**
3. Select your JFrog CLI installation (or use the one from Build Environment wrapper)
4. Optionally check **"Publish only on success"** to skip publishing on failed builds

This ensures build info is always published without needing to add an explicit `jf rt bp` command.

#### Option 2: Manual Command

Add a "Run JFrog CLI" build step with the command:
```
jf rt bp
```

### Notes for Freestyle jobs

- Commands must start with either `jf` or `jfrog` (e.g., `jf rt ping` or `jfrog rt ping`)
- The plugin automatically sets `JFROG_CLI_BUILD_NAME` and `JFROG_CLI_BUILD_NUMBER` environment variables
- Build info is automatically collected during `jf rt upload`, `jf mvn`, `jf gradle`, etc.
- Make sure JFrog CLI is configured as a tool in Jenkins (Manage Jenkins → Global Tool Configuration)
- The JFrog Platform instance must be configured in Jenkins (Manage Jenkins → Configure System)

Expand Down
63 changes: 45 additions & 18 deletions src/main/java/io/jenkins/plugins/jfrog/BinaryInstaller.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;

import static org.jfrog.build.client.DownloadResponse.SHA256_HEADER_NAME;

/**
* Installer for JFrog CLI binary.
*
Expand Down Expand Up @@ -92,7 +94,8 @@ public static FilePath performJfrogCliInstallation(FilePath toolLocation, TaskLi
JFrogPlatformInstance instance, String repository, String binaryName)
throws IOException, InterruptedException {

// Create unique lock key for this node + installation path + version combination
// Create unique lock key for this node + installation path combination.
// Version is intentionally excluded to serialize all writes to the same binary path.
String lockKey = createLockKey(toolLocation, binaryName, version);

// Get or create synchronization lock for this specific installation location
Expand All @@ -107,10 +110,11 @@ public static FilePath performJfrogCliInstallation(FilePath toolLocation, TaskLi
try {
// Check if CLI already exists and is the correct version
FilePath cliPath = toolLocation.child(binaryName);
if (isValidCliInstallation(cliPath, log) && isCorrectVersion(toolLocation, instance, repository, version, log)) {
boolean validCliExists = isValidCliInstallation(cliPath, log);
if (validCliExists && isCorrectVersion(toolLocation, instance, repository, version, binaryName, log)) {
log.getLogger().println("[BinaryInstaller] CLI already installed and up-to-date, skipping download");
return toolLocation;
} else if (isValidCliInstallation(cliPath, log)) {
} else if (validCliExists) {
log.getLogger().println("[BinaryInstaller] CLI exists but version mismatch detected, proceeding with upgrade");
} else {
log.getLogger().println("[BinaryInstaller] No valid CLI installation found, proceeding with fresh installation");
Expand All @@ -135,21 +139,20 @@ public static FilePath performJfrogCliInstallation(FilePath toolLocation, TaskLi
}

/**
* Creates a unique lock key for the installation location and version.
* Including version in the key ensures different versions can be installed concurrently
* and prevents race conditions during version upgrades.
*
* Creates a unique lock key for the installation location.
* Version is excluded so all operations targeting the same binary path are serialized.
*
* @param toolLocation Installation directory
* @param binaryName Binary file name
* @param version CLI version being installed
* @param version CLI version being installed (unused, kept for signature compatibility)
* @return Unique lock key string
*/
private static String createLockKey(FilePath toolLocation, String binaryName, String version) {
try {
return toolLocation.getRemote() + "/" + binaryName + "/" + version;
return toolLocation.getRemote() + "/" + binaryName;
} catch (Exception e) {
// Fallback to a simpler key if remote path access fails
return toolLocation.toString() + "/" + binaryName + "/" + version;
return "unknown-tool-location/" + binaryName;
}
}

Expand All @@ -165,7 +168,7 @@ private static boolean isValidCliInstallation(FilePath cliPath, TaskListener log
if (cliPath.exists()) {
// Check if file is executable and has reasonable size (> 1MB)
long fileSize = cliPath.length();
if (fileSize > 1024 * 1024) { // > 1MB
if (fileSize > 1024 * 1024 && isExecutable(cliPath)) { // > 1MB
log.getLogger().println("[BinaryInstaller] Found existing CLI: " + cliPath.getRemote() +
" (size: " + (fileSize / 1024 / 1024) + "MB)");
return true;
Expand All @@ -176,6 +179,26 @@ private static boolean isValidCliInstallation(FilePath cliPath, TaskListener log
}
return false;
}

/**
* Verify the CLI file is executable on the target node.
* On Windows, treat .exe files as executable.
*/
private static boolean isExecutable(FilePath cliPath) throws IOException, InterruptedException {
return cliPath.act(new MasterToSlaveFileCallable<Boolean>() {
@Override
public Boolean invoke(File file, VirtualChannel channel) {
if (!file.exists() || file.isDirectory()) {
return false;
}
String name = file.getName().toLowerCase();
if (name.endsWith(".exe")) {
return true;
}
return file.canExecute();
}
});
}

/**
* Check if the installed CLI is the correct version by comparing SHA256 hashes.
Expand All @@ -185,19 +208,21 @@ private static boolean isValidCliInstallation(FilePath cliPath, TaskListener log
* @param instance JFrog platform instance for version checking
* @param repository Repository containing the CLI
* @param version Version to check
* @param binaryName Name of the CLI binary (e.g., "jf" on Unix, "jf.exe" on Windows)
* @param log Task listener for logging
* @return true if CLI is the correct version, false otherwise
*/
private static boolean isCorrectVersion(FilePath toolLocation, JFrogPlatformInstance instance,
String repository, String version, TaskListener log) {
String repository, String version, String binaryName, TaskListener log) {
try {
// Use the same logic as shouldDownloadTool() from JFrogCliDownloader
// but do it here to avoid unnecessary JFrogCliDownloader.invoke() calls

JenkinsProxyConfiguration proxyConfiguration = new JenkinsProxyConfiguration();
String cliUrlSuffix = String.format("/%s/v2-jf/%s/jfrog-cli-%s/jf", repository,
// Use binaryName to construct the correct URL suffix (handles Windows jf.exe vs Unix jf)
String cliUrlSuffix = String.format("/%s/v2-jf/%s/jfrog-cli-%s/%s", repository,
StringUtils.defaultIfBlank(version, "[RELEASE]"),
OsUtils.getOsDetails());
OsUtils.getOsDetails(), binaryName);

JenkinsBuildInfoLog buildInfoLog = new JenkinsBuildInfoLog(log);
String artifactoryUrl = instance.inferArtifactoryUrl();
Expand All @@ -214,8 +239,8 @@ private static boolean isCorrectVersion(FilePath toolLocation, JFrogPlatformInst
// Get expected SHA256 from Artifactory
String expectedSha256 = getArtifactSha256(manager, cliUrlSuffix);
if (expectedSha256.isEmpty()) {
log.getLogger().println("[BinaryInstaller] No SHA256 available from server, assuming version check needed");
return false; // If no SHA256, let download process handle it
log.getLogger().println("[BinaryInstaller] No SHA256 available from server, reusing existing valid CLI");
return true;
}

// Check local SHA256 file
Expand All @@ -238,14 +263,16 @@ public Boolean invoke(File f, VirtualChannel channel) throws IOException, Interr
return false; // If version check fails, let download process handle it
}
}

/**
* Get SHA256 hash from Artifactory headers (same logic as in JFrogCliDownloader)
*/
private static String getArtifactSha256(ArtifactoryManager manager, String cliUrlSuffix) throws IOException {
Header[] headers = manager.downloadHeaders(cliUrlSuffix);
for (Header header : headers) {
if (header.getName().equalsIgnoreCase("X-Checksum-Sha256")) {
String headerName = header.getName();
if (headerName.equalsIgnoreCase(SHA256_HEADER_NAME) ||
headerName.equalsIgnoreCase("X-Artifactory-Checksum-Sha256")) {
return header.getValue();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ static void configureCliEnv(EnvVars env, String jfrogHomeTempDir, JFrogCliConfig
}
if (encryptionKey.shouldEncrypt()) {
// Set up a random encryption key to make sure no raw text secrets are stored in the file system
env.putIfAbsent(JFROG_CLI_ENCRYPTION_KEY, encryptionKey.getKeyOrFilePath());
// getKey() returns the actual 32-character encryption key content, not the file path
env.putIfAbsent(JFROG_CLI_ENCRYPTION_KEY, encryptionKey.getKey());
}
}

Expand Down
Loading
Loading