Add channel-aware release tooling and updater recovery
This commit is contained in:
280
scripts/publish-release-history.sh
Executable file
280
scripts/publish-release-history.sh
Executable file
@@ -0,0 +1,280 @@
|
||||
#!/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'
|
||||
Reference in New Issue
Block a user