#!/usr/bin/env bash # Builds, signs, and publishes a release to Gitea (git.kanjiv.at/KanjiG/mail-client). # # Usage: scripts/release.sh [notes] # scripts/release.sh 0.2.0 "Fixed X, added Y" # # Requires: # - TAURI_SIGNING_PRIVATE_KEY_PATH pointing at the updater signing key # (default: ~/.tauri/mail-client-update.key) # - GITEA_TOKEN or GITEA_PASSWORD env var for authenticating to the Gitea API # (GITEA_TOKEN preferred — a Gitea access token instead of the account password) # # Only builds for the platform it's run on. To publish Windows/macOS builds, # run this same script on/from those platforms with the same version — it # appends to the same release rather than overwriting it, and regenerates # latest.json to include whichever platform artifacts are present so far. set -euo pipefail VERSION="${1:?Usage: scripts/release.sh [notes]}" NOTES="${2:-Release $VERSION}" REPO_OWNER="KanjiG" REPO_NAME="mail-client" GITEA_URL="https://git.kanjiv.at" TAG="v$VERSION" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" cd "$PROJECT_ROOT" export TAURI_SIGNING_PRIVATE_KEY_PATH="${TAURI_SIGNING_PRIVATE_KEY_PATH:-$HOME/.tauri/mail-client-update.key}" export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-}" if [ -z "${GITEA_TOKEN:-}" ] && [ -z "${GITEA_PASSWORD:-}" ]; then echo "Set GITEA_TOKEN (preferred) or GITEA_PASSWORD before running this." >&2 exit 1 fi AUTH_HEADER=() if [ -n "${GITEA_TOKEN:-}" ]; then AUTH_HEADER=(-H "Authorization: token $GITEA_TOKEN") else AUTH_HEADER=(-u "$REPO_OWNER:$GITEA_PASSWORD") fi echo "==> Syncing version to $VERSION in package.json / tauri.conf.json" node -e " const fs = require('fs'); for (const f of ['package.json', 'src-tauri/tauri.conf.json']) { const j = JSON.parse(fs.readFileSync(f, 'utf8')); j.version = '$VERSION'; fs.writeFileSync(f, JSON.stringify(j, null, 2) + '\n'); } " sed -i "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml echo "==> Building frontend" npm run build echo "==> Building + signing Tauri bundle" npx tauri build BUNDLE_DIR="src-tauri/target/release/bundle" # Figure out which platform we're on and where the updater-relevant artifact + # signature landed. Tauri's updater only consumes specific bundle formats per # platform (AppImage on Linux, NSIS on Windows, .app.tar.gz on macOS) — not # .deb/.rpm, those are for manual installs only. UNAME="$(uname -s)" PLATFORM_KEY="" ARTIFACT="" case "$UNAME" in Linux) PLATFORM_KEY="linux-x86_64" ARTIFACT="$(find "$BUNDLE_DIR/appimage" -name '*.AppImage' | head -1)" ;; Darwin) ARCH="$(uname -m)" PLATFORM_KEY="darwin-$([ "$ARCH" = "arm64" ] && echo aarch64 || echo x86_64)" ARTIFACT="$(find "$BUNDLE_DIR/macos" -name '*.app.tar.gz' | head -1)" ;; MINGW*|MSYS*|CYGWIN*) PLATFORM_KEY="windows-x86_64" ARTIFACT="$(find "$BUNDLE_DIR/nsis" -name '*-setup.exe' | head -1)" ;; *) echo "Unrecognized platform: $UNAME" >&2 exit 1 ;; esac if [ -z "$ARTIFACT" ] || [ ! -f "$ARTIFACT" ]; then echo "Could not find the updater artifact for $PLATFORM_KEY under $BUNDLE_DIR" >&2 exit 1 fi SIGNATURE="$(cat "$ARTIFACT.sig")" FILENAME="$(basename "$ARTIFACT")" echo "==> Ensuring release $TAG exists on Gitea" RELEASE_JSON="$(curl -s "${AUTH_HEADER[@]}" "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases/tags/$TAG")" RELEASE_ID="$(echo "$RELEASE_JSON" | node -pe 'JSON.parse(require("fs").readFileSync(0)).id' 2>/dev/null || echo "")" if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "undefined" ]; then RELEASE_JSON="$(curl -s "${AUTH_HEADER[@]}" -X POST "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases" \ -H "Content-Type: application/json" \ -d "$(node -pe "JSON.stringify({tag_name: '$TAG', name: '$TAG', body: process.argv[1]})" "$NOTES")") RELEASE_ID="$(echo "$RELEASE_JSON" | node -pe 'JSON.parse(require("fs").readFileSync(0)).id')" fi echo " release id: $RELEASE_ID" echo "==> Uploading $FILENAME" curl -s "${AUTH_HEADER[@]}" -X POST \ "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases/$RELEASE_ID/assets?name=$FILENAME" \ -F "attachment=@$ARTIFACT" > /dev/null # latest.json lives on a SEPARATE, fixed-tag release ("latest-manifest"), # independent of the versioned release tags above. The updater's endpoint in # tauri.conf.json points at this fixed tag, not at "latest" — Gitea's support # for a GitHub-style /releases/latest/download/ alias varies by version, so a # fixed tag we fully control is more robust than depending on that. MANIFEST_TAG="latest-manifest" echo "==> Ensuring $MANIFEST_TAG release exists" MANIFEST_RELEASE_JSON="$(curl -s "${AUTH_HEADER[@]}" "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases/tags/$MANIFEST_TAG")" MANIFEST_RELEASE_ID="$(echo "$MANIFEST_RELEASE_JSON" | node -pe 'JSON.parse(require("fs").readFileSync(0)).id' 2>/dev/null || echo "")" if [ -z "$MANIFEST_RELEASE_ID" ] || [ "$MANIFEST_RELEASE_ID" = "undefined" ]; then MANIFEST_RELEASE_JSON="$(curl -s "${AUTH_HEADER[@]}" -X POST "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases" \ -H "Content-Type: application/json" \ -d "$(node -pe "JSON.stringify({tag_name: process.argv[1], name: 'Update manifest (do not download directly)', body: 'Holds latest.json for the in-app updater. Always points at the newest release.'})" "$MANIFEST_TAG")") MANIFEST_RELEASE_ID="$(echo "$MANIFEST_RELEASE_JSON" | node -pe 'JSON.parse(require("fs").readFileSync(0)).id')" fi echo "==> Fetching existing latest.json (if any) to merge platforms" EXISTING_LATEST="$(curl -sf "$GITEA_URL/$REPO_OWNER/$REPO_NAME/releases/download/$MANIFEST_TAG/latest.json" || echo '{}')" TMP_LATEST="$(mktemp)" node -e " const fs = require('fs'); let existing = {}; try { existing = JSON.parse(\`$EXISTING_LATEST\`); } catch { existing = {}; } const platforms = existing.platforms || {}; platforms['$PLATFORM_KEY'] = { signature: \`$SIGNATURE\`, url: '$GITEA_URL/$REPO_OWNER/$REPO_NAME/releases/download/$TAG/$FILENAME', }; const manifest = { version: '$VERSION', notes: $(node -pe "JSON.stringify(process.argv[1])" "$NOTES"), pub_date: new Date().toISOString(), platforms, }; fs.writeFileSync('$TMP_LATEST', JSON.stringify(manifest, null, 2)); " # Remove any previous latest.json asset before re-uploading (Gitea won't # overwrite an existing asset with the same name). EXISTING_ASSET_ID="$(curl -s "${AUTH_HEADER[@]}" "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases/$MANIFEST_RELEASE_ID" \ | node -pe " const rel = JSON.parse(require('fs').readFileSync(0)); const a = (rel.assets || []).find(x => x.name === 'latest.json'); a ? a.id : '' " 2>/dev/null || echo "")" if [ -n "$EXISTING_ASSET_ID" ] && [ "$EXISTING_ASSET_ID" != "undefined" ]; then curl -s "${AUTH_HEADER[@]}" -X DELETE \ "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases/$MANIFEST_RELEASE_ID/assets/$EXISTING_ASSET_ID" > /dev/null fi echo "==> Uploading latest.json to $MANIFEST_TAG" curl -s "${AUTH_HEADER[@]}" -X POST \ "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases/$MANIFEST_RELEASE_ID/assets?name=latest.json" \ -F "attachment=@$TMP_LATEST" > /dev/null rm -f "$TMP_LATEST" echo "==> Done." echo " Release: $GITEA_URL/$REPO_OWNER/$REPO_NAME/releases/tag/$TAG" echo " Manifest URL: $GITEA_URL/$REPO_OWNER/$REPO_NAME/releases/download/$MANIFEST_TAG/latest.json"