├── .all-contributorsrc ├── .github ├── FUNDING.yml └── workflows │ └── jenkins-security-scan.yml ├── .gitignore ├── CHANGELOG.md ├── Jenkinsfile ├── LICENSE.txt ├── README.md ├── Vagrantfile ├── build.gradle ├── config └── codenarc │ ├── rules.groovy │ └── rulesTest.groovy ├── docs ├── Building-Project.md ├── Design-Consideration.md ├── jira-changelog-trigger-configuration.png ├── jira-changelog-trigger-configuration_50.png ├── jira-comment-trigger-configuration.png └── jira-comment-trigger-configuration_50.png ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── integrationTest ├── groovy │ └── com │ │ └── ceilfors │ │ └── jenkins │ │ └── plugins │ │ └── jiratrigger │ │ ├── JiraTriggerAcceptanceTest.groovy │ │ ├── JiraTriggerIntegrationTest.groovy │ │ ├── UiTest.groovy │ │ ├── integration │ │ ├── FakeJiraCloudRunner.groovy │ │ ├── FakeJiraRunner.groovy │ │ ├── JenkinsBlockingQueue.groovy │ │ ├── JenkinsRunner.groovy │ │ ├── JiraChangelogTriggerProject.groovy │ │ ├── JiraCommentTriggerProject.groovy │ │ ├── JiraRunner.groovy │ │ ├── JiraTriggerProject.groovy │ │ └── JulLogLevelRule.groovy │ │ ├── jira │ │ └── JrjcJiraClientIntegrationTest.groovy │ │ └── ui │ │ ├── JiraChangelogTriggerConfigurationPage.groovy │ │ ├── JiraChangelogTriggerConfigurer.groovy │ │ ├── JiraCommentTriggerConfigurationPage.groovy │ │ ├── JiraCommentTriggerConfigurer.groovy │ │ ├── JiraTriggerConfigurationPage.groovy │ │ ├── JiraTriggerConfigurer.groovy │ │ └── JiraTriggerGlobalConfigurationPage.groovy └── resources │ └── com │ └── ceilfors │ └── jenkins │ └── plugins │ └── jiratrigger │ └── integration │ ├── addComment.json │ ├── cloudAddComment.json │ ├── updateCustomField.json │ ├── updateDescription.json │ └── updateStatus.json ├── jiraIntegrationTest └── groovy │ └── com │ └── ceilfors │ └── jenkins │ └── plugins │ └── jiratrigger │ └── integration │ ├── JiraEndToEndAcceptanceTest.groovy │ ├── RealJiraRunner.groovy │ └── RealJiraSetupRule.groovy ├── main ├── groovy │ └── com │ │ └── ceilfors │ │ └── jenkins │ │ └── plugins │ │ └── jiratrigger │ │ ├── ErrorCode.groovy │ │ ├── ExceptionLoggingFilter.groovy │ │ ├── JiraChangelogTrigger.groovy │ │ ├── JiraCommentReplier.groovy │ │ ├── JiraCommentTrigger.groovy │ │ ├── JiraIssueEnvironmentContributingAction.groovy │ │ ├── JiraTrigger.groovy │ │ ├── JiraTriggerErrorCode.groovy │ │ ├── JiraTriggerException.groovy │ │ ├── JiraTriggerExecutor.groovy │ │ ├── JiraTriggerGlobalConfiguration.groovy │ │ ├── JiraTriggerListener.groovy │ │ ├── JiraTriggerModule.groovy │ │ ├── JiraTriggerPlugin.groovy │ │ ├── ParameterMappingAction.groovy │ │ ├── changelog │ │ ├── ChangelogMatcher.groovy │ │ ├── CustomFieldChangelogMatcher.groovy │ │ └── JiraFieldChangelogMatcher.groovy │ │ ├── jira │ │ ├── AsynchronousWebhookRestClient.groovy │ │ ├── ExtendedJiraRestClient.groovy │ │ ├── JiraClient.groovy │ │ ├── JiraUtils.groovy │ │ ├── JrjcJiraClient.groovy │ │ ├── Webhook.groovy │ │ ├── WebhookInput.groovy │ │ ├── WebhookInputJsonGenerator.groovy │ │ ├── WebhookJsonParser.groovy │ │ ├── WebhookRestClient.groovy │ │ └── WebhooksJsonParser.groovy │ │ ├── parameter │ │ ├── CustomFieldParameterMapping.groovy │ │ ├── CustomFieldParameterResolver.groovy │ │ ├── DefaultParametersAction.groovy │ │ ├── IssueAttributePathParameterMapping.groovy │ │ ├── IssueAttributePathParameterResolver.groovy │ │ ├── ParameterErrorCode.groovy │ │ ├── ParameterMapping.groovy │ │ └── ParameterResolver.groovy │ │ └── webhook │ │ ├── BaseWebhookEvent.groovy │ │ ├── JiraWebhook.groovy │ │ ├── JiraWebhookCrumbExclusion.groovy │ │ ├── JiraWebhookListener.groovy │ │ ├── WebhookChangelogEvent.groovy │ │ ├── WebhookChangelogEventJsonParser.groovy │ │ ├── WebhookCommentEvent.groovy │ │ ├── WebhookCommentEventJsonParser.groovy │ │ └── WebhookJsonParserUtils.groovy └── resources │ ├── com │ └── ceilfors │ │ └── jenkins │ │ └── plugins │ │ └── jiratrigger │ │ ├── JiraChangelogTrigger │ │ ├── config.jelly │ │ └── help.html │ │ ├── JiraCommentTrigger │ │ ├── config.jelly │ │ ├── help-commentPattern.html │ │ └── help.html │ │ ├── JiraTrigger │ │ ├── help-jqlFilter.html │ │ └── help-parameterMappings.html │ │ ├── JiraTriggerGlobalConfiguration │ │ ├── config.jelly │ │ └── help-jiraCommentReply.html │ │ ├── changelog │ │ ├── ChangelogMatcher │ │ │ ├── help-comparingNewValue.html │ │ │ ├── help-comparingOldValue.html │ │ │ ├── help-newValue.html │ │ │ └── help-oldValue.html │ │ ├── CustomFieldChangelogMatcher │ │ │ ├── config.jelly │ │ │ ├── help-field.html │ │ │ └── help.html │ │ └── JiraFieldChangelogMatcher │ │ │ ├── config.jelly │ │ │ ├── help-field.html │ │ │ └── help.html │ │ └── parameter │ │ ├── CustomFieldParameterMapping │ │ ├── config.jelly │ │ ├── help-customFieldId.html │ │ └── help-jenkinsParameter.html │ │ └── IssueAttributePathParameterMapping │ │ ├── config.jelly │ │ ├── help-jenkinsParameter.html │ │ └── help.html │ └── xsd │ ├── maven-jellydoc-plugin │ ├── core.xsd │ ├── define.xsd │ └── source.txt │ ├── maven-site-jenkins-core │ ├── source.txt │ ├── taglib-form.xsd │ ├── taglib-hudson.xsd │ └── taglib-layout.xsd │ └── stapler │ ├── source.txt │ └── taglib.xsd └── test ├── groovy └── com │ └── ceilfors │ └── jenkins │ └── plugins │ └── jiratrigger │ ├── JiraCommentTriggerTest.groovy │ ├── JiraTriggerTest.groovy │ ├── TestUtils.groovy │ ├── changelog │ └── ChangelogMatcherTest.groovy │ ├── jira │ └── JiraUtilsTest.groovy │ ├── parameter │ ├── CustomFieldParameterResolverTest.groovy │ └── IssueAttributePathParameterResolverTest.groovy │ └── webhook │ └── JiraWebhookTest.groovy └── resources ├── com └── ceilfors │ └── jenkins │ └── plugins │ └── jiratrigger │ ├── parameter │ ├── TEST-136.json │ ├── empty_custom_field.json │ ├── multi_value_custom_field.json │ └── single_value_custom_field.json │ └── webhook │ ├── cloud_comment_added.json │ ├── issue_created.json │ ├── issue_updated_fields_updated.json │ ├── issue_updated_status_updated.json │ ├── issue_updated_with_comment.json │ └── issue_updated_without_comment.json └── rest-sample ├── comment.json └── issue_with_customfield.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "zztalker", 10 | "name": "Pavel Zaikin", 11 | "avatar_url": "https://avatars0.githubusercontent.com/u/1027857?v=4", 12 | "profile": "https://medium.com/@zztalker", 13 | "contributions": [ 14 | "code" 15 | ] 16 | }, 17 | { 18 | "login": "rodrigc", 19 | "name": "Craig Rodrigues", 20 | "avatar_url": "https://avatars1.githubusercontent.com/u/1895943?v=4", 21 | "profile": "https://linkedin.com/in/rodrigc", 22 | "contributions": [ 23 | "code" 24 | ] 25 | }, 26 | { 27 | "login": "sghill", 28 | "name": "Steve Hill", 29 | "avatar_url": "https://avatars3.githubusercontent.com/u/230004?v=4", 30 | "profile": "https://github.com/sghill", 31 | "contributions": [ 32 | "code" 33 | ] 34 | }, 35 | { 36 | "login": "duemir", 37 | "name": "Denys Digtiar", 38 | "avatar_url": "https://avatars.githubusercontent.com/u/348580?v=4", 39 | "profile": "https://www.duemir.net/", 40 | "contributions": [ 41 | "code" 42 | ] 43 | } 44 | ], 45 | "contributorsPerLine": 7, 46 | "projectName": "jira-trigger-plugin", 47 | "projectOwner": "jenkinsci", 48 | "repoType": "github", 49 | "repoHost": "https://github.com", 50 | "skipCi": true, 51 | "commitConvention": "angular" 52 | } 53 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ceilfors 4 | patreon: # Replace with a single Patreon username 5 | open_collective: jira-trigger-plugin 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/jenkins-security-scan.yml: -------------------------------------------------------------------------------- 1 | name: Jenkins Security Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [ opened, synchronize, reopened ] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | security-events: write 13 | contents: read 14 | actions: read 15 | 16 | jobs: 17 | security-scan: 18 | uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2 19 | with: 20 | java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. 21 | # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Jenkins ### 2 | work/ 3 | 4 | ### IDEA ### 5 | classes/ 6 | 7 | # Created by https://www.gitignore.io/api/java,intellij,gradle,vagrant,maven 8 | 9 | ### Java ### 10 | *.class 11 | 12 | # Mobile Tools for Java (J2ME) 13 | .mtj.tmp/ 14 | 15 | # Package Files # 16 | *.jar 17 | *.war 18 | *.ear 19 | 20 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 21 | hs_err_pid* 22 | 23 | 24 | ### Intellij ### 25 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 26 | 27 | *.iml 28 | 29 | ## Directory-based project format: 30 | .idea/ 31 | # if you remove the above rule, at least ignore the following: 32 | 33 | # User-specific stuff: 34 | # .idea/workspace.xml 35 | # .idea/tasks.xml 36 | # .idea/dictionaries 37 | # .idea/shelf 38 | 39 | # Sensitive or high-churn files: 40 | # .idea/dataSources.ids 41 | # .idea/dataSources.xml 42 | # .idea/sqlDataSources.xml 43 | # .idea/dynamic.xml 44 | # .idea/uiDesigner.xml 45 | 46 | # Gradle: 47 | # .idea/gradle.xml 48 | # .idea/libraries 49 | 50 | # Mongo Explorer plugin: 51 | # .idea/mongoSettings.xml 52 | 53 | ## File-based project format: 54 | *.ipr 55 | *.iws 56 | 57 | ## Plugin-specific files: 58 | 59 | # IntelliJ 60 | /out/ 61 | 62 | # mpeltonen/sbt-idea plugin 63 | .idea_modules/ 64 | 65 | # JIRA plugin 66 | atlassian-ide-plugin.xml 67 | 68 | # Crashlytics plugin (for Android Studio and IntelliJ) 69 | com_crashlytics_export_strings.xml 70 | crashlytics.properties 71 | crashlytics-build.properties 72 | fabric.properties 73 | 74 | 75 | ### Gradle ### 76 | .gradle 77 | build/ 78 | 79 | # Ignore Gradle GUI config 80 | gradle-app.setting 81 | 82 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 83 | !gradle-wrapper.jar 84 | 85 | # Cache of project 86 | .gradletasknamecache 87 | 88 | 89 | ### Vagrant ### 90 | .vagrant/ 91 | 92 | 93 | ### Maven ### 94 | target/ 95 | pom.xml.tag 96 | pom.xml.releaseBackup 97 | pom.xml.versionsBackup 98 | pom.xml.next 99 | release.properties 100 | dependency-reduced-pom.xml 101 | buildNumber.properties 102 | .mvn/timing.properties 103 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | /* Step is provided by: https://github.com/jenkins-infra/pipeline-library */ 4 | buildPluginWithGradle() 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Wisen Tanasa 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. -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | VAGRANTFILE_API_VERSION = "2" 5 | 6 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 7 | 8 | # Every Vagrant virtual environment requires a box to build off of. 9 | config.vm.box='atlassiandev/connect' 10 | 11 | config.vm.network "forwarded_port", guest: 3000, host: 3000 12 | config.vm.network "forwarded_port", guest: 8000, host: 8000 13 | config.vm.network "forwarded_port", guest: 2990, host: 2990 14 | config.vm.network "forwarded_port", guest: 1990, host: 1990 15 | 16 | 17 | # Required for NFS to work, pick any local IP 18 | config.vm.network "private_network", ip: "192.168.50.50" 19 | # Use NFS for shared folders for better performance 20 | config.vm.synced_folder ".", "/vagrant", type: "nfs" 21 | 22 | config.vm.provider "virtualbox" do |v| 23 | # Originally from Atlassian is 1/2 memory and full cpu, overridden as unnecessary 24 | cpus = 2 25 | mem = 1024 26 | 27 | v.customize ["modifyvm", :id, "--memory", mem] 28 | v.customize ["modifyvm", :id, "--cpus", cpus] 29 | v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] 30 | v.customize ["modifyvm", :id, "--natdnsproxy1", "on"] 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'pl.allegro.tech.build.axion-release' version '1.3.4' 3 | id 'org.jenkins-ci.jpi' version '0.49.0' 4 | id 'codenarc' 5 | id 'idea' 6 | } 7 | 8 | repositories { 9 | maven { 10 | url "https://repo.jenkins-ci.org/public/" 11 | } 12 | jcenter() 13 | maven { 14 | url "https://m2proxy.atlassian.com/repository/public" 15 | } 16 | } 17 | 18 | group = 'org.jenkins-ci.plugins' 19 | description = 'JIRA Trigger' 20 | 21 | java { 22 | toolchain { 23 | languageVersion.set(JavaLanguageVersion.of(8)) 24 | } 25 | } 26 | 27 | sourceSets { 28 | integrationTest { 29 | compileClasspath += main.output + test.output 30 | runtimeClasspath += main.output + test.output 31 | } 32 | jiraIntegrationTest { 33 | compileClasspath += main.output + test.output + integrationTest.output 34 | runtimeClasspath += main.output + test.output + integrationTest.output 35 | } 36 | } 37 | 38 | configurations { 39 | integrationTestCompileClasspath.extendsFrom testCompileClasspath 40 | integrationTestRuntimeClasspath.extendsFrom testRuntimeClasspath 41 | jiraIntegrationTestCompileClasspath.extendsFrom integrationTestCompileClasspath 42 | jiraIntegrationTestRuntimeClasspath.extendsFrom integrationTestRuntimeClasspath 43 | } 44 | 45 | dependencies { 46 | compileOnly 'org.codehaus.groovy:groovy-all:2.4.11' 47 | api('com.atlassian.jira:jira-rest-java-client-core:5.2.1') { 48 | exclude group: 'org.slf4j' 49 | exclude group: 'org.springframework' 50 | exclude group: 'javax.xml.stream', module: 'stax-api' 51 | } 52 | api 'io.atlassian.fugue:fugue:4.7.2' 53 | api('com.google.inject.extensions:guice-multibindings:4.0') { 54 | exclude group: 'com.google.inject', module: 'guice' // already provided by Jenkins 55 | } 56 | testImplementation platform('io.jenkins.tools.bom:bom-2.235.x:918.vae501d2cdc99') 57 | testImplementation 'org.jenkins-ci:test-annotations:1.2' 58 | testImplementation 'org.jenkins-ci.plugins:matrix-auth' 59 | testImplementation 'org.hamcrest:hamcrest-all:1.3' 60 | testImplementation 'org.spockframework:spock-core:1.0-groovy-2.4' 61 | testImplementation 'cglib:cglib-nodep:3.2.5' // used by Spock 62 | testImplementation 'org.objenesis:objenesis:2.5.1' // used by Spock 63 | integrationTestImplementation 'org.codehaus.groovy.modules.http-builder:http-builder:0.7.1' 64 | integrationTestImplementation 'org.jenkins-ci.main:jenkins-war:2.235.1' 65 | testImplementation 'org.jenkins-ci.plugins.workflow:workflow-job' 66 | } 67 | 68 | def integrationTest = tasks.register('integrationTest', Test) { 69 | testClassesDirs = sourceSets.integrationTest.output 70 | classpath = sourceSets.integrationTest.runtimeClasspath 71 | mustRunAfter test 72 | } 73 | tasks.register('jiraIntegrationTest', Test) { 74 | testClassesDirs = sourceSets.jiraIntegrationTest.output 75 | classpath = sourceSets.jiraIntegrationTest.runtimeClasspath 76 | mustRunAfter integrationTest 77 | } 78 | tasks.named('check').configure { 79 | dependsOn integrationTest 80 | } 81 | 82 | tasks.withType(Test).configureEach { 83 | reports.html.outputLocation.set(file("${reporting.baseDir}/${name}")) 84 | } 85 | 86 | jenkinsPlugin { 87 | jenkinsVersion = '2.235.1' 88 | pluginId = 'jira-trigger' 89 | humanReadableName = 'JIRA Trigger Plugin' 90 | homePage = uri('http://wiki.jenkins-ci.org/display/JENKINS/JIRA+Trigger+Plugin') 91 | gitHubUrl = 'https://github.com/ceilfors/jira-trigger-plugin' 92 | minimumJenkinsCoreVersion = '0.2.0' 93 | 94 | // enable injection of additional tests for checking the syntax of Jelly and other things 95 | generateTests = true 96 | 97 | fileExtension = 'jpi' 98 | 99 | developers { 100 | developer { 101 | id 'ceilfors' 102 | name 'Wisen Tanasa' 103 | email 'wisen@ceilfors.com' 104 | } 105 | } 106 | } 107 | 108 | scmVersion { 109 | tag { 110 | prefix = 'v' 111 | versionSeparator = '' 112 | } 113 | } 114 | project.version = scmVersion.version 115 | 116 | codenarc { 117 | configFile = file('config/codenarc/rules.groovy') 118 | toolVersion = '1.1' 119 | } 120 | codenarcTest { 121 | configFile = file('config/codenarc/rulesTest.groovy') 122 | } 123 | codenarcIntegrationTest { 124 | configFile = file('config/codenarc/rulesTest.groovy') 125 | } 126 | codenarcJiraIntegrationTest { 127 | configFile = file('config/codenarc/rulesTest.groovy') 128 | } 129 | 130 | idea { 131 | module { 132 | testSourceDirs += file('src/integrationTest/groovy') 133 | testSourceDirs += file('src/jiraIntegrationTest/groovy') 134 | scopes.TEST.plus += [ configurations.integrationTestCompileClasspath, configurations.jiraIntegrationTestRuntimeClasspath ] 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /config/codenarc/rulesTest.groovy: -------------------------------------------------------------------------------- 1 | ruleset { 2 | ruleset('file:config/codenarc/rules.groovy') { 3 | exclude 'JUnitPublicProperty' // Fields annotated with @org.junit.Rule violate this rule 4 | exclude 'NonFinalPublicField' // Fields annotated with @org.junit.Rule violate this rule 5 | exclude 'PublicInstanceField' // Fields annotated with @org.junit.Rule violate this rule 6 | exclude 'UnusedPrivateField' // Fields annotated with @org.junit.Rule violate this rule 7 | exclude 'MethodName' // More natural sentences in test cases 8 | exclude 'SystemErrPrint' // Needed to segment the integration test 9 | exclude 'JUnitPublicNonTestMethod' // Using Spock 10 | exclude 'NoDef' // Required for acceptance test DSL loose coupling 11 | exclude 'ClosureAsLastMethodParameter' // Required by Spock expectation to match method call by closure 12 | exclude 'FactoryMethodName' // Not important in test classes 13 | exclude 'EmptyClass' // Not important in test classes 14 | exclude 'Instanceof' // Not important in test classes 15 | exclude 'BuilderMethodWithSideEffects' // Not important in test classes, Jenkins use a lot of build keyword 16 | exclude 'ThrowRuntimeException' // Not important in test classes 17 | exclude 'UnnecessaryGetter' // Not important in test classes 18 | exclude 'DuplicateStringLiteral' // Not important in test classes 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/Building-Project.md: -------------------------------------------------------------------------------- 1 | # Setting Java 2 | 3 | This project is still running an old Java version. Once you install Java, make sure you are setting it to your 4 | environment variables. 5 | 6 | Example in macOS: 7 | 8 | ```bash 9 | export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home 10 | export PATH=/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home/bin:$PATH 11 | ``` 12 | 13 | # Building Locally 14 | 15 | - Running test and integration test: `./gradlew clean test` 16 | - Running Jenkins with the plugin pre-installed: `./gradlew server` 17 | 18 | See Jenkins [Gradle JPI Plugin page](https://wiki.jenkins-ci.org/display/JENKINS/Gradle+JPI+Plugin) for more details. 19 | 20 | # Running Integration Test with JIRA 21 | 22 | You will need to run JIRA locally to be able to execute the JIRA Integration Test of this plugin which is available from 23 | vagrant. More details can be found 24 | at [atlassian site](https://developer.atlassian.com/static/connect/docs/latest/developing/developing-locally.html). 25 | 26 | Quick start: 27 | 28 | 1. `vagrant up` 29 | 2. `vagrant ssh` 30 | 3. Accept Oracle license term for Java 31 | 4. `atlas-run-standalone --product jira --version 7.0.0 --plugins com.atlassian.jira.tests:jira-testkit-plugin:7.0.111` 32 | 5. Go to http://localhost:2990/jira in your browser and setup a JIRA project with name TEST (port is forwarded) 33 | 6. `./gradlew jiraIntegrationTest` 34 | 7. Restart JIRA if it starts to complain about license (It is using [timebomb license](https://developer.atlassian.com/market/add-on-licensing-for-developers/timebomb-licenses-for-testing) by default). 35 | 36 | Result of the acceptance test will be available at `$buildDir/reports/jiraIntegrationTest/index.html`. 37 | 38 | # Release 39 | 40 | 1. Run `./gradlew clean build` 41 | 2. `git tag -m vx.x.x vx.x.x` 42 | 3. `./gradlew clean publish` 43 | 44 | Make sure your credentials are set correctly in ~/.jenkins-ci.org. Also check out [the official documentation](https://wiki.jenkins-ci.org/display/JENKINS/Gradle+JPI+Plugin) if there's problem. 45 | 46 | 4. `git push --tags` 47 | 5. Update CHANGELOG.md 48 | -------------------------------------------------------------------------------- /docs/Design-Consideration.md: -------------------------------------------------------------------------------- 1 | # Why not JIRA plugin? 2 | 3 | Jenkins plugin has been chosen as it is perceived that more projects will use JIRA in the cloud and 4 | hosting their own Jenkins server. It is a lot easier for a Jenkins plugin to just hit a JIRA Cloud service 5 | than making a JIRA plugin to communicate with your own hosted Jenkins server e.g. exposing it to the internet. 6 | 7 | This plugin will try to have the communication protocol to work only in one direction, which is only from 8 | Jenkins to JIRA. With that said, in its initial version, JIRA Trigger will only support JIRA webhooks as 9 | it is a lot easier to develop and add the support of polling after that. 10 | 11 | # Avoiding library with net.sf.json-lib:json-lib transitive dependency 12 | 13 | Using a library that has a transitive dependency to `net.sf.json-lib:json-lib` will introduce an error when being 14 | used in conjunction with `GlobalConfiguration` Jenkins extension. This is happening because Jenkins already has a 15 | dependency to `org.kohsuke.stapler:json-lib`, which is a patched version of `net.sf.json-lib:json-lib`: 16 | 17 | ``` 18 | java.lang.LinkageError: loader constraint violation: loader (instance of hudson/PluginFirstClassLoader) previously initiated loading for a different type with name "net/sf/json/JSONObject" 19 | ... 20 | at hudson.ExtensionFinder$GuiceFinder$SezpozModule.resolve(ExtensionFinder.java:484) 21 | at com.google.inject.AbstractModule.configure(AbstractModule.java:62) 22 | ... 23 | at com.google.inject.Guice.createInjector(Guice.java:73) 24 | at hudson.ExtensionFinder$GuiceFinder.(ExtensionFinder.java:282) 25 | ... 26 | ``` 27 | 28 | The patched version of `org.kohsuke.stapler:json-lib` itself is problematic. At of the 29 | time of writing, the method 30 | `net.sf.json.AbstractJSON#_processValue` behaves differently even though the version is the same: 31 | org.kohsuke.stapler:json-lib:2.4-jenkins-3 vs net.sf.json-lib:json-lib:2.4. There is [a commit that 32 | fixes this issue since 2009](https://github.com/jenkinsci/json-lib/commit/3115c86237981793e162a1d95917bf2d686a1705) 33 | that somehow is not released until now. 34 | 35 | One of the library that has been used and problematic in the past is `net.rcarz:jira-client:0.5`. Excluding its 36 | transitive dependency to `json-lib` will introduce a problem during JIRA issue creation 37 | because the POST request body is not constructed properly by `AbstractJSON#_processValue`. 38 | 39 | The value which failed to be processed is in this form: `{"key":"JIRA_PROJECT_TEST"}`. This value is supposed 40 | to be resolved by the `JSONUtils.mayBeJSON` condition in `AbstractJSON`. 41 | 42 | -------------------------------------------------------------------------------- /docs/jira-changelog-trigger-configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/jira-trigger-plugin/ce886c83a97cf646fc6ca9408053fc6fde178a98/docs/jira-changelog-trigger-configuration.png -------------------------------------------------------------------------------- /docs/jira-changelog-trigger-configuration_50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/jira-trigger-plugin/ce886c83a97cf646fc6ca9408053fc6fde178a98/docs/jira-changelog-trigger-configuration_50.png -------------------------------------------------------------------------------- /docs/jira-comment-trigger-configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/jira-trigger-plugin/ce886c83a97cf646fc6ca9408053fc6fde178a98/docs/jira-comment-trigger-configuration.png -------------------------------------------------------------------------------- /docs/jira-comment-trigger-configuration_50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/jira-trigger-plugin/ce886c83a97cf646fc6ca9408053fc6fde178a98/docs/jira-comment-trigger-configuration_50.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/jira-trigger-plugin/ce886c83a97cf646fc6ca9408053fc6fde178a98/gradle.properties -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/jira-trigger-plugin/ce886c83a97cf646fc6ca9408053fc6fde178a98/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This settings file was auto generated by the Gradle buildInit task 3 | * by 'ceilfors' at '20/12/15 17:00' with Gradle 2.2.1 4 | * 5 | * The settings file is used to specify which projects to include in your build. 6 | * In a single project build this file can be empty or even removed. 7 | * 8 | * Detailed information about configuring a multi-project build in Gradle can be found 9 | * in the user guide at http://gradle.org/docs/2.2.1/userguide/multi_project_builds.html 10 | */ 11 | 12 | /* 13 | // To declare projects as part of a multi-project build use the 'include' method 14 | include 'shared' 15 | include 'api' 16 | include 'services:webservice' 17 | */ 18 | 19 | rootProject.name = 'jira-trigger-plugin' 20 | -------------------------------------------------------------------------------- /src/integrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/UiTest.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger 2 | 3 | import com.ceilfors.jenkins.plugins.jiratrigger.changelog.CustomFieldChangelogMatcher 4 | import com.ceilfors.jenkins.plugins.jiratrigger.changelog.JiraFieldChangelogMatcher 5 | import com.ceilfors.jenkins.plugins.jiratrigger.parameter.IssueAttributePathParameterMapping 6 | import com.ceilfors.jenkins.plugins.jiratrigger.ui.JiraChangelogTriggerConfigurer 7 | import com.ceilfors.jenkins.plugins.jiratrigger.ui.JiraCommentTriggerConfigurer 8 | import com.ceilfors.jenkins.plugins.jiratrigger.ui.JiraTriggerConfigurer 9 | import com.ceilfors.jenkins.plugins.jiratrigger.ui.JiraTriggerGlobalConfigurationPage 10 | import hudson.model.FreeStyleProject 11 | import jenkins.model.GlobalConfiguration 12 | import org.junit.Rule 13 | import org.jvnet.hudson.test.JenkinsRule 14 | import spock.lang.Specification 15 | 16 | import static org.hamcrest.Matchers.hasItem 17 | import static org.hamcrest.Matchers.instanceOf 18 | import static org.junit.Assert.assertThat 19 | /** 20 | * @author ceilfors 21 | */ 22 | class UiTest extends Specification { 23 | 24 | @Rule 25 | JenkinsRule jenkins = new JenkinsRule() 26 | 27 | JiraTriggerConfigurer createUiConfigurer(Class triggerType, String jobName) { 28 | if (triggerType == JiraCommentTrigger) { 29 | new JiraCommentTriggerConfigurer(jenkins, jobName) 30 | } else if (triggerType == JiraChangelogTrigger) { 31 | new JiraChangelogTriggerConfigurer(jenkins, jobName) 32 | } else { 33 | throw new UnsupportedOperationException("Trigger $triggerType is unsupported") 34 | } 35 | } 36 | 37 | def 'Sets Global configuration'() { 38 | given: 39 | def configPage = new JiraTriggerGlobalConfigurationPage(jenkins.createWebClient().goTo('configure')) 40 | 41 | when: 42 | configPage.setRootUrl('test root') 43 | configPage.setCredentials('test user', 'test password') 44 | configPage.setJiraCommentReply(true) 45 | configPage.save() 46 | 47 | then: 48 | def globalConfig = GlobalConfiguration.all().get(JiraTriggerGlobalConfiguration) 49 | globalConfig.jiraCommentReply 50 | globalConfig.jiraRootUrl == 'test root' 51 | globalConfig.jiraUsername == 'test user' 52 | globalConfig.jiraPassword.plainText == 'test password' 53 | } 54 | 55 | def 'Sets JQL filter'() { 56 | given: 57 | def jqlFilter = 'non default jql filter' 58 | FreeStyleProject project = jenkins.createFreeStyleProject('job') 59 | JiraTriggerConfigurer configurer = createUiConfigurer(triggerType, 'job') 60 | 61 | when: 62 | configurer.activate() 63 | 64 | then: 65 | assertThat(project.triggers.values(), hasItem(instanceOf(triggerType))) 66 | 67 | when: 68 | configurer.setJqlFilter(jqlFilter) 69 | def trigger = project.getTrigger(triggerType) 70 | 71 | then: 72 | trigger.jqlFilter == jqlFilter 73 | 74 | where: 75 | triggerType << [JiraCommentTrigger, JiraChangelogTrigger] 76 | } 77 | 78 | def 'Adds parameter mappings'() { 79 | given: 80 | FreeStyleProject project = jenkins.createFreeStyleProject('job') 81 | JiraTriggerConfigurer configurer = createUiConfigurer(triggerType, 'job') 82 | 83 | when: 84 | configurer.activate() 85 | configurer.addParameterMapping('parameter1', 'path1') 86 | configurer.addParameterMapping('parameter2', 'path2') 87 | def trigger = project.getTrigger(triggerType) 88 | 89 | then: 90 | trigger.parameterMappings.size() == 2 91 | trigger.parameterMappings[0] == new IssueAttributePathParameterMapping('parameter1', 'path1') 92 | trigger.parameterMappings[1] == new IssueAttributePathParameterMapping('parameter2', 'path2') 93 | 94 | where: 95 | triggerType << [JiraCommentTrigger, JiraChangelogTrigger] 96 | } 97 | 98 | def 'Sets comment pattern'() { 99 | given: 100 | def commentPattern = 'non default comment pattern' 101 | FreeStyleProject project = jenkins.createFreeStyleProject('job') 102 | def configurer = new JiraCommentTriggerConfigurer(jenkins, 'job') 103 | 104 | when: 105 | configurer.activate() 106 | configurer.setCommentPattern(commentPattern) 107 | def trigger = project.getTrigger(JiraCommentTrigger) 108 | 109 | then: 110 | trigger.commentPattern == commentPattern 111 | } 112 | 113 | def 'Adds field matchers'() { 114 | given: 115 | FreeStyleProject project = jenkins.createFreeStyleProject('job') 116 | def configurer = new JiraChangelogTriggerConfigurer(jenkins, 'job') 117 | 118 | when: 119 | configurer.activate() 120 | configurer.addCustomFieldChangelogMatcher('Custom Field 1', 'old 1', '') 121 | configurer.addJiraFieldChangelogMatcher('Jira Field 1', '', 'new 2') 122 | configurer.addCustomFieldChangelogMatcher('Custom Field 2', '', 'new 3') 123 | configurer.addJiraFieldChangelogMatcher('Jira Field 2', 'old 4', '') 124 | def matchers = project.getTrigger(JiraChangelogTrigger).changelogMatchers 125 | 126 | def matcher0 = new CustomFieldChangelogMatcher('Custom Field 1', '', 'old 1') 127 | matcher0.comparingNewValue = false 128 | def matcher1 = new JiraFieldChangelogMatcher('Jira Field 1', 'new 2', '') 129 | matcher1.comparingOldValue = false 130 | def matcher2 = new CustomFieldChangelogMatcher('Custom Field 2', 'new 3', '') 131 | matcher2.comparingOldValue = false 132 | def matcher3 = new JiraFieldChangelogMatcher('Jira Field 2', '', 'old 4') 133 | matcher3.comparingNewValue = false 134 | 135 | then: 136 | matchers.size() == 4 137 | matchers[0] == matcher0 138 | matchers[1] == matcher1 139 | matchers[2] == matcher2 140 | matchers[3] == matcher3 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/integrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/integration/FakeJiraCloudRunner.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.integration 2 | 3 | /** 4 | * @author ceilfors 5 | */ 6 | class FakeJiraCloudRunner extends FakeJiraRunner { 7 | 8 | FakeJiraCloudRunner(JenkinsRunner jenkinsRunner) { 9 | super(jenkinsRunner) 10 | } 11 | 12 | @Override 13 | void addComment(String issueKey, String comment) { 14 | Map body = createPostBody('cloudAddComment', issueKey) 15 | body.comment.body = comment 16 | restClient.post(body: body) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/integrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/integration/FakeJiraRunner.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.integration 2 | 3 | import groovy.json.JsonSlurper 4 | import groovyx.net.http.ContentType 5 | import groovyx.net.http.RESTClient 6 | /** 7 | * @author ceilfors 8 | */ 9 | class FakeJiraRunner implements JiraRunner { 10 | 11 | public static final String CUSTOM_FIELD_NAME = 'My Customer Custom Field' 12 | 13 | private final RESTClient restClient 14 | private int id = 1 15 | private final Map issueMap = [:] 16 | 17 | FakeJiraRunner(JenkinsRunner jenkinsRunner) { 18 | restClient = createRestClient(jenkinsRunner.webhookUrl) 19 | } 20 | 21 | @Override 22 | String createIssue() { 23 | createIssue('') 24 | } 25 | 26 | @Override 27 | String createIssue(String description) { 28 | String issueKey = "TEST-$id" 29 | id++ 30 | issueMap[issueKey] = new FakeIssue(issueKey: issueKey, description: description) 31 | issueKey 32 | } 33 | 34 | @Override 35 | void updateDescription(String issueKey, String description) { 36 | issueMap[issueKey].description = description 37 | Map body = createPostBody('updateDescription', issueKey) 38 | body.changelog.items[0].toString = description 39 | restClient.post(body: body) 40 | } 41 | 42 | @Override 43 | void updateStatus(String issueKey, String status) { 44 | Map body = createPostBody('updateStatus', issueKey) 45 | body.changelog.items[0].toString = status 46 | restClient.post(body: body) 47 | } 48 | 49 | @Override 50 | void updateCustomField(String issueKey, String fieldName, String value) { 51 | Map body = createPostBody('updateCustomField', issueKey) 52 | body.changelog.items[0].field = fieldName 53 | body.changelog.items[0].toString = value 54 | restClient.post(body: body) 55 | } 56 | 57 | @Override 58 | void addComment(String issueKey, String comment) { 59 | Map body = createPostBody('addComment', issueKey) 60 | body.comment.body = comment 61 | restClient.post(body: body) 62 | } 63 | 64 | @Override 65 | boolean validateIssueKey(String issueKey, String jqlFilter) { 66 | true 67 | } 68 | 69 | private RESTClient createRestClient(String jenkinsUrl) { 70 | new RESTClient(jenkinsUrl, ContentType.JSON) 71 | } 72 | 73 | protected Map createPostBody(String method, String issueKey) { 74 | def slurper = new JsonSlurper() 75 | Map body = slurper.parse(new FileReader(new File(this.class.getResource("${method}.json").toURI()))) as Map 76 | body.issue.key = issueKey 77 | body.issue.fields.description = issueMap[issueKey].description 78 | body 79 | } 80 | 81 | protected RESTClient getRestClient() { 82 | restClient 83 | } 84 | 85 | protected static class FakeIssue { 86 | String issueKey 87 | String description 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/integrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/integration/JenkinsBlockingQueue.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.integration 2 | 3 | import com.atlassian.jira.rest.client.api.domain.Issue 4 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraTriggerExecutor 5 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraTriggerListener 6 | import hudson.model.AbstractProject 7 | import jenkins.model.Jenkins 8 | 9 | import java.util.concurrent.ArrayBlockingQueue 10 | import java.util.concurrent.BlockingQueue 11 | import java.util.concurrent.CountDownLatch 12 | import java.util.concurrent.TimeUnit 13 | 14 | /** 15 | * @author ceilfors 16 | */ 17 | class JenkinsBlockingQueue { 18 | 19 | private final BlockingQueue scheduledJobsQueue = new ArrayBlockingQueue<>(1) 20 | private final CountDownLatch countDownLatch = new CountDownLatch(1) 21 | private long timeout = 5 22 | 23 | JenkinsBlockingQueue(Jenkins jenkins) { 24 | def jiraTriggerExecutor = jenkins.injector.getInstance(JiraTriggerExecutor) 25 | jiraTriggerExecutor.addJiraTriggerListener(new JiraTriggerListener() { 26 | 27 | @Override 28 | void buildScheduled(Issue issue, Collection jobs) { 29 | scheduledJobsQueue.offer(jobs, timeout, TimeUnit.SECONDS) 30 | countDownLatch.countDown() 31 | } 32 | 33 | @Override 34 | void buildNotScheduled(Issue issue) { 35 | scheduledJobsQueue.offer([], timeout, TimeUnit.SECONDS) 36 | countDownLatch.countDown() 37 | } 38 | }) 39 | } 40 | 41 | void setTimeout(long timeout) { 42 | this.timeout = timeout 43 | } 44 | 45 | Collection getScheduledJobs() { 46 | scheduledJobsQueue.poll(timeout, TimeUnit.SECONDS) 47 | } 48 | 49 | boolean isAnyJobScheduled() { 50 | countDownLatch.await(timeout, TimeUnit.SECONDS) 51 | Collection scheduledJobs = scheduledJobsQueue.peek() 52 | scheduledJobs != null && !scheduledJobs.isEmpty() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/integrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/integration/JenkinsRunner.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.integration 2 | 3 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraChangelogTrigger 4 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraCommentReplier 5 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraCommentTrigger 6 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraTriggerExecutor 7 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraTriggerGlobalConfiguration 8 | import com.ceilfors.jenkins.plugins.jiratrigger.jira.JiraClient 9 | import com.ceilfors.jenkins.plugins.jiratrigger.webhook.JiraWebhook 10 | import hudson.model.AbstractBuild 11 | import hudson.model.AbstractProject 12 | import hudson.model.FreeStyleProject 13 | import hudson.model.ParametersAction 14 | import hudson.model.Queue 15 | import hudson.model.StringParameterValue 16 | import jenkins.model.GlobalConfiguration 17 | import org.jvnet.hudson.test.JenkinsRule 18 | 19 | import static org.hamcrest.Matchers.containsInAnyOrder 20 | import static org.hamcrest.Matchers.empty 21 | import static org.hamcrest.Matchers.equalTo 22 | import static org.hamcrest.Matchers.is 23 | import static org.hamcrest.Matchers.not 24 | import static org.hamcrest.Matchers.nullValue 25 | import static org.junit.Assert.assertThat 26 | /** 27 | * @author ceilfors 28 | */ 29 | class JenkinsRunner extends JenkinsRule { 30 | 31 | private JenkinsBlockingQueue jenkinsQueue 32 | 33 | @Override 34 | void before() throws Throwable { 35 | super.before() 36 | jenkins.quietPeriod = 0 37 | jenkinsQueue = new JenkinsBlockingQueue(instance) 38 | 39 | JulLogLevelRule.configureLog() // Needed when @IgnoreRest is used in acceptance tests 40 | } 41 | 42 | AbstractBuild getScheduledBuild(AbstractProject project) { 43 | Queue.Item item = project.queueItem 44 | if (item == null) { 45 | project.getBuildByNumber(1) 46 | } else { 47 | item.future.get() 48 | (item.future.startCondition.get() as AbstractBuild) 49 | } 50 | } 51 | 52 | void buildShouldBeScheduled(String jobName, Map parameterMap = [:]) { 53 | Collection jobs = jenkinsQueue.scheduledJobs 54 | assertThat('Build is not scheduled! Check error logs.', jobs, is(not(nullValue()))) 55 | assertThat('Build is not scheduled!', jobs, is(not(empty()))) 56 | assertThat('Only one project is scheduled', jobs.size(), is(equalTo(1))) 57 | 58 | AbstractProject job = jobs.first() 59 | assertThat('Last build scheduled does not match the job name asserted', job.fullName, is(jobName)) 60 | if (parameterMap) { 61 | AbstractBuild build = getScheduledBuild(job) 62 | def parametersAction = build.getAction(ParametersAction) 63 | assertThat(parametersAction.parameters, 64 | containsInAnyOrder(*parameterMap.collect { key, value -> new StringParameterValue(key, value) })) 65 | } 66 | } 67 | 68 | void noBuildShouldBeScheduled() { 69 | if (jenkinsQueue.isAnyJobScheduled()) { 70 | Collection jobs = jenkinsQueue.scheduledJobs 71 | assertThat("Build is scheduled: ${jobs*.fullName}", jobs, is(empty())) 72 | } 73 | } 74 | 75 | JiraTriggerExecutor getJiraTriggerExecutor() { 76 | instance.injector.getInstance(JiraTriggerExecutor) 77 | } 78 | 79 | JiraWebhook getJiraWebhook() { 80 | instance.actions.find { it instanceof JiraWebhook } as JiraWebhook 81 | } 82 | 83 | String getWebhookUrl() { 84 | "${this.URL.toString()}${jiraWebhook.urlName}/" 85 | } 86 | 87 | JiraChangelogTriggerProject createJiraChangelogTriggeredProject(String name) { 88 | FreeStyleProject project = createFreeStyleProject(name) 89 | def trigger = new JiraChangelogTrigger() 90 | project.addTrigger(trigger) 91 | project.save() 92 | trigger.start(project, true) 93 | new JiraChangelogTriggerProject(project) 94 | } 95 | 96 | JiraCommentTriggerProject createJiraCommentTriggeredProject(String name) { 97 | FreeStyleProject project = createFreeStyleProject(name) 98 | def trigger = new JiraCommentTrigger() 99 | project.addTrigger(trigger) 100 | project.save() 101 | trigger.start(project, true) 102 | new JiraCommentTriggerProject(project) 103 | } 104 | 105 | def setJiraCommentReply(boolean active) { 106 | def globalConfig = GlobalConfiguration.all().get(JiraTriggerGlobalConfiguration) 107 | globalConfig.jiraCommentReply = active 108 | globalConfig.save() 109 | } 110 | 111 | void setJiraClient(JiraClient jiraClient) { 112 | // KLUDGE: Could not find a better way to override Guice injection 113 | jenkins.getDescriptorByType(JiraChangelogTrigger.JiraChangelogTriggerDescriptor).jiraClient = jiraClient 114 | jenkins.getDescriptorByType(JiraCommentTrigger.JiraCommentTriggerDescriptor).jiraClient = jiraClient 115 | jenkins.injector.getInstance(JiraTriggerExecutor).jiraTriggerListeners 116 | .grep(JiraCommentReplier)[0].jiraClient = jiraClient 117 | } 118 | 119 | void setQuietPeriod(int quietPeriod) { 120 | this.jenkins.quietPeriod = quietPeriod 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/integrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/integration/JiraChangelogTriggerProject.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.integration 2 | 3 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraChangelogTrigger 4 | import com.ceilfors.jenkins.plugins.jiratrigger.changelog.CustomFieldChangelogMatcher 5 | import com.ceilfors.jenkins.plugins.jiratrigger.changelog.JiraFieldChangelogMatcher 6 | import hudson.model.FreeStyleProject 7 | 8 | /** 9 | * @author ceilfors 10 | */ 11 | class JiraChangelogTriggerProject extends JiraTriggerProject { 12 | 13 | JiraChangelogTriggerProject(FreeStyleProject project) { 14 | super(project) 15 | } 16 | 17 | @Override 18 | JiraChangelogTrigger getJiraTrigger() { 19 | project.getTrigger(JiraChangelogTrigger) 20 | } 21 | 22 | void addJiraFieldChangelogMatcher(String fieldId, String oldValue, String newValue) { 23 | def matcher = new JiraFieldChangelogMatcher(fieldId, newValue, oldValue) 24 | matcher.comparingNewValue = newValue != null && !newValue.empty 25 | matcher.comparingOldValue = oldValue != null && !oldValue.empty 26 | jiraTrigger.changelogMatchers.add(matcher) 27 | project.save() 28 | } 29 | 30 | void addCustomFieldChangelogMatcher(String fieldName, String oldValue, String newValue) { 31 | def matcher = new CustomFieldChangelogMatcher(fieldName, newValue, oldValue) 32 | matcher.comparingNewValue = newValue != null && !newValue.empty 33 | matcher.comparingOldValue = oldValue != null && !oldValue.empty 34 | jiraTrigger.changelogMatchers.add(matcher) 35 | project.save() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/integrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/integration/JiraCommentTriggerProject.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.integration 2 | 3 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraCommentTrigger 4 | import hudson.model.FreeStyleProject 5 | /** 6 | * @author ceilfors 7 | */ 8 | class JiraCommentTriggerProject extends JiraTriggerProject { 9 | 10 | JiraCommentTriggerProject(FreeStyleProject project) { 11 | super(project) 12 | } 13 | 14 | @Override 15 | JiraCommentTrigger getJiraTrigger() { 16 | project.getTrigger(JiraCommentTrigger) 17 | } 18 | 19 | void setCommentPattern(String commentPattern) { 20 | jiraTrigger.setCommentPattern(commentPattern) 21 | project.save() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/integrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/integration/JiraRunner.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.integration 2 | 3 | import com.ceilfors.jenkins.plugins.jiratrigger.jira.JiraClient 4 | 5 | /** 6 | * @author ceilfors 7 | */ 8 | interface JiraRunner extends JiraClient { 9 | 10 | String createIssue() 11 | 12 | String createIssue(String description) 13 | 14 | void updateDescription(String issueKey, String description) 15 | 16 | void updateStatus(String issueKey, String status) 17 | 18 | void updateCustomField(String issueKey, String fieldName, String value) 19 | } -------------------------------------------------------------------------------- /src/integrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/integration/JiraTriggerProject.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.integration 2 | 3 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraTrigger 4 | import com.ceilfors.jenkins.plugins.jiratrigger.parameter.CustomFieldParameterMapping 5 | import com.ceilfors.jenkins.plugins.jiratrigger.parameter.IssueAttributePathParameterMapping 6 | import hudson.model.FreeStyleProject 7 | import hudson.model.ParametersDefinitionProperty 8 | import hudson.model.StringParameterDefinition 9 | /** 10 | * @author ceilfors 11 | */ 12 | abstract class JiraTriggerProject { 13 | 14 | protected FreeStyleProject project 15 | 16 | protected JiraTriggerProject(FreeStyleProject project) { 17 | this.project = project 18 | } 19 | 20 | void addParameterMapping(String jenkinsParameter, String issueAttributePath) { 21 | jiraTrigger.parameterMappings.add(new IssueAttributePathParameterMapping(jenkinsParameter, issueAttributePath)) 22 | project.save() 23 | } 24 | 25 | void addCustomFieldParameterMapping(String jenkinsParameter, String customFieldId) { 26 | jiraTrigger.parameterMappings.add(new CustomFieldParameterMapping(jenkinsParameter, customFieldId)) 27 | project.save() 28 | } 29 | 30 | void setJqlFilter(String jqlFilter) { 31 | jiraTrigger.jqlFilter = jqlFilter 32 | project.save() 33 | } 34 | 35 | void addParameter(String name, String defaultValue) { 36 | def parameterDefinition = new StringParameterDefinition(name, defaultValue) 37 | ParametersDefinitionProperty pdp = project.getProperty(ParametersDefinitionProperty) 38 | if (pdp != null) { 39 | pdp.parameterDefinitions.add(parameterDefinition) 40 | } else { 41 | project.addProperty(new ParametersDefinitionProperty([parameterDefinition])) 42 | } 43 | project.save() 44 | } 45 | 46 | String getAbsoluteUrl() { 47 | project.absoluteUrl 48 | } 49 | 50 | abstract JiraTrigger getJiraTrigger() 51 | } 52 | -------------------------------------------------------------------------------- /src/integrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/integration/JulLogLevelRule.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.integration 2 | 3 | import org.junit.rules.TestRule 4 | import org.junit.runner.Description 5 | import org.junit.runners.model.Statement 6 | 7 | import java.util.logging.ConsoleHandler 8 | import java.util.logging.Handler 9 | import java.util.logging.Level 10 | import java.util.logging.Logger 11 | 12 | /** 13 | * @author ceilfors 14 | */ 15 | class JulLogLevelRule implements TestRule { 16 | 17 | @Override 18 | Statement apply(Statement base, Description description) { 19 | Logger topLogger = Logger.getLogger('') 20 | 21 | Handler consoleHandler = topLogger.handlers.find { it instanceof ConsoleHandler } 22 | if (!consoleHandler) { 23 | consoleHandler = new ConsoleHandler() 24 | topLogger.addHandler(consoleHandler) 25 | } 26 | 27 | consoleHandler.setLevel(Level.FINEST) 28 | 29 | // Required when all of the test cases is in acceptance tests without @Ignore or @IgnoreRest 30 | configureLog() 31 | base 32 | } 33 | 34 | static void configureLog() { 35 | Logger.getLogger('com.gargoylesoftware.htmlunit').setLevel(Level.OFF) 36 | Logger.getLogger('com.ceilfors.jenkins.plugins').setLevel(Level.FINEST) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/integrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/jira/JrjcJiraClientIntegrationTest.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.jira 2 | 3 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraTriggerGlobalConfiguration 4 | import hudson.util.Secret 5 | import jenkins.model.GlobalConfiguration 6 | import org.junit.Rule 7 | import org.jvnet.hudson.test.JenkinsRule 8 | import spock.lang.Specification 9 | 10 | /** 11 | * @author ceilfors 12 | */ 13 | class JrjcJiraClientIntegrationTest extends Specification { 14 | 15 | @Rule 16 | JenkinsRule jenkins = new JenkinsRule() 17 | 18 | def 'Creates a new jira client cache when global configuration is updated'() { 19 | given: 20 | JiraTriggerGlobalConfiguration configuration = GlobalConfiguration.all().get(JiraTriggerGlobalConfiguration) 21 | configuration.jiraRootUrl = 'http://localhost:2990/jira' 22 | configuration.jiraUsername = 'admin' 23 | configuration.jiraPassword = Secret.fromString('admin') 24 | configuration.save() 25 | 26 | JrjcJiraClient jiraClient = jenkins.jenkins.injector.getInstance(JrjcJiraClient) 27 | 28 | when: 29 | def originalJiraRestClient = jiraClient.jiraRestClient 30 | 31 | then: 32 | jiraClient.jiraRestClient.is(originalJiraRestClient) 33 | 34 | when: 35 | configuration.jiraUsername = 'test' 36 | configuration.save() 37 | 38 | then: 39 | !jiraClient.jiraRestClient.is(originalJiraRestClient) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/integrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/ui/JiraChangelogTriggerConfigurationPage.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.ui 2 | 3 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraChangelogTrigger 4 | import com.ceilfors.jenkins.plugins.jiratrigger.changelog.CustomFieldChangelogMatcher 5 | import com.ceilfors.jenkins.plugins.jiratrigger.changelog.JiraFieldChangelogMatcher 6 | import com.gargoylesoftware.htmlunit.html.HtmlAnchor 7 | import com.gargoylesoftware.htmlunit.html.HtmlButton 8 | import com.gargoylesoftware.htmlunit.html.HtmlCheckBoxInput 9 | import com.gargoylesoftware.htmlunit.html.HtmlDivision 10 | import com.gargoylesoftware.htmlunit.html.HtmlPage 11 | import com.gargoylesoftware.htmlunit.html.HtmlTextInput 12 | import hudson.triggers.Trigger 13 | /** 14 | * @author ceilfors 15 | */ 16 | class JiraChangelogTriggerConfigurationPage extends JiraTriggerConfigurationPage { 17 | 18 | JiraChangelogTriggerConfigurationPage(HtmlPage configPage) { 19 | super(configPage) 20 | } 21 | 22 | @Override 23 | protected Class getTriggerType() { 24 | JiraChangelogTrigger 25 | } 26 | 27 | void addJiraFieldChangelogMatcher(String fieldId, String oldValue, String newValue) { 28 | addChangelogMatcher(JiraFieldChangelogMatcher.JiraFieldChangelogMatcherDescriptor.DISPLAY_NAME) 29 | lastFieldText.setValueAttribute(fieldId) 30 | setNewValue(newValue) 31 | setOldValue(oldValue) 32 | } 33 | 34 | def addCustomFieldChangelogMatcher(String fieldName, String oldValue, String newValue) { 35 | addChangelogMatcher(CustomFieldChangelogMatcher.CustomFieldChangelogMatcherDescriptor.DISPLAY_NAME) 36 | lastFieldText.setValueAttribute(fieldName) 37 | setNewValue(newValue) 38 | setOldValue(oldValue) 39 | } 40 | 41 | private void setNewValue(String newValue) { 42 | if (newValue) { 43 | lastComparingNewValueCheckBox.setChecked(true) 44 | lastNewValueText.setValueAttribute(newValue) 45 | } else { 46 | lastComparingNewValueCheckBox.setChecked(false) 47 | } 48 | } 49 | 50 | private void setOldValue(String oldValue) { 51 | if (oldValue) { 52 | lastComparingOldValueCheckBox.setChecked(true) 53 | lastOldValueText.setValueAttribute(oldValue) 54 | } else { 55 | lastComparingOldValueCheckBox.setChecked(false) 56 | } 57 | } 58 | 59 | private void addChangelogMatcher(String displayName) { 60 | HtmlButton addButton = getFirstByXPath(configPage, 61 | 'add changelog matcher button', '//button[contains(@suffix, "changelogMatchers")]') 62 | addButton.click() 63 | 64 | HtmlDivision parameterMappingDiv = addButton.parentNode.parentNode.parentNode as HtmlDivision 65 | HtmlAnchor attribute = getFirstByXPath(parameterMappingDiv, 66 | 'custom field changelog matcher button', "//a[contains(text(), '${displayName}')]") 67 | attribute.click() 68 | configPage.webClient.waitForBackgroundJavaScriptStartingBefore(1000) 69 | } 70 | 71 | private HtmlTextInput getLastFieldText() { 72 | getLastByXPath('field', '//input[contains(@name, "field")]') 73 | } 74 | 75 | private HtmlTextInput getLastNewValueText() { 76 | getLastByXPath('newValue', '//input[contains(@name, "newValue")]') 77 | } 78 | 79 | private HtmlTextInput getLastOldValueText() { 80 | getLastByXPath('oldValue', '//input[contains(@name, "oldValue")]') 81 | } 82 | 83 | private HtmlCheckBoxInput getLastComparingNewValueCheckBox() { 84 | getLastByXPath('comparingNewValue', '//input[contains(@name, "comparingNewValue")]') 85 | } 86 | 87 | private HtmlCheckBoxInput getLastComparingOldValueCheckBox() { 88 | getLastByXPath('comparingOldValue', '//input[contains(@name, "comparingOldValue")]') 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/integrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/ui/JiraChangelogTriggerConfigurer.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.ui 2 | 3 | import com.gargoylesoftware.htmlunit.html.HtmlPage 4 | import org.jvnet.hudson.test.JenkinsRule 5 | /** 6 | * @author ceilfors 7 | */ 8 | class JiraChangelogTriggerConfigurer extends JiraTriggerConfigurer { 9 | 10 | JiraChangelogTriggerConfigurer(JenkinsRule jenkinsRule, String jobName) { 11 | super(jenkinsRule, jobName) 12 | } 13 | 14 | JiraChangelogTriggerConfigurationPage configure() { 15 | JenkinsRule.WebClient webClient = jenkinsRule.createWebClient() 16 | webClient.options.setThrowExceptionOnScriptError(false) 17 | HtmlPage htmlPage = webClient.goTo("job/$jobName/configure") 18 | new JiraChangelogTriggerConfigurationPage(htmlPage) 19 | } 20 | 21 | void addJiraFieldChangelogMatcher(String fieldId, String oldValue, String newValue) { 22 | JiraChangelogTriggerConfigurationPage configPage = configure() 23 | configPage.addJiraFieldChangelogMatcher(fieldId, oldValue, newValue) 24 | configPage.save() 25 | } 26 | 27 | void addCustomFieldChangelogMatcher(String fieldName, String oldValue, String newValue) { 28 | JiraChangelogTriggerConfigurationPage configPage = configure() 29 | configPage.addCustomFieldChangelogMatcher(fieldName, oldValue, newValue) 30 | configPage.save() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/integrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/ui/JiraCommentTriggerConfigurationPage.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.ui 2 | 3 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraCommentTrigger 4 | import com.gargoylesoftware.htmlunit.html.HtmlPage 5 | import com.gargoylesoftware.htmlunit.html.HtmlTextInput 6 | import hudson.triggers.Trigger 7 | /** 8 | * @author ceilfors 9 | */ 10 | class JiraCommentTriggerConfigurationPage extends JiraTriggerConfigurationPage { 11 | 12 | JiraCommentTriggerConfigurationPage(HtmlPage configPage) { 13 | super(configPage) 14 | } 15 | 16 | @Override 17 | protected Class getTriggerType() { 18 | JiraCommentTrigger 19 | } 20 | 21 | void setCommentPattern(String commentPattern) { 22 | commentPatternText.setValueAttribute(commentPattern) 23 | } 24 | 25 | private HtmlTextInput getCommentPatternText() { 26 | getField('commentPattern') 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/integrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/ui/JiraCommentTriggerConfigurer.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.ui 2 | 3 | import com.gargoylesoftware.htmlunit.html.HtmlPage 4 | import org.jvnet.hudson.test.JenkinsRule 5 | /** 6 | * @author ceilfors 7 | */ 8 | class JiraCommentTriggerConfigurer extends JiraTriggerConfigurer { 9 | 10 | JiraCommentTriggerConfigurer(JenkinsRule jenkinsRule, String jobName) { 11 | super(jenkinsRule, jobName) 12 | } 13 | 14 | JiraCommentTriggerConfigurationPage configure() { 15 | HtmlPage htmlPage = jenkinsRule.createWebClient().goTo("job/$jobName/configure") 16 | new JiraCommentTriggerConfigurationPage(htmlPage) 17 | } 18 | 19 | def setCommentPattern(String commentPattern) { 20 | JiraTriggerConfigurationPage configPage = configure() 21 | configPage.setCommentPattern(commentPattern) 22 | configPage.save() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/integrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/ui/JiraTriggerConfigurationPage.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.ui 2 | 3 | import com.gargoylesoftware.htmlunit.html.DomNode 4 | import com.gargoylesoftware.htmlunit.html.HtmlAnchor 5 | import com.gargoylesoftware.htmlunit.html.HtmlButton 6 | import com.gargoylesoftware.htmlunit.html.HtmlCheckBoxInput 7 | import com.gargoylesoftware.htmlunit.html.HtmlDivision 8 | import com.gargoylesoftware.htmlunit.html.HtmlForm 9 | import com.gargoylesoftware.htmlunit.html.HtmlPage 10 | import com.gargoylesoftware.htmlunit.html.HtmlTextInput 11 | import hudson.triggers.Trigger 12 | import com.ceilfors.jenkins.plugins.jiratrigger.parameter.IssueAttributePathParameterMapping 13 | 14 | /** 15 | * @author ceilfors 16 | */ 17 | abstract class JiraTriggerConfigurationPage { 18 | 19 | protected HtmlPage configPage 20 | 21 | protected JiraTriggerConfigurationPage(HtmlPage configPage) { 22 | this.configPage = configPage 23 | } 24 | 25 | void save() { 26 | HtmlForm form = configPage.getFormByName('config') 27 | form.submit((HtmlButton) (form.getElementsByTagName('button')).last()) 28 | configPage.cleanUp() 29 | } 30 | 31 | void addParameterMapping(String jenkinsParameter, String attributePath) { 32 | HtmlButton addButton = getFirstByXPath(configPage, 33 | 'add parameter mapping button', '//button[contains(@suffix, "parameterMappings")]') 34 | addButton.click() 35 | 36 | HtmlDivision parameterMappingDiv = addButton.parentNode.parentNode.parentNode as HtmlDivision 37 | def displayName = IssueAttributePathParameterMapping.IssueAttributePathParameterMappingDescriptor.DISPLAY_NAME 38 | HtmlAnchor attribute = getFirstByXPath(parameterMappingDiv, 39 | 'issue attribute path parameter button', "//a[contains(text(), '${displayName}')]") 40 | attribute.click() 41 | configPage.webClient.waitForBackgroundJavaScriptStartingBefore(1000) 42 | 43 | lastJenkinsParameterText.setValueAttribute(jenkinsParameter) 44 | lastAttributePathText.setValueAttribute(attributePath) 45 | } 46 | 47 | void setJqlFilter(String jqlFilter) { 48 | jqlFilterText.setValueAttribute(jqlFilter) 49 | } 50 | 51 | protected HtmlTextInput getJqlFilterText() { 52 | getField('jqlFilter') 53 | } 54 | 55 | protected HtmlTextInput getLastJenkinsParameterText() { 56 | getLastByXPath('jenkinsParameter', '//input[contains(@name, "jenkinsParameter")]') 57 | } 58 | 59 | protected HtmlTextInput getLastAttributePathText() { 60 | getLastByXPath('attributePath', '//input[contains(@name, "issueAttributePath")]') 61 | } 62 | 63 | void activate() { 64 | triggerCheckBox.setChecked(true) 65 | } 66 | 67 | protected static T throwIfNotFound(String hint, Closure closure) { 68 | T result = closure.call() 69 | if (result) { 70 | return result 71 | } 72 | throw new RuntimeException("Couldn't find $hint") 73 | } 74 | 75 | protected T getField(String fieldName) { 76 | getFirstByXPath(fieldName, "//input[contains(@name, '${fieldName}')]") 77 | } 78 | 79 | protected T getFirstByXPath(String hint, xpathExpr) { 80 | getFirstByXPath(configPage, hint, xpathExpr) 81 | } 82 | 83 | protected T getLastByXPath(String hint, xpathExpr) { 84 | getLastByXPath(configPage, hint, xpathExpr) 85 | } 86 | 87 | protected T getFirstByXPath(DomNode node, String hint, xpathExpr) { 88 | throwIfNotFound(hint) { 89 | node.getFirstByXPath("//tr[@nameref='${nameref}']${xpathExpr}") 90 | } 91 | } 92 | 93 | protected T getLastByXPath(DomNode node, String hint, xpathExpr) { 94 | throwIfNotFound(hint) { 95 | node.getByXPath("//tr[@nameref='${nameref}']${xpathExpr}").last() 96 | } 97 | } 98 | 99 | protected HtmlCheckBoxInput getTriggerCheckBox() { 100 | throwIfNotFound("triggerCheckBox-${triggerType.simpleName}") { 101 | configPage.getFirstByXPath("""//input[contains(@name, "${triggerType.simpleName}")]""") 102 | } 103 | } 104 | 105 | protected String getNameref() { 106 | triggerCheckBox.getAttribute('id') 107 | } 108 | 109 | protected abstract Class getTriggerType() 110 | } 111 | -------------------------------------------------------------------------------- /src/integrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/ui/JiraTriggerConfigurer.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.ui 2 | 3 | import jenkins.model.Jenkins 4 | import org.jvnet.hudson.test.JenkinsRule 5 | /** 6 | * @author ceilfors 7 | */ 8 | abstract class JiraTriggerConfigurer { 9 | 10 | protected JenkinsRule jenkinsRule 11 | protected Jenkins jenkins 12 | protected String jobName 13 | 14 | protected JiraTriggerConfigurer(JenkinsRule jenkinsRule, String jobName) { 15 | this.jenkinsRule = jenkinsRule 16 | this.jenkins = jenkinsRule.instance 17 | this.jobName = jobName 18 | } 19 | 20 | void setJqlFilter(String jqlFilter) { 21 | JiraTriggerConfigurationPage configPage = configure() 22 | configPage.setJqlFilter(jqlFilter) 23 | configPage.save() 24 | } 25 | 26 | void addParameterMapping(String jenkinsParameter, String issueAttributePath) { 27 | JiraTriggerConfigurationPage configPage = configure() 28 | configPage.addParameterMapping(jenkinsParameter, issueAttributePath) 29 | configPage.save() 30 | } 31 | 32 | void activate() { 33 | JiraTriggerConfigurationPage configPage = configure() 34 | configPage.activate() 35 | configPage.save() 36 | } 37 | 38 | abstract JiraTriggerConfigurationPage configure() 39 | } 40 | -------------------------------------------------------------------------------- /src/integrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/ui/JiraTriggerGlobalConfigurationPage.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.ui 2 | 3 | import com.gargoylesoftware.htmlunit.html.HtmlButton 4 | import com.gargoylesoftware.htmlunit.html.HtmlCheckBoxInput 5 | import com.gargoylesoftware.htmlunit.html.HtmlForm 6 | import com.gargoylesoftware.htmlunit.html.HtmlPage 7 | import com.gargoylesoftware.htmlunit.html.HtmlTextInput 8 | 9 | /** 10 | * @author ceilfors 11 | */ 12 | class JiraTriggerGlobalConfigurationPage { 13 | 14 | private final HtmlPage configPage 15 | 16 | JiraTriggerGlobalConfigurationPage(HtmlPage configPage) { 17 | this.configPage = configPage 18 | } 19 | 20 | void save() { 21 | HtmlForm form = configPage.getFormByName('config') 22 | form.submit((HtmlButton) (form.getElementsByTagName('button')).last()) 23 | } 24 | 25 | void setRootUrl(String rootUrl) { 26 | jiraRootUrl.setValueAttribute(rootUrl) 27 | } 28 | 29 | void setCredentials(String username, String password) { 30 | jiraPassword.setValueAttribute(password) 31 | jiraUsername.setValueAttribute(username) 32 | } 33 | 34 | def setJiraCommentReply(boolean active) { 35 | jiraCommentReplyCheckBox.setChecked(active) 36 | } 37 | 38 | private HtmlTextInput getJiraPassword() { 39 | throwIfNotFound('jiraPassword') { 40 | configPage.getFirstByXPath('//input[contains(@name, "jiraPassword")]') 41 | } 42 | } 43 | 44 | private HtmlTextInput getJiraUsername() { 45 | throwIfNotFound('jiraUsername') { 46 | configPage.getFirstByXPath('//input[contains(@name, "jiraUsername")]') 47 | } 48 | } 49 | 50 | private HtmlTextInput getJiraRootUrl() { 51 | throwIfNotFound('jiraRootUrl') { 52 | configPage.getFirstByXPath('//input[contains(@name, "jiraRootUrl")]') 53 | } 54 | } 55 | 56 | private HtmlCheckBoxInput getJiraCommentReplyCheckBox() { 57 | throwIfNotFound('jiraCommentReply') { 58 | configPage.getFirstByXPath('//input[contains(@name, "jiraCommentReply")]') 59 | } 60 | } 61 | 62 | private static T throwIfNotFound(String hint, Closure closure) { 63 | T result = closure.call() 64 | if (result) { 65 | return result 66 | } 67 | throw new RuntimeException("Couldn't find $hint") 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/integrationTest/resources/com/ceilfors/jenkins/plugins/jiratrigger/integration/cloudAddComment.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": 1524205492300, 3 | "webhookEvent": "comment_created", 4 | "comment": { 5 | "self": "https://jira-trigger-plugin.atlassian.net/rest/api/2/issue/10023/comment/10021", 6 | "id": "10021", 7 | "author": { 8 | "self": "https://jira-trigger-plugin.atlassian.net/rest/api/2/user?username=admin", 9 | "name": "admin", 10 | "key": "admin", 11 | "accountId": "557058:eb43b4a2-fad1-4ff6-a2d9-38eee0b088b9", 12 | "avatarUrls": { 13 | "48x48": "https://avatar-cdn.atlassian.com/e610a58b5e6c136fa2b2f509969b15c3?s=48&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fe610a58b5e6c136fa2b2f509969b15c3%3Fd%3Dmm%26s%3D48%26noRedirect%3Dtrue", 14 | "24x24": "https://avatar-cdn.atlassian.com/e610a58b5e6c136fa2b2f509969b15c3?s=24&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fe610a58b5e6c136fa2b2f509969b15c3%3Fd%3Dmm%26s%3D24%26noRedirect%3Dtrue", 15 | "16x16": "https://avatar-cdn.atlassian.com/e610a58b5e6c136fa2b2f509969b15c3?s=16&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fe610a58b5e6c136fa2b2f509969b15c3%3Fd%3Dmm%26s%3D16%26noRedirect%3Dtrue", 16 | "32x32": "https://avatar-cdn.atlassian.com/e610a58b5e6c136fa2b2f509969b15c3?s=32&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fe610a58b5e6c136fa2b2f509969b15c3%3Fd%3Dmm%26s%3D32%26noRedirect%3Dtrue" 17 | }, 18 | "displayName": "Wisen Tanasa", 19 | "active": true, 20 | "timeZone": "Europe/London" 21 | }, 22 | "body": "build this please", 23 | "updateAuthor": { 24 | "self": "https://jira-trigger-plugin.atlassian.net/rest/api/2/user?username=admin", 25 | "name": "admin", 26 | "key": "admin", 27 | "accountId": "557058:eb43b4a2-fad1-4ff6-a2d9-38eee0b088b9", 28 | "avatarUrls": { 29 | "48x48": "https://avatar-cdn.atlassian.com/e610a58b5e6c136fa2b2f509969b15c3?s=48&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fe610a58b5e6c136fa2b2f509969b15c3%3Fd%3Dmm%26s%3D48%26noRedirect%3Dtrue", 30 | "24x24": "https://avatar-cdn.atlassian.com/e610a58b5e6c136fa2b2f509969b15c3?s=24&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fe610a58b5e6c136fa2b2f509969b15c3%3Fd%3Dmm%26s%3D24%26noRedirect%3Dtrue", 31 | "16x16": "https://avatar-cdn.atlassian.com/e610a58b5e6c136fa2b2f509969b15c3?s=16&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fe610a58b5e6c136fa2b2f509969b15c3%3Fd%3Dmm%26s%3D16%26noRedirect%3Dtrue", 32 | "32x32": "https://avatar-cdn.atlassian.com/e610a58b5e6c136fa2b2f509969b15c3?s=32&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fe610a58b5e6c136fa2b2f509969b15c3%3Fd%3Dmm%26s%3D32%26noRedirect%3Dtrue" 33 | }, 34 | "displayName": "Wisen Tanasa", 35 | "active": true, 36 | "timeZone": "Europe/London" 37 | }, 38 | "created": "2018-04-20T07:24:52.300+0100", 39 | "updated": "2018-04-20T07:24:52.300+0100" 40 | }, 41 | "issue": { 42 | "id": "10023", 43 | "self": "https://jira-trigger-plugin.atlassian.net/rest/api/2/issue/10023", 44 | "key": "TEST-24", 45 | "fields": { 46 | "summary": "test", 47 | "issuetype": { 48 | "self": "https://jira-trigger-plugin.atlassian.net/rest/api/2/issuetype/10002", 49 | "id": "10002", 50 | "description": "A task that needs to be done.", 51 | "iconUrl": "https://jira-trigger-plugin.atlassian.net/secure/viewavatar?size=xsmall&avatarId=10318&avatarType=issuetype", 52 | "name": "Task", 53 | "subtask": false, 54 | "avatarId": 10318 55 | }, 56 | "project": { 57 | "self": "https://jira-trigger-plugin.atlassian.net/rest/api/2/project/10000", 58 | "id": "10000", 59 | "key": "TEST", 60 | "name": "test", 61 | "projectTypeKey": "software", 62 | "avatarUrls": { 63 | "48x48": "https://jira-trigger-plugin.atlassian.net/secure/projectavatar?pid=10000&avatarId=10400", 64 | "24x24": "https://jira-trigger-plugin.atlassian.net/secure/projectavatar?size=small&pid=10000&avatarId=10400", 65 | "16x16": "https://jira-trigger-plugin.atlassian.net/secure/projectavatar?size=xsmall&pid=10000&avatarId=10400", 66 | "32x32": "https://jira-trigger-plugin.atlassian.net/secure/projectavatar?size=medium&pid=10000&avatarId=10400" 67 | } 68 | }, 69 | "assignee": null, 70 | "priority": { 71 | "self": "https://jira-trigger-plugin.atlassian.net/rest/api/2/priority/3", 72 | "iconUrl": "https://jira-trigger-plugin.atlassian.net/images/icons/priorities/medium.svg", 73 | "name": "Medium", 74 | "id": "3" 75 | }, 76 | "status": { 77 | "self": "https://jira-trigger-plugin.atlassian.net/rest/api/2/status/10000", 78 | "description": "", 79 | "iconUrl": "https://jira-trigger-plugin.atlassian.net/", 80 | "name": "To Do", 81 | "id": "10000", 82 | "statusCategory": { 83 | "self": "https://jira-trigger-plugin.atlassian.net/rest/api/2/statuscategory/2", 84 | "id": 2, 85 | "key": "new", 86 | "colorName": "blue-gray", 87 | "name": "To Do" 88 | } 89 | } 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /src/integrationTest/resources/com/ceilfors/jenkins/plugins/jiratrigger/integration/updateDescription.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": 1481183527124, 3 | "webhookEvent": "jira:issue_updated", 4 | "user": { 5 | "self": "http://localhost:2990/jira/rest/api/2/user?username=admin", 6 | "name": "admin", 7 | "key": "admin", 8 | "emailAddress": "admin@example.com", 9 | "avatarUrls": { 10 | "48x48": "http://localhost:2990/jira/secure/useravatar?avatarId=10122", 11 | "24x24": "http://localhost:2990/jira/secure/useravatar?size=small&avatarId=10122", 12 | "16x16": "http://localhost:2990/jira/secure/useravatar?size=xsmall&avatarId=10122", 13 | "32x32": "http://localhost:2990/jira/secure/useravatar?size=medium&avatarId=10122" 14 | }, 15 | "displayName": "admin", 16 | "active": true, 17 | "timeZone": "UTC" 18 | }, 19 | "issue": { 20 | "id": "10001", 21 | "self": "http://localhost:2990/jira/rest/api/2/issue/10001", 22 | "key": "TEST-2", 23 | "fields": { 24 | "issuetype": { 25 | "self": "http://localhost:2990/jira/rest/api/2/issuetype/3", 26 | "id": "3", 27 | "description": "A task that needs to be done.", 28 | "iconUrl": "http://localhost:2990/jira/secure/viewavatar?size=xsmall&avatarId=10318&avatarType=issuetype", 29 | "name": "Task", 30 | "subtask": false, 31 | "avatarId": 10318 32 | }, 33 | "components": [ 34 | 35 | ], 36 | "timespent": null, 37 | "timeoriginalestimate": null, 38 | "description": "New description", 39 | "project": { 40 | "self": "http://localhost:2990/jira/rest/api/2/project/10000", 41 | "id": "10000", 42 | "key": "TEST", 43 | "name": "TEST", 44 | "avatarUrls": { 45 | "48x48": "http://localhost:2990/jira/secure/projectavatar?avatarId=10324", 46 | "24x24": "http://localhost:2990/jira/secure/projectavatar?size=small&avatarId=10324", 47 | "16x16": "http://localhost:2990/jira/secure/projectavatar?size=xsmall&avatarId=10324", 48 | "32x32": "http://localhost:2990/jira/secure/projectavatar?size=medium&avatarId=10324" 49 | } 50 | }, 51 | "fixVersions": [ 52 | 53 | ], 54 | "aggregatetimespent": null, 55 | "resolution": null, 56 | "timetracking": { 57 | 58 | }, 59 | "attachment": [ 60 | 61 | ], 62 | "aggregatetimeestimate": null, 63 | "resolutiondate": null, 64 | "workratio": -1, 65 | "summary": "task summary", 66 | "lastViewed": null, 67 | "watches": { 68 | "self": "http://localhost:2990/jira/rest/api/2/issue/TEST-2/watchers", 69 | "watchCount": 1, 70 | "isWatching": true 71 | }, 72 | "creator": { 73 | "self": "http://localhost:2990/jira/rest/api/2/user?username=admin", 74 | "name": "admin", 75 | "key": "admin", 76 | "emailAddress": "admin@example.com", 77 | "avatarUrls": { 78 | "48x48": "http://localhost:2990/jira/secure/useravatar?avatarId=10122", 79 | "24x24": "http://localhost:2990/jira/secure/useravatar?size=small&avatarId=10122", 80 | "16x16": "http://localhost:2990/jira/secure/useravatar?size=xsmall&avatarId=10122", 81 | "32x32": "http://localhost:2990/jira/secure/useravatar?size=medium&avatarId=10122" 82 | }, 83 | "displayName": "admin", 84 | "active": true, 85 | "timeZone": "UTC" 86 | }, 87 | "subtasks": [ 88 | 89 | ], 90 | "created": "2016-12-08T07:51:57.433+0000", 91 | "reporter": { 92 | "self": "http://localhost:2990/jira/rest/api/2/user?username=admin", 93 | "name": "admin", 94 | "key": "admin", 95 | "emailAddress": "admin@example.com", 96 | "avatarUrls": { 97 | "48x48": "http://localhost:2990/jira/secure/useravatar?avatarId=10122", 98 | "24x24": "http://localhost:2990/jira/secure/useravatar?size=small&avatarId=10122", 99 | "16x16": "http://localhost:2990/jira/secure/useravatar?size=xsmall&avatarId=10122", 100 | "32x32": "http://localhost:2990/jira/secure/useravatar?size=medium&avatarId=10122" 101 | }, 102 | "displayName": "admin", 103 | "active": true, 104 | "timeZone": "UTC" 105 | }, 106 | "customfield_10000": null, 107 | "aggregateprogress": { 108 | "progress": 0, 109 | "total": 0 110 | }, 111 | "priority": { 112 | "self": "http://localhost:2990/jira/rest/api/2/priority/3", 113 | "iconUrl": "http://localhost:2990/jira/images/icons/priorities/major.svg", 114 | "name": "Major", 115 | "id": "3" 116 | }, 117 | "labels": [ 118 | 119 | ], 120 | "environment": null, 121 | "timeestimate": null, 122 | "aggregatetimeoriginalestimate": null, 123 | "versions": [ 124 | 125 | ], 126 | "duedate": null, 127 | "progress": { 128 | "progress": 0, 129 | "total": 0 130 | }, 131 | "comment": { 132 | "startAt": 0, 133 | "maxResults": 0, 134 | "total": 0, 135 | "comments": [ 136 | 137 | ] 138 | }, 139 | "issuelinks": [ 140 | 141 | ], 142 | "votes": { 143 | "self": "http://localhost:2990/jira/rest/api/2/issue/TEST-2/votes", 144 | "votes": 0, 145 | "hasVoted": false 146 | }, 147 | "worklog": { 148 | "startAt": 0, 149 | "maxResults": 20, 150 | "total": 0, 151 | "worklogs": [ 152 | 153 | ] 154 | }, 155 | "assignee": { 156 | "self": "http://localhost:2990/jira/rest/api/2/user?username=admin", 157 | "name": "admin", 158 | "key": "admin", 159 | "emailAddress": "admin@example.com", 160 | "avatarUrls": { 161 | "48x48": "http://localhost:2990/jira/secure/useravatar?avatarId=10122", 162 | "24x24": "http://localhost:2990/jira/secure/useravatar?size=small&avatarId=10122", 163 | "16x16": "http://localhost:2990/jira/secure/useravatar?size=xsmall&avatarId=10122", 164 | "32x32": "http://localhost:2990/jira/secure/useravatar?size=medium&avatarId=10122" 165 | }, 166 | "displayName": "admin", 167 | "active": true, 168 | "timeZone": "UTC" 169 | }, 170 | "updated": "2016-12-08T07:52:07.117+0000", 171 | "status": { 172 | "self": "http://localhost:2990/jira/rest/api/2/status/10000", 173 | "description": "\"\"", 174 | "iconUrl": "http://localhost:2990/jira/images/icons/status_generic.gif", 175 | "name": "To Do", 176 | "id": "10000", 177 | "statusCategory": { 178 | "self": "http://localhost:2990/jira/rest/api/2/statuscategory/2", 179 | "id": 2, 180 | "key": "new", 181 | "colorName": "blue-gray", 182 | "name": "To Do" 183 | } 184 | } 185 | } 186 | }, 187 | "changelog": { 188 | "id": "10000", 189 | "items": [ 190 | { 191 | "field": "description", 192 | "fieldtype": "jira", 193 | "from": null, 194 | "fromString": "Original description", 195 | "to": null, 196 | "toString": "New description" 197 | } 198 | ] 199 | } 200 | } -------------------------------------------------------------------------------- /src/jiraIntegrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/integration/JiraEndToEndAcceptanceTest.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.integration 2 | 3 | import org.junit.Rule 4 | import org.junit.rules.ExternalResource 5 | import org.junit.rules.RuleChain 6 | import spock.lang.Specification 7 | 8 | import static com.ceilfors.jenkins.plugins.jiratrigger.JiraCommentTrigger.DEFAULT_COMMENT 9 | 10 | /** 11 | * @author ceilfors 12 | */ 13 | class JiraEndToEndAcceptanceTest extends Specification { 14 | 15 | JenkinsRunner jenkins = new JenkinsRunner() 16 | 17 | @Rule 18 | RuleChain ruleChain = RuleChain 19 | .outerRule(new JulLogLevelRule()) 20 | .around(jenkins) 21 | .around(new RealJiraSetupRule(jenkins)) 22 | .around( 23 | new ExternalResource() { 24 | @Override 25 | protected void before() throws Throwable { 26 | jira = new RealJiraRunner(jenkins) 27 | } 28 | }) 29 | 30 | JiraRunner jira 31 | 32 | def 'Should trigger a build when a comment is added'() { 33 | given: 34 | def issueKey = jira.createIssue() 35 | jenkins.createJiraCommentTriggeredProject('job') 36 | 37 | when: 38 | jira.addComment(issueKey, DEFAULT_COMMENT) 39 | 40 | then: 41 | jenkins.buildShouldBeScheduled('job') 42 | } 43 | 44 | def 'Should trigger a build when an issue is updated'() { 45 | given: 46 | def issueKey = jira.createIssue('Original description') 47 | jenkins.createJiraChangelogTriggeredProject('job') 48 | 49 | when: 50 | jira.updateDescription(issueKey, 'New description') 51 | 52 | then: 53 | jenkins.buildShouldBeScheduled('job') 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/jiraIntegrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/integration/RealJiraRunner.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.integration 2 | 3 | import com.atlassian.jira.rest.client.api.GetCreateIssueMetadataOptionsBuilder 4 | import com.atlassian.jira.rest.client.api.IssueRestClient 5 | import com.atlassian.jira.rest.client.api.domain.CimProject 6 | import com.atlassian.jira.rest.client.api.domain.Issue 7 | import com.atlassian.jira.rest.client.api.domain.Transition 8 | import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder 9 | import com.atlassian.jira.rest.client.api.domain.input.TransitionInput 10 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraTriggerGlobalConfiguration 11 | import com.ceilfors.jenkins.plugins.jiratrigger.jira.JrjcJiraClient 12 | import groovy.util.logging.Log 13 | import jenkins.model.GlobalConfiguration 14 | 15 | /** 16 | * @author ceilfors 17 | */ 18 | @Log 19 | class RealJiraRunner extends JrjcJiraClient implements JiraRunner { 20 | 21 | JenkinsBlockingQueue jenkinsQueue 22 | JenkinsRunner jenkinsRunner 23 | 24 | RealJiraRunner(JenkinsRunner jenkinsRunner) { 25 | super(GlobalConfiguration.all().get(JiraTriggerGlobalConfiguration)) 26 | this.jenkinsQueue = new JenkinsBlockingQueue(jenkinsRunner.instance) 27 | this.jenkinsRunner = jenkinsRunner 28 | } 29 | 30 | String createIssue() { 31 | createIssue('') 32 | } 33 | 34 | String createIssue(String description) { 35 | Long issueTypeId = getIssueTypeId('TEST', 'Task') 36 | IssueInputBuilder issueInputBuilder = new IssueInputBuilder('TEST', issueTypeId, 'task summary') 37 | if (description) { 38 | issueInputBuilder.description = description 39 | } 40 | jiraRestClient.issueClient.createIssue(issueInputBuilder.build()).claim().key 41 | } 42 | 43 | @Override 44 | void updateDescription(String issueKey, String description) { 45 | def issue = new IssueInputBuilder().setDescription(description).build() 46 | jiraRestClient.issueClient.updateIssue(issueKey, issue).get(timeout, timeoutUnit) 47 | } 48 | 49 | @Override 50 | void updateStatus(String issueKey, String status) { 51 | def issue = jiraRestClient.issueClient.getIssue(issueKey, [IssueRestClient.Expandos.TRANSITIONS]) 52 | .get(timeout, timeoutUnit) 53 | jiraRestClient.issueClient.transition(issue, new TransitionInput(getTransition(issue, status).id)) 54 | .get(timeout, timeoutUnit) 55 | } 56 | 57 | private Transition getTransition(Issue issue, String status) { 58 | Iterable transitions = jiraRestClient.issueClient.getTransitions(issue).get(timeout, timeoutUnit) 59 | String transitionName 60 | if (status == 'Done') { 61 | transitionName = 'Done' 62 | } else if (status == 'In Progress') { 63 | transitionName = 'Start Progress' 64 | } else { 65 | throw new UnsupportedOperationException('Configure this method to support more transition name. ' + 66 | "Available transitions: ${transitions*.name}") 67 | } 68 | transitions.find { it.name == transitionName } as Transition 69 | } 70 | 71 | @Override 72 | void updateCustomField(String issueKey, String fieldName, String value) { 73 | String fieldId 74 | if (fieldName == RealJiraSetupRule.CUSTOM_FIELD_NAME) { 75 | fieldId = RealJiraSetupRule.customFieldId 76 | } else { 77 | throw new UnsupportedOperationException("$fieldName not supported") 78 | } 79 | def issue = new IssueInputBuilder().setFieldValue(fieldId, value).build() 80 | jiraRestClient.issueClient.updateIssue(issueKey, issue).get(timeout, timeoutUnit) 81 | } 82 | 83 | private Long getIssueTypeId(String project, String issueTypeName) { 84 | def options = new GetCreateIssueMetadataOptionsBuilder() 85 | .withProjectKeys(project) 86 | .withIssueTypeNames(issueTypeName) 87 | .build() 88 | Iterable metadata = jiraRestClient.issueClient.getCreateIssueMetadata(options).claim() 89 | metadata[0].issueTypes[0].id 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/jiraIntegrationTest/groovy/com/ceilfors/jenkins/plugins/jiratrigger/integration/RealJiraSetupRule.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.integration 2 | 3 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraTriggerGlobalConfiguration 4 | import com.ceilfors.jenkins.plugins.jiratrigger.jira.ExtendedJiraRestClient 5 | import com.ceilfors.jenkins.plugins.jiratrigger.jira.JrjcJiraClient 6 | import com.ceilfors.jenkins.plugins.jiratrigger.jira.Webhook 7 | import com.ceilfors.jenkins.plugins.jiratrigger.jira.WebhookInput 8 | import com.ceilfors.jenkins.plugins.jiratrigger.jira.WebhookRestClient 9 | import com.ceilfors.jenkins.plugins.jiratrigger.webhook.JiraWebhook 10 | import groovyx.net.http.HTTPBuilder 11 | import hudson.util.Secret 12 | import jenkins.model.GlobalConfiguration 13 | import org.junit.rules.ExternalResource 14 | 15 | /** 16 | * @author ceilfors 17 | */ 18 | @SuppressWarnings('AssignmentToStaticFieldFromInstanceMethod') 19 | class RealJiraSetupRule extends ExternalResource { 20 | 21 | public static final String CUSTOM_FIELD_NAME = 'My Customer Custom Field' 22 | public static String customFieldId 23 | 24 | String jiraRootUrl = 'http://localhost:2990/jira' 25 | String jiraUsername = 'admin' 26 | String jiraPassword = 'admin' 27 | JenkinsRunner jenkinsRunner 28 | 29 | RealJiraSetupRule(JenkinsRunner jenkinsRunner) { 30 | this.jenkinsRunner = jenkinsRunner 31 | } 32 | 33 | protected void before() throws Throwable { 34 | configureJenkinsWithNormalUser() 35 | 36 | JiraTriggerGlobalConfiguration configuration = new JiraTriggerGlobalConfiguration(jiraRootUrl, 'admin', 'admin') 37 | ExtendedJiraRestClient jiraRestClient = new JrjcJiraClient(configuration).jiraRestClient 38 | configureWebhook(jiraRestClient.webhookRestClient) 39 | configureCustomField() 40 | } 41 | 42 | /** 43 | * This method hits JIRA Test Kit plugin REST API as the official REST API doesn't 44 | * support listing screen ids. The Test Kit client is however not used due to dependency hell. 45 | * There are a lot of transitive dependencies 46 | * that are dependent on by the test kit but not being declared explicitly. Trying to pull them manually seems 47 | * almost pull the entire Atlassian SDK which is huge. Because of the issue, http builder is 48 | * used instead. 49 | */ 50 | private configureCustomField() { 51 | def http = new HTTPBuilder(jiraRootUrl + '/rest/testkit-test/1.0/') 52 | http.auth.basic 'admin', 'admin' 53 | 54 | def customFieldAlreadyAdded = false 55 | http.get(path: 'customFields/get') { resp, json -> 56 | def customField = json.find { it.name == CUSTOM_FIELD_NAME } 57 | if (customField) { 58 | customFieldId = customField.id 59 | customFieldAlreadyAdded = true 60 | } else { 61 | customFieldAlreadyAdded = false 62 | } 63 | } 64 | if (!customFieldAlreadyAdded) { 65 | http.post(path: 'customFields/create', requestContentType: 'application/json', body: [ 66 | name : CUSTOM_FIELD_NAME, 67 | description: 'A custom field that contains customer name', 68 | type : 'com.atlassian.jira.plugin.system.customfieldtypes:textarea' 69 | ]) { resp, json -> 70 | customFieldId = json.id 71 | } 72 | } 73 | 74 | List screensWithoutCustomField = [] 75 | http.get(path: 'screens') { resp, screens -> 76 | screens.each { screen -> 77 | boolean fieldAlreadyAdded = screen.tabs.find { tab -> tab.fields.find { it.name == CUSTOM_FIELD_NAME } } 78 | if (!fieldAlreadyAdded) { 79 | screensWithoutCustomField.add(screen.name) 80 | } 81 | } 82 | 83 | } 84 | 85 | for (String screen : screensWithoutCustomField) { 86 | http.get(path: 'screens/addField', query: [screen: screen, field: CUSTOM_FIELD_NAME]) 87 | } 88 | } 89 | 90 | def configureWebhook(WebhookRestClient webhookRestClient) { 91 | Iterable webhooks = webhookRestClient.getWebhooks().claim() 92 | webhooks = webhooks.findAll { it.name.contains('gradlew') } 93 | webhooks.each { webhook -> 94 | webhookRestClient.unregisterWebhook(webhook.selfUri).claim() 95 | } 96 | 97 | String vagrantHostDefaultIp = '10.0.2.2' 98 | [ 99 | [name: 'Acceptance Test (gradlew test)', 100 | url : jenkinsRunner.webhookUrl], 101 | [name: 'Acceptance Test (gradlew test) (Vagrant)', 102 | url : jenkinsRunner.webhookUrl.replace('localhost', vagrantHostDefaultIp)], 103 | [name: 'Local Jenkins (gradlew server)', 104 | url : "http://localhost:8080/${jenkinsRunner.jiraWebhook.urlName}/"], 105 | [name: 'Local Jenkins (gradlew server) (Vagrant)', 106 | url : "http://${vagrantHostDefaultIp}:8080/${jenkinsRunner.jiraWebhook.urlName}/"], 107 | ].each { 108 | webhookRestClient.registerWebhook( 109 | new WebhookInput(name: it.name, events: [ 110 | JiraWebhook.ISSUE_UPDATED_WEBHOOK_EVENT, 111 | JiraWebhook.COMMENT_CREATED_WEBHOOK_EVENT 112 | ], url: it.url)).claim() 113 | } 114 | } 115 | 116 | def configureJenkinsWithNormalUser() { 117 | JiraTriggerGlobalConfiguration configuration = GlobalConfiguration.all().get(JiraTriggerGlobalConfiguration) 118 | configuration.jiraRootUrl = jiraRootUrl 119 | configuration.jiraUsername = jiraUsername 120 | configuration.jiraPassword = Secret.fromString(jiraPassword) 121 | configuration.save() 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/ErrorCode.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger 2 | 3 | /** 4 | * @author ceilfors 5 | */ 6 | interface ErrorCode { 7 | 8 | String getCode() 9 | 10 | String name() 11 | } 12 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/ExceptionLoggingFilter.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger 2 | 3 | import groovy.util.logging.Log 4 | import org.apache.commons.lang.exception.ExceptionUtils 5 | 6 | import javax.servlet.Filter 7 | import javax.servlet.FilterChain 8 | import javax.servlet.FilterConfig 9 | import javax.servlet.ServletException 10 | import javax.servlet.ServletRequest 11 | import javax.servlet.ServletResponse 12 | import java.util.logging.Level 13 | 14 | /** 15 | * @author ceilfors 16 | */ 17 | @Log 18 | class ExceptionLoggingFilter implements Filter { 19 | 20 | @Override 21 | void init(FilterConfig filterConfig) throws ServletException { 22 | } 23 | 24 | @SuppressWarnings('CatchThrowable') 25 | @Override 26 | void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 27 | throws IOException, ServletException { 28 | try { 29 | chain.doFilter(request, response) 30 | } catch (Throwable e) { 31 | Throwable rootCause = ExceptionUtils.getRootCause(e) 32 | if (rootCause instanceof JiraTriggerException) { 33 | logException(rootCause) 34 | } 35 | throw e 36 | } 37 | } 38 | 39 | private static void logException(JiraTriggerException e) { 40 | if (e.errorCode == JiraTriggerErrorCode.JIRA_NOT_CONFIGURED) { 41 | log.severe("JIRA is not configured in Jenkins Global Settings. Please set the ${e.attributes['config']}.") 42 | } else { 43 | log.log(Level.SEVERE, 'Hit JiraTriggerException! ' + 44 | '(jira-trigger-plugin has failed to translate this exception to a human friendly error message, ' + 45 | 'please report a bug).', e) 46 | } 47 | } 48 | 49 | @Override 50 | void destroy() { 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/JiraChangelogTrigger.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger 2 | 3 | import com.atlassian.jira.rest.client.api.domain.ChangelogGroup 4 | import com.atlassian.jira.rest.client.api.domain.Issue 5 | import com.ceilfors.jenkins.plugins.jiratrigger.changelog.ChangelogMatcher 6 | import groovy.util.logging.Log 7 | import hudson.Extension 8 | import hudson.model.Cause 9 | import org.kohsuke.stapler.DataBoundConstructor 10 | import org.kohsuke.stapler.DataBoundSetter 11 | 12 | /** 13 | * Responsible for processing ChangelogGroup and determine if a build should be scheduled. 14 | * 15 | * @author ceilfors 16 | */ 17 | @Log 18 | class JiraChangelogTrigger extends JiraTrigger { 19 | 20 | @DataBoundSetter 21 | List changelogMatchers = [] 22 | 23 | @SuppressWarnings('UnnecessaryConstructor') 24 | @DataBoundConstructor 25 | JiraChangelogTrigger() { 26 | } 27 | 28 | @Override 29 | boolean filter(Issue issue, ChangelogGroup changelogGroup) { 30 | for (changelogMatcher in changelogMatchers) { 31 | if (!changelogMatcher.matches(changelogGroup)) { 32 | log.fine("[${job.fullName}] - Not scheduling build: The changelog [${changelogGroup}] doesn't " + 33 | "match with the changelog matcher [${changelogMatcher}]") 34 | return false 35 | } 36 | } 37 | true 38 | } 39 | 40 | @Override 41 | Cause getCause(Issue issue, ChangelogGroup changelogGroup) { 42 | new JiraChangelogTriggerCause() 43 | } 44 | 45 | @SuppressWarnings('UnnecessaryQualifiedReference') 46 | @Extension 47 | static class JiraChangelogTriggerDescriptor extends JiraTrigger.JiraTriggerDescriptor { 48 | 49 | @Override 50 | String getDisplayName() { 51 | 'Build when an issue is updated in JIRA' 52 | } 53 | 54 | @SuppressWarnings('GroovyUnusedDeclaration') // Jenkins jelly 55 | List getChangelogMatcherDescriptors() { 56 | jenkins.getDescriptorList(ChangelogMatcher) 57 | } 58 | } 59 | 60 | static class JiraChangelogTriggerCause extends Cause { 61 | 62 | @Override 63 | String getShortDescription() { 64 | 'JIRA issue is updated' 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/JiraCommentReplier.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger 2 | 3 | import com.atlassian.jira.rest.client.api.domain.Issue 4 | import com.ceilfors.jenkins.plugins.jiratrigger.jira.JiraClient 5 | import hudson.model.AbstractProject 6 | 7 | import javax.inject.Inject 8 | /** 9 | * @author ceilfors 10 | */ 11 | class JiraCommentReplier implements JiraTriggerListener { 12 | 13 | @Inject 14 | JiraClient jiraClient 15 | 16 | @Inject 17 | JiraTriggerGlobalConfiguration jiraTriggerGlobalConfiguration 18 | 19 | @Override 20 | void buildScheduled(Issue issue, Collection projects) { 21 | if (jiraTriggerGlobalConfiguration.jiraCommentReply) { 22 | jiraClient.addComment(issue.key, 'Build is scheduled for: ' + projects*.absoluteUrl.join(', ')) 23 | } 24 | } 25 | 26 | @Override 27 | void buildNotScheduled(Issue issue) { 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/JiraCommentTrigger.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger 2 | 3 | import com.atlassian.jira.rest.client.api.domain.Comment 4 | import com.atlassian.jira.rest.client.api.domain.Issue 5 | import groovy.util.logging.Log 6 | import hudson.Extension 7 | import hudson.model.Cause 8 | import org.kohsuke.stapler.DataBoundConstructor 9 | import org.kohsuke.stapler.DataBoundSetter 10 | 11 | /** 12 | * Responsible for processing Comment and determine if a job should be scheduled. 13 | * 14 | * @author ceilfors 15 | */ 16 | @Log 17 | class JiraCommentTrigger extends JiraTrigger { 18 | 19 | public static final String DEFAULT_COMMENT = 'build this please' 20 | 21 | @DataBoundSetter 22 | String commentPattern = JiraCommentTriggerDescriptor.DEFAULT_COMMENT_PATTERN 23 | 24 | @SuppressWarnings('UnnecessaryConstructor') 25 | @DataBoundConstructor 26 | JiraCommentTrigger() { 27 | } 28 | 29 | @Override 30 | boolean filter(Issue issue, Comment comment) { 31 | String commentBody = comment.body 32 | if (commentPattern) { 33 | if (!(commentBody ==~ commentPattern)) { 34 | log.fine("[${job.fullName}] - Not scheduling build: commentPattern [$commentPattern] doesn't match " + 35 | "with the comment body [$commentBody]") 36 | return false 37 | } 38 | } 39 | true 40 | } 41 | 42 | @Override 43 | Cause getCause(Issue issue, Comment comment) { 44 | new JiraCommentTriggerCause() 45 | } 46 | 47 | @SuppressWarnings('UnnecessaryQualifiedReference') 48 | @Extension 49 | static class JiraCommentTriggerDescriptor extends JiraTrigger.JiraTriggerDescriptor { 50 | 51 | @SuppressWarnings('GroovyUnusedDeclaration') // Jenkins jelly 52 | public static final String DEFAULT_COMMENT_PATTERN = "(?i)${DEFAULT_COMMENT}" 53 | 54 | @Override 55 | String getDisplayName() { 56 | 'Build when a comment is added to JIRA' 57 | } 58 | } 59 | 60 | static class JiraCommentTriggerCause extends Cause { 61 | 62 | @Override 63 | String getShortDescription() { 64 | 'JIRA comment is added' 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/JiraIssueEnvironmentContributingAction.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger 2 | 3 | import com.atlassian.jira.rest.client.api.domain.Issue 4 | import hudson.EnvVars 5 | import hudson.model.AbstractBuild 6 | import hudson.model.EnvironmentContributingAction 7 | import hudson.model.Run 8 | 9 | import javax.annotation.Nonnull 10 | 11 | /** 12 | * @author ceilfors 13 | */ 14 | class JiraIssueEnvironmentContributingAction implements EnvironmentContributingAction { 15 | 16 | String issueKey 17 | 18 | JiraIssueEnvironmentContributingAction(Issue issue) { 19 | this.issueKey = issue.key 20 | } 21 | 22 | @Override 23 | @Deprecated 24 | void buildEnvVars(AbstractBuild build, EnvVars env) { 25 | buildEnvironment(build, env) 26 | } 27 | 28 | @Override 29 | void buildEnvironment(@Nonnull Run run, @Nonnull EnvVars env) { 30 | env.put('JIRA_ISSUE_KEY', issueKey) 31 | } 32 | 33 | @Override 34 | String getIconFileName() { 35 | null 36 | } 37 | 38 | @Override 39 | String getDisplayName() { 40 | null 41 | } 42 | 43 | @Override 44 | String getUrlName() { 45 | null 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/JiraTrigger.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger 2 | 3 | import com.atlassian.jira.rest.client.api.AddressableEntity 4 | import com.atlassian.jira.rest.client.api.domain.Issue 5 | import com.ceilfors.jenkins.plugins.jiratrigger.jira.JiraClient 6 | import com.ceilfors.jenkins.plugins.jiratrigger.parameter.DefaultParametersAction 7 | import com.ceilfors.jenkins.plugins.jiratrigger.parameter.ParameterMapping 8 | import groovy.util.logging.Log 9 | import hudson.model.Action 10 | import hudson.model.Cause 11 | import hudson.model.CauseAction 12 | import hudson.model.Item 13 | import hudson.model.Job 14 | import hudson.triggers.Trigger 15 | import hudson.triggers.TriggerDescriptor 16 | import jenkins.model.Jenkins 17 | import jenkins.model.ParameterizedJobMixIn 18 | import org.kohsuke.stapler.DataBoundSetter 19 | 20 | import javax.inject.Inject 21 | import java.util.concurrent.CopyOnWriteArrayList 22 | 23 | /** 24 | * @author ceilfors 25 | */ 26 | @SuppressWarnings('Instanceof') 27 | @Log 28 | abstract class JiraTrigger extends Trigger { 29 | 30 | @DataBoundSetter 31 | String jqlFilter = '' 32 | 33 | @DataBoundSetter 34 | List parameterMappings = [] 35 | 36 | final boolean run(Issue issue, T t) { 37 | log.fine("[${job.fullName}] - Processing ${issue.key} - ${getId(t)}") 38 | 39 | if (!filter(issue, t)) { 40 | return false 41 | } 42 | if (jqlFilter) { 43 | if (!jiraTriggerDescriptor.jiraClient.validateIssueKey(issue.key, jqlFilter)) { 44 | log.fine("[${job.fullName}] - Not scheduling build: The issue ${issue.key} doesn't " + 45 | "match with the jqlFilter [$jqlFilter]") 46 | return false 47 | } 48 | } 49 | 50 | List actions = [] 51 | if (parameterMappings) { 52 | actions << new ParameterMappingAction(issue, parameterMappings) 53 | } 54 | actions << new DefaultParametersAction(this.job) 55 | actions << new JiraIssueEnvironmentContributingAction(issue) 56 | actions << new CauseAction(getCause(issue, t)) 57 | log.fine("[${job.fullName}] - Scheduling build for ${issue.key} - ${getId(t)}") 58 | 59 | ParameterizedJobMixIn.scheduleBuild2(job, -1, *actions) != null 60 | } 61 | 62 | @Override 63 | void start(Job project, boolean newInstance) { 64 | super.start(project, newInstance) 65 | jiraTriggerDescriptor.addTrigger(this) 66 | } 67 | 68 | @Override 69 | void stop() { 70 | super.stop() 71 | jiraTriggerDescriptor.removeTrigger(this) 72 | } 73 | 74 | Job getJob() { 75 | super.job 76 | } 77 | 78 | abstract boolean filter(Issue issue, T t) 79 | 80 | private String getId(T t) { 81 | t instanceof AddressableEntity ? (t as AddressableEntity).self : t.toString() 82 | } 83 | 84 | JiraTriggerDescriptor getJiraTriggerDescriptor() { 85 | super.descriptor as JiraTriggerDescriptor 86 | } 87 | 88 | abstract Cause getCause(Issue issue, T t) 89 | 90 | @SuppressWarnings('UnnecessaryTransientModifier') 91 | @Log 92 | static abstract class JiraTriggerDescriptor extends TriggerDescriptor { 93 | 94 | @Inject 95 | protected transient Jenkins jenkins 96 | 97 | @Inject 98 | transient JiraClient jiraClient 99 | 100 | private transient final List triggers = new CopyOnWriteArrayList<>() 101 | 102 | @Override 103 | boolean isApplicable(Item item) { 104 | item instanceof Job && item instanceof ParameterizedJobMixIn.ParameterizedJob 105 | } 106 | 107 | @SuppressWarnings('GroovyUnusedDeclaration') // Jenkins jelly 108 | List getParameterMappingDescriptors() { 109 | jenkins.getDescriptorList(ParameterMapping) 110 | } 111 | 112 | protected void addTrigger(JiraTrigger jiraTrigger) { 113 | triggers.add(jiraTrigger) 114 | log.finest("Added [${jiraTrigger.job.fullName}]:[${jiraTrigger.class.simpleName}] to triggers list") 115 | } 116 | 117 | protected void removeTrigger(JiraTrigger jiraTrigger) { 118 | boolean result = triggers.remove(jiraTrigger) 119 | if (result) { 120 | log.finest("Removed [${jiraTrigger.job.fullName}]:[${jiraTrigger.class.simpleName}] from triggers list") 121 | } else { 122 | if (jiraTrigger.job) { 123 | log.warning( 124 | "Bug! Failed to remove [${jiraTrigger.job.fullName}]:[${jiraTrigger.class.simpleName}] " + 125 | 'from triggers list. ' + 126 | 'The job might accidentally be triggered by JIRA. Restart Jenkins to recover.') 127 | } else { 128 | log.finest('Failed to remove trigger as it might not be started yet.' + 129 | 'This is normal for pipeline job.') 130 | } 131 | } 132 | } 133 | 134 | List allTriggers() { 135 | Collections.unmodifiableList(triggers) 136 | } 137 | 138 | @Override 139 | abstract String getDisplayName() 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/JiraTriggerErrorCode.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger 2 | 3 | /** 4 | * @author ceilfors 5 | */ 6 | enum JiraTriggerErrorCode implements ErrorCode { 7 | 8 | JIRA_NOT_CONFIGURED('1') 9 | 10 | private final String code 11 | 12 | JiraTriggerErrorCode(String code) { 13 | this.code = code 14 | } 15 | 16 | @Override 17 | String getCode() { 18 | code 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/JiraTriggerException.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger 2 | 3 | /** 4 | * @author ceilfors 5 | */ 6 | class JiraTriggerException extends RuntimeException { 7 | 8 | final ErrorCode errorCode 9 | final Map attributes = [:] 10 | 11 | JiraTriggerException(ErrorCode errorCode) { 12 | this.errorCode = errorCode 13 | } 14 | 15 | JiraTriggerException(ErrorCode errorCode, Throwable cause) { 16 | super(cause) 17 | this.errorCode = errorCode 18 | } 19 | 20 | JiraTriggerException add(String key, String value) { 21 | attributes.put(key, value) 22 | this 23 | } 24 | 25 | @Override 26 | String getMessage() { 27 | "Class: ${errorCode.class.simpleName}, Name: ${errorCode.name()}, " + 28 | "Code: ${errorCode.code}, Attributes: ${attributes}" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/JiraTriggerExecutor.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger 2 | 3 | import com.atlassian.jira.rest.client.api.domain.ChangelogGroup 4 | import com.atlassian.jira.rest.client.api.domain.Comment 5 | import com.atlassian.jira.rest.client.api.domain.Issue 6 | import com.ceilfors.jenkins.plugins.jiratrigger.webhook.JiraWebhookListener 7 | import com.ceilfors.jenkins.plugins.jiratrigger.webhook.WebhookChangelogEvent 8 | import com.ceilfors.jenkins.plugins.jiratrigger.webhook.WebhookCommentEvent 9 | import com.google.inject.Singleton 10 | import groovy.util.logging.Log 11 | import hudson.model.AbstractProject 12 | import jenkins.model.Jenkins 13 | 14 | import javax.inject.Inject 15 | import java.util.concurrent.CopyOnWriteArrayList 16 | import java.util.logging.Level 17 | 18 | import static com.ceilfors.jenkins.plugins.jiratrigger.JiraTrigger.JiraTriggerDescriptor 19 | 20 | /** 21 | * @author ceilfors 22 | */ 23 | @Singleton 24 | @Log 25 | class JiraTriggerExecutor implements JiraWebhookListener { 26 | 27 | private final Jenkins jenkins 28 | private final List jiraTriggerListeners = new CopyOnWriteArrayList<>() 29 | 30 | @Inject 31 | JiraTriggerExecutor(Jenkins jenkins) { 32 | this.jenkins = jenkins 33 | } 34 | 35 | @Inject 36 | private void setJiraTriggerListeners(Set jiraTriggerListeners) { 37 | this.jiraTriggerListeners.addAll(jiraTriggerListeners) 38 | } 39 | 40 | void addJiraTriggerListener(JiraTriggerListener jiraTriggerListener) { 41 | jiraTriggerListeners << jiraTriggerListener 42 | } 43 | 44 | @Override 45 | void commentCreated(WebhookCommentEvent commentEvent) { 46 | List scheduledProjects = scheduleBuilds(commentEvent.issue, commentEvent.comment) 47 | fireListeners(scheduledProjects, commentEvent.issue) 48 | } 49 | 50 | @Override 51 | void changelogCreated(WebhookChangelogEvent changelogEvent) { 52 | List scheduledProjects = scheduleBuilds(changelogEvent.issue, changelogEvent.changelog) 53 | fireListeners(scheduledProjects, changelogEvent.issue) 54 | } 55 | 56 | private void fireListeners(List scheduledProjects, Issue issue) { 57 | if (scheduledProjects) { 58 | jiraTriggerListeners*.buildScheduled(issue, scheduledProjects) 59 | } else { 60 | jiraTriggerListeners*.buildNotScheduled(issue) 61 | } 62 | } 63 | 64 | List scheduleBuilds(Issue issue, Comment comment) { 65 | scheduleBuildsInternal(JiraCommentTrigger, issue, comment) 66 | } 67 | 68 | List scheduleBuilds(Issue issue, ChangelogGroup changelogGroup) { 69 | scheduleBuildsInternal(JiraChangelogTrigger, issue, changelogGroup) 70 | } 71 | 72 | /** 73 | * @return the scheduled projects 74 | */ 75 | private List scheduleBuildsInternal( 76 | Class triggerClass, Issue issue, Object jiraObject) { 77 | List scheduledProjects = [] 78 | List triggers = getTriggers(triggerClass) 79 | for (trigger in triggers) { 80 | try { 81 | boolean scheduled = trigger.run(issue, jiraObject) 82 | if (scheduled) { 83 | scheduledProjects << trigger.job 84 | } 85 | } catch (e) { 86 | log.log(Level.WARNING, e) { 87 | "Error triggering \"${trigger.job?.fullName}\"".toString() 88 | } 89 | } 90 | } 91 | scheduledProjects 92 | } 93 | 94 | private List getTriggers(Class triggerClass) { 95 | JiraTriggerDescriptor descriptor = jenkins.getDescriptor(triggerClass) as JiraTriggerDescriptor 96 | List triggers = descriptor.allTriggers() 97 | if (!triggers) { 98 | log.fine("Couldn't find any projects that have ${triggerClass.simpleName} configured") 99 | } 100 | triggers 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/JiraTriggerGlobalConfiguration.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger 2 | 3 | import hudson.Extension 4 | import hudson.util.Secret 5 | import jenkins.model.GlobalConfiguration 6 | import net.sf.json.JSONObject 7 | import org.kohsuke.stapler.StaplerRequest 8 | 9 | /** 10 | * @author ceilfors 11 | */ 12 | @Extension 13 | class JiraTriggerGlobalConfiguration extends GlobalConfiguration { 14 | 15 | String jiraRootUrl 16 | String jiraUsername 17 | Secret jiraPassword 18 | boolean jiraCommentReply = false 19 | 20 | JiraTriggerGlobalConfiguration() { 21 | load() 22 | } 23 | 24 | JiraTriggerGlobalConfiguration(String jiraRootUrl, String jiraUsername, String jiraPassword) { 25 | this.jiraRootUrl = jiraRootUrl 26 | this.jiraUsername = jiraUsername 27 | this.setJiraPassword(Secret.fromString(jiraPassword)) 28 | } 29 | 30 | @Override 31 | boolean configure(StaplerRequest req, JSONObject formData) { 32 | req.bindJSON(this, formData) 33 | save() 34 | true 35 | } 36 | 37 | /** 38 | * Validates this global configuration. Do note that the validation must be called lazily as these 39 | * values are only mandatory when Jenkins is required to hit JIRA. 40 | */ 41 | void validateConfiguration() { 42 | JiraTriggerException exception = new JiraTriggerException(JiraTriggerErrorCode.JIRA_NOT_CONFIGURED) 43 | String key = 'config' 44 | if (!jiraRootUrl) { 45 | throw exception.add(key, 'jiraRootUrl') 46 | } 47 | if (!jiraPassword) { 48 | throw exception.add(key, 'jiraPassword') 49 | } 50 | if (!jiraUsername) { 51 | throw exception.add(key, 'jiraUsername') 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/JiraTriggerListener.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger 2 | 3 | import com.atlassian.jira.rest.client.api.domain.Issue 4 | import hudson.model.AbstractProject 5 | /** 6 | * @author ceilfors 7 | */ 8 | interface JiraTriggerListener { 9 | void buildScheduled(Issue issue, Collection projects) 10 | void buildNotScheduled(Issue issue) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/JiraTriggerModule.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger 2 | 3 | import com.ceilfors.jenkins.plugins.jiratrigger.jira.JiraClient 4 | import com.ceilfors.jenkins.plugins.jiratrigger.jira.JrjcJiraClient 5 | import com.ceilfors.jenkins.plugins.jiratrigger.webhook.JiraWebhookListener 6 | import com.google.inject.AbstractModule 7 | import com.google.inject.Scopes 8 | import com.google.inject.multibindings.Multibinder 9 | import hudson.Extension 10 | 11 | /** 12 | * @author ceilfors 13 | */ 14 | @Extension 15 | class JiraTriggerModule extends AbstractModule { 16 | 17 | @Override 18 | protected void configure() { 19 | bind(JiraWebhookListener).to(JiraTriggerExecutor).in(Scopes.SINGLETON) 20 | bind(JiraClient).to(JrjcJiraClient).in(Scopes.SINGLETON) 21 | 22 | Multibinder jiraTriggerListenerBinder = Multibinder.newSetBinder( 23 | binder(), JiraTriggerListener) 24 | jiraTriggerListenerBinder.addBinding().to(JiraCommentReplier) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/JiraTriggerPlugin.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger 2 | 3 | import hudson.Plugin 4 | import hudson.util.PluginServletFilter 5 | 6 | /** 7 | * @author ceilfors 8 | */ 9 | class JiraTriggerPlugin extends Plugin { 10 | 11 | @Override 12 | void start() throws Exception { 13 | PluginServletFilter.addFilter(new ExceptionLoggingFilter()) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/ParameterMappingAction.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger 2 | 3 | import com.atlassian.jira.rest.client.api.domain.Issue 4 | import com.ceilfors.jenkins.plugins.jiratrigger.parameter.ParameterMapping 5 | import hudson.model.ParametersAction 6 | import hudson.model.StringParameterValue 7 | 8 | /** 9 | * @author ceilfors 10 | */ 11 | class ParameterMappingAction extends ParametersAction { 12 | 13 | ParameterMappingAction(Issue issue, List parameterMappings) { 14 | super(parameterMappings.collect { p -> 15 | new StringParameterValue(p.jenkinsParameter, p.parameterResolver.resolve(issue) ?: '') 16 | }, parameterMappings*.jenkinsParameter) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/changelog/ChangelogMatcher.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.changelog 2 | 3 | import com.atlassian.jira.rest.client.api.domain.ChangelogGroup 4 | import com.atlassian.jira.rest.client.api.domain.FieldType 5 | import groovy.transform.EqualsAndHashCode 6 | import groovy.transform.ToString 7 | import hudson.Util 8 | import hudson.model.AbstractDescribableImpl 9 | import hudson.model.Descriptor 10 | import org.kohsuke.stapler.DataBoundSetter 11 | 12 | /** 13 | * @author ceilfors 14 | */ 15 | @ToString(includeNames = true) 16 | @EqualsAndHashCode 17 | abstract class ChangelogMatcher extends AbstractDescribableImpl { 18 | 19 | final FieldType fieldType 20 | final String field 21 | final String newValue 22 | final String oldValue 23 | boolean comparingNewValue = true 24 | boolean comparingOldValue = true 25 | 26 | protected ChangelogMatcher(FieldType fieldType, String field, String newValue, String oldValue) { 27 | this.fieldType = fieldType 28 | this.field = field 29 | this.newValue = newValue 30 | this.oldValue = oldValue 31 | } 32 | 33 | @DataBoundSetter 34 | void setComparingNewValue(boolean comparingNewValue) { 35 | this.comparingNewValue = comparingNewValue 36 | } 37 | 38 | @DataBoundSetter 39 | void setComparingOldValue(boolean comparingOldValue) { 40 | this.comparingOldValue = comparingOldValue 41 | } 42 | 43 | boolean matches(ChangelogGroup changelogGroup) { 44 | changelogGroup.items.find { 45 | it.fieldType == fieldType && 46 | it.field == field && 47 | (comparingNewValue ? Util.fixNull(it.toString as String).equalsIgnoreCase(newValue) : true) && 48 | (comparingOldValue ? Util.fixNull(it.fromString as String).equalsIgnoreCase(oldValue) : true) 49 | } 50 | } 51 | 52 | static abstract class ChangelogMatcherDescriptor extends Descriptor { 53 | 54 | @Override 55 | abstract String getDisplayName() 56 | } 57 | } -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/changelog/CustomFieldChangelogMatcher.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.changelog 2 | 3 | import com.atlassian.jira.rest.client.api.domain.FieldType 4 | import groovy.transform.EqualsAndHashCode 5 | import groovy.transform.ToString 6 | import hudson.Extension 7 | import org.kohsuke.stapler.DataBoundConstructor 8 | /** 9 | * @author ceilfors 10 | */ 11 | @ToString(includeSuper = true) 12 | @EqualsAndHashCode(callSuper = true) 13 | class CustomFieldChangelogMatcher extends ChangelogMatcher { 14 | 15 | @DataBoundConstructor 16 | CustomFieldChangelogMatcher(String field, String newValue, String oldValue) { 17 | super(FieldType.CUSTOM, field.trim(), newValue.trim(), oldValue.trim()) 18 | } 19 | 20 | @SuppressWarnings('UnnecessaryQualifiedReference') // Can't remove qualifier, IntelliJ bug? 21 | @Extension 22 | static class CustomFieldChangelogMatcherDescriptor extends ChangelogMatcher.ChangelogMatcherDescriptor { 23 | 24 | public static final String DISPLAY_NAME = 'Custom Field Matcher' 25 | 26 | @Override 27 | String getDisplayName() { 28 | DISPLAY_NAME 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/changelog/JiraFieldChangelogMatcher.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.changelog 2 | 3 | import com.atlassian.jira.rest.client.api.domain.FieldType 4 | import com.atlassian.jira.rest.client.api.domain.IssueFieldId 5 | import groovy.transform.EqualsAndHashCode 6 | import groovy.transform.ToString 7 | import hudson.Extension 8 | import hudson.util.ComboBoxModel 9 | import org.kohsuke.stapler.DataBoundConstructor 10 | /** 11 | * @author ceilfors 12 | */ 13 | @ToString(includeSuper = true) 14 | @EqualsAndHashCode(callSuper = true) 15 | class JiraFieldChangelogMatcher extends ChangelogMatcher { 16 | 17 | @DataBoundConstructor 18 | JiraFieldChangelogMatcher(String field, String newValue, String oldValue) { 19 | super(FieldType.JIRA, field.trim(), newValue.trim(), oldValue.trim()) 20 | } 21 | 22 | @SuppressWarnings('UnnecessaryQualifiedReference') // Can't remove qualifier, IntelliJ bug? 23 | @Extension 24 | static class JiraFieldChangelogMatcherDescriptor extends ChangelogMatcher.ChangelogMatcherDescriptor { 25 | 26 | public static final String DISPLAY_NAME = 'JIRA Field Matcher' 27 | 28 | @Override 29 | String getDisplayName() { 30 | DISPLAY_NAME 31 | } 32 | 33 | @SuppressWarnings('GroovyUnusedDeclaration') // jelly 34 | ComboBoxModel doFillFieldItems() { 35 | new ComboBoxModel(IssueFieldId.ids().toList()) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/jira/AsynchronousWebhookRestClient.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.jira 2 | 3 | import com.atlassian.httpclient.api.HttpClient 4 | import com.atlassian.jira.rest.client.internal.async.AbstractAsynchronousRestClient 5 | import com.atlassian.jira.rest.client.internal.json.JsonParser 6 | import io.atlassian.util.concurrent.Promise 7 | 8 | import javax.ws.rs.core.UriBuilder 9 | 10 | /** 11 | * @author ceilfors 12 | */ 13 | class AsynchronousWebhookRestClient extends AbstractAsynchronousRestClient implements WebhookRestClient { 14 | 15 | private final URI baseUri 16 | private final JsonParser> webhooksJsonParser = new WebhooksJsonParser() 17 | 18 | protected AsynchronousWebhookRestClient(final URI baseUri, HttpClient client) { 19 | super(client) 20 | this.baseUri = baseUri 21 | } 22 | 23 | @Override 24 | Promise registerWebhook(WebhookInput webhook) { 25 | post(uriBuilder.build(), webhook, new WebhookInputJsonGenerator()) 26 | } 27 | 28 | @Override 29 | Promise unregisterWebhook(URI webhookUri) { 30 | delete(webhookUri) 31 | } 32 | 33 | @Override 34 | Promise> getWebhooks() { 35 | getAndParse(uriBuilder.build(), webhooksJsonParser) 36 | } 37 | 38 | private UriBuilder getUriBuilder() { 39 | UriBuilder.fromUri(baseUri).path('webhook') 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/jira/ExtendedJiraRestClient.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.jira 2 | import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClient 3 | import com.atlassian.jira.rest.client.internal.async.DisposableHttpClient 4 | 5 | import javax.ws.rs.core.UriBuilder 6 | 7 | /** 8 | * Add missing functionality on top of the built-in JiraRestClient. 9 | * 10 | * @author ceilfors 11 | */ 12 | class ExtendedJiraRestClient extends AsynchronousJiraRestClient { 13 | 14 | private final WebhookRestClient webhookRestClient 15 | 16 | ExtendedJiraRestClient(URI serverUri, DisposableHttpClient httpClient) { 17 | super(serverUri, httpClient) 18 | this.webhookRestClient = new AsynchronousWebhookRestClient(UriBuilder.fromUri(serverUri) 19 | .path('/rest/webhooks/latest').build(), httpClient) 20 | } 21 | 22 | WebhookRestClient getWebhookRestClient() { 23 | webhookRestClient 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/jira/JiraClient.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.jira 2 | 3 | /** 4 | * Adapter layer to communicate with JIRA. There are too many 5 | * ways of communicating with JIRA i.e. JRJC, rcarz/jira-client, plain REST, etc. 6 | * 7 | * @author ceilfors 8 | */ 9 | interface JiraClient { 10 | 11 | void addComment(String issueKey, String comment) 12 | 13 | boolean validateIssueKey(String issueKey, String jqlFilter) 14 | } 15 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/jira/JiraUtils.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.jira 2 | 3 | import com.atlassian.jira.rest.client.api.domain.Comment 4 | 5 | /** 6 | * @author ceilfors 7 | */ 8 | class JiraUtils { 9 | 10 | static Long getIssueIdFromComment(Comment comment) { 11 | ((comment.self.toString() =~ '.*issue/(\\d+)/comment/\\d+.*')[0][1]) as Long 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/jira/JrjcJiraClient.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.jira 2 | 3 | import com.atlassian.jira.rest.client.api.domain.Comment 4 | import com.atlassian.jira.rest.client.api.domain.Issue 5 | import com.atlassian.jira.rest.client.api.domain.SearchResult 6 | import com.atlassian.jira.rest.client.auth.BasicHttpAuthenticationHandler 7 | import com.atlassian.jira.rest.client.internal.async.AsynchronousHttpClientFactory 8 | import com.atlassian.jira.rest.client.internal.async.DisposableHttpClient 9 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraTriggerGlobalConfiguration 10 | import com.google.inject.Singleton 11 | import groovy.util.logging.Log 12 | import hudson.Extension 13 | import hudson.XmlFile 14 | import hudson.model.Saveable 15 | import hudson.model.listeners.SaveableListener 16 | 17 | import javax.inject.Inject 18 | import java.util.concurrent.TimeUnit 19 | 20 | /** 21 | * @author ceilfors 22 | */ 23 | @Singleton 24 | @Log 25 | class JrjcJiraClient implements JiraClient { 26 | 27 | long timeout = 10 28 | TimeUnit timeoutUnit = TimeUnit.SECONDS 29 | 30 | JiraTriggerGlobalConfiguration jiraTriggerGlobalConfiguration 31 | 32 | private ExtendedJiraRestClient extendedJiraRestClient 33 | 34 | @Inject 35 | JrjcJiraClient(JiraTriggerGlobalConfiguration jiraTriggerGlobalConfiguration) { 36 | this.jiraTriggerGlobalConfiguration = jiraTriggerGlobalConfiguration 37 | } 38 | 39 | protected URI getServerUri() { 40 | jiraTriggerGlobalConfiguration.validateConfiguration() 41 | jiraTriggerGlobalConfiguration.jiraRootUrl.toURI() 42 | } 43 | 44 | protected DisposableHttpClient getHttpClient() { 45 | String username = jiraTriggerGlobalConfiguration.jiraUsername 46 | String password = jiraTriggerGlobalConfiguration.jiraPassword.plainText 47 | new AsynchronousHttpClientFactory() 48 | .createClient(serverUri, 49 | new BasicHttpAuthenticationHandler(username, password)) 50 | } 51 | 52 | ExtendedJiraRestClient getJiraRestClient() { 53 | if (extendedJiraRestClient == null) { 54 | this.extendedJiraRestClient = new ExtendedJiraRestClient(serverUri, httpClient) 55 | } 56 | extendedJiraRestClient 57 | } 58 | 59 | @Override 60 | boolean validateIssueKey(String issueKey, String jqlFilter) { 61 | String jql = "key=$issueKey and ($jqlFilter)" 62 | SearchResult searchResult = jiraRestClient.searchClient.searchJql(jql).get(timeout, timeoutUnit) 63 | searchResult.total != 0 64 | } 65 | 66 | @Override 67 | void addComment(String issueKey, String comment) { 68 | Issue issue = jiraRestClient.issueClient.getIssue(issueKey).get(timeout, timeoutUnit) 69 | jiraRestClient.issueClient.addComment(issue.commentsUri, Comment.valueOf(comment)).get(timeout, timeoutUnit) 70 | } 71 | 72 | @Extension 73 | static class ResourceCleaner extends SaveableListener { 74 | 75 | @Inject 76 | JrjcJiraClient jiraClient 77 | 78 | @SuppressWarnings('Instanceof') 79 | @Override 80 | void onChange(Saveable o, XmlFile file) { 81 | if (o instanceof JiraTriggerGlobalConfiguration) { 82 | if (jiraClient.extendedJiraRestClient != null) { 83 | jiraClient.extendedJiraRestClient.close() 84 | jiraClient.extendedJiraRestClient = null 85 | } 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/jira/Webhook.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.jira 2 | 3 | /** 4 | * @author ceilfors 5 | */ 6 | class Webhook { 7 | 8 | URI selfUri 9 | String url 10 | String name 11 | } 12 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/jira/WebhookInput.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.jira 2 | 3 | /** 4 | * @author ceilfors 5 | */ 6 | class WebhookInput { 7 | 8 | String url 9 | String name 10 | List events 11 | String jqlFilter 12 | } 13 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/jira/WebhookInputJsonGenerator.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.jira 2 | 3 | import com.atlassian.jira.rest.client.internal.json.gen.JsonGenerator 4 | import org.codehaus.jettison.json.JSONException 5 | import org.codehaus.jettison.json.JSONObject 6 | 7 | /** 8 | * @author ceilfors 9 | */ 10 | class WebhookInputJsonGenerator implements JsonGenerator { 11 | 12 | @Override 13 | JSONObject generate(WebhookInput webhook) throws JSONException { 14 | new JSONObject() 15 | .put('name', webhook.name) 16 | .put('url', webhook.url) 17 | .put('jqlFilter', webhook.jqlFilter) 18 | .put('events', webhook.events) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/jira/WebhookJsonParser.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.jira 2 | 3 | import com.atlassian.jira.rest.client.internal.json.JsonObjectParser 4 | import com.atlassian.jira.rest.client.internal.json.JsonParseUtil 5 | import org.codehaus.jettison.json.JSONException 6 | import org.codehaus.jettison.json.JSONObject 7 | 8 | /** 9 | * @author ceilfors 10 | */ 11 | class WebhookJsonParser implements JsonObjectParser { 12 | 13 | @Override 14 | Webhook parse(JSONObject json) throws JSONException { 15 | new Webhook( 16 | selfUri: JsonParseUtil.getSelfUri(json), 17 | name: json.getString('name'), 18 | url: json.getString('url'), 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/jira/WebhookRestClient.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.jira 2 | 3 | import io.atlassian.util.concurrent.Promise 4 | 5 | /** 6 | * @author ceilfors 7 | */ 8 | interface WebhookRestClient { 9 | 10 | Promise registerWebhook(WebhookInput webhook) 11 | 12 | Promise unregisterWebhook(URI webhookUri) 13 | 14 | Promise> getWebhooks() 15 | } 16 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/jira/WebhooksJsonParser.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.jira 2 | import com.atlassian.jira.rest.client.internal.json.JsonArrayParser 3 | import com.atlassian.jira.rest.client.internal.json.JsonParseUtil 4 | import org.codehaus.jettison.json.JSONArray 5 | import org.codehaus.jettison.json.JSONException 6 | /** 7 | * @author ceilfors 8 | */ 9 | class WebhooksJsonParser implements JsonArrayParser> { 10 | 11 | @Override 12 | Collection parse(JSONArray json) throws JSONException { 13 | JsonParseUtil.parseJsonArray(json, new WebhookJsonParser()) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/parameter/CustomFieldParameterMapping.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.parameter 2 | 3 | import groovy.transform.EqualsAndHashCode 4 | import hudson.Extension 5 | import org.kohsuke.stapler.DataBoundConstructor 6 | 7 | /** 8 | * @author ceilfors 9 | */ 10 | @EqualsAndHashCode(callSuper = true) 11 | class CustomFieldParameterMapping extends ParameterMapping { 12 | 13 | final String customFieldId 14 | 15 | @DataBoundConstructor 16 | CustomFieldParameterMapping(String jenkinsParameter, String customFieldId) { 17 | super(jenkinsParameter) 18 | this.customFieldId = customFieldId.trim() 19 | } 20 | 21 | @Override 22 | ParameterResolver getParameterResolver() { 23 | new CustomFieldParameterResolver(this) 24 | } 25 | 26 | @SuppressWarnings('UnnecessaryQualifiedReference') // Can't remove qualifier, IntelliJ bug? 27 | @Extension 28 | static class CustomFieldParameterMappingDescriptor extends ParameterMapping.ParameterMappingDescriptor { 29 | 30 | public static final String DISPLAY_NAME = 'Custom Field' 31 | 32 | @Override 33 | String getDisplayName() { 34 | DISPLAY_NAME 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/parameter/CustomFieldParameterResolver.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.parameter 2 | 3 | import com.atlassian.jira.rest.client.api.domain.Issue 4 | import com.atlassian.jira.rest.client.api.domain.IssueField 5 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraTriggerException 6 | import org.codehaus.jettison.json.JSONArray 7 | import org.codehaus.jettison.json.JSONObject 8 | 9 | /** 10 | * @author ceilfors 11 | */ 12 | @SuppressWarnings('Instanceof') 13 | class CustomFieldParameterResolver implements ParameterResolver { 14 | 15 | CustomFieldParameterMapping customFieldParameterMapping 16 | 17 | CustomFieldParameterResolver(CustomFieldParameterMapping customFieldParameterMapping) { 18 | this.customFieldParameterMapping = customFieldParameterMapping 19 | } 20 | 21 | String resolve(Issue issue) { 22 | String customFieldId = "customfield_${customFieldParameterMapping.customFieldId}" 23 | IssueField field = issue.fields.toList().find { f -> f.id == customFieldId } 24 | if (field) { 25 | extractValue(field) 26 | } else { 27 | throw new JiraTriggerException(ParameterErrorCode.FAILED_TO_RESOLVE) 28 | .add('customFieldId', customFieldId) 29 | } 30 | } 31 | 32 | private static String extractValue(IssueField field) { 33 | Object fieldValue = field.value 34 | if (fieldValue instanceof JSONArray) { 35 | return toList(fieldValue).collect { extractSingleValue(it) }.join(', ') 36 | } 37 | extractSingleValue(fieldValue) 38 | } 39 | 40 | private static List toList(JSONArray jsonArray) { 41 | (0..jsonArray.length() - 1).collect { i -> jsonArray.get(i) } 42 | } 43 | 44 | @SuppressWarnings('DuplicateStringLiteral') // Clearer with String literals 45 | private static String extractSingleValue(Object singleValue) { 46 | if (singleValue == null) { 47 | return null 48 | } else if (singleValue instanceof String) { 49 | return singleValue 50 | } else if (singleValue instanceof Number) { 51 | return String.valueOf(singleValue) 52 | } else if (singleValue instanceof JSONObject) { 53 | JSONObject object = singleValue 54 | if (object.has('value')) { 55 | String value = object.getString('value') 56 | if (object.has('child')) { 57 | value += " - ${object.getJSONObject('child').getString('value')}" 58 | } 59 | return value 60 | } else if (object.has('name')) { 61 | return object.getString('name') 62 | } 63 | } 64 | 65 | throw new JiraTriggerException(ParameterErrorCode.FAILED_TO_RESOLVE) 66 | .add('customFieldValue', singleValue.dump()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/parameter/DefaultParametersAction.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.parameter 2 | 3 | import hudson.model.Job 4 | import hudson.model.ParameterValue 5 | import hudson.model.ParametersAction 6 | import hudson.model.ParametersDefinitionProperty 7 | 8 | /** 9 | * DefaultParametersAction is required because we are using ParametersAction 10 | * in ParametersMappingAction. The default values of a parameterised job is only 11 | * automatically populated by ParameterizedJobMixIn.scheduleBuild2 when ParametersAction 12 | * absent from the scheduled actions. The proper solution is to use EnvironmentContributingAction 13 | * instead of ParametersMappingAction, but unfortunately this solution is not viable due to JENKINS-46482. 14 | * 15 | * Generating default values by ourselves (instead of relying on #scheduleBuild2) seems to be a common solution in 16 | * the community. Example: https://github.com/jenkinsci/ghprb-plugin/blob/c15d5106e78f7f028c6305dccf01b77cc9a724b3/ 17 | * src/main/java/org/jenkinsci/plugins/ghprb/GhprbTrigger.java#L386 18 | * 19 | * @author ceilfors 20 | */ 21 | class DefaultParametersAction extends ParametersAction { 22 | 23 | DefaultParametersAction(Job job) { 24 | super(getDefaultParameters(job)) 25 | } 26 | 27 | private static List getDefaultParameters(Job job) { 28 | ParametersDefinitionProperty pdp = job.getProperty(ParametersDefinitionProperty) 29 | pdp != null ? pdp.parameterDefinitions*.defaultParameterValue : [] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/parameter/IssueAttributePathParameterMapping.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.parameter 2 | 3 | import groovy.transform.EqualsAndHashCode 4 | import hudson.Extension 5 | import org.kohsuke.stapler.DataBoundConstructor 6 | /** 7 | * @author ceilfors 8 | */ 9 | @EqualsAndHashCode(callSuper = true) 10 | class IssueAttributePathParameterMapping extends ParameterMapping { 11 | 12 | final String issueAttributePath 13 | 14 | @DataBoundConstructor 15 | IssueAttributePathParameterMapping(String jenkinsParameter, String issueAttributePath) { 16 | super(jenkinsParameter) 17 | this.issueAttributePath = issueAttributePath.trim() 18 | } 19 | 20 | @Override 21 | ParameterResolver getParameterResolver() { 22 | new IssueAttributePathParameterResolver(this) 23 | } 24 | 25 | @SuppressWarnings('UnnecessaryQualifiedReference') // Can't remove qualifier, IntelliJ bug? 26 | @Extension 27 | static class IssueAttributePathParameterMappingDescriptor extends ParameterMapping.ParameterMappingDescriptor { 28 | 29 | public static final String DISPLAY_NAME = 'Issue Attribute Path' 30 | 31 | @Override 32 | String getDisplayName() { 33 | DISPLAY_NAME 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/parameter/IssueAttributePathParameterResolver.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.parameter 2 | 3 | import com.atlassian.jira.rest.client.api.domain.Issue 4 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraTriggerException 5 | 6 | /** 7 | * @author ceilfors 8 | */ 9 | class IssueAttributePathParameterResolver implements ParameterResolver { 10 | 11 | IssueAttributePathParameterMapping issueAttributePathParameterMapping 12 | 13 | IssueAttributePathParameterResolver(IssueAttributePathParameterMapping issueAttributePathParameterMapping) { 14 | this.issueAttributePathParameterMapping = issueAttributePathParameterMapping 15 | } 16 | 17 | String resolve(Issue issue) { 18 | resolveProperty(issue.properties, issueAttributePathParameterMapping.issueAttributePath) 19 | } 20 | 21 | /** 22 | * Resolves nested property from a Map. 23 | * 24 | * @param map the map which property to be resolved 25 | * @param property 26 | * @return the resolved property, null otherwise 27 | */ 28 | static String resolveProperty(Map map, String property) { 29 | try { 30 | if (!property.contains('.') && !map.containsKey(property)) { 31 | // If property is not nested, Eval.x returns null instead of throwing NPE 32 | throw new JiraTriggerException(ParameterErrorCode.FAILED_TO_RESOLVE) 33 | } 34 | Eval.x(map, 'x.' + property) 35 | } catch (MissingPropertyException e) { 36 | throw new JiraTriggerException(ParameterErrorCode.FAILED_TO_RESOLVE, e) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/parameter/ParameterErrorCode.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.parameter 2 | 3 | import com.ceilfors.jenkins.plugins.jiratrigger.ErrorCode 4 | 5 | /** 6 | * @author ceilfors 7 | */ 8 | enum ParameterErrorCode implements ErrorCode { 9 | 10 | FAILED_TO_RESOLVE('1') 11 | 12 | final String code 13 | 14 | ParameterErrorCode(String code) { 15 | this.code = code 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/parameter/ParameterMapping.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.parameter 2 | 3 | import groovy.transform.EqualsAndHashCode 4 | import hudson.model.AbstractDescribableImpl 5 | import hudson.model.Descriptor 6 | /** 7 | * @author ceilfors 8 | */ 9 | @EqualsAndHashCode 10 | abstract class ParameterMapping extends AbstractDescribableImpl { 11 | 12 | final String jenkinsParameter 13 | 14 | protected ParameterMapping(String jenkinsParameter) { 15 | this.jenkinsParameter = jenkinsParameter.trim() 16 | } 17 | 18 | abstract ParameterResolver getParameterResolver() 19 | 20 | static abstract class ParameterMappingDescriptor extends Descriptor { 21 | 22 | @Override 23 | abstract String getDisplayName() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/parameter/ParameterResolver.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.parameter 2 | 3 | import com.atlassian.jira.rest.client.api.domain.Issue 4 | /** 5 | * Responsible for providing a parameter value for Jenkins by digesting JIRA information. 6 | * 7 | * @author ceilfors 8 | */ 9 | interface ParameterResolver { 10 | 11 | String resolve(Issue issue) 12 | } 13 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/webhook/BaseWebhookEvent.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.webhook 2 | 3 | /** 4 | * @author ceilfors 5 | */ 6 | class BaseWebhookEvent { 7 | 8 | final long timestamp 9 | final String webhookEventType 10 | String userId 11 | String userKey 12 | 13 | protected BaseWebhookEvent(long timestamp, String webhookEventType) { 14 | this.timestamp = timestamp 15 | this.webhookEventType = webhookEventType 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/webhook/JiraWebhook.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.webhook 2 | 3 | import groovy.json.JsonOutput 4 | import groovy.json.JsonSlurper 5 | import groovy.util.logging.Log 6 | import hudson.Extension 7 | import hudson.model.UnprotectedRootAction 8 | import org.codehaus.jettison.json.JSONObject 9 | import org.kohsuke.stapler.StaplerRequest 10 | import org.kohsuke.stapler.interceptor.RequirePOST 11 | 12 | import javax.inject.Inject 13 | import java.util.logging.Level 14 | /** 15 | * The HTTP endpoint that receives JIRA Webhook. 16 | * 17 | * @author ceilfors 18 | */ 19 | @Log 20 | @Extension 21 | class JiraWebhook implements UnprotectedRootAction { 22 | 23 | public static final URL_NAME = 'jira-trigger-webhook-receiver' 24 | public static final ISSUE_UPDATED_WEBHOOK_EVENT = 'jira:issue_updated' 25 | public static final COMMENT_CREATED_WEBHOOK_EVENT = 'comment_created' 26 | private JiraWebhookListener jiraWebhookListener 27 | 28 | @Inject 29 | void setJiraWebhookListener(JiraWebhookListener jiraWebhookListener) { 30 | this.jiraWebhookListener = jiraWebhookListener 31 | } 32 | 33 | @Override 34 | String getIconFileName() { 35 | null 36 | } 37 | 38 | @Override 39 | String getDisplayName() { 40 | 'JIRA Trigger' 41 | } 42 | 43 | @Override 44 | String getUrlName() { 45 | URL_NAME 46 | } 47 | 48 | @SuppressWarnings('GroovyUnusedDeclaration') 49 | @RequirePOST 50 | void doIndex(StaplerRequest request) { 51 | processEvent(request, getRequestBody(request)) 52 | } 53 | 54 | void processEvent(StaplerRequest request, String webhookEvent) { 55 | logJson(webhookEvent) 56 | Map webhookEventMap = new JsonSlurper().parseText(webhookEvent) as Map 57 | RawWebhookEvent rawWebhookEvent = new RawWebhookEvent(request, webhookEventMap) 58 | JSONObject webhookJsonObject = new JSONObject(webhookEvent) 59 | boolean validEvent = false 60 | 61 | if (rawWebhookEvent.isChangelogEvent()) { 62 | log.fine("Received Webhook callback from changelog. Event type: ${rawWebhookEvent.eventType}") 63 | WebhookChangelogEvent changelogEvent = new WebhookChangelogEventJsonParser().parse(webhookJsonObject) 64 | changelogEvent.userId = rawWebhookEvent.userId 65 | changelogEvent.userKey = rawWebhookEvent.userKey 66 | jiraWebhookListener.changelogCreated(changelogEvent) 67 | validEvent = true 68 | } 69 | if (rawWebhookEvent.isCommentEvent()) { 70 | log.fine("Received Webhook callback from comment. Event type: ${rawWebhookEvent.eventType}") 71 | WebhookCommentEvent commentEvent = new WebhookCommentEventJsonParser().parse(webhookJsonObject) 72 | commentEvent.userId = rawWebhookEvent.userId 73 | commentEvent.userKey = rawWebhookEvent.userKey 74 | jiraWebhookListener.commentCreated(commentEvent) 75 | validEvent = true 76 | } 77 | if (!validEvent) { 78 | log.warning('Received Webhook callback with an invalid event type or a body without comment/changelog. ' + 79 | "Event type: ${rawWebhookEvent.eventType}. Event body contains: ${webhookEventMap.keySet()}.") 80 | } 81 | } 82 | 83 | private void logJson(String webhookEvent) { 84 | if (log.isLoggable(Level.FINEST)) { 85 | log.finest('Webhook event body:') 86 | log.finest(JsonOutput.prettyPrint(webhookEvent)) 87 | } 88 | } 89 | 90 | private String getRequestBody(StaplerRequest req) { 91 | req.inputStream.text 92 | } 93 | 94 | private static class RawWebhookEvent { 95 | 96 | final StaplerRequest request 97 | final Map webhookEventMap 98 | 99 | RawWebhookEvent(StaplerRequest request, Map webhookEventMap) { 100 | this.request = request 101 | this.webhookEventMap = webhookEventMap 102 | } 103 | 104 | boolean isChangelogEvent() { 105 | eventType == ISSUE_UPDATED_WEBHOOK_EVENT && webhookEventMap['changelog'] 106 | } 107 | 108 | boolean isCommentEvent() { 109 | (eventType == ISSUE_UPDATED_WEBHOOK_EVENT 110 | || eventType == COMMENT_CREATED_WEBHOOK_EVENT) && webhookEventMap['comment'] 111 | } 112 | 113 | String getUserId() { 114 | request.getParameter('user_id') 115 | } 116 | 117 | String getUserKey() { 118 | request.getParameter('user_key') 119 | } 120 | 121 | String getEventType() { 122 | webhookEventMap['webhookEvent'] 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/webhook/JiraWebhookCrumbExclusion.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.webhook 2 | 3 | import hudson.Extension 4 | import hudson.security.csrf.CrumbExclusion 5 | 6 | import javax.servlet.FilterChain 7 | import javax.servlet.ServletException 8 | import javax.servlet.http.HttpServletRequest 9 | import javax.servlet.http.HttpServletResponse 10 | 11 | /** 12 | * @author ceilfors 13 | */ 14 | @Extension 15 | class JiraWebhookCrumbExclusion extends CrumbExclusion { 16 | 17 | @Override 18 | boolean process(HttpServletRequest request, HttpServletResponse response, FilterChain chain) 19 | throws IOException, ServletException { 20 | String pathInfo = request.pathInfo 21 | if (pathInfo != null && (pathInfo == exclusionPath || pathInfo == exclusionPath + '/')) { 22 | chain.doFilter(request, response) 23 | return true 24 | } 25 | false 26 | } 27 | 28 | String getExclusionPath() { 29 | "/$JiraWebhook.URL_NAME" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/webhook/JiraWebhookListener.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.webhook 2 | 3 | /** 4 | * @author ceilfors 5 | */ 6 | interface JiraWebhookListener { 7 | 8 | void commentCreated(WebhookCommentEvent commentEvent) 9 | 10 | void changelogCreated(WebhookChangelogEvent changelogEvent) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/webhook/WebhookChangelogEvent.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.webhook 2 | import com.atlassian.jira.rest.client.api.domain.ChangelogGroup 3 | import com.atlassian.jira.rest.client.api.domain.Issue 4 | 5 | /** 6 | * @author ceilfors 7 | */ 8 | class WebhookChangelogEvent extends BaseWebhookEvent { 9 | 10 | final ChangelogGroup changelog 11 | final Issue issue 12 | 13 | WebhookChangelogEvent(long timestamp, String webhookEventType, Issue issue, ChangelogGroup changelog) { 14 | super(timestamp, webhookEventType) 15 | this.issue = issue 16 | this.changelog = changelog 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/webhook/WebhookChangelogEventJsonParser.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.webhook 2 | 3 | import com.atlassian.jira.rest.client.api.domain.ChangelogGroup 4 | import com.atlassian.jira.rest.client.api.domain.ChangelogItem 5 | import com.atlassian.jira.rest.client.internal.json.ChangelogItemJsonParser 6 | import com.atlassian.jira.rest.client.internal.json.IssueJsonParser 7 | import com.atlassian.jira.rest.client.internal.json.JsonObjectParser 8 | import com.atlassian.jira.rest.client.internal.json.JsonParseUtil 9 | import org.codehaus.jettison.json.JSONException 10 | import org.codehaus.jettison.json.JSONObject 11 | 12 | import static com.ceilfors.jenkins.plugins.jiratrigger.webhook.WebhookJsonParserUtils.satisfyRequiredKeys 13 | 14 | /** 15 | * @author ceilfors 16 | */ 17 | class WebhookChangelogEventJsonParser implements JsonObjectParser { 18 | 19 | /** 20 | * Not using ChangelogJsonParser because it is expecting "created" field which is not 21 | * being supplied from webhook event. 22 | */ 23 | private final ChangelogItemJsonParser changelogItemJsonParser = new ChangelogItemJsonParser() 24 | private final IssueJsonParser issueJsonParser = new IssueJsonParser(new JSONObject([:]), new JSONObject([:])) 25 | 26 | @Override 27 | WebhookChangelogEvent parse(JSONObject webhookEvent) throws JSONException { 28 | satisfyRequiredKeys(webhookEvent) 29 | 30 | Collection items = JsonParseUtil.parseJsonArray( 31 | webhookEvent.getJSONObject('changelog').getJSONArray('items'), changelogItemJsonParser) 32 | new WebhookChangelogEvent( 33 | webhookEvent.getLong('timestamp'), 34 | webhookEvent.getString('webhookEvent'), 35 | issueJsonParser.parse(webhookEvent.getJSONObject('issue')), 36 | new ChangelogGroup(null, null, items) 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/webhook/WebhookCommentEvent.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.webhook 2 | 3 | import com.atlassian.jira.rest.client.api.domain.Comment 4 | import com.atlassian.jira.rest.client.api.domain.Issue 5 | 6 | /** 7 | * @author ceilfors 8 | */ 9 | class WebhookCommentEvent extends BaseWebhookEvent { 10 | 11 | final Comment comment 12 | final Issue issue 13 | 14 | WebhookCommentEvent(long timestamp, String webhookEventType, Issue issue, Comment comment) { 15 | super(timestamp, webhookEventType) 16 | this.comment = comment 17 | this.issue = issue 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/webhook/WebhookCommentEventJsonParser.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.webhook 2 | 3 | import com.atlassian.jira.rest.client.internal.json.CommentJsonParser 4 | import com.atlassian.jira.rest.client.internal.json.IssueJsonParser 5 | import com.atlassian.jira.rest.client.internal.json.JsonObjectParser 6 | import org.codehaus.jettison.json.JSONException 7 | import org.codehaus.jettison.json.JSONObject 8 | 9 | import static com.ceilfors.jenkins.plugins.jiratrigger.webhook.WebhookJsonParserUtils.putIfAbsent 10 | import static com.ceilfors.jenkins.plugins.jiratrigger.webhook.WebhookJsonParserUtils.satisfyRequiredKeys 11 | 12 | /** 13 | * @author ceilfors 14 | */ 15 | class WebhookCommentEventJsonParser implements JsonObjectParser { 16 | 17 | private static final DATE_FIELD_NOT_EXIST = '1980-01-01T00:00:00.000+0000' 18 | private static final ISSUE_KEY = 'issue' 19 | 20 | private final IssueJsonParser issueJsonParser = new IssueJsonParser(new JSONObject([:]), new JSONObject([:])) 21 | 22 | /** 23 | * Fills details needed by JRC JSON Parser that are missing in JIRA Cloud Webhook events. 24 | */ 25 | private static void satisfyCloudRequiredKeys(JSONObject webhookEvent) { 26 | JSONObject fields = webhookEvent.getJSONObject(ISSUE_KEY).getJSONObject('fields') 27 | putIfAbsent(fields, 'created', DATE_FIELD_NOT_EXIST) 28 | putIfAbsent(fields, 'updated', DATE_FIELD_NOT_EXIST) 29 | } 30 | 31 | @Override 32 | WebhookCommentEvent parse(JSONObject webhookEvent) throws JSONException { 33 | satisfyRequiredKeys(webhookEvent) 34 | satisfyCloudRequiredKeys(webhookEvent) 35 | 36 | new WebhookCommentEvent( 37 | webhookEvent.getLong('timestamp'), 38 | webhookEvent.getString('webhookEvent'), 39 | issueJsonParser.parse(webhookEvent.getJSONObject(ISSUE_KEY)), 40 | new CommentJsonParser().parse(webhookEvent.getJSONObject('comment')) 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/groovy/com/ceilfors/jenkins/plugins/jiratrigger/webhook/WebhookJsonParserUtils.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.webhook 2 | 3 | import org.codehaus.jettison.json.JSONObject 4 | 5 | /** 6 | * @author ceilfors 7 | */ 8 | class WebhookJsonParserUtils { 9 | 10 | /** 11 | * Fills details needed by JRC JSON Parser that are missing in Webhook events. 12 | */ 13 | static void satisfyRequiredKeys(JSONObject webhookEvent) { 14 | JSONObject issue = webhookEvent.getJSONObject('issue') 15 | putIfAbsent(issue, 'expand', '') 16 | } 17 | 18 | static void putIfAbsent(JSONObject jsonObject, String key, Object value) { 19 | if (!jsonObject.opt(key)) { 20 | jsonObject.put(key, value) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/JiraChangelogTrigger/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/JiraChangelogTrigger/help.html: -------------------------------------------------------------------------------- 1 |
2 | Set up a trigger that listens to JIRA issue changes. See plugin documentation setup section before using this trigger. 3 |
-------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/JiraCommentTrigger/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/JiraCommentTrigger/help-commentPattern.html: -------------------------------------------------------------------------------- 1 |
2 | Triggers build only when the comment added to JIRA matches this pattern. 3 |
-------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/JiraCommentTrigger/help.html: -------------------------------------------------------------------------------- 1 |
2 | Set up a trigger so that when a comment is added to JIRA. See plugin documentation setup section before using this trigger. 3 |
-------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/JiraTrigger/help-jqlFilter.html: -------------------------------------------------------------------------------- 1 |
2 | A build will only be triggered if the updated issues matches this JQL filter. The updated issues in this particular 3 | case are the issues where the comments have been added in. 4 |
-------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/JiraTrigger/help-parameterMappings.html: -------------------------------------------------------------------------------- 1 |
2 | Maps JIRA issue attributes as Jenkins parameters. Can be used when this job is parameterized. 3 |
-------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/JiraTriggerGlobalConfiguration/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/JiraTriggerGlobalConfiguration/help-jiraCommentReply.html: -------------------------------------------------------------------------------- 1 |
2 | Jenkins will reply back to JIRA for build updates e.g. scheduled build, etc. 3 |
-------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/changelog/ChangelogMatcher/help-comparingNewValue.html: -------------------------------------------------------------------------------- 1 |
2 | Tick this if you want this matcher to compare the new value of the updated field. New value changes will be ignored 3 | otherwise. 4 |
-------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/changelog/ChangelogMatcher/help-comparingOldValue.html: -------------------------------------------------------------------------------- 1 |
2 | Tick this if you want this matcher to compare the old value of the updated field. Old value changes 3 | will be ignored otherwise. 4 |
-------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/changelog/ChangelogMatcher/help-newValue.html: -------------------------------------------------------------------------------- 1 |
2 | The new value of the updated field. 3 |
-------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/changelog/ChangelogMatcher/help-oldValue.html: -------------------------------------------------------------------------------- 1 |
2 | The old value of the updated field, meaning the value before it is updated. 3 |
-------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/changelog/CustomFieldChangelogMatcher/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/changelog/CustomFieldChangelogMatcher/help-field.html: -------------------------------------------------------------------------------- 1 |
2 | The custom field name that has been changed during the issue update. This refers to custom field name, so it 3 | should not be in customfield_* format. This field is case sensitive. 4 |
-------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/changelog/CustomFieldChangelogMatcher/help.html: -------------------------------------------------------------------------------- 1 |
2 | Matches custom field changes in the updated JIRA issue. 3 |
-------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/changelog/JiraFieldChangelogMatcher/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/changelog/JiraFieldChangelogMatcher/help-field.html: -------------------------------------------------------------------------------- 1 |
2 | The JIRA Field ID that has been changed during the issue update. 3 | Select one from the combo box below or type the ID if it's not available in the combo box. 4 |
-------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/changelog/JiraFieldChangelogMatcher/help.html: -------------------------------------------------------------------------------- 1 |
2 | Matches JIRA built-in field changes in the updated JIRA issue. 3 |
-------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/parameter/CustomFieldParameterMapping/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/parameter/CustomFieldParameterMapping/help-customFieldId.html: -------------------------------------------------------------------------------- 1 |
2 | The custom field id to be mapped (not the name). This field must contain numbers only e.g. 10000, 10020, etc. 3 |
-------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/parameter/CustomFieldParameterMapping/help-jenkinsParameter.html: -------------------------------------------------------------------------------- 1 |
2 | The Jenkins parameter name that will be filled in with the JIRA attribute value resolves from the attribute path. 3 |
-------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/parameter/IssueAttributePathParameterMapping/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/parameter/IssueAttributePathParameterMapping/help-jenkinsParameter.html: -------------------------------------------------------------------------------- 1 |
2 | The Jenkins parameter name that will be filled in with the JIRA attribute value resolves from the attribute path. 3 |
-------------------------------------------------------------------------------- /src/main/resources/com/ceilfors/jenkins/plugins/jiratrigger/parameter/IssueAttributePathParameterMapping/help.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | JIRA attribute path resolves an attribute value from the JIRA issue that has triggered the job. The attribute 5 | value is resolved from 6 | JRJC Issue Object 7 | To understand how the attribute path works, refer to the example below. 8 |

9 | 10 |

11 | Example attribute path 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 |
Attribute PathAttribute Value
id11120
descriptiondescription body
summarysummary content
status.nameTo Do
timeTracking.originalEstimateMinutes5
43 | 44 |

45 | Example Issue JSON that will be parsed to become JRJC Issue Object 46 |

47 |
48 | {
49 |   "expand": "renderedFields,names,schema,transitions,operations,editmeta,changelog,versionedRepresentations",
50 |   "id": "11120",
51 |   "self": "http://localhost:2990/jira/rest/api/2/issue/11120",
52 |   "key": "TEST-136",
53 |   "fields": {
54 |     ...
55 |     "description": "description body",
56 |     "summary": "summary content",
57 |     ...
58 |     "status": {
59 |       "name": "To Do",
60 |         ...
61 |     },
62 |     "timetracking": {
63 |       "originalEstimate": "5m",
64 |       "remainingEstimate": "10m",
65 |       "originalEstimateSeconds": 300,
66 |       "remainingEstimateSeconds": 600
67 |     }
68 |   }
69 | }
70 |     
71 |
-------------------------------------------------------------------------------- /src/main/resources/xsd/maven-jellydoc-plugin/source.txt: -------------------------------------------------------------------------------- 1 | https://github.com/kohsuke/maven-jellydoc-plugin/tree/master/maven-jellydoc-plugin/schemas -------------------------------------------------------------------------------- /src/main/resources/xsd/maven-site-jenkins-core/source.txt: -------------------------------------------------------------------------------- 1 | https://jenkins-ci.org/maven-site/jenkins-core/jelly-taglib-ref.html -------------------------------------------------------------------------------- /src/main/resources/xsd/stapler/source.txt: -------------------------------------------------------------------------------- 1 | http://stapler.kohsuke.org/taglib.xsd -------------------------------------------------------------------------------- /src/test/groovy/com/ceilfors/jenkins/plugins/jiratrigger/JiraCommentTriggerTest.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger 2 | 3 | import com.atlassian.jira.rest.client.api.domain.Comment 4 | import hudson.model.AbstractProject 5 | import hudson.model.Item 6 | import hudson.model.ItemGroup 7 | import hudson.model.Queue 8 | import org.junit.Rule 9 | import org.jvnet.hudson.test.JenkinsRule 10 | import spock.lang.Specification 11 | import spock.lang.Unroll 12 | 13 | import static com.ceilfors.jenkins.plugins.jiratrigger.TestUtils.createIssue 14 | 15 | /** 16 | * @author ceilfors 17 | */ 18 | @SuppressWarnings('GroovyAssignabilityCheck') 19 | class JiraCommentTriggerTest extends Specification { 20 | 21 | def project 22 | 23 | @Rule 24 | public JenkinsRule jenkinsRule = new JenkinsRule() 25 | 26 | private static interface TaskItem extends Queue.Task, Item { } 27 | 28 | def setup() { 29 | def projectParent = Mock(ItemGroup) 30 | projectParent.getFullName() >> '' 31 | project = Mock(AbstractProject) 32 | project.getOwnerTask() >> Mock(TaskItem) 33 | project.getParent() >> projectParent 34 | project.getName() >> 'project' 35 | project.isBuildable() >> true 36 | } 37 | 38 | @Unroll 39 | def 'Triggers build when comment body matches the comment pattern'(String commentBody, String commentPattern) { 40 | given: 41 | def comment = new Comment(null, commentBody, null, null, null, null, null, null) 42 | JiraCommentTrigger trigger = new JiraCommentTrigger(commentPattern: commentPattern) 43 | trigger.job = project 44 | 45 | when: 46 | boolean result = trigger.run(createIssue('TEST-123'), comment) 47 | 48 | then: 49 | result 50 | 51 | where: 52 | commentBody | commentPattern 53 | 'please build me' | 'please build me' 54 | 'please build me' | '(?i)please build me' 55 | 'PLEASE BUILD ME' | '(?i)please build me' 56 | 'start\n\nplease build me\n\nend' | '(?s).*please build me.*' 57 | } 58 | 59 | @Unroll 60 | def 'Does not trigger build when comment body matches the comment pattern'(String commentBody, 61 | String commentPattern) { 62 | given: 63 | def comment = new Comment(null, commentBody, null, null, null, null, null, null) 64 | JiraCommentTrigger trigger = new JiraCommentTrigger(commentPattern: commentPattern) 65 | trigger.job = project 66 | 67 | when: 68 | boolean result = trigger.run(createIssue('TEST-123'), comment) 69 | 70 | then: 71 | !result 72 | 73 | where: 74 | commentBody | commentPattern 75 | 'please do not build me' | 'please build me' 76 | ' please build me' | 'please build me' 77 | 'please build me\n' | 'please build me' 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/groovy/com/ceilfors/jenkins/plugins/jiratrigger/JiraTriggerTest.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger 2 | 3 | import hudson.model.FreeStyleProject 4 | import org.junit.Rule 5 | import org.jvnet.hudson.test.Issue 6 | import org.jvnet.hudson.test.JenkinsRule 7 | import spock.lang.Specification 8 | 9 | import static com.ceilfors.jenkins.plugins.jiratrigger.JiraCommentTrigger.JiraCommentTriggerDescriptor 10 | 11 | /** 12 | * @author ceilfors 13 | */ 14 | class JiraTriggerTest extends Specification { 15 | 16 | def project 17 | 18 | @Rule 19 | public JenkinsRule jenkinsRule = new JenkinsRule() 20 | 21 | def 'Should add/remove trigger when a trigger is created/deleted'() { 22 | setup: 23 | JiraCommentTriggerDescriptor descriptor = jenkinsRule.instance.getDescriptor(JiraCommentTrigger) 24 | 25 | when: 26 | FreeStyleProject job = jenkinsRule.createFreeStyleProject('job') 27 | def trigger = new JiraCommentTrigger() 28 | trigger.start(job, true) 29 | 30 | then: 31 | descriptor.allTriggers().size() == 1 32 | 33 | when: 34 | trigger.stop() 35 | 36 | then: 37 | descriptor.allTriggers().size() == 0 38 | } 39 | 40 | def 'Should be able to delete trigger after a job is renamed'() { 41 | setup: 42 | JiraCommentTriggerDescriptor descriptor = jenkinsRule.instance.getDescriptor(JiraCommentTrigger) 43 | 44 | when: 45 | FreeStyleProject job = jenkinsRule.createFreeStyleProject('job') 46 | def trigger = new JiraCommentTrigger() 47 | trigger.start(job, true) 48 | job.renameTo('newjob') 49 | trigger.stop() 50 | 51 | then: 52 | descriptor.allTriggers().size() == 0 53 | } 54 | 55 | @Issue('JENKINS-43642') 56 | def 'Should be able to stop JiraTrigger when the trigger is not started yet'() { 57 | setup: 58 | JiraCommentTriggerDescriptor descriptor = jenkinsRule.instance.getDescriptor(JiraCommentTrigger) 59 | 60 | when: 61 | FreeStyleProject job = jenkinsRule.createFreeStyleProject('job') 62 | def trigger = new JiraCommentTrigger() 63 | trigger.stop() 64 | trigger.start(job, true) 65 | 66 | then: 67 | descriptor.allTriggers().size() == 1 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/groovy/com/ceilfors/jenkins/plugins/jiratrigger/TestUtils.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger 2 | 3 | import com.atlassian.jira.rest.client.api.domain.Comment 4 | import com.atlassian.jira.rest.client.api.domain.Issue 5 | 6 | /** 7 | * @author ceilfors 8 | */ 9 | class TestUtils { 10 | 11 | static Comment createComment(String body) { 12 | new Comment(null, body, null, null, null, null, null, null) 13 | } 14 | 15 | static Issue createIssue(String issueKey) { 16 | new Issue(null, null, issueKey, null, null, null, null, null, null, null, null, null, null, 17 | null, null, null, null, null, null, null, null, null, null, null, null, null, 18 | null, null, null, null, null, null) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/groovy/com/ceilfors/jenkins/plugins/jiratrigger/changelog/ChangelogMatcherTest.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.changelog 2 | 3 | import com.atlassian.jira.rest.client.api.domain.ChangelogGroup 4 | import com.atlassian.jira.rest.client.api.domain.ChangelogItem 5 | import com.atlassian.jira.rest.client.api.domain.FieldType 6 | import spock.lang.Specification 7 | 8 | /** 9 | * @author ceilfors 10 | */ 11 | class ChangelogMatcherTest extends Specification { 12 | 13 | private class BasicChangelogMatcher extends ChangelogMatcher { 14 | 15 | BasicChangelogMatcher(FieldType fieldType, String field, String newValue, String oldValue) { 16 | super(fieldType, field, newValue, oldValue) 17 | } 18 | } 19 | 20 | def 'Should compare field value'(String fieldId, String matcherField, boolean result) { 21 | given: 22 | ChangelogGroup changelogGroup = new ChangelogGroup(null, null, [ 23 | new ChangelogItem(FieldType.JIRA, fieldId, '', '', '', ''), 24 | ]) 25 | 26 | when: 27 | def matcher = new BasicChangelogMatcher(FieldType.JIRA, matcherField, '', '') 28 | matcher.comparingOldValue = false 29 | matcher.comparingNewValue = false 30 | 31 | then: 32 | matcher.matches(changelogGroup) == result 33 | 34 | where: 35 | fieldId | matcherField | result 36 | 'status' | 'status' | true 37 | 'status' | 'description' | false 38 | 'description' | 'status' | false 39 | 'description' | 'description' | true 40 | } 41 | 42 | def 'Should handle multiple changelog item'(List fieldIds, boolean result) { 43 | given: 44 | ChangelogGroup changelogGroup = new ChangelogGroup(null, null, fieldIds.collect { 45 | new ChangelogItem(FieldType.JIRA, it, null, null, null, null) 46 | }) 47 | 48 | when: 49 | def matcher = new BasicChangelogMatcher(FieldType.JIRA, 'status', '', '') 50 | matcher.comparingOldValue = false 51 | matcher.comparingNewValue = false 52 | 53 | then: 54 | matcher.matches(changelogGroup) == result 55 | 56 | where: 57 | fieldIds | result 58 | ['status', 'description'] | true 59 | ['description', 'status'] | true 60 | ['description', 'summary'] | false 61 | ['description', 'status', 'summary'] | true 62 | } 63 | 64 | def 'Should compare new value when comparingNewValue flag is on'( 65 | String newValue, String matcherNewValue, boolean result) { 66 | given: 67 | ChangelogGroup changelogGroup = new ChangelogGroup(null, null, [ 68 | new ChangelogItem(FieldType.JIRA, 'status', '', '', '', newValue), 69 | ]) 70 | 71 | when: 72 | def matcher = new BasicChangelogMatcher(FieldType.JIRA, 'status', matcherNewValue, '') 73 | matcher.comparingNewValue = true 74 | matcher.comparingOldValue = false 75 | 76 | then: 77 | matcher.matches(changelogGroup) == result 78 | 79 | where: 80 | newValue | matcherNewValue | result 81 | 'new value' | 'new value' | true 82 | 'new value' | 'NEW VALUE' | true 83 | '2' | '1' | false 84 | '@@' | '!!' | false 85 | '' | '' | true 86 | null | '' | true 87 | } 88 | 89 | def 'Should not compare new value when comparingNewValue flag is off'( 90 | String newValue, String matcherNewValue, boolean result) { 91 | given: 92 | ChangelogGroup changelogGroup = new ChangelogGroup(null, null, [ 93 | new ChangelogItem(FieldType.JIRA, 'status', '', '', '', newValue), 94 | ]) 95 | 96 | when: 97 | def matcher = new BasicChangelogMatcher(FieldType.JIRA, 'status', matcherNewValue, '') 98 | matcher.comparingNewValue = false 99 | matcher.comparingOldValue = false 100 | 101 | then: 102 | matcher.matches(changelogGroup) 103 | 104 | where: 105 | newValue | matcherNewValue | result 106 | '2' | '1' | true 107 | '@@' | '!!' | true 108 | } 109 | 110 | def 'Should compare old value when comparingOldValue flag is on'( 111 | String oldValue, String matcherOldValue, boolean result) { 112 | given: 113 | ChangelogGroup changelogGroup = new ChangelogGroup(null, null, [ 114 | new ChangelogItem(FieldType.JIRA, 'status', '', oldValue, '', ''), 115 | ]) 116 | 117 | when: 118 | def matcher = new BasicChangelogMatcher(FieldType.JIRA, 'status', '', matcherOldValue) 119 | matcher.comparingNewValue = false 120 | matcher.comparingOldValue = true 121 | 122 | then: 123 | matcher.matches(changelogGroup) == result 124 | 125 | where: 126 | oldValue | matcherOldValue | result 127 | 'old value' | 'old value' | true 128 | 'old value' | 'OLD VALUE' | true 129 | '2' | '1' | false 130 | '@@' | '!!' | false 131 | '' | '' | true 132 | null | '' | true 133 | } 134 | 135 | def 'Should not compare old value when comparingOldValue flag is off'( 136 | String oldValue, String matcherOldValue, boolean result) { 137 | given: 138 | ChangelogGroup changelogGroup = new ChangelogGroup(null, null, [ 139 | new ChangelogItem(FieldType.JIRA, 'status', '', oldValue, '', ''), 140 | ]) 141 | 142 | when: 143 | def matcher = new BasicChangelogMatcher(FieldType.JIRA, 'status', '', matcherOldValue) 144 | matcher.comparingNewValue = false 145 | matcher.comparingOldValue = false 146 | 147 | then: 148 | matcher.matches(changelogGroup) == result 149 | 150 | where: 151 | oldValue | matcherOldValue | result 152 | '2' | '1' | true 153 | '@@' | '!!' | true 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/test/groovy/com/ceilfors/jenkins/plugins/jiratrigger/jira/JiraUtilsTest.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.jira 2 | 3 | import com.atlassian.jira.rest.client.api.domain.Comment 4 | import spock.lang.Specification 5 | 6 | /** 7 | * @author ceilfors 8 | */ 9 | class JiraUtilsTest extends Specification { 10 | 11 | def 'Should be able to get issue id from Comment object'(String selfString, Long issueId) { 12 | given: 13 | Comment comment = new Comment(selfString.toURI(), null, null, null, null, null, null, null) 14 | 15 | when: 16 | def result = JiraUtils.getIssueIdFromComment(comment) 17 | 18 | then: 19 | result == issueId 20 | 21 | where: 22 | selfString | issueId 23 | 'http://localhost:2990/jira/rest/api/2/issue/10003/comment/10000' | 10003L 24 | 'http://localhost:2990/jira/rest/api/2/issue/1/comment/10000' | 1L 25 | 'http://localhost:2990/jira/rest/api/2/issue/12341234567/comment/10000' | 12341234567L 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/groovy/com/ceilfors/jenkins/plugins/jiratrigger/parameter/CustomFieldParameterResolverTest.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.parameter 2 | 3 | import com.atlassian.jira.rest.client.api.domain.Issue 4 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraTriggerException 5 | import com.ceilfors.jenkins.plugins.jiratrigger.webhook.WebhookChangelogEventJsonParser 6 | import org.codehaus.jettison.json.JSONObject 7 | import spock.lang.Specification 8 | import spock.lang.Unroll 9 | 10 | /** 11 | * @author ceilfors 12 | */ 13 | class CustomFieldParameterResolverTest extends Specification { 14 | 15 | private Issue createIssueFromFile(fileName) { 16 | def jsonObject = new JSONObject(this.class.getResource("${fileName}.json").text) 17 | new WebhookChangelogEventJsonParser().parse(jsonObject).issue 18 | } 19 | 20 | @Unroll 21 | def 'Should be able to resolve #customFieldType custom field when the value is not empty'( 22 | String customFieldType, String customFieldId, String attributeValue) { 23 | when: 24 | CustomFieldParameterMapping mapping = new CustomFieldParameterMapping('parameter', customFieldId) 25 | CustomFieldParameterResolver subject = new CustomFieldParameterResolver(mapping) 26 | String result = subject.resolve(createIssueFromFile('single_value_custom_field')) 27 | 28 | then: 29 | result == attributeValue 30 | 31 | where: 32 | customFieldType | customFieldId | attributeValue 33 | 'Checkboxes' | '10100' | 'checkbox option 1' 34 | 'Date Picker' | '10101' | '2017-08-17' 35 | 'Date Time Picker' | '10102' | '2017-08-17T01:00:00.000+0000' 36 | 'Labels' | '10103' | 'label' 37 | 'Number Field' | '10104' | '1.0' 38 | 'Radio Buttons' | '10105' | 'radio option 1' 39 | 'Select List (multiple choices)' | '10107' | 'singlelist option 1' 40 | 'Select List (cascading)' | '10106' | 'cascade option 1' 41 | 'Select List (single choice)' | '10200' | 'single choice option 1' 42 | 'Text Field (multi-line)' | '10000' | 'barclays\r\nhalifax\r\nsantander' 43 | 'Text Field (single line)' | '10108' | 'text' 44 | 'User Picker (single user)' | '10110' | 'testusernameedited' 45 | 'URL Field' | '10109' | 'https://url.com' 46 | } 47 | 48 | @Unroll 49 | def 'Should be able to resolve #customFieldType custom field when it contains multiple values'( 50 | String customFieldType, String customFieldId, String attributeValue) { 51 | when: 52 | CustomFieldParameterMapping mapping = new CustomFieldParameterMapping('parameter', customFieldId) 53 | CustomFieldParameterResolver subject = new CustomFieldParameterResolver(mapping) 54 | String result = subject.resolve(createIssueFromFile('multi_value_custom_field')) 55 | 56 | then: 57 | result == attributeValue 58 | 59 | where: 60 | customFieldType | customFieldId | attributeValue 61 | 'Checkboxes' | '10100' | 'checkbox option 1, checkbox option 2' 62 | 'Labels' | '10103' | 'label, labela, labelb' 63 | 'Select List (multiple choices)' | '10107' | 'singlelist option 1, singlelist option 2' 64 | 'Select List (cascading)' | '10106' | 'cascade option 1 - child 1 1' 65 | } 66 | 67 | @Unroll 68 | def 'Should be able to resolve #customFieldType custom field when the value is empty'( 69 | String customFieldType, String customFieldId, String attributeValue) { 70 | when: 71 | CustomFieldParameterMapping mapping = new CustomFieldParameterMapping('parameter', customFieldId) 72 | CustomFieldParameterResolver subject = new CustomFieldParameterResolver(mapping) 73 | String result = subject.resolve(createIssueFromFile('empty_custom_field')) 74 | 75 | then: 76 | result == attributeValue 77 | 78 | where: 79 | customFieldType | customFieldId | attributeValue 80 | 'Checkboxes' | '10100' | null 81 | 'Date Picker' | '10101' | null 82 | 'Date Time Picker' | '10102' | null 83 | 'Labels' | '10103' | null 84 | 'Number Field' | '10104' | null 85 | 'Radio Buttons' | '10105' | null 86 | 'Select List (multiple choices)' | '10107' | null 87 | 'Select List (cascading)' | '10106' | null 88 | 'Select List (single choice)' | '10200' | null 89 | 'Text Field (multi-line)' | '10000' | null 90 | 'Text Field (single line)' | '10108' | null 91 | 'User Picker (single user)' | '10110' | null 92 | 'URL Field' | '10109' | null 93 | } 94 | 95 | @Unroll 96 | def 'Should throw exception when parameter custom field id is not available'(String customFieldId) { 97 | when: 98 | CustomFieldParameterMapping mapping = new CustomFieldParameterMapping('uhused', customFieldId) 99 | CustomFieldParameterResolver subject = new CustomFieldParameterResolver(mapping) 100 | subject.resolve(createIssueFromFile('single_value_custom_field')) 101 | 102 | then: 103 | thrown JiraTriggerException 104 | 105 | where: 106 | //noinspection SpellCheckingInspection 107 | customFieldId << [ 108 | '90000', 109 | 'abc', 110 | ] 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/test/groovy/com/ceilfors/jenkins/plugins/jiratrigger/parameter/IssueAttributePathParameterResolverTest.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.parameter 2 | 3 | import com.atlassian.jira.rest.client.api.domain.Issue 4 | import com.atlassian.jira.rest.client.internal.json.IssueJsonParser 5 | import com.ceilfors.jenkins.plugins.jiratrigger.JiraTriggerException 6 | import org.codehaus.jettison.json.JSONObject 7 | import spock.lang.Specification 8 | import spock.lang.Unroll 9 | 10 | /** 11 | * @author ceilfors 12 | */ 13 | class IssueAttributePathParameterResolverTest extends Specification { 14 | 15 | private Issue createIssueFromFile(issueKey) { 16 | def issueJsonObject = new JSONObject(this.class.getResource("${issueKey}.json").text) 17 | new IssueJsonParser(new JSONObject([:]), new JSONObject([:])).parse(issueJsonObject) 18 | } 19 | 20 | @Unroll 21 | def 'Should be able to resolve parameter by hitting JIRA'(String attributePath, String attributeValue) { 22 | given: 23 | IssueAttributePathParameterMapping mapping = new IssueAttributePathParameterMapping('parameter', attributePath) 24 | IssueAttributePathParameterResolver resolver = new IssueAttributePathParameterResolver(mapping) 25 | 26 | when: 27 | String result = resolver.resolve(createIssueFromFile('TEST-136')) 28 | 29 | then: 30 | result == attributeValue 31 | 32 | where: 33 | attributePath | attributeValue 34 | 'key' | 'TEST-136' 35 | 'project.key' | 'TEST' 36 | 'id' | 11120 37 | 'timeTracking.originalEstimateMinutes' | 5 38 | 'status.name' | 'To Do' 39 | 'summary' | 'summary content' 40 | } 41 | 42 | @Unroll 43 | def 'Should throw exception when parameter is not resolvable'(String attributePath) { 44 | given: 45 | IssueAttributePathParameterMapping mapping = new IssueAttributePathParameterMapping('unused', attributePath) 46 | IssueAttributePathParameterResolver resolver = new IssueAttributePathParameterResolver(mapping) 47 | 48 | when: 49 | resolver.resolve(createIssueFromFile('TEST-136')) 50 | 51 | then: 52 | thrown JiraTriggerException 53 | 54 | where: 55 | //noinspection SpellCheckingInspection 56 | attributePath << [ 57 | 'timeTracking.originalEstimateSeconds', 58 | 'typo', 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/groovy/com/ceilfors/jenkins/plugins/jiratrigger/webhook/JiraWebhookTest.groovy: -------------------------------------------------------------------------------- 1 | package com.ceilfors.jenkins.plugins.jiratrigger.webhook 2 | 3 | import com.atlassian.jira.rest.client.api.domain.ChangelogItem 4 | import com.atlassian.jira.rest.client.api.domain.FieldType 5 | import org.joda.time.DateTime 6 | import org.kohsuke.stapler.StaplerRequest 7 | import spock.lang.Specification 8 | 9 | import static org.hamcrest.Matchers.equalTo 10 | import static org.hamcrest.Matchers.is 11 | import static org.joda.time.DateTimeZone.UTC 12 | import static spock.util.matcher.HamcrestSupport.expect 13 | 14 | /** 15 | * @author ceilfors 16 | */ 17 | @SuppressWarnings(['GroovyAssignabilityCheck', 'GrReassignedInClosureLocalVar']) 18 | class JiraWebhookTest extends Specification { 19 | 20 | String createIssueCreatedEvent() { 21 | this.class.getResourceAsStream('issue_created.json').text 22 | } 23 | 24 | String createIssueUpdatedEvent() { 25 | this.class.getResourceAsStream('issue_updated_without_comment.json').text 26 | } 27 | 28 | String createIssueUpdatedWithCommentEvent() { 29 | this.class.getResourceAsStream('issue_updated_with_comment.json').text 30 | } 31 | 32 | String createIssueStatusUpdatedEvent() { 33 | this.class.getResourceAsStream('issue_updated_status_updated.json').text 34 | } 35 | 36 | String createCloudCommentAddedEvent() { 37 | this.class.getResourceAsStream('cloud_comment_added.json').text 38 | } 39 | 40 | def 'Should fire changelog created event when status field is updated'() { 41 | WebhookChangelogEvent changelogEvent = null 42 | 43 | given: 44 | JiraWebhook jiraWebhook = new JiraWebhook() 45 | 46 | def listener = Mock(JiraWebhookListener) 47 | jiraWebhook.setJiraWebhookListener(listener) 48 | 49 | when: 50 | jiraWebhook.processEvent(Mock(StaplerRequest), createIssueStatusUpdatedEvent()) 51 | 52 | then: 53 | 1 * listener.changelogCreated(_) >> { args -> changelogEvent = args[0] } 54 | expect changelogEvent.changelog.items.toList(), equalTo([ 55 | new ChangelogItem(FieldType.JIRA, 'resolution', '1', 'Fixed', '10000', 'Done'), 56 | new ChangelogItem(FieldType.JIRA, 'status', '10000', 'To Do', '10001', 'Done'), 57 | ]) 58 | } 59 | 60 | def 'Should store request parameter in context'() { 61 | WebhookCommentEvent commentEvent = null 62 | 63 | given: 64 | def listener = Mock(JiraWebhookListener) 65 | def staplerRequest = Mock(StaplerRequest) 66 | staplerRequest.getParameter('user_id') >> 'adminId' 67 | staplerRequest.getParameter('user_key') >> 'adminKey' 68 | JiraWebhook jiraWebhook = new JiraWebhook() 69 | jiraWebhook.setJiraWebhookListener(listener) 70 | 71 | when: 72 | jiraWebhook.processEvent(staplerRequest, createIssueUpdatedWithCommentEvent()) 73 | 74 | then: 75 | 1 * listener.commentCreated(_) >> { args -> commentEvent = args[0] } 76 | expect commentEvent.userId, equalTo('adminId') 77 | expect commentEvent.userKey, equalTo('adminKey') 78 | } 79 | 80 | def 'Should not fire comment created event when an is issue created'() { 81 | given: 82 | JiraWebhook jiraWebhook = new JiraWebhook() 83 | 84 | def listener = Mock(JiraWebhookListener) 85 | jiraWebhook.setJiraWebhookListener(listener) 86 | 87 | when: 88 | jiraWebhook.processEvent(Mock(StaplerRequest), createIssueCreatedEvent()) 89 | 90 | then: 91 | 0 * listener.commentCreated(_) 92 | } 93 | 94 | def 'Should fire comment created event when an issue is updated with comment'() { 95 | WebhookCommentEvent commentEvent = null 96 | 97 | given: 98 | JiraWebhook jiraWebhook = new JiraWebhook() 99 | 100 | def listener = Mock(JiraWebhookListener) 101 | jiraWebhook.setJiraWebhookListener(listener) 102 | 103 | when: 104 | jiraWebhook.processEvent(Mock(StaplerRequest), createIssueUpdatedWithCommentEvent()) 105 | 106 | then: 107 | def expectedDateTime = new DateTime(2015, 12, 20, 18, 25, 9, 582, UTC) 108 | 1 * listener.commentCreated(_) >> { args -> commentEvent = args[0] } 109 | expect commentEvent.comment.body, is('comment body') 110 | expect commentEvent.comment.author.name, is('admin') 111 | expect commentEvent.webhookEventType, is(JiraWebhook.ISSUE_UPDATED_WEBHOOK_EVENT) 112 | expect commentEvent.issue.creationDate.toDateTime(UTC), is(expectedDateTime) 113 | expect commentEvent.issue.updateDate.toDateTime(UTC), is(expectedDateTime) 114 | } 115 | 116 | def 'Should not fire comment created event when an issue is updated without comments'() { 117 | given: 118 | JiraWebhook jiraWebhook = new JiraWebhook() 119 | 120 | def listener = Mock(JiraWebhookListener) 121 | jiraWebhook.setJiraWebhookListener(listener) 122 | 123 | when: 124 | jiraWebhook.processEvent(Mock(StaplerRequest), createIssueUpdatedEvent()) 125 | 126 | then: 127 | 0 * listener.commentCreated(_) 128 | } 129 | 130 | def 'Should fire comment created event when a comment is added in JIRA Cloud'() { 131 | given: 132 | WebhookCommentEvent commentEvent = null 133 | JiraWebhook jiraWebhook = new JiraWebhook() 134 | 135 | def listener = Mock(JiraWebhookListener) 136 | jiraWebhook.setJiraWebhookListener(listener) 137 | 138 | when: 139 | jiraWebhook.processEvent(Mock(StaplerRequest), createCloudCommentAddedEvent()) 140 | 141 | then: 142 | def expectedDateTime = new DateTime(1980, 1, 1, 0, 0, 0, 0, UTC) 143 | 1 * listener.commentCreated(_) >> { args -> commentEvent = args[0] } 144 | expect commentEvent.comment.body, is('comment body') 145 | expect commentEvent.comment.author.name, is('admin') 146 | expect commentEvent.webhookEventType, is(JiraWebhook.COMMENT_CREATED_WEBHOOK_EVENT) 147 | expect commentEvent.issue.creationDate.toDateTime(UTC), is(expectedDateTime) 148 | expect commentEvent.issue.updateDate.toDateTime(UTC), is(expectedDateTime) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/test/resources/com/ceilfors/jenkins/plugins/jiratrigger/parameter/TEST-136.json: -------------------------------------------------------------------------------- 1 | { 2 | "expand": "renderedFields,names,schema,transitions,operations,editmeta,changelog,versionedRepresentations", 3 | "id": "11120", 4 | "self": "http://localhost:2990/jira/rest/api/2/issue/11120", 5 | "key": "TEST-136", 6 | "fields": { 7 | "issuetype": { 8 | "self": "http://localhost:2990/jira/rest/api/2/issuetype/10000", 9 | "id": "10000", 10 | "description": "A task that needs to be done.", 11 | "iconUrl": "http://localhost:2990/jira/secure/viewavatar?size=xsmall&avatarId=10318&avatarType=issuetype", 12 | "name": "Task", 13 | "subtask": false, 14 | "avatarId": 10318 15 | }, 16 | "components": [], 17 | "timespent": null, 18 | "timeoriginalestimate": 300, 19 | "description": "description body", 20 | "project": { 21 | "self": "http://localhost:2990/jira/rest/api/2/project/10000", 22 | "id": "10000", 23 | "key": "TEST", 24 | "name": "TEST", 25 | "avatarUrls": { 26 | "48x48": "http://localhost:2990/jira/secure/projectavatar?avatarId=10011", 27 | "24x24": "http://localhost:2990/jira/secure/projectavatar?size=small&avatarId=10011", 28 | "16x16": "http://localhost:2990/jira/secure/projectavatar?size=xsmall&avatarId=10011", 29 | "32x32": "http://localhost:2990/jira/secure/projectavatar?size=medium&avatarId=10011" 30 | } 31 | }, 32 | "fixVersions": [], 33 | "aggregatetimespent": null, 34 | "resolution": null, 35 | "timetracking": { 36 | "originalEstimate": "5m", 37 | "remainingEstimate": "10m", 38 | "originalEstimateSeconds": 300, 39 | "remainingEstimateSeconds": 600 40 | }, 41 | "attachment": [], 42 | "aggregatetimeestimate": 600, 43 | "resolutiondate": null, 44 | "workratio": 0, 45 | "summary": "summary content", 46 | "lastViewed": "2015-12-26T12:00:43.169+0000", 47 | "watches": { 48 | "self": "http://localhost:2990/jira/rest/api/2/issue/TEST-136/watchers", 49 | "watchCount": 1, 50 | "isWatching": true 51 | }, 52 | "creator": { 53 | "self": "http://localhost:2990/jira/rest/api/2/user?username=admin", 54 | "name": "admin", 55 | "key": "admin", 56 | "emailAddress": "admin@admin.com", 57 | "avatarUrls": { 58 | "48x48": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=48", 59 | "24x24": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=24", 60 | "16x16": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=16", 61 | "32x32": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=32" 62 | }, 63 | "displayName": "admin", 64 | "active": true, 65 | "timeZone": "UTC" 66 | }, 67 | "subtasks": [], 68 | "created": "2015-12-26T12:00:38.891+0000", 69 | "reporter": { 70 | "self": "http://localhost:2990/jira/rest/api/2/user?username=admin", 71 | "name": "admin", 72 | "key": "admin", 73 | "emailAddress": "admin@admin.com", 74 | "avatarUrls": { 75 | "48x48": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=48", 76 | "24x24": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=24", 77 | "16x16": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=16", 78 | "32x32": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=32" 79 | }, 80 | "displayName": "admin", 81 | "active": true, 82 | "timeZone": "UTC" 83 | }, 84 | "customfield_10000": "customer name", 85 | "aggregateprogress": { 86 | "progress": 0, 87 | "total": 600, 88 | "percent": 0 89 | }, 90 | "priority": { 91 | "self": "http://localhost:2990/jira/rest/api/2/priority/3", 92 | "iconUrl": "http://localhost:2990/jira/images/icons/priorities/medium.svg", 93 | "name": "Medium", 94 | "id": "3" 95 | }, 96 | "labels": [ 97 | "testlabel" 98 | ], 99 | "environment": null, 100 | "timeestimate": 600, 101 | "aggregatetimeoriginalestimate": 300, 102 | "versions": [], 103 | "duedate": "2015-12-01", 104 | "progress": { 105 | "progress": 0, 106 | "total": 600, 107 | "percent": 0 108 | }, 109 | "comment": { 110 | "startAt": 0, 111 | "maxResults": 0, 112 | "total": 0, 113 | "comments": [] 114 | }, 115 | "issuelinks": [], 116 | "votes": { 117 | "self": "http://localhost:2990/jira/rest/api/2/issue/TEST-136/votes", 118 | "votes": 0, 119 | "hasVoted": false 120 | }, 121 | "worklog": { 122 | "startAt": 0, 123 | "maxResults": 20, 124 | "total": 0, 125 | "worklogs": [] 126 | }, 127 | "assignee": null, 128 | "updated": "2015-12-26T12:00:38.891+0000", 129 | "status": { 130 | "self": "http://localhost:2990/jira/rest/api/2/status/10000", 131 | "description": "", 132 | "iconUrl": "http://localhost:2990/jira/images/icons/status_generic.gif", 133 | "name": "To Do", 134 | "id": "10000", 135 | "statusCategory": { 136 | "self": "http://localhost:2990/jira/rest/api/2/statuscategory/2", 137 | "id": 2, 138 | "key": "new", 139 | "colorName": "blue-gray", 140 | "name": "To Do" 141 | } 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /src/test/resources/com/ceilfors/jenkins/plugins/jiratrigger/webhook/cloud_comment_added.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": 1524205492300, 3 | "webhookEvent": "comment_created", 4 | "comment": { 5 | "self": "https://jira-trigger-plugin.atlassian.net/rest/api/2/issue/10023/comment/10021", 6 | "id": "10021", 7 | "author": { 8 | "self": "https://jira-trigger-plugin.atlassian.net/rest/api/2/user?username=admin", 9 | "name": "admin", 10 | "key": "admin", 11 | "accountId": "557058:eb43b4a2-fad1-4ff6-a2d9-38eee0b088b9", 12 | "avatarUrls": { 13 | "48x48": "https://avatar-cdn.atlassian.com/e610a58b5e6c136fa2b2f509969b15c3?s=48&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fe610a58b5e6c136fa2b2f509969b15c3%3Fd%3Dmm%26s%3D48%26noRedirect%3Dtrue", 14 | "24x24": "https://avatar-cdn.atlassian.com/e610a58b5e6c136fa2b2f509969b15c3?s=24&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fe610a58b5e6c136fa2b2f509969b15c3%3Fd%3Dmm%26s%3D24%26noRedirect%3Dtrue", 15 | "16x16": "https://avatar-cdn.atlassian.com/e610a58b5e6c136fa2b2f509969b15c3?s=16&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fe610a58b5e6c136fa2b2f509969b15c3%3Fd%3Dmm%26s%3D16%26noRedirect%3Dtrue", 16 | "32x32": "https://avatar-cdn.atlassian.com/e610a58b5e6c136fa2b2f509969b15c3?s=32&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fe610a58b5e6c136fa2b2f509969b15c3%3Fd%3Dmm%26s%3D32%26noRedirect%3Dtrue" 17 | }, 18 | "displayName": "Wisen Tanasa", 19 | "active": true, 20 | "timeZone": "Europe/London" 21 | }, 22 | "body": "comment body", 23 | "updateAuthor": { 24 | "self": "https://jira-trigger-plugin.atlassian.net/rest/api/2/user?username=admin", 25 | "name": "admin", 26 | "key": "admin", 27 | "accountId": "557058:eb43b4a2-fad1-4ff6-a2d9-38eee0b088b9", 28 | "avatarUrls": { 29 | "48x48": "https://avatar-cdn.atlassian.com/e610a58b5e6c136fa2b2f509969b15c3?s=48&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fe610a58b5e6c136fa2b2f509969b15c3%3Fd%3Dmm%26s%3D48%26noRedirect%3Dtrue", 30 | "24x24": "https://avatar-cdn.atlassian.com/e610a58b5e6c136fa2b2f509969b15c3?s=24&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fe610a58b5e6c136fa2b2f509969b15c3%3Fd%3Dmm%26s%3D24%26noRedirect%3Dtrue", 31 | "16x16": "https://avatar-cdn.atlassian.com/e610a58b5e6c136fa2b2f509969b15c3?s=16&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fe610a58b5e6c136fa2b2f509969b15c3%3Fd%3Dmm%26s%3D16%26noRedirect%3Dtrue", 32 | "32x32": "https://avatar-cdn.atlassian.com/e610a58b5e6c136fa2b2f509969b15c3?s=32&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fe610a58b5e6c136fa2b2f509969b15c3%3Fd%3Dmm%26s%3D32%26noRedirect%3Dtrue" 33 | }, 34 | "displayName": "Wisen Tanasa", 35 | "active": true, 36 | "timeZone": "Europe/London" 37 | }, 38 | "created": "2018-04-20T07:24:52.300+0100", 39 | "updated": "2018-04-20T07:24:52.300+0100" 40 | }, 41 | "issue": { 42 | "id": "10023", 43 | "self": "https://jira-trigger-plugin.atlassian.net/rest/api/2/issue/10023", 44 | "key": "TEST-24", 45 | "fields": { 46 | "summary": "test", 47 | "issuetype": { 48 | "self": "https://jira-trigger-plugin.atlassian.net/rest/api/2/issuetype/10002", 49 | "id": "10002", 50 | "description": "A task that needs to be done.", 51 | "iconUrl": "https://jira-trigger-plugin.atlassian.net/secure/viewavatar?size=xsmall&avatarId=10318&avatarType=issuetype", 52 | "name": "Task", 53 | "subtask": false, 54 | "avatarId": 10318 55 | }, 56 | "project": { 57 | "self": "https://jira-trigger-plugin.atlassian.net/rest/api/2/project/10000", 58 | "id": "10000", 59 | "key": "TEST", 60 | "name": "test", 61 | "projectTypeKey": "software", 62 | "avatarUrls": { 63 | "48x48": "https://jira-trigger-plugin.atlassian.net/secure/projectavatar?pid=10000&avatarId=10400", 64 | "24x24": "https://jira-trigger-plugin.atlassian.net/secure/projectavatar?size=small&pid=10000&avatarId=10400", 65 | "16x16": "https://jira-trigger-plugin.atlassian.net/secure/projectavatar?size=xsmall&pid=10000&avatarId=10400", 66 | "32x32": "https://jira-trigger-plugin.atlassian.net/secure/projectavatar?size=medium&pid=10000&avatarId=10400" 67 | } 68 | }, 69 | "assignee": null, 70 | "priority": { 71 | "self": "https://jira-trigger-plugin.atlassian.net/rest/api/2/priority/3", 72 | "iconUrl": "https://jira-trigger-plugin.atlassian.net/images/icons/priorities/medium.svg", 73 | "name": "Medium", 74 | "id": "3" 75 | }, 76 | "status": { 77 | "self": "https://jira-trigger-plugin.atlassian.net/rest/api/2/status/10000", 78 | "description": "", 79 | "iconUrl": "https://jira-trigger-plugin.atlassian.net/", 80 | "name": "To Do", 81 | "id": "10000", 82 | "statusCategory": { 83 | "self": "https://jira-trigger-plugin.atlassian.net/rest/api/2/statuscategory/2", 84 | "id": 2, 85 | "key": "new", 86 | "colorName": "blue-gray", 87 | "name": "To Do" 88 | } 89 | } 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /src/test/resources/com/ceilfors/jenkins/plugins/jiratrigger/webhook/issue_created.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": 1450635909598, 3 | "webhookEvent": "jira:issue_created", 4 | "user": { 5 | "self": "http://localhost:2990/jira/rest/api/2/user?username=admin", 6 | "name": "admin", 7 | "key": "admin", 8 | "emailAddress": "admin@admin.com", 9 | "avatarUrls": { 10 | "48x48": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=48", 11 | "24x24": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=24", 12 | "16x16": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=16", 13 | "32x32": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=32" 14 | }, 15 | "displayName": "admin", 16 | "active": true, 17 | "timeZone": "UTC" 18 | }, 19 | "issue": { 20 | "id": "10003", 21 | "self": "http://localhost:2990/jira/rest/api/2/issue/10003", 22 | "key": "TEST-4", 23 | "fields": { 24 | "issuetype": { 25 | "self": "http://localhost:2990/jira/rest/api/2/issuetype/10000", 26 | "id": "10000", 27 | "description": "A task that needs to be done.", 28 | "iconUrl": "http://localhost:2990/jira/secure/viewavatar?size=xsmall&avatarId=10318&avatarType=issuetype", 29 | "name": "Task", 30 | "subtask": false, 31 | "avatarId": 10318 32 | }, 33 | "components": [], 34 | "timespent": null, 35 | "timeoriginalestimate": null, 36 | "description": null, 37 | "project": { 38 | "self": "http://localhost:2990/jira/rest/api/2/project/10000", 39 | "id": "10000", 40 | "key": "TEST", 41 | "name": "TEST", 42 | "avatarUrls": { 43 | "48x48": "http://localhost:2990/jira/secure/projectavatar?avatarId=10011", 44 | "24x24": "http://localhost:2990/jira/secure/projectavatar?size=small&avatarId=10011", 45 | "16x16": "http://localhost:2990/jira/secure/projectavatar?size=xsmall&avatarId=10011", 46 | "32x32": "http://localhost:2990/jira/secure/projectavatar?size=medium&avatarId=10011" 47 | } 48 | }, 49 | "fixVersions": [], 50 | "aggregatetimespent": null, 51 | "resolution": null, 52 | "timetracking": {}, 53 | "attachment": [], 54 | "aggregatetimeestimate": null, 55 | "resolutiondate": null, 56 | "workratio": -1, 57 | "summary": "a", 58 | "lastViewed": null, 59 | "watches": { 60 | "self": "http://localhost:2990/jira/rest/api/2/issue/TEST-4/watchers", 61 | "watchCount": 0, 62 | "isWatching": false 63 | }, 64 | "creator": { 65 | "self": "http://localhost:2990/jira/rest/api/2/user?username=admin", 66 | "name": "admin", 67 | "key": "admin", 68 | "emailAddress": "admin@admin.com", 69 | "avatarUrls": { 70 | "48x48": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=48", 71 | "24x24": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=24", 72 | "16x16": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=16", 73 | "32x32": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=32" 74 | }, 75 | "displayName": "admin", 76 | "active": true, 77 | "timeZone": "UTC" 78 | }, 79 | "subtasks": [], 80 | "created": "2015-12-20T18:25:09.582+0000", 81 | "reporter": { 82 | "self": "http://localhost:2990/jira/rest/api/2/user?username=admin", 83 | "name": "admin", 84 | "key": "admin", 85 | "emailAddress": "admin@admin.com", 86 | "avatarUrls": { 87 | "48x48": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=48", 88 | "24x24": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=24", 89 | "16x16": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=16", 90 | "32x32": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=32" 91 | }, 92 | "displayName": "admin", 93 | "active": true, 94 | "timeZone": "UTC" 95 | }, 96 | "aggregateprogress": { 97 | "progress": 0, 98 | "total": 0 99 | }, 100 | "priority": { 101 | "self": "http://localhost:2990/jira/rest/api/2/priority/3", 102 | "iconUrl": "http://localhost:2990/jira/images/icons/priorities/medium.svg", 103 | "name": "Medium", 104 | "id": "3" 105 | }, 106 | "labels": [], 107 | "environment": null, 108 | "timeestimate": null, 109 | "aggregatetimeoriginalestimate": null, 110 | "versions": [], 111 | "duedate": null, 112 | "progress": { 113 | "progress": 0, 114 | "total": 0 115 | }, 116 | "comment": { 117 | "startAt": 0, 118 | "maxResults": 0, 119 | "total": 0, 120 | "comments": [] 121 | }, 122 | "issuelinks": [], 123 | "votes": { 124 | "self": "http://localhost:2990/jira/rest/api/2/issue/TEST-4/votes", 125 | "votes": 0, 126 | "hasVoted": false 127 | }, 128 | "worklog": { 129 | "startAt": 0, 130 | "maxResults": 20, 131 | "total": 0, 132 | "worklogs": [] 133 | }, 134 | "assignee": null, 135 | "updated": "2015-12-20T18:25:09.582+0000", 136 | "status": { 137 | "self": "http://localhost:2990/jira/rest/api/2/status/10000", 138 | "description": "", 139 | "iconUrl": "http://localhost:2990/jira/images/icons/status_generic.gif", 140 | "name": "To Do", 141 | "id": "10000", 142 | "statusCategory": { 143 | "self": "http://localhost:2990/jira/rest/api/2/statuscategory/2", 144 | "id": 2, 145 | "key": "new", 146 | "colorName": "blue-gray", 147 | "name": "To Do" 148 | } 149 | } 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /src/test/resources/rest-sample/comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "self": "http://localhost:2990/jira/rest/api/2/issue/10003/comment/10000", 3 | "id": "10000", 4 | "author": { 5 | "self": "http://localhost:2990/jira/rest/api/2/user?username=admin", 6 | "name": "admin", 7 | "key": "admin", 8 | "emailAddress": "admin@admin.com", 9 | "avatarUrls": { 10 | "48x48": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=48", 11 | "24x24": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=24", 12 | "16x16": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=16", 13 | "32x32": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=32" 14 | }, 15 | "displayName": "admin", 16 | "active": true, 17 | "timeZone": "UTC" 18 | }, 19 | "body": "comment body", 20 | "updateAuthor": { 21 | "self": "http://localhost:2990/jira/rest/api/2/user?username=admin", 22 | "name": "admin", 23 | "key": "admin", 24 | "emailAddress": "admin@admin.com", 25 | "avatarUrls": { 26 | "48x48": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=48", 27 | "24x24": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=24", 28 | "16x16": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=16", 29 | "32x32": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=32" 30 | }, 31 | "displayName": "admin", 32 | "active": true, 33 | "timeZone": "UTC" 34 | }, 35 | "created": "2015-12-20T19:04:58.421+0000", 36 | "updated": "2015-12-20T19:04:58.421+0000" 37 | } -------------------------------------------------------------------------------- /src/test/resources/rest-sample/issue_with_customfield.json: -------------------------------------------------------------------------------- 1 | { 2 | "expand": "renderedFields,names,schema,transitions,operations,editmeta,changelog,versionedRepresentations", 3 | "id": "11120", 4 | "self": "http://localhost:2990/jira/rest/api/2/issue/11120", 5 | "key": "TEST-136", 6 | "fields": { 7 | "issuetype": { 8 | "self": "http://localhost:2990/jira/rest/api/2/issuetype/10000", 9 | "id": "10000", 10 | "description": "A task that needs to be done.", 11 | "iconUrl": "http://localhost:2990/jira/secure/viewavatar?size=xsmall&avatarId=10318&avatarType=issuetype", 12 | "name": "Task", 13 | "subtask": false, 14 | "avatarId": 10318 15 | }, 16 | "components": [], 17 | "timespent": null, 18 | "timeoriginalestimate": 300, 19 | "description": "description body", 20 | "project": { 21 | "self": "http://localhost:2990/jira/rest/api/2/project/10000", 22 | "id": "10000", 23 | "key": "TEST", 24 | "name": "TEST", 25 | "avatarUrls": { 26 | "48x48": "http://localhost:2990/jira/secure/projectavatar?avatarId=10011", 27 | "24x24": "http://localhost:2990/jira/secure/projectavatar?size=small&avatarId=10011", 28 | "16x16": "http://localhost:2990/jira/secure/projectavatar?size=xsmall&avatarId=10011", 29 | "32x32": "http://localhost:2990/jira/secure/projectavatar?size=medium&avatarId=10011" 30 | } 31 | }, 32 | "fixVersions": [], 33 | "aggregatetimespent": null, 34 | "resolution": null, 35 | "timetracking": { 36 | "originalEstimate": "5m", 37 | "remainingEstimate": "10m", 38 | "originalEstimateSeconds": 300, 39 | "remainingEstimateSeconds": 600 40 | }, 41 | "attachment": [], 42 | "aggregatetimeestimate": 600, 43 | "resolutiondate": null, 44 | "workratio": 0, 45 | "summary": "summary content", 46 | "lastViewed": "2015-12-26T12:00:43.169+0000", 47 | "watches": { 48 | "self": "http://localhost:2990/jira/rest/api/2/issue/TEST-136/watchers", 49 | "watchCount": 1, 50 | "isWatching": true 51 | }, 52 | "creator": { 53 | "self": "http://localhost:2990/jira/rest/api/2/user?username=admin", 54 | "name": "admin", 55 | "key": "admin", 56 | "emailAddress": "admin@admin.com", 57 | "avatarUrls": { 58 | "48x48": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=48", 59 | "24x24": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=24", 60 | "16x16": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=16", 61 | "32x32": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=32" 62 | }, 63 | "displayName": "admin", 64 | "active": true, 65 | "timeZone": "UTC" 66 | }, 67 | "subtasks": [], 68 | "created": "2015-12-26T12:00:38.891+0000", 69 | "reporter": { 70 | "self": "http://localhost:2990/jira/rest/api/2/user?username=admin", 71 | "name": "admin", 72 | "key": "admin", 73 | "emailAddress": "admin@admin.com", 74 | "avatarUrls": { 75 | "48x48": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=48", 76 | "24x24": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=24", 77 | "16x16": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=16", 78 | "32x32": "http://www.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?d=mm&s=32" 79 | }, 80 | "displayName": "admin", 81 | "active": true, 82 | "timeZone": "UTC" 83 | }, 84 | "customfield_10000": "customer name", 85 | "aggregateprogress": { 86 | "progress": 0, 87 | "total": 600, 88 | "percent": 0 89 | }, 90 | "priority": { 91 | "self": "http://localhost:2990/jira/rest/api/2/priority/3", 92 | "iconUrl": "http://localhost:2990/jira/images/icons/priorities/medium.svg", 93 | "name": "Medium", 94 | "id": "3" 95 | }, 96 | "labels": [ 97 | "testlabel" 98 | ], 99 | "environment": null, 100 | "timeestimate": 600, 101 | "aggregatetimeoriginalestimate": 300, 102 | "versions": [], 103 | "duedate": "2015-12-01", 104 | "progress": { 105 | "progress": 0, 106 | "total": 600, 107 | "percent": 0 108 | }, 109 | "comment": { 110 | "startAt": 0, 111 | "maxResults": 0, 112 | "total": 0, 113 | "comments": [] 114 | }, 115 | "issuelinks": [], 116 | "votes": { 117 | "self": "http://localhost:2990/jira/rest/api/2/issue/TEST-136/votes", 118 | "votes": 0, 119 | "hasVoted": false 120 | }, 121 | "worklog": { 122 | "startAt": 0, 123 | "maxResults": 20, 124 | "total": 0, 125 | "worklogs": [] 126 | }, 127 | "assignee": null, 128 | "updated": "2015-12-26T12:00:38.891+0000", 129 | "status": { 130 | "self": "http://localhost:2990/jira/rest/api/2/status/10000", 131 | "description": "", 132 | "iconUrl": "http://localhost:2990/jira/images/icons/status_generic.gif", 133 | "name": "To Do", 134 | "id": "10000", 135 | "statusCategory": { 136 | "self": "http://localhost:2990/jira/rest/api/2/statuscategory/2", 137 | "id": 2, 138 | "key": "new", 139 | "colorName": "blue-gray", 140 | "name": "To Do" 141 | } 142 | } 143 | } 144 | } --------------------------------------------------------------------------------