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);
}