#!/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 [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}/ # Headers: Authorization: Bearer , X-Git-Hash: # Body: raw artifact bytes # GET {HUSKIES_CHANNEL_URL}/manifest.json # Headers: Authorization: Bearer # 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 [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