#!/bin/sh # publish-codeberg-release.sh — upload assets to a Codeberg release. # # Usage: # publish_codeberg_release ... # # Where: # e.g. VARASYS/ZDDC # e.g. zddc-server-v0.0.8-alpha.3 or archive-v0.0.3 # one or more files to attach to the release # # Prerequisites: # - $CODEBERG_TOKEN exported in the environment, with scope sufficient # to create/update releases on the target repo. (Codeberg/Gitea # terminology: "Application token with `write:repository` access".) # - curl, jq. # # Behavior: # - If a release for $tag does not exist on Codeberg, create it. The # prerelease flag is derived from the tag itself: if the version # part (text after the last 'v') contains a '-', it is a pre-release # (e.g. 'zddc-server-v0.0.8-alpha.2' → prerelease=true); # 'zddc-server-v0.0.7' → prerelease=false. Codeberg orders releases # by published-at and sets a "Latest" badge against the latest # non-prerelease, so this matters. # - For each asset: if a same-named asset already exists, delete it # first (Codeberg/Gitea API doesn't support in-place replacement). # Then upload the new bytes. # # Idempotent: re-running with the same args leaves the release with the # same set of assets. # # This file is meant to be sourced and invoked via the function name, but # it's also runnable directly as a script for quick testing — when run # directly (i.e., $0 ends in publish-codeberg-release.sh), the function # is called with the script's argv. # # NOTE: We do NOT `set -eu` at the top, because that would leak into any # caller that sources this file. The direct-run dispatch at the bottom # turns -eu on for that path only. CODEBERG_API="${CODEBERG_API:-https://codeberg.org/api/v1}" # True iff $1's "version part" (text after last 'v') contains '-'. # Tags with a '-' in the version part are pre-releases per the # pre-release-semver scheme (see AGENTS.md "Releasing"). _is_prerelease() { _ver="${1##*v}" case "$_ver" in *-*) return 0 ;; *) return 1 ;; esac } # Fetch a release by tag. Echoes the numeric release ID, or empty on 404. # Suppresses 404-on-stderr; other errors propagate. _get_release_id() { _repo="$1" _tag="$2" _resp=$(curl -fsS -H "Authorization: token $CODEBERG_TOKEN" \ "$CODEBERG_API/repos/$_repo/releases/tags/$_tag" 2>/dev/null) || _resp="" [ -z "$_resp" ] && return 0 printf '%s' "$_resp" | jq -r '.id // empty' } # Create a release for the given tag. Echoes the new release ID. Bombs # out on any error (the caller relies on stable behavior — releases # don't get half-created). _create_release() { _repo="$1" _tag="$2" if _is_prerelease "$_tag"; then _prerelease=true else _prerelease=false fi # Inline JSON; tag/name don't contain quotes per our naming rules. _body=$(printf '{"tag_name":"%s","name":"%s","prerelease":%s,"draft":false}' \ "$_tag" "$_tag" "$_prerelease") curl -fsS \ -X POST \ -H "Authorization: token $CODEBERG_TOKEN" \ -H "Content-Type: application/json" \ -d "$_body" \ "$CODEBERG_API/repos/$_repo/releases" \ | jq -r '.id' } # Echo the asset ID for an asset of the given filename in the given # release, or empty if no such asset. _find_asset_id() { _repo="$1" _release_id="$2" _name="$3" curl -fsS -H "Authorization: token $CODEBERG_TOKEN" \ "$CODEBERG_API/repos/$_repo/releases/$_release_id" \ | jq -r --arg n "$_name" '.assets[] | select(.name == $n) | .id' \ | head -1 } _delete_asset() { _repo="$1" _asset_id="$2" curl -fsS -X DELETE \ -H "Authorization: token $CODEBERG_TOKEN" \ "$CODEBERG_API/repos/$_repo/releases/assets/$_asset_id" >/dev/null } _upload_asset() { _repo="$1" _release_id="$2" _asset_path="$3" _name=$(basename "$_asset_path") # Codeberg/Gitea expects the file under field name "attachment", and # the desired display name as the ?name= query parameter (otherwise # the original filename is used; we set both for clarity). curl -fsS -X POST \ -H "Authorization: token $CODEBERG_TOKEN" \ -F "attachment=@${_asset_path}" \ "$CODEBERG_API/repos/$_repo/releases/$_release_id/assets?name=$(printf '%s' "$_name" | jq -sRr @uri)" \ >/dev/null } publish_codeberg_release() { if [ $# -lt 3 ]; then echo "usage: publish_codeberg_release ..." >&2 return 2 fi if [ -z "${CODEBERG_TOKEN:-}" ]; then echo "publish_codeberg_release: CODEBERG_TOKEN not set" >&2 return 2 fi _repo="$1" _tag="$2" shift 2 _release_id=$(_get_release_id "$_repo" "$_tag") if [ -z "$_release_id" ]; then echo " creating release for $_tag" _release_id=$(_create_release "$_repo" "$_tag") if [ -z "$_release_id" ] || [ "$_release_id" = "null" ]; then echo "publish_codeberg_release: failed to create release for $_tag" >&2 return 1 fi fi echo " release id: $_release_id" for _asset_path do if [ ! -f "$_asset_path" ]; then echo "publish_codeberg_release: asset not readable: $_asset_path" >&2 return 1 fi _name=$(basename "$_asset_path") _existing=$(_find_asset_id "$_repo" "$_release_id" "$_name") if [ -n "$_existing" ]; then echo " replacing existing asset $_name (id $_existing)" _delete_asset "$_repo" "$_existing" fi echo " uploading $_name" _upload_asset "$_repo" "$_release_id" "$_asset_path" done } # When invoked directly (not sourced), call the function with argv. case "${0##*/}" in publish-codeberg-release.sh) set -eu publish_codeberg_release "$@" ;; esac