fix: stop config saves from resetting env-configured GITEA_MIRROR_INTERVAL (#338) (#345)

Config saves now preserve every field the settings form doesn't expose (mirror interval and other env-only options) instead of resetting them to defaults.

Fixes #338
This commit is contained in:
ARUNAVO RAY
2026-07-16 20:57:29 +05:30
committed by GitHub
parent 5c33a5547b
commit c5b331c041
3 changed files with 139 additions and 47 deletions
+66
View File
@@ -191,3 +191,69 @@ test("DB row missing starredDuplicateStrategy defaults to suffix on read", () =>
const ui = mapDbToUiConfig({ githubConfig: { owner: "octo", token: "" } });
expect(ui.githubConfig.starredDuplicateStrategy).toBe("suffix");
});
// Regression for #338: saving any setting from the Configuration page reset
// giteaConfig.mirrorInterval (set via GITEA_MIRROR_INTERVAL) back to "8h"
// because the mapper hardcoded the default instead of preserving the stored value.
test("mapUiToDbConfig preserves env-configured mirrorInterval on save", () => {
const ui = buildMinimalUiConfigs();
const db = mapUiToDbConfig(ui.githubConfig, ui.giteaConfig, ui.mirrorOptions, ui.advancedOptions, {
giteaConfig: { mirrorInterval: "10m" },
});
expect(db.giteaConfig.mirrorInterval).toBe("10m");
});
test("mapUiToDbConfig defaults mirrorInterval to 8h without existing config", () => {
const ui = buildMinimalUiConfigs();
const db = mapUiToDbConfig(ui.githubConfig, ui.giteaConfig, ui.mirrorOptions, ui.advancedOptions);
expect(db.giteaConfig.mirrorInterval).toBe("8h");
});
test("mapUiToDbConfig preserves non-UI fields from existing config on save", () => {
const ui = buildMinimalUiConfigs();
const db = mapUiToDbConfig(ui.githubConfig, ui.giteaConfig, ui.mirrorOptions, ui.advancedOptions, {
githubConfig: { type: "organization", includeArchived: true, includePublic: false },
giteaConfig: {
createOrg: false,
templateOwner: "templates",
templateRepo: "base",
addTopics: false,
topicPrefix: "gh-",
preserveVisibility: true,
},
});
expect(db.githubConfig.type).toBe("organization");
expect(db.githubConfig.includeArchived).toBe(true);
expect(db.githubConfig.includePublic).toBe(false);
expect(db.giteaConfig.createOrg).toBe(false);
expect(db.giteaConfig.templateOwner).toBe("templates");
expect(db.giteaConfig.templateRepo).toBe("base");
expect(db.giteaConfig.addTopics).toBe(false);
expect(db.giteaConfig.topicPrefix).toBe("gh-");
expect(db.giteaConfig.preserveVisibility).toBe(true);
});
test("mapUiToDbConfig keeps env-configured full-copy forkStrategy when forks are included", () => {
const ui = buildMinimalUiConfigs();
const db = mapUiToDbConfig(ui.githubConfig, ui.giteaConfig, ui.mirrorOptions, ui.advancedOptions, {
giteaConfig: { forkStrategy: "full-copy" },
});
expect(db.giteaConfig.forkStrategy).toBe("full-copy");
});
test("mapUiToDbConfig lets skipForks override a stored forkStrategy", () => {
const ui = buildMinimalUiConfigs();
const withSkip: AdvancedOptions = { ...ui.advancedOptions, skipForks: true };
const db = mapUiToDbConfig(ui.githubConfig, ui.giteaConfig, ui.mirrorOptions, withSkip, {
giteaConfig: { forkStrategy: "full-copy" },
});
expect(db.giteaConfig.forkStrategy).toBe("skip");
});
test("mapUiToDbConfig resets a stale skip forkStrategy to reference when skipForks is unchecked", () => {
const ui = buildMinimalUiConfigs();
const db = mapUiToDbConfig(ui.githubConfig, ui.giteaConfig, ui.mirrorOptions, ui.advancedOptions, {
giteaConfig: { forkStrategy: "skip" },
});
expect(db.giteaConfig.forkStrategy).toBe("reference");
});
+39 -23
View File
@@ -51,28 +51,37 @@ function normalizeOrgList(orgs: string[] | undefined): string[] {
/**
* Maps UI config structure to database schema structure
*
* `existing` is the stored DB config (if any). Fields the Configuration form
* doesn't expose (mirrorInterval, topics, templates, ...) are preserved from it
* so a UI save can't silently reset values configured via environment variables
* (e.g. GITEA_MIRROR_INTERVAL, see issue #338).
*/
export function mapUiToDbConfig(
githubConfig: GitHubConfig,
giteaConfig: GiteaConfig,
mirrorOptions: MirrorOptions,
advancedOptions: AdvancedOptions
advancedOptions: AdvancedOptions,
existing?: {
githubConfig?: Partial<DbGitHubConfig>;
giteaConfig?: Partial<DbGiteaConfig>;
}
): { githubConfig: DbGitHubConfig; giteaConfig: DbGiteaConfig } {
// Map GitHub config to match database schema fields
const dbGithubConfig: DbGitHubConfig = {
// Map username to owner field
owner: githubConfig.username,
type: "personal", // Default to personal, could be made configurable
type: existing?.githubConfig?.type || "personal", // Not in UI; preserve stored value
token: githubConfig.token || "",
// Map checkbox fields with proper names
includeStarred: githubConfig.mirrorStarred,
includePrivate: githubConfig.privateRepositories,
includeCollaboratorRepos: githubConfig.includeCollaboratorRepos ?? true,
includeForks: !advancedOptions.skipForks, // Note: UI has skipForks, DB has includeForks
skipForks: advancedOptions.skipForks, // Add skipForks field
includeArchived: false, // Not in UI yet, default to false
includePublic: true, // Not in UI yet, default to true
includeArchived: existing?.githubConfig?.includeArchived ?? false, // Not in UI; preserve stored value
includePublic: existing?.githubConfig?.includePublic ?? true, // Not in UI; preserve stored value
// Organization related fields — opt-in allowlist (empty = all org repos)
includeOrganizations: normalizeOrgList(githubConfig.includeOrganizations),
@@ -102,28 +111,35 @@ export function mapUiToDbConfig(
organization: giteaConfig.organization, // Add organization field
preserveOrgStructure: giteaConfig.mirrorStrategy === "preserve" || giteaConfig.mirrorStrategy === "mixed", // Add preserveOrgStructure field
// Mirror interval and options
mirrorInterval: "8h", // Default value, could be made configurable
// Mirror interval — not in UI; preserve the stored value so a save doesn't
// reset an env-configured GITEA_MIRROR_INTERVAL back to the default (#338)
mirrorInterval: existing?.giteaConfig?.mirrorInterval || "8h",
lfs: mirrorOptions.mirrorLFS || false, // LFS mirroring option
wiki: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.wiki,
// Visibility settings
visibility: giteaConfig.visibility || "default",
preserveVisibility: false, // This should be a separate field, not the same as preserveOrgStructure
// Organization creation
createOrg: true, // Default to true
// Template settings (not in UI yet)
templateOwner: undefined,
templateRepo: undefined,
// Topics
addTopics: true, // Default to true
topicPrefix: undefined,
// Fork strategy
forkStrategy: advancedOptions.skipForks ? "skip" : "reference",
preserveVisibility: existing?.giteaConfig?.preserveVisibility ?? false, // Not in UI; preserve stored value
// Organization creation — not in UI; preserve stored value
createOrg: existing?.giteaConfig?.createOrg ?? true,
// Template settings (not in UI yet) — preserve stored values
templateOwner: existing?.giteaConfig?.templateOwner,
templateRepo: existing?.giteaConfig?.templateRepo,
// Topics — not in UI; preserve stored values
addTopics: existing?.giteaConfig?.addTopics ?? true,
topicPrefix: existing?.giteaConfig?.topicPrefix,
// Fork strategy — skipForks is the only UI control; keep an env-configured
// "full-copy" instead of downgrading it, but reset a stale "skip" once the
// user unchecks skipForks.
forkStrategy: advancedOptions.skipForks
? "skip"
: existing?.giteaConfig?.forkStrategy && existing.giteaConfig.forkStrategy !== "skip"
? existing.giteaConfig.forkStrategy
: "reference",
// Mirror options from UI
issueConcurrency: giteaConfig.issueConcurrency ?? 3,
+34 -24
View File
@@ -94,38 +94,48 @@ export const POST: APIRoute = async ({ request, locals }) => {
const existingConfig = existingConfigResult[0];
// Parse the stored configs once — used both to preserve fields the
// Configuration form doesn't expose (e.g. env-configured mirrorInterval,
// see issue #338) and to preserve tokens when the form submits them empty.
let existingGithub: Record<string, any> | undefined;
let existingGitea: Record<string, any> | undefined;
if (existingConfig) {
try {
existingGithub =
typeof existingConfig.githubConfig === "string"
? JSON.parse(existingConfig.githubConfig)
: existingConfig.githubConfig;
existingGitea =
typeof existingConfig.giteaConfig === "string"
? JSON.parse(existingConfig.giteaConfig)
: existingConfig.giteaConfig;
} catch (parseError) {
console.error("Failed to parse existing config:", parseError);
}
}
// Map UI structure to database schema structure first
const { githubConfig: mappedGithubConfig, giteaConfig: mappedGiteaConfig } = mapUiToDbConfig(
githubConfig,
giteaConfig,
mirrorOptions,
advancedOptions
advancedOptions,
{ githubConfig: existingGithub, giteaConfig: existingGitea }
);
// Preserve tokens if fields are empty
if (existingConfig) {
try {
const existingGithub =
typeof existingConfig.githubConfig === "string"
? JSON.parse(existingConfig.githubConfig)
: existingConfig.githubConfig;
const existingGitea =
typeof existingConfig.giteaConfig === "string"
? JSON.parse(existingConfig.giteaConfig)
: existingConfig.giteaConfig;
// Decrypt existing tokens before preserving
if (!mappedGithubConfig.token && existingGithub.token) {
mappedGithubConfig.token = decrypt(existingGithub.token);
}
if (!mappedGiteaConfig.token && existingGitea.token) {
mappedGiteaConfig.token = decrypt(existingGitea.token);
}
} catch (tokenError) {
console.error("Failed to preserve tokens:", tokenError);
try {
// Decrypt existing tokens before preserving
if (!mappedGithubConfig.token && existingGithub?.token) {
mappedGithubConfig.token = decrypt(existingGithub.token);
}
if (!mappedGiteaConfig.token && existingGitea?.token) {
mappedGiteaConfig.token = decrypt(existingGitea.token);
}
} catch (tokenError) {
console.error("Failed to preserve tokens:", tokenError);
}
// Encrypt tokens before saving