Add Gitea as a git provider for harness workspace repositories
Support Gitea alongside GitHub/GitLab for repo search, authenticated cloning, and pull request creation via Gitea API. Tasks can specify gitProvider and gitBaseUrl in their spec (defaults to github for backwards compat). Auto-discovers GITEA_TOKEN from env on boot.
This commit is contained in:
@@ -81,6 +81,7 @@ async function loadCredentialsFromEnv() {
|
||||
["GITHUB_TOKEN", "github", "GitHub (env)"],
|
||||
["GH_TOKEN", "github", "GitHub (env)"],
|
||||
["GITLAB_TOKEN", "gitlab", "GitLab (env)"],
|
||||
["GITEA_TOKEN", "gitea", "Gitea (env)"],
|
||||
];
|
||||
|
||||
for (const [envVar, provider, label] of envMap) {
|
||||
|
||||
@@ -3,10 +3,10 @@ import { db } from "./db";
|
||||
import { credentials as credentialsTable } from "@homelab/db";
|
||||
|
||||
export type Provider =
|
||||
| "github" | "gitlab"
|
||||
| "github" | "gitlab" | "gitea"
|
||||
| "anthropic" | "openai" | "openrouter" | "google" | "opencode-zen" | "opencode-go";
|
||||
|
||||
export const GIT_PROVIDERS: Provider[] = ["github", "gitlab"];
|
||||
export const GIT_PROVIDERS: Provider[] = ["github", "gitlab", "gitea"];
|
||||
export const AI_PROVIDERS: Provider[] = ["anthropic", "openai", "openrouter", "google", "opencode-zen", "opencode-go"];
|
||||
|
||||
export interface Credential {
|
||||
|
||||
@@ -21,12 +21,18 @@ export function iterationDir(taskId: string, iteration: number): string {
|
||||
|
||||
export function buildAuthenticatedCloneUrl(
|
||||
repo: string,
|
||||
provider: "github" | "gitlab",
|
||||
provider: "github" | "gitlab" | "gitea",
|
||||
token: string,
|
||||
baseUrl?: string,
|
||||
): string {
|
||||
// repo format: "owner/name"
|
||||
if (provider === "gitlab") {
|
||||
return `https://oauth2:${token}@gitlab.com/${repo}.git`;
|
||||
const host = baseUrl ? new URL(baseUrl).host : "gitlab.com";
|
||||
return `https://oauth2:${token}@${host}/${repo}.git`;
|
||||
}
|
||||
if (provider === "gitea") {
|
||||
const host = baseUrl ? new URL(baseUrl).host : "gitea.coreworlds.io";
|
||||
return `https://x-access-token:${token}@${host}/${repo}.git`;
|
||||
}
|
||||
return `https://x-access-token:${token}@github.com/${repo}.git`;
|
||||
}
|
||||
@@ -128,7 +134,17 @@ export async function createPullRequest(opts: {
|
||||
title: string;
|
||||
body: string;
|
||||
token: string;
|
||||
provider?: "github" | "gitlab" | "gitea";
|
||||
baseUrl?: string;
|
||||
baseBranch?: string;
|
||||
}): Promise<{ number: number; url: string }> {
|
||||
const provider = opts.provider || "github";
|
||||
|
||||
if (provider === "gitea") {
|
||||
return createGiteaPullRequest(opts);
|
||||
}
|
||||
|
||||
// GitHub via `gh` CLI
|
||||
const { stdout } = await exec(
|
||||
"gh",
|
||||
[
|
||||
@@ -147,3 +163,36 @@ export async function createPullRequest(opts: {
|
||||
|
||||
return JSON.parse(stdout.trim());
|
||||
}
|
||||
|
||||
async function createGiteaPullRequest(opts: {
|
||||
repo: string;
|
||||
head: string;
|
||||
title: string;
|
||||
body: string;
|
||||
token: string;
|
||||
baseUrl?: string;
|
||||
baseBranch?: string;
|
||||
}): Promise<{ number: number; url: string }> {
|
||||
const base = opts.baseUrl || "https://gitea.coreworlds.io";
|
||||
const res = await fetch(`${base}/api/v1/repos/${opts.repo}/pulls`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `token ${opts.token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: opts.title,
|
||||
body: opts.body,
|
||||
head: opts.head,
|
||||
base: opts.baseBranch || "main",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`Gitea PR creation failed (${res.status}): ${text}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return { number: data.number, url: data.html_url };
|
||||
}
|
||||
|
||||
@@ -119,9 +119,10 @@ async function runTask(task: Task): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const gitCreds = await getRawCredentialsByProvider("github");
|
||||
const gitToken = gitCreds[0]?.token;
|
||||
if (!gitToken) {
|
||||
const gitProvider = task.spec.gitProvider || "github";
|
||||
const gitCreds = await getRawCredentialsByProvider(gitProvider);
|
||||
const gitCred = gitCreds[0];
|
||||
if (!gitCred?.token) {
|
||||
await updateTask(task.id, {
|
||||
status: "failed",
|
||||
completedAt: Date.now(),
|
||||
@@ -129,7 +130,8 @@ async function runTask(task: Task): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const repoUrl = buildAuthenticatedCloneUrl(task.project, "github", gitToken);
|
||||
const gitBaseUrl = task.spec.gitBaseUrl || gitCred.baseUrl;
|
||||
const repoUrl = buildAuthenticatedCloneUrl(task.project, gitProvider, gitCred.token, gitBaseUrl);
|
||||
|
||||
await updateTask(task.id, {
|
||||
status: "running",
|
||||
@@ -183,7 +185,9 @@ async function runTask(task: Task): Promise<void> {
|
||||
head: branchName,
|
||||
title: `[harness] ${task.goal}`,
|
||||
body: `Automated by harness orchestrator.\n\nTask: ${task.slug}\nIterations: ${lastIterN}`,
|
||||
token: gitToken,
|
||||
token: gitCred.token,
|
||||
provider: gitProvider,
|
||||
baseUrl: gitBaseUrl,
|
||||
});
|
||||
|
||||
await updateTask(task.id, {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getRawCredentialsByProvider } from "./credentials";
|
||||
|
||||
export interface RepoResult {
|
||||
provider: "github" | "gitlab";
|
||||
provider: "github" | "gitlab" | "gitea";
|
||||
fullName: string;
|
||||
url: string;
|
||||
description: string;
|
||||
@@ -15,6 +15,7 @@ export async function searchRepos(query: string): Promise<RepoResult[]> {
|
||||
const results = await Promise.allSettled([
|
||||
searchGitHub(query),
|
||||
searchGitLab(query),
|
||||
searchGitea(query),
|
||||
]);
|
||||
|
||||
return results.flatMap(r => r.status === "fulfilled" ? r.value : []);
|
||||
@@ -98,3 +99,42 @@ async function searchGitLab(query: string): Promise<RepoResult[]> {
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function searchGitea(query: string): Promise<RepoResult[]> {
|
||||
const creds = await getRawCredentialsByProvider("gitea");
|
||||
if (creds.length === 0) return [];
|
||||
|
||||
const results: RepoResult[] = [];
|
||||
|
||||
for (const cred of creds) {
|
||||
const baseUrl = cred.baseUrl || "https://gitea.coreworlds.io";
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${baseUrl}/api/v1/repos/search?q=${encodeURIComponent(query)}&limit=10&sort=updated`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `token ${cred.token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) continue;
|
||||
|
||||
const data = await res.json();
|
||||
for (const repo of data.data || []) {
|
||||
results.push({
|
||||
provider: "gitea",
|
||||
fullName: repo.full_name,
|
||||
url: repo.html_url,
|
||||
description: repo.description || "",
|
||||
defaultBranch: repo.default_branch || "main",
|
||||
private: repo.private,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// skip failed credential
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ export interface TaskSpec {
|
||||
slug: string;
|
||||
goal: string;
|
||||
project: string;
|
||||
gitProvider?: "github" | "gitlab" | "gitea";
|
||||
gitBaseUrl?: string;
|
||||
agentId: string;
|
||||
maxIterations: number;
|
||||
criteria: { label: string; target: string }[];
|
||||
|
||||
Reference in New Issue
Block a user