From b5b3c58ee2aa62bb73a95c74c889c3e2b7e8550e Mon Sep 17 00:00:00 2001 From: Alexey Venderov Date: Wed, 12 Nov 2025 22:39:33 +0100 Subject: [PATCH 01/15] Extract shared logic into a separate interface --- .../BuildScanEnvironmentContributor.java | 14 +++--- .../injection/GradleBuildScanInjection.java | 7 +-- .../injection/GradleInjectionAware.java | 43 +++++------------ .../gradle/injection/InjectionAware.java | 46 +++++++++++++++++++ .../injection/MavenBuildScanInjection.java | 13 ++---- .../gradle/injection/MavenExtension.java | 2 +- .../MavenExtensionDownloadHandler.java | 46 +++++++++---------- .../gradle/injection/MavenInjectionAware.java | 43 +++++------------ 8 files changed, 107 insertions(+), 107 deletions(-) create mode 100644 plugin/src/main/java/hudson/plugins/gradle/injection/InjectionAware.java diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/BuildScanEnvironmentContributor.java b/plugin/src/main/java/hudson/plugins/gradle/injection/BuildScanEnvironmentContributor.java index 0dec8a12..3aacfd4e 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/BuildScanEnvironmentContributor.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/BuildScanEnvironmentContributor.java @@ -2,6 +2,7 @@ import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import com.google.common.annotations.VisibleForTesting; import hudson.EnvVars; import hudson.Extension; import hudson.model.EnvironmentContributor; @@ -34,11 +35,11 @@ public class BuildScanEnvironmentContributor extends EnvironmentContributor { private final ShortLivedTokenClientFactory shortLivedTokenClientFactory; public BuildScanEnvironmentContributor() { - this.shortLivedTokenClientFactory = new ShortLivedTokenClientFactory(); + this(new ShortLivedTokenClientFactory()); } - // required for testing - public BuildScanEnvironmentContributor(ShortLivedTokenClientFactory shortLivedTokenClientFactory) { + @VisibleForTesting + BuildScanEnvironmentContributor(ShortLivedTokenClientFactory shortLivedTokenClientFactory) { this.shortLivedTokenClientFactory = shortLivedTokenClientFactory; } @@ -67,10 +68,11 @@ public void buildEnvironmentFor(@Nonnull Run run, @Nonnull EnvVars envs, @Nonnul Secret shortLivedToken = getShortLivedToken(secretKey, logger); - run.addAction(DevelocityParametersAction.of(logger, shortLivedToken, secretPassword)); + run.addAction(DevelocityParametersAction.of(shortLivedToken, secretPassword)); } - private @Nullable Secret getShortLivedToken(Secret secretKey, DevelocityLogger logger) { + @Nullable + private Secret getShortLivedToken(Secret secretKey, DevelocityLogger logger) { if (secretKey == null) { return null; } @@ -143,7 +145,7 @@ static DevelocityParametersAction empty() { return EMPTY; } - private static DevelocityParametersAction of(DevelocityLogger logger, @Nullable Secret shortLivedToken, @Nullable Secret repoPassword) { + private static DevelocityParametersAction of(@Nullable Secret shortLivedToken, @Nullable Secret repoPassword) { List values = new ArrayList<>(); if (shortLivedToken != null) { values.add(new PasswordParameterValue(GRADLE_ENTERPRISE_ACCESS_KEY, shortLivedToken.getPlainText())); diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java b/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java index 88957f3c..9d45c8bf 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java @@ -18,8 +18,6 @@ import java.util.logging.Level; import java.util.logging.Logger; -import static hudson.plugins.gradle.injection.CopyUtil.*; - public class GradleBuildScanInjection implements GradleInjectionAware { private static final Logger LOGGER = Logger.getLogger(GradleBuildScanInjection.class.getName()); @@ -34,7 +32,7 @@ public class GradleBuildScanInjection implements GradleInjectionAware { private static final String GRADLE_DIR = ".gradle"; private static final String GRADLE_INIT_FILE = "init-build-scan.gradle"; - private final Supplier initScriptDigest = Suppliers.memoize(() -> unsafeResourceDigest(RESOURCE_INIT_SCRIPT_GRADLE)); + private final Supplier initScriptDigest = Suppliers.memoize(() -> CopyUtil.unsafeResourceDigest(RESOURCE_INIT_SCRIPT_GRADLE)); public void inject(Node node, EnvVars envGlobal, EnvVars envComputer) { if (node == null) { @@ -90,7 +88,7 @@ private void injectInitScript(VirtualChannel channel, String initScriptDirectory if (initScriptChanged(gradleInitScriptFile)) { LOGGER.info("Injecting Gradle init script " + gradleInitScriptFile); - copyResourceToNode(gradleInitScriptFile, RESOURCE_INIT_SCRIPT_GRADLE); + CopyUtil.copyResourceToNode(gradleInitScriptFile, RESOURCE_INIT_SCRIPT_GRADLE); } } @@ -184,5 +182,4 @@ private static String getRepositoryUsername(String gradlePluginRepositoryCredent private static String filePath(String... parts) { return String.join("/", parts); } - } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/GradleInjectionAware.java b/plugin/src/main/java/hudson/plugins/gradle/injection/GradleInjectionAware.java index 2c8f1950..fcdc8f16 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/GradleInjectionAware.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/GradleInjectionAware.java @@ -1,40 +1,21 @@ package hudson.plugins.gradle.injection; -import hudson.model.Node; -import hudson.plugins.gradle.util.CollectionUtil; +import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; +public interface GradleInjectionAware extends InjectionAware { -public interface GradleInjectionAware { - - default boolean isInjectionDisabledGlobally(InjectionConfig config) { - return config.isDisabled() || - InjectionUtil.isAnyInvalid( - InjectionConfig.checkRequiredUrl(config.getServer()), - InjectionConfig.checkRequiredVersion(config.getGradlePluginVersion()) - ); + @Override + default String getAgentVersion(InjectionConfig config) { + return config.getGradlePluginVersion(); } - default boolean isInjectionEnabledForNode(InjectionConfig config, Node node) { - if (isInjectionDisabledGlobally(config)) { - return false; - } - - Set disabledNodes = - CollectionUtil.safeStream(config.getGradleInjectionDisabledNodes()) - .map(NodeLabelItem::getLabel) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - Set enabledNodes = - CollectionUtil.safeStream(config.getGradleInjectionEnabledNodes()) - .map(NodeLabelItem::getLabel) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - return InjectionUtil.isInjectionEnabledForNode(node::getAssignedLabels, disabledNodes, enabledNodes); + @Override + default List getAgentInjectionDisabledNodes(InjectionConfig config) { + return config.getGradleInjectionDisabledNodes(); } + @Override + default List getAgentInjectionEnabledNodes(InjectionConfig config) { + return config.getGradleInjectionEnabledNodes(); + } } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionAware.java b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionAware.java new file mode 100644 index 00000000..37ee7d6a --- /dev/null +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionAware.java @@ -0,0 +1,46 @@ +package hudson.plugins.gradle.injection; + +import hudson.model.Node; +import hudson.plugins.gradle.util.CollectionUtil; + +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +public interface InjectionAware { + + String getAgentVersion(InjectionConfig config); + + List getAgentInjectionDisabledNodes(InjectionConfig config); + + List getAgentInjectionEnabledNodes(InjectionConfig config); + + default boolean isInjectionDisabledGlobally(InjectionConfig config) { + return config.isDisabled() || + InjectionUtil.isAnyInvalid( + InjectionConfig.checkRequiredUrl(config.getServer()), + InjectionConfig.checkRequiredVersion(getAgentVersion(config)) + ); + } + + default boolean isInjectionEnabledForNode(InjectionConfig config, Node node) { + if (isInjectionDisabledGlobally(config)) { + return false; + } + + Set disabledNodes = + CollectionUtil.safeStream(getAgentInjectionDisabledNodes(config)) + .map(NodeLabelItem::getLabel) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + Set enabledNodes = + CollectionUtil.safeStream(getAgentInjectionEnabledNodes(config)) + .map(NodeLabelItem::getLabel) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + return InjectionUtil.isInjectionEnabledForNode(node::getAssignedLabels, disabledNodes, enabledNodes); + } +} diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java index 98354129..53226dd7 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java @@ -4,7 +4,6 @@ import hudson.model.Node; import jenkins.model.Jenkins; -import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -14,9 +13,6 @@ import java.util.logging.Level; import java.util.logging.Logger; -import static hudson.plugins.gradle.injection.MavenExtClasspathUtils.constructExtClasspath; -import static hudson.plugins.gradle.injection.MavenExtClasspathUtils.isUnix; - public class MavenBuildScanInjection implements MavenInjectionAware { private static final Logger LOGGER = Logger.getLogger(MavenBuildScanInjection.class.getName()); @@ -74,7 +70,7 @@ private void inject(InjectionConfig config, Node node, FilePath nodeRootPath, Ma List extensions = new ArrayList<>(); FilePath controllerRootPath = Jenkins.get().getRootPath(); - MavenExtension develocityMavenExtension = MavenExtension.getDevelocityMavenExtension(config.getMavenExtensionVersion()); + MavenExtension develocityMavenExtension = MavenExtension.forVersion(config.getMavenExtensionVersion()); extensions.add(extensionsHandler.copyExtensionToAgent(develocityMavenExtension, controllerRootPath, nodeRootPath, extensionsDigest.get(develocityMavenExtension))); if (InjectionUtil.isInvalid(InjectionConfig.checkRequiredVersion(config.getCcudExtensionVersion()))) { extensionsHandler.deleteExtensionFromAgent(MavenExtension.CCUD, nodeRootPath); @@ -82,10 +78,10 @@ private void inject(InjectionConfig config, Node node, FilePath nodeRootPath, Ma extensions.add(extensionsHandler.copyExtensionToAgent(MavenExtension.CCUD, controllerRootPath, nodeRootPath, extensionsDigest.get(MavenExtension.CCUD))); } - boolean isUnix = isUnix(node); + boolean isUnix = MavenExtClasspathUtils.isUnix(node); List systemProperties = new ArrayList<>(); - systemProperties.add(new SystemProperty(MAVEN_EXT_CLASS_PATH_PROPERTY_KEY, constructExtClasspath(extensions, isUnix))); + systemProperties.add(new SystemProperty(MAVEN_EXT_CLASS_PATH_PROPERTY_KEY, MavenExtClasspathUtils.constructExtClasspath(extensions, isUnix))); systemProperties.add(new SystemProperty(DEVELOCITY_UPLOAD_IN_BACKGROUND_PROPERTY_KEY, "false")); systemProperties.add(new SystemProperty(BUILD_SCAN_UPLOAD_IN_BACKGROUND_PROPERTY_KEY, "false")); @@ -105,7 +101,7 @@ private void inject(InjectionConfig config, Node node, FilePath nodeRootPath, Ma // Configuration needed to support https://plugins.jenkins.io/maven-plugin/ extensions.add(extensionsHandler.copyExtensionToAgent(MavenExtension.CONFIGURATION, nodeRootPath)); - EnvUtil.setEnvVar(node, JENKINSGRADLEPLUGIN_MAVEN_PLUGIN_CONFIG_EXT_CLASSPATH, constructExtClasspath(extensions, isUnix)); + EnvUtil.setEnvVar(node, JENKINSGRADLEPLUGIN_MAVEN_PLUGIN_CONFIG_EXT_CLASSPATH, MavenExtClasspathUtils.constructExtClasspath(extensions, isUnix)); EnvUtil.setEnvVar(node, JENKINSGRADLEPLUGIN_MAVEN_PLUGIN_CONFIG_SERVER_URL, config.getServer()); if (config.isAllowUntrusted()) { EnvUtil.setEnvVar(node, JENKINSGRADLEPLUGIN_MAVEN_PLUGIN_CONFIG_ALLOW_UNTRUSTED_SERVER, "true"); @@ -127,5 +123,4 @@ private void cleanup(Node node, FilePath rootPath) { throw new IllegalStateException(e); } } - } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java index ecf37eae..d138ffcb 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java @@ -45,7 +45,7 @@ public String getDownloadMetadataFileName() { return downloadMetadataFileName; } - public static MavenExtension getDevelocityMavenExtension(String version) { + public static MavenExtension forVersion(String version) { return version.compareTo(LAST_GRADLE_ENTERPRISE_VERSION) > 0 ? DEVELOCITY : GRADLE_ENTERPRISE; } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java index 65a30f4c..f1ac031a 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java @@ -25,39 +25,39 @@ public class MavenExtensionDownloadHandler implements MavenInjectionAware { private final ExtensionClient extensionClient = new ExtensionClient(); public Map ensureExtensionsDownloaded(Supplier root, InjectionConfig injectionConfig) throws IOException { - if (!isInjectionDisabledGlobally(injectionConfig)) { - Map extensionsDigest = new HashMap<>(); - Path cacheDir = root.get().toPath().resolve(DOWNLOAD_CACHE_DIR); + if (isInjectionDisabledGlobally(injectionConfig)) { + return Collections.emptyMap(); + } - MavenExtension develocityMavenExtension = MavenExtension.getDevelocityMavenExtension(injectionConfig.getMavenExtensionVersion()); + Map extensionsDigest = new HashMap<>(); + Path cacheDir = root.get().toPath().resolve(DOWNLOAD_CACHE_DIR); - extensionsDigest.put(develocityMavenExtension, getOrDownloadExtensionDigest(injectionConfig, cacheDir, develocityMavenExtension)); - if (InjectionUtil.isValid(InjectionConfig.checkRequiredVersion(injectionConfig.getCcudExtensionVersion()))) { - extensionsDigest.put(MavenExtension.CCUD, getOrDownloadExtensionDigest(injectionConfig, cacheDir, MavenExtension.CCUD)); - } + MavenExtension develocityMavenExtension = MavenExtension.forVersion(injectionConfig.getMavenExtensionVersion()); - return extensionsDigest; + extensionsDigest.put(develocityMavenExtension, getOrDownloadExtensionDigest(injectionConfig, cacheDir, develocityMavenExtension)); + if (InjectionUtil.isValid(InjectionConfig.checkRequiredVersion(injectionConfig.getCcudExtensionVersion()))) { + extensionsDigest.put(MavenExtension.CCUD, getOrDownloadExtensionDigest(injectionConfig, cacheDir, MavenExtension.CCUD)); } - return Collections.emptyMap(); + return extensionsDigest; } public Map getExtensionDigests(Supplier rootDir, InjectionConfig injectionConfig) throws IOException { - if (!isInjectionDisabledGlobally(injectionConfig)) { - Map extensionDigests = new HashMap<>(); - Path cacheDir = rootDir.get().toPath().resolve(DOWNLOAD_CACHE_DIR); + if (isInjectionDisabledGlobally(injectionConfig)) { + return Collections.emptyMap(); + } - MavenExtension develocityMavenExtension = MavenExtension.getDevelocityMavenExtension(injectionConfig.getMavenExtensionVersion()); + Map extensionDigests = new HashMap<>(); + Path cacheDir = rootDir.get().toPath().resolve(DOWNLOAD_CACHE_DIR); - getExtensionDigest(cacheDir, develocityMavenExtension).ifPresent(it -> extensionDigests.put(develocityMavenExtension, it)); - if (InjectionUtil.isValid(InjectionConfig.checkRequiredVersion(injectionConfig.getCcudExtensionVersion()))) { - getExtensionDigest(cacheDir, MavenExtension.CCUD).ifPresent(it -> extensionDigests.put(MavenExtension.CCUD, it)); - } + MavenExtension develocityMavenExtension = MavenExtension.forVersion(injectionConfig.getMavenExtensionVersion()); - return extensionDigests; + getExtensionDigest(cacheDir, develocityMavenExtension).ifPresent(it -> extensionDigests.put(develocityMavenExtension, it)); + if (InjectionUtil.isValid(InjectionConfig.checkRequiredVersion(injectionConfig.getCcudExtensionVersion()))) { + getExtensionDigest(cacheDir, MavenExtension.CCUD).ifPresent(it -> extensionDigests.put(MavenExtension.CCUD, it)); } - return Collections.emptyMap(); + return extensionDigests; } private static Optional getExtensionDigest(Path parent, MavenExtension extension) throws IOException { @@ -84,12 +84,10 @@ private String getOrDownloadExtensionDigest(InjectionConfig injectionConfig, Pat if (!extensionVersion.equals(version)) { return downloadExtension(injectionConfig, parent, extension, metadataFile, version); - } else { - return extensionDigest; } - } else { - return downloadExtension(injectionConfig, parent, extension, metadataFile, version); + return extensionDigest; } + return downloadExtension(injectionConfig, parent, extension, metadataFile, version); } private String downloadExtension( diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenInjectionAware.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenInjectionAware.java index ac2ab45b..4f600d85 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenInjectionAware.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenInjectionAware.java @@ -1,13 +1,8 @@ package hudson.plugins.gradle.injection; -import hudson.model.Node; -import hudson.plugins.gradle.util.CollectionUtil; +import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -public interface MavenInjectionAware { +public interface MavenInjectionAware extends InjectionAware { String JENKINSGRADLEPLUGIN_MAVEN_PLUGIN_CONFIG_EXT_CLASSPATH = "JENKINSGRADLEPLUGIN_MAVEN_PLUGIN_CONFIG_EXT_CLASSPATH"; // Use different variables so Gradle and Maven injections can work independently on the same node @@ -37,32 +32,18 @@ public interface MavenInjectionAware { DEVELOCITY_CAPTURE_FILE_FINGERPRINTS_PROPERTY_KEY ); - default boolean isInjectionDisabledGlobally(InjectionConfig config) { - return config.isDisabled() || - InjectionUtil.isAnyInvalid( - InjectionConfig.checkRequiredUrl(config.getServer()), - InjectionConfig.checkRequiredVersion(config.getMavenExtensionVersion()) - ); + @Override + default String getAgentVersion(InjectionConfig config) { + return config.getMavenExtensionVersion(); } - default boolean isInjectionEnabledForNode(InjectionConfig config, Node node) { - if (isInjectionDisabledGlobally(config)) { - return false; - } - - Set disabledNodes = - CollectionUtil.safeStream(config.getMavenInjectionDisabledNodes()) - .map(NodeLabelItem::getLabel) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - Set enabledNodes = - CollectionUtil.safeStream(config.getMavenInjectionEnabledNodes()) - .map(NodeLabelItem::getLabel) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - return InjectionUtil.isInjectionEnabledForNode(node::getAssignedLabels, disabledNodes, enabledNodes); + @Override + default List getAgentInjectionDisabledNodes(InjectionConfig config) { + return config.getMavenInjectionDisabledNodes(); } + @Override + default List getAgentInjectionEnabledNodes(InjectionConfig config) { + return config.getMavenInjectionEnabledNodes(); + } } From 0cf70570f8b316a9867a7ed57fa79ed1717c305a Mon Sep 17 00:00:00 2001 From: Alexey Venderov Date: Wed, 12 Nov 2025 23:04:30 +0100 Subject: [PATCH 02/15] Cleanup --- .../gradle/injection/BuildScanEnvironmentContributor.java | 1 + .../gradle/injection/DevelocityComputerListener.java | 1 + .../plugins/gradle/injection/DevelocityErrorsAction.java | 2 ++ .../hudson/plugins/gradle/injection/GitScmListener.java | 1 + .../plugins/gradle/injection/InitScriptVariables.java | 8 -------- .../gradle/injection/InjectionConfigChangeListener.java | 1 + .../hudson/plugins/gradle/injection/MavenCoordinates.java | 4 ---- .../plugins/gradle/injection/MavenExtensionsHandler.java | 1 - 8 files changed, 6 insertions(+), 13 deletions(-) diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/BuildScanEnvironmentContributor.java b/plugin/src/main/java/hudson/plugins/gradle/injection/BuildScanEnvironmentContributor.java index 3aacfd4e..a896d26d 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/BuildScanEnvironmentContributor.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/BuildScanEnvironmentContributor.java @@ -34,6 +34,7 @@ public class BuildScanEnvironmentContributor extends EnvironmentContributor { private final ShortLivedTokenClientFactory shortLivedTokenClientFactory; + @SuppressWarnings("unused") public BuildScanEnvironmentContributor() { this(new ShortLivedTokenClientFactory()); } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityComputerListener.java b/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityComputerListener.java index 35a57cbb..8949b5cb 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityComputerListener.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityComputerListener.java @@ -29,6 +29,7 @@ public class DevelocityComputerListener extends ComputerListener { private final MavenExtensionDownloadHandler mavenExtensionDownloadHandler; private final Supplier injectionConfigSupplier; + @SuppressWarnings("unused") public DevelocityComputerListener() { this( new GradleBuildScanInjection(), diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityErrorsAction.java b/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityErrorsAction.java index c9a0776f..e42a9780 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityErrorsAction.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityErrorsAction.java @@ -23,8 +23,10 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +@SuppressWarnings("unused") @Extension public class DevelocityErrorsAction implements RootAction, StaplerProxy { + @Override public String getIconFileName() { return isVisible() && Jenkins.get().hasPermission(Jenkins.ADMINISTER) ? "/plugin/gradle/images/svgs/gradle-build-scan.svg" : null; diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/GitScmListener.java b/plugin/src/main/java/hudson/plugins/gradle/injection/GitScmListener.java index 8d016494..467d1a83 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/GitScmListener.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/GitScmListener.java @@ -26,6 +26,7 @@ import static hudson.plugins.gradle.injection.MavenInjectionAware.MAVEN_OPTS_HANDLER; import static hudson.plugins.gradle.injection.MavenOptsHandler.MAVEN_OPTS; +@SuppressWarnings("unused") @Extension public class GitScmListener extends SCMListener { diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/InitScriptVariables.java b/plugin/src/main/java/hudson/plugins/gradle/injection/InitScriptVariables.java index faa2b343..40eb88bc 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/InitScriptVariables.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/InitScriptVariables.java @@ -24,15 +24,7 @@ public enum InitScriptVariables { this.templateName = templateName; } - String getTemplateName() { - return templateName; - } - String getEnvVar() { return templateName.toUpperCase().replace('.', '_').replace('-', '_'); } - - String sysProp(String value) { - return "-D" + getTemplateName() + "=" + value; - } } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfigChangeListener.java b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfigChangeListener.java index 2860480d..393e09d9 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfigChangeListener.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfigChangeListener.java @@ -31,6 +31,7 @@ public class InjectionConfigChangeListener extends SaveableListener { private final Supplier globalEnvVarsSupplier; private final Supplier> computersSupplier; + @SuppressWarnings("unused") public InjectionConfigChangeListener() { this( new GradleBuildScanInjection(), diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenCoordinates.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenCoordinates.java index 262ae308..f4119ac8 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenCoordinates.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenCoordinates.java @@ -62,10 +62,6 @@ String artifactId() { return artifactId; } - String version() { - return version; - } - @Override public String toString() { return String.format("%s:%s:%s", groupId, artifactId, version); diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionsHandler.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionsHandler.java index 876c44a6..923fc93e 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionsHandler.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionsHandler.java @@ -5,7 +5,6 @@ import hudson.FilePath; import java.io.IOException; -import java.nio.file.Files; import java.util.Arrays; import java.util.Map; import java.util.Objects; From 47599eb5e4abc088cf651f4ef8f15d2695b9e41e Mon Sep 17 00:00:00 2001 From: Alexey Venderov Date: Thu, 13 Nov 2025 12:26:39 +0100 Subject: [PATCH 03/15] Implement minimum config for npm agent --- .../injection/GradleInjectionAware.java | 4 ++ .../gradle/injection/InjectionAware.java | 4 ++ .../gradle/injection/InjectionConfig.java | 65 ++++++++++++++++--- .../gradle/injection/MavenInjectionAware.java | 4 ++ .../injection/npm/NpmInjectionAware.java | 29 +++++++++ .../plugins/gradle/util/CollectionUtil.java | 2 +- .../injection/InjectionConfig/config.jelly | 26 ++++++++ .../InjectionConfig/help-npmAgentVersion.html | 3 + .../help-npmInjectionDisabledNodes.html | 5 ++ .../webapp/help-npmInjectionEnabledNodes.html | 5 ++ 10 files changed, 137 insertions(+), 10 deletions(-) create mode 100644 plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmInjectionAware.java create mode 100644 plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/help-npmAgentVersion.html create mode 100644 plugin/src/main/webapp/help-npmInjectionDisabledNodes.html create mode 100644 plugin/src/main/webapp/help-npmInjectionEnabledNodes.html diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/GradleInjectionAware.java b/plugin/src/main/java/hudson/plugins/gradle/injection/GradleInjectionAware.java index fcdc8f16..b0062eeb 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/GradleInjectionAware.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/GradleInjectionAware.java @@ -1,19 +1,23 @@ package hudson.plugins.gradle.injection; +import javax.annotation.Nullable; import java.util.List; public interface GradleInjectionAware extends InjectionAware { + @Nullable @Override default String getAgentVersion(InjectionConfig config) { return config.getGradlePluginVersion(); } + @Nullable @Override default List getAgentInjectionDisabledNodes(InjectionConfig config) { return config.getGradleInjectionDisabledNodes(); } + @Nullable @Override default List getAgentInjectionEnabledNodes(InjectionConfig config) { return config.getGradleInjectionEnabledNodes(); diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionAware.java b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionAware.java index 37ee7d6a..d42e2d09 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionAware.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionAware.java @@ -3,6 +3,7 @@ import hudson.model.Node; import hudson.plugins.gradle.util.CollectionUtil; +import javax.annotation.Nullable; import java.util.List; import java.util.Objects; import java.util.Set; @@ -10,10 +11,13 @@ public interface InjectionAware { + @Nullable String getAgentVersion(InjectionConfig config); + @Nullable List getAgentInjectionDisabledNodes(InjectionConfig config); + @Nullable List getAgentInjectionEnabledNodes(InjectionConfig config); default boolean isInjectionDisabledGlobally(InjectionConfig config) { diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfig.java b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfig.java index f230f1b4..ee75d3e8 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfig.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfig.java @@ -36,7 +36,9 @@ import org.kohsuke.stapler.verb.POST; import javax.annotation.CheckForNull; +import javax.annotation.Nullable; import java.io.IOException; +import java.io.Serializable; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -96,6 +98,10 @@ public class InjectionConfig extends GlobalConfiguration { private ImmutableList mavenInjectionDisabledNodes; private Boolean mavenCaptureGoalInputFiles; + private String npmAgentVersion; + private ImmutableList npmInjectionEnabledNodes; + private ImmutableList npmInjectionDisabledNodes; + private boolean enforceUrl; private boolean checkForBuildAgentErrors; @@ -234,8 +240,7 @@ public List getGradleInjectionEnabledNodes() { @DataBoundSetter public void setGradleInjectionEnabledNodes(List gradleInjectionEnabledNodes) { - this.gradleInjectionEnabledNodes = - gradleInjectionEnabledNodes == null ? null : ImmutableList.copyOf(gradleInjectionEnabledNodes); + this.gradleInjectionEnabledNodes = safeImmutableListCopy(gradleInjectionEnabledNodes); } @CheckForNull @@ -245,8 +250,7 @@ public List getGradleInjectionDisabledNodes() { @DataBoundSetter public void setGradleInjectionDisabledNodes(List gradleInjectionDisabledNodes) { - this.gradleInjectionDisabledNodes = - gradleInjectionDisabledNodes == null ? null : ImmutableList.copyOf(gradleInjectionDisabledNodes); + this.gradleInjectionDisabledNodes = safeImmutableListCopy(gradleInjectionDisabledNodes); } public Boolean isGradleCaptureTaskInputFiles() { @@ -323,8 +327,7 @@ public List getMavenInjectionEnabledNodes() { @DataBoundSetter public void setMavenInjectionEnabledNodes(List mavenInjectionEnabledNodes) { - this.mavenInjectionEnabledNodes = - mavenInjectionEnabledNodes == null ? null : ImmutableList.copyOf(mavenInjectionEnabledNodes); + this.mavenInjectionEnabledNodes = safeImmutableListCopy(mavenInjectionEnabledNodes); } @CheckForNull @@ -334,8 +337,7 @@ public List getMavenInjectionDisabledNodes() { @DataBoundSetter public void setMavenInjectionDisabledNodes(List mavenInjectionDisabledNodes) { - this.mavenInjectionDisabledNodes = - mavenInjectionDisabledNodes == null ? null : ImmutableList.copyOf(mavenInjectionDisabledNodes); + this.mavenInjectionDisabledNodes = safeImmutableListCopy(mavenInjectionDisabledNodes); } public Boolean isMavenCaptureGoalInputFiles() { @@ -347,6 +349,35 @@ public void setMavenCaptureGoalInputFiles(Boolean mavenCaptureGoalInputFiles) { this.mavenCaptureGoalInputFiles = mavenCaptureGoalInputFiles; } + public String getNpmAgentVersion() { + return npmAgentVersion; + } + + @DataBoundSetter + public void setNpmAgentVersion(String npmAgentVersion) { + this.npmAgentVersion = Util.fixEmptyAndTrim(npmAgentVersion); + } + + @CheckForNull + public List getNpmInjectionEnabledNodes() { + return npmInjectionEnabledNodes; + } + + @DataBoundSetter + public void setNpmInjectionEnabledNodes(List npmInjectionEnabledNodes) { + this.npmInjectionEnabledNodes = safeImmutableListCopy(npmInjectionEnabledNodes); + } + + @CheckForNull + public List getNpmInjectionDisabledNodes() { + return npmInjectionDisabledNodes; + } + + @DataBoundSetter + public void setNpmInjectionDisabledNodes(List npmInjectionDisabledNodes) { + this.npmInjectionDisabledNodes = safeImmutableListCopy(npmInjectionDisabledNodes); + } + @DataBoundSetter public void setVcsRepositoryFilter(String vcsRepositoryFilter) { this.parsedVcsRepositoryFilter = VcsRepositoryFilter.of(vcsRepositoryFilter); @@ -419,6 +450,9 @@ private void clearRepeatableProperties() { setMavenInjectionEnabledNodes(null); setMavenInjectionDisabledNodes(null); + + setNpmInjectionEnabledNodes(null); + setNpmInjectionDisabledNodes(null); } @Restricted(NoExternalUse.class) @@ -558,6 +592,16 @@ public FormValidation doCheckCcudExtensionVersion(@QueryParameter String value) return checkVersion(value); } + @Restricted(NoExternalUse.class) + @POST + public FormValidation doCheckNpmAgentVersion(@QueryParameter String value) { + if (doesNotHaveAdministerPermission()) { + return FormValidation.error("Validating npm agent version requires 'Administer' permission"); + } + + return checkVersion(value); + } + @SuppressWarnings("called by Jelly") @POST public ListBoxModel doFillGradlePluginRepositoryCredentialIdItems(@AncestorInPath Item project) { @@ -676,7 +720,7 @@ protected Object readResolve() throws IOException, FormException { StandardUsernamePasswordCredentials standardUsernameCredentials = new UsernamePasswordCredentialsImpl( CredentialsScope.GLOBAL, UUID.randomUUID().toString(), - "Migrated Gradle Plugin Respoitory credentials", + "Migrated Gradle Plugin Repository credentials", gradlePluginRepositoryUsername, gradlePluginRepositoryPassword.getPlainText() ); @@ -708,4 +752,7 @@ private static boolean doesNotHaveAdministerPermission() { return jenkins == null || !jenkins.hasPermission(Jenkins.ADMINISTER); } + private static ImmutableList safeImmutableListCopy(@Nullable List list) { + return list == null ? null : ImmutableList.copyOf(list); + } } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenInjectionAware.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenInjectionAware.java index 4f600d85..c2961569 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenInjectionAware.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenInjectionAware.java @@ -1,5 +1,6 @@ package hudson.plugins.gradle.injection; +import javax.annotation.Nullable; import java.util.List; public interface MavenInjectionAware extends InjectionAware { @@ -32,16 +33,19 @@ public interface MavenInjectionAware extends InjectionAware { DEVELOCITY_CAPTURE_FILE_FINGERPRINTS_PROPERTY_KEY ); + @Nullable @Override default String getAgentVersion(InjectionConfig config) { return config.getMavenExtensionVersion(); } + @Nullable @Override default List getAgentInjectionDisabledNodes(InjectionConfig config) { return config.getMavenInjectionDisabledNodes(); } + @Nullable @Override default List getAgentInjectionEnabledNodes(InjectionConfig config) { return config.getMavenInjectionEnabledNodes(); diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmInjectionAware.java b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmInjectionAware.java new file mode 100644 index 00000000..0c5f4247 --- /dev/null +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmInjectionAware.java @@ -0,0 +1,29 @@ +package hudson.plugins.gradle.injection.npm; + +import hudson.plugins.gradle.injection.InjectionAware; +import hudson.plugins.gradle.injection.InjectionConfig; +import hudson.plugins.gradle.injection.NodeLabelItem; + +import javax.annotation.Nullable; +import java.util.List; + +public interface NpmInjectionAware extends InjectionAware { + + @Nullable + @Override + default String getAgentVersion(InjectionConfig config) { + return config.getNpmAgentVersion(); + } + + @Nullable + @Override + default List getAgentInjectionDisabledNodes(InjectionConfig config) { + return config.getNpmInjectionDisabledNodes(); + } + + @Nullable + @Override + default List getAgentInjectionEnabledNodes(InjectionConfig config) { + return config.getNpmInjectionEnabledNodes(); + } +} diff --git a/plugin/src/main/java/hudson/plugins/gradle/util/CollectionUtil.java b/plugin/src/main/java/hudson/plugins/gradle/util/CollectionUtil.java index 0ca2ce1d..311e5f92 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/util/CollectionUtil.java +++ b/plugin/src/main/java/hudson/plugins/gradle/util/CollectionUtil.java @@ -13,7 +13,7 @@ public final class CollectionUtil { private CollectionUtil() { } - public static Stream safeStream(Collection col) { + public static Stream safeStream(@Nullable Collection col) { if (col == null) { return Stream.empty(); } diff --git a/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly b/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly index 1f01d897..ec3e6001 100644 --- a/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly +++ b/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly @@ -147,6 +147,32 @@ + + + + + + + +
+ +
+
+
+
+ + + +
+ +
+
+
+
+
+ diff --git a/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/help-npmAgentVersion.html b/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/help-npmAgentVersion.html new file mode 100644 index 00000000..784d6e83 --- /dev/null +++ b/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/help-npmAgentVersion.html @@ -0,0 +1,3 @@ +
+ The version of the Develocity npm agent to apply to npm builds. +
diff --git a/plugin/src/main/webapp/help-npmInjectionDisabledNodes.html b/plugin/src/main/webapp/help-npmInjectionDisabledNodes.html new file mode 100644 index 00000000..10986deb --- /dev/null +++ b/plugin/src/main/webapp/help-npmInjectionDisabledNodes.html @@ -0,0 +1,5 @@ +
+ The list of node labels where the Develocity npm agent auto-injection should be disabled. + By default, all nodes are enabled. + Disabled nodes have precedence over enabled nodes. +
diff --git a/plugin/src/main/webapp/help-npmInjectionEnabledNodes.html b/plugin/src/main/webapp/help-npmInjectionEnabledNodes.html new file mode 100644 index 00000000..36bf5bd9 --- /dev/null +++ b/plugin/src/main/webapp/help-npmInjectionEnabledNodes.html @@ -0,0 +1,5 @@ +
+ The list of node labels where the Develocity npm agent auto-injection should be enabled. + By default, all nodes are enabled. + Disabled nodes have precedence over enabled nodes. +
From 658b806a9d3db84785716cebf75c4d21675f2e6b Mon Sep 17 00:00:00 2001 From: Alexey Venderov Date: Thu, 13 Nov 2025 12:36:23 +0100 Subject: [PATCH 04/15] Refactor --- .../injection/GradleBuildScanInjection.java | 21 ++++++++++++------- .../injection/MavenBuildScanInjection.java | 9 +++++--- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java b/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java index 9d45c8bf..5ec3c720 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java @@ -18,6 +18,9 @@ import java.util.logging.Level; import java.util.logging.Logger; +import static hudson.plugins.gradle.injection.CopyUtil.copyResourceToNode; +import static hudson.plugins.gradle.injection.CopyUtil.unsafeResourceDigest; + public class GradleBuildScanInjection implements GradleInjectionAware { private static final Logger LOGGER = Logger.getLogger(GradleBuildScanInjection.class.getName()); @@ -32,7 +35,7 @@ public class GradleBuildScanInjection implements GradleInjectionAware { private static final String GRADLE_DIR = ".gradle"; private static final String GRADLE_INIT_FILE = "init-build-scan.gradle"; - private final Supplier initScriptDigest = Suppliers.memoize(() -> CopyUtil.unsafeResourceDigest(RESOURCE_INIT_SCRIPT_GRADLE)); + private final Supplier initScriptDigest = Suppliers.memoize(() -> unsafeResourceDigest(RESOURCE_INIT_SCRIPT_GRADLE)); public void inject(Node node, EnvVars envGlobal, EnvVars envComputer) { if (node == null) { @@ -62,14 +65,16 @@ private static String getInitScriptDirectory(EnvVars envGlobal, EnvVars envCompu if (gradleHomeOverride != null) { return filePath(gradleHomeOverride, INIT_DIR); - } else if (homeOverride != null) { - return filePath(homeOverride, GRADLE_DIR, INIT_DIR); - } else { - String home = EnvUtil.getEnv(envComputer, HOME); - Preconditions.checkState(home != null, "HOME is not set"); + } - return filePath(home, GRADLE_DIR, INIT_DIR); + if (homeOverride != null) { + return filePath(homeOverride, GRADLE_DIR, INIT_DIR); } + + String home = EnvUtil.getEnv(envComputer, HOME); + Preconditions.checkState(home != null, "HOME is not set"); + + return filePath(home, GRADLE_DIR, INIT_DIR); } private void inject(InjectionConfig config, Node node, String initScriptDirectory) { @@ -88,7 +93,7 @@ private void injectInitScript(VirtualChannel channel, String initScriptDirectory if (initScriptChanged(gradleInitScriptFile)) { LOGGER.info("Injecting Gradle init script " + gradleInitScriptFile); - CopyUtil.copyResourceToNode(gradleInitScriptFile, RESOURCE_INIT_SCRIPT_GRADLE); + copyResourceToNode(gradleInitScriptFile, RESOURCE_INIT_SCRIPT_GRADLE); } } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java index 53226dd7..76d680ad 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java @@ -13,6 +13,9 @@ import java.util.logging.Level; import java.util.logging.Logger; +import static hudson.plugins.gradle.injection.MavenExtClasspathUtils.constructExtClasspath; +import static hudson.plugins.gradle.injection.MavenExtClasspathUtils.isUnix; + public class MavenBuildScanInjection implements MavenInjectionAware { private static final Logger LOGGER = Logger.getLogger(MavenBuildScanInjection.class.getName()); @@ -78,10 +81,10 @@ private void inject(InjectionConfig config, Node node, FilePath nodeRootPath, Ma extensions.add(extensionsHandler.copyExtensionToAgent(MavenExtension.CCUD, controllerRootPath, nodeRootPath, extensionsDigest.get(MavenExtension.CCUD))); } - boolean isUnix = MavenExtClasspathUtils.isUnix(node); + boolean isUnix = isUnix(node); List systemProperties = new ArrayList<>(); - systemProperties.add(new SystemProperty(MAVEN_EXT_CLASS_PATH_PROPERTY_KEY, MavenExtClasspathUtils.constructExtClasspath(extensions, isUnix))); + systemProperties.add(new SystemProperty(MAVEN_EXT_CLASS_PATH_PROPERTY_KEY, constructExtClasspath(extensions, isUnix))); systemProperties.add(new SystemProperty(DEVELOCITY_UPLOAD_IN_BACKGROUND_PROPERTY_KEY, "false")); systemProperties.add(new SystemProperty(BUILD_SCAN_UPLOAD_IN_BACKGROUND_PROPERTY_KEY, "false")); @@ -101,7 +104,7 @@ private void inject(InjectionConfig config, Node node, FilePath nodeRootPath, Ma // Configuration needed to support https://plugins.jenkins.io/maven-plugin/ extensions.add(extensionsHandler.copyExtensionToAgent(MavenExtension.CONFIGURATION, nodeRootPath)); - EnvUtil.setEnvVar(node, JENKINSGRADLEPLUGIN_MAVEN_PLUGIN_CONFIG_EXT_CLASSPATH, MavenExtClasspathUtils.constructExtClasspath(extensions, isUnix)); + EnvUtil.setEnvVar(node, JENKINSGRADLEPLUGIN_MAVEN_PLUGIN_CONFIG_EXT_CLASSPATH, constructExtClasspath(extensions, isUnix)); EnvUtil.setEnvVar(node, JENKINSGRADLEPLUGIN_MAVEN_PLUGIN_CONFIG_SERVER_URL, config.getServer()); if (config.isAllowUntrusted()) { EnvUtil.setEnvVar(node, JENKINSGRADLEPLUGIN_MAVEN_PLUGIN_CONFIG_ALLOW_UNTRUSTED_SERVER, "true"); From 876f44076276f730dc6dcd82378bcf505bff6ac3 Mon Sep 17 00:00:00 2001 From: Alexey Venderov Date: Thu, 13 Nov 2025 21:08:32 +0100 Subject: [PATCH 05/15] Extract metadata manipulation logic into a separate class --- .../gradle/injection/ArtifactDigest.java | 3 ++ .../gradle/injection/ArtifactMetadata.java | 38 ++++++++++++++ .../plugins/gradle/injection/CopyUtil.java | 7 +-- .../gradle/injection/InjectionUtil.java | 14 +++--- .../gradle/injection/MavenExtension.java | 21 -------- .../MavenExtensionDownloadHandler.java | 49 +++++++++---------- .../injection/RepositoryCredentials.java | 4 ++ .../injection/extension/ExtensionClient.java | 4 +- 8 files changed, 77 insertions(+), 63 deletions(-) create mode 100644 plugin/src/main/java/hudson/plugins/gradle/injection/ArtifactDigest.java create mode 100644 plugin/src/main/java/hudson/plugins/gradle/injection/ArtifactMetadata.java create mode 100644 plugin/src/main/java/hudson/plugins/gradle/injection/RepositoryCredentials.java diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/ArtifactDigest.java b/plugin/src/main/java/hudson/plugins/gradle/injection/ArtifactDigest.java new file mode 100644 index 00000000..ad05cba7 --- /dev/null +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/ArtifactDigest.java @@ -0,0 +1,3 @@ +package hudson.plugins.gradle.injection; + +public record ArtifactDigest(String digest) {} diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/ArtifactMetadata.java b/plugin/src/main/java/hudson/plugins/gradle/injection/ArtifactMetadata.java new file mode 100644 index 00000000..e821fcf0 --- /dev/null +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/ArtifactMetadata.java @@ -0,0 +1,38 @@ +package hudson.plugins.gradle.injection; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +public record ArtifactMetadata(String version, ArtifactDigest digest) { + + private static final String SEPARATOR = ","; + + // Adapter from the old interface + public ArtifactMetadata(String version, String digest) { + this(version, new ArtifactDigest(digest)); + } + + public boolean isForVersion(@Nullable String requiredVersion) { + return version.equals(requiredVersion); + } + + public void writeToFile(Path metadataFile) throws IOException { + String content = version + SEPARATOR + digest.digest(); + Files.writeString(metadataFile, content); + } + + public static Optional readFromFile(Path metadataFile) throws IOException { + if (Files.exists(metadataFile)) { + String[] metadata = Files.readString(metadataFile).split(SEPARATOR); + if (metadata.length == 2) { + String version = metadata[0]; + ArtifactDigest digest = new ArtifactDigest(metadata[1]); + return Optional.of(new ArtifactMetadata(version, digest)); + } + } + return Optional.empty(); + } +} diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/CopyUtil.java b/plugin/src/main/java/hudson/plugins/gradle/injection/CopyUtil.java index f772e907..f6ab9125 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/CopyUtil.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/CopyUtil.java @@ -2,14 +2,9 @@ import hudson.FilePath; import hudson.Util; -import jenkins.model.Jenkins; -import org.apache.commons.io.IOUtils; import java.io.IOException; import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; public final class CopyUtil { @@ -24,7 +19,7 @@ public static void copyResourceToNode(FilePath nodePath, String resourceName) th } public static void copyDownloadedResourceToNode(FilePath controllerRootPath, FilePath nodePath, String resourceName) throws IOException, InterruptedException { - nodePath.copyFrom(controllerRootPath.child(MavenExtensionDownloadHandler.DOWNLOAD_CACHE_DIR).child(resourceName)); + nodePath.copyFrom(controllerRootPath.child(InjectionUtil.DOWNLOAD_CACHE_DIR).child(resourceName)); } public static String unsafeResourceDigest(String resourceName) { diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionUtil.java b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionUtil.java index 3692cd66..795aa260 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionUtil.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionUtil.java @@ -19,8 +19,8 @@ public final class InjectionUtil { private static final String MAVEN_PLUGIN_SHORT_NAME = "maven-plugin"; + public static final String DOWNLOAD_CACHE_DIR = "jenkins-gradle-plugin/cache"; public static final VersionNumber MINIMUM_SUPPORTED_MAVEN_PLUGIN_VERSION = new VersionNumber("3.20"); - public static final String JENKINSGRADLEPLUGIN_GLOBAL_AUTO_INJECTION_CHECK = "JENKINSGRADLEPLUGIN_GLOBAL_AUTO_INJECTION_CHECK"; private InjectionUtil() { @@ -36,13 +36,13 @@ public static Optional mavenPluginVersionNumber() { public static Optional maybeGetPlugin(String pluginShortName) { return Optional.ofNullable(Jenkins.getInstanceOrNull()) - .map(Jenkins::getPluginManager) - .map(pm -> pm.getPlugin(pluginShortName)); + .map(Jenkins::getPluginManager) + .map(pm -> pm.getPlugin(pluginShortName)); } public static boolean isSupportedMavenPluginVersion(@Nullable VersionNumber mavenPluginVersion) { return mavenPluginVersion != null - && !mavenPluginVersion.isOlderThan(MINIMUM_SUPPORTED_MAVEN_PLUGIN_VERSION); + && !mavenPluginVersion.isOlderThan(MINIMUM_SUPPORTED_MAVEN_PLUGIN_VERSION); } public static boolean isInvalid(FormValidation validation) { @@ -61,9 +61,9 @@ public static boolean isInjectionEnabledForNode(Supplier> assigne Set disabledNodes, Set enabledNodes) { Set labels = - CollectionUtil.safeStream(assignedLabels.get()) - .map(LabelAtom::getName) - .collect(Collectors.toSet()); + CollectionUtil.safeStream(assignedLabels.get()) + .map(LabelAtom::getName) + .collect(Collectors.toSet()); return isNotDisabled(labels, disabledNodes) && isEnabled(labels, enabledNodes); } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java index d138ffcb..df7f32ff 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java @@ -56,25 +56,4 @@ public String createDownloadUrl(String version, @Nullable String repositoryUrl) return String.format(repositoryUrl, this.getName(), version, this.getName(), version); } - - public static final class RepositoryCredentials { - - private final String username; - private final String password; - - public RepositoryCredentials(String username, String password) { - this.username = username; - this.password = password; - } - - public String username() { - return username; - } - - public String password() { - return password; - } - - } - } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java index f1ac031a..df236771 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java @@ -5,6 +5,7 @@ import hudson.Util; import hudson.plugins.gradle.injection.extension.ExtensionClient; +import javax.annotation.Nullable; import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; @@ -18,9 +19,9 @@ import java.util.Optional; import java.util.function.Supplier; -public class MavenExtensionDownloadHandler implements MavenInjectionAware { +import static hudson.plugins.gradle.injection.InjectionUtil.DOWNLOAD_CACHE_DIR; - public static final String DOWNLOAD_CACHE_DIR = "jenkins-gradle-plugin/cache"; +public class MavenExtensionDownloadHandler implements MavenInjectionAware { private final ExtensionClient extensionClient = new ExtensionClient(); @@ -62,13 +63,9 @@ public Map getExtensionDigests(Supplier rootDir, I private static Optional getExtensionDigest(Path parent, MavenExtension extension) throws IOException { Path metadataFile = parent.resolve(extension.getDownloadMetadataFileName()); - if (Files.exists(metadataFile)) { - String[] metadata = Files.readString(metadataFile).split(","); - - return Optional.of(metadata[1]); - } - - return Optional.empty(); + return ArtifactMetadata.readFromFile(metadataFile) + .map(ArtifactMetadata::digest) + .map(ArtifactDigest::digest); } private String getOrDownloadExtensionDigest(InjectionConfig injectionConfig, Path parent, MavenExtension extension) throws IOException { @@ -77,17 +74,14 @@ private String getOrDownloadExtensionDigest(InjectionConfig injectionConfig, Pat ? injectionConfig.getCcudExtensionVersion() : injectionConfig.getMavenExtensionVersion(); - if (Files.exists(metadataFile)) { - String[] metadata = Files.readString(metadataFile).split(","); - String extensionVersion = metadata[0]; - String extensionDigest = metadata[1]; + var cachedDigest = ArtifactMetadata.readFromFile(metadataFile) + .filter(m -> m.isForVersion(version)) + .map(ArtifactMetadata::digest) + .orElse(null); - if (!extensionVersion.equals(version)) { - return downloadExtension(injectionConfig, parent, extension, metadataFile, version); - } - return extensionDigest; - } - return downloadExtension(injectionConfig, parent, extension, metadataFile, version); + return cachedDigest != null + ? cachedDigest.digest() + : downloadExtension(injectionConfig, parent, extension, metadataFile, version); } private String downloadExtension( @@ -102,32 +96,33 @@ private String downloadExtension( Path jarFile = parent.resolve(extension.getEmbeddedJarName()); String downloadUrl = extension.createDownloadUrl(version, injectionConfig.getMavenExtensionRepositoryUrl()); - MavenExtension.RepositoryCredentials repositoryCredentials - = getRepositoryCredentials(injectionConfig.getMavenExtensionRepositoryCredentialId()); + RepositoryCredentials repositoryCredentials = + getRepositoryCredentials(injectionConfig.getMavenExtensionRepositoryCredentialId()); try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(jarFile))) { extensionClient.downloadExtension(downloadUrl, repositoryCredentials, outputStream); } String digest = Util.getDigestOf(jarFile.toFile()); - - Files.writeString(metadataFile, version + "," + digest); + var metadata = new ArtifactMetadata(version, digest); + metadata.writeToFile(metadataFile); return digest; } - private static MavenExtension.RepositoryCredentials getRepositoryCredentials(String repositoryCredentialId) { + @Nullable + private static RepositoryCredentials getRepositoryCredentials(@Nullable String repositoryCredentialId) { if (repositoryCredentialId == null) { return null; } - List allCredentials - = CredentialsProvider.lookupCredentialsInItem(StandardUsernamePasswordCredentials.class, null, null); + List allCredentials = + CredentialsProvider.lookupCredentialsInItem(StandardUsernamePasswordCredentials.class, null, null); return allCredentials.stream() .filter(it -> it.getId().equals(repositoryCredentialId)) .findFirst() - .map(it -> new MavenExtension.RepositoryCredentials(it.getUsername(), it.getPassword().getPlainText())) + .map(it -> new RepositoryCredentials(it.getUsername(), it.getPassword().getPlainText())) .orElse(null); } } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/RepositoryCredentials.java b/plugin/src/main/java/hudson/plugins/gradle/injection/RepositoryCredentials.java new file mode 100644 index 00000000..d4836e1e --- /dev/null +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/RepositoryCredentials.java @@ -0,0 +1,4 @@ +package hudson.plugins.gradle.injection; + +public record RepositoryCredentials(String username, String password) { +} diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/extension/ExtensionClient.java b/plugin/src/main/java/hudson/plugins/gradle/injection/extension/ExtensionClient.java index 83468595..86802380 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/extension/ExtensionClient.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/extension/ExtensionClient.java @@ -1,6 +1,6 @@ package hudson.plugins.gradle.injection.extension; -import hudson.plugins.gradle.injection.MavenExtension; +import hudson.plugins.gradle.injection.RepositoryCredentials; import okhttp3.Credentials; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -25,7 +25,7 @@ public ExtensionClient() { public void downloadExtension( String downloadUrl, - @Nullable MavenExtension.RepositoryCredentials repositoryCredentials, + @Nullable RepositoryCredentials repositoryCredentials, OutputStream outputStream ) throws IOException { Request.Builder requestBuilder = new Request.Builder().url(downloadUrl); From baf3cba326098c8424e7b49643cb01718c4aaf62 Mon Sep 17 00:00:00 2001 From: Alexey Venderov Date: Thu, 13 Nov 2025 21:18:37 +0100 Subject: [PATCH 06/15] Fix tests --- .../injection/MavenExtensionDownloadHandlerTest.groovy | 8 ++++---- .../MavenExtensionsHandlerIntegrationTest.groovy | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionDownloadHandlerTest.groovy b/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionDownloadHandlerTest.groovy index 2912fd69..cc8b159e 100644 --- a/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionDownloadHandlerTest.groovy +++ b/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionDownloadHandlerTest.groovy @@ -36,16 +36,16 @@ class MavenExtensionDownloadHandlerTest extends Specification { originalDevelocityDigest != null && originalCcudDigest != null when: - def originalDevelocityLastModified = new File(controllerFolder, MavenExtensionDownloadHandler.DOWNLOAD_CACHE_DIR + "/" + MavenExtension.DEVELOCITY.getEmbeddedJarName()).lastModified() - def originalCcudLastModified = new File(controllerFolder, MavenExtensionDownloadHandler.DOWNLOAD_CACHE_DIR + "/" + MavenExtension.CCUD.getEmbeddedJarName()).lastModified() + def originalDevelocityLastModified = new File(controllerFolder, InjectionUtil.DOWNLOAD_CACHE_DIR + "/" + MavenExtension.DEVELOCITY.getEmbeddedJarName()).lastModified() + def originalCcudLastModified = new File(controllerFolder, InjectionUtil.DOWNLOAD_CACHE_DIR + "/" + MavenExtension.CCUD.getEmbeddedJarName()).lastModified() def sameExtensions = mavenExtensionDownloadHandler.ensureExtensionsDownloaded({ controllerFolder }, originalConfig) then: sameExtensions.size() == 2 - def sameDevelocityLastModified = new File(controllerFolder, MavenExtensionDownloadHandler.DOWNLOAD_CACHE_DIR + "/" + MavenExtension.DEVELOCITY.getEmbeddedJarName()).lastModified() - def sameCcudLastModified = new File(controllerFolder, MavenExtensionDownloadHandler.DOWNLOAD_CACHE_DIR + "/" + MavenExtension.CCUD.getEmbeddedJarName()).lastModified() + def sameDevelocityLastModified = new File(controllerFolder, InjectionUtil.DOWNLOAD_CACHE_DIR + "/" + MavenExtension.DEVELOCITY.getEmbeddedJarName()).lastModified() + def sameCcudLastModified = new File(controllerFolder, InjectionUtil.DOWNLOAD_CACHE_DIR + "/" + MavenExtension.CCUD.getEmbeddedJarName()).lastModified() def sameDevelocityDigest = sameExtensions.get(MavenExtension.DEVELOCITY) def sameCcudDigest = sameExtensions.get(MavenExtension.CCUD) diff --git a/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionsHandlerIntegrationTest.groovy b/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionsHandlerIntegrationTest.groovy index a4476d69..486280b0 100644 --- a/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionsHandlerIntegrationTest.groovy +++ b/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionsHandlerIntegrationTest.groovy @@ -46,7 +46,7 @@ class MavenExtensionsHandlerIntegrationTest extends Specification { def agentFolder = tempFolder.newFolder() def controllerRoot = new FilePath(controllerFolder) def agentRoot = new FilePath(agentFolder) - def cacheDirectory = controllerRoot.child(MavenExtensionDownloadHandler.DOWNLOAD_CACHE_DIR) + def cacheDirectory = controllerRoot.child(InjectionUtil.DOWNLOAD_CACHE_DIR) cacheDirectory.child(MavenExtension.DEVELOCITY.getEmbeddedJarName()).write() .withCloseable { @@ -77,7 +77,7 @@ class MavenExtensionsHandlerIntegrationTest extends Specification { def agentFolder = tempFolder.newFolder() def controllerRoot = new FilePath(controllerFolder) def agentRoot = new FilePath(agentFolder) - def cacheDirectory = controllerRoot.child(MavenExtensionDownloadHandler.DOWNLOAD_CACHE_DIR) + def cacheDirectory = controllerRoot.child(InjectionUtil.DOWNLOAD_CACHE_DIR) cacheDirectory.child(MavenExtension.DEVELOCITY.getEmbeddedJarName()).write() .withCloseable { From 18d4a70e1b16e1bd849868eb987cf256044437e2 Mon Sep 17 00:00:00 2001 From: Alexey Venderov Date: Thu, 13 Nov 2025 21:50:46 +0100 Subject: [PATCH 07/15] Make agent downloading client more generic --- .../MavenExtensionDownloadHandler.java | 21 ++++++++--------- .../AgentDownloadClient.java} | 23 +++++-------------- .../download/RequestAuthenticator.java | 16 +++++++++++++ ...venExtensionsHandlerIntegrationTest.groovy | 13 ++++++----- 4 files changed, 39 insertions(+), 34 deletions(-) rename plugin/src/main/java/hudson/plugins/gradle/injection/{extension/ExtensionClient.java => download/AgentDownloadClient.java} (55%) create mode 100644 plugin/src/main/java/hudson/plugins/gradle/injection/download/RequestAuthenticator.java diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java index 123bb799..033cafd9 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java @@ -3,7 +3,8 @@ import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import hudson.Util; -import hudson.plugins.gradle.injection.extension.ExtensionClient; +import hudson.plugins.gradle.injection.download.AgentDownloadClient; +import hudson.plugins.gradle.injection.download.RequestAuthenticator; import javax.annotation.Nullable; import java.io.BufferedOutputStream; @@ -24,7 +25,7 @@ public class MavenExtensionDownloadHandler implements MavenInjectionAware { - private final ExtensionClient extensionClient = new ExtensionClient(); + private final AgentDownloadClient downloadClient = new AgentDownloadClient(); public Map ensureExtensionsDownloaded(Supplier root, InjectionConfig injectionConfig) throws IOException { if (isInjectionDisabledGlobally(injectionConfig)) { @@ -97,11 +98,10 @@ private String downloadExtension( Path jarFile = parent.resolve(extension.getEmbeddedJarName()); URI downloadUrl = extension.createDownloadUrl(version, injectionConfig.getMavenExtensionRepositoryUrl()); - RepositoryCredentials repositoryCredentials = - getRepositoryCredentials(injectionConfig.getMavenExtensionRepositoryCredentialId()); + RequestAuthenticator authenticator = createRequestAuthenticator(injectionConfig.getMavenExtensionRepositoryCredentialId()); try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(jarFile))) { - extensionClient.downloadExtension(downloadUrl, repositoryCredentials, outputStream); + downloadClient.download(downloadUrl, authenticator, outputStream); } String digest = Util.getDigestOf(jarFile.toFile()); @@ -111,19 +111,18 @@ private String downloadExtension( return digest; } - @Nullable - private static RepositoryCredentials getRepositoryCredentials(@Nullable String repositoryCredentialId) { + private static RequestAuthenticator createRequestAuthenticator(@Nullable String repositoryCredentialId) { if (repositoryCredentialId == null) { - return null; + return RequestAuthenticator.NONE; } List allCredentials = CredentialsProvider.lookupCredentialsInItem(StandardUsernamePasswordCredentials.class, null, null); return allCredentials.stream() - .filter(it -> it.getId().equals(repositoryCredentialId)) + .filter(c -> c.getId().equals(repositoryCredentialId)) .findFirst() - .map(it -> new RepositoryCredentials(it.getUsername(), it.getPassword().getPlainText())) - .orElse(null); + .map(c -> RequestAuthenticator.basic(c.getUsername(), c.getPassword().getPlainText())) + .orElse(RequestAuthenticator.NONE); } } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/extension/ExtensionClient.java b/plugin/src/main/java/hudson/plugins/gradle/injection/download/AgentDownloadClient.java similarity index 55% rename from plugin/src/main/java/hudson/plugins/gradle/injection/extension/ExtensionClient.java rename to plugin/src/main/java/hudson/plugins/gradle/injection/download/AgentDownloadClient.java index 107a975f..eadc67bf 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/extension/ExtensionClient.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/download/AgentDownloadClient.java @@ -1,44 +1,34 @@ -package hudson.plugins.gradle.injection.extension; +package hudson.plugins.gradle.injection.download; -import hudson.plugins.gradle.injection.RepositoryCredentials; -import okhttp3.Credentials; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; -import javax.annotation.Nullable; import java.io.BufferedInputStream; import java.io.IOException; import java.io.OutputStream; import java.net.URI; import java.util.concurrent.TimeUnit; -public final class ExtensionClient { +public final class AgentDownloadClient { private final OkHttpClient httpClient; - public ExtensionClient() { + public AgentDownloadClient() { this.httpClient = new OkHttpClient().newBuilder() .readTimeout(60, TimeUnit.SECONDS) .build(); } - public void downloadExtension( - URI downloadUrl, - @Nullable RepositoryCredentials repositoryCredentials, - OutputStream outputStream - ) throws IOException { + public void download(URI downloadUrl, RequestAuthenticator authenticator, OutputStream outputStream) throws IOException { Request.Builder requestBuilder = new Request.Builder().url(downloadUrl.toURL()); - if (repositoryCredentials != null) { - String basicCredentials = Credentials.basic(repositoryCredentials.username(), repositoryCredentials.password()); - requestBuilder.addHeader("Authorization", basicCredentials); - } + authenticator.authenticate(requestBuilder); try (Response response = httpClient.newCall(requestBuilder.build()).execute()) { ResponseBody responseBody = response.body(); if (!response.isSuccessful() || responseBody == null) { - throw new IOException("Could not download the extension from " + downloadUrl); + throw new IOException("Could not download the agent from " + downloadUrl); } try (BufferedInputStream bufferedInputStream = new BufferedInputStream(responseBody.byteStream())) { @@ -46,5 +36,4 @@ public void downloadExtension( } } } - } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/download/RequestAuthenticator.java b/plugin/src/main/java/hudson/plugins/gradle/injection/download/RequestAuthenticator.java new file mode 100644 index 00000000..912cfc95 --- /dev/null +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/download/RequestAuthenticator.java @@ -0,0 +1,16 @@ +package hudson.plugins.gradle.injection.download; + +import okhttp3.Credentials; +import okhttp3.Request; + +public interface RequestAuthenticator { + + RequestAuthenticator NONE = request -> { + }; + + void authenticate(Request.Builder request); + + static RequestAuthenticator basic(String username, String password) { + return request -> request.addHeader("Authorization", Credentials.basic(username, password)); + } +} diff --git a/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionsHandlerIntegrationTest.groovy b/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionsHandlerIntegrationTest.groovy index 486280b0..b4a286a1 100644 --- a/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionsHandlerIntegrationTest.groovy +++ b/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionsHandlerIntegrationTest.groovy @@ -1,7 +1,8 @@ package hudson.plugins.gradle.injection import hudson.FilePath -import hudson.plugins.gradle.injection.extension.ExtensionClient +import hudson.plugins.gradle.injection.download.AgentDownloadClient +import hudson.plugins.gradle.injection.download.RequestAuthenticator import org.junit.Rule import org.junit.rules.TemporaryFolder import spock.lang.Specification @@ -15,7 +16,7 @@ class MavenExtensionsHandlerIntegrationTest extends Specification { @Subject MavenExtensionsHandler mavenExtensionsHandler = new MavenExtensionsHandler() - ExtensionClient extensionClient = new ExtensionClient() + AgentDownloadClient downloadClient = new AgentDownloadClient() def "only copies configuration extension if it doesn't exist"() { given: @@ -50,12 +51,12 @@ class MavenExtensionsHandlerIntegrationTest extends Specification { cacheDirectory.child(MavenExtension.DEVELOCITY.getEmbeddedJarName()).write() .withCloseable { - extensionClient.downloadExtension(MavenExtension.DEVELOCITY.createDownloadUrl("2.1", null), null, it) + downloadClient.download(MavenExtension.DEVELOCITY.createDownloadUrl("2.1", null), RequestAuthenticator.NONE, it) } cacheDirectory.child(MavenExtension.CCUD.getEmbeddedJarName()).write() .withCloseable { - extensionClient.downloadExtension(MavenExtension.CCUD.createDownloadUrl("2.0.1", null), null, it) + downloadClient.download(MavenExtension.CCUD.createDownloadUrl("2.0.1", null), RequestAuthenticator.NONE, it) } when: @@ -81,12 +82,12 @@ class MavenExtensionsHandlerIntegrationTest extends Specification { cacheDirectory.child(MavenExtension.DEVELOCITY.getEmbeddedJarName()).write() .withCloseable { - extensionClient.downloadExtension(MavenExtension.DEVELOCITY.createDownloadUrl("2.1", null), null, it) + downloadClient.download(MavenExtension.DEVELOCITY.createDownloadUrl("2.1", null), RequestAuthenticator.NONE, it) } cacheDirectory.child(MavenExtension.CCUD.getEmbeddedJarName()).write() .withCloseable { - extensionClient.downloadExtension(MavenExtension.CCUD.createDownloadUrl("2.0.1", null), null, it) + downloadClient.download(MavenExtension.CCUD.createDownloadUrl("2.0.1", null), RequestAuthenticator.NONE, it) } when: From cf8f68c4b4cc0cd20d40bdf7b83327136193e2f8 Mon Sep 17 00:00:00 2001 From: Alexey Venderov Date: Thu, 13 Nov 2025 22:37:51 +0100 Subject: [PATCH 08/15] Download npm agent on configuration change --- .../injection/GradleBuildScanInjection.java | 3 +- .../InjectionConfigChangeListener.java | 21 ++++++- .../injection/MavenBuildScanInjection.java | 3 +- .../MavenExtensionDownloadHandler.java | 2 +- .../injection/RepositoryCredentials.java | 4 -- .../npm/NpmAgentDownloadHandler.java | 59 +++++++++++++++++++ .../injection/npm/NpmBuildScanInjection.java | 20 +++++++ .../InjectionConfigChangeListenerTest.groovy | 14 ++++- 8 files changed, 115 insertions(+), 11 deletions(-) delete mode 100644 plugin/src/main/java/hudson/plugins/gradle/injection/RepositoryCredentials.java create mode 100644 plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmAgentDownloadHandler.java create mode 100644 plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmBuildScanInjection.java diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java b/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java index 5ec3c720..fea118ce 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java @@ -12,6 +12,7 @@ import hudson.model.Node; import hudson.remoting.VirtualChannel; +import javax.annotation.Nullable; import java.io.IOException; import java.util.List; import java.util.Objects; @@ -37,7 +38,7 @@ public class GradleBuildScanInjection implements GradleInjectionAware { private final Supplier initScriptDigest = Suppliers.memoize(() -> unsafeResourceDigest(RESOURCE_INIT_SCRIPT_GRADLE)); - public void inject(Node node, EnvVars envGlobal, EnvVars envComputer) { + public void inject(@Nullable Node node, EnvVars envGlobal, EnvVars envComputer) { if (node == null) { return; } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfigChangeListener.java b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfigChangeListener.java index 393e09d9..fd92842c 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfigChangeListener.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfigChangeListener.java @@ -8,8 +8,11 @@ import hudson.model.Node; import hudson.model.Saveable; import hudson.model.listeners.SaveableListener; +import hudson.plugins.gradle.injection.npm.NpmAgentDownloadHandler; +import hudson.plugins.gradle.injection.npm.NpmBuildScanInjection; import jenkins.model.Jenkins; +import java.io.File; import java.util.Arrays; import java.util.Collection; import java.util.Map; @@ -28,6 +31,8 @@ public class InjectionConfigChangeListener extends SaveableListener { private final GradleBuildScanInjection gradleBuildScanInjection; private final MavenBuildScanInjection mavenBuildScanInjection; private final MavenExtensionDownloadHandler mavenExtensionDownloadHandler; + private final NpmBuildScanInjection npmBuildScanInjection; + private final NpmAgentDownloadHandler npmAgentDownloadHandler; private final Supplier globalEnvVarsSupplier; private final Supplier> computersSupplier; @@ -37,6 +42,8 @@ public InjectionConfigChangeListener() { new GradleBuildScanInjection(), new MavenBuildScanInjection(), new MavenExtensionDownloadHandler(), + new NpmBuildScanInjection(), + new NpmAgentDownloadHandler(), new JenkinsGlobalEnvVars(), new JenkinsComputers() ); @@ -47,12 +54,16 @@ public InjectionConfigChangeListener() { GradleBuildScanInjection gradleBuildScanInjection, MavenBuildScanInjection mavenBuildScanInjection, MavenExtensionDownloadHandler mavenExtensionDownloadHandler, + NpmBuildScanInjection npmBuildScanInjection, + NpmAgentDownloadHandler npmAgentDownloadHandler, Supplier globalEnvVarsSupplier, Supplier> computersSupplier ) { this.gradleBuildScanInjection = gradleBuildScanInjection; this.mavenBuildScanInjection = mavenBuildScanInjection; this.mavenExtensionDownloadHandler = mavenExtensionDownloadHandler; + this.npmBuildScanInjection = npmBuildScanInjection; + this.npmAgentDownloadHandler = npmAgentDownloadHandler; this.globalEnvVarsSupplier = globalEnvVarsSupplier; this.computersSupplier = computersSupplier; } @@ -68,9 +79,12 @@ public void onChange(Saveable saveable, XmlFile file) { } try { - Map extensionsDigest = mavenExtensionDownloadHandler.ensureExtensionsDownloaded( - () -> Jenkins.get().getRootDir(), injectionConfig - ); + Supplier root = () -> Jenkins.get().getRootDir(); + Map extensionsDigest = mavenExtensionDownloadHandler.ensureExtensionsDownloaded(root, injectionConfig); + ArtifactDigest npmAgentDigest = + npmBuildScanInjection.isInjectionDisabledGlobally(injectionConfig) + ? null + : npmAgentDownloadHandler.downloadNpmAgent(root, injectionConfig); for (Computer computer : computersSupplier.get()) { if (computer.isOnline()) { @@ -79,6 +93,7 @@ public void onChange(Saveable saveable, XmlFile file) { gradleBuildScanInjection.inject(node, globalEnvVars, computerEnvVars); mavenBuildScanInjection.inject(node, extensionsDigest); + npmBuildScanInjection.inject(node, npmAgentDigest, computerEnvVars); } } } catch (Exception e) { diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java index 76d680ad..8d6aeab1 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java @@ -4,6 +4,7 @@ import hudson.model.Node; import jenkins.model.Jenkins; +import javax.annotation.Nullable; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -33,7 +34,7 @@ public class MavenBuildScanInjection implements MavenInjectionAware { private final MavenExtensionsHandler extensionsHandler = new MavenExtensionsHandler(); - public void inject(Node node, Map extensionsDigest) { + public void inject(@Nullable Node node, Map extensionsDigest) { if (node == null) { return; } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java index 033cafd9..b0c8f58e 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java @@ -105,7 +105,7 @@ private String downloadExtension( } String digest = Util.getDigestOf(jarFile.toFile()); - var metadata = new ArtifactMetadata(version, digest); + ArtifactMetadata metadata = new ArtifactMetadata(version, digest); metadata.writeToFile(metadataFile); return digest; diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/RepositoryCredentials.java b/plugin/src/main/java/hudson/plugins/gradle/injection/RepositoryCredentials.java deleted file mode 100644 index d4836e1e..00000000 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/RepositoryCredentials.java +++ /dev/null @@ -1,4 +0,0 @@ -package hudson.plugins.gradle.injection; - -public record RepositoryCredentials(String username, String password) { -} diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmAgentDownloadHandler.java b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmAgentDownloadHandler.java new file mode 100644 index 00000000..5f786bca --- /dev/null +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmAgentDownloadHandler.java @@ -0,0 +1,59 @@ +package hudson.plugins.gradle.injection.npm; + +import hudson.Util; +import hudson.plugins.gradle.injection.ArtifactDigest; +import hudson.plugins.gradle.injection.ArtifactMetadata; +import hudson.plugins.gradle.injection.InjectionConfig; +import hudson.plugins.gradle.injection.download.AgentDownloadClient; +import hudson.plugins.gradle.injection.download.RequestAuthenticator; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Supplier; + +import static hudson.plugins.gradle.injection.InjectionUtil.DOWNLOAD_CACHE_DIR; + +public class NpmAgentDownloadHandler { + + private static final String FILENAME = "develocity-npm-agent"; + private static final String METADATA_FILENAME = FILENAME + ".meta"; + private static final String AGENT_FILENAME = FILENAME + ".tgz"; + + private final AgentDownloadClient downloadClient = new AgentDownloadClient(); + + public ArtifactDigest downloadNpmAgent(Supplier root, InjectionConfig injectionConfig) throws IOException { + String npmAgentVersion = injectionConfig.getNpmAgentVersion(); + + Path cacheDir = root.get().toPath().resolve(DOWNLOAD_CACHE_DIR); + Path metadataFile = cacheDir.resolve(METADATA_FILENAME); + + ArtifactDigest cachedDigest = ArtifactMetadata.readFromFile(metadataFile) + .filter(m -> m.isForVersion(npmAgentVersion)) + .map(ArtifactMetadata::digest) + .orElse(null); + + if (cachedDigest != null) { + return cachedDigest; + } + + // Download the NPM agent + Files.createDirectories(cacheDir); + Path agentFile = cacheDir.resolve(AGENT_FILENAME); + + URI downloadUrl = URI.create("https://registry.npmjs.org/@gradle-tech/develocity-agent/-/develocity-agent-%s.tgz".formatted(npmAgentVersion)); + try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(agentFile))) { + downloadClient.download(downloadUrl, RequestAuthenticator.NONE, outputStream); + } + + ArtifactDigest digest = new ArtifactDigest(Util.getDigestOf(agentFile.toFile())); + ArtifactMetadata metadata = new ArtifactMetadata(npmAgentVersion, digest); + metadata.writeToFile(metadataFile); + + return digest; + } +} diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmBuildScanInjection.java b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmBuildScanInjection.java new file mode 100644 index 00000000..da6696f4 --- /dev/null +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmBuildScanInjection.java @@ -0,0 +1,20 @@ +package hudson.plugins.gradle.injection.npm; + +import hudson.EnvVars; +import hudson.model.Node; +import hudson.plugins.gradle.injection.ArtifactDigest; + +import javax.annotation.Nullable; +import java.util.logging.Logger; + +public class NpmBuildScanInjection implements NpmInjectionAware { + + private static final Logger LOGGER = Logger.getLogger(NpmBuildScanInjection.class.getName()); + + public void inject(@Nullable Node node, @Nullable ArtifactDigest npmAgentDigest, EnvVars envComputer) { + if (node == null) { + return; + } + // TODO: Implement npm build scan injection logic + } +} diff --git a/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigChangeListenerTest.groovy b/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigChangeListenerTest.groovy index 1d1e6151..9a729fec 100644 --- a/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigChangeListenerTest.groovy +++ b/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigChangeListenerTest.groovy @@ -4,6 +4,8 @@ import hudson.EnvVars import hudson.XmlFile import hudson.model.Computer import hudson.model.Node +import hudson.plugins.gradle.injection.npm.NpmAgentDownloadHandler +import hudson.plugins.gradle.injection.npm.NpmBuildScanInjection import org.junit.Rule import org.junit.rules.TemporaryFolder import spock.lang.Specification @@ -21,6 +23,8 @@ class InjectionConfigChangeListenerTest extends Specification { def gradleBuildScanInjection = Mock(GradleBuildScanInjection) def mavenBuildScanInjection = Mock(MavenBuildScanInjection) def mavenExtensionDownloadHandler = Mock(MavenExtensionDownloadHandler) + def npmBuildScanInjection = Mock(NpmBuildScanInjection) + def npmAgentDownloadHandler = Mock(NpmAgentDownloadHandler) def injectionConfig = Mock(InjectionConfig) @Rule @@ -28,7 +32,15 @@ class InjectionConfigChangeListenerTest extends Specification { @Subject def injectionConfigChangeListener = - new InjectionConfigChangeListener(gradleBuildScanInjection, mavenBuildScanInjection, mavenExtensionDownloadHandler, { globalEnvVars }, { [computer] }) + new InjectionConfigChangeListener( + gradleBuildScanInjection, + mavenBuildScanInjection, + mavenExtensionDownloadHandler, + npmBuildScanInjection, + npmAgentDownloadHandler, + { globalEnvVars }, + { [computer] } + ) @Unroll def "performs injection when configuration changes (isGlobalAutoInjectionCheckEnabled=#isGlobalAutoInjectionCheckEnabled, isGlobalInjectionEnabled=#isGlobalInjectionEnabled, isComputerOffline=#isComputerOffline)"() { From 21840fb5de738acb5024a9ccede39649f6348399 Mon Sep 17 00:00:00 2001 From: Alexey Venderov Date: Sat, 15 Nov 2025 01:03:43 +0100 Subject: [PATCH 09/15] Implement npm agent auto-injection --- .../gradle/injection/ArtifactDigest.java | 7 +- .../injection/DevelocityComputerListener.java | 22 ++- .../plugins/gradle/injection/EnvUtil.java | 21 +-- .../plugins/gradle/injection/EnvVar.java | 6 + .../gradle/injection/GitScmListener.java | 103 ++++++++++---- .../injection/GradleBuildScanInjection.java | 2 +- .../gradle/injection/InitScriptVariables.java | 6 +- .../gradle/injection/InjectionAware.java | 9 ++ .../InjectionConfigChangeListener.java | 8 +- .../gradle/injection/InjectionUtil.java | 7 + .../injection/MavenBuildScanInjection.java | 2 +- .../MavenExtensionDownloadHandler.java | 14 +- .../download/AgentDownloadClient.java | 2 +- .../npm/NpmAgentDownloadHandler.java | 28 +++- .../injection/npm/NpmBuildScanInjection.java | 132 +++++++++++++++++- .../injection/npm/UnarchiveNpmAgent.java | 76 ++++++++++ .../injection/InjectionConfig/config.jelly | 2 +- .../DevelocityComputerListenerTest.groovy | 17 ++- 18 files changed, 393 insertions(+), 71 deletions(-) create mode 100644 plugin/src/main/java/hudson/plugins/gradle/injection/EnvVar.java create mode 100644 plugin/src/main/java/hudson/plugins/gradle/injection/npm/UnarchiveNpmAgent.java diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/ArtifactDigest.java b/plugin/src/main/java/hudson/plugins/gradle/injection/ArtifactDigest.java index ad05cba7..8661dfd6 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/ArtifactDigest.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/ArtifactDigest.java @@ -1,3 +1,8 @@ package hudson.plugins.gradle.injection; -public record ArtifactDigest(String digest) {} +public record ArtifactDigest(String digest) { + + public boolean matches(String otherDigest) { + return digest.equals(otherDigest); + } +} diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityComputerListener.java b/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityComputerListener.java index 8949b5cb..ced9f9e2 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityComputerListener.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityComputerListener.java @@ -6,9 +6,12 @@ import hudson.model.Computer; import hudson.model.Node; import hudson.model.TaskListener; +import hudson.plugins.gradle.injection.npm.NpmAgentDownloadHandler; +import hudson.plugins.gradle.injection.npm.NpmBuildScanInjection; import hudson.slaves.ComputerListener; import jenkins.model.Jenkins; +import java.io.File; import java.util.Map; import java.util.function.Supplier; import java.util.logging.Level; @@ -27,6 +30,8 @@ public class DevelocityComputerListener extends ComputerListener { private final GradleBuildScanInjection gradleBuildScanInjection; private final MavenBuildScanInjection mavenBuildScanInjection; private final MavenExtensionDownloadHandler mavenExtensionDownloadHandler; + private final NpmBuildScanInjection npmBuildScanInjection; + private final NpmAgentDownloadHandler npmAgentDownloadHandler; private final Supplier injectionConfigSupplier; @SuppressWarnings("unused") @@ -35,6 +40,8 @@ public DevelocityComputerListener() { new GradleBuildScanInjection(), new MavenBuildScanInjection(), new MavenExtensionDownloadHandler(), + new NpmBuildScanInjection(), + new NpmAgentDownloadHandler(), new JenkinsInjectionConfig() ); } @@ -44,11 +51,15 @@ public DevelocityComputerListener() { GradleBuildScanInjection gradleBuildScanInjection, MavenBuildScanInjection mavenBuildScanInjection, MavenExtensionDownloadHandler mavenExtensionDownloadHandler, + NpmBuildScanInjection npmBuildScanInjection, + NpmAgentDownloadHandler npmAgentDownloadHandler, Supplier injectionConfigSupplier ) { this.gradleBuildScanInjection = gradleBuildScanInjection; this.mavenBuildScanInjection = mavenBuildScanInjection; this.mavenExtensionDownloadHandler = mavenExtensionDownloadHandler; + this.npmBuildScanInjection = npmBuildScanInjection; + this.npmAgentDownloadHandler = npmAgentDownloadHandler; this.injectionConfigSupplier = injectionConfigSupplier; } @@ -61,15 +72,20 @@ public void onOnline(Computer computer, TaskListener listener) { return; } - Map extensionsDigest = mavenExtensionDownloadHandler.getExtensionDigests( - () -> Jenkins.get().getRootDir(), injectionConfig - ); + Supplier root = () -> Jenkins.get().getRootDir(); + // When the agent becomes online, all artifacts must be already downloaded on the controller. + Map extensionsDigest = mavenExtensionDownloadHandler.getExtensionDigests(root, injectionConfig); + ArtifactDigest npmAgentDigest = + npmBuildScanInjection + .ifInjectionEnabledGlobally(injectionConfig, () -> npmAgentDownloadHandler.getDownloadedNpmAgentDigest(root)) + .orElse(null); Node node = computer.getNode(); EnvVars computerEnvVars = computer.getEnvironment(); gradleBuildScanInjection.inject(node, globalEnvVars, computerEnvVars); mavenBuildScanInjection.inject(node, extensionsDigest); + npmBuildScanInjection.inject(node, npmAgentDigest, computerEnvVars); } catch (Throwable t) { /* * We should catch everything because this is not handled by {@link hudson.slaves.SlaveComputer#setChannel(Channel, OutputStream, Channel.Listener)} diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/EnvUtil.java b/plugin/src/main/java/hudson/plugins/gradle/injection/EnvUtil.java index 7074ec60..e0de13b4 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/EnvUtil.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/EnvUtil.java @@ -25,7 +25,7 @@ public static EnvVars globalEnvironment() { } EnvironmentVariablesNodeProperty nodeProperty = - jenkins.getGlobalNodeProperties().get(EnvironmentVariablesNodeProperty.class); + jenkins.getGlobalNodeProperties().get(EnvironmentVariablesNodeProperty.class); return nodeProperty != null ? nodeProperty.getEnvVars() : null; } @@ -33,7 +33,7 @@ public static EnvVars globalEnvironment() { @CheckForNull public static String getEnv(Node node, String key) { List all = - node.getNodeProperties().getAll(EnvironmentVariablesNodeProperty.class); + node.getNodeProperties().getAll(EnvironmentVariablesNodeProperty.class); if (all.isEmpty()) { return null; @@ -51,8 +51,9 @@ public static String getEnv(EnvVars env, String key) { public static void removeEnvVars(Node node, Collection keys) { keys.forEach(key -> removeEnvVar(node, key)); } - public static void removeEnvVars(Node node, InitScriptVariables[] keys) { - for (InitScriptVariables key : keys) { + + public static void removeEnvVars(Node node, EnvVar[] keys) { + for (EnvVar key : keys) { removeEnvVar(node, key); } } @@ -60,22 +61,24 @@ public static void removeEnvVars(Node node, InitScriptVariables[] keys) { public static void removeEnvVar(Node node, String key) { setEnvVar(node, key, null); } - public static void removeEnvVar(Node node, InitScriptVariables key) { + + public static void removeEnvVar(Node node, EnvVar key) { setEnvVar(node, key, null); } - public static void setEnvVar(Node node, InitScriptVariables key, @Nullable String value) { + public static void setEnvVar(Node node, EnvVar key, @Nullable String value) { setEnvVar(node, key.getEnvVar(), value); } + public static void setEnvVar(Node node, String key, @Nullable String value) { List all = - node.getNodeProperties().getAll(EnvironmentVariablesNodeProperty.class); + node.getNodeProperties().getAll(EnvironmentVariablesNodeProperty.class); if (all.isEmpty()) { if (value != null) { node.getNodeProperties().add( - new EnvironmentVariablesNodeProperty( - new EnvironmentVariablesNodeProperty.Entry(key, value))); + new EnvironmentVariablesNodeProperty( + new EnvironmentVariablesNodeProperty.Entry(key, value))); } // noop if null return; } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/EnvVar.java b/plugin/src/main/java/hudson/plugins/gradle/injection/EnvVar.java new file mode 100644 index 00000000..6b0c87d4 --- /dev/null +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/EnvVar.java @@ -0,0 +1,6 @@ +package hudson.plugins.gradle.injection; + +public interface EnvVar { + + String getEnvVar(); +} diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/GitScmListener.java b/plugin/src/main/java/hudson/plugins/gradle/injection/GitScmListener.java index 467d1a83..1d48fce9 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/GitScmListener.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/GitScmListener.java @@ -4,7 +4,11 @@ import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; -import hudson.model.*; +import hudson.model.Computer; +import hudson.model.EnvironmentContributingAction; +import hudson.model.InvisibleAction; +import hudson.model.Run; +import hudson.model.TaskListener; import hudson.model.listeners.SCMListener; import hudson.plugins.git.GitSCM; import hudson.plugins.git.UserRemoteConfig; @@ -25,6 +29,7 @@ import static hudson.plugins.gradle.injection.MavenInjectionAware.JENKINSGRADLEPLUGIN_MAVEN_PLUGIN_CONFIG_EXT_CLASSPATH; import static hudson.plugins.gradle.injection.MavenInjectionAware.MAVEN_OPTS_HANDLER; import static hudson.plugins.gradle.injection.MavenOptsHandler.MAVEN_OPTS; +import static hudson.plugins.gradle.injection.npm.NpmBuildScanInjection.DEVELOCITY_INTERNAL_DISABLE_AGENT; @SuppressWarnings("unused") @Extension @@ -34,12 +39,12 @@ public class GitScmListener extends SCMListener { @Override public void onCheckout( - Run build, - SCM scm, - FilePath workspace, - TaskListener listener, - @CheckForNull File changelogFile, - @CheckForNull SCMRevisionState pollingBaseline + Run build, + SCM scm, + FilePath workspace, + TaskListener listener, + @CheckForNull File changelogFile, + @CheckForNull SCMRevisionState pollingBaseline ) { try { InjectionConfig config = InjectionConfig.get(); @@ -56,16 +61,20 @@ public void onCheckout( // Check .mvn/extensions.xml for already applied Develocity extension for maven injection only disableMavenAutoInjectionIfAlreadyApplied(build, workspace, config, listener); + + // TODO: Figure out how to detect if npm injection is already configured for the project. + // If the project already has npm Develocity agent configured, + // we should set DEVELOCITY_URL depending on the config.isEnforceUrl() flag. } catch (Exception e) { LOGGER.error("Error occurred when processing onCheckout notification", e); } } private static void disableAutoInjection( - Run build, - FilePath workspace, - InjectionConfig config, - TaskListener listener + Run build, + FilePath workspace, + InjectionConfig config, + TaskListener listener ) throws Exception { Computer computer = workspace.toComputer(); if (computer == null) { @@ -74,11 +83,11 @@ private static void disableAutoInjection( EnvVars envVars = computer.buildEnvironment(listener); - if (shouldDisableGradleInjection(config)) { + if (InjectionStatus.GRADLE.isEnabled(config)) { build.addAction(GradleInjectionDisabledAction.INSTANCE); } - if (shouldDisableMavenInjection(config)) { + if (InjectionStatus.MAVEN.isEnabled(config)) { String currentMavenOpts = envVars.get(MavenOptsHandler.MAVEN_OPTS); if (currentMavenOpts != null) { String mavenOpts = Strings.nullToEmpty(MAVEN_OPTS_HANDLER.removeIfNeeded(currentMavenOpts)); @@ -86,13 +95,17 @@ private static void disableAutoInjection( build.addAction(new MavenInjectionDisabledAction(mavenOpts)); } } + + if (InjectionStatus.NPM.isEnabled(config)) { + build.addAction(NpmInjectionDisabledAction.INSTANCE); + } } private static void disableMavenAutoInjectionIfAlreadyApplied( - Run build, - FilePath workspace, - InjectionConfig config, - TaskListener listener + Run build, + FilePath workspace, + InjectionConfig config, + TaskListener listener ) throws Exception { Computer computer = workspace.toComputer(); if (computer == null) { @@ -105,10 +118,9 @@ private static void disableMavenAutoInjectionIfAlreadyApplied( if (currentMavenOpts != null) { Set knownExtensions = detect(config, workspace); if (!knownExtensions.isEmpty()) { - build.addAction( - new MavenInjectionDisabledAction( - new MavenOptsDevelocityFilter(knownExtensions, isUnix(computer)) - .filter(currentMavenOpts, config.isEnforceUrl()))); + MavenOptsDevelocityFilter mavenOptsFilter = new MavenOptsDevelocityFilter(knownExtensions, isUnix(computer)); + String filteredMavenOpts = mavenOptsFilter.filter(currentMavenOpts, config.isEnforceUrl()); + build.addAction(new MavenInjectionDisabledAction(filteredMavenOpts)); } } } @@ -131,8 +143,10 @@ private static boolean isInjectionEnabledForRepository(InjectionConfig config, S return true; } switch (config.matchesRepositoryFilter(url)) { - case EXCLUDED: return false; - case INCLUDED: return true; + case EXCLUDED: + return false; + case INCLUDED: + return true; } } } @@ -140,12 +154,31 @@ private static boolean isInjectionEnabledForRepository(InjectionConfig config, S return false; } - private static boolean shouldDisableGradleInjection(InjectionConfig config) { - return InjectionUtil.isValid(InjectionConfig.checkRequiredVersion(config.getGradlePluginVersion())); - } + private enum InjectionStatus { + GRADLE { + @Override + String getAgentVersion(InjectionConfig config) { + return config.getGradlePluginVersion(); + } + }, + MAVEN { + @Override + String getAgentVersion(InjectionConfig config) { + return config.getMavenExtensionVersion(); + } + }, + NPM { + @Override + String getAgentVersion(InjectionConfig config) { + return config.getNpmAgentVersion(); + } + }; - private static boolean shouldDisableMavenInjection(InjectionConfig config) { - return InjectionUtil.isValid(InjectionConfig.checkRequiredVersion(config.getMavenExtensionVersion())); + public boolean isEnabled(InjectionConfig config) { + return InjectionUtil.isValid(InjectionConfig.checkRequiredVersion(getAgentVersion(config))); + } + + abstract String getAgentVersion(InjectionConfig config); } /** @@ -162,7 +195,6 @@ private GradleInjectionDisabledAction() { public void buildEnvironment(@Nonnull Run run, @Nonnull EnvVars envVars) { envVars.put(DEVELOCITY_INJECTION_ENABLED.getEnvVar(), "false"); } - } /** @@ -181,7 +213,18 @@ public void buildEnvironment(@Nonnull Run run, @Nonnull EnvVars envVars) { envVars.put(MAVEN_OPTS, mavenOpts); envVars.put(JENKINSGRADLEPLUGIN_MAVEN_PLUGIN_CONFIG_EXT_CLASSPATH, ""); } - } + public static final class NpmInjectionDisabledAction extends InvisibleAction implements EnvironmentContributingAction { + + public static final NpmInjectionDisabledAction INSTANCE = new NpmInjectionDisabledAction(); + + private NpmInjectionDisabledAction() { + } + + @Override + public void buildEnvironment(@Nonnull Run run, @Nonnull EnvVars envVars) { + envVars.put(DEVELOCITY_INTERNAL_DISABLE_AGENT, "true"); + } + } } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java b/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java index fea118ce..9445f0e9 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java @@ -21,6 +21,7 @@ import static hudson.plugins.gradle.injection.CopyUtil.copyResourceToNode; import static hudson.plugins.gradle.injection.CopyUtil.unsafeResourceDigest; +import static hudson.plugins.gradle.injection.InjectionUtil.HOME; public class GradleBuildScanInjection implements GradleInjectionAware { @@ -28,7 +29,6 @@ public class GradleBuildScanInjection implements GradleInjectionAware { private static final String JENKINSGRADLEPLUGIN_BUILD_SCAN_OVERRIDE_GRADLE_HOME = "JENKINSGRADLEPLUGIN_BUILD_SCAN_OVERRIDE_GRADLE_HOME"; private static final String JENKINSGRADLEPLUGIN_BUILD_SCAN_OVERRIDE_HOME = "JENKINSGRADLEPLUGIN_BUILD_SCAN_OVERRIDE_HOME"; - private static final String HOME = "HOME"; @VisibleForTesting static final String RESOURCE_INIT_SCRIPT_GRADLE = "init-script.gradle"; diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/InitScriptVariables.java b/plugin/src/main/java/hudson/plugins/gradle/injection/InitScriptVariables.java index 40eb88bc..8bba649d 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/InitScriptVariables.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/InitScriptVariables.java @@ -1,6 +1,7 @@ package hudson.plugins.gradle.injection; -public enum InitScriptVariables { +public enum InitScriptVariables implements EnvVar { + DEVELOCITY_INJECTION_CUSTOM_VALUE("develocity-injection.custom-value"), DEVELOCITY_INIT_SCRIPT_NAME("develocity-injection.init-script-name"), DEVELOCITY_INJECTION_ENABLED("develocity-injection.enabled"), @@ -24,7 +25,8 @@ public enum InitScriptVariables { this.templateName = templateName; } - String getEnvVar() { + @Override + public String getEnvVar() { return templateName.toUpperCase().replace('.', '_').replace('-', '_'); } } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionAware.java b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionAware.java index d42e2d09..4cce2132 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionAware.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionAware.java @@ -6,7 +6,9 @@ import javax.annotation.Nullable; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; +import java.util.concurrent.Callable; import java.util.stream.Collectors; public interface InjectionAware { @@ -20,6 +22,13 @@ public interface InjectionAware { @Nullable List getAgentInjectionEnabledNodes(InjectionConfig config); + default Optional ifInjectionEnabledGlobally(InjectionConfig config, Callable action) throws Exception { + if (isInjectionDisabledGlobally(config)) { + return Optional.empty(); + } + return Optional.ofNullable(action.call()); + } + default boolean isInjectionDisabledGlobally(InjectionConfig config) { return config.isDisabled() || InjectionUtil.isAnyInvalid( diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfigChangeListener.java b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfigChangeListener.java index fd92842c..514b88e1 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfigChangeListener.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfigChangeListener.java @@ -80,11 +80,13 @@ public void onChange(Saveable saveable, XmlFile file) { try { Supplier root = () -> Jenkins.get().getRootDir(); + // This code runs on the controller, and we need to download artifacts + // before injecting them on agents. Map extensionsDigest = mavenExtensionDownloadHandler.ensureExtensionsDownloaded(root, injectionConfig); ArtifactDigest npmAgentDigest = - npmBuildScanInjection.isInjectionDisabledGlobally(injectionConfig) - ? null - : npmAgentDownloadHandler.downloadNpmAgent(root, injectionConfig); + npmBuildScanInjection + .ifInjectionEnabledGlobally(injectionConfig, () -> npmAgentDownloadHandler.downloadNpmAgent(root, injectionConfig)) + .orElse(null); for (Computer computer : computersSupplier.get()) { if (computer.isOnline()) { diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionUtil.java b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionUtil.java index 795aa260..4224a1ac 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionUtil.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionUtil.java @@ -9,6 +9,8 @@ import jenkins.model.Jenkins; import javax.annotation.Nullable; +import java.io.File; +import java.nio.file.Path; import java.util.Arrays; import java.util.Optional; import java.util.Set; @@ -19,6 +21,7 @@ public final class InjectionUtil { private static final String MAVEN_PLUGIN_SHORT_NAME = "maven-plugin"; + public static final String HOME = "HOME"; public static final String DOWNLOAD_CACHE_DIR = "jenkins-gradle-plugin/cache"; public static final VersionNumber MINIMUM_SUPPORTED_MAVEN_PLUGIN_VERSION = new VersionNumber("3.20"); public static final String JENKINSGRADLEPLUGIN_GLOBAL_AUTO_INJECTION_CHECK = "JENKINSGRADLEPLUGIN_GLOBAL_AUTO_INJECTION_CHECK"; @@ -26,6 +29,10 @@ public final class InjectionUtil { private InjectionUtil() { } + public static Path getDownloadCacheDir(Supplier rootDir) { + return rootDir.get().toPath().resolve(DOWNLOAD_CACHE_DIR); + } + public static boolean globalAutoInjectionCheckEnabled(EnvVars envVars) { return EnvUtil.getEnv(envVars, JENKINSGRADLEPLUGIN_GLOBAL_AUTO_INJECTION_CHECK) != null; } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java index 8d6aeab1..1c77c76d 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java @@ -69,7 +69,7 @@ private void inject(InjectionConfig config, Node node, FilePath nodeRootPath, Ma String server = config.getServer(); - LOGGER.info("Injecting Maven extensions " + nodeRootPath); + LOGGER.log(Level.INFO, "Injecting Maven extensions {0}", nodeRootPath); List extensions = new ArrayList<>(); FilePath controllerRootPath = Jenkins.get().getRootPath(); diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java index b0c8f58e..303446b1 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java @@ -21,8 +21,6 @@ import java.util.Optional; import java.util.function.Supplier; -import static hudson.plugins.gradle.injection.InjectionUtil.DOWNLOAD_CACHE_DIR; - public class MavenExtensionDownloadHandler implements MavenInjectionAware { private final AgentDownloadClient downloadClient = new AgentDownloadClient(); @@ -33,7 +31,7 @@ public Map ensureExtensionsDownloaded(Supplier roo } Map extensionsDigest = new HashMap<>(); - Path cacheDir = root.get().toPath().resolve(DOWNLOAD_CACHE_DIR); + Path cacheDir = InjectionUtil.getDownloadCacheDir(root); MavenExtension develocityMavenExtension = MavenExtension.forVersion(injectionConfig.getMavenExtensionVersion()); @@ -45,13 +43,13 @@ public Map ensureExtensionsDownloaded(Supplier roo return extensionsDigest; } - public Map getExtensionDigests(Supplier rootDir, InjectionConfig injectionConfig) throws IOException { + public Map getExtensionDigests(Supplier root, InjectionConfig injectionConfig) throws IOException { if (isInjectionDisabledGlobally(injectionConfig)) { return Collections.emptyMap(); } Map extensionDigests = new HashMap<>(); - Path cacheDir = rootDir.get().toPath().resolve(DOWNLOAD_CACHE_DIR); + Path cacheDir = InjectionUtil.getDownloadCacheDir(root); MavenExtension develocityMavenExtension = MavenExtension.forVersion(injectionConfig.getMavenExtensionVersion()); @@ -76,7 +74,7 @@ private String getOrDownloadExtensionDigest(InjectionConfig injectionConfig, Pat ? injectionConfig.getCcudExtensionVersion() : injectionConfig.getMavenExtensionVersion(); - var cachedDigest = ArtifactMetadata.readFromFile(metadataFile) + ArtifactDigest cachedDigest = ArtifactMetadata.readFromFile(metadataFile) .filter(m -> m.isForVersion(version)) .map(ArtifactMetadata::digest) .orElse(null); @@ -104,9 +102,9 @@ private String downloadExtension( downloadClient.download(downloadUrl, authenticator, outputStream); } + // TODO: Consider downloading the checksum file from the repository and verifying the download against it String digest = Util.getDigestOf(jarFile.toFile()); - ArtifactMetadata metadata = new ArtifactMetadata(version, digest); - metadata.writeToFile(metadataFile); + new ArtifactMetadata(version, digest).writeToFile(metadataFile); return digest; } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/download/AgentDownloadClient.java b/plugin/src/main/java/hudson/plugins/gradle/injection/download/AgentDownloadClient.java index eadc67bf..3eeae5f6 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/download/AgentDownloadClient.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/download/AgentDownloadClient.java @@ -16,7 +16,7 @@ public final class AgentDownloadClient { private final OkHttpClient httpClient; public AgentDownloadClient() { - this.httpClient = new OkHttpClient().newBuilder() + httpClient = new OkHttpClient().newBuilder() .readTimeout(60, TimeUnit.SECONDS) .build(); } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmAgentDownloadHandler.java b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmAgentDownloadHandler.java index 5f786bca..9ebc18d9 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmAgentDownloadHandler.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmAgentDownloadHandler.java @@ -4,6 +4,7 @@ import hudson.plugins.gradle.injection.ArtifactDigest; import hudson.plugins.gradle.injection.ArtifactMetadata; import hudson.plugins.gradle.injection.InjectionConfig; +import hudson.plugins.gradle.injection.InjectionUtil; import hudson.plugins.gradle.injection.download.AgentDownloadClient; import hudson.plugins.gradle.injection.download.RequestAuthenticator; @@ -16,20 +17,20 @@ import java.nio.file.Path; import java.util.function.Supplier; -import static hudson.plugins.gradle.injection.InjectionUtil.DOWNLOAD_CACHE_DIR; - public class NpmAgentDownloadHandler { private static final String FILENAME = "develocity-npm-agent"; private static final String METADATA_FILENAME = FILENAME + ".meta"; - private static final String AGENT_FILENAME = FILENAME + ".tgz"; + private static final String DEFAULT_REGISTRY_URL = "https://registry.npmjs.org"; + + public static final String AGENT_FILENAME = FILENAME + ".tgz"; private final AgentDownloadClient downloadClient = new AgentDownloadClient(); public ArtifactDigest downloadNpmAgent(Supplier root, InjectionConfig injectionConfig) throws IOException { String npmAgentVersion = injectionConfig.getNpmAgentVersion(); - Path cacheDir = root.get().toPath().resolve(DOWNLOAD_CACHE_DIR); + Path cacheDir = InjectionUtil.getDownloadCacheDir(root); Path metadataFile = cacheDir.resolve(METADATA_FILENAME); ArtifactDigest cachedDigest = ArtifactMetadata.readFromFile(metadataFile) @@ -45,15 +46,28 @@ public ArtifactDigest downloadNpmAgent(Supplier root, InjectionConfig inje Files.createDirectories(cacheDir); Path agentFile = cacheDir.resolve(AGENT_FILENAME); - URI downloadUrl = URI.create("https://registry.npmjs.org/@gradle-tech/develocity-agent/-/develocity-agent-%s.tgz".formatted(npmAgentVersion)); + URI downloadUrl = createDownloadUrl(npmAgentVersion); try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(agentFile))) { downloadClient.download(downloadUrl, RequestAuthenticator.NONE, outputStream); } + // TODO: Consider downloading the checksum file from the repository and verifying the download against it ArtifactDigest digest = new ArtifactDigest(Util.getDigestOf(agentFile.toFile())); - ArtifactMetadata metadata = new ArtifactMetadata(npmAgentVersion, digest); - metadata.writeToFile(metadataFile); + new ArtifactMetadata(npmAgentVersion, digest).writeToFile(metadataFile); return digest; } + + public ArtifactDigest getDownloadedNpmAgentDigest(Supplier root) throws IOException { + Path metadataFile = InjectionUtil.getDownloadCacheDir(root).resolve(METADATA_FILENAME); + return ArtifactMetadata.readFromFile(metadataFile) + .map(ArtifactMetadata::digest) + .orElse(null); + } + + private static URI createDownloadUrl(String npmAgentVersion) { + return URI.create( + "%s/@gradle-tech/develocity-agent/-/develocity-agent-%s.tgz".formatted(DEFAULT_REGISTRY_URL, npmAgentVersion) + ); + } } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmBuildScanInjection.java b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmBuildScanInjection.java index da6696f4..bbd0289e 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmBuildScanInjection.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmBuildScanInjection.java @@ -1,20 +1,150 @@ package hudson.plugins.gradle.injection.npm; import hudson.EnvVars; +import hudson.FilePath; import hudson.model.Node; import hudson.plugins.gradle.injection.ArtifactDigest; +import hudson.plugins.gradle.injection.EnvUtil; +import hudson.plugins.gradle.injection.EnvVar; +import hudson.plugins.gradle.injection.InjectionConfig; +import hudson.plugins.gradle.injection.InjectionUtil; +import hudson.remoting.RemoteInputStream; +import jenkins.model.Jenkins; import javax.annotation.Nullable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.logging.Level; import java.util.logging.Logger; +import static hudson.plugins.gradle.injection.InjectionUtil.HOME; + public class NpmBuildScanInjection implements NpmInjectionAware { private static final Logger LOGGER = Logger.getLogger(NpmBuildScanInjection.class.getName()); + public static final String DEVELOCITY_INTERNAL_DISABLE_AGENT = "DEVELOCITY_INTERNAL_DISABLE_AGENT"; + + private enum NpmAgentConfig implements EnvVar { + DEVELOCITY_URL, + DEVELOCITY_ALLOW_UNTRUSTED_SERVER, + DEVELOCITY_INTERNAL_ENABLE_JEST_REPORTER_INJECTION, + DEVELOCITY_VALUE_CI_AUTO_INJECTION("DEVELOCITY_VALUE_CIAutoInjection"), + NODE_OPTIONS; + + @Nullable + private final String envVar; + + NpmAgentConfig() { + this(null); + } + + NpmAgentConfig(@Nullable String envVar) { + this.envVar = envVar; + } + + @Override + public String getEnvVar() { + return envVar != null ? envVar : name(); + } + } + public void inject(@Nullable Node node, @Nullable ArtifactDigest npmAgentDigest, EnvVars envComputer) { if (node == null) { return; } - // TODO: Implement npm build scan injection logic + + FilePath userHome = getUserHome(node, envComputer).orElse(null); + if (userHome == null) { + LOGGER.log(Level.WARNING, "Could not determine user home"); + return; + } + + InjectionConfig config = InjectionConfig.get(); + boolean enabled = isInjectionEnabledForNode(config, node); + try { + if (enabled) { + if (npmAgentDigest != null) { + inject(node, userHome, npmAgentDigest, config); + } else { + LOGGER.log(Level.WARNING, "npm agent digest is not present even though injection is enabled"); + } + } else { + cleanup(node, userHome); + } + } catch (IllegalStateException e) { + if (enabled) { + LOGGER.log(Level.WARNING, "Unexpected exception while injecting build scans for npm", e); + } + } + } + + private void inject(Node node, FilePath userHome, ArtifactDigest npmAgentDigest, InjectionConfig config) { + LOGGER.log(Level.INFO, "Injecting npm agent version {0}", config.getNpmAgentVersion()); + + try { + FilePath controllerRootPath = Jenkins.get().getRootPath(); + + installNpmAgent(controllerRootPath, userHome, npmAgentDigest, config); + injectEnvironmentVariables(node, config); + } catch (IOException | InterruptedException e) { + throw new IllegalStateException(e); + } + } + + private void installNpmAgent(FilePath controllerRootPath, FilePath userHome, ArtifactDigest npmAgentDigest, InjectionConfig config) throws IOException, InterruptedException { + FilePath scopePath = userHome.child(".node_modules").child("@gradle-tech"); + FilePath agentPath = scopePath.child("develocity-agent"); + // ~/.node_libraries/@gradle-tech/develocity-agent/version.meta + FilePath versionMeta = agentPath.child("version.meta"); + if (!npmAgentChanged(versionMeta, npmAgentDigest)) { + return; + } + scopePath.deleteRecursive(); + // Unarchive the agent tarball from the controller to the agent node + FilePath agentFilePath = controllerRootPath.child(InjectionUtil.DOWNLOAD_CACHE_DIR).child(NpmAgentDownloadHandler.AGENT_FILENAME); + try (InputStream in = agentFilePath.read()) { + UnarchiveNpmAgent action = new UnarchiveNpmAgent(config.getNpmAgentVersion(), new RemoteInputStream(in, RemoteInputStream.Flag.GREEDY)); + agentPath.act(action); + } + versionMeta.write(npmAgentDigest.digest(), StandardCharsets.UTF_8.name()); + } + + private void injectEnvironmentVariables(Node node, InjectionConfig config) { + EnvUtil.setEnvVar(node, NpmAgentConfig.DEVELOCITY_URL, config.getServer()); + if (config.isAllowUntrusted()) { + EnvUtil.setEnvVar(node, NpmAgentConfig.DEVELOCITY_ALLOW_UNTRUSTED_SERVER, "true"); + } else { + EnvUtil.removeEnvVar(node, NpmAgentConfig.DEVELOCITY_ALLOW_UNTRUSTED_SERVER); + } + EnvUtil.setEnvVar(node, NpmAgentConfig.DEVELOCITY_INTERNAL_ENABLE_JEST_REPORTER_INJECTION, "true"); + EnvUtil.setEnvVar(node, NpmAgentConfig.DEVELOCITY_VALUE_CI_AUTO_INJECTION, "Jenkins"); + // TODO: Merge with existing NODE_OPTIONS if present + EnvUtil.setEnvVar(node, NpmAgentConfig.NODE_OPTIONS, "-r @gradle-tech/develocity-agent/preload"); + } + + private boolean npmAgentChanged(FilePath versionMeta, ArtifactDigest npmAgentDigest) throws IOException, InterruptedException { + if (!versionMeta.exists()) { + return true; + } + String currentDigest = versionMeta.readToString(); + return !npmAgentDigest.matches(currentDigest); + } + + private void cleanup(Node node, FilePath userHome) { + try { + FilePath scopePath = userHome.child(".node_modules").child("@gradle-tech"); + scopePath.deleteRecursive(); + + EnvUtil.removeEnvVars(node, NpmAgentConfig.values()); + } catch (IOException | InterruptedException e) { + throw new IllegalStateException(e); + } + } + + private static Optional getUserHome(Node node, EnvVars envComputer) { + return Optional.ofNullable(EnvUtil.getEnv(envComputer, HOME)).map(node::createPath); } } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/npm/UnarchiveNpmAgent.java b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/UnarchiveNpmAgent.java new file mode 100644 index 00000000..782aed54 --- /dev/null +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/UnarchiveNpmAgent.java @@ -0,0 +1,76 @@ +package hudson.plugins.gradle.injection.npm; + +import hudson.FilePath; +import hudson.remoting.VirtualChannel; +import hudson.util.IOUtils; +import jenkins.MasterToSlaveFileCallable; +import org.apache.tools.tar.TarEntry; +import org.apache.tools.tar.TarInputStream; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serial; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.attribute.FileTime; + +/** + * @see hudson.FilePath#untarFrom(InputStream, FilePath.TarCompression) + */ +public class UnarchiveNpmAgent extends MasterToSlaveFileCallable { + + @Serial + private static final long serialVersionUID = 1L; + + private static final String PACKAGE_ENTRY_PREFIX = "package/"; + + private final String version; + private final InputStream in; + + public UnarchiveNpmAgent(String version, InputStream in) { + this.version = version; + this.in = in; + } + + @Override + public Void invoke(File dir, VirtualChannel channel) throws IOException { + try (TarInputStream tar = new TarInputStream(FilePath.TarCompression.GZIP.extract(in), StandardCharsets.UTF_8.name())) { + TarEntry entry; + while ((entry = tar.getNextEntry()) != null) { + if (entry.isFile()) { + File file = new File(dir, normalizeEntryName(entry)); + File parent = file.getParentFile(); + if (parent != null) { + IOUtils.mkdirs(parent); + } + IOUtils.copy(tar, file); + + Files.setLastModifiedTime(file.toPath(), FileTime.from(entry.getModTime().toInstant())); + int mode = entry.getMode() & 0777; + if (mode != 0) { + new FilePath(file).chmod(mode); // noop on Windows + } + } + } + } catch (IOException e) { + throw wrapIntoIOException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw wrapIntoIOException(e); + } + return null; + } + + private IOException wrapIntoIOException(Throwable cause) { + return new IOException("Failed to extract the Develocity npm agent " + version, cause); + } + + private static String normalizeEntryName(TarEntry entry) throws IOException { + String entryName = entry.getName(); + if (!entryName.startsWith(PACKAGE_ENTRY_PREFIX)) { + throw new IOException("Unexpected entry in the agent tarball: " + entryName); + } + return entryName.substring(PACKAGE_ENTRY_PREFIX.length()); + } +} diff --git a/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly b/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly index ec3e6001..f436e283 100644 --- a/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly +++ b/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly @@ -147,7 +147,7 @@ - + diff --git a/plugin/src/test/groovy/hudson/plugins/gradle/injection/DevelocityComputerListenerTest.groovy b/plugin/src/test/groovy/hudson/plugins/gradle/injection/DevelocityComputerListenerTest.groovy index 8ec54419..57d6cf3c 100644 --- a/plugin/src/test/groovy/hudson/plugins/gradle/injection/DevelocityComputerListenerTest.groovy +++ b/plugin/src/test/groovy/hudson/plugins/gradle/injection/DevelocityComputerListenerTest.groovy @@ -4,6 +4,8 @@ import hudson.EnvVars import hudson.model.Computer import hudson.model.Node import hudson.model.TaskListener +import hudson.plugins.gradle.injection.npm.NpmAgentDownloadHandler +import hudson.plugins.gradle.injection.npm.NpmBuildScanInjection import org.junit.Rule import org.junit.rules.TemporaryFolder import spock.lang.Specification @@ -19,11 +21,20 @@ class DevelocityComputerListenerTest extends Specification { def gradleBuildScanInjection = Mock(GradleBuildScanInjection) def mavenBuildScanInjection = Mock(MavenBuildScanInjection) def mavenExtensionDownloadHandler = Mock(MavenExtensionDownloadHandler) + def npmBuildScanInjection = Mock(NpmBuildScanInjection) + def npmAgentDownloadHandler = Mock(NpmAgentDownloadHandler) def injectionConfig = Mock(InjectionConfig) @Subject def gradleEnterpriseComputerListener = - new DevelocityComputerListener(gradleBuildScanInjection, mavenBuildScanInjection, mavenExtensionDownloadHandler, { injectionConfig }) + new DevelocityComputerListener( + gradleBuildScanInjection, + mavenBuildScanInjection, + mavenExtensionDownloadHandler, + npmBuildScanInjection, + npmAgentDownloadHandler, + { injectionConfig } + ) @Rule public TemporaryFolder tempFolder = new TemporaryFolder() @@ -43,7 +54,7 @@ class DevelocityComputerListenerTest extends Specification { computer.getEnvironment() >> computerEnvVars def root = tempFolder.newFolder() - mavenExtensionDownloadHandler.ensureExtensionsDownloaded({ root }, injectionConfig) >> { } + mavenExtensionDownloadHandler.ensureExtensionsDownloaded({ root }, injectionConfig) >> {} when: gradleEnterpriseComputerListener.onOnline(computer, Mock(TaskListener)) @@ -69,7 +80,7 @@ class DevelocityComputerListenerTest extends Specification { computer.getNode() >> node computer.getEnvironment() >> computerEnvVars def root = tempFolder.newFolder() - mavenExtensionDownloadHandler.ensureExtensionsDownloaded({ root }, injectionConfig) >> { } + mavenExtensionDownloadHandler.ensureExtensionsDownloaded({ root }, injectionConfig) >> {} gradleBuildScanInjection.inject(node, globalEnvVars, computerEnvVars) >> { throw new ExpectedException() } mavenBuildScanInjection.inject(node, [:]) From 103feb80ac57d9d00a926912ee8c2d1ea9d2628c Mon Sep 17 00:00:00 2001 From: Alexey Venderov Date: Mon, 17 Nov 2025 00:49:34 +0100 Subject: [PATCH 10/15] Adopt tests to the new logic --- .../DevelocityComputerListenerTest.groovy | 19 ++++++++++++++----- .../InjectionConfigChangeListenerTest.groovy | 19 ++++++++++++++----- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/plugin/src/test/groovy/hudson/plugins/gradle/injection/DevelocityComputerListenerTest.groovy b/plugin/src/test/groovy/hudson/plugins/gradle/injection/DevelocityComputerListenerTest.groovy index 57d6cf3c..252cd698 100644 --- a/plugin/src/test/groovy/hudson/plugins/gradle/injection/DevelocityComputerListenerTest.groovy +++ b/plugin/src/test/groovy/hudson/plugins/gradle/injection/DevelocityComputerListenerTest.groovy @@ -12,6 +12,8 @@ import spock.lang.Specification import spock.lang.Subject import spock.lang.Unroll +import java.util.concurrent.Callable + import static hudson.plugins.gradle.injection.InjectionUtil.JENKINSGRADLEPLUGIN_GLOBAL_AUTO_INJECTION_CHECK class DevelocityComputerListenerTest extends Specification { @@ -49,18 +51,21 @@ class DevelocityComputerListenerTest extends Specification { injectionConfig.disabled >> !isGlobalInjectionEnabled def node = Mock(Node) - def computerEnvVars = new EnvVars([COMPUTER: "ture"]) + def computerEnvVars = new EnvVars([COMPUTER: "true"]) computer.getNode() >> node computer.getEnvironment() >> computerEnvVars def root = tempFolder.newFolder() - mavenExtensionDownloadHandler.ensureExtensionsDownloaded({ root }, injectionConfig) >> {} + mavenExtensionDownloadHandler.getExtensionDigests({ root }, injectionConfig) >> [:] + npmBuildScanInjection.ifInjectionEnabledGlobally(injectionConfig, _ as Callable) >> Optional.empty() when: gradleEnterpriseComputerListener.onOnline(computer, Mock(TaskListener)) then: (isInjectionExpected ? 1 : 0) * gradleBuildScanInjection.inject(node, globalEnvVars, computerEnvVars) + (isInjectionExpected ? 1 : 0) * mavenBuildScanInjection.inject(node, [:]) + (isInjectionExpected ? 1 : 0) * npmBuildScanInjection.inject(node, null, computerEnvVars) where: isGlobalAutoInjectionCheckEnabled | isGlobalInjectionEnabled || isInjectionExpected @@ -76,19 +81,23 @@ class DevelocityComputerListenerTest extends Specification { computer.name >> "testComputer" def node = Mock(Node) - def computerEnvVars = new EnvVars([COMPUTER: "ture"]) + def computerEnvVars = new EnvVars([COMPUTER: "true"]) computer.getNode() >> node computer.getEnvironment() >> computerEnvVars def root = tempFolder.newFolder() - mavenExtensionDownloadHandler.ensureExtensionsDownloaded({ root }, injectionConfig) >> {} + mavenExtensionDownloadHandler.getExtensionDigests({ root }, injectionConfig) >> [:] + npmBuildScanInjection.ifInjectionEnabledGlobally(injectionConfig, _ as Callable) >> Optional.empty() gradleBuildScanInjection.inject(node, globalEnvVars, computerEnvVars) >> { throw new ExpectedException() } - mavenBuildScanInjection.inject(node, [:]) when: gradleEnterpriseComputerListener.onOnline(computer, Mock(TaskListener)) then: noExceptionThrown() + + and: "other injections are not performed" + 0 * mavenBuildScanInjection.inject(node, [:]) + 0 * npmBuildScanInjection.inject(node, null, computerEnvVars) } } diff --git a/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigChangeListenerTest.groovy b/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigChangeListenerTest.groovy index 9a729fec..9da8e9da 100644 --- a/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigChangeListenerTest.groovy +++ b/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigChangeListenerTest.groovy @@ -12,6 +12,8 @@ import spock.lang.Specification import spock.lang.Subject import spock.lang.Unroll +import java.util.concurrent.Callable + import static hudson.plugins.gradle.injection.InjectionUtil.JENKINSGRADLEPLUGIN_GLOBAL_AUTO_INJECTION_CHECK class InjectionConfigChangeListenerTest extends Specification { @@ -52,17 +54,20 @@ class InjectionConfigChangeListenerTest extends Specification { computer.offline >> isComputerOffline def node = Mock(Node) - def computerEnvVars = new EnvVars([COMPUTER: "ture"]) + def computerEnvVars = new EnvVars([COMPUTER: "true"]) computer.getNode() >> node computer.getEnvironment() >> computerEnvVars def root = tempFolder.newFolder() - mavenExtensionDownloadHandler.ensureExtensionsDownloaded({ root }, injectionConfig) >> {} + mavenExtensionDownloadHandler.ensureExtensionsDownloaded({ root }, injectionConfig) >> [:] + npmBuildScanInjection.ifInjectionEnabledGlobally(injectionConfig, _ as Callable) >> Optional.empty() when: injectionConfigChangeListener.onChange(injectionConfig, UNUSED_XML_FILE) then: (isInjectionExpected ? 1 : 0) * gradleBuildScanInjection.inject(node, globalEnvVars, computerEnvVars) + (isInjectionExpected ? 1 : 0) * mavenBuildScanInjection.inject(node, [:]) + (isInjectionExpected ? 1 : 0) * npmBuildScanInjection.inject(node, null, computerEnvVars) where: isGlobalAutoInjectionCheckEnabled | isGlobalInjectionEnabled | isComputerOffline || isInjectionExpected @@ -82,19 +87,23 @@ class InjectionConfigChangeListenerTest extends Specification { computer.name >> "testComputer" def node = Mock(Node) - def computerEnvVars = new EnvVars([COMPUTER: "ture"]) + def computerEnvVars = new EnvVars([COMPUTER: "true"]) computer.getNode() >> node computer.getEnvironment() >> computerEnvVars def root = tempFolder.newFolder() - mavenExtensionDownloadHandler.ensureExtensionsDownloaded({ root }, injectionConfig) >> {} + mavenExtensionDownloadHandler.ensureExtensionsDownloaded({ root }, injectionConfig) >> [:] + npmBuildScanInjection.ifInjectionEnabledGlobally(injectionConfig, _ as Callable) >> Optional.empty() gradleBuildScanInjection.inject(node, globalEnvVars, computerEnvVars) >> { throw new ExpectedException() } - mavenBuildScanInjection.inject(node, [:]) when: injectionConfigChangeListener.onChange(injectionConfig, UNUSED_XML_FILE) then: noExceptionThrown() + + and: "other injections are not performed" + 0 * mavenBuildScanInjection.inject(node, [:]) + 0 * npmBuildScanInjection.inject(node, null, computerEnvVars) } } From 10c03176a340732bb40f7e35e318705b5581dc42 Mon Sep 17 00:00:00 2001 From: Alexey Venderov Date: Mon, 17 Nov 2025 11:30:28 +0100 Subject: [PATCH 11/15] Override default test timeout --- .../BuildScanInjectionMavenCrossVersionIntegrationTest.groovy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugin/src/test/groovy/hudson/plugins/gradle/injection/BuildScanInjectionMavenCrossVersionIntegrationTest.groovy b/plugin/src/test/groovy/hudson/plugins/gradle/injection/BuildScanInjectionMavenCrossVersionIntegrationTest.groovy index 822a7c38..b9b45392 100644 --- a/plugin/src/test/groovy/hudson/plugins/gradle/injection/BuildScanInjectionMavenCrossVersionIntegrationTest.groovy +++ b/plugin/src/test/groovy/hudson/plugins/gradle/injection/BuildScanInjectionMavenCrossVersionIntegrationTest.groovy @@ -5,6 +5,7 @@ import hudson.plugins.gradle.BuildScanAction import hudson.plugins.gradle.BuildScanBuildWrapper import hudson.tasks.Maven import org.jvnet.hudson.test.CreateFileBuilder +import org.jvnet.hudson.test.recipes.WithTimeout import spock.lang.Unroll @Unroll @@ -12,6 +13,7 @@ class BuildScanInjectionMavenCrossVersionIntegrationTest extends BaseMavenIntegr private static final String MINIMUM_SUPPORTED_MAVEN_VERSION = '3.3.1' + @WithTimeout(300) // 5 minutes def 'build scan is discovered from Maven build - #mavenVersion'(String mavenVersion) { given: mavenInstallationRule.mavenVersion = mavenVersion From 842f76bd467b749dfc6b6f3dcd13faa98f1cd8e3 Mon Sep 17 00:00:00 2001 From: Alexey Venderov Date: Thu, 27 Nov 2025 12:32:21 +0100 Subject: [PATCH 12/15] Add convenience methods --- .../hudson/plugins/gradle/injection/MavenExtension.java | 4 ++++ .../gradle/injection/download/AgentDownloadClient.java | 4 ++++ .../gradle/injection/npm/NpmAgentDownloadHandler.java | 3 +-- .../MavenExtensionsHandlerIntegrationTest.groovy | 9 ++++----- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java index 65cda6e5..d4700f29 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java @@ -52,6 +52,10 @@ public static MavenExtension forVersion(String version) { return version.compareTo(LAST_GRADLE_ENTERPRISE_VERSION) > 0 ? DEVELOCITY : GRADLE_ENTERPRISE; } + public URI createDownloadUrl(String version) { + return createDownloadUrl(version, null); + } + public URI createDownloadUrl(String version, @Nullable String repositoryUrl) { String extensionUrlTemplate = getNormalizedRepositoryUrl(repositoryUrl) + EXTENSION_REPOSITORY_PATH; return URI.create(String.format(extensionUrlTemplate, this.getName(), version, this.getName(), version)); diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/download/AgentDownloadClient.java b/plugin/src/main/java/hudson/plugins/gradle/injection/download/AgentDownloadClient.java index 3eeae5f6..ba28301e 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/download/AgentDownloadClient.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/download/AgentDownloadClient.java @@ -21,6 +21,10 @@ public AgentDownloadClient() { .build(); } + public void download(URI downloadUrl, OutputStream outputStream) throws IOException { + download(downloadUrl, RequestAuthenticator.NONE, outputStream); + } + public void download(URI downloadUrl, RequestAuthenticator authenticator, OutputStream outputStream) throws IOException { Request.Builder requestBuilder = new Request.Builder().url(downloadUrl.toURL()); authenticator.authenticate(requestBuilder); diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmAgentDownloadHandler.java b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmAgentDownloadHandler.java index 9ebc18d9..133b3a54 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmAgentDownloadHandler.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmAgentDownloadHandler.java @@ -6,7 +6,6 @@ import hudson.plugins.gradle.injection.InjectionConfig; import hudson.plugins.gradle.injection.InjectionUtil; import hudson.plugins.gradle.injection.download.AgentDownloadClient; -import hudson.plugins.gradle.injection.download.RequestAuthenticator; import java.io.BufferedOutputStream; import java.io.File; @@ -48,7 +47,7 @@ public ArtifactDigest downloadNpmAgent(Supplier root, InjectionConfig inje URI downloadUrl = createDownloadUrl(npmAgentVersion); try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(agentFile))) { - downloadClient.download(downloadUrl, RequestAuthenticator.NONE, outputStream); + downloadClient.download(downloadUrl, outputStream); } // TODO: Consider downloading the checksum file from the repository and verifying the download against it diff --git a/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionsHandlerIntegrationTest.groovy b/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionsHandlerIntegrationTest.groovy index b4a286a1..6438e692 100644 --- a/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionsHandlerIntegrationTest.groovy +++ b/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionsHandlerIntegrationTest.groovy @@ -2,7 +2,6 @@ package hudson.plugins.gradle.injection import hudson.FilePath import hudson.plugins.gradle.injection.download.AgentDownloadClient -import hudson.plugins.gradle.injection.download.RequestAuthenticator import org.junit.Rule import org.junit.rules.TemporaryFolder import spock.lang.Specification @@ -51,12 +50,12 @@ class MavenExtensionsHandlerIntegrationTest extends Specification { cacheDirectory.child(MavenExtension.DEVELOCITY.getEmbeddedJarName()).write() .withCloseable { - downloadClient.download(MavenExtension.DEVELOCITY.createDownloadUrl("2.1", null), RequestAuthenticator.NONE, it) + downloadClient.download(MavenExtension.DEVELOCITY.createDownloadUrl("2.1"), it) } cacheDirectory.child(MavenExtension.CCUD.getEmbeddedJarName()).write() .withCloseable { - downloadClient.download(MavenExtension.CCUD.createDownloadUrl("2.0.1", null), RequestAuthenticator.NONE, it) + downloadClient.download(MavenExtension.CCUD.createDownloadUrl("2.0.1"), it) } when: @@ -82,12 +81,12 @@ class MavenExtensionsHandlerIntegrationTest extends Specification { cacheDirectory.child(MavenExtension.DEVELOCITY.getEmbeddedJarName()).write() .withCloseable { - downloadClient.download(MavenExtension.DEVELOCITY.createDownloadUrl("2.1", null), RequestAuthenticator.NONE, it) + downloadClient.download(MavenExtension.DEVELOCITY.createDownloadUrl("2.1"), it) } cacheDirectory.child(MavenExtension.CCUD.getEmbeddedJarName()).write() .withCloseable { - downloadClient.download(MavenExtension.CCUD.createDownloadUrl("2.0.1", null), RequestAuthenticator.NONE, it) + downloadClient.download(MavenExtension.CCUD.createDownloadUrl("2.0.1"), it) } when: From 73c832461f97fcb563be4fb8bd0efa872f79b887 Mon Sep 17 00:00:00 2001 From: Alexey Venderov Date: Fri, 28 Nov 2025 18:59:44 +0100 Subject: [PATCH 13/15] Implement npm agent registry overriding --- .../gradle/injection/InjectionConfig.java | 21 ++++++++ .../gradle/injection/InjectionUtil.java | 8 +++ .../gradle/injection/MavenExtension.java | 16 ++---- .../npm/NpmAgentDownloadHandler.java | 8 +-- .../injection/InjectionConfig/config.jelly | 3 ++ .../help-npmAgentRegistryUrl.html | 4 ++ ...ectionConfigWithCasCIntegrationTest.groovy | 51 ++++++++++--------- .../src/test/resources/injection-config.yml | 6 +++ 8 files changed, 79 insertions(+), 38 deletions(-) create mode 100644 plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/help-npmAgentRegistryUrl.html diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfig.java b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfig.java index ee75d3e8..2473846b 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfig.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfig.java @@ -99,6 +99,7 @@ public class InjectionConfig extends GlobalConfiguration { private Boolean mavenCaptureGoalInputFiles; private String npmAgentVersion; + private String npmAgentRegistryUrl; private ImmutableList npmInjectionEnabledNodes; private ImmutableList npmInjectionDisabledNodes; @@ -358,6 +359,16 @@ public void setNpmAgentVersion(String npmAgentVersion) { this.npmAgentVersion = Util.fixEmptyAndTrim(npmAgentVersion); } + @CheckForNull + public String getNpmAgentRegistryUrl() { + return npmAgentRegistryUrl; + } + + @DataBoundSetter + public void setNpmAgentRegistryUrl(String npmAgentRegistryUrl) { + this.npmAgentRegistryUrl = Util.fixEmptyAndTrim(npmAgentRegistryUrl); + } + @CheckForNull public List getNpmInjectionEnabledNodes() { return npmInjectionEnabledNodes; @@ -602,6 +613,16 @@ public FormValidation doCheckNpmAgentVersion(@QueryParameter String value) { return checkVersion(value); } + @Restricted(NoExternalUse.class) + @POST + public FormValidation doCheckNpmAgentRegistryUrl(@QueryParameter String value) { + if (doesNotHaveAdministerPermission()) { + return FormValidation.error("Validating npm agent registry URL requires 'Administer' permission"); + } + + return checkUrl(value); + } + @SuppressWarnings("called by Jelly") @POST public ListBoxModel doFillGradlePluginRepositoryCredentialIdItems(@AncestorInPath Item project) { diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionUtil.java b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionUtil.java index 4224a1ac..7f4c889c 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionUtil.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionUtil.java @@ -2,6 +2,7 @@ import hudson.EnvVars; import hudson.PluginWrapper; +import hudson.Util; import hudson.model.labels.LabelAtom; import hudson.plugins.gradle.util.CollectionUtil; import hudson.util.FormValidation; @@ -29,6 +30,13 @@ public final class InjectionUtil { private InjectionUtil() { } + public static String getNormalizedUrl(@Nullable String url, String defaultUrl) { + if (url == null || InjectionUtil.isInvalid(InjectionConfig.checkUrl(url))) { + return defaultUrl; + } + return Util.removeTrailingSlash(url); + } + public static Path getDownloadCacheDir(Supplier rootDir) { return rootDir.get().toPath().resolve(DOWNLOAD_CACHE_DIR); } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java index d4700f29..429ee238 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java @@ -1,7 +1,5 @@ package hudson.plugins.gradle.injection; -import hudson.Util; - import javax.annotation.Nullable; import java.net.URI; @@ -12,7 +10,6 @@ public enum MavenExtension { CCUD("common-custom-user-data-maven-extension", "ccud_metadata", new MavenCoordinates("com.gradle", "common-custom-user-data-maven-extension")), CONFIGURATION("configuration-maven-extension", "configuration_metadata", new MavenCoordinates("com.gradle", "configuration-maven-extension")); - private static final String EXTENSION_REPOSITORY_PATH = "/com/gradle/%s/%s/%s-%s.jar"; private static final String DEFAULT_REPOSITORY_URL = "https://repo1.maven.org/maven2"; private static final String JAR_EXTENSION = ".jar"; private static final String LAST_GRADLE_ENTERPRISE_VERSION = "1.20.1"; @@ -57,14 +54,9 @@ public URI createDownloadUrl(String version) { } public URI createDownloadUrl(String version, @Nullable String repositoryUrl) { - String extensionUrlTemplate = getNormalizedRepositoryUrl(repositoryUrl) + EXTENSION_REPOSITORY_PATH; - return URI.create(String.format(extensionUrlTemplate, this.getName(), version, this.getName(), version)); - } - - private String getNormalizedRepositoryUrl(@Nullable String repositoryUrl) { - if (repositoryUrl == null || InjectionUtil.isInvalid(InjectionConfig.checkUrl(repositoryUrl))) { - return DEFAULT_REPOSITORY_URL; - } - return Util.removeTrailingSlash(repositoryUrl); + String normalizedRepositoryUrl = InjectionUtil.getNormalizedUrl(repositoryUrl, DEFAULT_REPOSITORY_URL); + return URI.create( + "%1$s/com/gradle/%2$s/%3$s/%2$s-%3$s.jar".formatted(normalizedRepositoryUrl, getName(), version) + ); } } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmAgentDownloadHandler.java b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmAgentDownloadHandler.java index 133b3a54..b5d9ae31 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmAgentDownloadHandler.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmAgentDownloadHandler.java @@ -7,6 +7,7 @@ import hudson.plugins.gradle.injection.InjectionUtil; import hudson.plugins.gradle.injection.download.AgentDownloadClient; +import javax.annotation.Nullable; import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; @@ -45,7 +46,7 @@ public ArtifactDigest downloadNpmAgent(Supplier root, InjectionConfig inje Files.createDirectories(cacheDir); Path agentFile = cacheDir.resolve(AGENT_FILENAME); - URI downloadUrl = createDownloadUrl(npmAgentVersion); + URI downloadUrl = createDownloadUrl(npmAgentVersion, injectionConfig.getNpmAgentRegistryUrl()); try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(agentFile))) { downloadClient.download(downloadUrl, outputStream); } @@ -64,9 +65,10 @@ public ArtifactDigest getDownloadedNpmAgentDigest(Supplier root) throws IO .orElse(null); } - private static URI createDownloadUrl(String npmAgentVersion) { + private static URI createDownloadUrl(String npmAgentVersion, @Nullable String registryUrl) { + String normalizedRegistryUrl = InjectionUtil.getNormalizedUrl(registryUrl, DEFAULT_REGISTRY_URL); return URI.create( - "%s/@gradle-tech/develocity-agent/-/develocity-agent-%s.tgz".formatted(DEFAULT_REGISTRY_URL, npmAgentVersion) + "%s/@gradle-tech/develocity-agent/-/develocity-agent-%s.tgz".formatted(normalizedRegistryUrl, npmAgentVersion) ); } } diff --git a/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly b/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly index f436e283..4fb13665 100644 --- a/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly +++ b/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly @@ -151,6 +151,9 @@ + + + diff --git a/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/help-npmAgentRegistryUrl.html b/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/help-npmAgentRegistryUrl.html new file mode 100644 index 00000000..2813d1bd --- /dev/null +++ b/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/help-npmAgentRegistryUrl.html @@ -0,0 +1,4 @@ +
+ The URL of the registry to use when resolving the Develocity npm agent. + Defaults to https://registry.npmjs.org. +
diff --git a/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigWithCasCIntegrationTest.groovy b/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigWithCasCIntegrationTest.groovy index 3b946670..0a5b47b3 100644 --- a/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigWithCasCIntegrationTest.groovy +++ b/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigWithCasCIntegrationTest.groovy @@ -12,6 +12,7 @@ import spock.lang.Unroll @Unroll @Subject(InjectionConfig.class) class InjectionConfigWithCasCIntegrationTest extends AbstractIntegrationTest { + @Rule public final RuleChain rules = RuleChain.outerRule(noSpaceInTmpDirs).around(new JenkinsConfiguredWithCodeRule()) @@ -19,29 +20,33 @@ class InjectionConfigWithCasCIntegrationTest extends AbstractIntegrationTest { def 'current configuration is readable with JCasC'() { expect: with(InjectionConfig.get()) { - it.allowUntrusted == true - it.ccudExtensionCustomCoordinates == "mycustom-ccud:ext" - it.ccudExtensionVersion == "2.0.1" - it.ccudPluginVersion == "2.0.2" - it.checkForBuildAgentErrors == true - it.enabled == true - it.enforceUrl == true - it.gradleCaptureTaskInputFiles == true - it.gradleInjectionDisabledNodes*.label == ["non-gradle-node"] - it.gradleInjectionEnabledNodes*.label == ["gradle-node"] - it.gradlePluginRepositoryUrl == "https://plugins.gradle.org" - it.gradlePluginVersion == "3.18.1" - it.injectMavenExtension == true - it.injectCcudExtension == true - it.mavenCaptureGoalInputFiles == true - it.mavenExtensionCustomCoordinates == "mycustom:ext" - it.mavenExtensionRepositoryUrl == "https://repo1.maven.org/maven2" - it.mavenExtensionVersion == "2.1" - it.mavenInjectionDisabledNodes*.label == ["non-maven-node"] - it.mavenInjectionEnabledNodes*.label == ["maven-node"] - it.server == "http://localhost:5086" - it.shortLivedTokenExpiry == 24 - it.vcsRepositoryFilter == "+:myrepo" + allowUntrusted + ccudExtensionCustomCoordinates == "mycustom-ccud:ext" + ccudExtensionVersion == "2.0.1" + ccudPluginVersion == "2.0.2" + checkForBuildAgentErrors + enabled + enforceUrl + gradleCaptureTaskInputFiles + gradleInjectionDisabledNodes*.label == ["non-gradle-node"] + gradleInjectionEnabledNodes*.label == ["gradle-node"] + gradlePluginRepositoryUrl == "https://plugins.gradle.org" + gradlePluginVersion == "3.18.1" + injectMavenExtension + injectCcudExtension + mavenCaptureGoalInputFiles == true + mavenExtensionCustomCoordinates == "mycustom:ext" + mavenExtensionRepositoryUrl == "https://repo1.maven.org/maven2" + mavenExtensionVersion == "2.1" + mavenInjectionDisabledNodes*.label == ["non-maven-node"] + mavenInjectionEnabledNodes*.label == ["maven-node"] + npmAgentVersion == "3.0.0" + npmAgentRegistryUrl == "https://registry.npmjs.org" + npmInjectionDisabledNodes*.label == ["non-npm-node"] + npmInjectionEnabledNodes*.label == ["npm-node"] + server == "http://localhost:5086" + shortLivedTokenExpiry == 24 + vcsRepositoryFilter == "+:myrepo" } } } diff --git a/plugin/src/test/resources/injection-config.yml b/plugin/src/test/resources/injection-config.yml index 8e51dc33..1ba3b06d 100644 --- a/plugin/src/test/resources/injection-config.yml +++ b/plugin/src/test/resources/injection-config.yml @@ -25,6 +25,12 @@ unclassified: - label: "non-maven-node" mavenInjectionEnabledNodes: - label: "maven-node" + npmAgentVersion: "3.0.0" + npmAgentRegistryUrl: "https://registry.npmjs.org" + npmInjectionDisabledNodes: + - label: "non-npm-node" + npmInjectionEnabledNodes: + - label: "npm-node" server: "http://localhost:5086" shortLivedTokenExpiry: 24 vcsRepositoryFilter: "+:myrepo" From 867cc5327195b5d68cde628f365df87931cd1708 Mon Sep 17 00:00:00 2001 From: Alexey Venderov Date: Sat, 29 Nov 2025 00:39:45 +0100 Subject: [PATCH 14/15] Enriched build scans for npm --- .../hudson/plugins/gradle/BuildToolType.java | 4 +- .../plugins/gradle/enriched/ScanDetail.java | 23 +++++- .../enriched/ScanDetailServiceTest.groovy | 73 ++++++++++--------- 3 files changed, 62 insertions(+), 38 deletions(-) diff --git a/plugin/src/main/java/hudson/plugins/gradle/BuildToolType.java b/plugin/src/main/java/hudson/plugins/gradle/BuildToolType.java index 5cbf8cf5..0a5db89c 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/BuildToolType.java +++ b/plugin/src/main/java/hudson/plugins/gradle/BuildToolType.java @@ -9,7 +9,9 @@ public enum BuildToolType { @JsonProperty("gradle") GRADLE, @JsonProperty("maven") - MAVEN; + MAVEN, + @JsonProperty("npm") + NPM; public String getAttributesUrlSuffix() { return String.format("/%s-attributes", name().toLowerCase(Locale.ROOT)); diff --git a/plugin/src/main/java/hudson/plugins/gradle/enriched/ScanDetail.java b/plugin/src/main/java/hudson/plugins/gradle/enriched/ScanDetail.java index 0608d33f..f47f50c6 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/enriched/ScanDetail.java +++ b/plugin/src/main/java/hudson/plugins/gradle/enriched/ScanDetail.java @@ -2,11 +2,13 @@ import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSetter; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.plugins.gradle.BuildToolType; import org.kohsuke.stapler.export.ExportedBean; import java.util.List; +import java.util.Map; import java.util.Objects; @ExportedBean @@ -15,7 +17,7 @@ public class ScanDetail { private final String url; - @JsonAlias({"rootProjectName", "topLevelProjectName"}) + @JsonAlias({"rootProjectName", "topLevelProjectName", "packageName"}) private String projectName; @SuppressFBWarnings(value = "UWF_UNWRITTEN_FIELD") private BuildToolType buildToolType; @@ -26,6 +28,18 @@ public class ScanDetail { @SuppressFBWarnings(value = "UWF_UNWRITTEN_FIELD") private boolean hasFailed; + // Converts NpmCommand object into a list of tasks + @JsonSetter("command") + private void setCommand(Map npmCommand) { + if (npmCommand == null) { + return; + } + Object command = npmCommand.get("command"); + if (command != null && (tasks == null || tasks.isEmpty())) { + tasks = List.of(command.toString()); + } + } + ScanDetail(String url) { this.url = url; } @@ -59,7 +73,12 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ScanDetail that = (ScanDetail) o; - return hasFailed == that.hasFailed && Objects.equals(url, that.url) && Objects.equals(projectName, that.projectName) && buildToolType == that.buildToolType && Objects.equals(buildToolVersion, that.buildToolVersion) && Objects.equals(tasks, that.tasks); + return hasFailed == that.hasFailed + && Objects.equals(url, that.url) + && Objects.equals(projectName, that.projectName) + && buildToolType == that.buildToolType + && Objects.equals(buildToolVersion, that.buildToolVersion) + && Objects.equals(tasks, that.tasks); } @Override diff --git a/plugin/src/test/groovy/hudson/plugins/gradle/enriched/ScanDetailServiceTest.groovy b/plugin/src/test/groovy/hudson/plugins/gradle/enriched/ScanDetailServiceTest.groovy index f3506c6e..8bcc33d3 100644 --- a/plugin/src/test/groovy/hudson/plugins/gradle/enriched/ScanDetailServiceTest.groovy +++ b/plugin/src/test/groovy/hudson/plugins/gradle/enriched/ScanDetailServiceTest.groovy @@ -55,7 +55,7 @@ class ScanDetailServiceTest extends Specification { def httpClient = Stub(CloseableHttpClient) def response = Stub(CloseableHttpResponse) response.getStatusLine() >> new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_FORBIDDEN, "") - httpClientFactory.buildHttpClient(_,_,_) >> httpClient + httpClientFactory.buildHttpClient(_, _, _) >> httpClient httpClient.execute(_) >> response when: @@ -66,7 +66,7 @@ class ScanDetailServiceTest extends Specification { } @Unroll - def 'Get scan detail with HTTP error on second request'(String buildToolType) { + def 'Get scan detail with HTTP error on second request'(String buildTool) { given: def scanDetailService = new ScanDetailService(getTestConfig()) def httpClientFactory = Stub(HttpClientFactory) @@ -77,13 +77,13 @@ class ScanDetailServiceTest extends Specification { response1.getEntity() >> new StringEntity( """ { - "buildToolType": "${buildToolType}", + "buildToolType": "${buildTool}", "buildToolVersion": "7.5.1" } """.stripIndent()) def response2 = Stub(CloseableHttpResponse) response2.getStatusLine() >> new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_INTERNAL_SERVER_ERROR, "") - httpClientFactory.buildHttpClient(_,_,_) >> httpClient + httpClientFactory.buildHttpClient(_, _, _) >> httpClient httpClient.execute(_) >>> [response1, response2] when: @@ -93,7 +93,7 @@ class ScanDetailServiceTest extends Specification { scanDetail == Optional.empty() where: - buildToolType << ["gradle", "maven"] + buildTool << ["gradle", "maven", "npm"] } @Unroll @@ -108,14 +108,14 @@ class ScanDetailServiceTest extends Specification { response1.getEntity() >> new StringEntity( """ { - "buildToolType": "${buildToolType}", + "buildToolType": "${buildTool}", "buildToolVersion": "7.5.1" } """.stripIndent()) def response2 = Stub(CloseableHttpResponse) response2.getStatusLine() >> new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "") response2.getEntity() >> new StringEntity("{This is not valid JSON}") - httpClientFactory.buildHttpClient(_,_,_) >> httpClient + httpClientFactory.buildHttpClient(_, _, _) >> httpClient httpClient.execute(_) >>> [response1, response2] when: @@ -125,7 +125,7 @@ class ScanDetailServiceTest extends Specification { scanDetail == Optional.empty() where: - buildToolType << ["gradle", "maven"] + buildTool << ["gradle", "maven", "npm"] } @Unroll @@ -140,37 +140,39 @@ class ScanDetailServiceTest extends Specification { response1.getEntity() >> new StringEntity( """ { - "buildToolType": "${buildToolType}", + "buildToolType": "${buildTool}", "buildToolVersion": "7.5.1" } """.stripIndent()) def response2 = Stub(CloseableHttpResponse) response2.getStatusLine() >> new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "") response2.getEntity() >> new StringEntity(httpResponseBody) - httpClientFactory.buildHttpClient(_,_,_) >> httpClient + httpClientFactory.buildHttpClient(_, _, _) >> httpClient httpClient.execute(_) >>> [response1, response2] - httpClientFactory.buildHttpClient(_,_,_) >> httpClient + httpClientFactory.buildHttpClient(_, _, _) >> httpClient when: def scanDetailResult = scanDetailService.getScanDetail("https://foo.bar/s/scanId") then: - def scanDetail = scanDetailResult.get() - scanDetail.url == "https://foo.bar/s/scanId" - scanDetail.buildToolType.toString() == buildToolType.toUpperCase() - scanDetail.buildToolVersion == "7.5.1" - scanDetail.projectName == "project" - scanDetail.tasks == [ "clean", "build" ] - !scanDetail.hasFailed + with(scanDetailResult.get()) { + url == "https://foo.bar/s/scanId" + buildToolType.toString() == buildTool.toUpperCase() + buildToolVersion == "7.5.1" + projectName == "project" + tasks == expectedTasks + !hasFailed + } where: - buildToolType | httpResponseBody - "gradle" | '{"foo":"bar","rootProjectName":"project","requestedTasks":["clean","build"],"hasFailed":false}' - "maven" | '{"foo":"bar","topLevelProjectName":"project","requestedGoals":["clean","build"],"hasFailed":false}' + buildTool | httpResponseBody | expectedTasks + "gradle" | '{"foo":"bar","rootProjectName":"project","requestedTasks":["clean","build"],"hasFailed":false}' | ["clean", "build"] + "maven" | '{"foo":"bar","topLevelProjectName":"project","requestedGoals":["clean","build"],"hasFailed":false}' | ["clean", "build"] + "npm" | '{"foo":"bar","packageName":"project","command":{"command":"install","arguments":["arg1", "arg2"]},"hasFailed":false}' | ["install"] } @Unroll - def 'Get scan detail'(String buildToolType, String httpResponseBody) { + def 'Get scan detail'(String buildTool, String httpResponseBody) { given: def scanDetailService = new ScanDetailService(getTestConfig()) def httpClientFactory = Stub(HttpClientFactory) @@ -181,7 +183,7 @@ class ScanDetailServiceTest extends Specification { response1.getEntity() >> new StringEntity( """ { - "buildToolType": "${buildToolType}", + "buildToolType": "${buildTool}", "buildToolVersion": "7.5.1" } """.stripIndent()) @@ -189,24 +191,25 @@ class ScanDetailServiceTest extends Specification { response2.getStatusLine() >> new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "") response2.getEntity() >> new StringEntity(httpResponseBody) httpClient.execute(_) >>> [response1, response2] - httpClientFactory.buildHttpClient(_,_,_) >> httpClient + httpClientFactory.buildHttpClient(_, _, _) >> httpClient when: def scanDetailResult = scanDetailService.getScanDetail("https://foo.bar/s/scanId") then: - def scanDetail = scanDetailResult.get() - scanDetail.url == "https://foo.bar/s/scanId" - scanDetail.buildToolType.toString() == buildToolType.toUpperCase() - scanDetail.buildToolVersion == "7.5.1" - scanDetail.projectName == "project" - scanDetail.tasks == [ "clean", "build" ] - !scanDetail.hasFailed + with(scanDetailResult.get()) { + url == "https://foo.bar/s/scanId" + buildToolType.toString() == buildTool.toUpperCase() + buildToolVersion == "7.5.1" + projectName == "project" + tasks == expectedTasks + !hasFailed + } where: - buildToolType | httpResponseBody - "gradle" | '{"foo":"bar","rootProjectName":"project","requestedTasks":["clean","build"],"hasFailed":false}' - "maven" | '{"topLevelProjectName":"project","requestedGoals":["clean","build"],"hasFailed":false}' + buildTool | httpResponseBody | expectedTasks + "gradle" | '{"rootProjectName":"project","requestedTasks":["clean","build"],"hasFailed":false}' | ["clean", "build"] + "maven" | '{"topLevelProjectName":"project","requestedGoals":["clean","build"],"hasFailed":false}' | ["clean", "build"] + "npm" | '{"packageName":"project","command":{"command":"install","arguments":["arg1", "arg2"]},"hasFailed":false}' | ["install"] } - } From 992a42b1c4a5848b028c888ac8e17d351e37fd82 Mon Sep 17 00:00:00 2001 From: Alexey Venderov Date: Thu, 4 Dec 2025 16:56:27 +0100 Subject: [PATCH 15/15] Remove [Beta] --- .../hudson/plugins/gradle/injection/InjectionConfig.java | 4 +++- .../gradle/injection/npm/NpmBuildScanInjection.java | 7 +++++-- .../plugins/gradle/injection/InjectionConfig/config.jelly | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfig.java b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfig.java index 2473846b..13f134e4 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfig.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfig.java @@ -667,7 +667,9 @@ private static ListBoxModel getAllCredentials(Item project) { private static FormValidation validateMavenCoordinates(String value) { String coord = Util.fixEmptyAndTrim(value); - return coord == null || MavenCoordinates.isValid(coord) ? FormValidation.ok() : FormValidation.error(Messages.InjectionConfig_InvalidMavenExtensionCustomCoordinates()); + return coord == null || MavenCoordinates.isValid(coord) + ? FormValidation.ok() + : FormValidation.error(Messages.InjectionConfig_InvalidMavenExtensionCustomCoordinates()); } public static FormValidation checkRequiredUrl(String value) { diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmBuildScanInjection.java b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmBuildScanInjection.java index bbd0289e..0f87ccf5 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmBuildScanInjection.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmBuildScanInjection.java @@ -27,6 +27,9 @@ public class NpmBuildScanInjection implements NpmInjectionAware { public static final String DEVELOCITY_INTERNAL_DISABLE_AGENT = "DEVELOCITY_INTERNAL_DISABLE_AGENT"; + private static final String NODE_LIBRARIES_DIR = ".node_libraries"; + private static final String GRADLE_TECH_SCOPE = "@gradle-tech"; + private enum NpmAgentConfig implements EnvVar { DEVELOCITY_URL, DEVELOCITY_ALLOW_UNTRUSTED_SERVER, @@ -95,7 +98,7 @@ private void inject(Node node, FilePath userHome, ArtifactDigest npmAgentDigest, } private void installNpmAgent(FilePath controllerRootPath, FilePath userHome, ArtifactDigest npmAgentDigest, InjectionConfig config) throws IOException, InterruptedException { - FilePath scopePath = userHome.child(".node_modules").child("@gradle-tech"); + FilePath scopePath = userHome.child(NODE_LIBRARIES_DIR).child(GRADLE_TECH_SCOPE); FilePath agentPath = scopePath.child("develocity-agent"); // ~/.node_libraries/@gradle-tech/develocity-agent/version.meta FilePath versionMeta = agentPath.child("version.meta"); @@ -135,7 +138,7 @@ private boolean npmAgentChanged(FilePath versionMeta, ArtifactDigest npmAgentDig private void cleanup(Node node, FilePath userHome) { try { - FilePath scopePath = userHome.child(".node_modules").child("@gradle-tech"); + FilePath scopePath = userHome.child(NODE_LIBRARIES_DIR).child(GRADLE_TECH_SCOPE); scopePath.deleteRecursive(); EnvUtil.removeEnvVars(node, NpmAgentConfig.values()); diff --git a/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly b/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly index 4fb13665..4eb919bb 100644 --- a/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly +++ b/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly @@ -147,7 +147,7 @@
- +