Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion src/main/java/jenkins/branch/MultiBranchProject.java
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,43 @@
}
}
for (final SCMSource source : scmSources) {
String scriptPathFetched = "";
try {
scriptPathFetched = (String) _factory.getClass().getMethod("getScriptPath").invoke(_factory);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "getScriptPath error: {0}", e.getMessage());
}

if (scriptPathFetched != null && scriptPathFetched != "") {

Check warning on line 699 in src/main/java/jenkins/branch/MultiBranchProject.java

View check run for this annotation

ci.jenkins.io / SpotBugs

ES_COMPARING_STRINGS_WITH_EQ

NORMAL: Comparison of String objects using == or != in jenkins.branch.MultiBranchProject.computeChildren(ChildObserver, TaskListener)
Raw output
<p>This code compares <code>java.lang.String</code> objects for reference equality using the == or != operators. Unless both strings are either constants in a source file, or have been interned using the <code>String.intern()</code> method, the same string value may be represented by two different String objects. Consider using the <code>equals(Object)</code> method instead.</p>
final String scriptPath = scriptPathFetched;
LOGGER.log(Level.INFO, "scriptPath: {0}", scriptPath);
try {
SCMSourceCriteria criteria = (probe, l) -> {
boolean exists = probe.stat(scriptPath).exists();
if (exists) {
l.getLogger().format(" '%s' found%n", scriptPath);
} else {
l.getLogger().format(" '%s' not found%n", scriptPath);
}

return exists;
};

source.fetch(criteria, new SCMHeadObserverImpl(source, observer, listener, _factory,
new IndexingCauseFactory(), null), listener);

observer.observed().forEach((branchName) -> {
LOGGER.log(Level.INFO, "Observed scriptPath: {0}, branch: {1}",
new Object[]{scriptPath, branchName});
});
} catch (IOException | InterruptedException | RuntimeException e) {
listener.error("[%tc] Could not fetch branches from source %s",
System.currentTimeMillis(), source.getId());
throw e;
}
continue;
}

try {
source.fetch(new SCMHeadObserverImpl(source, observer, listener, _factory,
new IndexingCauseFactory(), null), listener);
Expand Down Expand Up @@ -2360,7 +2397,7 @@
}

/**
* Adds the {@link MultiBranchProject.State#sourceActions} to
* Adds the {@link State#sourceActions} to
* {@link MultiBranchProject#getAllActions()}.
*
* @since 2.0
Expand Down
146 changes: 120 additions & 26 deletions src/main/java/jenkins/branch/OrganizationFolder.java
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@
import static jenkins.scm.api.SCMEvent.Type.CREATED;
import static jenkins.scm.api.SCMEvent.Type.UPDATED;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* A folder-like collection of {@link MultiBranchProject}s, one per repository.
*/
Expand Down Expand Up @@ -145,6 +148,21 @@ public final class OrganizationFolder extends ComputedFolder<MultiBranchProject<
*/
private BranchPropertyStrategy strategy;

/**
* Whether to create multiple projects based on Jenkinsfile path pattern.
*
* @since 2.0
*/
private boolean createMultipleProjects = false;

/**
* Pattern for extracting project name suffix from Jenkinsfile path.
* Default pattern extracts suffix after last hyphen before dot.
*
* @since 2.0
*/
private String projectNamePattern = ".*?(-[^.]+).*";

/**
* The persisted state maintained outside of the config file.
*
Expand Down Expand Up @@ -363,6 +381,46 @@ public void setStrategy(BranchPropertyStrategy strategy) {
this.strategy = strategy;
}

/**
* Gets whether to create multiple projects based on Jenkinsfile path pattern.
*
* @return true if multiple projects should be created.
* @since 2.0
*/
public boolean isCreateMultipleProjects() {
return createMultipleProjects;
}

/**
* Sets whether to create multiple projects based on Jenkinsfile path pattern.
*
* @param createMultipleProjects true to enable multiple projects creation.
* @since 2.0
*/
public void setCreateMultipleProjects(boolean createMultipleProjects) {
this.createMultipleProjects = createMultipleProjects;
}

/**
* Gets the pattern for extracting project name suffix from Jenkinsfile path.
*
* @return the project name pattern.
* @since 2.0
*/
public String getProjectNamePattern() {
return projectNamePattern != null ? projectNamePattern : ".*?(-[^.]+).*";
}

/**
* Sets the pattern for extracting project name suffix from Jenkinsfile path.
*
* @param projectNamePattern the pattern to use.
* @since 2.0
*/
public void setProjectNamePattern(String projectNamePattern) {
this.projectNamePattern = projectNamePattern;
}

/**
* The {@link BranchBuildStrategy}s to apply.
*
Expand All @@ -385,6 +443,16 @@ protected void submit(StaplerRequest2 req, StaplerResponse2 rsp) throws IOExcept
projectFactories.rebuildHetero(req, json, ExtensionList.lookup(MultiBranchProjectFactoryDescriptor.class), "projectFactories");
buildStrategies.rebuildHetero(req, json, ExtensionList.lookup(BranchBuildStrategyDescriptor.class), "buildStrategies");
strategy = req.bindJSON(BranchPropertyStrategy.class, json.getJSONObject("strategy"));

// Handle multiple projects configuration and track changes
boolean oldCreateMultipleProjects = this.createMultipleProjects;
String oldProjectNamePattern = this.projectNamePattern;

createMultipleProjects = json.optBoolean("createMultipleProjects", false);
projectNamePattern = Util.fixEmptyAndTrim(json.optString("projectNamePattern", ".*?(-[^.]+).*"));
if (projectNamePattern == null) {
projectNamePattern = ".*?(-[^.]+).*";
}

for (SCMNavigator n : navigators) {
n.afterSave(this);
Expand Down Expand Up @@ -421,6 +489,10 @@ protected void submit(StaplerRequest2 req, StaplerResponse2 rsp) throws IOExcept
this.facDigest = facDigest;
this.propsDigest = propsDigest;
this.bbsDigest = bbsDigest;

// Trigger rescan if multiple projects configuration changed
recalculateAfterSubmitted(oldCreateMultipleProjects != this.createMultipleProjects);
recalculateAfterSubmitted(!StringUtils.equals(oldProjectNamePattern, this.projectNamePattern));
}

/**
Expand Down Expand Up @@ -1317,6 +1389,7 @@ public TaskListener getListener() {
@Override
public ProjectObserver observe(@NonNull final String projectName) {
return new ProjectObserver() {
String newProjectName = projectName;
List<SCMSource> sources = new ArrayList<>();

@Override
Expand Down Expand Up @@ -1358,35 +1431,55 @@ private boolean recognizes(Map<String, Object> attributes, MultiBranchProjectFac

@Override
public void complete() throws IllegalStateException, IOException, InterruptedException {
String projectNamePatten = getProjectNamePattern();
LOGGER.info("createMultipleProjects: " + createMultipleProjects);
LOGGER.info("projectNamePatten: " + projectNamePatten);
try {
MultiBranchProjectFactory factory = null;
LOGGER.info("Scan " + projectName + " ...");
Map<String, Object> attributes = Collections.emptyMap();
for (MultiBranchProjectFactory candidateFactory : projectFactories) {
boolean recognizes = recognizes(attributes, candidateFactory);
LOGGER.fine(() -> candidateFactory + " recognizes " + projectName + " with " + attributes + "? " + recognizes);
if (recognizes) {
factory = candidateFactory;
break;
}
}
if (factory == null) {
return;
}
String folderName = NameEncoder.encode(projectName);
// HACK: observer.shouldUpdate will restore the buildable flag of the child, so pre-inspect
MultiBranchProject<?, ?> existing = items.get(folderName);
boolean wasBuildable = existing != null && existing.isBuildable();
boolean wasDisabled = existing != null && existing.isDisabled();
// END_HACK: now that we know if it was buildable, we can now proceed to see about updating
existing = observer.shouldUpdate(folderName);
try {
if (existing != null) {
completeExisting(factory, attributes, existing, wasBuildable, wasDisabled);
} else {
completeNew(factory, attributes, folderName);
newProjectName = projectName;
if (createMultipleProjects && projectNamePatten != null && !projectNamePatten.isEmpty()) {
try {
String scriptPath = (String) candidateFactory.getClass().getMethod("getScriptPath").invoke(candidateFactory);
LOGGER.info("scriptPath: " + scriptPath);
Pattern pattern = Pattern.compile(projectNamePatten);
Matcher matcher = pattern.matcher(scriptPath);
if (matcher.matches()) {
newProjectName = projectName + matcher.group(1);
}
} catch (Exception e) {
LOGGER.warning(() -> "Failed to get new project name from " + candidateFactory + ": " + e.getMessage());
continue;
}
}

String folderName = NameEncoder.encode(newProjectName);
// HACK: observer.shouldUpdate will restore the buildable flag of the child, so pre-inspect
MultiBranchProject<?, ?> existing = items.get(folderName);
boolean wasBuildable = existing != null && existing.isBuildable();
boolean wasDisabled = existing != null && existing.isDisabled();
// END_HACK: now that we know if it was buildable, we can now proceed to see about updating
existing = observer.shouldUpdate(folderName);
try {
if (existing != null) {
LOGGER.info("Update existing project: " + folderName);
completeExisting(candidateFactory, attributes, existing, wasBuildable, wasDisabled);
} else {
LOGGER.info("Create new project: " + folderName);
completeNew(candidateFactory, attributes, folderName);
}
} finally {
observer.completed(folderName);
}

if (!createMultipleProjects) {
break;
}
}
} finally {
observer.completed(folderName);
}
} catch (InterruptedException | IOException x) {
throw x;
Expand All @@ -1402,10 +1495,11 @@ private void completeExisting(MultiBranchProjectFactory factory, Map<String, Obj
factory.updateExistingProject(existing, attributes, listener);
ProjectNameProperty property =
existing.getProperties().get(ProjectNameProperty.class);
if (property == null || !projectName.equals(property.getName())) {
if (property == null || !newProjectName.equals(property.getName())) {
existing.getProperties().remove(ProjectNameProperty.class);
existing.addProperty(new ProjectNameProperty(projectName));
existing.addProperty(new ProjectNameProperty(newProjectName));
}
existing.setDisplayName(newProjectName);
for (AbstractFolderProperty<?> folderProperty : getProperties()) {
if (folderProperty instanceof OrganizationFolderProperty) {
((OrganizationFolderProperty) folderProperty).applyDecoration(existing,
Expand Down Expand Up @@ -1441,9 +1535,9 @@ private void completeNew(MultiBranchProjectFactory factory, Map<String, Object>
BulkChange bc = new BulkChange(project);
try {
if (!projectName.equals(folderName)) {
project.setDisplayName(projectName);
project.setDisplayName(newProjectName);
}
project.addProperty(new ProjectNameProperty(projectName));
project.addProperty(new ProjectNameProperty(newProjectName));
project.getSourcesList().addAll(createBranchSources());
for (AbstractFolderProperty<?> property: getProperties()) {
if (property instanceof OrganizationFolderProperty) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@
<f:entry field="navigators" title="${%Repository Sources}">
<f:repeatableHeteroProperty field="navigators" hasHeader="true"/>
</f:entry>

<f:entry field="createMultipleProjects" title="${%CreateMultipleProjects.DisplayName}">
<f:checkbox default="false" title="${%CreateMultipleProjects.Description}"/>
<f:entry field="projectNamePattern" title="${%ProjectNamePattern.DisplayName}">
<f:textbox default=".*?(-[^.]+).*"/>
</f:entry>
</f:entry>

<f:entry field="projectFactories" title="${%Project Recognizers}">
<f:repeatableHeteroProperty field="projectFactories" hasHeader="true" />
</f:entry>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CreateMultipleProjects.DisplayName=Create Multiple Projects
CreateMultipleProjects.Description=Enable multiple projects creation of the same repository based on Jenkinsfile path pattern

ProjectNamePattern.DisplayName=Project Name Pattern
ProjectNamePattern.Description=Regular expression pattern to extract project name suffix from Jenkinsfile path. Default pattern extracts suffix after last hyphen (e.g., 'Jenkinsfile-win' creates 'myproject-win')
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<div>
Controls whether to create multiple projects of the same repository based on Jenkinsfile path patterns.

<p>
When enabled, the system will examine Jenkinsfile paths and create separate projects
for different variants of the same repository based on the configured pattern.
</p>

<h3>Example</h3>
<p>
If you have Jenkinsfiles like:
<ul>
<li><code>Jenkinsfile</code> (main build)</li>
<li><code>Jenkinsfile-win</code> (Windows-specific build)</li>
<li><code>Jenkinsfile-mac</code> (Mac-specific build)</li>
</ul>
</p>
<p>
With the default pattern <code>.*?(-[^.]+).*</code>, this would create:
<ul>
<li><code>myproject</code> from <code>Jenkinsfile</code></li>
<li><code>myproject-win</code> from <code>Jenkinsfile-win</code></li>
<li><code>myproject-mac</code> from <code>Jenkinsfile-mac</code></li>
</ul>
</p>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<div>
Regular expression pattern used to extract project name suffix from Jenkinsfile path.

<p>
The pattern is applied to the Jenkinsfile path, and the first capture group
(if any) is appended to the base repository name to create the final project name.
</p>

<h3>Default Pattern</h3>
<p>
<code>.*?(-[^.]+).*</code>
</p>
<p>
This pattern captures everything after the last hyphen and before the final dot.
</p>

<h3>Examples</h3>
<table border="1" cellpadding="3" cellspacing="0">
<tr><th>Pattern</th><th>Jenkinsfile</th><th>Resulting Project</th></tr>
<tr>
<td><code>.*?(-[^.]+).*</code></td>
<td><code>Jenkinsfile-win</code></td>
<td><code>myproject-win</code></td>
</tr>
<tr>
<td><code>.*?(-[^.]+).*</code></td>
<td><code>Jenkinsfile</code></td>
<td><code>myproject</code></td>
</tr>
</table>

<h3>Pattern Tips</h3>
<ul>
<li>The first capture group's content is used as the suffix</li>
<li>If no match is found, the original repository name is used</li>
</ul>
</div>
Loading
Loading