Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import hudson.model.Item;
import hudson.model.Job;
import hudson.model.Result;
import hudson.model.Run;
import hudson.plugins.git.BranchSpec;
import hudson.plugins.git.GitSCM;
import hudson.plugins.git.GitStatus;
Expand Down Expand Up @@ -67,22 +68,33 @@ public static boolean isGitPluginInstalled() {
annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false))
public List getJobScm(
@ToolParam(description = "Full path of the Jenkins job (e.g., 'folder/job-name')") String jobFullName) {
var job = Jenkins.get().getItemByFullName(jobFullName, Job.class);
if (job instanceof SCMTriggerItem scmItem) {
if (job.hasPermission(Item.EXTENDED_READ)) {
return scmItem.getSCMs().stream()
.map(scm -> {
Object result = null;
if (scm.getType().equals("hudson.plugins.git.GitSCM")) {
result = GitScmUtil.extractGitScmInfo(scm);
}
return result;
})
.filter(Objects::nonNull)
.toList();
}
var maybeJob = resolveVisibleJob(jobFullName);
if (maybeJob.isEmpty())
return List.of();
var job = maybeJob.get();
if (!job.hasPermission(Item.EXTENDED_READ)) {
logLookupFailure("missing EXTENDED_READ", jobFullName, null, null);
return List.of();
}
if (!(job instanceof SCMTriggerItem scmItem)) {
logLookupFailure("job does not expose SCM configuration", jobFullName, null, null);
return List.of();
}

try {
return scmItem.getSCMs().stream()
.map(scm -> {
Object result = null;
if (scm.getType().equals("hudson.plugins.git.GitSCM"))
result = GitScmUtil.extractGitScmInfo(scm);
return result;
})
.filter(Objects::nonNull)
.toList();
} catch (RuntimeException e) {
logLookupFailure("failed to extract SCM details", jobFullName, null, e);
return List.of();
}
return List.of();
}

@Tool(
Expand All @@ -95,14 +107,70 @@ public List getBuildScm(
description = "Build number (optional, if not provided, updates the last build)",
required = false)
Integer buildNumber) {
return getBuildByNumberOrLast(jobFullName, buildNumber)
.map(build -> {
if (isGitPluginInstalled()) {
return List.of(GitScmUtil.extractGitScmInfo(build));
}
return List.of();
})
.orElse(List.of());
var maybeJob = resolveVisibleJob(jobFullName);
if (maybeJob.isEmpty())
return List.of();
Run<?, ?> build = resolveBuild(maybeJob.get(), buildNumber).orElse(null);
if (build == null)
return List.of();

if (!isGitPluginInstalled()) {
logLookupFailure("git plugin is not installed or inactive", jobFullName, buildNumber, null);
return List.of();
}

try {
return Optional.ofNullable(GitScmUtil.extractGitScmInfo(build)).map(List::of).orElse(List.of());
} catch (RuntimeException e) {
logLookupFailure("failed to extract build SCM details", jobFullName, buildNumber, e);
return List.of();
}
}

private Optional<Job<?, ?>> resolveVisibleJob(String jobFullName) {
var job = Jenkins.get().getItemByFullName(jobFullName, Job.class);
if (job == null) {
logLookupFailure("job not found or not visible", jobFullName, null, null);
return Optional.empty();
}
return Optional.of(job);
}

private Optional<Run<?, ?>> resolveBuild(Job<?, ?> job, @Nullable Integer buildNumber) {
Run<?, ?> build;
if (buildNumber == null || buildNumber <= 0) {
build = job.getLastBuild();
} else {
build = job.getBuildByNumber(buildNumber);
}

if (build == null) {
int requestedBuildNumber = (buildNumber == null || buildNumber <= 0) ? -1 : buildNumber;
logLookupFailure("build not found", job.getFullName(), requestedBuildNumber, null);
return Optional.empty();
}
return Optional.of(build);
}

private void logLookupFailure(
String reason, String jobFullName, @Nullable Integer buildNumber, @Nullable Throwable error) {
if (!LOGGER.isLoggable(Level.WARNING))
return;
var jenkins = Jenkins.get();
String user = jenkins.getAuthentication2().getName();
if (error == null) {
LOGGER.log(
Level.WARNING,
"SCM lookup failed: reason={0}, jobFullName={1}, buildNumber={2}, user={3}, rootUrl={4}",
new Object[] {reason, jobFullName, buildNumber, user, jenkins.getRootUrl()});
return;
}
LOGGER.log(
Level.WARNING,
String.format(
"SCM lookup failed: reason=%s, jobFullName=%s, buildNumber=%s, user=%s, rootUrl=%s",
reason, jobFullName, buildNumber, user, jenkins.getRootUrl()),
error);
}

@Tool(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,32 @@
import hudson.model.Run;
import jakarta.annotation.Nullable;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import lombok.NonNull;

public class JenkinsUtil {

private static final Logger LOGGER = Logger.getLogger(JenkinsUtil.class.getName());

public static Optional<Run> getBuildByNumberOrLast(@NonNull String fullJobName, @Nullable Integer buildNumber) {
return Optional.of(Jenkins.get())
.map(jenkins -> jenkins.getItemByFullName(fullJobName, Job.class))
.map(job -> {
if (buildNumber == null || buildNumber <= 0) {
return job.getLastBuild();
} else {
return job.getBuildByNumber(buildNumber);
}
});
var jenkins = Jenkins.get();
var job = jenkins.getItemByFullName(fullJobName, Job.class);
if (job == null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(
Level.FINE,
"Build lookup failed: job not found or not visible. jobFullName={0}, buildNumber={1}, user={2}, rootUrl={3}",
new Object[] {
fullJobName, buildNumber, jenkins.getAuthentication2().getName(), jenkins.getRootUrl()
});
}
return Optional.empty();
}

if (buildNumber == null || buildNumber <= 0)
return Optional.ofNullable(job.getLastBuild());
return Optional.ofNullable(job.getBuildByNumber(buildNumber));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,65 @@ void testGetBuildScm(
}
}

@McpClientTest
void testGetBuildScmReturnsEmptyForNonGitBuild(JenkinsRule jenkins, JenkinsMcpClientBuilder jenkinsMcpClientBuilder)
throws Exception {
FreeStyleProject project = jenkins.createFreeStyleProject("non-git-build-project");
jenkins.buildAndAssertSuccess(project);

try (var client = jenkinsMcpClientBuilder.jenkins(jenkins).build()) {
McpSchema.CallToolRequest request = new McpSchema.CallToolRequest(
"getBuildScm", Map.of("jobFullName", project.getFullName(), "buildNumber", 1));

var response = client.callTool(request);

assertThat(response.isError()).isFalse();
assertThat(response.content()).hasSize(1);
assertThat(response.content()).first().isInstanceOfSatisfying(McpSchema.TextContent.class, textContent -> {
assertThat(textContent.type()).isEqualTo("text");
assertThat(textContent.text()).contains("no results were found");
});
}
}

@McpClientTest
void testGetJobScmReturnsEmptyWhenJobIsNotVisibleOrMissing(
JenkinsRule jenkins, JenkinsMcpClientBuilder jenkinsMcpClientBuilder) throws Exception {
try (var client = jenkinsMcpClientBuilder.jenkins(jenkins).build()) {
McpSchema.CallToolRequest request =
new McpSchema.CallToolRequest("getJobScm", Map.of("jobFullName", "missing/folder/job"));

var response = client.callTool(request);

assertThat(response.isError()).isFalse();
assertThat(response.content()).hasSize(1);
assertThat(response.content()).first().isInstanceOfSatisfying(McpSchema.TextContent.class, textContent -> {
assertThat(textContent.type()).isEqualTo("text");
assertThat(textContent.text()).contains("no results were found");
});
}
}

@McpClientTest
void testGetBuildScmReturnsEmptyWhenBuildDoesNotExist(
JenkinsRule jenkins, JenkinsMcpClientBuilder jenkinsMcpClientBuilder) throws Exception {
FreeStyleProject project = jenkins.createFreeStyleProject("missing-build-project");

try (var client = jenkinsMcpClientBuilder.jenkins(jenkins).build()) {
McpSchema.CallToolRequest request = new McpSchema.CallToolRequest(
"getBuildScm", Map.of("jobFullName", project.getFullName(), "buildNumber", 9999));

var response = client.callTool(request);

assertThat(response.isError()).isFalse();
assertThat(response.content()).hasSize(1);
assertThat(response.content()).first().isInstanceOfSatisfying(McpSchema.TextContent.class, textContent -> {
assertThat(textContent.type()).isEqualTo("text");
assertThat(textContent.text()).contains("no results were found");
});
}
}

@McpClientTest
void testGetBuildChangeSets(
JenkinsRule jenkins, GitSampleRepoRule gitRepo, JenkinsMcpClientBuilder jenkinsMcpClientBuilder)
Expand Down