From b524a73522ef2c18678593cc91f9ec6651d4175b Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sat, 24 Oct 2026 22:11:01 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4 --- tools/posix/release.sh | 149 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 tools/posix/release.sh diff --git a/tools/posix/release.sh b/tools/posix/release.sh new file mode 100644 index 0000000..d06e034 --- /dev/null +++ b/tools/posix/release.sh @@ -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" </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"