Merge pull request #64 from zereight/feat/pipeline-support
feat: add pipeline management commands
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
|
48. `list_pipeline_jobs` - List all jobs in a specific pipeline
|
||||||
49. `get_pipeline_job` - Get details of a GitLab pipeline job number
|
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
|
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
|
51. `create_pipeline` - Create a new pipeline for a branch or tag
|
||||||
52. `list_milestones` - List milestones in a GitLab project with filtering options
|
52. `retry_pipeline` - Retry a failed or canceled pipeline
|
||||||
53. `get_milestone` - Get details of a specific milestone
|
53. `cancel_pipeline` - Cancel a running pipeline
|
||||||
54. `create_milestone` - Create a new milestone in a GitLab project
|
54. `list_merge_requests` - List merge requests in a GitLab project with filtering options
|
||||||
55. `edit_milestone ` - Edit an existing milestone in a GitLab project
|
55. `list_milestones` - List milestones in a GitLab project with filtering options
|
||||||
56. `delete_milestone` - Delete a milestone from a GitLab project
|
56. `get_milestone` - Get details of a specific milestone
|
||||||
57. `get_milestone_issue` - Get issues associated with a specific milestone
|
57. `create_milestone` - Create a new milestone in a GitLab project
|
||||||
58. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone
|
58. `edit_milestone ` - Edit an existing milestone in a GitLab project
|
||||||
59. `promote_milestone` - Promote a milestone to the next stage
|
59. `delete_milestone` - Delete a milestone from a GitLab project
|
||||||
60. `get_milestone_burndown_events` - Get burndown events for a specific milestone
|
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 -->
|
<!-- TOOLS-END -->
|
||||||
|
141
index.ts
141
index.ts
@ -86,6 +86,9 @@ import {
|
|||||||
GetPipelineSchema,
|
GetPipelineSchema,
|
||||||
ListPipelinesSchema,
|
ListPipelinesSchema,
|
||||||
ListPipelineJobsSchema,
|
ListPipelineJobsSchema,
|
||||||
|
CreatePipelineSchema,
|
||||||
|
RetryPipelineSchema,
|
||||||
|
CancelPipelineSchema,
|
||||||
// pipeline job schemas
|
// pipeline job schemas
|
||||||
GetPipelineJobOutputSchema,
|
GetPipelineJobOutputSchema,
|
||||||
GitLabPipelineJobSchema,
|
GitLabPipelineJobSchema,
|
||||||
@ -117,6 +120,9 @@ import {
|
|||||||
type ListPipelinesOptions,
|
type ListPipelinesOptions,
|
||||||
type GetPipelineOptions,
|
type GetPipelineOptions,
|
||||||
type ListPipelineJobsOptions,
|
type ListPipelineJobsOptions,
|
||||||
|
type CreatePipelineOptions,
|
||||||
|
type RetryPipelineOptions,
|
||||||
|
type CancelPipelineOptions,
|
||||||
type GitLabPipelineJob,
|
type GitLabPipelineJob,
|
||||||
type GitLabMilestones,
|
type GitLabMilestones,
|
||||||
type ListProjectMilestonesOptions,
|
type ListProjectMilestonesOptions,
|
||||||
@ -482,6 +488,21 @@ const allTools = [
|
|||||||
description: "Get the output/trace of a GitLab pipeline job number",
|
description: "Get the output/trace of a GitLab pipeline job number",
|
||||||
inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema),
|
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",
|
name: "list_merge_requests",
|
||||||
description: "List merge requests in a GitLab project with filtering options",
|
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();
|
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
|
* Get the repository tree for a project
|
||||||
* @param {string} projectId - The ID or URL-encoded path of the 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": {
|
case "list_merge_requests": {
|
||||||
const args = ListMergeRequestsSchema.parse(request.params.arguments);
|
const args = ListMergeRequestsSchema.parse(request.params.arguments);
|
||||||
const mergeRequests = await listMergeRequests(args.project_id, args);
|
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)"),
|
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
|
// Schema for the input parameters for pipeline job operations
|
||||||
export const GetPipelineJobOutputSchema = z.object({
|
export const GetPipelineJobOutputSchema = z.object({
|
||||||
project_id: z.string().describe("Project ID or URL-encoded path"),
|
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 ListPipelinesOptions = z.infer<typeof ListPipelinesSchema>;
|
||||||
export type GetPipelineOptions = z.infer<typeof GetPipelineSchema>;
|
export type GetPipelineOptions = z.infer<typeof GetPipelineSchema>;
|
||||||
export type ListPipelineJobsOptions = z.infer<typeof ListPipelineJobsSchema>;
|
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 GitLabMilestones = z.infer<typeof GitLabMilestonesSchema>;
|
||||||
export type ListProjectMilestonesOptions = z.infer<typeof ListProjectMilestonesSchema>;
|
export type ListProjectMilestonesOptions = z.infer<typeof ListProjectMilestonesSchema>;
|
||||||
export type GetProjectMilestoneOptions = z.infer<typeof GetProjectMilestoneSchema>;
|
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`,
|
url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/repository/branches?per_page=1`,
|
||||||
validate: data => Array.isArray(data),
|
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 allPassed = true;
|
||||||
|
let firstPipelineId = null;
|
||||||
|
|
||||||
for (const test of tests) {
|
for (const test of tests) {
|
||||||
try {
|
try {
|
||||||
@ -65,6 +71,11 @@ async function validateGitLabAPI() {
|
|||||||
|
|
||||||
if (test.validate(data)) {
|
if (test.validate(data)) {
|
||||||
console.log(`✅ ${test.name} - PASSED\n`);
|
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 {
|
} else {
|
||||||
console.log(`❌ ${test.name} - FAILED (invalid response format)\n`);
|
console.log(`❌ ${test.name} - FAILED (invalid response format)\n`);
|
||||||
allPassed = false;
|
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) {
|
if (allPassed) {
|
||||||
console.log("✅ All API validation tests passed!");
|
console.log("✅ All API validation tests passed!");
|
||||||
} else {
|
} else {
|
||||||
|
Reference in New Issue
Block a user