From acfe766b3f6a1b71a9b4ed8adf2a4892d6084e91 Mon Sep 17 00:00:00 2001 From: strangelookingnerd <49242855+strangelookingnerd@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:52:36 +0100 Subject: [PATCH 1/7] Migrate tests to JUnit Jupiter * Migrate annotations and imports * Migrate assertions * Remove public visibility for test classes and methods * Minor code cleanup --- .../plugins/jfrog/AddBuildInfoActionTest.java | 27 ++-- ...tifactoryInstallerCheckCliVersionTest.java | 48 +++---- .../jfrog/CliEnvConfiguratorProxyTest.java | 25 ++-- .../plugins/jfrog/CliEnvConfiguratorTest.java | 36 +++--- .../plugins/jfrog/CreateNoProxyValueTest.java | 59 ++++----- .../io/jenkins/plugins/jfrog/JfStepTest.java | 16 +-- .../JFrogPlatformBuilderTest.java | 12 +- .../JFrogPlatformInstanceTest.java | 12 +- .../JenkinsProxyConfigurationTest.java | 64 +++++----- .../integration/JFrogInstallationITest.java | 118 ++++++++---------- .../jfrog/integration/PipelineTestBase.java | 52 ++++---- .../jfrog/integration/TestRepository.java | 7 +- .../plugins/jfrog/jenkins/EnableJenkins.java | 54 -------- .../jfrog/jenkins/JUnit5JenkinsRule.java | 36 ------ .../jfrog/jenkins/JenkinsExtension.java | 43 ------- 15 files changed, 220 insertions(+), 389 deletions(-) delete mode 100644 src/test/java/io/jenkins/plugins/jfrog/jenkins/EnableJenkins.java delete mode 100644 src/test/java/io/jenkins/plugins/jfrog/jenkins/JUnit5JenkinsRule.java delete mode 100644 src/test/java/io/jenkins/plugins/jfrog/jenkins/JenkinsExtension.java diff --git a/src/test/java/io/jenkins/plugins/jfrog/AddBuildInfoActionTest.java b/src/test/java/io/jenkins/plugins/jfrog/AddBuildInfoActionTest.java index b480d59f..6f733a96 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/AddBuildInfoActionTest.java +++ b/src/test/java/io/jenkins/plugins/jfrog/AddBuildInfoActionTest.java @@ -3,7 +3,6 @@ import hudson.model.Action; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.jfrog.build.api.util.NullLog; -import org.junit.Rule; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -12,9 +11,9 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -22,14 +21,15 @@ import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.*; /** * @author yahavi **/ @ExtendWith(MockitoExtension.class) -public class AddBuildInfoActionTest { +@MockitoSettings(strictness = Strictness.LENIENT) +class AddBuildInfoActionTest { + private static final String RT_BP_OUTPUT = ("13:14:38 [\uD83D\uDD35Info] Deploying build info...\n" + "13:14:40 [\uD83D\uDD35Info] Build info successfully deployed.\n" + "{\n" + @@ -37,16 +37,13 @@ public class AddBuildInfoActionTest { "}"); private static final String EXPECTED_BUILD_INFO_URL = "http://127.0.0.1:8081/ui/builds/test/1/1682417678409/published?buildRepo=artifactory-build-info"; - @Rule - public MockitoRule rule = MockitoJUnit.rule().silent(); - @Captor - ArgumentCaptor valueCapture; + private ArgumentCaptor valueCapture; @Mock - WorkflowRun run; + private WorkflowRun run; - private static Stream positiveDataProvider() { + static Stream positiveDataProvider() { return Stream.of( Arguments.of("rt bp", RT_BP_OUTPUT), Arguments.of("rt build-publish", RT_BP_OUTPUT) @@ -55,16 +52,16 @@ private static Stream positiveDataProvider() { @ParameterizedTest @MethodSource("positiveDataProvider") - public void addBuildInfoActionPositiveTest(String command, String output) throws IOException { + void addBuildInfoActionPositiveTest(String command, String output) throws IOException { doNothing().when(run).addAction(valueCapture.capture()); runCliCommand(command, output); Mockito.verify(run, times(1)).addAction(isA(Action.class)); assertEquals(1, valueCapture.getAllValues().size()); - assertEquals(valueCapture.getValue().getUrlName(), EXPECTED_BUILD_INFO_URL); + assertEquals(EXPECTED_BUILD_INFO_URL, valueCapture.getValue().getUrlName()); } - private static Stream negativeDataProvider() { + static Stream negativeDataProvider() { return Stream.of( Arguments.of("rt u a b", RT_BP_OUTPUT), Arguments.of("rt bp", "{ \"a\": \"b\" }"), @@ -75,7 +72,7 @@ private static Stream negativeDataProvider() { @ParameterizedTest @MethodSource("negativeDataProvider") - public void addBuildInfoActionNegativeTest(String command, String output) throws IOException { + void addBuildInfoActionNegativeTest(String command, String output) throws IOException { runCliCommand(command, output); Mockito.verify(run, never()).addAction(isA(Action.class)); } diff --git a/src/test/java/io/jenkins/plugins/jfrog/ArtifactoryInstallerCheckCliVersionTest.java b/src/test/java/io/jenkins/plugins/jfrog/ArtifactoryInstallerCheckCliVersionTest.java index 05503eca..00def8d0 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/ArtifactoryInstallerCheckCliVersionTest.java +++ b/src/test/java/io/jenkins/plugins/jfrog/ArtifactoryInstallerCheckCliVersionTest.java @@ -1,49 +1,41 @@ package io.jenkins.plugins.jfrog; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; -import java.util.Arrays; -import java.util.Collection; +import java.util.stream.Stream; import static io.jenkins.plugins.jfrog.ArtifactoryInstaller.*; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * @author yahavi **/ -@RunWith(Parameterized.class) -public class ArtifactoryInstallerCheckCliVersionTest { - private final String inputVersion; - private final String expectedResult; - - public ArtifactoryInstallerCheckCliVersionTest(String inputVersion, String expectedResult) { - this.inputVersion = inputVersion; - this.expectedResult = expectedResult; - } - @Parameterized.Parameters - public static Collection dataProvider() { - return Arrays.asList( +class ArtifactoryInstallerCheckCliVersionTest { + + static Stream dataProvider() { + return Stream.of( // Positive tests - new Object[]{"", null}, - new Object[]{"2.6.1", null}, - new Object[]{"2.7.0", null}, + Arguments.of("", null), + Arguments.of("2.6.1", null), + Arguments.of("2.7.0", null), // Bad syntax - new Object[]{"bad version", BAD_VERSION_PATTERN_ERROR}, - new Object[]{"1.2", BAD_VERSION_PATTERN_ERROR}, - new Object[]{"1.2.a", BAD_VERSION_PATTERN_ERROR}, + Arguments.of("bad version", BAD_VERSION_PATTERN_ERROR), + Arguments.of("1.2", BAD_VERSION_PATTERN_ERROR), + Arguments.of("1.2.a", BAD_VERSION_PATTERN_ERROR), // Versions below minimum - new Object[]{"2.5.9", LOW_VERSION_PATTERN_ERROR}, - new Object[]{"2.6.0", LOW_VERSION_PATTERN_ERROR} + Arguments.of("2.5.9", LOW_VERSION_PATTERN_ERROR), + Arguments.of("2.6.0", LOW_VERSION_PATTERN_ERROR) ); } - @Test - public void testValidateCliVersion() { + @ParameterizedTest + @MethodSource("dataProvider") + void testValidateCliVersion(String inputVersion, String expectedResult) { assertEquals(expectedResult, validateCliVersion(inputVersion).getMessage()); } } diff --git a/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorProxyTest.java b/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorProxyTest.java index 8cfeca91..24322de9 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorProxyTest.java +++ b/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorProxyTest.java @@ -1,28 +1,27 @@ package io.jenkins.plugins.jfrog; import io.jenkins.plugins.jfrog.configuration.JenkinsProxyConfiguration; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import static io.jenkins.plugins.jfrog.CliEnvConfigurator.*; -import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.assertNull; /** * @author yahavi **/ @SuppressWarnings("HttpUrlsUsage") -public class CliEnvConfiguratorProxyTest extends CliEnvConfiguratorTest { +class CliEnvConfiguratorProxyTest extends CliEnvConfiguratorTest { - @Before - public void setUp() { - super.setUp(); + @BeforeEach + void beforeEach() { proxyConfiguration = new JenkinsProxyConfiguration(); proxyConfiguration.host = "acme.proxy.io"; } @Test - public void configureCliEnvHttpProxyTest() { + void configureCliEnvHttpProxyTest() { proxyConfiguration.port = 80; invokeConfigureCliEnv(); assertEnv(envVars, HTTP_PROXY_ENV, "http://acme.proxy.io:80"); @@ -31,7 +30,7 @@ public void configureCliEnvHttpProxyTest() { } @Test - public void configureCliEnvHttpsProxyTest() { + void configureCliEnvHttpsProxyTest() { proxyConfiguration.port = 443; invokeConfigureCliEnv(); assertEnv(envVars, HTTP_PROXY_ENV, "https://acme.proxy.io:443"); @@ -40,7 +39,7 @@ public void configureCliEnvHttpsProxyTest() { } @Test - public void configureCliEnvHttpProxyAuthTest() { + void configureCliEnvHttpProxyAuthTest() { proxyConfiguration.port = 80; proxyConfiguration.username = "andor"; proxyConfiguration.password = "RogueOne"; @@ -51,7 +50,7 @@ public void configureCliEnvHttpProxyAuthTest() { } @Test - public void configureCliEnvHttpsProxyAuthTest() { + void configureCliEnvHttpsProxyAuthTest() { proxyConfiguration.port = 443; proxyConfiguration.username = "andor"; proxyConfiguration.password = "RogueOne"; @@ -62,14 +61,14 @@ public void configureCliEnvHttpsProxyAuthTest() { } @Test - public void configureCliEnvNoOverrideHttpTest() { + void configureCliEnvNoOverrideHttpTest() { envVars.put(HTTP_PROXY_ENV, "http://acme2.proxy.io:777"); invokeConfigureCliEnv(); assertEnv(envVars, HTTP_PROXY_ENV, "http://acme2.proxy.io:777"); } @Test - public void configureCliEnvNoOverrideTest() { + void configureCliEnvNoOverrideTest() { envVars.put(HTTP_PROXY_ENV, "http://acme2.proxy.io:80"); envVars.put(HTTPS_PROXY_ENV, "http://acme2.proxy.io:443"); invokeConfigureCliEnv(); diff --git a/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorTest.java b/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorTest.java index 14f000d8..b44f3d0e 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorTest.java +++ b/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorTest.java @@ -4,26 +4,28 @@ import io.jenkins.plugins.jfrog.actions.JFrogCliConfigEncryption; import io.jenkins.plugins.jfrog.configuration.JenkinsProxyConfiguration; import jenkins.model.Jenkins; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import static io.jenkins.plugins.jfrog.CliEnvConfigurator.*; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; /** * @author yahavi **/ -public class CliEnvConfiguratorTest { - @Rule - public JenkinsRule jenkinsRule = new JenkinsRule(); - JenkinsProxyConfiguration proxyConfiguration; - EnvVars envVars; +@WithJenkins +class CliEnvConfiguratorTest { - @Before - public void setUp() { + protected JenkinsRule jenkinsRule; + protected JenkinsProxyConfiguration proxyConfiguration; + protected EnvVars envVars; + + @BeforeEach + void beforeEach(JenkinsRule rule) { + jenkinsRule = rule; envVars = new EnvVars(); envVars.put("JOB_NAME", "buildName"); envVars.put("BUILD_NUMBER", "1"); @@ -31,7 +33,7 @@ public void setUp() { } @Test - public void configureCliEnvBasicTest() { + void configureCliEnvBasicTest() { invokeConfigureCliEnv("a/b/c", new JFrogCliConfigEncryption(envVars)); assertEnv(envVars, JFROG_CLI_BUILD_NAME, "buildName"); assertEnv(envVars, JFROG_CLI_BUILD_NUMBER, "1"); @@ -40,7 +42,7 @@ public void configureCliEnvBasicTest() { } @Test - public void configEncryptionTest() { + void configEncryptionTest() { JFrogCliConfigEncryption configEncryption = new JFrogCliConfigEncryption(envVars); assertTrue(configEncryption.shouldEncrypt()); assertEquals(32, configEncryption.getKey().length()); @@ -50,7 +52,7 @@ public void configEncryptionTest() { } @Test - public void configEncryptionWithHomeDirTest() { + void configEncryptionWithHomeDirTest() { // Config JFROG_CLI_HOME_DIR to disable key encryption envVars.put(JFROG_CLI_HOME_DIR, "/a/b/c"); JFrogCliConfigEncryption configEncryption = new JFrogCliConfigEncryption(envVars); @@ -60,15 +62,15 @@ public void configEncryptionWithHomeDirTest() { assertFalse(envVars.containsKey(JFROG_CLI_ENCRYPTION_KEY)); } - void assertEnv(EnvVars envVars, String key, String expectedValue) { + protected static void assertEnv(EnvVars envVars, String key, String expectedValue) { assertEquals(expectedValue, envVars.get(key)); } - void invokeConfigureCliEnv() { + protected void invokeConfigureCliEnv() { this.invokeConfigureCliEnv("", new JFrogCliConfigEncryption(envVars)); } - void invokeConfigureCliEnv(String jfrogHomeTempDir, JFrogCliConfigEncryption configEncryption) { + protected void invokeConfigureCliEnv(String jfrogHomeTempDir, JFrogCliConfigEncryption configEncryption) { setProxyConfiguration(); configureCliEnv(envVars, jfrogHomeTempDir, configEncryption); } diff --git a/src/test/java/io/jenkins/plugins/jfrog/CreateNoProxyValueTest.java b/src/test/java/io/jenkins/plugins/jfrog/CreateNoProxyValueTest.java index 4bd6ec41..d76d2778 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/CreateNoProxyValueTest.java +++ b/src/test/java/io/jenkins/plugins/jfrog/CreateNoProxyValueTest.java @@ -1,50 +1,41 @@ package io.jenkins.plugins.jfrog; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; -import java.util.Arrays; -import java.util.Collection; +import java.util.stream.Stream; import static io.jenkins.plugins.jfrog.CliEnvConfigurator.createNoProxyValue; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * @author nathana **/ -@RunWith(Parameterized.class) -public class CreateNoProxyValueTest { - private final String noProxy; - private final String expectedResult; +class CreateNoProxyValueTest { - public CreateNoProxyValueTest(String noProxy, String expectedResult) { - this.noProxy = noProxy; - this.expectedResult = expectedResult; - } - - @Parameterized.Parameters - public static Collection dataProvider() { - return Arrays.asList( - new Object[]{"artifactory.jfrog.io", "artifactory.jfrog.io"}, - new Object[]{"artifactory.jfrog.io \n artifactory1.jfrog.io ", "artifactory.jfrog.io,artifactory1.jfrog.io"}, - new Object[]{" artifactory.jfrog.io \n \r artifactory1.jfrog.io;artifactory2.jfrog.io \n artifactory3.jfrog.io | artifactory4.jfrog.io \n artifactory5.jfrog.io ", "artifactory.jfrog.io,artifactory1.jfrog.io,artifactory2.jfrog.io,artifactory3.jfrog.io,artifactory4.jfrog.io,artifactory5.jfrog.io"}, - new Object[]{"\r\n", ""}, - new Object[]{";;;", ""}, - new Object[]{",,,", ""}, - new Object[]{"artifactory.jfrog.io;", "artifactory.jfrog.io"}, - new Object[]{"artifactory.jfrog.io,artifactory1.jfrog.io", "artifactory.jfrog.io,artifactory1.jfrog.io"}, - new Object[]{"artifactory.jfrog.io;artifactory1.jfrog.io;artifactory2.jfrog.io;artifactory3.jfrog.io", "artifactory.jfrog.io,artifactory1.jfrog.io,artifactory2.jfrog.io,artifactory3.jfrog.io"}, - new Object[]{"artifactory.jfrog.io|artifactory1.jfrog.io|artifactory2.jfrog.io|artifactory3.jfrog.io", "artifactory.jfrog.io,artifactory1.jfrog.io,artifactory2.jfrog.io,artifactory3.jfrog.io"}, - new Object[]{"artifactory.jfrog.io\nartifactory1.jfrog.io", "artifactory.jfrog.io,artifactory1.jfrog.io"}, - new Object[]{"artifactory.jfrog.io \nartifactory1.jfrog.io\nartifactory2.jfrog.io \n artifactory3.jfrog.io", "artifactory.jfrog.io,artifactory1.jfrog.io,artifactory2.jfrog.io,artifactory3.jfrog.io"}, - new Object[]{";artifactory.jfrog.io;", "artifactory.jfrog.io"}, - new Object[]{",artifactory.jfrog.io,", "artifactory.jfrog.io"} + static Stream dataProvider() { + return Stream.of( + Arguments.of("artifactory.jfrog.io", "artifactory.jfrog.io"), + Arguments.of("artifactory.jfrog.io \n artifactory1.jfrog.io ", "artifactory.jfrog.io,artifactory1.jfrog.io"), + Arguments.of(" artifactory.jfrog.io \n \r artifactory1.jfrog.io;artifactory2.jfrog.io \n artifactory3.jfrog.io | artifactory4.jfrog.io \n artifactory5.jfrog.io ", "artifactory.jfrog.io,artifactory1.jfrog.io,artifactory2.jfrog.io,artifactory3.jfrog.io,artifactory4.jfrog.io,artifactory5.jfrog.io"), + Arguments.of("\r\n", ""), + Arguments.of(";;;", ""), + Arguments.of(",,,", ""), + Arguments.of("artifactory.jfrog.io;", "artifactory.jfrog.io"), + Arguments.of("artifactory.jfrog.io,artifactory1.jfrog.io", "artifactory.jfrog.io,artifactory1.jfrog.io"), + Arguments.of("artifactory.jfrog.io;artifactory1.jfrog.io;artifactory2.jfrog.io;artifactory3.jfrog.io", "artifactory.jfrog.io,artifactory1.jfrog.io,artifactory2.jfrog.io,artifactory3.jfrog.io"), + Arguments.of("artifactory.jfrog.io|artifactory1.jfrog.io|artifactory2.jfrog.io|artifactory3.jfrog.io", "artifactory.jfrog.io,artifactory1.jfrog.io,artifactory2.jfrog.io,artifactory3.jfrog.io"), + Arguments.of("artifactory.jfrog.io\nartifactory1.jfrog.io", "artifactory.jfrog.io,artifactory1.jfrog.io"), + Arguments.of("artifactory.jfrog.io \nartifactory1.jfrog.io\nartifactory2.jfrog.io \n artifactory3.jfrog.io", "artifactory.jfrog.io,artifactory1.jfrog.io,artifactory2.jfrog.io,artifactory3.jfrog.io"), + Arguments.of(";artifactory.jfrog.io;", "artifactory.jfrog.io"), + Arguments.of(",artifactory.jfrog.io,", "artifactory.jfrog.io") ); } - @Test - public void createNoProxyValueTest() { + @ParameterizedTest + @MethodSource("dataProvider") + void createNoProxyValueTest(String noProxy, String expectedResult) { assertEquals(expectedResult, createNoProxyValue(noProxy)); } } diff --git a/src/test/java/io/jenkins/plugins/jfrog/JfStepTest.java b/src/test/java/io/jenkins/plugins/jfrog/JfStepTest.java index f8675110..1eb945ad 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/JfStepTest.java +++ b/src/test/java/io/jenkins/plugins/jfrog/JfStepTest.java @@ -8,7 +8,6 @@ import io.jenkins.plugins.jfrog.configuration.CredentialsConfig; import io.jenkins.plugins.jfrog.configuration.JFrogPlatformInstance; import org.jfrog.build.client.Version; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -21,21 +20,22 @@ import static io.jenkins.plugins.jfrog.JfStep.Execution.getJFrogCLIPath; import static io.jenkins.plugins.jfrog.JfStep.MIN_CLI_VERSION_PASSWORD_STDIN; import static io.jenkins.plugins.jfrog.JfrogInstallation.JFROG_BINARY_PATH; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; + /** * @author yahavi **/ -public class JfStepTest { +class JfStepTest { @ParameterizedTest @MethodSource("jfrogCLIPathProvider") void getJFrogCLIPathTest(EnvVars inputEnvVars, boolean isWindows, String expectedOutput) { - Assertions.assertEquals(expectedOutput, getJFrogCLIPath(inputEnvVars, isWindows)); + assertEquals(expectedOutput, getJFrogCLIPath(inputEnvVars, isWindows)); } - private static Stream jfrogCLIPathProvider() { + static Stream jfrogCLIPathProvider() { return Stream.of( // Unix agent Arguments.of(new EnvVars(JFROG_BINARY_PATH, "a/b/c"), false, "a/b/c/jf"), @@ -86,7 +86,7 @@ void getJfrogCliVersionTest() throws IOException, InterruptedException { * Plugin launchers do not support password-stdin, as they do not have access to the standard input by default. * * @param cliVersion The CLI version - * @param isPluginLauncher Whether the launcher is a plugin launcher + * @param envVars The env vars * @param expectedOutput The expected output */ @ParameterizedTest @@ -115,7 +115,7 @@ void testAddCredentialsArguments(String cliVersion, EnvVars envVars, String expe assertTrue(builder.toList().contains(expectedOutput)); } - private static Stream provideTestArguments() { + static Stream provideTestArguments() { String passwordFlag = "--password="; String passwordStdinFlag = "--password-stdin"; EnvVars envVarsTrue = mock(EnvVars.class); diff --git a/src/test/java/io/jenkins/plugins/jfrog/configuration/JFrogPlatformBuilderTest.java b/src/test/java/io/jenkins/plugins/jfrog/configuration/JFrogPlatformBuilderTest.java index 9c339e03..f6a6a99e 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/configuration/JFrogPlatformBuilderTest.java +++ b/src/test/java/io/jenkins/plugins/jfrog/configuration/JFrogPlatformBuilderTest.java @@ -1,18 +1,18 @@ package io.jenkins.plugins.jfrog.configuration; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * @author yahavi **/ @SuppressWarnings("HttpUrlsUsage") -public class JFrogPlatformBuilderTest { +class JFrogPlatformBuilderTest { @Test - public void testIsUnsafe() { + void testIsUnsafe() { assertFalse(JFrogPlatformBuilder.isUnsafe(false, "https://acme.jfrog.io")); assertFalse(JFrogPlatformBuilder.isUnsafe(false, "https://acme.jfrog.io")); assertFalse(JFrogPlatformBuilder.isUnsafe(true, "http://acme.jfrog.io")); @@ -23,7 +23,7 @@ public void testIsUnsafe() { } @Test - public void testIsInvalidProtocolOrEmptyUrl() { + void testIsInvalidProtocolOrEmptyUrl() { assertFalse(JFrogPlatformBuilder.isInvalidProtocolOrEmptyUrl("")); assertFalse(JFrogPlatformBuilder.isInvalidProtocolOrEmptyUrl("http://acme.jfrog.io")); assertFalse(JFrogPlatformBuilder.isInvalidProtocolOrEmptyUrl("https://acme.jfrog.io")); diff --git a/src/test/java/io/jenkins/plugins/jfrog/configuration/JFrogPlatformInstanceTest.java b/src/test/java/io/jenkins/plugins/jfrog/configuration/JFrogPlatformInstanceTest.java index 1f44e62b..acfbb162 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/configuration/JFrogPlatformInstanceTest.java +++ b/src/test/java/io/jenkins/plugins/jfrog/configuration/JFrogPlatformInstanceTest.java @@ -1,13 +1,13 @@ package io.jenkins.plugins.jfrog.configuration; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * @author yahavi **/ -public class JFrogPlatformInstanceTest { +class JFrogPlatformInstanceTest { private static final String DISTRIBUTION_URL = "https://acme.jfrog.io/distribution"; private static final String ARTIFACTORY_URL = "https://acme.jfrog.io/artifactory"; @@ -16,7 +16,7 @@ public class JFrogPlatformInstanceTest { private static final String SERVER_ID = "acme"; @Test - public void testInferArtifactoryUrl() { + void testInferArtifactoryUrl() { // Check that Artifactory URL inferred from the platform URL JFrogPlatformInstance jfrogPlatformInstance = new JFrogPlatformInstance(SERVER_ID, URL, null, "", "", ""); assertEquals("", jfrogPlatformInstance.getArtifactoryUrl()); @@ -29,7 +29,7 @@ public void testInferArtifactoryUrl() { } @Test - public void testInferDistributionUrl() { + void testInferDistributionUrl() { // Check that Distribution URL inferred from the platform URL JFrogPlatformInstance jfrogPlatformInstance = new JFrogPlatformInstance(SERVER_ID, URL, null, "", "", ""); assertEquals("", jfrogPlatformInstance.getDistributionUrl()); @@ -42,7 +42,7 @@ public void testInferDistributionUrl() { } @Test - public void testInferXrayUrl() { + void testInferXrayUrl() { // Check that Xray URL inferred from the platform URL JFrogPlatformInstance jfrogPlatformInstance = new JFrogPlatformInstance(SERVER_ID, URL, null, "", "", ""); assertEquals("", jfrogPlatformInstance.getXrayUrl()); diff --git a/src/test/java/io/jenkins/plugins/jfrog/configuration/JenkinsProxyConfigurationTest.java b/src/test/java/io/jenkins/plugins/jfrog/configuration/JenkinsProxyConfigurationTest.java index f3f5d744..647c235c 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/configuration/JenkinsProxyConfigurationTest.java +++ b/src/test/java/io/jenkins/plugins/jfrog/configuration/JenkinsProxyConfigurationTest.java @@ -2,57 +2,57 @@ import jenkins.model.Jenkins; import org.apache.commons.lang3.StringUtils; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.jvnet.hudson.test.JenkinsRule; -import java.util.Arrays; -import java.util.Collection; +import java.util.stream.Stream; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; -@RunWith(Parameterized.class) -public class JenkinsProxyConfigurationTest { +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; - @Rule - public JenkinsRule jenkinsRule = new JenkinsRule(); +@WithJenkins +class JenkinsProxyConfigurationTest { private final hudson.ProxyConfiguration jenkinsProxyConfiguration = new hudson.ProxyConfiguration("proxy.jfrog.io", 1234); - private final String url; - public JenkinsProxyConfigurationTest(String url) { - this.url = url; + private JenkinsRule jenkinsRule; + + @BeforeEach + void beforeEach(JenkinsRule rule) { + jenkinsRule = rule; } @SuppressWarnings("HttpUrlsUsage") - @Parameterized.Parameters - public static Collection dataProvider() { - return Arrays.asList( + static Stream dataProvider() { + return Stream.of( // HTTP - new Object[]{"http://acme.jfrog.io"}, - new Object[]{"http://acme.jfrog.io/"}, - new Object[]{"http://acme.jfrog.io/artifactory"}, - new Object[]{"http://acme.jfrog.io:8081/artifactory"}, + Arguments.of("http://acme.jfrog.io"), + Arguments.of("http://acme.jfrog.io/"), + Arguments.of("http://acme.jfrog.io/artifactory"), + Arguments.of("http://acme.jfrog.io:8081/artifactory"), // HTTPS - new Object[]{"https://acme.jfrog.io"}, - new Object[]{"https://acme.jfrog.io/"}, - new Object[]{"https://acme.jfrog.io/artifactory"}, - new Object[]{"https://acme.jfrog.io:8081/artifactory"}, + Arguments.of("https://acme.jfrog.io"), + Arguments.of("https://acme.jfrog.io/"), + Arguments.of("https://acme.jfrog.io/artifactory"), + Arguments.of("https://acme.jfrog.io:8081/artifactory"), // SSH - new Object[]{"ssh://acme.jfrog.io"}, - new Object[]{"ssh://acme.jfrog.io/"}, - new Object[]{"ssh://acme.jfrog.io/artifactory"}, - new Object[]{"ssh://acme.jfrog.io:8081/artifactory"} + Arguments.of("ssh://acme.jfrog.io"), + Arguments.of("ssh://acme.jfrog.io/"), + Arguments.of("ssh://acme.jfrog.io/artifactory"), + Arguments.of("ssh://acme.jfrog.io:8081/artifactory") ); } - @Test - public void testShouldBypassProxy() { + @ParameterizedTest + @MethodSource("dataProvider") + void testShouldBypassProxy(String url) { setupProxy("*"); assertTrue(new JenkinsProxyConfiguration().shouldBypassProxy(url)); diff --git a/src/test/java/io/jenkins/plugins/jfrog/integration/JFrogInstallationITest.java b/src/test/java/io/jenkins/plugins/jfrog/integration/JFrogInstallationITest.java index 0dbdfa1d..c437c47b 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/integration/JFrogInstallationITest.java +++ b/src/test/java/io/jenkins/plugins/jfrog/integration/JFrogInstallationITest.java @@ -6,7 +6,6 @@ import org.apache.commons.lang3.StringUtils; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.junit.jupiter.api.Test; -import org.jvnet.hudson.test.JenkinsRule; import java.io.ByteArrayOutputStream; import java.io.File; @@ -21,72 +20,63 @@ class JFrogInstallationITest extends PipelineTestBase { // JFrog CLI version which is accessible for all operating systems. - private static final String jfrogCliTestVersion = "2.29.2"; + private static final String JFROG_CLI_TEST_VERSION = "2.29.2"; /** * Download Jfrog CLI from 'releases.jfrog.io' and adds it as a global tool. - * - * @param jenkins Jenkins instance injected automatically. */ @Test - public void testJFrogCliInstallationFromReleases(JenkinsRule jenkins) throws Exception { - setupJenkins(jenkins); + void testJFrogCliInstallationFromReleases() throws Exception { + setupJenkins(); // Download specific CLI version. - configureJfrogCliFromReleases(jfrogCliTestVersion, true); - WorkflowRun job = runPipeline(jenkins, "basic_version_command"); + configureJfrogCliFromReleases(JFROG_CLI_TEST_VERSION, true); + WorkflowRun job = runPipeline("basic_version_command"); // Verify specific version was installed. - assertTrue(job.getLog().contains("jf version " + jfrogCliTestVersion)); + assertTrue(job.getLog().contains("jf version " + JFROG_CLI_TEST_VERSION)); // Download the latest CLI version. configureJfrogCliFromReleases(StringUtils.EMPTY, true); - job = runPipeline(jenkins, "basic_version_command"); + job = runPipeline("basic_version_command"); // Verify newer version was installed. - assertFalse(job.getLog().contains("jf version " + jfrogCliTestVersion)); + assertFalse(job.getLog().contains("jf version " + JFROG_CLI_TEST_VERSION)); } /** * Download JFrog CLI from client Artifactory and adds it as a global tool. - * - * @param jenkins Jenkins instance injected automatically. */ @Test - public void testJFrogCliInstallationFromArtifactory(JenkinsRule jenkins) throws Exception { - setupJenkins(jenkins); + void testJFrogCliInstallationFromArtifactory() throws Exception { // Download the latest CLI version. // Using remote repository to 'releases.jfrog.io' in the client's Artifactory. configureJfrogCliFromArtifactory(JFROG_CLI_TOOL_NAME_1, TEST_CONFIGURED_SERVER_ID, getRepoKey(TestRepository.CLI_REMOTE_REPO), true); - WorkflowRun job = runPipeline(jenkins, "basic_version_command"); + WorkflowRun job = runPipeline("basic_version_command"); assertTrue(job.getLog().contains("jf version ")); } // Gets JfrogInstallation directory in Jenkins work root. - private Path getJfrogInstallationDir(JenkinsRule jenkins) { + private Path getJfrogInstallationDir() { return jenkins.jenkins.getRootDir().toPath().resolve("tools").resolve("io.jenkins.plugins.jfrog.JfrogInstallation"); } /** * Check that only one copy of JFrog CLI is being downloaded to the expected location when running multiple jobs. - * - * @param jenkins Jenkins instance injected automatically. */ @Test - public void testDownloadingJFrogCliOnce(JenkinsRule jenkins) throws Exception { - initPipelineTest(jenkins); + void testDownloadingJFrogCliOnce() throws Exception { + initPipelineTest(); // After running job for the first time, CLI's binary should be downloaded. - runPipeline(jenkins, "basic_version_command"); - long lastModified = getCliLastModified(jenkins); + runPipeline("basic_version_command"); + long lastModified = getCliLastModified(); // Rerunning the job and verifying that the binary was not downloaded again by comparing the modification time. - runPipeline(jenkins, "basic_version_command"); - assertEquals(lastModified, getCliLastModified(jenkins)); + runPipeline("basic_version_command"); + assertEquals(lastModified, getCliLastModified()); } /** * Finds the binary file in the CLI tool directory and returns its modification time. - * - * @param jenkins Jenkins instance injected automatically. * @return binary's modification time. */ - private long getCliLastModified(JenkinsRule jenkins) { - Path binaryPath = getJfrogInstallationDir(jenkins); + private long getCliLastModified() { + Path binaryPath = getJfrogInstallationDir(); String name = BINARY_NAME; if (!jenkins.createLocalLauncher().isUnix()) { name += ".exe"; @@ -98,26 +88,24 @@ private long getCliLastModified(JenkinsRule jenkins) { /** * Check that only one copy of the Maven extractor is downloaded to the expected 'dependencies' directory. - * - * @param jenkins Jenkins instance injected automatically. */ @Test - public void testDownloadingMavenExtractor(JenkinsRule jenkins) throws Exception { - initPipelineTest(jenkins); + void testDownloadingMavenExtractor() throws Exception { + initPipelineTest(); // After running job for the first time, Mvn extractor should be downloaded. - runPipeline(jenkins, "mvn_command"); - Path mvnDependenciesDirPath = getJfrogInstallationDir(jenkins).resolve(JfrogDependenciesDirName).resolve("maven"); + runPipeline("mvn_command"); + Path mvnDependenciesDirPath = getJfrogInstallationDir().resolve(JfrogDependenciesDirName).resolve("maven"); long lastModified = getExtractorLastModified(mvnDependenciesDirPath); // Rerunning the job and verifying that the extractor was not downloaded again by comparing the modification time. - runPipeline(jenkins, "mvn_command"); + runPipeline("mvn_command"); assertEquals(lastModified, getExtractorLastModified(mvnDependenciesDirPath)); } /** - * Finds the extractor' jar in the mvn extractor directory and returns its modification time. + * Finds the extractor jar in the mvn extractor directory and returns its modification time. * * @param mvnDependenciesDirPath maven's extractors directory inside the dependencies' directory. - * @return extractor' jar modification time. + * @return extractor jar modification time. */ private long getExtractorLastModified(Path mvnDependenciesDirPath) throws IOException { File[] fileList = mvnDependenciesDirPath.toFile().listFiles(); @@ -134,52 +122,46 @@ private long getExtractorLastModified(Path mvnDependenciesDirPath) throws IOExce /** * Configure two JFrog CLI tools, one with Releases installer and one with Artifactory installer, and test functionality for both. - * - * @param jenkins Jenkins instance injected automatically. */ @Test - public void testCombineReleasesAndArtifactoryTools(JenkinsRule jenkins) throws Exception { - setupJenkins(jenkins); + void testCombineReleasesAndArtifactoryTools() throws Exception { + setupJenkins(); // Download the latest CLI version from releases.jfrog.io. configureJfrogCliFromReleases(StringUtils.EMPTY, false); // Download the latest CLI version from Artifactory. configureJfrogCliFromArtifactory(JFROG_CLI_TOOL_NAME_2, TEST_CONFIGURED_SERVER_ID, getRepoKey(TestRepository.CLI_REMOTE_REPO), false); - runPipeline(jenkins, "basic_commands"); - runPipeline(jenkins, "basic_commands_2"); + runPipeline("basic_commands"); + runPipeline("basic_commands_2"); } /** * Configure two JFrog CLI tools, one with Releases installer and one with Artifactory installer, and test functionality for both. - * - * @param jenkins Jenkins instance injected automatically. */ @Test - public void testCombineReleasesAndArtifactoryToolsDifferentOrder(JenkinsRule jenkins) throws Exception { - setupJenkins(jenkins); + void testCombineReleasesAndArtifactoryToolsDifferentOrder() throws Exception { + setupJenkins(); // Download the latest CLI version from Artifactory. configureJfrogCliFromArtifactory(JFROG_CLI_TOOL_NAME_2, TEST_CONFIGURED_SERVER_ID, getRepoKey(TestRepository.CLI_REMOTE_REPO), false); // Download a specific CLI version from releases.jfrog.io. - configureJfrogCliFromReleases(jfrogCliTestVersion, false); + configureJfrogCliFromReleases(JFROG_CLI_TEST_VERSION, false); // Running CLI installed from releases.jfrog.io and verify specific version was installed. - WorkflowRun job = runPipeline(jenkins, "basic_commands"); - assertTrue(job.getLog().contains("jf version " + jfrogCliTestVersion)); + WorkflowRun job = runPipeline("basic_commands"); + assertTrue(job.getLog().contains("jf version " + JFROG_CLI_TEST_VERSION)); // Running CLI installed from Artifactory and verify the latest version was installed. - job = runPipeline(jenkins, "basic_commands_2"); - assertFalse(job.getLog().contains("jf version " + jfrogCliTestVersion)); + job = runPipeline("basic_commands_2"); + assertFalse(job.getLog().contains("jf version " + JFROG_CLI_TEST_VERSION)); } /** * Configure JFrog CLI tool and test functionality for the build-info action. - * - * @param jenkins Jenkins instance injected automatically. */ @Test - public void testBuildInfoAction(JenkinsRule jenkins) throws Exception { - setupJenkins(jenkins); + void testBuildInfoAction() throws Exception { + setupJenkins(); // Download a specific CLI version from releases.jfrog.io. configureJfrogCliFromReleases(Strings.EMPTY, false); // Running CLI installed from releases.jfrog.io and verify specific version was installed. - WorkflowRun job = runPipeline(jenkins, "build_info"); + WorkflowRun job = runPipeline("build_info"); // Assert build info published assertTrue(job.getLog().contains("Build info successfully deployed")); // Check build-info Action @@ -192,26 +174,24 @@ public void testBuildInfoAction(JenkinsRule jenkins) throws Exception { /** * Test the functionality of providing arguments as list: jf (['rt', 'ping']) - * - * @param jenkins Jenkins instance injected automatically. */ @Test - public void testArgList(JenkinsRule jenkins) throws Exception { - setupJenkins(jenkins); + void testArgList() throws Exception { + setupJenkins(); // Download the latest CLI version from releases.jfrog.io. configureJfrogCliFromReleases(StringUtils.EMPTY, false); // Download the latest CLI version from Artifactory. configureJfrogCliFromArtifactory(JFROG_CLI_TOOL_NAME_1, TEST_CONFIGURED_SERVER_ID, getRepoKey(TestRepository.CLI_REMOTE_REPO), false); - runPipeline(jenkins, "basic_commands_array"); + runPipeline("basic_commands_array"); } @Test - public void testConfigurationAsCode(JenkinsRule jenkins) throws Exception { - setupJenkins(jenkins); + void testConfigurationAsCode() throws Exception { + setupJenkins(); // Download the latest CLI version from Artifactory. configureJfrogCliFromArtifactory(JFROG_CLI_TOOL_NAME_2, TEST_CONFIGURED_SERVER_ID, getRepoKey(TestRepository.CLI_REMOTE_REPO), false); // Download a specific CLI version from releases.jfrog.io. - configureJfrogCliFromReleases(jfrogCliTestVersion, false); + configureJfrogCliFromReleases(JFROG_CLI_TEST_VERSION, false); try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { // Export configuration to a local variable @@ -228,13 +208,13 @@ public void testConfigurationAsCode(JenkinsRule jenkins) throws Exception { } @Test - public void testOutput(JenkinsRule jenkins) throws Exception { - setupJenkins(jenkins); + void testOutput() throws Exception { + setupJenkins(); // Download the latest CLI version from releases.jfrog.io. configureJfrogCliFromReleases(StringUtils.EMPTY, false); // Run pipeline that asserts the output is correct - runPipeline(jenkins, "version_output"); + runPipeline("version_output"); } } diff --git a/src/test/java/io/jenkins/plugins/jfrog/integration/PipelineTestBase.java b/src/test/java/io/jenkins/plugins/jfrog/integration/PipelineTestBase.java index d6cb11b4..883cab84 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/integration/PipelineTestBase.java +++ b/src/test/java/io/jenkins/plugins/jfrog/integration/PipelineTestBase.java @@ -22,7 +22,6 @@ import io.jenkins.plugins.jfrog.configuration.CredentialsConfig; import io.jenkins.plugins.jfrog.configuration.JFrogPlatformBuilder; import io.jenkins.plugins.jfrog.configuration.JFrogPlatformInstance; -import io.jenkins.plugins.jfrog.jenkins.EnableJenkins; import jenkins.model.Jenkins; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; @@ -36,9 +35,9 @@ import org.jfrog.artifactory.client.ArtifactoryRequest; import org.jfrog.artifactory.client.ArtifactoryResponse; import org.jfrog.artifactory.client.impl.ArtifactoryRequestImpl; -import org.junit.Assert; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.jvnet.hudson.test.JenkinsRule; import java.io.IOException; @@ -46,39 +45,40 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.*; + +@WithJenkins +class PipelineTestBase { -@EnableJenkins -public class PipelineTestBase { private static long currentTime; private static Artifactory artifactoryClient; - private JenkinsRule jenkins; + protected JenkinsRule jenkins; private Slave slave; private static StringSubstitutor pipelineSubstitution; private static final String SLAVE_LABEL = "TestSlave"; - static final String PLATFORM_URL = System.getenv("JFROG_URL"); + protected static final String PLATFORM_URL = System.getenv("JFROG_URL"); private static final String ARTIFACTORY_URL = StringUtils.removeEnd(PLATFORM_URL, "/") + "/artifactory"; private static final String ARTIFACTORY_USERNAME = System.getenv("JFROG_USERNAME"); private static final String ARTIFACTORY_PASSWORD = System.getenv("JFROG_PASSWORD"); private static final String ACCESS_TOKEN = System.getenv("JFROG_ADMIN_TOKEN"); private static final Path INTEGRATION_BASE_PATH = Paths.get(".").toAbsolutePath().normalize() .resolve(Paths.get("src", "test", "resources", "integration")); - static final String JFROG_CLI_TOOL_NAME_1 = "jfrog-cli"; - static final String JFROG_CLI_TOOL_NAME_2 = "jfrog-cli-2"; - static final String TEST_CONFIGURED_SERVER_ID = "serverId"; + protected static final String JFROG_CLI_TOOL_NAME_1 = "jfrog-cli"; + protected static final String JFROG_CLI_TOOL_NAME_2 = "jfrog-cli-2"; + protected static final String TEST_CONFIGURED_SERVER_ID = "serverId"; // Set up jenkins and configure latest JFrog CLI. - public void initPipelineTest(JenkinsRule jenkins) throws Exception { - setupJenkins(jenkins); + protected void initPipelineTest() throws Exception { + setupJenkins(); // Download the latest CLI version. configureJfrogCliFromReleases(StringUtils.EMPTY, true); } - // Set up test' environment + // Set up test environment @BeforeAll - public static void setupEnvironment() { + static void beforeAll() { currentTime = System.currentTimeMillis(); verifyEnvironment(); createClients(); @@ -87,15 +87,19 @@ public static void setupEnvironment() { Arrays.stream(TestRepository.values()).forEach(PipelineTestBase::createRepo); } + @BeforeEach + void beforeEach(JenkinsRule rule) { + jenkins = rule; + } + @AfterAll - public static void tearDown() { + static void afterAll() { // Remove repositories Arrays.stream(TestRepository.values()).forEach(PipelineTestBase::removeRepo); artifactoryClient.close(); } - public void setupJenkins(JenkinsRule jenkins) throws IOException { - this.jenkins = jenkins; + protected void setupJenkins() throws IOException { createSlave(); setGlobalConfiguration(); } @@ -126,7 +130,7 @@ private void createSlave() { * @param name - Pipeline name from 'jenkins-jfrog-plugin/src/test/resources/integration/pipelines' * @return the Jenkins job */ - WorkflowRun runPipeline(JenkinsRule jenkins, String name) throws Exception { + protected WorkflowRun runPipeline(String name) throws Exception { WorkflowJob project = jenkins.createProject(WorkflowJob.class); FilePath slaveWs = slave.getWorkspaceFor(project); if (slaveWs == null) { @@ -159,7 +163,7 @@ private static void verifyEnvironment() { */ private void setGlobalConfiguration() throws IOException { JFrogPlatformBuilder.DescriptorImpl jfrogBuilder = (JFrogPlatformBuilder.DescriptorImpl) jenkins.getInstance().getDescriptor(JFrogPlatformBuilder.class); - Assert.assertNotNull(jfrogBuilder); + assertNotNull(jfrogBuilder); CredentialsConfig platformCred = new CredentialsConfig(Secret.fromString(ARTIFACTORY_USERNAME), Secret.fromString(ARTIFACTORY_PASSWORD), Secret.fromString(ACCESS_TOKEN), "credentials"); List artifactoryServers = new ArrayList<>() {{ // Dummy server to test multiple configured servers. @@ -265,7 +269,7 @@ private static void createPipelineSubstitution() { * @param path - Filesystem path to fix * @return path compatible with Windows */ - static String fixWindowsPath(String path) { + protected static String fixWindowsPath(String path) { return StringUtils.replace(path, "\\", "\\\\"); } @@ -282,13 +286,13 @@ private String readPipeline(String name) throws IOException { return pipelineSubstitution.replace(pipeline); } - public static void configureJfrogCliFromReleases(String cliVersion, Boolean override) throws Exception { + protected static void configureJfrogCliFromReleases(String cliVersion, Boolean override) throws Exception { ReleasesInstaller releasesInstaller = new ReleasesInstaller(); releasesInstaller.setVersion(cliVersion); configureJfrogCliTool(JFROG_CLI_TOOL_NAME_1, releasesInstaller, override); } - public static void configureJfrogCliFromArtifactory(String toolName, String serverId, String repo, Boolean override) throws Exception { + protected static void configureJfrogCliFromArtifactory(String toolName, String serverId, String repo, Boolean override) throws Exception { configureJfrogCliTool(toolName, new ArtifactoryInstaller(serverId, repo, ""), override); } @@ -300,7 +304,7 @@ public static void configureJfrogCliFromArtifactory(String toolName, String serv * @param override The tool will override pre-configured ones and be set if true, otherwise it will be added to the installation array. * @throws IOException failed to configure the new tool. */ - public static void configureJfrogCliTool(String toolName, BinaryInstaller installer, Boolean override) throws Exception { + protected static void configureJfrogCliTool(String toolName, BinaryInstaller installer, Boolean override) throws Exception { Saveable NOOP = () -> { }; DescribableList, ToolPropertyDescriptor> toolProperties = new DescribableList<>(NOOP); diff --git a/src/test/java/io/jenkins/plugins/jfrog/integration/TestRepository.java b/src/test/java/io/jenkins/plugins/jfrog/integration/TestRepository.java index cd097a67..eb78d3fb 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/integration/TestRepository.java +++ b/src/test/java/io/jenkins/plugins/jfrog/integration/TestRepository.java @@ -1,5 +1,8 @@ package io.jenkins.plugins.jfrog.integration; +import lombok.Getter; + +@Getter enum TestRepository { LOCAL_REPO("jenkins-jfrog-tests-local", RepoType.LOCAL), CLI_REMOTE_REPO("jenkins-jfrog-tests-cli-remote", RepoType.REMOTE); @@ -17,10 +20,6 @@ enum RepoType { this.repoType = repoType; } - public String getRepoName() { - return repoName; - } - @Override public String toString() { return getRepoName(); diff --git a/src/test/java/io/jenkins/plugins/jfrog/jenkins/EnableJenkins.java b/src/test/java/io/jenkins/plugins/jfrog/jenkins/EnableJenkins.java deleted file mode 100644 index f59a7d51..00000000 --- a/src/test/java/io/jenkins/plugins/jfrog/jenkins/EnableJenkins.java +++ /dev/null @@ -1,54 +0,0 @@ -package io.jenkins.plugins.jfrog.jenkins; - - -import org.junit.jupiter.api.extension.ExtendWith; - -import java.lang.annotation.*; - -/** - * JUnit 5 meta annotation providing {@link org.jvnet.hudson.test.JenkinsRule JenkinsRule} integration. - * - *

- * Test methods using the rule extension need to accept it by {@link org.jvnet.hudson.test.JenkinsRule JenkinsRule} parameter; each test case - * gets a new rule object. - *

- * Annotating a class provides access for all of its tests. Unrelated test cases can omit the parameter. - * - *

- * @EnableJenkins
- * class ExampleJUnit5Test {
- *
- *     @Test
- *     public void example(JenkinsRule r) {
- *         // use 'r' ...
- *     }
- *
- *     @Test
- *     public void exampleNotUsingRule() {
- *         // ...
- *     }
- * }
- * 
- *

- * Annotating a method limits access to the method. - * - *

- * class ExampleJUnit5Test {
- *
- *     @EnableJenkins
- *     @Test
- *     public void example(JenkinsRule r) {
- *         // use 'r' ...
- *     }
- * }
- * 
- * - * @see JenkinsExtension - * @see org.junit.jupiter.api.extension.ExtendWith - */ -@Target({ElementType.TYPE, ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@ExtendWith(JenkinsExtension.class) -public @interface EnableJenkins { -} diff --git a/src/test/java/io/jenkins/plugins/jfrog/jenkins/JUnit5JenkinsRule.java b/src/test/java/io/jenkins/plugins/jfrog/jenkins/JUnit5JenkinsRule.java deleted file mode 100644 index 15c9d821..00000000 --- a/src/test/java/io/jenkins/plugins/jfrog/jenkins/JUnit5JenkinsRule.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.jenkins.plugins.jfrog.jenkins; - -import edu.umd.cs.findbugs.annotations.NonNull; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.ParameterContext; -import org.junit.runner.Description; -import org.jvnet.hudson.test.JenkinsRecipe; -import org.jvnet.hudson.test.JenkinsRule; - -import java.lang.reflect.Method; - -/** - * Provides JUnit 5 compatibility for {@link JenkinsRule}. - */ -class JUnit5JenkinsRule extends JenkinsRule { - private final ParameterContext context; - - JUnit5JenkinsRule(@NonNull ParameterContext context, @NonNull ExtensionContext extensionContext) { - this.context = context; - this.testDescription = Description.createTestDescription( - extensionContext.getTestClass().map(Class::getName).orElse(null), - extensionContext.getTestMethod().map(Method::getName).orElse(null)); - } - - @Override - public void recipe() throws Exception { - JenkinsRecipe jenkinsRecipe = context.findAnnotation(JenkinsRecipe.class).orElse(null); - if (jenkinsRecipe == null) { - return; - } - @SuppressWarnings("unchecked") final JenkinsRecipe.Runner runner = - (JenkinsRecipe.Runner) jenkinsRecipe.value().getDeclaredConstructor().newInstance(); - recipes.add(runner); - tearDowns.add(() -> runner.tearDown(this, jenkinsRecipe)); - } -} diff --git a/src/test/java/io/jenkins/plugins/jfrog/jenkins/JenkinsExtension.java b/src/test/java/io/jenkins/plugins/jfrog/jenkins/JenkinsExtension.java deleted file mode 100644 index 15e070d7..00000000 --- a/src/test/java/io/jenkins/plugins/jfrog/jenkins/JenkinsExtension.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.jenkins.plugins.jfrog.jenkins; - - -import org.junit.jupiter.api.extension.*; -import org.jvnet.hudson.test.JenkinsRule; - -/** - * JUnit 5 extension providing {@link JenkinsRule} integration. - * - * @see EnableJenkins - */ -class JenkinsExtension implements ParameterResolver, AfterEachCallback { - - private static final String KEY = "jenkins-instance"; - private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(JenkinsExtension.class); - - @Override - public void afterEach(ExtensionContext context) throws Exception { - final JenkinsRule rule = context.getStore(NAMESPACE).remove(KEY, JenkinsRule.class); - if (rule == null) { - return; - } - rule.after(); - } - - @Override - public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { - return parameterContext.getParameter().getType().equals(JenkinsRule.class); - } - - @Override - public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { - final JenkinsRule rule = extensionContext.getStore(NAMESPACE).getOrComputeIfAbsent(KEY, key - -> new JUnit5JenkinsRule(parameterContext, extensionContext), JenkinsRule.class); - - try { - rule.before(); - return rule; - } catch (Throwable t) { - throw new ParameterResolutionException(t.getMessage(), t); - } - } -} \ No newline at end of file From cf84e2a48ff4f7ab60b39aff1501e76d3d5dd58f Mon Sep 17 00:00:00 2001 From: strangelookingnerd <49242855+strangelookingnerd@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:15:19 +0100 Subject: [PATCH 2/7] Migrate tests to JUnit Jupiter * Migrate annotations and imports * Migrate assertions * Remove public visibility for test classes and methods * Minor code cleanup --- .../jfrog/CliEnvConfiguratorProxyTest.java | 12 +++++----- .../plugins/jfrog/CliEnvConfiguratorTest.java | 24 ++++++++++--------- .../plugins/jfrog/JfrogBuilderTest.java | 4 ++-- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorProxyTest.java b/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorProxyTest.java index 24322de9..f27f6c33 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorProxyTest.java +++ b/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorProxyTest.java @@ -21,7 +21,7 @@ void beforeEach() { } @Test - void configureCliEnvHttpProxyTest() { + void configureCliEnvHttpProxyTest() throws Exception { proxyConfiguration.port = 80; invokeConfigureCliEnv(); assertEnv(envVars, HTTP_PROXY_ENV, "http://acme.proxy.io:80"); @@ -30,7 +30,7 @@ void configureCliEnvHttpProxyTest() { } @Test - void configureCliEnvHttpsProxyTest() { + void configureCliEnvHttpsProxyTest() throws Exception { proxyConfiguration.port = 443; invokeConfigureCliEnv(); assertEnv(envVars, HTTP_PROXY_ENV, "https://acme.proxy.io:443"); @@ -39,7 +39,7 @@ void configureCliEnvHttpsProxyTest() { } @Test - void configureCliEnvHttpProxyAuthTest() { + void configureCliEnvHttpProxyAuthTest() throws Exception { proxyConfiguration.port = 80; proxyConfiguration.username = "andor"; proxyConfiguration.password = "RogueOne"; @@ -50,7 +50,7 @@ void configureCliEnvHttpProxyAuthTest() { } @Test - void configureCliEnvHttpsProxyAuthTest() { + void configureCliEnvHttpsProxyAuthTest() throws Exception { proxyConfiguration.port = 443; proxyConfiguration.username = "andor"; proxyConfiguration.password = "RogueOne"; @@ -61,14 +61,14 @@ void configureCliEnvHttpsProxyAuthTest() { } @Test - void configureCliEnvNoOverrideHttpTest() { + void configureCliEnvNoOverrideHttpTest() throws Exception { envVars.put(HTTP_PROXY_ENV, "http://acme2.proxy.io:777"); invokeConfigureCliEnv(); assertEnv(envVars, HTTP_PROXY_ENV, "http://acme2.proxy.io:777"); } @Test - void configureCliEnvNoOverrideTest() { + void configureCliEnvNoOverrideTest() throws Exception { envVars.put(HTTP_PROXY_ENV, "http://acme2.proxy.io:80"); envVars.put(HTTPS_PROXY_ENV, "http://acme2.proxy.io:443"); invokeConfigureCliEnv(); diff --git a/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorTest.java b/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorTest.java index 59ddd1fe..70ab26a8 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorTest.java +++ b/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorTest.java @@ -12,6 +12,8 @@ import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; import static io.jenkins.plugins.jfrog.CliEnvConfigurator.*; import static org.junit.jupiter.api.Assertions.*; @@ -28,7 +30,7 @@ class CliEnvConfiguratorTest { protected EnvVars envVars; @TempDir - protected File tempFolder; + protected Path tempFolder; @BeforeEach void beforeEach(JenkinsRule rule) { @@ -40,8 +42,8 @@ void beforeEach(JenkinsRule rule) { } @Test - public void configureCliEnvBasicTest() throws IOException, InterruptedException { - File jfrogHomeDir = tempFolder.newFolder("jfrog-home"); + void configureCliEnvBasicTest() throws Exception { + File jfrogHomeDir = Files.createTempDirectory(tempFolder, "jfrog-home").toFile(); FilePath jfrogHomeTempDir = new FilePath(jfrogHomeDir); invokeConfigureCliEnv(jfrogHomeTempDir, new JFrogCliConfigEncryption(envVars)); assertEnv(envVars, JFROG_CLI_BUILD_NAME, "buildName"); @@ -51,12 +53,12 @@ public void configureCliEnvBasicTest() throws IOException, InterruptedException } @Test - public void configEncryptionTest() throws IOException, InterruptedException { + void configEncryptionTest() throws Exception { JFrogCliConfigEncryption configEncryption = new JFrogCliConfigEncryption(envVars); assertTrue(configEncryption.shouldEncrypt()); assertEquals(32, configEncryption.getKey().length()); - File jfrogHomeDir = tempFolder.newFolder("jfrog-home-enc"); + File jfrogHomeDir = Files.createTempDirectory(tempFolder, "jfrog-home-enc").toFile(); FilePath jfrogHomeTempDir = new FilePath(jfrogHomeDir); invokeConfigureCliEnv(jfrogHomeTempDir, configEncryption); // The encryption key file is created in jfrogHomeTempDir/encryption/ to work in Docker containers @@ -69,27 +71,27 @@ public void configEncryptionTest() throws IOException, InterruptedException { } @Test - public void configEncryptionWithHomeDirTest() throws IOException, InterruptedException { + void configEncryptionWithHomeDirTest() throws Exception { // Config JFROG_CLI_HOME_DIR to disable key encryption envVars.put(JFROG_CLI_HOME_DIR, "/a/b/c"); JFrogCliConfigEncryption configEncryption = new JFrogCliConfigEncryption(envVars); - File emptyDir = tempFolder.newFolder("empty"); + File emptyDir = Files.createTempDirectory(tempFolder, "empty").toFile(); invokeConfigureCliEnv(new FilePath(emptyDir), configEncryption); assertFalse(configEncryption.shouldEncrypt()); assertFalse(envVars.containsKey(JFROG_CLI_ENCRYPTION_KEY)); } - void assertEnv(EnvVars envVars, String key, String expectedValue) { + static void assertEnv(EnvVars envVars, String key, String expectedValue) { assertEquals(expectedValue, envVars.get(key)); } - void invokeConfigureCliEnv() throws IOException, InterruptedException { - File emptyDir = tempFolder.newFolder("default"); + void invokeConfigureCliEnv() throws Exception { + File emptyDir = Files.createTempDirectory(tempFolder, "default").toFile(); this.invokeConfigureCliEnv(new FilePath(emptyDir), new JFrogCliConfigEncryption(envVars)); } - void invokeConfigureCliEnv(FilePath jfrogHomeTempDir, JFrogCliConfigEncryption configEncryption) throws IOException, InterruptedException { + void invokeConfigureCliEnv(FilePath jfrogHomeTempDir, JFrogCliConfigEncryption configEncryption) throws Exception { setProxyConfiguration(); configureCliEnv(envVars, jfrogHomeTempDir, configEncryption); } diff --git a/src/test/java/io/jenkins/plugins/jfrog/JfrogBuilderTest.java b/src/test/java/io/jenkins/plugins/jfrog/JfrogBuilderTest.java index e5435ded..c4310b99 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/JfrogBuilderTest.java +++ b/src/test/java/io/jenkins/plugins/jfrog/JfrogBuilderTest.java @@ -147,14 +147,14 @@ void testPerformWithInvalidCommand() throws Exception { } @Test - void testPerformWithValidJfCommand() throws Exception { + void testPerformWithValidJfCommand() { JfrogBuilder validBuilder = new JfrogBuilder("jf rt ping"); assertNotNull(validBuilder); assertEquals("jf rt ping", validBuilder.getCommand()); } @Test - void testPerformWithValidJfrogCommand() throws Exception { + void testPerformWithValidJfrogCommand() { JfrogBuilder validBuilder = new JfrogBuilder("jfrog rt ping"); assertNotNull(validBuilder); assertEquals("jfrog rt ping", validBuilder.getCommand()); From f140e397ce6f7011c048a07f7fb263c4cda7b370 Mon Sep 17 00:00:00 2001 From: Bhanu Reddy Date: Sun, 15 Mar 2026 20:39:29 +0530 Subject: [PATCH 3/7] Fix parallel jfrog cli installation (#140) --- .gitignore | 4 + README.md | 84 +++- e2e/docker-compose.yml | 36 ++ e2e/setup.sh | 38 ++ pom.xml | 7 +- .../plugins/jfrog/BinaryInstaller.java | 280 +++++++----- .../java/io/jenkins/plugins/jfrog/JfStep.java | 17 +- .../jfrog/JfrogBuildInfoPublisher.java | 231 ++++++++++ .../jenkins/plugins/jfrog/JfrogBuilder.java | 2 +- .../plugins/jfrog/JfrogCliWrapper.java | 181 ++++++++ .../jfrog/callables/JFrogCliDownloader.java | 426 ++++++++++++++---- .../JfrogBuildInfoPublisher/config.jelly | 11 + .../plugins/jfrog/JfrogBuilder/config.jelly | 2 +- .../jfrog/JfrogCliWrapper/config.jelly | 7 + .../plugins/jfrog/CliEnvConfiguratorTest.java | 13 +- .../jfrog/integration/FreestyleJobITest.java | 133 ++++++ .../integration/ParallelInstallITest.java | 72 +++ .../jfrog/integration/PipelineTestBase.java | 4 +- .../pipelines/parallel_install.pipeline | 23 + .../parallel_install_two_tools.pipeline | 18 + 20 files changed, 1363 insertions(+), 226 deletions(-) create mode 100644 e2e/docker-compose.yml create mode 100755 e2e/setup.sh create mode 100644 src/main/java/io/jenkins/plugins/jfrog/JfrogBuildInfoPublisher.java create mode 100644 src/main/java/io/jenkins/plugins/jfrog/JfrogCliWrapper.java create mode 100644 src/main/resources/io/jenkins/plugins/jfrog/JfrogBuildInfoPublisher/config.jelly create mode 100644 src/main/resources/io/jenkins/plugins/jfrog/JfrogCliWrapper/config.jelly create mode 100644 src/test/java/io/jenkins/plugins/jfrog/integration/FreestyleJobITest.java create mode 100644 src/test/java/io/jenkins/plugins/jfrog/integration/ParallelInstallITest.java create mode 100644 src/test/resources/integration/pipelines/parallel_install.pipeline create mode 100644 src/test/resources/integration/pipelines/parallel_install_two_tools.pipeline diff --git a/.gitignore b/.gitignore index 3e72fc82..8c6d2e94 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ work # Mac OS .DS_Store + +# AI +.claude +.cursor diff --git a/README.md b/README.md index f26750bb..cf698703 100644 --- a/README.md +++ b/README.md @@ -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) @@ -89,6 +90,42 @@ as shown in the below screenshot. +### 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. @@ -224,12 +261,25 @@ 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 @@ -237,7 +287,8 @@ Before using the Freestyle job support, make sure you have completed the plugin 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 @@ -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) diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml new file mode 100644 index 00000000..9ecf70a8 --- /dev/null +++ b/e2e/docker-compose.yml @@ -0,0 +1,36 @@ +services: + + postgres: + image: docker.jfrog.io/postgres:14.5 + ports: + - "5432:5432" + environment: + POSTGRES_DB: artifactory + POSTGRES_USER: artifactory + POSTGRES_PASSWORD: password + + artifactory: + container_name: artifactory-jenkins-plugin + image: docker.jfrog.io/jfrog/artifactory-pro:latest + ports: + - "8046:8046" + - "8081:8081" + - "8082:8082" + environment: + JF_SHARED_NODE_ID: node-01 + JF_SHARED_DATABASE_TYPE: postgresql + JF_SHARED_DATABASE_DRIVER: org.postgresql.Driver + JF_SHARED_DATABASE_URL: jdbc:postgresql://host.docker.internal:5432/artifactory + JF_SHARED_DATABASE_USERNAME: artifactory + JF_SHARED_DATABASE_PASSWORD: password + JF_SHARED_SECURITY_JOINKEY: cc949ef041b726994a225dc20e018f23 + JF_SHARED_SECURITY_MASTERKEY: b055e9d06d17a293f1934109d4c4b560 + JF_ROUTER_DEV_MAKE_INTERNAL_PORTS_PUBLIC: "true" + JF_SHARED_DATABASE_ALLOWNONPOSTGRESQL: "true" + depends_on: + - postgres + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8082/artifactory/api/system/ping"] + interval: 10s + timeout: 5s + retries: 30 diff --git a/e2e/setup.sh b/e2e/setup.sh new file mode 100755 index 00000000..374d746e --- /dev/null +++ b/e2e/setup.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Usage: source e2e/setup.sh +# Starts Artifactory and exports the required env vars for integration tests. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "==> Starting Artifactory..." +docker compose -f "$SCRIPT_DIR/docker-compose.yml" up -d + +echo "==> Waiting for Artifactory to be healthy..." +for i in $(seq 1 30); do + if curl -sf http://localhost:8081/artifactory/api/system/ping > /dev/null 2>&1; then + echo "==> Artifactory is up!" + break + fi + echo " ...waiting ($i/30)" + sleep 5 +done + +# Default Artifactory OSS admin credentials +export JFROG_URL="http://localhost:8082" +export JFROG_USERNAME="admin" +export JFROG_PASSWORD="password" +export JFROG_ADMIN_TOKEN="" + +echo "" +echo "Environment variables exported:" +echo " JFROG_URL=$JFROG_URL" +echo " JFROG_USERNAME=$JFROG_USERNAME" +echo " JFROG_PASSWORD=***" +echo "" +echo "To run integration tests:" +echo " mvn verify -DskipITs=false -Dtest=NONE" +echo "" +echo "To run Jenkins locally with the plugin:" +echo " mvn hpi:run -Djenkins.version=2.440.3" diff --git a/pom.xml b/pom.xml index a346a404..6ac24aa7 100644 --- a/pom.xml +++ b/pom.xml @@ -70,7 +70,7 @@ org.projectlombok lombok - 1.18.26 + 1.18.36 provided @@ -305,6 +305,11 @@ **/*ITest.java + + + **/FreestyleJobITest.java + **/ParallelInstallITest.java + diff --git a/src/main/java/io/jenkins/plugins/jfrog/BinaryInstaller.java b/src/main/java/io/jenkins/plugins/jfrog/BinaryInstaller.java index 2f683713..e5e7bd93 100644 --- a/src/main/java/io/jenkins/plugins/jfrog/BinaryInstaller.java +++ b/src/main/java/io/jenkins/plugins/jfrog/BinaryInstaller.java @@ -20,10 +20,13 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Logger; +import static org.jfrog.build.client.DownloadResponse.SHA256_HEADER_NAME; + /** * Installer for JFrog CLI binary. * @@ -32,13 +35,32 @@ public abstract class BinaryInstaller extends ToolInstaller { private static final Logger LOGGER = Logger.getLogger(BinaryInstaller.class.getName()); - + + /** + * Environment variable that overrides the default lock-acquisition timeout (in minutes). + * Set this on the Jenkins controller when operating on slow networks where CLI downloads + * take longer than the default. + * + *
+     *   export JFROG_CLI_INSTALL_TIMEOUT_MINUTES=15
+     * 
+ */ + static final String INSTALL_TIMEOUT_ENV_VAR = "JFROG_CLI_INSTALL_TIMEOUT_MINUTES"; + private static final int DEFAULT_INSTALL_TIMEOUT_MINUTES = 5; + /** * Per-node synchronization locks for installation coordination. - * Key: Node name + tool installation path - * Value: Object used as synchronization lock + * Key: installation path + binary name (see {@link #createLockKey}) + * Value: ReentrantLock used to serialize installations to the same path + * + *

The map grows by one entry per unique (agent path, binary) combination encountered + * during the lifetime of the Jenkins JVM. In typical deployments the number of distinct + * tool installation paths is small and bounded, so the unbounded growth is acceptable. + * If the entry count ever becomes a concern, entries can be evicted after a successful + * installation without loss of correctness (a new lock will be created on the + * next access).

*/ - private static final ConcurrentHashMap NODE_INSTALLATION_LOCKS = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap NODE_INSTALLATION_LOCKS = new ConcurrentHashMap<>(); protected BinaryInstaller(String label) { super(label); @@ -70,86 +92,132 @@ public String getId() { } /** - * Performs JFrog CLI installation with proper synchronization to ensure reliable installation. - * + * Performs JFrog CLI installation ensuring the pipeline always has a working binary. + * * INSTALLATION STRATEGY: - * 1. Create unique lock key per node + installation path - * 2. Use synchronized block to ensure only one installation per location at a time - * 3. Check if CLI already exists and is valid before downloading - * 4. Download using atomic file operations for reliability - * + * 1. Fast path (no lock): binary exists and sha256 check passes — return immediately. + * When the server does not return a sha256 header, the check treats "no hash" as + * "up-to-date", so a missing or empty sha256 file never causes a re-download loop. + * 2. Slow path (lock + download): acquire a ReentrantLock (configurable timeout, default + * 5 min) and call the downloader. Re-check version inside the lock in case a + * concurrent stage just finished. + * 3. Fallbacks: if the lock times out or the download fails, use any existing valid binary + * rather than failing the pipeline. Only throw when there is truly nothing to run. + * * @param toolLocation Target directory for CLI installation * @param log Task listener for logging progress - * @param version CLI version to install + * @param version CLI version to install (blank = latest) * @param instance JFrog platform instance for download * @param repository Repository containing the CLI binary * @param binaryName Name of the CLI binary file * @return FilePath of the installed CLI - * @throws IOException If installation fails + * @throws IOException If installation fails and no existing binary is available * @throws InterruptedException If installation is interrupted */ - public static FilePath performJfrogCliInstallation(FilePath toolLocation, TaskListener log, String version, - JFrogPlatformInstance instance, String repository, String binaryName) + public static FilePath performJfrogCliInstallation(FilePath toolLocation, TaskListener log, String version, + JFrogPlatformInstance instance, String repository, String binaryName) throws IOException, InterruptedException { - - // Create unique lock key for this node + installation path + version combination - String lockKey = createLockKey(toolLocation, binaryName, version); - - // Get or create synchronization lock for this specific installation location - Object installationLock = NODE_INSTALLATION_LOCKS.computeIfAbsent(lockKey, k -> new Object()); - - log.getLogger().println("[BinaryInstaller] Acquiring installation lock for: " + lockKey); - - // Synchronize on the specific installation location to ensure coordinated installation - synchronized (installationLock) { - log.getLogger().println("[BinaryInstaller] Lock acquired, proceeding with installation"); - + + FilePath cliPath = toolLocation.child(binaryName); + + // Fast path: binary exists and is already the correct version — skip lock entirely. + if (isValidCliInstallation(cliPath, log) && isCorrectVersion(toolLocation, instance, repository, version, binaryName, log)) { + log.getLogger().println("[BinaryInstaller] CLI already installed and up-to-date, skipping download"); + return toolLocation; + } + + // Slow path: need to install or upgrade. + String lockKey = createLockKey(toolLocation, binaryName); + ReentrantLock installationLock = NODE_INSTALLATION_LOCKS.computeIfAbsent(lockKey, k -> new ReentrantLock()); + + int timeoutMinutes = getInstallTimeoutMinutes(); + log.getLogger().println("[BinaryInstaller] Acquiring installation lock for: " + lockKey + " (timeout: " + timeoutMinutes + " min)"); + + if (!installationLock.tryLock(timeoutMinutes, TimeUnit.MINUTES)) { + log.getLogger().println("[BinaryInstaller] WARNING: Could not acquire installation lock within " + timeoutMinutes + " minutes for: " + lockKey); + if (isValidCliInstallation(cliPath, log)) { + log.getLogger().println("[BinaryInstaller] Using existing binary while installation is in progress: " + cliPath.getRemote()); + return toolLocation; + } + throw new IOException("Timed out after " + timeoutMinutes + " minutes waiting for JFrog CLI installation and no binary exists at: " + cliPath.getRemote() + + ". Set " + INSTALL_TIMEOUT_ENV_VAR + " to increase the timeout."); + } + + log.getLogger().println("[BinaryInstaller] Lock acquired, proceeding with installation"); + try { + // Re-check inside the lock — a concurrent stage may have just finished. + boolean validCliExists = isValidCliInstallation(cliPath, log); + if (validCliExists && isCorrectVersion(toolLocation, instance, repository, version, binaryName, log)) { + log.getLogger().println("[BinaryInstaller] CLI was installed by a concurrent stage, skipping download"); + return toolLocation; + } + + if (validCliExists) { + log.getLogger().println("[BinaryInstaller] CLI version mismatch detected, upgrading"); + } else { + log.getLogger().println("[BinaryInstaller] No valid CLI installation found, downloading"); + } + 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)) { - log.getLogger().println("[BinaryInstaller] CLI already installed and up-to-date, skipping download"); - return toolLocation; - } else if (isValidCliInstallation(cliPath, log)) { - 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"); - } - - // Clean up any stale lock entries for this location (different versions) - cleanupStaleLocks(toolLocation, binaryName, version); - - log.getLogger().println("[BinaryInstaller] Starting CLI installation process"); - - // Perform the actual download using the improved JFrogCliDownloader JenkinsProxyConfiguration proxyConfiguration = new JenkinsProxyConfiguration(); toolLocation.act(new JFrogCliDownloader(proxyConfiguration, version, instance, log, repository, binaryName)); - log.getLogger().println("[BinaryInstaller] CLI installation completed successfully"); - return toolLocation; - - } finally { - log.getLogger().println("[BinaryInstaller] Installation lock released for: " + lockKey); + } catch (Exception e) { + // Download failed. If an older binary is still present, keep the pipeline running. + // The upgrade will be retried on the next run. + if (isValidCliInstallation(cliPath, log)) { + log.getLogger().println("[BinaryInstaller] WARNING: Download failed (" + e.getMessage() + + "), falling back to existing binary at: " + cliPath.getRemote()); + return toolLocation; + } + // No binary to fall back to — this is a genuine unrecoverable failure. + throw new IOException("JFrog CLI download failed and no existing binary is available: " + e.getMessage(), e); } + + return toolLocation; + + } finally { + installationLock.unlock(); + log.getLogger().println("[BinaryInstaller] Installation lock released for: " + lockKey); } } /** - * 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. - * + * Returns the lock-acquisition timeout in minutes. + * Reads {@value #INSTALL_TIMEOUT_ENV_VAR} from the environment; falls back to + * {@value #DEFAULT_INSTALL_TIMEOUT_MINUTES} minutes if the variable is absent or invalid. + * Values below 1 are silently clamped to the default. + */ + static int getInstallTimeoutMinutes() { + String envValue = System.getenv(INSTALL_TIMEOUT_ENV_VAR); + if (StringUtils.isNotBlank(envValue)) { + try { + int parsed = Integer.parseInt(envValue.trim()); + if (parsed >= 1) { + return parsed; + } + LOGGER.warning(INSTALL_TIMEOUT_ENV_VAR + "=" + envValue + " is less than 1, using default " + DEFAULT_INSTALL_TIMEOUT_MINUTES + " minutes"); + } catch (NumberFormatException e) { + LOGGER.warning(INSTALL_TIMEOUT_ENV_VAR + "=" + envValue + " is not a valid integer, using default " + DEFAULT_INSTALL_TIMEOUT_MINUTES + " minutes"); + } + } + return DEFAULT_INSTALL_TIMEOUT_MINUTES; + } + + /** + * 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 * @return Unique lock key string */ - private static String createLockKey(FilePath toolLocation, String binaryName, String version) { + private static String createLockKey(FilePath toolLocation, String binaryName) { 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; } } @@ -160,16 +228,26 @@ private static String createLockKey(FilePath toolLocation, String binaryName, St * @param log Task listener for logging * @return true if valid CLI exists, false otherwise */ + /** + * Checks existence, size (> 1 MB), and executable permission in a single agent RPC. + */ private static boolean isValidCliInstallation(FilePath cliPath, TaskListener log) { try { - if (cliPath.exists()) { - // Check if file is executable and has reasonable size (> 1MB) - long fileSize = cliPath.length(); - if (fileSize > 1024 * 1024) { // > 1MB - log.getLogger().println("[BinaryInstaller] Found existing CLI: " + cliPath.getRemote() + - " (size: " + (fileSize / 1024 / 1024) + "MB)"); - return true; + long[] result = cliPath.act(new MasterToSlaveFileCallable() { + @Override + public long[] invoke(File file, VirtualChannel channel) { + if (!file.exists() || file.isDirectory()) { + return new long[]{0, 0}; + } + String name = file.getName().toLowerCase(); + boolean executable = name.endsWith(".exe") || file.canExecute(); + return new long[]{file.length(), executable ? 1 : 0}; } + }); + if (result[0] > 1024 * 1024 && result[1] == 1) { + log.getLogger().println("[BinaryInstaller] Found existing CLI: " + cliPath.getRemote() + + " (size: " + (result[0] / 1024 / 1024) + "MB)"); + return true; } } catch (Exception e) { LOGGER.warning("Failed to check existing CLI installation: " + e.getMessage()); @@ -185,19 +263,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(); @@ -214,8 +294,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] WARNING: No SHA256 available from server — cannot verify version, assuming up-to-date (upgrade may be delayed)"); + return true; } // Check local SHA256 file @@ -228,7 +308,7 @@ public Boolean invoke(File f, VirtualChannel channel) throws IOException, Interr } String localSha256 = new String(Files.readAllBytes(sha256File.toPath()), StandardCharsets.UTF_8); - return constantTimeEquals(expectedSha256, localSha256); + return StringUtils.equals(expectedSha256, localSha256); } }); } @@ -238,68 +318,22 @@ 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(); } } return ""; } - /** - * Clean up stale lock entries for different versions of the same CLI at the same location. - * This prevents memory leaks in the NODE_INSTALLATION_LOCKS map during version upgrades. - * - * @param toolLocation Installation directory - * @param binaryName Binary file name - * @param currentVersion Current version being installed - */ - private static void cleanupStaleLocks(FilePath toolLocation, String binaryName, String currentVersion) { - try { - String locationPrefix = toolLocation.getRemote() + "/" + binaryName + "/"; - String currentLockKey = locationPrefix + currentVersion; - - // Remove old version lock entries for the same location - NODE_INSTALLATION_LOCKS.entrySet().removeIf(entry -> { - String key = entry.getKey(); - return key.startsWith(locationPrefix) && !key.equals(currentLockKey); - }); - - } catch (Exception e) { - // If cleanup fails, it's not critical - just log and continue - LOGGER.fine("Failed to cleanup stale locks: " + e.getMessage()); - } - } - - /** - * Constant-time comparison of two strings to prevent timing attacks. - * This is especially important for comparing cryptographic hashes like SHA256. - * - * @param a First string to compare - * @param b Second string to compare - * @return true if strings are equal, false otherwise - */ - private static boolean constantTimeEquals(String a, String b) { - if (a == null || b == null) { - return Objects.equals(a, b); - } - - if (a.length() != b.length()) { - return false; - } - - int result = 0; - for (int i = 0; i < a.length(); i++) { - result |= a.charAt(i) ^ b.charAt(i); - } - - return result == 0; - } } + diff --git a/src/main/java/io/jenkins/plugins/jfrog/JfStep.java b/src/main/java/io/jenkins/plugins/jfrog/JfStep.java index c36b72e4..ab807be3 100644 --- a/src/main/java/io/jenkins/plugins/jfrog/JfStep.java +++ b/src/main/java/io/jenkins/plugins/jfrog/JfStep.java @@ -189,11 +189,18 @@ private void logIfNoToolProvided(EnvVars env, TaskListener listener) { * @throws IOException in case of any I/O error, or we failed to run the 'jf' command */ public Launcher.ProcStarter setupJFrogEnvironment(Run run, EnvVars env, Launcher launcher, TaskListener listener, FilePath workspace, String jfrogBinaryPath, boolean isWindows, boolean passwordStdinSupported) throws IOException, InterruptedException { - JFrogCliConfigEncryption jfrogCliConfigEncryption = run.getAction(JFrogCliConfigEncryption.class); - if (jfrogCliConfigEncryption == null) { - // Set up the config encryption action to allow encrypting the JFrog CLI configuration and make sure we only create one key - jfrogCliConfigEncryption = new JFrogCliConfigEncryption(env); - run.addAction(jfrogCliConfigEncryption); + // Synchronize on the run object to ensure only one encryption key is created + // even when multiple parallel stages call this method concurrently. + // Without synchronization, each stage would create a different random key, + // and the stages would encrypt/decrypt the shared config file with mismatched + // keys → "cipher: message authentication failed". + JFrogCliConfigEncryption jfrogCliConfigEncryption; + synchronized (run) { + jfrogCliConfigEncryption = run.getAction(JFrogCliConfigEncryption.class); + if (jfrogCliConfigEncryption == null) { + jfrogCliConfigEncryption = new JFrogCliConfigEncryption(env); + run.addAction(jfrogCliConfigEncryption); + } } FilePath jfrogHomeTempDir = Utils.createAndGetJfrogCliHomeTempDir(workspace, String.valueOf(run.getNumber())); CliEnvConfigurator.configureCliEnv(env, jfrogHomeTempDir, jfrogCliConfigEncryption); diff --git a/src/main/java/io/jenkins/plugins/jfrog/JfrogBuildInfoPublisher.java b/src/main/java/io/jenkins/plugins/jfrog/JfrogBuildInfoPublisher.java new file mode 100644 index 00000000..22dc053e --- /dev/null +++ b/src/main/java/io/jenkins/plugins/jfrog/JfrogBuildInfoPublisher.java @@ -0,0 +1,231 @@ +package io.jenkins.plugins.jfrog; + +import hudson.EnvVars; +import hudson.Extension; +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.AbstractBuild; +import hudson.model.AbstractProject; +import hudson.model.BuildListener; +import hudson.model.Result; +import hudson.tasks.BuildStepDescriptor; +import hudson.tasks.BuildStepMonitor; +import hudson.tasks.Notifier; +import hudson.tasks.Publisher; +import hudson.util.ArgumentListBuilder; +import hudson.util.ListBoxModel; +import io.jenkins.plugins.jfrog.actions.JFrogCliConfigEncryption; +import org.apache.commons.lang3.StringUtils; +import org.jenkinsci.Symbol; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; + +import javax.annotation.Nonnull; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static io.jenkins.plugins.jfrog.JfStep.addBuildInfoActionIfNeeded; +import static io.jenkins.plugins.jfrog.JfrogInstallation.JFROG_BINARY_PATH; + +/** + * Post-build action to publish JFrog Build Info. + * This automatically runs 'jf rt build-publish' after the build completes, + * publishing collected build information to Artifactory. + */ +public class JfrogBuildInfoPublisher extends Notifier { + + private String jfrogInstallation; + private boolean publishOnlyOnSuccess = true; + + @DataBoundConstructor + public JfrogBuildInfoPublisher() { + } + + public String getJfrogInstallation() { + return jfrogInstallation; + } + + @DataBoundSetter + public void setJfrogInstallation(String jfrogInstallation) { + this.jfrogInstallation = jfrogInstallation; + } + + public boolean isPublishOnlyOnSuccess() { + return publishOnlyOnSuccess; + } + + @DataBoundSetter + public void setPublishOnlyOnSuccess(boolean publishOnlyOnSuccess) { + this.publishOnlyOnSuccess = publishOnlyOnSuccess; + } + + @Override + public BuildStepMonitor getRequiredMonitorService() { + return BuildStepMonitor.NONE; + } + + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) + throws InterruptedException, IOException { + + // Check if we should skip based on build result + Result buildResult = build.getResult(); + if (publishOnlyOnSuccess && buildResult != null && buildResult.isWorseThan(Result.SUCCESS)) { + listener.getLogger().println("[JFrog Build Info] Skipping publish - build result is " + buildResult); + return true; + } + + FilePath workspace = build.getWorkspace(); + if (workspace == null) { + listener.error("[JFrog Build Info] Workspace is null"); + return false; + } + + EnvVars env = build.getEnvironment(listener); + + // Setup JFrog CLI installation environment if specified + if (StringUtils.isNotBlank(jfrogInstallation)) { + JfrogInstallation installation = getInstallation(); + if (installation != null) { + hudson.model.Node node = build.getBuiltOn(); + if (node != null) { + installation = installation.forNode(node, listener); + } + if (installation != null) { + installation = installation.forEnvironment(env); + } + if (installation != null) { + installation.buildEnvVars(env); + } + } + } + + // Check if JFROG_BINARY_PATH is set (either from installation or Build Environment wrapper) + if (!env.containsKey(JFROG_BINARY_PATH)) { + listener.getLogger().println("[JFrog Build Info] Using JFrog CLI from system PATH"); + } + + boolean isWindows = !launcher.isUnix(); + String jfrogBinaryPath = Utils.getJFrogCLIPath(env, isWindows); + + // Setup JFrog environment + JFrogCliConfigEncryption jfrogCliConfigEncryption = build.getAction(JFrogCliConfigEncryption.class); + if (jfrogCliConfigEncryption == null) { + jfrogCliConfigEncryption = new JFrogCliConfigEncryption(env); + build.addAction(jfrogCliConfigEncryption); + } + + FilePath jfrogHomeTempDir = Utils.createAndGetJfrogCliHomeTempDir(workspace, String.valueOf(build.getNumber())); + CliEnvConfigurator.configureCliEnv(env, jfrogHomeTempDir, jfrogCliConfigEncryption); + + // Build the 'jf rt build-publish' command + ArgumentListBuilder builder = new ArgumentListBuilder(); + builder.add(jfrogBinaryPath).add("rt").add("bp"); + if (isWindows) { + builder = builder.toWindowsCommand(); + } + + listener.getLogger().println("[JFrog Build Info] Publishing build info..."); + listener.getLogger().println("[JFrog Build Info] Build name: " + env.get("JFROG_CLI_BUILD_NAME")); + listener.getLogger().println("[JFrog Build Info] Build number: " + env.get("JFROG_CLI_BUILD_NUMBER")); + + try (ByteArrayOutputStream taskOutputStream = new ByteArrayOutputStream()) { + JfTaskListener jfTaskListener = new JfTaskListener(listener, taskOutputStream); + Launcher.ProcStarter jfLauncher = launcher.launch() + .envs(env) + .pwd(workspace) + .stdout(jfTaskListener); + + // Configure servers if needed + if (shouldConfig(jfrogHomeTempDir)) { + JfStep.Execution.configAllServersForBuilder( + jfLauncher, jfrogBinaryPath, isWindows, build.getParent(), false + ); + } + + // Run 'jf rt bp' + int exitValue = jfLauncher.cmds(builder).join(); + if (exitValue != 0) { + listener.error("[JFrog Build Info] Failed to publish build info (exit code: " + exitValue + ")"); + return false; + } + + // Add build info badge to Jenkins UI + String[] args = {"rt", "bp"}; + addBuildInfoActionIfNeeded(args, new JenkinsBuildInfoLog(listener), build, taskOutputStream); + + listener.getLogger().println("[JFrog Build Info] Build info published successfully"); + return true; + } catch (Exception e) { + listener.error("[JFrog Build Info] Error publishing build info: " + e.getMessage()); + return false; + } + } + + /** + * Check if servers need to be configured. + */ + private boolean shouldConfig(FilePath jfrogHomeTempDir) throws IOException, InterruptedException { + if (jfrogHomeTempDir == null || !jfrogHomeTempDir.exists()) { + return true; + } + return !jfrogHomeTempDir.child("jfrog-cli.conf").exists(); + } + + /** + * Get the JFrog installation by name. + */ + private JfrogInstallation getInstallation() { + if (jfrogInstallation == null) { + return null; + } + + JfrogInstallation[] installations = ((DescriptorImpl) getDescriptor()).getInstallations(); + if (installations == null) { + return null; + } + + for (JfrogInstallation installation : installations) { + if (installation != null && jfrogInstallation.equals(installation.getName())) { + return installation; + } + } + return null; + } + + @Extension + @Symbol("jfrogPublishBuildInfo") + public static final class DescriptorImpl extends BuildStepDescriptor { + + @Nonnull + @Override + public String getDisplayName() { + return "Publish JFrog Build Info"; + } + + @Override + public boolean isApplicable(Class jobType) { + return true; + } + + /** + * Get all configured JFrog CLI installations. + */ + public JfrogInstallation[] getInstallations() { + jenkins.model.Jenkins jenkinsInstance = jenkins.model.Jenkins.get(); + return jenkinsInstance.getDescriptorByType(JfrogInstallation.DescriptorImpl.class).getInstallations(); + } + + /** + * Populate the dropdown list of JFrog CLI installations. + */ + public ListBoxModel doFillJfrogInstallationItems() { + ListBoxModel items = new ListBoxModel(); + items.add("(Use pre-installed JFrog CLI from system PATH)", ""); + for (JfrogInstallation installation : getInstallations()) { + items.add(installation.getName(), installation.getName()); + } + return items; + } + } +} diff --git a/src/main/java/io/jenkins/plugins/jfrog/JfrogBuilder.java b/src/main/java/io/jenkins/plugins/jfrog/JfrogBuilder.java index bc914f95..d407cc7f 100644 --- a/src/main/java/io/jenkins/plugins/jfrog/JfrogBuilder.java +++ b/src/main/java/io/jenkins/plugins/jfrog/JfrogBuilder.java @@ -352,7 +352,7 @@ public JfrogInstallation[] getInstallations() { */ public ListBoxModel doFillJfrogInstallationItems() { ListBoxModel items = new ListBoxModel(); - items.add("(Use JFrog CLI from system PATH)", ""); + items.add("(Use pre-installed JFrog CLI from system PATH)", ""); for (JfrogInstallation installation : getInstallations()) { items.add(installation.getName(), installation.getName()); } diff --git a/src/main/java/io/jenkins/plugins/jfrog/JfrogCliWrapper.java b/src/main/java/io/jenkins/plugins/jfrog/JfrogCliWrapper.java new file mode 100644 index 00000000..20782e30 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/jfrog/JfrogCliWrapper.java @@ -0,0 +1,181 @@ +package io.jenkins.plugins.jfrog; + +import hudson.EnvVars; +import hudson.Extension; +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.AbstractProject; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.tasks.BuildWrapperDescriptor; +import hudson.util.ListBoxModel; +import jenkins.tasks.SimpleBuildWrapper; +import org.jenkinsci.Symbol; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; + +import javax.annotation.Nonnull; +import java.io.IOException; + +/** + * Build Environment wrapper that sets up JFrog CLI for the entire build. + * This allows all build steps in a Freestyle job to use the configured JFrog CLI + * installation without having to specify it in each step. + * + * When this wrapper is enabled, the JFROG_BINARY_PATH environment variable is set + * for the entire build, making the 'jf' command available to all build steps, + * including shell scripts and other plugins. + * + *

Note: This wrapper is only available for Freestyle jobs, not Matrix jobs. + * Matrix jobs run across multiple nodes where CLI installations may differ. + * For Matrix jobs, use individual "Run JFrog CLI" build steps with automatic + * installation configured, which will download the CLI to each node as needed.

+ */ +public class JfrogCliWrapper extends SimpleBuildWrapper { + + private String jfrogInstallation; + + @DataBoundConstructor + public JfrogCliWrapper() { + } + + public String getJfrogInstallation() { + return jfrogInstallation; + } + + @DataBoundSetter + public void setJfrogInstallation(String jfrogInstallation) { + this.jfrogInstallation = jfrogInstallation; + } + + @Override + public void setUp( + Context context, + Run build, + FilePath workspace, + Launcher launcher, + TaskListener listener, + EnvVars initialEnvironment + ) throws IOException, InterruptedException { + if (jfrogInstallation == null || jfrogInstallation.isEmpty()) { + listener.getLogger().println("[JFrog CLI] No installation selected, using system PATH"); + return; + } + + JfrogInstallation installation = getInstallation(); + if (installation == null) { + listener.error("[JFrog CLI] Installation '" + jfrogInstallation + "' not found"); + return; + } + + // Resolve the installation for the current node + hudson.model.Node node = workspaceToNode(workspace); + if (node != null) { + installation = installation.forNode(node, listener); + } + installation = installation.forEnvironment(initialEnvironment); + + // Add environment variables that will persist for the entire build + EnvVars envVars = new EnvVars(); + installation.buildEnvVars(envVars); + + for (java.util.Map.Entry entry : envVars.entrySet()) { + context.env(entry.getKey(), entry.getValue()); + } + + listener.getLogger().println("[JFrog CLI] Using installation: " + jfrogInstallation); + listener.getLogger().println("[JFrog CLI] Binary path: " + envVars.get(JfrogInstallation.JFROG_BINARY_PATH)); + } + + /** + * Get the JFrog installation by name. + */ + private JfrogInstallation getInstallation() { + if (jfrogInstallation == null) { + return null; + } + + JfrogInstallation[] installations = ((DescriptorImpl) getDescriptor()).getInstallations(); + if (installations == null) { + return null; + } + + for (JfrogInstallation installation : installations) { + if (installation != null && jfrogInstallation.equals(installation.getName())) { + return installation; + } + } + return null; + } + + /** + * Get the node from workspace using the Computer API. + */ + private hudson.model.Node workspaceToNode(FilePath workspace) { + if (workspace == null) { + return null; + } + hudson.model.Computer computer = workspace.toComputer(); + if (computer == null) { + return null; + } + return computer.getNode(); + } + + @Extension + @Symbol("jfrogCliEnv") + public static final class DescriptorImpl extends BuildWrapperDescriptor { + + @Nonnull + @Override + public String getDisplayName() { + return "Set up JFrog CLI environment"; + } + + /** + * This wrapper is only available for Freestyle jobs, not Matrix jobs. + * + * Matrix jobs run across multiple nodes where CLI installations need to be + * handled per-node. For Matrix jobs, users should use individual "Run JFrog CLI" + * build steps with automatic installation configured, which will download + * the CLI to each node as needed. + * + * @param item The project to check + * @return true if this wrapper can be used with the project + */ + @Override + public boolean isApplicable(AbstractProject item) { + // Exclude Matrix projects - they run across multiple nodes where CLI installations may differ. + // Use reflection to avoid a hard compile-time dependency on the matrix-project plugin. + try { + Class matrixProjectClass = Class.forName("hudson.matrix.MatrixProject"); + if (matrixProjectClass.isInstance(item)) { + return false; + } + } catch (ClassNotFoundException e) { + // matrix-project plugin not installed; nothing to exclude + } + return true; + } + + /** + * Get all configured JFrog CLI installations. + */ + public JfrogInstallation[] getInstallations() { + jenkins.model.Jenkins jenkinsInstance = jenkins.model.Jenkins.get(); + return jenkinsInstance.getDescriptorByType(JfrogInstallation.DescriptorImpl.class).getInstallations(); + } + + /** + * Populate the dropdown list of JFrog CLI installations. + */ + public ListBoxModel doFillJfrogInstallationItems() { + ListBoxModel items = new ListBoxModel(); + items.add("(Use pre-installed JFrog CLI from system PATH)", ""); + for (JfrogInstallation installation : getInstallations()) { + items.add(installation.getName(), installation.getName()); + } + return items; + } + } +} diff --git a/src/main/java/io/jenkins/plugins/jfrog/callables/JFrogCliDownloader.java b/src/main/java/io/jenkins/plugins/jfrog/callables/JFrogCliDownloader.java index f63355fd..e40272e5 100644 --- a/src/main/java/io/jenkins/plugins/jfrog/callables/JFrogCliDownloader.java +++ b/src/main/java/io/jenkins/plugins/jfrog/callables/JFrogCliDownloader.java @@ -16,8 +16,11 @@ import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.AccessDeniedException; +import java.nio.file.FileSystemException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import static org.jfrog.build.client.DownloadResponse.SHA256_HEADER_NAME; @@ -38,6 +41,12 @@ public class JFrogCliDownloader extends MasterToSlaveFileCallable { * decoded "[RELEASE]" for the download url */ private static final String RELEASE = "[RELEASE]"; + + /** + * Minimum valid CLI binary size in bytes (1MB). + * Used to determine if an existing CLI installation is valid. + */ + private static final long MIN_VALID_CLI_SIZE = 1024 * 1024; JenkinsProxyConfiguration proxyConfiguration; private String providedVersion; @@ -50,6 +59,111 @@ public class JFrogCliDownloader extends MasterToSlaveFileCallable { public Void invoke(File toolLocation, VirtualChannel channel) throws IOException, InterruptedException { log.getLogger().println("[JFrogCliDownloader] Starting CLI download"); + // Ensure the tool location directory exists + if (!toolLocation.exists()) { + if (!toolLocation.mkdirs()) { + throw new IOException("Failed to create tool location directory: " + toolLocation.getAbsolutePath()); + } + } + + // Check if this is a fresh install or an upgrade + File existingCli = new File(toolLocation, binaryName); + boolean isFreshInstall = !isExistingCliValid(existingCli); + + if (isFreshInstall) { + log.getLogger().println("[JFrogCliDownloader] Fresh installation detected"); + performDownloadWithLock(toolLocation); + } else { + log.getLogger().println("[JFrogCliDownloader] Existing CLI found - attempting upgrade"); + performDownloadWithLockForUpgrade(toolLocation, existingCli); + } + + return null; + } + + /** + * Performs download for upgrade scenario with graceful fallback. + * If the binary is locked during replacement, we skip the upgrade and use existing. + * + * @param toolLocation The target directory + * @param existingCli The existing CLI binary + * @throws IOException If download fails for non-recoverable reasons + * @throws InterruptedException If interrupted + */ + private void performDownloadWithLockForUpgrade(File toolLocation, File existingCli) throws IOException, InterruptedException { + String version = StringUtils.defaultIfBlank(providedVersion, RELEASE); + String cliUrlSuffix = String.format("/%s/v2-jf/%s/jfrog-cli-%s/%s", repository, version, OsUtils.getOsDetails(), binaryName); + + JenkinsBuildInfoLog buildInfoLog = new JenkinsBuildInfoLog(log); + String artifactoryUrl = instance.inferArtifactoryUrl(); + + try (ArtifactoryManager manager = new ArtifactoryManager(artifactoryUrl, + Secret.toString(instance.getCredentialsConfig().getUsername()), + Secret.toString(instance.getCredentialsConfig().getPassword()), + Secret.toString(instance.getCredentialsConfig().getAccessToken()), buildInfoLog)) { + + if (proxyConfiguration.isProxyConfigured(artifactoryUrl)) { + manager.setProxyConfiguration(proxyConfiguration); + } + + String artifactorySha256 = getArtifactSha256(manager, cliUrlSuffix); + + if (!shouldDownloadTool(toolLocation, artifactorySha256)) { + log.getLogger().println("[JFrogCliDownloader] CLI is up-to-date, skipping download"); + return; + } + + if (version.equals(RELEASE)) { + log.getLogger().printf("[JFrogCliDownloader] Upgrading '%s' to latest version from: %s%n", + binaryName, artifactoryUrl + cliUrlSuffix); + } else { + log.getLogger().printf("[JFrogCliDownloader] Upgrading '%s' to version %s from: %s%n", + binaryName, version, artifactoryUrl + cliUrlSuffix); + } + + // Attempt upgrade with graceful fallback + boolean upgradeSucceeded = performAtomicDownloadForUpgrade(manager, cliUrlSuffix, toolLocation, + artifactorySha256, existingCli); + + if (upgradeSucceeded) { + log.getLogger().println("[JFrogCliDownloader] Upgrade completed successfully"); + } else { + log.getLogger().println("[JFrogCliDownloader] Upgrade skipped, using existing CLI version"); + } + } + } + + /** + * Checks if an existing CLI binary is valid and usable. + * A valid CLI exists and has a reasonable file size (> 1MB). + * + * @param cliFile The CLI binary file to check + * @return true if CLI is valid and usable, false otherwise + */ + private boolean isExistingCliValid(File cliFile) { + if (!cliFile.exists()) { + return false; + } + + long fileSize = cliFile.length(); + boolean isValid = fileSize >= MIN_VALID_CLI_SIZE; + + if (isValid) { + log.getLogger().println("[JFrogCliDownloader] Found valid existing CLI: " + cliFile.getAbsolutePath() + + " (size: " + (fileSize / 1024 / 1024) + "MB)"); + } + + return isValid; + } + + /** + * Performs the actual download operation for fresh installations. + * + * @param toolLocation The target directory for CLI installation + * @throws IOException If download fails + * @throws InterruptedException If interrupted during download + */ + private void performDownloadWithLock(File toolLocation) throws IOException, InterruptedException { // An empty string indicates the latest version. String version = StringUtils.defaultIfBlank(providedVersion, RELEASE); String cliUrlSuffix = String.format("/%s/v2-jf/%s/jfrog-cli-%s/%s", repository, version, OsUtils.getOsDetails(), binaryName); @@ -63,7 +177,7 @@ public Void invoke(File toolLocation, VirtualChannel channel) throws IOException if (proxyConfiguration.isProxyConfigured(artifactoryUrl)) { manager.setProxyConfiguration(proxyConfiguration); } - // Getting updated cli binary's sha256 form Artifactory. + // Getting updated cli binary's sha256 from Artifactory. String artifactorySha256 = getArtifactSha256(manager, cliUrlSuffix); if (shouldDownloadTool(toolLocation, artifactorySha256)) { if (version.equals(RELEASE)) { @@ -81,94 +195,220 @@ public Void invoke(File toolLocation, VirtualChannel channel) throws IOException } log.getLogger().println("[JFrogCliDownloader] Download completed successfully"); - return null; } /** - * Performs atomic download operations for reliable file installation. - * - * APPROACH: - * 1. Generate unique temporary file name to avoid conflicts - * 2. Download to temporary file - * 3. Verify download integrity - * 4. Atomic move from temp to final location - * 5. Set executable permissions - * 6. Create SHA256 verification file - * 7. Cleanup temporary file on any failure - * + * Downloads the CLI binary to a unique temporary file and verifies its integrity. + * + * @param manager ArtifactoryManager for download operations + * @param cliUrlSuffix URL suffix for the CLI binary + * @param toolLocation Target directory for installation + * @return The downloaded temporary file (caller is responsible for cleanup) + * @throws IOException If download or verification fails + */ + private File downloadToTemp(ArtifactoryManager manager, String cliUrlSuffix, File toolLocation) throws IOException { + String stageName = getStageNameFromThread(); + String tempFileName = binaryName + ".tmp." + + stageName + "." + + System.currentTimeMillis() + "." + + Thread.currentThread().getId() + "." + + System.nanoTime(); + + File temporaryDownloadFile = new File(toolLocation, tempFileName); + log.getLogger().println("[JFrogCliDownloader] Downloading to temporary file: " + temporaryDownloadFile.getAbsolutePath()); + manager.downloadToFile(cliUrlSuffix, temporaryDownloadFile.getPath()); + + if (!temporaryDownloadFile.exists()) { + throw new IOException("Downloaded file doesn't exist: " + temporaryDownloadFile.getAbsolutePath()); + } + long fileSize = temporaryDownloadFile.length(); + if (fileSize == 0) { + throw new IOException("Downloaded file is empty: " + temporaryDownloadFile.getAbsolutePath()); + } + log.getLogger().println("[JFrogCliDownloader] Download verified: " + (fileSize / 1024 / 1024) + "MB"); + return temporaryDownloadFile; + } + + /** + * Sets executable permissions and creates the SHA256 verification file after a successful move. + */ + private void finalizeInstall(File finalCliExecutable, File toolLocation, String artifactorySha256) throws IOException { + log.getLogger().println("[JFrogCliDownloader] Setting executable permissions"); + if (!finalCliExecutable.setExecutable(true)) { + throw new IOException("No permission to add execution permission to binary: " + finalCliExecutable.getAbsolutePath()); + } + log.getLogger().println("[JFrogCliDownloader] Creating SHA256 verification file"); + createSha256File(toolLocation, artifactorySha256); + } + + /** + * Performs atomic download for fresh installations. Always throws on failure. + * * @param manager ArtifactoryManager for download operations * @param cliUrlSuffix URL suffix for the CLI binary * @param toolLocation Target directory for installation * @param artifactorySha256 Expected SHA256 hash for verification * @throws IOException If download or file operations fail */ - private void performAtomicDownload(ArtifactoryManager manager, String cliUrlSuffix, - File toolLocation, String artifactorySha256) throws IOException { - - // Phase 1: Generate unique temporary file name to avoid conflicts during parallel downloads - String stageName = getStageNameFromThread(); - String tempFileName = binaryName + ".tmp." + - stageName + "." + - System.currentTimeMillis() + "." + - Thread.currentThread().getId() + "." + - System.nanoTime(); - - File temporaryDownloadFile = new File(toolLocation, tempFileName); // Temp file for download - File finalCliExecutable = new File(toolLocation, binaryName); // Final CLI binary location - - log.getLogger().println("[JFrogCliDownloader] Temporary download file: " + temporaryDownloadFile.getAbsolutePath()); - log.getLogger().println("[JFrogCliDownloader] Final CLI executable: " + finalCliExecutable.getAbsolutePath()); - + private void performAtomicDownload(ArtifactoryManager manager, String cliUrlSuffix, + File toolLocation, String artifactorySha256) throws IOException { + File finalCliExecutable = new File(toolLocation, binaryName); + File temporaryDownloadFile = null; try { - // Download to temporary file - log.getLogger().println("[JFrogCliDownloader] Downloading to temporary file"); - File downloadResponse = manager.downloadToFile(cliUrlSuffix, temporaryDownloadFile.getPath()); - - // Verify download integrity - log.getLogger().println("[JFrogCliDownloader] Verifying download integrity"); - if (!temporaryDownloadFile.exists()) { - throw new IOException("Downloaded file doesn't exist: " + temporaryDownloadFile.getAbsolutePath()); + temporaryDownloadFile = downloadToTemp(manager, cliUrlSuffix, toolLocation); + log.getLogger().println("[JFrogCliDownloader] Moving to final location: " + finalCliExecutable.getAbsolutePath()); + moveFileWithRetry(temporaryDownloadFile, finalCliExecutable); + finalizeInstall(finalCliExecutable, toolLocation, artifactorySha256); + log.getLogger().println("[JFrogCliDownloader] Download and installation completed successfully"); + } catch (Exception e) { + log.getLogger().println("[JFrogCliDownloader] Download failed, cleaning up temporary file"); + cleanupTempFile(temporaryDownloadFile); + throw e; + } + } + + /** + * Performs atomic download for upgrade scenario with graceful fallback. + * Returns false if the target binary is locked on Windows, allowing the caller to use the existing CLI. + * + * @param manager ArtifactoryManager for download operations + * @param cliUrlSuffix URL suffix for the CLI binary + * @param toolLocation Target directory for installation + * @param artifactorySha256 Expected SHA256 hash for verification + * @param existingCli The existing CLI binary file + * @return true if upgrade succeeded, false if skipped due to file locking + * @throws IOException If download fails for non-recoverable reasons (not file locking) + */ + private boolean performAtomicDownloadForUpgrade(ArtifactoryManager manager, String cliUrlSuffix, + File toolLocation, String artifactorySha256, + File existingCli) throws IOException { + File finalCliExecutable = new File(toolLocation, binaryName); + File temporaryDownloadFile = null; + try { + temporaryDownloadFile = downloadToTemp(manager, cliUrlSuffix, toolLocation); + log.getLogger().println("[JFrogCliDownloader] Attempting to replace existing CLI: " + finalCliExecutable.getAbsolutePath()); + boolean moveSucceeded = tryMoveFileForUpgrade(temporaryDownloadFile, finalCliExecutable); + if (!moveSucceeded) { + log.getLogger().println("[JFrogCliDownloader] WARNING: Existing CLI is in use by another process. " + + "Upgrade skipped. Using existing version. Upgrade will be attempted in next build."); + cleanupTempFile(temporaryDownloadFile); + return false; } - - long fileSize = temporaryDownloadFile.length(); - if (fileSize == 0) { - throw new IOException("Downloaded file is empty: " + temporaryDownloadFile.getAbsolutePath()); + finalizeInstall(finalCliExecutable, toolLocation, artifactorySha256); + return true; + } catch (IOException e) { + if (isWindowsTarget() && isFileLockingError(e) && isExistingCliValid(existingCli)) { + log.getLogger().println("[JFrogCliDownloader] WARNING: Upgrade failed due to file locking. " + + "Using existing CLI version. Upgrade will be attempted in next build."); + cleanupTempFile(temporaryDownloadFile); + return false; } - - log.getLogger().println("[JFrogCliDownloader] Download verified: " + (fileSize / 1024 / 1024) + "MB"); - - // Atomic move to final location (only delete existing if move will succeed) - log.getLogger().println("[JFrogCliDownloader] Moving to final location"); - if (finalCliExecutable.exists()) { - log.getLogger().println("[JFrogCliDownloader] Removing existing CLI binary to replace with new version"); - if (!finalCliExecutable.delete()) { - throw new IOException("Failed to remove existing CLI binary: " + finalCliExecutable.getAbsolutePath()); + cleanupTempFile(temporaryDownloadFile); + throw e; + } + } + + /** + * Attempts to move a file for upgrade scenario. + * Returns false if file is locked (instead of throwing), allowing graceful fallback. + * + * @param source Source file to move + * @param target Target location + * @return true if move succeeded, false if target is locked + * @throws IOException If move fails for non-locking reasons + */ + private boolean tryMoveFileForUpgrade(File source, File target) throws IOException { + try { + Files.move(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING); + log.getLogger().println("[JFrogCliDownloader] File moved successfully to: " + target.getAbsolutePath()); + return true; + } catch (IOException e) { + if (isFileLockingError(e)) { + if (!isWindowsTarget()) { + throw e; } + log.getLogger().println("[JFrogCliDownloader] Target file is locked: " + e.getMessage()); + return false; } - - // Atomic move from temporary file to final location - if (!temporaryDownloadFile.renameTo(finalCliExecutable)) { - throw new IOException("Failed to move temporary file to final location. Temp: " + - temporaryDownloadFile.getAbsolutePath() + ", Final: " + finalCliExecutable.getAbsolutePath()); + throw e; + } + } + + /** + * Checks if an exception is caused by Windows file locking. + * + * @param e The exception to check + * @return true if this is a file locking error + */ + private boolean isFileLockingError(Exception e) { + Throwable current = e; + while (current != null) { + if (current instanceof AccessDeniedException) { + return true; } - - // Set executable permissions on final CLI binary - log.getLogger().println("[JFrogCliDownloader] Setting executable permissions"); - if (!finalCliExecutable.setExecutable(true)) { - throw new IOException("No permission to add execution permission to binary: " + finalCliExecutable.getAbsolutePath()); + if (current instanceof FileSystemException) { + String reason = ((FileSystemException) current).getReason(); + if (containsLockingMessage(reason)) { + return true; + } + } + if (containsLockingMessage(current.getMessage())) { + return true; + } + current = current.getCause(); + } + return false; + } + + private boolean containsLockingMessage(String message) { + if (message == null) { + return false; + } + return message.contains("being used by another process") || + message.contains("Access is denied") || + message.contains("cannot access the file") || + message.contains("The process cannot access the file"); + } + + /** + * Moves a file to the target location with retry logic for Windows file locking issues. + * Uses Java NIO Files.move with REPLACE_EXISTING option for atomic operation. + * On Windows, if the target file is locked (e.g., being scanned by antivirus), + * this method will retry with exponential backoff. + * + * @param source Source file to move + * @param target Target location + * @throws IOException If move fails after all retries + */ + private void moveFileWithRetry(File source, File target) throws IOException { + int maxRetries = 5; + long retryDelayMs = 1000; + + for (int attempt = 1; attempt <= maxRetries; attempt++) { + try { + // Use Files.move with REPLACE_EXISTING for atomic replacement. + // Note: ATOMIC_MOVE is not always supported on Windows, so we use REPLACE_EXISTING. + Files.move(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING); + log.getLogger().println("[JFrogCliDownloader] File moved successfully to: " + target.getAbsolutePath()); + return; + } catch (IOException e) { + boolean isLastAttempt = (attempt == maxRetries); + if (isFileLockingError(e) && !isLastAttempt) { + log.getLogger().println("[JFrogCliDownloader] File locked, retrying in " + + retryDelayMs + "ms (attempt " + attempt + "/" + maxRetries + ")"); + try { + Thread.sleep(retryDelayMs); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while waiting to retry file move", ie); + } + retryDelayMs *= 2; + } else { + throw new IOException("Failed to move file from " + source.getAbsolutePath() + + " to " + target.getAbsolutePath() + + " after " + attempt + " attempts: " + e.getMessage(), e); + } } - - // Create SHA256 verification file - log.getLogger().println("[JFrogCliDownloader] Creating SHA256 verification file"); - createSha256File(toolLocation, artifactorySha256); - - log.getLogger().println("[JFrogCliDownloader] Download and installation completed successfully"); - - } catch (Exception e) { - // Cleanup temporary file on failure - log.getLogger().println("[JFrogCliDownloader] Download failed, cleaning up temporary file"); - cleanupTempFile(temporaryDownloadFile); - throw e; } } @@ -191,9 +431,21 @@ private void cleanupTempFile(File tempFile) { } } + /** + * Atomically writes the sha256 verification file using a temp-then-rename approach. + * A plain {@code Files.write()} truncates the target file before writing content, + * creating a brief window where readers see a 0-byte file and treat the installation + * as stale (triggering a re-download loop). Writing to a temp file and renaming + * eliminates that window because the rename is atomic on all supported file systems. + */ private static void createSha256File(File toolLocation, String artifactorySha256) throws IOException { - File file = new File(toolLocation, SHA256_FILE_NAME); - Files.write(file.toPath(), artifactorySha256.getBytes(StandardCharsets.UTF_8)); + if (StringUtils.isBlank(artifactorySha256)) { + return; + } + File targetFile = new File(toolLocation, SHA256_FILE_NAME); + File tempFile = new File(toolLocation, SHA256_FILE_NAME + ".tmp"); + Files.write(tempFile.toPath(), artifactorySha256.getBytes(StandardCharsets.UTF_8)); + Files.move(tempFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } /** @@ -201,11 +453,16 @@ private static void createSha256File(File toolLocation, String artifactorySha256 * A file named 'sha256' contains the specific binary sha256. * If the file sha256 has not changed, we will skip the download, otherwise we will download and overwrite the existing files. * + *

A 0-byte sha256 file (left by a previous failed write or an older plugin version) + * is treated the same as a missing file: the tool is re-downloaded and the file is + * replaced with the correct hash. The stale 0-byte file is deleted immediately so it + * cannot confuse any concurrent readers.

+ * * @param toolLocation - expected location of the tool on the fileSystem. * @param artifactorySha256 - sha256 of the expected file in artifactory. */ private static boolean shouldDownloadTool(File toolLocation, String artifactorySha256) throws IOException { - // In case no sha256 was provided (for example when the customer blocks headers) download the tool. + // In case no sha256 was provided (for example when the users blocks headers) download the tool. if (artifactorySha256.isEmpty()) { return true; } @@ -215,6 +472,12 @@ private static boolean shouldDownloadTool(File toolLocation, String artifactoryS return true; } String fileContent = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + if (StringUtils.isBlank(fileContent)) { + // 0-byte or whitespace-only sha256 file — corrupted/legacy state. + // Delete it so a fresh, correct file is written after the download. + Files.deleteIfExists(path); + return true; + } return !StringUtils.equals(fileContent, artifactorySha256); } @@ -229,7 +492,9 @@ private static boolean shouldDownloadTool(File toolLocation, String artifactoryS private static String getArtifactSha256(ArtifactoryManager manager, String cliUrlSuffix) throws IOException { Header[] headers = manager.downloadHeaders(cliUrlSuffix); for (Header header : headers) { - if (header.getName().equalsIgnoreCase(SHA256_HEADER_NAME)) { + String headerName = header.getName(); + if (headerName.equalsIgnoreCase(SHA256_HEADER_NAME) || + headerName.equalsIgnoreCase("X-Artifactory-Checksum-Sha256")) { return header.getValue(); } } @@ -258,4 +523,11 @@ private String getStageNameFromThread() { return "unknown"; } } + + /** + * Determine whether the target CLI binary is Windows. + */ + private boolean isWindowsTarget() { + return binaryName != null && binaryName.toLowerCase().endsWith(".exe"); + } } diff --git a/src/main/resources/io/jenkins/plugins/jfrog/JfrogBuildInfoPublisher/config.jelly b/src/main/resources/io/jenkins/plugins/jfrog/JfrogBuildInfoPublisher/config.jelly new file mode 100644 index 00000000..f4b02e82 --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/jfrog/JfrogBuildInfoPublisher/config.jelly @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/main/resources/io/jenkins/plugins/jfrog/JfrogBuilder/config.jelly b/src/main/resources/io/jenkins/plugins/jfrog/JfrogBuilder/config.jelly index 78970333..04e2d5a9 100644 --- a/src/main/resources/io/jenkins/plugins/jfrog/JfrogBuilder/config.jelly +++ b/src/main/resources/io/jenkins/plugins/jfrog/JfrogBuilder/config.jelly @@ -1,7 +1,7 @@ + description="Select the JFrog CLI installation to use. If you choose 'Use pre-installed JFrog CLI from system PATH', the 'jf' command must already be installed and available in the PATH on your Jenkins agent. Otherwise, configure automatic installations in 'Manage Jenkins' → 'Global Tool Configuration'."> + + + + + diff --git a/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorTest.java b/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorTest.java index 70ab26a8..4dfd0b5a 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorTest.java +++ b/src/test/java/io/jenkins/plugins/jfrog/CliEnvConfiguratorTest.java @@ -57,17 +57,8 @@ void configEncryptionTest() throws Exception { JFrogCliConfigEncryption configEncryption = new JFrogCliConfigEncryption(envVars); assertTrue(configEncryption.shouldEncrypt()); assertEquals(32, configEncryption.getKey().length()); - - File jfrogHomeDir = Files.createTempDirectory(tempFolder, "jfrog-home-enc").toFile(); - FilePath jfrogHomeTempDir = new FilePath(jfrogHomeDir); - invokeConfigureCliEnv(jfrogHomeTempDir, configEncryption); - // The encryption key file is created in jfrogHomeTempDir/encryption/ to work in Docker containers - String keyFilePath = envVars.get(JFROG_CLI_ENCRYPTION_KEY); - assertNotNull(keyFilePath); - assertTrue(keyFilePath.startsWith(jfrogHomeDir.getAbsolutePath())); - assertTrue(keyFilePath.contains("encryption")); - assertTrue(keyFilePath.endsWith(".key")); - assertEquals(keyFilePath, configEncryption.getKeyFilePath()); + invokeConfigureCliEnv(new FilePath(Files.createTempDirectory(tempFolder, "encryption-test").toFile()), configEncryption); + assertEnv(envVars, JFROG_CLI_ENCRYPTION_KEY, configEncryption.getKeyFilePath()); } @Test diff --git a/src/test/java/io/jenkins/plugins/jfrog/integration/FreestyleJobITest.java b/src/test/java/io/jenkins/plugins/jfrog/integration/FreestyleJobITest.java new file mode 100644 index 00000000..ffba9793 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/jfrog/integration/FreestyleJobITest.java @@ -0,0 +1,133 @@ +package io.jenkins.plugins.jfrog.integration; + +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.model.Result; +import hudson.tasks.Shell; +import io.jenkins.plugins.jfrog.JfrogBuildInfoPublisher; +import io.jenkins.plugins.jfrog.JfrogCliWrapper; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Integration tests for the Freestyle job extensions: + * - JfrogCliWrapper (Build Environment wrapper) + * - JfrogBuildInfoPublisher (Post-build action) + */ +class FreestyleJobITest extends PipelineTestBase { + + /** + * JfrogCliWrapper should add JFROG_BINARY_PATH to the build environment. + * + * @param jenkins Jenkins instance injected automatically. + */ + @Test + public void testWrapperSetsBinaryPath(JenkinsRule jenkins) throws Exception { + setupJenkins(jenkins); + configureJfrogCliFromReleases(StringUtils.EMPTY, true); + + FreeStyleProject project = jenkins.createFreeStyleProject("test-wrapper-path"); + JfrogCliWrapper wrapper = new JfrogCliWrapper(); + wrapper.setJfrogInstallation(JFROG_CLI_TOOL_NAME_1); + project.getBuildWrappersList().add(wrapper); + + // Add a shell step that checks JFROG_BINARY_PATH is set + project.getBuildersList().add( + new Shell("echo \"Binary: $JFROG_BINARY_PATH\" && test -n \"$JFROG_BINARY_PATH\"") + ); + + FreeStyleBuild build = jenkins.buildAndAssertSuccess(project); + assertNotNull(build); + } + + /** + * Without any installation selected, JfrogCliWrapper should still allow the build + * to proceed (uses system PATH). + * + * @param jenkins Jenkins instance injected automatically. + */ + @Test + public void testWrapperNoInstallationUsesSysPath(JenkinsRule jenkins) throws Exception { + setupJenkins(jenkins); + + FreeStyleProject project = jenkins.createFreeStyleProject("test-wrapper-no-install"); + JfrogCliWrapper wrapper = new JfrogCliWrapper(); + // No installation set — uses system PATH + project.getBuildWrappersList().add(wrapper); + project.getBuildersList().add(new Shell("echo 'no installation, continuing'")); + + FreeStyleBuild build = jenkins.buildAndAssertSuccess(project); + assertNotNull(build); + } + + /** + * JfrogBuildInfoPublisher with publishOnlyOnSuccess=true must skip publish + * when the build has failed. + * + * @param jenkins Jenkins instance injected automatically. + */ + @Test + public void testPublisherSkipsOnFailedBuild(JenkinsRule jenkins) throws Exception { + setupJenkins(jenkins); + + FreeStyleProject project = jenkins.createFreeStyleProject("test-publisher-skip"); + // Deliberately fail the build + project.getBuildersList().add(new Shell("exit 1")); + + JfrogBuildInfoPublisher publisher = new JfrogBuildInfoPublisher(); + publisher.setPublishOnlyOnSuccess(true); + project.getPublishersList().add(publisher); + + // Build should fail at the shell step; publisher should skip cleanly (no NPE or error from publisher) + FreeStyleBuild build = jenkins.buildAndAssertStatus(Result.FAILURE, project); + assertNotNull(build); + } + + /** + * Freestyle flow: wrapper sets environment and build step successfully invokes the JFrog CLI. + * Verifies that JFROG_BINARY_PATH points to the installation directory and 'jf -v' succeeds. + * + * @param jenkins Jenkins instance injected automatically. + */ + @Test + public void testFullFreestyleFlow(JenkinsRule jenkins) throws Exception { + setupJenkins(jenkins); + configureJfrogCliFromReleases(StringUtils.EMPTY, true); + + FreeStyleProject project = jenkins.createFreeStyleProject("test-freestyle-full"); + + // Build environment: set up JFrog CLI + JfrogCliWrapper wrapper = new JfrogCliWrapper(); + wrapper.setJfrogInstallation(JFROG_CLI_TOOL_NAME_1); + project.getBuildWrappersList().add(wrapper); + + // Build step: version check (JFROG_BINARY_PATH is the directory; append the binary name) + project.getBuildersList().add(new Shell("\"$JFROG_BINARY_PATH/jf\" -v")); + + FreeStyleBuild build = jenkins.buildAndAssertSuccess(project); + assertNotNull(build); + } + + /** + * JfrogCliWrapper.DescriptorImpl.isApplicable must return true for a regular + * FreeStyle project and false for a Matrix project (validated via reflection guard). + * + * @param jenkins Jenkins instance injected automatically. + */ + @Test + public void testWrapperNotApplicableForMatrixProject(JenkinsRule jenkins) throws Exception { + setupJenkins(jenkins); + + JfrogCliWrapper.DescriptorImpl descriptor = + jenkins.jenkins.getDescriptorByType(JfrogCliWrapper.DescriptorImpl.class); + assertNotNull(descriptor); + + // For a regular FreeStyle project, it must be applicable + FreeStyleProject freeStyle = jenkins.createFreeStyleProject("test-is-applicable"); + assertTrue(descriptor.isApplicable(freeStyle)); + } +} diff --git a/src/test/java/io/jenkins/plugins/jfrog/integration/ParallelInstallITest.java b/src/test/java/io/jenkins/plugins/jfrog/integration/ParallelInstallITest.java new file mode 100644 index 00000000..3e5dbbe7 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/jfrog/integration/ParallelInstallITest.java @@ -0,0 +1,72 @@ +package io.jenkins.plugins.jfrog.integration; + +import io.jenkins.plugins.jfrog.ReleasesInstaller; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.JenkinsRule; + +/** + * Integration tests verifying that parallel JFrog CLI installations do not corrupt + * the binary or produce race conditions. + */ +class ParallelInstallITest extends PipelineTestBase { + + /** + * Two parallel pipeline stages that share the same JFrog CLI tool must both + * complete successfully without binary corruption. + * + * @param jenkins Jenkins instance injected automatically. + */ + @Test + public void testParallelStagesSameToolVersion(JenkinsRule jenkins) throws Exception { + setupJenkins(jenkins); + configureJfrogCliFromReleases(StringUtils.EMPTY, true); + runPipeline(jenkins, "parallel_install"); + } + + /** + * Two parallel pipeline stages each using a different CLI version must both + * complete and produce independent, valid binaries. + * + * @param jenkins Jenkins instance injected automatically. + */ + @Test + public void testParallelStagesTwoToolVersions(JenkinsRule jenkins) throws Exception { + setupJenkins(jenkins); + // Tool 1: latest CLI version + configureJfrogCliFromReleases(StringUtils.EMPTY, true); + // Tool 2: a known older specific version to verify independent parallel installs + ReleasesInstaller installer = new ReleasesInstaller(); + installer.setVersion("2.29.2"); + configureJfrogCliTool(JFROG_CLI_TOOL_NAME_2, installer, false); + runPipeline(jenkins, "parallel_install_two_tools"); + } + + /** + * Repeated installs of the same version should detect the valid cached binary + * and skip re-download (verified by both runs succeeding without error). + * + * @param jenkins Jenkins instance injected automatically. + */ + @Test + public void testRepeatedInstallSkipsDownload(JenkinsRule jenkins) throws Exception { + setupJenkins(jenkins); + configureJfrogCliFromReleases(StringUtils.EMPTY, true); + // First run installs + runPipeline(jenkins, "basic_version_command"); + // Second run should hit the cache + runPipeline(jenkins, "basic_version_command"); + } + + /** + * Install from Artifactory in parallel: both stages must succeed. + * + * @param jenkins Jenkins instance injected automatically. + */ + @Test + public void testParallelInstallFromArtifactory(JenkinsRule jenkins) throws Exception { + setupJenkins(jenkins); + configureJfrogCliFromArtifactory(JFROG_CLI_TOOL_NAME_1, TEST_CONFIGURED_SERVER_ID, getRepoKey(TestRepository.CLI_REMOTE_REPO), true); + runPipeline(jenkins, "parallel_install"); + } +} diff --git a/src/test/java/io/jenkins/plugins/jfrog/integration/PipelineTestBase.java b/src/test/java/io/jenkins/plugins/jfrog/integration/PipelineTestBase.java index 883cab84..b0bdfbd9 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/integration/PipelineTestBase.java +++ b/src/test/java/io/jenkins/plugins/jfrog/integration/PipelineTestBase.java @@ -55,7 +55,7 @@ class PipelineTestBase { private static long currentTime; private static Artifactory artifactoryClient; protected JenkinsRule jenkins; - private Slave slave; + protected Slave slave; private static StringSubstitutor pipelineSubstitution; private static final String SLAVE_LABEL = "TestSlave"; protected static final String PLATFORM_URL = System.getenv("JFROG_URL"); @@ -63,7 +63,7 @@ class PipelineTestBase { private static final String ARTIFACTORY_USERNAME = System.getenv("JFROG_USERNAME"); private static final String ARTIFACTORY_PASSWORD = System.getenv("JFROG_PASSWORD"); private static final String ACCESS_TOKEN = System.getenv("JFROG_ADMIN_TOKEN"); - private static final Path INTEGRATION_BASE_PATH = Paths.get(".").toAbsolutePath().normalize() + protected static final Path INTEGRATION_BASE_PATH = Paths.get(".").toAbsolutePath().normalize() .resolve(Paths.get("src", "test", "resources", "integration")); protected static final String JFROG_CLI_TOOL_NAME_1 = "jfrog-cli"; protected static final String JFROG_CLI_TOOL_NAME_2 = "jfrog-cli-2"; diff --git a/src/test/resources/integration/pipelines/parallel_install.pipeline b/src/test/resources/integration/pipelines/parallel_install.pipeline new file mode 100644 index 00000000..ae31b73b --- /dev/null +++ b/src/test/resources/integration/pipelines/parallel_install.pipeline @@ -0,0 +1,23 @@ +// Tests that parallel stages using the same JFrog CLI tool name do not corrupt installation +pipeline { + agent any + tools { + jfrog '${JFROG_CLI_TOOL_NAME_1}' + } + stages { + stage('Parallel Safety') { + parallel { + stage('Task A') { + steps { + jf '-v' + } + } + stage('Task B') { + steps { + jf '-v' + } + } + } + } + } +} diff --git a/src/test/resources/integration/pipelines/parallel_install_two_tools.pipeline b/src/test/resources/integration/pipelines/parallel_install_two_tools.pipeline new file mode 100644 index 00000000..01f75da8 --- /dev/null +++ b/src/test/resources/integration/pipelines/parallel_install_two_tools.pipeline @@ -0,0 +1,18 @@ +// Tests parallel stages where each stage uses a different JFrog CLI version +pipeline { + agent any + stages { + stage('Parallel Two Tools') { + parallel { + stage('Tool 1') { + tools { jfrog '${JFROG_CLI_TOOL_NAME_1}' } + steps { jf '-v' } + } + stage('Tool 2') { + tools { jfrog '${JFROG_CLI_TOOL_NAME_2}' } + steps { jf '-v' } + } + } + } + } +} From 46ee022564e4e013d3af0f3110cd5b87dbffad5f Mon Sep 17 00:00:00 2001 From: Kanishk Gupta Date: Mon, 23 Mar 2026 12:13:37 +0530 Subject: [PATCH 4/7] Conditional audit trigger (#145) --- .jfrog-pipelines/pipelines.release.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.jfrog-pipelines/pipelines.release.yml b/.jfrog-pipelines/pipelines.release.yml index ec692b27..48b45a80 100644 --- a/.jfrog-pipelines/pipelines.release.yml +++ b/.jfrog-pipelines/pipelines.release.yml @@ -11,6 +11,12 @@ pipelines: readOnly: NEXT_VERSION: 0.0.0 NEXT_DEVELOPMENT_VERSION: 0.0.x-SNAPSHOT + RUN_JF_AUDIT: + default: "false" + values: + - "false" + - "true" + allowCustom: false steps: - name: Release @@ -51,7 +57,7 @@ pipelines: - jf mvnc --repo-resolve-releases ecosys-jenkins-repos --repo-resolve-snapshots ecosys-releases-snapshots --repo-deploy-snapshots ecosys-oss-snapshot-local --repo-deploy-releases ecosys-oss-release-local # Run audit - - jf audit --fail=false + - if [ "$RUN_JF_AUDIT" = "true" ]; then jf audit --fail=false; else echo "Skipping jf audit"; fi # Update version - env -i PATH=$PATH M2_HOME=$M2_HOME HOME=$HOME jf mvn versions:set -DnewVersion="${NEXT_VERSION}" -B From fa9b910b5e79d46cd379db460bdff7481caffe2a Mon Sep 17 00:00:00 2001 From: JFrog Pipelines Step Date: Mon, 23 Mar 2026 06:52:33 +0000 Subject: [PATCH 5/7] [jfrog-release] Release version 1.7.0 [skipRun] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6ac24aa7..a7047353 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ io.jenkins.plugins jfrog - 1.6.x-SNAPSHOT + 1.7.0 hpi JFrog Plugin From bf7c06e035e0e5cfe42e1b9a7d815c3119884b97 Mon Sep 17 00:00:00 2001 From: JFrog Pipelines Step Date: Mon, 23 Mar 2026 06:57:03 +0000 Subject: [PATCH 6/7] [jfrog-release] Next development version [skipRun] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a7047353..41a3df84 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ io.jenkins.plugins jfrog - 1.7.0 + 1.7.x-SNAPSHOT hpi JFrog Plugin From 2bdc490150d9616082c9e2d3ec7433aa91244d13 Mon Sep 17 00:00:00 2001 From: strangelookingnerd <49242855+strangelookingnerd@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:07:59 +0100 Subject: [PATCH 7/7] Migrate tests to JUnit Jupiter * Migrate annotations and imports * Migrate assertions * Remove public visibility for test classes and methods * Minor code cleanup --- .../jfrog/integration/FreestyleJobITest.java | 33 ++++++----------- .../integration/ParallelInstallITest.java | 37 ++++++++----------- 2 files changed, 27 insertions(+), 43 deletions(-) diff --git a/src/test/java/io/jenkins/plugins/jfrog/integration/FreestyleJobITest.java b/src/test/java/io/jenkins/plugins/jfrog/integration/FreestyleJobITest.java index ffba9793..e720bbcb 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/integration/FreestyleJobITest.java +++ b/src/test/java/io/jenkins/plugins/jfrog/integration/FreestyleJobITest.java @@ -8,7 +8,7 @@ import io.jenkins.plugins.jfrog.JfrogCliWrapper; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Test; -import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -18,16 +18,15 @@ * - JfrogCliWrapper (Build Environment wrapper) * - JfrogBuildInfoPublisher (Post-build action) */ +@WithJenkins class FreestyleJobITest extends PipelineTestBase { /** * JfrogCliWrapper should add JFROG_BINARY_PATH to the build environment. - * - * @param jenkins Jenkins instance injected automatically. */ @Test - public void testWrapperSetsBinaryPath(JenkinsRule jenkins) throws Exception { - setupJenkins(jenkins); + void testWrapperSetsBinaryPath() throws Exception { + setupJenkins(); configureJfrogCliFromReleases(StringUtils.EMPTY, true); FreeStyleProject project = jenkins.createFreeStyleProject("test-wrapper-path"); @@ -47,12 +46,10 @@ public void testWrapperSetsBinaryPath(JenkinsRule jenkins) throws Exception { /** * Without any installation selected, JfrogCliWrapper should still allow the build * to proceed (uses system PATH). - * - * @param jenkins Jenkins instance injected automatically. */ @Test - public void testWrapperNoInstallationUsesSysPath(JenkinsRule jenkins) throws Exception { - setupJenkins(jenkins); + void testWrapperNoInstallationUsesSysPath() throws Exception { + setupJenkins(); FreeStyleProject project = jenkins.createFreeStyleProject("test-wrapper-no-install"); JfrogCliWrapper wrapper = new JfrogCliWrapper(); @@ -67,12 +64,10 @@ public void testWrapperNoInstallationUsesSysPath(JenkinsRule jenkins) throws Exc /** * JfrogBuildInfoPublisher with publishOnlyOnSuccess=true must skip publish * when the build has failed. - * - * @param jenkins Jenkins instance injected automatically. */ @Test - public void testPublisherSkipsOnFailedBuild(JenkinsRule jenkins) throws Exception { - setupJenkins(jenkins); + void testPublisherSkipsOnFailedBuild() throws Exception { + setupJenkins(); FreeStyleProject project = jenkins.createFreeStyleProject("test-publisher-skip"); // Deliberately fail the build @@ -90,12 +85,10 @@ public void testPublisherSkipsOnFailedBuild(JenkinsRule jenkins) throws Exceptio /** * Freestyle flow: wrapper sets environment and build step successfully invokes the JFrog CLI. * Verifies that JFROG_BINARY_PATH points to the installation directory and 'jf -v' succeeds. - * - * @param jenkins Jenkins instance injected automatically. */ @Test - public void testFullFreestyleFlow(JenkinsRule jenkins) throws Exception { - setupJenkins(jenkins); + void testFullFreestyleFlow() throws Exception { + setupJenkins(); configureJfrogCliFromReleases(StringUtils.EMPTY, true); FreeStyleProject project = jenkins.createFreeStyleProject("test-freestyle-full"); @@ -115,12 +108,10 @@ public void testFullFreestyleFlow(JenkinsRule jenkins) throws Exception { /** * JfrogCliWrapper.DescriptorImpl.isApplicable must return true for a regular * FreeStyle project and false for a Matrix project (validated via reflection guard). - * - * @param jenkins Jenkins instance injected automatically. */ @Test - public void testWrapperNotApplicableForMatrixProject(JenkinsRule jenkins) throws Exception { - setupJenkins(jenkins); + void testWrapperNotApplicableForMatrixProject() throws Exception { + setupJenkins(); JfrogCliWrapper.DescriptorImpl descriptor = jenkins.jenkins.getDescriptorByType(JfrogCliWrapper.DescriptorImpl.class); diff --git a/src/test/java/io/jenkins/plugins/jfrog/integration/ParallelInstallITest.java b/src/test/java/io/jenkins/plugins/jfrog/integration/ParallelInstallITest.java index 3e5dbbe7..f6c76433 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/integration/ParallelInstallITest.java +++ b/src/test/java/io/jenkins/plugins/jfrog/integration/ParallelInstallITest.java @@ -3,70 +3,63 @@ import io.jenkins.plugins.jfrog.ReleasesInstaller; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Test; -import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; /** * Integration tests verifying that parallel JFrog CLI installations do not corrupt * the binary or produce race conditions. */ +@WithJenkins class ParallelInstallITest extends PipelineTestBase { /** * Two parallel pipeline stages that share the same JFrog CLI tool must both * complete successfully without binary corruption. - * - * @param jenkins Jenkins instance injected automatically. */ @Test - public void testParallelStagesSameToolVersion(JenkinsRule jenkins) throws Exception { - setupJenkins(jenkins); + void testParallelStagesSameToolVersion() throws Exception { + setupJenkins(); configureJfrogCliFromReleases(StringUtils.EMPTY, true); - runPipeline(jenkins, "parallel_install"); + runPipeline("parallel_install"); } /** * Two parallel pipeline stages each using a different CLI version must both * complete and produce independent, valid binaries. - * - * @param jenkins Jenkins instance injected automatically. */ @Test - public void testParallelStagesTwoToolVersions(JenkinsRule jenkins) throws Exception { - setupJenkins(jenkins); + void testParallelStagesTwoToolVersions() throws Exception { + setupJenkins(); // Tool 1: latest CLI version configureJfrogCliFromReleases(StringUtils.EMPTY, true); // Tool 2: a known older specific version to verify independent parallel installs ReleasesInstaller installer = new ReleasesInstaller(); installer.setVersion("2.29.2"); configureJfrogCliTool(JFROG_CLI_TOOL_NAME_2, installer, false); - runPipeline(jenkins, "parallel_install_two_tools"); + runPipeline("parallel_install_two_tools"); } /** * Repeated installs of the same version should detect the valid cached binary * and skip re-download (verified by both runs succeeding without error). - * - * @param jenkins Jenkins instance injected automatically. */ @Test - public void testRepeatedInstallSkipsDownload(JenkinsRule jenkins) throws Exception { - setupJenkins(jenkins); + void testRepeatedInstallSkipsDownload() throws Exception { + setupJenkins(); configureJfrogCliFromReleases(StringUtils.EMPTY, true); // First run installs - runPipeline(jenkins, "basic_version_command"); + runPipeline("basic_version_command"); // Second run should hit the cache - runPipeline(jenkins, "basic_version_command"); + runPipeline("basic_version_command"); } /** * Install from Artifactory in parallel: both stages must succeed. - * - * @param jenkins Jenkins instance injected automatically. */ @Test - public void testParallelInstallFromArtifactory(JenkinsRule jenkins) throws Exception { - setupJenkins(jenkins); + void testParallelInstallFromArtifactory() throws Exception { + setupJenkins(); configureJfrogCliFromArtifactory(JFROG_CLI_TOOL_NAME_1, TEST_CONFIGURED_SERVER_ID, getRepoKey(TestRepository.CLI_REMOTE_REPO), true); - runPipeline(jenkins, "parallel_install"); + runPipeline("parallel_install"); } }