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
145 changes: 119 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 @@
*/
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 @@
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 @@
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) {

Check warning on line 453 in src/main/java/jenkins/branch/OrganizationFolder.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 453 is only partially covered, one branch is missing
projectNamePattern = ".*?(-[^.]+).*";

Check warning on line 454 in src/main/java/jenkins/branch/OrganizationFolder.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 454 is not covered by tests
}

for (SCMNavigator n : navigators) {
n.afterSave(this);
Expand Down Expand Up @@ -421,6 +489,10 @@
this.facDigest = facDigest;
this.propsDigest = propsDigest;
this.bbsDigest = bbsDigest;

// Trigger rescan if multiple projects configuration changed
recalculateAfterSubmitted(oldCreateMultipleProjects != this.createMultipleProjects);

Check warning on line 494 in src/main/java/jenkins/branch/OrganizationFolder.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 494 is only partially covered, one branch is missing
recalculateAfterSubmitted(!StringUtils.equals(oldProjectNamePattern, this.projectNamePattern));

Check warning on line 495 in src/main/java/jenkins/branch/OrganizationFolder.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 495 is only partially covered, one branch is missing
}

/**
Expand Down Expand Up @@ -1317,6 +1389,7 @@
@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,54 @@

@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()) {

Check warning on line 1445 in src/main/java/jenkins/branch/OrganizationFolder.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 1445 is only partially covered, 5 branches are missing
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);
LOGGER.info("newProjectName: " + newProjectName);
}
} catch (Exception e) {
LOGGER.warning(() -> "Failed to get new project name from " + candidateFactory + ": " + e.getMessage());
continue;

Check warning on line 1457 in src/main/java/jenkins/branch/OrganizationFolder.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 1447-1457 are not covered by tests
}
}

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) {
completeExisting(candidateFactory, attributes, existing, wasBuildable, wasDisabled);
} else {
completeNew(candidateFactory, attributes, folderName);
}
} finally {
observer.completed(folderName);
}

if (!createMultipleProjects) {

Check warning on line 1478 in src/main/java/jenkins/branch/OrganizationFolder.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 1478 is only partially covered, one branch is missing
break;
}
}
} finally {
observer.completed(folderName);
}
} catch (InterruptedException | IOException x) {
throw x;
Expand All @@ -1402,10 +1494,11 @@
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())) {

Check warning on line 1497 in src/main/java/jenkins/branch/OrganizationFolder.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 1497 is only partially covered, 2 branches are missing
existing.getProperties().remove(ProjectNameProperty.class);
existing.addProperty(new ProjectNameProperty(projectName));
existing.addProperty(new ProjectNameProperty(newProjectName));

Check warning on line 1499 in src/main/java/jenkins/branch/OrganizationFolder.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 1499 is not covered by tests
}
existing.setDisplayName(newProjectName);
for (AbstractFolderProperty<?> folderProperty : getProperties()) {
if (folderProperty instanceof OrganizationFolderProperty) {
((OrganizationFolderProperty) folderProperty).applyDecoration(existing,
Expand Down Expand Up @@ -1441,9 +1534,9 @@
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