Publish curated projectbrain source from 0244fbf232898e6dc076ff2159d96a6da55ab5b5
This commit is contained in:
1 parent
eca2891b07
commit
d5c9615684
10 files changed
+170
-31
No files matched your search
@@ -10,9 +10,10 @@ type RequestOptions = { method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; qu
|
||||
type GiteaBranchProtection = { enable_status_check?: boolean; status_check_contexts?: string[] };
|
||||
type GiteaCombinedStatus = { state?: string; statuses?: Array<{ context?: string; status?: string }> };
|
||||
|
||||
import { GITEA_TESTED_MERGE_RANGE, giteaMergeCompatibility } from "./compatibility.js";
|
||||
|
||||
export class GiteaClient {
|
||||
private readonly apiBase: string;
|
||||
private mergeStrategyKey?: "Do" | "do";
|
||||
|
||||
constructor(baseUrl: string, private readonly token: string, private readonly fetcher: typeof fetch = fetch) {
|
||||
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 });
|
||||
}
|
||||
|
||||
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"> {
|
||||
if (this.mergeStrategyKey) return this.mergeStrategyKey;
|
||||
const { version } = await this.request<{ version?: string }>("/version");
|
||||
const legacyMatch = /^1\.(14|25|26|27)(?:\.\d+)?(?:[-+].*)?$/.exec(version ?? "");
|
||||
const modernMatch = /^(28)(?:\.\d+){1,2}(?:[-+].*)?$/.exec(version ?? "");
|
||||
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;
|
||||
// A server upgrade must not inherit an indefinitely cached merge contract.
|
||||
const compatibility = await this.getUpstreamCompatibility();
|
||||
if (!compatibility.supported || !compatibility.strategyKey) throw new GiteaApiError(500, "GITEA_VERSION_UNSUPPORTED", `Gitea upstream merge negotiation failed: ${compatibility.reason}; tested ${compatibility.testedRange}`);
|
||||
return compatibility.strategyKey;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -88,6 +88,17 @@ export const successfulFullActionRun = (runs: GiteaActionRun[], branch: string,
|
||||
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[] => {
|
||||
const exactRuns = exactHeadActionRuns(runs, branch, headSha);
|
||||
if (exactRuns.length === 0) throw new Error("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL");
|
||||
|
||||
@@ -3,7 +3,7 @@ import { z } from "zod";
|
||||
import type { AppEnv } from "../config/env.js";
|
||||
import { prisma } from "../db/prisma.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 type { GiteaContent } from "../integrations/gitea/types.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");
|
||||
assertFeatureDeliveryActionEvidence(delivery.spec.validationEvidence, expected_head_sha);
|
||||
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);
|
||||
const mergeReadyPullRequest = await client.getPullRequest(link!.owner, link!.repository, pullRequestNumber);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { z } from "zod";
|
||||
import type { AppEnv } from "../config/env.js";
|
||||
import { prisma } from "../db/prisma.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 type { GiteaContent } from "../integrations/gitea/types.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);
|
||||
if (!configured) return successEnvelope({ configured: false, reachable: false, mcpEnabled: env.PROJECTBRAIN_MCP_ENABLED, publicUrl: env.PROJECTBRAIN_PUBLIC_URL, linkedRepositories: 0 }, getRequestId(request));
|
||||
try {
|
||||
const [user, linkedRepositories] = await Promise.all([getGiteaClient(env).getCurrentUser(), prisma.giteaRepositoryLink.count()]);
|
||||
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 client = getGiteaClient(env);
|
||||
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) {
|
||||
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");
|
||||
assertFeatureDeliveryActionEvidence(delivery.spec.validationEvidence, parsed.data.expectedHeadSha);
|
||||
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;
|
||||
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);
|
||||
|
||||
Reference in new issue
Block a user