mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-07-06 09:50:59 +02:00
* fix: hoist migrateSucceeded above try so catch can update DB on failure (fixes #268) `let migrateSucceeded` was declared inside the try block of mirrorGithubRepoToGitea and mirrorGitHubRepoToGiteaOrg, but the catch block referenced it. Block-scoping made it invisible to catch, so any error inside the try (network timeout, transient 5xx, etc.) crashed the catch with `ReferenceError: migrateSucceeded is not defined` before reaching the DB update that marks the repo "failed". Result: repos stuck in "mirroring" forever with no entry in the activity log. Hoisting the declaration above the try restores the intended behavior: catch updates the repo to failed, clears mirroredLocation when migrate hadn't succeeded, writes a failed activity-log entry, and re-throws with the original error message preserved. TypeScript was flagging this as "Cannot find name 'migrateSucceeded'" but esbuild stripped the types during build, so the bug shipped. * test: replace integration test with structural source check (#268) The behavioral version of this regression test passed locally but failed in CI because of mock.module pollution between files: bun's mock.module is process-wide, so my mock for @/lib/gitea-enhanced leaked into gitea-enhanced.test.ts (its real-module assertions saw my null-returning mocks), and gitea-enhanced.test.ts's own @/lib/http-client mock could supersede mine depending on file discovery order, causing my mirrorGithubRepoToGitea call to not throw at all in CI. Replace with a structural assertion that reads gitea.ts and verifies `let migrateSucceeded` is declared before the outermost try in both mirrorGithubRepoToGitea and mirrorGitHubRepoToGiteaOrg. Verified the new test fails on the pre-fix source with a clear error message pointing to issue #268, and passes on the fixed source.
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Regression test for issue #268.
|
||||
*
|
||||
* `let migrateSucceeded = false;` was declared *inside* the try block
|
||||
* of mirrorGithubRepoToGitea and mirrorGitHubRepoToGiteaOrg, but the
|
||||
* catch block referenced it. `let` is block-scoped to the try, so any
|
||||
* error inside try made the catch crash with `ReferenceError:
|
||||
* migrateSucceeded is not defined` before reaching the DB update that
|
||||
* marks the repo "failed". Result: repos stuck in "mirroring" forever
|
||||
* with no entry in the activity log (see issue logs).
|
||||
*
|
||||
* This test asserts the declaration is hoisted above the try block in
|
||||
* both functions. It deliberately reads the source rather than calling
|
||||
* the functions, because behavioral tests for these functions require
|
||||
* heavy module mocks that pollute other test files (bun's mock.module
|
||||
* is process-wide and persists across files).
|
||||
*/
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const SOURCE = readFileSync(
|
||||
join(import.meta.dir, "gitea.ts"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
/**
|
||||
* Locate the body of a function declaration by name. Walks from the
|
||||
* declaration, balances parens to skip the parameter list (which can
|
||||
* contain destructured object literals with their own braces), then
|
||||
* finds the body's opening brace and its matching close.
|
||||
*/
|
||||
function extractFunctionBody(source: string, declarationStart: RegExp): string {
|
||||
const match = source.match(declarationStart);
|
||||
if (!match) {
|
||||
throw new Error(`Could not locate declaration ${declarationStart}`);
|
||||
}
|
||||
let i = match.index! + match[0].length;
|
||||
// Skip whitespace until the opening paren of the parameter list.
|
||||
while (i < source.length && source[i] !== "(") i++;
|
||||
if (source[i] !== "(") {
|
||||
throw new Error(`No '(' after ${declarationStart}`);
|
||||
}
|
||||
// Balance parens to find the end of the parameter list. Braces inside
|
||||
// the parameter list (e.g. destructured `{ foo, bar }`) are allowed
|
||||
// and ignored.
|
||||
let parenDepth = 0;
|
||||
for (; i < source.length; i++) {
|
||||
if (source[i] === "(") parenDepth++;
|
||||
else if (source[i] === ")") {
|
||||
parenDepth--;
|
||||
if (parenDepth === 0) {
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Skip return-type annotation, => arrow, whitespace, until the body's `{`.
|
||||
while (i < source.length && source[i] !== "{") i++;
|
||||
if (source[i] !== "{") {
|
||||
throw new Error(`No body '{' for ${declarationStart}`);
|
||||
}
|
||||
// Balance braces for the body.
|
||||
let braceDepth = 0;
|
||||
const startIdx = i;
|
||||
for (; i < source.length; i++) {
|
||||
if (source[i] === "{") braceDepth++;
|
||||
else if (source[i] === "}") {
|
||||
braceDepth--;
|
||||
if (braceDepth === 0) {
|
||||
return source.slice(startIdx, i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(`Unterminated body for ${declarationStart}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm that within a function body, the first `let migrateSucceeded`
|
||||
* declaration occurs BEFORE the function's outermost `try {`.
|
||||
*
|
||||
* If the declaration is inside the try block, the catch block can't see
|
||||
* it (ReferenceError in production = repo stuck mirroring).
|
||||
*/
|
||||
function assertMigrateSucceededDeclaredBeforeTry(body: string, label: string) {
|
||||
const declIdx = body.indexOf("let migrateSucceeded");
|
||||
expect(declIdx, `${label}: 'let migrateSucceeded' should exist`).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// The function's outermost try is the first standalone `try {` in
|
||||
// the body — assignments and inner try/catches don't share its name.
|
||||
const tryIdx = body.search(/\btry\s*\{/);
|
||||
expect(tryIdx, `${label}: outermost 'try {' should exist`).toBeGreaterThanOrEqual(0);
|
||||
|
||||
expect(
|
||||
declIdx,
|
||||
`${label}: 'let migrateSucceeded' must be declared BEFORE the try block ` +
|
||||
`so the catch block can read it. If declared inside try, it's block-scoped ` +
|
||||
`and the catch will throw ReferenceError, leaving repos stuck in 'mirroring'. ` +
|
||||
`See issue #268.`
|
||||
).toBeLessThan(tryIdx);
|
||||
|
||||
// And it should still be assigned to true after the migrate call —
|
||||
// otherwise the catch can't tell whether to clear mirroredLocation.
|
||||
expect(
|
||||
body.includes("migrateSucceeded = true"),
|
||||
`${label}: 'migrateSucceeded = true' assignment should exist after the migrate call`
|
||||
).toBe(true);
|
||||
|
||||
// And the catch must read it.
|
||||
expect(
|
||||
body.includes("if (!migrateSucceeded)"),
|
||||
`${label}: catch block should read 'migrateSucceeded' to decide whether to clear mirroredLocation`
|
||||
).toBe(true);
|
||||
}
|
||||
|
||||
describe("issue #268 — migrateSucceeded scoping regression", () => {
|
||||
test("mirrorGithubRepoToGitea declares migrateSucceeded outside try", () => {
|
||||
const body = extractFunctionBody(
|
||||
SOURCE,
|
||||
/export const mirrorGithubRepoToGitea = async\b/
|
||||
);
|
||||
assertMigrateSucceededDeclaredBeforeTry(body, "mirrorGithubRepoToGitea");
|
||||
});
|
||||
|
||||
test("mirrorGitHubRepoToGiteaOrg declares migrateSucceeded outside try", () => {
|
||||
const body = extractFunctionBody(
|
||||
SOURCE,
|
||||
/export async function mirrorGitHubRepoToGiteaOrg\b/
|
||||
);
|
||||
assertMigrateSucceededDeclaredBeforeTry(body, "mirrorGitHubRepoToGiteaOrg");
|
||||
});
|
||||
});
|
||||
+8
-6
@@ -539,6 +539,11 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
repository: Repository;
|
||||
config: Partial<Config>;
|
||||
}): Promise<any> => {
|
||||
// Declared here (not inside try) so the catch block can read it.
|
||||
// `let` is block-scoped — declaring inside try makes it inaccessible
|
||||
// from catch, which previously caused a ReferenceError that swallowed
|
||||
// the real error and left repos stuck in "mirroring" state.
|
||||
let migrateSucceeded = false;
|
||||
try {
|
||||
if (!config.userId || !config.githubConfig || !config.giteaConfig) {
|
||||
throw new Error("github config and gitea config are required.");
|
||||
@@ -837,10 +842,6 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
);
|
||||
}
|
||||
|
||||
// Track whether the Gitea migrate call succeeded so the catch block
|
||||
// knows whether to clear mirroredLocation (only safe before migrate succeeds)
|
||||
let migrateSucceeded = false;
|
||||
|
||||
const response = await httpPost(
|
||||
apiUrl,
|
||||
migratePayload,
|
||||
@@ -1321,6 +1322,9 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
giteaOrgId: number;
|
||||
orgName: string;
|
||||
}) {
|
||||
// Declared here (not inside try) so the catch block can read it.
|
||||
// See note in mirrorGithubRepoToGitea for the scoping bug this prevents.
|
||||
let migrateSucceeded = false;
|
||||
try {
|
||||
if (
|
||||
!config.giteaConfig?.url ||
|
||||
@@ -1528,8 +1532,6 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
);
|
||||
}
|
||||
|
||||
let migrateSucceeded = false;
|
||||
|
||||
const migrateRes = await httpPost(
|
||||
apiUrl,
|
||||
migratePayload,
|
||||
|
||||
Reference in New Issue
Block a user