Files
punktfunk/scripts/ci/gitea-release.sh
enricobuehler 0205c7b8d6
ci / rust (push) Failing after 37s
apple / swift (push) Successful in 56s
ci / web (push) Successful in 42s
ci / docs-site (push) Failing after 27m33s
android / android (push) Failing after 28m53s
windows-host / package (push) Failing after 28m55s
deb / build-publish (push) Successful in 2m28s
decky / build-publish (push) Successful in 23s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
ci / bench (push) Successful in 4m34s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 46s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m20s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 4m4s
flatpak / build-publish (push) Successful in 4m19s
docker / deploy-docs (push) Successful in 24s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 7m38s
release / apple (push) Successful in 4m36s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m48s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m25s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 50s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m6s
ci(release): split canary/stable tracks + unified Gitea Releases
A push to main publishes canary builds to canary channels (fast iteration,
unchanged); a single vX.Y.Z tag releases every platform at one version to the
stable channels and attaches all artifacts (.deb/.rpm/.msix/.apk/.aab/.dmg +
flatpak/decky/host-installer) to one Gitea Release. Collapses the
host-v*/win-v*/host-win-v* tag namespaces into v* — the channel split makes the
version-shadow bug structurally impossible (canary and stable are separate repos,
never a shared version line).

- scripts/ci/gitea-release.{sh,ps1}: one idempotent release helper
  (create-or-fetch + delete-before-upload), replacing 3 copy-pasted inline blocks
  and fixing their latent 409-on-reupload bug; prerelease flag auto-derived from
  the tag (an -rc tag won't shadow "Latest")
- channels: apt canary/stable distributions; rpm *-canary/base groups; flatpak
  canary/stable OSTree branches + a 2nd .Canary.flatpakref; generic-registry
  canary/ vs latest/ aliases; Play internal/alpha; Apple TestFlight vs notarized DMG
- android versionName threaded through gradle (versionCode stays run_number);
  Apple canary = TestFlight-only (no DMG/tvOS); canary base bumped to 0.3.0
- docs: new docs-site channels.md (subscribe table + cut-a-release runbook +
  box migration), refreshed ci.md workflow table + packaging READMEs

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 17:26:38 +00:00

96 lines
4.9 KiB
Bash

# shellcheck shell=bash
# Shared Gitea Release helpers for the punktfunk CI workflows (Linux + macOS runners).
#
# Source this file, then call ensure_release / upsert_asset. It replaces the three
# copy-pasted inline blocks that used to live in release.yml / flatpak.yml / decky.yml,
# and fixes a latent bug those had: the bare asset POST returns 409 if an asset with the
# same name already exists, so re-running a workflow — or reusing the rolling `canary`
# release with stable filenames — would fail. upsert_asset deletes the old asset first.
#
# Callers run under Gitea Actions' default `bash -eo pipefail`, so a non-zero return from
# these functions aborts the step (the desired behaviour on a real failure).
#
# Env (Gitea Actions sets the first two automatically in every step):
# GITHUB_SERVER_URL e.g. https://git.unom.io
# GITHUB_REPOSITORY e.g. unom/punktfunk
# GITEA_TOKEN a PAT with repository (release) write scope — set from secrets.REGISTRY_TOKEN
# (the same PAT the package uploads use; it must carry `write:repository`,
# not only `write:package`, or the release-asset POST 403s)
#
# Requires: curl + python3 (python3 is already a proven dependency on every runner that
# attaches releases today — macOS, the fedora flatpak container, the node:bookworm decky
# image; the .deb runner installs it alongside its other apt deps).
_gitea_api() { printf '%s/api/v1/repos/%s' "${GITHUB_SERVER_URL:?}" "${GITHUB_REPOSITORY:?}"; }
# Tiny JSON / URL helpers. python3 reads the TOP-LEVEL "id" only, so there is no ambiguity
# with the nested author.id / assets[].id fields a string-grep would trip over.
_json_id() { python3 -c 'import json,sys;print(json.load(sys.stdin).get("id",""))' 2>/dev/null; }
_json_asset_id() {
python3 -c 'import json,sys
want=sys.argv[1]
for a in json.load(sys.stdin):
if a.get("name")==want:
print(a.get("id",""));break' "$1" 2>/dev/null
}
_urlencode() { python3 -c 'import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1],safe=""))' "$1"; }
# ensure_release TAG NAME PRERELEASE [TARGET_COMMITISH]
# Idempotently create (or fetch) the release for TAG; prints its numeric id on stdout.
# PRERELEASE is "true", "false", or "auto" — auto marks it a prerelease iff TAG carries a
# `-` pre-release suffix (e.g. v0.2.0-rc1), so an rc never becomes the repo's "Latest" release
# (Gitea's /releases/latest surfaces the newest non-prerelease). TARGET_COMMITISH (optional)
# creates the git tag if it does not exist yet — for a `vX.Y.Z` release the tag already exists
# (it is the trigger), so TARGET is omitted and create-vs-fetch hinges on the release object.
ensure_release() {
local tag="${1:?tag}" name="${2:?name}" prerelease="${3:?prerelease}" target="${4:-}"
local api body id
if [ "$prerelease" = auto ]; then
case "$tag" in *-*) prerelease=true ;; *) prerelease=false ;; esac
fi
api="$(_gitea_api)"
if [ -n "$target" ]; then
body=$(printf '{"tag_name":"%s","name":"%s","prerelease":%s,"target_commitish":"%s"}' \
"$tag" "$name" "$prerelease" "$target")
else
body=$(printf '{"tag_name":"%s","name":"%s","prerelease":%s}' "$tag" "$name" "$prerelease")
fi
# Try to create. On any failure (almost always "release already exists"), fall back to
# fetching it by tag. Either path MUST yield an id, or we error loudly — so a 401/scope
# problem can't masquerade as a successful no-op.
id=$(curl -fsS -X POST "$api/releases" \
-H "Authorization: token ${GITEA_TOKEN:?}" -H 'Content-Type: application/json' \
-d "$body" 2>/dev/null | _json_id || true)
if [ -z "$id" ]; then
id=$(curl -fsS "$api/releases/tags/$tag" \
-H "Authorization: token ${GITEA_TOKEN:?}" 2>/dev/null | _json_id || true)
fi
if [ -z "$id" ]; then
echo "gitea-release: could not create or find a release for tag '$tag'" >&2
return 1
fi
printf '%s' "$id"
}
# upsert_asset RELEASE_ID FILE [NAME]
# Attach FILE to the release, replacing any existing asset of the same name first so that
# re-runs and rolling canary re-uploads are idempotent (a plain POST 409s on a dup name).
upsert_asset() {
local rid="${1:?release id}" file="${2:?file}" name="${3:-}"
local api existing
[ -n "$name" ] || name="$(basename "$file")"
[ -f "$file" ] || { echo "gitea-release: asset file not found: $file" >&2; return 1; }
api="$(_gitea_api)"
existing=$(curl -fsS "$api/releases/$rid/assets" \
-H "Authorization: token ${GITEA_TOKEN:?}" 2>/dev/null \
| _json_asset_id "$name" || true)
if [ -n "$existing" ]; then
curl -fsS -o /dev/null -X DELETE "$api/releases/$rid/assets/$existing" \
-H "Authorization: token ${GITEA_TOKEN:?}" || true
fi
curl -fsS -o /dev/null -X POST "$api/releases/$rid/assets?name=$(_urlencode "$name")" \
-H "Authorization: token ${GITEA_TOKEN:?}" \
-F "attachment=@$file"
echo "gitea-release: uploaded '$name' -> release $rid"
}