@alexsarrell/jenkins-mcp-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +65 -0
- package/dist/jenkins-client.d.ts +29 -0
- package/dist/jenkins-client.js +212 -0
- package/dist/tools/builds.d.ts +4 -0
- package/dist/tools/builds.js +207 -0
- package/dist/tools/discovery.d.ts +4 -0
- package/dist/tools/discovery.js +110 -0
- package/dist/tools/jobs.d.ts +4 -0
- package/dist/tools/jobs.js +78 -0
- package/dist/tools/pipeline.d.ts +4 -0
- package/dist/tools/pipeline.js +208 -0
- package/dist/types.d.ts +143 -0
- package/dist/types.js +1 -0
- package/dist/utils/formatters.d.ts +12 -0
- package/dist/utils/formatters.js +166 -0
- package/dist/utils/path-resolver.d.ts +17 -0
- package/dist/utils/path-resolver.js +36 -0
- package/package.json +32 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatQueue, ok, error } from "../utils/formatters.js";
|
|
3
|
+
export function registerDiscoveryTools(client, register) {
|
|
4
|
+
// 16. searchBuildLogs
|
|
5
|
+
register("searchBuildLogs", "Search build logs for a text pattern across recent builds. Like grep for Jenkins logs. Searches the last N builds (default: 5) and returns matching lines with context.", z.object({
|
|
6
|
+
jobPath: z.string().describe("Full job path"),
|
|
7
|
+
pattern: z.string().describe("Text pattern to search for (case-insensitive substring match)"),
|
|
8
|
+
buildNumber: z.number().optional().describe("Search a specific build number only"),
|
|
9
|
+
lastN: z.number().optional().default(5).describe("Number of recent builds to search (default: 5, max: 20)"),
|
|
10
|
+
}), async (args) => {
|
|
11
|
+
const jobPath = args.jobPath;
|
|
12
|
+
const pattern = args.pattern;
|
|
13
|
+
const buildNumber = args.buildNumber;
|
|
14
|
+
const lastN = Math.min(args.lastN || 5, 20);
|
|
15
|
+
try {
|
|
16
|
+
const patternLower = pattern.toLowerCase();
|
|
17
|
+
const results = [];
|
|
18
|
+
if (buildNumber) {
|
|
19
|
+
// Search specific build
|
|
20
|
+
const matches = await searchBuildLog(client, jobPath, buildNumber, patternLower);
|
|
21
|
+
if (matches.length > 0) {
|
|
22
|
+
results.push(`Build #${buildNumber} (${matches.length} matches):`);
|
|
23
|
+
results.push(...matches.map((m) => ` L${m.line}: ${m.text}`));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
// Get recent builds
|
|
28
|
+
const data = await client.get(jobPath, "/api/json", {
|
|
29
|
+
tree: "builds[number]{0," + lastN + "}",
|
|
30
|
+
});
|
|
31
|
+
const jobData = data;
|
|
32
|
+
const builds = jobData.builds || [];
|
|
33
|
+
for (const build of builds) {
|
|
34
|
+
const matches = await searchBuildLog(client, jobPath, build.number, patternLower);
|
|
35
|
+
if (matches.length > 0) {
|
|
36
|
+
results.push(`Build #${build.number} (${matches.length} matches):`);
|
|
37
|
+
results.push(...matches.slice(0, 20).map((m) => ` L${m.line}: ${m.text}`));
|
|
38
|
+
if (matches.length > 20) {
|
|
39
|
+
results.push(` ... and ${matches.length - 20} more matches`);
|
|
40
|
+
}
|
|
41
|
+
results.push("");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (results.length === 0) {
|
|
46
|
+
return ok(`No matches found for "${pattern}" in ${buildNumber ? `build #${buildNumber}` : `last ${lastN} builds`}.`);
|
|
47
|
+
}
|
|
48
|
+
return ok(`Search results for "${pattern}":\n\n${results.join("\n")}`);
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
return handleError(e);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
// 17. getQueue
|
|
55
|
+
register("getQueue", "View the Jenkins build queue - shows jobs waiting to be built, why they're waiting, and if they're stuck.", z.object({}), async () => {
|
|
56
|
+
try {
|
|
57
|
+
const data = await client.getAbsolute("/queue/api/json", {
|
|
58
|
+
tree: "items[id,task[name,url],why,buildableStartMilliseconds,stuck,blocked]",
|
|
59
|
+
});
|
|
60
|
+
const queue = data;
|
|
61
|
+
return ok(formatQueue(queue.items || []));
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
return handleError(e);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
// 18. enableDisableJob
|
|
68
|
+
register("enableDisableJob", "Enable or disable a Jenkins job. Disabled jobs cannot be triggered.", z.object({
|
|
69
|
+
jobPath: z.string().describe("Full job path"),
|
|
70
|
+
enabled: z.boolean().describe("true to enable, false to disable"),
|
|
71
|
+
}), async (args) => {
|
|
72
|
+
const jobPath = args.jobPath;
|
|
73
|
+
const enabled = args.enabled;
|
|
74
|
+
try {
|
|
75
|
+
const action = enabled ? "/enable" : "/disable";
|
|
76
|
+
await client.post(jobPath, action);
|
|
77
|
+
return ok(`Job ${jobPath} ${enabled ? "enabled" : "disabled"} successfully.`);
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
return handleError(e);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async function searchBuildLog(client, jobPath, buildNumber, patternLower) {
|
|
85
|
+
try {
|
|
86
|
+
const logText = await client.getRaw(jobPath, `/${buildNumber}/consoleText`);
|
|
87
|
+
const lines = logText.split("\n");
|
|
88
|
+
const matches = [];
|
|
89
|
+
for (let i = 0; i < lines.length; i++) {
|
|
90
|
+
if (lines[i].toLowerCase().includes(patternLower)) {
|
|
91
|
+
matches.push({
|
|
92
|
+
line: i + 1,
|
|
93
|
+
text: lines[i].substring(0, 200), // Truncate long lines
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return matches;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function handleError(e) {
|
|
104
|
+
if (e && typeof e === "object" && "errorCode" in e) {
|
|
105
|
+
const je = e;
|
|
106
|
+
return error(`[${je.errorCode}] ${je.message}`);
|
|
107
|
+
}
|
|
108
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
109
|
+
return error(msg);
|
|
110
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { JenkinsClient } from "../jenkins-client.js";
|
|
3
|
+
import type { ToolResult } from "../types.js";
|
|
4
|
+
export declare function registerJobTools(client: JenkinsClient, register: (name: string, description: string, schema: z.ZodType, handler: (args: Record<string, unknown>) => Promise<ToolResult>) => void): void;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatJobList, formatJobDetail, ok, error, truncateText } from "../utils/formatters.js";
|
|
3
|
+
export function registerJobTools(client, register) {
|
|
4
|
+
// 1. getJobs
|
|
5
|
+
register("getJobs", "List Jenkins jobs at root level or in a specific folder. Returns job names, status, and last build info. For multibranch pipelines, lists branches as sub-jobs.", z.object({
|
|
6
|
+
folder: z.string().optional().describe("Folder path (e.g., 'my-folder' or 'folder/subfolder'). Omit for root level."),
|
|
7
|
+
limit: z.number().optional().default(50).describe("Maximum number of jobs to return (default: 50)"),
|
|
8
|
+
}), async (args) => {
|
|
9
|
+
const folder = args.folder;
|
|
10
|
+
const limit = args.limit || 50;
|
|
11
|
+
try {
|
|
12
|
+
const tree = "jobs[name,url,color,description,fullName,lastBuild[number,result,timestamp]]";
|
|
13
|
+
let data;
|
|
14
|
+
if (folder) {
|
|
15
|
+
data = await client.get(folder, "/api/json", { tree });
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
data = await client.getAbsolute("/api/json", { tree });
|
|
19
|
+
}
|
|
20
|
+
const result = data;
|
|
21
|
+
const jobs = (result.jobs || []).slice(0, limit);
|
|
22
|
+
return ok(formatJobList(jobs));
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
return handleError(e);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
// 2. getJob
|
|
29
|
+
register("getJob", "Get detailed information about a specific Jenkins job including status, health, parameter definitions, and last build info. For multibranch pipelines, also lists branches.", z.object({
|
|
30
|
+
jobPath: z.string().describe("Full job path (e.g., 'my-folder/my-job' or 'pipeline/main'). For branches with slashes, use :: separator: 'pipeline::feature/branch'"),
|
|
31
|
+
}), async (args) => {
|
|
32
|
+
const jobPath = args.jobPath;
|
|
33
|
+
try {
|
|
34
|
+
const tree = "name,fullName,url,color,description,buildable,healthReport[description,score],lastBuild[number,result,timestamp,url],lastSuccessfulBuild[number,url],lastFailedBuild[number,url],property[parameterDefinitions[name,type,description,defaultParameterValue[value]]],jobs[name,color,url,fullName]";
|
|
35
|
+
const data = await client.get(jobPath, "/api/json", { tree });
|
|
36
|
+
return ok(formatJobDetail(data));
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
return handleError(e);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
// 3. getJobConfig
|
|
43
|
+
register("getJobConfig", "Get the XML configuration of a Jenkins job (config.xml). Useful for understanding job setup, viewing pipeline definitions, or preparing edits.", z.object({
|
|
44
|
+
jobPath: z.string().describe("Full job path (e.g., 'my-folder/my-job')"),
|
|
45
|
+
}), async (args) => {
|
|
46
|
+
const jobPath = args.jobPath;
|
|
47
|
+
try {
|
|
48
|
+
const xml = await client.getRaw(jobPath, "/config.xml");
|
|
49
|
+
return ok(truncateText(xml));
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
return handleError(e);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
// 4. updateJobConfig
|
|
56
|
+
register("updateJobConfig", "Update a Jenkins job's XML configuration. Send the complete config.xml content. Use getJobConfig first to read the current config, modify it, then submit.", z.object({
|
|
57
|
+
jobPath: z.string().describe("Full job path (e.g., 'my-folder/my-job')"),
|
|
58
|
+
configXml: z.string().describe("Complete XML configuration for the job"),
|
|
59
|
+
}), async (args) => {
|
|
60
|
+
const jobPath = args.jobPath;
|
|
61
|
+
const configXml = args.configXml;
|
|
62
|
+
try {
|
|
63
|
+
await client.post(jobPath, "/config.xml", configXml, "application/xml");
|
|
64
|
+
return ok(`Job config updated successfully: ${jobPath}`);
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
return handleError(e);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function handleError(e) {
|
|
72
|
+
if (e && typeof e === "object" && "errorCode" in e) {
|
|
73
|
+
const je = e;
|
|
74
|
+
return error(`[${je.errorCode}] ${je.message}`);
|
|
75
|
+
}
|
|
76
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
77
|
+
return error(msg);
|
|
78
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { JenkinsClient } from "../jenkins-client.js";
|
|
3
|
+
import type { ToolResult } from "../types.js";
|
|
4
|
+
export declare function registerPipelineTools(client: JenkinsClient, register: (name: string, description: string, schema: z.ZodType, handler: (args: Record<string, unknown>) => Promise<ToolResult>) => void): void;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatStages, formatDuration, ok, error, truncateText } from "../utils/formatters.js";
|
|
3
|
+
export function registerPipelineTools(client, register) {
|
|
4
|
+
// 11. getPipelineStages
|
|
5
|
+
register("getPipelineStages", "Get pipeline stages overview for a build - shows stage names, status, and duration. Works with Pipeline (Workflow) jobs. Useful for understanding which stage failed.", z.object({
|
|
6
|
+
jobPath: z.string().describe("Full job path"),
|
|
7
|
+
buildNumber: z.number().optional().describe("Build number (default: last build)"),
|
|
8
|
+
}), async (args) => {
|
|
9
|
+
const jobPath = args.jobPath;
|
|
10
|
+
const buildNumber = args.buildNumber;
|
|
11
|
+
const num = buildNumber ?? "lastBuild";
|
|
12
|
+
try {
|
|
13
|
+
const data = await client.get(jobPath, `/${num}/wfapi/describe`);
|
|
14
|
+
const run = data;
|
|
15
|
+
if (!run.stages || run.stages.length === 0) {
|
|
16
|
+
return ok("No pipeline stages found. This may not be a Pipeline job, or the build hasn't started stages yet.");
|
|
17
|
+
}
|
|
18
|
+
return ok(formatStages(run.stages));
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
return handleError(e, "This endpoint requires a Pipeline (Workflow) job. Classic Freestyle jobs don't have stages.");
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
// 12. getStageLog
|
|
25
|
+
register("getStageLog", "Get the console log for a specific pipeline stage. Provide the stage name as it appears in getPipelineStages output.", z.object({
|
|
26
|
+
jobPath: z.string().describe("Full job path"),
|
|
27
|
+
buildNumber: z.number().describe("Build number"),
|
|
28
|
+
stageName: z.string().describe("Stage name (as shown in getPipelineStages)"),
|
|
29
|
+
}), async (args) => {
|
|
30
|
+
const jobPath = args.jobPath;
|
|
31
|
+
const buildNumber = args.buildNumber;
|
|
32
|
+
const stageName = args.stageName;
|
|
33
|
+
try {
|
|
34
|
+
// First, get the pipeline description to find the stage ID
|
|
35
|
+
const data = await client.get(jobPath, `/${buildNumber}/wfapi/describe`);
|
|
36
|
+
const run = data;
|
|
37
|
+
const stage = run.stages?.find((s) => s.name.toLowerCase() === stageName.toLowerCase());
|
|
38
|
+
if (!stage) {
|
|
39
|
+
const available = run.stages?.map((s) => s.name).join(", ") || "none";
|
|
40
|
+
return error(`Stage "${stageName}" not found.`, `Available stages: ${available}`);
|
|
41
|
+
}
|
|
42
|
+
// Get the stage log using the node ID
|
|
43
|
+
const logData = await client.getRaw(jobPath, `/${buildNumber}/execution/node/${stage.id}/wfapi/log`);
|
|
44
|
+
// The wfapi/log endpoint returns JSON with a text field
|
|
45
|
+
let logText;
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(logData);
|
|
48
|
+
logText = parsed.text;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// If it's not JSON, use raw text
|
|
52
|
+
logText = logData;
|
|
53
|
+
}
|
|
54
|
+
// Strip HTML tags that Jenkins sometimes includes
|
|
55
|
+
logText = logText.replace(/<[^>]*>/g, "");
|
|
56
|
+
return ok(truncateText(`--- Stage Log: ${stage.name} (${stage.status}, ${formatDuration(stage.durationMillis)}) ---\n\n${logText}`));
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
return handleError(e);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
// 13. getPipelineScript
|
|
63
|
+
register("getPipelineScript", "Get the Jenkinsfile/pipeline script used in a build. Fetches from the replay page. Useful for reviewing or modifying the script before replaying.", z.object({
|
|
64
|
+
jobPath: z.string().describe("Full job path"),
|
|
65
|
+
buildNumber: z.number().describe("Build number"),
|
|
66
|
+
}), async (args) => {
|
|
67
|
+
const jobPath = args.jobPath;
|
|
68
|
+
const buildNumber = args.buildNumber;
|
|
69
|
+
try {
|
|
70
|
+
const html = await client.getRaw(jobPath, `/${buildNumber}/replay`);
|
|
71
|
+
// Parse the main script from the replay page
|
|
72
|
+
// The replay page has a <textarea> with class "ace-editor" or name "_.mainScript"
|
|
73
|
+
const mainScriptMatch = html.match(/name="_.mainScript"[^>]*>([\s\S]*?)<\/textarea>/);
|
|
74
|
+
const scripts = [];
|
|
75
|
+
if (mainScriptMatch) {
|
|
76
|
+
// Decode HTML entities
|
|
77
|
+
const script = decodeHtmlEntities(mainScriptMatch[1]);
|
|
78
|
+
scripts.push({ name: "Jenkinsfile (main)", content: script });
|
|
79
|
+
}
|
|
80
|
+
// Also look for loaded library scripts
|
|
81
|
+
const scriptBlocks = html.matchAll(/name="_.([^"]+)"[^>]*class="[^"]*jenkins-readonly-crumb[^"]*"[^>]*>([\s\S]*?)<\/textarea>/g);
|
|
82
|
+
for (const match of scriptBlocks) {
|
|
83
|
+
if (match[1] !== "mainScript") {
|
|
84
|
+
scripts.push({
|
|
85
|
+
name: match[1],
|
|
86
|
+
content: decodeHtmlEntities(match[2]),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Try alternative pattern if nothing found
|
|
91
|
+
if (scripts.length === 0) {
|
|
92
|
+
// Try finding any textarea with script content
|
|
93
|
+
const textareaMatches = html.matchAll(/<textarea[^>]*name="([^"]*)"[^>]*>([\s\S]*?)<\/textarea>/g);
|
|
94
|
+
for (const match of textareaMatches) {
|
|
95
|
+
const name = match[1].replace("_.", "");
|
|
96
|
+
if (name && match[2].trim()) {
|
|
97
|
+
scripts.push({
|
|
98
|
+
name,
|
|
99
|
+
content: decodeHtmlEntities(match[2]),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (scripts.length === 0) {
|
|
105
|
+
return error("Could not extract pipeline script from replay page.", "The build may not be a Pipeline job, or replay may not be available.");
|
|
106
|
+
}
|
|
107
|
+
const lines = [];
|
|
108
|
+
for (const s of scripts) {
|
|
109
|
+
lines.push(`=== ${s.name} ===`);
|
|
110
|
+
lines.push(s.content);
|
|
111
|
+
lines.push("");
|
|
112
|
+
}
|
|
113
|
+
return ok(truncateText(lines.join("\n")));
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
return handleError(e, "Replay page may not be available. Ensure the build is a Pipeline job.");
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// 14. replayBuild
|
|
120
|
+
register("replayBuild", "Replay a pipeline build with optional script modifications. If no script is provided, replays with the same script. Use getPipelineScript first to get the current script, modify it, then replay.", z.object({
|
|
121
|
+
jobPath: z.string().describe("Full job path"),
|
|
122
|
+
buildNumber: z.number().describe("Build number to replay"),
|
|
123
|
+
mainScript: z.string().optional().describe("Modified Jenkinsfile content. If omitted, replays with the original script."),
|
|
124
|
+
}), async (args) => {
|
|
125
|
+
const jobPath = args.jobPath;
|
|
126
|
+
const buildNumber = args.buildNumber;
|
|
127
|
+
let mainScript = args.mainScript;
|
|
128
|
+
try {
|
|
129
|
+
// If no script provided, fetch current one
|
|
130
|
+
if (!mainScript) {
|
|
131
|
+
const html = await client.getRaw(jobPath, `/${buildNumber}/replay`);
|
|
132
|
+
const match = html.match(/name="_.mainScript"[^>]*>([\s\S]*?)<\/textarea>/);
|
|
133
|
+
if (match) {
|
|
134
|
+
mainScript = decodeHtmlEntities(match[1]);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
return error("Could not extract current pipeline script for replay.", "Try providing the script explicitly via the mainScript parameter.");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Submit replay
|
|
141
|
+
const formData = new URLSearchParams();
|
|
142
|
+
formData.set("mainScript", mainScript);
|
|
143
|
+
formData.set("json", JSON.stringify({ mainScript }));
|
|
144
|
+
const result = await client.postForm(jobPath, `/${buildNumber}/replay/run`, formData);
|
|
145
|
+
// The response redirects to the new build page
|
|
146
|
+
const location = result.response.headers.get("location");
|
|
147
|
+
if (location) {
|
|
148
|
+
return ok(`Replay triggered successfully.\nNew build: ${location}`);
|
|
149
|
+
}
|
|
150
|
+
return ok(`Replay triggered for ${jobPath} build #${buildNumber}.`);
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
return handleError(e, "Replay requires Pipeline job type and appropriate permissions.");
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
// 15. restartFromStage
|
|
157
|
+
register("restartFromStage", "Restart a pipeline from a specific stage. Requires the Declarative Pipeline plugin with 'Restart from Stage' support. Only works with top-level stages in Declarative pipelines and completed builds.", z.object({
|
|
158
|
+
jobPath: z.string().describe("Full job path"),
|
|
159
|
+
buildNumber: z.number().describe("Build number"),
|
|
160
|
+
stageName: z.string().describe("Stage name to restart from"),
|
|
161
|
+
}), async (args) => {
|
|
162
|
+
const jobPath = args.jobPath;
|
|
163
|
+
const buildNumber = args.buildNumber;
|
|
164
|
+
const stageName = args.stageName;
|
|
165
|
+
try {
|
|
166
|
+
// First verify the stage exists
|
|
167
|
+
const data = await client.get(jobPath, `/${buildNumber}/wfapi/describe`);
|
|
168
|
+
const run = data;
|
|
169
|
+
const stage = run.stages?.find((s) => s.name.toLowerCase() === stageName.toLowerCase());
|
|
170
|
+
if (!stage) {
|
|
171
|
+
const available = run.stages?.map((s) => s.name).join(", ") || "none";
|
|
172
|
+
return error(`Stage "${stageName}" not found.`, `Available stages: ${available}`);
|
|
173
|
+
}
|
|
174
|
+
// Try the restart endpoint
|
|
175
|
+
// This uses the Pipeline: Stage Step plugin's restart functionality
|
|
176
|
+
const formData = new URLSearchParams();
|
|
177
|
+
formData.set("stageName", stage.name);
|
|
178
|
+
formData.set("json", JSON.stringify({ stageName: stage.name }));
|
|
179
|
+
const result = await client.postForm(jobPath, `/${buildNumber}/restart/restart`, formData);
|
|
180
|
+
const location = result.response.headers.get("location");
|
|
181
|
+
if (location) {
|
|
182
|
+
return ok(`Restart from stage "${stage.name}" triggered.\nNew build: ${location}`);
|
|
183
|
+
}
|
|
184
|
+
return ok(`Restart from stage "${stage.name}" triggered for ${jobPath} build #${buildNumber}.`);
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
return handleError(e, "Restart from stage requires: (1) Declarative Pipeline plugin, (2) a completed build, (3) a top-level stage. Not all Jenkins installations support this.");
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function decodeHtmlEntities(text) {
|
|
192
|
+
return text
|
|
193
|
+
.replace(/&/g, "&")
|
|
194
|
+
.replace(/</g, "<")
|
|
195
|
+
.replace(/>/g, ">")
|
|
196
|
+
.replace(/"/g, '"')
|
|
197
|
+
.replace(/'/g, "'")
|
|
198
|
+
.replace(/'/g, "'")
|
|
199
|
+
.replace(///g, "/");
|
|
200
|
+
}
|
|
201
|
+
function handleError(e, suggestion) {
|
|
202
|
+
if (e && typeof e === "object" && "errorCode" in e) {
|
|
203
|
+
const je = e;
|
|
204
|
+
return error(`[${je.errorCode}] ${je.message}`, suggestion);
|
|
205
|
+
}
|
|
206
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
207
|
+
return error(msg, suggestion);
|
|
208
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
export interface JenkinsConfig {
|
|
2
|
+
url: string;
|
|
3
|
+
user: string;
|
|
4
|
+
token: string;
|
|
5
|
+
}
|
|
6
|
+
export interface JenkinsJob {
|
|
7
|
+
_class: string;
|
|
8
|
+
name: string;
|
|
9
|
+
url: string;
|
|
10
|
+
color: string;
|
|
11
|
+
description: string | null;
|
|
12
|
+
fullName: string;
|
|
13
|
+
buildable?: boolean;
|
|
14
|
+
healthReport?: Array<{
|
|
15
|
+
description: string;
|
|
16
|
+
score: number;
|
|
17
|
+
}>;
|
|
18
|
+
lastBuild?: {
|
|
19
|
+
number: number;
|
|
20
|
+
result: string | null;
|
|
21
|
+
timestamp: number;
|
|
22
|
+
url: string;
|
|
23
|
+
};
|
|
24
|
+
lastSuccessfulBuild?: {
|
|
25
|
+
number: number;
|
|
26
|
+
url: string;
|
|
27
|
+
};
|
|
28
|
+
lastFailedBuild?: {
|
|
29
|
+
number: number;
|
|
30
|
+
url: string;
|
|
31
|
+
};
|
|
32
|
+
jobs?: JenkinsJob[];
|
|
33
|
+
property?: Array<{
|
|
34
|
+
_class: string;
|
|
35
|
+
parameterDefinitions?: Array<{
|
|
36
|
+
name: string;
|
|
37
|
+
type: string;
|
|
38
|
+
description: string;
|
|
39
|
+
defaultParameterValue?: {
|
|
40
|
+
value: string;
|
|
41
|
+
};
|
|
42
|
+
}>;
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
export interface JenkinsBuild {
|
|
46
|
+
_class: string;
|
|
47
|
+
number: number;
|
|
48
|
+
url: string;
|
|
49
|
+
result: string | null;
|
|
50
|
+
building: boolean;
|
|
51
|
+
duration: number;
|
|
52
|
+
estimatedDuration: number;
|
|
53
|
+
timestamp: number;
|
|
54
|
+
displayName: string;
|
|
55
|
+
description: string | null;
|
|
56
|
+
fullDisplayName: string;
|
|
57
|
+
actions?: Array<{
|
|
58
|
+
_class: string;
|
|
59
|
+
causes?: Array<{
|
|
60
|
+
shortDescription: string;
|
|
61
|
+
userName?: string;
|
|
62
|
+
}>;
|
|
63
|
+
}>;
|
|
64
|
+
artifacts?: BuildArtifact[];
|
|
65
|
+
changeSets?: Array<{
|
|
66
|
+
items: Array<{
|
|
67
|
+
msg: string;
|
|
68
|
+
author: {
|
|
69
|
+
fullName: string;
|
|
70
|
+
};
|
|
71
|
+
commitId: string;
|
|
72
|
+
}>;
|
|
73
|
+
}>;
|
|
74
|
+
}
|
|
75
|
+
export interface BuildArtifact {
|
|
76
|
+
displayPath: string;
|
|
77
|
+
fileName: string;
|
|
78
|
+
relativePath: string;
|
|
79
|
+
}
|
|
80
|
+
export interface PipelineStage {
|
|
81
|
+
id: string;
|
|
82
|
+
name: string;
|
|
83
|
+
status: string;
|
|
84
|
+
startTimeMillis: number;
|
|
85
|
+
durationMillis: number;
|
|
86
|
+
pauseDurationMillis: number;
|
|
87
|
+
stageFlowNodes?: Array<{
|
|
88
|
+
id: string;
|
|
89
|
+
name: string;
|
|
90
|
+
status: {
|
|
91
|
+
result: string;
|
|
92
|
+
};
|
|
93
|
+
}>;
|
|
94
|
+
}
|
|
95
|
+
export interface PipelineRun {
|
|
96
|
+
id: string;
|
|
97
|
+
name: string;
|
|
98
|
+
status: string;
|
|
99
|
+
startTimeMillis: number;
|
|
100
|
+
durationMillis: number;
|
|
101
|
+
stages: PipelineStage[];
|
|
102
|
+
}
|
|
103
|
+
export interface TestResult {
|
|
104
|
+
failCount: number;
|
|
105
|
+
passCount: number;
|
|
106
|
+
skipCount: number;
|
|
107
|
+
totalCount: number;
|
|
108
|
+
suites?: Array<{
|
|
109
|
+
name: string;
|
|
110
|
+
cases: Array<{
|
|
111
|
+
className: string;
|
|
112
|
+
name: string;
|
|
113
|
+
status: string;
|
|
114
|
+
duration: number;
|
|
115
|
+
errorDetails?: string;
|
|
116
|
+
errorStackTrace?: string;
|
|
117
|
+
}>;
|
|
118
|
+
}>;
|
|
119
|
+
}
|
|
120
|
+
export interface QueueItem {
|
|
121
|
+
id: number;
|
|
122
|
+
task: {
|
|
123
|
+
name: string;
|
|
124
|
+
url: string;
|
|
125
|
+
};
|
|
126
|
+
why: string | null;
|
|
127
|
+
buildableStartMilliseconds: number;
|
|
128
|
+
stuck: boolean;
|
|
129
|
+
blocked: boolean;
|
|
130
|
+
}
|
|
131
|
+
export interface JenkinsError {
|
|
132
|
+
statusCode: number;
|
|
133
|
+
message: string;
|
|
134
|
+
errorCode: string;
|
|
135
|
+
}
|
|
136
|
+
export interface ToolResult {
|
|
137
|
+
[key: string]: unknown;
|
|
138
|
+
content: Array<{
|
|
139
|
+
type: "text";
|
|
140
|
+
text: string;
|
|
141
|
+
}>;
|
|
142
|
+
isError?: boolean;
|
|
143
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { JenkinsJob, JenkinsBuild, PipelineStage, QueueItem, ToolResult } from "../types.js";
|
|
2
|
+
export declare function truncateText(text: string, maxBytes?: number): string;
|
|
3
|
+
export declare function formatJobStatus(color: string): string;
|
|
4
|
+
export declare function formatTimestamp(ts: number): string;
|
|
5
|
+
export declare function formatDuration(ms: number): string;
|
|
6
|
+
export declare function formatJobList(jobs: JenkinsJob[]): string;
|
|
7
|
+
export declare function formatJobDetail(job: JenkinsJob): string;
|
|
8
|
+
export declare function formatBuild(build: JenkinsBuild): string;
|
|
9
|
+
export declare function formatStages(stages: PipelineStage[]): string;
|
|
10
|
+
export declare function formatQueue(items: QueueItem[]): string;
|
|
11
|
+
export declare function ok(text: string): ToolResult;
|
|
12
|
+
export declare function error(message: string, suggestion?: string): ToolResult;
|