Compare commits

...
12 Commits
Author SHA1 Message Date
Test User 9158a95d57 v0.9.0 2021-04-09 09:59:29 -07:00
Test User d0f3e387e7 Bump @expo/config-plugins to fix eas build:configure on Windows 2021-04-09 09:58:43 -07:00
jkh 158856f8ec [eas-cli] upgrade @urql/core and add retry (#320) 2021-04-09 07:19:04 -07:00
Dominik Sokal 1e59957753 Update CHANGELOG.md 2021-04-09 11:50:36 +02:00
Dominik Sokal f97d46040c add commands for managing webhooks (#314)
* add webhook:create command

* add webhook:list command

* add webhook:view command

* add webhook:update command

* add webhook:delete command

* generate graphql code

* update CHANGELOG

* address PR feedback
2021-04-09 11:50:07 +02:00
Dominik Sokal d7485cb5d2 fix first ios build for a project without ios.bundleIdentifier in app.json (#319)
* fix first ios build for a project without ios.bundleIdentifier in app.json

* update CHANGELOG

* address PR feedback

* address more PR feedback
2021-04-09 11:31:17 +02:00
071fdb7f4a [eas submit] Added new common error messages (#311)
* [eas submit] Added new common error messages

* Update CHANGELOG

* Apply suggestions from code review

Co-authored-by: James Ide <[email protected]>
Co-authored-by: Brent Vatne <[email protected]>

Co-authored-by: James Ide <[email protected]>
Co-authored-by: Brent Vatne <[email protected]>
2021-04-09 08:17:33 +02:00
jkh 6dd96ffb20 [eas-cli] add more info to transient errors (#316) 2021-04-08 16:26:34 -04:00
Quinlan Jung f56fb0f7de [eas-cli] setupProvisioningProfile using graphql (#301)
* [eas-cli] setupProvisioningProfile using graphql

refactor

fix lint

setup provisioning profile

* refactor stuff

* add comments for ppl

* simplify logic

* tests

* pr feedback
2021-04-08 11:19:14 -07:00
Wojciech Kozyra 7d28f6add9 add LICENSE (#313) 2021-04-08 12:35:50 +02:00
Juwan Wheatley eebef3e461 [eas-cli] use autocomplete list for secrets:delete (#309)
* [eas-cli] use autocomplete list for secrets:delete

* update changelog

* check if secret is account-wide and tell user that it could affect multiple apps
2021-04-07 13:08:00 -04:00
Dominik Sokal 8336f630ea update CHANGELOG 2021-04-06 12:55:17 +02:00
36 changed files with 3270 additions and 1426 deletions

No files matched your search

+15 -3
View File
@@ -8,12 +8,23 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Retry graphQL call on network or transient server errors. ([#320](https://github.com/expo/eas-cli/pull/320) by [@jkhales](https://github.com/jkhales))
- Display more friendly error messages when `eas submit` fails. ([#311](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
- Add support for managing webhooks (new commands: `webhook:create`, `webhook:view`, `webhook:list`, `webhook:update`, and `webhook:delete`). ([#314](https://github.com/expo/eas-cli/pull/314) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix the issue where the first iOS build fails for a project without `ios.bundleIdentifier` set in `app.json`. ([#319](https://github.com/expo/eas-cli/pull/319) by [@dsokal](https://github.com/dsokal))
- Bump @expo/config-plugins to fix `eas build:configure` on Windows
### 🧹 Chores
## [0.8.1](https://github.com/expo/eas-cli/releases/tag/v0.8.1) - 2021-04-06
### 🐛 Bug fixes
- Fix the issue where the build status never got updated when running `eas build`. ([#310](https://github.com/expo/eas-cli/pull/310) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
## [0.8.0](https://github.com/expo/eas-cli/releases/tag/v0.8.0) - 2021-04-06
### 🛠 Breaking changes
@@ -22,11 +33,12 @@ This is the log of notable changes to EAS CLI and related packages.
- `secrets:create` now uses flags rather than positional arguments ([#300](https://github.com/expo/eas-cli/pull/300) by [@fiberjw](https://github.com/fiberjw))
- `secrets:create`'s `target` arg is now called `scope` ([#300](https://github.com/expo/eas-cli/pull/300) by [@fiberjw](https://github.com/fiberjw))
- `secrets:list`'s `target` property is now called `scope` ([#300](https://github.com/expo/eas-cli/pull/300) by [@fiberjw](https://github.com/fiberjw))
- `secrets:delete`'s `ID` arg is now optional ([#309](https://github.com/expo/eas-cli/pull/309) by [@fiberjw](https://github.com/fiberjw))
- `secrets:delete`'s now allows users to choose secrets from a list ([#309](https://github.com/expo/eas-cli/pull/309) by [@fiberjw](https://github.com/fiberjw))
### 🎉 New features
- `build:view` and `build:list` now showing the distribution type (store / internal) and release channel. ([#284](https://github.com/expo/eas-cli/pull/284) by [@vthibault](https://github.com/vthibault))
- Add analytics to EAS Build. ([#162](https://github.com/expo/eas-cli/pull/162) by [@wkozyra95](https://github.com/wkozyra95))
- Improve tar archive support in EAS Submit. ([#297](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2020-present 650 Industries, Inc. (aka Expo)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.8.1"
"version": "0.9.0"
}
+92 -27
View File
@@ -48,9 +48,14 @@ eas --help COMMAND
* [`eas device:view [UDID]`](#eas-deviceview-udid)
* [`eas help [COMMAND]`](#eas-help-command)
* [`eas secrets:create`](#eas-secretscreate)
* [`eas secrets:delete ID`](#eas-secretsdelete-id)
* [`eas secrets:delete [ID]`](#eas-secretsdelete-id)
* [`eas secrets:list`](#eas-secretslist)
* [`eas submit --platform=(android|ios)`](#eas-submit---platformandroidios)
* [`eas webhook:create`](#eas-webhookcreate)
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
* [`eas webhook:list`](#eas-webhooklist)
* [`eas webhook:update`](#eas-webhookupdate)
* [`eas webhook:view ID`](#eas-webhookview-id)
## `eas account:login`
@@ -64,7 +69,7 @@ ALIASES
$ eas login
```
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.8.1/build/commands/account/login.ts)_
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/account/login.ts)_
## `eas account:logout`
@@ -78,7 +83,7 @@ ALIASES
$ eas logout
```
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.8.1/build/commands/account/logout.ts)_
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/account/logout.ts)_
## `eas account:view`
@@ -92,7 +97,7 @@ ALIASES
$ eas whoami
```
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.8.1/build/commands/account/view.ts)_
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -103,8 +108,6 @@ USAGE
$ eas analytics [STATUS]
```
_See code: [build/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.8.1/build/commands/analytics.ts)_
## `eas build`
Start a build
@@ -122,7 +125,7 @@ OPTIONS
--[no-]wait Wait for build(s) to complete
```
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.8.1/build/commands/build/index.ts)_
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -133,8 +136,6 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [build/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.8.1/build/commands/build/cancel.ts)_
## `eas build:configure`
Configure the project to support EAS Build.
@@ -148,7 +149,7 @@ OPTIONS
--allow-experimental Enable experimental configuration steps.
```
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.8.1/build/commands/build/configure.ts)_
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/build/configure.ts)_
## `eas build:list`
@@ -164,7 +165,7 @@ OPTIONS
--status=(in-queue|in-progress|errored|finished|canceled)
```
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.8.1/build/commands/build/list.ts)_
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/build/list.ts)_
## `eas build:view [BUILD_ID]`
@@ -175,7 +176,7 @@ USAGE
$ eas build:view [BUILD_ID]
```
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.8.1/build/commands/build/view.ts)_
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/build/view.ts)_
## `eas credentials`
@@ -186,7 +187,7 @@ USAGE
$ eas credentials
```
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.8.1/build/commands/credentials.ts)_
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/credentials.ts)_
## `eas device:create`
@@ -197,7 +198,7 @@ USAGE
$ eas device:create
```
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.8.1/build/commands/device/create.ts)_
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/device/create.ts)_
## `eas device:list`
@@ -211,7 +212,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.8.1/build/commands/device/list.ts)_
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -222,7 +223,7 @@ USAGE
$ eas device:view [UDID]
```
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.8.1/build/commands/device/view.ts)_
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/device/view.ts)_
## `eas help [COMMAND]`
@@ -255,15 +256,13 @@ OPTIONS
--value=value Value of the secret
```
_See code: [build/commands/secrets/create.ts](https://github.com/expo/eas-cli/blob/v0.8.1/build/commands/secrets/create.ts)_
## `eas secrets:delete ID`
## `eas secrets:delete [ID]`
Delete an environment secret by ID.
```
USAGE
$ eas secrets:delete ID
$ eas secrets:delete [ID]
ARGUMENTS
ID ID of the secret to delete
@@ -272,8 +271,6 @@ DESCRIPTION
Unsure where to find the secret's ID? Run eas secrets:list
```
_See code: [build/commands/secrets/delete.ts](https://github.com/expo/eas-cli/blob/v0.8.1/build/commands/secrets/delete.ts)_
## `eas secrets:list`
Lists environment secrets available for your current app
@@ -283,8 +280,6 @@ USAGE
$ eas secrets:list
```
_See code: [build/commands/secrets/list.ts](https://github.com/expo/eas-cli/blob/v0.8.1/build/commands/secrets/list.ts)_
## `eas submit --platform=(android|ios)`
Submits build artifact to app store
@@ -309,8 +304,7 @@ OPTIONS
iOS specific options=asc-app-id App Store Connect unique application Apple ID
number. Providing this param results in skipping
app creation step. Learn more:
https://expo.fyi/asc-app-id
app creation step. Learn more.
iOS specific options=bundle-identifier Your iOS Bundle Identifier (default:
expo.ios.bundleIdentifier from app config)
@@ -363,5 +357,76 @@ EXAMPLES
and provide its App ID
```
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.8.1/build/commands/submit.ts)_
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/submit.ts)_
## `eas webhook:create`
Create a webhook on the current project.
```
USAGE
$ eas webhook:create
OPTIONS
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
--secret=secret Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature'
header.
--url=url Webhook URL
```
## `eas webhook:delete [ID]`
Delete a webhook on the current project.
```
USAGE
$ eas webhook:delete [ID]
ARGUMENTS
ID ID of the webhook to delete
```
## `eas webhook:list`
List webhooks on the current project.
```
USAGE
$ eas webhook:list
OPTIONS
--event=(BUILD) Event type that triggers the webhook
```
## `eas webhook:update`
Create a webhook on the current project.
```
USAGE
$ eas webhook:update
OPTIONS
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
--id=id (required) Webhook ID
--secret=secret Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature'
header.
--url=url Webhook URL
```
## `eas webhook:view ID`
View a webhook on the current project.
```
USAGE
$ eas webhook:view ID
ARGUMENTS
ID ID of the webhook to view
```
<!-- commandsstop -->
File diff suppressed because it is too large. Load diff
+5 -4
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.8.1",
"version": "0.9.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -12,9 +12,9 @@
"@amplitude/node": "^1.5.0",
"@expo/apple-utils": "0.0.0-alpha.17",
"@expo/config": "~3.3.19",
"@expo/config-plugins": "1.0.21-alpha.0",
"@expo/config-plugins": "1.0.24",
"@expo/eas-build-job": "0.2.22",
"@expo/eas-json": "^0.8.0",
"@expo/eas-json": "^0.9.0",
"@expo/json-file": "^8.2.24",
"@expo/plist": "^0.0.11",
"@expo/plugin-warn-if-update-available": "^1.7.0",
@@ -25,7 +25,8 @@
"@oclif/config": "^1",
"@oclif/errors": "^1.3.3",
"@oclif/plugin-help": "^3",
"@urql/core": "^1.13.0",
"@urql/core": "^2.0.0",
"@urql/exchange-retry": "^0.2.0",
"chalk": "^4.1.0",
"cli-table3": "^0.6.0",
"dateformat": "^3.0.3",
+7 -5
View File
@@ -1,4 +1,4 @@
import { getConfig } from '@expo/config';
import { ExpoConfig, getConfig } from '@expo/config';
import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import fs from 'fs-extra';
@@ -73,7 +73,7 @@ export default class Build extends Command {
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
let { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
if (!(await isEasEnabledForProjectAsync(projectId))) {
@@ -93,7 +93,7 @@ export default class Build extends Command {
return;
}
await ensureProjectConfiguredAsync(projectDir);
exp = (await ensureProjectConfiguredAsync(projectDir)) ?? exp;
const commandCtx = await createCommandContextAsync({
requestedPlatform: platform,
@@ -189,9 +189,9 @@ async function promptForPlatformAsync(): Promise<RequestedPlatform> {
return platform;
}
async function ensureProjectConfiguredAsync(projectDir: string): Promise<void> {
async function ensureProjectConfiguredAsync(projectDir: string): Promise<ExpoConfig | null> {
if (await fs.pathExists(path.join(projectDir, 'eas.json'))) {
return;
return null;
}
const confirm = await confirmAsync({
message: 'This app is not set up for building with EAS. Set it up now?',
@@ -207,6 +207,8 @@ async function ensureProjectConfiguredAsync(projectDir: string): Promise<void> {
'Build process requires clean git working tree, please commit all your changes and run `eas build` again'
);
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
return exp;
} else {
throw new Error(
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
@@ -3,14 +3,24 @@ import { Command } from '@oclif/command';
import chalk from 'chalk';
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
import {
EnvironmentSecretWithScope,
EnvironmentSecretsQuery,
} from '../../graphql/queries/EnvironmentSecretsQuery';
import Log from '../../log';
import {
isEasEnabledForProjectAsync,
warnEasUnavailable,
} from '../../project/isEasEnabledForProject';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { toggleConfirmAsync } from '../../prompts';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions';
import { EnvironmentSecretScope } from './create';
export default class EnvironmentSecretDelete extends Command {
static description = `Delete an environment secret by ID.
@@ -19,7 +29,7 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
static args = [
{
name: 'id',
required: true,
required: false,
description: `ID of the secret to delete`,
},
];
@@ -30,6 +40,8 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const projectFullName = await getProjectFullNameAsync(exp);
const projectAccountName = await getProjectAccountNameAsync(exp);
if (!(await isEasEnabledForProjectAsync(projectId))) {
warnEasUnavailable();
@@ -37,23 +49,50 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
return;
}
const {
let {
args: { id },
} = this.parse(EnvironmentSecretDelete);
let secret: EnvironmentSecretWithScope | undefined;
if (!id) {
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectFullName);
({ secret } = await promptAsync({
type: 'autocomplete',
name: 'secret',
message: 'Pick the secret to be deleted:',
choices: secrets.map(secret => ({
title: `${secret.name} (${secret.scope})`,
value: secret,
})),
}));
id = secret?.id;
}
Log.addNewLineIfNone();
Log.warn(
`You are about to permamently delete secret with id: "${id}".\nThis action is irreversible.`
`You are about to permamently delete secret${
secret?.name ? ` "${secret?.name}"` : ''
} with id: "${id}".\nThis action is irreversible.`
);
Log.newLine();
const confirmed = await toggleConfirmAsync({ message: 'Are you sure you wish to proceed?' });
const confirmed = await toggleConfirmAsync({
message: `Are you sure you wish to proceed?${
secret?.scope === EnvironmentSecretScope.ACCOUNT
? ' This secret is applied across your whole account and may affect multiple apps.'
: ''
}`,
});
if (!confirmed) {
Log.error(`Canceled deletion of secret with id: "${id}".`);
Log.error(
`Canceled deletion of secret${secret?.name ? ` "${secret?.name}"` : ''} with id: "${id}".`
);
process.exit(1);
}
await EnvironmentSecretMutation.delete(id);
Log.withTick(`️Deleted secret with id "${id}".`);
Log.withTick(`️Deleted secret${secret?.name ? ` "${secret?.name}"` : ''} with id "${id}".`);
}
}
+1 -11
View File
@@ -4,7 +4,6 @@ import chalk from 'chalk';
import Table from 'cli-table3';
import dateFormat from 'dateformat';
import { EnvironmentSecretFragment } from '../../graphql/generated';
import { EnvironmentSecretsQuery } from '../../graphql/queries/EnvironmentSecretsQuery';
import Log from '../../log';
import {
@@ -18,7 +17,6 @@ import {
getProjectIdAsync,
} from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { EnvironmentSecretScope } from './create';
export default class EnvironmentSecretsList extends Command {
static description = 'Lists environment secrets available for your current app';
@@ -43,15 +41,7 @@ export default class EnvironmentSecretsList extends Command {
throw new Error("Please run this command inside your project's directory");
}
const [accountSecrets, appSecrets] = await Promise.all([
EnvironmentSecretsQuery.byAcccountNameAsync(projectAccountName),
EnvironmentSecretsQuery.byAppFullNameAsync(projectFullName),
]);
const secrets = [
...appSecrets.map(s => ({ ...s, scope: EnvironmentSecretScope.PROJECT })),
...accountSecrets.map(s => ({ ...s, scope: EnvironmentSecretScope.ACCOUNT })),
] as (EnvironmentSecretFragment & { scope: EnvironmentSecretScope })[];
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectFullName);
const table = new Table({
head: ['Name', 'Scope', 'ID', 'Updated at'],
@@ -0,0 +1,50 @@
import { getConfig } from '@expo/config';
import { Command, flags } from '@oclif/command';
import ora from 'ora';
import { WebhookType } from '../../graphql/generated';
import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { prepareInputParamsAsync } from '../../webhooks/input';
export default class WebhookCreate extends Command {
static description = 'Create a webhook on the current project.';
static flags = {
event: flags.enum({
description: 'Event type that triggers the webhook',
options: [WebhookType.Build],
default: WebhookType.Build,
}),
url: flags.string({
description: 'Webhook URL',
}),
secret: flags.string({
description:
"Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature' header.",
}),
};
async run() {
await ensureLoggedInAsync();
const { flags } = this.parse(WebhookCreate);
const webhookInputParams = await prepareInputParamsAsync(flags);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const spinner = ora('Creating a webhook').start();
try {
await WebhookMutation.createWebhookAsync(projectId, webhookInputParams);
spinner.succeed('Successfully created a webhook');
} catch (err) {
spinner.fail('Failed to create a webhook');
throw err;
}
}
}
@@ -0,0 +1,102 @@
import { getConfig } from '@expo/config';
import { Command } from '@oclif/command';
import assert from 'assert';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import ora from 'ora';
import { WebhookFragment } from '../../graphql/generated';
import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
import Log from '../../log';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions';
import { formatWebhook } from '../../webhooks/formatWebhook';
export default class WebhookDelete extends Command {
static description = 'Delete a webhook on the current project.';
static args = [
{
name: 'ID',
required: false,
description: 'ID of the webhook to delete',
},
];
async run() {
await ensureLoggedInAsync();
let {
args: { ID: webhookId },
} = this.parse(WebhookDelete);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
let webhook: WebhookFragment | undefined =
webhookId && (await WebhookQuery.byIdAsync(webhookId));
if (!webhookId) {
const webhooks = await fetchWebhooksByAppIdAsync(projectId);
if (webhooks.length === 0) {
process.exit(1);
}
({ webhook } = await promptAsync({
type: 'autocomplete',
name: 'webhook',
message: 'Pick the webhook to be deleted:',
choices: webhooks.map(i => ({
title: `${chalk.bold(i.url)} (Event: ${i.event}, ID: ${i.id})`,
value: i,
})),
}));
webhookId = nullthrows(webhook).id;
}
assert(webhook, 'Webhook must be defined here');
Log.addNewLineIfNone();
Log.log(formatWebhook(webhook));
Log.newLine();
Log.warn(`You are about to permamently delete this webhook.\nThis action is irreversible.`);
Log.newLine();
const confirmed = await toggleConfirmAsync({
message: 'Are you sure you wish to proceed?',
});
if (!confirmed) {
Log.error(`Canceled deletion of the webhook`);
process.exit(1);
}
const spinner = ora('Deleting the webhook').start();
try {
await WebhookMutation.deleteWebhookAsync(webhookId);
spinner.succeed('Successfully deleted the webhook');
} catch (err) {
spinner.fail('Failed to delete the webhook');
throw err;
}
}
}
async function fetchWebhooksByAppIdAsync(appId: string): Promise<WebhookFragment[]> {
const spinner = ora('Fetching webhooks').start();
try {
const webhooks = await WebhookQuery.byAppIdAsync(appId);
if (webhooks.length === 0) {
spinner.fail('There are no webhooks on the project');
return [];
} else {
spinner.succeed(`Successfully fetched ${webhooks.length} webhooks`);
return webhooks;
}
} catch (err) {
spinner.fail("Something went wrong and we couldn't fetch the webhook list");
throw err;
}
}
@@ -0,0 +1,60 @@
import { getConfig } from '@expo/config';
import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import ora from 'ora';
import { WebhookType } from '../../graphql/generated';
import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
import Log from '../../log';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { formatWebhook } from '../../webhooks/formatWebhook';
export default class WebhookList extends Command {
static description = 'List webhooks on the current project.';
static flags = {
event: flags.enum({
description: 'Event type that triggers the webhook',
options: [WebhookType.Build],
}),
};
async run() {
await ensureLoggedInAsync();
const {
flags: { event },
} = this.parse(WebhookList);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const projectFullName = await getProjectFullNameAsync(exp);
const spinner = ora(`Fetching the list of webhook on project ${projectFullName}`).start();
try {
const webhooks = await WebhookQuery.byAppIdAsync(projectId, event && { event });
if (webhooks.length === 0) {
spinner.fail(`There are no webhooks on project ${projectFullName}`);
} else {
spinner.succeed(`Found ${webhooks.length} webhooks on project ${projectFullName}`);
const list = webhooks
.map(webhook => formatWebhook(webhook))
.join(`\n\n${chalk.dim('———')}\n\n`);
Log.log(`\n${list}`);
}
} catch (err) {
spinner.fail(
`Something went wrong and we couldn't fetch the webhook list for the project ${projectFullName}`
);
throw err;
}
}
}
@@ -0,0 +1,54 @@
import { Command, flags } from '@oclif/command';
import pick from 'lodash/pick';
import ora from 'ora';
import { WebhookType } from '../../graphql/generated';
import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
import { ensureLoggedInAsync } from '../../user/actions';
import { prepareInputParamsAsync } from '../../webhooks/input';
export default class WebhookUpdate extends Command {
static description = 'Create a webhook on the current project.';
static flags = {
id: flags.string({
description: 'Webhook ID',
required: true,
}),
event: flags.enum({
description: 'Event type that triggers the webhook',
options: [WebhookType.Build],
default: WebhookType.Build,
}),
url: flags.string({
description: 'Webhook URL',
}),
secret: flags.string({
description:
"Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature' header.",
}),
};
async run() {
await ensureLoggedInAsync();
const { flags } = this.parse(WebhookUpdate);
const webhookId = flags.id;
const webhook = await WebhookQuery.byIdAsync(webhookId);
const webhookInputParams = await prepareInputParamsAsync(
pick(flags, ['event', 'url', 'secret']),
webhook
);
const spinner = ora('Updating a webhook').start();
try {
await WebhookMutation.updateWebhookAsync(webhookId, webhookInputParams);
spinner.succeed('Successfully updated a webhook');
} catch (err) {
spinner.fail('Failed to update a webhook');
throw err;
}
}
}
@@ -0,0 +1,36 @@
import { Command } from '@oclif/command';
import ora from 'ora';
import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
import Log from '../../log';
import { ensureLoggedInAsync } from '../../user/actions';
import { formatWebhook } from '../../webhooks/formatWebhook';
export default class WebhookView extends Command {
static description = 'View a webhook on the current project.';
static args = [
{
name: 'ID',
required: true,
description: 'ID of the webhook to view',
},
];
async run() {
await ensureLoggedInAsync();
const {
args: { ID: webhookId },
} = this.parse(WebhookView);
const spinner = ora(`Fetching the webhook details for ID ${webhookId}`).start();
try {
const webhook = await WebhookQuery.byIdAsync(webhookId);
spinner.succeed(`Found the webhook details`);
Log.log(`\n${formatWebhook(webhook)}`);
} catch (err) {
spinner.fail(`Couldn't find the webhook with ID ${webhookId}`);
throw err;
}
}
}
@@ -4,7 +4,10 @@ import {
AppleDistributionCertificateFragment,
AppleProvisioningProfileFragment,
AppleTeamFragment,
IosAppBuildCredentialsFragment,
IosDistributionType,
} from '../../graphql/generated';
import { IosAppCredentialsWithBuildCredentialsQueryResult } from '../ios/api/graphql/queries/IosAppCredentialsQuery';
import {
DistributionCertificate,
DistributionCertificateStoreInfo,
@@ -107,6 +110,18 @@ export const testDistCertFragmentOneDependency: AppleDistributionCertificateFrag
],
};
export const testIosAppBuildCredentialsFragment: IosAppBuildCredentialsFragment = {
id: 'test-ios-app-build-credentials-id',
iosDistributionType: IosDistributionType.AppStore,
distributionCertificate: testDistCertFragmentNoDependencies,
provisioningProfile: testProvisioningProfileFragment,
};
export const testIosAppCredentialsWithBuildCredentialsQueryResult: IosAppCredentialsWithBuildCredentialsQueryResult = {
id: 'test-app-credential-id',
iosAppBuildCredentialsArray: [testIosAppBuildCredentialsFragment],
};
export const testDistCert: DistributionCertificate = {
certP12: 'Y2VydHAxMg==',
certPassword: 'test-password',
@@ -3,7 +3,6 @@ import nullthrows from 'nullthrows';
import { AppleDistributionCertificateFragment } from '../../../../graphql/generated';
import Log from '../../../../log';
import { Action, CredentialsManager } from '../../../CredentialsManager';
import { Context } from '../../../context';
import { askForUserProvidedAsync } from '../../../utils/promptForCredentials';
import { AppLookupParams } from '../../api/GraphqlClient';
@@ -15,22 +14,13 @@ import { MissingCredentialsNonInteractiveError } from '../../errors';
import { generateProvisioningProfileAsync } from '../ProvisioningProfileUtils';
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
export class CreateProvisioningProfile implements Action {
private _provisioningProfile?: AppleProvisioningProfileMutationResult;
export class CreateProvisioningProfile {
constructor(
private app: AppLookupParams,
private distributionCertificate: AppleDistributionCertificateFragment
) {}
public get provisioningProfile(): AppleProvisioningProfileMutationResult {
assert(
this._provisioningProfile,
'provisioningProfile can be accessed only after calling .runAsync()'
);
return this._provisioningProfile;
}
async runAsync(_manager: CredentialsManager, ctx: Context): Promise<void> {
async runAsync(ctx: Context): Promise<AppleProvisioningProfileMutationResult> {
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'Creating Provisioning Profiles is only supported in interactive mode.'
@@ -43,7 +33,7 @@ export class CreateProvisioningProfile implements Action {
this.app,
appleTeam
);
this._provisioningProfile = await ctx.newIos.createProvisioningProfileAsync(
const provisioningProfileMutationResult = await ctx.newIos.createProvisioningProfileAsync(
this.app,
appleAppIdentifier,
{
@@ -52,6 +42,7 @@ export class CreateProvisioningProfile implements Action {
}
);
Log.succeed('Created provisioning profile');
return provisioningProfileMutationResult;
}
private async provideOrGenerateAsync(
@@ -0,0 +1,216 @@
import nullthrows from 'nullthrows';
import {
AppleDistributionCertificateFragment,
AppleProvisioningProfileFragment,
IosAppBuildCredentialsFragment,
IosDistributionType,
} from '../../../../graphql/generated';
import Log from '../../../../log';
import { confirmAsync } from '../../../../prompts';
import { CredentialsManager } from '../../../CredentialsManager';
import { Context } from '../../../context';
import { AppLookupParams } from '../../api/GraphqlClient';
import { ProvisioningProfileStoreInfo } from '../../appstore/Credentials.types';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import { validateProvisioningProfileAsync } from '../../validators/validateProvisioningProfile';
import { formatProvisioningProfileFromApple } from '../ProvisioningProfileUtils';
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { ConfigureProvisioningProfile } from './ConfigureProvisioningProfile';
import { CreateProvisioningProfile } from './CreateProvisioningProfile';
import { SetupDistributionCertificate } from './SetupDistributionCertificate';
export class SetupProvisioningProfile {
constructor(private app: AppLookupParams) {}
async areBuildCredentialsSetupAsync(ctx: Context): Promise<boolean> {
const buildCredentials = await this.getBuildCredentialsAsync(ctx);
if (!buildCredentials) {
return false;
}
const { distributionCertificate, provisioningProfile } = buildCredentials;
if (!distributionCertificate || !provisioningProfile) {
return false;
}
return await this.isCurrentProfileValidAsync(ctx, provisioningProfile, distributionCertificate);
}
async getBuildCredentialsAsync(ctx: Context): Promise<IosAppBuildCredentialsFragment | null> {
const appCredentials = await ctx.newIos.getIosAppCredentialsWithBuildCredentialsAsync(
this.app,
{
iosDistributionType: IosDistributionType.AppStore,
}
);
if (!appCredentials || appCredentials.iosAppBuildCredentialsArray.length === 0) {
return null;
}
const [buildCredentials] = appCredentials.iosAppBuildCredentialsArray;
return buildCredentials;
}
async assignNewAndDeleteOldProfileAsync(
ctx: Context,
distCert: AppleDistributionCertificateFragment,
currentProfile: AppleProvisioningProfileFragment
): Promise<IosAppBuildCredentialsFragment> {
const buildCredentials = await this.createAndAssignProfileAsync(ctx, distCert);
// delete 'currentProfile' since its no longer valid
await ctx.newIos.deleteProvisioningProfilesAsync([currentProfile.id]);
return buildCredentials;
}
async createAndAssignProfileAsync(
ctx: Context,
distCert: AppleDistributionCertificateFragment
): Promise<IosAppBuildCredentialsFragment> {
const provisioningProfile = await new CreateProvisioningProfile(this.app, distCert).runAsync(
ctx
);
return await this.assignBuildCredentialsAsync(ctx, distCert, provisioningProfile);
}
async configureAndAssignProfileAsync(
ctx: Context,
distCert: AppleDistributionCertificateFragment,
originalProvisioningProfile: AppleProvisioningProfileFragment
): Promise<IosAppBuildCredentialsFragment | null> {
const profileConfigurator = new ConfigureProvisioningProfile(
this.app,
distCert,
originalProvisioningProfile
);
const updatedProvisioningProfile = await profileConfigurator.runAsync(ctx);
if (!updatedProvisioningProfile) {
return null;
}
return await this.assignBuildCredentialsAsync(ctx, distCert, updatedProvisioningProfile);
}
async assignBuildCredentialsAsync(
ctx: Context,
distCert: AppleDistributionCertificateFragment,
provisioningProfile: AppleProvisioningProfileFragment
): Promise<IosAppBuildCredentialsFragment> {
const appleTeam = nullthrows(await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app));
const appleAppIdentifier = await ctx.newIos.createOrGetExistingAppleAppIdentifierAsync(
this.app,
appleTeam
);
return await ctx.newIos.createOrUpdateIosAppBuildCredentialsAsync(this.app, {
appleTeam,
appleAppIdentifierId: appleAppIdentifier.id,
appleDistributionCertificateId: distCert.id,
appleProvisioningProfileId: provisioningProfile.id,
iosDistributionType: IosDistributionType.AppStore,
});
}
async getProvisioningProfileAsync(
ctx: Context
): Promise<AppleProvisioningProfileFragment | null> {
const buildCredentials = await this.getBuildCredentialsAsync(ctx);
if (!buildCredentials) {
return null;
}
return buildCredentials.provisioningProfile ?? null;
}
async runAsync(
manager: CredentialsManager,
ctx: Context
): Promise<IosAppBuildCredentialsFragment> {
const areBuildCredentialsSetup = await this.areBuildCredentialsSetupAsync(ctx);
if (areBuildCredentialsSetup) {
return nullthrows(await this.getBuildCredentialsAsync(ctx));
}
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'Provisioning profile is not configured correctly. Please run this command again in interactive mode.'
);
}
const distCertAction = new SetupDistributionCertificate(this.app, IosDistributionType.AppStore);
await distCertAction.runAsync(manager, ctx);
const distCert = distCertAction.distributionCertificate;
const currentProfile = await this.getProvisioningProfileAsync(ctx);
if (!currentProfile) {
return await this.createAndAssignProfileAsync(ctx, distCert);
}
// See if the profile we have exists on the Apple Servers
const existingProfiles = await ctx.appStore.listProvisioningProfilesAsync(
this.app.bundleIdentifier
);
const currentProfileFromServer = this.getCurrentProfileStoreInfo(
existingProfiles,
currentProfile
);
if (!currentProfileFromServer) {
return await this.assignNewAndDeleteOldProfileAsync(ctx, distCert, currentProfile);
}
const confirm = await confirmAsync({
message: `${formatProvisioningProfileFromApple(
currentProfileFromServer
)} \n Would you like to reuse the original profile?`,
});
if (!confirm) {
return await this.assignNewAndDeleteOldProfileAsync(ctx, distCert, currentProfile);
}
// If we get here, we've verified the current profile still exists on Apple
// But something wasn't quite right, so we want to fix and update it
const updatedProfile = await this.configureAndAssignProfileAsync(ctx, distCert, currentProfile);
if (!updatedProfile) {
// Something went wrong, so just create a new profile instead
return await this.assignNewAndDeleteOldProfileAsync(ctx, distCert, currentProfile);
}
return updatedProfile;
}
private async isCurrentProfileValidAsync(
ctx: Context,
currentProfile: AppleProvisioningProfileFragment,
currentCertificate: AppleDistributionCertificateFragment
): Promise<boolean> {
const { provisioningProfile, developerPortalIdentifier } = currentProfile;
const { certificateP12, certificatePassword } = currentCertificate;
if (!provisioningProfile || !certificateP12 || !certificatePassword) {
return false;
}
const validationResult = await validateProvisioningProfileAsync(
ctx,
{
provisioningProfile,
...(developerPortalIdentifier
? { provisioningProfileId: developerPortalIdentifier }
: null),
},
{
certP12: certificateP12,
certPassword: certificatePassword,
},
this.app.bundleIdentifier
);
if (!validationResult.ok) {
Log.warn(validationResult.error);
return false;
}
return true;
}
private getCurrentProfileStoreInfo(
profiles: ProvisioningProfileStoreInfo[],
currentProfile: AppleProvisioningProfileFragment
): ProvisioningProfileStoreInfo | null {
return (
profiles.find(profile =>
currentProfile.developerPortalIdentifier
? currentProfile.developerPortalIdentifier === profile.provisioningProfileId
: currentProfile.provisioningProfile === profile.provisioningProfile
) ?? null
);
}
}
@@ -0,0 +1,20 @@
import {
AppleDistributionCertificateFragment,
AppleProvisioningProfileFragment,
} from '../../../../../graphql/generated';
import { testProvisioningProfileFragment } from '../../../../__tests__/fixtures-ios';
import { Context } from '../../../../context';
import { AppLookupParams } from '../../../api/GraphqlClient';
import { AppleProvisioningProfileMutationResult } from '../../../api/graphql/mutations/AppleProvisioningProfileMutation';
export class ConfigureProvisioningProfile {
constructor(
private app: AppLookupParams,
private distributionCertificate: AppleDistributionCertificateFragment,
private originalProvisioningProfile: AppleProvisioningProfileFragment
) {}
public async runAsync(ctx: Context): Promise<AppleProvisioningProfileMutationResult | null> {
return testProvisioningProfileFragment;
}
}
@@ -0,0 +1,16 @@
import { AppleDistributionCertificateFragment } from '../../../../../graphql/generated';
import { testProvisioningProfileFragment } from '../../../../__tests__/fixtures-ios';
import { Context } from '../../../../context';
import { AppLookupParams } from '../../../api/GraphqlClient';
import { AppleProvisioningProfileMutationResult } from '../../../api/graphql/mutations/AppleProvisioningProfileMutation';
export class CreateProvisioningProfile {
constructor(
private app: AppLookupParams,
private distributionCertificate: AppleDistributionCertificateFragment
) {}
async runAsync(ctx: Context): Promise<AppleProvisioningProfileMutationResult> {
return testProvisioningProfileFragment;
}
}
@@ -0,0 +1,17 @@
import {
AppleDistributionCertificateFragment,
IosDistributionType,
} from '../../../../../graphql/generated';
import { Action, CredentialsManager } from '../../../../CredentialsManager';
import { testDistCertFragmentOneDependency } from '../../../../__tests__/fixtures-ios';
import { Context } from '../../../../context';
import { AppLookupParams } from '../../../api/GraphqlClient';
export class SetupDistributionCertificate implements Action {
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
public get distributionCertificate(): AppleDistributionCertificateFragment {
return testDistCertFragmentOneDependency;
}
public async runAsync(_manager: CredentialsManager, _ctx: Context): Promise<void> {}
}
@@ -1,6 +1,6 @@
import { confirmAsync } from '../../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../../__tests__/fixtures-appstore';
import { createCtxMock, createManagerMock } from '../../../../__tests__/fixtures-context';
import { createCtxMock } from '../../../../__tests__/fixtures-context';
import {
testDistCertFragmentNoDependencies,
testProvisioningProfile,
@@ -13,7 +13,6 @@ jest.mock('../../../../../prompts');
describe('CreateProvisioningProfile', () => {
it('creates a Provisioning Profile in Interactive Mode', async () => {
const manager = createManagerMock();
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -28,7 +27,7 @@ describe('CreateProvisioningProfile', () => {
appLookupParams,
testDistCertFragmentNoDependencies
);
await createProvProfAction.runAsync(manager, ctx);
await createProvProfAction.runAsync(ctx);
// expect provisioning profile to be created on expo servers
expect((ctx.newIos.createProvisioningProfileAsync as any).mock.calls.length).toBe(1);
@@ -36,7 +35,6 @@ describe('CreateProvisioningProfile', () => {
expect((ctx.appStore.createProvisioningProfileAsync as any).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const manager = createManagerMock();
const ctx = createCtxMock({
nonInteractive: true,
});
@@ -45,7 +43,7 @@ describe('CreateProvisioningProfile', () => {
appLookupParams,
testDistCertFragmentNoDependencies
);
await expect(createProvProfAction.runAsync(manager, ctx)).rejects.toThrowError(
await expect(createProvProfAction.runAsync(ctx)).rejects.toThrowError(
MissingCredentialsNonInteractiveError
);
@@ -0,0 +1,162 @@
import { confirmAsync } from '../../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../../__tests__/fixtures-appstore';
import { createCtxMock, createManagerMock } from '../../../../__tests__/fixtures-context';
import {
testAppleAppIdentifierFragment,
testIosAppBuildCredentialsFragment,
testIosAppCredentialsWithBuildCredentialsQueryResult,
} from '../../../../__tests__/fixtures-ios';
import { getNewIosApiMockWithoutCredentials } from '../../../../__tests__/fixtures-new-ios';
import { ManageIosBeta } from '../../../../manager/ManageIosBeta';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { validateProvisioningProfileAsync } from '../../../validators/validateProvisioningProfile';
import { SetupProvisioningProfile } from '../SetupProvisioningProfile';
jest.mock('../../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
jest.mock('../SetupDistributionCertificate');
jest.mock('../ConfigureProvisioningProfile');
jest.mock('../CreateProvisioningProfile');
jest.mock('../../../validators/validateProvisioningProfile');
describe('SetupProvisioningProfile', () => {
it('repairs existing Provisioning Profile with bad build credentials in Interactive Mode', async () => {
(validateProvisioningProfileAsync as jest.Mock).mockImplementation(() => ({
error: 'testing: everything is fine, ignore this',
ok: false,
}));
const manager = createManagerMock();
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
listProvisioningProfilesAsync: jest.fn(() => [
{
provisioningProfileId:
testIosAppCredentialsWithBuildCredentialsQueryResult.iosAppBuildCredentialsArray[0]
.provisioningProfile.developerPortalIdentifier,
},
]),
},
newIos: {
...getNewIosApiMockWithoutCredentials(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
() => testIosAppCredentialsWithBuildCredentialsQueryResult
),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(
() => testIosAppBuildCredentialsFragment
),
},
});
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
const setupProvisioningProfileAction = new SetupProvisioningProfile(appLookupParams);
await setupProvisioningProfileAction.runAsync(manager, ctx);
// expect build credentials to be created or updated on expo servers
expect((ctx.newIos.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(1);
// expect provisioning profile not to be deleted on expo servers
expect((ctx.newIos.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(0);
});
it('sets up a new Provisioning Profile with bad build credentials in Interactive Mode', async () => {
(validateProvisioningProfileAsync as jest.Mock).mockImplementation(() => ({
error: 'testing: everything is fine, ignore this',
ok: false,
}));
const manager = createManagerMock();
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
listProvisioningProfilesAsync: jest.fn(() => []),
},
newIos: {
...getNewIosApiMockWithoutCredentials(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
() => testIosAppCredentialsWithBuildCredentialsQueryResult
),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
},
});
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
const setupProvisioningProfileAction = new SetupProvisioningProfile(appLookupParams);
await setupProvisioningProfileAction.runAsync(manager, ctx);
// expect build credentials to be created or updated on expo servers
expect((ctx.newIos.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(1);
// expect provisioning profile to be deleted on expo servers
expect((ctx.newIos.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(1);
});
it('skips setting up a Provisioning Profile with prior build credentials configured properly in Interactive Mode', async () => {
(validateProvisioningProfileAsync as jest.Mock).mockImplementation(() => ({
ok: true,
}));
const manager = createManagerMock();
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
//listProvisioningProfilesAsync: jest.fn(() => []),
},
newIos: {
...getNewIosApiMockWithoutCredentials(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
() => testIosAppCredentialsWithBuildCredentialsQueryResult
),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
},
});
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
const setupProvisioningProfileAction = new SetupProvisioningProfile(appLookupParams);
await setupProvisioningProfileAction.runAsync(manager, ctx);
// expect build credentials not to be created or updated on expo servers
expect((ctx.newIos.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(0);
// expect provisioning profile not to be deleted on expo servers
expect((ctx.newIos.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(0);
});
it('sets up a Provisioning Profile with no prior build credentials configured in Interactive Mode', async () => {
const manager = createManagerMock();
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
},
newIos: {
...getNewIosApiMockWithoutCredentials(),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
},
});
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
const setupProvisioningProfileAction = new SetupProvisioningProfile(appLookupParams);
await setupProvisioningProfileAction.runAsync(manager, ctx);
// expect build credentials to be created or updated on expo servers
expect((ctx.newIos.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(1);
// expect provisioning profile not to be deleted on expo servers
expect((ctx.newIos.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(0);
});
it('errors in Non Interactive Mode', async () => {
const manager = createManagerMock();
const ctx = createCtxMock({
nonInteractive: true,
});
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
const setupProvisioningProfileAction = new SetupProvisioningProfile(appLookupParams);
await expect(setupProvisioningProfileAction.runAsync(manager, ctx)).rejects.toThrowError(
MissingCredentialsNonInteractiveError
);
// expect build credentials not to be created or updated on expo servers
expect((ctx.newIos.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(0);
// expect provisioning profile not to be deleted on expo servers
expect((ctx.newIos.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(0);
});
});
@@ -2,6 +2,7 @@ import { PlistArray, PlistObject } from '@expo/plist';
import crypto from 'crypto';
import minimatch from 'minimatch';
import Log from '../../../log';
import { Context } from '../../context';
import { DistributionCertificate, ProvisioningProfile } from '../appstore/Credentials.types';
import { getP12CertFingerprint } from '../utils/p12Certificate';
@@ -14,8 +15,8 @@ interface ValidationResult {
export async function validateProvisioningProfileAsync(
ctx: Context,
profile: ProvisioningProfile,
distCert: DistributionCertificate,
profile: Pick<ProvisioningProfile, 'provisioningProfile' | 'provisioningProfileId'>,
distCert: Pick<DistributionCertificate, 'certP12' | 'certPassword'>,
bundleIdentifier: string
): Promise<ValidationResult> {
const resultWithoutApple = validateProvisioningProfileWithoutApple(
@@ -23,21 +24,33 @@ export async function validateProvisioningProfileAsync(
distCert,
bundleIdentifier
);
// already failed, return early
if (!resultWithoutApple.ok) {
return resultWithoutApple;
}
if (!ctx.appStore.authCtx) {
Log.warn(
"Skipping Provisioning Profile validation on Apple Servers because we aren't authenticated."
);
return resultWithoutApple;
}
return await validateProvisioningProfileWithAppleAsync(ctx, profile, bundleIdentifier);
}
async function validateProvisioningProfileWithAppleAsync(
ctx: Context,
profile: ProvisioningProfile,
profile: Pick<ProvisioningProfile, 'provisioningProfile' | 'provisioningProfileId'>,
bundleIdentifier: string
): Promise<ValidationResult> {
const profilesFromApple = await ctx.appStore.listProvisioningProfilesAsync(bundleIdentifier);
const configuredProfileFromApple = profilesFromApple.find(
appleProfile => appleProfile.provisioningProfileId === profile.provisioningProfileId
const configuredProfileFromApple = profilesFromApple.find(appleProfile =>
profile.provisioningProfileId
? appleProfile.provisioningProfileId === profile.provisioningProfileId
: appleProfile.provisioningProfile === profile.provisioningProfile
);
if (!configuredProfileFromApple) {
return {
@@ -49,8 +62,8 @@ async function validateProvisioningProfileWithAppleAsync(
}
export function validateProvisioningProfileWithoutApple(
provisioningProfile: ProvisioningProfile,
distCert: DistributionCertificate,
provisioningProfile: Pick<ProvisioningProfile, 'provisioningProfile' | 'provisioningProfileId'>,
distCert: Pick<DistributionCertificate, 'certP12' | 'certPassword'>,
bundleIdentifier: string
): ValidationResult {
try {
+18
View File
@@ -1,11 +1,16 @@
import {
CombinedError as GraphqlError,
OperationResult,
cacheExchange,
createClient as createUrqlClient,
dedupExchange,
fetchExchange,
} from '@urql/core';
import { retryExchange } from '@urql/exchange-retry';
import fetch from 'node-fetch';
import { getExpoApiBaseUrl } from '../api';
import Log from '../log';
import { getAccessToken, getSessionSecret } from '../user/sessionStorage';
type AccessTokenHeaders = {
@@ -18,6 +23,16 @@ type SessionHeaders = {
export const graphqlClient = createUrqlClient({
url: getExpoApiBaseUrl() + '/--/graphql',
exchanges: [
dedupExchange,
cacheExchange,
retryExchange({
maxDelayMs: 4000,
retryIf: err =>
!!(err && (err.networkError || err.graphQLErrors.some(e => e?.extensions?.isTransient))),
}),
fetchExchange,
],
// @ts-expect-error Type 'typeof fetch' is not assignable to type '(input: RequestInfo, init?: RequestInit | undefined) => Promise<Response>'.
fetch,
fetchOptions: (): { headers?: AccessTokenHeaders | SessionHeaders } => {
@@ -45,6 +60,9 @@ export async function withErrorHandlingAsync<T>(promise: Promise<OperationResult
const { data, error } = await promise;
if (error) {
if (error.graphQLErrors.some(e => e?.extensions?.isTransient)) {
Log.error(`We've encountered a transient error, please try again shortly.`);
}
throw error;
}
+328 -165
View File
@@ -89,6 +89,8 @@ export type RootQuery = {
* query object
*/
meActor?: Maybe<Actor>;
/** Top-level query object for querying Webhooks. */
webhook: WebhookQuery;
};
@@ -161,8 +163,8 @@ export type ActivityTimelineProjectActivity = {
export type Actor = {
id: Scalars['ID'];
firstName?: Maybe<Scalars['String']>;
created?: Maybe<Scalars['DateTime']>;
isExpoAdmin?: Maybe<Scalars['Boolean']>;
created: Scalars['DateTime'];
isExpoAdmin: Scalars['Boolean'];
/** Associated accounts */
accounts: Array<Account>;
/** Access Tokens belonging to this actor */
@@ -189,19 +191,12 @@ export type Account = {
__typename?: 'Account';
id: Scalars['ID'];
name: Scalars['String'];
isCurrent?: Maybe<Scalars['Boolean']>;
/** @deprecated Build packs are no longer supported */
availableBuilds?: Maybe<Scalars['Int']>;
unlimitedBuilds?: Maybe<Scalars['Boolean']>;
/** @deprecated No longer needed */
subscriptionChangesPending?: Maybe<Scalars['Boolean']>;
/** @deprecated Build packs are no longer supported */
willAutoRenewBuilds?: Maybe<Scalars['Boolean']>;
pushSecurityEnabled?: Maybe<Scalars['Boolean']>;
isCurrent: Scalars['Boolean'];
pushSecurityEnabled: Scalars['Boolean'];
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
/** short lived property for test */
offers?: Maybe<Array<Maybe<Offer>>>;
/** Offers set on this account */
offers?: Maybe<Array<Offer>>;
/** Snacks associated with this account */
snacks: Array<Snack>;
/** Apps associated with this account */
@@ -219,13 +214,6 @@ export type Account = {
userInvitations: Array<UserInvitation>;
/** Billing information */
billing?: Maybe<Billing>;
/**
* Api tokens made for project
* @deprecated Legacy access tokens are deprecated
*/
accessTokens: Array<Maybe<AccessToken>>;
/** @deprecated Legacy access tokens are deprecated */
requiresAccessTokenForPushSecurity: Scalars['Boolean'];
/** iOS credentials for account */
appleTeams: Array<AppleTeam>;
appleAppIdentifiers: Array<AppleAppIdentifier>;
@@ -235,6 +223,18 @@ export type Account = {
appleDevices: Array<AppleDevice>;
/** Environment secrets for an account */
environmentSecrets: Array<EnvironmentSecret>;
/** @deprecated Legacy access tokens are deprecated */
accessTokens: Array<Maybe<AccessToken>>;
/** @deprecated Legacy access tokens are deprecated */
requiresAccessTokenForPushSecurity: Scalars['Boolean'];
/** @deprecated See isCurrent */
unlimitedBuilds: Scalars['Boolean'];
/** @deprecated Build packs are no longer supported */
availableBuilds?: Maybe<Scalars['Int']>;
/** @deprecated No longer needed */
subscriptionChangesPending?: Maybe<Scalars['Boolean']>;
/** @deprecated Build packs are no longer supported */
willAutoRenewBuilds?: Maybe<Scalars['Boolean']>;
};
@@ -382,12 +382,12 @@ export type Project = {
name: Scalars['String'];
fullName: Scalars['String'];
description: Scalars['String'];
/** @deprecated Field no longer supported */
iconUrl?: Maybe<Scalars['String']>;
slug: Scalars['String'];
username: Scalars['String'];
updated: Scalars['DateTime'];
published: Scalars['Boolean'];
username: Scalars['String'];
/** @deprecated Field no longer supported */
iconUrl?: Maybe<Scalars['String']>;
};
/** Represents an Exponent App (or Experience in legacy terms) */
@@ -397,52 +397,22 @@ export type App = Project & {
name: Scalars['String'];
fullName: Scalars['String'];
description: Scalars['String'];
slug: Scalars['String'];
updated: Scalars['DateTime'];
published: Scalars['Boolean'];
ownerAccount: Account;
githubUrl?: Maybe<Scalars['String']>;
playStoreUrl?: Maybe<Scalars['String']>;
appStoreUrl?: Maybe<Scalars['String']>;
/** @deprecated Field no longer supported */
iconUrl?: Maybe<Scalars['String']>;
icon?: Maybe<AppIcon>;
slug: Scalars['String'];
sdkVersion: Scalars['String'];
isDeprecated?: Maybe<Scalars['Boolean']>;
username: Scalars['String'];
updated: Scalars['DateTime'];
pushSecurityEnabled?: Maybe<Scalars['Boolean']>;
ownerAccount: Account;
/** @deprecated Use 'privacySetting' instead. */
privacy: Scalars['String'];
isDeprecated: Scalars['Boolean'];
privacySetting: AppPrivacy;
latestReleaseId: Scalars['ID'];
/** @deprecated 'likes' have been deprecated. */
isLikedByMe: Scalars['Boolean'];
published: Scalars['Boolean'];
/** @deprecated 'likes' have been deprecated. */
likeCount: Scalars['Int'];
/** @deprecated 'likes' have been deprecated. */
trendScore: Scalars['Float'];
/** @deprecated 'likes' have been deprecated. */
likedBy: Array<Maybe<User>>;
/** @deprecated Field no longer supported */
releases: Array<Maybe<AppRelease>>;
pushSecurityEnabled: Scalars['Boolean'];
/** (EAS Build) Builds associated with this app */
builds: Array<Build>;
buildJobs: Array<BuildJob>;
/** @deprecated Field no longer supported */
users?: Maybe<Array<Maybe<User>>>;
/** @deprecated Field no longer supported */
lastPublishedTime: Scalars['DateTime'];
/** @deprecated Field no longer supported */
packageUsername: Scalars['String'];
/** @deprecated Field no longer supported */
packageName: Scalars['String'];
/**
* Api tokens made for project
* @deprecated Legacy access tokens are deprecated
*/
accessTokens: Array<Maybe<AccessToken>>;
/** @deprecated Legacy access tokens are deprecated */
requiresAccessTokenForPushSecurity: Scalars['Boolean'];
/** iOS app credentials for the project */
iosAppCredentials: Array<IosAppCredentials>;
/** Android app credentials for the project */
@@ -459,21 +429,36 @@ export type App = Project & {
activityTimelineProjectActivities: Array<ActivityTimelineProjectActivity>;
/** Environment secrets for an app */
environmentSecrets: Array<EnvironmentSecret>;
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppLikedByArgs = {
offset?: Maybe<Scalars['Int']>;
limit?: Maybe<Scalars['Int']>;
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppReleasesArgs = {
platform: AppPlatform;
offset?: Maybe<Scalars['Int']>;
limit?: Maybe<Scalars['Int']>;
/** Webhooks for an app */
webhooks: Array<Webhook>;
/** @deprecated Use ownerAccount.name instead */
username: Scalars['String'];
/** @deprecated Field no longer supported */
iconUrl?: Maybe<Scalars['String']>;
/** @deprecated Use 'privacySetting' instead. */
privacy: Scalars['String'];
/** @deprecated Field no longer supported */
lastPublishedTime: Scalars['DateTime'];
/** @deprecated Field no longer supported */
packageUsername: Scalars['String'];
/** @deprecated Field no longer supported */
packageName: Scalars['String'];
/** @deprecated Legacy access tokens are deprecated */
accessTokens: Array<Maybe<AccessToken>>;
/** @deprecated Legacy access tokens are deprecated */
requiresAccessTokenForPushSecurity: Scalars['Boolean'];
/** @deprecated 'likes' have been deprecated. */
isLikedByMe: Scalars['Boolean'];
/** @deprecated 'likes' have been deprecated. */
likeCount: Scalars['Int'];
/** @deprecated 'likes' have been deprecated. */
trendScore: Scalars['Float'];
/** @deprecated 'likes' have been deprecated. */
likedBy: Array<Maybe<User>>;
/** @deprecated Field no longer supported */
users?: Maybe<Array<Maybe<User>>>;
/** @deprecated Field no longer supported */
releases: Array<Maybe<AppRelease>>;
};
@@ -544,6 +529,27 @@ export type AppEnvironmentSecretsArgs = {
filterNames?: Maybe<Array<Scalars['String']>>;
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppWebhooksArgs = {
filter?: Maybe<WebhookFilter>;
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppLikedByArgs = {
offset?: Maybe<Scalars['Int']>;
limit?: Maybe<Scalars['Int']>;
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppReleasesArgs = {
platform: AppPlatform;
offset?: Maybe<Scalars['Int']>;
limit?: Maybe<Scalars['Int']>;
};
export type AppIcon = {
__typename?: 'AppIcon';
url: Scalars['String'];
@@ -560,6 +566,46 @@ export enum AppPrivacy {
Hidden = 'HIDDEN'
}
export enum BuildStatus {
InQueue = 'IN_QUEUE',
InProgress = 'IN_PROGRESS',
Errored = 'ERRORED',
Finished = 'FINISHED',
Canceled = 'CANCELED'
}
export enum AppPlatform {
Ios = 'IOS',
Android = 'ANDROID'
}
/** Represents an EAS Build */
export type Build = ActivityTimelineProjectActivity & {
__typename?: 'Build';
id: Scalars['ID'];
actor?: Maybe<Actor>;
activityTimestamp: Scalars['DateTime'];
project: Project;
/** @deprecated User type is deprecated */
initiatingUser?: Maybe<User>;
initiatingActor?: Maybe<Actor>;
artifacts?: Maybe<BuildArtifacts>;
logFiles: Array<Scalars['String']>;
updatedAt?: Maybe<Scalars['DateTime']>;
createdAt?: Maybe<Scalars['DateTime']>;
status: BuildStatus;
expirationDate?: Maybe<Scalars['DateTime']>;
platform: AppPlatform;
appVersion?: Maybe<Scalars['String']>;
sdkVersion?: Maybe<Scalars['String']>;
releaseChannel?: Maybe<Scalars['String']>;
metrics?: Maybe<BuildMetrics>;
distribution?: Maybe<DistributionType>;
buildProfile?: Maybe<Scalars['String']>;
gitCommitHash?: Maybe<Scalars['String']>;
error?: Maybe<BuildError>;
};
/** Represents a human (not robot) actor. */
export type User = Actor & {
__typename?: 'User';
@@ -569,22 +615,17 @@ export type User = Actor & {
firstName?: Maybe<Scalars['String']>;
lastName?: Maybe<Scalars['String']>;
fullName?: Maybe<Scalars['String']>;
profilePhoto?: Maybe<Scalars['String']>;
created?: Maybe<Scalars['DateTime']>;
lastLogin?: Maybe<Scalars['DateTime']>;
lastPasswordReset?: Maybe<Scalars['DateTime']>;
profilePhoto: Scalars['String'];
created: Scalars['DateTime'];
industry?: Maybe<Scalars['String']>;
location?: Maybe<Scalars['String']>;
appCount?: Maybe<Scalars['Int']>;
appCount: Scalars['Int'];
githubUsername?: Maybe<Scalars['String']>;
twitterUsername?: Maybe<Scalars['String']>;
appetizeCode?: Maybe<Scalars['String']>;
emailVerified?: Maybe<Scalars['Boolean']>;
isOnboarded?: Maybe<Scalars['Boolean']>;
isLegacy?: Maybe<Scalars['Boolean']>;
wasLegacy?: Maybe<Scalars['Boolean']>;
isEmailUnsubscribed?: Maybe<Scalars['Boolean']>;
isExpoAdmin?: Maybe<Scalars['Boolean']>;
emailVerified: Scalars['Boolean'];
isEmailUnsubscribed: Scalars['Boolean'];
isExpoAdmin: Scalars['Boolean'];
isSecondFactorAuthenticationEnabled: Scalars['Boolean'];
/** Get all certified second factor authentication methods */
secondFactorDevices: Array<UserSecondFactorDevice>;
@@ -596,11 +637,6 @@ export type User = Actor & {
snacks: Array<Snack>;
/** Apps this user has published */
apps: Array<App>;
/**
* Likes this user has made
* @deprecated 'likes' have been deprecated.
*/
likes?: Maybe<Array<Maybe<App>>>;
/** Whether this user has any pending user invitations. Only resolves for the viewer. */
hasPendingUserInvitations: Scalars['Boolean'];
/** Pending UserInvitations for this user. Only resolves for the viewer. */
@@ -610,6 +646,18 @@ export type User = Actor & {
* Only resolves for the viewer.
*/
featureGates: Scalars['JSONObject'];
/** @deprecated Field no longer supported */
lastPasswordReset?: Maybe<Scalars['DateTime']>;
/** @deprecated Field no longer supported */
lastLogin?: Maybe<Scalars['DateTime']>;
/** @deprecated Field no longer supported */
isOnboarded?: Maybe<Scalars['Boolean']>;
/** @deprecated Field no longer supported */
isLegacy?: Maybe<Scalars['Boolean']>;
/** @deprecated Field no longer supported */
wasLegacy?: Maybe<Scalars['Boolean']>;
/** @deprecated 'likes' have been deprecated. */
likes?: Maybe<Array<Maybe<App>>>;
};
@@ -629,15 +677,15 @@ export type UserAppsArgs = {
/** Represents a human (not robot) actor. */
export type UserLikesArgs = {
offset: Scalars['Int'];
limit: Scalars['Int'];
export type UserFeatureGatesArgs = {
filter?: Maybe<Array<Scalars['String']>>;
};
/** Represents a human (not robot) actor. */
export type UserFeatureGatesArgs = {
filter?: Maybe<Array<Scalars['String']>>;
export type UserLikesArgs = {
offset: Scalars['Int'];
limit: Scalars['Int'];
};
/** A second factor device belonging to a User */
@@ -706,59 +754,6 @@ export enum Role {
}
export enum AppPlatform {
Ios = 'IOS',
Android = 'ANDROID'
}
export type AppRelease = {
__typename?: 'AppRelease';
id: Scalars['ID'];
hash?: Maybe<Scalars['String']>;
publishedTime?: Maybe<Scalars['DateTime']>;
publishingUsername?: Maybe<Scalars['String']>;
sdkVersion?: Maybe<Scalars['String']>;
version?: Maybe<Scalars['String']>;
s3Key?: Maybe<Scalars['String']>;
s3Url?: Maybe<Scalars['String']>;
manifest?: Maybe<Scalars['JSON']>;
};
export enum BuildStatus {
InQueue = 'IN_QUEUE',
InProgress = 'IN_PROGRESS',
Errored = 'ERRORED',
Finished = 'FINISHED',
Canceled = 'CANCELED'
}
/** Represents an EAS Build */
export type Build = ActivityTimelineProjectActivity & {
__typename?: 'Build';
id: Scalars['ID'];
actor?: Maybe<Actor>;
activityTimestamp: Scalars['DateTime'];
project: Project;
/** @deprecated User type is deprecated */
initiatingUser?: Maybe<User>;
initiatingActor?: Maybe<Actor>;
artifacts?: Maybe<BuildArtifacts>;
logFiles: Array<Scalars['String']>;
updatedAt?: Maybe<Scalars['DateTime']>;
createdAt?: Maybe<Scalars['DateTime']>;
status: BuildStatus;
expirationDate?: Maybe<Scalars['DateTime']>;
platform: AppPlatform;
appVersion?: Maybe<Scalars['String']>;
sdkVersion?: Maybe<Scalars['String']>;
releaseChannel?: Maybe<Scalars['String']>;
metrics?: Maybe<BuildMetrics>;
distribution?: Maybe<DistributionType>;
buildProfile?: Maybe<Scalars['String']>;
gitCommitHash?: Maybe<Scalars['String']>;
error?: Maybe<BuildError>;
};
export type BuildArtifacts = {
__typename?: 'BuildArtifacts';
buildUrl?: Maybe<Scalars['String']>;
@@ -806,6 +801,19 @@ export type BuildJob = ActivityTimelineProjectActivity & {
releaseChannel?: Maybe<Scalars['String']>;
};
export type AppRelease = {
__typename?: 'AppRelease';
id: Scalars['ID'];
hash?: Maybe<Scalars['String']>;
publishedTime?: Maybe<Scalars['DateTime']>;
publishingUsername?: Maybe<Scalars['String']>;
sdkVersion?: Maybe<Scalars['String']>;
version?: Maybe<Scalars['String']>;
s3Key?: Maybe<Scalars['String']>;
s3Url?: Maybe<Scalars['String']>;
manifest?: Maybe<Scalars['JSON']>;
};
export type BuildArtifact = {
__typename?: 'BuildArtifact';
url: Scalars['String'];
@@ -1099,6 +1107,24 @@ export type EnvironmentSecret = {
updatedAt: Scalars['DateTime'];
};
export type WebhookFilter = {
event?: Maybe<WebhookType>;
};
export enum WebhookType {
Build = 'BUILD'
}
export type Webhook = {
__typename?: 'Webhook';
id: Scalars['ID'];
appId: Scalars['ID'];
event: WebhookType;
url: Scalars['String'];
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
};
export type UserPermission = {
__typename?: 'UserPermission';
permissions: Array<Permission>;
@@ -1491,6 +1517,16 @@ export type UserQueryByUsernameArgs = {
username: Scalars['String'];
};
export type WebhookQuery = {
__typename?: 'WebhookQuery';
byId: Webhook;
};
export type WebhookQueryByIdArgs = {
id: Scalars['ID'];
};
export type RootMutation = {
__typename?: 'RootMutation';
/**
@@ -1549,6 +1585,8 @@ export type RootMutation = {
emailSubscription: EmailSubscriptionMutation;
/** Mutations that create and delete EnvironmentSecrets */
environmentSecret: EnvironmentSecretMutation;
/** Mutations that create, delete, update Webhooks */
webhook: WebhookMutation;
};
@@ -2419,8 +2457,8 @@ export type Robot = Actor & {
__typename?: 'Robot';
id: Scalars['ID'];
firstName?: Maybe<Scalars['String']>;
created?: Maybe<Scalars['DateTime']>;
isExpoAdmin?: Maybe<Scalars['Boolean']>;
created: Scalars['DateTime'];
isExpoAdmin: Scalars['Boolean'];
/** Associated accounts */
accounts: Array<Account>;
/** Access Tokens belonging to this actor */
@@ -2584,7 +2622,7 @@ export type UploadSession = {
export type UploadSessionCreateUploadSessionArgs = {
type?: Maybe<UploadSessionType>;
type: UploadSessionType;
};
export enum UploadSessionType {
@@ -2926,6 +2964,44 @@ export type DeleteEnvironmentSecretResult = {
id: Scalars['ID'];
};
export type WebhookMutation = {
__typename?: 'WebhookMutation';
/** Create a Webhook */
createWebhook: Webhook;
/** Update a Webhook */
updateWebhook: Webhook;
/** Delete a Webhook */
deleteWebhook: DeleteWebhookResult;
};
export type WebhookMutationCreateWebhookArgs = {
appId: Scalars['String'];
webhookInput: WebhookInput;
};
export type WebhookMutationUpdateWebhookArgs = {
webhookId: Scalars['ID'];
webhookInput: WebhookInput;
};
export type WebhookMutationDeleteWebhookArgs = {
webhookId: Scalars['ID'];
};
export type WebhookInput = {
url: Scalars['String'];
secret: Scalars['String'];
event: WebhookType;
};
export type DeleteWebhookResult = {
__typename?: 'DeleteWebhookResult';
id: Scalars['ID'];
};
export enum StandardOffer {
/** $29 USD per month, 30 day trial */
Default = 'DEFAULT',
@@ -4091,6 +4167,58 @@ export type CreateUploadSessionMutation = (
) }
);
export type CreateWebhookMutationVariables = Exact<{
appId: Scalars['String'];
webhookInput: WebhookInput;
}>;
export type CreateWebhookMutation = (
{ __typename?: 'RootMutation' }
& { webhook: (
{ __typename?: 'WebhookMutation' }
& { createWebhook: (
{ __typename?: 'Webhook' }
& Pick<Webhook, 'id'>
& WebhookFragment
) }
) }
);
export type UpdateWebhookMutationVariables = Exact<{
webhookId: Scalars['ID'];
webhookInput: WebhookInput;
}>;
export type UpdateWebhookMutation = (
{ __typename?: 'RootMutation' }
& { webhook: (
{ __typename?: 'WebhookMutation' }
& { updateWebhook: (
{ __typename?: 'Webhook' }
& Pick<Webhook, 'id'>
& WebhookFragment
) }
) }
);
export type DeleteWebhookMutationVariables = Exact<{
webhookId: Scalars['ID'];
}>;
export type DeleteWebhookMutation = (
{ __typename?: 'RootMutation' }
& { webhook: (
{ __typename?: 'WebhookMutation' }
& { deleteWebhook: (
{ __typename?: 'DeleteWebhookResult' }
& Pick<DeleteWebhookResult, 'id'>
) }
) }
);
export type BuildsByIdQueryVariables = Exact<{
buildId: Scalars['ID'];
}>;
@@ -4257,6 +4385,45 @@ export type CurrentUserQuery = (
)> }
);
export type WebhooksByAppIdQueryVariables = Exact<{
appId: Scalars['String'];
webhookFilter?: Maybe<WebhookFilter>;
}>;
export type WebhooksByAppIdQuery = (
{ __typename?: 'RootQuery' }
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
& Pick<App, 'id'>
& { webhooks: Array<(
{ __typename?: 'Webhook' }
& Pick<Webhook, 'id'>
& WebhookFragment
)> }
) }
)> }
);
export type WebhookByIdQueryVariables = Exact<{
webhookId: Scalars['ID'];
}>;
export type WebhookByIdQuery = (
{ __typename?: 'RootQuery' }
& { webhook: (
{ __typename?: 'WebhookQuery' }
& { byId: (
{ __typename?: 'Webhook' }
& Pick<Webhook, 'id'>
& WebhookFragment
) }
) }
);
export type AppFragment = (
{ __typename?: 'App' }
& Pick<App, 'id' | 'fullName' | 'slug'>
@@ -4295,6 +4462,11 @@ export type EnvironmentSecretFragment = (
& Pick<EnvironmentSecret, 'id' | 'name' | 'createdAt'>
);
export type WebhookFragment = (
{ __typename?: 'Webhook' }
& Pick<Webhook, 'id' | 'event' | 'url' | 'createdAt' | 'updatedAt'>
);
export type AppleAppIdentifierFragment = (
{ __typename?: 'AppleAppIdentifier' }
& Pick<AppleAppIdentifier, 'id' | 'bundleIdentifier'>
@@ -4342,7 +4514,7 @@ export type AppleDistributionCertificateFragment = (
export type AppleProvisioningProfileFragment = (
{ __typename?: 'AppleProvisioningProfile' }
& Pick<AppleProvisioningProfile, 'id' | 'expiration' | 'developerPortalIdentifier' | 'provisioningProfile'>
& Pick<AppleProvisioningProfile, 'id' | 'expiration' | 'developerPortalIdentifier' | 'provisioningProfile' | 'updatedAt' | 'status'>
& { appleTeam?: Maybe<(
{ __typename?: 'AppleTeam' }
& Pick<AppleTeam, 'id' | 'appleTeamIdentifier' | 'appleTeamName'>
@@ -4367,21 +4539,12 @@ export type IosAppBuildCredentialsFragment = (
& Pick<IosAppBuildCredentials, 'id' | 'iosDistributionType'>
& { distributionCertificate?: Maybe<(
{ __typename?: 'AppleDistributionCertificate' }
& Pick<AppleDistributionCertificate, 'id' | 'certificateP12' | 'certificatePassword' | 'serialNumber' | 'developerPortalIdentifier' | 'validityNotBefore' | 'validityNotAfter' | 'updatedAt'>
& { appleTeam?: Maybe<(
{ __typename?: 'AppleTeam' }
& Pick<AppleTeam, 'id' | 'appleTeamIdentifier' | 'appleTeamName'>
)> }
& Pick<AppleDistributionCertificate, 'id'>
& AppleDistributionCertificateFragment
)>, provisioningProfile?: Maybe<(
{ __typename?: 'AppleProvisioningProfile' }
& Pick<AppleProvisioningProfile, 'id' | 'expiration' | 'developerPortalIdentifier' | 'provisioningProfile' | 'updatedAt' | 'status'>
& { appleDevices: Array<(
{ __typename?: 'AppleDevice' }
& Pick<AppleDevice, 'id' | 'identifier' | 'name' | 'model' | 'deviceClass'>
)>, appleTeam?: Maybe<(
{ __typename?: 'AppleTeam' }
& Pick<AppleTeam, 'id' | 'appleTeamIdentifier' | 'appleTeamName'>
)> }
& Pick<AppleProvisioningProfile, 'id'>
& AppleProvisioningProfileFragment
)> }
);
@@ -0,0 +1,81 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import {
CreateWebhookMutation,
CreateWebhookMutationVariables,
DeleteWebhookMutation,
DeleteWebhookMutationVariables,
UpdateWebhookMutation,
UpdateWebhookMutationVariables,
WebhookFragment,
WebhookInput,
} from '../generated';
import { WebhookFragmentNode } from '../types/Webhook';
export const WebhookMutation = {
async createWebhookAsync(appId: string, webhookInput: WebhookInput): Promise<WebhookFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateWebhookMutation, CreateWebhookMutationVariables>(
gql`
mutation CreateWebhookMutation($appId: String!, $webhookInput: WebhookInput!) {
webhook {
createWebhook(appId: $appId, webhookInput: $webhookInput) {
id
...WebhookFragment
}
}
}
${print(WebhookFragmentNode)}
`,
{ appId, webhookInput }
)
.toPromise()
);
return data.webhook.createWebhook;
},
async updateWebhookAsync(
webhookId: string,
webhookInput: WebhookInput
): Promise<WebhookFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<UpdateWebhookMutation, UpdateWebhookMutationVariables>(
gql`
mutation UpdateWebhookMutation($webhookId: ID!, $webhookInput: WebhookInput!) {
webhook {
updateWebhook(webhookId: $webhookId, webhookInput: $webhookInput) {
id
...WebhookFragment
}
}
}
${print(WebhookFragmentNode)}
`,
{ webhookId, webhookInput }
)
.toPromise()
);
return data.webhook.updateWebhook;
},
async deleteWebhookAsync(webhookId: string): Promise<void> {
await withErrorHandlingAsync(
graphqlClient
.mutation<DeleteWebhookMutation, DeleteWebhookMutationVariables>(
gql`
mutation DeleteWebhookMutation($webhookId: ID!) {
webhook {
deleteWebhook(webhookId: $webhookId) {
id
}
}
}
`,
{ webhookId }
)
.toPromise()
);
},
};
@@ -1,6 +1,7 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { EnvironmentSecretScope } from '../../commands/secrets/create';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import {
EnvironmentSecretFragment,
@@ -9,6 +10,10 @@ import {
} from '../generated';
import { EnvironmentSecretFragmentNode } from '../types/EnvironmentSecret';
export type EnvironmentSecretWithScope = EnvironmentSecretFragment & {
scope: EnvironmentSecretScope;
};
export const EnvironmentSecretsQuery = {
async byAcccountNameAsync(accountName: string): Promise<EnvironmentSecretFragment[]> {
const data = await withErrorHandlingAsync(
@@ -60,4 +65,18 @@ export const EnvironmentSecretsQuery = {
return data.app?.byFullName.environmentSecrets ?? [];
},
async allAsync(
projectAccountName: string,
projectFullName: string
): Promise<EnvironmentSecretWithScope[]> {
const [accountSecrets, appSecrets] = await Promise.all([
this.byAcccountNameAsync(projectAccountName),
this.byAppFullNameAsync(projectFullName),
]);
return [
...appSecrets.map(s => ({ ...s, scope: EnvironmentSecretScope.PROJECT })),
...accountSecrets.map(s => ({ ...s, scope: EnvironmentSecretScope.ACCOUNT })),
];
},
};
@@ -0,0 +1,61 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import {
WebhookByIdQuery,
WebhookByIdQueryVariables,
WebhookFilter,
WebhookFragment,
WebhooksByAppIdQuery,
WebhooksByAppIdQueryVariables,
} from '../generated';
import { WebhookFragmentNode } from '../types/Webhook';
export const WebhookQuery = {
async byAppIdAsync(appId: string, webhookFilter?: WebhookFilter): Promise<WebhookFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<WebhooksByAppIdQuery, WebhooksByAppIdQueryVariables>(
gql`
query WebhooksByAppId($appId: String!, $webhookFilter: WebhookFilter) {
app {
byId(appId: $appId) {
id
webhooks(filter: $webhookFilter) {
id
...WebhookFragment
}
}
}
}
${print(WebhookFragmentNode)}
`,
{ appId, webhookFilter }
)
.toPromise()
);
return data.app?.byId.webhooks ?? [];
},
async byIdAsync(webhookId: string): Promise<WebhookFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<WebhookByIdQuery, WebhookByIdQueryVariables>(
gql`
query WebhookById($webhookId: ID!) {
webhook {
byId(id: $webhookId) {
id
...WebhookFragment
}
}
}
${print(WebhookFragmentNode)}
`,
{ webhookId }
)
.toPromise()
);
return data.webhook.byId;
},
};
@@ -0,0 +1,11 @@
import gql from 'graphql-tag';
export const WebhookFragmentNode = gql`
fragment WebhookFragment on Webhook {
id
event
url
createdAt
updatedAt
}
`;
@@ -6,6 +6,8 @@ export const AppleProvisioningProfileFragmentNode = gql`
expiration
developerPortalIdentifier
provisioningProfile
updatedAt
status
appleTeam {
id
appleTeamIdentifier
@@ -1,43 +1,22 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { AppleDistributionCertificateFragmentNode } from './AppleDistributionCertificate';
import { AppleProvisioningProfileFragmentNode } from './AppleProvisioningProfile';
export const IosAppBuildCredentialsFragmentNode = gql`
fragment IosAppBuildCredentialsFragment on IosAppBuildCredentials {
id
iosDistributionType
distributionCertificate {
id
certificateP12
certificatePassword
serialNumber
developerPortalIdentifier
validityNotBefore
validityNotAfter
updatedAt
appleTeam {
id
appleTeamIdentifier
appleTeamName
}
...AppleDistributionCertificateFragment
}
provisioningProfile {
id
expiration
developerPortalIdentifier
provisioningProfile
updatedAt
status
appleDevices {
id
identifier
name
model
deviceClass
}
appleTeam {
id
appleTeamIdentifier
appleTeamName
}
...AppleProvisioningProfileFragment
}
}
${print(AppleDistributionCertificateFragmentNode)}
${print(AppleProvisioningProfileFragmentNode)}
`;
@@ -4,6 +4,8 @@ import { SubmissionError } from '../SubmissionService.types';
enum SubmissionErrorCode {
ARCHIVE_DOWNLOAD_NOT_FOUND_ERROR = 'SUBMISSION_SERVICE_COMMON_ARCHIVE_DOWNLOAD_NOT_FOUND_ERROR',
ARCHIVE_DOWNLOAD_FORBIDDEN_ERROR = 'SUBMISSION_SERVICE_COMMON_ARCHIVE_DOWNLOAD_FORBIDDEN_ERROR',
ARCHIVE_EXTRACT_NO_FILES_FOUND_ERROR = 'SUBMISSION_SERVICE_COMMON_ARCHIVE_EXTRACT_NO_FILES_FOUND_ERROR',
ARCHIVE_EXTRACT_CORRUPT_ARCHIVE_ERROR = 'SUBMISSION_SERVICE_COMMON_ARCHIVE_EXTRACT_CORRUPT_ARCHIVE_ERROR',
ANDROID_UNKNOWN_ERROR = 'SUBMISSION_SERVICE_ANDROID_UNKNOWN_ERROR',
ANDROID_FIRST_UPLOAD_ERROR = 'SUBMISSION_SERVICE_ANDROID_FIRST_UPLOAD_ERROR',
ANDROID_OLD_VERSION_CODE_ERROR = 'SUBMISSION_SERVICE_ANDROID_OLD_VERSION_CODE_ERROR',
@@ -12,6 +14,7 @@ enum SubmissionErrorCode {
IOS_UNKNOWN_ERROR = 'SUBMISSION_SERVICE_IOS_UNKNOWN_ERROR',
IOS_MISSING_APP_ICON = 'SUBMISSION_SERVICE_IOS_MISSING_APP_ICON',
IOS_INVALID_SIGNATURE = 'SUBMISSION_SERVICE_IOS_INVALID_SIGNATURE',
IOS_INCORRECT_CREDENTIALS = 'SUBMISSION_SERVICE_IOS_INVALID_CREDENTIALS',
}
const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
@@ -19,6 +22,11 @@ const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
"Failed to download the archive file (Response code: 404 Not Found). Please make sure the URL you've provided is correct.",
[SubmissionErrorCode.ARCHIVE_DOWNLOAD_FORBIDDEN_ERROR]:
'Failed to download the archive file (Response code: 403 Forbidden). This is most probably caused by trying to upload an expired build artifact. All Expo build artifacts expire after 30 days.',
[SubmissionErrorCode.ARCHIVE_EXTRACT_CORRUPT_ARCHIVE_ERROR]:
'The compressed archive is corrupt, in an unsupported format, or contains an invalid application format. Supported files include .apk, .aab, and .ipa files and one of these files compressed into a .tar.gz archive.',
[SubmissionErrorCode.ARCHIVE_EXTRACT_NO_FILES_FOUND_ERROR]:
"EAS Submit couldn't find a valid build artifact within provided compressed archive.\n" +
'If you provide a tar.gz archive, it should contain at least one .apk/.aab/.ipa file, depending on the submission platform.',
[SubmissionErrorCode.ANDROID_UNKNOWN_ERROR]:
"We couldn't figure out what went wrong. Please see logs to learn more.",
[SubmissionErrorCode.ANDROID_FIRST_UPLOAD_ERROR]:
@@ -47,6 +55,8 @@ const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
'Your app signature seems to be invalid.\n' +
"Please check your iOS Distribution Certificate and your app's Provisioning Profile.\n" +
`${learnMore('https://docs.expo.io/distribution/app-signing')}`,
[SubmissionErrorCode.IOS_INCORRECT_CREDENTIALS]:
'Your Apple ID or app-specific password is incorrect. Please verify that you entered them correctly and try again.',
};
export function printSubmissionError(error: SubmissionError): boolean {
@@ -0,0 +1,12 @@
import { WebhookFragment } from '../graphql/generated';
import formatFields from '../utils/formatFields';
export function formatWebhook(webhook: WebhookFragment): string {
return formatFields([
{ label: 'ID', value: webhook.id },
{ label: 'Event', value: webhook.event },
{ label: 'URL', value: webhook.url },
{ label: 'Created at', value: new Date(webhook.createdAt).toLocaleString() },
{ label: 'Updated at', value: new Date(webhook.updatedAt).toLocaleString() },
]);
}
+71
View File
@@ -0,0 +1,71 @@
import nullthrows from 'nullthrows';
import { URL } from 'url';
import { WebhookFragment, WebhookInput, WebhookType } from '../graphql/generated';
import Log from '../log';
import { promptAsync } from '../prompts';
export async function prepareInputParamsAsync(
{
event,
url: maybeUrl,
secret: maybeSecret,
}: {
event: WebhookType;
url?: string;
secret?: string;
},
existingWebhook?: WebhookFragment
): Promise<WebhookInput> {
let url: string | undefined = maybeUrl;
let secret: string | undefined = maybeSecret;
if (!url || !validateURL(url)) {
const urlValidationMessage =
'The provided webhook URL is invalid and must be an absolute URL, including a scheme.';
if (url && !validateURL(url)) {
Log.error(urlValidationMessage);
}
({ url } = await promptAsync({
type: 'text',
name: 'url',
message: 'Webhook URL:',
initial: url ? undefined : existingWebhook?.url,
validate: (value: string) => validateURL(value) || urlValidationMessage,
}));
}
if (!secret || !validateSecret(secret)) {
const secretValidationMessage =
'Webhook secret has be at least 16 and not more than 1000 characters long';
if (secret && !validateSecret(secret)) {
Log.error(secretValidationMessage);
}
({ secret } = await promptAsync({
type: 'text',
name: 'secret',
message: 'Webhook secret:',
validate: (value: string) => validateSecret(value) || secretValidationMessage,
}));
}
return {
event,
url: nullthrows(url),
secret: nullthrows(secret),
};
}
export function validateURL(url: string): boolean {
try {
// eslint-disable-next-line no-new
new URL(url);
return true;
} catch (err) {
return false;
}
}
export function validateSecret(secret: string): boolean {
return secret.length >= 16 && secret.length <= 1000;
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with the eas.json",
"version": "0.8.0",
"version": "0.9.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
+56 -53
View File
@@ -1125,24 +1125,24 @@
"@babel/preset-env" "^7.4.4"
"@babel/preset-typescript" "^7.3.3"
"@expo/[email protected]1-alpha.0":
version "1.0.21-alpha.0"
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-1.0.21-alpha.0.tgz#cc6d428809a8b1f483de1f4ca6b8219308bb3680"
integrity sha512-npi9sttSQOxl/NWc/FSlIy+ws9HgUoX3vsbu+rgTmD2ARtF5D8x5NR5upnWWpwoq6ZXaYXwDXNcKZHWLGrCfjA==
"@expo/[email protected]4":
version "1.0.24"
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-1.0.24.tgz#d04353500ca4ce6f6eb5f7058ac63e6f09e43486"
integrity sha512-DB4CgkeZEKG2DWC0GzbLSHuWlxWZ6d3wE95Cjy6DOyR60cFyxo+1eriaxLaBCApX3KrFAdxJFsxvwK9zbCtuzQ==
dependencies:
"@expo/config-types" "^40.0.0-beta.2"
"@expo/configure-splash-screen" "0.3.4"
"@expo/image-utils" "0.3.10"
"@expo/json-file" "8.2.28-alpha.0"
"@expo/plist" "0.0.11"
"@expo/image-utils" "0.3.12"
"@expo/json-file" "8.2.28"
"@expo/plist" "0.0.12"
find-up "~5.0.0"
fs-extra "9.0.0"
getenv "0.7.0"
getenv "^1.0.0"
glob "7.1.6"
resolve-from "^5.0.0"
slash "^3.0.0"
slugify "^1.3.4"
xcode "^2.1.0"
xcode "^3.0.1"
xml2js "^0.4.23"
"@expo/config-types@^40.0.0-beta.2":
@@ -1190,15 +1190,15 @@
dependencies:
"@hapi/joi" "^17.1.1"
"@expo/[email protected]0":
version "0.3.10"
resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.3.10.tgz#bdb9d6a2a7280680dd56d7f9dd283863b58d1e1e"
integrity sha512-EebukeUnzyk4ts1E1vMQSb0p8otYqWKsZNDZEoqHtERhxMSO7WhQLqa7/z2kB/YMHRJjrhaa3Aa2X5zjYot1kA==
"@expo/[email protected]2":
version "0.3.12"
resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.3.12.tgz#23a7dbf2cfe40fa9dd0c355fe7b4ca15ef83808e"
integrity sha512-QMMAjyCJUFXI8b6AOrVC40gubkt++qYfRJdKgbegf9JHQNul/O4Q1ENWTgid+PH0EUqlMLbv6kJ6ai0ZjO08kw==
dependencies:
"@expo/spawn-async" "1.5.0"
chalk "^4.0.0"
fs-extra "9.0.0"
getenv "0.7.0"
getenv "^1.0.0"
jimp "0.12.1"
mime "^2.4.4"
node-fetch "^2.6.0"
@@ -1218,10 +1218,10 @@
lodash "^4.17.15"
write-file-atomic "^2.3.0"
"@expo/[email protected]-alpha.0":
version "8.2.28-alpha.0"
resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.2.28-alpha.0.tgz#0e5280c82a17165be12c6a95cd95ec108c6dfe7b"
integrity sha512-cCQdw/Nfw8doXjN3onvUnWkuJjtVxx2iUjSOLMydvgI87YpW3x05uUXOVs4P+77YFVoFS6xbki+fmKK2JSCf8w==
"@expo/[email protected]":
version "8.2.28"
resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.2.28.tgz#9218bb70ad12d8be03c6376990b01fbeb4a15b72"
integrity sha512-C28McDuCIOmCmp6e2ZgIpNaT/fZG+L3/WFv5x2DCrooOQmamrkLSH/4Dib0NUmLgWVhgIhXL2hU4RB+LjzVvkQ==
dependencies:
"@babel/code-frame" "~7.10.4"
fs-extra "9.0.0"
@@ -1248,7 +1248,16 @@
qqjs "^0.3.10"
tslib "^1.9.3"
"@expo/[email protected]1", "@expo/plist@^0.0.11":
"@expo/[email protected]2":
version "0.0.12"
resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.0.12.tgz#59d95db7f8f3cd5427a210a0f442dc1616b270d7"
integrity sha512-anGvLk58fxfeHY2PbtH79VxhrLDPGd+173pHYYXNg9HlfbrEVLI2Vo0ZBOrr/cYr7cgU5A/WNcMphRoO/KfYZQ==
dependencies:
base64-js "^1.2.3"
xmlbuilder "^14.0.0"
xmldom "~0.5.0"
"@expo/plist@^0.0.11":
version "0.0.11"
resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.0.11.tgz#5d7900dc31df57d45a3524c061dde34aacc8dabf"
integrity sha512-yza93QHDkbdkdwu/PXef0eJSCMkMNdrHujK5G1viZLaZt0Rxw2s+geTyjgJsYpwqQEAoOYVpKlVymOenK+bFQg==
@@ -1352,24 +1361,7 @@
"@graphql-codegen/plugin-helpers" "^1.18.2"
tslib "~2.0.1"
"@graphql-codegen/plugin-helpers@^1.17.8", "@graphql-codegen/plugin-helpers@^1.17.9", "@graphql-codegen/plugin-helpers@^1.18.1":
version "1.18.1"
resolved "https://registry.yarnpkg.com/@graphql-codegen/plugin-helpers/-/plugin-helpers-1.18.1.tgz#e89833cfc4b3359ed3616626e590a89077ed0da6"
integrity sha512-jURyHDy8sUTUtRqF6U+akkPANmOYxJ7P47WExAknludiZCMOuoC6QUTxaHZcWNLCWsJFnKNta+oGHqXgkyA+VQ==
dependencies:
"@graphql-tools/utils" "^6"
camel-case "4.1.1"
common-tags "1.8.0"
constant-case "3.0.3"
import-from "3.0.0"
lodash "~4.17.20"
lower-case "2.0.1"
param-case "3.0.3"
pascal-case "3.1.1"
tslib "~2.0.1"
upper-case "2.0.1"
"@graphql-codegen/plugin-helpers@^1.18.2":
"@graphql-codegen/plugin-helpers@^1.17.8", "@graphql-codegen/plugin-helpers@^1.17.9", "@graphql-codegen/plugin-helpers@^1.18.1", "@graphql-codegen/plugin-helpers@^1.18.2":
version "1.18.2"
resolved "https://registry.yarnpkg.com/@graphql-codegen/plugin-helpers/-/plugin-helpers-1.18.2.tgz#57011076cb8b8f5d04d37d226a5eda300c01be94"
integrity sha512-SvX+Ryq2naLcoD6jJNxtzc/moWTgHJ+X0KRfvhGWTa+xtFTS02i+PWOR89YYPcD8+LF6GmyFBjx2FmLCx4JwMg==
@@ -1640,6 +1632,11 @@
is-promise "4.0.0"
tslib "~2.0.1"
"@graphql-typed-document-node/core@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.0.tgz#0eee6373e11418bfe0b5638f654df7a4ca6a3950"
integrity sha512-wYn6r8zVZyQJ6rQaALBEln5B1pzxb9shV5Ef97kTvn6yVGrqyXVnDqnU24MXnFubR+rZjBY9NWuxX3FB2sTsjg==
"@hapi/address@^4.0.1":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-4.1.0.tgz#d60c5c0d930e77456fdcde2598e77302e2955e1d"
@@ -3539,11 +3536,20 @@
dependencies:
eslint-visitor-keys "^1.1.0"
"@urql/core@^1.13.0":
version "1.13.0"
resolved "https://registry.yarnpkg.com/@urql/core/-/core-1.13.0.tgz#811154b5951b4282dd698a8c9dff4ff3825ca215"
integrity sha512-g7Kn2a0TDQotOqvGR7bRC+JqviA4TYtiDZH5M9Roszx7LiqZBkViSr5zmdwlNZjR4kSDn+kSXlCV+Ht0Uqls0Q==
"@urql/core@>=1.15.0", "@urql/core@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@urql/core/-/core-2.0.0.tgz#b4936dd51fe84cb570506d9ee2579149689f7535"
integrity sha512-Qj24CG8ullqZZsYmjrSH0JhH+nY7kj8GbVbA9si3KUjlYs75A/MBQU3i97j6oWyGldDBapyis2CfaQeXKbv8rA==
dependencies:
"@graphql-typed-document-node/core" "^3.1.0"
wonka "^4.0.14"
"@urql/exchange-retry@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@urql/exchange-retry/-/exchange-retry-0.2.0.tgz#e90a99bf8280ad2b8926bea7f2157a6f59dc8aec"
integrity sha512-eawDIkTSVudv+zMaOlm898UX9lkJnUry2PYqD7INeFWghkHmlIPm6wg5J/GBGAyFjqaOj1OWgAWYcu7sV4eljg==
dependencies:
"@urql/core" ">=1.15.0"
wonka "^4.0.14"
"@vercel/ncc@^0.23.0":
@@ -10771,7 +10777,7 @@ [email protected]:
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
[email protected], semver@^7.1.3, semver@^7.3.2:
[email protected]:
version "7.3.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
@@ -10781,7 +10787,7 @@ semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.3.4:
semver@^7.1.3, semver@^7.3.2, semver@^7.3.4:
version "7.3.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97"
integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==
@@ -10862,7 +10868,7 @@ signedsource@^1.0.0:
resolved "https://registry.yarnpkg.com/signedsource/-/signedsource-1.0.0.tgz#1ddace4981798f93bd833973803d80d52e93ad6a"
integrity sha1-HdrOSYF5j5O9gzlzgD2A1S6TrWo=
simple-plist@^1.0.0, simple-plist@^1.1.0:
simple-plist@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/simple-plist/-/simple-plist-1.1.1.tgz#54367ca28bc5996a982c325c1c4a4c1a05f4047c"
integrity sha512-pKMCVKvZbZTsqYR6RKgLfBHkh2cV89GXcA/0CVPje3sOiNOnXA8+rp/ciAMZ7JRaUdLzlEM6JFfUn+fS6Nt3hg==
@@ -12386,15 +12392,7 @@ ws@^7.2.3:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==
xcode@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/xcode/-/xcode-2.1.0.tgz#bab64a7e954bb50ca8d19da7e09531c65a43ecfe"
integrity sha512-uCrmPITrqTEzhn0TtT57fJaNaw8YJs1aCzs+P/QqxsDbvPZSv7XMPPwXrKvHtD6pLjBM/NaVwraWJm8q83Y4iQ==
dependencies:
simple-plist "^1.0.0"
uuid "^3.3.2"
xcode@^3.0.0:
xcode@^3.0.0, xcode@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/xcode/-/xcode-3.0.1.tgz#3efb62aac641ab2c702458f9a0302696146aa53c"
integrity sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==
@@ -12462,6 +12460,11 @@ [email protected], xmldom@~0.1.31:
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff"
integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==
xmldom@~0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.5.0.tgz#193cb96b84aa3486127ea6272c4596354cb4962e"
integrity sha512-Foaj5FXVzgn7xFzsKeNIde9g6aFBxTPi37iwsno8QvApmtg7KYrr+OPyRHcJF7dud2a5nGRBXK3n0dL62Gf7PA==
xtend@^4.0.0, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"