OPC # 0006: OPC Git Trunk-Based management
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -201,14 +201,37 @@ export async function getGitLog(path?: string, limit = 20): Promise<GitCommit[]>
|
||||
|
||||
// ── Promotion / Branch Ladder API ────────────────────────────────────────────
|
||||
|
||||
export interface BuildHistoryRecord {
|
||||
id: string;
|
||||
status: 'Running' | 'Succeeded' | 'Failed';
|
||||
startedAt: string;
|
||||
durationMs: number | null;
|
||||
commitSha: string | null;
|
||||
imageDigest: string | null;
|
||||
}
|
||||
|
||||
export async function getImageBuildHistory(limit = 30): Promise<BuildHistoryRecord[]> {
|
||||
const res = await fetch(`${BASE_URL}/api/image/history?limit=${limit}`);
|
||||
if (!res.ok) throw new Error(`Failed to get build history: ${res.statusText}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export interface CommitInfo {
|
||||
sha: string;
|
||||
shortSha: string;
|
||||
message: string;
|
||||
author: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export interface BranchStatus {
|
||||
branch: string;
|
||||
exists: boolean;
|
||||
shortHash: string | null;
|
||||
lastCommitSummary: string | null;
|
||||
aheadOfNext: number;
|
||||
behindNext: number;
|
||||
unreleasedLines: string[];
|
||||
branch: string;
|
||||
exists: boolean;
|
||||
shortHash: string | null;
|
||||
lastCommitSummary: string | null;
|
||||
aheadOfNext: number;
|
||||
behindNext: number;
|
||||
unreleasedCommits: CommitInfo[];
|
||||
}
|
||||
|
||||
export interface PromotionRecord {
|
||||
@@ -225,8 +248,8 @@ export interface PromotionRecord {
|
||||
log: string[];
|
||||
}
|
||||
|
||||
export async function getLadderStatus(): Promise<BranchStatus[]> {
|
||||
const res = await fetch(`${BASE_URL}/api/promotions/ladder`);
|
||||
export async function getLadderStatus(repo = 'Clarity'): Promise<BranchStatus[]> {
|
||||
const res = await fetch(`${BASE_URL}/api/promotions/ladder?repo=${encodeURIComponent(repo)}`);
|
||||
if (!res.ok) throw new Error(`Failed to get ladder status: ${res.statusText}`);
|
||||
return res.json();
|
||||
}
|
||||
@@ -246,6 +269,7 @@ export function triggerPromotion(
|
||||
onLine: (line: string) => void,
|
||||
onDone: (record: PromotionRecord) => void,
|
||||
onError: (err: string) => void,
|
||||
repo = 'Clarity',
|
||||
): () => void {
|
||||
let cancelled = false;
|
||||
const controller = new AbortController();
|
||||
@@ -255,7 +279,73 @@ export function triggerPromotion(
|
||||
const res = await fetch(`${BASE_URL}/api/promotions/promote`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ from, to, requestedBy, note }),
|
||||
body: JSON.stringify({ from, to, requestedBy, note, repo }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!res.ok || !res.body) { onError(res.statusText); return; }
|
||||
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (!cancelled) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const parts = buffer.split('\n\n');
|
||||
buffer = parts.pop() ?? '';
|
||||
for (const chunk of parts) {
|
||||
const dataLine = chunk.replace(/^data:\s*/m, '').trim();
|
||||
if (!dataLine) continue;
|
||||
try {
|
||||
const msg = JSON.parse(dataLine);
|
||||
if (msg.done && msg.promotion) onDone(msg.promotion as PromotionRecord);
|
||||
else if (typeof msg.line === 'string') onLine(msg.line);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (!cancelled) onError(e instanceof Error ? e.message : 'Unknown error');
|
||||
}
|
||||
})();
|
||||
|
||||
return () => { cancelled = true; controller.abort(); };
|
||||
}
|
||||
|
||||
export async function resetBranch(branch: string, toSha: string, repo: string): Promise<void> {
|
||||
const res = await fetch(`${BASE_URL}/api/promotions/reset`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ branch, toSha, repo }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error((body as { error?: string }).error ?? res.statusText);
|
||||
}
|
||||
}
|
||||
|
||||
/** Cherry-picks the specified commits (by full SHA) from `from` to `to` and streams SSE progress. */
|
||||
export function triggerCherryPick(
|
||||
shas: string[],
|
||||
from: string,
|
||||
to: string,
|
||||
requestedBy: string,
|
||||
note: string | undefined,
|
||||
onLine: (line: string) => void,
|
||||
onDone: (record: PromotionRecord) => void,
|
||||
onError: (err: string) => void,
|
||||
repo = 'Clarity',
|
||||
): () => void {
|
||||
let cancelled = false;
|
||||
const controller = new AbortController();
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch(`${BASE_URL}/api/promotions/cherry-pick`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ shas, from, to, requestedBy, note, repo }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user