diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index d2b62e99a2f2..46794c280264 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -54,7 +54,6 @@ import hudson.model.ParameterDefinition; import hudson.model.ParameterDefinition.ParameterDescriptor; import hudson.model.PasswordParameterDefinition; -import hudson.model.Queue; import hudson.model.Run; import hudson.model.Slave; import hudson.model.TimeZoneProperty; @@ -157,6 +156,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import jenkins.console.ConsoleUrlProvider; +import jenkins.console.WithConsoleUrl; import jenkins.model.GlobalConfiguration; import jenkins.model.GlobalConfigurationCategory; import jenkins.model.Jenkins; @@ -1981,20 +1981,14 @@ public static String joinPath(String... components) { } /** - * Computes the link to the console for the run for the specified executable, taking {@link ConsoleUrlProvider} into account. - * @param executable the executable (normally a {@link Run}) - * @return the absolute URL for accessing the build console for the executable, or null if there is no build associated with the executable + * Computes the link to the console for the run for the specified object, taking {@link ConsoleUrlProvider} into account. + * @param withConsoleUrl the object to compute a console url for (can be {@link Run}, a {@code PlaceholderExecutable}...) + * @return the absolute URL for accessing the build console for the given object, or null if there is no console URL defined for the object. * @since 2.433 */ - public static @CheckForNull String getConsoleUrl(Queue.Executable executable) { - if (executable == null) { - return null; - } else if (executable instanceof Run) { - return ConsoleUrlProvider.getRedirectUrl((Run) executable); - } else { - // Handles cases such as PlaceholderExecutable for Pipeline node steps. - return getConsoleUrl(executable.getParentExecutable()); - } + public static @CheckForNull String getConsoleUrl(WithConsoleUrl withConsoleUrl) { + String consoleUrl = withConsoleUrl.getConsoleUrl(); + return consoleUrl != null ? Stapler.getCurrentRequest().getContextPath() + '/' + consoleUrl : null; } /** diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java index a4878a2c8f91..081f73dd1625 100644 --- a/core/src/main/java/hudson/model/Computer.java +++ b/core/src/main/java/hudson/model/Computer.java @@ -1102,6 +1102,8 @@ protected void removeExecutor(final Executor e) { if (ciBase != null) { // TODO confirm safe to assume non-null and use getInstance() ciBase.removeComputer(Computer.this); } + } else if (isIdle()) { + threadPoolForRemoting.submit(() -> Listeners.notify(ComputerListener.class, false, l -> l.onIdle(this))); } } }; diff --git a/core/src/main/java/hudson/model/Job.java b/core/src/main/java/hudson/model/Job.java index 10637d666d68..2ba53d7182a0 100644 --- a/core/src/main/java/hudson/model/Job.java +++ b/core/src/main/java/hudson/model/Job.java @@ -90,6 +90,7 @@ import jenkins.model.BuildDiscarder; import jenkins.model.BuildDiscarderProperty; import jenkins.model.DirectlyModifiableTopLevelItemGroup; +import jenkins.model.HistoricalBuild; import jenkins.model.Jenkins; import jenkins.model.JenkinsLocationConfiguration; import jenkins.model.ModelObjectWithChildren; @@ -636,9 +637,9 @@ protected HistoryWidget createHistoryWidget() { throw new IllegalStateException("HistoryWidget is now created via WidgetFactory implementation"); } - public static final HistoryWidget.Adapter HISTORY_ADAPTER = new Adapter<>() { + public static final HistoryWidget.Adapter HISTORY_ADAPTER = new Adapter<>() { @Override - public int compare(Run record, String key) { + public int compare(HistoricalBuild record, String key) { try { int k = Integer.parseInt(key); return record.getNumber() - k; @@ -648,12 +649,12 @@ public int compare(Run record, String key) { } @Override - public String getKey(Run record) { + public String getKey(HistoricalBuild record) { return String.valueOf(record.getNumber()); } @Override - public boolean isBuilding(Run record) { + public boolean isBuilding(HistoricalBuild record) { return record.isBuilding(); } diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java index a8d4ca08ea0d..08eba90b906c 100644 --- a/core/src/main/java/hudson/model/Queue.java +++ b/core/src/main/java/hudson/model/Queue.java @@ -109,6 +109,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; +import jenkins.console.WithConsoleUrl; import jenkins.model.Jenkins; import jenkins.model.queue.AsynchronousExecution; import jenkins.model.queue.CompositeCauseOfBlockage; @@ -2088,7 +2089,7 @@ default Collection getSubTasks() { * used to render the HTML that indicates this executable is executing. */ @StaplerAccessibleType - public interface Executable extends Runnable { + public interface Executable extends Runnable, WithConsoleUrl { /** * Task from which this executable was created. * @@ -2131,6 +2132,16 @@ default long getEstimatedDuration() { return Executables.getParentOf(this).getEstimatedDuration(); } + /** + * Handles cases such as {@code PlaceholderExecutable} for Pipeline node steps. + * @return by default, that of {@link #getParentExecutable} if defined + */ + @Override + default String getConsoleUrl() { + Executable parent = getParentExecutable(); + return parent != null ? parent.getConsoleUrl() : null; + } + /** * Used to render the HTML. Should be a human readable text of what this executable is. */ diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java index c6eedc42785f..5359634d9642 100644 --- a/core/src/main/java/hudson/model/Run.java +++ b/core/src/main/java/hudson/model/Run.java @@ -109,10 +109,13 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.zip.GZIPInputStream; +import jenkins.console.ConsoleUrlProvider; +import jenkins.console.WithConsoleUrl; import jenkins.model.ArtifactManager; import jenkins.model.ArtifactManagerConfiguration; import jenkins.model.ArtifactManagerFactory; import jenkins.model.BuildDiscarder; +import jenkins.model.HistoricalBuild; import jenkins.model.Jenkins; import jenkins.model.JenkinsLocationConfiguration; import jenkins.model.RunAction2; @@ -157,7 +160,7 @@ */ @ExportedBean public abstract class Run, RunT extends Run> - extends Actionable implements ExtensionPoint, Comparable, AccessControlled, PersistenceRoot, DescriptorByNameOwner, OnMaster, StaplerProxy { + extends Actionable implements ExtensionPoint, Comparable, AccessControlled, PersistenceRoot, DescriptorByNameOwner, OnMaster, StaplerProxy, HistoricalBuild, WithConsoleUrl { /** * The original {@link Queue.Item#getId()} has not yet been mapped onto the {@link Run} instance. @@ -165,15 +168,6 @@ public abstract class Run, RunT extends Run - * When a build is {@link #isBuilding() in progress}, this method - * returns an intermediate result. - * @return The status of the build, if it has completed or some build step has set a status; may be null if the build is ongoing. - */ @Exported + @Override public @CheckForNull Result getResult() { return result; } @@ -518,6 +505,7 @@ public void setResult(@NonNull Result r) { /** * Gets the subset of {@link #getActions()} that consists of {@link BuildBadgeAction}s. */ + @Override public @NonNull List getBadgeActions() { List r = getActions(BuildBadgeAction.class); if (isKeepLog()) { @@ -527,11 +515,8 @@ public void setResult(@NonNull Result r) { return r; } - /** - * Returns true if the build is not completed yet. - * This includes "not started yet" state. - */ @Exported + @Override public boolean isBuilding() { return state.compareTo(State.POST_PRODUCTION) < 0; } @@ -650,11 +635,11 @@ public final boolean isKeepLog() { } /** - * When the build is scheduled. - * + * {@inheritDoc} * @see #getStartTimeInMillis() */ @Exported + @Override public @NonNull Calendar getTimestamp() { GregorianCalendar c = new GregorianCalendar(); c.setTimeInMillis(timestamp); @@ -689,73 +674,12 @@ public final long getStartTimeInMillis() { } @Exported + @Override @CheckForNull public String getDescription() { return description; } - - /** - * Returns the length-limited description. - * The method tries to take HTML tags within the description into account, but it is a best-effort attempt. - * Also, the method will likely not work properly if a non-HTML {@link hudson.markup.MarkupFormatter} is used. - * @return The length-limited description. - * @deprecated truncated description based on the {@link #TRUNCATED_DESCRIPTION_LIMIT} setting. - */ - @Deprecated - public @CheckForNull String getTruncatedDescription() { - if (TRUNCATED_DESCRIPTION_LIMIT < 0) { // disabled - return description; - } - if (TRUNCATED_DESCRIPTION_LIMIT == 0) { // Someone wants to suppress descriptions, why not? - return ""; - } - - final int maxDescrLength = TRUNCATED_DESCRIPTION_LIMIT; - final String localDescription = description; - if (localDescription == null || localDescription.length() < maxDescrLength) { - return localDescription; - } - - final String ending = "..."; - final int sz = localDescription.length(), maxTruncLength = maxDescrLength - ending.length(); - - boolean inTag = false; - int displayChars = 0; - int lastTruncatablePoint = -1; - - for (int i = 0; i < sz; i++) { - char ch = localDescription.charAt(i); - if (ch == '<') { - inTag = true; - } else if (ch == '>') { - inTag = false; - if (displayChars <= maxTruncLength) { - lastTruncatablePoint = i + 1; - } - } - if (!inTag) { - displayChars++; - if (displayChars <= maxTruncLength && ch == ' ') { - lastTruncatablePoint = i; - } - } - } - - String truncDesc = localDescription; - - // Could not find a preferred truncatable index, force a trunc at maxTruncLength - if (lastTruncatablePoint == -1) - lastTruncatablePoint = maxTruncLength; - - if (displayChars >= maxDescrLength) { - truncDesc = truncDesc.substring(0, lastTruncatablePoint) + ending; - } - - return truncDesc; - - } - /** * Gets the string that says how long since this build has started. * @@ -774,9 +698,7 @@ public String getDescription() { return Util.XS_DATETIME_FORMATTER2.format(Instant.ofEpochMilli(timestamp)); } - /** - * Gets the string that says how long the build took to run. - */ + @Override public @NonNull String getDurationString() { if (hasntStartedYet()) { return Messages.Run_NotStartedYet(); @@ -795,9 +717,7 @@ public long getDuration() { return duration; } - /** - * Gets the icon color for display. - */ + @Override public @NonNull BallColor getIconColor() { if (!isBuilding()) { // already built @@ -832,6 +752,7 @@ public String toString() { } @Exported + @Override public String getFullDisplayName() { return project.getFullDisplayName() + ' ' + getDisplayName(); } @@ -857,6 +778,7 @@ public void setDisplayName(String value) throws IOException { } @Exported(visibility = 2) + @Override public int getNumber() { return number; } @@ -1034,14 +956,8 @@ protected void dropLinks() { return nextBuild; } - /** - * Returns the URL of this {@link Run}, relative to the context root of Hudson. - * - * @return - * String like "job/foo/32/" with trailing slash but no leading slash. - */ - // I really messed this up. I'm hoping to fix this some time - // it shouldn't have trailing '/', and instead it should have leading '/' + + @Override public @NonNull String getUrl() { // RUN may be accessed using permalinks, as "/lastSuccessful" or other, so try to retrieve this base URL @@ -1059,6 +975,14 @@ protected void dropLinks() { return project.getUrl() + getNumber() + '/'; } + /** + * @see ConsoleUrlProvider#consoleUrlOf + */ + @Override + public String getConsoleUrl() { + return ConsoleUrlProvider.consoleUrlOf(this); + } + /** * Obtains the absolute URL to this build. * @@ -1097,6 +1021,12 @@ protected void dropLinks() { return new File(project.getBuildDir(), Integer.toString(number)); } + @Override + public List getParameterValues() { + ParametersAction a = getAction(ParametersAction.class); + return a != null ? a.getParameters() : List.of(); + } + /** * Gets an object responsible for storing and retrieving build artifacts. * If {@link #pickArtifactManager} has previously been called on this build, @@ -2165,14 +2095,6 @@ public void doBuildStatus(StaplerRequest2 req, StaplerResponse2 rsp) throws IOEx rsp.sendRedirect2(req.getContextPath() + "/images/48x48/" + getBuildStatusUrl()); } - public @NonNull String getBuildStatusUrl() { - return getIconColor().getImage(); - } - - public String getBuildStatusIconClassName() { - return getIconColor().getIconClassName(); - } - public static class Summary { /** * Is this build worse or better, compared to the previous build? diff --git a/core/src/main/java/hudson/model/TopLevelItemDescriptor.java b/core/src/main/java/hudson/model/TopLevelItemDescriptor.java index 817c1194a3d5..b6177005c2d2 100644 --- a/core/src/main/java/hudson/model/TopLevelItemDescriptor.java +++ b/core/src/main/java/hudson/model/TopLevelItemDescriptor.java @@ -293,6 +293,6 @@ public static ExtensionList all() { @Restricted(NoExternalUse.class) public FormValidation doCheckDisplayNameOrNull(@AncestorInPath TopLevelItem item, @QueryParameter String value) { - return Jenkins.get().doCheckDisplayName(value, item.getName()); + return Jenkins.get().checkDisplayName(value, item); } } diff --git a/core/src/main/java/hudson/security/ChainedServletFilter.java b/core/src/main/java/hudson/security/ChainedServletFilter.java index 98bc725c2b1a..a0a458f810f6 100644 --- a/core/src/main/java/hudson/security/ChainedServletFilter.java +++ b/core/src/main/java/hudson/security/ChainedServletFilter.java @@ -24,28 +24,29 @@ package hudson.security; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.FilterConfig; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; -import org.kohsuke.stapler.CompatibleFilter; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; /** * Servlet {@link Filter} that chains multiple {@link Filter}s. * * @author Kohsuke Kawaguchi + * @deprecated use {@link ChainedServletFilter2} */ -public class ChainedServletFilter implements CompatibleFilter { +@Deprecated +public class ChainedServletFilter implements Filter { // array is assumed to be immutable once set protected volatile Filter[] filters; diff --git a/core/src/main/java/hudson/security/ChainedServletFilter2.java b/core/src/main/java/hudson/security/ChainedServletFilter2.java new file mode 100644 index 000000000000..027738bfdfc9 --- /dev/null +++ b/core/src/main/java/hudson/security/ChainedServletFilter2.java @@ -0,0 +1,122 @@ +/* + * The MIT License + * + * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi + * + * 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.security; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +/** + * Servlet {@link Filter} that chains multiple {@link Filter}s. + * + * @author Kohsuke Kawaguchi + */ +public class ChainedServletFilter2 implements Filter { + // array is assumed to be immutable once set + protected volatile Filter[] filters; + + public ChainedServletFilter2() { + filters = new Filter[0]; + } + + public ChainedServletFilter2(Filter... filters) { + this(Arrays.asList(filters)); + } + + public ChainedServletFilter2(Collection filters) { + setFilters(filters); + } + + public void setFilters(Collection filters) { + this.filters = filters.toArray(new Filter[0]); + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + if (LOGGER.isLoggable(Level.FINEST)) + for (Filter f : filters) + LOGGER.finest("ChainedServletFilter2 contains: " + f); + + for (Filter f : filters) + f.init(filterConfig); + } + + private static final Pattern UNINTERESTING_URIS = Pattern.compile("/(images|jsbundles|css|scripts|adjuncts)/|/favicon[.](ico|svg)|/ajax"); + + @Override + public void doFilter(ServletRequest request, ServletResponse response, final FilterChain chain) throws IOException, ServletException { + String uri = request instanceof HttpServletRequest ? ((HttpServletRequest) request).getRequestURI() : "?"; + Level level = UNINTERESTING_URIS.matcher(uri).find() ? Level.FINER : Level.FINE; + LOGGER.log(level, () -> "starting filter on " + uri); + + new FilterChain() { + private int position = 0; + // capture the array for thread-safety + private final Filter[] filters = ChainedServletFilter2.this.filters; + + @Override + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + if (position == filters.length) { + LOGGER.log(level, () -> uri + " end: " + status()); + chain.doFilter(request, response); + } else { + Filter next = filters[position++]; + try { + LOGGER.log(level, () -> uri + " @" + position + " " + next + " »"); + next.doFilter(request, response, this); + LOGGER.log(level, () -> uri + " @" + position + " " + next + " « success: " + status()); + } catch (IOException | ServletException | RuntimeException x) { + LOGGER.log(level, () -> uri + " @" + position + " " + next + " « " + x + ": " + status()); + throw x; + } + } + } + + private int status() { + return response instanceof HttpServletResponse ? ((HttpServletResponse) response).getStatus() : 0; + } + }.doFilter(request, response); + + } + + @Override + public void destroy() { + for (Filter f : filters) + f.destroy(); + } + + private static final Logger LOGGER = Logger.getLogger(ChainedServletFilter2.class.getName()); +} diff --git a/core/src/main/java/hudson/security/LegacySecurityRealm.java b/core/src/main/java/hudson/security/LegacySecurityRealm.java index 94c895782f74..120b8d8e2cab 100644 --- a/core/src/main/java/hudson/security/LegacySecurityRealm.java +++ b/core/src/main/java/hudson/security/LegacySecurityRealm.java @@ -89,7 +89,7 @@ public Filter createFilter(FilterConfig filterConfig) { // when using container-authentication we can't hit /login directly. // we first have to hit protected /loginEntry, then let the container // trap that into /login. - return new ChainedServletFilter(filters); + return new ChainedServletFilter2(filters); } /** diff --git a/core/src/main/java/hudson/security/SecurityRealm.java b/core/src/main/java/hudson/security/SecurityRealm.java index d10e7994ae22..d99e3487eaa6 100644 --- a/core/src/main/java/hudson/security/SecurityRealm.java +++ b/core/src/main/java/hudson/security/SecurityRealm.java @@ -678,7 +678,7 @@ private Filter createFilterImpl(FilterConfig filterConfig) { } filters.add(new RememberMeAuthenticationFilter(sc.manager2, sc.rememberMe2)); filters.addAll(commonFilters()); - return new ChainedServletFilter(filters); + return new ChainedServletFilter2(filters); } protected final List commonFilters() { @@ -781,7 +781,7 @@ public GroupDetails loadGroupByGroupname2(String groupname, boolean fetchMembers */ @Override public Filter createFilter(FilterConfig filterConfig) { - return new ChainedServletFilter(); + return new ChainedServletFilter2(); } /** diff --git a/core/src/main/java/hudson/slaves/ComputerListener.java b/core/src/main/java/hudson/slaves/ComputerListener.java index bd794c456c50..f5d220c1104e 100644 --- a/core/src/main/java/hudson/slaves/ComputerListener.java +++ b/core/src/main/java/hudson/slaves/ComputerListener.java @@ -211,6 +211,13 @@ public void onTemporarilyOffline(Computer c, OfflineCause cause) {} */ public void onConfigurationChange() {} + /** + * Indicates that the computer has become idle. + * + * @since TODO + */ + public void onIdle(Computer c) {} + /** * Registers this {@link ComputerListener} so that it will start receiving events. * diff --git a/core/src/main/java/hudson/widgets/BuildHistoryWidget.java b/core/src/main/java/hudson/widgets/BuildHistoryWidget.java index 2c171b594f5a..bf75e2bf0c7a 100644 --- a/core/src/main/java/hudson/widgets/BuildHistoryWidget.java +++ b/core/src/main/java/hudson/widgets/BuildHistoryWidget.java @@ -33,6 +33,7 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; +import jenkins.model.HistoricalBuild; import jenkins.model.Jenkins; import jenkins.model.queue.QueueItem; import jenkins.widgets.HistoryPageFilter; @@ -47,7 +48,7 @@ *

* This widget enhances {@link HistoryWidget} by groking the notion * that {@link #owner} can be in the queue toward the next build. - * + * @param typically {@link HistoricalBuild} * @author Kohsuke Kawaguchi */ public class BuildHistoryWidget extends HistoryWidget { @@ -84,7 +85,6 @@ public HistoryPageFilter getHistoryPageFilter() { final HistoryPageFilter historyPageFilter = newPageFilter(); historyPageFilter.add(baseList, getQueuedItems()); - historyPageFilter.widget = this; return updateFirstTransientBuildKey(historyPageFilter); } diff --git a/core/src/main/java/hudson/widgets/HistoryWidget.java b/core/src/main/java/hudson/widgets/HistoryWidget.java index 44f6de67ee6f..e7dcafbb5426 100644 --- a/core/src/main/java/hudson/widgets/HistoryWidget.java +++ b/core/src/main/java/hudson/widgets/HistoryWidget.java @@ -31,7 +31,6 @@ import hudson.model.Job; import hudson.model.ModelObject; import hudson.model.Queue; -import hudson.model.Run; import hudson.util.AlternativeUiTextProvider; import jakarta.servlet.ServletException; import java.io.IOException; @@ -40,6 +39,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import jenkins.model.HistoricalBuild; import jenkins.util.SystemProperties; import jenkins.widgets.HistoryPageEntry; import jenkins.widgets.HistoryPageFilter; @@ -53,12 +53,12 @@ import org.kohsuke.stapler.StaplerResponse2; /** - * Displays the history of records (normally {@link Run}s) on the side panel. + * Displays the history of records on the side panel. * * @param - * Owner of the widget. + * Owner of the widget, typically {@link Job} * @param - * Type individual record. + * Type individual record, typically {@link HistoricalBuild} * @author Kohsuke Kawaguchi */ public class HistoryWidget extends Widget { @@ -142,7 +142,7 @@ public String getFirstTransientBuildKey() { * @return * The history page filter that was passed in. */ - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") // TODO actually not type-safe protected HistoryPageFilter updateFirstTransientBuildKey(HistoryPageFilter historyPageFilter) { updateFirstTransientBuildKey(historyPageFilter.runs); return historyPageFilter; @@ -195,7 +195,7 @@ private List> toPageEntries(Iterable historyItemList) { /** * Get a {@link jenkins.widgets.HistoryPageFilter} for rendering a page of queue items. */ - public HistoryPageFilter getHistoryPageFilter() { + public HistoryPageFilter getHistoryPageFilter() { HistoryPageFilter historyPageFilter = newPageFilter(); historyPageFilter.add(baseList); @@ -205,6 +205,7 @@ public HistoryPageFilter getHistoryPageFilter() { protected HistoryPageFilter newPageFilter() { HistoryPageFilter historyPageFilter = new HistoryPageFilter<>(THRESHOLD); + historyPageFilter.widget = this; if (newerThan != null) { historyPageFilter.setNewerThan(newerThan); diff --git a/core/src/main/java/jenkins/console/ConsoleUrlProvider.java b/core/src/main/java/jenkins/console/ConsoleUrlProvider.java index 3310f13fd332..dcd3883ba07c 100644 --- a/core/src/main/java/jenkins/console/ConsoleUrlProvider.java +++ b/core/src/main/java/jenkins/console/ConsoleUrlProvider.java @@ -62,8 +62,8 @@ public interface ConsoleUrlProvider extends Describable { * Get a URL relative to the context path of Jenkins which should be used to link to the console for the specified build. *

Should only be used in the context of serving an HTTP request. * @param run the build - * @return the URL for the console for the specified build, relative to the context of Jenkins, or {@code null} - * if this implementation does not want to server a special console view for this build. + * @return the URL for the console for the specified build, relative to the context of Jenkins (should not start with {@code /}), or {@code null} + * if this implementation does not want to serve a special console view for this build. */ @CheckForNull String getConsoleUrl(Run run); @@ -80,6 +80,14 @@ default Descriptor getDescriptor() { * @return the URL for the console for the specified build, relative to the web server root */ static @NonNull String getRedirectUrl(Run run) { + return Stapler.getCurrentRequest().getContextPath() + '/' + run.getConsoleUrl(); + } + + /** + * Looks up the {@link #getConsoleUrl} value from the first provider to offer one. + * @since TODO + */ + static @NonNull String consoleUrlOf(Run run) { final List providers = new ArrayList<>(); User currentUser = User.current(); if (currentUser != null) { @@ -104,6 +112,8 @@ default Descriptor getDescriptor() { if (tempUrl != null) { if (new URI(tempUrl).isAbsolute()) { LOGGER.warning(() -> "Ignoring absolute console URL " + tempUrl + " for " + run + " from " + provider.getClass()); + } else if (tempUrl.startsWith("/")) { + LOGGER.warning(() -> "Ignoring URL " + tempUrl + " starting with / for " + run + " from " + provider.getClass()); } else { // Found a valid non-null URL. url = tempUrl; @@ -118,11 +128,7 @@ default Descriptor getDescriptor() { // Reachable if DefaultConsoleUrlProvider is not one of the configured providers, including if no providers are configured at all. url = run.getUrl() + "console"; } - if (url.startsWith("/")) { - return Stapler.getCurrentRequest2().getContextPath() + url; - } else { - return Stapler.getCurrentRequest2().getContextPath() + '/' + url; - } + return url; } /** diff --git a/core/src/main/java/jenkins/console/WithConsoleUrl.java b/core/src/main/java/jenkins/console/WithConsoleUrl.java new file mode 100644 index 000000000000..d667b40fae66 --- /dev/null +++ b/core/src/main/java/jenkins/console/WithConsoleUrl.java @@ -0,0 +1,43 @@ +/* + * The MIT License + * + * Copyright 2024 CloudBees, 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 jenkins.console; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +/** + * A model object that may have a console URL. + */ +@Restricted(Beta.class) +public interface WithConsoleUrl { + + /** + * @return a URL relative to the context root without leading slash, such as {@code job/xxx/123/console}; + * or null if unknown or not applicable + */ + @CheckForNull + String getConsoleUrl(); +} diff --git a/core/src/main/java/jenkins/model/HistoricalBuild.java b/core/src/main/java/jenkins/model/HistoricalBuild.java new file mode 100644 index 000000000000..2f9f14aefff7 --- /dev/null +++ b/core/src/main/java/jenkins/model/HistoricalBuild.java @@ -0,0 +1,211 @@ +/* + * The MIT License + * + * Copyright 2024 CloudBees, 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 jenkins.model; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.markup.MarkupFormatter; +import hudson.model.BallColor; +import hudson.model.BuildBadgeAction; +import hudson.model.ModelObject; +import hudson.model.ParameterValue; +import hudson.model.ParametersAction; +import hudson.model.Queue; +import hudson.model.Result; +import hudson.model.Run; +import hudson.widgets.BuildHistoryWidget; +import java.util.Calendar; +import java.util.List; +import jenkins.util.SystemProperties; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +/** + * A {@link Run}-like object as it might be displayed by {@link BuildHistoryWidget}. + * + * @since TODO + */ +@Restricted(Beta.class) +public interface HistoricalBuild extends ModelObject { + + /** + * @return A build number + */ + int getNumber(); + + /** + * Returns the URL of this {@link HistoricalBuild}, relative to the context root of Jenkins. + * + * @return + * String like "job/foo/32/" with trailing slash but no leading slash. + */ + @NonNull String getUrl(); + + /** + * Returns a human-readable description which is used on the main build page. + *

+ * It can also be quite long, and it may use markup in a format defined by a {@link hudson.markup.MarkupFormatter}. + * {@link #getTruncatedDescription()} may be used to retrieve a size-limited description, + * but it implies some limitations. + * @return the build description. + */ + @CheckForNull + String getDescription(); + + /** + * @return a human-readable full display name of this build. + */ + @NonNull + String getFullDisplayName(); + + /** + * Get the {@link Queue.Item#getId()} of the original queue item from where this {@link HistoricalBuild} instance + * originated. + * @return The queue item ID. + */ + long getQueueId(); + + /** + * Returns the build result. + * + *

+ * When a build is {@link #isBuilding() in progress}, this method + * returns an intermediate result. + * @return The status of the build, if it has completed or some build step has set a status; may be null if the build is ongoing. + */ + @CheckForNull + Result getResult(); + + /** @see ParametersAction#getParameters */ + @NonNull + List getParameterValues(); + + /** + * Returns true if the build is not completed yet. + * This includes "not started yet" state. + */ + boolean isBuilding(); + + /** + * Gets the icon color for display. + */ + @NonNull + BallColor getIconColor(); + + @NonNull + default String getBuildStatusIconClassName() { + return getIconColor().getIconClassName(); + } + + @NonNull + default String getBuildStatusUrl() { + return getIconColor().getImage(); + } + + /** + * When the build is scheduled. + */ + @NonNull + Calendar getTimestamp(); + + /** + * Gets the string that says how long the build took to run. + */ + @NonNull + String getDurationString(); + + /** + * Gets the list of {@link BuildBadgeAction}s applicable to this instance. + */ + @NonNull + List getBadgeActions(); + + /** + * Returns the length-limited description. + * The method tries to take HTML tags within the description into account, but it is a best-effort attempt. + * Also, the method will likely not work properly if a non-HTML {@link MarkupFormatter} is used. + * @return The length-limited description. + */ + @CheckForNull + default String getTruncatedDescription() { + String description = getDescription(); + /* + * Target size limit for truncated {@link #description}s in the Build History Widget. + * This is applied to the raw, unformatted description. Especially complex formatting + * like hyperlinks can result in much less text being shown than this might imply. + * Negative values will disable truncation, {@code 0} will enforce empty strings. + */ + int truncatedDescriptionLimit = SystemProperties.getInteger("historyWidget.descriptionLimit", 100); + if (truncatedDescriptionLimit < 0) { // disabled + return description; + } + if (truncatedDescriptionLimit == 0) { // Someone wants to suppress descriptions, why not? + return ""; + } + + if (description == null || description.length() < truncatedDescriptionLimit) { + return description; + } + + final String ending = "..."; + final int sz = description.length(), maxTruncLength = truncatedDescriptionLimit - ending.length(); + + boolean inTag = false; + int displayChars = 0; + int lastTruncatablePoint = -1; + + for (int i = 0; i < sz; i++) { + char ch = description.charAt(i); + if (ch == '<') { + inTag = true; + } else if (ch == '>') { + inTag = false; + if (displayChars <= maxTruncLength) { + lastTruncatablePoint = i + 1; + } + } + if (!inTag) { + displayChars++; + if (displayChars <= maxTruncLength && ch == ' ') { + lastTruncatablePoint = i; + } + } + } + + String truncDesc = description; + + // Could not find a preferred truncatable index, force a trunc at maxTruncLength + if (lastTruncatablePoint == -1) + lastTruncatablePoint = maxTruncLength; + + if (displayChars >= truncatedDescriptionLimit) { + truncDesc = truncDesc.substring(0, lastTruncatablePoint) + ending; + } + + return truncDesc; + + } + +} diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 6b3500e6b66a..3c756bbb7ba0 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -5372,8 +5372,9 @@ public View getStaplerFallback() { * job that the user is configuring though to prevent a validation warning * if the user sets the displayName to what it currently is. */ - boolean isDisplayNameUnique(String displayName, String currentJobName) { - Collection itemCollection = items.values(); + boolean isDisplayNameUnique(ItemGroup itemGroup, String displayName, String currentJobName) { + + Collection itemCollection = (Collection) itemGroup.getItems(t -> t instanceof TopLevelItem); // if there are a lot of projects, we'll have to store their // display names in a HashSet or something for a quick check @@ -5397,8 +5398,8 @@ else if (displayName.equals(item.getDisplayName())) { * @param name The name to test * @param currentJobName The name of the job that the user is configuring */ - boolean isNameUnique(String name, String currentJobName) { - Item item = getItem(name); + boolean isNameUnique(ItemGroup itemGroup, String name, String currentJobName) { + Item item = itemGroup.getItem(name); if (null == item) { // the candidate name didn't return any items so the name is unique @@ -5420,17 +5421,45 @@ else if (item.getName().equals(currentJobName)) { * existing display names or project names * @param displayName The display name to test * @param jobName The name of the job the user is configuring + * + * @deprecated use {@link TopLevelItemDescriptor#doCheckDisplayNameOrNull(TopLevelItem, String)} */ + @Deprecated public FormValidation doCheckDisplayName(@QueryParameter String displayName, @QueryParameter String jobName) { displayName = displayName.trim(); LOGGER.fine(() -> "Current job name is " + jobName); - if (!isNameUnique(displayName, jobName)) { + if (!isNameUnique(this, displayName, jobName)) { + return FormValidation.warning(Messages.Jenkins_CheckDisplayName_NameNotUniqueWarning(displayName)); + } + else if (!isDisplayNameUnique(this, displayName, jobName)) { + return FormValidation.warning(Messages.Jenkins_CheckDisplayName_DisplayNameNotUniqueWarning(displayName)); + } + else { + return FormValidation.ok(); + } + } + + /** + * Checks to see if the candidate displayName collides with any + * existing display names or project names in the items parent group + * @param displayName The display name to test + * @param item The item to check for duplicates + */ + @Restricted(NoExternalUse.class) + public FormValidation checkDisplayName(String displayName, + TopLevelItem item) { + displayName = displayName.trim(); + String jobName = item.getName(); + + LOGGER.fine(() -> "Current job name is " + jobName); + + if (!isNameUnique(item.getParent(), displayName, jobName)) { return FormValidation.warning(Messages.Jenkins_CheckDisplayName_NameNotUniqueWarning(displayName)); } - else if (!isDisplayNameUnique(displayName, jobName)) { + else if (!isDisplayNameUnique(item.getParent(), displayName, jobName)) { return FormValidation.warning(Messages.Jenkins_CheckDisplayName_DisplayNameNotUniqueWarning(displayName)); } else { diff --git a/core/src/main/java/jenkins/widgets/HistoryPageEntry.java b/core/src/main/java/jenkins/widgets/HistoryPageEntry.java index 98f0a7aac58e..2770fa928b02 100644 --- a/core/src/main/java/jenkins/widgets/HistoryPageEntry.java +++ b/core/src/main/java/jenkins/widgets/HistoryPageEntry.java @@ -26,16 +26,16 @@ import edu.umd.cs.findbugs.annotations.NonNull; import hudson.model.Run; +import jenkins.model.HistoricalBuild; import jenkins.model.queue.QueueItem; /** * Represents an entry used by the {@link HistoryPageFilter}. * *

- * Wraps {@link QueueItem} and {@link Run} instances from the build queue, normalizing + * Wraps {@link QueueItem} and {@link HistoricalBuild} instances from the build queue, normalizing * access to the info required for pagination. - * - * + * @param typically {@link HistoricalBuild} or {@link QueueItem} * @author tom.fennelly@gmail.com */ public class HistoryPageEntry { @@ -57,7 +57,7 @@ public long getEntryId() { protected static long getEntryId(@NonNull Object entry) { if (entry instanceof QueueItem) { return ((QueueItem) entry).getId(); - } else if (entry instanceof Run run) { + } else if (entry instanceof HistoricalBuild run) { return Long.MIN_VALUE + run.getNumber(); } else if (entry instanceof Number) { // Used for testing purposes because of JENKINS-30899 and JENKINS-30909 diff --git a/core/src/main/java/jenkins/widgets/HistoryPageFilter.java b/core/src/main/java/jenkins/widgets/HistoryPageFilter.java index 6b6d9fa214cb..f51c27c22122 100644 --- a/core/src/main/java/jenkins/widgets/HistoryPageFilter.java +++ b/core/src/main/java/jenkins/widgets/HistoryPageFilter.java @@ -30,9 +30,6 @@ import hudson.model.AbstractBuild; import hudson.model.Job; import hudson.model.ParameterValue; -import hudson.model.ParametersAction; -import hudson.model.Queue; -import hudson.model.Run; import hudson.search.UserSearchProperty; import hudson.util.Iterators; import hudson.widgets.HistoryWidget; @@ -43,11 +40,13 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; +import jenkins.model.HistoricalBuild; import jenkins.model.queue.QueueItem; /** * History page filter. - * + * @param typically {@link HistoricalBuild} * @author tom.fennelly@gmail.com */ public class HistoryPageFilter { @@ -57,10 +56,10 @@ public class HistoryPageFilter { private Long olderThan; private String searchString; - // Need to use different Lists for QueueItem and Runs because + // Need to use different Lists for QueueItem and HistoricalBuilds because // we need access to them separately in the jelly files for rendering. public final List> queueItems = new ArrayList<>(); - public final List> runs = new ArrayList<>(); + public final List> runs = new ArrayList<>(); @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD", justification = "read by Stapler") public boolean hasUpPage = false; // there are newer builds than on this page @@ -242,13 +241,9 @@ public int compare(Object o1, Object o2) { } private long getNextBuildNumber(@NonNull Object entry) { - if (entry instanceof QueueItem) { - Queue.Task task = ((QueueItem) entry).getTask(); - if (task instanceof Job) { - return ((Job) task).getNextBuildNumber(); - } - } else if (entry instanceof Run) { - return ((Run) entry).getParent().getNextBuildNumber(); + // TODO refactor into method on HistoryWidget + if (widget != null && widget.owner instanceof Job job) { + return job.getNextBuildNumber(); } // TODO maybe this should be an error? @@ -261,12 +256,13 @@ private void addQueueItem(QueueItem item) { updateNewestOldest(entry.getEntryId()); } - private void addRun(Run run) { - HistoryPageEntry entry = new HistoryPageEntry<>(run); + private void addRun(HistoricalBuild run) { + HistoryPageEntry entry = new HistoryPageEntry<>(run); // Assert that runs have been added in descending order if (!runs.isEmpty()) { if (entry.getEntryId() > runs.get(runs.size() - 1).getEntryId()) { - throw new IllegalStateException("Runs were out of order"); + throw new IllegalStateException("Cannot add newer " + run + " to descending-order list " + + runs.stream().map(HistoryPageEntry::getEntry).collect(Collectors.toList())); } } runs.add(entry); @@ -288,8 +284,7 @@ private boolean add(Object entry) { } addQueueItem(item); return true; - } else if (entry instanceof Run) { - Run run = (Run) entry; + } else if (entry instanceof HistoricalBuild run) { if (searchString != null && !fitsSearchParams(run)) { return false; } @@ -322,7 +317,7 @@ private boolean fitsSearchParams(@NonNull QueueItem item) { return false; } - private boolean fitsSearchParams(@NonNull Run run) { + private boolean fitsSearchParams(@NonNull HistoricalBuild run) { if (searchString == null) { return true; } @@ -340,8 +335,8 @@ private boolean fitsSearchParams(@NonNull Run run) { } else if (run instanceof AbstractBuild && fitsSearchBuildVariables((AbstractBuild) run)) { return true; } else { - ParametersAction parametersAction = run.getAction(ParametersAction.class); - if (parametersAction != null && fitsSearchBuildParameters(parametersAction)) { + List parameters = run.getParameterValues(); + if (fitsSearchBuildParameters(parameters)) { return true; } } @@ -381,8 +376,7 @@ private boolean fitsSearchBuildVariables(AbstractBuild runAsBuild) { return false; } - private boolean fitsSearchBuildParameters(ParametersAction parametersAction) { - List parameters = parametersAction.getParameters(); + private boolean fitsSearchBuildParameters(List parameters) { for (ParameterValue parameter : parameters) { if (!parameter.isSensitive() && fitsSearchString(parameter.getValue())) { return true; diff --git a/core/src/main/resources/hudson/model/UpdateCenter/ConnectionCheckJob/row_es.properties b/core/src/main/resources/hudson/model/UpdateCenter/ConnectionCheckJob/row_es.properties index a8b6a75ee4aa..62fbb51ea4a5 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/ConnectionCheckJob/row_es.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/ConnectionCheckJob/row_es.properties @@ -20,4 +20,4 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -Preparation=Preaparación +Preparation=Preparación diff --git a/core/src/main/resources/hudson/widgets/BuildHistoryWidget/entries.jelly b/core/src/main/resources/hudson/widgets/BuildHistoryWidget/entries.jelly deleted file mode 100644 index 54309319f0a3..000000000000 --- a/core/src/main/resources/hudson/widgets/BuildHistoryWidget/entries.jelly +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - diff --git a/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly b/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly index dc367464b57c..60c3bdec4aba 100644 --- a/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly +++ b/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly @@ -23,7 +23,7 @@ THE SOFTWARE. --> diff --git a/core/src/main/resources/lib/hudson/buildProgressBar.jelly b/core/src/main/resources/lib/hudson/buildProgressBar.jelly index 55d2a50c825b..49aac35f0909 100644 --- a/core/src/main/resources/lib/hudson/buildProgressBar.jelly +++ b/core/src/main/resources/lib/hudson/buildProgressBar.jelly @@ -26,8 +26,8 @@ THE SOFTWARE. Progress bar for a build in progress. - - Build in progress. Must have a url property. + + Build in progress. Must have a url property; may have an executor property. Executor that's carrying out the build. If null, defaults to "build.executor" @@ -37,9 +37,17 @@ THE SOFTWARE. - + + + + + + + + + diff --git a/pom.xml b/pom.xml index 86e2ff541a3c..9f52b05a6ff9 100644 --- a/pom.xml +++ b/pom.xml @@ -73,9 +73,9 @@ THE SOFTWARE. - 2.476 + 2.477 -SNAPSHOT - 2024-09-04T12:22:06Z + 2024-09-10T14:09:56Z github @@ -139,7 +139,6 @@ THE SOFTWARE. maven-compiler-plugin - true alwaysNew io.jenkins.plugins font-awesome-api - 6.6.0-1 + 6.6.0-2 io.jenkins.plugins @@ -218,7 +218,7 @@ THE SOFTWARE. org.jenkins-ci.plugins cloudbees-folder - 6.942.vb_43318a_156b_2 + 6.951.v5f91d88d76b_b_ test @@ -230,7 +230,7 @@ THE SOFTWARE. org.jenkins-ci.plugins junit - 1296.vb_f538b_c88630 + 1300.v03d9d8a_cf1fb_ test @@ -408,14 +408,6 @@ THE SOFTWARE. true - - org.apache.maven.plugins - maven-compiler-plugin - - - true - - diff --git a/test/src/test/java/jenkins/console/ConsoleUrlProviderTest.java b/test/src/test/java/jenkins/console/ConsoleUrlProviderTest.java index b9881fbeddac..9a97ff0748df 100644 --- a/test/src/test/java/jenkins/console/ConsoleUrlProviderTest.java +++ b/test/src/test/java/jenkins/console/ConsoleUrlProviderTest.java @@ -58,9 +58,9 @@ public void getRedirectUrl() throws Exception { // Custom URL without leading slash b.setDescription("custom my/build/console"); assertCustomConsoleUrl(r.contextPath + "/my/build/console", b); - // Custom URL with leading slash + // Custom URL with leading slash -> not supported, falls back to default b.setDescription("custom /my/build/console"); - assertCustomConsoleUrl(r.contextPath + "/my/build/console", b); + assertCustomConsoleUrl(r.contextPath + '/' + b.getUrl() + "console", b); // Default URL is used when extensions throw exceptions. b.setDescription("NullPointerException"); assertCustomConsoleUrl(r.contextPath + '/' + b.getUrl() + "console", b); diff --git a/test/src/test/java/jenkins/model/JenkinsTest.java b/test/src/test/java/jenkins/model/JenkinsTest.java index a8d846c5d79b..c44b12cdd037 100644 --- a/test/src/test/java/jenkins/model/JenkinsTest.java +++ b/test/src/test/java/jenkins/model/JenkinsTest.java @@ -203,8 +203,8 @@ public void testIsDisplayNameUniqueTrue() throws Exception { p.setDisplayName("displayName"); Jenkins jenkins = Jenkins.get(); - assertTrue(jenkins.isDisplayNameUnique("displayName1", curJobName)); - assertTrue(jenkins.isDisplayNameUnique(jobName, curJobName)); + assertTrue(jenkins.isDisplayNameUnique(jenkins, "displayName1", curJobName)); + assertTrue(jenkins.isDisplayNameUnique(jenkins, jobName, curJobName)); } @Test @@ -220,7 +220,7 @@ public void testIsDisplayNameUniqueFalse() throws Exception { p.setDisplayName(displayName); Jenkins jenkins = Jenkins.get(); - assertFalse(jenkins.isDisplayNameUnique(displayName, curJobName)); + assertFalse(jenkins.isDisplayNameUnique(jenkins, displayName, curJobName)); } @Test @@ -233,7 +233,7 @@ public void testIsDisplayNameUniqueSameAsCurrentJob() throws Exception { Jenkins jenkins = Jenkins.get(); // should be true as we don't test against the current job - assertTrue(jenkins.isDisplayNameUnique(displayName, curJobName)); + assertTrue(jenkins.isDisplayNameUnique(jenkins, displayName, curJobName)); } @Test @@ -244,7 +244,7 @@ public void testIsNameUniqueTrue() throws Exception { j.createFreeStyleProject(jobName); Jenkins jenkins = Jenkins.get(); - assertTrue(jenkins.isNameUnique("jobName1", curJobName)); + assertTrue(jenkins.isNameUnique(jenkins, "jobName1", curJobName)); } @Test @@ -255,7 +255,7 @@ public void testIsNameUniqueFalse() throws Exception { j.createFreeStyleProject(jobName); Jenkins jenkins = Jenkins.get(); - assertFalse(jenkins.isNameUnique(jobName, curJobName)); + assertFalse(jenkins.isNameUnique(jenkins, jobName, curJobName)); } @Test @@ -267,7 +267,7 @@ public void testIsNameUniqueSameAsCurrentJob() throws Exception { Jenkins jenkins = Jenkins.get(); // true because we don't test against the current job - assertTrue(jenkins.isNameUnique(curJobName, curJobName)); + assertTrue(jenkins.isNameUnique(jenkins, curJobName, curJobName)); } @Test @@ -281,7 +281,7 @@ public void testDoCheckDisplayNameUnique() throws Exception { p.setDisplayName("displayName"); Jenkins jenkins = Jenkins.get(); - FormValidation v = jenkins.doCheckDisplayName("1displayName", curJobName); + FormValidation v = jenkins.checkDisplayName("1displayName", curProject); assertEquals(FormValidation.ok(), v); } @@ -297,7 +297,7 @@ public void testDoCheckDisplayNameSameAsDisplayName() throws Exception { p.setDisplayName(displayName); Jenkins jenkins = Jenkins.get(); - FormValidation v = jenkins.doCheckDisplayName(displayName, curJobName); + FormValidation v = jenkins.checkDisplayName(displayName, curProject); assertEquals(FormValidation.Kind.WARNING, v.kind); } @@ -313,7 +313,7 @@ public void testDoCheckDisplayNameSameAsJobName() throws Exception { p.setDisplayName(displayName); Jenkins jenkins = Jenkins.get(); - FormValidation v = jenkins.doCheckDisplayName(jobName, curJobName); + FormValidation v = jenkins.checkDisplayName(jobName, curProject); assertEquals(FormValidation.Kind.WARNING, v.kind); } diff --git a/war/package.json b/war/package.json index 1adda658dd6b..e6b18f5a2a37 100644 --- a/war/package.json +++ b/war/package.json @@ -26,12 +26,12 @@ "@babel/cli": "7.25.6", "@babel/core": "7.25.2", "@babel/preset-env": "7.25.4", - "@eslint/js": "9.9.1", + "@eslint/js": "9.10.0", "babel-loader": "9.1.3", "clean-webpack-plugin": "4.0.0", "css-loader": "7.1.2", "css-minimizer-webpack-plugin": "7.0.0", - "eslint": "9.9.1", + "eslint": "9.10.0", "eslint-config-prettier": "9.1.0", "eslint-formatter-checkstyle": "8.40.0", "globals": "15.9.0", diff --git a/war/pom.xml b/war/pom.xml index 937b8e72a5e5..47b268761f3a 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -311,7 +311,7 @@ THE SOFTWARE. org.jenkins-ci.plugins junit - 1296.vb_f538b_c88630 + 1300.v03d9d8a_cf1fb_ hpi @@ -400,7 +400,7 @@ THE SOFTWARE. io.jenkins.plugins font-awesome-api - 6.6.0-1 + 6.6.0-2 hpi diff --git a/war/yarn.lock b/war/yarn.lock index f12c358b3dac..eb19ca5a25e3 100644 --- a/war/yarn.lock +++ b/war/yarn.lock @@ -1878,10 +1878,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:9.9.1": - version: 9.9.1 - resolution: "@eslint/js@npm:9.9.1" - checksum: 10c0/a3a91de2ce78469f7c4eee78c1eba77360706e1d0fa0ace2e19102079bcf237b851217c85ea501dc92c4c3719d60d9df966977abc8554d4c38e3638c1f53dcb2 +"@eslint/js@npm:9.10.0": + version: 9.10.0 + resolution: "@eslint/js@npm:9.10.0" + checksum: 10c0/2ac45a002dc1ccf25be46ea61001ada8d77248d1313ab4e53f3735e5ae00738a757874e41f62ad6fbd49df7dffeece66e5f53ff0d7b78a99ce4c68e8fea66753 languageName: node linkType: hard @@ -1892,6 +1892,15 @@ __metadata: languageName: node linkType: hard +"@eslint/plugin-kit@npm:^0.1.0": + version: 0.1.0 + resolution: "@eslint/plugin-kit@npm:0.1.0" + dependencies: + levn: "npm:^0.4.1" + checksum: 10c0/fae97cd4efc1c32501c286abba1b5409848ce8c989e1ca6a5bb057a304a2cd721e6e957f6bc35ce95cfd0871e822ed42df3c759fecdad72c30e70802e26f83c7 + languageName: node + linkType: hard + "@humanwhocodes/module-importer@npm:^1.0.1": version: 1.0.1 resolution: "@humanwhocodes/module-importer@npm:1.0.1" @@ -3516,15 +3525,16 @@ __metadata: languageName: node linkType: hard -"eslint@npm:9.9.1": - version: 9.9.1 - resolution: "eslint@npm:9.9.1" +"eslint@npm:9.10.0": + version: 9.10.0 + resolution: "eslint@npm:9.10.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.2.0" "@eslint-community/regexpp": "npm:^4.11.0" "@eslint/config-array": "npm:^0.18.0" "@eslint/eslintrc": "npm:^3.1.0" - "@eslint/js": "npm:9.9.1" + "@eslint/js": "npm:9.10.0" + "@eslint/plugin-kit": "npm:^0.1.0" "@humanwhocodes/module-importer": "npm:^1.0.1" "@humanwhocodes/retry": "npm:^0.3.0" "@nodelib/fs.walk": "npm:^1.2.8" @@ -3547,7 +3557,6 @@ __metadata: is-glob: "npm:^4.0.0" is-path-inside: "npm:^3.0.3" json-stable-stringify-without-jsonify: "npm:^1.0.1" - levn: "npm:^0.4.1" lodash.merge: "npm:^4.6.2" minimatch: "npm:^3.1.2" natural-compare: "npm:^1.4.0" @@ -3561,7 +3570,7 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10c0/5e71efda7c0a14ee95436d5cdfed04ee61dfb1d89d7a32b50a424de2e680af82849628ea6581950c2e0726491f786a3cfd0032ce013c1c5093786e475cfdfb33 + checksum: 10c0/7357f3995b15043eea83c8c0ab16c385ce3f28925c1b11cfcd6b2ede8faab3d91ede84a68173dd5f6e3e176e177984e6218de58b7b8388e53e2881f1ec07c836 languageName: node linkType: hard @@ -4378,12 +4387,12 @@ __metadata: "@babel/cli": "npm:7.25.6" "@babel/core": "npm:7.25.2" "@babel/preset-env": "npm:7.25.4" - "@eslint/js": "npm:9.9.1" + "@eslint/js": "npm:9.10.0" babel-loader: "npm:9.1.3" clean-webpack-plugin: "npm:4.0.0" css-loader: "npm:7.1.2" css-minimizer-webpack-plugin: "npm:7.0.0" - eslint: "npm:9.9.1" + eslint: "npm:9.10.0" eslint-config-prettier: "npm:9.1.0" eslint-formatter-checkstyle: "npm:8.40.0" globals: "npm:15.9.0"