mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-10-25 00:31:54 +02:00
Compare what Gitea or Forgejo holds under every owner this account mirrors into with the repositories table. Mirrors of the configured source that have no row are reported as untracked and can be adopted; rows marked mirrored whose repository is gone are confirmed with a direct check and can be reset to imported so the next run recreates them. Native repositories and mirrors of other hosts are listed and never touched, and nothing is deleted or archived. Exposed as POST /api/cleanup/reconcile and as a dialog on the Automation tab. Closes #284 Claude-Session: https://claude.ai/code/session_01Tp9pmi65a8k5jLMQFLf4JX
678 lines
20 KiB
TypeScript
678 lines
20 KiB
TypeScript
/**
|
||
* Shared helpers for E2E tests.
|
||
*
|
||
* Exports constants, the GiteaAPI wrapper, auth helpers (sign-up / sign-in),
|
||
* the saveConfig helper, and a generic waitFor polling utility.
|
||
*/
|
||
|
||
import {
|
||
expect,
|
||
request as playwrightRequest,
|
||
type Page,
|
||
type APIRequestContext,
|
||
} from "@playwright/test";
|
||
|
||
// ─── Constants ───────────────────────────────────────────────────────────────
|
||
|
||
export const APP_URL = process.env.APP_URL || "http://localhost:4321";
|
||
export const GITEA_URL = process.env.GITEA_URL || "http://localhost:3333";
|
||
export const FAKE_GITHUB_URL =
|
||
process.env.FAKE_GITHUB_URL || "http://localhost:4580";
|
||
export const GIT_SERVER_URL =
|
||
process.env.GIT_SERVER_URL || "http://localhost:4590";
|
||
|
||
export const GITEA_ADMIN_USER = "e2e_admin";
|
||
export const GITEA_ADMIN_PASS = "e2eAdminPass123!";
|
||
export const GITEA_ADMIN_EMAIL = "admin@e2e-test.local";
|
||
|
||
export const APP_USER_EMAIL = "e2e@test.local";
|
||
export const APP_USER_PASS = "E2eTestPass123!";
|
||
export const APP_USER_NAME = "e2e-tester";
|
||
|
||
export const GITEA_MIRROR_ORG = "github-mirrors";
|
||
|
||
// ─── waitFor ─────────────────────────────────────────────────────────────────
|
||
|
||
/** Retry a function until it returns truthy or timeout is reached. */
|
||
export async function waitFor(
|
||
fn: () => Promise<boolean>,
|
||
{
|
||
timeout = 60_000,
|
||
interval = 2_000,
|
||
label = "condition",
|
||
}: { timeout?: number; interval?: number; label?: string } = {},
|
||
): Promise<void> {
|
||
const deadline = Date.now() + timeout;
|
||
let lastErr: Error | undefined;
|
||
while (Date.now() < deadline) {
|
||
try {
|
||
if (await fn()) return;
|
||
} catch (e) {
|
||
lastErr = e instanceof Error ? e : new Error(String(e));
|
||
}
|
||
await new Promise((r) => setTimeout(r, interval));
|
||
}
|
||
throw new Error(
|
||
`waitFor("${label}") timed out after ${timeout}ms` +
|
||
(lastErr ? `: ${lastErr.message}` : ""),
|
||
);
|
||
}
|
||
|
||
// ─── GiteaAPI ────────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Direct HTTP helper for talking to Gitea's API.
|
||
*
|
||
* Uses a manually-created APIRequestContext so it can be shared across
|
||
* beforeAll / afterAll / individual tests without hitting Playwright's
|
||
* "fixture from beforeAll cannot be reused" restriction.
|
||
*/
|
||
export class GiteaAPI {
|
||
private token = "";
|
||
private ctx: APIRequestContext | null = null;
|
||
|
||
constructor(private baseUrl: string) {}
|
||
|
||
/** Lazily create (and cache) a Playwright APIRequestContext. */
|
||
private async getCtx(): Promise<APIRequestContext> {
|
||
if (!this.ctx) {
|
||
this.ctx = await playwrightRequest.newContext({
|
||
baseURL: this.baseUrl,
|
||
});
|
||
}
|
||
return this.ctx;
|
||
}
|
||
|
||
/** Dispose of the underlying context – call in afterAll. */
|
||
async dispose(): Promise<void> {
|
||
if (this.ctx) {
|
||
await this.ctx.dispose();
|
||
this.ctx = null;
|
||
}
|
||
}
|
||
|
||
/** Create the admin user via Gitea's sign-up form (first user becomes admin). */
|
||
async ensureAdminUser(): Promise<void> {
|
||
const ctx = await this.getCtx();
|
||
|
||
// Check if admin already exists by trying basic-auth
|
||
try {
|
||
const resp = await ctx.get(`/api/v1/user`, {
|
||
headers: {
|
||
Authorization: `Basic ${btoa(`${GITEA_ADMIN_USER}:${GITEA_ADMIN_PASS}`)}`,
|
||
},
|
||
failOnStatusCode: false,
|
||
});
|
||
if (resp.ok()) {
|
||
console.log("[GiteaAPI] Admin user already exists");
|
||
return;
|
||
}
|
||
} catch {
|
||
// Expected on first run
|
||
}
|
||
|
||
// Register through the form – first user auto-becomes admin
|
||
console.log("[GiteaAPI] Creating admin via sign-up form...");
|
||
const signUpResp = await ctx.post(`/user/sign_up`, {
|
||
form: {
|
||
user_name: GITEA_ADMIN_USER,
|
||
password: GITEA_ADMIN_PASS,
|
||
retype: GITEA_ADMIN_PASS,
|
||
email: GITEA_ADMIN_EMAIL,
|
||
},
|
||
failOnStatusCode: false,
|
||
maxRedirects: 5,
|
||
});
|
||
console.log(`[GiteaAPI] Sign-up response status: ${signUpResp.status()}`);
|
||
|
||
// Verify
|
||
const check = await ctx.get(`/api/v1/user`, {
|
||
headers: {
|
||
Authorization: `Basic ${btoa(`${GITEA_ADMIN_USER}:${GITEA_ADMIN_PASS}`)}`,
|
||
},
|
||
failOnStatusCode: false,
|
||
});
|
||
if (!check.ok()) {
|
||
throw new Error(
|
||
`Failed to verify admin user after creation (status ${check.status()})`,
|
||
);
|
||
}
|
||
console.log("[GiteaAPI] Admin user verified");
|
||
}
|
||
|
||
/** Generate a Gitea API token for the admin user. */
|
||
async createToken(): Promise<string> {
|
||
if (this.token) return this.token;
|
||
const ctx = await this.getCtx();
|
||
|
||
const tokenName = `e2e-token-${Date.now()}`;
|
||
const resp = await ctx.post(`/api/v1/users/${GITEA_ADMIN_USER}/tokens`, {
|
||
headers: {
|
||
Authorization: `Basic ${btoa(`${GITEA_ADMIN_USER}:${GITEA_ADMIN_PASS}`)}`,
|
||
"Content-Type": "application/json",
|
||
},
|
||
data: {
|
||
name: tokenName,
|
||
scopes: [
|
||
"read:user",
|
||
"write:user",
|
||
"read:organization",
|
||
"write:organization",
|
||
"read:repository",
|
||
"write:repository",
|
||
"read:issue",
|
||
"write:issue",
|
||
"read:misc",
|
||
"write:misc",
|
||
"read:admin",
|
||
"write:admin",
|
||
],
|
||
},
|
||
});
|
||
expect(
|
||
resp.ok(),
|
||
`Failed to create Gitea token: ${resp.status()}`,
|
||
).toBeTruthy();
|
||
const data = await resp.json();
|
||
this.token = data.sha1 || data.token;
|
||
console.log(`[GiteaAPI] Created token: ${tokenName}`);
|
||
return this.token;
|
||
}
|
||
|
||
/** Create an organization in Gitea. */
|
||
async ensureOrg(orgName: string): Promise<void> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
|
||
// Check if org exists
|
||
const check = await ctx.get(`/api/v1/orgs/${orgName}`, {
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
});
|
||
if (check.ok()) {
|
||
console.log(`[GiteaAPI] Org ${orgName} already exists`);
|
||
return;
|
||
}
|
||
|
||
const resp = await ctx.post(`/api/v1/orgs`, {
|
||
headers: {
|
||
Authorization: `token ${token}`,
|
||
"Content-Type": "application/json",
|
||
},
|
||
data: {
|
||
username: orgName,
|
||
full_name: orgName,
|
||
description: "E2E test mirror organization",
|
||
visibility: "public",
|
||
},
|
||
});
|
||
expect(resp.ok(), `Failed to create org: ${resp.status()}`).toBeTruthy();
|
||
console.log(`[GiteaAPI] Created org: ${orgName}`);
|
||
}
|
||
|
||
/** List repos in a Gitea org. */
|
||
async listOrgRepos(orgName: string): Promise<any[]> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const resp = await ctx.get(`/api/v1/orgs/${orgName}/repos`, {
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
});
|
||
if (!resp.ok()) return [];
|
||
return resp.json();
|
||
}
|
||
|
||
/** List repos for the admin user. */
|
||
async listUserRepos(): Promise<any[]> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const resp = await ctx.get(`/api/v1/users/${GITEA_ADMIN_USER}/repos`, {
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
});
|
||
if (!resp.ok()) return [];
|
||
return resp.json();
|
||
}
|
||
|
||
/** Get a specific repo. */
|
||
async getRepo(owner: string, name: string): Promise<any | null> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const resp = await ctx.get(`/api/v1/repos/${owner}/${name}`, {
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
});
|
||
if (!resp.ok()) return null;
|
||
return resp.json();
|
||
}
|
||
|
||
/** List branches for a repo. */
|
||
async listBranches(owner: string, name: string): Promise<any[]> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const resp = await ctx.get(`/api/v1/repos/${owner}/${name}/branches`, {
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
});
|
||
if (!resp.ok()) return [];
|
||
return resp.json();
|
||
}
|
||
|
||
/** List tags for a repo. */
|
||
async listTags(owner: string, name: string): Promise<any[]> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const resp = await ctx.get(`/api/v1/repos/${owner}/${name}/tags`, {
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
});
|
||
if (!resp.ok()) return [];
|
||
return resp.json();
|
||
}
|
||
|
||
/** List commits for a repo (on default branch). */
|
||
async listCommits(
|
||
owner: string,
|
||
name: string,
|
||
opts?: { sha?: string; limit?: number },
|
||
): Promise<any[]> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const params = new URLSearchParams();
|
||
if (opts?.sha) params.set("sha", opts.sha);
|
||
if (opts?.limit) params.set("limit", String(opts.limit));
|
||
const qs = params.toString() ? `?${params.toString()}` : "";
|
||
const resp = await ctx.get(
|
||
`/api/v1/repos/${owner}/${name}/commits${qs}`,
|
||
{
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
},
|
||
);
|
||
if (!resp.ok()) return [];
|
||
return resp.json();
|
||
}
|
||
|
||
/** Get a single branch (includes the commit SHA). */
|
||
async getBranch(
|
||
owner: string,
|
||
name: string,
|
||
branch: string,
|
||
): Promise<any | null> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const resp = await ctx.get(
|
||
`/api/v1/repos/${owner}/${name}/branches/${branch}`,
|
||
{
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
},
|
||
);
|
||
if (!resp.ok()) return null;
|
||
return resp.json();
|
||
}
|
||
|
||
/** Get file content from a repo. */
|
||
async getFileContent(
|
||
owner: string,
|
||
name: string,
|
||
filePath: string,
|
||
ref?: string,
|
||
): Promise<string | null> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const refQuery = ref ? `?ref=${encodeURIComponent(ref)}` : "";
|
||
const resp = await ctx.get(
|
||
`/api/v1/repos/${owner}/${name}/raw/${filePath}${refQuery}`,
|
||
{
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
},
|
||
);
|
||
if (!resp.ok()) return null;
|
||
return resp.text();
|
||
}
|
||
|
||
/** Get a commit by SHA. */
|
||
async getCommit(
|
||
owner: string,
|
||
name: string,
|
||
sha: string,
|
||
): Promise<any | null> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const resp = await ctx.get(
|
||
`/api/v1/repos/${owner}/${name}/git/commits/${sha}`,
|
||
{
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
},
|
||
);
|
||
if (!resp.ok()) return null;
|
||
return resp.json();
|
||
}
|
||
|
||
/** Trigger mirror sync for a repo via the Gitea API directly. */
|
||
async triggerMirrorSync(owner: string, name: string): Promise<boolean> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const resp = await ctx.post(
|
||
`/api/v1/repos/${owner}/${name}/mirror-sync`,
|
||
{
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
},
|
||
);
|
||
return resp.ok() || resp.status() === 200;
|
||
}
|
||
|
||
/** Delete a repo on Gitea directly (the app is not told). */
|
||
async deleteRepo(owner: string, name: string): Promise<boolean> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const resp = await ctx.delete(`/api/v1/repos/${owner}/${name}`, {
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
});
|
||
return resp.ok() || resp.status() === 404;
|
||
}
|
||
|
||
getTokenValue(): string {
|
||
return this.token;
|
||
}
|
||
}
|
||
|
||
// ─── App auth helpers ────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Sign up + sign in to the gitea-mirror app using the Better Auth REST API
|
||
* and return the session cookie string.
|
||
*/
|
||
export async function getAppSessionCookies(
|
||
request: APIRequestContext,
|
||
): Promise<string> {
|
||
// 1. Try sign-in first (user may already exist from a previous test / run)
|
||
const signInResp = await request.post(`${APP_URL}/api/auth/sign-in/email`, {
|
||
data: { email: APP_USER_EMAIL, password: APP_USER_PASS },
|
||
failOnStatusCode: false,
|
||
});
|
||
|
||
if (signInResp.ok()) {
|
||
const cookies = extractSetCookies(signInResp);
|
||
if (cookies) {
|
||
console.log("[App] Signed in (existing user)");
|
||
return cookies;
|
||
}
|
||
}
|
||
|
||
// 2. Register
|
||
const signUpResp = await request.post(`${APP_URL}/api/auth/sign-up/email`, {
|
||
data: {
|
||
name: APP_USER_NAME,
|
||
email: APP_USER_EMAIL,
|
||
password: APP_USER_PASS,
|
||
},
|
||
failOnStatusCode: false,
|
||
});
|
||
const signUpStatus = signUpResp.status();
|
||
console.log(`[App] Sign-up response: ${signUpStatus}`);
|
||
|
||
// After sign-up Better Auth may already set a session cookie
|
||
const signUpCookies = extractSetCookies(signUpResp);
|
||
if (signUpCookies) {
|
||
console.log("[App] Got session from sign-up response");
|
||
return signUpCookies;
|
||
}
|
||
|
||
// 3. Sign in after registration
|
||
const postRegSignIn = await request.post(
|
||
`${APP_URL}/api/auth/sign-in/email`,
|
||
{
|
||
data: { email: APP_USER_EMAIL, password: APP_USER_PASS },
|
||
failOnStatusCode: false,
|
||
},
|
||
);
|
||
if (!postRegSignIn.ok()) {
|
||
const body = await postRegSignIn.text();
|
||
throw new Error(
|
||
`Sign-in after registration failed (${postRegSignIn.status()}): ${body}`,
|
||
);
|
||
}
|
||
const cookies = extractSetCookies(postRegSignIn);
|
||
if (!cookies) {
|
||
throw new Error("Sign-in succeeded but no session cookie was returned");
|
||
}
|
||
console.log("[App] Signed in (after registration)");
|
||
return cookies;
|
||
}
|
||
|
||
/**
|
||
* Extract session cookies from a response's `set-cookie` headers.
|
||
*/
|
||
export function extractSetCookies(
|
||
resp: Awaited<ReturnType<APIRequestContext["post"]>>,
|
||
): string {
|
||
const raw = resp
|
||
.headersArray()
|
||
.filter((h) => h.name.toLowerCase() === "set-cookie");
|
||
if (raw.length === 0) return "";
|
||
|
||
const pairs: string[] = [];
|
||
for (const header of raw) {
|
||
const nv = header.value.split(";")[0].trim();
|
||
if (nv) pairs.push(nv);
|
||
}
|
||
|
||
return pairs.join("; ");
|
||
}
|
||
|
||
/**
|
||
* Sign in via the browser UI so the browser context gets session cookies.
|
||
*/
|
||
export async function signInViaBrowser(page: Page): Promise<string> {
|
||
const signInResp = await page.request.post(
|
||
`${APP_URL}/api/auth/sign-in/email`,
|
||
{
|
||
data: { email: APP_USER_EMAIL, password: APP_USER_PASS },
|
||
failOnStatusCode: false,
|
||
},
|
||
);
|
||
|
||
if (!signInResp.ok()) {
|
||
const signUpResp = await page.request.post(
|
||
`${APP_URL}/api/auth/sign-up/email`,
|
||
{
|
||
data: {
|
||
name: APP_USER_NAME,
|
||
email: APP_USER_EMAIL,
|
||
password: APP_USER_PASS,
|
||
},
|
||
failOnStatusCode: false,
|
||
},
|
||
);
|
||
console.log(`[Browser] Sign-up status: ${signUpResp.status()}`);
|
||
|
||
const retryResp = await page.request.post(
|
||
`${APP_URL}/api/auth/sign-in/email`,
|
||
{
|
||
data: { email: APP_USER_EMAIL, password: APP_USER_PASS },
|
||
failOnStatusCode: false,
|
||
},
|
||
);
|
||
if (!retryResp.ok()) {
|
||
console.log(`[Browser] Sign-in retry failed: ${retryResp.status()}`);
|
||
}
|
||
}
|
||
|
||
await page.goto(`${APP_URL}/`);
|
||
await page.waitForLoadState("networkidle");
|
||
const url = page.url();
|
||
console.log(`[Browser] After sign-in, URL: ${url}`);
|
||
|
||
const cookies = await page.context().cookies();
|
||
return cookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
||
}
|
||
|
||
// ─── Config helper ───────────────────────────────────────────────────────────
|
||
|
||
/** Save app config via the API. */
|
||
export async function saveConfig(
|
||
request: APIRequestContext,
|
||
giteaToken: string,
|
||
cookies: string,
|
||
overrides: Record<string, any> = {},
|
||
): Promise<void> {
|
||
const giteaConfigDefaults = {
|
||
url: GITEA_URL,
|
||
username: GITEA_ADMIN_USER,
|
||
token: giteaToken,
|
||
organization: GITEA_MIRROR_ORG,
|
||
visibility: "public",
|
||
starredReposOrg: "github-stars",
|
||
preserveOrgStructure: false,
|
||
mirrorStrategy: "single-org",
|
||
backupStrategy: "disabled",
|
||
blockSyncOnBackupFailure: false,
|
||
};
|
||
|
||
const configPayload = {
|
||
githubConfig: {
|
||
username: "e2e-test-user",
|
||
token: "fake-github-token-for-e2e",
|
||
privateRepositories: false,
|
||
mirrorStarred: true,
|
||
},
|
||
giteaConfig: { ...giteaConfigDefaults, ...(overrides.giteaConfig || {}) },
|
||
scheduleConfig: {
|
||
enabled: false,
|
||
interval: 3600,
|
||
},
|
||
cleanupConfig: {
|
||
enabled: false,
|
||
retentionDays: 86400,
|
||
deleteIfNotInGitHub: false,
|
||
orphanedRepoAction: "skip",
|
||
dryRun: true,
|
||
},
|
||
mirrorOptions: {
|
||
mirrorReleases: false,
|
||
mirrorLFS: false,
|
||
mirrorMetadata: false,
|
||
metadataComponents: {
|
||
issues: false,
|
||
pullRequests: false,
|
||
labels: false,
|
||
milestones: false,
|
||
wiki: false,
|
||
},
|
||
},
|
||
advancedOptions: {
|
||
skipForks: false,
|
||
starredCodeOnly: false,
|
||
},
|
||
};
|
||
|
||
const resp = await request.post(`${APP_URL}/api/config`, {
|
||
data: configPayload,
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Cookie: cookies,
|
||
},
|
||
failOnStatusCode: false,
|
||
});
|
||
|
||
const status = resp.status();
|
||
console.log(`[App] Save config response: ${status}`);
|
||
|
||
if (status >= 400) {
|
||
const body = await resp.text();
|
||
console.log(`[App] Config error body: ${body}`);
|
||
}
|
||
|
||
expect(status, "Config save should not return server error").toBeLessThan(
|
||
500,
|
||
);
|
||
}
|
||
|
||
// ─── Dashboard / repo helpers ────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Fetch the list of repository IDs from the app's dashboard API.
|
||
* Optionally filter to repos with a given status.
|
||
*/
|
||
export async function getRepositoryIds(
|
||
request: APIRequestContext,
|
||
cookies: string,
|
||
opts?: { status?: string },
|
||
): Promise<{ ids: string[]; repos: any[] }> {
|
||
const dashResp = await request.get(`${APP_URL}/api/dashboard`, {
|
||
headers: { Cookie: cookies },
|
||
failOnStatusCode: false,
|
||
});
|
||
if (!dashResp.ok()) return { ids: [], repos: [] };
|
||
|
||
const dashData = await dashResp.json();
|
||
const repos: any[] = dashData.repositories ?? dashData.repos ?? [];
|
||
|
||
const filtered = opts?.status
|
||
? repos.filter((r: any) => r.status === opts.status)
|
||
: repos;
|
||
|
||
return {
|
||
ids: filtered.map((r: any) => r.id),
|
||
repos: filtered,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Trigger mirror jobs for the given repository IDs via the app API,
|
||
* then wait for a specified delay for async processing.
|
||
*/
|
||
export async function triggerMirrorJobs(
|
||
request: APIRequestContext,
|
||
cookies: string,
|
||
repositoryIds: string[],
|
||
waitMs = 30_000,
|
||
): Promise<number> {
|
||
const mirrorResp = await request.post(`${APP_URL}/api/job/mirror-repo`, {
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Cookie: cookies,
|
||
},
|
||
data: { repositoryIds },
|
||
failOnStatusCode: false,
|
||
});
|
||
|
||
const status = mirrorResp.status();
|
||
if (waitMs > 0) {
|
||
await new Promise((r) => setTimeout(r, waitMs));
|
||
}
|
||
return status;
|
||
}
|
||
|
||
/**
|
||
* Trigger sync-repo (re-sync already-mirrored repos) for the given
|
||
* repository IDs, then wait for processing.
|
||
*/
|
||
export async function triggerSyncRepo(
|
||
request: APIRequestContext,
|
||
cookies: string,
|
||
repositoryIds: string[],
|
||
waitMs = 25_000,
|
||
): Promise<number> {
|
||
const syncResp = await request.post(`${APP_URL}/api/job/sync-repo`, {
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Cookie: cookies,
|
||
},
|
||
data: { repositoryIds },
|
||
failOnStatusCode: false,
|
||
});
|
||
|
||
const status = syncResp.status();
|
||
if (waitMs > 0) {
|
||
await new Promise((r) => setTimeout(r, waitMs));
|
||
}
|
||
return status;
|
||
}
|