Files
huskies/script/ci-publish-artifact
T

122 lines
4.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# script/ci-publish-artifact — upload a built sled artifact to a release
# channel and verify the channel's manifest reflects the published commit.
#
# Usage: script/ci-publish-artifact <artifact-path> [git-hash]
#
# git-hash defaults to `git rev-parse --short HEAD` when omitted.
#
# Required env:
# HUSKIES_CHANNEL_URL Base URL of the release channel (Gitea secret).
# HUSKIES_CHANNEL_TOKEN Bearer token authorised to publish (Gitea secret).
#
# Protocol against the channel host:
# POST {HUSKIES_CHANNEL_URL}/<artifact-name>
# Headers: Authorization: Bearer <token>, X-Git-Hash: <hash>
# Body: raw artifact bytes
# GET {HUSKIES_CHANNEL_URL}/manifest.json
# Headers: Authorization: Bearer <token>
# Body: JSON object with a "git_hash" field
#
# 5xx responses and network errors are retried with backoff; 4xx responses
# (including auth failures) fail immediately since retrying won't fix them.
set -euo pipefail
ARTIFACT_PATH="${1:?Usage: script/ci-publish-artifact <artifact-path> [git-hash]}"
GIT_HASH="${2:-$(git rev-parse --short HEAD)}"
if [ -z "${HUSKIES_CHANNEL_URL:-}" ]; then
echo "Error: HUSKIES_CHANNEL_URL is not set." >&2
exit 1
fi
if [ -z "${HUSKIES_CHANNEL_TOKEN:-}" ]; then
echo "Error: HUSKIES_CHANNEL_TOKEN is not set." >&2
exit 1
fi
if [ ! -f "$ARTIFACT_PATH" ]; then
echo "Error: artifact not found at $ARTIFACT_PATH" >&2
exit 1
fi
ARTIFACT_NAME="$(basename "$ARTIFACT_PATH")"
CHANNEL_URL="${HUSKIES_CHANNEL_URL%/}"
UPLOAD_URL="${CHANNEL_URL}/${ARTIFACT_NAME}"
MANIFEST_URL="${CHANNEL_URL}/manifest.json"
UPLOAD_MAX_ATTEMPTS="${HUSKIES_CI_PUBLISH_MAX_ATTEMPTS:-3}"
UPLOAD_BACKOFF_SECS="${HUSKIES_CI_PUBLISH_BACKOFF_SECS:-1}"
MANIFEST_MAX_ATTEMPTS=3
MANIFEST_BACKOFF_SECS=1
RESPONSE_FILE="$(mktemp)"
trap 'rm -f "$RESPONSE_FILE"' EXIT
# ── Upload ────────────────────────────────────────────────────────────────
attempt=1
while :; do
echo "==> Uploading ${ARTIFACT_NAME} (${GIT_HASH}), attempt ${attempt}/${UPLOAD_MAX_ATTEMPTS}..."
HTTP_CODE=$(curl -sS --connect-timeout 10 --max-time 60 \
-o "$RESPONSE_FILE" -w "%{http_code}" \
-X POST \
-H "Authorization: Bearer ${HUSKIES_CHANNEL_TOKEN}" \
-H "X-Git-Hash: ${GIT_HASH}" \
--data-binary "@${ARTIFACT_PATH}" \
"${UPLOAD_URL}") || HTTP_CODE="000"
RESPONSE_BODY="$(cat "$RESPONSE_FILE" 2>/dev/null || true)"
case "$HTTP_CODE" in
2??)
echo "==> Upload succeeded (HTTP ${HTTP_CODE})."
break
;;
401|403)
echo "Error: upload rejected — authentication failed (HTTP ${HTTP_CODE})." >&2
echo "Response: ${RESPONSE_BODY}" >&2
exit 1
;;
4??)
echo "Error: upload rejected by the channel (HTTP ${HTTP_CODE}); not retrying a client error." >&2
echo "Response: ${RESPONSE_BODY}" >&2
exit 1
;;
esac
if [ "$attempt" -ge "$UPLOAD_MAX_ATTEMPTS" ]; then
echo "Error: upload failed after ${UPLOAD_MAX_ATTEMPTS} attempts (last HTTP ${HTTP_CODE})." >&2
echo "Response: ${RESPONSE_BODY}" >&2
exit 1
fi
echo "==> Transient failure (HTTP ${HTTP_CODE}); retrying in ${UPLOAD_BACKOFF_SECS}s..."
sleep "$UPLOAD_BACKOFF_SECS"
attempt=$((attempt + 1))
UPLOAD_BACKOFF_SECS=$((UPLOAD_BACKOFF_SECS * 2))
done
# ── Verify manifest ──────────────────────────────────────────────────────
attempt=1
while :; do
echo "==> Verifying channel manifest reflects ${GIT_HASH} (attempt ${attempt}/${MANIFEST_MAX_ATTEMPTS})..."
MANIFEST_JSON=$(curl -sS --connect-timeout 10 --max-time 30 \
-H "Authorization: Bearer ${HUSKIES_CHANNEL_TOKEN}" \
"${MANIFEST_URL}") || MANIFEST_JSON=""
MANIFEST_HASH=$(printf '%s' "$MANIFEST_JSON" \
| python3 -c "import sys,json; print(json.load(sys.stdin).get('git_hash',''))" 2>/dev/null || echo "")
if [ "$MANIFEST_HASH" = "$GIT_HASH" ]; then
echo "==> Published ${ARTIFACT_NAME} (${GIT_HASH}) to ${CHANNEL_URL}; manifest verified."
exit 0
fi
if [ "$attempt" -ge "$MANIFEST_MAX_ATTEMPTS" ]; then
echo "Error: manifest mismatch — channel reports git_hash '${MANIFEST_HASH}', expected '${GIT_HASH}'." >&2
echo "Manifest: ${MANIFEST_JSON}" >&2
exit 1
fi
sleep "$MANIFEST_BACKOFF_SECS"
attempt=$((attempt + 1))
MANIFEST_BACKOFF_SECS=$((MANIFEST_BACKOFF_SECS * 2))
done