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.
99 lines
4.3 KiB
Markdown
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.
|