From 1135ef4d7c0eec99dfc72e94717941126d297471 Mon Sep 17 00:00:00 2001 From: artale Date: Wed, 1 Jul 2026 14:05:28 +0200 Subject: [PATCH] feat: add forgejo actions coverage receipt --- COMMANDS.md | 3 +++ src/fable5/forgejo-cli.test.ts | 15 ++++++++++++++- src/fable5/forgejo-cli.ts | 35 ++++++++++++++++++++++++++++++++++ src/fable5/index.ts | 4 ++-- src/index.ts | 14 ++++++++++++++ 5 files changed, 68 insertions(+), 3 deletions(-) diff --git a/COMMANDS.md b/COMMANDS.md index 8463e83..9aedb00 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -272,6 +272,9 @@ fable-agent plinius godmode "improve explanation quality" - `--body ` - `--out ` - `--dry-run` +- `forgejo actions-coverage` + - `--repo ` + - `--output ` - `forgejo intake ` - `--repo ` - `--out ` diff --git a/src/fable5/forgejo-cli.test.ts b/src/fable5/forgejo-cli.test.ts index dd2f9df..6d8a1c0 100644 --- a/src/fable5/forgejo-cli.test.ts +++ b/src/fable5/forgejo-cli.test.ts @@ -2,13 +2,26 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { describe, expect, it } from "vitest"; -import { createForgejoPullRequest, getForgejoFileContent, listForgejoIssues } from "./forgejo-cli.js"; +import { createForgejoActionsCoverageReceipt, createForgejoPullRequest, getForgejoFileContent, listForgejoIssues } from "./forgejo-cli.js"; function tmpDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), "forgejo-cli-")); } describe("forgejo cli helpers", () => { + it("documents Forgejo Actions coverage without scraping or mutation", () => { + const receipt = createForgejoActionsCoverageReceipt("org/repo", new Date("2026-07-01T00:00:00.000Z")); + + expect(receipt).toMatchObject({ + schema: "fable.forgejo.actions_coverage.v1", + repo: "org/repo", + decision: "api-only", + policy: "no-web-ui-scraping-no-action-mutation", + deployAttempted: false, + }); + expect(receipt.rows.find((row) => row.capability === "actions cancel/rerun/trigger")?.status).toBe("blocked"); + }); + it("lists issues via Forgejo API", async () => { const calls: string[] = []; const issues = await listForgejoIssues({ diff --git a/src/fable5/forgejo-cli.ts b/src/fable5/forgejo-cli.ts index db69d63..32088c1 100644 --- a/src/fable5/forgejo-cli.ts +++ b/src/fable5/forgejo-cli.ts @@ -25,6 +25,41 @@ export interface ForgejoPullRequestReceipt { }; } +export interface ForgejoActionsCoverageReceipt { + schema: "fable.forgejo.actions_coverage.v1"; + createdAt: string; + repo?: string; + rows: Array<{ capability: string; status: "covered" | "unsupported" | "blocked"; reason: string }>; + decision: "api-only"; + policy: "no-web-ui-scraping-no-action-mutation"; + deployAttempted: false; +} + +export function createForgejoActionsCoverageReceipt(repo?: string, now = new Date()): ForgejoActionsCoverageReceipt { + return { + schema: "fable.forgejo.actions_coverage.v1", + createdAt: now.toISOString(), + repo, + rows: [ + { capability: "actions runs", status: "unsupported", reason: "Forgejo full run details commonly require UI endpoints; Fable will not scrape by default" }, + { capability: "actions jobs", status: "unsupported", reason: "use official API only when available" }, + { capability: "actions logs", status: "unsupported", reason: "logs may contain secrets; no web UI scraping" }, + { capability: "actions artifacts", status: "unsupported", reason: "artifacts may contain secrets; no web UI scraping" }, + { capability: "actions cancel/rerun/trigger", status: "blocked", reason: "mutating CI actions requires explicit implementation and approval" }, + { capability: "pull request create", status: "covered", reason: "forgejo pr-create emits fable.forgejo.pull_request.v1" }, + ], + decision: "api-only", + policy: "no-web-ui-scraping-no-action-mutation", + deployAttempted: false, + }; +} + +export function writeForgejoActionsCoverageReceipt(file: string, receipt: ForgejoActionsCoverageReceipt): string { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`); + return file; +} + export async function forgejoApi(opts: ForgejoCliOptions, endpoint: string, init: { method?: string; body?: unknown } = {}): Promise { const base = requireBaseUrl(opts.baseUrl); const res = await (opts.fetcher ?? fetch)(`${base}/api/v1${endpoint}`, { diff --git a/src/fable5/index.ts b/src/fable5/index.ts index 2b808e6..6b1a74b 100644 --- a/src/fable5/index.ts +++ b/src/fable5/index.ts @@ -102,8 +102,8 @@ export type { PromptFingerprintFinding, PromptFingerprintReceipt } from "./promp export { createJailbreakAssessmentReceipt, writeJailbreakAssessmentReceipt } from "./jailbreak-assessment.js"; export type { JailbreakAssessmentOptions, JailbreakAssessmentReceipt, JailbreakAssessmentScores, JailbreakDecision } from "./jailbreak-assessment.js"; -export { createForgejoPullRequest, forgejoApi, getForgejoFileContent, getForgejoRepository, listForgejoIssues, listForgejoPullRequests, listForgejoRepositories } from "./forgejo-cli.js"; -export type { ForgejoCliFetch, ForgejoCliOptions, ForgejoPullRequestReceipt } from "./forgejo-cli.js"; +export { createForgejoActionsCoverageReceipt, createForgejoPullRequest, forgejoApi, getForgejoFileContent, getForgejoRepository, listForgejoIssues, listForgejoPullRequests, listForgejoRepositories, writeForgejoActionsCoverageReceipt } from "./forgejo-cli.js"; +export type { ForgejoActionsCoverageReceipt, ForgejoCliFetch, ForgejoCliOptions, ForgejoPullRequestReceipt } from "./forgejo-cli.js"; export { createPlanReceipt, writePlanReceipt } from "./plan-receipt.js"; export type { PlanAllowedOutcome, PlanPhase, PlanReceipt, PlanReceiptOptions, PlanTaskType } from "./plan-receipt.js"; diff --git a/src/index.ts b/src/index.ts index 9eb2387..006c22d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1824,6 +1824,20 @@ forgejo if (result.receipt.receipts.quarantine) process.exit(1); }); +forgejo + .command("actions-coverage") + .description("Write a Forgejo Actions CLI coverage receipt without web scraping or action mutation") + .option("--repo ", "Repo slug, e.g. org/repo") + .option("--output ", "Receipt output path", path.join(".fable", "forgejo", "actions-coverage-live.json")) + .action(async (opts: { repo?: string; output?: string }) => { + const { createForgejoActionsCoverageReceipt, writeForgejoActionsCoverageReceipt } = await import("./fable5/forgejo-cli.js"); + const receipt = createForgejoActionsCoverageReceipt(opts.repo); + const out = opts.output ?? path.join(".fable", "forgejo", "actions-coverage-live.json"); + writeForgejoActionsCoverageReceipt(out, receipt); + console.log(JSON.stringify(receipt, null, 2)); + console.log(`\n Receipt: ${out}\n`); + }); + forgejo .command("comment ") .description("Post a scanned receipt-backed comment to a Forgejo issue or PR")