Index: test/net/sourceforge/cruisecontrol/publishers/JiraPublisherTest.java =================================================================== --- test/net/sourceforge/cruisecontrol/publishers/JiraPublisherTest.java (revision 3558) +++ test/net/sourceforge/cruisecontrol/publishers/JiraPublisherTest.java (working copy) @@ -41,155 +41,210 @@ import net.sourceforge.cruisecontrol.util.XMLLogHelper; import org.jdom.Element; +import javax.xml.rpc.ServiceException; +import java.rmi.RemoteException; + /** - * Unit test for the Jabber publisher which publishes - * a link to the build results via Jabber Instant Messaging framework. - * Currently, tests are limited to message generation and parameter - * validation. Testing of the XMPPConnection mechanism will require a + * Unit test for the Jira publisher which creates a new issue + * for newly failed build via SOAP to a Jira instance. + * + * Testing of the SOAP publishing mechanism requires a * publicly accessible Jabber server to order to login as the recipient. - * Only then can this test represent 100% code coverage. + * Only then can this test represent 100% code coverage. This functional test is disabled. * - * @see net.sourceforge.cruisecontrol.publishers.JabberPublisher - * @see net.sourceforge.cruisecontrol.publishers.LinkJabberPublisher + * @see JiraPublisher * - * @author Jonas Edgeworth - * @version 1.0 + * @author Jerome Lacoste */ -public class LinkJabberPublisherTest extends TestCase { +public class JiraPublisherTest extends TestCase { - private LinkJabberPublisher publisher; + private JiraPublisher publisher; + private Element successLog; private XMLLogHelper successLogHelper; + private Element newFailureLog; + private Element reFailedLog; + private Element buildFixedLog; private String baseURLString = "http://mybuildserver.com:8080/buildservlet/BuildServlet"; protected void setUp() throws Exception { - successLogHelper = createLogHelper(true, true); - publisher = new LinkJabberPublisher(); + successLog = createLog(true, true); + successLogHelper = new XMLLogHelper(successLog); + newFailureLog = createLog(false, true); + reFailedLog = createLog(false, false); + buildFixedLog = createLog(true, false); + publisher = new JiraPublisher(); } protected XMLLogHelper createLogHelper(boolean success, boolean lastBuildSuccess) { + Element cruisecontrolElement = createLog(success, lastBuildSuccess); + + return new XMLLogHelper(cruisecontrolElement); + } + + private Element createLog(boolean success, boolean lastBuildSuccess) { Element cruisecontrolElement = new Element("cruisecontrol"); Element infoElement = new Element("info"); Element logFileElement = new Element("property"); logFileElement.setAttribute("name", "logfile"); - logFileElement.setAttribute("value", "log20020206120000.xml"); + logFileElement.setAttribute("value", "log20070927232937.xml"); infoElement.addContent(logFileElement); + cruisecontrolElement.addContent(infoElement); + Element timestampElement = new Element("property"); + timestampElement.setAttribute("name", "cctimestamp"); + timestampElement.setAttribute("value", "20070927232937"); + infoElement.addContent(timestampElement); + Element projectNameElement = new Element("property"); projectNameElement.setAttribute("name", "projectname"); - projectNameElement.setAttribute("value", "Jabbertest"); + projectNameElement.setAttribute("value", "Jiratest"); infoElement.addContent(projectNameElement); + Element lastBuildElement = new Element("property"); + lastBuildElement.setAttribute("name", "lastbuildsuccessful"); + lastBuildElement.setAttribute("value", "" + lastBuildSuccess); + infoElement.addContent(lastBuildElement); + Element buildElement = new Element("build"); if (!success) { buildElement.setAttribute("error", "Something went wrong"); } cruisecontrolElement.addContent(buildElement); - - return new XMLLogHelper(cruisecontrolElement); + return cruisecontrolElement; } /** - * Test the message creation process assuring that the - * generated string matches the associated build URL - * @throws CruiseControlException + * This one should be disabled most of the time as it publishes against official Jira. + * Cf http://jira.atlassian.com/browse/TST-12633 for example + * @throws Exception */ - public void testCreateMessage() throws CruiseControlException { - publisher.setBuildResultsURL(baseURLString); - assertEquals( - "Build results for successful build of project Jabbertest: " + baseURLString + "?log=log20020206120000", - publisher.createMessage(successLogHelper)); + public void disabled_testIntegrationTestPublishToOfficialJira() throws Exception { + JiraPublisherTest test = new JiraPublisherTest(); + test.setUp(); + Element log = test.createLog(false, true); + + // cf com.atlassian.jira_soapclient.ClientConstants + test.publisher.setUsername("soaptester"); + test.publisher.setPassword("soaptester"); + test.publisher.setJiraProject("TST"); + + test.publisher.setJiraUrl("http://jira.atlassian.com"); + + test.publisher.setBuildResultsURL(baseURLString); + + test.publisher.validate(); + test.publisher.publish(log); } + static class MockJiraPublisher extends JiraPublisher { + private XMLLogHelper helper = null; + + protected void createJiraIssue(XMLLogHelper helper) throws ServiceException, RemoteException, + CruiseControlException { + this.helper = helper; + } + + public boolean hasSentIssue() { + return helper != null; + } + } + + public void testOnlyNewBrokenBuildsTriggerIssueCreation1() throws CruiseControlException { + ensureValidateDoesntFailWhenNoMissingRequiredAttribute("url", "project", "username", "password", baseURLString); + + assertPublishEquals("don't send upon success", false, successLog); + } + + public void testOnlyNewBrokenBuildsTriggerIssueCreation2() throws CruiseControlException { + ensureValidateDoesntFailWhenNoMissingRequiredAttribute("url", "project", "username", "password", baseURLString); + + assertPublishEquals("only send upon new failure", true, newFailureLog); + } + + public void testOnlyNewBrokenBuildsTriggerIssueCreation3() throws CruiseControlException { + ensureValidateDoesntFailWhenNoMissingRequiredAttribute("url", "project", "username", "password", baseURLString); + + assertPublishEquals("don't send upon existing failure", false, reFailedLog); + } + + public void testOnlyNewBrokenBuildsTriggerIssueCreation4() throws CruiseControlException { + ensureValidateDoesntFailWhenNoMissingRequiredAttribute("url", "project", "username", "password", baseURLString); + + assertPublishEquals("don't send upon build fixed", false, buildFixedLog); + } + + private void assertPublishEquals(String message, boolean expectedPublished, Element log) + throws CruiseControlException { + MockJiraPublisher pub1 = new MockJiraPublisher(); + pub1.publish(log); + assertEquals(message, expectedPublished, pub1.hasSentIssue()); + } + + /** * Test the message creation process assuring that the * generated string matches the associated build URL - * with additional parameters passed included. - * @throws CruiseControlException + * @throws net.sourceforge.cruisecontrol.CruiseControlException */ - public void testQuestionMarkInBuildResultsURL() throws CruiseControlException { - publisher.setBuildResultsURL(baseURLString + "?key=value"); - + public void testCreateDescription() throws CruiseControlException { + publisher.setBuildResultsURL(baseURLString); assertEquals( - "Build results for successful build of project Jabbertest: " - + baseURLString - + "?key=value&log=log20020206120000", - publisher.createMessage(successLogHelper)); + "CruiseControl build failed for project Jiratest\n" + + "\n" + + "Check the build results at: " + baseURLString + "?log=log20070927232937", + publisher.createDescription(successLogHelper)); } /** * Test all validation scenarios related to the - * configuration parameters for the LinkJabberPublisher. + * configuration parameters for the JiraPublisher. * Currently, the validation checks the following:

- * If host is null
- * If username is null
- * If password is null
- * If recipient is null
- * If buildResultsURL is null
- * If username is not of the form username@myserver.com
- * If recipient is of the form recipient@myserver.com

- * Validation has not been tested against groupchat configurations. + * validation fails if one required attribute is missing
+ * validation doesn't fail if no required attribute is missing
*/ public void testValidate() { - // Test if host is null - publisher.setPassword("asdfdsaf"); - publisher.setRecipient("recipient"); - publisher.setBuildResultsURL("http://foo.com"); + ensureValidateFailsOnRequiredMissingAttribute(null, "project", "username", "password", baseURLString); + ensureValidateFailsOnRequiredMissingAttribute("url", null, "username", "password", baseURLString); + ensureValidateFailsOnRequiredMissingAttribute("url", "project", null, "password", baseURLString); + ensureValidateFailsOnRequiredMissingAttribute("url", "project", "username", null, baseURLString); + ensureValidateFailsOnRequiredMissingAttribute("url", "project", "username", "password", null); + + ensureValidateDoesntFailWhenNoMissingRequiredAttribute("url", "project", "username", "password", baseURLString); + } + + private void ensureValidateFailsOnRequiredMissingAttribute(String url, String jiraProject, String password, + String username, String buildResultsURL) { + publisher.setJiraUrl(url); + publisher.setJiraProject(jiraProject); + publisher.setUsername(username); + publisher.setPassword(password); + publisher.setBuildResultsURL(buildResultsURL); + try { publisher.validate(); - fail("should throw exception if host not set"); + fail("should throw exception if one required attribute is not set"); } catch (CruiseControlException e) { } - // Test if username is null - publisher.setHost("host"); - publisher.setUsername(null); + } + + private void ensureValidateDoesntFailWhenNoMissingRequiredAttribute(String url, String jiraProject, String password, + String username, String buildResultsURL) { + publisher.setJiraUrl(url); + publisher.setJiraProject(jiraProject); + publisher.setUsername(username); + publisher.setPassword(password); + publisher.setBuildResultsURL(buildResultsURL); + try { publisher.validate(); - fail("should throw exception if username not set"); } catch (CruiseControlException e) { + fail("should not throw exception if all required attribute are set"); } - // Test is username is of the incorrect form - publisher.setUsername("username@adsfdsaf.com"); - try { - publisher.validate(); - fail("should throw exception if username is of the wrong form"); - } catch (CruiseControlException e) { - } - // Test if password is null - publisher.setUsername("username"); - publisher.setPassword(null); - try { - publisher.validate(); - fail("should throw exception if password not set"); - } catch (CruiseControlException e) { - } - // Test if recipient is null - publisher.setPassword("adsfadsf"); - publisher.setRecipient(null); - try { - publisher.validate(); - fail("should throw exception if recipient not set"); - } catch (CruiseControlException e) { - } - // Test if recipient is of the incorrect form - publisher.setRecipient("recipient"); - try { - publisher.validate(); - fail("should throw exception if recipient is of the wrong form"); - } catch (CruiseControlException e) { - } - // Test if build results url is null - publisher.setRecipient("recipient@foo.com"); - publisher.setBuildResultsURL(null); - try { - publisher.validate(); - fail("should throw exception if BuildResultURL not set"); - } catch (CruiseControlException e) { - } } } \ No newline at end of file Index: src/net/sourceforge/cruisecontrol/publishers/JiraPublisher.java =================================================================== --- src/net/sourceforge/cruisecontrol/publishers/JiraPublisher.java (revision 0) +++ src/net/sourceforge/cruisecontrol/publishers/JiraPublisher.java (revision 0) @@ -0,0 +1,188 @@ +/******************************************************************************** + * CruiseControl, a Continuous Integration Toolkit + * Copyright (c) 2001-2003, ThoughtWorks, Inc. + * 200 E. Randolph, 25th Floor + * Chicago, IL 60601 USA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * + Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * + Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ********************************************************************************/ +package net.sourceforge.cruisecontrol.publishers; + +import _soapclient.JiraSoapService; +import _soapclient.JiraSoapServiceServiceLocator; +import com.atlassian.jira.rpc.soap.beans.RemoteIssue; +import net.sourceforge.cruisecontrol.CruiseControlException; +import net.sourceforge.cruisecontrol.Publisher; +import net.sourceforge.cruisecontrol.util.ValidationHelper; +import net.sourceforge.cruisecontrol.util.XMLLogHelper; +import org.apache.log4j.Logger; +import org.jdom.Element; + +import java.util.Calendar; + +/** + * Publish issues to Jira via SOAP upon newly failed builds. + * + * @author Jerome Lacoste + * @version 1.0 + */ + +public class JiraPublisher implements Publisher { + + private static final Logger LOG = Logger.getLogger(JiraPublisher.class); + + private String jiraUrl; + private String username; + private String password; + private String jiraProject; + private String issueType = "1"; + private String issuePriority = "1"; + + private String buildResultsURL; + + public void setBuildResultsURL(String buildResultsURL) { + this.buildResultsURL = buildResultsURL; + } + + public void setJiraUrl(String jiraUrl) { + this.jiraUrl = jiraUrl; + } + + public void setJiraProject(String jiraProject) { + this.jiraProject = jiraProject; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setIssueType(String issueType) { + this.issueType = issueType; + } + + public void setIssuePriority(String issuePriority) { + this.issuePriority = issuePriority; + } + + /** + * Validate that all the mandatory parameters were specified in order + * to properly initial the Jabber client service. Note that this is called + * after the configuration file is read. + * + * @throws net.sourceforge.cruisecontrol.CruiseControlException if there was a configuration error. + */ + public void validate() throws CruiseControlException { + + ValidationHelper.assertIsSet(jiraProject, "jiraProject", this.getClass()); + ValidationHelper.assertIsSet(username, "username", this.getClass()); + + ValidationHelper.assertIsSet(password, "password", this.getClass()); + ValidationHelper.assertIsSet(jiraUrl, "jiraUrl", this.getClass()); + + ValidationHelper.assertIsSet(buildResultsURL, "buildresulturl", this.getClass()); + } + + /** + * Publish the results to the Jabber transport via an instant message. + * + * @param cruisecontrolLog + * @throws net.sourceforge.cruisecontrol.CruiseControlException + */ + public void publish(Element cruisecontrolLog) throws CruiseControlException { + XMLLogHelper helper = new XMLLogHelper(cruisecontrolLog); + if (!helper.isBuildNewlyBroken()) { + LOG.debug("No new issue to report. Skipping."); + return; + } + + try { + createJiraIssue(helper); + } catch (Throwable e) { + LOG.error("Unable to send message to Jira", e); + } + } + + protected void createJiraIssue(XMLLogHelper helper) throws javax.xml.rpc.ServiceException, java.rmi.RemoteException, + CruiseControlException { + LOG.info("Creating a test issue on " + jiraUrl + " ..."); + JiraSoapServiceServiceLocator jiraSoapServiceGetter = new JiraSoapServiceServiceLocator(); + + jiraSoapServiceGetter.setJirasoapserviceV2EndpointAddress(jiraUrl + "/rpc/soap/jirasoapservice-v2"); + JiraSoapService jiraSoapService = jiraSoapServiceGetter.getJirasoapserviceV2(); + + String token = jiraSoapService.login(username, password); + + // Create the issue + RemoteIssue issue = new RemoteIssue(); + issue.setProject(jiraProject); + issue.setType(issueType); + issue.setPriority(issuePriority); + issue.setDuedate(Calendar.getInstance()); + + // RemoteComponent component = new RemoteComponent(); + // component.setId("10242"); + // issue.setComponents(new RemoteComponent[]{component}); + + issue.setSummary("CI build failed: " + helper.getBuildTimestamp()); + + // optional + // RemoteVersion version = new RemoteVersion(); + // version.setId("10330"); + // RemoteVersion[] remoteVersions = new RemoteVersion[]{version}; + // issue.setFixVersions(remoteVersions); + + String description = createDescription(helper); + + issue.setDescription(description); + + RemoteIssue returnedIssue = jiraSoapService.createIssue(token, issue); + LOG.info("Successfully created issue " + jiraUrl + "/browse/" + returnedIssue.getKey()); + } + + /** + * Creates the Issue description to be sent to Jira. + * + * @return String that makes up the body of the Issue description + * @throws net.sourceforge.cruisecontrol.CruiseControlException + */ + protected String createDescription(XMLLogHelper logHelper) throws CruiseControlException { + StringBuffer message = new StringBuffer(); + message.append("CruiseControl build failed for project ").append(logHelper.getProjectName()); + message.append("\n").append("\n"); + message.append("Check the build results at: "); + message.append(logHelper.createBuildResultsURL(buildResultsURL)); + return message.toString(); + } +} \ No newline at end of file Index: docs/configxml.html =================================================================== --- docs/configxml.html (revision 3558) +++ docs/configxml.html (working copy) @@ -144,6 +144,7 @@ <httpfile> <include.projects> <jabber> +<jirapublisher> <labelincrementer> <listeners> <lockfilebootstrapper> @@ -336,6 +337,7 @@ <htmlemail/> <http> <jabber/> + <jirapublisher/> <onfailure/> <onsuccess/> <rss/> @@ -7173,6 +7175,11 @@ via a Jabber server using XMPP + <jirapublisher> + 0 .. * + Creates an issue in a Jira issue tracker using SOAP when a build stopped passing. + + <onfailure> 0 .. 1 Executes all publishers specified as child elements if and only @@ -8863,6 +8870,72 @@

top +

<jabber>

+ +
+
+<cruisecontrol>
+  <project>
+    <publishers>
+      <jirapublisher>
+
+ +

Creates an issue in a Jira issue tracker using SOAP when a build stops passing.

+

See the url for information

+ +

Attributes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AttributeRequiredDescription
jiraurlYesThe Jira url.
jiraprojectYesThe name of the jira project.
usernameYesUsername of the account used to create the issue
passwordYesPassword of the account
buildresultsurlYesIssue will include link to the build results.
issuetypeNoType of the issue. Defaults to "1"
issuepiorityNoPriority of the issue. Defaults to "1"
+
+ + +
+ top

<onfailure>

Index: lib/axis-wsdl4j-1.5.1.jar =================================================================== Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream Property changes on: lib/axis-wsdl4j-1.5.1.jar ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Index: lib/commons-discovery-0.2.jar =================================================================== Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream Property changes on: lib/commons-discovery-0.2.jar ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Index: lib/axis-1.3.jar =================================================================== Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream Property changes on: lib/axis-1.3.jar ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Index: lib/jira-rpc-samples-3.6-1.jar =================================================================== Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream Property changes on: lib/jira-rpc-samples-3.6-1.jar ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Index: lib/axis-jaxrpc-1.3.jar =================================================================== Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream Property changes on: lib/axis-jaxrpc-1.3.jar ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Index: lib/commons-logging-1.0.4.jar =================================================================== Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream Property changes on: lib/commons-logging-1.0.4.jar ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Index: src/net/sourceforge/cruisecontrol/default-plugins.properties =================================================================== --- src/net/sourceforge/cruisecontrol/default-plugins.properties (revision 3558) +++ src/net/sourceforge/cruisecontrol/default-plugins.properties (working copy) @@ -72,6 +72,7 @@ # label incrementer -- only one! labelincrementer=net.sourceforge.cruisecontrol.labelincrementers.DefaultLabelIncrementer cvslabelincrementer=net.sourceforge.cruisecontrol.labelincrementers.CVSLabelIncrementer +emptylabelincrementer=net.sourceforge.cruisecontrol.labelincrementers.EmptyLabelIncrementer formattedlabelincrementer=net.sourceforge.cruisecontrol.labelincrementers.FormattedLabelIncrementer propertyfilelabelincrementer=net.sourceforge.cruisecontrol.labelincrementers.PropertyFileLabelIncrementer p4changelistlabelincrementer=net.sourceforge.cruisecontrol.labelincrementers.P4ChangelistLabelIncrementer @@ -89,6 +90,7 @@ htmlemail=net.sourceforge.cruisecontrol.publishers.HTMLEmailPublisher http=net.sourceforge.cruisecontrol.publishers.HTTPPublisher jabber=net.sourceforge.cruisecontrol.publishers.LinkJabberPublisher +jirapublisher=net.sourceforge.cruisecontrol.publishers.JiraPublisher onfailure=net.sourceforge.cruisecontrol.publishers.OnFailurePublisher onsuccess=net.sourceforge.cruisecontrol.publishers.OnSuccessPublisher compoundpublisher=net.sourceforge.cruisecontrol.publishers.CompoundPublisher