#!/usr/bin/env bash set -Eeuo pipefail umask 077 SERVER_URL=${GITEA_SERVER_URL:-} REPOSITORY=${GITEA_REPOSITORY:-} TAG=${GITEA_REF_NAME:-} SOURCE_SHA=${GITEA_SHA:-} TOKEN=${GITEA_TOKEN:-} RELEASE_DIR=${KAIDI_RELEASE_DIR:-dist/release} RELEASE_NAME=${KAIDI_RELEASE_NAME:-Kaidi Finance $TAG} RELEASE_BODY=${KAIDI_RELEASE_BODY:-Kaidi Finance $TAG Source: $SOURCE_SHA} WORK=$(mktemp -d) AUTH_HEADER=$WORK/gitea-auth-header cleanup() { rm -rf "$WORK" } trap cleanup EXIT fail() { printf 'Gitea release failed: %s\n' "$1" >&2 exit 1 } case "$SERVER_URL" in https://*) ;; *) fail 'GITEA_SERVER_URL must use HTTPS' ;; esac [[ "$REPOSITORY" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] \ || fail 'GITEA_REPOSITORY must be OWNER/REPO' [[ "$TAG" =~ ^v[0-9A-Za-z][0-9A-Za-z._+-]{0,127}$ ]] || fail 'GITEA_REF_NAME is invalid' [[ "$SOURCE_SHA" =~ ^[0-9A-Fa-f]{40}$ ]] || fail 'GITEA_SHA must be a 40-character commit SHA' [ -n "$TOKEN" ] && [ "${#TOKEN}" -le 512 ] && [[ "$TOKEN" != *$'\n'* ]] && [[ "$TOKEN" != *$'\r'* ]] \ || fail 'GITEA_TOKEN is invalid' [ -d "$RELEASE_DIR" ] || fail 'release directory is missing' printf 'Authorization: token %s\nAccept: application/json\n' "$TOKEN" > "$AUTH_HEADER" chmod 0600 "$AUTH_HEADER" API="${SERVER_URL%/}/api/v1/repos/$REPOSITORY" ENCODED_TAG=$(jq -rn --arg value "$TAG" '$value | @uri') request() { local method=$1 url=$2 output=$3 input=${4:-} status local args=( --silent --show-error --proto '=https' --tlsv1.2 --request "$method" --header "@$AUTH_HEADER" --output "$output" --write-out '%{http_code}' ) if [ -n "$input" ]; then args+=(--header 'Content-Type: application/json' --data-binary "@$input") fi status=$(curl "${args[@]}" "$url") || fail "$method $url could not be completed" printf '%s\n' "$status" } existing_status=$(request GET "$API/releases/tags/$ENCODED_TAG" "$WORK/existing.json") case "$existing_status" in 404) ;; 200) fail "release $TAG already exists; published releases are immutable" ;; *) fail "release lookup returned HTTP $existing_status" ;; esac jq -n \ --arg tag "$TAG" \ --arg commit "$SOURCE_SHA" \ --arg name "$RELEASE_NAME" \ --arg body "$RELEASE_BODY" \ '{tag_name:$tag,target_commitish:$commit,name:$name,body:$body,draft:true,prerelease:false}' \ > "$WORK/create.json" create_status=$(request POST "$API/releases" "$WORK/release.json" "$WORK/create.json") [ "$create_status" = 201 ] || fail "draft release creation returned HTTP $create_status" RELEASE_ID=$(jq -er '.id | numbers' "$WORK/release.json") || fail 'draft release id is missing' VERSION=${TAG#v} ARTIFACT="kaidi-finance-$VERSION.tar.gz" ASSETS=( "$ARTIFACT" backend-sbom.cdx.json frontend-sbom.cdx.json release-manifest.json release-manifest.sig release-public.pem install.sh bootstrap-checksums.txt SHA256SUMS ) : > "$WORK/expected-assets.jsonl" for name in "${ASSETS[@]}"; do path="$RELEASE_DIR/$name" [ -f "$path" ] && [ ! -L "$path" ] || fail "release asset $name is missing or unsafe" size=$(wc -c < "$path" | tr -d '[:space:]') jq -cn --arg name "$name" --argjson size "$size" '{name:$name,size:$size}' \ >> "$WORK/expected-assets.jsonl" encoded_name=$(jq -rn --arg value "$name" '$value | @uri') upload_status=$(curl --silent --show-error --proto '=https' --tlsv1.2 \ --header "@$AUTH_HEADER" \ --form "attachment=@$path;type=application/octet-stream" \ --output "$WORK/upload.json" --write-out '%{http_code}' \ "$API/releases/$RELEASE_ID/assets?name=$encoded_name") \ || fail "asset $name upload could not be completed" [ "$upload_status" = 201 ] || fail "asset $name upload returned HTTP $upload_status" done jq -s 'sort_by(.name)' "$WORK/expected-assets.jsonl" > "$WORK/expected-assets.json" verify_status=$(request GET "$API/releases/$RELEASE_ID" "$WORK/verify.json") [ "$verify_status" = 200 ] || fail "draft release verification returned HTTP $verify_status" jq -e --slurpfile expected "$WORK/expected-assets.json" \ '([.assets[] | {name,size}] | sort_by(.name)) == $expected[0]' "$WORK/verify.json" >/dev/null \ || fail 'uploaded release asset names or sizes do not match the signed local set' jq -n \ --arg commit "$SOURCE_SHA" \ --arg name "$RELEASE_NAME" \ --arg body "$RELEASE_BODY" \ '{target_commitish:$commit,name:$name,body:$body,draft:false,prerelease:false}' \ > "$WORK/publish.json" publish_status=$(request PATCH "$API/releases/$RELEASE_ID" "$WORK/published.json" "$WORK/publish.json") [ "$publish_status" = 200 ] || fail "release publication returned HTTP $publish_status" jq -e '.draft == false and .prerelease == false and (.assets | length) == 9' \ "$WORK/published.json" >/dev/null || fail 'published release state is invalid' printf 'Published Gitea release %s with nine verified assets\n' "$TAG"