Compare commits

..

3 Commits

Author SHA1 Message Date
ARUNAVO RAY
1dd3dea231 fix preserve strategy fork owner routing (#215) 2026-03-06 10:15:47 +05:30
Arunavo Ray
db783c4225 nix: reduce bun install CI stalls 2026-03-06 09:41:22 +05:30
github-actions[bot]
8a4716bdbd chore: sync version to 3.12.3 2026-03-06 03:35:40 +00:00
5 changed files with 111 additions and 6 deletions

View File

@@ -24,7 +24,7 @@ permissions:
jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 25
timeout-minutes: 45
steps:
- uses: actions/checkout@v4

View File

@@ -49,6 +49,20 @@
bunNix = ./bun.nix;
};
# bun2nix defaults to isolated installs on Linux, which can be
# very slow in CI for larger dependency trees and may appear stuck.
# Use hoisted linker and fail fast on lockfile drift.
bunInstallFlags = if pkgs.stdenv.hostPlatform.isDarwin then [
"--linker=hoisted"
"--backend=copyfile"
"--frozen-lockfile"
"--no-progress"
] else [
"--linker=hoisted"
"--frozen-lockfile"
"--no-progress"
];
# Let the bun2nix hook handle dependency installation via the
# pre-fetched cache, but skip its default build/check/install
# phases since we have custom ones.

View File

@@ -1,7 +1,7 @@
{
"name": "gitea-mirror",
"type": "module",
"version": "3.12.2",
"version": "3.12.3",
"engines": {
"bun": ">=1.2.9"
},

View File

@@ -72,10 +72,21 @@ mock.module("./gitea", () => {
const mirrorStrategy =
config?.githubConfig?.mirrorStrategy ||
(config?.giteaConfig?.preserveOrgStructure ? "preserve" : "flat-user");
const configuredGitHubOwner =
(config?.githubConfig?.owner || config?.githubConfig?.username || "")
.trim()
.toLowerCase();
const repoOwner = repository?.owner?.trim().toLowerCase();
switch (mirrorStrategy) {
case "preserve":
return repository?.organization || config?.giteaConfig?.defaultOwner || "giteauser";
if (repository?.organization) {
return repository.organization;
}
if (configuredGitHubOwner && repoOwner && repoOwner !== configuredGitHubOwner) {
return repository.owner;
}
return config?.giteaConfig?.defaultOwner || "giteauser";
case "single-org":
return config?.giteaConfig?.organization || config?.giteaConfig?.defaultOwner || "giteauser";
case "mixed":
@@ -99,7 +110,7 @@ mock.module("./gitea", () => {
return mockDbSelectResult[0].destinationOrg;
}
return config?.giteaConfig?.defaultOwner || "giteauser";
return mockGetGiteaRepoOwner({ config, repository });
});
return {
isRepoPresentInGitea: mockIsRepoPresentInGitea,
@@ -376,6 +387,7 @@ describe("Gitea Repository Mirroring", () => {
describe("getGiteaRepoOwner - Organization Override Tests", () => {
const baseConfig: Partial<Config> = {
githubConfig: {
owner: "testuser",
username: "testuser",
token: "token",
preserveOrgStructure: false,
@@ -484,6 +496,18 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
expect(result).toBe("giteauser");
});
test("preserve strategy: personal repos owned by another user keep source owner namespace", () => {
const repo = {
...baseRepo,
owner: "nice-user",
fullName: "nice-user/test-repo",
organization: undefined,
isForked: true,
};
const result = getGiteaRepoOwner({ config: baseConfig, repository: repo });
expect(result).toBe("nice-user");
});
test("preserve strategy: org repos go to same org name", () => {
const repo = { ...baseRepo, organization: "myorg" };
const result = getGiteaRepoOwner({ config: baseConfig, repository: repo });
@@ -589,4 +613,26 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
expect(result).toBe("FOO");
});
test("getGiteaRepoOwnerAsync preserves external personal owner for preserve strategy", async () => {
const configWithUser: Partial<Config> = {
...baseConfig,
userId: "user-id",
};
const repo = {
...baseRepo,
owner: "nice-user",
fullName: "nice-user/test-repo",
organization: undefined,
isForked: true,
};
const result = await getGiteaRepoOwnerAsync({
config: configWithUser,
repository: repo,
});
expect(result).toBe("nice-user");
});
});

View File

@@ -138,14 +138,35 @@ export const getGiteaRepoOwner = ({
// Get the mirror strategy - use preserveOrgStructure for backward compatibility
const mirrorStrategy = config.githubConfig.mirrorStrategy ||
(config.giteaConfig.preserveOrgStructure ? "preserve" : "flat-user");
const configuredGitHubOwner =
(
config.githubConfig.owner ||
(config.githubConfig as typeof config.githubConfig & { username?: string }).username ||
""
)
.trim()
.toLowerCase();
switch (mirrorStrategy) {
case "preserve":
// Keep GitHub structure - org repos go to same org, personal repos to user (or override)
// Keep GitHub structure:
// - org repos stay in the same org
// - personal repos owned by other users keep their source owner namespace
// - personal repos owned by the configured account go to defaultOwner
if (repository.organization) {
return repository.organization;
}
// Use personal repos override if configured, otherwise use username
const normalizedRepoOwner = repository.owner.trim().toLowerCase();
if (
normalizedRepoOwner &&
configuredGitHubOwner &&
normalizedRepoOwner !== configuredGitHubOwner
) {
return repository.owner;
}
// Personal repos from the configured GitHub account go to the configured default owner
return config.giteaConfig.defaultOwner;
case "single-org":
@@ -376,6 +397,23 @@ export const mirrorGithubRepoToGitea = async ({
// Get the correct owner based on the strategy (with organization overrides)
let repoOwner = await getGiteaRepoOwnerAsync({ config, repository });
const mirrorStrategy = config.githubConfig.mirrorStrategy ||
(config.giteaConfig.preserveOrgStructure ? "preserve" : "flat-user");
const configuredGitHubOwner = (
config.githubConfig.owner ||
(config.githubConfig as typeof config.githubConfig & { username?: string }).username ||
""
)
.trim()
.toLowerCase();
const normalizedRepoOwner = repository.owner.trim().toLowerCase();
const isExternalPersonalRepoInPreserveMode =
mirrorStrategy === "preserve" &&
!repository.organization &&
!repository.isStarred &&
normalizedRepoOwner !== "" &&
configuredGitHubOwner !== "" &&
normalizedRepoOwner !== configuredGitHubOwner;
// Determine the actual repository name to use (handle duplicates for starred repos)
let targetRepoName = repository.name;
@@ -520,6 +558,13 @@ export const mirrorGithubRepoToGitea = async ({
(orgError.message.includes('Permission denied') ||
orgError.message.includes('Authentication failed') ||
orgError.message.includes('does not have permission'))) {
if (isExternalPersonalRepoInPreserveMode) {
throw new Error(
`Cannot create/access namespace "${repoOwner}" for ${repository.fullName}. ` +
`Refusing fallback to "${config.giteaConfig.defaultOwner}" in preserve mode to avoid cross-owner overwrite.`
);
}
console.warn(`[Fallback] Organization creation/access failed. Attempting to mirror to user account instead.`);
// Update the repository owner to use the user account