fix: stop false-positive orphan archiving and heal sync 405s on archived-* renamed mirrors (#331)#336
Conversation
…ved-* renamed mirrors (#331) Three related fixes for the "repos keep getting archived and then fail to sync with HTTP 405" report: 1. Orphan cleanup no longer archives on bulk-list absence alone. Repos added via the "+" Add Repository dialog (foreign owner, not starred) can never appear in the authenticated bulk fetches, so every cleanup cycle deterministically flagged them as orphaned and archived them. identifyOrphanedRepositories() now runs a targeted per-repo confirmation (starred check or repos.get) and only treats a clean 404 as gone; any other outcome fails safe. 2. The archived-* rename is persisted. archiveGiteaRepo() now returns the actual post-rename name and the cleanup service records it in mirroredLocation, so the DB no longer points at a name that only 301-redirects. 3. Sync self-heals repos renamed in Gitea/Forgejo. Requests to a renamed repo get a 301; fetch follows it, downgrading POST to GET, which lands on the POST-only mirror-sync endpoint as a 405. The sync candidate loop now adopts the canonical owner/name from the GET response body before POSTing, tries an archived-{name} fallback for archived repos (guarded by an original_url source match), and keeps archived repos archived: no mirror-interval PATCH, status stays 'archived' per the documented Manual Sync contract. Also hardens mirrorGitHubReleasesToGitea to derive GitHub coordinates from fullName so Gitea-side names can never leak into GitHub API calls. Verified end-to-end on Forgejo 15.0.3 (rootless): pre-fix reproduces the exact 405; post-fix the stale-name sync succeeds (GET stale -> 301, GET canonical -> 200, POST canonical mirror-sync -> 200), archived repos keep interval "0s" with no PATCH issued, and non-archived renamed repos heal and get the configured interval applied.
Deploying gitea-mirror-website with
|
| Latest commit: |
d552e20
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://afc79070.gitea-mirror-website.pages.dev |
| Branch Preview URL: | https://fix-331-archived-rename-sync.gitea-mirror-website.pages.dev |
🐳 Docker Image Built SuccessfullyYour PR image is available for testing: Image Tag: Pull and Testdocker pull ghcr.io/raylabshq/gitea-mirror:pr-336
docker run -d -p 4321:4321 -e BETTER_AUTH_SECRET=your-secret-here -e BETTER_AUTH_URL=http://localhost:4321 --name gitea-mirror-test ghcr.io/raylabshq/gitea-mirror:pr-336Docker Compose Testingservices:
gitea-mirror:
image: ghcr.io/raylabshq/gitea-mirror:pr-336
ports:
- "4321:4321"
environment:
- BETTER_AUTH_SECRET=your-secret-here
- BETTER_AUTH_URL=http://localhost:4321
- BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:4321
📦 View in GitHub Packages |
🔍 Vulnerabilities of
|
| digest | sha256:8d13e5b66f95e3f46b051d23547df96dace626a12620c19015810d20ed69d942 |
| vulnerabilities | |
| platform | linux/amd64 |
| size | 293 MB |
| packages | 824 |
📦 Base Image debian:13
| also known as |
|
| digest | sha256:de6a8f94c0e84f57a8e29769966b9d8c199b0891634280ad75ad804cf9827825 |
| vulnerabilities |
Description
Description
Description
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
Description
Description
Description
Description
Description
Description
Description
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
Description
Description
Description
Description
Description
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
Description
Description
Description
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
|
Overview
Labels (8 changes)
-org.opencontainers.image.created=2026-07-02T10:09:11.333Z
+org.opencontainers.image.created=2026-05-13T03:50:40.734Z
-org.opencontainers.image.description=Gitea Mirror auto-syncs GitHub repos to your self-hosted Gitea/Forgejo, with a sleek Web UI and easy Docker deployment.
+org.opencontainers.image.description=Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one
-org.opencontainers.image.licenses=AGPL-3.0
+org.opencontainers.image.licenses=NOASSERTION
-org.opencontainers.image.revision=d552e205689c180cc7afbb9bb5bf0e131186e64e
+org.opencontainers.image.revision=0d9b296af33f2b851fcbf4df3e9ec89751734ba4
-org.opencontainers.image.source=https://github.com/RayLabsHQ/gitea-mirror
+org.opencontainers.image.source=https://github.com/oven-sh/bun
-org.opencontainers.image.title=gitea-mirror
+org.opencontainers.image.title=bun
-org.opencontainers.image.url=https://github.com/RayLabsHQ/gitea-mirror
+org.opencontainers.image.url=https://github.com/oven-sh/bun
-org.opencontainers.image.version=pr-336
+org.opencontainers.image.version=1.3.14-debian |
Fixes the remaining half of #331: repos getting renamed to
archived-*in Forgejo/Gitea even though they still exist on GitHub, and every subsequent sync of those repos failing with HTTP 405.Root cause
The archiving was deterministic, not transient. Repos added via the "+" Add Repository dialog are stored with
isStarred: falseand a foreign owner — a combination that can never appear in the authenticated bulk fetches (listForAuthenticatedUserwithowner,collaborator,organization_memberaffiliation, plus the starred list). So the orphan-cleanup pass (startup + every 6h + every scheduler cycle, default actionarchive) flagged each of them as "no longer on GitHub" and archived them within one cycle. That's why fresh repos were archived within the hour — 9 of the reporter's 10 renamed repos are alive and unarchived on GitHub.The 405 is a redirect artifact. Archiving renames the repo to
archived-{name}in Forgejo but the DB kept the old name. Requests to the old name get a 301;fetchfollows it silently — GETs succeed against the new repo, but the POST to/mirror-syncis method-downgraded to GET per the WHATWG spec and lands on a POST-only endpoint → 405 Method Not Allowed.The fix (three parts)
repository-cleanup-service.ts) — a repo missing from the bulk list is only orphaned if a targeted per-repo check (checkRepoIsStarredByAuthenticatedUser/repos.get) returns a clean 404. Any other outcome (found, rate limit, network error) fails safe and logs a warning.gitea.ts) —archiveGiteaRepo()returns the actual post-rename name and the cleanup service records it inmirroredLocation, so the DB never points at a 301.gitea-enhanced.ts) — the sync candidate loop now adopts the canonical owner/name from the GET response body before POSTing (defeats silent redirects for any Gitea-side rename), and archived repos additionally try anarchived-{name}fallback guarded by anoriginal_urlsource match. Archived repos stay archived: no mirror-interval PATCH, status remainsarchived, matching the documented Manual Sync contract ("disables automatic syncs — use Manual Sync when you want to refresh").Also hardens
mirrorGitHubReleasesToGiteato derive GitHub coordinates fromfullName, so a Gitea-side name can never leak into GitHub API calls.Verification
archived-*mirror) syncs successfully — access log showsGET stale → 301,GET canonical → 200,POST canonical /mirror-sync → 200 OK, andmirror_updatedadvances. Archived repos keep interval0swith no PATCH issued; a non-archived renamed repo heals and gets the configured interval (8h0m0s).gitea-archive.test.ts,repository-cleanup-service.test.ts, extendedgitea-enhanced.test.ts). Full suite: 381 pass / 0 fail.Fixes #331