Publish curated projectbrain source from 0244fbf232898e6dc076ff2159d96a6da55ab5b5

This commit is contained in:
Public Source Publisher committed 2026-10-06 22:49:00 +00:00
1 parent eca2891b07
commit d5c9615684
10 files changed
+170 -31

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 `88fab8df1542019289e947f092c2b387e0825c60`. 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 `0244fbf232898e6dc076ff2159d96a6da55ab5b5`. It contains no private Git history or operational evidence. Changes are published from the canonical repository.
+23 -13
View File
@@ -1,6 +1,6 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"sourceRevision": "88fab8df1542019289e947f092c2b387e0825c60", "sourceRevision": "0244fbf232898e6dc076ff2159d96a6da55ab5b5",
"files": [ "files": [
{ {
"path": ".dockerignore", "path": ".dockerignore",
@@ -309,13 +309,18 @@
}, },
{ {
"path": "backend/src/integrations/gitea/client.ts", "path": "backend/src/integrations/gitea/client.ts",
"sha256": "f5b8e15f010582a06d241e7c316931a84ecff767165a035f090fb14385570642", "sha256": "1f88276e1174e47d262b58445c374915fbe835a27e1b1254f0bfe77f987767ca",
"bytes": 16437 "bytes": 16592
},
{
"path": "backend/src/integrations/gitea/compatibility.ts",
"sha256": "3572debae1ba2a1109ff3e2193577448e7ae57d23932c77e9a6dd44040540b17",
"bytes": 1356
}, },
{ {
"path": "backend/src/integrations/gitea/policy.ts", "path": "backend/src/integrations/gitea/policy.ts",
"sha256": "4c9d46a0b75fa235eda9e96ddfff72f975b6015da735c009fd10186d6a7ba6db", "sha256": "677d93e603f78c6a80cde5a7e2bc4c5529f507a9fa3780f1825bb29d3c4b584d",
"bytes": 5628 "bytes": 6240
}, },
{ {
"path": "backend/src/integrations/gitea/portfolio.ts", "path": "backend/src/integrations/gitea/portfolio.ts",
@@ -364,8 +369,8 @@
}, },
{ {
"path": "backend/src/mcp/server.ts", "path": "backend/src/mcp/server.ts",
"sha256": "988dbb35051a854e71a92f08e34cb4b8ef415467f7857a28bab82f337e151258", "sha256": "c50d62ce8c32e73b820d4cd0a6b4ea64118a758cf8e8e6bdd3da43f37c670ce6",
"bytes": 78383 "bytes": 78439
}, },
{ {
"path": "backend/src/mcp/skills.ts", "path": "backend/src/mcp/skills.ts",
@@ -589,8 +594,8 @@
}, },
{ {
"path": "backend/src/routes/giteaIntegration.ts", "path": "backend/src/routes/giteaIntegration.ts",
"sha256": "72ab237e51764caca06633579045af8b9a5d3c79f15984795d81a3a4220315c2", "sha256": "69dc078288aa0aa860ab4f09a97a93a28e6786047d602723f393c6b27410d8bf",
"bytes": 45137 "bytes": 46130
}, },
{ {
"path": "backend/src/routes/health.ts", "path": "backend/src/routes/health.ts",
@@ -979,8 +984,8 @@
}, },
{ {
"path": "backend/test/database-autonomy.test.ts", "path": "backend/test/database-autonomy.test.ts",
"sha256": "026e683c392e3b17bfea586327944a030a364b4f3c21312b0b8655aec30fdfcb", "sha256": "eca93db598282b142003bb9ffabb1a578b93175da36004a909ee112bbf1a4f52",
"bytes": 47937 "bytes": 50622
}, },
{ {
"path": "backend/test/database-repository-sync.test.ts", "path": "backend/test/database-repository-sync.test.ts",
@@ -1022,10 +1027,15 @@
"sha256": "f190c40c5cbebfe78b4705acd687b7a9429d90ca39ac328068c6ce47566adb96", "sha256": "f190c40c5cbebfe78b4705acd687b7a9429d90ca39ac328068c6ce47566adb96",
"bytes": 1127 "bytes": 1127
}, },
{
"path": "backend/test/gitea-compatibility.test.ts",
"sha256": "b92c6b4d3b84692b7d7076a2b0011cb0709f200f17a674edc75ed5494cdbcb17",
"bytes": 2538
},
{ {
"path": "backend/test/gitea-integration.test.ts", "path": "backend/test/gitea-integration.test.ts",
"sha256": "fedc1f48b814c0bbd79c80823f3b76f3210c8935a602e845b133dfc026dc7097", "sha256": "2b7fb94dd40bd50c9e0aa2147540e690d8275ca37f2d948aaeb6ed9128b24b0e",
"bytes": 36414 "bytes": 37780
}, },
{ {
"path": "backend/test/gitea-portfolio-mcp.test.ts", "path": "backend/test/gitea-portfolio-mcp.test.ts",
+16 -8
View File
@@ -10,9 +10,10 @@ type RequestOptions = { method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; qu
type GiteaBranchProtection = { enable_status_check?: boolean; status_check_contexts?: string[] }; type GiteaBranchProtection = { enable_status_check?: boolean; status_check_contexts?: string[] };
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";
export class GiteaClient { export class GiteaClient {
private readonly apiBase: string; private readonly apiBase: string;
private mergeStrategyKey?: "Do" | "do";
constructor(baseUrl: string, private readonly token: string, private readonly fetcher: typeof fetch = fetch) { constructor(baseUrl: string, private readonly token: string, private readonly fetcher: typeof fetch = fetch) {
this.apiBase = `${baseUrl.replace(/\/+$/, "")}/api/v1`; this.apiBase = `${baseUrl.replace(/\/+$/, "")}/api/v1`;
@@ -160,14 +161,21 @@ export class GiteaClient {
return this.request<GiteaPullRequest>(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/pulls`, { method: "POST", body: input }); return this.request<GiteaPullRequest>(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/pulls`, { method: "POST", body: input });
} }
async getUpstreamCompatibility() {
const payload = await this.request<{ version?: unknown } | null>("/version");
return {
source: "upstream:/api/v1/version",
testedRange: GITEA_TESTED_MERGE_RANGE,
checkedAt: new Date().toISOString(),
...giteaMergeCompatibility(payload?.version),
};
}
private async getMergeStrategyKey(): Promise<"Do" | "do"> { private async getMergeStrategyKey(): Promise<"Do" | "do"> {
if (this.mergeStrategyKey) return this.mergeStrategyKey; // A server upgrade must not inherit an indefinitely cached merge contract.
const { version } = await this.request<{ version?: string }>("/version"); const compatibility = await this.getUpstreamCompatibility();
const legacyMatch = /^1\.(14|25|26|27)(?:\.\d+)?(?:[-+].*)?$/.exec(version ?? ""); if (!compatibility.supported || !compatibility.strategyKey) throw new GiteaApiError(500, "GITEA_VERSION_UNSUPPORTED", `Gitea upstream merge negotiation failed: ${compatibility.reason}; tested ${compatibility.testedRange}`);
const modernMatch = /^(28)(?:\.\d+){1,2}(?:[-+].*)?$/.exec(version ?? ""); return compatibility.strategyKey;
if (!legacyMatch && !modernMatch) throw new GiteaApiError(500, "GITEA_VERSION_UNSUPPORTED", "Gitea merge compatibility requires an identified 1.14, 1.25, 1.26, 1.27, or 28.x server");
this.mergeStrategyKey = legacyMatch && ["14", "25"].includes(legacyMatch[1] ?? "") ? "Do" : "do";
return this.mergeStrategyKey;
} }
async mergePullRequest(owner: string, repository: string, number: number, input: { method: "merge" | "rebase" | "rebase-merge" | "squash" | "fast-forward-only"; headCommitId: string }) { async mergePullRequest(owner: string, repository: string, number: number, input: { method: "merge" | "rebase" | "rebase-merge" | "squash" | "fast-forward-only"; headCommitId: string }) {
@@ -0,0 +1,19 @@
/** Upstream /api/v1/version negotiation; never accepts connector build metadata. */
export const GITEA_TESTED_MERGE_RANGE = ">=1.25.0 <1.28.0 || >=28.0.0 <29.0.0 (stable); legacy 1.14.x";
export function giteaMergeCompatibility(value: unknown) {
const match = typeof value === "string" && value.length <= 128
? /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/.exec(value.trim())
: null;
if (!match) return { supported: false, reason: "upstream_version_missing_or_malformed", normalizedVersion: null, strategyKey: null } as const;
const [major, minor, patch] = match.slice(1, 4).map(Number);
if (![major, minor, patch].every(Number.isSafeInteger)) return { supported: false, reason: "upstream_version_missing_or_malformed", normalizedVersion: null, strategyKey: null } as const;
const normalizedVersion = `${major}.${minor}.${patch}${match[4] ? `-${match[4]}` : ""}${match[5] ? `+${match[5]}` : ""}`;
const supported = !match[4] && ((major === 1 && (minor === 14 || (minor >= 25 && minor < 28))) || major === 28);
return {
supported,
reason: supported ? "tested_merge_api_contract" : "upstream_version_outside_tested_range",
normalizedVersion,
strategyKey: supported ? (major === 1 && minor <= 25 ? "Do" : "do") : null,
} as const;
}
+11
View File
@@ -88,6 +88,17 @@ export const successfulFullActionRun = (runs: GiteaActionRun[], branch: string,
return latest; return latest;
}; };
export const assertSuccessfulPullRequestFullActionRun = (runs: GiteaActionRun[], branch: string, headSha: string): GiteaActionRun => {
const latest = exactHeadActionRuns(runs, branch, headSha)
.filter((run) => run.event === "pull_request" && isManagedFullValidationRun(run))
.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");
}
assertSuccessfulActionRuns(runs, branch, headSha);
return latest;
};
export const assertSuccessfulActionRuns = (runs: GiteaActionRun[], branch: string, headSha: string): GiteaActionRun[] => { export const assertSuccessfulActionRuns = (runs: GiteaActionRun[], branch: string, headSha: string): GiteaActionRun[] => {
const exactRuns = exactHeadActionRuns(runs, branch, headSha); const exactRuns = exactHeadActionRuns(runs, branch, headSha);
if (exactRuns.length === 0) throw new Error("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL"); if (exactRuns.length === 0) throw new Error("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL");
+2 -2
View File
@@ -3,7 +3,7 @@ import { z } from "zod";
import type { AppEnv } from "../config/env.js"; import type { AppEnv } from "../config/env.js";
import { prisma } from "../db/prisma.js"; import { prisma } from "../db/prisma.js";
import { GiteaApiError } from "../integrations/gitea/client.js"; import { GiteaApiError } from "../integrations/gitea/client.js";
import { assertChatGptBranch, assertExpectedFileSha, assertFileSize, assertReadablePath, assertSuccessfulActionRuns, assertWritablePath, exactHeadActionRuns, findReusableDeliveryPullRequest, successfulFullActionRun } from "../integrations/gitea/policy.js"; import { assertChatGptBranch, assertExpectedFileSha, assertFileSize, assertReadablePath, assertSuccessfulActionRuns, assertSuccessfulPullRequestFullActionRun, assertWritablePath, exactHeadActionRuns, findReusableDeliveryPullRequest, successfulFullActionRun } from "../integrations/gitea/policy.js";
import { finishExternalOperation, getGiteaClient, listLinkedRepositories, markExternalOperationExecuting, prepareExternalOperation, recordMcpAudit, type LinkedRepository } from "../integrations/gitea/service.js"; import { finishExternalOperation, getGiteaClient, listLinkedRepositories, markExternalOperationExecuting, prepareExternalOperation, recordMcpAudit, type LinkedRepository } from "../integrations/gitea/service.js";
import type { GiteaContent } from "../integrations/gitea/types.js"; import type { GiteaContent } from "../integrations/gitea/types.js";
import { assertFeatureDeliveryActionEvidence, canSyncFeatureDeliveryValidation, getFeatureDelivery, listFeatureDeliveries, openFeatureDeliveryPullRequest, reconcileFeatureDeliveryPullRequest, recordFeatureDeliveryActionValidation, recordFeatureDeliveryProgress, startFeatureDelivery, updateFeatureDelivery } from "../repositories/featureDeliveryRepository.js"; import { assertFeatureDeliveryActionEvidence, canSyncFeatureDeliveryValidation, getFeatureDelivery, listFeatureDeliveries, openFeatureDeliveryPullRequest, reconcileFeatureDeliveryPullRequest, recordFeatureDeliveryActionValidation, recordFeatureDeliveryProgress, startFeatureDelivery, updateFeatureDelivery } from "../repositories/featureDeliveryRepository.js";
@@ -778,7 +778,7 @@ export const createProjectBrainMcpServer = (env: AppEnv, actor: string) => {
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, limit: 30 });
assertSuccessfulActionRuns(actionRuns.workflow_runs, branch, expected_head_sha); assertSuccessfulPullRequestFullActionRun(actionRuns.workflow_runs, branch, expected_head_sha);
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);
+10 -4
View File
@@ -5,7 +5,7 @@ import { z } from "zod";
import type { AppEnv } from "../config/env.js"; import type { AppEnv } from "../config/env.js";
import { prisma } from "../db/prisma.js"; import { prisma } from "../db/prisma.js";
import { GiteaApiError } from "../integrations/gitea/client.js"; import { GiteaApiError } from "../integrations/gitea/client.js";
import { assertExpectedFileSha, assertFileSize, assertProjectBrainBranch, assertReadablePath, assertSuccessfulActionRuns, assertWritablePath, findReusableDeliveryPullRequest, successfulFullActionRun } from "../integrations/gitea/policy.js"; import { assertExpectedFileSha, assertFileSize, assertProjectBrainBranch, assertReadablePath, assertSuccessfulActionRuns, assertSuccessfulPullRequestFullActionRun, assertWritablePath, findReusableDeliveryPullRequest, successfulFullActionRun } from "../integrations/gitea/policy.js";
import { finishExternalOperation, getGiteaClient, getLinkedRepository, GiteaRepositoryOwnershipConflictError, GiteaRepositoryTransferBlockedError, linkRepository, listLinkedRepositories, listProjectRepositories, markExternalOperationExecuting, prepareExternalOperation, recordMcpAudit, unlinkRepository, unlinkRepositoryById, type LinkedRepository } from "../integrations/gitea/service.js"; import { finishExternalOperation, getGiteaClient, getLinkedRepository, GiteaRepositoryOwnershipConflictError, GiteaRepositoryTransferBlockedError, linkRepository, listLinkedRepositories, listProjectRepositories, markExternalOperationExecuting, prepareExternalOperation, recordMcpAudit, unlinkRepository, unlinkRepositoryById, type LinkedRepository } from "../integrations/gitea/service.js";
import type { GiteaContent } from "../integrations/gitea/types.js"; import type { GiteaContent } from "../integrations/gitea/types.js";
import { assertFeatureDeliveryActionEvidence, getFeatureDelivery, openFeatureDeliveryPullRequest, reconcileFeatureDeliveryPullRequest, recordFeatureDeliveryActionValidation, recordFeatureDeliveryProgress, startFeatureDelivery } from "../repositories/featureDeliveryRepository.js"; import { assertFeatureDeliveryActionEvidence, getFeatureDelivery, openFeatureDeliveryPullRequest, reconcileFeatureDeliveryPullRequest, recordFeatureDeliveryActionValidation, recordFeatureDeliveryProgress, startFeatureDelivery } from "../repositories/featureDeliveryRepository.js";
@@ -200,8 +200,9 @@ export const registerGiteaIntegrationRoutes = async (app: FastifyInstance, optio
const configured = Boolean(env.PROJECTBRAIN_GITEA_URL && env.PROJECTBRAIN_GITEA_TOKEN); const configured = Boolean(env.PROJECTBRAIN_GITEA_URL && env.PROJECTBRAIN_GITEA_TOKEN);
if (!configured) return successEnvelope({ configured: false, reachable: false, mcpEnabled: env.PROJECTBRAIN_MCP_ENABLED, publicUrl: env.PROJECTBRAIN_PUBLIC_URL, linkedRepositories: 0 }, getRequestId(request)); if (!configured) return successEnvelope({ configured: false, reachable: false, mcpEnabled: env.PROJECTBRAIN_MCP_ENABLED, publicUrl: env.PROJECTBRAIN_PUBLIC_URL, linkedRepositories: 0 }, getRequestId(request));
try { try {
const [user, linkedRepositories] = await Promise.all([getGiteaClient(env).getCurrentUser(), prisma.giteaRepositoryLink.count()]); const client = getGiteaClient(env);
return successEnvelope({ configured: true, reachable: true, baseUrl: env.PROJECTBRAIN_GITEA_URL, identity: user.login, mcpEnabled: env.PROJECTBRAIN_MCP_ENABLED, mcpUrl: env.PROJECTBRAIN_PUBLIC_URL ? `${env.PROJECTBRAIN_PUBLIC_URL.replace(/\/$/, "")}/mcp` : undefined, linkedRepositories }, getRequestId(request)); const [user, linkedRepositories, upstreamGitea] = await Promise.all([client.getCurrentUser(), prisma.giteaRepositoryLink.count(), client.getUpstreamCompatibility()]);
return successEnvelope({ configured: true, reachable: true, baseUrl: env.PROJECTBRAIN_GITEA_URL, identity: user.login, upstreamGitea, mcpEnabled: env.PROJECTBRAIN_MCP_ENABLED, mcpUrl: env.PROJECTBRAIN_PUBLIC_URL ? `${env.PROJECTBRAIN_PUBLIC_URL.replace(/\/$/, "")}/mcp` : undefined, linkedRepositories }, getRequestId(request));
} catch (error) { } catch (error) {
return successEnvelope({ configured: true, reachable: false, baseUrl: env.PROJECTBRAIN_GITEA_URL, mcpEnabled: env.PROJECTBRAIN_MCP_ENABLED, error: integrationError(error).message, linkedRepositories: await prisma.giteaRepositoryLink.count() }, getRequestId(request)); return successEnvelope({ configured: true, reachable: false, baseUrl: env.PROJECTBRAIN_GITEA_URL, mcpEnabled: env.PROJECTBRAIN_MCP_ENABLED, error: integrationError(error).message, linkedRepositories: await prisma.giteaRepositoryLink.count() }, getRequestId(request));
} }
@@ -406,7 +407,12 @@ export const registerGiteaIntegrationRoutes = async (app: FastifyInstance, optio
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, limit: 30 });
assertSuccessfulActionRuns(actionRuns.workflow_runs, branch, parsed.data.expectedHeadSha); assertSuccessfulPullRequestFullActionRun(actionRuns.workflow_runs, branch, 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 mergeReadyHead = await client.getBranch(link.owner, link.repository, branch);
if (mergeReadyPullRequest.state !== "open" || mergeReadyPullRequest.merged || mergeReadyPullRequest.head.ref !== branch || mergeReadyPullRequest.base.ref !== link.defaultBranch || mergeReadyPullRequest.head.sha !== parsed.data.expectedHeadSha || mergeReadyHead.commit.id !== parsed.data.expectedHeadSha) throw new Error("FEATURE_DELIVERY_MERGE_HEAD_CONFLICT");
if (mergeReadyPullRequest.draft === true || mergeReadyPullRequest.mergeable === false) throw new Error("FEATURE_DELIVERY_PULL_REQUEST_NOT_READY");
auditId = (await beginWorkbenchMutation({ link, actor: request.authUser?.username ?? "owner", toolName: "merge_pull_request", branch, started })).operationId; auditId = (await beginWorkbenchMutation({ link, actor: request.authUser?.username ?? "owner", toolName: "merge_pull_request", branch, started })).operationId;
await client.mergePullRequest(link.owner, link.repository, number, { method: parsed.data.method, headCommitId: parsed.data.expectedHeadSha }); await client.mergePullRequest(link.owner, link.repository, number, { method: parsed.data.method, headCommitId: parsed.data.expectedHeadSha });
const merged = await client.getPullRequest(link.owner, link.repository, number); const merged = await client.getPullRequest(link.owner, link.repository, number);
+27 -1
View File
@@ -301,7 +301,7 @@ databaseDescribe("autonomy database write isolation", () => {
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({});
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, 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: 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" },
] }); ] });
@@ -323,6 +323,32 @@ databaseDescribe("autonomy database write isolation", () => {
expect(repaired.spec.validationResults).toEqual([]); expect(repaired.spec.validationResults).toEqual([]);
const synced = await client.callTool({ name: "sync_feature_validation", arguments: { project_id: slug, delivery_id: delivery.id } }); const synced = await client.callTool({ name: "sync_feature_validation", arguments: { project_id: slug, delivery_id: delivery.id } });
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: [
{ 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" },
] });
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(merge).not.toHaveBeenCalled();
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" },
] });
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(merge).not.toHaveBeenCalled();
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" },
] });
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(merge).not.toHaveBeenCalled();
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: 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 } });
expect(merged.isError, JSON.stringify(merged.content)).not.toBe(true); expect(merged.isError, JSON.stringify(merged.content)).not.toBe(true);
expect(merge).toHaveBeenCalledTimes(1); expect(merge).toHaveBeenCalledTimes(1);
+36
View File
@@ -0,0 +1,36 @@
import { describe, expect, it, vi } from "vitest";
import { giteaMergeCompatibility } from "../src/integrations/gitea/compatibility.js";
import { GiteaClient } from "../src/integrations/gitea/client.js";
describe("upstream Gitea merge negotiation", () => {
it.each(["28.0.0", "v28.0.0", "28.0.0+gitea.build-123", " v1.27.3+build.42 ", "1.25.0"])("accepts attested stable version %s", (value) => {
expect(giteaMergeCompatibility(value)).toMatchObject({ supported: true });
});
it.each([undefined, null, {}, 28, "", "1.24.9", "29.0.0", "2.0.0", "28.0", "028.0.0", "28.0.0+", "28.0.0+bad/metadata", "28.0.0-rc.1", "28.0.0-garbage suffix", "28.0.0+" + "a".repeat(128)])("fails closed for %s", (value) => {
expect(giteaMergeCompatibility(value).supported).toBe(false);
});
it("does not accept a connector version in place of the upstream version", async () => {
const fetcher = vi.fn(async () => new Response(JSON.stringify({ connectorVersion: "28.0.0", version: "1.24.9" })));
const client = new GiteaClient("https://gitea.example.com", "test-token", fetcher as typeof fetch);
expect(await client.getUpstreamCompatibility()).toMatchObject({ supported: false, source: "upstream:/api/v1/version", normalizedVersion: "1.24.9" });
});
it("uses current upstream 28 when connector build is 1.14", async () => {
const fetcher = vi.fn(async () => new Response(JSON.stringify({ connectorVersion: "1.14.0", version: "28.0.0" })));
const client = new GiteaClient("https://gitea.example.com", "test-token", fetcher as typeof fetch);
expect(await client.getUpstreamCompatibility()).toMatchObject({ supported: true, normalizedVersion: "28.0.0", strategyKey: "do" });
});
it("re-negotiates before each merge and blocks an unsupported server upgrade", async () => {
let version = "28.0.0";
let writes = 0;
const fetcher = vi.fn(async (input: string | URL | Request) => {
if (String(input).endsWith("/version")) return new Response(JSON.stringify({ version }));
writes += 1;
return new Response(null, { status: 204 });
});
const client = new GiteaClient("https://gitea.example.com", "test-token", fetcher as typeof fetch);
await client.mergePullRequest("Jens", "canary", 1, { method: "squash", headCommitId: "a".repeat(40) });
version = "29.0.0";
await expect(client.mergePullRequest("Jens", "canary", 1, { method: "squash", headCommitId: "a".repeat(40) })).rejects.toMatchObject({ code: "GITEA_VERSION_UNSUPPORTED" });
expect(writes).toBe(1);
});
});
+25 -2
View File
@@ -1,7 +1,7 @@
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import { loadEnv } from "../src/config/env.js"; import { loadEnv } from "../src/config/env.js";
import { GiteaClient, GiteaApiError } from "../src/integrations/gitea/client.js"; import { GiteaClient, GiteaApiError } from "../src/integrations/gitea/client.js";
import { assertChatGptBranch, assertExpectedFileSha, assertFileSize, assertProjectBrainBranch, assertReadablePath, assertSuccessfulActionRuns, normalizeRepositoryPath } from "../src/integrations/gitea/policy.js"; import { assertChatGptBranch, assertExpectedFileSha, assertFileSize, assertProjectBrainBranch, assertReadablePath, assertSuccessfulActionRuns, assertSuccessfulPullRequestFullActionRun, normalizeRepositoryPath } from "../src/integrations/gitea/policy.js";
import type { GiteaActionRun } from "../src/integrations/gitea/types.js"; import type { GiteaActionRun } from "../src/integrations/gitea/types.js";
import { exchangeAuthorizationCode, exchangeRefreshToken, issueAuthorizationCode, registerOAuthClient, verifyMcpAccessToken } from "../src/security/mcpOAuth.js"; import { exchangeAuthorizationCode, exchangeRefreshToken, issueAuthorizationCode, registerOAuthClient, verifyMcpAccessToken } from "../src/security/mcpOAuth.js";
import { createHash, createHmac } from "node:crypto"; import { createHash, createHmac } from "node:crypto";
@@ -295,7 +295,7 @@ describe("Gitea API client", () => {
await client.mergePullRequest("Acme", "Repo", 17, { method: "merge", headCommitId: "b".repeat(40) }); await client.mergePullRequest("Acme", "Repo", 17, { method: "merge", headCommitId: "b".repeat(40) });
await client.mergePullRequest("Acme", "Repo", 18, { method: "squash", headCommitId: "c".repeat(40) }); await client.mergePullRequest("Acme", "Repo", 18, { method: "squash", headCommitId: "c".repeat(40) });
expect(fetcher.mock.calls.filter(([input]) => String(input).endsWith("/version"))).toHaveLength(1); expect(fetcher.mock.calls.filter(([input]) => String(input).endsWith("/version"))).toHaveLength(2);
const firstBody = JSON.parse(String(fetcher.mock.calls[1][1]?.body)) as Record<string, unknown>; const firstBody = JSON.parse(String(fetcher.mock.calls[1][1]?.body)) as Record<string, unknown>;
expect(firstBody[strategyKey]).toBe("merge"); expect(firstBody[strategyKey]).toBe("merge");
expect(firstBody[strategyKey === "Do" ? "do" : "Do"]).toBeUndefined(); expect(firstBody[strategyKey === "Do" ? "do" : "Do"]).toBeUndefined();
@@ -539,4 +539,27 @@ describe("ChatGPT feature delivery approval", () => {
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");
expect(() => assertSuccessfulActionRuns([run(1, "success", "stale-sha")], "chatgpt/feature", "current-sha")).toThrow("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL"); expect(() => assertSuccessfulActionRuns([run(1, "success", "stale-sha")], "chatgpt/feature", "current-sha")).toThrow("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL");
}); });
it("requires a successful canonical pull-request full run on the exact head before merge", () => {
const headSha = "a".repeat(40);
const branch = "chatgpt/feature";
const run = (overrides: Partial<GiteaActionRun> = {}): GiteaActionRun => ({
id: 10,
run_number: 10,
display_title: "Managed validation / full",
path: ".gitea/workflows/managed-validation.yml",
event: "pull_request",
head_sha: headSha,
head_branch: branch,
status: "completed",
conclusion: "success",
html_url: "https://gitea.example.com/runs/10",
...overrides,
});
expect(assertSuccessfulPullRequestFullActionRun([run()], branch, headSha)).toMatchObject({ event: "pull_request", head_sha: headSha, conclusion: "success" });
expect(() => assertSuccessfulPullRequestFullActionRun([run({ head_sha: "b".repeat(40) })], branch, headSha)).toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
expect(() => assertSuccessfulPullRequestFullActionRun([run({ event: "workflow_dispatch" })], branch, headSha)).toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
expect(() => assertSuccessfulPullRequestFullActionRun([run({ conclusion: "failure" })], branch, headSha)).toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
});
}); });