From 9a6c4f2ef97643a77d393bf5a5f939aa87ad0d19 Mon Sep 17 00:00:00 2001 From: Filipe Roque Date: Mon, 3 Jul 2023 14:49:36 +0100 Subject: [PATCH] Adds auto-favoriting exclusion for workflow libraries Without testing if a remote URL is a match for the buildData it might log an error when testing a specific commit against the wrong git repository: Unexpected error when retrieving changeset hudson.plugins.git.GitException: Error: git whatchanged --no-abbrev ... Should fix JENKINS-43400. Fixing the previous behavior leads to auto-favoriting the author of the last commit for the workflow library for all builds that contains the workflow library. Bumps supported Jenkins to 2.289.3, required by pipeline-groovy-lib. pipeline-groovy-lib contains duplicate code from deprecated plugin workflow-cps-global-lib-plugin. Could not add support for both plugins due the java package being the same. --- Jenkinsfile | 2 +- pom.xml | 41 +--- .../autofavorite/FavoritingScmListener.java | 96 ++++++++- .../autofavorite/WorkFlowLibrariesTest.java | 202 ++++++++++++++++++ src/test/resources/job-repository/Jenkinsfile | 18 ++ .../library-repository/vars/helloWorld.groovy | 3 + 6 files changed, 318 insertions(+), 44 deletions(-) create mode 100644 src/test/java/io/jenkins/blueocean/autofavorite/WorkFlowLibrariesTest.java create mode 100644 src/test/resources/job-repository/Jenkinsfile create mode 100644 src/test/resources/library-repository/vars/helloWorld.groovy diff --git a/Jenkinsfile b/Jenkinsfile index 1b0437b..e8ba77e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -2,5 +2,5 @@ buildPlugin(useContainerAgent: true, configurations: [ [ platform: "linux", jdk: "8" ], [ platform: "windows", jdk: "8" ], - [ platform: "linux", jdk: "11", jenkins: "2.222.3" ] + [ platform: "linux", jdk: "11" ] ]) diff --git a/pom.xml b/pom.xml index fa981b5..3514a54 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ 8 - 2.176.4 + 2.289.3 false @@ -54,14 +54,6 @@ org.jenkins-ci.plugins git - 3.6.0 - - - - org.jenkins-ci - annotation-indexer - - org.jenkins-ci.plugins @@ -70,7 +62,6 @@ org.jenkins-ci.plugins branch-api - 2.0.11 org.jvnet.hudson.plugins @@ -81,26 +72,18 @@ org.jenkinsci.plugins pipeline-model-definition - 1.2.2 test - - - - org.apache.commons - commons-lang3 - - - - org.jenkins-ci.plugins - junit - test + io.jenkins.plugins + pipeline-groovy-lib + 591.v3a_7f422b_d058 + true + org.jenkins-ci.plugins - command-launcher - 1.3 + junit test @@ -109,17 +92,11 @@ io.jenkins.tools.bom - bom-2.176.x - 9 + bom-2.289.x + 1500.ve4d05cd32975 import pom - - junit - junit - 4.13.1 - test - diff --git a/src/main/java/io/jenkins/blueocean/autofavorite/FavoritingScmListener.java b/src/main/java/io/jenkins/blueocean/autofavorite/FavoritingScmListener.java index df2f736..e5437a0 100644 --- a/src/main/java/io/jenkins/blueocean/autofavorite/FavoritingScmListener.java +++ b/src/main/java/io/jenkins/blueocean/autofavorite/FavoritingScmListener.java @@ -1,6 +1,18 @@ package io.jenkins.blueocean.autofavorite; +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.annotation.CheckForNull; + import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; @@ -17,27 +29,28 @@ import hudson.plugins.git.GitSCM; import hudson.plugins.git.GitTool; import hudson.plugins.git.Revision; +import hudson.plugins.git.UserRemoteConfig; import hudson.plugins.git.util.BuildData; import hudson.scm.SCM; import hudson.scm.SCMRevisionState; import hudson.util.LogTaskListener; import io.jenkins.blueocean.autofavorite.user.FavoritingUserProperty; import jenkins.branch.MultiBranchProject; +import jenkins.model.Jenkins; +import jenkins.plugins.git.GitSCMSource; +import jenkins.scm.api.SCMSource; import org.apache.commons.io.IOUtils; import org.eclipse.jgit.errors.MissingObjectException; import org.jenkinsci.plugins.gitclient.Git; import org.jenkinsci.plugins.gitclient.GitClient; import org.jenkinsci.plugins.workflow.job.WorkflowRun; - -import javax.annotation.CheckForNull; -import java.io.File; -import java.io.IOException; -import java.io.StringReader; -import java.io.StringWriter; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; +import org.jenkinsci.plugins.workflow.libs.FolderLibraries; +import org.jenkinsci.plugins.workflow.libs.GlobalLibraries; +import org.jenkinsci.plugins.workflow.libs.LibrariesAction; +import org.jenkinsci.plugins.workflow.libs.LibraryConfiguration; +import org.jenkinsci.plugins.workflow.libs.LibraryRecord; +import org.jenkinsci.plugins.workflow.libs.LibraryRetriever; +import org.jenkinsci.plugins.workflow.libs.SCMSourceRetriever; @Extension public class FavoritingScmListener extends SCMListener { @@ -65,7 +78,23 @@ public void onCheckout(Run build, SCM scm, FilePath workspace, TaskListene return; } - BuildData buildData = build.getAction(BuildData.class); + final List urls = ((GitSCM) scm).getUserRemoteConfigs().stream() + .map(UserRemoteConfig::getUrl) + .collect(Collectors.toList()); + + final List buildDataList = build.getActions(BuildData.class); + BuildData buildData = buildDataList.stream() + .filter(bd -> !Sets.intersection(Sets.newHashSet(urls), bd.remoteUrls).isEmpty()) + .findFirst() + .orElse(null); + + if (Jenkins.get().getPlugin("pipeline-groovy-lib") != null) { + if (shouldSkipLibrariesRepository(build, urls)) { + LOGGER.fine("Remote repository is for a Workflow Library. Skipping auto favoriting."); + return; + } + } + if (buildData == null) { LOGGER.fine("No Git Build Data is present. Favoriting cannot be run."); return; @@ -135,6 +164,51 @@ public void onCheckout(Run build, SCM scm, FilePath workspace, TaskListene } } + private boolean shouldSkipLibrariesRepository(final Run build, final List urls) { + final LibrariesAction librariesAction = build.getAction(LibrariesAction.class); + if (librariesAction != null && !librariesAction.getLibraries().isEmpty()) { + for (final LibraryRecord libraryRecord : librariesAction.getLibraries()) { + final String name = libraryRecord.getName(); + + if (libraryMatchesUrls(urls, name, GlobalLibraries.get().getLibraries())) { + return true; + } + + final MultiBranchProject multiBranchProject = (MultiBranchProject) ((WorkflowRun) build).getParent().getParent(); + for (Object property : multiBranchProject.getProperties()) { + if (property instanceof FolderLibraries) { + FolderLibraries folderLibraries = (FolderLibraries) property; + final List libraryConfigurations = folderLibraries.getLibraries(); + if (libraryMatchesUrls(urls, name, libraryConfigurations)) { + return true; + } + } + } + } + } + return false; + } + + private boolean libraryMatchesUrls(final List urls, final String name, final List libraryConfigurations) { + for (final LibraryConfiguration library : libraryConfigurations) { + if (library.getName().equals(name)) { + final LibraryRetriever retriever = library.getRetriever(); + if (retriever instanceof SCMSourceRetriever) { + final SCMSourceRetriever scmSourceRetriever = (SCMSourceRetriever) retriever; + final SCMSource scmSource = scmSourceRetriever.getScm(); + if (scmSource instanceof GitSCMSource) { + final GitSCMSource gitSCMSource = (GitSCMSource) scmSource; + final String remote = gitSCMSource.getRemote(); + if (urls.contains(remote)) { + return true; + } + } + } + } + } + return false; + } + private GitChangeSet getChangeSet(GitSCM scm, FilePath workspace, Revision lastBuiltRevision, TaskListener listener) throws IOException, InterruptedException { Git gitBuilder = Git.with(listener, new EnvVars()) .in(workspace); diff --git a/src/test/java/io/jenkins/blueocean/autofavorite/WorkFlowLibrariesTest.java b/src/test/java/io/jenkins/blueocean/autofavorite/WorkFlowLibrariesTest.java new file mode 100644 index 0000000..5e0c5f8 --- /dev/null +++ b/src/test/java/io/jenkins/blueocean/autofavorite/WorkFlowLibrariesTest.java @@ -0,0 +1,202 @@ +package io.jenkins.blueocean.autofavorite; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import hudson.model.Result; +import hudson.model.User; +import hudson.plugins.favorite.Favorites; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import hudson.plugins.git.GitSCM; +import jenkins.branch.BranchSource; +import jenkins.branch.MultiBranchProject.BranchIndexing; +import jenkins.plugins.git.GitSCMSource; +import jenkins.plugins.git.traits.BranchDiscoveryTrait; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.libs.FolderLibraries; +import org.jenkinsci.plugins.workflow.libs.GlobalLibraries; +import org.jenkinsci.plugins.workflow.libs.LibraryConfiguration; +import org.jenkinsci.plugins.workflow.libs.SCMSourceRetriever; +import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.JenkinsRule; + +public class WorkFlowLibrariesTest { + + @Rule + public TemporaryFolder folder= new TemporaryFolder(); + + @Rule + public JenkinsRule j = new JenkinsRule(); + + private Path jobRepository; + private Path libraryRepository; + + @Before + public void setUp() throws Exception { + + GitSCM.ALLOW_LOCAL_CHECKOUT = true; + + jobRepository = folder.newFolder().toPath(); + libraryRepository = folder.newFolder().toPath(); + + createJobRepository(jobRepository); + createLibraryRepository(libraryRepository); + + } + + @Test + public void testGlobal() throws Exception { + + setupGlobalLibraries(libraryRepository); + + assertNull(User.getById("name1", false)); + assertNull(User.getById("name2", false)); + User.getById("name1", true); + User.getById("name2", true); + + final WorkflowMultiBranchProject project = createWorkflowMultiBranchProject(jobRepository); + project.addProperty(createFolderLibraries(libraryRepository)); + final WorkflowJob job = runPipeline(project); + + final User name1 = User.getById("name1", false); + assertNotNull(name1); + assertTrue(Favorites.isFavorite(name1, job)); + + final User name2 = User.getById("name2", false); + assertNotNull(name2); + assertFalse(Favorites.isFavorite(name2, job)); + + } + + + @Test + public void testFolder() throws Exception { + + assertNull(User.getById("name1", false)); + assertNull(User.getById("name2", false)); + User.getById("name1", true); + User.getById("name2", true); + + final WorkflowMultiBranchProject project = createWorkflowMultiBranchProject(jobRepository); + project.addProperty(createFolderLibraries(libraryRepository)); + final WorkflowJob job = runPipeline(project); + + final User name1 = User.getById("name1", false); + assertNotNull(name1); + assertTrue(Favorites.isFavorite(name1, job)); + + final User name2 = User.getById("name2", false); + assertNotNull(name2); + assertFalse(Favorites.isFavorite(name2, job)); + + } + + private WorkflowMultiBranchProject createWorkflowMultiBranchProject(final Path jobRepository) throws java.io.IOException, InterruptedException { + WorkflowMultiBranchProject mbp = j.createProject(WorkflowMultiBranchProject.class, "WorkflowMultiBranchProject"); + GitSCMSource gitSCMSource = new GitSCMSource(jobRepository.toString()); + gitSCMSource.setCredentialsId(""); + gitSCMSource.getTraits().add(new BranchDiscoveryTrait()); + mbp.getSourcesList().add(new BranchSource(gitSCMSource)); + + return mbp; + } + + private WorkflowJob runPipeline(final WorkflowMultiBranchProject mbp) throws java.io.IOException, InterruptedException { + BranchIndexing indexing = mbp.getIndexing(); + indexing.run(); + + while (indexing.getResult() == Result.NOT_BUILT) { + Thread.sleep(TimeUnit.SECONDS.toMillis(1)); + } + + assertEquals(Result.SUCCESS, indexing.getResult()); + + WorkflowJob job = mbp.getItem("master"); + while (job.getBuilds().isEmpty()) { + Thread.sleep(5); + } + + WorkflowRun run = job.getBuildByNumber(1); + assertNotNull(run); + + while (run.getResult() == null) { + /* poll faster as long as we still need to removeBuildData */ + Thread.sleep(TimeUnit.SECONDS.toMillis(1)); + } + + return job; + } + + private void setupGlobalLibraries(final Path libraryRepository) { + List libraries = new ArrayList<>(); + GitSCMSource scmSource = new GitSCMSource(libraryRepository.toString()); + SCMSourceRetriever scmSourceRetriever = new SCMSourceRetriever(scmSource); + final LibraryConfiguration libraryConfiguration = new LibraryConfiguration( + "shared-library", + scmSourceRetriever); + libraryConfiguration.setDefaultVersion("master"); + libraries.add(libraryConfiguration); + GlobalLibraries.get().setLibraries(libraries); + } + + private FolderLibraries createFolderLibraries(final Path libraryRepository) { + List libraries = new ArrayList<>(); + GitSCMSource scmSource = new GitSCMSource(libraryRepository.toString()); + SCMSourceRetriever scmSourceRetriever = new SCMSourceRetriever(scmSource); + final LibraryConfiguration libraryConfiguration = new LibraryConfiguration( + "shared-library", + scmSourceRetriever); + libraryConfiguration.setDefaultVersion("master"); + libraries.add(libraryConfiguration); + return new FolderLibraries(libraries); + } + + private void createJobRepository(final Path path) throws IOException, GitAPIException { + Files.copy(getClass().getResourceAsStream("/job-repository/Jenkinsfile"), + path.resolve("Jenkinsfile")); + + try (Git git = Git.init().setDirectory(path.toFile()).call()) { + + git.add().addFilepattern("Jenkinsfile").call(); + + git.commit() + .setMessage("some commit message 1") + .setAuthor("name1", "name1@example.com") + .call(); + } + } + + private void createLibraryRepository(final Path path) throws IOException, GitAPIException { + final Path vars = path.resolve("vars"); + vars.toFile().mkdir(); + + Files.copy(getClass().getResourceAsStream("/library-repository/vars/helloWorld.groovy"), + vars.resolve("helloWorld.groovy")); + + try (Git git = Git.init().setDirectory(path.toFile()).call()) { + + git.add().addFilepattern("vars/helloWorld.groovy").call(); + + git.commit() + .setMessage("some commit message 2") + .setAuthor("name2", "name2@example.com") + .call(); + } + } +} diff --git a/src/test/resources/job-repository/Jenkinsfile b/src/test/resources/job-repository/Jenkinsfile new file mode 100644 index 0000000..eaee86d --- /dev/null +++ b/src/test/resources/job-repository/Jenkinsfile @@ -0,0 +1,18 @@ +@Library("shared-library") _ + +pipeline { + agent any + + stages { + stage('Hello') { + steps { + echo 'Hello World' + } + } + stage('Hello from library') { + steps { + helloWorld() + } + } + } +} diff --git a/src/test/resources/library-repository/vars/helloWorld.groovy b/src/test/resources/library-repository/vars/helloWorld.groovy new file mode 100644 index 0000000..2fdb8e3 --- /dev/null +++ b/src/test/resources/library-repository/vars/helloWorld.groovy @@ -0,0 +1,3 @@ +def call(Map config = [:]) { + sh "echo Hello ${config.name}. Today is ${config.dayOfWeek}." +}