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 |
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 |
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 |
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 |
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 |
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.
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 |
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 |
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.
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 |
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 |
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 |
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 |
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 |
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 |
${%Notify Room}
22 |
${%Text Format}
23 |
${%Notification Type}
24 |
${%Color}
25 |
${%Card Icon}
26 |
${%Message template}
27 |
28 |
29 |
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 extends Entry> 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 extends Entry> 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 extends Entry> 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.