Compare commits

...
17 Commits
Author SHA1 Message Date
Dominik Sokal def91af01f v0.31.0 2021-10-08 14:26:59 +02:00
Dominik Sokal 5eb0fb75ac print platform emoji next to build details url when building for multiple platforms 2021-10-08 14:23:11 +02:00
Dominik Sokal de8c69f766 update CHANGELOG.md 2021-10-08 13:58:55 +02:00
Wojciech KozyraandDominik Sokal 5879ac424d [eas-cli] do not set default image (#675)
* [eas-cli] do not set default image

* upgrade @expo/eas-build-job

* remove redundant import

* update CHANGELOG

Co-authored-by: Dominik Sokal <[email protected]>
2021-10-08 13:56:57 +02:00
Dominik Sokal e30f62ec12 add option to review adhoc devices when reusing profile (#673)
* add option to review adhoc devices when reusing profile

* update CHANGELOG

* update snapshot

* address PR feedback
2021-10-08 10:46:37 +02:00
Bartłomiej Klocek dc4e36e8a8 [eas-cli] Improved service account display in submission summary (#674) 2021-10-08 09:17:46 +02:00
Quinlan Jung 12ded8c9a9 [eas-cli] integrated credentials service with android submissions (#664)
* [eas-cli] integrated credentials service with android submissions

* pr feedback

* tests

* changelog

* pr feedback

* dont print path in summary if it doesnt exist

* fix tests
2021-10-06 13:16:02 -07:00
Dominik Sokal 7378c6fd0e change CHANGELOG commit message 2021-10-06 16:23:02 +02:00
Expo CI 77a8b08834 [skip ci] update CHANGELOG.md 2021-10-06 14:15:53 +00:00
Dominik Sokal 79db9eb482 v0.30.1 2021-10-06 16:09:23 +02:00
Dominik Sokal 59f6d36114 don't use PAT for release 2021-10-06 16:07:24 +02:00
Dominik Sokal c36c26f5a9 fix building monorepo projects on Windows (#671)
* fix building monorepo projects on Windows

* update CHANGELOG

* address PR feedback
2021-10-06 15:34:42 +02:00
Dominik Sokal d6692f20b5 volta pin [email protected] and [email protected] 2021-10-06 09:50:33 +02:00
Brent Vatne 6e37f596df [eas-cli] Bump @expo/config @expo/prebuild-config @expo/config-plugins to fix iOS Info.plist warning (#670)
* [eas-cli] Bump @expo/config @expo/prebuild-config @expo/config-plugins

* Update CHANGELOG
2021-10-06 09:39:57 +02:00
Dominik Sokal d326891ef8 fix committing changelog changes from CI 2021-10-05 18:36:52 +02:00
Dominik Sokal 46a34327d9 fix --json flag when running on GitHub Actions (#669)
* fix --json flag when running on GitHub Actions

* update CHANGELOG
2021-10-05 15:38:34 +02:00
Dominik Sokal 97242af30f update CHANGELOG 2021-10-05 13:23:34 +02:00
59 changed files with 572 additions and 417 deletions

No files matched your search

+1 -1
View File
@@ -159,7 +159,7 @@ jobs:
git config --global user.email "[email protected]"
git config --global user.name "Expo CI"
git add CHANGELOG.md
git commit -m "[skip ci] update CHANGELOG.md"
git commit -m "update CHANGELOG.md"
git push
- name: Prepare changelog for Slack
run: |
+20 -1
View File
@@ -6,14 +6,33 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
- Enable auto-incrementing on Android. ([#645](https://github.com/expo/eas-cli/pull/645) by [@jkhales](https://github.com/jkhales))
- Don't resolve the iOS builder image on the client side. EAS Build will use the appropriate iOS image for a given Expo SDK version unless the `image` is defined. This only applies to managed projects. ([#675](https://github.com/expo/eas-cli/pull/675) by [@wkozyra95](https://github.com/wkozyra95) + [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Integrate credentials service with Android submissions. ([#664](https://github.com/expo/eas-cli/pull/664) by [@quinlanj](https://github.com/quinlanj))
- Add option to review ad-hoc devices when reusing provisioning profile. ([#673](https://github.com/expo/eas-cli/pull/673) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
### 🧹 Chores
## [0.30.1](https://github.com/expo/eas-cli/releases/tag/0.30.1) - 2021-10-06
### 🐛 Bug fixes
- Fix `--json` flag when running EAS CLI on GitHub actions. ([#669](https://github.com/expo/eas-cli/pull/669) by [@dsokal](https://github.com/dsokal))
- Fix `"ios: mods.ios.infoPlist: Failed to find Info.plist linked to Xcode project."` warning when running `eas build` in a managed project.([#670](https://github.com/expo/eas-cli/pull/670) by [@brentvatne](https://github.com/brentvatne))
- Fix building monorepo projects on Windows. ([#671](https://github.com/expo/eas-cli/pull/671) by [@dsokal](https://github.com/dsokal))
## [0.30.0](https://github.com/expo/eas-cli/releases/tag/v0.30.0) - 2021-10-05
### 🛠 Breaking changes
- Enable auto-incrementing on Android. ([#645](https://github.com/expo/eas-cli/pull/645) by [@jkhales](https://github.com/jkhales))
### 🧹 Chores
- Ports detection for Google Service Account Keys into the credentials service. ([#660](https://github.com/expo/eas-cli/pull/660) by [@quinlanj](https://github.com/quinlanj))
- Improve iOS credentials printing. ([#657](https://github.com/expo/eas-cli/pull/657) by [@quinlanj](https://github.com/quinlanj))
- Automate `eas-cli` releases. ([#668](https://github.com/expo/eas-cli/pull/668) by [@dsokal](https://github.com/dsokal))
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.30.0"
"version": "0.31.0"
}
+2 -2
View File
@@ -31,7 +31,7 @@
"typescript": "4.4.2"
},
"volta": {
"node": "16.8.0",
"yarn": "1.22.11"
"node": "16.10.0",
"yarn": "1.22.15"
}
}
+26 -26
View File
@@ -73,7 +73,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/account/login.js)_
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/account/login.js)_
## `eas account:logout`
@@ -87,7 +87,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/account/logout.js)_
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/account/logout.js)_
## `eas account:view`
@@ -101,7 +101,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/account/view.js)_
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/account/view.js)_
## `eas analytics [STATUS]`
@@ -112,7 +112,7 @@ USAGE
$ eas analytics [STATUS]
```
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/analytics.js)_
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/analytics.js)_
## `eas build`
@@ -145,7 +145,7 @@ OPTIONS
--[no-]wait Wait for build(s) to complete
```
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/build/index.js)_
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/build/index.js)_
## `eas build:cancel [BUILD_ID]`
@@ -156,7 +156,7 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/build/cancel.js)_
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/build/cancel.js)_
## `eas build:configure`
@@ -170,7 +170,7 @@ OPTIONS
-p, --platform=(android|ios|all) Platform to configure
```
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/build/configure.js)_
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/build/configure.js)_
## `eas build:list`
@@ -203,7 +203,7 @@ OPTIONS
--status=(new|in-queue|in-progress|errored|finished|canceled)
```
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/build/list.js)_
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/build/list.js)_
## `eas build:view [BUILD_ID]`
@@ -217,7 +217,7 @@ OPTIONS
--json Enable JSON output, non-JSON messages will be printed to stderr
```
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/build/view.js)_
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/build/view.js)_
## `eas config`
@@ -232,7 +232,7 @@ OPTIONS
--profile=profile
```
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/config.js)_
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/config.js)_
## `eas credentials`
@@ -243,7 +243,7 @@ USAGE
$ eas credentials
```
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/credentials.js)_
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/credentials.js)_
## `eas device:create`
@@ -254,7 +254,7 @@ USAGE
$ eas device:create
```
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/device/create.js)_
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/device/create.js)_
## `eas device:list`
@@ -268,7 +268,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/device/list.js)_
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/device/list.js)_
## `eas device:view [UDID]`
@@ -279,7 +279,7 @@ USAGE
$ eas device:view [UDID]
```
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/device/view.js)_
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/device/view.js)_
## `eas diagnostics`
@@ -290,7 +290,7 @@ USAGE
$ eas diagnostics
```
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/diagnostics.js)_
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/diagnostics.js)_
## `eas help [COMMAND]`
@@ -318,7 +318,7 @@ USAGE
$ eas project:info
```
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/project/info.js)_
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/project/info.js)_
## `eas project:init`
@@ -332,7 +332,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/project/init.js)_
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/project/init.js)_
## `eas secret:create`
@@ -349,7 +349,7 @@ OPTIONS
--value=value Value of the secret
```
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/secret/create.js)_
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/secret/create.js)_
## `eas secret:delete`
@@ -366,7 +366,7 @@ DESCRIPTION
Unsure where to find the secret's ID? Run eas secrets:list
```
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/secret/delete.js)_
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/secret/delete.js)_
## `eas secret:list`
@@ -377,7 +377,7 @@ USAGE
$ eas secret:list
```
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/secret/list.js)_
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/secret/list.js)_
## `eas submit`
@@ -410,7 +410,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/submit.js)_
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/submit.js)_
## `eas webhook:create`
@@ -429,7 +429,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/webhook/create.js)_
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/webhook/create.js)_
## `eas webhook:delete [ID]`
@@ -443,7 +443,7 @@ ARGUMENTS
ID ID of the webhook to delete
```
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/webhook/delete.js)_
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/webhook/delete.js)_
## `eas webhook:list`
@@ -457,7 +457,7 @@ OPTIONS
--event=(BUILD) Event type that triggers the webhook
```
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/webhook/list.js)_
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/webhook/list.js)_
## `eas webhook:update`
@@ -477,7 +477,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/webhook/update.js)_
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/webhook/update.js)_
## `eas webhook:view ID`
@@ -491,5 +491,5 @@ ARGUMENTS
ID ID of the webhook to view
```
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.30.0/packages/eas-cli/src/commands/webhook/view.js)_
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.31.0/packages/eas-cli/src/commands/webhook/view.js)_
<!-- commandsstop -->
+7 -6
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.30.0",
"version": "0.31.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -9,15 +9,15 @@
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.25",
"@expo/config": "5.0.9",
"@expo/config-plugins": "3.1.0",
"@expo/eas-build-job": "0.2.48",
"@expo/eas-json": "^0.30.0",
"@expo/config": "6.0.2",
"@expo/config-plugins": "4.0.2",
"@expo/eas-build-job": "0.2.52",
"@expo/eas-json": "^0.31.0",
"@expo/json-file": "8.2.33",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.14",
"@expo/plugin-warn-if-update-available": "1.7.0",
"@expo/prebuild-config": "2.1.0",
"@expo/prebuild-config": "3.0.2",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.0",
"@expo/sdk-runtime-versions": "1.0.0",
@@ -61,6 +61,7 @@
"qrcode-terminal": "0.12.0",
"resolve-from": "5.0.0",
"semver": "7.3.5",
"slash": "3.0.0",
"strip-ansi": "6.0.0",
"tar": "6.1.11",
"terminal-link": "2.1.1",
@@ -1,6 +1,7 @@
import { Android, ArchiveSource, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
import { AndroidBuildProfile } from '@expo/eas-json';
import path from 'path';
import slash from 'slash';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
import { getUsername } from '../../project/projectUtils';
@@ -25,7 +26,8 @@ export async function prepareJobAsync(
): Promise<Job> {
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildProfile: AndroidBuildProfile = ctx.buildProfile;
const projectRootDirectory = path.relative(await vcs.getRootPathAsync(), ctx.projectDir) || '.';
const projectRootDirectory =
slash(path.relative(await vcs.getRootPathAsync(), ctx.projectDir)) || '.';
const { credentials } = jobData;
const buildCredentials = credentials
? {
@@ -51,7 +53,7 @@ export async function prepareJobAsync(
projectRootDirectory,
projectArchive: jobData.projectArchive,
builderEnvironment: {
image: buildProfile.image ?? 'default',
image: buildProfile.image,
node: buildProfile.node,
yarn: buildProfile.yarn,
ndk: buildProfile.ndk,
+4 -14
View File
@@ -1,6 +1,6 @@
import { ArchiveSource, Ios, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
import path from 'path';
import semver from 'semver';
import slash from 'slash';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { getUsername } from '../../project/projectUtils';
@@ -24,7 +24,8 @@ export async function prepareJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData
): Promise<Job> {
const projectRootDirectory = path.relative(await vcs.getRootPathAsync(), ctx.projectDir) || '.';
const projectRootDirectory =
slash(path.relative(await vcs.getRootPathAsync(), ctx.projectDir)) || '.';
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
if (jobData.credentials) {
@@ -41,7 +42,7 @@ export async function prepareJobAsync(
projectRootDirectory,
distribution: ctx.buildProfile.simulator ? 'simulator' : ctx.buildProfile.distribution,
builderEnvironment: {
image: resolveImage(ctx),
image: ctx.buildProfile.image,
node: ctx.buildProfile.node,
yarn: ctx.buildProfile.yarn,
bundler: ctx.buildProfile.bundler,
@@ -73,17 +74,6 @@ export async function prepareJobAsync(
return sanitizeJob(job);
}
function resolveImage(ctx: BuildContext<Platform.IOS>): Ios.BuilderEnvironment['image'] {
// see https://linear.app/expo/issue/ENG-1396/make-default-image-dependent-on-sdk-version
if (!ctx.buildProfile.image && ctx.exp.sdkVersion) {
const majorSdkVersion = semver.major(ctx.exp.sdkVersion);
if (majorSdkVersion <= 41) {
return 'macos-catalina-10.15-xcode-12.4';
}
}
return ctx.buildProfile.image ?? 'default';
}
function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.TargetCredentials {
return {
provisioningProfileBase64: targetCredentials.provisioningProfile,
@@ -24,7 +24,9 @@ export function printLogsUrls(builds: BuildFragment[]): void {
builds.forEach(build => {
const logsUrl = getBuildLogsUrl(build);
Log.log(
`${appPlatformDisplayNames[build.platform]} build details: ${chalk.underline(logsUrl)}`
`${appPlatformEmojis[build.platform]} ${
appPlatformDisplayNames[build.platform]
} build details: ${chalk.underline(logsUrl)}`
);
});
}
+1 -1
View File
@@ -2,7 +2,7 @@ import { ExpoConfig, getPackageJson } from '@expo/config';
export function isExpoUpdatesInstalled(projectDir: string): boolean {
const packageJson = getPackageJson(projectDir);
return packageJson.dependencies && 'expo-updates' in packageJson.dependencies;
return !!(packageJson.dependencies && 'expo-updates' in packageJson.dependencies);
}
export function ensureValidVersions(exp: ExpoConfig): void {
@@ -75,11 +75,7 @@ export default class BranchCreate extends EasCommand {
flags,
} = this.parse(BranchCreate);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
@@ -98,10 +98,7 @@ export default class BranchDelete extends EasCommand {
flags: { json: jsonFlag },
} = this.parse(BranchDelete);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
+1 -4
View File
@@ -67,10 +67,7 @@ export default class BranchList extends EasCommand {
async runAsync(): Promise<void> {
const { flags } = this.parse(BranchList);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const branches = await listBranchesAsync({ projectId });
@@ -162,11 +162,7 @@ export default class BranchPublish extends EasCommand {
// If a group was specified, that means we are republishing it.
republish = group ? true : republish;
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, {
skipSDKVersionRequirement: true,
isPublicConfig: true,
@@ -74,10 +74,7 @@ export default class BranchRename extends EasCommand {
flags: { json: jsonFlag, from: currentName, to: newName },
} = this.parse(BranchRename);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
+1 -5
View File
@@ -90,11 +90,7 @@ export default class BranchView extends EasCommand {
flags: { json: jsonFlag },
} = this.parse(BranchView);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
@@ -117,7 +117,7 @@ export default class BuildCancel extends EasCommand {
async runAsync(): Promise<void> {
const { BUILD_ID: buildIdFromArg } = this.parse(BuildCancel).args;
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const projectFullName = await getProjectFullNameAsync(exp);
@@ -31,7 +31,7 @@ export default class BuildConfigure extends EasCommand {
await configureAsync({
platform,
projectDir: (await findProjectRootAsync()) ?? process.cwd(),
projectDir: await findProjectRootAsync(),
});
Log.newLine();
+1 -1
View File
@@ -143,7 +143,7 @@ export default class Build extends EasCommand {
const flags = await this.sanitizeFlagsAsync(rawFlags);
const { requestedPlatform } = flags;
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const projectDir = await findProjectRootAsync();
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await vcs.ensureRepoExistsAsync();
+1 -1
View File
@@ -76,7 +76,7 @@ export default class BuildList extends EasCommand {
const graphqlBuildStatus = toGraphQLBuildStatus(buildStatus);
const graphqlBuildDistribution = toGraphQLBuildDistribution(buildDistribution);
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const projectName = await getProjectFullNameAsync(exp);
+1 -1
View File
@@ -34,7 +34,7 @@ export default class BuildView extends EasCommand {
enableJsonOutput();
}
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const projectName = await getProjectFullNameAsync(exp);
@@ -84,10 +84,7 @@ export default class ChannelCreate extends EasCommand {
flags: { json: jsonFlag },
} = this.parse(ChannelCreate);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
@@ -121,10 +121,7 @@ export default class ChannelEdit extends EasCommand {
flags: { branch: branchName, json: jsonFlag },
} = this.parse(ChannelEdit);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
@@ -80,10 +80,7 @@ export default class ChannelList extends EasCommand {
flags: { json: jsonFlag },
} = this.parse(ChannelList);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
@@ -346,10 +346,7 @@ export default class ChannelRollout extends EasCommand {
flags: { branch: branchName, percent },
} = this.parse(ChannelRollout);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
@@ -215,10 +215,7 @@ export default class ChannelView extends EasCommand {
flags: { json: jsonFlag },
} = this.parse(ChannelView);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
+1 -1
View File
@@ -27,7 +27,7 @@ export default class Config extends EasCommand {
profile?: string;
};
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const projectDir = await findProjectRootAsync();
await handleDeprecatedEasJsonAsync(projectDir, false);
const reader = new EasJsonReader(projectDir);
+1 -1
View File
@@ -22,7 +22,7 @@ export default class BuildList extends EasCommand {
async runAsync(): Promise<void> {
let appleTeamIdentifier = this.parse(BuildList).flags['apple-team-id'];
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = await getProjectAccountNameAsync(exp);
+1 -1
View File
@@ -29,7 +29,7 @@ If you are not sure what is the UDID of the device you are looking for, run:
throw new Error('Device UDID is missing');
}
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = await getProjectAccountNameAsync(exp);
@@ -34,10 +34,7 @@ export default class ProjectInfo extends EasCommand {
static description = 'information about the current project';
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const { app } = await projectInfoByIdAsync(projectId);
@@ -10,10 +10,7 @@ export default class ProjectInit extends EasCommand {
static aliases = ['init'];
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
if (exp.extra?.eas?.projectId) {
@@ -50,7 +50,7 @@ export default class EnvironmentSecretCreate extends EasCommand {
flags: { name, value: secretValue, scope, force },
} = this.parse(EnvironmentSecretCreate);
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = await getProjectAccountNameAsync(exp);
@@ -32,7 +32,7 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
};
async runAsync(): Promise<void> {
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const projectAccountName = await getProjectAccountNameAsync(exp);
+1 -1
View File
@@ -20,7 +20,7 @@ export default class EnvironmentSecretList extends EasCommand {
static description = 'Lists environment secrets available for your current app';
async runAsync(): Promise<void> {
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const projectAccountName = await getProjectAccountNameAsync(exp);
+1 -1
View File
@@ -93,7 +93,7 @@ See how to configure submits with eas.json: ${learnMore('https://docs.expo.dev/s
const { flags: rawFlags } = this.parse(Submit);
const flags = await this.sanitizeFlagsAsync(rawFlags);
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
@@ -30,10 +30,7 @@ export default class WebhookCreate extends EasCommand {
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 projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
@@ -29,10 +29,7 @@ export default class WebhookDelete extends EasCommand {
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 projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
@@ -29,10 +29,7 @@ export default class WebhookList extends EasCommand {
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 projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const projectFullName = await getProjectFullNameAsync(exp);
@@ -22,6 +22,7 @@ describe(CreateGoogleServiceAccountKey, () => {
'/google-service-account-key.json': JSON.stringify({
type: 'service_account',
private_key: 'super secret',
client_email: '[email protected]',
}),
});
@@ -45,6 +45,7 @@ describe(SetupGoogleServiceAccountKey, () => {
'/google-service-account-key.json': JSON.stringify({
type: 'service_account',
private_key: 'super secret',
client_email: '[email protected]',
}),
});
const ctx = createCtxMock({
@@ -19,6 +19,8 @@ export type KeystoreWithType = Keystore & {
export type GoogleServiceAccountKey = {
[key: string]: any;
private_key: string;
type: string;
client_email: string;
};
export type AndroidCredentials = {
@@ -11,6 +11,7 @@ beforeAll(() => {
const mockDetectableServiceAccountJson = JSON.stringify({
type: 'service_account',
private_key: 'super secret',
client_email: '[email protected]',
});
vol.fromJSON({
@@ -13,6 +13,7 @@ import { GoogleServiceAccountKey } from '../credentials';
export const MinimalGoogleServiceAccountKeySchema = Joi.object({
type: Joi.string().required(),
private_key: Joi.string().required(),
client_email: Joi.string().required(),
});
function fileIsServiceAccountKey(keyJsonPath: string): boolean {
@@ -12,7 +12,7 @@ import {
IosDistributionType,
} from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync, pressAnyKeyToContinueAsync } from '../../../prompts';
import { confirmAsync, pressAnyKeyToContinueAsync, promptAsync } from '../../../prompts';
import differenceBy from '../../../utils/expodash/differenceBy';
import { CredentialsContext } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
@@ -23,6 +23,12 @@ import { assignBuildCredentialsAsync, getBuildCredentialsAsync } from './BuildCr
import { chooseDevicesAsync, formatDeviceLabel } from './DeviceUtils';
import { SetupDistributionCertificate } from './SetupDistributionCertificate';
enum ReuseAction {
Yes,
ShowDevices,
No,
}
export class SetupAdhocProvisioningProfile {
constructor(private app: AppLookupParams) {}
@@ -184,10 +190,23 @@ export class SetupAdhocProvisioningProfile {
);
if (allRegisteredDevicesAreProvisioned) {
return await confirmAsync({
message: `All your registered devices are present in the Provisioning Profile. Would you like to reuse it?`,
initial: true,
});
const reuseAction = await this.promptForReuseActionAsync();
if (reuseAction === ReuseAction.Yes) {
return true;
} else if (reuseAction === ReuseAction.No) {
return false;
} else {
Log.newLine();
Log.log('Devices registered in the Provisioning Profile:');
for (const device of provisionedDevices) {
Log.log(`- ${formatDeviceLabel(device)}`);
}
Log.newLine();
return (
(await this.promptForReuseActionAsync({ showShowDevicesOption: false })) ===
ReuseAction.Yes
);
}
} else {
const missingDevices = differenceBy(registeredAppleDevices, provisionedDevices, 'identifier');
Log.warn(`The provisioning profile is missing the following devices:`);
@@ -201,6 +220,36 @@ export class SetupAdhocProvisioningProfile {
}
}
private async promptForReuseActionAsync({
showShowDevicesOption = true,
} = {}): Promise<ReuseAction> {
const { selected } = await promptAsync({
type: 'select',
name: 'selected',
message: `${
showShowDevicesOption
? 'All your registered devices are present in the Provisioning Profile. '
: ''
}Would you like to reuse the profile?`,
choices: [
{ title: 'Yes', value: ReuseAction.Yes },
...(showShowDevicesOption
? [
{
title: 'Show devices and ask me again',
value: ReuseAction.ShowDevices,
},
]
: []),
{
title: 'No, let me choose devices again',
value: ReuseAction.No,
},
],
});
return selected;
}
private async registerDevicesAsync(
ctx: CredentialsContext,
appleTeam: AppleTeamFragment
@@ -12,7 +12,9 @@ Developer Portal ID 9839M6AY8W
Apple Team 77KQ969CHE
Updated 6 months ago
App Store Configuration
Distribution Certificate
Serial Number test-serial
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
@@ -26,7 +28,9 @@ Expiration Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 5 months ago
Ad Hoc Configuration
Distribution Certificate
Serial Number test-serial
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
@@ -128,11 +128,9 @@ export function displayProjectCredentials(
const isMultitarget = targets.length > 1;
Log.addNewLineIfNone();
const fields = [
{ label: 'Project Credentials Configuration', value: '' },
{ label: 'Project', value: projectFullName },
];
Log.log(chalk.cyan.bold('Project Credentials Configuration'));
Log.newLine();
const fields = [{ label: 'Project', value: projectFullName }];
for (const [targetName, buildCredentials] of Object.entries(appBuildCredentials)) {
if (isMultitarget) {
fields.push({ label: '', value: '' });
@@ -148,10 +146,12 @@ function displayIosAppBuildCredentials(
buildCredentials: IosAppBuildCredentialsFragment,
fields: { label: string; value: string }[]
): void {
fields.push({ label: '', value: '' });
fields.push({
label: `${prettyIosDistributionType(buildCredentials.iosDistributionType)} Configuration`,
value: '',
});
fields.push({ label: '', value: '' });
const maybeDistCert = buildCredentials.distributionCertificate;
fields.push({ label: 'Distribution Certificate', value: '' });
if (maybeDistCert) {
@@ -192,9 +192,13 @@ function displayIosAppBuildCredentials(
});
}
if (appleDevices && appleDevices.length > 0) {
fields.push({ label: 'Provisioned devices', value: '' });
for (const appleDevice of appleDevices) {
fields.push({ label: ' -', value: formatAppleDevice(appleDevice) });
const [firstAppleDevice, ...rest] = appleDevices;
fields.push({
label: 'Provisioned devices',
value: `- ${formatAppleDevice(firstAppleDevice)}`,
});
for (const appleDevice of rest) {
fields.push({ label: '', value: `- ${formatAppleDevice(appleDevice)}` });
}
}
fields.push({ label: 'Updated', value: `${fromNow(new Date(updatedAt))} ago` });
+1 -1
View File
@@ -20,7 +20,7 @@ export async function createContextAsync({
cwd?: string;
user: Actor;
}): Promise<DeviceManagerContext> {
const projectDir = await findProjectRootAsync(cwd);
const projectDir = await findProjectRootAsync({ cwd });
let exp: ExpoConfig | null = null;
if (projectDir) {
const config = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -26,15 +26,32 @@ describe(findProjectRootAsync, () => {
vol.reset();
});
it('returns null if not inside the project directory', async () => {
it('throws if not inside the project directory', async () => {
vol.fromJSON(
{
'./README.md': '1',
},
'/app'
);
const projectRoot = await findProjectRootAsync('/app');
expect(projectRoot).toBeNull();
await expect(findProjectRootAsync({ cwd: '/app' })).rejects.toThrow(
'Please run this command inside a project directory.'
);
});
it('defaults to process.cwd() if defaultToProcessCwd = true', async () => {
const spy = jest.spyOn(process, 'cwd').mockReturnValue('/this/is/fake/process/cwd');
try {
vol.fromJSON(
{
'./README.md': '1',
},
'/app'
);
const projectDir = await findProjectRootAsync({ cwd: '/app', defaultToProcessCwd: true });
expect(projectDir).toBe('/this/is/fake/process/cwd');
} finally {
spy.mockRestore();
}
});
it('returns the root directory of the project', async () => {
@@ -46,7 +63,7 @@ describe(findProjectRootAsync, () => {
},
'/app'
);
const projectRoot = await findProjectRootAsync('/app/src');
const projectRoot = await findProjectRootAsync({ cwd: '/app/src' });
expect(projectRoot).toBe('/app');
});
});
+17 -3
View File
@@ -52,9 +52,23 @@ export async function getProjectAccountNameAsync(exp: ExpoConfig): Promise<strin
return getProjectAccountName(exp, user);
}
export async function findProjectRootAsync(cwd?: string): Promise<string | null> {
export async function findProjectRootAsync({
cwd,
defaultToProcessCwd = false,
}: {
cwd?: string;
defaultToProcessCwd?: boolean;
} = {}): Promise<string> {
const projectRootDir = await pkgDir(cwd);
return projectRootDir ?? null;
if (!projectRootDir) {
if (!defaultToProcessCwd) {
throw new Error('Please run this command inside a project directory.');
} else {
return process.cwd();
}
} else {
return projectRootDir;
}
}
export async function setProjectIdAsync(
@@ -129,7 +143,7 @@ export async function getProjectIdAsync(
}
// Set the project ID if it is missing.
const projectDir = await findProjectRootAsync(process.cwd());
const projectDir = await findProjectRootAsync();
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
@@ -144,12 +144,9 @@ export default class AndroidSubmitCommand {
sourceType: ServiceAccountSourceType.path,
path: serviceAccountKeyPath,
});
} else if (this.ctx.nonInteractive) {
return result(new Error('Set serviceAccountKeyPath in the submit profile (eas.json).'));
} else {
return result({
sourceType: ServiceAccountSourceType.detect,
});
}
return result({
sourceType: ServiceAccountSourceType.credentialsService,
});
}
}
@@ -1,5 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import fs from 'fs-extra';
import chalk from 'chalk';
import {
AndroidSubmissionConfigInput,
@@ -8,6 +8,7 @@ import {
SubmissionFragment,
} from '../../graphql/generated';
import { SubmissionMutation } from '../../graphql/mutations/SubmissionMutation';
import formatFields from '../../utils/formatFields';
import { Archive, ArchiveSource, getArchiveAsync } from '../ArchiveSource';
import BaseSubmitter, { SubmissionInput } from '../BaseSubmitter';
import {
@@ -16,7 +17,11 @@ import {
printSummary,
} from '../utils/summary';
import { AndroidPackageSource, getAndroidPackageAsync } from './AndroidPackageSource';
import { ServiceAccountSource, getServiceAccountAsync } from './ServiceAccountSource';
import {
ServiceAccountKeyResult,
ServiceAccountSource,
getServiceAccountKeyResultAsync,
} from './ServiceAccountSource';
export interface AndroidSubmissionOptions
extends Pick<
@@ -32,7 +37,7 @@ export interface AndroidSubmissionOptions
interface ResolvedSourceOptions {
androidPackage: string;
archive: Archive;
serviceAccountPath: string;
serviceAccountKeyResult: ServiceAccountKeyResult;
}
export default class AndroidSubmitter extends BaseSubmitter<
@@ -73,19 +78,22 @@ export default class AndroidSubmitter extends BaseSubmitter<
private async resolveSourceOptionsAsync(): Promise<ResolvedSourceOptions> {
const androidPackage = await getAndroidPackageAsync(this.options.androidPackageSource);
const archive = await getArchiveAsync(this.options.archiveSource);
const serviceAccountPath = await getServiceAccountAsync(this.options.serviceAccountSource);
const serviceAccountKeyResult = await getServiceAccountKeyResultAsync(
this.ctx,
this.options.serviceAccountSource,
androidPackage
);
return {
androidPackage,
archive,
serviceAccountPath,
serviceAccountKeyResult,
};
}
private async formatSubmissionConfigAsync(
options: AndroidSubmissionOptions,
{ archive, androidPackage, serviceAccountPath }: ResolvedSourceOptions
{ archive, androidPackage, serviceAccountKeyResult }: ResolvedSourceOptions
): Promise<AndroidSubmissionConfigInput> {
const serviceAccount = await fs.readFile(serviceAccountPath, 'utf-8');
const { track, releaseStatus, changesNotSentForReview } = options;
return {
applicationIdentifier: androidPackage,
@@ -93,13 +101,13 @@ export default class AndroidSubmitter extends BaseSubmitter<
track,
changesNotSentForReview,
releaseStatus,
googleServiceAccountKeyJson: serviceAccount,
...serviceAccountKeyResult.result,
};
}
private prepareSummaryData(
options: AndroidSubmissionOptions,
{ archive, androidPackage, serviceAccountPath }: ResolvedSourceOptions
{ archive, androidPackage, serviceAccountKeyResult }: ResolvedSourceOptions
): SummaryData {
const { projectId, track, releaseStatus, changesNotSentForReview } = options;
@@ -110,7 +118,7 @@ export default class AndroidSubmitter extends BaseSubmitter<
track,
changesNotSentForReview: changesNotSentForReview ?? undefined,
releaseStatus: releaseStatus ?? undefined,
serviceAccountPath,
formattedServiceAccount: formatServiceAccountSummary(serviceAccountKeyResult),
...formatArchiveSourceSummary(archive),
};
}
@@ -119,9 +127,9 @@ export default class AndroidSubmitter extends BaseSubmitter<
type SummaryData = {
androidPackage: string;
changesNotSentForReview?: boolean;
formattedServiceAccount: string;
projectId: string;
releaseStatus?: SubmissionAndroidReleaseStatus;
serviceAccountPath: string;
track: SubmissionAndroidTrack;
} & ArchiveSourceSummaryFields;
@@ -131,8 +139,43 @@ const SummaryHumanReadableKeys: Record<keyof SummaryData, string> = {
archiveUrl: 'Download URL',
changesNotSentForReview: 'Changes not sent for a review',
formattedBuild: 'Build',
formattedServiceAccount: 'Google Service Account Key',
projectId: 'Project ID',
releaseStatus: 'Release status',
serviceAccountPath: 'Google Service Key',
track: 'Release track',
};
function formatServiceAccountSummary({ summary }: ServiceAccountKeyResult): string {
const {
email: serviceAccountEmail,
path: serviceAccountKeyPath,
source: serviceAccountKeySource,
} = summary;
const fields = [
{
label: 'Key Source',
value: serviceAccountKeySource,
},
{
label: 'Key Path',
value: serviceAccountKeyPath,
},
{
label: 'Account E-mail',
value: serviceAccountEmail,
},
];
const filteredFields = fields.filter(({ value }) => value !== undefined && value !== null) as {
label: string;
value: string;
}[];
return (
'\n' +
formatFields(filteredFields, {
labelFormat: label => ` ${chalk.dim(label)}:`,
})
);
}
@@ -1,20 +1,20 @@
import { Platform } from '@expo/eas-build-job';
import chalk from 'chalk';
import glob from 'fast-glob';
import fs from 'fs-extra';
import path from 'path';
import nullthrows from 'nullthrows';
import { SetupGoogleServiceAccountKey } from '../../credentials/android/actions/SetupGoogleServiceAccountKey';
import { readAndValidateServiceAccountKey } from '../../credentials/android/utils/googleServiceAccountKey';
import Log, { learnMore } from '../../log';
import { findProjectRootAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { filterAsync } from '../../utils/filterAsync';
import { findAccountByName } from '../../user/Account';
import { SubmissionContext } from '../context';
import { isExistingFileAsync } from '../utils/files';
export enum ServiceAccountSourceType {
path,
prompt,
detect,
// credentialsService,
// ...
credentialsService,
}
interface ServiceAccountSourceBase {
@@ -30,66 +30,116 @@ interface ServiceAccountPromptSource extends ServiceAccountSourceBase {
sourceType: ServiceAccountSourceType.prompt;
}
interface ServiceAccountDetectSource extends ServiceAccountSourceBase {
sourceType: ServiceAccountSourceType.detect;
export interface ServiceAccountCredentialsServiceSource extends ServiceAccountSourceBase {
sourceType: ServiceAccountSourceType.credentialsService;
}
export type ServiceAccountKeyResult = {
result: ServiceAccountKeyFile | ServiceAccountKeyFromExpoServers;
summary: ServiceAccountKeySummary;
};
type ServiceAccountKeySummary = {
source: 'local' | 'EAS servers';
path?: string;
email: string;
};
type ServiceAccountKeyFile = {
googleServiceAccountKeyJson: string;
};
type ServiceAccountKeyFromExpoServers = {
googleServiceAccountKeyId: string;
};
export type ServiceAccountSource =
| ServiceAccountPathSource
| ServiceAccountPromptSource
| ServiceAccountDetectSource;
| ServiceAccountCredentialsServiceSource;
export async function getServiceAccountAsync(source: ServiceAccountSource): Promise<string> {
export async function getServiceAccountKeyResultAsync(
ctx: SubmissionContext<Platform.ANDROID>,
source: ServiceAccountSource,
androidApplicationIdentifier: string
): Promise<ServiceAccountKeyResult> {
if (source.sourceType === ServiceAccountSourceType.credentialsService) {
return await getServiceAccountFromCredentialsServiceAsync(ctx, androidApplicationIdentifier);
} else {
return await getServiceAccountLocallyAsync(source);
}
}
async function getServiceAccountLocallyAsync(
source: ServiceAccountSource
): Promise<ServiceAccountKeyResult> {
const serviceAccountKeyPath = await getServiceAccountKeyPathAsync(source);
const serviceAccountKey = readAndValidateServiceAccountKey(serviceAccountKeyPath);
return {
result: { googleServiceAccountKeyJson: await fs.readFile(serviceAccountKeyPath, 'utf-8') },
summary: {
source: 'local',
path: serviceAccountKeyPath,
email: serviceAccountKey.client_email,
},
};
}
export async function getServiceAccountKeyPathAsync(source: ServiceAccountSource): Promise<string> {
switch (source.sourceType) {
case ServiceAccountSourceType.path:
return await handlePathSourceAsync(source);
case ServiceAccountSourceType.prompt:
return await handlePromptSourceAsync(source);
case ServiceAccountSourceType.detect:
return await handleDetectSourceAsync(source);
case ServiceAccountSourceType.credentialsService: {
throw new Error(`ServiceAccountSource ${source} does not return a path.`);
}
}
}
export async function getServiceAccountFromCredentialsServiceAsync(
ctx: SubmissionContext<Platform.ANDROID>,
androidApplicationIdentifier: string
): Promise<ServiceAccountKeyResult> {
const appLookupParams = {
account: nullthrows(
findAccountByName(ctx.user.accounts, ctx.accountName),
`You do not have access to account: ${ctx.accountName}`
),
projectName: ctx.projectName,
androidApplicationIdentifier,
};
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
const androidAppCredentials = await setupGoogleServiceAccountKeyAction.runAsync(
ctx.credentialsCtx
);
const googleServiceAccountKey = nullthrows(
androidAppCredentials.googleServiceAccountKeyForSubmissions,
'Credentials Service must provide a valid GoogleServiceAccountKey'
);
return {
result: {
googleServiceAccountKeyId: googleServiceAccountKey.id,
},
summary: {
source: 'EAS servers',
email: googleServiceAccountKey.clientEmail,
},
};
}
async function handlePathSourceAsync(source: ServiceAccountPathSource): Promise<string> {
if (!(await isExistingFileAsync(source.path))) {
Log.warn(`File ${source.path} doesn't exist.`);
return await getServiceAccountAsync({ sourceType: ServiceAccountSourceType.prompt });
return await getServiceAccountKeyPathAsync({ sourceType: ServiceAccountSourceType.prompt });
}
return source.path;
}
async function handleDetectSourceAsync(_source: ServiceAccountDetectSource): Promise<string> {
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const foundFilePaths = await glob('**/*.json', {
cwd: projectDir,
ignore: ['app.json', 'package*.json', 'tsconfig.json', 'node_modules'],
});
const googleServiceFiles = await filterAsync(
foundFilePaths.map(file => path.join(projectDir, file)),
fileIsGoogleServicesAsync
);
if (googleServiceFiles.length > 1) {
const selectedPath = await displayPathChooserAsync(googleServiceFiles, projectDir);
if (selectedPath !== false) {
return selectedPath;
}
} else if (googleServiceFiles.length === 1) {
const [detectedPath] = googleServiceFiles;
if (await confirmDetectedPathAsync(detectedPath)) {
return detectedPath;
}
}
return await getServiceAccountAsync({ sourceType: ServiceAccountSourceType.prompt });
}
async function handlePromptSourceAsync(_source: ServiceAccountPromptSource): Promise<string> {
const path = await askForServiceAccountPathAsync();
return await getServiceAccountAsync({
return await getServiceAccountKeyPathAsync({
sourceType: ServiceAccountSourceType.path,
path,
});
@@ -124,53 +174,3 @@ async function askForServiceAccountPathAsync(): Promise<string> {
});
return filePath;
}
async function displayPathChooserAsync(
paths: string[],
projectDir: string
): Promise<string | false> {
const choices = paths.map<{ title: string; value: string | false }>(f => ({
value: f,
title: f.startsWith(projectDir) ? path.relative(projectDir, f) : f,
}));
choices.push({
title: 'None of the above',
value: false,
});
Log.log(
'Multiple Google Service Account JSON keys have been found inside your project directory.'
);
const { selectedPath } = await promptAsync({
name: 'selectedPath',
type: 'select',
message: 'Choose the key you want to use for this submission:',
choices,
});
Log.addNewLineIfNone();
return selectedPath;
}
async function confirmDetectedPathAsync(path: string): Promise<boolean> {
Log.log(`A Google Service Account JSON key has been found at\n ${chalk.underline(path)}`);
const { confirmed } = await promptAsync({
name: 'confirmed',
type: 'confirm',
message: 'Would you like to use this file?',
initial: true,
});
Log.addNewLineIfNone();
return confirmed;
}
async function fileIsGoogleServicesAsync(path: string): Promise<boolean> {
try {
const jsonFile = await fs.readJson(path);
return jsonFile.type === 'service_account';
} catch (e) {
return false;
}
}
@@ -44,7 +44,11 @@ describe(AndroidSubmitCommand, () => {
const fakeFiles: Record<string, string> = {
'/apks/fake.apk': 'fake apk',
'/google-service-account.json': JSON.stringify({ service: 'account' }),
'/google-service-account.json': JSON.stringify({
type: 'service_account',
private_key: 'super secret',
client_email: '[email protected]',
}),
};
const fakeBuildFragment: Partial<BuildFragment> = {
@@ -1,39 +1,70 @@
import { Platform } from '@expo/eas-build-job';
import { AndroidReleaseStatus, AndroidReleaseTrack } from '@expo/eas-json';
import { vol } from 'memfs';
import { v4 as uuidv4 } from 'uuid';
import { asMock } from '../../../__tests__/utils';
import { findProjectRootAsync } from '../../../project/projectUtils';
import { testAndroidAppCredentialsFragment } from '../../../credentials/__tests__/fixtures-android';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { SetupGoogleServiceAccountKey } from '../../../credentials/android/actions/SetupGoogleServiceAccountKey';
import { createTestProject } from '../../../project/__tests__/project-utils';
import { promptAsync } from '../../../prompts';
import { createSubmissionContextAsync } from '../../context';
import {
ServiceAccountSource,
ServiceAccountSourceType,
getServiceAccountAsync,
getServiceAccountKeyPathAsync,
getServiceAccountKeyResultAsync,
} from '../ServiceAccountSource';
jest.mock('fs');
jest.mock('../../../prompts');
jest.mock('../../../project/projectUtils');
jest.mock('../../../credentials/android/actions/SetupGoogleServiceAccountKey');
jest.mock('../../../user/User', () => ({
getUserAsync: jest.fn(() => mockJester),
}));
jest.mock('../../../user/Account', () => ({
findAccountByName: jest.fn(() => mockJester.accounts[0]),
}));
const testProject = createTestProject(mockJester, {
android: {
package: 'com.expo.test.project',
},
});
const mockManifest = { exp: testProject.appJSON.expo };
jest.mock('@expo/config', () => ({
getConfig: jest.fn(() => mockManifest),
}));
describe(getServiceAccountAsync, () => {
beforeAll(() => {
const mockDetectableServiceAccountJson = JSON.stringify({
type: 'service_account',
});
const projectId = uuidv4();
const mockDetectableServiceAccountJson = JSON.stringify({
type: 'service_account',
private_key: 'super secret',
client_email: '[email protected]',
});
vol.fromJSON({
'/google-service-account.json': JSON.stringify({ service: 'account' }),
'/project_dir/subdir/service-account.json': mockDetectableServiceAccountJson,
'/project_dir/another-service-account.json': mockDetectableServiceAccountJson,
'/other_dir/invalid_file.txt': 'this is not even a JSON',
});
});
afterAll(() => {
vol.reset();
beforeAll(() => {
vol.fromJSON({
'/google-service-account.json': mockDetectableServiceAccountJson,
'/project_dir/subdir/service-account.json': mockDetectableServiceAccountJson,
'/project_dir/another-service-account.json': mockDetectableServiceAccountJson,
'/other_dir/invalid_file.txt': 'this is not even a JSON',
});
jest
.spyOn(SetupGoogleServiceAccountKey.prototype, 'runAsync')
.mockImplementation(async () => testAndroidAppCredentialsFragment);
});
afterAll(() => {
vol.reset();
});
afterEach(() => {
asMock(promptAsync).mockClear();
});
afterEach(() => {
asMock(promptAsync).mockClear();
jest.restoreAllMocks();
});
describe(getServiceAccountKeyPathAsync, () => {
describe('when source is ServiceAccountSourceType.path', () => {
it("prompts for path if the provided file doesn't exist", async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
@@ -43,7 +74,7 @@ describe(getServiceAccountAsync, () => {
sourceType: ServiceAccountSourceType.path,
path: '/doesnt-exist.json',
};
const serviceAccountPath = await getServiceAccountAsync(source);
const serviceAccountPath = await getServiceAccountKeyPathAsync(source);
expect(promptAsync).toHaveBeenCalled();
expect(serviceAccountPath).toBe('/google-service-account.json');
});
@@ -53,7 +84,7 @@ describe(getServiceAccountAsync, () => {
sourceType: ServiceAccountSourceType.path,
path: '/google-service-account.json',
};
await getServiceAccountAsync(source);
await getServiceAccountKeyPathAsync(source);
expect(promptAsync).not.toHaveBeenCalled();
});
@@ -62,7 +93,7 @@ describe(getServiceAccountAsync, () => {
sourceType: ServiceAccountSourceType.path,
path: '/google-service-account.json',
};
const serviceAccountPath = await getServiceAccountAsync(source);
const serviceAccountPath = await getServiceAccountKeyPathAsync(source);
expect(serviceAccountPath).toBe('/google-service-account.json');
});
});
@@ -75,7 +106,7 @@ describe(getServiceAccountAsync, () => {
const source: ServiceAccountSource = {
sourceType: ServiceAccountSourceType.prompt,
};
const serviceAccountPath = await getServiceAccountAsync(source);
const serviceAccountPath = await getServiceAccountKeyPathAsync(source);
expect(promptAsync).toHaveBeenCalled();
expect(serviceAccountPath).toBe('/google-service-account.json');
});
@@ -94,105 +125,105 @@ describe(getServiceAccountAsync, () => {
const source: ServiceAccountSource = {
sourceType: ServiceAccountSourceType.prompt,
};
const serviceAccountPath = await getServiceAccountAsync(source);
const serviceAccountPath = await getServiceAccountKeyPathAsync(source);
expect(promptAsync).toHaveBeenCalledTimes(3);
expect(serviceAccountPath).toBe('/google-service-account.json');
});
});
});
describe('when source is ServiceAccountSourceType.detect', () => {
it('detects a single google-services file and prompts for confirmation', async () => {
asMock(findProjectRootAsync).mockResolvedValueOnce('/project_dir/subdir'); // should find 1 file
asMock(promptAsync).mockResolvedValueOnce({
confirmed: true,
});
const serviceAccountPath = await getServiceAccountAsync({
sourceType: ServiceAccountSourceType.detect,
});
expect(promptAsync).toHaveBeenCalledWith(expect.objectContaining({ type: 'confirm' }));
expect(serviceAccountPath).toBe('/project_dir/subdir/service-account.json');
describe(getServiceAccountKeyResultAsync, () => {
it('returns a local Service Account Key file with a ServiceAccountSourceType.path source', async () => {
const ctx = await createSubmissionContextAsync({
platform: Platform.ANDROID,
projectDir: testProject.projectRoot,
projectId,
archiveFlags: {
url: 'http://expo.dev/fake.apk',
},
profile: {
track: AndroidReleaseTrack.internal,
releaseStatus: AndroidReleaseStatus.draft,
changesNotSentForReview: false,
},
nonInteractive: true,
});
it('falls back to prompt, when no valid files are found in the dir', async () => {
asMock(findProjectRootAsync).mockResolvedValueOnce('/other_dir'); // no valid files in that dir
asMock(promptAsync).mockResolvedValueOnce({
filePath: '/google-service-account.json',
});
const serviceAccountPath = await getServiceAccountAsync({
sourceType: ServiceAccountSourceType.detect,
});
expect(promptAsync).toHaveBeenCalledWith(expect.objectContaining({ type: 'text' }));
expect(serviceAccountPath).toBe('/google-service-account.json');
const source: ServiceAccountSource = {
sourceType: ServiceAccountSourceType.path,
path: '/project_dir/subdir/service-account.json',
};
const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, source, 'test.sdf');
expect(serviceAccountResult).toMatchObject({
result: {
googleServiceAccountKeyJson: mockDetectableServiceAccountJson,
},
summary: {
source: 'local',
path: '/project_dir/subdir/service-account.json',
email: '[email protected]serviceaccount.com',
},
});
});
it('falls back to prompt, when user rejects to use detected file', async () => {
asMock(findProjectRootAsync).mockResolvedValueOnce('/project_dir/subdir'); // should find 1 file
asMock(promptAsync)
.mockResolvedValueOnce({
confirmed: false,
})
.mockResolvedValueOnce({
filePath: '/google-service-account.json',
});
const source: ServiceAccountSource = {
sourceType: ServiceAccountSourceType.detect,
};
const serviceAccountPath = await getServiceAccountAsync(source);
expect(promptAsync).toHaveBeenCalledTimes(2);
expect(promptAsync).toHaveBeenCalledWith(expect.objectContaining({ type: 'confirm' }));
expect(promptAsync).toHaveBeenLastCalledWith(expect.objectContaining({ type: 'text' }));
expect(serviceAccountPath).toBe('/google-service-account.json');
it('returns a local Service Account Key file with a ServiceAccountSourceType.prompt source', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
filePath: '/project_dir/subdir/service-account.json',
}));
const ctx = await createSubmissionContextAsync({
platform: Platform.ANDROID,
projectDir: testProject.projectRoot,
projectId,
archiveFlags: {
url: 'http://expo.dev/fake.apk',
},
profile: {
track: AndroidReleaseTrack.internal,
releaseStatus: AndroidReleaseStatus.draft,
changesNotSentForReview: false,
},
nonInteractive: true,
});
it('displays a chooser, when multiple files are found', async () => {
asMock(findProjectRootAsync).mockResolvedValueOnce('/project_dir'); // should find 2 files here
asMock(promptAsync).mockResolvedValueOnce({
selectedPath: '/project_dir/another-service-account.json',
});
const serviceAccountPath = await getServiceAccountAsync({
sourceType: ServiceAccountSourceType.detect,
});
const expectedChoices = expect.arrayContaining([
expect.objectContaining({ value: '/project_dir/another-service-account.json' }),
expect.objectContaining({ value: '/project_dir/subdir/service-account.json' }),
expect.objectContaining({ value: false }),
]);
expect(promptAsync).toHaveBeenCalledWith(
expect.objectContaining({
type: 'select',
choices: expectedChoices,
})
);
expect(serviceAccountPath).toBe('/project_dir/another-service-account.json');
const source: ServiceAccountSource = {
sourceType: ServiceAccountSourceType.prompt,
};
const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, source, 'test.sdf');
expect(serviceAccountResult).toMatchObject({
result: {
googleServiceAccountKeyJson: mockDetectableServiceAccountJson,
},
summary: {
source: 'local',
path: '/project_dir/subdir/service-account.json',
email: '[email protected]',
},
});
});
it('falls back to prompt, when user selects the "None of above"', async () => {
asMock(findProjectRootAsync).mockResolvedValueOnce('/project_dir'); // should find 2 files here
asMock(promptAsync)
.mockResolvedValueOnce({
selectedPath: false,
})
.mockResolvedValueOnce({
filePath: '/google-service-account.json',
});
const serviceAccountPath = await getServiceAccountAsync({
sourceType: ServiceAccountSourceType.detect,
});
expect(promptAsync).toHaveBeenCalledTimes(2);
expect(promptAsync).toHaveBeenCalledWith(expect.objectContaining({ type: 'select' }));
expect(promptAsync).toHaveBeenLastCalledWith(expect.objectContaining({ type: 'text' }));
expect(serviceAccountPath).toBe('/google-service-account.json');
it('returns a remote Service Account Key file with a ServiceAccountSourceType.credentialService source', async () => {
const ctx = await createSubmissionContextAsync({
platform: Platform.ANDROID,
projectDir: testProject.projectRoot,
projectId,
archiveFlags: {
url: 'http://expo.dev/fake.apk',
},
profile: {
track: AndroidReleaseTrack.internal,
releaseStatus: AndroidReleaseStatus.draft,
changesNotSentForReview: false,
},
nonInteractive: true,
});
const serviceAccountResult = await getServiceAccountKeyResultAsync(
ctx,
{
sourceType: ServiceAccountSourceType.credentialsService,
},
'test.application.identiifer'
);
expect(serviceAccountResult).toMatchObject({
result: { googleServiceAccountKeyId: 'test-id' },
summary: { source: 'EAS servers', email: '[email protected]' },
});
});
});
+11 -1
View File
@@ -4,9 +4,12 @@ import { SubmitProfile } from '@expo/eas-json';
import { CredentialsContext } from '../credentials/context';
import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName } from '../project/projectUtils';
import { Actor } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
export interface SubmissionContext<T extends Platform> {
accountName: string;
archiveFlags: SubmitArchiveFlags;
credentialsCtx: CredentialsContext;
exp: ExpoConfig;
@@ -15,6 +18,8 @@ export interface SubmissionContext<T extends Platform> {
profile: SubmitProfile<T>;
projectDir: string;
projectId: string;
projectName: string;
user: Actor;
}
export interface SubmitArchiveFlags {
@@ -37,14 +42,19 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
const { projectDir, nonInteractive } = params;
const exp = getExpoConfig(projectDir, { env: params.env });
const { env, ...rest } = params;
const user = await ensureLoggedInAsync();
const projectName = exp.slug;
const accountName = getProjectAccountName(exp, user);
let credentialsCtx: CredentialsContext | undefined = params.credentialsCtx;
if (!credentialsCtx) {
const user = await ensureLoggedInAsync();
credentialsCtx = new CredentialsContext({ projectDir, user, exp, nonInteractive });
}
return {
...rest,
accountName,
credentialsCtx,
exp,
projectName,
user,
};
}
+2 -6
View File
@@ -9,9 +9,7 @@ export function enableJsonOutput(): void {
return;
}
stdoutWrite = process.stdout.write;
process.stdout.write = (...args: any) => {
return process.stderr.write.call(null, args);
};
process.stdout.write = process.stderr.write.bind(process.stderr);
}
export function printJsonOnlyOutput(value: object): void {
@@ -20,9 +18,7 @@ export function printJsonOnlyOutput(value: object): void {
process.stdout.write = stdoutWrite;
Log.log(JSON.stringify(sanitizeValue(value), null, 2));
} finally {
process.stdout.write = (...args: any) => {
return process.stderr.write.call(null, args);
};
process.stdout.write = process.stderr.write.bind(process.stderr);
}
}
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with the eas.json",
"version": "0.30.0",
"version": "0.31.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/eas-build-job": "0.2.48",
"@expo/eas-build-job": "0.2.52",
"@expo/json-file": "8.2.33",
"env-string": "1.0.1",
"fs-extra": "10.0.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "scripts",
"private": true,
"version": "0.30.0",
"version": "0.31.0",
"scripts": {
"release-changelog": "ts-node src/changelog.ts"
},
Generated
+45 -28
View File
@@ -860,14 +860,15 @@
mv "~2"
safe-json-stringify "~1"
"@expo/config-plugins@3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-3.1.0.tgz#0752ff33c5eab21cf42034a44e79df97f0f867f8"
integrity sha512-V5qxaxCAExBM0TXmbU1QKiZcAGP3ecu7KXede8vByT15cro5PkcWu2sSdJCYbHQ/gw6Vf/i8sr8gKlN8V8TSLg==
"@expo/config-plugins@4.0.2":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-4.0.2.tgz#6c83ae373c12a240ccadc2322bd8267e4eb91f7b"
integrity sha512-j8IOVH0yL7ZDYcoozdpQb3ZuwUM/4eZ1QWy4icovjlyXGtRFyJvWZitZkA/7HK1+0mS2ZNSW28elp+LVTX3UfA==
dependencies:
"@expo/config-types" "^42.0.0"
"@expo/config-types" "^43.0.0"
"@expo/json-file" "8.2.33"
"@expo/plist" "0.0.14"
"@expo/plist" "0.0.15"
"@react-native/normalize-color" "^2.0.0"
chalk "^4.1.2"
debug "^4.3.1"
find-up "~5.0.0"
@@ -880,19 +881,19 @@
xcode "^3.0.1"
xml2js "^0.4.23"
"@expo/config-types@^42.0.0":
version "42.0.0"
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-42.0.0.tgz#3e3e125ec092c0c34dbfaf19be5480402de3d677"
integrity sha512-Rj02OMZke2MrGa/1Y/EScmR7VuWbDEHPJyvfFyyLbadUt+Yv6isCdeFzDt71I7gJlPR9T4fzixeYLrtXXOTq0w==
"@expo/config-types@^43.0.0":
version "43.0.0"
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-43.0.0.tgz#e283a4a1b72a655a6a2a745bd277caef58c8edb0"
integrity sha512-GBbEtLdGX40DJ1wTixWtOHseupA5xWclV57KNZ58ebauWAOyK3Fyni7GD2OCOzxTIFi1nSjqgO9JlMp3ayP4Tw==
"@expo/config@5.0.9":
version "5.0.9"
resolved "https://registry.yarnpkg.com/@expo/config/-/config-5.0.9.tgz#5221af5394599d861515ef8513731f21fbb322db"
integrity sha512-eZj+cf03wkQQdHSpYvrmiqAsn2dJV10uhHIwXyeFBaFvhds0NgThOldJZfOppQ4QUaGobB/vaJ7UqUa3B0PCMw==
"@expo/config@6.0.2":
version "6.0.2"
resolved "https://registry.yarnpkg.com/@expo/config/-/config-6.0.2.tgz#d778be85a6b9f9fdb1f453464ba4c3420ba410d1"
integrity sha512-UjNlbs3gYfYnb+8If5dijepRz2+J8sXdmF9bvGQ2LUULdpO0Bx7EbLErctzrFXlK3j7jw1esPWWNfVq5wIlEwg==
dependencies:
"@babel/code-frame" "~7.10.4"
"@expo/config-plugins" "3.1.0"
"@expo/config-types" "^42.0.0"
"@expo/config-plugins" "4.0.2"
"@expo/config-types" "^43.0.0"
"@expo/json-file" "8.2.33"
getenv "^1.0.0"
glob "7.1.6"
@@ -902,12 +903,13 @@
slugify "^1.3.4"
sucrase "^3.20.0"
"@expo/[email protected].48":
version "0.2.48"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.48.tgz#ebc2abea02cb3f23be9714318bea04515980b03d"
integrity sha512-QWg44uXndNw3ZKrP+1FD2Li1VSQoSWn94iQ6j6lwMfVfFpp6DiiZ73HXf4Bxix52cl5DTmpaE7hxYyhxSvV0yA==
"@expo/[email protected].52":
version "0.2.52"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.52.tgz#889025a412c21e2c77a0c42d342916e2a2569a50"
integrity sha512-1bfOp/dMOf3zbnfJnES3y2z/+pAwNUIIaicnMURY2JLars+3gkQOKm26nk+FnTTE/8QykmIocls6RhlPqCNnUg==
dependencies:
joi "^17.4.2"
semver "^7.3.5"
"@expo/[email protected]":
version "0.3.16"
@@ -971,6 +973,15 @@
base64-js "^1.2.3"
xmlbuilder "^14.0.0"
"@expo/[email protected]":
version "0.0.15"
resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.0.15.tgz#41ef37b7bbe6b81c48bf4a5c359661c766bb9e90"
integrity sha512-LDxiS0KNZAGJu4fIJhbEKczmb+zeftl1NU0LE0tj0mozoMI5HSKdMUchgvnBm35bwBl8ekKkAfJJ0ONxljWQjQ==
dependencies:
"@xmldom/xmldom" "~0.7.0"
base64-js "^1.2.3"
xmlbuilder "^14.0.0"
"@expo/[email protected]":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@expo/plugin-warn-if-update-available/-/plugin-warn-if-update-available-1.7.0.tgz#d47fb15c9b0c8af91b20c9472e5f848972b472d8"
@@ -986,19 +997,20 @@
lodash.template "^4.4.0"
semver "^7.3.2"
"@expo/prebuild-config@2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-2.1.0.tgz#d1dbead511b3959237e542e326a9db757e97fcab"
integrity sha512-obpbnV0+Otv7Dbx8kkbSd62xL9HYZRDPdmdcVWuML7lv7Zo4r+OyS6vYpUmln9htp0gtjuc6+X9FiC74bbGkVA==
"@expo/prebuild-config@3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-3.0.2.tgz#8b0fae285efbfede82d6b3aaee96ea8ea10307de"
integrity sha512-iEKcjbbwrICRgqAPNcyI3g2MkZx3gkmD9KE7flfWJwC+6XzYC8DBz9vN5mVAu8GX3S9H16sC2tF4ZspVC6WHDg==
dependencies:
"@expo/config" "5.0.9"
"@expo/config-plugins" "3.1.0"
"@expo/config-types" "^42.0.0"
"@expo/config" "6.0.2"
"@expo/config-plugins" "4.0.2"
"@expo/config-types" "^43.0.0"
"@expo/image-utils" "0.3.16"
"@expo/json-file" "8.2.33"
debug "^4.3.1"
fs-extra "^9.0.0"
resolve-from "^5.0.0"
semver "7.3.2"
"@expo/[email protected]":
version "1.0.0"
@@ -2607,6 +2619,11 @@
dependencies:
"@octokit/openapi-types" "^9.5.0"
"@react-native/normalize-color@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@react-native/normalize-color/-/normalize-color-2.0.0.tgz#da955909432474a9a0fe1cbffc66576a0447f567"
integrity sha512-Wip/xsc5lw8vsBlmY2MO/gFLp3MvuZ2baBZjDeTjjndMgM0h5sxz7AZR62RDPGgstp8Np7JzjvVqVT7tpFZqsw==
"@samverschueren/stream-to-observable@^0.3.0":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301"
@@ -9640,7 +9657,7 @@ sisteransi@^1.0.4:
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
slash@^3.0.0:
[email protected], slash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==