Skip to content

Harden backup against GitLab soft-deletion and mirror branch protection#3

Closed
SupremeCommanderHedgehog wants to merge 1 commit into
mainfrom
harden-gitlab-soft-deletion
Closed

Harden backup against GitLab soft-deletion and mirror branch protection#3
SupremeCommanderHedgehog wants to merge 1 commit into
mainfrom
harden-gitlab-soft-deletion

Conversation

@SupremeCommanderHedgehog

Copy link
Copy Markdown
Owner

Fixes the silent backup failures tracked in #2.

Root causes

  1. GitLab soft-deletion (outage, all repos). A project deleted on gitlab.com Free lingers ~7 days in read-only retention, still occupying its path. git push --mirror403 You are not allowed to push code to this project. Get-GitLabProject sees it as existing (HTTP 200, not 404), so the script neither recreates nor restores it — it 403-loops every run.
  2. Default-branch protection (chronic, active repos). GitLab auto-protects the default branch; push --mirror needs force-updates on active repos (rebased branches, deleted PR refs), which the protected-branch hook rejects. Private repos only needed fast-forwards, masking this for weeks.

Changes

  • Get-GitLabProjectDeletionSchedule — reads marked_for_deletion_at (fallback to deprecated marked_for_deletion_on) via PSObject, so an absent field yields $null under Set-StrictMode instead of throwing.
  • Restore-GitLabProject — restores a pending-deletion project (numeric-id route; the encoded-path form returns 405).
  • Clear-GitLabProtectedBranches — strips mirror branch protection before each push (per_page=100; empty-list result filtered for StrictMode).
  • backup.ps1 loop wires these in and logs a WARN on restore.

Verification

  • Three independent full backup.ps1 runs; final: 7 succeeded, 0 failed, exit 0.
  • PSScriptAnalyzer clean on both files.
  • High-effort code review: 2 confirmed correctness findings applied (StrictMode field access; pagination); 2 plausible cleanups consciously dismissed (async-restore race — empirically synchronous; per-run strip cost — negligible, creation-time alternative uses a deprecated field).

🤖 Generated with Claude Code

The scheduled backup fired on time but silently pushed nothing for
affected repos, because git push --mirror failed for two independent
reasons the log recorded only as "exit 1":

1. Soft-deletion: a project deleted on gitlab.com (Free) lingers in a
   ~7-day retention, still occupying its path but read-only, so pushes
   return 403. Get-GitLabProject sees it as existing (HTTP 200, not
   404), so the script neither recreates nor restores it and 403-loops
   every run.
2. Branch protection: GitLab auto-protects the default branch, and
   push --mirror needs force-updates on active repos (rebased branches,
   deleted PR refs), which the protected-branch hook rejects.

Fix:
- Get-GitLabProjectDeletionSchedule reads marked_for_deletion_at (with
  the deprecated marked_for_deletion_on as fallback) via PSObject so an
  absent field yields $null under Set-StrictMode; backup.ps1 restores
  pending-deletion projects and logs a WARN.
- Clear-GitLabProtectedBranches strips mirror branch protection before
  each push (per_page=100; empty-list result filtered for StrictMode).

Verified: full backup.ps1 run is 7 succeeded, 0 failed, exit 0;
PSScriptAnalyzer clean.

Closes #2

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AMMuyUCUP5qoeD9wvesThC
@SupremeCommanderHedgehog

Copy link
Copy Markdown
Owner Author

Squash-merged to main as 2d7e9eb, signed with the repo owner's GPG key (5741F291946EBD4A8B698BA1BE707B220C995478). Done locally rather than via the GitHub merge button because gh pr merge produces an unsigned squash commit. Closes #2.

SupremeCommanderHedgehog added a commit that referenced this pull request Jul 18, 2026
…on (#3)

The scheduled backup fired on time but silently pushed nothing for
affected repos, because git push --mirror failed for two independent
reasons the log recorded only as "exit 1":

1. Soft-deletion: a project deleted on gitlab.com (Free) lingers in a
   ~7-day retention, still occupying its path but read-only, so pushes
   return 403. Get-GitLabProject sees it as existing (HTTP 200, not
   404), so the script neither recreates nor restores it and 403-loops.
2. Branch protection: GitLab auto-protects the default branch, and
   push --mirror needs force-updates on active repos (rebased branches,
   deleted PR refs), which the protected-branch hook rejects.

Fix:
- Get-GitLabProjectDeletionSchedule reads marked_for_deletion_at (with
  the deprecated marked_for_deletion_on as fallback) via PSObject so an
  absent field yields $null under Set-StrictMode; backup.ps1 restores
  pending-deletion projects and logs a WARN.
- Clear-GitLabProtectedBranches strips mirror branch protection before
  each push (per_page=100; empty-list result filtered for StrictMode).

Verified: full backup.ps1 run is 7 succeeded, 0 failed, exit 0;
PSScriptAnalyzer clean.

Closes #2

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AMMuyUCUP5qoeD9wvesThC
@SupremeCommanderHedgehog
SupremeCommanderHedgehog deleted the harden-gitlab-soft-deletion branch July 18, 2026 04:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant