Files
kaidi/scripts/publish-gitea-release.sh
T
Qiufeng fa4517c263
Release / release (push) Canceled after 0s
feat: add verified clean reinstall workflow
2026-08-18 01:13:05 +08:00

132 lines
4.9 KiB
Bash
Executable File

#!/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
--connect-timeout 15 --max-time 600 --retry 5 --retry-all-errors --retry-delay 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
purge.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 \
--connect-timeout 15 --max-time 1800 --retry 6 --retry-all-errors --retry-delay 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) == 10' \
"$WORK/published.json" >/dev/null || fail 'published release state is invalid'
printf 'Published Gitea release %s with ten verified assets\n' "$TAG"