Repository: jenkinsci/jira-plugin Branch: master Commit: 61f365c12335 Files: 300 Total size: 775.0 KB Directory structure: gitextract_wo4b4tn0/ ├── .git-blame-ignore-revs ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── 1-report-bug.yml │ │ ├── 2-feature-request.yml │ │ ├── 3-documentation.yml │ │ └── config.yml │ ├── pull_request_template.md │ ├── release-drafter.yml │ ├── renovate.json │ └── workflows/ │ ├── crowdin.yml │ ├── jenkins-security-scan.yml │ ├── release-drafter.yml │ └── sonarcloud.yml ├── .gitignore ├── .mvn/ │ ├── extensions.xml │ └── maven.config ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── Jenkinsfile ├── LICENSE.md ├── README.md ├── crowdin.yml ├── docker-compose.yml ├── docs/ │ ├── .nojekyll │ ├── README.md │ ├── _sidebar.md │ ├── changelog.md │ ├── features.md │ ├── index.html │ ├── system-properties.md │ ├── troubleshooting.md │ └── usage-examples.md ├── pom.xml └── src/ ├── main/ │ ├── java/ │ │ ├── com/ │ │ │ └── atlassian/ │ │ │ └── httpclient/ │ │ │ ├── apache/ │ │ │ │ └── httpcomponents/ │ │ │ │ ├── ApacheAsyncHttpClient.java │ │ │ │ ├── CommonBuilder.java │ │ │ │ ├── CompletableFuturePromiseHttpPromiseAsyncClient.java │ │ │ │ ├── DefaultHttpClientFactory.java │ │ │ │ ├── DefaultMessage.java │ │ │ │ ├── DefaultRequest.java │ │ │ │ ├── DefaultResponse.java │ │ │ │ ├── EntityByteArrayInputStream.java │ │ │ │ ├── Headers.java │ │ │ │ ├── MavenUtils.java │ │ │ │ ├── PromiseHttpAsyncClient.java │ │ │ │ ├── RedirectStrategy.java │ │ │ │ └── RequestEntityEffect.java │ │ │ └── base/ │ │ │ └── event/ │ │ │ ├── AbstractHttpRequestEvent.java │ │ │ ├── HttpRequestCompletedEvent.java │ │ │ └── HttpRequestFailedEvent.java │ │ └── hudson/ │ │ └── plugins/ │ │ └── jira/ │ │ ├── CredentialsHelper.java │ │ ├── EmptyFriendlyURLConverter.java │ │ ├── EnvironmentExpander.java │ │ ├── JiraBuildAction.java │ │ ├── JiraCarryOverAction.java │ │ ├── JiraChangeLogAnnotator.java │ │ ├── JiraCreateIssueNotifier.java │ │ ├── JiraCreateReleaseNotes.java │ │ ├── JiraEnvironmentContributingAction.java │ │ ├── JiraEnvironmentVariableBuilder.java │ │ ├── JiraFolderProperty.java │ │ ├── JiraGlobalConfiguration.java │ │ ├── JiraIssueMigrator.java │ │ ├── JiraIssueUpdateBuilder.java │ │ ├── JiraIssueUpdater.java │ │ ├── JiraJobAction.java │ │ ├── JiraMailAddressResolver.java │ │ ├── JiraProjectProperty.java │ │ ├── JiraReleaseVersionUpdater.java │ │ ├── JiraReleaseVersionUpdaterBuilder.java │ │ ├── JiraRestService.java │ │ ├── JiraSession.java │ │ ├── JiraSessionFactory.java │ │ ├── JiraSite.java │ │ ├── JiraVersionCreator.java │ │ ├── JiraVersionCreatorBuilder.java │ │ ├── RunScmChangeExtractor.java │ │ ├── Updater.java │ │ ├── VersionCreator.java │ │ ├── VersionReleaser.java │ │ ├── auth/ │ │ │ └── BearerHttpAuthenticationHandler.java │ │ ├── extension/ │ │ │ ├── ExtendedAsynchronousJiraRestClient.java │ │ │ ├── ExtendedAsynchronousMyPermissionsRestClient.java │ │ │ ├── ExtendedAsynchronousVersionRestClient.java │ │ │ ├── ExtendedJiraRestClient.java │ │ │ ├── ExtendedMyPermissionsRestClient.java │ │ │ ├── ExtendedVersion.java │ │ │ ├── ExtendedVersionInput.java │ │ │ ├── ExtendedVersionInputJsonGenerator.java │ │ │ ├── ExtendedVersionJsonParser.java │ │ │ └── ExtendedVersionRestClient.java │ │ ├── listissuesparameter/ │ │ │ ├── JiraIssueParameterDefinition.java │ │ │ └── JiraIssueParameterValue.java │ │ ├── model/ │ │ │ ├── JiraIssue.java │ │ │ ├── JiraIssueField.java │ │ │ └── JiraVersion.java │ │ ├── pipeline/ │ │ │ ├── CommentStep.java │ │ │ ├── IssueFieldUpdateStep.java │ │ │ ├── IssueSelectorStep.java │ │ │ └── SearchIssuesStep.java │ │ ├── selector/ │ │ │ ├── AbstractIssueSelector.java │ │ │ ├── DefaultIssueSelector.java │ │ │ ├── ExplicitIssueSelector.java │ │ │ ├── JqlIssueSelector.java │ │ │ └── perforce/ │ │ │ ├── JobIssueSelector.java │ │ │ └── P4JobIssueSelector.java │ │ └── versionparameter/ │ │ ├── JiraVersionParameterDefinition.java │ │ ├── JiraVersionParameterValue.java │ │ └── VersionComparator.java │ ├── resources/ │ │ ├── atlassian-httpclient-plugin-0.23.0.pom │ │ ├── hudson/ │ │ │ └── plugins/ │ │ │ └── jira/ │ │ │ ├── JiraBuildAction/ │ │ │ │ └── summary.jelly │ │ │ ├── JiraCreateIssueNotifier/ │ │ │ │ ├── config.jelly │ │ │ │ ├── help-actionIdOnSuccess.html │ │ │ │ ├── help-assignee.html │ │ │ │ ├── help-component.html │ │ │ │ ├── help-priorityId.html │ │ │ │ ├── help-projectKey.html │ │ │ │ ├── help-testDescription.html │ │ │ │ └── help-typeId.html │ │ │ ├── JiraCreateReleaseNotes/ │ │ │ │ ├── config.jelly │ │ │ │ ├── help-jiraEnvironmentVariable.html │ │ │ │ ├── help-jiraFilter.html │ │ │ │ ├── help-jiraProjectKey.html │ │ │ │ └── help-jiraRelease.html │ │ │ ├── JiraEnvironmentVariableBuilder/ │ │ │ │ ├── config.jelly │ │ │ │ └── help.html │ │ │ ├── JiraFolderProperty/ │ │ │ │ └── config.jelly │ │ │ ├── JiraGlobalConfiguration/ │ │ │ │ └── config.jelly │ │ │ ├── JiraIssueMigrator/ │ │ │ │ ├── config.jelly │ │ │ │ ├── help-addRelease.html │ │ │ │ ├── help-jiraProjectKey.html │ │ │ │ ├── help-jiraQuery.html │ │ │ │ ├── help-jiraRelease.html │ │ │ │ └── help-jiraReplaceVersion.html │ │ │ ├── JiraIssueUpdateBuilder/ │ │ │ │ ├── config.jelly │ │ │ │ ├── help-comment.html │ │ │ │ ├── help-jqlSearch.html │ │ │ │ ├── help-workflowActionName.html │ │ │ │ └── help.html │ │ │ ├── JiraIssueUpdater/ │ │ │ │ └── config.jelly │ │ │ ├── JiraProjectProperty/ │ │ │ │ └── config.jelly │ │ │ ├── JiraReleaseVersionUpdater/ │ │ │ │ ├── config.jelly │ │ │ │ ├── help-jiraDescription.html │ │ │ │ ├── help-jiraProjectKey.html │ │ │ │ └── help-jiraRelease.html │ │ │ ├── JiraReleaseVersionUpdaterBuilder/ │ │ │ │ ├── config.jelly │ │ │ │ ├── help-jiraDescription.html │ │ │ │ ├── help-jiraProjectKey.html │ │ │ │ └── help-jiraRelease.html │ │ │ ├── JiraSite/ │ │ │ │ ├── config.jelly │ │ │ │ ├── config.properties │ │ │ │ ├── config_de.properties │ │ │ │ ├── config_fr.properties │ │ │ │ ├── config_it.properties │ │ │ │ ├── config_ja.properties │ │ │ │ ├── config_nl.properties │ │ │ │ ├── config_pl.properties │ │ │ │ ├── help-alternativeUrl.html │ │ │ │ ├── help-appendChangeTimestamp.html │ │ │ │ ├── help-credentialsId.html │ │ │ │ ├── help-credentialsId_de.html │ │ │ │ ├── help-credentialsId_fr.html │ │ │ │ ├── help-credentialsId_ja.html │ │ │ │ ├── help-dateTimePattern.html │ │ │ │ ├── help-disableChangelogAnnotations.html │ │ │ │ ├── help-groupVisibility.html │ │ │ │ ├── help-maxIssuesFromJqlSearch.html │ │ │ │ ├── help-readTimeout.html │ │ │ │ ├── help-recordScmChanges.html │ │ │ │ ├── help-recordScmChanges_fr.html │ │ │ │ ├── help-recordScmChanges_ja.html │ │ │ │ ├── help-roleVisibility.html │ │ │ │ ├── help-supportsWikiStyleComment.html │ │ │ │ ├── help-supportsWikiStyleComment_de.html │ │ │ │ ├── help-supportsWikiStyleComment_fr.html │ │ │ │ ├── help-supportsWikiStyleComment_ja.html │ │ │ │ ├── help-threadExecutorNumber.html │ │ │ │ ├── help-timeout.html │ │ │ │ ├── help-updateJiraIssueForAllStatus.html │ │ │ │ ├── help-url.html │ │ │ │ ├── help-url_de.html │ │ │ │ ├── help-url_fr.html │ │ │ │ ├── help-url_ja.html │ │ │ │ ├── help-useHTTPAuth.html │ │ │ │ ├── help-userPattern.html │ │ │ │ ├── help-userPattern_de.html │ │ │ │ ├── help-userPattern_fr.html │ │ │ │ └── help-userPattern_ja.html │ │ │ ├── JiraVersionCreator/ │ │ │ │ ├── config.jelly │ │ │ │ ├── help-failIfAlreadyExists.html │ │ │ │ ├── help-jiraProjectKey.html │ │ │ │ └── help-jiraVersion.html │ │ │ ├── JiraVersionCreatorBuilder/ │ │ │ │ ├── config.jelly │ │ │ │ ├── help-failIfAlreadyExists.html │ │ │ │ ├── help-jiraProjectKey.html │ │ │ │ └── help-jiraVersion.html │ │ │ ├── MavenJiraIssueUpdater/ │ │ │ │ └── config.jelly │ │ │ ├── Messages.properties │ │ │ ├── Messages_de.properties │ │ │ ├── Messages_fr.properties │ │ │ ├── Messages_it.properties │ │ │ ├── Messages_ja.properties │ │ │ ├── Messages_pl.properties │ │ │ ├── listissuesparameter/ │ │ │ │ ├── JiraIssueParameterDefinition/ │ │ │ │ │ ├── config.jelly │ │ │ │ │ ├── help-altSummaryFields.html │ │ │ │ │ ├── help-jiraIssueFilter.html │ │ │ │ │ └── index.jelly │ │ │ │ └── JiraIssueParameterValue/ │ │ │ │ └── value.jelly │ │ │ ├── pipeline/ │ │ │ │ ├── CommentStep/ │ │ │ │ │ └── config.jelly │ │ │ │ ├── IssueFieldUpdateStep/ │ │ │ │ │ ├── config.jelly │ │ │ │ │ ├── help-fieldId.html │ │ │ │ │ └── help.html │ │ │ │ ├── IssueSelectorStep/ │ │ │ │ │ └── config.jelly │ │ │ │ └── SearchIssuesStep/ │ │ │ │ └── config.jelly │ │ │ ├── selector/ │ │ │ │ ├── DefaultIssueSelector/ │ │ │ │ │ └── config.jelly │ │ │ │ ├── ExplicitIssueSelector/ │ │ │ │ │ └── config.jelly │ │ │ │ ├── JqlIssueSelector/ │ │ │ │ │ └── config.jelly │ │ │ │ └── perforce/ │ │ │ │ ├── P4JobIssueSelector/ │ │ │ │ │ └── config.jelly │ │ │ │ └── PerforceJobIssueSelector/ │ │ │ │ └── config.jelly │ │ │ └── versionparameter/ │ │ │ ├── JiraVersionParameterDefinition/ │ │ │ │ ├── config.jelly │ │ │ │ ├── help-jiraProjectKey.html │ │ │ │ ├── help-jiraReleasePattern.html │ │ │ │ ├── help-jiraShowReleased.html │ │ │ │ └── index.jelly │ │ │ └── JiraVersionParameterValue/ │ │ │ └── value.jelly │ │ └── index.jelly │ └── webapp/ │ ├── help-jira-create-issue.html │ ├── help-release-migrate.html │ ├── help-release.html │ ├── help-version-create.html │ ├── help.html │ ├── help_de.html │ ├── help_fr.html │ └── help_ja.html ├── spotbugs/ │ └── excludesFilter.xml └── test/ ├── java/ │ ├── JiraConfig.java │ ├── JiraTester.java │ ├── JiraTesterBearerAuth.java │ ├── com/ │ │ └── atlassian/ │ │ └── httpclient/ │ │ └── apache/ │ │ └── httpcomponents/ │ │ ├── ApacheAsyncHttpClientTest.java │ │ └── CompletableFuturePromiseHttpPromiseAsyncClientTest.java │ └── hudson/ │ └── plugins/ │ └── jira/ │ ├── BuildListenerResultMethodMock.java │ ├── ChangingWorkflowTest.java │ ├── CliParameterTest.java │ ├── ConfigAsCodeTest.java │ ├── CredentialsHelperTest.java │ ├── DescriptorImplTest.java │ ├── EmptyFriendlyURLConverterTest.java │ ├── EnvironmentExpanderTest.java │ ├── JiraBuildActionTest.java │ ├── JiraChangeLogAnnotatorTest.java │ ├── JiraCreateIssueNotifierTest.java │ ├── JiraCreateReleaseNotesTest.java │ ├── JiraEnvironmentContributingActionTest.java │ ├── JiraEnvironmentVariableBuilderTest.java │ ├── JiraFolderPropertyTest.java │ ├── JiraGlobalConfigurationSaveTest.java │ ├── JiraGlobalConfigurationTest.java │ ├── JiraIssueMigratorTest.java │ ├── JiraIssueParameterDefResultTest.java │ ├── JiraIssueUpdateBuilderTest.java │ ├── JiraIssueUpdaterTest.java │ ├── JiraJobActionTest.java │ ├── JiraProjectPropertyTest.java │ ├── JiraReleaseVersionUpdateBuilderTest.java │ ├── JiraRestServiceProxyTest.java │ ├── JiraRestServiceTest.java │ ├── JiraSessionTest.java │ ├── JiraSiteSecurity1029Test.java │ ├── JiraSiteTest.java │ ├── JiraVersionCreatorBuilderTest.java │ ├── MailResolverDisabledTest.java │ ├── MailResolverWithExtensionTest.java │ ├── MockAffectedFile.java │ ├── UnmaskMailTest.java │ ├── UpdaterTest.java │ ├── VersionCreatorTest.java │ ├── VersionReleaserTest.java │ ├── auth/ │ │ ├── BearerHttpAuthenticationHandlerTest.java │ │ └── JiraRestServiceBearerAuthTest.java │ ├── listissuesparameter/ │ │ ├── JiraIssueParameterDefinitionTest.java │ │ └── JiraIssueParameterTest.java │ ├── pipeline/ │ │ ├── CommentStepTest.java │ │ ├── IssueFieldUpdateStepTest.java │ │ ├── IssueSelectorStepTest.java │ │ └── SearchIssuesStepTest.java │ ├── selector/ │ │ ├── DefaultIssueSelectorTest.java │ │ ├── ExplicitIssueSelectorTest.java │ │ ├── JqlIssueSelectorTest.java │ │ └── perforce/ │ │ ├── JobIssueSelectorTest.java │ │ └── P4JobIssueSelectorTest.java │ └── versionparameter/ │ ├── JiraReleaseVersionParameterTest.java │ ├── JiraVersionParameterDefinitionTest.java │ └── VersionComparatorTest.java └── resources/ ├── hudson/ │ └── plugins/ │ └── jira/ │ ├── JiraBuildActionTest/ │ │ └── binaryCompatibility/ │ │ ├── config.xml │ │ └── jobs/ │ │ └── project/ │ │ ├── builds/ │ │ │ └── 2/ │ │ │ └── build.xml │ │ └── config.xml │ ├── multiple-sites.yml │ ├── oldJiraProjectProperty.xml │ └── single-site.yml └── jira.properties ================================================ FILE CONTENTS ================================================ ================================================ FILE: .git-blame-ignore-revs ================================================ # .git-blame-ignore-revs # Reformatting of code with Spotless 969d12b2b997f23561af51530f9394e8ca34dfd7 ================================================ FILE: .github/CODEOWNERS ================================================ * @jenkinsci/jira-plugin-developers ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [ olamy, rantoniuk] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/1-report-bug.yml ================================================ name: "🐛 Bug report" type: "Bug" description: Create a bug report to help us improve body: - type: markdown attributes: value: | **Never report security issues on GitHub or other public channels (Gitter/Twitter/etc.)** Follow these instruction to report security issues: https://www.jenkins.io/security/#reporting-vulnerabilities - type: textarea attributes: label: Jenkins and plugins versions report description: | For easier bug reporting, you can get the full list of plugins with this Groovy script that you can run in **Jenkins > Manage Jenkins > Script Console**: ```groovy println("Jenkins: ${Jenkins.instance.getVersion()}") println("OS: ${System.getProperty('os.name')} - ${System.getProperty('os.version')}") println "---" Jenkins.instance.pluginManager.plugins .collect() .sort { it.getShortName() } .each { plugin -> println("${plugin.getShortName()}:${plugin.getVersion()}") } return ``` placeholder: | Jenkins: 2.326 OS: Linux - 3.10.0-1160.45.1.el7.x86_64 --- ace-editor:1.1 ant:1.13 antisamy-markup-formatter:2.5 apache-httpcomponents-client-4-api:4.5.13-1.0 authentication-tokens:1.4 bootstrap4-api:4.6.0-3 bootstrap5-api:5.1.3-4 bouncycastle-api:2.25 ... value: |
Environment ```text Paste the output here ```
validations: required: true - type: textarea attributes: label: What Operating System are you using (both controller, and any agents involved in the problem)? validations: required: true - type: textarea attributes: label: Reproduction steps description: | Write bullet-point reproduction steps. Be explicit about any relevant configuration, jobs, build history, user accounts, etc., redacting confidential information as needed. The best reproduction steps start with a clean Jenkins install, perhaps a `docker run` command if possible. Use screenshots where appropriate, copy textual output otherwise. When in doubt, do both. Include relevant logs, debug if needed - https://www.jenkins.io/doc/book/system-administration/viewing-logs/ placeholder: | 1. Step 1: ... 2. Step 2: ... validations: required: true - type: textarea attributes: label: Expected Results description: What was your expected result? validations: required: true - type: textarea attributes: label: Actual Results description: What was the actual result? validations: required: true - type: textarea attributes: label: Anything else? description: You can provide additional context below. ================================================ FILE: .github/ISSUE_TEMPLATE/2-feature-request.yml ================================================ name: "🚀 Feature request" type: "feature" description: I have a suggestion body: - type: textarea attributes: label: What feature do you want to see added? description: A clear and concise description of your feature request. validations: required: true - type: textarea attributes: label: Upstream changes description: Link here any upstream changes that might be relevant to this request ================================================ FILE: .github/ISSUE_TEMPLATE/3-documentation.yml ================================================ name: "📝 Documentation" labels: ["documentation"] description: "Let us know if any documentation is missing or could be improved" body: - type: textarea attributes: label: Describe your use-case which is not covered by existing documentation. description: If it is easier to submit a documentation patch instead of writing an issue, just do it! validations: required: true - type: textarea attributes: label: Reference any relevant documentation, other materials or issues/pull requests that can be used for inspiration. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: Community forum url: https://community.jenkins.io/ about: Please ask and answer questions here - name: Mailing lists url: https://www.jenkins.io/mailing-lists/ about: You can also raise a question in one of the user or developer mailing lists ================================================ FILE: .github/pull_request_template.md ================================================ ### Related issue ### Changes ### Tests - [ ] I have updated/added relevant documentation in the `docs/` directory - [ ] I have verified that the Code Coverage is not lower than before / that all the changes are covered as needed - [ ] I have tested my changes with a Jira Cloud / Jira Server ================================================ FILE: .github/release-drafter.yml ================================================ _extends: github:jenkinsci/.github:/.github/release-drafter.yml name-template: $RESOLVED_VERSION tag-template: jira-$RESOLVED_VERSION version-resolver: major: labels: - 'major' - 'breaking' minor: labels: - 'minor' - 'dependencies' patch: labels: - 'bugfix' - 'patch' default: minor ================================================ FILE: .github/renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "github>jenkinsci/renovate-config" ], "packageRules": [ { "matchManagers": ["maven"], "matchPackageNames": [ "io.atlassian.fugue:fugue" ], "enabled": false }, { "matchManagers": ["maven"], "groupName": "atlassian", "matchPackageNames": [ "/^com\.atlassian\.plugins:.*$/", "/^com\.atlassian\.jira:.*$/", "/^io\.atlassian\.util\.concurrent:.*$/" ] }, { "matchManagers": ["maven"], "groupName": "jenkins", "matchPackageNames": [ "/^io\.jenkins\.tools\.bom:.*$/", "/^org\.jenkins-ci\.plugins:plugin$/" ] } ] } ================================================ FILE: .github/workflows/crowdin.yml ================================================ --- name: Crowdin on: push: branches: - master workflow_dispatch: permissions: actions: write contents: write pull-requests: write jobs: synchronize-with-crowdin: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: crowdin action uses: crowdin/github-action@v2 with: upload_translations: true download_translations: true skip_untranslated_files: true auto_approve_imported: true push_translations: true export_only_approved: true commit_message: 'New Crowdin translations' create_pull_request: true pull_request_title: 'Update localization' pull_request_labels: 'localization' base_url: 'https://jenkins.crowdin.com' config: 'crowdin.yml' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_PROJECT_ID: 35 CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} ================================================ FILE: .github/workflows/jenkins-security-scan.yml ================================================ name: Jenkins Security Scan on: push: branches: - master pull_request: types: [ opened, synchronize, reopened ] workflow_dispatch: permissions: security-events: write contents: read actions: read jobs: security-scan: uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2 with: java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default. ================================================ FILE: .github/workflows/release-drafter.yml ================================================ name: Release Drafter on: push: branches: - master jobs: update_release_draft: runs-on: ubuntu-latest steps: - uses: release-drafter/release-drafter@v7 with: token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/sonarcloud.yml ================================================ name: SonarCloud analysis on: push: branches: [ "master" ] tags: - 'jira*' pull_request: branches: [ "master" ] workflow_dispatch: permissions: pull-requests: read jobs: Analysis: runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v6 with: fetch-depth: 0 persist-credentials: false - uses: actions/setup-java@v5 with: java-version: '21' distribution: 'corretto' cache: 'maven' - name: Run the Maven verify phase env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: > mvn --batch-mode -Penable-jacoco -Dsonar.organization=jenkinsci -Dsonar.projectKey=jenkinsci_jira-plugin -Dsonar.qualitygate.wait test jacoco:report org.sonarsource.scanner.maven:sonar-maven-plugin:5.5.0.6356:sonar ================================================ FILE: .gitignore ================================================ .classpath .project .settings .factorypath build target bin work/ .work .env* .docker *.code-workspace # ignore all Idea files except for common codestyle settings .DS_Store *.iml .vscode/ .idea/* atlassian-ide-plugin.xml ================================================ FILE: .mvn/extensions.xml ================================================ io.jenkins.tools.incrementals git-changelist-maven-extension 1.13 ================================================ FILE: .mvn/maven.config ================================================ -Pconsume-incrementals -Pmight-produce-incrementals ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: check-yaml - id: check-added-large-files - repo: https://github.com/ORCID/pre-commit-spotless rev: v1.0.0 hooks: - id: spotless-check types_or: [java] - id: spotless-apply types_or: [java] ================================================ FILE: CONTRIBUTING.md ================================================ # Contribution guidelines General rules: - check the [general Jenkins development guide](https://www.jenkins.io/doc/developer/book/) - make sure to provide tests - when adding new fields, make sure to [include backward-compatibility](https://www.jenkins.io/doc/developer/persistence/backward-compatibility/) and tests for that - mark the Pull Request as _draft_ initially, to make sure all the checks pass correctly, then convert it to non-draft. ## Setting up your environment ### Install pre-commit hooks The [pre-commit](https://pre-commit.com/#install) hooks run various checks to make sure no unwanted files are committed and that the submitted change follows the code style and formatting rules: ```sh brew install pre-commit && pre-commit install --install-hooks ``` ## Notes for maintainers ### Local testing Use [docker-compose](./docker-compose.yml) to run a local Jenkins instance with the plugin installed. The configuration includes local volumes for both: Jenkins and ssh-agent, so you can easily test the plugin in a clean environment. ### Atlassian sources import To resolve [some binary compatibility issues](https://github.com/jenkinsci/jira-plugin/pull/140), the sources from the artifact [com.atlassian.httpclient:atlassian-httpclient-plugin:0.23](https://packages.atlassian.com/maven-external/com/atlassian/httpclient/atlassian-httpclient-plugin/0.23.0/) has been imported in the project to have control over http(s) protocol transport layer. The downloaded sources didn't have any license headers but based on the [pom](https://packages.atlassian.com/maven-external/com/atlassian/httpclient/atlassian-httpclient-plugin/0.23.0/atlassian-httpclient-plugin-0.23.0.pom) sources are Apache License (see pom in src/main/resources/atlassian-httpclient-plugin-0.23.0.pom) ### Testing There is a [Jira Cloud](https://jenkins-jira-plugin.atlassian.net/) test instance that the changes can be tested against, official maintainers are admins that can grant access for testing to PR submitters on a need-to-have basis. ### Releasing the plugin See [releasing Jenkins plugins](https://www.jenkins.io/doc/developer/publishing/releasing-manually/). ================================================ FILE: Jenkinsfile ================================================ /* See the documentation for more options: https://github.com/jenkins-infra/pipeline-library/ */ buildPlugin( forkCount: '1C', // run this number of tests in parallel for faster feedback. If the number terminates with a 'C', the value will be multiplied by the number of available CPU cores useContainerAgent: true, // Set to `false` if you need to use Docker for containerized tests configurations: [ [platform: 'linux', jdk: 25], [platform: 'windows', jdk: 21], ]) ================================================ FILE: LICENSE.md ================================================ The MIT License Copyright (c) 2007, Kohsuke Kawaguchi and jira-plugin contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Jenkins Jira Plugin [![Jenkins Version](https://img.shields.io/badge/Jenkins-2.479.3-green.svg?label=min.%20Jenkins)](https://jenkins.io/download/) [![Jenkins Plugin Installs](https://img.shields.io/jenkins/plugin/i/jira.svg?color=blue)](https://stats.jenkins.io/pluginversions/jira.html) [![GitHub release](https://img.shields.io/github/release/jenkinsci/jira-plugin.svg?label=Release)](https://github.com/jenkinsci/jira-plugin/releases/latest) [![Jenkins CI](https://ci.jenkins.io/buildStatus/icon?job=Plugins/jira-plugin/master)](https://ci.jenkins.io/job/Plugins/job/jira-plugin/) [![Contributors](https://img.shields.io/github/contributors/jenkinsci/jira-plugin.svg)](https://github.com/jenkinsci/jira-plugin/graphs/contributors) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=jenkinsci_jira-plugin&metric=bugs)](https://sonarcloud.io/summary/new_code?id=jenkinsci_jira-plugin) [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=jenkinsci_jira-plugin&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=jenkinsci_jira-plugin) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=jenkinsci_jira-plugin&metric=coverage)](https://sonarcloud.io/summary/new_code?id=jenkinsci_jira-plugin) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=jenkinsci_jira-plugin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=jenkinsci_jira-plugin) 1. See user documentation at [https://jenkinsci.github.io/jira-plugin/](https://jenkinsci.github.io/jira-plugin/). 1. Use [Declarative pipelines](https://www.jenkins.io/doc/book/pipeline/#declarative-versus-scripted-pipeline-syntax). 1. Check [jira plugin steps reference](https://www.jenkins.io/doc/pipeline/steps/jira/). ## i18n [![da translation](https://img.shields.io/badge/dynamic/json?color=blue&label=da&style=flat&logo=crowdin&query=%24.progress.0.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200016380-35.json)](https://jenkins.crowdin.com/jira-plugin) [![de translation](https://img.shields.io/badge/dynamic/json?color=blue&label=de&style=flat&logo=crowdin&query=%24.progress.1.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200016380-35.json)](https://jenkins.crowdin.com/jira-plugin) [![en translation](https://img.shields.io/badge/dynamic/json?color=blue&label=en&style=flat&logo=crowdin&query=%24.progress.2.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200016380-35.json)](https://jenkins.crowdin.com/jira-plugin) [![es translation](https://img.shields.io/badge/dynamic/json?color=blue&label=es&style=flat&logo=crowdin&query=%24.progress.3.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200016380-35.json)](https://jenkins.crowdin.com/jira-plugin) [![fr translation](https://img.shields.io/badge/dynamic/json?color=blue&label=fr&style=flat&logo=crowdin&query=%24.progress.4.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200016380-35.json)](https://jenkins.crowdin.com/jira-plugin) [![it translation](https://img.shields.io/badge/dynamic/json?color=blue&label=it&style=flat&logo=crowdin&query=%24.progress.5.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200016380-35.json)](https://jenkins.crowdin.com/jira-plugin) [![ja translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ja&style=flat&logo=crowdin&query=%24.progress.6.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200016380-35.json)](https://jenkins.crowdin.com/jira-plugin) [![pl translation](https://img.shields.io/badge/dynamic/json?color=blue&label=pl&style=flat&logo=crowdin&query=%24.progress.7.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200016380-35.json)](https://jenkins.crowdin.com/jira-plugin) This plugin uses [CrowdIn platform](https://jenkins.crowdin.com/jira-plugin) as the frontend to manage translations. If you would like to contribute translation of this plugin in your language, you're most welcome! For details, see [jenkins.io CrowdIn introduction](https://www.jenkins.io/doc/developer/crowdin/translating-plugins/). ================================================ FILE: crowdin.yml ================================================ --- files: - source: '/src/main/resources/hudson/plugins/jira/**/*.properties' ignore: - '/src/main/resources/hudson/plugins/jira/**/%file_name%_%two_letters_code%.properties' translation: '/src/main/resources/hudson/plugins/jira/**/%file_name%_%two_letters_code%.properties' escape_quotes: 0 escape_special_characters: 0 project_id_env: CROWDIN_PROJECT_ID api_token_env: CROWDIN_PERSONAL_TOKEN ================================================ FILE: docker-compose.yml ================================================ # docker compose up -d --build --force-recreate services: jenkins: image: jenkins/jenkins:lts restart: on-failure hostname: jenkins ports: - "8080:8080" - "50000:50000" volumes: - .docker/jenkins-data:/var/jenkins_home:rw - ./casc.d:/var/jenkins_home/casc.d/:ro # Mounting the ssh private key as "container secret" makes it available in JCasc as the variable ${SSH_AGENT_KEY} - ./secrets/id_jenkins.pem:/run/secrets/SSH_AGENT_KEY:ro environment: - JENKINS_EXT_URL=http://localhost:8080 - CASC_JENKINS_CONFIG=/var/jenkins_home/casc.d/ # - org.jenkinsci.plugins.durabletask.BourneShellScript.LAUNCH_DIAGNOSTICS=true # - JENKINS_OPTS=-Djenkins.install.runSetupWizard=false jenkins-agent: image: jenkins/ssh-agent:latest-jdk21 restart: on-failure hostname: agent #privileged: true depends_on: - jenkins volumes: - .docker/jenkins-agent-data:/home/jenkins:rw # - /var/run/docker.sock:/var/run/docker.sock:rw environment: - JENKINS_AGENT_SSH_PUBKEY=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBpNqXQ4x7fPPUBbYPxKF77Zqq6d35iPCD2chg644OUD noreply@jenkinsagent.local volumes: jenkins-data: jenkins-agent-data: ================================================ FILE: docs/.nojekyll ================================================ ================================================ FILE: docs/README.md ================================================ # Jenkins Jira plugin This plugin integrates with Jenkins the [Atlassian Jira Software](http://www.atlassian.com/software/jira/) (both Cloud and Server versions). ## Configuration !> **Jira Cloud** does not support Bearer Authentication To integrate Jenkins with Atlassian Jira Cloud, you need to use an API token as a _service user_. Jira Cloud requires an email address for all users, so you cannot create a user without one. ### Steps 1. **Create an API Token** Follow the [Atlassian API tokens documentation](https://confluence.atlassian.com/cloud/api-tokens-938839638.html) to generate a new API token. 2. **Add a Global Jenkins Credential** - **Username:** Your Atlassian ID email address - **Password:** The API token you created 3. **Test Your API Token** Verify your API token by running the following command (replace ``, ``, ``, and `TEST-1` with your details): ```bash curl -X GET -u : -H "Content-Type: application/json" \ https://.atlassian.net/rest/api/latest/issue/TEST-1 ``` A successful response returns the issue details in JSON format. 4. **Check for CAPTCHA** Ensure that CAPTCHA is **not** triggered for your user, as this will prevent the API token from working. For more information, see the [CAPTCHA section in Atlassian REST API documentation](https://developer.atlassian.com/cloud/jira/platform/jira-rest-api-basic-authentication/). 5. **Test Connection** Finally, use the **Validate Settings** button on the plugin configuration page, to see if it can connect to the Jira instance. ![plugin-configuration](images/Plugin_Configuration.png) ## Something doesn't work? First, check [Github Issues](https://github.com/jenkinsci/jira-plugin/issues) for already reported bugs. Then, Contribute or Sponsor! We all love Open Source, but... Open Source Software relies on contributions of fellow developers. Please contribute by [opening Pull Requests](#contributing) or if you are not a developer, consider sponsoring one of the maintainers - see ["Sponsor this project" section.](https://github.com/jenkinsci/jira-plugin) ================================================ FILE: docs/_sidebar.md ================================================ * [Home](/) * [Features](features.md) * [System Properties](system-properties.md) * [Usage Examples](usage-examples.md) * [Troubleshooting](troubleshooting.md) * [Changelog](changelog.md) ================================================ FILE: docs/changelog.md ================================================ Changelog === ### Newer versions See [GitHub releases](https://github.com/jenkinsci/jira-plugin/releases) ### Unreleased Release date: _March 2, 2020_ * Changed all references of JIRA to Jira per [Atlassian branding updates](https://community.atlassian.com/t5/Feedback-Forum-articles/A-new-look-for-Atlassian/ba-p/638077) * JiraCreateIssueNotifier: regard all statuses in "done" category as finished ### 3.0.11 Release date: _Nov 21, 2019_ * Fix security issue ### 3.0.10 Release date: _Sep 26, 2019_ * dependencies cleanup (remove dependency on org.codehaus.jackson:*) ### 3.0.9 Release date: _Aug 21, 2019_ * JENKINS-59001 IssueSelectorStep should handle NullPointerException caused by having no configured sites * JENKINS-57899 Jira site configuration lost after a restart ### 3.0.8 Release date: _Jun 28, 2019_ * JENKINS-58244 JIRA Site at folder level doesn't show credentials for non-admin users * JENKINS-57664 On Release of a Version recieve a Null Pointer exception ### 3.0.7 Release date: _May 1, 2019_ * JENKINS-56951 JIRA plugin config incorrectly mixed with properties section * JENKINS-52906 jira-plugin: FAILED TO EXPORT hudson.plugins.jira.JiraProjectProperty * JENKINS-33222 Concurrent builds are blocking with jira-plugin * JENKINS-19195 Add description field to Release JIRA version action ### 3.0.6 Release date: _March 30, 2019_ * JENKINS-56810 Add current date in startDate when creating new version * JENKINS-56697 Simplify constructor, main URL is the only mandatory field * JENKINS-50643 Jira Plugin v2.4.2 leaks selectors resulting in Too Many Open Files ## 3.0.5 Release date: _Jul 7, 2018_ * JENKINS-54469 Unable to set Thread Executor Size when configuring Jira Plugin * JENKINS-54131 "Failed to parse changelog" in JIRA plugin 3.0.3 * JENKINS-54116 Jenkins Jira Plugin - Unable to add version ### 3.0.4 Release date: _Oct 26, 2018_ * JENKINS-54144 Job fails as JiraSCMListsener/JiraSite potentially creating Executor with 0 threads ### 3.0.3 Release date: _Oct 16, 2018_ * JENKINS-54042 Fix some misconfiguration between connect time and read timeout * JENKINS-53808 Binary compatibility broken between JIRA plugin 3.0.2 and Artifactory plugin 2.16.2 * JENKINS-53642 Configuration for "Add timestamp to JIRA comments" not observed/remembered ### 3.0.2 Release date: _Sep 25, 2018_ * SECURITY-1029 - CSRF vulnerability and missing permission checks in Jira Plugin allowed capturing credentials ### 3.0.1 Release date: _Aug 22, 2018_ * JENKINS-54093 Jenkins Jira Plugin sets connection timeout to default = 10 after Jenkins restart * JENKINS-53150 Remove Perforce Plugin dependency * JENKINS-51164 JIRA plugin doesn't honor proxy excludes * JENKINS-45789 Use Credentials Plugin for JIRA Global Configuration User ### 3.0.0 Release date: _May 20, 2018_ * JENKINS-51312 Jira plugin core 2.60.1 * JENKINS-51310 Update JIRA plugin to use jackson2-api-plugin ### 2.5.2 Release date: _May 4, 2018_ * JENKINS-49975 BasicHttpCache error with JIRA Plugin 2.5.1 * JENKINS-49231 jira-plugin 2.5.1 throws exception fails build * JENKINS-48357 Binary Compatibility between JIRA Plugin and Apache HttpComponents Client 4.x API * JENKINS-25829 Proxy configuration does not work ### 2.4.2 Release date: _Aug 8, 2017_ * JENKINS-45992 Cannot validate JIRA site settings ### Version 2.4 Release date: _Aug 3, 2017_ * JENKINS-44524 Support adding JIRA sites on folder ### 2.3.1 Release date: _May 23, 2017_ * JENKINS-40571 JiraVersionCreatorBuilder fields missing in post-build form ### 2.3 Release date: _Dec 19, 2016_ * JENKINS-39192 Support for updating custom fields * JENKINS-39091 Jira plugin fails to log in to Jira site after dependency updates * JENKINS-38142 Possibility to specify JIRA site in jenkins pipeline project * JENKINS-36726 Plugin comments on subsequent successful builds * JENKINS-35998 No issue update if at least one issue does not exists * JENKINS-34661 Bump LTS to 1.642.3 * JENKINS-33996 Issue selector step improvement * JENKINS-33859 NPE when well-formed Jira Issue doesn't exist * JENKINS-32602 jira-plugin missing License * JENKINS-32492 Rename all dropdown labels to include JIRA: prefix * JENKINS-32491 Rename Workflow Plugin to Pipeline * JENKINS-31164 Issue Type must be selectable from fetched dropdown * JENKINS-24207 Use Perforce Jobs attached to changelist to track JIRA issues * JENKINS-19286 Support multiple fix versions ### 2.2.1 Release date: _Mar 26, 2016_ * JENKINS-33293 (Jira) Updater throws NullPointerException for labels * JENKINS-33211 NullPointerException in JiraVersionParameterValue.java ### 2.2 Release date: _Feb 20, 2016_ * Split each SCM changes in paragraphs * support release candidates (RCs) via Maven ComparableVersion * Console logging improvements in various places * JiraEnvironmentVariableBuilder support * Support adding labels to updated issues * (optionally) add scm entry change date and time to description in JIRA tickets * JENKINS-32504 Make JiraEnvironmentVariableBuilder compatible with pipeline * JENKINS-32276 JIRA Release Version Parameter is truncating list of Jira versions * JENKINS-32170 Support CLI parameter submission for JiraIssueParameterDefinition * JENKINS-32106 Issue type "UNKNOWN" in Release Notes * JENKINS-31268 @Exported returns double XML value for getter * JENKINS-31113 Configurable HTTP timeout parameter for JiraRestService ### 2.1 Release date: _Now 18, 2015_ * Bumped Jenkins Core to LTS v. 1.609.3 * Added dependencies: mailer-plugin, matrix-plugin * Removed dependencies: maven-plugin * JENKINS-32949 Issue with JIRA plugins in JENKINS * JENKINS-31626 Expand JIRA Project Key variable in other build tasks * JENKINS-31349 Jira configuration - Validate Settings doesn't validate username/password * JENKINS-30829 JIRA Generate Release Notes needs default Environment Variable * JENKINS-30305 Allow CLI parameter submission for JiraVersionParameterDefinition * JENKINS-26701 Jira Plugin: I need sorting option in "JIRA Release Version Parameter". Is it possible? * JENKINS-25828 JIRA version name/value not picked up by remote API * JENKINS-17156 If Updater fails to update due to missing permission, it crashes and never flushes the comment queue * JENKINS-13436 Message logged by JIRA plugin should mention that the message relates to a Jenkins job. * JENKINS-12578 Jira issue parameter value is not exposed via remote API * JENKINS-3709 Jira login information should be scrambled in the configuration file ### 2.0.3 Release date: _Oct 26, 2015_ * JENKINS-30682 Ticket creation fails when no components in JIRA project exist / JIRA rejects empty component list * JENKINS-30408 JIRA REST API requests lead to 404 (not found) * JENKINS-30333 Thread Leak due to use of deprecated JiraSize.createSession ### 2.0.1 Release date: _Sep 10, 2015_ * JENKINS-30242 Update to 1.13.2 breaks global configuration submission when jira-plugin installed ### 2.0 Release date: _Sep 2, 2015_ * switch from JIRA RPC SOAP to JIRA REST API communication - the former has been deprecated and dropped since JIRA v.7.0. * JENKINS-23257 Non well-formed response from JIRA error is hard to diagnose * JENKINS-18227 Add support for Atlassian OnDemand JIRA * JENKINS-18166 Add support for JIRA REST API - JIRA SOAP API will be removed in JIRA 7 * JENKINS-10223 This is a valid URL but it doesn't look like JIRA ### 1.41 Release date: _Jun 10, 2015_ * JENKINS-22628 Change comments/description to use String.format() instead. * JENKINS-21776 Jenkins jira comment text typo * JENKINS-20528 Unable to link to Jira * JENKINS-9549 JIRA plugin does not create links to JIRA repository * JENKINS-1904 Can't get issue links generated with out user/password ### Version 1.39 Release date: _Oct 6, 2013_ * Ability only to comment issue without processing of workflow (pull #38) ### 1.38 Release date: _Aug 23, 2013_ * Post build step to create new JIRA version (pull #30) ### 1.37 Release date: _Jun 21, 2013_ * Error with empty alternative url issue #18229 ### 1.35 Release date: _Jul 29, 2012_ * Prevents multiple comments on one issue for matrix builds. (PR #13) ### 1.34 Release date: _Jun 11, 2012_ * Fix NPE when Jenkins user does not have access to perform any workflow actions JENKINS-13998 ### 1.33 Release date: _Jun 1, 2012_ * Support workflow steps as build actions and/or post-build notifiers JENKINS-13652 ### 1.32 Release date: _May 15, 2012_ * Option to show archived versions. ### 1.31 Release date: _May 1, 2012_ * Add JiraIssueMigrator - a post build action that will move issues to a new fixVersion based on a JQL query. * Add Additional filtering of issues to be included in the release notes. Defaults to 'status in (Resolved, Closed)' ### 1.30 Release date: _April 25, 2012_ * Add build parameter that providers a drop-down with JIRA release versions * Add a build wrapper that will assemble release notes based on issues in the release version and store it in an environment variable * JENKINS-123 Issue summary * JENKINS-124 Another Issue summary * JENKINS-321 Yet another issue summary * Add a post-build action that will mark a version as released in JIRA ### 1.29 Release date: _August 25, 2011_ * JENKINS-10817 Jira-plugin should add the overall build result to the issue's comment * Include revisions also for non-subversion plugins; include revisions also if we don't have a repository browser * Defined a new parameter type for parameterized builds that allow you to select a JIRA ticket (from the result of a JQL query) ### 1.28 Release date: _Jun 15, 2011_ * Improve the form validation error check JENKINS-9625 * Supported security level of the comment JENKINS-1489 ### 1.27 Release date: _Feb 27, 2011_ * Updates for Jenkins ### 1.26 Release date: _Jan 14, 2011_ * JENKINS-2508 : JIRA plugin not updating JIRA when perforce plugin used. ### 1.25 * JENKINS-6758: Failed to save system settings with JIRA Plugin. ### 1.24 * JENKINS-6462: Version 1.355 of Hudson and Jira Plugin 1.21: Images in Jira comments are not showing up. ### Version 1.23 * JENKINS-6264, JENKINS-6282: IndexOutOfBoundsException when no issue pattern is configured (default pattern wasn't used) * JENKINS-6381: configured patterned wasn't used for changelog annotation. Default pattern was always used for that. * improved default pattern to not match commit messages with dots in the number part (like 'projectname-1.2.3'). These messages are e.g. used by the Maven release plugin ### Version 1.22 * JENKINS-6043: Issue pattern can be configurable * JENKINS-6225: option to update jira issue whatever the build result is (even if failed) ### 1.21 * JENKINS-5989: option to record scm changes in jira. * JENKINS-6007: Added French localization. ### 1.20 * Added Japanese localization (JENKINS-5788) ### 1.19 * Fix: Prevent carrying forward invalid issue ids forever ### 1.18 * Case insensitive matching of JIRA ids also in the 'recent changes' view (JENKINS-4132) * fetch missing details for JIRA issues - i.e. completes issue title tooltip in 'recent changes' view (JENKINS-5252) * prevent build FAILURE if JIRA site is not available (JENKINS-3046) ### 1.17 * Fixed an ArrayIndexOutOfBoundsException when JIRA issues contain '$' in the name. * Support underscore in project names (JENKINS-4092) * Support digits in project names (JENKINS-729) * Case insensitive matching of JIRA ids (JENKINS-4132) * Don't strip JIRA id from posted comment * German translation ### 1.15 Release date: _Aug 22, 2008_ * Update JIRA if the build is UNSTABLE or better. Previously only updated if the build was stable. * Include relevant SCM comment in the JIRA comment which should make JIRA ticket history more meaningful. ### 1.13 Release date: _Auh 5, 2008_ * Fixed a performance issue in a large enterprise deployment of JIRA (JENKINS-1703) ### Version 1.12 * A typo in the commit message shouldn't break builds (JENKINS-1593) * Postpone JIRA updates until a successful build is obtained (JENKINS-506) ### Version 1.11 * Added more logging and debug flag to examine issues that people are reporting (report) ### Version 1.10 * Wiki-style notation option wasn't persisted (JENKINS-977) * Fixed a packaging problem (JENKINS-1127) ### 1.9 * Fixed NPE when failed to talk to JIRA (JENKINS-1097) ### 1.8 * Be more graceful in dealing with URLs (JENKINS-896) * URLs need to be escaped (JENKINS-943) ### 1.7 * Fixed NPE when username/password is not set (JENKINS-828) ### 1.6 * Relaxed the JIRA project key regexp a little bit to allow numbers (JENKINS-729) ### 1.5 * Issue hyperlinking is now smart enough not to be confused by strings that look like JIRA issue that actually aren't. ### 1.4 * Fixed a bug that prevented tooltips for JIRA issues from being displayed JENKINS-694 ================================================ FILE: docs/features.md ================================================ # Jenkins Jira plugin features ### Using Jira REST API This plugin has an optional feature to update Jira issues with a back pointer to Jenkins build pages. This allows the submitter and watchers to quickly find out which build they need to pick up to get the fix. ![plugin-configuration](images/Plugin_Configuration.png) ### Jira Issue links in build Changelog When you configure your Jira site in Jenkins, the plugin will automatically hyperlink all matching issue names to Jira. If you have additionally provided username/password to Jira, the hyperlinks will also contain tooltips with the issue summary. ![example-annotated-changelog](images/example_annotated_changelog.png) ### Updating Jira issues with back pointers If you also want to use this feature, you need to supply a valid user id/password. If you need the comment only to be visible to a certain Jira group, e.g. _Software Development_, enter the groupname. Now you also need to configure jobs. I figured you might not always have write access to the Jira (say you have a Jenkins build for one of the Apache commons project that you depend on), so that's why this is optional. The following screen shows how a Jira issue is updated: ![jira-comments](images/Jira_Comments.jpg) By taking advantages of Jenkins' [fingerprint](https://wiki.jenkins.io/display/JENKINS/Fingerprint) feature, when your other projects that depend on this project pick up a build with a fix, those build numbers can also be recorded in Jira. This is quite handy when a bug is fixed in one of the libraries, yet the submitter wants a fix in a different project. This happens often in my work, where a bug is reported against JAX-WS but the fix is in JAXB. For curious mind, see [this thread for how this works behind the scene](http://jenkins.361315.n4.nabble.com/How-can-does-Hudson-Jira-integration-works-td374680.html). ### Referencing Jira Release version To reference Jira Release versions in your build, you can pull these releases directly from Jira by adding the Jira Release Version Parameter. This can be useful for generating release notes, trigerring parameterized build, etc. ![version-parameters](images/version_parameters.png) ### Generating Release Notes You can also generate release notes to be used during your build. These notes can be retrieved from an environment variable. See the [Maven Project Plugin](https://wiki.jenkins.io/display/JENKINS/Maven+Project+Plugin) for the environment variables found within the POM. ![release-notes](images/release_notes.png) After your build has run, you can also have the plugin mark a release as resolved. This typically will be a release you specified in your Build Parameters. ![marking-as-resolved](images/mark_as_resolved.png) The plugin can also move certain issues matching a JQL query to a new release version. ![moving-issues](images/move_issues.png) Sample usage of generated Release Notes: ![release-notes-config](images/release_notes_config.png) ### Jira Authentication & Permissions required **Note:** As a rule of thumb, **you should be always using a service account** (instead of a personal account) to integrate Jenkins with Jira. Make sure that the Jira user used by Jenkins has enough permissions to execute its actions. You can do that via Jira Permission Helper tool. - For creating Jira issues, the user has to be able to Create Issues in the specified project - If you additionally enter assignee or component field values, make sure that: - both of the fields are assigned to the corresponding Jira Screen - the Jira user is Assignable in the project - the Jenkins Jira user can Assign issues ================================================ FILE: docs/index.html ================================================ Document
================================================ FILE: docs/system-properties.md ================================================ # System Properties There are some cases when you might want to change the plugin's behavior globally, by overriding [Jenkins system properties](https://www.jenkins.io/doc/book/managing/system-properties/). This plugin provides the following additional settings, that are not available via UI: - `-Dhudson.plugins.jira.JiraMailAddressResolver.disabled=true` Use to disable resolving user email from Jira usernames. ================================================ FILE: docs/troubleshooting.md ================================================ # Common issues ## Adding a custom Log Recorder To help debug any issues with this plugin, it's useful to look at more detailed logs. To enable them, create a [custom Log Recorder](https://www.jenkins.io/doc/book/system-administration/viewing-logs/#logs-in-jenkins) for Logger `hudson.plugins.jira` and Log Level `FINE`. ## Jenkins <---> Jira SSL connectivity problems If you encounter stacktrace like this: ```stacktrace Caused by: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target ``` make sure the JRE/JDK that Jenkins master is running (or the Jenkins slaves are running) contain the valid CA chain certificates that Jira is running with. You can test it using this [SSLPoke.java class](https://gist.github.com/warden/e4ef13ea60f24d458405613be4ddbc51): ```sh $ wget -O SSLPoke.java https://gist.githubusercontent.com/warden/e4ef13ea60f24d458405613be4ddbc51/raw/7f258a30be4ddea7b67239b40ae305f6a2e98e0a/SSLPoke.java $ /usr/java/jdk1.8.0_131/bin/javac SSLPoke.java $ /usr/java/jdk1.8.0_131/jre/bin/java SSLPoke jira.domain.com 443 Successfully connected ``` References: - [Jenkins fails with PKIX Path building error](https://stackoverflow.com/questions/52842214/jenkins-fails-with-pkix-path-building-error) - [PKIX path building failed error message ](https://support.cloudbees.com/hc/en-us/articles/217078498-PKIX-path-building-failed-error-message) ================================================ FILE: docs/usage-examples.md ================================================ # Usage examples ## jiraCommentIssues usage example You need keep reference to used scm. As an example, you can write a flow: ```groovy node { def gitScm = git url: 'git@github.com:jenkinsci/jira-plugin.git', branch: 'master' sh 'make something' jiraCommentIssues( issueSelector: DefaultSelector(), scm: gitScm) gitScm = null } ``` Note that a pointer to scm class should be better cleared to not serialize scm object between steps. You can add some labels to issue in jira: ```groovy jiraCommentIssues( issueSelector: DefaultSelector(), scm: gitScm, labels: [ "$version", "jenkins" ]) ``` ## jiraExecuteWorkflow usage example ```groovy node { jiraExecuteWorkflow( jqlSearch: "project = EX and labels = 'jenkins' and labels = '${version}'", workflowActionName: 'Resolve Issue', comment: 'comment') } ``` ## jiraCreateReleaseNotes usage example ```groovy node { jiraCreateReleaseNotes(jiraProjectKey: 'TST', jiraRelease: '1.1.1', jiraEnvironmentVariable: 'notes', jiraFilter: 'status in (Resolved, Closed)') { //do some useful here //release notes can be found in environment variable jiraEnvironmentVariable print env.notes } } ``` ## jiraMarkVersionReleased usage example ```groovy node { jiraMarkVersionReleased( jiraProjectKey: 'TEST', jiraRelease: '1.1.1') } ``` ## jiraUpdateIssueField usage example ```groovy node { jiraUpdateIssueField( issueSelector: ExplicitSelector("JIRA-123"), fieldId: "10001", fieldValue: "value" ) } ``` ## SearchIssuesStep Custom pipeline step (see [step-api](https://github.com/jenkinsci/workflow-plugin/blob/master/step-api/README.md)) that allow to search by jql query directly from workflow. usage: ```groovy node { List issueKeys = jiraSearch(jql: "project = EX and labels = 'jenkins' and labels = '${version}'") } ``` ## CommentStep Interface for Pipeline job types that simply want to post a comment e.g.: ```groovy node { jiraComment(issueKey: "EX-111", body: "Job '${env.JOB_NAME}' (${env.BUILD_NUMBER}) built. Please go to ${env.BUILD_URL}.") } ``` ## JiraEnvironmentVariableBuilder Not supported in Pipeline. You can get current jira url (if you are not using the Groovy sandbox): ```groovy import hudson.plugins.jira.JiraSite; node { String jiraUrl = JiraSite.get(currentBuild.rawBuild).name env.JIRA_URL = jiraUrl } ``` To replace JIRA_ISSUES env variable, you can use pipeline step jiraIssueSelector: ```groovy List issueKeys = jiraIssueSelector() ``` or if you use custom issue selector: ```groovy List issueKeys = jiraIssueSelector(new CustomIssueSelector()) ``` ## Other features Some features are currently not supported in pipeline. If you are adding new features please make sure that they support Jenkins pipeline Plugin. See [here](https://github.com/jenkinsci/workflow-plugin/blob/master/COMPATIBILITY.md) for some information. See [here](https://github.com/jenkinsci/workflow-plugin/blob/master/basic-steps/CORE-STEPS.md) for more information how core jenkins steps integrate with workflow jobs. Running a notifiers is trickier since normally a flow in progress has no status yet, unlike a freestyle project whose status is determined before the notifier is called (never supported). So notifiers will never be implemented as you can use the catchError step and run jira action manually. I'm going to create a special pipeline steps to replace this notifiers in future. ================================================ FILE: pom.xml ================================================ 4.0.0 org.jenkins-ci.plugins plugin 6.2138.v03274d462c13 jira ${revision}${changelist} hpi Jenkins Jira plugin Integrates Jenkins to Jira https://github.com/jenkinsci/${project.artifactId}-plugin The MIT license https://github.com/jenkinsci/jira-plugin/raw/master/LICENSE.md repo olamy Olivier Lamy warden Radek Antoniuk scm:git:https://github.com/${gitHubRepo}.git scm:git:git@github.com:${gitHubRepo}.git jira-3.20 https://github.com/${gitHubRepo} Github https://github.com/jenkinsci/jira-plugin/issues 3.22 -SNAPSHOT jenkinsci/${project.artifactId}-plugin atlassian-event,atlassian-httpclient-api,atlassian-util-concurrent,fugue,jira-rest-java-client-api,jira-rest-java-client-core,maven-artifact,plexus-utils,sal-api true 6.0.2 4.7.2 2.492 ${jenkins.baseline}.3 false false false io.jenkins.tools.bom bom-${jenkins.baseline}.x 5473.vb_9533d9e5d88 pom import com.atlassian.plugins atlassian-plugins-core 8.2.1 com.atlassian.jira jira-rest-java-client-api ${jira-rest-client.version} com.google.code.findbugs jsr305 com.google.guava guava joda-time joda-time com.atlassian.jira jira-rest-java-client-core ${jira-rest-client.version} com.atlassian.httpclient atlassian-httpclient-library com.atlassian.httpclient atlassian-httpclient-plugin com.google.code.findbugs jsr305 com.google.guava guava commons-codec commons-codec joda-time joda-time org.apache.commons commons-lang3 org.apache.httpcomponents httpasyncclient org.apache.httpcomponents httpasyncclient-cache org.apache.httpcomponents httpclient org.apache.httpcomponents httpclient-cache org.apache.httpcomponents httpcore org.apache.httpcomponents httpcore-nio org.apache.httpcomponents httpmime org.codehaus.jettison jettison org.glassfish.jersey.core jersey-client org.glassfish.jersey.core jersey-common org.glassfish.jersey.media jersey-media-jaxb org.glassfish.jersey.media jersey-media-json-jettison org.slf4j slf4j-api org.springframework spring-beans org.springframework spring-core org.springframework spring-jcl io.atlassian.fugue fugue ${fugue.version} io.atlassian.util.concurrent atlassian-util-concurrent 4.1.0 io.jenkins.plugins caffeine-api io.jenkins.plugins commons-collections4-api 4.5.0-8.va_d5448ef9011 io.jenkins.plugins commons-lang3-api io.jenkins.plugins jersey2-api io.jenkins.plugins joda-time-api io.jenkins.plugins json-api org.apache.maven maven-artifact 3.9.14 org.apache.commons commons-lang3 org.jenkins-ci.plugins apache-httpcomponents-client-4-api org.jenkins-ci.plugins branch-api org.jenkins-ci.plugins credentials org.jenkins-ci.plugins jackson2-api org.jenkins-ci.plugins mailer org.jenkins-ci.plugins matrix-project org.jenkins-ci.plugins p4 1.17.1 true org.apache.commons commons-compress org.jenkins-ci.plugins * org.jenkins-ci.plugins.workflow workflow-job org.jenkins-ci.plugins.workflow workflow-step-api true io.jenkins.configuration-as-code test-harness test org.jenkins-ci.plugins command-launcher test org.jenkins-ci.plugins.workflow workflow-basic-steps test org.jenkins-ci.plugins.workflow workflow-multibranch test org.jenkins-ci.plugins.workflow workflow-scm-step test org.jenkins-ci.plugins.workflow workflow-step-api tests test org.mockito mockito-core test org.mockito mockito-junit-jupiter test true false repo.jenkins-ci.org https://repo.jenkins-ci.org/public/ true false repo.jenkins-ci.org https://repo.jenkins-ci.org/public/ org.eluder.coveralls coveralls-maven-plugin 4.3.0 javax.xml.bind jaxb-api 2.3.1 com.github.spotbugs spotbugs-maven-plugin spotbugs check verify ${spotbugs.failOnError} ${spotbugs.effort} ${spotbugs.threshold} hudson.plugins.jira.* org.apache.maven.plugins maven-surefire-plugin false hudson.plugins.jira.JiraFolderProperty,hudson.plugins.jira.JiraSite,hudson.plugins.jira.JiraJobAction,com.atlassian.jira.rest.client.api.domain.IssueType ================================================ FILE: src/main/java/com/atlassian/httpclient/apache/httpcomponents/ApacheAsyncHttpClient.java ================================================ package com.atlassian.httpclient.apache.httpcomponents; import static io.atlassian.util.concurrent.Promises.rejected; import static java.lang.String.format; import com.atlassian.event.api.EventPublisher; import com.atlassian.httpclient.api.DefaultResponseTransformation; import com.atlassian.httpclient.api.HttpClient; import com.atlassian.httpclient.api.HttpStatus; import com.atlassian.httpclient.api.Request; import com.atlassian.httpclient.api.Response; import com.atlassian.httpclient.api.ResponsePromise; import com.atlassian.httpclient.api.ResponsePromises; import com.atlassian.httpclient.api.ResponseTransformation; import com.atlassian.httpclient.api.factory.HttpClientOptions; import com.atlassian.httpclient.base.event.HttpRequestCompletedEvent; import com.atlassian.httpclient.base.event.HttpRequestFailedEvent; import com.atlassian.sal.api.ApplicationProperties; import com.atlassian.sal.api.executor.ThreadLocalContextManager; import hudson.ProxyConfiguration; import io.atlassian.util.concurrent.ThreadFactories; import java.io.IOException; import java.net.URI; import java.security.GeneralSecurityException; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.X509Certificate; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Pattern; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import jenkins.model.Jenkins; import org.apache.commons.lang3.StringUtils; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpException; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.StatusLine; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.config.CookieSpecs; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpHead; import org.apache.http.client.methods.HttpOptions; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.client.methods.HttpTrace; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.ssl.SSLContextBuilder; import org.apache.http.conn.ssl.TrustSelfSignedStrategy; import org.apache.http.conn.ssl.X509HostnameVerifier; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.ProxyAuthenticationStrategy; import org.apache.http.impl.conn.DefaultRoutePlanner; import org.apache.http.impl.conn.DefaultSchemePortResolver; import org.apache.http.impl.conn.SystemDefaultDnsResolver; import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; import org.apache.http.impl.nio.client.HttpAsyncClients; import org.apache.http.impl.nio.conn.ManagedNHttpClientConnectionFactory; import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager; import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor; import org.apache.http.impl.nio.reactor.IOReactorConfig; import org.apache.http.nio.conn.NoopIOSessionStrategy; import org.apache.http.nio.conn.SchemeIOSessionStrategy; import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy; import org.apache.http.nio.reactor.IOReactorException; import org.apache.http.nio.reactor.IOReactorExceptionHandler; import org.apache.http.protocol.BasicHttpContext; import org.apache.http.protocol.HttpContext; import org.apache.http.util.Args; import org.apache.http.util.TextUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; public final class ApacheAsyncHttpClient implements HttpClient, DisposableBean { private final Logger log = LoggerFactory.getLogger(this.getClass()); private static final Supplier httpClientVersion = () -> MavenUtils.getVersion("com.atlassian.httpclient", "atlassian-httpclient-api"); private final Function eventConsumer; private final Supplier applicationName; private final ThreadLocalContextManager threadLocalContextManager; private final ExecutorService callbackExecutor; private final HttpClientOptions httpClientOptions; private final CloseableHttpAsyncClient nonCachingHttpClient; public ApacheAsyncHttpClient( EventPublisher eventConsumer, ApplicationProperties applicationProperties, ThreadLocalContextManager threadLocalContextManager) { this(eventConsumer, applicationProperties, threadLocalContextManager, new HttpClientOptions()); } public ApacheAsyncHttpClient( EventPublisher eventConsumer, ApplicationProperties applicationProperties, ThreadLocalContextManager threadLocalContextManager, HttpClientOptions options) { this( new DefaultApplicationNameSupplier(applicationProperties), new EventConsumerFunction(eventConsumer), threadLocalContextManager, options); } public ApacheAsyncHttpClient( final Supplier applicationName, final Function eventConsumer, final ThreadLocalContextManager threadLocalContextManager, final HttpClientOptions options) { this.eventConsumer = Objects.requireNonNull(eventConsumer); this.applicationName = Objects.requireNonNull(applicationName); this.threadLocalContextManager = Objects.requireNonNull(threadLocalContextManager); this.httpClientOptions = Objects.requireNonNull(options); try { final HttpAsyncClientBuilder clientBuilder = createClientBuilder(); this.nonCachingHttpClient = clientBuilder.build(); this.callbackExecutor = options.getCallbackExecutor(); nonCachingHttpClient.start(); } catch (IOReactorException e) { throw new RuntimeException("Reactor " + options.getThreadPrefix() + "not set up correctly", e); } } private HttpAsyncClientBuilder createClientBuilder() throws IOReactorException { final HttpClientOptions options = httpClientOptions; final IOReactorConfig reactorConfig = IOReactorConfig.custom() .setIoThreadCount(options.getIoThreadCount()) .setSelectInterval(options.getIoSelectInterval()) .setInterestOpQueued(true) .build(); final DefaultConnectingIOReactor ioReactor = new DefaultConnectingIOReactor(reactorConfig); ioReactor.setExceptionHandler(new IOReactorExceptionHandler() { @Override public boolean handle(final IOException e) { log.error("IO exception in reactor ", e); return false; } @Override public boolean handle(final RuntimeException e) { log.error("Fatal runtime error", e); return false; } }); final PoolingNHttpClientConnectionManager connectionManager = new PoolingNHttpClientConnectionManager( ioReactor, ManagedNHttpClientConnectionFactory.INSTANCE, getRegistry(options), DefaultSchemePortResolver.INSTANCE, SystemDefaultDnsResolver.INSTANCE, options.getConnectionPoolTimeToLive(), TimeUnit.MILLISECONDS) { @Override protected void finalize() throws Throwable { // prevent the PoolingClientAsyncConnectionManager from logging - this causes exceptions due to // the ClassLoader probably having been removed when the plugin shuts down. Added a // PluginEventListener to make sure the shutdown method is called while the plugin classloader // is still active. try { this.shutdown(); } catch (Throwable e) { // ignore e.printStackTrace(); } } }; connectionManager.setDefaultMaxPerRoute(options.getMaxConnectionsPerHost()); final RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout((int) options.getConnectionTimeout()) .setConnectionRequestTimeout((int) options.getLeaseTimeout()) .setCookieSpec(options.getIgnoreCookies() ? CookieSpecs.IGNORE_COOKIES : CookieSpecs.DEFAULT) .setSocketTimeout((int) options.getSocketTimeout()) .build(); final HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom() .setThreadFactory(ThreadFactories.namedThreadFactory( options.getThreadPrefix() + "-io", ThreadFactories.Type.DAEMON)) .setDefaultIOReactorConfig(reactorConfig) .setConnectionManager(connectionManager) .setRedirectStrategy(new RedirectStrategy()) .setUserAgent(getUserAgent(options)) .setDefaultRequestConfig(requestConfig); if (Jenkins.get() != null) { ProxyConfiguration proxyConfiguration = Jenkins.getInstance().proxy; if (proxyConfiguration != null) { final HttpHost proxy = new HttpHost(proxyConfiguration.name, proxyConfiguration.port); // clientBuilder.setProxy( proxy ); if (StringUtils.isNotBlank(proxyConfiguration.getUserName())) { clientBuilder.setProxyAuthenticationStrategy(ProxyAuthenticationStrategy.INSTANCE); CredentialsProvider credsProvider = new BasicCredentialsProvider(); credsProvider.setCredentials( new AuthScope(proxyConfiguration.name, proxyConfiguration.port), new UsernamePasswordCredentials( proxyConfiguration.getUserName(), proxyConfiguration.getPassword())); clientBuilder.setDefaultCredentialsProvider(credsProvider); } clientBuilder.setRoutePlanner( new JenkinsProxyRoutePlanner(proxy, proxyConfiguration.getNoProxyHostPatterns())); } } return clientBuilder; } private class JenkinsProxyRoutePlanner extends DefaultRoutePlanner { private final HttpHost proxy; private final List nonProxyHosts; public JenkinsProxyRoutePlanner(HttpHost proxy, List nonProxyHosts) { super(null); this.proxy = Args.notNull(proxy, "Proxy host"); this.nonProxyHosts = nonProxyHosts; } @Override protected HttpHost determineProxy(HttpHost target, HttpRequest request, HttpContext context) throws HttpException { return bypassProxy(target.getHostName()) ? null : this.proxy; } private boolean bypassProxy(String host) { for (Pattern p : nonProxyHosts) { if (p.matcher(host).matches()) { return true; } } return false; } } private Registry getRegistry(final HttpClientOptions options) { try { final TrustSelfSignedStrategy strategy = options.trustSelfSignedCertificates() ? new TrustSelfSignedStrategy() : null; final SSLContext sslContext = new SSLContextBuilder() .useTLS() .loadTrustMaterial(null, strategy) .build(); final SSLIOSessionStrategy sslioSessionStrategy = new SSLIOSessionStrategy( sslContext, split(System.getProperty("https.protocols")), split(System.getProperty("https.cipherSuites")), options.trustSelfSignedCertificates() ? getSelfSignedVerifier() : SSLIOSessionStrategy.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); return RegistryBuilder.create() .register("http", NoopIOSessionStrategy.INSTANCE) .register("https", sslioSessionStrategy) .build(); } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) { return getFallbackRegistry(e); } } private X509HostnameVerifier getSelfSignedVerifier() { return new X509HostnameVerifier() { @Override public void verify(final String host, final SSLSocket ssl) throws IOException { log.debug("Verification for certificates from {} disabled", host); } @Override public void verify(final String host, final X509Certificate cert) throws SSLException { log.debug("Verification for certificates from {} disabled", host); } @Override public void verify(final String host, final String[] cns, final String[] subjectAlts) throws SSLException { log.debug("Verification for certificates from {} disabled", host); } @Override public boolean verify(final String host, final SSLSession sslSession) { log.debug("Verification for certificates from {} disabled", host); return true; } }; } private Registry getFallbackRegistry(final GeneralSecurityException e) { log.error("Error when creating scheme session strategy registry", e); return RegistryBuilder.create() .register("http", NoopIOSessionStrategy.INSTANCE) .register("https", SSLIOSessionStrategy.getDefaultStrategy()) .build(); } private String getUserAgent(HttpClientOptions options) { return format( "Atlassian HttpClient %s / %s / %s", httpClientVersion.get(), applicationName.get(), options.getUserAgent()); } @Override public final ResponsePromise execute(final Request request) { try { return doExecute(request); } catch (Throwable t) { return ResponsePromises.toResponsePromise(rejected(t)); } } private ResponsePromise doExecute(final Request request) { httpClientOptions.getRequestPreparer().accept(request); final long start = System.currentTimeMillis(); final HttpRequestBase op; final String uri = request.getUri().toString(); final Request.Method method = request.getMethod(); switch (method) { case GET: op = new HttpGet(uri); break; case POST: op = new HttpPost(uri); break; case PUT: op = new HttpPut(uri); break; case DELETE: op = new HttpDelete(uri); break; case OPTIONS: op = new HttpOptions(uri); break; case HEAD: op = new HttpHead(uri); break; case TRACE: op = new HttpTrace(uri); break; default: throw new UnsupportedOperationException(method.toString()); } if (request.hasEntity()) { new RequestEntityEffect(request).apply(op); } for (Map.Entry entry : request.getHeaders().entrySet()) { op.setHeader(entry.getKey(), entry.getValue()); } final PromiseHttpAsyncClient asyncClient = getPromiseHttpAsyncClient(request); return ResponsePromises.toResponsePromise(asyncClient .execute(op, new BasicHttpContext()) .fold( throwable -> { final long requestDuration = System.currentTimeMillis() - start; publishEvent(request, requestDuration, throwable); throw new RuntimeException(throwable); }, httpResponse -> { final long requestDuration = System.currentTimeMillis() - start; publishEvent( request, requestDuration, httpResponse.getStatusLine().getStatusCode()); try { return translate(httpResponse); } catch (IOException e) { throw new RuntimeException(e); } })); } private void publishEvent(Request request, long requestDuration, int statusCode) { if (HttpStatus.OK.code <= statusCode && statusCode < HttpStatus.MULTIPLE_CHOICES.code) { eventConsumer.apply(new HttpRequestCompletedEvent( request.getUri().toString(), request.getMethod().name(), statusCode, requestDuration, request.getAttributes())); } else { eventConsumer.apply(new HttpRequestFailedEvent( request.getUri().toString(), request.getMethod().name(), statusCode, requestDuration, request.getAttributes())); } } private void publishEvent(Request request, long requestDuration, Throwable ex) { eventConsumer.apply(new HttpRequestFailedEvent( request.getUri().toString(), request.getMethod().name(), ex.toString(), requestDuration, request.getAttributes())); } private PromiseHttpAsyncClient getPromiseHttpAsyncClient(Request request) { log.trace("Creating new HttpAsyncClient"); final CloseableHttpAsyncClient nonCachingHttpClient; try { final HttpAsyncClientBuilder clientBuilder = createClientBuilder(); nonCachingHttpClient = clientBuilder.build(); nonCachingHttpClient.start(); } catch (IOReactorException e) { throw new RuntimeException("Reactor " + httpClientOptions.getThreadPrefix() + "not set up correctly", e); } return new CompletableFuturePromiseHttpPromiseAsyncClient<>( nonCachingHttpClient, threadLocalContextManager, callbackExecutor); } private Response translate(HttpResponse httpResponse) throws IOException { StatusLine status = httpResponse.getStatusLine(); Response.Builder responseBuilder = DefaultResponse.builder() .setMaxEntitySize(httpClientOptions.getMaxEntitySize()) .setStatusCode(status.getStatusCode()) .setStatusText(status.getReasonPhrase()); Header[] httpHeaders = httpResponse.getAllHeaders(); for (Header httpHeader : httpHeaders) { responseBuilder.setHeader(httpHeader.getName(), httpHeader.getValue()); } final HttpEntity entity = httpResponse.getEntity(); if (entity != null) { responseBuilder.setEntityStream(entity.getContent()); } return responseBuilder.build(); } @Override public void destroy() throws Exception { try { callbackExecutor.shutdown(); } catch (Exception e) { log.warn("skip fail to shutdown callbackExecutor:" + e.getMessage(), e); } try { nonCachingHttpClient.close(); } catch (Exception e) { log.warn("skip fail to shutdown nonCachingHttpClient:" + e.getMessage(), e); } } @Override public void flushCacheByUriPattern(Pattern urlPattern) { // httpCacheStorage.flushByUriPattern(urlPattern); } private static final class NoOpThreadLocalContextManager implements ThreadLocalContextManager { @Override public C getThreadLocalContext() { return null; } @Override public void setThreadLocalContext(C context) {} @Override public void clearThreadLocalContext() {} } private static final class DefaultApplicationNameSupplier implements Supplier { private final ApplicationProperties applicationProperties; public DefaultApplicationNameSupplier(ApplicationProperties applicationProperties) { this.applicationProperties = Objects.requireNonNull(applicationProperties); } @Override public String get() { return format( "%s-%s (%s)", applicationProperties.getDisplayName(), applicationProperties.getVersion(), applicationProperties.getBuildNumber()); } } private static class EventConsumerFunction implements Function { private final EventPublisher eventPublisher; public EventConsumerFunction(EventPublisher eventPublisher) { this.eventPublisher = eventPublisher; } @Override public Void apply(Object event) { if (eventPublisher != null) { eventPublisher.publish(event); } return null; } } private static String[] split(final String s) { if (TextUtils.isBlank(s)) { return null; } return s.split(" *, *"); } @Override public Request.Builder newRequest() { return DefaultRequest.builder(this); } @Override public Request.Builder newRequest(URI uri) { return DefaultRequest.builder(this).setUri(uri); } @Override public Request.Builder newRequest(URI uri, String contentType, String entity) { return DefaultRequest.builder(this) .setContentType(contentType) .setEntity(entity) .setUri(uri); } @Override public Request.Builder newRequest(String uri) { return newRequest(URI.create(uri)); } @Override public Request.Builder newRequest(String uri, String contentType, String entity) { return newRequest(URI.create(uri), contentType, entity); } @Override public ResponseTransformation.Builder transformation() { return DefaultResponseTransformation.builder(); } } ================================================ FILE: src/main/java/com/atlassian/httpclient/apache/httpcomponents/CommonBuilder.java ================================================ package com.atlassian.httpclient.apache.httpcomponents; import com.atlassian.httpclient.api.Common; import java.io.InputStream; import java.nio.charset.Charset; import java.util.Map; public class CommonBuilder implements Common> { private final Headers.Builder headersBuilder = new Headers.Builder(); private InputStream entityStream; @Override public CommonBuilder setHeader(final String name, final String value) { headersBuilder.setHeader(name, value); return this; } @Override public CommonBuilder setHeaders(final Map headers) { headersBuilder.setHeaders(headers); return this; } @Override public CommonBuilder setEntity(final String entity) { if (entity != null) { final String charset = "UTF-8"; byte[] bytes = entity.getBytes(Charset.forName(charset)); setEntityStream(new EntityByteArrayInputStream(bytes), charset); } else { setEntityStream(null, null); } return this; } @Override public CommonBuilder setEntityStream(final InputStream entityStream) { this.entityStream = entityStream; return this; } @Override public CommonBuilder setContentCharset(final String contentCharset) { headersBuilder.setContentCharset(contentCharset); return this; } @Override public CommonBuilder setContentType(final String contentType) { headersBuilder.setContentType(contentType); return this; } @Override public CommonBuilder setEntityStream(final InputStream entityStream, final String charset) { setEntityStream(entityStream); headersBuilder.setContentCharset(charset); return this; } public InputStream getEntityStream() { return entityStream; } public Headers getHeaders() { return headersBuilder.build(); } } ================================================ FILE: src/main/java/com/atlassian/httpclient/apache/httpcomponents/CompletableFuturePromiseHttpPromiseAsyncClient.java ================================================ package com.atlassian.httpclient.apache.httpcomponents; import com.atlassian.sal.api.executor.ThreadLocalContextManager; import io.atlassian.util.concurrent.Promise; import io.atlassian.util.concurrent.Promises; import java.io.IOException; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.Future; import java.util.concurrent.TimeoutException; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.concurrent.FutureCallback; import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; import org.apache.http.protocol.HttpContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; final class CompletableFuturePromiseHttpPromiseAsyncClient implements PromiseHttpAsyncClient { private final Logger log = LoggerFactory.getLogger(this.getClass()); private final CloseableHttpAsyncClient client; private final ThreadLocalContextManager threadLocalContextManager; private final Executor executor; CompletableFuturePromiseHttpPromiseAsyncClient( CloseableHttpAsyncClient client, ThreadLocalContextManager threadLocalContextManager, Executor executor) { this.client = Objects.requireNonNull(client); this.threadLocalContextManager = Objects.requireNonNull(threadLocalContextManager); this.executor = new ThreadLocalDelegateExecutor<>(threadLocalContextManager, executor); } @Override public Promise execute(HttpUriRequest request, HttpContext context) { // TODO after migrating from atlassian-util-concurrent 3.0.0 to 4.0.0 the SettableFuture.create() maybe obsolete // ? CompletableFuture future = new CompletableFuture<>(); Future clientFuture = client.execute( request, context, new ThreadLocalContextAwareFutureCallback(threadLocalContextManager) { @Override void doCompleted(final HttpResponse httpResponse) { executor.execute(() -> future.complete(httpResponse)); log.trace("Closing in doCompleted()"); closeClient(); } @Override void doFailed(final Exception ex) { executor.execute(() -> future.completeExceptionally(ex)); log.trace("Closing in doFailed()"); closeClient(); } @Override void doCancelled() { final TimeoutException timeoutException = new TimeoutException(); executor.execute(() -> future.completeExceptionally(timeoutException)); log.trace("Closing in doCancelled()"); closeClient(); } }); return Promises.forFuture(clientFuture, executor); } private void closeClient() { try { client.close(); } catch (IOException e) { log.error("Close failed", e); } } static void runInContext( ThreadLocalContextManager threadLocalContextManager, C threadLocalContext, ClassLoader contextClassLoader, Runnable runnable) { final C oldThreadLocalContext = threadLocalContextManager.getThreadLocalContext(); final ClassLoader oldCcl = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(contextClassLoader); threadLocalContextManager.setThreadLocalContext(threadLocalContext); runnable.run(); } finally { threadLocalContextManager.setThreadLocalContext(oldThreadLocalContext); Thread.currentThread().setContextClassLoader(oldCcl); } } private abstract static class ThreadLocalContextAwareFutureCallback implements FutureCallback { private final ThreadLocalContextManager threadLocalContextManager; private final C threadLocalContext; private final ClassLoader contextClassLoader; private ThreadLocalContextAwareFutureCallback(ThreadLocalContextManager threadLocalContextManager) { this.threadLocalContextManager = Objects.requireNonNull(threadLocalContextManager); this.threadLocalContext = threadLocalContextManager.getThreadLocalContext(); this.contextClassLoader = Thread.currentThread().getContextClassLoader(); } abstract void doCompleted(HttpResponse response); abstract void doFailed(Exception ex); abstract void doCancelled(); @Override public final void completed(final HttpResponse response) { runInContext( threadLocalContextManager, threadLocalContext, contextClassLoader, () -> doCompleted(response)); } @Override public final void failed(final Exception ex) { runInContext(threadLocalContextManager, threadLocalContext, contextClassLoader, () -> doFailed(ex)); } @Override public final void cancelled() { runInContext(threadLocalContextManager, threadLocalContext, contextClassLoader, this::doCancelled); } } private static final class ThreadLocalDelegateExecutor implements Executor { private final Executor delegate; private final ThreadLocalContextManager manager; ThreadLocalDelegateExecutor(ThreadLocalContextManager manager, Executor delegate) { this.delegate = Objects.requireNonNull(delegate); this.manager = Objects.requireNonNull(manager); } @Override public void execute(Runnable runnable) { delegate.execute(new ThreadLocalDelegateRunnable<>(manager, runnable)); } } private static final class ThreadLocalDelegateRunnable implements Runnable { private final C context; private final Runnable delegate; private final ClassLoader contextClassLoader; private final ThreadLocalContextManager manager; ThreadLocalDelegateRunnable(ThreadLocalContextManager manager, Runnable delegate) { this.delegate = delegate; this.manager = manager; this.context = manager.getThreadLocalContext(); this.contextClassLoader = Thread.currentThread().getContextClassLoader(); } @Override public void run() { runInContext(manager, context, contextClassLoader, delegate); } } } ================================================ FILE: src/main/java/com/atlassian/httpclient/apache/httpcomponents/DefaultHttpClientFactory.java ================================================ package com.atlassian.httpclient.apache.httpcomponents; import com.atlassian.event.api.EventPublisher; import com.atlassian.httpclient.api.HttpClient; import com.atlassian.httpclient.api.factory.HttpClientFactory; import com.atlassian.httpclient.api.factory.HttpClientOptions; import com.atlassian.sal.api.ApplicationProperties; import com.atlassian.sal.api.executor.ThreadLocalContextManager; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Objects; import org.springframework.beans.factory.DisposableBean; public final class DefaultHttpClientFactory implements HttpClientFactory, DisposableBean { private final EventPublisher eventPublisher; private final ApplicationProperties applicationProperties; private final ThreadLocalContextManager threadLocalContextManager; // shared http client private static ApacheAsyncHttpClient httpClient; public DefaultHttpClientFactory( EventPublisher eventPublisher, ApplicationProperties applicationProperties, ThreadLocalContextManager threadLocalContextManager) { this.eventPublisher = Objects.requireNonNull(eventPublisher); this.applicationProperties = Objects.requireNonNull(applicationProperties); this.threadLocalContextManager = Objects.requireNonNull(threadLocalContextManager); } @Override public HttpClient create(HttpClientOptions options) { return doCreate(options, threadLocalContextManager); } @Override public HttpClient create(HttpClientOptions options, ThreadLocalContextManager threadLocalContextManager) { return doCreate(options, threadLocalContextManager); } @Override public void dispose(@NonNull final HttpClient httpClient) throws Exception { if (httpClient instanceof ApacheAsyncHttpClient) { ((ApacheAsyncHttpClient) httpClient).destroy(); } } private HttpClient doCreate(HttpClientOptions options, ThreadLocalContextManager threadLocalContextManager) { Objects.requireNonNull(options); // we create only one http client instance as we don't need more if (httpClient != null) { return httpClient; } synchronized (this) { httpClient = new ApacheAsyncHttpClient( eventPublisher, applicationProperties, threadLocalContextManager, options); return httpClient; } } @Override public void destroy() throws Exception { httpClient.destroy(); } } ================================================ FILE: src/main/java/com/atlassian/httpclient/apache/httpcomponents/DefaultMessage.java ================================================ package com.atlassian.httpclient.apache.httpcomponents; import com.atlassian.httpclient.api.Message; import io.atlassian.fugue.Option; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.Charset; import java.util.Map; import org.apache.http.util.CharArrayBuffer; /** * An abstract base class for HTTP messages (i.e. Request and Response) with support for * header and entity management. */ abstract class DefaultMessage implements Message { private final InputStream entityStream; private final Headers headers; private final long maxEntitySize; private boolean hasRead; public DefaultMessage(final Headers headers, final InputStream entityStream, Option maxEntitySize) { this.maxEntitySize = maxEntitySize.getOrElse((long) Integer.MAX_VALUE); this.headers = headers; this.entityStream = entityStream; } @Override public String getContentType() { return headers.getContentType(); } @Override public String getContentCharset() { return headers.getContentCharset(); } public String getAccept() { return headers.getHeader("Accept"); } @Override public InputStream getEntityStream() throws IllegalStateException { checkRead(); return entityStream; } @Override public String getEntity() throws IllegalStateException, IllegalArgumentException { String entity = null; if (hasEntity()) { checkValidSize(); final String charsetAsString = getContentCharset(); final Charset charset = charsetAsString != null ? Charset.forName(charsetAsString) : Charset.forName("UTF-8"); try { InputStream instream = getEntityStream(); if (instream == null) { return null; } try { int bufferLength = 4096; String lengthHeader = getHeader("Content-Length"); if (lengthHeader != null) { bufferLength = Integer.parseInt(lengthHeader); } Reader reader = new InputStreamReader(instream, charset); CharArrayBuffer buffer = new CharArrayBuffer(bufferLength); char[] tmp = new char[1024]; int l; while ((l = reader.read(tmp)) != -1) { if (buffer.length() + l > maxEntitySize) { throw new IllegalArgumentException("HTTP entity too large to be buffered in memory"); } buffer.append(tmp, 0, l); } return buffer.toString(); } finally { instream.close(); } } catch (IOException e) { throw new IllegalStateException("Unable to convert response body to String", e); } } return entity; } @Override public boolean hasEntity() { return entityStream != null; } @Override public boolean hasReadEntity() { return hasRead; } @Override public Map getHeaders() { return headers.getHeaders(); } @Override public String getHeader(String name) { return headers.getHeader(name); } public Message validate() { if (hasEntity() && headers.getContentType() == null) { throw new IllegalStateException("Property contentType must be set when entity is present"); } return this; } private void checkRead() throws IllegalStateException { if (entityStream != null) { if (hasRead) { throw new IllegalStateException("Entity may only be accessed once"); } hasRead = true; } } private void checkValidSize() throws IllegalArgumentException { Integer contentLength; String lengthHeader = getHeader("Content-Length"); if (lengthHeader != null) { contentLength = Integer.parseInt(lengthHeader); if (contentLength > maxEntitySize) { throw new IllegalArgumentException("HTTP entity too large to be buffered in memory"); } } } } ================================================ FILE: src/main/java/com/atlassian/httpclient/apache/httpcomponents/DefaultRequest.java ================================================ package com.atlassian.httpclient.apache.httpcomponents; import static com.atlassian.httpclient.api.Request.Method.DELETE; import static com.atlassian.httpclient.api.Request.Method.GET; import static com.atlassian.httpclient.api.Request.Method.HEAD; import static com.atlassian.httpclient.api.Request.Method.OPTIONS; import static com.atlassian.httpclient.api.Request.Method.POST; import static com.atlassian.httpclient.api.Request.Method.PUT; import static com.atlassian.httpclient.api.Request.Method.TRACE; import com.atlassian.httpclient.api.EntityBuilder; import com.atlassian.httpclient.api.HttpClient; import com.atlassian.httpclient.api.Request; import com.atlassian.httpclient.api.ResponsePromise; import io.atlassian.fugue.Option; import java.io.InputStream; import java.net.URI; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; public class DefaultRequest extends DefaultMessage implements Request { private final URI uri; private final boolean cacheDisabled; private final Map attributes; private final Method method; private final Option contentLength; private DefaultRequest( URI uri, boolean cacheDisabled, Map attributes, Headers headers, Method method, InputStream entityStream, Option contentLength) { super(headers, entityStream, Option.none()); this.uri = uri; this.cacheDisabled = cacheDisabled; this.attributes = attributes; this.method = method; this.contentLength = contentLength; } public static DefaultRequestBuilder builder(HttpClient httpClient) { return new DefaultRequestBuilder(httpClient); } @Override public Method getMethod() { return method; } @Override public URI getUri() { return uri; } @Override public String getAccept() { return super.getAccept(); } @Override public String getAttribute(String name) { return attributes.get(name); } @Override public Map getAttributes() { return Collections.unmodifiableMap(attributes); } @Override public Option getContentLength() { return contentLength; } @Override public boolean isCacheDisabled() { return cacheDisabled; } @Override public Request validate() { super.validate(); Objects.nonNull(uri); Objects.nonNull(method); switch (method) { case GET: case DELETE: case HEAD: if (hasEntity()) { throw new IllegalStateException("Request method " + method + " does not support an entity"); } break; case POST: case PUT: case TRACE: // no-op break; } return this; } public static class DefaultRequestBuilder implements Request.Builder { private final HttpClient httpClient; private final Map attributes; private final CommonBuilder commonBuilder; private URI uri; private boolean cacheDisabled; private Method method; private Option contentLength; public DefaultRequestBuilder(final HttpClient httpClient) { this.httpClient = httpClient; this.attributes = new HashMap<>(); commonBuilder = new CommonBuilder<>(); setAccept("*/*"); contentLength = Option.none(); } @Override public DefaultRequestBuilder setUri(final URI uri) { this.uri = uri; return this; } @Override public DefaultRequestBuilder setAccept(final String accept) { setHeader("Accept", accept); return this; } @Override public DefaultRequestBuilder setCacheDisabled() { this.cacheDisabled = true; return this; } @Override public DefaultRequestBuilder setAttribute(final String name, final String value) { attributes.put(name, value); return this; } @Override public DefaultRequestBuilder setAttributes(final Map properties) { attributes.putAll(properties); return this; } @Override public DefaultRequestBuilder setEntity(final EntityBuilder entityBuilder) { EntityBuilder.Entity entity = entityBuilder.build(); final Map headers = entity.getHeaders(); for (Map.Entry headerEntry : headers.entrySet()) { setHeader(headerEntry.getKey(), headerEntry.getValue()); } setEntityStream(entity.getInputStream()); return this; } @Override public DefaultRequestBuilder setHeader(final String name, final String value) { commonBuilder.setHeader(name, value); return this; } @Override public DefaultRequestBuilder setHeaders(final Map headers) { commonBuilder.setHeaders(headers); return this; } @Override public DefaultRequestBuilder setEntity(final String entity) { commonBuilder.setEntity(entity); setContentLength(entity.length()); return this; } @Override public DefaultRequestBuilder setEntityStream(final InputStream entityStream) { commonBuilder.setEntityStream(entityStream); return this; } @Override public DefaultRequestBuilder setContentCharset(final String contentCharset) { commonBuilder.setContentCharset(contentCharset); return this; } @Override public DefaultRequestBuilder setContentType(final String contentType) { commonBuilder.setContentType(contentType); return this; } @Override public DefaultRequestBuilder setEntityStream(final InputStream entityStream, final String charset) { setEntityStream(entityStream); commonBuilder.setContentCharset(charset); return this; } @Override public DefaultRequestBuilder setContentLength(final long contentLength) { if (contentLength < 0) { throw new IllegalArgumentException("Content length must be greater than or equal to 0"); } this.contentLength = Option.some(contentLength); return this; } @Override public DefaultRequest build() { return new DefaultRequest( uri, cacheDisabled, attributes, commonBuilder.getHeaders(), method, commonBuilder.getEntityStream(), contentLength); } @Override public ResponsePromise get() { return execute(GET); } @Override public ResponsePromise post() { return execute(POST); } @Override public ResponsePromise put() { return execute(PUT); } @Override public ResponsePromise delete() { return execute(DELETE); } @Override public ResponsePromise options() { return execute(OPTIONS); } @Override public ResponsePromise head() { return execute(HEAD); } @Override public ResponsePromise trace() { return execute(TRACE); } @Override public ResponsePromise execute(Method method) { Objects.nonNull(method); setMethod(method); return httpClient.execute(build().validate()); } public void setMethod(final Method method) { this.method = method; } } } ================================================ FILE: src/main/java/com/atlassian/httpclient/apache/httpcomponents/DefaultResponse.java ================================================ package com.atlassian.httpclient.apache.httpcomponents; import com.atlassian.httpclient.api.Response; import io.atlassian.fugue.Option; import java.io.InputStream; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public final class DefaultResponse extends DefaultMessage implements Response { private int statusCode; private String statusText; private Logger log = LoggerFactory.getLogger(DefaultResponse.class); public DefaultResponse( Headers headers, InputStream entityStream, Option maxEntitySize, int statusCode, String statusText) { super(headers, entityStream, maxEntitySize); this.statusCode = statusCode; this.statusText = statusText; } public static DefaultResponseBuilder builder() { return new DefaultResponseBuilder(); } @Override public int getStatusCode() { return statusCode; } @Override public String getStatusText() { return statusText; } @Override public boolean isInformational() { return statusCode >= 100 && statusCode < 200; } @Override public boolean isSuccessful() { return statusCode >= 200 && statusCode < 300; } @Override public boolean isOk() { return statusCode == 200; } @Override public boolean isCreated() { return statusCode == 201; } @Override public boolean isNoContent() { return statusCode == 204; } @Override public boolean isRedirection() { return statusCode >= 300 && statusCode < 400; } @Override public boolean isSeeOther() { return statusCode == 303; } @Override public boolean isNotModified() { return statusCode == 304; } @Override public boolean isClientError() { return statusCode >= 400 && statusCode < 500; } @Override public boolean isBadRequest() { return statusCode == 400; } @Override public boolean isUnauthorized() { return statusCode == 401; } @Override public boolean isForbidden() { return statusCode == 403; } @Override public boolean isNotFound() { return statusCode == 404; } @Override public boolean isConflict() { return statusCode == 409; } @Override public boolean isServerError() { return statusCode >= 500 && statusCode < 600; } @Override public boolean isInternalServerError() { return statusCode == 500; } @Override public boolean isServiceUnavailable() { return statusCode == 503; } @Override public boolean isError() { return isClientError() || isServerError(); } @Override public boolean isNotSuccessful() { return isInformational() || isRedirection() || isError(); } @Override public Option getContentLength() { String lengthString = getHeader(Headers.Names.CONTENT_LENGTH); if (lengthString != null) { try { Option parsedLength = Option.some(Long.parseLong(lengthString)); return parsedLength.flatMap(aLong -> { if (aLong < 0) { log.warn("Unable to parse content length. Received out of range value {}", aLong); return Option.none(); } else { return Option.some(aLong); } }); } catch (NumberFormatException e) { log.warn("Unable to parse content length {}", lengthString); return Option.none(); } } else { return Option.none(); } } public static class DefaultResponseBuilder implements Builder { private final CommonBuilder commonBuilder; private String statusText; private int statusCode; private long maxEntitySize; private DefaultResponseBuilder() { this.commonBuilder = new CommonBuilder(); } @Override public DefaultResponseBuilder setContentType(final String contentType) { commonBuilder.setContentType(contentType); return this; } @Override public DefaultResponseBuilder setContentCharset(final String contentCharset) { commonBuilder.setContentCharset(contentCharset); return this; } @Override public DefaultResponseBuilder setHeaders(final Map headers) { commonBuilder.setHeaders(headers); return this; } @Override public DefaultResponseBuilder setHeader(final String name, final String value) { commonBuilder.setHeader(name, value); return this; } @Override public DefaultResponseBuilder setEntity(final String entity) { commonBuilder.setEntity(entity); return this; } @Override public DefaultResponseBuilder setEntityStream(final InputStream entityStream, final String encoding) { commonBuilder.setEntityStream(entityStream); commonBuilder.setContentCharset(encoding); return this; } @Override public DefaultResponseBuilder setEntityStream(final InputStream entityStream) { commonBuilder.setEntityStream(entityStream); return this; } @Override public DefaultResponseBuilder setStatusText(final String statusText) { this.statusText = statusText; return this; } @Override public DefaultResponseBuilder setStatusCode(final int statusCode) { this.statusCode = statusCode; return this; } public DefaultResponseBuilder setMaxEntitySize(long maxEntitySize) { this.maxEntitySize = maxEntitySize; return this; } @Override public DefaultResponse build() { return new DefaultResponse( commonBuilder.getHeaders(), commonBuilder.getEntityStream(), Option.option(maxEntitySize), statusCode, statusText); } } } ================================================ FILE: src/main/java/com/atlassian/httpclient/apache/httpcomponents/EntityByteArrayInputStream.java ================================================ package com.atlassian.httpclient.apache.httpcomponents; import java.io.ByteArrayInputStream; public class EntityByteArrayInputStream extends ByteArrayInputStream { private byte[] bytes; public EntityByteArrayInputStream(byte[] bytes) { super(bytes); this.bytes = bytes; } public byte[] getBytes() { return bytes; } } ================================================ FILE: src/main/java/com/atlassian/httpclient/apache/httpcomponents/Headers.java ================================================ package com.atlassian.httpclient.apache.httpcomponents; import com.atlassian.httpclient.api.Buildable; import java.nio.charset.Charset; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.apache.http.protocol.HTTP; public class Headers { private final Map headers; private final String contentCharset; private final String contentType; private Headers(Map headers, String contentCharset, String contentType) { this.headers = headers; this.contentCharset = contentCharset; this.contentType = contentType; } public String getContentCharset() { return contentCharset; } public String getContentType() { return contentType; } public Map getHeaders() { Map headers = new HashMap<>(this.headers); if (contentType != null) { headers.put(Names.CONTENT_TYPE, buildContentType()); } return Collections.unmodifiableMap(headers); } public String getHeader(final String name) { String value; if (name.equalsIgnoreCase(Names.CONTENT_TYPE)) { value = buildContentType(); } else { value = headers.get(name); } return value; } private String buildContentType() { String value = contentType != null ? contentType : "text/plain"; if (contentCharset != null) { value += "; charset=" + contentCharset; } return value; } public static class Builder implements Buildable { private final Map headers = new HashMap<>(); private String contentType; private String contentCharset; public Builder setHeaders(Map headers) { this.headers.clear(); for (Map.Entry entry : headers.entrySet()) { setHeader(entry.getKey(), entry.getValue()); } return this; } public Builder setHeader(String name, String value) { if (name.equalsIgnoreCase("Content-Type")) { parseContentType(value); } else { headers.put(name, value); } return this; } public Builder setContentLength(long contentLength) { if (contentLength < 0) { throw new IllegalArgumentException("Content-Length must be greater than or equal to 0"); } setHeader(Names.CONTENT_LENGTH, Long.toString(contentLength)); return this; } public Builder setContentCharset(String contentCharset) { this.contentCharset = contentCharset != null ? Charset.forName(contentCharset).name() : null; return this; } public Builder setContentType(String contentType) { parseContentType(contentType); return this; } private void parseContentType(String value) { if (value != null) { String[] parts = value.split(";"); if (parts.length >= 1) { contentType = parts[0].trim(); } if (parts.length >= 2) { String subtype = parts[1].trim(); if (subtype.startsWith("charset=")) { setContentCharset(subtype.substring(8)); } else if (subtype.startsWith("boundary=")) { contentType = contentType.concat(';' + subtype); } } } else { contentType = null; } } @Override public Headers build() { return new Headers(headers, contentCharset, contentType); } } public static class Names { public static final String CONTENT_LENGTH = HTTP.CONTENT_LEN; public static final String CONTENT_TYPE = HTTP.CONTENT_TYPE; private Names() {} } } ================================================ FILE: src/main/java/com/atlassian/httpclient/apache/httpcomponents/MavenUtils.java ================================================ package com.atlassian.httpclient.apache.httpcomponents; import static java.lang.String.format; import java.io.InputStream; import java.util.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; final class MavenUtils { private static final Logger logger = LoggerFactory.getLogger(MavenUtils.class); private static final String UNKNOWN_VERSION = "unknown"; static String getVersion(String groupId, String artifactId) { final Properties props = new Properties(); try (InputStream is = Thread.currentThread() .getContextClassLoader() .getResourceAsStream(getPomFilePath(groupId, artifactId))) { props.load(is); return props.getProperty("version", UNKNOWN_VERSION); } catch (Exception e) { logger.debug("Could not find version for maven artifact {}:{}", groupId, artifactId); logger.debug("Got the following exception:", e); return UNKNOWN_VERSION; } } private static String getPomFilePath(String groupId, String artifactId) { return format("META-INF/maven/%s/%s/pom.properties", groupId, artifactId); } } ================================================ FILE: src/main/java/com/atlassian/httpclient/apache/httpcomponents/PromiseHttpAsyncClient.java ================================================ package com.atlassian.httpclient.apache.httpcomponents; import io.atlassian.util.concurrent.Promise; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.protocol.HttpContext; interface PromiseHttpAsyncClient { Promise execute(HttpUriRequest request, HttpContext context); } ================================================ FILE: src/main/java/com/atlassian/httpclient/apache/httpcomponents/RedirectStrategy.java ================================================ package com.atlassian.httpclient.apache.httpcomponents; import java.net.URI; import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.ProtocolException; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpHead; import org.apache.http.client.methods.HttpPatch; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.impl.client.DefaultRedirectStrategy; import org.apache.http.protocol.HttpContext; public class RedirectStrategy extends DefaultRedirectStrategy { final String[] REDIRECT_METHODS = { HttpHead.METHOD_NAME, HttpGet.METHOD_NAME, HttpPost.METHOD_NAME, HttpPut.METHOD_NAME, HttpDelete.METHOD_NAME, HttpPatch.METHOD_NAME }; @Override public boolean isRedirectable(String method) { for (String m : REDIRECT_METHODS) { if (m.equalsIgnoreCase(method)) { return true; } } return false; } @Override public HttpUriRequest getRedirect(final HttpRequest request, final HttpResponse response, final HttpContext context) throws ProtocolException { URI uri = getLocationURI(request, response, context); String method = request.getRequestLine().getMethod(); if (method.equalsIgnoreCase(HttpHead.METHOD_NAME)) { return new HttpHead(uri); } else if (method.equalsIgnoreCase(HttpGet.METHOD_NAME)) { return new HttpGet(uri); } else if (method.equalsIgnoreCase(HttpPost.METHOD_NAME)) { final HttpPost post = new HttpPost(uri); if (request instanceof HttpEntityEnclosingRequest) { post.setEntity(((HttpEntityEnclosingRequest) request).getEntity()); } return post; } else if (method.equalsIgnoreCase(HttpPut.METHOD_NAME)) { return new HttpPut(uri); } else if (method.equalsIgnoreCase(HttpDelete.METHOD_NAME)) { return new HttpDelete(uri); } else if (method.equalsIgnoreCase(HttpPatch.METHOD_NAME)) { return new HttpPatch(uri); } else { return new HttpGet(uri); } } } ================================================ FILE: src/main/java/com/atlassian/httpclient/apache/httpcomponents/RequestEntityEffect.java ================================================ package com.atlassian.httpclient.apache.httpcomponents; import com.atlassian.httpclient.api.Request; import io.atlassian.fugue.Effect; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import org.apache.commons.io.IOUtils; import org.apache.http.HttpEntity; import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.InputStreamEntity; public class RequestEntityEffect implements Effect { private final Request request; public RequestEntityEffect(final Request request) { this.request = request; } @Override public void apply(final HttpRequestBase httpRequestBase) { if (httpRequestBase instanceof HttpEntityEnclosingRequestBase) { ((HttpEntityEnclosingRequestBase) httpRequestBase).setEntity(getHttpEntity(request)); } else { throw new UnsupportedOperationException( "HTTP method " + request.getMethod() + " does not support sending an entity"); } } private HttpEntity getHttpEntity(final Request request) { HttpEntity entity = null; if (request.hasEntity()) { InputStream entityStream = request.getEntityStream(); if (entityStream instanceof ByteArrayInputStream) { byte[] bytes; if (entityStream instanceof EntityByteArrayInputStream) { bytes = ((EntityByteArrayInputStream) entityStream).getBytes(); } else { try { bytes = IOUtils.toByteArray(entityStream); } catch (IOException e) { throw new RuntimeException(e); } } entity = new ByteArrayEntity(bytes); } else { long contentLength = request.getContentLength().getOrElse(-1L); entity = new InputStreamEntity(entityStream, contentLength); } } return entity; } } ================================================ FILE: src/main/java/com/atlassian/httpclient/base/event/AbstractHttpRequestEvent.java ================================================ package com.atlassian.httpclient.base.event; import java.util.Map; abstract class AbstractHttpRequestEvent { private final String url; private final String httpMethod; private final long requestDuration; private final Map properties; private int statusCode; private String error; public AbstractHttpRequestEvent( String url, String httpMethod, int statusCode, long requestDuration, Map properties) { this.url = url; this.httpMethod = httpMethod; this.statusCode = statusCode; this.requestDuration = requestDuration; this.properties = properties; } public AbstractHttpRequestEvent( String url, String httpMethod, String error, long requestDuration, Map properties) { this.url = url; this.httpMethod = httpMethod; this.error = error; this.requestDuration = requestDuration; this.properties = properties; } public String getUrl() { return url; } public int getStatusCode() { return statusCode; } public String getError() { return error; } public long getRequestDuration() { return requestDuration; } public Map getProperties() { return properties; } } ================================================ FILE: src/main/java/com/atlassian/httpclient/base/event/HttpRequestCompletedEvent.java ================================================ package com.atlassian.httpclient.base.event; import java.util.Map; public final class HttpRequestCompletedEvent extends AbstractHttpRequestEvent { public HttpRequestCompletedEvent( String url, String httpMethod, int statusCode, long requestDuration, Map properties) { super(url, httpMethod, statusCode, requestDuration, properties); } } ================================================ FILE: src/main/java/com/atlassian/httpclient/base/event/HttpRequestFailedEvent.java ================================================ package com.atlassian.httpclient.base.event; import java.util.Map; public final class HttpRequestFailedEvent extends AbstractHttpRequestEvent { public HttpRequestFailedEvent( String url, String httpMethod, int statusCode, long elapsed, Map properties) { super(url, httpMethod, statusCode, elapsed, properties); } public HttpRequestFailedEvent( String url, String httpMethod, String error, long elapsed, Map properties) { super(url, httpMethod, error, elapsed, properties); } } ================================================ FILE: src/main/java/hudson/plugins/jira/CredentialsHelper.java ================================================ package hudson.plugins.jira; import com.cloudbees.plugins.credentials.CredentialsMatchers; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.CredentialsScope; import com.cloudbees.plugins.credentials.SystemCredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.cloudbees.plugins.credentials.common.StandardListBoxModel; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials; import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.model.Descriptor.FormException; import hudson.model.Item; import hudson.model.Queue; import hudson.model.queue.Tasks; import hudson.security.ACL; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import hudson.util.Secret; import java.io.IOException; import java.net.URL; import java.util.List; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.model.Jenkins; import org.apache.commons.lang3.StringUtils; /** * Helper class for vary credentials operations. * * @author Zhenlei Huang */ public class CredentialsHelper { private static final Logger LOGGER = Logger.getLogger(CredentialsHelper.class.getName()); @CheckForNull protected static StandardUsernamePasswordCredentials lookupSystemCredentials( @CheckForNull String credentialsId, @CheckForNull URL url) { if (credentialsId == null) { return null; } return CredentialsMatchers.firstOrNull( CredentialsProvider.lookupCredentials( StandardUsernamePasswordCredentials.class, Jenkins.get(), ACL.SYSTEM, URIRequirementBuilder.fromUri(url != null ? url.toExternalForm() : null) .build()), CredentialsMatchers.withId(credentialsId)); } protected static StandardUsernamePasswordCredentials migrateCredentials( @NonNull String username, String password, @CheckForNull URL url) throws FormException { List credentials = CredentialsMatchers.filter( CredentialsProvider.lookupCredentials( StandardUsernamePasswordCredentials.class, Jenkins.get(), ACL.SYSTEM, URIRequirementBuilder.fromUri(url != null ? url.toExternalForm() : null) .build()), CredentialsMatchers.withUsername(username)); for (StandardUsernamePasswordCredentials c : credentials) { if (StringUtils.equals(password, Secret.toString(c.getPassword()))) { return c; // found } } // Create new credentials with the principal and secret if we couldn't find any existing credentials StandardUsernamePasswordCredentials newCredentials = new UsernamePasswordCredentialsImpl( CredentialsScope.SYSTEM, null, "Migrated by Jira Plugin", username, password); SystemCredentialsProvider.getInstance().getCredentials().add(newCredentials); try { SystemCredentialsProvider.getInstance().save(); LOGGER.log( Level.INFO, "Provided username and password were successfully migrated and stored as {0}", newCredentials.getId()); } catch (IOException e) { LOGGER.log(Level.WARNING, "Unable to store migrated credentials", e); } return newCredentials; } protected static ListBoxModel doFillCredentialsIdItems(Item item, String credentialsId, String uri) { StandardListBoxModel result = new StandardListBoxModel(); if (item == null) { if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { return result.includeCurrentValue(credentialsId); } } else { if (!item.hasPermission(Item.EXTENDED_READ) && !item.hasPermission(CredentialsProvider.USE_ITEM)) { return result.includeCurrentValue(credentialsId); } } return result.includeEmptyValue() .includeMatchingAs( item instanceof Queue.Task ? Tasks.getAuthenticationOf((Queue.Task) item) : ACL.SYSTEM, item, StandardCredentials.class, URIRequirementBuilder.fromUri(uri).build(), CredentialsMatchers.anyOf( CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class), CredentialsMatchers.instanceOf(UsernamePasswordCredentials.class))) .includeCurrentValue(credentialsId); } protected static FormValidation doCheckFillCredentialsId(Item item, String credentialsId, String uri) { if (item == null) { if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { return FormValidation.ok(); } } else { if (!item.hasPermission(Item.EXTENDED_READ) && !item.hasPermission(CredentialsProvider.USE_ITEM)) { return FormValidation.ok(); } } if (StringUtils.isEmpty(credentialsId)) { return FormValidation.ok(); } if (!(findCredentials(item, credentialsId, uri).isPresent())) { return FormValidation.error("Cannot find currently selected credentials"); } return FormValidation.ok(); } protected static Optional findCredentials( Item item, String credentialsId, String uri) { return Optional.ofNullable(CredentialsMatchers.firstOrNull( CredentialsProvider.lookupCredentials( StandardUsernamePasswordCredentials.class, item, item instanceof Queue.Task ? Tasks.getAuthenticationOf((Queue.Task) item) : ACL.SYSTEM, URIRequirementBuilder.fromUri(uri).build()), CredentialsMatchers.withId(credentialsId))); } } ================================================ FILE: src/main/java/hudson/plugins/jira/EmptyFriendlyURLConverter.java ================================================ package hudson.plugins.jira; import java.net.MalformedURLException; import java.net.URL; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.beanutils.Converter; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; /** * It's little hackish. */ @Restricted(NoExternalUse.class) public class EmptyFriendlyURLConverter implements Converter { private static final Logger LOGGER = Logger.getLogger(JiraProjectProperty.class.getName()); @Override public Object convert(Class aClass, Object o) { if (o == null || "".equals(o) || "null".equals(o)) { return null; } try { return new URL(o.toString()); } catch (MalformedURLException e) { LOGGER.log(Level.WARNING, "{0} is not a valid URL", o); return null; } } } ================================================ FILE: src/main/java/hudson/plugins/jira/EnvironmentExpander.java ================================================ package hudson.plugins.jira; import hudson.EnvVars; import hudson.model.Run; import hudson.model.TaskListener; import java.io.IOException; public class EnvironmentExpander { public static EnvVars getEnvVars(Run run, TaskListener listener) { if (run == null || listener == null) { return null; } try { return run.getEnvironment(listener); } catch (IOException | InterruptedException e) { return null; } } public static String expandVariable(String variable, Run run, TaskListener listener) { EnvVars envVars = getEnvVars(run, listener); return expandVariable(variable, envVars); } public static String expandVariable(String variable, EnvVars envVars) { if (envVars == null) { return variable; } return envVars.expand(variable); } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraBuildAction.java ================================================ package hudson.plugins.jira; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.model.Run; import hudson.plugins.jira.model.JiraIssue; import java.net.URL; import java.util.HashSet; import java.util.Set; import jenkins.model.RunAction2; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; /** * Jira issues related to the build. * * @author Kohsuke Kawaguchi */ @ExportedBean public class JiraBuildAction implements RunAction2 { private final HashSet issues; private transient Run owner; public JiraBuildAction(@NonNull Set issues) { this.issues = new HashSet<>(issues); } // Leave it in place for binary compatibility. /** * @param owner the owner of this action * @param issues the Jira issues * * @deprecated use {@link #JiraBuildAction(java.util.Set)} instead */ @Deprecated public JiraBuildAction(Run owner, @NonNull Set issues) { this(issues); // the owner will be set by #onAttached(hudson.model.Run) } @Override public void onAttached(Run r) { this.owner = r; } @Override public void onLoad(Run r) { this.owner = r; } @Override public String getIconFileName() { return null; } @Override public String getDisplayName() { return Messages.JiraBuildAction_DisplayName(); } @Override public String getUrlName() { return "jira"; } public Run getOwner() { return owner; } @Exported(inline = true) public Set getIssues() { return issues; } @Exported public String getServerURL() { JiraSite jiraSite = JiraSite.get(owner.getParent()); URL url = jiraSite != null ? jiraSite.getUrl() : null; return url != null ? url.toString() : null; } /** * Finds {@link JiraIssue} whose ID matches the given one. * * @param issueID e.g. JENKINS-1234 * @return JIRAIssue representing the issueID */ public JiraIssue getIssue(String issueID) { for (JiraIssue issue : issues) { if (issue.getKey().equals(issueID)) { return issue; } } return null; } public void addIssues(Set issuesToBeSaved) { this.issues.addAll(issuesToBeSaved); } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraCarryOverAction.java ================================================ package hudson.plugins.jira; import hudson.Util; import hudson.model.InvisibleAction; import hudson.plugins.jira.model.JiraIssue; import java.util.Arrays; import java.util.Collection; import java.util.Set; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; /** * Remembers Jira IDs that need to be updated later, * when we get a successful build. * * @author Kohsuke Kawaguchi */ @ExportedBean public class JiraCarryOverAction extends InvisibleAction { /** * ','-separate IDs, for compact persistence. */ private final String ids; public JiraCarryOverAction(Set issues) { StringBuilder buf = new StringBuilder(); boolean first = true; for (JiraIssue issue : issues) { if (first) { first = false; } else { buf.append(","); } buf.append(issue.getKey()); } this.ids = buf.toString(); } @Exported public Collection getIDs() { return Arrays.asList(Util.tokenize(ids, ",")); } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraChangeLogAnnotator.java ================================================ package hudson.plugins.jira; import hudson.Extension; import hudson.MarkupText; import hudson.Util; import hudson.model.Job; import hudson.model.Run; import hudson.plugins.jira.model.JiraIssue; import hudson.scm.ChangeLogAnnotator; import hudson.scm.ChangeLogSet.Entry; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.LinkedHashSet; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; /** * {@link ChangeLogAnnotator} that picks up Jira issue IDs. * * @author Kohsuke Kawaguchi */ @Extension public class JiraChangeLogAnnotator extends ChangeLogAnnotator { private static final Logger LOGGER = Logger.getLogger(JiraChangeLogAnnotator.class.getName()); public JiraChangeLogAnnotator() { LOGGER.fine("JiraChangeLogAnnotator created"); } @Override public void annotate(Run run, Entry change, MarkupText text) { JiraSite site = getSiteForProject(run.getParent()); if (site == null) { LOGGER.fine("not configured with Jira site"); return; // not configured with Jira } if (site.getDisableChangelogAnnotations()) { LOGGER.info("ChangeLog annotations are disabled.\n Due to this also Related Issues won't be visible."); return; } LOGGER.log(Level.FINE, "Using site: {0}", site.getUrl()); // if there's any recorded detail information, try to use that, too. JiraBuildAction a = run.getAction(JiraBuildAction.class); Set issuesToBeSaved = new LinkedHashSet<>(); Pattern pattern = site.getIssuePattern(); if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Using issue pattern: " + pattern); } String plainText = text.getText(); Matcher m = pattern.matcher(plainText); while (m.find()) { if (m.groupCount() >= 1) { String id = m.group(1); if (StringUtils.isNotBlank(site.credentialsId) && !hasProjectForIssue(id, site, run)) { LOGGER.log(Level.INFO, "No known Jira project corresponding to id: ''{0}''", id); continue; } LOGGER.log(Level.FINE, "Annotating Jira id: ''{0}''", id); URL url, alternativeUrl; try { url = site.getUrl(id); } catch (MalformedURLException e) { throw new AssertionError(e); // impossible } try { alternativeUrl = site.getAlternativeUrl(id); if (alternativeUrl != null) { url = alternativeUrl; } } catch (MalformedURLException e) { LOGGER.log(Level.WARNING, "Failed to construct alternative URL for Jira link. " + e.getMessage()); // This should not fail, since we already have an URL object. Exceptions would happen elsewhere. throw new AssertionError(e); } JiraIssue issue = null; if (a != null) { issue = a.getIssue(id); } if (issue == null) { try { issue = site.getIssue(id); if (issue != null) { issuesToBeSaved.add(issue); } } catch (Exception e) { LOGGER.log(Level.FINE, "Error getting remote issue " + id, e); } } if (issue == null) { text.addMarkup(m.start(1), m.end(1), "", ""); } else { text.addMarkup( m.start(1), m.end(1), String.format("", url, Util.escape(issue.getSummary())), ""); } } else { LOGGER.log(Level.WARNING, "The Jira pattern ''{0}'' doesn't define a capturing group!", pattern); } } if (!issuesToBeSaved.isEmpty()) { saveIssues(run, a, issuesToBeSaved); } } /** * Checks if the given Jira id will be likely to exist in this issue tracker. * This method checks whether the key portion is a valid key (except that * it can potentially use stale data). Number portion is not checked at all. * * @param id String like MNG-1234 */ protected boolean hasProjectForIssue(String id, JiraSite site, Run run) { int idx = id.indexOf('-'); if (idx == -1) { return false; } Set keys = site.getProjectKeys(run.getParent()); return keys.contains(id.substring(0, idx).toUpperCase()); } private void saveIssues(Run build, JiraBuildAction a, Set issuesToBeSaved) { if (a != null) { a.addIssues(issuesToBeSaved); } else { JiraBuildAction action = new JiraBuildAction(issuesToBeSaved); build.addAction(action); } try { build.save(); } catch (final IOException e) { LOGGER.log(Level.WARNING, "Error saving updated build", e); } } JiraSite getSiteForProject(Job project) { return JiraSite.get(project); } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraCreateIssueNotifier.java ================================================ package hudson.plugins.jira; import static hudson.plugins.jira.JiraRestService.BUG_ISSUE_TYPE_ID; import com.atlassian.jira.rest.client.api.RestClientException; import com.atlassian.jira.rest.client.api.StatusCategory; import com.atlassian.jira.rest.client.api.domain.Issue; import com.atlassian.jira.rest.client.api.domain.IssueType; import com.atlassian.jira.rest.client.api.domain.Priority; import com.atlassian.jira.rest.client.api.domain.Status; import hudson.EnvVars; import hudson.Extension; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Item; import hudson.model.Result; import hudson.model.TaskListener; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Notifier; import hudson.tasks.Publisher; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.List; import java.util.logging.Logger; import java.util.stream.Collectors; import net.sf.json.JSONObject; import org.apache.commons.lang3.StringUtils; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest2; /** * When a build fails it creates jira issues. * Repeated failures does not create a new issue but update the existing issue * until the issue is closed. * * @author Rupali Behera rupali@vertisinfotech.com */ public class JiraCreateIssueNotifier extends Notifier { private static final Logger LOG = Logger.getLogger(JiraCreateIssueNotifier.class.getName()); private String projectKey; private String testDescription; private String assignee; private String component; private Long typeId; private Long priorityId; private Integer actionIdOnSuccess; enum finishedStatuses { Closed, Done, Resolved } @DataBoundConstructor public JiraCreateIssueNotifier( String projectKey, String testDescription, String assignee, String component, Long typeId, Long priorityId, Integer actionIdOnSuccess) { if (projectKey == null) { throw new IllegalArgumentException("Project key cannot be null"); } this.projectKey = projectKey; this.testDescription = testDescription; this.assignee = assignee; this.component = component; this.typeId = typeId; this.priorityId = priorityId; this.actionIdOnSuccess = actionIdOnSuccess; } public String getProjectKey() { return projectKey; } public void setProjectKey(String projectKey) { this.projectKey = projectKey; } public String getTestDescription() { return testDescription; } public void setTestDescription(String testDescription) { this.testDescription = testDescription; } public String getAssignee() { return assignee; } public void setAssignee(String assignee) { this.assignee = assignee; } public String getComponent() { return component; } public void setComponent(String component) { this.component = component; } public Long getTypeId() { return typeId; } public Long getPriorityId() { return priorityId; } public Integer getActionIdOnSuccess() { return actionIdOnSuccess; } @Override public BuildStepDescriptor getDescriptor() { return DESCRIPTOR; } @Extension public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); @Override public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException { String jobDirPath = build.getProject().getBuildDir().getPath(); String filename = jobDirPath + File.separator + "issue.txt"; EnvVars vars = build.getEnvironment(TaskListener.NULL); Result currentBuildResult = build.getResult(); Result previousBuildResult = null; AbstractBuild previousBuild = build.getPreviousCompletedBuild(); if (previousBuild != null) { previousBuildResult = previousBuild.getResult(); } if (currentBuildResult != Result.ABORTED && previousBuild != null) { try { if (currentBuildResult == Result.FAILURE) { currentBuildResultFailure(build, listener, previousBuildResult, filename, vars); } if (currentBuildResult == Result.SUCCESS) { currentBuildResultSuccess(build, listener, previousBuildResult, filename, vars); } } catch (RestClientException e) { listener.getLogger().println(e.getMessage()); } } return true; } /** * It creates a issue in the given project, with the given description, * assignee,components and summary. * The created issue ID is saved to the file at "filename". * * @param build build * @param filename filename * @return issue id * @throws IOException if IO fails * @throws InterruptedException if thread is interrupted */ private Issue createJiraIssue(AbstractBuild build, String filename) throws IOException, InterruptedException { EnvVars vars = build.getEnvironment(TaskListener.NULL); JiraSession session = getJiraSession(build); String buildName = getBuildName(vars); String summary = String.format("Build %s failed", buildName); String description = String.format( "%s%n%nThe build %s has failed.%nFirst failed run: %s", (this.testDescription.equals("")) ? "No description is provided" : vars.expand(this.testDescription), buildName, getBuildDetailsString(vars)); Iterable components = Arrays.stream(component.split(",")) .filter(s -> !StringUtils.isEmpty(s)) .map(StringUtils::trim) .collect(Collectors.toList()); Long type = typeId; if (type == null || type == 0) { // zero is default / invalid selection LOG.info("Returning default issue type id " + BUG_ISSUE_TYPE_ID); type = BUG_ISSUE_TYPE_ID; } Long priority = priorityId; if (priority != null && priority == 0) { priority = null; // remove invalid priority selection } Issue issue = session.createIssue(projectKey, description, assignee, components, summary, type, priority); writeInFile(filename, issue); return issue; } /** * Returns the status of the issue. * * @param build build * @param id issue key * @return Status of the issue * @throws IOException if IO fails */ private Status getStatus(AbstractBuild build, String id) throws IOException { JiraSession session = getJiraSession(build); Issue issue = session.getIssueByKey(id); return issue.getStatus(); } /** * Adds a comment to the existing issue. * * @param build build * @param listener listener * @param id issue key * @param comment comment text * @throws IOException if IO fails */ private void addComment(AbstractBuild build, BuildListener listener, String id, String comment) throws IOException { JiraSession session = getJiraSession(build); session.addCommentWithoutConstrains(id, comment); listener.getLogger().println(String.format("[%s] Commented issue", id)); } /** * Returns the issue id * * @param filename file from which to read the issue name * @return issue ID * @throws IOException if IO fails * @throws InterruptedException if thread is interrupted */ private String getIssue(String filename) throws IOException, InterruptedException { String issueId = ""; Path path = Paths.get(filename); if (Files.notExists(path)) { return null; } try (BufferedReader br = Files.newBufferedReader(path)) { String issue; while ((issue = br.readLine()) != null) { issueId = issue; } return StringUtils.trimToNull(issueId); } catch (FileNotFoundException e) { return null; } } JiraSite getSiteForProject(AbstractProject project) { return JiraSite.get(project); } /** * Returns the jira session. * * @param build build * @return JiraSession * @throws IOException if IO fails */ private JiraSession getJiraSession(AbstractBuild build) throws IOException { JiraSite site = getSiteForProject(build.getProject()); if (site == null) { throw new IllegalStateException( "Jira site needs to be configured in the project " + build.getFullDisplayName()); } JiraSession session = site.getSession(build.getProject()); if (session == null) { throw new IllegalStateException("Remote access for Jira isn't configured in Jenkins"); } return session; } /** * @param filename filename */ private void deleteFile(String filename) { File file = new File(filename); if (file.exists() && !file.delete()) { LOG.warning("WARNING: couldn't delete file: " + filename); } } /** * Writes the issue id in the file, which is stored in the Job's directory. * * @param filename filenam * @param issue issue * @throws FileNotFoundException if file not found */ private void writeInFile(String filename, Issue issue) throws IOException { // olamy really weird to write an empty file especially with null // but backward compat and unit tests assert that..... // can't believe such stuff has been merged...... try (BufferedWriter bw = Files.newBufferedWriter(Paths.get(filename))) { bw.write(issue.getKey() == null ? "null" : issue.getKey()); } } /** * when the current build fails it checks for the previous build's result, * creates jira issue if the result was "success" and adds comment if the result * was "fail". * It adds comment until the previously created issue is closed. */ private void currentBuildResultFailure( AbstractBuild build, BuildListener listener, Result previousBuildResult, String filename, EnvVars vars) throws InterruptedException, IOException, RestClientException { if (previousBuildResult == Result.FAILURE) { String comment = String.format("Build is still failing.%nFailed run: %s", getBuildDetailsString(vars)); // Get the issue-id which was filed when the previous built failed String issueId = getIssue(filename); if (issueId != null) { try { // The status of the issue which was filed when the previous build failed Status status = getStatus(build, issueId); // Issue Closed, need to open new one if (isDone(status)) { listener.getLogger().println("The previous build also failed but the issue is closed"); deleteFile(filename); Issue issue = createJiraIssue(build, filename); LOG.info(String.format("[%s] created.", issue.getKey())); listener.getLogger().println("Build failed, created Jira issue " + issue.getKey()); } else { addComment(build, listener, issueId, comment); LOG.info(String.format("[%s] The previous build also failed, comment added.", issueId)); } } catch (IOException e) { LOG.warning(String.format("[%s] - error processing Jira change: %s", issueId, e.getMessage())); } } } if (previousBuildResult == Result.SUCCESS || previousBuildResult == Result.ABORTED) { try { Issue issue = createJiraIssue(build, filename); LOG.info(String.format("[%s] created.", issue.getKey())); listener.getLogger().println("Build failed, created Jira issue " + issue.getKey()); } catch (IOException e) { listener.error("Error creating Jira issue : " + e.getMessage()); LOG.warning("Error creating Jira issue\n" + e.getMessage()); } } } /** * when the current build's result is "success", * it checks for the previous build's result and adds comment until the * previously created issue is closed. * * @param build build * @param previousBuildResult previous build result * @param filename filename * @param vars variables * @throws InterruptedException if thread isinterrupted * @throws IOException if IO fails */ private void currentBuildResultSuccess( AbstractBuild build, BuildListener listener, Result previousBuildResult, String filename, EnvVars vars) throws InterruptedException, IOException { if (previousBuildResult == Result.FAILURE || previousBuildResult == Result.SUCCESS) { String comment = String.format("Previously failing build now is OK.%n Passed run: %s", getBuildDetailsString(vars)); String issueId = getIssue(filename); // if issue exists it will check the status and comment or delete the file // accordingly if (issueId != null) { try { Status status = getStatus(build, issueId); // if issue is in closed status if (isDone(status)) { LOG.info(String.format("%s is closed", issueId)); deleteFile(filename); } else { LOG.info(String.format("%s is not closed, comment was added.", issueId)); addComment(build, listener, issueId, comment); if (actionIdOnSuccess != null && actionIdOnSuccess > 0) { progressWorkflowAction(build, issueId, actionIdOnSuccess); } } } catch (IOException e) { listener.error("Error updating Jira issue " + issueId + " : " + e.getMessage()); LOG.warning("Error updating Jira issue " + issueId + "\n" + e); } } } } static boolean isDone(Status status) { if (status.getName().equalsIgnoreCase(finishedStatuses.Closed.toString()) || status.getName().equalsIgnoreCase(finishedStatuses.Resolved.toString()) || status.getName().equalsIgnoreCase(finishedStatuses.Done.toString())) { return true; } StatusCategory category = status.getStatusCategory(); if (category == null) { return false; } return "done".equals(category.getKey()); } private void progressWorkflowAction(AbstractBuild build, String issueId, Integer actionId) throws IOException { JiraSession session = getJiraSession(build); session.progressWorkflowAction(issueId, actionId); } /** * Returns build details string in wiki format, with hyperlinks. * * @param vars environment variables * @return build details string */ private String getBuildDetailsString(EnvVars vars) { final String buildURL = vars.get("BUILD_URL"); return String.format("[%s|%s] [console log|%s]", getBuildName(vars), buildURL, buildURL.concat("console")); } /** * Returns build name in format BUILD#10 * * @param vars environment variables * @return String build name */ private String getBuildName(EnvVars vars) { final String jobName = vars.get("JOB_NAME"); final String buildNumber = vars.get("BUILD_NUMBER"); return String.format("%s #%s", jobName, buildNumber); } public static class DescriptorImpl extends BuildStepDescriptor { public DescriptorImpl() { super(JiraCreateIssueNotifier.class); } public FormValidation doCheckProjectKey(@QueryParameter String value) throws IOException { if (value.length() == 0) { return FormValidation.error("Please set the project key"); } return FormValidation.ok(); } public ListBoxModel doFillPriorityIdItems(@AncestorInPath final Item item) { ListBoxModel items = new ListBoxModel().add(""); // optional field List sites = JiraSite.getJiraSites(item); for (JiraSite site : sites) { JiraSession session = site.getSession(item); if (session != null) { for (Priority priority : session.getPriorities()) { items.add("[" + site.getName() + "] " + priority.getName(), String.valueOf(priority.getId())); } } } return items; } public ListBoxModel doFillTypeIdItems(@AncestorInPath final Item item) { ListBoxModel items = new ListBoxModel().add(""); // optional field List sites = JiraSite.getJiraSites(item); for (JiraSite site : sites) { JiraSession session = site.getSession(item); if (session != null) { for (IssueType type : session.getIssueTypes()) { items.add("[" + site.getName() + "] " + type.getName(), String.valueOf(type.getId())); } } } return items; } @Override public JiraCreateIssueNotifier newInstance(StaplerRequest2 req, JSONObject formData) throws FormException { return req.bindJSON(JiraCreateIssueNotifier.class, formData); } @Override public boolean isApplicable(Class jobType) { return true; } @Override public String getDisplayName() { return Messages.JiraCreateIssueNotifier_DisplayName(); } @Override public String getHelpFile() { return "/plugin/jira/help-jira-create-issue.html"; } } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraCreateReleaseNotes.java ================================================ package hudson.plugins.jira; import static org.apache.commons.lang3.StringUtils.defaultIfEmpty; import static org.apache.commons.lang3.StringUtils.isEmpty; import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Job; import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; import hudson.tasks.BuildStepMonitor; import hudson.tasks.BuildWrapperDescriptor; import java.io.IOException; import java.util.HashMap; import java.util.Map; import jenkins.tasks.SimpleBuildWrapper; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; public class JiraCreateReleaseNotes extends SimpleBuildWrapper { @Extension @Symbol("jiraCreateReleaseNotes") public static final class Descriptor extends BuildWrapperDescriptor { @Override public String getDisplayName() { return "Generate Release Notes"; } @Override public boolean isApplicable(final AbstractProject item) { return true; } } public static final String DEFAULT_FILTER = "status in (Resolved, Closed)"; public static final String DEFAULT_ENVVAR_NAME = "RELEASE_NOTES"; private String jiraEnvironmentVariable; private String jiraProjectKey; private String jiraRelease; private String jiraFilter; // not in use anymore, as it always resets the given filter with the DEFAULT_FILTER public JiraCreateReleaseNotes( final String jiraProjectKey, final String jiraRelease, final String jiraEnvironmentVariable) { this(jiraProjectKey, jiraRelease, jiraEnvironmentVariable, DEFAULT_FILTER); } @DataBoundConstructor public JiraCreateReleaseNotes( final String jiraProjectKey, final String jiraRelease, final String jiraEnvironmentVariable, final String jiraFilter) { this.jiraRelease = jiraRelease; this.jiraProjectKey = jiraProjectKey; this.jiraEnvironmentVariable = defaultIfEmpty(jiraEnvironmentVariable, DEFAULT_ENVVAR_NAME); this.jiraFilter = defaultIfEmpty(jiraFilter, DEFAULT_FILTER); } public String getJiraEnvironmentVariable() { return jiraEnvironmentVariable; } public String getJiraFilter() { return jiraFilter; } public String getJiraProjectKey() { return jiraProjectKey; } public String getJiraRelease() { return jiraRelease; } public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } public void setJiraEnvironmentVariable(final String jiraEnvironmentVariable) { this.jiraEnvironmentVariable = jiraEnvironmentVariable; } public void setJiraFilter(final String jiraFilter) { this.jiraFilter = jiraFilter; } public void setJiraProjectKey(final String jiraProjectKey) { this.jiraProjectKey = jiraProjectKey; } public void setJiraRelease(final String jiraRelease) { this.jiraRelease = jiraRelease; } JiraSite getSiteForProject(Job project) { return JiraSite.get(project); } @Override public void setUp( Context context, Run run, FilePath workspace, Launcher launcher, TaskListener listener, EnvVars initialEnvironment) throws IOException, InterruptedException { final JiraSite site = getSiteForProject(run.getParent()); String realRelease = null; String realProjectKey = null; String releaseNotes = "No Release Notes"; String realFilter = DEFAULT_FILTER; try { realRelease = run.getEnvironment(listener).expand(jiraRelease); realProjectKey = run.getEnvironment(listener).expand(jiraProjectKey); realFilter = run.getEnvironment(listener).expand(jiraFilter); if (isEmpty(realRelease)) { throw new IllegalArgumentException("No version specified"); } if (isEmpty(realProjectKey)) { throw new IllegalArgumentException("No project specified"); } if ((realRelease != null) && !realRelease.isEmpty()) { releaseNotes = site.getReleaseNotesForFixVersion(realProjectKey, realRelease, realFilter); } else { listener.getLogger().printf("No release version found, skipping Release Notes generation%n"); } } catch (Exception e) { e.printStackTrace(listener.fatalError( "Unable to generate release notes for Jira version %s/%s: %s", realRelease, realProjectKey, e)); if (listener instanceof BuildListener) { ((BuildListener) listener).finished(Result.FAILURE); } } final Map envMap = new HashMap<>(); envMap.put(jiraEnvironmentVariable, releaseNotes); context.getEnv().putAll(envMap); } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraEnvironmentContributingAction.java ================================================ package hudson.plugins.jira; import edu.umd.cs.findbugs.annotations.Nullable; import hudson.EnvVars; import hudson.model.AbstractBuild; import hudson.model.EnvironmentContributingAction; import hudson.model.InvisibleAction; /* * JiraEnvironmentVariableBuilder adds an instance of this class to the build to provide the environment variables * */ public class JiraEnvironmentContributingAction extends InvisibleAction implements EnvironmentContributingAction { public static final String ISSUES_VARIABLE_NAME = "JIRA_ISSUES"; public static final String JIRA_URL_VARIABLE_NAME = "JIRA_URL"; public static final String ISSUES_SIZE_VARIABLE_NAME = "JIRA_ISSUES_SIZE"; private final String issuesList; private final Integer issuesSize; private final String jiraUrl; @Nullable public String getIssuesList() { return issuesList; } public Integer getNumberOfIssues() { return issuesSize == null ? Integer.valueOf(0) : issuesSize; } @Nullable public String getJiraUrl() { return jiraUrl; } public JiraEnvironmentContributingAction(String issuesList, Integer issuesSize, String jiraUrl) { this.issuesList = issuesList; this.issuesSize = issuesSize; this.jiraUrl = jiraUrl; } @Override public void buildEnvVars(AbstractBuild ab, EnvVars ev) { if (ev != null) { ev.put(ISSUES_VARIABLE_NAME, getIssuesList()); ev.put(ISSUES_SIZE_VARIABLE_NAME, getNumberOfIssues().toString()); ev.put(JIRA_URL_VARIABLE_NAME, getJiraUrl()); } } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraEnvironmentVariableBuilder.java ================================================ package hudson.plugins.jira; import com.atlassian.jira.rest.client.api.RestClientException; import hudson.Extension; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.plugins.jira.selector.AbstractIssueSelector; import hudson.plugins.jira.selector.DefaultIssueSelector; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.Builder; import java.io.IOException; import java.util.Set; import jenkins.model.Jenkins; import org.apache.commons.lang3.StringUtils; import org.kohsuke.stapler.DataBoundConstructor; /** * Adds Jira related environment variables to the build */ public class JiraEnvironmentVariableBuilder extends Builder { private AbstractIssueSelector issueSelector; @DataBoundConstructor public JiraEnvironmentVariableBuilder(AbstractIssueSelector issueSelector) { this.issueSelector = issueSelector; } public AbstractIssueSelector getIssueSelector() { AbstractIssueSelector uis = this.issueSelector; if (uis == null) { uis = new DefaultIssueSelector(); } return (this.issueSelector = uis); } JiraSite getSiteForProject(AbstractProject project) { return JiraSite.get(project); } @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { JiraSite site = getSiteForProject(build.getProject()); if (site == null) { listener.getLogger().println(Messages.JiraEnvironmentVariableBuilder_NoJiraSite()); return false; } Set ids; try { ids = getIssueSelector().findIssueIds(build, site, listener); } catch (RestClientException e) { listener.getLogger().println(e.getMessage()); return false; } String idList = StringUtils.join(ids, ","); Integer idListSize = ids.size(); listener.getLogger() .println(Messages.JiraEnvironmentVariableBuilder_Updating( JiraEnvironmentContributingAction.ISSUES_VARIABLE_NAME, idList)); listener.getLogger() .println(Messages.JiraEnvironmentVariableBuilder_Updating( JiraEnvironmentContributingAction.ISSUES_SIZE_VARIABLE_NAME, idListSize)); build.addAction(new JiraEnvironmentContributingAction(idList, idListSize, site.getName())); return true; } /** * Descriptor for {@link JiraEnvironmentVariableBuilder}. */ @Extension public static final class DescriptorImpl extends BuildStepDescriptor { @Override public boolean isApplicable(Class klass) { return true; } @Override public String getDisplayName() { return Messages.JiraEnvironmentVariableBuilder_DisplayName(); } public boolean hasIssueSelectors() { return Jenkins.get().getDescriptorList(AbstractIssueSelector.class).size() > 0; } } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraFolderProperty.java ================================================ package hudson.plugins.jira; import com.cloudbees.hudson.plugins.folder.AbstractFolder; import com.cloudbees.hudson.plugins.folder.AbstractFolderProperty; import com.cloudbees.hudson.plugins.folder.AbstractFolderPropertyDescriptor; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.model.ItemGroup; import java.util.Collections; import java.util.List; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; /** * Provides folder level Jira configuration. */ public class JiraFolderProperty extends AbstractFolderProperty> { /** * Hold the Jira sites configuration. */ private List sites = Collections.emptyList(); /** * Constructor. */ @DataBoundConstructor public JiraFolderProperty() {} /** * Return the Jira sites. * * @return the Jira sites */ public JiraSite[] getSites() { return sites.toArray(new JiraSite[0]); } /** * @param site the Jira site * @deprecated use {@link #setSites(List)} instead */ @Deprecated public void setSites(JiraSite site) { sites.add(site); } @DataBoundSetter public void setSites(List sites) { this.sites = sites; } /** * @deprecated use {@link JiraSite#getSitesFromFolders(ItemGroup)} */ @Deprecated public static List getSitesFromFolders(ItemGroup itemGroup) { return JiraSite.getSitesFromFolders(itemGroup); } /** * Descriptor class. */ @Extension public static class DescriptorImpl extends AbstractFolderPropertyDescriptor { @NonNull @Override public String getDisplayName() { return Messages.JiraFolderProperty_DisplayName(); } } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraGlobalConfiguration.java ================================================ package hudson.plugins.jira; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; import hudson.util.PersistedList; import java.util.List; import jenkins.model.GlobalConfiguration; import jenkins.model.Jenkins; import org.kohsuke.stapler.DataBoundSetter; @Extension public class JiraGlobalConfiguration extends GlobalConfiguration { @NonNull public static JiraGlobalConfiguration get() { return (JiraGlobalConfiguration) Jenkins.get().getDescriptorOrDie(JiraGlobalConfiguration.class); } @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Backwards compatibility") public List sites = new PersistedList<>(this); public JiraGlobalConfiguration() { load(); } public List getSites() { return sites; } @DataBoundSetter public void setSites(List sites) { this.sites = sites; save(); } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraIssueMigrator.java ================================================ package hudson.plugins.jira; import hudson.Extension; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Result; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Notifier; import hudson.tasks.Publisher; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest2; public class JiraIssueMigrator extends Notifier { private static final long serialVersionUID = 6909671291180081586L; private String jiraProjectKey; private String jiraRelease; private String jiraReplaceVersion; private String jiraQuery; private boolean addRelease; @DataBoundConstructor public JiraIssueMigrator( String jiraProjectKey, String jiraRelease, String jiraQuery, String jiraReplaceVersion, boolean addRelease) { this.jiraRelease = jiraRelease; this.jiraProjectKey = jiraProjectKey; this.jiraQuery = jiraQuery; this.jiraReplaceVersion = jiraReplaceVersion; this.addRelease = addRelease; } public String getJiraRelease() { return jiraRelease; } public void setJiraRelease(String jiraRelease) { this.jiraRelease = jiraRelease; } public String getJiraProjectKey() { return jiraProjectKey; } public void setJiraProjectKey(String jiraProjectKey) { this.jiraProjectKey = jiraProjectKey; } public String getJiraQuery() { return jiraQuery; } public void setJiraQuery(String jiraQuery) { this.jiraQuery = jiraQuery; } public String getJiraReplaceVersion() { return jiraReplaceVersion; } public void setJiraReplaceVersion(String jiraReplaceVersion) { this.jiraReplaceVersion = jiraReplaceVersion; } public boolean isAddRelease() { return addRelease; } public void setAddRelease(boolean addRelease) { this.addRelease = addRelease; } @Override public BuildStepDescriptor getDescriptor() { return DESCRIPTOR; } @Extension public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) { String realRelease = null; String realReplace = null; String realQuery = ""; String realProjectKey = null; try { realRelease = build.getEnvironment(listener).expand(jiraRelease); realReplace = build.getEnvironment(listener).expand(jiraReplaceVersion); realProjectKey = build.getEnvironment(listener).expand(jiraProjectKey); if (realRelease == null || realRelease.isEmpty()) { throw new IllegalArgumentException("Release is Empty"); } if (realProjectKey == null || realProjectKey.isEmpty()) { throw new IllegalArgumentException("No project specified"); } realQuery = build.getEnvironment(listener).expand(jiraQuery); if (realQuery == null || realQuery.isEmpty()) { throw new IllegalArgumentException("JQL query is Empty"); } JiraSite site = getJiraSiteForProject(build.getProject()); if (addRelease) { site.addFixVersionToIssue(realProjectKey, realRelease, realQuery); } else { if (realReplace == null || realReplace.isEmpty()) { site.migrateIssuesToFixVersion(realProjectKey, realRelease, realQuery); } else { site.replaceFixVersion(realProjectKey, realReplace, realRelease, realQuery); } } } catch (Exception e) { e.printStackTrace( listener.fatalError("Unable to release jira version %s/%s: %s", realRelease, realProjectKey, e)); listener.finished(Result.FAILURE); return false; } return true; } JiraSite getJiraSiteForProject(AbstractProject project) { return JiraSite.get(project); } @Override public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } public static class DescriptorImpl extends BuildStepDescriptor { public DescriptorImpl() { super(JiraIssueMigrator.class); } @Override public JiraIssueMigrator newInstance(StaplerRequest2 req, JSONObject formData) throws FormException { return req.bindJSON(JiraIssueMigrator.class, formData); } @Override public boolean isApplicable(Class jobType) { return true; } @Override public String getDisplayName() { return Messages.JiraReleaseVersionMigrator_DisplayName(); } @Override public String getHelpFile() { return "/plugin/jira/help-release-migrate.html"; } } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraIssueUpdateBuilder.java ================================================ /* * Copyright 2012 MeetMe, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package hudson.plugins.jira; import com.atlassian.jira.rest.client.api.RestClientException; import hudson.EnvVars; import hudson.Extension; import hudson.Util; import hudson.model.*; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.Builder; import hudson.util.FormValidation; import java.io.IOException; import jenkins.tasks.SimpleBuildStep; import org.apache.commons.lang3.StringUtils; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; /** * Build step that will mass-update all issues matching a JQL query, using the specified workflow * action name (e.g., "Resolve Issue", "Close Issue"). * * @author Joe Hansche jhansche@myyearbook.com */ public class JiraIssueUpdateBuilder extends Builder implements SimpleBuildStep { private final String jqlSearch; private final String workflowActionName; private final String comment; @DataBoundConstructor public JiraIssueUpdateBuilder(String jqlSearch, String workflowActionName, String comment) { this.jqlSearch = Util.fixEmptyAndTrim(jqlSearch); this.workflowActionName = Util.fixEmptyAndTrim(workflowActionName); this.comment = Util.fixEmptyAndTrim(comment); } /** * @return the jql */ public String getJqlSearch() { return jqlSearch; } /** * @return the workflowActionName */ public String getWorkflowActionName() { return workflowActionName; } /** * @return the comment */ public String getComment() { return comment; } JiraSite getSiteForJob(Job job) { return JiraSite.get(job); } /** * Performs the actual update based on job configuration. */ @Override public void perform(Run run, EnvVars env, TaskListener listener) throws InterruptedException, IOException { String realComment = Util.fixEmptyAndTrim(env.expand(comment)); String realJql = Util.fixEmptyAndTrim(env.expand(jqlSearch)); String realWorkflowActionName = Util.fixEmptyAndTrim(env.expand(workflowActionName)); JiraSite site = getSiteForJob(run.getParent()); if (site == null) { listener.getLogger().println(Messages.NoJiraSite()); run.setResult(Result.FAILURE); return; } if (StringUtils.isNotEmpty(realWorkflowActionName)) { listener.getLogger().println(Messages.JiraIssueUpdateBuilder_UpdatingWithAction(realWorkflowActionName)); } listener.getLogger().println("[Jira] JQL: " + realJql); try { if (!site.progressMatchingIssues(realJql, realWorkflowActionName, realComment, listener.getLogger())) { listener.getLogger().println(Messages.JiraIssueUpdateBuilder_SomeIssuesFailed()); run.setResult(Result.UNSTABLE); } } catch (RestClientException e) { listener.getLogger().println(e.getMessage()); run.setResult(Result.FAILURE); } } @Override public boolean requiresWorkspace() { return false; } @Override public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } /** * Descriptor for {@link JiraIssueUpdateBuilder}. */ @Extension @Symbol("jiraExecuteWorkflow") public static final class DescriptorImpl extends BuildStepDescriptor { /** * Performs on-the-fly validation of the form field 'Jql'. * * @param value This parameter receives the value that the user has typed. * @return Indicates the outcome of the validation. This is sent to the browser. */ public FormValidation doCheckJqlSearch(@QueryParameter String value) { if (value.length() == 0) { return FormValidation.error(Messages.JiraIssueUpdateBuilder_NoJqlSearch()); } return FormValidation.ok(); } public FormValidation doCheckWorkflowActionName(@QueryParameter String value) { if (Util.fixNull(value).trim().length() == 0) { return FormValidation.warning(Messages.JiraIssueUpdateBuilder_NoWorkflowAction()); } return FormValidation.ok(); } @Override public boolean isApplicable(Class klass) { return true; } /** * This human readable name is used in the configuration screen. */ @Override public String getDisplayName() { return Messages.JiraIssueUpdateBuilder_DisplayName(); } } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraIssueUpdater.java ================================================ package hudson.plugins.jira; import hudson.EnvVars; import hudson.Extension; import hudson.Launcher; import hudson.matrix.MatrixAggregatable; import hudson.matrix.MatrixAggregator; import hudson.matrix.MatrixBuild; import hudson.matrix.MatrixRun; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.jira.selector.AbstractIssueSelector; import hudson.plugins.jira.selector.DefaultIssueSelector; import hudson.scm.SCM; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Publisher; import hudson.tasks.Recorder; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.List; import jenkins.model.Jenkins; import jenkins.tasks.SimpleBuildStep; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; /** * Parses build changelog for Jira issue IDs and then * updates Jira issues accordingly. * * @author Kohsuke Kawaguchi */ public class JiraIssueUpdater extends Recorder implements MatrixAggregatable, SimpleBuildStep { private AbstractIssueSelector issueSelector; private SCM scm; private List labels; @DataBoundConstructor public JiraIssueUpdater(AbstractIssueSelector issueSelector, SCM scm, List labels) { this.issueSelector = issueSelector; this.scm = scm; if (labels != null) { this.labels = labels; } else { this.labels = new ArrayList(); } } @Override public void perform(Run run, EnvVars env, TaskListener listener) throws InterruptedException, IOException { // Don't do anything for individual matrix runs. if (run instanceof MatrixRun) { return; } else if (run instanceof AbstractBuild) { AbstractBuild abstractBuild = (AbstractBuild) run; Updater updater = new Updater(abstractBuild.getParent().getScm(), labels); updater.perform(run, listener, getIssueSelector()); } else if (scm != null) { Updater updater = new Updater(scm, labels); updater.perform(run, listener, getIssueSelector()); } else { throw new IllegalArgumentException( "Unsupported run type " + run.getClass().getName()); } } @Override public boolean requiresWorkspace() { return false; } @Override public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } @Override public DescriptorImpl getDescriptor() { return DESCRIPTOR; } public AbstractIssueSelector getIssueSelector() { AbstractIssueSelector uis = this.issueSelector; if (uis == null) { uis = new DefaultIssueSelector(); } return (this.issueSelector = uis); } public SCM getScm() { return scm; } public List getLabels() { return labels; } @Extension public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); @Override public MatrixAggregator createAggregator(MatrixBuild build, Launcher launcher, BuildListener listener) { return new MatrixAggregator(build, launcher, listener) { @Override public boolean endBuild() throws InterruptedException, IOException { PrintStream logger = listener.getLogger(); logger.println("End of Matrix Build. Updating Jira."); Updater updater = new Updater(this.build.getParent().getScm(), labels); return updater.perform(this.build, this.listener, getIssueSelector()); } }; } @Symbol("jiraCommentIssues") public static final class DescriptorImpl extends BuildStepDescriptor { private DescriptorImpl() { super(JiraIssueUpdater.class); } @Override public String getDisplayName() { // Displayed in the publisher section return Messages.JiraIssueUpdater_DisplayName(); } @Override public String getHelpFile() { return "/plugin/jira/help.html"; } @Override @SuppressWarnings("unchecked") public boolean isApplicable(Class jobType) { return true; } public boolean hasIssueSelectors() { return Jenkins.getInstance() .getDescriptorList(AbstractIssueSelector.class) .size() > 1; } } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraJobAction.java ================================================ package hudson.plugins.jira; import com.atlassian.jira.rest.client.api.RestClientException; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; import hudson.model.Action; import hudson.model.Job; import hudson.model.TaskListener; import hudson.model.listeners.RunListener; import hudson.plugins.jira.model.JiraIssue; import java.io.IOException; import java.net.URL; import java.net.URLDecoder; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import jenkins.branch.MultiBranchProject; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; /** * JiraJobAction is to store a reference to the {@link JiraIssue} that represents work * being done for a {@link WorkflowJob} (branch or PR) belonging to a {@link MultiBranchProject} * * Any branches with the whole key in the name or after a prefix will have this action attached. * e.g. "JENKINS-1234" and "feature/JENKINS-1234" will have this action with the issue JENKINS-1234 referenced */ @ExportedBean public class JiraJobAction implements Action { private static final Logger LOGGER = Logger.getLogger(JiraJobAction.class.getName()); private final Job owner; private final JiraIssue issue; @DataBoundConstructor public JiraJobAction(Job owner, JiraIssue issue) { this.owner = owner; this.issue = issue; } /** * @return issue representing the job */ @Exported public JiraIssue getIssue() { return issue; } /** * @return url of the Jira server */ @Exported @Nullable public String getServerURL() { JiraSite jiraSite = JiraSite.get(owner); URL url = jiraSite != null ? jiraSite.getUrl() : null; return url != null ? url.toString() : null; } /** * Adds a {@link JiraJobAction} to a {@link WorkflowJob} if it belongs to a {@link MultiBranchProject} * and its name contains an Jira issue key * @param job to add the property to * @param site to fetch issue data * @throws IOException if something goes wrong fetching the Jira issue */ public static void setAction(@NonNull Job job, @NonNull JiraSite site) throws IOException { // If there is already a action set then skip if (job.getAction(JiraJobAction.class) != null) { return; } // Exclude all non-multibranch workflow jobs if (!(job.getParent() instanceof MultiBranchProject)) { return; } // Find the first Jira issue key in the branch name // If it exists, create the action and set it Pattern pattern = site.getIssuePattern(); // Pipeline will URL encode job names if the branch or PR name contains a '/' or other non-URL safe characters String decodedJobName = URLDecoder.decode(job.getName(), "UTF-8"); String issueKey = null; Matcher m = pattern.matcher(decodedJobName); while (m.find()) { if (m.groupCount() >= 1) { issueKey = m.group(1); break; } } if (issueKey != null) { JiraIssue issue = site.getIssue(issueKey); if (issue != null) { job.addAction(new JiraJobAction(job, issue)); job.save(); } } } @Override public String getIconFileName() { return null; } @Override public String getDisplayName() { return "Jira"; } @Override public String getUrlName() { return "jira"; } @Extension public static final class RunListenerImpl extends RunListener { @Override public void onStarted(WorkflowRun workflowRun, TaskListener listener) { WorkflowJob parent = workflowRun.getParent(); JiraSite site = JiraSite.get(parent); if (site != null) { try { setAction(parent, site); } catch (IOException | RestClientException e) { LOGGER.log(Level.WARNING, "Could not set JiraJobAction for <" + parent.getFullName() + ">", e); listener.getLogger().println(e.getMessage()); } } } } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraMailAddressResolver.java ================================================ package hudson.plugins.jira; import hudson.Extension; import hudson.model.Job; import hudson.model.User; import hudson.tasks.MailAddressResolver; import java.util.List; import java.util.logging.Logger; import java.util.regex.Pattern; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest2; /** * Resolve user email by searching his userId as username in Jira. * * @author Honza Brázdil jbrazdil@redhat.com */ @Extension public class JiraMailAddressResolver extends MailAddressResolver { private static final Logger LOGGER = Logger.getLogger(JiraMailAddressResolver.class.getName()); /** * Boolean to disable the Jira mail address resolver. *

* To disable set the System property "-Dhudson.plugins.jira.JiraMailAddressResolver.disabled=true" */ public static boolean disabled = Boolean.getBoolean(JiraMailAddressResolver.class.getName() + ".disabled"); @Override public String findMailAddressFor(User u) { if (disabled) { return null; } String username = u.getId(); Job job = null; StaplerRequest2 req = Stapler.getCurrentRequest2(); if (req != null) { job = req.findAncestorObject(Job.class); } List sites = job == null ? JiraGlobalConfiguration.get().getSites() : JiraSite.getJiraSites(job); for (JiraSite site : sites) { JiraSession session = site.getSession(job); if (session == null) { continue; } com.atlassian.jira.rest.client.api.domain.User user = session.service.getUser(username); if (user != null) { String email = user.getEmailAddress(); if (email != null) { email = unmaskEmail(email); return email; } } } return null; } private static final String PRE = "[( \\[<_{\"=]+"; private static final String POST = "[) \\]>_}\"=]+"; private static final Pattern AT = Pattern.compile(PRE + "[aA][tT]" + POST); private static final Pattern DOT = Pattern.compile(PRE + "[dD][oO0][tT]" + POST); // unmask emails like "john dot doe at example dot com" to john.doe@example.com static String unmaskEmail(String email) { email = AT.matcher(email).replaceAll("@"); email = DOT.matcher(email).replaceAll("."); return email; } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraProjectProperty.java ================================================ package hudson.plugins.jira; import com.cloudbees.hudson.plugins.folder.AbstractFolder; import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; import hudson.Util; import hudson.init.InitMilestone; import hudson.init.Initializer; import hudson.model.Job; import hudson.model.JobProperty; import hudson.model.JobPropertyDescriptor; import hudson.util.ListBoxModel; import java.util.List; import java.util.stream.Stream; import jenkins.model.Jenkins; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; /** * Associates {@link Job} with {@link JiraSite}. * * @author Kohsuke Kawaguchi */ public class JiraProjectProperty extends JobProperty> { /** * Used to find {@link JiraSite}. Matches {@link JiraSite#getName()}. Always * non-null (but beware that this value might become stale if the system * config is changed.) */ public final String siteName; @DataBoundConstructor public JiraProjectProperty(String siteName) { siteName = Util.fixEmptyAndTrim(siteName); if (siteName == null) { // defaults to the first one List sites = JiraGlobalConfiguration.get().getSites(); if (!sites.isEmpty()) { siteName = sites.get(0).getName(); } } this.siteName = siteName; } /** * Gets the {@link JiraSite} that this project belongs to. * * @return null if the configuration becomes out of sync. */ @Nullable public JiraSite getSite() { List sites = JiraGlobalConfiguration.get().getSites(); if (siteName == null && sites.size() > 0) { // default return sites.get(0); } Stream streams = sites.stream(); if (owner != null) { Stream stream2 = JiraFolderProperty.getSitesFromFolders(owner.getParent()).stream(); streams = Stream.concat(streams, stream2).parallel(); } return streams.filter(jiraSite -> jiraSite.getName().equals(siteName)) .findFirst() .orElse(null); } @Extension public static final class DescriptorImpl extends JobPropertyDescriptor { @Deprecated protected transient List sites; @Override @SuppressWarnings("unchecked") public boolean isApplicable(Class jobType) { return Job.class.isAssignableFrom(jobType); } @Override public String getDisplayName() { return Messages.JiraProjectProperty_DisplayName(); } /** * @param site the Jira site * * @deprecated use {@link JiraGlobalConfiguration#setSites(List)} instead */ @Deprecated public void setSites(JiraSite site) { JiraGlobalConfiguration.get().getSites().add(site); } /** * @return array of sites * * @deprecated use {@link JiraGlobalConfiguration#getSites()} instead */ @Deprecated public JiraSite[] getSites() { return JiraGlobalConfiguration.get().getSites().toArray(new JiraSite[0]); } @SuppressWarnings("unused") // Used by stapler public ListBoxModel doFillSiteNameItems(@AncestorInPath AbstractFolder folder) { ListBoxModel items = new ListBoxModel(); for (JiraSite site : JiraGlobalConfiguration.get().getSites()) { items.add(site.getName()); } if (folder != null) { List sitesFromFolder = JiraFolderProperty.getSitesFromFolders(folder); sitesFromFolder.stream().map(JiraSite::getName).forEach(items::add); } return items; } @SuppressWarnings("unused") // Used to start migration after all extensions are loaded @Initializer(after = InitMilestone.EXTENSIONS_AUGMENTED) public void migrate() { DescriptorImpl descriptor = (DescriptorImpl) Jenkins.getInstance().getDescriptor(JiraProjectProperty.class); if (descriptor != null) { descriptor.load(); // force readResolve without registering descriptor as configurable } } @SuppressWarnings("deprecation") // Migrate configuration protected Object readResolve() { if (sites != null) { JiraGlobalConfiguration jiraGlobalConfiguration = (JiraGlobalConfiguration) Jenkins.getInstance().getDescriptorOrDie(JiraGlobalConfiguration.class); jiraGlobalConfiguration.load(); jiraGlobalConfiguration.getSites().addAll(sites); jiraGlobalConfiguration.save(); sites = null; DescriptorImpl oldDescriptor = (DescriptorImpl) Jenkins.getInstance().getDescriptor(JiraProjectProperty.class); if (oldDescriptor != null) { oldDescriptor.save(); } } return this; } } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraReleaseVersionUpdater.java ================================================ package hudson.plugins.jira; import hudson.Extension; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Notifier; import hudson.tasks.Publisher; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest2; /** * Task which releases the jira version specified in the parameters when the build completes. * * @author Justen Walker justen.walker@gmail.com * @deprecated Replaced by {@link JiraReleaseVersionUpdaterBuilder} which can be used as a PostBuild step with conditional triggering.
* Kept for backward compatibility. */ @Deprecated public class JiraReleaseVersionUpdater extends Notifier { private static final long serialVersionUID = 699563338312232811L; private String jiraProjectKey; private String jiraRelease; private String jiraDescription; @Deprecated public JiraReleaseVersionUpdater(String jiraProjectKey, String jiraRelease) { this.jiraRelease = jiraRelease; this.jiraProjectKey = jiraProjectKey; } @DataBoundConstructor public JiraReleaseVersionUpdater(String jiraProjectKey, String jiraRelease, String jiraDescription) { this.jiraRelease = jiraRelease; this.jiraProjectKey = jiraProjectKey; this.jiraDescription = jiraDescription; } public String getJiraRelease() { return jiraRelease; } public void setJiraRelease(String jiraRelease) { this.jiraRelease = jiraRelease; } public String getJiraProjectKey() { return jiraProjectKey; } public void setJiraProjectKey(String jiraProjectKey) { this.jiraProjectKey = jiraProjectKey; } public String getJiraDescription() { return jiraDescription; } public void setJiraDescription(String jiraDescription) { this.jiraDescription = jiraDescription; } @Override public BuildStepDescriptor getDescriptor() { return DESCRIPTOR; } @Extension public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) { return new VersionReleaser() .perform(build.getProject(), jiraProjectKey, jiraRelease, jiraDescription, build, listener); } @Override public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } public static class DescriptorImpl extends BuildStepDescriptor { public DescriptorImpl() { super(JiraReleaseVersionUpdater.class); } @Override public JiraReleaseVersionUpdater newInstance(StaplerRequest2 req, JSONObject formData) throws FormException { return req.bindJSON(JiraReleaseVersionUpdater.class, formData); } @Override public boolean isApplicable(Class jobType) { return true; } @Override public String getDisplayName() { return Messages.JiraReleaseVersionBuilder_DisplayName(); } @Override public String getHelpFile() { return "/plugin/jira/help-release.html"; } } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraReleaseVersionUpdaterBuilder.java ================================================ package hudson.plugins.jira; import hudson.EnvVars; import hudson.Extension; import hudson.model.AbstractProject; import hudson.model.Descriptor; import hudson.model.Run; import hudson.model.TaskListener; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.Builder; import jenkins.tasks.SimpleBuildStep; import net.sf.json.JSONObject; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest2; /** * Created by Reda on 18/12/2014. */ public class JiraReleaseVersionUpdaterBuilder extends Builder implements SimpleBuildStep { private String jiraProjectKey; private String jiraRelease; private String jiraDescription; @Extension public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); @Deprecated public JiraReleaseVersionUpdaterBuilder(String jiraProjectKey, String jiraRelease) { this.jiraRelease = jiraRelease; this.jiraProjectKey = jiraProjectKey; } @DataBoundConstructor public JiraReleaseVersionUpdaterBuilder(String jiraProjectKey, String jiraRelease, String jiraDescription) { this.jiraRelease = jiraRelease; this.jiraProjectKey = jiraProjectKey; this.jiraDescription = jiraDescription; } public String getJiraRelease() { return jiraRelease; } public void setJiraRelease(String jiraRelease) { this.jiraRelease = jiraRelease; } public String getJiraProjectKey() { return jiraProjectKey; } public void setJiraProjectKey(String jiraProjectKey) { this.jiraProjectKey = jiraProjectKey; } public String getJiraDescription() { return jiraDescription; } public void setJiraDescription(String jiraDescription) { this.jiraDescription = jiraDescription; } @Override public void perform(Run run, EnvVars env, TaskListener listener) { new VersionReleaser().perform(run.getParent(), jiraProjectKey, jiraRelease, jiraDescription, run, listener); } @Override public boolean requiresWorkspace() { return false; } @Override public Descriptor getDescriptor() { return DESCRIPTOR; } @Symbol("jiraMarkVersionReleased") public static final class DescriptorImpl extends BuildStepDescriptor { private DescriptorImpl() { super(JiraReleaseVersionUpdaterBuilder.class); } @Override public boolean isApplicable(Class jobType) { return true; } @Override public String getDisplayName() { // Placed in the build settings section return Messages.JiraReleaseVersionBuilder_DisplayName(); } @Override public String getHelpFile() { return "/plugin/jira/help.html"; } @Override public JiraReleaseVersionUpdaterBuilder newInstance(StaplerRequest2 req, JSONObject formData) throws FormException { return req.bindJSON(JiraReleaseVersionUpdaterBuilder.class, formData); } } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraRestService.java ================================================ /* * Copyright 2015 Hao Cheng Lee * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package hudson.plugins.jira; import static java.util.logging.Level.FINE; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import com.atlassian.jira.rest.client.api.RestClientException; import com.atlassian.jira.rest.client.api.domain.BasicIssue; import com.atlassian.jira.rest.client.api.domain.BasicProject; import com.atlassian.jira.rest.client.api.domain.BasicUser; import com.atlassian.jira.rest.client.api.domain.Comment; import com.atlassian.jira.rest.client.api.domain.Component; import com.atlassian.jira.rest.client.api.domain.Issue; import com.atlassian.jira.rest.client.api.domain.IssueFieldId; import com.atlassian.jira.rest.client.api.domain.IssueType; import com.atlassian.jira.rest.client.api.domain.Permissions; import com.atlassian.jira.rest.client.api.domain.Priority; import com.atlassian.jira.rest.client.api.domain.SearchResult; import com.atlassian.jira.rest.client.api.domain.Status; import com.atlassian.jira.rest.client.api.domain.Transition; import com.atlassian.jira.rest.client.api.domain.User; import com.atlassian.jira.rest.client.api.domain.Version; import com.atlassian.jira.rest.client.api.domain.input.ComplexIssueInputFieldValue; import com.atlassian.jira.rest.client.api.domain.input.FieldInput; import com.atlassian.jira.rest.client.api.domain.input.IssueInput; import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder; import com.atlassian.jira.rest.client.api.domain.input.TransitionInput; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import hudson.ProxyConfiguration; import hudson.plugins.jira.extension.ExtendedJiraRestClient; import hudson.plugins.jira.extension.ExtendedVersion; import hudson.plugins.jira.extension.ExtendedVersionInput; import hudson.plugins.jira.model.JiraIssueField; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import jenkins.model.Jenkins; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpHost; import org.apache.http.client.fluent.Content; import org.apache.http.client.fluent.Request; import org.apache.http.client.utils.URIBuilder; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; public class JiraRestService { private static final Logger LOGGER = Logger.getLogger(JiraRestService.class.getName()); public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd"); /** * Base URI path for a REST API call. It must be relative to site's base * URI. */ public static final String BASE_API_PATH = "rest/api/2"; static final long BUG_ISSUE_TYPE_ID = 1L; private final URI uri; private final ExtendedJiraRestClient jiraRestClient; private final ObjectMapper objectMapper; private final String authHeader; private final String baseApiPath; private final int timeout; @Deprecated public JiraRestService(URI uri, ExtendedJiraRestClient jiraRestClient, String username, String password) { this(uri, jiraRestClient, username, password, JiraSite.DEFAULT_TIMEOUT); } public JiraRestService( URI uri, ExtendedJiraRestClient jiraRestClient, String username, String password, int timeout) { this.uri = uri; this.objectMapper = new ObjectMapper(); this.timeout = timeout; final String login = username + ":" + password; try { byte[] encodeBase64 = Base64.encodeBase64(login.getBytes("UTF-8")); this.authHeader = "Basic " + new String(encodeBase64, "UTF-8"); } catch (UnsupportedEncodingException e) { LOGGER.warning("Jira REST encode username:password error. cause: " + e.getMessage()); throw new RuntimeException("failed to encode username:password using Base64"); } this.jiraRestClient = jiraRestClient; baseApiPath = buildBaseApiPath(uri); } public JiraRestService(URI uri, ExtendedJiraRestClient jiraRestClient, String token, int timeout) { this.uri = uri; this.objectMapper = new ObjectMapper(); this.timeout = timeout; this.authHeader = "Bearer " + token; this.jiraRestClient = jiraRestClient; baseApiPath = buildBaseApiPath(uri); } private String buildBaseApiPath(URI uri) { final StringBuilder builder = new StringBuilder(); if (uri.getPath() != null) { builder.append(uri.getPath()); if (!uri.getPath().endsWith("/")) { builder.append('/'); } } else { builder.append('/'); } builder.append(BASE_API_PATH); return builder.toString(); } public void addComment(String issueId, String commentBody, String groupVisibility, String roleVisibility) { final URIBuilder builder = new URIBuilder(uri).setPath(String.format("%s/issue/%s/comment", baseApiPath, issueId)); final Comment comment; if (StringUtils.isNotBlank(groupVisibility)) { comment = Comment.createWithGroupLevel(commentBody, groupVisibility); } else if (StringUtils.isNotBlank(roleVisibility)) { comment = Comment.createWithRoleLevel(commentBody, roleVisibility); } else { comment = Comment.valueOf(commentBody); } try { jiraRestClient.getIssueClient().addComment(builder.build(), comment).get(timeout, TimeUnit.SECONDS); } catch (RestClientException | URISyntaxException | InterruptedException | ExecutionException | TimeoutException e) { LOGGER.log(WARNING, "Jira REST client add comment error. cause: " + e.getMessage(), e); throw new RestClientException( "[Jira] Jira REST client add comment error. cause: " + e.getMessage(), e.getCause()); } } public Issue getIssue(String issueKey) { LOGGER.log(FINE, "[Jira] Fetching issue {0}", issueKey); try { return jiraRestClient.getIssueClient().getIssue(issueKey).get(timeout, TimeUnit.SECONDS); } catch (RestClientException | InterruptedException | ExecutionException | TimeoutException e) { if (e.getCause() != null && e.getCause() instanceof RestClientException && ((RestClientException) e.getCause()).getStatusCode().isPresent() && ((RestClientException) e.getCause()).getStatusCode().get() == 404) { LOGGER.log(INFO, "Issue '" + issueKey + "' not found in Jira."); throw new RestClientException("[Jira] Issue '" + issueKey + "' not found in Jira.", e.getCause()); } else { LOGGER.log(WARNING, "Jira REST client get issue error. cause: " + e.getMessage(), e); throw new RestClientException( "[Jira] Jira REST client get issue error. cause: " + e.getMessage(), e.getCause()); } } } public List getIssueTypes() { try { return StreamSupport.stream( jiraRestClient .getMetadataClient() .getIssueTypes() .get(timeout, TimeUnit.SECONDS) .spliterator(), false) .collect(Collectors.toList()); } catch (RestClientException | InterruptedException | ExecutionException | TimeoutException e) { LOGGER.log(WARNING, "Jira REST client get issue types error. cause: " + e.getMessage(), e); throw new RestClientException( "[Jira] Jira REST client get issue types error. cause: " + e.getMessage(), e.getCause()); } } public List getPriorities() { try { return StreamSupport.stream( jiraRestClient .getMetadataClient() .getPriorities() .get(timeout, TimeUnit.SECONDS) .spliterator(), false) .collect(Collectors.toList()); } catch (RestClientException | InterruptedException | ExecutionException | TimeoutException e) { LOGGER.log(WARNING, "Jira REST client get priorities error. cause: " + e.getMessage(), e); throw new RestClientException( "[Jira] Jira REST client get priorities error. cause: " + e.getMessage(), e.getCause()); } } public List getProjectsKeys() { Iterable projects = Collections.emptyList(); try { projects = jiraRestClient.getProjectClient().getAllProjects().get(timeout, TimeUnit.SECONDS); } catch (RestClientException | InterruptedException | ExecutionException | TimeoutException e) { LOGGER.log(WARNING, "Jira REST client get project keys error. cause: " + e.getMessage(), e); throw new RestClientException( "[Jira] Jira REST client get project keys error. cause: " + e.getMessage(), e.getCause()); } final List keys = new ArrayList<>(); for (BasicProject project : projects) { keys.add(project.getKey()); } return keys; } public List getIssuesFromJqlSearch(String jqlSearch, Integer maxResults) throws RestClientException { LOGGER.log(FINE, "[Jira] Executing JQL: {0}", jqlSearch); try { Set neededFields = new HashSet<>( Arrays.asList("summary", "issuetype", "created", "updated", "project", "status", "fixVersions")); final SearchResult searchResult = jiraRestClient .getSearchClient() .searchJql(jqlSearch, maxResults, 0, neededFields) .get(timeout, TimeUnit.SECONDS); return StreamSupport.stream(searchResult.getIssues().spliterator(), false) .collect(Collectors.toList()); } catch (RestClientException | TimeoutException | CancellationException | ExecutionException | InterruptedException e) { LOGGER.log(WARNING, "Jira REST client get issue from jql search error. cause: " + e.getMessage(), e); throw new RestClientException( "[Jira] Jira REST client get issue from jql search error. cause: " + e.getMessage(), e.getCause()); } } public List getVersions(String projectKey) { final URIBuilder builder = new URIBuilder(uri).setPath(String.format("%s/project/%s/versions", baseApiPath, projectKey)); List> decoded = Collections.emptyList(); try { URI uri = builder.build(); final Content content = buildGetRequest(uri).execute().returnContent(); decoded = objectMapper.readValue(content.asString(), new TypeReference>>() {}); } catch (URISyntaxException | IOException e) { LOGGER.log(WARNING, "Jira REST client get versions error. cause: " + e.getMessage(), e); throw new RestClientException( "[Jira] Jira REST client get versions error. cause: " + e.getMessage(), e.getCause()); } return decoded.stream() .map(decodedVersion -> { final DateTime startDate = decodedVersion.containsKey("startDate") ? DATE_TIME_FORMATTER.parseDateTime((String) decodedVersion.get("startDate")) : null; final DateTime releaseDate = decodedVersion.containsKey("releaseDate") ? DATE_TIME_FORMATTER.parseDateTime((String) decodedVersion.get("releaseDate")) : null; return new ExtendedVersion( URI.create((String) decodedVersion.get("self")), Long.parseLong((String) decodedVersion.get("id")), (String) decodedVersion.get("name"), (String) decodedVersion.get("description"), (Boolean) decodedVersion.get("archived"), (Boolean) decodedVersion.get("released"), startDate, releaseDate); }) .collect(Collectors.toList()); } public Version addVersion(String projectKey, String versionName) { final ExtendedVersionInput versionInput = new ExtendedVersionInput(projectKey, versionName, null, DateTime.now(), null, false, false); try { return jiraRestClient .getExtendedVersionRestClient() .createExtendedVersion(versionInput) .get(timeout, TimeUnit.SECONDS); } catch (RestClientException | InterruptedException | ExecutionException | TimeoutException e) { LOGGER.log(WARNING, "Jira REST client add version error. cause: " + e.getMessage(), e); throw new RestClientException( "[Jira] Jira REST client add version error. cause: " + e.getMessage(), e.getCause()); } } public void releaseVersion(String projectKey, ExtendedVersion version) { final URIBuilder builder = new URIBuilder(uri).setPath(String.format("%s/version/%s", baseApiPath, version.getId())); final ExtendedVersionInput versionInput = new ExtendedVersionInput( projectKey, version.getName(), version.getDescription(), version.getStartDate(), version.getReleaseDate(), version.isArchived(), version.isReleased()); try { jiraRestClient .getExtendedVersionRestClient() .updateExtendedVersion(builder.build(), versionInput) .get(timeout, TimeUnit.SECONDS); } catch (RestClientException | URISyntaxException | InterruptedException | ExecutionException | TimeoutException e) { LOGGER.log(WARNING, "Jira REST client release version error. cause: " + e.getMessage(), e); throw new RestClientException( "[Jira] Jira REST client release version error. cause: " + e.getMessage(), e.getCause()); } } @Deprecated public BasicIssue createIssue( String projectKey, String description, String assignee, Iterable components, String summary) { return createIssue(projectKey, description, assignee, components, summary, BUG_ISSUE_TYPE_ID, null); } public BasicIssue createIssue( String projectKey, String description, String assignee, Iterable components, String summary, @NonNull Long issueTypeId, @Nullable Long priorityId) { IssueInputBuilder builder = new IssueInputBuilder(); builder.setProjectKey(projectKey) .setDescription(description) .setIssueTypeId(issueTypeId) .setSummary(summary); if (priorityId != null) { builder.setPriorityId(priorityId); } if (StringUtils.isNotBlank(assignee)) { final Map valuesMap = new HashMap<>(2); valuesMap.put("name", assignee); // server valuesMap.put("accountId", assignee); // cloud // Need to use "accountId" as specified here: // // https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-user-privacy-api-migration-guide/ // // See upstream fix for setAssigneeName: // // https://bitbucket.org/atlassian/jira-rest-java-client/pull-requests/104/change-field-name-from-name-to-id-for/diff builder.setFieldInput( new FieldInput(IssueFieldId.ASSIGNEE_FIELD, new ComplexIssueInputFieldValue(valuesMap))); } if (StreamSupport.stream(components.spliterator(), false).count() > 0) { builder.setComponentsNames(components); } final IssueInput issueInput = builder.build(); try { return jiraRestClient.getIssueClient().createIssue(issueInput).get(timeout, TimeUnit.SECONDS); } catch (RestClientException | InterruptedException | ExecutionException | TimeoutException e) { LOGGER.log(WARNING, "Jira REST createIssue error: " + e.getMessage(), e); throw new RestClientException("[Jira] Jira REST createIssue error. cause: " + e.getMessage(), e.getCause()); } } public User getUser(String username) { try { return jiraRestClient.getUserClient().getUser(username).get(timeout, TimeUnit.SECONDS); } catch (RestClientException | InterruptedException | ExecutionException | TimeoutException e) { if (e.getCause() != null && e.getCause() instanceof RestClientException && ((RestClientException) e.getCause()).getStatusCode().isPresent() && ((RestClientException) e.getCause()).getStatusCode().get() == 404) { LOGGER.log(INFO, "User '" + username + "' not found in Jira."); throw new RestClientException("[Jira] User '" + username + "' not found in Jira.", e.getCause()); } else { LOGGER.log(WARNING, "Jira REST client get user error. cause: " + e.getMessage(), e); throw new RestClientException( "[Jira] Jira REST client get user error. cause: " + e.getMessage(), e.getCause()); } } } public void updateIssue(String issueKey, List fixVersions) { final IssueInput issueInput = new IssueInputBuilder().setFixVersions(fixVersions).build(); try { jiraRestClient.getIssueClient().updateIssue(issueKey, issueInput).get(timeout, TimeUnit.SECONDS); } catch (RestClientException | InterruptedException | ExecutionException | TimeoutException e) { LOGGER.log(WARNING, "Jira REST client update issue error. cause: " + e.getMessage(), e); throw new RestClientException( "[Jira] Jira REST client update issue error. cause: " + e.getMessage(), e.getCause()); } } public void setIssueLabels(String issueKey, List labels) { final IssueInput issueInput = new IssueInputBuilder() .setFieldValue(IssueFieldId.LABELS_FIELD.id, labels) .build(); try { jiraRestClient.getIssueClient().updateIssue(issueKey, issueInput).get(timeout, TimeUnit.SECONDS); } catch (RestClientException | InterruptedException | ExecutionException | TimeoutException e) { LOGGER.log(WARNING, "Jira REST client update labels error for issue " + issueKey, e); throw new RestClientException( "[Jira] Jira REST client update labels error for issue: " + issueKey + ". cause: " + e.getMessage(), e.getCause()); } } public void setIssueFields(String issueKey, List fields) { IssueInputBuilder builder = new IssueInputBuilder(); for (JiraIssueField field : fields) { builder.setFieldValue(field.getId(), field.getValue()); } final IssueInput issueInput = builder.build(); try { jiraRestClient.getIssueClient().updateIssue(issueKey, issueInput).get(timeout, TimeUnit.SECONDS); } catch (RestClientException | InterruptedException | ExecutionException | TimeoutException e) { LOGGER.log(WARNING, "Jira REST client update fields error for issue " + issueKey, e); throw new RestClientException( "[Jira] Jira REST client update fields error for issue: " + issueKey + ". cause: " + e.getMessage(), e.getCause()); } } public Issue progressWorkflowAction(String issueKey, Integer actionId) { final TransitionInput transitionInput = new TransitionInput(actionId); final Issue issue = getIssue(issueKey); try { jiraRestClient.getIssueClient().transition(issue, transitionInput).get(timeout, TimeUnit.SECONDS); } catch (RestClientException | InterruptedException | ExecutionException | TimeoutException e) { LOGGER.log(WARNING, "Jira REST client process workflow action error. cause: " + e.getMessage(), e); throw new RestClientException( "[Jira] Jira REST client process workflow action error. cause: " + e.getMessage(), e.getCause()); } return issue; } public List getAvailableActions(String issueKey) { final Issue issue = getIssue(issueKey); try { final Iterable transitions = jiraRestClient.getIssueClient().getTransitions(issue).get(timeout, TimeUnit.SECONDS); return StreamSupport.stream(transitions.spliterator(), false).collect(Collectors.toList()); } catch (RestClientException | InterruptedException | ExecutionException | TimeoutException e) { LOGGER.log(WARNING, "Jira REST client get available actions error. cause: " + e.getMessage(), e); throw new RestClientException( "[Jira] Jira REST client get available actions error. cause: " + e.getMessage(), e.getCause()); } } public List getStatuses() { try { final Iterable statuses = jiraRestClient.getMetadataClient().getStatuses().get(timeout, TimeUnit.SECONDS); return StreamSupport.stream(statuses.spliterator(), false).collect(Collectors.toList()); } catch (RestClientException | InterruptedException | ExecutionException | TimeoutException e) { LOGGER.log(WARNING, "Jira REST client get statuses error. cause: " + e.getMessage(), e); throw new RestClientException( "[Jira] Jira REST client get statuses error. cause: " + e.getMessage(), e.getCause()); } } public List getComponents(String projectKey) { final URIBuilder builder = new URIBuilder(uri).setPath(String.format("%s/project/%s/components", baseApiPath, projectKey)); try { final Content content = buildGetRequest(builder.build()).execute().returnContent(); final List> decoded = objectMapper.readValue(content.asString(), new TypeReference>>() {}); final List components = new ArrayList<>(); for (final Map decodeComponent : decoded) { BasicUser lead = null; if (decodeComponent.containsKey("lead")) { final Map decodedLead = (Map) decodeComponent.get("lead"); lead = new BasicUser( URI.create((String) decodedLead.get("self")), (String) decodedLead.get("name"), (String) decodedLead.get("displayName"), (String) decodedLead.get("accountId")); } final Component component = new Component( URI.create((String) decodeComponent.get("self")), Long.parseLong((String) decodeComponent.get("id")), (String) decodeComponent.get("name"), (String) decodeComponent.get("description"), lead); components.add(component); } return components; } catch (URISyntaxException | IOException e) { LOGGER.log(WARNING, "Jira REST client process workflow action error. cause: " + e.getMessage(), e); throw new RestClientException( "[Jira] Jira REST client process workflow action error. cause: " + e.getMessage(), e.getCause()); } } private Request buildGetRequest(URI uri) { Request request = Request.Get(uri); ProxyConfiguration proxyConfiguration = Jenkins.get().proxy; if (proxyConfiguration != null) { final HttpHost proxyHost = new HttpHost(proxyConfiguration.name, proxyConfiguration.port); boolean shouldByPassProxy = proxyConfiguration.getNoProxyHostPatterns().stream() .anyMatch(it -> it.matcher(uri.getHost()).matches()); if (!shouldByPassProxy) { request.viaProxy(proxyHost); } } return request.connectTimeout(timeoutInMilliseconds()) .socketTimeout(timeoutInMilliseconds()) .addHeader("Authorization", authHeader) .addHeader("Content-Type", "application/json"); } protected int timeoutInMilliseconds() { return (int) TimeUnit.SECONDS.toMillis(timeout); } public String getBaseApiPath() { return baseApiPath; } /** * Get User's permissions */ public Permissions getMyPermissions() { return jiraRestClient .getExtendedMyPermissionsRestClient() .getMyPermissions() .claim(); } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraSession.java ================================================ package hudson.plugins.jira; import static org.apache.commons.lang3.StringUtils.isNotEmpty; import com.atlassian.jira.rest.client.api.RestClientException; import com.atlassian.jira.rest.client.api.domain.BasicIssue; import com.atlassian.jira.rest.client.api.domain.Component; import com.atlassian.jira.rest.client.api.domain.Issue; import com.atlassian.jira.rest.client.api.domain.IssueType; import com.atlassian.jira.rest.client.api.domain.Permissions; import com.atlassian.jira.rest.client.api.domain.Priority; import com.atlassian.jira.rest.client.api.domain.Status; import com.atlassian.jira.rest.client.api.domain.Transition; import com.atlassian.jira.rest.client.api.domain.Version; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import hudson.plugins.jira.extension.ExtendedVersion; import hudson.plugins.jira.model.JiraIssueField; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeoutException; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; /** * Connection to Jira. * Jira has a built-in timeout for a session, so after some inactive period the * session will become invalid. The caller must make sure that this doesn't * happen. * * @author Kohsuke Kawaguchi */ public class JiraSession { private static final Logger LOGGER = Logger.getLogger(JiraSession.class.getName()); public final JiraRestService service; /** * Lazily computed list of project keys. */ private Set projectKeys; private final String jiraSiteName; private final Integer maxIssuesFromJqlSearch; /* package */ JiraSession(JiraSite site, JiraRestService jiraRestService) { this.service = jiraRestService; this.jiraSiteName = site.getName(); this.maxIssuesFromJqlSearch = site.getMaxIssuesFromJqlSearch(); } /** * Returns the set of project keys (like MNG, JENKINS, etc) that are * available in this Jira. * Guarantees to return all project keys in upper case. */ public Set getProjectKeys() { if (projectKeys == null) { LOGGER.fine("Fetching remote project key list from " + jiraSiteName); final List keys = service.getProjectsKeys(); projectKeys = new HashSet<>(keys); LOGGER.fine("Project list=" + projectKeys); } return projectKeys; } /** * Adds a comment to the existing issue. Constrains the visibility of the * comment the the supplied groupVisibility. */ public void addComment(String issueId, String comment, String groupVisibility, String roleVisibility) { service.addComment(issueId, comment, groupVisibility, roleVisibility); } /** * Adds new labels to the existing issue. * Old labels remains untouched. * * @param issueId Jira issue ID like "MNG-1235". * @param labels New labels to add. */ public void addLabels(String issueId, List labels) { List newLabels = new ArrayList(); Issue existingIssue = service.getIssue(issueId); if (existingIssue.getLabels() != null) { newLabels.addAll(existingIssue.getLabels()); } boolean changed = false; for (String label : labels) { if (!newLabels.contains(label)) { newLabels.add(label); changed = true; } } if (changed) { service.setIssueLabels(issueId, newLabels); } } /** * Adds new to or updates existing fields of the issue. * Can add or update custom fields. * * @param issueId Jira issue ID like "PRJ-123" * @param fields Fields to add or update */ public void addFields(String issueId, List fields) { service.setIssueFields(issueId, fields); } /** * Gets the details of one issue. * * @param id Issue ID like "MNG-1235". * @return null if no such issue exists. */ public Issue getIssue(String id) { return service.getIssue(id); } /** * Gets all issues that match the given JQL filter * * @param jqlSearch JQL query string to execute * @return issues matching the JQL query */ public List getIssuesFromJqlSearch(final String jqlSearch) throws RestClientException { return service.getIssuesFromJqlSearch(jqlSearch, maxIssuesFromJqlSearch); } /** * Get all versions from the given project * * @param projectKey The key for the project * @return An array of versions */ public List getVersions(String projectKey) { LOGGER.fine("Fetching versions from project: " + projectKey); return service.getVersions(projectKey); } /** * Get a version by its name * * @param projectKey The key for the project * @param name The version name * @return A RemoteVersion, or null if not found */ public ExtendedVersion getVersionByName(String projectKey, String name) { LOGGER.fine("Fetching versions from project: " + projectKey); List versions = getVersions(projectKey); if (versions == null) { return null; } for (ExtendedVersion version : versions) { if (version.getName().equals(name)) { return version; } } return null; } public List getIssuesWithFixVersion(String projectKey, String version) throws TimeoutException { return getIssuesWithFixVersion(projectKey, version, ""); } public List getIssuesWithFixVersion(String projectKey, String version, String filter) throws TimeoutException { LOGGER.fine("Fetching versions from project: " + projectKey + " with fixVersion:" + version); if (isNotEmpty(filter)) { return service.getIssuesFromJqlSearch( String.format("project = \"%s\" and fixVersion = \"%s\" and " + filter, projectKey, version), maxIssuesFromJqlSearch); } return service.getIssuesFromJqlSearch( String.format("project = \"%s\" and fixVersion = \"%s\"", projectKey, version), maxIssuesFromJqlSearch); } /** * Get all issue types * * @return An array of issue types */ public List getIssueTypes() { LOGGER.fine("Fetching issue types"); return service.getIssueTypes(); } /** * Get all priorities * * @return An array of priorities */ public List getPriorities() { LOGGER.fine("Fetching priorities"); return service.getPriorities(); } /** * Release given version in given project */ public void releaseVersion(String projectKey, ExtendedVersion version) { LOGGER.fine("Releasing version: " + version.getName()); service.releaseVersion(projectKey, version); } /** * Replaces the fix version list of all issues matching the JQL Query with the version specified. * * @param projectKey The Jira Project key * @param version The replacement version * @param query The JQL Query */ public void migrateIssuesToFixVersion(String projectKey, String version, String query) throws TimeoutException { Version newVersion = getVersionByName(projectKey, version); if (newVersion == null) { LOGGER.warning("Version " + version + " was not found"); return; } LOGGER.fine("Fetching versions with JQL:" + query); List issues = service.getIssuesFromJqlSearch(query, maxIssuesFromJqlSearch); if (issues == null || issues.isEmpty()) { return; } LOGGER.fine("Found issues: " + issues.size()); issues.stream().forEach(issue -> { LOGGER.fine("Migrating issue: " + issue.getKey()); service.updateIssue(issue.getKey(), Collections.singletonList(newVersion)); }); } /** * Replaces the given fromVersion with toVersion in all issues matching the JQL query. * * @param projectKey The Jira Project * @param fromVersion The name of the version to replace * @param toVersion The name of the replacement version * @param query The JQL Query */ public void replaceFixVersion(String projectKey, String fromVersion, String toVersion, String query) throws TimeoutException, RestClientException { Version newVersion = getVersionByName(projectKey, toVersion); if (newVersion == null) { LOGGER.warning("Version " + toVersion + " was not found"); return; } LOGGER.fine("Fetching versions with JQL:" + query); List issues = service.getIssuesFromJqlSearch(query, maxIssuesFromJqlSearch); if (issues == null) { return; } LOGGER.fine("Found issues: " + issues.size()); for (Issue issue : issues) { Set newVersions = new HashSet<>(); newVersions.add(newVersion); Iterable issueVersions = Optional.ofNullable(issue.getFixVersions()).orElse(Collections.emptyList()); LOGGER.fine(String.format("[%s] current versions: %s", issue.getKey(), issueVersions)); if (StringUtils.startsWith(fromVersion, "/") && StringUtils.endsWith(fromVersion, "/")) { String regEx = StringUtils.removeStart(fromVersion, "/"); regEx = StringUtils.removeEnd(regEx, "/"); Pattern fromVersionPattern = Pattern.compile(regEx); LOGGER.fine("Using regular expression: " + regEx); for (Version currentVersion : issueVersions) { Matcher versionToRemove = fromVersionPattern.matcher(currentVersion.getName()); if (!versionToRemove.matches()) { newVersions.add(currentVersion); } } } else { for (Version currentVersion : issueVersions) { if (!currentVersion.getName().equals(fromVersion)) { newVersions.add(currentVersion); } } } LOGGER.info(String.format("Moving issues matching JQL query: \"%s\" to version %s", query, toVersion)); service.updateIssue(issue.getKey(), new ArrayList(newVersions)); } } /** * Adds the specified version to the fix version list of all issues matching the JQL. * * @param projectKey The Jira Project * @param version The version to add * @param query The JQL Query */ public void addFixVersion(String projectKey, String version, String query) throws TimeoutException, RestClientException { Version newVersion = getVersionByName(projectKey, version); if (newVersion == null) { LOGGER.warning("Version " + version + " was not found"); return; } LOGGER.fine("Fetching issues with JQL:" + query); List issues = service.getIssuesFromJqlSearch(query, maxIssuesFromJqlSearch); if (issues == null || issues.isEmpty()) { return; } LOGGER.fine("Found issues: " + issues.size()); for (Issue issue : issues) { LOGGER.fine("Adding version: " + newVersion.getName() + " to issue: " + issue.getKey()); List fixVersions = new ArrayList<>(); Optional.ofNullable(issue.getFixVersions()) .orElse(Collections.emptyList()) .forEach(fixVersions::add); fixVersions.add(newVersion); service.updateIssue(issue.getKey(), fixVersions); } } /** * Progresses the issue's workflow by performing the specified action. The issue's new status is returned. * * @return The new status */ public String progressWorkflowAction(String issueKey, Integer actionId) { LOGGER.fine("Progressing issue " + issueKey + " with workflow action: " + actionId); final Issue issue = service.progressWorkflowAction(issueKey, actionId); getStatusById(issue.getStatus().getId()); return getStatusById(issue.getStatus().getId()); } /** * Returns the matching action id for a given action name. * * @return The action id, or null if the action cannot be found. */ public Integer getActionIdForIssue(String issueKey, String workflowAction) { List actions = service.getAvailableActions(issueKey); if (actions != null) { for (Transition action : actions) { if (action.getName() != null && action.getName().equalsIgnoreCase(workflowAction)) { return action.getId(); } } } return null; } /** * Returns the status name by status id. * * @return status name */ public String getStatusById(Long statusId) { String status = getKnownStatuses().get(statusId); if (status == null) { LOGGER.warning("Jira status could not be found: " + statusId + ". Checking Jira for new status types."); knownStatuses = null; // Try again, just in case the admin has recently added a new status. This should be a rare condition. status = getKnownStatuses().get(statusId); } return status; } private Map knownStatuses = null; /** * Returns all known statuses. * * @return Map with statusId and status name */ private Map getKnownStatuses() { if (knownStatuses == null) { List statuses = service.getStatuses(); knownStatuses = new HashMap<>(statuses.size()); statuses.stream().forEach(status -> knownStatuses.put(status.getId(), status.getName())); } return knownStatuses; } /** * Returns issue-id of the created issue * * @return The issue id * * @deprecated use {@link #createIssue(String, String, String, Iterable, String, Long, Long)} */ @Deprecated public Issue createIssue( String projectKey, String description, String assignee, Iterable components, String summary) { return createIssue(projectKey, description, assignee, components, summary, null, null); } public Issue createIssue( String projectKey, String description, String assignee, Iterable components, String summary, @NonNull Long issueTypeId, @Nullable Long priorityId) { final BasicIssue basicIssue = service.createIssue(projectKey, description, assignee, components, summary, issueTypeId, priorityId); return service.getIssue(basicIssue.getKey()); } /** * Adds a comment to the existing issue.There is no constrains to the visibility of the comment. */ public void addCommentWithoutConstrains(String issueId, String comment) { service.addComment(issueId, comment, null, null); } /** * Returns information about the specific issue as identified by the issue id * * @return issue object */ public Issue getIssueByKey(String issueId) { return service.getIssue(issueId); } /** * Returns all the components for the particular project * * @return An array of components */ public List getComponents(String projectKey) { return service.getComponents(projectKey); } /** * Creates a new version and returns it * * @param version version id to create * @return created Version instance * */ public Version addVersion(String version, String projectKey) { return service.addVersion(projectKey, version); } /** * Get User's permissions */ public Permissions getMyPermissions() { return service.getMyPermissions(); } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraSessionFactory.java ================================================ package hudson.plugins.jira; import com.atlassian.jira.rest.client.auth.BasicHttpAuthenticationHandler; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import hudson.plugins.jira.JiraSite.ExtendedAsynchronousJiraRestClientFactory; import hudson.plugins.jira.auth.BearerHttpAuthenticationHandler; import hudson.plugins.jira.extension.ExtendedJiraRestClient; import java.net.URI; /** * Jira Session factory implementation * * @author Elia Bracci */ public class JiraSessionFactory { /** * This method takes as parameters the JiraSite class, the jira URI and * credentials and returns a JiraSession with Basic authentication if * useBearerAuth is set to false, otherwise it returns a JiraSession with Bearer * authentication if useBearerAuth is set to true. * * @param jiraSite jiraSite class * @param uri jira uri * @param credentials Jenkins credentials * @return JiraSession instance */ public static JiraSession create(JiraSite jiraSite, URI uri, StandardUsernamePasswordCredentials credentials) { ExtendedJiraRestClient jiraRestClient; JiraRestService jiraRestService; if (jiraSite.isUseBearerAuth()) { BearerHttpAuthenticationHandler bearerHttpAuthenticationHandler = new BearerHttpAuthenticationHandler( credentials.getPassword().getPlainText()); jiraRestClient = new ExtendedAsynchronousJiraRestClientFactory() .create(uri, bearerHttpAuthenticationHandler, jiraSite.getHttpClientOptions()); jiraRestService = new JiraRestService( uri, jiraRestClient, credentials.getPassword().getPlainText(), jiraSite.getReadTimeout()); } else { jiraRestClient = new ExtendedAsynchronousJiraRestClientFactory() .create( uri, new BasicHttpAuthenticationHandler( credentials.getUsername(), credentials.getPassword().getPlainText()), jiraSite.getHttpClientOptions()); jiraRestService = new JiraRestService( uri, jiraRestClient, credentials.getUsername(), credentials.getPassword().getPlainText(), jiraSite.getReadTimeout()); } return new JiraSession(jiraSite, jiraRestService); } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraSite.java ================================================ package hudson.plugins.jira; import static org.apache.commons.lang3.StringUtils.isEmpty; import static org.apache.commons.lang3.StringUtils.isNotEmpty; import com.atlassian.event.api.EventPublisher; import com.atlassian.httpclient.apache.httpcomponents.DefaultHttpClientFactory; import com.atlassian.httpclient.api.HttpClient; import com.atlassian.httpclient.api.factory.HttpClientOptions; import com.atlassian.jira.rest.client.api.AuthenticationHandler; import com.atlassian.jira.rest.client.api.JiraRestClientFactory; import com.atlassian.jira.rest.client.api.RestClientException; import com.atlassian.jira.rest.client.api.domain.Issue; import com.atlassian.jira.rest.client.auth.BasicHttpAuthenticationHandler; import com.atlassian.jira.rest.client.internal.async.AtlassianHttpClientDecorator; import com.atlassian.jira.rest.client.internal.async.DisposableHttpClient; import com.atlassian.sal.api.ApplicationProperties; import com.atlassian.sal.api.UrlMode; import com.atlassian.sal.api.executor.ThreadLocalContextManager; import com.cloudbees.hudson.plugins.folder.AbstractFolder; import com.cloudbees.hudson.plugins.folder.Folder; import com.cloudbees.plugins.credentials.CredentialsMatchers; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import com.cloudbees.plugins.credentials.domains.DomainRequirement; import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; import hudson.Util; import hudson.model.*; import hudson.model.Descriptor.FormException; import hudson.plugins.jira.extension.ExtendedAsynchronousJiraRestClient; import hudson.plugins.jira.extension.ExtendedJiraRestClient; import hudson.plugins.jira.extension.ExtendedVersion; import hudson.plugins.jira.model.JiraIssue; import hudson.security.ACL; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import hudson.util.Secret; import jakarta.servlet.ServletException; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import javax.annotation.PreDestroy; import jenkins.model.Jenkins; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.interceptor.RequirePOST; /** * You must get instance of this only by using the static {@link #get} or {@link #getSitesFromFolders(ItemGroup)} methods * The constructors are only used by Jenkins *

* Represents an external Jira installation and configuration * needed to access this Jira. *

* When adding new fields do not miss to look at readResolve method!! * * @author Kohsuke Kawaguchi */ public class JiraSite extends AbstractDescribableImpl { private static final Logger LOGGER = Logger.getLogger(JiraSite.class.getName()); /** * Regexp pattern that identifies Jira issue token. * If this pattern changes help pages (help-issue-pattern_xy.html) must be updated * First char must be a letter, then at least one letter, digit or underscore. * See issue JENKINS-729, JENKINS-4092 */ public static final Pattern DEFAULT_ISSUE_PATTERN = Pattern.compile("([a-zA-Z][a-zA-Z0-9_]+-[1-9][0-9]*)([^.]|\\.[^0-9]|\\.$|$)"); /** * Default rest api client calls timeout, in seconds * See issue JENKINS-31113 */ public static final int DEFAULT_TIMEOUT = 10; public static final int DEFAULT_READ_TIMEOUT = 30; public static final int DEFAULT_THREAD_EXECUTOR_NUMBER = 10; public static final Integer DEFAULT_ISSUES_FROM_JQL = 100; public static final Integer MAX_ALLOWED_ISSUES_FROM_JQL = 5000; /** * URL of Jira for Jenkins access, like {@code http://jira.codehaus.org/}. * Mandatory. Normalized to end with '/' */ public final URL url; /** * URL of Jira for normal access, like {@code http://jira.codehaus.org/}. * Mandatory. Normalized to end with '/' */ @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Backwards compatibility") public URL alternativeUrl; /** * Jira requires HTTP Authentication for login */ @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Backwards compatibility") public boolean useHTTPAuth; /** * The id of the credentials to use. Optional. */ @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Backwards compatibility") public String credentialsId; /** * Jira requires Bearer Authentication for login */ @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Backwards compatibility") public boolean useBearerAuth; /** * User name needed to login. Optional. * * @deprecated use credentialsId */ @Deprecated private transient String userName; /** * Password needed to login. Optional. * * @deprecated use credentialsId */ @Deprecated private transient Secret password; /** * Group visibility to constrain the visibility of the added comment. Optional. */ @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Backwards compatibility") public String groupVisibility; /** * Role visibility to constrain the visibility of the added comment. Optional. */ @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Backwards compatibility") public String roleVisibility; /** * True if this Jira is configured to allow Confluence-style Wiki comment. */ @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Backwards compatibility") public boolean supportsWikiStyleComment; /** * to record scm changes in jira issue * * @since 1.21 */ @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Backwards compatibility") public boolean recordScmChanges; /** * Disable annotating the changelogs * * @since todo */ @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Backwards compatibility") public boolean disableChangelogAnnotations; /** * user defined pattern * * @since 1.22 */ private String userPattern; private transient Pattern userPat; /** * updated jira issue for all status * * @since 1.22 */ @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Backwards compatibility") public boolean updateJiraIssueForAllStatus; /** * connection timeout used when calling jira rest api, in seconds */ @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Backwards compatibility") public int timeout = DEFAULT_TIMEOUT; /** * response timeout for jira rest call * * @since 3.0.3 */ private int readTimeout = DEFAULT_READ_TIMEOUT; /** * thread pool number * * @since 3.0.3 */ private int threadExecutorNumber = DEFAULT_THREAD_EXECUTOR_NUMBER; /** * Configuration for formatting (date -> text) in jira comments. */ private String dateTimePattern; /** * To add scm entry change date and time in jira comments. */ private boolean appendChangeTimestamp; /** * To allow configurable value of max issues from jql search via jira site global configuration. */ private int maxIssuesFromJqlSearch = DEFAULT_ISSUES_FROM_JQL; private int ioThreadCount = Integer.getInteger(JiraSite.class.getName() + ".httpclient.options.ioThreadCount", 2); /** * List of project keys (i.e., "MNG" portion of "MNG-512"), * last time we checked. Copy on write semantics. */ // TODO: seems like this is never invalidated (never set to null) // should we implement to invalidate this (say every hour)? private transient volatile Set projects; private transient Cache> issueCache = makeIssueCache(); /** * Used to guard the computation of {@link #projects} */ private transient Lock projectUpdateLock = new ReentrantLock(); private transient JiraSession jiraSession; private static ExecutorService executorService; // Deprecate the previous constructor but leave it in place for Java-level compatibility. @Deprecated public JiraSite( URL url, @CheckForNull URL alternativeUrl, @CheckForNull String credentialsId, boolean supportsWikiStyleComment, boolean recordScmChanges, @CheckForNull String userPattern, boolean updateJiraIssueForAllStatus, @CheckForNull String groupVisibility, @CheckForNull String roleVisibility, boolean useHTTPAuth) { this( url, alternativeUrl, credentialsId, supportsWikiStyleComment, recordScmChanges, userPattern, updateJiraIssueForAllStatus, groupVisibility, roleVisibility, useHTTPAuth, DEFAULT_TIMEOUT, DEFAULT_READ_TIMEOUT, DEFAULT_THREAD_EXECUTOR_NUMBER); } // Deprecate the previous constructor but leave it in place for Java-level compatibility. @Deprecated public JiraSite( URL url, @CheckForNull URL alternativeUrl, String userName, String password, boolean supportsWikiStyleComment, boolean recordScmChanges, @CheckForNull String userPattern, boolean updateJiraIssueForAllStatus, @CheckForNull String groupVisibility, @CheckForNull String roleVisibility, boolean useHTTPAuth) throws FormException { this( url, alternativeUrl, CredentialsHelper.migrateCredentials(userName, password, url), supportsWikiStyleComment, recordScmChanges, userPattern, updateJiraIssueForAllStatus, groupVisibility, roleVisibility, useHTTPAuth); } // Deprecate the previous constructor but leave it in place for Java-level compatibility. @Deprecated public JiraSite( URL url, URL alternativeUrl, StandardUsernamePasswordCredentials credentials, boolean supportsWikiStyleComment, boolean recordScmChanges, String userPattern, boolean updateJiraIssueForAllStatus, String groupVisibility, String roleVisibility, boolean useHTTPAuth) throws FormException { this( url, alternativeUrl, (String) null, supportsWikiStyleComment, recordScmChanges, userPattern, updateJiraIssueForAllStatus, groupVisibility, roleVisibility, useHTTPAuth, DEFAULT_TIMEOUT, DEFAULT_READ_TIMEOUT, DEFAULT_THREAD_EXECUTOR_NUMBER); if (credentials != null) { // we verify the credential really exists otherwise we migrate it StandardUsernamePasswordCredentials standardUsernamePasswordCredentials = CredentialsHelper.lookupSystemCredentials(credentials.getId(), url); if (standardUsernamePasswordCredentials == null) { credentials = CredentialsHelper.migrateCredentials( credentials.getUsername(), credentials.getPassword().getPlainText(), url); } } setCredentialsId(credentials == null ? null : credentials.getId()); } // Deprecate the previous constructor but leave it in place for Java-level compatibility. @Deprecated public JiraSite( URL url, URL alternativeUrl, String credentialsId, boolean supportsWikiStyleComment, boolean recordScmChanges, String userPattern, boolean updateJiraIssueForAllStatus, String groupVisibility, String roleVisibility, boolean useHTTPAuth, int timeout, int readTimeout, int threadExecutorNumber) { if (url != null) { url = toURL(url.toExternalForm()); } if (alternativeUrl != null) { alternativeUrl = toURL(alternativeUrl.toExternalForm()); } this.url = url; this.credentialsId = credentialsId; this.timeout = timeout; this.readTimeout = readTimeout; this.threadExecutorNumber = threadExecutorNumber; this.alternativeUrl = alternativeUrl; this.supportsWikiStyleComment = supportsWikiStyleComment; this.recordScmChanges = recordScmChanges; setUserPattern(userPattern); this.updateJiraIssueForAllStatus = updateJiraIssueForAllStatus; setGroupVisibility(groupVisibility); setRoleVisibility(roleVisibility); this.useHTTPAuth = useHTTPAuth; this.jiraSession = null; } @DataBoundConstructor public JiraSite(String url) { URL mainURL = toURL(url); if (mainURL == null) { throw new AssertionError("URL cannot be empty"); } this.url = mainURL; } // Deprecate the previous constructor but leave it in place for Java-level compatibility. @Deprecated public JiraSite( URL url, URL alternativeUrl, StandardUsernamePasswordCredentials credentials, boolean supportsWikiStyleComment, boolean recordScmChanges, String userPattern, boolean updateJiraIssueForAllStatus, String groupVisibility, String roleVisibility, boolean useHTTPAuth, int timeout, int readTimeout, int threadExecutorNumber) { this( url, alternativeUrl, credentials == null ? null : credentials.getId(), supportsWikiStyleComment, recordScmChanges, userPattern, updateJiraIssueForAllStatus, groupVisibility, roleVisibility, useHTTPAuth, timeout, readTimeout, threadExecutorNumber); } // Deprecate the previous constructor but leave it in place for Java-level compatibility. @Deprecated public JiraSite( URL url, URL alternativeUrl, StandardUsernamePasswordCredentials credentials, boolean supportsWikiStyleComment, boolean recordScmChanges, String userPattern, boolean updateJiraIssueForAllStatus, String groupVisibility, String roleVisibility, boolean useHTTPAuth, int timeout, int readTimeout, int threadExecutorNumber, boolean useBearerAuth) { this( url, alternativeUrl, credentials == null ? null : credentials.getId(), supportsWikiStyleComment, recordScmChanges, userPattern, updateJiraIssueForAllStatus, groupVisibility, roleVisibility, useHTTPAuth, timeout, readTimeout, threadExecutorNumber); this.useBearerAuth = useBearerAuth; } static URL toURL(String url) { url = Util.fixEmptyAndTrim(url); if (url == null) { return null; } if (!url.endsWith("/")) { url = url + "/"; } try { return new URL(url); } catch (MalformedURLException e) { throw new AssertionError(e); } } @DataBoundSetter public void setDisableChangelogAnnotations(boolean disableChangelogAnnotations) { this.disableChangelogAnnotations = disableChangelogAnnotations; } public boolean getDisableChangelogAnnotations() { return disableChangelogAnnotations; } /** * Sets connect timeout (in seconds). * If not specified, a default timeout will be used. * * @param timeoutSec Timeout in seconds */ @DataBoundSetter public void setTimeout(int timeoutSec) { this.timeout = timeoutSec; } public int getTimeout() { return timeout; } /** * Sets read timeout (in seconds). * If not specified, a default timeout will be used. * * @param readTimeout Timeout in seconds */ @DataBoundSetter public void setReadTimeout(int readTimeout) { this.readTimeout = readTimeout; } public int getReadTimeout() { return readTimeout; } public String getCredentialsId() { return credentialsId; } @DataBoundSetter public void setCredentialsId(String credentialsId) { this.credentialsId = Util.fixEmptyAndTrim(credentialsId); } @DataBoundSetter public void setDateTimePattern(String dateTimePattern) { this.dateTimePattern = Util.fixEmptyAndTrim(dateTimePattern); } @DataBoundSetter public void setThreadExecutorNumber(int threadExecutorNumber) { this.threadExecutorNumber = threadExecutorNumber; } public int getThreadExecutorNumber() { return threadExecutorNumber; } @DataBoundSetter public void setAppendChangeTimestamp(boolean appendChangeTimestamp) { this.appendChangeTimestamp = appendChangeTimestamp; } public String getDateTimePattern() { return dateTimePattern; } public boolean isAppendChangeTimestamp() { return appendChangeTimestamp; } public URL getAlternativeUrl() { return alternativeUrl; } public boolean isUseHTTPAuth() { return useHTTPAuth; } public boolean isUseBearerAuth() { return useBearerAuth; } public String getGroupVisibility() { return groupVisibility; } public String getRoleVisibility() { return roleVisibility; } public boolean isSupportsWikiStyleComment() { return supportsWikiStyleComment; } public boolean isRecordScmChanges() { return recordScmChanges; } public boolean isUpdateJiraIssueForAllStatus() { return updateJiraIssueForAllStatus; } @DataBoundSetter public void setAlternativeUrl(String alternativeUrl) { this.alternativeUrl = toURL(alternativeUrl); } @DataBoundSetter public void setUseHTTPAuth(boolean useHTTPAuth) { this.useHTTPAuth = useHTTPAuth; } @DataBoundSetter public void setUseBearerAuth(boolean useBearerAuth) { this.useBearerAuth = useBearerAuth; } @DataBoundSetter public void setGroupVisibility(String groupVisibility) { this.groupVisibility = Util.fixEmptyAndTrim(groupVisibility); } @DataBoundSetter public void setRoleVisibility(String roleVisibility) { this.roleVisibility = Util.fixEmptyAndTrim(roleVisibility); } @DataBoundSetter public void setSupportsWikiStyleComment(boolean supportsWikiStyleComment) { this.supportsWikiStyleComment = supportsWikiStyleComment; } @DataBoundSetter public void setRecordScmChanges(boolean recordScmChanges) { this.recordScmChanges = recordScmChanges; } @DataBoundSetter public void setUserPattern(String userPattern) { this.userPattern = Util.fixEmptyAndTrim(userPattern); if (this.userPattern == null) { this.userPat = null; } else { this.userPat = Pattern.compile(this.userPattern); } } @DataBoundSetter public void setUpdateJiraIssueForAllStatus(boolean updateJiraIssueForAllStatus) { this.updateJiraIssueForAllStatus = updateJiraIssueForAllStatus; } @DataBoundSetter public void setMaxIssuesFromJqlSearch(int maxIssuesFromJqlSearch) { this.maxIssuesFromJqlSearch = maxIssuesFromJqlSearch > MAX_ALLOWED_ISSUES_FROM_JQL ? MAX_ALLOWED_ISSUES_FROM_JQL : maxIssuesFromJqlSearch; } public int getMaxIssuesFromJqlSearch() { return maxIssuesFromJqlSearch; } @SuppressWarnings("unused") protected Object readResolve() throws FormException { JiraSite jiraSite; if (credentialsId == null && userName != null && password != null) { // Migrate credentials jiraSite = new JiraSite( url, alternativeUrl, userName, password.getPlainText(), supportsWikiStyleComment, recordScmChanges, userPattern, updateJiraIssueForAllStatus, groupVisibility, roleVisibility, useHTTPAuth); } else { jiraSite = new JiraSite( url, alternativeUrl, credentialsId, supportsWikiStyleComment, recordScmChanges, userPattern, updateJiraIssueForAllStatus, groupVisibility, roleVisibility, useHTTPAuth, timeout, readTimeout, threadExecutorNumber); } jiraSite.setAppendChangeTimestamp(appendChangeTimestamp); jiraSite.setDisableChangelogAnnotations(disableChangelogAnnotations); jiraSite.setDateTimePattern(dateTimePattern); jiraSite.setUseBearerAuth(useBearerAuth); if (this.maxIssuesFromJqlSearch <= 0) { jiraSite.setMaxIssuesFromJqlSearch(DEFAULT_ISSUES_FROM_JQL); } else { jiraSite.setMaxIssuesFromJqlSearch(maxIssuesFromJqlSearch); } return jiraSite; } protected static Cache> makeIssueCache() { return Caffeine.newBuilder().expireAfterAccess(2, TimeUnit.MINUTES).build(); } public String getName() { return url.toExternalForm(); } /** * @deprecated should not be used */ @Deprecated public JiraSession getSession() { return getSession(null); } /** * Gets a remote access session to this Jira site (job-aware) * Creates one if none exists already. * * @return null if remote access is not supported. */ @Nullable public JiraSession getSession(Item item) { return getSession(item, false); } JiraSession getSession(Item item, boolean uiValidation) { if (jiraSession == null) { jiraSession = createSession(item, uiValidation); } return jiraSession; } JiraSession createSession(Item item) { return createSession(item, false); } /** * Creates a remote access session to this Jira. * * @return null if remote access is not supported. */ JiraSession createSession(Item item, boolean uiValidation) { ItemGroup itemGroup = map(item); item = itemGroup instanceof Folder ? ((Folder) itemGroup) : item; StandardUsernamePasswordCredentials credentials = resolveCredentials(item, uiValidation); if (credentials == null) { LOGGER.fine("no Jira credentials available for " + item); return null; // remote access not supported } URI uri; try { uri = url.toURI(); } catch (URISyntaxException e) { LOGGER.warning("convert URL to URI error: " + e.getMessage()); throw new RuntimeException("failed to create JiraSession due to convert URI error"); } LOGGER.fine("creating Jira Session: " + uri); return JiraSessionFactory.create(this, uri, credentials); } Lock getProjectUpdateLock() { return projectUpdateLock; } /** * This method only supports credential matching by credentialsId. * Older methods are not and will not be supported as the credentials should have been migrated already. * * @param item can be null if top level * @param uiValidation if true and credentials not found at item level will not go up */ private StandardUsernamePasswordCredentials resolveCredentials(Item item, boolean uiValidation) { if (credentialsId == null) { LOGGER.fine("credentialsId is null"); return null; // remote access not supported } List req = URIRequirementBuilder.fromUri(url != null ? url.toExternalForm() : null) .build(); if (item != null) { StandardUsernamePasswordCredentials credentials = CredentialsMatchers.firstOrNull( CredentialsProvider.lookupCredentials( StandardUsernamePasswordCredentials.class, item, ACL.SYSTEM, req), CredentialsMatchers.withId(credentialsId)); if (credentials != null) { return credentials; } // during UI validation of the configuration we definitely don't want to expose // global credentials if (uiValidation) { return null; } } return CredentialsMatchers.firstOrNull( CredentialsProvider.lookupCredentials( StandardUsernamePasswordCredentials.class, Jenkins.get(), ACL.SYSTEM, req), CredentialsMatchers.withId(credentialsId)); } protected HttpClientOptions getHttpClientOptions() { final HttpClientOptions options = new HttpClientOptions(); options.setRequestTimeout(readTimeout, TimeUnit.SECONDS); options.setSocketTimeout(timeout, TimeUnit.SECONDS); options.setCallbackExecutor(getExecutorService()); options.setIoThreadCount(ioThreadCount); return options; } private ExecutorService getExecutorService() { if (executorService == null) { synchronized (JiraSite.class) { int nThreads = threadExecutorNumber; if (nThreads < 1) { LOGGER.warning("nThreads " + nThreads + " cannot be lower than 1 so use default " + DEFAULT_THREAD_EXECUTOR_NUMBER); nThreads = DEFAULT_THREAD_EXECUTOR_NUMBER; } executorService = Executors.newFixedThreadPool(nThreads, new ThreadFactory() { final AtomicInteger threadNumber = new AtomicInteger(0); @Override public Thread newThread(Runnable r) { return new Thread(r, "jira-plugin-http-request-" + threadNumber.getAndIncrement() + "-thread"); } }); } } return executorService; } // not really used but let's leave when it will be implemented @PreDestroy public void destroy() { try { this.jiraSession = null; } catch (Exception e) { LOGGER.log(Level.WARNING, "skip error destroying JiraSite:" + e.getMessage(), e); } } // ----------------------------------------------------------------------------------- // internal classes we want to override // ----------------------------------------------------------------------------------- public static class ExtendedAsynchronousJiraRestClientFactory implements JiraRestClientFactory { public ExtendedJiraRestClient create( final URI serverUri, final AuthenticationHandler authenticationHandler, HttpClientOptions options) { final DisposableHttpClient httpClient = createClient(serverUri, authenticationHandler, options); Thread t = Thread.currentThread(); ClassLoader orig = t.getContextClassLoader(); t.setContextClassLoader(JiraSite.class.getClassLoader()); try { return new ExtendedAsynchronousJiraRestClient(serverUri, httpClient); } finally { t.setContextClassLoader(orig); } } @Override public ExtendedJiraRestClient create(final URI serverUri, final AuthenticationHandler authenticationHandler) { final DisposableHttpClient httpClient = createClient(serverUri, authenticationHandler, new HttpClientOptions()); return new ExtendedAsynchronousJiraRestClient(serverUri, httpClient); } @Override public ExtendedJiraRestClient createWithBasicHttpAuthentication( final URI serverUri, final String username, final String password) { return create(serverUri, new BasicHttpAuthenticationHandler(username, password)); } @Override public ExtendedJiraRestClient createWithAuthenticationHandler( final URI serverUri, final AuthenticationHandler authenticationHandler) { return create(serverUri, authenticationHandler); } @Override public ExtendedJiraRestClient create(final URI serverUri, final HttpClient httpClient) { final DisposableHttpClient disposableHttpClient = createClient(httpClient); return new ExtendedAsynchronousJiraRestClient(serverUri, disposableHttpClient); } } private static DisposableHttpClient createClient( final URI serverUri, final AuthenticationHandler authenticationHandler, HttpClientOptions options) { final DefaultHttpClientFactory defaultHttpClientFactory = new DefaultHttpClientFactory( new NoOpEventPublisher(), new RestClientApplicationProperties(serverUri), new ThreadLocalContextManager() { @Override public Object getThreadLocalContext() { return null; } @Override public void setThreadLocalContext(Object context) {} @Override public void clearThreadLocalContext() {} }); final HttpClient httpClient = defaultHttpClientFactory.create(options); return new AtlassianHttpClientDecorator(httpClient, authenticationHandler) { @Override public void destroy() throws Exception { defaultHttpClientFactory.dispose(httpClient); } }; } private static DisposableHttpClient createClient(final HttpClient client) { return new AtlassianHttpClientDecorator(client, null) { @Override public void destroy() throws Exception { // This should never be implemented. This is simply creation of a wrapper // for AtlassianHttpClient which is extended by a destroy method. // Destroy method should never be called for AtlassianHttpClient coming from // a client! Imagine you create a RestClient, pass your own HttpClient there // and it gets destroy. } }; } private static class NoOpEventPublisher implements EventPublisher { @Override public void publish(Object o) {} @Override public void register(Object o) {} @Override public void unregister(Object o) {} @Override public void unregisterAll() {} } @SuppressWarnings("deprecation") private static class RestClientApplicationProperties implements ApplicationProperties { private final String baseUrl; private RestClientApplicationProperties(URI jiraURI) { this.baseUrl = jiraURI.getPath(); } @Override public String getBaseUrl() { return baseUrl; } /** * We'll always have an absolute URL as a client. */ @NonNull @Override public String getBaseUrl(UrlMode urlMode) { return baseUrl; } @NonNull @Override public String getDisplayName() { return "Atlassian Jira Rest Java Client"; } @NonNull @Override public String getPlatformId() { return ApplicationProperties.PLATFORM_JIRA; } @NonNull @Override public String getVersion() { return ""; } @NonNull @Override public Date getBuildDate() { throw new UnsupportedOperationException(); } @NonNull @Override public String getBuildNumber() { return String.valueOf(0); } @Override public File getHomeDirectory() { return new File("."); } @Override public String getPropertyValue(final String s) { throw new UnsupportedOperationException("Not implemented"); } @NonNull @Override public String getApplicationFileEncoding() { return System.getProperty("file.encoding"); } @NonNull @Override public Optional getLocalHomeDirectory() { return Optional.empty(); } @NonNull @Override public Optional getSharedHomeDirectory() { return Optional.empty(); } } // ----------------------------------------------------------------------------------- // // ----------------------------------------------------------------------------------- /** * @return the server URL */ @Nullable public URL getUrl() { return this.url != null ? this.url : this.alternativeUrl; } /** * Computes the URL to the given issue. */ public URL getUrl(JiraIssue issue) throws IOException { return getUrl(issue.getKey()); } /** * Computes the URL to the given issue. */ public URL getUrl(String id) throws MalformedURLException { return new URL(url, "browse/" + id.toUpperCase()); } /** * Computes the alternative link URL to the given issue. */ public URL getAlternativeUrl(String id) throws MalformedURLException { return alternativeUrl == null ? null : new URL(alternativeUrl, "browse/" + id.toUpperCase()); } /** * Gets the user-defined issue pattern if any. * * @return the pattern or null */ public Pattern getUserPattern() { if (userPattern == null) { return null; } if (userPat == null) { // We don't care about any thread race- or visibility issues here. // The worst thing which could happen, is that the pattern // is compiled multiple times. userPat = Pattern.compile(userPattern); } return userPat; } public Pattern getIssuePattern() { Pattern result = getUserPattern(); return result == null ? DEFAULT_ISSUE_PATTERN : result; } /** * Gets the list of project IDs in this Jira. * This information could be bit old, or it can be null. */ public Set getProjectKeys(Item item) { // FIXME it means projects list will be never updated until Jenkins is restarted... if (projects == null) { try { if (getProjectUpdateLock().tryLock(3, TimeUnit.SECONDS)) { try { JiraSession session = getSession(item); if (session != null) { projects = Collections.unmodifiableSet(session.getProjectKeys()); } } finally { getProjectUpdateLock().unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); // process this interruption later } catch (RestClientException e) { return Collections.emptySet(); } } // fall back to empty if failed to talk to the server if (projects == null) { return Collections.emptySet(); } return projects; } /** * Returns the remote issue with the given id or null if it wasn't found. */ @CheckForNull public JiraIssue getIssue(final String id) throws IOException { Optional issue = issueCache.get(id, s -> { if (this.jiraSession == null) { return Optional.empty(); } return Optional.ofNullable(this.jiraSession.getIssue(id)); }); if (issue == null || !issue.isPresent()) { return null; } return new JiraIssue(issue.get()); } @Deprecated public boolean existsIssue(String id) { try { return getIssue(id) != null; } catch (IOException | RestClientException e) { // restoring backward compat means even avoid exception throw new RuntimeException(e.getMessage(), e); } } /** * Returns all versions for the given project key. * * @param projectKey Project Key * @return A set of JiraVersions * @deprecated use {@link JiraSession#getVersions(String)} */ @Deprecated public Set getVersions(String projectKey) { if (this.jiraSession == null) { LOGGER.warning("Jira session could not be established"); return Collections.emptySet(); } return new HashSet<>(this.jiraSession.getVersions(projectKey)); } /** * Generates release notes for a given version. * * @param projectKey the project key * @param versionName the version * @param filter Additional JQL Filter. Example: status in (Resolved,Closed) * @return release notes * @throws TimeoutException if too long */ public String getReleaseNotesForFixVersion(String projectKey, String versionName, String filter) throws TimeoutException { if (this.jiraSession == null) { LOGGER.warning("Jira session could not be established"); return ""; } List issues = this.jiraSession.getIssuesWithFixVersion(projectKey, versionName, filter); if (issues.isEmpty()) { return ""; } Map> releaseNotes = new HashMap<>(); for (Issue issue : issues) { String key = issue.getKey(); String summary = issue.getSummary(); String status = issue.getStatus().getName(); String type = issue.getIssueType().getName(); Set issueSet; if (releaseNotes.containsKey(type)) { issueSet = releaseNotes.get(type); } else { issueSet = new HashSet<>(); releaseNotes.put(type, issueSet); } issueSet.add(String.format(" - [%s] %s (%s)", key, summary, status)); } StringBuilder sb = new StringBuilder(); for (Map.Entry> entry : releaseNotes.entrySet()) { sb.append(String.format("# %s%n", entry.getKey())); for (String issue : entry.getValue()) { sb.append(issue); sb.append("\n"); } } return sb.toString(); } /** * Migrates issues matching the jql query provided to a new fix version. * * @param projectKey The project key * @param toVersion The new fixVersion * @param query A JQL Query * @throws TimeoutException if too long */ public void replaceFixVersion(String projectKey, String fromVersion, String toVersion, String query) throws TimeoutException, RestClientException { if (this.jiraSession == null) { LOGGER.warning("Jira session could not be established"); return; } this.jiraSession.replaceFixVersion(projectKey, fromVersion, toVersion, query); } /** * Migrates issues matching the jql query provided to a new fix version. * * @param projectKey The project key * @param versionName The new fixVersion * @param query A JQL Query * @throws TimeoutException if too long */ public void migrateIssuesToFixVersion(String projectKey, String versionName, String query) throws TimeoutException { if (this.jiraSession == null) { LOGGER.warning("Jira session could not be established"); return; } this.jiraSession.migrateIssuesToFixVersion(projectKey, versionName, query); } /** * Adds new fix version to issues matching the jql. * * @param projectKey the project key * @param versionName the version * @param query the query * @throws TimeoutException if too long */ public void addFixVersionToIssue(String projectKey, String versionName, String query) throws TimeoutException, RestClientException { if (this.jiraSession == null) { LOGGER.warning("Jira session could not be established"); return; } this.jiraSession.addFixVersion(projectKey, versionName, query); } /** * Progresses all issues matching the JQL search, using the given workflow action. Optionally * adds a comment to the issue(s) at the same time. * * @param jqlSearch the query * @param workflowActionName the workflowActionName * @param comment the comment * @param console the console */ public boolean progressMatchingIssues( String jqlSearch, String workflowActionName, String comment, PrintStream console) throws RestClientException { if (this.jiraSession == null) { LOGGER.warning("Jira session could not be established"); console.println(Messages.FailedToConnect()); return false; } boolean success = true; List issues = this.jiraSession.getIssuesFromJqlSearch(jqlSearch); if (isEmpty(workflowActionName)) { console.println("[Jira] No workflow action was specified, " + "thus no status update will be made for any of the matching issues."); } for (Issue issue : issues) { String issueKey = issue.getKey(); if (isNotEmpty(comment)) { this.jiraSession.addComment(issueKey, comment, null, null); } if (isEmpty(workflowActionName)) { continue; } Integer actionId = this.jiraSession.getActionIdForIssue(issueKey, workflowActionName); if (actionId == null) { LOGGER.fine(String.format( "Invalid workflow action %s for issue %s; issue status = %s", workflowActionName, issueKey, issue.getStatus())); console.println(Messages.JiraIssueUpdateBuilder_UnknownWorkflowAction(issueKey, workflowActionName)); success = false; continue; } String newStatus = this.jiraSession.progressWorkflowAction(issueKey, actionId); console.println(String.format( "[Jira] Issue %s transitioned to \"%s\" due to action \"%s\".", issueKey, newStatus, workflowActionName)); } return success; } @Extension public static class DescriptorImpl extends Descriptor { @Override public String getDisplayName() { return "Jira Site"; } @SuppressWarnings("unused") // used by stapler public FormValidation doCheckUrl(@QueryParameter String value) throws IOException, ServletException { return checkUrl(value); } @SuppressWarnings("unused") // used by stapler public FormValidation doCheckAlternativeUrl(@QueryParameter String value) throws IOException, ServletException { return checkUrl(value); } private FormValidation checkUrl(String url) { if (Util.fixEmptyAndTrim(url) == null) { return FormValidation.ok(); } try { new URL(url); } catch (MalformedURLException e) { return FormValidation.error(String.format("Malformed URL (%s)", url), e); } return FormValidation.ok(); } /** * Checks if the user name and password are valid. */ @RequirePOST public FormValidation doValidate( @QueryParameter String url, @QueryParameter String credentialsId, @QueryParameter String groupVisibility, @QueryParameter String roleVisibility, @QueryParameter boolean useHTTPAuth, @QueryParameter String alternativeUrl, @QueryParameter int timeout, @QueryParameter int readTimeout, @QueryParameter int threadExecutorNumber, @QueryParameter boolean useBearerAuth, @AncestorInPath Item item) { if (item == null) { Jenkins.get().checkPermission(Jenkins.ADMINISTER); } else { item.checkPermission(Item.CONFIGURE); } url = Util.fixEmpty(url); alternativeUrl = Util.fixEmpty(alternativeUrl); URL mainURL, alternativeURL = null; try { if (url == null) { return FormValidation.error("No URL given"); } mainURL = new URL(url); } catch (MalformedURLException e) { return FormValidation.error(String.format("Malformed URL (%s)", url), e); } try { if (alternativeUrl != null) { alternativeURL = new URL(alternativeUrl); } } catch (MalformedURLException e) { return FormValidation.error(String.format("Malformed alternative URL (%s)", alternativeUrl), e); } credentialsId = Util.fixEmpty(credentialsId); JiraSite site = getBuilder() .withMainURL(mainURL) .withAlternativeURL(alternativeURL) .withCredentialsId(credentialsId) .withGroupVisibility(groupVisibility) .withRoleVisibility(roleVisibility) .withUseHTTPAuth(useHTTPAuth) .build(); if (threadExecutorNumber < 1) { return FormValidation.error(Messages.JiraSite_threadExecutorMinimunSize("1")); } if (timeout < 0) { return FormValidation.error(Messages.JiraSite_timeoutMinimunValue("1")); } if (readTimeout < 0) { return FormValidation.error(Messages.JiraSite_readTimeoutMinimunValue("1")); } site.setTimeout(timeout); site.setReadTimeout(readTimeout); site.setThreadExecutorNumber(threadExecutorNumber); site.setUseBearerAuth(useBearerAuth); try { JiraSession session = site.getSession(item, true); if (session == null) { return FormValidation.error("Cannot validate configuration"); } session.getMyPermissions(); return FormValidation.ok("Success"); } catch (RestClientException e) { LOGGER.log(Level.WARNING, "Failed to login to Jira at " + url, e); } finally { if (site != null) { site.destroy(); } } return FormValidation.error("Failed to login to Jira"); } @SuppressWarnings("unused") // Used by stapler public ListBoxModel doFillCredentialsIdItems( @AncestorInPath final Item item, @QueryParameter final String credentialsId, @QueryParameter final String url) { return CredentialsHelper.doFillCredentialsIdItems(item, credentialsId, url); } @SuppressWarnings("unused") // Used by stapler public FormValidation doCheckCredentialsId( @AncestorInPath final Item item, @QueryParameter final String value, @QueryParameter final String url) { return CredentialsHelper.doCheckFillCredentialsId(item, value, url); } Builder getBuilder() { return new Builder(); } } static class Builder { private URL mainURL; private URL alternativeURL; private String credentialsId; private boolean supportsWikiStyleComment; private boolean recordScmChanges; private String userPattern; private boolean updateJiraIssueForAllStatus; private String groupVisibility; private String roleVisibility; private boolean useHTTPAuth; public Builder withMainURL(URL mainURL) { this.mainURL = mainURL; return this; } public Builder withAlternativeURL(URL alternativeURL) { this.alternativeURL = alternativeURL; return this; } public Builder withCredentialsId(String credentialsId) { this.credentialsId = credentialsId; return this; } public Builder withSupportsWikiStyleComment(boolean supportsWikiStyleComment) { this.supportsWikiStyleComment = supportsWikiStyleComment; return this; } public Builder withRecordScmChanges(boolean recordScmChanges) { this.recordScmChanges = recordScmChanges; return this; } public Builder withUserPattern(String userPattern) { this.userPattern = userPattern; return this; } public Builder withUpdateJiraIssueForAllStatus(boolean updateJiraIssueForAllStatus) { this.updateJiraIssueForAllStatus = updateJiraIssueForAllStatus; return this; } public Builder withGroupVisibility(String groupVisibility) { this.groupVisibility = groupVisibility; return this; } public Builder withRoleVisibility(String roleVisibility) { this.roleVisibility = roleVisibility; return this; } public Builder withUseHTTPAuth(boolean useHTTPAuth) { this.useHTTPAuth = useHTTPAuth; return this; } public JiraSite build() { return new JiraSite( mainURL, alternativeURL, credentialsId, supportsWikiStyleComment, recordScmChanges, userPattern, updateJiraIssueForAllStatus, groupVisibility, roleVisibility, useHTTPAuth); } } // helper methods // yes this class hierarchy can be a real big mess... /** * @param item the Jenkins {@link Item} can be a {@link Job} or {@link Folder} * @return the parent as {@link ItemGroup} which can be {@link Jenkins} or {@link Folder} */ public static ItemGroup map(Item item) { ItemGroup parent = null; if (item != null) { parent = item instanceof Folder ? (Folder) item : item.getParent(); } return parent; } /** * Creates automatically jiraSession for each jiraSite found */ public static List getJiraSites(Item item) { ItemGroup itemGroup = JiraSite.map(item); List sites = (itemGroup instanceof Folder) ? getSitesFromFolders(itemGroup) : JiraGlobalConfiguration.get().getSites(); sites.stream().forEach(jiraSite -> jiraSite.getSession(item)); return sites; } /** * Creates automatically jiraSession for each jiraSite found */ public static List getSitesFromFolders(ItemGroup itemGroup) { List result = new ArrayList<>(); while (itemGroup instanceof AbstractFolder) { AbstractFolder folder = (AbstractFolder) itemGroup; JiraFolderProperty jiraFolderProperty = folder.getProperties().get(JiraFolderProperty.class); if (jiraFolderProperty != null && jiraFolderProperty.getSites().length != 0) { List sites = Arrays.asList(jiraFolderProperty.getSites()); // setup session for each so it's ready to use sites.forEach(jiraSite -> jiraSite.getSession(folder)); result.addAll(sites); } itemGroup = folder.getParent(); } return result; } /** * Gets the effective {@link JiraSite} associated with the given project * and creates automatically jiraSession for each jiraSite found * * @return null if no such was found. */ @Nullable public static JiraSite get(Job p) { JiraSite found = null; if (p != null) { JiraProjectProperty jpp = p.getProperty(JiraProjectProperty.class); if (jpp != null) { // Looks in global configuration for the site configured JiraSite site = jpp.getSite(); if (site != null) { found = site; } } } if (found == null && p != null) { // Check up the folder chain if a site is defined there // This only supports one site per folder List sitesFromFolders = getSitesFromFolders(p.getParent()); if (sitesFromFolders.size() > 0) { found = sitesFromFolders.get(0); } } if (found == null) { // none is explicitly configured. try the default --- // if only one is configured, that must be it. List sites = JiraGlobalConfiguration.get().getSites(); if (sites != null && sites.size() == 1) { found = sites.get(0); } } if (found != null) { // we create the session here found.getSession(p); } return found; } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraVersionCreator.java ================================================ package hudson.plugins.jira; import hudson.Extension; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Notifier; import hudson.tasks.Publisher; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.StaplerRequest2; /** * A build step which creates new Jira version * * @author Artem Koshelev artkoshelev@gmail.com * @deprecated Replaced by {@link JiraVersionCreatorBuilder}. Read its * description to see why. Kept for backward compatibility. */ @Deprecated public class JiraVersionCreator extends Notifier { private String jiraVersion; private String jiraProjectKey; private Boolean failIfAlreadyExists = true; @DataBoundConstructor public JiraVersionCreator(String jiraVersion, String jiraProjectKey) { this.jiraVersion = jiraVersion; this.jiraProjectKey = jiraProjectKey; } @Override public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } public String getJiraVersion() { return jiraVersion; } public void setJiraVersion(String jiraVersion) { this.jiraVersion = jiraVersion; } public String getJiraProjectKey() { return jiraProjectKey; } public void setJiraProjectKey(String jiraProjectKey) { this.jiraProjectKey = jiraProjectKey; } public boolean isFailIfAlreadyExists() { return failIfAlreadyExists; } @DataBoundSetter public void setFailIfAlreadyExists(boolean failIfAlreadyExists) { this.failIfAlreadyExists = failIfAlreadyExists; } @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) { VersionCreator versionCreator = new VersionCreator(); versionCreator.setFailIfAlreadyExists(failIfAlreadyExists); versionCreator.setJiraVersion(jiraVersion); versionCreator.setJiraProjectKey(jiraProjectKey); return versionCreator.perform(build.getProject(), build, listener); } @Override public BuildStepDescriptor getDescriptor() { return DESCRIPTOR; } protected Object readResolve() { if (failIfAlreadyExists == null) { setFailIfAlreadyExists(true); } return this; } @Extension public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); public static class DescriptorImpl extends BuildStepDescriptor { public DescriptorImpl() { super(JiraVersionCreator.class); } @Override public boolean isApplicable(Class jobType) { return true; } @Override public JiraVersionCreator newInstance(StaplerRequest2 req, JSONObject formData) throws FormException { return req.bindJSON(JiraVersionCreator.class, formData); } @Override public String getDisplayName() { return Messages.JiraVersionCreator_DisplayName(); } @Override public String getHelpFile() { return "/plugin/jira/help-version-create.html"; } } } ================================================ FILE: src/main/java/hudson/plugins/jira/JiraVersionCreatorBuilder.java ================================================ package hudson.plugins.jira; import hudson.EnvVars; import hudson.Extension; import hudson.model.AbstractProject; import hudson.model.Run; import hudson.model.TaskListener; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Builder; import jenkins.tasks.SimpleBuildStep; import net.sf.json.JSONObject; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.StaplerRequest2; /** * A build step which creates new Jira version. It has the same functionality as * {@link JiraVersionCreator} but can be used multiple times in the same build * (e.g. for different projects) and supports conditional triggering. * * @author marcin.czerwinski marcinczrw@gmail.com */ public class JiraVersionCreatorBuilder extends Builder implements SimpleBuildStep { private String jiraVersion; private String jiraProjectKey; private Boolean failIfAlreadyExists = true; @DataBoundConstructor public JiraVersionCreatorBuilder(String jiraVersion, String jiraProjectKey) { this.jiraVersion = jiraVersion; this.jiraProjectKey = jiraProjectKey; } @Override public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } public String getJiraVersion() { return jiraVersion; } public void setJiraVersion(String jiraVersion) { this.jiraVersion = jiraVersion; } public String getJiraProjectKey() { return jiraProjectKey; } public void setJiraProjectKey(String jiraProjectKey) { this.jiraProjectKey = jiraProjectKey; } public boolean isFailIfAlreadyExists() { return failIfAlreadyExists; } protected Object readResolve() { if (failIfAlreadyExists == null) { setFailIfAlreadyExists(true); } return this; } @DataBoundSetter public void setFailIfAlreadyExists(boolean failIfAlreadyExists) { this.failIfAlreadyExists = failIfAlreadyExists; } @Override public void perform(Run run, EnvVars env, TaskListener listener) { VersionCreator versionCreator = new VersionCreator(); versionCreator.setFailIfAlreadyExists(failIfAlreadyExists); versionCreator.setJiraVersion(jiraVersion); versionCreator.setJiraProjectKey(jiraProjectKey); versionCreator.perform(run.getParent(), run, listener); } @Override public boolean requiresWorkspace() { return false; } @Override public BuildStepDescriptor getDescriptor() { return DESCRIPTOR; } @Extension public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); @Symbol("jiraCreateVersion") public static class DescriptorImpl extends BuildStepDescriptor { public DescriptorImpl() { super(JiraVersionCreatorBuilder.class); } @Override public boolean isApplicable(Class jobType) { return true; } @Override public JiraVersionCreatorBuilder newInstance(StaplerRequest2 req, JSONObject formData) throws FormException { return req.bindJSON(JiraVersionCreatorBuilder.class, formData); } @Override public String getDisplayName() { return Messages.JiraVersionCreatorBuilder_DisplayName(); } @Override public String getHelpFile() { return "/plugin/jira/help-version-create.html"; } } } ================================================ FILE: src/main/java/hudson/plugins/jira/RunScmChangeExtractor.java ================================================ package hudson.plugins.jira; import hudson.model.AbstractBuild; import hudson.model.AbstractBuild.DependencyChange; import hudson.model.AbstractProject; import hudson.model.Run; import hudson.scm.ChangeLogSet; import hudson.scm.ChangeLogSet.Entry; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; public class RunScmChangeExtractor { // for reflection, until JENKINS-24141 private static final String GET_CHANGESET_METHOD = "getChangeSets"; private static final String CANNOT_ACCESS_GET_CHANGESET_METHOD = "cannot call " + GET_CHANGESET_METHOD; private RunScmChangeExtractor() {} public static List> getChanges(Run run) { if (run instanceof AbstractBuild) { ChangeLogSet cs = ((AbstractBuild) run).getChangeSet(); return cs.isEmptySet() ? Collections.>emptyList() : Collections.>singletonList(cs); } else if (run == null) { throw new IllegalStateException("run cannot be null!"); } else { return getChangesUsingReflection(run); } } /** * return changeSets using java reflection api, for example for workflow * jobs. * * until JENKINS-24141 * * @param run * - run that implements some type with GET_CHANGESET_METHOD * @return collection of scm ChangeLogSet entries */ @SuppressWarnings("unchecked") static List> getChangesUsingReflection(Run run) { Method getChangeSetMethod = null; for (Method method : run.getClass().getMethods()) { if (method.getName().equals(GET_CHANGESET_METHOD) && List.class.isAssignableFrom(method.getReturnType())) { getChangeSetMethod = method; break; } } if (getChangeSetMethod != null) { try { Object result = getChangeSetMethod.invoke(run, new Object[] {}); return (List>) result; } catch (IllegalAccessException e) { throw new IllegalStateException(CANNOT_ACCESS_GET_CHANGESET_METHOD, e); } catch (IllegalArgumentException e) { throw new IllegalStateException(CANNOT_ACCESS_GET_CHANGESET_METHOD, e); } catch (InvocationTargetException e) { throw new IllegalStateException(CANNOT_ACCESS_GET_CHANGESET_METHOD, e); } } else { // if run don't have GET_CHANGESET_METHOD, we don't support it throw new IllegalArgumentException( "Unsupported Run type " + run.getClass().getName()); } } public static Map getDependencyChanges(Run run) { if (run instanceof AbstractBuild) { Run previousBuild = run.getPreviousCompletedBuild(); if (previousBuild instanceof AbstractBuild) { return ((AbstractBuild) run).getDependencyChanges((AbstractBuild) previousBuild); } } // jenkins workflow plugin etc. return new HashMap(); } } ================================================ FILE: src/main/java/hudson/plugins/jira/Updater.java ================================================ package hudson.plugins.jira; import static java.lang.String.format; import com.atlassian.jira.rest.client.api.RestClientException; import com.atlassian.jira.rest.client.api.domain.Issue; import hudson.Util; import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.jira.model.JiraIssue; import hudson.plugins.jira.selector.AbstractIssueSelector; import hudson.scm.ChangeLogSet; import hudson.scm.ChangeLogSet.AffectedFile; import hudson.scm.ChangeLogSet.Entry; import hudson.scm.RepositoryBrowser; import hudson.scm.SCM; import java.io.IOException; import java.io.PrintStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.rmi.RemoteException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.model.Jenkins; import org.apache.commons.lang3.StringUtils; /** * Actual Jira update logic. * * @author Kohsuke Kawaguchi */ class Updater { private SCM scm; private List labels; private static final Logger LOGGER = Logger.getLogger(Updater.class.getName()); /** * Debug flag. */ public static boolean debug = false; Updater(SCM scm) { this(scm, new ArrayList<>()); } Updater(SCM scm, List labels) { this.scm = scm; if (labels == null) { this.labels = new ArrayList<>(); } else { this.labels = labels; } } boolean perform(Run run, TaskListener listener, AbstractIssueSelector selector) { PrintStream logger = listener.getLogger(); Set issues = null; try { JiraSite site = JiraSite.get(run.getParent()); if (site == null) { logger.println(Messages.NoJiraSite()); run.setResult(Result.FAILURE); return true; } String rootUrl = Jenkins.get().getRootUrl(); if (rootUrl == null) { logger.println(Messages.NoJenkinsUrl()); run.setResult(Result.FAILURE); return true; } Set ids = selector.findIssueIds(run, site, listener); if (ids.isEmpty()) { if (debug) { logger.println("No Jira issues found."); } return true; // nothing found here. } JiraSession session = site.getSession(run.getParent()); if (session == null) { logger.println(Messages.NoRemoteAccess()); run.setResult(Result.FAILURE); return true; } boolean doUpdate = false; // in case of workflow, it may be null if (site.updateJiraIssueForAllStatus || run.getResult() == null) { doUpdate = true; } else { doUpdate = run.getResult().isBetterOrEqualTo(Result.UNSTABLE); } boolean useWikiStyleComments = site.supportsWikiStyleComment; issues = getJiraIssues(ids, session, logger); run.addAction(new JiraBuildAction(issues)); if (doUpdate) { submitComments( run, logger, rootUrl, issues, session, useWikiStyleComments, site.recordScmChanges, site.groupVisibility, site.roleVisibility); } else { // this build didn't work, so carry forward the issues to the next build run.addAction(new JiraCarryOverAction(issues)); } } catch (Exception e) { LOGGER.log(Level.WARNING, "Error updating Jira issues. Saving issues for next build.", e); logger.println(e.getMessage()); if (issues != null && !issues.isEmpty()) { // updating issues failed, so carry forward issues to the next build run.addAction(new JiraCarryOverAction(issues)); } } return true; } /** * Submits comments for the given issues. * Removes from issues issues which have been successfully updated or are invalid * * @param build build * @param logger logger * @param jenkinsRootUrl jenkins root URL * @param session session * @param useWikiStyleComments whether to use wiki style comments * @param recordScmChanges whether to record SCM changes * @param groupVisibility group visibility * @throws RestClientException when HTTP request fails */ void submitComments( Run build, PrintStream logger, String jenkinsRootUrl, Set issues, JiraSession session, boolean useWikiStyleComments, boolean recordScmChanges, String groupVisibility, String roleVisibility) { // copy to prevent ConcurrentModificationException Set copy = new HashSet<>(issues); for (JiraIssue issue : copy) { logger.println(Messages.UpdatingIssue(issue.getKey())); try { session.addComment( issue.getKey(), createComment(build, useWikiStyleComments, jenkinsRootUrl, recordScmChanges, issue), groupVisibility, roleVisibility); if (!labels.isEmpty()) { session.addLabels(issue.getKey(), labels); } } catch (RestClientException e) { if (e.getStatusCode().or(0).equals(404)) { logger.println(issue.getKey() + " - Jira issue not found. Dropping comment from update queue."); } if (e.getStatusCode().or(0).equals(403)) { logger.println( issue.getKey() + " - Jenkins Jira user does not have permissions to comment on this issue. Preserving comment for future update."); continue; } if (e.getStatusCode().or(0).equals(401)) { logger.println(issue.getKey() + " - Jenkins Jira authentication problem. Preserving comment for future update."); continue; } logger.println(Messages.FailedToUpdateIssueWithCarryOver(issue.getKey())); logger.println(e.getLocalizedMessage()); } // if no exception is thrown during update, remove from the list as successfully updated issues.remove(issue); } } private static Set getJiraIssues(Set ids, JiraSession session, PrintStream logger) throws RemoteException { Set issues = new LinkedHashSet<>(ids.size()); for (String id : ids) { Issue issue = session.getIssue(id); if (issue == null) { logger.println(id + " issue doesn't exist in Jira"); continue; } issues.add(new JiraIssue(issue)); } return issues; } /** * Creates a comment to be used in Jira for the build. * For example: *
     *  SUCCESS: Integrated in Job #nnnn (See [http://jenkins.domain/job/Job/nnnn/])\r
     *  JIRA-XXXX: Commit message. (Author _author@email.domain_:
     *  [https://bitbucket.org/user/repo/changeset/9af8e4c4c909/])\r
     * 
*/ private String createComment( Run build, boolean wikiStyle, String jenkinsRootUrl, boolean recordScmChanges, JiraIssue jiraIssue) { Result result = build.getResult(); // if we run from workflow we dont known final result if (result == null) { return format( wikiStyle ? "Integrated in [%2$s|%3$s]\n%4$s" : "Integrated in Jenkins build %2$s (See [%3$s])\n%4$s", jenkinsRootUrl, build.getFullDisplayName(), Util.encode(jenkinsRootUrl + build.getUrl()), getScmComments(wikiStyle, build, recordScmChanges, jiraIssue)); } else { return format( wikiStyle ? "%6$s: Integrated in !%1$simages/16x16/%3$s! [%2$s|%4$s]\n%5$s" : "%6$s: Integrated in Jenkins build %2$s (See [%4$s])\n%5$s", jenkinsRootUrl, build.getFullDisplayName(), result.color.getImage(), Util.encode(jenkinsRootUrl + build.getUrl()), getScmComments(wikiStyle, build, recordScmChanges, jiraIssue), result.toString()); } } private String getScmComments(boolean wikiStyle, Run run, boolean recordScmChanges, JiraIssue jiraIssue) { StringBuilder comment = new StringBuilder(); for (ChangeLogSet set : RunScmChangeExtractor.getChanges(run)) { for (Entry change : set) { if (jiraIssue != null && !StringUtils.containsIgnoreCase(change.getMsg(), jiraIssue.getKey())) { continue; } comment.append(createScmChangeEntryDescription(run, change, wikiStyle, recordScmChanges)); } } if (jiraIssue != null) { final Run prev = run.getPreviousCompletedBuild(); if (prev != null) { final JiraCarryOverAction a = prev.getAction(JiraCarryOverAction.class); if (a != null && a.getIDs().contains(jiraIssue.getKey())) { comment.append(getScmComments(wikiStyle, prev, recordScmChanges, jiraIssue)); } } } return comment.toString(); } protected String createScmChangeEntryDescription( Run run, Entry change, boolean wikiStyle, boolean recordScmChanges) { StringBuilder description = new StringBuilder(); RepositoryBrowser repoBrowser = getRepositoryBrowser(run); JiraSite site = JiraSite.get(run.getParent()); if (change.getMsg() != null) { description.append(change.getMsg()); } String revision = getRevision(change); if (revision != null) { description.append(" ("); appendAuthorToDescription(change, description); if (site.isAppendChangeTimestamp() && change.getTimestamp() > 0) { appendChangeTimestampToDescription(description, site, change.getTimestamp()); description.append(" "); } appendRevisionToDescription(change, wikiStyle, description, repoBrowser, revision); description.append(")"); } description.append("\n"); if (recordScmChanges) { appendAffectedFilesToDescription(change, description); } return description.toString(); } protected void appendAuthorToDescription(Entry change, StringBuilder description) { if (change.getAuthor() != null) { change.getAuthor(); String uid = change.getAuthor().getId(); if (StringUtils.isNotBlank(uid)) { description.append(uid).append(": "); } } } protected void appendRevisionToDescription( Entry change, boolean wikiStyle, StringBuilder description, RepositoryBrowser repoBrowser, String revision) { URL url = null; if (repoBrowser != null) { try { url = repoBrowser.getChangeSetLink(change); } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to calculate SCM repository browser link", e); } } if (url != null && StringUtils.isNotBlank(url.toExternalForm())) { if (wikiStyle) { description.append("[").append(revision).append("|"); description.append(url.toExternalForm()).append("]"); } else { description.append("[").append(url.toExternalForm()).append("]"); } } else { description.append("rev ").append(revision); } } protected void appendAffectedFilesToDescription(Entry change, StringBuilder description) { // see http://issues.jenkins-ci.org/browse/JENKINS-2508 // added additional try .. catch; getAffectedFiles is not supported // by all SCM implementations try { for (AffectedFile affectedFile : change.getAffectedFiles()) { description.append("* "); if (affectedFile.getEditType() != null) { description .append("(") .append(affectedFile.getEditType().getName()) .append(") "); } if (affectedFile.getPath() != null) { description.append(affectedFile.getPath()); } description.append("\n"); } } catch (UnsupportedOperationException e) { LOGGER.warning("Unsupported SCM operation 'getAffectedFiles'. Fall back to getAffectedPaths."); for (String affectedPath : change.getAffectedPaths()) { description.append("* ").append(affectedPath).append("\n"); } } } protected void appendChangeTimestampToDescription(StringBuilder description, JiraSite site, long timestamp) { DateFormat df = null; if (StringUtils.isBlank(site.getDateTimePattern())) { // default format for current locale df = new SimpleDateFormat("d/M/yy hh:mm a", Locale.getDefault()); } else { df = new SimpleDateFormat(site.getDateTimePattern()); } Date changeDate = new Date(timestamp); String dateTimeString = df.format(changeDate); description.append(dateTimeString); } private RepositoryBrowser getRepositoryBrowser(Run run) { SCM scm = getScm(); if (scm != null) { return scm.getEffectiveBrowser(); } return null; } private static String getRevision(Entry entry) { String commitId = entry.getCommitId(); if (commitId != null) { return commitId; } // fall back to old SVN-specific solution, if we have only installed an old subversion-plugin // which doesn't implement getCommitId, yet try { Class clazz = entry.getClass(); Method method = clazz.getMethod("getRevision", (Class[]) null); Object revObj = method.invoke(entry, (Object[]) null); return (revObj != null) ? revObj.toString() : null; } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { return null; } } private SCM getScm() { return scm; } } ================================================ FILE: src/main/java/hudson/plugins/jira/VersionCreator.java ================================================ package hudson.plugins.jira; import static org.apache.commons.lang3.StringUtils.isEmpty; import hudson.model.BuildListener; import hudson.model.Job; import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.jira.extension.ExtendedVersion; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.logging.Logger; /** * Performs an action which creates new Jira version. */ class VersionCreator { private static final Logger LOGGER = Logger.getLogger(VersionCreator.class.getName()); private boolean failIfAlreadyExists = true; private String jiraVersion; private String jiraProjectKey; public VersionCreator setFailIfAlreadyExists(boolean failIfAlreadyExists) { this.failIfAlreadyExists = failIfAlreadyExists; return this; } public VersionCreator setJiraVersion(String jiraVersion) { this.jiraVersion = jiraVersion; return this; } public VersionCreator setJiraProjectKey(String jiraProjectKey) { this.jiraProjectKey = jiraProjectKey; return this; } protected boolean perform(Job project, Run build, TaskListener listener) { String realVersion = null; String realProjectKey = null; try { realVersion = build.getEnvironment(listener).expand(jiraVersion); realProjectKey = build.getEnvironment(listener).expand(jiraProjectKey); if (isEmpty(realVersion)) { throw new IllegalArgumentException("No version specified"); } if (isEmpty(realProjectKey)) { throw new IllegalArgumentException("No project specified"); } String finalRealVersion = realVersion; JiraSession session = getSiteForProject(project).getSession(project); List existingVersions = Optional.ofNullable(session.getVersions(realProjectKey)).orElse(Collections.emptyList()); // check if version already exists if (existingVersions.stream().anyMatch(v -> v.getName().equals(finalRealVersion))) { listener.getLogger().println(Messages.JiraVersionCreator_VersionExists(realVersion, realProjectKey)); if (failIfAlreadyExists) { if (listener instanceof BuildListener) { ((BuildListener) listener).finished(Result.FAILURE); } return false; } // version exists but we don't fail the build return true; } listener.getLogger().println(Messages.JiraVersionCreator_CreatingVersion(realVersion, realProjectKey)); addVersion(realVersion, realProjectKey, session); return true; } catch (Exception e) { e.printStackTrace( listener.fatalError("Unable to add version %s to Jira project %s", realVersion, realProjectKey, e)); } return false; } /** * Creates given version in given project * @param version version name * @param projectKey project key * @param session session */ protected void addVersion(String version, String projectKey, JiraSession session) { if (session == null) { LOGGER.warning("Jira session could not be established"); return; } session.addVersion(version, projectKey); } protected JiraSite getSiteForProject(Job project) { return JiraSite.get(project); } } ================================================ FILE: src/main/java/hudson/plugins/jira/VersionReleaser.java ================================================ package hudson.plugins.jira; import hudson.model.BuildListener; import hudson.model.Job; import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.jira.extension.ExtendedVersion; import java.util.List; import java.util.Optional; import java.util.logging.Logger; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; /** * used by JiraReleaseVersionUpdaterBuilder to mark a version as released */ public class VersionReleaser { private static final Logger LOGGER = Logger.getLogger(VersionReleaser.class.getName()); protected boolean perform( Job project, String jiraProjectKey, String jiraRelease, String jiraDescription, Run run, TaskListener listener) { String realRelease = "NOT_SET"; String realProjectKey = null; String realDescription; try { realProjectKey = run.getEnvironment(listener).expand(jiraProjectKey); realRelease = run.getEnvironment(listener).expand(jiraRelease); realDescription = run.getEnvironment(listener).expand(jiraDescription); if (StringUtils.isEmpty(realRelease)) { throw new IllegalArgumentException("Release is Empty"); } if (StringUtils.isEmpty(realProjectKey)) { throw new IllegalArgumentException("No project specified"); } String finalRealRelease = realRelease; JiraSite site = getSiteForProject(project); Optional sameNamedVersion = site.getVersions(realProjectKey).stream() .filter(version -> version.getName().equals(finalRealRelease) && version.isReleased()) .findFirst(); if (sameNamedVersion.isPresent()) { listener.getLogger().println(Messages.VersionReleaser_AlreadyReleased(realRelease, realProjectKey)); } else { listener.getLogger().println(Messages.VersionReleaser_MarkingReleased(realRelease, realProjectKey)); releaseVersion(realProjectKey, realRelease, realDescription, site.getSession(project)); } } catch (Exception e) { listener.fatalError("Unable to release jira version %s/%s: %s", realRelease, realProjectKey, e); if (listener instanceof BuildListener) { ((BuildListener) listener).finished(Result.FAILURE); } return false; } return true; } /** * Release a given version. * * @param projectKey The Project Key * @param versionName The name of the version * @param versionDescription The description of the version */ protected void releaseVersion( String projectKey, String versionName, String versionDescription, JiraSession session) { if (session == null) { LOGGER.warning("Jira session could not be established"); return; } List versions = session.getVersions(projectKey); java.util.Optional matchingVersion = versions.stream() .filter(version -> version.getName().equals(versionName)) .findFirst(); if (matchingVersion.isPresent()) { ExtendedVersion version = matchingVersion.get(); ExtendedVersion releaseVersion = new ExtendedVersion( version.getSelf(), version.getId(), version.getName(), !StringUtils.isEmpty(versionDescription) ? versionDescription : version.getDescription(), version.isArchived(), true, version.getStartDate(), new DateTime()); session.releaseVersion(projectKey, releaseVersion); } } protected JiraSite getSiteForProject(Job project) { return JiraSite.get(project); } } ================================================ FILE: src/main/java/hudson/plugins/jira/auth/BearerHttpAuthenticationHandler.java ================================================ package hudson.plugins.jira.auth; import com.atlassian.httpclient.api.Request.Builder; import com.atlassian.jira.rest.client.api.AuthenticationHandler; /** * Authentication handler for bearer authentication * * @author Elia Bracci */ public class BearerHttpAuthenticationHandler implements AuthenticationHandler { private static final String AUTHORIZATION_HEADER = "Authorization"; private final String token; /** * Bearer http authentication handler constructor * @param token pat or api token to use for bearer authentication */ public BearerHttpAuthenticationHandler(final String token) { this.token = token; } @Override public void configure(Builder builder) { builder.setHeader(AUTHORIZATION_HEADER, "Bearer " + token); } } ================================================ FILE: src/main/java/hudson/plugins/jira/extension/ExtendedAsynchronousJiraRestClient.java ================================================ package hudson.plugins.jira.extension; import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClient; import com.atlassian.jira.rest.client.internal.async.DisposableHttpClient; import java.net.URI; import javax.ws.rs.core.UriBuilder; public class ExtendedAsynchronousJiraRestClient extends AsynchronousJiraRestClient implements ExtendedJiraRestClient { private final ExtendedVersionRestClient extendedVersionRestClient; private final ExtendedMyPermissionsRestClient extendedMyPermissionsRestClient; public ExtendedAsynchronousJiraRestClient(URI serverUri, DisposableHttpClient httpClient) { super(serverUri, httpClient); final URI baseUri = UriBuilder.fromUri(serverUri).path("/rest/api/latest").build(); extendedVersionRestClient = new ExtendedAsynchronousVersionRestClient(baseUri, httpClient); extendedMyPermissionsRestClient = new ExtendedAsynchronousMyPermissionsRestClient(baseUri, httpClient); } @Override public ExtendedVersionRestClient getExtendedVersionRestClient() { return extendedVersionRestClient; } @Override public ExtendedMyPermissionsRestClient getExtendedMyPermissionsRestClient() { return extendedMyPermissionsRestClient; } } ================================================ FILE: src/main/java/hudson/plugins/jira/extension/ExtendedAsynchronousMyPermissionsRestClient.java ================================================ package hudson.plugins.jira.extension; import com.atlassian.httpclient.api.HttpClient; import com.atlassian.jira.rest.client.api.domain.Permissions; import com.atlassian.jira.rest.client.internal.async.AsynchronousMyPermissionsRestClient; import com.atlassian.jira.rest.client.internal.json.PermissionsJsonParser; import io.atlassian.util.concurrent.Promise; import java.net.URI; import javax.ws.rs.core.UriBuilder; public class ExtendedAsynchronousMyPermissionsRestClient extends AsynchronousMyPermissionsRestClient implements ExtendedMyPermissionsRestClient { private static final String URI_PREFIX = "mypermissions"; private final URI baseUri; private final PermissionsJsonParser permissionsJsonParser = new PermissionsJsonParser(); ExtendedAsynchronousMyPermissionsRestClient(final URI baseUri, final HttpClient client) { super(baseUri, client); this.baseUri = baseUri; } @Override public Promise getMyPermissions() { final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri).path(URI_PREFIX); uriBuilder.queryParam("permissions", "BROWSE_PROJECTS"); return getAndParse(uriBuilder.build(), permissionsJsonParser); } } ================================================ FILE: src/main/java/hudson/plugins/jira/extension/ExtendedAsynchronousVersionRestClient.java ================================================ package hudson.plugins.jira.extension; import com.atlassian.httpclient.api.HttpClient; import com.atlassian.jira.rest.client.internal.async.AsynchronousVersionRestClient; import io.atlassian.util.concurrent.Promise; import java.net.URI; import javax.ws.rs.core.UriBuilder; public class ExtendedAsynchronousVersionRestClient extends AsynchronousVersionRestClient implements ExtendedVersionRestClient { private final URI versionRootUri; ExtendedAsynchronousVersionRestClient(URI baseUri, HttpClient client) { super(baseUri, client); versionRootUri = UriBuilder.fromUri(baseUri).path("version").build(); } @Override public Promise getExtendedVersion(URI versionUri) { return getAndParse(versionUri, new ExtendedVersionJsonParser()); } @Override public Promise createExtendedVersion(ExtendedVersionInput versionInput) { return postAndParse( versionRootUri, versionInput, new ExtendedVersionInputJsonGenerator(), new ExtendedVersionJsonParser()); } @Override public Promise updateExtendedVersion(URI versionUri, ExtendedVersionInput versionInput) { return putAndParse( versionUri, versionInput, new ExtendedVersionInputJsonGenerator(), new ExtendedVersionJsonParser()); } } ================================================ FILE: src/main/java/hudson/plugins/jira/extension/ExtendedJiraRestClient.java ================================================ package hudson.plugins.jira.extension; import com.atlassian.jira.rest.client.api.JiraRestClient; public interface ExtendedJiraRestClient extends JiraRestClient { ExtendedVersionRestClient getExtendedVersionRestClient(); ExtendedMyPermissionsRestClient getExtendedMyPermissionsRestClient(); } ================================================ FILE: src/main/java/hudson/plugins/jira/extension/ExtendedMyPermissionsRestClient.java ================================================ package hudson.plugins.jira.extension; import com.atlassian.jira.rest.client.api.domain.Permissions; import io.atlassian.util.concurrent.Promise; public interface ExtendedMyPermissionsRestClient { Promise getMyPermissions(); } ================================================ FILE: src/main/java/hudson/plugins/jira/extension/ExtendedVersion.java ================================================ package hudson.plugins.jira.extension; import com.atlassian.jira.rest.client.api.domain.Version; import edu.umd.cs.findbugs.annotations.Nullable; import java.net.URI; import org.joda.time.DateTime; public class ExtendedVersion extends Version { private final DateTime startDate; public ExtendedVersion( URI self, @Nullable Long id, String name, String description, boolean archived, boolean released, @Nullable DateTime startDate, @Nullable DateTime releaseDate) { super(self, id, name, description, archived, released, releaseDate); this.startDate = startDate; } @Nullable public DateTime getStartDate() { return startDate; } } ================================================ FILE: src/main/java/hudson/plugins/jira/extension/ExtendedVersionInput.java ================================================ package hudson.plugins.jira.extension; import com.atlassian.jira.rest.client.api.domain.input.VersionInput; import edu.umd.cs.findbugs.annotations.Nullable; import java.util.Objects; import org.apache.commons.lang3.builder.ToStringBuilder; import org.joda.time.DateTime; public class ExtendedVersionInput extends VersionInput { private final DateTime startDate; public ExtendedVersionInput( String projectKey, String name, @Nullable String description, @Nullable DateTime startDate, @Nullable DateTime releaseDate, boolean isArchived, boolean isReleased) { super(projectKey, name, description, releaseDate, isArchived, isReleased); this.startDate = startDate; } @Override public String toString() { return new ToStringBuilder(this) .append("parent", super.toString()) .append("startDate", startDate) .toString(); } @Override public boolean equals(Object obj) { if (obj instanceof ExtendedVersionInput) { ExtendedVersionInput that = (ExtendedVersionInput) obj; return Objects.equals(this.startDate, that.startDate); } return super.equals(obj); } @Override public int hashCode() { return Objects.hash(startDate, super.hashCode()); } DateTime getStartDate() { return startDate; } } ================================================ FILE: src/main/java/hudson/plugins/jira/extension/ExtendedVersionInputJsonGenerator.java ================================================ package hudson.plugins.jira.extension; import com.atlassian.jira.rest.client.internal.json.JsonParseUtil; import com.atlassian.jira.rest.client.internal.json.gen.JsonGenerator; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; public class ExtendedVersionInputJsonGenerator implements JsonGenerator { @Override public JSONObject generate(ExtendedVersionInput version) throws JSONException { final JSONObject jsonObject = new JSONObject(); jsonObject.put("name", version.getName()); jsonObject.put("project", version.getProjectKey()); if (version.getDescription() != null) { jsonObject.put("description", version.getDescription()); } if (version.getStartDate() != null) { jsonObject.put("startDate", JsonParseUtil.formatDate(version.getStartDate())); } if (version.getReleaseDate() != null) { jsonObject.put("releaseDate", JsonParseUtil.formatDate(version.getReleaseDate())); } jsonObject.put("released", version.isReleased()); jsonObject.put("archived", version.isArchived()); return jsonObject; } } ================================================ FILE: src/main/java/hudson/plugins/jira/extension/ExtendedVersionJsonParser.java ================================================ package hudson.plugins.jira.extension; import com.atlassian.jira.rest.client.internal.json.JsonObjectParser; import com.atlassian.jira.rest.client.internal.json.JsonParseUtil; import java.net.URI; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; import org.joda.time.DateTime; public class ExtendedVersionJsonParser implements JsonObjectParser { @Override public ExtendedVersion parse(JSONObject json) throws JSONException { final URI self = JsonParseUtil.getSelfUri(json); final Long id = JsonParseUtil.getOptionalLong(json, "id"); final String name = json.getString("name"); final String description = JsonParseUtil.getOptionalString(json, "description"); final boolean isArchived = json.getBoolean("archived"); final boolean isReleased = json.getBoolean("released"); final String startDateStr = JsonParseUtil.getOptionalString(json, "startDate"); final String releaseDateStr = JsonParseUtil.getOptionalString(json, "releaseDate"); final DateTime startDate = parseDate(startDateStr); final DateTime releaseDate = parseDate(releaseDateStr); return new ExtendedVersion(self, id, name, description, isArchived, isReleased, startDate, releaseDate); } private DateTime parseDate(String dateStr) { if (dateStr != null) { return dateStr.length() > "YYYY-MM-RR".length() ? JsonParseUtil.parseDateTime(dateStr) : JsonParseUtil.parseDate(dateStr); } else { return null; } } } ================================================ FILE: src/main/java/hudson/plugins/jira/extension/ExtendedVersionRestClient.java ================================================ package hudson.plugins.jira.extension; import com.atlassian.jira.rest.client.api.VersionRestClient; import io.atlassian.util.concurrent.Promise; import java.net.URI; public interface ExtendedVersionRestClient extends VersionRestClient { Promise getExtendedVersion(URI versionUri); Promise createExtendedVersion(ExtendedVersionInput versionInput); Promise updateExtendedVersion(URI versionUri, ExtendedVersionInput versionInput); } ================================================ FILE: src/main/java/hudson/plugins/jira/listissuesparameter/JiraIssueParameterDefinition.java ================================================ /* * Copyright 2011-2012 Insider Guides, Inc., MeetMe, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package hudson.plugins.jira.listissuesparameter; import static hudson.Util.fixNull; import com.atlassian.jira.rest.client.api.RestClientException; import com.atlassian.jira.rest.client.api.domain.Issue; import com.atlassian.jira.rest.client.api.domain.IssueField; import hudson.Extension; import hudson.cli.CLICommand; import hudson.model.Item; import hudson.model.Job; import hudson.model.ParameterDefinition; import hudson.model.ParameterValue; import hudson.model.ParametersDefinitionProperty; import hudson.plugins.jira.JiraSession; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.Messages; import hudson.util.ListBoxModel; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeoutException; import net.sf.json.JSONObject; import org.apache.commons.lang3.StringUtils; import org.jenkinsci.Symbol; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest2; import org.kohsuke.stapler.interceptor.RequirePOST; public class JiraIssueParameterDefinition extends ParameterDefinition { private static final long serialVersionUID = 3927562542249244416L; private String jiraIssueFilter; private String altSummaryFields; @DataBoundConstructor public JiraIssueParameterDefinition(String name, String description, String jiraIssueFilter) { super(name, description); this.jiraIssueFilter = jiraIssueFilter; } @Override public ParameterValue createValue(StaplerRequest2 req) { String[] values = req.getParameterValues(getName()); if (values == null || values.length != 1 || values[0].isEmpty()) { return null; } return new JiraIssueParameterValue(getName(), values[0]); } @Override public ParameterValue createValue(StaplerRequest2 req, JSONObject formData) { JiraIssueParameterValue value = req.bindJSON(JiraIssueParameterValue.class, formData); return value; } @Override public ParameterValue createValue(CLICommand command, String value) throws IOException, InterruptedException { return new JiraIssueParameterValue(getName(), value); } public List getIssues() throws IOException, TimeoutException, RestClientException { Job job = Stapler.getCurrentRequest2().findAncestorObject(Job.class); return getIssues(job); } List getIssues(Job job) { JiraSite site = JiraSite.get(job); if (site == null) { throw new IllegalStateException( "Jira site needs to be configured in the project " + job.getFullDisplayName()); } JiraSession session = site.getSession(job); if (session == null) { throw new IllegalStateException("Remote access for Jira isn't configured in Jenkins"); } List issues = session.getIssuesFromJqlSearch(jiraIssueFilter); List issueValues = new ArrayList<>(); for (Issue issue : fixNull(issues)) { issueValues.add(new Result(issue, this.altSummaryFields)); } return issueValues; } public String getJiraIssueFilter() { return jiraIssueFilter; } public void setJiraIssueFilter(String jiraIssueFilter) { this.jiraIssueFilter = jiraIssueFilter; } public String getAltSummaryFields() { return altSummaryFields; } @DataBoundSetter public void setAltSummaryFields(String altSummaryFields) { this.altSummaryFields = altSummaryFields; } @Extension @Symbol("jiraIssue") public static class DescriptorImpl extends ParameterDescriptor { @Override public String getDisplayName() { return "Jira Issue Parameter"; } @RequirePOST public ListBoxModel doFillValueItems(@AncestorInPath Job job, @QueryParameter String name) { ListBoxModel items = new ListBoxModel(); if (job.hasPermission(Item.BUILD)) { ParametersDefinitionProperty prop = job.getProperty(ParametersDefinitionProperty.class); if (prop != null) { ParameterDefinition def = prop.getParameterDefinition(name); if (def instanceof JiraIssueParameterDefinition jiraIssueDef) { List issueValues = jiraIssueDef.getIssues(job); issueValues.forEach(it -> items.add(it.key + ": " + it.summary, it.key)); } } } if (items.isEmpty()) { items.add(Messages.JiraIssueParameterDefinition_NoIssueMatchedSearch(), ""); } return items; } } public static class Result { public final String key; public final String summary; public Result(final Issue issue) { this(issue, null); } public Result(final Issue issue, String altSummaryFields) { this.key = issue.getKey(); if (StringUtils.isEmpty(altSummaryFields)) { this.summary = issue.getSummary(); } else { String[] fields = altSummaryFields.split(","); StringBuilder sb = new StringBuilder(); for (String f : fields) { String fn = f.trim(); if (StringUtils.isNotEmpty(fn)) { IssueField field = issue.getFieldByName(fn); if (field != null && field.getValue() != null) { String fv = field.getValue().toString(); if (StringUtils.isNotEmpty(fv)) { sb.append(fv); sb.append(' '); } } } } this.summary = sb.toString().trim(); } } } } ================================================ FILE: src/main/java/hudson/plugins/jira/listissuesparameter/JiraIssueParameterValue.java ================================================ /* * Copyright 2011-2012 Insider Guides, Inc., MeetMe, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package hudson.plugins.jira.listissuesparameter; import hudson.EnvVars; import hudson.model.AbstractBuild; import hudson.model.ParameterValue; import hudson.model.Run; import hudson.util.VariableResolver; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.export.Exported; public class JiraIssueParameterValue extends ParameterValue { private static final long serialVersionUID = -1078274709338167211L; private String value; @DataBoundConstructor public JiraIssueParameterValue(final String name, final String value) { super(name); this.value = value; } @Override public void buildEnvironment(final Run run, final EnvVars env) { Object paramValue = getValue(); env.put(getName(), paramValue != null ? paramValue.toString() : ""); } @Override public VariableResolver createVariableResolver(final AbstractBuild build) { return new VariableResolver() { @Override public String resolve(final String name) { if (JiraIssueParameterValue.this.name.equals(name)) { Object paramValue = getValue(); return paramValue != null ? paramValue.toString() : ""; } return null; } }; } public void setValue(final String value) { this.value = value; } @Override @Exported public Object getValue() { return value; } @Override public String toString() { return "(JiraIssueParameterValue) " + getName() + "='" + value + "'"; } } ================================================ FILE: src/main/java/hudson/plugins/jira/model/JiraIssue.java ================================================ package hudson.plugins.jira.model; import com.atlassian.jira.rest.client.api.domain.Issue; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.plugins.jira.JiraSite; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; /** * One Jira issue. * This class is used to persist crucial issue information * so that Jenkins can display it without talking to Jira. * * @author Kohsuke Kawaguchi * @see JiraSite#getUrl(JiraIssue) */ @ExportedBean public final class JiraIssue implements Comparable { /** Note these fields have not been renamed for backward compatibility purposes */ private final String id; private final String title; public JiraIssue(String key, String summary) { this.id = key; this.title = summary; } /** * @return Jira ID, like "MNG-1235". */ @Exported public String getKey() { return id; } /** * @return Summary of the issue. For example, in case of MNG-1235, this is "NPE In DiagnosisUtils while using tomcat plugin" */ @Exported public String getSummary() { return title; } public JiraIssue(Issue issue) { this(issue.getKey(), issue.getSummary()); } @Override public int compareTo(@NonNull JiraIssue that) { return this.id.compareTo(that.id); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((id == null) ? 0 : id.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } JiraIssue other = (JiraIssue) obj; if (id == null) { if (other.id != null) { return false; } } else if (!id.equals(other.id)) { return false; } return true; } } ================================================ FILE: src/main/java/hudson/plugins/jira/model/JiraIssueField.java ================================================ package hudson.plugins.jira.model; public class JiraIssueField implements Comparable { private final String fieldId; private final Object fieldValue; public JiraIssueField(String fieldId, Object fieldValue) { this.fieldId = fieldId; this.fieldValue = fieldValue; } @Override public int compareTo(JiraIssueField that) { return this.compareTo(that); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((fieldId == null) ? 0 : fieldId.hashCode()); result = prime * result + ((fieldValue == null) ? 0 : fieldValue.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } JiraIssueField other = (JiraIssueField) obj; if (fieldId == null) { if (other.fieldId != null) { return false; } } else if (!fieldId.equals(other.fieldId)) { return false; } if (fieldValue == null) { if (other.fieldValue != null) { return false; } } else if (!fieldValue.equals(other.fieldValue)) { return false; } return true; } public String getId() { return fieldId; } public Object getValue() { return fieldValue; } } ================================================ FILE: src/main/java/hudson/plugins/jira/model/JiraVersion.java ================================================ package hudson.plugins.jira.model; import com.atlassian.jira.rest.client.api.domain.Version; import hudson.plugins.jira.extension.ExtendedVersion; import java.util.Calendar; public class JiraVersion implements Comparable { private final String name; private String description; private Calendar startDate; private final Calendar releaseDate; private final boolean released; private final boolean archived; public JiraVersion(String name, Calendar releaseDate, boolean released, boolean archived) { this.name = name; this.releaseDate = releaseDate; this.released = released; this.archived = archived; } @Deprecated public JiraVersion(String name, Calendar startDate, Calendar releaseDate, boolean released, boolean archived) { this.name = name; this.startDate = startDate; this.releaseDate = releaseDate; this.released = released; this.archived = archived; } public JiraVersion( String name, String description, Calendar startDate, Calendar releaseDate, boolean released, boolean archived) { this.name = name; this.description = description; this.startDate = startDate; this.releaseDate = releaseDate; this.released = released; this.archived = archived; } public JiraVersion(Version version) { this( version.getName(), version.getReleaseDate() == null ? null : version.getReleaseDate().toGregorianCalendar(), version.isReleased(), version.isArchived()); } public JiraVersion(ExtendedVersion version) { this( version.getName(), version.getDescription(), version.getStartDate() == null ? null : version.getStartDate().toGregorianCalendar(), version.getReleaseDate() == null ? null : version.getReleaseDate().toGregorianCalendar(), version.isReleased(), version.isArchived()); } @Override public int compareTo(JiraVersion that) { int result = this.releaseDate.compareTo(that.releaseDate); if (result == 0) { return this.name.compareTo(that.name); } return result; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (archived ? 1231 : 1237); result = prime * result + ((name == null) ? 0 : name.hashCode()); result = prime * result + ((releaseDate == null) ? 0 : releaseDate.hashCode()); result = prime * result + (released ? 1231 : 1237); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } JiraVersion other = (JiraVersion) obj; if (archived != other.archived) { return false; } if (name == null) { if (other.name != null) { return false; } } else if (!name.equals(other.name)) { return false; } if (startDate == null) { if (other.startDate != null) { return false; } } else if (!startDate.equals(other.startDate)) { return false; } if (releaseDate == null) { if (other.releaseDate != null) { return false; } } else if (!releaseDate.equals(other.releaseDate)) { return false; } if (released != other.released) { return false; } return true; } public String getName() { return name; } public String getDescription() { return description; } public Calendar getStartDate() { return startDate; } public Calendar getReleaseDate() { return releaseDate; } public boolean isReleased() { return released; } public boolean isArchived() { return archived; } } ================================================ FILE: src/main/java/hudson/plugins/jira/pipeline/CommentStep.java ================================================ package hudson.plugins.jira.pipeline; import com.atlassian.jira.rest.client.api.RestClientException; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.jira.JiraSession; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.Messages; import java.util.Collections; import java.util.HashSet; import java.util.Set; import org.jenkinsci.plugins.workflow.steps.Step; import org.jenkinsci.plugins.workflow.steps.StepContext; import org.jenkinsci.plugins.workflow.steps.StepDescriptor; import org.jenkinsci.plugins.workflow.steps.StepExecution; import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; import org.kohsuke.stapler.DataBoundConstructor; /** * Simple add comment step. * * @author jan zajic */ public class CommentStep extends Step { public final String issueKey; public final String body; @DataBoundConstructor public CommentStep(@NonNull String issueKey, @NonNull String body) { this.issueKey = issueKey; this.body = body; } public String getIssueKey() { return issueKey; } public String getBody() { return body; } @Override public StepExecution start(StepContext context) throws Exception { return new CommentStepExecution(this, context); } @Extension(optional = true) public static final class DescriptorImpl extends StepDescriptor { @Override public Set> getRequiredContext() { Set> context = new HashSet<>(); Collections.addAll(context, Run.class, TaskListener.class); return Collections.unmodifiableSet(context); } @Override public String getFunctionName() { return "jiraComment"; } @Override public String getDisplayName() { return Messages.CommentStep_Descriptor_DisplayName(); } } /** * @author jan zajic */ public static class CommentStepExecution extends SynchronousNonBlockingStepExecution { private static final long serialVersionUID = 1L; private final transient CommentStep step; protected CommentStepExecution(CommentStep step, @NonNull StepContext context) { super(context); this.step = step; } @Override protected Void run() throws Exception { JiraSite site = JiraSite.get(getContext().get(Run.class).getParent()); if (site == null) { return null; } JiraSession session = site.getSession(getContext().get(Run.class).getParent()); if (session == null) { getContext().get(TaskListener.class).getLogger().println(Messages.FailedToConnect()); return null; } try { session.addComment(step.issueKey, step.body, site.groupVisibility, site.roleVisibility); } catch (RestClientException e) { getContext().get(TaskListener.class).getLogger().println(e.getMessage()); } return null; } } } ================================================ FILE: src/main/java/hudson/plugins/jira/pipeline/IssueFieldUpdateStep.java ================================================ package hudson.plugins.jira.pipeline; import com.atlassian.jira.rest.client.api.RestClientException; import hudson.EnvVars; import hudson.Extension; import hudson.Util; import hudson.model.AbstractProject; import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.jira.EnvironmentExpander; import hudson.plugins.jira.JiraSession; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.Messages; import hudson.plugins.jira.model.JiraIssueField; import hudson.plugins.jira.selector.AbstractIssueSelector; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.Builder; import hudson.util.FormValidation; import jakarta.servlet.ServletException; import java.io.IOException; import java.io.PrintStream; import java.util.Collections; import java.util.List; import java.util.Set; import jenkins.tasks.SimpleBuildStep; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; /** * Issue custom fields updater * * @author Dmitry Frolov tekillaz.dev@gmail.com * */ public class IssueFieldUpdateStep extends Builder implements SimpleBuildStep { private AbstractIssueSelector issueSelector; public AbstractIssueSelector getIssueSelector() { return this.issueSelector; } @DataBoundSetter public void setIssueSelector(AbstractIssueSelector issueSelector) { this.issueSelector = issueSelector; } public String fieldId; public String getFieldId() { return this.fieldId; } @DataBoundSetter public void setFieldId(String fieldId) { this.fieldId = fieldId; } public String fieldValue; public String getFieldValue() { return this.fieldValue; } @DataBoundSetter public void setFieldValue(String fieldValue) { this.fieldValue = fieldValue; } @DataBoundConstructor public IssueFieldUpdateStep(AbstractIssueSelector issueSelector, String fieldId, String fieldValue) { this.issueSelector = issueSelector; this.fieldId = fieldId; this.fieldValue = fieldValue; } public String prepareFieldId(String fieldId) { String prepared = fieldId; if (!prepared.startsWith("customfield_")) { prepared = "customfield_" + prepared; } return prepared; } @Override public void perform(Run run, EnvVars env, TaskListener listener) throws IOException { PrintStream logger = listener.getLogger(); AbstractIssueSelector selector = issueSelector; if (selector == null) { logger.println("[Jira][IssueFieldUpdateStep] No issue selector found!"); throw new IOException("[Jira][IssueFieldUpdateStep] No issue selector found!"); } JiraSite site = JiraSite.get(run.getParent()); if (site == null) { logger.println(Messages.NoJiraSite()); run.setResult(Result.FAILURE); return; } JiraSession session = site.getSession(run.getParent()); if (session == null) { logger.println(Messages.NoRemoteAccess()); run.setResult(Result.FAILURE); return; } Set issues; try { issues = selector.findIssueIds(run, site, listener); if (issues.isEmpty()) { logger.println("[Jira][IssueFieldUpdateStep] Issue list is empty!"); return; } } catch (RestClientException e) { logger.println(e.getMessage()); return; } List fields = Collections.singletonList(new JiraIssueField( prepareFieldId(getFieldId()), EnvironmentExpander.expandVariable(getFieldValue(), env))); try { for (String issue : issues) { submitFields(session, issue, fields, logger); } } catch (RestClientException e) { logger.println(e.getMessage()); } } @Override public boolean requiresWorkspace() { return false; } /** * @deprecated no reason for this to be exposed/public, use perform(...) instead */ @Deprecated public void submitFields(JiraSession session, String issueId, List fields, PrintStream logger) { try { session.addFields(issueId, fields); } catch (RestClientException e) { if (e.getStatusCode().or(0).equals(404)) { logger.println("[Jira] " + issueId + " - Jira issue not found"); } if (e.getStatusCode().or(0).equals(403)) { logger.println("[Jira] " + issueId + " - Jenkins Jira user does not have permissions to comment on this issue"); } if (e.getStatusCode().or(0).equals(401)) { logger.println("[Jira] " + issueId + " - Jenkins Jira authentication problem"); } logger.println(Messages.FailedToUpdateIssue(issueId)); logger.println(e.getLocalizedMessage()); } } @Override public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } @Extension @Symbol("jiraUpdateIssueField") public static class DescriptorImpl extends BuildStepDescriptor { public FormValidation doCheckField_id(@QueryParameter String value) throws IOException, ServletException { if (Util.fixNull(value).trim().length() == 0) { return FormValidation.warning(Messages.JiraIssueFieldUpdater_NoIssueFieldID()); } if (!value.matches("\\d+")) { return FormValidation.error(Messages.JiraIssueFieldUpdater_NotAtIssueFieldID()); } return FormValidation.ok(); } @Override public boolean isApplicable(Class jobType) { return true; } @Override public String getDisplayName() { return Messages.JiraIssueFieldUpdater_DisplayName(); } } } ================================================ FILE: src/main/java/hudson/plugins/jira/pipeline/IssueSelectorStep.java ================================================ package hudson.plugins.jira.pipeline; import com.atlassian.jira.rest.client.api.RestClientException; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.model.Descriptor; import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.Messages; import hudson.plugins.jira.selector.AbstractIssueSelector; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Optional; import java.util.Set; import jenkins.model.Jenkins; import org.jenkinsci.plugins.workflow.steps.Step; import org.jenkinsci.plugins.workflow.steps.StepContext; import org.jenkinsci.plugins.workflow.steps.StepDescriptor; import org.jenkinsci.plugins.workflow.steps.StepExecution; import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; /** * Step that run selected issue selector. * * @see hudson.plugins.jira.selector.AbstractIssueSelector */ public class IssueSelectorStep extends Step { private AbstractIssueSelector issueSelector; @DataBoundConstructor public IssueSelectorStep() {} @DataBoundSetter public void setIssueSelector(AbstractIssueSelector issueSelector) { this.issueSelector = issueSelector; } public AbstractIssueSelector getIssueSelector() { return issueSelector; } @Override public StepExecution start(StepContext context) throws Exception { return new IssueSelectorStepExecution(this, context); } @Extension(optional = true) public static final class DescriptorImpl extends StepDescriptor { public Collection> getApplicableDescriptors() { return Jenkins.get().getDescriptorList(AbstractIssueSelector.class); } @Override public Set> getRequiredContext() { Set> context = new HashSet<>(); Collections.addAll(context, Run.class, TaskListener.class); return Collections.unmodifiableSet(context); } @Override public String getFunctionName() { return "jiraIssueSelector"; } @Override public String getDisplayName() { return Messages.IssueSelectorStep_Descriptor_DisplayName(); } } public static class IssueSelectorStepExecution extends SynchronousNonBlockingStepExecution> { private static final long serialVersionUID = 1L; private final transient IssueSelectorStep step; protected IssueSelectorStepExecution(IssueSelectorStep step, @NonNull StepContext context) { super(context); this.step = step; } @Override protected Set run() throws Exception { TaskListener listener = getContext().get(TaskListener.class); Run run = getContext().get(Run.class); try { return Optional.ofNullable(JiraSite.get(run.getParent())) .map(site -> step.getIssueSelector().findIssueIds(run, site, listener)) .orElseGet(() -> { listener.getLogger().println(Messages.NoJiraSite()); run.setResult(Result.FAILURE); return new HashSet<>(); }); } catch (RestClientException e) { listener.getLogger().println(e.getMessage()); run.setResult(Result.FAILURE); return new HashSet<>(); } } } } ================================================ FILE: src/main/java/hudson/plugins/jira/pipeline/SearchIssuesStep.java ================================================ package hudson.plugins.jira.pipeline; import com.atlassian.jira.rest.client.api.RestClientException; import com.atlassian.jira.rest.client.api.domain.Issue; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.AbortException; import hudson.Extension; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.jira.JiraSession; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.Messages; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import org.jenkinsci.plugins.workflow.steps.Step; import org.jenkinsci.plugins.workflow.steps.StepContext; import org.jenkinsci.plugins.workflow.steps.StepDescriptor; import org.jenkinsci.plugins.workflow.steps.StepExecution; import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; import org.kohsuke.stapler.DataBoundConstructor; /** * Simple search issues step * * @author jan zajic */ public class SearchIssuesStep extends Step { public final String jql; @DataBoundConstructor public SearchIssuesStep(@NonNull String jql) { this.jql = jql; } public String getJql() { return jql; } @Override public StepExecution start(StepContext context) throws Exception { return new SearchStepExecution(this, context); } @Extension(optional = true) public static final class DescriptorImpl extends StepDescriptor { @Override public Set> getRequiredContext() { Set> context = new HashSet<>(); Collections.addAll(context, Run.class, TaskListener.class); return Collections.unmodifiableSet(context); } @Override public String getFunctionName() { return "jiraSearch"; } @Override public String getDisplayName() { return Messages.SearchIssuesStep_Descriptor_DisplayName(); } } /** * @author jan zajic */ public static class SearchStepExecution extends SynchronousNonBlockingStepExecution> { private static final long serialVersionUID = 1L; private final transient SearchIssuesStep step; protected SearchStepExecution(SearchIssuesStep step, @NonNull StepContext context) { super(context); this.step = step; } @Override protected List run() throws Exception { JiraSite site = JiraSite.get(getContext().get(Run.class).getParent()); JiraSession session = site.getSession(getContext().get(Run.class).getParent()); if (session == null) { getContext().get(TaskListener.class).getLogger().println(Messages.FailedToConnect()); throw new AbortException("Cannot open Jira session - error occurred"); } List resultList = new ArrayList<>(); try { List issuesFromJqlSearch = session.getIssuesFromJqlSearch(step.jql); for (Issue issue : issuesFromJqlSearch) { resultList.add(issue.getKey()); } } catch (RestClientException e) { getContext().get(TaskListener.class).getLogger().println(e.getMessage()); } return resultList; } } } ================================================ FILE: src/main/java/hudson/plugins/jira/selector/AbstractIssueSelector.java ================================================ package hudson.plugins.jira.selector; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.ExtensionPoint; import hudson.model.AbstractDescribableImpl; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.jira.JiraSite; import java.util.Set; /** * Strategy of finding issues which should be updated after completed run. * * @author Franta Mejta */ public abstract class AbstractIssueSelector extends AbstractDescribableImpl implements ExtensionPoint { /** * Finds the strings that match Jira issue ID patterns. * * This method returns all likely candidates and shouldn't check * if such ID actually exists or not. * * @param run The completed run. * @param site Jira site configured for current job. * @param listener Current's run listener. * @return Set of ids of issues which should be updated. */ public abstract Set findIssueIds( @NonNull Run run, @NonNull JiraSite site, @NonNull TaskListener listener); } ================================================ FILE: src/main/java/hudson/plugins/jira/selector/DefaultIssueSelector.java ================================================ package hudson.plugins.jira.selector; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.model.AbstractBuild; import hudson.model.AbstractBuild.DependencyChange; import hudson.model.Descriptor; import hudson.model.ParameterValue; import hudson.model.ParametersAction; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.jira.JiraCarryOverAction; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.Messages; import hudson.plugins.jira.RunScmChangeExtractor; import hudson.plugins.jira.listissuesparameter.JiraIssueParameterValue; import hudson.scm.ChangeLogSet; import hudson.scm.ChangeLogSet.Entry; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; public class DefaultIssueSelector extends AbstractIssueSelector { private static final Logger LOGGER = Logger.getLogger(DefaultIssueSelector.class.getName()); @DataBoundConstructor public DefaultIssueSelector() {} /** * See {@link #addIssuesRecursive(Run, JiraSite, TaskListener, Set)} */ @Override public Set findIssueIds( @NonNull final Run run, @NonNull final JiraSite site, @NonNull final TaskListener listener) { HashSet issuesIds = new LinkedHashSet<>(); addIssuesRecursive(run, site, listener, issuesIds); return issuesIds; } @Extension @Symbol("DefaultSelector") public static final class DescriptorImpl extends Descriptor { @Override public String getDisplayName() { return Messages.DefaultIssueSelector_DisplayName(); } } protected Logger getLogger() { return LOGGER; } /** * Finds the strings that match Jira issue ID patterns. This method returns * all likely candidates and doesn't check if such ID actually exists or * not. We don't want to use {@link JiraSite#existsIssue(String)} here so * that new projects in Jira can be detected. * */ protected static void findIssues(Run build, Set issueIds, Pattern pattern, TaskListener listener) { for (ChangeLogSet set : RunScmChangeExtractor.getChanges(build)) { for (Entry change : set) { LOGGER.fine("Looking for Jira ID in " + change.getMsg()); Matcher m = pattern.matcher(change.getMsg()); while (m.find()) { if (m.groupCount() >= 1) { String content = StringUtils.upperCase(m.group(1)); issueIds.add(content); } else { listener.getLogger() .println("Warning: The Jira pattern " + pattern + " doesn't define a capturing group!"); } } } } } /** * Calls {@link #findIssues(Run, Set, Pattern, TaskListener)} with * {@link JiraSite#getIssuePattern()} as pattern */ protected void addIssuesFromChangeLog(Run build, JiraSite site, TaskListener listener, Set issueIds) { Pattern pattern = site.getIssuePattern(); findIssues(build, issueIds, pattern, listener); } /** * Adds issues to issueIds. Adds issues carried over from previous build, * issues from current build and from dependent builds * {@link #addIssuesCarriedOverFromPreviousBuild(Run, JiraSite, TaskListener, Set)} * {@link #addIssuesFromCurrentBuild(Run, JiraSite, TaskListener, Set)} * {@link #addIssuesFromDependentBuilds(Run, JiraSite, TaskListener, Set)} */ protected void addIssuesRecursive(Run build, JiraSite site, TaskListener listener, Set issuesIds) { addIssuesCarriedOverFromPreviousBuild(build, site, listener, issuesIds); addIssuesFromCurrentBuild(build, site, listener, issuesIds); addIssuesFromDependentBuilds(build, site, listener, issuesIds); } /** * Adds issues to issueIds from the current build. Issues from parameters * are added as well as issues matching pattern * {@link #addIssuesFromChangeLog(Run, JiraSite, TaskListener, Set)} * {@link #addIssuesFromParameters(Run, JiraSite, TaskListener, Set)} */ protected void addIssuesFromCurrentBuild( Run build, JiraSite site, TaskListener listener, Set issueIds) { addIssuesFromChangeLog(build, site, listener, issueIds); addIssuesFromParameters(build, site, listener, issueIds); } /** * Adds issues to issueIds by examining dependency changes from last build. * For each dependency change * {@link #addIssuesRecursive(Run, JiraSite, TaskListener, Set)} is called. */ protected void addIssuesFromDependentBuilds( Run build, JiraSite site, TaskListener listener, Set issueIds) { Pattern pattern = site.getIssuePattern(); for (DependencyChange depc : RunScmChangeExtractor.getDependencyChanges(build).values()) { for (AbstractBuild b : depc.getBuilds()) { getLogger().finer("Searching for Jira issues in dependency " + b + " of " + build); // Fix JENKINS-44989 // The original code before refactoring just called "findIssues", not "findIssueIdsRecursive" findIssues(b, issueIds, pattern, listener); } } } /** * Adds issues to issueIds from parameters */ protected void addIssuesFromParameters( Run build, JiraSite site, TaskListener listener, Set issueIds) { // Now look for any JiraIssueParameterValue's set in the build // Implements JENKINS-12312 ParametersAction parameters = build.getAction(ParametersAction.class); if (parameters != null) { for (ParameterValue val : parameters.getParameters()) { if (val instanceof JiraIssueParameterValue) { String issueId = ((JiraIssueParameterValue) val).getValue().toString(); if (issueIds.add(issueId)) { getLogger().finer("Added perforce issue " + issueId + " from build " + build); } } } } } /** * Adds issues that were carried over from previous build to issueIds */ protected void addIssuesCarriedOverFromPreviousBuild( Run build, JiraSite site, TaskListener listener, Set ids) { Run prev = build.getPreviousCompletedBuild(); if (prev != null) { JiraCarryOverAction a = prev.getAction(JiraCarryOverAction.class); if (a != null) { getLogger().finer("Searching for Jira issues in previously failed build " + prev.number); Collection jobIDs = a.getIDs(); ids.addAll(jobIDs); if (getLogger().isLoggable(Level.FINER)) { for (String jobId : a.getIDs()) { getLogger().finer("Adding job " + jobId); } } } } } } ================================================ FILE: src/main/java/hudson/plugins/jira/selector/ExplicitIssueSelector.java ================================================ package hudson.plugins.jira.selector; import edu.umd.cs.findbugs.annotations.CheckForNull; import hudson.EnvVars; import hudson.Extension; import hudson.model.Descriptor; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.jira.EnvironmentExpander; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.Messages; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; public class ExplicitIssueSelector extends AbstractIssueSelector { @CheckForNull private List jiraIssueKeys; private String issueKeys; @DataBoundConstructor public ExplicitIssueSelector(String issueKeys) { this.jiraIssueKeys = StringUtils.isNotBlank(issueKeys) ? Arrays.asList(issueKeys.split(",")) : Collections.emptyList(); this.issueKeys = issueKeys; } public ExplicitIssueSelector(List jiraIssueKeys) { this.jiraIssueKeys = jiraIssueKeys; } public ExplicitIssueSelector() { this.jiraIssueKeys = Collections.emptyList(); } public void setIssueKeys(String issueKeys) { this.issueKeys = issueKeys; this.jiraIssueKeys = StringUtils.isNotBlank(issueKeys) ? Arrays.asList(issueKeys.split(",")) : new ArrayList<>(); } public String getIssueKeys() { return issueKeys; } @Override public Set findIssueIds(Run run, JiraSite site, TaskListener listener) { EnvVars envVars = EnvironmentExpander.getEnvVars(run, listener); List issueKeys = new ArrayList<>(); for (String issue : jiraIssueKeys) { issueKeys.add(EnvironmentExpander.expandVariable(issue, envVars)); } return new HashSet(issueKeys); } @Extension @Symbol("ExplicitSelector") public static final class DescriptorImpl extends Descriptor { @Override public String getDisplayName() { return Messages.IssueSelector_ExplicitIssueSelector_DisplayName(); } } } ================================================ FILE: src/main/java/hudson/plugins/jira/selector/JqlIssueSelector.java ================================================ package hudson.plugins.jira.selector; import static hudson.Util.fixNull; import com.atlassian.jira.rest.client.api.RestClientException; import com.atlassian.jira.rest.client.api.domain.Issue; import hudson.Extension; import hudson.model.Descriptor; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.jira.EnvironmentExpander; import hudson.plugins.jira.JiraSession; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.Messages; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; public class JqlIssueSelector extends AbstractIssueSelector { private String jql; @DataBoundConstructor public JqlIssueSelector(String jql) { this.jql = jql; } public void setJql(String jql) { this.jql = jql; } public String getJql() { return jql; } @Override public Set findIssueIds(Run run, JiraSite site, TaskListener listener) { try { JiraSession session = site.getSession(run.getParent()); if (session == null) { throw new IllegalStateException("Remote access for Jira isn't configured in Jenkins"); } String expandedJql = EnvironmentExpander.expandVariable(jql, run, listener); List issues = session.getIssuesFromJqlSearch(expandedJql); List issueKeys = new ArrayList<>(); for (Issue issue : fixNull(issues)) { issueKeys.add(issue.getKey()); } // deduplication return new HashSet<>(issueKeys); } catch (RestClientException e) { throw new IllegalStateException("Can't open rest session to Jira site " + site, e); } } @Extension @Symbol("JqlSelector") public static final class DescriptorImpl extends Descriptor { @Override public String getDisplayName() { return Messages.IssueSelector_JqlIssueSelector_DisplayName(); } } } ================================================ FILE: src/main/java/hudson/plugins/jira/selector/perforce/JobIssueSelector.java ================================================ package hudson.plugins.jira.selector.perforce; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.selector.DefaultIssueSelector; import java.util.Set; /** * Base class for job selectors. Perforce offers mechanism to associate Jira * issues with change lists called jobs. The classes inheriting from this class * find issues by examining jobs associated with changes * * @author Jacek Tomaka * @since 2.3 */ public abstract class JobIssueSelector extends DefaultIssueSelector { /** * See {@link #addJobIdsFromChangeLog(Run, JiraSite, TaskListener, Set)} */ @Override protected void addIssuesFromChangeLog(Run build, JiraSite site, TaskListener listener, Set issueIds) { addJobIdsFromChangeLog(build, site, listener, issueIds); } /** * Adds job ids from change log to issueIds. */ protected abstract void addJobIdsFromChangeLog( Run build, JiraSite site, TaskListener listener, Set issueIds); } ================================================ FILE: src/main/java/hudson/plugins/jira/selector/perforce/P4JobIssueSelector.java ================================================ package hudson.plugins.jira.selector.perforce; import com.perforce.p4java.core.IFix; import hudson.Extension; import hudson.model.Descriptor; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.Messages; import hudson.plugins.jira.RunScmChangeExtractor; import hudson.plugins.jira.selector.AbstractIssueSelector; import hudson.scm.ChangeLogSet; import hudson.scm.ChangeLogSet.Entry; import java.util.List; import java.util.Set; import java.util.logging.Logger; import org.jenkinsci.Symbol; import org.jenkinsci.plugins.p4.changes.P4ChangeEntry; import org.kohsuke.stapler.DataBoundConstructor; /** * Job selector for Perforce Software SCM plugin (P4) * * @author Jacek Tomaka * @since 2.3 */ public class P4JobIssueSelector extends JobIssueSelector { private static final Logger LOGGER = Logger.getLogger(P4JobIssueSelector.class.getName()); @Extension(optional = true) @Symbol("P4Selector") public static final class DescriptorImpl extends Descriptor { @Override public String getDisplayName() { return Messages.P4JobIssueSelector_DisplayName(); } } @DataBoundConstructor public P4JobIssueSelector() {} @Override protected void addJobIdsFromChangeLog(Run build, JiraSite site, TaskListener listener, Set issueIds) { getLogger().finer("Searching for Jira issues in Perforce jobs in " + build); for (ChangeLogSet set : RunScmChangeExtractor.getChanges(build)) { for (Entry change : set) { getLogger().fine("Looking for Jira IDs as Perforce Jobs in " + change.getMsg()); if (P4ChangeEntry.class.isAssignableFrom(change.getClass())) { P4ChangeEntry p4ChangeEntry = (P4ChangeEntry) change; List jobs = p4ChangeEntry.getJobs(); if (jobs != null) { for (IFix job : jobs) { String jobId = job.getJobId(); if (issueIds.add(jobId)) { getLogger().finer("Added Perforce job id " + jobId + " from build " + build); } } } } } } } @Override protected Logger getLogger() { return LOGGER; } } ================================================ FILE: src/main/java/hudson/plugins/jira/versionparameter/JiraVersionParameterDefinition.java ================================================ package hudson.plugins.jira.versionparameter; import com.atlassian.jira.rest.client.api.RestClientException; import com.atlassian.jira.rest.client.api.domain.Version; import hudson.Extension; import hudson.cli.CLICommand; import hudson.model.Item; import hudson.model.Job; import hudson.model.ParameterDefinition; import hudson.model.ParameterValue; import hudson.model.ParametersDefinitionProperty; import hudson.plugins.jira.JiraSession; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.Messages; import hudson.util.ListBoxModel; import java.io.IOException; import java.util.List; import java.util.Objects; import java.util.regex.Pattern; import java.util.stream.Collectors; import net.sf.json.JSONObject; import org.jenkinsci.Symbol; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest2; import org.kohsuke.stapler.interceptor.RequirePOST; public class JiraVersionParameterDefinition extends ParameterDefinition { private static final long serialVersionUID = 4232979892748310160L; private String projectKey; private boolean showReleased = false; private boolean showArchived = false; private boolean showUnreleased = false; private Pattern pattern = null; @DataBoundConstructor public JiraVersionParameterDefinition( String name, String description, String jiraProjectKey, String jiraReleasePattern, String jiraShowReleased, String jiraShowArchived, String jiraShowUnreleased) { super(name); setDescription(description); setJiraProjectKey(jiraProjectKey); setJiraReleasePattern(jiraReleasePattern); setJiraShowReleased(jiraShowReleased); setJiraShowArchived(jiraShowArchived); setShowUnreleased(jiraShowUnreleased); } @Override public ParameterValue createValue(StaplerRequest2 req) { String[] values = req.getParameterValues(getName()); if (values == null || values.length != 1 || values[0].isEmpty()) { return null; } return new JiraVersionParameterValue(getName(), values[0]); } @Override public ParameterValue createValue(StaplerRequest2 req, JSONObject formData) { JiraVersionParameterValue value = req.bindJSON(JiraVersionParameterValue.class, formData); return value; } @Override public ParameterValue createValue(CLICommand command, String value) throws IOException, InterruptedException { return new JiraVersionParameterValue(getName(), value); } public List getVersions() throws IOException, RestClientException { Job contextJob = Stapler.getCurrentRequest2().findAncestorObject(Job.class); return getVersions(contextJob); } List getVersions(Job contextJob) { JiraSite site = JiraSite.get(contextJob); if (site == null) { throw new IllegalStateException( "Jira site needs to be configured in the project " + contextJob.getFullDisplayName()); } JiraSession session = site.getSession(contextJob); if (session == null) { throw new IllegalStateException("Remote access for Jira isn't configured in Jenkins"); } return session.getVersions(projectKey).stream() .sorted(VersionComparator.INSTANCE) .filter(this::match) .map(Result::new) .collect(Collectors.toList()); } private boolean match(Version version) { // Match regex if it exists if (pattern != null) { if (!pattern.matcher(version.getName()).matches()) { return false; } } boolean isReleased = version.isReleased(); boolean isArchived = version.isArchived(); boolean showAllVersions = !showReleased && !showUnreleased && !showArchived; if (showAllVersions) { return true; } if (showReleased && isReleased && !isArchived) { return true; } if (showArchived && isArchived) { return true; } if (showUnreleased && !isReleased && !isArchived) { return true; } return false; } public String getJiraReleasePattern() { if (pattern == null) { return ""; } return pattern.pattern(); } public void setJiraReleasePattern(String pattern) { if (pattern == null || pattern.isEmpty()) { this.pattern = null; } else { this.pattern = Pattern.compile(pattern); } } public String getJiraProjectKey() { return projectKey; } public void setJiraProjectKey(String projectKey) { this.projectKey = projectKey; } public String getJiraShowReleased() { return Boolean.toString(showReleased); } public void setJiraShowReleased(String showReleased) { this.showReleased = Boolean.parseBoolean(showReleased); } public String getJiraShowArchived() { return Boolean.toString(showArchived); } public void setJiraShowArchived(String showArchived) { this.showArchived = Boolean.parseBoolean(showArchived); } public String getJiraShowUnreleased() { return Boolean.toString(showUnreleased); } public void setShowUnreleased(String jiraShowUnreleased) { this.showUnreleased = Boolean.parseBoolean(jiraShowUnreleased); } @Extension @Symbol("jiraReleaseVersion") public static class DescriptorImpl extends ParameterDescriptor { @Override public String getDisplayName() { return "Jira Release Version Parameter"; } @RequirePOST public ListBoxModel doFillVersionItems(@AncestorInPath Job job, @QueryParameter String name) { ListBoxModel items = new ListBoxModel(); if (job.hasPermission(Item.BUILD)) { ParametersDefinitionProperty prop = job.getProperty(ParametersDefinitionProperty.class); if (prop != null) { ParameterDefinition def = prop.getParameterDefinition(name); if (def instanceof JiraVersionParameterDefinition jiraVersionDef) { List issueValues = jiraVersionDef.getVersions(job); issueValues.forEach(it -> items.add(it.name)); } } } if (items.isEmpty()) { items.add(Messages.JiraVersionParameterDefinition_NoVersionsMatchedSearch(), ""); } return items; } } public static class Result { public final String name; public final Long id; public Result(final Version version) { this.name = version.getName(); this.id = version.getId(); } @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; Result result = (Result) o; return Objects.equals(name, result.name) && Objects.equals(id, result.id); } @Override public int hashCode() { return Objects.hash(name, id); } } } ================================================ FILE: src/main/java/hudson/plugins/jira/versionparameter/JiraVersionParameterValue.java ================================================ package hudson.plugins.jira.versionparameter; import hudson.EnvVars; import hudson.model.AbstractBuild; import hudson.model.ParameterValue; import hudson.model.Run; import hudson.util.VariableResolver; import org.kohsuke.stapler.DataBoundConstructor; public class JiraVersionParameterValue extends ParameterValue { /** * */ private static final long serialVersionUID = 7715888375360839484L; private String version; @DataBoundConstructor public JiraVersionParameterValue(final String name, final String version) { super(name); if (version == null) { throw new IllegalArgumentException("Version cannot be null"); } this.version = version; } @Override public void buildEnvironment(final Run run, final EnvVars env) { env.put(getName(), getVersion()); } @Override public VariableResolver createVariableResolver(final AbstractBuild build) { return name -> JiraVersionParameterValue.this.name.equals(name) ? getVersion() : null; } public void setVersion(final String version) { this.version = version; } public String getVersion() { return version; } @Override public Object getValue() { return getVersion(); } @Override public String toString() { return "(JiraVersionParameterValue) " + getName() + "='" + version + "'"; } } ================================================ FILE: src/main/java/hudson/plugins/jira/versionparameter/VersionComparator.java ================================================ package hudson.plugins.jira.versionparameter; import com.atlassian.jira.rest.client.api.domain.Version; import java.util.Comparator; import org.apache.maven.artifact.versioning.ComparableVersion; /** * This comparator can ordering the following formats versions: * 9.9.9.9.9 * V-5.2.3 * PDFREPORT-2.3.4 * PDFREPORT-2.3 * 1.12.2.3.4 * 1.3.4 * 1.1.1.2 * 1.1.1.1 */ public class VersionComparator implements Comparator { public static final VersionComparator INSTANCE = new VersionComparator(); @Override public int compare(Version rev1, Version rev2) { ComparableVersion comparableVersion1 = new ComparableVersion(rev1.getName()); ComparableVersion comparableVersion2 = new ComparableVersion(rev2.getName()); int comparisonResult = comparableVersion2.compareTo(comparableVersion1); if (comparisonResult > 0) { return 1; } else if (comparisonResult < 0) { return -1; } else return 0; } } ================================================ FILE: src/main/resources/atlassian-httpclient-plugin-0.23.0.pom ================================================ 4.0.0 com.atlassian.httpclient atlassian-httpclient-parent 0.23.0 atlassian-httpclient-plugin atlassian-plugin Atlassian HTTP Client, Apache HTTP components impl Implementation of the HTTP Client API based on Apache HTTP Components Apache License 2.0 http://www.apache.org/licenses/LICENSE-2.0 repo 0.19 9000 com.atlassian.httpclient atlassian-httpclient-api ${project.version} com.atlassian.sal sal-api provided true com.atlassian.event atlassian-event provided true com.atlassian.analytics analytics-api provided org.slf4j slf4j-api provided org.springframework spring-context provided com.atlassian.plugins atlassian-plugins-osgi ${atlassian-plugins.version} provided javax.servlet servlet-api provided com.atlassian.fugue fugue provided com.atlassian.bundles jsr305 provided org.apache.httpcomponents httpasyncclient-cache ${httpasyncclient.version} org.apache.httpcomponents httpclient-cache ${httpcore.version} commons-logging commons-logging org.apache.httpcomponents httpasyncclient ${httpasyncclient.version} commons-logging commons-logging org.apache.httpcomponents httpmime ${httpclient.version} commons-logging commons-logging com.atlassian.junit atlassian-junit 0.1 test org.eclipse.jetty jetty-server org.eclipse.jetty jetty-servlet org.eclipse.jetty jetty-server 7.6.8.v20121106 test org.eclipse.jetty jetty-servlet 7.6.8.v20121106 test org.hamcrest hamcrest-core test org.hamcrest hamcrest-library test com.github.stefanbirkner system-rules 1.5.0 test com.fasterxml.jackson.core jackson-annotations 2.5.2 test com.fasterxml.jackson.core jackson-core 2.5.2 test com.fasterxml.jackson.core jackson-databind 2.5.2 test com.atlassian.maven.plugins maven-amps-plugin true com.atlassian.sal.api*;version="${sal.version}", com.atlassian.util.concurrent*;version="${atlassian-util-concurrent-api.version}", com.google.common*;version="${guava.version}", javax.net.ssl, javax.servlet.*, org.osgi*, org.slf4j*;version="1.5", org.springframework*, org.xml.sax*, com.atlassian.fugue*, *;resolution:=optional com.atlassian.httpclient.api*;version="${httpclient.api.version}", com.atlassian.fugue* * ${http.port} ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraBuildAction/summary.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraCreateIssueNotifier/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraCreateIssueNotifier/help-actionIdOnSuccess.html ================================================
Optional
The Jira issue status will transition with the configured workflow action id when the build status returns success, for no transition set '0'.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraCreateIssueNotifier/help-assignee.html ================================================
Optional
This will assign the issue to the given assignee (Jira username).
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraCreateIssueNotifier/help-component.html ================================================
Optional
Component names that the issue should be assigned to, separated by comma, e.g.: jira-component,jenkins-integration.
You can find the components in Jira under Projects tab -> <PROJECT> -> Components.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraCreateIssueNotifier/help-priorityId.html ================================================
Optional
This will be the priority of the Jira issue. For project default leave empty.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraCreateIssueNotifier/help-projectKey.html ================================================
Required
Specify the projectkey in Uppercase, e.g.: OJRA.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraCreateIssueNotifier/help-testDescription.html ================================================
Optional
This will be the description placed in the Jira issue.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraCreateIssueNotifier/help-typeId.html ================================================
Optional
This will be the type of the Jira issue, defaults to Bug.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraCreateReleaseNotes/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraCreateReleaseNotes/help-jiraEnvironmentVariable.html ================================================

Specify the environment variable to which the release notes will be stored, defaults to RELEASE_NOTES.

This can be used in another build step which supports environments.

================================================ FILE: src/main/resources/hudson/plugins/jira/JiraCreateReleaseNotes/help-jiraFilter.html ================================================

Apply additional filtering criteria to the issue filter. This will be concatenated with an AND operator.

Defaults To:

status in (Released, Closed)
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraCreateReleaseNotes/help-jiraProjectKey.html ================================================

Specify Jira project key. A project key is the all capitals part before the issue number in Jira.

(EXAMPLE-100)

================================================ FILE: src/main/resources/hudson/plugins/jira/JiraCreateReleaseNotes/help-jiraRelease.html ================================================

Specify the name of the parameter which will contain the release version. This can reference a build parameter.

================================================ FILE: src/main/resources/hudson/plugins/jira/JiraEnvironmentVariableBuilder/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraEnvironmentVariableBuilder/help.html ================================================
Extracts Jira information for the build to environment variables.
Available variables:
  • JIRA_ISSUES - A comma separated list of issues which are referenced in the version control system changelog
  • JIRA_ISSUES_SIZE - Size of the list described above
  • JIRA_URL - Primary URL for the Jira server

Typical usage:

  1. Add this build step
  2. Use the "Progress Jira issues by workflow action" or "Move issues matching JQL to the specified version" with JQL like:
    issue in (${JIRA_ISSUES})

================================================ FILE: src/main/resources/hudson/plugins/jira/JiraFolderProperty/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraGlobalConfiguration/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraIssueMigrator/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraIssueMigrator/help-addRelease.html ================================================

Add Jira Release instead of replace. If checked, Replace Jira Release field will be ignored.

================================================ FILE: src/main/resources/hudson/plugins/jira/JiraIssueMigrator/help-jiraProjectKey.html ================================================

Specify the project key. A project key is the all capitals part before the issue number in Jira.

(EXAMPLE-100)

================================================ FILE: src/main/resources/hudson/plugins/jira/JiraIssueMigrator/help-jiraQuery.html ================================================

Issues which match this JQL Query will be moved to this release version.

This can contain $PARAM values which will be replaced by the build parameters.

Example:

project = PROJECT and fixVersion = "$RELEASE_VERSION" and status not in (Resolved, Closed)
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraIssueMigrator/help-jiraRelease.html ================================================

Specify the name of the parameter which will contain the release version. This can reference a build parameter.

================================================ FILE: src/main/resources/hudson/plugins/jira/JiraIssueMigrator/help-jiraReplaceVersion.html ================================================
Optional

If a a value is provided, then only this version will be replaced instead of all versions. This is useful if you group issues using fixVersions and want to keep these extra versions during the migration. If the value is blank, then all fixVersions will be replaced with the Release Version.
If you want to replace versions matching a pattern you can use regular expressions by surrounding the string with slashes, e.g. /.*SNAPSHOT$/

================================================ FILE: src/main/resources/hudson/plugins/jira/JiraIssueUpdateBuilder/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraIssueUpdateBuilder/help-comment.html ================================================
An optional comment to be added to the issue after updating the workflow. If left empty, no comment will be added.

This can contain $PARAM values which will be replaced by the build parameters.

================================================ FILE: src/main/resources/hudson/plugins/jira/JiraIssueUpdateBuilder/help-jqlSearch.html ================================================
Issues which match this JQL Query will be progressed using the specified workflow action.

This can contain $PARAM values which will be replaced by the build parameters.

Example:

project = JENKINS and fixVersion = "$RELEASE_VERSION" and status not in (Resolved, Closed)
or (e.g., combined with a Jira Issue Parameter, selecting one issue from a JQL result set):
issue = $ISSUE_ID

================================================ FILE: src/main/resources/hudson/plugins/jira/JiraIssueUpdateBuilder/help-workflowActionName.html ================================================
The workflow action to be performed on the selected Jira issues.

Be mindful of the issues being selected by the JQL query, because not all actions are valid for all issue statuses.

NOTE: the Jenkins user must have access to perform the workflow step, as if the user were logged in and viewing the issue in a web browser.

================================================ FILE: src/main/resources/hudson/plugins/jira/JiraIssueUpdateBuilder/help.html ================================================
Performs a Jira workflow action for every issue that matches the JQL query. A common use might be to consider a ticket "confirmed" in the last build step of a job, or to mark an issue as "merged" if the job is used to merge changes from one SCM repository to another.

Optionally, include a comment that will be attached to those tickets that are modified as a result of this build step.

================================================ FILE: src/main/resources/hudson/plugins/jira/JiraIssueUpdater/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraProjectProperty/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraReleaseVersionUpdater/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraReleaseVersionUpdater/help-jiraDescription.html ================================================
Optional
Specify a description of the release version. This can reference a build parameter.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraReleaseVersionUpdater/help-jiraProjectKey.html ================================================

Specify the project key. A project key is the all capitals part before the issue number in Jira.

(EXAMPLE-100)

================================================ FILE: src/main/resources/hudson/plugins/jira/JiraReleaseVersionUpdater/help-jiraRelease.html ================================================
Specify the name of the parameter which will contain the release version. This can reference a build parameter.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraReleaseVersionUpdaterBuilder/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraReleaseVersionUpdaterBuilder/help-jiraDescription.html ================================================
Optional
Specify a description of the release version. This can reference a build parameter.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraReleaseVersionUpdaterBuilder/help-jiraProjectKey.html ================================================

Specify the project key. A project key is the all capitals part before the issue number in Jira.

(EXAMPLE-100)

================================================ FILE: src/main/resources/hudson/plugins/jira/JiraReleaseVersionUpdaterBuilder/help-jiraRelease.html ================================================
Specify the name of the parameter which will contain the release version. This can reference a build parameter.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/config.jelly ================================================
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/config.properties ================================================ site.alternativeUrl=Jira alternative URL site.timeout=in seconds site.useBearerAuth=Note: Bearer authentication is only supported in Jira Server, for Jira Cloud leave this unchecked ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/config_de.properties ================================================ Jira\ sites=Jira Instanzen Supports\ Wiki\ notation=Untersttzt Wiki-Notation ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/config_fr.properties ================================================ Jira\ sites=Sites Jira Supports\ Wiki\ notation= Support des annotations wiki. Record\ Scm\ changes= Enregistrement des changements du scm. ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/config_it.properties ================================================ site.alternativeUrl=URL alternativo di Jira site.timeout=in secondi site.useBearerAuth=Nota: L'autenticazione del portatore \u00e8 supportata solo in Jira Server, perch\u00e9 Jira Cloud lascia questo non controllato ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/config_ja.properties ================================================ Jira\ sites=Jira \u30B5\u30A4\u30C8 Supports\ Wiki\ notation=Wiki\u8A18\u6CD5\u306E\u30B5\u30DD\u30FC\u30C8 Record\ Scm\ changes=SCM\u306E\u5909\u66F4\u3092\u8A18\u9332 Issue\ Pattern=\u8AB2\u984CID\u30D1\u30BF\u30FC\u30F3 Update\ Jira\ Issues\ For\ All\ Build\ Results=\u3059\u3079\u3066\u306E\u30D3\u30EB\u30C9\u7D50\u679C\u306B\u3064\u3044\u3066Jira\u306E\u8AB2\u984C\u3092\u66F4\u65B0 ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/config_nl.properties ================================================ # The MIT License # # Copyright (c) 2004-2010, Sun Microsystems, Inc. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. Jira\ sites=Jira-sites Record\ Scm\ changes=Houd SCM-veranderingen bij Supports\ Wiki\ notation=Ondersteunt Wiki-notatie ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/config_pl.properties ================================================ site.alternativeUrl=Alternatywny Jira URL site.timeout=w sekundach site.useBearerAuth=Uwaga: uwierzytelnianie Bearer jest obs\u0142ugiwane tylko dla Jira Server. Je\u015bli u\u017cywasz Jira Cloud pozostaw to pole niezaznaczone ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-alternativeUrl.html ================================================
Specify the root URL of your Jira installation for "normal" access, like https://issues.apache.org/jira/.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-appendChangeTimestamp.html ================================================
If activated, SCM change entries date and time will be recorded in Jira.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-credentialsId.html ================================================
If the remote API support is enabled in this Jira, you can set a valid account information. This would allow projects to update their issues whenever builds integrate those changes, by using this configured account. Each project can choose whether to actually update Jira via a selection in the publish section.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-credentialsId_de.html ================================================
Falls die Remote-API Unterstützung in Jira aktiviert ist, kann man hier die Login-Informationen angeben. Damit können Jenkins-Projekte ihre zugehörigen Issues aktualisieren, falls Änderungen für sie in einen Build integriert wurden. Ob die Aktualisierung tatsächlich passiert, kann per Projekt konfiguriert werden.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-credentialsId_fr.html ================================================
Si l'api "remote" est activé dans Jira, vous pouvez configurer un compte valide. Celà permettra aux projets de mettre à jour les entrées Jira avec les "builds" Jenkins intégrant le changement en utilisant le compte configuré Chaque projet peut choisir de mettre à jour ou pas avec une sélection dans la partie "publisher"
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-credentialsId_ja.html ================================================
JiraでリモートAPIがサポートされている場合、正しいアカウント情報を設定します。 設定すると、ビルドが変更を統合した際にissueを更新します。 また、プロジェクトの設定画面で、どのJiraを更新するか選択することができます。
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-dateTimePattern.html ================================================
See javadoc for SimpleDateFormat for help. If not set, DateFormat.SHORT for the current locale will be used.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-disableChangelogAnnotations.html ================================================
Disable creating Jira hyperlinks in the changeset.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-groupVisibility.html ================================================
Enter the name of the Jira group that has permission to view the comment, leave the field empty to make the comment available to all Jira users.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-maxIssuesFromJqlSearch.html ================================================
Specifies the maximum number of issues to load from the JQL search query. If this number exceeds the limit configured in Jira, only the maximum allowed by Jira will be retrieved.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-readTimeout.html ================================================
Read timeout for Jira REST API calls (in seconds).
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-recordScmChanges.html ================================================
If activated, scm changes will be recorded in Jira : link to the scm repository browser and paths changes.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-recordScmChanges_fr.html ================================================
Si activé, les changements du scm seront enregistrés dans Jira : avec un lien vers le "browser" de scm et les fichiers changé.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-recordScmChanges_ja.html ================================================
SCMリポジトリブラウザへのリンクやパスの変更が、SCMが変更されるとJiraに記録されます。
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-roleVisibility.html ================================================
Enter the name of the Jira project role that has permission to view the comment, leave the field empty to make the comment available to all Jira users.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-supportsWikiStyleComment.html ================================================
Check this box if this Jira supports Wiki notations in comments. When checked, Jenkins will post comments that take advantage of the Wiki notation. If left unchecked, Jenkins will only post plain-text comments.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-supportsWikiStyleComment_de.html ================================================
Aktivieren Sie diese Checkbox, falls die Jira Instanz Wiki-Notation unterstützt. Falls aktiv, wird Jenkins Kommentare in Wiki-Notation senden. Andernfalls werden einfache Textkommentare gesendet.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-supportsWikiStyleComment_fr.html ================================================
Cochez si votre Jira supporte la syntaxe WIKI dans les commentaires. Si activé, Jenkins postera des commentaires qui utiliseront cette syntaxe. Si non activé, Jenkins postera des commentaires au format texte.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-supportsWikiStyleComment_ja.html ================================================
このJiraがコメントの記述にWiki記法をサポートしているなら、このチェックボックスをチェックしてください。 チェックすると、JenkinsはWiki記法を使用してコメントをポストします。 チェックしないと、Jenkinsはプレーンテキストのままコメントをポストします。 
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-threadExecutorNumber.html ================================================
Size of the Thread Pool Executor to query Jira. If you have a lot of builds using the Jira plugin, it might be a good idea to have a minimum size of 10.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-timeout.html ================================================
Connection timeout for Jira REST API calls (in seconds).
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-updateJiraIssueForAllStatus.html ================================================

If this is unchecked, issues will be only updated if the build is SUCCESSful or UNSTABLE.

If this is checked, related Jira issues will be always updated, regardless of the build result.

================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-url.html ================================================
Specify the root URL of your Jira installation, like http://issues.apache.org/jira/.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-url_de.html ================================================
Geben Sie die Root-URL der Jira Instanz an, z.B. http://issues.apache.org/jira/
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-url_fr.html ================================================
URL racine de votre installation Jira, exemple : http://issues.apache.org/jira/
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-url_ja.html ================================================
http://issues.apache.org/jira/ のように、JiraのルートURLを指定してください。
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-useHTTPAuth.html ================================================
This option forces Jenkins to connect to Jira using HTTP Basic Authentication, instead of logging in over RPC.

It is not clear why someone would prefer this over normal login, and indeed getIssueWithFixVersion and JQL search is known to break with this setting. Therefore I recommend not enabling this unless you have specific reasons to prefer this (and please let us know why you want this so that we can fix this documentation).

================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-userPattern.html ================================================
You can define your own pattern to search for Jira issue ids in the SCM logs.
If empty the default one is used : ([a-zA-Z][a-zA-Z0-9_]+-[1-9][0-9]*)([^.]|\.[^0-9]|\.$|$)
Note that the pattern must contain one matching group (on index 1) which should match the actual Jira id.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-userPattern_de.html ================================================
Eigenes Suchmuster (regulärer Ausdruck) zum Erkennen von Jira IssueS definieren.
Falls leer, wird das Standardmuster benutzt: ([a-zA-Z][a-zA-Z0-9_]+-[1-9][0-9]*)([^.]|\.[^0-9]|\.$|$)
Bitte beachten, dass das Muster eine Matching-Gruppe (mit Index 1) enthalten muss, welche die Jira Id enthalten muss!
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-userPattern_fr.html ================================================
Vous pouvez définir ici votre pattern pour rechercher les Ids Jira dans les commentaires du commit dans le scm. Si vide celui par défaut est utilisé : ([a-zA-Z][a-zA-Z0-9_]+-[1-9][0-9]*)([^.]|\.[^0-9]|\.$|$)
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraSite/help-userPattern_ja.html ================================================
SCMのログからJiraの課題IDを検索するパターンを定義することができます。 設定しないとデフォルトのパターン ([a-zA-Z][a-zA-Z0-9_]+-[1-9][0-9]*)([^.]|\.[^0-9]|\.$|$) が使用されます。
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraVersionCreator/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraVersionCreator/help-failIfAlreadyExists.html ================================================
When checked (default), the build will fail if the version already exists in Jira. When unchecked, the build will continue successfully even if the version already exists.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraVersionCreator/help-jiraProjectKey.html ================================================

Specify the project key. A project key is the all capitals part before the issue number in Jira.

(EXAMPLE-100)

================================================ FILE: src/main/resources/hudson/plugins/jira/JiraVersionCreator/help-jiraVersion.html ================================================
Specify the name of the parameter which will contain the release version. This can reference a build parameter.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraVersionCreatorBuilder/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/JiraVersionCreatorBuilder/help-failIfAlreadyExists.html ================================================
When checked (default), the build will fail if the version already exists in Jira. When unchecked, the build will continue successfully even if the version already exists.
================================================ FILE: src/main/resources/hudson/plugins/jira/JiraVersionCreatorBuilder/help-jiraProjectKey.html ================================================

Specify the project key. A project key is the all capitals part before the issue number in Jira.

(EXAMPLE-100)

================================================ FILE: src/main/resources/hudson/plugins/jira/JiraVersionCreatorBuilder/help-jiraVersion.html ================================================
Specify the name of the parameter which will contain the release version. This can reference a build parameter.
================================================ FILE: src/main/resources/hudson/plugins/jira/MavenJiraIssueUpdater/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/Messages.properties ================================================ JiraIssueUpdater.DisplayName=Jira: Update relevant issues JiraFolderProperty.DisplayName=Associated Jira JiraProjectProperty.DisplayName=Associated Jira JiraProjectProperty.JiraUrlMandatory=Jira URL is a mandatory field JiraProjectProperty.NotAJiraUrl=This is a valid URL but it doesn''t look like Jira JiraProjectProperty.NoWsdlAvailable=This looks like a Jira instance but no WSDL is available. Perhaps SOAP support is not enabled yet? JiraBuildAction.DisplayName=Jira issues DefaultIssueSelector.DisplayName=Default selector PerforceJobIssueSelector.DisplayName=Perforce job selector P4JobIssueSelector.DisplayName=Perforce Software (P4) job selector JiraReleaseVersionBuilder.DisplayName=Jira: Mark a version as Released JiraReleaseVersionMigrator.DisplayName=Jira: Move issues matching JQL to the specified version JiraIssueUpdateBuilder.DisplayName=Jira: Progress issues by workflow action JiraIssueUpdateBuilder.NoJqlSearch=Please set the JQL used to select the issues to update. JiraIssueUpdateBuilder.NoWorkflowAction=A workflow action is required. JiraIssueUpdateBuilder.UpdatingWithAction=[Jira] Updating issues using workflow action {0}. JiraIssueUpdateBuilder.Failed=[Jira] An error occurred while progressing issues: JiraIssueUpdateBuilder.UnknownWorkflowAction=[Jira] Unable to update issue {0}: invalid workflow action "{1}". Perhaps the Jenkins user does not have permission to perform the action on the Jira issue? JiraIssueUpdateBuilder.SomeIssuesFailed=[Jira] At least one issue failed to update. See log above for more details. JiraVersionCreator.DisplayName=Jira: Create new version VersionReleaser.AlreadyReleased=[Jira] The version {0} is already released in project {1}, so nothing to do. VersionReleaser.MarkingReleased=[Jira] Marking version {0} in project {1} as released. JiraVersionCreator.VersionExists=[Jira] A version with name {0} already exists in project {1}, so nothing to do. JiraVersionCreator.CreatingVersion=[Jira] Creating version {0} in project {1}. JiraEnvironmentVariableBuilder.DisplayName=Jira: Add related environment variables to build JiraEnvironmentVariableBuilder.NoJiraSite=[Jira] No Jira site is configured for this project. This must be a project configuration error JiraEnvironmentVariableBuilder.Updating=[Jira] Setting {0} to {1}. CommentStep.Descriptor.DisplayName=Jira: Add a comment to issue(s) SearchIssuesStep.Descriptor.DisplayName=Jira: Search issues IssueSelectorStep.Descriptor.DisplayName=Jira: Issue selector JiraCreateIssueNotifier.DisplayName=Jira: Create issue IssueSelector.ExplicitIssueSelector.DisplayName=Explicit selector IssueSelector.JqlIssueSelector.DisplayName=JQL selector JiraVersionCreatorBuilder.DisplayName=Jira: Create new version JiraIssueFieldUpdater.DisplayName=Jira: Issue custom field updater JiraIssueFieldUpdater.NoIssueFieldID=Issue field ID required JiraIssueFieldUpdater.NotAtIssueFieldID=Not an issue field ID IssueFieldUpdaterStep.UpdatingIssue=[Jira][IssueFieldUpdaterStep] Updating issue {0} FailedToUpdateIssue=[Jira] Failed to update issue {0}. FailedToUpdateIssueWithCarryOver=[Jira] Failed to update issue {0}. Carrying over to next build. UpdatingIssue=[Jira] Updating issue {0} FailedToConnect=[Jira] Failed to connect to Jira NoJenkinsUrl=[Jira] Jenkins URL is not configured yet. Go to system configuration to set this value NoJiraSite=[Jira] No Jira site is configured for this project. This must be a project configuration error NoRemoteAccess=[Jira] The system configuration does not allow remote Jira access ErrorCommentingIssues=[Jira] Could not comment on some issues: {0} JiraSite.threadExecutorMinimunSize = Thread Executor Size must be at least {0} (higher values are recommended) JiraSite.timeoutMinimunValue = Connection timeout must be at least {0} JiraSite.readTimeoutMinimunValue = Read timeout must be at least {0} JiraIssueParameterDefinition.NoIssueMatchedSearch = No issues matched the search JiraVersionParameterDefinition.NoVersionsMatchedSearch = No version matched the search ================================================ FILE: src/main/resources/hudson/plugins/jira/Messages_de.properties ================================================ JiraProjectProperty.DisplayName=Zugeh\u00f6rige Jira-Instanz JiraProjectProperty.JiraUrlMandatory=Jira URL ist ein Pflichtfeld. JiraProjectProperty.NotAJiraUrl=Dies ist eine g\u00f6ltige URL, aber anscheinend keine Jira URL. JiraBuildAction.DisplayName=Jira Issues FailedToConnect=Verbindung zu Jira fehlgeschlagen. NoJenkinsUrl=Jenkins URL ist noch nicht konfiguriert. Bitte geben Sie sie in der Systemkonfiguration ein. NoJiraSite=Keine Jira Instanz konfiguriert f\u00fcr dieses Projekt. NoRemoteAccess=Die Jira Konfiguration erlaubt keinen Remote-Zugriff auf Jira. ErrorCommentingIssues=Konnte einige Vorg\a00e4nge nicht kommentieren: {0} ================================================ FILE: src/main/resources/hudson/plugins/jira/Messages_fr.properties ================================================ JiraIssueUpdater.DisplayName=Jira : Mise \u00e0 jour des demandes JiraProjectProperty.DisplayName=Jira associ\u00e9 JiraProjectProperty.JiraUrlMandatory=L'URL Jira est un champ obligatoire JiraProjectProperty.NotAJiraUrl=C''est une URL valide mais cel\u00e0 n'a pas l'air d'\u00eatre une URL Jira JiraProjectProperty.NoWsdlAvailable=Ceci ressemble \u00e0 une instance Jira mais WSDL n''est pas disponible. Peut \u00eatre que le support SOAP n''est pas actif ? JiraBuildAction.DisplayName=Demandes Jira DefaultIssueSelector.DisplayName=S\u00e9lecteur par d\u00e9faut PerforceJobIssueSelector.DisplayName=S\u00e9lecteur Perforce job P4JobIssueSelector.DisplayName=S\u00e9lecteur Perforce Software (P4) job JiraReleaseVersionBuilder.DisplayName=Jira : Marquer une version r\u00e9leas\u00e9e JiraReleaseVersionMigrator.DisplayName=Jira : D\u00e9placer les demandes correspondant au JQL vers la version sp\u00e9cifi\u00e9e JiraIssueUpdateBuilder.DisplayName=Jira : Appliquer une transition de flux de travaux aux demandes. JiraIssueUpdateBuilder.NoJqlSearch=Merci de sp\u00e9cifier la JQL \u00e0 utiliser pour s\u00e9lectionner les demandes \u00e0 mettre \u00e0 jour. JiraIssueUpdateBuilder.NoWorkflowAction=Une action du flux de travaux est requise. JiraIssueUpdateBuilder.UpdatingWithAction=[Jira] Mise \u00e0 jour des demandes suivant l''action du flux de travaux {0}. JiraIssueUpdateBuilder.Failed=[Jira] Une erreur est survenue durant la mise \u00e0 jour des demandes : JiraIssueUpdateBuilder.UnknownWorkflowAction=[Jira] Impossible de trouver la demande {0} : "{1}" n''est pas une action de flux de travaux valide. Jenkins n''a peut \u00eatre pas le droit d''ex\u00e9cuter cette action sur cette demande ? JiraIssueUpdateBuilder.SomeIssuesFailed=[Jira] Au moins une demande n''a pas \u00e9t\u00e9 mise \u00e0 jour. Consultez les logs pour plus de d\u00e9tails. JiraVersionCreator.DisplayName=Jira : Cr\u00e9er une nouvelle version VersionReleaser.AlreadyReleased=[Jira] La version {0} est d\u00e9j\u00e0 releas\u00e9e dans le projet {1}, il n''y a donc rien \u00e0 faire. VersionReleaser.MarkingReleased=[Jira] La version {0} du projet {1} a \u00e9t\u00e9 marqu\u00e9e comme releas\u00e9e. JiraVersionCreator.VersionExists=[Jira] Une version nomm\u00e9e {0} existe d\u00e9j\u00e0 dans le projet {1}, il n''y a donc rien \u00e0 faire. JiraVersionCreator.CreatingVersion=[Jira] Cr\u00e9ation de la version {0} dans le projet {1}. JiraEnvironmentVariableBuilder.DisplayName=Jira : Ajout des variables d''environnement \u00e0 la construction JiraEnvironmentVariableBuilder.NoJiraSite=[Jira] Aucun site Jira n''est configur\u00e9 pour ce projet. C''est certainement une erreur de configuration du projet JiraEnvironmentVariableBuilder.Updating=[Jira] D\u00e9finition de la variable {0} avec la valeur {1}. CommentStep.Descriptor.DisplayName=Jira : Ajout un commentaire aux demandes SearchIssuesStep.Descriptor.DisplayName=Jira : Rechercher des demandes IssueSelectorStep.Descriptor.DisplayName=Jira : S\u00e9lecteur de demande JiraCreateIssueNotifier.DisplayName=Jira : Cr\u00e9er une demande IssueSelector.ExplicitIssueSelector.DisplayName=Selecteur explicite IssueSelector.JqlIssueSelector.DisplayName=S\u00e9lecteur JQL JiraVersionCreatorBuilder.DisplayName=Jira : Cr\u00e9er une nouvelle version JiraIssueFieldUpdater.DisplayName=Jira : Mise \u00e0 jour d''un champ personnalis\u00e9 JiraIssueFieldUpdater.NoIssueFieldID=ID du champ personnalis\u00e9 requis JiraIssueFieldUpdater.NotAtIssueFieldID=Ce n''est pas un ID de champ IssueFieldUpdaterStep.UpdatingIssue=[Jira][IssueFieldUpdaterStep] Mise \u00e0 jour de la demande {0} FailedToUpdateIssue=[Jira] Echec de la mise \u00e0 jour de la demande {0}. FailedToUpdateIssueWithCarryOver=[Jira] La mise \u00e0 jour de la demande {0} a echou\u00e9e. Report sur la prochaine construction. UpdatingIssue=[Jira] Mise \u00e0 jour de la demande {0} FailedToConnect=Impossible de se connecter \u00e0 Jira NoJenkinsUrl=L''URL de Jenkins n''est pas encore configur\u00e9. Allez dans la configuration de Jenkins pour la mettre \u00e0 jour NoJiraSite=Aucun site Jira n''est configur\u00e9 pour ce projet. Cel\u00e0 doit \u00eatre une erreur de configuration du projet. NoRemoteAccess=La configuration ne permet l'acc\u00e8s distant aux demandes Jira. ErrorCommentingIssues=Impossible de commenter certaines demandes : {0} ================================================ FILE: src/main/resources/hudson/plugins/jira/Messages_it.properties ================================================ JiraIssueUpdater.DisplayName=Jira: Aggiornare i problemi rilevanti JiraFolderProperty.DisplayName=Jira Associato JiraProjectProperty.DisplayName=Jira Associato JiraProjectProperty.JiraUrlMandatory=Jira URL \u00e8 un campo obbligatorio JiraProjectProperty.NotAJiraUrl=Questo \u00e8 un URL valido ma non appare come Jira JiraProjectProperty.NoWsdlAvailable=Questa sembra un'istanza Jira, ma non \u00e8 disponibile nessuna WSDL. Forse il supporto SOAP non \u00e8 ancora abilitato? JiraBuildAction.DisplayName=Problemi da Jira DefaultIssueSelector.DisplayName=Selettore predefinito PerforceJobIssueSelector.DisplayName=Selettore lavoro di Perforce P4JobIssueSelector.DisplayName=Perforce Software (P4) lavoro selettore JiraReleaseVersionBuilder.DisplayName=Jira: Segna una versione come pubblicata JiraReleaseVersionMigrator.DisplayName=Jira: Sposta i problemi corrispondenti a JQL nella versione specificata JiraIssueUpdateBuilder.DisplayName=Jira: Problemi di avanzamento per azione del flusso di lavoro JiraIssueUpdateBuilder.NoJqlSearch=Imposta il JQL usato per selezionare i problemi da aggiornare. JiraIssueUpdateBuilder.NoWorkflowAction=\u00c8 necessaria un'azione del flusso di lavoro. JiraIssueUpdateBuilder.UpdatingWithAction=[Jira] Aggiornamento problemi utilizzando l'azione del flusso di lavoro {0}. JiraIssueUpdateBuilder.Failed=[Jira] Si \u00e8 verificato un errore durante la progressione dei problemi: JiraIssueUpdateBuilder.UnknownWorkflowAction=[Jira] Impossibile aggiornare il problema {0}: azione del flusso di lavoro non valida "{1}". Forse l'utente Jenkins non ha il permesso di eseguire l'azione sul problema Jira? JiraIssueUpdateBuilder.SomeIssuesFailed=[Jira] Non \u00e8 stato possibile aggiornare almeno un problema. Vedi il log qui sopra per maggiori dettagli. JiraVersionCreator.DisplayName=Jira: Crea una nuova versione VersionReleaser.AlreadyReleased=[Jira] La versione {0} \u00e8 gi\u00e0 rilasciata nel progetto {1}, quindi niente da fare. VersionReleaser.MarkingReleased=[Jira] Marcatura versione {0} nel progetto {1} come rilasciato. JiraVersionCreator.VersionExists=[Jira] La versione {0} \u00e8 gi\u00e0 rilasciata nel progetto {1}, quindi niente da fare. JiraVersionCreator.CreatingVersion=[Jira] Creare la versione {0} nel progetto {1}. JiraEnvironmentVariableBuilder.DisplayName=Jira: Aggiungi le variabili di ambiente correlate da costruire JiraEnvironmentVariableBuilder.NoJiraSite=[Jira] Nessun sito Jira \u00e8 configurato per questo progetto. Deve essere un errore di configurazione del progetto JiraEnvironmentVariableBuilder.Updating=[Jira] Impostazione {0} a {1}. CommentStep.Descriptor.DisplayName=Jira: Aggiungi un commento ai problemi SearchIssuesStep.Descriptor.DisplayName=Jira: Cerca problemi IssueSelectorStep.Descriptor.DisplayName=Jira: Selettore dei problemi JiraCreateIssueNotifier.DisplayName=Jira: Crea il problema IssueSelector.ExplicitIssueSelector.DisplayName=Selettore esplicito IssueSelector.JqlIssueSelector.DisplayName=Selettore JQL JiraVersionCreatorBuilder.DisplayName=Jira: Crea una nuova versione JiraIssueFieldUpdater.DisplayName=Jira: Problema aggiornamento campo personalizzato JiraIssueFieldUpdater.NoIssueFieldID=Numero ID campo richiesto JiraIssueFieldUpdater.NotAtIssueFieldID=Non \u00e8 un ID campo di errore IssueFieldUpdaterStep.UpdatingIssue=[Jira][IssueFieldUpdaterStep] Aggiornamento del problema {0} FailedToUpdateIssue=[Jira] non \u00e8 stato possibile aggiornare il problema {0}. FailedToUpdateIssueWithCarryOver=[Jira] non \u00e8 stato possibile aggiornare il problema {0}. Portare alla prossima costruzione. UpdatingIssue=[Jira] Aggiornamento del problema {0} FailedToConnect=[Jira] non \u00e8 riuscito a connettersi a Jira NoJenkinsUrl=[Jira] Jenkins URL non \u00e8 ancora configurato. Vai alla configurazione di sistema per impostare questo valore NoJiraSite=[Jira] Nessun sito Jira \u00e8 configurato per questo progetto. Deve essere un errore di configurazione del progetto NoRemoteAccess=[Jira] La configurazione di sistema non consente l'accesso remoto a Jira ErrorCommentingIssues=[Jira] Impossibile commentare alcuni problemi: {0} JiraSite.threadExecutorMinimunSize = La dimensione dell'esecutore del thread deve essere almeno {0} (sono raccomandati valori superiori) JiraSite.timeoutMinimunValue = Il timeout della connessione deve essere almeno {0} JiraSite.readTimeoutMinimunValue = Il timeout della connessione deve essere almeno {0} ================================================ FILE: src/main/resources/hudson/plugins/jira/Messages_ja.properties ================================================ JiraProjectProperty.DisplayName=\u9023\u643A\u3059\u308BJira JiraProjectProperty.JiraUrlMandatory=Jira\u306EURL\u306F\u5FC5\u9808\u3067\u3059\u3002 JiraProjectProperty.NotAJiraUrl=\u6B63\u3057\u3044URL\u3067\u3059\u304C\u3001Jira\u306EURL\u3067\u306F\u306A\u3044\u3088\u3046\u3067\u3059\u3002 JiraBuildAction.DisplayName=Jira issues FailedToConnect=Jira\u306B\u63A5\u7D9A\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002 NoJenkinsUrl=Jenkins URL\u304C\u307E\u3060\u8A2D\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u30B7\u30B9\u30C6\u30E0\u306E\u8A2D\u5B9A\u753B\u9762\u3067\u3001Jenkins URL\u3092\u8A2D\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002 NoJiraSite=\u3053\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3067\u306FJira\u30B5\u30A4\u30C8\u304C\u8A2D\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E\u8A2D\u5B9A\u306B\u30A8\u30E9\u30FC\u304C\u3042\u308A\u307E\u3059\u3002 NoRemoteAccess=\u30B7\u30B9\u30C6\u30E0\u306E\u8A2D\u5B9A\u3067\u3001\u30EA\u30E2\u30FC\u30C8\u306EJira\u3078\u306E\u30A2\u30AF\u30BB\u30B9\u304C\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002 ================================================ FILE: src/main/resources/hudson/plugins/jira/Messages_pl.properties ================================================ JiraIssueUpdater.DisplayName=Jira: Aktualizuj powi\u0105zane zg\u0142oszenia JiraFolderProperty.DisplayName=Powi\u0105zana instancja Jira JiraProjectProperty.DisplayName=Powi\u0105zana instancja Jira JiraProjectProperty.JiraUrlMandatory=Adres Jira URL jest wymagany JiraProjectProperty.NotAJiraUrl=To jest poprawny URL, ale nie wygl\u0105da jak adres serwera Jira JiraProjectProperty.NoWsdlAvailable=\u017badne WSDL nie jest dost\u0119pne, czy obs\u0142uga SOAP zosta\u0142a w\u0142\u0105czona? JiraBuildAction.DisplayName=Zg\u0142oszenia Jira DefaultIssueSelector.DisplayName=Domy\u015blny selektor PerforceJobIssueSelector.DisplayName=Selektor Perforce P4JobIssueSelector.DisplayName=Selektor Perforce (P4) JiraReleaseVersionBuilder.DisplayName=Jira: Oznacz wersj\u0119 jako wydan\u0105 JiraReleaseVersionMigrator.DisplayName=Jira: Przenie\u015b zg\u0142oszenia pasuj\u0105ce do JQL do okre\u015blonej wersji JiraIssueUpdateBuilder.DisplayName=Jira: Uruchom proces dla zg\u0142osze\u0144 JiraIssueUpdateBuilder.NoJqlSearch=Ustaw JQL u\u017cywany do znalezienia zg\u0142osze\u0144 do aktualizacji. JiraIssueUpdateBuilder.NoWorkflowAction=Wyb\u00f3r akcji jest wymagany. JiraIssueUpdateBuilder.UpdatingWithAction=[Jira] Aktualizowanie zg\u0142osze\u0144 przy u\u017cyciu akcji {0}. JiraIssueUpdateBuilder.Failed=[Jira] Wyst\u0105pi\u0142 b\u0142\u0105d podczas procesowania zg\u0142osze\u0144: JiraIssueUpdateBuilder.UnknownWorkflowAction=[Jira] Nie mo\u017cna zaktualizowa\u0107 zg\u0142oszenia {0}: nieprawid\u0142owa akcja "{1}". By\u0107 mo\u017ce u\u017cytkownik Jenkins nie ma uprawnie\u0144 do wykonywania dzia\u0142a\u0144 w Jira? JiraIssueUpdateBuilder.SomeIssuesFailed=[Jira] Nie uda\u0142o si\u0119 zaktualizowa\u0107 co najmniej jednego problemu. Zobacz log powy\u017cej, aby uzyska\u0107 wi\u0119cej informacji. JiraVersionCreator.DisplayName=[Jira] Utw\u00f3rz now\u0105 wersj\u0119 VersionReleaser.AlreadyReleased=[Jira] Wersja {0} jest ju\u017c wydana w projekcie {1} - brak akcji. VersionReleaser.MarkingReleased=[Jira] Oznaczanie wersji {0} w projekcie {1} jako wydana. JiraVersionCreator.VersionExists=[Jira] Wersja o nazwie {0} ju\u017c istnieje w projekcie {1} - brak akcji. JiraVersionCreator.CreatingVersion=[Jira] Tworzenie wersji {0} w projekcie {1}. JiraEnvironmentVariableBuilder.DisplayName=[Jira] Dodaj powi\u0105zane zmienne \u015brodowiskowe do budowania JiraEnvironmentVariableBuilder.NoJiraSite=[Jira] \u017badna strona nie jest skonfigurowana dla tego projektu. To musi by\u0107 b\u0142\u0105d konfiguracji projektu JiraEnvironmentVariableBuilder.Updating=[Jira] Ustawiam {0} na {1}. CommentStep.Descriptor.DisplayName=Jira: Dodaj komentarz do zg\u0142osze\u0144 SearchIssuesStep.Descriptor.DisplayName=Jira: Szukaj zg\u0142osze\u0144 IssueSelectorStep.Descriptor.DisplayName=Jira: Selektor zg\u0142osze\u0144 JiraCreateIssueNotifier.DisplayName=Jira: Utw\u00f3rz zg\u0142oszenie IssueSelector.ExplicitIssueSelector.DisplayName=Selektor wybi\u00f3rczy IssueSelector.JqlIssueSelector.DisplayName=[Jira] Selektor JQL JiraVersionCreatorBuilder.DisplayName=[Jira] Utw\u00f3rz now\u0105 wersj\u0119 JiraIssueFieldUpdater.DisplayName=Jira: Aktualizacja p\u00f3l niestandardowych JiraIssueFieldUpdater.NoIssueFieldID=ID pola niestandardowego jest wymagany JiraIssueFieldUpdater.NotAtIssueFieldID=To nie jest ID pola niestandardowego IssueFieldUpdaterStep.UpdatingIssue=[Jira][IssueFieldUpdaterStep] Aktualizacja zg\u0142oszenia {0} FailedToUpdateIssue=[Jira] Nie uda\u0142o si\u0119 zaktualizowa\u0107 zg\u0142oszenia {0}. FailedToUpdateIssueWithCarryOver=[Jira] Nie uda\u0142o si\u0119 zaktualizowa\u0107 problemu {0}. Przeniesienie do nast\u0119pnego build'u. UpdatingIssue=[Jira] Aktualizacja zg\u0142oszenia {0} FailedToConnect=[Jira] Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107 z serwerem NoJenkinsUrl=[Jira] URL Jenkins nie jest skonfigurowany. Przejd\u017a do konfiguracji systemu, aby ustawi\u0107 t\u0119 warto\u015b\u0107 NoJiraSite=[Jira] Instancja Jira nie zosta\u0142a ustawiona - sprawd\u017a konfiguracj\u0119 projektu NoRemoteAccess=[Jira] Konfiguracja nie pozwala na zdalny dost\u0119p do Jira ErrorCommentingIssues=[Jira] Nie mo\u017cna skomentowa\u0107 niekt\u00f3rych zg\u0142osze\u0144: {0} JiraSite.threadExecutorMinimunSize = Thread Executor Size musi by\u0107 co najmniej {0} (zalecane s\u0105 wy\u017csze warto\u015bci) JiraSite.timeoutMinimunValue = Limit czasu po\u0142\u0105czenia musi wynosi\u0107 co najmniej {0} JiraSite.readTimeoutMinimunValue = Limit czasu odczytu musi wynosi\u0107 co najmniej {0} ================================================ FILE: src/main/resources/hudson/plugins/jira/listissuesparameter/JiraIssueParameterDefinition/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/listissuesparameter/JiraIssueParameterDefinition/help-altSummaryFields.html ================================================

Optionally, specify a comma-delimited list of fields to use instead of the issue summary as the title in the dropdown. Fields will be concatenated with spaces.

Example:

Field1,Field2
================================================ FILE: src/main/resources/hudson/plugins/jira/listissuesparameter/JiraIssueParameterDefinition/help-jiraIssueFilter.html ================================================
Specify the JQL search on Jira instance. For a build, Jenkins will run this query, populate a drop-down list box, then ask the user to select one.
================================================ FILE: src/main/resources/hudson/plugins/jira/listissuesparameter/JiraIssueParameterDefinition/index.jelly ================================================
================================================ FILE: src/main/resources/hudson/plugins/jira/listissuesparameter/JiraIssueParameterValue/value.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/pipeline/CommentStep/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/pipeline/IssueFieldUpdateStep/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/pipeline/IssueFieldUpdateStep/help-fieldId.html ================================================
Issue custom field ID. For example, 10100
================================================ FILE: src/main/resources/hudson/plugins/jira/pipeline/IssueFieldUpdateStep/help.html ================================================
Updates custom Jira issue field.
================================================ FILE: src/main/resources/hudson/plugins/jira/pipeline/IssueSelectorStep/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/pipeline/SearchIssuesStep/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/selector/DefaultIssueSelector/config.jelly ================================================ Selects all issues found in change lists or parameters of current build or in any of changed dependencies. Selection is using globally configured pattern that is evaluated against commit message. ================================================ FILE: src/main/resources/hudson/plugins/jira/selector/ExplicitIssueSelector/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/selector/JqlIssueSelector/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/selector/perforce/P4JobIssueSelector/config.jelly ================================================ Selects jobs associated with Perforce change lists or parameters of current build or in any of changed dependencies. It works with Perforce Software (P4) SCM plugin. ================================================ FILE: src/main/resources/hudson/plugins/jira/selector/perforce/PerforceJobIssueSelector/config.jelly ================================================ Selects jobs associated with Perforce change lists or parameters of current build or in any of changed dependencies. It works with Perforce SCM plugin. ================================================ FILE: src/main/resources/hudson/plugins/jira/versionparameter/JiraVersionParameterDefinition/config.jelly ================================================ ================================================ FILE: src/main/resources/hudson/plugins/jira/versionparameter/JiraVersionParameterDefinition/help-jiraProjectKey.html ================================================

Specify the project key. A project key is the all capitals part before the issue number in Jira.

(EXAMPLE-100)

================================================ FILE: src/main/resources/hudson/plugins/jira/versionparameter/JiraVersionParameterDefinition/help-jiraReleasePattern.html ================================================

Specify a regular expression which release names have to match to be listed. Leave this blank to match all issues.

Example:

v[0-9]+([.][0-9]+)+ will match v1.0.1, v123, v12.0.1
================================================ FILE: src/main/resources/hudson/plugins/jira/versionparameter/JiraVersionParameterDefinition/help-jiraShowReleased.html ================================================
All versions are displayed by default when all checkboxes are unchecked.
================================================ FILE: src/main/resources/hudson/plugins/jira/versionparameter/JiraVersionParameterDefinition/index.jelly ================================================
================================================ FILE: src/main/resources/hudson/plugins/jira/versionparameter/JiraVersionParameterValue/value.jelly ================================================ ================================================ FILE: src/main/resources/index.jelly ================================================
This plugin integrates Jenkins to Atlassian Jira.
================================================ FILE: src/main/webapp/help-jira-create-issue.html ================================================
Creates a Jira issue in the project with the given key.
  • If current build failed, checks if issue was already created. If yes, adds a comment, if not, creates a new Jira issue.
  • If current build passed, checks if issue from p.1 is Closed or Done. If not, adds a comment.
================================================ FILE: src/main/webapp/help-release-migrate.html ================================================
Migrates a set of Jira Issues that match a JQL query to a new fix version.
================================================ FILE: src/main/webapp/help-release.html ================================================
Modify the given version in Jira and set it to Released status.
================================================ FILE: src/main/webapp/help-version-create.html ================================================
Creates a new version in Jira in the project with the given key. By default, if the version already exists, the build will fail. This behavior can be changed by unchecking the "Fail if version already exists" option.
================================================ FILE: src/main/webapp/help.html ================================================
This causes Jenkins to remotely login to Jira (via its REST API) and post a comment with the build number which integrates the given change.
================================================ FILE: src/main/webapp/help_de.html ================================================
Lässt Jenkins einen Kommentar mit der Build-Nummer der Änderung zum zugehörigen Jira Issue hinzufügen.
================================================ FILE: src/main/webapp/help_fr.html ================================================
Ce plugin va se connecter à Jira (via l'API REST) et ajouter un commentaire avec le numéro de "build" Jenkins qui contient le changement.
================================================ FILE: src/main/webapp/help_ja.html ================================================
JenkinsがREST API経由でJiraにログインして、変更を統合したビルド番号とともにコメントをポストします。
================================================ FILE: src/spotbugs/excludesFilter.xml ================================================ ================================================ FILE: src/test/java/JiraConfig.java ================================================ import java.util.ResourceBundle; public final class JiraConfig { private static final ResourceBundle CONFIG = ResourceBundle.getBundle("jira"); public static String getUrl() { return CONFIG.getString("url"); } public static String getUsername() { return CONFIG.getString("username"); } public static String getPassword() { return CONFIG.getString("password"); } public static String getToken() { return CONFIG.getString("token"); } } ================================================ FILE: src/test/java/JiraTester.java ================================================ import com.atlassian.jira.rest.client.api.domain.Component; import com.atlassian.jira.rest.client.api.domain.Issue; import com.atlassian.jira.rest.client.api.domain.IssueType; import com.atlassian.jira.rest.client.api.domain.Status; import com.atlassian.jira.rest.client.api.domain.Transition; import com.atlassian.jira.rest.client.api.domain.User; import hudson.plugins.jira.JiraRestService; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.JiraSite.ExtendedAsynchronousJiraRestClientFactory; import hudson.plugins.jira.extension.ExtendedJiraRestClient; import hudson.plugins.jira.extension.ExtendedVersion; import java.net.URI; import java.net.URL; import java.util.List; /** * Test bed to play with Jira. * * @author Kohsuke Kawaguchi */ public class JiraTester { public static void main(String[] args) throws Exception { final URI uri = new URL(JiraConfig.getUrl()).toURI(); final ExtendedJiraRestClient jiraRestClient = new ExtendedAsynchronousJiraRestClientFactory() .createWithBasicHttpAuthentication(uri, JiraConfig.getUsername(), JiraConfig.getPassword()); final JiraRestService restService = new JiraRestService( uri, jiraRestClient, JiraConfig.getUsername(), JiraConfig.getPassword(), JiraSite.DEFAULT_TIMEOUT); final String projectKey = "TESTPROJECT"; final String issueId = "TESTPROJECT-425"; final Integer actionId = 21; final Issue issue = restService.getIssue(issueId); System.out.println("issue:" + issue); final List availableActions = restService.getAvailableActions(issueId); for (Transition action : availableActions) { System.out.println("Action:" + action); } for (IssueType issueType : restService.getIssueTypes()) { System.out.println(" issue type: " + issueType); } // restService.addVersion("TESTPROJECT", "0.0.2"); final List components = restService.getComponents(projectKey); for (Component component : components) { System.out.println("component: " + component); } // BasicComponent backendComponent = null; // final Iterable components1 = Lists.newArrayList(backendComponent); // restService.createIssue("TESTPROJECT", "This is a test issue created using Jira jenkins plugin. Please // ignore it.", "TESTUSER", components1, "test issue from Jira jenkins plugin"); final List searchResults = restService.getIssuesFromJqlSearch("project = \"TESTPROJECT\"", 100); for (Issue searchResult : searchResults) { System.out.println("JQL search result: " + searchResult); } final List projectsKeys = restService.getProjectsKeys(); for (String projectsKey : projectsKeys) { System.out.println("project key: " + projectsKey); } final List statuses = restService.getStatuses(); for (Status status : statuses) { System.out.println("status:" + status); } final User user = restService.getUser("TESTUSER"); System.out.println("user: " + user); final List versions = restService.getVersions(projectKey); for (ExtendedVersion version : versions) { System.out.println("version: " + version); } // Version releaseVersion = new Version(version.getSelf(), version.getId(), version.getName(), // version.getDescription(), version.isArchived(), true, new DateTime()); // System.out.println(" >>>> release version 0.0.2"); // restService.releaseVersion("TESTPROJECT", releaseVersion); // System.out.println(" >>> update issue TESTPROJECT-425"); // restService.updateIssue(issueId, Collections.singletonList(releaseVersion)); // final Issue updatedIssue = restService.progressWorkflowAction(issueId, actionId); // System.out.println("Updated issue:" + updatedIssue); for (int i = 0; i < 10; i++) { callUniq(restService); } for (int i = 0; i < 10; i++) { callDuplicate(restService); } } private static void callUniq(final JiraRestService restService) throws Exception { long start = System.currentTimeMillis(); List issues = restService.getIssuesFromJqlSearch( "key in ('JENKINS-53320','JENKINS-51057')", JiraSite.MAX_ALLOWED_ISSUES_FROM_JQL); long end = System.currentTimeMillis(); System.out.println("time uniq " + (end - start)); } private static void callDuplicate(final JiraRestService restService) throws Exception { long start = System.currentTimeMillis(); List issues = restService.getIssuesFromJqlSearch( "key in ('JENKINS-53320','JENKINS-53320','JENKINS-53320','JENKINS-53320','JENKINS-53320','JENKINS-51057','JENKINS-51057','JENKINS-51057','JENKINS-51057','JENKINS-51057')", JiraSite.MAX_ALLOWED_ISSUES_FROM_JQL); long end = System.currentTimeMillis(); System.out.println("time duplicate " + (end - start)); } } ================================================ FILE: src/test/java/JiraTesterBearerAuth.java ================================================ import com.atlassian.jira.rest.client.api.domain.Component; import com.atlassian.jira.rest.client.api.domain.Issue; import com.atlassian.jira.rest.client.api.domain.IssueType; import com.atlassian.jira.rest.client.api.domain.Status; import com.atlassian.jira.rest.client.api.domain.Transition; import com.atlassian.jira.rest.client.api.domain.User; import hudson.plugins.jira.JiraRestService; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.JiraSite.ExtendedAsynchronousJiraRestClientFactory; import hudson.plugins.jira.auth.BearerHttpAuthenticationHandler; import hudson.plugins.jira.extension.ExtendedJiraRestClient; import hudson.plugins.jira.extension.ExtendedVersion; import java.net.URI; import java.net.URL; import java.util.List; /** * Test bed to play with Jira. * * @author Elia Bracci */ public class JiraTesterBearerAuth { public static void main(String[] args) throws Exception { final URI uri = new URL(JiraConfig.getUrl()).toURI(); final BearerHttpAuthenticationHandler handler = new BearerHttpAuthenticationHandler(JiraConfig.getToken()); final ExtendedJiraRestClient jiraRestClient = new ExtendedAsynchronousJiraRestClientFactory().createWithAuthenticationHandler(uri, handler); final JiraRestService restService = new JiraRestService(uri, jiraRestClient, JiraConfig.getToken(), JiraSite.DEFAULT_TIMEOUT); final String projectKey = "TESTPROJECT"; final String issueId = "TESTPROJECT-425"; final Integer actionId = 21; final Issue issue = restService.getIssue(issueId); System.out.println("issue:" + issue); final List availableActions = restService.getAvailableActions(issueId); for (Transition action : availableActions) { System.out.println("Action:" + action); } for (IssueType issueType : restService.getIssueTypes()) { System.out.println(" issue type: " + issueType); } // restService.addVersion("TESTPROJECT", "0.0.2"); final List components = restService.getComponents(projectKey); for (Component component : components) { System.out.println("component: " + component); } // BasicComponent backendComponent = null; // final Iterable components1 = Lists.newArrayList(backendComponent); // restService.createIssue("TESTPROJECT", "This is a test issue created using Jira jenkins plugin. Please // ignore it.", "TESTUSER", components1, "test issue from Jira jenkins plugin"); final List searchResults = restService.getIssuesFromJqlSearch("project = \"TESTPROJECT\"", 100); for (Issue searchResult : searchResults) { System.out.println("JQL search result: " + searchResult); } final List projectsKeys = restService.getProjectsKeys(); for (String projectsKey : projectsKeys) { System.out.println("project key: " + projectsKey); } final List statuses = restService.getStatuses(); for (Status status : statuses) { System.out.println("status:" + status); } final User user = restService.getUser("TESTUSER"); System.out.println("user: " + user); final List versions = restService.getVersions(projectKey); for (ExtendedVersion version : versions) { System.out.println("version: " + version); } // Version releaseVersion = new Version(version.getSelf(), version.getId(), version.getName(), // version.getDescription(), version.isArchived(), true, new DateTime()); // System.out.println(" >>>> release version 0.0.2"); // restService.releaseVersion("TESTPROJECT", releaseVersion); // System.out.println(" >>> update issue TESTPROJECT-425"); // restService.updateIssue(issueId, Collections.singletonList(releaseVersion)); // final Issue updatedIssue = restService.progressWorkflowAction(issueId, actionId); // System.out.println("Updated issue:" + updatedIssue); for (int i = 0; i < 10; i++) { callUniq(restService); } for (int i = 0; i < 10; i++) { callDuplicate(restService); } } private static void callUniq(final JiraRestService restService) throws Exception { long start = System.currentTimeMillis(); List issues = restService.getIssuesFromJqlSearch("key in ('JENKINS-53320','JENKINS-51057')", 100); long end = System.currentTimeMillis(); System.out.println("time uniq " + (end - start)); } private static void callDuplicate(final JiraRestService restService) throws Exception { long start = System.currentTimeMillis(); List issues = restService.getIssuesFromJqlSearch( "key in ('JENKINS-53320','JENKINS-53320','JENKINS-53320','JENKINS-53320','JENKINS-53320','JENKINS-51057','JENKINS-51057','JENKINS-51057','JENKINS-51057','JENKINS-51057')", 100); long end = System.currentTimeMillis(); System.out.println("time duplicate " + (end - start)); } } ================================================ FILE: src/test/java/com/atlassian/httpclient/apache/httpcomponents/ApacheAsyncHttpClientTest.java ================================================ package com.atlassian.httpclient.apache.httpcomponents; import static org.junit.jupiter.api.Assertions.assertEquals; import com.atlassian.httpclient.api.Response; import com.atlassian.httpclient.api.factory.HttpClientOptions; import com.atlassian.sal.api.ApplicationProperties; import com.atlassian.sal.api.UrlMode; import com.atlassian.sal.api.executor.ThreadLocalContextManager; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import hudson.ProxyConfiguration; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.Base64; import java.util.Date; import java.util.Optional; import java.util.concurrent.TimeUnit; import jenkins.model.Jenkins; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.util.Callback; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; @WithJenkins public class ApacheAsyncHttpClientTest { private final ConnectionFactory connectionFactory = new HttpConnectionFactory(); private Server server; private ServerConnector connector; static final String CONTENT_RESPONSE = "Sounds Good"; public void prepare(Handler handler) throws Exception { server = new Server(); connector = new ServerConnector(server, connectionFactory); server.addConnector(connector); server.setHandler(handler); server.start(); } @AfterEach void dispose() throws Exception { if (server != null) { server.stop(); } } @Test void simple_get(JenkinsRule r) throws Exception { TestHandler testHandler = new TestHandler(); prepare(testHandler); ApacheAsyncHttpClient httpClient = new ApacheAsyncHttpClient( null, buildApplicationProperties(), new NoOpThreadLocalContextManager(), new HttpClientOptions()); Response response = httpClient .newRequest("http://localhost:" + connector.getLocalPort() + "/foo") .get() .get(10, TimeUnit.SECONDS); assertEquals(200, response.getStatusCode()); assertEquals(CONTENT_RESPONSE, IOUtils.toString(response.getEntityStream())); } @Test void simple_post(JenkinsRule r) throws Exception { TestHandler testHandler = new TestHandler(); prepare(testHandler); ApacheAsyncHttpClient httpClient = new ApacheAsyncHttpClient( null, buildApplicationProperties(), new NoOpThreadLocalContextManager(), new HttpClientOptions()); Response response = httpClient .newRequest("http://localhost:" + connector.getLocalPort() + "/foo") .setEntity("FOO") .setContentType("text") .post() .get(10, TimeUnit.SECONDS); assertEquals(200, response.getStatusCode()); assertEquals(CONTENT_RESPONSE, IOUtils.toString(response.getEntityStream())); assertEquals("FOO", testHandler.postReceived); } @Test void simple_get_with_non_proxy_host(JenkinsRule r) throws Exception { ProxyTestHandler testHandler = new ProxyTestHandler(); prepare(testHandler); Jenkins.get().proxy = new ProxyConfiguration("localhost", connector.getLocalPort(), "foo", "bar", "www.apache.org"); ApacheAsyncHttpClient httpClient = new ApacheAsyncHttpClient( null, buildApplicationProperties(), new NoOpThreadLocalContextManager(), new HttpClientOptions()); Response response = httpClient.newRequest("http://www.apache.org").get().get(30, TimeUnit.SECONDS); assertEquals(200, response.getStatusCode()); // assertEquals(CONTENT_RESPONSE, IOUtils.toString(response.getEntityStream())); } @Test void simple_get_with_proxy(JenkinsRule r) throws Exception { ProxyTestHandler testHandler = new ProxyTestHandler(); prepare(testHandler); Jenkins.get().proxy = new ProxyConfiguration("localhost", connector.getLocalPort(), "foo", "bar"); ApacheAsyncHttpClient httpClient = new ApacheAsyncHttpClient( null, buildApplicationProperties(), new NoOpThreadLocalContextManager(), new HttpClientOptions()); Response response = httpClient.newRequest("http://jenkins.io").get().get(30, TimeUnit.SECONDS); assertEquals(200, response.getStatusCode()); assertEquals(CONTENT_RESPONSE, IOUtils.toString(response.getEntityStream())); } @Test void simple_post_with_proxy(JenkinsRule r) throws Exception { ProxyTestHandler testHandler = new ProxyTestHandler(); prepare(testHandler); Jenkins.get().proxy = new ProxyConfiguration("localhost", connector.getLocalPort(), "foo", "bar"); ApacheAsyncHttpClient httpClient = new ApacheAsyncHttpClient( null, buildApplicationProperties(), new NoOpThreadLocalContextManager(), new HttpClientOptions()); Response response = httpClient .newRequest("http://jenkins.io") .setEntity("FOO") .setContentType("text") .post() .get(30, TimeUnit.SECONDS); // we are sure to hit the proxy first :-) assertEquals(200, response.getStatusCode()); assertEquals(CONTENT_RESPONSE, IOUtils.toString(response.getEntityStream())); assertEquals("FOO", testHandler.postReceived); } public static class ProxyTestHandler extends Handler.Abstract { String postReceived; final String user = "foo"; final String password = "bar"; final String serverHost = "server"; final String realm = "test_realm"; @Override public boolean handle( org.eclipse.jetty.server.Request request, org.eclipse.jetty.server.Response response, Callback callback) throws IOException { final String credentials = Base64.getEncoder().encodeToString((user + ":" + password).getBytes("UTF-8")); String authorization = request.getHeaders().get(HttpHeader.PROXY_AUTHORIZATION.asString()); if (authorization == null) { response.setStatus(HttpStatus.PROXY_AUTHENTICATION_REQUIRED_407); response.getHeaders().add(HttpHeader.PROXY_AUTHENTICATE.asString(), "Basic realm=\"" + realm + "\""); callback.succeeded(); return true; } else { String prefix = "Basic "; if (authorization.startsWith(prefix)) { String attempt = authorization.substring(prefix.length()); if (!credentials.equals(attempt)) { callback.succeeded(); return true; } } } if (StringUtils.equalsIgnoreCase("post", request.getMethod())) { postReceived = Content.Source.asString(request, StandardCharsets.UTF_8); } Content.Sink.write(response, true, CONTENT_RESPONSE, callback); return true; } } public static class TestHandler extends Handler.Abstract { String postReceived; @Override public boolean handle( org.eclipse.jetty.server.Request request, org.eclipse.jetty.server.Response response, Callback callback) throws IOException { if (StringUtils.equalsIgnoreCase("post", request.getMethod())) { postReceived = Content.Source.asString(request, StandardCharsets.UTF_8); } Content.Sink.write(response, true, CONTENT_RESPONSE, callback); return true; } } private ApplicationProperties buildApplicationProperties() { ApplicationProperties applicationProperties = new ApplicationProperties() { @Override public String getBaseUrl() { return null; } @NonNull @Override public String getBaseUrl(UrlMode urlMode) { return null; } @NonNull @Override public String getDisplayName() { return "Foo"; } @NonNull @Override public String getPlatformId() { return null; } @NonNull @Override public String getVersion() { return "1"; } @NonNull @Override public Date getBuildDate() { return null; } @NonNull @Override public String getBuildNumber() { return "1"; } @Nullable @Override public File getHomeDirectory() { return null; } @Override public String getPropertyValue(String s) { return null; } @NonNull @Override public String getApplicationFileEncoding() { return System.getProperty("file.encoding"); } @NonNull @Override public Optional getLocalHomeDirectory() { return Optional.empty(); } @NonNull @Override public Optional getSharedHomeDirectory() { return Optional.empty(); } }; return applicationProperties; } private static final class NoOpThreadLocalContextManager implements ThreadLocalContextManager { @Override public C getThreadLocalContext() { return null; } @Override public void setThreadLocalContext(C context) {} @Override public void clearThreadLocalContext() {} } } ================================================ FILE: src/test/java/com/atlassian/httpclient/apache/httpcomponents/CompletableFuturePromiseHttpPromiseAsyncClientTest.java ================================================ package com.atlassian.httpclient.apache.httpcomponents; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.atlassian.sal.api.executor.ThreadLocalContextManager; import java.io.IOException; import java.util.concurrent.Executor; import java.util.concurrent.Future; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.concurrent.FutureCallback; import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; import org.apache.http.protocol.HttpContext; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.Answer; @ExtendWith(MockitoExtension.class) class CompletableFuturePromiseHttpPromiseAsyncClientTest { @Mock private CloseableHttpAsyncClient client; @Mock private ThreadLocalContextManager threadLocalContextManager; @Mock private Executor executor; @Mock private HttpUriRequest request; @Mock private HttpResponse response; @Mock private HttpContext context; @InjectMocks private CompletableFuturePromiseHttpPromiseAsyncClient asyncClient; @Test void ensureCloseHttpclientOnCompletion() throws IOException { when(client.execute(eq(request), eq(context), any())).then((Answer>) invocation -> { invocation.getArgument(2, FutureCallback.class).completed(response); return mock(Future.class); }); asyncClient.execute(request, context); verify(client).close(); } @Test void ensureCloseHttpclientOnFailure() throws IOException { when(client.execute(eq(request), eq(context), any())).then((Answer>) invocation -> { invocation.getArgument(2, FutureCallback.class).failed(null); return mock(Future.class); }); asyncClient.execute(request, context); verify(client).close(); } @Test void ensureCloseHttpclientOnCancellation() throws IOException { when(client.execute(eq(request), eq(context), any())).then((Answer>) invocation -> { invocation.getArgument(2, FutureCallback.class).cancelled(); return mock(Future.class); }); asyncClient.execute(request, context); verify(client).close(); } } ================================================ FILE: src/test/java/hudson/plugins/jira/BuildListenerResultMethodMock.java ================================================ package hudson.plugins.jira; import hudson.model.Result; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; public class BuildListenerResultMethodMock implements Answer { private Result result; @Override public Void answer(InvocationOnMock invocation) throws Throwable { this.result = invocation.getArgument(0, Result.class); return null; } public Result getResult() { return result; } } ================================================ FILE: src/test/java/hudson/plugins/jira/ChangingWorkflowTest.java ================================================ package hudson.plugins.jira; import static org.apache.commons.lang3.RandomStringUtils.randomNumeric; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.nullValue; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.atlassian.jira.rest.client.api.domain.Issue; import com.atlassian.jira.rest.client.api.domain.Transition; import hudson.model.Item; import java.io.PrintStream; import java.util.Arrays; import java.util.Collections; import java.util.concurrent.TimeoutException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; /** * User: lanwen * Date: 10.09.13 * Time: 0:57 */ @ExtendWith(MockitoExtension.class) public class ChangingWorkflowTest { public static final String NON_EMPTY_COMMENT = "Non empty comment"; private final String ISSUE_JQL = "jql"; private final String NON_EMPTY_WORKFLOW_LOWERCASE = "workflow"; @Mock private JiraSite site; @Mock private JiraRestService restService; @Mock private JiraSession mockSession; @Mock private Item mockItem; private JiraSession spySession; @BeforeEach void setupSpy() { spySession = spy(new JiraSession(site, restService)); } @Test void onGetActionItInvokesServiceMethod() { spySession.getActionIdForIssue(ISSUE_JQL, NON_EMPTY_WORKFLOW_LOWERCASE); verify(restService, times(1)).getAvailableActions(eq(ISSUE_JQL)); } @Test void getActionIdReturnsNullWhenServiceReturnsNull() { doReturn(null).when(restService).getAvailableActions(ISSUE_JQL); assertThat(spySession.getActionIdForIssue(ISSUE_JQL, NON_EMPTY_WORKFLOW_LOWERCASE), nullValue()); } @Test void getActionIdIteratesOverAllActionsEvenOneOfNamesIsNull() { Transition action1 = mock(Transition.class); Transition action2 = mock(Transition.class); doReturn(null).when(action1).getName(); doReturn("name").when(action2).getName(); doReturn(Arrays.asList(action1, action2)).when(restService).getAvailableActions(ISSUE_JQL); assertThat(spySession.getActionIdForIssue(ISSUE_JQL, NON_EMPTY_WORKFLOW_LOWERCASE), nullValue()); verify(action1, times(1)).getName(); verify(action2, times(2)).getName(); // one for null check, other for equals } @Test void getActionIdReturnsNullWhenNullWorkflowUsed() { String workflowAction = null; Transition action1 = mock(Transition.class); when(action1.getName()).thenReturn("name"); when(restService.getAvailableActions(ISSUE_JQL)).thenReturn(Collections.singletonList(action1)); assertThat(spySession.getActionIdForIssue(ISSUE_JQL, workflowAction), nullValue()); } @Test void getActionIdReturnsIdWhenFoundIgnorecaseWorkflow() { String id = randomNumeric(5); Transition action1 = mock(Transition.class); when(action1.getName()).thenReturn(NON_EMPTY_WORKFLOW_LOWERCASE.toUpperCase()); when(restService.getAvailableActions(ISSUE_JQL)).thenReturn(Arrays.asList(action1)); when(action1.getId()).thenReturn(Integer.valueOf(id)); assertThat( spySession.getActionIdForIssue(ISSUE_JQL, NON_EMPTY_WORKFLOW_LOWERCASE), equalTo(Integer.valueOf(id))); } @Test void addCommentsOnNonEmptyWorkflowAndNonEmptyComment() throws Exception { when(site.getSession(any(), anyBoolean())).thenCallRealMethod(); when(site.getSession(any())).thenCallRealMethod(); when(site.createSession(any(), anyBoolean())).thenReturn(mockSession); site.getSession(mockItem); when(mockSession.getIssuesFromJqlSearch(anyString())).thenReturn(Arrays.asList(mock(Issue.class))); when(site.progressMatchingIssues(anyString(), any(), anyString(), any(PrintStream.class))) .thenCallRealMethod(); site.progressMatchingIssues( ISSUE_JQL, NON_EMPTY_WORKFLOW_LOWERCASE, NON_EMPTY_COMMENT, mock(PrintStream.class)); verify(mockSession, times(1)).addComment(any(), eq(NON_EMPTY_COMMENT), isNull(), isNull()); verify(mockSession, times(1)).progressWorkflowAction(any(), anyInt()); } @Test void addCommentsOnNullWorkflowAndNonEmptyComment() throws Exception { when(site.getSession(any())).thenCallRealMethod(); when(site.getSession(any(), anyBoolean())).thenCallRealMethod(); when(site.createSession(any(), anyBoolean())).thenReturn(mockSession); site.getSession(mockItem); when(mockSession.getIssuesFromJqlSearch(anyString())).thenReturn(Arrays.asList(mock(Issue.class))); when(site.progressMatchingIssues(anyString(), any(), anyString(), any(PrintStream.class))) .thenCallRealMethod(); site.progressMatchingIssues(ISSUE_JQL, "", NON_EMPTY_COMMENT, mock(PrintStream.class)); verify(mockSession, times(1)).addComment(any(), eq(NON_EMPTY_COMMENT), isNull(), isNull()); } @Test void dontAddCommentsOnNullWorkflowAndNullComment() throws TimeoutException { site.progressMatchingIssues(ISSUE_JQL, null, null, mock(PrintStream.class)); verify(mockSession, never()).addComment(anyString(), anyString(), isNull(), isNull()); } } ================================================ FILE: src/test/java/hudson/plugins/jira/CliParameterTest.java ================================================ package hudson.plugins.jira; import static hudson.cli.CLICommandInvoker.Matcher.succeeded; import static org.hamcrest.MatcherAssert.assertThat; import hudson.cli.BuildCommand; import hudson.cli.CLICommandInvoker; import hudson.model.FreeStyleProject; import hudson.model.ParametersDefinitionProperty; import hudson.plugins.jira.listissuesparameter.JiraIssueParameterDefinition; import hudson.plugins.jira.versionparameter.JiraVersionParameterDefinition; import java.io.IOException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; @WithJenkins class CliParameterTest { private JenkinsRule jenkins; FreeStyleProject project; @BeforeEach void setup(JenkinsRule jenkins) throws IOException { this.jenkins = jenkins; project = jenkins.createFreeStyleProject(); } @Test void jiraIssueParameterViaCli() throws Exception { project.addProperty(new ParametersDefinitionProperty( new JiraIssueParameterDefinition("jiraissue", "description", "filter"))); CLICommandInvoker invoker = new CLICommandInvoker(jenkins, new BuildCommand()); CLICommandInvoker.Result result = invoker.invokeWithArgs(project.getName(), "-s", "-p", "jiraissue=TEST-1"); assertThat(result, succeeded()); } @Test void jiraVersionParameterViaCli() throws Exception { project.addProperty(new ParametersDefinitionProperty(new JiraVersionParameterDefinition( "jiraversion", "description", "PROJ", "RELEASE", "true", "false", "false"))); CLICommandInvoker invoker = new CLICommandInvoker(jenkins, new BuildCommand()); CLICommandInvoker.Result result = invoker.invokeWithArgs(project.getName(), "-s", "-p", "jiraversion=1.0"); assertThat(result, succeeded()); } } ================================================ FILE: src/test/java/hudson/plugins/jira/ConfigAsCodeTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import io.jenkins.plugins.casc.ConfigurationContext; import io.jenkins.plugins.casc.Configurator; import io.jenkins.plugins.casc.ConfiguratorRegistry; import io.jenkins.plugins.casc.misc.ConfiguredWithCode; import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule; import io.jenkins.plugins.casc.misc.junit.jupiter.WithJenkinsConfiguredWithCode; import io.jenkins.plugins.casc.model.CNode; import io.jenkins.plugins.casc.model.Mapping; import java.util.List; import java.util.Objects; import org.junit.jupiter.api.Test; @WithJenkinsConfiguredWithCode class ConfigAsCodeTest { @Test @ConfiguredWithCode("multiple-sites.yml") void shouldSupportConfigurationAsCode(JenkinsConfiguredWithCodeRule r) throws Exception { List sites = JiraGlobalConfiguration.get().getSites(); assertThat(sites, hasSize(2)); assertEquals( "https://issues.jenkins-ci.org/", Objects.requireNonNull(sites.get(0).getUrl()).toExternalForm()); assertEquals( "https://jira.com/", Objects.requireNonNull(sites.get(1).getUrl()).toExternalForm()); } @Test @ConfiguredWithCode("single-site.yml") void shouldExportConfigurationAsCode(JenkinsConfiguredWithCodeRule r) throws Exception { ConfiguratorRegistry registry = ConfiguratorRegistry.get(); ConfigurationContext context = new ConfigurationContext(registry); final Configurator c = context.lookupOrFail(JiraGlobalConfiguration.class); final CNode node = c.describe(JiraGlobalConfiguration.get(), context); assertNotNull(node); final Mapping mapping = node.asMapping(); Mapping sites = mapping.get("sites").asSequence().get(0).asMapping(); assertThat(sites.getScalarValue("url"), is("https://jira.com/")); } } ================================================ FILE: src/test/java/hudson/plugins/jira/CredentialsHelperTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.hasSize; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.CredentialsScope; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import com.cloudbees.plugins.credentials.domains.Domain; import com.cloudbees.plugins.credentials.domains.DomainSpecification; import com.cloudbees.plugins.credentials.domains.HostnameSpecification; import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import hudson.model.Descriptor.FormException; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; /** * @author Zhenlei Huang */ @WithJenkins class CredentialsHelperTest { @Test void lookupSystemCredentials(JenkinsRule r) throws IOException, FormException { assertNull(CredentialsHelper.lookupSystemCredentials("nonexistent-credentials-id", null)); StandardUsernamePasswordCredentials c = new UsernamePasswordCredentialsImpl(CredentialsScope.SYSTEM, null, null, "username", "password"); CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), c); assertEquals(c, CredentialsHelper.lookupSystemCredentials(c.getId(), null)); assertEquals(c, CredentialsHelper.lookupSystemCredentials(c.getId(), new URL("http://example.org"))); } @Test void lookupSystemCredentialsWithDomainRestriction(JenkinsRule r) throws IOException, FormException { Domain domain = new Domain( "example", "test domain", Arrays.asList(new HostnameSpecification("example.org", null))); StandardUsernamePasswordCredentials c = new UsernamePasswordCredentialsImpl(CredentialsScope.SYSTEM, null, null, "username", "password"); CredentialsProvider.lookupStores(r.jenkins).iterator().next().addDomain(domain, c); assertEquals(c, CredentialsHelper.lookupSystemCredentials(c.getId(), null)); assertEquals(c, CredentialsHelper.lookupSystemCredentials(c.getId(), new URL("http://example.org"))); assertNull(CredentialsHelper.lookupSystemCredentials(c.getId(), new URL("http://nonexistent.url"))); } @Test void migrateCredentials(JenkinsRule r) throws MalformedURLException, FormException { assertThat( CredentialsProvider.lookupStores(r.jenkins).iterator().next().getCredentials(Domain.global()), empty()); StandardUsernamePasswordCredentials c = CredentialsHelper.migrateCredentials("username", "password", new URL("http://example.org")); assertEquals("Migrated by Jira Plugin", c.getDescription()); assertThat( CredentialsProvider.lookupStores(r.jenkins).iterator().next().getCredentials(Domain.global()), hasSize(1)); } @Test void migrateCredentialsWithExsitingCredentials(JenkinsRule r) throws IOException, FormException { Domain domain = new Domain( "example", "test domain", Arrays.asList(new HostnameSpecification("example.org", null))); StandardUsernamePasswordCredentials c = new UsernamePasswordCredentialsImpl(CredentialsScope.SYSTEM, null, null, "username", "password"); CredentialsProvider.lookupStores(r.jenkins).iterator().next().addDomain(domain, c); StandardUsernamePasswordCredentials cred = CredentialsHelper.migrateCredentials("username", "password", new URL("http://example.org")); assertEquals(c, cred); } } ================================================ FILE: src/test/java/hudson/plugins/jira/DescriptorImplTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.atlassian.jira.rest.client.api.RestClientException; import com.atlassian.jira.rest.client.api.domain.Permissions; import com.cloudbees.plugins.credentials.CredentialsNameProvider; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.CredentialsScope; import com.cloudbees.plugins.credentials.CredentialsStore; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import com.cloudbees.plugins.credentials.domains.Domain; import com.cloudbees.plugins.credentials.domains.HostnameSpecification; import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Descriptor.FormException; import hudson.model.FreeStyleBuild; import hudson.model.FreeStyleProject; import hudson.model.Item; import hudson.model.Run; import hudson.model.User; import hudson.security.ACL; import hudson.security.ACLContext; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import java.io.IOException; import java.net.URL; import java.util.Arrays; import jenkins.model.Jenkins; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.MockAuthorizationStrategy; import org.jvnet.hudson.test.MockFolder; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.mockito.Mockito; /** * Created by warden on 14.09.15. */ @WithJenkins class DescriptorImplTest { AbstractBuild build = Mockito.mock(FreeStyleBuild.class); Run run = mock(Run.class); AbstractProject project = mock(FreeStyleProject.class); JiraSite site = mock(JiraSite.class); JiraSession session = mock(JiraSession.class); JiraSite.DescriptorImpl descriptor = spy(new JiraSite.DescriptorImpl()); JiraSite.Builder builder = spy(new JiraSite.Builder()); @Test void doFillCredentialsIdItems(JenkinsRule r) throws IOException, FormException { MockFolder dummy = r.createFolder("dummy"); r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); MockAuthorizationStrategy as = new MockAuthorizationStrategy(); as.grant(Jenkins.ADMINISTER).everywhere().to("admin"); as.grant(Item.READ).onItems(dummy).to("alice"); as.grant(Item.CONFIGURE).onItems(dummy).to("dev"); r.jenkins.setAuthorizationStrategy(as); Domain domain = new Domain("example", "test domain", Arrays.asList(new HostnameSpecification("example.org", null))); StandardUsernamePasswordCredentials c1 = new UsernamePasswordCredentialsImpl(CredentialsScope.SYSTEM, null, null, "username", "password"); CredentialsStore credentialsStore = CredentialsProvider.lookupStores(r.jenkins).iterator().next(); credentialsStore.addDomain(domain, c1); StandardUsernamePasswordCredentials c2 = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, null, null, "uid", "pwd"); credentialsStore = CredentialsProvider.lookupStores(dummy).iterator().next(); credentialsStore.addCredentials(domain, c2); JiraSite.DescriptorImpl descriptor = r.jenkins.getDescriptorByType(JiraSite.DescriptorImpl.class); try (ACLContext ignored = ACL.as(User.getById("admin", true))) { ListBoxModel options = descriptor.doFillCredentialsIdItems(null, null, "http://example.org"); assertThat(options.toString(), options, hasSize(3)); assertTrue(listBoxModelContainsName(options, CredentialsNameProvider.name(c1)), options.toString()); options = descriptor.doFillCredentialsIdItems(null, null, "http://nonexistent.url"); assertThat(options.toString(), options, hasSize(1)); assertEquals("", options.get(0).value); options = descriptor.doFillCredentialsIdItems(dummy, null, "http://example.org"); assertThat(options.toString(), options, hasSize(2)); assertTrue(listBoxModelContainsName(options, CredentialsNameProvider.name(c2)), options.toString()); } try (ACLContext ignored = ACL.as(User.getById("alice", true))) { ListBoxModel options = descriptor.doFillCredentialsIdItems(dummy, null, "http://example.org"); assertThat(options.toString(), options, hasSize(1)); } try (ACLContext ignored = ACL.as(User.getById("dev", true))) { ListBoxModel options = descriptor.doFillCredentialsIdItems(dummy, null, "http://example.org"); assertThat(options.toString(), options, hasSize(2)); } } private boolean listBoxModelContainsName(ListBoxModel options, String name) { return options.stream() .filter(option -> name.equals(option.name)) .findFirst() .isPresent(); } @Test void validateFormConnectionErrors(JenkinsRule r) throws Exception { builder.withMainURL(new URL("http://test.com")); when(descriptor.getBuilder()).thenReturn(builder); when(builder.build()).thenReturn(site); when(build.getParent()).thenReturn(project); when(site.getSession(project, true)).thenReturn(session); when(session.getMyPermissions()).thenThrow(RestClientException.class); FormValidation validation = descriptor.doValidate( "http://localhost:8080", null, null, null, false, null, JiraSite.DEFAULT_TIMEOUT, JiraSite.DEFAULT_READ_TIMEOUT, JiraSite.DEFAULT_THREAD_EXECUTOR_NUMBER, false, project); assertEquals(FormValidation.Kind.ERROR, validation.kind); verify(site).getSession(project, true); validation = descriptor.doValidate( "http://localhost:8080", null, null, null, false, null, -1, JiraSite.DEFAULT_READ_TIMEOUT, JiraSite.DEFAULT_THREAD_EXECUTOR_NUMBER, false, project); assertEquals(Messages.JiraSite_timeoutMinimunValue("1"), validation.getLocalizedMessage()); assertEquals(FormValidation.Kind.ERROR, validation.kind); verify(site).getSession(project, true); validation = descriptor.doValidate( "http://localhost:8080", null, null, null, false, null, JiraSite.DEFAULT_TIMEOUT, -1, JiraSite.DEFAULT_THREAD_EXECUTOR_NUMBER, false, project); assertEquals(Messages.JiraSite_readTimeoutMinimunValue("1"), validation.getMessage()); assertEquals(FormValidation.Kind.ERROR, validation.kind); verify(site).getSession(project, true); validation = descriptor.doValidate( "http://localhost:8080", null, null, null, false, null, JiraSite.DEFAULT_TIMEOUT, JiraSite.DEFAULT_READ_TIMEOUT, -1, false, project); assertEquals(Messages.JiraSite_threadExecutorMinimunSize("1"), validation.getMessage()); assertEquals(FormValidation.Kind.ERROR, validation.kind); verify(site).getSession(project, true); } @Test void validateFormConnectionOK(JenkinsRule r) throws Exception { builder.withMainURL(new URL("http://test.com")); when(descriptor.getBuilder()).thenReturn(builder); when(builder.build()).thenReturn(site); when(site.getSession(project, true)).thenReturn(session); when(session.getMyPermissions()).thenReturn(mock(Permissions.class)); FormValidation validation = descriptor.doValidate( "http://localhost:8080", null, null, null, false, null, JiraSite.DEFAULT_TIMEOUT, JiraSite.DEFAULT_READ_TIMEOUT, JiraSite.DEFAULT_THREAD_EXECUTOR_NUMBER, false, project); verify(builder).build(); verify(site).getSession(project, true); assertEquals(FormValidation.Kind.OK, validation.kind); } } ================================================ FILE: src/test/java/hudson/plugins/jira/EmptyFriendlyURLConverterTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.nullValue; import java.net.URL; import org.apache.commons.lang3.StringUtils; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.WithoutJenkins; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; /** * @author lanwen (Merkushev Kirill) */ @WithJenkins public class EmptyFriendlyURLConverterTest { public static final String SOME_VALID_URL = "http://localhost/"; @Test @WithoutJenkins void shouldHandleURLClass() throws Exception { URL someUrl = new URL(SOME_VALID_URL); assertThat(new EmptyFriendlyURLConverter().convert(URL.class, someUrl), Matchers.is(someUrl)); } @Test @WithoutJenkins void shouldHandleStringClass() throws Exception { assertThat( new EmptyFriendlyURLConverter().convert(URL.class, SOME_VALID_URL), Matchers.is(new URL(SOME_VALID_URL))); } @Test @WithoutJenkins void shouldHandleNull() throws Exception { assertThat(new EmptyFriendlyURLConverter().convert(URL.class, null), nullValue()); } @Test @WithoutJenkins void shouldHandleEmptyString() throws Exception { assertThat(new EmptyFriendlyURLConverter().convert(URL.class, StringUtils.EMPTY), nullValue()); } @Test @WithoutJenkins void shouldHandleNullAsString() throws Exception { assertThat(new EmptyFriendlyURLConverter().convert(URL.class, "null"), nullValue()); } /** * Requires jenkins rule because of LOGGER usage starts descriptor creating */ @Test void shouldHandleMalformedUrlAsString(JenkinsRule j) throws Exception { assertThat(new EmptyFriendlyURLConverter().convert(URL.class, "bla"), nullValue()); } } ================================================ FILE: src/test/java/hudson/plugins/jira/EnvironmentExpanderTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsEqual.equalTo; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import hudson.EnvVars; import hudson.model.AbstractBuild; import hudson.model.BuildListener; import hudson.model.FreeStyleBuild; import java.io.IOException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; class EnvironmentExpanderTest { private static final String VARIABLE = "${ISSUE_ID}"; private static final String ENVIRONMENT_KEY = "ISSUE_ID"; private static final String ENVIRONMENT_VALUE = "EXAMPLE-1"; EnvVars env; BuildListener buildListener = mock(BuildListener.class); AbstractBuild currentBuild = mock(FreeStyleBuild.class); @BeforeEach void createCharacteristicEnvironment() throws IOException, InterruptedException { env = new EnvVars(); env.put("BUILD_NUMBER", "1"); env.put("BUILD_URL", "/some/url/to/job"); env.put("JOB_NAME", "EnvironmentExpander Test Job"); doReturn(env).when(currentBuild).getEnvironment(Mockito.any()); } @Test void returnVariableWhenValueNotFound() { String value = EnvironmentExpander.expandVariable(VARIABLE, env); assertThat(value, equalTo(VARIABLE)); } @Test void returnValueWhenFound() { env.put(ENVIRONMENT_KEY, ENVIRONMENT_VALUE); String value = EnvironmentExpander.expandVariable(VARIABLE, env); assertThat(value, equalTo(ENVIRONMENT_VALUE)); env.remove(ENVIRONMENT_KEY); } @Test void returnVariableFromNullRunEnvironment() { String value = EnvironmentExpander.expandVariable(VARIABLE, null, null); assertThat(value, equalTo(VARIABLE)); } @Test void returnValueFromRunEnvironment() { env.put(ENVIRONMENT_KEY, ENVIRONMENT_VALUE); String value = EnvironmentExpander.expandVariable(VARIABLE, currentBuild, buildListener); assertThat(value, equalTo(ENVIRONMENT_VALUE)); env.remove(ENVIRONMENT_KEY); } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraBuildActionTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; import hudson.model.Job; import hudson.model.Run; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.jvnet.hudson.test.recipes.LocalData; /** * Place needed resources in src/test/resources * */ class JiraBuildActionTest { /** * Test if existing serialized JiraBuildAction information will be loaded after upgrading jira * plugin version to with new JiraBuildAction class version introduced in PR-72. */ @Test @LocalData @WithJenkins void binaryCompatibility(JenkinsRule r) throws Exception { assertEquals("Jenkins JiraBuildActionTest config", r.jenkins.getSystemMessage()); Job job = r.getInstance().getItemByFullName("/project", Job.class); Run run = job.getBuildByNumber(2); assertEquals("job/project/2/", run.getUrl()); JiraBuildAction jba = run.getAction(JiraBuildAction.class); assertThat(jba.getOwner().getDisplayName(), is(run.getDisplayName())); assertThat(jba.getIssue("JIRA-123").getSummary(), is("Issue summary")); } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraChangeLogAnnotatorTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNot.not; import static org.hamcrest.core.StringContains.containsString; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import hudson.MarkupText; import hudson.model.Run; import hudson.plugins.jira.model.JiraIssue; import java.io.IOException; import java.net.URL; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Pattern; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.jvnet.hudson.test.Issue; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.Answer; /** * @author Kohsuke Kawaguchi */ @ExtendWith(MockitoExtension.class) class JiraChangeLogAnnotatorTest { private static final String TITLE = "title with $sign to confuse TextMarkup.replace"; @Mock(strictness = Mock.Strictness.LENIENT) private JiraSite site; @Mock private Run run; @Mock(strictness = Mock.Strictness.LENIENT) private JiraSession session; @BeforeEach void before() throws Exception { when(session.getProjectKeys()).thenReturn(new HashSet(Arrays.asList("DUMMY", "JENKINS"))); when(site.getSession(any())).thenReturn(session); when(site.getProjectUpdateLock()).thenReturn(new ReentrantLock()); when(site.getUrl(Mockito.anyString())).thenAnswer((Answer) invocation -> { String id = invocation.getArguments()[0].toString(); return new URL("http://dummy/" + id); }); when(site.getProjectKeys(run.getParent())).thenCallRealMethod(); when(site.getIssuePattern()).thenCallRealMethod(); } @Test void annotate() { when(run.getAction(JiraBuildAction.class)) .thenReturn(new JiraBuildAction(Collections.singleton(new JiraIssue("DUMMY-1", TITLE)))); MarkupText text = new MarkupText("marking up DUMMY-1."); JiraChangeLogAnnotator annotator = spy(new JiraChangeLogAnnotator()); doReturn(site).when(annotator).getSiteForProject(Mockito.any()); annotator.annotate(run, null, text); // make sure '$' didn't confuse the JiraChangeLogAnnotator assertThat(text.toString(false), containsString(TITLE)); assertThat(text.toString(false), containsString("href")); } @Test void annotateDisabledOnSiteLevel() { JiraChangeLogAnnotator annotator = spy(new JiraChangeLogAnnotator()); doReturn(site).when(annotator).getSiteForProject(Mockito.any()); doReturn(true).when(site).getDisableChangelogAnnotations(); MarkupText text = new MarkupText("marking up DUMMY-1."); annotator.annotate(run, null, text); assertThat(text.toString(false), not(containsString("href"))); } @Test void annotateWf() { when(run.getAction(JiraBuildAction.class)) .thenReturn(new JiraBuildAction(Collections.singleton(new JiraIssue("DUMMY-1", TITLE)))); MarkupText text = new MarkupText("marking up DUMMY-1."); JiraChangeLogAnnotator annotator = spy(new JiraChangeLogAnnotator()); doReturn(site).when(annotator).getSiteForProject(Mockito.any()); annotator.annotate(run, null, text); // make sure '$' didn't confuse the JiraChangeLogAnnotator assertThat(text.toString(false), containsString(TITLE)); } /** * Jenkins' MarkupText#findTokens() doesn't work in our case if * the whole pattern matches the following word boundary character * (but not matching group 1). * Regression test for this. */ @Test void wordBoundaryProblem() { JiraChangeLogAnnotator annotator = spy(new JiraChangeLogAnnotator()); doReturn(site).when(annotator).getSiteForProject(Mockito.any()); // old changelog annotator used MarkupText#findTokens // That broke because of the space after the issue id. MarkupText text = new MarkupText("DUMMY-4071 Text "); annotator.annotate(run, null, text); assertThat(text.toString(false), is("DUMMY-4071 Text ")); text = new MarkupText("DUMMY-1,comment"); annotator.annotate(run, null, text); assertThat(text.toString(false), is("DUMMY-1,comment")); text = new MarkupText("DUMMY-1.comment"); annotator.annotate(run, null, text); assertThat(text.toString(false), is("DUMMY-1.comment")); text = new MarkupText("DUMMY-1!comment"); annotator.annotate(run, null, text); assertThat(text.toString(false), is("DUMMY-1!comment")); text = new MarkupText("DUMMY-1\tcomment"); annotator.annotate(run, null, text); assertThat(text.toString(false), is("DUMMY-1\tcomment")); } @Test void matchMultipleIssueIds() { JiraChangeLogAnnotator annotator = spy(new JiraChangeLogAnnotator()); doReturn(site).when(annotator).getSiteForProject(Mockito.any()); MarkupText text = new MarkupText("DUMMY-1 Text DUMMY-2,DUMMY-3 DUMMY-4!"); annotator.annotate(run, null, text); assertThat( text.toString(false), is("DUMMY-1 Text " + "DUMMY-2," + "DUMMY-3 " + "DUMMY-4!")); } @Test void hasProjectForIssueIsCaseInsensitive() { JiraChangeLogAnnotator annotator = spy(new JiraChangeLogAnnotator()); doReturn(site).when(annotator).getSiteForProject(Mockito.any()); MarkupText text = new MarkupText("fixed DUMMY-42"); annotator.annotate(mock(Run.class), null, text); assertThat(annotator.hasProjectForIssue("JENKINS-123", site, run), is(true)); assertThat(annotator.hasProjectForIssue("jenKiNs-123", site, run), is(true)); assertThat(annotator.hasProjectForIssue("dummy-4711", site, run), is(true)); assertThat(annotator.hasProjectForIssue("OThEr-4711", site, run), is(false)); } @Test @Issue("4132") void caseInsensitiveAnnotate() { JiraChangeLogAnnotator annotator = spy(new JiraChangeLogAnnotator()); doReturn(site).when(annotator).getSiteForProject(Mockito.any()); MarkupText text = new MarkupText("fixed DUMMY-42"); annotator.annotate(mock(Run.class), null, text); assertThat(text.toString(false), is("fixed DUMMY-42")); } /** * Tests that missing issues - i.e. issues not saved to build - * are fetched from remote. */ @Test @Issue("5252") void getIssueDetailsForMissingIssues() throws IOException { Run run = mock(Run.class); JiraChangeLogAnnotator annotator = spy(new JiraChangeLogAnnotator()); doReturn(site).when(annotator).getSiteForProject(Mockito.any()); JiraIssue issue = new JiraIssue("DUMMY-42", TITLE); when(site.getIssue(Mockito.anyString())).thenReturn(issue); MarkupText text = new MarkupText("fixed DUMMY-42"); annotator.annotate(run, null, text); assertThat(text.toString(false), containsString(TITLE)); } /** * Tests that no exception is thrown if user issue pattern is invalid (contains * no groups) */ @Test void invalidUserPattern() { JiraChangeLogAnnotator annotator = spy(new JiraChangeLogAnnotator()); when(site.getIssuePattern()).thenReturn(Pattern.compile("[a-zA-Z][a-zA-Z0-9_]+-[1-9][0-9]*")); doReturn(site).when(annotator).getSiteForProject(Mockito.any()); MarkupText text = new MarkupText("fixed DUMMY-42"); annotator.annotate(mock(Run.class), null, text); assertThat(text.toString(false), not(containsString(TITLE))); } /** * Tests that only the 1st matching group is hyperlinked and not the whole * pattern. * Previous implementation did so. */ @Test void matchOnlyMatchGroup1() throws IOException { JiraChangeLogAnnotator annotator = spy(new JiraChangeLogAnnotator()); doReturn(site).when(annotator).getSiteForProject(Mockito.any()); when(site.getIssuePattern()).thenReturn(Pattern.compile("([a-zA-Z][a-zA-Z0-9_]+-[1-9][0-9]*)abc")); MarkupText text = new MarkupText("fixed DUMMY-42abc"); annotator.annotate(mock(Run.class), null, text); assertThat(text.toString(false), is("fixed DUMMY-42abc")); // check again when issue != null: JiraIssue issue = new JiraIssue("DUMMY-42", TITLE); when(site.getIssue(Mockito.anyString())).thenReturn(issue); text = new MarkupText("fixed DUMMY-42abc"); annotator.annotate(mock(Run.class), null, text); assertThat( text.toString(false), is( "fixed DUMMY-42abc")); } /** * Tests that setting the "alternative Url" property actually * changes the link also. */ @Test void alternativeURLAnnotate() throws Exception { when(site.getAlternativeUrl(Mockito.anyString())).thenAnswer((Answer) invocation -> { String id = invocation.getArguments()[0].toString(); return new URL("http://altdummy/" + id); }); Run run = mock(Run.class); when(run.getAction(JiraBuildAction.class)) .thenReturn(new JiraBuildAction(Collections.singleton(new JiraIssue("DUMMY-1", TITLE)))); MarkupText text = new MarkupText("marking up DUMMY-1."); JiraChangeLogAnnotator annotator = spy(new JiraChangeLogAnnotator()); doReturn(site).when(annotator).getSiteForProject(Mockito.any()); annotator.annotate(run, null, text); assertThat(text.toString(false), containsString(" jiraComponents = new ArrayList<>(); Launcher launcher = mock(Launcher.class); BuildListener buildListener = mock(BuildListener.class); PrintStream logger = mock(PrintStream.class); JiraSite site = mock(JiraSite.class); JiraSession session = mock(JiraSession.class); EnvVars env; FreeStyleProject project = mock(FreeStyleProject.class); AbstractBuild previousBuild = mock(FreeStyleBuild.class); AbstractBuild currentBuild = mock(FreeStyleBuild.class); File temporaryDirectory; @TempDir public File temporaryFolder; @BeforeEach void createCommonMocks() throws IOException, InterruptedException { env = new EnvVars(); env.put("BUILD_NUMBER", "10"); env.put("BUILD_URL", "/some/url/to/job"); env.put("JOB_NAME", "Some job"); env.put("DESCRIPTION", DESCRIPTION); jiraComponents.add(new Component(null, null, "componentA", null, null)); when(currentBuild.getParent()).thenReturn(project); when(site.getSession(currentBuild.getParent())).thenReturn(session); when(site.getSession(previousBuild.getParent())).thenReturn(session); doReturn(env).when(currentBuild).getEnvironment(Mockito.any()); temporaryDirectory = newFolder(temporaryFolder, "junit"); when(project.getBuildDir()).thenReturn(temporaryDirectory); when(currentBuild.getProject()).thenReturn(project); when(currentBuild.getEnvironment(buildListener)).thenReturn(env); when(currentBuild.getPreviousCompletedBuild()).thenReturn(previousBuild); when(buildListener.getLogger()).thenReturn(logger); when(session.getComponents(Mockito.anyString())).thenReturn(jiraComponents); } @Test @WithoutJenkins void performSuccessFailure() throws Exception { when(previousBuild.getResult()).thenReturn(Result.SUCCESS); when(currentBuild.getResult()).thenReturn(Result.FAILURE); JiraCreateIssueNotifier notifier = spy(new JiraCreateIssueNotifier(JIRA_PROJECT, "", "", "", 1L, 1L, 1)); doReturn(site).when(notifier).getSiteForProject(Mockito.any()); Issue issue = mock(Issue.class); when(session.createIssue( Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyIterable(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyLong())) .thenReturn(issue); assertTrue(notifier.perform(currentBuild, launcher, buildListener)); } @Test @WithoutJenkins void performSuccessFailureWithEnv() throws Exception { when(previousBuild.getResult()).thenReturn(Result.SUCCESS); when(currentBuild.getResult()).thenReturn(Result.FAILURE); JiraCreateIssueNotifier notifier = spy(new JiraCreateIssueNotifier(JIRA_PROJECT, DESCRIPTION_PARAM, "", "", 1L, 1L, 1)); doReturn(site).when(notifier).getSiteForProject(Mockito.any()); Issue issue = mock(Issue.class); when(session.createIssue( Mockito.anyString(), contains(DESCRIPTION), Mockito.anyString(), Mockito.anyIterable(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyLong())) .thenReturn(issue); assertTrue(notifier.perform(currentBuild, launcher, buildListener)); } @Test @WithoutJenkins void performFailureFailure() throws Exception { JiraCreateIssueNotifier notifier = spy(new JiraCreateIssueNotifier(JIRA_PROJECT, DESCRIPTION, ASSIGNEE, COMPONENT, 1L, 1L, 1)); doReturn(site).when(notifier).getSiteForProject(Mockito.any()); Issue issue = mock(Issue.class); Status status = new Status(null, null, "1", "Open", null, null); when(session.createIssue( Mockito.anyString(), contains(DESCRIPTION), Mockito.anyString(), Mockito.anyIterable(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyLong())) .thenReturn(issue); when(session.getIssueByKey(Mockito.anyString())).thenReturn(issue); when(issue.getStatus()).thenReturn(status); assertEquals(0, temporaryDirectory.list().length); when(previousBuild.getResult()).thenReturn(Result.SUCCESS); when(currentBuild.getResult()).thenReturn(Result.FAILURE); assertTrue(notifier.perform(currentBuild, launcher, buildListener)); assertEquals(1, temporaryDirectory.list().length); when(previousBuild.getResult()).thenReturn(Result.FAILURE); when(currentBuild.getResult()).thenReturn(Result.FAILURE); assertTrue(notifier.perform(currentBuild, launcher, buildListener)); assertEquals(1, temporaryDirectory.list().length); when(issue.getStatus()) .thenReturn(new Status( null, null, "6", JiraCreateIssueNotifier.finishedStatuses.Closed.toString(), null, null)); assertTrue(notifier.perform(currentBuild, launcher, buildListener)); assertEquals(1, temporaryDirectory.list().length); } @Test @WithoutJenkins void performFailureSuccessIssueOpen() throws Exception { Long typeId = 1L; Long priorityId = 0L; Integer actionIdOnSuccess = 5; JiraCreateIssueNotifier notifier = spy(new JiraCreateIssueNotifier(JIRA_PROJECT, "", "", "", typeId, priorityId, actionIdOnSuccess)); assertEquals(typeId, notifier.getTypeId()); assertEquals(priorityId, notifier.getPriorityId()); assertEquals(actionIdOnSuccess, notifier.getActionIdOnSuccess()); doReturn(site).when(notifier).getSiteForProject(Mockito.any()); Issue issue = mock(Issue.class); Status status = new Status(null, null, "1", "Open", null, null); when(session.createIssue( Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyIterable(), Mockito.anyString(), Mockito.eq(typeId), Mockito.isNull())) .thenReturn(issue); when(issue.getStatus()).thenReturn(status); when(session.getIssueByKey(Mockito.anyString())).thenReturn(issue); assertEquals(0, temporaryDirectory.list().length); when(previousBuild.getResult()).thenReturn(Result.SUCCESS); when(currentBuild.getResult()).thenReturn(Result.FAILURE); assertTrue(notifier.perform(currentBuild, launcher, buildListener)); assertEquals(1, temporaryDirectory.list().length); when(previousBuild.getResult()).thenReturn(Result.FAILURE); when(currentBuild.getResult()).thenReturn(Result.SUCCESS); assertTrue(notifier.perform(currentBuild, launcher, buildListener)); verify(session).progressWorkflowAction("null", actionIdOnSuccess); assertEquals(1, temporaryDirectory.list().length); } @Test @WithoutJenkins void performFailureSuccessIssueClosedWithComponents() throws Exception { JiraCreateIssueNotifier notifier = spy(new JiraCreateIssueNotifier(JIRA_PROJECT, "", "", "", 1L, 1L, 1)); doReturn(site).when(notifier).getSiteForProject(Mockito.any()); Issue issue = mock(Issue.class); Status status = new Status(null, null, JiraCreateIssueNotifier.finishedStatuses.Closed.toString(), null, null, null); when(session.createIssue( Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyIterable(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyLong())) .thenReturn(issue); when(issue.getStatus()).thenReturn(status); when(session.getIssueByKey(Mockito.anyString())).thenReturn(issue); assertEquals(0, temporaryDirectory.list().length); when(previousBuild.getResult()).thenReturn(Result.SUCCESS); when(currentBuild.getResult()).thenReturn(Result.FAILURE); assertTrue(notifier.perform(currentBuild, launcher, buildListener)); assertEquals(1, temporaryDirectory.list().length); when(previousBuild.getResult()).thenReturn(Result.FAILURE); when(currentBuild.getResult()).thenReturn(Result.SUCCESS); assertTrue(notifier.perform(currentBuild, launcher, buildListener)); // file should be deleted assertEquals(0, temporaryDirectory.list().length); } @Test @WithoutJenkins void isDone() { assertTrue(JiraCreateIssueNotifier.isDone(new Status(null, null, "Closed", null, null, null))); assertTrue(JiraCreateIssueNotifier.isDone(new Status(null, null, "Done", null, null, null))); assertTrue(JiraCreateIssueNotifier.isDone(new Status(null, null, "Resolved", null, null, null))); assertTrue(JiraCreateIssueNotifier.isDone( new Status(null, null, "Abandoned", null, null, new StatusCategory(null, "Done", null, "done", null)))); assertFalse(JiraCreateIssueNotifier.isDone( new Status(null, null, "Abandoned", null, null, new StatusCategory(null, "ToDo", null, "todo", null)))); } @Test void doFillPriorityIdItems(JenkinsRule j) throws Exception { String credId_1 = "cred-1-id"; String credId_2 = "cred-2-id"; String pwd1 = "pwd1"; String pwd2 = "pwd2"; UsernamePasswordCredentialsImpl cred1 = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, credId_1, null, "user1", pwd1); UsernamePasswordCredentialsImpl cred2 = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, credId_2, null, "user2", pwd2); SystemCredentialsProvider systemProvider = SystemCredentialsProvider.getInstance(); systemProvider.getCredentials().add(cred1); systemProvider.save(); { // test at project level URL url = new URL("https://pacific-ale.com.au"); JiraSite jiraSite = mock(JiraSite.class); when(jiraSite.getUrl()).thenReturn(url); when(jiraSite.getCredentialsId()).thenReturn(credId_1); when(jiraSite.getName()).thenReturn(url.toExternalForm()); JiraSession jiraSession = mock(JiraSession.class); when(jiraSession.getPriorities()) .thenReturn(Collections.singletonList(new Priority(null, 2L, "priority-1", null, null, null))); when(jiraSite.getSession(any())).thenReturn(jiraSession); JiraGlobalConfiguration.get().setSites(Collections.singletonList(jiraSite)); FreeStyleProject p = j.jenkins.createProject( FreeStyleProject.class, "p" + j.jenkins.getItems().size()); ListBoxModel options = JiraCreateIssueNotifier.DESCRIPTOR.doFillPriorityIdItems(p); assertNotNull(options); assertThat(options.size(), Matchers.equalTo(2)); assertThat(options.get(1).value, Matchers.equalTo("2")); assertThat(options.get(1).name, Matchers.containsString("priority-1")); assertThat(options.get(1).name, Matchers.containsString("https://pacific-ale.com.au")); } { // test at folder level Folder folder = j.jenkins.createProject( Folder.class, "folder" + j.jenkins.getItems().size()); CredentialsStore folderStore = JiraFolderPropertyTest.getFolderStore(folder); folderStore.addCredentials(Domain.global(), cred2); JiraFolderProperty foo = new JiraFolderProperty(); JiraSite jiraSite = mock(JiraSite.class); URL url = new URL("https://pale-ale.com.au"); when(jiraSite.getUrl()).thenReturn(url); when(jiraSite.getCredentialsId()).thenReturn(credId_2); when(jiraSite.getName()).thenReturn(url.toExternalForm()); JiraSession jiraSession = mock(JiraSession.class); when(jiraSession.getPriorities()) .thenReturn(Collections.singletonList(new Priority(null, 3L, "priority-2", null, null, null))); when(jiraSite.getSession(any())).thenReturn(jiraSession); foo.setSites(Collections.singletonList(jiraSite)); folder.getProperties().add(foo); ListBoxModel options = JiraCreateIssueNotifier.DESCRIPTOR.doFillPriorityIdItems(folder); assertNotNull(options); assertEquals(2, options.size()); assertEquals("3", options.get(1).value); assertTrue(options.get(1).name.contains("priority-2")); assertTrue(options.get(1).name.contains("https://pale-ale.com.au")); } } @Test void doFillTypeItems(JenkinsRule j) throws Exception { String credId_1 = "cred-1-id"; String credId_2 = "cred-2-id"; String pwd1 = "pwd1"; String pwd2 = "pwd2"; UsernamePasswordCredentialsImpl cred1 = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, credId_1, null, "user1", pwd1); UsernamePasswordCredentialsImpl cred2 = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, credId_2, null, "user2", pwd2); SystemCredentialsProvider systemProvider = SystemCredentialsProvider.getInstance(); systemProvider.getCredentials().add(cred1); systemProvider.save(); { // test at project level URL url = new URL("https://pacific-ale.com.au"); JiraSite jiraSite = mock(JiraSite.class); when(jiraSite.getUrl()).thenReturn(url); when(jiraSite.getCredentialsId()).thenReturn(credId_1); when(jiraSite.getName()).thenReturn(url.toExternalForm()); JiraSession jiraSession = mock(JiraSession.class); when(jiraSession.getIssueTypes()) .thenReturn(Collections.singletonList(new IssueType(null, 1L, "type-1", true, null, null))); when(jiraSite.getSession(any())).thenReturn(jiraSession); JiraGlobalConfiguration.get().setSites(Collections.singletonList(jiraSite)); FreeStyleProject p = j.jenkins.createProject( FreeStyleProject.class, "p" + j.jenkins.getItems().size()); ListBoxModel options = JiraCreateIssueNotifier.DESCRIPTOR.doFillTypeIdItems(p); assertNotNull(options); assertThat(options.size(), Matchers.equalTo(2)); assertThat(options.get(1).value, Matchers.equalTo("1")); assertThat(options.get(1).name, Matchers.containsString("type-1")); assertThat(options.get(1).name, Matchers.containsString("https://pacific-ale.com.au")); } { // test at folder level Folder folder = j.jenkins.createProject( Folder.class, "folder" + j.jenkins.getItems().size()); CredentialsStore folderStore = JiraFolderPropertyTest.getFolderStore(folder); folderStore.addCredentials(Domain.global(), cred2); JiraFolderProperty foo = new JiraFolderProperty(); JiraSite jiraSite = mock(JiraSite.class); URL url = new URL("https://pale-ale.com.au"); when(jiraSite.getUrl()).thenReturn(url); when(jiraSite.getCredentialsId()).thenReturn(credId_2); when(jiraSite.getName()).thenReturn(url.toExternalForm()); JiraSession jiraSession = mock(JiraSession.class); when(jiraSession.getIssueTypes()) .thenReturn(Collections.singletonList(new IssueType(null, 2L, "type-2", false, null, null))); when(jiraSite.getSession(any())).thenReturn(jiraSession); foo.setSites(Collections.singletonList(jiraSite)); folder.getProperties().add(foo); ListBoxModel options = JiraCreateIssueNotifier.DESCRIPTOR.doFillTypeIdItems(folder); assertNotNull(options); assertEquals(2, options.size()); assertEquals("2", options.get(1).value); assertTrue(options.get(1).name.contains("type-2")); assertTrue(options.get(1).name.contains("https://pale-ale.com.au")); } } @Test @WithoutJenkins void testCreateIssueNotifierExceptionLogging() throws Exception { when(previousBuild.getResult()).thenReturn(Result.SUCCESS); when(currentBuild.getResult()).thenReturn(Result.FAILURE); Throwable throwable = mock(Throwable.class); when(buildListener.getLogger()).thenReturn(logger); JiraCreateIssueNotifier notifier = spy(new JiraCreateIssueNotifier(JIRA_PROJECT, DESCRIPTION_PARAM, "", "", 1L, 1L, 1)); doReturn(site).when(notifier).getSiteForProject(Mockito.any()); doThrow(new RestClientException("[Jira] Jira REST createIssue error. Cause: 401 error", throwable)) .when(session) .createIssue( Mockito.anyString(), contains(DESCRIPTION), Mockito.anyString(), Mockito.anyIterable(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyLong()); notifier.perform(currentBuild, launcher, buildListener); verify(logger).println("[Jira] Jira REST createIssue error. Cause: 401 error"); } private static File newFolder(File root, String... subDirs) throws IOException { String subFolder = String.join("/", subDirs); File result = new File(root, subFolder); if (!result.mkdirs()) { throw new IOException("Couldn't create folders " + root); } return result; } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraCreateReleaseNotesTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.atlassian.jira.rest.client.api.domain.Issue; import com.atlassian.jira.rest.client.api.domain.IssueType; import com.atlassian.jira.rest.client.api.domain.Status; import hudson.EnvVars; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Item; import hudson.model.Result; import hudson.tasks.BuildWrapper; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeoutException; import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class JiraCreateReleaseNotesTest { private static final String JIRA_RELEASE = Long.toString(System.currentTimeMillis()); private static final String JIRA_PRJ = "TEST_PRJ"; private static final String JIRA_RELEASE_PARAM = "${JIRA_RELEASE}"; private static final String JIRA_PRJ_PARAM = "${JIRA_PRJ}"; private static final String JIRA_VARIABLE = "ReleaseNotes"; private static final String JIRA_OTHER_FILTER = "status in (Resolved, Done, Closed)"; @Mock(strictness = Mock.Strictness.LENIENT) AbstractBuild build; @Mock Launcher launcher; @Mock(strictness = Mock.Strictness.LENIENT) BuildListener buildListener; @Mock(strictness = Mock.Strictness.LENIENT) EnvVars env; @Mock AbstractProject project; @Mock JiraSite site; private final PrintWriter printWriter = new PrintWriter(OutputStream.nullOutputStream()); @Mock JiraSession session; @Mock Item mockItem; @BeforeEach void createCommonMocks() throws IOException, InterruptedException { when(build.getEnvironment(buildListener)).thenReturn(env); when(buildListener.fatalError(Mockito.anyString(), Mockito.any(Object[].class))) .thenReturn(printWriter); when(env.expand(Mockito.anyString())).thenAnswer(invocationOnMock -> { Object[] args = invocationOnMock.getArguments(); String expanded = (String) args[0]; if (expanded.equals(JIRA_PRJ_PARAM)) { return JIRA_PRJ; } else if (expanded.equals(JIRA_RELEASE_PARAM)) { return JIRA_RELEASE; } else { return expanded; } }); when(site.createSession(any(), anyBoolean())).thenReturn(session); when(site.getSession(any(), anyBoolean())).thenCallRealMethod(); site.getSession(mockItem, false); } @Test void defaults() { JiraCreateReleaseNotes jcrn = new JiraCreateReleaseNotes(JIRA_PRJ, JIRA_RELEASE, ""); assertEquals(JiraCreateReleaseNotes.DEFAULT_ENVVAR_NAME, jcrn.getJiraEnvironmentVariable()); assertEquals(JiraCreateReleaseNotes.DEFAULT_FILTER, jcrn.getJiraFilter()); } @Test void jiraApiCallDefaultFilter() throws InterruptedException, IOException, TimeoutException { JiraCreateReleaseNotes jcrn = spy(new JiraCreateReleaseNotes(JIRA_PRJ, JIRA_RELEASE, JIRA_VARIABLE)); doReturn(site).when(jcrn).getSiteForProject(Mockito.any()); jcrn.setUp(build, launcher, buildListener); verify(site).getReleaseNotesForFixVersion(JIRA_PRJ, JIRA_RELEASE, JiraCreateReleaseNotes.DEFAULT_FILTER); } @Test void jiraApiCallOtherFilter() throws InterruptedException, IOException, TimeoutException { JiraCreateReleaseNotes jcrn = spy(new JiraCreateReleaseNotes(JIRA_PRJ, JIRA_RELEASE, JIRA_VARIABLE, JIRA_OTHER_FILTER)); doReturn(site).when(jcrn).getSiteForProject(Mockito.any()); BuildListenerResultMethodMock finishedListener = new BuildListenerResultMethodMock(); jcrn.setUp(build, launcher, buildListener); verify(site).getReleaseNotesForFixVersion(JIRA_PRJ, JIRA_RELEASE, JIRA_OTHER_FILTER); // assert that build not fail assertThat(finishedListener.getResult(), Matchers.nullValue()); } @Test void failBuildOnErrorEmptyProjectKey() throws InterruptedException, IOException { JiraCreateReleaseNotes jcrn = spy(new JiraCreateReleaseNotes("", JIRA_RELEASE, JIRA_VARIABLE, JIRA_OTHER_FILTER)); doReturn(site).when(jcrn).getSiteForProject(Mockito.any()); BuildListenerResultMethodMock finishedListener = new BuildListenerResultMethodMock(); Mockito.doAnswer(finishedListener).when(buildListener).finished(Mockito.any()); jcrn.setUp(build, launcher, buildListener); assertThat(finishedListener.getResult(), Matchers.equalTo(Result.FAILURE)); } @Test void failBuildOnErrorEmptyRelease() throws InterruptedException, IOException { JiraCreateReleaseNotes jcrn = spy(new JiraCreateReleaseNotes(JIRA_PRJ, "", JIRA_VARIABLE, JIRA_OTHER_FILTER)); doReturn(site).when(jcrn).getSiteForProject(Mockito.any()); BuildListenerResultMethodMock finishedListener = new BuildListenerResultMethodMock(); Mockito.doAnswer(finishedListener).when(buildListener).finished(Mockito.any()); jcrn.setUp(build, launcher, buildListener); assertThat(finishedListener.getResult(), Matchers.equalTo(Result.FAILURE)); } @Test void releaseNotesContent() throws Exception { JiraCreateReleaseNotes jcrn = spy(new JiraCreateReleaseNotes(JIRA_PRJ, JIRA_RELEASE, JIRA_VARIABLE)); doReturn(site).when(jcrn).getSiteForProject(Mockito.any()); when(site.getReleaseNotesForFixVersion(JIRA_PRJ, JIRA_RELEASE, JiraCreateReleaseNotes.DEFAULT_FILTER)) .thenCallRealMethod(); Issue issue1 = Mockito.mock(Issue.class); IssueType issueType1 = Mockito.mock(IssueType.class); Status issueStatus = Mockito.mock(Status.class); when(issue1.getIssueType()).thenReturn(issueType1); when(issue1.getStatus()).thenReturn(issueStatus); when(issueType1.getName()).thenReturn("Bug"); Issue issue2 = Mockito.mock(Issue.class); IssueType issueType2 = Mockito.mock(IssueType.class); when(issue2.getIssueType()).thenReturn(issueType2); when(issue2.getStatus()).thenReturn(issueStatus); when(issueType2.getName()).thenReturn("Feature"); when(session.getIssuesWithFixVersion(JIRA_PRJ, JIRA_RELEASE, JiraCreateReleaseNotes.DEFAULT_FILTER)) .thenReturn(Arrays.asList(issue1, issue2)); BuildWrapper.Environment environment = jcrn.setUp(build, launcher, buildListener); Map envVars = new HashMap<>(); environment.buildEnvVars(envVars); String releaseNotes = envVars.get(jcrn.getJiraEnvironmentVariable()); assertNotNull(releaseNotes); assertThat(releaseNotes, Matchers.containsString(issueType1.getName())); assertThat(releaseNotes, Matchers.containsString(issueType2.getName())); assertThat(releaseNotes, Matchers.not(Matchers.containsString("UNKNOWN"))); } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraEnvironmentContributingActionTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import hudson.EnvVars; import hudson.model.AbstractBuild; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; class JiraEnvironmentContributingActionTest { private static final String JIRA_URL = "http://example.com"; private static final String ISSUES_LIST = "ISS-1,ISS-2"; private static final Integer ISSUES_SIZE = 2; @Test public void buildEnvVarsEnvIsNull() { JiraEnvironmentContributingAction action = new JiraEnvironmentContributingAction(ISSUES_LIST, ISSUES_SIZE, JIRA_URL); AbstractBuild build = mock(AbstractBuild.class); action.buildEnvVars(build, null); // just expecting no exception } @Test public void passedVariablesAreNull() { JiraEnvironmentContributingAction action = new JiraEnvironmentContributingAction(ISSUES_LIST, ISSUES_SIZE, JIRA_URL); AbstractBuild build = mock(AbstractBuild.class); action.buildEnvVars(build, null); // just expecting no exception } @Test public void buildEnvVarsAddVariables() { JiraEnvironmentContributingAction action = new JiraEnvironmentContributingAction(ISSUES_LIST, ISSUES_SIZE, JIRA_URL); AbstractBuild build = mock(AbstractBuild.class); EnvVars envVars = mock(EnvVars.class); action.buildEnvVars(build, envVars); ArgumentCaptor keys = ArgumentCaptor.forClass(String.class); ArgumentCaptor values = ArgumentCaptor.forClass(String.class); verify(envVars, times(3)).put(keys.capture(), values.capture()); assertThat(keys.getAllValues().get(0), is(JiraEnvironmentContributingAction.ISSUES_VARIABLE_NAME)); assertThat(values.getAllValues().get(0), is(ISSUES_LIST)); assertThat(keys.getAllValues().get(1), is(JiraEnvironmentContributingAction.ISSUES_SIZE_VARIABLE_NAME)); assertThat(values.getAllValues().get(1), is(ISSUES_SIZE.toString())); assertThat(keys.getAllValues().get(2), is(JiraEnvironmentContributingAction.JIRA_URL_VARIABLE_NAME)); assertThat(values.getAllValues().get(2), is(JIRA_URL)); } @Test public void noExceptionWhenNullsPassed() { JiraEnvironmentContributingAction action = new JiraEnvironmentContributingAction(null, null, null); AbstractBuild build = mock(AbstractBuild.class); EnvVars envVars = mock(EnvVars.class); action.buildEnvVars(build, envVars); ArgumentCaptor keys = ArgumentCaptor.forClass(String.class); ArgumentCaptor values = ArgumentCaptor.forClass(String.class); verify(envVars, times(3)).put(keys.capture(), values.capture()); assertThat(keys.getAllValues().get(0), is(JiraEnvironmentContributingAction.ISSUES_VARIABLE_NAME)); assertThat(values.getAllValues().get(0), nullValue()); assertThat(keys.getAllValues().get(1), is(JiraEnvironmentContributingAction.ISSUES_SIZE_VARIABLE_NAME)); assertThat(values.getAllValues().get(1), is("0")); assertThat(keys.getAllValues().get(2), is(JiraEnvironmentContributingAction.JIRA_URL_VARIABLE_NAME)); assertThat(values.getAllValues().get(2), nullValue()); } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraEnvironmentVariableBuilderTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.CoreMatchers.anyOf; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.*; import com.atlassian.jira.rest.client.api.RestClientException; import hudson.EnvVars; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Action; import hudson.model.BuildListener; import hudson.model.Node; import hudson.plugins.jira.selector.AbstractIssueSelector; import hudson.plugins.jira.selector.DefaultIssueSelector; import hudson.plugins.jira.selector.ExplicitIssueSelector; import java.io.IOException; import java.io.PrintStream; import java.util.Arrays; import java.util.LinkedHashSet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.WithoutJenkins; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.mockito.ArgumentCaptor; @WithJenkins class JiraEnvironmentVariableBuilderTest { private static final String JIRA_URL = "http://example.com"; private static final String ISSUE_ID_1 = "ISS-1"; private static final String ISSUE_ID_2 = "ISS-2"; // Ordering of set created from collection intializer seems to depend on which JDK is used // but isn't important for this purpose private static final String EXPECTED_JIRA_ISSUES_1 = ISSUE_ID_1 + "," + ISSUE_ID_2; private static final String EXPECTED_JIRA_ISSUES_2 = ISSUE_ID_2 + "," + ISSUE_ID_1; AbstractBuild build; Launcher launcher; BuildListener listener; EnvVars env; AbstractProject project; JiraSite site; AbstractIssueSelector issueSelector; PrintStream logger; Node node; @BeforeEach void createMocks() throws IOException, InterruptedException { build = mock(AbstractBuild.class); launcher = mock(Launcher.class); listener = mock(BuildListener.class); env = mock(EnvVars.class); project = mock(AbstractProject.class); site = mock(JiraSite.class); issueSelector = mock(AbstractIssueSelector.class); logger = mock(PrintStream.class); when(site.getName()).thenReturn(JIRA_URL); when(listener.getLogger()).thenReturn(logger); when(issueSelector.findIssueIds(build, site, listener)) .thenReturn(new LinkedHashSet<>(Arrays.asList(ISSUE_ID_1, ISSUE_ID_2))); when(build.getProject()).thenReturn(project); when(build.getEnvironment(listener)).thenReturn(env); } @Test @WithoutJenkins public void testIssueSelectorDefaultsToDefault() { final JiraEnvironmentVariableBuilder builder = new JiraEnvironmentVariableBuilder(null); assertThat(builder.getIssueSelector(), instanceOf(DefaultIssueSelector.class)); } @Test @WithoutJenkins public void testSetIssueSelectorPersists() { final JiraEnvironmentVariableBuilder builder = new JiraEnvironmentVariableBuilder(issueSelector); assertThat(builder.getIssueSelector(), is(issueSelector)); } @Test @WithoutJenkins public void testPerformWithNoSiteFailsBuild() throws InterruptedException, IOException { JiraEnvironmentVariableBuilder builder = spy(new JiraEnvironmentVariableBuilder(issueSelector)); doReturn(null).when(builder).getSiteForProject(project); assertThat(builder.perform(build, launcher, listener), is(false)); verify(logger, times(1)).println(Messages.JiraEnvironmentVariableBuilder_NoJiraSite()); } @Test @WithoutJenkins public void testPerformAddsAction() throws InterruptedException, IOException { JiraEnvironmentVariableBuilder builder = spy(new JiraEnvironmentVariableBuilder(issueSelector)); doReturn(site).when(builder).getSiteForProject(project); boolean result = builder.perform(build, launcher, listener); assertThat(result, is(true)); ArgumentCaptor captor = ArgumentCaptor.forClass(Action.class); verify(build).addAction(captor.capture()); assertThat(captor.getValue(), instanceOf(JiraEnvironmentContributingAction.class)); JiraEnvironmentContributingAction action = (JiraEnvironmentContributingAction) (captor.getValue()); assertThat(action.getJiraUrl(), is(JIRA_URL)); assertThat(action.getIssuesList(), anyOf(is(EXPECTED_JIRA_ISSUES_1), is(EXPECTED_JIRA_ISSUES_2))); assertThat(action.getNumberOfIssues(), is(2)); } @Test public void testHasIssueSelectors_HasDefaultSelector(JenkinsRule r) { JiraEnvironmentVariableBuilder builder = new JiraEnvironmentVariableBuilder(null); assertThat(builder.getIssueSelector(), instanceOf(DefaultIssueSelector.class)); JiraEnvironmentVariableBuilder.DescriptorImpl descriptor = (JiraEnvironmentVariableBuilder.DescriptorImpl) r.jenkins.getDescriptor(builder.getClass()); assertTrue(descriptor.hasIssueSelectors()); } @Test public void testHasIssueSelectors(JenkinsRule r) { ExplicitIssueSelector explicitIssueSelector = new ExplicitIssueSelector(); JiraEnvironmentVariableBuilder builder = new JiraEnvironmentVariableBuilder(explicitIssueSelector); assertEquals(explicitIssueSelector, builder.getIssueSelector()); JiraEnvironmentVariableBuilder.DescriptorImpl descriptor = (JiraEnvironmentVariableBuilder.DescriptorImpl) r.jenkins.getDescriptor(builder.getClass()); assertTrue(descriptor.hasIssueSelectors()); } @Test @WithoutJenkins public void testEnvBuilderExceptionLogging(JenkinsRule r) throws IOException, InterruptedException { Throwable throwable = mock(Throwable.class); PrintStream logger = mock(PrintStream.class); when(listener.getLogger()).thenReturn(logger); doThrow(new RestClientException("[Jira] Jira REST findIssueIds error. Cause: 401 error", throwable)) .when(issueSelector) .findIssueIds(build, site, listener); JiraEnvironmentVariableBuilder builder = spy(new JiraEnvironmentVariableBuilder(issueSelector)); doReturn(site).when(builder).getSiteForProject(project); assertThat(builder.perform(build, launcher, listener), is(false)); verify(logger).println("[Jira] Jira REST findIssueIds error. Cause: 401 error"); } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraFolderPropertyTest.java ================================================ package hudson.plugins.jira; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import com.cloudbees.hudson.plugins.folder.Folder; import com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.CredentialsStore; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.StreamSupport; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; @WithJenkins public class JiraFolderPropertyTest { @Test void configRoundtrip(JenkinsRule r) throws Exception { Folder d = r.jenkins.createProject(Folder.class, "d"); r.configRoundtrip(d); assertNull(d.getProperties().get(JiraFolderProperty.class)); List list = new ArrayList<>(); list.add(new JiraSite("https://test.com")); JiraFolderProperty foo = new JiraFolderProperty(); foo.setSites(list); foo.setSites(new JiraSite("https://otherTest.com")); d.getProperties().add(foo); r.configRoundtrip(d); JiraFolderProperty prop = d.getProperties().get(JiraFolderProperty.class); assertNotNull(prop); List actual = Arrays.asList(prop.getSites()); r.assertEqualDataBoundBeans(list, actual); } public static CredentialsStore getFolderStore(Folder f) { return StreamSupport.stream(CredentialsProvider.lookupStores(f).spliterator(), false) .filter(s -> s.getProvider() instanceof FolderCredentialsProvider && s.getContext() == f) .findFirst() .orElse(null); } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraGlobalConfigurationSaveTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.hamcrest.core.Is.is; import java.net.URL; import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; @WithJenkins class JiraGlobalConfigurationSaveTest { @Test @Issue("JENKINS-57899") void jiraSitesListSaved(JenkinsRule r) throws Throwable { String jiraUrl = "https://issues.jenkins.io/"; JiraGlobalConfiguration jiraGlobalConfiguration = JiraGlobalConfiguration.get(); jiraGlobalConfiguration.setSites(Collections.singletonList(new JiraSite(jiraUrl))); List sites = jiraGlobalConfiguration.getSites(); assertThat(sites, is(hasSize(1))); JiraSite jiraSite = sites.get(0); URL url = jiraSite.getUrl(); assertThat(url, is(notNullValue())); assertThat(url.toString(), is(jiraUrl)); r.restart(); jiraGlobalConfiguration = JiraGlobalConfiguration.get(); sites = jiraGlobalConfiguration.getSites(); assertThat(sites, is(hasSize(1))); jiraSite = sites.get(0); url = jiraSite.getUrl(); assertThat(url, is(notNullValue())); assertThat(url.toString(), is(jiraUrl)); } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraGlobalConfigurationTest.java ================================================ package hudson.plugins.jira; import static org.junit.jupiter.api.Assertions.*; import com.thoughtworks.xstream.XStream; import hudson.plugins.jira.JiraProjectProperty.DescriptorImpl; import hudson.util.XStream2; import java.io.InputStream; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; @WithJenkins class JiraGlobalConfigurationTest { @Test void migrateOldConfiguration(JenkinsRule r) throws Exception { String url = "https://backwardsCompatURL.com/"; XStream xstream = new XStream2(); JiraSite expected = new JiraSite(url); InputStream resource = getClass().getResourceAsStream("oldJiraProjectProperty.xml"); DescriptorImpl instance = (DescriptorImpl) xstream.fromXML(resource); assertNotNull(instance); assertNull(instance.sites); JiraSite actual = JiraGlobalConfiguration.get().getSites().get(0); assertEquals(url, actual.getName()); r.assertEqualDataBoundBeans(expected, actual); } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraIssueMigratorTest.java ================================================ package hudson.plugins.jira; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.atlassian.jira.rest.client.api.RestClientException; import hudson.EnvVars; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import java.io.IOException; import java.util.concurrent.TimeoutException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class JiraIssueMigratorTest { private static final String PROJECT_KEY = "PROJECT"; private static final String RELEASE = "release"; private static final String RELEASE_TO_REPLACE = "releaseToReplace"; private static final String QUERY = "query"; AbstractBuild build; Launcher launcher; BuildListener listener; EnvVars envVars; JiraSite jiraSite; AbstractProject project; JiraIssueMigrator jiraIssueMigrator; @BeforeEach void prepareMocks() throws IOException, TimeoutException, InterruptedException { build = mock(AbstractBuild.class); launcher = mock(Launcher.class); listener = mock(BuildListener.class); envVars = mock(EnvVars.class); jiraSite = mock(JiraSite.class); project = mock(AbstractProject.class); when(build.getEnvironment(listener)).thenReturn(envVars); when(envVars.expand(PROJECT_KEY)).thenReturn(PROJECT_KEY); when(envVars.expand(RELEASE)).thenReturn(RELEASE); when(envVars.expand(QUERY)).thenReturn(QUERY); when(envVars.expand(RELEASE_TO_REPLACE)).thenReturn(RELEASE_TO_REPLACE); when(envVars.expand(null)).thenReturn(null); when(build.getProject()).thenReturn(project); } @Test void addingVersion() throws IOException, TimeoutException, RestClientException { boolean addRelease = true; jiraIssueMigrator = spy(new JiraIssueMigrator(PROJECT_KEY, RELEASE, QUERY, null, addRelease)); doReturn(jiraSite).when(jiraIssueMigrator).getJiraSiteForProject(project); boolean result = jiraIssueMigrator.perform(build, launcher, listener); verify(jiraSite, never()).migrateIssuesToFixVersion(anyString(), anyString(), anyString()); verify(jiraSite, never()).replaceFixVersion(anyString(), anyString(), anyString(), anyString()); verify(jiraSite, times(1)).addFixVersionToIssue(PROJECT_KEY, RELEASE, QUERY); assertTrue(result); } @Test void migratingToVersion() throws IOException, TimeoutException, RestClientException { jiraIssueMigrator = spy(new JiraIssueMigrator(PROJECT_KEY, RELEASE, QUERY, null, false)); doReturn(jiraSite).when(jiraIssueMigrator).getJiraSiteForProject(project); boolean result = jiraIssueMigrator.perform(build, launcher, listener); verify(jiraSite, never()).addFixVersionToIssue(anyString(), anyString(), anyString()); verify(jiraSite, never()).replaceFixVersion(anyString(), anyString(), anyString(), anyString()); verify(jiraSite, times(1)).migrateIssuesToFixVersion(PROJECT_KEY, RELEASE, QUERY); assertTrue(result); } @Test void replacingVersion() throws IOException, TimeoutException, RestClientException { jiraIssueMigrator = spy(new JiraIssueMigrator(PROJECT_KEY, RELEASE, QUERY, RELEASE_TO_REPLACE, false)); doReturn(jiraSite).when(jiraIssueMigrator).getJiraSiteForProject(project); boolean result = jiraIssueMigrator.perform(build, launcher, listener); verify(jiraSite, never()).addFixVersionToIssue(anyString(), anyString(), anyString()); verify(jiraSite, never()).migrateIssuesToFixVersion(anyString(), anyString(), anyString()); verify(jiraSite, times(1)).replaceFixVersion(PROJECT_KEY, RELEASE_TO_REPLACE, RELEASE, QUERY); assertTrue(result); } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraIssueParameterDefResultTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.atlassian.jira.rest.client.api.domain.Issue; import com.atlassian.jira.rest.client.api.domain.IssueField; import hudson.plugins.jira.listissuesparameter.JiraIssueParameterDefinition; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class JiraIssueParameterDefResultTest { private Issue issueMock; @BeforeEach void prepareMocks() { issueMock = mock(Issue.class); IssueField fieldMock1 = mock(IssueField.class); IssueField fieldMock2 = mock(IssueField.class); IssueField fieldMock3 = mock(IssueField.class); when(issueMock.getFieldByName("TestField1")).thenReturn(fieldMock1); when(issueMock.getFieldByName("TestField2")).thenReturn(fieldMock2); when(issueMock.getFieldByName("TestField3")).thenReturn(fieldMock3); when(issueMock.getFieldByName("TestField4")).thenReturn(null); when(issueMock.getSummary()).thenReturn("Summary"); when(fieldMock1.getValue()).thenReturn("Field1"); when(fieldMock2.getValue()).thenReturn("Field2"); when(fieldMock3.getValue()).thenReturn(""); } @Test void testSummaryResult() { JiraIssueParameterDefinition.Result result = new JiraIssueParameterDefinition.Result(issueMock, ""); assertThat("Summary", equalTo(result.summary)); } @Test void testAltSummaryResultCommaSep() { JiraIssueParameterDefinition.Result result = new JiraIssueParameterDefinition.Result(issueMock, "TestField1,TestField2"); assertThat("Field1 Field2", equalTo(result.summary)); } @Test void testAltSummaryResultCommaSpaceSep() { JiraIssueParameterDefinition.Result result = new JiraIssueParameterDefinition.Result(issueMock, "TestField1, TestField2"); assertThat("Field1 Field2", equalTo(result.summary)); } @Test void testAltSummaryResultMissingFieldIgnored() { JiraIssueParameterDefinition.Result result = new JiraIssueParameterDefinition.Result(issueMock, "TestField1, TestField4"); assertThat("Field1", equalTo(result.summary)); } @Test void testAltSummaryResultEmptyFieldIgnored() { JiraIssueParameterDefinition.Result result = new JiraIssueParameterDefinition.Result(issueMock, "TestField1, TestField3"); assertThat("Field1", equalTo(result.summary)); } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraIssueUpdateBuilderTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.*; import com.atlassian.jira.rest.client.api.RestClientException; import hudson.EnvVars; import hudson.FilePath; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Result; import hudson.model.TaskListener; import java.io.IOException; import java.io.PrintStream; import java.util.Collections; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; class JiraIssueUpdateBuilderTest { private Launcher launcher; private FilePath workspace; private TaskListener listener; private AbstractBuild build; private EnvVars env; private AbstractProject project; private PrintStream logger; private Result result; private JiraSite site; @BeforeEach void createMocks() throws IOException, InterruptedException { launcher = mock(Launcher.class); listener = mock(TaskListener.class); env = mock(EnvVars.class); project = mock(AbstractProject.class); logger = mock(PrintStream.class); build = mock(AbstractBuild.class); site = mock(JiraSite.class); when(build.getEnvironment(listener)).thenReturn(env); when(build.getParent()).thenReturn(project); when(listener.getLogger()).thenReturn(logger); result = Result.SUCCESS; doAnswer(invocation -> { Object[] args = invocation.getArguments(); result = (Result) args[0]; return null; }) .when(build) .setResult(any()); } @Test void performNoSite() throws InterruptedException, IOException { JiraIssueUpdateBuilder builder = spy(new JiraIssueUpdateBuilder(null, null, null)); doReturn(null).when(builder).getSiteForJob(any()); builder.perform(build, workspace, launcher, listener); assertThat(result, is(Result.FAILURE)); } @Test void validateFailureResult() throws InterruptedException, IOException { JiraIssueUpdateBuilder builder = spy(new JiraIssueUpdateBuilder(null, null, null)); Throwable throwable = mock(Throwable.class); doReturn(site).when(builder).getSiteForJob(any()); doThrow(new RestClientException("Verify failure result", throwable)) .when(site) .progressMatchingIssues(any(), any(), any(), any()); builder.perform(build, workspace, launcher, listener); assertThat(result, is(Result.FAILURE)); } @Test void performProgressFails() throws InterruptedException, IOException { JiraIssueUpdateBuilder builder = spy(new JiraIssueUpdateBuilder(null, null, null)); doReturn(site).when(builder).getSiteForJob(any()); doReturn(false).when(site).progressMatchingIssues(anyString(), anyString(), anyString(), any()); builder.perform(build, workspace, launcher, listener); assertThat(result, is(Result.UNSTABLE)); } @Test void performProgressOK() throws InterruptedException, IOException { JiraIssueUpdateBuilder builder = spy(new JiraIssueUpdateBuilder(null, null, null)); doReturn(site).when(builder).getSiteForJob(any()); doReturn(true).when(site).progressMatchingIssues(any(), any(), any(), any()); builder.perform(build, workspace, launcher, listener); assertThat(result, is(Result.SUCCESS)); } @WithJenkins @Test void testPipelineWithJiraSite(JenkinsRule r) throws Exception { JiraGlobalConfiguration jiraGlobalConfiguration = JiraGlobalConfiguration.get(); jiraGlobalConfiguration.setSites(Collections.singletonList(site)); doReturn(true).when(site).progressMatchingIssues(anyString(), anyString(), anyString(), any()); WorkflowJob job = r.createProject(WorkflowJob.class); job.setDefinition(new CpsFlowDefinition(""" jiraExecuteWorkflow(jqlSearch: 'search', workflowActionName: 'action', comment: 'comment') """, true)); WorkflowRun b = r.buildAndAssertStatus(Result.SUCCESS, job); r.assertLogContains("[Jira] Updating issues using workflow action action.", b); } @Test void testIssueUpdateBuilderRestException() throws InterruptedException, IOException { Throwable throwable = mock(Throwable.class); PrintStream logger = mock(PrintStream.class); when(listener.getLogger()).thenReturn(logger); JiraIssueUpdateBuilder builder = spy(new JiraIssueUpdateBuilder(null, null, null)); doReturn(site).when(builder).getSiteForJob(any()); doThrow(new RestClientException("[Jira] Jira REST progressMatchingIssues error. Cause: 401 error", throwable)) .when(site) .progressMatchingIssues(any(), any(), any(), any()); builder.perform(build, workspace, launcher, listener); verify(logger).println("[Jira] Jira REST progressMatchingIssues error. Cause: 401 error"); } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraIssueUpdaterTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.jira.selector.AbstractIssueSelector; import hudson.plugins.jira.selector.DefaultIssueSelector; import hudson.scm.ChangeLogParser; import hudson.scm.SCM; import java.util.Arrays; import java.util.List; import java.util.Set; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; class JiraIssueUpdaterTest { @Test void issueSelectorDefaultsToDefault() { final JiraIssueUpdater updater = new JiraIssueUpdater(null, null, null); assertThat(updater.getIssueSelector(), instanceOf(DefaultIssueSelector.class)); } @Test void setIssueSelectorPersists() { class TestSelector extends AbstractIssueSelector { @Override public Set findIssueIds(Run run, JiraSite site, TaskListener listener) { throw new UnsupportedOperationException("Not supported yet."); } } final JiraIssueUpdater updater = new JiraIssueUpdater(new TestSelector(), null, null); assertThat(updater.getIssueSelector(), instanceOf(TestSelector.class)); } @Test void setScmPersists() { class TestSCM extends SCM { @Override public ChangeLogParser createChangeLogParser() { throw new UnsupportedOperationException("Not supported yet."); } } final JiraIssueUpdater updater = new JiraIssueUpdater(null, new TestSCM(), null); assertThat(updater.getScm(), instanceOf(TestSCM.class)); } @Test void setLabelsPersists() { List testLabels = Arrays.asList("testLabel1", "testLabel2"); final JiraIssueUpdater updater = new JiraIssueUpdater(null, null, testLabels); assertThat(updater.getLabels(), is(testLabels)); } @WithJenkins @Test void testPipeline(JenkinsRule r) throws Exception { WorkflowJob job = r.createProject(WorkflowJob.class); job.setDefinition(new CpsFlowDefinition(""" jiraCommentIssues(issueSelector: DefaultSelector(), scm: null) """, true)); WorkflowRun b = r.buildAndAssertStatus(Result.FAILURE, job); r.assertLogContains(" Unsupported run type", b); } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraJobActionTest.java ================================================ package hudson.plugins.jira; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; import com.atlassian.jira.rest.client.api.RestClientException; import hudson.model.TaskListener; import hudson.plugins.jira.model.JiraIssue; import java.io.PrintStream; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.WithoutJenkins; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.mockito.MockedStatic; import org.mockito.Mockito; @WithJenkins class JiraJobActionTest { JiraSite site; WorkflowJob job; WorkflowMultiBranchProject mbp; final JiraIssue issue = new JiraIssue("EXAMPLE-123", "I like cake"); @BeforeEach void setup(JenkinsRule r) throws Exception { mbp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "mbp"); site = spy(new JiraSite("https://foo.com")); doReturn(JiraSite.DEFAULT_ISSUE_PATTERN).when(site).getIssuePattern(); doReturn(issue).when(site).getIssue("EXAMPLE-123"); } @Test void detectBranchNameIssue() throws Exception { job = new WorkflowJob(mbp, "feature/EXAMPLE-123"); JiraJobAction.setAction(job, site); JiraJobAction action = job.getAction(JiraJobAction.class); assertNotNull(action.getIssue()); assertEquals("EXAMPLE-123", action.getIssue().getKey()); assertEquals("I like cake", action.getIssue().getSummary()); } @Test void detectBranchNameIssueWithEncodedJobName() throws Exception { job = new WorkflowJob(mbp, "feature%2FEXAMPLE-123"); JiraJobAction.setAction(job, site); JiraJobAction action = job.getAction(JiraJobAction.class); assertNotNull(action.getIssue()); assertEquals("EXAMPLE-123", action.getIssue().getKey()); assertEquals("I like cake", action.getIssue().getSummary()); } @Test void detectBranchNameIssueJustIssueKey() throws Exception { job = new WorkflowJob(mbp, "EXAMPLE-123"); JiraJobAction.setAction(job, site); JiraJobAction action = job.getAction(JiraJobAction.class); assertNotNull(action.getIssue()); assertEquals("EXAMPLE-123", action.getIssue().getKey()); assertEquals("I like cake", action.getIssue().getSummary()); } @Test void detectBranchNameIssueNoIssueKey() throws Exception { job = new WorkflowJob(mbp, "NOTHING INTERESTING"); JiraJobAction.setAction(job, site); JiraJobAction action = job.getAction(JiraJobAction.class); assertNull(action); } @Test @WithoutJenkins void testJobActionRestException() { Throwable throwable = mock(Throwable.class); PrintStream logger = mock(PrintStream.class); TaskListener listener = mock(TaskListener.class); WorkflowRun run = mock(WorkflowRun.class); WorkflowJob parent = mock(WorkflowJob.class); when(listener.getLogger()).thenReturn(logger); when(run.getParent()).thenReturn(parent); try (MockedStatic jobActionMockedStatic = Mockito.mockStatic(JiraJobAction.class); MockedStatic jiraSiteMockedStatic = Mockito.mockStatic(JiraSite.class)) { jiraSiteMockedStatic.when(() -> JiraSite.get(parent)).thenReturn(site); jobActionMockedStatic .when(() -> JiraJobAction.setAction(parent, site)) .thenThrow( new RestClientException("[Jira] Jira REST setAction error. Cause: 401 error", throwable)); JiraJobAction.RunListenerImpl.fireStarted(run, listener); verify(logger).println("[Jira] Jira REST setAction error. Cause: 401 error"); } } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraProjectPropertyTest.java ================================================ package hudson.plugins.jira; import static org.junit.jupiter.api.Assertions.*; import com.cloudbees.hudson.plugins.folder.Folder; import hudson.model.FreeStyleProject; import io.jenkins.plugins.casc.misc.ConfiguredWithCode; import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule; import io.jenkins.plugins.casc.misc.junit.jupiter.WithJenkinsConfiguredWithCode; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; @WithJenkinsConfiguredWithCode class JiraProjectPropertyTest { private JenkinsRule r; private FreeStyleProject freeStyleProject; private Folder folder; private List firstList; @BeforeEach void initialize(JenkinsConfiguredWithCodeRule r) throws Exception { this.r = r; folder = r.jenkins.createProject(Folder.class, "first"); JiraFolderProperty jiraFolderProperty = new JiraFolderProperty(); firstList = new ArrayList<>(); firstList.add(new JiraSite("https://first.com/")); jiraFolderProperty.setSites(firstList); folder.getProperties().add(jiraFolderProperty); } @Test void getSitesNullWithoutFolder() throws Exception { FreeStyleProject freeStyleProject = r.createFreeStyleProject(); JiraProjectProperty prop = new JiraProjectProperty(null); freeStyleProject.addProperty(prop); JiraProjectProperty actual = freeStyleProject.getProperty(JiraProjectProperty.class); assertNotNull(actual); assertNull(actual.getSite()); } @Test void getSitesNullWithFolder() throws Exception { freeStyleProject = folder.createProject(FreeStyleProject.class, "something"); JiraProjectProperty prop = new JiraProjectProperty(null); freeStyleProject.addProperty(prop); JiraProjectProperty property = freeStyleProject.getProperty(JiraProjectProperty.class); assertNotNull(property); assertNull(property.getSite()); } @Test @ConfiguredWithCode("single-site.yml") void getSiteFromProjectProperty() { JiraProjectProperty prop = new JiraProjectProperty(null); JiraSite site = prop.getSite(); @SuppressWarnings("ConstantConditions") String actual = site.getUrl().toExternalForm(); assertEquals("https://jira.com/", actual); } @Test @ConfiguredWithCode("single-site.yml") void getSiteFromSingleEntry() throws Exception { freeStyleProject = r.createFreeStyleProject(); JiraSite expected = JiraGlobalConfiguration.get().getSites().get(0); JiraProjectProperty prop = new JiraProjectProperty(null); freeStyleProject.addProperty(prop); JiraProjectProperty property = freeStyleProject.getProperty(JiraProjectProperty.class); assertNotNull(property); assertNotNull(property.getSite()); assertEquals(expected.getName(), property.siteName); r.assertEqualDataBoundBeans(expected, property.getSite()); } @Test @ConfiguredWithCode("multiple-sites.yml") void getSiteFromFirstGlobalMultipleEntryMultipleSites() throws Exception { freeStyleProject = r.createFreeStyleProject(); JiraSite expected = JiraGlobalConfiguration.get().getSites().get(0); JiraProjectProperty prop = new JiraProjectProperty(null); freeStyleProject.addProperty(prop); JiraProjectProperty property = freeStyleProject.getProperty(JiraProjectProperty.class); assertNotNull(property); assertNotNull(property.getSite()); assertEquals(expected.getName(), property.siteName); r.assertEqualDataBoundBeans(expected, property.getSite()); } @Test @ConfiguredWithCode("multiple-sites.yml") void getSiteFromSecondGlobalEntryMultipleSites() throws Exception { freeStyleProject = r.createFreeStyleProject(); JiraSite expected = new JiraSite("https://jira.com/"); JiraProjectProperty prop = new JiraProjectProperty(expected.getName()); freeStyleProject.addProperty(prop); JiraProjectProperty property = freeStyleProject.getProperty(JiraProjectProperty.class); assertNotNull(property); assertNotNull(property.getSite()); assertEquals(expected.getName(), property.siteName); r.assertEqualDataBoundBeans(expected, property.getSite()); } @Test @ConfiguredWithCode("single-site.yml") void getSiteFromFirstFolderLayer() throws Exception { freeStyleProject = folder.createProject(FreeStyleProject.class, "something"); JiraSite expected = firstList.get(0); JiraProjectProperty prop = new JiraProjectProperty(expected.getName()); freeStyleProject.addProperty(prop); JiraProjectProperty property = freeStyleProject.getProperty(JiraProjectProperty.class); assertNotNull(property); assertNotNull(property.getSite()); assertEquals(expected.getName(), property.siteName); r.assertEqualDataBoundBeans(expected, property.getSite()); } @Test @ConfiguredWithCode("single-site.yml") void getSiteFromNestedFolderLayer() throws Exception { Folder secondFolder = folder.createProject(Folder.class, "second"); freeStyleProject = secondFolder.createProject(FreeStyleProject.class, "something"); // testing we can get value from folder above. JiraSite expected = firstList.get(0); JiraProjectProperty prop = new JiraProjectProperty(expected.getName()); freeStyleProject.addProperty(prop); JiraProjectProperty property = freeStyleProject.getProperty(JiraProjectProperty.class); assertNotNull(property); assertNotNull(property.getSite()); assertEquals(expected.getName(), property.siteName); r.assertEqualDataBoundBeans(expected, property.getSite()); } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraReleaseVersionUpdateBuilderTest.java ================================================ package hudson.plugins.jira; import static org.mockito.Mockito.*; import hudson.model.Result; import java.util.Collections; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; class JiraReleaseVersionUpdateBuilderTest { private JiraSite site; @BeforeEach void createMocks() { site = mock(JiraSite.class); } @WithJenkins @Test void testPipeline(JenkinsRule r) throws Exception { JiraGlobalConfiguration jiraGlobalConfiguration = JiraGlobalConfiguration.get(); jiraGlobalConfiguration.setSites(Collections.singletonList(site)); WorkflowJob job = r.createProject(WorkflowJob.class); job.setDefinition(new CpsFlowDefinition(""" jiraMarkVersionReleased(jiraProjectKey: 'PROJECT', jiraRelease: 'release', jiraDescription: 'description') """, true)); WorkflowRun b = r.buildAndAssertStatus(Result.SUCCESS, job); r.assertLogContains("[Jira] Marking version release in project PROJECT as released.", b); } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraRestServiceProxyTest.java ================================================ package hudson.plugins.jira; import static org.junit.jupiter.api.Assertions.*; import hudson.ProxyConfiguration; import hudson.plugins.jira.extension.ExtendedJiraRestClient; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URI; import jenkins.model.Jenkins; import org.apache.http.HttpHost; import org.apache.http.client.fluent.Request; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; @WithJenkins class JiraRestServiceProxyTest { private final ConnectionFactory connectionFactory = new HttpConnectionFactory(); private final URI JIRA_URI = URI.create("http://example.com:8080/"); private final String USERNAME = "user"; private final String PASSWORD = "password"; private Server server; private ServerConnector connector; private ExtendedJiraRestClient client; @BeforeEach void prepare() throws Exception { server = new Server(); connector = new ServerConnector(server, connectionFactory); server.addConnector(connector); server.start(); } @AfterEach void dispose() throws Exception { if (server != null) { server.stop(); } } @Test void withProxy(JenkinsRule r) throws Exception { int localPort = connector.getLocalPort(); Jenkins.get().proxy = new ProxyConfiguration("localhost", localPort); Object objectProxy = getProxyObjectFromRequest(); assertNotNull(objectProxy); assertEquals(HttpHost.class, objectProxy.getClass()); HttpHost proxyHost = (HttpHost) objectProxy; assertEquals("localhost", proxyHost.getHostName()); assertEquals(localPort, proxyHost.getPort()); } @Test void withProxyAndNoProxyHosts(JenkinsRule r) throws Exception { int localPort = connector.getLocalPort(); Jenkins.get().proxy = new ProxyConfiguration("localhost", localPort); Jenkins.get().proxy.setNoProxyHost("example.com|google.com"); assertNull(getProxyObjectFromRequest()); } @Test void withoutProxy(JenkinsRule r) throws Exception { assertNull(getProxyObjectFromRequest()); } private Object getProxyObjectFromRequest() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, NoSuchFieldException { JiraRestService service = new JiraRestService(JIRA_URI, client, USERNAME, PASSWORD, JiraSite.DEFAULT_TIMEOUT); Method m = service.getClass().getDeclaredMethod("buildGetRequest", URI.class); m.setAccessible(true); Request buildGetRequestValue = (Request) m.invoke(service, JIRA_URI); assertNotNull(buildGetRequestValue); Field proxy = buildGetRequestValue.getClass().getDeclaredField("proxy"); proxy.setAccessible(true); return proxy.get(buildGetRequestValue); } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraRestServiceTest.java ================================================ package hudson.plugins.jira; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import com.atlassian.jira.rest.client.api.RestClientException; import com.atlassian.jira.rest.client.api.SearchRestClient; import com.atlassian.jira.rest.client.api.domain.SearchResult; import hudson.plugins.jira.extension.ExtendedJiraRestClient; import io.atlassian.util.concurrent.Promise; import java.net.URI; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; class JiraRestServiceTest { private final URI JIRA_URI = URI.create("http://example.com:8080/"); private final String USERNAME = "user"; private final String PASSWORD = "password"; private ExtendedJiraRestClient client; private SearchRestClient searchRestClient; private Promise promise; private SearchResult searchResult; @BeforeEach void createMocks() throws InterruptedException, ExecutionException, TimeoutException { client = mock(ExtendedJiraRestClient.class); searchRestClient = mock(SearchRestClient.class); promise = mock(Promise.class); searchResult = mock(SearchResult.class); doReturn(searchRestClient).when(client).getSearchClient(); doReturn(promise).when(searchRestClient).searchJql(any(), any(), anyInt(), any()); doReturn(searchResult).when(promise).get(anyLong(), any()); } @Test void baseApiPath() { JiraRestService service = new JiraRestService(JIRA_URI, client, USERNAME, PASSWORD, JiraSite.DEFAULT_TIMEOUT); assertEquals("/" + JiraRestService.BASE_API_PATH, service.getBaseApiPath()); URI uri = URI.create("https://example.com/path/to/jira"); service = new JiraRestService(uri, client, USERNAME, PASSWORD, JiraSite.DEFAULT_TIMEOUT); assertEquals("/path/to/jira/" + JiraRestService.BASE_API_PATH, service.getBaseApiPath()); } @Test void getIssuesFromJqlSearchRestException() throws InterruptedException, ExecutionException, TimeoutException { Throwable throwable = mock(Throwable.class); JiraRestService service = spy(new JiraRestService(JIRA_URI, client, USERNAME, PASSWORD, JiraSite.DEFAULT_TIMEOUT)); doThrow(new RestClientException("Verify Rest client exception", throwable)) .when(promise) .get(Mockito.anyLong(), Mockito.any()); assertThrows(RestClientException.class, () -> service.getIssuesFromJqlSearch("*", null)); } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraSessionTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasProperty; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.atlassian.jira.rest.client.api.RestClientException; import com.atlassian.jira.rest.client.api.domain.Issue; import com.atlassian.jira.rest.client.api.domain.Version; import hudson.plugins.jira.extension.ExtendedVersion; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeoutException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class JiraSessionTest { private static final String PROJECT_KEY = "myKey"; private static final String QUERY = "query"; private JiraSession jiraSession = null; @Mock private JiraSite site; @Mock private final JiraRestService service = null; @BeforeEach void prepareMocks() throws IOException, InterruptedException { jiraSession = spy(new JiraSession(site, service)); } @Test void replaceWithFixVersionByRegex() throws URISyntaxException, TimeoutException, RestClientException { final ExtendedVersion newVersion = new ExtendedVersion(new URI("self"), 3L, "v3.0", null, false, false, null, null); List myVersions = new ArrayList<>(); myVersions.add(newVersion); when(jiraSession.getVersions(PROJECT_KEY)).thenReturn(myVersions); ArrayList issues = new ArrayList<>(); issues.add(getIssue(Arrays.asList("v1.0"), 1L)); issues.add(getIssue(Arrays.asList("v1.0", "v2.0", "v2.0.0"), 2L)); when(service.getIssuesFromJqlSearch(QUERY, 0)).thenReturn(issues); jiraSession.replaceFixVersion(PROJECT_KEY, "/v1.*/", newVersion.getName(), QUERY); ArgumentCaptor issueKeys = ArgumentCaptor.forClass(String.class); ArgumentCaptor versionList = ArgumentCaptor.forClass(List.class); verify(service, times(issues.size())).updateIssue(issueKeys.capture(), versionList.capture()); // First Issue, current FixVersion replaced by new one assertThat(issueKeys.getAllValues().get(0), equalTo(issues.get(0).getKey())); List firstIssueUpdatedFixVersions = versionList.getAllValues().get(0); assertThat(firstIssueUpdatedFixVersions.size(), equalTo(1)); assertThat(firstIssueUpdatedFixVersions.get(0).getName(), equalTo(newVersion.getName())); // Second Issue, current FixVersion stays, new fixVersion added. assertThat(issueKeys.getAllValues().get(1), equalTo(issues.get(1).getKey())); List secondIssueUpdatedFixVersions = versionList.getAllValues().get(1); assertThat(secondIssueUpdatedFixVersions.size(), equalTo(3)); // Check that the collection contains versions with these names in any order assertThat(secondIssueUpdatedFixVersions, hasItem(hasProperty("name", equalTo(newVersion.getName())))); assertThat(secondIssueUpdatedFixVersions, hasItem(hasProperty("name", equalTo("v2.0")))); assertThat(secondIssueUpdatedFixVersions, hasItem(hasProperty("name", equalTo("v2.0.0")))); } @Test void replaceFixVersion() throws URISyntaxException, TimeoutException, RestClientException { final ExtendedVersion newVersion = new ExtendedVersion(new URI("self"), 3L, "v3.0", null, false, false, null, null); List myVersions = new ArrayList<>(); myVersions.add(newVersion); when(jiraSession.getVersions(PROJECT_KEY)).thenReturn(myVersions); ArrayList issues = new ArrayList<>(); issues.add(getIssue(Arrays.asList("v1.0"), 1L)); issues.add(getIssue(Arrays.asList("v1.0", "v1.0.0", "v2.0.0"), 2L)); issues.add(getIssue(null, 3L)); when(service.getIssuesFromJqlSearch(QUERY, 0)).thenReturn(issues); jiraSession.replaceFixVersion(PROJECT_KEY, "v1.0", newVersion.getName(), QUERY); ArgumentCaptor issueKeys = ArgumentCaptor.forClass(String.class); ArgumentCaptor versionList = ArgumentCaptor.forClass(List.class); verify(service, times(issues.size())).updateIssue(issueKeys.capture(), versionList.capture()); // First Issue, current FixVersion replaced by new one assertThat(issueKeys.getAllValues().get(0), equalTo(issues.get(0).getKey())); List firstIssueUpdatedFixVersions = versionList.getAllValues().get(0); assertThat(firstIssueUpdatedFixVersions.size(), equalTo(1)); assertThat(firstIssueUpdatedFixVersions.get(0), equalTo(newVersion)); // Second Issue, current FixVersion stays, new fixVersion added. assertThat(issueKeys.getAllValues().get(1), equalTo(issues.get(1).getKey())); List secondIssueUpdatedFixVersions = versionList.getAllValues().get(1); assertThat(secondIssueUpdatedFixVersions.size(), equalTo(3)); assertThat(secondIssueUpdatedFixVersions, hasItem(hasProperty("name", equalTo(newVersion.getName())))); assertThat(secondIssueUpdatedFixVersions, hasItem(hasProperty("name", equalTo("v1.0.0")))); assertThat(secondIssueUpdatedFixVersions, hasItem(hasProperty("name", equalTo("v2.0.0")))); // Third Issue, no FixVersion, new fixVersion added. List thirdIssueVersions = versionList.getAllValues().get(2); assertThat(thirdIssueVersions.size(), equalTo(1)); assertThat(thirdIssueVersions.get(0), equalTo(newVersion)); } private Issue getIssue(List versions, long id) throws URISyntaxException { List fixVersions = null; if (versions != null) { fixVersions = new ArrayList<>(); for (String fixVersion : versions) { fixVersions.add(new Version( new URI("self"), ThreadLocalRandom.current().nextLong(), fixVersion, null, false, false, null)); } } return new Issue( "", new URI(""), PROJECT_KEY + id, ThreadLocalRandom.current().nextLong(), null, null, null, null, null, null, null, null, null, null, null, null, null, fixVersions, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); } @Test void shouldAddVersionToAllIssues() throws Exception { final ExtendedVersion newVersion = new ExtendedVersion(new URI("self"), 10L, "v4.0", null, false, false, null, null); List myVersions = List.of(newVersion); when(jiraSession.getVersions(PROJECT_KEY)).thenReturn(myVersions); List issues = new ArrayList<>(); issues.add(getIssue(Arrays.asList("v1.0"), 1L)); issues.add(getIssue(Arrays.asList("v2.0", "v3.0"), 2L)); when(service.getIssuesFromJqlSearch(QUERY, 0)).thenReturn(issues); jiraSession.addFixVersion(PROJECT_KEY, newVersion.getName(), QUERY); ArgumentCaptor issueKeys = ArgumentCaptor.forClass(String.class); ArgumentCaptor versionList = ArgumentCaptor.forClass(List.class); verify(service, times(issues.size())).updateIssue(issueKeys.capture(), versionList.capture()); // First issue: should have original + new version List firstIssueVersions = versionList.getAllValues().get(0); assertThat(firstIssueVersions, hasItem(hasProperty("name", equalTo("v1.0")))); assertThat(firstIssueVersions, hasItem(hasProperty("name", equalTo("v4.0")))); assertThat(firstIssueVersions.size(), equalTo(2)); // Second issue: should have both original + new version List secondIssueVersions = versionList.getAllValues().get(1); assertThat(secondIssueVersions, hasItem(hasProperty("name", equalTo("v2.0")))); assertThat(secondIssueVersions, hasItem(hasProperty("name", equalTo("v3.0")))); assertThat(secondIssueVersions, hasItem(hasProperty("name", equalTo("v4.0")))); assertThat(secondIssueVersions.size(), equalTo(3)); } @Test void shouldAddVersionWhenNoFixVersions() throws Exception { final ExtendedVersion newVersion = new ExtendedVersion(new URI("self"), 11L, "v5.0", null, false, false, null, null); List myVersions = List.of(newVersion); when(jiraSession.getVersions(PROJECT_KEY)).thenReturn(myVersions); List issues = List.of(getIssue(null, 3L)); when(service.getIssuesFromJqlSearch(QUERY, 0)).thenReturn(issues); jiraSession.addFixVersion(PROJECT_KEY, newVersion.getName(), QUERY); ArgumentCaptor issueKeys = ArgumentCaptor.forClass(String.class); ArgumentCaptor versionList = ArgumentCaptor.forClass(List.class); verify(service).updateIssue(issueKeys.capture(), versionList.capture()); List updatedVersions = versionList.getValue(); assertThat(updatedVersions.size(), equalTo(1)); assertThat(updatedVersions.get(0).getName(), equalTo("v5.0")); } @Test void shouldNotCallUpdateIfNoIssues() throws Exception { final ExtendedVersion newVersion = new ExtendedVersion(new URI("self"), 12L, "v6.0", null, false, false, null, null); List myVersions = List.of(newVersion); when(jiraSession.getVersions(PROJECT_KEY)).thenReturn(myVersions); when(service.getIssuesFromJqlSearch(QUERY, 0)).thenReturn(new ArrayList<>()); jiraSession.addFixVersion(PROJECT_KEY, newVersion.getName(), QUERY); verify(service, times(0)).updateIssue(anyString(), anyList()); } @Test void shouldReturnIfVersionNotFound() throws Exception { when(jiraSession.getVersions(PROJECT_KEY)).thenReturn(List.of()); jiraSession.addFixVersion(PROJECT_KEY, "nonexistent", QUERY); verify(service, times(0)).getIssuesFromJqlSearch(anyString(), anyInt()); verify(service, times(0)).updateIssue(anyString(), anyList()); } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraSiteSecurity1029Test.java ================================================ package hudson.plugins.jira; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import com.cloudbees.hudson.plugins.folder.Folder; import com.cloudbees.plugins.credentials.CredentialsScope; import com.cloudbees.plugins.credentials.CredentialsStore; import com.cloudbees.plugins.credentials.SystemCredentialsProvider; import com.cloudbees.plugins.credentials.domains.Domain; import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import hudson.model.Item; import hudson.model.User; import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URI; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Base64; import java.util.HashMap; import jenkins.model.Jenkins; import jenkins.security.ApiTokenProperty; import net.sf.json.JSONObject; import org.htmlunit.HttpMethod; import org.htmlunit.Page; import org.htmlunit.WebRequest; import org.htmlunit.WebResponse; import org.htmlunit.util.NameValuePair; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.MockAuthorizationStrategy; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; @WithJenkins public class JiraSiteSecurity1029Test { private HttpServer server; private URI serverUri; private FakeJiraServlet servlet; @Test @Issue("SECURITY-1029") void cannotLeakCredentials(JenkinsRule j) throws Exception { setupServer(j); final String ADMIN = "admin"; final String USER = "user"; final String USER_FOLDER_CONFIGURE = "folder_configure"; j.jenkins.setCrumbIssuer(null); j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() .grant(Jenkins.ADMINISTER) .everywhere() .to(ADMIN) .grant(Jenkins.READ, Item.READ) .everywhere() .to(USER) .grant(Jenkins.READ, Item.READ, Item.CONFIGURE) .everywhere() .to(USER_FOLDER_CONFIGURE)); String credId_1 = "cred-1-id"; String credId_2 = "cred-2-id"; String credId_3 = "cred-3-id"; String pwd1 = "pwd1"; String pwd2 = "pwd2"; String pwd3 = "pwd3"; UsernamePasswordCredentialsImpl cred1 = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, credId_1, null, "user1", pwd1); UsernamePasswordCredentialsImpl cred2 = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, credId_2, null, "user2", pwd2); UsernamePasswordCredentialsImpl cred3 = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, credId_3, null, "user3", pwd3); SystemCredentialsProvider systemProvider = SystemCredentialsProvider.getInstance(); systemProvider.getCredentials().add(cred1); systemProvider.getCredentials().add(cred2); systemProvider.save(); User admin = User.getById(ADMIN, true); admin.addProperty(new ApiTokenProperty()); admin.getProperty(ApiTokenProperty.class).changeApiToken(); User user = User.getById(USER, true); user.addProperty(new ApiTokenProperty()); user.getProperty(ApiTokenProperty.class).changeApiToken(); User userFolderConfigure = User.getById(USER_FOLDER_CONFIGURE, true); userFolderConfigure.addProperty(new ApiTokenProperty()); userFolderConfigure.getProperty(ApiTokenProperty.class).changeApiToken(); { // as an admin I should be able to validate my url / credentials JenkinsRule.WebClient wc = j.createWebClient(); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); wc = wc.withBasicApiToken(admin); String jiraSiteValidateUrl = j.getURL() + "descriptorByName/" + JiraSite.class.getName() + "/validate"; WebRequest request = new WebRequest(new URL(jiraSiteValidateUrl), HttpMethod.POST); request.setRequestParameters(Arrays.asList( new NameValuePair("threadExecutorNumber", "1"), new NameValuePair("url", serverUri.toString()), new NameValuePair("credentialsId", credId_1), new NameValuePair("useHTTPAuth", "true"))); Page page = wc.getPage(request); assertThat(page.getWebResponse().getStatusCode(), equalTo(200)); assertThat(servlet.getPasswordAndReset(), equalTo(pwd1)); } { // as an user with just read access, I may not be able to leak any credentials JenkinsRule.WebClient wc = j.createWebClient(); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); wc.withBasicApiToken(user); String jiraSiteValidateUrl = j.getURL() + "descriptorByName/" + JiraSite.class.getName() + "/validate"; WebRequest request = new WebRequest(new URL(jiraSiteValidateUrl), HttpMethod.POST); request.setRequestParameters(Arrays.asList( new NameValuePair("threadExecutorNumber", "1"), new NameValuePair("url", serverUri.toString()), new NameValuePair("credentialsId", credId_2), new NameValuePair("useHTTPAuth", "true"))); Page page = wc.getPage(request); // to avoid trouble, we always validate when the user has not the good permission assertThat(page.getWebResponse().getStatusCode(), equalTo(403)); assertThat(servlet.getPasswordAndReset(), nullValue()); } { // as an user with just read access, I may not be able to leak any credentials in folder Folder folder = j.jenkins.createProject( Folder.class, "folder" + j.jenkins.getItems().size()); JenkinsRule.WebClient wc = j.createWebClient(); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); wc.withBasicApiToken(user); String jiraSiteValidateUrl = j.jenkins.getRootUrl() + folder.getUrl() + "descriptorByName/" + JiraSite.class.getName() + "/validate"; WebRequest request = new WebRequest(new URL(jiraSiteValidateUrl), HttpMethod.POST); request.setRequestParameters(Arrays.asList( new NameValuePair("threadExecutorNumber", "1"), new NameValuePair("url", serverUri.toString()), new NameValuePair("credentialsId", credId_2), new NameValuePair("useHTTPAuth", "true"))); Page page = wc.getPage(request); // to avoid trouble, we always validate when the user has not the good permission assertThat(page.getWebResponse().getStatusCode(), equalTo(403)); assertThat(servlet.getPasswordAndReset(), nullValue()); } { // as an user with just read access, I may not be able to leak any credentials with fake id in a folder Folder folder = j.jenkins.createProject( Folder.class, "folder" + j.jenkins.getItems().size()); JenkinsRule.WebClient wc = j.createWebClient(); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); wc.withBasicApiToken(userFolderConfigure); String jiraSiteValidateUrl = j.jenkins.getRootUrl() + folder.getUrl() + "descriptorByName/" + JiraSite.class.getName() + "/validate"; WebRequest request = new WebRequest(new URL(jiraSiteValidateUrl), HttpMethod.POST); request.setRequestParameters(Arrays.asList( new NameValuePair("threadExecutorNumber", "1"), new NameValuePair("url", serverUri.toString()), new NameValuePair("credentialsId", "aussie-beer-is-the-best"), // use a non existing id on purpose new NameValuePair("useHTTPAuth", "true"))); Page page = wc.getPage(request); WebResponse webResponse = page.getWebResponse(); assertThat(webResponse.getStatusCode(), equalTo(200)); assertThat(webResponse.getContentAsString(), containsString("Cannot validate configuration")); assertThat(servlet.getPasswordAndReset(), nullValue()); } { // as an user with configure access, I can access Folder folder = j.jenkins.createProject( Folder.class, "folder" + j.jenkins.getItems().size()); JenkinsRule.WebClient wc = j.createWebClient(); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); wc.withBasicApiToken(userFolderConfigure); String jiraSiteValidateUrl = j.jenkins.getRootUrl() + folder.getUrl() + "descriptorByName/" + JiraSite.class.getName() + "/validate"; WebRequest request = new WebRequest(new URL(jiraSiteValidateUrl), HttpMethod.POST); request.setRequestParameters(Arrays.asList( new NameValuePair("threadExecutorNumber", "1"), new NameValuePair("url", serverUri.toString()), new NameValuePair("credentialsId", credId_2), new NameValuePair("useHTTPAuth", "true"))); Page page = wc.getPage(request); // to avoid trouble, we always validate when the user has the good permission assertThat(page.getWebResponse().getStatusCode(), equalTo(200)); assertThat(servlet.getPasswordAndReset(), equalTo(pwd2)); } { // as an user with folder access, I can access Folder folder = j.jenkins.createProject( Folder.class, "folder" + j.jenkins.getItems().size()); CredentialsStore folderStore = JiraFolderPropertyTest.getFolderStore(folder); folderStore.addCredentials(Domain.global(), cred3); JenkinsRule.WebClient wc = j.createWebClient(); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); wc.withBasicApiToken(userFolderConfigure); String jiraSiteValidateUrl = j.jenkins.getRootUrl() + folder.getUrl() + "descriptorByName/" + JiraSite.class.getName() + "/validate"; WebRequest request = new WebRequest(new URL(jiraSiteValidateUrl), HttpMethod.POST); request.setRequestParameters(Arrays.asList( new NameValuePair("threadExecutorNumber", "1"), new NameValuePair("url", serverUri.toString()), new NameValuePair("credentialsId", credId_3), new NameValuePair("useHTTPAuth", "true"))); Page page = wc.getPage(request); // to avoid trouble, we always validate when the user has the good permission assertThat(page.getWebResponse().getStatusCode(), equalTo(200)); assertThat(servlet.getPasswordAndReset(), equalTo(pwd3)); } } public void setupServer(JenkinsRule j) throws Exception { // auto-bind to available port server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0); servlet = new FakeJiraServlet(j); server.createContext("/", servlet); server.start(); InetSocketAddress address = server.getAddress(); serverUri = new URI(String.format("http://%s:%d/", address.getHostString(), address.getPort())); servlet.setServerUrl(serverUri); } @AfterEach void stopEmbeddedHttpServer() { server.stop(1); } private static class FakeJiraServlet implements HttpHandler { private JenkinsRule jenkinsRule; private URI serverUri; private String pwdCollected; FakeJiraServlet(JenkinsRule jenkinsRule) { this.jenkinsRule = jenkinsRule; } public void setServerUrl(URI serverUri) { this.serverUri = serverUri; } public String getPasswordAndReset() { String result = pwdCollected; this.pwdCollected = null; return result; } @Override public void handle(HttpExchange he) throws IOException { String path = he.getRequestURI().getPath(); String authBasicBase64 = he.getRequestHeaders().getFirst("Authorization"); String authBase64 = authBasicBase64.substring("Basic ".length()); String auth = new String(Base64.getDecoder().decode(authBase64), StandardCharsets.UTF_8); String[] authArray = auth.split(":"); String user = authArray[0]; String pwd = authArray[1]; this.pwdCollected = pwd; try { if ("GET".equals(he.getRequestMethod()) && "/rest/api/latest/mypermissions".equals(path)) { myPermissions(he); } else { he.sendResponseHeaders(HttpURLConnection.HTTP_NOT_FOUND, -1); } } finally { he.close(); } } private void myPermissions(HttpExchange he) throws IOException { Object body = new HashMap() { { put("permissions", new HashMap() { { put("perm_1", new HashMap() { { put("id", 1); put("key", "perm_key"); put("name", "perm_name"); put("description", null); put("havePermission", "true"); } }); } }); } }; String response = JSONObject.fromObject(body).toString(); byte[] bytes = response.getBytes(StandardCharsets.UTF_8); he.sendResponseHeaders(HttpURLConnection.HTTP_OK, bytes.length); try (OutputStream os = he.getResponseBody()) { os.write(bytes); } } } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraSiteTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import com.cloudbees.hudson.plugins.folder.AbstractFolderProperty; import com.cloudbees.hudson.plugins.folder.AbstractFolderPropertyDescriptor; import com.cloudbees.hudson.plugins.folder.Folder; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.CredentialsScope; import com.cloudbees.plugins.credentials.SystemCredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import com.cloudbees.plugins.credentials.domains.Domain; import com.cloudbees.plugins.credentials.domains.DomainSpecification; import com.cloudbees.plugins.credentials.domains.HostnameSpecification; import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import hudson.model.Descriptor.FormException; import hudson.model.FreeStyleProject; import hudson.model.Job; import hudson.plugins.jira.model.JiraIssue; import hudson.util.DescribableList; import hudson.util.Secret; import hudson.util.XStream2; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; import java.util.Collections; import jenkins.model.Jenkins; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.WithoutJenkins; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; @WithJenkins class JiraSiteTest { private static final String ANY_USER = "Kohsuke"; private static final String ANY_PASSWORD = "Kawaguchi"; private URL validPrimaryUrl; private URL exampleOrg; @BeforeEach void init() throws MalformedURLException { validPrimaryUrl = new URL("https://nonexistent.url"); exampleOrg = new URL("https://example.org/"); } @Test void createSessionWithProvidedCredentials(JenkinsRule r) throws FormException { JiraSite site = new JiraSite( validPrimaryUrl, null, new UsernamePasswordCredentialsImpl(CredentialsScope.SYSTEM, null, null, ANY_USER, ANY_PASSWORD), false, false, null, false, null, null, true); site.setTimeout(1); JiraSession session = site.getSession(null); assertNotNull(session); assertEquals(session, site.getSession(null)); } @Test @Issue("JENKINS-64083") void createSessionWithGlobalCredentials(JenkinsRule r) throws FormException { JiraSite site = new JiraSite( validPrimaryUrl, null, new UsernamePasswordCredentialsImpl(CredentialsScope.SYSTEM, null, null, ANY_USER, ANY_PASSWORD), false, false, null, false, null, null, true); site.setTimeout(1); JiraSession session = site.getSession(mock(Job.class)); assertNotNull(session); assertEquals(session, site.getSession(null)); } @Test void createSessionReturnsNullIfCredentialsIsNull(JenkinsRule r) throws FormException { JiraSite site = new JiraSite( validPrimaryUrl, null, (StandardUsernamePasswordCredentials) null, false, false, null, false, null, null, true); site.setTimeout(1); JiraSession session = site.getSession(null); assertEquals(session, site.getSession(null)); assertNull(session); } @Test void deserializeMigrateCredentials(JenkinsRule j) throws MalformedURLException, FormException { JiraSiteOld old = new JiraSiteOld( validPrimaryUrl, null, ANY_USER, ANY_PASSWORD, false, false, null, false, null, null, true); XStream2 xStream2 = new XStream2(); String xml = xStream2.toXML(old); // trick to get old version config of JiraSite xml = xml.replace( this.getClass().getName() + "_-" + JiraSiteOld.class.getSimpleName(), JiraSite.class.getName()); assertThat(xml, containsString(validPrimaryUrl.toExternalForm())); assertThat(xml, containsString("userName")); assertThat(xml, containsString("password")); assertThat(xml, not(containsString("credentialsId"))); assertThat( CredentialsProvider.lookupStores(j.jenkins).iterator().next().getCredentials(Domain.global()), empty()); JiraSite site = (JiraSite) xStream2.fromXML(xml); assertNotNull(site); assertNotNull(site.credentialsId); assertEquals( ANY_USER, CredentialsHelper.lookupSystemCredentials(site.credentialsId, null) .getUsername()); assertEquals( ANY_PASSWORD, CredentialsHelper.lookupSystemCredentials(site.credentialsId, null) .getPassword() .getPlainText()); } @Test void deserializeNormal(JenkinsRule j) throws IOException, FormException { Domain domain = new Domain( "example", "test domain", Arrays.asList(new HostnameSpecification("example.org", null))); StandardUsernamePasswordCredentials c = new UsernamePasswordCredentialsImpl(CredentialsScope.SYSTEM, null, null, ANY_USER, ANY_PASSWORD); CredentialsProvider.lookupStores(j.jenkins).iterator().next().addDomain(domain, c); JiraSite site = new JiraSite(exampleOrg, null, c.getId(), false, false, null, false, null, null, true); site.setUseBearerAuth(true); XStream2 xStream2 = new XStream2(); String xml = xStream2.toXML(site); assertThat(xml, not(containsString("userName"))); assertThat(xml, not(containsString("password"))); assertThat(xml, containsString("credentialsId")); JiraSite site1 = (JiraSite) xStream2.fromXML(xml); assertNotNull(site1.credentialsId); assertTrue(site1.useBearerAuth); } @WithoutJenkins @Test void deserializeWithoutCredentials() { JiraSite site = new JiraSite(exampleOrg, null, (String) null, false, false, null, false, null, null, true); XStream2 xStream2 = new XStream2(); String xml = xStream2.toXML(site); assertThat(xml, not(containsString("credentialsId"))); JiraSite site1 = (JiraSite) xStream2.fromXML(xml); assertNotNull(site1.url); assertEquals(exampleOrg, site1.url); assertNull(site1.credentialsId); } private static class JiraSiteOld extends JiraSite { public String userName; public Secret password; JiraSiteOld( URL url, URL alternativeUrl, String userName, String password, boolean supportsWikiStyleComment, boolean recordScmChanges, String userPattern, boolean updateJiraIssueForAllStatus, String groupVisibility, String roleVisibility, boolean useHTTPAuth) throws FormException { super( url, alternativeUrl, (StandardUsernamePasswordCredentials) null, supportsWikiStyleComment, recordScmChanges, userPattern, updateJiraIssueForAllStatus, groupVisibility, roleVisibility, useHTTPAuth); this.userName = userName; this.password = Secret.fromString(password); } } @Test @WithoutJenkins void alternativeURLNotNull() throws FormException { JiraSite site = new JiraSite( validPrimaryUrl, exampleOrg, (StandardUsernamePasswordCredentials) null, false, false, null, false, null, null, true); assertNotNull(site.getAlternativeUrl()); assertEquals(exampleOrg, site.getAlternativeUrl()); } @Test @WithoutJenkins void ensureUrlEndsWithSlash() { JiraSite jiraSite = new JiraSite(validPrimaryUrl.toExternalForm()); jiraSite.setAlternativeUrl(exampleOrg.toExternalForm()); assertTrue(jiraSite.getUrl().toExternalForm().endsWith("/")); assertTrue(jiraSite.getAlternativeUrl().toExternalForm().endsWith("/")); URL url1 = JiraSite.toURL(validPrimaryUrl.toExternalForm()); URL url2 = JiraSite.toURL(exampleOrg.toExternalForm()); assertTrue(url1.toExternalForm().endsWith("/")); assertTrue(url2.toExternalForm().endsWith("/")); } @Test @WithoutJenkins void urlNulls() { JiraSite jiraSite = new JiraSite(validPrimaryUrl.toExternalForm()); jiraSite.setAlternativeUrl(" "); assertNotNull(jiraSite.getUrl()); assertNull(jiraSite.getAlternativeUrl()); } @Test @WithoutJenkins void toUrlConvertsEmptyStringToNull() { URL emptyString = JiraSite.toURL(""); assertNull(emptyString); } @Test @WithoutJenkins void toUrlConvertsOnlyWhitespaceToNull() { URL whitespace = JiraSite.toURL(" "); assertNull(whitespace); } @WithoutJenkins @Test void ensureMainUrlIsMandatory() { assertThrows(AssertionError.class, () -> { new JiraSite(""); }); } @Test @WithoutJenkins void ensureAlternativeUrlIsNotMandatory() { JiraSite jiraSite = new JiraSite(validPrimaryUrl.toExternalForm()); jiraSite.setAlternativeUrl(""); assertNull(jiraSite.getAlternativeUrl()); } @WithoutJenkins @Test void malformedUrl() { assertThrows(AssertionError.class, () -> { new JiraSite("malformed.url"); }); } @WithoutJenkins @Test void malformedAlternativeUrl() { JiraSite jiraSite = new JiraSite(validPrimaryUrl.toExternalForm()); assertThrows(AssertionError.class, () -> jiraSite.setAlternativeUrl("malformed.url")); } @Test @WithoutJenkins void credentialsAreNullByDefault() { JiraSite jiraSite = new JiraSite(exampleOrg.toExternalForm()); jiraSite.setCredentialsId(""); assertNull(jiraSite.getCredentialsId()); } @Test void credentials(JenkinsRule r) throws Exception { JiraSite jiraSite = new JiraSite(exampleOrg.toExternalForm()); String cred = "cred-1"; String user = "user1"; String pwd = "pwd1"; UsernamePasswordCredentialsImpl credentials = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, cred, null, user, pwd); SystemCredentialsProvider systemProvider = SystemCredentialsProvider.getInstance(); systemProvider.getCredentials().add(credentials); systemProvider.save(); jiraSite.setCredentialsId(cred); assertNotNull(jiraSite.getCredentialsId()); assertEquals( credentials.getUsername(), CredentialsHelper.lookupSystemCredentials(cred, null).getUsername()); assertEquals( credentials.getPassword(), CredentialsHelper.lookupSystemCredentials(cred, null).getPassword()); } @Test @WithoutJenkins void siteAsProjectProperty() throws Exception { JiraSite jiraSite = new JiraSite(new URL("https://foo.org/").toExternalForm()); Job job = mock(Job.class); JiraProjectProperty jpp = mock(JiraProjectProperty.class); when(job.getProperty(JiraProjectProperty.class)).thenReturn(jpp); when(jpp.getSite()).thenReturn(jiraSite); assertEquals(jiraSite.getUrl(), JiraSite.get(job).getUrl()); } @Test void projectPropertySiteAndParentBothNull(JenkinsRule j) { JiraGlobalConfiguration jiraGlobalConfiguration = mock(JiraGlobalConfiguration.class); Job job = mock(Job.class); JiraProjectProperty jpp = mock(JiraProjectProperty.class); when(job.getProperty(JiraProjectProperty.class)).thenReturn(jpp); when(jpp.getSite()).thenReturn(null); when(job.getParent()).thenReturn(null); when(jiraGlobalConfiguration.getSites()).thenReturn(Collections.emptyList()); assertNull(JiraSite.get(job)); } @Test void noProjectProperty(JenkinsRule j) throws Exception { JiraGlobalConfiguration.get().setSites(null); Job job = j.jenkins.createProject(FreeStyleProject.class, "foo"); assertNull(JiraSite.get(job)); } @Test void noProjectPropertyUpFoldersWithNoProperty(JenkinsRule j) throws Exception { JiraGlobalConfiguration jiraGlobalConfiguration = mock(JiraGlobalConfiguration.class); Folder folder2 = spy(createFolder(j, null)); Folder folder1 = spy(createFolder(j, folder2)); Job job = folder1.createProject(FreeStyleProject.class, "foo"); DescribableList, AbstractFolderPropertyDescriptor> folder1Properties = spy(new DescribableList(Jenkins.get())); DescribableList, AbstractFolderPropertyDescriptor> folder2Properties = spy(new DescribableList(Jenkins.get())); doReturn(folder1Properties).when(folder1).getProperties(); doReturn(folder2Properties).when(folder2).getProperties(); doReturn(Collections.emptyList()).when(jiraGlobalConfiguration).getSites(); assertNull(JiraSite.get(job)); } @Test void noProjectPropertyFindFolderPropertyWithNullZeroLengthAndValidSites(JenkinsRule j) throws Exception { JiraSite jiraSite1 = new JiraSite(new URL("https://example1.org/").toExternalForm()); JiraSite jiraSite2 = new JiraSite(new URL("https://example2.org/").toExternalForm()); Folder folder1 = spy(createFolder(j, null)); Job job = folder1.createProject(FreeStyleProject.class, "foo"); DescribableList, AbstractFolderPropertyDescriptor> folder1Properties = new DescribableList(Jenkins.get()); Folder folder2 = spy(createFolder(j, folder1)); DescribableList, AbstractFolderPropertyDescriptor> folder2Properties = new DescribableList(Jenkins.get()); JiraFolderProperty jfp = new JiraFolderProperty(); jfp.setSites(Arrays.asList(jiraSite2, jiraSite1)); folder1Properties.add(jfp); folder1.addProperty(folder1Properties.get(JiraFolderProperty.class)); folder2Properties.add(jfp); folder2.addProperty(folder2Properties.get(JiraFolderProperty.class)); assertEquals(jiraSite2.getUrl(), JiraSite.get(job).getUrl()); } @Test void siteConfiguredGlobally(JenkinsRule j) throws Exception { JiraSite jiraSite = new JiraSite(new URL("https://foo.org/").toExternalForm()); JiraGlobalConfiguration.get().setSites(Collections.singletonList(jiraSite)); Job job = mock(Job.class); doReturn(null).when(job).getProperty(JiraProjectProperty.class); assertEquals(jiraSite.getUrl(), JiraSite.get(job).getUrl()); } @Test @WithoutJenkins void getIssueWithoutSession() throws Exception { JiraSite jiraSite = new JiraSite(new URL("https://foo.org/").toExternalForm()); // Verify that no session will be created assertNull(jiraSite.getSession(null)); JiraIssue issue = jiraSite.getIssue("JIRA-1235"); assertNull(issue); } @Test @WithoutJenkins void ensureMaxIssuesFromJqlEndsUpMaxAllowed() throws MalformedURLException { JiraSite jiraSite = new JiraSite(new URL("https://example1.org/").toExternalForm()); jiraSite.setMaxIssuesFromJqlSearch(6000); assertEquals(5000, jiraSite.getMaxIssuesFromJqlSearch()); } private Folder createFolder(JenkinsRule j, Folder folder) throws IOException { return folder == null ? j.jenkins.createProject( Folder.class, "folder" + j.jenkins.getItems().size()) : folder.createProject( Folder.class, "folder" + j.jenkins.getItems().size()); } } ================================================ FILE: src/test/java/hudson/plugins/jira/JiraVersionCreatorBuilderTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import hudson.model.Result; import hudson.util.XStream2; import java.util.Collections; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.WithoutJenkins; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; class JiraVersionCreatorBuilderTest { private JiraSite site; private JiraSession session; private final XStream2 xStream2 = new XStream2(); @BeforeEach void createMocks() { site = mock(JiraSite.class); session = mock(JiraSession.class); } @WithJenkins @Test void testPipelineWithJiraSite(JenkinsRule r) throws Exception { JiraGlobalConfiguration jiraGlobalConfiguration = JiraGlobalConfiguration.get(); jiraGlobalConfiguration.setSites(Collections.singletonList(site)); doReturn(session).when(site).getSession(any()); WorkflowJob job = r.createProject(WorkflowJob.class); job.setDefinition(new CpsFlowDefinition(""" jiraCreateVersion(jiraVersion: 'Version', jiraProjectKey: 'project-key') """, true)); WorkflowRun b = r.buildAndAssertStatus(Result.SUCCESS, job); r.assertLogContains("[Jira] Creating version Version in project project-key.", b); } @Test @WithoutJenkins void readResolveSetsFailIfAlreadyExistsWhenMissingInConfig() { String xml = """ 1.0 PROJ """; JiraVersionCreatorBuilder builder = (JiraVersionCreatorBuilder) xStream2.fromXML(xml); assertTrue(builder.isFailIfAlreadyExists()); xml = """ 1.2 PROJ """; JiraVersionCreator notifier = (JiraVersionCreator) xStream2.fromXML(xml); assertThat(notifier.getJiraProjectKey(), is("PROJ")); assertThat(notifier.getJiraVersion(), is("1.2")); assertTrue(notifier.isFailIfAlreadyExists()); } @Test @WithoutJenkins void readResolvePresentInConfig() { String xml = """ 1.0 PROJ false """; JiraVersionCreatorBuilder builder = (JiraVersionCreatorBuilder) xStream2.fromXML(xml); assertFalse(builder.isFailIfAlreadyExists()); xml = """ 1.0 PROJ false """; JiraVersionCreator notifier = (JiraVersionCreator) xStream2.fromXML(xml); assertFalse(notifier.isFailIfAlreadyExists()); } } ================================================ FILE: src/test/java/hudson/plugins/jira/MailResolverDisabledTest.java ================================================ /* * The MIT License * * Copyright (c) 2015 schristou88 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson.plugins.jira; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsNull.nullValue; import hudson.model.User; import java.util.Collections; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; /** * @author schristou88 */ @WithJenkins class MailResolverDisabledTest { @BeforeAll static void setUp() throws Exception { System.setProperty("hudson.plugins.jira.JiraMailAddressResolver.disabled", "true"); } @Test void disableEmailResolver(JenkinsRule r) { User user = User.get("bob", true, Collections.emptyMap()); assertThat(JiraMailAddressResolver.resolve(user), is(nullValue())); } } ================================================ FILE: src/test/java/hudson/plugins/jira/MailResolverWithExtensionTest.java ================================================ /* * The MIT License * * Copyright (c) 2015 schristou88 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson.plugins.jira; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.model.User; import hudson.security.HudsonPrivateSecurityRealm; import java.net.URI; import java.util.Collections; import java.util.HashMap; import java.util.Map; import jenkins.model.Jenkins; import jenkins.security.SecurityListener; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.TestExtension; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.core.userdetails.UserDetails; @WithJenkins @ExtendWith(MockitoExtension.class) class MailResolverWithExtensionTest { @Mock(strictness = Mock.Strictness.LENIENT) JiraSite site; JiraSession session; @Mock JiraRestService service; @BeforeEach void createMocks() throws Exception { session = new JiraSession(site, service); Mockito.when(site.getSession(any())).thenReturn(session); Map avatars = new HashMap<>(); // pre check condition in Jira User constructor and do not ask me why!! avatars.put("48x48", new URI("https://foo.com")); com.atlassian.jira.rest.client.api.domain.User jiraUser = new com.atlassian.jira.rest.client.api.domain.User( null, "foo", "bar", "foo@beer.com", true, null, avatars, null); doReturn(session).when(site).getSession(any()); doReturn(jiraUser).when(service).getUser("foo"); } @Test void emailResolverWithSecurityExtension(JenkinsRule r) throws Exception { HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(true); realm.createAccount("foo", "pacific_ale"); r.jenkins.setSecurityRealm(realm); JiraGlobalConfiguration.get().setSites(Collections.singletonList(site)); r.createWebClient().login("foo", "pacific_ale"); } @TestExtension public static class DummySecurityListener extends SecurityListener { @Override protected void authenticated2(@NonNull UserDetails details) { check(details.getUsername()); } @Override protected void authenticated(@NonNull org.acegisecurity.userdetails.UserDetails details) { check(details.getUsername()); } private void check(String userId) { User user = User.getById(userId, false); JiraMailAddressResolver jiraMailAddressResolver = Jenkins.get() .getExtensionList(JiraMailAddressResolver.class) .get(0); String email = jiraMailAddressResolver.findMailAddressFor(user); assertThat(email, is("foo@beer.com")); } } } ================================================ FILE: src/test/java/hudson/plugins/jira/MockAffectedFile.java ================================================ package hudson.plugins.jira; import hudson.scm.ChangeLogSet.AffectedFile; /* * required to mocking subclass of AffectedFile */ public interface MockAffectedFile extends AffectedFile {} ================================================ FILE: src/test/java/hudson/plugins/jira/UnmaskMailTest.java ================================================ package hudson.plugins.jira; import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; /** * Test unmasking email. Eg: john dot doe at example dot com to john.doe@example.com * * @author Honza Brázdil (jbrazdil@redhat.com) */ class UnmaskMailTest { @Test void unmaskMailTest() { test("user@example.com", "user at example dot com"); test("user@example.com", "user AT example DOT com"); test("user@example.com", "user aT example dOt com"); test("user@example.com", "user At example d0t com"); test("user@example.com", "user at example D0t com"); test("user@example.com", "user[at]example[dot]com"); test("user@example.com", "user{AT}example{DOT}com"); test("user@example.com", "userexamplecom"); test("user@example.com", "user\"At\"example\"d0t\"com"); test("user@example.com", "user_at_example_D0t_com"); test("user@example.com", "user(at)example(dot)com"); test("user@example.com", "user [at] example [dot] com"); test("user@example.com", "user {AT} example {DOT} com"); test("user@example.com", "user example com"); test("user@example.com", "user \"At\" example \"d0t\" com"); test("user@example.com", "user _at_ example _D0t_ com"); test("user@example.com", "user (at) example (dot) com"); test("john.doe.junior@my.site.eu", "john dot doe DOT junior at my dOt site D0T eu"); test("john.doe.junior@my.site.eu", "john(dot)doe[DOT]junior{at}mysite_D0T_eu"); test("john.doe.junior@my.site.eu", "john (dot) doe [DOT] junior {at} my site _D0T_ eu"); test("atdot@dotat.com", "atdot at dotat dot com"); test("atdot@dotat.com", "atdot AT dotat DOT com"); test("atdot@dotat.com", "atdot aT dotat dOt com"); test("atdot@dotat.com", "atdot At dotat d0t com"); test("atdot@dotat.com", "atdot at dotat D0t com"); } private void test(String expected, String masked) { assertEquals(expected, JiraMailAddressResolver.unmaskEmail(masked)); } } ================================================ FILE: src/test/java/hudson/plugins/jira/UpdaterTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.atlassian.jira.rest.client.api.RestClientException; import com.atlassian.jira.rest.client.api.domain.Comment; import com.atlassian.jira.rest.client.api.domain.Issue; import hudson.model.FreeStyleBuild; import hudson.model.FreeStyleProject; import hudson.model.Job; import hudson.model.Result; import hudson.model.Run; import hudson.model.User; import hudson.plugins.jira.model.JiraIssue; import hudson.scm.ChangeLogSet; import hudson.scm.ChangeLogSet.Entry; import hudson.scm.EditType; import hudson.scm.SCM; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Set; import org.hamcrest.Matchers; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.WithoutJenkins; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.mockito.Mockito; import org.mockito.stubbing.Answer; /** * Test case for the Jira {@link Updater}. * * @author kutzi */ @SuppressWarnings("unchecked") @WithJenkins public class UpdaterTest { private Updater updater; private static class MockEntry extends Entry { private final String msg; MockEntry(String msg) { this.msg = msg; } @Override public Collection getAffectedPaths() { return null; } @Override public User getAuthor() { return null; } @Override public String getMsg() { return this.msg; } } @BeforeEach void prepare() { SCM scm = mock(SCM.class); this.updater = new Updater(scm); } @Test void getScmCommentsFromPreviousBuilds(JenkinsRule r) { final FreeStyleProject project = mock(FreeStyleProject.class); final FreeStyleBuild build1 = mock(FreeStyleBuild.class); final MockEntry entry1 = new MockEntry("FOOBAR-1: The first build"); { ChangeLogSet changeLogSet = mock(ChangeLogSet.class); when(build1.getChangeSet()).thenReturn(changeLogSet); List> changeSets = new ArrayList<>(); changeSets.add(changeLogSet); when(build1.getChangeSets()).thenReturn(changeSets); when(build1.getResult()).thenReturn(Result.FAILURE); doReturn(project).when(build1).getProject(); doReturn(new JiraCarryOverAction(new HashSet(Arrays.asList(new JiraIssue("FOOBAR-1", null))))) .when(build1) .getAction(JiraCarryOverAction.class); final Set entries = new HashSet(Arrays.asList(entry1)); when(changeLogSet.iterator()).thenAnswer(invocation -> entries.iterator()); } final FreeStyleBuild build2 = mock(FreeStyleBuild.class); final MockEntry entry2 = new MockEntry("FOOBAR-2: The next build"); { ChangeLogSet changeLogSet = mock(ChangeLogSet.class); when(build2.getChangeSet()).thenReturn(changeLogSet); List> changeSets = new ArrayList<>(); changeSets.add(changeLogSet); when(build2.getChangeSets()).thenReturn(changeSets); when(build2.getPreviousCompletedBuild()).thenReturn(build1); when(build2.getResult()).thenReturn(Result.SUCCESS); doReturn(project).when(build2).getProject(); final Set entries = new HashSet(Arrays.asList(entry2)); when(changeLogSet.iterator()).thenAnswer(invocation -> entries.iterator()); } final List comments = new ArrayList(); final JiraSession session = mock(JiraSession.class); doAnswer((Answer) invocation -> { Comment rc = Comment.createWithGroupLevel((String) invocation.getArguments()[1], (String) invocation.getArguments()[2]); comments.add(rc); return null; }) .when(session) .addComment(anyString(), anyString(), anyString(), anyString()); this.updater = new Updater(build2.getProject().getScm()); final Set ids = new HashSet(Arrays.asList(new JiraIssue("FOOBAR-1", null), new JiraIssue("FOOBAR-2", null))); updater.submitComments(build2, System.out, "http://jenkins", ids, session, false, false, "", ""); assertEquals(2, comments.size()); assertThat(comments.get(0).getBody(), Matchers.containsString(entry1.getMsg())); assertThat(comments.get(1).getBody(), Matchers.containsString(entry2.getMsg())); } /** * Tests that the generated comment matches the expectations - * especially that the Jira id is not stripped from the comment. */ @Test @org.jvnet.hudson.test.Issue("4572") void comment(JenkinsRule r) { // mock Jira session: JiraSession session = mock(JiraSession.class); final Issue mockIssue = Mockito.mock(Issue.class); when(session.getIssue(Mockito.anyString())).thenReturn(mockIssue); final List comments = new ArrayList<>(); Answer answer = (Answer) invocation -> { comments.add((String) invocation.getArguments()[1]); return null; }; doAnswer(answer) .when(session) .addComment(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyString()); // mock build: FreeStyleBuild build = mock(FreeStyleBuild.class); FreeStyleProject project = mock(FreeStyleProject.class); when(build.getParent()).thenReturn(project); when(build.getProject()).thenReturn(project); ChangeLogSet changeLogSet = mock(ChangeLogSet.class); when(build.getChangeSet()).thenReturn(changeLogSet); when(build.getResult()).thenReturn(Result.SUCCESS); Set entries = new HashSet(Arrays.asList(new MockEntry("Fixed FOOBAR-4711"))); when(changeLogSet.iterator()).thenReturn(entries.iterator()); List> changeSets = new ArrayList<>(); changeSets.add(changeLogSet); when(build.getChangeSets()).thenReturn(changeSets); // test: Set ids = new HashSet(Arrays.asList(new JiraIssue("FOOBAR-4711", "Title"))); Updater updaterCurrent = new Updater(build.getParent().getScm()); updaterCurrent.submitComments(build, System.out, "http://jenkins", ids, session, false, false, "", ""); assertEquals(1, comments.size()); String comment = comments.get(0); assertTrue(comment.contains("FOOBAR-4711")); // must also work case-insensitively (JENKINS-4132) comments.clear(); entries = new HashSet(Arrays.asList(new MockEntry("Fixed Foobar-4711"))); when(changeLogSet.iterator()).thenReturn(entries.iterator()); ids = new HashSet(Arrays.asList(new JiraIssue("FOOBAR-4711", "Title"))); updaterCurrent.submitComments(build, System.out, "http://jenkins", ids, session, false, false, "", ""); assertEquals(1, comments.size()); comment = comments.get(0); assertTrue(comment.contains("Foobar-4711")); } /** * /** * Checks if issues are correctly removed from the carry over list. */ @Test @org.jvnet.hudson.test.Issue("17156") @WithoutJenkins void issueIsRemovedFromCarryOverListAfterSubmission() { // mock build: FreeStyleBuild build = mock(FreeStyleBuild.class); FreeStyleProject project = mock(FreeStyleProject.class); when(build.getParent()).thenReturn(project); when(build.getProject()).thenReturn(project); ChangeLogSet changeLogSet = ChangeLogSet.createEmpty(build); when(build.getChangeSet()).thenReturn(changeLogSet); when(build.getResult()).thenReturn(Result.SUCCESS); final JiraIssue firstIssue = new JiraIssue("FOOBAR-1", "Title"); final JiraIssue secondIssue = new JiraIssue("ALIBA-1", "Title"); final JiraIssue thirdIssue = new JiraIssue("MOONA-1", "Title"); final JiraIssue deletedIssue = new JiraIssue("FOOBAR-2", "Title"); final JiraIssue forbiddenIssue = new JiraIssue("LASSO-17", "Title"); // assume that there is a following list of jira issues from scm commit messages out of // hudson.plugins.jira.JiraCarryOverAction Set issues = new HashSet(Arrays.asList(firstIssue, secondIssue, forbiddenIssue, thirdIssue)); // mock Jira session: JiraSession session = mock(JiraSession.class); final List comments = new ArrayList<>(); Answer answer = (Answer) invocation -> { Comment c = Comment.createWithGroupLevel( (String) invocation.getArguments()[0], (String) invocation.getArguments()[1]); comments.add(c); return null; }; doAnswer(answer) .when(session) .addComment(eq(firstIssue.getKey()), Mockito.anyString(), Mockito.anyString(), Mockito.anyString()); doAnswer(answer) .when(session) .addComment(eq(secondIssue.getKey()), Mockito.anyString(), Mockito.anyString(), Mockito.anyString()); doAnswer(answer) .when(session) .addComment(eq(thirdIssue.getKey()), Mockito.anyString(), Mockito.anyString(), Mockito.anyString()); // issue for the caught exception doThrow(new RestClientException(new Throwable(), 404)) .when(session) .addComment(eq(deletedIssue.getKey()), Mockito.anyString(), Mockito.anyString(), Mockito.anyString()); doThrow(new RestClientException(new Throwable(), 403)) .when(session) .addComment(eq(forbiddenIssue.getKey()), Mockito.anyString(), Mockito.anyString(), Mockito.anyString()); final String groupVisibility = ""; final String roleVisibility = ""; Updater updaterCurrent = new Updater(build.getParent().getScm()); updaterCurrent.submitComments( build, System.out, "http://jenkins", issues, session, false, false, groupVisibility, roleVisibility); // expected issue list final Set expectedIssuesToCarryOver = new LinkedHashSet(); expectedIssuesToCarryOver.add(forbiddenIssue); assertThat(issues, is(expectedIssuesToCarryOver)); } @TempDir public File folder; /** * Test that workflow job - run instance of type WorkflowJob - can * return changeSets using java reflection api * */ @Test void getChangesUsingReflectionForWorkflowJob(JenkinsRule r) throws IOException { WorkflowJob workflowJob = new WorkflowJob(r.getInstance(), "job"); WorkflowRun workflowRun = new WorkflowRun(workflowJob); ChangeLogSet.createEmpty(workflowRun); List> changesUsingReflection = RunScmChangeExtractor.getChangesUsingReflection(workflowRun); assertNotNull(changesUsingReflection); assertTrue(changesUsingReflection.isEmpty()); } @WithoutJenkins @Test void getChangesUsingReflectionForunknownJob() { Run run = mock(Run.class); assertThrows(IllegalArgumentException.class, () -> RunScmChangeExtractor.getChangesUsingReflection(run)); } /** * Test formatting of scm entry change time. * */ @Test @WithoutJenkins void appendChangeTimestampToDescription() { Updater updater = new Updater(null); StringBuilder description = new StringBuilder(); Calendar calendar = Calendar.getInstance(); calendar.set(2016, 0, 1, 0, 0, 0); JiraSite site = mock(JiraSite.class); when(site.getDateTimePattern()).thenReturn("yyyy-MM-dd HH:mm:ss"); updater.appendChangeTimestampToDescription(description, site, calendar.getTimeInMillis()); System.out.println(description.toString()); assertThat(description.toString(), equalTo("2016-01-01 00:00:00")); } /** * Test formatting of scm entry change description. * */ @Test void dateTimeInChangeDescription(JenkinsRule rule) { rule.getInstance(); Calendar calendar = Calendar.getInstance(); calendar.set(2016, 0, 1, 0, 0, 0); JiraSite site = mock(JiraSite.class); when(site.isAppendChangeTimestamp()).thenReturn(true); when(site.getDateTimePattern()).thenReturn("yyyy-MM-dd HH:mm:ss"); Run r = mock(Run.class); Job j = mock(Job.class); when(r.getParent()).thenReturn(j); JiraProjectProperty jiraProjectProperty = mock(JiraProjectProperty.class); when(j.getProperty(JiraProjectProperty.class)).thenReturn(jiraProjectProperty); when(jiraProjectProperty.getSite()).thenReturn(site); ChangeLogSet.Entry entry = mock(ChangeLogSet.Entry.class); when(entry.getTimestamp()).thenReturn(calendar.getTimeInMillis()); when(entry.getCommitId()).thenReturn("dsgsvds2re3dsv"); User mockAuthor = mock(User.class); when(mockAuthor.getId()).thenReturn("jenkins-user"); when(entry.getAuthor()).thenReturn(mockAuthor); Updater updater = new Updater(null); String description = updater.createScmChangeEntryDescription(r, entry, false, false); System.out.println(description); assertThat(description, containsString("2016-01-01 00:00:00")); assertThat(description, containsString("jenkins-user")); assertThat(description, containsString("dsgsvds2re3dsv")); } /** * Test formatting of scm entry change description * when no format is provided (e.g. when null). * */ @Test @WithoutJenkins void appendChangeTimestampToDescriptionNullFormat() { // set default locale -> predictable test without explicit format Locale.setDefault(Locale.ENGLISH); Updater updater = new Updater(null); JiraSite site = mock(JiraSite.class); when(site.isAppendChangeTimestamp()).thenReturn(true); when(site.getDateTimePattern()).thenReturn(null); // when(site.getDateTimePattern()).thenReturn("d/M/yy hh:mm a"); Calendar calendar = Calendar.getInstance(); calendar.set(2016, 0, 1, 0, 0, 0); StringBuilder builder = new StringBuilder(); updater.appendChangeTimestampToDescription(builder, site, calendar.getTimeInMillis()); assertThat(builder.toString(), equalTo("1/1/16 12:00 AM")); } /** * Test formatting of scm entry change description * when no format is provided (e.g. when empty string). * */ @Test @WithoutJenkins void appendChangeTimestampToDescriptionNoFormat() { // set default locale -> predictable test without explicit format Locale.setDefault(Locale.ENGLISH); Updater updater = new Updater(null); JiraSite site = mock(JiraSite.class); when(site.isAppendChangeTimestamp()).thenReturn(true); when(site.getDateTimePattern()).thenReturn(null); // when(site.getDateTimePattern()).thenReturn("d/M/yy hh:mm a"); Calendar calendar = Calendar.getInstance(); calendar.set(2016, 0, 1, 0, 0, 0); StringBuilder builder = new StringBuilder(); updater.appendChangeTimestampToDescription(builder, site, calendar.getTimeInMillis()); assertThat(builder.toString(), equalTo("1/1/16 12:00 AM")); } /** * Test formatting of scm entry change description coverage primary wiki * style appendRevisionToDescription and appendAffectedFilesToDescription * */ @Test void tesDescriptionWithAffectedFiles(JenkinsRule rule) { rule.getInstance(); Calendar calendar = Calendar.getInstance(); calendar.set(2016, 0, 1, 0, 0, 0); JiraSite site = mock(JiraSite.class); when(site.isAppendChangeTimestamp()).thenReturn(false); Run r = mock(Run.class); Job j = mock(Job.class); when(r.getParent()).thenReturn(j); JiraProjectProperty jiraProjectProperty = mock(JiraProjectProperty.class); when(j.getProperty(JiraProjectProperty.class)).thenReturn(jiraProjectProperty); when(jiraProjectProperty.getSite()).thenReturn(site); ChangeLogSet.Entry entry = mock(ChangeLogSet.Entry.class); when(entry.getTimestamp()).thenReturn(calendar.getTimeInMillis()); when(entry.getCommitId()).thenReturn("dsgsvds2re3dsv"); User mockAuthor = mock(User.class); when(mockAuthor.getId()).thenReturn("jenkins-user"); when(entry.getAuthor()).thenReturn(mockAuthor); Collection affectedFiles = new ArrayList(); MockAffectedFile affectedFile1 = mock(MockAffectedFile.class); when(affectedFile1.getEditType()).thenReturn(EditType.ADD); when(affectedFile1.getPath()).thenReturn("hudson/plugins/jira/File1"); affectedFiles.add(affectedFile1); MockAffectedFile corruptedFile = mock(MockAffectedFile.class); when(corruptedFile.getEditType()).thenReturn(null); when(corruptedFile.getPath()).thenReturn(null); affectedFiles.add(corruptedFile); MockAffectedFile affectedFile2 = mock(MockAffectedFile.class); when(affectedFile2.getEditType()).thenReturn(EditType.DELETE); when(affectedFile2.getPath()).thenReturn("hudson/plugins/jira/File2"); affectedFiles.add(affectedFile2); MockAffectedFile affectedFile3 = mock(MockAffectedFile.class); when(affectedFile3.getEditType()).thenReturn(EditType.EDIT); when(affectedFile3.getPath()).thenReturn("hudson/plugins/jira/File3"); affectedFiles.add(affectedFile3); doReturn(affectedFiles).when(entry).getAffectedFiles(); Updater updater = new Updater(null); String description = updater.createScmChangeEntryDescription(r, entry, true, true); System.out.println(description); assertThat( description, equalTo(" (jenkins-user: rev dsgsvds2re3dsv)\n" + "* (add) hudson/plugins/jira/File1\n" + "* \n" + "* (delete) hudson/plugins/jira/File2\n" + "* (edit) hudson/plugins/jira/File3\n")); } } ================================================ FILE: src/test/java/hudson/plugins/jira/VersionCreatorTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.atlassian.jira.rest.client.api.RestClientException; import hudson.EnvVars; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Result; import hudson.plugins.jira.extension.ExtendedVersion; import java.io.IOException; import java.io.PrintStream; import java.util.Arrays; import org.joda.time.DateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.Answer; @ExtendWith(MockitoExtension.class) class VersionCreatorTest { private static final String JIRA_VER = Long.toString(System.currentTimeMillis()); private static final String JIRA_PRJ = "TEST_PRJ"; private static final String JIRA_VER_PARAM = "${JIRA_VER}"; private static final String JIRA_PRJ_PARAM = "${JIRA_PRJ}"; private static final Long ANY_ID = System.currentTimeMillis(); private static final DateTime ANY_DATE = new DateTime(); @Mock AbstractBuild build; @Mock BuildListener listener; @Mock PrintStream logger; @Mock EnvVars env; @Mock AbstractProject project; @Mock JiraSite site; @Mock JiraSession session; @Captor ArgumentCaptor versionCaptor; @Captor ArgumentCaptor projectCaptor; private VersionCreator versionCreator = spy(VersionCreator.class); private ExtendedVersion existingVersion = new ExtendedVersion(null, ANY_ID, JIRA_VER, null, false, false, ANY_DATE, ANY_DATE); @BeforeEach void createMocks() { when(site.getSession(any())).thenReturn(session); when(env.expand(Mockito.anyString())).thenAnswer((Answer) invocationOnMock -> { Object[] args = invocationOnMock.getArguments(); String expanded = (String) args[0]; if (expanded.equals(JIRA_PRJ_PARAM)) { return JIRA_PRJ; } else if (expanded.equals(JIRA_VER_PARAM)) { return JIRA_VER; } else { return expanded; } }); when(listener.getLogger()).thenReturn(logger); doReturn(site).when(versionCreator).getSiteForProject(any()); } @Test void callsJiraWithSpecifiedParameters() throws InterruptedException, IOException, RestClientException { when(build.getEnvironment(listener)).thenReturn(env); when(site.getSession(any())).thenReturn(session); // for new version, verify the addVersion method is called when(session.getVersions(JIRA_PRJ)).thenReturn(null); versionCreator.setJiraProjectKey(JIRA_PRJ); versionCreator.setJiraVersion(JIRA_VER); boolean result = versionCreator.perform(project, build, listener); verify(session, times(1)).addVersion(versionCaptor.capture(), projectCaptor.capture()); assertThat(projectCaptor.getValue(), is(JIRA_PRJ)); assertThat(versionCaptor.getValue(), is(JIRA_VER)); verify(logger, times(1)).println(Messages.JiraVersionCreator_CreatingVersion(JIRA_VER, JIRA_PRJ)); assertThat(result, is(true)); // for existing version, verify the addVersion method is not called reset(session); when(session.getVersions(JIRA_PRJ)).thenReturn(Arrays.asList(existingVersion)); versionCreator.setJiraProjectKey(JIRA_PRJ); versionCreator.setJiraVersion(JIRA_VER); result = versionCreator.perform(project, build, listener); verify(session, times(0)).addVersion(versionCaptor.capture(), projectCaptor.capture()); verify(logger, times(1)).println(Messages.JiraVersionCreator_VersionExists(JIRA_VER, JIRA_PRJ)); verify(listener).finished(Result.FAILURE); assertThat(result, is(false)); } @Test void expandsEnvParameters() throws InterruptedException, IOException, RestClientException { when(build.getEnvironment(listener)).thenReturn(env); when(site.getSession(any())).thenReturn(session); // for new version, verify the addVersion method is called when(session.getVersions(JIRA_PRJ)).thenReturn(null); versionCreator.setJiraProjectKey(JIRA_PRJ_PARAM); versionCreator.setJiraVersion(JIRA_VER_PARAM); boolean result = versionCreator.perform(project, build, listener); verify(session, times(1)).addVersion(versionCaptor.capture(), projectCaptor.capture()); assertThat(projectCaptor.getValue(), is(JIRA_PRJ)); assertThat(versionCaptor.getValue(), is(JIRA_VER)); assertThat(result, is(true)); // for existing version, verify the addVersion method is called reset(session); when(session.getVersions(JIRA_PRJ)).thenReturn(Arrays.asList(existingVersion)); versionCreator.setJiraProjectKey(JIRA_PRJ_PARAM); versionCreator.setJiraVersion(JIRA_VER_PARAM); result = versionCreator.perform(project, build, listener); verify(session, times(0)).addVersion(versionCaptor.capture(), projectCaptor.capture()); verify(logger, times(1)).println(Messages.JiraVersionCreator_VersionExists(JIRA_VER, JIRA_PRJ)); verify(listener).finished(Result.FAILURE); assertThat(result, is(false)); } @Test void buildDidNotFailWhenVersionExists() throws IOException, InterruptedException, RestClientException { when(build.getEnvironment(listener)).thenReturn(env); ExtendedVersion releasedVersion = new ExtendedVersion(null, ANY_ID, JIRA_VER, null, false, true, ANY_DATE, ANY_DATE); when(site.getSession(any())).thenReturn(session); when(session.getVersions(JIRA_PRJ)).thenReturn(Arrays.asList(releasedVersion)); versionCreator.setJiraProjectKey(JIRA_PRJ_PARAM); versionCreator.setJiraVersion(JIRA_VER_PARAM); versionCreator.perform(project, build, listener); verify(session, times(0)).addVersion(any(), any()); } @Test void buildDoesNotFailWhenVersionExistsAndFailIfAlreadyExistsIsFalse() throws IOException, InterruptedException, RestClientException { when(build.getEnvironment(listener)).thenReturn(env); when(site.getSession(any())).thenReturn(session); when(session.getVersions(JIRA_PRJ)).thenReturn(Arrays.asList(existingVersion)); // Set failIfAlreadyExists to false versionCreator.setFailIfAlreadyExists(false); versionCreator.setJiraProjectKey(JIRA_PRJ); versionCreator.setJiraVersion(JIRA_VER); boolean result = versionCreator.perform(project, build, listener); // Verify that addVersion is not called (because version already exists) verify(session, times(0)).addVersion(any(), any()); // Verify the message is logged verify(logger, times(1)).println(Messages.JiraVersionCreator_VersionExists(JIRA_VER, JIRA_PRJ)); // Verify build is NOT failed verify(listener, times(0)).finished(Result.FAILURE); // Verify the perform method returns true assertThat(result, is(true)); } } ================================================ FILE: src/test/java/hudson/plugins/jira/VersionReleaserTest.java ================================================ package hudson.plugins.jira; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import hudson.EnvVars; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.plugins.jira.extension.ExtendedVersion; import java.io.PrintStream; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import org.joda.time.DateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.Answer; @ExtendWith(MockitoExtension.class) class VersionReleaserTest { private static final String JIRA_VER = Long.toString(System.currentTimeMillis()); private static final String JIRA_PRJ = "TEST_PRJ"; private static final String JIRA_DES = "TEST_DES"; private static final String JIRA_VER_PARAM = "${JIRA_VER}"; private static final String JIRA_PRJ_PARAM = "${JIRA_PRJ}"; private static final String JIRA_DES_PARAM = "${JIRA_DES}"; private static final Long ANY_ID = System.currentTimeMillis(); private static final DateTime ANY_DATE = new DateTime(); @Mock AbstractBuild build; @Mock BuildListener listener; @Mock PrintStream logger; @Mock EnvVars env; @Mock AbstractProject project; @Mock(strictness = Mock.Strictness.LENIENT) JiraSite site; @Mock JiraSession session; @Captor ArgumentCaptor versionCaptor; @Captor ArgumentCaptor projectCaptor; private VersionReleaser versionReleaser = spy(VersionReleaser.class); private ExtendedVersion existingVersion = new ExtendedVersion(null, ANY_ID, JIRA_VER, JIRA_DES, false, false, ANY_DATE, ANY_DATE); @BeforeEach void createMocks() throws Exception { when(site.getSession(any())).thenReturn(session); when(build.getEnvironment(listener)).thenReturn(env); when(env.expand(Mockito.anyString())).thenAnswer((Answer) invocationOnMock -> { Object[] args = invocationOnMock.getArguments(); String expanded = (String) args[0]; if (expanded.equals(JIRA_PRJ_PARAM)) { return JIRA_PRJ; } else if (expanded.equals(JIRA_VER_PARAM)) { return JIRA_VER; } else if (expanded.equals(JIRA_DES_PARAM)) { return JIRA_DES; } else { return expanded; } }); when(listener.getLogger()).thenReturn(logger); doReturn(site).when(versionReleaser).getSiteForProject(any()); } @Test void callsJiraWithSpecifiedParameters() { when(session.getVersions(JIRA_PRJ)).thenReturn(Collections.singletonList(existingVersion)); when(site.getVersions(JIRA_PRJ)).thenReturn(new HashSet<>(Arrays.asList(existingVersion))); when(site.getSession(any())).thenReturn(session); versionReleaser.perform(project, JIRA_PRJ, JIRA_VER, JIRA_DES, build, listener); verify(session).releaseVersion(projectCaptor.capture(), versionCaptor.capture()); assertThat(projectCaptor.getValue(), is(JIRA_PRJ)); assertThat(versionCaptor.getValue().getName(), is(JIRA_VER)); assertThat(versionCaptor.getValue().getDescription(), is(JIRA_DES)); } @Test void expandsEnvParameters() { when(session.getVersions(JIRA_PRJ)).thenReturn(Collections.singletonList(existingVersion)); when(site.getVersions(JIRA_PRJ)).thenReturn(new HashSet<>(Arrays.asList(existingVersion))); when(site.getSession(any())).thenReturn(session); versionReleaser.perform(project, JIRA_PRJ_PARAM, JIRA_VER_PARAM, JIRA_DES_PARAM, build, listener); verify(session).releaseVersion(projectCaptor.capture(), versionCaptor.capture()); assertThat(projectCaptor.getValue(), is(JIRA_PRJ)); assertThat(versionCaptor.getValue().getName(), is(JIRA_VER)); assertThat(versionCaptor.getValue().getDescription(), is(JIRA_DES)); } @Test void buildDidNotFailWhenVersionExists() { ExtendedVersion releasedVersion = new ExtendedVersion(null, ANY_ID, JIRA_VER, JIRA_DES, false, true, ANY_DATE, ANY_DATE); when(site.getVersions(JIRA_PRJ)).thenReturn(new HashSet<>(Arrays.asList(releasedVersion))); versionReleaser.perform(project, JIRA_PRJ_PARAM, JIRA_VER_PARAM, JIRA_DES_PARAM, build, listener); verify(session, times(0)).releaseVersion(projectCaptor.capture(), versionCaptor.capture()); } } ================================================ FILE: src/test/java/hudson/plugins/jira/auth/BearerHttpAuthenticationHandlerTest.java ================================================ package hudson.plugins.jira.auth; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import com.atlassian.httpclient.api.Request; import org.junit.jupiter.api.Test; class BearerHttpAuthenticationHandlerTest { @Test void testConfigure() { String token = "token"; BearerHttpAuthenticationHandler handler = new BearerHttpAuthenticationHandler(token); Request.Builder builder = mock(Request.Builder.class); handler.configure(builder); verify(builder).setHeader("Authorization", "Bearer " + token); } } ================================================ FILE: src/test/java/hudson/plugins/jira/auth/JiraRestServiceBearerAuthTest.java ================================================ package hudson.plugins.jira.auth; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import com.atlassian.jira.rest.client.api.RestClientException; import com.atlassian.jira.rest.client.api.SearchRestClient; import com.atlassian.jira.rest.client.api.domain.SearchResult; import hudson.plugins.jira.JiraRestService; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.extension.ExtendedJiraRestClient; import io.atlassian.util.concurrent.Promise; import java.net.URI; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; class JiraRestServiceBearerAuthTest { private final URI JIRA_URI = URI.create("http://example.com:8080/"); private final String TOKEN = "token"; private ExtendedJiraRestClient client; private SearchRestClient searchRestClient; private Promise promise; private SearchResult searchResult; @BeforeEach void createMocks() throws InterruptedException, ExecutionException, TimeoutException { client = mock(ExtendedJiraRestClient.class); searchRestClient = mock(SearchRestClient.class); promise = mock(Promise.class); searchResult = mock(SearchResult.class); doReturn(searchRestClient).when(client).getSearchClient(); doReturn(promise).when(searchRestClient).searchJql(any(), any(), anyInt(), any()); doReturn(searchResult).when(promise).get(anyLong(), any()); } @Test void baseApiPath() { JiraRestService service = new JiraRestService(JIRA_URI, client, TOKEN, JiraSite.DEFAULT_TIMEOUT); assertEquals("/" + JiraRestService.BASE_API_PATH, service.getBaseApiPath()); URI uri = URI.create("https://example.com/path/to/jira"); service = new JiraRestService(uri, client, TOKEN, JiraSite.DEFAULT_TIMEOUT); assertEquals("/path/to/jira/" + JiraRestService.BASE_API_PATH, service.getBaseApiPath()); } @Test void getIssuesFromJqlSearchTimeout() throws ExecutionException, InterruptedException, TimeoutException { Throwable throwable = mock(Throwable.class); JiraRestService service = spy(new JiraRestService(JIRA_URI, client, TOKEN, JiraSite.DEFAULT_TIMEOUT)); doThrow(new RestClientException("Verify rest client exception", throwable)) .when(promise) .get(Mockito.anyLong(), Mockito.any()); assertThrows(RestClientException.class, () -> service.getIssuesFromJqlSearch("*", null)); } } ================================================ FILE: src/test/java/hudson/plugins/jira/listissuesparameter/JiraIssueParameterDefinitionTest.java ================================================ package hudson.plugins.jira.listissuesparameter; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.atlassian.jira.rest.client.api.domain.Issue; import hudson.model.Item; import hudson.model.Job; import hudson.model.ParameterValue; import hudson.model.ParametersDefinitionProperty; import hudson.plugins.jira.Messages; import hudson.util.ListBoxModel; import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.NullSource; import org.kohsuke.stapler.StaplerRequest2; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class JiraIssueParameterDefinitionTest { private JiraIssueParameterDefinition definition = new JiraIssueParameterDefinition("PARAM_NAME", "desc", "jqlQuery"); static Stream createValueInvalidParameters() { return Stream.of( Arguments.of((Object) new String[] {}), Arguments.of((Object) new String[] {"a", "b"}), Arguments.of((Object) new String[] {""})); } @ParameterizedTest @NullSource @MethodSource("createValueInvalidParameters") void shouldCreateNullParameterForInvalidValues(String[] values, @Mock StaplerRequest2 req) { when(req.getParameterValues(any())).thenReturn(values); ParameterValue result = definition.createValue(req); assertNull(result); } @Test void shouldCreateValue(@Mock StaplerRequest2 req) { when(req.getParameterValues(any())).thenReturn(new String[] {"value"}); ParameterValue result = definition.createValue(req); assertNotNull(result); assertEquals("PARAM_NAME", result.getName()); assertEquals("value", result.getValue()); } @Nested class DescriptorImplTest { private JiraIssueParameterDefinition.DescriptorImpl uut = new JiraIssueParameterDefinition.DescriptorImpl(); @Test void shouldFillValueItems( @Mock Job job, @Mock ParametersDefinitionProperty propertyDef, @Mock JiraIssueParameterDefinition paramDef, @Mock Issue issue) { when(job.hasPermission(Item.BUILD)).thenReturn(true); when(job.getProperty(ParametersDefinitionProperty.class)).thenReturn(propertyDef); when(propertyDef.getParameterDefinition("PARAM_NAME")).thenReturn(paramDef); when(issue.getKey()).thenReturn("JIRA-1234"); when(issue.getSummary()).thenReturn("Summary"); JiraIssueParameterDefinition.Result item = new JiraIssueParameterDefinition.Result(issue, null); when(paramDef.getIssues(any())).thenReturn(List.of(item)); ListBoxModel result = uut.doFillValueItems(job, "PARAM_NAME"); assertThat(result, hasSize(1)); ListBoxModel.Option option = result.get(0); assertEquals("JIRA-1234", option.value); assertEquals("JIRA-1234: Summary", option.name); verify(job).hasPermission(Item.BUILD); } @Test void shouldNotFillValueItemsIfPermissionMissing(@Mock Job job) { ListBoxModel result = uut.doFillValueItems(job, "PARAM_NAME"); assertThat(result, hasSize(1)); ListBoxModel.Option option = result.get(0); assertEquals("", option.value); assertEquals(Messages.JiraIssueParameterDefinition_NoIssueMatchedSearch(), option.name); verify(job).hasPermission(Item.BUILD); } @Test void shouldHaveNoSearchMatchesItemIfSearchMatchesNoItem( @Mock Job job, @Mock ParametersDefinitionProperty propertyDef, @Mock JiraIssueParameterDefinition paramDef, @Mock Issue issue) { when(job.hasPermission(Item.BUILD)).thenReturn(true); when(job.getProperty(ParametersDefinitionProperty.class)).thenReturn(propertyDef); when(propertyDef.getParameterDefinition("PARAM_NAME")).thenReturn(paramDef); ListBoxModel result = uut.doFillValueItems(job, "PARAM_NAME"); assertThat(result, hasSize(1)); ListBoxModel.Option option = result.get(0); assertEquals("", option.value); assertEquals(Messages.JiraIssueParameterDefinition_NoIssueMatchedSearch(), option.name); verify(job).hasPermission(Item.BUILD); } } } ================================================ FILE: src/test/java/hudson/plugins/jira/listissuesparameter/JiraIssueParameterTest.java ================================================ package hudson.plugins.jira.listissuesparameter; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasProperty; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import hudson.EnvVars; import hudson.model.ParametersDefinitionProperty; import hudson.model.Run; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; @WithJenkins class JiraIssueParameterTest { @Test void scriptedPipeline(JenkinsRule r) throws Exception { WorkflowJob p = r.createProject(WorkflowJob.class); p.setDefinition(new CpsFlowDefinition(""" properties([ parameters([ jiraIssue(name: 'JIRA_ISSUE', description: 'Jira Test Description', jiraIssueFilter: 'project=PRJ') ]) ])""", true)); r.buildAndAssertSuccess(p); ParametersDefinitionProperty parameters = p.getProperty(ParametersDefinitionProperty.class); assertThat(parameters, is(notNullValue())); assertThat( parameters.getParameterDefinitions(), hasItem(allOf( instanceOf(JiraIssueParameterDefinition.class), hasProperty("name", is("JIRA_ISSUE")), hasProperty("description", is("Jira Test Description")), hasProperty("jiraIssueFilter", is("project=PRJ"))))); } @Test void nullValueDoesNotThrowException(JenkinsRule r) throws Exception { // Test that JiraIssueParameterValue with null value doesn't throw NPE JiraIssueParameterValue paramValue = new JiraIssueParameterValue("JIRA_ISSUE", null); // Test buildEnvironment method doesn't throw NPE with null value EnvVars envVars = new EnvVars(); WorkflowJob job = r.createProject(WorkflowJob.class); job.setDefinition(new CpsFlowDefinition("echo 'test'", true)); Run run = r.buildAndAssertSuccess(job); assertDoesNotThrow(() -> paramValue.buildEnvironment(run, envVars)); assertThat(envVars.get("JIRA_ISSUE"), is("")); // Test that toString works with null value assertDoesNotThrow(() -> paramValue.toString()); } @Test void nonNullValueInBuildEnvironment(JenkinsRule r) throws Exception { // Test that JiraIssueParameterValue with non-null value works correctly JiraIssueParameterValue paramValue = new JiraIssueParameterValue("JIRA_ISSUE", "TEST-123"); // Test buildEnvironment method with non-null value EnvVars envVars = new EnvVars(); WorkflowJob job = r.createProject(WorkflowJob.class); job.setDefinition(new CpsFlowDefinition("echo 'test'", true)); Run run = r.buildAndAssertSuccess(job); assertDoesNotThrow(() -> paramValue.buildEnvironment(run, envVars)); assertThat(envVars.get("JIRA_ISSUE"), is("TEST-123")); } @Test void variableResolverHandlesNullAndNonNullValues() throws Exception { // Test with null value JiraIssueParameterValue nullParamValue = new JiraIssueParameterValue("JIRA_ISSUE", null); hudson.util.VariableResolver nullResolver = nullParamValue.createVariableResolver(null); // Should return empty string for matching parameter name with null value assertThat(nullResolver.resolve("JIRA_ISSUE"), is("")); // Should return null for non-matching parameter name assertThat(nullResolver.resolve("OTHER_PARAM"), is(nullValue())); // Test with non-null value JiraIssueParameterValue nonNullParamValue = new JiraIssueParameterValue("JIRA_ISSUE", "TEST-123"); hudson.util.VariableResolver nonNullResolver = nonNullParamValue.createVariableResolver(null); // Should return the actual value for matching parameter name assertThat(nonNullResolver.resolve("JIRA_ISSUE"), is("TEST-123")); // Should return null for non-matching parameter name assertThat(nonNullResolver.resolve("OTHER_PARAM"), is(nullValue())); } @Test void originalNullPointerExceptionScenarioFixed(JenkinsRule r) throws Exception { // This test replicates the original issue scenario where a null JIRA_ISSUE parameter // would cause a NullPointerException during pipeline execution with ${params.JIRA_ISSUE} JiraIssueParameterValue paramValue = new JiraIssueParameterValue("JIRA_ISSUE", null); // Test the buildEnvironment path (used when setting environment variables) EnvVars envVars = new EnvVars(); WorkflowJob job = r.createProject(WorkflowJob.class); job.setDefinition(new CpsFlowDefinition("echo \"Ticket: ${params.JIRA_ISSUE}\"", true)); Run run = r.buildAndAssertSuccess(job); // This should not throw NPE and should set empty string assertDoesNotThrow(() -> paramValue.buildEnvironment(run, envVars)); assertThat(envVars.get("JIRA_ISSUE"), is("")); // Test the variable resolver path (used during pipeline script evaluation) hudson.util.VariableResolver resolver = paramValue.createVariableResolver(null); // This should not throw NPE and should return empty string String resolvedValue = assertDoesNotThrow(() -> resolver.resolve("JIRA_ISSUE")); assertThat(resolvedValue, is("")); } } ================================================ FILE: src/test/java/hudson/plugins/jira/pipeline/CommentStepTest.java ================================================ package hudson.plugins.jira.pipeline; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; import com.atlassian.jira.rest.client.api.RestClientException; import com.google.inject.Inject; import hudson.model.*; import hudson.plugins.jira.JiraProjectProperty; import hudson.plugins.jira.JiraSession; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.pipeline.CommentStep.CommentStepExecution; import java.io.PrintStream; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jenkinsci.plugins.workflow.steps.StepConfigTester; import org.jenkinsci.plugins.workflow.steps.StepContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.mockito.Mockito; @WithJenkins class CommentStepTest { private JenkinsRule jenkinsRule; @Inject CommentStep.DescriptorImpl descriptor; @BeforeEach void setUp(JenkinsRule jenkinsRule) { this.jenkinsRule = jenkinsRule; jenkinsRule.getInstance().getInjector().injectMembers(this); } @Test void configRoundTrip() throws Exception { configRoundTrip("EXAMPLE-1", "comment"); } private void configRoundTrip(String issueKey, String body) throws Exception { CommentStep configRoundTrip = new StepConfigTester(jenkinsRule).configRoundTrip(new CommentStep(issueKey, body)); assertEquals(issueKey, configRoundTrip.getIssueKey()); assertEquals(body, configRoundTrip.getBody()); } @Test void callSessionAddComment() throws Exception { JiraSession session = mock(JiraSession.class); final String issueKey = "KEY"; final String body = "dsgsags"; AbstractProject mockProject = mock(FreeStyleProject.class); Run mockRun = mock(Run.class); JiraProjectProperty jiraProjectProperty = mock(JiraProjectProperty.class); JiraSite site = mock(JiraSite.class); when(jiraProjectProperty.getSite()).thenReturn(site); when(site.getSession(mockProject)).thenReturn(session); when(mockRun.getParent()).thenReturn(mockProject); when(mockRun.getParent().getProperty(JiraProjectProperty.class)).thenReturn(jiraProjectProperty); final List assertCalledParams = new ArrayList<>(); Mockito.doAnswer(invocation -> { String issueId = invocation.getArgument(0, String.class); String comment = invocation.getArgument(1, String.class); System.out.println("issueId: " + issueId); System.out.println("comment: " + comment); assertThat(issueId, equalTo(issueKey)); assertThat(comment, equalTo(body)); assertCalledParams.addAll(Arrays.asList(invocation.getArguments())); return null; }) .when(session) .addComment(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); Map r = new HashMap<>(); r.put("issueKey", issueKey); r.put("body", body); CommentStep step = (CommentStep) descriptor.newInstance(r); StepContext ctx = mock(StepContext.class); when(ctx.get(Node.class)).thenReturn(jenkinsRule.getInstance()); when(ctx.get(Run.class)).thenReturn(mockRun); assertThat(assertCalledParams, hasSize(0)); CommentStepExecution start = (CommentStepExecution) step.start(ctx); start.run(); assertThat(assertCalledParams, hasSize(4)); } @Test void addCommentRestException() throws Exception { JiraSession session = mock(JiraSession.class); final String issueKey = "KEY"; final String body = "dsgsags"; AbstractProject mockProject = mock(FreeStyleProject.class); Run mockRun = mock(Run.class); JiraProjectProperty jiraProjectProperty = mock(JiraProjectProperty.class); JiraSite site = mock(JiraSite.class); when(jiraProjectProperty.getSite()).thenReturn(site); when(site.getSession(mockProject)).thenReturn(session); when(mockRun.getParent()).thenReturn(mockProject); when(mockRun.getParent().getProperty(JiraProjectProperty.class)).thenReturn(jiraProjectProperty); final List assertCalledParams = new ArrayList<>(); Throwable throwable = mock(Throwable.class); PrintStream logger = mock(PrintStream.class); TaskListener listener = mock(TaskListener.class); Mockito.doThrow(new RestClientException("[Jira] Jira REST addComment error. Cause: 401 error", throwable)) .when(session) .addComment(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); Map r = new HashMap<>(); r.put("issueKey", issueKey); r.put("body", body); CommentStep step = (CommentStep) descriptor.newInstance(r); StepContext ctx = mock(StepContext.class); when(ctx.get(Node.class)).thenReturn(jenkinsRule.getInstance()); when(ctx.get(Run.class)).thenReturn(mockRun); when(ctx.get(TaskListener.class)).thenReturn(listener); when(listener.getLogger()).thenReturn(logger); assertThat(assertCalledParams, hasSize(0)); CommentStepExecution start = (CommentStepExecution) step.start(ctx); start.run(); verify(logger).println("[Jira] Jira REST addComment error. Cause: 401 error"); } } ================================================ FILE: src/test/java/hudson/plugins/jira/pipeline/IssueFieldUpdateStepTest.java ================================================ package hudson.plugins.jira.pipeline; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import static org.mockito.Mockito.doThrow; import com.atlassian.jira.rest.client.api.RestClientException; import hudson.EnvVars; import hudson.Launcher; import hudson.model.*; import hudson.plugins.jira.JiraGlobalConfiguration; import hudson.plugins.jira.JiraSession; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.model.JiraIssueField; import hudson.plugins.jira.selector.ExplicitIssueSelector; import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule; import io.jenkins.plugins.casc.misc.junit.jupiter.WithJenkinsConfiguredWithCode; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Random; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.jvnet.hudson.test.JenkinsRule; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; /** @author Dmitry Frolov tekillaz.dev@gmail.com */ @WithJenkinsConfiguredWithCode @ExtendWith(MockitoExtension.class) class IssueFieldUpdateStepTest { private static final String BUILD_NUMBER_VAR = "${BUILD_NUMBER}"; @Mock(strictness = Mock.Strictness.LENIENT) JiraSite site; @Mock(strictness = Mock.Strictness.LENIENT) JiraSession session; @Mock PrintStream logger; @Mock(strictness = Mock.Strictness.LENIENT) AbstractBuild build; @Mock(strictness = Mock.Strictness.LENIENT) Launcher launcher; @Mock(strictness = Mock.Strictness.LENIENT) BuildListener listener; @Mock AbstractProject project; @Mock(strictness = Mock.Strictness.LENIENT) Job job; private JenkinsRule r; @BeforeEach void before(JenkinsConfiguredWithCodeRule r) { this.r = r; when(build.getParent()).thenReturn(project); when(build.getProject()).thenReturn(project); when(listener.getLogger()).thenReturn(logger); doReturn(session).when(site).getSession(any()); JiraGlobalConfiguration.get().setSites(Collections.singletonList(site)); // when(jiraGlobalConfiguration.getSites()).thenReturn(sites); // .getSession(build.getParent())).thenReturn(session); when(job.getBuild(any())).thenReturn(build); } @Test void checkPrepareFieldId() { List field_test = Arrays.asList("10100", "customfield_10100", "field_10100"); List field_after = Arrays.asList("customfield_10100", "customfield_10100", "customfield_field_10100"); IssueFieldUpdateStep jifu = new IssueFieldUpdateStep(null, null, ""); for (int i = 0; i < field_test.size(); i++) { assertEquals(jifu.prepareFieldId(field_test.get(i)), field_after.get(i), "Check field id conversion #" + i); } } @Test void shouldFailIfSelectorIsNull() throws IOException, InterruptedException { IssueFieldUpdateStep jifu = spy(new IssueFieldUpdateStep(null, "", "")); assertThrows(IOException.class, () -> jifu.perform(build, null, launcher, listener)); } // @ConfiguredWithCode("single-site.yml") @Test void checkSubmit() throws InterruptedException, IOException { Random random = new Random(); Integer randomBuildNumber = random.nextInt(85) + 15; // random number 15 < r < 99 String issueId = "ISSUE-" + random.nextInt(1000) + 999; String beforeFieldid = "field" + random.nextInt(100) + 99; String beforeFieldValue = "Some comment, build #${BUILD_NUMBER}"; EnvVars env = new EnvVars(); env.put("BUILD_NUMBER", randomBuildNumber.toString()); when(build.getEnvironment(listener)).thenReturn(env); final ExplicitIssueSelector issueSelector = new ExplicitIssueSelector(issueId); final List issuesAfter = new ArrayList(); final List fieldsAfter = new ArrayList(); doAnswer(invocation -> { String id = (String) invocation.getArguments()[0]; List jif = (List) invocation.getArguments()[1]; issuesAfter.add(id); fieldsAfter.addAll(jif); return null; }) .when(session) .addFields(anyString(), anyList()); IssueFieldUpdateStep jifu = spy(new IssueFieldUpdateStep(issueSelector, beforeFieldid, beforeFieldValue)); jifu.perform(build, null, launcher, listener); assertEquals(issuesAfter.get(0), issueId, "Check issue value"); assertEquals("customfield_" + beforeFieldid, fieldsAfter.get(0).getId()); assertThat(fieldsAfter.get(0).getValue().toString(), containsString("build #" + randomBuildNumber.toString())); jifu.perform(build, null, env, launcher, listener); assertThat(fieldsAfter.get(1).getValue().toString(), containsString("build #" + randomBuildNumber.toString())); } @Test void testPipeline() throws Exception { WorkflowJob job = r.createProject(WorkflowJob.class); job.setDefinition(new CpsFlowDefinition(""" jiraUpdateIssueField( issueSelector: ExplicitSelector("JIRA-123"), fieldId: "field", fieldValue: "value" ) """, true)); r.buildAndAssertStatus(Result.SUCCESS, job); } @Test void issueFieldFindIssueIdsRestException() throws IOException, InterruptedException { Random random = new Random(); Integer randomBuildNumber = random.nextInt(85) + 15; // random number 15 < r < 99 String issueId = "ISSUE-" + random.nextInt(1000) + 999; String beforeFieldid = "field" + random.nextInt(100) + 99; String beforeFieldValue = "Some comment, build #${BUILD_NUMBER}"; EnvVars env = new EnvVars(); env.put("BUILD_NUMBER", randomBuildNumber.toString()); when(build.getEnvironment(listener)).thenReturn(env); final ExplicitIssueSelector issueSelector = spy(new ExplicitIssueSelector(issueId)); Throwable throwable = mock(Throwable.class); PrintStream logger = mock(PrintStream.class); when(listener.getLogger()).thenReturn(logger); final List issuesAfter = new ArrayList(); final List fieldsAfter = new ArrayList(); doAnswer(invocation -> { String id = (String) invocation.getArguments()[0]; List jif = (List) invocation.getArguments()[1]; issuesAfter.add(id); fieldsAfter.addAll(jif); return null; }) .when(session) .addFields(anyString(), anyList()); IssueFieldUpdateStep jifu = spy(new IssueFieldUpdateStep(issueSelector, beforeFieldid, beforeFieldValue)); doThrow(new RestClientException("[Jira] Jira REST findIssueIds error. Cause: 401 error", throwable)) .when(issueSelector) .findIssueIds(build, site, listener); jifu.perform(build, null, launcher, listener); verify(logger).println("[Jira] Jira REST findIssueIds error. Cause: 401 error"); } @Test void issueFieldSubmitFieldsRestException() throws IOException, InterruptedException { Random random = new Random(); Integer randomBuildNumber = random.nextInt(85) + 15; // random number 15 < r < 99 String issueId = "ISSUE-" + random.nextInt(1000) + 999; String beforeFieldid = "field" + random.nextInt(100) + 99; String beforeFieldValue = "Some comment, build #${BUILD_NUMBER}"; EnvVars env = new EnvVars(); env.put("BUILD_NUMBER", randomBuildNumber.toString()); when(build.getEnvironment(listener)).thenReturn(env); final ExplicitIssueSelector issueSelector = new ExplicitIssueSelector(issueId); Throwable throwable = mock(Throwable.class); PrintStream logger = mock(PrintStream.class); when(listener.getLogger()).thenReturn(logger); final List issuesAfter = new ArrayList(); final List fieldsAfter = new ArrayList(); doAnswer(invocation -> { String id = (String) invocation.getArguments()[0]; List jif = (List) invocation.getArguments()[1]; issuesAfter.add(id); fieldsAfter.addAll(jif); return null; }) .when(session) .addFields(anyString(), anyList()); IssueFieldUpdateStep jifu = spy(new IssueFieldUpdateStep(issueSelector, beforeFieldid, beforeFieldValue)); doThrow(new RestClientException("[Jira] Jira REST submitFields error. Cause: 401 error", throwable)) .when(jifu) .submitFields(any(JiraSession.class), anyString(), anyList(), any(PrintStream.class)); jifu.perform(build, null, launcher, listener); verify(logger).println("[Jira] Jira REST submitFields error. Cause: 401 error"); } } ================================================ FILE: src/test/java/hudson/plugins/jira/pipeline/IssueSelectorStepTest.java ================================================ package hudson.plugins.jira.pipeline; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.mockito.Mockito.*; import com.atlassian.jira.rest.client.api.RestClientException; import com.google.inject.Inject; import hudson.model.Node; import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.jira.JiraGlobalConfiguration; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.selector.AbstractIssueSelector; import java.io.PrintStream; import java.util.Collections; import java.util.HashMap; import java.util.Set; import org.jenkinsci.plugins.workflow.steps.StepContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @WithJenkins @ExtendWith(MockitoExtension.class) class IssueSelectorStepTest { @Inject private IssueSelectorStep.DescriptorImpl descriptor; @Mock private AbstractIssueSelector issueSelector; @Mock(strictness = Mock.Strictness.LENIENT) private TaskListener listener; @Mock private PrintStream logger; @Mock private Run run; @Mock(strictness = Mock.Strictness.LENIENT) private StepContext stepContext; private IssueSelectorStep.IssueSelectorStepExecution stepExecution; private IssueSelectorStep subject; @BeforeEach void setUp(JenkinsRule jenkinsRule) throws Exception { jenkinsRule.getInstance().getInjector().injectMembers(this); when(listener.getLogger()).thenReturn(logger); when(stepContext.get(Node.class)).thenReturn(jenkinsRule.getInstance()); when(stepContext.get(Run.class)).thenReturn(run); when(stepContext.get(TaskListener.class)).thenReturn(listener); subject = (IssueSelectorStep) descriptor.newInstance(new HashMap<>()); subject.setIssueSelector(issueSelector); } @Test void runWithNullSite() throws Exception { stepExecution = spy((IssueSelectorStep.IssueSelectorStepExecution) subject.start(stepContext)); // doReturn(Optional.empty()).when(stepExecution).getOptionalJiraSite(); doCallRealMethod().when(run).getParent(); Set ids = stepExecution.run(); verify(run, times(1)).setResult(Result.FAILURE); assertThat(ids, hasSize(0)); } @Test void run() throws Exception { JiraSite site = mock(JiraSite.class); JiraGlobalConfiguration jiraGlobalConfiguration = JiraGlobalConfiguration.get(); jiraGlobalConfiguration.setSites(Collections.singletonList(site)); stepExecution = spy((IssueSelectorStep.IssueSelectorStepExecution) subject.start(stepContext)); doCallRealMethod().when(run).getParent(); stepExecution.run(); verify(run, times(0)).setResult(Result.FAILURE); verify(issueSelector).findIssueIds(run, site, listener); } @Test void runWithRestException() throws Exception { JiraSite site = mock(JiraSite.class); JiraGlobalConfiguration jiraGlobalConfiguration = JiraGlobalConfiguration.get(); jiraGlobalConfiguration.setSites(Collections.singletonList(site)); stepExecution = spy((IssueSelectorStep.IssueSelectorStepExecution) subject.start(stepContext)); doCallRealMethod().when(run).getParent(); Throwable throwable = mock(Throwable.class); doThrow(new RestClientException("[Jira] Jira REST findIssueIds error. Cause: 401 error", throwable)) .when(issueSelector) .findIssueIds(run, site, listener); stepExecution.run(); verify(run, times(1)).setResult(Result.FAILURE); verify(logger).println("[Jira] Jira REST findIssueIds error. Cause: 401 error"); } } ================================================ FILE: src/test/java/hudson/plugins/jira/pipeline/SearchIssuesStepTest.java ================================================ package hudson.plugins.jira.pipeline; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; import com.atlassian.jira.rest.client.api.RestClientException; import com.atlassian.jira.rest.client.api.domain.Issue; import com.google.inject.Inject; import hudson.model.*; import hudson.plugins.jira.JiraProjectProperty; import hudson.plugins.jira.JiraSession; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.pipeline.SearchIssuesStep.SearchStepExecution; import java.io.PrintStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jenkinsci.plugins.workflow.steps.StepConfigTester; import org.jenkinsci.plugins.workflow.steps.StepContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; @WithJenkins class SearchIssuesStepTest { private JenkinsRule jenkinsRule; @Inject SearchIssuesStep.DescriptorImpl descriptor; @BeforeEach void setUp(JenkinsRule jenkinsRule) throws Exception { this.jenkinsRule = jenkinsRule; jenkinsRule.getInstance().getInjector().injectMembers(this); } @Test void configRoundTrip() throws Exception { configRoundTrip(""); configRoundTrip("key='EXAMPLE-1'"); } private void configRoundTrip(String jql) throws Exception { SearchIssuesStep step = new StepConfigTester(jenkinsRule).configRoundTrip(new SearchIssuesStep(jql)); assertEquals(jql, step.getJql()); } @Test void callGetIssuesFromJqlSearch() throws Exception { JiraSession session = mock(JiraSession.class); String jql = "key='EXAMPLE-1'"; Issue issue = mock(Issue.class); when(issue.getKey()).thenReturn("EXAMPLE-1"); final List assertCalledList = new ArrayList<>(); when(session.getIssuesFromJqlSearch(jql)).then(invocation -> { Issue issue2 = mock(Issue.class); when(issue2.getKey()).thenReturn("EXAMPLE-1"); assertCalledList.add(issue2); return assertCalledList; }); JiraSite site = mock(JiraSite.class); AbstractProject mockProject = mock(FreeStyleProject.class); Run mockRun = mock(Run.class); Job mockJob = mock(Job.class); JiraProjectProperty jiraProjectProperty = mock(JiraProjectProperty.class); when(jiraProjectProperty.getSite()).thenReturn(site); when(site.getSession(mockProject)).thenReturn(session); when(mockRun.getParent()).thenReturn(mockProject); when(mockRun.getParent().getProperty(JiraProjectProperty.class)).thenReturn(jiraProjectProperty); Map r = new HashMap<>(); r.put("jql", jql); SearchIssuesStep step = (SearchIssuesStep) descriptor.newInstance(r); StepContext ctx = mock(StepContext.class); when(ctx.get(Node.class)).thenReturn(jenkinsRule.getInstance()); when(ctx.get(Run.class)).thenReturn(mockRun); SearchStepExecution start = (SearchStepExecution) step.start(ctx); List returnedList = start.run(); assertThat(assertCalledList, hasSize(1)); assertThat(returnedList, hasSize(1)); assertThat(assertCalledList.iterator().next().getKey(), equalTo("EXAMPLE-1")); assertThat(returnedList.iterator().next(), equalTo("EXAMPLE-1")); } @Test void getIssuesFromJqlSearchRestException() throws Exception { JiraSession session = mock(JiraSession.class); String jql = "key='EXAMPLE-1'"; Issue issue = mock(Issue.class); when(issue.getKey()).thenReturn("EXAMPLE-1"); Throwable throwable = mock(Throwable.class); doThrow(new RestClientException("[Jira] Jira REST getIssuesFromJqlSearch error. Cause: 401 error", throwable)) .when(session) .getIssuesFromJqlSearch(jql); JiraSite site = mock(JiraSite.class); AbstractProject mockProject = mock(FreeStyleProject.class); Run mockRun = mock(Run.class); JiraProjectProperty jiraProjectProperty = mock(JiraProjectProperty.class); when(jiraProjectProperty.getSite()).thenReturn(site); when(site.getSession(mockProject)).thenReturn(session); when(mockRun.getParent()).thenReturn(mockProject); when(mockRun.getParent().getProperty(JiraProjectProperty.class)).thenReturn(jiraProjectProperty); Map r = new HashMap<>(); r.put("jql", jql); SearchIssuesStep step = (SearchIssuesStep) descriptor.newInstance(r); StepContext ctx = mock(StepContext.class); when(ctx.get(Node.class)).thenReturn(jenkinsRule.getInstance()); when(ctx.get(Run.class)).thenReturn(mockRun); TaskListener listener = mock(TaskListener.class); PrintStream logger = mock(PrintStream.class); when(ctx.get(TaskListener.class)).thenReturn(listener); when(listener.getLogger()).thenReturn(logger); SearchStepExecution start = (SearchStepExecution) step.start(ctx); start.run(); verify(logger).println("[Jira] Jira REST getIssuesFromJqlSearch error. Cause: 401 error"); } } ================================================ FILE: src/test/java/hudson/plugins/jira/selector/DefaultIssueSelectorTest.java ================================================ package hudson.plugins.jira.selector; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import hudson.model.BuildListener; import hudson.model.FreeStyleBuild; import hudson.model.ParameterValue; import hudson.model.ParametersAction; import hudson.model.User; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.listissuesparameter.JiraIssueParameterValue; import hudson.scm.ChangeLogSet; import hudson.scm.ChangeLogSet.Entry; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.regex.Pattern; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.Issue; class DefaultIssueSelectorTest { private static class MockEntry extends Entry { private final String msg; MockEntry(String msg) { this.msg = msg; } @Override public Collection getAffectedPaths() { return null; } @Override public User getAuthor() { return null; } @Override public String getMsg() { return this.msg; } } @Test @Issue("4132") void projectNamesAllowed() { FreeStyleBuild build = mock(FreeStyleBuild.class); ChangeLogSet changeLogSet = mock(ChangeLogSet.class); BuildListener listener = mock(BuildListener.class); JiraSite site = mock(JiraSite.class); when(site.getIssuePattern()).thenReturn(JiraSite.DEFAULT_ISSUE_PATTERN); Set entries = new HashSet(Arrays.asList( new MockEntry("Fixed JI123-4711"), new MockEntry("Fixed foo_bar-4710"), new MockEntry("Fixed FoO_bAr-4711"), new MockEntry("Fixed something.\nJFoO_bAr_MULTI-4718"), new MockEntry("TR-123: foo"), new MockEntry("[ABC-42] hallo"), new MockEntry("#123: this one must not match"), new MockEntry("ABC-: this one must also not match"), new MockEntry("ABC-: \n\nABC-127:\nthis one should match"), new MockEntry("ABC-: \n\nABC-128:\nthis one should match"), new MockEntry("ABC-: \n\nXYZ-10:\nXYZ-20 this one too"), new MockEntry("Fixed DOT-4."), new MockEntry("Fixed DOT-5. Did it right this time"))); when(build.getChangeSet()).thenReturn(changeLogSet); when(changeLogSet.iterator()).thenReturn(entries.iterator()); List> changeSets = new ArrayList<>(); changeSets.add(changeLogSet); when(build.getChangeSets()).thenReturn(changeSets); Set expected = new HashSet(Arrays.asList( "JI123-4711", "FOO_BAR-4710", "FOO_BAR-4711", "JFOO_BAR_MULTI-4718", "TR-123", "ABC-42", "ABC-127", "ABC-128", "XYZ-10", "XYZ-20", "DOT-4", "DOT-5")); Set result = new DefaultIssueSelector().findIssueIds(build, site, listener); assertEquals(expected.size(), result.size()); assertEquals(expected, result); } /** * Tests that the JiraIssueParameters are identified as updateable Jira * issues. */ @Test @Issue("12312") void findIssuesWithJiraParameters() { FreeStyleBuild build = mock(FreeStyleBuild.class); ChangeLogSet changeLogSet = mock(ChangeLogSet.class); BuildListener listener = mock(BuildListener.class); JiraSite site = mock(JiraSite.class); when(site.getIssuePattern()).thenReturn(JiraSite.DEFAULT_ISSUE_PATTERN); JiraIssueParameterValue parameter = mock(JiraIssueParameterValue.class); JiraIssueParameterValue parameterTwo = mock(JiraIssueParameterValue.class); ParametersAction action = mock(ParametersAction.class); List parameters = new ArrayList<>(); when(changeLogSet.iterator()).thenReturn(Collections.EMPTY_LIST.iterator()); when(build.getChangeSet()).thenReturn(changeLogSet); when(build.getAction(ParametersAction.class)).thenReturn(action); when(action.getParameters()).thenReturn(parameters); when(parameter.getValue()).thenReturn("JIRA-123"); when(parameterTwo.getValue()).thenReturn("JIRA-321"); // Initial state contains zero parameters Set ids = new DefaultIssueSelector().findIssueIds(build, site, listener); assertTrue(ids.isEmpty()); parameters.add(parameter); ids = new DefaultIssueSelector().findIssueIds(build, site, listener); assertEquals(1, ids.size()); assertEquals("JIRA-123", ids.iterator().next()); parameters.add(parameterTwo); ids = new DefaultIssueSelector().findIssueIds(build, site, listener); assertEquals(2, ids.size()); Set expected = new TreeSet(Arrays.asList("JIRA-123", "JIRA-321")); assertEquals(expected, ids); } @Test @Issue("6043") void userPatternNotMatch() { FreeStyleBuild build = mock(FreeStyleBuild.class); ChangeLogSet changeLogSet = mock(ChangeLogSet.class); when(build.getChangeSet()).thenReturn(changeLogSet); Set entries = new HashSet(Arrays.asList(new MockEntry("Fixed FOO_BAR-4711"))); when(changeLogSet.iterator()).thenReturn(entries.iterator()); Set ids = new LinkedHashSet<>(); DefaultIssueSelector.findIssues(build, ids, Pattern.compile("[(w)]"), mock(BuildListener.class)); assertEquals(0, ids.size()); } @Test @Issue("6043") void userPatternMatchTwoIssuesInOneComment() { FreeStyleBuild build = mock(FreeStyleBuild.class); ChangeLogSet changeLogSet = mock(ChangeLogSet.class); when(build.getChangeSet()).thenReturn(changeLogSet); Set entries = new HashSet(Arrays.asList( new MockEntry("Fixed toto [FOOBAR-4711] [FOOBAR-21] "), new MockEntry("[TEST-9] with [dede]"), new MockEntry("toto [maven-release-plugin] prepare release foo-2.2.3"))); when(changeLogSet.iterator()).thenReturn(entries.iterator()); List> changeSets = new ArrayList>(); changeSets.add(changeLogSet); when(build.getChangeSets()).thenReturn(changeSets); Set ids = new LinkedHashSet<>(); Pattern pat = Pattern.compile("\\[(\\w+-\\d+)\\]"); DefaultIssueSelector.findIssues(build, ids, pat, mock(BuildListener.class)); assertEquals(3, ids.size()); assertTrue(ids.contains("TEST-9")); assertTrue(ids.contains("FOOBAR-4711")); assertTrue(ids.contains("FOOBAR-21")); } @Test @Issue("6043") void userPatternMatch() { FreeStyleBuild build = mock(FreeStyleBuild.class); ChangeLogSet changeLogSet = mock(ChangeLogSet.class); when(build.getChangeSet()).thenReturn(changeLogSet); Set entries = new HashSet(Arrays.asList( new MockEntry("Fixed toto [FOOBAR-4711]"), new MockEntry("[TEST-9] with [dede]"), new MockEntry("toto [maven-release-plugin] prepare release foo-2.2.3"))); when(changeLogSet.iterator()).thenReturn(entries.iterator()); List> changeSets = new ArrayList<>(); changeSets.add(changeLogSet); when(build.getChangeSets()).thenReturn(changeSets); Set ids = new LinkedHashSet<>(); Pattern pat = Pattern.compile("\\[(\\w+-\\d+)\\]"); DefaultIssueSelector.findIssues(build, ids, pat, mock(BuildListener.class)); assertEquals(2, ids.size()); assertTrue(ids.contains("TEST-9")); assertTrue(ids.contains("FOOBAR-4711")); } /** * Tests that the default pattern doesn't match strings like 'project-1.1'. * These patterns are used e.g. by the maven release plugin. */ @Test void defaultPatternNotToMatchMavenRelease() { FreeStyleBuild build = mock(FreeStyleBuild.class); ChangeLogSet changeLogSet = mock(ChangeLogSet.class); when(build.getChangeSet()).thenReturn(changeLogSet); // commit messages like the one from the Maven release plugin must not // match Set entries = new HashSet(Arrays.asList(new MockEntry("prepare release project-4.7.1"))); when(changeLogSet.iterator()).thenReturn(entries.iterator()); Set ids = new LinkedHashSet<>(); DefaultIssueSelector.findIssues(build, ids, JiraSite.DEFAULT_ISSUE_PATTERN, null); assertEquals(0, ids.size()); } } ================================================ FILE: src/test/java/hudson/plugins/jira/selector/ExplicitIssueSelectorTest.java ================================================ package hudson.plugins.jira.selector; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.hamcrest.core.IsEqual.equalTo; import java.util.Collections; import java.util.Set; import org.junit.jupiter.api.Test; class ExplicitIssueSelectorTest { private static final String TEST_KEY = "EXAMPLE-1"; @Test void returnsExplicitCollections() { ExplicitIssueSelector jqlUpdaterIssueSelector = new ExplicitIssueSelector(Collections.singletonList(TEST_KEY)); Set foundIssueIds = jqlUpdaterIssueSelector.findIssueIds(null, null, null); assertThat(foundIssueIds, hasSize(1)); assertThat(foundIssueIds.iterator().next(), equalTo(TEST_KEY)); } } ================================================ FILE: src/test/java/hudson/plugins/jira/selector/JqlIssueSelectorTest.java ================================================ package hudson.plugins.jira.selector; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.hamcrest.collection.IsEmptyCollection.empty; import static org.hamcrest.core.IsEqual.equalTo; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.atlassian.jira.rest.client.api.domain.Issue; import hudson.model.AbstractProject; import hudson.model.Run; import hudson.plugins.jira.JiraSession; import hudson.plugins.jira.JiraSite; import java.io.IOException; import java.util.Collections; import java.util.Set; import java.util.concurrent.TimeoutException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class JqlIssueSelectorTest { private static final String TEST_JQL = "key='EXAMPLE-1'"; @Mock private JiraSite site; @Mock private JiraSession session; @Mock private AbstractProject project; @Mock private Run run; @BeforeEach void prepare() throws IOException { when(run.getParent()).thenReturn(project); when(site.getSession(project)).thenReturn(session); } @Test void dontDependOnRunAndTaskListener() { JqlIssueSelector jqlUpdaterIssueSelector = new JqlIssueSelector(TEST_JQL); Set foundIssues = jqlUpdaterIssueSelector.findIssueIds(run, site, null); assertThat(foundIssues, empty()); } @Test void callGetIssuesFromJqlSearch() throws IOException, TimeoutException { Issue issue = mock(Issue.class); when(issue.getKey()).thenReturn("EXAMPLE-1"); when(session.getIssuesFromJqlSearch(TEST_JQL)).thenReturn(Collections.singletonList(issue)); JqlIssueSelector jqlUpdaterIssueSelector = new JqlIssueSelector(TEST_JQL); Set foundIssueIds = jqlUpdaterIssueSelector.findIssueIds(run, site, null); assertThat(foundIssueIds, hasSize(1)); assertThat(foundIssueIds.iterator().next(), equalTo("EXAMPLE-1")); } } ================================================ FILE: src/test/java/hudson/plugins/jira/selector/perforce/JobIssueSelectorTest.java ================================================ package hudson.plugins.jira.selector.perforce; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import hudson.model.BuildListener; import hudson.model.FreeStyleBuild; import hudson.model.ParameterValue; import hudson.model.ParametersAction; import hudson.plugins.jira.JiraCarryOverAction; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.listissuesparameter.JiraIssueParameterValue; import hudson.scm.ChangeLogSet; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.TreeSet; import org.junit.jupiter.api.Test; public abstract class JobIssueSelectorTest { protected abstract JobIssueSelector createJobIssueSelector(); @Test public void findsIssuesWithJiraParameters() { FreeStyleBuild build = mock(FreeStyleBuild.class); ChangeLogSet changeLogSet = mock(ChangeLogSet.class); BuildListener listener = mock(BuildListener.class); JiraSite jiraSite = mock(JiraSite.class); JiraIssueParameterValue parameter = mock(JiraIssueParameterValue.class); JiraIssueParameterValue parameterTwo = mock(JiraIssueParameterValue.class); ParametersAction action = mock(ParametersAction.class); List parameters = new ArrayList<>(); when(listener.getLogger()).thenReturn(System.out); when(changeLogSet.iterator()).thenReturn(Collections.EMPTY_LIST.iterator()); when(build.getChangeSet()).thenReturn(changeLogSet); when(build.getAction(ParametersAction.class)).thenReturn(action); when(action.getParameters()).thenReturn(parameters); when(parameter.getValue()).thenReturn("JIRA-123"); when(parameterTwo.getValue()).thenReturn("JIRA-321"); Set ids; JobIssueSelector jobIssueSelector = createJobIssueSelector(); // Initial state contains zero parameters ids = jobIssueSelector.findIssueIds(build, jiraSite, listener); assertTrue(ids.isEmpty()); parameters.add(parameter); ids = jobIssueSelector.findIssueIds(build, jiraSite, listener); assertEquals(1, ids.size()); assertEquals("JIRA-123", ids.iterator().next()); parameters.add(parameterTwo); ids = jobIssueSelector.findIssueIds(build, jiraSite, listener); assertEquals(2, ids.size()); Set expected = new TreeSet(Arrays.asList("JIRA-123", "JIRA-321")); assertEquals(expected, ids); } @Test public void findsCarriedOnIssues() { FreeStyleBuild build = mock(FreeStyleBuild.class); FreeStyleBuild previousBuild = mock(FreeStyleBuild.class); ArrayList issues = new ArrayList<>(); issues.add("GC-131"); JiraCarryOverAction jiraCarryOverAction = mock(JiraCarryOverAction.class); when(build.getPreviousCompletedBuild()).thenReturn(previousBuild); when(previousBuild.getAction(JiraCarryOverAction.class)).thenReturn(jiraCarryOverAction); when(jiraCarryOverAction.getIDs()).thenReturn(issues); ChangeLogSet changeLogSet = mock(ChangeLogSet.class); BuildListener listener = mock(BuildListener.class); JiraSite jiraSite = mock(JiraSite.class); when(listener.getLogger()).thenReturn(System.out); when(changeLogSet.iterator()).thenReturn(Collections.EMPTY_LIST.iterator()); when(build.getChangeSet()).thenReturn(changeLogSet); JobIssueSelector jobIssueSelector = createJobIssueSelector(); Set ids = jobIssueSelector.findIssueIds(build, jiraSite, listener); assertEquals(1, ids.size()); Set expected = new TreeSet<>(Collections.singletonList("GC-131")); assertEquals(expected, ids); } } ================================================ FILE: src/test/java/hudson/plugins/jira/selector/perforce/P4JobIssueSelectorTest.java ================================================ package hudson.plugins.jira.selector.perforce; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.perforce.p4java.impl.generic.core.Fix; import hudson.model.BuildListener; import hudson.model.FreeStyleBuild; import hudson.plugins.jira.JiraSite; import hudson.scm.ChangeLogSet; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import org.jenkinsci.plugins.p4.changes.P4ChangeEntry; import org.jenkinsci.plugins.p4.changes.P4ChangeSet; import org.junit.jupiter.api.Test; class P4JobIssueSelectorTest extends JobIssueSelectorTest { @Test void findsTwoP4Jobs() { final String jobIdIW1231 = "IW-1231"; final String jobIdEC3453 = "EC-3453"; FreeStyleBuild build = mock(FreeStyleBuild.class); BuildListener listener = mock(BuildListener.class); ChangeLogSet changeLogSet = mock(ChangeLogSet.class); P4ChangeSet perforceChangeLogSet = mock(P4ChangeSet.class); Fix fixIW1231 = mock(Fix.class); Fix fixEC3453 = mock(Fix.class); when(fixIW1231.getJobId()).thenReturn(jobIdIW1231); when(fixEC3453.getJobId()).thenReturn(jobIdEC3453); when(listener.getLogger()).thenReturn(System.out); when(build.getChangeSet()).thenReturn(changeLogSet); ArrayList entries = new ArrayList<>(); P4ChangeEntry entry1 = new P4ChangeEntry(perforceChangeLogSet); entry1.getJobs().add(fixIW1231); entries.add(entry1); P4ChangeEntry entry2 = new P4ChangeEntry(perforceChangeLogSet); entry2.getJobs().add(fixEC3453); entries.add(entry2); when(changeLogSet.iterator()).thenReturn(entries.iterator()); List> changeSets = new ArrayList<>(); changeSets.add(changeLogSet); when(build.getChangeSets()).thenReturn(changeSets); Set expected = new HashSet<>(Arrays.asList(jobIdEC3453, jobIdIW1231)); P4JobIssueSelector selector = new P4JobIssueSelector(); JiraSite jiraSite = mock(JiraSite.class); Set result = selector.findIssueIds(build, jiraSite, listener); assertEquals(expected.size(), result.size()); assertEquals(expected, result); } @Override protected JobIssueSelector createJobIssueSelector() { return new P4JobIssueSelector(); } } ================================================ FILE: src/test/java/hudson/plugins/jira/versionparameter/JiraReleaseVersionParameterTest.java ================================================ package hudson.plugins.jira.versionparameter; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import hudson.model.ParametersDefinitionProperty; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; @WithJenkins class JiraReleaseVersionParameterTest { @Test void scriptedPipeline(JenkinsRule r) throws Exception { WorkflowJob p = r.createProject(WorkflowJob.class); p.setDefinition(new CpsFlowDefinition(""" properties([ parameters([ jiraReleaseVersion(name: 'JIRA', description: 'Jira Test Description', jiraProjectKey: 'PRJ', jiraReleasePattern: 'v[0-9]+', jiraShowReleased: 'true', jiraShowArchived: 'true') ]) ])""", true)); r.buildAndAssertSuccess(p); ParametersDefinitionProperty parameters = p.getProperty(ParametersDefinitionProperty.class); assertThat(parameters, is(notNullValue())); assertThat( parameters.getParameterDefinitions(), hasItem(allOf( instanceOf(JiraVersionParameterDefinition.class), hasProperty("name", is("JIRA")), hasProperty("description", is("Jira Test Description")), hasProperty("jiraProjectKey", is("PRJ"))))); } } ================================================ FILE: src/test/java/hudson/plugins/jira/versionparameter/JiraVersionParameterDefinitionTest.java ================================================ package hudson.plugins.jira.versionparameter; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; import static org.mockito.Mockito.mockStatic; import com.atlassian.jira.rest.client.api.domain.Version; import hudson.cli.CLICommand; import hudson.model.Item; import hudson.model.Job; import hudson.model.ParameterValue; import hudson.model.ParametersDefinitionProperty; import hudson.plugins.jira.JiraSession; import hudson.plugins.jira.JiraSite; import hudson.plugins.jira.Messages; import hudson.plugins.jira.extension.ExtendedVersion; import hudson.util.ListBoxModel; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.NullSource; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest2; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; class JiraVersionParameterDefinitionTest { List versions = new ArrayList<>(); JiraVersionParameterDefinition.Result resReleasedVer; JiraVersionParameterDefinition.Result resUnreleasedVer; JiraVersionParameterDefinition.Result resArchivedVer; JiraVersionParameterDefinition.Result resReleasedArchivedVer; private Job job; private JiraSite site; private JiraSession session; private StaplerRequest2 request2; ExtendedVersion extReleasedVer = new ExtendedVersion(null, 1L, "1.0", "", false, true, null, null); ExtendedVersion extUnReleasedVer = new ExtendedVersion(null, 2L, "1.1", "", false, false, null, null); ExtendedVersion extArchivedVer = new ExtendedVersion(null, 3L, "1.2", "", true, false, null, null); ExtendedVersion extReleasedArchivedVer = new ExtendedVersion(null, 4L, "1.3", "", true, true, null, null); @BeforeEach void createMocksAndVersions() { versions.add(extReleasedVer); versions.add(extUnReleasedVer); versions.add(extArchivedVer); versions.add(extReleasedArchivedVer); resReleasedVer = new JiraVersionParameterDefinition.Result(extReleasedVer); resUnreleasedVer = new JiraVersionParameterDefinition.Result(extUnReleasedVer); resArchivedVer = new JiraVersionParameterDefinition.Result(extArchivedVer); resReleasedArchivedVer = new JiraVersionParameterDefinition.Result(extReleasedArchivedVer); job = mock(Job.class); site = mock(JiraSite.class); session = mock(JiraSession.class); request2 = mock(StaplerRequest2.class); } @Test void parameterValueMethodOverrides() throws Exception { JiraVersionParameterDefinition definition = new JiraVersionParameterDefinition("pname", "pdesc", "JIRAKEY", null, "false", "true", "true"); assertEquals("JIRAKEY", definition.getJiraProjectKey()); assertEquals("false", definition.getJiraShowReleased()); assertEquals("true", definition.getJiraShowArchived()); assertEquals("true", definition.getJiraShowUnreleased()); assertEquals("pdesc", definition.getDescription()); CLICommand cliCommand = mock(CLICommand.class); ParameterValue value = definition.createValue(cliCommand, "Jira Version 1.2.3"); assertEquals(definition.getName(), value.getName()); assertEquals("Jira Version 1.2.3", value.getValue()); } static Stream createValueInvalidParameters() { return Stream.of( Arguments.of((Object) new String[] {}), Arguments.of((Object) new String[] {"a", "b"}), Arguments.of((Object) new String[] {""})); } @ParameterizedTest @NullSource @MethodSource("createValueInvalidParameters") void shouldCreateNullParameterForInvalidValues(String[] values) { JiraVersionParameterDefinition definition = new JiraVersionParameterDefinition("pname", "pdesc", "JIRAKEY", null, "false", "true", "true"); when(request2.getParameterValues(any())).thenReturn(values); ParameterValue result = definition.createValue(request2); assertNull(result); } @Test void shouldCreateValue() { JiraVersionParameterDefinition definition = new JiraVersionParameterDefinition("pname", "pdesc", "JIRAKEY", null, "false", "true", "true"); when(request2.getParameterValues(any())).thenReturn(new String[] {"value"}); ParameterValue result = definition.createValue(request2); assertNotNull(result); assertEquals("pname", result.getName()); assertEquals("value", result.getValue()); } @Test void showReleasedVersions() { withJiraStaticMocks(() -> { JiraVersionParameterDefinition def = new JiraVersionParameterDefinition("name", "desc", "PROJ", null, "true", "false", "false"); List result; try { result = def.getVersions(); } catch (IOException e) { throw new RuntimeException(e); } List expected = new ArrayList<>(List.of(resReleasedVer)); assertEquals(expected, result); }); } @Test void showUnreleasedVersions() { withJiraStaticMocks(() -> { JiraVersionParameterDefinition def = new JiraVersionParameterDefinition("name", "desc", "PROJ", null, "false", "false", "true"); List result; try { result = def.getVersions(); } catch (IOException e) { throw new RuntimeException(e); } List expected = new ArrayList<>(List.of(resUnreleasedVer)); assertEquals(expected, result); }); } @Test void showArchivedVersions() { withJiraStaticMocks(() -> { JiraVersionParameterDefinition def = new JiraVersionParameterDefinition("name", "desc", "PROJ", null, "false", "true", "false"); List result; try { result = def.getVersions(); } catch (IOException e) { throw new RuntimeException(e); } List expected = new ArrayList<>(List.of(resReleasedArchivedVer, resArchivedVer)); assertEquals(expected, result); }); } @Test void showAllVersions() { withJiraStaticMocks(() -> { JiraVersionParameterDefinition def = new JiraVersionParameterDefinition("name", "desc", "PROJ", null, "false", "false", "false"); List result; try { result = def.getVersions(); } catch (IOException e) { throw new RuntimeException(e); } List expected = new ArrayList<>(List.of(resReleasedArchivedVer, resArchivedVer, resUnreleasedVer, resReleasedVer)); assertEquals(expected, result); }); } @Test void showReleasedUnreleasedVersions() { withJiraStaticMocks(() -> { JiraVersionParameterDefinition def = new JiraVersionParameterDefinition("name", "desc", "PROJ", null, "true", "false", "true"); List result; try { result = def.getVersions(); } catch (IOException e) { throw new RuntimeException(e); } List expected = new ArrayList<>(List.of(resUnreleasedVer, resReleasedVer)); assertEquals(expected, result); }); } @Test void showReleasedArchivedVersions() { withJiraStaticMocks(() -> { JiraVersionParameterDefinition def = new JiraVersionParameterDefinition("name", "desc", "PROJ", null, "true", "true", "false"); List result; try { result = def.getVersions(); } catch (IOException e) { throw new RuntimeException(e); } List expected = new ArrayList<>(List.of(resReleasedArchivedVer, resArchivedVer, resReleasedVer)); assertEquals(expected, result); }); } @Test void showUnreleasedArchivedVersions() { withJiraStaticMocks(() -> { JiraVersionParameterDefinition def = new JiraVersionParameterDefinition("name", "desc", "PROJ", null, "false", "true", "true"); List result; try { result = def.getVersions(); } catch (IOException e) { throw new RuntimeException(e); } List expected = new ArrayList<>(List.of(resReleasedArchivedVer, resArchivedVer, resUnreleasedVer)); assertEquals(expected, result); }); } @Test void equalResults() { JiraVersionParameterDefinition.Result res1 = new JiraVersionParameterDefinition.Result(extReleasedVer); JiraVersionParameterDefinition.Result res2 = new JiraVersionParameterDefinition.Result(extReleasedVer); assertTrue(res1.equals(res2)); } @Test void diffResults() { JiraVersionParameterDefinition.Result res1 = new JiraVersionParameterDefinition.Result(extReleasedVer); JiraVersionParameterDefinition.Result res2 = new JiraVersionParameterDefinition.Result(extUnReleasedVer); assertFalse(res1.equals(res2)); } @Test void nullResultCompare() { JiraVersionParameterDefinition.Result res1 = new JiraVersionParameterDefinition.Result(extReleasedVer); JiraVersionParameterDefinition.Result res2 = null; assertFalse(res1.equals(res2)); } @Test void diffClassResultCompare() { JiraVersionParameterDefinition.Result res1 = new JiraVersionParameterDefinition.Result(extReleasedVer); assertFalse(res1.equals(extReleasedVer)); } @Test void sameNameDiffIdResultCompare() { JiraVersionParameterDefinition.Result res1 = new JiraVersionParameterDefinition.Result(extReleasedVer); ExtendedVersion version = new ExtendedVersion(null, 2L, "1.0", "", false, true, null, null); JiraVersionParameterDefinition.Result res2 = new JiraVersionParameterDefinition.Result(version); assertFalse(res1.equals(res2)); } @Test void compareResultEqHashCode() { JiraVersionParameterDefinition.Result res1 = new JiraVersionParameterDefinition.Result(extReleasedVer); JiraVersionParameterDefinition.Result res2 = new JiraVersionParameterDefinition.Result(extReleasedVer); assertEquals(res1.hashCode(), res2.hashCode()); } @Test void compareResultNotEqHashCode() { JiraVersionParameterDefinition.Result res1 = new JiraVersionParameterDefinition.Result(extReleasedVer); JiraVersionParameterDefinition.Result res2 = new JiraVersionParameterDefinition.Result(extArchivedVer); assertNotEquals(res1.hashCode(), res2.hashCode()); } @Test void getJiraShowUnreleasedOn() { JiraVersionParameterDefinition def = new JiraVersionParameterDefinition("name", "desc", "PROJ", null, "false", "true", "true"); assertEquals("true", def.getJiraShowUnreleased()); } private void withJiraStaticMocks(Runnable testLogic) { try (MockedStatic staplerMock = mockStatic(Stapler.class); MockedStatic siteMock = mockStatic(JiraSite.class)) { staplerMock.when(Stapler::getCurrentRequest2).thenReturn(request2); siteMock.when(() -> JiraSite.get(job)).thenReturn(site); when(request2.findAncestorObject(Job.class)).thenReturn(job); when(site.getSession(job)).thenReturn(session); when(session.getVersions("PROJ")).thenReturn(versions); testLogic.run(); } catch (Exception e) { throw new RuntimeException(e); } } @Nested @ExtendWith(MockitoExtension.class) class DescriptorImplTest { private JiraVersionParameterDefinition.DescriptorImpl uut = new JiraVersionParameterDefinition.DescriptorImpl(); @Test void shouldFillVersionItems( @Mock Job job, @Mock ParametersDefinitionProperty propertyDef, @Mock JiraVersionParameterDefinition paramDef) { when(job.hasPermission(Item.BUILD)).thenReturn(true); when(job.getProperty(ParametersDefinitionProperty.class)).thenReturn(propertyDef); when(propertyDef.getParameterDefinition("PARAM_NAME")).thenReturn(paramDef); Version version = new Version(null, 1L, "1.0.0", "", false, false, null); JiraVersionParameterDefinition.Result item = new JiraVersionParameterDefinition.Result(version); when(paramDef.getVersions(any())).thenReturn(List.of(item)); ListBoxModel result = uut.doFillVersionItems(job, "PARAM_NAME"); assertThat(result, hasSize(1)); ListBoxModel.Option option = result.get(0); assertEquals("1.0.0", option.value); assertEquals("1.0.0", option.name); verify(job).hasPermission(Item.BUILD); } @Test void shouldNotFillVersionsItemsIfPermissionMissing(@Mock Job job) { ListBoxModel result = uut.doFillVersionItems(job, "PARAM_NAME"); assertThat(result, hasSize(1)); ListBoxModel.Option option = result.get(0); assertEquals("", option.value); assertEquals(Messages.JiraVersionParameterDefinition_NoVersionsMatchedSearch(), option.name); verify(job).hasPermission(Item.BUILD); } @Test void shouldHaveNoSearchMatchesItemIfSearchMatchesNoItem( @Mock Job job, @Mock ParametersDefinitionProperty propertyDef, @Mock JiraVersionParameterDefinition paramDef) { when(job.hasPermission(Item.BUILD)).thenReturn(true); when(job.getProperty(ParametersDefinitionProperty.class)).thenReturn(propertyDef); when(propertyDef.getParameterDefinition("PARAM_NAME")).thenReturn(paramDef); ListBoxModel result = uut.doFillVersionItems(job, "PARAM_NAME"); assertThat(result, hasSize(1)); ListBoxModel.Option option = result.get(0); assertEquals("", option.value); assertEquals(Messages.JiraVersionParameterDefinition_NoVersionsMatchedSearch(), option.name); verify(job).hasPermission(Item.BUILD); } } } ================================================ FILE: src/test/java/hudson/plugins/jira/versionparameter/VersionComparatorTest.java ================================================ package hudson.plugins.jira.versionparameter; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import com.atlassian.jira.rest.client.api.domain.Version; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import org.hamcrest.collection.ArrayMatching; import org.junit.jupiter.api.Test; class VersionComparatorTest { @Test void complexCompare() { String[] input = { "9.9.9.9.9", "V-5.2.3", "PDFREPORT-2.3.4", "PDFREPORT-2.3", "1.12.2.3.4", "1.3.4", "PDFREPORT-", "1.1.1.2", "VER 1.0", "1.1.1.1", "FOO-1.1.1-RC1", "FOO-1.1.1-RC2", "1.1.1-RC1", "1.1.1-RC2", }; String[] expected = { "9.9.9.9.9", "1.12.2.3.4", "1.3.4", "1.1.1.2", "1.1.1.1", "1.1.1-RC2", "1.1.1-RC1", "VER 1.0", "V-5.2.3", "PDFREPORT-2.3.4", "PDFREPORT-2.3", "PDFREPORT-", "FOO-1.1.1-RC2", "FOO-1.1.1-RC1", }; List result = Arrays.asList(input).stream() .map(s -> new Version(null, null, s, null, false, false, null)) .sorted(VersionComparator.INSTANCE) .map(Version::getName) .collect(Collectors.toList()); assertThat(expected, ArrayMatching.arrayContaining(result.toArray(new String[result.size()]))); } @Test void singleComparisonsTests() { assertEquals(0, compare("1.1.1.1", "1.1.1.1")); assertEquals(1, compare("A-1.1.1.1", "1.1.1.1")); assertEquals(-1, compare("1.1.1.1", "A-1.1.1.1")); assertEquals(1, compare("1.1.1.1", "1.1.1.1.1")); assertEquals(-1, compare("1.1.1.1", "1.1.1")); assertEquals(1, compare("1.1.1.2", "1.1.1.3")); assertEquals(-1, compare("2.2.2.1", "1.1.1.2")); assertEquals(-1, compare("2.2.2", "1.1.1.2")); assertEquals(-1, compare("2.0", "1.0.15.3")); assertEquals(1, compare("2.0.5.4", "4.0")); assertEquals(-1, compare("1.12.1.1", "1.1.1.2")); assertEquals(1, compare("1.1.1-RC1", "1.1.1-RC2")); assertEquals(1, compare("PDFREPORT-2.3.4", "1.2.3")); assertEquals(1, compare("PDFREPORT-2.3.4", "4.5.6")); assertEquals(1, compare("PDFREPORT-2.3.4", "x")); assertEquals(0, compare("PDFREPORT2-", "PDFREPORT2-")); assertEquals(1, compare("PDFREPORT-", "PDFREPORT2-")); assertEquals(-1, compare("1.1.2-RC1", "1.1.1-RC2")); assertEquals(-1, compare("2.2.2-RC1", "1.1.1-RC1")); assertEquals(1, compare("FOO-1.1.1-RC1", "FOO-1.1.1-RC2")); } private int compare(String v1, String v2) { return VersionComparator.INSTANCE.compare( new Version(null, null, v1, null, false, false, null), new Version(null, null, v2, null, false, false, null)); } } ================================================ FILE: src/test/resources/hudson/plugins/jira/JiraBuildActionTest/binaryCompatibility/config.xml ================================================ 2.222.1 Jenkins JiraBuildActionTest config ================================================ FILE: src/test/resources/hudson/plugins/jira/JiraBuildActionTest/binaryCompatibility/jobs/project/builds/2/build.xml ================================================ 39 1585749717875 1585749717879 SUCCESS 240 UTF-8 false /var/jenkins_home/jobs/global/workspace 2.222.1 admin 2 JIRA-123 Issue summary ================================================ FILE: src/test/resources/hudson/plugins/jira/JiraBuildActionTest/binaryCompatibility/jobs/project/config.xml ================================================ false https://jira.atlassian.net/ true false false false false TEST-1 TEST-1 ================================================ FILE: src/test/resources/hudson/plugins/jira/multiple-sites.yml ================================================ unclassified: jiraglobalconfiguration: sites: - url: "https://issues.jenkins-ci.org/" - url: "https://jira.com/" ================================================ FILE: src/test/resources/hudson/plugins/jira/oldJiraProjectProperty.xml ================================================ https://backwardsCompatURL.com/ false false false false false 10 30 10 false ================================================ FILE: src/test/resources/hudson/plugins/jira/single-site.yml ================================================ unclassified: jiraglobalconfiguration: sites: - url: "https://jira.com/" ================================================ FILE: src/test/resources/jira.properties ================================================ url=http://host/jira/rpc/soap/jirasoapservice-v2 username=user password=passwd token=token