An installed title's version line now reads `0.1 → 0.3` where the catalog has moved on, so a card answers both questions somebody brings to it: what is installed, and is there anything better. Which version is installed was already recorded — `state.json` has always carried it — what was missing was anywhere to act on it. The card leads with Play (or Open, for a hosted title) and puts the rest behind a ⋮ button: Upgrade, which fetches whatever the catalog now has, and Uninstall. Upgrade stays visible while disabled rather than appearing and disappearing — a menu whose items come and go makes a person hunt for the one they used last time, and greyed out already says "not now". Playing stays the headline even with an upgrade waiting: the build on the disk still runs, and wanting to play it is not the same as wanting to wait for a download. The menu is a `<details>`, so its open state is the DOM's and the keyboard needs no teaching. Closing it on an outside click is the grid's job, not a card's: cards are rebuilt on every render, so a listener per card would be a listener per render. Package names lose their spaces — `WarpEngineClient-2.3.0-arm64.dmg` — because a space in a release asset is a space in every curl, script and shell command that touches it. Set per target rather than globally: nsis and portable would otherwise resolve to the same .exe name and overwrite each other. `productName` is untouched, so the app is still called WarpEngine Client where a person sees it — in the Dock and in /Applications. Tested on a sandbox store by rewriting one state record to claim an older build, which is what the engine actually compares: the window then offered `Upgrade:on` for that title and `Upgrade:off` for the current one, and pressing it took the record from 0.1 to 0.2 with the old payload removed first. The self-test asserts that pairing on every installed card, because a closed menu photographs identically whether or not its items are right. In Hungarian the catalog refresh and the new Upgrade both wanted "Frissítés"; the refresh is an icon with a tooltip, and a tooltip can afford to say *Katalógus frissítése*. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
141 lines
5.8 KiB
Bash
Executable File
141 lines
5.8 KiB
Bash
Executable File
#!/bin/sh
|
|
# Publish the built packages as a Gitea release.
|
|
#
|
|
# The version comes from package.json, so the tag follows whatever `npm version`
|
|
# set — there is nothing to keep in sync by hand. The release is created if it is
|
|
# not there yet, and an attachment with a name already on it is replaced rather
|
|
# than refused, which is what makes a rebuild-and-upload repeatable.
|
|
#
|
|
# scripts/release.sh every package in dist/
|
|
# scripts/release.sh dist/foo.dmg just these
|
|
#
|
|
# Assumes `tea` is installed and logged in (see the devarea repo: `make tea`).
|
|
set -eu
|
|
|
|
LOGIN="${TEA_LOGIN:-ttg}"
|
|
NOTES="${NOTES:-RELEASE_NOTES.md}"
|
|
DIST="${DIST:-dist}"
|
|
|
|
say() { echo "[release] $*"; }
|
|
die() { echo "[release] error: $*" >&2; exit 1; }
|
|
|
|
command -v tea >/dev/null 2>&1 || die "tea is not installed — see the devarea repo, 'make tea'"
|
|
command -v node >/dev/null 2>&1 || die "node is required"
|
|
[ -f package.json ] || die "run this from the repository root"
|
|
|
|
# Version and title from package.json, read with node. This project needs no interpreter
|
|
# beyond the one it already builds with — the app itself carries no Python any more, and
|
|
# neither should the script that ships it.
|
|
VERSION="$(node -p 'require("./package.json").version')"
|
|
TITLE="$(node -p 'const d = require("./package.json"); (d.productName || d.name) + " " + d.version')"
|
|
TAG="${TAG:-v$VERSION}"
|
|
[ -n "$VERSION" ] || die "cannot read the version from package.json"
|
|
[ -n "$TITLE" ] || die "cannot work out a release title"
|
|
|
|
# The repository is whatever this checkout pushes to, so a fork publishes to the
|
|
# fork without editing anything.
|
|
REPO="${REPO:-$(git remote get-url origin 2>/dev/null |
|
|
sed -e 's#.*[:/]\([^/]*/[^/]*\)$#\1#' -e 's#\.git$##')}"
|
|
[ -n "$REPO" ] || die "cannot work out the Gitea repo — set REPO=owner/name"
|
|
|
|
# What to upload: the arguments, or the packages in dist/ that belong to *this*
|
|
# version. Two things this has to get right:
|
|
#
|
|
# - the version filter, because dist/ keeps whatever earlier builds left there
|
|
# and a release would quietly get the previous version's files attached;
|
|
# - the spaces. The built names have none since 2.3.0 — `WarpEngineClient-2.3.0-arm64.dmg`
|
|
# — but a path given as an argument still can, so the list stays one path per line in
|
|
# a file, read with `while IFS= read -r`. Holding it in a single variable and looping
|
|
# over $list splits it on the space, and publishes nothing.
|
|
LIST="$(mktemp)"
|
|
trap 'rm -f "$LIST"' EXIT
|
|
if [ "$#" -gt 0 ]; then
|
|
for given in "$@"; do printf '%s\n' "$given"; done > "$LIST"
|
|
else
|
|
find "$DIST" -maxdepth 1 -type f -name "*$VERSION*" \
|
|
\( -name '*.dmg' -o -name '*-mac.zip' -o -name '*.exe' -o -name '*.AppImage' -o -name '*.deb' \) \
|
|
2>/dev/null | sort > "$LIST" || true
|
|
fi
|
|
[ -s "$LIST" ] || die "no $VERSION packages in $DIST — run 'make dist' first"
|
|
|
|
say "$REPO $TAG (version $VERSION), login $LOGIN"
|
|
|
|
# The release id, or empty when there is no such tag. `tea api` exits 0 even for a
|
|
# 404 — it answers {"message":"not found"} — so the body is what has to be read.
|
|
release_id() {
|
|
tea api "/repos/$REPO/releases/tags/$TAG" 2>/dev/null | node -e '
|
|
try {
|
|
console.log(JSON.parse(require("fs").readFileSync(0, "utf8")).id || "")
|
|
} catch {
|
|
// Not JSON, or no such release: an empty answer is the "no release yet" case.
|
|
}
|
|
'
|
|
}
|
|
|
|
# --- the release itself ----------------------------------------------------
|
|
if [ -n "$(release_id)" ]; then
|
|
say "the release already exists"
|
|
else
|
|
say "creating the release: $TITLE"
|
|
if [ -f "$NOTES" ]; then
|
|
tea releases create --login "$LOGIN" --repo "$REPO" --tag "$TAG" \
|
|
--title "$TITLE" --note-file "$NOTES" >/dev/null
|
|
else
|
|
say "no $NOTES — the release gets a one-line note"
|
|
tea releases create --login "$LOGIN" --repo "$REPO" --tag "$TAG" \
|
|
--title "$TITLE" --note "Packages built from $TAG." >/dev/null
|
|
fi
|
|
fi
|
|
|
|
RELEASE_ID="$(release_id)"
|
|
[ -n "$RELEASE_ID" ] || die "the release $TAG could not be created or found"
|
|
|
|
# --- the attachments -------------------------------------------------------
|
|
# Anything already attached under this name, dropped: replacing rather than
|
|
# refusing is what makes a rebuild-and-upload repeatable. Called before every
|
|
# attempt, so a retry cannot leave two copies behind.
|
|
drop_existing() {
|
|
ids="$(tea api "/repos/$REPO/releases/$RELEASE_ID/assets" | node -e '
|
|
const name = process.argv[1]
|
|
for (const asset of JSON.parse(require("fs").readFileSync(0, "utf8"))) {
|
|
if (asset.name === name) console.log(asset.id)
|
|
}
|
|
' -- "$1")"
|
|
for id in $ids; do
|
|
say "replacing $1"
|
|
tea api -X DELETE "/repos/$REPO/releases/$RELEASE_ID/assets/$id" >/dev/null
|
|
done
|
|
}
|
|
|
|
while IFS= read -r asset; do
|
|
[ -n "$asset" ] || continue
|
|
[ -f "$asset" ] || die "no such file: $asset"
|
|
name="$(basename "$asset")"
|
|
size="$(node -e 'console.log((require("fs").statSync(process.argv[1]).size / 1e6).toFixed(0) + " MB")' -- "$asset")"
|
|
|
|
# Retried, because a 100 MB upload does fail on its own: publishing 1.2.0 got
|
|
# "invalid username, password or token" on the second package while the first
|
|
# had just gone up with the same token, and the identical command succeeded on
|
|
# the next run. One flake should not cost a rebuild.
|
|
attempt=1
|
|
while :; do
|
|
drop_existing "$name"
|
|
say "uploading $name ($size) — large packages take a few minutes"
|
|
if tea releases assets create --login "$LOGIN" --repo "$REPO" "$TAG" "$asset" >/dev/null; then
|
|
break
|
|
fi
|
|
[ "$attempt" -lt 3 ] || die "$name could not be uploaded after $attempt attempts"
|
|
attempt=$((attempt + 1))
|
|
say "that failed — attempt $attempt of 3"
|
|
done
|
|
done < "$LIST"
|
|
|
|
say "done:"
|
|
tea api "/repos/$REPO/releases/$RELEASE_ID" | node -e '
|
|
const release = JSON.parse(require("fs").readFileSync(0, "utf8"))
|
|
for (const asset of release.assets || []) {
|
|
console.log(` ${asset.name} ${(asset.size / 1e6).toFixed(0)} MB`)
|
|
}
|
|
console.log(` ${release.html_url}`)
|
|
'
|