[feat/pipeline-support] feat: add pipeline management commands
- Add create_pipeline command to trigger new pipelines - Add retry_pipeline command to retry failed pipelines - Add cancel_pipeline command to cancel running pipelines - Add pipeline tests to validate-api.js - Update README with new pipeline commands Closes #46
This commit is contained in:
23
README.md
23
README.md
@ -137,14 +137,17 @@ $ sh scripts/image_push.sh docker_user_name
|
||||
48. `list_pipeline_jobs` - List all jobs in a specific pipeline
|
||||
49. `get_pipeline_job` - Get details of a GitLab pipeline job number
|
||||
50. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job number
|
||||
51. `list_merge_requests` - List merge requests in a GitLab project with filtering options
|
||||
52. `list_milestones` - List milestones in a GitLab project with filtering options
|
||||
53. `get_milestone` - Get details of a specific milestone
|
||||
54. `create_milestone` - Create a new milestone in a GitLab project
|
||||
55. `edit_milestone ` - Edit an existing milestone in a GitLab project
|
||||
56. `delete_milestone` - Delete a milestone from a GitLab project
|
||||
57. `get_milestone_issue` - Get issues associated with a specific milestone
|
||||
58. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone
|
||||
59. `promote_milestone` - Promote a milestone to the next stage
|
||||
60. `get_milestone_burndown_events` - Get burndown events for a specific milestone
|
||||
51. `create_pipeline` - Create a new pipeline for a branch or tag
|
||||
52. `retry_pipeline` - Retry a failed or canceled pipeline
|
||||
53. `cancel_pipeline` - Cancel a running pipeline
|
||||
54. `list_merge_requests` - List merge requests in a GitLab project with filtering options
|
||||
55. `list_milestones` - List milestones in a GitLab project with filtering options
|
||||
56. `get_milestone` - Get details of a specific milestone
|
||||
57. `create_milestone` - Create a new milestone in a GitLab project
|
||||
58. `edit_milestone ` - Edit an existing milestone in a GitLab project
|
||||
59. `delete_milestone` - Delete a milestone from a GitLab project
|
||||
60. `get_milestone_issue` - Get issues associated with a specific milestone
|
||||
61. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone
|
||||
62. `promote_milestone` - Promote a milestone to the next stage
|
||||
63. `get_milestone_burndown_events` - Get burndown events for a specific milestone
|
||||
<!-- TOOLS-END -->
|
||||
|
141
index.ts
141
index.ts
@ -86,6 +86,9 @@ import {
|
||||
GetPipelineSchema,
|
||||
ListPipelinesSchema,
|
||||
ListPipelineJobsSchema,
|
||||
CreatePipelineSchema,
|
||||
RetryPipelineSchema,
|
||||
CancelPipelineSchema,
|
||||
// pipeline job schemas
|
||||
GetPipelineJobOutputSchema,
|
||||
GitLabPipelineJobSchema,
|
||||
@ -117,6 +120,9 @@ import {
|
||||
type ListPipelinesOptions,
|
||||
type GetPipelineOptions,
|
||||
type ListPipelineJobsOptions,
|
||||
type CreatePipelineOptions,
|
||||
type RetryPipelineOptions,
|
||||
type CancelPipelineOptions,
|
||||
type GitLabPipelineJob,
|
||||
type GitLabMilestones,
|
||||
type ListProjectMilestonesOptions,
|
||||
@ -482,6 +488,21 @@ const allTools = [
|
||||
description: "Get the output/trace of a GitLab pipeline job number",
|
||||
inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema),
|
||||
},
|
||||
{
|
||||
name: "create_pipeline",
|
||||
description: "Create a new pipeline for a branch or tag",
|
||||
inputSchema: zodToJsonSchema(CreatePipelineSchema),
|
||||
},
|
||||
{
|
||||
name: "retry_pipeline",
|
||||
description: "Retry a failed or canceled pipeline",
|
||||
inputSchema: zodToJsonSchema(RetryPipelineSchema),
|
||||
},
|
||||
{
|
||||
name: "cancel_pipeline",
|
||||
description: "Cancel a running pipeline",
|
||||
inputSchema: zodToJsonSchema(CancelPipelineSchema),
|
||||
},
|
||||
{
|
||||
name: "list_merge_requests",
|
||||
description: "List merge requests in a GitLab project with filtering options",
|
||||
@ -2484,6 +2505,87 @@ async function getPipelineJobOutput(projectId: string, jobId: number): Promise<s
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new pipeline
|
||||
*
|
||||
* @param {string} projectId - The ID or URL-encoded path of the project
|
||||
* @param {string} ref - The branch or tag to run the pipeline on
|
||||
* @param {Array} variables - Optional variables for the pipeline
|
||||
* @returns {Promise<GitLabPipeline>} The created pipeline
|
||||
*/
|
||||
async function createPipeline(
|
||||
projectId: string,
|
||||
ref: string,
|
||||
variables?: Array<{ key: string; value: string }>
|
||||
): Promise<GitLabPipeline> {
|
||||
projectId = decodeURIComponent(projectId); // Decode project ID
|
||||
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipeline`);
|
||||
|
||||
const body: any = { ref };
|
||||
if (variables && variables.length > 0) {
|
||||
body.variables = variables.reduce((acc, { key, value }) => {
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: DEFAULT_HEADERS,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
await handleGitLabError(response);
|
||||
const data = await response.json();
|
||||
return GitLabPipelineSchema.parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry a pipeline
|
||||
*
|
||||
* @param {string} projectId - The ID or URL-encoded path of the project
|
||||
* @param {number} pipelineId - The ID of the pipeline to retry
|
||||
* @returns {Promise<GitLabPipeline>} The retried pipeline
|
||||
*/
|
||||
async function retryPipeline(projectId: string, pipelineId: number): Promise<GitLabPipeline> {
|
||||
projectId = decodeURIComponent(projectId); // Decode project ID
|
||||
const url = new URL(
|
||||
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/retry`
|
||||
);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: DEFAULT_HEADERS,
|
||||
});
|
||||
|
||||
await handleGitLabError(response);
|
||||
const data = await response.json();
|
||||
return GitLabPipelineSchema.parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a pipeline
|
||||
*
|
||||
* @param {string} projectId - The ID or URL-encoded path of the project
|
||||
* @param {number} pipelineId - The ID of the pipeline to cancel
|
||||
* @returns {Promise<GitLabPipeline>} The canceled pipeline
|
||||
*/
|
||||
async function cancelPipeline(projectId: string, pipelineId: number): Promise<GitLabPipeline> {
|
||||
projectId = decodeURIComponent(projectId); // Decode project ID
|
||||
const url = new URL(
|
||||
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/cancel`
|
||||
);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: DEFAULT_HEADERS,
|
||||
});
|
||||
|
||||
await handleGitLabError(response);
|
||||
const data = await response.json();
|
||||
return GitLabPipelineSchema.parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the repository tree for a project
|
||||
* @param {string} projectId - The ID or URL-encoded path of the project
|
||||
@ -3409,6 +3511,45 @@ server.setRequestHandler(CallToolRequestSchema, async request => {
|
||||
};
|
||||
}
|
||||
|
||||
case "create_pipeline": {
|
||||
const { project_id, ref, variables } = CreatePipelineSchema.parse(request.params.arguments);
|
||||
const pipeline = await createPipeline(project_id, ref, variables);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Created pipeline #${pipeline.id} for ${ref}. Status: ${pipeline.status}\nWeb URL: ${pipeline.web_url}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "retry_pipeline": {
|
||||
const { project_id, pipeline_id } = RetryPipelineSchema.parse(request.params.arguments);
|
||||
const pipeline = await retryPipeline(project_id, pipeline_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Retried pipeline #${pipeline.id}. Status: ${pipeline.status}\nWeb URL: ${pipeline.web_url}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "cancel_pipeline": {
|
||||
const { project_id, pipeline_id } = CancelPipelineSchema.parse(request.params.arguments);
|
||||
const pipeline = await cancelPipeline(project_id, pipeline_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Canceled pipeline #${pipeline.id}. Status: ${pipeline.status}\nWeb URL: ${pipeline.web_url}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "list_merge_requests": {
|
||||
const args = ListMergeRequestsSchema.parse(request.params.arguments);
|
||||
const mergeRequests = await listMergeRequests(args.project_id, args);
|
||||
|
30
schemas.ts
30
schemas.ts
@ -157,6 +157,33 @@ export const ListPipelineJobsSchema = z.object({
|
||||
per_page: z.number().optional().describe("Number of items per page (max 100)"),
|
||||
});
|
||||
|
||||
// Schema for creating a new pipeline
|
||||
export const CreatePipelineSchema = z.object({
|
||||
project_id: z.string().describe("Project ID or URL-encoded path"),
|
||||
ref: z.string().describe("The branch or tag to run the pipeline on"),
|
||||
variables: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string().describe("The key of the variable"),
|
||||
value: z.string().describe("The value of the variable"),
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.describe("An array of variables to use for the pipeline"),
|
||||
});
|
||||
|
||||
// Schema for retrying a pipeline
|
||||
export const RetryPipelineSchema = z.object({
|
||||
project_id: z.string().describe("Project ID or URL-encoded path"),
|
||||
pipeline_id: z.number().describe("The ID of the pipeline to retry"),
|
||||
});
|
||||
|
||||
// Schema for canceling a pipeline
|
||||
export const CancelPipelineSchema = z.object({
|
||||
project_id: z.string().describe("Project ID or URL-encoded path"),
|
||||
pipeline_id: z.number().describe("The ID of the pipeline to cancel"),
|
||||
});
|
||||
|
||||
// Schema for the input parameters for pipeline job operations
|
||||
export const GetPipelineJobOutputSchema = z.object({
|
||||
project_id: z.string().describe("Project ID or URL-encoded path"),
|
||||
@ -1281,6 +1308,9 @@ export type GitLabPipeline = z.infer<typeof GitLabPipelineSchema>;
|
||||
export type ListPipelinesOptions = z.infer<typeof ListPipelinesSchema>;
|
||||
export type GetPipelineOptions = z.infer<typeof GetPipelineSchema>;
|
||||
export type ListPipelineJobsOptions = z.infer<typeof ListPipelineJobsSchema>;
|
||||
export type CreatePipelineOptions = z.infer<typeof CreatePipelineSchema>;
|
||||
export type RetryPipelineOptions = z.infer<typeof RetryPipelineSchema>;
|
||||
export type CancelPipelineOptions = z.infer<typeof CancelPipelineSchema>;
|
||||
export type GitLabMilestones = z.infer<typeof GitLabMilestonesSchema>;
|
||||
export type ListProjectMilestonesOptions = z.infer<typeof ListProjectMilestonesSchema>;
|
||||
export type GetProjectMilestoneOptions = z.infer<typeof GetProjectMilestoneSchema>;
|
||||
|
@ -43,9 +43,15 @@ async function validateGitLabAPI() {
|
||||
url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/repository/branches?per_page=1`,
|
||||
validate: data => Array.isArray(data),
|
||||
},
|
||||
{
|
||||
name: "List pipelines",
|
||||
url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines?per_page=5`,
|
||||
validate: data => Array.isArray(data),
|
||||
},
|
||||
];
|
||||
|
||||
let allPassed = true;
|
||||
let firstPipelineId = null;
|
||||
|
||||
for (const test of tests) {
|
||||
try {
|
||||
@ -65,6 +71,11 @@ async function validateGitLabAPI() {
|
||||
|
||||
if (test.validate(data)) {
|
||||
console.log(`✅ ${test.name} - PASSED\n`);
|
||||
|
||||
// If we found pipelines, save the first one for additional testing
|
||||
if (test.name === "List pipelines" && data.length > 0) {
|
||||
firstPipelineId = data[0].id;
|
||||
}
|
||||
} else {
|
||||
console.log(`❌ ${test.name} - FAILED (invalid response format)\n`);
|
||||
allPassed = false;
|
||||
@ -76,6 +87,53 @@ async function validateGitLabAPI() {
|
||||
}
|
||||
}
|
||||
|
||||
// Test pipeline-specific endpoints if we have a pipeline ID
|
||||
if (firstPipelineId) {
|
||||
console.log(`Found pipeline #${firstPipelineId}, testing pipeline-specific endpoints...\n`);
|
||||
|
||||
const pipelineTests = [
|
||||
{
|
||||
name: `Get pipeline #${firstPipelineId} details`,
|
||||
url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines/${firstPipelineId}`,
|
||||
validate: data => data.id === firstPipelineId && data.status,
|
||||
},
|
||||
{
|
||||
name: `List pipeline #${firstPipelineId} jobs`,
|
||||
url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines/${firstPipelineId}/jobs`,
|
||||
validate: data => Array.isArray(data),
|
||||
},
|
||||
];
|
||||
|
||||
for (const test of pipelineTests) {
|
||||
try {
|
||||
console.log(`Testing: ${test.name}`);
|
||||
const response = await fetch(test.url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${GITLAB_TOKEN}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (test.validate(data)) {
|
||||
console.log(`✅ ${test.name} - PASSED\n`);
|
||||
} else {
|
||||
console.log(`❌ ${test.name} - FAILED (invalid response format)\n`);
|
||||
allPassed = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`❌ ${test.name} - FAILED`);
|
||||
console.log(` Error: ${error.message}\n`);
|
||||
allPassed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allPassed) {
|
||||
console.log("✅ All API validation tests passed!");
|
||||
} else {
|
||||
|
Reference in New Issue
Block a user