diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 0de44e03..c54e0e90 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -8,7 +8,7 @@ on: jobs: test: runs-on: macOS-latest - timeout-minutes: 20 + timeout-minutes: 10 strategy: matrix: api-level: [21, 23, 29] @@ -27,9 +27,13 @@ jobs: - name: run action uses: ./ - with: + with: api-level: ${{ matrix.api-level }} target: default - abi: x86 + arch: x86 + profile: Nexus 6 headless: true disable-animations: true + script: | + adb devices -l + adb --help diff --git a/README.md b/README.md index 11641c57..dbe5478e 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,14 @@ This action must be run on a **macOS** VM, e.g. `macOS-latest` or `macOS-10.14`. ## Configurations | | **Required** | **Default** | **Description** | -|----------------------|--------------|-------------|------------------------------------------------------------------------------------------------------------------------------------------------------| +|----------------------|--------------|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `api-level` | Required | N/A | API level of the platform system image - e.g. 23 for Android Marshmallow, 29 for Android 10. **Minimum API level supported is 21**. | | `target` | Optional | `default` | Target of the system image - `default` or `google_apis`. | -| `abi` | Optional | `x86` | CPU / ABI of the system image - `x86` or `x86_64`. | +| `arch` | Optional | `x86` | CPU architecture of the system image - `x86` or `x86_64`. | +| `profile` | Optional | N/A | Hardware profile used for creating the AVD - e.g. `Nexus 6`. For a list of all profiles available, run `$ANDROID_HOME/tools/bin/avdmanager list` and refer to the results under "Available Android Virtual Devices". | | `headless` | Optional | `true` | Whether to launch emulator without UI - `true` or `false`. When set to `true` this is equivalent to running the emulator with `emulator -no-window`. | | `disable-animations` | Optional | `true` | Whether to disable animations - `true` or `false`. | +| `script` | Required | N/A | Custom script to run - e.g. to run Android instrumented tests on the emulator: `./gradlew connectedCheck` | ## Usage diff --git a/__tests__/input-validator.test.ts b/__tests__/input-validator.test.ts index ddfaa917..7d546f06 100644 --- a/__tests__/input-validator.test.ts +++ b/__tests__/input-validator.test.ts @@ -56,22 +56,22 @@ describe('target validator tests', () => { }); }); -describe('abi validator tests', () => { - it('Throws if abi is unknown', () => { +describe('arch validator tests', () => { + it('Throws if arch is unknown', () => { const func = () => { - validator.checkAbi('some-abi'); + validator.checkArch('some-arch'); }; - expect(func).toThrowError(`Value for input.abi 'some-abi' is unknown. Supported options: ${validator.VALID_ABIS}`); + expect(func).toThrowError(`Value for input.arch 'some-arch' is unknown. Supported options: ${validator.VALID_ARCHS}`); }); - it('Validates successfully with valid abi', () => { + it('Validates successfully with valid arch', () => { const func1 = () => { - validator.checkAbi('x86'); + validator.checkArch('x86'); }; expect(func1).not.toThrow(); const func2 = () => { - validator.checkAbi('x86_64'); + validator.checkArch('x86_64'); }; expect(func2).not.toThrow(); }); diff --git a/action.yml b/action.yml index 7fba3f88..08f1f3c0 100644 --- a/action.yml +++ b/action.yml @@ -11,15 +11,20 @@ inputs: target: description: 'target of the system image - default or google_apis' default: 'default' - abi: - description: 'CPU / ABI of the system image - x86 or x86_64' + arch: + description: 'CPU architecture of the system image - x86 or x86_64' default: 'x86' + profile: + description: 'Hardware profile used for creating the AVD - e.g. `Nexus 6`.' headless: description: 'whether to launch emulator in without UI - true or false' default: 'true' disable-animations: description: 'whether to disable animations - true or false' default: 'true' + script: + description: 'custom script to run - e.g. `./gradlew connectedCheck`' + required: true runs: using: 'node12' main: 'lib/main.js' diff --git a/lib/emulator-launcher.js b/lib/emulator-manager.js similarity index 65% rename from lib/emulator-launcher.js rename to lib/emulator-manager.js index b5f0bb9e..9c20d661 100644 --- a/lib/emulator-launcher.js +++ b/lib/emulator-manager.js @@ -17,35 +17,53 @@ var __importStar = (this && this.__importStar) || function (mod) { Object.defineProperty(exports, "__esModule", { value: true }); const exec = __importStar(require("@actions/exec")); const EMULATOR_BOOT_TIMEOUT_SECONDS = 120; +const AVD_MANAGER_PATH = `${process.env.ANDROID_HOME}/tools/bin/avdmanager`; +const ADB_PATH = `${process.env.ANDROID_HOME}/platform-tools/adb`; /** * Creates and launches a new AVD instance with the specified configurations. */ -function launchEmulator(apiLevel, target, abi, device, headless, disableAnimations) { +function launchEmulator(apiLevel, target, arch, profile, headless, disableAnimations) { return __awaiter(this, void 0, void 0, function* () { - const avdmangerPath = `${process.env.ANDROID_HOME}/tools/bin/avdmanager`; - const adbPath = `${process.env.ANDROID_HOME}/platform-tools/adb`; // create a new AVD - console.log('Creating AVD.'); - yield exec.exec(`${avdmangerPath} create avd -n test --force --abi "${target}/${abi}" --package "system-images;android-${apiLevel};${target};${abi}" --device "${device}"`); + if (profile.trim() !== '') { + console.log(`Creating AVD with custom profile ${profile}`); + yield exec.exec(`${AVD_MANAGER_PATH} create avd --force -n test --abi "${target}/${arch}" --package "system-images;android-${apiLevel};${target};${arch}" --device "${profile}"`); + } + else { + console.log(`Creating AVD without custom profile.`); + yield exec.exec(`echo "no" | ${AVD_MANAGER_PATH} create avd --force -n test --abi "${target}/${arch}" --package "system-images;android-${apiLevel};${target};${arch}"`); + } // start emulator console.log('Starting emulator.'); const noWindow = headless ? '-no-window' : ''; yield exec.exec(`bash -c \\"${process.env.ANDROID_HOME}/emulator/emulator -avd test ${noWindow} -no-snapshot -noaudio -no-boot-anim &"`); // wait for emulator to complete booting yield waitForDevice(); - yield exec.exec(`${adbPath} shell input keyevent 82`); + yield exec.exec(`${ADB_PATH} shell input keyevent 82`); // disable animations if (disableAnimations) { console.log('Disabling animations.'); - yield exec.exec(`${adbPath} shell settings put global window_animation_scale 0.0`); - yield exec.exec(`${adbPath} shell settings put global transition_animation_scale 0.0`); - yield exec.exec(`${adbPath} shell settings put global animator_duration_scale 0.0`); + yield exec.exec(`${ADB_PATH} shell settings put global window_animation_scale 0.0`); + yield exec.exec(`${ADB_PATH} shell settings put global transition_animation_scale 0.0`); + yield exec.exec(`${ADB_PATH} shell settings put global animator_duration_scale 0.0`); } - // kill emulator - yield exec.exec(`${adbPath} -s emulator-5554 emu kill`); }); } exports.launchEmulator = launchEmulator; +/** + * Kills the running emulator on the defaut port. + */ +function killEmulator() { + return __awaiter(this, void 0, void 0, function* () { + try { + yield exec.exec(`${ADB_PATH} -s emulator-5554 emu kill`); + } + catch (error) { + console.log('No emulator running on port 5554'); + } + }); +} +exports.killEmulator = killEmulator; /** * Wait for emulator to boot. */ @@ -72,8 +90,8 @@ function waitForDevice() { break; } } - catch (e) { - console.error(e.message); + catch (error) { + console.error(error.message); } if (attempts < maxAttemps) { yield delay(retryInterval * 1000); diff --git a/lib/input-validator.js b/lib/input-validator.js index 4b5c52f8..e0459312 100644 --- a/lib/input-validator.js +++ b/lib/input-validator.js @@ -2,7 +2,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.MIN_API_LEVEL = 21; exports.VALID_TARGETS = ['default', 'google_apis']; -exports.VALID_ABIS = ['x86', 'x86_64']; +exports.VALID_ARCHS = ['x86', 'x86_64']; function checkApiLevel(apiLevel) { if (isNaN(Number(apiLevel)) || !Number.isInteger(Number(apiLevel))) { throw new Error(`Unexpected API level: '${apiLevel}'.`); @@ -18,12 +18,12 @@ function checkTarget(target) { } } exports.checkTarget = checkTarget; -function checkAbi(abi) { - if (!exports.VALID_ABIS.includes(abi)) { - throw new Error(`Value for input.abi '${abi}' is unknown. Supported options: ${exports.VALID_ABIS}.`); +function checkArch(arch) { + if (!exports.VALID_ARCHS.includes(arch)) { + throw new Error(`Value for input.arch '${arch}' is unknown. Supported options: ${exports.VALID_ARCHS}.`); } } -exports.checkAbi = checkAbi; +exports.checkArch = checkArch; function checkHeadless(headless) { if (headless !== 'true' && headless !== 'false') { throw new Error(`Input for input.headless should be either 'true' or 'false'.`); diff --git a/lib/main.js b/lib/main.js index 89d6b551..a80906de 100644 --- a/lib/main.js +++ b/lib/main.js @@ -18,7 +18,8 @@ Object.defineProperty(exports, "__esModule", { value: true }); const core = __importStar(require("@actions/core")); const sdk_installer_1 = require("./sdk-installer"); const input_validator_1 = require("./input-validator"); -const emulator_launcher_1 = require("./emulator-launcher"); +const emulator_manager_1 = require("./emulator-manager"); +const exec = __importStar(require("@actions/exec")); function run() { return __awaiter(this, void 0, void 0, function* () { try { @@ -35,10 +36,13 @@ function run() { const target = core.getInput('target'); input_validator_1.checkTarget(target); console.log(`target: ${target}`); - // CPU / ABI of the system image - const abi = core.getInput('abi'); - input_validator_1.checkAbi(abi); - console.log(`cpu/abi: ${abi}`); + // CPU architecture of the system image + const arch = core.getInput('arch'); + input_validator_1.checkArch(arch); + console.log(`CPI architecture: ${arch}`); + // Hardware profile used for creating the AVD + const profile = core.getInput('profile'); + console.log(`Hardware profile: ${profile}`); // headless mode const headlessInput = core.getInput('headless'); input_validator_1.checkHeadless(headlessInput); @@ -49,15 +53,20 @@ function run() { input_validator_1.checkDisableAnimations(disableAnimationsInput); const disableAnimations = disableAnimationsInput === 'true'; console.log(`disable animations: ${disableAnimations}`); + // custom scrpt to run + const script = core.getInput('script', { required: true }); // install SDK - yield sdk_installer_1.installAndroidSdk(apiLevel, target, abi); - // launch emulator - // TODO get from input (source list of all profiles) - const device = 'Nexus 6P'; - yield emulator_launcher_1.launchEmulator(apiLevel, target, abi, device, headless, disableAnimations); - // TODO start emulator + yield sdk_installer_1.installAndroidSdk(apiLevel, target, arch); + // launch an emulator + yield emulator_manager_1.launchEmulator(apiLevel, target, arch, profile, headless, disableAnimations); + // execute the custom script + yield exec.exec(`${script}`); + // finally kill the emulator + yield emulator_manager_1.killEmulator(); } catch (error) { + // kill the emulator so the action can exit + yield emulator_manager_1.killEmulator(); core.setFailed(error.message); } }); diff --git a/lib/sdk-installer.js b/lib/sdk-installer.js index 5455a86b..db55c27f 100644 --- a/lib/sdk-installer.js +++ b/lib/sdk-installer.js @@ -18,12 +18,13 @@ Object.defineProperty(exports, "__esModule", { value: true }); const exec = __importStar(require("@actions/exec")); const tc = __importStar(require("@actions/tool-cache")); const fs = __importStar(require("fs")); +const BUILD_TOOLS_VERSION = '29.0.2'; const SDK_URL = 'https://dl.google.com/android/repository/sdk-tools-darwin-4333796.zip'; /** * Downloads and installs the Android SDK for the macOS platform, including SDK platform for the chosen API level, latest build tools, platform tools, Android Emulator, - * and the system image for the chosen API level, cpu/abi, and target. + * and the system image for the chosen API level, CPU arch, and target. */ -function installAndroidSdk(apiLevel, target, abi) { +function installAndroidSdk(apiLevel, target, arch) { return __awaiter(this, void 0, void 0, function* () { // download Android SDK if not already installed if (fs.existsSync(`${process.env.ANDROID_HOME}/tools/bin/sdkmanager`)) { @@ -38,8 +39,9 @@ function installAndroidSdk(apiLevel, target, abi) { console.log('Installing build tools, platform tools, platform and system image.'); const sdkmangerPath = `${process.env.ANDROID_HOME}/tools/bin/sdkmanager`; yield exec.exec(`echo "y" | ${sdkmangerPath} --licenses > /dev/null`); - yield exec.exec(`${sdkmangerPath} tools platform-tools "platforms;android-${apiLevel}"`); - yield exec.exec(`${sdkmangerPath} "system-images;android-${apiLevel};${target};${abi}"`); + yield exec.exec(`${sdkmangerPath} "build-tools;${BUILD_TOOLS_VERSION}"`); + yield exec.exec(`${sdkmangerPath} platform-tools "platforms;android-${apiLevel}"`); + yield exec.exec(`${sdkmangerPath} "system-images;android-${apiLevel};${target};${arch}"`); yield exec.exec(`bash -c \\"${sdkmangerPath} --update > /dev/null"`); }); } diff --git a/src/emulator-launcher.ts b/src/emulator-manager.ts similarity index 53% rename from src/emulator-launcher.ts rename to src/emulator-manager.ts index bad092f0..a4784867 100644 --- a/src/emulator-launcher.ts +++ b/src/emulator-manager.ts @@ -1,17 +1,21 @@ import * as exec from '@actions/exec'; const EMULATOR_BOOT_TIMEOUT_SECONDS = 120; +const AVD_MANAGER_PATH = `${process.env.ANDROID_HOME}/tools/bin/avdmanager`; +const ADB_PATH = `${process.env.ANDROID_HOME}/platform-tools/adb`; /** * Creates and launches a new AVD instance with the specified configurations. */ -export async function launchEmulator(apiLevel: number, target: string, abi: string, device: string, headless: boolean, disableAnimations: boolean): Promise { - const avdmangerPath = `${process.env.ANDROID_HOME}/tools/bin/avdmanager`; - const adbPath = `${process.env.ANDROID_HOME}/platform-tools/adb`; - +export async function launchEmulator(apiLevel: number, target: string, arch: string, profile: string, headless: boolean, disableAnimations: boolean): Promise { // create a new AVD - console.log('Creating AVD.'); - await exec.exec(`${avdmangerPath} create avd -n test --force --abi "${target}/${abi}" --package "system-images;android-${apiLevel};${target};${abi}" --device "${device}"`); + if (profile.trim() !== '') { + console.log(`Creating AVD with custom profile ${profile}`); + await exec.exec(`${AVD_MANAGER_PATH} create avd --force -n test --abi "${target}/${arch}" --package "system-images;android-${apiLevel};${target};${arch}" --device "${profile}"`); + } else { + console.log(`Creating AVD without custom profile.`); + await exec.exec(`bash -c \\"echo no | ${AVD_MANAGER_PATH} create avd --force -n test --abi '${target}/${arch}' --package 'system-images;android-${apiLevel};${target};${arch}'`); + } // start emulator console.log('Starting emulator.'); @@ -20,18 +24,26 @@ export async function launchEmulator(apiLevel: number, target: string, abi: stri // wait for emulator to complete booting await waitForDevice(); - await exec.exec(`${adbPath} shell input keyevent 82`); + await exec.exec(`${ADB_PATH} shell input keyevent 82`); // disable animations if (disableAnimations) { console.log('Disabling animations.'); - await exec.exec(`${adbPath} shell settings put global window_animation_scale 0.0`); - await exec.exec(`${adbPath} shell settings put global transition_animation_scale 0.0`); - await exec.exec(`${adbPath} shell settings put global animator_duration_scale 0.0`); + await exec.exec(`${ADB_PATH} shell settings put global window_animation_scale 0.0`); + await exec.exec(`${ADB_PATH} shell settings put global transition_animation_scale 0.0`); + await exec.exec(`${ADB_PATH} shell settings put global animator_duration_scale 0.0`); } +} - // kill emulator - await exec.exec(`${adbPath} -s emulator-5554 emu kill`); +/** + * Kills the running emulator on the defaut port. + */ +export async function killEmulator(): Promise { + try { + await exec.exec(`${ADB_PATH} -s emulator-5554 emu kill`); + } catch (error) { + console.log('No emulator running on port 5554'); + } } /** @@ -58,8 +70,8 @@ async function waitForDevice(): Promise { booted = true; break; } - } catch (e) { - console.error(e.message); + } catch (error) { + console.error(error.message); } if (attempts < maxAttemps) { diff --git a/src/input-validator.ts b/src/input-validator.ts index 635663d8..a635d319 100644 --- a/src/input-validator.ts +++ b/src/input-validator.ts @@ -1,6 +1,6 @@ export const MIN_API_LEVEL = 21; export const VALID_TARGETS: Array = ['default', 'google_apis']; -export const VALID_ABIS: Array = ['x86', 'x86_64']; +export const VALID_ARCHS: Array = ['x86', 'x86_64']; export function checkApiLevel(apiLevel: string): void { if (isNaN(Number(apiLevel)) || !Number.isInteger(Number(apiLevel))) { @@ -17,9 +17,9 @@ export function checkTarget(target: string): void { } } -export function checkAbi(abi: string): void { - if (!VALID_ABIS.includes(abi)) { - throw new Error(`Value for input.abi '${abi}' is unknown. Supported options: ${VALID_ABIS}.`); +export function checkArch(arch: string): void { + if (!VALID_ARCHS.includes(arch)) { + throw new Error(`Value for input.arch '${arch}' is unknown. Supported options: ${VALID_ARCHS}.`); } } diff --git a/src/main.ts b/src/main.ts index d73dd0bb..042d7114 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,8 @@ import * as core from '@actions/core'; import { installAndroidSdk } from './sdk-installer'; -import { checkApiLevel, checkTarget, checkAbi, checkHeadless, checkDisableAnimations } from './input-validator'; -import { launchEmulator } from './emulator-launcher'; +import { checkApiLevel, checkTarget, checkArch, checkHeadless, checkDisableAnimations } from './input-validator'; +import { launchEmulator, killEmulator } from './emulator-manager'; +import * as exec from '@actions/exec'; async function run() { try { @@ -21,10 +22,14 @@ async function run() { checkTarget(target); console.log(`target: ${target}`); - // CPU / ABI of the system image - const abi = core.getInput('abi'); - checkAbi(abi); - console.log(`cpu/abi: ${abi}`); + // CPU architecture of the system image + const arch = core.getInput('arch'); + checkArch(arch); + console.log(`CPI architecture: ${arch}`); + + // Hardware profile used for creating the AVD + const profile = core.getInput('profile'); + console.log(`Hardware profile: ${profile}`); // headless mode const headlessInput = core.getInput('headless'); @@ -38,16 +43,26 @@ async function run() { const disableAnimations = disableAnimationsInput === 'true'; console.log(`disable animations: ${disableAnimations}`); + // custom scrpt to run + const scriptInput = core.getInput('script', { required: true }); + const commands = scriptInput.split(/\r?\n/); + // install SDK - await installAndroidSdk(apiLevel, target, abi); + await installAndroidSdk(apiLevel, target, arch); - // launch emulator - // TODO get from input (source list of all profiles) - const device = 'Nexus 6P'; - await launchEmulator(apiLevel, target, abi, device, headless, disableAnimations); + // launch an emulator + await launchEmulator(apiLevel, target, arch, profile, headless, disableAnimations); - // TODO start emulator + // execute the custom script + commands.forEach(async command => { + await exec.exec(`${command}`); + }); + + // finally kill the emulator + await killEmulator(); } catch (error) { + // kill the emulator so the action can exit + await killEmulator(); core.setFailed(error.message); } } diff --git a/src/sdk-installer.ts b/src/sdk-installer.ts index c7a60a33..980812a6 100644 --- a/src/sdk-installer.ts +++ b/src/sdk-installer.ts @@ -2,13 +2,14 @@ import * as exec from '@actions/exec'; import * as tc from '@actions/tool-cache'; import * as fs from 'fs'; +const BUILD_TOOLS_VERSION = '29.0.2'; const SDK_URL = 'https://dl.google.com/android/repository/sdk-tools-darwin-4333796.zip'; /** * Downloads and installs the Android SDK for the macOS platform, including SDK platform for the chosen API level, latest build tools, platform tools, Android Emulator, - * and the system image for the chosen API level, cpu/abi, and target. + * and the system image for the chosen API level, CPU arch, and target. */ -export async function installAndroidSdk(apiLevel: number, target: string, abi: string): Promise { +export async function installAndroidSdk(apiLevel: number, target: string, arch: string): Promise { // download Android SDK if not already installed if (fs.existsSync(`${process.env.ANDROID_HOME}/tools/bin/sdkmanager`)) { console.log('Android SDK already installed.'); @@ -22,7 +23,8 @@ export async function installAndroidSdk(apiLevel: number, target: string, abi: s console.log('Installing build tools, platform tools, platform and system image.'); const sdkmangerPath = `${process.env.ANDROID_HOME}/tools/bin/sdkmanager`; await exec.exec(`echo "y" | ${sdkmangerPath} --licenses > /dev/null`); - await exec.exec(`${sdkmangerPath} tools platform-tools "platforms;android-${apiLevel}"`); - await exec.exec(`${sdkmangerPath} "system-images;android-${apiLevel};${target};${abi}"`); + await exec.exec(`${sdkmangerPath} "build-tools;${BUILD_TOOLS_VERSION}"`); + await exec.exec(`${sdkmangerPath} platform-tools "platforms;android-${apiLevel}"`); + await exec.exec(`${sdkmangerPath} "system-images;android-${apiLevel};${target};${arch}"`); await exec.exec(`bash -c \\"${sdkmangerPath} --update > /dev/null"`); }