183 lines
6.5 KiB
Bash
183 lines
6.5 KiB
Bash
#!/usr/bin/env bash
|
|
# Publish a built artifact to the SilverDESK App Catalogue.
|
|
#
|
|
# NOTE: the silverdesk copy (tools/catalogue/publish-to-catalogue.sh) is the operator
|
|
# copy; this one is used by CI. Keep the two in step.
|
|
#
|
|
# Usage:
|
|
# publish-to-catalogue.sh --slug <s> --platform <android|macos|linux|windows> \
|
|
# --arch <a> --format <apk|dmg|zip|deb|appimage|msi|exe|msix|pwa|nupkg> \
|
|
# --version <v> --version-code <n> --channel <alpha|beta|rc|stable> \
|
|
# [--notes-file f] [--signer-identity id] [--sparkle-signature sig] \
|
|
# [--dry-run] <file>
|
|
#
|
|
# Environment:
|
|
# CATALOGUE_PUBLISH_KEY publish key (required; never printed)
|
|
# CATALOGUE_URL base URL (default https://desk.silverlined.uk)
|
|
#
|
|
# Exit codes: 0 = created/added (201) or promoted (200: same bytes re-published
|
|
# to a higher channel), 3 = already published (409, safe to rerun),
|
|
# 1 = any other failure (server message printed).
|
|
#
|
|
# The uploaded FileName is the local file's base name sanitised to the server's
|
|
# safe-name rule (spaces become '-', other unsafe characters are dropped); the
|
|
# local path itself is used as given. Release notes are limited to 500 chars.
|
|
set -euo pipefail
|
|
|
|
DEFAULT_URL="https://desk.silverlined.uk"
|
|
|
|
usage() {
|
|
sed -n '2,23p' "$0" | sed 's/^# \{0,1\}//'
|
|
}
|
|
|
|
die() {
|
|
echo "error: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
# The server's IsSafeName: ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ and no "..".
|
|
sanitize_name() {
|
|
local n="$1"
|
|
n="${n// /-}"
|
|
n="$(printf '%s' "$n" | LC_ALL=C tr -cd 'A-Za-z0-9._-')"
|
|
while [[ "$n" == *..* ]]; do n="${n//../.}"; done
|
|
while [ -n "$n" ] && [[ ! "$n" =~ ^[A-Za-z0-9] ]]; do n="${n:1}"; done
|
|
printf '%s' "${n:0:128}"
|
|
}
|
|
|
|
SLUG=""; PLATFORM=""; ARCH=""; FORMAT=""; VERSION=""; VERSION_CODE=""
|
|
CHANNEL=""; NOTES_FILE=""; SIGNER=""; SPARKLE_SIG=""; DRY_RUN=0; FILE=""
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--slug) SLUG="${2:-}"; shift 2 ;;
|
|
--platform) PLATFORM="${2:-}"; shift 2 ;;
|
|
--arch) ARCH="${2:-}"; shift 2 ;;
|
|
--format) FORMAT="${2:-}"; shift 2 ;;
|
|
--version) VERSION="${2:-}"; shift 2 ;;
|
|
--version-code) VERSION_CODE="${2:-}"; shift 2 ;;
|
|
--channel) CHANNEL="${2:-}"; shift 2 ;;
|
|
--notes-file) NOTES_FILE="${2:-}"; shift 2 ;;
|
|
--signer-identity) SIGNER="${2:-}"; shift 2 ;;
|
|
--sparkle-signature) SPARKLE_SIG="${2:-}"; shift 2 ;;
|
|
--dry-run) DRY_RUN=1; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
--*) die "unknown option: $1" ;;
|
|
*) FILE="$1"; shift ;;
|
|
esac
|
|
done
|
|
|
|
[ -n "$SLUG" ] || die "--slug is required"
|
|
[ -n "$ARCH" ] || die "--arch is required"
|
|
[ -n "$VERSION" ] || die "--version is required"
|
|
[ -n "$VERSION_CODE" ] || die "--version-code is required"
|
|
[ -n "$FILE" ] || die "artifact file is required"
|
|
[ -f "$FILE" ] || die "artifact not found: $FILE"
|
|
NOTES=""
|
|
if [ -n "$NOTES_FILE" ]; then
|
|
[ -f "$NOTES_FILE" ] || die "notes file not found: $NOTES_FILE"
|
|
NOTES="$(cat "$NOTES_FILE")"
|
|
NOTES_LEN="$(printf '%s' "$NOTES" | LC_ALL=C.UTF-8 wc -m 2>/dev/null | tr -d ' ')"
|
|
[ "${NOTES_LEN:-0}" -le 500 ] || die "release notes are $NOTES_LEN characters; the catalogue allows at most 500"
|
|
fi
|
|
|
|
# Map lowercase CLI values to the server's enum names.
|
|
case "$PLATFORM" in
|
|
android) PLATFORM_ENUM="Android" ;;
|
|
macos) PLATFORM_ENUM="MacOS" ;;
|
|
linux) PLATFORM_ENUM="Linux" ;;
|
|
windows) PLATFORM_ENUM="Windows" ;;
|
|
*) die "unknown --platform '$PLATFORM' (android|macos|linux|windows)" ;;
|
|
esac
|
|
|
|
case "$FORMAT" in
|
|
apk) FORMAT_ENUM="Apk" ;;
|
|
dmg) FORMAT_ENUM="Dmg" ;;
|
|
zip) FORMAT_ENUM="Zip" ;;
|
|
deb) FORMAT_ENUM="Deb" ;;
|
|
appimage) FORMAT_ENUM="AppImage" ;;
|
|
msi) FORMAT_ENUM="Msi" ;;
|
|
exe) FORMAT_ENUM="Exe" ;;
|
|
msix) FORMAT_ENUM="Msix" ;;
|
|
pwa) FORMAT_ENUM="Pwa" ;;
|
|
nupkg) FORMAT_ENUM="Nupkg" ;;
|
|
*) die "unknown --format '$FORMAT' (apk|dmg|zip|deb|appimage|msi|exe|msix|pwa|nupkg)" ;;
|
|
esac
|
|
|
|
case "$CHANNEL" in
|
|
alpha) CHANNEL_ENUM="Alpha" ;;
|
|
beta) CHANNEL_ENUM="Beta" ;;
|
|
rc) CHANNEL_ENUM="ReleaseCandidate" ;;
|
|
stable) CHANNEL_ENUM="Stable" ;;
|
|
*) die "unknown --channel '$CHANNEL' (alpha|beta|rc|stable)" ;;
|
|
esac
|
|
|
|
BASE_URL="${CATALOGUE_URL:-$DEFAULT_URL}"
|
|
BASE_URL="${BASE_URL%/}"
|
|
URL="$BASE_URL/api/catalogue/publish"
|
|
FILE_NAME="$(sanitize_name "$(basename "$FILE")")"
|
|
[ -n "$FILE_NAME" ] || die "cannot derive a safe upload file name from: $(basename "$FILE")"
|
|
|
|
# Build the multipart field list as an array so values with spaces survive.
|
|
# Text fields use --form-string so a leading @ or < is never read as a file.
|
|
FIELDS=(
|
|
--form-string "AppSlug=$SLUG"
|
|
--form-string "Platform=$PLATFORM_ENUM"
|
|
--form-string "Arch=$ARCH"
|
|
--form-string "Format=$FORMAT_ENUM"
|
|
--form-string "Version=$VERSION"
|
|
--form-string "VersionCode=$VERSION_CODE"
|
|
--form-string "Channel=$CHANNEL_ENUM"
|
|
--form-string "FileName=$FILE_NAME"
|
|
)
|
|
[ -n "$SIGNER" ] && FIELDS+=(--form-string "SignerIdentity=$SIGNER")
|
|
[ -n "$SPARKLE_SIG" ] && FIELDS+=(--form-string "SparkleEdSignature=$SPARKLE_SIG")
|
|
[ -n "$NOTES" ] && FIELDS+=(--form-string "ReleaseNotes=$NOTES")
|
|
# The file part must be last (the server reads every field before streaming it).
|
|
# The path is quoted for curl so ';' or ',' in it are not parsed as part options.
|
|
FILE_ESC="${FILE//\\/\\\\}"
|
|
FILE_ESC="${FILE_ESC//\"/\\\"}"
|
|
FIELDS+=(-F "file=@\"$FILE_ESC\";filename=$FILE_NAME")
|
|
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
# Print the command with the key redacted; the key never appears in output.
|
|
printf 'curl -sS -X POST'
|
|
printf ' -H "%s"' "Authorization: Bearer ***"
|
|
for arg in "${FIELDS[@]}"; do printf ' %q' "$arg"; done
|
|
printf ' %q\n' "$URL"
|
|
exit 0
|
|
fi
|
|
|
|
[ -n "${CATALOGUE_PUBLISH_KEY:-}" ] || die "CATALOGUE_PUBLISH_KEY is not set"
|
|
|
|
# Pass the bearer header through a curl config read from a process
|
|
# substitution, so the key never lands in argv or the shell trace.
|
|
BODY_FILE="$(mktemp)"
|
|
trap 'rm -f "$BODY_FILE"' EXIT
|
|
HTTP_CODE="$(curl -sS -o "$BODY_FILE" -w '%{http_code}' -X POST \
|
|
-K <(printf 'header = "Authorization: Bearer %s"\n' "$CATALOGUE_PUBLISH_KEY") \
|
|
"${FIELDS[@]}" "$URL")" || die "request to $URL failed"
|
|
|
|
BODY="$(cat "$BODY_FILE")"
|
|
MESSAGE="$(printf '%s' "$BODY" | sed -n 's/.*"message" *: *"\([^"]*\)".*/\1/p' | head -n 1)"
|
|
[ -n "$MESSAGE" ] || MESSAGE="$BODY"
|
|
|
|
case "$HTTP_CODE" in
|
|
201)
|
|
echo "published $SLUG $VERSION ($CHANNEL_ENUM, $PLATFORM_ENUM/$ARCH)"
|
|
exit 0
|
|
;;
|
|
200)
|
|
echo "promoted $SLUG $VERSION to $CHANNEL_ENUM ($PLATFORM_ENUM/$ARCH): identical file already published"
|
|
exit 0
|
|
;;
|
|
409)
|
|
echo "already published: $SLUG $VERSION ($CHANNEL_ENUM, $PLATFORM_ENUM/$ARCH): $MESSAGE"
|
|
exit 3
|
|
;;
|
|
*)
|
|
echo "publish failed (HTTP $HTTP_CODE): $MESSAGE" >&2
|
|
exit 1
|
|
;;
|
|
esac
|