diff --git a/changelog.html b/changelog.html index 5b0149943b..77239d0c2e 100644 --- a/changelog.html +++ b/changelog.html @@ -60,18 +60,45 @@ Upcoming changes Internal/build: jenkins-ui (NPM module) is private, used only internally. (issue 34629)
  • - Do not print stack trace if a plugin is missing its dependencies. + Do not print stack trace during a plugin installation if it is missing its dependencies. (issue 34683)
  • Allow specifying custom AbortExceptions. (pull 2288)
  • - Internal: CLI command disconnect-node extracted from core to CLI. - (issue 34328) + Add a hudson.model.UpdateCenter.defaultUpdateSiteId system property, + which allows specifying an alternate default Update Site ID. + (issue 34674)
  • - Developer API: Switch Jenkins.getInstance() to @Nonnull - (new system property). + Remove the historical initialization of CVS changelog parser for jobs without explicit SCM definition. + Warning! This change may potentially cause a regression if a Jenkins plugin depends on this default behavior and injects changelogs without SCM. + (issue 4610) +
  • + Prevent hanging of the Installation Wizard if the default Update Site ID cannot be resolved. + In such case an error message will be displayed. + (issue 34675) +
  • + Do not fail with error when enabling a plugin, which has been already enabled. + It prevents errors in the new Installation Wizard, which installs plugins in parallel. + (issue 34710) +
  • + Disable JSESSIONID in URLs when running in the JBoss web container. + It prevents Error 404 due to invalid links starting from Jenkins 1.556. + More info: WFLY-4782 + (issue 34675) +
  • + Internal: CLI commands disconnect-node and reload-configuration were extracted from the core to CLI. + (issue 34328 and + issue 31900) +
  • + Internal: Support latest source version to avoid compile time warnings with JDK7. + (issue 32978) +
  • + Developer API: Switch Jenkins.getInstance() to @Nonnull. + (new system property) (pull 2297) +
  • + Tool installers now follow http redirects properly (issue 23507) diff --git a/core/pom.xml b/core/pom.xml index 5d0b5a5887..cae0dfbd24 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -195,7 +195,7 @@ THE SOFTWARE. org.jenkins-ci annotation-indexer - 1.7 + 1.11 org.jenkins-ci @@ -539,7 +539,7 @@ THE SOFTWARE. net.java.sezpoz sezpoz - 1.9 + 1.11 org.kohsuke.jinterop diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index 348bc2d4e2..6b2529eabe 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -26,6 +26,7 @@ package hudson; import jenkins.util.SystemProperties; +import com.google.common.annotations.VisibleForTesting; import com.jcraft.jzlib.GZIPInputStream; import com.jcraft.jzlib.GZIPOutputStream; import hudson.Launcher.LocalLauncher; @@ -72,6 +73,8 @@ import org.apache.tools.ant.Project; import org.apache.tools.ant.types.FileSet; import org.apache.tools.zip.ZipEntry; import org.apache.tools.zip.ZipFile; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.Stapler; import javax.annotation.CheckForNull; @@ -92,6 +95,7 @@ import java.io.RandomAccessFile; import java.io.Serializable; import java.io.Writer; import java.net.HttpURLConnection; +import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.net.URLConnection; @@ -189,6 +193,11 @@ import org.jenkinsci.remoting.RoleSensitive; * @see VirtualFile */ public final class FilePath implements Serializable { + /** + * Maximum http redirects we will follow. This defaults to the same number as Firefox/Chrome tolerates. + */ + private static final int MAX_REDIRECTS = 20; + /** * When this {@link FilePath} represents the remote path, * this field is always non-null on master (the field represents @@ -756,6 +765,10 @@ public final class FilePath implements Serializable { * @since 1.299 */ public boolean installIfNecessaryFrom(@Nonnull URL archive, @CheckForNull TaskListener listener, @Nonnull String message) throws IOException, InterruptedException { + return installIfNecessaryFrom(archive, listener, message, MAX_REDIRECTS); + } + + private boolean installIfNecessaryFrom(@Nonnull URL archive, @CheckForNull TaskListener listener, @Nonnull String message, int maxRedirects) throws InterruptedException, IOException { try { FilePath timestamp = this.child(".timestamp"); long lastModified = timestamp.lastModified(); @@ -778,14 +791,28 @@ public final class FilePath implements Serializable { } } - if (lastModified != 0 && con instanceof HttpURLConnection) { + if (con instanceof HttpURLConnection) { HttpURLConnection httpCon = (HttpURLConnection) con; int responseCode = httpCon.getResponseCode(); - if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) { - return false; - } else if (responseCode != HttpURLConnection.HTTP_OK) { - listener.getLogger().println("Skipping installation of " + archive + " to " + remote + " due to server error: " + responseCode + " " + httpCon.getResponseMessage()); - return false; + if (responseCode == HttpURLConnection.HTTP_MOVED_PERM + || responseCode == HttpURLConnection.HTTP_MOVED_TEMP) { + // follows redirect + if (maxRedirects > 0) { + String location = httpCon.getHeaderField("Location"); + listener.getLogger().println("Following redirect " + archive.toExternalForm() + " -> " + location); + return installIfNecessaryFrom(getUrlFactory().newURL(location), listener, message, maxRedirects - 1); + } else { + listener.getLogger().println("Skipping installation of " + archive + " to " + remote + " due to too many redirects."); + return false; + } + } + if (lastModified != 0) { + if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) { + return false; + } else if (responseCode != HttpURLConnection.HTTP_OK) { + listener.getLogger().println("Skipping installation of " + archive + " to " + remote + " due to server error: " + responseCode + " " + httpCon.getResponseMessage()); + return false; + } } } @@ -2521,6 +2548,31 @@ public final class FilePath implements Serializable { }); } + private static final UrlFactory DEFAULT_URL_FACTORY = new UrlFactory(); + + @Restricted(NoExternalUse.class) + static class UrlFactory { + public URL newURL(String location) throws MalformedURLException { + return new URL(location); + } + } + + private UrlFactory urlFactory; + + @VisibleForTesting + @Restricted(NoExternalUse.class) + void setUrlFactory(UrlFactory urlFactory) { + this.urlFactory = urlFactory; + } + + private UrlFactory getUrlFactory() { + if (urlFactory != null) { + return urlFactory; + } else { + return DEFAULT_URL_FACTORY; + } + } + /** * Short for {@code validateFileMask(path, value, true)} */ diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index 733c630031..56a4b0b5ad 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -559,9 +559,10 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas } if (dependencyURL != null) { - dependencySet.add(dependencyURL); // And transitive deps... addDependencies(dependencyURL, fromPath, dependencySet); + // And then add the current plugin + dependencySet.add(dependencyURL); } } } diff --git a/core/src/main/java/hudson/model/Job.java b/core/src/main/java/hudson/model/Job.java index 263787ffcb..69cc02708e 100644 --- a/core/src/main/java/hudson/model/Job.java +++ b/core/src/main/java/hudson/model/Job.java @@ -369,6 +369,7 @@ public abstract class Job, RunT extends Run,RunT extends Run - - - - - ${%login} + ${%login} diff --git a/core/src/main/resources/hudson/triggers/TimerTrigger/help-spec.html b/core/src/main/resources/hudson/triggers/TimerTrigger/help-spec.html index d097fc4193..bfe1d96e21 100644 --- a/core/src/main/resources/hudson/triggers/TimerTrigger/help-spec.html +++ b/core/src/main/resources/hudson/triggers/TimerTrigger/help-spec.html @@ -73,8 +73,10 @@ H/15 * * * * # every ten minutes in the first half of every hour (three times, perhaps at :04, :14, :24) H(0-29)/10 * * * * -# once every two hours every weekday (perhaps at 9:38 AM, 11:38 AM, 1:38 PM, 3:38 PM) -H 9-16/2 * * 1-5 +# once every two hours at 45 minutes past the hour starting at 9:45 AM and finishing at 3:45 PM every weekday. +45 9-16/2 * * 1-5 +# once in every two hours slot between 9 AM and 5 PM every weekday (perhaps at 10:38 AM, 12:38 PM, 2:38 PM, 4:38 PM) +H H(9-16)/2 * * 1-5 # once a day on the 1st and 15th of every month except December H H 1,15 1-11 * diff --git a/core/src/main/resources/jenkins/model/CoreEnvironmentContributor/buildEnv.groovy b/core/src/main/resources/jenkins/model/CoreEnvironmentContributor/buildEnv.groovy index bfcc1c0335..31cdf2c1ff 100644 --- a/core/src/main/resources/jenkins/model/CoreEnvironmentContributor/buildEnv.groovy +++ b/core/src/main/resources/jenkins/model/CoreEnvironmentContributor/buildEnv.groovy @@ -3,7 +3,7 @@ package jenkins.model.CoreEnvironmentContributor; def l = namespace(lib.JenkinsTagLib) // also advertises those contributed by Run.getCharacteristicEnvVars() -["BUILD_NUMBER","BUILD_ID","BUILD_DISPLAY_NAME","JOB_NAME","BUILD_TAG","EXECUTOR_NUMBER","NODE_NAME","NODE_LABELS","WORKSPACE","JENKINS_HOME","JENKINS_URL","BUILD_URL","JOB_URL"].each { name -> +["BUILD_NUMBER","BUILD_ID","BUILD_DISPLAY_NAME","JOB_NAME", "JOB_BASE_NAME","BUILD_TAG","EXECUTOR_NUMBER","NODE_NAME","NODE_LABELS","WORKSPACE","JENKINS_HOME","JENKINS_URL","BUILD_URL","JOB_URL"].each { name -> l.buildEnvVar(name:name) { raw(_("${name}.blurb")) } diff --git a/core/src/main/resources/jenkins/model/CoreEnvironmentContributor/buildEnv.properties b/core/src/main/resources/jenkins/model/CoreEnvironmentContributor/buildEnv.properties index b1c4d4d8e1..8eed43cf12 100644 --- a/core/src/main/resources/jenkins/model/CoreEnvironmentContributor/buildEnv.properties +++ b/core/src/main/resources/jenkins/model/CoreEnvironmentContributor/buildEnv.properties @@ -1,7 +1,8 @@ BUILD_NUMBER.blurb=The current build number, such as "153" BUILD_ID.blurb=The current build ID, identical to BUILD_NUMBER for builds created in 1.597+, but a YYYY-MM-DD_hh-mm-ss timestamp for older builds BUILD_DISPLAY_NAME.blurb=The display name of the current build, which is something like "#153" by default. -JOB_NAME.blurb=Name of the project of this build, such as "foo" or "foo/bar". (To strip off folder paths from a Bourne shell script, try: $'{'JOB_NAME##*/}) +JOB_NAME.blurb=Name of the project of this build, such as "foo" or "foo/bar". +JOB_BASE_NAME.blurb=Short Name of the project of this build stripping off folder paths, such as "foo" for "bar/foo". BUILD_TAG.blurb=String of "jenkins-$'{'JOB_NAME}-$'{'BUILD_NUMBER}". Convenient to put into a resource file, a jar file, etc for easier identification. EXECUTOR_NUMBER.blurb=\ The unique number that identifies the current executor \ diff --git a/core/src/test/java/hudson/FilePathTest.java b/core/src/test/java/hudson/FilePathTest.java index 6788c93f54..7b515ec213 100644 --- a/core/src/test/java/hudson/FilePathTest.java +++ b/core/src/test/java/hudson/FilePathTest.java @@ -38,9 +38,11 @@ import java.io.OutputStream; import java.io.RandomAccessFile; import java.net.ConnectException; import java.net.HttpURLConnection; +import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -637,6 +639,28 @@ public class FilePathTest { assertTrue(log, log.contains("504 Gateway Timeout")); } + @Issue("JENKINS-23507") + @Test public void installIfNecessaryFollowsRedirects() throws Exception{ + File tmp = temp.getRoot(); + final FilePath d = new FilePath(tmp); + FilePath.UrlFactory urlFactory = mock(FilePath.UrlFactory.class); + d.setUrlFactory(urlFactory); + final HttpURLConnection con = mock(HttpURLConnection.class); + final HttpURLConnection con2 = mock(HttpURLConnection.class); + final URL url = someUrlToZipFile(con); + when(con.getResponseCode()).thenReturn(HttpURLConnection.HTTP_MOVED_TEMP); + URL url2 = someUrlToZipFile(con2); + String someUrl = url2.toExternalForm(); + when(con.getHeaderField("Location")).thenReturn(someUrl); + when(urlFactory.newURL(someUrl)).thenReturn(url2); + when(con2.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(con2.getInputStream()).thenReturn(someZippedContent()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + String message = "going ahead"; + assertTrue(d.installIfNecessaryFrom(url, new StreamTaskListener(baos), message)); + } + private URL someUrlToZipFile(final URLConnection con) throws IOException { final URLStreamHandler urlHandler = new URLStreamHandler() { diff --git a/pom.xml b/pom.xml index b75d90ba61..e2e6d88d52 100644 --- a/pom.xml +++ b/pom.xml @@ -577,7 +577,7 @@ THE SOFTWARE. org.codehaus.mojo animal-sniffer-maven-plugin - 1.9 + 1.15 diff --git a/test/src/test/java/hudson/cli/DeleteBuildsCommandTest.java b/test/src/test/java/hudson/cli/DeleteBuildsCommandTest.java new file mode 100644 index 0000000000..59d338c2d6 --- /dev/null +++ b/test/src/test/java/hudson/cli/DeleteBuildsCommandTest.java @@ -0,0 +1,317 @@ +/* + * The MIT License + * + * Copyright 2016 Red Hat, 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. + */ + +package hudson.cli; + +import hudson.model.ExecutorTest; +import hudson.model.FreeStyleProject; +import hudson.model.Job; +import hudson.model.Run; +import hudson.model.labels.LabelAtom; +import hudson.tasks.Shell; +import jenkins.model.Jenkins; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import static hudson.cli.CLICommandInvoker.Matcher.failedWith; +import static hudson.cli.CLICommandInvoker.Matcher.hasNoStandardOutput; +import static hudson.cli.CLICommandInvoker.Matcher.succeeded; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; + +/** + * @author pjanouse + */ +public class DeleteBuildsCommandTest { + + private CLICommandInvoker command; + + @Rule public final JenkinsRule j = new JenkinsRule(); + + @Before public void setUp() { + command = new CLICommandInvoker(j, "delete-builds"); + } + + @Test public void deleteBuildsShouldFailWithoutJobReadPermission() throws Exception { + j.createFreeStyleProject("aProject").scheduleBuild2(0).get(); + + final CLICommandInvoker.Result result = command + .authorizedTo(Jenkins.READ) + .invokeWithArgs("aProject", "1"); + assertThat(result, failedWith(3)); + assertThat(result, hasNoStandardOutput()); + assertThat(result.stderr(), containsString("ERROR: No such job 'aProject'")); + } + + @Test public void deleteBuildsShouldFailWithoutRunDeletePermission() throws Exception { + j.createFreeStyleProject("aProject").scheduleBuild2(0).get(); + + final CLICommandInvoker.Result result = command + .authorizedTo(Jenkins.READ, Job.READ) + .invokeWithArgs("aProject", "1"); + assertThat(result, failedWith(6)); + assertThat(result, hasNoStandardOutput()); + assertThat(result.stderr(), containsString("ERROR: user is missing the Run/Delete permission")); + } + + @Test public void deleteBuildsShouldFailIfJobDoesNotExist() throws Exception { + final CLICommandInvoker.Result result = command + .authorizedTo(Jenkins.READ, Job.READ, Run.DELETE) + .invokeWithArgs("never_created", "1"); + assertThat(result, failedWith(3)); + assertThat(result, hasNoStandardOutput()); + assertThat(result.stderr(), containsString("ERROR: No such job 'never_created'")); + } + + @Test public void deleteBuildsShouldFailIfJobNameIsEmpty() throws Exception { + j.createFreeStyleProject("aProject").scheduleBuild2(0).get(); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(1)); + + final CLICommandInvoker.Result result = command + .authorizedTo(Jenkins.READ, Job.READ, Run.DELETE) + .invokeWithArgs("", "1"); + assertThat(result, failedWith(3)); + assertThat(result, hasNoStandardOutput()); + assertThat(result.stderr(), containsString("ERROR: No such job ''; perhaps you meant 'aProject'?")); + } + + @Test public void deleteBuildsShouldSuccess() throws Exception { + j.createFreeStyleProject("aProject").scheduleBuild2(0).get(); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(1)); + + final CLICommandInvoker.Result result = command + .authorizedTo(Jenkins.READ, Job.READ, Run.DELETE) + .invokeWithArgs("aProject", "1"); + assertThat(result, succeeded()); + assertThat(result.stdout(), containsString("Deleted 1 builds")); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0)); + } + + @Test public void deleteBuildsShouldSuccessIfBuildDoesNotExist() throws Exception { + j.createFreeStyleProject("aProject").scheduleBuild2(0).get(); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(1)); + + final CLICommandInvoker.Result result = command + .authorizedTo(Jenkins.READ, Job.READ, Run.DELETE) + .invokeWithArgs("aProject", "2"); + assertThat(result, succeeded()); + assertThat(result.stdout(), containsString("Deleted 0 builds")); + } + + @Test public void deleteBuildsShouldSuccessIfBuildNumberZeroSpecified() throws Exception { + j.createFreeStyleProject("aProject").scheduleBuild2(0).get(); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(1)); + + final CLICommandInvoker.Result result = command + .authorizedTo(Jenkins.READ, Job.READ, Run.DELETE) + .invokeWithArgs("aProject", "0"); + assertThat(result, succeeded()); + assertThat(result.stdout(), containsString("Deleted 0 builds")); + } + + @Test public void deleteBuildsShouldSuccessEvenTheBuildIsRunning() throws Exception { + FreeStyleProject project = j.createFreeStyleProject("aProject"); + ExecutorTest.startBlockingBuild(project); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(1)); + + final CLICommandInvoker.Result result = command + .authorizedTo(Jenkins.READ, Job.READ, Run.DELETE) + .invokeWithArgs("aProject", "1"); + assertThat(result, succeeded()); + assertThat(result.stdout(), containsString("Deleted 1 builds")); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0)); + assertThat(project.isBuilding(), equalTo(false)); + } + + @Test public void deleteBuildsShouldSuccessEvenTheBuildIsStuckInTheQueue() throws Exception { + FreeStyleProject project = j.createFreeStyleProject("aProject"); + project.getBuildersList().add(new Shell("echo 1")); + project.setAssignedLabel(new LabelAtom("never_created")); + assertThat("Job wasn't scheduled properly", project.scheduleBuild(0), equalTo(true)); + Thread.sleep(1000); + assertThat("Job wasn't scheduled properly - it isn't in the queue", project.isInQueue(), equalTo(true)); + assertThat("Job wasn't scheduled properly - it is running on non-exist node", project.isBuilding(), equalTo(false)); + + final CLICommandInvoker.Result result = command + .authorizedTo(Jenkins.READ, Job.READ, Run.DELETE) + .invokeWithArgs("aProject", "1"); + assertThat(result, succeeded()); + assertThat(result.stdout(), containsString("Deleted 0 builds")); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0)); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).isBuilding(), equalTo(false)); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).isInQueue(), equalTo(true)); + + Jenkins.getInstance().getQueue().cancel(project); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0)); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).isBuilding(), equalTo(false)); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).isInQueue(), equalTo(false)); + } + + @Test public void deleteBuildsManyShouldSuccess() throws Exception { + FreeStyleProject project = j.createFreeStyleProject("aProject"); + j.buildAndAssertSuccess(project); + j.buildAndAssertSuccess(project); + j.buildAndAssertSuccess(project); + j.buildAndAssertSuccess(project); + j.buildAndAssertSuccess(project); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(5)); + + CLICommandInvoker.Result result = command + .authorizedTo(Jenkins.READ, Job.READ, Run.DELETE) + .invokeWithArgs("aProject", "1,2"); + assertThat(result, succeeded()); + assertThat(result.stdout(), containsString("Deleted 2 builds")); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(3)); + + result = command + .authorizedTo(Jenkins.READ, Job.READ, Run.DELETE) + .invokeWithArgs("aProject", "3-5"); + assertThat(result, succeeded()); + assertThat(result.stdout(), containsString("Deleted 3 builds")); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0)); + } + + @Test public void deleteBuildsManyShouldSuccessEvenABuildIsSpecifiedTwice() throws Exception { + FreeStyleProject project = j.createFreeStyleProject("aProject"); + j.buildAndAssertSuccess(project); + j.buildAndAssertSuccess(project); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(2)); + + CLICommandInvoker.Result result = command + .authorizedTo(Jenkins.READ, Job.READ, Run.DELETE) + .invokeWithArgs("aProject", "1,1"); + assertThat(result, succeeded()); + assertThat(result.stdout(), containsString("Deleted 1 builds")); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(1)); + + result = command + .authorizedTo(Jenkins.READ, Job.READ, Run.DELETE) + .invokeWithArgs("aProject", "1-1,1-2,2-2"); + assertThat(result, succeeded()); + assertThat(result.stdout(), containsString("Deleted 1 builds")); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0)); + } + + @Test public void deleteBuildsManyShouldSuccessEvenLastBuildDoesNotExist() throws Exception { + FreeStyleProject project = j.createFreeStyleProject("aProject"); + j.buildAndAssertSuccess(project); + j.buildAndAssertSuccess(project); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(2)); + + CLICommandInvoker.Result result = command + .authorizedTo(Jenkins.READ, Job.READ, Run.DELETE) + .invokeWithArgs("aProject", "1,3"); + assertThat(result, succeeded()); + assertThat(result.stdout(), containsString("Deleted 1 builds")); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(1)); + + result = command + .authorizedTo(Jenkins.READ, Job.READ, Run.DELETE) + .invokeWithArgs("aProject", "2-3"); + assertThat(result, succeeded()); + assertThat(result.stdout(), containsString("Deleted 1 builds")); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0)); + } + + @Test public void deleteBuildsManyShouldSuccessEvenMiddleBuildDoesNotExist() throws Exception { + FreeStyleProject project = j.createFreeStyleProject("aProject"); + j.buildAndAssertSuccess(project); + j.buildAndAssertSuccess(project); + j.buildAndAssertSuccess(project); + j.buildAndAssertSuccess(project); + j.buildAndAssertSuccess(project); + j.buildAndAssertSuccess(project); + project.getBuildByNumber(2).delete(); + project.getBuildByNumber(5).delete(); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(4)); + + CLICommandInvoker.Result result = command + .authorizedTo(Jenkins.READ, Job.READ, Run.DELETE) + .invokeWithArgs("aProject", "1,2,3"); + assertThat(result, succeeded()); + assertThat(result.stdout(), containsString("Deleted 2 builds")); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(2)); + + result = command + .authorizedTo(Jenkins.READ, Job.READ, Run.DELETE) + .invokeWithArgs("aProject", "4-6"); + assertThat(result, succeeded()); + assertThat(result.stdout(), containsString("Deleted 2 builds")); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0)); + } + + @Test public void deleteBuildsManyShouldSuccessEvenFirstBuildDoesNotExist() throws Exception { + FreeStyleProject project = j.createFreeStyleProject("aProject"); + j.buildAndAssertSuccess(project); + j.buildAndAssertSuccess(project); + j.buildAndAssertSuccess(project); + project.getBuildByNumber(1).delete(); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(2)); + + CLICommandInvoker.Result result = command + .authorizedTo(Jenkins.READ, Job.READ, Run.DELETE) + .invokeWithArgs("aProject", "1,2"); + assertThat(result, succeeded()); + assertThat(result.stdout(), containsString("Deleted 1 builds")); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(1)); + + result = command + .authorizedTo(Jenkins.READ, Job.READ, Run.DELETE) + .invokeWithArgs("aProject", "2-3"); + assertThat(result, succeeded()); + assertThat(result.stdout(), containsString("Deleted 1 builds")); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0)); + } + + @Test public void deleteBuildsManyShouldSuccessEvenTheFirstAndLastBuildDoesNotExist() throws Exception { + FreeStyleProject project = j.createFreeStyleProject("aProject"); + j.buildAndAssertSuccess(project); + j.buildAndAssertSuccess(project); + j.buildAndAssertSuccess(project); + j.buildAndAssertSuccess(project); + j.buildAndAssertSuccess(project); + project.getBuildByNumber(1).delete(); + project.getBuildByNumber(3).delete(); + project.getBuildByNumber(5).delete(); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(2)); + + CLICommandInvoker.Result result = command + .authorizedTo(Jenkins.READ, Job.READ, Run.DELETE) + .invokeWithArgs("aProject", "1,2,3"); + assertThat(result, succeeded()); + assertThat(result.stdout(), containsString("Deleted 1 builds")); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(1)); + + result = command + .authorizedTo(Jenkins.READ, Job.READ, Run.DELETE) + .invokeWithArgs("aProject", "3-5"); + assertThat(result, succeeded()); + assertThat(result.stdout(), containsString("Deleted 1 builds")); + assertThat(((FreeStyleProject) j.jenkins.getItem("aProject")).getBuilds(), hasSize(0)); + } +} diff --git a/test/src/test/java/hudson/model/ExecutorTest.java b/test/src/test/java/hudson/model/ExecutorTest.java index 5d5b034fcc..86bdebbe6e 100644 --- a/test/src/test/java/hudson/model/ExecutorTest.java +++ b/test/src/test/java/hudson/model/ExecutorTest.java @@ -144,14 +144,21 @@ public class ExecutorTest { assertThat(log, containsString("Disconnected by Johnny : Taking offline to break your buil")); } - private Future startBlockingBuild(FreeStyleProject p) throws Exception { + /** + * Start a project with an infinite build step + * + * @param project {@link FreeStyleProject} to start + * @return A {@link Future} object represents the started build + * @throws Exception if somethink wrong happened + */ + public static Future startBlockingBuild(FreeStyleProject project) throws Exception { final OneShotEvent e = new OneShotEvent(); - p.getBuildersList().add(new BlockingBuilder(e)); + project.getBuildersList().add(new BlockingBuilder(e)); - Future r = p.scheduleBuild2(0); + Future r = project.scheduleBuild2(0); e.block(); // wait until we are safe to interrupt - assertTrue(p.getLastBuild().isBuilding()); + assertTrue(project.getLastBuild().isBuilding()); return r; } diff --git a/war/src/main/js/util/jenkins.js b/war/src/main/js/util/jenkins.js index e0681dc7a5..bd3b748424 100644 --- a/war/src/main/js/util/jenkins.js +++ b/war/src/main/js/util/jenkins.js @@ -200,12 +200,17 @@ exports.testConnectivity = function(siteId, handler) { handler(false, true, response.message); } + // Define statuses, which need additional check iteration via async job on the Jenkins master + // Statuses like "OK" or "SKIPPED" are considered as fine. var uncheckedStatuses = ['PRECHECK', 'CHECKING', 'UNCHECKED']; if(uncheckedStatuses.indexOf(response.data.updatesite) >= 0 || uncheckedStatuses.indexOf(response.data.internet) >= 0) { setTimeout(testConnectivity, 100); } else { - if(response.status !== 'ok' || response.data.updatesite !== 'OK' || response.data.internet !== 'OK') { + // Update site should be always reachable, but we do not require the internet connection + // if it's explicitly skipped by the update center + if(response.status !== 'ok' || response.data.updatesite !== 'OK' || + (response.data.internet !== 'OK' && response.data.internet !== 'SKIPPED')) { // no connectivity, but not fatal handler(false, false); }