356 lines
9.6 KiB
Bash
Executable File
356 lines
9.6 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 dist/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 PATH instead of the local release signing key
|
|
-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:-}"
|
|
|
|
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
|
|
;;
|
|
-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"
|
|
|
|
"$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 [[ -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
|
|
NOTES_OUTPUT_DIRECTORY="${PROJECT_ROOT}/dist/${TAG}"
|
|
mkdir -p -- "$NOTES_OUTPUT_DIRECTORY"
|
|
cp -- "$NOTES_PATH" "${NOTES_OUTPUT_DIRECTORY}/release-notes.md"
|
|
printf 'Release notes written to %s\n' \
|
|
"${NOTES_OUTPUT_DIRECTORY}/release-notes.md"
|
|
exit 0
|
|
fi
|
|
|
|
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 [[ -z "$SIGNING_KEY" ]]
|
|
then
|
|
SIGNING_KEY="${PROJECT_ROOT}/release-keys/launcher-update-private.pem"
|
|
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
|
|
|
|
if [[ "$DRY_RUN" -eq 1 ]]
|
|
then
|
|
OUTPUT_DIRECTORY="${TEMPORARY_ROOT}/dist/${TAG}"
|
|
else
|
|
OUTPUT_DIRECTORY="${PROJECT_ROOT}/dist/${TAG}"
|
|
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; dist/ 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.'
|