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