Files
pristontale-eu-linux/scripts/release.sh

382 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
usage()
{
cat <<'EOF'
Usage: ./scripts/release.sh CHANNEL VERSION [OPTIONS]
Build a verified local release package in .artifacts/releases/vVERSION/. This
command does not commit, tag, push, create a Gitea release, or upload files.
Channels:
stable
prerelease (aliases: pre, beta)
Options:
--description TEXT Developer-written Markdown release description
--description-file PATH Read developer-written Markdown from PATH
--no-auto-changelog Omit commit-derived change notes
--changelog-only Write and display release notes without building
--editor Edit the final notes with ${EDITOR:-nano}
--dry-run Build and verify in a temporary directory only
--skip-tests Skip configured local preflight checks
--signing-key PATH Use an external private signing key at PATH
--output-directory PATH Write the version package to PATH
-h, --help Show this help
EOF
}
if [[ $# -lt 1 ]]
then
usage >&2
exit 2
fi
if [[ "$1" == "-h" || "$1" == "--help" ]]
then
usage
exit 0
fi
if [[ $# -lt 2 ]]
then
usage >&2
exit 2
fi
CHANNEL_INPUT="$1"
VERSION="$2"
shift 2
case "$CHANNEL_INPUT" in
stable)
CHANNEL="stable"
;;
prerelease|pre|beta)
CHANNEL="prerelease"
;;
*)
printf 'Unsupported release channel: %s\n' "$CHANNEL_INPUT" >&2
exit 2
;;
esac
DESCRIPTION=""
DESCRIPTION_FILE=""
AUTO_CHANGELOG=1
CHANGELOG_ONLY=0
OPEN_EDITOR=0
DRY_RUN=0
SKIP_TESTS=0
SIGNING_KEY="${PTLINUX_SIGNING_KEY:-}"
REQUESTED_OUTPUT_DIRECTORY=""
while [[ $# -gt 0 ]]
do
case "$1" in
--description)
[[ $# -ge 2 ]] || { printf '%s\n' '--description requires text.' >&2; exit 2; }
DESCRIPTION="$2"
shift 2
;;
--description-file)
[[ $# -ge 2 ]] || { printf '%s\n' '--description-file requires a path.' >&2; exit 2; }
DESCRIPTION_FILE="$2"
shift 2
;;
--no-auto-changelog)
AUTO_CHANGELOG=0
shift
;;
--changelog-only)
CHANGELOG_ONLY=1
shift
;;
--editor)
OPEN_EDITOR=1
shift
;;
--dry-run)
DRY_RUN=1
shift
;;
--skip-tests)
SKIP_TESTS=1
shift
;;
--signing-key)
[[ $# -ge 2 ]] || { printf '%s\n' '--signing-key requires a path.' >&2; exit 2; }
SIGNING_KEY="$2"
shift 2
;;
--output-directory)
[[ $# -ge 2 ]] || { printf '%s\n' '--output-directory requires a path.' >&2; exit 2; }
REQUESTED_OUTPUT_DIRECTORY="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
printf 'Unknown release option: %s\n' "$1" >&2
usage >&2
exit 2
;;
esac
done
if [[ -n "$DESCRIPTION" && -n "$DESCRIPTION_FILE" ]]
then
printf '%s\n' 'Use either --description or --description-file, not both.' >&2
exit 2
fi
SCRIPT_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd -- "${SCRIPT_ROOT}/.." && pwd)"
CONFIG_PATH="${PROJECT_ROOT}/release/config.json"
if [[ -n "${PTLINUX_PYTHON:-}" ]]
then
PYTHON_BIN="$PTLINUX_PYTHON"
elif [[ -x "${PROJECT_ROOT}/.venv/bin/python" ]]
then
PYTHON_BIN="${PROJECT_ROOT}/.venv/bin/python"
else
PYTHON_BIN="python3"
fi
if [[ "$(git -C "$PROJECT_ROOT" rev-parse --show-toplevel 2>/dev/null || true)" != "$PROJECT_ROOT" ]]
then
printf 'Release command is not inside the expected Git repository: %s\n' "$PROJECT_ROOT" >&2
exit 1
fi
cd -- "$PROJECT_ROOT"
export PYTHONPATH="${PROJECT_ROOT}/src${PYTHONPATH:+:${PYTHONPATH}}"
"$PYTHON_BIN" -m ptlinux.update_resolver validate "$CHANNEL" "$VERSION"
TAG="v${VERSION}"
COMMIT="$(git rev-parse HEAD)"
SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)"
if [[ -n "$(git status --porcelain --untracked-files=normal)" ]]
then
printf '%s\n' \
"Warning: packaging the current working tree on top of source commit ${COMMIT}." \
'The signed per-file manifest records the exact packaged content.' >&2
fi
if [[ -n "$DESCRIPTION_FILE" && ! -f "$DESCRIPTION_FILE" ]]
then
printf 'Release description file does not exist: %s\n' "$DESCRIPTION_FILE" >&2
exit 1
fi
TEMPORARY_ROOT="$(mktemp -d)"
cleanup()
{
rm -rf -- "$TEMPORARY_ROOT"
}
trap cleanup EXIT
NOTES_PATH="${TEMPORARY_ROOT}/release-notes.md"
if [[ "$DRY_RUN" -eq 1 && "$CHANGELOG_ONLY" -eq 0 ]]
then
OUTPUT_CANDIDATE="${TEMPORARY_ROOT}/releases/${TAG}"
else
OUTPUT_CANDIDATE="${REQUESTED_OUTPUT_DIRECTORY:-${PROJECT_ROOT}/.artifacts/releases/${TAG}}"
fi
OUTPUT_DIRECTORY="$(
"$PYTHON_BIN" -c \
'import sys; from pathlib import Path; from scripts.release_lib import _safe_output_directory; print(_safe_output_directory(Path(sys.argv[1])))' \
"$OUTPUT_CANDIDATE"
)"
if [[ -z "$DESCRIPTION" && -z "$DESCRIPTION_FILE" && -t 0 && "$CHANGELOG_ONLY" -eq 0 ]]
then
printf '%s\n' 'Release description' '-------------------' '' \
'Enter release notes. Finish with Ctrl+D.' ''
DESCRIPTION_FILE="${TEMPORARY_ROOT}/description.md"
cat < /dev/tty > "$DESCRIPTION_FILE"
fi
{
printf '# PristonTale EU Launcher %s\n\n' "$VERSION"
if [[ -n "$DESCRIPTION" ]]
then
printf '%s\n\n' "$DESCRIPTION"
elif [[ -n "$DESCRIPTION_FILE" ]]
then
cat -- "$DESCRIPTION_FILE"
if [[ -s "$DESCRIPTION_FILE" && "$(tail -c 1 -- "$DESCRIPTION_FILE" | wc -l)" -eq 0 ]]
then
printf '\n'
fi
printf '\n'
fi
if [[ "$AUTO_CHANGELOG" -eq 1 ]]
then
mapfile -t RELEASE_TAGS < <(git tag --merged HEAD)
PREVIOUS_TAG="$(
"$PYTHON_BIN" -m ptlinux.update_resolver previous-tag \
"$CHANNEL" "$VERSION" "${RELEASE_TAGS[@]}"
)"
printf '## Changes\n\n'
if [[ -n "$PREVIOUS_TAG" ]]
then
CHANGELOG_RANGE="${PREVIOUS_TAG}..HEAD"
else
CHANGELOG_RANGE="HEAD"
fi
CHANGELOG="$(git log "$CHANGELOG_RANGE" --no-merges --pretty=format:'- %s (`%h`)')"
if [[ -n "$CHANGELOG" ]]
then
printf '%s\n' "$CHANGELOG"
else
printf '%s\n' '- No commits since the previous release.'
fi
printf '\n'
fi
printf '%s\n' '---' '' \
"Version: \`${VERSION}\` " \
"Channel: \`${CHANNEL}\` " \
"Tag: \`${TAG}\` " \
"Source commit: \`${COMMIT}\`"
} > "$NOTES_PATH"
if [[ "$OPEN_EDITOR" -eq 1 ]]
then
EDITOR_COMMAND="${EDITOR:-nano}"
"$EDITOR_COMMAND" "$NOTES_PATH"
fi
printf '%s\n' '--------------------------------------------------' 'Release Notes' \
'--------------------------------------------------'
cat -- "$NOTES_PATH"
printf '%s\n' '--------------------------------------------------'
if [[ "$CHANGELOG_ONLY" -eq 1 ]]
then
mkdir -p -- "$OUTPUT_DIRECTORY"
cp -- "$NOTES_PATH" "${OUTPUT_DIRECTORY}/release-notes.md"
printf 'Release notes written to %s\n' \
"${OUTPUT_DIRECTORY}/release-notes.md"
exit 0
fi
if [[ -z "$SIGNING_KEY" ]]
then
printf '%s\n' 'A release signing key is required.' >&2
printf '%s\n' \
'Use --signing-key PATH or set PTLINUX_SIGNING_KEY to a path outside the repository.' >&2
exit 1
fi
SIGNING_KEY="$(
"$PYTHON_BIN" -c \
'import sys
from pathlib import Path
from scripts.release_lib import ReleaseToolError, _safe_signing_key_path
try:
print(_safe_signing_key_path(Path(sys.argv[1]), output_directory=Path(sys.argv[2])))
except ReleaseToolError as exc:
print(f"Invalid release signing key: {exc}", file=sys.stderr)
raise SystemExit(2)' \
"$SIGNING_KEY" "$OUTPUT_DIRECTORY"
)"
if [[ "$SKIP_TESTS" -ne 1 ]]
then
mapfile -t PREFLIGHT_COMMANDS < <(
"$PYTHON_BIN" -c \
'import json,sys; value=json.load(open(sys.argv[1],encoding="utf-8")); print("\n".join(value["preflight"]))' \
"$CONFIG_PATH"
)
for PREFLIGHT_COMMAND in "${PREFLIGHT_COMMANDS[@]}"
do
printf 'Preflight: %s\n' "$PREFLIGHT_COMMAND"
PTLINUX_PYTHON="$PYTHON_BIN" bash -o pipefail -c "$PREFLIGHT_COMMAND" \
< /dev/null
done
else
printf '%s\n' 'Skipping local preflight checks by request.'
fi
if [[ ! -f "$SIGNING_KEY" || -L "$SIGNING_KEY" ]]
then
printf 'Release signing key is missing: %s\n' "$SIGNING_KEY" >&2
printf '%s\n' 'Use --signing-key PATH or set PTLINUX_SIGNING_KEY.' >&2
exit 1
fi
SOURCE_DATE_EPOCH="$SOURCE_DATE_EPOCH" \
RELEASE_COMMIT="$COMMIT" \
RELEASE_TAG="$TAG" \
RELEASE_NOTES_FILE="$NOTES_PATH" \
PTLINUX_PYTHON="$PYTHON_BIN" \
PTLINUX_SIGNING_KEY="$SIGNING_KEY" \
REQUIRE_RELEASE_SIGNATURE=1 \
ALLOW_SOURCE_VERSION_OVERRIDE=1 \
"${SCRIPT_ROOT}/build-release.sh" "$VERSION" "$CHANNEL" "$OUTPUT_DIRECTORY"
PTLINUX_PYTHON="$PYTHON_BIN" \
REQUIRE_RELEASE_SIGNATURE=1 \
"${SCRIPT_ROOT}/verify-release.sh" "$VERSION" "$CHANNEL" "$OUTPUT_DIRECTORY"
if [[ "$DRY_RUN" -eq 1 ]]
then
printf '%s\n' \
'Dry run complete. The release built and verified in a temporary directory; .artifacts/ was not changed.'
exit 0
fi
mkdir -p -- "${OUTPUT_DIRECTORY}/channel"
cp -- "${OUTPUT_DIRECTORY}/manifest.json" \
"${OUTPUT_DIRECTORY}/channel/${CHANNEL}.json"
cp -- "${OUTPUT_DIRECTORY}/manifest.json.sig" \
"${OUTPUT_DIRECTORY}/channel/${CHANNEL}.json.sig"
mapfile -t PACKAGE_METADATA < <(
"$PYTHON_BIN" -c \
'import json,sys; value=json.load(open(sys.argv[1], encoding="utf-8")); archive=value["archive"]; print(archive["layout"]); print(value["minimum_updater_version"]); print(archive["size"]); print(archive["sha256"]); print(archive["url"])' \
"${OUTPUT_DIRECTORY}/manifest.json"
)
ARCHIVE_LAYOUT="${PACKAGE_METADATA[0]}"
MINIMUM_UPDATER_VERSION="${PACKAGE_METADATA[1]}"
ARCHIVE_SIZE="${PACKAGE_METADATA[2]}"
ARCHIVE_SHA256="${PACKAGE_METADATA[3]}"
ARCHIVE_URL="${PACKAGE_METADATA[4]}"
if [[ "$CHANNEL" == "prerelease" ]]
then
PRERELEASE_INSTRUCTION='select the Gitea "Pre-release" checkbox'
else
PRERELEASE_INSTRUCTION='leave the Gitea "Pre-release" checkbox clear'
fi
printf '\nManual Gitea release package is ready in %s\n' "$OUTPUT_DIRECTORY"
printf 'Create tag/release: %s\n' "$TAG"
printf 'Release type: %s; %s.\n' "$CHANNEL" "$PRERELEASE_INSTRUCTION"
printf 'Archive layout: %s.\n' "$ARCHIVE_LAYOUT"
printf 'Minimum updater: %s.\n' "$MINIMUM_UPDATER_VERSION"
printf 'Archive size: %s bytes.\n' "$ARCHIVE_SIZE"
printf 'Archive SHA256: %s\n' "$ARCHIVE_SHA256"
printf 'Signed archive URL: %s\n' "$ARCHIVE_URL"
if [[ "$ARCHIVE_LAYOUT" == "wrapped" ]]
then
printf '%s\n' \
'Compatibility: legacy bridge archive; older launchers can install it automatically.'
else
printf '%s\n' \
'Compatibility: flat archive; launchers older than the minimum require a bridge release or installer update.'
fi
printf '%s\n' 'Upload these five files:' \
" ${OUTPUT_DIRECTORY}/PristonTale-EU-${VERSION}.tar.gz" \
" ${OUTPUT_DIRECTORY}/PristonTale-EU-${VERSION}.tar.gz.sha256" \
" ${OUTPUT_DIRECTORY}/manifest.json" \
" ${OUTPUT_DIRECTORY}/manifest.json.sig" \
" ${OUTPUT_DIRECTORY}/release-notes.md"
printf '%s\n' \
'Wait for every upload to finish and confirm the archive has the exact nonzero size above.' \
'Publish the channel files only after all five Gitea attachments are present.'
printf 'Channel files ready for a later repository update: %s/channel/%s.json{,.sig}\n' \
"$OUTPUT_DIRECTORY" "$CHANNEL"
printf '%s\n' 'No commit, tag, push, Gitea release, or upload was performed.'