Publishing 1.2.0 put the first package up and then failed the second with "invalid username, password or token" — the same token, seconds later, and the identical command succeeded on the next run. A flake at 118 MB should not cost a rebuild, so each upload gets up to three attempts, and the attachment already on the release is dropped before every attempt so a retry cannot leave two copies. The shipped bundle was checked rather than assumed: unpacked from the release zip, `codesign --verify --deep --strict` is valid on disk and satisfies its designated requirement, and the app drives the real store — ten cards, the menu populated, the categories counted. With the quarantine flag set it is killed on launch (exit 137), which is the ad-hoc-signing limitation the README already documents. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
149 lines
5.6 KiB
Bash
Executable File
149 lines
5.6 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 python3 >/dev/null 2>&1 || die "python3 is required"
|
|
[ -f package.json ] || die "run this from the repository root"
|
|
|
|
# Version and title in one go, through a heredoc rather than a quoted one-liner:
|
|
# nesting python quoting inside shell quoting inside a command substitution is how
|
|
# this produced an empty title on its first outing.
|
|
VERSION="$(python3 - <<'PY'
|
|
import json
|
|
print(json.load(open("package.json"))["version"])
|
|
PY
|
|
)"
|
|
TITLE="$(python3 - <<'PY'
|
|
import json
|
|
d = json.load(open("package.json"))
|
|
print((d.get("productName") or d["name"]) + " " + d["version"])
|
|
PY
|
|
)"
|
|
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. "WarpEngine Store-1.1.0-arm64.dmg" has one, so the list lives one
|
|
# path per line in a file and is read with `while IFS= read -r`. Holding it in
|
|
# a single variable and looping over $list splits it on the space.
|
|
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 | python3 -c '
|
|
import json, sys
|
|
try:
|
|
print(json.load(sys.stdin).get("id") or "")
|
|
except Exception:
|
|
pass
|
|
'
|
|
}
|
|
|
|
# --- 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" | python3 -c "
|
|
import json, sys
|
|
name = sys.argv[1]
|
|
for a in json.load(sys.stdin):
|
|
if a['name'] == name:
|
|
print(a['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="$(python3 -c "import os,sys; print(f'{os.path.getsize(sys.argv[1])/1e6:.0f} 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" | python3 -c "
|
|
import json, sys
|
|
r = json.load(sys.stdin)
|
|
for a in r.get('assets') or []:
|
|
print(f\" {a['name']} {a['size']/1e6:.0f} MB\")
|
|
print(f\" {r['html_url']}\")"
|