From 8924964c6c1396e56d0ac5cc91bdbe92d87e881e Mon Sep 17 00:00:00 2001 From: Gennadi Kudrjavtsev Date: Wed, 23 Jul 2025 16:48:38 +0000 Subject: [PATCH] feat: add cleanup option for AVD after execution and validation tests --- README.md | 25 +++++++++++++++++++++++++ __tests__/input-validator.test.ts | 21 +++++++++++++++++++++ action-types.yml | 2 ++ action.yml | 3 +++ lib/emulator-manager.js | 22 +++++++++++++++++++++- lib/input-validator.js | 8 +++++++- lib/main.js | 17 ++++++++++++++++- src/emulator-manager.ts | 16 ++++++++++++++++ src/input-validator.ts | 6 ++++++ src/main.ts | 25 +++++++++++++++++++++++-- 10 files changed, 140 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 167b6cd69..c3a91e8e3 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,30 @@ jobs: script: ./gradlew connectedCheck ``` +If you want to automatically clean up the created AVD after the tests run (to save disk space): + +```yml +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: run tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 29 + cleanup-avd: true + script: ./gradlew connectedCheck +``` + If you need a specific [SDK Extensions](https://developer.android.com/guide/sdk-extensions) for the system image but not the platform: ```yml @@ -223,6 +247,7 @@ jobs: | `disable-spellchecker` | Optional | `false` | Whether to disable spellchecker - `true` or `false`. | | `disable-linux-hw-accel` | Optional | `auto` | Whether to disable hardware acceleration on Linux machines - `true`, `false` or `auto`.| | `enable-hw-keyboard` | Optional | `false` | Whether to enable hardware keyboard - `true` or `false`. | +| `cleanup-avd` | Optional | `false` | Whether to delete the created AVD after execution - `true` or `false`. | | `emulator-build` | Optional | N/A | Build number of a specific version of the emulator binary to use e.g. `6061023` for emulator v29.3.0.0. | | `working-directory` | Optional | `./` | A custom working directory - e.g. `./android` if your root Gradle project is under the `./android` sub-directory within your repository. Will be used for `script` & `pre-emulator-launch-script`. | | `ndk` | Optional | N/A | Version of NDK to install - e.g. `21.0.6113669` | diff --git a/__tests__/input-validator.test.ts b/__tests__/input-validator.test.ts index 068501208..caa31d466 100644 --- a/__tests__/input-validator.test.ts +++ b/__tests__/input-validator.test.ts @@ -190,6 +190,27 @@ describe('enable-hw-keyboard validator tests', () => { }); }); +describe('cleanup-avd validator tests', () => { + it('Throws if cleanup-avd is not a boolean', () => { + const func = () => { + validator.checkCleanupAvd('yes'); + }; + expect(func).toThrowError(`Input for input.cleanup-avd should be either 'true' or 'false'.`); + }); + + it('Validates successfully if cleanup-avd is either true or false', () => { + const func1 = () => { + validator.checkCleanupAvd('true'); + }; + expect(func1).not.toThrow(); + + const func2 = () => { + validator.checkCleanupAvd('false'); + }; + expect(func2).not.toThrow(); + }); +}); + describe('emulator-build validator tests', () => { it('Throws if emulator-build is not a number', () => { const func = () => { diff --git a/action-types.yml b/action-types.yml index 49bcc51be..045f894d6 100644 --- a/action-types.yml +++ b/action-types.yml @@ -54,6 +54,8 @@ inputs: type: string enable-hw-keyboard: type: boolean + cleanup-avd: + type: boolean emulator-build: type: string working-directory: diff --git a/action.yml b/action.yml index 30a20249e..fb4cbb9ec 100644 --- a/action.yml +++ b/action.yml @@ -57,6 +57,9 @@ inputs: enable-hw-keyboard: description: 'whether to enable hardware keyboard - `true` or `false`.' default: 'false' + cleanup-avd: + description: 'whether to delete the created AVD after execution - `true` or `false`' + default: 'false' emulator-build: description: 'build number of a specific version of the emulator binary to use - e.g. `6061023` for emulator v29.3.0.0' working-directory: diff --git a/lib/emulator-manager.js b/lib/emulator-manager.js index d57b97e96..32caac2f5 100644 --- a/lib/emulator-manager.js +++ b/lib/emulator-manager.js @@ -32,7 +32,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.killEmulator = exports.launchEmulator = exports.createAvd = void 0; +exports.deleteAvd = exports.killEmulator = exports.launchEmulator = exports.createAvd = void 0; const exec = __importStar(require("@actions/exec")); const fs = __importStar(require("fs")); /** @@ -142,6 +142,26 @@ function killEmulator(port) { }); } exports.killEmulator = killEmulator; +/** + * Deletes the specified AVD. + */ +function deleteAvd(avdName) { + return __awaiter(this, void 0, void 0, function* () { + try { + console.log(`::group::Delete AVD`); + console.log(`Deleting AVD '${avdName}'.`); + yield exec.exec(`avdmanager delete avd -n "${avdName}"`); + console.log(`AVD '${avdName}' deleted successfully.`); + } + catch (error) { + console.log(`Failed to delete AVD '${avdName}': ${error instanceof Error ? error.message : error}`); + } + finally { + console.log(`::endgroup::`); + } + }); +} +exports.deleteAvd = deleteAvd; function adb(port, command) { return __awaiter(this, void 0, void 0, function* () { return yield exec.exec(`adb -s emulator-${port} ${command}`); diff --git a/lib/input-validator.js b/lib/input-validator.js index bcb4443f1..328369885 100644 --- a/lib/input-validator.js +++ b/lib/input-validator.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.checkDiskSize = exports.checkEmulatorBuild = exports.checkEnableHardwareKeyboard = exports.checkDisableLinuxHardwareAcceleration = exports.checkDisableSpellchecker = exports.checkDisableAnimations = exports.checkPort = exports.checkForceAvdCreation = exports.checkChannel = exports.checkArch = exports.playstoreTargetSubstitution = exports.MAX_PORT = exports.MIN_PORT = exports.VALID_CHANNELS = exports.VALID_ARCHS = exports.MIN_API_LEVEL = void 0; +exports.checkDiskSize = exports.checkEmulatorBuild = exports.checkCleanupAvd = exports.checkEnableHardwareKeyboard = exports.checkDisableLinuxHardwareAcceleration = exports.checkDisableSpellchecker = exports.checkDisableAnimations = exports.checkPort = exports.checkForceAvdCreation = exports.checkChannel = exports.checkArch = exports.playstoreTargetSubstitution = exports.MAX_PORT = exports.MIN_PORT = exports.VALID_CHANNELS = exports.VALID_ARCHS = exports.MIN_API_LEVEL = void 0; exports.MIN_API_LEVEL = 15; exports.VALID_ARCHS = ['x86', 'x86_64', 'arm64-v8a']; exports.VALID_CHANNELS = ['stable', 'beta', 'dev', 'canary']; @@ -67,6 +67,12 @@ function checkEnableHardwareKeyboard(enableHardwareKeyboard) { } } exports.checkEnableHardwareKeyboard = checkEnableHardwareKeyboard; +function checkCleanupAvd(cleanupAvd) { + if (!isValidBoolean(cleanupAvd)) { + throw new Error(`Input for input.cleanup-avd should be either 'true' or 'false'.`); + } +} +exports.checkCleanupAvd = checkCleanupAvd; function checkEmulatorBuild(emulatorBuild) { if (isNaN(Number(emulatorBuild)) || !Number.isInteger(Number(emulatorBuild))) { throw new Error(`Unexpected emulator build: '${emulatorBuild}'.`); diff --git a/lib/main.js b/lib/main.js index 50e7a5090..06ebcde78 100644 --- a/lib/main.js +++ b/lib/main.js @@ -43,6 +43,8 @@ const fs_1 = require("fs"); function run() { return __awaiter(this, void 0, void 0, function* () { let port = input_validator_1.MIN_PORT; + let avdName = ''; + let cleanupAvd = false; try { console.log(`::group::Configure emulator`); let linuxSupportKVM = false; @@ -95,7 +97,7 @@ function run() { (0, input_validator_1.checkDiskSize)(diskSize); console.log(`Disk size: ${diskSize}`); // custom name used for creating the AVD - const avdName = core.getInput('avd-name'); + avdName = core.getInput('avd-name'); console.log(`AVD name: ${avdName}`); // force AVD creation const forceAvdCreationInput = core.getInput('force-avd-creation'); @@ -135,6 +137,11 @@ function run() { (0, input_validator_1.checkEnableHardwareKeyboard)(enableHardwareKeyboardInput); const enableHardwareKeyboard = enableHardwareKeyboardInput === 'true'; console.log(`enable hardware keyboard: ${enableHardwareKeyboard}`); + // cleanup AVD after execution + const cleanupAvdInput = core.getInput('cleanup-avd'); + (0, input_validator_1.checkCleanupAvd)(cleanupAvdInput); + cleanupAvd = cleanupAvdInput === 'true'; + console.log(`cleanup AVD: ${cleanupAvd}`); // emulator build const emulatorBuildInput = core.getInput('emulator-build'); if (emulatorBuildInput) { @@ -222,10 +229,18 @@ function run() { } // finally kill the emulator yield (0, emulator_manager_1.killEmulator)(port); + // cleanup AVD if requested + if (cleanupAvd) { + yield (0, emulator_manager_1.deleteAvd)(avdName); + } } catch (error) { // kill the emulator so the action can exit yield (0, emulator_manager_1.killEmulator)(port); + // cleanup AVD if requested, even on error + if (cleanupAvd) { + yield (0, emulator_manager_1.deleteAvd)(avdName); + } core.setFailed(error instanceof Error ? error.message : error); } }); diff --git a/src/emulator-manager.ts b/src/emulator-manager.ts index 5a8161627..c5425cac4 100644 --- a/src/emulator-manager.ts +++ b/src/emulator-manager.ts @@ -130,6 +130,22 @@ export async function killEmulator(port: number): Promise { } } +/** + * Deletes the specified AVD. + */ +export async function deleteAvd(avdName: string): Promise { + try { + console.log(`::group::Delete AVD`); + console.log(`Deleting AVD '${avdName}'.`); + await exec.exec(`avdmanager delete avd -n "${avdName}"`); + console.log(`AVD '${avdName}' deleted successfully.`); + } catch (error) { + console.log(`Failed to delete AVD '${avdName}': ${error instanceof Error ? error.message : error}`); + } finally { + console.log(`::endgroup::`); + } +} + async function adb(port: number, command: string): Promise { return await exec.exec(`adb -s emulator-${port} ${command}`); } diff --git a/src/input-validator.ts b/src/input-validator.ts index 2fd0c6237..6f37f9e4a 100644 --- a/src/input-validator.ts +++ b/src/input-validator.ts @@ -63,6 +63,12 @@ export function checkEnableHardwareKeyboard(enableHardwareKeyboard: string): voi } } +export function checkCleanupAvd(cleanupAvd: string): void { + if (!isValidBoolean(cleanupAvd)) { + throw new Error(`Input for input.cleanup-avd should be either 'true' or 'false'.`); + } +} + export function checkEmulatorBuild(emulatorBuild: string): void { if (isNaN(Number(emulatorBuild)) || !Number.isInteger(Number(emulatorBuild))) { throw new Error(`Unexpected emulator build: '${emulatorBuild}'.`); diff --git a/src/main.ts b/src/main.ts index 8562dbcd0..67e26306e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,12 +9,13 @@ import { checkForceAvdCreation, checkChannel, checkEnableHardwareKeyboard, + checkCleanupAvd, checkDiskSize, checkPort, playstoreTargetSubstitution, MIN_PORT, } from './input-validator'; -import { createAvd, launchEmulator, killEmulator } from './emulator-manager'; +import { createAvd, launchEmulator, killEmulator, deleteAvd } from './emulator-manager'; import * as exec from '@actions/exec'; import { parseScript } from './script-parser'; import { getChannelId } from './channel-id-mapper'; @@ -22,6 +23,9 @@ import { accessSync, constants } from 'fs'; async function run() { let port: number = MIN_PORT; + let avdName: string = ''; + let cleanupAvd: boolean = false; + try { console.log(`::group::Configure emulator`); let linuxSupportKVM = false; @@ -85,7 +89,7 @@ async function run() { console.log(`Disk size: ${diskSize}`); // custom name used for creating the AVD - const avdName = core.getInput('avd-name'); + avdName = core.getInput('avd-name'); console.log(`AVD name: ${avdName}`); // force AVD creation @@ -134,6 +138,12 @@ async function run() { const enableHardwareKeyboard = enableHardwareKeyboardInput === 'true'; console.log(`enable hardware keyboard: ${enableHardwareKeyboard}`); + // cleanup AVD after execution + const cleanupAvdInput = core.getInput('cleanup-avd'); + checkCleanupAvd(cleanupAvdInput); + cleanupAvd = cleanupAvdInput === 'true'; + console.log(`cleanup AVD: ${cleanupAvd}`); + // emulator build const emulatorBuildInput = core.getInput('emulator-build'); if (emulatorBuildInput) { @@ -231,9 +241,20 @@ async function run() { // finally kill the emulator await killEmulator(port); + + // cleanup AVD if requested + if (cleanupAvd) { + await deleteAvd(avdName); + } } catch (error) { // kill the emulator so the action can exit await killEmulator(port); + + // cleanup AVD if requested, even on error + if (cleanupAvd) { + await deleteAvd(avdName); + } + core.setFailed(error instanceof Error ? error.message : (error as string)); } }