/* * The MIT License * * Copyright (c) 2010, NDS Group Ltd., James Nord * * 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.jvnet.hudson.plugins.m2release; import hudson.Extension; import hudson.Launcher; import hudson.Util; import hudson.maven.AbstractMavenProject; import hudson.maven.MavenBuild; import hudson.maven.MavenModule; import hudson.maven.MavenModuleSet; import hudson.maven.MavenModuleSetBuild; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Action; import hudson.model.BuildListener; import hudson.model.Descriptor; import hudson.model.Hudson; import hudson.model.Item; import hudson.security.Permission; import hudson.tasks.BuildWrapper; import hudson.tasks.BuildWrapperDescriptor; import hudson.tasks.Builder; import hudson.util.FormValidation; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Map; import javax.servlet.ServletException; import net.sf.json.JSONObject; import org.jvnet.hudson.plugins.m2release.nexus.Stage; import org.jvnet.hudson.plugins.m2release.nexus.StageClient; import org.jvnet.hudson.plugins.m2release.nexus.StageException; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Wraps a {@link MavenBuild} to be able to run the <a * href="http://maven.apache.org/plugins/maven-release-plugin/">maven release plugin</a> on demand, with the * ability to auto close a Nexus Pro Staging Repo * * @author James Nord * @version 0.3 * @since 0.1 */ public class M2ReleaseBuildWrapper extends BuildWrapper { private transient Logger log = LoggerFactory.getLogger(M2ReleaseBuildWrapper.class); private transient boolean doRelease = false; private transient boolean closeNexusStage = true; private transient Map<String, String> versions; private transient boolean appendHudsonBuildNumber; private transient String repoDescription; private transient String scmUsername; private transient String scmPassword; private transient String scmCommentPrefix; private transient boolean appendHusonUserName; private transient String hudsonUserName; public String releaseGoals = DescriptorImpl.DEFAULT_RELEASE_GOALS; public String defaultVersioningMode = DescriptorImpl.DEFAULT_VERSIONING; public boolean selectCustomScmCommentPrefix = DescriptorImpl.DEFAULT_SELECT_CUSTOM_SCM_COMMENT_PREFIX; public boolean selectAppendHudsonUsername = DescriptorImpl.DEFAULT_SELECT_APPEND_HUDSON_USERNAME; @DataBoundConstructor public M2ReleaseBuildWrapper(String releaseGoals, String defaultVersioningMode, boolean selectCustomScmCommentPrefix, boolean selectAppendHudsonUsername) { super(); this.releaseGoals = releaseGoals; this.defaultVersioningMode = defaultVersioningMode; this.selectCustomScmCommentPrefix = selectCustomScmCommentPrefix; this.selectAppendHudsonUsername = selectAppendHudsonUsername; } @Override public Environment setUp(AbstractBuild build, Launcher launcher, final BuildListener listener) throws IOException, InterruptedException { final String originalGoals; MavenModuleSet mmSet; final String mavenOpts; synchronized (getModuleSet(build)) { if (!doRelease) { // we are not performing a release so don't need a custom tearDown. return new Environment() { /** intentionally blank */ }; } // reset for the next build. doRelease = false; mmSet = getModuleSet(build); if (mmSet != null) { originalGoals = mmSet.getGoals(); String thisBuildGoals = releaseGoals; if (versions != null) { thisBuildGoals = generateVersionString(build.getNumber()) + thisBuildGoals; } if (scmUsername != null) { thisBuildGoals = "-Dusername=" + scmUsername + " " + thisBuildGoals; } if (scmPassword != null) { thisBuildGoals = "-Dpassword=" + scmPassword + " " + thisBuildGoals; } if (scmCommentPrefix != null) { final StringBuilder sb = new StringBuilder(); sb.append("\"-DscmCommentPrefix="); sb.append(scmCommentPrefix); if(appendHusonUserName) { sb.append(String.format("(%s)", hudsonUserName)); } sb.append("\" "); sb.append(thisBuildGoals); thisBuildGoals = sb.toString(); } mmSet.setGoals(thisBuildGoals); } else { // can this be so? originalGoals = null; } mavenOpts = mmSet.getMavenOpts(); // TODO: remove this and pull the release version out when creating the action. M2ReleaseBadgeAction releaseBuildIcon = build.getAction(M2ReleaseBadgeAction.class); releaseBuildIcon.setTooltipText("Release - " + getReleaseVersion(mmSet.getRootModule())); } return new Environment() { @Override public void buildEnvVars(java.util.Map<String, String> env) { if (mavenOpts != null && !env.containsKey("MAVEN_OPTS")) { env.put("MAVEN_OPTS", mavenOpts); } }; @Override public boolean tearDown(AbstractBuild bld, BuildListener lstnr) throws IOException, InterruptedException { boolean retVal = true; // TODO only re-set the build goals if they are still releaseGoals to avoid mid-air collisions. final MavenModuleSet mmSet = getModuleSet(bld); final boolean localcloseStage; String version = null; synchronized (mmSet) { mmSet.setGoals(originalGoals); // get a local variable so we don't have to synchronise on mmSet any more than we have to. localcloseStage = closeNexusStage; version = getReleaseVersion(mmSet.getRootModule()); versions = null; } if (localcloseStage) { StageClient client = new StageClient(new URL(getDescriptor().getNexusURL()), getDescriptor().getNexusUser(), getDescriptor().getNexusPassword()); try { MavenModule rootModule = mmSet.getRootModule(); // TODO grab the version that we have just released... Stage stage = client.getOpenStageID(rootModule.getModuleName().groupId, rootModule.getModuleName().artifactId, version); if (stage != null) { lstnr.getLogger().println("[M2Release] Closing repository " + stage); client.closeStage(stage, repoDescription); lstnr.getLogger().append("[M2Release] Closed staging repository."); } else { retVal = false; lstnr.fatalError("[M2Release] Could not find nexus stage repository for project.\n"); } } catch (StageException ex) { lstnr.fatalError("[M2Release] Could not close repository , %s\n", ex.toString()); retVal = false; } } return retVal; } }; } void enableRelease() { doRelease = true; } void setVersions(Map<String, String> versions) { // expects a map of key="-Dproject.rel.${m.moduleName}" value="version" this.versions = versions; } public void setAppendHudsonBuildNumber(boolean appendHudsonBuildNumber) { this.appendHudsonBuildNumber = appendHudsonBuildNumber; } public void setCloseNexusStage(boolean closeNexusStage) { this.closeNexusStage = closeNexusStage; } public void setRepoDescription(String repoDescription) { this.repoDescription = repoDescription; } public void setScmUsername(String scmUsername) { this.scmUsername = scmUsername; } public void setScmPassword(String scmPassword) { this.scmPassword = scmPassword; } public void setScmCommentPrefix(String scmCommentPrefix) { this.scmCommentPrefix = scmCommentPrefix; } public void setAppendHusonUserName(boolean appendHusonUserName) { this.appendHusonUserName = appendHusonUserName; } /** * @return the defaultVersioningMode */ public String getDefaultVersioningMode() { return defaultVersioningMode; } /** * @param defaultVersioningMode the defaultVersioningMode to set */ public void setDefaultVersioningMode(String defaultVersioningMode) { this.defaultVersioningMode = defaultVersioningMode; } public boolean isSelectCustomScmCommentPrefix() { return selectCustomScmCommentPrefix; } public void setSelectCustomScmCommentPrefix(boolean selectCustomScmCommentPrefix) { this.selectCustomScmCommentPrefix = selectCustomScmCommentPrefix; } public boolean isSelectAppendHudsonUsername() { return selectAppendHudsonUsername; } public void setSelectAppendHudsonUsername(boolean selectAppendHudsonUsername) { this.selectAppendHudsonUsername = selectAppendHudsonUsername; } public void setHudsonUserName(String hudsonUserName) { this.hudsonUserName = hudsonUserName; } private String generateVersionString(int buildNumber) { // -Dproject.rel.org.mycompany.group.project=version .... StringBuilder sb = new StringBuilder(); for (String key : versions.keySet()) { sb.append(key); sb.append('='); sb.append(versions.get(key)); if (appendHudsonBuildNumber && key.startsWith("-Dproject.rel")) { //$NON-NLS-1$ sb.append('-'); sb.append(buildNumber); } sb.append(' '); } return sb.toString(); } private MavenModuleSet getModuleSet(AbstractBuild<?,?> build) { if (build instanceof MavenBuild) { MavenBuild m2Build = (MavenBuild) build; MavenModule mm = m2Build.getProject(); MavenModuleSet mmSet = mm.getParent(); return mmSet; } else if (build instanceof MavenModuleSetBuild) { MavenModuleSetBuild m2moduleSetBuild = (MavenModuleSetBuild) build; MavenModuleSet mmSet = m2moduleSetBuild.getProject(); return mmSet; } else { return null; } } @Override public Action getProjectAction(AbstractProject job) { return new M2ReleaseAction((MavenModuleSet) job, defaultVersioningMode, selectCustomScmCommentPrefix, selectAppendHudsonUsername); } public static boolean hasReleasePermission(AbstractProject job) { return job.hasPermission(DescriptorImpl.CREATE_RELEASE); } public static void checkReleasePermission(AbstractProject job) { job.checkPermission(DescriptorImpl.CREATE_RELEASE); } private String getReleaseVersion(MavenModule moduleName) { String retVal = null; String key = "-Dproject.rel." + moduleName.getModuleName().toString(); // versions is null if we let Maven work out the version if (versions != null) { retVal = versions.get(key); if (retVal == null) { // try autoVersionSubmodules retVal = versions.get("-DreleaseVersion"); //$NON-NLS-1$ } } else { // we are auto versioning - so take a best guess and hope our last build was of the same version! retVal = moduleName.getVersion().replace("-SNAPSHOT", ""); //$NON-NLS-1$ //$NON-NLS-2$ } return retVal; } /** * Hudson defines a method {@link Builder#getDescriptor()}, which returns the corresponding * {@link Descriptor} object. Since we know that it's actually {@link DescriptorImpl}, override the method * and give a better return type, so that we can access {@link DescriptorImpl} methods more easily. This is * not necessary, but just a coding style preference. */ @Override public DescriptorImpl getDescriptor() { // see Descriptor javadoc for more about what a descriptor is. return (DescriptorImpl) super.getDescriptor(); } @Extension public static class DescriptorImpl extends BuildWrapperDescriptor { public static final String DEFAULT_RELEASE_GOALS = "-Dresume=false release:prepare release:perform"; //$NON-NLS-1$ public static final Permission CREATE_RELEASE = new Permission(Item.PERMISSIONS, "Release", //$NON-NLS-1$ Messages._CreateReleasePermission_Description(), Hudson.ADMINISTER); public static final String VERSIONING_AUTO = "auto"; //$NON-NLS-1$ public static final String VERSIONING_SPECIFY_VERSIONS = "specify_versions"; //$NON-NLS-1$ public static final String VERSIONING_SPECIFY_VERSION = "specify_version"; //$NON-NLS-1$ public static final String DEFAULT_VERSIONING = VERSIONING_AUTO; //$NON-NLS-1$ public static final boolean DEFAULT_SELECT_CUSTOM_SCM_COMMENT_PREFIX = false; public static final boolean DEFAULT_SELECT_APPEND_HUDSON_USERNAME = false; private boolean nexusSupport = false; private String nexusURL = null; private String nexusUser = "deployment"; //$NON-NLS-1$ private String nexusPassword = "deployment123"; //$NON-NLS-1$ public DescriptorImpl() { super(M2ReleaseBuildWrapper.class); load(); } @Override public boolean isApplicable(AbstractProject<?, ?> item) { return (item instanceof AbstractMavenProject); } @Override public boolean configure(StaplerRequest staplerRequest, JSONObject json) throws FormException { nexusSupport = json.containsKey("nexusSupport"); //$NON-NLS-1$ if (nexusSupport) { JSONObject nexusParams = json.getJSONObject("nexusSupport"); //$NON-NLS-1$ nexusURL = Util.fixEmpty(nexusParams.getString("nexusURL")); //$NON-NLS-1$ if (nexusURL != null && nexusURL.endsWith("/")) { //$NON-NLS-1$ nexusURL = nexusURL.substring(0, nexusURL.length() - 1); } nexusUser = Util.fixEmpty(nexusParams.getString("nexusUser")); //$NON-NLS-1$ nexusPassword = nexusParams.getString("nexusPassword"); //$NON-NLS-1$ } save(); return true; // indicate that everything is good so far } @Override public String getDisplayName() { return Messages.Wrapper_DisplayName(); } public String getNexusURL() { return nexusURL; } public String getNexusUser() { return nexusUser; } public String getNexusPassword() { return nexusPassword; } public boolean isNexusSupport() { return nexusSupport; } /** * Checks if the Nexus URL exists and we can authenticate against it. */ public FormValidation doUrlCheck(@QueryParameter String urlValue, final @QueryParameter String usernameValue, final @QueryParameter String passwordValue) throws IOException, ServletException { // this method can be used to check if a file exists anywhere in the file system, // so it should be protected. if (!Hudson.getInstance().hasPermission(Hudson.ADMINISTER)) { return FormValidation.ok(); } urlValue = Util.fixEmptyAndTrim(urlValue); if (urlValue == null) { return FormValidation.ok(); } final String testURL; if (urlValue.endsWith("/")) { testURL = urlValue.substring(0, urlValue.length() - 1); } else { testURL = urlValue; } URL url = null; try { url = new URL(testURL); if (!(url.getProtocol().equals("http") || url.getProtocol().equals("https"))) { return FormValidation.error("protocol must be http or https"); } StageClient client = new StageClient(new URL(testURL), usernameValue, passwordValue); client.checkAuthentication(); } catch (MalformedURLException ex) { return FormValidation.error(url + " is not a valid URL"); } catch (StageException ex) { FormValidation stageError = FormValidation.error(ex.getMessage()); stageError.initCause(ex); return stageError; } return FormValidation.ok(); } } }