action.yml.bak 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. name: 'Save Cache'
  2. description: 'Archives and uploads cache to GitHub Releases. Supports both single and multiple cache paths/keys/buckets.'
  3. inputs:
  4. cache_path:
  5. description: 'Single path where cache is saved (use cache_paths for multiple)'
  6. required: false
  7. type: string
  8. cache_paths:
  9. description: 'Multiline list of paths where caches are saved (one per line)'
  10. required: false
  11. type: string
  12. default: ""
  13. cache_key:
  14. description: 'Unique key for the cache (use cache_keys for multiple)'
  15. required: false
  16. type: string
  17. cache_keys:
  18. description: 'Multiline list of unique keys for caches (one per line, must match cache_paths)'
  19. required: false
  20. type: string
  21. default: ""
  22. cache_bucket:
  23. description: 'Single tag name for the release (use cache_buckets for multiple)'
  24. required: false
  25. default: 'general-cache'
  26. cache_buckets:
  27. description: 'Multiline list of tag names for releases (one per line, must match cache_paths)'
  28. required: false
  29. type: string
  30. default: ""
  31. compression_level:
  32. description: 'Compression level (0-9). 0 = No compression, 1 = Fast, 19 = Best.'
  33. required: false
  34. type: number
  35. default: 6
  36. github_token:
  37. description: 'GitHub token for release upload authentication'
  38. required: true
  39. type: string
  40. runs:
  41. using: 'composite'
  42. steps:
  43. - name: Archive, Split, and Upload Cache
  44. shell: bash
  45. env:
  46. GITHUB_TOKEN: ${{ inputs.github_token }}
  47. GH_TOKEN: ${{ inputs.github_token }}
  48. TARGET_REPO: "${{ github.repository }}"
  49. run: |
  50. set -euo pipefail
  51. # Validate and parse inputs
  52. PATHS_INPUT="${{ inputs.cache_paths }}"
  53. KEYS_INPUT="${{ inputs.cache_keys }}"
  54. BUCKETS_INPUT="${{ inputs.cache_buckets }}"
  55. # If multiple inputs are empty, fall back to single inputs
  56. if [ -z "$PATHS_INPUT" ]; then
  57. PATHS_INPUT="${{ inputs.cache_path }}"
  58. fi
  59. if [ -z "$KEYS_INPUT" ]; then
  60. KEYS_INPUT="${{ inputs.cache_key }}"
  61. fi
  62. if [ -z "$BUCKETS_INPUT" ]; then
  63. BUCKETS_INPUT="${{ inputs.cache_bucket }}"
  64. fi
  65. # Split inputs into arrays
  66. IFS=$'\n' read -rd '' -a PATHS_ARRAY <<<"$PATHS_INPUT" || true
  67. IFS=$'\n' read -rd '' -a KEYS_ARRAY <<<"$KEYS_INPUT" || true
  68. IFS=$'\n' read -rd '' -a BUCKETS_ARRAY <<<"$BUCKETS_INPUT" || true
  69. # Validate array lengths match
  70. if [[ ${#PATHS_ARRAY[@]} -ne ${#KEYS_ARRAY[@]} ]] || [[ ${#PATHS_ARRAY[@]} -ne ${#BUCKETS_ARRAY[@]} ]]; then
  71. echo "::error::Mismatch in array lengths: paths=${#PATHS_ARRAY[@]}, keys=${#KEYS_ARRAY[@]}, buckets=${#BUCKETS_ARRAY[@]}"
  72. exit 1
  73. fi
  74. # Git configuration for releases
  75. git config user.name "github-actions[bot]"
  76. git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
  77. TARGET_COMMIT=$(git rev-parse HEAD)
  78. # Helper function to handle release creation
  79. ensure_release() {
  80. local tag_name="$1"
  81. local attempts=0
  82. if gh release view "$tag_name" --repo "$TARGET_REPO" >/dev/null 2>&1; then
  83. echo "Release '$tag_name' already exists. Ready for upload."
  84. gh release edit "$tag_name" --latest=false --repo "$TARGET_REPO" >/dev/null 2>&1 || true
  85. return 0
  86. fi
  87. echo "Tag '$tag_name' not found or release is missing. Creating release..."
  88. gh release create "$tag_name" \
  89. --target "$TARGET_COMMIT" \
  90. --title "Build Cache" \
  91. --notes "Automated storage for build assets" \
  92. --latest=false \
  93. --repo "$TARGET_REPO"
  94. until gh release view "$tag_name" --repo "$TARGET_REPO" >/dev/null 2>&1; do
  95. attempts=$((attempts + 1))
  96. if [ "$attempts" -ge 5 ]; then
  97. echo "::error::Release '$tag_name' is still not visible after creation."
  98. return 1
  99. fi
  100. sleep 3
  101. done
  102. gh release edit "$tag_name" --latest=false --repo "$TARGET_REPO" >/dev/null 2>&1 || true
  103. }
  104. upload_with_retry() {
  105. local file=$1
  106. local tag_name=$2
  107. local max_attempts=3
  108. local timeout_duration="30m" # Increased timeout for large files
  109. for ((i=1; i<=max_attempts; i++)); do
  110. echo " Attempt $i for $file..."
  111. if timeout $timeout_duration gh release upload "$tag_name" "$file" --clobber --repo "$TARGET_REPO"; then
  112. echo " ✅ Successfully uploaded $file"
  113. return 0
  114. else
  115. local exit_code=$?
  116. if [ $exit_code -eq 124 ]; then
  117. echo " ⚠ Attempt $i timed out (timeout after $timeout_duration). Retrying..."
  118. else
  119. echo " ⚠ Attempt $i failed with exit code $exit_code. Retrying..."
  120. fi
  121. fi
  122. if [ $i -lt $max_attempts ]; then
  123. sleep 5
  124. fi
  125. done
  126. return 1
  127. }
  128. # Process each cache entry
  129. for ((idx=0; idx<${#PATHS_ARRAY[@]}; idx++)); do
  130. CACHE_PATH="${PATHS_ARRAY[$idx]}"
  131. CACHE_KEY="${KEYS_ARRAY[$idx]}"
  132. CACHE_BUCKET="${BUCKETS_ARRAY[$idx]}"
  133. [ -z "$CACHE_PATH" ] && continue
  134. TAG_NAME="$CACHE_BUCKET"
  135. BASE_FILENAME="cache-$CACHE_KEY"
  136. if [ "${{ inputs.compression_level }}" -eq 0 ]; then
  137. FILENAME="$BASE_FILENAME.tar"
  138. else
  139. FILENAME="$BASE_FILENAME.tzst"
  140. fi
  141. echo "::group:: [$((idx+1))/${#PATHS_ARRAY[@]}] Checking Release State ($CACHE_BUCKET)"
  142. ensure_release "$TAG_NAME"
  143. echo "::endgroup::"
  144. echo "::group:: [$((idx+1))/${#PATHS_ARRAY[@]}] Archiving Path: $CACHE_PATH"
  145. if [ ! -d "$CACHE_PATH" ] && [ ! -f "$CACHE_PATH" ]; then
  146. echo "⚠️ Target path not found. Skipping cache save."
  147. echo "::endgroup::"
  148. continue
  149. fi
  150. # Print concise summary: total size, top-level entries, and one-level-deep for largest entries
  151. summarize_dir() {
  152. local path="$1"
  153. local top_n=${2:-10}
  154. local sub_n=${3:-5}
  155. if [ ! -e "$path" ]; then
  156. echo "Path $path not found"
  157. return
  158. fi
  159. echo "--- Cache save summary for: $path ---"
  160. du -sh "$path" || true
  161. du -h --max-depth=1 "$path" 2>/dev/null | sort -hr | head -n $top_n || true
  162. echo "Showing one-level-deep details for top entries (up to $sub_n each):"
  163. while read -r line; do
  164. entry_path=$(echo "$line" | awk '{print $2}')
  165. [ -z "$entry_path" ] && continue
  166. if [ "$entry_path" = "$path" ]; then
  167. continue
  168. fi
  169. echo "-> $entry_path :"
  170. du -h --max-depth=1 "$entry_path" 2>/dev/null | sort -hr | head -n $sub_n || true
  171. done < <(du -h --max-depth=1 "$path" 2>/dev/null | sort -hr | head -n $top_n)
  172. echo "-------------------------------------"
  173. }
  174. summarize_dir "$CACHE_PATH" 10 5 || true
  175. DIR_NAME=$(dirname "$CACHE_PATH")
  176. BASE_NAME=$(basename "$CACHE_PATH")
  177. if [ "${{ inputs.compression_level }}" -eq 0 ]; then
  178. tar --posix -h -cvf "$FILENAME" -C "$DIR_NAME" "$BASE_NAME" > /dev/null 2>&1
  179. else
  180. tar --posix -h -I "zstd -T0 -${{ inputs.compression_level }}" -cvf "$FILENAME" -C "$DIR_NAME" "$BASE_NAME" > /dev/null 2>&1
  181. fi
  182. FILE_SIZE=$(stat -c%s "$FILENAME")
  183. echo "Archive created: $FILENAME ($FILE_SIZE bytes)"
  184. echo "::endgroup::"
  185. echo "::group:: [$((idx+1))/${#PATHS_ARRAY[@]}] Uploading Assets ($CACHE_BUCKET)"
  186. MAX_SIZE=2000000000 # ~1.86GB
  187. # Check if cache has changed by comparing with previous release asset
  188. SKIP_UPLOAD=false
  189. if gh release view "$TAG_NAME" --repo "$TARGET_REPO" >/dev/null 2>&1; then
  190. echo "Checking if cache has changed..."
  191. # Try to get the size of the previous asset with the same name
  192. PREV_SIZE=$(gh release view "$TAG_NAME" --repo "$TARGET_REPO" --json assets --jq ".assets[] | select(.name == \"$FILENAME\") | .size" 2>/dev/null || echo "0")
  193. if [ "$PREV_SIZE" = "$FILE_SIZE" ]; then
  194. echo "⚠️ Cache size unchanged ($FILE_SIZE bytes). Skipping upload to save bandwidth."
  195. SKIP_UPLOAD=true
  196. fi
  197. fi
  198. if [ "$SKIP_UPLOAD" = "true" ]; then
  199. rm -f "$FILENAME"
  200. echo "::endgroup::"
  201. continue
  202. fi
  203. if [ "$FILE_SIZE" -gt "$MAX_SIZE" ]; then
  204. echo "⚠️ File > 2GB. Splitting into 1.5GB chunks..."
  205. split -b 1500M -a 2 "$FILENAME" "${FILENAME}.part"
  206. for part in "${FILENAME}".part*; do
  207. echo " Uploading $part..."
  208. if ! upload_with_retry "$part" "$TAG_NAME"; then
  209. echo "::error::Failed to upload part $part after 3 attempts."
  210. exit 1
  211. fi
  212. rm -f "$part"
  213. done
  214. rm -rf "$FILENAME"
  215. else
  216. echo " File size: $((FILE_SIZE / 1024 / 1024))MB"
  217. echo " Uploading $FILENAME..."
  218. if ! upload_with_retry "$FILENAME" "$TAG_NAME"; then
  219. echo "::error::Failed to upload $FILENAME after 3 attempts."
  220. exit 1
  221. fi
  222. rm -f "$FILENAME"
  223. fi
  224. echo "::endgroup::"
  225. done