131 lines
5.7 KiB
Bash
Executable File
131 lines
5.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Publish a release on Gitea: create the release for an existing tag from its
|
|
# CHANGELOG.md section and attach the binaries built by `make dist`.
|
|
#
|
|
# scripts/release.sh [--dry-run|--check] [vX.Y.Z]
|
|
#
|
|
# The tag defaults to the one at HEAD. It must be pushed already. The token
|
|
# (Gitea: Settings -> Applications, repository scope with write access) is
|
|
# read from $GITEA_TOKEN or from the file $GITEA_TOKEN_FILE, which defaults to
|
|
# ~/.config/arr-tui-release-token. It never appears in the process list or in
|
|
# any output. The server and repository come from the "origin" remote; set
|
|
# GITEA_URL (e.g. https://git.example.com) if its web address differs.
|
|
#
|
|
# --dry-run build and show what would be published, change nothing
|
|
# --check only verify the token, the repository and that the release is free
|
|
set -euo pipefail
|
|
|
|
cd "$(dirname "$0")/.."
|
|
|
|
mode=release
|
|
tag=
|
|
for arg in "$@"; do
|
|
case $arg in
|
|
--dry-run) mode=dry ;;
|
|
--check) mode=check ;;
|
|
-h|--help) sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
v*) tag=$arg ;;
|
|
*) echo "unknown argument: $arg" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
die() { echo "release: $*" >&2; exit 1; }
|
|
|
|
# --- repository and server from the origin remote ---------------------------
|
|
origin=$(git remote get-url origin) || die "no origin remote"
|
|
if [[ $origin =~ ^ssh://[^@]+@([^:/]+)(:[0-9]+)?/(.+)$ ]]; then
|
|
host=${BASH_REMATCH[1]} path=${BASH_REMATCH[3]}
|
|
elif [[ $origin =~ ^[^@]+@([^:]+):(.+)$ ]]; then
|
|
host=${BASH_REMATCH[1]} path=${BASH_REMATCH[2]}
|
|
elif [[ $origin =~ ^https?://([^/]+)/(.+)$ ]]; then
|
|
host=${BASH_REMATCH[1]} path=${BASH_REMATCH[2]}
|
|
else
|
|
die "cannot parse the origin remote: $origin"
|
|
fi
|
|
repo=${path%.git}
|
|
base=${GITEA_URL:-https://$host}
|
|
api=$base/api/v1/repos/$repo
|
|
|
|
# --- tag --------------------------------------------------------------------
|
|
[[ -n $tag ]] || tag=$(git tag --points-at HEAD | grep '^v' | head -n1) || true
|
|
[[ -n $tag ]] || die "no tag given and none at HEAD"
|
|
git rev-parse -q --verify "refs/tags/$tag" >/dev/null || die "tag $tag does not exist"
|
|
git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null 2>&1 || die "tag $tag is not pushed to origin"
|
|
[[ $(git rev-parse "$tag^{commit}") == "$(git ls-remote origin "refs/tags/$tag^{}" | cut -f1)" ]] \
|
|
|| die "tag $tag differs from the one on origin"
|
|
|
|
# --- token ------------------------------------------------------------------
|
|
token=${GITEA_TOKEN:-}
|
|
if [[ -z $token ]]; then
|
|
file=${GITEA_TOKEN_FILE:-$HOME/.config/arr-tui-release-token}
|
|
[[ -r $file ]] || die "no token: set GITEA_TOKEN or put it in $file"
|
|
[[ $(stat -c %a "$file" 2>/dev/null || stat -f %Lp "$file") =~ ^[0-7]00$ ]] \
|
|
|| echo "release: warning: $file is readable by other users, run chmod 600" >&2
|
|
token=$(tr -d '[:space:]' < "$file")
|
|
fi
|
|
[[ -n $token ]] || die "the token is empty"
|
|
|
|
tmp=$(mktemp -d)
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
umask 077
|
|
# curl reads the header from a file so the token is not visible in `ps`.
|
|
printf 'Authorization: token %s\n' "$token" > "$tmp/auth"
|
|
unset token
|
|
call() { curl -sS -H "@$tmp/auth" "$@"; }
|
|
|
|
# --- checks against the server ----------------------------------------------
|
|
info=$(call "$api") || die "cannot reach $base"
|
|
python3 - "$info" "$repo" <<'PY' || die "no access to $repo (token or repository wrong)"
|
|
import json, sys
|
|
d = json.loads(sys.argv[1])
|
|
if d.get("full_name", "").lower() != sys.argv[2].lower() or not d.get("permissions", {}).get("push"):
|
|
sys.exit(1)
|
|
PY
|
|
existing=$(call -o /dev/null -w '%{http_code}' "$api/releases/tags/$tag")
|
|
[[ $existing == 404 ]] || die "a release for $tag already exists (HTTP $existing)"
|
|
echo "release: $repo on $base, $tag is free"
|
|
[[ $mode == check ]] && exit 0
|
|
|
|
# --- release text from the changelog ------------------------------------------
|
|
python3 - "$tag" "$tmp/release.json" <<'PY' || die "CHANGELOG.md has no section for $tag"
|
|
import json, re, sys
|
|
tag, out = sys.argv[1], sys.argv[2]
|
|
text = open("CHANGELOG.md").read()
|
|
m = re.search(r"^## " + re.escape(tag) + r"\b[^\n]*\n(.*?)(?=^## |\Z)", text, re.S | re.M)
|
|
if not m or not m.group(1).strip():
|
|
sys.exit(1)
|
|
body = m.group(1).strip() + (
|
|
"\n\nBinaries are attached for linux, macOS (amd64 and arm64) and windows; "
|
|
"verify them with `SHA256SUMS`. Build from source with `make dist`.")
|
|
json.dump({"tag_name": tag, "name": "arr-tui " + tag, "body": body,
|
|
"draft": False, "prerelease": "-" in tag}, open(out, "w"))
|
|
PY
|
|
|
|
# --- build ----------------------------------------------------------------------
|
|
make test >/dev/null || die "tests failed"
|
|
make dist VERSION="$tag" >/dev/null
|
|
assets=(dist/*)
|
|
echo "release: built ${#assets[@]} files:"
|
|
for a in "${assets[@]}"; do echo " $(basename "$a")"; done
|
|
[[ $(./dist/arr-tui-linux-amd64 --version 2>/dev/null || true) == "arr-tui $tag" ]] \
|
|
|| echo "release: note: ./dist/arr-tui-linux-amd64 --version does not report $tag (not runnable here?)" >&2
|
|
|
|
if [[ $mode == dry ]]; then
|
|
echo "release: dry run, nothing published. Release text:"
|
|
python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["body"])' "$tmp/release.json"
|
|
exit 0
|
|
fi
|
|
|
|
# --- publish --------------------------------------------------------------------
|
|
id=$(call -H 'Content-Type: application/json' -d "@$tmp/release.json" "$api/releases" \
|
|
| python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("id") or ""); sys.exit(0 if d.get("id") else 1)') \
|
|
|| die "creating the release failed"
|
|
for a in "${assets[@]}"; do
|
|
name=$(basename "$a")
|
|
call -F "attachment=@$a" "$api/releases/$id/assets?name=$name" \
|
|
| python3 -c 'import json,sys; d=json.load(sys.stdin); sys.exit(0 if d.get("name") else 1)' \
|
|
|| die "uploading $name failed (the release $id exists without it)"
|
|
echo "release: uploaded $name"
|
|
done
|
|
echo "release: $base/$repo/releases/tag/$tag"
|