Merge pull request #64 from zereight/feat/pipeline-support

feat: add pipeline management commands
This commit is contained in:
zereight
2025-05-30 00:42:09 +09:00
committed by GitHub
4 changed files with 242 additions and 10 deletions

View File

@ -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
View File

@ -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);

View File

@ -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>;

View File

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