--- name: gitea-release-workflow description: Use when tagging a release for a plugin/package on a self-hosted Gitea instance, especially when the release needs to be downloadable by an external system (an auto-updater, a CI job, etc.). --- # Gitea Release Workflow ## Creating a release ```bash curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \ -d '{"tag_name": "v1.0.0", "name": "v1.0.0", "body": "", "draft": false, "prerelease": false}' \ "$HOST/api/v1/repos/$OWNER/$REPO/releases" ``` Verify it landed: ```bash curl -s -H "Authorization: token $TOKEN" "$HOST/api/v1/repos/$OWNER/$REPO/releases/latest" ``` Note: on at least one real self-hosted instance, `/releases/latest` 404s for unauthenticated requests even on a public repo — always send the token, don't assume public-repo endpoints are auth-free. ## KNOWN GOTCHA: release-asset downloads can be unreachable **Confirmed live on a real self-hosted Gitea instance**: uploading a zip as a release asset and then trying to fetch it via the asset's own `browser_download_url` field returned an **internal address** (`http://localhost:3000/...`) instead of the instance's real public hostname — a `ROOT_URL` misconfiguration server-side. This makes the uploaded asset **completely unreachable from outside the Gitea server itself**, even though the release object and its metadata (version, changelog) are fine. **Symptom**: `update-check`-style API calls that only read release metadata work fine; the actual **download** step 404s or times out. **Workaround that reliably works**: don't rely on the release-asset upload/download mechanism at all for the actual file bytes. Instead use Gitea's **repo-archive endpoint**, which zips the tagged git tree on-the-fly and is served from the same working API path pattern as everything else: ```bash curl -s -H "Authorization: token $TOKEN" -o output.zip \ "$HOST/api/v1/repos/$OWNER/$REPO/archive/$TAG.zip" ``` This means: the release object still gets created (for its version/ changelog metadata, which downstream consumers do need), but you do **not** need to upload a zip as a release asset at all — the actual distributable content is whatever's in the tagged git tree, fetched via `/archive/{tag}.zip` at request time. **Before trusting the asset-download path on a NEW Gitea instance**: test it once (`curl` the `browser_download_url` directly) rather than assuming it works — if it does work on a given instance, the asset-upload approach is simpler and fine to use; the archive-endpoint workaround is only needed on instances that have this specific misconfiguration. Don't apply the workaround blindly without confirming the actual failure mode exists on the instance you're using. ## Archive-endpoint folder-naming gotcha The `/archive/{tag}.zip` endpoint names the zip's top-level folder based on the repo (exact naming varies by Gitea version — sometimes the repo name, sometimes lowercased). If the repo's own files live in a subdirectory rather than at repo root, the resulting zip gets a **double-nested** top-level folder. Keep single-purpose repos (one plugin/package per repo) flat at the root to avoid this — see the `wordpress-plugin-rebrand` skill's flatten-to-root step. ## Forking into a new org ```bash curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \ -d '{"organization": "TargetOrg", "name": "new-repo-name"}' \ "$HOST/api/v1/repos/$SOURCE_OWNER/$SOURCE_REPO/forks" ``` The `name` field renames the fork on creation — you don't have to accept the source repo's own name. Fork inherits the **source repo's full git history**; if you're about to do a wholesale rebrand and don't need that history preserved, a `git init` + single clean commit + `git push --force` is often cleaner than carrying an unrelated-looking history forward — but only force-push a repo that's genuinely fresh with nothing else depending on its prior state. ## Verify a release actually works end-to-end Don't stop at "the release object exists" — actually fetch the archive and inspect it: ```bash curl -s -H "Authorization: token $TOKEN" -o /tmp/verify.zip \ "$HOST/api/v1/repos/$OWNER/$REPO/archive/$TAG.zip" unzip -l /tmp/verify.zip | head -20 ``` Confirm: single non-double-nested top-level folder, expected files present, no truncation.