#!/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 --path [--retention-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 --path [--retention-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/dev/null || echo "$input" | base64 -D 2>/dev/null } # Extract backend IDs from the JWT token's scp claim # Format: "Actions.Results::" 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='' for bid in "${BLOCK_IDS[@]}"; do BLOCK_LIST_XML+="${bid}" done BLOCK_LIST_XML+='' 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})"