352 lines
11 KiB
Bash
Executable File
352 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
if [[ $# -ne 4 ]]
|
|
then
|
|
printf 'Usage: %s VERSION CHANNEL OUTPUT_DIR RELEASE_NOTES\n' "$0" >&2
|
|
exit 2
|
|
fi
|
|
|
|
VERSION="$1"
|
|
CHANNEL="$2"
|
|
OUTPUT_DIRECTORY="$(realpath -- "$3")"
|
|
RELEASE_NOTES="$(realpath -- "$4")"
|
|
TAG="${RELEASE_TAG:-v${VERSION}}"
|
|
COMMIT="${RELEASE_COMMIT:?RELEASE_COMMIT is required}"
|
|
PRERELEASE="${RELEASE_PRERELEASE:?RELEASE_PRERELEASE is required}"
|
|
API_URL="${GITHUB_API_URL:?GITHUB_API_URL is required}"
|
|
REPOSITORY="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
|
|
SERVER_URL="${GITHUB_SERVER_URL:?GITHUB_SERVER_URL is required}"
|
|
TOKEN="${RELEASE_TOKEN:?RELEASE_TOKEN is required}"
|
|
RELEASE_BRANCH="${RELEASE_BRANCH:-main}"
|
|
PYTHON_BIN="${PTLINUX_PYTHON:-python3}"
|
|
SCRIPT_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
ARCHIVE_PREFIX="$(jq -er '.archive_prefix' "${SCRIPT_ROOT}/../release/config.json")"
|
|
ARCHIVE="${OUTPUT_DIRECTORY}/${ARCHIVE_PREFIX}-${VERSION}.tar.gz"
|
|
CHECKSUM="${ARCHIVE}.sha256"
|
|
MANIFEST="${OUTPUT_DIRECTORY}/manifest.json"
|
|
MANIFEST_SIGNATURE="${OUTPUT_DIRECTORY}/manifest.json.sig"
|
|
RELEASE_NOTES_ASSET="${OUTPUT_DIRECTORY}/release-notes.md"
|
|
API_ROOT="${API_URL%/}/repos/${REPOSITORY}"
|
|
RESPONSE_DIRECTORY="$(mktemp -d)"
|
|
|
|
cleanup()
|
|
{
|
|
rm -rf -- "$RESPONSE_DIRECTORY"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
RELEASE_MODEL="$(
|
|
"$PYTHON_BIN" "${SCRIPT_ROOT}/../ptlinux/update_resolver.py" inspect-tag "$TAG"
|
|
)"
|
|
if [[ "$(jq -r '.version' <<< "$RELEASE_MODEL")" != "$VERSION" \
|
|
|| "$(jq -r '.channel' <<< "$RELEASE_MODEL")" != "$CHANNEL" \
|
|
|| "$(jq -r '.prerelease' <<< "$RELEASE_MODEL")" != "$PRERELEASE" ]]
|
|
then
|
|
printf 'Release tag, version, channel, and prerelease flag are inconsistent.\n' >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! "$COMMIT" =~ ^[0-9a-f]{40,64}$ ]]
|
|
then
|
|
printf 'Release commit is not a lowercase Git object ID: %s\n' "$COMMIT" >&2
|
|
exit 1
|
|
fi
|
|
|
|
for REQUIRED_ARTIFACT in \
|
|
"$ARCHIVE" \
|
|
"$CHECKSUM" \
|
|
"$MANIFEST" \
|
|
"$MANIFEST_SIGNATURE" \
|
|
"$RELEASE_NOTES_ASSET"
|
|
do
|
|
if [[ ! -s "$REQUIRED_ARTIFACT" ]]
|
|
then
|
|
printf 'Required release artifact is missing or empty: %s\n' "$REQUIRED_ARTIFACT" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
api_request()
|
|
{
|
|
local METHOD="$1"
|
|
local URL="$2"
|
|
local OUTPUT="$3"
|
|
shift 3
|
|
curl \
|
|
--silent \
|
|
--show-error \
|
|
--connect-timeout 20 \
|
|
--max-time 180 \
|
|
--request "$METHOD" \
|
|
--header "Authorization: token ${TOKEN}" \
|
|
--header 'Accept: application/json' \
|
|
--output "$OUTPUT" \
|
|
--write-out '%{http_code}' \
|
|
"$@" \
|
|
"$URL"
|
|
}
|
|
|
|
fetch_all_releases()
|
|
{
|
|
local OUTPUT="$1"
|
|
local PAGE=1
|
|
local PAGE_COUNT
|
|
local PAGE_FILE
|
|
local STATUS
|
|
local COMBINED
|
|
printf '%s\n' '[]' > "$OUTPUT"
|
|
while (( PAGE <= 100 ))
|
|
do
|
|
PAGE_FILE="${RESPONSE_DIRECTORY}/releases-page-${PAGE}.json"
|
|
STATUS="$(
|
|
api_request GET "${API_ROOT}/releases?limit=100&page=${PAGE}" "$PAGE_FILE"
|
|
)"
|
|
if [[ "$STATUS" != "200" ]]
|
|
then
|
|
printf 'Could not fetch release history page %s: HTTP %s\n' \
|
|
"$PAGE" "$STATUS" >&2
|
|
exit 1
|
|
fi
|
|
if ! PAGE_COUNT="$(jq -er 'if type == "array" then length else error("not an array") end' "$PAGE_FILE")"
|
|
then
|
|
printf 'Gitea release history page %s is not an array.\n' "$PAGE" >&2
|
|
exit 1
|
|
fi
|
|
COMBINED="${RESPONSE_DIRECTORY}/releases-combined-${PAGE}.json"
|
|
jq -s '.[0] + .[1]' "$OUTPUT" "$PAGE_FILE" > "$COMBINED"
|
|
mv -- "$COMBINED" "$OUTPUT"
|
|
if (( PAGE_COUNT < 100 ))
|
|
then
|
|
return
|
|
fi
|
|
((PAGE += 1))
|
|
done
|
|
printf '%s\n' 'Gitea release history exceeded the 10,000-release safety limit.' >&2
|
|
exit 1
|
|
}
|
|
|
|
EXISTING_RESPONSE="${RESPONSE_DIRECTORY}/existing.json"
|
|
EXISTING_STATUS="$(api_request GET "${API_ROOT}/releases/tags/${TAG}" "$EXISTING_RESPONSE")"
|
|
case "$EXISTING_STATUS" in
|
|
404)
|
|
;;
|
|
200)
|
|
printf 'Release %s already exists. Published releases are immutable.\n' "$TAG" >&2
|
|
exit 1
|
|
;;
|
|
*)
|
|
printf 'Could not check existing release %s: HTTP %s\n' "$TAG" "$EXISTING_STATUS" >&2
|
|
jq -r '.message // empty' "$EXISTING_RESPONSE" >&2 || true
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
RELEASE_PAYLOAD="${RESPONSE_DIRECTORY}/release-payload.json"
|
|
jq -n \
|
|
--rawfile body "$RELEASE_NOTES" \
|
|
--arg name "PristonTale EU Launcher ${VERSION}" \
|
|
--arg tag_name "$TAG" \
|
|
--arg target_commitish "$COMMIT" \
|
|
--argjson prerelease "$PRERELEASE" \
|
|
'{body:$body,draft:false,name:$name,prerelease:$prerelease,tag_name:$tag_name,target_commitish:$target_commitish}' \
|
|
> "$RELEASE_PAYLOAD"
|
|
|
|
CREATE_RESPONSE="${RESPONSE_DIRECTORY}/created.json"
|
|
CREATE_STATUS="$(
|
|
api_request POST "${API_ROOT}/releases" "$CREATE_RESPONSE" \
|
|
--header 'Content-Type: application/json' \
|
|
--data-binary "@${RELEASE_PAYLOAD}"
|
|
)"
|
|
if [[ "$CREATE_STATUS" != "201" ]]
|
|
then
|
|
printf 'Gitea release creation failed: HTTP %s\n' "$CREATE_STATUS" >&2
|
|
jq -r '.message // empty' "$CREATE_RESPONSE" >&2 || true
|
|
exit 1
|
|
fi
|
|
RELEASE_ID="$(jq -er '.id | select(type == "number" and . > 0)' "$CREATE_RESPONSE")"
|
|
|
|
upload_asset()
|
|
{
|
|
local FILE="$1"
|
|
local NAME
|
|
local RESPONSE
|
|
local STATUS
|
|
NAME="$(basename -- "$FILE")"
|
|
RESPONSE="${RESPONSE_DIRECTORY}/asset-${NAME}.json"
|
|
STATUS="$(
|
|
api_request POST "${API_ROOT}/releases/${RELEASE_ID}/assets?name=${NAME}" "$RESPONSE" \
|
|
--form "attachment=@${FILE};type=application/octet-stream"
|
|
)"
|
|
if [[ "$STATUS" != "201" ]]
|
|
then
|
|
printf 'Gitea asset upload failed for %s: HTTP %s\n' "$NAME" "$STATUS" >&2
|
|
jq -r '.message // empty' "$RESPONSE" >&2 || true
|
|
exit 1
|
|
fi
|
|
if [[ "$(jq -r '.name // empty' "$RESPONSE")" != "$NAME" ]]
|
|
then
|
|
printf 'Gitea returned the wrong attachment name for %s.\n' "$NAME" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
upload_asset "$ARCHIVE"
|
|
upload_asset "$CHECKSUM"
|
|
upload_asset "$MANIFEST"
|
|
upload_asset "$MANIFEST_SIGNATURE"
|
|
upload_asset "$RELEASE_NOTES_ASSET"
|
|
|
|
VERIFY_RESPONSE="${RESPONSE_DIRECTORY}/verified.json"
|
|
VERIFY_STATUS="$(api_request GET "${API_ROOT}/releases/tags/${TAG}" "$VERIFY_RESPONSE")"
|
|
if [[ "$VERIFY_STATUS" != "200" ]]
|
|
then
|
|
printf 'Could not re-query published release %s: HTTP %s\n' "$TAG" "$VERIFY_STATUS" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$(jq -r '.prerelease' "$VERIFY_RESPONSE")" != "$PRERELEASE" ]]
|
|
then
|
|
printf 'Published release prerelease flag does not match channel %s.\n' "$CHANNEL" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$(jq -r '.tag_name // empty' "$VERIFY_RESPONSE")" != "$TAG" \
|
|
|| "$(jq -r '.draft' "$VERIFY_RESPONSE")" != "false" \
|
|
|| "$(jq -r '.target_commitish // empty' "$VERIFY_RESPONSE")" != "$COMMIT" ]]
|
|
then
|
|
printf 'Published release identity or target commit does not match %s.\n' "$TAG" >&2
|
|
exit 1
|
|
fi
|
|
for REQUIRED_ARTIFACT in \
|
|
"$ARCHIVE" \
|
|
"$CHECKSUM" \
|
|
"$MANIFEST" \
|
|
"$MANIFEST_SIGNATURE" \
|
|
"$RELEASE_NOTES_ASSET"
|
|
do
|
|
ASSET_NAME="$(basename -- "$REQUIRED_ARTIFACT")"
|
|
EXPECTED_SIZE="$(stat -c '%s' -- "$REQUIRED_ARTIFACT")"
|
|
ACTUAL_SIZE="$(
|
|
jq -er --arg name "$ASSET_NAME" \
|
|
'.assets[] | select(.name == $name) | .size' "$VERIFY_RESPONSE"
|
|
)"
|
|
if [[ "$ACTUAL_SIZE" != "$EXPECTED_SIZE" ]]
|
|
then
|
|
printf 'Published asset size mismatch for %s: expected %s, got %s.\n' \
|
|
"$ASSET_NAME" "$EXPECTED_SIZE" "$ACTUAL_SIZE" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
RELEASES_RESPONSE="${RESPONSE_DIRECTORY}/releases.json"
|
|
fetch_all_releases "$RELEASES_RESPONSE"
|
|
PUBLISHED_AT="$(jq -er '.published_at' "$VERIFY_RESPONSE")"
|
|
HISTORY="${OUTPUT_DIRECTORY}/history.json"
|
|
HISTORY_SIGNATURE="${OUTPUT_DIRECTORY}/history.json.sig"
|
|
"$PYTHON_BIN" "${SCRIPT_ROOT}/generate_history.py" \
|
|
"$RELEASES_RESPONSE" "$HISTORY" --generated-at "$PUBLISHED_AT"
|
|
if ! jq -e \
|
|
--arg tag "$TAG" \
|
|
--arg channel "$CHANNEL" \
|
|
'.releases[] | select(.tag == $tag and .channel == $channel and .status == "active")' \
|
|
"$HISTORY" >/dev/null
|
|
then
|
|
printf 'Published release %s is absent or inactive in generated history.\n' "$TAG" >&2
|
|
exit 1
|
|
fi
|
|
SIGNING_ARGUMENTS=(
|
|
"$HISTORY"
|
|
"$HISTORY_SIGNATURE"
|
|
"${PTLINUX_SIGNING_KEY:?PTLINUX_SIGNING_KEY is required}"
|
|
)
|
|
if [[ -n "${PTLINUX_UPDATE_PUBLIC_KEY:-}" ]]
|
|
then
|
|
SIGNING_ARGUMENTS+=(--public-key "$PTLINUX_UPDATE_PUBLIC_KEY")
|
|
fi
|
|
"$PYTHON_BIN" "${SCRIPT_ROOT}/sign_metadata.py" "${SIGNING_ARGUMENTS[@]}"
|
|
|
|
CHANNEL_CONTENT="$(base64 -w 0 -- "$MANIFEST")"
|
|
CHANNEL_SIGNATURE_CONTENT="$(base64 -w 0 -- "$MANIFEST_SIGNATURE")"
|
|
HISTORY_CONTENT="$(base64 -w 0 -- "$HISTORY")"
|
|
HISTORY_SIGNATURE_CONTENT="$(base64 -w 0 -- "$HISTORY_SIGNATURE")"
|
|
CHANNEL_PATH="updates/${CHANNEL}.json"
|
|
CHANNEL_SIGNATURE_PATH="updates/${CHANNEL}.json.sig"
|
|
|
|
file_operation()
|
|
{
|
|
local PATH_VALUE="$1"
|
|
local CONTENT_VALUE="$2"
|
|
local RESPONSE="${RESPONSE_DIRECTORY}/content-$(basename -- "$PATH_VALUE").json"
|
|
local STATUS
|
|
STATUS="$(api_request GET "${API_ROOT}/contents/${PATH_VALUE}?ref=${RELEASE_BRANCH}" "$RESPONSE")"
|
|
case "$STATUS" in
|
|
200)
|
|
jq -cn \
|
|
--arg content "$CONTENT_VALUE" \
|
|
--arg path "$PATH_VALUE" \
|
|
--arg sha "$(jq -er '.sha' "$RESPONSE")" \
|
|
'{content:$content,operation:"update",path:$path,sha:$sha}'
|
|
;;
|
|
404)
|
|
jq -cn \
|
|
--arg content "$CONTENT_VALUE" \
|
|
--arg path "$PATH_VALUE" \
|
|
'{content:$content,operation:"create",path:$path}'
|
|
;;
|
|
*)
|
|
printf 'Could not inspect repository metadata path %s: HTTP %s\n' \
|
|
"$PATH_VALUE" "$STATUS" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
CHANNEL_OPERATION="$(file_operation "$CHANNEL_PATH" "$CHANNEL_CONTENT")"
|
|
CHANNEL_SIGNATURE_OPERATION="$(
|
|
file_operation "$CHANNEL_SIGNATURE_PATH" "$CHANNEL_SIGNATURE_CONTENT"
|
|
)"
|
|
HISTORY_OPERATION="$(file_operation updates/history.json "$HISTORY_CONTENT")"
|
|
HISTORY_SIGNATURE_OPERATION="$(
|
|
file_operation updates/history.json.sig "$HISTORY_SIGNATURE_CONTENT"
|
|
)"
|
|
CHANNEL_PAYLOAD="${RESPONSE_DIRECTORY}/channel-payload.json"
|
|
jq -n \
|
|
--arg branch "$RELEASE_BRANCH" \
|
|
--arg message "Update ${CHANNEL} channel to ${VERSION}" \
|
|
--argjson channel "$CHANNEL_OPERATION" \
|
|
--argjson channel_signature "$CHANNEL_SIGNATURE_OPERATION" \
|
|
--argjson history "$HISTORY_OPERATION" \
|
|
--argjson history_signature "$HISTORY_SIGNATURE_OPERATION" \
|
|
'{branch:$branch,message:$message,files:[$channel,$channel_signature,$history,$history_signature]}' \
|
|
> "$CHANNEL_PAYLOAD"
|
|
|
|
CHANNEL_RESPONSE="${RESPONSE_DIRECTORY}/channel-response.json"
|
|
CHANNEL_STATUS="$(
|
|
api_request POST "${API_ROOT}/contents" "$CHANNEL_RESPONSE" \
|
|
--header 'Content-Type: application/json' \
|
|
--data-binary "@${CHANNEL_PAYLOAD}"
|
|
)"
|
|
if [[ "$CHANNEL_STATUS" != "201" ]]
|
|
then
|
|
printf 'Atomic channel/history metadata update failed: HTTP %s\n' "$CHANNEL_STATUS" >&2
|
|
jq -r '.message // empty' "$CHANNEL_RESPONSE" >&2 || true
|
|
exit 1
|
|
fi
|
|
|
|
ARCHIVE_SHA256="$(cut -d ' ' -f 1 -- "$CHECKSUM")"
|
|
SUMMARY_FILE="${GITHUB_STEP_SUMMARY:-/dev/null}"
|
|
{
|
|
printf '## Published release\n\n'
|
|
printf '| Field | Value |\n|---|---|\n'
|
|
printf '| Release | PristonTale EU Launcher %s |\n' "$VERSION"
|
|
printf '| Channel | `%s` |\n' "$CHANNEL"
|
|
printf '| Tag | `%s` |\n' "$TAG"
|
|
printf '| Commit | `%s` |\n' "$COMMIT"
|
|
printf '| Archive | `%s` |\n' "$(basename -- "$ARCHIVE")"
|
|
printf '| SHA-256 | `%s` |\n' "$ARCHIVE_SHA256"
|
|
printf '| Manifest | `manifest.json` |\n'
|
|
printf '| Status | Published |\n'
|
|
} >> "$SUMMARY_FILE"
|
|
|
|
printf 'Published %s release %s (%s) with verified assets and channel metadata.\n' \
|
|
"$CHANNEL" "$TAG" "${SERVER_URL%/}/${REPOSITORY}/releases/tag/${TAG}"
|