Step 4 of milestone 481 (Scribe note 5071: a full rename). - namespace and applicationId com.fabledsword.inkwell; the Kotlin package moves with them, and ktlint re-sorted the imports the rename reordered (checked locally with CI's ktlint 1.4.0 and detekt 1.23.7, both clean) - uniffi: class Inkwell in com.fabledsword.inkwell.core, InkwellApplication, InkwellTheme, Theme.Inkwell, log tags, prefs and work names, client agent inkwell-android - the lane publishes inkwell.apk / inkwell-android.json; fetch-clients, guard-forward, publish-release and write-manifest read the same names A new applicationId is a new app. The old ThoughtSync app keeps its own store and stays installed beside it. Notes cross over by syncing, and the old app is then removed by hand. Kept: the signing keyAlias is still "thoughtsync". It names the key inside the existing keystore, and the key, and so the certificate, are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
170 lines
7.4 KiB
Bash
Executable File
170 lines
7.4 KiB
Bash
Executable File
#!/usr/bin/env sh
|
|
#
|
|
# Collect every client this image should hand out, into one directory.
|
|
#
|
|
# fetch-clients.sh <dev|stable> <destdir>
|
|
#
|
|
# The server serves clients from `DATA_DIR/client/` or from the copy baked into the
|
|
# image (`client_dist.py`). This is what fills the second one. It runs in CI, right
|
|
# before `docker build`, and writes the FIXED filenames that module looks for.
|
|
#
|
|
# THE CHANNEL IS A PROPERTY OF THE IMAGE. A `:dev` image serves dev clients;
|
|
# `:latest` serves stable ones. Passed in rather than derived here, because the
|
|
# caller is the thing that knows which image it is building.
|
|
#
|
|
# NEVER FAILS. A platform with nothing published means the server advertises
|
|
# nothing for it and the UI hides that download — a supported state, and the only
|
|
# one available before a platform's first build has ever published. Turning eight
|
|
# fetches into eight ways to redden an otherwise fine lane would be strictly worse
|
|
# than shipping an image that offers four clients instead of five.
|
|
#
|
|
# WHY THE VERSION IS FETCHED AND NOT DERIVED. The obvious shortcut is to run
|
|
# `version.sh display desktop` here — this job has the checkout, after all. It is
|
|
# wrong: this commit may not be the commit the channel is serving. A push touching
|
|
# only `src/` does not rebuild the desktop, so the channel still holds an older
|
|
# build, and a locally-derived version would describe those bytes with this
|
|
# commit's number. The size check in `client_dist.py` would not catch it, because
|
|
# the size IS measured from the real file — it would sail through and lie about the
|
|
# version only. So the version comes from the channel, beside the bytes it
|
|
# describes, and only `size`/`sha256` are measured here.
|
|
set -eu
|
|
|
|
channel="${1:?usage: fetch-clients.sh <dev|stable> <destdir>}"
|
|
dest="${2:?usage: fetch-clients.sh <dev|stable> <destdir>}"
|
|
|
|
case "$channel" in dev|stable) : ;; *)
|
|
echo "fetch-clients.sh: unknown channel '$channel'" >&2; exit 2 ;;
|
|
esac
|
|
|
|
SERVER="${GITHUB_SERVER_URL:-https://git.fabledsword.com}"
|
|
REPO="${GITHUB_REPOSITORY:-bvandeusen/inkwell}"
|
|
# The channel's release tag, not its name — `dev` lives on `dev-rolling`
|
|
# (packaging/channel-tag.sh, Scribe #2184).
|
|
tag="$(sh "$(dirname "$0")/channel-tag.sh" "$channel")"
|
|
BASE="$SERVER/$REPO/releases/download/$tag"
|
|
|
|
mkdir -p "$dest"
|
|
|
|
# Authenticated when we have a token — these releases are private (issue 2091), so
|
|
# on this instance we always do. Anonymous still works against a public fork.
|
|
fetch() {
|
|
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
|
curl -fsSL -H "Authorization: token $GITHUB_TOKEN" -o "$2" "$1"
|
|
else
|
|
curl -fsSL -o "$2" "$1"
|
|
fi
|
|
}
|
|
|
|
# One field out of a small flat JSON object. `grep`/`sed` rather than a parser
|
|
# because this runs in the CI image's busybox sh and adding a jq dependency to buy
|
|
# one string is not a trade worth making. The sidecars are written by us and are
|
|
# one level deep.
|
|
field() {
|
|
grep -oE "\"$2\"[[:space:]]*:[[:space:]]*\"?[^,\"}]+\"?" "$1" 2>/dev/null \
|
|
| head -1 | sed -E 's/.*:[[:space:]]*"?([^"]*)"?[[:space:]]*$/\1/'
|
|
}
|
|
|
|
bytes() { wc -c < "$1" | tr -d ' '; }
|
|
digest() { sha256sum "$1" | cut -d' ' -f1; }
|
|
|
|
# The sidecar shape `client_dist.py` reads. `size` and `sha256` are measured from
|
|
# the file that actually landed, so a truncated download cannot be described as a
|
|
# whole one.
|
|
sidecar() {
|
|
_file="$1"; _out="$2"; _name="$3"; _code="$4"
|
|
# `version_code` is QUOTED here, and that is not a slip. This function only ever
|
|
# writes DESKTOP sidecars, whose ordering key is Tauri's `1.0.<minutes>` — which
|
|
# unquoted is not valid JSON at all, so every sidecar this wrote would fail to
|
|
# parse and the server would advertise nothing. Android's sidecar is a different
|
|
# file, copied verbatim from its lane, and keeps its integer.
|
|
printf '{\n "version_name": "%s",\n "version_code": "%s",\n "size": %s,\n "sha256": "%s"\n}\n' \
|
|
"$_name" "$_code" "$(bytes "$_file")" "$(digest "$_file")" > "$_out"
|
|
}
|
|
|
|
echo "==> Collecting the $channel clients"
|
|
|
|
# --- Android -----------------------------------------------------------------
|
|
#
|
|
# Its sidecar is published whole by the Android lane — an APK keeps its version in
|
|
# a binary AXML manifest, so the values are recorded where they were already known.
|
|
# Copied verbatim rather than rebuilt here.
|
|
if fetch "$BASE/inkwell.apk" "$dest/inkwell.apk" &&
|
|
fetch "$BASE/inkwell-android.json" "$dest/inkwell-android.json"; then
|
|
echo " android $(field "$dest/inkwell-android.json" version_name)"
|
|
else
|
|
# Both or neither. Half a pair is worse than none: the server would read a
|
|
# sidecar describing an APK that is not there, or an APK it cannot state a
|
|
# version for.
|
|
echo "::warning::No Android client on the $channel channel — this image ships without one."
|
|
rm -f "$dest/inkwell.apk" "$dest/inkwell-android.json"
|
|
fi
|
|
|
|
# --- desktop -----------------------------------------------------------------
|
|
#
|
|
# One sidecar on the channel carries the version PAIR for all four bundles, because
|
|
# they are one build: `version_name` is what a person reads, `version_code` is the
|
|
# ordering key, and the key is also what the bundle filenames are stamped with.
|
|
# Written by `write-manifest.sh`, which is the step that speaks for what the channel
|
|
# serves.
|
|
bake_desktop() {
|
|
desk="$dest/.desktop-release.json"
|
|
if ! fetch "$BASE/inkwell-desktop.json" "$desk"; then
|
|
echo "::warning::No desktop release on the $channel channel — this image ships without desktop clients."
|
|
rm -f "$desk"
|
|
return 0
|
|
fi
|
|
|
|
name="$(field "$desk" version_name)"
|
|
key="$(field "$desk" version_code)"
|
|
rm -f "$desk"
|
|
|
|
if [ -z "$name" ] || [ -z "$key" ]; then
|
|
echo "::warning::The $channel desktop sidecar named no version — skipping desktop clients."
|
|
return 0
|
|
fi
|
|
echo " desktop $name (key $key)"
|
|
|
|
# Bundle filenames are stamped with the ORDERING KEY — what Tauri puts in them,
|
|
# and what `write-manifest.sh` already selects on. Constructed rather than
|
|
# discovered from the release's asset list: one shape, no JSON walk, and a name
|
|
# that does not resolve is caught by the fetch failing rather than by matching
|
|
# the wrong file.
|
|
#
|
|
# `<platform id>|<published name>|<name on disk>`
|
|
for row in \
|
|
"linux-deb|Inkwell_${key}_amd64.deb|inkwell.deb" \
|
|
"linux-pacman|inkwell-${key}-1-x86_64.pkg.tar.zst|inkwell.pkg.tar.zst" \
|
|
"linux-appimage|Inkwell_${key}_amd64.AppImage|inkwell.AppImage" \
|
|
"windows|Inkwell_${key}_x64-setup.exe|inkwell-setup.exe"
|
|
do
|
|
id="${row%%|*}"; rest="${row#*|}"
|
|
remote="${rest%%|*}"; local_name="${rest#*|}"
|
|
|
|
if ! fetch "$BASE/$remote" "$dest/$local_name"; then
|
|
echo "::warning::$channel has no $remote — this image ships without the $id client."
|
|
rm -f "$dest/$local_name"
|
|
continue
|
|
fi
|
|
|
|
# The AppImage is the only bundle that replaces itself in place, so the updater
|
|
# verifies a signature before it does. Without one it is not servable as an
|
|
# update, and `client_dist.py` treats it as absent rather than offering it
|
|
# unverifiable — so drop the bundle too rather than baking 95 MB nothing can use.
|
|
if [ "$id" = "linux-appimage" ]; then
|
|
if ! fetch "$BASE/$remote.sig" "$dest/$local_name.sig"; then
|
|
echo "::warning::$remote has no signature on $channel — dropping the AppImage."
|
|
rm -f "$dest/$local_name" "$dest/$local_name.sig"
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
sidecar "$dest/$local_name" "$dest/inkwell-$id.json" "$name" "$key"
|
|
echo " $id $(bytes "$dest/$local_name") bytes"
|
|
done
|
|
}
|
|
|
|
bake_desktop
|
|
|
|
echo "==> Baked in:"
|
|
ls -l "$dest"
|