#!/usr/bin/env bash set -euo pipefail if [[ $# -ne 1 ]] then printf 'Usage: %s OUTPUT_DIR\n' "$0" >&2 exit 2 fi OUTPUT_DIRECTORY="$(realpath -m -- "$1")" 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)" STATUS_FILE="${RELEASE_STATUS_FILE:-${SCRIPT_ROOT}/../release/statuses.json}" API_ROOT="${API_URL%/}/repos/${REPOSITORY}" RESPONSE_DIRECTORY="$(mktemp -d)" cleanup() { rm -rf -- "$RESPONSE_DIRECTORY" } trap cleanup EXIT mkdir -p -- "$OUTPUT_DIRECTORY" 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 } RELEASES_RESPONSE="${RESPONSE_DIRECTORY}/releases.json" fetch_all_releases "$RELEASES_RESPONSE" GENERATED_AT="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" HISTORY="${OUTPUT_DIRECTORY}/history.json" HISTORY_SIGNATURE="${OUTPUT_DIRECTORY}/history.json.sig" "$PYTHON_BIN" "${SCRIPT_ROOT}/generate_history.py" \ "$RELEASES_RESPONSE" "$HISTORY" --generated-at "$GENERATED_AT" \ --status-file "$STATUS_FILE" 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_DIRECTORY="${RESPONSE_DIRECTORY}/channels" mkdir -p -- "$CHANNEL_DIRECTORY" CHANNELS_TO_PUBLISH=() for CHANNEL in stable prerelease do RECORD="$( jq -cer --arg channel "$CHANNEL" \ '([.releases[] | select(.channel == $channel)] | (map(select(.status == "active"))[0] // .[0]))' \ "$HISTORY" 2>/dev/null || true )" if [[ -z "$RECORD" || "$RECORD" == "null" ]] then continue fi MANIFEST_URL="$(jq -er '.manifest_url' <<< "$RECORD")" MANIFEST_SIGNATURE_URL="$(jq -er '.manifest_signature_url' <<< "$RECORD")" for ASSET_URL in "$MANIFEST_URL" "$MANIFEST_SIGNATURE_URL" do case "$ASSET_URL" in "${SERVER_URL%/}/"*) ;; *) printf 'Refusing to send release credentials to untrusted URL: %s\n' \ "$ASSET_URL" >&2 exit 1 ;; esac done SOURCE_MANIFEST="${CHANNEL_DIRECTORY}/${CHANNEL}-source.json" SOURCE_SIGNATURE="${CHANNEL_DIRECTORY}/${CHANNEL}-source.json.sig" MANIFEST_STATUS="$(api_request GET "$MANIFEST_URL" "$SOURCE_MANIFEST")" if [[ "$MANIFEST_STATUS" != "200" ]] then printf 'Could not fetch selected %s manifest: HTTP %s\n' \ "$CHANNEL" "$MANIFEST_STATUS" >&2 exit 1 fi SIGNATURE_STATUS="$( api_request GET "$MANIFEST_SIGNATURE_URL" "$SOURCE_SIGNATURE" )" if [[ "$SIGNATURE_STATUS" != "200" ]] then printf 'Could not fetch selected %s manifest signature: HTTP %s\n' \ "$CHANNEL" "$SIGNATURE_STATUS" >&2 exit 1 fi VERIFICATION_ARGUMENTS=("$SOURCE_MANIFEST" "$SOURCE_SIGNATURE") if [[ -n "${PTLINUX_UPDATE_PUBLIC_KEY:-}" ]] then VERIFICATION_ARGUMENTS+=(--public-key "$PTLINUX_UPDATE_PUBLIC_KEY") fi "$PYTHON_BIN" "${SCRIPT_ROOT}/verify_metadata.py" \ "${VERIFICATION_ARGUMENTS[@]}" CHANNEL_MANIFEST="${CHANNEL_DIRECTORY}/${CHANNEL}.json" CHANNEL_VERSION="$( "$PYTHON_BIN" "${SCRIPT_ROOT}/prepare_channel_metadata.py" \ "$HISTORY" "$CHANNEL" "$SOURCE_MANIFEST" "$CHANNEL_MANIFEST" )" CHANNEL_SIGNATURE="${CHANNEL_MANIFEST}.sig" CHANNEL_SIGNING_ARGUMENTS=( "$CHANNEL_MANIFEST" "$CHANNEL_SIGNATURE" "${PTLINUX_SIGNING_KEY:?PTLINUX_SIGNING_KEY is required}" ) if [[ -n "${PTLINUX_UPDATE_PUBLIC_KEY:-}" ]] then CHANNEL_SIGNING_ARGUMENTS+=(--public-key "$PTLINUX_UPDATE_PUBLIC_KEY") fi "$PYTHON_BIN" "${SCRIPT_ROOT}/sign_metadata.py" \ "${CHANNEL_SIGNING_ARGUMENTS[@]}" printf '%s\n' "$CHANNEL_VERSION" > "${CHANNEL_DIRECTORY}/${CHANNEL}.version" CHANNELS_TO_PUBLISH+=("$CHANNEL") done 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 } OPERATIONS='[]' for CHANNEL in "${CHANNELS_TO_PUBLISH[@]}" do CHANNEL_MANIFEST="${CHANNEL_DIRECTORY}/${CHANNEL}.json" CHANNEL_SIGNATURE="${CHANNEL_MANIFEST}.sig" CHANNEL_OPERATION="$( file_operation "updates/${CHANNEL}.json" "$(base64 -w 0 -- "$CHANNEL_MANIFEST")" )" CHANNEL_SIGNATURE_OPERATION="$( file_operation "updates/${CHANNEL}.json.sig" \ "$(base64 -w 0 -- "$CHANNEL_SIGNATURE")" )" OPERATIONS="$( jq -cn \ --argjson operations "$OPERATIONS" \ --argjson channel "$CHANNEL_OPERATION" \ --argjson signature "$CHANNEL_SIGNATURE_OPERATION" \ '$operations + [$channel, $signature]' )" done HISTORY_OPERATION="$( file_operation updates/history.json "$(base64 -w 0 -- "$HISTORY")" )" HISTORY_SIGNATURE_OPERATION="$( file_operation updates/history.json.sig "$(base64 -w 0 -- "$HISTORY_SIGNATURE")" )" OPERATIONS="$( jq -cn \ --argjson operations "$OPERATIONS" \ --argjson history "$HISTORY_OPERATION" \ --argjson history_signature "$HISTORY_SIGNATURE_OPERATION" \ '$operations + [$history, $history_signature]' )" PAYLOAD="${RESPONSE_DIRECTORY}/history-payload.json" jq -n \ --arg branch "$RELEASE_BRANCH" \ --arg message 'Publish signed launcher release status' \ --argjson files "$OPERATIONS" \ '{branch:$branch,message:$message,files:$files}' \ > "$PAYLOAD" PUBLISH_RESPONSE="${RESPONSE_DIRECTORY}/history-publish.json" PUBLISH_STATUS="$( api_request POST "${API_ROOT}/contents" "$PUBLISH_RESPONSE" \ --header 'Content-Type: application/json' \ --data-binary "@${PAYLOAD}" )" if [[ "$PUBLISH_STATUS" != "201" ]] then printf 'Signed release-history publication failed: HTTP %s\n' "$PUBLISH_STATUS" >&2 jq -r '.message // empty' "$PUBLISH_RESPONSE" >&2 || true exit 1 fi SUMMARY_FILE="${GITHUB_STEP_SUMMARY:-/dev/null}" { printf '## Published launcher release status\n\n' printf -- '- Generated: `%s`\n' "$GENERATED_AT" printf -- '- History: `updates/history.json`\n' printf -- '- Signature: `updates/history.json.sig`\n' for CHANNEL in "${CHANNELS_TO_PUBLISH[@]}" do printf -- '- %s channel: `%s`\n' \ "$CHANNEL" "$(cat -- "${CHANNEL_DIRECTORY}/${CHANNEL}.version")" done } >> "$SUMMARY_FILE" printf 'Published signed launcher history and status-aware channel metadata.\n'