Files
agent-skills/skills/gitea-release-workflow/SKILL.md
T
Malin f69f2580b0 docs: gitea-release-workflow — asset downloads work via public host substitution
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.
2026-08-12 13:06:54 +02:00

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_urlon 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.