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>
240 lines
12 KiB
Bash
240 lines
12 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# Write the updater manifest (`latest.json`) for one channel and attach it to that
|
|
# channel's release.
|
|
#
|
|
# WHY A SEPARATE STEP: the Linux and Windows bundles are built by two jobs in two
|
|
# workspaces, and neither can see the other's output — but ONE manifest has to
|
|
# describe both platforms. So this runs after both, reads what actually landed on
|
|
# the release, and writes the manifest from that. Building it inside either job
|
|
# would produce a manifest that silently omits the other platform, and a missing
|
|
# platform reads to a user as "no update available" rather than as a broken feed.
|
|
#
|
|
# WHAT IT READS: the release's own asset list. The signature for each bundle is a
|
|
# `.sig` asset published beside it (see publish-release.sh); its CONTENT is what
|
|
# goes in the manifest, which is why each one is downloaded rather than linked.
|
|
#
|
|
# Tauri's expected shape:
|
|
# { "version": "0.1.0", "pub_date": "...", "notes": "...",
|
|
# "platforms": { "<target>-<arch>": { "signature": "...", "url": "..." } } }
|
|
set -euo pipefail
|
|
|
|
: "${GITHUB_TOKEN:?GITHUB_TOKEN is required}"
|
|
: "${GITHUB_SERVER_URL:?GITHUB_SERVER_URL is required}"
|
|
: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required (owner/repo)}"
|
|
: "${RELEASE_TAG:?RELEASE_TAG is required (the release holding the bundles)}"
|
|
: "${APP_VERSION:?APP_VERSION is required (the version the bundles carry)}"
|
|
# The version a PERSON reads, published beside the manifest so the image build can
|
|
# describe the bundles it bakes in without re-deriving anything. Required rather
|
|
# than defaulted: a missing value here would silently publish a sidecar naming the
|
|
# wrong build, and there is nothing downstream that could catch it.
|
|
: "${DISPLAY_VERSION:?DISPLAY_VERSION is required (the human-readable version)}"
|
|
|
|
# The manifest is published to the release that HOLDS the bundles. There is no
|
|
# second place any more.
|
|
#
|
|
# There used to be: `MANIFEST_TAG` let the manifest live on a `stable` pointer
|
|
# release while the bundles sat on a versioned `v*` one, because the app can only
|
|
# read a URL that never changes and a versioned tag is not that. M314 step 3 made
|
|
# `stable` a rolling release that holds its own bundles, exactly like `dev`, so the
|
|
# split had nothing left to bridge — and a parameter that can only ever be passed
|
|
# its own default is a branch nobody exercises and a comment that goes stale.
|
|
|
|
API="$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY"
|
|
AUTH=(-H "Authorization: token $GITHUB_TOKEN")
|
|
NOTES="${RELEASE_NOTES:-}"
|
|
|
|
work="$(mktemp -d)"
|
|
trap 'rm -rf "$work"' EXIT INT TERM
|
|
|
|
echo "==> Reading assets on release $RELEASE_TAG"
|
|
release="$(curl -sS "${AUTH[@]}" "$API/releases/tags/$RELEASE_TAG")"
|
|
release_id="$(printf '%s' "$release" | grep -oE '"id"[[:space:]]*:[[:space:]]*[0-9]+' | head -1 | grep -oE '[0-9]+')"
|
|
[ -n "$release_id" ] || { echo "ERROR: no release tagged $RELEASE_TAG" >&2; exit 1; }
|
|
assets="$(curl -sS "${AUTH[@]}" "$API/releases/$release_id/assets")"
|
|
|
|
# Asset names, one per line. The API returns them in a single JSON blob; this is
|
|
# the only field needed, and grep beats adding a jq dependency to the CI image.
|
|
names="$(printf '%s' "$assets" | grep -oE '"name"[[:space:]]*:[[:space:]]*"[^"]+"' | sed -E 's/.*"([^"]+)"$/\1/')"
|
|
|
|
download_url() { printf '%s/%s/releases/download/%s/%s' "$GITHUB_SERVER_URL" "$GITHUB_REPOSITORY" "$RELEASE_TAG" "$1"; }
|
|
|
|
# One platform entry, or nothing if that platform's bundle or signature is absent.
|
|
# Emitting a partial entry would be worse than emitting none: the app would try to
|
|
# install something it can't verify.
|
|
platform_entry() {
|
|
local target="$1" pattern="$2" bundle sig_name
|
|
# Matched on THIS build's version, not just the file extension.
|
|
#
|
|
# The rolling dev release accumulates every build's assets, and picking the first
|
|
# extension match returned the OLDEST one — so the manifest advertised the new
|
|
# version while pointing at an old binary. The client would install the older
|
|
# build, still be told the newer version was available, and update forever. The
|
|
# signature check couldn't catch it either: the old bundle's signature is
|
|
# perfectly valid FOR THE OLD BUNDLE.
|
|
bundle="$(printf '%s\n' "$names" | grep -F "_${APP_VERSION}_" | grep -E "$pattern" | head -1 || true)"
|
|
# A hard failure, not a skip: reaching here means this platform's build didn't
|
|
# upload, and the whole point is to never advertise a bundle that isn't there.
|
|
[ -n "$bundle" ] || { echo " no $APP_VERSION bundle matching $pattern — skipping $target" >&2; return; }
|
|
sig_name="$bundle.sig"
|
|
if ! printf '%s\n' "$names" | grep -qxF "$sig_name"; then
|
|
echo " $bundle has no $sig_name — skipping $target (was the build signed?)" >&2
|
|
return
|
|
fi
|
|
curl -fsSL "${AUTH[@]}" -o "$work/sig" "$(download_url "$sig_name")"
|
|
# The signature is base64 on one line already; strip any stray newline so it
|
|
# can't break the JSON string it's about to become.
|
|
local signature
|
|
signature="$(tr -d '\r\n' < "$work/sig")"
|
|
printf ' "%s": { "signature": "%s", "url": "%s" }' "$target" "$signature" "$(download_url "$bundle")"
|
|
}
|
|
|
|
echo "==> Building the manifest"
|
|
entries=()
|
|
# `.AppImage` only on Linux: the updater replaces the running bundle in place, which
|
|
# a package-manager install (deb/pacman) must never have done to it.
|
|
if entry="$(platform_entry "linux-x86_64" '\.AppImage$')" && [ -n "$entry" ]; then entries+=("$entry"); fi
|
|
if entry="$(platform_entry "windows-x86_64" '\.exe$')" && [ -n "$entry" ]; then entries+=("$entry"); fi
|
|
|
|
if [ ${#entries[@]} -eq 0 ]; then
|
|
echo "ERROR: no signed bundle on $RELEASE_TAG — refusing to publish an empty manifest." >&2
|
|
echo " (An empty manifest would tell every client it is up to date.)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# No `date -u -Is` — busybox date in the CI image doesn't take it.
|
|
pub_date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
|
{
|
|
printf '{\n'
|
|
printf ' "version": "%s",\n' "$APP_VERSION"
|
|
printf ' "pub_date": "%s",\n' "$pub_date"
|
|
printf ' "notes": "%s",\n' "$NOTES"
|
|
printf ' "platforms": {\n'
|
|
for i in "${!entries[@]}"; do
|
|
[ "$i" -eq 0 ] || printf ',\n'
|
|
printf '%s' "${entries[$i]}"
|
|
done
|
|
printf '\n }\n'
|
|
printf '}\n'
|
|
} > "$work/latest.json"
|
|
|
|
echo "==> Manifest:"
|
|
cat "$work/latest.json"
|
|
|
|
# Both files go on the same release the bundles were just read from — which is also
|
|
# the one `publish-release.sh` created or refreshed moments earlier, so it is
|
|
# guaranteed to exist by the time this runs.
|
|
|
|
# Replace rather than duplicate: Forgejo rejects a second asset with the same name,
|
|
# and these files are rewritten on every publish by design.
|
|
replace_asset() {
|
|
local path="$1" name="$2" escaped old_id
|
|
escaped="${name//./\\.}"
|
|
old_id="$(printf '%s' "$assets" \
|
|
| grep -oE "\"id\"[[:space:]]*:[[:space:]]*[0-9]+[^}]*\"name\"[[:space:]]*:[[:space:]]*\"$escaped\"" \
|
|
| head -1 | grep -oE '[0-9]+' | head -1 || true)"
|
|
if [ -n "${old_id:-}" ]; then
|
|
echo "==> Removing the previous $name (id $old_id)"
|
|
curl -fsS -X DELETE "${AUTH[@]}" "$API/releases/$release_id/assets/$old_id" >/dev/null
|
|
fi
|
|
echo "==> Uploading $name to $RELEASE_TAG"
|
|
curl -fsS -X POST "${AUTH[@]}" "$API/releases/$release_id/assets?name=$name" \
|
|
-F "attachment=@$path" >/dev/null
|
|
}
|
|
|
|
replace_asset "$work/latest.json" "latest.json"
|
|
|
|
# The version pair, for whoever needs to describe these bundles without rebuilding
|
|
# them — today the image build, which bakes the desktop clients in and writes each
|
|
# one a sidecar (`packaging/fetch-clients.sh`).
|
|
#
|
|
# It is published HERE, beside the manifest, because this is the step that speaks
|
|
# for what the channel serves: both files are written in the same breath from the
|
|
# same two values, so they cannot disagree about which build is current. A consumer
|
|
# deriving the version from its own checkout instead would describe these bytes
|
|
# with whatever commit it happened to be on.
|
|
#
|
|
# No `size` or `sha256` — those are per-artifact and there are four. Whoever
|
|
# downloads a bundle measures the bytes it actually got, which is the only way to
|
|
# tell a truncated download from a whole one.
|
|
printf '{\n "version_name": "%s",\n "version_code": "%s"\n}\n' \
|
|
"$DISPLAY_VERSION" "$APP_VERSION" > "$work/inkwell-desktop.json"
|
|
replace_asset "$work/inkwell-desktop.json" "inkwell-desktop.json"
|
|
|
|
# --- TEMPORARY: the one-shot bridge off a retired channel tag ------------------
|
|
#
|
|
# The dev channel's tag was renamed `dev` -> `dev-rolling`, because a tag named `dev`
|
|
# shadowed the branch and broke `git push origin dev` (Scribe #2184). Desktop apps
|
|
# installed before the rename have `.../download/dev/latest.json` compiled in, so
|
|
# without this they would never be offered another build.
|
|
#
|
|
# BRIDGE_TAG names the old release; the SAME manifest is written there too. Its URLs
|
|
# are absolute and name `dev-rolling` assets, so an old app updates once into a build
|
|
# that reads the new tag, and never reads this one again. Only latest.json goes
|
|
# across: the sidecar is read by CI and the image build, which already follow the
|
|
# new tag.
|
|
#
|
|
# A missing bridge release is logged and skipped, not fatal — by then it has been
|
|
# deleted on purpose. A failed WRITE to one that exists fails the job: a bridge that
|
|
# silently stops is an installed base stranded without a word. Remove this block,
|
|
# the BRIDGE_TAG line in desktop.yml, and the old release and tag together.
|
|
if [ -n "${BRIDGE_TAG:-}" ]; then
|
|
bridge_release="$(curl -sS "${AUTH[@]}" "$API/releases/tags/$BRIDGE_TAG" || true)"
|
|
bridge_id="$(printf '%s' "$bridge_release" | grep -oE '"id"[[:space:]]*:[[:space:]]*[0-9]+' | head -1 | grep -oE '[0-9]+' || true)"
|
|
if [ -z "$bridge_id" ]; then
|
|
echo "==> No release tagged $BRIDGE_TAG — nothing to bridge (retired?)."
|
|
else
|
|
# A subshell, so replace_asset's globals point at the bridge release for this
|
|
# one call only and the prune below still works on $RELEASE_TAG.
|
|
(
|
|
release_id="$bridge_id"
|
|
RELEASE_TAG="$BRIDGE_TAG"
|
|
assets="$(curl -sS "${AUTH[@]}" "$API/releases/$release_id/assets")"
|
|
replace_asset "$work/latest.json" "latest.json"
|
|
)
|
|
echo "==> Bridged: $BRIDGE_TAG/latest.json now advertises the same build."
|
|
fi
|
|
fi
|
|
|
|
echo "==> Done. $RELEASE_TAG now advertises $DISPLAY_VERSION ($APP_VERSION) for ${#entries[@]} platform(s)."
|
|
|
|
# --- prune superseded builds from a rolling channel ---------------------------
|
|
#
|
|
# The dev release is republished on every push and its assets otherwise accumulate
|
|
# forever — an AppImage alone is ~100 MB, so a week of pushes is gigabytes on the
|
|
# Git host for builds nobody can reach (the manifest only ever names the newest).
|
|
#
|
|
# Only for a rolling channel. A versioned release must keep its assets: that IS the
|
|
# archive, and the stable pointer's URLs aim at it.
|
|
if [ "${PRUNE_OLD_ASSETS:-false}" = "true" ]; then
|
|
echo "==> Pruning superseded assets from $RELEASE_TAG"
|
|
# Re-read: the manifest upload above changed the asset list.
|
|
current="$(curl -sS "${AUTH[@]}" "$API/releases/$release_id/assets")"
|
|
printf '%s' "$current" \
|
|
| grep -oE '"id"[[:space:]]*:[[:space:]]*[0-9]+[^}]*"name"[[:space:]]*:[[:space:]]*"[^"]+"' \
|
|
| while IFS= read -r row; do
|
|
asset_id="$(printf '%s' "$row" | grep -oE '[0-9]+' | head -1)"
|
|
asset_name="$(printf '%s' "$row" | sed -E 's/.*"name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')"
|
|
# Keep FIXED-NAME assets and everything belonging to the current build.
|
|
#
|
|
# A fixed name is self-limiting: each publish replaces that same name, so
|
|
# it cannot accumulate and the reason this prune exists does not apply to
|
|
# it. It is also the only kind of URL that stays addressable on a rolling
|
|
# tag, which is the whole point of having one — deleting it breaks
|
|
# whatever was pointing at it.
|
|
#
|
|
# The Android client is on that list for a second reason too: it is built
|
|
# by a DIFFERENT workflow with its own run number, so its version never
|
|
# matches $APP_VERSION here and a version-stamped name would be pruned on
|
|
# every desktop push regardless. That is exactly what happened on run
|
|
# 4098, which swept the APK run 4092 had just published.
|
|
case "$asset_name" in
|
|
latest.json|inkwell-desktop.json|inkwell.apk|inkwell-android.json) continue ;;
|
|
*"$APP_VERSION"*) continue ;;
|
|
esac
|
|
echo " removing $asset_name"
|
|
curl -fsS -X DELETE "${AUTH[@]}" "$API/releases/$release_id/assets/$asset_id" >/dev/null || \
|
|
echo " (couldn't remove $asset_name — leaving it)" >&2
|
|
done
|
|
fi
|