Skip to content

Commit

Permalink
fix: add patch for watchers limit and enospc error
Browse files Browse the repository at this point in the history
  • Loading branch information
byCedric committed Oct 28, 2019
1 parent a80a00b commit cbd9ace
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 14 deletions.
23 changes: 17 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
47 changes: 47 additions & 0 deletions build/system.js
Original file line number Diff line number Diff line change
@@ -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;
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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();
28 changes: 28 additions & 0 deletions src/system.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
57 changes: 49 additions & 8 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,50 @@
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();
expect(install.install).toBeCalledWith('latest', 'npm');
});

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');
});
Expand All @@ -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');
});
Expand Down
56 changes: 56 additions & 0 deletions tests/system.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});

0 comments on commit cbd9ace

Please sign in to comment.