From 831bcb12d38028687ace4684d972c6983fe5c924 Mon Sep 17 00:00:00 2001 From: Cedric van Putten Date: Wed, 30 Oct 2019 20:54:15 +0000 Subject: [PATCH] fix: use cmd suffix when authenticating on windows --- build/expo.js | 6 ++- build/src/expo.js | 40 ++++++++++++++++++++ build/src/index.js | 28 ++++++++++++++ build/src/install.js | 90 ++++++++++++++++++++++++++++++++++++++++++++ build/src/system.js | 47 +++++++++++++++++++++++ build/tests/utils.js | 19 ++++++++++ src/expo.ts | 7 +++- tests/expo.test.ts | 9 +++++ tests/system.test.ts | 18 ++++----- tests/utils.ts | 17 +++++++++ tsconfig.json | 2 +- 11 files changed, 269 insertions(+), 14 deletions(-) create mode 100644 build/src/expo.js create mode 100644 build/src/index.js create mode 100644 build/src/install.js create mode 100644 build/src/system.js create mode 100644 build/tests/utils.js create mode 100644 tests/utils.ts diff --git a/build/expo.js b/build/expo.js index 34442d78..f3b81443 100644 --- a/build/expo.js +++ b/build/expo.js @@ -28,7 +28,11 @@ function authenticate(username, password) { if (!username || !password) { return core.debug('Skipping authentication, `expo-username` and/or `expo-password` not set...'); } - yield cli.exec('expo', ['login', `--username=${username}`], { + // github actions toolkit will handle commands with `.cmd` on windows, we need that + const bin = process.platform === 'win32' + ? 'expo.cmd' + : 'expo'; + yield cli.exec(bin, ['login', `--username=${username}`], { env: Object.assign(Object.assign({}, process.env), { EXPO_CLI_PASSWORD: password }), }); }); diff --git a/build/src/expo.js b/build/src/expo.js new file mode 100644 index 00000000..f3b81443 --- /dev/null +++ b/build/src/expo.js @@ -0,0 +1,40 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const core = __importStar(require("@actions/core")); +const cli = __importStar(require("@actions/exec")); +/** + * Authenticate at Expo using `expo login`. + * This step is required for publishing and building new apps. + * It uses the `EXPO_CLI_PASSWORD` environment variable for improved security. + */ +function authenticate(username, password) { + return __awaiter(this, void 0, void 0, function* () { + if (!username || !password) { + return core.debug('Skipping authentication, `expo-username` and/or `expo-password` not set...'); + } + // github actions toolkit will handle commands with `.cmd` on windows, we need that + const bin = process.platform === 'win32' + ? 'expo.cmd' + : 'expo'; + yield cli.exec(bin, ['login', `--username=${username}`], { + env: Object.assign(Object.assign({}, process.env), { EXPO_CLI_PASSWORD: password }), + }); + }); +} +exports.authenticate = authenticate; diff --git a/build/src/index.js b/build/src/index.js new file mode 100644 index 00000000..3c0af110 --- /dev/null +++ b/build/src/index.js @@ -0,0 +1,28 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const core_1 = require("@actions/core"); +const expo_1 = require("./expo"); +const install_1 = require("./install"); +const system_1 = require("./system"); +function run() { + return __awaiter(this, void 0, void 0, function* () { + const path = yield install_1.install(core_1.getInput('expo-version') || 'latest', core_1.getInput('expo-packager') || 'npm'); + core_1.addPath(path); + yield expo_1.authenticate(core_1.getInput('expo-username'), core_1.getInput('expo-password')); + const shouldPatchWatchers = core_1.getInput('expo-patch-watchers') || 'true'; + if (shouldPatchWatchers !== 'false') { + yield system_1.patchWatchers(); + } + }); +} +exports.run = run; +run(); diff --git a/build/src/install.js b/build/src/install.js new file mode 100644 index 00000000..256e1533 --- /dev/null +++ b/build/src/install.js @@ -0,0 +1,90 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const cache = __importStar(require("@actions/tool-cache")); +const cli = __importStar(require("@actions/exec")); +const io = __importStar(require("@actions/io")); +const path = __importStar(require("path")); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const registry = require('libnpm'); +/** + * Resolve the provided semver to exact version of `expo-cli`. + * This uses the npm registry and accepts latest, dist-tags or version ranges. + * It's used to determine the cached version of `expo-cli`. + */ +function resolve(version) { + return __awaiter(this, void 0, void 0, function* () { + return (yield registry.manifest(`expo-cli@${version}`)).version; + }); +} +exports.resolve = resolve; +/** + * Install `expo-cli`, by version, using the packager. + * Here you can provide any semver range or dist tag used in the registry. + * It returns the path where Expo is installed. + */ +function install(version, packager) { + return __awaiter(this, void 0, void 0, function* () { + const exact = yield resolve(version); + let root = yield fromCache(exact); + if (!root) { + root = yield fromPackager(exact, packager); + root = yield toCache(exact, root); + } + return path.join(root, 'node_modules', '.bin'); + }); +} +exports.install = install; +/** + * Install `expo-cli`, by version, using npm or yarn. + * It creates a temporary directory to store all required files. + */ +function fromPackager(version, packager) { + return __awaiter(this, void 0, void 0, function* () { + const root = process.env['RUNNER_TEMP'] || ''; + const tool = yield io.which(packager); + yield io.mkdirP(root); + yield cli.exec(tool, ['add', `expo-cli@${version}`], { cwd: root }); + return root; + }); +} +exports.fromPackager = fromPackager; +/** + * Get the path to the `expo-cli` from cache, if any. + * Note, this cache is **NOT** shared between jobs. + * + * @see https://github.com/actions/toolkit/issues/47 + */ +function fromCache(version) { + return __awaiter(this, void 0, void 0, function* () { + return cache.find('expo-cli', version); + }); +} +exports.fromCache = fromCache; +/** + * Store the root of `expo-cli` in the cache, for future reuse. + * Note, this cache is **NOT** shared between jobs. + * + * @see https://github.com/actions/toolkit/issues/47 + */ +function toCache(version, root) { + return __awaiter(this, void 0, void 0, function* () { + return cache.cacheDir(root, 'expo-cli', version); + }); +} +exports.toCache = toCache; diff --git a/build/src/system.js b/build/src/system.js new file mode 100644 index 00000000..9ace82e4 --- /dev/null +++ b/build/src/system.js @@ -0,0 +1,47 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const core = __importStar(require("@actions/core")); +const cli = __importStar(require("@actions/exec")); +/** + * Try to patch the default watcher/inotify limit. + * This is a limitation from GitHub Actions and might be an issue in some Expo projects. + * It sets the system's `fs.inotify` limits to a more sensible setting. + * + * @see https://github.com/expo/expo-github-action/issues/20 + */ +function patchWatchers() { + return __awaiter(this, void 0, void 0, function* () { + if (process.platform !== 'linux') { + return core.debug('Skipping patch for watchers, not running on Linux...'); + } + core.debug('Patching system watchers for the `ENOSPC` error...'); + try { + // see https://github.com/expo/expo-cli/issues/277#issuecomment-452685177 + yield cli.exec('sudo sysctl fs.inotify.max_user_instances=524288'); + yield cli.exec('sudo sysctl fs.inotify.max_user_watches=524288'); + yield cli.exec('sudo sysctl fs.inotify.max_queued_events=524288'); + yield cli.exec('sudo sysctl -p'); + } + catch (_a) { + core.warning('Looks like we can\'t patch watchers/inotify limits, you might encouter the `ENOSPC` error.'); + core.warning('For more info, https://github.com/expo/expo-github-action/issues/20'); + } + }); +} +exports.patchWatchers = patchWatchers; diff --git a/build/tests/utils.js b/build/tests/utils.js new file mode 100644 index 00000000..aaa176a5 --- /dev/null +++ b/build/tests/utils.js @@ -0,0 +1,19 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +// keep track of the original one to revert the platform +const originalPlatform = process.platform; +/** + * Change the node platform for testing purposes. + * With this you can fake the `process.platform`. + */ +function setPlatform(platform) { + Object.defineProperty(process, 'platform', { value: platform }); +} +exports.setPlatform = setPlatform; +/** + * Revert the platform to the original one. + */ +function resetPlatform() { + setPlatform(originalPlatform); +} +exports.resetPlatform = resetPlatform; diff --git a/src/expo.ts b/src/expo.ts index c67b1390..260dae6f 100644 --- a/src/expo.ts +++ b/src/expo.ts @@ -11,7 +11,12 @@ export async function authenticate(username: string, password: string) { return core.debug('Skipping authentication, `expo-username` and/or `expo-password` not set...'); } - await cli.exec('expo', ['login', `--username=${username}`], { + // github actions toolkit will handle commands with `.cmd` on windows, we need that + const bin = process.platform === 'win32' + ? 'expo.cmd' + : 'expo'; + + await cli.exec(bin, ['login', `--username=${username}`], { env: { ...process.env, EXPO_CLI_PASSWORD: password, diff --git a/tests/expo.test.ts b/tests/expo.test.ts index b72f88f4..acacf30e 100644 --- a/tests/expo.test.ts +++ b/tests/expo.test.ts @@ -5,6 +5,7 @@ jest.mock('@actions/core', () => core); jest.mock('@actions/exec', () => cli); import * as expo from '../src/expo'; +import { setPlatform, resetPlatform } from './utils'; describe('authenticate', () => { test('skips authentication without credentials', async () => { @@ -33,4 +34,12 @@ describe('authenticate', () => { }, }); }); + + test('executes login command with `.cmd` suffix on windows', async () => { + setPlatform('win32'); + await expo.authenticate('bycedric', 'mypassword'); + expect(cli.exec).toBeCalled(); + expect(cli.exec.mock.calls[0][0]).toBe('expo.cmd'); + resetPlatform(); + }); }); diff --git a/tests/system.test.ts b/tests/system.test.ts index 63eb7a3b..fd8b681c 100644 --- a/tests/system.test.ts +++ b/tests/system.test.ts @@ -5,19 +5,15 @@ jest.mock('@actions/core', () => core); jest.mock('@actions/exec', () => cli); import * as system from '../src/system'; +import { setPlatform, resetPlatform } from './utils'; describe('patchWatchers', () => { - const originalPlatform = process.platform; - const changePlatform = (platform: NodeJS.Platform) => { - Object.defineProperty(process, 'platform', { value: platform }); - }; - afterEach(() => { - changePlatform(originalPlatform); + resetPlatform(); }); it('increses fs inotify settings with sysctl', async () => { - changePlatform('linux'); + setPlatform('linux'); await system.patchWatchers(); expect(cli.exec).toHaveBeenCalledWith('sudo sysctl fs.inotify.max_user_instances=524288'); expect(cli.exec).toHaveBeenCalledWith('sudo sysctl fs.inotify.max_user_watches=524288'); @@ -28,7 +24,7 @@ describe('patchWatchers', () => { it('warns for unsuccessful patches', async () => { const error = new Error('Something went wrong'); cli.exec.mockRejectedValue(error); - changePlatform('linux'); + setPlatform('linux'); await system.patchWatchers(); expect(core.warning).toBeCalledWith(expect.stringContaining('can\'t patch watchers')); expect(core.warning).toBeCalledWith( @@ -37,19 +33,19 @@ describe('patchWatchers', () => { }); it('skips on windows platform', async () => { - changePlatform('win32'); + setPlatform('win32'); await system.patchWatchers(); expect(cli.exec).not.toHaveBeenCalled(); }); it('skips on macos platform', async () => { - changePlatform('darwin'); + setPlatform('darwin'); await system.patchWatchers(); expect(cli.exec).not.toHaveBeenCalled(); }); it('runs on linux platform', async () => { - changePlatform('linux'); + setPlatform('linux'); await system.patchWatchers(); expect(cli.exec).toHaveBeenCalled(); }); diff --git a/tests/utils.ts b/tests/utils.ts new file mode 100644 index 00000000..9cdb9a4a --- /dev/null +++ b/tests/utils.ts @@ -0,0 +1,17 @@ +// keep track of the original one to revert the platform +const originalPlatform = process.platform; + +/** + * Change the node platform for testing purposes. + * With this you can fake the `process.platform`. + */ +export function setPlatform(platform: NodeJS.Platform) { + Object.defineProperty(process, 'platform', { value: platform }); +} + +/** + * Revert the platform to the original one. + */ +export function resetPlatform() { + setPlatform(originalPlatform); +} diff --git a/tsconfig.json b/tsconfig.json index 4c44fe8a..895e4365 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,6 @@ }, "exclude": [ "node_modules", - "**/*.test.ts" + "tests/**" ] }