diff --git a/src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryAdder.java b/src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryAdder.java index 4d281002..ab20dc9a 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryAdder.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryAdder.java @@ -128,7 +128,7 @@ listener.getLogger().println("Only using first definition of library " + name); continue; } - String version = cfg.defaultedVersion(libraryVersions.remove(name)); + String version = cfg.defaultedVersion(libraryVersions.remove(name), build, listener); Boolean changelog = cfg.defaultedChangelogs(libraryChangelogs.remove(name)); String source = kind.getClass().getName(); if (cfg instanceof LibraryResolver.ResolvedLibraryConfiguration) { diff --git a/src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration.java b/src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration.java index 5549c6fe..5fc2ffd5 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration.java @@ -25,6 +25,7 @@ package org.jenkinsci.plugins.workflow.libs; import hudson.AbortException; +import hudson.EnvVars; import hudson.Extension; import hudson.ExtensionList; import hudson.Util; @@ -32,8 +33,19 @@ import hudson.model.Descriptor; import hudson.model.DescriptorVisibilityFilter; import hudson.model.Item; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.scm.SCM; import hudson.util.FormValidation; import jenkins.model.Jenkins; +import jenkins.scm.api.SCMFileSystem; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMRevision; +import org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition; +import org.jenkinsci.plugins.workflow.flow.FlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.multibranch.BranchJobProperty; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.AncestorInPath; @@ -46,6 +58,10 @@ import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; + +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.util.List; import java.util.Collection; /** @@ -58,6 +74,15 @@ public class LibraryConfiguration extends AbstractDescribableImpl run, @NonNull TaskListener listener, PrintStream logger) { + if (!("hudson.plugins.git.GitSCM".equals(scm.getClass().getName()))) + return null; + + String runVersion = null; + + // Avoid importing GitSCM and so requiring that + // it is always installed even if not used by + // particular Jenkins deployment (using e.g. + // SVN, Gerritt, etc.). Our aim is to query this: + // runVersion = scm0.getBranches().first().getExpandedName(run.getEnvironment(listener)); + // https://mkyong.com/java/how-to-use-reflection-to-call-java-method-at-runtime/ + Class noparams[] = {}; + Class[] paramEnvVars = new Class[1]; + paramEnvVars[0] = EnvVars.class; + + // https://javadoc.jenkins.io/plugin/git/hudson/plugins/git/GitSCM.html#getBranches() => + // https://javadoc.jenkins.io/plugin/git/hudson/plugins/git/BranchSpec.html#toString() + Method methodGetBranches = null; + try { + methodGetBranches = scm.getClass().getDeclaredMethod("getBranches", noparams); + } catch (Exception x) { + // NoSuchMethodException | SecurityException | NullPointerException + methodGetBranches = null; + } + if (methodGetBranches != null) { + Object branchList = null; + try { + branchList = methodGetBranches.invoke(scm); + } catch (Exception x) { + // InvocationTargetException | IllegalAccessException + branchList = null; + } + if (branchList != null && branchList instanceof List) { + Object branch0 = ((List) branchList).get(0); + if (branch0 != null && "hudson.plugins.git.BranchSpec".equals(branch0.getClass().getName())) { + Method methodGetExpandedName = null; + try { + methodGetExpandedName = branch0.getClass().getDeclaredMethod("getExpandedName", paramEnvVars); + } catch (Exception x) { + methodGetExpandedName = null; + } + if (methodGetExpandedName != null) { + // Handle possible shell-templated branch specs: + Object expandedBranchName = null; + try { + expandedBranchName = methodGetExpandedName.invoke(branch0, run.getEnvironment(listener)); + } catch (Exception x) { + // IllegalAccessException | IOException + expandedBranchName = null; + } + if (expandedBranchName != null) { + runVersion = expandedBranchName.toString(); + } + } else { + if (logger != null) { + logger.println("defaultedVersion(): " + + "did not find method BranchSpec.getExpandedName()"); + } + } + if (runVersion == null || "".equals(runVersion)) { + runVersion = branch0.toString(); + } + } else { + // unknown branchspec class, make no blind guesses + if (logger != null) { + logger.println("defaultedVersion(): " + + "list of branches did not return a " + + "BranchSpec class instance, but " + + (branch0 == null ? "null" : + branch0.getClass().getName())); + } + } + } else { + if (logger != null) { + logger.println("defaultedVersion(): " + + "getBranches() did not return a " + + "list of branches: " + + (branchList == null ? "null" : + branchList.getClass().getName())); + } + } + } else { + // not really the GitSCM we know? + if (logger != null) { + logger.println("defaultedVersion(): " + + "did not find method GitSCM.getBranches()"); + } + } + + // Still alive? Chop off leading '*/' + // (if any) from single-branch MBP and + // plain "Pipeline" job definitions. + if (runVersion != null) { + runVersion = runVersion.replaceFirst("^\\*/", ""); + if (logger != null) { + logger.println("defaultedVersion(): " + + "Discovered runVersion '" + runVersion + + "' in GitSCM source of the pipeline"); + } + } + + return runVersion; + } + + private String extractDefaultedVersionSCMFS(@NonNull SCM scm, @NonNull Run run, @NonNull TaskListener listener, PrintStream logger) { + String runVersion = null; + Item runParent = run.getParent(); // never returns null + + SCMFileSystem fs; + try { + fs = SCMFileSystem.of(runParent, scm); + if (fs == null && logger != null) { + logger.println("defaultedVersion(): " + + "got no SCMFileSystem: " + + "method of() returned null"); + } + } catch (Exception x) { + fs = null; + if (logger != null) { + logger.println("defaultedVersion(): " + + "failed to get SCMFileSystem: " + + x.getMessage()); + } + } + if (fs == null) + return null; + + SCMRevision rev = fs.getRevision(); + if (rev == null) { + if (logger != null) { + logger.println("defaultedVersion(): " + + "got no SCMRevision from SCMFileSystem"); + } + return null; + } + + SCMHead head = rev.getHead(); // never returns null + if (logger != null) { + logger.println("defaultedVersion(): " + + "got SCMHead of SCMRevision from SCMFileSystem: " + + "name='" + head.getName() + "' " + + "toString='" + head.toString() + "'"); + } + + // TODO: Never saw this succeed getting an SCMRevision, + // so currently not blindly assigning runVersion: + //runVersion = head.getName(); + + return runVersion; + } + + private String extractDefaultedVersionSCM(@NonNull SCM scm, @NonNull Run run, @NonNull TaskListener listener, PrintStream logger) { + String runVersion = null; + + if (logger != null) { + logger.println("defaultedVersion(): " + + "inspecting first listed SCM: " + + scm.toString()); + } + + // TODO: If this hack gets traction, try available methods + // until a non-null result. + // Ideally SCM API itself would have all classes return this + // value (or null if branch concept is not supported there): + runVersion = extractDefaultedVersionSCMFS(scm, run, listener, logger); + if (runVersion == null) { + runVersion = extractDefaultedVersionGitSCM(scm, run, listener, logger); + } + + if (runVersion == null) { + // got SVN, Gerritt or some other SCM - + // add handling when needed and known how + // or rely on BRANCH_NAME (if set) below... + if (logger != null) { + logger.println("defaultedVersion(): " + + "the first listed SCM was not of currently " + + "supported class with recognized branch support: " + + scm.getClass().getName()); + } + } + + return runVersion; + } + + private String defaultedVersionSCM(@NonNull Run run, @NonNull TaskListener listener, PrintStream logger) { + // Ask for SCM source of the pipeline (if any), + // as the most authoritative source of the branch + // name we want. If we get an SCM class we can + // query deeper (supports branch concept), then + // extract the branch name of the script source. + SCM scm0 = null; + String runVersion = null; + Item runParent = run.getParent(); + + if (runParent != null && runParent instanceof WorkflowJob) { + // This covers both "Multibranch Pipeline" + // and "Pipeline script from SCM" jobs; + // it also covers "inline" pipeline scripts + // but should return an empty Collection of + // SCMs since there is no SCM attached to + // the "static source". + // TODO: If there are "pre-loaded libraries" + // in a Jenkins deployment, can they interfere? + if (logger != null) { + logger.println("defaultedVersion(): inspecting WorkflowJob for a FlowDefinition"); + } + FlowDefinition fd = ((WorkflowJob)runParent).getDefinition(); + if (fd != null) { + if (fd instanceof CpsScmFlowDefinition) { + CpsScmFlowDefinition csfd = (CpsScmFlowDefinition)fd; + if (logger != null) { + logger.println("defaultedVersion(): inspecting CpsScmFlowDefinition '" + + csfd.getClass().getName() + + "' for an SCM it might use (with" + + (csfd.isLightweight() ? "" : "out") + + " lightweight checkout)"); + } + scm0 = csfd.getScm(); + + if (scm0 == null) { + if (logger != null) { + logger.println("defaultedVersion(): CpsScmFlowDefinition '" + + csfd.getClass().getName() + + "' is not associated with an SCM"); + } + } else if (!isDefaultedVersionSCMSupported(scm0)) { + if (logger != null) { + logger.println("defaultedVersion(): CpsScmFlowDefinition '" + + csfd.getClass().getName() + + "' is associated with an SCM class we can not query for branches: " + + scm0.toString()); + } + scm0 = null; + } + } + + if (scm0 == null) { + Collection fdscms = (Collection) fd.getSCMs(); + if (fdscms.isEmpty()) { + if (logger != null) { + logger.println("defaultedVersion(): generic FlowDefinition '" + + fd.getClass().getName() + + "' is not associated with any SCMs"); + } + } else { + if (logger != null) { + logger.println("defaultedVersion(): inspecting generic FlowDefinition '" + + fd.getClass().getName() + + "' for SCMs it might use"); + } + for (SCM scmN : fdscms) { + if (logger != null) { + logger.println("defaultedVersion(): inspecting SCM '" + + scmN.getClass().getName() + + "': " + scmN.toString()); + } + if (isDefaultedVersionSCMSupported(scmN)) { + // The best we can do here is accept + // the first seen SCM (with branch + // support which we know how to query). + scm0 = scmN; + break; + } + } + } + } + } + + // WARNING: the WorkflowJob.getSCMs() does not return SCMs + // associated with the current build configuration, but + // rather those that were associated with previous runs + // for different branches (with lastSuccessfulBuild on + // top), so we should not fall back looking at those! + // And we inspect (MBP) BranchJobProperty early in the + // main defaultedVersion() method. + } // if (runParent != null && runParent instanceof WorkflowJob) + + // If no hit with runParent, look into the run itself: + if (scm0 == null && run instanceof WorkflowRun) { + // This covers both "Multibranch Pipeline" + // and "Pipeline script from SCM" jobs; + // it also covers "inline" pipeline scripts + // but throws a hudson.AbortException since + // there is no SCM attached. + + // NOTE: the list of SCMs used by the run does not + // seem trustworthy: if an "inline" pipeline script + // (not from SCM) is used, there is no "relevant" + // branch name to request; however during work on + // JENKINS-69731 I was concerned that the list of + // SCMs might get populated as @Library lines are + // processed and some SCM sources get checked out. + // Experimentally it seems this is not happening, + // and whole pipeline script source is pre-processed + // first (calling this method for many @Library + // lines), and checkouts happen later (populating + // list of SCMs). This is specifically tested by + // checkDefaultVersion_inline_BRANCH_NAME() case. + + if (logger != null) { + logger.println("defaultedVersion(): inspecting WorkflowRun"); + } + try { + WorkflowRun wfRun = (WorkflowRun) run; + Collection wfrscms = (Collection) wfRun.getSCMs(); + if (wfrscms.isEmpty()) { + if (logger != null) { + logger.println("defaultedVersion(): WorkflowRun '" + + wfRun.getClass().getName() + + "' is not associated with any SCMs"); + } + } else { + // Somewhat a guess in the dark... + scm0 = wfRun.getSCMs().get(0); + } + } catch (Exception x) { + if (logger != null) { + logger.println("defaultedVersion(): " + + "Did not get first listed SCM: " + + x.getMessage()); + } + } + } // if (scm0 == null && run instanceof WorkflowRun) + + // Got some hit? Drill deeper! + if (scm0 != null) { + runVersion = extractDefaultedVersionSCM(scm0, run, listener, logger); + } + + return runVersion; + } + @NonNull String defaultedVersion(@CheckForNull String version) throws AbortException { + return defaultedVersion(version, null, null); + } + + @NonNull String defaultedVersion(@CheckForNull String version, Run run, TaskListener listener) throws AbortException { + PrintStream logger = null; + if (traceDefaultedVersion && listener != null) { + logger = listener.getLogger(); + } + if (logger != null) { + logger.println("defaultedVersion(): Resolving '" + version + "'"); + } + if (version == null) { if (defaultVersion == null) { throw new AbortException("No version specified for library " + name); } else { return defaultVersion; } - } else if (allowVersionOverride) { + } else if (allowVersionOverride + && !("${BRANCH_NAME}".equals(version)) + && !(version.startsWith("${env.") && version.endsWith("}")) + ) { return version; + } else if (allowVersionEnvvar && version.startsWith("${env.") && version.endsWith("}")) { + String runVersion = null; + String envVersion = version.substring(6, version.length() - 1); + Item runParent = null; + if (run != null && listener != null) { + try { + runParent = run.getParent(); + } catch (Exception x) { + // no-op, keep null + } + } + + if (logger != null) { + logger.println("defaultedVersion(): " + + "Resolving envvar '" + envVersion + "'; " + + (runParent == null ? "without" : "have") + + " a runParent object"); + } + + // without a runParent we can't validateVersion() anyway + if (runParent != null) { + try { + runVersion = run.getEnvironment(listener).get(envVersion, null); + if (logger != null) { + if (runVersion != null) { + logger.println("defaultedVersion(): Resolved envvar " + envVersion + "='" + runVersion + "'"); + } else { + logger.println("defaultedVersion(): Did not resolve envvar " + envVersion + ": not in env"); + } + } + } catch (Exception x) { + runVersion = null; + if (logger != null) { + logger.println("defaultedVersion(): Did not resolve envvar " + envVersion + ": " + x.getMessage()); + } + } + } else { + if (logger != null) { + logger.println("defaultedVersion(): Trying to default: " + + "without a runParent we can't validateVersion() anyway"); + } + } + + if (runParent == null || runVersion == null || "".equals(runVersion)) { + // Current build does not know the requested envvar, + // or it's an empty string, or this request has null + // args for run/listener needed for validateVersion() + // below, or some other problem occurred. + // Fall back if we can: + if (logger != null) { + logger.println("defaultedVersion(): Trying to default: " + + "runVersion is " + + (runVersion == null ? "null" : + ("".equals(runVersion) ? "empty" : runVersion))); + } + if (defaultVersion == null) { + throw new AbortException("No version specified for library " + name); + } else { + return defaultVersion; + } + } + + // Check if runVersion is resolvable by LibraryRetriever + // implementation (SCM, HTTP, etc.); fall back if not: + if (retriever != null) { + if (logger != null) { + logger.println("defaultedVersion(): Trying to validate runVersion: " + runVersion); + } + + FormValidation fv = retriever.validateVersion(name, runVersion, runParent); + + if (fv != null && fv.kind == FormValidation.Kind.OK) { + return runVersion; + } + } + + // No retriever, or its validateVersion() did not confirm + // usability of BRANCH_NAME string value as the version... + if (logger != null) { + logger.println("defaultedVersion(): Trying to default: " + + "could not resolve runVersion which is " + + ("".equals(runVersion) ? "empty" : runVersion)); + } + if (defaultVersion == null) { + throw new AbortException(envVersion + " version " + runVersion + + " was not found, and no default version specified, for library " + name); + } else { + return defaultVersion; + } + } else if (allowBRANCH_NAME && "${BRANCH_NAME}".equals(version)) { + String runVersion = null; + Item runParent = null; + if (run != null && listener != null) { + try { + runParent = run.getParent(); + } catch (Exception x) { + // no-op, keep null + } + } + + if (logger != null) { + logger.println("defaultedVersion(): Resolving BRANCH_NAME; " + + (runParent == null ? "without" : "have") + + " a runParent object"); + } + + // without a runParent we can't validateVersion() anyway + if (runParent != null) { + // For a first shot, ask if the job says anything? + // If not, we have more complex SCM-dependent queries + // for WorkflowJob to try below... + if (runParent instanceof WorkflowJob) { + if (logger != null) { + logger.println("defaultedVersion(): inspecting WorkflowJob for BranchJobProperty"); + } + BranchJobProperty property = ((WorkflowJob)runParent).getProperty(BranchJobProperty.class); + if (property != null) { + try { + runVersion = property.getBranch().getName(); + if (logger != null) { + logger.println("defaultedVersion(): WorkflowJob BranchJobProperty refers to " + runVersion); + } + } catch (Exception x) { + runVersion = null; + if (logger != null) { + logger.println("defaultedVersion(): WorkflowJob BranchJobProperty " + + "does not refer to a runVersion: " + x.getMessage()); + } + } + } else { + if (logger != null) { + logger.println("defaultedVersion(): WorkflowJob is not associated with a BranchJobProperty"); + } + } + } + + // Next, check if envvar BRANCH_NAME is defined? + // Trust the plugins and situations where it is set. + if (runVersion == null) { + try { + runVersion = run.getEnvironment(listener).get("BRANCH_NAME", null); + if (logger != null) { + if (runVersion != null) { + logger.println("defaultedVersion(): Resolved envvar BRANCH_NAME='" + runVersion + "'"); + } else { + logger.println("defaultedVersion(): Did not resolve envvar BRANCH_NAME: not in env"); + } + } + } catch (Exception x) { + runVersion = null; + if (logger != null) { + logger.println("defaultedVersion(): Did not resolve envvar BRANCH_NAME: " + x.getMessage()); + } + } + } + + if (runVersion == null) { + // Probably not in a multibranch pipeline workflow + // type of job? + // Ask for SCM source of the pipeline (if any), + // as the most authoritative source of the branch + // name we want, if they know something: + runVersion = defaultedVersionSCM(run, listener, logger); + } + + // Note: if runVersion remains null (unresolved - + // with other job types and/or SCMs maybe setting + // other envvar names), we might drill into names + // like GIT_BRANCH, GERRIT_BRANCH etc. but it would + // not be too scalable. So gotta stop somewhere. + // We would however look into (MBP-defined for PRs) + // CHANGE_BRANCH and CHANGE_TARGET as other fallbacks + // below. + } else { + if (logger != null) { + logger.println("defaultedVersion(): Trying to default: " + + "without a runParent we can't validateVersion() anyway"); + } + } + + if (runParent == null || runVersion == null || "".equals(runVersion)) { + // Current build does not know a BRANCH_NAME envvar, + // or it's an empty string, or this request has null + // args for run/listener needed for validateVersion() + // below, or some other problem occurred. + // Fall back if we can: + if (logger != null) { + logger.println("defaultedVersion(): Trying to default: " + + "runVersion is " + + (runVersion == null ? "null" : + ("".equals(runVersion) ? "empty" : runVersion))); + } + if (defaultVersion == null) { + throw new AbortException("No version specified for library " + name); + } else { + return defaultVersion; + } + } + + // Check if runVersion is resolvable by LibraryRetriever + // implementation (SCM, HTTP, etc.); fall back if not: + if (retriever != null) { + if (logger != null) { + logger.println("defaultedVersion(): Trying to validate runVersion: " + runVersion); + } + + FormValidation fv = retriever.validateVersion(name, runVersion, runParent); + + if (fv != null && fv.kind == FormValidation.Kind.OK) { + return runVersion; + } + + if (runVersion.startsWith("PR-") && allowBRANCH_NAME_PR) { + // MultiBranch Pipeline support for pull requests + // sets BRANCH_NAME="PR-123" and keeps source + // and target branch names in CHANGE_BRANCH and + // CHANGE_TARGET respectively. + + // First check for possible PR-source branch of + // pipeline coordinated with a PR of trusted + // shared library (if branch exists in library, + // after repo protections involved, it is already + // somewhat trustworthy): + try { + runVersion = run.getEnvironment(listener).get("CHANGE_BRANCH", null); + } catch (Exception x) { + runVersion = null; + } + if (runVersion != null && !("".equals(runVersion))) { + if (logger != null) { + logger.println("defaultedVersion(): Trying to validate CHANGE_BRANCH: " + runVersion); + } + fv = retriever.validateVersion(name, runVersion, runParent); + + if (fv != null && fv.kind == FormValidation.Kind.OK) { + return runVersion; + } + } + + // Next check for possible PR-target branch of + // pipeline coordinated with existing version of + // trusted shared library: + try { + runVersion = run.getEnvironment(listener).get("CHANGE_TARGET", null); + } catch (Exception x) { + runVersion = null; + } + if (runVersion != null && !("".equals(runVersion))) { + if (logger != null) { + logger.println("defaultedVersion(): Trying to validate CHANGE_TARGET: " + runVersion); + } + fv = retriever.validateVersion(name, runVersion, runParent); + + if (fv != null && fv.kind == FormValidation.Kind.OK) { + return runVersion; + } + } + } // else not a PR or not allowBRANCH_NAME_PR + } + + // No retriever, or its validateVersion() did not confirm + // usability of BRANCH_NAME string value as the version... + if (logger != null) { + logger.println("defaultedVersion(): Trying to default: " + + "could not resolve runVersion which is " + + (runVersion == null ? "null" : + ("".equals(runVersion) ? "empty" : runVersion))); + } + if (defaultVersion == null) { + throw new AbortException("BRANCH_NAME version " + runVersion + + " was not found, and no default version specified, for library " + name); + } else { + return defaultVersion; + } } else { throw new AbortException("Version override not permitted for library " + name); } @@ -172,16 +886,61 @@ public FormValidation doCheckName(@QueryParameter String name) { } @RequirePOST - public FormValidation doCheckDefaultVersion(@AncestorInPath Item context, @QueryParameter String defaultVersion, @QueryParameter boolean implicit, @QueryParameter boolean allowVersionOverride, @QueryParameter String name) { + public FormValidation doCheckDefaultVersion(@AncestorInPath Item context, @QueryParameter String defaultVersion, @QueryParameter boolean implicit, @QueryParameter boolean allowVersionOverride, @QueryParameter boolean allowVersionEnvvar, @QueryParameter boolean allowBRANCH_NAME, @QueryParameter boolean allowBRANCH_NAME_PR, @QueryParameter String name) { if (defaultVersion.isEmpty()) { if (implicit) { return FormValidation.error("If you load a library implicitly, you must specify a default version."); } + if (allowBRANCH_NAME) { + return FormValidation.error("If you allow use of literal '${BRANCH_NAME}' for overriding a default version, you must define that version as fallback."); + } + if (allowVersionEnvvar) { + return FormValidation.error("If you allow use of literal '${env.VARNAME}' pattern for overriding a default version, you must define that version as fallback."); + } if (!allowVersionOverride) { return FormValidation.error("If you deny overriding a default version, you must define that version."); } + if (allowBRANCH_NAME_PR) { + return FormValidation.warning("This setting has no effect when you do not allow use of literal '${BRANCH_NAME}' for overriding a default version"); + } return FormValidation.ok(); } else { + if ("${BRANCH_NAME}".equals(defaultVersion)) { + if (!allowBRANCH_NAME) { + return FormValidation.error("Use of literal '${BRANCH_NAME}' not allowed in this configuration."); + } + + // The context is not a particular Run (might be a Job) + // so we can't detect which BRANCH_NAME is relevant: + String msg = "Cannot validate default version: " + + "literal '${BRANCH_NAME}' is reserved " + + "for pipeline files from SCM"; + if (implicit) { + // Someone might want to bind feature branches of + // job definitions and implicit libs by default?.. + return FormValidation.warning(msg); + } else { + return FormValidation.error(msg); + } + } + + if (defaultVersion.startsWith("${env.") && defaultVersion.endsWith("}")) { + if (!allowVersionEnvvar) { + return FormValidation.error("Use of literal '${env.VARNAME}' pattern not allowed in this configuration."); + } + + String msg = "Cannot set default version to " + + "literal '${env.VARNAME}' pattern"; + // TOTHINK: Should this be an error? + // What if users intentionally want the (implicit?) + // library version to depend on envvars without a + // fallback? and what about git clones with no + // specified "version" to use preference of GitHub + // or similar platform's project/org settings as + // the final sensible fallback? + return FormValidation.error(msg); + } + for (LibraryResolver resolver : ExtensionList.lookup(LibraryResolver.class)) { for (LibraryConfiguration config : resolver.fromConfiguration(Stapler.getCurrentRequest2())) { if (config.getName().equals(name)) { diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration/config.jelly b/src/main/resources/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration/config.jelly index 336a96e5..9520bfc6 100644 --- a/src/main/resources/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration/config.jelly @@ -34,9 +34,21 @@ THE SOFTWARE. - + + + + + + + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration/help-allowBRANCH_NAME.html b/src/main/resources/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration/help-allowBRANCH_NAME.html new file mode 100644 index 00000000..06b4b58b --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration/help-allowBRANCH_NAME.html @@ -0,0 +1,17 @@ +
+ If checked, scripts may select a custom version of the library + by appending literally @${BRANCH_NAME} in the + @Library annotation, to use same SCM branch name + of the library codebase as that of the pipeline being built.
+ If such branch name is not resolvable as an environment variable + or not present in library storage (SCM, release artifacts...), + it would fall back to default version you select here.
+ Keep in mind that while the markup for such variable version + specification is intentionally similar to what you would use + in pipeline Groovy code, for simpler use and maintenance, the + actual strings are expanded by the plugin as it pre-processes + the pipeline script before compilation. Tokens spelled in the + @Library annotation are not Groovy variables! + The values substituted here are not influenced by run-time + interpretation of the pipeline script source text! +
diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration/help-allowBRANCH_NAME_PR.html b/src/main/resources/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration/help-allowBRANCH_NAME_PR.html new file mode 100644 index 00000000..815b3adc --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration/help-allowBRANCH_NAME_PR.html @@ -0,0 +1,10 @@ +
+ If checked, and if scripts are allowed to select a custom version + of the library by appending literally @${BRANCH_NAME} + in the @Library annotation, then for pull request + builds additional fall-back library branches would include the + names used as CHANGE_BRANCH (name of source branch + of pipeline pull request) and CHANGE_TARGET (name + of target branch of pipeline pull request), if such branch names + already exist in the trusted shared library repository. +
diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration/help-allowVersionEnvvar.html b/src/main/resources/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration/help-allowVersionEnvvar.html new file mode 100644 index 00000000..a4c50cbd --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration/help-allowVersionEnvvar.html @@ -0,0 +1,18 @@ +
+ If checked, scripts may select a custom version of the library + by appending literally @${env.VARNAME} pattern in + the @Library annotation, to use current value of + chosen environment variable named VARNAME if it + is defined in job properties or on the build agent.
+ If such branch name is not resolvable as an environment variable + or not present in library storage (SCM, release artifacts...), + it would fall back to default version you select here.
+ Keep in mind that while the markup for such variable version + specification is intentionally similar to what you would use + in pipeline Groovy code, for simpler use and maintenance, the + actual strings are expanded by the plugin as it pre-processes + the pipeline script before compilation. Tokens spelled in the + @Library annotation are not Groovy variables! + The values substituted here are not influenced by run-time + interpretation of the pipeline script source text! +
diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration/help-traceDefaultedVersion.html b/src/main/resources/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration/help-traceDefaultedVersion.html new file mode 100644 index 00000000..3a0dc8dd --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration/help-traceDefaultedVersion.html @@ -0,0 +1,5 @@ +
+ If checked, the defaultedVersion method would print + its progress trying to resolve @${BRANCH_NAME} in the + @Library annotation into the build log. +
diff --git a/src/test/java/org/jenkinsci/plugins/workflow/libs/LibraryConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/workflow/libs/LibraryConfigurationTest.java index c91a4627..9ad3e0f7 100644 --- a/src/test/java/org/jenkinsci/plugins/workflow/libs/LibraryConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/workflow/libs/LibraryConfigurationTest.java @@ -26,9 +26,11 @@ import java.util.Collections; +import hudson.AbortException; import hudson.plugins.git.GitSCM; import org.hamcrest.Matchers; import org.junit.Test; +import org.junit.Assert; import static org.junit.Assert.*; import org.junit.Rule; import org.jvnet.hudson.test.Issue; @@ -92,6 +94,109 @@ public class LibraryConfigurationTest { assertNull(cfg.getDefaultVersion()); } + @Issue("JENKINS-69731") + @Test public void nullPresentDefaultedVersion() { + String libraryName = "valid-name"; + String defaultVersion = "master"; + + LibraryConfiguration cfg = new LibraryConfiguration(libraryName, new SCMRetriever(new GitSCM("https://phony.jenkins.io/bar.git"))); + cfg.setDefaultVersion(defaultVersion); + + assertEquals("master", cfg.getDefaultVersion()); + try { + assertEquals("master", cfg.defaultedVersion(null)); + } catch(AbortException ae) { + Assert.fail("LibraryConfiguration.defaultedVersion() threw an AbortException when it was not expected: " + ae.getMessage()); + } + } + + @Issue("JENKINS-69731") + @Test public void nullAbsentDefaultedVersion() { + String libraryName = "valid-name"; + + LibraryConfiguration cfg = new LibraryConfiguration(libraryName, new SCMRetriever(new GitSCM("https://phony.jenkins.io/bar.git"))); + + assertEquals(null, cfg.getDefaultVersion()); + assertThrows(AbortException.class, () -> cfg.defaultedVersion(null)); + } + + @Issue("JENKINS-69731") + @Test public void forbiddenOverrideDefaultedVersion() { + String libraryName = "valid-name"; + + LibraryConfiguration cfg = new LibraryConfiguration(libraryName, new SCMRetriever(new GitSCM("https://phony.jenkins.io/bar.git"))); + cfg.setAllowVersionOverride(false); + + assertEquals(false, cfg.isAllowVersionOverride()); + assertThrows(AbortException.class, () -> cfg.defaultedVersion("branchname")); + } + + @Issue("JENKINS-69731") + @Test public void allowedOverrideDefaultedVersion() { + String libraryName = "valid-name"; + + LibraryConfiguration cfg = new LibraryConfiguration(libraryName, new SCMRetriever(new GitSCM("https://phony.jenkins.io/bar.git"))); + cfg.setAllowVersionOverride(true); + + assertEquals(true, cfg.isAllowVersionOverride()); + try { + assertEquals("branchname", cfg.defaultedVersion("branchname")); + } catch(AbortException ae) { + Assert.fail("LibraryConfiguration.defaultedVersion() threw an AbortException when it was not expected: " + ae.getMessage()); + } + } + + @Issue("JENKINS-69731") + @Test public void notAllowedOverrideDefaultedVersionWhenBRANCH_NAME() { + String libraryName = "valid-name"; + + LibraryConfiguration cfg = new LibraryConfiguration(libraryName, new SCMRetriever(new GitSCM("https://phony.jenkins.io/bar.git"))); + cfg.setAllowVersionOverride(true); + cfg.setAllowBRANCH_NAME(false); + cfg.setTraceDefaultedVersion(true); + + assertEquals(true, cfg.isAllowVersionOverride()); + assertEquals(false, cfg.isAllowBRANCH_NAME()); + assertEquals(true, cfg.isTraceDefaultedVersion()); + assertThrows(AbortException.class, () -> cfg.defaultedVersion("${BRANCH_NAME}")); + /* This SHOULD NOT return a version string that literally remains '${BRANCH_NAME}'! */ + } + + @Issue("JENKINS-69731") + @Test public void allowedBRANCH_NAMEnoRunPresentDefaultedVersion() { + String libraryName = "valid-name"; + String defaultVersion = "master"; + + LibraryConfiguration cfg = new LibraryConfiguration(libraryName, new SCMRetriever(new GitSCM("https://phony.jenkins.io/bar.git"))); + cfg.setDefaultVersion(defaultVersion); + cfg.setAllowBRANCH_NAME(true); + cfg.setTraceDefaultedVersion(true); + + assertEquals(true, cfg.isAllowBRANCH_NAME()); + try { + assertEquals("master", cfg.defaultedVersion("${BRANCH_NAME}", null, null)); + } catch(AbortException ae) { + Assert.fail("LibraryConfiguration.defaultedVersion() threw an AbortException when it was not expected: " + ae.getMessage()); + } + } + + @Issue("JENKINS-69731") + @Test public void allowedBRANCH_NAMEnoRunAbsentDefaultedVersion() { + String libraryName = "valid-name"; + + LibraryConfiguration cfg = new LibraryConfiguration(libraryName, new SCMRetriever(new GitSCM("https://phony.jenkins.io/bar.git"))); + cfg.setAllowBRANCH_NAME(true); + cfg.setTraceDefaultedVersion(true); + + assertEquals(true, cfg.isAllowBRANCH_NAME()); + assertThrows(AbortException.class, () -> cfg.defaultedVersion("${BRANCH_NAME}", null, null)); + } + /* Note: further tests for JENKINS-69731 behaviors with allowBRANCH_NAME + * would rely on having a Run with or without a BRANCH_NAME envvar, and + * a TaskListener, and a (mock?) LibraryRetriever that would confirm or + * deny existence of a requested "version" (e.g. Git branch) of the lib. + * For examples, see e.g. SCMSourceRetrieverTest codebase. + */ } diff --git a/src/test/java/org/jenkinsci/plugins/workflow/libs/SCMSourceRetrieverTest.java b/src/test/java/org/jenkinsci/plugins/workflow/libs/SCMSourceRetrieverTest.java index f8c8110a..2600bada 100644 --- a/src/test/java/org/jenkinsci/plugins/workflow/libs/SCMSourceRetrieverTest.java +++ b/src/test/java/org/jenkinsci/plugins/workflow/libs/SCMSourceRetrieverTest.java @@ -26,19 +26,41 @@ import edu.umd.cs.findbugs.annotations.NonNull; import hudson.AbortException; +import hudson.EnvVars; +import hudson.ExtensionList; import hudson.FilePath; +import hudson.model.Computer; +import hudson.model.EnvironmentContributingAction; +import hudson.model.EnvironmentContributor; import hudson.model.Item; +import hudson.model.Job; +import hudson.model.Node; import hudson.model.Result; +import hudson.model.Run; import hudson.model.TaskListener; +import hudson.plugins.git.ApiTokenPropertyConfiguration; +import hudson.plugins.git.BranchSpec; +import hudson.plugins.git.GitSCM; import hudson.scm.ChangeLogSet; import hudson.scm.SCM; +import hudson.slaves.EnvironmentVariablesNodeProperty; +import hudson.slaves.NodeProperty; +import hudson.slaves.NodePropertyDescriptor; +import hudson.slaves.OfflineCause; import hudson.slaves.WorkspaceList; import java.io.File; import java.io.IOException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.logging.Level; +import hudson.util.DescribableList; +import jenkins.branch.BranchProperty; +import jenkins.branch.BranchSource; +import jenkins.branch.DefaultBranchPropertyStrategy; import jenkins.plugins.git.GitSCMSource; import jenkins.plugins.git.GitSampleRepoRule; import jenkins.scm.api.SCMHead; @@ -48,9 +70,13 @@ import jenkins.scm.api.SCMSource; import jenkins.scm.api.SCMSourceCriteria; import jenkins.scm.api.SCMSourceDescriptor; +import jenkins.scm.impl.SingleSCMSource; +import org.htmlunit.WebResponse; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.multibranch.*; import static hudson.ExtensionList.lookupSingleton; import hudson.plugins.git.extensions.impl.CloneOption; @@ -78,6 +104,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.matchesPattern; import static org.jenkinsci.plugins.workflow.libs.SCMBasedRetriever.PROHIBITED_DOUBLE_DOT; +import static org.junit.Assume.assumeFalse; import org.jvnet.hudson.test.FlagRule; import org.jvnet.hudson.test.LoggerRule; @@ -86,15 +113,181 @@ public class SCMSourceRetrieverTest { @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); @Rule public JenkinsRule r = new JenkinsRule(); @Rule public GitSampleRepoRule sampleRepo = new GitSampleRepoRule(); + @Rule public GitSampleRepoRule sampleRepo2 = new GitSampleRepoRule(); @Rule public FlagRule includeSrcTest = new FlagRule<>(() -> SCMBasedRetriever.INCLUDE_SRC_TEST_IN_LIBRARIES, v -> SCMBasedRetriever.INCLUDE_SRC_TEST_IN_LIBRARIES = v); @Rule public LoggerRule logging = new LoggerRule().record(SCMBasedRetriever.class, Level.FINE); - @Issue("JENKINS-40408") - @Test public void lease() throws Exception { + // Repetitive helpers for test cases dealing with @Issue("JENKINS-69731") and others + private void sampleRepoNotifyCommit(GitSampleRepoRule sampleRepo) throws Exception { + try { + sampleRepo.notifyCommit(r); + } catch(java.lang.NoSuchMethodError ignored) { + // Some versions of git-plugin maybe mix up "WebClient" + // from "JenkinsRule" and from "htmlunit" packages, while + // using the former implicitly. Maybe something else; says: + // java.lang.NoSuchMethodError: 'com.gargoylesoftware.htmlunit.Page + // org.jvnet.hudson.test.JenkinsRule$WebClient.goTo(java.lang.String, java.lang.String)' + // The catch-code below is an adapted copy of + // GitSampleRepoRule.notifyCommit() as of git-plugin-5.0.0: + + // SKIPPED: protected code: sampleRepo.synchronousPolling(r); + String notifyCommitToken = ApiTokenPropertyConfiguration.get().generateApiToken("notifyCommit").getString("value"); + JenkinsRule.WebClient wc = r.createWebClient(); + WebResponse webResponse = + wc.goTo("git/notifyCommit?url=" + sampleRepo.bareUrl() + "&token=" + notifyCommitToken, "text/plain").getWebResponse(); + // SKIPPED logging of the response + r.waitUntilNoActivity(); + } + } + + private void sampleRepo1ContentMaster() throws Exception { + sampleRepo1ContentMaster(null, null); + } + + private void sampleRepo1ContentMaster(String subdir) throws Exception { + sampleRepo1ContentMaster(subdir, null); + } + + private void sampleRepo1ContentMaster(String subdir, String addCustom) throws Exception { + if (subdir != null && !(subdir.endsWith("/"))) subdir += "/"; + if (subdir == null) subdir = ""; sampleRepo.init(); - sampleRepo.write("vars/myecho.groovy", "def call() {echo 'something special'}"); + sampleRepo.write(subdir + "vars/myecho.groovy", "def call() {echo 'something special'}"); + if (addCustom == null) { + sampleRepo.git("add", subdir + "vars"); + } else { + if (addCustom == "") { + if (subdir == "") { + sampleRepo.git("add", "."); + } else { + sampleRepo.git("add", subdir); + } + } else { + sampleRepo.git("add", addCustom); + } + } + sampleRepo.git("commit", "--message=init"); + } + + private void sampleRepo1ContentMasterAddLibraryCommit() throws Exception { + sampleRepo1ContentMasterAddLibraryCommit(null); + } + + private void sampleRepo1ContentMasterAddLibraryCommit(String subdir) throws Exception { + if (subdir != null && !(subdir.endsWith("/"))) subdir += "/"; + if (subdir == null) subdir = ""; + sampleRepo.write("vars/myecho.groovy", "def call() {echo 'something even more special'}"); sampleRepo.git("add", "vars"); + sampleRepo.git("commit", "--message=library_commit"); + } + + private void sampleRepo1ContentMasterFeature() throws Exception { + sampleRepo1ContentMasterFeature(null); + } + + private void sampleRepo1ContentMasterFeature(String subdir) throws Exception { + if (subdir != null && !(subdir.endsWith("/"))) subdir += "/"; + if (subdir == null) subdir = ""; + sampleRepo.init(); + sampleRepo.write(subdir + "vars/myecho.groovy", "def call() {echo 'something special'}"); + sampleRepo.git("add", subdir + "vars"); + sampleRepo.git("commit", "--message=init"); + sampleRepo.git("checkout", "-b", "feature"); + sampleRepo.write(subdir + "vars/myecho.groovy", "def call() {echo 'something very special'}"); + sampleRepo.git("add", subdir + "vars"); + sampleRepo.git("commit", "--message=init"); + } + + private void sampleRepo1ContentMasterFeatureStable() throws Exception { + sampleRepo1ContentMasterFeatureStable(null); + } + + private void sampleRepo1ContentMasterFeatureStable(String subdir) throws Exception { + if (subdir != null && !(subdir.endsWith("/"))) subdir += "/"; + if (subdir == null) subdir = ""; + sampleRepo.init(); + sampleRepo.write(subdir + "vars/myecho.groovy", "def call() {echo 'something special'}"); + sampleRepo.git("add", subdir + "vars"); + sampleRepo.git("commit", "--message=init"); + sampleRepo.git("checkout", "-b", "feature"); + sampleRepo.write(subdir + "vars/myecho.groovy", "def call() {echo 'something very special'}"); + sampleRepo.git("add", subdir + "vars"); sampleRepo.git("commit", "--message=init"); + sampleRepo.git("checkout", "-b", "stable"); + sampleRepo.write(subdir + "vars/myecho.groovy", "def call() {echo 'something reliable'}"); + sampleRepo.git("add", subdir + "vars"); + sampleRepo.git("commit", "--message=init"); + } + + private void sampleRepo2ContentMasterFeature() throws Exception { + sampleRepo2ContentMasterFeature(null); + } + + private void sampleRepo2ContentMasterFeature(String subdir) throws Exception { + if (subdir != null && !(subdir.endsWith("/"))) subdir += "/"; + if (subdir == null) subdir = ""; + sampleRepo2.init(); + sampleRepo2.write(subdir + "vars/myecho2.groovy", "def call() {echo 'something weird'}"); + sampleRepo2.git("add", subdir + "vars"); + sampleRepo2.git("commit", "--message=init"); + sampleRepo2.git("checkout", "-b", "feature"); + sampleRepo2.write(subdir + "vars/myecho2.groovy", "def call() {echo 'something wonderful'}"); + sampleRepo2.git("add", subdir + "vars"); + sampleRepo2.git("commit", "--message=init"); + } + + private void sampleRepo2ContentSameMasterFeatureBogus_BRANCH_NAME() throws Exception { + sampleRepo2ContentSameMasterFeatureBogus_BRANCH_NAME(null, false); + } + + private void sampleRepo2ContentSameMasterFeatureBogus_BRANCH_NAME(Boolean addJenkinsfileStatic) throws Exception { + sampleRepo2ContentSameMasterFeatureBogus_BRANCH_NAME(null, addJenkinsfileStatic); + } + + private void sampleRepo2ContentSameMasterFeatureBogus_BRANCH_NAME(String subdir) throws Exception { + sampleRepo2ContentSameMasterFeatureBogus_BRANCH_NAME(subdir, false); + } + + private void sampleRepo2ContentSameMasterFeatureBogus_BRANCH_NAME(String subdir, Boolean addJenkinsfileStatic) throws Exception { + if (subdir != null && !(subdir.endsWith("/"))) subdir += "/"; + if (subdir == null) subdir = ""; + sampleRepo2.init(); + sampleRepo2.write(subdir + "Jenkinsfile", "@Library('branchylib@${BRANCH_NAME}') import myecho; myecho()"); + if (addJenkinsfileStatic) { + sampleRepo2.write("Jenkinsfile-static", "@Library('branchylib@stable') import myecho; myecho()"); + sampleRepo2.git("add", subdir + "Jenkinsfile*"); + } else { + sampleRepo2.git("add", subdir + "Jenkinsfile"); + } + sampleRepo2.git("commit", "--message=init"); + sampleRepo2.git("branch", "feature"); + sampleRepo2.git("branch", "bogus"); + } + + private void sampleRepo2ContentUniqueMasterFeatureBogus_staticStrings() throws Exception { + sampleRepo2ContentUniqueMasterFeatureBogus_staticStrings(null); + } + + private void sampleRepo2ContentUniqueMasterFeatureBogus_staticStrings(String subdir) throws Exception { + if (subdir != null && !(subdir.endsWith("/"))) subdir += "/"; + if (subdir == null) subdir = ""; + sampleRepo2.init(); + sampleRepo2.write(subdir + "Jenkinsfile", "@Library('branchylib@master') import myecho; myecho()"); + sampleRepo2.git("add", subdir + "Jenkinsfile"); + sampleRepo2.git("commit", "--message=master"); + sampleRepo2.git("checkout", "-b", "feature"); + sampleRepo2.write(subdir + "Jenkinsfile", "@Library('branchylib@feature') import myecho; myecho()"); + sampleRepo2.git("add", subdir + "Jenkinsfile"); + sampleRepo2.git("commit", "--message=feature"); + sampleRepo2.git("checkout", "-b", "bogus"); + sampleRepo2.write(subdir + "Jenkinsfile", "@Library('branchylib@bogus') import myecho; myecho()"); + sampleRepo2.git("add", subdir + "Jenkinsfile"); + sampleRepo2.git("commit", "--message=bogus"); + } + + @Issue("JENKINS-40408") + @Test public void lease() throws Exception { + sampleRepo1ContentMaster(); GlobalLibraries.get().setLibraries(Collections.singletonList( new LibraryConfiguration("echoing", new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true))))); @@ -115,10 +308,7 @@ public class SCMSourceRetrieverTest { @Issue("JENKINS-41497") @Test public void includeChanges() throws Exception { - sampleRepo.init(); - sampleRepo.write("vars/myecho.groovy", "def call() {echo 'something special'}"); - sampleRepo.git("add", "vars"); - sampleRepo.git("commit", "--message=init"); + sampleRepo1ContentMaster(); GlobalLibraries.get().setLibraries(Collections.singletonList( new LibraryConfiguration("include_changes", new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true))))); @@ -129,9 +319,7 @@ public class SCMSourceRetrieverTest { WorkflowRun a = r.buildAndAssertSuccess(p); r.assertLogContains("something special", a); } - sampleRepo.write("vars/myecho.groovy", "def call() {echo 'something even more special'}"); - sampleRepo.git("add", "vars"); - sampleRepo.git("commit", "--message=library_commit"); + sampleRepo1ContentMasterAddLibraryCommit(); try (WorkspaceList.Lease lease = r.jenkins.toComputer().getWorkspaceList().acquire(base)) { WorkflowRun b = r.buildAndAssertSuccess(p); List> changeSets = b.getChangeSets(); @@ -149,10 +337,7 @@ public class SCMSourceRetrieverTest { @Issue("JENKINS-41497") @Test public void dontIncludeChanges() throws Exception { - sampleRepo.init(); - sampleRepo.write("vars/myecho.groovy", "def call() {echo 'something special'}"); - sampleRepo.git("add", "vars"); - sampleRepo.git("commit", "--message=init"); + sampleRepo1ContentMaster(); LibraryConfiguration lc = new LibraryConfiguration("dont_include_changes", new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true))); lc.setIncludeInChangesets(false); GlobalLibraries.get().setLibraries(Collections.singletonList(lc)); @@ -162,9 +347,7 @@ public class SCMSourceRetrieverTest { try (WorkspaceList.Lease lease = r.jenkins.toComputer().getWorkspaceList().acquire(base)) { WorkflowRun a = r.buildAndAssertSuccess(p); } - sampleRepo.write("vars/myecho.groovy", "def call() {echo 'something even more special'}"); - sampleRepo.git("add", "vars"); - sampleRepo.git("commit", "--message=library_commit"); + sampleRepo1ContentMasterAddLibraryCommit(); try (WorkspaceList.Lease lease = r.jenkins.toComputer().getWorkspaceList().acquire(base)) { WorkflowRun b = r.buildAndAssertSuccess(p); List> changeSets = b.getChangeSets(); @@ -175,10 +358,7 @@ public class SCMSourceRetrieverTest { @Issue("JENKINS-38609") @Test public void libraryPath() throws Exception { - sampleRepo.init(); - sampleRepo.write("sub/path/vars/myecho.groovy", "def call() {echo 'something special'}"); - sampleRepo.git("add", "sub"); - sampleRepo.git("commit", "--message=init"); + sampleRepo1ContentMaster("sub/path"); SCMSourceRetriever scm = new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)); LibraryConfiguration lc = new LibraryConfiguration("root_sub_path", scm); lc.setIncludeInChangesets(false); @@ -192,10 +372,7 @@ public class SCMSourceRetrieverTest { @Issue("JENKINS-38609") @Test public void libraryPathSecurity() throws Exception { - sampleRepo.init(); - sampleRepo.write("sub/path/vars/myecho.groovy", "def call() {echo 'something special'}"); - sampleRepo.git("add", "sub"); - sampleRepo.git("commit", "--message=init"); + sampleRepo1ContentMaster("sub/path"); SCMSourceRetriever scm = new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)); LibraryConfiguration lc = new LibraryConfiguration("root_sub_path", scm); lc.setIncludeInChangesets(false); @@ -218,6 +395,1012 @@ public class SCMSourceRetrieverTest { assertThat("foo\\..\\bar", matchesPattern(PROHIBITED_DOUBLE_DOT)); } + @Issue("JENKINS-69731") + @Test public void checkDefaultVersion_inline_staticStrings() throws Exception { + sampleRepo1ContentMasterFeature(); + SCMSourceRetriever scm = new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)); + LibraryConfiguration lc = new LibraryConfiguration("branchylib", scm); + lc.setDefaultVersion("master"); + lc.setIncludeInChangesets(false); + lc.setAllowBRANCH_NAME(true); + lc.setTraceDefaultedVersion(true); + GlobalLibraries.get().setLibraries(Collections.singletonList(lc)); + + // Basename "libname" notation => use specified default branch + WorkflowJob p1 = r.jenkins.createProject(WorkflowJob.class, "p1"); + p1.setDefinition(new CpsFlowDefinition("@Library('branchylib') import myecho; myecho()", true)); + WorkflowRun b1 = r.buildAndAssertSuccess(p1); + r.assertLogContains("Loading library branchylib@master", b1); + r.assertLogContains("something special", b1); + + // Use specified branch + WorkflowJob p2 = r.jenkins.createProject(WorkflowJob.class, "p2"); + p2.setDefinition(new CpsFlowDefinition("@Library('branchylib@master') import myecho; myecho()", true)); + WorkflowRun b2 = r.buildAndAssertSuccess(p2); + r.assertLogContains("Loading library branchylib@master", b2); + r.assertLogContains("something special", b2); + + // Use another specified branch + WorkflowJob p3 = r.jenkins.createProject(WorkflowJob.class, "p3"); + p3.setDefinition(new CpsFlowDefinition("@Library('branchylib@feature') import myecho; myecho()", true)); + WorkflowRun b3 = r.buildAndAssertSuccess(p3); + r.assertLogContains("Loading library branchylib@feature", b3); + r.assertLogContains("something very special", b3); + + // Use a specified but missing branch + WorkflowJob p4 = r.jenkins.createProject(WorkflowJob.class, "p4"); + p4.setDefinition(new CpsFlowDefinition("@Library('branchylib@bogus') import myecho; myecho()", true)); + WorkflowRun b4 = r.buildAndAssertStatus(Result.FAILURE, p4); + r.assertLogContains("ERROR: No version bogus found for library branchylib", b4); + r.assertLogContains("org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:", b4); + r.assertLogContains("WorkflowScript: Loading libraries failed", b4); + } + + @Issue("JENKINS-69731") + @Test public void checkDefaultVersion_inline_BRANCH_NAME() throws Exception { + // Test that @Library('branchylib@${BRANCH_NAME}') + // falls back to default for "Pipeline script" which + // is not "from SCM", even when we try to confuse it + // by having some checkouts (and so list of SCMs). + + // Do not let caller-provided BRANCH_NAME interfere here + assumeFalse("SKIP by pre-test assumption: " + + "An externally provided BRANCH_NAME envvar interferes with tested logic", + System.getenv("BRANCH_NAME") != null); + + sampleRepo1ContentMasterFeature(); + SCMSourceRetriever scm = new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)); + LibraryConfiguration lc = new LibraryConfiguration("branchylib", scm); + lc.setDefaultVersion("master"); + lc.setIncludeInChangesets(false); + lc.setAllowVersionOverride(true); + lc.setAllowBRANCH_NAME(true); + lc.setTraceDefaultedVersion(true); + + sampleRepo2ContentMasterFeature(); + SCMSourceRetriever scm2 = new SCMSourceRetriever(new GitSCMSource(null, sampleRepo2.toString(), "", "*", "", true)); + LibraryConfiguration lc2 = new LibraryConfiguration("branchylib2", scm2); + lc2.setDefaultVersion("master"); + lc2.setIncludeInChangesets(false); + lc2.setAllowVersionOverride(true); + lc2.setAllowBRANCH_NAME(true); + lc2.setTraceDefaultedVersion(true); + + // Configure two libs to make a mess :) + GlobalLibraries.get().setLibraries(Arrays.asList(lc, lc2)); + + // Branch context for job not set - fall back to default + WorkflowJob p0 = r.jenkins.createProject(WorkflowJob.class, "p0"); + p0.setDefinition(new CpsFlowDefinition("@Library('branchylib@${BRANCH_NAME}') import myecho; myecho()", true)); + WorkflowRun b0 = r.buildAndAssertSuccess(p0); + r.assertLogContains("Loading library branchylib@master", b0); + r.assertLogContains("something special", b0); + + // Branch context for second lib might be confused as "feature" + // because the first loaded lib would become part of SCMs list + // for this build, and there are no other SCMs in the list (an + // inline pipeline). In fact the second lib should fall back to + // "master" because the pipeline script is not from Git so there + // is no "BRANCH_NAME" of its own. + WorkflowJob p1 = r.jenkins.createProject(WorkflowJob.class, "p1"); + p1.setDefinition(new CpsFlowDefinition("@Library('branchylib@feature') import myecho; myecho(); @Library('branchylib2@${BRANCH_NAME}') import myecho2; myecho2()", true)); + WorkflowRun b1 = r.buildAndAssertSuccess(p1); + r.assertLogContains("Loading library branchylib@feature", b1); + r.assertLogContains("Loading library branchylib2@master", b1); + r.assertLogContains("something very special", b1); + r.assertLogContains("something weird", b1); + } + + @Issue("JENKINS-69731") + @Test public void checkDefaultVersion_MBP_BRANCH_NAME() throws Exception { + // Create a MultiBranch Pipeline job instantiated from Git + // and check behaviors with BRANCH_NAME="master", + // BRANCH_NAME="feature", and BRANCH_NAME="bogus" + // TODO? BRANCH_NAME="" + // Note: An externally provided BRANCH_NAME envvar + // does not interfere with tested logic, since MBP + // sets the value for launched builds. + + sampleRepo1ContentMasterFeature(); + SCMSourceRetriever scm = new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)); + LibraryConfiguration lc = new LibraryConfiguration("branchylib", scm); + lc.setDefaultVersion("master"); + lc.setIncludeInChangesets(false); + lc.setAllowBRANCH_NAME(true); + lc.setTraceDefaultedVersion(true); + GlobalLibraries.get().setLibraries(Collections.singletonList(lc)); + + // Inspired in part by tests like + // https://github.com/jenkinsci/workflow-multibranch-plugin/blob/master/src/test/java/org/jenkinsci/plugins/workflow/multibranch/NoTriggerBranchPropertyWorkflowTest.java#L132 + sampleRepo2ContentSameMasterFeatureBogus_BRANCH_NAME(); + + WorkflowMultiBranchProject mbp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "mbp"); + BranchSource branchSource = new BranchSource(new GitSCMSource("source-id", sampleRepo2.toString(), "", "*", "", false)); + mbp.getSourcesList().add(branchSource); + // Note: this notification causes discovery of branches, + // definition of MBP "leaf" jobs, and launch of builds, + // so below we just make sure they complete and analyze + // the outcomes. + sampleRepoNotifyCommit(sampleRepo2); + r.waitUntilNoActivity(); + System.out.println("Jobs generated by MBP: " + mbp.getItems().toString()); + + WorkflowJob p1 = mbp.getItem("master"); + WorkflowRun b1 = p1.getLastBuild(); + r.waitForCompletion(b1); + assertFalse(p1.isBuilding()); + r.assertBuildStatusSuccess(b1); + r.assertLogContains("Loading library branchylib@master", b1); + r.assertLogContains("something special", b1); + + WorkflowJob p2 = mbp.getItem("feature"); + WorkflowRun b2 = p2.getLastBuild(); + r.waitForCompletion(b2); + assertFalse(p2.isBuilding()); + r.assertBuildStatusSuccess(b2); + r.assertLogContains("Loading library branchylib@feature", b2); + r.assertLogContains("something very special", b2); + + // library branch "bogus" does not exist => fall back to default (master) + WorkflowJob p3 = mbp.getItem("bogus"); + WorkflowRun b3 = p3.getLastBuild(); + r.waitForCompletion(b3); + assertFalse(p3.isBuilding()); + r.assertBuildStatusSuccess(b3); + r.assertLogContains("Loading library branchylib@master", b3); + r.assertLogContains("something special", b3); + + // TODO: test lc.setAllowBRANCH_NAME_PR(true) for PR builds + } + + @Issue("JENKINS-69731") + @Test public void checkDefaultVersion_MBP_staticStrings() throws Exception { + // Test that lc.setAllowBRANCH_NAME(false) does not + // preclude fixed branch names (they should work), + // like @Library('branchylib@master') + + sampleRepo1ContentMasterFeature(); + SCMSourceRetriever scm = new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)); + LibraryConfiguration lc = new LibraryConfiguration("branchylib", scm); + lc.setDefaultVersion("master"); + lc.setIncludeInChangesets(false); + lc.setAllowVersionOverride(true); + lc.setAllowBRANCH_NAME(false); + lc.setTraceDefaultedVersion(true); + GlobalLibraries.get().setLibraries(Collections.singletonList(lc)); + + // Inspired in part by tests like + // https://github.com/jenkinsci/workflow-multibranch-plugin/blob/master/src/test/java/org/jenkinsci/plugins/workflow/multibranch/NoTriggerBranchPropertyWorkflowTest.java#L132 + sampleRepo2ContentUniqueMasterFeatureBogus_staticStrings(); + WorkflowMultiBranchProject mbp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "mbp"); + BranchSource branchSource = new BranchSource(new GitSCMSource("source-id", sampleRepo2.toString(), "", "*", "", false)); + mbp.getSourcesList().add(branchSource); + // Note: this notification causes discovery of branches, + // definition of MBP "leaf" jobs, and launch of builds, + // so below we just make sure they complete and analyze + // the outcomes. + sampleRepoNotifyCommit(sampleRepo2); + r.waitUntilNoActivity(); + System.out.println("Jobs generated by MBP: " + mbp.getItems().toString()); + + WorkflowJob p1 = mbp.getItem("master"); + WorkflowRun b1 = p1.getLastBuild(); + r.waitForCompletion(b1); + assertFalse(p1.isBuilding()); + r.assertBuildStatusSuccess(b1); + r.assertLogContains("Loading library branchylib@master", b1); + r.assertLogContains("something special", b1); + + WorkflowJob p2 = mbp.getItem("feature"); + WorkflowRun b2 = p2.getLastBuild(); + r.waitForCompletion(b2); + assertFalse(p2.isBuilding()); + r.assertBuildStatusSuccess(b2); + r.assertLogContains("Loading library branchylib@feature", b2); + r.assertLogContains("something very special", b2); + + WorkflowJob p3 = mbp.getItem("bogus"); + WorkflowRun b3 = p3.getLastBuild(); + r.waitForCompletion(b3); + assertFalse(p3.isBuilding()); + r.assertBuildStatus(Result.FAILURE, b3); + r.assertLogContains("ERROR: Could not resolve bogus", b3); + r.assertLogContains("ambiguous argument 'bogus^{commit}': unknown revision or path not in the working tree", b3); + r.assertLogContains("ERROR: No version bogus found for library branchylib", b3); + r.assertLogContains("WorkflowScript: Loading libraries failed", b3); + } + + @Issue("JENKINS-69731") + @Test public void checkDefaultVersion_MBP_BRANCH_NAME_notAllowed() throws Exception { + // Test that lc.setAllowBRANCH_NAME(false) causes + // @Library('libname@${BRANCH_NAME}') to always fail + // (not treated as a "version override" for funny + // branch name that is literally "${BRANCH_NAME}"). + + sampleRepo1ContentMasterFeature(); + SCMSourceRetriever scm = new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)); + LibraryConfiguration lc = new LibraryConfiguration("branchylib", scm); + lc.setDefaultVersion("master"); + lc.setIncludeInChangesets(false); + lc.setAllowVersionOverride(true); + lc.setAllowBRANCH_NAME(false); + lc.setTraceDefaultedVersion(true); + GlobalLibraries.get().setLibraries(Collections.singletonList(lc)); + + // Inspired in part by tests like + // https://github.com/jenkinsci/workflow-multibranch-plugin/blob/master/src/test/java/org/jenkinsci/plugins/workflow/multibranch/NoTriggerBranchPropertyWorkflowTest.java#L132 + sampleRepo2ContentSameMasterFeatureBogus_BRANCH_NAME(); + WorkflowMultiBranchProject mbp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "mbp"); + BranchSource branchSource = new BranchSource(new GitSCMSource("source-id", sampleRepo2.toString(), "", "*", "", false)); + mbp.getSourcesList().add(branchSource); + // Note: this notification causes discovery of branches, + // definition of MBP "leaf" jobs, and launch of builds, + // so below we just make sure they complete and analyze + // the outcomes. + sampleRepoNotifyCommit(sampleRepo2); + r.waitUntilNoActivity(); + System.out.println("Jobs generated by MBP: " + mbp.getItems().toString()); + + WorkflowJob p1 = mbp.getItem("master"); + WorkflowRun b1 = p1.getLastBuild(); + r.waitForCompletion(b1); + assertFalse(p1.isBuilding()); + r.assertBuildStatus(Result.FAILURE, b1); + r.assertLogContains("ERROR: Version override not permitted for library branchylib", b1); + r.assertLogContains("WorkflowScript: Loading libraries failed", b1); + + WorkflowJob p2 = mbp.getItem("feature"); + WorkflowRun b2 = p2.getLastBuild(); + r.waitForCompletion(b2); + assertFalse(p2.isBuilding()); + r.assertBuildStatus(Result.FAILURE, b2); + r.assertLogContains("ERROR: Version override not permitted for library branchylib", b2); + r.assertLogContains("WorkflowScript: Loading libraries failed", b2); + + WorkflowJob p3 = mbp.getItem("bogus"); + WorkflowRun b3 = p3.getLastBuild(); + r.waitForCompletion(b3); + assertFalse(p3.isBuilding()); + r.assertBuildStatus(Result.FAILURE, b3); + r.assertLogContains("ERROR: Version override not permitted for library branchylib", b3); + r.assertLogContains("WorkflowScript: Loading libraries failed", b3); + } + + @Issue("JENKINS-69731") + @Test public void checkDefaultVersion_MBPsingleBranch_staticStrings() throws Exception { + // Test that lc.setAllowBRANCH_NAME(false) does not + // preclude fixed branch names (they should work), + // like @Library('branchylib@feature') when used + // for MBP with "Single repository and branch" as + // the SCM source. + + sampleRepo1ContentMasterFeature(); + SCMSourceRetriever scm = new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)); + LibraryConfiguration lc = new LibraryConfiguration("branchylib", scm); + lc.setDefaultVersion("master"); + lc.setIncludeInChangesets(false); + lc.setAllowVersionOverride(true); + lc.setAllowBRANCH_NAME(false); + lc.setTraceDefaultedVersion(true); + GlobalLibraries.get().setLibraries(Collections.singletonList(lc)); + + // Inspired in part by tests like + // https://github.com/jenkinsci/workflow-multibranch-plugin/blob/master/src/test/java/org/jenkinsci/plugins/workflow/multibranch/NoTriggerBranchPropertyWorkflowTest.java#L132 + sampleRepo2ContentUniqueMasterFeatureBogus_staticStrings(); + WorkflowMultiBranchProject mbp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "mbp"); + GitSCM gitSCM = new GitSCM( + GitSCM.createRepoList(sampleRepo2.toString(), null), + Collections.singletonList(new BranchSpec("*/feature")), + null, null, Collections.emptyList()); + // We test an MBP with two leaf jobs where we + // set options "List of branches to build"/ + // "Branch Specifier" to a same and a different + // value than the "name" of branchSource. + // While the "name" becomes "BRANCH_NAME" envvar + // (and MBP job name generated for the branch), + // the specifier is what gets placed into SCMs list. + BranchSource branchSource1 = new BranchSource( + new SingleSCMSource("feature-id1", "feature", gitSCM), + new DefaultBranchPropertyStrategy(new BranchProperty[0])); + BranchSource branchSource2 = new BranchSource( + new SingleSCMSource("feature-id2", "featurette", gitSCM), + new DefaultBranchPropertyStrategy(new BranchProperty[0])); + mbp.getSourcesList().add(branchSource1); + mbp.getSourcesList().add(branchSource2); + mbp.save(); + // Rescan to actually define leaf jobs: + mbp.scheduleBuild(0); + sampleRepoNotifyCommit(sampleRepo2); + r.waitUntilNoActivity(); + System.out.println("All Jenkins items: " + r.jenkins.getItems().toString()); + System.out.println("MBP sources: " + mbp.getSourcesList().toString()); + System.out.println("MBP source 0: " + mbp.getSourcesList().get(0).getSource().toString()); + System.out.println("MBP source 1: " + mbp.getSourcesList().get(1).getSource().toString()); + System.out.println("Jobs generated by MBP: " + mbp.getItems().toString()); + assumeFalse("SKIP by pre-test assumption: " + + "MBP should have generated 'feature' and 'featurette' pipeline job", mbp.getItems().size() != 2); + + // In case of MBP with "Single repository and branch" + // it only defines one job (per single-branch source), + // so those for other known branches should be null: + WorkflowJob p1 = mbp.getItem("master"); + assertNull(p1); + + WorkflowJob p2 = mbp.getItem("feature"); + assertNotNull(p2); + WorkflowRun b2 = p2.getLastBuild(); + r.waitForCompletion(b2); + assertFalse(p2.isBuilding()); + r.assertBuildStatusSuccess(b2); + r.assertLogContains("Loading library branchylib@feature", b2); + r.assertLogContains("something very special", b2); + + WorkflowJob p3 = mbp.getItem("bogus"); + assertNull(p3); + + // For fixed branch in @Library spec, we see the + // SingleSCMSource checkout out the "*/feature" + // specified in its GitSCM and so request the + // @Library('branchylib@feature') spelled there. + // And then MBP sets BRANCH_NAME='featurette' + // (and leaf job) per SingleSCMSource "name". + WorkflowJob p4 = mbp.getItem("featurette"); + assertNotNull(p4); + WorkflowRun b4 = p4.getLastBuild(); + r.waitForCompletion(b4); + assertFalse(p4.isBuilding()); + r.assertBuildStatusSuccess(b4); + System.out.println("Envvar BRANCH_NAME set into 'featurette' job: " + b4.getEnvironment().get("BRANCH_NAME")); + // We use same gitSCM source, and so same static + // version of the library: + r.assertLogContains("Loading library branchylib@feature", b4); + r.assertLogContains("something very special", b4); + } + + @Issue("JENKINS-69731") + @Test public void checkDefaultVersion_MBPsingleBranch_BRANCH_NAME() throws Exception { + // Test that @Library('branchylib@${BRANCH_NAME}') works + // also for MBP with "Single repository and branch" as + // the SCM source. + + sampleRepo1ContentMasterFeature(); + SCMSourceRetriever scm = new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)); + LibraryConfiguration lc = new LibraryConfiguration("branchylib", scm); + lc.setDefaultVersion("master"); + lc.setIncludeInChangesets(false); + lc.setAllowVersionOverride(true); + lc.setAllowBRANCH_NAME(true); + lc.setTraceDefaultedVersion(true); + GlobalLibraries.get().setLibraries(Collections.singletonList(lc)); + + // Inspired in part by tests like + // https://github.com/jenkinsci/workflow-multibranch-plugin/blob/master/src/test/java/org/jenkinsci/plugins/workflow/multibranch/NoTriggerBranchPropertyWorkflowTest.java#L132 + sampleRepo2ContentSameMasterFeatureBogus_BRANCH_NAME(); + WorkflowMultiBranchProject mbp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "mbp"); + GitSCM gitSCM = new GitSCM( + GitSCM.createRepoList(sampleRepo2.toString(), null), + Collections.singletonList(new BranchSpec("*/feature")), + null, null, Collections.emptyList()); + // We test an MBP with two leaf jobs where we + // set options "List of branches to build"/ + // "Branch Specifier" to a same and a different + // value than the "name" of branchSource. + // While the "name" becomes "BRANCH_NAME" envvar + // (and MBP job name generated for the branch), + // the specifier is what gets placed into SCMs list. + BranchSource branchSource1 = new BranchSource( + new SingleSCMSource("feature-id1", "feature", gitSCM), + new DefaultBranchPropertyStrategy(new BranchProperty[0])); + BranchSource branchSource2 = new BranchSource( + new SingleSCMSource("feature-id2", "featurette", gitSCM), + new DefaultBranchPropertyStrategy(new BranchProperty[0])); + mbp.getSourcesList().add(branchSource1); + mbp.getSourcesList().add(branchSource2); + mbp.save(); + // Rescan to actually define leaf jobs: + mbp.scheduleBuild(0); + sampleRepoNotifyCommit(sampleRepo2); + r.waitUntilNoActivity(); + System.out.println("All Jenkins items: " + r.jenkins.getItems().toString()); + System.out.println("MBP sources: " + mbp.getSourcesList().toString()); + System.out.println("MBP source 0: " + mbp.getSourcesList().get(0).getSource().toString()); + System.out.println("MBP source 1: " + mbp.getSourcesList().get(1).getSource().toString()); + System.out.println("Jobs generated by MBP: " + mbp.getItems().toString()); + assumeFalse("SKIP by pre-test assumption: " + + "MBP should have generated 'feature' and 'featurette' pipeline job", mbp.getItems().size() != 2); + + // In case of MBP with "Single repository and branch" + // it only defines one job (per single-branch source), + // so those for other known branches should be null: + WorkflowJob p1 = mbp.getItem("master"); + assertNull(p1); + + WorkflowJob p2 = mbp.getItem("feature"); + assertNotNull(p2); + WorkflowRun b2 = p2.getLastBuild(); + r.waitForCompletion(b2); + assertFalse(p2.isBuilding()); + r.assertBuildStatusSuccess(b2); + r.assertLogContains("Loading library branchylib@feature", b2); + r.assertLogContains("something very special", b2); + + WorkflowJob p3 = mbp.getItem("bogus"); + assertNull(p3); + + // For fixed branch in @Library spec, we see the + // SingleSCMSource checkout out the "*/feature" + // specified in its GitSCM and so evaluate the + // @Library('branchylib@${BRANCH_NAME}')... + // And then MBP sets BRANCH_NAME='featurette' + // (and leaf job) per SingleSCMSource "name". + WorkflowJob p4 = mbp.getItem("featurette"); + assertNotNull(p4); + WorkflowRun b4 = p4.getLastBuild(); + r.waitForCompletion(b4); + assertFalse(p4.isBuilding()); + r.assertBuildStatusSuccess(b4); + System.out.println("Envvar BRANCH_NAME set into 'featurette' job: " + b4.getEnvironment().get("BRANCH_NAME")); + // Library does not have a "featurette" branch, + // so if that source were tried according to the + // single-branch source name, should fall back + // to "master". But if it were tried according + // to the actual branch of pipeline script, it + // should use "feature". + // For now, I've sided with the MBP plugin which + // goes to great lengths to make-believe that + // the "name" specified in config is the branch + // name (also setting it into the WorkflowJob + // BranchJobProperty), even if it does not exist + // in actual SCM. + r.assertLogContains("Loading library branchylib@master", b4); + r.assertLogContains("something special", b4); + } + + @Issue("JENKINS-69731") + @Test public void checkDefaultVersion_singleBranch_staticStrings() throws Exception { + // Test that lc.setAllowBRANCH_NAME(false) does not + // preclude fixed branch names (they should work), + // like @Library('branchylib@master') when used for + // a simple "Pipeline" job with static SCM source. + + sampleRepo1ContentMasterFeature(); + SCMSourceRetriever scm = new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)); + LibraryConfiguration lc = new LibraryConfiguration("branchylib", scm); + lc.setDefaultVersion("master"); + lc.setIncludeInChangesets(false); + lc.setAllowVersionOverride(true); + lc.setAllowBRANCH_NAME(false); + lc.setTraceDefaultedVersion(true); + GlobalLibraries.get().setLibraries(Collections.singletonList(lc)); + + // Inspired in part by tests like + // https://github.com/jenkinsci/workflow-multibranch-plugin/blob/master/src/test/java/org/jenkinsci/plugins/workflow/multibranch/NoTriggerBranchPropertyWorkflowTest.java#L132 + sampleRepo2ContentUniqueMasterFeatureBogus_staticStrings(); + //GitSCM gitSCM = new GitSCM(sampleRepo2.toString()); + GitSCM gitSCM = new GitSCM( + GitSCM.createRepoList(sampleRepo2.toString(), null), + Collections.singletonList(new BranchSpec("*/feature")), + null, null, Collections.emptyList()); + + WorkflowJob p0 = r.jenkins.createProject(WorkflowJob.class, "p0"); + p0.setDefinition(new CpsScmFlowDefinition(gitSCM, "Jenkinsfile")); + sampleRepoNotifyCommit(sampleRepo2); + r.waitUntilNoActivity(); + + WorkflowRun b0 = r.buildAndAssertSuccess(p0); + r.assertLogContains("Loading library branchylib@feature", b0); + r.assertLogContains("something very special", b0); + } + + @Issue("JENKINS-69731") + @Test public void checkDefaultVersion_singleBranch_BRANCH_NAME() throws Exception { + // Test that lc.setAllowBRANCH_NAME(true) enables + // @Library('branchylib@${BRANCH_NAME}') also for + // a simple "Pipeline" job with static SCM source, + // and that even lc.setAllowVersionOverride(false) + // does not intervene here. + assumeFalse("SKIP by pre-test assumption: " + + "An externally provided BRANCH_NAME envvar interferes with tested logic", + System.getenv("BRANCH_NAME") != null); + + sampleRepo1ContentMasterFeature(); + SCMSourceRetriever scm = new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)); + LibraryConfiguration lc = new LibraryConfiguration("branchylib", scm); + lc.setDefaultVersion("master"); + lc.setIncludeInChangesets(false); + lc.setAllowVersionOverride(false); + lc.setAllowBRANCH_NAME(true); + lc.setTraceDefaultedVersion(true); + GlobalLibraries.get().setLibraries(Collections.singletonList(lc)); + + // Inspired in part by tests like + // https://github.com/jenkinsci/workflow-multibranch-plugin/blob/master/src/test/java/org/jenkinsci/plugins/workflow/multibranch/NoTriggerBranchPropertyWorkflowTest.java#L132 + sampleRepo2ContentSameMasterFeatureBogus_BRANCH_NAME(); + + // Get a non-default branch loaded for this single-branch build: + GitSCM gitSCM = new GitSCM( + GitSCM.createRepoList(sampleRepo2.toString(), null), + Collections.singletonList(new BranchSpec("*/feature")), + null, null, Collections.emptyList()); + + WorkflowJob p0 = r.jenkins.createProject(WorkflowJob.class, "p0"); + p0.setDefinition(new CpsScmFlowDefinition(gitSCM, "Jenkinsfile")); + sampleRepoNotifyCommit(sampleRepo2); + r.waitUntilNoActivity(); + + WorkflowRun b0 = r.buildAndAssertSuccess(p0); + r.assertLogContains("Loading library branchylib@feature", b0); + r.assertLogContains("something very special", b0); + } + + @Issue("JENKINS-69731") + @Test public void checkDefaultVersion_singleBranch_BRANCH_NAME_after_staticStrings() throws Exception { + // Test that using @Library('branchylib@static') + // in one build of a job definition, and then a + // @Library('branchylib@${BRANCH_NAME}') next, + // both behave well. + // For context see e.g. WorkflowJob.getSCMs(): + // https://github.com/jonsten/workflow-job-plugin/blob/master/src/main/java/org/jenkinsci/plugins/workflow/job/WorkflowJob.java#L539 + // https://issues.jenkins.io/browse/JENKINS-40255 + // how it looks at a history of EARLIER builds + // (preferring successes) and not at the current + // job definition. + // Note: being a piece of test-driven development, + // this test does not fail as soon as it gets an + // "unexpected" log message (so far expected due + // to the bug being hunted), but counts the faults + // and asserts in the end whether there were none. + assumeFalse("SKIP by pre-test assumption: " + + "An externally provided BRANCH_NAME envvar interferes with tested logic", + System.getenv("BRANCH_NAME") != null); + + sampleRepo1ContentMasterFeatureStable(); + SCMSourceRetriever scm = new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)); + LibraryConfiguration lc = new LibraryConfiguration("branchylib", scm); + lc.setDefaultVersion("master"); + lc.setIncludeInChangesets(false); + lc.setAllowVersionOverride(false); + lc.setAllowBRANCH_NAME(true); + lc.setTraceDefaultedVersion(true); + GlobalLibraries.get().setLibraries(Collections.singletonList(lc)); + + // Inspired in part by tests like + // https://github.com/jenkinsci/workflow-multibranch-plugin/blob/master/src/test/java/org/jenkinsci/plugins/workflow/multibranch/NoTriggerBranchPropertyWorkflowTest.java#L132 + sampleRepo2ContentSameMasterFeatureBogus_BRANCH_NAME(true); + + // Get a non-default branch loaded for this single-branch build: + GitSCM gitSCM = new GitSCM( + GitSCM.createRepoList(sampleRepo2.toString(), null), + Collections.singletonList(new BranchSpec("*/feature")), + null, null, Collections.emptyList()); + + sampleRepoNotifyCommit(sampleRepo2); + r.waitUntilNoActivity(); + + // First run a job definition with a fixed library version, + // e.g. like a custom Replay might in the field, or before + // redefining an "inline" pipeline to one coming from SCM. + // Pepper job history with successes and faults: + long failCount = 0; + WorkflowJob p1 = r.jenkins.createProject(WorkflowJob.class, "p1"); + p1.setDefinition(new CpsFlowDefinition("@Library('branchylib@stable') import myecho; myecho()", true)); + WorkflowRun b1 = r.buildAndAssertStatus(Result.FAILURE, p1); + r.assertLogContains("ERROR: Version override not permitted for library branchylib", b1); + r.assertLogContains("WorkflowScript: Loading libraries failed", b1); + + // Use default version: + p1.setDefinition(new CpsFlowDefinition("@Library('branchylib') import myecho; myecho()", true)); + WorkflowRun b2 = r.buildAndAssertSuccess(p1); + r.assertLogContains("Loading library branchylib@master", b2); + r.assertLogContains("something special", b2); + + // Now redefine the same job to come from SCM and use a + // run-time resolved library version (WorkflowJob getSCMs + // behavior should not be a problem): + p1.setDefinition(new CpsScmFlowDefinition(gitSCM, "Jenkinsfile")); + WorkflowRun b3 = r.buildAndAssertSuccess(p1); + try { + // In case of misbehavior this loads "master" version: + r.assertLogContains("Loading library branchylib@feature", b3); + r.assertLogContains("something very special", b3); + } catch (AssertionError ae) { + failCount++; + // Make sure it was not some other problem: + r.assertLogContains("Loading library branchylib@master", b3); + r.assertLogContains("something special", b3); + } + + // Override with a static version: + lc.setAllowVersionOverride(true); + p1.setDefinition(new CpsFlowDefinition("@Library('branchylib@stable') import myecho; myecho()", true)); + WorkflowRun b4 = r.buildAndAssertSuccess(p1); + r.assertLogContains("Loading library branchylib@stable", b4); + r.assertLogContains("something reliable", b4); + + // Dynamic version again: + p1.setDefinition(new CpsScmFlowDefinition(gitSCM, "Jenkinsfile")); + WorkflowRun b5 = r.buildAndAssertSuccess(p1); + try { + // In case of misbehavior this loads "stable" version: + r.assertLogContains("Loading library branchylib@feature", b5); + r.assertLogContains("something very special", b5); + } catch (AssertionError ae) { + failCount++; + // Make sure it was not some other problem: + r.assertLogContains("Loading library branchylib@stable", b5); + r.assertLogContains("something reliable", b5); + } + + // SCM source pointing at static version + p1.setDefinition(new CpsScmFlowDefinition(gitSCM, "Jenkinsfile-static")); + WorkflowRun b6 = r.buildAndAssertSuccess(p1); + r.assertLogContains("Loading library branchylib@stable", b6); + r.assertLogContains("something reliable", b6); + + // Dynamic version again; seems with the change of filename it works okay: + p1.setDefinition(new CpsScmFlowDefinition(gitSCM, "Jenkinsfile")); + WorkflowRun b7 = r.buildAndAssertSuccess(p1); + try { + // In case of misbehavior this loads "stable" version: + r.assertLogContains("Loading library branchylib@feature", b7); + r.assertLogContains("something very special", b7); + } catch (AssertionError ae) { + failCount++; + // Make sure it was not some other problem: + r.assertLogContains("Loading library branchylib@stable", b7); + r.assertLogContains("something reliable", b7); + } + + assertEquals("All BRANCH_NAME resolutions are expected to checkout feature",0, failCount); + } + + @Issue("JENKINS-69731") + @Test public void checkDefaultVersion_singleBranch_BRANCH_NAME_doubleQuotes() throws Exception { + // Similar to above, the goal of this test is to + // verify that substitution of ${BRANCH_NAME} is + // not impacted by content of Groovy variables. + // The @Library annotation version resolution + // happens before Groovy->Java->... compilation. + // Note that at this time LibraryDecorator.java + // forbids use of non-constant strings; so this + // test would be dynamically skipped as long as + // this behavior happens. + // TODO: If this behavior does change, extend + // the test to also try ${env.VARNAME} confusion. + + assumeFalse("SKIP by pre-test assumption: " + + "An externally provided BRANCH_NAME envvar interferes with tested logic", + System.getenv("BRANCH_NAME") != null); + + sampleRepo1ContentMasterFeature(); + SCMSourceRetriever scm = new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)); + LibraryConfiguration lc = new LibraryConfiguration("branchylib", scm); + lc.setDefaultVersion("master"); + lc.setIncludeInChangesets(false); + lc.setAllowVersionOverride(false); + lc.setAllowBRANCH_NAME(true); + lc.setTraceDefaultedVersion(true); + GlobalLibraries.get().setLibraries(Collections.singletonList(lc)); + + // Inspired in part by tests like + // https://github.com/jenkinsci/workflow-multibranch-plugin/blob/master/src/test/java/org/jenkinsci/plugins/workflow/multibranch/NoTriggerBranchPropertyWorkflowTest.java#L132 + sampleRepo2.init(); + sampleRepo2.write("Jenkinsfile", "BRANCH_NAME='whatever'; @Library(\"branchylib@${BRANCH_NAME}\") import myecho; myecho()"); + sampleRepo2.git("add", "Jenkinsfile"); + sampleRepo2.git("commit", "--message=init"); + sampleRepo2.git("branch", "feature"); + sampleRepo2.git("branch", "bogus"); + + // Get a non-default branch loaded for this single-branch build: + GitSCM gitSCM = new GitSCM( + GitSCM.createRepoList(sampleRepo2.toString(), null), + Collections.singletonList(new BranchSpec("*/feature")), + null, null, Collections.emptyList()); + + WorkflowJob p1 = r.jenkins.createProject(WorkflowJob.class, "p1"); + p1.setDefinition(new CpsScmFlowDefinition(gitSCM, "Jenkinsfile")); + sampleRepoNotifyCommit(sampleRepo2); + r.waitUntilNoActivity(); + p1.scheduleBuild2(0); + r.waitUntilNoActivity(); + WorkflowRun b1 = p1.getLastBuild(); + r.waitForCompletion(b1); + assertFalse(p1.isBuilding()); + + // LibraryDecorator may forbid use of double-quotes + try { + r.assertBuildStatus(Result.FAILURE, b1); + r.assertLogContains("WorkflowScript: @Library value", b1); + r.assertLogContains("was not a constant; did you mean to use the", b1); + r.assertLogContains("step instead?", b1); + // assertions survived, skip the test + // not exactly "pre-test" but oh well + assumeFalse("SKIP by pre-test assumption: " + + "LibraryDecorator forbids use of double-quotes for @Library annotation", true); + } catch(AssertionError x) { + // Chosen library version should not be "whatever" + // causing fallback to "master", but "feature" per + // pipeline SCM branch name. + r.assertBuildStatusSuccess(b1); + r.assertLogContains("Loading library branchylib@feature", b1); + r.assertLogContains("something very special", b1); + } + } + + @Issue("JENKINS-69731") + @Test public void checkDefaultVersion_singleBranch_BRANCH_NAME_lightweight() throws Exception { + // Test that lightweight checkouts from SCM allow + // @Library('branchylib@${BRANCH_NAME}') to see + // sufficient SCM context to determine the branch. + assumeFalse("SKIP by pre-test assumption: " + + "An externally provided BRANCH_NAME envvar interferes with tested logic", + System.getenv("BRANCH_NAME") != null); + + sampleRepo1ContentMasterFeature(); + SCMSourceRetriever scm = new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)); + LibraryConfiguration lc = new LibraryConfiguration("branchylib", scm); + lc.setDefaultVersion("master"); + lc.setIncludeInChangesets(false); + lc.setAllowVersionOverride(false); + lc.setAllowBRANCH_NAME(true); + lc.setTraceDefaultedVersion(true); + GlobalLibraries.get().setLibraries(Collections.singletonList(lc)); + + // Inspired in part by tests like + // https://github.com/jenkinsci/workflow-multibranch-plugin/blob/master/src/test/java/org/jenkinsci/plugins/workflow/multibranch/NoTriggerBranchPropertyWorkflowTest.java#L132 + sampleRepo2ContentSameMasterFeatureBogus_BRANCH_NAME(); + + // Get a non-default branch loaded for this single-branch build: + GitSCM gitSCM = new GitSCM( + GitSCM.createRepoList(sampleRepo2.toString(), null), + Collections.singletonList(new BranchSpec("*/feature")), + null, null, Collections.emptyList()); + + CpsScmFlowDefinition csfd = new CpsScmFlowDefinition(gitSCM, "Jenkinsfile"); + csfd.setLightweight(true); + WorkflowJob p0 = r.jenkins.createProject(WorkflowJob.class, "p0"); + p0.setDefinition(csfd); + sampleRepoNotifyCommit(sampleRepo2); + r.waitUntilNoActivity(); + + WorkflowRun b0 = r.buildAndAssertSuccess(p0); + r.assertLogContains("Loading library branchylib@feature", b0); + r.assertLogContains("something very special", b0); + } + + @Issue("JENKINS-69731") + @Test public void checkDefaultVersion_inline_allowVersionEnvvar() throws Exception { + // Test that @Library('branchylib@${env.TEST_VAR_NAME}') + // is resolved with the TEST_VAR_NAME="feature" in environment. + + // Do not let caller-provided BRANCH_NAME interfere here + assumeFalse("SKIP by pre-test assumption: " + + "An externally provided TEST_VAR_NAME envvar interferes with tested logic", + System.getenv("TEST_VAR_NAME") != null); + + sampleRepo1ContentMasterFeatureStable(); + SCMSourceRetriever scm = new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)); + LibraryConfiguration lc = new LibraryConfiguration("branchylib", scm); + lc.setDefaultVersion("master"); + lc.setIncludeInChangesets(false); + lc.setAllowVersionOverride(true); + lc.setAllowVersionEnvvar(true); + lc.setTraceDefaultedVersion(true); + GlobalLibraries.get().setLibraries(Collections.singletonList(lc)); + + // TEST_VAR_NAME for job not set - fall back to default + WorkflowJob p0 = r.jenkins.createProject(WorkflowJob.class, "p0"); + p0.setDefinition(new CpsFlowDefinition("@Library('branchylib@${env.TEST_VAR_NAME}') import myecho; myecho()", true)); + WorkflowRun b0 = r.buildAndAssertSuccess(p0); + r.assertLogContains("Loading library branchylib@master", b0); + r.assertLogContains("something special", b0); + + // TEST_VAR_NAME injected into env, use its value for library checkout + // https://github.com/jenkinsci/envinject-plugin/blob/master/src/test/java/org/jenkinsci/plugins/envinject/EnvInjectPluginActionTest.java + WorkflowJob p1 = r.jenkins.createProject(WorkflowJob.class, "p1"); + p1.setDefinition(new CpsFlowDefinition("@Library('branchylib@${env.TEST_VAR_NAME}') import myecho; myecho(); try { echo \"Groovy TEST_VAR_NAME='${TEST_VAR_NAME}'\"; } catch (groovy.lang.MissingPropertyException mpe) { echo \"Groovy TEST_VAR_NAME missing: ${mpe.getMessage()}\"; } ; echo \"env.TEST_VAR_NAME='${env.TEST_VAR_NAME}'\"", true)); + + // Inject envvar to server global settings: + DescribableList, NodePropertyDescriptor> globalNodeProperties = r.jenkins.getGlobalNodeProperties(); + List envVarsNodePropertyList = globalNodeProperties.getAll(EnvironmentVariablesNodeProperty.class); + + EnvironmentVariablesNodeProperty newEnvVarsNodeProperty = null; + EnvVars envVars = null; + + if (envVarsNodePropertyList == null || envVarsNodePropertyList.isEmpty()) { + newEnvVarsNodeProperty = new EnvironmentVariablesNodeProperty(); + globalNodeProperties.add(newEnvVarsNodeProperty); + envVars = newEnvVarsNodeProperty.getEnvVars(); + } else { + envVars = envVarsNodePropertyList.get(0).getEnvVars(); + } + envVars.put("TEST_VAR_NAME", "stable"); + r.jenkins.save(); + + WorkflowRun b1 = r.buildAndAssertSuccess(p1); + r.assertLogContains("Loading library branchylib@stable", b1); + r.assertLogContains("something reliable", b1); + + // Now try a more direct way to inject environment + // variables into a Job/Run without extra plugins: + + // See research commented at + // https://github.com/jenkinsci/pipeline-groovy-lib-plugin/pull/19#discussion_r990781686 + + // General override idea was lifted from + // https://github.com/jenkinsci/subversion-plugin/blob/c63586b2e57ab15cd5142dd349b53886194d90af/src/test/java/hudson/scm/SubversionSCMTest.java#L1383 + // test-case recursiveEnvironmentVariables() + + // Per https://github.com/jenkinsci/jenkins/blob/031f40c50899ec4e5fa4d886a1c006a5330f2627/core/src/main/java/hudson/ExtensionList.java#L296 + // in implementation of `add(index, T)` the index is ignored. + // So we save a copy in same order, drop the list, add our + // override as the only entry (hence highest priority) + // and re-add the original list contents. + ExtensionList ecList = EnvironmentContributor.all(); + List ecOrig = new ArrayList(); + for (EnvironmentContributor ec : ecList) { + ecOrig.add(ec); + } + ecList.removeAll(ecOrig); + assumeFalse("SKIP by pre-test assumption: " + + "EnvironmentContributor.all() should be empty now", !ecList.isEmpty()); + + ecList.add(new EnvironmentContributor() { + @Override public void buildEnvironmentFor(Run run, EnvVars ev, TaskListener tl) throws IOException, InterruptedException { + if (tl != null) + tl.getLogger().println("[DEBUG:RUN] Injecting TEST_VAR_NAME='feature' to EnvVars"); + ev.put("TEST_VAR_NAME", "feature"); + } + @Override public void buildEnvironmentFor(Job run, EnvVars ev, TaskListener tl) throws IOException, InterruptedException { + if (tl != null) + tl.getLogger().println("[DEBUG:JOB] Injecting TEST_VAR_NAME='feature' to EnvVars"); + ev.put("TEST_VAR_NAME", "feature"); + } + }); + for (EnvironmentContributor ec : ecOrig) { + ecList.add(ec); + } + + p1.scheduleBuild2(0); + r.waitUntilNoActivity(); + WorkflowRun b2 = p1.getLastBuild(); + r.waitForCompletion(b2); + assertFalse(p1.isBuilding()); + r.assertBuildStatusSuccess(b2); + + System.out.println("[DEBUG:EXT:p1b2] wfJob env: " + p1.getEnvironment(null, null)); + System.out.println("[DEBUG:EXT:p1b2] wfRun env: " + b2.getEnvironment()); + System.out.println("[DEBUG:EXT:p1b2] wfRun envContribActions: " + b2.getActions(EnvironmentContributingAction.class)); + + // Our first try is expected to fail currently, since + // WorkflowRun::getEnvironment() takes "env" from + // super-class, and overlays with envvars from global + // configuration. However, if in the future behavior + // of workflow changes, it is not consequential - just + // something to adjust "correct" expectations for. + r.assertLogContains("Loading library branchylib@stable", b2); + r.assertLogContains("something reliable", b2); + + // For the next try, however, we remove global config + // part and expect the injected envvar to take hold: + envVars.remove("TEST_VAR_NAME"); + r.jenkins.save(); + + WorkflowRun b3 = r.buildAndAssertSuccess(p1); + + System.out.println("[DEBUG:EXT:p1b3] wfJob env: " + p1.getEnvironment(null, null)); + System.out.println("[DEBUG:EXT:p1b3] wfRun env: " + b3.getEnvironment()); + System.out.println("[DEBUG:EXT:p1b3] wfRun envContribActions: " + b3.getActions(EnvironmentContributingAction.class)); + + r.assertLogContains("Loading library branchylib@feature", b3); + r.assertLogContains("something very special", b3); + + // Below we do a similar trick with built-in agent settings + // which (as a Computer=>Node) has lowest priority behind + // global and injected envvars (see Run::getEnvironment()). + // Check with the injected envvars (they override) like above + // first, and with ecList contents restored to ecOrig state + // only (so only Computer envvars are applied). + // Trick here is that the "jenkins.model.Jenkins" is inherited + // from Node and its toComputer() returns the "built-in" (nee + // "master"), instance of "hudson.model.Hudson$MasterComputer" + // whose String getName() is actually empty. + // Pipeline scripts are initially processed only by this node, + // further run on the controller, and then the actual work is + // distributed to agents (often via remoting proxy methods) + // if any are defined. + Computer builtInComputer = r.jenkins.toComputer(); + Node builtInNode = builtInComputer.getNode(); +/* + EnvironmentVariablesNodeProperty builtInEnvProp = builtInNode.getNodeProperty(EnvironmentVariablesNodeProperty.class); + + if (builtInEnvProp == null) { + builtInEnvProp = new EnvironmentVariablesNodeProperty(); + //builtInEnvProp.setNode(builtInNode); + builtInNode.getNodeProperties().add(builtInEnvProp); + } + EnvVars builtInEnvVars = builtInEnvProp.getEnvVars(); + builtInEnvVars.put("TEST_VAR_NAME", "stable"); + builtInEnvProp.buildEnvVars(new EnvVars("TEST_VAR_NAME", "stable"), null); + */ + builtInNode.getNodeProperties().add(new EnvironmentVariablesNodeProperty(new EnvironmentVariablesNodeProperty.Entry("TEST_VAR_NAME", "stable"))); + r.jenkins.save(); + builtInNode.save(); + + System.out.println("[DEBUG] Restart the 'built-in' Computer connection to clear its cachedEnvironment and recognize added envvar"); + builtInComputer.setTemporarilyOffline(true, new OfflineCause.ByCLI("Restart built-in to reread envvars config")); + builtInComputer.waitUntilOffline(); + builtInComputer.disconnect(new OfflineCause.ByCLI("Restart built-in to reread envvars config")); + r.waitUntilNoActivity(); + Thread.sleep(3000); + builtInComputer.setTemporarilyOffline(false, null); + builtInComputer.connect(true); + builtInComputer.waitUntilOnline(); + + System.out.println("[DEBUG] builtIn node env: " + builtInComputer.getEnvironment()); + + // Both injected var and build node envvar setting present; + // injected var wins: + WorkflowRun b4 = r.buildAndAssertSuccess(p1); + System.out.println("[DEBUG:EXT:p1b4] wfJob env: " + p1.getEnvironment(null, null)); + System.out.println("[DEBUG:EXT:p1b4] wfRun env: " + b4.getEnvironment()); + System.out.println("[DEBUG:EXT:p1b4] wfRun envContribActions: " + b4.getActions(EnvironmentContributingAction.class)); + r.assertLogContains("Loading library branchylib@feature", b4); + r.assertLogContains("something very special", b4); + r.assertLogContains("Groovy TEST_VAR_NAME='feature'", b4); + r.assertLogContains("env.TEST_VAR_NAME='feature'", b4); + + // Only build agent envvars are present: drop all, + // add back original (before our mock above): + List ecCurr = new ArrayList(); + for (EnvironmentContributor ec : ecList) { + ecCurr.add(ec); + } + ecList.removeAll(ecCurr); + for (EnvironmentContributor ec : ecOrig) { + ecList.add(ec); + } + + System.out.println("[DEBUG:EXT:p1b5] EnvironmentContributor.all(): " + EnvironmentContributor.all()); + System.out.println("[DEBUG:EXT:p1b5] builtIn node env: " + builtInComputer.getEnvironment()); + System.out.println("[DEBUG:EXT:p1b5] wfJob env in builtIn: " + p1.getEnvironment(builtInNode, null)); + System.out.println("[DEBUG:EXT:p1b5] wfJob env (without node): " + p1.getEnvironment(null, null)); + WorkflowRun b5 = r.buildAndAssertSuccess(p1); + System.out.println("[DEBUG:EXT:p1b5] wfRun env: " + b5.getEnvironment()); + System.out.println("[DEBUG:EXT:p1b5] wfRun envContribActions: " + b5.getActions(EnvironmentContributingAction.class)); + r.assertLogContains("Loading library branchylib@stable", b5); + r.assertLogContains("something reliable", b5); + // Why oh why is the build agent's (cached) envvar not resolved + // even after reconnect?.. Probably a burden of "built-in" until + // Jenkins restart?.. + r.assertLogContains("Groovy TEST_VAR_NAME missing", b5); + r.assertLogContains("env.TEST_VAR_NAME='null'", b5); + + // Let's try just that - restart Jenkins to fully reinit the + // built-in node with its config: + r.jenkins.reload(); + r.waitUntilNoActivity(); + + // Feed it to Java reflection, to clear the internal cache... + Field ccc = Computer.class.getDeclaredField("cachedEnvironment"); + ccc.setAccessible(true); + ccc.set(builtInComputer, null); + + // Still, "built-in" node's envvars are ignored (technically they - + // now that the cache is cleared - reload global config values for + // the "MasterComputer" as its own). Checked on standalone instance + // configured interactively that even a complete Jenkins restart + // does not let configured "built-in" node envvars become recognized. + // Loosely related to https://github.com/jenkinsci/jenkins/pull/1728 + // So we keep this test here as a way to notice if core functionality + // ever changes. + WorkflowRun b6 = r.buildAndAssertSuccess(p1); + r.assertLogContains("Loading library branchylib@stable", b6); + r.assertLogContains("something reliable", b6); + r.assertLogContains("Groovy TEST_VAR_NAME missing", b6); + r.assertLogContains("env.TEST_VAR_NAME='null'", b6); + } + @Issue("JENKINS-43802") @Test public void owner() throws Exception { GlobalLibraries.get().setLibraries(Collections.singletonList( @@ -325,10 +1508,7 @@ public static class BasicSCMSource extends SCMSource { @Issue("JENKINS-66629") @Test public void renameDeletesOldLibsWorkspace() throws Exception { - sampleRepo.init(); - sampleRepo.write("vars/myecho.groovy", "def call() {echo 'something special'}"); - sampleRepo.git("add", "vars"); - sampleRepo.git("commit", "--message=init"); + sampleRepo1ContentMaster(); GlobalLibraries.get().setLibraries(Collections.singletonList( new LibraryConfiguration("delete_removes_libs_workspace", new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true))))); @@ -349,10 +1529,7 @@ public static class BasicSCMSource extends SCMSource { @Issue("JENKINS-66629") @Test public void deleteRemovesLibsWorkspace() throws Exception { - sampleRepo.init(); - sampleRepo.write("vars/myecho.groovy", "def call() {echo 'something special'}"); - sampleRepo.git("add", "vars"); - sampleRepo.git("commit", "--message=init"); + sampleRepo1ContentMaster(); GlobalLibraries.get().setLibraries(Collections.singletonList( new LibraryConfiguration("delete_removes_libs_workspace", new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true))))); @@ -397,10 +1574,7 @@ public static class BasicSCMSource extends SCMSource { } @Test public void cloneModeLibraryPath() throws Exception { - sampleRepo.init(); - sampleRepo.write("sub/path/vars/myecho.groovy", "def call() {echo 'something special'}"); - sampleRepo.git("add", "sub"); - sampleRepo.git("commit", "--message=init"); + sampleRepo1ContentMaster("sub/path", "sub"); SCMSourceRetriever scm = new SCMSourceRetriever(new GitSCMSource(sampleRepo.toString())); LibraryConfiguration lc = new LibraryConfiguration("root_sub_path", scm); lc.setIncludeInChangesets(false); @@ -418,10 +1592,7 @@ public static class BasicSCMSource extends SCMSource { } @Test public void cloneModeLibraryPathSecurity() throws Exception { - sampleRepo.init(); - sampleRepo.write("sub/path/vars/myecho.groovy", "def call() {echo 'something special'}"); - sampleRepo.git("add", "sub"); - sampleRepo.git("commit", "--message=init"); + sampleRepo1ContentMaster("sub/path", "sub"); SCMSourceRetriever scm = new SCMSourceRetriever(new GitSCMSource(sampleRepo.toString())); LibraryConfiguration lc = new LibraryConfiguration("root_sub_path", scm); lc.setIncludeInChangesets(false); @@ -475,5 +1646,4 @@ public static class BasicSCMSource extends SCMSource { r.assertLogContains("got something special", b); r.assertLogNotContains("Excluding src/test/ from checkout", b); } - }