Skip to content
This repository has been archived by the owner on Dec 15, 2021. It is now read-only.

Commit

Permalink
Browse files Browse the repository at this point in the history
[FIXED JENKINS-30531] Set SCMRevisionAction when we do a build, so cl…
…ient code can get access to this record.

Has the happy side effect of fixing JENKINS-30222: scm as a global variable can be used unqualified from library code.
  • Loading branch information
jglick committed Nov 3, 2015
1 parent 59e6cf3 commit 4e9bd11
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 52 deletions.
Expand Up @@ -126,7 +126,7 @@ public Object getProperty(String property) {
return super.getProperty(property);
}

@CheckForNull Run<?,?> $build() throws IOException {
public @CheckForNull Run<?,?> $build() throws IOException {
FlowExecutionOwner owner = execution.getOwner();
Queue.Executable qe = owner.getExecutable();
if (qe instanceof Run) {
Expand Down
Expand Up @@ -24,52 +24,37 @@

package org.jenkinsci.plugins.workflow.multibranch;

import com.thoughtworks.xstream.converters.Converter;
import groovy.lang.GroovyShell;
import hudson.Extension;
import hudson.model.Action;
import hudson.model.ItemGroup;
import hudson.model.Queue;
import hudson.model.TaskListener;
import hudson.scm.SCM;
import hudson.util.DescribableList;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import jenkins.branch.Branch;
import jenkins.scm.api.SCMHead;
import jenkins.scm.api.SCMHeadObserver;
import jenkins.scm.api.SCMRevision;
import jenkins.scm.api.SCMRevisionAction;
import jenkins.scm.api.SCMSource;
import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution;
import org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition;
import org.jenkinsci.plugins.workflow.cps.GroovyShellDecorator;
import org.jenkinsci.plugins.workflow.flow.FlowDefinition;
import org.jenkinsci.plugins.workflow.flow.FlowDefinitionDescriptor;
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.jenkinsci.plugins.workflow.pickles.Pickle;
import org.jenkinsci.plugins.workflow.support.pickles.SingleTypedPickleFactory;
import org.jenkinsci.plugins.workflow.support.pickles.XStreamPickle;

/**
* Adds an {@code scm} global variable to the script.
* This makes it possible to run {@code checkout scm} to get your project sources in the right branch.
* Checks out the desired version of {@link WorkflowMultiBranchProject#SCRIPT}.
*/
class SCMBinder extends FlowDefinition {

private static final Map<CpsFlowExecution,SCM> scms = Collections.synchronizedMap(new WeakHashMap<CpsFlowExecution,SCM>());

@Override public FlowExecution create(FlowExecutionOwner handle, TaskListener listener, List<? extends Action> actions) throws Exception {
Queue.Executable exec = handle.getExecutable();
if (!(exec instanceof WorkflowRun)) {
throw new IllegalStateException("inappropriate context");
}
WorkflowJob job = ((WorkflowRun) exec).getParent();
WorkflowRun build = (WorkflowRun) exec;
WorkflowJob job = build.getParent();
BranchJobProperty property = job.getProperty(BranchJobProperty.class);
if (property == null) {
throw new IllegalStateException("inappropriate context");
Expand All @@ -88,14 +73,13 @@ class SCMBinder extends FlowDefinition {
SCM scm;
if (tip != null) {
scm = scmSource.build(head, tip);
build.addAction(new SCMRevisionAction(tip));
} else {
listener.error("Could not determine exact tip revision of " + branch.getName() + "; falling back to nondeterministic checkout");
// Build might fail later anyway, but reason should become clear: for example, branch was deleted before indexing could run.
scm = branch.getScm();
}
CpsFlowExecution execution = new CpsScmFlowDefinition(scm, WorkflowMultiBranchProject.SCRIPT).create(handle, listener, actions);
scms.put(execution, scm); // stash for later
return execution;
return new CpsScmFlowDefinition(scm, WorkflowMultiBranchProject.SCRIPT).create(handle, listener, actions);
}

// Not registered as an @Extension, but in case someone calls it:
Expand All @@ -107,31 +91,4 @@ class SCMBinder extends FlowDefinition {
};
}

// Note that this cannot be a GlobalVariable, since that must always be set.
@Extension public static class Decorator extends GroovyShellDecorator {

@Override public void configureShell(CpsFlowExecution context, GroovyShell shell) {
if (context != null) {
SCM scm = scms.remove(context);
if (scm != null) {
shell.setVariable("scm", scm);
}
}
}

}

/**
* Ensures that {@code scm} is saved in its XML representation.
* Necessary for {@code GitSCM} which is marked {@link Serializable}
* yet includes a {@link DescribableList} which relies on a custom {@link Converter}.
*/
@Extension public static class Pickler extends SingleTypedPickleFactory<SCM> {

@Override protected Pickle pickle(SCM scm) {
return new XStreamPickle(scm);
}

}

}
@@ -0,0 +1,99 @@
/*
* The MIT License
*
* Copyright 2015 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.workflow.multibranch;

import com.thoughtworks.xstream.converters.Converter;
import hudson.AbortException;
import hudson.Extension;
import hudson.model.ItemGroup;
import hudson.model.Job;
import hudson.model.Run;
import hudson.scm.SCM;
import hudson.util.DescribableList;
import java.io.Serializable;
import jenkins.branch.Branch;
import jenkins.scm.api.SCMRevisionAction;
import jenkins.scm.api.SCMSource;
import org.jenkinsci.plugins.workflow.cps.CpsScript;
import org.jenkinsci.plugins.workflow.cps.GlobalVariable;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.jenkinsci.plugins.workflow.pickles.Pickle;
import org.jenkinsci.plugins.workflow.support.pickles.SingleTypedPickleFactory;
import org.jenkinsci.plugins.workflow.support.pickles.XStreamPickle;

/**
* Adds an {@code scm} global variable to the script.
* This makes it possible to run {@code checkout scm} to get your project sources in the right branch.
*/
@Extension public class SCMVar extends GlobalVariable {

@Override public String getName() {
return "scm";
}

@Override public SCM getValue(CpsScript script) throws Exception {
Run<?,?> build = script.$build();
// TODO some code overlap with SCMBinder.create, but not obvious how to factor out common parts
if (!(build instanceof WorkflowRun)) {
throw new AbortException("not available outside a workflow build");
}
Job<?,?> job = build.getParent();
BranchJobProperty property = job.getProperty(BranchJobProperty.class);
if (property == null) {
throw new IllegalStateException("inappropriate context");
}
Branch branch = property.getBranch();
SCMRevisionAction revisionAction = build.getAction(SCMRevisionAction.class);
if (revisionAction != null) {
ItemGroup<?> parent = job.getParent();
if (!(parent instanceof WorkflowMultiBranchProject)) {
throw new IllegalStateException("inappropriate context");
}
SCMSource scmSource = ((WorkflowMultiBranchProject) parent).getSCMSource(branch.getSourceId());
if (scmSource == null) {
throw new IllegalStateException(branch.getSourceId() + " not found");
}
return scmSource.build(branch.getHead(), revisionAction.getRevision());
} else {
return branch.getScm();
}
}

/**
* Ensures that {@code scm} is saved in its XML representation.
* Necessary for {@code GitSCM} which is marked {@link Serializable}
* yet includes a {@link DescribableList} which relies on a custom {@link Converter}.
* Note that a script which merely calls {@code checkout scm}, even after a restart, does not rely on this;
* but one which saves {@code scm} somewhere and uses it later would.
*/
@Extension public static class Pickler extends SingleTypedPickleFactory<SCM> {

@Override protected Pickle pickle(SCM scm) {
return new XStreamPickle(scm);
}

}

}
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
The MIT License
Copyright 2015 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.
-->

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core">
Represents the SCM configuration in a multibranch project build.
Use <code>checkout scm</code> to check out sources matching <code>Jenkinsfile</code>.
</j:jelly>
Expand Up @@ -28,6 +28,7 @@
import hudson.Util;
import hudson.model.Result;
import hudson.model.RootAction;
import hudson.plugins.git.util.BuildData;
import hudson.plugins.mercurial.MercurialInstallation;
import hudson.plugins.mercurial.MercurialSCMSource;
import hudson.tools.ToolProperty;
Expand All @@ -36,7 +37,10 @@
import jenkins.branch.BranchProperty;
import jenkins.branch.BranchSource;
import jenkins.branch.DefaultBranchPropertyStrategy;
import jenkins.plugins.git.AbstractGitSCMSource;
import jenkins.plugins.git.GitSCMSource;
import jenkins.scm.api.SCMRevision;
import jenkins.scm.api.SCMRevisionAction;
import jenkins.scm.impl.subversion.SubversionSCMSource;
import org.apache.commons.io.FileUtils;
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval;
Expand All @@ -53,6 +57,7 @@
import org.junit.Rule;
import org.junit.runners.model.Statement;
import org.jvnet.hudson.test.BuildWatcher;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.RestartableJenkinsRule;

public class SCMBinderTest {
Expand All @@ -63,11 +68,12 @@ public class SCMBinderTest {
@Rule public SubversionSampleRepoRule sampleSvnRepo = new SubversionSampleRepoRule();
@Rule public MercurialSampleRepoRule sampleHgRepo = new MercurialSampleRepoRule();

// TODO move to SCMVarTest
@Test public void scmPickle() throws Exception {
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
sampleGitRepo.init();
sampleGitRepo.write("Jenkinsfile", "semaphore 'wait'; node {checkout scm; echo readFile('file')}");
sampleGitRepo.write("Jenkinsfile", "def _scm = scm; semaphore 'wait'; node {checkout _scm; echo readFile('file')}");
sampleGitRepo.write("file", "initial content");
sampleGitRepo.git("add", "Jenkinsfile");
sampleGitRepo.git("commit", "--all", "--message=flow");
Expand All @@ -88,6 +94,7 @@ public class SCMBinderTest {
assertNotNull(b1);
assertEquals(1, b1.getNumber());
story.j.assertLogContains("initial content", story.j.waitForCompletion(b1));
assertRevisionAction(b1);
}
});
}
Expand All @@ -110,6 +117,7 @@ public class SCMBinderTest {
WorkflowRun b1 = p.getLastBuild();
assertNotNull(b1);
assertEquals(1, b1.getNumber());
assertRevisionAction(b1);
sampleGitRepo.write("Jenkinsfile", "node {checkout scm; echo readFile('file').toUpperCase()}");
sa.approveSignature("method java.lang.String toUpperCase");
sampleGitRepo.write("file", "subsequent content");
Expand All @@ -119,10 +127,21 @@ public class SCMBinderTest {
assertEquals(2, b2.getNumber());
story.j.assertLogContains("initial content", story.j.waitForCompletion(b1));
story.j.assertLogContains("SUBSEQUENT CONTENT", b2);
assertRevisionAction(b2);
}
});
}

private static void assertRevisionAction(WorkflowRun build) {
BuildData data = build.getAction(BuildData.class);
assertNotNull(data);
SCMRevisionAction revisionAction = build.getAction(SCMRevisionAction.class);
assertNotNull(revisionAction);
SCMRevision revision = revisionAction.getRevision();
assertEquals(AbstractGitSCMSource.SCMRevisionImpl.class, revision.getClass());
assertEquals(data.lastBuild.marked.getSha1().getName(), ((AbstractGitSCMSource.SCMRevisionImpl) revision).getHash());
}

@Test public void exactRevisionSubversion() throws Exception {
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
Expand Down Expand Up @@ -194,6 +213,8 @@ public class SCMBinderTest {
});
}

// TODO move to SCMVarTest
@Issue("JENKINS-30222")
@Test public void globalVariable() {
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
Expand All @@ -210,7 +231,7 @@ public class SCMBinderTest {
" body.delegate = config\n" +
" body()\n" +
" node {\n" +
" checkout body.owner.scm\n" +
" checkout scm\n" +
" echo \"loaded ${readFile config.file}\"\n" +
" }\n" +
"}\n");
Expand Down

0 comments on commit 4e9bd11

Please sign in to comment.