From cbd9aceff6b964db4b8d8ac94a7b8b3a879dcfb3 Mon Sep 17 00:00:00 2001 From: Cedric van Putten Date: Mon, 28 Oct 2019 18:16:33 +0000 Subject: [PATCH] fix: add patch for watchers limit and enospc error --- README.md | 23 +++++++++++++----- action.yml | 3 +++ build/index.js | 5 ++++ build/system.js | 47 ++++++++++++++++++++++++++++++++++++ src/index.ts | 7 ++++++ src/system.ts | 28 ++++++++++++++++++++++ tests/index.test.ts | 57 +++++++++++++++++++++++++++++++++++++------- tests/system.test.ts | 56 +++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 212 insertions(+), 14 deletions(-) create mode 100644 build/system.js create mode 100644 src/system.ts create mode 100644 tests/system.test.ts diff --git a/README.md b/README.md index cb475c9f..e9c65717 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,13 @@ Also, this action takes care of authentication when both `expo-username` and `ex This action is customizable through variables; they are defined in the [`action.yml`][link-expo-cli-action]. Here is a summary of all the variables that you can use and their purpose. -variable | description ---- | --- -`expo-username` | The username of your Expo account. _(you can hardcode this or use secrets)_ -`expo-password` | The password of your Expo account. _**([use this with secrets][link-actions-secrets])**_ -`expo-version` | The Expo CLI you want to use. _(can be any semver range, defaults to `latest`)_ -`expo-packager` | The package manager you want to use to install the CLI. _(can be `npm` or `yarn`, defaults to `npm`)_ +variable | description +--- | --- +`expo-username` | The username of your Expo account. _(you can hardcode this or use secrets)_ +`expo-password` | The password of your Expo account. _**([use this with secrets][link-actions-secrets])**_ +`expo-version` | The Expo CLI you want to use. _(can be any semver range, defaults to `latest`)_ +`expo-packager` | The package manager you want to use to install the CLI. _(can be `npm` or `yarn`, defaults to `npm`)_ +`expo-patch-watchers` | If it should patch the `fs.inotify.` limits causing `ENOSPC` errors on Linux. _(can be `true` or `false`, defaults to `true`)_ > It's recommended to set the `expo-version` to avoid breaking changes when a new major version is released. > For more info on how to use this, please read the [workflow syntax documentation][link-actions-syntax-with]. @@ -227,6 +228,16 @@ Please note that this approach has its limitations and make sure you understand When GitHub releases this caching feature, we will implement this feature and it and make it significantly faster. +#### ENOSPC errors on Linux + +React Native bundles are created by the Metro bundler, even when using Expo. +Unfortunately, this Metro bundler requires quite some resources. +As of writing, GitHub Actions has some small default values for the `fs.inotify` settings. +Inside we included a patch that increases these limits for the "active workflow" run. +It increases the `max_user_instances`, `max_user_watches` and `max_queued_events` to `524288`. +You can disable this patch by setting the `expo-patch-watchers` to `false`. + + ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/action.yml b/action.yml index 340dd94f..89343079 100644 --- a/action.yml +++ b/action.yml @@ -19,3 +19,6 @@ input: expo-packager: description: The package manager used to install the Expo CLI. (can be yarn or npm) default: npm + expo-patch-watchers: + description: If Expo should fix the default watchers limit, helps with ENOSPC errors. (can be true or false) + default: true diff --git a/build/index.js b/build/index.js index f256cc0a..3c0af110 100644 --- a/build/index.js +++ b/build/index.js @@ -12,11 +12,16 @@ 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; diff --git a/build/system.js b/build/system.js new file mode 100644 index 00000000..9ace82e4 --- /dev/null +++ b/build/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/src/index.ts b/src/index.ts index 6a911690..46a931cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import { addPath, getInput } from '@actions/core'; import { authenticate } from './expo'; import { install } from './install'; +import { patchWatchers } from './system'; export async function run() { const path = await install( @@ -14,6 +15,12 @@ export async function run() { getInput('expo-username'), getInput('expo-password'), ); + + const shouldPatchWatchers = getInput('expo-patch-watchers') || 'true'; + + if (shouldPatchWatchers !== 'false') { + await patchWatchers(); + } } run(); diff --git a/src/system.ts b/src/system.ts new file mode 100644 index 00000000..d3fedf5b --- /dev/null +++ b/src/system.ts @@ -0,0 +1,28 @@ +import * as core from '@actions/core'; +import * as cli from '@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 + */ +export async function patchWatchers() { + 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 + await cli.exec('sudo sysctl fs.inotify.max_user_instances=524288'); + await cli.exec('sudo sysctl fs.inotify.max_user_watches=524288'); + await cli.exec('sudo sysctl fs.inotify.max_queued_events=524288'); + await cli.exec('sudo sysctl -p'); + } catch { + 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'); + } +} diff --git a/tests/index.test.ts b/tests/index.test.ts index e3be636a..99ff5950 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,13 +1,42 @@ const core = { addPath: jest.fn(), getInput: jest.fn() }; +const exec = { exec: jest.fn() }; const expo = { authenticate: jest.fn() }; const install = { install: jest.fn() }; +const system = { patchWatchers: jest.fn() }; jest.mock('@actions/core', () => core); +jest.mock('@actions/exec', () => exec); jest.mock('../src/expo', () => expo); jest.mock('../src/install', () => install); +jest.mock('../src/system', () => system); import { run } from '../src/index'; +interface MockInputProps { + version?: string; + packager?: string; + username?: string; + password?: string; + patchWatchers?: string; +} + +const mockInput = (props: MockInputProps = {}) => { + // fix: kind of dirty workaround for missing "mock 'value' based on arguments" + const input = (name: string) => { + switch (name) { + case 'expo-version': return props.version || ''; + case 'expo-packager': return props.packager || ''; + case 'expo-username': return props.username || ''; + case 'expo-password': return props.password || ''; + case 'expo-patch-watchers': return props.patchWatchers || ''; + default: return ''; + } + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + core.getInput = input as any; +}; + describe('run', () => { test('installs latest expo-cli with npm by default', async () => { await run(); @@ -15,9 +44,7 @@ describe('run', () => { }); test('installs provided version expo-cli with yarn', async () => { - // fix: kind of dirty workaround for missing "mock 'value' for arg 'expo-version'" - core.getInput.mockReturnValueOnce('3.0.10'); - core.getInput.mockReturnValueOnce('yarn'); + mockInput({ version: '3.0.10', packager: 'yarn' }); await run(); expect(install.install).toBeCalledWith('3.0.10', 'yarn'); }); @@ -28,12 +55,26 @@ describe('run', () => { expect(core.addPath).toBeCalledWith('/expo/install/path'); }); + test('patches the system when set to true', async () => { + mockInput({ patchWatchers: 'true' }); + await run(); + expect(system.patchWatchers).toHaveBeenCalled(); + }); + + test('patches the system when not set', async () => { + mockInput({ patchWatchers: '' }); + await run(); + expect(system.patchWatchers).toHaveBeenCalled(); + }); + + test('skips the system patch when set to false', async () => { + mockInput({ patchWatchers: 'false' }); + await run(); + expect(system.patchWatchers).not.toHaveBeenCalled(); + }); + test('authenticates with provided credentials', async () => { - // fix: kind of dirty workaround for missing "mock 'value' for arg 'expo-version'" - core.getInput.mockReturnValueOnce('irrelevant'); - core.getInput.mockReturnValueOnce('irrelevant'); - core.getInput.mockReturnValueOnce('bycedric'); - core.getInput.mockReturnValueOnce('mypassword'); + mockInput({ username: 'bycedric', password: 'mypassword', patchWatchers: 'false' }); await run(); expect(expo.authenticate).toBeCalledWith('bycedric', 'mypassword'); }); diff --git a/tests/system.test.ts b/tests/system.test.ts new file mode 100644 index 00000000..63eb7a3b --- /dev/null +++ b/tests/system.test.ts @@ -0,0 +1,56 @@ +const core = { debug: jest.fn(), warning: jest.fn() }; +const cli = { exec: jest.fn() }; + +jest.mock('@actions/core', () => core); +jest.mock('@actions/exec', () => cli); + +import * as system from '../src/system'; + +describe('patchWatchers', () => { + const originalPlatform = process.platform; + const changePlatform = (platform: NodeJS.Platform) => { + Object.defineProperty(process, 'platform', { value: platform }); + }; + + afterEach(() => { + changePlatform(originalPlatform); + }); + + it('increses fs inotify settings with sysctl', async () => { + changePlatform('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'); + expect(cli.exec).toHaveBeenCalledWith('sudo sysctl fs.inotify.max_queued_events=524288'); + expect(cli.exec).toHaveBeenCalledWith('sudo sysctl -p'); + }); + + it('warns for unsuccessful patches', async () => { + const error = new Error('Something went wrong'); + cli.exec.mockRejectedValue(error); + changePlatform('linux'); + await system.patchWatchers(); + expect(core.warning).toBeCalledWith(expect.stringContaining('can\'t patch watchers')); + expect(core.warning).toBeCalledWith( + expect.stringContaining('https://github.com/expo/expo-github-action/issues/20') + ); + }); + + it('skips on windows platform', async () => { + changePlatform('win32'); + await system.patchWatchers(); + expect(cli.exec).not.toHaveBeenCalled(); + }); + + it('skips on macos platform', async () => { + changePlatform('darwin'); + await system.patchWatchers(); + expect(cli.exec).not.toHaveBeenCalled(); + }); + + it('runs on linux platform', async () => { + changePlatform('linux'); + await system.patchWatchers(); + expect(cli.exec).toHaveBeenCalled(); + }); +});