Compare commits

...
21 Commits
Author SHA1 Message Date
Brent Vatne 8fe5268a73 v0.33.1 2021-10-22 11:10:45 -07:00
Brent Vatne faba1ad8e3 [eas-cli] Skip validating updates scripts integration, which no longer exist in SDK 43+ (#706) 2021-10-22 11:09:50 -07:00
Dominik Sokal ea4885c710 fix typo 2021-10-21 10:10:47 +02:00
Brent Vatne 4d85002d2b [eas-cli] Update the submission prompt options 2021-10-20 11:43:55 -07:00
jkh 535b17f592 [eas-cli] use config-plugin to compute runtime version (#697) 2021-10-20 10:58:59 -07:00
Expo CI e556b45d4d update CHANGELOG.md 2021-10-20 15:49:25 +00:00
jonsamp 48130dcbdc v0.33.0 2021-10-20 11:43:29 -04:00
1419ca6325 [eas-cli] Make "production" the default profile name (#677)
* build defaults to production, falls back to release

* allow submit to use production profile

* fix typo

* lint

* use a common type

* lint

* adds changelog

* adds test for getDefaultProfilesAsync

* updates default config

* self feedback

* adds channel names

* removes channel from default eas json config

* de-nest getDefaultProfilesAsync

* remove "_" variable name

* fix changelog

* incorporate feedback

* restore readme to main

* lint

* Update CHANGELOG.md

Co-authored-by: Wojciech Kozyra <[email protected]>

* Update CHANGELOG.md

Co-authored-by: Dominik Sokal <[email protected]>

* Update packages/eas-cli/src/build/utils/devClient.ts

Co-authored-by: Dominik Sokal <[email protected]>

Co-authored-by: Wojciech Kozyra <[email protected]>
Co-authored-by: Dominik Sokal <[email protected]>
2021-10-18 12:26:08 -04:00
Dominik Sokal 8513dc0645 update CHANGELOG.md 2021-10-18 14:59:13 +02:00
Dominik Sokal 70e8f8370b unify reading app.json (#692) 2021-10-18 14:58:08 +02:00
Dominik Sokal 135c90a8b8 eas submit - skip second prompt for apple id when already signed in with apple (#691)
* eas submit - skip second prompt for apple id when already signed in with apple

* update CHANGELOG.md
2021-10-18 14:37:47 +02:00
Dominik Sokal fa588c2d24 don't capitalize credentials.json 2021-10-18 12:33:35 +02:00
Dominik Sokal b4ce7a91c2 rename generic workflow -> bare workflow (#690) 2021-10-18 10:24:24 +02:00
Dominik Sokal 8ca9d66c32 fix typos 2021-10-15 16:04:54 +02:00
Dominik Sokal a86fabde59 re-generate graphql code 2021-10-15 11:44:44 +02:00
Expo CI 40a896675f update CHANGELOG.md 2021-10-15 09:05:59 +00:00
Dominik Sokal cf7848e3c5 v0.32.0 2021-10-15 11:00:07 +02:00
Bartłomiej Klocek 3d4394a9bc [eas-cli][submit] Ability to select a build from the list (#688)
* [eas-cli][submit] Ability to select a build from the list

* Add tests

* update CHANGELOG.md
2021-10-15 10:42:42 +02:00
Quinlan Jung 05f4c08d09 [eas-cli] create/list/revoke asc api keys via baconlane (#687)
* [eas-cli] manage ascApiKeys via baconlane

* expose methods

* update CHANGELOG.md

* pr feedback

* fix tests

* grammar
2021-10-14 13:22:19 -07:00
Quinlan JungandDominik Sokal a4107a86f9 [eas-cli] support ascApiKeys in ios submission (#678)
* [eas-cli] support ascApiKeys in ios submission

* format feedback

* get values from eas.json

* more pr feedback

* changelog stuff

* fyi

* tests

* fix lint

* Update CHANGELOG.md

Co-authored-by: Dominik Sokal <[email protected]>

* Update packages/eas-cli/src/submit/ios/IosSubmitCommand.ts

Co-authored-by: Dominik Sokal <[email protected]>

Co-authored-by: Dominik Sokal <[email protected]>
2021-10-11 14:49:26 -07:00
Quinlan JungandDominik Sokal a5c695dd88 [eas-json] add asc api key parameters to eas json (#684)
* [eas-json] add asc api key parameters to eas json

* changelog stuff

* Update CHANGELOG.md

Co-authored-by: Dominik Sokal <[email protected]>

Co-authored-by: Dominik Sokal <[email protected]>
2021-10-11 14:41:13 -07:00
53 changed files with 1912 additions and 1458 deletions

No files matched your search

+40
View File
@@ -8,12 +8,52 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Compute runtime version using config-plugins. ([#697](https://github.com/expo/eas-cli/pull/697) by [@jkhales](https://github.com/jkhales))
### 🐛 Bug fixes
- [eas-cli] Skip validating updates scripts integration, which no longer exist in SDK 43+ ([#706](https://github.com/expo/eas-cli/pull/706) by [@brentvatne](https://github.com/brentvatne))
### 🧹 Chores
## [0.33.0](https://github.com/expo/eas-cli/releases/tag/0.33.0) - 2021-10-20
### 🎉 New features
- Make "production" the default profile for building and submitting. ([#677](https://github.com/expo/eas-cli/pull/677)) by [@jonsamp](https://github.com/jonsamp)
- Previously, the default profile when running `eas build` or `eas submit` was "release". We're changing it to a more recognizable name that is consistent with our docs, which is now "production". You can always specify a profile manually with the `--profile` flag. For this major version, if you do not have a profile named "production", and still have a profile named "release", we will fall back to the "release" profile as the default, however you'll see a warning as we're going to remove that behavior in the next major release of EAS CLI.
- To upgrade, update **eas.json** to have a "production" profile in both the `build` and `submit` objects. If you already have a project set up, this will replace the existing "release" profile. After the change, eas.json should have the following profiles:
```json
{
"build": {
"production": { ... }
},
"submit": {
"production": { ... }
}
}
```
### 🐛 Bug fixes
- Skip the second prompt for Apple ID if the user is already signed in with Apple. ([#691](https://github.com/expo/eas-cli/pull/691) by [@dsokal](https://github.com/dsokal))
- Unify reading `app.json`. ([#692](https://github.com/expo/eas-cli/pull/692) by [@dsokal](https://github.com/dsokal))
## [0.32.0](https://github.com/expo/eas-cli/releases/tag/0.32.0) - 2021-10-15
### 🎉 New features
- Add App Store Connect Api Key support to iOS submissions. ([#678](https://github.com/expo/eas-cli/pull/678) by [@quinlanj](https://github.com/quinlanj))
- Create/list/revoke App Store Connect Api keys via Apple apis. ([#687](https://github.com/expo/eas-cli/pull/687) by [@quinlanj](https://github.com/quinlanj))
- Add ability to select a build from a list in `eas submit` interactive mode. ([#688](https://github.com/expo/eas-cli/pull/688) by [@barthap](https://github.com/barthap))
### 🐛 Bug fixes
- Fix printing App Store Connect URL after submission. ([#683](https://github.com/expo/eas-cli/pull/683) by [@brentvatne](https://github.com/brentvatne))
### 🧹 Chores
- Add App Store Connect Api Key fields to `eas.json`. ([#684](https://github.com/expo/eas-cli/pull/684) by [@quinlanj](https://github.com/quinlanj))
- Enable no-underscore-dangle eslint rule. ([#686](https://github.com/expo/eas-cli/pull/686) by [@dsokal](https://github.com/dsokal))
## [0.31.1](https://github.com/expo/eas-cli/releases/tag/0.31.1) - 2021-10-08
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.31.1"
"version": "0.33.1"
}
+31 -29
View File
@@ -73,7 +73,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/account/login.js)_
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.33.1/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.31.1/packages/eas-cli/src/commands/account/logout.js)_
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.33.1/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.31.1/packages/eas-cli/src/commands/account/view.js)_
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.33.1/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.31.1/packages/eas-cli/src/commands/analytics.js)_
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/analytics.js)_
## `eas build`
@@ -138,14 +138,15 @@ OPTIONS
--non-interactive Run command in non-interactive mode
--profile=PROFILE_NAME [default: release] Name of the build profile from eas.json
--profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined
in eas.json.
--skip-project-configuration Skip project configuration
--[no-]wait Wait for build(s) to complete
```
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/build/index.js)_
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/build/index.js)_
## `eas build:cancel [BUILD_ID]`
@@ -156,7 +157,7 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/build/cancel.js)_
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/build/cancel.js)_
## `eas build:configure`
@@ -170,7 +171,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.31.1/packages/eas-cli/src/commands/build/configure.js)_
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/build/configure.js)_
## `eas build:list`
@@ -203,7 +204,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.31.1/packages/eas-cli/src/commands/build/list.js)_
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/build/list.js)_
## `eas build:view [BUILD_ID]`
@@ -217,7 +218,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.31.1/packages/eas-cli/src/commands/build/view.js)_
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/build/view.js)_
## `eas config`
@@ -232,7 +233,7 @@ OPTIONS
--profile=profile
```
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/config.js)_
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/config.js)_
## `eas credentials`
@@ -243,7 +244,7 @@ USAGE
$ eas credentials
```
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/credentials.js)_
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/credentials.js)_
## `eas device:create`
@@ -254,7 +255,7 @@ USAGE
$ eas device:create
```
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/device/create.js)_
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/device/create.js)_
## `eas device:list`
@@ -268,7 +269,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/device/list.js)_
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/device/list.js)_
## `eas device:view [UDID]`
@@ -279,7 +280,7 @@ USAGE
$ eas device:view [UDID]
```
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/device/view.js)_
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/device/view.js)_
## `eas diagnostics`
@@ -290,7 +291,7 @@ USAGE
$ eas diagnostics
```
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/diagnostics.js)_
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/diagnostics.js)_
## `eas help [COMMAND]`
@@ -318,7 +319,7 @@ USAGE
$ eas project:info
```
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/project/info.js)_
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/project/info.js)_
## `eas project:init`
@@ -332,7 +333,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/project/init.js)_
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/project/init.js)_
## `eas secret:create`
@@ -349,7 +350,7 @@ OPTIONS
--value=value Value of the secret
```
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/secret/create.js)_
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/secret/create.js)_
## `eas secret:delete`
@@ -366,7 +367,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.31.1/packages/eas-cli/src/commands/secret/delete.js)_
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/secret/delete.js)_
## `eas secret:list`
@@ -377,7 +378,7 @@ USAGE
$ eas secret:list
```
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/secret/list.js)_
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/secret/list.js)_
## `eas submit`
@@ -394,7 +395,7 @@ OPTIONS
--non-interactive Run command in non-interactive mode
--path=path Path to the .apk/.aab/.ipa file
--profile=profile Name of the submit profile from eas.json. Defaults to "release" if defined in
--profile=profile Name of the submit profile from eas.json. Defaults to "production" if defined in
eas.json.
--url=url App archive url
@@ -404,13 +405,14 @@ OPTIONS
--[no-]wait Wait for submission to complete
DESCRIPTION
See how to configure submits with eas.json: https://docs.expo.dev/submit/eas-json/
See how to configure submits with eas.json:
https://docs.expo.dev/submit/eas-json/
ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/submit.js)_
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/submit.js)_
## `eas webhook:create`
@@ -429,7 +431,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/webhook/create.js)_
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/webhook/create.js)_
## `eas webhook:delete [ID]`
@@ -443,7 +445,7 @@ ARGUMENTS
ID ID of the webhook to delete
```
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/webhook/delete.js)_
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/webhook/delete.js)_
## `eas webhook:list`
@@ -457,7 +459,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.31.1/packages/eas-cli/src/commands/webhook/list.js)_
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/webhook/list.js)_
## `eas webhook:update`
@@ -477,7 +479,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/webhook/update.js)_
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/webhook/update.js)_
## `eas webhook:view ID`
@@ -491,5 +493,5 @@ ARGUMENTS
ID ID of the webhook to view
```
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/webhook/view.js)_
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.33.1/packages/eas-cli/src/commands/webhook/view.js)_
<!-- commandsstop -->
+417 -999
View File
File diff suppressed because it is too large. Load diff
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.31.1",
"version": "0.33.1",
"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": "6.0.2",
"@expo/config-plugins": "4.0.2",
"@expo/config": "6.0.3",
"@expo/config-plugins": "4.0.3",
"@expo/eas-build-job": "0.2.52",
"@expo/eas-json": "^0.31.0",
"@expo/eas-json": "^0.33.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": "3.0.2",
"@expo/prebuild-config": "3.0.3",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.0",
"@expo/sdk-runtime-versions": "1.0.0",
@@ -1,6 +1,5 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig, AndroidManifest } from '@expo/config-plugins';
import fs from 'fs-extra';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
@@ -10,17 +9,6 @@ import { ensureValidVersions } from '../utils/updates';
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
const updatedBuildGradleContents =
AndroidConfig.Updates.ensureBuildGradleContainsConfigurationScript(
projectDir,
buildGradleContents
);
await fs.writeFile(buildGradlePath, updatedBuildGradleContents);
}
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
const androidManifest = await getAndroidManifestAsync(projectDir);
@@ -39,7 +27,7 @@ export async function syncUpdatesConfigurationAsync(
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
try {
await ensureUpdatesConfiguredAsync(projectDir, exp);
await ensureUpdatesConfiguredAsync(projectDir);
} catch (error) {
Log.error(
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
@@ -62,15 +50,9 @@ export async function syncUpdatesConfigurationAsync(
}
}
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
const gradleScriptApply = AndroidConfig.Updates.formatApplyLineForBuildGradle(projectDir);
throw new Error(`Missing ${gradleScriptApply} in ${buildGradlePath}`);
}
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in AndroidManifest.xml
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
const androidManifest = await getAndroidManifestAsync(projectDir);
if (!AndroidConfig.Updates.isMainApplicationMetaDataSet(androidManifest)) {
@@ -31,7 +31,7 @@ beforeEach(async () => {
await fs.mkdirp(os.tmpdir());
});
// generic workflow
// bare workflow
describe(bumpVersionAsync, () => {
it('throws an informative error when multiple flavors are detected in the android project', async () => {
await initProjectWithGradleFileAsync(
@@ -69,7 +69,7 @@ describe(bumpVersionAsync, () => {
});
it('bumps expo.android.versionCode and buildGradle versionCode when strategy = BumpStrategy.VERSION_CODE', async () => {
const nativeVersionCode = 100; // this should be overwritten by bumpVersionAsync
const fakeExp = initGenericProject({ versionCode: nativeVersionCode });
const fakeExp = initBareWorkflowProject({ versionCode: nativeVersionCode });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
@@ -91,7 +91,7 @@ describe(bumpVersionAsync, () => {
it('bumps expo.version and gradle versionCode when strategy = BumpStrategy.SHORT_VERSION', async () => {
const nativeVersionName = '1.0.0'; // this should be overwritten by bumpVersionAsync
const fakeExp = initGenericProject({ versionName: nativeVersionName });
const fakeExp = initBareWorkflowProject({ versionName: nativeVersionName });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
@@ -112,7 +112,7 @@ describe(bumpVersionAsync, () => {
});
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initGenericProject();
const fakeExp = initBareWorkflowProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP,
@@ -185,9 +185,9 @@ describe(bumpVersionInAppJsonAsync, () => {
});
describe(maybeResolveVersionsAsync, () => {
describe('generic project', () => {
describe('bare project', () => {
it('reads the versions from native code', async () => {
const exp = initGenericProject();
const exp = initBareWorkflowProject();
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/repo',
exp,
@@ -211,7 +211,7 @@ describe(maybeResolveVersionsAsync, () => {
});
});
function initGenericProject({
function initBareWorkflowProject({
versionCode = 123,
versionName = '3.0.0',
}: {
@@ -6,7 +6,7 @@ import { AndroidBuildProfile, AndroidVersionAutoIncrement } from '@expo/eas-json
import Log from '../../log';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
warnIfAndroidPackageDefinedInAppConfigForGenericProject,
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject,
} from '../../project/android/applicationId';
import { resolveWorkflowAsync } from '../../project/workflow';
import vcs from '../../vcs';
@@ -21,7 +21,7 @@ export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void
return;
}
warnIfAndroidPackageDefinedInAppConfigForGenericProject(ctx.projectDir, ctx.exp);
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
await AndroidConfig.EasBuild.configureEasBuildAsync(ctx.projectDir);
+12 -6
View File
@@ -97,20 +97,22 @@ export async function configureAsync(options: {
const EAS_JSON_MANAGED_DEFAULT: EasJson = {
build: {
release: {},
development: {
developmentClient: true,
distribution: 'internal',
},
preview: {
distribution: 'internal',
},
production: {},
},
submit: {
release: {},
production: {},
},
};
const EAS_JSON_GENERIC_DEFAULT: EasJson = {
const EAS_JSON_BARE_DEFAULT: EasJson = {
build: {
release: {},
development: {
distribution: 'internal',
android: {
@@ -120,9 +122,13 @@ const EAS_JSON_GENERIC_DEFAULT: EasJson = {
buildConfiguration: 'Debug',
},
},
preview: {
distribution: 'internal',
},
production: {},
},
submit: {
release: {},
production: {},
},
};
@@ -139,7 +145,7 @@ export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<v
const easJson =
ctx.hasAndroidNativeProject && ctx.hasIosNativeProject
? EAS_JSON_GENERIC_DEFAULT
? EAS_JSON_BARE_DEFAULT
: EAS_JSON_MANAGED_DEFAULT;
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
@@ -1,6 +1,5 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import fs from 'fs-extra';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
@@ -12,19 +11,6 @@ import { readPlistAsync, writePlistAsync } from './plist';
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
let xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, xcodeProject)) {
xcodeProject = IOSConfig.Updates.ensureBundleReactNativePhaseContainsConfigurationScript(
projectDir,
xcodeProject
);
await fs.writeFile(
IOSConfig.Paths.getPBXProjectPath(projectDir),
// eslint-disable-next-line node/no-sync
xcodeProject.writeSync()
);
}
let expoPlist = await readExpoPlistAsync(projectDir);
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, accountName)) {
@@ -41,7 +27,7 @@ export async function syncUpdatesConfigurationAsync(
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
try {
await ensureUpdatesConfiguredAsync(projectDir, exp);
await ensureUpdatesConfiguredAsync(projectDir);
} catch (error) {
Log.error(
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
@@ -62,15 +48,9 @@ export async function syncUpdatesConfigurationAsync(
}
}
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
const xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, xcodeProject)) {
const script = 'expo-updates/scripts/create-manifest-ios.sh';
const buildPhase = '"Bundle React Native code and images"';
throw new Error(`Path to ${script} is missing in a ${buildPhase} build phase.`);
}
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in Expo.plist
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
const expoPlist = await readExpoPlistAsync(projectDir);
if (!IOSConfig.Updates.isPlistConfigurationSet(expoPlist)) {
throw new Error('Missing values in Expo.plist');
@@ -49,10 +49,10 @@ describe(evaluateTemplateString, () => {
});
});
// generic workflow
// bare workflow
describe(bumpVersionAsync, () => {
it('bumps expo.ios.buildNumber and CFBundleVersion when strategy = BumpStrategy.BUILD_NUMBER', async () => {
const fakeExp = initGenericProject();
const fakeExp = initBareWorkflowProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
@@ -74,7 +74,7 @@ describe(bumpVersionAsync, () => {
});
it('bumps expo.ios.buildNumber and CFBundleVersion for non default Info.plist location', async () => {
const fakeExp = initGenericProject({ infoPlistName: 'Info2.plist' });
const fakeExp = initBareWorkflowProject({ infoPlistName: 'Info2.plist' });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
@@ -98,7 +98,7 @@ describe(bumpVersionAsync, () => {
});
it('bumps expo.version and CFBundleShortVersionString when strategy = BumpStrategy.SHORT_VERSION', async () => {
const fakeExp = initGenericProject();
const fakeExp = initBareWorkflowProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
@@ -120,7 +120,7 @@ describe(bumpVersionAsync, () => {
});
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initGenericProject();
const fakeExp = initBareWorkflowProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP,
@@ -194,9 +194,9 @@ describe(bumpVersionInAppJsonAsync, () => {
});
describe(readBuildNumberAsync, () => {
describe('generic project', () => {
describe('bare project', () => {
it('reads the build number from native code', async () => {
const exp = initGenericProject();
const exp = initBareWorkflowProject();
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
expect(buildNumber).toBe('1');
});
@@ -212,14 +212,14 @@ describe(readBuildNumberAsync, () => {
});
describe(readShortVersionAsync, () => {
describe('generic project', () => {
describe('bare project', () => {
it('reads the short version from native code', async () => {
const exp = initGenericProject();
const exp = initBareWorkflowProject();
const appVersion = await readShortVersionAsync('/repo', exp, {});
expect(appVersion).toBe('1.0.0');
});
it('evaluates interpolated build number', async () => {
const exp = initGenericProject({
const exp = initBareWorkflowProject({
appVersion: '$(CURRENT_PROJECT_VERSION)',
});
const buildNumber = await readShortVersionAsync('/repo', exp, {
@@ -231,7 +231,7 @@ describe(readShortVersionAsync, () => {
describe('managed project', () => {
it('reads the version from app config', async () => {
const exp = initGenericProject();
const exp = initBareWorkflowProject();
const appVersion = await readShortVersionAsync('/repo', exp, {});
expect(appVersion).toBe('1.0.0');
});
@@ -286,7 +286,7 @@ describe(getInfoPlistPath, () => {
});
});
function initGenericProject({
function initBareWorkflowProject({
appVersion = '1.0.0',
version = '1',
infoPlistName = 'Info.plist',
+2 -2
View File
@@ -6,7 +6,7 @@ import type { XCBuildConfiguration } from 'xcode';
import Log from '../../log';
import {
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
warnIfBundleIdentifierDefinedInAppConfigForGenericProject,
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject,
} from '../../project/ios/bundleIdentifier';
import { resolveWorkflowAsync } from '../../project/workflow';
import { ConfigureContext } from '../context';
@@ -20,7 +20,7 @@ export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
return;
}
warnIfBundleIdentifierDefinedInAppConfigForGenericProject(ctx.projectDir, ctx.exp);
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
if (isExpoUpdatesInstalled(ctx.projectDir)) {
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
+12 -3
View File
@@ -1,6 +1,6 @@
import * as ExpoConfig from '@expo/config';
import JsonFile from '@expo/json-file';
import assert from 'assert';
import fs from 'fs-extra';
export async function updateAppJsonConfigAsync(
{
@@ -15,10 +15,19 @@ export async function updateAppJsonConfigAsync(
const paths = ExpoConfig.getConfigFilePaths(projectDir);
assert(paths.staticConfigPath, "can't update dynamic config");
const rawStaticConfig = await fs.readJSON(paths.staticConfigPath);
const rawStaticConfig = readAppJson(paths.staticConfigPath);
rawStaticConfig.expo = rawStaticConfig.expo ?? {};
modifyConfig(rawStaticConfig.expo);
await fs.writeJson(paths.staticConfigPath, rawStaticConfig, { spaces: 2 });
await JsonFile.writeAsync(paths.staticConfigPath, rawStaticConfig, { json5: false });
modifyConfig(exp);
}
// TODO: remove this once @expo/config exports getStaticConfig
export function readAppJson(appJsonPath: string): any {
const config = JsonFile.read(appJsonPath, { json5: true });
if (config) {
return config as any;
}
throw new Error(`Failed to read app.json`);
}
+17 -20
View File
@@ -1,5 +1,5 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import resolveFrom from 'resolve-from';
@@ -10,39 +10,36 @@ import { appPlatformDisplayNames } from '../../platform';
import { resolveWorkflowAsync } from '../../project/workflow';
import { confirmAsync } from '../../prompts';
import { expoCommandAsync } from '../../utils/expoCommand';
import zipObject from '../../utils/expodash/zipObject';
import { ProfileData } from '../../utils/profiles';
import { reviewAndCommitChangesAsync } from './repository';
export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
projectDir,
platforms,
profile,
nonInteractive = false,
buildProfiles = [],
}: {
projectDir: string;
platforms: Platform[];
profile: string;
nonInteractive?: boolean;
buildProfiles?: ProfileData<BuildProfile>[];
}): Promise<void> {
if (await isExpoDevClientInstalledAsync(projectDir)) {
return;
}
const easJsonReader = new EasJsonReader(projectDir);
const devClientPerPlatformList = await Promise.all(
platforms.map(async platform => {
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, profile);
return buildProfile.developmentClient ?? false;
})
const buildProfilesWithDevelopmentClientRequired = buildProfiles.filter(
buildProfile => buildProfile.profile.developmentClient
);
const isDevClientRequired = devClientPerPlatformList.some(i => i);
if (!isDevClientRequired) {
const isDevelopmentClientRequired = buildProfilesWithDevelopmentClientRequired.some(Boolean);
if (!isDevelopmentClientRequired) {
return;
}
const devClientPerPlatform = zipObject(platforms, devClientPerPlatformList);
const platformsToCheck = platforms.filter(platform => devClientPerPlatform[platform]);
const platformsToCheck = buildProfilesWithDevelopmentClientRequired.map(
({ platform }) => platform
);
const workflowPerPlatformList = await Promise.all(
platformsToCheck.map(platform => resolveWorkflowAsync(projectDir, platform))
);
@@ -67,15 +64,15 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
if (install) {
await installExpoDevClientAsync(projectDir, { nonInteractive });
} else {
error(`Install ${chalk.bold('expo-dev-client')} on your own and come back later.`, {
error(`Install ${chalk.bold('expo-dev-client')} manually and come back later.`, {
exit: 1,
});
}
} else {
Log.warn(`Unfortunately, you need to install ${chalk.bold('expo-dev-client')} on your own.`);
Log.warn(`You'll need to install ${chalk.bold('expo-dev-client')} manually.`);
Log.warn(
learnMore('https://docs.expo.dev/clients/installation/', {
learnMoreMessage: 'See installation instructions on how to do it',
learnMoreMessage: 'See installation instructions on how to do it.',
dim: false,
})
);
+19 -50
View File
@@ -1,5 +1,6 @@
import { ExpoConfig, getConfig, getDefaultTarget } from '@expo/config';
import { getRuntimeVersionForSDKVersion } from '@expo/sdk-runtime-versions';
import { ExpoConfig, getConfig } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { flags } from '@oclif/command';
import assert from 'assert';
import chalk from 'chalk';
@@ -28,6 +29,7 @@ import {
collectAssetsAsync,
uploadAssetsAsync,
} from '../../project/publish';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync, selectAsync } from '../../prompts';
import { formatUpdate } from '../../update/utils';
import uniqBy from '../../utils/expodash/uniqBy';
@@ -168,7 +170,7 @@ export default class BranchPublish extends EasCommand {
isPublicConfig: true,
});
const runtimeVersions = getRuntimeVersionObject(exp, platformFlag, projectDir);
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
const projectId = await getProjectIdAsync(exp);
@@ -388,59 +390,26 @@ export default class BranchPublish extends EasCommand {
}
}
function getRuntimeVersionObject(
async function getRuntimeVersionObjectAsync(
exp: ExpoConfig,
platformFlag: string,
platformFlag: PlatformFlag,
projectDir: string
): Record<string, string> {
let { runtimeVersion: defaultRuntimeVersion, sdkVersion } = exp;
): Promise<Record<string, string>> {
const platforms = (platformFlag === 'all' ? ['android', 'ios'] : [platformFlag]) as Platform[];
// When a SDK version is supplied instead of a runtime version and we're in the managed workflow
// construct the runtimeVersion with special meaning indicating that the runtime is an
// Expo SDK preset runtime that can be launched in Expo Go.
const isManagedProject = getDefaultTarget(projectDir) === 'managed';
if (!defaultRuntimeVersion && sdkVersion && isManagedProject) {
Log.withTick('Generating runtime version from sdk version');
defaultRuntimeVersion = getRuntimeVersionForSDKVersion(sdkVersion);
}
const iOSRuntimeVersion = (exp as any).ios?.runtimeVersion; // TODO-JJ remove cast to any
const androidRuntimeVersion = (exp as any).android?.runtimeVersion; // TODO-JJ remove cast to any
let runtimeVersions: Record<string, string>;
switch (platformFlag) {
case 'ios': {
runtimeVersions = {
ios: iOSRuntimeVersion ?? defaultRuntimeVersion,
};
break;
for (const platform of platforms) {
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
if (isPolicy) {
const isManaged = (await resolveWorkflowAsync(projectDir, platform)) === Workflow.MANAGED;
if (!isManaged) {
throw new Error('Runtime version policies are only supported in the managed workflow.');
}
}
case 'android': {
runtimeVersions = {
android: androidRuntimeVersion ?? defaultRuntimeVersion,
};
break;
}
case 'all': {
runtimeVersions = {
ios: iOSRuntimeVersion ?? defaultRuntimeVersion,
android: androidRuntimeVersion ?? defaultRuntimeVersion,
};
break;
}
default:
throw new Error('Platform flag must be "ios", "android", or "all"');
}
if (Object.values(runtimeVersions).some(runtime => !runtime)) {
throw new Error(
"Couldn't find a 'runtimeVersion' for every platform. Please specify it under the 'expo' key in 'app.json'"
);
}
if (Object.values(runtimeVersions).some(runtime => typeof runtime !== 'string')) {
throw new Error(
`Please ensure that all of the runtime versions defined in the app.json are strings.`
);
}
return runtimeVersions;
return Object.fromEntries(
platforms.map(platform => [platform, Updates.getRuntimeVersion(exp, platform)])
);
}
function formatUpdateTitle(
+48 -32
View File
@@ -1,5 +1,6 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import {
BuildProfile,
EasJsonReader,
hasMismatchedExtendsAsync,
isUsingDeprecatedFormatAsync,
@@ -49,13 +50,14 @@ import {
} from '../../submit/submit';
import { printSubmissionDetailsUrls } from '../../submit/utils/urls';
import { enableJsonOutput } from '../../utils/json';
import { ProfileData, getProfilesAsync } from '../../utils/profiles';
import vcs from '../../vcs';
interface RawBuildFlags {
platform?: string;
'skip-credentials-check': boolean;
'skip-project-configuration': boolean;
profile: string;
profile?: string;
'non-interactive': boolean;
local: boolean;
wait: boolean;
@@ -68,7 +70,7 @@ interface RawBuildFlags {
interface BuildFlags {
requestedPlatform: RequestedPlatform;
skipProjectConfiguration: boolean;
profile: string;
profile: string | null;
nonInteractive: boolean;
local: boolean;
wait: boolean;
@@ -99,8 +101,8 @@ export default class Build extends EasCommand {
description: 'Skip project configuration',
}),
profile: flags.string({
default: 'release',
description: 'Name of the build profile from eas.json',
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
}),
'non-interactive': flags.boolean({
@@ -152,44 +154,60 @@ export default class Build extends EasCommand {
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
const platforms = toPlatforms(requestedPlatform);
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
const easJsonReader = new EasJsonReader(projectDir);
const buildProfiles = await getProfilesAsync({
platforms,
projectDir,
profile: flags.profile,
nonInteractive: flags.nonInteractive,
profileName: flags.profile,
async readProfileAsync(platform, profileName) {
return await easJsonReader.readBuildProfileAsync(platform, profileName);
},
});
const startedBuilds: BuildFragment[] = [];
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
projectDir,
nonInteractive: flags.nonInteractive,
buildProfiles,
});
const startedBuilds: {
build: BuildFragment;
buildProfile: ProfileData<BuildProfile>;
}[] = [];
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
for (const platform of platforms) {
for (const buildProfile of buildProfiles) {
const { build: maybeBuild, buildCtx } = await this.prepareAndStartBuildAsync({
projectDir,
platform,
flags,
moreBuilds: platforms.length > 1,
buildProfile,
});
if (maybeBuild) {
startedBuilds.push(maybeBuild);
startedBuilds.push({ build: maybeBuild, buildProfile });
}
buildCtxByPlatform[toAppPlatform(platform)] = buildCtx;
buildCtxByPlatform[toAppPlatform(buildProfile.platform)] = buildCtx;
}
if (flags.local) {
return;
}
Log.newLine();
printLogsUrls(startedBuilds);
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
Log.newLine();
const submissions: SubmissionFragment[] = [];
if (flags.autoSubmit) {
for (const build of startedBuilds) {
for (const startedBuild of startedBuilds) {
const submission = await this.prepareAndStartSubmissionAsync({
build,
credentialsCtx: nullthrows(buildCtxByPlatform[build.platform]?.credentialsCtx),
build: startedBuild.build,
credentialsCtx: nullthrows(
buildCtxByPlatform[startedBuild.build.platform]?.credentialsCtx
),
flags,
moreBuilds: startedBuilds.length > 1,
projectDir,
buildProfile: startedBuild.buildProfile,
});
submissions.push(submission);
}
@@ -203,7 +221,7 @@ export default class Build extends EasCommand {
return;
}
const builds = await waitForBuildEndAsync(startedBuilds.map(build => build.id));
const builds = await waitForBuildEndAsync(startedBuilds.map(({ build }) => build.id));
printBuildResults(builds, flags.json);
const haveAllBuildsFailedOrCanceled = builds.every(
@@ -252,7 +270,7 @@ export default class Build extends EasCommand {
return {
requestedPlatform,
skipProjectConfiguration: flags['skip-project-configuration'],
profile,
profile: profile ?? null,
nonInteractive,
local: flags['local'],
wait: flags['wait'],
@@ -263,34 +281,31 @@ export default class Build extends EasCommand {
};
}
private async prepareAndStartBuildAsync<T extends Platform>({
private async prepareAndStartBuildAsync({
projectDir,
platform,
flags,
moreBuilds,
buildProfile,
}: {
projectDir: string;
platform: T;
flags: BuildFlags;
moreBuilds: boolean;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<T> }> {
const easJsonReader = new EasJsonReader(projectDir);
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, flags.profile);
buildProfile: ProfileData<BuildProfile>;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({
buildProfileName: flags.profile,
buildProfileName: buildProfile.profileName,
clearCache: flags.clearCache,
buildProfile,
buildProfile: buildProfile.profile,
local: flags.local,
nonInteractive: flags.nonInteractive,
platform,
platform: buildProfile.platform,
projectDir,
skipProjectConfiguration: flags.skipProjectConfiguration,
});
if (moreBuilds) {
Log.newLine();
const appPlatform = toAppPlatform(platform);
const appPlatform = toAppPlatform(buildProfile.platform);
Log.log(
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
`${appPlatformDisplayNames[appPlatform]} build`
@@ -330,16 +345,17 @@ export default class Build extends EasCommand {
flags,
moreBuilds,
projectDir,
buildProfile,
}: {
build: BuildFragment;
credentialsCtx: CredentialsContext;
flags: BuildFlags;
moreBuilds: boolean;
projectDir: string;
buildProfile: ProfileData<BuildProfile>;
}): Promise<SubmissionFragment> {
const easJsonReader = new EasJsonReader(projectDir);
const platform = toPlatform(build.platform);
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, flags.profile);
const submitProfile = await easJsonReader.readSubmitProfileAsync(platform, flags.submitProfile);
const submissionCtx = await createSubmissionContextAsync({
platform,
@@ -348,7 +364,7 @@ export default class Build extends EasCommand {
profile: submitProfile,
archiveFlags: { id: build.id },
nonInteractive: flags.nonInteractive,
env: buildProfile.env,
env: buildProfile.profile.env,
credentialsCtx,
});
+16 -8
View File
@@ -20,6 +20,7 @@ import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils
import { SubmitArchiveFlags, createSubmissionContextAsync } from '../submit/context';
import { submitAsync, waitToCompleteAsync } from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { getProfilesAsync } from '../utils/profiles';
interface RawFlags {
platform?: string;
@@ -54,7 +55,7 @@ See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit
}),
profile: flags.string({
description:
'Name of the submit profile from eas.json. Defaults to "release" if defined in eas.json.',
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
}),
latest: flags.boolean({
description: 'Submit the latest build for specified platform',
@@ -101,23 +102,30 @@ See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit
return;
}
const easJsonReader = new EasJsonReader(projectDir);
const platforms = toPlatforms(flags.requestedPlatform);
const easJsonReader = new EasJsonReader(projectDir);
const submissionProfiles = await getProfilesAsync({
platforms,
profileName: flags.profile,
async readProfileAsync(platform, profileName) {
return await easJsonReader.readSubmitProfileAsync(platform, profileName);
},
});
const submissions: SubmissionFragment[] = [];
for (const platform of platforms) {
const profile = await easJsonReader.readSubmitProfileAsync(platform, flags.profile);
for (const submissionProfile of submissionProfiles) {
const ctx = await createSubmissionContextAsync({
platform,
platform: submissionProfile.platform,
projectDir,
projectId,
profile,
profile: submissionProfile.profile,
archiveFlags: flags.archiveFlags,
nonInteractive: flags.nonInteractive,
});
if (platforms.length > 1) {
if (submissionProfiles.length > 1) {
Log.newLine();
const appPlatform = toAppPlatform(platform);
const appPlatform = toAppPlatform(submissionProfile.platform);
Log.log(
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
`${appPlatformDisplayNames[appPlatform]} submission`
@@ -5,3 +5,9 @@ export class MissingCredentialsNonInteractiveError extends Error {
);
}
}
export class MissingCredentialsError extends Error {
constructor(message?: string) {
super(message ?? 'Credentials are not set up.');
}
}
@@ -93,7 +93,7 @@ export default class IosCredentialsProvider {
}
const confirmSetup = await confirmAsync({
message: `Would you like to setup Push Notifications for your project?`,
message: `Would you like to set up Push Notifications for your project?`,
});
if (!confirmSetup) {
return null;
@@ -0,0 +1,71 @@
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import * as uuid from 'uuid';
import Log, { learnMore } from '../../../log';
import { promptAsync } from '../../../prompts';
import { getCredentialsFromUserAsync } from '../../utils/promptForCredentials';
import { AscApiKey } from '../appstore/Credentials.types';
import { AscApiKeyPath, MinimalAscApiKey, ascApiKeyMetadataSchema } from '../credentials';
export async function promptForAscApiKeyAsync(): Promise<AscApiKeyPath> {
Log.log(
chalk.bold('An App Store Connect Api key is required to upload your app to the Apple App Store')
);
Log.log(
`If you're not sure what this is or how to create one, ${learnMore(
'https://expo.fyi/creating-asc-api-key'
)}`
);
const { keyP8Path } = await promptAsync({
type: 'text',
name: 'keyP8Path',
message: 'Path to App Store Connect Api Key:',
initial: 'AuthKey_ABCD.p8',
// eslint-disable-next-line async-protect/async-suffix
validate: async (filePath: string) => {
try {
const stats = await fs.stat(filePath);
if (stats.isFile()) {
return true;
}
return 'Input is not a file.';
} catch {
return 'File does not exist.';
}
},
});
const regex = /^AuthKey_(?<keyId>\w+)\.p8$/; // Common ASC Api file name downloaded from Apple
const bestEffortKeyId = path.basename(keyP8Path).match(regex)?.groups?.keyId;
const ascApiKeyMetadata = await getCredentialsFromUserAsync(ascApiKeyMetadataSchema, {
keyId: bestEffortKeyId,
});
return { ...ascApiKeyMetadata, keyP8Path };
}
export async function promptForIssuerIdAsync(): Promise<string> {
Log.log(chalk.bold('An App Store Connect Issuer ID is required'));
Log.log(
`If you're not sure what this is or how to find yours, ${learnMore(
'https://expo.fyi/asc-issuer-id'
)}`
);
const { issuerId } = await promptAsync({
type: 'text',
name: 'issuerId',
message: 'App Store Connect Issuer ID:',
validate: (input: string) => uuid.validate(input),
});
return issuerId;
}
export async function getMinimalAscApiKeyAsync(ascApiKey: AscApiKey): Promise<MinimalAscApiKey> {
return {
...ascApiKey,
issuerId: ascApiKey.issuerId ?? (await promptForIssuerIdAsync()),
};
}
@@ -39,7 +39,7 @@ export class SetupTargetBuildCredentials {
try {
return await this.setupBuildCredentialsAsync(ctx);
} catch (error) {
Log.error('Failed to setup credentials.');
Log.error('Failed to set up credentials.');
throw error;
}
}
@@ -1,4 +1,6 @@
import {
AscApiKey,
AscApiKeyInfo,
DistributionCertificate,
DistributionCertificateStoreInfo,
ProvisioningProfile,
@@ -6,6 +8,12 @@ import {
PushKey,
PushKeyStoreInfo,
} from './Credentials.types';
import {
createAscApiKeyAsync,
getAscApiKeyAsync,
listAscApiKeysAsync,
revokeAscApiKeyAsync,
} from './ascApiKey';
import { AuthCtx, Options as AuthenticateOptions, authenticateAsync } from './authenticate';
import {
createDistributionCertificateAsync,
@@ -136,4 +144,24 @@ export default class AppStoreApi {
distCertSerialNumber
);
}
public async listAscApiKeysAsync(): Promise<AscApiKeyInfo[]> {
const ctx = await this.ensureAuthenticatedAsync();
return await listAscApiKeysAsync(ctx);
}
public async getAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo> {
const ctx = await this.ensureAuthenticatedAsync();
return await getAscApiKeyAsync(ctx, keyId);
}
public async createAscApiKeyAsync({ nickname }: { nickname: string }): Promise<AscApiKey> {
const ctx = await this.ensureAuthenticatedAsync();
return await createAscApiKeyAsync(ctx, { nickname });
}
public async revokeAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo> {
const ctx = await this.ensureAuthenticatedAsync();
return await revokeAscApiKeyAsync(ctx, keyId);
}
}
@@ -1,3 +1,5 @@
import { UserRole } from '@expo/apple-utils';
export interface Device {
id: string;
teamId: string;
@@ -66,3 +68,16 @@ export interface PushKey {
teamId: string;
teamName?: string;
}
export type AscApiKeyInfo = {
keyId: string;
issuerId?: string;
teamId: string;
name: string;
teamName?: string;
roles: UserRole[];
};
export type AscApiKey = AscApiKeyInfo & {
keyP8: string;
};
@@ -0,0 +1,98 @@
import { ApiKey, ApiKeyType, UserRole } from '@expo/apple-utils';
import { asMock } from '../../../../__tests__/utils';
import {
createAscApiKeyAsync,
getAscApiKeyAsync,
listAscApiKeysAsync,
revokeAscApiKeyAsync,
} from '../ascApiKey';
import { getRequestContext } from '../authenticate';
jest.mock('@expo/apple-utils');
jest.mock('../authenticate');
jest.mock('../../../../ora');
const mockTeam = {
id: 'test-id',
name: 'test-name',
};
const mockAuthCtx = {
appleId: 'test-appleId',
team: mockTeam,
};
const mockApiKey = {
type: 'apiKeys',
id: 'NL67AN9Q6Q',
attributes: {
nickname: 'ejb-apple-utils-admin',
lastUsed: '2021-06-28T12:30:41-07:00',
revokingDate: null,
isActive: true,
canDownload: false,
privateKey: null,
roles: ['ADMIN'],
allAppsVisible: true,
keyType: 'PUBLIC_API',
},
downloadAsync: jest.fn(() => 'super secret'),
revokeAsync: jest.fn(() => mockApiKey),
} as unknown as ApiKey;
const mockAscApiKeyInfo = {
issuerId: undefined,
keyId: 'NL67AN9Q6Q',
name: 'ejb-apple-utils-admin',
roles: ['ADMIN'],
teamId: 'test-id',
teamName: 'test-name',
};
beforeEach(() => {
jest.clearAllMocks();
});
test(`listAscApiKeysAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'getAsync').mockImplementation(async () => []);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await listAscApiKeysAsync(mockAuthCtx);
expect(ApiKey.getAsync).toHaveBeenLastCalledWith(mockRequestContext);
expect(result).toEqual([]);
});
test(`getAscApiKeyAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'infoAsync').mockImplementation(async () => mockApiKey);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await getAscApiKeyAsync(mockAuthCtx, 'test-key-id');
expect(ApiKey.infoAsync).toHaveBeenLastCalledWith(mockRequestContext, { id: 'test-key-id' });
expect(result).toEqual(mockAscApiKeyInfo);
});
test(`createAscApiKeyAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'createAsync').mockImplementation(async () => mockApiKey);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await createAscApiKeyAsync(mockAuthCtx, { nickname: 'test-name' });
expect(ApiKey.createAsync).toHaveBeenLastCalledWith(mockRequestContext, {
nickname: 'test-name',
allAppsVisible: true,
roles: [UserRole.ADMIN],
keyType: ApiKeyType.PUBLIC_API,
});
expect(result).toEqual({ ...mockAscApiKeyInfo, keyP8: 'super secret' });
});
test(`revokeAscApiKeyAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'infoAsync').mockImplementation(async () => mockApiKey);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await revokeAscApiKeyAsync(mockAuthCtx, 'test-key-id');
expect(ApiKey.infoAsync).toHaveBeenLastCalledWith(mockRequestContext, {
id: 'test-key-id',
});
expect(mockApiKey.revokeAsync).toBeCalled();
expect(result).toEqual(mockAscApiKeyInfo);
});
@@ -0,0 +1,104 @@
import { ApiKey, ApiKeyProps, ApiKeyType, UserRole } from '@expo/apple-utils';
import Log from '../../../log';
import { ora } from '../../../ora';
import { AscApiKey, AscApiKeyInfo } from './Credentials.types';
import { AuthCtx, getRequestContext } from './authenticate';
export async function listAscApiKeysAsync(authCtx: AuthCtx): Promise<AscApiKeyInfo[]> {
const spinner = ora(`Fetching App Store Connect API Keys.`).start();
try {
const context = getRequestContext(authCtx);
const keys = await ApiKey.getAsync(context);
spinner.succeed(`Fetched App Store Connect API Keys.`);
return keys.map(key => getAscApiKeyInfo(key, authCtx));
} catch (error) {
spinner.fail(`Failed to fetch App Store Connect API Keys.`);
throw error;
}
}
export async function getAscApiKeyAsync(authCtx: AuthCtx, keyId: string): Promise<AscApiKeyInfo> {
const spinner = ora(`Fetching App Store Connect API Key.`).start();
try {
const context = getRequestContext(authCtx);
const apiKey = await ApiKey.infoAsync(context, { id: keyId });
spinner.succeed(`Fetched App Store Connect API Key (ID: ${keyId}).`);
return getAscApiKeyInfo(apiKey, authCtx);
} catch (error) {
Log.error(error);
spinner.fail(`Failed to fetch App Store Connect API Key.`);
throw error;
}
}
export async function createAscApiKeyAsync(
authCtx: AuthCtx,
{
nickname,
allAppsVisible,
roles,
keyType,
}: Partial<Pick<ApiKeyProps, 'nickname' | 'roles' | 'allAppsVisible' | 'keyType'>>
): Promise<AscApiKey> {
const spinner = ora(`Creating App Store Connect API Key.`).start();
try {
const context = getRequestContext(authCtx);
const key = await ApiKey.createAsync(context, {
nickname: nickname ?? `[expo] ${new Date().getTime()}`,
allAppsVisible: allAppsVisible ?? true,
roles: roles ?? [UserRole.ADMIN],
keyType: keyType ?? ApiKeyType.PUBLIC_API,
});
const keyP8 = await key.downloadAsync();
if (!keyP8) {
const { nickname, roles } = key.attributes;
const humanReadableKey = `App Store Connect Key '${nickname}' (${
key.id
}) with roles {${roles.join(',')}}`;
if (!key.attributes.canDownload) {
// this case would be unexpected because we just created the key
throw new Error(`${humanReadableKey} is not available for download from Apple.`);
} else if (!key.attributes.isActive) {
throw new Error(`${humanReadableKey} is inactive and could not be downloaded.`);
}
throw new Error(`Failed to download .p8 file of ${humanReadableKey}.`);
}
return {
...getAscApiKeyInfo(key, authCtx),
keyP8,
};
} catch (err: any) {
spinner.fail('Failed to create App Store Connect API Key.');
throw err;
}
}
export async function revokeAscApiKeyAsync(
authCtx: AuthCtx,
keyId: string
): Promise<AscApiKeyInfo> {
const spinner = ora(`Revoking App Store Connect API Key.`).start();
try {
const context = getRequestContext(authCtx);
const apiKey = await ApiKey.infoAsync(context, { id: keyId });
const revokedKey = await apiKey.revokeAsync();
spinner.succeed(`Revoked App Store Connect API Key.`);
return getAscApiKeyInfo(revokedKey, authCtx);
} catch (error) {
Log.error(error);
spinner.fail(`Failed to revoke App Store Connect API Key.`);
throw error;
}
}
export function getAscApiKeyInfo(apiKey: ApiKey, authCtx: AuthCtx): AscApiKeyInfo {
return {
name: apiKey.attributes.nickname,
keyId: apiKey.id,
issuerId: apiKey.attributes.provider?.id,
teamId: authCtx.team.id,
teamName: authCtx.team.name,
roles: apiKey.attributes.roles,
};
}
@@ -1,3 +1,5 @@
import { UserRole } from '@expo/apple-utils';
import { AppleDevice } from '../../graphql/generated';
import Log from '../../log';
import { CredentialSchema } from '../utils/promptForCredentials';
@@ -100,6 +102,38 @@ export const distributionCertificateSchema: CredentialSchema<DistributionCertifi
},
};
export type MinimalAscApiKey = {
keyP8: string;
keyId: string;
issuerId: string;
teamId?: string;
teamName?: string;
roles?: UserRole[];
name?: string;
};
export interface AscApiKeyPath {
keyP8Path: string;
keyId: string;
issuerId: string;
}
export const ascApiKeyMetadataSchema: CredentialSchema<Omit<MinimalAscApiKey, 'keyP8'>> = {
name: 'App Store Connect API Key',
questions: [
{
field: 'keyId',
type: 'string',
question: 'Key ID:',
},
{
field: 'issuerId',
type: 'string',
question: 'Issuer ID:',
},
],
};
export const pushKeySchema: CredentialSchema<PushKey> = {
name: 'Apple Push Notifications service key',
questions: [
@@ -87,7 +87,7 @@ const highLevelActions: ActionInfo[] = [
},
{
value: ActionType.ManageCredentialsJson,
title: 'Credentials.json: Upload/Download credentials between EAS servers and your local json ',
title: 'credentials.json: Upload/Download credentials between EAS servers and your local json ',
scope: Scope.Manager,
},
{
@@ -82,7 +82,7 @@ const highLevelActions: ActionInfo[] = [
},
{
value: ActionType.ManageCredentialsJson,
title: 'Credentials.json: Upload/Download credentials between EAS servers and your local json ',
title: 'credentials.json: Upload/Download credentials between EAS servers and your local json ',
scope: Scope.Manager,
},
{
+56 -130
View File
@@ -350,13 +350,16 @@ export type Offer = {
trialLength?: Maybe<Scalars['Int']>;
type: OfferType;
features?: Maybe<Array<Maybe<Feature>>>;
prerequisite?: Maybe<OfferPrerequisite>;
};
export enum OfferType {
/** Term subscription */
Subscription = 'SUBSCRIPTION',
/** Advanced Purchase of Paid Resource */
Prepaid = 'PREPAID'
Prepaid = 'PREPAID',
/** Addon, or supplementary subscription */
Addon = 'ADDON'
}
export enum Feature {
@@ -370,6 +373,12 @@ export enum Feature {
OpenSource = 'OPEN_SOURCE'
}
export type OfferPrerequisite = {
__typename?: 'OfferPrerequisite';
type: Scalars['String'];
stripeIds: Array<Scalars['String']>;
};
export type Snack = Project & {
__typename?: 'Snack';
id: Scalars['ID'];
@@ -1379,6 +1388,7 @@ export type UserPermission = {
export type Billing = {
__typename?: 'Billing';
id: Scalars['ID'];
payment?: Maybe<PaymentDetails>;
subscription?: Maybe<SubscriptionDetails>;
/** History of invoices */
@@ -1413,6 +1423,8 @@ export type Address = {
export type SubscriptionDetails = {
__typename?: 'SubscriptionDetails';
id: Scalars['ID'];
planId?: Maybe<Scalars['String']>;
addons: Array<AddonDetails>;
name?: Maybe<Scalars['String']>;
nextInvoice?: Maybe<Scalars['DateTime']>;
cancelledAt?: Maybe<Scalars['DateTime']>;
@@ -1422,6 +1434,13 @@ export type SubscriptionDetails = {
status?: Maybe<Scalars['String']>;
};
export type AddonDetails = {
__typename?: 'AddonDetails';
id: Scalars['ID'];
planId: Scalars['String'];
name: Scalars['String'];
};
export type Charge = {
__typename?: 'Charge';
id: Scalars['ID'];
@@ -2471,22 +2490,8 @@ export type BuildMutation = {
* @deprecated Use cancelBuild instead
*/
cancel: Build;
/**
* Create an Android generic build
* @deprecated Use createAndroidBuild instead
*/
createAndroidGenericBuild: CreateBuildResult;
/**
* Create an Android managed build
* @deprecated Use createAndroidBuild instead
*/
createAndroidManagedBuild: CreateBuildResult;
/** Create an Android build */
createAndroidBuild: CreateBuildResult;
/** Create an iOS generic build */
createIosGenericBuild: CreateBuildResult;
/** Create an iOS managed build */
createIosManagedBuild: CreateBuildResult;
/** Create an iOS build */
createIosBuild: CreateBuildResult;
};
@@ -2502,20 +2507,6 @@ export type BuildMutationDeleteBuildArgs = {
};
export type BuildMutationCreateAndroidGenericBuildArgs = {
appId: Scalars['ID'];
job: AndroidGenericJobInput;
metadata?: Maybe<BuildMetadataInput>;
};
export type BuildMutationCreateAndroidManagedBuildArgs = {
appId: Scalars['ID'];
job: AndroidManagedJobInput;
metadata?: Maybe<BuildMetadataInput>;
};
export type BuildMutationCreateAndroidBuildArgs = {
appId: Scalars['ID'];
job: AndroidJobInput;
@@ -2523,27 +2514,14 @@ export type BuildMutationCreateAndroidBuildArgs = {
};
export type BuildMutationCreateIosGenericBuildArgs = {
appId: Scalars['ID'];
job: IosGenericJobInput;
metadata?: Maybe<BuildMetadataInput>;
};
export type BuildMutationCreateIosManagedBuildArgs = {
appId: Scalars['ID'];
job: IosManagedJobInput;
metadata?: Maybe<BuildMetadataInput>;
};
export type BuildMutationCreateIosBuildArgs = {
appId: Scalars['ID'];
job: IosJobInput;
metadata?: Maybe<BuildMetadataInput>;
};
export type AndroidGenericJobInput = {
export type AndroidJobInput = {
type: BuildWorkflow;
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: Maybe<Scalars['String']>;
@@ -2553,8 +2531,15 @@ export type AndroidGenericJobInput = {
cache?: Maybe<BuildCacheInput>;
gradleCommand?: Maybe<Scalars['String']>;
artifactPath?: Maybe<Scalars['String']>;
buildType?: Maybe<AndroidBuildType>;
username?: Maybe<Scalars['String']>;
};
export enum BuildWorkflow {
Generic = 'GENERIC',
Managed = 'MANAGED'
}
export type ProjectArchiveSourceInput = {
type: ProjectArchiveSourceType;
bucketKey?: Maybe<Scalars['String']>;
@@ -2603,6 +2588,12 @@ export type BuildCacheInput = {
customPaths?: Maybe<Array<Maybe<Scalars['String']>>>;
};
export enum AndroidBuildType {
Apk = 'APK',
AppBundle = 'APP_BUNDLE',
DevelopmentClient = 'DEVELOPMENT_CLIENT'
}
export type BuildMetadataInput = {
trackingContext?: Maybe<Scalars['JSONObject']>;
appVersion?: Maybe<Scalars['String']>;
@@ -2623,11 +2614,6 @@ export type BuildMetadataInput = {
username?: Maybe<Scalars['String']>;
};
export enum BuildWorkflow {
Generic = 'GENERIC',
Managed = 'MANAGED'
}
export enum BuildCredentialsSource {
Local = 'LOCAL',
Remote = 'REMOTE'
@@ -2650,46 +2636,8 @@ export enum EasBuildDeprecationInfoType {
Internal = 'INTERNAL'
}
export type AndroidManagedJobInput = {
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: Maybe<Scalars['String']>;
updates?: Maybe<BuildUpdatesInput>;
secrets?: Maybe<AndroidJobSecretsInput>;
builderEnvironment?: Maybe<AndroidBuilderEnvironmentInput>;
cache?: Maybe<BuildCacheInput>;
buildType?: Maybe<AndroidManagedBuildType>;
username?: Maybe<Scalars['String']>;
};
export enum AndroidManagedBuildType {
Apk = 'APK',
AppBundle = 'APP_BUNDLE',
DevelopmentClient = 'DEVELOPMENT_CLIENT'
}
export type AndroidJobInput = {
export type IosJobInput = {
type: BuildWorkflow;
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: Maybe<Scalars['String']>;
updates?: Maybe<BuildUpdatesInput>;
secrets?: Maybe<AndroidJobSecretsInput>;
builderEnvironment?: Maybe<AndroidBuilderEnvironmentInput>;
cache?: Maybe<BuildCacheInput>;
gradleCommand?: Maybe<Scalars['String']>;
artifactPath?: Maybe<Scalars['String']>;
buildType?: Maybe<AndroidBuildType>;
username?: Maybe<Scalars['String']>;
};
export enum AndroidBuildType {
Apk = 'APK',
AppBundle = 'APP_BUNDLE',
DevelopmentClient = 'DEVELOPMENT_CLIENT'
}
export type IosGenericJobInput = {
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: Maybe<Scalars['String']>;
@@ -2698,10 +2646,11 @@ export type IosGenericJobInput = {
secrets?: Maybe<IosJobSecretsInput>;
builderEnvironment?: Maybe<IosBuilderEnvironmentInput>;
cache?: Maybe<BuildCacheInput>;
scheme: Scalars['String'];
schemeBuildConfiguration?: Maybe<IosSchemeBuildConfiguration>;
scheme?: Maybe<Scalars['String']>;
buildConfiguration?: Maybe<Scalars['String']>;
artifactPath?: Maybe<Scalars['String']>;
buildType?: Maybe<IosBuildType>;
username?: Maybe<Scalars['String']>;
};
export type IosJobSecretsInput = {
@@ -2731,46 +2680,6 @@ export type IosBuilderEnvironmentInput = {
env?: Maybe<Scalars['JSONObject']>;
};
export enum IosSchemeBuildConfiguration {
Release = 'RELEASE',
Debug = 'DEBUG'
}
export type IosManagedJobInput = {
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: Maybe<Scalars['String']>;
updates?: Maybe<BuildUpdatesInput>;
distribution?: Maybe<DistributionType>;
secrets?: Maybe<IosJobSecretsInput>;
builderEnvironment?: Maybe<IosBuilderEnvironmentInput>;
cache?: Maybe<BuildCacheInput>;
buildType?: Maybe<IosManagedBuildType>;
username?: Maybe<Scalars['String']>;
};
export enum IosManagedBuildType {
Release = 'RELEASE',
DevelopmentClient = 'DEVELOPMENT_CLIENT'
}
export type IosJobInput = {
type: BuildWorkflow;
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: Maybe<Scalars['String']>;
updates?: Maybe<BuildUpdatesInput>;
distribution?: Maybe<DistributionType>;
secrets?: Maybe<IosJobSecretsInput>;
builderEnvironment?: Maybe<IosBuilderEnvironmentInput>;
cache?: Maybe<BuildCacheInput>;
scheme?: Maybe<Scalars['String']>;
buildConfiguration?: Maybe<Scalars['String']>;
artifactPath?: Maybe<Scalars['String']>;
buildType?: Maybe<IosBuildType>;
username?: Maybe<Scalars['String']>;
};
export enum IosBuildType {
Release = 'RELEASE',
DevelopmentClient = 'DEVELOPMENT_CLIENT'
@@ -2956,11 +2865,18 @@ export type CreateIosSubmissionInput = {
export type IosSubmissionConfigInput = {
appleAppSpecificPasswordId?: Maybe<Scalars['String']>;
appleAppSpecificPassword?: Maybe<Scalars['String']>;
ascApiKey?: Maybe<AscApiKeyInput>;
archiveUrl?: Maybe<Scalars['String']>;
appleIdUsername: Scalars['String'];
ascAppIdentifier: Scalars['String'];
};
export type AscApiKeyInput = {
keyP8: Scalars['String'];
keyIdentifier: Scalars['String'];
issuerIdentifier: Scalars['String'];
};
export type CreateAndroidSubmissionInput = {
appId: Scalars['ID'];
config: AndroidSubmissionConfigInput;
@@ -3512,6 +3428,16 @@ export enum StandardOffer {
Support = 'SUPPORT'
}
export enum IosSchemeBuildConfiguration {
Release = 'RELEASE',
Debug = 'DEBUG'
}
export enum IosManagedBuildType {
Release = 'RELEASE',
DevelopmentClient = 'DEVELOPMENT_CLIENT'
}
export enum CacheControlScope {
Public = 'PUBLIC',
Private = 'PRIVATE'
@@ -12,7 +12,7 @@ describe(resolveWorkflowAsync, () => {
const projectDir = '/app';
test('generic workflow for both platforms', async () => {
test('bare workflow for both platforms', async () => {
vol.fromJSON(
{
'./ios/helloworld.xcodeproj/project.pbxproj': 'fake',
@@ -27,7 +27,7 @@ describe(resolveWorkflowAsync, () => {
await expect(resolveWorkflowAsync(projectDir, Platform.IOS)).resolves.toBe(Workflow.GENERIC);
});
test('generic workflow for single platform', async () => {
test('bare workflow for single platform', async () => {
vol.fromJSON(
{
'./ios/helloworld.xcodeproj/project.pbxproj': 'fake',
@@ -25,7 +25,7 @@ beforeEach(async () => {
});
describe(getApplicationIdAsync, () => {
describe('generic projects', () => {
describe('bare projects', () => {
it('reads applicationId from build.gradle', async () => {
vol.fromJSON(
{
@@ -21,7 +21,7 @@ beforeEach(async () => {
});
describe(resolveGradleBuildContextAsync, () => {
describe('generic projects', () => {
describe('bare projects', () => {
it('resolves to default config (no flavor)', async () => {
vol.fromJSON(
{
@@ -6,6 +6,7 @@ import chalk from 'chalk';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { readAppJson } from '../../build/utils/appJson';
import Log, { learnMore } from '../../log';
import { getProjectConfigDescription, getUsername } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
@@ -39,7 +40,7 @@ export async function getApplicationIdAsync(
): Promise<string> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
warnIfAndroidPackageDefinedInAppConfigForGenericProject(projectDir, exp);
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(projectDir, exp);
const errorMessage = 'Could not read application id from Android project.';
if (gradleContext) {
@@ -112,7 +113,7 @@ async function configureApplicationIdAsync(projectDir: string, exp: ExpoConfig):
validate: value => (isApplicationIdValid(value) ? true : INVALID_APPLICATION_ID_MESSAGE),
});
const rawStaticConfig = await fs.readJSON(paths.staticConfigPath);
const rawStaticConfig = readAppJson(paths.staticConfigPath);
rawStaticConfig.expo = {
...rawStaticConfig.expo,
android: { ...rawStaticConfig.expo?.android, package: packageName },
@@ -129,7 +130,7 @@ function isApplicationIdValid(applicationId: string): boolean {
}
let warnPrinted = false;
export function warnIfAndroidPackageDefinedInAppConfigForGenericProject(
export function warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(
projectDir: string,
exp: ExpoConfig
): void {
@@ -137,7 +138,7 @@ export function warnIfAndroidPackageDefinedInAppConfigForGenericProject(
Log.warn(
`Specifying "android.package" in ${getProjectConfigDescription(
projectDir
)} is deprecated for generic projects.\n` +
)} is deprecated for bare workflow projects.\n` +
'EAS Build depends only on the value in the native code. Please remove the deprecated configuration.'
);
warnPrinted = true;
@@ -29,7 +29,7 @@ beforeEach(async () => {
const originalFs = jest.requireActual('fs') as typeof fs;
describe(getBundleIdentifierAsync, () => {
describe('generic projects', () => {
describe('bare projects', () => {
it('reads bundle identifier from project.', async () => {
vol.fromJSON(
{
@@ -5,6 +5,7 @@ import assert from 'assert';
import chalk from 'chalk';
import fs from 'fs-extra';
import { readAppJson } from '../../build/utils/appJson';
import Log, { learnMore } from '../../log';
import { promptAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions';
@@ -34,7 +35,7 @@ export async function getBundleIdentifierAsync(
): Promise<string> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
warnIfBundleIdentifierDefinedInAppConfigForGenericProject(projectDir, exp);
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(projectDir, exp);
const bundleIdentifier = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(projectDir, {
targetName,
@@ -107,7 +108,7 @@ async function configureBundleIdentifierAsync(
validate: value => (isBundleIdentifierValid(value) ? true : INVALID_BUNDLE_IDENTIFIER_MESSAGE),
});
const rawStaticConfig = await fs.readJSON(paths.staticConfigPath);
const rawStaticConfig = readAppJson(paths.staticConfigPath);
rawStaticConfig.expo = {
...rawStaticConfig.expo,
ios: { ...rawStaticConfig.expo?.ios, bundleIdentifier },
@@ -124,7 +125,7 @@ function isBundleIdentifierValid(bundleIdentifier: string): boolean {
}
let warnPrinted = false;
export function warnIfBundleIdentifierDefinedInAppConfigForGenericProject(
export function warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(
projectDir: string,
exp: ExpoConfig
): void {
@@ -132,7 +133,7 @@ export function warnIfBundleIdentifierDefinedInAppConfigForGenericProject(
Log.warn(
`Specifying "ios.bundleIdentifier" in ${getProjectConfigDescription(
projectDir
)} is deprecated for generic projects.\n` +
)} is deprecated for bare workflow projects.\n` +
'EAS Build depends only on the value in the native code. Please remove the deprecated configuration.'
);
warnPrinted = true;
+133 -11
View File
@@ -1,22 +1,26 @@
import { Platform } from '@expo/eas-build-job';
import chalk from 'chalk';
import prompts from 'prompts';
import { URL } from 'url';
import * as uuid from 'uuid';
import { BuildFragment } from '../graphql/generated';
import { AppPlatform, BuildFragment } from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log, { learnMore } from '../log';
import { appPlatformDisplayNames } from '../platform';
import { confirmAsync, promptAsync } from '../prompts';
import { getLatestBuildForSubmissionAsync } from './utils/builds';
import { getRecentBuildsForSubmissionAsync } from './utils/builds';
import { isExistingFileAsync, uploadAppArchiveAsync } from './utils/files';
export const BUILD_LIST_ITEM_COUNT = 4;
export enum ArchiveSourceType {
url,
latest,
path,
buildId,
buildList,
prompt,
}
@@ -46,6 +50,10 @@ interface ArchiveBuildIdSource extends ArchiveSourceBase {
id: string;
}
interface ArchiveBuildListSource extends ArchiveSourceBase {
sourceType: ArchiveSourceType.buildList;
}
interface ArchivePromptSource extends ArchiveSourceBase {
sourceType: ArchiveSourceType.prompt;
}
@@ -61,6 +69,7 @@ export type ArchiveSource =
| ArchiveLatestSource
| ArchivePathSource
| ArchiveBuildIdSource
| ArchiveBuildListSource
| ArchivePromptSource;
export async function getArchiveAsync(source: ArchiveSource): Promise<Archive> {
@@ -80,6 +89,9 @@ export async function getArchiveAsync(source: ArchiveSource): Promise<Archive> {
case ArchiveSourceType.buildId: {
return await handleBuildIdSourceAsync(source);
}
case ArchiveSourceType.buildList: {
return await handleBuildListSourceAsync(source);
}
}
}
@@ -113,7 +125,7 @@ async function handleUrlSourceAsync(source: ArchiveUrlSource): Promise<Archive>
async function handleLatestSourceAsync(source: ArchiveLatestSource): Promise<Archive> {
try {
const latestBuild = await getLatestBuildForSubmissionAsync(
const [latestBuild] = await getRecentBuildsForSubmissionAsync(
toAppPlatform(source.platform),
source.projectId
);
@@ -197,6 +209,116 @@ async function handleBuildIdSourceAsync(source: ArchiveBuildIdSource): Promise<A
}
}
async function handleBuildListSourceAsync(source: ArchiveBuildListSource): Promise<Archive> {
try {
const appPlatform = toAppPlatform(source.platform);
const expiryDate = new Date(); // artifacts expire after 30 days
expiryDate.setDate(expiryDate.getDate() - 30);
const recentBuilds = await getRecentBuildsForSubmissionAsync(appPlatform, source.projectId, {
limit: BUILD_LIST_ITEM_COUNT,
});
if (recentBuilds.length < 1) {
Log.error(
chalk.bold(
`Couldn't find any ${appPlatformDisplayNames[appPlatform]} builds for this project on EAS servers. ` +
"It looks like you haven't run 'eas build' yet."
)
);
return getArchiveAsync({
...source,
sourceType: ArchiveSourceType.prompt,
});
}
if (recentBuilds.every(it => new Date(it.updatedAt) < expiryDate)) {
Log.error(
chalk.bold(
'It looks like all of your build artifacts have expired. ' +
'EAS keeps your build artifacts only for 30 days.'
)
);
return getArchiveAsync({
...source,
sourceType: ArchiveSourceType.prompt,
});
}
const choices = recentBuilds.map(build => formatBuildChoice(build, expiryDate));
choices.push({
title: 'None of the above (select another option)',
value: null,
});
const { selectedBuild } = await promptAsync({
name: 'selectedBuild',
type: 'select',
message: 'Which build would you like to submit?',
choices: choices.map(choice => ({ ...choice, title: `- ${choice.title}` })),
// @ts-expect-error field documented in npm, but not defined in typescript
warn: 'This artifact has expired',
});
if (selectedBuild == null) {
return getArchiveAsync({
...source,
sourceType: ArchiveSourceType.prompt,
});
}
return {
build: selectedBuild,
source,
};
} catch (err) {
Log.error(err);
throw err;
}
}
function formatBuildChoice(build: BuildFragment, expiryDate: Date): prompts.Choice {
const {
id,
platform,
updatedAt,
appVersion,
sdkVersion,
runtimeVersion,
buildProfile,
appBuildVersion,
releaseChannel,
} = build;
const formatValue = (field?: string | null): string =>
field ? chalk.bold(field) : chalk.dim('Unknown');
const buildDate = new Date(updatedAt);
const maybeRuntimeVersion = runtimeVersion ? `Runtime: ${formatValue(runtimeVersion)}` : null;
const maybeSdkVersion = sdkVersion ? `SDK: ${formatValue(sdkVersion)}` : null;
const appBuildVersionString = `${
platform === AppPlatform.Android ? 'Version code' : 'Build number'
}: ${formatValue(appBuildVersion)}`;
const title = [
`ID: ${chalk.dim(id)}, Finished at: ${chalk.bold(buildDate.toLocaleString())}`,
[
`\tApp version: ${formatValue(appVersion)}, ${appBuildVersionString}`,
maybeRuntimeVersion,
maybeSdkVersion,
]
.filter(it => it != null)
.join(', '),
`\tProfile: ${formatValue(buildProfile)}, Release channel: ${formatValue(releaseChannel)}`,
].join('\n');
return {
title,
value: build,
disabled: buildDate < expiryDate,
};
}
async function handlePromptSourceAsync(source: ArchivePromptSource): Promise<Archive> {
const { sourceType: sourceTypeRaw } = await promptAsync({
name: 'sourceType',
@@ -204,16 +326,16 @@ async function handlePromptSourceAsync(source: ArchivePromptSource): Promise<Arc
message: 'What would you like to submit?',
choices: [
{
title: 'Latest finished build from EAS',
value: ArchiveSourceType.latest,
title: 'Select a build from EAS',
value: ArchiveSourceType.buildList,
},
{ title: 'I have a url to the app archive', value: ArchiveSourceType.url },
{ title: 'Provide a URL to the app archive', value: ArchiveSourceType.url },
{
title: 'Local app binary file',
title: 'Provide a path to a local app binary file',
value: ArchiveSourceType.path,
},
{
title: 'A build identified by a build ID',
title: 'Provide a build ID to identify a build on EAS',
value: ArchiveSourceType.buildId,
},
],
@@ -236,10 +358,10 @@ async function handlePromptSourceAsync(source: ArchivePromptSource): Promise<Arc
path,
});
}
case ArchiveSourceType.latest: {
case ArchiveSourceType.buildList: {
return getArchiveAsync({
...source,
sourceType: ArchiveSourceType.latest,
sourceType: ArchiveSourceType.buildList,
});
}
case ArchiveSourceType.buildId: {
@@ -250,7 +372,7 @@ async function handlePromptSourceAsync(source: ArchivePromptSource): Promise<Arc
id,
});
}
case ArchiveSourceType.prompt:
default:
throw new Error('This should never happen');
}
}
@@ -9,8 +9,13 @@ import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { toAppPlatform } from '../../graphql/types/AppPlatform';
import { confirmAsync, promptAsync } from '../../prompts';
import { uploadAsync } from '../../uploads';
import { Archive, ArchiveSourceType, getArchiveAsync } from '../ArchiveSource';
import { getLatestBuildForSubmissionAsync } from '../utils/builds';
import {
Archive,
ArchiveSourceType,
BUILD_LIST_ITEM_COUNT,
getArchiveAsync,
} from '../ArchiveSource';
import { getRecentBuildsForSubmissionAsync } from '../utils/builds';
jest.mock('fs');
jest.mock('../../log');
@@ -32,6 +37,7 @@ const MOCK_BUILD_FRAGMENT: Partial<BuildFragment> = {
},
appVersion: '1.2.3',
platform: AppPlatform.Android,
updatedAt: Date.now(),
};
const SOURCE_STUB_INPUT = {
@@ -140,7 +146,7 @@ describe(getArchiveAsync, () => {
it('handles latest build source', async () => {
const projectId = uuidv4();
asMock(getLatestBuildForSubmissionAsync).mockResolvedValueOnce(MOCK_BUILD_FRAGMENT);
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([MOCK_BUILD_FRAGMENT]);
const archive = await getArchiveAsync({
...SOURCE_STUB_INPUT,
@@ -148,7 +154,7 @@ describe(getArchiveAsync, () => {
sourceType: ArchiveSourceType.latest,
});
expect(getLatestBuildForSubmissionAsync).toBeCalledWith(
expect(getRecentBuildsForSubmissionAsync).toBeCalledWith(
toAppPlatform(SOURCE_STUB_INPUT.platform),
projectId
);
@@ -156,7 +162,7 @@ describe(getArchiveAsync, () => {
});
it('prompts again if no builds exists when selected latest', async () => {
asMock(getLatestBuildForSubmissionAsync).mockResolvedValueOnce(null);
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([]);
asMock(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
@@ -169,6 +175,59 @@ describe(getArchiveAsync, () => {
assertArchiveResult(archive, ArchiveSourceType.url);
});
it('handles build-list-select source', async () => {
const projectId = uuidv4();
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([MOCK_BUILD_FRAGMENT]);
asMock(promptAsync).mockResolvedValueOnce({ selectedBuild: MOCK_BUILD_FRAGMENT });
const archive = await getArchiveAsync({
...SOURCE_STUB_INPUT,
projectId,
sourceType: ArchiveSourceType.buildList,
});
expect(getRecentBuildsForSubmissionAsync).toBeCalledWith(
toAppPlatform(SOURCE_STUB_INPUT.platform),
projectId,
{ limit: BUILD_LIST_ITEM_COUNT }
);
assertArchiveResult(archive, ArchiveSourceType.buildList);
});
it('prompts again if all builds have expired', async () => {
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([
{
...MOCK_BUILD_FRAGMENT,
updatedAt: new Date(Date.now() - 31 * 24 * 3600 * 1000),
},
]);
asMock(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
const archive = await getArchiveAsync({
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.buildList,
});
assertArchiveResult(archive, ArchiveSourceType.url);
});
it('falls back to prompt if user selected "None of the above"', async () => {
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([MOCK_BUILD_FRAGMENT]);
asMock(promptAsync)
.mockResolvedValueOnce({ selectedBuild: null })
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
const archive = await getArchiveAsync({
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.buildList,
});
assertArchiveResult(archive, ArchiveSourceType.url);
});
it('handles path source', async () => {
const path = '/archive.apk';
await fs.writeFile(path, 'some content');
@@ -15,7 +15,7 @@ import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutatio
import { createTestProject } from '../../../project/__tests__/project-utils';
import { getProjectIdAsync } from '../../../project/projectUtils';
import { createSubmissionContextAsync } from '../../context';
import { getLatestBuildForSubmissionAsync } from '../../utils/builds';
import { getRecentBuildsForSubmissionAsync } from '../../utils/builds';
import AndroidSubmitCommand from '../AndroidSubmitCommand';
jest.mock('fs');
@@ -137,7 +137,7 @@ describe(AndroidSubmitCommand, () => {
it('assigns the build ID to submission', async () => {
const projectId = uuidv4();
asMock(getLatestBuildForSubmissionAsync).mockResolvedValueOnce(fakeBuildFragment);
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([fakeBuildFragment]);
const ctx = await createSubmissionContextAsync({
platform: Platform.ANDROID,
@@ -0,0 +1,78 @@
import fs from 'fs-extra';
import { promptForAscApiKeyAsync } from '../../credentials/ios/actions/AscApiKeyUtils';
import { AscApiKeyPath, MinimalAscApiKey } from '../../credentials/ios/credentials';
import Log from '../../log';
import { isExistingFileAsync } from '../utils/files';
export enum AscApiKeySourceType {
path,
prompt,
}
interface AscApiKeySourceBase {
sourceType: AscApiKeySourceType;
}
interface AscApiKeyPromptSource extends AscApiKeySourceBase {
sourceType: AscApiKeySourceType.prompt;
}
interface AscApiKeyEnvVarSource extends AscApiKeySourceBase {
sourceType: AscApiKeySourceType.path;
path: AscApiKeyPath;
}
export type AscApiKeySource = AscApiKeyEnvVarSource | AscApiKeyPromptSource;
type AscApiKeySummary = {
source: 'local' | 'EAS servers';
path?: string;
keyId: string;
};
export type AscApiKeyResult = {
result: MinimalAscApiKey;
summary: AscApiKeySummary;
};
export async function getAscApiKeyLocallyAsync(source: AscApiKeySource): Promise<AscApiKeyResult> {
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
const { keyP8Path, keyId, issuerId } = ascApiKeyPath;
const keyP8 = await fs.readFile(keyP8Path, 'utf-8');
return {
result: { keyP8, keyId, issuerId },
summary: {
source: 'local',
path: keyP8Path,
keyId,
},
};
}
export async function getAscApiKeyPathAsync(source: AscApiKeySource): Promise<AscApiKeyPath> {
switch (source.sourceType) {
case AscApiKeySourceType.path:
return await handlePathSourceAsync(source);
case AscApiKeySourceType.prompt:
return await handlePromptSourceAsync(source);
}
}
async function handlePathSourceAsync(source: AscApiKeyEnvVarSource): Promise<AscApiKeyPath> {
const { keyP8Path } = source.path;
if (!(await isExistingFileAsync(keyP8Path))) {
Log.warn(`File ${keyP8Path} doesn't exist.`);
return await getAscApiKeyPathAsync({ sourceType: AscApiKeySourceType.prompt });
}
return source.path;
}
async function handlePromptSourceAsync(_source: AscApiKeyPromptSource): Promise<AscApiKeyPath> {
const ascApiKeyPath = await promptForAscApiKeyAsync();
return await getAscApiKeyPathAsync({
sourceType: AscApiKeySourceType.path,
path: ascApiKeyPath,
});
}
@@ -4,6 +4,7 @@ import chalk from 'chalk';
import getenv from 'getenv';
import wrapAnsi from 'wrap-ansi';
import { MissingCredentialsError } from '../../credentials/errors';
import { SubmissionFragment } from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import { promptAsync } from '../../prompts';
@@ -16,6 +17,7 @@ import {
AppSpecificPasswordSource,
AppSpecificPasswordSourceType,
} from './AppSpecificPasswordSource';
import { AscApiKeySource, AscApiKeySourceType } from './AscApiKeySource';
import IosSubmitter, { IosSubmissionOptions } from './IosSubmitter';
export default class IosSubmitCommand {
@@ -28,15 +30,37 @@ export default class IosSubmitCommand {
return await submitter.submitAsync();
}
private async resolveCredentialSubmissionOptionsAsync(): Promise<
| { appSpecificPasswordSource: Result<AppSpecificPasswordSource> }
| { ascApiKeySource: Result<AscApiKeySource> }
> {
// Fall back to app specific password if no ascApiKey defined
const ascApiKeySource = this.resolveAscApiKeySource();
const shouldUseAppSpecificPassword =
!ascApiKeySource.ok && ascApiKeySource.enforceError() instanceof MissingCredentialsError;
if (shouldUseAppSpecificPassword) {
return { appSpecificPasswordSource: this.resolveAppSpecificPasswordSource() };
} else {
return { ascApiKeySource };
}
}
private async resolveSubmissionOptionsAsync(): Promise<IosSubmissionOptions> {
const archiveSource = this.resolveArchiveSource();
const appSpecificPasswordSource = this.resolveAppSpecificPasswordSource();
const credentialsSource = await this.resolveCredentialSubmissionOptionsAsync();
const maybeAppSpecificPasswordSource =
'appSpecificPasswordSource' in credentialsSource
? credentialsSource.appSpecificPasswordSource
: null;
const maybeAscApiKeySource =
'ascApiKeySource' in credentialsSource ? credentialsSource.ascApiKeySource : null;
const ascAppIdentifier = await this.resolveAscAppIdentifierAsync();
const appleIdUsername = await this.resolveAppleIdUsernameAsync();
const errored = [
archiveSource,
appSpecificPasswordSource,
...(maybeAppSpecificPasswordSource ? [maybeAppSpecificPasswordSource] : []),
...(maybeAscApiKeySource ? [maybeAscApiKeySource] : []),
ascAppIdentifier,
appleIdUsername,
].filter(r => !r.ok);
@@ -51,7 +75,16 @@ export default class IosSubmitCommand {
appleIdUsername: appleIdUsername.enforceValue(),
ascAppIdentifier: ascAppIdentifier.enforceValue(),
archiveSource: archiveSource.enforceValue(),
appSpecificPasswordSource: appSpecificPasswordSource.enforceValue(),
...(maybeAppSpecificPasswordSource
? {
appSpecificPasswordSource: maybeAppSpecificPasswordSource.enforceValue(),
}
: null),
...(maybeAscApiKeySource
? {
ascApiKeySource: maybeAscApiKeySource.enforceValue(),
}
: null),
};
}
@@ -72,6 +105,35 @@ export default class IosSubmitCommand {
}
}
private resolveAscApiKeySource(): Result<AscApiKeySource> {
const { ascApiKeyPath, ascApiKeyIssuerId, ascApiKeyId } = this.ctx.profile;
if (ascApiKeyPath && ascApiKeyIssuerId && ascApiKeyId) {
return result({
sourceType: AscApiKeySourceType.path,
path: {
keyP8Path: ascApiKeyPath,
issuerId: ascApiKeyIssuerId,
keyId: ascApiKeyId,
},
});
}
// interpret this to mean the user had some intention of passing in ASC Api key
if (ascApiKeyPath || ascApiKeyIssuerId || ascApiKeyId) {
Log.warn(`ascApiKeyPath, ascApiKeyIssuerId and ascApiKeyId must all be defined in eas.json`);
return result({
sourceType: AscApiKeySourceType.prompt,
});
}
return result(
new MissingCredentialsError(
'Set the ascApiKeyPath, ascApiKeyIssuerId and ascApiKeyId fields in eas.json.'
)
);
}
private resolveArchiveSource(): Result<ArchiveSource> {
try {
return result(resolveArchiveSource(this.ctx, Platform.IOS));
@@ -115,6 +177,10 @@ export default class IosSubmitCommand {
return result(envAppleId);
}
if (this.ctx.credentialsCtx.appStore.authCtx?.appleId) {
return result(this.ctx.credentialsCtx.appStore.authCtx.appleId);
}
// Get the email address that was last used and set it as
// the default value for quicker authentication.
const lastAppleId = await UserSettings.getAsync('appleId', null);
@@ -1,7 +1,9 @@
import { Platform } from '@expo/eas-build-job';
import chalk from 'chalk';
import { IosSubmissionConfigInput, 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 {
@@ -13,17 +15,20 @@ import {
AppSpecificPasswordSource,
getAppSpecificPasswordAsync,
} from './AppSpecificPasswordSource';
import { AscApiKeyResult, AscApiKeySource, getAscApiKeyLocallyAsync } from './AscApiKeySource';
export interface IosSubmissionOptions
extends Pick<IosSubmissionConfigInput, 'appleIdUsername' | 'ascAppIdentifier'> {
projectId: string;
archiveSource: ArchiveSource;
appSpecificPasswordSource: AppSpecificPasswordSource;
appSpecificPasswordSource?: AppSpecificPasswordSource;
ascApiKeySource?: AscApiKeySource;
}
interface ResolvedSourceOptions {
archive: Archive;
appSpecificPassword: string;
appSpecificPassword?: string;
ascApiKeyResult?: AscApiKeyResult;
}
export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmissionOptions> {
@@ -60,18 +65,22 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
private async resolveSourceOptionsAsync(): Promise<ResolvedSourceOptions> {
const archive = await getArchiveAsync(this.options.archiveSource);
const appSpecificPassword = await getAppSpecificPasswordAsync(
this.options.appSpecificPasswordSource
);
const maybeAppSpecificPassword = this.options.appSpecificPasswordSource
? await getAppSpecificPasswordAsync(this.options.appSpecificPasswordSource)
: null;
const maybeAppStoreConnectApiKey = this.options.ascApiKeySource
? await getAscApiKeyLocallyAsync(this.options.ascApiKeySource)
: null;
return {
archive,
appSpecificPassword,
...(maybeAppSpecificPassword ? { appSpecificPassword: maybeAppSpecificPassword } : null),
...(maybeAppStoreConnectApiKey ? { ascApiKeyResult: maybeAppStoreConnectApiKey } : null),
};
}
private async formatSubmissionConfigAsync(
options: IosSubmissionOptions,
{ archive, appSpecificPassword }: ResolvedSourceOptions
{ archive, appSpecificPassword, ascApiKeyResult }: ResolvedSourceOptions
): Promise<IosSubmissionConfigInput> {
const { appleIdUsername, ascAppIdentifier } = options;
return {
@@ -79,12 +88,21 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
appleIdUsername,
archiveUrl: archive.url,
appleAppSpecificPassword: appSpecificPassword,
...(ascApiKeyResult?.result
? {
ascApiKey: {
keyP8: ascApiKeyResult?.result.keyP8,
keyIdentifier: ascApiKeyResult?.result.keyId,
issuerIdentifier: ascApiKeyResult?.result.issuerId,
},
}
: null),
};
}
private prepareSummaryData(
options: IosSubmissionOptions,
{ archive }: ResolvedSourceOptions
{ archive, ascApiKeyResult }: ResolvedSourceOptions
): SummaryData {
const { appleIdUsername, ascAppIdentifier, projectId } = options;
@@ -93,6 +111,9 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
ascAppIdentifier,
appleIdUsername,
projectId,
...(ascApiKeyResult
? { formattedAscApiKey: formatServiceAccountSummary(ascApiKeyResult) }
: null),
...formatArchiveSourceSummary(archive),
};
}
@@ -102,6 +123,7 @@ type SummaryData = {
ascAppIdentifier: string;
appleIdUsername: string;
projectId: string;
formattedAscApiKey?: string;
} & ArchiveSourceSummaryFields;
const SummaryHumanReadableKeys: Record<keyof SummaryData, string> = {
@@ -111,4 +133,36 @@ const SummaryHumanReadableKeys: Record<keyof SummaryData, string> = {
archiveUrl: 'Archive URL',
archivePath: 'Archive Path',
formattedBuild: 'Build',
formattedAscApiKey: 'App Store Connect Api Key',
};
function formatServiceAccountSummary({ summary }: AscApiKeyResult): string {
const { source, path, keyId } = summary;
const fields = [
{
label: 'Key Source',
value: source,
},
{
label: 'Key Path',
value: path,
},
{
label: 'Key ID',
value: keyId,
},
];
const filteredFields = fields.filter(({ value }) => value !== undefined && value !== null) as {
label: string;
value: string;
}[];
return (
'\n' +
formatFields(filteredFields, {
labelFormat: label => ` ${chalk.dim(label)}:`,
})
);
}
@@ -0,0 +1,174 @@
import { vol } from 'memfs';
import { asMock } from '../../../__tests__/utils';
import { getCredentialsFromUserAsync } from '../../../credentials/utils/promptForCredentials';
import { promptAsync } from '../../../prompts';
import {
AscApiKeySource,
AscApiKeySourceType,
getAscApiKeyLocallyAsync,
getAscApiKeyPathAsync,
} from '../AscApiKeySource';
jest.mock('fs');
jest.mock('../../../prompts');
jest.mock('../../../credentials/utils/promptForCredentials');
beforeAll(() => {
vol.fromJSON({
'/asc-api-key.p8': 'super secret',
'/project_dir/subdir/asc-api-key.p8': 'super secret',
'/project_dir/another-asc-api-key.p8': 'super secret',
});
});
afterAll(() => {
vol.reset();
});
afterEach(() => {
asMock(promptAsync).mockClear();
});
describe(getAscApiKeyPathAsync, () => {
describe('when source is AscApiKeySourceType.path', () => {
it("prompts for path if the provided file doesn't exist", async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementationOnce(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/doesnt-exist.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
expect(promptAsync).toHaveBeenCalled();
expect(ascApiKeyPath).toEqual({
keyP8Path: '/asc-api-key.p8',
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
});
});
it("doesn't prompt for path if the provided file exists", async () => {
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/asc-api-key.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
await getAscApiKeyPathAsync(source);
expect(promptAsync).not.toHaveBeenCalled();
});
it('returns the provided file path if the file exists', async () => {
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/asc-api-key.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
expect(ascApiKeyPath).toEqual({
issuerId: 'test-issuer-id',
keyId: 'test-key-id',
keyP8Path: '/asc-api-key.p8',
});
});
});
describe('when source is AscApiKeySourceType.prompt', () => {
it('prompts for path', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementationOnce(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.prompt,
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
expect(promptAsync).toHaveBeenCalled();
expect(ascApiKeyPath).toEqual({
keyP8Path: '/asc-api-key.p8',
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
});
});
it('prompts for path until the user provides an existing file', async () => {
asMock(promptAsync)
.mockImplementationOnce(() => ({
keyP8Path: '/doesnt-exist.p8',
}))
.mockImplementationOnce(() => ({
keyP8Path: '/blah.p8',
}))
.mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.prompt,
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
expect(promptAsync).toHaveBeenCalledTimes(3);
expect(ascApiKeyPath).toEqual({
keyP8Path: '/asc-api-key.p8',
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
});
});
});
});
describe(getAscApiKeyLocallyAsync, () => {
it('returns a local Asc Api Key file with a AscApiKeySourceType.path source', async () => {
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/asc-api-key.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
const ascApiKeyResult = await getAscApiKeyLocallyAsync(source);
expect(ascApiKeyResult).toMatchObject({
result: {
keyP8: 'super secret',
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
},
summary: {
source: 'local',
path: '/asc-api-key.p8',
keyId: 'test-key-id',
},
});
});
it('returns a local Asc Api Key file with a AscApiKeySourceType.prompt source', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementationOnce(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.prompt,
};
const serviceAccountResult = await getAscApiKeyLocallyAsync(source);
expect(serviceAccountResult).toMatchObject({
result: {
keyP8: 'super secret',
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
},
summary: {
source: 'local',
path: '/asc-api-key.p8',
keyId: 'test-key-id',
},
});
});
});
+8 -7
View File
@@ -1,16 +1,17 @@
import { AppPlatform, BuildFragment, BuildStatus } from '../../graphql/generated';
import { AppPlatform, BuildFragment, BuildStatus, DistributionType } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
export async function getLatestBuildForSubmissionAsync(
export async function getRecentBuildsForSubmissionAsync(
platform: AppPlatform,
appId: string
): Promise<BuildFragment | null> {
const [build] = await BuildQuery.allForAppAsync(appId, {
limit: 1,
appId: string,
{ limit = 1 }: { limit?: number } = {}
): Promise<BuildFragment[]> {
return await BuildQuery.allForAppAsync(appId, {
limit,
filter: {
platform,
distribution: DistributionType.Store,
status: BuildStatus.Finished,
},
});
return build;
}
@@ -0,0 +1,76 @@
import { Platform } from '@expo/eas-build-job';
import { getProfilesAsync } from '../profiles';
describe(getProfilesAsync, () => {
test('defaults to production profile', async () => {
const callback = jest.fn();
const result = await getProfilesAsync({
platforms: [Platform.ANDROID, Platform.IOS],
profileName: undefined,
readProfileAsync: callback,
});
expect(result[0].profileName).toBe('production');
expect(result[1].profileName).toBe('production');
expect(callback).toBeCalledWith(Platform.ANDROID, 'production');
expect(callback).toBeCalledWith(Platform.IOS, 'production');
});
test('defaults to release profile when production profile is non-existent', async () => {
const callback = jest
.fn()
.mockRejectedValueOnce(() => {
throw new Error();
})
.mockRejectedValueOnce(() => {
throw new Error();
});
const result = await getProfilesAsync({
platforms: [Platform.ANDROID, Platform.IOS],
profileName: undefined,
readProfileAsync: callback,
});
expect(result[0].profileName).toBe('release');
expect(result[1].profileName).toBe('release');
expect(callback).toBeCalledWith(Platform.ANDROID, 'production');
expect(callback).toBeCalledWith(Platform.IOS, 'production');
expect(callback).toBeCalledWith(Platform.ANDROID, 'release');
expect(callback).toBeCalledWith(Platform.IOS, 'release');
});
test('fails when neither production or release profiles are present', async () => {
const callback = jest
.fn()
.mockRejectedValueOnce(() => {
throw new Error();
})
.mockRejectedValueOnce(() => {
throw new Error();
});
await expect(
getProfilesAsync({
platforms: [Platform.ANDROID],
profileName: undefined,
readProfileAsync: callback,
})
).rejects.toThrowError(/There is no profile named "production" in eas.json/);
});
test('gets a specific profile', async () => {
const callback = jest.fn();
const result = await getProfilesAsync({
platforms: [Platform.ANDROID, Platform.IOS],
profileName: 'custom-profile',
readProfileAsync: callback,
});
expect(result[0].profileName).toBe('custom-profile');
expect(result[1].profileName).toBe('custom-profile');
expect(callback).toBeCalledWith(Platform.ANDROID, 'custom-profile');
expect(callback).toBeCalledWith(Platform.IOS, 'custom-profile');
});
});
+52
View File
@@ -0,0 +1,52 @@
import { Platform } from '@expo/eas-build-job';
import Log from '../log';
export type ProfileData<T> = {
profile: T;
platform: Platform;
profileName: string;
};
export async function getProfilesAsync<T>({
platforms,
profileName: profileNameArg,
// eslint-disable-next-line async-protect/async-suffix
readProfileAsync,
}: {
platforms: Platform[];
profileName?: string | null;
readProfileAsync: (platform: Platform, profileName: string) => Promise<T>;
}): Promise<ProfileData<T>[]> {
const results = platforms.map(async function (platform) {
let profile;
let profileName = profileNameArg;
if (!profileName) {
try {
profile = await readProfileAsync(platform, 'production');
profileName = 'production';
} catch (error) {
try {
profile = await readProfileAsync(platform, 'release');
profileName = 'release';
Log.warn(
'The default profile changed from "release" to "production". We detected that you still have a "release" build profile, so we are using it. Update eas.json to have a profile named "production" under the `build` key, or specify which profile you\'d like to use with the --profile flag. This fallback behavior will be removed in the next major version of EAS CLI.'
);
} catch (error) {
throw new Error('There is no profile named "production" in eas.json');
}
}
} else {
profile = await readProfileAsync(platform, profileName);
}
return {
profile,
profileName,
platform,
};
});
return await Promise.all(results);
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with the eas.json",
"version": "0.31.0",
"version": "0.33.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
+9 -3
View File
@@ -81,18 +81,24 @@ export class EasJsonReader {
public async readSubmitProfileAsync<T extends Platform>(
platform: T,
profileName?: string
profileNameArg?: string
): Promise<SubmitProfile<T>> {
let profileName = profileNameArg;
if (!profileName) {
const profileNames = await this.getSubmitProfileNamesAsync({
throwIfEasJsonDoesNotExist: false,
});
if (profileNames.includes('release')) {
return await this.readSubmitProfileAsync(platform, 'release');
if (profileNames.includes('production')) {
profileName = 'production';
} else if (profileNames.includes('release')) {
profileName = 'release';
} else {
return getDefaultSubmitProfile(platform);
}
}
const easJson = await this.readAndValidateAsync();
const profile = easJson?.submit?.[profileName];
if (!profile) {
+3
View File
@@ -90,6 +90,9 @@ export const AndroidSubmitProfileSchema = Joi.object({
});
export const IosSubmitProfileSchema = Joi.object({
ascApiKeyPath: Joi.string(),
ascApiKeyId: Joi.string(),
ascApiKeyIssuerId: Joi.string(),
appleId: Joi.string(),
ascAppId: Joi.string(),
appleTeamId: Joi.string(),
+8 -1
View File
@@ -26,6 +26,9 @@ export const AndroidSubmitProfileFieldsToEvaluate: (keyof AndroidSubmitProfile)[
];
export interface IosSubmitProfile {
ascApiKeyPath?: string;
ascApiKeyIssuerId?: string;
ascApiKeyId?: string;
appleId?: string;
ascAppId?: string;
appleTeamId?: string;
@@ -35,7 +38,11 @@ export interface IosSubmitProfile {
appName?: string;
}
export const IosSubmitProfileFieldsToEvaluate: (keyof IosSubmitProfile)[] = [];
export const IosSubmitProfileFieldsToEvaluate: (keyof IosSubmitProfile)[] = [
'ascApiKeyPath',
'ascApiKeyIssuerId',
'ascApiKeyId',
];
export type SubmitProfile<TPlatform extends Platform = Platform> =
TPlatform extends Platform.ANDROID
@@ -14,13 +14,13 @@ beforeEach(async () => {
test('minimal valid eas.json for both platforms', async () => {
await fs.writeJson('/project/eas.json', {
build: {
release: {},
production: {},
},
});
const reader = new EasJsonReader('/project');
const iosProfile = await reader.readBuildProfileAsync(Platform.IOS, 'release');
const androidProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'release');
const iosProfile = await reader.readBuildProfileAsync(Platform.IOS, 'production');
const androidProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'production');
expect({
distribution: 'store',
@@ -36,7 +36,7 @@ test('minimal valid eas.json for both platforms', async () => {
test('valid eas.json for development client builds', async () => {
await fs.writeJson('/project/eas.json', {
build: {
release: {},
production: {},
debug: {
developmentClient: true,
android: {
@@ -219,7 +219,7 @@ test('valid profile extending other profile with platform specific caching', asy
test('valid eas.json with missing profile', async () => {
await fs.writeJson('/project/eas.json', {
build: {
release: {},
production: {},
},
});
@@ -231,14 +231,14 @@ test('valid eas.json with missing profile', async () => {
test('invalid eas.json when using wrong buildType', async () => {
await fs.writeJson('/project/eas.json', {
build: {
release: { android: { buildType: 'archive' } },
production: { android: { buildType: 'archive' } },
},
});
const reader = new EasJsonReader('/project');
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'release');
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'production');
await expect(promise).rejects.toThrowError(
'eas.json is not valid [ValidationError: "build.release.android.buildType" must be one of [apk, app-bundle]]'
'eas.json is not valid [ValidationError: "build.production.android.buildType" must be one of [apk, app-bundle]]'
);
});
@@ -246,33 +246,33 @@ test('empty json', async () => {
await fs.writeJson('/project/eas.json', {});
const reader = new EasJsonReader('/project');
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'release');
await expect(promise).rejects.toThrowError('There is no profile named release in eas.json');
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'production');
await expect(promise).rejects.toThrowError('There is no profile named production in eas.json');
});
test('invalid semver value', async () => {
await fs.writeJson('/project/eas.json', {
build: {
release: { node: '12.0.0-alpha' },
production: { node: '12.0.0-alpha' },
},
});
const reader = new EasJsonReader('/project');
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'release');
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'production');
await expect(promise).rejects.toThrowError(
'eas.json is not valid [ValidationError: "build.release.node" failed custom validation because 12.0.0-alpha is not a valid version]'
'eas.json is not valid [ValidationError: "build.production.node" failed custom validation because 12.0.0-alpha is not a valid version]'
);
});
test('get profile names', async () => {
await fs.writeJson('/project/eas.json', {
build: {
release: { node: '12.0.0-alpha' },
production: { node: '12.0.0-alpha' },
blah: { node: '12.0.0-alpha' },
},
});
const reader = new EasJsonReader('/project');
const allProfileNames = await reader.getBuildProfileNamesAsync();
expect(allProfileNames.sort()).toEqual(['blah', 'release'].sort());
expect(allProfileNames.sort()).toEqual(['blah', 'production'].sort());
});
@@ -14,13 +14,13 @@ beforeEach(async () => {
test('minimal allowed eas.json for both platforms', async () => {
await fs.writeJson('/project/eas.json', {
submit: {
release: {},
production: {},
},
});
const reader = new EasJsonReader('/project');
const iosProfile = await reader.readSubmitProfileAsync(Platform.IOS, 'release');
const androidProfile = await reader.readSubmitProfileAsync(Platform.ANDROID, 'release');
const iosProfile = await reader.readSubmitProfileAsync(Platform.IOS, 'production');
const androidProfile = await reader.readSubmitProfileAsync(Platform.ANDROID, 'production');
expect(androidProfile).toEqual({
changesNotSentForReview: false,
@@ -35,7 +35,7 @@ test('minimal allowed eas.json for both platforms', async () => {
test('android config with all required values', async () => {
await fs.writeJson('/project/eas.json', {
submit: {
release: {
production: {
android: {
serviceAccountKeyPath: './path.json',
track: 'beta',
@@ -46,7 +46,7 @@ test('android config with all required values', async () => {
});
const reader = new EasJsonReader('/project');
const androidProfile = await reader.readSubmitProfileAsync(Platform.ANDROID, 'release');
const androidProfile = await reader.readSubmitProfileAsync(Platform.ANDROID, 'production');
expect(androidProfile).toEqual({
serviceAccountKeyPath: './path.json',
@@ -59,7 +59,7 @@ test('android config with all required values', async () => {
test('android config with serviceAccountKeyPath set to env var', async () => {
await fs.writeJson('/project/eas.json', {
submit: {
release: {
production: {
android: {
serviceAccountKeyPath: '$GOOGLE_SERVICE_ACCOUNT',
track: 'beta',
@@ -72,7 +72,7 @@ test('android config with serviceAccountKeyPath set to env var', async () => {
try {
process.env.GOOGLE_SERVICE_ACCOUNT = './path.json';
const reader = new EasJsonReader('/project');
const androidProfile = await reader.readSubmitProfileAsync(Platform.ANDROID, 'release');
const androidProfile = await reader.readSubmitProfileAsync(Platform.ANDROID, 'production');
expect(androidProfile).toEqual({
serviceAccountKeyPath: './path.json',
@@ -88,23 +88,68 @@ test('android config with serviceAccountKeyPath set to env var', async () => {
test('ios config with all required values', async () => {
await fs.writeJson('/project/eas.json', {
submit: {
release: {
production: {
ios: {
appleId: '[email protected]',
ascAppId: '1223423523',
appleTeamId: 'QWERTY',
ascApiKeyPath: './path-ABCD.p8',
ascApiKeyIssuerId: 'abc-123-def-456',
ascApiKeyId: 'ABCD',
},
},
},
});
const reader = new EasJsonReader('/project');
const iosProfile = await reader.readSubmitProfileAsync(Platform.IOS, 'release');
const iosProfile = await reader.readSubmitProfileAsync(Platform.IOS, 'production');
expect(iosProfile).toEqual({
appleId: '[email protected]',
appleTeamId: 'QWERTY',
ascAppId: '1223423523',
ascApiKeyPath: './path-ABCD.p8',
ascApiKeyIssuerId: 'abc-123-def-456',
ascApiKeyId: 'ABCD',
language: 'en-US',
});
});
test('ios config with ascApiKey fields set to env var', async () => {
await fs.writeJson('/project/eas.json', {
submit: {
release: {
ios: {
appleId: '[email protected]',
ascAppId: '1223423523',
appleTeamId: 'QWERTY',
ascApiKeyPath: '$ASC_API_KEY_PATH',
ascApiKeyIssuerId: '$ASC_API_KEY_ISSUER_ID',
ascApiKeyId: '$ASC_API_KEY_ID',
},
},
},
});
try {
process.env.ASC_API_KEY_PATH = './path-ABCD.p8';
process.env.ASC_API_KEY_ISSUER_ID = 'abc-123-def-456';
process.env.ASC_API_KEY_ID = 'ABCD';
const reader = new EasJsonReader('/project');
const iosProfile = await reader.readSubmitProfileAsync(Platform.IOS, 'release');
expect(iosProfile).toEqual({
appleId: '[email protected]',
ascAppId: '1223423523',
appleTeamId: 'QWERTY',
ascApiKeyPath: './path-ABCD.p8',
ascApiKeyIssuerId: 'abc-123-def-456',
ascApiKeyId: 'ABCD',
language: 'en-US',
});
} finally {
process.env.ASC_API_KEY_PATH = undefined;
process.env.ASC_API_KEY_ISSUER_ID = undefined;
process.env.ASC_API_KEY_ID = undefined;
}
});
@@ -35,7 +35,7 @@ test('migration for default manged eas.json', async () => {
});
});
test('migration for default generic eas.json', async () => {
test('migration for default bare workflow eas.json', async () => {
const easJson = {
builds: {
android: {
@@ -194,7 +194,7 @@ test('migration for example manged eas.json', async () => {
});
});
test('migration for example generic eas.json', async () => {
test('migration for example bare workflow eas.json', async () => {
const easJson = {
builds: {
android: {
Generated
+22 -22
View File
@@ -848,10 +848,10 @@
mv "~2"
safe-json-stringify "~1"
"@expo/[email protected].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==
"@expo/[email protected].3":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-4.0.3.tgz#8680dea2709c70f3a9c9e77ca6057335758341a8"
integrity sha512-vJ+4YOXxAjznpIG0WQHNsbeeJQXS/ZxzVPHhOO3le8Lkl+WJ2eH7XypulPRalBI4E0WLFtzDzLIdnqYJyjzvaw==
dependencies:
"@expo/config-types" "^43.0.0"
"@expo/json-file" "8.2.33"
@@ -867,20 +867,20 @@
semver "^7.3.5"
slash "^3.0.0"
xcode "^3.0.1"
xml2js "^0.4.23"
xml2js "0.4.23"
"@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/[email protected].2":
version "6.0.2"
resolved "https://registry.yarnpkg.com/@expo/config/-/config-6.0.2.tgz#d778be85a6b9f9fdb1f453464ba4c3420ba410d1"
integrity sha512-UjNlbs3gYfYnb+8If5dijepRz2+J8sXdmF9bvGQ2LUULdpO0Bx7EbLErctzrFXlK3j7jw1esPWWNfVq5wIlEwg==
"@expo/[email protected].3":
version "6.0.3"
resolved "https://registry.yarnpkg.com/@expo/config/-/config-6.0.3.tgz#48e5d453ae00ca090ea47799f4a9f352d5136a89"
integrity sha512-Qo2OVDfKbCyLiG3g2vqrPQXBBGRB2I3ADIB/8nZ3Ypy7QhqOA1L7WYhTQQr3T1zknHTMzy7u61ArHweiAnEEpw==
dependencies:
"@babel/code-frame" "~7.10.4"
"@expo/config-plugins" "4.0.2"
"@expo/config-plugins" "4.0.3"
"@expo/config-types" "^43.0.0"
"@expo/json-file" "8.2.33"
getenv "^1.0.0"
@@ -899,10 +899,10 @@
joi "^17.4.2"
semver "^7.3.5"
"@expo/[email protected]6":
version "0.3.16"
resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.3.16.tgz#df4b38b728d29ae8bed686866dc264a2d64577e6"
integrity sha512-ZggQK5w7awqYdA/TE0DT02nYxWirQm2r7NNy043zVtzBCtjhLpFpluk1v9W0pH4+nT1ChGk1c67j0mYRKcBkjg==
"@expo/[email protected]7":
version "0.3.17"
resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.3.17.tgz#75e2606749ffa1284de570245f668503e4a06c3f"
integrity sha512-zaOj24JK5F+pPGpULP8D9aAbApcR7ixV2GjT43YzZqjHuyxQ5knI+EsbmZ1TNnA1vNJJKMYrigB5+chcUGwkpw==
dependencies:
"@expo/spawn-async" "1.5.0"
chalk "^4.0.0"
@@ -985,15 +985,15 @@
lodash.template "^4.4.0"
semver "^7.3.2"
"@expo/[email protected].2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-3.0.2.tgz#8b0fae285efbfede82d6b3aaee96ea8ea10307de"
integrity sha512-iEKcjbbwrICRgqAPNcyI3g2MkZx3gkmD9KE7flfWJwC+6XzYC8DBz9vN5mVAu8GX3S9H16sC2tF4ZspVC6WHDg==
"@expo/[email protected].3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-3.0.3.tgz#448f48aab2a28af6b12dc1a95696dbd08f5668f3"
integrity sha512-7r/Wfzs2QNm8RvnbT1LFZ+Xkb3vrcLVGwkRt1BHPxmRFwq4SNwhkMyLPopQEjUhJU3AqLn6EKItS39+9c2f8vg==
dependencies:
"@expo/config" "6.0.2"
"@expo/config-plugins" "4.0.2"
"@expo/config" "6.0.3"
"@expo/config-plugins" "4.0.3"
"@expo/config-types" "^43.0.0"
"@expo/image-utils" "0.3.16"
"@expo/image-utils" "0.3.17"
"@expo/json-file" "8.2.33"
debug "^4.3.1"
fs-extra "^9.0.0"
@@ -10939,7 +10939,7 @@ xml-name-validator@^3.0.0:
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
xml2js@^0.4.23:
[email protected]:
version "0.4.23"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==