CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 6s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python tests (push) Successful in 10s
CI & Build / integration (push) Successful in 44s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 3m20s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m20s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 9m13s
The rolling dev release lived on a tag named `dev`, beside the branch named `dev`. Once a clone had fetched tags, `git push origin dev` failed with "src refspec dev matches more than one" (Scribe #2184, note #3042), and every session had to know to spell out refs/heads/dev. The channel is still `dev` everywhere a person sees it: the app's setting, `install.sh --channel dev`, the stored pref. Only the release tag moves, to `dev-rolling`, matching roundtable-android. `stable` has no branch to collide with and keeps its name. - packaging/channel-tag.sh is the one channel -> tag mapping CI reads: the publish steps in android.yml and desktop.yml, the manifest job, fetch-clients.sh and guard-forward.sh. guard-forward exits 2 on an unmapped channel instead of fetching an empty URL and passing. - update.rs and install.sh carry their own copy because neither can run it; update.rs gains a test that no channel feed is named like a branch. - tests/test_channel_tag.py runs the script: no tag is a branch name, dev is exactly dev-rolling, an unknown channel fails with no output. - publish-release.sh titles the release "ThoughtSync dev (rolling)", so the tag name does not leak into what people read. TEMPORARY bridge: desktop apps installed before this have .../download/dev/latest.json compiled in. The dev manifest job sets BRIDGE_TAG=dev, and write-manifest.sh writes the same latest.json to the old `dev` release. Its URLs name dev-rolling assets, so those apps update once into a build that reads the new tag. The bridge, and the old release and tag, are removed once installed apps have crossed over. Until then the push still needs the explicit refspec, as ci-requirements.md now says. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DwoKYuw3qJmUUYsJeNherB
264 lines
12 KiB
Bash
Executable File
264 lines
12 KiB
Bash
Executable File
#!/bin/sh
|
|
#
|
|
# ThoughtSync desktop — one-command Linux installer.
|
|
#
|
|
# curl -fsSL https://git.fabledsword.com/bvandeusen/thoughtsync/raw/branch/dev/desktop/packaging/install.sh | sh
|
|
#
|
|
# Two channels, the SAME two the app's own updater offers (src-tauri/src/update.rs):
|
|
# stable (default) — the rolling build from every merge to `main`.
|
|
# dev — the rolling build from every green push to `dev`.
|
|
# Both are fixed-tag releases: the tag never moves and the assets are pruned to the
|
|
# current build, so the tag alone names the newest one. `stable` only became one in
|
|
# M314 step 3, when `main` started publishing — before that it was a manifest-only
|
|
# pointer at whatever `v*` tag somebody had last cut, and this script carried a
|
|
# fallback that chased the `v*` release its manifest named. That came out once
|
|
# `main` had published to `stable` for real (`b6673c6`); the two channels are the
|
|
# same shape now and nothing here should special-case one of them again.
|
|
# Pick one with `--channel dev` or `TS_CHANNEL=dev`. Through a pipe the options go
|
|
# after a `--`: curl -fsSL <url> | sh -s -- --channel dev
|
|
#
|
|
# Served from `dev` rather than `main`: `main` exists but trails day-to-day work by
|
|
# a long way, so the copy there would install an older script. Move the documented
|
|
# URL to `main` after a dev→main merge lands, not before.
|
|
#
|
|
# Fetches the LATEST published release on the chosen channel for this machine's
|
|
# architecture and installs it, ending with a working app + menu entry. Native-first:
|
|
# * Arch/CachyOS (pacman) -> the native .pkg.tar.zst (system libs; needs sudo).
|
|
# * Debian/Ubuntu (dpkg+apt) -> the native .deb (system libs; needs sudo).
|
|
# * everything else (Fedora/openSUSE/…) -> the de-bundled AppImage,
|
|
# installed user-locally (no sudo). The AppImage's graphics libs are
|
|
# stripped in CI (see debundle-graphics.sh), so it uses the host GPU stack
|
|
# and renders where a stock Tauri AppImage would black-window (issue 2021).
|
|
#
|
|
# POSIX sh (dash-safe) so `curl … | sh` works everywhere. Dependency-light and
|
|
# auditable on purpose — read it before you pipe it.
|
|
set -eu
|
|
|
|
INSTANCE="https://git.fabledsword.com"
|
|
REPO="bvandeusen/thoughtsync"
|
|
API="$INSTANCE/api/v1/repos/$REPO"
|
|
|
|
say() { printf '==> %s\n' "$1"; }
|
|
die() { printf 'error: %s\n' "$1" >&2; exit 1; }
|
|
have() { command -v "$1" >/dev/null 2>&1; }
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
ThoughtSync desktop installer.
|
|
|
|
install.sh [--channel stable|dev]
|
|
|
|
--channel stable newest build from main (default)
|
|
--channel dev rolling build from the latest green push to `dev`
|
|
-h, --help this text
|
|
|
|
The channel can also come from TS_CHANNEL. Through a pipe, pass options after
|
|
`--`: curl -fsSL <url> | sh -s -- --channel dev
|
|
USAGE
|
|
}
|
|
|
|
# --- channel ----------------------------------------------------------------
|
|
channel="${TS_CHANNEL:-stable}"
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--channel)
|
|
[ $# -ge 2 ] || die "--channel needs a value (stable or dev)."
|
|
channel="$2"; shift 2 ;;
|
|
--channel=*) channel="${1#*=}"; shift ;;
|
|
-h | --help) usage; exit 0 ;;
|
|
*) die "unknown option: $1 (try --help)" ;;
|
|
esac
|
|
done
|
|
case "$channel" in
|
|
stable | dev) : ;;
|
|
*) die "unknown channel '$channel' — expected stable or dev." ;;
|
|
esac
|
|
|
|
have curl || die "curl is required."
|
|
|
|
# --- architecture gate ------------------------------------------------------
|
|
# Only x86_64 is built today; arm64 will be added when the CI matrix grows. The
|
|
# release-asset naming carries the arch, but since only one arch ships now we
|
|
# match by file extension below and just guard the arch here.
|
|
arch="$(uname -m)"
|
|
case "$arch" in
|
|
x86_64 | amd64) : ;;
|
|
*) die "ThoughtSync ships x86_64 Linux builds only right now (this machine: $arch)." ;;
|
|
esac
|
|
|
|
# --- resolve the release for this channel -----------------------------------
|
|
say "Finding the latest ThoughtSync build on the $channel channel…"
|
|
|
|
# ONE lookup, both channels. Each is a release whose tag never moves and whose assets
|
|
# are pruned to the current build, so the tag alone names the newest build on that
|
|
# channel — which is exactly what an installer wants and what the in-app updater
|
|
# already reads.
|
|
# The channel's RELEASE TAG, which is not always its name: `dev` publishes on the
|
|
# tag `dev-rolling`, because a tag called `dev` shadowed the branch of the same name
|
|
# and broke `git push origin dev` (Scribe #2184). Same mapping as
|
|
# packaging/channel-tag.sh — inlined because this script is fetched alone through a
|
|
# pipe and has nothing to source.
|
|
case "$channel" in
|
|
dev) tag=dev-rolling ;;
|
|
*) tag="$channel" ;;
|
|
esac
|
|
json="$(curl -fsSL "$API/releases/tags/$tag" 2>/dev/null)" ||
|
|
die "the $channel channel has nothing published yet."
|
|
|
|
# Pull asset URLs straight out of the release JSON (no jq). Anchored on the closing
|
|
# quote so a `…AppImage.sig` URL can't be truncated into a match of its own.
|
|
asset_url() {
|
|
printf '%s' "$json" | grep -oE "https?://[^\"]+$1\"" | head -1 | tr -d '"'
|
|
}
|
|
appimage_url="$(asset_url '\.AppImage')"
|
|
deb_url="$(asset_url '\.deb')"
|
|
pkg_url="$(asset_url '\.pkg\.tar\.[a-z]+')"
|
|
version="$(printf '%s' "$json" | grep -oE '"tag_name":"[^"]+"' | head -1 | sed -E 's/.*:"([^"]+)".*/\1/')"
|
|
[ -n "$appimage_url" ] || [ -n "$deb_url" ] || [ -n "$pkg_url" ] ||
|
|
die "the $channel release (${version:-unknown}) has no installable Linux asset."
|
|
say "Installing ${version:-unknown} from the $channel channel"
|
|
|
|
tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp"' EXIT INT TERM
|
|
|
|
# Tell the app which channel it was installed from. The installer is the only thing
|
|
# that knows, and without this the app kept its own `stable` default and a dev install
|
|
# checked the stable feed — which advertises an OLDER version — reporting "up to date"
|
|
# forever (issue 2183).
|
|
#
|
|
# A plain file rather than a write into the app's SQLite store: shell has no business
|
|
# knowing that schema, and a file it can't misread is the narrowest possible contract.
|
|
# The app reads it at startup (src-tauri/src/update.rs, INSTALL_MARKER) and only acts
|
|
# when the value CHANGED, so switching channel in the app isn't undone on next launch.
|
|
#
|
|
# The directory is Tauri's app-data dir for identifier com.fabledsword.thoughtsync;
|
|
# both sides hardcode it, so a change to the identifier has to change both.
|
|
record_channel() {
|
|
marker_dir="${XDG_DATA_HOME:-$HOME/.local/share}/com.fabledsword.thoughtsync"
|
|
# Best-effort: a failure here costs the channel setting, not the install, and a
|
|
# native install run as root would only be writing into root's home anyway.
|
|
mkdir -p "$marker_dir" 2>/dev/null && printf '%s\n' "$channel" > "$marker_dir/install-channel" 2>/dev/null || true
|
|
}
|
|
|
|
# Both native paths install system-wide, so they need root. Resolved once here
|
|
# rather than duplicated per branch; the AppImage path below never calls this.
|
|
need_root() {
|
|
if [ "$(id -u)" -eq 0 ]; then sudo=""; else
|
|
have sudo || die "a native install needs root; re-run as root or install sudo."
|
|
sudo="sudo"
|
|
fi
|
|
say "Installing (you may be prompted for your password)…"
|
|
}
|
|
|
|
# Both native paths are package-manager-owned, so the app cannot replace itself
|
|
# in place (update.rs refuses, by design). Say so at the end of those paths rather
|
|
# than letting someone discover it from a greyed-out button.
|
|
native_update_note() {
|
|
printf ' A package-manager install can'\''t update itself in-app.\n'
|
|
if [ "$channel" = "dev" ]; then
|
|
printf ' Re-run this script with --channel dev to move to a newer dev build.\n'
|
|
else
|
|
printf ' Re-run this script to move to a newer release.\n'
|
|
fi
|
|
}
|
|
|
|
# --- native pacman path (Arch/CachyOS/Manjaro) ------------------------------
|
|
# Preferred over the AppImage on Arch: pacman pulls webkit2gtk-4.1 itself and the
|
|
# app then runs against the host graphics stack, which is what keeps the
|
|
# EGL_BAD_PARAMETER black window (issue 2021) from coming back. It also means the
|
|
# app is tracked by the package manager and uninstalls cleanly.
|
|
if have pacman && [ -n "$pkg_url" ]; then
|
|
say "Arch-family system detected — installing the native pacman package"
|
|
# Keep the published filename: pacman -U expects a *.pkg.tar.* name and refuses
|
|
# a file that doesn't look like a package, whatever its actual contents.
|
|
pkg_file="$tmp/$(basename "$pkg_url")"
|
|
curl -fSL -o "$pkg_file" "$pkg_url"
|
|
need_root
|
|
$sudo pacman -U --noconfirm "$pkg_file"
|
|
record_channel
|
|
say "Done. Launch ThoughtSync from your application menu, or run thoughtsync."
|
|
native_update_note
|
|
exit 0
|
|
fi
|
|
|
|
# --- native .deb path (Debian/Ubuntu) ---------------------------------------
|
|
if have dpkg && have apt-get && [ -n "$deb_url" ]; then
|
|
say "Debian-family system detected — installing the native .deb"
|
|
curl -fSL -o "$tmp/thoughtsync.deb" "$deb_url"
|
|
need_root
|
|
# apt-get resolves the .deb's dependencies (webkit2gtk etc.). dpkg is the
|
|
# fallback if this apt is too old for local-file installs — it leaves the deps
|
|
# unconfigured, so `apt-get -f install` is what actually completes that path.
|
|
$sudo apt-get install -y "$tmp/thoughtsync.deb" ||
|
|
{ $sudo dpkg -i "$tmp/thoughtsync.deb" || true; $sudo apt-get -f install -y; }
|
|
record_channel
|
|
say "Done. Launch ThoughtSync from your application menu."
|
|
native_update_note
|
|
exit 0
|
|
fi
|
|
|
|
# --- universal AppImage path (user-local, no sudo) --------------------------
|
|
# Install into ~/Applications/ThoughtSync.AppImage — the SAME location the app's
|
|
# own self-integration uses (src/integration.rs) — so the running app sees
|
|
# itself already installed and never makes a second copy or menu entry.
|
|
say "Installing the de-bundled AppImage (user-local, no sudo)"
|
|
[ -n "$appimage_url" ] || die "the $channel release (${version:-unknown}) has no AppImage asset."
|
|
|
|
apps_dir="$HOME/Applications"
|
|
dest="$apps_dir/ThoughtSync.AppImage"
|
|
mkdir -p "$apps_dir"
|
|
say "Downloading $(basename "$appimage_url")…"
|
|
curl -fSL -o "$tmp/ThoughtSync.AppImage" "$appimage_url"
|
|
chmod +x "$tmp/ThoughtSync.AppImage"
|
|
mv -f "$tmp/ThoughtSync.AppImage" "$dest"
|
|
|
|
# Menu entry — written to match integration.rs verbatim (same paths + fields),
|
|
# so the app reports is_integrated=true and won't duplicate it.
|
|
apps_menu="$HOME/.local/share/applications"
|
|
icons_dir="$HOME/.local/share/icons"
|
|
mkdir -p "$apps_menu" "$icons_dir"
|
|
|
|
# Best-effort: pull the real icon out of the AppImage (.DirIcon) so the menu
|
|
# entry looks right immediately. Extraction is a non-GUI unsquash (no FUSE, no
|
|
# black-window risk); if it fails we fall back to the themed name and the app
|
|
# writes its embedded icon on first launch anyway.
|
|
icon_ref="thoughtsync"
|
|
if ( cd "$tmp" && "$dest" --appimage-extract .DirIcon >/dev/null 2>&1 ) \
|
|
&& cp -L "$tmp/squashfs-root/.DirIcon" "$icons_dir/thoughtsync.png" 2>/dev/null; then
|
|
icon_ref="$icons_dir/thoughtsync.png"
|
|
fi
|
|
rm -rf "$tmp/squashfs-root" 2>/dev/null || true
|
|
|
|
# StartupWMClass is the BINARY name, not the product name and not the AppImage
|
|
# filename: the AppImage's AppRun execs usr/bin/thoughtsync, and GTK derives
|
|
# WM_CLASS from whatever it ends up running. Anything else here means the window
|
|
# never associates with this entry and the taskbar shows a second, generic icon.
|
|
cat > "$apps_menu/thoughtsync.desktop" <<EOF
|
|
[Desktop Entry]
|
|
Type=Application
|
|
Name=ThoughtSync
|
|
Comment=Capture a fleeting thought in a second
|
|
Exec=$dest %U
|
|
Icon=$icon_ref
|
|
Terminal=false
|
|
Categories=Utility;Office;
|
|
StartupWMClass=thoughtsync
|
|
EOF
|
|
|
|
have update-desktop-database && update-desktop-database "$apps_menu" >/dev/null 2>&1 || true
|
|
|
|
# Convenience CLI launcher.
|
|
mkdir -p "$HOME/.local/bin"
|
|
ln -sf "$dest" "$HOME/.local/bin/thoughtsync"
|
|
|
|
record_channel
|
|
|
|
say "Installed to $dest"
|
|
printf ' Launch it from your application menu, or run \033[1mthoughtsync\033[0m'
|
|
printf ' (if ~/.local/bin is on your PATH).\n'
|
|
# This is the one path where the app can update itself, so say what it will follow.
|
|
if [ "$channel" = "dev" ]; then
|
|
printf ' In-app updates will follow the \033[1mdev\033[0m channel.'
|
|
printf ' Change it in Sync → App updates.\n'
|
|
fi
|