├── .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 |
2 | If you don't have a webhook url take a look at this intro to Discord Webhooks. 3 |
Paste the entire url in the textbox. 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/nz/co/jammehcow/jenkinsdiscord/DiscordPipelineStep/help-webhookURL.html: -------------------------------------------------------------------------------- 1 |
2 | If you don't have a webhook url take a look at this intro to Discord Webhooks. 3 |
Paste the entire url in the textbox. 4 |
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 |
2 | If you don't have a webhook url take a look at this intro to Discord Webhooks. 3 |
Paste the entire url in the textbox. 4 |
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 |
2 | Message text which supports limited markdown. 3 |
4 |
See Markdown Text 101 (Chat Formatting: Bold, Italic, Underline) for more info on markdown support. 5 |
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 | ![Post-build dropdown with Discord Webhooks selected](https://github.com/jammehcow/jenkins-discord/blob/master/.github/usage_01.jpg) 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 | ![Standard options in the Discord Webhook config](https://github.com/jammehcow/jenkins-discord/blob/master/.github/usage_02.jpg) 45 | ![Advanced tab in the config](https://github.com/jammehcow/jenkins-discord/blob/master/.github/usage_03.jpg) 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 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 | --------------------------------------------------------------------------------