diff --git a/.releaserc.js b/.releaserc.js
index cfa51dc8..e63bd50f 100644
--- a/.releaserc.js
+++ b/.releaserc.js
@@ -16,7 +16,12 @@ module.exports = {
plugins: [
['@semantic-release/commit-analyzer', {
preset: 'conventionalcommits',
- releaseRules: rules.map(({ type, release }) => ({ type, release })),
+ releaseRules: [
+ { breaking: true, release: 'major' },
+ { revert: true, release: 'patch' },
+ ].concat(
+ rules.map(({ type, release, breaking }) => ({ type, release, breaking }))
+ ),
}],
['@semantic-release/release-notes-generator', {
preset: 'conventionalcommits',
diff --git a/README.md b/README.md
index b5d78ee4..fbc6ebc1 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@
## What's inside?
-With this Expo action, you have full access to the [Expo CLI][link-expo-cli] itself.
+With this Expo action, you have full access to [Expo CLI][link-expo-cli] and [EAS CLI][link-eas-cli] itself.
It allows you to fully automate the `expo publish` or `expo build` process, leaving you with more time available for your project.
There are some additional features included to make the usage of this action as simple as possible, like caching and authentication.
@@ -37,21 +37,22 @@ There are some additional features included to make the usage of this action as
This action is customizable through variables; they are defined in the [`action.yml`](action.yml).
Here is a summary of all the variables that you can use and their purpose.
-variable | default | description
---- | --- | ---
-`expo-username` | - | The username of your Expo account _(e.g. `bycedric`)_
-`expo-token` | - | The token of your Expo account _(e.g. [`${{ secrets.EXPO_TOKEN }}`][link-actions-secrets])_
-`expo-password` | - | The password of your Expo account _(e.g. [`${{ secrets.EXPO_CLI_PASSWORD }}`][link-actions-secrets])_
-`expo-version` | `latest` | The Expo CLI version to use, can be any [SemVer][link-semver-playground]. _(e.g. `3.x`)_
-`expo-packager` | `yarn` | The package manager to install the CLI with. _(e.g. `npm`)_
-`expo-cache` | `false` | If it should use the [GitHub actions (remote) cache](#using-the-built-in-cache).
-`expo-cache-key` | - | An optional custom (remote) cache key. _(**use with caution**)_
-`expo-patch-watchers` | `true` | If it should [patch the `fs.inotify.` limits](#enospc-errors-on-linux).
+| variable | default | description |
+| ---------------- | ------- | ------------------------------------------------------------------------------------------------------------ |
+| `expo-version` | - | [Expo CLI](https://github.com/expo/expo-cli) version to install, skips when omitted. |
+| `expo-cache` | `false` | If it should use the [GitHub actions (remote) cache](#using-the-built-in-cache). |
+| `expo-cache-key` | - | An optional custom (remote) cache key. _(**use with caution**)_ |
+| `eas-version` | - | [EAS CLI](https://github.com/expo/eas-cli) version to install, skips when omitted. (`latest` is recommended) |
+| `eas-cache` | `false` | If it should use the [GitHub actions (remote) cache](#using-the-built-in-cache). |
+| `eas-cache-key` | - | An optional custom (remote) cache key. _(**use with caution**)_ |
+| `packager` | `yarn` | The package manager to use. _(e.g. `npm`)_ |
+| `token` | - | The token of your Expo account _(e.g. [`${{ secrets.EXPO_TOKEN }}`][link-actions-secrets])_ |
+| `username` | - | The username of your Expo account _(e.g. `bycedric`)_ |
+| `password` | - | The password of your Expo account _(e.g. [`${{ secrets.EXPO_CLI_PASSWORD }}`][link-actions-secrets])_ |
+| `patch-watchers` | `true` | If it should [patch the `fs.inotify.` limits](#enospc-errors-on-linux). |
> Never hardcode `expo-token` or `expo-password` in your workflow, use [secrets][link-actions-secrets] to store them.
-> It's also recommended to set the `expo-version` to avoid breaking changes when a new major version is released.
-
## Example workflows
Before you dive into the workflow examples, you should know the basics of GitHub Actions.
@@ -59,16 +60,17 @@ You can read more about this in the [GitHub Actions documentation][link-actions]
1. [Publish on any push to master](#publish-on-any-push-to-master)
2. [Cache Expo CLI for other jobs](#cache-expo-cli-for-other-jobs)
-3. [Test PRs and publish a review version](#test-prs-and-publish-a-review-version)
-4. [Test PRs on multiple nodes and systems](#test-prs-on-multiple-nodes-and-systems)
-5. [Test and build web every day at 08:00](#test-and-build-web-every-day-at-0800)
-6. [Authenticate using an Expo token](#authenticate-using-an-expo-token)
+3. [Creating a new EAS build](#publish-on-any-push-to-master)
+4. [Test PRs and publish a review version](#test-prs-and-publish-a-review-version)
+5. [Test PRs on multiple nodes and systems](#test-prs-on-multiple-nodes-and-systems)
+6. [Test and build web every day at 08:00](#test-and-build-web-every-day-at-0800)
+7. [Authenticate using credentials](#authenticate-using-credentials)
### Publish on any push to master
Below you can see the example configuration to publish whenever the master branch is updated.
The workflow listens to the `push` event and sets up Node 12 using the [Setup Node Action][link-actions-node].
-It also authenticates the Expo project by defining both `expo-username` and `expo-password`.
+It also auto-authenticates when the `token` is provided.
```yml
name: Expo Publish
@@ -88,8 +90,7 @@ jobs:
- uses: expo/expo-github-action@v5
with:
expo-version: 4.x
- expo-username: ${{ secrets.EXPO_CLI_USERNAME }}
- expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}
+ token: ${{ secrets.EXPO_TOKEN }}
- run: yarn install
- run: expo publish
```
@@ -120,13 +121,43 @@ jobs:
- uses: expo/expo-github-action@v5
with:
expo-version: 4.x
- expo-username: ${{ secrets.EXPO_CLI_USERNAME }}
- expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}
expo-cache: true
+ token: ${{ secrets.EXPO_TOKEN }}
- run: yarn install
- run: expo publish
```
+### Creating a new EAS build
+
+You can also install [EAS](https://docs.expo.io/eas/) CLI with this Github Action.
+Below we've swapped `expo-version` with `eas-version`, but you can also use them together.
+Both the `token` and `username`/`password` is shared between both Expo and EAS CLI.
+
+> We recommend using `latest` for `eas-version` to always have the most up-to-date version.
+
+```yml
+name: EAS build
+on:
+ push:
+ branches:
+ - master
+jobs:
+ build:
+ name: Create new build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-node@v1
+ with:
+ node-version: 14.x
+ - uses: expo/expo-github-action@v5
+ with:
+ eas-version: latest
+ token: ${{ secrets.EXPO_TOKEN }}
+ - run: yarn install
+ - run: eas build
+```
+
### Test PRs and publish a review version
Reviewing pull requests can take some time if you have to read every line of code.
@@ -148,8 +179,7 @@ jobs:
- uses: expo/expo-github-action@v5
with:
expo-version: 4.x
- expo-username: ${{ secrets.EXPO_CLI_USERNAME }}
- expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}
+ token: ${{ secrets.EXPO_TOKEN }}
- run: yarn install
- run: expo publish --release-channel=pr-${{ github.event.number }}
- uses: unsplash/comment-on-pr@master
@@ -164,7 +194,7 @@ jobs:
With GitHub Actions, it's reasonably easy to set up a matrix build and test the app on multiple environments.
These matrixes can help to make sure your app runs smoothly on a broad set of different development machines.
-> If you don't need automatic authentication, you can omit the `expo-username` and `expo-password` variables.
+> If you don't need automatic authentication, you can omit the `token` variables.
```yml
name: Expo CI
@@ -217,10 +247,10 @@ jobs:
- run: expo build:web
```
-### Authenticate using an Expo token
+### Authenticate using credentials
-Instead of username and password, you can also authenticate using a token.
-This might help increasing security and avoids adding username and password to your repository secrets.
+Instead of using an access token, you can also authenticate using credentials.
+This is only possible when Expo CLI is installed.
```yml
name: Expo Publish
@@ -240,7 +270,8 @@ jobs:
- uses: expo/expo-github-action@v5
with:
expo-version: 4.x
- expo-token: ${{ secrets.EXPO_TOKEN }}
+ username: ${{ secrets.EXPO_CLI_USERNAME }}
+ password: ${{ secrets.EXPO_CLI_PASSWORD }}
- run: yarn install
- run: expo publish
```
@@ -263,7 +294,7 @@ Under the hood, it uses the [`@action/cache`][link-actions-cache-package] packag
This action generates a unique cache key for the OS, used packager, and exact version of the Expo CLI.
If you need more control over this cache, you can define a custom cache key with `expo-cache-key`.
-> Note, this cache will count towards your [repo cache limit][link-actions-cache-limit].
+> Note, this cache will count towards your [repo cache limit][link-actions-cache-limit]. The Expo and EAS CLI are stored in different caches.
### ENOSPC errors on Linux
@@ -272,7 +303,7 @@ Creating these bundles require quite some resources.
As of writing, GitHub actions has some small default values for the `fs.inotify` settings.
Inside this action, we included a patch that increases these limits for the current workflow.
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`.
+You can disable this patch by setting the `patch-watchers` to `false`.
@@ -288,4 +319,5 @@ You can disable this patch by setting the `expo-patch-watchers` to `false`.
[link-expo-cli]: https://docs.expo.io/workflow/expo-cli/
[link-expo-cli-password]: https://github.com/expo/expo-cli/blob/master/packages/expo-cli/src/accounts.ts#L88-L90
[link-expo-release-channels]: https://docs.expo.io/distribution/release-channels/
+[link-eas-cli]: https://github.com/expo/eas-cli#readme
[link-semver-playground]: https://semver.npmjs.com/
diff --git a/action.yml b/action.yml
index 58229e35..c12c442f 100644
--- a/action.yml
+++ b/action.yml
@@ -10,22 +10,28 @@ runs:
main: build/index.js
inputs:
expo-version:
- description: The Expo CLI version to install. (use any semver/dist-tag available)
- default: latest
- expo-username:
+ description: The Expo CLI version to install (use any semver/dist-tag available).
+ expo-cache:
+ description: If Expo CLI should be stored in the GitHub Actions cache.
+ default: false
+ expo-cache-key:
+ description: A custom remote cache key to use for Expo CLI.
+ eas-version:
+ description: The EAS CLI version to install (use any semver/dist-tag available).
+ eas-cache:
+ description: If EAS CLI should be stored in the GitHub Actions cache.
+ default: false
+ eas-cache-key:
+ description: A custom remote cache key to use for EAS CLI.
+ username:
description: Your Expo username, for authentication.
- expo-token:
+ token:
description: Your Expo token, for authentication. (use with secrets)
- expo-password:
+ password:
description: Your Expo password, for authentication. (use with secrets)
- expo-packager:
+ packager:
description: The package manager used to install the Expo CLI. (can be yarn or npm)
default: yarn
- expo-patch-watchers:
+ patch-watchers:
description: If Expo should fix the default watchers limit, helps with ENOSPC errors. (can be true or false)
default: true
- expo-cache:
- description: If Expo should be stored in the GitHub Actions cache (can be true or false)
- default: false
- expo-cache-key:
- description: A custom remote cache key to use (best to let GitHub Actions handle it)
diff --git a/build/index.js b/build/index.js
index d85639b0..9b71dbad 100644
--- a/build/index.js
+++ b/build/index.js
@@ -49560,18 +49560,18 @@ const cache_1 = __webpack_require__(5722);
* It returns the path where Expo is installed.
*/
async function install(config) {
- let root = await cache_1.fromLocalCache(config.version);
+ let root = await cache_1.fromLocalCache(config);
if (!root && config.cache) {
- root = await cache_1.fromRemoteCache(config.version, config.packager, config.cacheKey);
+ root = await cache_1.fromRemoteCache(config);
}
else {
core.info('Skipping remote cache, not enabled...');
}
if (!root) {
- root = await fromPackager(config.version, config.packager);
- root = await cache_1.toLocalCache(root, config.version);
+ root = await fromPackager(config);
+ root = await cache_1.toLocalCache(root, config);
if (config.cache) {
- await cache_1.toRemoteCache(root, config.version, config.packager, config.cacheKey);
+ await cache_1.toRemoteCache(root, config);
}
}
return path.join(root, 'node_modules', '.bin');
@@ -49581,11 +49581,11 @@ exports.install = install;
* Install `expo-cli`, by version, using npm or yarn.
* It creates a temporary directory to store all required files.
*/
-async function fromPackager(version, packager) {
+async function fromPackager(config) {
const root = process.env['RUNNER_TEMP'] || '';
- const tool = await io.which(packager);
+ const tool = await io.which(config.packager);
await io.mkdirP(root);
- await cli.exec(tool, ['add', `expo-cli@${version}`], { cwd: root });
+ await cli.exec(tool, ['add', `${config.package}@${config.version}`], { cwd: root });
return root;
}
exports.fromPackager = fromPackager;
@@ -63093,8 +63093,7 @@ if (typeof Object.create === 'function') {
Object.defineProperty(exports, "__esModule", { value: true });
const run_1 = __webpack_require__(4180);
-const tools_1 = __webpack_require__(3534);
-run_1.run().catch(tools_1.handleError);
+run_1.run();
/***/ }),
@@ -74612,19 +74611,35 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
-exports.handleError = exports.maybeWarnForUpdate = exports.maybePatchWatchers = exports.maybeAuthenticate = exports.resolveVersion = void 0;
+exports.handleError = exports.maybeWarnForUpdate = exports.maybePatchWatchers = exports.maybeAuthenticate = exports.resolveVersion = exports.getBinaryName = exports.getBoolean = void 0;
const core = __importStar(__webpack_require__(6470));
const cli = __importStar(__webpack_require__(4986));
const semver_1 = __importDefault(__webpack_require__(7876));
// eslint-disable-next-line @typescript-eslint/no-var-requires
const registry = __webpack_require__(5019);
+/**
+ * Get a boolean value from string, useful for Github Actions boolean inputs.
+ */
+function getBoolean(value, defaultValue = false) {
+ return (value.toLowerCase() || (defaultValue ? 'true' : 'false')) === 'true';
+}
+exports.getBoolean = getBoolean;
+/**
+ * Convert `expo-cli` or `eas-cli` to just their binary name.
+ * For windows we have to use `
.cmd`, toolkit will handle the Windows binary with that.
+ */
+function getBinaryName(name, forWindows = false) {
+ const bin = name.toLowerCase().replace('-cli', '');
+ return forWindows ? `${bin}.cmd` : bin;
+}
+exports.getBinaryName = getBinaryName;
/**
* 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`.
*/
-async function resolveVersion(version) {
- return (await registry.manifest(`expo-cli@${version}`)).version;
+async function resolveVersion(name, version) {
+ return (await registry.manifest(`${name}@${version}`)).version;
}
exports.resolveVersion = resolveVersion;
/**
@@ -74632,24 +74647,31 @@ exports.resolveVersion = resolveVersion;
* If both of them are set, token has priority.
*/
async function maybeAuthenticate(options = {}) {
- // github actions toolkit will handle commands with `.cmd` on windows, we need that
- const bin = process.platform === 'win32' ? 'expo.cmd' : 'expo';
if (options.token) {
- await cli.exec(bin, ['whoami'], {
- env: { ...process.env, EXPO_TOKEN: options.token },
- });
+ if (options.cli) {
+ const bin = getBinaryName(options.cli, process.platform === 'win32');
+ await cli.exec(bin, ['whoami'], {
+ env: { ...process.env, EXPO_TOKEN: options.token },
+ });
+ }
+ else {
+ core.info('Skipping token validation: no CLI installed, can\'t run `whoami`.');
+ }
return core.exportVariable('EXPO_TOKEN', options.token);
}
if (options.username || options.password) {
+ if (options.cli !== 'expo-cli') {
+ return core.warning('Skipping authentication: only Expo CLI supports programmatic credentials, use `token` instead.');
+ }
if (!options.username || !options.password) {
- return core.info('Skipping authentication: `expo-username` and/or `expo-password` not set...');
+ return core.info('Skipping authentication: `username` and/or `password` not set...');
}
+ const bin = getBinaryName(options.cli, process.platform === 'win32');
await cli.exec(bin, ['login', `--username=${options.username}`], {
env: { ...process.env, EXPO_CLI_PASSWORD: options.password },
});
- return;
}
- core.info('Skipping authentication: `expo-token`, `expo-username`, and/or `expo-password` not set...');
+ core.info('Skipping authentication: `token`, `username`, and/or `password` not set...');
}
exports.maybeAuthenticate = maybeAuthenticate;
/**
@@ -74682,12 +74704,13 @@ exports.maybePatchWatchers = maybePatchWatchers;
* If there is, create a warning for people to upgrade their workflow.
* Because this introduces additional requests, it should only be executed when necessary.
*/
-async function maybeWarnForUpdate() {
- const latest = await resolveVersion('latest');
- const current = await resolveVersion(core.getInput('expo-version') || 'latest');
+async function maybeWarnForUpdate(name) {
+ const binaryName = getBinaryName(name);
+ const latest = await resolveVersion(name, 'latest');
+ const current = await resolveVersion(name, core.getInput(`${getBinaryName(name)}-version`) || 'latest');
if (semver_1.default.diff(latest, current) === 'major') {
core.warning(`There is a new major version available of the Expo CLI (${latest})`);
- core.warning(`If you run into issues, try upgrading your workflow to "expo-version: ${semver_1.default.major(latest)}.x"`);
+ core.warning(`If you run into issues, try upgrading your workflow to "${binaryName}-version: ${semver_1.default.major(latest)}.x"`);
}
}
exports.maybeWarnForUpdate = maybeWarnForUpdate;
@@ -74695,9 +74718,9 @@ exports.maybeWarnForUpdate = maybeWarnForUpdate;
* Handle errors when this action fails, providing useful next-steps for developers.
* This mostly checks if the installed version is the latest version.
*/
-async function handleError(error) {
+async function handleError(name, error) {
try {
- await maybeWarnForUpdate();
+ await maybeWarnForUpdate(name);
}
catch {
// If this fails, ignore it
@@ -80923,35 +80946,68 @@ module.exports = minVersion
"use strict";
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+ o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || function (mod) {
+ if (mod && mod.__esModule) return mod;
+ var result = {};
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+ __setModuleDefault(result, mod);
+ return result;
+};
Object.defineProperty(exports, "__esModule", { value: true });
exports.run = void 0;
const core_1 = __webpack_require__(6470);
const install_1 = __webpack_require__(1655);
-const tools_1 = __webpack_require__(3534);
+const tools = __importStar(__webpack_require__(3534));
async function run() {
- const config = {
- version: core_1.getInput('expo-version') || 'latest',
- packager: core_1.getInput('expo-packager') || 'yarn',
- cache: (core_1.getInput('expo-cache') || 'false') === 'true',
- cacheKey: core_1.getInput('expo-cache-key') || undefined,
- };
- // Resolve the exact requested Expo CLI version
- config.version = await tools_1.resolveVersion(config.version);
- const path = await core_1.group(config.cache
- ? `Installing Expo CLI (${config.version}) from cache or with ${config.packager}`
- : `Installing Expo CLI (${config.version}) with ${config.packager}`, () => install_1.install(config));
- core_1.addPath(path);
- await core_1.group('Checking current authenticated account', () => tools_1.maybeAuthenticate({
- token: core_1.getInput('expo-token') || undefined,
- username: core_1.getInput('expo-username') || undefined,
- password: core_1.getInput('expo-password') || undefined,
+ const expoVersion = await installCli('expo-cli');
+ const easVersion = await installCli('eas-cli');
+ await core_1.group('Checking current authenticated account', () => tools.maybeAuthenticate({
+ cli: expoVersion ? 'expo-cli' : easVersion ? 'eas-cli' : undefined,
+ token: core_1.getInput('token') || undefined,
+ username: core_1.getInput('username') || undefined,
+ password: core_1.getInput('password') || undefined,
}));
- const shouldPatchWatchers = core_1.getInput('expo-patch-watchers') || 'true';
- if (shouldPatchWatchers !== 'false') {
- await core_1.group('Patching system watchers for the `ENOSPC` error', () => tools_1.maybePatchWatchers());
+ if (tools.getBoolean(core_1.getInput('patch-watchers'), true)) {
+ await core_1.group('Patching system watchers for the `ENOSPC` error', () => tools.maybePatchWatchers());
}
}
exports.run = run;
+async function installCli(name) {
+ const shortName = tools.getBinaryName(name);
+ const inputVersion = core_1.getInput(`${shortName}-version`);
+ const packager = core_1.getInput('packager') || 'yarn';
+ if (!inputVersion) {
+ return core_1.info(`Skipping installation of ${name}, \`${shortName}-version\` not provided.`);
+ }
+ const version = await tools.resolveVersion(name, inputVersion);
+ const cache = tools.getBoolean(core_1.getInput(`${shortName}-cache`), false);
+ try {
+ const path = await core_1.group(cache
+ ? `Installing ${name} (${version}) from cache or with ${packager}`
+ : `Installing ${name} (${version}) with ${packager}`, () => install_1.install({
+ packager, version, cache,
+ package: name,
+ cacheKey: core_1.getInput(`${shortName}-cache-key`) || undefined,
+ }));
+ core_1.addPath(path);
+ }
+ catch (error) {
+ tools.handleError(name, error);
+ }
+ return version;
+}
/***/ }),
@@ -101170,8 +101226,8 @@ const os_1 = __importDefault(__webpack_require__(2087));
*
* @see https://github.com/actions/toolkit/issues/47
*/
-async function fromLocalCache(version) {
- return toolCache.find('expo-cli', version);
+async function fromLocalCache(config) {
+ return toolCache.find(config.package, config.version);
}
exports.fromLocalCache = fromLocalCache;
/**
@@ -101180,18 +101236,18 @@ exports.fromLocalCache = fromLocalCache;
*
* @see https://github.com/actions/toolkit/issues/47
*/
-async function toLocalCache(root, version) {
- return toolCache.cacheDir(root, 'expo-cli', version);
+async function toLocalCache(root, config) {
+ return toolCache.cacheDir(root, config.package, config.version);
}
exports.toLocalCache = toLocalCache;
/**
* Download the remotely stored `expo-cli` from cache, if any.
* Note, this cache is shared between jobs.
*/
-async function fromRemoteCache(version, packager, customCacheKey) {
+async function fromRemoteCache(config) {
// see: https://github.com/actions/toolkit/blob/8a4134761f09d0d97fb15f297705fd8644fef920/packages/tool-cache/src/tool-cache.ts#L401
- const target = path_1.default.join(process.env['RUNNER_TOOL_CACHE'] || '', 'expo-cli', version, os_1.default.arch());
- const cacheKey = customCacheKey || getRemoteKey(version, packager);
+ const target = path_1.default.join(process.env['RUNNER_TOOL_CACHE'] || '', config.package, config.version, os_1.default.arch());
+ const cacheKey = config.cacheKey || getRemoteKey(config);
try {
// When running with nektos/act, or other custom environments, the cache might not be set up.
const hit = await cache_1.restoreCache([target], cacheKey);
@@ -101210,8 +101266,8 @@ exports.fromRemoteCache = fromRemoteCache;
* Store the root of `expo-cli` in the remote cache, for future reuse.
* Note, this cache is shared between jobs.
*/
-async function toRemoteCache(source, version, packager, customCacheKey) {
- const cacheKey = customCacheKey || getRemoteKey(version, packager);
+async function toRemoteCache(source, config) {
+ const cacheKey = config.cacheKey || getRemoteKey(config);
try {
await cache_1.saveCache([source], cacheKey);
}
@@ -101225,8 +101281,8 @@ exports.toRemoteCache = toRemoteCache;
/**
* Get the cache key to use when (re)storing the Expo CLI from remote cache.
*/
-function getRemoteKey(version, packager) {
- return `expo-cli-${process.platform}-${os_1.default.arch()}-${packager}-${version}`;
+function getRemoteKey(config) {
+ return `${config.package}-${process.platform}-${os_1.default.arch()}-${config.packager}-${config.version}`;
}
/**
* Handle any incoming errors from cache methods.
diff --git a/src/cache.ts b/src/cache.ts
index 25c67984..0b12d2ff 100644
--- a/src/cache.ts
+++ b/src/cache.ts
@@ -4,14 +4,18 @@ import * as toolCache from '@actions/tool-cache';
import path from 'path';
import os from 'os';
+import type { InstallConfig } from './install';
+
+export type CacheConfig = Omit;
+
/**
* 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
*/
-export async function fromLocalCache(version: string): Promise {
- return toolCache.find('expo-cli', version);
+export async function fromLocalCache(config: CacheConfig): Promise {
+ return toolCache.find(config.package, config.version);
}
/**
@@ -20,18 +24,18 @@ export async function fromLocalCache(version: string): Promise {
- return toolCache.cacheDir(root, 'expo-cli', version);
+export async function toLocalCache(root: string, config: CacheConfig): Promise {
+ return toolCache.cacheDir(root, config.package, config.version);
}
/**
* Download the remotely stored `expo-cli` from cache, if any.
* Note, this cache is shared between jobs.
*/
-export async function fromRemoteCache(version: string, packager: string, customCacheKey?: string): Promise {
+export async function fromRemoteCache(config: CacheConfig): Promise {
// see: https://github.com/actions/toolkit/blob/8a4134761f09d0d97fb15f297705fd8644fef920/packages/tool-cache/src/tool-cache.ts#L401
- const target = path.join(process.env['RUNNER_TOOL_CACHE'] || '', 'expo-cli', version, os.arch());
- const cacheKey = customCacheKey || getRemoteKey(version, packager);
+ const target = path.join(process.env['RUNNER_TOOL_CACHE'] || '', config.package, config.version, os.arch());
+ const cacheKey = config.cacheKey || getRemoteKey(config);
try {
// When running with nektos/act, or other custom environments, the cache might not be set up.
@@ -50,8 +54,8 @@ export async function fromRemoteCache(version: string, packager: string, customC
* Store the root of `expo-cli` in the remote cache, for future reuse.
* Note, this cache is shared between jobs.
*/
-export async function toRemoteCache(source: string, version: string, packager: string, customCacheKey?: string): Promise {
- const cacheKey = customCacheKey || getRemoteKey(version, packager);
+export async function toRemoteCache(source: string, config: CacheConfig): Promise {
+ const cacheKey = config.cacheKey || getRemoteKey(config);
try {
await saveCache([source], cacheKey);
@@ -65,8 +69,8 @@ export async function toRemoteCache(source: string, version: string, packager: s
/**
* Get the cache key to use when (re)storing the Expo CLI from remote cache.
*/
-function getRemoteKey(version: string, packager: string): string {
- return `expo-cli-${process.platform}-${os.arch()}-${packager}-${version}`;
+function getRemoteKey(config: Omit): string {
+ return `${config.package}-${process.platform}-${os.arch()}-${config.packager}-${config.version}`;
}
/**
diff --git a/src/index.ts b/src/index.ts
index c999498e..33c2e475 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,3 @@
import { run } from './run';
-import { handleError } from './tools';
-run().catch(handleError);
+run();
diff --git a/src/install.ts b/src/install.ts
index dda44b5a..f137efdf 100644
--- a/src/install.ts
+++ b/src/install.ts
@@ -4,11 +4,18 @@ import * as io from '@actions/io';
import * as path from 'path';
import { fromLocalCache, fromRemoteCache, toLocalCache, toRemoteCache } from './cache';
+import { PackageName } from './tools';
export type InstallConfig = {
+ /** The exact version to install */
version: string;
+ /** The name of the package to install */
+ package: PackageName;
+ /** The packager to install with, likely to be `yarn` or `npm` */
packager: string;
+ /** If remote caching is enabled or not */
cache?: boolean;
+ /** The custom remote cache key */
cacheKey?: string;
};
@@ -18,20 +25,20 @@ export type InstallConfig = {
* It returns the path where Expo is installed.
*/
export async function install(config: InstallConfig): Promise {
- let root: string | undefined = await fromLocalCache(config.version);
+ let root: string | undefined = await fromLocalCache(config);
if (!root && config.cache) {
- root = await fromRemoteCache(config.version, config.packager, config.cacheKey);
+ root = await fromRemoteCache(config);
} else {
core.info('Skipping remote cache, not enabled...');
}
if (!root) {
- root = await fromPackager(config.version, config.packager);
- root = await toLocalCache(root, config.version);
+ root = await fromPackager(config);
+ root = await toLocalCache(root, config);
if (config.cache) {
- await toRemoteCache(root, config.version, config.packager, config.cacheKey);
+ await toRemoteCache(root, config);
}
}
@@ -42,12 +49,12 @@ export async function install(config: InstallConfig): Promise {
* Install `expo-cli`, by version, using npm or yarn.
* It creates a temporary directory to store all required files.
*/
-export async function fromPackager(version: string, packager: string): Promise {
+export async function fromPackager(config: InstallConfig): Promise {
const root = process.env['RUNNER_TEMP'] || '';
- const tool = await io.which(packager);
+ const tool = await io.which(config.packager);
await io.mkdirP(root);
- await cli.exec(tool, ['add', `expo-cli@${version}`], { cwd: root });
+ await cli.exec(tool, ['add', `${config.package}@${config.version}`], { cwd: root });
return root;
}
diff --git a/src/run.ts b/src/run.ts
index 7c864b4f..59a08fcb 100644
--- a/src/run.ts
+++ b/src/run.ts
@@ -1,43 +1,58 @@
-import { addPath, getInput, group } from '@actions/core';
+import { addPath, getInput, group, info } from '@actions/core';
-import { install, InstallConfig } from './install';
-import { maybeAuthenticate, maybePatchWatchers, resolveVersion } from './tools';
+import { install } from './install';
+import * as tools from './tools';
export async function run(): Promise {
- const config: InstallConfig = {
- version: getInput('expo-version') || 'latest',
- packager: getInput('expo-packager') || 'yarn',
- cache: (getInput('expo-cache') || 'false') === 'true',
- cacheKey: getInput('expo-cache-key') || undefined,
- };
-
- // Resolve the exact requested Expo CLI version
- config.version = await resolveVersion(config.version);
-
- const path = await group(
- config.cache
- ? `Installing Expo CLI (${config.version}) from cache or with ${config.packager}`
- : `Installing Expo CLI (${config.version}) with ${config.packager}`,
- () => install(config),
- );
-
- addPath(path);
+ const expoVersion = await installCli('expo-cli');
+ const easVersion = await installCli('eas-cli');
await group(
'Checking current authenticated account',
- () => maybeAuthenticate({
- token: getInput('expo-token') || undefined,
- username: getInput('expo-username') || undefined,
- password: getInput('expo-password') || undefined,
+ () => tools.maybeAuthenticate({
+ cli: expoVersion ? 'expo-cli' : easVersion ? 'eas-cli' : undefined,
+ token: getInput('token') || undefined,
+ username: getInput('username') || undefined,
+ password: getInput('password') || undefined,
}),
);
- const shouldPatchWatchers = getInput('expo-patch-watchers') || 'true';
-
- if (shouldPatchWatchers !== 'false') {
+ if (tools.getBoolean(getInput('patch-watchers'), true)) {
await group(
'Patching system watchers for the `ENOSPC` error',
- () => maybePatchWatchers(),
+ () => tools.maybePatchWatchers(),
+ );
+ }
+}
+
+async function installCli(name: tools.PackageName): Promise {
+ const shortName = tools.getBinaryName(name);
+ const inputVersion = getInput(`${shortName}-version`);
+ const packager = getInput('packager') || 'yarn';
+
+ if (!inputVersion) {
+ return info(`Skipping installation of ${name}, \`${shortName}-version\` not provided.`);
+ }
+
+ const version = await tools.resolveVersion(name, inputVersion);
+ const cache = tools.getBoolean(getInput(`${shortName}-cache`), false);
+
+ try {
+ const path = await group(
+ cache
+ ? `Installing ${name} (${version}) from cache or with ${packager}`
+ : `Installing ${name} (${version}) with ${packager}`,
+ () => install({
+ packager, version, cache,
+ package: name,
+ cacheKey: getInput(`${shortName}-cache-key`) || undefined,
+ }),
);
+
+ addPath(path);
+ } catch (error) {
+ tools.handleError(name, error);
}
+
+ return version;
}
diff --git a/src/tools.ts b/src/tools.ts
index 2083468f..462b93a5 100644
--- a/src/tools.ts
+++ b/src/tools.ts
@@ -5,19 +5,38 @@ import semver from 'semver';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const registry = require('libnpm');
-type AuthenticateOptions = {
+export type PackageName = 'expo-cli' | 'eas-cli';
+
+export type AuthenticateOptions = {
+ cli?: PackageName;
token?: string;
username?: string;
password?: string;
};
+/**
+ * Get a boolean value from string, useful for Github Actions boolean inputs.
+ */
+export function getBoolean(value: string, defaultValue = false): boolean {
+ return (value.toLowerCase() || (defaultValue ? 'true' : 'false')) === 'true';
+}
+
+/**
+ * Convert `expo-cli` or `eas-cli` to just their binary name.
+ * For windows we have to use `.cmd`, toolkit will handle the Windows binary with that.
+ */
+export function getBinaryName(name: PackageName, forWindows = false): string {
+ const bin = name.toLowerCase().replace('-cli', '');
+ return forWindows ? `${bin}.cmd` : bin;
+}
+
/**
* 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`.
*/
-export async function resolveVersion(version: string): Promise {
- return (await registry.manifest(`expo-cli@${version}`)).version;
+export async function resolveVersion(name: PackageName, version: string): Promise {
+ return (await registry.manifest(`${name}@${version}`)).version;
}
/**
@@ -25,27 +44,35 @@ export async function resolveVersion(version: string): Promise {
* If both of them are set, token has priority.
*/
export async function maybeAuthenticate(options: AuthenticateOptions = {}): Promise {
- // github actions toolkit will handle commands with `.cmd` on windows, we need that
- const bin = process.platform === 'win32' ? 'expo.cmd' : 'expo';
-
if (options.token) {
- await cli.exec(bin, ['whoami'], {
- env: { ...process.env, EXPO_TOKEN: options.token },
- });
+ if (options.cli) {
+ const bin = getBinaryName(options.cli, process.platform === 'win32');
+ await cli.exec(bin, ['whoami'], {
+ env: { ...process.env, EXPO_TOKEN: options.token },
+ });
+ } else {
+ core.info('Skipping token validation: no CLI installed, can\'t run `whoami`.');
+ }
+
return core.exportVariable('EXPO_TOKEN', options.token);
}
if (options.username || options.password) {
+ if (options.cli !== 'expo-cli') {
+ return core.warning('Skipping authentication: only Expo CLI supports programmatic credentials, use `token` instead.');
+ }
+
if (!options.username || !options.password) {
- return core.info('Skipping authentication: `expo-username` and/or `expo-password` not set...');
+ return core.info('Skipping authentication: `username` and/or `password` not set...');
}
+
+ const bin = getBinaryName(options.cli, process.platform === 'win32');
await cli.exec(bin, ['login', `--username=${options.username}`], {
env: { ...process.env, EXPO_CLI_PASSWORD: options.password },
});
- return;
}
- core.info('Skipping authentication: `expo-token`, `expo-username`, and/or `expo-password` not set...');
+ core.info('Skipping authentication: `token`, `username`, and/or `password` not set...');
}
/**
@@ -79,13 +106,14 @@ export async function maybePatchWatchers(): Promise {
* If there is, create a warning for people to upgrade their workflow.
* Because this introduces additional requests, it should only be executed when necessary.
*/
-export async function maybeWarnForUpdate(): Promise {
- const latest = await resolveVersion('latest');
- const current = await resolveVersion(core.getInput('expo-version') || 'latest');
+export async function maybeWarnForUpdate(name: PackageName): Promise {
+ const binaryName = getBinaryName(name);
+ const latest = await resolveVersion(name, 'latest');
+ const current = await resolveVersion(name, core.getInput(`${getBinaryName(name)}-version`) || 'latest');
if (semver.diff(latest, current) === 'major') {
core.warning(`There is a new major version available of the Expo CLI (${latest})`);
- core.warning(`If you run into issues, try upgrading your workflow to "expo-version: ${semver.major(latest)}.x"`);
+ core.warning(`If you run into issues, try upgrading your workflow to "${binaryName}-version: ${semver.major(latest)}.x"`);
}
}
@@ -93,9 +121,9 @@ export async function maybeWarnForUpdate(): Promise {
* Handle errors when this action fails, providing useful next-steps for developers.
* This mostly checks if the installed version is the latest version.
*/
-export async function handleError(error: Error) {
+export async function handleError(name: PackageName, error: Error) {
try {
- await maybeWarnForUpdate();
+ await maybeWarnForUpdate(name);
} catch {
// If this fails, ignore it
}
diff --git a/tests/cache.test.ts b/tests/cache.test.ts
index e2abb20b..887b8fbe 100644
--- a/tests/cache.test.ts
+++ b/tests/cache.test.ts
@@ -7,29 +7,29 @@ import * as toolCache from '@actions/tool-cache';
import * as cache from '../src/cache';
import * as utils from './utils';
-describe('fromLocalCache', () => {
+describe(cache.fromLocalCache, () => {
it('fetches the version specific cache', async () => {
const path = join('path', 'to', 'local', 'cache');
// todo: check why jest wants `never` instead of `string`
const find = jest.spyOn(toolCache, 'find').mockResolvedValue(path as never);
- const result = await cache.fromLocalCache('3.20.1');
+ const result = await cache.fromLocalCache({ package: 'eas-cli', version: '4.2.0', packager: 'npm' });
expect(result).toBe(path);
- expect(find).toBeCalledWith('expo-cli', '3.20.1');
+ expect(find).toBeCalledWith('eas-cli', '4.2.0');
});
});
-describe('toLocalCache', () => {
+describe(cache.toLocalCache, () => {
it('stores the version specific cache', async () => {
const path = join('path', 'to', 'local', 'cache');
const root = join('path', 'from', 'source');
const cacheDir = jest.spyOn(toolCache, 'cacheDir').mockResolvedValue(path);
- const result = await cache.toLocalCache(root, '3.20.1');
+ const result = await cache.toLocalCache(root, { package: 'expo-cli', version: '4.2.0', packager: 'yarn' });
expect(result).toBe(path);
- expect(cacheDir).toBeCalledWith(root, 'expo-cli', '3.20.1');
+ expect(cacheDir).toBeCalledWith(root, 'expo-cli', '4.2.0');
});
});
-describe('fromRemoteCache', () => {
+describe(cache.fromRemoteCache, () => {
let spy: { [key: string]: jest.SpyInstance } = {};
beforeEach(() => {
@@ -50,7 +50,7 @@ describe('fromRemoteCache', () => {
});
it('restores remote cache with default key', async () => {
- expect(await cache.fromRemoteCache('3.20.1', 'yarn')).toBeUndefined();
+ expect(await cache.fromRemoteCache({ package: 'expo-cli', version: '3.20.1', packager: 'yarn' })).toBeUndefined();
expect(remoteCache.restoreCache).toBeCalledWith(
[join('cache', 'path', 'expo-cli', '3.20.1', os.arch())],
`expo-cli-${process.platform}-${os.arch()}-yarn-3.20.1`,
@@ -58,36 +58,45 @@ describe('fromRemoteCache', () => {
});
it('restores remote cache with custom key', async () => {
- expect(await cache.fromRemoteCache('3.20.0', 'yarn', 'custom-cache-key')).toBeUndefined();
+ expect(
+ await cache.fromRemoteCache({
+ package: 'eas-cli',
+ version: '4.2.0',
+ packager: 'yarn',
+ cacheKey: 'custom-cache-key',
+ })
+ ).toBeUndefined();
expect(remoteCache.restoreCache).toBeCalledWith(
- [join('cache', 'path', 'expo-cli', '3.20.0', os.arch())],
+ [join('cache', 'path', 'eas-cli', '4.2.0', os.arch())],
'custom-cache-key',
);
});
it('returns path when remote cache exists', async () => {
spy.restore.mockResolvedValueOnce(true);
- await expect(cache.fromRemoteCache('3.20.1', 'npm')).resolves.toBe(
- join('cache', 'path', 'expo-cli', '3.20.1', os.arch())
- );
+ expect(
+ await cache.fromRemoteCache({ package: 'expo-cli', version: '3.20.1', packager: 'npm' })
+ ).toBe(join('cache', 'path', 'expo-cli', '3.20.1', os.arch()));
});
it('fails when remote cache throws', async () => {
const error = new Error('Remote cache restore failed');
spy.restore.mockRejectedValueOnce(error);
- await expect(cache.fromRemoteCache('3.20.1', 'yarn')).rejects.toBe(error);
+ await expect(cache.fromRemoteCache({ package: 'eas-cli', version: '3.20.1', packager: 'yarn' })).rejects.toBe(error);
});
it('skips remote cache when unavailable', async () => {
// see: https://github.com/actions/toolkit/blob/9167ce1f3a32ad495fc1dbcb574c03c0e013ae53/packages/cache/src/internal/cacheHttpClient.ts#L41
const error = new Error('Cache Service Url not found, unable to restore cache.');
spy.restore.mockRejectedValueOnce(error);
- await expect(cache.fromRemoteCache('3.20.1', 'yarn')).resolves.toBeUndefined();
+ expect(
+ await cache.fromRemoteCache({ package: 'expo-cli', version: '3.20.1', packager: 'yarn' })
+ ).toBeUndefined();
expect(spy.warning).toHaveBeenCalledWith(expect.stringContaining('Skipping remote cache'));
});
});
-describe('toRemoteCache', () => {
+describe(cache.toRemoteCache, () => {
let spy: { [key: string]: jest.SpyInstance } = {};
beforeEach(() => {
@@ -103,29 +112,38 @@ describe('toRemoteCache', () => {
});
it('saves remote cache with default key', async () => {
- expect(await cache.toRemoteCache(join('local', 'path'), '3.20.1', 'npm')).toBeUndefined();
+ expect(await cache.toRemoteCache(join('local', 'path'), { package: 'eas-cli', version: '3.20.1', packager: 'npm' })).toBeUndefined();
expect(remoteCache.saveCache).toBeCalledWith(
[join('local', 'path')],
- `expo-cli-${process.platform}-${os.arch()}-npm-3.20.1`
+ `eas-cli-${process.platform}-${os.arch()}-npm-3.20.1`
);
});
it('saves remote cache with custom key', async () => {
- expect(await cache.toRemoteCache(join('local', 'path'), '3.20.1', 'yarn', 'custom-cache-key')).toBeUndefined();
+ expect(await cache.toRemoteCache(
+ join('local', 'path'),
+ { package: 'eas-cli', version: '3.20.1', packager: 'yarn', cacheKey: 'custom-cache-key' }
+ )).toBeUndefined();
expect(remoteCache.saveCache).toBeCalledWith([join('local', 'path')], 'custom-cache-key');
});
it('fails when remote cache throws', async () => {
const error = new Error('Remote cache save failed');
spy.save.mockRejectedValueOnce(error);
- await expect(cache.toRemoteCache(join('local', 'path'), '3.20.1', 'yarn')).rejects.toBe(error);
+ await expect(cache.toRemoteCache(
+ join('local', 'path'),
+ { package: 'expo-cli', version: '3.20.1', packager: 'yarn' },
+ )).rejects.toBe(error);
});
it('skips remote cache when unavailable', async () => {
// see: https://github.com/actions/toolkit/blob/9167ce1f3a32ad495fc1dbcb574c03c0e013ae53/packages/cache/src/internal/cacheHttpClient.ts#L41
const error = new Error('Cache Service Url not found, unable to restore cache.');
spy.save.mockRejectedValueOnce(error);
- await expect(cache.toRemoteCache(join('local', 'path'), '3.20.1', 'yarn')).resolves.toBeUndefined();
+ await expect(cache.toRemoteCache(
+ join('local', 'path'),
+ { package: 'expo-cli', version: '3.20.1', packager: 'yarn' },
+ )).resolves.toBeUndefined();
expect(spy.warning).toHaveBeenCalledWith(expect.stringContaining('Skipping remote cache'));
});
});
diff --git a/tests/install.test.ts b/tests/install.test.ts
index 2d5b65c7..a717dcec 100644
--- a/tests/install.test.ts
+++ b/tests/install.test.ts
@@ -19,7 +19,7 @@ import * as utils from './utils';
describe('install', () => {
it('installs path from local cache', async () => {
cache.fromLocalCache.mockResolvedValue(join('cache', 'path'));
- const expoPath = await install.install({ version: '3.0.10', packager: 'npm' });
+ const expoPath = await install.install({ package: 'expo-cli', version: '3.0.10', packager: 'npm' });
expect(expoPath).toBe(join('cache', 'path', 'node_modules', '.bin'));
});
@@ -27,18 +27,18 @@ describe('install', () => {
utils.setEnv('RUNNER_TEMP', join('temp', 'path'));
cache.fromLocalCache.mockResolvedValue(undefined);
cache.toLocalCache.mockResolvedValue(join('cache', 'path'));
- const expoPath = await install.install({ version: '3.0.10', packager: 'npm' });
+ const expoPath = await install.install({ package: 'expo-cli', version: '3.0.10', packager: 'npm' });
expect(expoPath).toBe(join('cache', 'path', 'node_modules', '.bin'));
- expect(cache.toLocalCache).toBeCalledWith(join('temp', 'path'), '3.0.10');
+ expect(cache.toLocalCache).toBeCalledWith(join('temp', 'path'), { package: 'expo-cli', version: '3.0.10', packager: 'npm' });
utils.restoreEnv();
});
it('installs path from remote cache', async () => {
cache.fromLocalCache.mockResolvedValue(undefined);
cache.fromRemoteCache.mockResolvedValue(join('cache', 'path'));
- const expoPath = await install.install({ version: '3.20.1', packager: 'npm', cache: true });
+ const expoPath = await install.install({ package: 'eas-cli', version: '4.2.0', packager: 'yarn', cache: true });
expect(expoPath).toBe(join('cache', 'path', 'node_modules', '.bin'));
- expect(cache.fromRemoteCache).toBeCalledWith('3.20.1', 'npm', undefined);
+ expect(cache.fromRemoteCache).toBeCalledWith({ package: 'eas-cli', version: '4.2.0', packager: 'yarn', cache: true });
});
it('installs path from packager and cache it remotely', async () => {
@@ -46,22 +46,22 @@ describe('install', () => {
cache.fromLocalCache.mockResolvedValue(undefined);
cache.fromRemoteCache.mockResolvedValue(undefined);
cache.toLocalCache.mockResolvedValue(join('cache', 'path'));
- const expoPath = await install.install({ version: '3.20.1', packager: 'npm', cache: true });
+ const expoPath = await install.install({ package: 'expo-cli', version: '3.20.1', packager: 'npm', cache: true });
expect(expoPath).toBe(join('cache', 'path', 'node_modules', '.bin'));
- expect(cache.toRemoteCache).toBeCalledWith(join('cache', 'path'), '3.20.1', 'npm', undefined);
+ expect(cache.toRemoteCache).toBeCalledWith(join('cache', 'path'), { package: 'expo-cli', version: '3.20.1', packager: 'npm', cache: true });
utils.restoreEnv();
});
});
describe('fromPackager', () => {
it('resolves tool path', async () => {
- await install.fromPackager('3.0.10', 'npm');
+ await install.fromPackager({ package: 'expo-cli', version: '3.0.10', packager: 'npm' });
expect(io.which).toBeCalledWith('npm');
});
it('creates temporary folder', async () => {
utils.setEnv('RUNNER_TEMP', join('temp', 'path'));
- await install.fromPackager('latest', 'yarn');
+ await install.fromPackager({ package: 'eas-cli', version: 'latest', packager: 'yarn' });
expect(io.mkdirP).toBeCalledWith(join('temp', 'path'));
utils.restoreEnv();
});
@@ -69,11 +69,11 @@ describe('fromPackager', () => {
it('installs expo with tool', async () => {
utils.setEnv('RUNNER_TEMP', join('temp', 'path'));
io.which.mockResolvedValue('npm');
- const expoPath = await install.fromPackager('beta', 'npm');
+ const expoPath = await install.fromPackager({ package: 'eas-cli', version: 'beta', packager: 'npm' });
expect(expoPath).toBe(join('temp', 'path'));
expect(cli.exec).toBeCalled();
expect(cli.exec.mock.calls[0][0]).toBe('npm');
- expect(cli.exec.mock.calls[0][1]).toStrictEqual(['add', 'expo-cli@beta']);
+ expect(cli.exec.mock.calls[0][1]).toStrictEqual(['add', 'eas-cli@beta']);
expect(cli.exec.mock.calls[0][2]).toMatchObject({ cwd: join('temp', 'path') });
utils.restoreEnv();
});
diff --git a/tests/run.test.ts b/tests/run.test.ts
index c666bdb0..414706ef 100644
--- a/tests/run.test.ts
+++ b/tests/run.test.ts
@@ -1,12 +1,15 @@
const core = {
addPath: jest.fn(),
getInput: jest.fn(),
- group: (message: string, action: () => Promise) => action()
+ group: (message: string, action: () => Promise) => action(),
+ info: jest.fn(),
};
const exec = { exec: jest.fn() };
const install = { install: jest.fn() };
const tools = {
- resolveVersion: jest.fn(v => v),
+ getBoolean: jest.fn((v, d) => v ? v === 'true' : d),
+ getBinaryName: jest.fn(v => v.replace('-cli', '')),
+ resolveVersion: jest.fn((n, v) => v),
maybeAuthenticate: jest.fn(),
maybePatchWatchers: jest.fn(),
};
@@ -18,64 +21,7 @@ jest.mock('../src/install', () => install);
import { run } from '../src/run';
-interface MockInputProps {
- version?: string;
- packager?: string;
- token?: string;
- username?: string;
- password?: string;
- patchWatchers?: string;
- cache?: string;
- cacheKey?: 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-token':
- return props.token || '';
- case 'expo-username':
- return props.username || '';
- case 'expo-password':
- return props.password || '';
- case 'expo-patch-watchers':
- return props.patchWatchers || '';
- case 'expo-cache':
- return props.cache || '';
- case 'expo-cache-key':
- return props.cacheKey || '';
- default:
- return '';
- }
- };
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- core.getInput = input as any;
-};
-
describe('run', () => {
- it('installs latest expo-cli with yarn by default', async () => {
- await run();
- expect(install.install).toBeCalledWith({ version: 'latest', packager: 'yarn', cache: false });
- });
-
- it('installs provided version expo-cli with npm', async () => {
- mockInput({ version: '3.0.10', packager: 'npm' });
- await run();
- expect(install.install).toBeCalledWith({ version: '3.0.10', packager: 'npm', cache: false });
- });
-
- it('installs path to global path', async () => {
- install.install.mockResolvedValue('/expo/install/path');
- await run();
- expect(core.addPath).toBeCalledWith('/expo/install/path');
- });
-
it('patches the system when set to true', async () => {
mockInput({ patchWatchers: 'true' });
await run();
@@ -105,4 +51,126 @@ describe('run', () => {
await run();
expect(tools.maybeAuthenticate).toBeCalledWith({ token: 'ABC123' });
});
+
+ ['expo', 'eas'].forEach(cliName => {
+ const packageName = `${cliName}-cli`;
+
+ describe(packageName, () => {
+ it(`skips installation without \`${cliName}-version\``, async () => {
+ mockInput();
+ await run();
+ expect(install.install).not.toBeCalledWith({ package: packageName });
+ });
+
+ it('installs with yarn by default', async () => {
+ mockInput({ [`${cliName}Version`]: 'latest' });
+ await run();
+ expect(install.install).toBeCalledWith({
+ package:
+ packageName,
+ version: 'latest',
+ packager: 'yarn',
+ cache: false,
+ });
+ });
+
+ it('installs provided version with npm', async () => {
+ mockInput({ [`${cliName}Version`]: '3.0.10', packager: 'npm' });
+ await run();
+ expect(install.install).toBeCalledWith({
+ package: packageName,
+ version: '3.0.10',
+ packager: 'npm',
+ cache: false,
+ });
+ });
+
+ it('installs with yarn and cache enabled', async () => {
+ mockInput({
+ packager: 'yarn',
+ [`${cliName}Version`]: '4.2.0',
+ [`${cliName}Cache`]: 'true',
+ });
+ await run();
+ expect(install.install).toBeCalledWith({
+ package: packageName,
+ version: '4.2.0',
+ packager: 'yarn',
+ cache: true,
+ });
+ });
+
+ it('installs with yarn and custom cache key', async () => {
+ mockInput({
+ packager: 'yarn',
+ [`${cliName}Version`]: '4.2.0',
+ [`${cliName}Cache`]: 'true',
+ [`${cliName}CacheKey`]: 'custom-key'
+ });
+ await run();
+ expect(install.install).toBeCalledWith({
+ package: packageName,
+ version: '4.2.0',
+ packager: 'yarn',
+ cache: true,
+ cacheKey: 'custom-key',
+ });
+ });
+
+ it('installs path to global path', async () => {
+ install.install.mockResolvedValue(`/${cliName}/install/path`);
+ await run();
+ expect(core.addPath).toBeCalledWith(`/${cliName}/install/path`);
+ });
+ });
+ });
});
+
+interface MockInputProps {
+ expoVersion?: string;
+ expoCache?: string;
+ expoCacheKey?: string;
+ easVersion?: string;
+ easCache?: string;
+ easCacheKey?: string;
+ packager?: string;
+ token?: string;
+ username?: string;
+ password?: string;
+ patchWatchers?: string;
+}
+
+function 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.expoVersion || '';
+ case 'expo-cache':
+ return props.expoCache || '';
+ case 'expo-cache-key':
+ return props.expoCacheKey || '';
+ case 'eas-version':
+ return props.easVersion || '';
+ case 'eas-cache':
+ return props.easCache || '';
+ case 'eas-cache-key':
+ return props.easCacheKey || '';
+ case 'packager':
+ return props.packager || '';
+ case 'token':
+ return props.token || '';
+ case 'username':
+ return props.username || '';
+ case 'password':
+ return props.password || '';
+ case 'patch-watchers':
+ return props.patchWatchers || '';
+ default:
+ return '';
+ }
+ };
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ core.getInput = input as any;
+}
diff --git a/tests/tools.test.ts b/tests/tools.test.ts
index 9b932ac7..94ccbb10 100644
--- a/tests/tools.test.ts
+++ b/tests/tools.test.ts
@@ -7,15 +7,57 @@ import * as cli from '@actions/exec';
import * as tools from '../src/tools';
import * as utils from './utils';
-describe('resolveVersion', () => {
+describe(tools.getBoolean, () => {
+ it('rreturns false for empty strings by default', () => {
+ expect(tools.getBoolean('')).toBeFalsy();
+ });
+
+ it('returns true for empty strings with default set to true', () => {
+ expect(tools.getBoolean('', true)).toBeTruthy();
+ });
+
+ it('returns true for `true` strings by default', () => {
+ expect(tools.getBoolean('true')).toBeTruthy();
+ });
+
+ it('returns false for `false` strings with default set to false', () => {
+ expect(tools.getBoolean('false', false)).toBeFalsy();
+ });
+
+ it('returns false for `false` strings with default set to true', () => {
+ expect(tools.getBoolean('false', true)).toBeFalsy();
+ });
+});
+
+describe(tools.getBinaryName, () => {
+ it('returns expo for `expo-cli`', () => {
+ expect(tools.getBinaryName('expo-cli')).toBe('expo');
+ });
+
+ it('returns eas for `eas-cli`', () => {
+ expect(tools.getBinaryName('eas-cli')).toBe('eas');
+ });
+
+ it('returns eas.cmd for `eas-cli` for windows', () => {
+ expect(tools.getBinaryName('eas-cli', true)).toBe('eas.cmd');
+ });
+});
+
+describe(tools.resolveVersion, () => {
it('fetches exact version of expo-cli', async () => {
registry.manifest.mockResolvedValue({ version: '3.0.10' });
- expect(await tools.resolveVersion('latest')).toBe('3.0.10');
+ expect(await tools.resolveVersion('expo-cli', 'latest')).toBe('3.0.10');
expect(registry.manifest).toBeCalledWith('expo-cli@latest');
});
+
+ it('fetches exact version of eas-cli', async () => {
+ registry.manifest.mockResolvedValue({ version: '4.2.0' });
+ expect(await tools.resolveVersion('eas-cli', 'latest')).toBe('4.2.0');
+ expect(registry.manifest).toBeCalledWith('eas-cli@latest');
+ });
});
-describe('maybeAuthenticate', () => {
+describe(tools.maybeAuthenticate, () => {
const token = 'ABC123';
const username = 'bycedric';
const password = 'mypassword';
@@ -45,7 +87,7 @@ describe('maybeAuthenticate', () => {
it('executes whoami command with token through environment', async () => {
utils.setEnv('TEST_INCLUDED', 'hellyeah');
- await tools.maybeAuthenticate({ token });
+ await tools.maybeAuthenticate({ token, cli: 'expo-cli' });
expect(spy.exec).toBeCalled();
expect(spy.exec.mock.calls[0][1]).toStrictEqual(['whoami']);
expect(spy.exec.mock.calls[0][2]).toMatchObject({
@@ -60,28 +102,34 @@ describe('maybeAuthenticate', () => {
it('fails when token is incorrect', async () => {
const error = new Error('Not logged in');
spy.exec.mockRejectedValue(error);
- await expect(tools.maybeAuthenticate({ token })).rejects.toBe(error);
+ await expect(tools.maybeAuthenticate({ token, cli: 'expo-cli' })).rejects.toBe(error);
+ });
+
+ it('skips validation without cli', async () => {
+ await tools.maybeAuthenticate({ token });
+ expect(spy.exec).not.toBeCalled();
+ expect(spy.info).toBeCalledWith(expect.stringContaining('Skipping token validation'));
});
it('executes whoami command with `expo` on macos', async () => {
utils.setPlatform('darwin');
- await tools.maybeAuthenticate({ token });
+ await tools.maybeAuthenticate({ token, cli: 'expo-cli' });
expect(spy.exec).toBeCalled();
expect(spy.exec.mock.calls[0][0]).toBe('expo');
utils.restorePlatform();
});
- it('executes whoami command with `expo` on ubuntu', async () => {
+ it('executes whoami command with `eas` on ubuntu', async () => {
utils.setPlatform('linux');
- await tools.maybeAuthenticate({ token });
+ await tools.maybeAuthenticate({ token, cli: 'eas-cli' });
expect(spy.exec).toBeCalled();
- expect(spy.exec.mock.calls[0][0]).toBe('expo');
+ expect(spy.exec.mock.calls[0][0]).toBe('eas');
utils.restorePlatform();
});
it('executes whoami command with `expo.cmd` on windows', async () => {
utils.setPlatform('win32');
- await tools.maybeAuthenticate({ token });
+ await tools.maybeAuthenticate({ token, cli: 'expo-cli' });
expect(spy.exec).toBeCalled();
expect(spy.exec.mock.calls[0][0]).toBe('expo.cmd');
utils.restorePlatform();
@@ -95,29 +143,43 @@ describe('maybeAuthenticate', () => {
spy = {
exec: jest.spyOn(cli, 'exec').mockImplementation(),
info: jest.spyOn(core, 'info').mockImplementation(),
+ warning: jest.spyOn(core, 'warning').mockImplementation(),
};
});
afterAll(() => {
spy.exec.mockRestore();
spy.info.mockRestore();
+ spy.warning.mockRestore();
+ });
+
+ it('skips authentication without cli', async () => {
+ await tools.maybeAuthenticate({});
+ expect(spy.exec).not.toBeCalled();
+ expect(spy.info).toBeCalledWith(expect.stringContaining('Skipping authentication'));
});
it('skips authentication without credentials', async () => {
- await tools.maybeAuthenticate();
+ await tools.maybeAuthenticate({ cli: 'expo-cli' });
expect(spy.exec).not.toBeCalled();
expect(spy.info).toBeCalledWith(expect.stringContaining('Skipping authentication'));
});
it('skips authentication without password', async () => {
- await tools.maybeAuthenticate({ username });
+ await tools.maybeAuthenticate({ username, cli: 'expo-cli' });
expect(spy.exec).not.toBeCalled();
expect(spy.info).toBeCalledWith(expect.stringContaining('Skipping authentication'));
});
+ it('skips authentication with credentials for eas-cli', async () => {
+ await tools.maybeAuthenticate({ username, password, cli: 'eas-cli' });
+ expect(spy.exec).not.toBeCalled();
+ expect(spy.warning).toBeCalledWith(expect.stringContaining('Skipping authentication'));
+ });
+
it('executes login command with password through environment', async () => {
utils.setEnv('TEST_INCLUDED', 'hellyeah');
- await tools.maybeAuthenticate({ username, password })
+ await tools.maybeAuthenticate({ username, password, cli: 'expo-cli' })
expect(spy.exec).toBeCalled();
expect(spy.exec.mock.calls[0][1]).toStrictEqual(['login', `--username=${username}`]);
expect(spy.exec.mock.calls[0][2]).toMatchObject({
@@ -132,14 +194,14 @@ describe('maybeAuthenticate', () => {
it('fails when credentials are incorrect', async () => {
const error = new Error('Invalid username/password. Please try again.');
spy.exec.mockRejectedValue(error);
- await expect(tools.maybeAuthenticate({ username, password }))
+ await expect(tools.maybeAuthenticate({ username, password, cli: 'expo-cli' }))
.rejects
.toBe(error);
});
it('executes login command with `expo` on macos', async () => {
utils.setPlatform('darwin');
- await tools.maybeAuthenticate({ username, password });
+ await tools.maybeAuthenticate({ username, password, cli: 'expo-cli' });
expect(spy.exec).toBeCalled();
expect(spy.exec.mock.calls[0][0]).toBe('expo');
utils.restorePlatform();
@@ -147,7 +209,7 @@ describe('maybeAuthenticate', () => {
it('executes login command with `expo` on ubuntu', async () => {
utils.setPlatform('linux');
- await tools.maybeAuthenticate({ username, password });
+ await tools.maybeAuthenticate({ username, password, cli: 'expo-cli' });
expect(spy.exec).toBeCalled();
expect(spy.exec.mock.calls[0][0]).toBe('expo');
utils.restorePlatform();
@@ -155,7 +217,7 @@ describe('maybeAuthenticate', () => {
it('executes login command with `expo.cmd` on windows', async () => {
utils.setPlatform('win32');
- await tools.maybeAuthenticate({ username, password });
+ await tools.maybeAuthenticate({ username, password, cli: 'expo-cli' });
expect(spy.exec).toBeCalled();
expect(spy.exec.mock.calls[0][0]).toBe('expo.cmd');
utils.restorePlatform();
@@ -163,7 +225,7 @@ describe('maybeAuthenticate', () => {
});
});
-describe('maybePatchWatchers', () => {
+describe(tools.maybePatchWatchers, () => {
let spy: { [key: string]: jest.SpyInstance } = {};
beforeEach(() => {
@@ -226,7 +288,7 @@ describe('maybePatchWatchers', () => {
});
});
-describe('maybeWarnForUpdate', () => {
+describe(tools.maybeWarnForUpdate, () => {
let spy: { [key: string]: jest.SpyInstance } = {};
beforeEach(() => {
@@ -241,7 +303,7 @@ describe('maybeWarnForUpdate', () => {
registry.manifest
.mockResolvedValueOnce({ version: '4.1.0' })
.mockResolvedValueOnce({ version: '4.0.1'});
- await tools.maybeWarnForUpdate();
+ await tools.maybeWarnForUpdate('eas-cli');
expect(spy.warning).not.toBeCalled();
})
@@ -249,13 +311,13 @@ describe('maybeWarnForUpdate', () => {
registry.manifest
.mockResolvedValueOnce({ version: '4.1.0' })
.mockResolvedValueOnce({ version: '3.0.1'});
- await tools.maybeWarnForUpdate();
+ await tools.maybeWarnForUpdate('expo-cli');
expect(spy.warning).toBeCalledWith('There is a new major version available of the Expo CLI (4.1.0)');
expect(spy.warning).toBeCalledWith('If you run into issues, try upgrading your workflow to "expo-version: 4.x"');
})
});
-describe('handleError', () => {
+describe(tools.handleError, () => {
let spy: { [key: string]: jest.SpyInstance } = {};
beforeEach(() => {
@@ -266,17 +328,17 @@ describe('handleError', () => {
spy.setFailed.mockRestore();
});
- it('marks the job as failed', async () => {
+ it('marks the job as failed with expo-cli', async () => {
const error = new Error('test');
registry.manifest.mockResolvedValue('4.0.0');
- await tools.handleError(error);
+ await tools.handleError('expo-cli', error);
expect(core.setFailed).toBeCalledWith(error);
});
it('fails with original error when update warning failed', async () => {
const error = new Error('test');
registry.manifest.mockRejectedValue(new Error('npm issue'));
- await tools.handleError(error);
+ await tools.handleError('eas-cli', error);
expect(core.setFailed).toBeCalledWith(error);
});
});