Files
mail-client/scripts/release.sh
T
KanjiG 4b2248655b Add auto-update support (tauri-plugin-updater) and Gitea release pipeline
Ed25519-signed updates, checked via a fixed-tag latest.json manifest on
Gitea Releases; scripts/release.sh automates build+sign+publish. Adds
a manual "Nach Updates suchen" trigger plus a silent startup check.
2026-07-28 21:38:49 +02:00

174 lines
7.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Builds, signs, and publishes a release to Gitea (git.kanjiv.at/KanjiG/mail-client).
#
# Usage: scripts/release.sh <version> [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 <version> [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"