package hudson.plugins.release; import hudson.Extension; import hudson.Launcher; import hudson.Util; import hudson.maven.MavenModuleSet; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Action; import hudson.model.BuildBadgeAction; import hudson.model.BuildListener; import hudson.model.Cause; import hudson.model.Descriptor; import hudson.model.FreeStyleProject; import hudson.model.Item; import hudson.model.ParameterDefinition; import hudson.model.ParameterValue; import hudson.model.ParametersAction; import hudson.model.ParametersDefinitionProperty; import hudson.model.StringParameterValue; import hudson.tasks.BuildStep; import hudson.tasks.BuildWrapper; import hudson.tasks.BuildWrapperDescriptor; import hudson.tasks.Builder; import hudson.util.VariableResolver; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import javax.servlet.ServletException; import net.sf.json.JSONArray; import net.sf.json.JSONObject; import org.apache.commons.lang.ArrayUtils; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; /** * Wraps a build with pre and post build steps. These steps can take * any action as part of the special release build. * * @author Peter Hayes * @since 1.0 */ public class ReleaseWrapper extends BuildWrapper { private static final String DEFAULT_RELEASE_VERSION_TEMPLATE = "Release #$RELEASE_VERSION"; private String releaseVersionTemplate; private boolean doNotKeepLog; private List<ParameterDefinition> parameterDefinitions = new ArrayList<ParameterDefinition>(); private List<Builder> preBuildSteps = new ArrayList<Builder>(); private List<Builder> postBuildSteps = new ArrayList<Builder>(); /** * @stapler-constructor */ public ReleaseWrapper() { } public String getReleaseVersionTemplate() { return releaseVersionTemplate; } public void setReleaseVersionTemplate(String releaseVersionTemplate) { this.releaseVersionTemplate = releaseVersionTemplate; } public boolean isDoNotKeepLog() { return doNotKeepLog; } public void setDoNotKeepLog(boolean doNotKeepLog) { this.doNotKeepLog = doNotKeepLog; } public List<ParameterDefinition> getParameterDefinitions() { return parameterDefinitions; } public void setParameterDefinitions(List<ParameterDefinition> parameterDefinitions) { this.parameterDefinitions = parameterDefinitions; } /** * @return Returns the preBuildSteps. */ public List<Builder> getPreBuildSteps() { return preBuildSteps; } /** * @param preBuildSteps The preBuildSteps to set. */ public void setPreBuildSteps(List<Builder> preBuildSteps) { this.preBuildSteps = preBuildSteps; } /** * @return Returns the postBuildSteps. */ public List<Builder> getPostBuildSteps() { return postBuildSteps; } /** * @param postBuildSteps The postBuildSteps to set. */ public void setPostBuildSteps(List<Builder> postSuccessBuildSteps) { this.postBuildSteps = postSuccessBuildSteps; } @Override public Action getProjectAction(AbstractProject job) { return new ReleaseAction(job); } @Override public Environment setUp(AbstractBuild build, final Launcher launcher, BuildListener listener) throws IOException, InterruptedException { final ReleaseBuildBadgeAction releaseBuildBadge = build.getAction(ReleaseBuildBadgeAction.class); if (releaseBuildBadge == null) { return new Environment() { }; } // Set the release version now by resolving build parameters against build and release version template ParametersAction parametersAction = build.getAction(ParametersAction.class); if (parametersAction != null) { // set up variable resolver from parameters action VariableResolver<String> resolver = createVariableResolver(parametersAction, build); // resolve template against resolver String releaseVersion = Util.replaceMacro(releaseVersionTemplate != null && !"".equals(releaseVersionTemplate) ? releaseVersionTemplate : DEFAULT_RELEASE_VERSION_TEMPLATE, resolver); // if release version is same as original, then blank it out if (DEFAULT_RELEASE_VERSION_TEMPLATE.equals(releaseVersion)) { releaseVersion = null; } releaseBuildBadge.releaseVersion = releaseVersion; } if (!executeBuildSteps(preBuildSteps, build, launcher, listener)) { throw new IOException("Could not execute pre-build steps"); } // return environment return new Environment() { @Override public boolean tearDown(AbstractBuild build, BuildListener listener) throws IOException, InterruptedException { // save build if (!doNotKeepLog) { build.keepLog(); } // set description if we can derive version if (releaseBuildBadge.getReleaseVersion() != null) { // set build description to indicate release build.setDescription(releaseBuildBadge.getReleaseVersion()); } return executeBuildSteps(postBuildSteps, build, launcher, listener); } }; } /* * Copied method from ParametersAction to reverse order of resolvers * per HUDSON-5094 */ private VariableResolver<String> createVariableResolver(ParametersAction parametersAction, AbstractBuild<?,?> build) { VariableResolver[] resolvers = new VariableResolver[parametersAction.getParameters().size()+1]; int i=0; for (ParameterValue p : parametersAction.getParameters()) resolvers[i++] = p.createVariableResolver(build); resolvers[i] = build.getBuildVariableResolver(); ArrayUtils.reverse(resolvers); return new VariableResolver.Union<String>(resolvers); } private boolean executeBuildSteps(List<Builder> buildSteps, AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { boolean shouldContinue = true; // execute prebuild steps, stop processing if indicated if (buildSteps != null) { for (BuildStep buildStep : buildSteps) { if (!shouldContinue) { break; } shouldContinue = buildStep.prebuild(build, listener); } // execute build step, stop processing if indicated for (BuildStep buildStep : buildSteps) { if (!shouldContinue) { break; } shouldContinue = buildStep.perform(build, launcher, listener); } } return shouldContinue; } public static boolean hasReleasePermission(AbstractProject job) { return job.hasPermission(Item.BUILD); } public static void checkReleasePermission(AbstractProject job) { job.checkPermission(Item.BUILD); } @Extension public static final class DescriptorImpl extends BuildWrapperDescriptor { @Override public String getDisplayName() { return "Configure release build"; } @Override public BuildWrapper newInstance(StaplerRequest req, JSONObject formData) throws FormException { ReleaseWrapper instance = new ReleaseWrapper(); instance.releaseVersionTemplate = formData.getString("releaseVersionTemplate"); instance.doNotKeepLog = formData.getBoolean("doNotKeepLog"); instance.parameterDefinitions = Descriptor.newInstancesFromHeteroList(req, formData, "parameters", ParameterDefinition.all()); instance.preBuildSteps = Descriptor.newInstancesFromHeteroList(req, formData, "preBuildSteps", Builder.all()); instance.postBuildSteps = Descriptor.newInstancesFromHeteroList(req, formData, "postBuildSteps", Builder.all()); return instance; } @Override public boolean isApplicable(AbstractProject<?, ?> item) { return FreeStyleProject.class.isInstance(item) || MavenModuleSet.class.isInstance(item); } } public class ReleaseAction implements Action { private AbstractProject project; private String releaseVersion; private String developmentVersion; public ReleaseAction(AbstractProject project) { this.project = project; } public List<ParameterDefinition> getParameterDefinitions() { return parameterDefinitions; } /** * {@inheritDoc} */ public String getDisplayName() { return "Release"; } /** * {@inheritDoc} */ public String getIconFileName() { return ReleaseWrapper.hasReleasePermission(project) ? "package.gif" : null; } /** * {@inheritDoc} */ public String getUrlName() { return "release"; } /** * @return Returns the project. */ public AbstractProject getProject() { return project; } /** * @return The list of previous release version identifiers */ public List<String> getPreviousReleaseVersions() { LinkedList<String> previousReleaseVersions = new LinkedList<String>(); for (Iterator iter = project.getBuilds().iterator(); iter.hasNext(); ) { AbstractBuild build = (AbstractBuild) iter.next(); ReleaseBuildBadgeAction badge = build.getAction(ReleaseBuildBadgeAction.class); if (badge != null) { if (badge.getReleaseVersion() != null) { previousReleaseVersions.add(badge.getReleaseVersion()); } } } return previousReleaseVersions; } public String getReleaseVersion() { return releaseVersion; } public void setReleaseVersion(String releaseVersion) { this.releaseVersion = releaseVersion; } public String getDevelopmentVersion() { return developmentVersion; } public void setDevelopmentVersion(String developmentVersion) { this.developmentVersion = developmentVersion; } /** * Gets the {@link ParameterDefinition} of the given name, if any. */ public ParameterDefinition getParameterDefinition(String name) { if (parameterDefinitions == null) { return null; } for (ParameterDefinition pd : parameterDefinitions) if (pd.getName().equals(name)) return pd; return null; } /* * TODO Would be nice if this method was accessible from AbstractProject */ private List<ParameterValue> getDefaultParametersValues() { ParametersDefinitionProperty paramDefProp = (ParametersDefinitionProperty) project.getProperty(ParametersDefinitionProperty.class); ArrayList<ParameterValue> defValues = new ArrayList<ParameterValue>(); /* * This check is made ONLY if someone will call this method even if isParametrized() is false. */ if(paramDefProp == null) return defValues; /* Scan for all parameter with an associated default values */ for(ParameterDefinition paramDefinition : paramDefProp.getParameterDefinitions()) { ParameterValue defaultValue = paramDefinition.getDefaultParameterValue(); if(defaultValue != null) defValues.add(defaultValue); } return defValues; } public void doSubmit(StaplerRequest req, StaplerResponse resp) throws IOException, ServletException { // verify permission ReleaseWrapper.checkReleasePermission(project); // bind development / release version req.bindParameters(this); // create parameter list List<ParameterValue> paramValues = getDefaultParametersValues(); if (getParameterDefinitions() != null && !getParameterDefinitions().isEmpty()) { JSONObject formData = req.getSubmittedForm(); JSONArray a = JSONArray.fromObject(formData.get("parameter")); for (Object o : a) { JSONObject jo = (JSONObject) o; String name = jo.getString("name"); ParameterDefinition d = getParameterDefinition(name); if(d==null) throw new IllegalArgumentException("No such parameter definition: " + name); ParameterValue value = d.createValue(req, jo); paramValues.add(d.createValue(req, jo)); } } else { // add version if specified if (releaseVersion != null && !"".equals(releaseVersion)) { paramValues.add(new StringParameterValue("RELEASE_VERSION", releaseVersion)); } if (developmentVersion != null && !"".equals(developmentVersion)) { paramValues.add(new StringParameterValue("DEVELOPMENT_VERSION", developmentVersion)); } } // schedule release build if (!project.scheduleBuild(0, new Cause.UserCause(), new ReleaseBuildBadgeAction(), new ParametersAction(paramValues))) { // TODO redirect to error page? } // redirect to status page resp.sendRedirect(project.getAbsoluteUrl()); } } public static class ReleaseBuildBadgeAction implements BuildBadgeAction { private String releaseVersion; public ReleaseBuildBadgeAction() { } public String getReleaseVersion() { return releaseVersion; } public String getIconFileName() { return null; } public String getDisplayName() { return null; } public String getUrlName() { return null; } } }