#!/usr/bin/env bash # # Publish the built desktop bundles as assets on a Fabled-Git (Forgejo) Release. # # WHY: CI Actions artifacts are ephemeral, per-run, and auth-gated — useless as a # distribution/fetch target. The install script (install.sh) and the in-app # updater both need a STABLE, versioned URL. A Release attached to the pushed # `v*` tag is that target: `/releases/latest` always points at the newest one, # and each asset has a permanent browser_download_url. # # WHEN: CI-only, and ONLY on a `v*` tag build (the workflow gates this step with # `if: startsWith(github.ref, 'refs/tags/v')`). Cutting the tag is the operator's # action (rule 2) — this script never creates a tag, it only publishes a Release # for a tag that already exists. # # The build + de-bundle steps run first; this consumes their output: # target/release/bundle/appimage/*.AppImage (de-bundled) # target/release/bundle/deb/*.deb # target/release/bundle/arch/*.pkg.tar.* (prebuilt pacman) # target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe # # Instance-agnostic: server + repo come from the runner's github.* context # (Forgejo populates them for compatibility), so nothing is hardcoded to one host. # # Idempotent: re-running for the same tag reuses the existing Release and # replaces same-named assets, so a re-run (or workflow_dispatch retry) is safe. set -euo pipefail : "${GITHUB_TOKEN:?GITHUB_TOKEN is required (runner-injected; needs contents:write)}" : "${GITHUB_SERVER_URL:?GITHUB_SERVER_URL is required}" : "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required (owner/repo)}" : "${GITHUB_REF_NAME:?GITHUB_REF_NAME is required (the tag, e.g. v0.1.0)}" # The release to publish to. Defaults to the pushed tag (the versioned, stable # case). M10.9 also calls this with RELEASE_TAG=dev to maintain the rolling # development channel — a release whose tag never moves, because Forgejo has no # `/releases/latest/download/` route for an updater to point at. RELEASE_TAG="${RELEASE_TAG:-$GITHUB_REF_NAME}" RELEASE_PRERELEASE="${RELEASE_PRERELEASE:-false}" API="$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY" TAG="$RELEASE_TAG" AUTH=(-H "Authorization: token $GITHUB_TOKEN") SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" BUNDLE_ROOT="$REPO_ROOT/target/release/bundle" # Cross-compiled Windows output lands under the target triple, not the host root. WIN_BUNDLE_ROOT="$REPO_ROOT/target/x86_64-pc-windows-msvc/release/bundle" # --- collect the assets to upload ------------------------------------------- shopt -s nullglob # nullglob (set above) drops the patterns that didn't match, which is what lets the # Linux job and the Windows job each run this script against the SAME release and # upload only what they actually built — they run in separate workspaces, so neither # can see the other's bundles. The release is created once and reused (409 path). # The `.sig` files are the updater's whole trust story — a bundle published without # its signature is one the app will refuse, so they ship together or not at all. # They only exist when the build ran with a signing key (M10.9); nullglob drops # them silently otherwise, which is the correct behaviour for an unsigned build. ASSETS=( "$BUNDLE_ROOT"/appimage/*.AppImage "$BUNDLE_ROOT"/appimage/*.AppImage.sig "$BUNDLE_ROOT"/deb/*.deb "$BUNDLE_ROOT"/arch/*.pkg.tar.* "$WIN_BUNDLE_ROOT"/nsis/*.exe "$WIN_BUNDLE_ROOT"/nsis/*.exe.sig ) if [ ${#ASSETS[@]} -eq 0 ]; then echo "ERROR: no bundles under $BUNDLE_ROOT — did the tauri build run?" >&2 exit 1 fi echo "==> Publishing release $TAG with ${#ASSETS[@]} asset(s):" for a in "${ASSETS[@]}"; do echo " $(basename "$a") ($(du -h "$a" | cut -f1))"; done # curl wrapper that returns the response body on stdout and fails the script on # an HTTP >=400 that we didn't explicitly allow (via ALLOW_CODES). api() { local method="$1" url="$2"; shift 2 local out code out="$(curl -sS -X "$method" "${AUTH[@]}" -w $'\n%{http_code}' "$url" "$@")" code="${out##*$'\n'}" out="${out%$'\n'*}" if [ "$code" -ge 400 ] && [[ " ${ALLOW_CODES:-} " != *" $code "* ]]; then echo "ERROR: $method $url -> HTTP $code" >&2 echo "$out" >&2 return 1 fi printf '%s' "$out" } # First integer value of a "id": pair — the release id is the first "id" in # the release object. Avoids a jq dependency (not guaranteed in the CI image). first_id() { grep -oE '"id"[[:space:]]*:[[:space:]]*[0-9]+' | head -1 | grep -oE '[0-9]+'; } # --- create (or reuse) the release for the tag ------------------------------ # The install command printed on a release has to install THAT release's channel. # install.sh defaults to stable, so the rolling dev release must opt in explicitly # — otherwise someone following the instructions here lands on a tagged build and # wonders why the version they were sent isn't what they got. if [ "$TAG" = "dev" ]; then INSTALL_TAIL='sh -s -- --channel dev' # Backticks BARE, not `\``. The heredoc below is unquoted, so there the backslash # is the shell's — it suppresses command substitution and never reaches the JSON. # Here single quotes already do that job, so a backslash would survive into the # body as `\``, which is not a legal JSON escape: Forgejo answers 422. CHANNEL_NOTE='\n\nThis is the rolling **dev** channel: republished on every green push to `dev`, and pruned to the current build.' else INSTALL_TAIL='sh' CHANNEL_NOTE='' fi echo "==> Creating release for $TAG" BODY=$(cat </dev/null fi [ -n "${RELEASE_ID:-}" ] || { echo "ERROR: could not resolve release id" >&2; exit 1; } echo " release id = $RELEASE_ID" # Existing assets (name -> id), so a re-run replaces rather than duplicates. existing="$(api GET "$API/releases/$RELEASE_ID/assets")" # --- upload each asset ------------------------------------------------------ for asset in "${ASSETS[@]}"; do name="$(basename "$asset")" # If an asset with this name already exists, delete it first (Forgejo rejects # a duplicate name). Match the "id" that precedes this asset's "name". old_id="$(printf '%s' "$existing" \ | grep -oE "\"id\"[[:space:]]*:[[:space:]]*[0-9]+[^}]*\"name\"[[:space:]]*:[[:space:]]*\"$name\"" \ | first_id || true)" if [ -n "${old_id:-}" ]; then echo "==> Replacing existing asset $name (id $old_id)" api DELETE "$API/releases/$RELEASE_ID/assets/$old_id" >/dev/null fi echo "==> Uploading $name" api POST "$API/releases/$RELEASE_ID/assets?name=$name" -F "attachment=@$asset" >/dev/null done echo "==> Done. Release $TAG published with $(printf '%s\n' "${ASSETS[@]##*/}" | tr '\n' ' ')"