├── .github
├── release-drafter-yml
├── usage_01.jpg
├── usage_02.jpg
├── usage_03.jpg
├── dependabot.yml
└── workflows
│ └── cd.yaml
├── .gitattributes
├── Jenkinsfile
├── .mvn
├── maven.config
└── extensions.xml
├── src
├── main
│ ├── resources
│ │ └── nz
│ │ │ └── co
│ │ │ └── jammehcow
│ │ │ └── jenkinsdiscord
│ │ │ ├── WebhookPublisher
│ │ │ ├── help-notes.html
│ │ │ ├── help-enableArtifactList.html
│ │ │ ├── help-branchName.html
│ │ │ ├── help-thumbnailURL.html
│ │ │ ├── help-statusTitle.html
│ │ │ ├── help-enableFooterInfo.html
│ │ │ ├── help-sendOnStateChange.html
│ │ │ ├── help-webhookURL.html
│ │ │ ├── help-enableUrlLinking.html
│ │ │ └── config.jelly
│ │ │ └── DiscordPipelineStep
│ │ │ ├── help-footer.html
│ │ │ ├── help-image.html
│ │ │ ├── help-thumbnail.html
│ │ │ ├── help-unstable.html
│ │ │ ├── help-title.html
│ │ │ ├── help-result.html
│ │ │ ├── help-successful.html
│ │ │ ├── help-webhookURL.html
│ │ │ ├── help-link.html
│ │ │ ├── help-description.html
│ │ │ └── config.jelly
│ └── java
│ │ └── nz
│ │ └── co
│ │ └── jammehcow
│ │ └── jenkinsdiscord
│ │ ├── exception
│ │ └── WebhookException.java
│ │ ├── util
│ │ └── EmbedDescription.java
│ │ ├── DiscordWebhook.java
│ │ ├── DiscordPipelineStep.java
│ │ └── WebhookPublisher.java
└── test
│ └── java
│ └── nz
│ └── co
│ └── jammehcow
│ └── jenkinsdiscord
│ └── BasicTest.java
├── .gitignore
├── LICENSE.md
├── README.md
└── pom.xml
/.github/release-drafter-yml:
--------------------------------------------------------------------------------
1 | _extends: .github
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | .jpg filter=lfs diff=lfs merge=lfs -text
2 |
--------------------------------------------------------------------------------
/Jenkinsfile:
--------------------------------------------------------------------------------
1 | buildPlugin(platforms: ['linux'], jdkVersions: [8, 11])
--------------------------------------------------------------------------------
/.mvn/maven.config:
--------------------------------------------------------------------------------
1 | -Pconsume-incrementals
2 | -Pmight-produce-incrementals
3 | -Dchangelist.format=%d.v%s
--------------------------------------------------------------------------------
/.github/usage_01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vmfunc/discord-notifier-plugin/master/.github/usage_01.jpg
--------------------------------------------------------------------------------
/.github/usage_02.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vmfunc/discord-notifier-plugin/master/.github/usage_02.jpg
--------------------------------------------------------------------------------
/.github/usage_03.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vmfunc/discord-notifier-plugin/master/.github/usage_03.jpg
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/WebhookPublisher/help-notes.html:
--------------------------------------------------------------------------------
1 |
2 | Notes to add above the embed.
3 |
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/DiscordPipelineStep/help-footer.html:
--------------------------------------------------------------------------------
1 |
2 | Text in footer of message
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/DiscordPipelineStep/help-image.html:
--------------------------------------------------------------------------------
1 |
2 | URL of the image under everything
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/DiscordPipelineStep/help-thumbnail.html:
--------------------------------------------------------------------------------
1 |
2 | URL to image displayed on the right.
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/WebhookPublisher/help-enableArtifactList.html:
--------------------------------------------------------------------------------
1 |
2 | Enable the listing of artifacts
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/WebhookPublisher/help-branchName.html:
--------------------------------------------------------------------------------
1 |
2 | The branch that was used to build. Leave empty to omit.
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/WebhookPublisher/help-thumbnailURL.html:
--------------------------------------------------------------------------------
1 |
2 | Link to an image that will be shown on the right side of Discord message (Optional)
3 |
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/WebhookPublisher/help-statusTitle.html:
--------------------------------------------------------------------------------
1 |
2 | The name that gets displayed on top of the message. Leave empty to use standard naming
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/DiscordPipelineStep/help-unstable.html:
--------------------------------------------------------------------------------
1 |
2 | Set to true to set the color to yellow (only when the build was successful, else this is omitted).
3 |
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/DiscordPipelineStep/help-title.html:
--------------------------------------------------------------------------------
1 |
2 | Message title
3 |
4 | To set title to job name, use env.JOB_NAME.
5 |
6 |
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/WebhookPublisher/help-enableFooterInfo.html:
--------------------------------------------------------------------------------
1 |
2 | Enabling this will send the "Jenkins vX.X.X, Discord Notifier vX.X.X" as the footer of the embed.
3 |
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/WebhookPublisher/help-sendOnStateChange.html:
--------------------------------------------------------------------------------
1 |
2 | Enabling this will only send the message to the Discord channel if the state of the build differs from the previous.
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/DiscordPipelineStep/help-result.html:
--------------------------------------------------------------------------------
1 |
2 | Enum to set the build result. Accepts values SUCCESS|UNSTABLE|FAILURE|ABORTED.
3 | This field is preferred over success and unstable.
4 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "maven"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 | - package-ecosystem: "github-actions"
8 | directory: "/"
9 | schedule:
10 | interval: "weekly"
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/DiscordPipelineStep/help-successful.html:
--------------------------------------------------------------------------------
1 |
2 | Either colors the message as green or red depending on success.
3 | Use currentBuild.resultIsBetterOrEqualTo('SUCCESS') to color the message green on success or red otherwise.
4 |
5 |
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/WebhookPublisher/help-webhookURL.html:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/DiscordPipelineStep/help-webhookURL.html:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/DiscordPipelineStep/help-link.html:
--------------------------------------------------------------------------------
1 |
2 | When set the title of the message comes clickable and leads to this url.
3 |
4 | To link the executed job use env.BUILD_URL.
5 | This requires Jenkins URL to be set in Global Settings.
6 |
7 |
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/WebhookPublisher/help-enableUrlLinking.html:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 |
3 | # IntelliJ project files
4 | .idea/
5 | *.iml
6 | work/
7 |
8 | ### Maven ###
9 | target/
10 | pom.xml.tag
11 | pom.xml.releaseBackup
12 | pom.xml.versionsBackup
13 | pom.xml.next
14 | release.properties
15 | dependency-reduced-pom.xml
16 | buildNumber.properties
17 | .mvn/timing.properties
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/DiscordPipelineStep/help-description.html:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/.mvn/extensions.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | io.jenkins.tools.incrementals
4 | git-changelist-maven-extension
5 | 1.3
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/main/java/nz/co/jammehcow/jenkinsdiscord/exception/WebhookException.java:
--------------------------------------------------------------------------------
1 | package nz.co.jammehcow.jenkinsdiscord.exception;
2 |
3 | /**
4 | * @author jammehcow
5 | */
6 |
7 | public class WebhookException extends Exception {
8 | public WebhookException() { super(); }
9 |
10 | public WebhookException(String message) { super(message); }
11 |
12 | public WebhookException(String message, Throwable cause) { super(message, cause); }
13 |
14 | public WebhookException(Throwable cause) { super(cause); }
15 | }
16 |
--------------------------------------------------------------------------------
/src/test/java/nz/co/jammehcow/jenkinsdiscord/BasicTest.java:
--------------------------------------------------------------------------------
1 | package nz.co.jammehcow.jenkinsdiscord;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.fail;
6 |
7 | public class BasicTest {
8 | @Test
9 | public void webhookClassDoesntThrow() {
10 | try {
11 | DiscordWebhook wh = new DiscordWebhook("http://exampl.e");
12 | wh.setContent("content");
13 | wh.setDescription("desc");
14 | wh.setStatus(DiscordWebhook.StatusColor.GREEN);
15 | } catch (Exception e) {
16 | fail();
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 James Upjohn
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/DiscordPipelineStep/config.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 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/main/resources/nz/co/jammehcow/jenkinsdiscord/WebhookPublisher/config.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 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/.github/workflows/cd.yaml:
--------------------------------------------------------------------------------
1 | name: cd
2 | on:
3 | workflow_dispatch:
4 | check_run:
5 | types:
6 | - completed
7 |
8 | jobs:
9 | validate:
10 | runs-on: ubuntu-latest
11 | outputs:
12 | should_release: ${{ steps.verify-ci-status.outputs.result == 'success' && steps.interesting-categories.outputs.interesting == 'true' }}
13 | steps:
14 | - name: Verify CI status
15 | uses: jenkins-infra/verify-ci-status-action@v1.2.0
16 | id: verify-ci-status
17 | with:
18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
19 | output_result: true
20 |
21 | - name: Release Drafter
22 | uses: release-drafter/release-drafter@v5
23 | if: steps.verify-ci-status.outputs.result == 'success'
24 | with:
25 | name: next
26 | tag: next
27 | version: next
28 | env:
29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30 |
31 | - name: Check interesting categories
32 | uses: jenkins-infra/interesting-category-action@v1.0.0
33 | id: interesting-categories
34 | if: steps.verify-ci-status.outputs.result == 'success'
35 | with:
36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37 |
38 | release:
39 | runs-on: ubuntu-latest
40 | needs: [validate]
41 | if: needs.validate.outputs.should_release == 'true'
42 | steps:
43 | - name: Check out
44 | uses: actions/checkout@v2.4.0
45 | with:
46 | fetch-depth: 0
47 | - name: Set up JDK 8
48 | uses: actions/setup-java@v2.5.0
49 | with:
50 | distribution: temurin
51 | java-version: 8
52 | - name: Release
53 | uses: jenkins-infra/jenkins-maven-cd-action@v1.2.0
54 | with:
55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
56 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
57 | MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }}
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Discord Notifier
2 |
3 | Discord Notifier provides a bridge between Jenkins and Discord through the built-in webhook functionality.
4 |
5 | ## The purpose
6 |
7 | The Jenkins Discord Webhook plugin was made to share results of a build to a Discord channel using the webhooks that Discord provides.
8 |
9 | Through this plugin you are able to:
10 | - [x] Get success and fail messages about your build
11 | - [x] Link to build artifacts
12 | - [x] List SCM changes to the build
13 | - [x] Only send notifications on state change
14 |
15 | ## Download
16 |
17 | Discord notifier is available in official jenkins repos.
18 |
19 | ## Usage
20 |
21 | This plugin uses the post-build feature to execute a request.
22 |
23 | After installing, go to your job's configure section and add the *Discord Notifier* item. Then proceed to enter your webhook URL.
24 |
25 | 
26 |
27 | There are a few options you can choose from:
28 | - Webhook URL
29 | - The URL of the webhook (pretty self-explanatory) provided by Discord
30 | - Send only on state change
31 | - Checking this will only send the message when the state of the current build differs from the previous
32 | - Send only failed
33 | - checking this will only send the failed job.
34 | - Advanced:
35 | - thumbnail
36 | - If set, the image under the URL shows up on the right side of Discord message.
37 | - Enable URL linking
38 | - Enables the title, build summary and build id to be linked to the build. Requires the URL to be set in Jenkin's global configuration
39 | - Enable artifact list
40 | - Enables the listing of the artifacts generated by the build
41 | - Enable version info in footer
42 | - Adds the "Jenkins version, Discord Webhook version" text in the footer of the message
43 |
44 | 
45 | 
46 |
47 | ## Pipeline
48 |
49 | Discord Notifier supports Jenkins Pipeline. The only required parameter is webhookURL (the URL of the webhook, of course) - but there isn't much point of sending nothing.
50 |
51 | ### Parameters
52 |
53 | - webhookURL (required)
54 | - The URL of the webhook (pretty self-explanatory) provided by Discord.
55 | - title
56 | - The title of the embed.
57 | - link
58 | - If set, the title becomes a link to this URL.
59 | - thumbnail
60 | - If set, the image under the URL shows up on the right side of Discord message.
61 | - image
62 | - If set, the image under the URL shows up under discord message
63 | - description
64 | - The description of the message (the main chunk of text), can be markdown formatted, [Markdown Text 101 (Chat Formatting: Bold, Italic, Underline)](https://support.discordapp.com/hc/en-us/articles/210298617-Markdown-Text-101-Chat-Formatting-Bold-Italic-Underline-).
65 | - footer
66 | - The text in footer of the message.
67 | - result
68 | - Sets the left side colour of the embed (SUCCESS - green, UNSTABLE - yellow, FAILURE - red, ABORTED - grey).
69 |
70 | ### Example
71 |
72 | ````
73 |
74 | discordSend description: "Jenkins Pipeline Build", footer: "Footer Text", link: env.BUILD_URL, result: currentBuild.currentResult, title: JOB_NAME, webhookURL: "Webhook URL"
75 |
76 | ````
77 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 |
6 | org.jenkins-ci.plugins
7 | plugin
8 | 4.33
9 |
10 |
11 |
12 | Discord Notifier
13 | Discord Notifier allows you to send Discord embeds about your builds via Discord's webhooks.
14 |
15 | https://github.com/jenkinsci/discord-notifier-plugin/blob/master/README.md
16 |
17 | nz.co.jammehcow
18 | discord-notifier
19 | ${changelist}
20 | hpi
21 |
22 |
23 | 1.4.15
24 | 999999-SNAPSHOT
25 | jenkinsci/discord-notifier-plugin
26 | 2.235.1
27 | 8
28 | false
29 |
30 |
31 |
32 | scm:git:git://github.com/${gitHubRepo}.git
33 | scm:git:git@github.com:${gitHubRepo}.git
34 | https://github.com/${gitHubRepo}
35 | ${scmTag}
36 |
37 |
38 |
39 |
40 | MIT License
41 | http://opensource.org/licenses/MIT
42 |
43 |
44 |
45 |
46 |
47 | jammehcow
48 | James Upjohn
49 | jspartan250@gmail.com
50 |
51 |
52 | KocproZ
53 | Kacper Stasiuk
54 | kocproz@protonmail.com
55 |
56 |
57 |
58 |
59 |
60 | repo.jenkins-ci.org
61 | https://repo.jenkins-ci.org/public/
62 |
63 |
64 |
65 |
66 |
67 | repo.jenkins-ci.org
68 | https://repo.jenkins-ci.org/public/
69 |
70 |
71 |
72 |
73 |
74 |
75 | io.jenkins.tools.bom
76 | bom-2.235.x
77 | 918.vae501d2cdc99
78 | import
79 | pom
80 |
81 |
82 |
83 |
84 |
85 |
86 | com.konghq
87 | unirest-java
88 | 3.13.6
89 |
90 |
91 |
92 |
93 | org.jenkins-ci.plugins.workflow
94 | workflow-step-api
95 |
96 |
97 |
98 |
99 | org.jenkins-ci.plugins
100 | matrix-project
101 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/src/main/java/nz/co/jammehcow/jenkinsdiscord/util/EmbedDescription.java:
--------------------------------------------------------------------------------
1 | package nz.co.jammehcow.jenkinsdiscord.util;
2 |
3 | import jenkins.scm.RunWithSCM;
4 | import hudson.model.Run;
5 | import hudson.scm.ChangeLogSet;
6 | import java.util.ArrayList;
7 | import java.util.Arrays;
8 | import java.util.IllegalFormatException;
9 | import jenkins.model.JenkinsLocationConfiguration;
10 |
11 | import java.util.LinkedList;
12 | import java.util.List;
13 | import org.apache.commons.lang.StringUtils;
14 |
15 | /**
16 | * @author jammehcow
17 | */
18 |
19 | public class EmbedDescription {
20 | private static final int maxEmbedStringLength = 2048; // The maximum length of an embed description.
21 |
22 | private LinkedList changesList = new LinkedList<>();
23 | private LinkedList artifactsList = new LinkedList<>();
24 | private String prefix;
25 | private String finalDescription;
26 |
27 | public EmbedDescription(
28 | Run build,
29 | JenkinsLocationConfiguration globalConfig,
30 | String prefix,
31 | boolean enableArtifactsList,
32 | boolean showChangeset,
33 | String scmWebUrl
34 | ) {
35 | String artifactsURL = globalConfig.getUrl() + build.getUrl() + "artifact/";
36 | this.prefix = StringUtils.trimToNull(prefix);
37 |
38 | if (showChangeset) {
39 | ArrayList changes = new ArrayList<>();
40 | List changeSets = ((RunWithSCM)build).getChangeSets();
41 | for (ChangeLogSet i : changeSets)
42 | changes.addAll(Arrays.asList(i.getItems()));
43 | if (changes.isEmpty()) {
44 | this.changesList.add("\n*No changes.*\n");
45 | } else {
46 | this.changesList.add("\n**Changes:**\n");
47 |
48 | boolean withLinks;
49 | try {
50 | String dummy = String.format(scmWebUrl, "");
51 | withLinks = true;
52 | } catch (IllegalFormatException ex) {
53 | withLinks = false;
54 | }
55 |
56 | for (Object o : changes) {
57 | ChangeLogSet.Entry entry = (ChangeLogSet.Entry) o;
58 |
59 | String commitID = entry.getCommitId();
60 | String commitDisplayStr;
61 | if (commitID == null) commitDisplayStr = "null ";
62 | else if (commitID.length() < 6) commitDisplayStr = commitID;
63 | else commitDisplayStr = commitID.substring(0, 6);
64 |
65 | String msg = entry.getMsg().trim();
66 | int nl = msg.indexOf("\n");
67 | if (nl >= 0)
68 | msg = msg.substring(0, nl).trim();
69 | msg = EscapeMarkdown(msg);
70 |
71 | String author = entry.getAuthor().getFullName();
72 |
73 | if (withLinks) {
74 | String url = String.format(scmWebUrl, commitID);
75 | this.changesList.add(String.format(" - [`%s`](%s) *%s - %s*%n",
76 | commitDisplayStr, url, msg, author));
77 | } else {
78 | this.changesList.add(String.format(" - `%s` *%s - %s*%n",
79 | commitDisplayStr, msg, author));
80 | }
81 | }
82 | }
83 | }
84 |
85 | if (enableArtifactsList) {
86 | this.artifactsList.add("\n**Artifacts:**\n");
87 | //noinspection unchecked
88 | List artifacts = build.getArtifacts();
89 | if (artifacts.size() == 0) {
90 | this.artifactsList.add("\n*No artifacts saved.*");
91 | } else {
92 | for (Run.Artifact artifact : artifacts) {
93 | this.artifactsList.add(" - " + artifactsURL + artifact.getHref() + "\n");
94 | }
95 | }
96 | }
97 |
98 | while (this.getCurrentDescription().length() > maxEmbedStringLength) {
99 | if (this.changesList.size() > 5) {
100 | // Dwindle the changes list down to 5 changes.
101 | while (this.changesList.size() != 5) this.changesList.removeLast();
102 | } else if (this.artifactsList.size() > 1) {
103 | this.artifactsList.clear();
104 | this.artifactsList.add(artifactsURL);
105 | } else {
106 | // Worst case scenario: truncate the description.
107 | this.finalDescription = this.getCurrentDescription().substring(0, maxEmbedStringLength - 1);
108 | return;
109 | }
110 | }
111 |
112 | this.finalDescription = this.getCurrentDescription();
113 | }
114 |
115 | private String getCurrentDescription() {
116 | StringBuilder description = new StringBuilder();
117 | if (this.prefix != null)
118 | description.append(this.prefix);
119 |
120 | // Collate the changes and artifacts into the description.
121 | for (String changeEntry : this.changesList) description.append(changeEntry);
122 | for (String artifact : this.artifactsList) description.append(artifact);
123 |
124 | return description.toString().trim();
125 | }
126 |
127 | @Override
128 | public String toString() {
129 | return this.finalDescription;
130 | }
131 |
132 | // https://support.discord.com/hc/en-us/articles/210298617
133 | private static String EscapeMarkdown(String text) {
134 | return text.replace("\\", "\\\\").replace("*", "\\*").replace("_", "\\_").replace("~", "\\~")
135 | .replace("`", "\\`");
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/main/java/nz/co/jammehcow/jenkinsdiscord/DiscordWebhook.java:
--------------------------------------------------------------------------------
1 | package nz.co.jammehcow.jenkinsdiscord;
2 |
3 | import jenkins.model.Jenkins;
4 | import kong.unirest.HttpResponse;
5 | import kong.unirest.JsonNode;
6 | import kong.unirest.Proxy;
7 | import kong.unirest.Unirest;
8 | import kong.unirest.UnirestException;
9 | import nz.co.jammehcow.jenkinsdiscord.exception.WebhookException;
10 | import org.json.JSONArray;
11 | import org.json.JSONObject;
12 |
13 | import java.io.InputStream;
14 |
15 | /**
16 | * Author: jammehcow.
17 | * Date: 22/04/17.
18 | */
19 | class DiscordWebhook {
20 | private String webhookUrl;
21 | private JSONObject obj;
22 | private JSONObject embed;
23 | private JSONArray fields;
24 | private InputStream file;
25 | private String filename;
26 |
27 | static final int TITLE_LIMIT = 256;
28 | static final int DESCRIPTION_LIMIT = 2048;
29 | static final int FOOTER_LIMIT = 2048;
30 |
31 | enum StatusColor {
32 | /**
33 | * Green "you're sweet as" color.
34 | */
35 | GREEN(1681177),
36 | /**
37 | * Yellow "go, but I'm watching you" color.
38 | */
39 | YELLOW(16776970),
40 | /**
41 | * Red "something ain't right" color.
42 | */
43 | RED(11278871),
44 | /**
45 | * Grey. Just grey.
46 | */
47 | GREY(13487565);
48 | private long code;
49 |
50 | StatusColor(int code) {
51 | this.code = code;
52 | }
53 | }
54 |
55 | /**
56 | * Instantiates a new Discord webhook.
57 | *
58 | * @param url the webhook URL
59 | */
60 | DiscordWebhook(String url) {
61 | this.webhookUrl = url;
62 | this.obj = new JSONObject();
63 | this.obj.put("username", "Jenkins");
64 | this.obj.put("avatar_url", "https://get.jenkins.io/art/jenkins-logo/1024x1024/headshot.png");
65 | this.embed = new JSONObject();
66 | this.fields = new JSONArray();
67 | }
68 |
69 | /**
70 | * Sets the embed title.
71 | *
72 | * @param title the title text
73 | * @return this
74 | */
75 | public DiscordWebhook setTitle(String title) {
76 | this.embed.put("title", title);
77 | return this;
78 | }
79 |
80 | public DiscordWebhook setCustomUsername(String username) {
81 | if (username != null && !username.equals(""))
82 | this.obj.put("username", username);
83 | else
84 | {
85 | // unset will allow default discord username to be used (as specified in discord's integration settings)
86 | this.obj.remove("username");
87 | }
88 | return this;
89 | }
90 |
91 | public DiscordWebhook setCustomAvatarUrl(String url) {
92 | if (url != null && !url.equals(""))
93 | this.obj.put("avatar_url", url);
94 | else
95 | {
96 | // unset will allow default avatar to be used (as specified in discord's integration settings)
97 | this.obj.remove("avatar_url");
98 | }
99 | return this;
100 | }
101 |
102 | /**
103 | * Sets the embed title url.
104 | *
105 | * @param buildUrl the build url
106 | * @return this
107 | */
108 | public DiscordWebhook setURL(String buildUrl) {
109 | this.embed.put("url", buildUrl);
110 | return this;
111 | }
112 |
113 | /**
114 | * Sets the build status (for the embed's color).
115 | *
116 | * @param isSuccess if the build is successful
117 | * @return this
118 | */
119 | public DiscordWebhook setStatus(StatusColor isSuccess) {
120 | this.embed.put("color", isSuccess.code);
121 | return this;
122 | }
123 |
124 | /**
125 | * Sets the embed description.
126 | *
127 | * @param content the content
128 | * @return this
129 | */
130 | public DiscordWebhook setDescription(String content) {
131 | this.embed.put("description", content);
132 | return this;
133 | }
134 |
135 | public DiscordWebhook setContent(String content) {
136 | this.obj.put("content", content);
137 | return this;
138 | }
139 |
140 | /**
141 | * Sets the URL of image at the bottom of embed.
142 | * @param url URL of image
143 | * @return this
144 | */
145 | public DiscordWebhook setImage(String url) {
146 | JSONObject image = new JSONObject();
147 | image.put("url", url);
148 | this.embed.put("image", image);
149 | return this;
150 | }
151 |
152 | /**
153 | * Sets the URL of image on the right side.
154 | * @param url URL of image
155 | * @return this
156 | */
157 | public DiscordWebhook setThumbnail(String url) {
158 | JSONObject thumbnail = new JSONObject();
159 | thumbnail.put("url", url);
160 | this.embed.put("thumbnail", thumbnail);
161 | return this;
162 | }
163 |
164 | public DiscordWebhook addField(String name, String value) {
165 | JSONObject field = new JSONObject();
166 | field.put("name", name);
167 | field.put("value", value);
168 | this.fields.put(field);
169 | return this;
170 | }
171 |
172 | /**
173 | * Sets the embed's footer text.
174 | *
175 | * @param text the footer text
176 | * @return this
177 | */
178 | public DiscordWebhook setFooter(String text) {
179 | this.embed.put("footer", new JSONObject().put("text", text));
180 | return this;
181 | }
182 |
183 | DiscordWebhook setFile(InputStream is, String filename) {
184 | this.file = is;
185 | this.filename = filename;
186 | return this;
187 | }
188 |
189 | /**
190 | * Send the payload to Discord.
191 | *
192 | * @throws WebhookException the webhook exception
193 | */
194 | public void send() throws WebhookException {
195 |
196 | this.embed.put("fields", fields);
197 | if (this.embed.toString().length() > 6000)
198 | throw new WebhookException("Embed object larger than the limit (" + this.embed.toString().length() + ">6000).");
199 |
200 | this.obj.put("embeds", new JSONArray().put(this.embed));
201 |
202 | try {
203 | final Jenkins instance = Jenkins.getInstanceOrNull();
204 | if (instance != null && instance.proxy != null) {
205 | String proxyIP = instance.proxy.name;
206 | int proxyPort = instance.proxy.port;
207 | if (!proxyIP.equals("")) {
208 | Unirest.config().proxy(new Proxy(proxyIP, proxyPort));
209 | }
210 | }
211 | HttpResponse response;
212 | if (file != null) {
213 | response = Unirest.post(this.webhookUrl)
214 | .field("payload_json", obj.toString())
215 | .field("file", file, filename)
216 | .asJson();
217 | } else {
218 | response = Unirest.post(this.webhookUrl)
219 | .field("payload_json", obj.toString())
220 | .asJson();
221 | }
222 |
223 | if (response.getStatus() < 200 || response.getStatus() >= 300) {
224 | throw new WebhookException(response.getBody().getObject().toString(2));
225 | }
226 | } catch (UnirestException e) { e.printStackTrace(); }
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/src/main/java/nz/co/jammehcow/jenkinsdiscord/DiscordPipelineStep.java:
--------------------------------------------------------------------------------
1 | package nz.co.jammehcow.jenkinsdiscord;
2 |
3 | import hudson.Extension;
4 | import hudson.model.Result;
5 | import hudson.model.Run;
6 | import hudson.model.TaskListener;
7 | import nz.co.jammehcow.jenkinsdiscord.exception.WebhookException;
8 | import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl;
9 | import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl;
10 | import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousNonBlockingStepExecution;
11 | import org.jenkinsci.plugins.workflow.steps.StepContextParameter;
12 | import org.kohsuke.stapler.DataBoundConstructor;
13 | import org.kohsuke.stapler.DataBoundSetter;
14 |
15 | import javax.annotation.Nonnull;
16 | import javax.inject.Inject;
17 | import jenkins.model.JenkinsLocationConfiguration;
18 |
19 | import static nz.co.jammehcow.jenkinsdiscord.DiscordWebhook.*;
20 | import nz.co.jammehcow.jenkinsdiscord.util.EmbedDescription;
21 |
22 | public class DiscordPipelineStep extends AbstractStepImpl {
23 | private final String webhookURL;
24 |
25 | private String title;
26 | private String link;
27 | private String description;
28 | private String footer;
29 | private String image;
30 | private String thumbnail;
31 | private String result;
32 | private String notes;
33 | private String customAvatarUrl;
34 | private String customUsername;
35 | private boolean successful;
36 | private boolean unstable;
37 | private boolean enableArtifactsList;
38 | private boolean showChangeset;
39 | private String scmWebUrl;
40 |
41 | @DataBoundConstructor
42 | public DiscordPipelineStep(String webhookURL) {
43 | this.webhookURL = webhookURL;
44 | }
45 |
46 | public String getWebhookURL() {
47 | return webhookURL;
48 | }
49 |
50 | public String getTitle() {
51 | return title;
52 | }
53 |
54 | @DataBoundSetter
55 | public void setTitle(String title) {
56 | this.title = title;
57 | }
58 |
59 | public String getLink() {
60 | return link;
61 | }
62 |
63 | @DataBoundSetter
64 | public void setLink(String link) {
65 | this.link = link;
66 | }
67 |
68 | public String getDescription() {
69 | return description;
70 | }
71 |
72 | @DataBoundSetter
73 | public void setDescription(String description) {
74 | this.description = description;
75 | }
76 |
77 | public String getFooter() {
78 | return footer;
79 | }
80 |
81 | @DataBoundSetter
82 | public void setFooter(String footer) {
83 | this.footer = footer;
84 | }
85 |
86 | public boolean isSuccessful() {
87 | return successful;
88 | }
89 |
90 | @DataBoundSetter
91 | public void setSuccessful(boolean successful) {
92 | this.successful = successful;
93 | }
94 |
95 | public boolean isUnstable() {
96 | return unstable;
97 | }
98 |
99 | @DataBoundSetter
100 | public void setUnstable(boolean unstable) {
101 | this.unstable = unstable;
102 | }
103 |
104 | @DataBoundSetter
105 | public void setImage(String url) {
106 | this.image = url;
107 | }
108 |
109 | public String getImage() {
110 | return image;
111 | }
112 |
113 | @DataBoundSetter
114 | public void setThumbnail(String url) {
115 | this.thumbnail = url;
116 | }
117 |
118 | public String getThumbnail() {
119 | return thumbnail;
120 | }
121 |
122 | @DataBoundSetter
123 | public void setResult(String result) {
124 | this.result = result;
125 | }
126 |
127 | public String getResult() {
128 | return result;
129 | }
130 |
131 | @DataBoundSetter
132 | public void setNotes(String notes) {
133 | this.notes = notes;
134 | }
135 |
136 | public String getNotes() {
137 | return notes;
138 | }
139 |
140 | @DataBoundSetter
141 | public void setCustomAvatarUrl(String customAvatarUrl) {
142 | this.customAvatarUrl = customAvatarUrl;
143 | }
144 |
145 | public String getCustomAvatarUrl() {
146 | return customAvatarUrl;
147 | }
148 |
149 | @DataBoundSetter
150 | public void setCustomUsername(String customUsername) {
151 | this.customUsername = customUsername;
152 | }
153 |
154 | public String getCustomUsername() {
155 | return customUsername;
156 | }
157 |
158 | @DataBoundSetter
159 | public void setEnableArtifactsList(boolean enable) {
160 | this.enableArtifactsList = enable;
161 | }
162 |
163 | public boolean getEnableArtifactsList() {
164 | return enableArtifactsList;
165 | }
166 |
167 | @DataBoundSetter
168 | public void setShowChangeset(boolean show) {
169 | this.showChangeset = show;
170 | }
171 |
172 | public boolean getShowChangeset() {
173 | return showChangeset;
174 | }
175 |
176 | @DataBoundSetter
177 | public void setScmWebUrl(String url) {
178 | this.scmWebUrl = url;
179 | }
180 |
181 | public String getScmWebUrl() {
182 | return scmWebUrl;
183 | }
184 |
185 | public static class DiscordPipelineStepExecution extends AbstractSynchronousNonBlockingStepExecution {
186 | @Inject
187 | transient DiscordPipelineStep step;
188 |
189 | @StepContextParameter
190 | private transient TaskListener listener;
191 |
192 | @Override
193 | protected Void run() throws Exception {
194 | listener.getLogger().println("Sending notification to Discord.");
195 |
196 | DiscordWebhook.StatusColor statusColor;
197 | statusColor = StatusColor.YELLOW;
198 | if (step.getResult() == null) {
199 | if (step.isSuccessful()) statusColor = DiscordWebhook.StatusColor.GREEN;
200 | if (step.isSuccessful() && step.isUnstable()) statusColor = DiscordWebhook.StatusColor.YELLOW;
201 | if (!step.isSuccessful() && !step.isUnstable()) statusColor = DiscordWebhook.StatusColor.RED;
202 | } else if (step.getResult().equals(Result.SUCCESS.toString())) {
203 | statusColor = StatusColor.GREEN;
204 | } else if (step.getResult().equals(Result.UNSTABLE.toString())) {
205 | statusColor = StatusColor.YELLOW;
206 | } else if (step.getResult().equals(Result.FAILURE.toString())) {
207 | statusColor = StatusColor.RED;
208 | } else if (step.getResult().equals(Result.ABORTED.toString())) {
209 | statusColor = StatusColor.GREY;
210 | } else {
211 | listener.getLogger().println(step.getResult() + " is not a valid result");
212 | }
213 |
214 | DiscordWebhook wh = new DiscordWebhook(step.getWebhookURL());
215 | wh.setTitle(checkLimitAndTruncate("title", step.getTitle(), TITLE_LIMIT));
216 | wh.setURL(step.getLink());
217 | wh.setThumbnail(step.getThumbnail());
218 |
219 | if (step.getEnableArtifactsList() || step.getShowChangeset()) {
220 | JenkinsLocationConfiguration globalConfig = JenkinsLocationConfiguration.get();
221 | Run build = getContext().get(Run.class);
222 | wh.setDescription(new EmbedDescription(build, globalConfig, step.getDescription(), step.getEnableArtifactsList(), step.getShowChangeset(), step.getScmWebUrl())
223 | .toString()
224 | );
225 | } else {
226 | wh.setDescription(checkLimitAndTruncate("description", step.getDescription(), DESCRIPTION_LIMIT));
227 | }
228 |
229 | wh.setImage(step.getImage());
230 | wh.setFooter(checkLimitAndTruncate("footer", step.getFooter(), FOOTER_LIMIT));
231 | wh.setStatus(statusColor);
232 | wh.setContent(step.getNotes());
233 |
234 | if (step.getCustomAvatarUrl() != null)
235 | wh.setCustomAvatarUrl(step.getCustomAvatarUrl());
236 |
237 | if (step.getCustomUsername() != null)
238 | wh.setCustomUsername(step.getCustomUsername());
239 |
240 | try { wh.send(); }
241 | catch (WebhookException e) { e.printStackTrace(listener.getLogger()); }
242 |
243 | return null;
244 | }
245 |
246 | private String checkLimitAndTruncate(String fieldName, String value, int limit) {
247 | if (value == null) return "";
248 | if (value.length() > limit) {
249 | listener.getLogger().printf("Warning: '%s' field has more than %d characters (%d). It will be truncated.%n",
250 | fieldName,
251 | limit,
252 | value.length());
253 | return value.substring(0, limit);
254 | }
255 | return value;
256 | }
257 |
258 | private static final long serialVersionUID = 1L;
259 | }
260 |
261 | @Extension
262 | public static class DescriptorImpl extends AbstractStepDescriptorImpl {
263 | public DescriptorImpl() { super(DiscordPipelineStepExecution.class); }
264 |
265 | @Override
266 | public String getFunctionName() {
267 | return "discordSend";
268 | }
269 |
270 | @Nonnull
271 | @Override
272 | public String getDisplayName() {
273 | return "Send an embed message to Webhook URL";
274 | }
275 | }
276 | }
277 |
--------------------------------------------------------------------------------
/src/main/java/nz/co/jammehcow/jenkinsdiscord/WebhookPublisher.java:
--------------------------------------------------------------------------------
1 | package nz.co.jammehcow.jenkinsdiscord;
2 |
3 | import hudson.EnvVars;
4 | import hudson.Extension;
5 | import hudson.Launcher;
6 | import hudson.matrix.MatrixConfiguration;
7 | import hudson.model.AbstractBuild;
8 | import hudson.model.AbstractProject;
9 | import hudson.model.BuildListener;
10 | import hudson.model.Result;
11 | import hudson.tasks.BuildStepDescriptor;
12 | import hudson.tasks.BuildStepMonitor;
13 | import hudson.tasks.Notifier;
14 | import hudson.tasks.Publisher;
15 | import hudson.util.FormValidation;
16 | import jenkins.model.JenkinsLocationConfiguration;
17 | import nz.co.jammehcow.jenkinsdiscord.exception.WebhookException;
18 | import nz.co.jammehcow.jenkinsdiscord.util.EmbedDescription;
19 | import org.kohsuke.stapler.DataBoundConstructor;
20 | import org.kohsuke.stapler.QueryParameter;
21 |
22 | import java.io.IOException;
23 | import java.util.Map;
24 |
25 | /**
26 | * Author: jammehcow.
27 | * Date: 22/04/17.
28 | */
29 |
30 | public class WebhookPublisher extends Notifier {
31 | private final String webhookURL;
32 | private final String branchName;
33 | private final String statusTitle;
34 | private final String thumbnailURL;
35 | private final String notes;
36 | private final String customAvatarUrl;
37 | private final String customUsername;
38 | private final boolean sendOnStateChange;
39 | private final boolean sendOnlyFailed;
40 | private boolean enableUrlLinking;
41 | private final boolean enableArtifactList;
42 | private final boolean enableFooterInfo;
43 | private boolean showChangeset;
44 | private boolean sendLogFile;
45 | private boolean sendStartNotification;
46 | private static final String NAME = "Discord Notifier";
47 | private static final String VERSION = "1.4.11";
48 | private final String scmWebUrl;
49 |
50 | @DataBoundConstructor
51 | public WebhookPublisher(
52 | String webhookURL,
53 | String thumbnailURL,
54 | boolean sendOnStateChange,
55 | String statusTitle,
56 | String notes,
57 | String branchName,
58 | String customAvatarUrl,
59 | String customUsername,
60 | boolean sendOnStateFailed, boolean sendOnlyFailed, boolean enableUrlLinking,
61 | boolean enableArtifactList,
62 | boolean enableFooterInfo,
63 | boolean showChangeset,
64 | boolean sendLogFile,
65 | boolean sendStartNotification,
66 | String scmWebUrl
67 | ) {
68 | this.webhookURL = webhookURL;
69 | this.thumbnailURL = thumbnailURL;
70 | this.sendOnStateChange = sendOnStateChange;
71 | this.sendOnlyFailed = sendOnlyFailed;
72 | this.enableUrlLinking = enableUrlLinking;
73 | this.enableArtifactList = enableArtifactList;
74 | this.enableFooterInfo = enableFooterInfo;
75 | this.showChangeset = showChangeset;
76 | this.branchName = branchName;
77 | this.statusTitle = statusTitle;
78 | this.notes = notes;
79 | this.customAvatarUrl = customAvatarUrl;
80 | this.customUsername = customUsername;
81 | this.sendLogFile = sendLogFile;
82 | this.sendStartNotification = sendStartNotification;
83 | this.scmWebUrl = scmWebUrl;
84 | }
85 |
86 | public String getWebhookURL() {
87 | return this.webhookURL;
88 | }
89 |
90 | public String getBranchName() {
91 | return this.branchName;
92 | }
93 |
94 | public String getStatusTitle() {
95 | return this.statusTitle;
96 | }
97 |
98 | public String getCustomAvatarUrl() {
99 | return this.customAvatarUrl;
100 | }
101 |
102 | public String getCustomUsername() {
103 | return this.customUsername;
104 | }
105 |
106 | public String getNotes() {
107 | return this.notes;
108 | }
109 |
110 | public String getThumbnailURL() {
111 | return this.thumbnailURL;
112 | }
113 |
114 | public boolean isSendOnStateChange() {
115 | return this.sendOnStateChange;
116 | }
117 |
118 | public boolean isSendOnlyFailed() {
119 | return this.sendOnlyFailed;
120 | }
121 |
122 |
123 | public boolean isEnableUrlLinking() {
124 | return this.enableUrlLinking;
125 | }
126 |
127 | public boolean isEnableArtifactList() {
128 | return this.enableArtifactList;
129 | }
130 |
131 | public boolean isEnableFooterInfo() {
132 | return this.enableFooterInfo;
133 | }
134 |
135 | public boolean isShowChangeset() {
136 | return this.showChangeset;
137 | }
138 |
139 | public boolean isSendLogFile() {
140 | return this.sendLogFile;
141 | }
142 |
143 | public boolean isSendStartNotification() {
144 | return this.sendStartNotification;
145 | }
146 |
147 | public String getScmWebUrl() {
148 | return this.scmWebUrl;
149 | }
150 |
151 | @Override
152 | public boolean needsToRunAfterFinalized() {
153 | return true;
154 | }
155 |
156 | @Override
157 | public boolean prebuild(AbstractBuild, ?> build, BuildListener listener) {
158 | final EnvVars env;
159 | listener.getLogger().println(sendStartNotification);
160 | if (sendStartNotification) {
161 | try {
162 | env = build.getEnvironment(listener);
163 | DiscordWebhook wh = new DiscordWebhook(env.expand(this.webhookURL));
164 | AbstractProject project = build.getProject();
165 | String description;
166 | JenkinsLocationConfiguration globalConfig = JenkinsLocationConfiguration.get();
167 | wh.setStatus(DiscordWebhook.StatusColor.GREEN);
168 | if (this.statusTitle != null && !this.statusTitle.isEmpty()) {
169 | wh.setTitle("Build started: " + env.expand(this.statusTitle));
170 | } else {
171 | wh.setTitle("Build started: " + project.getDisplayName() + " #" + build.getId());
172 | }
173 | String branchNameString = "";
174 | if (branchName != null && !branchName.isEmpty()) {
175 | branchNameString = "**Branch:** " + env.expand(branchName) + "\n";
176 | }
177 | if (this.enableUrlLinking) {
178 | String url = globalConfig.getUrl() + build.getUrl();
179 | description = branchNameString
180 | + "**Build:** "
181 | + getMarkdownHyperlink(build.getId(), url);
182 | wh.setURL(url);
183 | } else {
184 | description = branchNameString
185 | + "**Build:** "
186 | + build.getId();
187 | }
188 | wh.setDescription(new EmbedDescription(build, globalConfig, description, false, false, null).toString());
189 | wh.send();
190 | } catch (WebhookException | InterruptedException | IOException e1) {
191 | e1.printStackTrace(listener.getLogger());
192 | }
193 | }
194 | return true;
195 | }
196 |
197 | //TODO clean this function
198 | @Override
199 | public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
200 | final EnvVars env = build.getEnvironment(listener);
201 | // The global configuration, used to fetch the instance url
202 | JenkinsLocationConfiguration globalConfig = JenkinsLocationConfiguration.get();
203 | if (build.getResult() == null) {
204 | listener.getLogger().println("[Discord Notifier] build.getResult() is null!");
205 | return true;
206 | }
207 |
208 | // Create a new webhook payload
209 | DiscordWebhook wh = new DiscordWebhook(env.expand(this.webhookURL));
210 |
211 | if (this.webhookURL.isEmpty()) {
212 | // Stop the plugin from continuing when the webhook URL isn't set. Shouldn't happen due to form validation
213 | listener.getLogger().println("The Discord webhook is not set!");
214 | return true;
215 | }
216 |
217 | if (this.enableUrlLinking && (globalConfig.getUrl() == null || globalConfig.getUrl().isEmpty())) {
218 | // Disable linking when the instance URL isn't set
219 | listener.getLogger().println("Your Jenkins URL is not set (or is set to localhost)! Disabling linking.");
220 | this.enableUrlLinking = false;
221 | }
222 |
223 | if (this.sendOnStateChange) {
224 | if (build.getPreviousBuild() != null && build.getResult().equals(build.getPreviousBuild().getResult())) {
225 | // Stops the webhook payload being created if the status is the same as the previous
226 | return true;
227 | }
228 | }
229 |
230 | if (this.sendOnlyFailed) {
231 | if (!build.getResult().equals(Result.FAILURE)) {
232 | return true;
233 | }
234 | }
235 |
236 | if (this.sendLogFile) {
237 | wh.setFile(build.getLogInputStream(), "build" + build.getNumber() + ".log");
238 | }
239 |
240 | DiscordWebhook.StatusColor statusColor = DiscordWebhook.StatusColor.GREEN;
241 | Result buildresult = build.getResult();
242 | if (!buildresult.isCompleteBuild()) return true;
243 | if (buildresult.isBetterOrEqualTo(Result.SUCCESS)) statusColor = DiscordWebhook.StatusColor.GREEN;
244 | if (buildresult.isWorseThan(Result.SUCCESS)) statusColor = DiscordWebhook.StatusColor.YELLOW;
245 | if (buildresult.isWorseThan(Result.UNSTABLE)) statusColor = DiscordWebhook.StatusColor.RED;
246 |
247 | AbstractProject project = build.getProject();
248 | StringBuilder combinationString = new StringBuilder();
249 | if (this.statusTitle != null && !this.statusTitle.isEmpty()) {
250 | wh.setTitle(env.expand(this.statusTitle));
251 | } else {
252 | wh.setTitle(project.getDisplayName() + " #" + build.getId());
253 | }
254 |
255 | //Check if MatrixConfiguration
256 | if (project instanceof MatrixConfiguration) {
257 | wh.setTitle(project.getParent().getDisplayName() + " #" + build.getId());
258 | combinationString.append("**Configuration matrix:**\n");
259 | for (Map.Entry e : ((MatrixConfiguration) project).getCombination().entrySet())
260 | combinationString.append(" - ").append(e.getKey()).append(": ").append(e.getValue()).append("\n");
261 | }
262 |
263 | String branchNameString = "";
264 | if (branchName != null && !branchName.isEmpty()) {
265 | branchNameString = "**Branch:** " + env.expand(branchName) + "\n";
266 | }
267 |
268 | String descriptionPrefix;
269 | // Adds links to the description and title if enableUrlLinking is enabled
270 | if (this.enableUrlLinking) {
271 | String url = globalConfig.getUrl() + build.getUrl();
272 | descriptionPrefix = branchNameString
273 | + "**Build:** "
274 | + getMarkdownHyperlink(build.getId(), url)
275 | + "\n**Status:** "
276 | + getMarkdownHyperlink(build.getResult().toString().toLowerCase(), url) + "\n";
277 | wh.setURL(url);
278 | } else {
279 | descriptionPrefix = branchNameString
280 | + "**Build:** "
281 | + build.getId()
282 | + "\n**Status:** "
283 | + build.getResult().toString().toLowerCase() + "\n";
284 | }
285 | descriptionPrefix += combinationString;
286 |
287 | if (notes != null && notes.length() > 0) {
288 | wh.setContent(env.expand(notes));
289 | }
290 |
291 | if (customAvatarUrl != null && !customAvatarUrl.isEmpty()) {
292 | wh.setCustomAvatarUrl(customAvatarUrl);
293 | }
294 |
295 | if (customUsername != null && !customUsername.isEmpty()) {
296 | wh.setCustomUsername(customUsername);
297 | }
298 |
299 | wh.setThumbnail(thumbnailURL);
300 | wh.setDescription(
301 | new EmbedDescription(build, globalConfig, descriptionPrefix, this.enableArtifactList, this.showChangeset, this.scmWebUrl)
302 | .toString()
303 | );
304 | wh.setStatus(statusColor);
305 |
306 | if (this.enableFooterInfo)
307 | wh.setFooter("Jenkins v" + build.getHudsonVersion() + ", " + getDescriptor().getDisplayName() + " v" + getDescriptor().getVersion());
308 |
309 | try {
310 | listener.getLogger().println("Sending notification to Discord.");
311 | wh.send();
312 | } catch (WebhookException e) {
313 | e.printStackTrace(listener.getLogger());
314 | }
315 |
316 | return true;
317 | }
318 |
319 | public BuildStepMonitor getRequiredMonitorService() {
320 | return BuildStepMonitor.NONE;
321 | }
322 |
323 | @Override
324 | public DescriptorImpl getDescriptor() {
325 | return (DescriptorImpl) super.getDescriptor();
326 | }
327 |
328 | @Extension
329 | public static final class DescriptorImpl extends BuildStepDescriptor {
330 | public boolean isApplicable(Class extends AbstractProject> aClass) {
331 | return true;
332 | }
333 |
334 | public FormValidation doCheckWebhookURL(@QueryParameter String value) {
335 | if (!value.matches("https://(canary\\.|ptb\\.|)discord(app)*\\.com/api/webhooks/\\d{18}/(\\w|-|_)*(/?)"))
336 | return FormValidation.error("Please enter a valid Discord webhook URL.");
337 | return FormValidation.ok();
338 | }
339 |
340 | public String getDisplayName() {
341 | return NAME;
342 | }
343 |
344 | public String getVersion() {
345 | return VERSION;
346 | }
347 | }
348 |
349 | private static String getMarkdownHyperlink(String content, String url) {
350 | url = url.replaceAll("\\)", "\\\\\\)");
351 | return "[" + content + "](" + url + ")";
352 | }
353 | }
354 |
--------------------------------------------------------------------------------