Skip to content

Commit fea06c9

Browse files
committed
ci(android): share SDK paths and cache installed image revisions
1 parent 2b2555f commit fea06c9

3 files changed

Lines changed: 125 additions & 10 deletions

File tree

‎.github/actions/setup-android-sdk/action.yml‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ outputs:
1818
build-tools-dir:
1919
description: 'Absolute path of the installed build-tools directory'
2020
value: ${{ steps.install.outputs.build-tools-dir }}
21+
system-image-dir:
22+
description: 'Absolute path of the API 36 x86_64 Play Store system image'
23+
value: ${{ steps.install.outputs.system-image-dir }}
24+
system-image-revision:
25+
description: 'Installed system image revision, when requested'
26+
value: ${{ steps.install.outputs.system-image-revision }}
2127

2228
runs:
2329
using: 'composite'
@@ -32,6 +38,7 @@ runs:
3238
set -euo pipefail
3339
SDK_ROOT="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}}"
3440
BUILD_TOOLS_DIR="$SDK_ROOT/build-tools/$BUILD_TOOLS_VERSION"
41+
SYSTEM_IMAGE_DIR="$SDK_ROOT/system-images/android-36/google_apis_playstore/x86_64"
3542
# Every build-tools binary a caller runs: the helper build (aapt2, d8, zipalign,
3643
# apksigner) and the fixture APK scripts (aapt, apksigner).
3744
INSTALLED=true
@@ -58,7 +65,14 @@ runs:
5865
fi
5966
if [ "$INSTALL_SYSTEM_IMAGE" = true ]; then
6067
"$SDKMANAGER" --install 'system-images;android-36;google_apis_playstore;x86_64' --channel=0
68+
IMAGE_REVISION="$(sed -n 's/^Pkg.Revision[[:space:]]*=[[:space:]]*//p' "$SYSTEM_IMAGE_DIR/source.properties" | tr -d '\r' | sed 's/[[:space:]]*$//')"
69+
if ! [[ "$IMAGE_REVISION" =~ ^[0-9]+([.][0-9]+)*$ ]]; then
70+
echo '::error::Android system image has no numeric Pkg.Revision.' >&2
71+
exit 1
72+
fi
73+
echo "system-image-revision=$IMAGE_REVISION" >> "$GITHUB_OUTPUT"
6174
fi
6275
fi
6376
echo "AGENT_DEVICE_ANDROID_BUILD_TOOLS=$BUILD_TOOLS_VERSION" >> "$GITHUB_ENV"
6477
echo "build-tools-dir=$BUILD_TOOLS_DIR" >> "$GITHUB_OUTPUT"
78+
echo "system-image-dir=$SYSTEM_IMAGE_DIR" >> "$GITHUB_OUTPUT"

‎.github/actions/setup-android-system-image/action.yml‎

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,37 @@ description: 'Cache the API 36 Play Store image shared by Android emulator lanes
44
runs:
55
using: 'composite'
66
steps:
7+
- name: Setup Android SDK
8+
id: sdk
9+
uses: ./.github/actions/setup-android-sdk
10+
711
- name: Resolve Android system image cache
8-
id: image
12+
id: key
913
shell: bash
14+
env:
15+
CACHE_PREFIX: android-system-image-${{ runner.os }}-${{ runner.arch }}-api36-google_apis_playstore-x86_64-${{ hashFiles('.github/actions/setup-android-system-image/action.yml', '.github/actions/setup-android-sdk/action.yml') }}
1016
run: |
1117
set -euo pipefail
12-
echo "path=${ANDROID_HOME:?}/system-images/android-36/google_apis_playstore/x86_64" >> "$GITHUB_OUTPUT"
13-
echo "week=$(date -u +%G-W%V)" >> "$GITHUB_OUTPUT"
18+
echo "prefix=$CACHE_PREFIX-$(date -u +%G-W%V)" >> "$GITHUB_OUTPUT"
1419
15-
# The SDK installer still checks for upstream revisions. Rotate weekly so an immutable cache
16-
# does not require downloading the same updated image on every run until eviction.
20+
# Only revision keys are saved. The current key restores the most recently saved compatible revision.
1721
- name: Restore Android system image
1822
id: restore
1923
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.3
2024
with:
21-
path: ${{ steps.image.outputs.path }}
22-
key: android-system-image-${{ runner.os }}-${{ runner.arch }}-api36-google_apis_playstore-x86_64-${{ steps.image.outputs.week }}-${{ hashFiles('.github/actions/setup-android-system-image/action.yml') }}
25+
path: ${{ steps.sdk.outputs.system-image-dir }}
26+
key: ${{ steps.key.outputs.prefix }}-current
27+
restore-keys: ${{ steps.key.outputs.prefix }}-revision-
2328

2429
- name: Install Android system image
30+
id: install
2531
uses: ./.github/actions/setup-android-sdk
2632
with:
2733
system-image: 'true'
2834

2935
- name: Save Android system image
30-
if: steps.restore.outputs.cache-hit != 'true'
36+
if: steps.restore.outputs.cache-matched-key != format('{0}-revision-{1}', steps.key.outputs.prefix, steps.install.outputs.system-image-revision)
3137
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.3
3238
with:
33-
path: ${{ steps.image.outputs.path }}
34-
key: ${{ steps.restore.outputs.cache-primary-key }}
39+
path: ${{ steps.sdk.outputs.system-image-dir }}
40+
key: ${{ format('{0}-revision-{1}', steps.key.outputs.prefix, steps.install.outputs.system-image-revision) }}

‎test/repo-guards/android-sdk-setup-action.test.ts‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import assert from 'node:assert/strict';
22
import fs from 'node:fs';
3+
import { spawnSync } from 'node:child_process';
34
import path from 'node:path';
45
import { fileURLToPath } from 'node:url';
56
import { test } from 'vitest';
7+
import { parse } from 'yaml';
8+
import { mkdtempForTestSync } from '@agent-device/test-kit';
69

710
// `.github/actions/setup-android-sdk` owns the pinned build-tools version and the sdkmanager
811
// install. The version decides the helper APK bytes, so a second copy can drift silently.
@@ -39,3 +42,95 @@ test('setup-android-sdk lets a real licence failure stop the step', () => {
3942
assert.doesNotMatch(action, /--licenses[^\n]*\|\|/);
4043
assert.match(action, /AGENT_DEVICE_ANDROID_BUILD_TOOLS=\$BUILD_TOOLS_VERSION" >> "\$GITHUB_ENV"/);
4144
});
45+
46+
function systemImageFixture(rootVariable: 'ANDROID_HOME' | 'ANDROID_SDK_ROOT') {
47+
const directory = mkdtempForTestSync('sdk-system-image-');
48+
const sdkRoot = path.join(directory, 'sdk');
49+
const image = path.join(sdkRoot, 'system-images/android-36/google_apis_playstore/x86_64');
50+
const action = parse(fs.readFileSync(path.join(REPO_ROOT, OWNER_DIR, 'action.yml'), 'utf8')) as {
51+
inputs: { 'build-tools': { default: string } };
52+
runs: { steps: { run: string }[] };
53+
};
54+
const version = action.inputs['build-tools'].default;
55+
const tools = path.join(sdkRoot, 'build-tools', version);
56+
fs.mkdirSync(tools, { recursive: true });
57+
for (const tool of ['aapt', 'aapt2', 'd8', 'zipalign', 'apksigner'])
58+
fs.writeFileSync(path.join(tools, tool), '#!/bin/sh\nexit 0\n', { mode: 0o755 });
59+
fs.mkdirSync(path.join(sdkRoot, 'platforms/android-36'), { recursive: true });
60+
fs.writeFileSync(path.join(sdkRoot, 'platforms/android-36/android.jar'), '');
61+
const manager = path.join(sdkRoot, 'cmdline-tools/latest/bin/sdkmanager');
62+
fs.mkdirSync(path.dirname(manager), { recursive: true });
63+
const calls = path.join(directory, 'calls');
64+
fs.writeFileSync(
65+
manager,
66+
`#!/bin/bash
67+
set -euo pipefail
68+
printf '%s\\n' "$*" >> "$SDK_FIXTURE_CALLS"
69+
if [ "$1" = --install ]; then
70+
mkdir -p "$SDK_FIXTURE_IMAGE"
71+
printf 'Pkg.Revision=%s\\n' "$SDK_FIXTURE_REVISION" > "$SDK_FIXTURE_IMAGE/source.properties"
72+
fi
73+
`,
74+
{ mode: 0o755 },
75+
);
76+
return {
77+
image,
78+
calls,
79+
run: (install: boolean, revision = '9') => {
80+
const output = path.join(directory, 'output');
81+
fs.writeFileSync(output, '');
82+
const result = spawnSync('bash', ['-c', action.runs.steps[0]!.run], {
83+
encoding: 'utf8',
84+
env: {
85+
...process.env,
86+
ANDROID_HOME: rootVariable === 'ANDROID_HOME' ? sdkRoot : '',
87+
ANDROID_SDK_ROOT:
88+
rootVariable === 'ANDROID_SDK_ROOT' ? sdkRoot : path.join(directory, 'other'),
89+
BUILD_TOOLS_VERSION: version,
90+
INSTALL_SYSTEM_IMAGE: String(install),
91+
SDK_FIXTURE_CALLS: calls,
92+
SDK_FIXTURE_IMAGE: image,
93+
SDK_FIXTURE_REVISION: revision,
94+
GITHUB_OUTPUT: output,
95+
GITHUB_ENV: path.join(directory, 'env'),
96+
},
97+
});
98+
const outputs = new Map(
99+
fs
100+
.readFileSync(output, 'utf8')
101+
.trim()
102+
.split('\n')
103+
.map((line) => {
104+
const separator = line.indexOf('=');
105+
return [line.slice(0, separator), line.slice(separator + 1)] as const;
106+
}),
107+
);
108+
return { ...result, outputs };
109+
},
110+
};
111+
}
112+
113+
test.each(['ANDROID_HOME', 'ANDROID_SDK_ROOT'] as const)(
114+
'system image outputs follow %s and the installed revision',
115+
(rootVariable) => {
116+
const fixture = systemImageFixture(rootVariable);
117+
const initial = fixture.run(false);
118+
assert.equal(initial.status, 0, initial.stderr);
119+
assert.equal(initial.outputs.get('system-image-dir'), fixture.image);
120+
assert.equal(initial.outputs.has('system-image-revision'), false);
121+
assert.equal(fs.existsSync(fixture.calls), false);
122+
for (const revision of ['9', '10.1']) {
123+
const installed = fixture.run(true, revision);
124+
assert.equal(installed.status, 0, installed.stderr);
125+
assert.equal(installed.outputs.get('system-image-dir'), fixture.image);
126+
assert.equal(installed.outputs.get('system-image-revision'), revision);
127+
}
128+
},
129+
);
130+
131+
test('an invalid installed image revision cannot produce a cache identity', () => {
132+
const fixture = systemImageFixture('ANDROID_HOME');
133+
const installed = fixture.run(true, 'invalid');
134+
assert.equal(installed.status, 1);
135+
assert.equal(installed.outputs.has('system-image-revision'), false);
136+
});

0 commit comments

Comments
 (0)