Skip to content

Commit 917e072

Browse files
authored
refactor: improve error handling with update check (#76)
1 parent ed7f67d commit 917e072

15 files changed

Lines changed: 100892 additions & 90514 deletions

build/index.js

Lines changed: 100448 additions & 90151 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@
2626
"@actions/exec": "^1.0.4",
2727
"@actions/io": "^1.0.2",
2828
"@actions/tool-cache": "^1.6.1",
29-
"libnpm": "^3.0.1"
29+
"libnpm": "^3.0.1",
30+
"semver": "^7.3.4"
3031
},
3132
"devDependencies": {
3233
"@types/jest": "^26.0.16",
3334
"@types/node": "^14.14.10",
35+
"@types/semver": "^7.3.4",
3436
"@typescript-eslint/eslint-plugin": "^4.9.0",
3537
"@typescript-eslint/parser": "^4.9.0",
3638
"@zeit/ncc": "^0.22.3",

src/expo.ts

Lines changed: 0 additions & 71 deletions
This file was deleted.

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as core from '@actions/core';
21
import { run } from './run';
2+
import { handleError } from './tools';
33

4-
run().catch(core.setFailed);
4+
run().catch(handleError);

src/install.ts

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ import * as core from '@actions/core';
22
import * as cli from '@actions/exec';
33
import * as io from '@actions/io';
44
import * as path from 'path';
5-
import { fromLocalCache, fromRemoteCache, toLocalCache, toRemoteCache } from './cache';
65

7-
// eslint-disable-next-line @typescript-eslint/no-var-requires
8-
const registry = require('libnpm');
6+
import { fromLocalCache, fromRemoteCache, toLocalCache, toRemoteCache } from './cache';
97

108
export type InstallConfig = {
119
version: string;
@@ -14,36 +12,26 @@ export type InstallConfig = {
1412
cacheKey?: string;
1513
};
1614

17-
/**
18-
* Resolve the provided semver to exact version of `expo-cli`.
19-
* This uses the npm registry and accepts latest, dist-tags or version ranges.
20-
* It's used to determine the cached version of `expo-cli`.
21-
*/
22-
export async function resolve(version: string): Promise<string> {
23-
return (await registry.manifest(`expo-cli@${version}`)).version;
24-
}
25-
2615
/**
2716
* Install `expo-cli`, by version, using the packager.
2817
* Here you can provide any semver range or dist tag used in the registry.
2918
* It returns the path where Expo is installed.
3019
*/
3120
export async function install(config: InstallConfig): Promise<string> {
32-
const exact = await resolve(config.version);
33-
let root: string | undefined = await fromLocalCache(exact);
21+
let root: string | undefined = await fromLocalCache(config.version);
3422

3523
if (!root && config.cache) {
36-
root = await fromRemoteCache(exact, config.packager, config.cacheKey);
24+
root = await fromRemoteCache(config.version, config.packager, config.cacheKey);
3725
} else {
3826
core.info('Skipping remote cache, not enabled...');
3927
}
4028

4129
if (!root) {
42-
root = await fromPackager(exact, config.packager);
43-
root = await toLocalCache(root, exact);
30+
root = await fromPackager(config.version, config.packager);
31+
root = await toLocalCache(root, config.version);
4432

4533
if (config.cache) {
46-
await toRemoteCache(root, exact, config.packager, config.cacheKey);
34+
await toRemoteCache(root, config.version, config.packager, config.cacheKey);
4735
}
4836
}
4937

src/run.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { addPath, getInput, group } from '@actions/core';
2-
import { authenticate } from './expo';
2+
33
import { install, InstallConfig } from './install';
4-
import { patchWatchers } from './system';
4+
import { maybeAuthenticate, maybePatchWatchers, resolveVersion } from './tools';
55

66
export async function run(): Promise<void> {
77
const config: InstallConfig = {
@@ -11,18 +11,21 @@ export async function run(): Promise<void> {
1111
cacheKey: getInput('expo-cache-key') || undefined,
1212
};
1313

14+
// Resolve the exact requested Expo CLI version
15+
config.version = await resolveVersion(config.version);
16+
1417
const path = await group(
1518
config.cache
16-
? `Installing Expo CLI from cache or with ${config.packager}`
17-
: `Installing Expo CLI with ${config.packager}`,
19+
? `Installing Expo CLI (${config.version}) from cache or with ${config.packager}`
20+
: `Installing Expo CLI (${config.version}) with ${config.packager}`,
1821
() => install(config),
1922
);
2023

2124
addPath(path);
2225

2326
await group(
2427
'Checking current authenticated account',
25-
() => authenticate({
28+
() => maybeAuthenticate({
2629
token: getInput('expo-token') || undefined,
2730
username: getInput('expo-username') || undefined,
2831
password: getInput('expo-password') || undefined,
@@ -34,7 +37,7 @@ export async function run(): Promise<void> {
3437
if (shouldPatchWatchers !== 'false') {
3538
await group(
3639
'Patching system watchers for the `ENOSPC` error',
37-
() => patchWatchers(),
40+
() => maybePatchWatchers(),
3841
);
3942
}
4043
}

src/system.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/tools.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import * as core from '@actions/core';
2+
import * as cli from '@actions/exec';
3+
import semver from 'semver';
4+
5+
// eslint-disable-next-line @typescript-eslint/no-var-requires
6+
const registry = require('libnpm');
7+
8+
type AuthenticateOptions = {
9+
token?: string;
10+
username?: string;
11+
password?: string;
12+
};
13+
14+
/**
15+
* Resolve the provided semver to exact version of `expo-cli`.
16+
* This uses the npm registry and accepts latest, dist-tags or version ranges.
17+
* It's used to determine the cached version of `expo-cli`.
18+
*/
19+
export async function resolveVersion(version: string): Promise<string> {
20+
return (await registry.manifest(`expo-cli@${version}`)).version;
21+
}
22+
23+
/**
24+
* Authenticate with Expo using either the token or username/password method.
25+
* If both of them are set, token has priority.
26+
*/
27+
export async function maybeAuthenticate(options: AuthenticateOptions = {}): Promise<void> {
28+
// github actions toolkit will handle commands with `.cmd` on windows, we need that
29+
const bin = process.platform === 'win32' ? 'expo.cmd' : 'expo';
30+
31+
if (options.token) {
32+
await cli.exec(bin, ['whoami'], {
33+
env: { ...process.env, EXPO_TOKEN: options.token },
34+
});
35+
return core.exportVariable('EXPO_TOKEN', options.token);
36+
}
37+
38+
if (options.username || options.password) {
39+
if (!options.username || !options.password) {
40+
return core.info('Skipping authentication: `expo-username` and/or `expo-password` not set...');
41+
}
42+
await cli.exec(bin, ['login', `--username=${options.username}`], {
43+
env: { ...process.env, EXPO_CLI_PASSWORD: options.password },
44+
});
45+
return;
46+
}
47+
48+
core.info('Skipping authentication: `expo-token`, `expo-username`, and/or `expo-password` not set...');
49+
}
50+
51+
/**
52+
* Try to patch the default watcher/inotify limit.
53+
* This is a limitation from GitHub Actions and might be an issue in some Expo projects.
54+
* It sets the system's `fs.inotify` limits to a more sensible setting.
55+
*
56+
* @see https://github.com/expo/expo-github-action/issues/20
57+
*/
58+
export async function maybePatchWatchers(): Promise<void> {
59+
if (process.platform !== 'linux') {
60+
return core.info('Skipping patch for watchers, not running on Linux...');
61+
}
62+
63+
core.info('Patching system watchers for the `ENOSPC` error...');
64+
65+
try {
66+
// see https://github.com/expo/expo-cli/issues/277#issuecomment-452685177
67+
await cli.exec('sudo sysctl fs.inotify.max_user_instances=524288');
68+
await cli.exec('sudo sysctl fs.inotify.max_user_watches=524288');
69+
await cli.exec('sudo sysctl fs.inotify.max_queued_events=524288');
70+
await cli.exec('sudo sysctl -p');
71+
} catch {
72+
core.warning("Looks like we can't patch watchers/inotify limits, you might encouter the `ENOSPC` error.");
73+
core.warning('For more info, https://github.com/expo/expo-github-action/issues/20');
74+
}
75+
}
76+
77+
/**
78+
* Check if there is a new major version available.
79+
* If there is, create a warning for people to upgrade their workflow.
80+
* Because this introduces additional requests, it should only be executed when necessary.
81+
*/
82+
export async function maybeWarnForUpdate(): Promise<void> {
83+
const latest = await resolveVersion('latest');
84+
const current = await resolveVersion(core.getInput('expo-version') || 'latest');
85+
86+
if (semver.diff(latest, current) === 'major') {
87+
core.warning(`There is a new major version available of the Expo CLI (${latest})`);
88+
core.warning(`If you run into issues, try upgrading your workflow to "expo-version: ${semver.major(latest)}.x"`);
89+
}
90+
}
91+
92+
/**
93+
* Handle errors when this action fails, providing useful next-steps for developers.
94+
* This mostly checks if the installed version is the latest version.
95+
*/
96+
export async function handleError(error: Error) {
97+
try {
98+
await maybeWarnForUpdate();
99+
} catch {
100+
// If this fails, ignore it
101+
}
102+
103+
core.setFailed(error);
104+
}

tests/cache.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import os from 'os';
22
import { join } from 'path';
33
import * as remoteCache from '@actions/cache';
44
import * as toolCache from '@actions/tool-cache';
5+
56
import * as cache from '../src/cache';
67
import * as utils from './utils';
78

0 commit comments

Comments
 (0)