├── .gitignore ├── src ├── main │ ├── resources │ │ ├── jenkins │ │ │ └── plugins │ │ │ │ └── hipchat │ │ │ │ ├── HipChatNotifier │ │ │ │ ├── HipChatJobProperty │ │ │ │ │ └── index.jelly │ │ │ │ ├── help.html │ │ │ │ ├── help-sendAs.html │ │ │ │ ├── help-credentialId.html │ │ │ │ ├── help-server.html │ │ │ │ ├── help-cardProvider.html │ │ │ │ ├── help-v2Enabled.html │ │ │ │ ├── help-room.html │ │ │ │ ├── help-notifications.jelly │ │ │ │ ├── help-defaultNotifications.jelly │ │ │ │ ├── global.jelly │ │ │ │ └── config.jelly │ │ │ │ ├── workflow │ │ │ │ └── HipChatSendStep │ │ │ │ │ ├── help-server.html │ │ │ │ │ ├── help-sendAs.html │ │ │ │ │ ├── help-icon.html │ │ │ │ │ ├── help-textFormat.html │ │ │ │ │ ├── help-v2enabled.html │ │ │ │ │ ├── help-room.html │ │ │ │ │ ├── help-failOnError.html │ │ │ │ │ ├── help-color.html │ │ │ │ │ ├── help-credentialId.html │ │ │ │ │ ├── help-message.html │ │ │ │ │ ├── help-notify.html │ │ │ │ │ ├── help.html │ │ │ │ │ └── config.jelly │ │ │ │ ├── ext │ │ │ │ └── tokens │ │ │ │ │ ├── BuildDescriptionMacro │ │ │ │ │ └── help.jelly │ │ │ │ │ ├── BlueOceanUrlMacro │ │ │ │ │ └── help.jelly │ │ │ │ │ ├── TestReportUrlMacro │ │ │ │ │ └── help.jelly │ │ │ │ │ ├── BuildDurationMacro │ │ │ │ │ └── help.jelly │ │ │ │ │ ├── CommitMessageMacro │ │ │ │ │ └── help.jelly │ │ │ │ │ └── HipchatChangesMacro │ │ │ │ │ └── help.jelly │ │ │ │ ├── model │ │ │ │ └── NotificationConfig │ │ │ │ │ └── config.jelly │ │ │ │ └── Messages.properties │ │ ├── index.jelly │ │ └── schema │ │ │ ├── icon.json │ │ │ ├── description.json │ │ │ ├── notification.json │ │ │ └── card.json │ ├── java │ │ └── jenkins │ │ │ └── plugins │ │ │ └── hipchat │ │ │ ├── CardProviderDescriptor.java │ │ │ ├── exceptions │ │ │ ├── InvalidResponseCodeException.java │ │ │ └── NotificationException.java │ │ │ ├── utils │ │ │ ├── GuiceUtils.java │ │ │ ├── BuildUtils.java │ │ │ ├── TokenMacroUtils.java │ │ │ └── CredentialUtils.java │ │ │ ├── impl │ │ │ ├── NoopCardProvider.java │ │ │ ├── HipChatV1Service.java │ │ │ ├── HipChatV2Service.java │ │ │ └── DefaultCardProvider.java │ │ │ ├── ext │ │ │ ├── httpclient │ │ │ │ ├── TLSSocketFactory.java │ │ │ │ └── ProxyRoutePlanner.java │ │ │ └── tokens │ │ │ │ ├── BlueOceanUrlMacro.java │ │ │ │ ├── BuildDescriptionMacro.java │ │ │ │ ├── BuildDurationMacro.java │ │ │ │ ├── TestReportUrlMacro.java │ │ │ │ ├── CommitMessageMacro.java │ │ │ │ └── HipchatChangesMacro.java │ │ │ ├── model │ │ │ ├── MatrixTriggerMode.java │ │ │ ├── Constants.java │ │ │ ├── NotificationConfig.java │ │ │ └── NotificationType.java │ │ │ ├── CardProvider.java │ │ │ ├── upgrade │ │ │ └── ConfigurationMigrator.java │ │ │ ├── HipChatService.java │ │ │ ├── workflow │ │ │ └── HipChatSendStep.java │ │ │ └── HipChatNotifier.java │ └── webapp │ │ ├── help-projectConfig-credential.html │ │ ├── help-projectConfig-hipChatMessages.html │ │ └── help-projectConfig-hipChatRoom.html └── test │ ├── resources │ └── logging.properties │ └── java │ └── jenkins │ └── plugins │ └── hipchat │ ├── ext │ └── tokens │ │ ├── BuildDescriptionMacroTest.java │ │ ├── CommitMessageMacroTest.java │ │ ├── BuildDurationMacroTest.java │ │ ├── AbstractChangeLogMacroTest.java │ │ └── HipchatChangesMacroTest.java │ ├── model │ └── NotificationTypeTest.java │ ├── utils │ └── BuildUtilsTest.java │ └── workflow │ └── HipChatSendStepTest.java ├── Jenkinsfile ├── README.markdown └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | hipchat.iml 2 | target 3 | .classpath 4 | .idea 5 | .project 6 | .settings 7 | bin 8 | work 9 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/HipChatNotifier/HipChatJobProperty/index.jelly: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | /* `buildPlugin` step provided by: https://github.com/jenkins-infra/pipeline-library */ 4 | buildPlugin() 5 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/HipChatNotifier/help.html: -------------------------------------------------------------------------------- 1 |
2 | This plugin allows to post build notifications to HipChat chat rooms.
3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/workflow/HipChatSendStep/help-server.html: -------------------------------------------------------------------------------- 1 |
2 | Allows overriding the HipChat Plugin default HipChat Server. 3 |
-------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/workflow/HipChatSendStep/help-sendAs.html: -------------------------------------------------------------------------------- 1 |
2 | Allows overriding the HipChat Plugin default Send As value (v1 API only). 3 |
-------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/workflow/HipChatSendStep/help-icon.html: -------------------------------------------------------------------------------- 1 |
2 | Link to an icon that should be displayed in the notification, when cards are enabled. Defaults to Jenkins logo. 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/workflow/HipChatSendStep/help-textFormat.html: -------------------------------------------------------------------------------- 1 |
2 | Enable this setting to send the notification in text format. HTML format is used if this setting is disabled. 3 |
4 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/CardProviderDescriptor.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat; 2 | 3 | import hudson.model.Descriptor; 4 | 5 | public abstract class CardProviderDescriptor extends Descriptor { 6 | } 7 | -------------------------------------------------------------------------------- /src/test/resources/logging.properties: -------------------------------------------------------------------------------- 1 | handlers= java.util.logging.ConsoleHandler 2 | .level=SEVERE 3 | java.util.logging.ConsoleHandler.level = SEVERE 4 | java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/workflow/HipChatSendStep/help-v2enabled.html: -------------------------------------------------------------------------------- 1 |
2 | Allows overriding the HipChat Plugin default API Version to use - v1 or v2.
3 | hipchatSend Workflow step defaults to v2 API 4 |
-------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/workflow/HipChatSendStep/help-room.html: -------------------------------------------------------------------------------- 1 |
2 | Allows overriding the HipChat Plugin default room.
3 | hipchatSend room: "room-name", message: "Build Started: ${env.JOB_NAME} ${env.BUILD_NUMBER}" 4 |
-------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/ext/tokens/BuildDescriptionMacro/help.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 |
$${BUILD_DESCRIPTION}
4 |
5 | Displays the build's description. 6 |
7 |
8 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 5 |
6 | This plugin is a HipChat notifier that can publish build status to HipChat rooms. 7 |
8 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/ext/tokens/BlueOceanUrlMacro/help.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 |
$${BLUE_OCEAN_URL}
4 |
5 | Displays a Blue Ocean enabled URL for the current build. 6 |
7 |
8 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/ext/tokens/TestReportUrlMacro/help.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 |
$${TEST_REPORT_URL}
4 |
5 | Displays a direct URL pointing to the current build's test report. 6 |
7 |
8 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/ext/tokens/BuildDurationMacro/help.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 |
$${BUILD_DURATION}
4 |
5 | Displays the build's duration in human readable format (e.g.: "42 sec") 6 |
7 |
8 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/workflow/HipChatSendStep/help-failOnError.html: -------------------------------------------------------------------------------- 1 |
2 | If set to true, then the step will abort the Workflow run as a failure if there is an error sending message.
3 | hipchatSend failOnError: true, message: "Build Started: ${env.JOB_NAME} ${env.BUILD_NUMBER}" 4 |
-------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/workflow/HipChatSendStep/help-color.html: -------------------------------------------------------------------------------- 1 |
2 | OPTIONAL Background color for message.
3 | Valid values: YELLOW, GREEN, RED, PURPLE, GRAY, RANDOM.
4 | Defaults to 'GRAY'.
5 | hipchatSend color: "YELLOW", message: "Build Started: ${env.JOB_NAME} ${env.BUILD_NUMBER}" 6 |
-------------------------------------------------------------------------------- /src/main/webapp/help-projectConfig-credential.html: -------------------------------------------------------------------------------- 1 |
2 | The credential selected here will be used to send out the notification. If no credential is specified here, the 3 | credential selected in the global configuration will be used instead. 4 |
5 | In case a new credential needs to be created, make sure that the newly created credential uses "Secret text" kind. 6 |
7 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/HipChatNotifier/help-sendAs.html: -------------------------------------------------------------------------------- 1 |
2 |

Optionally specify a name to send the notifications as. If not specified, messages will be sent as "Jenkins".

3 |

Only used in v1 mode, in v2 mode the OAuth2 access token's label will be used instead.

4 |

The value must be less than 15 characters long.

5 |
-------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/exceptions/InvalidResponseCodeException.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.exceptions; 2 | 3 | import jenkins.plugins.hipchat.Messages; 4 | 5 | public class InvalidResponseCodeException extends NotificationException { 6 | 7 | public InvalidResponseCodeException(int responseCode) { 8 | super(Messages.InvalidResponseCode(responseCode)); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/exceptions/NotificationException.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.exceptions; 2 | 3 | public class NotificationException extends Exception { 4 | 5 | public NotificationException(String message) { 6 | super(message); 7 | } 8 | 9 | public NotificationException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/schema/icon.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "url": { 6 | "type": "string", 7 | "minLength": 1 8 | }, 9 | "url@2x": { 10 | "type": "string", 11 | "minLength": 1 12 | } 13 | }, 14 | "required": [ 15 | "url" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/workflow/HipChatSendStep/help-credentialId.html: -------------------------------------------------------------------------------- 1 |
2 | The credential selected here will be used to send out the notification. If no credential is specified here, the 3 | credential selected in the global configuration will be used instead. 4 |
5 | In case a new credential needs to be created, make sure that the newly created credential uses "Secret text" kind. 6 |
7 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/workflow/HipChatSendStep/help-message.html: -------------------------------------------------------------------------------- 1 |
2 | REQUIRED The message body
3 | Valid length range: 1 - 10000.
4 | Message may include global variables, for example environment and currentBuild variables:
5 | 6 | hipchatSend "${env.JOB_NAME} ${env.BUILD_NUMBER} status: ${currentBuild.result} (Open)" 7 | 8 |
-------------------------------------------------------------------------------- /src/main/webapp/help-projectConfig-hipChatMessages.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Configures the message that will be displayed in the room. If left blank a default value will be used. 4 | All normal build variables can be used with the notation $VAR_NAME. For a complete list of additional 5 | variables supported, please check out the help message of the Notifications setting. 6 |

7 |
8 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/HipChatNotifier/help-credentialId.html: -------------------------------------------------------------------------------- 1 |
2 | Stores credential necessary for this plugin to send out notifications to HipChat rooms. The credential selected here 3 | will be used as a default value if there is no credential selected at the job level. 4 |
5 | In case a new credential needs to be created, make sure that the newly created credential uses "Secret text" kind. 6 |
7 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/workflow/HipChatSendStep/help-notify.html: -------------------------------------------------------------------------------- 1 |
2 | OPTIONAL Whether this message should trigger a user notification (change the tab color, play a sound, notify mobile phones, etc). Each recipient's notification preferences are taken into account.
3 | Defaults to false.
4 | hipchatSend notify: true, message: "Build Started: ${env.JOB_NAME} ${env.BUILD_NUMBER}" 5 |
-------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/utils/GuiceUtils.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.utils; 2 | 3 | import jenkins.model.Jenkins; 4 | 5 | public class GuiceUtils { 6 | 7 | public static T get(Class clazz) { 8 | Jenkins jenkins = Jenkins.getInstance(); 9 | if (jenkins != null) { 10 | return jenkins.getInjector().getInstance(clazz); 11 | } 12 | throw new IllegalStateException("Jenkins instance is not available"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/ext/tokens/CommitMessageMacro/help.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 |
$${COMMIT_MESSAGE}
4 |
5 | Displays the content of the last commit message associated with the current build's changeset. 6 |
7 |
escape
8 |
If true, the commit message will be HTML escaped. Defaults to true.
9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/HipChatNotifier/help-server.html: -------------------------------------------------------------------------------- 1 |
2 | Specify the host name of the HipChat server here: 3 |
    4 |
  • If you are using HipChat from the cloud, you can leave this field empty or just use 'api.hipchat.com'.
  • 5 |
  • If you are running a self-hosted HipChat server, specify the hostname in this field.
  • 6 |
7 | Do not specify the protocol in this setting, the plugin will always connect to the HipChat server using HTTPS 8 | protocol. 9 |
10 | -------------------------------------------------------------------------------- /src/main/resources/schema/description.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "value": { 6 | "type": "string", 7 | "minLength": 1, 8 | "maxLength": 1000 9 | }, 10 | "format": { 11 | "type": "string", 12 | "minLength": 1, 13 | "enum": ["html", "text"] 14 | } 15 | }, 16 | "required": [ 17 | "value", 18 | "format" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/main/webapp/help-projectConfig-hipChatRoom.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Enter the room names to which notifications should be sent. Note that this can include names of 4 | rooms OR room id numbers, e.g. "Dev Team", and that multiple values may appear comma separated. 5 | While names are more readable, room numbers will not change over time and are therefore more resilient. 6 | Use your admin token to get a list of room ids here. 7 |

8 |
9 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/HipChatNotifier/help-cardProvider.html: -------------------------------------------------------------------------------- 1 |
2 | The CardProvider implementation selected here will determine how the HipChat notification cards are being 3 | constructed. Out of the box two implementation exist: one that creates an activity card with some extra test 4 | details, and one that disables cards altogether.
5 | Custom card providers can be implemented using Jenkins's extension system, and will be offered as an option here 6 | when found by the extension lookup mechanism. 7 |
-------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/HipChatNotifier/help-v2Enabled.html: -------------------------------------------------------------------------------- 1 |
2 | Globally enable v2 API usage. 3 |

When v2 API is enabled, the auth tokens stored within the Jenkins configuration will have to be valid OAuth2 4 | access tokens with send_notification scope. The "Send As" setting value will not have an effect, as HipChat will 5 | use the OAuth2 access token's "label" as sender.

6 |

When disabled, the auth tokens needs to be regular HipChat v1 API tokens and the value of the "Send As" setting 7 | is obeyed.

8 |
9 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/workflow/HipChatSendStep/help.html: -------------------------------------------------------------------------------- 1 |
2 | Simple step for sending a HipChat message to designated room.
3 | Use the advanced settings to override the HipChat Plugin global configuration to include: HipChat Server, API Token, API Version (1 or 2) and Send As (v1 API only).
4 | Please see the HipChat Plugin global configuration for more details on the fields. 5 | 6 | Usage Example:
7 | 8 | hipchatSend "Build Started - ${env.JOB_NAME} ${env.BUILD_NUMBER} (Open)" 9 | 10 |
-------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/utils/BuildUtils.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.utils; 2 | 3 | import hudson.model.Result; 4 | import hudson.model.Run; 5 | import javax.inject.Singleton; 6 | 7 | @Singleton 8 | public class BuildUtils { 9 | 10 | public Result findPreviousBuildResult(Run run) { 11 | do { 12 | run = run.getPreviousBuild(); 13 | if (run == null || run.isBuilding()) { 14 | return null; 15 | } 16 | } while (run.getResult() == Result.ABORTED || run.getResult() == Result.NOT_BUILT); 17 | return run.getResult(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/ext/tokens/HipchatChangesMacro/help.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 |
$${HIPCHAT_CHANGES}
4 |
5 | The Hipchat plugin's idea of short changelog report 6 | (e.g.: "Started by changes from alice (42 file(s) changed)"). May be deprecated in the future in favor of the 7 | $${CHANGES} variable. 8 |
9 |
$${HIPCHAT_CHANGES_OR_CAUSE}
10 |
11 | Either returns the short changelog (as defined above), or the build cause if no change was detected. 12 |
13 |
14 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/HipChatNotifier/help-room.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Enter the name of the room to which notifications should be sent. Note that this can be either the name of the 4 | room OR the id number, e.g. "Dev Team", and that multiple names may appear comma separated. 5 | While names are more readable, room numbers will not change over time and are therefore more resilient. 6 | Use your admin token to get a list of room ids here. 7 |

8 |

9 | You can customize the room name per-project, but should always enter a default here. 10 |

11 | The HipChat Message Api is used to send the message. 12 |

13 |
14 | -------------------------------------------------------------------------------- /src/test/java/jenkins/plugins/hipchat/ext/tokens/BuildDescriptionMacroTest.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.ext.tokens; 2 | 3 | import static jenkins.plugins.hipchat.model.Constants.*; 4 | import static org.assertj.core.api.Assertions.*; 5 | import static org.mockito.BDDMockito.*; 6 | 7 | import hudson.model.Run; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.Mock; 11 | import org.mockito.runners.MockitoJUnitRunner; 12 | 13 | @RunWith(MockitoJUnitRunner.class) 14 | public class BuildDescriptionMacroTest { 15 | 16 | @Mock 17 | private Run run; 18 | private BuildDescriptionMacro macro = new BuildDescriptionMacro(); 19 | 20 | @Test 21 | public void shouldReturnBuildDuration() { 22 | given(run.getDescription()).willReturn("hello world description"); 23 | String result = macro.evaluate(run, null, null, BUILD_DESCRIPTION, null, null); 24 | 25 | assertThat(result).isEqualTo("hello world description"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/impl/NoopCardProvider.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.impl; 2 | 3 | import hudson.Extension; 4 | import hudson.model.Run; 5 | import hudson.model.TaskListener; 6 | import jenkins.plugins.hipchat.CardProvider; 7 | import jenkins.plugins.hipchat.CardProviderDescriptor; 8 | import jenkins.plugins.hipchat.Messages; 9 | import jenkins.plugins.hipchat.model.notifications.Card; 10 | import jenkins.plugins.hipchat.model.notifications.Icon; 11 | 12 | @Extension 13 | public class NoopCardProvider extends CardProvider { 14 | 15 | @Override 16 | public Card getCard(Run run, TaskListener taskListener, Icon icon, String message) { 17 | return null; 18 | } 19 | 20 | @Override 21 | public CardProviderDescriptor getDescriptor() { 22 | return new DescriptorImpl(); 23 | } 24 | 25 | @Extension 26 | public static class DescriptorImpl extends CardProviderDescriptor { 27 | 28 | @Override 29 | public String getDisplayName() { 30 | return Messages.NoopCardProvider(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/model/NotificationConfig/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ${it.getStatus()} 12 | 13 | 14 | 15 | 16 | ${it.toString()} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/HipChatNotifier/help-notifications.jelly: -------------------------------------------------------------------------------- 1 |
2 |

3 | Notifications defined here will override all default notifications configured in the global plugin settings. 4 | In case the list is left empty, the default notifications will be sent out. 5 |

6 |

Notification types must occur only once within this setting.

7 |

8 | If the message template is left empty, the project's default message template will be used. If that's empty as 9 | well, then the plugin falls back to the default message templates. 10 |

11 |

12 | The card icon controls what icon to display when card notifications are enabled. When left empty, the default 13 | Jenkins logo will be displayed. 14 |

15 |

16 | The message template may contain any normal build variable (using the $VAR_NAME notation). 17 | Additionally the following variables are provided for convenience: 18 |

19 | 20 |
21 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/ext/httpclient/TLSSocketFactory.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.ext.httpclient; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import javax.net.ssl.SSLSocket; 7 | import org.apache.http.conn.ssl.SSLConnectionSocketFactory; 8 | import org.apache.http.ssl.SSLContexts; 9 | 10 | public class TLSSocketFactory extends SSLConnectionSocketFactory { 11 | 12 | public TLSSocketFactory() { 13 | super(SSLContexts.createDefault(), getDefaultHostnameVerifier()); 14 | } 15 | 16 | @Override 17 | protected void prepareSocket(SSLSocket socket) throws IOException { 18 | String[] supportedProtocols = socket.getSupportedProtocols(); 19 | List protocols = new ArrayList(5); 20 | for (String supportedProtocol : supportedProtocols) { 21 | if (!supportedProtocol.startsWith("SSL")) { 22 | protocols.add(supportedProtocol); 23 | } 24 | } 25 | socket.setEnabledProtocols(protocols.toArray(new String[protocols.size()])); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/model/MatrixTriggerMode.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Borrowed from https://github.com/jenkinsci/email-ext-plugin 3 | */ 4 | package jenkins.plugins.hipchat.model; 5 | 6 | import jenkins.plugins.hipchat.Messages; 7 | import org.jvnet.localizer.Localizable; 8 | 9 | /** 10 | * Controls when the e-mail gets sent in case of the matrix project. 11 | */ 12 | public enum MatrixTriggerMode { 13 | 14 | ONLY_PARENT(Messages._MatrixTriggerMode_OnlyParent(), true, false), 15 | ONLY_CONFIGURATIONS(Messages._MatrixTriggerMode_OnlyConfigurations(), false, true), 16 | BOTH(Messages._MatrixTriggerMode_Both(), true, true); // traditional default behaviour 17 | 18 | private final Localizable description; 19 | 20 | public final boolean forParent; 21 | public final boolean forChild; 22 | 23 | private MatrixTriggerMode(Localizable description, boolean forParent, boolean forChild) { 24 | this.description = description; 25 | this.forParent = forParent; 26 | this.forChild = forChild; 27 | } 28 | 29 | public String getDescription() { 30 | return description.toString(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/resources/schema/notification.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "from": { 6 | "type": "string", 7 | "minLength": 0, 8 | "maxLength": 64 9 | }, 10 | "message_format": { 11 | "type": "string", 12 | "enum": ["html", "text"], 13 | "default": "html" 14 | }, 15 | "color": { 16 | "type": "string", 17 | "enum": ["yellow", "green", "red", "purple", "gray", "random"], 18 | "default": "yellow" 19 | }, 20 | "attach_to": { 21 | "type": "string", 22 | "minLength": 0, 23 | "maxLength": 36 24 | }, 25 | "notify": { 26 | "type": "boolean", 27 | "default": false 28 | }, 29 | "message": { 30 | "type": "string", 31 | "minLength": 1, 32 | "maxLength": 10000 33 | }, 34 | "card": { 35 | "$ref": "card.json#/" 36 | } 37 | }, 38 | "required": [ 39 | "message" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/utils/TokenMacroUtils.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.utils; 2 | 3 | import hudson.model.Run; 4 | import hudson.scm.ChangeLogSet; 5 | import hudson.scm.ChangeLogSet.Entry; 6 | import java.lang.reflect.Method; 7 | import java.util.Collections; 8 | import java.util.List; 9 | import java.util.logging.Level; 10 | import java.util.logging.Logger; 11 | 12 | public class TokenMacroUtils { 13 | 14 | private static final Logger LOGGER = Logger.getLogger(TokenMacroUtils.class.getName()); 15 | 16 | public static ChangeLogSet getFirstChangeSet(Run run) { 17 | List> entries; 18 | try { 19 | Method method = run.getClass().getMethod("getChangeSets"); 20 | entries = (List>) method.invoke(run); 21 | } catch (ReflectiveOperationException roe) { 22 | LOGGER.log(Level.WARNING, String.format("Unable to retrieve changesets from Run instance: %s", run), 23 | roe); 24 | entries = Collections.emptyList(); 25 | } 26 | return entries.isEmpty() ? null : entries.get(0); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/jenkins/plugins/hipchat/ext/tokens/CommitMessageMacroTest.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.ext.tokens; 2 | 3 | import static jenkins.plugins.hipchat.model.Constants.*; 4 | import static org.assertj.core.api.Assertions.*; 5 | 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.mockito.runners.MockitoJUnitRunner; 10 | 11 | @RunWith(MockitoJUnitRunner.class) 12 | public class CommitMessageMacroTest extends AbstractChangeLogMacroTest { 13 | 14 | private CommitMessageMacro macro; 15 | 16 | @Before 17 | public void configure() { 18 | macro = new CommitMessageMacro(); 19 | } 20 | 21 | @Test 22 | public void shouldReturnCommitMessageEscaped() { 23 | String result = macro.evaluate(build, null, COMMIT_MESSAGE); 24 | 25 | assertThat(result).isNotNull().isEqualTo("<strong>foo</strong>"); 26 | } 27 | 28 | @Test 29 | public void shouldReturnCommitMessageAsIsWhenEscapingIsDisabled() { 30 | macro.escape = false; 31 | 32 | String result = macro.evaluate(build, null, COMMIT_MESSAGE); 33 | 34 | assertThat(result).isNotNull().isEqualTo("foo"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/ext/httpclient/ProxyRoutePlanner.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.ext.httpclient; 2 | 3 | import hudson.ProxyConfiguration; 4 | import java.net.Proxy; 5 | import org.apache.http.HttpException; 6 | import org.apache.http.HttpHost; 7 | import org.apache.http.HttpRequest; 8 | import org.apache.http.impl.conn.DefaultRoutePlanner; 9 | import org.apache.http.impl.conn.DefaultSchemePortResolver; 10 | import org.apache.http.protocol.HttpContext; 11 | 12 | public class ProxyRoutePlanner extends DefaultRoutePlanner { 13 | 14 | private final ProxyConfiguration proxyConfiguration; 15 | 16 | public ProxyRoutePlanner(ProxyConfiguration proxyConfiguration) { 17 | super(DefaultSchemePortResolver.INSTANCE); 18 | this.proxyConfiguration = proxyConfiguration; 19 | } 20 | 21 | @Override 22 | protected HttpHost determineProxy(HttpHost target, HttpRequest request, HttpContext context) throws HttpException { 23 | Proxy proxy = proxyConfiguration.createProxy(target.getHostName()); 24 | if (Proxy.Type.DIRECT.equals(proxy.type())) { 25 | return null; 26 | } 27 | 28 | return new HttpHost(proxyConfiguration.name, proxyConfiguration.port); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/ext/tokens/BlueOceanUrlMacro.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.ext.tokens; 2 | 3 | import static jenkins.plugins.hipchat.model.Constants.*; 4 | 5 | import hudson.Extension; 6 | import hudson.FilePath; 7 | import hudson.model.AbstractBuild; 8 | import hudson.model.Run; 9 | import hudson.model.TaskListener; 10 | import java.util.Collections; 11 | import java.util.List; 12 | import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider; 13 | import org.jenkinsci.plugins.tokenmacro.DataBoundTokenMacro; 14 | 15 | @Extension 16 | public class BlueOceanUrlMacro extends DataBoundTokenMacro { 17 | 18 | @Override 19 | public String evaluate(AbstractBuild context, TaskListener listener, String macroName) { 20 | return evaluate(context, null, listener, macroName); 21 | } 22 | 23 | @Override 24 | public String evaluate(Run run, FilePath workspace, TaskListener listener, String macroName) { 25 | return DisplayURLProvider.get().getRunURL(run); 26 | } 27 | 28 | @Override 29 | public boolean acceptsMacroName(String macroName) { 30 | return BLUE_OCEAN_URL.equals(macroName); 31 | } 32 | 33 | @Override 34 | public List getAcceptedMacroNames() { 35 | return Collections.singletonList(BLUE_OCEAN_URL); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/jenkins/plugins/hipchat/ext/tokens/BuildDurationMacroTest.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.ext.tokens; 2 | 3 | import static jenkins.plugins.hipchat.model.Constants.*; 4 | import static org.assertj.core.api.Assertions.*; 5 | import static org.mockito.BDDMockito.*; 6 | 7 | import hudson.model.AbstractBuild; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.Mock; 11 | import org.mockito.internal.util.reflection.Whitebox; 12 | import org.mockito.runners.MockitoJUnitRunner; 13 | 14 | @RunWith(MockitoJUnitRunner.class) 15 | public class BuildDurationMacroTest { 16 | 17 | @Mock 18 | private AbstractBuild build; 19 | private BuildDurationMacro macro = new BuildDurationMacro(); 20 | 21 | @Test 22 | public void shouldReturnBuildDuration() { 23 | given(build.getDuration()).willReturn(39000l); 24 | String result = macro.evaluate(build, null, BUILD_DURATION, null, null); 25 | 26 | assertThat(result).isEqualTo("39 sec"); 27 | } 28 | 29 | @Test 30 | public void shouldReturnBuildDurationForUnfinishedBuilds() { 31 | given(build.getDuration()).willReturn(0l); 32 | Whitebox.setInternalState(build, "timestamp", System.currentTimeMillis() - 5000l); 33 | String result = macro.evaluate(build, null, BUILD_DURATION, null, null); 34 | 35 | assertThat(result).isNotEqualTo("0 ms"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/jenkins/plugins/hipchat/model/NotificationTypeTest.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.model; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import jenkins.plugins.hipchat.Messages; 6 | import org.junit.Test; 7 | 8 | public class NotificationTypeTest { 9 | 10 | @Test 11 | public void testAbortedStatus() { 12 | assertThat(NotificationType.ABORTED.getStatus().contains(Messages.Aborted())); 13 | } 14 | 15 | @Test 16 | public void testBackToNormalStatus() { 17 | assertThat(NotificationType.BACK_TO_NORMAL.getStatus().contains(Messages.BackToNormal())); 18 | } 19 | 20 | @Test 21 | public void testFailureStatus() { 22 | assertThat(NotificationType.FAILURE.getStatus().contains(Messages.Failure())); 23 | } 24 | 25 | @Test 26 | public void testNotBuiltStatus() { 27 | assertThat(NotificationType.NOT_BUILT.getStatus().contains(Messages.NotBuilt())); 28 | } 29 | 30 | @Test 31 | public void testStartedStatus() { 32 | assertThat(NotificationType.STARTED.getStatus().contains(Messages.Started())); 33 | } 34 | 35 | @Test 36 | public void testSuccessStatus() { 37 | assertThat(NotificationType.SUCCESS.getStatus().contains(Messages.Success())); 38 | } 39 | 40 | @Test 41 | public void testUnstableStatus() { 42 | assertThat(NotificationType.UNSTABLE.getStatus().contains(Messages.Unstable())); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/ext/tokens/BuildDescriptionMacro.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.ext.tokens; 2 | 3 | import static jenkins.plugins.hipchat.model.Constants.*; 4 | 5 | import com.google.common.collect.ListMultimap; 6 | import hudson.Extension; 7 | import hudson.FilePath; 8 | import hudson.model.AbstractBuild; 9 | import hudson.model.Run; 10 | import hudson.model.TaskListener; 11 | import java.util.Collections; 12 | import java.util.List; 13 | import java.util.Map; 14 | import org.jenkinsci.plugins.tokenmacro.TokenMacro; 15 | 16 | @Extension 17 | public class BuildDescriptionMacro extends TokenMacro { 18 | 19 | @Override 20 | public boolean acceptsMacroName(String macroName) { 21 | return BUILD_DESCRIPTION.equals(macroName); 22 | } 23 | 24 | @Override 25 | public String evaluate(AbstractBuild context, TaskListener listener, String macroName, 26 | Map arguments, ListMultimap argumentMultimap) { 27 | return evaluate(context, null, listener, macroName, arguments, argumentMultimap); 28 | } 29 | 30 | @Override 31 | public String evaluate(Run run, FilePath workspace, TaskListener listener, 32 | String macroName, Map arguments, ListMultimap argumentMultimap) { 33 | return run.getDescription(); 34 | } 35 | 36 | @Override 37 | public List getAcceptedMacroNames() { 38 | return Collections.singletonList(BUILD_DESCRIPTION); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/workflow/HipChatSendStep/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ${it} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/HipChatNotifier/help-defaultNotifications.jelly: -------------------------------------------------------------------------------- 1 |
2 |

3 | The default notifications listed here will be used when a given project does not specify which events should 4 | trigger HipChat notifications. Please note that the default notifications are only sent if the HipChat 5 | Notifications Post Build Action is added to the job (otherwise the plugin won't get invoked). In other words: to 6 | utilize the default notifications, you'll need to do 3 things: 7 |

    8 |
  • Configure default notifications in the global settings (i.e. here)
  • 9 |
  • Add the HipChat Post Build Action to the job where you want to trigger the default notifications.
  • 10 |
  • Leave the job level Notifications setting completely empty.
  • 11 |
12 |

13 |

Notification types must occur only once within this setting.

14 |

15 | If the message template is left empty, the project's default message template will be used. If that's empty as 16 | well, then the plugin falls back to the default message templates. 17 |

18 |

19 | The card icon controls what icon to display when card notifications are enabled. When left empty, the default 20 | Jenkins logo will be displayed. 21 |

22 |

23 | The message template may contain any normal build variable (using the $VAR_NAME notation). 24 | Additionally the following variables are provided for convenience: 25 |

26 | 27 |
28 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/ext/tokens/BuildDurationMacro.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.ext.tokens; 2 | 3 | import static jenkins.plugins.hipchat.model.Constants.*; 4 | 5 | import com.google.common.collect.ListMultimap; 6 | import hudson.Extension; 7 | import hudson.FilePath; 8 | import hudson.Util; 9 | import hudson.model.AbstractBuild; 10 | import hudson.model.Run; 11 | import hudson.model.TaskListener; 12 | import java.util.Collections; 13 | import java.util.List; 14 | import java.util.Map; 15 | import org.jenkinsci.plugins.tokenmacro.TokenMacro; 16 | 17 | @Extension 18 | public class BuildDurationMacro extends TokenMacro { 19 | 20 | @Override 21 | public boolean acceptsMacroName(String macroName) { 22 | return BUILD_DURATION.equals(macroName); 23 | } 24 | 25 | @Override 26 | public String evaluate(AbstractBuild context, TaskListener listener, String macroName, 27 | Map arguments, ListMultimap argumentMultimap) { 28 | return evaluate(context, null, listener, macroName, arguments, argumentMultimap); 29 | } 30 | 31 | @Override 32 | public String evaluate(Run run, FilePath workspace, TaskListener listener, String macroName, 33 | Map arguments, ListMultimap argumentMultimap) { 34 | long duration = run.getDuration(); 35 | if (duration == 0l) { 36 | return Util.getTimeSpanString(System.currentTimeMillis() - run.getStartTimeInMillis()); 37 | } else { 38 | return Util.getTimeSpanString(duration); 39 | } 40 | } 41 | 42 | @Override 43 | public List getAcceptedMacroNames() { 44 | return Collections.singletonList(BUILD_DURATION); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/ext/tokens/TestReportUrlMacro.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.ext.tokens; 2 | 3 | import static jenkins.plugins.hipchat.model.Constants.*; 4 | 5 | import com.google.common.collect.ListMultimap; 6 | import hudson.Extension; 7 | import hudson.FilePath; 8 | import hudson.model.AbstractBuild; 9 | import hudson.model.Run; 10 | import hudson.model.TaskListener; 11 | import hudson.tasks.test.AbstractTestResultAction; 12 | import java.util.Collections; 13 | import java.util.List; 14 | import java.util.Map; 15 | import org.jenkinsci.plugins.tokenmacro.TokenMacro; 16 | 17 | @Extension 18 | public class TestReportUrlMacro extends TokenMacro { 19 | 20 | @Override 21 | public boolean acceptsMacroName(String macroName) { 22 | return TEST_REPORT_URL.equals(macroName); 23 | } 24 | 25 | @Override 26 | public String evaluate(AbstractBuild context, TaskListener listener, String macroName, 27 | Map arguments, ListMultimap argumentMultimap) { 28 | return evaluate(context, null, listener, macroName, arguments, argumentMultimap); 29 | } 30 | 31 | @Override 32 | public String evaluate(Run run, FilePath workspace, TaskListener listener, String macroName, 33 | Map arguments, ListMultimap argumentMultimap) { 34 | AbstractTestResultAction testResults = run.getAction(AbstractTestResultAction.class); 35 | if (testResults != null) { 36 | return BUILD_URL_MACRO + testResults.getUrlName(); 37 | } else { 38 | return ""; 39 | } 40 | } 41 | 42 | @Override 43 | public boolean hasNestedContent() { 44 | return true; 45 | } 46 | 47 | @Override 48 | public List getAcceptedMacroNames() { 49 | return Collections.singletonList(TEST_REPORT_URL); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/jenkins/plugins/hipchat/utils/BuildUtilsTest.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.utils; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.mockito.BDDMockito.*; 5 | 6 | import hudson.model.Result; 7 | import hudson.model.Run; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.Mock; 11 | import org.mockito.runners.MockitoJUnitRunner; 12 | 13 | @RunWith(MockitoJUnitRunner.class) 14 | public class BuildUtilsTest { 15 | 16 | @Mock 17 | private Run run; 18 | @Mock 19 | private Run firstRun; 20 | @Mock 21 | private Run secondRun; 22 | @Mock 23 | private Run thirdRun; 24 | private final BuildUtils buildUtils = new BuildUtils(); 25 | 26 | @Test 27 | public void shouldFindPreviousResultAcrossMultipleIterations() { 28 | given(run.getPreviousBuild()).willReturn(thirdRun); 29 | given(thirdRun.getResult()).willReturn(Result.ABORTED); 30 | given(thirdRun.getPreviousBuild()).willReturn(secondRun); 31 | given(secondRun.getResult()).willReturn(Result.NOT_BUILT); 32 | given(secondRun.getPreviousBuild()).willReturn(firstRun); 33 | given(firstRun.getResult()).willReturn(Result.SUCCESS); 34 | 35 | Result result = buildUtils.findPreviousBuildResult(run); 36 | 37 | assertThat(result).isEqualTo(Result.SUCCESS); 38 | } 39 | 40 | @Test 41 | public void shouldReturnNullIfPreviousBuildIsNull() { 42 | given(run.getPreviousBuild()).willReturn(null); 43 | 44 | Result result = buildUtils.findPreviousBuildResult(run); 45 | 46 | assertThat(result).isNull(); 47 | } 48 | 49 | @Test 50 | public void shouldReturnNullIfPreviousBuildIsStillRunning() { 51 | given(run.getPreviousBuild()).willReturn(firstRun); 52 | given(firstRun.getResult()).willReturn(Result.SUCCESS); 53 | given(firstRun.isBuilding()).willReturn(true); 54 | 55 | Result result = buildUtils.findPreviousBuildResult(run); 56 | 57 | assertThat(result).isNull(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/HipChatNotifier/global.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
${%Notify Room}${%Text Format}${%Notification Type}${%Color}${%Card Icon}${%Message template}
33 | 34 | 35 | 36 |
37 |
38 |
39 | 41 |
42 |
43 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/Messages.properties: -------------------------------------------------------------------------------- 1 | # Localization for config pages 2 | DisplayName=HipChat Notifications 3 | MatrixTriggerMode.OnlyParent=Trigger only the parent job 4 | MatrixTriggerMode.OnlyConfigurations=Trigger for each configuration 5 | MatrixTriggerMode.Both=Trigger for parent and each configuration 6 | InvalidSendAs=When using the v1 API the Send As value MUST be configured and it MUST be less than 15 characters. 7 | DefaultCardProvider=Default cards 8 | NoopCardProvider=Do not display cards 9 | TestNotification=Test Notification {0} 10 | TestNotificationSent=Test Notification Sent 11 | CredentialMissing=Unable to find credential with ID "{0}". Have you configured a valid 'Secret text' credential in the \ 12 | config? 13 | TestNotificationFailed=Test Notification Failed:\n{0} 14 | HipChatSendStepDisplayName=Send HipChat Message 15 | 16 | # Messages sent to HipChat 17 | StartWithChanges=Started by changes from {0} ({1} file(s) changed) 18 | 19 | Started=Build started 20 | BackToNormal=Build is back to normal 21 | Success=Build successful 22 | Failure=Build failed 23 | Aborted=Build aborted 24 | NotBuilt=Module not built 25 | Unstable=Build is unstable 26 | NoChanges=No changes 27 | 28 | JobStarted=$JOB_NAME #$BUILD_NUMBER $STATUS ($HIPCHAT_CHANGES_OR_CAUSE) (View build) 29 | JobCompleted=$JOB_NAME #$BUILD_NUMBER $STATUS after $BUILD_DURATION (View build) 30 | 31 | # Messages displayed on Cards 32 | CardTitle=$JOB_NAME 33 | TestsSuccessful=Tests Successful 34 | TestsFailed=Tests Failed 35 | TestsSkipped=Tests Skipped 36 | BuildOutput=Build Output 37 | Here=Here 38 | TestReport=Test Report 39 | 40 | # Messages to display in the build logs 41 | NotificationSuccessful=[INFO] HipChat notification sent to the following rooms: {0} 42 | InvalidResponseCode=Unexpected response code from HipChat: {0} 43 | IOException=Unexpected IO error occurred while sending notification: {0} 44 | MacroEvaluationFailed=[ERROR] Failed to evaluate tokens in the provided message template due to: {0} 45 | NotificationFailed=[ERROR] HipChat notification failed with error message: {0} 46 | UnresolvedMacro=[WARNING] An error occurred while trying to resolve a macro for the HipChat card: {0} 47 | 48 | # Error Messages 49 | MessageRequiredError=HipChat message not sent. Message property must be supplied. 50 | -------------------------------------------------------------------------------- /src/main/resources/jenkins/plugins/hipchat/HipChatNotifier/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ${it.description} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
${%Notify Room}${%Text Format}${%Notification Type}${%Color}${%Card Icon}${%Message template}
30 | 31 | 32 | 33 |
34 |
35 |
36 | 37 | 38 | 39 | 40 | Default: '${descriptor.getStartJobMessageDefault()}' 41 | 42 | 43 | 44 | 45 | Default: '${descriptor.getCompleteJobMessageDefault()}' 46 | 47 | 48 |
49 | -------------------------------------------------------------------------------- /src/test/java/jenkins/plugins/hipchat/ext/tokens/AbstractChangeLogMacroTest.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.ext.tokens; 2 | 3 | import static org.mockito.BDDMockito.*; 4 | 5 | import hudson.model.AbstractBuild; 6 | import hudson.model.User; 7 | import hudson.scm.ChangeLogSet; 8 | import hudson.scm.ChangeLogSet.Entry; 9 | import java.util.Arrays; 10 | import java.util.Collection; 11 | import java.util.Iterator; 12 | import java.util.List; 13 | import org.junit.Before; 14 | import org.mockito.Mock; 15 | 16 | public class AbstractChangeLogMacroTest { 17 | 18 | @Mock 19 | protected AbstractBuild build; 20 | 21 | @Before 22 | public void setup() { 23 | given(build.hasChangeSetComputed()).willReturn(true); 24 | User mockUser = mock(User.class); 25 | given(mockUser.getDisplayName()).willReturn("alice"); 26 | 27 | ChangeLogSet.Entry mockEntry = mock(ChangeLogSet.Entry.class); 28 | given(mockEntry.getAuthor()).willReturn(mockUser); 29 | Collection mockList = mock(List.class); 30 | given(mockList.size()).willReturn(20); 31 | given(mockEntry.getAffectedFiles()).willReturn(mockList); 32 | 33 | mockUser = mock(User.class); 34 | given(mockUser.getDisplayName()).willReturn("bob"); 35 | ChangeLogSet.Entry secondMockEntry = mock(ChangeLogSet.Entry.class); 36 | given(secondMockEntry.getAuthor()).willReturn(mockUser); 37 | mockList = mock(List.class); 38 | given(mockList.size()).willReturn(22); 39 | given(secondMockEntry.getAffectedFiles()).willReturn(mockList); 40 | given(secondMockEntry.getMsgEscaped()).willReturn("<strong>foo</strong>\n\nMore info about fix"); 41 | given(secondMockEntry.getMsg()).willReturn("foo"); 42 | 43 | given(build.getChangeSet()).willReturn(new FakeChangeLogSet(mockEntry, secondMockEntry)); 44 | } 45 | 46 | protected class FakeChangeLogSet extends ChangeLogSet { 47 | 48 | private final Entry[] entries; 49 | 50 | protected FakeChangeLogSet(Entry... entries) { 51 | super(null); 52 | this.entries = entries; 53 | } 54 | 55 | @Override 56 | public boolean isEmptySet() { 57 | return true; 58 | } 59 | 60 | @Override 61 | public Iterator iterator() { 62 | return Arrays.asList(entries).iterator(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/ext/tokens/CommitMessageMacro.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.ext.tokens; 2 | 3 | import static java.util.logging.Level.*; 4 | import static jenkins.plugins.hipchat.model.Constants.*; 5 | 6 | import hudson.Extension; 7 | import hudson.FilePath; 8 | import hudson.Util; 9 | import hudson.model.AbstractBuild; 10 | import hudson.model.Run; 11 | import hudson.model.TaskListener; 12 | import hudson.scm.ChangeLogSet; 13 | import hudson.scm.ChangeLogSet.Entry; 14 | import java.util.Collections; 15 | import java.util.List; 16 | import java.util.logging.Logger; 17 | import jenkins.plugins.hipchat.utils.TokenMacroUtils; 18 | import org.jenkinsci.plugins.tokenmacro.DataBoundTokenMacro; 19 | 20 | @Extension 21 | public class CommitMessageMacro extends DataBoundTokenMacro { 22 | 23 | private static final Logger LOGGER = Logger.getLogger(CommitMessageMacro.class.getName()); 24 | 25 | @Parameter 26 | public boolean escape = true; 27 | 28 | @Override 29 | public String evaluate(AbstractBuild context, TaskListener listener, String macroName) { 30 | if (!context.hasChangeSetComputed()) { 31 | LOGGER.log(FINE, "No changeset computed for job {0}", context.getProject().getFullDisplayName()); 32 | } else { 33 | return getCommitMessage(context.getChangeSet()); 34 | } 35 | return ""; 36 | } 37 | 38 | @Override 39 | public String evaluate(Run run, FilePath workspace, TaskListener listener, String macroName) { 40 | if (run instanceof AbstractBuild) { 41 | return evaluate((AbstractBuild) run, listener, macroName); 42 | } else { 43 | return getCommitMessage(TokenMacroUtils.getFirstChangeSet(run)); 44 | } 45 | } 46 | 47 | @Override 48 | public boolean acceptsMacroName(String macroName) { 49 | return COMMIT_MESSAGE.equals(macroName); 50 | } 51 | 52 | @Override 53 | public List getAcceptedMacroNames() { 54 | return Collections.singletonList(COMMIT_MESSAGE); 55 | } 56 | 57 | private String getCommitMessage(ChangeLogSet changeSet) { 58 | if (changeSet != null) { 59 | Object[] items = changeSet.getItems(); 60 | if (items != null && items.length > 0) { 61 | Entry entry = (Entry) items[items.length - 1]; 62 | LOGGER.log(FINEST, "Entry {0}", entry); 63 | return stripMessage(escape ? entry.getMsgEscaped() : entry.getMsg()); 64 | } 65 | } 66 | return ""; 67 | } 68 | 69 | private String stripMessage(String message) { 70 | return Util.fixNull(message).split("\r?\n")[0]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/model/Constants.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.model; 2 | 3 | public class Constants { 4 | 5 | public static final String DEFAULT_ICON_URL = "https://bit.ly/2ctIstd"; 6 | public static final String STATUS = "STATUS"; 7 | public static final String HIPCHAT_MESSAGE_TEMPLATE = "HIPCHAT_MESSAGE_TEMPLATE"; 8 | 9 | //legacy token macro names 10 | public static final String JOB_DISPLAY_NAME = "JOB_DISPLAY_NAME"; 11 | public static final String PROJECT_DISPLAY_NAME = "PROJECT_DISPLAY_NAME"; 12 | public static final String CAUSE = "CAUSE"; 13 | public static final String URL = "URL"; 14 | public static final String DURATION = "DURATION"; 15 | public static final String CHANGES_OR_CAUSE = "CHANGES_OR_CAUSE"; 16 | public static final String CHANGES = "CHANGES"; 17 | public static final String COMMIT_MESSAGE_TEXT = "COMMIT_MESSAGE_TEXT"; 18 | public static final String COMMIT_MESSAGE = "COMMIT_MESSAGE"; 19 | public static final String SUCCESS_TEST_COUNT = "SUCCESS_TEST_COUNT"; 20 | public static final String SKIPPED_TEST_COUNT = "SKIPPED_TEST_COUNT"; 21 | public static final String FAILED_TEST_COUNT = "FAILED_TEST_COUNT"; 22 | public static final String TEST_COUNT = "TEST_COUNT"; 23 | 24 | //supported token macro names 25 | public static final String BLUE_OCEAN_URL = "BLUE_OCEAN_URL"; 26 | public static final String BUILD_DESCRIPTION = "BUILD_DESCRIPTION"; 27 | public static final String BUILD_DURATION = "BUILD_DURATION"; 28 | public static final String TEST_REPORT_URL = "TEST_REPORT_URL"; 29 | public static final String HIPCHAT_CHANGES = "HIPCHAT_CHANGES"; 30 | public static final String HIPCHAT_CHANGES_OR_CAUSE = "HIPCHAT_CHANGES_OR_CAUSE"; 31 | 32 | //token migration constants 33 | public static final String BUILD_DURATION_MACRO = "${" + BUILD_DURATION + "}"; 34 | public static final String PROJECT_DISPLAY_NAME_MACRO = "${PROJECT_DISPLAY_NAME}"; 35 | public static final String TOTAL_TEST_COUNT_MACRO = "${TEST_COUNTS,var=\"total\"}"; 36 | public static final String FAILED_TEST_COUNT_MACRO = "${TEST_COUNTS,var=\"fail\"}"; 37 | public static final String SKIPPED_TEST_COUNT_MACRO = "${TEST_COUNTS,var=\"skip\"}"; 38 | public static final String SUCCESS_TEST_COUNT_MACRO = "${TEST_COUNTS,var=\"pass\"}"; 39 | public static final String TEST_REPORT_URL_MACRO = "${" + TEST_REPORT_URL + "}"; 40 | public static final String BUILD_URL_MACRO = "${BUILD_URL}"; 41 | public static final String COMMIT_MESSAGE_MACRO = "${" + COMMIT_MESSAGE + "}"; 42 | public static final String ESCAPED_COMMIT_MESSAGE_MACRO = "${" + COMMIT_MESSAGE + ",escape=false}"; 43 | public static final String HIPCHAT_CHANGES_MACRO = "${" + HIPCHAT_CHANGES + "}"; 44 | public static final String HIPCHAT_CHANGES_OR_CAUSE_MACRO = "${" + HIPCHAT_CHANGES_OR_CAUSE + "}"; 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/jenkins/plugins/hipchat/ext/tokens/HipchatChangesMacroTest.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.ext.tokens; 2 | 3 | import static jenkins.plugins.hipchat.model.Constants.*; 4 | import static org.assertj.core.api.Assertions.*; 5 | import static org.mockito.BDDMockito.*; 6 | 7 | import hudson.model.AbstractProject; 8 | import hudson.model.CauseAction; 9 | import hudson.model.ItemGroup; 10 | import hudson.model.User; 11 | import hudson.scm.ChangeLogSet; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.mockito.Mock; 15 | import org.mockito.runners.MockitoJUnitRunner; 16 | 17 | @RunWith(MockitoJUnitRunner.class) 18 | public class HipchatChangesMacroTest extends AbstractChangeLogMacroTest { 19 | 20 | @Mock 21 | private AbstractProject project; 22 | @Mock 23 | private ItemGroup itemGroup; 24 | private final HipchatChangesMacro macro = new HipchatChangesMacro(); 25 | 26 | @Test 27 | public void shouldReturnChangesCorrectly() { 28 | String result = macro.evaluate(build, null, CHANGES, null, null); 29 | assertThat(result).isNotNull().isNotEmpty().contains("alice", "bob", "42"); 30 | } 31 | 32 | @Test 33 | public void shouldNotFailIfAffectedFilesCannotBeDetermined() { 34 | given(build.getParent()).willReturn(project); 35 | given(project.getParent()).willReturn(itemGroup); 36 | given(itemGroup.getFullDisplayName()).willReturn(""); 37 | given(build.hasChangeSetComputed()).willReturn(true); 38 | User mockUser = mock(User.class); 39 | given(mockUser.getDisplayName()).willReturn("alice"); 40 | ChangeLogSet.Entry mockEntry = mock(ChangeLogSet.Entry.class); 41 | given(mockEntry.getAuthor()).willReturn(mockUser); 42 | given(mockEntry.getAffectedFiles()).willThrow(UnsupportedOperationException.class); 43 | given(build.getChangeSet()).willReturn(new FakeChangeLogSet(mockEntry)); 44 | 45 | String result = macro.evaluate(build, null, CHANGES, null, null); 46 | 47 | assertThat(result).isNotNull().isNotEmpty().contains("alice", "0"); 48 | } 49 | 50 | @Test 51 | public void changesOrCauseContainsChangesForAbstractBuild() { 52 | String result = macro.evaluate(build, null, CHANGES_OR_CAUSE, null, null); 53 | 54 | assertThat(result).isNotNull().isNotEmpty().contains("alice", "bob", "42"); 55 | } 56 | 57 | @Test 58 | public void changesOrCauseReturnsCauseIfChangesAreNotFound() { 59 | given(build.getParent()).willReturn(project); 60 | given(project.getParent()).willReturn(itemGroup); 61 | given(itemGroup.getFullDisplayName()).willReturn(""); 62 | given(build.hasChangeSetComputed()).willReturn(false); 63 | CauseAction mockAction = mock(CauseAction.class); 64 | given(mockAction.getShortDescription()).willReturn("buildCause"); 65 | given(build.getAction(eq(CauseAction.class))).willReturn(mockAction); 66 | 67 | String result = macro.evaluate(build, null, CHANGES_OR_CAUSE, null, null); 68 | 69 | assertThat(result).isNotNull().isNotEmpty().contains("buildCause"); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/CardProvider.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat; 2 | 3 | import hudson.ExtensionPoint; 4 | import hudson.model.AbstractDescribableImpl; 5 | import hudson.model.Run; 6 | import hudson.model.TaskListener; 7 | import jenkins.plugins.hipchat.model.notifications.Attribute; 8 | import jenkins.plugins.hipchat.model.notifications.Card; 9 | import jenkins.plugins.hipchat.model.notifications.Icon; 10 | import jenkins.plugins.hipchat.model.notifications.Value; 11 | 12 | /** 13 | * An extension point that can be used to allow full control over HipChat notification cards for individual 14 | * notifications. The provider itself is globally configured, so any job-specific behavior would be the responsibility 15 | * of the plugin itself. 16 | */ 17 | public abstract class CardProvider extends AbstractDescribableImpl implements ExtensionPoint { 18 | 19 | @Override 20 | public CardProviderDescriptor getDescriptor() { 21 | return (CardProviderDescriptor) super.getDescriptor(); 22 | } 23 | 24 | /** 25 | * Returns a card corresponding to the build notification. 26 | * 27 | * @param run The build run. 28 | * @param taskListener The taskListener associated with the current build. 29 | * @param message The fully resolved notification message. 30 | * @return The card that has been constructed for this notification. May be null if no card should be displayed. 31 | * @deprecated Implement {@link #getCard(hudson.model.Run, hudson.model.TaskListener, 32 | * jenkins.plugins.hipchat.model.notifications.Icon, java.lang.String)} instead. 33 | */ 34 | @Deprecated 35 | public Card getCard(Run run, TaskListener taskListener, String message) { 36 | return getCard(run, taskListener, null, message); 37 | } 38 | 39 | /** 40 | * Returns a card corresponding to the build notification. 41 | * 42 | * @param run The build run. 43 | * @param taskListener The taskListener associated with the current build. 44 | * @param icon The icon to include in the message. May be null. 45 | * @param message The fully resolved notification message. 46 | * @return The card that has been constructed for this notification. May be null if no card should be displayed. 47 | */ 48 | public abstract Card getCard(Run run, TaskListener taskListener, Icon icon, String message); 49 | 50 | /** 51 | * A simple factory method to easily create attribute for the Card. Attributes are individual information fields 52 | * that can be displayed on the card. See HipChat API reference for more details. 53 | * 54 | * @param label The label for the data. 55 | * @param value The data value corresponding to the label. 56 | * @param style The style that determines the color of the background for the value. 57 | * @param url Used as the href of a link that will be generated (the link's title will be the 'value') on the card. 58 | * @return The Attribute representation of the provided details. 59 | */ 60 | protected Attribute attribute(String label, String value, Value.Style style, String url) { 61 | return new Attribute().withLabel(label).withValue(new Value().withLabel(value).withStyle(style).withUrl(url)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/model/NotificationConfig.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.model; 2 | 3 | import hudson.Extension; 4 | import hudson.model.Describable; 5 | import hudson.model.Descriptor; 6 | import jenkins.model.Jenkins; 7 | import jenkins.plugins.hipchat.model.notifications.Icon; 8 | import jenkins.plugins.hipchat.model.notifications.Notification.Color; 9 | import org.apache.commons.lang.StringUtils; 10 | import org.kohsuke.stapler.DataBoundConstructor; 11 | 12 | public class NotificationConfig implements Describable { 13 | 14 | private final boolean notifyEnabled; 15 | private final boolean textFormat; 16 | private final NotificationType notificationType; 17 | private final Color color; 18 | private final String icon; 19 | private final String messageTemplate; 20 | 21 | @DataBoundConstructor 22 | public NotificationConfig(boolean notifyEnabled, boolean textFormat, NotificationType notificationType, Color color, 23 | String icon, String messageTemplate) { 24 | this.notifyEnabled = notifyEnabled; 25 | this.textFormat = textFormat; 26 | this.notificationType = notificationType; 27 | this.color = color; 28 | this.icon = icon; 29 | this.messageTemplate = messageTemplate; 30 | } 31 | 32 | public boolean isNotifyEnabled() { 33 | return notifyEnabled; 34 | } 35 | 36 | public NotificationType getNotificationType() { 37 | return notificationType; 38 | } 39 | 40 | public boolean isTextFormat() { 41 | return textFormat; 42 | } 43 | 44 | public Color getColor() { 45 | return color; 46 | } 47 | 48 | public String getIcon() { 49 | return icon; 50 | } 51 | 52 | public Icon getIconObject() { 53 | return StringUtils.isEmpty(icon) ? null : new Icon().withUrl(icon); 54 | } 55 | 56 | public String getMessageTemplate() { 57 | return messageTemplate; 58 | } 59 | 60 | /** 61 | * Returns a copy of the notification config that will contain the same settings, but the message template will be 62 | * overridden with the freshly provided one. 63 | * 64 | * @param messageTemplate The new message template to use. 65 | * @return A new {@link NotificationConfig} instance that has its message template updated. 66 | */ 67 | public NotificationConfig overrideMessageTemplate(String messageTemplate) { 68 | return new NotificationConfig(notifyEnabled, textFormat, notificationType, color, icon, messageTemplate); 69 | } 70 | 71 | @Override 72 | public Descriptor getDescriptor() { 73 | return Jenkins.getInstance().getDescriptorByType(DescriptorImpl.class); 74 | } 75 | 76 | @Override 77 | public String toString() { 78 | return "Notification{" + "notifyEnabled=" + notifyEnabled + ", notificationType=" + notificationType 79 | + ", color=" + color + ", icon=" + icon + ", messageTemplate=" + messageTemplate + '}'; 80 | } 81 | 82 | @Extension 83 | public static final class DescriptorImpl extends Descriptor { 84 | 85 | @Override 86 | public String getDisplayName() { 87 | return "HipChat Notification"; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/jenkins/plugins/hipchat/workflow/HipChatSendStepTest.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.workflow; 2 | 3 | import hudson.model.Result; 4 | import jenkins.plugins.hipchat.Messages; 5 | import jenkins.plugins.hipchat.model.Constants; 6 | import jenkins.plugins.hipchat.model.notifications.Notification.Color; 7 | import org.jenkinsci.plugins.workflow.job.WorkflowRun; 8 | import org.jenkinsci.plugins.workflow.steps.StepConfigTester; 9 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 10 | import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; 11 | import org.junit.Rule; 12 | import org.junit.Test; 13 | import org.junit.runners.model.Statement; 14 | import org.jvnet.hudson.test.RestartableJenkinsRule; 15 | 16 | public class HipChatSendStepTest { 17 | 18 | @Rule 19 | public RestartableJenkinsRule story = new RestartableJenkinsRule(); 20 | 21 | @Test 22 | public void configRoundTrip() throws Exception { 23 | story.addStep(new Statement() { 24 | @Override 25 | public void evaluate() throws Throwable { 26 | HipChatSendStep step1 = new HipChatSendStep("message"); 27 | step1.color = Color.GREEN; 28 | step1.room = "room"; 29 | step1.icon = Constants.DEFAULT_ICON_URL; 30 | step1.v2enabled = true; 31 | step1.notify = false; 32 | 33 | HipChatSendStep step2 = new StepConfigTester(story.j).configRoundTrip(step1); 34 | story.j.assertEqualDataBoundBeans(step1, step2); 35 | } 36 | }); 37 | } 38 | 39 | @Test 40 | public void emptyMessageShouldLogError() throws Exception { 41 | story.addStep(new Statement() { 42 | @Override 43 | public void evaluate() throws Throwable { 44 | WorkflowJob job = story.j.jenkins.createProject(WorkflowJob.class, "workflow"); 45 | //just define message 46 | job.setDefinition(new CpsFlowDefinition("hipchatSend(message: '');", true)); 47 | WorkflowRun run = story.j.assertBuildStatusSuccess(job.scheduleBuild2(0).get()); 48 | //should result in an error in log 49 | story.j.assertLogContains(Messages.MessageRequiredError(), run); 50 | } 51 | }); 52 | } 53 | 54 | @Test 55 | public void emptyMessageShouldFailBuildIfEnabled() throws Exception { 56 | story.addStep(new Statement() { 57 | @Override 58 | public void evaluate() throws Throwable { 59 | WorkflowJob job = story.j.jenkins.createProject(WorkflowJob.class, "workflow"); 60 | //just define message 61 | job.setDefinition(new CpsFlowDefinition("hipchatSend(message: '', failOnError: true);", true)); 62 | WorkflowRun run = story.j.assertBuildStatus(Result.FAILURE, job.scheduleBuild2(0).get()); 63 | //should result in an error in log 64 | story.j.assertLogContains(Messages.MessageRequiredError(), run); 65 | } 66 | }); 67 | } 68 | 69 | @Test 70 | public void buildFailsOnHipChatError() throws Exception { 71 | story.addStep(new Statement() { 72 | @Override 73 | public void evaluate() throws Throwable { 74 | WorkflowJob job = story.j.jenkins.createProject(WorkflowJob.class, "workflow"); 75 | job.setDefinition(new CpsFlowDefinition("hipchatSend(message: 'message', server: 'server'," 76 | + " token: 'token', room: 'room', color: 'GREEN', v2enabled: true, failOnError: true);", true)); 77 | story.j.assertBuildStatus(Result.FAILURE, job.scheduleBuild2(0).get()); 78 | } 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/impl/HipChatV1Service.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.impl; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | import java.util.logging.Level; 8 | import java.util.logging.Logger; 9 | import jenkins.plugins.hipchat.HipChatService; 10 | import jenkins.plugins.hipchat.Messages; 11 | import jenkins.plugins.hipchat.exceptions.InvalidResponseCodeException; 12 | import jenkins.plugins.hipchat.exceptions.NotificationException; 13 | import jenkins.plugins.hipchat.model.notifications.Notification; 14 | import org.apache.http.NameValuePair; 15 | import org.apache.http.client.entity.UrlEncodedFormEntity; 16 | import org.apache.http.client.methods.CloseableHttpResponse; 17 | import org.apache.http.client.methods.HttpPost; 18 | import org.apache.http.impl.client.CloseableHttpClient; 19 | import org.apache.http.message.BasicNameValuePair; 20 | 21 | public class HipChatV1Service extends HipChatService { 22 | 23 | private static final Logger logger = Logger.getLogger(HipChatV1Service.class.getName()); 24 | private static final String[] DEFAULT_ROOMS = new String[0]; 25 | 26 | private final String server; 27 | private final String token; 28 | private final String[] roomIds; 29 | private final String sendAs; 30 | 31 | public HipChatV1Service(String server, String token, String roomIds, String sendAs) { 32 | this.server = server; 33 | this.token = token; 34 | this.roomIds = roomIds == null ? DEFAULT_ROOMS : roomIds.split("\\s*,\\s*"); 35 | this.sendAs = sendAs; 36 | } 37 | 38 | @Override 39 | public void publish(Notification notification) throws NotificationException { 40 | for (String roomId : roomIds) { 41 | logger.log(Level.FINE, "Posting: {0} to {1}: {2}", new Object[]{sendAs, roomId, notification}); 42 | CloseableHttpClient httpClient = getHttpClient(); 43 | CloseableHttpResponse httpResponse = null; 44 | 45 | try { 46 | HttpPost post = new HttpPost("https://" + server + "/v1/rooms/message"); 47 | List nvps = new ArrayList<>(6); 48 | nvps.add(new BasicNameValuePair("auth_token", token)); 49 | nvps.add(new BasicNameValuePair("from", sendAs)); 50 | nvps.add(new BasicNameValuePair("room_id", roomId)); 51 | nvps.add(new BasicNameValuePair("message", notification.getMessage())); 52 | nvps.add(new BasicNameValuePair("message_format", notification.getMessageFormat().value())); 53 | nvps.add(new BasicNameValuePair("color", notification.getColor().value())); 54 | nvps.add(new BasicNameValuePair("notify", notification.isNotify() ? "1" : "0")); 55 | post.setEntity(new UrlEncodedFormEntity(nvps, "UTF-8")); 56 | 57 | httpResponse = httpClient.execute(post); 58 | int responseCode = httpResponse.getStatusLine().getStatusCode(); 59 | // Always read response to ensure the inputstream is closed 60 | String response = readResponse(httpResponse.getEntity()); 61 | 62 | if (responseCode != 200) { 63 | logger.log(Level.WARNING, "HipChat post may have failed. ResponseCode: {0}, Response: {1}", 64 | new Object[]{responseCode, response}); 65 | throw new InvalidResponseCodeException(responseCode); 66 | } 67 | } catch (IOException ioe) { 68 | logger.log(Level.WARNING, "An IO error occurred while posting HipChat notification", ioe); 69 | throw new NotificationException(Messages.IOException(ioe.toString())); 70 | } finally { 71 | closeQuietly(httpResponse, httpClient); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/impl/HipChatV2Service.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.impl; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.ObjectWriter; 5 | import hudson.Util; 6 | import java.io.IOException; 7 | import java.util.logging.Level; 8 | import java.util.logging.Logger; 9 | import jenkins.plugins.hipchat.HipChatService; 10 | import jenkins.plugins.hipchat.Messages; 11 | import jenkins.plugins.hipchat.exceptions.InvalidResponseCodeException; 12 | import jenkins.plugins.hipchat.exceptions.NotificationException; 13 | import jenkins.plugins.hipchat.model.notifications.Notification; 14 | import org.apache.http.client.methods.CloseableHttpResponse; 15 | import org.apache.http.client.methods.HttpPost; 16 | import org.apache.http.entity.ContentType; 17 | import org.apache.http.entity.StringEntity; 18 | import org.apache.http.impl.client.CloseableHttpClient; 19 | 20 | public class HipChatV2Service extends HipChatService { 21 | 22 | private static final Logger LOGGER = Logger.getLogger(HipChatV2Service.class.getName()); 23 | private static final String[] DEFAULT_ROOMS = new String[0]; 24 | private static final int MAX_MESSAGE_LENGTH = 10000; 25 | private static final ObjectWriter writer = new ObjectMapper().writerWithView(Notification.class); 26 | 27 | private final String server; 28 | private final String token; 29 | private final String[] roomIds; 30 | 31 | public HipChatV2Service(String server, String token, String roomIds) { 32 | this.server = server; 33 | this.token = token; 34 | this.roomIds = roomIds == null ? DEFAULT_ROOMS : roomIds.split("\\s*,\\s*"); 35 | } 36 | 37 | @Override 38 | public void publish(Notification notification) throws NotificationException { 39 | if (notification.getMessage().length() > MAX_MESSAGE_LENGTH) { 40 | LOGGER.log(Level.INFO, "HipChat notification message was too long, truncating to maximum message length"); 41 | notification.setMessage(notification.getMessage().substring(0, MAX_MESSAGE_LENGTH - 3) + "..."); 42 | } 43 | for (String roomId : roomIds) { 44 | LOGGER.log(Level.FINE, "Posting to {0} room: {1}", new Object[]{roomId, notification}); 45 | CloseableHttpClient httpClient = getHttpClient(); 46 | CloseableHttpResponse httpResponse = null; 47 | 48 | try { 49 | HttpPost post = new HttpPost("https://" + server + "/v2/room/" + Util.rawEncode(roomId) 50 | + "/notification"); 51 | post.addHeader("Authorization", "Bearer " + token); 52 | post.setEntity(new StringEntity(writer.writeValueAsString(notification), ContentType.APPLICATION_JSON)); 53 | 54 | httpResponse = httpClient.execute(post); 55 | int responseCode = httpResponse.getStatusLine().getStatusCode(); 56 | // Always read response to ensure the inputstream is closed 57 | String response = readResponse(httpResponse.getEntity()); 58 | 59 | if (responseCode != 204) { 60 | if (LOGGER.isLoggable(Level.WARNING)) { 61 | LOGGER.log(Level.WARNING, "HipChat post may have failed. ResponseCode: {0}, Response: {1}", 62 | new Object[]{responseCode, response}); 63 | throw new InvalidResponseCodeException(responseCode); 64 | } 65 | } 66 | } catch (IOException ioe) { 67 | LOGGER.log(Level.WARNING, "An IO error occurred while posting HipChat notification", ioe); 68 | throw new NotificationException(Messages.IOException(ioe.toString())); 69 | } finally { 70 | closeQuietly(httpResponse, httpClient); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/upgrade/ConfigurationMigrator.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.upgrade; 2 | 3 | import static jenkins.plugins.hipchat.utils.GuiceUtils.get; 4 | 5 | import hudson.BulkChange; 6 | import hudson.Extension; 7 | import hudson.Plugin; 8 | import hudson.Util; 9 | import hudson.model.AbstractProject; 10 | import hudson.model.listeners.ItemListener; 11 | import hudson.util.VersionNumber; 12 | 13 | import java.io.IOException; 14 | import java.util.logging.Level; 15 | import java.util.logging.Logger; 16 | 17 | import jenkins.model.Jenkins; 18 | import jenkins.plugins.hipchat.HipChatNotifier; 19 | import jenkins.plugins.hipchat.HipChatNotifier.DescriptorImpl; 20 | import jenkins.plugins.hipchat.HipChatNotifier.HipChatJobProperty; 21 | import jenkins.plugins.hipchat.utils.CredentialUtils; 22 | 23 | @Extension 24 | public class ConfigurationMigrator extends ItemListener { 25 | 26 | private static final Logger LOGGER = Logger.getLogger(ConfigurationMigrator.class.getName()); 27 | 28 | @Override 29 | public void onLoaded() { 30 | Jenkins jenkins = Jenkins.getInstance(); 31 | HipChatNotifier.DescriptorImpl descriptor = jenkins.getDescriptorByType(DescriptorImpl.class); 32 | Plugin plugin = jenkins.getPlugin("hipchat"); 33 | if (plugin == null) { 34 | return; 35 | } 36 | VersionNumber pluginVersion = plugin.getWrapper().getVersionNumber(); 37 | if (pluginVersion.isOlderThan(new VersionNumber(descriptor.getConfigVersion()))) { 38 | return; 39 | } 40 | 41 | for (AbstractProject item : jenkins.getAllItems(AbstractProject.class)) { 42 | HipChatNotifier notifier = item.getPublishersList().get(HipChatNotifier.class); 43 | HipChatJobProperty property = item.getProperty(HipChatJobProperty.class); 44 | BulkChange bc = new BulkChange(item); 45 | try { 46 | if (property != null) { 47 | if (notifier != null) { 48 | notifier.setRoom(property.getRoom()); 49 | notifier.setStartNotification(property.getStartNotification()); 50 | notifier.setNotifyAborted(property.getNotifyAborted()); 51 | notifier.setNotifyBackToNormal(property.getNotifyBackToNormal()); 52 | notifier.setNotifyFailure(property.getNotifyFailure()); 53 | notifier.setNotifyNotBuilt(property.getNotifyNotBuilt()); 54 | notifier.setNotifySuccess(property.getNotifySuccess()); 55 | notifier.setNotifyUnstable(property.getNotifyUnstable()); 56 | notifier.setNotifications(null); 57 | notifier.readResolve(); 58 | } 59 | try { 60 | item.removeProperty(HipChatJobProperty.class); 61 | LOGGER.log(Level.INFO, "Successfully migrated project configuration for build job: {0}", 62 | item.getFullDisplayName()); 63 | } catch (IOException ioe) { 64 | LOGGER.log(Level.WARNING, "An error occurred while trying to update job configuration for " 65 | + item.getName(), ioe); 66 | } 67 | } 68 | if (notifier != null && Util.fixEmpty(notifier.getToken()) != null) { 69 | LOGGER.log(Level.FINER, "Attempting to migrate credentials for job: {0}", 70 | item.getFullDisplayName()); 71 | get(CredentialUtils.class).migrateJobCredential(descriptor, item, notifier); 72 | LOGGER.log(Level.FINER, "Successfully migrated credential for job: {0}", item.getFullDisplayName()); 73 | item.save(); 74 | } 75 | bc.commit(); 76 | } catch (IOException ioe) { 77 | LOGGER.log(Level.SEVERE, "Unable to save configuration for job: " + item.getFullName(), ioe); 78 | } finally { 79 | bc.abort(); 80 | } 81 | } 82 | descriptor.setConfigVersion(pluginVersion.toString()); 83 | descriptor.save(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/ext/tokens/HipchatChangesMacro.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.ext.tokens; 2 | 3 | import static java.util.logging.Level.*; 4 | import static jenkins.plugins.hipchat.model.Constants.*; 5 | 6 | import com.google.common.collect.ImmutableList; 7 | import com.google.common.collect.ListMultimap; 8 | import com.google.common.collect.Sets; 9 | import hudson.Extension; 10 | import hudson.FilePath; 11 | import hudson.model.AbstractBuild; 12 | import hudson.model.CauseAction; 13 | import hudson.model.Run; 14 | import hudson.model.TaskListener; 15 | import hudson.model.User; 16 | import hudson.scm.ChangeLogSet; 17 | import hudson.scm.ChangeLogSet.Entry; 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.Set; 21 | import java.util.logging.Logger; 22 | import jenkins.plugins.hipchat.Messages; 23 | import jenkins.plugins.hipchat.utils.TokenMacroUtils; 24 | import org.apache.commons.lang.StringUtils; 25 | import org.jenkinsci.plugins.tokenmacro.TokenMacro; 26 | 27 | @Extension 28 | public class HipchatChangesMacro extends TokenMacro { 29 | 30 | private static final Logger LOGGER = Logger.getLogger(HipchatChangesMacro.class.getName()); 31 | private static final List SUPPORTED_TOKENS = ImmutableList.of(HIPCHAT_CHANGES, HIPCHAT_CHANGES_OR_CAUSE); 32 | 33 | @Override 34 | public boolean acceptsMacroName(String macroName) { 35 | return SUPPORTED_TOKENS.contains(macroName); 36 | } 37 | 38 | @Override 39 | public String evaluate(AbstractBuild context, TaskListener listener, String macroName, 40 | Map arguments, ListMultimap argumentMultimap) { 41 | ChangeLogSet changeLogSet = null; 42 | if (!context.hasChangeSetComputed()) { 43 | LOGGER.log(FINE, "No changeset computed for job {0}", context.getProject().getFullDisplayName()); 44 | } else { 45 | changeLogSet = context.getChangeSet(); 46 | } 47 | return getChangesOrCause(context, changeLogSet, macroName); 48 | } 49 | 50 | @Override 51 | public String evaluate(Run run, FilePath workspace, TaskListener listener, String macroName, 52 | Map arguments, ListMultimap argumentMultimap) { 53 | if (run instanceof AbstractBuild) { 54 | return evaluate((AbstractBuild) run, listener, macroName, arguments, argumentMultimap); 55 | } else { 56 | return getChangesOrCause(run, TokenMacroUtils.getFirstChangeSet(run), macroName); 57 | } 58 | } 59 | 60 | @Override 61 | public List getAcceptedMacroNames() { 62 | return SUPPORTED_TOKENS; 63 | } 64 | 65 | private String getChangesOrCause(Run run, ChangeLogSet changeSet, String macroName) { 66 | String changes = null; 67 | if (changeSet != null) { 68 | Set authors = Sets.newHashSet(); 69 | int changedFiles = 0; 70 | for (Object o : changeSet.getItems()) { 71 | ChangeLogSet.Entry entry = (Entry) o; 72 | LOGGER.log(FINEST, "Entry {0}", entry); 73 | 74 | User author = entry.getAuthor(); 75 | if (author == null) { 76 | //author may be null in certain cases with git 77 | author = User.getUnknown(); 78 | } 79 | authors.add(author.getDisplayName()); 80 | try { 81 | changedFiles += entry.getAffectedFiles().size(); 82 | } catch (UnsupportedOperationException uoe) { 83 | LOGGER.log(FINE, "Unable to collect the affected files", uoe); 84 | } 85 | } 86 | if (changedFiles == 0 && authors.isEmpty()) { 87 | LOGGER.log(FINE, "No changes detected"); 88 | } else { 89 | changes = Messages.StartWithChanges(StringUtils.join(authors, ", "), changedFiles); 90 | } 91 | } 92 | 93 | if (HIPCHAT_CHANGES.equals(macroName)) { 94 | return changes != null ? changes : Messages.NoChanges(); 95 | } else { 96 | return changes != null ? changes : getCause(run); 97 | } 98 | } 99 | 100 | private String getCause(Run context) { 101 | CauseAction cause = context.getAction(CauseAction.class); 102 | if (cause != null) { 103 | return cause.getShortDescription(); 104 | } 105 | return ""; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/resources/schema/card.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "definitions": { 5 | "icon": { 6 | "oneOf": [ 7 | { 8 | "$ref": "icon.json#/" 9 | }, 10 | { 11 | "type": "string", 12 | "minLength": 1 13 | } 14 | ] 15 | } 16 | }, 17 | "properties": { 18 | "style": { 19 | "type": "string", 20 | "enum": ["file", "image", "application", "link", "media"], 21 | "minLength": 1, 22 | "maxLength": 16 23 | }, 24 | "description": { 25 | "oneOf": [ 26 | { 27 | "$ref": "description.json#/" 28 | }, 29 | { 30 | "type": "string", 31 | "minLength": 1, 32 | "maxLength": 1000 33 | } 34 | ] 35 | }, 36 | "format": { 37 | "type": "string", 38 | "enum": ["compact", "medium"], 39 | "minLength": 1, 40 | "maxLength": 25 41 | }, 42 | "url": { 43 | "type": "string", 44 | "minLength": 1 45 | }, 46 | "title": { 47 | "type": "string", 48 | "minLength": 1, 49 | "maxLength": 500 50 | }, 51 | "thumbnail": { 52 | "type": "object", 53 | "properties": { 54 | "url": { 55 | "type": "string", 56 | "minLength": 1, 57 | "maxLength": 250 58 | }, 59 | "width": { 60 | "type": "number" 61 | }, 62 | "url@2x": { 63 | "type": "string", 64 | "minLength": 1, 65 | "maxLength": 250 66 | }, 67 | "height": { 68 | "type": "number" 69 | } 70 | }, 71 | "required": [ 72 | "url" 73 | ] 74 | }, 75 | "activity": { 76 | "type": "object", 77 | "properties": { 78 | "html": { 79 | "type": "string", 80 | "minLength": 1 81 | }, 82 | "icon": { 83 | "$ref": "#/definitions/icon" 84 | } 85 | }, 86 | "required": [ 87 | "html" 88 | ] 89 | }, 90 | "attributes": { 91 | "type": "array", 92 | "items": { 93 | "type": "object", 94 | "properties": { 95 | "label": { 96 | "type": "string", 97 | "minLength": 1, 98 | "maxLength": 50 99 | }, 100 | "value": { 101 | "type": "object", 102 | "properties": { 103 | "url": { 104 | "type": "string", 105 | "minLength": 1 106 | }, 107 | "style": { 108 | "type": "string", 109 | "enum": ["lozenge-success", "lozenge-error", "lozenge-current", "lozenge-complete", "lozenge-moved", "lozenge"], 110 | "minLength": 1 111 | }, 112 | "label": { 113 | "type": "string", 114 | "minLength": 1 115 | }, 116 | "icon": { 117 | "$ref": "#/definitions/icon" 118 | } 119 | }, 120 | "required": [ 121 | "label" 122 | ] 123 | } 124 | }, 125 | "required": [ 126 | "value" 127 | ] 128 | } 129 | }, 130 | "id": { 131 | "type": "string", 132 | "minLength": 1 133 | }, 134 | "icon": { 135 | "$ref": "#/definitions/icon" 136 | } 137 | }, 138 | "required": [ 139 | "style", 140 | "title", 141 | "id" 142 | ] 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/HipChatService.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat; 2 | 3 | import hudson.ProxyConfiguration; 4 | import hudson.Util; 5 | import java.io.Closeable; 6 | import java.io.IOException; 7 | import jenkins.model.Jenkins; 8 | import jenkins.plugins.hipchat.exceptions.NotificationException; 9 | import jenkins.plugins.hipchat.ext.httpclient.ProxyRoutePlanner; 10 | import jenkins.plugins.hipchat.ext.httpclient.TLSSocketFactory; 11 | import jenkins.plugins.hipchat.model.notifications.Notification; 12 | import jenkins.plugins.hipchat.model.notifications.Notification.Color; 13 | import jenkins.plugins.hipchat.model.notifications.Notification.MessageFormat; 14 | import org.apache.http.HttpEntity; 15 | import org.apache.http.auth.AuthScope; 16 | import org.apache.http.auth.UsernamePasswordCredentials; 17 | import org.apache.http.client.config.RequestConfig; 18 | import org.apache.http.impl.client.BasicCredentialsProvider; 19 | import org.apache.http.impl.client.CloseableHttpClient; 20 | import org.apache.http.impl.client.HttpClientBuilder; 21 | import org.apache.http.impl.client.HttpClients; 22 | import org.apache.http.impl.client.ProxyAuthenticationStrategy; 23 | import org.apache.http.util.EntityUtils; 24 | 25 | public abstract class HipChatService { 26 | 27 | /** 28 | * HTTP Connection timeout when making calls to HipChat. 29 | */ 30 | private static final Integer DEFAULT_TIMEOUT = 10000; 31 | 32 | protected CloseableHttpClient getHttpClient() { 33 | HttpClientBuilder httpClientBuilder = HttpClients.custom() 34 | .setDefaultRequestConfig( 35 | RequestConfig.custom() 36 | .setConnectTimeout(DEFAULT_TIMEOUT).setSocketTimeout(DEFAULT_TIMEOUT).build()) 37 | .setSSLSocketFactory(new TLSSocketFactory()); 38 | 39 | if (Jenkins.getInstance() != null) { 40 | ProxyConfiguration proxy = Jenkins.getInstance().proxy; 41 | 42 | if (proxy != null) { 43 | httpClientBuilder.setRoutePlanner(new ProxyRoutePlanner(proxy)); 44 | if (Util.fixEmpty(proxy.getUserName()) != null) { 45 | BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); 46 | credentialsProvider.setCredentials(new AuthScope(proxy.name, proxy.port), 47 | new UsernamePasswordCredentials(proxy.getUserName(), proxy.getPassword())); 48 | httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); 49 | httpClientBuilder.setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy()); 50 | } 51 | } 52 | } 53 | 54 | return httpClientBuilder.build(); 55 | } 56 | 57 | /** 58 | * Publishes a notification to HipChat. 59 | * 60 | * @param message The message to send. 61 | * @param color The notification color to use. 62 | * @throws NotificationException If there was an error while publishing the notification. 63 | * @deprecated This method currently does not expose all the available HipChat functionalities, use 64 | * {@link #publish(jenkins.plugins.hipchat.model.notifications.Notification)} instead. 65 | */ 66 | @Deprecated 67 | public final void publish(String message, String color) throws NotificationException { 68 | publish(message, color, !color.equalsIgnoreCase("green")); 69 | } 70 | 71 | public void publish(String message, String color, boolean notify) throws NotificationException { 72 | publish(message, color, notify, false); 73 | } 74 | 75 | public void publish(String message, String color, boolean notify, boolean textFormat) throws NotificationException { 76 | publish(new Notification() 77 | .withMessage(message) 78 | .withColor(Color.fromValue(color)) 79 | .withNotify(notify) 80 | .withMessageFormat(textFormat ? MessageFormat.TEXT : MessageFormat.HTML)); 81 | } 82 | 83 | public abstract void publish(Notification notification) throws NotificationException; 84 | 85 | protected final String readResponse(HttpEntity entity) throws IOException { 86 | return entity != null ? EntityUtils.toString(entity) : null; 87 | } 88 | 89 | protected final void closeQuietly(Closeable... closeables) { 90 | if (closeables != null) { 91 | for (Closeable closeable : closeables) { 92 | if (closeable != null) { 93 | try { 94 | closeable.close(); 95 | } catch (IOException ioe) { 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/impl/DefaultCardProvider.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.impl; 2 | 3 | import static jenkins.plugins.hipchat.model.Constants.*; 4 | import static jenkins.plugins.hipchat.model.notifications.Value.Style.*; 5 | 6 | import hudson.Extension; 7 | import hudson.model.Run; 8 | import hudson.model.TaskListener; 9 | import hudson.tasks.test.AbstractTestResultAction; 10 | import java.io.IOException; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.UUID; 14 | import java.util.logging.Level; 15 | import java.util.logging.Logger; 16 | import jenkins.plugins.hipchat.CardProvider; 17 | import jenkins.plugins.hipchat.CardProviderDescriptor; 18 | import jenkins.plugins.hipchat.Messages; 19 | import jenkins.plugins.hipchat.model.Constants; 20 | import jenkins.plugins.hipchat.model.notifications.Activity; 21 | import jenkins.plugins.hipchat.model.notifications.Attribute; 22 | import jenkins.plugins.hipchat.model.notifications.Card; 23 | import jenkins.plugins.hipchat.model.notifications.Card.Style; 24 | import jenkins.plugins.hipchat.model.notifications.Icon; 25 | import org.apache.commons.lang.StringUtils; 26 | import org.jenkinsci.plugins.tokenmacro.MacroEvaluationException; 27 | import org.jenkinsci.plugins.tokenmacro.TokenMacro; 28 | 29 | @Extension 30 | public class DefaultCardProvider extends CardProvider { 31 | 32 | private static final Logger LOGGER = Logger.getLogger(DefaultCardProvider.class.getName()); 33 | 34 | @Override 35 | public Card getCard(Run run, TaskListener taskListener, Icon icon, String message) { 36 | if (icon == null) { 37 | icon = new Icon().withUrl(Constants.DEFAULT_ICON_URL); 38 | } 39 | 40 | try { 41 | return new Card() 42 | .withStyle(Style.APPLICATION) 43 | .withUrl(TokenMacro.expandAll(run, null, taskListener, Constants.BUILD_URL_MACRO)) 44 | .withFormat(Card.Format.MEDIUM) 45 | .withId(UUID.randomUUID().toString()) 46 | .withTitle(TokenMacro.expandAll(run, null, taskListener, Messages.CardTitle(), false, null)) 47 | .withIcon(icon) 48 | .withAttributes(getAttributes(run, taskListener)) 49 | .withActivity(new Activity() 50 | .withHtml(message) 51 | .withIcon(icon)); 52 | } catch (MacroEvaluationException | IOException ex) { 53 | LOGGER.log(Level.WARNING, "Failed to resolve token macros", ex); 54 | } catch (InterruptedException ie) { 55 | Thread.currentThread().interrupt(); 56 | } 57 | 58 | return null; 59 | } 60 | 61 | private List getAttributes(Run run, TaskListener taskListener) 62 | throws IOException, InterruptedException { 63 | List ret = new ArrayList<>(); 64 | if (run.getAction(AbstractTestResultAction.class) != null) { 65 | try { 66 | String count = TokenMacro.expand(run, null, taskListener, SUCCESS_TEST_COUNT_MACRO); 67 | if (StringUtils.isNotEmpty(count)) { 68 | ret.add(attribute(Messages.TestsSuccessful(), count, 69 | "0".equals(count) ? LOZENGE_ERROR : LOZENGE_SUCCESS, null)); 70 | } 71 | count = TokenMacro.expand(run, null, taskListener, FAILED_TEST_COUNT_MACRO); 72 | if (StringUtils.isNotEmpty(count)) { 73 | ret.add(attribute(Messages.TestsFailed(), count, 74 | "0".equals(count) ? LOZENGE_SUCCESS : LOZENGE_ERROR, null)); 75 | } 76 | count = TokenMacro.expand(run, null, taskListener, SKIPPED_TEST_COUNT_MACRO); 77 | if (StringUtils.isNotEmpty(count)) { 78 | ret.add(attribute(Messages.TestsSkipped(), count, 79 | "0".equals(count) ? LOZENGE_SUCCESS : LOZENGE_CURRENT, null)); 80 | } 81 | if (!ret.isEmpty()) { 82 | ret.add(attribute(Messages.TestReport(), Messages.Here(), null, 83 | TokenMacro.expand(run, null, taskListener, TEST_REPORT_URL_MACRO))); 84 | } 85 | } catch (MacroEvaluationException mee) { 86 | taskListener.getLogger().println(Messages.UnresolvedMacro(mee.getMessage())); 87 | } 88 | } 89 | return ret.isEmpty() ? null : ret; 90 | } 91 | 92 | @Override 93 | public CardProviderDescriptor getDescriptor() { 94 | return new DescriptorImpl(); 95 | } 96 | 97 | @Extension 98 | public static class DescriptorImpl extends CardProviderDescriptor { 99 | 100 | @Override 101 | public String getDisplayName() { 102 | return Messages.DefaultCardProvider(); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/model/NotificationType.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.model; 2 | 3 | import static jenkins.plugins.hipchat.model.Constants.*; 4 | import static jenkins.plugins.hipchat.utils.GuiceUtils.*; 5 | 6 | import com.google.common.annotations.VisibleForTesting; 7 | import com.google.common.collect.ImmutableMap; 8 | import hudson.ExtensionList; 9 | import hudson.Util; 10 | import hudson.model.AbstractBuild; 11 | import hudson.model.BuildListener; 12 | import hudson.model.Result; 13 | import java.io.IOException; 14 | import jenkins.model.Jenkins; 15 | import jenkins.plugins.hipchat.CardProvider; 16 | import jenkins.plugins.hipchat.HipChatNotifier.DescriptorImpl; 17 | import jenkins.plugins.hipchat.Messages; 18 | import jenkins.plugins.hipchat.exceptions.NotificationException; 19 | import jenkins.plugins.hipchat.impl.NoopCardProvider; 20 | import jenkins.plugins.hipchat.model.notifications.Notification; 21 | import jenkins.plugins.hipchat.model.notifications.Notification.MessageFormat; 22 | import jenkins.plugins.hipchat.utils.BuildUtils; 23 | import org.jenkinsci.plugins.tokenmacro.MacroEvaluationException; 24 | import org.jenkinsci.plugins.tokenmacro.TokenMacro; 25 | 26 | public enum NotificationType { 27 | 28 | STARTED(true) { 29 | 30 | @Override 31 | public String getStatus() { 32 | return Messages.Started(); 33 | } 34 | }, 35 | ABORTED { 36 | 37 | @Override 38 | public String getStatus() { 39 | return Messages.Aborted(); 40 | } 41 | }, 42 | SUCCESS { 43 | 44 | @Override 45 | public String getStatus() { 46 | return Messages.Success(); 47 | } 48 | }, 49 | FAILURE { 50 | 51 | @Override 52 | public String getStatus() { 53 | return Messages.Failure(); 54 | } 55 | }, 56 | NOT_BUILT { 57 | 58 | @Override 59 | public String getStatus() { 60 | return Messages.NotBuilt(); 61 | } 62 | }, 63 | BACK_TO_NORMAL { 64 | 65 | @Override 66 | public String getStatus() { 67 | return Messages.BackToNormal(); 68 | } 69 | }, 70 | UNSTABLE { 71 | 72 | @Override 73 | public String getStatus() { 74 | return Messages.Unstable(); 75 | } 76 | }; 77 | 78 | private final boolean startType; 79 | 80 | private NotificationType() { 81 | this(false); 82 | } 83 | 84 | private NotificationType(boolean startType) { 85 | this.startType = startType; 86 | } 87 | 88 | public abstract String getStatus(); 89 | 90 | public boolean isStartType() { 91 | return startType; 92 | } 93 | 94 | public final Notification getNotification(NotificationConfig config, AbstractBuild build, 95 | BuildListener buildListener) throws NotificationException { 96 | return getNotification(config, build, buildListener, get(BuildUtils.class), Jenkins.getInstance()); 97 | } 98 | 99 | @VisibleForTesting 100 | Notification getNotification(NotificationConfig config, AbstractBuild build, BuildListener buildListener, 101 | BuildUtils buildUtils, Jenkins jenkins) throws NotificationException { 102 | String messageTemplate = Util.replaceMacro(config.getMessageTemplate(), ImmutableMap.of(STATUS, getStatus())); 103 | CardProvider cardProvider = ExtensionList.lookup(CardProvider.class) 104 | .getDynamic(jenkins.getDescriptorByType(DescriptorImpl.class).getCardProvider()); 105 | if (cardProvider == null) { 106 | cardProvider = new NoopCardProvider(); 107 | } 108 | 109 | try { 110 | String message = TokenMacro.expandAll(build, buildListener, messageTemplate, false, null); 111 | return new Notification() 112 | .withColor(config.getColor()) 113 | .withMessageFormat(config.isTextFormat() ? MessageFormat.TEXT : MessageFormat.HTML) 114 | .withNotify(config.isNotifyEnabled()) 115 | .withMessage(message) 116 | .withCard(cardProvider.getCard(build, buildListener, config.getIconObject(), message)); 117 | } catch (MacroEvaluationException | IOException ex) { 118 | buildListener.getLogger().println(Messages.MacroEvaluationFailed(ex.toString())); 119 | throw new NotificationException(Messages.MacroEvaluationFailed(ex.getMessage()), ex); 120 | } catch (InterruptedException ie) { 121 | Thread.currentThread().interrupt(); 122 | throw new NotificationException(Messages.MacroEvaluationFailed(ie.getMessage()), ie); 123 | } 124 | } 125 | 126 | public static final NotificationType fromResults(Result previousResult, Result result) { 127 | if (result == Result.ABORTED) { 128 | return ABORTED; 129 | } else if (result == Result.FAILURE) { 130 | return FAILURE; 131 | } else if (result == Result.NOT_BUILT) { 132 | return NOT_BUILT; 133 | } else if (result == Result.UNSTABLE) { 134 | return UNSTABLE; 135 | } else if (result == Result.SUCCESS) { 136 | if (previousResult != null && previousResult != Result.SUCCESS) { 137 | return BACK_TO_NORMAL; 138 | } else { 139 | return SUCCESS; 140 | } 141 | } 142 | 143 | throw new IllegalStateException("Unable to determine notification type"); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # HipChat plugin for Jenkins 2 | 3 | A Jenkins plugin that sends notifications to HipChat chat rooms for build events. 4 | 5 | ## Features 6 | 7 | * Supports both v1 and v2 API (v1 API support to be removed in next major version) 8 | * Can send notifications for the following build statuses 9 | * Build Started 10 | * Build Aborted 11 | * Build Failed 12 | * Not Built (e.g. when an earlier stage prevented a multi-stage build to finish) 13 | * Build Successful 14 | * Build Unstable (e.g. tests failed, but compilation was successful) 15 | * Back To Normal (e.g. when the current build was successful, but the previous wasn't for some reason) 16 | * The room name can be parameterized 17 | * Supports different notification modes for matrix builds 18 | * Can be used from pipeline builds to send notifications 19 | * Supports HipChat [card notifications](https://developer.atlassian.com/hipchat/guide/sending-messages) 20 | 21 | ## Configuration 22 | 23 | The plugin allows two levels of configuration, each explained in the below sections. The assumption should be that if a project level setting can not be found, the plugin will fall back to the global configuration. 24 | 25 | ### Global configuration 26 | 27 | These settings can be found under **Manage Jenkins** -> **Configure System** and look for **Global HipChat Notifier Settings**. The settings listed here are: 28 | 29 | * **HipChat Server Host**: The hostname (and optionally the port number) for the HipChat server in use. Note that the server will *always* be accessed via HTTPS. Defaults to api.hipchat.com for cloud installations. The plugin will only connect to the server using TLS protocol, access via SSL protocol is disabled. 30 | * **Use v2 API**: Whether to use HipChat v2 API when interacting with the HipChat server. Note: that support for v1 version of the API is going to be removed in the next major version. 31 | * **Credentials**: The 'secret text' kind of credential to be used when accessing HipChat REST endpoints. When using v2 API, the Credentials must be a valid OAuth2 token obtained as described in the [v2 API documentation](https://developer.atlassian.com/hipchat/guide/hipchat-rest-api). When v2 API is disabled, the plugin will use v1 API to perform its operations. The v1 API requires API Tokens (which are different from OAuth2 tokens!). 32 | * **Room**: Allows to specify the name or ID of the room where the notification(s) should be sent. If the name of the room is not known at the time of the configuration, you can also use build variables (e.g. $HIPCHAT_ROOM). Multiple values can be provided, in which case use comma to separate the values. 33 | * **Send As**: Specifies the sender of the notification when using the v1 API. The value must be less than 15 characters long. 34 | * **Card Provider**: Here you can select a card provider implementation which is responsible to render a HipChat Card as needed. See below for more information on custom Card providers. Cards are only supported when v2 version of the HipChat API is in use. 35 | * **Default notifications**: Configure the default set of notifications. These will be used when there is no notification configured at the job level. Note that the default notifications are only sent if the HipChat Notifications Post Build Action is added to the job (otherwise the plugin won't get invoked). 36 | 37 | ### Job level configuration 38 | 39 | To set up the plugin for an individual job, go to the job's configuration page and add the **HipChat Notifications** post-build action. The settings listed there are: 40 | 41 | * **Credentials**: Use in case you have a room-specific token to override the globally set Credentials. 42 | * **Project Room**: Allows to specify the name or ID of the room where the notification(s) should be sent. If the name of the room is not known at the time of the configuration, you can also use build variables (e.g. $HIPCHAT_ROOM). Multiple values can be provided, in which case use comma to separate the values. 43 | * **Notifications**: The list of notifications that should be sent out upon build events. If this setting is left empty, the plugin will fall back to the Default notifications setting in the Global configuration. The settings available for each notification entry: 44 | * **Notify Room**: Whether the message should trigger a HipChat notification 45 | * **Text Format**: When checked, the message will be sent in text format, instead of the default HTML format. When using text format, emoticons will be properly displayed in messages, but links must be printed in full length. 46 | * **Notification Type**: Select which build status/result should trigger this notification. 47 | * **Color**: Select the color of the notification message. 48 | * **Card Icon**: The icon URL to use when using card notifications. 49 | * **Message template**: The specific message template to use for this notification 50 | * **Message templates**: These templates are used as default values when the notification-specific message template is not defined. 51 | * **Job started**: The message to use when the build starts. 52 | * **Job completed**: The message to use when the build completes regardless of the build result. 53 | 54 | ### Message template format 55 | 56 | The message templates used by the plugin now fully support [token-macro](https://github.com/jenkinsci/token-macro-plugin) tokens, for a comprehensive list of the available tokens, please check out the help texts for the message template settings. 57 | 58 | In addition to the out of the box supported tokens from the token-macro-plugin, the plugin can also utilize various other tokens provided by other plugins. Having the [email-ext-plugin](https://wiki.jenkins-ci.org/display/JENKINS/Email-ext+plugin) installed on Jenkins will make the following (non-comprehensive list of) tokens available for example: 59 | * TRIGGER_NAME 60 | * TEST_COUNTS 61 | * FAILED_TESTS 62 | 63 | If you find that one of the above listed tokens do not work with the plugin, you should probably check first whether the email-ext-plugin is installed on your Jenkins instance. The same rule applies for other third party token provider plugins. 64 | 65 | The HipChat plugin also provides the following token implementations: 66 | 67 | | Token name | Content | Example value | 68 | | --- | --- | --- | 69 | | BLUE_OCEAN_URL | [Blue Ocean UI](https://jenkins.io/projects/blueocean/) friendly link to the currently built job | http://localhost:8080/jenkins/job/foobar/1/display/redirect | 70 | | BUILD_DESCRIPTION | The description of the current build | Example build description | 71 | | BUILD_DURATION | The duration of the build in human readable format | 42 min | 72 | | COMMIT_MESSAGE | The first line of the last changeset's commit message | Initial commit | 73 | | HIPCHAT_CHANGES | Human readable details of the new changesets or "No changes" if changesets weren't computed for this build | Started by changes from bjensen (1 file(s) changed) | 74 | | HIPCHAT_CHANGES_OR_CAUSE | Returns HIPCHAT_CHANGES if it was successfully calculated, otherwise returns the cause of the build | Started by user Admin | 75 | | TEST_REPORT_URL | Direct link to the test reports | http://localhost:8080/jenkins/job/foobar/1/testReport | 76 | 77 | ### Proxy support 78 | 79 | The plugin utilizes the proxy configuration in Jenkins when making external HTTPS connections. To configure proxy in Jenkins, follow the [Jenkins documentation](https://wiki.jenkins-ci.org/display/JENKINS/JenkinsBehindProxy). 80 | 81 | The currently supported features are: 82 | * authenticated proxies 83 | * "No Proxy Host" setting 84 | 85 | ### Pipeline support 86 | 87 | When using pipeline projects, HipChat messages can be sent using the following DSL: 88 | 89 | ``` 90 | hipchatSend color: 'YELLOW', credentialId: 'myid', failOnError: true, message: 'test', notify: true, room: 'Jenkins', sendAs: 'Jenkins', server: 'api.hipchat.com', textFormat: true, v2enabled: true 91 | ``` 92 | 93 | Note that the following parameters for the hipchatSend step are planned to be deprecated in the next major version: 94 | 95 | * sendAs 96 | * server 97 | * token 98 | * v2enabled 99 | 100 | ## Support for custom Card Providers 101 | 102 | HipChat supports various kinds of cards for its notifications, as such the card implementation in the Jenkins HipChat plugin has been done in a pluggable manner. In case the out of the box available card implementations do not fit your needs, the following extension will need to be written: 103 | 104 | ```java 105 | @Extension 106 | public class MyCoolCardProvider extends CardProvider { 107 | 108 | private static final Logger LOGGER = Logger.getLogger(MyCoolCardProvider.class.getName()); 109 | 110 | @Override 111 | public Card getCard(Run run, TaskListener taskListener, Icon icon, String message) { 112 | // implement magic 113 | } 114 | 115 | @Override 116 | public CardProviderDescriptor getDescriptor() { 117 | return new DescriptorImpl(); 118 | } 119 | 120 | @Extension 121 | public static class DescriptorImpl extends CardProviderDescriptor { 122 | 123 | @Override 124 | public String getDisplayName() { 125 | return "My cool card provider"; 126 | } 127 | } 128 | } 129 | ``` 130 | 131 | The return value type Card represents a HipChat card and exposes all of its available properties as it is defined in the [HipChat API documentation](https://developer.atlassian.com/hipchat/guide/sending-messages). 132 | 133 | The idea behind the extensible approach is that lots of different card implementations can be made available. If you do end up writing a custom CardProvider, please open a pull request, so that others can benefit from it too. Having these custom implementations contributed should also ensure (to a reasonable degree) that future API changes will be reflected on these implementations as the changes are being made. 134 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/workflow/HipChatSendStep.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.workflow; 2 | 3 | import hudson.AbortException; 4 | import hudson.Extension; 5 | import hudson.ExtensionList; 6 | import hudson.FilePath; 7 | import hudson.model.Item; 8 | import hudson.model.Run; 9 | import hudson.model.TaskListener; 10 | import java.util.logging.Level; 11 | import java.util.logging.Logger; 12 | import javax.annotation.Nonnull; 13 | import javax.inject.Inject; 14 | 15 | import hudson.util.ListBoxModel; 16 | import hudson.util.Secret; 17 | import java.io.IOException; 18 | import jenkins.model.Jenkins; 19 | import jenkins.plugins.hipchat.CardProvider; 20 | import jenkins.plugins.hipchat.HipChatNotifier; 21 | import jenkins.plugins.hipchat.HipChatService; 22 | import jenkins.plugins.hipchat.Messages; 23 | import jenkins.plugins.hipchat.exceptions.NotificationException; 24 | import jenkins.plugins.hipchat.impl.NoopCardProvider; 25 | import jenkins.plugins.hipchat.model.notifications.Icon; 26 | import jenkins.plugins.hipchat.model.notifications.Notification; 27 | import jenkins.plugins.hipchat.model.notifications.Notification.Color; 28 | import jenkins.plugins.hipchat.model.notifications.Notification.MessageFormat; 29 | import jenkins.plugins.hipchat.utils.BuildUtils; 30 | import jenkins.plugins.hipchat.utils.CredentialUtils; 31 | import org.apache.commons.lang.StringUtils; 32 | import org.jenkinsci.plugins.plaincredentials.StringCredentials; 33 | import org.jenkinsci.plugins.tokenmacro.MacroEvaluationException; 34 | import org.jenkinsci.plugins.tokenmacro.TokenMacro; 35 | import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; 36 | import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; 37 | import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousNonBlockingStepExecution; 38 | import org.jenkinsci.plugins.workflow.steps.StepContextParameter; 39 | import org.kohsuke.stapler.AncestorInPath; 40 | import org.kohsuke.stapler.DataBoundConstructor; 41 | import org.kohsuke.stapler.DataBoundSetter; 42 | import org.kohsuke.stapler.QueryParameter; 43 | import org.kohsuke.stapler.interceptor.RequirePOST; 44 | 45 | /** 46 | * Workflow step to send a HipChat room notification. 47 | */ 48 | public class HipChatSendStep extends AbstractStepImpl { 49 | 50 | private static final Logger logger = Logger.getLogger(HipChatSendStep.class.getName()); 51 | 52 | public final String message; 53 | 54 | @DataBoundSetter 55 | public Color color; 56 | 57 | @DataBoundSetter 58 | public String icon; 59 | 60 | /** 61 | * @deprecated Use {@link #credentialId instead}. 62 | */ 63 | @Deprecated 64 | @DataBoundSetter 65 | public String token; 66 | 67 | @DataBoundSetter 68 | public String credentialId; 69 | 70 | @DataBoundSetter 71 | public String room; 72 | 73 | @DataBoundSetter 74 | public String server; 75 | 76 | @DataBoundSetter 77 | public boolean notify; 78 | 79 | @DataBoundSetter 80 | public boolean textFormat; 81 | 82 | @DataBoundSetter 83 | public Boolean v2enabled; 84 | 85 | @DataBoundSetter 86 | public String sendAs; 87 | 88 | @DataBoundSetter 89 | public boolean failOnError; 90 | 91 | @DataBoundConstructor 92 | public HipChatSendStep(@Nonnull String message) { 93 | this.message = message; 94 | } 95 | 96 | @Extension 97 | public static class DescriptorImpl extends AbstractStepDescriptorImpl { 98 | 99 | public DescriptorImpl() { 100 | super(HipChatSendStepExecution.class); 101 | } 102 | 103 | @RequirePOST 104 | public ListBoxModel doFillCredentialIdItems(@AncestorInPath Item context, @QueryParameter String server) { 105 | // permission checks are implemented in CredentialUtils 106 | return Jenkins.getInstance().getDescriptorByType(HipChatNotifier.DescriptorImpl.class) 107 | .doFillCredentialIdItems(context, server); 108 | } 109 | 110 | @Override 111 | public String getFunctionName() { 112 | return "hipchatSend"; 113 | } 114 | 115 | @Override 116 | public String getDisplayName() { 117 | return Messages.HipChatSendStepDisplayName(); 118 | } 119 | 120 | } 121 | 122 | public static class HipChatSendStepExecution extends AbstractSynchronousNonBlockingStepExecution { 123 | 124 | private static final long serialVersionUID = 1L; 125 | 126 | @Inject 127 | private transient BuildUtils buildUtils; 128 | @Inject 129 | private transient CredentialUtils credentialUtils; 130 | @Inject 131 | private transient HipChatSendStep step; 132 | @StepContextParameter 133 | private transient TaskListener listener; 134 | @StepContextParameter 135 | private transient Run run; 136 | 137 | @Override 138 | protected Void run() throws Exception { 139 | if (StringUtils.isBlank(step.message)) { 140 | //allow entire run to fail based on failOnError field 141 | if (step.failOnError) { 142 | throw new AbortException(Messages.MessageRequiredError()); 143 | } else { 144 | listener.error(Messages.MessageRequiredError()); 145 | } 146 | return null; 147 | } 148 | 149 | //default to global config values if not set in step, but allow step to override all global settings 150 | HipChatNotifier.DescriptorImpl hipChatDesc = 151 | Jenkins.getInstance().getDescriptorByType(HipChatNotifier.DescriptorImpl.class); 152 | 153 | String room = firstNonEmpty(step.room, hipChatDesc.getRoom()); 154 | String server = firstNonEmpty(step.server, hipChatDesc.getServer()); 155 | String sendAs = firstNonEmpty(step.sendAs, hipChatDesc.getSendAs()); 156 | String credentialId = step.credentialId; 157 | String token = null; 158 | if (StringUtils.isEmpty(credentialId)) { 159 | if (StringUtils.isEmpty(step.token)) { 160 | credentialId = hipChatDesc.getCredentialId(); 161 | } else { 162 | token = step.token; 163 | } 164 | } 165 | 166 | if (StringUtils.isNotEmpty(credentialId)) { 167 | StringCredentials creds = credentialUtils.resolveCredential(run.getParent(), credentialId, server); 168 | if (creds != null) { 169 | token = Secret.toString(creds.getSecret()); 170 | } 171 | } 172 | //default to gray if not set in step 173 | Color color = step.color != null ? step.color : Color.GRAY; 174 | boolean v2enabled = step.v2enabled != null ? step.v2enabled : hipChatDesc.isV2Enabled(); 175 | 176 | HipChatService hipChatService = HipChatNotifier.getHipChatService(server, token, v2enabled, room, sendAs); 177 | 178 | logger.log(Level.FINER, "HipChat publish settings: api v2 - {0} server - {1} token - {2} room - {3}", 179 | new Object[]{v2enabled, server, token, room}); 180 | 181 | //attempt to publish message, log NotificationException, will allow run to continue 182 | try { 183 | FilePath workspace = null; 184 | try { 185 | workspace = getContext().get(FilePath.class); 186 | } catch (IOException | InterruptedException ex) { 187 | //workspace is not always available, ignore these exceptions 188 | } 189 | String message = TokenMacro.expandAll(run, workspace, listener, 190 | HipChatNotifier.migrateMessageTemplate(step.message), false, null); 191 | 192 | CardProvider cardProvider = ExtensionList.lookup(CardProvider.class).getDynamic(Jenkins.getInstance() 193 | .getDescriptorByType(jenkins.plugins.hipchat.HipChatNotifier.DescriptorImpl.class) 194 | .getCardProvider()); 195 | if (cardProvider == null) { 196 | cardProvider = new NoopCardProvider(); 197 | } 198 | 199 | hipChatService.publish(new Notification() 200 | .withColor(color) 201 | .withMessage(message) 202 | .withCard(cardProvider.getCard(run, listener, 203 | StringUtils.isEmpty(step.icon) ? null : new Icon().withUrl(step.icon), message)) 204 | .withNotify(step.notify) 205 | .withMessageFormat(step.textFormat ? MessageFormat.TEXT : MessageFormat.HTML)); 206 | listener.getLogger().println(Messages.NotificationSuccessful(room)); 207 | } catch (MacroEvaluationException | IOException | NotificationException ex) { 208 | listener.getLogger().println(Messages.NotificationFailed(ex.getMessage())); 209 | //allow entire run to fail based on failOnError field 210 | if (step.failOnError) { 211 | throw new AbortException(Messages.NotificationFailed(ex.getMessage())); 212 | } else { 213 | listener.error(Messages.NotificationFailed(ex.getMessage())); 214 | } 215 | } catch (InterruptedException ie) { 216 | Thread.currentThread().interrupt(); 217 | } 218 | 219 | return null; 220 | } 221 | 222 | private String firstNonEmpty(String value, String defaultValue) { 223 | return StringUtils.isNotEmpty(value) ? value : defaultValue; 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/utils/CredentialUtils.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat.utils; 2 | 3 | import com.cloudbees.plugins.credentials.CredentialsMatchers; 4 | import com.cloudbees.plugins.credentials.CredentialsProvider; 5 | import com.cloudbees.plugins.credentials.CredentialsScope; 6 | import com.cloudbees.plugins.credentials.CredentialsStore; 7 | import com.cloudbees.plugins.credentials.common.AbstractIdCredentialsListBoxModel; 8 | import com.cloudbees.plugins.credentials.common.StandardCredentials; 9 | import com.cloudbees.plugins.credentials.common.StandardListBoxModel; 10 | import com.cloudbees.plugins.credentials.domains.Domain; 11 | import com.cloudbees.plugins.credentials.domains.DomainRequirement; 12 | import com.cloudbees.plugins.credentials.domains.DomainSpecification; 13 | import com.cloudbees.plugins.credentials.domains.HostnameSpecification; 14 | import com.cloudbees.plugins.credentials.domains.SchemeSpecification; 15 | import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; 16 | import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials; 17 | import hudson.Util; 18 | import hudson.model.AbstractProject; 19 | import hudson.model.Item; 20 | import hudson.model.Queue.Task; 21 | import hudson.model.queue.Tasks; 22 | import hudson.security.ACL; 23 | import hudson.util.ListBoxModel; 24 | import hudson.util.Secret; 25 | import jenkins.model.Jenkins; 26 | import jenkins.plugins.hipchat.HipChatNotifier; 27 | import jenkins.plugins.hipchat.HipChatNotifier.DescriptorImpl; 28 | 29 | import org.acegisecurity.Authentication; 30 | import org.jenkinsci.plugins.plaincredentials.StringCredentials; 31 | import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl; 32 | 33 | import javax.annotation.CheckForNull; 34 | import javax.inject.Singleton; 35 | import java.io.IOException; 36 | import java.util.ArrayList; 37 | import java.util.List; 38 | import java.util.UUID; 39 | 40 | /** 41 | * This class is here to help with credential related tasks, such as credential lookup and migration of insecurely 42 | * stored credentials. 43 | */ 44 | @Singleton 45 | public class CredentialUtils { 46 | 47 | /** 48 | * Finds the credential with the given credentialId in the CredentialStore. 49 | * 50 | * @param context The context (job) to be used to find the right credential. 51 | * @param credentialId The ID of the credential. 52 | * @param server The URL to the HipChat server to ensure that we find the credential under the right security 53 | * domain. 54 | * @return The found credential, or null if the credential cannot be found. 55 | */ 56 | public StringCredentials resolveCredential(Item context, @CheckForNull String credentialId, String server) { 57 | return credentialId == null ? null 58 | : CredentialsMatchers.firstOrNull(CredentialsProvider.lookupCredentials(StringCredentials.class, 59 | context, ACL.SYSTEM, requirements(server)), 60 | CredentialsMatchers.withId(credentialId)); 61 | } 62 | 63 | /** 64 | * Retrieves the UI model object containing all acceptable credentials. This method can operate in two modes 65 | * essentially: 66 | *
    67 | *
  • When item is null: in this case the credentials will be looked up globally. In this case the assumption 68 | * is that we are displaying the credential dropdown on the global config page.
  • 69 | *
  • When item is not null: in this case the credentials will be looked up within the context of the job. In 70 | * this case the assumption is that we are displaying the credential dropdown on the job config page.
  • 71 | *
72 | * 73 | * @param item The context (job) to use to find the the credentials. May be null. In job config mode, the current 74 | * value of the credential setting will be extracted from this item. 75 | * @param globalCredentialId In global config mode, use this as the currently selected credential. 76 | * @param server The URL to the HipChat server to ensure that we find the credentials under the right security 77 | * domain. 78 | * @return The UI model containing all matching credentials, or only the current selection if the user does not have 79 | * the right set of permissions. 80 | */ 81 | public ListBoxModel getAvailableCredentials(@CheckForNull Item item, String globalCredentialId, String server) { 82 | String currentValue = getCurrentlySelectedCredentialId(item, globalCredentialId); 83 | if ((item == null && !Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) 84 | || (item != null && !item.hasPermission(Item.EXTENDED_READ) 85 | && !item.hasPermission(CredentialsProvider.USE_ITEM))) { 86 | return new StandardListBoxModel().includeCurrentValue(currentValue); 87 | } 88 | 89 | if (item != null && !item.hasPermission(Item.CONFIGURE)) { 90 | return new StandardListBoxModel().includeCurrentValue(currentValue); 91 | } 92 | 93 | AbstractIdCredentialsListBoxModel model = new StandardListBoxModel() 94 | .includeEmptyValue(); 95 | Authentication credentialAuthentication = item instanceof Task ? 96 | Tasks.getAuthenticationOf((Task) item) : ACL.SYSTEM; 97 | 98 | if (item == null) { 99 | model = model.includeAs(credentialAuthentication, Jenkins.getInstance(), StringCredentials.class, 100 | requirements(server)); 101 | } else { 102 | model = model.includeAs(credentialAuthentication, item, StringCredentials.class, requirements(server)); 103 | } 104 | if (currentValue != null) { 105 | model = model.includeCurrentValue(currentValue); 106 | } 107 | return model; 108 | } 109 | 110 | /** 111 | * Migrates the credential stored in global config from the old insecure format to the Credential system. 112 | * 113 | * @param descriptor The descriptor of this plugin. 114 | * @throws IOException If there was an error whilst migrating the credential. 115 | */ 116 | public void migrateGlobalCredential(DescriptorImpl descriptor) throws IOException { 117 | List credentials = CredentialsProvider.lookupCredentials(StringCredentials.class, 118 | Jenkins.getInstance(), ACL.SYSTEM, requirements(descriptor.getServer())); 119 | String room = Util.fixEmpty(descriptor.getRoom()) != null ? descriptor.getRoom() : "Global"; 120 | String credentialId = storeCredential(descriptor, credentials, room, descriptor.getToken()); 121 | 122 | descriptor.setToken(null); 123 | descriptor.setCredentialId(credentialId); 124 | } 125 | 126 | /** 127 | * Migrates the credential stored in a job config from the old insecure format to the Credential system. 128 | * 129 | * @param descriptor The descriptor of this plugin. 130 | * @param item The job where the plugin is configured. 131 | * @param notifier The plugin instance corresponding to this job. 132 | * @throws IOException If there was an error whilst migrating the credential. 133 | */ 134 | public void migrateJobCredential(DescriptorImpl descriptor, Item item, HipChatNotifier notifier) 135 | throws IOException { 136 | List credentials = CredentialsProvider.lookupCredentials(StringCredentials.class, item, 137 | ACL.SYSTEM, requirements(descriptor.getServer())); 138 | String room = Util.fixEmpty(notifier.getRoom()) != null ? notifier.getRoom() : descriptor.getRoom(); 139 | String credentialId = storeCredential(descriptor, credentials, room, notifier.getToken()); 140 | 141 | notifier.setToken(null); 142 | notifier.setCredentialId(credentialId); 143 | } 144 | 145 | private String storeCredential(DescriptorImpl descriptor, List credentials, String room, 146 | String token) throws IOException { 147 | String server = descriptor.getServer(); 148 | List takenIds = new ArrayList(); 149 | for (StringCredentials credential : credentials) { 150 | takenIds.add(credential.getId()); 151 | if (credential.getId().startsWith("HipChat-") && token.equals(Secret.toString(credential.getSecret()))) { 152 | return credential.getId(); 153 | } 154 | } 155 | 156 | CredentialsStore store = CredentialsProvider.lookupStores(Jenkins.getInstance()).iterator().next(); 157 | String id = generateCredentialId(descriptor, takenIds, room); 158 | BaseStandardCredentials credential = new StringCredentialsImpl(CredentialsScope.GLOBAL, id, id, 159 | Secret.fromString(token)); 160 | if (store.isDomainsModifiable()) { 161 | Domain domain = store.getDomainByName(server); 162 | if (domain == null) { 163 | List specs = new ArrayList(); 164 | specs.add(new HostnameSpecification(server, null)); 165 | specs.add(new SchemeSpecification("https")); 166 | domain = new Domain(server, null, specs); 167 | store.addDomain(domain, credential); 168 | } else { 169 | store.addCredentials(domain, credential); 170 | } 171 | } else { 172 | store.addCredentials(Domain.global(), credential); 173 | } 174 | 175 | return credential.getId(); 176 | } 177 | 178 | private String getCurrentlySelectedCredentialId(Item item, String globalCredentialId) { 179 | if (item == null) { 180 | return globalCredentialId; 181 | } else if (item instanceof AbstractProject) { 182 | HipChatNotifier notifier = ((AbstractProject) item).getPublishersList().get(HipChatNotifier.class); 183 | return notifier == null ? null : notifier.getCredentialId(); 184 | } else { 185 | return null; 186 | } 187 | } 188 | 189 | private String generateCredentialId(DescriptorImpl descriptor, List takenIds, String room) { 190 | //Simplify the room name 191 | room = Util.fixEmpty(room) == null ? UUID.randomUUID().toString() : room.split(",")[0]; 192 | String id = "HipChat-" + (descriptor.isV2Enabled() ? room + "-Token" : "API-Token") 193 | .replaceAll("[^a-zA-Z0-9_.-]", "_"); 194 | String candidate = id; 195 | int i = 2; 196 | while (takenIds.contains(candidate)) { 197 | candidate = id + "-" + i++; 198 | } 199 | return candidate; 200 | } 201 | 202 | private List requirements(String server) { 203 | return URIRequirementBuilder.fromUri("https://" + server).build(); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.jenkins-ci.plugins 7 | plugin 8 | 2.17 9 | 10 | 11 | org.jvnet.hudson.plugins 12 | hipchat 13 | hpi 14 | 2.2.2-SNAPSHOT 15 | Jenkins HipChat Plugin 16 | A Build status publisher that notifies channels on a HipChat server 17 | http://wiki.jenkins-ci.org/display/JENKINS/HipChat+Plugin 18 | 19 | 20 | 3.3 21 | UTF-8 22 | UTF-8 23 | 1.11 24 | 2.17 25 | 7 26 | 27 | 28 | 29 | 30 | MIT license 31 | All source code is under the MIT license. 32 | 33 | 34 | 35 | 36 | 37 | aldaris 38 | Peter Major 39 | 40 | 41 | 42 | 43 | scm:git:ssh://github.com/jenkinsci/hipchat-plugin.git 44 | scm:git:ssh://git@github.com/jenkinsci/hipchat-plugin.git 45 | https://github.com/jenkinsci/hipchat-plugin 46 | HEAD 47 | 48 | 49 | 50 | 51 | 52 | org.apache.httpcomponents 53 | httpclient 54 | 4.5 55 | 56 | 57 | commons-logging 58 | commons-logging 59 | 1.0.4 60 | provided 61 | 62 | 63 | org.jenkins-ci.plugins 64 | display-url-api 65 | 0.2 66 | 67 | 68 | org.jenkins-ci.plugins 69 | matrix-project 70 | 1.4 71 | 72 | 73 | org.jenkins-ci.plugins 74 | junit 75 | 1.3 76 | 77 | 78 | org.jenkins-ci.plugins 79 | credentials 80 | 2.1.5 81 | 82 | 83 | org.jenkins-ci.plugins 84 | plain-credentials 85 | 1.3 86 | 87 | 88 | org.jenkins-ci.plugins 89 | token-macro 90 | 2.0 91 | 92 | 93 | org.jenkins-ci.plugins.workflow 94 | workflow-step-api 95 | ${workflow.version} 96 | 97 | 98 | com.fasterxml.jackson.core 99 | jackson-annotations 100 | 2.8.5 101 | 102 | 103 | com.fasterxml.jackson.core 104 | jackson-databind 105 | 2.8.5 106 | 107 | 108 | org.jenkins-ci 109 | version-number 110 | 1.4 111 | provided 112 | 113 | 114 | org.jenkins-ci.plugins.workflow 115 | workflow-cps 116 | ${workflow.version} 117 | test 118 | 119 | 120 | org.jenkins-ci.plugins.workflow 121 | workflow-job 122 | ${workflow.version} 123 | test 124 | 125 | 126 | org.jenkins-ci.plugins.workflow 127 | workflow-step-api 128 | tests 129 | ${workflow.version} 130 | test 131 | 132 | 133 | junit 134 | junit 135 | 4.12 136 | test 137 | 138 | 139 | org.assertj 140 | assertj-core 141 | 2.2.0 142 | test 143 | 144 | 145 | org.mockito 146 | mockito-core 147 | 1.10.19 148 | test 149 | 150 | 151 | 152 | 153 | 154 | org.apache.httpcomponents 155 | httpclient 156 | 157 | 158 | org.jenkins-ci.plugins 159 | display-url-api 160 | 161 | 162 | org.jenkins-ci.plugins 163 | credentials 164 | 165 | 166 | org.jenkins-ci.plugins 167 | plain-credentials 168 | 169 | 170 | org.jenkins-ci.plugins 171 | matrix-project 172 | 173 | 174 | org.jenkins-ci.plugins 175 | junit 176 | 177 | 178 | org.jenkins-ci.plugins 179 | token-macro 180 | 181 | 182 | org.jenkins-ci.plugins.workflow 183 | workflow-step-api 184 | 185 | 186 | org.jenkins-ci.plugins.workflow 187 | workflow-cps 188 | 189 | 190 | org.jenkins-ci.plugins.workflow 191 | workflow-job 192 | 193 | 194 | com.fasterxml.jackson.core 195 | jackson-annotations 196 | 197 | 198 | com.fasterxml.jackson.core 199 | jackson-databind 200 | 201 | 202 | org.jenkins-ci 203 | version-number 204 | 205 | 206 | org.jenkins-ci.plugins.workflow 207 | workflow-step-api 208 | tests 209 | 210 | 211 | junit 212 | junit 213 | 214 | 215 | org.assertj 216 | assertj-core 217 | 218 | 219 | org.mockito 220 | mockito-core 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | org.jenkins-ci.tools 229 | maven-hpi-plugin 230 | 231 | true 232 | 233 | 234 | 235 | maven-release-plugin 236 | 2.5.2 237 | 238 | 239 | maven-surefire-plugin 240 | 2.18.1 241 | 242 | 243 | ${project.build.directory}/test-classes/logging.properties 244 | 245 | 246 | 247 | 248 | org.jsonschema2pojo 249 | jsonschema2pojo-maven-plugin 250 | 0.4.29 251 | 252 | 253 | 254 | 255 | 256 | maven-compiler-plugin 257 | 258 | 1.7 259 | 1.7 260 | 261 | 262 | 263 | org.jsonschema2pojo 264 | jsonschema2pojo-maven-plugin 265 | 266 | 267 | 268 | generate 269 | 270 | 271 | 272 | 273 | ${basedir}/src/main/resources/schema 274 | ${project.build.directory}/generated-sources/json-schema 275 | jenkins.plugins.hipchat.model.notifications 276 | 1.7 277 | true 278 | false 279 | false 280 | true 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | repo.jenkins-ci.org 289 | https://repo.jenkins-ci.org/public/ 290 | 291 | 292 | 293 | 294 | 295 | repo.jenkins-ci.org 296 | https://repo.jenkins-ci.org/public/ 297 | 298 | 299 | 300 | -------------------------------------------------------------------------------- /src/main/java/jenkins/plugins/hipchat/HipChatNotifier.java: -------------------------------------------------------------------------------- 1 | package jenkins.plugins.hipchat; 2 | 3 | import static jenkins.plugins.hipchat.model.Constants.*; 4 | import static jenkins.plugins.hipchat.utils.GuiceUtils.*; 5 | import static jenkins.plugins.hipchat.model.NotificationType.*; 6 | 7 | import com.google.common.collect.ImmutableMap; 8 | import hudson.Extension; 9 | import hudson.ExtensionList; 10 | import hudson.Launcher; 11 | import hudson.Util; 12 | import hudson.matrix.MatrixAggregatable; 13 | import hudson.matrix.MatrixAggregator; 14 | import hudson.matrix.MatrixBuild; 15 | import hudson.matrix.MatrixProject; 16 | import hudson.matrix.MatrixRun; 17 | import hudson.model.AbstractBuild; 18 | import hudson.model.AbstractProject; 19 | import hudson.model.BuildListener; 20 | import hudson.model.Item; 21 | import hudson.model.Job; 22 | import hudson.model.JobPropertyDescriptor; 23 | import hudson.model.Result; 24 | import hudson.tasks.BuildStepDescriptor; 25 | import hudson.tasks.BuildStepMonitor; 26 | import hudson.tasks.Notifier; 27 | import hudson.tasks.Publisher; 28 | import hudson.util.FormValidation; 29 | import hudson.util.ListBoxModel; 30 | import hudson.util.ListBoxModel.Option; 31 | import hudson.util.Secret; 32 | import hudson.util.VersionNumber; 33 | import jenkins.model.Jenkins; 34 | import jenkins.plugins.hipchat.exceptions.NotificationException; 35 | import jenkins.plugins.hipchat.impl.DefaultCardProvider; 36 | import jenkins.plugins.hipchat.impl.HipChatV1Service; 37 | import jenkins.plugins.hipchat.impl.HipChatV2Service; 38 | import jenkins.plugins.hipchat.model.MatrixTriggerMode; 39 | import jenkins.plugins.hipchat.model.NotificationConfig; 40 | import jenkins.plugins.hipchat.model.NotificationType; 41 | import jenkins.plugins.hipchat.model.notifications.Notification.Color; 42 | import jenkins.plugins.hipchat.utils.BuildUtils; 43 | import jenkins.plugins.hipchat.utils.CredentialUtils; 44 | import net.sf.json.JSONObject; 45 | import org.apache.commons.lang.StringUtils; 46 | import org.jenkinsci.plugins.plaincredentials.StringCredentials; 47 | import org.kohsuke.stapler.DataBoundConstructor; 48 | import org.kohsuke.stapler.QueryParameter; 49 | import org.kohsuke.stapler.StaplerRequest; 50 | import org.kohsuke.stapler.export.Exported; 51 | import org.kohsuke.stapler.AncestorInPath; 52 | import org.kohsuke.stapler.interceptor.RequirePOST; 53 | 54 | import java.io.IOException; 55 | import java.util.ArrayList; 56 | import java.util.List; 57 | import java.util.logging.Level; 58 | import java.util.logging.Logger; 59 | 60 | @SuppressWarnings({"unchecked"}) 61 | public class HipChatNotifier extends Notifier implements MatrixAggregatable { 62 | 63 | private static final Logger logger = Logger.getLogger(HipChatNotifier.class.getName()); 64 | private static final ImmutableMap MESSAGE_MIRATION_MAPPING = ImmutableMap.builder() 65 | .put(DURATION, BUILD_DURATION_MACRO) 66 | .put(JOB_DISPLAY_NAME, PROJECT_DISPLAY_NAME_MACRO) 67 | .put(TEST_COUNT, TOTAL_TEST_COUNT_MACRO) 68 | .put(FAILED_TEST_COUNT, FAILED_TEST_COUNT_MACRO) 69 | .put(SKIPPED_TEST_COUNT, SKIPPED_TEST_COUNT_MACRO) 70 | .put(SUCCESS_TEST_COUNT, SUCCESS_TEST_COUNT_MACRO) 71 | .put(URL, BUILD_URL_MACRO) 72 | .put(COMMIT_MESSAGE, COMMIT_MESSAGE_MACRO) 73 | .put(COMMIT_MESSAGE_TEXT, ESCAPED_COMMIT_MESSAGE_MACRO) 74 | .put(CHANGES, HIPCHAT_CHANGES_MACRO) 75 | .put(CHANGES_OR_CAUSE, HIPCHAT_CHANGES_OR_CAUSE_MACRO) 76 | .build(); 77 | 78 | /** 79 | * @deprecated Use {@link #credentialId} instead. This field only exists for upgrade purposes. 80 | */ 81 | @Deprecated 82 | private transient String token; 83 | /** 84 | * @deprecated Use {@link #notifications} instead. This field only exists for upgrade purposes. 85 | */ 86 | @Deprecated 87 | private transient boolean startNotification; 88 | /** 89 | * @deprecated Use {@link #notifications} instead. This field only exists for upgrade purposes. 90 | */ 91 | @Deprecated 92 | private transient boolean notifySuccess; 93 | /** 94 | * @deprecated Use {@link #notifications} instead. This field only exists for upgrade purposes. 95 | */ 96 | @Deprecated 97 | private transient boolean notifyAborted; 98 | /** 99 | * @deprecated Use {@link #notifications} instead. This field only exists for upgrade purposes. 100 | */ 101 | @Deprecated 102 | private transient boolean notifyNotBuilt; 103 | /** 104 | * @deprecated Use {@link #notifications} instead. This field only exists for upgrade purposes. 105 | */ 106 | @Deprecated 107 | private transient boolean notifyUnstable; 108 | /** 109 | * @deprecated Use {@link #notifications} instead. This field only exists for upgrade purposes. 110 | */ 111 | @Deprecated 112 | private transient boolean notifyFailure; 113 | /** 114 | * @deprecated Use {@link #notifications} instead. This field only exists for upgrade purposes. 115 | */ 116 | @Deprecated 117 | private transient boolean notifyBackToNormal; 118 | private String credentialId; 119 | private String room; 120 | private List notifications; 121 | private MatrixTriggerMode matrixTriggerMode; 122 | 123 | private String startJobMessage; 124 | private String completeJobMessage; 125 | 126 | @DataBoundConstructor 127 | public HipChatNotifier(String credentialId, String room, List notifications, 128 | MatrixTriggerMode matrixTriggerMode, String startJobMessage, String completeJobMessage) { 129 | this.credentialId = credentialId; 130 | this.room = room; 131 | this.notifications = notifications; 132 | this.matrixTriggerMode = matrixTriggerMode; 133 | 134 | this.startJobMessage = startJobMessage; 135 | this.completeJobMessage = completeJobMessage; 136 | } 137 | 138 | public String getCredentialId() { 139 | return credentialId; 140 | } 141 | 142 | public void setCredentialId(String credentialId) { 143 | this.credentialId = credentialId; 144 | } 145 | 146 | public void setStartNotification(boolean startNotification) { 147 | this.startNotification = startNotification; 148 | } 149 | 150 | public void setNotifySuccess(boolean notifySuccess) { 151 | this.notifySuccess = notifySuccess; 152 | } 153 | 154 | public void setNotifyAborted(boolean notifyAborted) { 155 | this.notifyAborted = notifyAborted; 156 | } 157 | 158 | public void setNotifyNotBuilt(boolean notifyNotBuilt) { 159 | this.notifyNotBuilt = notifyNotBuilt; 160 | } 161 | 162 | public void setNotifyUnstable(boolean notifyUnstable) { 163 | this.notifyUnstable = notifyUnstable; 164 | } 165 | 166 | public void setNotifyFailure(boolean notifyFailure) { 167 | this.notifyFailure = notifyFailure; 168 | } 169 | 170 | public void setNotifyBackToNormal(boolean notifyBackToNormal) { 171 | this.notifyBackToNormal = notifyBackToNormal; 172 | } 173 | 174 | public MatrixTriggerMode getMatrixTriggerMode() { 175 | return matrixTriggerMode == null ? MatrixTriggerMode.BOTH : matrixTriggerMode; 176 | } 177 | 178 | public void setMatrixTriggerMode(MatrixTriggerMode matrixTriggerMode) { 179 | this.matrixTriggerMode = matrixTriggerMode; 180 | } 181 | 182 | public void setNotifications(List notifications) { 183 | this.notifications = notifications; 184 | } 185 | 186 | public List getNotifications() { 187 | return notifications; 188 | } 189 | /* notification message configurations */ 190 | 191 | public String getStartJobMessage() { 192 | return startJobMessage; 193 | } 194 | 195 | public void setStartJobMessage(String startJobMessage) { 196 | this.startJobMessage = startJobMessage; 197 | } 198 | 199 | public String getCompleteJobMessage() { 200 | return completeJobMessage; 201 | } 202 | 203 | public void setCompleteJobMessage(String completeJobMessage) { 204 | this.completeJobMessage = completeJobMessage; 205 | } 206 | 207 | public String getRoom() { 208 | return room; 209 | } 210 | 211 | public void setRoom(String room) { 212 | this.room = room; 213 | } 214 | 215 | public Object readResolve() { 216 | if (notifications == null) { 217 | notifications = new ArrayList(7); 218 | if (startNotification) { 219 | notifications.add(new NotificationConfig(false, false, STARTED, Color.GREEN, null, null)); 220 | } 221 | if (notifySuccess) { 222 | notifications.add(new NotificationConfig(false, false, SUCCESS, Color.GREEN, null, null)); 223 | } 224 | if (notifyAborted) { 225 | notifications.add(new NotificationConfig(true, false, ABORTED, Color.GRAY, null, null)); 226 | } 227 | if (notifyNotBuilt) { 228 | notifications.add(new NotificationConfig(true, false, NOT_BUILT, Color.GRAY, null, null)); 229 | } 230 | if (notifyUnstable) { 231 | notifications.add(new NotificationConfig(true, false, UNSTABLE, Color.YELLOW, null, null)); 232 | } 233 | if (notifyFailure) { 234 | notifications.add(new NotificationConfig(true, false, FAILURE, Color.RED, null, null)); 235 | } 236 | if (notifyBackToNormal) { 237 | notifications.add(new NotificationConfig(false, false, BACK_TO_NORMAL, Color.GREEN, null, null)); 238 | } 239 | } 240 | return this; 241 | } 242 | 243 | /** 244 | * Return the room name defined in the job configuration, or if that's empty return the room name from the global 245 | * configuration. 246 | * If the room name is parameterized, this will also try to resolve those parameters. 247 | * 248 | * @param build The current build for which we need to get the room. 249 | * @return The room name tied to the current build. 250 | */ 251 | public String getResolvedRoom(AbstractBuild build) { 252 | return Util.replaceMacro(StringUtils.isBlank(room) ? getDescriptor().getRoom() : room, 253 | build.getBuildVariableResolver()); 254 | } 255 | 256 | public String getToken() { 257 | return token; 258 | } 259 | 260 | public void setToken(String token) { 261 | this.token = token; 262 | } 263 | 264 | @Override 265 | public DescriptorImpl getDescriptor() { 266 | return Jenkins.getInstance().getDescriptorByType(DescriptorImpl.class); 267 | } 268 | 269 | @Override 270 | public boolean needsToRunAfterFinalized() { 271 | //This is here to ensure that the reported build status is actually correct. If we were to return false here, 272 | //other build plugins could still modify the build result, making the sent out HipChat notification incorrect. 273 | return true; 274 | } 275 | 276 | @Override 277 | public BuildStepMonitor getRequiredMonitorService() { 278 | return BuildStepMonitor.NONE; 279 | } 280 | 281 | @Override 282 | public boolean prebuild(AbstractBuild build, BuildListener listener) { 283 | logger.fine("Creating build start notification"); 284 | if (!(build instanceof MatrixRun) || getMatrixTriggerMode().forChild) { 285 | publishNotificationIfEnabled(STARTED, build, listener); 286 | } 287 | 288 | return true; 289 | } 290 | 291 | @Override 292 | public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) 293 | throws InterruptedException, IOException { 294 | if (!(build instanceof MatrixRun) || getMatrixTriggerMode().forChild) { 295 | notifyOnBuildComplete(build, listener); 296 | } 297 | 298 | return true; 299 | } 300 | 301 | private void notifyOnBuildComplete(AbstractBuild build, BuildListener listener) { 302 | logger.fine("Creating build completed notification"); 303 | Result result = build.getResult(); 304 | Result previousResult = get(BuildUtils.class).findPreviousBuildResult(build); 305 | 306 | NotificationType notificationType = NotificationType.fromResults(previousResult, result); 307 | publishNotificationIfEnabled(notificationType, build, listener); 308 | } 309 | 310 | private void publishNotificationIfEnabled(NotificationType notificationType, AbstractBuild build, 311 | BuildListener listener) { 312 | logger.log(Level.FINE, "Checking if notification {0} is enabled", notificationType); 313 | NotificationConfig notificationConfig = getNotificationConfig(notificationType); 314 | if (notificationConfig != null) { 315 | logger.log(Level.FINE, "Notification config found for notification type {0}: {1}", 316 | new Object[]{notificationType, notificationConfig.toString()}); 317 | String messageTemplate = Util.fixEmpty(notificationConfig.getMessageTemplate()); 318 | if (messageTemplate == null) { 319 | if (notificationType.isStartType()) { 320 | messageTemplate = Util.fixEmpty(getStartJobMessage()) == null 321 | ? getDescriptor().getStartJobMessageDefault() : getStartJobMessage(); 322 | } else { 323 | messageTemplate = Util.fixEmpty(getCompleteJobMessage()) == null 324 | ? getDescriptor().getCompleteJobMessageDefault() : getCompleteJobMessage(); 325 | } 326 | } 327 | notificationConfig = notificationConfig.overrideMessageTemplate(migrateMessageTemplate(messageTemplate)); 328 | 329 | try { 330 | getHipChatService(build).publish(notificationType.getNotification(notificationConfig, build, listener)); 331 | listener.getLogger().println(Messages.NotificationSuccessful(getResolvedRoom(build))); 332 | } catch (NotificationException ne) { 333 | listener.getLogger().println(Messages.NotificationFailed(ne.getMessage())); 334 | } 335 | } 336 | } 337 | 338 | public static String migrateMessageTemplate(String oldMessageTemplate) { 339 | return Util.replaceMacro(oldMessageTemplate, MESSAGE_MIRATION_MAPPING); 340 | } 341 | 342 | private NotificationConfig getNotificationConfig(NotificationType notificationType) { 343 | List configs = Util.fixNull(notifications).isEmpty() 344 | ? Util.fixNull(getDescriptor().getDefaultNotifications()) : notifications; 345 | for (NotificationConfig notificationConfig : configs) { 346 | if (notificationType.equals(notificationConfig.getNotificationType())) { 347 | return notificationConfig; 348 | } 349 | } 350 | return null; 351 | } 352 | 353 | private HipChatService getHipChatService(AbstractBuild build) throws NotificationException { 354 | DescriptorImpl desc = getDescriptor(); 355 | StringCredentials credentials = get(CredentialUtils.class).resolveCredential(build.getParent(), 356 | Util.fixEmpty(credentialId) != null ? credentialId : desc.getCredentialId(), desc.getServer()); 357 | if (credentials == null) { 358 | throw new NotificationException(Messages.CredentialMissing(credentialId)); 359 | } 360 | return getHipChatService(desc.getServer(), Secret.toString(credentials.getSecret()), desc.isV2Enabled(), 361 | getResolvedRoom(build), desc.getSendAs()); 362 | } 363 | 364 | /** 365 | * Obtains a {@link HipChatService} implementation corresponding to the provided settings. 366 | * 367 | * @param server The URL for the HipChat server. 368 | * @param token The auth token to use when sending the notification. 369 | * @param v2Enabled Whether v1 or v2 API should be used. 370 | * @param room The room to notify. 371 | * @param sendAs The username to use as the sender when using the v1 API. 372 | * @return An API version specific {@link HipChatService} instance. 373 | */ 374 | public static HipChatService getHipChatService(String server, String token, boolean v2Enabled, String room, 375 | String sendAs) { 376 | if (v2Enabled) { 377 | return new HipChatV2Service(server, token, room); 378 | } else { 379 | return new HipChatV1Service(server, token, room, sendAs); 380 | } 381 | } 382 | 383 | @Override 384 | public MatrixAggregator createAggregator(MatrixBuild build, Launcher launcher, BuildListener listener) { 385 | return new MatrixAggregator(build, launcher, listener) { 386 | 387 | @Override 388 | public boolean startBuild() throws InterruptedException, IOException { 389 | if (getMatrixTriggerMode().forParent) { 390 | publishNotificationIfEnabled(STARTED, build, listener); 391 | } 392 | return true; 393 | } 394 | 395 | @Override 396 | public boolean endBuild() throws InterruptedException, IOException { 397 | if (getMatrixTriggerMode().forParent) { 398 | notifyOnBuildComplete(build, listener); 399 | } 400 | return true; 401 | } 402 | }; 403 | } 404 | 405 | @Extension 406 | public static class DescriptorImpl extends BuildStepDescriptor { 407 | 408 | /** 409 | * @deprecated Use {@link #credentialId} instead. This field only exists for upgrade purposes. 410 | */ 411 | @Deprecated 412 | private transient String token; 413 | private String server = "api.hipchat.com"; 414 | private String credentialId; 415 | private boolean v2Enabled = false; 416 | private String room; 417 | private String sendAs = "Jenkins"; 418 | private String cardProvider = DefaultCardProvider.class.getName(); 419 | private List defaultNotifications; 420 | private String configVersion; 421 | private static int testNotificationCount = 0; 422 | 423 | public DescriptorImpl() { 424 | load(); 425 | if (Util.fixEmpty(token) != null) { 426 | try { 427 | get(CredentialUtils.class).migrateGlobalCredential(this); 428 | } catch (IOException ioe) { 429 | logger.log(Level.SEVERE, "Unable to migrate globally stored auth token to a credential", ioe); 430 | } 431 | } 432 | if (Util.fixEmpty(configVersion) == null) { 433 | configVersion = "1.0.0"; 434 | } 435 | } 436 | 437 | public String getServer() { 438 | return server; 439 | } 440 | 441 | public void setServer(String server) { 442 | this.server = server; 443 | } 444 | 445 | public String getCredentialId() { 446 | return credentialId; 447 | } 448 | 449 | public void setCredentialId(String credentialId) { 450 | this.credentialId = credentialId; 451 | } 452 | 453 | public String getToken() { 454 | return token; 455 | } 456 | 457 | public void setToken(String token) { 458 | this.token = token; 459 | } 460 | 461 | public boolean isV2Enabled() { 462 | return v2Enabled; 463 | } 464 | 465 | public void setV2Enabled(boolean v2Enabled) { 466 | this.v2Enabled = v2Enabled; 467 | } 468 | 469 | public String getRoom() { 470 | return room; 471 | } 472 | 473 | public void setRoom(String room) { 474 | this.room = room; 475 | } 476 | 477 | public String getSendAs() { 478 | return sendAs; 479 | } 480 | 481 | public void setSendAs(String sendAs) { 482 | this.sendAs = sendAs; 483 | } 484 | 485 | public String getCardProvider() { 486 | return cardProvider; 487 | } 488 | 489 | public void setCardProvider(String cardProvider) { 490 | this.cardProvider = cardProvider; 491 | } 492 | 493 | public List getDefaultNotifications() { 494 | return defaultNotifications; 495 | } 496 | 497 | public void setDefaultNotifications(List defaultNotifications) { 498 | this.defaultNotifications = defaultNotifications; 499 | } 500 | 501 | public String getConfigVersion() { 502 | return configVersion; 503 | } 504 | 505 | public void setConfigVersion(String configVersion) { 506 | this.configVersion = configVersion; 507 | } 508 | 509 | /* Default notification messages for UI */ 510 | public String getStartJobMessageDefault() { 511 | return Messages.JobStarted(); 512 | } 513 | 514 | public String getCompleteJobMessageDefault() { 515 | return Messages.JobCompleted(); 516 | } 517 | 518 | @Override 519 | public boolean isApplicable(Class aClass) { 520 | return true; 521 | } 522 | 523 | public boolean isMatrixProject(Object project) { 524 | return project instanceof MatrixProject; 525 | } 526 | 527 | @Override 528 | public boolean configure(StaplerRequest request, JSONObject formData) throws FormException { 529 | request.bindJSON(this, formData); 530 | 531 | save(); 532 | return super.configure(request, formData); 533 | } 534 | 535 | public FormValidation doCheckSendAs(@QueryParameter boolean v2Enabled, @QueryParameter String sendAs) { 536 | sendAs = Util.fixEmpty(sendAs); 537 | if (!v2Enabled) { 538 | if (sendAs == null || sendAs.length() > 15) { 539 | return FormValidation.error(Messages.InvalidSendAs()); 540 | } 541 | } 542 | return FormValidation.ok(); 543 | } 544 | 545 | @RequirePOST 546 | public FormValidation doSendTestNotification(@AncestorInPath AbstractProject context, 547 | @QueryParameter String server, @QueryParameter String credentialId, @QueryParameter boolean v2Enabled, 548 | @QueryParameter String room, @QueryParameter String sendAs) { 549 | Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); 550 | StringCredentials credentials = get(CredentialUtils.class).resolveCredential(context, credentialId, server); 551 | if (credentials == null) { 552 | return FormValidation.error(Messages.CredentialMissing(credentialId)); 553 | } 554 | HipChatService service = getHipChatService(server, Secret.toString(credentials.getSecret()), 555 | v2Enabled, room, sendAs); 556 | try { 557 | service.publish(Messages.TestNotification(++testNotificationCount), "yellow", true); 558 | return FormValidation.ok(Messages.TestNotificationSent()); 559 | } catch (NotificationException ne) { 560 | return FormValidation.error(Messages.TestNotificationFailed(ne.getMessage())); 561 | } 562 | } 563 | 564 | @RequirePOST 565 | public ListBoxModel doFillCardProviderItems() { 566 | Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); 567 | ExtensionList providers = ExtensionList.lookup(CardProvider.class); 568 | List