From 76bdf910442f80381fd0c4cecbc3d224f9082ff4 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 28 Feb 2025 17:08:18 -0500 Subject: [PATCH 1/8] Reproduce `LabelScript` incompatibility using `RealJenkinsRule` --- pipeline-model-definition/pom.xml | 44 ++++++ .../pipeline/modeldefinition/UpgradeTest.java | 126 ++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 pipeline-model-definition/src/test/java/org/jenkinsci/plugins/pipeline/modeldefinition/UpgradeTest.java diff --git a/pipeline-model-definition/pom.xml b/pipeline-model-definition/pom.xml index 79c468b62..4d227646e 100644 --- a/pipeline-model-definition/pom.xml +++ b/pipeline-model-definition/pom.xml @@ -61,6 +61,49 @@ + + org.apache.maven.plugins + maven-dependency-plugin + + + old-releases + + copy + + generate-test-resources + + ${project.build.testOutputDirectory}/old-releases + true + + + org.jenkinsci.plugins + pipeline-stage-tags-metadata + ${old-release.version} + hpi + + + org.jenkinsci.plugins + pipeline-model-api + ${old-release.version} + hpi + + + org.jenkinsci.plugins + pipeline-model-extensions + ${old-release.version} + hpi + + + org.jenkinsci.plugins + pipeline-model-definition + ${old-release.version} + hpi + + + + + + @@ -252,5 +295,6 @@ false true + 2.2221.vc657003fb_d93 diff --git a/pipeline-model-definition/src/test/java/org/jenkinsci/plugins/pipeline/modeldefinition/UpgradeTest.java b/pipeline-model-definition/src/test/java/org/jenkinsci/plugins/pipeline/modeldefinition/UpgradeTest.java new file mode 100644 index 000000000..97943294b --- /dev/null +++ b/pipeline-model-definition/src/test/java/org/jenkinsci/plugins/pipeline/modeldefinition/UpgradeTest.java @@ -0,0 +1,126 @@ +/* + * The MIT License + * + * Copyright 2025 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.pipeline.modeldefinition; + +import hudson.model.ParametersAction; +import hudson.model.ParametersDefinitionProperty; +import hudson.model.StringParameterDefinition; +import hudson.model.StringParameterValue; +import hudson.util.VersionNumber; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.List; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.InboundAgentRule; +import org.jvnet.hudson.test.PrefixedOutputStream; +import org.jvnet.hudson.test.RealJenkinsRule; +import org.jvnet.hudson.test.TailLog; + +public final class UpgradeTest { + + @Rule public RealJenkinsRule rr = new RealJenkinsRule(); + @Rule public InboundAgentRule iar = new InboundAgentRule(); + + @Test public void deserLabelScript() throws Throwable { + var plugins = rr.getHome().toPath().resolve("plugins"); + Files.move(plugins.resolve("pipeline-stage-tags-metadata.jpi"), plugins.resolve("pipeline-stage-tags-metadata.jpi.orig")); + Files.move(plugins.resolve("pipeline-model-api.jpi"), plugins.resolve("pipeline-model-api.jpi.orig")); + Files.move(plugins.resolve("pipeline-model-extensions.jpi"), plugins.resolve("pipeline-model-extensions.jpi.orig")); + Files.move(plugins.resolve("pipeline-model-definition.jpl"), plugins.resolve("pipeline-model-definition.jpl.orig")); + for (var plugin : List.of("pipeline-stage-tags-metadata", "pipeline-model-api", "pipeline-model-extensions", "pipeline-model-definition")) { + try (var is = UpgradeTest.class.getResourceAsStream("/old-releases/" + plugin + ".hpi")) { + Files.copy(is, plugins.resolve(plugin + ".jpi")); + } + } + rr.startJenkins(); + var oldVersion = rr.call(r -> { + var version = r.jenkins.pluginManager.getPlugin("pipeline-model-definition").getVersionNumber(); + assertThat(r.jenkins.pluginManager.getPlugin("pipeline-model-extensions").getVersionNumber(), is(version)); + return version.toString(); + }); + iar.createAgent(rr, InboundAgentRule.Options.newBuilder().name("remote").color(PrefixedOutputStream.Color.YELLOW).build()); + try (var tail = new TailLog(rr, "p", 1).withColor(PrefixedOutputStream.Color.MAGENTA)) { + rr.run(r -> { + var p = r.createProject(WorkflowJob.class, "p"); + p.addProperty(new ParametersDefinitionProperty(new StringParameterDefinition("PROCEED"))); + p.setDefinition(new CpsFlowDefinition( + """ + pipeline { + agent { + label 'remote' + } + stages { + stage('all') { + steps { + sh ''' + set +x + echo waiting for a signal at "$PROCEED" + until [ -f "$PROCEED" ] + do + date + sleep 5 + done + ''' + } + } + } + } + """, true)); + var b = p.scheduleBuild2(0, new ParametersAction(new StringParameterValue("PROCEED", new File(r.jenkins.root, "proceed").getAbsolutePath()))).waitForStart(); + r.waitForMessage("waiting for a signal", b); + }); + rr.stopJenkins(); + Files.move(plugins.resolve("pipeline-stage-tags-metadata.jpi.orig"), plugins.resolve("pipeline-stage-tags-metadata.jpi"), StandardCopyOption.REPLACE_EXISTING); + Files.move(plugins.resolve("pipeline-model-api.jpi.orig"), plugins.resolve("pipeline-model-api.jpi"), StandardCopyOption.REPLACE_EXISTING); + Files.move(plugins.resolve("pipeline-model-extensions.jpi.orig"), plugins.resolve("pipeline-model-extensions.jpi"), StandardCopyOption.REPLACE_EXISTING); + Files.move(plugins.resolve("pipeline-model-definition.jpl.orig"), plugins.resolve("pipeline-model-definition.jpl")); + Files.delete(plugins.resolve("pipeline-model-definition.jpi")); + rr.then(r -> { + var newVersion = r.jenkins.pluginManager.getPlugin("pipeline-model-definition").getVersionNumber(); + assertThat(newVersion, greaterThan(new VersionNumber(oldVersion))); + assertThat(r.jenkins.pluginManager.getPlugin("pipeline-model-extensions").getVersionNumber(), is(newVersion)); + System.err.println("Upgraded from " + oldVersion + " to " + newVersion); + var p = r.jenkins.getItemByFullName("p", WorkflowJob.class); + var b = p.getBuildByNumber(1); + r.waitForMessage("Resuming build at ", b); + r.waitForMessage("Ready to run at ", b); + var proceed = Path.of(((StringParameterValue) b.getAction(ParametersAction.class).getParameter("PROCEED")).getValue()); + System.err.println("Touching " + proceed); + Files.writeString(proceed, "go"); + r.assertBuildStatusSuccess(r.waitForCompletion(b)); + }); + tail.waitForCompletion(); + } + } + +} From e74f76848b8a05decc8457bfa27537d108615927 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 28 Feb 2025 18:47:41 -0500 Subject: [PATCH 2/8] Seem to have a solution at least for base agent types --- .../pipeline/modeldefinition/Upgrade.java | 117 +++ .../resources/compat/CheckoutScript.groovy | 82 ++ .../main/resources/compat/LabelScript.groovy | 56 ++ .../resources/compat/ModelInterpreter.groovy | 869 ++++++++++++++++++ .../pipeline/modeldefinition/UpgradeTest.java | 3 +- 5 files changed, 1126 insertions(+), 1 deletion(-) create mode 100644 pipeline-model-definition/src/main/java/org/jenkinsci/plugins/pipeline/modeldefinition/Upgrade.java create mode 100644 pipeline-model-definition/src/main/resources/compat/CheckoutScript.groovy create mode 100644 pipeline-model-definition/src/main/resources/compat/LabelScript.groovy create mode 100644 pipeline-model-definition/src/main/resources/compat/ModelInterpreter.groovy diff --git a/pipeline-model-definition/src/main/java/org/jenkinsci/plugins/pipeline/modeldefinition/Upgrade.java b/pipeline-model-definition/src/main/java/org/jenkinsci/plugins/pipeline/modeldefinition/Upgrade.java new file mode 100644 index 000000000..b7693f152 --- /dev/null +++ b/pipeline-model-definition/src/main/java/org/jenkinsci/plugins/pipeline/modeldefinition/Upgrade.java @@ -0,0 +1,117 @@ +/* + * The MIT License + * + * Copyright 2025 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.pipeline.modeldefinition; + +import groovy.lang.GroovyShell; +import hudson.Extension; +import hudson.util.VersionNumber; +import java.io.File; +import java.net.URL; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.xml.parsers.SAXParserFactory; +import org.jenkinsci.plugins.pipeline.modeldefinition.agent.DeclarativeAgentScript2; +import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution; +import org.jenkinsci.plugins.workflow.cps.GroovyShellDecorator; +import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * Detects old builds (predating for example {@link DeclarativeAgentScript2}) and loads old Groovy resources to match. + */ +@Extension public final class Upgrade extends GroovyShellDecorator { + + private static final Logger LOGGER = Logger.getLogger(Upgrade.class.getName()); + + @Override public void configureShell(CpsFlowExecution context, GroovyShell shell) { + if (context == null) { + return; + } + var owner = context.getOwner(); + if (owner == null) { + return; + } + try { + if (!isOld(owner)) { + LOGGER.fine(() -> context + " does not seem to be old"); + return; + } + } catch (Exception x) { + LOGGER.log(Level.WARNING, "failed to check " + context, x); + return; + } + var cl = shell.getClassLoader(); + var base = cl.getResourceLoader(); + cl.setResourceLoader(filename -> { + URL url; + // TODO convert into an extension point for benefit of other plugins extending DeclarativeAgentScript + if (filename.equals("org.jenkinsci.plugins.pipeline.modeldefinition.agent.CheckoutScript")) { + url = Upgrade.class.getResource("/compat/CheckoutScript.groovy"); + } else if (filename.equals("org.jenkinsci.plugins.pipeline.modeldefinition.agent.impl.LabelScript")) { + url = Upgrade.class.getResource("/compat/LabelScript.groovy"); + } else if (filename.equals("org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter")) { + url = Upgrade.class.getResource("/compat/ModelInterpreter.groovy"); + } else { + url = base.loadGroovySource(filename); + } + if (url != null) { + LOGGER.fine(() -> "for " + context + " loading " + filename + " ⇒ " + url); + } + return url; + }); + } + + @Override public GroovyShellDecorator forTrusted() { + return this; + } + + private static boolean isOld(FlowExecutionOwner owner) throws Exception { + var rootDir = owner.getRootDir(); + var buildXml = rootDir.toPath().resolve("build.xml"); + var parser = SAXParserFactory.newDefaultInstance().newSAXParser(); + // TODO disable entity includes etc. + var old = new AtomicBoolean(); + parser.parse(new File(rootDir, "build.xml"), new DefaultHandler() { + @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + var plugin = attributes.getValue("plugin"); + if (plugin != null) { + int at = plugin.indexOf('@'); + if (at != -1 && plugin.substring(0, at).equals("pipeline-model-definition")) { + var version = new VersionNumber(plugin.substring(at + 1)); + LOGGER.fine(() -> "got " + version + " off " + qName); + if (version.isOlderThan(new VersionNumber("2.2234"))) { + old.set(true); + } + } + } + } + }); + return old.get(); + } + +} diff --git a/pipeline-model-definition/src/main/resources/compat/CheckoutScript.groovy b/pipeline-model-definition/src/main/resources/compat/CheckoutScript.groovy new file mode 100644 index 000000000..8c0d5de63 --- /dev/null +++ b/pipeline-model-definition/src/main/resources/compat/CheckoutScript.groovy @@ -0,0 +1,82 @@ +/* + * The MIT License + * + * Copyright (c) 2017, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + + +package org.jenkinsci.plugins.pipeline.modeldefinition.agent + +import org.jenkinsci.plugins.pipeline.modeldefinition.SyntheticStageNames +import org.jenkinsci.plugins.workflow.cps.CpsScript + +class CheckoutScript implements Serializable { + + static Closure doCheckout(CpsScript script, DeclarativeAgent agent, String customWorkspace = null, Closure body) { + return { + if (customWorkspace) { + script.ws(customWorkspace) { + checkoutAndRun(script, agent, body).call() + } + } else { + checkoutAndRun(script, agent, body).call() + } + } + } + + private static Closure checkoutAndRun(CpsScript script, DeclarativeAgent agent, Closure body) { + return { + def checkoutMap = [:] + + if (agent.isDoCheckout() && agent.hasScmContext(script)) { + String subDir = agent.subdirectory + if (subDir != null && subDir != "") { + script.dir(subDir) { + checkoutMap.putAll(performCheckout(script, agent)) + } + } else { + checkoutMap.putAll(performCheckout(script, agent)) + } + } + if (checkoutMap) { + script.withEnv(checkoutMap.collect { k, v -> "${k}=${v}" }) { + body.call() + } + } else { + body.call() + } + } + } + + private static Map performCheckout(CpsScript script, DeclarativeAgent agent) { + def checkoutMap = [:] + if (!agent.inStage) { + script.stage(SyntheticStageNames.checkout()) { + checkoutMap.putAll(script.checkout(script.scm) ?: [:]) + } + } else { + // No stage when we're in a nested stage already + checkoutMap.putAll(script.checkout(script.scm) ?: [:]) + } + + return checkoutMap + } +} diff --git a/pipeline-model-definition/src/main/resources/compat/LabelScript.groovy b/pipeline-model-definition/src/main/resources/compat/LabelScript.groovy new file mode 100644 index 000000000..745b6021d --- /dev/null +++ b/pipeline-model-definition/src/main/resources/compat/LabelScript.groovy @@ -0,0 +1,56 @@ +/* + * The MIT License + * + * Copyright (c) 2016, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + + +package org.jenkinsci.plugins.pipeline.modeldefinition.agent.impl + + +import org.jenkinsci.plugins.pipeline.modeldefinition.agent.CheckoutScript +import org.jenkinsci.plugins.pipeline.modeldefinition.agent.DeclarativeAgentScript +import org.jenkinsci.plugins.workflow.cps.CpsScript + +class LabelScript extends DeclarativeAgentScript