/* * The MIT License * * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Bruce Chapman, Erik Ramfelt, Jean-Baptiste Quenot, Luca Domenico Milanesio * * 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 ch.ethz.origo; import hudson.Extension; import hudson.Functions; import hudson.Launcher; import hudson.model.BuildListener; import hudson.model.Result; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Hudson; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Notifier; import hudson.tasks.Publisher; import hudson.tasks.Mailer; import hudson.util.FormValidation; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; import org.apache.xmlrpc.XmlRpcException; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; /** * {@link Publisher} that creates/updates an Origo issue based on the build * result. * * @author Patrick Ruckstuhl */ public class OrigoIssuePublisher extends Notifier { @Extension public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> { private String apiUrl; private String projectName; private String userKey; private String issueSubject; private String issueTag; private boolean issuePrivate = true; private String hudsonUrl; public DescriptorImpl() { load(); } @Override public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { req.bindParameters(this, "origo.issues."); // hudsonUrl is read from Mailer hudsonUrl = Mailer.descriptor().getUrl(); save(); return super.configure(req, formData); } public FormValidation doCheckApiUrl(@QueryParameter String value) { try { new URL(value); return FormValidation.ok(); } catch (MalformedURLException e) { return FormValidation.error("Invalid url specified"); } } public FormValidation doCheckIssueSubject(@QueryParameter String value) { if (StringUtils.isNotEmpty(value)) { return FormValidation.ok(); } else { return FormValidation.error("Issue subject is mandatory"); } } public FormValidation doCheckIssueTag(@QueryParameter String value) { if (StringUtils.isNotEmpty(value)) { return FormValidation.ok(); } else { return FormValidation.error("Issue tag is mandatory"); } } public FormValidation doCheckProjectName(@QueryParameter String value) { if (StringUtils.isNotEmpty(value)) { return FormValidation.ok(); } else { return FormValidation.error("Project name is mandatory"); } } public FormValidation doCheckUserKey(@QueryParameter String value) { if (StringUtils.isNotEmpty(value)) { return FormValidation.ok(); } else { return FormValidation.error("User key is mandatory"); } } public String getApiUrl() { return apiUrl; } @Override public String getDisplayName() { return "Origo Issues"; } public String getHudsonUrl() { return hudsonUrl; } public String getIssueSubject() { return issueSubject; } public String getIssueTag() { return issueTag; } public String getProjectName() { return projectName; } public String getUserKey() { return userKey; } @SuppressWarnings("rawtypes") @Override public boolean isApplicable(Class<? extends AbstractProject> jobType) { return true; } public boolean isIssuePrivate() { return issuePrivate; } @Override public Publisher newInstance(StaplerRequest req, JSONObject formData) throws FormException { if (hudsonUrl == null) { // if Hudson URL is not configured yet, infer some default hudsonUrl = Functions.inferHudsonURL(req); save(); } return super.newInstance(req, formData); } public void setApiUrl(String apiUrl) { this.apiUrl = apiUrl; } public void setHudsonUrl(String hudsonUrl) { this.hudsonUrl = hudsonUrl; } public void setIssuePrivate(boolean issuePrivate) { this.issuePrivate = issuePrivate; } public void setIssueSubject(String issueSubject) { this.issueSubject = issueSubject; } public void setIssueTag(String issueTag) { this.issueTag = issueTag; } public void setProjectName(String projectName) { this.projectName = projectName; } public void setUserKey(String userKey) { this.userKey = userKey; } } private static final Logger LOGGER = Logger.getLogger(OrigoIssuePublisher.class.getName()); static final String APPLICATION_KEY = "KEYFORTHEORIGOHUDSONISSUESPLUGIN"; private String apiUrl; private String projectName; private String userKey; private String issueSubject; private String issueTag; private boolean issuePrivate; private OrigoApiClient client; OrigoIssuePublisher(String apiUrl, String projectName, String userKey, String issueSubject, String issueTag, boolean issuePrivate, OrigoApiClient client) { super(); this.apiUrl = apiUrl; this.projectName = projectName; this.userKey = userKey; this.issueSubject = issueSubject; this.issueTag = issueTag; this.issuePrivate = issuePrivate; this.client = client; } @DataBoundConstructor public OrigoIssuePublisher(String apiUrl, String projectName, String userKey, String issueSubject, String issueTag, boolean issuePrivate) { super(); this.apiUrl = apiUrl; this.projectName = projectName; this.userKey = userKey; this.issueSubject = issueSubject; this.issueTag = issueTag; this.issuePrivate = issuePrivate; } public String getApiUrl() { return apiUrl; } @Override public DescriptorImpl getDescriptor() { return Hudson.getInstance().getDescriptorByType(OrigoIssuePublisher.DescriptorImpl.class); } public String getIssueSubject() { return issueSubject; } public String getIssueTag() { return issueTag; } public String getProjectName() { return projectName; } public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.BUILD; } public String getUserKey() { return userKey; } public boolean isIssuePrivate() { return issuePrivate; } private OrigoApiClient createClient() throws MalformedURLException { if(client == null) { return new OrigoApiClient(new URL(apiUrl)); }else{ return client; } } @Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) { try { boolean isNewFailure = isNewFailure(build); boolean isRecovery = isRecovery(build); if (isNewFailure || isRecovery) { OrigoApiClient client= createClient(); // login String session = client.login(userKey, APPLICATION_KEY); LOGGER.fine("Got session" + session); // get project id Integer projectId = client.retrieveProjectId(session, projectName); if (isNewFailure) { openNewIssue(build, client, session, projectId); } else if (isRecovery) { closeExistingIssue(build, client, session, projectId); } } } catch (Exception e) { LOGGER.log(Level.SEVERE, "Unable to update origo issue.", e); } return true; } private void closeExistingIssue(AbstractBuild<?, ?> build, OrigoApiClient client, String session, Integer projectId) throws XmlRpcException { // search issue HashMap<String, String> searchArgs = new HashMap<String, String>(); searchArgs.put("status", "open"); searchArgs.put("tags", issueTag); Object[] issues = client.searchIssue(session, projectId, searchArgs); if (issues != null && issues.length == 1) { // close issue Map<Object, Object> issue = (Map<Object, Object>) issues[0]; Integer issueId = (Integer) issue.get("issue_id"); LOGGER.fine("Found issue with id " + issueId); String issueDescription = "Build fixed see: " + createLinkUrl(build); client.extendedCommentIssue(session, projectId, issueId, issueDescription, "status::closed," + issueTag); } else { LOGGER.warning("Did not find exactly one match."); } } private String createLinkUrl(AbstractBuild<?, ?> build) { return getDescriptor().getHudsonUrl() + build.getUrl(); } private boolean isNewFailure(AbstractBuild<?, ?> build) { Result previousResult = build.getPreviousBuild() != null ? build.getPreviousBuild().getResult() : Result.SUCCESS; Result currentResult = build.getResult(); return previousResult == Result.SUCCESS && (currentResult == Result.FAILURE || build.getResult() == Result.UNSTABLE); } private boolean isRecovery(AbstractBuild<?, ?> build) { Result previousResult = build.getPreviousBuild() != null ? build.getPreviousBuild().getResult() : Result.SUCCESS; Result currentResult = build.getResult(); return currentResult == Result.SUCCESS && (previousResult == Result.FAILURE || build.getPreviousBuild().getResult() == Result.UNSTABLE); } private void openNewIssue(AbstractBuild<?, ?> build, OrigoApiClient client, String session, Integer projectId) throws XmlRpcException { // create issue String issueDescription = "Build failed see: " + createLinkUrl(build); System.out.println(issueDescription); client.addIssue(session, projectId, issueSubject, issueDescription, "status::open," + issueTag, issuePrivate); } }