Confirmed on Gitea 1.27.1 (2026-08-12): browser_download_url in the API response points at an internal hostname (gitea.barky) but the file downloads correctly once you swap in the instance's public hostname. Verified end-to-end with a real binary asset (md5sum match) on a private repo accessed via token.
5.5 KiB
name, description
| name | description |
|---|---|
| gitea-release-workflow | 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
curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d '{"tag_name": "v1.0.0", "name": "v1.0.0", "body": "<changelog>", "draft": false, "prerelease": false}' \
"$HOST/api/v1/repos/$OWNER/$REPO/releases"
Verify it landed:
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.
Attaching a binary asset (e.g. a compiled build artifact) to a release
For releases that need to ship a real binary (a compiled .app, a zip, an
installer) rather than just source-tree metadata, upload it as a release
asset after creating the release:
curl -s -X POST -H "Authorization: token $TOKEN" \
-F "attachment=@/path/to/file.ext" \
"$HOST/api/v1/repos/$OWNER/$REPO/releases/$RELEASE_ID/assets"
The response includes browser_download_url — on this instance
(confirmed on Gitea 1.27.1, 2026-08-12) it returns an internal address
(http://gitea.barky/attachments/<uuid>) even though the download itself
works fine once you substitute the public hostname:
https://devops.cloudhost.es/attachments/<uuid>. This is a narrower,
now-fixed case of the ROOT_URL gotcha below — as of this instance's current
state, asset upload/download for private repos accessed with a token
works correctly end-to-end; only the URL string in the API response is
wrong. Don't assume this generalizes to every repo/instance — verify per
the "Verify a release actually works end-to-end" section, but know that
the fix here is a simple hostname swap, not the full archive-endpoint
workaround.
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:
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
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:
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.