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

99 lines
4.3 KiB
Markdown

---
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": "<changelog>", "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.