name: 'Cache' description: 'Cache build files under ~/.cache/ using a key file hash' inputs: path: description: 'A list of files, directories, or wildcard patterns to cache. Defaults to ~/.cache/' required: false default: '~/.cache/' key-prefix: description: 'Prefix for the cache key, combined with key-file hash' required: true key-file: description: 'The file to hash for generating the cache key' required: true restore-keys: description: 'An ordered list of prefix-matched keys for restoring stale cache' required: false default: '' outputs: cache-hit: description: 'A boolean value to indicate an exact match was found for the key' value: ${{ steps.restore-cache.outputs.cache-hit }} cache-primary-key: description: 'The primary key used for cache lookup' value: ${{ steps.restore-cache.outputs.cache-primary-key }} cache-matched-key: description: 'The key of the cache entry that was restored' value: ${{ steps.restore-cache.outputs.cache-matched-key }} runs: using: "composite" steps: - name: Generate cache key id: gen-key shell: bash run: | KEY_HASH=$(sha256sum "${{ inputs.key-file }}" | awk '{print $1}') echo "cache-key=${{ inputs.key-prefix }}-${KEY_HASH}" >> $GITHUB_OUTPUT - name: Restore cache id: restore-cache shell: bash env: CACHE_KEY: ${{ steps.gen-key.outputs.cache-key }} RESTORE_KEYS: ${{ inputs.restore-keys }} CACHE_PATH: ${{ inputs.path }} run: | CACHE_URL="${ACTIONS_CACHE_URL:-${ACTIONS_RUNTIME_URL:-}}" TOKEN="${ACTIONS_RUNTIME_TOKEN:-}" if [ -z "$CACHE_URL" ] || [ -z "$TOKEN" ]; then echo "::warning::Cache environment variables not set. Skipping cache restore." echo "cache-hit=false" >> $GITHUB_OUTPUT echo "cache-primary-key=${CACHE_KEY}" >> $GITHUB_OUTPUT echo "cache-matched-key=" >> $GITHUB_OUTPUT exit 0 fi CACHE_URL="${CACHE_URL%/}/" API_BASE="${CACHE_URL}_apis/artifactcache/" EXPANDED_PATH=$(eval echo "$CACHE_PATH") PATHS_HASH=$(echo "$EXPANDED_PATH" | sha256sum | awk '{print $1}') CACHE_VERSION="zstd-${PATHS_HASH}" PRIMARY_KEY="${CACHE_KEY}" KEYS="$PRIMARY_KEY" if [ -n "$RESTORE_KEYS" ]; then while IFS= read -r rk; do if [ -n "$rk" ]; then KEYS="${KEYS},${rk}" fi done <<< "$RESTORE_KEYS" fi ENCODED_KEYS=$(echo "$KEYS" | sed 's/,/%2C/g') echo "Looking up cache with keys: ${KEYS}" HTTP_CODE=$(mktemp) CACHE_ENTRY=$(mktemp) curl -sL -w "%{http_code}" -o "$CACHE_ENTRY" \ -H "Authorization: Bearer ${TOKEN}" \ "${API_BASE}cache?keys=${ENCODED_KEYS}&version=${CACHE_VERSION}" \ 2>/dev/null > "$HTTP_CODE" || true STATUS=$(cat "$HTTP_CODE") rm -f "$HTTP_CODE" if [ "$STATUS" = "204" ] || [ "$STATUS" = "404" ] || [ ! -s "$CACHE_ENTRY" ]; then echo "Cache not found for keys: ${KEYS}" rm -f "$CACHE_ENTRY" echo "cache-hit=false" >> $GITHUB_OUTPUT echo "cache-primary-key=${PRIMARY_KEY}" >> $GITHUB_OUTPUT echo "cache-matched-key=" >> $GITHUB_OUTPUT exit 0 fi ARCHIVE_LOCATION=$(grep -o '"archiveLocation":"[^"]*"' "$CACHE_ENTRY" | head -1 | cut -d'"' -f4) MATCHED_KEY=$(grep -o '"cacheKey":"[^"]*"' "$CACHE_ENTRY" | head -1 | cut -d'"' -f4) rm -f "$CACHE_ENTRY" if [ -z "$ARCHIVE_LOCATION" ]; then echo "Cache not found (no archive location)" echo "cache-hit=false" >> $GITHUB_OUTPUT echo "cache-primary-key=${PRIMARY_KEY}" >> $GITHUB_OUTPUT echo "cache-matched-key=" >> $GITHUB_OUTPUT exit 0 fi echo "Cache found with key: ${MATCHED_KEY}" EXACT_HIT="false" if [ "$PRIMARY_KEY" = "$MATCHED_KEY" ]; then EXACT_HIT="true" fi TMPDIR=$(mktemp -d) ARCHIVE_PATH="${TMPDIR}/cache.tar.zst" echo "Downloading cache archive..." curl -sL -o "$ARCHIVE_PATH" "$ARCHIVE_LOCATION" echo "Extracting cache..." mkdir -p "$EXPANDED_PATH" if command -v zstd &>/dev/null; then tar -I zstd -xf "$ARCHIVE_PATH" -C "/" 2>/dev/null || true else tar -xf "$ARCHIVE_PATH" -C "/" 2>/dev/null || true fi rm -rf "$TMPDIR" echo "cache-hit=${EXACT_HIT}" >> $GITHUB_OUTPUT echo "cache-primary-key=${PRIMARY_KEY}" >> $GITHUB_OUTPUT echo "cache-matched-key=${MATCHED_KEY}" >> $GITHUB_OUTPUT - name: Save cache if: always() shell: bash env: CACHE_KEY: ${{ steps.gen-key.outputs.cache-key }} CACHE_PATH: ${{ inputs.path }} CACHE_HIT: ${{ steps.restore-cache.outputs.cache-hit }} run: | CACHE_URL="${ACTIONS_CACHE_URL:-${ACTIONS_RUNTIME_URL:-}}" TOKEN="${ACTIONS_RUNTIME_TOKEN:-}" if [ -z "$CACHE_URL" ] || [ -z "$TOKEN" ]; then echo "::warning::Cache environment variables not set. Skipping cache save." exit 0 fi CACHE_URL="${CACHE_URL%/}/" API_BASE="${CACHE_URL}_apis/artifactcache/" EXPANDED_PATH=$(eval echo "$CACHE_PATH") PATHS_HASH=$(echo "$EXPANDED_PATH" | sha256sum | awk '{print $1}') CACHE_VERSION="zstd-${PATHS_HASH}" PRIMARY_KEY="${CACHE_KEY}" if [ "$CACHE_HIT" = "true" ]; then echo "Cache hit on primary key, skipping save." exit 0 fi echo "Checking if cache directory has content..." if [ ! -d "$EXPANDED_PATH" ] || [ -z "$(ls -A "$EXPANDED_PATH" 2>/dev/null)" ]; then echo "Cache directory is empty, skipping save." exit 0 fi TMPDIR=$(mktemp -d) REL_PATH="$(echo "$EXPANDED_PATH" | sed 's|^/||')" ARCHIVE_PATH="${TMPDIR}/cache.tar.zst" COMPRESS_METHOD="zstd" if command -v zstd &>/dev/null; then tar -I zstd -cf "$ARCHIVE_PATH" -C "/" "$REL_PATH" 2>/dev/null || COMPRESS_METHOD="none" else COMPRESS_METHOD="none" fi if [ "$COMPRESS_METHOD" = "none" ]; then echo "zstd not available, trying gzip..." ARCHIVE_PATH="${TMPDIR}/cache.tar.gz" tar -czf "$ARCHIVE_PATH" -C "/" "$REL_PATH" 2>/dev/null || { echo "Failed to create archive" rm -rf "$TMPDIR" exit 0 } fi ARCHIVE_SIZE=$(stat -c%s "$ARCHIVE_PATH" 2>/dev/null || stat -f%z "$ARCHIVE_PATH" 2>/dev/null || echo "0") echo "Reserving cache with key: ${PRIMARY_KEY} (size: ${ARCHIVE_SIZE} bytes)" RESERVE_RESPONSE=$(mktemp) curl -sL -o "$RESERVE_RESPONSE" -w "%{http_code}" \ -X POST \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/json" \ -d "{\"key\":\"${PRIMARY_KEY}\",\"version\":\"${CACHE_VERSION}\",\"cacheSize\":${ARCHIVE_SIZE}}" \ "${API_BASE}caches" 2>/dev/null > /dev/null || echo "000" CACHE_ID=$(grep -o '"cacheId":[0-9]*' "$RESERVE_RESPONSE" | head -1 | cut -d':' -f2) rm -f "$RESERVE_RESPONSE" if [ -z "$CACHE_ID" ]; then echo "No cache ID returned, cache may already exist or reservation failed" rm -rf "$TMPDIR" exit 0 fi echo "Uploading cache (ID: ${CACHE_ID})..." UPLOAD_STATUS=$(curl -sL -o /dev/null -w "%{http_code}" \ -X PATCH \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/octet-stream" \ --data-binary "@${ARCHIVE_PATH}" \ "${API_BASE}caches/${CACHE_ID}" 2>/dev/null || echo "000") if [ "$UPLOAD_STATUS" != "200" ]; then echo "Upload failed with status: ${UPLOAD_STATUS}" rm -rf "$TMPDIR" exit 0 fi echo "Committing cache..." COMMIT_STATUS=$(curl -sL -o /dev/null -w "%{http_code}" \ -X POST \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/json" \ -d "{\"size\":${ARCHIVE_SIZE}}" \ "${API_BASE}caches/${CACHE_ID}" 2>/dev/null || echo "000") rm -rf "$TMPDIR" if [ "$COMMIT_STATUS" = "200" ]; then echo "Cache saved successfully" else echo "Commit failed with status: ${COMMIT_STATUS}" fi