Skip to content

Commit f72718b

Browse files
authored
Do not run symlink tests on Windows if symlinks are not supported (#26203)
* Do not run symlink tests on Windows if symlinks are not supported Windows does not support symbolic links by default. Tests fail for new contributors on Windows if they do not adjust their Windows configuration to allow symbolic links. * Fast incremental build * Create a symbolic link on Windows to confirm they are supported * Fix the copyright year - 2026 GitHub Copilot caught the mistake * Remove a white space change * Revert "Fast incremental build" Don't change production This reverts commit 0750dd8. * Fail tests on CI if Windows symlinks are not enabled We do not want to fail the test on a developer computer if symlinks are not enabled, but we do want to fail the test if a CI agent on Windows does not have symbolic links enabled.
1 parent 4a7abf9 commit f72718b

File tree

3 files changed

+103
-1
lines changed

3 files changed

+103
-1
lines changed

test/src/test/java/hudson/model/DirectoryBrowserSupportTest.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
package hudson.model;
2626

27+
import static hudson.model.WindowsUtil.isWindowsSymlinkSupported;
2728
import static org.hamcrest.MatcherAssert.assertThat;
2829
import static org.hamcrest.Matchers.allOf;
2930
import static org.hamcrest.Matchers.contains;
@@ -589,6 +590,8 @@ public boolean delete() {
589590
@Test
590591
@Issue("SECURITY-904")
591592
void symlink_outsideWorkspace_areNotAllowed() throws Exception {
593+
assumeTrue(!Functions.isWindows() || isWindowsSymlinkSupported());
594+
592595
FreeStyleProject p = j.createFreeStyleProject();
593596

594597
File secretsFolder = new File(j.jenkins.getRootDir(), "secrets");
@@ -728,6 +731,8 @@ void symlink_outsideWorkspace_areNotAllowed() throws Exception {
728731
@Test
729732
@Issue("SECURITY-904")
730733
void symlink_avoidLeakingInformation_aboutIllegalFolder() throws Exception {
734+
assumeTrue(!Functions.isWindows() || isWindowsSymlinkSupported());
735+
731736
FreeStyleProject p = j.createFreeStyleProject();
732737

733738
File secretsFolder = new File(j.jenkins.getRootDir(), "secrets");
@@ -800,7 +805,7 @@ void symlink_avoidLeakingInformation_aboutIllegalFolder() throws Exception {
800805
@Test
801806
@Issue("SECURITY-904")
802807
void junctionAndSymlink_outsideWorkspace_areNotAllowed_windowsJunction() throws Exception {
803-
assumeTrue(Functions.isWindows());
808+
assumeTrue(Functions.isWindows() && isWindowsSymlinkSupported());
804809

805810
FreeStyleProject p = j.createFreeStyleProject();
806811

@@ -1020,6 +1025,8 @@ void directSymlink_forTestingZip() throws Exception {
10201025
@Test
10211026
@Issue({"SECURITY-904", "SECURITY-1452"})
10221027
void symlink_insideWorkspace_areNotAllowedAnymore() throws Exception {
1028+
assumeTrue(!Functions.isWindows() || isWindowsSymlinkSupported());
1029+
10231030
FreeStyleProject p = j.createFreeStyleProject();
10241031

10251032
// build once to have the workspace set up
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright (c) 2026
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
package hudson.model;
26+
27+
import static org.junit.jupiter.api.Assertions.assertTrue;
28+
29+
import java.io.IOException;
30+
import java.nio.file.Files;
31+
import java.nio.file.Path;
32+
import java.util.concurrent.atomic.AtomicReference;
33+
34+
/**
35+
* Utility methods for Windows specific details on tests.
36+
*
37+
* @author Mark Waite
38+
*/
39+
public class WindowsUtil {
40+
41+
private static final AtomicReference<Boolean> symlinkSupported = new AtomicReference<>();
42+
43+
/**
44+
* Fail the test if running in CI and symlinks are not supported.
45+
* We do not want to fail the test on a developer computer if
46+
* symlinks are not enabled, but we do want to fail the test if a
47+
* CI agent on Windows does not have symbolic links enabled.
48+
*/
49+
private static void assertConfiguration(boolean supported) {
50+
if (System.getenv("CI") != null) {
51+
assertTrue(supported, "Jenkins CI configurations must enable symlinks on Windows");
52+
}
53+
}
54+
55+
/**
56+
* Returns true if Windows allows a symbolic link to be created.
57+
*
58+
* @return true if Windows allows a symbolic link to be created.
59+
* @throws IOException if creation or removal of temporary files fails
60+
*/
61+
public static boolean isWindowsSymlinkSupported() throws IOException {
62+
// Fast path, don't acquire unnecessary lock
63+
Boolean supported = symlinkSupported.get();
64+
if (supported != null) {
65+
assertConfiguration(supported);
66+
return supported;
67+
}
68+
synchronized (WindowsUtil.class) {
69+
supported = symlinkSupported.get();
70+
if (supported != null) {
71+
assertConfiguration(supported);
72+
return supported;
73+
}
74+
Path tempDir = Files.createTempDirectory("symlink-check");
75+
Path target = Files.createFile(tempDir.resolve("target.txt"));
76+
Path link = tempDir.resolve("link.txt");
77+
78+
try {
79+
Files.createSymbolicLink(link, target);
80+
Files.delete(link);
81+
symlinkSupported.set(true);
82+
} catch (IOException | UnsupportedOperationException uoe) {
83+
symlinkSupported.set(false);
84+
} finally {
85+
Files.delete(target);
86+
Files.delete(tempDir);
87+
}
88+
}
89+
assertConfiguration(symlinkSupported.get());
90+
return symlinkSupported.get();
91+
}
92+
}

test/src/test/java/hudson/tasks/ArtifactArchiverTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
package hudson.tasks;
2626

27+
import static hudson.model.WindowsUtil.isWindowsSymlinkSupported;
2728
import static org.hamcrest.MatcherAssert.assertThat;
2829
import static org.hamcrest.Matchers.lessThan;
2930
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
@@ -281,6 +282,7 @@ void followSymlinksEnabledForOldConfig() throws Exception {
281282
@Issue("SECURITY-162")
282283
@Test
283284
void outsideSymlinks() throws Exception {
285+
assumeTrue(!Functions.isWindows() || isWindowsSymlinkSupported());
284286
final FreeStyleProject p = j.createFreeStyleProject();
285287
p.getBuildersList().add(new TestBuilder() {
286288
@Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
@@ -519,6 +521,7 @@ public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListen
519521
@Test
520522
@Issue("JENKINS-55049")
521523
void lengthOfArtifactIsCorrect_eventForInvalidSymlink() throws Exception {
524+
assumeTrue(!Functions.isWindows() || isWindowsSymlinkSupported());
522525
FreeStyleProject p = j.createFreeStyleProject();
523526
p.getBuildersList().add(new TestBuilder() {
524527
@Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {

0 commit comments

Comments
 (0)