Merge branch 'master' into migrate_to_junit5_core_2

This commit is contained in:
Mark Waite 2025-05-15 20:46:09 -06:00 committed by GitHub
commit ea1e0b9568
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 1167 additions and 199 deletions

View File

@ -41,7 +41,7 @@ THE SOFTWARE.
<commons-fileupload2.version>2.0.0-M2</commons-fileupload2.version>
<groovy.version>2.4.21</groovy.version>
<jelly.version>1.1-jenkins-20250108</jelly.version>
<stapler.version>1979.v5048d87384d7</stapler.version>
<stapler.version>1983.v93c53e94b_c04</stapler.version>
</properties>
<dependencyManagement>

View File

@ -135,6 +135,7 @@ import net.sf.json.JSONObject;
import org.apache.commons.io.IOUtils;
import org.apache.commons.jelly.XMLOutput;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.QueryParameter;
@ -1073,6 +1074,11 @@ public abstract class Run<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
}
}
@Restricted(DoNotUse.class) // Jelly
public final boolean hasCustomArtifactManager() {
return artifactManager != null;
}
/**
* Gets the directory where the artifacts are archived.
* @deprecated Should only be used from {@link StandardArtifactManager} or subclasses.

View File

@ -43,6 +43,7 @@ import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.SortedMap;
import java.util.function.IntPredicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.lazy.AbstractLazyLoadRunMap;
@ -240,6 +241,7 @@ public final class RunMap<R extends Run<?, R>> extends AbstractLazyLoadRunMap<R>
return r.createReference();
}
@Restricted(NoExternalUse.class)
@Override
protected boolean allowLoad(int buildNumber) {
if (job == null) {
@ -256,6 +258,27 @@ public final class RunMap<R extends Run<?, R>> extends AbstractLazyLoadRunMap<R>
return true;
}
@Restricted(NoExternalUse.class)
@Override
protected IntPredicate createLoadAllower() {
if (job == null) {
LOGGER.fine(() -> "deprecated constructor without Job used on " + dir);
return buildNumber -> true;
}
@SuppressWarnings("unchecked")
var allowers = RunListener.all().stream().map(l -> l.createLoadAllower(job)).toList();
return buildNumber -> {
for (var allower : allowers) {
if (!allower.test(buildNumber)) {
LOGGER.finer(() -> allower + " declined to load " + buildNumber + " in " + job);
return false;
}
}
LOGGER.finest(() -> "no RunListener declined to load " + buildNumber + " in " + job + " so proceeding");
return true;
};
}
@Override
protected R retrieve(File d) throws IOException {
if (new File(d, "build.xml").exists()) {

View File

@ -47,6 +47,7 @@ import java.io.File;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.function.IntPredicate;
import jenkins.model.lazy.AbstractLazyLoadRunMap;
import jenkins.util.Listeners;
import org.jvnet.tiger_types.Types;
@ -187,6 +188,16 @@ public abstract class RunListener<R extends Run> implements ExtensionPoint {
return true;
}
/**
* Allows listeners to veto build loading during startup.
* Same behavior as {@link #allowLoad} but permits an implementation to more
* efficiently respond to numerous queries about historical build numbers.
*/
@Restricted(Beta.class)
public IntPredicate createLoadAllower(@NonNull Job<?, ?> job) {
return buildNumber -> allowLoad(job, buildNumber);
}
/**
* Registers this object as an active listener so that it can start getting
* callbacks invoked.

View File

@ -24,6 +24,7 @@
package hudson.security;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.model.User;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
@ -181,6 +182,7 @@ public class BasicAuthenticationFilter implements CompatibleFilter {
d.include(req, rsp);
}
@SuppressFBWarnings(value = "UNVALIDATED_REDIRECT", justification = "Authenticated by the container")
private void prepareRedirect(HttpServletResponse rsp, String path) {
rsp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
rsp.setHeader("Location", path);

View File

@ -939,6 +939,9 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
return super.encode(rawPassword);
} catch (IllegalArgumentException ex) {
if (ex.getMessage().equals("password cannot be more than 72 bytes")) {
if (rawPassword.toString().matches("\\A\\p{ASCII}+\\z")) {
throw new IllegalArgumentException(Messages.HudsonPrivateSecurityRealm_CreateAccount_BCrypt_PasswordTooLong_ASCII());
}
throw new IllegalArgumentException(Messages.HudsonPrivateSecurityRealm_CreateAccount_BCrypt_PasswordTooLong());
}
throw ex;

View File

@ -25,6 +25,7 @@
package hudson.security;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.DescriptorExtensionList;
import hudson.Extension;
import hudson.ExtensionPoint;
@ -382,6 +383,7 @@ public abstract class SecurityRealm implements Describable<SecurityRealm>, Exten
rsp.sendRedirect2(getPostLogOutUrl2(req, auth));
}
@SuppressFBWarnings(value = "INSECURE_COOKIE", justification = "TODO needs triage")
private void resetRememberMeCookie(StaplerRequest2 req, StaplerResponse2 rsp, String contextPath) {
Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, "");
cookie.setMaxAge(0);

View File

@ -72,8 +72,12 @@ public class RenderOnDemandClosure {
assert !bodyStack.isEmpty(); // there must be at least one, which is the direct child of <l:renderOnDemand>
Map<String, Object> variables = new HashMap<>();
for (String v : Util.fixNull(attributesToCapture).split(","))
variables.put(v.intern(), context.getVariable(v));
String _attributesToCapture = Util.fixEmpty(attributesToCapture);
if (_attributesToCapture != null) {
for (String v : _attributesToCapture.split(",")) {
variables.put(v.intern(), context.getVariable(v));
}
}
// capture the current base of context for descriptors
currentDescriptorByNameUrl = Descriptor.getCurrentDescriptorByNameUrl();
@ -86,6 +90,7 @@ public class RenderOnDemandClosure {
for (String adjunct : _adjuncts) {
this.adjuncts[i++] = adjunct.intern();
}
LOGGER.fine(() -> "captured " + variables);
}
/**
@ -96,6 +101,7 @@ public class RenderOnDemandClosure {
return new HttpResponse() {
@Override
public void generateResponse(StaplerRequest2 req, StaplerResponse2 rsp, Object node) throws IOException, ServletException {
LOGGER.fine(() -> "rendering " + req.getPathInfo());
req.getWebApp().getDispatchValidator().allowDispatch(req, rsp);
try {
new DefaultScriptInvoker() {

View File

@ -761,6 +761,7 @@ public class SetupWizard extends PageDecorator {
}
@Override
@SuppressFBWarnings(value = "UNVALIDATED_REDIRECT", justification = "TODO needs triage")
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// Force root requests to the setup wizard
if (request instanceof HttpServletRequest && !Jenkins.get().getInstallState().isSetupComplete()) {

View File

@ -4964,6 +4964,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
/**
* Changes the icon size by changing the cookie
*/
@SuppressFBWarnings(value = "INSECURE_COOKIE", justification = "TODO needs triage")
public void doIconSize(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException, ServletException {
String qs = req.getQueryString();
if (qs == null)

View File

@ -50,6 +50,7 @@ import java.util.Spliterator;
import java.util.Spliterators;
import java.util.TreeMap;
import java.util.function.IntConsumer;
import java.util.function.IntPredicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
@ -343,6 +344,7 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R>
kids = MemoryReductionUtil.EMPTY_STRING_ARRAY;
}
SortedIntList list = new SortedIntList(kids.length / 2);
var allower = createLoadAllower();
for (String s : kids) {
if (!BUILD_NUMBER.matcher(s).matches()) {
// not a build directory
@ -350,7 +352,7 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R>
}
try {
int buildNumber = Integer.parseInt(s);
if (allowLoad(buildNumber)) {
if (allower.test(buildNumber)) {
list.add(buildNumber);
} else {
LOGGER.fine(() -> "declining to consider " + buildNumber + " in " + dir);
@ -368,6 +370,11 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R>
return true;
}
@Restricted(NoExternalUse.class)
protected IntPredicate createLoadAllower() {
return this::allowLoad;
}
/**
* Permits a previous blocked build number to be eligible for loading.
* @param buildNumber a build number

View File

@ -1,2 +0,0 @@
new hudson.util.IOException2($t) :: $t instanceof Throwable => new java.io.IOException($t);;
new hudson.util.IOException2($s, $t) :: $s instanceof String && $t instanceof Throwable => new java.io.IOException($s, $t);;

View File

@ -38,9 +38,7 @@ THE SOFTWARE.
</t:summary>
</j:forEach>
<t:artifactList caption="${%Last Successful Artifacts}"
build="${it.lastSuccessfulBuild}" baseURL="lastSuccessfulBuild/"
permission="${it.lastSuccessfulBuild.ARTIFACTS}"/>
<t:artifactList build="${it.lastSuccessfulBuild}" caption="${%Last Successful Artifacts}"/>
</table>
<!-- merge fragments from the actions -->

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
The MIT License
Copyright 2025 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.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout" xmlns:t="/lib/hudson">
<l:ajax>
<j:if test="${!h.artifactsPermissionEnabled or h.artifactsPermissionEnabled and it.hasPermission(it.ARTIFACTS)}">
<j:set var="artifacts" value="${it.getArtifactsUpTo(it.LIST_CUTOFF+1)}" />
<j:if test="${!empty(artifacts)}">
<t:summary icon="symbol-cube" href="${rootURL}/${it.url}artifact/">
<a href="${rootURL}/${it.url}artifact/">${caption != null ? caption : request2.getParameter('caption')}</a>
<j:if test="${it.building}">
<p>
${%building}
</p>
</j:if>
<j:if test="${size(artifacts) le it.LIST_CUTOFF}">
<!-- if not too many, just list them -->
<table class="fileList">
<j:forEach var="f" items="${artifacts}">
<tr>
<td>
<l:icon class="icon-document icon-sm"/>
</td>
<td>
<a href="${rootURL}/${it.url}artifact/${f.href}">${f.displayPath}</a>
</td>
<td class="fileSize">
${h.humanReadableByteSize(f.getFileSize())}
</td>
<td>
<j:if test="${app.fingerprintMap.ready}">
<a href="${rootURL}/${it.url}artifact/${f.href}/*fingerprint*/">
<l:icon class="icon-fingerprint icon-sm"/>
</a>
<st:nbsp/>
</j:if>
<a href="${rootURL}/${it.url}artifact/${f.href}/*view*/">${%view}</a>
</td>
</tr>
</j:forEach>
</table>
</j:if>
<!-- otherwise just show link to directory browser -->
</t:summary>
</j:if>
</j:if>
</l:ajax>
</j:jelly>

View File

@ -67,8 +67,7 @@ THE SOFTWARE.
<table>
<t:artifactList build="${it}" caption="${%Build Artifacts}"
permission="${it.ARTIFACTS}" />
<t:artifactList build="${it}" caption="${%Build Artifacts}"/>
<!-- give actions a chance to contribute summary item -->
<j:forEach var="a" items="${it.allActions}">

View File

@ -72,7 +72,7 @@ THE SOFTWARE.
<l:card title="Summary">
<div>
<table>
<t:artifactList build="${it}" caption="${%Build Artifacts}" permission="${it.ARTIFACTS}" />
<t:artifactList build="${it}" caption="${%Build Artifacts}"/>
<!-- give actions a chance to contribute summary item -->
<j:forEach var="a" items="${it.allActions}">

View File

@ -37,7 +37,8 @@ HudsonPrivateSecurityRealm.ManageUserLinks.Description=Create/delete/modify user
HudsonPrivateSecurityRealm.CreateAccount.TextNotMatchWordInImage=Text didn''t match the word shown in the image
HudsonPrivateSecurityRealm.CreateAccount.PasswordNotMatch=Password didn''t match
HudsonPrivateSecurityRealm.CreateAccount.FIPS.PasswordLengthInvalid=Password must be at least 14 characters long
HudsonPrivateSecurityRealm.CreateAccount.BCrypt.PasswordTooLong=Jenkins own user database currently only supports passwords of up to 72 bytes UTF-8 (72 basic ASCII characters, 24-36 CJK characters, or 18 emoji). Please use a shorter password.
HudsonPrivateSecurityRealm.CreateAccount.BCrypt.PasswordTooLong.ASCII=Password cannot be longer than 72 characters.
HudsonPrivateSecurityRealm.CreateAccount.BCrypt.PasswordTooLong=Password cannot be longer than 72 characters (a-z, A-Z, 0-9, and basic punctuation; fewer when using other characters, like Chinese characters or emoji).
HudsonPrivateSecurityRealm.CreateAccount.PasswordRequired=Password is required
HudsonPrivateSecurityRealm.CreateAccount.UserNameRequired=User name is required
HudsonPrivateSecurityRealm.CreateAccount.UserNameInvalidCharacters=User name must only contain alphanumeric characters, underscore and dash

View File

@ -28,7 +28,7 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<l:layout permission="${app.ADMINISTER}" title="${%threadDump}" type="one-column">
<f:breadcrumb-config-outline title="${%threadDump}" />
<l:breadcrumb title="${%threadDump}" />
<l:main-panel>
<h1>${%threadDumpTitle}</h1>
<j:set var="tdumps" value="${it.getAllThreadDumps()}"/>

View File

@ -23,60 +23,28 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
<st:documentation>
Generates a listing of the build artifacts.
Depending on the size of the artifact, this will either produce a list or a link to the directory view.
<st:attribute name="build" type="hudson.model.Build" use="required">
<st:attribute name="build" type="hudson.model.Run" use="required">
Build object for which the artifacts are displayed
</st:attribute>
<st:attribute name="caption" use="required">
Human readable title text
</st:attribute>
<st:attribute name="baseURL">
If the hyperlink to artifacts are at another URL, specify the prefix.
</st:attribute>
</st:documentation>
<j:if test="${!h.isArtifactsPermissionEnabled() or h.isArtifactsPermissionEnabled() and h.hasPermission(it,attrs.permission)}">
<j:set var="artifacts" value="${build.getArtifactsUpTo(build.LIST_CUTOFF+1)}" />
<j:if test="${!empty(artifacts)}">
<t:summary icon="symbol-cube" href="${baseURL}artifact/">
<a href="${baseURL}artifact/">${caption}</a>
<j:if test="${build.building}">
<p>
${%building}
</p>
</j:if>
<j:if test="${size(artifacts) le build.LIST_CUTOFF}">
<!-- if not too many, just list them -->
<table class="fileList">
<j:forEach var="f" items="${artifacts}">
<tr>
<td>
<l:icon class="icon-document icon-sm"/>
</td>
<td>
<a href="${baseURL}artifact/${f.href}">${f.displayPath}</a>
</td>
<td class="fileSize">
${h.humanReadableByteSize(f.getFileSize())}
</td>
<td>
<j:if test="${app.fingerprintMap.ready}">
<a href="${baseURL}artifact/${f.href}/*fingerprint*/">
<l:icon class="icon-fingerprint icon-sm"/>
</a>
<st:nbsp/>
</j:if>
<a href="${baseURL}artifact/${f.href}/*view*/">${%view}</a>
</td>
</tr>
</j:forEach>
</table>
</j:if>
<!-- otherwise just show link to directory browser -->
</t:summary>
</j:if>
</j:if>
<j:choose>
<j:when test="${build == null}">
<!-- nothing to render -->
</j:when>
<j:when test="${build.hasCustomArtifactManager()}">
<tbody class="artifact-list" data-url="${rootURL}/${build.url}artifactList" data-caption="${caption}"/>
<st:adjunct includes="lib.hudson.artifactList"/>
</j:when>
<j:otherwise>
<st:include it="${build}" page="artifactList"/>
</j:otherwise>
</j:choose>
</j:jelly>

View File

@ -1,7 +1,7 @@
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
* Copyright 2025 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
@ -22,25 +22,18 @@
* THE SOFTWARE.
*/
package hudson.util;
import java.io.IOException;
/**
* {@link IOException} with linked exception.
*
* @author Kohsuke Kawaguchi
* @deprecated Just use {@link IOException}, which since Java 6 supports a cause.
*/
@Deprecated
public class IOException2 extends IOException {
public IOException2(Throwable cause) {
super(cause);
}
public IOException2(String s, Throwable cause) {
super(s, cause);
}
}
// TODO package this logic into a generic utility like renderOnDemand/refreshPart with Behaviour.specify
var e = document.querySelector(".artifact-list");
fetch(
e.getAttribute("data-url") +
"?" +
new URLSearchParams({ caption: e.getAttribute("data-caption") }),
).then((rsp) => {
if (rsp.ok) {
rsp.text().then((responseText) => {
e.innerHTML = responseText;
Behaviour.applySubtree(e);
layoutUpdateCallback.call();
});
}
});

View File

@ -37,8 +37,12 @@ THE SOFTWARE.
This is useful for programmatically adding the context menu
</st:attribute>
<st:attribute name="hasMenu">
If true, this breadcrumb item will include a '⌄' symbol to display a dropdown menu with items
from the '{breadcrumb.href}/contextMenu' path. Since 2.361.
If true, this breadcrumb item will display a dropdown menu with items
from the '{breadcrumb.href}/contextMenu' path on hover. Since 2.361.
</st:attribute>
<st:attribute name="hasChildrenMenu">
If true, this breadcrumb item will display a dropdown menu with items
from the '{breadcrumb.href}/childrenContextMenu' path on hover. Since TODO.
</st:attribute>
</st:documentation>
@ -50,7 +54,7 @@ THE SOFTWARE.
<span>${attrs.title}</span>
</j:when>
<j:otherwise>
<a href="${attrs.href}" class="${attrs.hasMenu ? 'hoverable-model-link' : ''}">
<a href="${attrs.href}" class="${attrs.hasMenu ? 'hoverable-model-link' : ''} ${attrs.hasChildrenMenu ? 'hoverable-children-model-link' : ''}">
${attrs.title}
</a>
</j:otherwise>

View File

@ -44,7 +44,8 @@ THE SOFTWARE.
<j:if test="${anc.object != app}">
<l:breadcrumb title="${anc.object.displayName}"
href="${anc.url}/"
hasMenu="${h.isModelWithContextMenu(anc.object)}" />
hasMenu="${h.isModelWithContextMenu(anc.object)}"
hasChildrenMenu="${h.isModelWithChildren(anc.object)}" />
</j:if>
</j:if>
</j:forEach>

View File

@ -157,8 +157,16 @@ public class HudsonPrivateSecurityRealmTest {
}
@Issue("JENKINS-75533")
public void ensureExpectedMessage() {
public void ensureExpectedMessageAscii() {
final IllegalArgumentException ex = Assert.assertThrows(IllegalArgumentException.class, () -> HudsonPrivateSecurityRealm.PASSWORD_HASH_ENCODER.encode("1234567890123456789012345678901234567890123456789012345678901234567890123"));
assertThat(ex.getMessage(), is(Messages.HudsonPrivateSecurityRealm_CreateAccount_BCrypt_PasswordTooLong_ASCII()));
}
@Issue("JENKINS-75533")
public void ensureExpectedMessageEmoji() {
final IllegalArgumentException ex = Assert.assertThrows(IllegalArgumentException.class, () -> HudsonPrivateSecurityRealm.PASSWORD_HASH_ENCODER.encode(
"\uD83E\uDD20\uD83E\uDD20\uD83E\uDD20\uD83E\uDD20\uD83E\uDD20\uD83E\uDD20\uD83E\uDD20\uD83E\uDD20\uD83E\uDD20" +
"\uD83E\uDD20\uD83E\uDD20\uD83E\uDD20\uD83E\uDD20\uD83E\uDD20\uD83E\uDD20\uD83E\uDD20\uD83E\uDD20\uD83E\uDD20\uD83E\uDD20")); // 🤠
assertThat(ex.getMessage(), is(Messages.HudsonPrivateSecurityRealm_CreateAccount_BCrypt_PasswordTooLong()));
}
}

View File

@ -23,18 +23,18 @@
"lint": "yarn lint:js && yarn lint:css"
},
"devDependencies": {
"@babel/cli": "7.27.1",
"@babel/cli": "7.27.2",
"@babel/core": "7.27.1",
"@babel/preset-env": "7.27.1",
"@eslint/js": "9.25.1",
"@babel/preset-env": "7.27.2",
"@eslint/js": "9.26.0",
"babel-loader": "10.0.0",
"clean-webpack-plugin": "4.0.0",
"css-loader": "7.1.2",
"css-minimizer-webpack-plugin": "7.0.2",
"eslint": "9.25.1",
"eslint-config-prettier": "10.1.2",
"eslint": "9.26.0",
"eslint-config-prettier": "10.1.5",
"eslint-formatter-checkstyle": "8.40.0",
"globals": "16.0.0",
"globals": "16.1.0",
"handlebars-loader": "1.7.3",
"mini-css-extract-plugin": "2.9.2",
"postcss": "8.5.3",
@ -42,13 +42,13 @@
"postcss-preset-env": "10.1.6",
"postcss-scss": "4.0.9",
"prettier": "3.5.3",
"sass": "1.87.0",
"sass": "1.88.0",
"sass-loader": "16.0.5",
"style-loader": "4.0.0",
"stylelint": "16.19.1",
"stylelint-checkstyle-reporter": "1.0.0",
"stylelint-config-standard-scss": "14.0.0",
"webpack": "5.99.7",
"webpack": "5.99.8",
"webpack-cli": "6.0.1",
"webpack-remove-empty-scripts": "1.0.4"
},

10
pom.xml
View File

@ -28,7 +28,7 @@ THE SOFTWARE.
<parent>
<groupId>org.jenkins-ci</groupId>
<artifactId>jenkins</artifactId>
<version>1.131</version>
<version>1.132</version>
<relativePath />
</parent>
@ -73,9 +73,9 @@ THE SOFTWARE.
</issueManagement>
<properties>
<revision>2.509</revision>
<revision>2.511</revision>
<changelist>-SNAPSHOT</changelist>
<project.build.outputTimestamp>2025-04-29T13:33:24Z</project.build.outputTimestamp>
<project.build.outputTimestamp>2025-05-13T14:33:55Z</project.build.outputTimestamp>
<!-- configuration for patch tracker plugin -->
<project.patchManagement.system>github</project.patchManagement.system>
@ -87,7 +87,7 @@ THE SOFTWARE.
<changelog.url>https://www.jenkins.io/changelog</changelog.url>
<!-- Bundled Remoting version -->
<remoting.version>3307.v632ed11b_3a_c7</remoting.version>
<remoting.version>3309.v27b_9314fd1a_4</remoting.version>
<spotbugs.effort>Max</spotbugs.effort>
<spotbugs.threshold>Medium</spotbugs.threshold>
@ -97,7 +97,7 @@ THE SOFTWARE.
<bridge-method-injector.version>1.31</bridge-method-injector.version>
<spotless.check.skip>false</spotless.check.skip>
<!-- Make sure to keep the jetty-ee9-maven-plugin version in war/pom.xml in sync with the Jetty release in Winstone: -->
<winstone.version>8.8</winstone.version>
<winstone.version>8.9</winstone.version>
<node.version>22.15.0</node.version>
</properties>

View File

@ -31,7 +31,7 @@ function generateJumplistAccessors() {
*/
function generateDropdowns() {
behaviorShim.specify(
".hoverable-model-link",
".hoverable-model-link, .hoverable-children-model-link",
"-hoverable-dropdown-",
1000,
(element) =>
@ -45,17 +45,65 @@ function generateDropdowns() {
return;
}
fetch(Path.combinePath(href, "contextMenu"))
.then((response) => response.json())
.then((json) =>
instance.setContent(
Utils.generateDropdownItems(
mapChildrenItemsToDropdownItems(json.items),
),
),
)
.catch((error) => console.log(`Jumplist request failed: ${error}`))
.finally(() => (instance.loaded = true));
const hasModelLink = element.classList.contains(
"hoverable-model-link",
);
const hasChildrenLink = element.classList.contains(
"hoverable-children-model-link",
);
const sections = {
model: null,
children: null,
};
const fetchSection = function (urlSuffix) {
return fetch(Path.combinePath(href, urlSuffix))
.then((response) => response.json())
.then((json) => {
const items = mapChildrenItemsToDropdownItems(json.items);
const section = document.createElement("div");
section.appendChild(Utils.generateDropdownItems(items));
return section;
});
};
const promises = [];
if (hasModelLink) {
promises.push(
fetchSection("contextMenu").then((section) => {
sections.model = section;
}),
);
}
if (hasChildrenLink) {
promises.push(
fetchSection("childrenContextMenu").then((section) => {
sections.children = section;
}),
);
}
Promise.all(promises)
.then(() => {
const container = document.createElement("div");
container.className = "jenkins-dropdown__split-container";
if (sections.model) {
container.appendChild(sections.model);
}
if (sections.children) {
container.appendChild(sections.children);
}
instance.setContent(container);
})
.catch((error) => {
console.log(`Dropdown fetch failed: ${error}`);
})
.finally(() => {
instance.loaded = true;
});
},
false,
{
@ -74,6 +122,7 @@ function generateDropdowns() {
(element) =>
Utils.generateDropdown(element, (instance) => {
const href = element.dataset.href;
const jumplistType = !element.classList.contains("children")
? "contextMenu"
: "childrenContextMenu";

View File

@ -288,3 +288,21 @@ $dropdown-padding: 0.375rem;
height: 1.25rem;
}
}
.jenkins-dropdown__split-container {
display: grid;
grid-template-columns: auto auto;
& > * {
min-width: 215px;
&:not(:first-of-type) {
background: color-mix(
in sRGB,
var(--text-color-secondary) 5%,
transparent
);
border-left: var(--jenkins-border--subtle);
}
}
}

View File

@ -80,7 +80,7 @@ THE SOFTWARE.
<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>ionicons-api</artifactId>
<version>87.ve74b_60c10b_57</version>
<version>88.va_4187cb_eddf1</version>
</dependency>
<dependency>
<groupId>io.jenkins.plugins</groupId>
@ -123,7 +123,7 @@ THE SOFTWARE.
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-api</artifactId>
<version>1371.ve334280b_d611</version>
<version>1373.v7b_813f10efa_b_</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
@ -183,7 +183,7 @@ THE SOFTWARE.
<dependency>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>jenkins-test-harness</artifactId>
<version>2443.v1f61c6816c2c</version>
<version>2447.v8a_ce9e0fca_32</version>
<scope>test</scope>
<exclusions>
<exclusion>
@ -235,7 +235,7 @@ THE SOFTWARE.
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>junit</artifactId>
<version>1322.v1556dc1c59a_f</version>
<version>1334.vd3b_b_2094e438</version>
<scope>test</scope>
</dependency>
<dependency>

View File

@ -27,6 +27,7 @@ package hudson.model;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.isA;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@ -38,18 +39,23 @@ import hudson.FilePath;
import hudson.Launcher;
import hudson.XmlFile;
import hudson.model.listeners.SaveableListener;
import hudson.remoting.Callable;
import hudson.tasks.ArtifactArchiver;
import hudson.tasks.BuildTrigger;
import hudson.tasks.Builder;
import hudson.tasks.Fingerprinter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import jenkins.model.ArtifactManager;
import jenkins.model.ArtifactManagerConfiguration;
import jenkins.model.ArtifactManagerFactory;
@ -71,6 +77,8 @@ import org.kohsuke.stapler.DataBoundConstructor;
@Category(SmokeTest.class)
public class RunTest {
private static final Logger LOGGER = Logger.getLogger(RunTest.class.getName());
@Rule public JenkinsRule j = new JenkinsRule();
@Issue("JENKINS-17935")
@ -276,4 +284,104 @@ public class RunTest {
}
}
@Test public void slowArtifactManager() throws Exception {
ArtifactManagerConfiguration.get().getArtifactManagerFactories().add(new SlowMgr.Factory());
var p = j.createFreeStyleProject();
j.jenkins.getWorkspaceFor(p).child("f").write("", null);
p.getPublishersList().add(new ArtifactArchiver("f"));
var b = j.buildAndAssertSuccess(p);
assertThat(b.getArtifactManager(), isA(SlowMgr.class));
var wc = j.createWebClient();
wc.getOptions().setJavaScriptEnabled(false);
wc.getPage(b);
wc.getPage(p);
}
public static final class SlowMgr extends ArtifactManager {
static final AtomicBoolean deleted = new AtomicBoolean();
@Override public boolean delete() {
return !deleted.getAndSet(true);
}
@Override public void onLoad(Run<?, ?> build) {}
@Override public void archive(FilePath workspace, Launcher launcher, BuildListener listener, Map<String, String> artifacts) {
LOGGER.info(() -> "Pretending to archive " + artifacts);
}
@Override public VirtualFile root() {
return new VirtualFile() {
@Override public <V> V run(Callable<V, IOException> callable) throws IOException {
LOGGER.info("Sleeping");
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException x) {
throw new IOException(x);
}
throw new IllegalStateException();
}
@Override public String getName() {
return "";
}
@Override public URI toURI() {
return URI.create("no://where");
}
@Override public VirtualFile getParent() {
return null;
}
@Override public boolean isDirectory() throws IOException {
return true;
}
@Override public boolean isFile() throws IOException {
return false;
}
@Override public boolean exists() throws IOException {
return true;
}
@Override public VirtualFile[] list() throws IOException {
return new VirtualFile[0];
}
@Override public VirtualFile child(String name) {
return null;
}
@Override public long length() throws IOException {
return 0;
}
@Override public long lastModified() throws IOException {
return 0;
}
@Override public boolean canRead() throws IOException {
return true;
}
@Override public InputStream open() throws IOException {
throw new FileNotFoundException();
}
};
}
public static final class Factory extends ArtifactManagerFactory {
@DataBoundConstructor public Factory() {}
@Override public ArtifactManager managerFor(Run<?, ?> build) {
LOGGER.info(() -> "Picking manager for " + build);
return new SlowMgr();
}
@TestExtension("slowArtifactManager") public static final class DescriptorImpl extends ArtifactManagerFactoryDescriptor {}
}
}
}

View File

@ -1,5 +1,6 @@
package hudson.node_monitors;
import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@ -11,6 +12,8 @@ import hudson.model.User;
import hudson.slaves.DumbSlave;
import hudson.slaves.OfflineCause;
import hudson.slaves.SlaveComputer;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.InboundAgentRule;
@ -47,8 +50,15 @@ public class ResponseTimeMonitorTest {
j.disconnectSlave(s);
assertNull(ResponseTimeMonitor.DESCRIPTOR.monitor(c));
// Now reconnect and make sure we get a non-null response.
c.connect(false).get(); // wait until it's connected
// Retry to compensate for test being flaky in CI
await().atMost(5, TimeUnit.SECONDS)
.ignoreException(ExecutionException.class)
.until(() -> {
// Now reconnect and make sure we get a non-null response.
c.connect(false).get(); // wait until it's connected
return true;
}
);
assertNotNull(ResponseTimeMonitor.DESCRIPTOR.monitor(c));
}

View File

@ -30,12 +30,20 @@ import static org.junit.Assert.assertNull;
import hudson.model.InvisibleAction;
import hudson.model.RootAction;
import hudson.widgets.RenderOnDemandClosure;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.logging.Level;
import org.htmlunit.ScriptResult;
import org.htmlunit.WebClientUtil;
import org.htmlunit.html.HtmlPage;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.LoggerRule;
import org.jvnet.hudson.test.MemoryAssert;
import org.jvnet.hudson.test.TestExtension;
/**
@ -46,6 +54,7 @@ import org.jvnet.hudson.test.TestExtension;
public class RenderOnDemandTest {
@Rule public JenkinsRule j = new JenkinsRule();
@Rule public LoggerRule logging = new LoggerRule().record(RenderOnDemandClosure.class, Level.FINE);
/**
* Makes sure that the behavior rules are applied to newly inserted nodes,
@ -64,27 +73,40 @@ public class RenderOnDemandTest {
assertEquals("AlphaBravoCharlie", r.getJavaScriptResult().toString());
}
/*
@Ignore("just informational")
@Issue("JENKINS-16341")
@Test
public void testMemoryConsumption() throws Exception {
j.createWebClient().goTo("self/testBehaviour"); // prime caches
var wc = j.createWebClient();
callTestBehaviour(wc); // prime caches
int total = 0;
for (MemoryAssert.HistogramElement element : MemoryAssert.increasedMemory(new Callable<Void>() {
@Override public Void call() throws Exception {
j.createWebClient().goTo("self/testBehaviour");
return null;
int count = 50;
for (var element : MemoryAssert.increasedMemory(() -> {
for (int i = 0; i < count; i++) {
System.err.println("#" + i);
callTestBehaviour(wc);
}
}, new Filter() {
@Override public boolean accept(Object obj, Object referredFrom, Field reference) {
return !obj.getClass().getName().contains("htmlunit");
}
})) {
var o = new Object();
var sr = new SoftReference<>(o);
var wr = new WeakReference<>(o);
o = null;
MemoryAssert.assertGC(wr, true);
return null;
}, (obj, referredFrom, reference) -> !obj.getClass().getName().contains("htmlunit"))) {
total += element.byteSize;
if (element.instanceCount == count) {
System.out.print("");
}
System.out.println(element.className + " ×" + element.instanceCount + ": " + element.byteSize);
}
System.out.println("total: " + total);
}
*/
private void callTestBehaviour(JenkinsRule.WebClient wc) throws Exception {
var p = wc.goTo("self/testBehaviour");
p.executeJavaScript("renderOnDemand(document.getElementsBySelector('.lazy')[0])");
WebClientUtil.waitForJSExec(p.getWebClient());
}
/**
* Makes sure that scripts get evaluated.

View File

@ -79,6 +79,25 @@ public class BindTest {
assertThat(root.invocations, is(1));
}
@Test
public void bindWithWellKnownURLWithQuotes() throws Exception {
final RootActionWithWellKnownURLWithQuotes root = ExtensionList.lookupSingleton(RootActionWithWellKnownURLWithQuotes.class);
try (JenkinsRule.WebClient wc = j.createWebClient()) {
final HtmlPage htmlPage = wc.goTo(root.getUrlName());
final String scriptUrl = htmlPage
.getElementsByTagName("script")
.stream()
.filter(it -> it.getAttribute("src").startsWith(j.contextPath + "/$stapler/bound/script" + j.contextPath + "/the'Well'Known\\'Root'With'Quotes?"))
.findFirst()
.orElseThrow()
.getAttribute("src");
final Page script = wc.goTo(StringUtils.removeStart(scriptUrl, j.contextPath + "/"), "text/javascript");
assertThat(script.getWebResponse().getContentAsString(), is("varname = makeStaplerProxy('" + j.contextPath + "/the\\'Well\\'Known\\\\\\'Root\\'With\\'Quotes','test',['annotatedJsMethod2','byName2']);"));
}
assertThat(root.invocations, is(1));
}
@Test
public void bindNull() throws Exception {
final RootActionImpl root = ExtensionList.lookupSingleton(RootActionImpl.class);
@ -173,4 +192,26 @@ public class BindTest {
invocations++;
}
}
@TestExtension
public static class RootActionWithWellKnownURLWithQuotes extends InvisibleAction implements RootAction, WithWellKnownURL {
private int invocations;
@Override
public String getUrlName() {
return "the'Well'Known\\'Root'With'Quotes";
}
@Override
public String getWellKnownUrl() {
return "/" + getUrlName();
}
@JavaScriptMethod
public void annotatedJsMethod2(String foo) {}
public void jsByName2() {
invocations++;
}
}
}

View File

@ -0,0 +1,10 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:st="jelly:stapler">
<l:layout title="The Root">
<l:main-panel>
<h1>Root Action</h1>
<st:bind var="varname" value="${it}"/>
<st:adjunct includes="org.kohsuke.stapler.BindTest.RootActionWithWellKnownURL.adjunct"/>
</l:main-panel>
</l:layout>
</j:jelly>

View File

@ -317,14 +317,14 @@ THE SOFTWARE.
<!-- detached after 1.577 -->
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>junit</artifactId>
<version>1322.v1556dc1c59a_f</version>
<version>1334.vd3b_b_2094e438</version>
<type>hpi</type>
</artifactItem>
<artifactItem>
<!-- dependency of junit, plugin-util-api, and workflow-support -->
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-api</artifactId>
<version>1371.ve334280b_d611</version>
<version>1373.v7b_813f10efa_b_</version>
<type>hpi</type>
</artifactItem>
<artifactItem>
@ -533,7 +533,7 @@ THE SOFTWARE.
<!-- dependency of junit and matrix-auth -->
<groupId>io.jenkins.plugins</groupId>
<artifactId>ionicons-api</artifactId>
<version>87.ve74b_60c10b_57</version>
<version>88.va_4187cb_eddf1</version>
<type>hpi</type>
</artifactItem>
<artifactItem>
@ -653,7 +653,7 @@ THE SOFTWARE.
contains a version of Jetty that is older than this, trigger Dependabot in jenkinsci/winstone and release the
result before proceeding with the update here.
-->
<version>12.0.20</version>
<version>12.0.21</version>
<configuration>
<!--
Reload webapp when you hit ENTER. (See JETTY-282 for more)

735
yarn.lock

File diff suppressed because it is too large Load Diff