feat(P05): ship infrastructure — Gitea API client, release notes, npm publishConfig, ciagent projects cmd, --project flag
---ci--- phase: 5 milestone: v1.0 plan: 05 task: SHIP-01-04 MULTI-01 MULTI-02 status: execute ---/ci---
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
export interface GiteaReleaseConfig {
|
||||
baseUrl: string;
|
||||
token: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
}
|
||||
|
||||
export interface GiteaRelease {
|
||||
id: number;
|
||||
tag_name: string;
|
||||
name: string;
|
||||
body: string;
|
||||
url: string;
|
||||
html_url: string;
|
||||
draft: boolean;
|
||||
prerelease: boolean;
|
||||
}
|
||||
|
||||
export class GiteaClient {
|
||||
private config: GiteaReleaseConfig;
|
||||
|
||||
constructor(config: GiteaReleaseConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async createRelease(params: {
|
||||
tag_name: string;
|
||||
name: string;
|
||||
body: string;
|
||||
draft?: boolean;
|
||||
prerelease?: boolean;
|
||||
}): Promise<GiteaRelease> {
|
||||
const url = `${this.config.baseUrl}/api/v1/repos/${this.config.owner}/${this.config.repo}/releases`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `token ${this.config.token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tag_name: params.tag_name,
|
||||
name: params.name,
|
||||
body: params.body,
|
||||
draft: params.draft ?? false,
|
||||
prerelease: params.prerelease ?? false,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`Gitea API error: ${response.status} ${text}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<GiteaRelease>;
|
||||
}
|
||||
|
||||
async listReleases(): Promise<GiteaRelease[]> {
|
||||
const url = `${this.config.baseUrl}/api/v1/repos/${this.config.owner}/${this.config.repo}/releases`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Authorization": `token ${this.config.token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Gitea API error: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<GiteaRelease[]>;
|
||||
}
|
||||
|
||||
async getReleaseByTag(tag: string): Promise<GiteaRelease | null> {
|
||||
const url = `${this.config.baseUrl}/api/v1/repos/${this.config.owner}/${this.config.repo}/releases/tags/${tag}`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Authorization": `token ${this.config.token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Gitea API error: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<GiteaRelease>;
|
||||
}
|
||||
}
|
||||
|
||||
export function generateReleaseNotes(projectPath: string, fromTag: string | null, toTag: string): string {
|
||||
let gitLogCmd: string;
|
||||
if (fromTag) {
|
||||
gitLogCmd = `git log ${fromTag}..${toTag} --oneline`;
|
||||
} else {
|
||||
gitLogCmd = `git log ${toTag} --oneline`;
|
||||
}
|
||||
|
||||
let logOutput: string;
|
||||
try {
|
||||
logOutput = execSync(gitLogCmd, { cwd: projectPath, encoding: "utf-8" }).trim();
|
||||
} catch {
|
||||
return `## What's Changed\n\nNo commits found between ${fromTag || "beginning"} and ${toTag}.\n`;
|
||||
}
|
||||
|
||||
if (!logOutput) {
|
||||
return `## What's Changed\n\nNo commits found between ${fromTag || "beginning"} and ${toTag}.\n`;
|
||||
}
|
||||
|
||||
const lines = logOutput.split("\n").filter(Boolean);
|
||||
|
||||
const featCommits: string[] = [];
|
||||
const fixCommits: string[] = [];
|
||||
const testCommits: string[] = [];
|
||||
const otherCommits: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const subject = line.replace(/^[a-f0-9]+\s+/, "");
|
||||
if (/^feat/i.test(subject)) {
|
||||
featCommits.push(subject);
|
||||
} else if (/^fix/i.test(subject)) {
|
||||
fixCommits.push(subject);
|
||||
} else if (/^test/i.test(subject)) {
|
||||
testCommits.push(subject);
|
||||
} else {
|
||||
otherCommits.push(subject);
|
||||
}
|
||||
}
|
||||
|
||||
const sections: string[] = [];
|
||||
|
||||
if (featCommits.length > 0) {
|
||||
sections.push("### New Features\n");
|
||||
for (const c of featCommits) {
|
||||
sections.push(`- ${c}`);
|
||||
}
|
||||
sections.push("");
|
||||
}
|
||||
|
||||
if (fixCommits.length > 0) {
|
||||
sections.push("### Bug Fixes\n");
|
||||
for (const c of fixCommits) {
|
||||
sections.push(`- ${c}`);
|
||||
}
|
||||
sections.push("");
|
||||
}
|
||||
|
||||
if (testCommits.length > 0) {
|
||||
sections.push("### Tests\n");
|
||||
for (const c of testCommits) {
|
||||
sections.push(`- ${c}`);
|
||||
}
|
||||
sections.push("");
|
||||
}
|
||||
|
||||
if (otherCommits.length > 0) {
|
||||
sections.push("### Other Changes\n");
|
||||
for (const c of otherCommits) {
|
||||
sections.push(`- ${c}`);
|
||||
}
|
||||
sections.push("");
|
||||
}
|
||||
|
||||
return `## What's Changed\n\n${sections.join("\n")}`;
|
||||
}
|
||||
Reference in New Issue
Block a user