/* * The MIT License * * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Brian Westrich, Martin Eigenbrodt * * 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 hudson.tasks; import hudson.Launcher; import hudson.Extension; import hudson.Util; import hudson.model.AutoCompletionCandidates; import hudson.security.AccessControlled; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Action; import hudson.model.BuildListener; import hudson.model.DependecyDeclarer; import hudson.model.DependencyGraph; import hudson.model.DependencyGraph.Dependency; import hudson.model.Hudson; import hudson.model.Item; import hudson.model.Items; import hudson.model.Project; import hudson.model.Result; import hudson.model.Run; import hudson.model.Cause.UpstreamCause; import hudson.model.TaskListener; import hudson.model.listeners.ItemListener; import hudson.util.AutoCompleteSeeder; import hudson.util.FormValidation; import net.sf.json.JSONObject; import org.apache.commons.lang3.StringUtils; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.QueryParameter; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.StringTokenizer; import java.util.logging.Level; import java.util.logging.Logger; /** * Triggers builds of other projects. * * <p> * Despite what the name suggests, this class doesn't actually trigger other jobs * as a part of {@link #perform} method. Its main job is to simply augument * {@link DependencyGraph}. Jobs are responsible for triggering downstream jobs * on its own, because dependencies may come from other sources. * * <p> * This class, however, does provide the {@link #execute(AbstractBuild, BuildListener, BuildTrigger)} * method as a convenience method to invoke downstream builds. * * @author Kohsuke Kawaguchi */ public class BuildTrigger extends Recorder implements DependecyDeclarer { /** * Comma-separated list of other projects to be scheduled. */ private String childProjects; /** * Threshold status to trigger other builds. * * For compatibility reasons, this field could be null, in which case * it should read as "SUCCESS". */ private final Result threshold; @DataBoundConstructor public BuildTrigger(String childProjects, boolean evenIfUnstable) { this(childProjects,evenIfUnstable ? Result.UNSTABLE : Result.SUCCESS); } public BuildTrigger(String childProjects, Result threshold) { if(childProjects==null) throw new IllegalArgumentException(); this.childProjects = childProjects; this.threshold = threshold; } public BuildTrigger(List<AbstractProject> childProjects, Result threshold) { this((Collection<AbstractProject>)childProjects,threshold); } public BuildTrigger(Collection<? extends AbstractProject> childProjects, Result threshold) { this(Items.toNameList(childProjects),threshold); } public String getChildProjectsValue() { return childProjects; } public Result getThreshold() { if(threshold==null) return Result.SUCCESS; else return threshold; } public List<AbstractProject> getChildProjects() { return Items.fromNameList(childProjects,AbstractProject.class); } public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } /** * Checks if this trigger has the exact same set of children as the given list. */ public boolean hasSame(Collection<? extends AbstractProject> projects) { List<AbstractProject> children = getChildProjects(); return children.size()==projects.size() && children.containsAll(projects); } @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) { return true; } /** * @deprecated since 1.341; use {@link #execute(AbstractBuild,BuildListener)} */ @Deprecated public static boolean execute(AbstractBuild build, BuildListener listener, BuildTrigger trigger) { return execute(build, listener); } /** * Convenience method to trigger downstream builds. * * @param build * The current build. Its downstreams will be triggered. * @param listener * Receives the progress report. */ public static boolean execute(AbstractBuild build, BuildListener listener) { PrintStream logger = listener.getLogger(); // Check all downstream Project of the project, not just those defined by BuildTrigger final DependencyGraph graph = Hudson.getInstance().getDependencyGraph(); List<Dependency> downstreamProjects = new ArrayList<Dependency>( graph.getDownstreamDependencies(build.getProject())); // Sort topologically Collections.sort(downstreamProjects, new Comparator<Dependency>() { public int compare(Dependency lhs, Dependency rhs) { // Swapping lhs/rhs to get reverse sort: return graph.compare(rhs.getDownstreamProject(), lhs.getDownstreamProject()); } }); for (Dependency dep : downstreamProjects) { AbstractProject p = dep.getDownstreamProject(); if (p.isDisabled()) { logger.println(Messages.BuildTrigger_Disabled(p.getName())); continue; } List<Action> buildActions = new ArrayList<Action>(); if (dep.shouldTriggerBuild(build, listener, buildActions)) { // this is not completely accurate, as a new build might be triggered // between these calls String name = p.getName()+" #"+p.getNextBuildNumber(); if(p.scheduleBuild(p.getQuietPeriod(), new UpstreamCause((Run)build), buildActions.toArray(new Action[buildActions.size()]))) { logger.println(Messages.BuildTrigger_Triggering(name)); } else { logger.println(Messages.BuildTrigger_InQueue(name)); } } } return true; } public void buildDependencyGraph(AbstractProject owner, DependencyGraph graph) { for (AbstractProject p : getChildProjects()) { if (!StringUtils.equals(p.getName(), owner.getName())) { graph.addDependency(new Dependency(owner, p) { @Override public boolean shouldTriggerBuild(AbstractBuild build, TaskListener listener, List<Action> actions) { return build.getResult().isBetterOrEqualTo(threshold); } }); } } } @Override public boolean needsToRunAfterFinalized() { return true; } /** * Called from {@link hudson.tasks.BuildTrigger.DescriptorImpl.ItemListenerImpl} when a job is renamed. * * @return true if this {@link BuildTrigger} is changed and needs to be saved. */ public boolean onJobRenamed(String oldName, String newName) { // quick test if(!childProjects.contains(oldName)) return false; boolean changed = false; // we need to do this per string, since old Project object is already gone. String[] projects = childProjects.split(","); for( int i=0; i<projects.length; i++ ) { if(projects[i].trim().equals(oldName)) { projects[i] = newName; changed = true; } } if(changed) { StringBuilder b = new StringBuilder(); for (String p : projects) { if(b.length()>0) b.append(','); b.append(p); } childProjects = b.toString(); } return changed; } /** * Correct broken data gracefully (#1537) */ private Object readResolve() { if(childProjects==null) return childProjects=""; return this; } @Extension public static class DescriptorImpl extends BuildStepDescriptor<Publisher> { public String getDisplayName() { return Messages.BuildTrigger_DisplayName(); } @Override public String getHelpFile() { return "/help/project-config/downstream.html"; } @Override public Publisher newInstance(StaplerRequest req, JSONObject formData) throws FormException { return new BuildTrigger( formData.getString("childProjects"), formData.has("evenIfUnstable") && formData.getBoolean("evenIfUnstable")); } @Override public boolean isApplicable(Class<? extends AbstractProject> jobType) { return true; } public boolean showEvenIfUnstableOption(Class<? extends AbstractProject> jobType) { // UGLY: for promotion process, this option doesn't make sense. return !jobType.getName().contains("PromotionProcess"); } /** * Form validation method. */ public FormValidation doCheck(@AncestorInPath AccessControlled subject, @AncestorInPath AbstractProject current, @QueryParameter String value ) { // Require CONFIGURE permission on this project if(!subject.hasPermission(Item.CONFIGURE)) return FormValidation.ok(); StringTokenizer tokens = new StringTokenizer(Util.fixNull(value),","); while(tokens.hasMoreTokens()) { String projectName = tokens.nextToken().trim(); Item item = Hudson.getInstance().getItemByFullName(projectName,Item.class); if(item==null) return FormValidation.error(Messages.BuildTrigger_NoSuchProject(projectName,AbstractProject.findNearest(projectName).getName())); if(!(item instanceof AbstractProject)) return FormValidation.error(Messages.BuildTrigger_NotBuildable(projectName)); if (StringUtils.equals(projectName, current.getName())) { return FormValidation.error(Messages.BuildTrigger_FailedUsingCurrentProject()); } } return FormValidation.ok(); } public AutoCompletionCandidates doAutoCompleteChildProjectsValue(@QueryParameter String value) { AutoCompletionCandidates c = new AutoCompletionCandidates(); List<Item> items = Hudson.getInstance().getItems(Item.class); List<String> queries = new AutoCompleteSeeder(value).getSeeds(); for (String term : queries) { for (Item item : items) { if (item.getName().startsWith(term)) { c.add(item.getName()); } } } return c; } @Extension public static class ItemListenerImpl extends ItemListener { @Override public void onRenamed(Item item, String oldName, String newName) { // update BuildTrigger of other projects that point to this object. // can't we generalize this? for( Project<?,?> p : Hudson.getInstance().getProjects() ) { BuildTrigger t = p.getPublishersList().get(BuildTrigger.class); if(t!=null) { if(t.onJobRenamed(oldName,newName)) { try { p.save(); } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to persist project setting during rename from "+oldName+" to "+newName,e); } } } } } } } private static final Logger LOGGER = Logger.getLogger(BuildTrigger.class.getName()); }