diff --git a/README.md b/README.md index 7dce4e67..d3177e01 100644 --- a/README.md +++ b/README.md @@ -122,9 +122,58 @@ These were retrieved from github [here][2]. * `PROMOTED_ID` - ID of the build being promoted * ex: 2012-04-12_17-13-03 * `PROMOTED_USER_NAME` - the user who triggered the promotion -* `PROMOTED_JOB_FULL_NAME` - the full name of the promoted -job - +* `PROMOTED_JOB_FULL_NAME` - the full name of the promoted job + +## Job DSL support + +```groovy +freeStyleJob(String jobname) { + properties{ + promotions { + promotion { + name(String promotionName) + icon(String iconName) + conditions { + selfPromotion(boolean evenIfUnstable = true) + parameterizedSelfPromotion(boolean evenIfUnstable = true, String parameterName, String parameterValue) + releaseBuild() + downstream(boolean evenIfUnstable = true, String jobs) + upstream(String promotionNames) + manual(String user){ + parameters{ + textParam(String parameterName, String defaultValue, String description) + } + } + actions { + shell(String command) + } + } + } + } +} +``` + +See [StepContext](https://jenkinsci.github.io/job-dsl-plugin/#path/job-steps) in the API Viewer for full documentation about the possible actions. + +### Example + +```groovy +freeStyleJob('test-job') { + properties{ + promotions { + promotion { + name('Development') + conditions { + manual('testuser') + } + actions { + shell('echo hello;') + } + } + } + } +} +``` ## Contributing diff --git a/pom.xml b/pom.xml index 3f575f4f..13d75d49 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.jenkins-ci.plugins plugin - 1.554.2 + 1.565 promoted-builds @@ -13,8 +13,8 @@ Jenkins promoted builds plugin http://wiki.jenkins-ci.org/display/JENKINS/Promoted+Builds+Plugin - - + + 3.0.1 true @@ -115,6 +115,19 @@ 1.5.3 true + + org.jenkins-ci.plugins + job-dsl + 1.42 + jar + true + + + org.jvnet.hudson + xstream + 1.4.7-jenkins-1 + true + @@ -127,7 +140,7 @@ org.codehaus.mojo findbugs-maven-plugin ${findbugs-maven-plugin.version} - + ${findbugs.failOnError} true @@ -151,4 +164,4 @@ - + diff --git a/src/main/java/hudson/plugins/promoted_builds/JobPropertyImpl.java b/src/main/java/hudson/plugins/promoted_builds/JobPropertyImpl.java index 82d2a76c..a0d9443d 100644 --- a/src/main/java/hudson/plugins/promoted_builds/JobPropertyImpl.java +++ b/src/main/java/hudson/plugins/promoted_builds/JobPropertyImpl.java @@ -1,5 +1,23 @@ package hudson.plugins.promoted_builds; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.ListIterator; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.lang.StringUtils; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.Ancestor; +import org.kohsuke.stapler.StaplerRequest; + import hudson.Extension; import hudson.Util; import hudson.model.AbstractBuild; @@ -19,26 +37,10 @@ import hudson.model.listeners.ItemListener; import hudson.remoting.Callable; import hudson.util.IOUtils; +import jenkins.model.Jenkins; import net.sf.json.JSONArray; import net.sf.json.JSONObject; -import org.apache.commons.lang.StringUtils; -import org.kohsuke.stapler.Ancestor; -import org.kohsuke.stapler.StaplerRequest; - -import java.io.File; -import java.io.FileFilter; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.ListIterator; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; -import jenkins.model.Jenkins; - /** * Promotion processes defined for a project. * @@ -71,11 +73,23 @@ public JobPropertyImpl(AbstractProject owner) throws Descriptor.FormExcepti this.owner = owner; init(); } + /** + * Programmatic construction. + */ public JobPropertyImpl(JobPropertyImpl other, AbstractProject owner) throws Descriptor.FormException, IOException { this.owner = owner; this.activeProcessNames.addAll(other.activeProcessNames); loadAllProcesses(other.getRootDir()); } + + /** + * Programmatic construction. + */ + @Restricted(NoExternalUse.class) + public JobPropertyImpl(Set activeProcessNames) { + this.activeProcessNames.addAll(activeProcessNames); + } + private JobPropertyImpl(StaplerRequest req, JSONObject json) throws Descriptor.FormException, IOException { // a hack to get the owning AbstractProject. // this is needed here so that we can load items @@ -109,6 +123,7 @@ private JobPropertyImpl(StaplerRequest req, JSONObject json) throws Descriptor.F } init(); } + private void loadAllProcesses(File rootDir) throws IOException { File[] subdirs = rootDir.listFiles(new FileFilter() { public boolean accept(File child) { @@ -395,4 +410,4 @@ public JobPropertyImpl newInstance(StaplerRequest req, JSONObject json) throws D } private static final Logger LOGGER = Logger.getLogger(JobPropertyImpl.class.getName()); -} +} \ No newline at end of file diff --git a/src/main/java/hudson/plugins/promoted_builds/conditions/ManualCondition.java b/src/main/java/hudson/plugins/promoted_builds/conditions/ManualCondition.java index af13912f..ca9b643a 100644 --- a/src/main/java/hudson/plugins/promoted_builds/conditions/ManualCondition.java +++ b/src/main/java/hudson/plugins/promoted_builds/conditions/ManualCondition.java @@ -55,10 +55,18 @@ public class ManualCondition extends PromotionCondition { public ManualCondition() { } + /* + * Restrict the Condition to specific user(s) + * @since 2.24 + */ public String getUsers() { return users; } + public void setUsers(String users) { + this.users = users; + } + public List getParameterDefinitions() { return parameterDefinitions; } @@ -151,10 +159,10 @@ public boolean isInGroupList() { return false; } public Future approve(AbstractBuild build, PromotionProcess promotionProcess, List paramValues) throws IOException{ - if (canApprove(promotionProcess, build)) { + if (canApprove(promotionProcess, build)) { // add approval to build - ManualApproval approval=new ManualApproval(promotionProcess.getName(), paramValues); - build.addAction(approval); + ManualApproval approval=new ManualApproval(promotionProcess.getName(), paramValues); + build.addAction(approval); build.save(); // check for promotion @@ -166,9 +174,9 @@ public List createDefaultValues(){ List paramValues = new ArrayList(); if (parameterDefinitions != null && !parameterDefinitions.isEmpty()) { - for (ParameterDefinition d:parameterDefinitions){ - paramValues.add(d.getDefaultParameterValue()); - } + for (ParameterDefinition d:parameterDefinitions){ + paramValues.add(d.getDefaultParameterValue()); + } } return paramValues; } @@ -184,7 +192,7 @@ public void doApprove(StaplerRequest req, StaplerResponse rsp, @AncestorInPath PromotionProcess promotionProcess, @AncestorInPath AbstractBuild build) throws IOException, ServletException { - JSONObject formData = req.getSubmittedForm(); + JSONObject formData = req.getSubmittedForm(); if (canApprove(promotionProcess, build)) { List paramValues = new ArrayList(); diff --git a/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/ConditionsContext.java b/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/ConditionsContext.java new file mode 100755 index 00000000..11ade444 --- /dev/null +++ b/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/ConditionsContext.java @@ -0,0 +1,171 @@ +package hudson.plugins.promoted_builds.integrations.jobdsl; + +import groovy.lang.Closure; +import groovy.util.Node; + +import java.util.HashMap; +import java.util.Map; + +import javaposse.jobdsl.dsl.Context; +import javaposse.jobdsl.dsl.FileJobManagement; +import javaposse.jobdsl.dsl.helpers.BuildParametersContext; + +import org.apache.commons.io.FileUtils; + +public class ConditionsContext implements Context { + + // Self Promotion Condition + private boolean isSelfPromotion = false; + + private boolean evenIfUnstable = false; + + // Parametzerized Self Promotion Condition + private boolean isParameterizedSelfPromotion = false; + + private boolean evenIfUnstableParameterized = false; + + private String parameterName = null; + + private String parameterValue = null; + + // Manual Promotion Condition + private boolean isManual = false; + + private String users = null; + + final Map params = new HashMap(); + + // Release Build Condition + private boolean isReleaseBuild = false; + + // Downstream Build Condition + private boolean isDownstreamPass = false; + + private boolean evenIfUnstableDownstream = false; + + private String jobs = null; + + // Upstream Build Condition + private boolean isUpstreamPromotion = false; + + private String promotionNames = null; + + public void selfPromotion(Boolean evenIfUnstable) { + isSelfPromotion = true; + if (evenIfUnstable) { + this.evenIfUnstable = evenIfUnstable; + } + } + + public void parameterizedSelfPromotion(Boolean evenIfUnstable, String parameterName, String parameterValue) { + isParameterizedSelfPromotion = true; + if (evenIfUnstable) { + this.isParameterizedSelfPromotion = evenIfUnstable; + } + this.parameterName = parameterName; + this.parameterValue = parameterValue; + } + + public void manual(String users) { + isManual = true; + this.users = users; + } + + public void manual(String users, Closure parametersClosure) { + isManual = true; + this.users = users; + parameters(parametersClosure); + } + + public void releaseBuild() { + isReleaseBuild = true; + } + + public void parameters(Closure parametersClosure) { + // delegate to main BuildParametersContext + BuildParametersContext parametersContext = new BuildParametersContext( + new FileJobManagement(FileUtils.getTempDirectory())); + executeInContext(parametersClosure, parametersContext); + params.putAll(parametersContext.getBuildParameterNodes()); + } + + public void downstream(Boolean evenIfUnstable, String jobs) { + isDownstreamPass = true; + if (evenIfUnstable) { + this.evenIfUnstableDownstream = evenIfUnstable; + } + this.jobs = jobs; + } + + public void upstream(String promotionNames) { + isUpstreamPromotion = true; + this.promotionNames = promotionNames; + } + + private static void executeInContext(Closure configClosure, Object context) { + configClosure.setDelegate(context); + configClosure.setResolveStrategy(Closure.DELEGATE_FIRST); + configClosure.call(); + } + + public Map getParams() { + return params; + } + + public boolean isSelfPromotion() { + return isSelfPromotion; + } + + public boolean isEvenIfUnstable() { + return evenIfUnstable; + } + + public boolean isParameterizedSelfPromotion() { + return isParameterizedSelfPromotion; + } + + public boolean isEvenIfUnstableParameterized() { + return evenIfUnstableParameterized; + } + + public String getParameterName() { + return parameterName; + } + + public String getParameterValue() { + return parameterValue; + } + + public boolean isManual() { + return isManual; + } + + public String getUsers() { + return users; + } + + public boolean isReleaseBuild() { + return isReleaseBuild; + } + + public boolean isDownstreamPass() { + return isDownstreamPass; + } + + public boolean isEvenIfUnstableDownstream() { + return evenIfUnstableDownstream; + } + + public String getJobs() { + return jobs; + } + + public boolean isUpstreamPromotion() { + return isUpstreamPromotion; + } + + public String getPromotionNames() { + return promotionNames; + } + +} diff --git a/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/JobDslManualCondition.java b/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/JobDslManualCondition.java new file mode 100755 index 00000000..02ea43b7 --- /dev/null +++ b/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/JobDslManualCondition.java @@ -0,0 +1,28 @@ +package hudson.plugins.promoted_builds.integrations.jobdsl; + +import groovy.util.Node; +import hudson.plugins.promoted_builds.conditions.ManualCondition; + +import java.util.Collection; + +import com.thoughtworks.xstream.annotations.XStreamAlias; + +/** + * Special holder for the ManualCondition generated by the Job DSL Plugin + * + * @author Dennis Schulte + */ +@XStreamAlias("hudson.plugins.promoted_builds.conditions.ManualCondition") +public class JobDslManualCondition extends ManualCondition { + + private Collection parameterDefinitionNodes; + + public Collection getParameterDefinitionNodes() { + return parameterDefinitionNodes; + } + + public void setParameterDefinitionNodes(Collection parameterDefinitionNodes) { + this.parameterDefinitionNodes = parameterDefinitionNodes; + } + +} diff --git a/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/JobDslPromotionProcess.java b/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/JobDslPromotionProcess.java new file mode 100755 index 00000000..be54af8f --- /dev/null +++ b/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/JobDslPromotionProcess.java @@ -0,0 +1,79 @@ +package hudson.plugins.promoted_builds.integrations.jobdsl; + +import java.util.ArrayList; +import java.util.List; + +import com.thoughtworks.xstream.annotations.XStreamAlias; + +import groovy.util.Node; +import hudson.plugins.promoted_builds.PromotionCondition; + +/** + * Special holder for the PromotionProcess generated by the Job DSL Plugin + * + * @author Dennis Schulte + */ +@XStreamAlias("hudson.plugins.promoted_builds.PromotionProcess") +public final class JobDslPromotionProcess { + + private String name; + /** + * The icon that represents this promotion process. This is the name of + * the GIF icon that can be found in ${rootURL}/plugin/promoted-builds/icons/16x16/ + * and ${rootURL}/plugin/promoted-builds/icons/32x32/, e.g. "star-gold". + */ + private String icon; + + /** + * The label that promotion process can be run on. + */ + private String assignedLabel; + + /** + * {@link PromotionCondition}s. All have to be met for a build to be promoted. + */ + private List conditions = new ArrayList(); + + private List buildSteps = new ArrayList(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + public String getAssignedLabel() { + return assignedLabel; + } + + public void setAssignedLabel(String assignedLabel) { + this.assignedLabel = assignedLabel; + } + + public List getConditions() { + return conditions; + } + + public void setConditions(List conditions) { + this.conditions = conditions; + } + + public List getBuildSteps() { + return buildSteps; + } + + public void setBuildSteps(List buildSteps) { + this.buildSteps = buildSteps; + } + +} diff --git a/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/JobDslPromotionProcessConverter.java b/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/JobDslPromotionProcessConverter.java new file mode 100755 index 00000000..8a1bb758 --- /dev/null +++ b/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/JobDslPromotionProcessConverter.java @@ -0,0 +1,126 @@ +package hudson.plugins.promoted_builds.integrations.jobdsl; + +import java.util.Collection; + +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.converters.reflection.AbstractReflectionConverter; +import com.thoughtworks.xstream.converters.reflection.ReflectionConverter; +import com.thoughtworks.xstream.converters.reflection.ReflectionProvider; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; +import com.thoughtworks.xstream.mapper.Mapper; + +import groovy.util.Node; +import hudson.PluginManager; +import hudson.PluginWrapper; +import jenkins.model.Jenkins; + +/** + * XStream Converter for the PromotionProcess for the Job DSL Plugin + * + * @author Dennis Schulte + */ +public class JobDslPromotionProcessConverter extends ReflectionConverter { + + public JobDslPromotionProcessConverter(Mapper mapper, ReflectionProvider reflectionProvider) { + super(mapper, reflectionProvider); + } + + private String classOwnership; + + private PluginManager pm; + + @Override + public boolean canConvert(@SuppressWarnings("rawtypes") Class type) { + return type.equals(JobDslPromotionProcess.class); + } + + @Override + public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { + JobDslPromotionProcess promotionProcess = (JobDslPromotionProcess) source; + // attributes + String plugin = obtainClassOwnership(); + if (plugin != null) { + writer.addAttribute("plugin", plugin); + } + // nodes + if (promotionProcess != null) { + if (promotionProcess.getName() != null) { + writer.startNode("name"); + writer.setValue(promotionProcess.getName()); + writer.endNode(); + } + if (promotionProcess.getIcon() != null) { + writer.startNode("icon"); + writer.setValue(promotionProcess.getIcon()); + writer.endNode(); + } + if (promotionProcess.getAssignedLabel() != null) { + String assignedLabel = promotionProcess.getAssignedLabel(); + if (assignedLabel != null) { + writer.startNode("assignedLabel"); + writer.setValue(assignedLabel); + writer.endNode(); + } + } + if (promotionProcess.getConditions() != null) { + writer.startNode("conditions"); + context.convertAnother(promotionProcess.getConditions()); + writer.endNode(); + } + if (promotionProcess.getBuildSteps() != null) { + writer.startNode("buildSteps"); + for (Node node : promotionProcess.getBuildSteps()) { + writer.startNode(node.name().toString()); + if (node.value() instanceof Collection) { + for (Object subNode : (Collection) node.value()) { + convertNode((Node) subNode, writer); + } + } else { + writer.setValue(node.value().toString()); + } + writer.endNode(); + } + writer.endNode(); + } + } + } + + private void convertNode(Node node, HierarchicalStreamWriter writer) { + writer.startNode(node.name().toString()); + if (node.value() instanceof Collection) { + for (Object subNode : (Collection) node.value()) { + convertNode((Node) subNode, writer); + } + } else { + writer.setValue(node.value().toString()); + } + writer.endNode(); + } + + private String obtainClassOwnership() { + if (this.classOwnership != null) { + return this.classOwnership; + } + if (pm == null) { + Jenkins j = Jenkins.getInstance(); + if (j != null) { + pm = j.getPluginManager(); + } + } + if (pm == null) { + return null; + } + // TODO: possibly recursively scan super class to discover dependencies + PluginWrapper p = pm.whichPlugin(hudson.plugins.promoted_builds.PromotionProcess.class); + this.classOwnership = p != null ? p.getShortName() + '@' + trimVersion(p.getVersion()) : null; + return this.classOwnership; + } + + static String trimVersion(String version) { + // TODO seems like there should be some trick with VersionNumber to do + // this + return version.replaceFirst(" .+$", ""); + } +} diff --git a/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/ManualConditionConverter.java b/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/ManualConditionConverter.java new file mode 100755 index 00000000..d7171411 --- /dev/null +++ b/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/ManualConditionConverter.java @@ -0,0 +1,71 @@ +package hudson.plugins.promoted_builds.integrations.jobdsl; + +import groovy.util.Node; + +import java.util.Collection; + +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.converters.reflection.ReflectionConverter; +import com.thoughtworks.xstream.converters.reflection.ReflectionProvider; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; +import com.thoughtworks.xstream.mapper.Mapper; + +/** + * XStream Converter for the ManualCondition for the Job DSL Plugin + * + * @author Dennis Schulte + */ +public class ManualConditionConverter extends ReflectionConverter { + + public ManualConditionConverter(Mapper mapper, ReflectionProvider reflectionProvider) { + super(mapper, reflectionProvider); + // TODO Auto-generated constructor stub + } + + @Override + public boolean canConvert(@SuppressWarnings("rawtypes") Class type) { + return type.equals(JobDslManualCondition.class); + } + + @Override + public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { + JobDslManualCondition mc = (JobDslManualCondition) source; + if (mc.getUsers() != null) { + writer.startNode("users"); + writer.setValue(mc.getUsers()); + writer.endNode(); + } + writer.startNode("parameterDefinitions"); + Collection parameterDefinitionNodes = mc.getParameterDefinitionNodes(); + if(parameterDefinitionNodes != null && !parameterDefinitionNodes.isEmpty()){ + for (Node node : mc.getParameterDefinitionNodes()) { + writer.startNode(node.name().toString()); + if (node.value() instanceof Collection) { + for (Object subNode : (Collection) node.value()) { + convertNode((Node) subNode, writer); + } + } else { + writer.setValue(node.value().toString()); + } + writer.endNode(); + } + } + writer.endNode(); + } + + private void convertNode(Node node, HierarchicalStreamWriter writer) { + writer.startNode(node.name().toString()); + if (node.value() instanceof Collection) { + for (Object subNode : (Collection) node.value()) { + convertNode((Node) subNode, writer); + } + } else { + writer.setValue(node.value().toString()); + } + writer.endNode(); + } + +} diff --git a/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/PromotionContext.java b/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/PromotionContext.java new file mode 100755 index 00000000..b1327819 --- /dev/null +++ b/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/PromotionContext.java @@ -0,0 +1,108 @@ +package hudson.plugins.promoted_builds.integrations.jobdsl; + +import groovy.lang.Closure; +import groovy.util.Node; +import hudson.plugins.promoted_builds.PromotionCondition; +import hudson.plugins.promoted_builds.conditions.DownstreamPassCondition; +import hudson.plugins.promoted_builds.conditions.ParameterizedSelfPromotionCondition; +import hudson.plugins.promoted_builds.conditions.SelfPromotionCondition; +import hudson.plugins.promoted_builds.conditions.UpstreamPromotionCondition; + +import java.util.ArrayList; +import java.util.List; + +import javaposse.jobdsl.dsl.Context; +import javaposse.jobdsl.dsl.FileJobManagement; +import javaposse.jobdsl.dsl.helpers.step.StepContext; + +import org.apache.commons.io.FileUtils; + +class PromotionContext implements Context { + + private List conditions = new ArrayList(); + + private List actions = new ArrayList(); + + private String icon; + + private String restrict; + + private String name; + + public void name(String name) { + this.name = name; + } + + public void icon(String icon) { + this.icon = icon; + } + + public void restrict(String restrict) { + this.restrict = restrict; + } + + public void conditions(Closure conditionClosure) { + // delegate to ConditionsContext + ConditionsContext conditionContext = new ConditionsContext(); + executeInContext(conditionClosure, conditionContext); + if (conditionContext.isSelfPromotion()) { + conditions.add(new SelfPromotionCondition(conditionContext.isEvenIfUnstable())); + } + if (conditionContext.isParameterizedSelfPromotion()) { + conditions.add(new ParameterizedSelfPromotionCondition(conditionContext.isEvenIfUnstableParameterized(), conditionContext + .getParameterName(), conditionContext.getParameterValue())); + } + if (conditionContext.isManual()) { + JobDslManualCondition condition = new JobDslManualCondition(); + condition.setUsers(conditionContext.getUsers()); + if (conditionContext.getParams() != null) { + condition.setParameterDefinitionNodes(conditionContext.getParams().values()); + } + conditions.add(condition); + } + if (conditionContext.isReleaseBuild()) { + conditions.add(new ReleasePromotionCondition()); + } + if (conditionContext.isDownstreamPass()) { + conditions.add(new DownstreamPassCondition(conditionContext.getJobs(), conditionContext.isEvenIfUnstableDownstream())); + } + if (conditionContext.isUpstreamPromotion()) { + conditions.add(new UpstreamPromotionCondition(conditionContext.getPromotionNames())); + } + } + + public void actions(Closure actionsClosure) { + // delegate to StepContext + StepContext stepContext = new StepContext(new FileJobManagement(FileUtils.getTempDirectory()), null); + executeInContext(actionsClosure, stepContext); + actions.addAll(stepContext.getStepNodes()); + } + + private static void executeInContext(Closure configClosure, Object context) { + configClosure.setDelegate(context); + configClosure.setResolveStrategy(Closure.DELEGATE_FIRST); + configClosure.call(); + } + + public List getConditions() { + return conditions; + } + + public List getActions() { + return actions; + } + + public String getIcon() { + return icon; + } + + public String getRestrict() { + return restrict; + } + + public String getName() { + return name; + } + + +} diff --git a/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/PromotionsContext.java b/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/PromotionsContext.java new file mode 100755 index 00000000..24a54ed4 --- /dev/null +++ b/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/PromotionsContext.java @@ -0,0 +1,59 @@ +package hudson.plugins.promoted_builds.integrations.jobdsl; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.google.common.base.Preconditions; + +import groovy.lang.Closure; +import javaposse.jobdsl.dsl.Context; + +public class PromotionsContext implements Context { + + Set names = new HashSet(); + + Map promotionContexts = new HashMap(); + + /** + * PromotionNodes: + * 1. dev + * 2. test + * + * AND + * + * Sub PromotionNode for every promotion + * 1. + * dev + * . + * . + * . + * + * 2. + * test + * . + * . + * . + * + * + * @param promotionClosure + * @return + */ + public void promotion(Closure promotionClosure) { + PromotionContext promotionContext = new PromotionContext(); + executeInContext(promotionClosure, promotionContext); + Preconditions.checkNotNull(promotionContext.getName(), "promotion name cannot be null"); + Preconditions.checkArgument(promotionContext.getName().length() > 0); + names.add(promotionContext.getName()); + promotionContexts.put(promotionContext.getName(),promotionContext); + + } + + private static void executeInContext(Closure configClosure, Object context) { + configClosure.setDelegate(context); + configClosure.setResolveStrategy(Closure.DELEGATE_FIRST); + configClosure.call(); + } + +} diff --git a/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/PromotionsExtensionPoint.java b/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/PromotionsExtensionPoint.java new file mode 100755 index 00000000..fe9a7d56 --- /dev/null +++ b/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/PromotionsExtensionPoint.java @@ -0,0 +1,138 @@ +package hudson.plugins.promoted_builds.integrations.jobdsl; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.thoughtworks.xstream.XStream; + +import hudson.Extension; +import hudson.model.AbstractItem; +import hudson.model.Item; +import hudson.model.Items; +import hudson.model.Descriptor.FormException; +import hudson.plugins.promoted_builds.JobPropertyImpl; +import hudson.util.IOUtils; +import hudson.util.XStream2; +import javaposse.jobdsl.dsl.helpers.properties.PropertiesContext; +import javaposse.jobdsl.plugin.ContextExtensionPoint; +import javaposse.jobdsl.plugin.DslEnvironment; +import javaposse.jobdsl.plugin.DslExtensionMethod; + +/** + * The Job DSL Extension Point for the Promotions. See also {@linktourl https://github.com/jenkinsci/job-dsl-plugin/wiki/Extending-the-DSL} + * + * @author Dennis Schulte + */ +@Extension +public class PromotionsExtensionPoint extends ContextExtensionPoint { + + private static final Logger LOGGER = Logger.getLogger(PromotionsExtensionPoint.class.getName()); + + private static final XStream XSTREAM = new XStream2(); + + @DslExtensionMethod(context = PropertiesContext.class) + public Object promotions(Runnable closure, DslEnvironment dslEnvironment) throws FormException, IOException { + PromotionsContext context = new PromotionsContext(); + executeInContext(closure, context); + dslEnvironment.put("processNames", context.names); + JobPropertyImpl jobProperty = new JobPropertyImpl(context.names); + Map promotionProcesses = new HashMap(); + for (String processName : context.names) { + PromotionContext promotionContext = context.promotionContexts.get(processName); + JobDslPromotionProcess jobDslPromotionProcess = new JobDslPromotionProcess(); + jobDslPromotionProcess.setName(processName); + jobDslPromotionProcess.setIcon(promotionContext.getIcon()); + jobDslPromotionProcess.setAssignedLabel(promotionContext.getRestrict()); + jobDslPromotionProcess.setBuildSteps(promotionContext.getActions()); + jobDslPromotionProcess.setConditions(promotionContext.getConditions()); + promotionProcesses.put(processName,jobDslPromotionProcess); + } + dslEnvironment.put("promotionProcesses", promotionProcesses); + return jobProperty; + } + + @Override + public void notifyItemCreated(Item item, DslEnvironment dslEnvironment) { + notifyItemCreated(item, dslEnvironment, false); + } + + @SuppressWarnings("unchecked") + public void notifyItemCreated(Item item, DslEnvironment dslEnvironment, boolean update) { + LOGGER.log(Level.INFO, String.format("Creating promotions for %s", item.getName())); + Map promotionProcesses = (Map) dslEnvironment.get("promotionProcesses"); + Set names = (Set) dslEnvironment.get("processNames"); + if (names != null && names.size() > 0) { + for (String name : names) { + JobDslPromotionProcess promotionProcess = promotionProcesses.get(name); + XSTREAM.registerConverter(new JobDslPromotionProcessConverter(XSTREAM.getMapper(), XSTREAM.getReflectionProvider())); + XSTREAM.registerConverter(new ManualConditionConverter(XSTREAM.getMapper(), XSTREAM.getReflectionProvider())); + String xml = XSTREAM.toXML(promotionProcess); + File dir = new File(item.getRootDir(), "promotions/" + name); + File configXml = Items.getConfigFile(dir).getFile(); + boolean created = configXml.getParentFile().mkdirs(); + String createUpdate; + if(created){ + createUpdate = "Added"; + }else{ + createUpdate = "Updated"; + } + try { + InputStream in = new ByteArrayInputStream(xml.getBytes("UTF-8")); + IOUtils.copy(in, configXml); + LOGGER.log(Level.INFO, String.format(createUpdate + " promotion with name %s for %s", name, item.getName())); + update = true; + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("Error handling extension code", e); + } catch (IOException e) { + throw new IllegalStateException("Error handling extension code", e); + } + } + } + + // Only update if a promotion was actually added, updated, or removed. + if (update) { + try { + LOGGER.log(Level.INFO, String.format("Reloading config for %s", item.getName())); + ((AbstractItem) item).doReload(); + } catch (Exception e) { + throw new IllegalStateException("Unable to cast item to AbstractItem and reload config", e); + } + } + } + + @Override + public void notifyItemUpdated(Item item, DslEnvironment dslEnvironment) { + LOGGER.log(Level.INFO, String.format("Updating promotions for %s", item.getName())); + @SuppressWarnings("unchecked") + Set newPromotions = (Set) dslEnvironment.get("processNames"); + File dir = new File(item.getRootDir(), "promotions/"); + boolean update = false; + // Delete removed promotions + if (newPromotions != null) { + File[] files = dir.listFiles(); + if (files != null) { + for (File promotion : files) { + if (!newPromotions.contains(promotion.getName())) { + boolean deleted = promotion.delete(); + if(deleted){ + LOGGER.log(Level.INFO, String.format("Deleted promotion with name %s for %s", promotion.getName(), item.getName())); + update = true; + } + } + } + } + } + + // Delegate to create-method + this.notifyItemCreated(item, dslEnvironment, update); + } + +} diff --git a/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/ReleasePromotionCondition.java b/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/ReleasePromotionCondition.java new file mode 100755 index 00000000..5be0b0ff --- /dev/null +++ b/src/main/java/hudson/plugins/promoted_builds/integrations/jobdsl/ReleasePromotionCondition.java @@ -0,0 +1,15 @@ +package hudson.plugins.promoted_builds.integrations.jobdsl; + +import hudson.plugins.promoted_builds.PromotionCondition; + +import com.thoughtworks.xstream.annotations.XStreamAlias; + +/** + * Special holder for the ReleasePromotionCondition generated by the Job DSL Plugin + * + * @author Dennis Schulte + */ +@XStreamAlias("hudson.plugins.release.promotion.ReleasePromotionCondition") +public class ReleasePromotionCondition extends PromotionCondition { + +} diff --git a/src/test/java/hudson/plugins/promoted_builds/integrations/jobdsl/JobDslManualConditionConverterTest.java b/src/test/java/hudson/plugins/promoted_builds/integrations/jobdsl/JobDslManualConditionConverterTest.java new file mode 100644 index 00000000..f0951f9f --- /dev/null +++ b/src/test/java/hudson/plugins/promoted_builds/integrations/jobdsl/JobDslManualConditionConverterTest.java @@ -0,0 +1,30 @@ +package hudson.plugins.promoted_builds.integrations.jobdsl; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.apache.commons.lang.StringUtils; +import org.junit.Test; + +import com.thoughtworks.xstream.XStream; + +import hudson.util.XStream2; + +public class JobDslManualConditionConverterTest { + + private static final XStream XSTREAM = new XStream2(); + + @Test + public void testShouldGenerateValidXml() throws Exception { + //Given + JobDslManualCondition mc = new JobDslManualCondition(); + mc.setUsers("testusers"); + //When + XSTREAM.registerConverter(new ManualConditionConverter(XSTREAM.getMapper(), XSTREAM.getReflectionProvider())); + String xml = XSTREAM.toXML(mc); + //Then + assertNotNull(xml); + System.out.println(xml); + assertTrue(StringUtils.contains(xml, "hudson.plugins.promoted__builds.conditions.ManualCondition")); + } +} diff --git a/src/test/java/hudson/plugins/promoted_builds/integrations/jobdsl/JobDslPromotionProcessConverterTest.java b/src/test/java/hudson/plugins/promoted_builds/integrations/jobdsl/JobDslPromotionProcessConverterTest.java new file mode 100644 index 00000000..35a0ef8f --- /dev/null +++ b/src/test/java/hudson/plugins/promoted_builds/integrations/jobdsl/JobDslPromotionProcessConverterTest.java @@ -0,0 +1,54 @@ +package hudson.plugins.promoted_builds.integrations.jobdsl; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.junit.Test; + +import com.thoughtworks.xstream.XStream; + +import groovy.util.Node; +import hudson.plugins.promoted_builds.PromotionCondition; +import hudson.plugins.promoted_builds.conditions.SelfPromotionCondition; +import hudson.util.XStream2; + +public class JobDslPromotionProcessConverterTest { + + private static final XStream XSTREAM = new XStream2(); + + @Test + public void testShouldGenerateValidXml() throws Exception { + // Given + JobDslPromotionProcess pp = new JobDslPromotionProcess(); + //Conditions + List conditions = new ArrayList(); + conditions.add(new SelfPromotionCondition(true)); + //BuildSteps + List buildSteps = new ArrayList(); + Node node = new Node(null, "hudson.tasks.Shell"); + Node subNode = new Node(node, "command"); + buildSteps.add(node); + subNode.setValue("echo hello;"); + Node node2 = new Node(null, "hudson.plugins.parameterizedtrigger.TriggerBuilder"); + Node subNode2 = new Node(node2, "configs"); + Node subsubNode2 = new Node(subNode2, "hudson.plugins.parameterizedtrigger.BlockableBuildTriggerConfig"); + Node subsubsubNode2a = new Node(subsubNode2, "projects"); + subsubsubNode2a.setValue("anoter-project"); + Node subsubsubNode2b = new Node(subsubNode2, "condition"); + subsubsubNode2b.setValue("ALWAYS"); + buildSteps.add(node2); + pp.setBuildSteps(buildSteps); + pp.setConditions(conditions); + // When + XSTREAM.registerConverter(new JobDslPromotionProcessConverter(XSTREAM.getMapper(), XSTREAM.getReflectionProvider())); + String xml = XSTREAM.toXML(pp); + // Then + assertNotNull(xml); + System.out.println(xml); + assertTrue(StringUtils.contains(xml, "hudson.plugins.promoted__builds.PromotionProcess")); + } +} diff --git a/src/test/java/hudson/plugins/promoted_builds/integrations/jobdsl/PromotionsDslContextExtensionTest.java b/src/test/java/hudson/plugins/promoted_builds/integrations/jobdsl/PromotionsDslContextExtensionTest.java new file mode 100644 index 00000000..c7629d0f --- /dev/null +++ b/src/test/java/hudson/plugins/promoted_builds/integrations/jobdsl/PromotionsDslContextExtensionTest.java @@ -0,0 +1,44 @@ +package hudson.plugins.promoted_builds.integrations.jobdsl; + +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.model.queue.QueueTaskFuture; + +import java.io.File; + +import javaposse.jobdsl.plugin.RemovedJobAction; +import javaposse.jobdsl.plugin.ExecuteDslScripts; + +import org.apache.commons.io.FileUtils; +import org.junit.Test; +import org.jvnet.hudson.test.HudsonTestCase; + +public class PromotionsDslContextExtensionTest extends HudsonTestCase { + + @Test + public void testShouldGenerateTheDefindedJob() throws Exception { + // Given + String dsl = FileUtils.readFileToString(new File("src/test/resources/example-dsl.groovy")); + FreeStyleProject seedJob = createFreeStyleProject(); + seedJob.getBuildersList().add( + new ExecuteDslScripts(new ExecuteDslScripts.ScriptLocation(Boolean.TRUE.toString(), null, dsl), false, RemovedJobAction.DELETE)); + // When + QueueTaskFuture scheduleBuild2 = seedJob.scheduleBuild2(0); + // Then + assertBuildStatusSuccess(scheduleBuild2); + } + + + @Test + public void testShouldGenerateTheDefindedComplexJob() throws Exception { + // Given + String dsl = FileUtils.readFileToString(new File("src/test/resources/complex-example-dsl.groovy")); + FreeStyleProject seedJob = createFreeStyleProject(); + seedJob.getBuildersList().add( + new ExecuteDslScripts(new ExecuteDslScripts.ScriptLocation(Boolean.TRUE.toString(), null, dsl), false, RemovedJobAction.DELETE)); + // When + QueueTaskFuture scheduleBuild2 = seedJob.scheduleBuild2(0); + // Then + assertBuildStatusSuccess(scheduleBuild2); + } +} diff --git a/src/test/resources/complex-example-dsl.groovy b/src/test/resources/complex-example-dsl.groovy new file mode 100644 index 00000000..70011276 --- /dev/null +++ b/src/test/resources/complex-example-dsl.groovy @@ -0,0 +1,45 @@ +freeStyleJob('test-job-complex') { + properties{ + promotions { + promotion { + name("Development") + icon("star-red") + restrict('slave1') + conditions { + manual('testuser'){ + parameters{ + textParam("parameterName","defaultValue","description") + } + } + } + actions { + downstreamParameterized { + trigger("deploy-application","SUCCESS",false,["buildStepFailure": "FAILURE","failure":"FAILURE","unstable":"UNSTABLE"]) { + predefinedProp("ENVIRONMENT","dev-server") + predefinedProp("APPLICATION_NAME", "\${PROMOTED_JOB_FULL_NAME}") + predefinedProp("BUILD_ID","\${PROMOTED_NUMBER}") + } + } + } + } + promotion { + name("Test") + icon("star-yellow") + restrict('slave2') + conditions { + manual('testuser') + upstream("Development") + } + actions { + downstreamParameterized { + trigger("deploy-application","SUCCESS",false,["buildStepFailure": "FAILURE","failure":"FAILURE","unstable":"UNSTABLE"]) { + predefinedProp("ENVIRONMENT","test-server") + predefinedProp("APPLICATION_NAME", "\${PROMOTED_JOB_FULL_NAME}") + predefinedProp("BUILD_ID","\${PROMOTED_NUMBER}") + } + } + } + } + } + } +} diff --git a/src/test/resources/example-dsl.groovy b/src/test/resources/example-dsl.groovy new file mode 100644 index 00000000..2178b231 --- /dev/null +++ b/src/test/resources/example-dsl.groovy @@ -0,0 +1,15 @@ +freeStyleJob('test-job') { + properties{ + promotions { + promotion { + name('Development') + conditions { + manual('tester') + } + actions { + shell('echo hello;') + } + } + } + } +}