Files
agent-skills/skills/gitea-release-workflow/SKILL.md
T
iWP Claudy 39a637410c Initial skill library: 9 skills for delegate briefs
WordPress plugin rebrand/conventions/remote-CLI patterns, Gitea release
workflow, bastille jail provisioning, remote shell quoting safety, server
fleet map, delegate brief writing, and verification discipline -- all
derived from real incidents this session, plus two skills adapted (MIT
license, attributed) from obra/superpowers and andrej-karpathy-skills.
2026-08-02 20:08:29 +02:00

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

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.