mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-03-13 22:12:54 +03:00
feat: smart force-push protection with backup strategies (#206)
* feat: smart force-push protection with backup strategies (#187) Replace blunt `backupBeforeSync` boolean with `backupStrategy` enum offering four modes: disabled, always, on-force-push (default), and block-on-force-push. This dramatically reduces backup storage for large mirror collections by only creating snapshots when force-pushes are actually detected. Detection works by comparing branch SHAs between Gitea and GitHub APIs before each sync — no git cloning required. Fail-open design ensures detection errors never block sync. Key changes: - Add force-push detection module (branch SHA comparison via APIs) - Add backup strategy resolver with backward-compat migration - Add pending-approval repo status with approve/dismiss UI + API - Add block-on-force-push mode requiring manual approval - Fix checkAncestry to only treat 404 as confirmed force-push (transient errors skip branch instead of false-positive blocking) - Fix approve-sync to bypass detection gate (skipForcePushDetection) - Fix backup execution to not be hard-gated by deprecated flag - Persist backupStrategy through config-mapper round-trip * fix: resolve four bugs in smart force-push protection P0: Approve flow re-blocks itself — approve-sync now calls syncGiteaRepoEnhanced with skipForcePushDetection: true so the detection+block gate is bypassed on approved syncs. P1: backupStrategy not persisted — added to both directions of the config-mapper. Don't inject a default in the mapper; let resolveBackupStrategy handle fallback so legacy backupBeforeSync still works for E2E tests and existing configs. P1: Backup hard-gated by deprecated backupBeforeSync — added force flag to createPreSyncBundleBackup; strategy-driven callers and approve-sync pass force: true to bypass the legacy guard. P1: checkAncestry false positives — now only returns false for 404/422 (confirmed force-push). Transient errors (rate limits, 500s) are rethrown so detectForcePush skips that branch (fail-open). * test(e2e): migrate backup tests from backupBeforeSync to backupStrategy Update E2E tests to use the new backupStrategy enum ("always", "disabled") instead of the deprecated backupBeforeSync boolean. * docs: add backup strategy UI screenshot * refactor(ui): move Destructive Update Protection to GitHub config tab Relocates the backup strategy section from GiteaConfigForm to GitHubConfigForm since it protects against GitHub-side force-pushes. Adds ShieldAlert icon to match other section header patterns. * docs: add force-push protection documentation and Beta badge Add docs/FORCE_PUSH_PROTECTION.md covering detection mechanism, backup strategies, API usage, and troubleshooting. Link it from README features list and support section. Mark the feature as Beta in the UI with an outline badge. * fix(ui): match Beta badge style to Git LFS badge
This commit is contained in:
@@ -6,13 +6,13 @@
|
||||
* by the 02-mirror-workflow suite.
|
||||
*
|
||||
* What is tested:
|
||||
* B1. Enable backupBeforeSync in config
|
||||
* B1. Enable backupStrategy: "always" in config
|
||||
* B2. Confirm mirrored repos exist in Gitea (precondition)
|
||||
* B3. Trigger a re-sync with backup enabled — verify the backup code path
|
||||
* runs (snapshot activity entries appear in the activity log)
|
||||
* B4. Inspect activity log for snapshot-related entries
|
||||
* B5. Enable blockSyncOnBackupFailure and verify the flag is persisted
|
||||
* B6. Disable backup and verify config resets cleanly
|
||||
* B6. Disable backup (backupStrategy: "disabled") and verify config resets cleanly
|
||||
*/
|
||||
|
||||
import { test, expect } from "@playwright/test";
|
||||
@@ -54,10 +54,10 @@ test.describe("E2E: Backup configuration", () => {
|
||||
const giteaToken = giteaApi.getTokenValue();
|
||||
expect(giteaToken, "Gitea token required").toBeTruthy();
|
||||
|
||||
// Save config with backup enabled
|
||||
// Save config with backup strategy set to "always"
|
||||
await saveConfig(request, giteaToken, appCookies, {
|
||||
giteaConfig: {
|
||||
backupBeforeSync: true,
|
||||
backupStrategy: "always",
|
||||
blockSyncOnBackupFailure: false,
|
||||
backupRetentionCount: 5,
|
||||
backupDirectory: "data/repo-backups",
|
||||
@@ -75,7 +75,7 @@ test.describe("E2E: Backup configuration", () => {
|
||||
const configData = await configResp.json();
|
||||
const giteaCfg = configData.giteaConfig ?? configData.gitea ?? {};
|
||||
console.log(
|
||||
`[Backup] Config saved: backupBeforeSync=${giteaCfg.backupBeforeSync}, blockOnFailure=${giteaCfg.blockSyncOnBackupFailure}`,
|
||||
`[Backup] Config saved: backupStrategy=${giteaCfg.backupStrategy}, blockOnFailure=${giteaCfg.blockSyncOnBackupFailure}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -202,7 +202,7 @@ test.describe("E2E: Backup configuration", () => {
|
||||
expect(
|
||||
backupJobs.length,
|
||||
"Expected at least one backup/snapshot activity entry when " +
|
||||
"backupBeforeSync is enabled and repos exist in Gitea",
|
||||
"backupStrategy is 'always' and repos exist in Gitea",
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
// Check for any failed backups
|
||||
@@ -247,7 +247,7 @@ test.describe("E2E: Backup configuration", () => {
|
||||
// Update config to block sync on backup failure
|
||||
await saveConfig(request, giteaToken, appCookies, {
|
||||
giteaConfig: {
|
||||
backupBeforeSync: true,
|
||||
backupStrategy: "always",
|
||||
blockSyncOnBackupFailure: true,
|
||||
backupRetentionCount: 5,
|
||||
backupDirectory: "data/repo-backups",
|
||||
@@ -284,7 +284,7 @@ test.describe("E2E: Backup configuration", () => {
|
||||
// Disable backup
|
||||
await saveConfig(request, giteaToken, appCookies, {
|
||||
giteaConfig: {
|
||||
backupBeforeSync: false,
|
||||
backupStrategy: "disabled",
|
||||
blockSyncOnBackupFailure: false,
|
||||
},
|
||||
});
|
||||
@@ -297,7 +297,7 @@ test.describe("E2E: Backup configuration", () => {
|
||||
const configData = await configResp.json();
|
||||
const giteaCfg = configData.giteaConfig ?? configData.gitea ?? {};
|
||||
console.log(
|
||||
`[Backup] After disable: backupBeforeSync=${giteaCfg.backupBeforeSync}`,
|
||||
`[Backup] After disable: backupStrategy=${giteaCfg.backupStrategy}`,
|
||||
);
|
||||
}
|
||||
console.log("[Backup] Backup configuration test complete");
|
||||
|
||||
Reference in New Issue
Block a user