From 0b65e407847e38941ce5b70d3d6b8045655b5b00 Mon Sep 17 00:00:00 2001 From: ARUNAVO RAY Date: Wed, 24 Jun 2026 17:18:46 +0530 Subject: [PATCH] fix(releases): create releases only for tags present in Gitea; stop sending target (#331) (#333) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release creation failed on some Gitea/Forgejo instances with "HTTP 404: The target couldn't be found", so no release (and therefore no assets) was ever created — re-syncing never recovered. Root cause: the create payload always sent `target: target_commitish` (e.g. "main"). When the release's git tag is not yet present in the Gitea mirror — which happens when Gitea's own git mirror clone lags behind the metadata sync — Gitea tries to *create* the tag from `target`; if that ref can't be resolved it returns a generic 404 ("The target couldn't be found"), and if it can, it would create a brand-new tag at the wrong commit. Reproduced the reporter's exact stack (Forgejo 15 rootless + read_only + cap_drop ALL + postgres, plus Gitea 1.20-1.26 and Forgejo 1.21-15): a healthy repo always succeeds — the 404 only occurs when the tag is absent at create time. Fix: - Before creating a release, verify the git tag already exists in Gitea. If it isn't synced yet, skip it (logged) and let a later sync create it once the mirror has the tag — never create a tag via `target`. - Drop the `target` field from both the create and update payloads. For a mirror the tag is synced from upstream, so Gitea attaches the release to the existing tag; `target` is unnecessary and is what triggers the 404. - Surface skipped-missing-tag releases in the summary log for diagnosability. Verified end-to-end against the real mirror function on a Forgejo instance: a release whose tag exists is created with its assets; a release whose tag was removed is skipped cleanly (no 404, no bogus tag) and picked up once the tag is present. --- src/lib/gitea.ts | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/src/lib/gitea.ts b/src/lib/gitea.ts index 5fab0d0..ad76a5a 100644 --- a/src/lib/gitea.ts +++ b/src/lib/gitea.ts @@ -2938,6 +2938,7 @@ export async function mirrorGitHubReleasesToGitea({ let mirroredCount = 0; let skippedCount = 0; + let skippedMissingTagCount = 0; let totalAssetsUploaded = 0; let totalAssetsFailed = 0; @@ -2996,7 +2997,8 @@ export async function mirrorGitHubReleasesToGitea({ `${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repoName}/releases/${existingRelease.id}`, { tag_name: release.tag_name, - target: release.target_commitish, + // Omit `target` — the release already exists and is anchored to its tag; + // re-sending target_commitish risks the same "target not found" 404 (#331). title: release.name || release.tag_name, body: releaseNote, draft: release.draft, @@ -3040,18 +3042,43 @@ export async function mirrorGitHubReleasesToGitea({ continue; } + // The git tag must already exist in Gitea before we create a release for it. + // For a mirror, tags are synced from upstream by Gitea's own git mirror, which + // can lag behind this metadata sync (e.g. a large/slow initial clone). If the + // tag isn't present yet, skip and let a later sync pick it up — do NOT ask Gitea + // to create the release against a `target` branch: + // - if the target can't be resolved Gitea returns 404 "The target couldn't be + // found" and the release is lost (#331), + // - if it can, Gitea would create a brand-new tag at the wrong commit. + const tagExists = await httpGet( + `${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repoName}/tags/${encodeURIComponent(release.tag_name)}`, + { Authorization: `token ${decryptedConfig.giteaConfig.token}` } + ) + .then(() => true) + .catch(() => false); + + if (!tagExists) { + console.warn( + `[Releases] Tag ${release.tag_name} is not present in Gitea yet — skipping release for now (the git mirror may still be syncing; it will be retried on the next sync)` + ); + skippedMissingTagCount++; + continue; + } + // Create new release with changelog/body content (includes GitHub date header) if (originalReleaseNote) { console.log(`[Releases] Including changelog for ${release.tag_name} (${originalReleaseNote.length} characters + GitHub date header)`); } else { console.log(`[Releases] Creating release ${release.tag_name} with GitHub date header (no changelog)`); } - + const createReleaseResponse = await httpPost( `${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repoName}/releases`, { tag_name: release.tag_name, - target: release.target_commitish, + // Intentionally omit `target`: the tag already exists (verified above), so + // Gitea attaches the release to it. Sending target_commitish can 404 with + // "The target couldn't be found" on some Gitea/Forgejo versions (#331). title: release.name || release.tag_name, body: releaseNote, draft: release.draft, @@ -3087,9 +3114,15 @@ export async function mirrorGitHubReleasesToGitea({ } console.log( - `✅ Mirrored/Updated ${mirroredCount} releases to Gitea (${skippedCount} already up-to-date); assets uploaded: ${totalAssetsUploaded}, failed: ${totalAssetsFailed}` + `✅ Mirrored/Updated ${mirroredCount} releases to Gitea (${skippedCount} already up-to-date, ${skippedMissingTagCount} skipped: tag not synced yet); assets uploaded: ${totalAssetsUploaded}, failed: ${totalAssetsFailed}` ); + if (skippedMissingTagCount > 0) { + console.warn( + `[Releases] ${skippedMissingTagCount} release(s) skipped because their git tag is not in Gitea yet for ${repository.fullName} — these will be created automatically once the git mirror finishes syncing the tags` + ); + } + if (totalAssetsFailed > 0) { console.error( `[Releases] ⚠️ ${totalAssetsFailed} release asset(s) failed to mirror for ${repository.fullName} — they will be retried on the next sync`