From 343db2a4c194f176f8e838a6f15843c60b550532 Mon Sep 17 00:00:00 2001 From: JP Cottin Date: Sun, 26 Jul 2026 16:23:54 -0700 Subject: [PATCH] Key the system-image cache on the image revision Weekly rotation bounded how long a stale entry could linger, but it still left a window: within a week the exact key already exists, actions/cache does not re-save on a hit, so a newly published image revision would be re-downloaded by sdkmanager on every run until the week rolled over. Split restore and save so the entry can be keyed on the revision that actually ended up on disk, which is only knowable after sdkmanager has run. Restore takes whatever copy exists via the key prefix, sdkmanager brings it up to date, and save writes it back under rev -- guarded so it only writes when the restored entry was not already that revision. A new revision therefore enters the cache on the first run that sees it, and a steady state costs nothing. Correctness was never at risk either way: sdkmanager always installs the newest revision, so CI has never tested a stale image. This is purely about not paying for the same download repeatedly. --- .github/actions/preview-emulator/action.yml | 46 ++++++++++++++------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/.github/actions/preview-emulator/action.yml b/.github/actions/preview-emulator/action.yml index f1918ae..825702a 100644 --- a/.github/actions/preview-emulator/action.yml +++ b/.github/actions/preview-emulator/action.yml @@ -47,26 +47,26 @@ runs: shell: bash run: | echo "path=${ANDROID_HOME:-${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}}" >> "$GITHUB_OUTPUT" - # actions/cache never re-saves on an exact key hit, so a fixed key would - # freeze the image at whatever revision was current when it was first - # cached: sdkmanager would re-fetch each new revision on every run and - # the cache would never catch up, while CI quietly tested a stale image. - # Rotating weekly bounds that. The restore-keys prefix means a rotation - # restores last week's copy and lets sdkmanager fetch just the delta, - # rather than paying for a full download. - echo "week=$(date -u +%G-W%V)" >> "$GITHUB_OUTPUT" # The system image is the one large download here (system.img alone is # ~4.1 GiB, ~2 GiB compressed). The preview emulator package is # deliberately NOT cached: it moves often and pinning a stale build would - # defeat the purpose of these jobs. sdkmanager still runs after a cache - # hit, so a newer image revision is still picked up. + # defeat the purpose of these jobs. + # + # Restore and save are split so the entry can be keyed on the image + # revision, which is only knowable after sdkmanager has run. A single + # actions/cache with a fixed key never re-saves on a hit, so it would pin + # whatever revision was cached first and re-download every subsequent + # revision on every run, forever. Here: restore whatever copy exists, + # let sdkmanager bring it up to date, then save under the revision that + # actually ended up on disk. A new revision lands in the cache on the + # first run that sees it. - name: Restore cached system image - id: sysimg-cache - uses: actions/cache@v4 + id: sysimg-restore + uses: actions/cache/restore@v4 with: path: ${{ steps.sdk.outputs.path }}/system-images/android-${{ inputs.api-level }}/${{ inputs.target }}/${{ inputs.abi }} - key: sysimg-v1-android-${{ inputs.api-level }}-${{ inputs.target }}-${{ inputs.abi }}-${{ steps.sdk.outputs.week }} + key: sysimg-v1-android-${{ inputs.api-level }}-${{ inputs.target }}-${{ inputs.abi }}-rev restore-keys: | sysimg-v1-android-${{ inputs.api-level }}-${{ inputs.target }}-${{ inputs.abi }} @@ -100,6 +100,7 @@ runs: # The AVD must be created BEFORE emulators;latest is installed: with the # preview package present, avdmanager create silently no-ops. - name: Install system image and create AVD + id: sysimg shell: bash run: | set +o pipefail # see "Update SDK cmdline-tools" -- `yes |` vs pipefail @@ -108,11 +109,17 @@ runs: PKG='system-images;android-${{ inputs.api-level }};${{ inputs.target }};${{ inputs.abi }}' # Timed so the value of caching the image can be read straight off the # job log rather than inferred. - echo "SYSIMG-CACHE hit=${{ steps.sysimg-cache.outputs.cache-hit || 'false' }}" + echo "SYSIMG-CACHE restored-from='${{ steps.sysimg-restore.outputs.cache-matched-key }}'" T0=$(date +%s) yes | "$SDK/cmdline-tools/latest/bin/sdkmanager" --channel=3 --install "$PKG" platform-tools > /dev/null echo "SYSIMG-INSTALL seconds=$(( $(date +%s) - T0 ))" - du -sh "$SDK/system-images/android-${{ inputs.api-level }}/${{ inputs.target }}/${{ inputs.abi }}" 2>/dev/null | sed 's/^/SYSIMG-SIZE /' || true + IMGDIR="$SDK/system-images/android-${{ inputs.api-level }}/${{ inputs.target }}/${{ inputs.abi }}" + du -sh "$IMGDIR" 2>/dev/null | sed 's/^/SYSIMG-SIZE /' || true + # Key the cache on the revision that actually ended up on disk. + REV="$(sed -n 's/^Pkg\.Revision=//p' "$IMGDIR/source.properties" 2>/dev/null | head -1 | tr -d ' \r')" + [ -n "$REV" ] || REV=unknown + echo "SYSIMG-REVISION $REV" + echo "revision=$REV" >> "$GITHUB_OUTPUT" # Mimic the emulator-runner action's invocation (--abi and --device # skip the auto-select/prompt path where bare creates silently no-op). echo no | "$SDK/cmdline-tools/latest/bin/avdmanager" create avd --force -n "$AVD" \ @@ -153,6 +160,15 @@ runs: >> "$HOME/.android/avd/$AVD.avd/config.ini" echo "===== AVD root ini ====="; cat "$HOME/.android/avd/$AVD.ini" + # Only writes when the restored entry is not already this revision, so a + # steady state costs nothing and a new revision is picked up once. + - name: Save system image to cache + if: steps.sysimg-restore.outputs.cache-matched-key != format('sysimg-v1-android-{0}-{1}-{2}-rev{3}', inputs.api-level, inputs.target, inputs.abi, steps.sysimg.outputs.revision) + uses: actions/cache/save@v4 + with: + path: ${{ steps.sdk.outputs.path }}/system-images/android-${{ inputs.api-level }}/${{ inputs.target }}/${{ inputs.abi }} + key: sysimg-v1-android-${{ inputs.api-level }}-${{ inputs.target }}-${{ inputs.abi }}-rev${{ steps.sysimg.outputs.revision }} + - name: Install preview emulator shell: bash run: |