Skip to content

Commit

Permalink
Merge pull request #12 from stephenc/jenkins-38887
Browse files Browse the repository at this point in the history
[JENKINS-38887] Allow triggering ComputedFolders and other Queue.Task implementations
  • Loading branch information
stephenc committed Mar 10, 2017
2 parents 9a3db1e + 4512e19 commit bd29aea
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 33 deletions.
20 changes: 20 additions & 0 deletions pom.xml
Expand Up @@ -39,6 +39,7 @@
</pluginRepositories>
<properties>
<jenkins.version>1.642.3</jenkins.version>
<scm-api.version>2.0.7</scm-api.version>
</properties>
<dependencies>
<dependency>
Expand Down Expand Up @@ -94,5 +95,24 @@
<version>2.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>branch-api</artifactId>
<version>2.0.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>scm-api</artifactId>
<version>${scm-api.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>scm-api</artifactId>
<version>${scm-api.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Expand Up @@ -2,18 +2,24 @@

import hudson.Extension;
import hudson.model.AutoCompletionCandidates;
import hudson.model.Describable;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.ItemVisitor;
import hudson.model.Job;
import hudson.model.ParameterDefinition;
import hudson.model.ParameterValue;
import hudson.model.ParametersDefinitionProperty;
import hudson.model.Queue;
import hudson.model.TopLevelItem;
import hudson.util.FormValidation;
import java.util.ArrayList;
import java.util.List;
import jenkins.model.Jenkins;
import jenkins.model.ParameterizedJobMixIn;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl;
import org.jenkinsci.plugins.workflow.steps.Step;
Expand Down Expand Up @@ -126,8 +132,61 @@ public String getDisplayName() {
return "Build a job";
}

public AutoCompletionCandidates doAutoCompleteJob(@AncestorInPath ItemGroup<?> context, @QueryParameter String value) {
return AutoCompletionCandidates.ofJobNames(ParameterizedJobMixIn.ParameterizedJob.class, value, context);
public AutoCompletionCandidates doAutoCompleteJob(@AncestorInPath ItemGroup<?> container, @QueryParameter final String value) {
// TODO remove code copy&pasted from AutoCompletionCandidates.ofJobNames when it supports testing outside Item bound
final AutoCompletionCandidates candidates = new AutoCompletionCandidates();
class Visitor extends ItemVisitor {
String prefix;

Visitor(String prefix) {
this.prefix = prefix;
}

@Override
public void onItem(Item i) {
String n = contextualNameOf(i);
if ((n.startsWith(value) || value.startsWith(n))
// 'foobar' is a valid candidate if the current value is 'foo'.
// Also, we need to visit 'foo' if the current value is 'foo/bar'
&& (value.length() > n.length() || !n.substring(value.length()).contains("/"))
// but 'foobar/zot' isn't if the current value is 'foo'
// we'll first show 'foobar' and then wait for the user to type '/' to show the rest
&& i.hasPermission(Item.READ)
// and read permission required
) {
if (i instanceof Queue.Task && n.startsWith(value))
candidates.add(n);

// recurse
String oldPrefix = prefix;
prefix = n;
super.onItem(i);
prefix = oldPrefix;
}
}

private String contextualNameOf(Item i) {
if (prefix.endsWith("/") || prefix.length() == 0)
return prefix + i.getName();
else
return prefix + '/' + i.getName();
}
}

if (container == null || container == Jenkins.getInstance()) {
new Visitor("").onItemGroup(Jenkins.getInstance());
} else {
new Visitor("").onItemGroup(container);
if (value.startsWith("/"))
new Visitor("/").onItemGroup(Jenkins.getInstance());

for (StringBuilder p = new StringBuilder("../"); value.startsWith(p.toString()); p .append("../")) {
container = ((Item) container).getParent();
new Visitor(p.toString()).onItemGroup(container);
}
}
return candidates;
// END of copy&paste
}

@Restricted(DoNotUse.class) // for use from config.jelly
Expand All @@ -143,5 +202,36 @@ public FormValidation doCheckPropagate(@QueryParameter boolean value, @QueryPara
return FormValidation.ok();
}

public FormValidation doCheckWait(@AncestorInPath ItemGroup<?> context, @QueryParameter boolean value, @QueryParameter String job) {
if (!value) {
return FormValidation.ok();
}
Item item = Jenkins.getActiveInstance().getItem(job, context, Item.class);
if (item == null) {
return FormValidation.ok();
}
if (item instanceof Job) {
return FormValidation.ok();
}
return FormValidation.error(Messages.BuildTriggerStep_no_wait_for_non_jobs());
}

public FormValidation doCheckJob(@AncestorInPath ItemGroup<?> context, @QueryParameter String value) {
if (StringUtils.isBlank(value)) {
return FormValidation.warning(Messages.BuildTriggerStep_no_job_configured());
}
Item item = Jenkins.getActiveInstance().getItem(value, context, Item.class);
if (item == null) {
return FormValidation.error(Messages.BuildTriggerStep_cannot_find(value));
}
if (item instanceof Queue.Task) {
return FormValidation.ok();
}
if (item instanceof Describable) {
return FormValidation.error(Messages.BuildTriggerStep_unsupported(((Describable)item).getDescriptor().getDisplayName()));
}
return FormValidation.error(Messages.BuildTriggerStep_unsupported(item.getClass().getName()));
}

}
}
Expand Up @@ -9,7 +9,9 @@
import hudson.model.Cause;
import hudson.model.CauseAction;
import hudson.model.Computer;
import hudson.model.Describable;
import hudson.model.Executor;
import hudson.model.Item;
import hudson.model.Job;
import hudson.model.ParameterDefinition;
import hudson.model.ParameterValue;
Expand All @@ -20,7 +22,10 @@
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.queue.QueueTaskFuture;
import hudson.model.queue.ScheduleResult;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
Expand Down Expand Up @@ -53,39 +58,86 @@ public class BuildTriggerStepExecution extends AbstractStepExecutionImpl {
@Override
public boolean start() throws Exception {
String job = step.getJob();
final ParameterizedJobMixIn.ParameterizedJob project = Jenkins.getActiveInstance().getItem(job, invokingRun.getParent(), ParameterizedJobMixIn.ParameterizedJob.class);
if (project == null) {
throw new AbortException("No parameterized job named " + job + " found");
Item item = Jenkins.getActiveInstance().getItem(job, invokingRun.getParent(), Item.class);
if (item == null) {
throw new AbortException("No item named " + job + " found");
}
listener.getLogger().println("Scheduling project: " + ModelHyperlinkNote.encodeTo(project));

node.addAction(new LabelAction(Messages.BuildTriggerStepExecution_building_(project.getFullDisplayName())));
List<Action> actions = new ArrayList<>();
if (step.getWait()) {
StepContext context = getContext();
actions.add(new BuildTriggerAction(context, step.isPropagate()));
LOGGER.log(Level.FINER, "scheduling a build of {0} from {1}", new Object[] {project, context});
}
actions.add(new CauseAction(new Cause.UpstreamCause(invokingRun)));
List<ParameterValue> parameters = step.getParameters();
if (parameters != null) {
parameters = completeDefaultParameters(parameters, (Job) project);
actions.add(new ParametersAction(parameters));
if (step.getWait() && !(item instanceof Job)) {
// TODO find some way of allowing ComputedFolders to hook into the listener code
throw new AbortException("Waiting for non-job items is not supported");
}
Integer quietPeriod = step.getQuietPeriod();
// TODO use new convenience method in 1.621
if (quietPeriod == null) {
quietPeriod = project.getQuietPeriod();
}
QueueTaskFuture<?> f = new ParameterizedJobMixIn() {
@Override protected Job asJob() {
return (Job) project;
if (item instanceof ParameterizedJobMixIn.ParameterizedJob) {
final ParameterizedJobMixIn.ParameterizedJob project = (ParameterizedJobMixIn.ParameterizedJob) item;
listener.getLogger().println("Scheduling project: " + ModelHyperlinkNote.encodeTo(project));

node.addAction(new LabelAction(Messages.BuildTriggerStepExecution_building_(project.getFullDisplayName())));
List<Action> actions = new ArrayList<>();
if (step.getWait()) {
StepContext context = getContext();
actions.add(new BuildTriggerAction(context, step.isPropagate()));
LOGGER.log(Level.FINER, "scheduling a build of {0} from {1}", new Object[]{project, context});
}
actions.add(new CauseAction(new Cause.UpstreamCause(invokingRun)));
List<ParameterValue> parameters = step.getParameters();
if (parameters != null) {
parameters = completeDefaultParameters(parameters, (Job) project);
actions.add(new ParametersAction(parameters));
}
Integer quietPeriod = step.getQuietPeriod();
// TODO use new convenience method in 1.621
if (quietPeriod == null) {
quietPeriod = project.getQuietPeriod();
}
QueueTaskFuture<?> f = new ParameterizedJobMixIn() {
@Override
protected Job asJob() {
return (Job) project;
}
}.scheduleBuild2(quietPeriod, actions.toArray(new Action[actions.size()]));
if (f == null) {
throw new AbortException("Failed to trigger build of " + project.getFullName());
}
}.scheduleBuild2(quietPeriod, actions.toArray(new Action[actions.size()]));
if (f == null) {
throw new AbortException("Failed to trigger build of " + project.getFullName());
} else if (item instanceof Queue.Task){
if (step.getParameters() != null && !step.getParameters().isEmpty()) {
throw new AbortException("Item type does not support parameters");
}
Queue.Task task = (Queue.Task) item;
listener.getLogger().println("Scheduling item: " + ModelHyperlinkNote.encodeTo(item));
node.addAction(new LabelAction(Messages.BuildTriggerStepExecution_building_(task.getFullDisplayName())));
List<Action> actions = new ArrayList<>();
if (step.getWait()) {
StepContext context = getContext();
actions.add(new BuildTriggerAction(context, step.isPropagate()));
LOGGER.log(Level.FINER, "scheduling a build of {0} from {1}", new Object[]{task, context});
}
actions.add(new CauseAction(new Cause.UpstreamCause(invokingRun)));
Integer quietPeriod = step.getQuietPeriod();
if (quietPeriod == null) {
try {
Method getQuietPeriod = task.getClass().getMethod("getQuietPeriod");
if (getQuietPeriod.getReturnType().equals(int.class)) {
quietPeriod = (Integer) getQuietPeriod.invoke(task);
}
} catch (NoSuchMethodException e) {
// ignore, best effort only
} catch (IllegalAccessError | IllegalArgumentException | InvocationTargetException e) {
LOGGER.log(Level.WARNING, "Could not determine quiet period of " + item.getFullName(), e);
}
}
if (quietPeriod == null) {
quietPeriod = Jenkins.getActiveInstance().getQuietPeriod();
}
ScheduleResult scheduleResult = Jenkins.getActiveInstance().getQueue().schedule2(task, quietPeriod,actions);
if (scheduleResult.isRefused()) {
throw new AbortException("Failed to trigger build of " + item.getFullName());
}
} else {
throw new AbortException("The item named " + job + " is a "
+ (item instanceof Describable
? ((Describable) item).getDescriptor().getDisplayName()
: item.getClass().getName())
+ " which is not something that can be built");
}

if (step.getWait()) {
return false;
} else {
Expand Down
Expand Up @@ -7,8 +7,8 @@ l.ajax {
// Cf. BuildTriggerStepExecution:
def contextName = request.getParameter('context')
def context = contextName != null ? app.getItemByFullName(contextName) : null
def job = app.getItem(jobName, (hudson.model.Item) context, jenkins.model.ParameterizedJobMixIn.ParameterizedJob)
if (job != null) {
def job = app.getItem(jobName, (hudson.model.Item) context, hudson.model.Item)
if (job instanceof jenkins.model.ParameterizedJobMixIn.ParameterizedJob) {
def pdp = job.getProperty(hudson.model.ParametersDefinitionProperty)
if (pdp != null) {
// Cf. ParametersDefinitionProperty/index.jelly:
Expand All @@ -23,6 +23,10 @@ l.ajax {
} else {
text("${job.fullDisplayName} is not parameterized")
}
} else if (job instanceof hudson.model.Queue.Task) {
text("${job.fullDisplayName} is not parameterized")
} else if (job != null) {
text("${job.fullDisplayName} is not buildable")
} else {
text("no such job ${jobName}")
}
Expand Down
Expand Up @@ -23,4 +23,8 @@
BuildTriggerStep.explicitly_disabling_both_propagate_and_wait=\
Explicitly disabling both <code>propagate</code> and <code>wait</code> is redundant, since <code>propagate</code> is ignored when <code>wait</code> is disabled. \
For brevity, leave <code>propagate</code> at its default.
BuildTriggerStep.no_wait_for_non_jobs=Waiting for non-job items is not supported
BuildTriggerStep.no_job_configured=No job configured
BuildTriggerStep.cannot_find=No such job {0}
BuildTriggerStep.unsupported=Building a {0} is not supported
BuildTriggerStepExecution.building_=Building {0}

0 comments on commit bd29aea

Please sign in to comment.