Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add expo token authentication method #57

Merged
merged 2 commits into from
Aug 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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