tools: release.sh publishes the ISO, and keeps only the newest one
A release on the Forgejo carries the ISO and its sha256. The server is a small VPS shared with other projects, so publishing a release deletes the ISO from every older one; their notes, tags and checksums stay, with a line saying how to rebuild the image from the tag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
parent
4265794437
commit
b524a73522
1 changed files with 149 additions and 0 deletions
149
tools/posix/release.sh
Normal file
149
tools/posix/release.sh
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#
|
||||
# Publish a release on the Forgejo the project lives on, with the ISO attached.
|
||||
#
|
||||
# tools/posix/release.sh v0.2.0 notes from the commits since the last tag
|
||||
# tools/posix/release.sh v0.2.0 --notes FILE notes from a file
|
||||
# tools/posix/release.sh v0.2.0 --prerelease
|
||||
# tools/posix/release.sh v0.2.0 --dry-run say what would happen, change nothing
|
||||
#
|
||||
# **Only the newest release keeps its ISO.** The server is a small VPS shared
|
||||
# with other projects, and an ISO is a third of a gigabyte. So publishing one
|
||||
# deletes the ISO from every older release; their notes, tags and checksums
|
||||
# stay, and each says how to rebuild its image from the tag. Every release is
|
||||
# reproducible from its tag -- world.lock pins the packages -- so nothing is
|
||||
# lost but the download.
|
||||
#
|
||||
# The ISO must be built from the commit being released, and that commit must
|
||||
# already be on the server: the release tags it there. The token is read from
|
||||
# $FORGEJO_TOKEN or ~/.config/pandor-git-token and goes only into request
|
||||
# headers, never into a command line that `ps` would show or a file.
|
||||
|
||||
set -eu
|
||||
|
||||
FORGEJO=${FORGEJO:-https://pandor.pl/git}
|
||||
REPO=${REPO:-nexoniarz/SchOS}
|
||||
API="$FORGEJO/api/v1/repos/$REPO"
|
||||
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
|
||||
ISO="$ROOT/schos.iso"
|
||||
|
||||
die() { echo "release: $*" >&2; exit 1; }
|
||||
|
||||
[ $# -ge 1 ] || die "usage: $0 TAG [--notes FILE] [--prerelease] [--dry-run]"
|
||||
TAG=$1; shift
|
||||
NOTES=""; PRE=false; DRY=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case $1 in
|
||||
--notes) NOTES=$2; shift 2 ;;
|
||||
--prerelease) PRE=true; shift ;;
|
||||
--dry-run) DRY=1; shift ;;
|
||||
*) die "unknown option $1" ;;
|
||||
esac
|
||||
done
|
||||
case $TAG in v[0-9]*) ;; *) die "tags look like v0.2.0, not '$TAG'" ;; esac
|
||||
|
||||
if [ -n "${FORGEJO_TOKEN:-}" ]; then
|
||||
TOKEN=$FORGEJO_TOKEN
|
||||
elif [ -r "$HOME/.config/pandor-git-token" ]; then
|
||||
TOKEN=$(cat "$HOME/.config/pandor-git-token")
|
||||
else
|
||||
die "no token: set FORGEJO_TOKEN or write one to ~/.config/pandor-git-token"
|
||||
fi
|
||||
# curl reads its headers from this file descriptor, so the token is never an
|
||||
# argument.
|
||||
auth() { printf 'Authorization: token %s\n' "$TOKEN"; }
|
||||
api() { # METHOD PATH [curl args...]
|
||||
m=$1; p=$2; shift 2
|
||||
auth | curl -sS --fail-with-body -H @- -X "$m" "$@" "$API$p"
|
||||
}
|
||||
json() { python3 -c "import json,sys; d=json.load(sys.stdin); $1"; }
|
||||
|
||||
SHA=$(git -C "$ROOT" rev-parse HEAD)
|
||||
[ -f "$ISO" ] || die "no schos.iso; run make iso first"
|
||||
# Built after the commit was made, or it is an image of something else.
|
||||
COMMITTED=$(git -C "$ROOT" log -1 --format=%ct HEAD)
|
||||
BUILT=$(stat -c %Y "$ISO" 2>/dev/null || stat -f %m "$ISO")
|
||||
[ "$BUILT" -ge "$COMMITTED" ] || die "schos.iso is older than HEAD; run make iso"
|
||||
if ! git -C "$ROOT" diff --quiet HEAD -- kernel include boot cmd lib ports tools Makefile; then
|
||||
echo "release: warning: the tree has uncommitted changes; the ISO may include them" >&2
|
||||
fi
|
||||
api GET "/git/commits/$SHA" >/dev/null 2>&1 || die "HEAD ($SHA) is not on the server; push first"
|
||||
if api GET "/releases/tags/$TAG" >/dev/null 2>&1; then
|
||||
die "release $TAG already exists"
|
||||
fi
|
||||
|
||||
NAME="schos-$TAG.iso"
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
HASH=$( (sha256sum "$ISO" 2>/dev/null || shasum -a 256 "$ISO") | cut -d' ' -f1)
|
||||
printf '%s %s\n' "$HASH" "$NAME" > "$WORK/$NAME.sha256"
|
||||
SIZE=$(( $(wc -c < "$ISO") / 1048576 ))
|
||||
|
||||
if [ -n "$NOTES" ]; then
|
||||
cp "$NOTES" "$WORK/notes.md"
|
||||
else
|
||||
PREV=$(git -C "$ROOT" describe --tags --abbrev=0 HEAD 2>/dev/null || true)
|
||||
{
|
||||
if [ -n "$PREV" ]; then
|
||||
echo "## Since $PREV"
|
||||
git -C "$ROOT" log --format='- %s' "$PREV..HEAD"
|
||||
else
|
||||
echo "## The first release"
|
||||
echo
|
||||
echo "The most recent changes:"
|
||||
echo
|
||||
git -C "$ROOT" log --format='- %s' -n 30 HEAD
|
||||
fi
|
||||
} > "$WORK/notes.md"
|
||||
fi
|
||||
cat >> "$WORK/notes.md" <<EOF
|
||||
|
||||
## The image
|
||||
|
||||
\`$NAME\` · $SIZE MB · UEFI x86-64 · sha256 \`$HASH\`
|
||||
|
||||
Write it to a USB stick (\`dd if=$NAME of=/dev/sdX bs=4M\`, or Rufus/Etcher in
|
||||
DD mode) and boot it. It runs from RAM and writes nothing to the machine's
|
||||
disks.
|
||||
EOF
|
||||
|
||||
echo "release: $TAG at ${SHA%"${SHA#????????????}"}, $NAME ($SIZE MB)"
|
||||
if [ $DRY = 1 ]; then
|
||||
echo "--- notes ---"; cat "$WORK/notes.md"; echo "-------------"
|
||||
fi
|
||||
|
||||
# Older releases first, so a failed upload below never leaves two ISOs on the
|
||||
# server; the worst case is a release without one, fixed by running this again.
|
||||
OLD=$(api GET "/releases?limit=50" | json '
|
||||
for r in d:
|
||||
for a in r["assets"]:
|
||||
if a["name"].endswith(".iso"):
|
||||
print(r["id"], a["id"], r["tag_name"], a["name"])')
|
||||
echo "$OLD" | while read -r rid aid tag name; do
|
||||
[ -n "$rid" ] || continue
|
||||
echo "release: removing $name from $tag"
|
||||
[ $DRY = 1 ] && continue
|
||||
api DELETE "/releases/$rid/assets/$aid" >/dev/null
|
||||
body=$(api GET "/releases/$rid" | json 'print(d["body"])')
|
||||
case $body in *"ISO for this release was removed"*) continue ;; esac
|
||||
printf '%s\n\n_The ISO for this release was removed to make room for newer ones. Build it from the tag: `git checkout %s && make iso`._\n' \
|
||||
"$body" "$tag" | python3 -c 'import json,sys; print(json.dumps({"body": sys.stdin.read()}))' \
|
||||
> "$WORK/patch.json"
|
||||
api PATCH "/releases/$rid" -H 'Content-Type: application/json' --data @"$WORK/patch.json" >/dev/null
|
||||
done
|
||||
|
||||
[ $DRY = 1 ] && { echo "release: dry run; nothing changed"; exit 0; }
|
||||
|
||||
python3 - "$TAG" "$SHA" "$PRE" "$WORK/notes.md" > "$WORK/release.json" <<'EOF'
|
||||
import json, sys
|
||||
tag, sha, pre, notes = sys.argv[1:]
|
||||
print(json.dumps({"tag_name": tag, "target_commitish": sha, "name": "SchOS " + tag,
|
||||
"body": open(notes).read(), "prerelease": pre == "true"}))
|
||||
EOF
|
||||
RID=$(api POST /releases -H 'Content-Type: application/json' --data @"$WORK/release.json" \
|
||||
| json 'print(d["id"])')
|
||||
echo "release: uploading $NAME"
|
||||
api POST "/releases/$RID/assets?name=$NAME" -F "attachment=@$ISO" >/dev/null
|
||||
api POST "/releases/$RID/assets?name=$NAME.sha256" -F "attachment=@$WORK/$NAME.sha256" >/dev/null
|
||||
echo "release: $FORGEJO/$REPO/releases/tag/$TAG"
|
||||
Loading…
Reference in a new issue