mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-14 10:01:19 +02:00
- publish-release.sh: the fallback takes only an untagged-... draft titled
exactly "Frame Control X.Y.Z" (optionally ": subtitle"); a pre-release's
draft ("9.8.7-rc.1") or one bound to another tag is refused.
- updater.js: ignore the page in update.json/the API entirely and always
link to releases/tag/v<version> built from the validated version, so a
path like tag/..\..\other/repo can't reach shell.openExternal.
- Tests for both, including backslash and %2e%2e traversal.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
104 lines
5.3 KiB
Bash
Executable File
104 lines
5.3 KiB
Bash
Executable File
#!/bin/sh
|
|
# Publish a tested draft release so running copies of Frame Control offer it
|
|
# (docs/releasing.md). Checks every installer is attached with a SHA-256
|
|
# digest first, since the app's updater refuses assets without one, then
|
|
# attaches update.json, the manifest the updater reads.
|
|
# Usage: scripts/publish-release.sh [--dry-run] v0.4.0
|
|
#
|
|
# Everything goes through the REST API (gh api), not `gh release view/edit`:
|
|
# those look a draft up by its tag, and a draft can show tag_name
|
|
# "untagged-..." until it's published, so they report "release not found"
|
|
# (and GraphQL is often rate-limited). A draft's html_url is an untagged-...
|
|
# link that dies on publishing, so update.json's page is built from the tag.
|
|
set -eu
|
|
dry=""
|
|
[ "${1:-}" = "--dry-run" ] && { dry=1; shift; }
|
|
tag="${1:?usage: $0 [--dry-run] vX.Y.Z}"
|
|
repo=saphid/frame-control
|
|
expected="Frame-Control-mac-arm64.dmg Frame-Control-mac-arm64.zip Frame-Control-Setup-x64.exe
|
|
Frame-Control-win-x64.zip Frame-Control-linux-x86_64.AppImage Frame-Control-linux-arm64.AppImage
|
|
Frame-Control-linux-amd64.deb Frame-Control-linux-arm64.deb"
|
|
page="https://github.com/$repo/releases/tag/$tag"
|
|
|
|
version=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' "$(dirname "$0")/../app/package.json")
|
|
[ "v$version" = "$tag" ] || echo "note: app/package.json here says $version (the release was built from the tag)"
|
|
|
|
tmp=$(mktemp -d)
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
|
|
# Publishing sets tag_name; if the tag didn't exist GitHub would create it on
|
|
# the default branch, which isn't what was built and tested.
|
|
gh api "repos/$repo/git/ref/tags/$tag" >/dev/null 2>&1 \
|
|
|| { echo "tag $tag isn't on GitHub; push it first (git push origin $tag)" >&2; exit 1; }
|
|
|
|
# Every release, drafts included, one JSON object per line.
|
|
gh api --paginate "repos/$repo/releases?per_page=100" --jq '.[]' > "$tmp/releases"
|
|
# The release whose tag_name is the tag; failing that, the one untagged-... draft
|
|
# titled "Frame Control X.Y.Z" (release.yml's title), optionally ": subtitle".
|
|
python3 - "$tag" "$tmp/releases" > "$tmp/release.json" <<'EOF'
|
|
import json, re, sys
|
|
tag, path = sys.argv[1], sys.argv[2]
|
|
rels = [json.loads(line) for line in open(path) if line.strip()]
|
|
hits = [r for r in rels if r.get("tag_name") == tag]
|
|
if not hits:
|
|
# Exactly this version: "Frame Control 0.4.0", or that followed by ": <subtitle>".
|
|
# Never "Frame Control 0.4.0-rc.1" or "0.4.00", and only drafts with no real tag.
|
|
title = re.compile(r"Frame Control " + re.escape(tag.lstrip("v")) + r"(: .*)?", re.S)
|
|
hits = [r for r in rels if r.get("draft") and str(r.get("tag_name") or "").startswith("untagged-")
|
|
and title.fullmatch(r.get("name") or "")]
|
|
if len(hits) != 1:
|
|
why = "no release" if not hits else "%d releases (ids %s)" % (len(hits), ", ".join(str(r["id"]) for r in hits))
|
|
sys.exit("found %s for %s; expected one draft" % (why, tag))
|
|
r = hits[0]
|
|
if not r.get("draft"):
|
|
print("note: %s is already published; refreshing update.json and marking it latest" % tag, file=sys.stderr)
|
|
json.dump(r, sys.stdout)
|
|
EOF
|
|
id=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["id"])' "$tmp/release.json")
|
|
echo "release $id ($(python3 -c 'import json,sys; r=json.load(open(sys.argv[1])); print(("draft" if r["draft"] else "published") + ", tag_name " + str(r["tag_name"]))' "$tmp/release.json"))"
|
|
|
|
missing=""
|
|
for name in $expected; do
|
|
digest=$(python3 -c 'import json,sys
|
|
d=json.load(open(sys.argv[1])); n=sys.argv[2]
|
|
print(next((a.get("digest") or "" for a in d["assets"] if a["name"]==n), "absent"))' "$tmp/release.json" "$name")
|
|
case "$digest" in
|
|
sha256:*) echo "ok $name" ;;
|
|
absent) echo "MISSING $name"; missing=1 ;;
|
|
*) echo "NO HASH $name"; missing=1 ;;
|
|
esac
|
|
done
|
|
[ -z "$missing" ] || { echo "not publishing: fix the assets above" >&2; exit 1; }
|
|
|
|
# update.json: what running copies read (app/updater.js), from github.com's
|
|
# latest/download link rather than the rate-limited REST API.
|
|
python3 - "$tmp/release.json" "$tag" "$page" "$expected" > "$tmp/update.json" <<'EOF'
|
|
import json, sys
|
|
d = json.load(open(sys.argv[1]))
|
|
tag, page, names = sys.argv[2], sys.argv[3], set(sys.argv[4].split())
|
|
print(json.dumps({"version": tag.lstrip("v"), "page": page, "notes": (d.get("body") or "")[:4000],
|
|
"assets": [{"name": a["name"], "size": a["size"], "digest": a["digest"]}
|
|
for a in d["assets"] if a["name"] in names]}, indent=1))
|
|
EOF
|
|
old=$(python3 -c 'import json,sys
|
|
d=json.load(open(sys.argv[1]))
|
|
print(" ".join(str(a["id"]) for a in d["assets"] if a["name"]=="update.json"))' "$tmp/release.json")
|
|
|
|
if [ -n "$dry" ]; then
|
|
echo "dry run: would replace update.json (old asset ids: ${old:-none}) with:"
|
|
cat "$tmp/update.json"
|
|
echo "dry run: would PATCH release $id: tag_name=$tag draft=false prerelease=false make_latest=true"
|
|
exit 0
|
|
fi
|
|
|
|
for asset in $old; do
|
|
gh api -X DELETE "repos/$repo/releases/assets/$asset" >/dev/null
|
|
done
|
|
gh api -X POST "https://uploads.github.com/repos/$repo/releases/$id/assets?name=update.json" \
|
|
-H "Content-Type: application/json" --input "$tmp/update.json" >/dev/null
|
|
echo "ok update.json"
|
|
|
|
gh api -X PATCH "repos/$repo/releases/$id" -f tag_name="$tag" -F draft=false -F prerelease=false \
|
|
-f make_latest=true --jq '"published " + .tag_name + " at " + .html_url'
|
|
echo "running copies will offer $tag at their next check"
|