Publish curated projectbrain source from 246c0a7f35e32f54fa1bf8a3f720b01bb00dd9af

This commit is contained in:
Public Source Publisher committed 2026-10-07 01:36:52 +00:00
1 parent 759d263081
commit 98f7618af2
11 files changed
+540 -63

No files matched your search

+1 -1
View File
@@ -1,3 +1,3 @@
# Public source export # Public source export
This source snapshot was generated from the private canonical repository at revision `1e4b1f8d0f6933393c04d27837f0c8d1313c04a5`. It contains no private Git history or operational evidence. Changes are published from the canonical repository. This source snapshot was generated from the private canonical repository at revision `246c0a7f35e32f54fa1bf8a3f720b01bb00dd9af`. It contains no private Git history or operational evidence. Changes are published from the canonical repository.
+19 -19
View File
@@ -1,6 +1,6 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"sourceRevision": "1e4b1f8d0f6933393c04d27837f0c8d1313c04a5", "sourceRevision": "246c0a7f35e32f54fa1bf8a3f720b01bb00dd9af",
"files": [ "files": [
{ {
"path": ".dockerignore", "path": ".dockerignore",
@@ -309,8 +309,8 @@
}, },
{ {
"path": "backend/src/integrations/gitea/client.ts", "path": "backend/src/integrations/gitea/client.ts",
"sha256": "1f88276e1174e47d262b58445c374915fbe835a27e1b1254f0bfe77f987767ca", "sha256": "9ff455172f445e04f7bbac04b2fdaebbbc79eab58f92d9431a868e210875c0ca",
"bytes": 16592 "bytes": 23963
}, },
{ {
"path": "backend/src/integrations/gitea/compatibility.ts", "path": "backend/src/integrations/gitea/compatibility.ts",
@@ -319,8 +319,8 @@
}, },
{ {
"path": "backend/src/integrations/gitea/policy.ts", "path": "backend/src/integrations/gitea/policy.ts",
"sha256": "677d93e603f78c6a80cde5a7e2bc4c5529f507a9fa3780f1825bb29d3c4b584d", "sha256": "e7326db011edc22e437131e6d64a517a6627e642809a23fb28fee7d4300109fe",
"bytes": 6240 "bytes": 11897
}, },
{ {
"path": "backend/src/integrations/gitea/portfolio.ts", "path": "backend/src/integrations/gitea/portfolio.ts",
@@ -334,8 +334,8 @@
}, },
{ {
"path": "backend/src/integrations/gitea/types.ts", "path": "backend/src/integrations/gitea/types.ts",
"sha256": "96d115230cc462d1c0bf353ab92530708c0430f98a14b38440c70287a520c45b", "sha256": "bb5f55930da1507b832949ce3de0bfaec41e765f67fa4973b873adb97550335a",
"bytes": 3525 "bytes": 3576
}, },
{ {
"path": "backend/src/mcp/analysisWidget.ts", "path": "backend/src/mcp/analysisWidget.ts",
@@ -369,8 +369,8 @@
}, },
{ {
"path": "backend/src/mcp/server.ts", "path": "backend/src/mcp/server.ts",
"sha256": "c50d62ce8c32e73b820d4cd0a6b4ea64118a758cf8e8e6bdd3da43f37c670ce6", "sha256": "98bd7f5df04996f1a44d51bd8110309eba548bc29e0272fe84420ceafe345255",
"bytes": 78439 "bytes": 78477
}, },
{ {
"path": "backend/src/mcp/skills.ts", "path": "backend/src/mcp/skills.ts",
@@ -594,8 +594,8 @@
}, },
{ {
"path": "backend/src/routes/giteaIntegration.ts", "path": "backend/src/routes/giteaIntegration.ts",
"sha256": "69dc078288aa0aa860ab4f09a97a93a28e6786047d602723f393c6b27410d8bf", "sha256": "b474ba26897e392ae6bb43d79d59237ae60b9be2ceb9f0a04b0b27b22580c75b",
"bytes": 46130 "bytes": 46165
}, },
{ {
"path": "backend/src/routes/health.ts", "path": "backend/src/routes/health.ts",
@@ -984,8 +984,8 @@
}, },
{ {
"path": "backend/test/database-autonomy.test.ts", "path": "backend/test/database-autonomy.test.ts",
"sha256": "eca93db598282b142003bb9ffabb1a578b93175da36004a909ee112bbf1a4f52", "sha256": "f05c5a72a4acf7dcef0ccb1b4ddf6334c559b06375959b0d4dbb2e916d9914be",
"bytes": 50622 "bytes": 51663
}, },
{ {
"path": "backend/test/database-repository-sync.test.ts", "path": "backend/test/database-repository-sync.test.ts",
@@ -1034,8 +1034,8 @@
}, },
{ {
"path": "backend/test/gitea-integration.test.ts", "path": "backend/test/gitea-integration.test.ts",
"sha256": "2b7fb94dd40bd50c9e0aa2147540e690d8275ca37f2d948aaeb6ed9128b24b0e", "sha256": "3c5cc8dd5d575996f89cda4e9db1d5b31f92121ed9e78945b1799740a8f9bcfd",
"bytes": 37780 "bytes": 38396
}, },
{ {
"path": "backend/test/gitea-portfolio-mcp.test.ts", "path": "backend/test/gitea-portfolio-mcp.test.ts",
@@ -1054,8 +1054,8 @@
}, },
{ {
"path": "backend/test/native-ci-discovery.test.ts", "path": "backend/test/native-ci-discovery.test.ts",
"sha256": "7b86c426bb31b47f0ea7207896e18bdaaee396a830437c13ed33564945f07746", "sha256": "65637cdd3cd6b73935fffa7f3f28e34705b9ac83d1a921a6f4d79e395befc8b0",
"bytes": 4926 "bytes": 16744
}, },
{ {
"path": "backend/test/operatingSystemRoutes.test.ts", "path": "backend/test/operatingSystemRoutes.test.ts",
@@ -1099,8 +1099,8 @@
}, },
{ {
"path": "backend/test/remediation-regressions.test.ts", "path": "backend/test/remediation-regressions.test.ts",
"sha256": "c13fcafb7ad72699e7d4aa2a64b555ea4bce6c88fb2ccd425d5154751af14d80", "sha256": "4a5a2e7a0d98b3fdcf65940b1cfdd34e82034544010c4d2a681047a9cea8152d",
"bytes": 5380 "bytes": 5848
}, },
{ {
"path": "backend/test/repository-analysis.test.ts", "path": "backend/test/repository-analysis.test.ts",
+117 -4
View File
@@ -1,4 +1,4 @@
import type { CreateGiteaRepositoryInput, GiteaActionJobList, GiteaActionRunnerList, GiteaActionRunList, GiteaBranch, GiteaContent, GiteaIssue, GiteaIssueComment, GiteaLabel, GiteaPullRequest, GiteaRepository, GiteaSearchResult, GiteaWebhook } from "./types.js"; import type { CreateGiteaRepositoryInput, GiteaActionJobList, GiteaActionRunnerList, GiteaActionRun, GiteaActionRunList, GiteaBranch, GiteaContent, GiteaIssue, GiteaIssueComment, GiteaLabel, GiteaPullRequest, GiteaRepository, GiteaSearchResult, GiteaWebhook } from "./types.js";
export class GiteaApiError extends Error { export class GiteaApiError extends Error {
constructor(public readonly status: number, public readonly code: string, message: string) { constructor(public readonly status: number, public readonly code: string, message: string) {
@@ -11,6 +11,17 @@ type GiteaBranchProtection = { enable_status_check?: boolean; status_check_conte
type GiteaCombinedStatus = { state?: string; statuses?: Array<{ context?: string; status?: string }> }; type GiteaCombinedStatus = { state?: string; statuses?: Array<{ context?: string; status?: string }> };
import { GITEA_TESTED_MERGE_RANGE, giteaMergeCompatibility } from "./compatibility.js"; import { GITEA_TESTED_MERGE_RANGE, giteaMergeCompatibility } from "./compatibility.js";
import { actionRunMatchesPullRequestNumber, isManagedValidationWorkflowPath, managedValidationWorkflowPullRequestNumber } from "./policy.js";
const ACTION_JOBS_PAGE_SIZE = 100;
const ACTION_JOBS_MAX_PAGES = 10;
const ACTION_RUNS_PAGE_SIZE = 100;
const ACTION_RUNS_MAX_PAGES = 10;
const runIsNewer = (candidate: GiteaActionRun, current: GiteaActionRun): boolean =>
candidate.run_number > current.run_number
|| (candidate.run_number === current.run_number && (candidate.run_attempt ?? 0) > (current.run_attempt ?? 0))
|| (candidate.run_number === current.run_number && (candidate.run_attempt ?? 0) === (current.run_attempt ?? 0) && candidate.id > current.id);
export class GiteaClient { export class GiteaClient {
private readonly apiBase: string; private readonly apiBase: string;
@@ -233,12 +244,114 @@ export class GiteaClient {
return this.request<GiteaLabel[]>(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/issues/${issueNumber}/labels`, { method: "PUT", body: { labels } }); return this.request<GiteaLabel[]>(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/issues/${issueNumber}/labels`, { method: "PUT", body: { labels } });
} }
listActionRuns(owner: string, repository: string, input: { branch?: string; headSha?: string; limit?: number } = {}) { async listActionRuns(owner: string, repository: string, input: { branch?: string; headSha?: string; pullRequestNumber?: number; limit?: number } = {}) {
return this.request<GiteaActionRunList>(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/actions/runs`, { const path = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/actions/runs`;
const query = (page?: number) => ({
// Filtering by branch upstream hides native PR runs with head_branch="". // Filtering by branch upstream hides native PR runs with head_branch="".
// Exact-head callers perform branch/PR reconciliation in policy.ts. // Exact-head callers perform branch/PR reconciliation in policy.ts.
query: { branch: input.headSha ? undefined : input.branch, head_sha: input.headSha, limit: input.limit ?? 30 }, branch: input.headSha ? undefined : input.branch,
head_sha: input.headSha,
limit: input.headSha ? ACTION_RUNS_PAGE_SIZE : input.limit ?? 30,
page: input.headSha ? page ?? 1 : undefined,
}); });
let response = await this.request<GiteaActionRunList>(path, { query: query(1) });
if (!Array.isArray(response.workflow_runs)) throw new GiteaApiError(502, "GITEA_ACTION_RUNS_INVALID", "Gitea returned an invalid Actions run list");
if (input.headSha) {
if (!Number.isSafeInteger(response.total_count) || response.total_count < 0 || response.workflow_runs.length > ACTION_RUNS_PAGE_SIZE) {
throw new GiteaApiError(502, "GITEA_ACTION_RUNS_INVALID", "Gitea returned an invalid exact-head Actions run page");
}
const workflowRuns = [...response.workflow_runs];
const expectedCount = response.total_count;
const seenRuns = new Set<string>();
const addPage = (runs: GiteaActionRun[]) => {
for (const run of runs) {
if (!run || typeof run !== "object" || !Number.isSafeInteger(run.id) || run.id < 1
|| !Number.isSafeInteger(run.run_number) || run.run_number < 1
|| (run.run_attempt !== undefined && (!Number.isSafeInteger(run.run_attempt) || run.run_attempt < 1))
|| typeof run.path !== "string" || typeof run.event !== "string" || typeof run.head_sha !== "string") {
throw new GiteaApiError(502, "GITEA_ACTION_RUNS_INVALID", "Gitea returned an invalid exact-head Actions run");
}
const identity = `${run.id}:${run.run_attempt ?? 0}`;
if (seenRuns.has(identity)) throw new GiteaApiError(502, "GITEA_ACTION_RUNS_INVALID", "Gitea repeated an exact-head Actions run");
seenRuns.add(identity);
}
};
addPage(workflowRuns);
if (workflowRuns.length > expectedCount) throw new GiteaApiError(502, "GITEA_ACTION_RUNS_INVALID", "Gitea returned excess exact-head Actions runs");
for (let page = 2; workflowRuns.length < expectedCount && page <= ACTION_RUNS_MAX_PAGES; page += 1) {
const next = await this.request<GiteaActionRunList>(path, { query: query(page) });
if (!Number.isSafeInteger(next.total_count) || next.total_count !== expectedCount || !Array.isArray(next.workflow_runs)
|| next.workflow_runs.length > ACTION_RUNS_PAGE_SIZE || next.workflow_runs.length === 0) {
throw new GiteaApiError(502, "GITEA_ACTION_RUNS_TRUNCATED", "Gitea exact-head Actions run pagination was incomplete or changed");
}
addPage(next.workflow_runs);
workflowRuns.push(...next.workflow_runs);
if (workflowRuns.length > expectedCount) throw new GiteaApiError(502, "GITEA_ACTION_RUNS_INVALID", "Gitea returned excess exact-head Actions runs");
}
if (workflowRuns.length !== expectedCount) throw new GiteaApiError(502, "GITEA_ACTION_RUNS_TRUNCATED", "Gitea exact-head Actions run list exceeded the bounded pagination limit");
response = { ...response, workflow_runs: workflowRuns };
}
const latestByEvent = new Map<string, GiteaActionRun>();
for (const run of response.workflow_runs) {
if (!isManagedValidationWorkflowPath(run.path) || !["workflow_dispatch", "pull_request"].includes(run.event)) continue;
if (input.headSha && run.head_sha !== input.headSha) continue;
if (input.pullRequestNumber !== undefined && run.event === "pull_request"
&& !actionRunMatchesPullRequestNumber(run, input.pullRequestNumber)) continue;
const group = run.event === "pull_request"
? `${run.event}:${managedValidationWorkflowPullRequestNumber(run.path) ?? run.pull_requests?.find((pr) => pr.number !== undefined)?.number ?? "unbound"}`
: run.event;
const current = latestByEvent.get(group);
if (!current || runIsNewer(run, current)) latestByEvent.set(group, run);
}
const jobLists = await Promise.all([...latestByEvent.values()].map(async (run) => ({
key: `${run.id}:${run.run_attempt ?? 0}`,
jobs: await this.listActionRunJobs(owner, repository, run.id, run.run_attempt),
})));
const jobsByRun = new Map(jobLists.map(({ key, jobs }) => [key, jobs]));
return {
...response,
workflow_runs: response.workflow_runs.map((run) => {
const jobs = jobsByRun.get(`${run.id}:${run.run_attempt ?? 0}`);
return jobs ? { ...run, jobs } : run;
}),
};
}
private async listActionRunJobs(owner: string, repository: string, runId: number, runAttempt?: number) {
if (!Number.isSafeInteger(runId) || runId < 1 || (runAttempt !== undefined && (!Number.isSafeInteger(runAttempt) || runAttempt < 0))) {
throw new GiteaApiError(502, "GITEA_ACTION_RUN_INVALID", "Gitea returned invalid workflow run identity");
}
const runPath = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/actions/runs/${runId}`;
const attemptPath = Number.isSafeInteger(runAttempt) && runAttempt! > 0 ? `/attempts/${runAttempt}` : "";
const path = `${runPath}${attemptPath}/jobs`;
const jobs = [];
let expectedCount: number | undefined;
for (let page = 1; page <= ACTION_JOBS_MAX_PAGES; page += 1) {
const result = await this.request<GiteaActionJobList>(path, {
query: { page, limit: ACTION_JOBS_PAGE_SIZE },
});
if (!Number.isSafeInteger(result.total_count) || result.total_count < 0 || !Array.isArray(result.jobs) || result.jobs.length > ACTION_JOBS_PAGE_SIZE) {
throw new GiteaApiError(502, "GITEA_ACTION_JOBS_INVALID", "Gitea returned an invalid Actions job page");
}
if (expectedCount !== undefined && result.total_count !== expectedCount) {
throw new GiteaApiError(502, "GITEA_ACTION_JOBS_CHANGED", "Gitea Actions job pages changed during validation");
}
expectedCount = result.total_count;
const seenIds = new Set(jobs.map((job) => job.id));
for (const job of result.jobs) {
if (!job || typeof job !== "object" || !Number.isSafeInteger(job.id) || job.id < 1 || seenIds.has(job.id)) {
throw new GiteaApiError(502, "GITEA_ACTION_JOBS_INVALID", "Gitea returned invalid or duplicate Actions job identities");
}
seenIds.add(job.id);
}
jobs.push(...result.jobs);
if (jobs.length > expectedCount) throw new GiteaApiError(502, "GITEA_ACTION_JOBS_INVALID", "Gitea returned duplicate or excess Actions jobs");
if (jobs.length === expectedCount) return jobs;
if (result.jobs.length === 0) throw new GiteaApiError(502, "GITEA_ACTION_JOBS_TRUNCATED", "Gitea Actions job pagination was incomplete");
}
throw new GiteaApiError(502, "GITEA_ACTION_JOBS_TRUNCATED", "Gitea Actions job list exceeded the bounded pagination limit");
} }
listUserActionRunners() { listUserActionRunners() {
+110 -23
View File
@@ -55,54 +55,140 @@ export const assertFileSize = (content: string): void => {
}; };
const actionWorkflowKey = (run: GiteaActionRun): string => const actionWorkflowKey = (run: GiteaActionRun): string =>
`${run.event}:${run.path.split("@")[0]?.trim() || run.display_title.trim() || `workflow-${run.id}`}`; `${run.event}:${(typeof run.path === "string" ? run.path.split("@")[0]?.trim() : "") || (typeof run.display_title === "string" ? run.display_title.trim() : "") || `workflow-${run.id}`}`;
const managedValidationWorkflowPaths = new Set([
"managed-validation.yml",
".gitea/workflows/managed-validation.yml",
]);
type ManagedWorkflowPath = { canonicalBase: boolean; valid: boolean; ref?: string };
const parseManagedValidationWorkflowPath = (path: unknown): ManagedWorkflowPath => {
if (typeof path !== "string" || path.length === 0 || path.length > 512) return { canonicalBase: false, valid: false };
const parts = path.split("@");
if (!managedValidationWorkflowPaths.has(parts[0] ?? "")) return { canonicalBase: false, valid: false };
if (parts.length === 1) return { canonicalBase: true, valid: true };
if (parts.length !== 2) return { canonicalBase: true, valid: false };
const ref = parts[1] ?? "";
const pullRequestRef = /^refs\/pull\/([1-9]\d*)\/head$/.exec(ref);
if (pullRequestRef && Number.isSafeInteger(Number(pullRequestRef[1]))) return { canonicalBase: true, valid: true, ref };
if (/^refs\/heads\/[a-zA-Z0-9][a-zA-Z0-9._/-]*$/.test(ref)
&& !ref.includes("..") && !ref.includes("//") && !ref.endsWith("/")) {
return { canonicalBase: true, valid: true, ref };
}
return { canonicalBase: true, valid: false };
};
export const isManagedValidationWorkflowPath = (path: unknown): boolean => parseManagedValidationWorkflowPath(path).valid;
export const managedValidationWorkflowPullRequestNumber = (path: unknown): number | undefined => {
const info = parseManagedValidationWorkflowPath(path);
const match = info.valid && info.ref ? /^refs\/pull\/([1-9]\d*)\/head$/.exec(info.ref) : null;
const number = match ? Number(match[1]) : undefined;
return Number.isSafeInteger(number) ? number : undefined;
};
export const actionRunMatchesPullRequestNumber = (run: GiteaActionRun, expectedNumber: number): boolean => {
if (!Number.isSafeInteger(expectedNumber) || expectedNumber < 1) return false;
if (!run || typeof run !== "object" || !isManagedValidationWorkflowPath(run.path)) return false;
const pathNumber = managedValidationWorkflowPullRequestNumber(run.path);
if (pathNumber !== undefined && pathNumber !== expectedNumber) return false;
const references = run.pull_requests ?? [];
if (!Array.isArray(references)) return false;
const explicitNumbers: number[] = [];
for (const reference of references) {
if (!reference || typeof reference !== "object") return false;
if (reference.number !== undefined) explicitNumbers.push(reference.number);
}
if (explicitNumbers.some((number) => !Number.isSafeInteger(number) || number < 1 || number !== expectedNumber)) return false;
return pathNumber === expectedNumber || explicitNumbers.includes(expectedNumber);
};
// Native Gitea PR runs may omit head_branch. Keep the immutable SHA as the // Native Gitea PR runs may omit head_branch. Keep the immutable SHA as the
// primary identity, and reject contradictory PR metadata when it is supplied. // primary identity, and reject contradictory PR metadata when it is supplied.
export const exactHeadActionRuns = (runs: GiteaActionRun[], branch: string, headSha: string): GiteaActionRun[] => export const exactHeadActionRuns = (runs: GiteaActionRun[], branch: string, headSha: string): GiteaActionRun[] =>
runs.filter((run) => { runs.filter((run) => {
if (run.head_sha !== headSha) return false; if (!run || typeof run !== "object" || run.head_sha !== headSha) return false;
if (run.head_branch !== undefined && typeof run.head_branch !== "string") return false;
if (run.event === "pull_request" && run.pull_requests !== undefined && !Array.isArray(run.pull_requests)) return false;
const references = run.event === "pull_request" ? run.pull_requests ?? [] : [];
if (references.some((pr) => {
if (!pr || typeof pr !== "object") return true;
if (pr.number !== undefined && (!Number.isSafeInteger(pr.number) || pr.number < 1)) return true;
if (pr.head !== undefined && (!pr.head || typeof pr.head !== "object")) return true;
if (pr.head?.ref !== undefined && (typeof pr.head.ref !== "string" || pr.head.ref !== branch)) return true;
if (pr.head?.sha !== undefined && (typeof pr.head.sha !== "string" || pr.head.sha !== headSha)) return true;
return false;
})) return false;
if (run.head_branch === branch) return true; if (run.head_branch === branch) return true;
if (run.head_branch?.trim() || run.event !== "pull_request") return false; if (run.head_branch?.trim() || run.event !== "pull_request") return false;
const references = run.pull_requests ?? []; return references.length === 0 || references.every((pr) =>
return references.length === 0 || references.some((pr) =>
(!pr.head?.ref || pr.head.ref === branch) && (!pr.head?.sha || pr.head.sha === headSha)); (!pr.head?.ref || pr.head.ref === branch) && (!pr.head?.sha || pr.head.sha === headSha));
}); });
const isManagedFullValidationRun = (run: GiteaActionRun): boolean => { const isManagedFullValidationRun = (run: GiteaActionRun): boolean => {
const workflowPath = run.path.split("@")[0]?.trim().toLowerCase() ?? ""; if (run.event !== "workflow_dispatch" && run.event !== "pull_request") return false;
const title = run.display_title.trim().toLowerCase(); const path = parseManagedValidationWorkflowPath(run.path);
return (run.event === "workflow_dispatch" || run.event === "pull_request") if (!path.valid) return false;
&& workflowPath.endsWith("/managed-validation.yml") if (run.event === "pull_request") return !path.ref || path.ref.startsWith("refs/pull/");
&& title === "managed validation / full"; return !path.ref || path.ref.startsWith("refs/heads/");
}; };
export const successfulFullActionRun = (runs: GiteaActionRun[], branch: string, headSha: string): GiteaActionRun => { const hasSuccessfulFullJob = (run: GiteaActionRun, branch: string): boolean => {
if (!Array.isArray(run.jobs)) return false;
const fullJobs = run.jobs.filter((job) => typeof job?.name === "string" && job.name.trim().toLowerCase() === "full");
if (fullJobs.length !== 1) return false;
const [job] = fullJobs;
if (!job || job.status !== "completed" || job.conclusion !== "success") return false;
if (!Number.isSafeInteger(job.id) || job.id < 1 || job.run_id !== run.id) return false;
if (!Number.isSafeInteger(run.run_attempt) || run.run_attempt! < 1 || job.run_attempt !== run.run_attempt) return false;
if (typeof job.head_sha !== "string" || job.head_sha !== run.head_sha) return false;
if (job.head_branch?.trim() && job.head_branch !== branch) return false;
return true;
};
const hasValidRunOrdering = (run: GiteaActionRun): boolean =>
Number.isSafeInteger(run.id) && run.id > 0
&& Number.isSafeInteger(run.run_number) && run.run_number > 0
&& (run.run_attempt === undefined || (Number.isSafeInteger(run.run_attempt) && run.run_attempt >= 1));
const compareRunFreshness = (left: GiteaActionRun, right: GiteaActionRun): number =>
(right.run_number - left.run_number)
|| ((right.run_attempt ?? 0) - (left.run_attempt ?? 0))
|| (right.id - left.id);
const latestManagedFullRun = (runs: GiteaActionRun[], branch: string, headSha: string, expectedPullRequestNumber?: number): GiteaActionRun | undefined => {
const fullRuns = exactHeadActionRuns(runs, branch, headSha) const fullRuns = exactHeadActionRuns(runs, branch, headSha)
.filter(isManagedFullValidationRun) .filter(isManagedFullValidationRun)
.sort((left, right) => right.run_number - left.run_number || right.id - left.id); .filter((run) => expectedPullRequestNumber === undefined || actionRunMatchesPullRequestNumber(run, expectedPullRequestNumber));
const latest = fullRuns[0]; if (fullRuns.some((run) => !hasValidRunOrdering(run))) throw new Error("FEATURE_DELIVERY_FULL_VALIDATION_REQUIRED");
if (!latest || latest.status !== "completed" || latest.conclusion !== "success") { return fullRuns.sort(compareRunFreshness)[0];
};
export const successfulFullActionRun = (runs: GiteaActionRun[], branch: string, headSha: string, expectedPullRequestNumber?: number): GiteaActionRun => {
const latest = latestManagedFullRun(runs, branch, headSha, expectedPullRequestNumber);
if (!latest || latest.status !== "completed" || latest.conclusion !== "success" || !hasSuccessfulFullJob(latest, branch)) {
throw new Error("FEATURE_DELIVERY_FULL_VALIDATION_REQUIRED"); throw new Error("FEATURE_DELIVERY_FULL_VALIDATION_REQUIRED");
} }
return latest; return latest;
}; };
export const assertSuccessfulPullRequestFullActionRun = (runs: GiteaActionRun[], branch: string, headSha: string): GiteaActionRun => { export const assertSuccessfulPullRequestFullActionRun = (runs: GiteaActionRun[], branch: string, headSha: string, expectedPullRequestNumber?: number): GiteaActionRun => {
const latest = exactHeadActionRuns(runs, branch, headSha) const latest = latestManagedFullRun(runs.filter((run) => run.event === "pull_request"), branch, headSha, expectedPullRequestNumber);
.filter((run) => run.event === "pull_request" && isManagedFullValidationRun(run)) if (!latest || latest.status !== "completed" || latest.conclusion !== "success" || !hasSuccessfulFullJob(latest, branch)) {
.sort((left, right) => right.run_number - left.run_number || right.id - left.id)[0];
if (!latest || latest.status !== "completed" || latest.conclusion !== "success") {
throw new Error("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED"); throw new Error("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
} }
assertSuccessfulActionRuns(runs, branch, headSha); assertSuccessfulActionRuns(runs, branch, headSha, expectedPullRequestNumber);
return latest; return latest;
}; };
export const assertSuccessfulActionRuns = (runs: GiteaActionRun[], branch: string, headSha: string): GiteaActionRun[] => { export const assertSuccessfulActionRuns = (runs: GiteaActionRun[], branch: string, headSha: string, expectedPullRequestNumber?: number): GiteaActionRun[] => {
const exactRuns = exactHeadActionRuns(runs, branch, headSha); const candidates = exactHeadActionRuns(runs, branch, headSha);
if (candidates.some((run) => typeof run.path !== "string" || run.path.length === 0)) throw new Error("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL");
const exactRuns = candidates;
if (exactRuns.length === 0) throw new Error("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL"); if (exactRuns.length === 0) throw new Error("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL");
successfulFullActionRun(exactRuns, branch, headSha); successfulFullActionRun(exactRuns, branch, headSha, expectedPullRequestNumber);
// Gitea retains failed attempts when the same workflow is rerun for an // Gitea retains failed attempts when the same workflow is rerun for an
// unchanged revision. Authorization therefore follows the newest run of each // unchanged revision. Authorization therefore follows the newest run of each
@@ -111,7 +197,8 @@ export const assertSuccessfulActionRuns = (runs: GiteaActionRun[], branch: strin
for (const run of exactRuns) { for (const run of exactRuns) {
const key = actionWorkflowKey(run); const key = actionWorkflowKey(run);
const current = latestByWorkflow.get(key); const current = latestByWorkflow.get(key);
if (!current || run.run_number > current.run_number || (run.run_number === current.run_number && run.id > current.id)) { if (!hasValidRunOrdering(run)) throw new Error("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL");
if (!current || compareRunFreshness(current, run) > 0) {
latestByWorkflow.set(key, run); latestByWorkflow.set(key, run);
} }
} }
+2
View File
@@ -66,6 +66,7 @@ export type GiteaSearchResult = {
export type GiteaActionRun = { export type GiteaActionRun = {
id: number; id: number;
run_number: number; run_number: number;
run_attempt?: number;
display_title: string; display_title: string;
path: string; path: string;
event: string; event: string;
@@ -77,6 +78,7 @@ export type GiteaActionRun = {
started_at?: string; started_at?: string;
completed_at?: string; completed_at?: string;
pull_requests?: Array<{ number?: number; head?: { ref?: string; sha?: string }; base?: { ref?: string; sha?: string } }>; pull_requests?: Array<{ number?: number; head?: { ref?: string; sha?: string }; base?: { ref?: string; sha?: string } }>;
jobs?: GiteaActionJob[];
}; };
export type GiteaActionRunList = { total_count: number; workflow_runs: GiteaActionRun[] }; export type GiteaActionRunList = { total_count: number; workflow_runs: GiteaActionRun[] };
+2 -2
View File
@@ -777,8 +777,8 @@ export const createProjectBrainMcpServer = (env: AppEnv, actor: string) => {
const head = await client.getBranch(link!.owner, link!.repository, branch); const head = await client.getBranch(link!.owner, link!.repository, branch);
if (head.commit.id !== expected_head_sha) throw new Error("FEATURE_DELIVERY_MERGE_HEAD_CONFLICT"); if (head.commit.id !== expected_head_sha) throw new Error("FEATURE_DELIVERY_MERGE_HEAD_CONFLICT");
assertFeatureDeliveryActionEvidence(delivery.spec.validationEvidence, expected_head_sha); assertFeatureDeliveryActionEvidence(delivery.spec.validationEvidence, expected_head_sha);
const actionRuns = await client.listActionRuns(link!.owner, link!.repository, { branch, headSha: expected_head_sha, limit: 30 }); const actionRuns = await client.listActionRuns(link!.owner, link!.repository, { branch, headSha: expected_head_sha, pullRequestNumber, limit: 30 });
assertSuccessfulPullRequestFullActionRun(actionRuns.workflow_runs, branch, expected_head_sha); assertSuccessfulPullRequestFullActionRun(actionRuns.workflow_runs, branch, expected_head_sha, pullRequestNumber);
await client.waitForRequiredPullRequestChecks(link!.owner, link!.repository, link!.defaultBranch, expected_head_sha); await client.waitForRequiredPullRequestChecks(link!.owner, link!.repository, link!.defaultBranch, expected_head_sha);
const mergeReadyPullRequest = await client.getPullRequest(link!.owner, link!.repository, pullRequestNumber); const mergeReadyPullRequest = await client.getPullRequest(link!.owner, link!.repository, pullRequestNumber);
+2 -2
View File
@@ -406,8 +406,8 @@ export const registerGiteaIntegrationRoutes = async (app: FastifyInstance, optio
const head = await client.getBranch(link.owner, link.repository, branch); const head = await client.getBranch(link.owner, link.repository, branch);
if (head.commit.id !== parsed.data.expectedHeadSha) throw new Error("FEATURE_DELIVERY_MERGE_HEAD_CONFLICT"); if (head.commit.id !== parsed.data.expectedHeadSha) throw new Error("FEATURE_DELIVERY_MERGE_HEAD_CONFLICT");
assertFeatureDeliveryActionEvidence(delivery.spec.validationEvidence, parsed.data.expectedHeadSha); assertFeatureDeliveryActionEvidence(delivery.spec.validationEvidence, parsed.data.expectedHeadSha);
const actionRuns = await client.listActionRuns(link.owner, link.repository, { branch, headSha: parsed.data.expectedHeadSha, limit: 30 }); const actionRuns = await client.listActionRuns(link.owner, link.repository, { branch, headSha: parsed.data.expectedHeadSha, pullRequestNumber: number, limit: 30 });
assertSuccessfulPullRequestFullActionRun(actionRuns.workflow_runs, branch, parsed.data.expectedHeadSha); assertSuccessfulPullRequestFullActionRun(actionRuns.workflow_runs, branch, parsed.data.expectedHeadSha, number);
await client.waitForRequiredPullRequestChecks(link.owner, link.repository, link.defaultBranch, parsed.data.expectedHeadSha); await client.waitForRequiredPullRequestChecks(link.owner, link.repository, link.defaultBranch, parsed.data.expectedHeadSha);
const mergeReadyPullRequest = await client.getPullRequest(link.owner, link.repository, number); const mergeReadyPullRequest = await client.getPullRequest(link.owner, link.repository, number);
const mergeReadyHead = await client.getBranch(link.owner, link.repository, branch); const mergeReadyHead = await client.getBranch(link.owner, link.repository, branch);
+20 -5
View File
@@ -297,12 +297,27 @@ databaseDescribe("autonomy database write isolation", () => {
expect((await getAutonomyState(slug)).activeLease?.token).toBe(resumed.work!.token); expect((await getAutonomyState(slug)).activeLease?.token).toBe(resumed.work!.token);
const head = "b".repeat(40); const head = "b".repeat(40);
let remoteMerged = false; let remoteMerged = false;
const fullJob = (runId: number, headSha: string, conclusion = "success") => ({
id: runId * 10,
html_url: `https://gitea.example.com/runs/${runId}/jobs/${runId * 10}`,
run_id: runId,
name: "full",
labels: [],
run_attempt: 1,
head_sha: headSha,
head_branch: "chatgpt/existing",
status: "completed",
conclusion,
runner_id: 1,
steps: [],
created_at: "2026-10-06T21:33:06Z",
});
vi.spyOn(GiteaClient.prototype, "getPullRequest").mockImplementation(async () => ({ number: 1, title: "Feature", html_url: "https://gitea.example.com/pulls/1", state: remoteMerged ? "closed" : "open", merged: remoteMerged, merge_commit_sha: remoteMerged ? "c".repeat(40) : undefined, head: { ref: "chatgpt/existing", sha: head }, base: { ref: "main", sha: "d".repeat(40) }, mergeable: true })); vi.spyOn(GiteaClient.prototype, "getPullRequest").mockImplementation(async () => ({ number: 1, title: "Feature", html_url: "https://gitea.example.com/pulls/1", state: remoteMerged ? "closed" : "open", merged: remoteMerged, merge_commit_sha: remoteMerged ? "c".repeat(40) : undefined, head: { ref: "chatgpt/existing", sha: head }, base: { ref: "main", sha: "d".repeat(40) }, mergeable: true }));
vi.spyOn(GiteaClient.prototype, "getBranch").mockResolvedValue({ name: "chatgpt/existing", commit: { id: head, message: "Repair" }, protected: false }); vi.spyOn(GiteaClient.prototype, "getBranch").mockResolvedValue({ name: "chatgpt/existing", commit: { id: head, message: "Repair" }, protected: false });
vi.spyOn(GiteaClient.prototype, "getContents").mockResolvedValue({ type: "file", path: "src/feature.ts", name: "feature.ts", sha: "file-sha", size: 4, encoding: "base64", content: "dGVzdA==" }); vi.spyOn(GiteaClient.prototype, "getContents").mockResolvedValue({ type: "file", path: "src/feature.ts", name: "feature.ts", sha: "file-sha", size: 4, encoding: "base64", content: "dGVzdA==" });
const write = vi.spyOn(GiteaClient.prototype, "updateFile").mockResolvedValue({}); const write = vi.spyOn(GiteaClient.prototype, "updateFile").mockResolvedValue({});
const actionRuns = vi.spyOn(GiteaClient.prototype, "listActionRuns").mockResolvedValue({ total_count: 2, workflow_runs: [ const actionRuns = vi.spyOn(GiteaClient.prototype, "listActionRuns").mockResolvedValue({ total_count: 2, workflow_runs: [
{ id: 2, run_number: 2, display_title: "Managed validation / full", path: ".gitea/workflows/managed-validation.yml", event: "pull_request", head_sha: head, head_branch: "chatgpt/existing", status: "completed", conclusion: "success", html_url: "https://gitea.example.com/runs/2" }, { id: 2, run_number: 2, run_attempt: 1, display_title: "ci: validate full", path: ".gitea/workflows/managed-validation.yml@refs/pull/1/head", event: "pull_request", head_sha: head, head_branch: "chatgpt/existing", pull_requests: [{ number: 1, head: { ref: "chatgpt/existing", sha: head } }], status: "completed", conclusion: "success", html_url: "https://gitea.example.com/runs/2", jobs: [fullJob(2, head)] },
{ id: 3, run_number: 3, display_title: "CI", path: ".gitea/workflows/ci.yml", event: "pull_request", head_sha: head, head_branch: "chatgpt/existing", status: "completed", conclusion: "success", html_url: "https://gitea.example.com/runs/3" }, { id: 3, run_number: 3, display_title: "CI", path: ".gitea/workflows/ci.yml", event: "pull_request", head_sha: head, head_branch: "chatgpt/existing", status: "completed", conclusion: "success", html_url: "https://gitea.example.com/runs/3" },
] }); ] });
vi.spyOn(GiteaClient.prototype, "getBranchProtection").mockResolvedValue({ enable_status_check: true, status_check_contexts: ["CI / test"] }); vi.spyOn(GiteaClient.prototype, "getBranchProtection").mockResolvedValue({ enable_status_check: true, status_check_contexts: ["CI / test"] });
@@ -325,28 +340,28 @@ databaseDescribe("autonomy database write isolation", () => {
expect(synced.isError, JSON.stringify(synced.content)).not.toBe(true); expect(synced.isError, JSON.stringify(synced.content)).not.toBe(true);
actionRuns.mockResolvedValue({ total_count: 1, workflow_runs: [ actionRuns.mockResolvedValue({ total_count: 1, workflow_runs: [
{ id: 4, run_number: 4, display_title: "Managed validation / full", path: ".gitea/workflows/managed-validation.yml", event: "pull_request", head_sha: "c".repeat(40), head_branch: "chatgpt/existing", status: "completed", conclusion: "success", html_url: "https://gitea.example.com/runs/4" }, { id: 4, run_number: 4, run_attempt: 1, display_title: "ci: validate full", path: ".gitea/workflows/managed-validation.yml@refs/pull/1/head", event: "pull_request", head_sha: "c".repeat(40), head_branch: "chatgpt/existing", pull_requests: [{ number: 1, head: { ref: "chatgpt/existing", sha: "c".repeat(40) } }], status: "completed", conclusion: "success", html_url: "https://gitea.example.com/runs/4", jobs: [fullJob(4, "c".repeat(40))] },
] }); ] });
const stalePrRun = await client.callTool({ name: "merge_feature_delivery", arguments: { project_id: slug, delivery_id: delivery.id, expected_head_sha: head, delete_branch: false } }); const stalePrRun = await client.callTool({ name: "merge_feature_delivery", arguments: { project_id: slug, delivery_id: delivery.id, expected_head_sha: head, delete_branch: false } });
expect(stalePrRun.isError, JSON.stringify(stalePrRun.content)).toBe(true); expect(stalePrRun.isError, JSON.stringify(stalePrRun.content)).toBe(true);
expect(merge).not.toHaveBeenCalled(); expect(merge).not.toHaveBeenCalled();
actionRuns.mockResolvedValue({ total_count: 1, workflow_runs: [ actionRuns.mockResolvedValue({ total_count: 1, workflow_runs: [
{ id: 5, run_number: 5, display_title: "Managed validation / full", path: ".gitea/workflows/managed-validation.yml", event: "workflow_dispatch", head_sha: head, head_branch: "chatgpt/existing", status: "completed", conclusion: "success", html_url: "https://gitea.example.com/runs/5" }, { id: 5, run_number: 5, run_attempt: 1, display_title: "ci: validate full", path: ".gitea/workflows/managed-validation.yml", event: "workflow_dispatch", head_sha: head, head_branch: "chatgpt/existing", status: "completed", conclusion: "success", html_url: "https://gitea.example.com/runs/5", jobs: [fullJob(5, head)] },
] }); ] });
const dispatchOnly = await client.callTool({ name: "merge_feature_delivery", arguments: { project_id: slug, delivery_id: delivery.id, expected_head_sha: head, delete_branch: false } }); const dispatchOnly = await client.callTool({ name: "merge_feature_delivery", arguments: { project_id: slug, delivery_id: delivery.id, expected_head_sha: head, delete_branch: false } });
expect(dispatchOnly.isError, JSON.stringify(dispatchOnly.content)).toBe(true); expect(dispatchOnly.isError, JSON.stringify(dispatchOnly.content)).toBe(true);
expect(merge).not.toHaveBeenCalled(); expect(merge).not.toHaveBeenCalled();
actionRuns.mockResolvedValue({ total_count: 1, workflow_runs: [ actionRuns.mockResolvedValue({ total_count: 1, workflow_runs: [
{ id: 6, run_number: 6, display_title: "Managed validation / full", path: ".gitea/workflows/managed-validation.yml", event: "pull_request", head_sha: head, head_branch: "chatgpt/existing", status: "completed", conclusion: "failure", html_url: "https://gitea.example.com/runs/6" }, { id: 6, run_number: 6, run_attempt: 1, display_title: "ci: validate full", path: ".gitea/workflows/managed-validation.yml@refs/pull/1/head", event: "pull_request", head_sha: head, head_branch: "chatgpt/existing", pull_requests: [{ number: 1, head: { ref: "chatgpt/existing", sha: head } }], status: "completed", conclusion: "failure", html_url: "https://gitea.example.com/runs/6", jobs: [fullJob(6, head, "failure")] },
] }); ] });
const failedPrFull = await client.callTool({ name: "merge_feature_delivery", arguments: { project_id: slug, delivery_id: delivery.id, expected_head_sha: head, delete_branch: false } }); const failedPrFull = await client.callTool({ name: "merge_feature_delivery", arguments: { project_id: slug, delivery_id: delivery.id, expected_head_sha: head, delete_branch: false } });
expect(failedPrFull.isError, JSON.stringify(failedPrFull.content)).toBe(true); expect(failedPrFull.isError, JSON.stringify(failedPrFull.content)).toBe(true);
expect(merge).not.toHaveBeenCalled(); expect(merge).not.toHaveBeenCalled();
actionRuns.mockResolvedValue({ total_count: 2, workflow_runs: [ actionRuns.mockResolvedValue({ total_count: 2, workflow_runs: [
{ id: 2, run_number: 2, display_title: "Managed validation / full", path: ".gitea/workflows/managed-validation.yml", event: "pull_request", head_sha: head, head_branch: "chatgpt/existing", status: "completed", conclusion: "success", html_url: "https://gitea.example.com/runs/2" }, { id: 2, run_number: 2, run_attempt: 1, display_title: "ci: validate full", path: ".gitea/workflows/managed-validation.yml@refs/pull/1/head", event: "pull_request", head_sha: head, head_branch: "chatgpt/existing", pull_requests: [{ number: 1, head: { ref: "chatgpt/existing", sha: head } }], status: "completed", conclusion: "success", html_url: "https://gitea.example.com/runs/2", jobs: [fullJob(2, head)] },
{ id: 3, run_number: 3, display_title: "CI", path: ".gitea/workflows/ci.yml", event: "pull_request", head_sha: head, head_branch: "chatgpt/existing", status: "completed", conclusion: "success", html_url: "https://gitea.example.com/runs/3" }, { id: 3, run_number: 3, display_title: "CI", path: ".gitea/workflows/ci.yml", event: "pull_request", head_sha: head, head_branch: "chatgpt/existing", status: "completed", conclusion: "success", html_url: "https://gitea.example.com/runs/3" },
] }); ] });
const merged = await client.callTool({ name: "merge_feature_delivery", arguments: { project_id: slug, delivery_id: delivery.id, expected_head_sha: head, delete_branch: false } }); const merged = await client.callTool({ name: "merge_feature_delivery", arguments: { project_id: slug, delivery_id: delivery.id, expected_head_sha: head, delete_branch: false } });
+4 -1
View File
@@ -533,7 +533,8 @@ describe("ChatGPT feature delivery approval", () => {
it("requires every Gitea Actions workflow for the exact branch head to succeed", () => { it("requires every Gitea Actions workflow for the exact branch head to succeed", () => {
const run = (id: number, conclusion: string, headSha = "current-sha"): GiteaActionRun => ({ const run = (id: number, conclusion: string, headSha = "current-sha"): GiteaActionRun => ({
id, run_number: id, display_title: "Managed validation / full", path: ".gitea/workflows/managed-validation.yml", event: "workflow_dispatch", head_sha: headSha, id, run_number: id, display_title: "Managed validation / full", path: ".gitea/workflows/managed-validation.yml", event: "workflow_dispatch", head_sha: headSha,
head_branch: "chatgpt/feature", status: "completed", conclusion, html_url: `https://gitea.example.com/runs/${id}`, run_attempt: 1, head_branch: "chatgpt/feature", status: "completed", conclusion, html_url: `https://gitea.example.com/runs/${id}`,
jobs: [{ id: id * 10, html_url: `https://gitea.example.com/runs/${id}/jobs/${id * 10}`, run_id: id, name: "full", labels: [], run_attempt: 1, head_sha: headSha, head_branch: "chatgpt/feature", status: "completed", conclusion, runner_id: 1, steps: [], created_at: "2026-10-06T21:33:06Z" }],
}); });
expect(assertSuccessfulActionRuns([run(1, "success"), run(2, "success")], "chatgpt/feature", "current-sha")).toHaveLength(2); expect(assertSuccessfulActionRuns([run(1, "success"), run(2, "success")], "chatgpt/feature", "current-sha")).toHaveLength(2);
expect(() => assertSuccessfulActionRuns([run(1, "success"), run(2, "failure")], "chatgpt/feature", "current-sha")).toThrow("FEATURE_DELIVERY_FULL_VALIDATION_REQUIRED"); expect(() => assertSuccessfulActionRuns([run(1, "success"), run(2, "failure")], "chatgpt/feature", "current-sha")).toThrow("FEATURE_DELIVERY_FULL_VALIDATION_REQUIRED");
@@ -546,6 +547,7 @@ describe("ChatGPT feature delivery approval", () => {
const run = (overrides: Partial<GiteaActionRun> = {}): GiteaActionRun => ({ const run = (overrides: Partial<GiteaActionRun> = {}): GiteaActionRun => ({
id: 10, id: 10,
run_number: 10, run_number: 10,
run_attempt: 1,
display_title: "Managed validation / full", display_title: "Managed validation / full",
path: ".gitea/workflows/managed-validation.yml", path: ".gitea/workflows/managed-validation.yml",
event: "pull_request", event: "pull_request",
@@ -554,6 +556,7 @@ describe("ChatGPT feature delivery approval", () => {
status: "completed", status: "completed",
conclusion: "success", conclusion: "success",
html_url: "https://gitea.example.com/runs/10", html_url: "https://gitea.example.com/runs/10",
jobs: [{ id: 100, html_url: "https://gitea.example.com/runs/10/jobs/100", run_id: 10, name: "full", labels: [], run_attempt: 1, head_sha: headSha, head_branch: branch, status: "completed", conclusion: "success", runner_id: 1, steps: [], created_at: "2026-10-06T21:33:06Z" }],
...overrides, ...overrides,
}); });
+247 -6
View File
@@ -1,15 +1,33 @@
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import { GiteaClient } from "../src/integrations/gitea/client.js"; import { GiteaClient } from "../src/integrations/gitea/client.js";
import { assertSuccessfulActionRuns, exactHeadActionRuns } from "../src/integrations/gitea/policy.js"; import { assertSuccessfulActionRuns, assertSuccessfulPullRequestFullActionRun, exactHeadActionRuns } from "../src/integrations/gitea/policy.js";
import type { GiteaActionRun } from "../src/integrations/gitea/types.js"; import type { GiteaActionJob, GiteaActionRun } from "../src/integrations/gitea/types.js";
const branch = "codex/native-ci"; const branch = "codex/native-ci";
const sha = "a".repeat(40); const sha = "a".repeat(40);
const context = "CI / build-test (pull_request)"; const context = "CI / build-test (pull_request)";
const fullJob = (runId: number, headSha = sha, overrides: Partial<GiteaActionJob> = {}): GiteaActionJob => ({
id: runId * 10,
html_url: `https://gitea.example/runs/${runId}/jobs/${runId * 10}`,
run_id: runId,
name: "full",
labels: [],
run_attempt: 1,
head_sha: headSha,
head_branch: branch,
status: "completed",
conclusion: "success",
runner_id: 1,
steps: [],
created_at: "2026-10-06T21:33:06Z",
...overrides,
});
const run = (id: number, overrides: Partial<GiteaActionRun> = {}): GiteaActionRun => ({ const run = (id: number, overrides: Partial<GiteaActionRun> = {}): GiteaActionRun => ({
id, run_number: id, display_title: "Managed validation / full", path: ".gitea/workflows/managed-validation.yml", id, run_number: id, display_title: "Managed validation / full", path: ".gitea/workflows/managed-validation.yml",
run_attempt: 1,
event: "pull_request", head_sha: sha, head_branch: "", status: "completed", event: "pull_request", head_sha: sha, head_branch: "", status: "completed",
conclusion: "success", html_url: `https://gitea.example/runs/${id}`, ...overrides, conclusion: "success", html_url: `https://gitea.example/runs/${id}`, jobs: [fullJob(id)], ...overrides,
}); });
describe("native PR validation discovery", () => { describe("native PR validation discovery", () => {
@@ -24,6 +42,133 @@ describe("native PR validation discovery", () => {
expect(assertSuccessfulActionRuns([matching], branch, sha)).toEqual([matching]); expect(assertSuccessfulActionRuns([matching], branch, sha)).toEqual([matching]);
}); });
it("accepts Gitea 28 native PR full validation fields and its exact successful full job", () => {
const nativeRun = run(22352, {
run_number: 17,
display_title: "ci: validate QuizMonitor backend tests",
path: "managed-validation.yml@refs/pull/6/head",
head_branch: undefined,
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
jobs: [fullJob(22352, sha, { id: 38976, head_branch: undefined })],
});
expect(assertSuccessfulPullRequestFullActionRun([nativeRun], branch, sha)).toBe(nativeRun);
expect(assertSuccessfulPullRequestFullActionRun([nativeRun], branch, sha, 6)).toBe(nativeRun);
});
it("binds merge validation to the requested PR number despite a blank native head branch", () => {
const prSix = run(29, {
path: "managed-validation.yml@refs/pull/6/head",
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
head_branch: "",
});
expect(() => assertSuccessfulPullRequestFullActionRun([prSix], branch, sha, 99))
.toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
expect(() => assertSuccessfulPullRequestFullActionRun([run(30, {
path: "managed-validation.yml@refs/pull/6/head",
pull_requests: [{ number: 99, head: { ref: branch, sha } }],
head_branch: "",
})], branch, sha, 6)).toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
expect(() => assertSuccessfulPullRequestFullActionRun([run(34, {
path: ".gitea/workflows/managed-validation.yml",
head_branch: "",
})], branch, sha, 6)).toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
expect(() => assertSuccessfulPullRequestFullActionRun([run(35, {
path: "managed-validation.yml@refs/pull/6/head",
pull_requests: [{ number: 6, head: { ref: branch, sha } }, { number: 99, head: { ref: branch, sha } }],
head_branch: "",
})], branch, sha, 6)).toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
});
it("rejects a newer failed attempt for the requested PR when Gitea omits head_branch", () => {
const successful = run(31, {
run_number: 17,
run_attempt: 1,
path: "managed-validation.yml@refs/pull/6/head",
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
head_branch: "",
jobs: [fullJob(31)],
});
const failed = run(32, {
run_number: 17,
run_attempt: 2,
path: "managed-validation.yml@refs/pull/6/head",
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
head_branch: "",
jobs: [fullJob(32, sha, { run_attempt: 2, conclusion: "failure" })],
});
expect(() => assertSuccessfulPullRequestFullActionRun([successful, failed], branch, sha, 6))
.toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
});
it("keeps failed noncanonical PR workflows in the exact-head merge gate", () => {
const full = run(37, {
path: "managed-validation.yml@refs/pull/6/head",
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
head_branch: "",
});
const failedQualityWorkflow = run(38, {
path: ".gitea/workflows/security-check.yml@refs/pull/6/head",
conclusion: "failure",
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
head_branch: "",
jobs: undefined,
});
expect(() => assertSuccessfulPullRequestFullActionRun([full, failedQualityWorkflow], branch, sha, 6))
.toThrow("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL");
});
it("rejects malformed path metadata without throwing a generic runtime error", () => {
const valid = run(33, {
path: "managed-validation.yml@refs/pull/6/head",
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
head_branch: "",
});
const malformed = run(34, { event: "push", path: null as unknown as string, head_branch: branch });
expect(() => assertSuccessfulPullRequestFullActionRun([valid, malformed], branch, sha, 6))
.toThrow("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL");
});
it.each([
["job id", { id: undefined as unknown as number }],
["run id", { run_id: undefined as unknown as number }],
["attempt", { run_attempt: undefined as unknown as number }],
["head SHA", { head_sha: undefined as unknown as string }],
])("rejects a full job missing its %s identity", (_label, overrides) => {
expect(() => assertSuccessfulPullRequestFullActionRun([run(36, { jobs: [fullJob(36, sha, overrides)] })], branch, sha))
.toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
});
it.each([
["missing", []],
["failed", [fullJob(20, sha, { conclusion: "failure" })]],
["wrong run", [fullJob(21, sha, { run_id: 999 })]],
["wrong attempt", [fullJob(22, sha, { run_attempt: 2 })]],
["wrong head", [fullJob(23, "b".repeat(40))]],
["duplicate", [fullJob(24), fullJob(24, sha, { id: 241 })]],
])("rejects a %s canonical full job", (_label, jobs) => {
expect(() => assertSuccessfulPullRequestFullActionRun([run(20, { jobs })], branch, sha))
.toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
});
it("does not let an older successful attempt authorize a newer failed full attempt", () => {
const successful = run(25, { run_number: 17, run_attempt: 1, jobs: [fullJob(25)] });
const failed = run(26, {
run_number: 17,
run_attempt: 2,
jobs: [fullJob(26, sha, { run_attempt: 2, conclusion: "failure" })],
});
expect(() => assertSuccessfulPullRequestFullActionRun([successful, failed], branch, sha))
.toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
});
it.each(["untrusted/managed-validation.yml", "other.yml"]) ("does not treat %s or its title as canonical", (path) => {
expect(() => assertSuccessfulPullRequestFullActionRun([run(27, {
path,
display_title: "Managed validation / full",
})], branch, sha)).toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
});
it.each(["pending", "queued", "waiting", "in_progress"])("does not authorize %s", (status) => { it.each(["pending", "queued", "waiting", "in_progress"])("does not authorize %s", (status) => {
expect(() => assertSuccessfulActionRuns([run(1, { status, conclusion: undefined })], branch, sha)).toThrow("FEATURE_DELIVERY_FULL_VALIDATION_REQUIRED"); expect(() => assertSuccessfulActionRuns([run(1, { status, conclusion: undefined })], branch, sha)).toThrow("FEATURE_DELIVERY_FULL_VALIDATION_REQUIRED");
}); });
@@ -47,12 +192,108 @@ describe("native PR validation discovery", () => {
], branch, sha)).toThrow("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL"); ], branch, sha)).toThrow("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL");
}); });
it("queries by exact SHA without the upstream branch filter", async () => { it("queries by exact SHA and fetches jobs for the exact run attempt", async () => {
const fetcher = vi.fn(async () => new Response(JSON.stringify({ total_count: 1, workflow_runs: [run(1)] }))); const workflowRun = run(1);
await new GiteaClient("https://gitea.example", "test-only", fetcher as typeof fetch).listActionRuns("Jens", "Repo", { branch, headSha: sha }); const job = fullJob(1);
const fetcher = vi.fn(async (input: string | URL | Request) => {
const url = new URL(String(input));
if (url.pathname.endsWith("/actions/runs")) return new Response(JSON.stringify({ total_count: 1, workflow_runs: [workflowRun] }));
if (url.pathname.endsWith("/actions/runs/1/attempts/1/jobs")) return new Response(JSON.stringify({ total_count: 1, jobs: [job] }));
return new Response("unexpected request", { status: 404 });
});
const result = await new GiteaClient("https://gitea.example", "test-only", fetcher as typeof fetch).listActionRuns("Jens", "Repo", { branch, headSha: sha });
const url = new URL(String(fetcher.mock.calls[0]![0])); const url = new URL(String(fetcher.mock.calls[0]![0]));
expect(url.searchParams.get("head_sha")).toBe(sha); expect(url.searchParams.get("head_sha")).toBe(sha);
expect(url.searchParams.has("branch")).toBe(false); expect(url.searchParams.has("branch")).toBe(false);
expect(result.workflow_runs[0]?.jobs).toEqual([job]);
expect(fetcher).toHaveBeenCalledTimes(2);
expect(new URL(String(fetcher.mock.calls[1]![0])).searchParams.get("limit")).toBe("100");
});
it("only fetches validation jobs for the requested PR when another PR has the same head SHA", async () => {
const unrelated = run(40, {
path: "managed-validation.yml@refs/pull/99/head",
pull_requests: [{ number: 99, head: { ref: branch, sha } }],
head_branch: "",
jobs: undefined,
});
const expected = run(41, {
path: "managed-validation.yml@refs/pull/6/head",
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
head_branch: "",
jobs: undefined,
});
const fetcher = vi.fn(async (input: string | URL | Request) => {
const url = new URL(String(input));
if (url.pathname.endsWith("/actions/runs")) return new Response(JSON.stringify({ total_count: 2, workflow_runs: [unrelated, expected] }));
if (url.pathname.endsWith("/actions/runs/41/attempts/1/jobs")) return new Response(JSON.stringify({ total_count: 1, jobs: [fullJob(41)] }));
return new Response("unexpected request", { status: 404 });
});
const result = await new GiteaClient("https://gitea.example", "test-only", fetcher as typeof fetch)
.listActionRuns("Jens", "Repo", { branch, headSha: sha, pullRequestNumber: 6 });
expect(result.workflow_runs.find((item) => item.id === 41)?.jobs).toHaveLength(1);
expect(result.workflow_runs.find((item) => item.id === 40)?.jobs).toBeUndefined();
expect(fetcher).toHaveBeenCalledTimes(2);
});
it("rejects duplicate job identities across paginated job results", async () => {
const workflowRun = run(42);
const firstPage = Array.from({ length: 100 }, (_, index) => fullJob(42, sha, { id: index + 1 }));
const secondPage = [fullJob(42, sha, { id: 100 })];
const fetcher = vi.fn(async (input: string | URL | Request) => {
const url = new URL(String(input));
if (url.pathname.endsWith("/actions/runs")) return new Response(JSON.stringify({ total_count: 1, workflow_runs: [workflowRun] }));
if (url.pathname.endsWith("/attempts/1/jobs")) {
return new Response(JSON.stringify({ total_count: 101, jobs: url.searchParams.get("page") === "1" ? firstPage : secondPage }));
}
return new Response("unexpected request", { status: 404 });
});
await expect(new GiteaClient("https://gitea.example", "test-only", fetcher as typeof fetch)
.listActionRuns("Jens", "Repo", { branch, headSha: sha })).rejects.toMatchObject({ code: "GITEA_ACTION_JOBS_INVALID" });
});
it("paginates the exact-head run inventory so a later failed PR run blocks merge", async () => {
const successful = run(43, {
run_number: 17,
path: "managed-validation.yml@refs/pull/6/head",
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
head_branch: "",
jobs: undefined,
});
const failed = run(44, {
run_number: 18,
path: "managed-validation.yml@refs/pull/6/head",
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
head_branch: "",
conclusion: "failure",
jobs: undefined,
});
const fetcher = vi.fn(async (input: string | URL | Request) => {
const url = new URL(String(input));
if (url.pathname.endsWith("/actions/runs")) {
return new Response(JSON.stringify({ total_count: 2, workflow_runs: url.searchParams.get("page") === "1" ? [successful] : [failed] }));
}
if (url.pathname.endsWith("/actions/runs/44/attempts/1/jobs")) {
return new Response(JSON.stringify({ total_count: 1, jobs: [fullJob(44, sha, { conclusion: "failure" })] }));
}
return new Response("unexpected request", { status: 404 });
});
const result = await new GiteaClient("https://gitea.example", "test-only", fetcher as typeof fetch)
.listActionRuns("Jens", "Repo", { branch, headSha: sha, pullRequestNumber: 6 });
expect(result.workflow_runs).toHaveLength(2);
expect(fetcher).toHaveBeenCalledTimes(3);
expect(() => assertSuccessfulPullRequestFullActionRun(result.workflow_runs, branch, sha, 6))
.toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
});
it("rejects repeated exact-head run identities across API pages", async () => {
const repeated = run(45);
const fetcher = vi.fn(async (input: string | URL | Request) => {
const url = new URL(String(input));
return new Response(JSON.stringify({ total_count: 2, workflow_runs: [repeated] }));
});
await expect(new GiteaClient("https://gitea.example", "test-only", fetcher as typeof fetch)
.listActionRuns("Jens", "Repo", { branch, headSha: sha })).rejects.toMatchObject({ code: "GITEA_ACTION_RUNS_INVALID" });
}); });
it.each([ it.each([
@@ -15,6 +15,7 @@ const actionRun = (input: {
}): GiteaActionRun => ({ }): GiteaActionRun => ({
id: input.id, id: input.id,
run_number: input.id, run_number: input.id,
run_attempt: 1,
display_title: input.workflow ?? "Managed validation / full", display_title: input.workflow ?? "Managed validation / full",
path: `.gitea/workflows/${input.workflow ?? "managed-validation.yml"}`, path: `.gitea/workflows/${input.workflow ?? "managed-validation.yml"}`,
event: input.workflow ? "pull_request" : "workflow_dispatch", event: input.workflow ? "pull_request" : "workflow_dispatch",
@@ -23,6 +24,21 @@ const actionRun = (input: {
status: input.status ?? "completed", status: input.status ?? "completed",
conclusion: input.conclusion, conclusion: input.conclusion,
html_url: `https://gitea.example.com/runs/${input.id}`, html_url: `https://gitea.example.com/runs/${input.id}`,
jobs: input.workflow ? [] : [{
id: input.id * 10,
html_url: `https://gitea.example.com/runs/${input.id}/jobs/${input.id * 10}`,
run_id: input.id,
name: "full",
labels: [],
run_attempt: 1,
head_sha: input.headSha ?? "current-sha",
head_branch: "chatgpt/feature",
status: input.status ?? "completed",
conclusion: input.conclusion,
runner_id: 1,
steps: [],
created_at: "2026-10-06T21:33:06Z",
}],
}); });
const productionEnv = { const productionEnv = {