diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 13e79499e..d46ce3932 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,7 @@ 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 @@ -22,15 +23,14 @@ brew install pre-commit && pre-commit install --install-hooks 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. -```bash ### Atlassian sources import -To resolve some binary compatibility issues [JENKINS-48357](https://issues.jenkins-ci.org/browse/JENKINS-48357), +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) +sources are Apache License (see pom in src/main/resources/atlassian-httpclient-plugin-0.23.0.pom) ### Testing @@ -38,4 +38,4 @@ There is a [Jira Cloud](https://jenkins-jira-plugin.atlassian.net/) test instanc ### Releasing the plugin -Make sure you have your `~/.m2/settings.xml` configured accordingly - refer to [releasing Jenkins plugins](https://www.jenkins.io/doc/developer/publishing/releasing/). +See [releasing Jenkins plugins](https://www.jenkins.io/doc/developer/publishing/releasing-manually/). diff --git a/README.md b/README.md index 878118007..5aaadaa82 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ [![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) -See user documentation at [https://jenkinsci.github.io/jira-plugin/](https://jenkinsci.github.io/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 @@ -22,10 +24,3 @@ See user documentation at [https://jenkinsci.github.io/jira-plugin/](https://jen 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/). -## Contributing - -There have been many developers involved in the development of this plugin and there are many downstream users who depend on it. Tests help us assure that we're delivering a reliable plugin and that we've communicated our intent to other developers in a way that they can detect when they run tests. - -- each change should be covered by appropriate unit tests -- in case it is not testable via a unit test, it should be tested against a real Jira instance - possibly both Jira Server and Jira Cloud. There is a [Jira Cloud test instance](https://jenkins-jira-plugin.atlassian.net/) that we are using for testing the plugin releases - let us know in the Pull Request in case you need access for testing - diff --git a/src/main/java/hudson/plugins/jira/JiraVersionCreator.java b/src/main/java/hudson/plugins/jira/JiraVersionCreator.java index 7b8ffc7eb..0ef45ef66 100644 --- a/src/main/java/hudson/plugins/jira/JiraVersionCreator.java +++ b/src/main/java/hudson/plugins/jira/JiraVersionCreator.java @@ -11,19 +11,22 @@ 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 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) { @@ -52,9 +55,22 @@ 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) { - return new VersionCreator().perform(build.getProject(), jiraVersion, jiraProjectKey, build, listener); + VersionCreator versionCreator = new VersionCreator(); + versionCreator.setFailIfAlreadyExists(failIfAlreadyExists); + versionCreator.setJiraVersion(jiraVersion); + versionCreator.setJiraProjectKey(jiraProjectKey); + return versionCreator.perform(build.getProject(), build, listener); } @Override @@ -62,6 +78,14 @@ public BuildStepDescriptor getDescriptor() { return DESCRIPTOR; } + protected Object readResolve() { + if (failIfAlreadyExists == null) { + setFailIfAlreadyExists(true); + } + + return this; + } + @Extension public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); diff --git a/src/main/java/hudson/plugins/jira/JiraVersionCreatorBuilder.java b/src/main/java/hudson/plugins/jira/JiraVersionCreatorBuilder.java index c88a797cd..11ab242f6 100644 --- a/src/main/java/hudson/plugins/jira/JiraVersionCreatorBuilder.java +++ b/src/main/java/hudson/plugins/jira/JiraVersionCreatorBuilder.java @@ -12,6 +12,7 @@ import net.sf.json.JSONObject; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.StaplerRequest2; /** @@ -25,6 +26,7 @@ public class JiraVersionCreatorBuilder extends Builder implements SimpleBuildSte private String jiraVersion; private String jiraProjectKey; + private Boolean failIfAlreadyExists = true; @DataBoundConstructor public JiraVersionCreatorBuilder(String jiraVersion, String jiraProjectKey) { @@ -53,9 +55,30 @@ 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) { - new VersionCreator().perform(run.getParent(), jiraVersion, jiraProjectKey, run, listener); + VersionCreator versionCreator = new VersionCreator(); + versionCreator.setFailIfAlreadyExists(failIfAlreadyExists); + versionCreator.setJiraVersion(jiraVersion); + versionCreator.setJiraProjectKey(jiraProjectKey); + versionCreator.perform(run.getParent(), run, listener); } @Override diff --git a/src/main/java/hudson/plugins/jira/VersionCreator.java b/src/main/java/hudson/plugins/jira/VersionCreator.java index d35ec6ae2..d7e80903e 100644 --- a/src/main/java/hudson/plugins/jira/VersionCreator.java +++ b/src/main/java/hudson/plugins/jira/VersionCreator.java @@ -20,8 +20,28 @@ class VersionCreator { private static final Logger LOGGER = Logger.getLogger(VersionCreator.class.getName()); - protected boolean perform( - Job project, String jiraVersion, String jiraProjectKey, Run build, TaskListener listener) { + 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; @@ -42,13 +62,17 @@ protected boolean perform( List existingVersions = Optional.ofNullable(session.getVersions(realProjectKey)).orElse(Collections.emptyList()); - // past logic to fail the build if the version already exists + // check if version already exists if (existingVersions.stream().anyMatch(v -> v.getName().equals(finalRealVersion))) { listener.getLogger().println(Messages.JiraVersionCreator_VersionExists(realVersion, realProjectKey)); - if (listener instanceof BuildListener) { - ((BuildListener) listener).finished(Result.FAILURE); + if (failIfAlreadyExists) { + if (listener instanceof BuildListener) { + ((BuildListener) listener).finished(Result.FAILURE); + } + return false; } - return false; + // version exists but we don't fail the build + return true; } listener.getLogger().println(Messages.JiraVersionCreator_CreatingVersion(realVersion, realProjectKey)); @@ -59,9 +83,6 @@ protected boolean perform( listener.fatalError("Unable to add version %s to Jira project %s", realVersion, realProjectKey, e)); } - if (listener instanceof BuildListener) { - ((BuildListener) listener).finished(Result.FAILURE); - } return false; } diff --git a/src/main/resources/hudson/plugins/jira/JiraVersionCreator/config.jelly b/src/main/resources/hudson/plugins/jira/JiraVersionCreator/config.jelly index 6cff006ec..c35f0ccc0 100644 --- a/src/main/resources/hudson/plugins/jira/JiraVersionCreator/config.jelly +++ b/src/main/resources/hudson/plugins/jira/JiraVersionCreator/config.jelly @@ -6,4 +6,7 @@ + + + \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/jira/JiraVersionCreator/help-failIfAlreadyExists.html b/src/main/resources/hudson/plugins/jira/JiraVersionCreator/help-failIfAlreadyExists.html new file mode 100644 index 000000000..1b85e84c8 --- /dev/null +++ b/src/main/resources/hudson/plugins/jira/JiraVersionCreator/help-failIfAlreadyExists.html @@ -0,0 +1,4 @@ +
+ 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. +
\ No newline at end of file diff --git a/src/main/resources/hudson/plugins/jira/JiraVersionCreatorBuilder/config.jelly b/src/main/resources/hudson/plugins/jira/JiraVersionCreatorBuilder/config.jelly index 6cff006ec..c35f0ccc0 100644 --- a/src/main/resources/hudson/plugins/jira/JiraVersionCreatorBuilder/config.jelly +++ b/src/main/resources/hudson/plugins/jira/JiraVersionCreatorBuilder/config.jelly @@ -6,4 +6,7 @@ + + + \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/jira/JiraVersionCreatorBuilder/help-failIfAlreadyExists.html b/src/main/resources/hudson/plugins/jira/JiraVersionCreatorBuilder/help-failIfAlreadyExists.html new file mode 100644 index 000000000..1b85e84c8 --- /dev/null +++ b/src/main/resources/hudson/plugins/jira/JiraVersionCreatorBuilder/help-failIfAlreadyExists.html @@ -0,0 +1,4 @@ +
+ 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. +
\ No newline at end of file diff --git a/src/main/webapp/help-version-create.html b/src/main/webapp/help-version-create.html index f38b8a54e..2b8de2e14 100644 --- a/src/main/webapp/help-version-create.html +++ b/src/main/webapp/help-version-create.html @@ -1,3 +1,3 @@
- Creates a new version in Jira in the project with the given key. If the version already exists, the build will fail. + 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.
diff --git a/src/test/java/hudson/plugins/jira/JiraVersionCreatorBuilderTest.java b/src/test/java/hudson/plugins/jira/JiraVersionCreatorBuilderTest.java index 669822e0e..591bedca5 100644 --- a/src/test/java/hudson/plugins/jira/JiraVersionCreatorBuilderTest.java +++ b/src/test/java/hudson/plugins/jira/JiraVersionCreatorBuilderTest.java @@ -1,9 +1,15 @@ 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.*; +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; @@ -11,6 +17,7 @@ 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 { @@ -19,6 +26,8 @@ class JiraVersionCreatorBuilderTest { private JiraSession session; + private final XStream2 xStream2 = new XStream2(); + @BeforeEach void createMocks() { site = mock(JiraSite.class); @@ -38,4 +47,56 @@ void testPipelineWithJiraSite(JenkinsRule r) throws Exception { 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()); + } } diff --git a/src/test/java/hudson/plugins/jira/VersionCreatorTest.java b/src/test/java/hudson/plugins/jira/VersionCreatorTest.java index 4f237acfb..91788001d 100644 --- a/src/test/java/hudson/plugins/jira/VersionCreatorTest.java +++ b/src/test/java/hudson/plugins/jira/VersionCreatorTest.java @@ -96,7 +96,9 @@ void callsJiraWithSpecifiedParameters() throws InterruptedException, IOException // for new version, verify the addVersion method is called when(session.getVersions(JIRA_PRJ)).thenReturn(null); - boolean result = versionCreator.perform(project, JIRA_VER, JIRA_PRJ, build, listener); + 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)); @@ -106,7 +108,9 @@ void callsJiraWithSpecifiedParameters() throws InterruptedException, IOException // for existing version, verify the addVersion method is not called reset(session); when(session.getVersions(JIRA_PRJ)).thenReturn(Arrays.asList(existingVersion)); - result = versionCreator.perform(project, JIRA_VER, JIRA_PRJ, build, listener); + 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); @@ -120,7 +124,9 @@ void expandsEnvParameters() throws InterruptedException, IOException, RestClient // for new version, verify the addVersion method is called when(session.getVersions(JIRA_PRJ)).thenReturn(null); - boolean result = versionCreator.perform(project, JIRA_VER_PARAM, JIRA_PRJ_PARAM, build, listener); + 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)); @@ -129,7 +135,9 @@ void expandsEnvParameters() throws InterruptedException, IOException, RestClient // for existing version, verify the addVersion method is called reset(session); when(session.getVersions(JIRA_PRJ)).thenReturn(Arrays.asList(existingVersion)); - result = versionCreator.perform(project, JIRA_VER_PARAM, JIRA_PRJ_PARAM, build, listener); + 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); @@ -143,8 +151,32 @@ void buildDidNotFailWhenVersionExists() throws IOException, InterruptedException 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); - versionCreator.perform(project, JIRA_VER_PARAM, JIRA_PRJ_PARAM, 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)); } }