131 lines
4.9 KiB
Bash
Executable File
131 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
|
|
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) == 9' \
|
|
"$WORK/published.json" >/dev/null || fail 'published release state is invalid'
|
|
|
|
printf 'Published Gitea release %s with nine verified assets\n' "$TAG"
|