#!/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"