Release assets were uploaded in only one place — the create branch of mirrorGitHubReleasesToGitea() (src/lib/gitea.ts). When a Gitea release already exists, the code takes the update branch, PATCHes the changelog/title, and continues without ever touching assets. There was no idempotent reconciliation.
So any release whose assets weren't fully uploaded on that single create-path run — first sync interrupted, container restart, a transient download/upload failure, large multi-MB assets — stays permanently asset-less, and re-syncing always hits the update branch and can never recover it. Asset failures were also swallowed to console.error + continue, so the job still reported success ("no errors in the logs", per the issue).
Reproduced on a real Forgejo pull-mirror
Using shauninman/MinUI against Forgejo 11 (native mirror: true repo, as gitea-mirror creates):
Clean first sync of the real function works — both releases created, all 4 assets (~74 MB each) uploaded.
Deleting the assets off v20251127-1 (= interrupted first upload) and re-running the real function logs Updating existing release v20251127-1 and leaves it at 0 assets — exactly the reported symptom (Forgejo's 4.6 MB auto-generated source archive only).
Also ruled out every infra cause: Forgejo accepts release creation + 35 MB+ uploads on mirror repos; its native mirror creates 43 tags but 0 releases; Bun's fetch downloads the GitHub asset fine.
The fix
reconcileReleaseAssets() — idempotent reconciler run on both the create and update paths. Compares Gitea's existing attachments to GitHub's by name + size: skips matches, uploads missing ones, replaces size-mismatched copies. Existing broken releases self-heal on the next sync.
classifyAssetsForReconciliation() — pure decision function extracted for unit testing (the lack of asset tests is why this slipped past #310).
Surfaced failures — asset upload counts in the summary log + a visible ⚠️ warning when any fail, instead of silent swallowing.
5 new unit tests covering the broken state, partial backfill, idempotency, size-mismatch replacement, and the no-assets case.
Verification
bun test src/lib/gitea-releases.test.ts → 15/15 pass.
End-to-end against the real mirrorGitHubReleasesToGitea on a Forgejo pull-mirror: v20251127-1 went 0 → 2 assets (Backfilled 2 missing/changed asset(s)), already-present assets skipped (no re-download), second run uploads 0 (fully idempotent).
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.
## 📋 Pull Request Information
**Original PR:** https://github.com/RayLabsHQ/gitea-mirror/pull/332
**Author:** [@arunavo4](https://github.com/arunavo4)
**Created:** 6/23/2026
**Status:** ✅ Merged
**Merged:** 6/23/2026
**Merged by:** [@arunavo4](https://github.com/arunavo4)
**Base:** `main` ← **Head:** `fix/release-asset-reconciliation-331`
---
### 📝 Commits (1)
- [`48a701b`](https://github.com/RayLabsHQ/gitea-mirror/commit/48a701b72868b9e1a528467a4a91e5d159652ef4) fix(releases): mirror assets idempotently so missing assets self-heal (#331)
### 📊 Changes
**2 files changed** (+289 additions, -48 deletions)
<details>
<summary>View changed files</summary>
📝 `src/lib/gitea-releases.test.ts` (+83 -1)
📝 `src/lib/gitea.ts` (+206 -47)
</details>
### 📄 Description
Fixes #331
## The bug
Release assets were uploaded in **only one place** — the *create* branch of `mirrorGitHubReleasesToGitea()` (`src/lib/gitea.ts`). When a Gitea release already exists, the code takes the *update* branch, PATCHes the changelog/title, and `continue`s **without ever touching assets**. There was no idempotent reconciliation.
So any release whose assets weren't fully uploaded on that single create-path run — first sync interrupted, container restart, a transient download/upload failure, large multi-MB assets — stays **permanently asset-less**, and re-syncing always hits the update branch and can never recover it. Asset failures were also swallowed to `console.error` + `continue`, so the job still reported success ("no errors in the logs", per the issue).
## Reproduced on a real Forgejo pull-mirror
Using `shauninman/MinUI` against Forgejo 11 (native `mirror: true` repo, as gitea-mirror creates):
- Clean first sync of the **real** function works — both releases created, all 4 assets (~74 MB each) uploaded.
- Deleting the assets off `v20251127-1` (= interrupted first upload) and re-running the real function logs `Updating existing release v20251127-1` and leaves it at **0 assets** — exactly the reported symptom (Forgejo's 4.6 MB auto-generated source archive only).
Also ruled out every infra cause: Forgejo accepts release creation + 35 MB+ uploads on mirror repos; its native mirror creates **43 tags but 0 releases**; Bun's `fetch` downloads the GitHub asset fine.
## The fix
- **`reconcileReleaseAssets()`** — idempotent reconciler run on **both** the create and update paths. Compares Gitea's existing attachments to GitHub's by **name + size**: skips matches, uploads missing ones, replaces size-mismatched copies. Existing broken releases **self-heal on the next sync**.
- **`classifyAssetsForReconciliation()`** — pure decision function extracted for unit testing (the lack of asset tests is why this slipped past #310).
- **Surfaced failures** — asset upload counts in the summary log + a visible `⚠️` warning when any fail, instead of silent swallowing.
- **5 new unit tests** covering the broken state, partial backfill, idempotency, size-mismatch replacement, and the no-assets case.
## Verification
- `bun test src/lib/gitea-releases.test.ts` → **15/15 pass**.
- End-to-end against the **real `mirrorGitHubReleasesToGitea`** on a Forgejo pull-mirror: `v20251127-1` went **0 → 2 assets** (`Backfilled 2 missing/changed asset(s)`), already-present assets skipped (no re-download), second run uploads 0 (fully idempotent).
---
<sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
📋 Pull Request Information
Original PR: https://github.com/RayLabsHQ/gitea-mirror/pull/332
Author: @arunavo4
Created: 6/23/2026
Status: ✅ Merged
Merged: 6/23/2026
Merged by: @arunavo4
Base:
main← Head:fix/release-asset-reconciliation-331📝 Commits (1)
48a701bfix(releases): mirror assets idempotently so missing assets self-heal (#331)📊 Changes
2 files changed (+289 additions, -48 deletions)
View changed files
📝
src/lib/gitea-releases.test.ts(+83 -1)📝
src/lib/gitea.ts(+206 -47)📄 Description
Fixes #331
The bug
Release assets were uploaded in only one place — the create branch of
mirrorGitHubReleasesToGitea()(src/lib/gitea.ts). When a Gitea release already exists, the code takes the update branch, PATCHes the changelog/title, andcontinues without ever touching assets. There was no idempotent reconciliation.So any release whose assets weren't fully uploaded on that single create-path run — first sync interrupted, container restart, a transient download/upload failure, large multi-MB assets — stays permanently asset-less, and re-syncing always hits the update branch and can never recover it. Asset failures were also swallowed to
console.error+continue, so the job still reported success ("no errors in the logs", per the issue).Reproduced on a real Forgejo pull-mirror
Using
shauninman/MinUIagainst Forgejo 11 (nativemirror: truerepo, as gitea-mirror creates):v20251127-1(= interrupted first upload) and re-running the real function logsUpdating existing release v20251127-1and leaves it at 0 assets — exactly the reported symptom (Forgejo's 4.6 MB auto-generated source archive only).Also ruled out every infra cause: Forgejo accepts release creation + 35 MB+ uploads on mirror repos; its native mirror creates 43 tags but 0 releases; Bun's
fetchdownloads the GitHub asset fine.The fix
reconcileReleaseAssets()— idempotent reconciler run on both the create and update paths. Compares Gitea's existing attachments to GitHub's by name + size: skips matches, uploads missing ones, replaces size-mismatched copies. Existing broken releases self-heal on the next sync.classifyAssetsForReconciliation()— pure decision function extracted for unit testing (the lack of asset tests is why this slipped past #310).⚠️warning when any fail, instead of silent swallowing.Verification
bun test src/lib/gitea-releases.test.ts→ 15/15 pass.mirrorGitHubReleasesToGiteaon a Forgejo pull-mirror:v20251127-1went 0 → 2 assets (Backfilled 2 missing/changed asset(s)), already-present assets skipped (no re-download), second run uploads 0 (fully idempotent).🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.