diff --git a/src/index.ts b/src/index.ts index b89d536..c3da8b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -750,6 +750,131 @@ server.tool( async ({ owner, repo, run_id, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/actions/runs/${run_id}`)), ); +server.tool( + 'gitea_actions_dispatch', + 'Trigger a workflow dispatch (e.g. pre-release, deploy)', + { + ...OwnerRepo, + workflow: z.string().describe('Workflow filename (e.g. pre-release.yml)'), + ref: z.string().describe('Branch or tag to run on (e.g. dev, main)'), + inputs: z.record(z.string()).optional().describe('Workflow input key-value pairs'), + ...ConnectionParam, + }, + async ({ owner, repo, workflow, ref, inputs, connection }) => + formatResponse(await clientFor(connection).post( + `/repos/${owner}/${repo}/actions/workflows/${workflow}/dispatches`, + { ref, inputs: inputs ?? {} }, + )), +); + +server.tool( + 'gitea_actions_jobs_list', + 'List jobs for a workflow run', + { ...OwnerRepo, run_id: z.number().describe('Run ID'), ...ConnectionParam }, + async ({ owner, repo, run_id, connection }) => + formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/actions/runs/${run_id}/jobs`)), +); + +server.tool( + 'gitea_actions_job_logs', + 'Get log output for a workflow job', + { ...OwnerRepo, job_id: z.number().describe('Job ID'), ...ConnectionParam }, + async ({ owner, repo, job_id, connection }) => { + const client = clientFor(connection); + const res = await client.get(`/repos/${owner}/${repo}/actions/jobs/${job_id}/logs`); + if (res.status >= 400) return formatResponse(res); + // Logs come as plain text + const text = typeof res.data === 'string' ? res.data : JSON.stringify(res.data); + return { content: [{ type: 'text' as const, text }] }; + }, +); + +server.tool( + 'gitea_release_asset_upload', + 'Upload a file as a release asset (provide base64-encoded content)', + { + ...OwnerRepo, + release_id: z.number().describe('Release ID'), + name: z.string().describe('Asset filename'), + content_base64: z.string().describe('Base64-encoded file content'), + ...ConnectionParam, + }, + async ({ owner, repo, release_id, name, content_base64, connection }) => { + const client = clientFor(connection); + // Gitea expects multipart form data for asset upload + // For now, use the API with the binary content + const res = await client.post( + `/repos/${owner}/${repo}/releases/${release_id}/assets?name=${encodeURIComponent(name)}`, + Buffer.from(content_base64, 'base64'), + ); + return formatResponse(res); + }, +); + +server.tool( + 'gitea_release_asset_delete', + 'Delete a release asset', + { + ...OwnerRepo, + release_id: z.number().describe('Release ID'), + asset_id: z.number().describe('Asset ID'), + ...ConnectionParam, + }, + async ({ owner, repo, release_id, asset_id, connection }) => + formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/releases/${release_id}/assets/${asset_id}`)), +); + +server.tool( + 'gitea_bulk_file_push', + 'Push the same file content to multiple repos (uses Contents API)', + { + owner: z.string().describe('Organization name'), + repos: z.array(z.string()).describe('List of repository names'), + path: z.string().describe('File path in each repo (e.g. .mokogitea/workflows/pre-release.yml)'), + content_base64: z.string().describe('Base64-encoded file content'), + message: z.string().describe('Commit message'), + branch: z.string().optional().describe('Target branch (default: main)'), + ...ConnectionParam, + }, + async ({ owner, repos, path, content_base64, message, branch, connection }) => { + const client = clientFor(connection); + const targetBranch = branch ?? 'main'; + const results: Array<{ repo: string; status: string }> = []; + + for (const repo of repos) { + try { + // Get current file SHA + const existing = await client.get(`/repos/${owner}/${repo}/contents/${path}?ref=${targetBranch}`); + const sha = (existing.data as { sha?: string })?.sha; + + if (sha) { + // Update existing file + await client.put(`/repos/${owner}/${repo}/contents/${path}`, { + content: content_base64, + sha, + message, + branch: targetBranch, + }); + results.push({ repo, status: 'updated' }); + } else { + // Create new file + await client.post(`/repos/${owner}/${repo}/contents/${path}`, { + content: content_base64, + message, + branch: targetBranch, + }); + results.push({ repo, status: 'created' }); + } + } catch (e) { + results.push({ repo, status: `error: ${e}` }); + } + } + + const summary = results.map(r => `${r.repo}: ${r.status}`).join('\n'); + return { content: [{ type: 'text' as const, text: summary }] }; + }, +); + // ── Organizations ─────────────────────────────────────────────────────── server.tool(