From c8e5bd75691da01f3d294a8a008218b645c70d27 Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Mon, 5 Aug 2024 15:20:09 +0100 Subject: [PATCH] Update master --- .github/PULL_REQUEST_TEMPLATE.md | 12 +- .../workflows/publish-release-artifact.yml | 10 +- ath.sh | 2 +- bom/pom.xml | 4 +- .../java/hudson/ClassicPluginStrategy.java | 2 +- core/src/main/java/hudson/PluginManager.java | 4 +- .../java/hudson/lifecycle/ExitLifecycle.java | 6 + .../main/java/hudson/lifecycle/Lifecycle.java | 10 + .../main/java/hudson/model/AbstractItem.java | 94 +-- .../java/hudson/model/AbstractProject.java | 1 + .../hudson/model/AdministrativeMonitor.java | 7 +- .../hudson/model/BuildAuthorizationToken.java | 5 - .../main/java/hudson/model/Descriptor.java | 5 +- core/src/main/java/hudson/model/Queue.java | 5 + .../main/java/hudson/model/UpdateCenter.java | 182 +++++- core/src/main/java/hudson/model/User.java | 2 +- .../main/java/hudson/model/UserProperty.java | 2 +- .../hudson/model/UserPropertyDescriptor.java | 4 +- .../hudson/model/listeners/ItemListener.java | 25 + .../hudson/model/queue/MappingWorksheet.java | 3 + .../hudson/model/queue/QueueTaskFilter.java | 1 + .../main/java/hudson/model/queue/SubTask.java | 2 + .../userproperty/UserPropertyCategory.java | 2 +- .../main/java/hudson/util/BootFailure.java | 2 + .../java/jenkins/agents/WebSocketAgents.java | 5 +- ...erRetentionCheckIntervalConfiguration.java | 4 +- .../jenkins/model/ParameterizedJobMixIn.java | 1 + .../RemoveYuiUserExperimentalFlag.java | 49 ++ .../jenkins/model/queue/ItemDeletion.java | 109 +++- .../java/jenkins/widgets/BuildTimeTrend.java | 8 + .../hudson/TcpSlaveAgentListener/index.jelly | 7 +- .../hudson/model/Job/buildTimeTrend.jelly | 47 +- .../model/Job/buildTimeTrend_resources.css | 11 +- .../model/Job/buildTimeTrend_resources.js | 55 +- .../end-of-life-data.json | 46 +- .../resources/lib/form/secretTextarea.jelly | 28 +- .../lib/form/secretTextarea/secret.css | 6 +- .../lib/form/secretTextarea/secret.js | 35 +- .../main/resources/lib/layout/layout.jelly | 47 +- pom.xml | 8 +- test/pom.xml | 10 +- .../ParametersDefinitionPropertyTest.java | 31 + ...ildKeepsRunningWhenFaultySubTasksTest.java | 1 + .../hudson/util/FormFieldValidatorTest.java | 125 +++- .../jenkins/widgets/BuildTimeTrendTest.java | 12 +- .../java/lib/form/SecretTextareaTest.java | 15 +- .../ValidatingDescribable/config.jelly | 20 + war/package.json | 22 +- war/src/main/scss/components/_table.scss | 27 +- war/src/main/scss/form/_layout.scss | 2 +- .../main/webapp/scripts/hudson-behavior.js | 30 +- war/yarn.lock | 599 +++++++++--------- 52 files changed, 1165 insertions(+), 587 deletions(-) create mode 100644 core/src/main/java/jenkins/model/experimentalflags/RemoveYuiUserExperimentalFlag.java create mode 100644 test/src/test/resources/hudson/util/FormFieldValidatorTest/ValidatingDescribable/config.jelly diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 456f8fca6da7..a2ed88450df5 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -28,16 +28,17 @@ For refactoring and code cleanup changes, exercise the code before and after the ### Proposed changelog entries -- JENKINS-XXXXX, human-readable text +- human-readable text @@ -45,6 +46,11 @@ You may add multiple changelog entries if applicable by adding a new entry to th N/A + + ```[tasklist] ### Submitter checklist - [ ] The Jira issue, if it exists, is well-described. diff --git a/.github/workflows/publish-release-artifact.yml b/.github/workflows/publish-release-artifact.yml index 04e3d0aa993e..ae5e6bf92602 100644 --- a/.github/workflows/publish-release-artifact.yml +++ b/.github/workflows/publish-release-artifact.yml @@ -73,7 +73,7 @@ jobs: wget -q https://get.jenkins.io/${REPO}/${PROJECT_VERSION}/${FILE_NAME} - name: Upload Release Asset id: upload-war - uses: softprops/action-gh-release@a74c6b72af54cfa997e81df42d94703d6313a2d0 + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -108,7 +108,7 @@ jobs: - name: Upload Release Asset id: upload-deb if: always() - uses: softprops/action-gh-release@a74c6b72af54cfa997e81df42d94703d6313a2d0 + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -144,7 +144,7 @@ jobs: - name: Upload Release Asset id: upload-rpm if: always() - uses: softprops/action-gh-release@a74c6b72af54cfa997e81df42d94703d6313a2d0 + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -180,7 +180,7 @@ jobs: - name: Upload Release Asset id: upload-msi if: always() - uses: softprops/action-gh-release@a74c6b72af54cfa997e81df42d94703d6313a2d0 + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -216,7 +216,7 @@ jobs: - name: Upload Release Asset id: upload-suse-rpm if: always() - uses: softprops/action-gh-release@a74c6b72af54cfa997e81df42d94703d6313a2d0 + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/ath.sh b/ath.sh index 36af3cea0e85..3cd7dfd86d75 100644 --- a/ath.sh +++ b/ath.sh @@ -6,7 +6,7 @@ set -o xtrace cd "$(dirname "$0")" # https://github.com/jenkinsci/acceptance-test-harness/releases -export ATH_VERSION=5883.vdea_99c1762a_d +export ATH_VERSION=5911.v5f88b_6d0c450 if [[ $# -eq 0 ]]; then export JDK=17 diff --git a/bom/pom.xml b/bom/pom.xml index 150deffe0389..8f8e6412f7b0 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -40,7 +40,7 @@ THE SOFTWARE. 2.0.0-M2 2.0.13 - 1881.vd39f3ee5c629 + 1892.v73465f3d074d 2.4.21 @@ -113,7 +113,7 @@ THE SOFTWARE. commons-codec commons-codec - 1.17.0 + 1.17.1 commons-collections diff --git a/core/src/main/java/hudson/ClassicPluginStrategy.java b/core/src/main/java/hudson/ClassicPluginStrategy.java index 8c39f14ef2d5..3d6edf832f9f 100644 --- a/core/src/main/java/hudson/ClassicPluginStrategy.java +++ b/core/src/main/java/hudson/ClassicPluginStrategy.java @@ -252,7 +252,7 @@ private void fix(Attributes atts, List optionalDepende for (Dependency d : DetachedPluginsUtil.getImpliedDependencies(pluginName, jenkinsVersion)) { LOGGER.fine(() -> "implied dep " + pluginName + " → " + d.shortName); - pluginManager.considerDetachedPlugin(d.shortName); + pluginManager.considerDetachedPlugin(d.shortName, pluginName); optionalDependencies.add(d); } } diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index eda266735173..75dd52c4d7b7 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -615,7 +615,7 @@ public void run(Reactor reactor) throws Exception { }}); } - void considerDetachedPlugin(String shortName) { + void considerDetachedPlugin(String shortName, String source) { if (new File(rootDir, shortName + ".jpi").isFile() || new File(rootDir, shortName + ".hpi").isFile() || new File(rootDir, shortName + ".jpl").isFile() || @@ -627,7 +627,7 @@ void considerDetachedPlugin(String shortName) { for (String loadedFile : loadPluginsFromWar(getDetachedLocation(), (dir, name) -> normalisePluginName(name).equals(shortName))) { String loaded = normalisePluginName(loadedFile); File arc = new File(rootDir, loaded + ".jpi"); - LOGGER.info(() -> "Loading a detached plugin as a dependency: " + arc); + LOGGER.info(() -> "Loading a detached plugin " + arc + " as a dependency of " + source); try { plugins.add(strategy.createPluginWrapper(arc)); } catch (IOException e) { diff --git a/core/src/main/java/hudson/lifecycle/ExitLifecycle.java b/core/src/main/java/hudson/lifecycle/ExitLifecycle.java index f8fcc3abefbf..038dafc442a3 100644 --- a/core/src/main/java/hudson/lifecycle/ExitLifecycle.java +++ b/core/src/main/java/hudson/lifecycle/ExitLifecycle.java @@ -26,6 +26,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; +import hudson.util.BootFailure; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.model.Jenkins; @@ -72,4 +73,9 @@ public void restart() { System.exit(exitOnRestart); } + + @Override + public void onBootFailure(BootFailure problem) { + restart(); + } } diff --git a/core/src/main/java/hudson/lifecycle/Lifecycle.java b/core/src/main/java/hudson/lifecycle/Lifecycle.java index dbc53d2b5005..fcd7769aeff2 100644 --- a/core/src/main/java/hudson/lifecycle/Lifecycle.java +++ b/core/src/main/java/hudson/lifecycle/Lifecycle.java @@ -32,6 +32,8 @@ import hudson.Util; import hudson.init.InitMilestone; import hudson.init.Initializer; +import hudson.util.BootFailure; +import hudson.util.JenkinsReloadFailed; import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; @@ -324,6 +326,14 @@ public boolean supportsDynamicLoad() { return true; } + /** + * Called when Jenkins has failed to boot. + * @param problem a boot failure (could be {@link JenkinsReloadFailed}) + * @since TODO + */ + public void onBootFailure(BootFailure problem) { + } + @Restricted(NoExternalUse.class) public static final class PlaceholderLifecycle extends ExitLifecycle { diff --git a/core/src/main/java/hudson/model/AbstractItem.java b/core/src/main/java/hudson/model/AbstractItem.java index 614b1c13eb4d..adebec8f289f 100644 --- a/core/src/main/java/hudson/model/AbstractItem.java +++ b/core/src/main/java/hudson/model/AbstractItem.java @@ -25,7 +25,6 @@ package hudson.model; -import static hudson.model.queue.Executables.getParentOf; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import com.infradna.tool.bridge_method_injector.WithBridgeMethods; @@ -39,9 +38,6 @@ import hudson.cli.declarative.CLIResolver; import hudson.model.listeners.ItemListener; import hudson.model.listeners.SaveableListener; -import hudson.model.queue.SubTask; -import hudson.model.queue.Tasks; -import hudson.model.queue.WorkUnit; import hudson.security.ACL; import hudson.security.ACLContext; import hudson.security.AccessControlled; @@ -57,12 +53,8 @@ import java.nio.charset.Charset; import java.nio.file.Files; import java.util.Collection; -import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; -import java.util.Map; -import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; @@ -705,11 +697,13 @@ public void delete(StaplerRequest req, StaplerResponse rsp) throws IOException, * *

* Any exception indicates the deletion has failed, but {@link AbortException} would prevent the caller - * from showing the stack trace. This + * from showing the stack trace. + * @see ItemDeletion */ @Override public void delete() throws IOException, InterruptedException { checkPermission(DELETE); + ItemListener.checkBeforeDelete(this); boolean responsibleForAbortingBuilds = !ItemDeletion.contains(this); boolean ownsRegistration = ItemDeletion.register(this); if (!ownsRegistration && ItemDeletion.isRegistered(this)) { @@ -719,87 +713,7 @@ public void delete() throws IOException, InterruptedException { try { // if a build is in progress. Cancel it. if (responsibleForAbortingBuilds || ownsRegistration) { - Queue queue = Queue.getInstance(); - if (this instanceof Queue.Task) { - // clear any items in the queue so they do not get picked up - queue.cancel((Queue.Task) this); - } - // now cancel any child items - this happens after ItemDeletion registration, so we can use a snapshot - for (Queue.Item i : queue.getItems()) { - Item item = Tasks.getItemOf(i.task); - while (item != null) { - if (item == this) { - if (!queue.cancel(i)) { - LOGGER.warning(() -> "failed to cancel " + i); - } - break; - } - if (item.getParent() instanceof Item) { - item = (Item) item.getParent(); - } else { - break; - } - } - } - // interrupt any builds in progress (and this should be a recursive test so that folders do not pay - // the 15 second delay for every child item). This happens after queue cancellation, so will be - // a complete set of builds in flight - Map buildsInProgress = new LinkedHashMap<>(); - for (Computer c : Jenkins.get().getComputers()) { - for (Executor e : c.getAllExecutors()) { - final WorkUnit workUnit = e.getCurrentWorkUnit(); - final Queue.Executable executable = workUnit != null ? workUnit.getExecutable() : null; - final SubTask subtask = executable != null ? getParentOf(executable) : null; - - if (subtask != null) { - Item item = Tasks.getItemOf(subtask); - while (item != null) { - if (item == this) { - buildsInProgress.put(e, e.getCurrentExecutable()); - e.interrupt(Result.ABORTED); - break; - } - if (item.getParent() instanceof Item) { - item = (Item) item.getParent(); - } else { - break; - } - } - } - } - } - if (!buildsInProgress.isEmpty()) { - // give them 15 seconds or so to respond to the interrupt - long expiration = System.nanoTime() + TimeUnit.SECONDS.toNanos(15); - // comparison with executor.getCurrentExecutable() == computation currently should always be true - // as we no longer recycle Executors, but safer to future-proof in case we ever revisit recycling - while (!buildsInProgress.isEmpty() && expiration - System.nanoTime() > 0L) { - // we know that ItemDeletion will prevent any new builds in the queue - // ItemDeletion happens-before Queue.cancel so we know that the Queue will stay clear - // Queue.cancel happens-before collecting the buildsInProgress list - // thus buildsInProgress contains the complete set we need to interrupt and wait for - for (Iterator> iterator = - buildsInProgress.entrySet().iterator(); - iterator.hasNext(); ) { - Map.Entry entry = iterator.next(); - // comparison with executor.getCurrentExecutable() == executable currently should always be - // true as we no longer recycle Executors, but safer to future-proof in case we ever - // revisit recycling. - if (!entry.getKey().isAlive() - || entry.getValue() != entry.getKey().getCurrentExecutable()) { - iterator.remove(); - } - // I don't know why, but we have to keep interrupting - entry.getKey().interrupt(Result.ABORTED); - } - Thread.sleep(50L); - } - if (!buildsInProgress.isEmpty()) { - throw new Failure(Messages.AbstractItem_FailureToStopBuilds( - buildsInProgress.size(), getFullDisplayName() - )); - } - } + ItemDeletion.cancelBuildsInProgress(this); } if (this instanceof ItemGroup) { // delete individual items first diff --git a/core/src/main/java/hudson/model/AbstractProject.java b/core/src/main/java/hudson/model/AbstractProject.java index c323f50bca0b..c568f8df8b31 100644 --- a/core/src/main/java/hudson/model/AbstractProject.java +++ b/core/src/main/java/hudson/model/AbstractProject.java @@ -1011,6 +1011,7 @@ public List getActions() { * null if no information is available (for example, * if no build was done yet.) */ + @SuppressWarnings("deprecation") @Override public Node getLastBuiltOn() { // where was it built on? diff --git a/core/src/main/java/hudson/model/AdministrativeMonitor.java b/core/src/main/java/hudson/model/AdministrativeMonitor.java index e2f69d654ca8..bdbfb48027d3 100644 --- a/core/src/main/java/hudson/model/AdministrativeMonitor.java +++ b/core/src/main/java/hudson/model/AdministrativeMonitor.java @@ -200,7 +200,10 @@ public void doDisable(StaplerRequest req, StaplerResponse rsp) throws IOExceptio * Form UI elements that change system state, e.g. toggling a feature on or off, need to be hidden from users * lacking Administer permission. *

+ * @since 2.233 + * @deprecated Callers should use {@link #checkRequiredPermission()} or {@link #hasRequiredPermission()}. */ + @Deprecated public Permission getRequiredPermission() { return Jenkins.ADMINISTER; } @@ -213,6 +216,7 @@ public Permission getRequiredPermission() { *

* @see #getRequiredPermission() * @see #hasRequiredPermission() + * @since 2.468 */ public void checkRequiredPermission() { Jenkins.get().checkPermission(getRequiredPermission()); @@ -226,6 +230,7 @@ public void checkRequiredPermission() { *

* @see #getRequiredPermission() * @see #checkRequiredPermission() + * @since 2.468 */ public boolean hasRequiredPermission() { return Jenkins.get().hasPermission(getRequiredPermission()); @@ -236,7 +241,7 @@ public boolean hasRequiredPermission() { * * @return true if the current user has the minimum required permission to view any administrative monitor. * - * @since TODO + * @since 2.468 */ public static boolean hasPermissionToDisplay() { return Jenkins.get().hasAnyPermission(Jenkins.SYSTEM_READ, Jenkins.MANAGE); diff --git a/core/src/main/java/hudson/model/BuildAuthorizationToken.java b/core/src/main/java/hudson/model/BuildAuthorizationToken.java index f101eb3d6e74..a09ed113e1cf 100644 --- a/core/src/main/java/hudson/model/BuildAuthorizationToken.java +++ b/core/src/main/java/hudson/model/BuildAuthorizationToken.java @@ -29,7 +29,6 @@ import hudson.security.ACL; import java.io.IOException; import javax.servlet.http.HttpServletResponse; -import jenkins.security.ApiTokenProperty; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; @@ -82,10 +81,6 @@ public static void checkPermission(Job project, BuildAuthorizationToken to return; } - if (req.getAttribute(ApiTokenProperty.class.getName()) instanceof User) { - return; - } - rsp.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED); rsp.addHeader("Allow", "POST"); throw HttpResponses.forwardToView(project, "requirePOST.jelly"); diff --git a/core/src/main/java/hudson/model/Descriptor.java b/core/src/main/java/hudson/model/Descriptor.java index 07789531e94d..b5eb07784d27 100644 --- a/core/src/main/java/hudson/model/Descriptor.java +++ b/core/src/main/java/hudson/model/Descriptor.java @@ -594,6 +594,9 @@ public T newInstance(@Nullable StaplerRequest req, @NonNull JSONObject formData) return verifyNewInstance(bindJSON(req, clazz, formData, true)); } } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException | RuntimeException e) { + if (e instanceof RuntimeException && e instanceof HttpResponse) { + throw (RuntimeException) e; + } throw new LinkageError("Failed to instantiate " + clazz + " from " + RedactSecretJsonInErrorMessageSanitizer.INSTANCE.sanitize(formData), e); } } @@ -674,7 +677,7 @@ public Object instantiate(Class actualType, JSONObject json) { + actualType.getName() + " " + json); } } catch (Exception x) { - LOGGER.log(Level.WARNING, "falling back to default instantiation " + actualType.getName() + " " + json, x); + LOGGER.log(x instanceof HttpResponse ? Level.FINE : Level.WARNING, "falling back to default instantiation " + actualType.getName() + " " + json, x); // If nested objects are not using newInstance, bindJSON will wind up throwing the same exception anyway, // so logging above will result in a duplicated stack trace. // However if they *are* then this is the only way to find errors in that newInstance. diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java index d489f042100d..0d299fb9426d 100644 --- a/core/src/main/java/hudson/model/Queue.java +++ b/core/src/main/java/hudson/model/Queue.java @@ -130,6 +130,7 @@ import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.stapler.interceptor.RequirePOST; @@ -2414,6 +2415,10 @@ public Api getApi() throws AccessDeniedException { } } + public HttpResponse doIndex(StaplerRequest req) { + return HttpResponses.text("Queue item exists. For details check, for example, " + req.getRequestURI() + "api/json?tree=cancelled,executable[url]"); + } + protected Object readResolve() { this.future = new FutureImpl(task); return this; diff --git a/core/src/main/java/hudson/model/UpdateCenter.java b/core/src/main/java/hudson/model/UpdateCenter.java index 03ab31314372..4df95b7b3f45 100644 --- a/core/src/main/java/hudson/model/UpdateCenter.java +++ b/core/src/main/java/hudson/model/UpdateCenter.java @@ -58,14 +58,18 @@ import hudson.util.PersistedList; import hudson.util.VersionNumber; import hudson.util.XStream2; +import java.io.BufferedInputStream; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.UncheckedIOException; import java.lang.reflect.Constructor; import java.net.HttpRetryException; import java.net.HttpURLConnection; import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.net.UnknownHostException; @@ -86,6 +90,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TreeSet; import java.util.UUID; @@ -1322,6 +1327,10 @@ public File download(DownloadJob job, URL src) throws IOException { sha512 != null ? new DigestOutputStream(_out, sha512) : _out, sha256) : _out, sha1) : _out; InputStream in = con.getInputStream(); CountingInputStream cin = new CountingInputStream(in)) { + if (LOGGER.isLoggable(Level.FINE)) { + var sourceUrlString = getSourceUrl(src, con); + LOGGER.fine(() -> "Downloading " + job.getName() + " from " + sourceUrlString); + } while ((len = cin.read(buf)) >= 0) { out.write(buf, 0, len); final int count = cin.getCount(); @@ -1358,15 +1367,22 @@ public File download(DownloadJob job, URL src) throws IOException { return tmp; } catch (IOException e) { // assist troubleshooting in case of e.g. "too many redirects" by printing actual URL - String extraMessage = ""; - if (con != null && con.getURL() != null && !src.toString().equals(con.getURL().toString())) { - // Two URLs are considered equal if different hosts resolve to same IP. Prefer to log in case of string inequality, - // because who knows how the server responds to different host name in the request header? - // Also, since it involved name resolution, it'd be an expensive operation. - extraMessage = " (redirected to: " + con.getURL() + ")"; + throw new IOException("Failed to download from " + getSourceUrl(src, con), e); + } + } + + private static String getSourceUrl(@NonNull URL src, @CheckForNull URLConnection connection) { + var sourceUrlString = src.toExternalForm(); + if (connection != null) { + var connectionURL = connection.getURL(); + if (connectionURL != null) { + var finalUrlString = connectionURL.toExternalForm(); + if (!sourceUrlString.equals(finalUrlString)) { + return sourceUrlString + " → " + finalUrlString; + } } - throw new IOException("Failed to download from " + src + extraMessage, e); } + return sourceUrlString; } /** @@ -1798,6 +1814,83 @@ public void run() { String getComputedSHA512(); } + @SuppressFBWarnings(value = "WEAK_MESSAGE_DIGEST_SHA1", justification = "SHA-1 is only used as a fallback if SHA-256/SHA-512 are not available") + private static class FileWithComputedChecksums implements WithComputedChecksums { + + private final File file; + + private String computedSHA1; + private String computedSHA256; + private String computedSHA512; + + FileWithComputedChecksums(File file) { + this.file = Objects.requireNonNull(file); + } + + @Override + public synchronized String getComputedSHA1() { + if (computedSHA1 != null) { + return computedSHA1; + } + + MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(e); + } + computedSHA1 = computeDigest(messageDigest); + return computedSHA1; + } + + @Override + public synchronized String getComputedSHA256() { + if (computedSHA256 != null) { + return computedSHA256; + } + + MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(e); + } + computedSHA256 = computeDigest(messageDigest); + return computedSHA256; + } + + @Override + public synchronized String getComputedSHA512() { + if (computedSHA512 != null) { + return computedSHA512; + } + + MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance("SHA-512"); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(e); + } + computedSHA512 = computeDigest(messageDigest); + return computedSHA512; + } + + private String computeDigest(MessageDigest digest) { + try (InputStream is = new FileInputStream(file); + BufferedInputStream bis = new BufferedInputStream(is)) { + byte[] buffer = new byte[1024]; + int read = bis.read(buffer, 0, buffer.length); + while (read > -1) { + digest.update(buffer, 0, read); + read = bis.read(buffer, 0, buffer.length); + } + return Base64.getEncoder().encodeToString(digest.digest()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + /** * Base class for a job that downloads a file from the Jenkins project. */ @@ -2234,7 +2327,24 @@ public void _run() throws IOException, InstallationStatus { return; } try { - super._run(); + File cached = getCached(this); + if (cached != null) { + File dst = getDestination(); + + // A bit naive, but following the corresponding logic in UpdateCenterConfiguration#download... + File tmp = new File(dst.getPath() + ".tmp"); + Files.copy(cached.toPath(), tmp.toPath(), StandardCopyOption.REPLACE_EXISTING); + + config.postValidate(this, tmp); + + /* + * Will unfortunately validate the checksum a second time, but this should still be faster than + * network I/O and at least allows us to reuse code... + */ + config.install(this, tmp, dst); + } else { + super._run(); + } // if this is a bundled plugin, make sure it won't get overwritten PluginWrapper pw = plugin.getInstalled(); @@ -2267,6 +2377,62 @@ public void _run() throws IOException, InstallationStatus { } } + /** + * If we happen to have the file already in the {@coode WEB-INF/detached-plugins} directory and it happens to + * match the checksum we were expecting, then save ourselves a trip to the download site. This method is + * best-effort, and if anything goes wrong we simply fall back to the standard download path. + * + * @return The cached file, or null for a cache miss + */ + @CheckForNull + private File getCached(DownloadJob job) { + URL src; + try { + /* + * Could make PluginManager#getDetachedLocation public and consume it here, but this method is + * best-effort anyway. + */ + src = Jenkins.get().servletContext.getResource(String.format("/WEB-INF/detached-plugins/%s.hpi", plugin.name)); + } catch (MalformedURLException e) { + return null; + } + + if (src == null || !"file".equals(src.getProtocol())) { + return null; + } + + try { + config.preValidate(this, src); + } catch (IOException e) { + return null; + } + + File cached; + try { + cached = new File(src.toURI()); + } catch (URISyntaxException e) { + return null; + } + + if (!cached.isFile()) { + return null; + } + + WithComputedChecksums withComputedChecksums = new FileWithComputedChecksums(cached); + try { + verifyChecksums(withComputedChecksums, plugin, cached); + } catch (IOException | UncheckedIOException | UnsupportedOperationException e) { + return null; + } + + // Allow us to reuse UpdateCenter.InstallationJob#replace. + job.computedSHA1 = withComputedChecksums.getComputedSHA1(); + job.computedSHA256 = withComputedChecksums.getComputedSHA256(); + job.computedSHA512 = withComputedChecksums.getComputedSHA512(); + + return cached; + } + /** * Indicates there is another installation job for this plugin * @since 2.1 diff --git a/core/src/main/java/hudson/model/User.java b/core/src/main/java/hudson/model/User.java index e588a79b347a..792622eb3c54 100644 --- a/core/src/main/java/hudson/model/User.java +++ b/core/src/main/java/hudson/model/User.java @@ -344,7 +344,7 @@ public synchronized void addProperty(@NonNull UserProperty p) throws IOException * The properties not included in the list will be let untouched. * It will call the {@link UserProperty#setUser(User)} method and at the end, {@link #save()} once. * - * @since TODO + * @since 2.468 */ public synchronized void addProperties(@NonNull List multipleProperties) throws IOException { List newProperties = new ArrayList<>(this.properties); diff --git a/core/src/main/java/hudson/model/UserProperty.java b/core/src/main/java/hudson/model/UserProperty.java index 6538a5fdf661..a6ebeb738b23 100644 --- a/core/src/main/java/hudson/model/UserProperty.java +++ b/core/src/main/java/hudson/model/UserProperty.java @@ -86,7 +86,7 @@ public static DescriptorExtensionList all( /** * Returns all the registered {@link UserPropertyCategory} descriptors for a given category. * - * @since TODO + * @since 2.468 */ public static List allByCategoryClass(@NonNull Class categoryClass) { DescriptorExtensionList all = all(); diff --git a/core/src/main/java/hudson/model/UserPropertyDescriptor.java b/core/src/main/java/hudson/model/UserPropertyDescriptor.java index ff3171c1fbf7..66762bf3c716 100644 --- a/core/src/main/java/hudson/model/UserPropertyDescriptor.java +++ b/core/src/main/java/hudson/model/UserPropertyDescriptor.java @@ -85,7 +85,7 @@ public boolean isEnabled() { * * @return never null, always the same value for a given instance of {@link Descriptor}. * - * @since TODO + * @since 2.468 */ public @NonNull UserPropertyCategory getUserPropertyCategory() { // As this method is expected to be overloaded by subclasses @@ -120,7 +120,7 @@ public boolean isEnabled() { * * @return String name corresponding to the symbol of {@link #getUserPropertyCategory()} * - * @since TODO + * @since 2.468 */ @Deprecated protected @CheckForNull String getUserPropertyCategoryAsString() { diff --git a/core/src/main/java/hudson/model/listeners/ItemListener.java b/core/src/main/java/hudson/model/listeners/ItemListener.java index aef431c3207b..fe09a9c56373 100644 --- a/core/src/main/java/hudson/model/listeners/ItemListener.java +++ b/core/src/main/java/hudson/model/listeners/ItemListener.java @@ -35,6 +35,8 @@ import java.util.logging.Level; import java.util.logging.Logger; import jenkins.util.Listeners; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; /** * Receives notifications about CRUD operations of {@link Item}. @@ -94,6 +96,16 @@ public void onCopied(Item src, Item item) { public void onLoaded() { } + /** + * Called before an item is deleted, providing the ability to veto the deletion operation before it starts. + * @param item the item being deleted + * @throws Failure to veto the operation. + * @throws InterruptedException If a blocking condition was interrupted, also vetoing the operation. + * @since TODO + */ + public void onCheckDelete(Item item) throws Failure, InterruptedException { + } + /** * Called right before a job is going to be deleted. * @@ -205,6 +217,19 @@ public static void fireOnUpdated(final Item item) { Listeners.notify(ItemListener.class, false, l -> l.onUpdated(item)); } + @Restricted(NoExternalUse.class) + public static void checkBeforeDelete(Item item) throws Failure, InterruptedException { + for (ItemListener l : all()) { + try { + l.onCheckDelete(item); + } catch (Failure e) { + throw e; + } catch (RuntimeException x) { + LOGGER.log(Level.WARNING, "failed to send event to listener of " + l.getClass(), x); + } + } + } + /** @since 1.548 */ public static void fireOnDeleted(final Item item) { Listeners.notify(ItemListener.class, false, l -> l.onDeleted(item)); diff --git a/core/src/main/java/hudson/model/queue/MappingWorksheet.java b/core/src/main/java/hudson/model/queue/MappingWorksheet.java index 9dbeaa3816f4..8150dd1bc132 100644 --- a/core/src/main/java/hudson/model/queue/MappingWorksheet.java +++ b/core/src/main/java/hudson/model/queue/MappingWorksheet.java @@ -190,7 +190,9 @@ public class WorkChunk extends ReadOnlyList { * If the previous execution of this task run on a certain node * and this task prefers to run on the same node, return that. * Otherwise null. + * @deprecated Unused. */ + @Deprecated public final ExecutorChunk lastBuiltOn; @@ -200,6 +202,7 @@ private WorkChunk(List base, int index) { this.index = index; this.assignedLabel = getAssignedLabel(base.get(0)); + @SuppressWarnings("deprecation") Node lbo = base.get(0).getLastBuiltOn(); for (ExecutorChunk ec : executors) { if (ec.node == lbo) { diff --git a/core/src/main/java/hudson/model/queue/QueueTaskFilter.java b/core/src/main/java/hudson/model/queue/QueueTaskFilter.java index e2a113dfaa59..a36c5ca7c753 100644 --- a/core/src/main/java/hudson/model/queue/QueueTaskFilter.java +++ b/core/src/main/java/hudson/model/queue/QueueTaskFilter.java @@ -52,6 +52,7 @@ public Label getAssignedLabel() { return base.getAssignedLabel(); } + @Deprecated @Override public Node getLastBuiltOn() { return base.getLastBuiltOn(); diff --git a/core/src/main/java/hudson/model/queue/SubTask.java b/core/src/main/java/hudson/model/queue/SubTask.java index f8b7dd435088..0690d074617c 100644 --- a/core/src/main/java/hudson/model/queue/SubTask.java +++ b/core/src/main/java/hudson/model/queue/SubTask.java @@ -62,7 +62,9 @@ default Label getAssignedLabel() { * and this task prefers to run on the same node, return that. * Otherwise null. * @return by default, null + * @deprecated Unused. */ + @Deprecated default Node getLastBuiltOn() { return null; } diff --git a/core/src/main/java/hudson/model/userproperty/UserPropertyCategory.java b/core/src/main/java/hudson/model/userproperty/UserPropertyCategory.java index 803b7e2d3527..5d5467b6eed4 100644 --- a/core/src/main/java/hudson/model/userproperty/UserPropertyCategory.java +++ b/core/src/main/java/hudson/model/userproperty/UserPropertyCategory.java @@ -43,7 +43,7 @@ * as the catch-all "unclassified".) Categories themselves are extensible — plugins may introduce * its own category as well, although that should only happen if you are creating a big enough subsystem. * - * @since TODO + * @since 2.468 * @see UserProperty */ public abstract class UserPropertyCategory implements ExtensionPoint, ModelObject { diff --git a/core/src/main/java/hudson/util/BootFailure.java b/core/src/main/java/hudson/util/BootFailure.java index a460ffea148e..f23cf7fa66b7 100644 --- a/core/src/main/java/hudson/util/BootFailure.java +++ b/core/src/main/java/hudson/util/BootFailure.java @@ -15,6 +15,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.ServletContext; +import jenkins.model.Jenkins; import jenkins.util.groovy.GroovyHookScript; import org.kohsuke.stapler.WebApp; @@ -51,6 +52,7 @@ public void publish(ServletContext context, @CheckForNull File home) { .bind("servletContext", context) .bind("attempts", loadAttempts(home)) .run(); + Jenkins.get().getLifecycle().onBootFailure(this); } /** diff --git a/core/src/main/java/jenkins/agents/WebSocketAgents.java b/core/src/main/java/jenkins/agents/WebSocketAgents.java index 005587c6365a..d9560f156ebd 100644 --- a/core/src/main/java/jenkins/agents/WebSocketAgents.java +++ b/core/src/main/java/jenkins/agents/WebSocketAgents.java @@ -36,6 +36,7 @@ import hudson.remoting.ChannelBuilder; import hudson.remoting.ChunkHeader; import hudson.remoting.Engine; +import hudson.slaves.SlaveComputer; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; @@ -107,7 +108,9 @@ public HttpResponse doIndex(StaplerRequest req, StaplerResponse rsp) throws IOEx Capability remoteCapability = Capability.fromASCII(remoteCapabilityStr); LOGGER.fine(() -> "received " + remoteCapability); rsp.setHeader(Capability.KEY, new Capability().toASCII()); - rsp.setHeader(Engine.REMOTING_MINIMUM_VERSION_HEADER, RemotingVersionInfo.getMinimumSupportedVersion().toString()); + if (!SlaveComputer.ALLOW_UNSUPPORTED_REMOTING_VERSIONS) { + rsp.setHeader(Engine.REMOTING_MINIMUM_VERSION_HEADER, RemotingVersionInfo.getMinimumSupportedVersion().toString()); + } rsp.setHeader(Engine.WEBSOCKET_COOKIE_HEADER, cookie); return WebSockets.upgrade(new Session(state, agent, remoteCapability)); } diff --git a/core/src/main/java/jenkins/model/GlobalComputerRetentionCheckIntervalConfiguration.java b/core/src/main/java/jenkins/model/GlobalComputerRetentionCheckIntervalConfiguration.java index c73b862ef1e5..46d760fe8ee2 100644 --- a/core/src/main/java/jenkins/model/GlobalComputerRetentionCheckIntervalConfiguration.java +++ b/core/src/main/java/jenkins/model/GlobalComputerRetentionCheckIntervalConfiguration.java @@ -23,7 +23,7 @@ public class GlobalComputerRetentionCheckIntervalConfiguration extends GlobalCon /** * Gets the check interval for computer retention. * - * @since TODO + * @since 2.463 */ public int getComputerRetentionCheckInterval() { if (computerRetentionCheckInterval <= 0) { @@ -42,7 +42,7 @@ public int getComputerRetentionCheckInterval() { * * @param interval new check interval in seconds * @throws IllegalArgumentException interval must be greater than zero - * @since TODO + * @since 2.463 */ private void setComputerRetentionCheckInterval(int interval) throws IllegalArgumentException { if (interval <= 0) { diff --git a/core/src/main/java/jenkins/model/ParameterizedJobMixIn.java b/core/src/main/java/jenkins/model/ParameterizedJobMixIn.java index 734534220901..662ffa9359e2 100644 --- a/core/src/main/java/jenkins/model/ParameterizedJobMixIn.java +++ b/core/src/main/java/jenkins/model/ParameterizedJobMixIn.java @@ -218,6 +218,7 @@ public final void doBuild(StaplerRequest req, StaplerResponse rsp, @QueryParamet Queue.Item item = Jenkins.get().getQueue().schedule2(asJob(), delay.getTimeInSeconds(), getBuildCause(asJob(), req)).getItem(); if (item != null) { + // TODO JENKINS-66105 use SC_SEE_OTHER if !ScheduleResult.created rsp.sendRedirect(SC_CREATED, req.getContextPath() + '/' + item.getUrl()); } else { rsp.sendRedirect("."); diff --git a/core/src/main/java/jenkins/model/experimentalflags/RemoveYuiUserExperimentalFlag.java b/core/src/main/java/jenkins/model/experimentalflags/RemoveYuiUserExperimentalFlag.java new file mode 100644 index 000000000000..e8f8dcc31775 --- /dev/null +++ b/core/src/main/java/jenkins/model/experimentalflags/RemoveYuiUserExperimentalFlag.java @@ -0,0 +1,49 @@ +/* + * The MIT License + * + * Copyright (c) 2024, Markus Winter + * + * 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 jenkins.model.experimentalflags; + +import edu.umd.cs.findbugs.annotations.Nullable; +import hudson.Extension; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Extension +@Restricted(NoExternalUse.class) +public class RemoveYuiUserExperimentalFlag extends BooleanUserExperimentalFlag { + public RemoveYuiUserExperimentalFlag() { + super("remove-yui.flag"); + } + + @Override + public String getDisplayName() { + return "Remove YUI"; + } + + @Nullable + @Override + public String getShortDescription() { + return "Remove YUI from all Jenkins UI pages. This will break anything that depends on YUI"; + } +} diff --git a/core/src/main/java/jenkins/model/queue/ItemDeletion.java b/core/src/main/java/jenkins/model/queue/ItemDeletion.java index e5c9fc2ca87e..fd18edde04a5 100644 --- a/core/src/main/java/jenkins/model/queue/ItemDeletion.java +++ b/core/src/main/java/jenkins/model/queue/ItemDeletion.java @@ -28,25 +28,42 @@ import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.ExtensionList; +import hudson.model.AbstractItem; import hudson.model.Action; +import hudson.model.Computer; +import hudson.model.Executor; +import hudson.model.Failure; import hudson.model.Item; +import hudson.model.Messages; import hudson.model.Queue; +import hudson.model.Result; +import hudson.model.queue.Executables; +import hudson.model.queue.SubTask; import hudson.model.queue.Tasks; +import hudson.model.queue.WorkUnit; import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Logger; +import jenkins.model.Jenkins; import net.jcip.annotations.GuardedBy; /** * A {@link Queue.QueueDecisionHandler} that blocks items being deleted from entering the queue. - * + * @see AbstractItem#delete() * @since 2.55 */ @Extension public class ItemDeletion extends Queue.QueueDecisionHandler { + private static final Logger LOGGER = Logger.getLogger(ItemDeletion.class.getName()); + /** * Lock to guard the {@link #registrations} set. */ @@ -176,4 +193,94 @@ public boolean shouldSchedule(Queue.Task p, List actions) { } return true; } + + /** + * Cancels any builds in progress of this item (if a job) or descendants (if a folder). + * Also cancels any associated queue items. + * @param initiatingItem an item being deleted + * @since TODO + */ + public static void cancelBuildsInProgress(@NonNull Item initiatingItem) throws Failure, InterruptedException { + Queue queue = Queue.getInstance(); + if (initiatingItem instanceof Queue.Task) { + // clear any items in the queue so they do not get picked up + queue.cancel((Queue.Task) initiatingItem); + } + // now cancel any child items - this happens after ItemDeletion registration, so we can use a snapshot + for (Queue.Item i : queue.getItems()) { + Item item = Tasks.getItemOf(i.task); + while (item != null) { + if (item == initiatingItem) { + if (!queue.cancel(i)) { + LOGGER.warning(() -> "failed to cancel " + i); + } + break; + } + if (item.getParent() instanceof Item) { + item = (Item) item.getParent(); + } else { + break; + } + } + } + // interrupt any builds in progress (and this should be a recursive test so that folders do not pay + // the 15 second delay for every child item). This happens after queue cancellation, so will be + // a complete set of builds in flight + Map buildsInProgress = new LinkedHashMap<>(); + for (Computer c : Jenkins.get().getComputers()) { + for (Executor e : c.getAllExecutors()) { + final WorkUnit workUnit = e.getCurrentWorkUnit(); + final Queue.Executable executable = workUnit != null ? workUnit.getExecutable() : null; + final SubTask subtask = executable != null ? Executables.getParentOf(executable) : null; + if (subtask != null) { + Item item = Tasks.getItemOf(subtask); + while (item != null) { + if (item == initiatingItem) { + buildsInProgress.put(e, e.getCurrentExecutable()); + e.interrupt(Result.ABORTED); + break; + } + if (item.getParent() instanceof Item) { + item = (Item) item.getParent(); + } else { + break; + } + } + } + } + } + if (!buildsInProgress.isEmpty()) { + // give them 15 seconds or so to respond to the interrupt + long expiration = System.nanoTime() + TimeUnit.SECONDS.toNanos(15); + // comparison with executor.getCurrentExecutable() == computation currently should always be true + // as we no longer recycle Executors, but safer to future-proof in case we ever revisit recycling + while (!buildsInProgress.isEmpty() && expiration - System.nanoTime() > 0L) { + // we know that ItemDeletion will prevent any new builds in the queue + // ItemDeletion happens-before Queue.cancel so we know that the Queue will stay clear + // Queue.cancel happens-before collecting the buildsInProgress list + // thus buildsInProgress contains the complete set we need to interrupt and wait for + for (Iterator> iterator = + buildsInProgress.entrySet().iterator(); + iterator.hasNext(); ) { + Map.Entry entry = iterator.next(); + // comparison with executor.getCurrentExecutable() == executable currently should always be + // true as we no longer recycle Executors, but safer to future-proof in case we ever + // revisit recycling. + if (!entry.getKey().isAlive() + || entry.getValue() != entry.getKey().getCurrentExecutable()) { + iterator.remove(); + } + // I don't know why, but we have to keep interrupting + entry.getKey().interrupt(Result.ABORTED); + } + Thread.sleep(50L); + } + if (!buildsInProgress.isEmpty()) { + throw new Failure(Messages.AbstractItem_FailureToStopBuilds( + buildsInProgress.size(), initiatingItem.getFullDisplayName() + )); + } + } + } + } diff --git a/core/src/main/java/jenkins/widgets/BuildTimeTrend.java b/core/src/main/java/jenkins/widgets/BuildTimeTrend.java index 59ab879f05da..dd0f0434ecb3 100644 --- a/core/src/main/java/jenkins/widgets/BuildTimeTrend.java +++ b/core/src/main/java/jenkins/widgets/BuildTimeTrend.java @@ -25,7 +25,9 @@ package jenkins.widgets; import hudson.model.AbstractBuild; +import hudson.model.AbstractProject; import hudson.model.BallColor; +import hudson.model.Job; import hudson.model.Node; import hudson.model.Run; import jenkins.console.ConsoleUrlProvider; @@ -37,6 +39,10 @@ @Restricted(DoNotUse.class) // only for buildTimeTrend.jelly public class BuildTimeTrend extends RunListProgressiveRendering { + public boolean isAbstractProject(Job job) { + return job instanceof AbstractProject; + } + @Override protected void calculate(Run build, JSONObject element) { BallColor iconColor = build.getIconColor(); element.put("iconName", iconColor.getIconName()); @@ -46,6 +52,8 @@ public class BuildTimeTrend extends RunListProgressiveRendering { element.put("displayName", build.getDisplayName()); element.put("duration", build.getDuration()); element.put("durationString", build.getDurationString()); + element.put("timestampString", build.getTimestampString()); + element.put("timestampString2", build.getTimestampString2()); element.put("consoleUrl", ConsoleUrlProvider.getRedirectUrl(build)); if (build instanceof AbstractBuild) { AbstractBuild b = (AbstractBuild) build; diff --git a/core/src/main/resources/hudson/TcpSlaveAgentListener/index.jelly b/core/src/main/resources/hudson/TcpSlaveAgentListener/index.jelly index 1b04d3c2c9ae..9e4b36af14a2 100644 --- a/core/src/main/resources/hudson/TcpSlaveAgentListener/index.jelly +++ b/core/src/main/resources/hudson/TcpSlaveAgentListener/index.jelly @@ -40,8 +40,11 @@ THE SOFTWARE. - - + + + + + Jenkins diff --git a/core/src/main/resources/hudson/model/Job/buildTimeTrend.jelly b/core/src/main/resources/hudson/model/Job/buildTimeTrend.jelly index 18f874a6751f..7efb53f362a4 100644 --- a/core/src/main/resources/hudson/model/Job/buildTimeTrend.jelly +++ b/core/src/main/resources/hudson/model/Job/buildTimeTrend.jelly @@ -25,7 +25,7 @@ THE SOFTWARE. - + @@ -45,29 +45,36 @@ THE SOFTWARE. +

${%Build Time Trend}

-
+
+
+ + ${handler.setBuilds(it.builds)} + + + + + + + + + + + + + + + + + +
${%S}${%Build}${%Time Since}${%Duration}${%Agent}
+ +
[${%Build time graph}]
- - -
- - ${handler.setBuilds(it.builds)} - - - - - - - - - - -
${%Build}${%Duration}${%Agent}
-
diff --git a/core/src/main/resources/hudson/model/Job/buildTimeTrend_resources.css b/core/src/main/resources/hudson/model/Job/buildTimeTrend_resources.css index 13efe1c1e375..af8ba3705509 100644 --- a/core/src/main/resources/hudson/model/Job/buildTimeTrend_resources.css +++ b/core/src/main/resources/hudson/model/Job/buildTimeTrend_resources.css @@ -1,3 +1,10 @@ -img.build-time-graph { - float: right; +#buildTimeTrend { + display: flex; + gap: 15px; +} + +@media (max-width: 1300px) { + #buildTimeTrend { + flex-direction: column-reverse; + } } diff --git a/core/src/main/resources/hudson/model/Job/buildTimeTrend_resources.js b/core/src/main/resources/hudson/model/Job/buildTimeTrend_resources.js index 8a6cbfd49253..9ea25a3441de 100644 --- a/core/src/main/resources/hudson/model/Job/buildTimeTrend_resources.js +++ b/core/src/main/resources/hudson/model/Job/buildTimeTrend_resources.js @@ -2,30 +2,31 @@ * Public method to be called by progressiveRendering's callback */ window.buildTimeTrend_displayBuilds = function (data) { - var p = document.getElementById("trend"); - var isDistributedBuildsEnabled = - "true" === p.getAttribute("data-is-distributed-build-enabled"); - var rootURL = document.head.getAttribute("data-rooturl"); + const p = document.getElementById("trend"); + p.classList.remove("jenkins-hidden"); - for (var x = 0; data.length > x; x++) { - var e = data[x]; - var tr = document.createElement("tr"); + const showAgent = "true" === p.dataset.showAgent; + const rootURL = document.head.getAttribute("data-rooturl"); + + for (let x = 0; data.length > x; x++) { + const e = data[x]; + let tr = document.createElement("tr"); let td = document.createElement("td"); td.setAttribute("data", e.iconColorOrdinal); - - let link = document.createElement("a"); - link.classList.add("build-status-link"); - link.href = e.consoleUrl; - td.appendChild(link); + td.classList.add("jenkins-table__cell--tight", "jenkins-table__icon"); + let div = document.createElement("div"); + div.classList.add("jenkins-table__cell__button-wrapper"); let svg = generateSVGIcon(e.iconName); - link.appendChild(svg); + svg.setAttribute("tooltip", e.iconColorDescription); + div.appendChild(svg); + td.appendChild(div); tr.appendChild(td); td = document.createElement("td"); td.setAttribute("data", e.number); - link = document.createElement("a"); + let link = document.createElement("a"); link.href = e.number + "/"; link.classList.add("model-link", "inside"); link.innerText = escapeHTML(e.displayName); @@ -33,15 +34,20 @@ window.buildTimeTrend_displayBuilds = function (data) { td.appendChild(link); tr.appendChild(td); + td = document.createElement("td"); + td.setAttribute("data", e.timestampString2); + td.textContent = e.timestampString; + tr.appendChild(td); + td = document.createElement("td"); td.setAttribute("data", e.duration); td.innerText = escapeHTML(e.durationString); tr.appendChild(td); - if (isDistributedBuildsEnabled) { - var buildInfo = null; - var buildInfoStr = escapeHTML(e.builtOnStr || ""); + if (showAgent) { + let buildInfo = null; + let buildInfoStr = escapeHTML(e.builtOnStr || ""); if (e.builtOn) { buildInfo = document.createElement("a"); buildInfo.href = rootURL + "/computer/" + e.builtOn; @@ -58,6 +64,19 @@ window.buildTimeTrend_displayBuilds = function (data) { } tr.appendChild(td); } + + let tdConsole = document.createElement("td"); + tdConsole.classList.add("jenkins-table__cell--tight"); + let div2 = document.createElement("div"); + div2.classList.add("jenkins-table__cell__button-wrapper"); + link = document.createElement("a"); + link.classList.add("jenkins-button", "jenkins-button--tertiary"); + link.href = e.consoleUrl; + link.appendChild(generateSVGIcon("console")); + div2.appendChild(link); + tdConsole.appendChild(div2); + tr.appendChild(tdConsole); + p.appendChild(tr); Behaviour.applySubtree(tr); } @@ -132,7 +151,7 @@ window.displayBuilds = function (data) { var div2 = document.createElement("div"); div2.classList.add("jenkins-table__cell__button-wrapper"); var a3 = document.createElement("a"); - a3.classList.add("jenkins-button"); + a3.classList.add("jenkins-button", "jenkins-button--tertiary"); a3.href = e.consoleUrl; a3.innerHTML = p.dataset.consoleOutputIcon; div2.appendChild(a3); diff --git a/core/src/main/resources/jenkins/monitor/OperatingSystemEndOfLifeAdminMonitor/end-of-life-data.json b/core/src/main/resources/jenkins/monitor/OperatingSystemEndOfLifeAdminMonitor/end-of-life-data.json index e3df71741554..7dfa00bc6772 100644 --- a/core/src/main/resources/jenkins/monitor/OperatingSystemEndOfLifeAdminMonitor/end-of-life-data.json +++ b/core/src/main/resources/jenkins/monitor/OperatingSystemEndOfLifeAdminMonitor/end-of-life-data.json @@ -1,7 +1,7 @@ [ { "pattern": "AlmaLinux.* 8.*", - "endOfLife": "2029-03-31" + "endOfLife": "2029-03-01" }, { "pattern": "AlmaLinux.* 9.*", @@ -17,38 +17,39 @@ }, { "pattern": "Alpine Linux v3.16", - "endOfLife": "2024-05-01" + "endOfLife": "2024-05-23" }, { "pattern": "Alpine Linux v3.17", - "endOfLife": "2024-11-01" + "endOfLife": "2024-11-22" }, { "pattern": "Alpine Linux v3.18", - "endOfLife": "2025-05-01" + "endOfLife": "2025-05-09" }, { "pattern": "Alpine Linux v3.19", "endOfLife": "2025-11-01" }, + { + "pattern": "Alpine Linux v3.20", + "endOfLife": "2026-04-01" + }, { "pattern": "Amazon Linux 2", - "start": "2023-05-01", "endOfLife": "2023-11-16" }, { "pattern": "Amazon Linux 2023", - "start": "2027-09-15", "endOfLife": "2028-03-15" }, { "pattern": "CentOS Linux.* 7.*", - "start": "2023-05-01", "endOfLife": "2023-11-16" }, { "pattern": "CentOS Linux.* 8.*", - "endOfLife": "2029-03-31" + "endOfLife": "2021-12-31" }, { "pattern": "Debian.* 10.*", @@ -60,45 +61,47 @@ }, { "pattern": "Debian.* 12.*", - "endOfLife": "2028-06-30" + "endOfLife": "2028-06-10" }, { "pattern": "Fedora.* 36.*", - "endOfLife": "2023-05-18" + "endOfLife": "2023-05-16" }, { "pattern": "Fedora.* 37.*", - "endOfLife": "2023-12-15" + "endOfLife": "2023-12-05" }, { "pattern": "Fedora.* 38.*", - "endOfLife": "2024-05-18" + "endOfLife": "2024-05-21" }, { "pattern": "Fedora.* 39.*", - "endOfLife": "2024-12-07" + "endOfLife": "2024-11-12" + }, + { + "pattern": "Fedora.* 40.*", + "endOfLife": "2025-05-13" }, { "pattern": "Oracle Linux.* 7.*", - "start": "2023-05-01", "endOfLife": "2023-11-16" }, { "pattern": "Oracle Linux.* 8.*", - "endOfLife": "2029-03-31" + "endOfLife": "2029-07-31" }, { "pattern": "Oracle Linux.* 9.*", - "endOfLife": "2034-06-30" + "endOfLife": "2032-06-30" }, { "pattern": "Red Hat Enterprise Linux.* 7.*", - "start": "2023-05-01", "endOfLife": "2023-11-16" }, { "pattern": "Red Hat Enterprise Linux.* 8.*", - "endOfLife": "2029-03-31" + "endOfLife": "2029-05-31" }, { "pattern": "Red Hat Enterprise Linux.* 9.*", @@ -106,7 +109,7 @@ }, { "pattern": "Rocky Linux.* 8.*", - "endOfLife": "2029-03-31" + "endOfLife": "2029-05-31" }, { "pattern": "Rocky Linux.* 9.*", @@ -114,7 +117,6 @@ }, { "pattern": "Scientific Linux.* 7.*", - "start": "2023-05-01", "endOfLife": "2023-11-16" }, { @@ -152,5 +154,9 @@ { "pattern": "Ubuntu.* 23.04.*", "endOfLife": "2024-01-20" + }, + { + "pattern": "Ubuntu.* 24.04.*", + "endOfLife": "2029-04-25" } ] diff --git a/core/src/main/resources/lib/form/secretTextarea.jelly b/core/src/main/resources/lib/form/secretTextarea.jelly index ca95219fc3f3..323a8435d0f3 100644 --- a/core/src/main/resources/lib/form/secretTextarea.jelly +++ b/core/src/main/resources/lib/form/secretTextarea.jelly @@ -2,7 +2,7 @@ - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - -