Skip to content

Commit

Permalink
feat: add expo token authentication method (#57)
Browse files Browse the repository at this point in the history
  • Loading branch information
byCedric authored Aug 28, 2020
1 parent ce3c32e commit 1c36889
Show file tree
Hide file tree
Showing 12 changed files with 360 additions and 73 deletions.
8 changes: 8 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"printWidth": 120,
"tabWidth": 2,
"singleQuote": true,
"jsxBracketSameLine": true,
"trailingComma": "es5",
"arrowParens": "avoid"
}
41 changes: 38 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,21 @@ 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).

> Never hardcode your `expo-password` in your workflow, use [secrets][link-actions-secrets] to store them.
> 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.
> `expo-token` is available from Expo CLI `3.25.0`.

## Example workflows

Before you dive into the workflow examples, you should know the basics of GitHub Actions.
Expand All @@ -60,6 +65,7 @@ You can read more about this in the [GitHub Actions documentation][link-actions]
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)

### Publish on any push to master

Expand Down Expand Up @@ -214,15 +220,44 @@ jobs:
- run: expo build:web
```

### Authenticate using an Expo token

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.

```yml
name: Expo Publish
on:
push:
branches:
- master
jobs:
publish:
name: Install and publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12.x
- uses: expo/expo-github-action@v5
with:
expo-version: 3.x
expo-token: ${{ secrets.EXPO_TOKEN }}
- run: yarn install
- run: expo publish
```

## Things to know

### Automatic Expo login

You need to authenticate for some Expo commands like `expo publish` and `expo build`.
This action gives you configuration options to keep your workflow simple.
Under the hood, it uses the [`EXPO_CLI_PASSWORD`][link-expo-cli-password] environment variable to make this as secure as possible.
You can choose if you want to authenticate using an `EXPO_TOKEN` or account credentials.
Under the hood, it uses the [`EXPO_CLI_PASSWORD`][link-expo-cli-password] environment variable to make credentials authentication as secure as possible.

> Note, this action only uses your credentials to authenticate with Expo. It doesn't store these anywhere.
> Note, this action only uses your token or credentials to authenticate with Expo. It doesn't store these anywhere.

### Using the built-in cache

Expand Down
2 changes: 2 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ inputs:
default: latest
expo-username:
description: Your Expo username, for authentication.
expo-token:
description: Your Expo token, for authentication. (use with secrets)
expo-password:
description: Your Expo password, for authentication. (use with secrets)
expo-packager:
Expand Down
53 changes: 45 additions & 8 deletions build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12072,7 +12072,11 @@ function run() {
cacheKey: core_1.getInput('expo-cache-key') || undefined,
});
core_1.addPath(path);
yield expo_1.authenticate(core_1.getInput('expo-username'), core_1.getInput('expo-password'));
yield expo_1.authenticate({
token: core_1.getInput('expo-token') || undefined,
username: core_1.getInput('expo-username') || undefined,
password: core_1.getInput('expo-password') || undefined,
});
const shouldPatchWatchers = core_1.getInput('expo-patch-watchers') || 'true';
if (shouldPatchWatchers !== 'false') {
yield system_1.patchWatchers();
Expand Down Expand Up @@ -20722,28 +20726,61 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.authenticate = void 0;
exports.authenticate = exports.authWithToken = exports.authWithCredentials = void 0;
const core = __importStar(__webpack_require__(470));
const cli = __importStar(__webpack_require__(986));
/**
* Authenticate at Expo using `expo login`.
* This step is required for publishing and building new apps.
* It uses the `EXPO_CLI_PASSWORD` environment variable for improved security.
*/
function authenticate(username, password) {
function authWithCredentials(username, password) {
return __awaiter(this, void 0, void 0, function* () {
if (!username || !password) {
return core.info('Skipping authentication, `expo-username` and/or `expo-password` not set...');
return core.info('Skipping authentication: `expo-username` and/or `expo-password` not set...');
}
// github actions toolkit will handle commands with `.cmd` on windows, we need that
const bin = process.platform === 'win32'
? 'expo.cmd'
: 'expo';
const bin = process.platform === 'win32' ? 'expo.cmd' : 'expo';
yield cli.exec(bin, ['login', `--username=${username}`], {
env: Object.assign(Object.assign({}, process.env), { EXPO_CLI_PASSWORD: password }),
});
});
}
exports.authWithCredentials = authWithCredentials;
/**
* Authenticate with Expo using `EXPO_TOKEN`.
* This exports the EXPO_TOKEN environment variable for all future steps within the workflow.
* It also double-checks if this token is valid and for what user, by running `expo whoami`.
*
* @see https://github.com/actions/toolkit/blob/905b2c7b0681b11056141a60055f1ba77358b7e9/packages/core/src/core.ts#L39
*/
function authWithToken(token) {
return __awaiter(this, void 0, void 0, function* () {
if (!token) {
return core.info('Skipping authentication: `expo-token` not set...');
}
// github actions toolkit will handle commands with `.cmd` on windows, we need that
const bin = process.platform === 'win32' ? 'expo.cmd' : 'expo';
yield cli.exec(bin, ['whoami'], {
env: Object.assign(Object.assign({}, process.env), { EXPO_TOKEN: token }),
});
core.exportVariable('EXPO_TOKEN', token);
});
}
exports.authWithToken = authWithToken;
/**
* Authenticate with Expo using either the token or username/password method.
* If both of them are set, token has priority.
*/
function authenticate(options) {
if (options.token) {
return authWithToken(options.token);
}
if (options.username || options.password) {
return authWithCredentials(options.username, options.password);
}
core.info('Skipping authentication: `expo-token`, `expo-username`, and/or `expo-password` not set...');
}
exports.authenticate = authenticate;


Expand Down Expand Up @@ -67064,7 +67101,7 @@ function patchWatchers() {
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("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');
}
});
Expand Down
55 changes: 50 additions & 5 deletions src/expo.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import * as core from '@actions/core';
import * as cli from '@actions/exec';

type AuthOptions = {
token?: string;
username?: string;
password?: string;
};

/**
* Authenticate at Expo using `expo login`.
* This step is required for publishing and building new apps.
* It uses the `EXPO_CLI_PASSWORD` environment variable for improved security.
*/
export async function authenticate(username: string, password: string) {
export async function authWithCredentials(username?: string, password?: string) {
if (!username || !password) {
return core.info('Skipping authentication, `expo-username` and/or `expo-password` not set...');
return core.info('Skipping authentication: `expo-username` and/or `expo-password` not set...');
}

// github actions toolkit will handle commands with `.cmd` on windows, we need that
const bin = process.platform === 'win32'
? 'expo.cmd'
: 'expo';
const bin = process.platform === 'win32' ? 'expo.cmd' : 'expo';

await cli.exec(bin, ['login', `--username=${username}`], {
env: {
Expand All @@ -23,3 +27,44 @@ export async function authenticate(username: string, password: string) {
},
});
}

/**
* Authenticate with Expo using `EXPO_TOKEN`.
* This exports the EXPO_TOKEN environment variable for all future steps within the workflow.
* It also double-checks if this token is valid and for what user, by running `expo whoami`.
*
* @see https://github.com/actions/toolkit/blob/905b2c7b0681b11056141a60055f1ba77358b7e9/packages/core/src/core.ts#L39
*/
export async function authWithToken(token?: string) {
if (!token) {
return core.info('Skipping authentication: `expo-token` not set...');
}

// github actions toolkit will handle commands with `.cmd` on windows, we need that
const bin = process.platform === 'win32' ? 'expo.cmd' : 'expo';

await cli.exec(bin, ['whoami'], {
env: {
...process.env,
EXPO_TOKEN: token,
},
});

core.exportVariable('EXPO_TOKEN', token);
}

/**
* Authenticate with Expo using either the token or username/password method.
* If both of them are set, token has priority.
*/
export function authenticate(options: AuthOptions) {
if (options.token) {
return authWithToken(options.token);
}

if (options.username || options.password) {
return authWithCredentials(options.username, options.password);
}

core.info('Skipping authentication: `expo-token`, `expo-username`, and/or `expo-password` not set...');
}
13 changes: 4 additions & 9 deletions src/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,17 @@ import * as core from '@actions/core';
import * as cli from '@actions/exec';
import * as io from '@actions/io';
import * as path from 'path';
import {
fromLocalCache,
fromRemoteCache,
toLocalCache,
toRemoteCache,
} from './cache';
import { fromLocalCache, fromRemoteCache, toLocalCache, toRemoteCache } from './cache';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const registry = require('libnpm');

interface InstallConfig {
type InstallConfig = {
version: string;
packager: string;
cache?: boolean;
cacheKey?: string;
}
};

/**
* Resolve the provided semver to exact version of `expo-cli`.
Expand All @@ -44,7 +39,7 @@ export async function install(config: InstallConfig) {
}

if (!root) {
root = await fromPackager(exact, config.packager)
root = await fromPackager(exact, config.packager);
root = await toLocalCache(root, exact);

if (config.cache) {
Expand Down
9 changes: 5 additions & 4 deletions src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ export async function run() {

addPath(path);

await authenticate(
getInput('expo-username'),
getInput('expo-password'),
);
await authenticate({
token: getInput('expo-token') || undefined,
username: getInput('expo-username') || undefined,
password: getInput('expo-password') || undefined,
});

const shouldPatchWatchers = getInput('expo-patch-watchers') || 'true';

Expand Down
2 changes: 1 addition & 1 deletion src/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function patchWatchers() {
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("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');
}
}
33 changes: 23 additions & 10 deletions tests/cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,29 @@ describe('toLocalCache', () => {
});

describe('fromRemoteCache', () => {
const spy = {
restore: jest.spyOn(remoteCache, 'restoreCache').mockImplementation(),
};
let spy: { [key: string]: jest.SpyInstance } = {};

beforeEach(() => {
spy = {
restore: jest.spyOn(remoteCache, 'restoreCache').mockImplementation(),
};
});

beforeAll(() => {
utils.setEnv('RUNNER_TOOL_CACHE', join('cache', 'path'));
});

afterAll(() => {
utils.restoreEnv();
spy.restore.mockRestore();
});

it('restores remote cache with default key', async () => {
expect(await cache.fromRemoteCache('3.20.1', '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`,
`expo-cli-${process.platform}-${os.arch()}-yarn-3.20.1`,
`expo-cli-${process.platform}-${os.arch()}-yarn-3.20.1`
);
});

Expand All @@ -54,14 +59,14 @@ describe('fromRemoteCache', () => {
expect(remoteCache.restoreCache).toBeCalledWith(
join('cache', 'path', 'expo-cli', '3.20.0', os.arch()),
'custom-cache-key',
'custom-cache-key',
'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()),
join('cache', 'path', 'expo-cli', '3.20.1', os.arch())
);
});

Expand All @@ -73,15 +78,23 @@ describe('fromRemoteCache', () => {
});

describe('toRemoteCache', () => {
const spy = {
save: jest.spyOn(remoteCache, 'saveCache').mockImplementation(),
};
let spy: { [key: string]: jest.SpyInstance } = {};

beforeEach(() => {
spy = {
save: jest.spyOn(remoteCache, 'saveCache').mockImplementation(),
};
});

afterAll(() => {
spy.save.mockRestore();
});

it('saves remote cache with default key', async () => {
expect(await cache.toRemoteCache(join('local', 'path'), '3.20.1', 'npm')).toBeUndefined();
expect(remoteCache.saveCache).toBeCalledWith(
join('local', 'path'),
`expo-cli-${process.platform}-${os.arch()}-npm-3.20.1`,
`expo-cli-${process.platform}-${os.arch()}-npm-3.20.1`
);
});

Expand Down
Loading

0 comments on commit 1c36889

Please sign in to comment.