Files
thoughtsync/desktop/packaging/publish-release.sh
T
bvandeusenandClaude Opus 5 ab961f13ce
Desktop (Tauri) / Windows installer (cross-compiled) (push) Failing after 48s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m40s
desktop: cross-compiled Windows NSIS installer lane (task 2015)
Adds a `windows` job to desktop.yml on the new ci-tauri-win image, producing a
Windows -setup.exe without any Windows hardware. A Windows container can't run
on a Linux host, so cross-compilation is the only route: --runner cargo-xwin
supplies the MSVC CRT/SDK (pre-warmed into the image) and links with lld-link,
and makensis builds the installer.

NSIS only. .msi needs WiX v3, a Windows program — per Tauri, ".msi installers
can only be created on Windows". It comes back if a Windows node ever exists.

Kept as a separate job so a Windows-side failure can never block the Linux
artifacts, which are the primary product today. publish-release.sh now globs
the windows target root too; nullglob means each job uploads only what its own
workspace contains, and the release is created once and reused via the 409
path, so both jobs can publish to the same release safely.

No app code changes were needed. The AppImage self-integration UI already
gates on is_appimage (AccountView.vue:131, DesktopIntegrationPrompt.vue:22),
and $APPIMAGE is never set on Windows, so the OOBE prompt and Settings toggle
hide themselves.

Recorded plainly in ci-requirements.md that this is the weakest-verified lane
we have: Tauri calls Linux->Windows cross-compilation "not tested as much" and
a last resort, and a Linux runner cannot execute a Windows binary. Green means
it built. A real Windows machine check is mandatory before trusting a release,
and installers are unsigned until a certificate exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi
2026-07-25 20:14:55 -04:00

122 lines
5.9 KiB
Bash
Executable File

#!/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:
# desktop/src-tauri/target/release/bundle/appimage/*.AppImage (de-bundled)
# desktop/src-tauri/target/release/bundle/deb/*.deb
# desktop/src-tauri/target/release/bundle/arch/*.pkg.tar.* (prebuilt pacman)
# desktop/src-tauri/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)}"
API="$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY"
TAG="$GITHUB_REF_NAME"
AUTH=(-H "Authorization: token $GITHUB_TOKEN")
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
BUNDLE_ROOT="$REPO_ROOT/desktop/src-tauri/target/release/bundle"
# Cross-compiled Windows output lands under the target triple, not the host root.
WIN_BUNDLE_ROOT="$REPO_ROOT/desktop/src-tauri/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).
ASSETS=(
"$BUNDLE_ROOT"/appimage/*.AppImage
"$BUNDLE_ROOT"/deb/*.deb
"$BUNDLE_ROOT"/arch/*.pkg.tar.*
"$WIN_BUNDLE_ROOT"/nsis/*.exe
)
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": <n> 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 ------------------------------
echo "==> Creating release for $TAG"
BODY=$(cat <<JSON
{"tag_name":"$TAG","name":"ThoughtSync $TAG","draft":false,"prerelease":false,
"body":"ThoughtSync desktop $TAG.\n\n- **Debian / Ubuntu** — native \`.deb\`\n- **Arch / CachyOS** — native \`.pkg.tar.*\`\n- **everything else** — \`.AppImage\` (de-bundled graphics: renders on any GPU/Wayland setup)\n\nInstall / update — picks the right one for your system:\n\`\`\`\ncurl -fsSL $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/raw/branch/dev/desktop/packaging/install.sh | sh\n\`\`\`"}
JSON
)
# 409 = a release for this tag already exists (re-run) — fall through to lookup.
release="$(ALLOW_CODES=409 api POST "$API/releases" -H "Content-Type: application/json" -d "$BODY")"
RELEASE_ID="$(printf '%s' "$release" | first_id || true)"
if [ -z "${RELEASE_ID:-}" ]; then
echo " release exists; fetching it by tag"
release="$(api GET "$API/releases/tags/$TAG")"
RELEASE_ID="$(printf '%s' "$release" | first_id)"
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' ' ')"