claude script to upload artifacts
Build Mumble Web 2 / linux_build (push) Successful in 1m25s
Build Mumble Web 2 / android_build (push) Successful in 6m18s
Build Mumble Web 2 / windows_build (push) Successful in 9m44s
Build Mumble Web 2 / macos_build (push) Failing after 10m6s

This commit is contained in:
2026-03-10 21:03:51 -06:00
parent cba1187f2d
commit 52e48efdcf
2 changed files with 288 additions and 5 deletions
+1 -5
View File
@@ -81,11 +81,7 @@ jobs:
tar -czf mumble-web2-gui-macos-arm64.tar.gz -C gui dist
- name: Upload mumble-web2-gui Artifact
uses: https://gitea.com/actions/gitea-upload-artifact@v4
with:
name: mumble-web2-gui-macos-arm64
path: mumble-web2-gui-macos-arm64.tar.gz
retention-days: 5
run: ./scripts/upload-artifact.sh --name mumble-web2-gui-macos-arm64 --path mumble-web2-gui-macos-arm64.tar.gz
windows_build:
runs-on: windows
+287
View File
@@ -0,0 +1,287 @@
#!/usr/bin/env bash
#
# upload-artifact.sh - Upload artifacts to Gitea Actions using the v4 artifact protocol.
# Replaces gitea-upload-artifact@v4 action with a simple shell script.
#
# Usage:
# ./upload-artifact.sh --name <artifact-name> --path <file-or-dir> [--retention-days <days>]
#
# Required environment variables (set automatically by Gitea Actions runner):
# ACTIONS_RUNTIME_TOKEN - JWT bearer token
# ACTIONS_RESULTS_URL - Artifact service base URL
#
set -euo pipefail
CHUNK_SIZE=$((8 * 1024 * 1024)) # 8 MB
# ---------- argument parsing ----------
ARTIFACT_NAME=""
ARTIFACT_PATH=""
RETENTION_DAYS=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) ARTIFACT_NAME="$2"; shift 2 ;;
--path) ARTIFACT_PATH="$2"; shift 2 ;;
--retention-days) RETENTION_DAYS="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done
if [[ -z "$ARTIFACT_NAME" || -z "$ARTIFACT_PATH" ]]; then
echo "Usage: $0 --name <name> --path <path> [--retention-days <days>]" >&2
exit 1
fi
if [[ -z "${ACTIONS_RUNTIME_TOKEN:-}" ]]; then
echo "Error: ACTIONS_RUNTIME_TOKEN is not set" >&2
exit 1
fi
if [[ -z "${ACTIONS_RESULTS_URL:-}" ]]; then
echo "Error: ACTIONS_RESULTS_URL is not set" >&2
exit 1
fi
# ---------- helpers ----------
# Decode base64url (JWT uses base64url without padding)
base64url_decode() {
local input="$1"
# Replace URL-safe chars with standard base64 chars
input="${input//-/+}"
input="${input//_//}"
# Add padding
local pad=$(( 4 - ${#input} % 4 ))
if [[ $pad -lt 4 ]]; then
for ((i=0; i<pad; i++)); do input+="="; done
fi
echo "$input" | base64 -d 2>/dev/null || echo "$input" | base64 -D 2>/dev/null
}
# Extract backend IDs from the JWT token's scp claim
# Format: "Actions.Results:<runBackendId>:<jobBackendId>"
extract_backend_ids() {
local token="$ACTIONS_RUNTIME_TOKEN"
# JWT has 3 parts separated by dots; payload is the second
local payload
payload=$(echo "$token" | cut -d'.' -f2)
local decoded
decoded=$(base64url_decode "$payload")
# Extract the scp claim - look for the Actions.Results entry
local scp_value
# Try jq first, fall back to manual parsing
if command -v jq &>/dev/null; then
scp_value=$(echo "$decoded" | jq -r '.scp // empty')
else
# Simple extraction: find "scp":"..." or "scp": "..."
scp_value=$(echo "$decoded" | sed -n 's/.*"scp"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
fi
if [[ -z "$scp_value" ]]; then
echo "Error: Could not extract scp claim from token" >&2
exit 1
fi
# The scp may contain multiple space-separated scopes
# Find the one starting with "Actions.Results:"
local results_scope=""
for scope in $scp_value; do
if [[ "$scope" == Actions.Results:* ]]; then
results_scope="$scope"
break
fi
done
if [[ -z "$results_scope" ]]; then
echo "Error: No Actions.Results scope found in token" >&2
exit 1
fi
WORKFLOW_RUN_BACKEND_ID=$(echo "$results_scope" | cut -d':' -f2)
WORKFLOW_JOB_RUN_BACKEND_ID=$(echo "$results_scope" | cut -d':' -f3)
if [[ -z "$WORKFLOW_RUN_BACKEND_ID" || -z "$WORKFLOW_JOB_RUN_BACKEND_ID" ]]; then
echo "Error: Could not parse backend IDs from scope: $results_scope" >&2
exit 1
fi
}
# Strip trailing slash from URL
normalize_url() {
echo "${1%/}"
}
# ---------- main flow ----------
echo "==> Uploading artifact: $ARTIFACT_NAME"
echo " Path: $ARTIFACT_PATH"
# 1. Extract backend IDs from JWT
extract_backend_ids
echo " Run backend ID: $WORKFLOW_RUN_BACKEND_ID"
echo " Job backend ID: $WORKFLOW_JOB_RUN_BACKEND_ID"
BASE_URL=$(normalize_url "$ACTIONS_RESULTS_URL")
TWIRP_BASE="${BASE_URL}/twirp/github.actions.results.api.v1.ArtifactService"
# 2. Create zip of the artifact content
TMPDIR_UPLOAD=$(mktemp -d)
trap 'rm -rf "$TMPDIR_UPLOAD"' EXIT
ZIP_FILE="${TMPDIR_UPLOAD}/artifact.zip"
if [[ -f "$ARTIFACT_PATH" ]]; then
# Single file - zip it at the top level
PARENT_DIR=$(dirname "$ARTIFACT_PATH")
FILE_NAME=$(basename "$ARTIFACT_PATH")
(cd "$PARENT_DIR" && zip -q "$ZIP_FILE" "$FILE_NAME")
elif [[ -d "$ARTIFACT_PATH" ]]; then
# Directory - zip its contents
(cd "$ARTIFACT_PATH" && zip -qr "$ZIP_FILE" .)
else
echo "Error: Path does not exist: $ARTIFACT_PATH" >&2
exit 1
fi
ZIP_SIZE=$(wc -c < "$ZIP_FILE" | tr -d ' ')
echo " Zip size: $ZIP_SIZE bytes"
# Compute SHA-256 hash of the zip
if command -v sha256sum &>/dev/null; then
ZIP_HASH=$(sha256sum "$ZIP_FILE" | cut -d' ' -f1)
elif command -v shasum &>/dev/null; then
ZIP_HASH=$(shasum -a 256 "$ZIP_FILE" | cut -d' ' -f1)
else
echo "Error: Neither sha256sum nor shasum found" >&2
exit 1
fi
echo " SHA-256: $ZIP_HASH"
# 3. CreateArtifact - get signed upload URL
echo "==> Creating artifact..."
CREATE_BODY="{\"workflow_run_backend_id\":\"${WORKFLOW_RUN_BACKEND_ID}\",\"workflow_job_run_backend_id\":\"${WORKFLOW_JOB_RUN_BACKEND_ID}\",\"name\":\"${ARTIFACT_NAME}\",\"version\":4}"
CREATE_RESPONSE=$(curl -sS -X POST \
"${TWIRP_BASE}/CreateArtifact" \
-H "Authorization: Bearer ${ACTIONS_RUNTIME_TOKEN}" \
-H "Content-Type: application/json" \
-d "$CREATE_BODY")
echo " Create response: $CREATE_RESPONSE"
# Extract signed upload URL
if command -v jq &>/dev/null; then
SIGNED_URL=$(echo "$CREATE_RESPONSE" | jq -r '.signed_upload_url // .signedUploadUrl // empty')
CREATE_OK=$(echo "$CREATE_RESPONSE" | jq -r '.ok // empty')
else
# Fallback: extract URL from JSON manually
SIGNED_URL=$(echo "$CREATE_RESPONSE" | sed -n 's/.*"signed_upload_url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
if [[ -z "$SIGNED_URL" ]]; then
SIGNED_URL=$(echo "$CREATE_RESPONSE" | sed -n 's/.*"signedUploadUrl"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
fi
CREATE_OK="true"
fi
if [[ -z "$SIGNED_URL" ]]; then
echo "Error: Failed to get signed upload URL" >&2
echo "Response: $CREATE_RESPONSE" >&2
exit 1
fi
echo " Got signed upload URL"
# 4. Upload zip in chunks using Azure Blob block protocol
NUM_CHUNKS=$(( (ZIP_SIZE + CHUNK_SIZE - 1) / CHUNK_SIZE ))
echo "==> Uploading artifact data (${ZIP_SIZE} bytes in ${NUM_CHUNKS} chunk(s) of up to ${CHUNK_SIZE} bytes)..."
# Split the zip into chunks
CHUNK_PREFIX="${TMPDIR_UPLOAD}/chunk_"
split -b "$CHUNK_SIZE" "$ZIP_FILE" "$CHUNK_PREFIX"
BLOCK_IDS=()
CHUNK_INDEX=0
for CHUNK_FILE in "${CHUNK_PREFIX}"*; do
THIS_CHUNK=$(wc -c < "$CHUNK_FILE" | tr -d ' ')
# Generate block ID: zero-padded index, base64-encoded
BLOCK_ID_RAW=$(printf "%05d" "$CHUNK_INDEX")
BLOCK_ID=$(echo -n "$BLOCK_ID_RAW" | base64)
BLOCK_IDS+=("$BLOCK_ID")
# URL-encode the block ID for the query parameter
BLOCK_ID_ENCODED=$(echo "$BLOCK_ID" | sed 's/+/%2B/g; s/\//%2F/g; s/=/%3D/g')
UPLOAD_URL="${SIGNED_URL}&comp=block&blockid=${BLOCK_ID_ENCODED}"
HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" -X PUT \
"$UPLOAD_URL" \
-H "Content-Type: application/octet-stream" \
-H "Content-Length: ${THIS_CHUNK}" \
--data-binary "@${CHUNK_FILE}")
if [[ "$HTTP_CODE" != "201" ]]; then
echo "Error: Chunk upload failed with HTTP $HTTP_CODE (chunk $CHUNK_INDEX)" >&2
exit 1
fi
rm -f "$CHUNK_FILE"
echo " Uploaded chunk $((CHUNK_INDEX + 1))/${NUM_CHUNKS} ($THIS_CHUNK bytes)"
CHUNK_INDEX=$((CHUNK_INDEX + 1))
done
# 5. Commit block list
echo "==> Committing block list (${#BLOCK_IDS[@]} blocks)..."
BLOCK_LIST_XML='<?xml version="1.0" encoding="UTF-8" standalone="yes"?><BlockList>'
for bid in "${BLOCK_IDS[@]}"; do
BLOCK_LIST_XML+="<Latest>${bid}</Latest>"
done
BLOCK_LIST_XML+='</BlockList>'
BLOCKLIST_URL="${SIGNED_URL}&comp=blocklist"
HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" -X PUT \
"$BLOCKLIST_URL" \
-H "Content-Type: application/xml" \
-d "$BLOCK_LIST_XML")
if [[ "$HTTP_CODE" != "201" ]]; then
echo "Error: Block list commit failed with HTTP $HTTP_CODE" >&2
exit 1
fi
echo " Block list committed"
# 6. FinalizeArtifact
echo "==> Finalizing artifact..."
FINALIZE_BODY="{\"workflow_run_backend_id\":\"${WORKFLOW_RUN_BACKEND_ID}\",\"workflow_job_run_backend_id\":\"${WORKFLOW_JOB_RUN_BACKEND_ID}\",\"name\":\"${ARTIFACT_NAME}\",\"size\":\"${ZIP_SIZE}\",\"hash\":{\"value\":\"sha256:${ZIP_HASH}\"}}"
FINALIZE_RESPONSE=$(curl -sS -X POST \
"${TWIRP_BASE}/FinalizeArtifact" \
-H "Authorization: Bearer ${ACTIONS_RUNTIME_TOKEN}" \
-H "Content-Type: application/json" \
-d "$FINALIZE_BODY")
echo " Finalize response: $FINALIZE_RESPONSE"
# Check success
if command -v jq &>/dev/null; then
FINALIZE_OK=$(echo "$FINALIZE_RESPONSE" | jq -r '.ok // empty')
ARTIFACT_ID=$(echo "$FINALIZE_RESPONSE" | jq -r '.artifact_id // .artifactId // empty')
else
FINALIZE_OK=$(echo "$FINALIZE_RESPONSE" | sed -n 's/.*"ok"[[:space:]]*:[[:space:]]*\(true\|false\).*/\1/p')
ARTIFACT_ID=$(echo "$FINALIZE_RESPONSE" | sed -n 's/.*"artifact_id"[[:space:]]*:[[:space:]]*"\{0,1\}\([0-9]*\)"\{0,1\}.*/\1/p')
fi
if [[ "$FINALIZE_OK" != "true" ]]; then
echo "Error: Finalize failed" >&2
echo "Response: $FINALIZE_RESPONSE" >&2
exit 1
fi
echo "==> Artifact '$ARTIFACT_NAME' uploaded successfully (ID: ${ARTIFACT_ID:-unknown})"