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:
@@ -15,6 +15,8 @@ import { PipelineState, createInitialPipelineState } from "../types/pipeline.js"
|
||||
import { resolveBackend } from "../backends/index.js";
|
||||
import { BackendUnavailableError } from "../backends/types.js";
|
||||
import { getAgent } from "../agents/index.js";
|
||||
import { CIAgentFiles } from "../core/ciagent-files.js";
|
||||
import { GiteaClient, generateReleaseNotes } from "../core/gitea.js";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { execSync } from "node:child_process";
|
||||
@@ -642,6 +644,83 @@ export function createRollbackCommand(): Command {
|
||||
});
|
||||
}
|
||||
|
||||
export function createProjectsCommand(): Command {
|
||||
const cmd = new Command("projects");
|
||||
cmd.description("Manage CIAgent projects in multi-project mode");
|
||||
|
||||
cmd.command("list")
|
||||
.description("List all registered projects")
|
||||
.action(() => {
|
||||
const projectPath = process.cwd();
|
||||
|
||||
if (!isCIAgentInitialized(projectPath)) {
|
||||
console.error("CIAgent project not initialized. Run 'ciagent init' first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config = loadConfig(projectPath);
|
||||
const ciFiles = new CIAgentFiles(projectPath);
|
||||
const projects = ciFiles.listProjects();
|
||||
const activeProject = config.active_project || ciFiles.getActiveProject();
|
||||
|
||||
if (projects.length === 0) {
|
||||
console.log("No projects registered.");
|
||||
console.log("Use 'ciagent projects add <slug> <name>' to add a project.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("─── CIAgent Projects ───\n");
|
||||
for (const project of projects) {
|
||||
const isActive = project.slug === activeProject;
|
||||
const marker = isActive ? " *" : "";
|
||||
console.log(` ${project.slug} — ${project.name}${marker}`);
|
||||
}
|
||||
console.log("\n * = active project");
|
||||
});
|
||||
|
||||
cmd.command("add <slug> <name>")
|
||||
.description("Add a new project")
|
||||
.action((slug: string, name: string) => {
|
||||
const projectPath = process.cwd();
|
||||
|
||||
if (!isCIAgentInitialized(projectPath)) {
|
||||
console.error("CIAgent project not initialized. Run 'ciagent init' first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const ciFiles = new CIAgentFiles(projectPath);
|
||||
ciFiles.addProject(slug, name);
|
||||
console.log(`✓ Project added: ${slug} (${name})`);
|
||||
});
|
||||
|
||||
cmd.command("set <slug>")
|
||||
.description("Set the active project")
|
||||
.action((slug: string) => {
|
||||
const projectPath = process.cwd();
|
||||
|
||||
if (!isCIAgentInitialized(projectPath)) {
|
||||
console.error("CIAgent project not initialized. Run 'ciagent init' first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const ciFiles = new CIAgentFiles(projectPath);
|
||||
const projects = ciFiles.listProjects();
|
||||
|
||||
if (!projects.some((p) => p.slug === slug)) {
|
||||
console.error(`Project "${slug}" not found. Registered projects: ${projects.map((p) => p.slug).join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
ciFiles.setActiveProject(slug);
|
||||
const config = loadConfig(projectPath);
|
||||
config.active_project = slug;
|
||||
saveConfig(projectPath, config);
|
||||
console.log(`✓ Active project set to: ${slug}`);
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
export function createShipCommand(): Command {
|
||||
return new Command("ship")
|
||||
.description("Auto-complete phase: verify, security, commit, tag")
|
||||
@@ -713,6 +792,35 @@ export function createShipCommand(): Command {
|
||||
});
|
||||
console.log(` ✓ Tagged: ${version.tag}`);
|
||||
|
||||
if (config.gitea && config.gitea.owner && config.gitea.repo) {
|
||||
const apiToken = process.env[config.gitea.api_token_env];
|
||||
if (apiToken) {
|
||||
try {
|
||||
const previousTag = getPreviousTag(projectPath, version.tag);
|
||||
const releaseNotes = generateReleaseNotes(projectPath, previousTag, version.tag);
|
||||
|
||||
const giteaClient = new GiteaClient({
|
||||
baseUrl: config.gitea.base_url,
|
||||
token: apiToken,
|
||||
owner: config.gitea.owner,
|
||||
repo: config.gitea.repo,
|
||||
});
|
||||
|
||||
const release = await giteaClient.createRelease({
|
||||
tag_name: version.tag,
|
||||
name: version.tag,
|
||||
body: releaseNotes,
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
});
|
||||
|
||||
console.log(` ✓ Release created: ${release.html_url}`);
|
||||
} catch (giteaErr) {
|
||||
console.warn(` ⚠ Gitea release failed: ${giteaErr instanceof Error ? giteaErr.message : String(giteaErr)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.git.auto_push) {
|
||||
execSync(`git push origin ${version.tag}`, { cwd: projectPath, stdio: "pipe" });
|
||||
console.log(` ✓ Pushed tag: ${version.tag}`);
|
||||
@@ -819,4 +927,20 @@ function resolveMergeTarget(projectPath: string, milestoneType: string): string
|
||||
} catch {}
|
||||
|
||||
return "main";
|
||||
}
|
||||
|
||||
function getPreviousTag(projectPath: string, currentTag: string): string | null {
|
||||
try {
|
||||
const tags = execSync("git tag -l --sort=-v:refname", { cwd: projectPath, encoding: "utf-8" })
|
||||
.split("\n")
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const currentIdx = tags.indexOf(currentTag);
|
||||
if (currentIdx >= 0 && currentIdx + 1 < tags.length) {
|
||||
return tags[currentIdx + 1];
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user