Compare commits

...
40 Commits
Author SHA1 Message Date
Dominik Sokal 7b202f2aa7 v0.6.0 2021-03-09 10:42:59 +01:00
Dominik SokalandWojciech Kozyra fa6e9c1735 [ENG-444] support internal distribution in non-interactive builds (#269)
* support internal distribution in non-interactive builds

* update CHANGELOG.md

* address PR feedback

* Update packages/eas-cli/src/credentials/ios/actions/new/SetupAdhocProvisioningProfile.ts

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

* address more PR feedback

* update error message

Co-authored-by: Wojciech Kozyra <[email protected]>
2021-03-09 10:40:52 +01:00
Wojciech KozyraandDominik Sokal c3300036d8 [eas-cli] handle unknown team name in device list (#268)
* [eas-cli] handle unknown team name in device list

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-03-05 14:05:06 +01:00
Dominik Sokal 5efcdb2940 [eas-cli] use Metadata type from @expo/eas-build-job (#267) 2021-03-05 09:43:23 +01:00
Wojciech Kozyra 8113450ea4 [eas-json] add experimental.disableIosBundleIdentifierValidation flag (#263) 2021-03-04 15:12:45 +01:00
Dominik Sokal d10b0c3c65 iOS builds: default to build configuration 'Release' (#266)
* iOS builds: default to build configuration 'Release'

* update CHANGELOG.md

* fix tests
2021-03-04 10:25:59 +01:00
Bartosz Kaszubowski e8de0e8712 add installation section to eas-cli readme (#255) 2021-03-03 16:53:50 -08:00
Bartłomiej Klocek e1da1099da [eas submit] UX improvements (#144)
* Add finish message for iOS Submissions

* Improve summary table

* Improve table look

* Update CHANGELOG
2021-03-03 16:57:08 +01:00
Dominik Sokal 264aa8fefa [ENG-326] send more build metadata: release channel, build profile, git commit hash (#265)
* send more build metadata: release channel, build profile, git commit hash

* update CHANGELOG.md

* address PR feedback

* upgrade @expo/config-plugins 1.0.19-alpha.0 -> 1.0.21-alpha.0

* address PR feedback
2021-03-03 16:00:59 +01:00
jkh 941d0294b7 [eas-cli] use generated graphql types (#262) 2021-03-03 06:53:54 -08:00
Wojciech Kozyra 366f4e8914 [eas-cli] display size of the archive (#264) 2021-03-03 14:11:31 +01:00
Dominik Sokal 1460c3c5a2 add first item to the PR checklist (#260) 2021-03-03 09:16:37 +01:00
jkh 6271472827 [eas-cli] add branch:delete command (#248) 2021-03-02 09:43:53 -08:00
Dominik Sokal 3bc4a054ae update CHANGELOG.md 2021-03-02 11:35:19 +01:00
Dominik Sokal dd85d559d7 v0.5.0 2021-03-02 11:14:43 +01:00
Wojciech Kozyra ab10b082e5 [eas-cli] fix submit with tar.gz files (#257)
* [eas-cli] fix submit with tar.gz files

* [eas-cli] update CHANGELOG.md
2021-03-02 10:52:19 +01:00
Dominik Sokal 60b60066f9 enable internal distribution support for self-managed credentials (#256)
* enable internal distribution support for self-managed credentials

* update CHANGELOG.md

* address PR feedback

* fix typo
2021-03-02 10:49:59 +01:00
Wojciech Kozyra 9e9e517698 [eas-cli] detect untracked files when checking for changes in repository (#259)
* [eas-cli] detect untracked files when checking for changes in repository

* Update CHANGELOG.md
2021-03-02 10:49:27 +01:00
Wojciech Kozyra 020c0f2b98 [eas-cli] commit all files before build (#251)
* [eas-cli] commit all files before build

* Update CHANGELOG.md
2021-03-01 10:08:25 +01:00
Wojciech Kozyra dd74ebdc51 [eas-cli] use error.expoApiV2ErrorCode to check error code (#252) 2021-03-01 09:24:22 +01:00
Wojciech KozyraandDominik Sokal ddf7381efb [eas-cli] EAS Build - build cache support (#247)
* [eas-cli] EAS Build - build cache support

* Update CHANGELOG

* Update CHANGELOG.md

Co-authored-by: Dominik Sokal <[email protected]>
2021-03-01 09:24:05 +01:00
Cedric van Puttenandjkh 3b67b912e4 [eas-cli] Add channel edit list and view commands (#249)
* [eas-cli] add channel:view command

* [eas-cli] add channel:list and channel:edit commands

* [eas-cli] Fix styling issue in channel edit command

* [eas-cli] Assume single branch and update when listing

* [eas-cli] Move graphql queries back to command files

* [eas-cli] Cleam up graphql limits

* [eas-cli] Show current branch when editing channel

* [eas-cli] Reword channel:edit description

Co-authored-by: jkh <[email protected]>

* [eas-cli] Reword channel:edit question

Co-authored-by: jkh <[email protected]>

* [eas-cli] Update table output of channel commands

* [eas-cli] Remove limit from properties and hardcode limit

Co-authored-by: jkh <[email protected]>

* [eas-cli] Reformat todos to fit repo better

Co-authored-by: jkh <[email protected]>

* [eas-cli] Remove limit from channel list query

Co-authored-by: jkh <[email protected]>
2021-02-27 14:54:02 +01:00
Cedric van Putten 37d7fa4510 [eas-cli] Fix json output for branch and channel commands (#250) 2021-02-27 14:30:03 +01:00
jkh 096601473a [eas-cli] fix tsc in prompt.ts (#254) 2021-02-26 07:42:28 -08:00
jkh 2e25ed81ca [eas-cli] rename fileHash to fileSHA256 (#244) 2021-02-23 04:35:48 -08:00
jkh 4e11ea1d77 [eas-cli] add default bundler for branch:publish (#237) 2021-02-23 04:28:37 -08:00
Dominik Sokal d9d470426b upgrade @expo/eas-build-job 0.2.12 -> 0.2.13 (#245)
* upgrade @expo/eas-build-job 0.2.12 -> 0.2.13

* fix tests

* update CHANGELOG.md
2021-02-23 12:52:24 +01:00
Dominik Sokal 119b4ad8b2 update CHANGELOG.md 2021-02-23 11:26:26 +01:00
Dominik Sokal ae1e9f000d update README.md 2021-02-23 11:25:46 +01:00
Dominik Sokal eb0505f8f8 v0.4.3 2021-02-23 11:12:02 +01:00
Dominik Sokal c8f7800ab5 update CHANGELOG.md 2021-02-23 11:10:15 +01:00
Wojciech Kozyra bc4e7becdd [eas-cli] use fixed version of @expo/eas-json (#243) 2021-02-22 15:03:06 +01:00
jkh 71ece19c40 [eas-cli] rename updateGroup, updateMessage, branchName, channelName (#242) 2021-02-19 12:07:39 -08:00
Dominik Sokal e9053c0d15 add support for ios simulator builds (#240)
* add support for ios simulator builds

* update CHANGELOG.md

* bump @expo/eas-build-job to 0.2.12

* fix lint errors
2021-02-19 13:35:06 +01:00
Dominik Sokal cf76296142 update CHANGELOG.md 2021-02-18 11:43:33 +01:00
Dominik Sokal 5e5d3626f4 v0.4.2 2021-02-18 11:36:14 +01:00
Dominik Sokal 5808c5b412 add CHANGELOG.md (#239) 2021-02-18 11:30:13 +01:00
Dominik Sokal cd64392d44 [eas-cli] bump @expo/config-plugins to 1.0.19-alpha.0 (#238) 2021-02-18 11:20:39 +01:00
Wojciech Kozyra 79f889e16c v0.4.1 2021-02-16 19:17:33 +01:00
Wojciech Kozyra 595bfecf1c [eas-cli] fix "buildType" is not allowed error 2021-02-16 19:15:10 +01:00
77 changed files with 2633 additions and 844 deletions

No files matched your search

+6
View File
@@ -1,3 +1,9 @@
<!-- Thanks for contributing to _EAS CLI_! Before you submit your pull request, please make sure to check the following boxes by putting an x in the [ ] (don't: [x ], [ x], do: [x]) -->
# Checklist
- [ ] I've added an entry to [CHANGELOG.md](https://github.com/expo/eas-cli/blob/main/CHANGELOG.md#main) if necessary.
# Why
Please describe the motivation for this PR, and link to relevant GitHub issues, forums posts, or feature requests.
+76
View File
@@ -0,0 +1,76 @@
# Changelog
This is the log of notable changes to EAS CLI and related packages.
## main
### 🛠 Breaking changes
- Generic iOS projects: build release builds by default. ([#266](https://github.com/expo/eas-cli/pull/266) by [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Log the size of the archived project when uploading. ([#264](https://github.com/expo/eas-cli/pull/264) by [@wkozyra95](https://github.com/wkozyra95))
- Add more build metadata (release channel, build profile name, git commit hash). ([#265](https://github.com/expo/eas-cli/pull/265) by [@dsokal](https://github.com/dsokal))
- Display App Store link after successful submission. ([#144](https://github.com/expo/eas-cli/pull/144) by [@barthap](https://github.com/barthap))
- Add `experimental.disableIosBundleIdentifierValidation` flag to eas.json. ([#263](https://github.com/expo/eas-cli/pull/263) by [@wkozyra95](https://github.com/wkozyra95))
- Support internal distribution in non-interactive builds. ([#269](https://github.com/expo/eas-cli/pull/269) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Print Apple Team ID in the output of `device:list` when the team name is unknown. ([#268](https://github.com/expo/eas-cli/pull/268) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
## [0.5.0](https://github.com/expo/eas-cli/releases/tag/v0.5.0) - 2021-03-02
### 🎉 New features
- Add support for build cache. ([#247](https://github.com/expo/eas-cli/pull/247) by [@wkozyra95](https://github.com/wkozyra95))
- Enable internal distribution support for self-managed credentials. ([#256](https://github.com/expo/eas-cli/pull/256) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Make sure all files are committed before build. ([#251](https://github.com/expo/eas-cli/pull/251) by [@wkozyra95](https://github.com/wkozyra95))
- Fix `eas submit` support for tar.gz files. ([#257](https://github.com/expo/eas-cli/pull/257)) by [@wkozyra95](https://github.com/wkozyra95))
- Show untracked files when checking `git status`. ([#259](https://github.com/expo/eas-cli/pull/259) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Upgrade `@expo/eas-build-job` from `0.2.12` to `0.2.13`. ([#245](https://github.com/expo/eas-cli/pull/245) by [@dsokal](https://github.com/dsokal))
## [0.4.3](https://github.com/expo/eas-cli/releases/tag/v0.4.3) - 2021-02-23
### 🎉 New features
- Add support for iOS simulator builds. ([#240](https://github.com/expo/eas-cli/pull/240) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Use fixed version of `@expo/eas-json`. ([#243](https://github.com/expo/eas-cli/pull/243) by [@wkozyra95](https://github.com/wkozyra95))
## [0.4.2](https://github.com/expo/eas-cli/releases/tag/v0.4.2) - 2021-02-18
### 🐛 Bug fixes
- Fix detecting application target (iOS builds). ([#238](https://github.com/expo/eas-cli/pull/238) by [@dsokal](https://github.com/dsokal))
## [0.4.1](https://github.com/expo/eas-cli/releases/tag/v0.4.1) - 2021-02-16
### 🐛 Bug fixes
- Fix `"buildType" is not allowed` error. ([#595bf](https://github.com/expo/eas-cli/commit/595bfecf1cbff0f76e7fd2049fe16f6f38bbe150) by [@wkozyra95](https://github.com/wkozyra95))
## [0.4.0](https://github.com/expo/eas-cli/releases/tag/v0.4.0) - 2021-02-16
### 🎉 New features
- Add build:cancel command. ([#219](https://github.com/expo/eas-cli/pull/219) by [@wkozyra95](https://github.com/wkozyra95))
- Implement version auto increment for iOS builds. ([#231](https://github.com/expo/eas-cli/pull/231) by [@dsokal](https://github.com/dsokal))
- Add support for builder environment customizations. ([#230](https://github.com/expo/eas-cli/pull/230) by [@wkozyra95](https://github.com/wkozyra95))
- Add `schemeBuildConfiguration` option for generic iOS builds. ([#234](https://github.com/expo/eas-cli/pull/234) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix `--no-wait` flag for `eas build`. ([#226](https://github.com/expo/eas-cli/pull/226) by [@paul-ridgway](https://github.com/paul-ridgway))
- Fix running builds from project subdirectories. ([#229](https://github.com/expo/eas-cli/pull/229) by [@wkozyra95](https://github.com/wkozyra95))
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.4.0"
"version": "0.6.0"
}
+29 -28
View File
@@ -7,27 +7,28 @@ EAS command line tool
[![Downloads/week](https://img.shields.io/npm/dw/eas-cli.svg)](https://npmjs.org/package/eas-cli)
[![License](https://img.shields.io/npm/l/eas-cli.svg)](https://github.com/expo/eas-cli/blob/main/package.json)
<!-- toc -->
* [eas-cli](#eas-cli)
* [Installation](#installation)
* [Usage](#usage)
* [Commands](#commands)
<!-- tocstop -->
# Installation
```sh
npm install -g eas-cli
# or
yarn global add eas-cli
```
# Usage
<!-- usage -->
```sh-session
$ npm install -g eas-cli
$ eas COMMAND
running command...
$ eas (-v|--version|version)
eas-cli/0.4.0 darwin-x64 node-v14.15.4
$ eas --help [COMMAND]
USAGE
$ eas COMMAND
...
```sh
eas COMMAND
# runs the command
eas (-v|--version|version)
# prints the version
eas --help COMMAND
# outputs help for specific command
```
<!-- usagestop -->
# Commands
@@ -59,7 +60,7 @@ ALIASES
$ eas login
```
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.4.0/build/commands/account/login.ts)_
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.6.0/build/commands/account/login.ts)_
## `eas account:logout`
@@ -73,7 +74,7 @@ ALIASES
$ eas logout
```
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.4.0/build/commands/account/logout.ts)_
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.6.0/build/commands/account/logout.ts)_
## `eas account:view`
@@ -87,7 +88,7 @@ ALIASES
$ eas whoami
```
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.4.0/build/commands/account/view.ts)_
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.6.0/build/commands/account/view.ts)_
## `eas build`
@@ -106,7 +107,7 @@ OPTIONS
--[no-]wait Wait for build(s) to complete
```
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.4.0/build/commands/build/index.ts)_
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.6.0/build/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -117,7 +118,7 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [build/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.4.0/build/commands/build/cancel.ts)_
_See code: [build/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.6.0/build/commands/build/cancel.ts)_
## `eas build:configure`
@@ -132,7 +133,7 @@ OPTIONS
--allow-experimental Enable experimental configuration steps.
```
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.4.0/build/commands/build/configure.ts)_
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.6.0/build/commands/build/configure.ts)_
## `eas build:list`
@@ -148,7 +149,7 @@ OPTIONS
--status=(in-queue|in-progress|errored|finished|canceled)
```
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.4.0/build/commands/build/list.ts)_
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.6.0/build/commands/build/list.ts)_
## `eas build:view [BUILD_ID]`
@@ -159,7 +160,7 @@ USAGE
$ eas build:view [BUILD_ID]
```
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.4.0/build/commands/build/view.ts)_
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.6.0/build/commands/build/view.ts)_
## `eas credentials`
@@ -170,7 +171,7 @@ USAGE
$ eas credentials
```
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.4.0/build/commands/credentials.ts)_
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.6.0/build/commands/credentials.ts)_
## `eas device:create`
@@ -181,7 +182,7 @@ USAGE
$ eas device:create
```
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.4.0/build/commands/device/create.ts)_
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.6.0/build/commands/device/create.ts)_
## `eas device:list`
@@ -195,7 +196,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.4.0/build/commands/device/list.ts)_
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.6.0/build/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -206,7 +207,7 @@ USAGE
$ eas device:view [UDID]
```
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.4.0/build/commands/device/view.ts)_
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.6.0/build/commands/device/view.ts)_
## `eas help [COMMAND]`
@@ -303,5 +304,5 @@ EXAMPLES
and provide its App ID
```
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.4.0/build/commands/submit.ts)_
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.6.0/build/commands/submit.ts)_
<!-- commandsstop -->
+1
View File
@@ -3,6 +3,7 @@ schema: 'https://staging.exp.host/--/graphql'
documents:
- 'src/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
- 'src/commands/**/*.ts'
generates:
src/graphql/generated.ts:
plugins:
+393 -20
View File
@@ -1378,6 +1378,49 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "environmentSecrets",
"description": "Environment secrets for an account",
"args": [
{
"name": "filterNames",
"description": null,
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "EnvironmentSecret",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@@ -2682,11 +2725,11 @@
"deprecationReason": null
},
{
"name": "updateChannelByChannelName",
"description": "get an EAS channel owned by the app by channelName",
"name": "updateChannelByName",
"description": "get an EAS channel owned by the app by name",
"args": [
{
"name": "channelName",
"name": "name",
"description": null,
"type": {
"kind": "NON_NULL",
@@ -2766,11 +2809,11 @@
"deprecationReason": null
},
{
"name": "updateBranchByBranchName",
"description": "get an EAS branch owned by the app by branchName",
"name": "updateBranchByName",
"description": "get an EAS branch owned by the app by name",
"args": [
{
"name": "branchName",
"name": "name",
"description": null,
"type": {
"kind": "NON_NULL",
@@ -2844,6 +2887,49 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "environmentSecrets",
"description": "Environment secrets for an app",
"args": [
{
"name": "filterNames",
"description": null,
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "EnvironmentSecret",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@@ -4830,6 +4916,12 @@
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "SIMULATOR",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
@@ -7299,7 +7391,7 @@
"deprecationReason": null
},
{
"name": "channelName",
"name": "name",
"description": "",
"args": [],
"type": {
@@ -7459,7 +7551,7 @@
"deprecationReason": null
},
{
"name": "branchName",
"name": "name",
"description": "",
"args": [],
"type": {
@@ -7679,7 +7771,7 @@
"deprecationReason": null
},
{
"name": "updateGroup",
"name": "group",
"description": "",
"args": [],
"type": {
@@ -7727,7 +7819,7 @@
"deprecationReason": null
},
{
"name": "updateMessage",
"name": "message",
"description": "",
"args": [],
"type": {
@@ -7739,15 +7831,15 @@
"deprecationReason": null
},
{
"name": "branchName",
"name": "branch",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"kind": "OBJECT",
"name": "UpdateBranch",
"ofType": null
}
},
@@ -7766,6 +7858,81 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "EnvironmentSecret",
"description": "",
"fields": [
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdAt",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updatedAt",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "UserPermission",
@@ -10327,6 +10494,22 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "environmentSecret",
"description": "Mutations that create and delete EnvironmentSecrets",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "EnvironmentSecretMutation",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@@ -13919,7 +14102,7 @@
"defaultValue": null
},
{
"name": "channelName",
"name": "name",
"description": null,
"type": {
"kind": "NON_NULL",
@@ -14149,7 +14332,7 @@
"defaultValue": null
},
{
"name": "branchName",
"name": "name",
"description": null,
"type": {
"kind": "NON_NULL",
@@ -14173,7 +14356,7 @@
},
{
"name": "editUpdateBranch",
"description": "Edit an EAS branch. The branch can be specified either by its ID or\nwith the combination of (appId, branchName).",
"description": "Edit an EAS branch. The branch can be specified either by its ID or\nwith the combination of (appId, name).",
"args": [
{
"name": "input",
@@ -14297,7 +14480,7 @@
"defaultValue": null
},
{
"name": "branchName",
"name": "name",
"description": "",
"type": {
"kind": "SCALAR",
@@ -14307,7 +14490,7 @@
"defaultValue": null
},
{
"name": "newBranchName",
"name": "newName",
"description": "",
"type": {
"kind": "NON_NULL",
@@ -14401,7 +14584,7 @@
"defaultValue": null
},
{
"name": "updateMessage",
"name": "message",
"description": "",
"type": {
"kind": "SCALAR",
@@ -14506,7 +14689,7 @@
"fields": null,
"inputFields": [
{
"name": "fileHash",
"name": "fileSHA256",
"description": "",
"type": {
"kind": "NON_NULL",
@@ -16144,6 +16327,196 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "EnvironmentSecretMutation",
"description": "",
"fields": [
{
"name": "createEnvironmentSecretForAccount",
"description": "Create an environment secret for an Account",
"args": [
{
"name": "environmentSecretData",
"description": null,
"type": {
"kind": "INPUT_OBJECT",
"name": "CreateEnvironmentSecretInput",
"ofType": null
},
"defaultValue": null
},
{
"name": "accountId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "EnvironmentSecret",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createEnvironmentSecretForApp",
"description": "Create an environment secret for an App",
"args": [
{
"name": "environmentSecretData",
"description": null,
"type": {
"kind": "INPUT_OBJECT",
"name": "CreateEnvironmentSecretInput",
"ofType": null
},
"defaultValue": null
},
{
"name": "appId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "EnvironmentSecret",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deleteEnvironmentSecret",
"description": "Delete an environment secret",
"args": [
{
"name": "id",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DeleteEnvironmentSecretResult",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "CreateEnvironmentSecretInput",
"description": "",
"fields": null,
"inputFields": [
{
"name": "name",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "value",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DeleteEnvironmentSecretResult",
"description": "",
"fields": [
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "BaseSearchResult",
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.4.0",
"version": "0.6.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -10,9 +10,9 @@
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.17",
"@expo/config": "~3.3.19",
"@expo/config-plugins": "1.0.18",
"@expo/eas-build-job": "0.2.10",
"@expo/eas-json": "^0.4.0",
"@expo/config-plugins": "1.0.21-alpha.0",
"@expo/eas-build-job": "0.2.16",
"@expo/eas-json": "^0.6.0",
"@expo/json-file": "^8.2.24",
"@expo/plist": "^0.0.11",
"@expo/plugin-warn-if-update-available": "^1.7.0",
@@ -48,7 +48,7 @@ describe(ensureCredentialsAsync, () => {
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Generic,
Workflow.GENERIC,
CredentialsSource.AUTO,
false
);
@@ -61,7 +61,7 @@ describe(ensureCredentialsAsync, () => {
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Generic,
Workflow.GENERIC,
CredentialsSource.AUTO,
false
);
@@ -75,7 +75,7 @@ describe(ensureCredentialsAsync, () => {
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Generic,
Workflow.GENERIC,
CredentialsSource.AUTO,
false
);
@@ -93,7 +93,7 @@ describe(ensureCredentialsAsync, () => {
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Generic,
Workflow.GENERIC,
CredentialsSource.AUTO,
false
);
@@ -111,7 +111,7 @@ describe(ensureCredentialsAsync, () => {
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Generic,
Workflow.GENERIC,
CredentialsSource.AUTO,
false
);
@@ -126,7 +126,7 @@ describe(ensureCredentialsAsync, () => {
});
try {
await ensureCredentialsAsync(provider, Workflow.Generic, CredentialsSource.AUTO, true);
await ensureCredentialsAsync(provider, Workflow.GENERIC, CredentialsSource.AUTO, true);
throw new Error('ensureCredentialsAsync should throw an Error');
} catch (e) {
expect(e.message).toMatch(
@@ -147,7 +147,7 @@ describe(ensureCredentialsAsync, () => {
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Generic,
Workflow.GENERIC,
CredentialsSource.AUTO,
false
);
@@ -165,7 +165,7 @@ describe(ensureCredentialsAsync, () => {
});
try {
await ensureCredentialsAsync(provider, Workflow.Generic, CredentialsSource.AUTO, false);
await ensureCredentialsAsync(provider, Workflow.GENERIC, CredentialsSource.AUTO, false);
throw new Error('ensureCredentialsAsync should throw an Error');
} catch (e) {
expect(e.message).toMatch(
@@ -186,7 +186,7 @@ describe(ensureCredentialsAsync, () => {
});
try {
await ensureCredentialsAsync(provider, Workflow.Generic, CredentialsSource.AUTO, true);
await ensureCredentialsAsync(provider, Workflow.GENERIC, CredentialsSource.AUTO, true);
throw new Error('ensureCredentialsAsync should throw an Error');
} catch (e) {
expect(e.message).toMatch('Credentials for this app are not configured');
@@ -204,7 +204,7 @@ describe(ensureCredentialsAsync, () => {
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Managed,
Workflow.MANAGED,
CredentialsSource.AUTO,
false
);
@@ -217,7 +217,7 @@ describe(ensureCredentialsAsync, () => {
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Managed,
Workflow.MANAGED,
CredentialsSource.AUTO,
false
);
@@ -231,7 +231,7 @@ describe(ensureCredentialsAsync, () => {
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Managed,
Workflow.MANAGED,
CredentialsSource.AUTO,
false
);
@@ -247,7 +247,7 @@ describe(ensureCredentialsAsync, () => {
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Managed,
Workflow.MANAGED,
CredentialsSource.AUTO,
false
);
@@ -265,7 +265,7 @@ describe(ensureCredentialsAsync, () => {
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Generic,
Workflow.GENERIC,
CredentialsSource.LOCAL,
false
);
@@ -283,7 +283,7 @@ describe(ensureCredentialsAsync, () => {
});
const src = await ensureCredentialsAsync(
provider,
Workflow.Generic,
Workflow.GENERIC,
CredentialsSource.REMOTE,
false
);
@@ -90,3 +90,21 @@ async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig)
throw new Error('Missing values in AndroidManifest.xml');
}
}
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
try {
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
if (!androidManifestPath) {
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
}
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
androidManifestPath
);
return AndroidConfig.Manifest.getMainApplicationMetaDataValue(
androidManifest,
AndroidConfig.Updates.Config.RELEASE_CHANNEL
);
} catch (err) {
return null;
}
}
@@ -25,7 +25,7 @@ function isApplicationIdValid(applicationId: string): boolean {
}
export async function ensureApplicationIdIsValidAsync(projectDir: string) {
const applicationId = await ensureAppIdentifierIsDefinedAsync(projectDir, Platform.Android);
const applicationId = await ensureAppIdentifierIsDefinedAsync(projectDir, Platform.ANDROID);
if (!isApplicationIdValid(applicationId)) {
const configDescription = getProjectConfigDescription(projectDir);
Log.error(
+9 -9
View File
@@ -22,15 +22,15 @@ export async function prepareAndroidBuildAsync(
commandCtx: CommandContext,
easConfig: EasConfig
): Promise<() => Promise<string>> {
const buildCtx = createBuildContext<Platform.Android>({
const buildCtx = createBuildContext<Platform.ANDROID>({
commandCtx,
platform: Platform.Android,
platform: Platform.ANDROID,
easConfig,
});
const { buildProfile } = buildCtx;
if (
buildProfile.workflow === Workflow.Generic &&
buildProfile.workflow === Workflow.GENERIC &&
!(await fs.pathExists(path.join(commandCtx.projectDir, 'android')))
) {
throw new Error(
@@ -41,7 +41,7 @@ export async function prepareAndroidBuildAsync(
}
if (
buildProfile.workflow === Workflow.Generic &&
buildProfile.workflow === Workflow.GENERIC &&
buildProfile.distribution === 'internal' &&
buildProfile.gradleCommand?.match(/bundle/)
) {
@@ -67,7 +67,7 @@ This means that it will most likely produce an AAB and you will not be able to i
projectConfiguration: {},
ensureCredentialsAsync: ensureAndroidCredentialsAsync,
ensureProjectConfiguredAsync: async () => {
if (buildCtx.buildProfile.workflow === Workflow.Generic) {
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
await validateAndSyncProjectConfigurationAsync(commandCtx.projectDir, commandCtx.exp);
}
},
@@ -75,15 +75,15 @@ This means that it will most likely produce an AAB and you will not be able to i
});
}
function shouldProvideCredentials(ctx: BuildContext<Platform.Android>): boolean {
function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean {
return (
ctx.buildProfile.workflow === Workflow.Managed ||
(ctx.buildProfile.workflow === Workflow.Generic && !ctx.buildProfile.withoutCredentials)
ctx.buildProfile.workflow === Workflow.MANAGED ||
(ctx.buildProfile.workflow === Workflow.GENERIC && !ctx.buildProfile.withoutCredentials)
);
}
async function ensureAndroidCredentialsAsync(
ctx: BuildContext<Platform.Android>
ctx: BuildContext<Platform.ANDROID>
): Promise<CredentialsResult<AndroidCredentials> | undefined> {
if (!shouldProvideCredentials(ctx)) {
return;
@@ -2,6 +2,7 @@ import {
Android,
ArchiveSource,
ArchiveSourceType,
Cache,
Job,
Workflow,
sanitizeJob,
@@ -22,13 +23,13 @@ interface JobData {
}
export async function prepareJobAsync(
ctx: BuildContext<Platform.Android>,
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData
): Promise<Job> {
if (ctx.buildProfile.workflow === Workflow.Generic) {
if (ctx.buildProfile.workflow === Workflow.GENERIC) {
const partialJob = await prepareGenericJobAsync(ctx, jobData, ctx.buildProfile);
return sanitizeJob(partialJob);
} else if (ctx.buildProfile.workflow === Workflow.Managed) {
} else if (ctx.buildProfile.workflow === Workflow.MANAGED) {
const partialJob = await prepareManagedJobAsync(ctx, jobData, ctx.buildProfile);
return sanitizeJob(partialJob);
} else {
@@ -37,9 +38,10 @@ export async function prepareJobAsync(
}
interface CommonJobProperties {
platform: Platform.Android;
platform: Platform.ANDROID;
projectArchive: ArchiveSource;
builderEnvironment: Android.BuilderEnvironment;
cache: Cache | null;
secrets: {
buildCredentials?: {
keystore: Android.Keystore;
@@ -49,7 +51,7 @@ interface CommonJobProperties {
}
async function prepareJobCommonAsync(
ctx: BuildContext<Platform.Android>,
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData
): Promise<Partial<CommonJobProperties>> {
const secretEnvs = await readSecretEnvsAsync(ctx.commandCtx.projectDir);
@@ -68,7 +70,7 @@ async function prepareJobCommonAsync(
: {};
return {
platform: Platform.Android,
platform: Platform.ANDROID,
projectArchive: {
type: ArchiveSourceType.S3,
bucketKey: jobData.archiveBucketKey,
@@ -80,6 +82,7 @@ async function prepareJobCommonAsync(
ndk: ctx.buildProfile.ndk,
env: ctx.buildProfile.env,
},
cache: ctx.buildProfile.cache,
secrets: {
...(secretEnvs ? { secretEnvs } : {}),
...buildCredentials,
@@ -88,7 +91,7 @@ async function prepareJobCommonAsync(
}
async function prepareGenericJobAsync(
ctx: BuildContext<Platform.Android>,
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData,
buildProfile: AndroidGenericBuildProfile
): Promise<Partial<Android.GenericJob>> {
@@ -96,7 +99,7 @@ async function prepareGenericJobAsync(
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
return {
...(await prepareJobCommonAsync(ctx, jobData)),
type: Workflow.Generic,
type: Workflow.GENERIC,
gradleCommand: buildProfile.gradleCommand,
artifactPath: buildProfile.artifactPath,
releaseChannel: buildProfile.releaseChannel,
@@ -105,7 +108,7 @@ async function prepareGenericJobAsync(
}
async function prepareManagedJobAsync(
ctx: BuildContext<Platform.Android>,
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData,
buildProfile: AndroidManagedBuildProfile
): Promise<Partial<Android.ManagedJob>> {
@@ -114,7 +117,7 @@ async function prepareManagedJobAsync(
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.projectDir);
return {
...(await prepareJobCommonAsync(ctx, jobData)),
type: Workflow.Managed,
type: Workflow.MANAGED,
username: accountName,
buildType: buildProfile.buildType,
releaseChannel: buildProfile.releaseChannel,
+12 -6
View File
@@ -6,6 +6,7 @@ import { apiClient } from '../api';
import Log from '../log';
import { promptAsync } from '../prompts';
import { UploadType, uploadAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
import { platformDisplayNames } from './constants';
import { BuildContext } from './context';
@@ -112,8 +113,7 @@ export async function prepareBuildRequestForPlatformAsync<
}
);
} catch (error) {
const body = error?.response?.body;
if (body && JSON.parse(body)?.errors?.[0]?.code === 'TURTLE_DEPRECATED_JOB_FORMAT') {
if (error?.expoApiV2ErrorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli');
}
throw error;
@@ -136,7 +136,10 @@ async function uploadProjectAsync<TPlatform extends Platform>(
projectTarball.path,
createProgressTracker({
total: projectTarball.size,
message: 'Uploading to EAS Build',
message: ratio =>
`Uploading to EAS Build (${formatBytes(projectTarball.size * ratio)} / ${formatBytes(
projectTarball.size
)})`,
completedMessage: `Uploaded to EAS`,
})
);
@@ -183,7 +186,7 @@ enum ShouldCommitChanges {
}
async function reviewAndCommitChangesAsync(
commitMessage: string,
initialCommitMessage: string,
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
): Promise<void> {
if (nonInteractive) {
@@ -212,10 +215,13 @@ async function reviewAndCommitChangesAsync(
"Aborting, run the command again once you're ready. Make sure to commit any changes you've made."
);
} else if (selected === ShouldCommitChanges.Yes) {
await commitPromptAsync(commitMessage);
await commitPromptAsync({ initialCommitMessage });
Log.withTick('Committed changes.');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await showDiffAsync();
await reviewAndCommitChangesAsync(commitMessage, { nonInteractive, askedFirstTime: false });
await reviewAndCommitChangesAsync(initialCommitMessage, {
nonInteractive,
askedFirstTime: false,
});
}
}
+3 -3
View File
@@ -132,7 +132,7 @@ enum ShouldCommitChanges {
}
async function reviewAndCommitChangesAsync(
commitMessage: string,
initialCommitMessage: string,
askedFirstTime: boolean = true
): Promise<void> {
const { selected } = await promptAsync({
@@ -152,10 +152,10 @@ async function reviewAndCommitChangesAsync(
});
if (selected === ShouldCommitChanges.Yes) {
await commitPromptAsync(commitMessage);
await commitPromptAsync({ initialCommitMessage });
Log.withTick('Committed changes');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await showDiffAsync();
await reviewAndCommitChangesAsync(commitMessage, false);
await reviewAndCommitChangesAsync(initialCommitMessage, false);
}
}
+2 -2
View File
@@ -7,6 +7,6 @@ export const platformDisplayNames = {
};
export const platformEmojis = {
[Platform.iOS]: '🍎',
[Platform.Android]: '🤖',
[Platform.IOS]: '🍎',
[Platform.ANDROID]: '🤖',
};
+1 -1
View File
@@ -77,7 +77,7 @@ export interface ConfigureContext {
hasIosNativeProject: boolean;
}
type PlatformBuildProfile<T extends Platform> = T extends Platform.Android
type PlatformBuildProfile<T extends Platform> = T extends Platform.ANDROID
? AndroidBuildProfile
: iOSBuildProfile;
+2 -2
View File
@@ -51,11 +51,11 @@ async function startBuildsAsync(
}[] = [];
if (shouldBuildAndroid) {
const sendBuildRequestAsync = await prepareAndroidBuildAsync(commandCtx, easConfig);
builds.push({ platform: Platform.Android, sendBuildRequestAsync });
builds.push({ platform: Platform.ANDROID, sendBuildRequestAsync });
}
if (shouldBuildiOS) {
const sendBuildRequestAsync = await prepareIosBuildAsync(commandCtx, easConfig);
builds.push({ platform: Platform.iOS, sendBuildRequestAsync });
builds.push({ platform: Platform.IOS, sendBuildRequestAsync });
}
return Promise.all(
builds.map(async ({ platform, sendBuildRequestAsync }) => ({
+2 -2
View File
@@ -21,13 +21,13 @@ async function ensureCredentialsAutoAsync(
): Promise<CredentialsSource.LOCAL | CredentialsSource.REMOTE> {
const platform = platformDisplayNames[provider.platform];
switch (workflow) {
case Workflow.Managed:
case Workflow.MANAGED:
if (await provider.hasLocalAsync()) {
return CredentialsSource.LOCAL;
} else {
return CredentialsSource.REMOTE;
}
case Workflow.Generic: {
case Workflow.GENERIC: {
const hasLocal = await provider.hasLocalAsync();
const hasRemote = await provider.hasRemoteAsync();
if (hasRemote && hasLocal) {
@@ -86,3 +86,12 @@ async function writeExpoPlistAsync(
await writePlistAsync(expoPlistPath, expoPlist);
await gitAddAsync(expoPlistPath, { intentToAdd: true });
}
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
try {
const expoPlist = await readExpoPlistAsync(projectDir);
return expoPlist[IOSConfig.Updates.Config.RELEASE_CHANNEL] ?? null;
} catch (err) {
return null;
}
}
+7 -7
View File
@@ -20,14 +20,14 @@ export async function prepareIosBuildAsync(
commandCtx: CommandContext,
easConfig: EasConfig
): Promise<() => Promise<string>> {
const buildCtx = createBuildContext<Platform.iOS>({
const buildCtx = createBuildContext<Platform.IOS>({
commandCtx,
platform: Platform.iOS,
platform: Platform.IOS,
easConfig,
});
if (
buildCtx.buildProfile.workflow === Workflow.Generic &&
buildCtx.buildProfile.workflow === Workflow.GENERIC &&
!(await fs.pathExists(path.join(commandCtx.projectDir, 'ios')))
) {
throw new Error(
@@ -39,9 +39,9 @@ export async function prepareIosBuildAsync(
let iosBuildScheme: string | undefined;
let iosApplicationTarget: string | undefined;
if (buildCtx.buildProfile.workflow === Workflow.Generic) {
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
iosBuildScheme = buildCtx.buildProfile.scheme ?? (await resolveSchemeAsync(buildCtx));
iosApplicationTarget = await IOSConfig.Target.getApplicationTargetForSchemeAsync(
iosApplicationTarget = await IOSConfig.BuildScheme.getApplicationTargetForSchemeAsync(
buildCtx.commandCtx.projectDir,
iosBuildScheme
);
@@ -67,8 +67,8 @@ export async function prepareIosBuildAsync(
});
}
async function resolveSchemeAsync(ctx: BuildContext<Platform.iOS>): Promise<string> {
const schemes = IOSConfig.Scheme.getSchemesFromXcodeproj(ctx.commandCtx.projectDir);
async function resolveSchemeAsync(ctx: BuildContext<Platform.IOS>): Promise<string> {
const schemes = IOSConfig.BuildScheme.getSchemesFromXcodeproj(ctx.commandCtx.projectDir);
if (schemes.length === 1) {
return schemes[0];
}
@@ -22,7 +22,7 @@ function isBundleIdentifierValid(bundleIdentifier: string): boolean {
}
export async function ensureBundleIdentifierIsValidAsync(projectDir: string) {
const bundleIdentifier = await ensureAppIdentifierIsDefinedAsync(projectDir, Platform.iOS);
const bundleIdentifier = await ensureAppIdentifierIsDefinedAsync(projectDir, Platform.IOS);
if (!isBundleIdentifierValid(bundleIdentifier)) {
const configDescription = getProjectConfigDescription(projectDir);
Log.error(
+27 -10
View File
@@ -1,7 +1,12 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import { Workflow } from '@expo/eas-build-job';
import { VersionAutoIncrement, iOSBuildProfile } from '@expo/eas-json';
import {
EasJsonReader,
VersionAutoIncrement,
iOSBuildProfile,
iOSGenericBuildProfile,
} from '@expo/eas-json';
import Log from '../../log';
import { ConfigureContext } from '../context';
@@ -17,8 +22,14 @@ export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
if (!ctx.hasIosNativeProject) {
return;
}
await configureBundleIdentifierAsync(ctx.projectDir, ctx.exp);
await ensureBundleIdentifierIsValidAsync(ctx.projectDir);
const disableIosBundleIdentifierValidation = (
await new EasJsonReader(ctx.projectDir, 'all').readRawAsync()
)?.experimental?.disableIosBundleIdentifierValidation;
if (!disableIosBundleIdentifierValidation) {
await configureBundleIdentifierAsync(ctx.projectDir, ctx.exp);
await ensureBundleIdentifierIsValidAsync(ctx.projectDir);
}
if (isExpoUpdatesInstalled(ctx.projectDir)) {
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
@@ -38,14 +49,20 @@ export async function validateAndSyncProjectConfigurationAsync({
const { workflow, autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement);
if (workflow === Workflow.Generic) {
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(
projectDir
);
if (!bundleIdentifierFromPbxproj || bundleIdentifierFromPbxproj !== exp.ios?.bundleIdentifier) {
throw new Error(
'Bundle identifier is not configured correctly in your Xcode project. Please run "eas build:configure" to configure it.'
if (workflow === Workflow.GENERIC) {
const genericBuildProfile = buildProfile as iOSGenericBuildProfile;
if (!genericBuildProfile.disableIosBundleIdentifierValidation) {
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(
projectDir
);
if (
!bundleIdentifierFromPbxproj ||
bundleIdentifierFromPbxproj !== exp.ios?.bundleIdentifier
) {
throw new Error(
'Bundle identifier is not configured correctly in your Xcode project. Please run "eas build:configure" to configure it.'
);
}
}
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
@@ -1,5 +1,5 @@
import { Workflow } from '@expo/eas-build-job';
import { CredentialsSource, DistributionType } from '@expo/eas-json';
import { CredentialsSource, iOSDistributionType } from '@expo/eas-json';
import assert from 'assert';
import { createCredentialsContextAsync } from '../../credentials/context';
@@ -13,7 +13,7 @@ import { ensureCredentialsAsync } from '../credentials';
import { Platform } from '../types';
export async function ensureIosCredentialsAsync(
ctx: BuildContext<Platform.iOS>
ctx: BuildContext<Platform.IOS>
): Promise<CredentialsResult<IosCredentials> | undefined> {
if (!shouldProvideCredentials(ctx)) {
return;
@@ -27,7 +27,7 @@ export async function ensureIosCredentialsAsync(
},
workflow: ctx.buildProfile.workflow,
credentialsSource: ctx.buildProfile.credentialsSource,
distribution: ctx.buildProfile.distribution ?? DistributionType.STORE,
distribution: ctx.buildProfile.distribution ?? 'store',
nonInteractive: ctx.commandCtx.nonInteractive,
skipCredentialsCheck: ctx.commandCtx.skipCredentialsCheck,
});
@@ -37,7 +37,7 @@ interface ResolveCredentialsParams {
app: AppLookupParams;
workflow: Workflow;
credentialsSource: CredentialsSource;
distribution: DistributionType;
distribution: iOSDistributionType;
nonInteractive: boolean;
skipCredentialsCheck: boolean;
}
@@ -68,6 +68,6 @@ export async function resolveIosCredentialsAsync(
};
}
function shouldProvideCredentials(ctx: BuildContext<Platform.iOS>): boolean {
return true;
function shouldProvideCredentials(ctx: BuildContext<Platform.IOS>): boolean {
return ctx.buildProfile.distribution !== 'simulator';
}
+26 -18
View File
@@ -1,9 +1,10 @@
import {
ArchiveSource,
ArchiveSourceType,
Cache,
Ios,
Job,
Workflow,
iOS,
sanitizeJob,
} from '@expo/eas-build-job';
import { iOSGenericBuildProfile, iOSManagedBuildProfile } from '@expo/eas-json';
@@ -31,13 +32,13 @@ interface JobData {
}
export async function prepareJobAsync(
ctx: BuildContext<Platform.iOS>,
ctx: BuildContext<Platform.IOS>,
jobData: JobData
): Promise<Job> {
if (ctx.buildProfile.workflow === Workflow.Generic) {
if (ctx.buildProfile.workflow === Workflow.GENERIC) {
const partialJob = await prepareGenericJobAsync(ctx, jobData, ctx.buildProfile);
return sanitizeJob(partialJob);
} else if (ctx.buildProfile.workflow === Workflow.Managed) {
} else if (ctx.buildProfile.workflow === Workflow.MANAGED) {
const partialJob = await prepareManagedJobAsync(ctx, jobData, ctx.buildProfile);
return sanitizeJob(partialJob);
} else {
@@ -46,18 +47,20 @@ export async function prepareJobAsync(
}
interface CommonJobProperties {
platform: Platform.iOS;
platform: Platform.IOS;
projectArchive: ArchiveSource;
builderEnvironment: iOS.BuilderEnvironment;
releaseChannel: string;
builderEnvironment: Ios.BuilderEnvironment;
releaseChannel?: string;
distribution?: Ios.DistributionType;
cache: Cache | null;
secrets: {
buildCredentials: iOS.BuildCredentials;
buildCredentials: Ios.BuildCredentials;
secretEnvs?: Record<string, string>;
};
}
async function prepareJobCommonAsync(
ctx: BuildContext<Platform.iOS>,
ctx: BuildContext<Platform.IOS>,
{
archiveBucketKey,
credentials,
@@ -83,11 +86,12 @@ async function prepareJobCommonAsync(
}
return {
platform: Platform.iOS,
platform: Platform.IOS,
projectArchive: {
type: ArchiveSourceType.S3,
bucketKey: archiveBucketKey,
},
distribution: ctx.buildProfile.distribution,
builderEnvironment: {
image: ctx.buildProfile.image,
node: ctx.buildProfile.node,
@@ -96,6 +100,7 @@ async function prepareJobCommonAsync(
fastlane: ctx.buildProfile.fastlane,
env: ctx.buildProfile.env,
},
cache: ctx.buildProfile.cache,
secrets: {
...(secretEnvs ? { secretEnvs } : {}),
buildCredentials,
@@ -103,7 +108,7 @@ async function prepareJobCommonAsync(
};
}
function prepareTargetCredentials(targetCredentials: IosTargetCredentials): iOS.TargetCredentials {
function prepareTargetCredentials(targetCredentials: IosTargetCredentials): Ios.TargetCredentials {
return {
provisioningProfileBase64: targetCredentials.provisioningProfile,
distributionCertificate: {
@@ -114,10 +119,10 @@ function prepareTargetCredentials(targetCredentials: IosTargetCredentials): iOS.
}
async function prepareGenericJobAsync(
ctx: BuildContext<Platform.iOS>,
ctx: BuildContext<Platform.IOS>,
jobData: JobData,
buildProfile: iOSGenericBuildProfile
): Promise<Partial<iOS.GenericJob>> {
): Promise<Partial<Ios.GenericJob>> {
const projectRootDirectory =
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
return {
@@ -126,9 +131,12 @@ async function prepareGenericJobAsync(
credentials: jobData.credentials,
targetName: jobData.projectConfiguration.iosApplicationTarget,
})),
type: Workflow.Generic,
type: Workflow.GENERIC,
scheme: jobData.projectConfiguration.iosBuildScheme,
schemeBuildConfiguration: buildProfile.schemeBuildConfiguration,
schemeBuildConfiguration:
buildProfile.schemeBuildConfiguration === 'Auto'
? undefined
: buildProfile.schemeBuildConfiguration,
artifactPath: buildProfile.artifactPath,
releaseChannel: buildProfile.releaseChannel,
projectRootDirectory,
@@ -136,10 +144,10 @@ async function prepareGenericJobAsync(
}
async function prepareManagedJobAsync(
ctx: BuildContext<Platform.iOS>,
ctx: BuildContext<Platform.IOS>,
jobData: JobData,
buildProfile: iOSManagedBuildProfile
): Promise<Partial<iOS.ManagedJob>> {
): Promise<Partial<Ios.ManagedJob>> {
const projectRootDirectory =
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.projectDir);
@@ -150,7 +158,7 @@ async function prepareManagedJobAsync(
credentials: jobData.credentials,
targetName,
})),
type: Workflow.Managed,
type: Workflow.MANAGED,
buildType: buildProfile.buildType,
username: accountName,
releaseChannel: buildProfile.releaseChannel,
+33 -5
View File
@@ -1,8 +1,13 @@
import { CredentialsSource, DistributionType } from '@expo/eas-json';
import { Metadata } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import { getAppIdentifierAsync } from '../project/projectUtils';
import { gitCommitHashAsync } from '../utils/git';
import { readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync } from './android/UpdatesModule';
import { BuildContext } from './context';
import { BuildMetadata, Platform } from './types';
import { readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync } from './ios/UpdatesModule';
import { Platform } from './types';
import { isExpoUpdatesInstalled } from './utils/updates';
/**
* We use require() to exclude package.json from TypeScript's analysis since it lives outside
@@ -18,17 +23,40 @@ export async function collectMetadata<T extends Platform>(
}: {
credentialsSource?: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
}
): Promise<BuildMetadata> {
): Promise<Metadata> {
return {
trackingContext: ctx.trackingCtx,
appVersion: ctx.commandCtx.exp.version!,
cliVersion: packageJSON.version,
workflow: ctx.buildProfile.workflow,
credentialsSource,
sdkVersion: ctx.commandCtx.exp.sdkVersion,
trackingContext: ctx.trackingCtx,
distribution: ctx.buildProfile.distribution ?? DistributionType.STORE,
releaseChannel: await resolveReleaseChannel(ctx),
distribution: ctx.buildProfile.distribution ?? 'store',
appName: ctx.commandCtx.exp.name,
appIdentifier:
(await getAppIdentifierAsync(ctx.commandCtx.projectDir, ctx.platform)) ?? undefined,
buildProfile: ctx.commandCtx.profile,
gitCommitHash: await gitCommitHashAsync(),
};
}
async function resolveReleaseChannel<T extends Platform>(
ctx: BuildContext<T>
): Promise<string | undefined> {
if (!isExpoUpdatesInstalled(ctx.commandCtx.projectDir)) {
return undefined;
}
if (ctx.buildProfile.releaseChannel) {
return ctx.buildProfile.releaseChannel;
}
let maybeReleaseChannel: string | null;
if (ctx.platform === Platform.ANDROID) {
maybeReleaseChannel = await readAndroidReleaseChannelSafelyAsync(ctx.commandCtx.projectDir);
} else {
maybeReleaseChannel = await readIosReleaseChannelSafelyAsync(ctx.commandCtx.projectDir);
}
return maybeReleaseChannel ?? 'default';
}
+4 -70
View File
@@ -1,10 +1,5 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import {
AndroidBuildProfile,
CredentialsSource,
DistributionType,
iOSBuildProfile,
} from '@expo/eas-json';
import { Metadata, Platform } from '@expo/eas-build-job';
import { AndroidBuildProfile, iOSBuildProfile } from '@expo/eas-json';
export enum RequestedPlatform {
Android = 'android',
@@ -31,7 +26,7 @@ export interface Build {
createdAt: string;
updatedAt: string;
artifacts?: BuildArtifacts;
metadata?: Partial<BuildMetadata>;
metadata?: Partial<Metadata>;
}
interface BuildArtifacts {
@@ -39,68 +34,7 @@ interface BuildArtifacts {
logsUrl: string;
}
export type BuildMetadata = {
/**
* Application version (the expo.version key in app.json/app.config.js)
*/
appVersion: string;
/**
* EAS CLI version
*/
cliVersion: string;
/**
* Build workflow
* It's either 'generic' or 'managed'
*/
workflow: Workflow;
/**
* Credentials source
* Credentials could be obtained either from credential.json or EAS servers.
*/
credentialsSource?: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
/**
* Expo SDK version
* It's determined by the expo package version in package.json.
* It's undefined if the expo package is not installed for the project.
*/
sdkVersion?: string;
/**
* Release channel (for expo-updates)
* It's undefined if the expo-updates package is not installed for the project.
*/
releaseChannel?: string;
/**
* Tracking context
* It's used to track build process across different Expo services and tools.
*/
trackingContext: TrackingContext;
/**
* Distribution type
* Indicates whether this is a build for store or internal distribution.
*/
distribution: DistributionType;
/**
* App name (expo.name in app.json/app.config.js)
*/
appName?: string;
/**
* App identifier:
* - iOS builds: the bundle identifier (expo.ios.bundleIdentifier in app.json/app.config.js)
* - Android builds: the application id (expo.android.package in app.json/app.config.js)
*/
appIdentifier?: string;
};
export type PlatformBuildProfile<T extends Platform> = T extends Platform.Android
export type PlatformBuildProfile<T extends Platform> = T extends Platform.ANDROID
? AndroidBuildProfile
: iOSBuildProfile;
@@ -1,4 +1,3 @@
import { DistributionType } from '@expo/eas-json';
import assert from 'assert';
import chalk from 'chalk';
@@ -47,7 +46,7 @@ export function printBuildResults(accountName: string, builds: (Build | null)[])
}
function printBuildResult(accountName: string, build: Build): void {
if (build.metadata?.distribution === DistributionType.INTERNAL) {
if (build.metadata?.distribution === 'internal') {
const logsUrl = getBuildLogsUrl({
buildId: build.id,
account: accountName,
+28 -13
View File
@@ -8,6 +8,7 @@ import { v4 as uuidv4 } from 'uuid';
import Log from '../../log';
import { confirmAsync, promptAsync } from '../../prompts';
import { formatBytes } from '../../utils/files';
import {
doesGitRepoExistAsync,
getGitDiffOutputAsync,
@@ -43,12 +44,11 @@ async function ensureGitRepoExistsAsync(): Promise<void> {
Log.log("We're going to make an initial commit for you repository.");
await spawnAsync('git', ['add', '-A']);
await commitPromptAsync('Initial commit');
await commitPromptAsync({ initialCommitMessage: 'Initial commit', commitAllFiles: true });
}
async function isGitStatusCleanAsync(): Promise<boolean> {
const changes = await gitStatusAsync();
const changes = await gitStatusAsync({ showUntracked: true });
return changes.length === 0;
}
@@ -90,7 +90,7 @@ async function ensureGitStatusIsCleanAsync(nonInteractive = false): Promise<void
message: `Commit changes to git?`,
});
if (answer) {
await commitPromptAsync();
await commitPromptAsync({ commitAllFiles: true });
} else {
throw new Error('Please commit all changes. Aborting...');
}
@@ -120,7 +120,6 @@ async function makeProjectTarballAsync(): Promise<{ path: string; size: number }
try {
await spawnAsync('git', [
'clone',
'--local',
'--no-hardlinks',
'--depth',
'1',
@@ -130,19 +129,26 @@ async function makeProjectTarballAsync(): Promise<{ path: string; size: number }
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
'.',
]);
} finally {
// Stop the timer
} catch (err) {
clearTimeout(timer);
await fs.remove(shallowClonePath);
const duration = endTimer(compressTimerLabel);
if (spinner.isSpinning) {
const prettyTime = formatMilliseconds(duration);
spinner.succeed(`Compressed project files ${chalk.dim(prettyTime)}`);
spinner.fail();
}
throw err;
} finally {
await fs.remove(shallowClonePath);
}
clearTimeout(timer);
const { size } = await fs.stat(tarPath);
const duration = endTimer(compressTimerLabel);
if (spinner.isSpinning) {
const prettyTime = formatMilliseconds(duration);
spinner.succeed(
`Compressed project files ${chalk.dim(`${prettyTime} (${formatBytes(size)})`)}`
);
}
return { size, path: tarPath };
}
@@ -151,7 +157,13 @@ async function showDiffAsync() {
await gitDiffAsync({ withPager: outputTooLarge });
}
async function commitPromptAsync(initialCommitMessage?: string): Promise<void> {
async function commitPromptAsync({
initialCommitMessage,
commitAllFiles,
}: {
initialCommitMessage?: string;
commitAllFiles?: boolean;
} = {}): Promise<void> {
const { message } = await promptAsync({
type: 'text',
name: 'message',
@@ -159,6 +171,9 @@ async function commitPromptAsync(initialCommitMessage?: string): Promise<void> {
initial: initialCommitMessage,
validate: (input: string) => input !== '',
});
if (commitAllFiles) {
await spawnAsync('git', ['add', '-A']);
}
await commitChangedFilesAsync(message);
}
+25 -23
View File
@@ -4,7 +4,11 @@ import chalk from 'chalk';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { UpdateBranch } from '../../graphql/generated';
import {
CreateUpdateBranchForAppMutation,
CreateUpdateBranchForAppMutationVariables,
UpdateBranch,
} from '../../graphql/generated';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import { findProjectRootAsync, getProjectAccountName } from '../../project/projectUtils';
@@ -14,35 +18,33 @@ import { getBranchNameAsync } from '../../utils/git';
export async function createUpdateBranchOnAppAsync({
appId,
branchName,
}: {
appId: string;
branchName: string;
}): Promise<Pick<UpdateBranch, 'id' | 'branchName'>> {
const data = await withErrorHandlingAsync(
name,
}: CreateUpdateBranchForAppMutationVariables): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
const result = await withErrorHandlingAsync(
graphqlClient
.mutation<
{ updateBranch: { createUpdateBranchForApp: Pick<UpdateBranch, 'id' | 'branchName'> } },
{ appId: string; branchName: string }
>(
.mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>(
gql`
mutation createUpdateBranchForApp($appId: ID!, $branchName: String!) {
mutation createUpdateBranchForApp($appId: ID!, $name: String!) {
updateBranch {
createUpdateBranchForApp(appId: $appId, branchName: $branchName) {
createUpdateBranchForApp(appId: $appId, name: $name) {
id
branchName
name
}
}
}
`,
{
appId,
branchName,
name,
}
)
.toPromise()
);
return data.updateBranch.createUpdateBranchForApp;
const newBranch = result.updateBranch.createUpdateBranchForApp;
if (!newBranch) {
throw new Error(`Could not create branch ${name}.`);
}
return newBranch;
}
export default class BranchCreate extends Command {
@@ -51,7 +53,7 @@ export default class BranchCreate extends Command {
static args = [
{
name: 'branchName',
name: 'name',
required: false,
description: 'Name of the branch to create',
},
@@ -66,7 +68,7 @@ export default class BranchCreate extends Command {
async run() {
let {
args: { branchName },
args: { name },
flags,
} = this.parse(BranchCreate);
@@ -82,14 +84,14 @@ export default class BranchCreate extends Command {
projectName: exp.slug,
});
if (!branchName) {
if (!name) {
const validationMessage = 'Branch name may not be empty.';
if (flags.json) {
throw new Error(validationMessage);
}
({ branchName } = await promptAsync({
({ name } = await promptAsync({
type: 'text',
name: 'branchName',
name: 'name',
message: 'Please name the branch:',
initial:
(await getBranchNameAsync()) || `branch-${Math.random().toString(36).substr(2, 4)}`,
@@ -97,7 +99,7 @@ export default class BranchCreate extends Command {
}));
}
const newBranch = await createUpdateBranchOnAppAsync({ appId: projectId, branchName });
const newBranch = await createUpdateBranchOnAppAsync({ appId: projectId, name });
if (flags.json) {
Log.log(newBranch);
@@ -105,7 +107,7 @@ export default class BranchCreate extends Command {
}
Log.withTick(
`️Created a new branch: ${chalk.bold(newBranch.branchName)} on project ${chalk.bold(
`️Created a new branch: ${chalk.bold(newBranch.name)} on project ${chalk.bold(
`@${accountName}/${exp.slug}`
)}.`
);
@@ -0,0 +1,156 @@
import { getConfig } from '@expo/config';
import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
DeleteUpdateBranchMutation,
DeleteUpdateBranchMutationVariables,
DeleteUpdateBranchResult,
GetBranchInfoQuery,
GetBranchInfoQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import { findProjectRootAsync, getProjectAccountName } from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions';
async function getBranchInfoAsync({
appId,
name,
}: GetBranchInfoQueryVariables): Promise<{ branchId: string }> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<GetBranchInfoQuery, GetBranchInfoQueryVariables>(
gql`
query GetBranchInfo($appId: String!, $name: String!) {
app {
byId(appId: $appId) {
id
updateBranchByName(name: $name) {
id
name
}
}
}
}
`,
{
appId,
name,
}
)
.toPromise()
);
const branchId = data.app?.byId.updateBranchByName.id;
if (!branchId) {
throw new Error(`Could not find branch ${name} on ${appId}`);
}
return { branchId };
}
async function deleteBranchOnAppAsync({
branchId,
}: DeleteUpdateBranchMutationVariables): Promise<DeleteUpdateBranchResult> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<DeleteUpdateBranchMutation, DeleteUpdateBranchMutationVariables>(
gql`
mutation DeleteUpdateBranch($branchId: ID!) {
updateBranch {
deleteUpdateBranch(branchId: $branchId) {
id
}
}
}
`,
{
branchId,
}
)
.toPromise()
);
return data.updateBranch.deleteUpdateBranch;
}
export default class BranchDelete extends Command {
static hidden = true;
static description = 'Republish an update group';
static args = [
{
name: 'name',
required: false,
description: 'Name of the branch to delete',
},
];
static flags = {
json: flags.boolean({
description: `return JSON with the edited branch's ID and name.`,
default: false,
}),
};
async run() {
let {
args: { name },
flags: { json: jsonFlag },
} = this.parse(BranchDelete);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = await getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const projectId = await ensureProjectExistsAsync({
accountName,
projectName: exp.slug,
});
if (!name) {
const validationMessage = 'branch name may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ name } = await promptAsync({
type: 'text',
name: 'name',
message: 'Please enter the name of the branch to delete:',
validate: value => (value ? true : validationMessage),
}));
}
const { branchId } = await getBranchInfoAsync({ appId: projectId, name });
if (!jsonFlag) {
Log.addNewLineIfNone();
Log.warn(
`You are about to permamently delete branch: "${name}" and all of the updates published on it.` +
`\nThis action is irreversable.`
);
Log.newLine();
const confirmed = await toggleConfirmAsync({ message: 'Are you sure you wish to proceed?' });
if (!confirmed) {
Log.error(`Cancelled deletion of branch: "${name}".`);
process.exit(1);
}
}
const deletionResult = await deleteBranchOnAppAsync({
branchId,
});
if (jsonFlag) {
Log.log(JSON.stringify(deletionResult));
}
Log.withTick(
`️Deleted branch "${name}" and all of its updates on project ${chalk.bold(
`@${accountName}/${exp.slug}`
)}.`
);
}
}
+57 -47
View File
@@ -1,11 +1,18 @@
import { Command, flags } from '@oclif/command';
import { CLIError } from '@oclif/errors';
import CliTable from 'cli-table3';
import gql from 'graphql-tag';
import { format } from 'timeago.js';
import { graphqlClient } from '../../graphql/client';
import { RootQuery, Update, UpdateBranch } from '../../graphql/generated';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
BranchesByAppQuery,
BranchesByAppQueryVariables,
Maybe,
Robot,
Update,
UpdateBranch,
User,
} from '../../graphql/generated';
import Log from '../../log';
import { findProjectRootAsync, getProjectFullNameAsync } from '../../project/projectUtils';
import { getActorDisplayName } from '../../user/actions';
@@ -37,12 +44,7 @@ export default class BranchList extends Command {
Log.log(JSON.stringify(branches, null, 2));
} else {
const table = new CliTable({ head: ['Branch', 'Latest update'] });
table.push(
...branches.map((branch: UpdateBranch) => [
branch.branchName,
formatUpdate(branch.updates[0]),
])
);
table.push(...branches.map(branch => [branch.name, formatUpdate(branch.updates[0])]));
Log.log(table.toString());
if (branches.length >= BRANCHES_LIMIT) {
Log.warn(`Showing first ${BRANCHES_LIMIT} branches, some results might be omitted.`);
@@ -50,62 +52,70 @@ export default class BranchList extends Command {
}
}
async listBranchesAsync({ fullName }: { fullName: string }): Promise<UpdateBranch[]> {
const { data, error } = await graphqlClient
.query<RootQuery>(
gql`
query BranchesByAppQuery($fullName: String!, $limit: Int!) {
app {
byFullName(fullName: $fullName) {
id
fullName
updateBranches(offset: 0, limit: $limit) {
async listBranchesAsync({
fullName,
}: {
fullName: string;
}): Promise<
(Pick<UpdateBranch, 'id' | 'name'> & {
updates: (Pick<Update, 'id' | 'updatedAt' | 'message'> & {
actor?: Maybe<Pick<User, 'username' | 'id'> | Pick<Robot, 'firstName' | 'id'>>;
})[];
})[]
> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<BranchesByAppQuery, BranchesByAppQueryVariables>(
gql`
query BranchesByAppQuery($fullName: String!, $limit: Int!) {
app {
byFullName(fullName: $fullName) {
id
branchName
updates(offset: 0, limit: 1) {
fullName
updateBranches(offset: 0, limit: $limit) {
id
actor {
__typename
name
updates(offset: 0, limit: 1) {
id
... on User {
username
}
... on Robot {
firstName
actor {
__typename
id
... on User {
username
}
... on Robot {
firstName
}
}
updatedAt
message
}
updatedAt
updateMessage
}
}
}
}
`,
{
fullName,
limit: BRANCHES_LIMIT,
}
`,
{
fullName,
limit: BRANCHES_LIMIT,
}
)
.toPromise();
if (error) {
if (error.networkError) {
throw new CLIError(`Fetching branches failed: ${error.networkError.message}`);
} else {
throw new CLIError(error.graphQLErrors.map(e => e.message).join('\n'));
}
}
)
.toPromise()
);
return data?.app?.byFullName.updateBranches ?? [];
}
}
function formatUpdate(update: Update | undefined): string {
function formatUpdate(
update: Pick<Update, 'id' | 'updatedAt' | 'message'> & {
actor?: Maybe<Pick<User, 'username' | 'id'> | Pick<Robot, 'firstName' | 'id'>>;
}
): string {
if (!update) {
return 'N/A';
}
const message = update.updateMessage ? `"${update.updateMessage}" ` : '';
const message = update.message ? `"${update.message}" ` : '';
return `${message}(${format(update.updatedAt, 'en_US')} by ${getActorDisplayName(
update.actor as any
)})`;
+32 -13
View File
@@ -12,7 +12,12 @@ import {
getBranchByNameAsync,
getProjectAccountNameAsync,
} from '../../project/projectUtils';
import { buildUpdateInfoGroupAsync, collectAssets, uploadAssetsAsync } from '../../project/publish';
import {
buildBundlesAsync,
buildUpdateInfoGroupAsync,
collectAssets,
uploadAssetsAsync,
} from '../../project/publish';
import { promptAsync } from '../../prompts';
import { getLastCommitMessageAsync } from '../../utils/git';
@@ -37,11 +42,21 @@ export default class BranchPublish extends Command {
description: `return a json with the edited branch's ID and name.`,
default: false,
}),
'skip-bundler': flags.boolean({
description: `skip running Expo CLI to bundle the app before publishing`,
default: false,
}),
};
async run() {
let {
flags: { json: jsonFlag, branch: branchName, message: updateMessage, 'input-dir': inputDir },
flags: {
json: jsonFlag,
branch: name,
message,
'input-dir': inputDir,
'skip-bundler': skipBundler,
},
} = this.parse(BranchPublish);
const projectDir = await findProjectRootAsync(process.cwd());
@@ -63,25 +78,25 @@ export default class BranchPublish extends Command {
projectName: slug,
});
if (!branchName) {
if (!name) {
const validationMessage = 'branch name may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ branchName } = await promptAsync({
({ name } = await promptAsync({
type: 'text',
name: 'branchName',
name: 'name',
message: 'Please enter the name of the branch to publish on:',
validate: value => (value ? true : validationMessage),
}));
}
if (!updateMessage) {
if (!message) {
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ publishMessage: updateMessage } = await promptAsync({
({ publishMessage: message } = await promptAsync({
type: 'text',
name: 'publishMessage',
message: `Please enter a publication message.`,
@@ -92,9 +107,13 @@ export default class BranchPublish extends Command {
const { id: branchId } = await getBranchByNameAsync({
appId: projectId,
branchName: branchName!,
name: name!,
});
if (!skipBundler) {
await buildBundlesAsync({ projectDir, inputDir });
}
let updateInfoGroup;
const assetSpinner = ora('Uploading assets...').start();
try {
@@ -114,7 +133,7 @@ export default class BranchPublish extends Command {
branchId,
updateInfoGroup,
runtimeVersion,
updateMessage,
message,
});
publishSpinner.succeed('Published!');
} catch (e) {
@@ -123,7 +142,7 @@ export default class BranchPublish extends Command {
}
if (jsonFlag) {
Log.log(newUpdateGroup);
Log.log(JSON.stringify(newUpdateGroup));
} else {
const outputMessage = new Table({
wordWrap: true,
@@ -148,10 +167,10 @@ export default class BranchPublish extends Command {
});
outputMessage.push(
[chalk.dim('project'), `@${accountName}/${slug}`],
[chalk.dim('branch'), branchName],
[chalk.dim('branch'), name],
[chalk.dim('runtimeVersion'), runtimeVersion],
[chalk.dim('updateGroupID'), newUpdateGroup.updateGroup],
[chalk.dim('message'), updateMessage]
[chalk.dim('groupID'), newUpdateGroup.group],
[chalk.dim('message'), message]
);
Log.log(outputMessage.toString());
}
+15 -19
View File
@@ -4,7 +4,12 @@ import chalk from 'chalk';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { EditUpdateBranchInput, UpdateBranch } from '../../graphql/generated';
import {
EditUpdateBranchInput,
EditUpdateBranchMutation,
EditUpdateBranchMutationVariables,
UpdateBranch,
} from '../../graphql/generated';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import { findProjectRootAsync, getProjectAccountName } from '../../project/projectUtils';
@@ -13,27 +18,18 @@ import { ensureLoggedInAsync } from '../../user/actions';
async function renameUpdateBranchOnAppAsync({
appId,
currentName,
name,
newName,
}: {
appId: string;
currentName: string;
newName: string;
}): Promise<UpdateBranch> {
}: EditUpdateBranchInput): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<
{ updateBranch: { editUpdateBranch: UpdateBranch } },
{
input: EditUpdateBranchInput;
}
>(
.mutation<EditUpdateBranchMutation, EditUpdateBranchMutationVariables>(
gql`
mutation EditUpdateBranch($input: EditUpdateBranchInput!) {
updateBranch {
editUpdateBranch(input: $input) {
id
branchName
name
}
}
}
@@ -41,8 +37,8 @@ async function renameUpdateBranchOnAppAsync({
{
input: {
appId,
branchName: currentName,
newBranchName: newName,
name,
newName,
},
}
)
@@ -114,18 +110,18 @@ export default class BranchRename extends Command {
const editedBranch = await renameUpdateBranchOnAppAsync({
appId: projectId,
currentName: currentName!,
name: currentName!,
newName: newName!,
});
if (jsonFlag) {
Log.log(editedBranch);
Log.log(JSON.stringify(editedBranch));
return;
}
Log.withTick(
`️Renamed branch from ${currentName} to ${chalk.bold(
editedBranch.branchName
editedBranch.name
)} on project ${chalk.bold(`@${accountName}/${exp.slug}`)}.`
);
}
+46 -52
View File
@@ -6,7 +6,15 @@ import gql from 'graphql-tag';
import { groupBy } from 'lodash';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { Update } from '../../graphql/generated';
import {
Maybe,
Robot,
Update,
UpdateBranch,
User,
ViewBranchQuery,
ViewBranchQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
@@ -14,51 +22,33 @@ import { promptAsync } from '../../prompts';
const PAGE_LIMIT = 10_000;
type TruncatedUpdate = Pick<Update, 'updateGroup' | 'updateMessage' | 'createdAt' | 'actor'>;
type TruncatedUpdate = Pick<Update, 'group' | 'message' | 'createdAt' | 'actor'>;
async function viewUpdateBranchAsync({
appId,
branchName,
}: {
appId: string;
branchName: string;
}): Promise<{
id: string;
branchName: string;
updates: TruncatedUpdate[];
}> {
name,
}: Pick<ViewBranchQueryVariables, 'appId' | 'name'>): Promise<
Pick<UpdateBranch, 'id' | 'name'> & {
updates: (Pick<Update, 'id' | 'group' | 'message' | 'createdAt'> & {
actor?: Maybe<Pick<User, 'firstName' | 'id'> | Pick<Robot, 'firstName' | 'id'>>;
})[];
}
> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<
{
app: {
byId: {
updateBranchByBranchName: {
id: string;
branchName: string;
updates: TruncatedUpdate[];
};
};
};
},
{
appId: string;
branchName: string;
limit: number;
}
>(
.query<ViewBranchQuery, ViewBranchQueryVariables>(
gql`
query ViewBranch($appId: String!, $branchName: String!, $limit: Int!) {
query ViewBranch($appId: String!, $name: String!, $limit: Int!) {
app {
byId(appId: $appId) {
id
updateBranchByBranchName(branchName: $branchName) {
updateBranchByName(name: $name) {
id
branchName
name
updates(offset: 0, limit: $limit) {
id
updateGroup
updateMessage
group
message
createdAt
actor {
id
@@ -77,13 +67,17 @@ async function viewUpdateBranchAsync({
`,
{
appId,
branchName,
name,
limit: PAGE_LIMIT,
}
)
.toPromise()
);
return data.app.byId.updateBranchByBranchName;
const updateBranch = data.app?.byId.updateBranchByName;
if (!updateBranch) {
throw new Error(`Could not find branch "${name}"`);
}
return updateBranch;
}
export default class BranchView extends Command {
@@ -92,7 +86,7 @@ export default class BranchView extends Command {
static args = [
{
name: 'branchName',
name: 'name',
required: false,
description: 'Name of the branch to view',
},
@@ -107,7 +101,7 @@ export default class BranchView extends Command {
async run() {
let {
args: { branchName },
args: { name },
flags: { json: jsonFlag },
} = this.parse(BranchView);
@@ -124,14 +118,14 @@ export default class BranchView extends Command {
projectName: slug,
});
if (!branchName) {
if (!name) {
const validationMessage = 'Branch name may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ branchName } = await promptAsync({
({ name } = await promptAsync({
type: 'text',
name: 'branchName',
name: 'name',
message: 'Please enter the name of the branch to view:',
validate: value => (value ? true : validationMessage),
}));
@@ -139,38 +133,38 @@ export default class BranchView extends Command {
const UpdateBranch = await viewUpdateBranchAsync({
appId: projectId,
branchName,
name,
});
const updates = Object.values(groupBy(UpdateBranch.updates, u => u.updateGroup)).map(
updateGroup => updateGroup[0]
const updates = Object.values(groupBy(UpdateBranch.updates, u => u.group)).map(
group => group[0]
);
if (jsonFlag) {
Log.log({ ...UpdateBranch, updates });
Log.log(JSON.stringify({ ...UpdateBranch, updates }));
return;
}
const updateGroupTable = new Table({
head: ['createdAt', 'updateMessage', 'updateGroup', 'actor'],
const groupTable = new Table({
head: ['createdAt', 'message', 'group', 'actor'],
wordWrap: true,
});
for (const update of updates) {
updateGroupTable.push([
groupTable.push([
new Date(update.createdAt).toLocaleString(),
update.updateMessage,
update.updateGroup,
update.message,
update.group,
update.actor?.firstName,
]);
}
Log.withTick(
`️Branch: ${chalk.bold(UpdateBranch.branchName)} on project ${chalk.bold(
`️Branch: ${chalk.bold(UpdateBranch.name)} on project ${chalk.bold(
`@${accountName}/${slug}`
)}. Branch ID: ${chalk.bold(UpdateBranch.id)}`
);
Log.log(chalk.bold('Recent update groups published on this branch:'));
Log.log(updateGroupTable.toString());
Log.log(groupTable.toString());
}
}
+10 -4
View File
@@ -6,7 +6,13 @@ import ora from 'ora';
import { platformEmojis } from '../../build/constants';
import { Platform } from '../../build/types';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { AppPlatform, Build, BuildStatus } from '../../graphql/generated';
import {
AppPlatform,
Build,
BuildStatus,
CancelBuildMutation,
CancelBuildMutationVariables,
} from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log';
import {
@@ -19,7 +25,7 @@ import { confirmAsync, selectAsync } from '../../prompts';
async function cancelBuildAsync(buildId: string): Promise<Pick<Build, 'id' | 'status'>> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<{ build: { cancel: { id: string; status: BuildStatus } } }>(
.mutation<CancelBuildMutation, CancelBuildMutationVariables>(
gql`
mutation CancelBuildMutation($buildId: ID!) {
build(buildId: $buildId) {
@@ -38,8 +44,8 @@ async function cancelBuildAsync(buildId: string): Promise<Pick<Build, 'id' | 'st
}
const appPlatformMap = {
[AppPlatform.Android]: Platform.Android,
[AppPlatform.Ios]: Platform.iOS,
[AppPlatform.Android]: Platform.ANDROID,
[AppPlatform.Ios]: Platform.IOS,
};
function formatUnfinishedBuild(
+28 -29
View File
@@ -4,7 +4,10 @@ import chalk from 'chalk';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { UpdateChannel } from '../../graphql/generated';
import {
CreateUpdateChannelOnAppMutation,
CreateUpdateChannelOnAppMutationVariables,
} from '../../graphql/generated';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import {
@@ -23,32 +26,21 @@ async function createUpdateChannelOnAppAsync({
appId: string;
channelName: string;
branchId: string;
}): Promise<UpdateChannel> {
}): Promise<CreateUpdateChannelOnAppMutation> {
// Point the new channel at a branch with its same name.
const branchMapping = JSON.stringify({
data: [{ branchId, branchMappingLogic: 'true' }],
version: 0,
});
const data = await withErrorHandlingAsync(
return await withErrorHandlingAsync(
graphqlClient
.mutation<
{ updateChannel: { createUpdateChannelForApp: UpdateChannel } },
{ appId: string; channelName: string; branchMapping: string }
>(
.mutation<CreateUpdateChannelOnAppMutation, CreateUpdateChannelOnAppMutationVariables>(
gql`
mutation CreateUpdateChannelForApp(
$appId: ID!
$channelName: String!
$branchMapping: String!
) {
mutation CreateUpdateChannelOnApp($appId: ID!, $name: String!, $branchMapping: String!) {
updateChannel {
createUpdateChannelForApp(
appId: $appId
channelName: $channelName
branchMapping: $branchMapping
) {
createUpdateChannelForApp(appId: $appId, name: $name, branchMapping: $branchMapping) {
id
channelName
name
branchMapping
}
}
@@ -56,13 +48,12 @@ async function createUpdateChannelOnAppAsync({
`,
{
appId,
channelName,
name: channelName,
branchMapping,
}
)
.toPromise()
);
return data.updateChannel.createUpdateChannelForApp;
}
export default class ChannelCreate extends Command {
@@ -71,7 +62,7 @@ export default class ChannelCreate extends Command {
static args = [
{
name: 'channelName',
name: 'name',
required: false,
description: 'Name of the channel to create',
},
@@ -87,7 +78,7 @@ export default class ChannelCreate extends Command {
async run() {
let {
args: { channelName },
args: { name: channelName },
flags: { json: jsonFlag },
} = this.parse(ChannelCreate);
@@ -109,9 +100,9 @@ export default class ChannelCreate extends Command {
if (jsonFlag) {
throw new Error(validationMessage);
}
({ channelName } = await promptAsync({
({ name: channelName } = await promptAsync({
type: 'text',
name: 'channelName',
name: 'name',
message: 'Please name the channel:',
validate: value => (value ? true : validationMessage),
}));
@@ -122,32 +113,40 @@ export default class ChannelCreate extends Command {
try {
const existingBranch = await getBranchByNameAsync({
appId: projectId,
branchName: channelName,
name: channelName,
});
branchId = existingBranch.id;
branchMessage = `We found a branch with the same name`;
} catch (e) {
const newBranch = await createUpdateBranchOnAppAsync({
appId: projectId,
branchName: channelName,
name: channelName,
});
branchId = newBranch.id;
branchMessage = `We also went ahead and made a branch with the same name`;
}
const newChannel = await createUpdateChannelOnAppAsync({
const {
updateChannel: { createUpdateChannelForApp: newChannel },
} = await createUpdateChannelOnAppAsync({
appId: projectId,
channelName,
branchId,
});
if (!newChannel) {
throw new Error(
`Could not create channel with name ${channelName} on app with id ${projectId}`
);
}
if (jsonFlag) {
Log.log(newChannel);
Log.log(JSON.stringify(newChannel));
return;
}
Log.withTick(
`️Created a new channel ${chalk.bold(newChannel.channelName)} on project ${chalk.bold(
`️Created a new channel ${chalk.bold(newChannel.name)} on project ${chalk.bold(
`@${accountName}/${slug}`
)}. ${branchMessage} and have pointed the channel at it. You can now update your app by publishing!`
);
@@ -0,0 +1,196 @@
import { getConfig } from '@expo/config';
import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
GetChannelByNameToEditQuery,
GetChannelByNameToEditQueryVariables,
UpdateBranch,
UpdateChannel,
UpdateChannelBranchMappingMutation,
UpdateChannelBranchMappingMutationVariables,
} from '../../graphql/generated';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import {
findProjectRootAsync,
getBranchByNameAsync,
getProjectAccountNameAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
async function getChannelByNameForAppAsync({
appId,
channelName,
}: GetChannelByNameToEditQueryVariables): Promise<
Pick<UpdateChannel, 'id' | 'name'> & {
updateBranches: Pick<UpdateBranch, 'id' | 'name'>[];
}
> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<GetChannelByNameToEditQuery, GetChannelByNameToEditQueryVariables>(
gql`
query GetChannelByNameToEdit($appId: String!, $channelName: String!) {
app {
byId(appId: $appId) {
id
updateChannelByName(name: $channelName) {
id
name
updateBranches(offset: 0, limit: 1) {
id
name
}
}
}
}
}
`,
{ appId, channelName }
)
.toPromise()
);
const updateChannelByNameResult = data.app?.byId.updateChannelByName;
if (!updateChannelByNameResult) {
throw new Error(`Could not find a channel named ${channelName} on app with id ${appId}`);
}
return updateChannelByNameResult;
}
async function updateChannelBranchMappingAsync({
channelId,
branchMapping,
}: UpdateChannelBranchMappingMutationVariables): Promise<{
id: string;
name: string;
branchMapping: string;
}> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<UpdateChannelBranchMappingMutation, UpdateChannelBranchMappingMutationVariables>(
gql`
mutation UpdateChannelBranchMapping($channelId: ID!, $branchMapping: String!) {
updateChannel {
editUpdateChannel(channelId: $channelId, branchMapping: $branchMapping) {
id
name
branchMapping
}
}
}
`,
{ channelId, branchMapping }
)
.toPromise()
);
const channel = data.updateChannel.editUpdateChannel;
if (!channel) {
throw new Error(`Could not fine channel with id ${channelId}`);
}
return data.updateChannel.editUpdateChannel!;
}
export default class ChannelEdit extends Command {
static hidden = true;
static description = 'Point a channel at a new branch.';
static args = [
{
name: 'name',
required: false,
description: 'Name of the channel to edit',
},
];
static flags = {
branch: flags.string({
description: 'Name of the branch to point to',
}),
json: flags.boolean({
description: 'print output as a JSON object with the channel ID, name and branch mapping.',
default: false,
}),
};
async run() {
let {
args: { name: channelName },
flags: { branch: branchName, json: jsonFlag },
} = this.parse(ChannelEdit);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const accountName = await getProjectAccountNameAsync(projectDir);
const {
exp: { slug },
} = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await ensureProjectExistsAsync({
accountName,
projectName: slug,
});
if (!channelName) {
const validationMessage = 'A channel name is required to edit a specific channel.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ name: channelName } = await promptAsync({
type: 'text',
name: 'name',
message: 'Please enter the name of the channel to edit:',
validate: value => (value ? true : validationMessage),
}));
}
const existingChannel = await getChannelByNameForAppAsync({ appId: projectId, channelName });
// todo: refactor when multiple branches per channel are available
const existingBranch = existingChannel.updateBranches[0];
Log.addNewLineIfNone();
Log.log(
chalk`Channel {bold ${existingChannel.name}} is currently set to branch {bold ${existingBranch.name}}`
);
Log.addNewLineIfNone();
if (!branchName) {
const validationMessage = 'branch name may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ name: branchName } = await promptAsync({
type: 'text',
name: 'name',
message: chalk`What branch should it change to?`,
validate: value => (value ? true : validationMessage),
}));
}
const branch = await getBranchByNameAsync({ appId: projectId, name: branchName! });
const channel = await updateChannelBranchMappingAsync({
channelId: existingChannel.id,
// todo: move branch mapping logic to utility
branchMapping: JSON.stringify({
data: [{ branchId: branch.id, branchMappingLogic: 'true' }],
version: 0,
}),
});
if (jsonFlag) {
Log.log(JSON.stringify(channel));
return;
}
Log.withTick(
chalk`Channel {bold ${channel.name}} is now set to branch {bold ${branch.name}}.\n`
);
Log.log(
chalk`Users with builds on channel {bold ${channel.name}} will now receive the active update on {bold ${branch.name}}.`
);
}
}
@@ -0,0 +1,129 @@
import { getConfig } from '@expo/config';
import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import Table from 'cli-table3';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
GetAllChannelsForAppQuery,
GetAllChannelsForAppQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
const CHANNEL_LIMIT = 10_000;
async function getAllUpdateChannelForAppAsync({
appId,
}: {
appId: string;
}): Promise<GetAllChannelsForAppQuery> {
return await withErrorHandlingAsync(
graphqlClient
.query<GetAllChannelsForAppQuery, GetAllChannelsForAppQueryVariables>(
gql`
query GetAllChannelsForApp($appId: String!, $offset: Int!, $limit: Int!) {
app {
byId(appId: $appId) {
id
updateChannels(offset: $offset, limit: $limit) {
id
name
updateBranches(offset: 0, limit: 1) {
id
name
updates(offset: 0, limit: 1) {
id
group
message
createdAt
actor {
id
... on User {
firstName
}
... on Robot {
firstName
}
}
}
}
}
}
}
}
`,
{ appId, offset: 0, limit: CHANNEL_LIMIT }
)
.toPromise()
);
}
export default class ChannelList extends Command {
static hidden = true;
static description = 'List all channels on the current project.';
static flags = {
json: flags.boolean({
description: 'print output as a JSON object with the channel ID, name and branch mapping.',
default: false,
}),
};
async run() {
const {
flags: { json: jsonFlag },
} = this.parse(ChannelList);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const accountName = await getProjectAccountNameAsync(projectDir);
const {
exp: { slug },
} = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await ensureProjectExistsAsync({
accountName,
projectName: slug,
});
const getAllUpdateChannelForAppResult = await getAllUpdateChannelForAppAsync({
appId: projectId,
});
const channels = getAllUpdateChannelForAppResult.app?.byId.updateChannels;
if (!channels) {
throw new Error(`Could not find channels on project with id ${projectId}`);
}
if (jsonFlag) {
Log.log(JSON.stringify(channels));
return;
}
const table = new Table({
head: ['channel', 'branch', 'update', 'message', 'created-at', 'actor'],
wordWrap: true,
});
for (const channel of channels) {
// TODO (cedric): refactor when multiple branches per channel are available
const branch = channel.updateBranches[0];
const update = branch.updates[0];
table.push([
channel.name,
branch.name,
update?.group,
update?.message,
update?.createdAt && new Date(update.createdAt).toLocaleString(),
update?.actor?.firstName,
]);
}
Log.log(chalk`{bold Channels for this app:}`);
Log.log(table.toString());
}
}
@@ -0,0 +1,152 @@
import { getConfig } from '@expo/config';
import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import Table from 'cli-table3';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
GetChannelByNameForAppQuery,
GetChannelByNameForAppQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
async function getUpdateChannelByNameForAppAsync({
appId,
channelName,
}: GetChannelByNameForAppQueryVariables): Promise<GetChannelByNameForAppQuery> {
return await withErrorHandlingAsync(
graphqlClient
.query<GetChannelByNameForAppQuery, GetChannelByNameForAppQueryVariables>(
gql`
query GetChannelByNameForApp($appId: String!, $channelName: String!) {
app {
byId(appId: $appId) {
id
updateChannelByName(name: $channelName) {
id
name
createdAt
updateBranches(offset: 0, limit: 1) {
id
name
updates(offset: 0, limit: 1) {
id
group
message
createdAt
actor {
id
... on User {
firstName
}
... on Robot {
firstName
}
}
}
}
}
}
}
}
`,
{ appId, channelName }
)
.toPromise()
);
}
export default class ChannelView extends Command {
static hidden = true;
static description = 'View a channel on the current project.';
static args = [
{
name: 'name',
required: false,
description: 'Name of the channel to view',
},
];
static flags = {
json: flags.boolean({
description: 'print output as a JSON object with the channel ID, name and branch mapping.',
default: false,
}),
};
async run() {
let {
args: { name: channelName },
flags: { json: jsonFlag },
} = this.parse(ChannelView);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const accountName = await getProjectAccountNameAsync(projectDir);
const {
exp: { slug },
} = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await ensureProjectExistsAsync({
accountName,
projectName: slug,
});
if (!channelName) {
const validationMessage = 'A channel name is required to view a specific channel.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ name: channelName } = await promptAsync({
type: 'text',
name: 'name',
message: 'Please name the channel:',
validate: value => (value ? true : validationMessage),
}));
}
const getUpdateChannelByNameForAppresult = await getUpdateChannelByNameForAppAsync({
appId: projectId,
channelName,
});
const channel = getUpdateChannelByNameForAppresult.app?.byId.updateChannelByName;
if (!channel) {
throw new Error(`Could not fine channel with name ${channelName}`);
}
if (jsonFlag) {
Log.log(JSON.stringify(channel));
return;
}
const table = new Table({
head: ['branch', 'update', 'message', 'created-at', 'actor'],
wordWrap: true,
});
for (const branch of channel.updateBranches) {
// TODO (cedric): refactor when multiple branches per channel are available
const update = branch.updates[0];
table.push([
branch.name,
update?.group,
update?.message,
update?.createdAt && new Date(update.createdAt).toLocaleString(),
update?.actor?.firstName,
]);
}
Log.withTick(
chalk`Channel: {bold ${channel.name}} on project {bold ${accountName}/${slug}}. Channel ID: {bold ${channel.id}}`
);
Log.log(chalk`{bold Recent update groups published on this branch:}`);
Log.log(table.toString());
}
}
+6 -2
View File
@@ -42,7 +42,9 @@ export default class BuildList extends Command {
name: 'appleTeamIdentifier',
message: 'What Apple Team would you like to list devices for?',
choices: teams.map(team => ({
title: `${team.appleTeamName} (ID: ${team.appleTeamIdentifier})`,
title: team.appleTeamName
? `${team.appleTeamName} (ID: ${team.appleTeamIdentifier})`
: team.appleTeamIdentifier,
value: team.appleTeamIdentifier,
})),
});
@@ -71,7 +73,9 @@ export default class BuildList extends Command {
if (result?.appleDevices.length) {
const { appleTeamName, appleDevices } = result;
spinner.succeed(`Found ${appleDevices.length} devices for team ${appleTeamName}`);
spinner.succeed(
`Found ${appleDevices.length} devices for team ${appleTeamName ?? appleTeamIdentifier}`
);
const list = appleDevices
.map(device =>
@@ -24,7 +24,7 @@ interface Options {
}
export default class AndroidCredentialsProvider implements CredentialsProvider {
public readonly platform = Platform.Android;
public readonly platform = Platform.ANDROID;
constructor(private ctx: Context, private options: Options) {}
@@ -1,7 +1,7 @@
import { Platform } from '@expo/eas-build-job';
import { CredentialsSource, DistributionType } from '@expo/eas-json';
import { CredentialsSource, iOSDistributionType } from '@expo/eas-json';
import { IosDistributionType } from '../../graphql/generated';
import { AppleTeamFragment, IosDistributionType } from '../../graphql/generated';
import Log from '../../log';
import { findAccountByName } from '../../user/Account';
import { CredentialsManager } from '../CredentialsManager';
@@ -10,6 +10,9 @@ import { Context } from '../context';
import * as credentialsJsonReader from '../credentialsJson/read';
import type { IosCredentials } from '../credentialsJson/read';
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
import { resolveAppleTeamIfAuthenticatedAsync } from './actions/new/AppleTeamUtils';
import { AppleTeamMissingError, MissingCredentialsNonInteractiveError } from './errors';
import { isAdHocProfile } from './utils/provisioningProfile';
export { IosCredentials };
@@ -23,7 +26,7 @@ interface PartialIosCredentials {
interface Options {
app: AppLookupParams;
distribution: DistributionType;
distribution: iOSDistributionType;
skipCredentialsCheck?: boolean;
}
@@ -34,11 +37,16 @@ interface AppLookupParams {
}
export default class IosCredentialsProvider implements CredentialsProvider {
public readonly platform = Platform.iOS;
public readonly platform = Platform.IOS;
constructor(private ctx: Context, private options: Options) {}
public async hasRemoteAsync(): Promise<boolean> {
// TODO: this is temporary
// remove this check when we implement syncing local credentials for internal distribution
if (this.options.distribution === 'internal' && (await this.hasLocalAsync())) {
return false;
}
const { distributionCertificate, provisioningProfile } = await this.fetchRemoteAsync();
return !!(
distributionCertificate?.certP12 ||
@@ -48,10 +56,6 @@ export default class IosCredentialsProvider implements CredentialsProvider {
}
public async hasLocalAsync(): Promise<boolean> {
if (this.options.distribution === DistributionType.INTERNAL) {
// TODO: add support for using credentials.json for internal distribution
return false;
}
if (!(await credentialsJsonReader.fileExistsAsync(this.ctx.projectDir))) {
return false;
}
@@ -102,11 +106,32 @@ export default class IosCredentialsProvider implements CredentialsProvider {
}
private async getLocalAsync(): Promise<IosCredentials> {
if (this.options.distribution === DistributionType.INTERNAL) {
// TODO: add support for using credentials.json for internal distribution
throw new Error('Using credentials.json for internal distribution is not supported yet.');
const credentials = await credentialsJsonReader.readIosCredentialsAsync(this.ctx.projectDir);
if (credentialsJsonReader.isCredentialsMap(credentials)) {
for (const targetName of Object.keys(credentials)) {
this.assertProvisioningProfileType(credentials[targetName].provisioningProfile, targetName);
}
} else {
this.assertProvisioningProfileType(credentials.provisioningProfile);
}
return credentials;
}
private assertProvisioningProfileType(provisionigProfile: string, targetName?: string) {
const isAdHoc = isAdHocProfile(provisionigProfile);
if (this.options.distribution === 'internal' && !isAdHoc) {
throw new Error(
`You must use an adhoc provisioning profile${
targetName && ` (target '${targetName})'`
} for internal distribution`
);
} else if (this.options.distribution !== 'internal' && isAdHoc) {
throw new Error(
`You can't use an adhoc provisioning profile${
targetName && ` (target '${targetName})'`
} for app store distribution`
);
}
return await credentialsJsonReader.readIosCredentialsAsync(this.ctx.projectDir);
}
private async getRemoteAsync(): Promise<IosCredentials> {
@@ -147,7 +172,7 @@ export default class IosCredentialsProvider implements CredentialsProvider {
}
private async fetchRemoteAsync(): Promise<PartialIosCredentials> {
if (this.options.distribution === DistributionType.INTERNAL) {
if (this.options.distribution === 'internal') {
const { app } = this.options;
const account = findAccountByName(this.ctx.user.accounts, app.accountName);
if (!account) {
@@ -160,31 +185,36 @@ export default class IosCredentialsProvider implements CredentialsProvider {
projectName: this.options.app.projectName,
};
// for now, let's require the user to authenticate with Apple
const { team } = await this.ctx.appStore.ensureAuthenticatedAsync();
const appleTeam = await this.ctx.newIos.createOrGetExistingAppleTeamAsync(appLookupParams, {
appleTeamIdentifier: team.id,
appleTeamName: team.name,
});
const [distCert, provisioningProfile] = await Promise.all([
this.ctx.newIos.getDistributionCertificateForAppAsync(
appLookupParams,
appleTeam,
IosDistributionType.AdHoc
),
this.ctx.newIos.getProvisioningProfileAsync(
appLookupParams,
appleTeam,
IosDistributionType.AdHoc
),
]);
return {
provisioningProfile: provisioningProfile?.provisioningProfile ?? undefined,
distributionCertificate: {
certP12: distCert?.certificateP12 ?? undefined,
certPassword: distCert?.certificatePassword ?? undefined,
},
};
let appleTeam: AppleTeamFragment | null = null;
if (!this.ctx.nonInteractive) {
await this.ctx.appStore.ensureAuthenticatedAsync();
appleTeam = await resolveAppleTeamIfAuthenticatedAsync(this.ctx, appLookupParams);
}
try {
const [distCert, provisioningProfile] = await Promise.all([
this.ctx.newIos.getDistributionCertificateForAppAsync(
appLookupParams,
IosDistributionType.AdHoc,
{ appleTeam }
),
this.ctx.newIos.getProvisioningProfileAsync(appLookupParams, IosDistributionType.AdHoc, {
appleTeam,
}),
]);
return {
provisioningProfile: provisioningProfile?.provisioningProfile ?? undefined,
distributionCertificate: {
certP12: distCert?.certificateP12 ?? undefined,
certPassword: distCert?.certificatePassword ?? undefined,
},
};
} catch (err) {
if (err instanceof AppleTeamMissingError && this.ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError();
}
throw err;
}
} else {
const [distCert, provisioningProfile] = await Promise.all([
this.ctx.ios.getDistributionCertificateAsync(this.options.app),
@@ -1,4 +1,4 @@
import { DistributionType } from '@expo/eas-json';
import { iOSDistributionType } from '@expo/eas-json';
import assert from 'assert';
import chalk from 'chalk';
@@ -21,7 +21,7 @@ type AppCredentialsAndDistCert = {
};
export class SetupBuildCredentials implements Action {
constructor(private app: AppLookupParams, private distribution: DistributionType) {}
constructor(private app: AppLookupParams, private distribution: iOSDistributionType) {}
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
await ctx.bestEffortAppStoreAuthenticateAsync();
@@ -32,16 +32,11 @@ export class SetupBuildCredentials implements Action {
let iosAppBuildCredentials: IosAppBuildCredentialsFragment | null = null;
try {
if (this.distribution === DistributionType.INTERNAL) {
if (this.distribution === 'internal') {
const account = findAccountByName(ctx.user.accounts, this.app.accountName);
if (!account) {
throw new Error(`You do not have access to the ${this.app.accountName} account`);
}
if (ctx.nonInteractive) {
throw new Error('Internal distribution builds are not supported in non-interactive mode');
}
// for now, let's require the user to authenticate with Apple
await ctx.appStore.ensureAuthenticatedAsync();
const action = new SetupAdhocProvisioningProfile({
account,
projectName: this.app.projectName,
@@ -11,7 +11,7 @@ export class UpdateCredentialsJson implements Action {
constructor(private app: AppLookupParams) {}
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
const bundleIdentifer = await ensureAppIdentifierIsDefinedAsync(ctx.projectDir, Platform.iOS);
const bundleIdentifer = await ensureAppIdentifierIsDefinedAsync(ctx.projectDir, Platform.IOS);
Log.log('Updating iOS credentials in credentials.json');
await updateIosCredentialsAsync(ctx, bundleIdentifer);
Log.succeed(
@@ -0,0 +1,16 @@
import { AppleTeamFragment } from '../../../../graphql/generated';
import { Context } from '../../../context';
import { AppLookupParams } from '../../api/GraphqlClient';
export async function resolveAppleTeamIfAuthenticatedAsync(
ctx: Context,
app: AppLookupParams
): Promise<AppleTeamFragment | null> {
if (!ctx.appStore.authCtx) {
return null;
}
return await ctx.newIos.createOrGetExistingAppleTeamAsync(app, {
appleTeamIdentifier: ctx.appStore.authCtx.team.id,
appleTeamName: ctx.appStore.authCtx.team.name,
});
}
@@ -6,6 +6,7 @@ import {
IosAppBuildCredentialsFragment,
IosDistributionType,
} from '../../../../graphql/generated';
import Log from '../../../../log';
import { confirmAsync } from '../../../../prompts';
import { Action, CredentialsManager } from '../../../CredentialsManager';
import { Context } from '../../../context';
@@ -15,6 +16,8 @@ import { AppleDeviceFragmentWithAppleTeam } from '../../api/graphql/queries/Appl
import { AppleProvisioningProfileQueryResult } from '../../api/graphql/queries/AppleProvisioningProfileQuery';
import { ProvisioningProfileStoreInfo } from '../../appstore/Credentials.types';
import { ProfileClass } from '../../appstore/provisioningProfile';
import { AppleTeamMissingError, MissingCredentialsNonInteractiveError } from '../../errors';
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { chooseDevices } from './DeviceUtils';
import { doUDIDsMatch, isDevPortalAdhocProfileValid } from './ProvisioningProfileUtils';
import { SetupDistributionCertificate } from './SetupDistributionCertificate';
@@ -33,15 +36,55 @@ export class SetupAdhocProvisioningProfile implements Action {
}
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
const authCtx = await ctx.appStore.ensureAuthenticatedAsync();
if (ctx.nonInteractive) {
try {
await this.runNonInteractiveAsync(manager, ctx);
} catch (err) {
if (err instanceof AppleTeamMissingError) {
throw new MissingCredentialsNonInteractiveError();
}
throw err;
}
} else {
await this.runInteractiveAsync(manager, ctx);
}
}
// 0. Fetch apple team object
const appleTeam = await ctx.newIos.createOrGetExistingAppleTeamAsync(this.app, {
appleTeamIdentifier: authCtx.team.id,
appleTeamName: authCtx.team.name,
});
private async runNonInteractiveAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
// 1. Setup Distribution Certificate
const distCertAction = new SetupDistributionCertificate(this.app);
await manager.runActionAsync(distCertAction);
// 1. Fetch devices registered on EAS servers.
// 2. Fetch profile from EAS servers
const currentProfile = await ctx.newIos.getProvisioningProfileAsync(
this.app,
IosDistributionType.AdHoc
);
if (!currentProfile) {
throw new MissingCredentialsNonInteractiveError();
}
// TODO: implement validation
Log.warn(
'Provisioning Profile is not validated for non-interactive internal distribution builds.'
);
// app credentials should exist here because the profile exists
const appCredentials = nullthrows(
await ctx.newIos.getIosAppCredentialsWithBuildCredentialsAsync(this.app, {
iosDistributionType: IosDistributionType.AdHoc,
})
);
this._iosAppBuildCredentials = appCredentials.iosAppBuildCredentialsArray[0];
}
private async runInteractiveAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
// 0. Ensure the user is authenticated with Apple and resolve the Apple team object
await ctx.appStore.ensureAuthenticatedAsync();
const appleTeam = nullthrows(await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app));
// 1. Fetch devices registered on EAS servers
const registeredAppleDevices = await ctx.newIos.getDevicesForAppleTeamAsync(
this.app,
appleTeam
@@ -58,17 +101,17 @@ export class SetupAdhocProvisioningProfile implements Action {
await manager.runActionAsync(distCertAction);
const distCert = distCertAction.distributionCertificate;
// 3. Fetch profile from EAS servers
let currentProfileFromExpoServers = (await ctx.newIos.getProvisioningProfileAsync(
this.app,
let profileFromExpoServersToUse:
| AppleProvisioningProfileQueryResult
| AppleProvisioningProfileMutationResult
| null = await ctx.newIos.getProvisioningProfileAsync(this.app, IosDistributionType.AdHoc, {
appleTeam,
IosDistributionType.AdHoc
)) as AppleProvisioningProfileQueryResult | AppleProvisioningProfileMutationResult | null;
});
// 4. Choose devices for internal distribution
let chosenDevices: AppleDeviceFragmentWithAppleTeam[];
if (currentProfileFromExpoServers) {
const appleDeviceIdentifiersFromProfile = ((currentProfileFromExpoServers.appleDevices ??
if (profileFromExpoServersToUse) {
const appleDeviceIdentifiersFromProfile = ((profileFromExpoServersToUse.appleDevices ??
[]) as AppleDevice[]).map(({ identifier }) => identifier);
let shouldAskToChooseDevices = true;
@@ -110,10 +153,10 @@ export class SetupAdhocProvisioningProfile implements Action {
this.app,
appleTeam
);
if (currentProfileFromExpoServers) {
if (profileFromExpoServersToUse) {
if (profileWasRecreated) {
currentProfileFromExpoServers = await ctx.newIos.updateProvisioningProfileAsync(
currentProfileFromExpoServers.id,
profileFromExpoServersToUse = await ctx.newIos.updateProvisioningProfileAsync(
profileFromExpoServersToUse.id,
{
appleProvisioningProfile: profileToUse.provisioningProfile,
developerPortalIdentifier: profileToUse.provisioningProfileId,
@@ -121,7 +164,7 @@ export class SetupAdhocProvisioningProfile implements Action {
);
}
} else {
currentProfileFromExpoServers = await ctx.newIos.createProvisioningProfileAsync(
profileFromExpoServersToUse = await ctx.newIos.createProvisioningProfileAsync(
this.app,
appleAppIdentifier,
{
@@ -138,7 +181,7 @@ export class SetupAdhocProvisioningProfile implements Action {
appleTeam,
appleAppIdentifierId: appleAppIdentifier.id,
appleDistributionCertificateId: distCert.id,
appleProvisioningProfileId: currentProfileFromExpoServers.id,
appleProvisioningProfileId: profileFromExpoServersToUse.id,
iosDistributionType: IosDistributionType.AdHoc,
}
);
@@ -13,6 +13,8 @@ import { Context } from '../../../context';
import { AppLookupParams } from '../../api/GraphqlClient';
import { AppleDistributionCertificateMutationResult } from '../../api/graphql/mutations/AppleDistributionCertificateMutation';
import { getValidCertSerialNumbers } from '../../appstore/CredentialsUtils';
import { AppleTeamMissingError, MissingCredentialsNonInteractiveError } from '../../errors';
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { CreateDistributionCertificate } from './CreateDistributionCertificate';
import { formatDistributionCertificate } from './DistributionCertificateUtils';
@@ -31,21 +33,48 @@ export class SetupDistributionCertificate implements Action {
}
public async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
assert(
ctx.appStore.authCtx,
'authCtx is defined in this context - enforced by ensureAuthenticatedAsync call in SetupBuildCredentials'
);
const appleTeam = await ctx.newIos.createOrGetExistingAppleTeamAsync(this.app, {
appleTeamIdentifier: ctx.appStore.authCtx.team.id,
appleTeamName: ctx.appStore.authCtx.team.name,
});
const appleTeam = await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app);
const currentCertificate = await ctx.newIos.getDistributionCertificateForAppAsync(
this.app,
appleTeam,
IosDistributionType.AdHoc
);
try {
const currentCertificate = await ctx.newIos.getDistributionCertificateForAppAsync(
this.app,
IosDistributionType.AdHoc,
{ appleTeam }
);
if (ctx.nonInteractive) {
await this.runNonInteractiveAsync(ctx, currentCertificate);
} else {
await this.runInteractiveAsync(ctx, manager, currentCertificate);
}
} catch (err) {
if (err instanceof AppleTeamMissingError && ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError();
}
throw err;
}
}
private async runNonInteractiveAsync(
ctx: Context,
currentCertificate: AppleDistributionCertificateFragment | null
): Promise<void> {
// TODO: implement validation
Log.addNewLineIfNone();
Log.warn(
'Distribution Certificate is not validated for non-interactive internal distribution builds.'
);
if (!currentCertificate) {
throw new MissingCredentialsNonInteractiveError();
}
this._distributionCertificate = currentCertificate;
}
private async runInteractiveAsync(
ctx: Context,
manager: CredentialsManager,
currentCertificate: AppleDistributionCertificateFragment | null
): Promise<void> {
if (await this.isCurrentCertificateValidAsync(ctx, currentCertificate)) {
assert(currentCertificate, 'currentCertificate is defined here');
this._distributionCertificate = currentCertificate;
@@ -11,6 +11,7 @@ import {
} from '../../../graphql/generated';
import { Account } from '../../../user/Account';
import { DistributionCertificate } from '../appstore/Credentials.types';
import { AppleTeamMissingError } from '../errors';
import { AppleAppIdentifierMutation } from './graphql/mutations/AppleAppIdentifierMutation';
import {
AppleDistributionCertificateMutation,
@@ -98,6 +99,25 @@ export async function createOrUpdateIosAppBuildCredentialsAsync(
}
export async function getIosAppCredentialsWithBuildCredentialsAsync(
appLookupParams: AppLookupParams,
{ iosDistributionType }: { iosDistributionType: IosDistributionType }
): Promise<IosAppCredentialsWithBuildCredentialsQueryResult | null> {
const { account, bundleIdentifier } = appLookupParams;
const appleAppIdentifier = await AppleAppIdentifierQuery.byBundleIdentifierAsync(
account.name,
bundleIdentifier
);
if (!appleAppIdentifier) {
return null;
}
const projectFullName = formatProjectFullName(appLookupParams);
return await IosAppCredentialsQuery.withBuildCredentialsByAppIdentifierIdAsync(projectFullName, {
appleAppIdentifierId: appleAppIdentifier.id,
iosDistributionType,
});
}
export async function getIosAppCredentialsWithCommonFieldsAsync(
appLookupParams: AppLookupParams
): Promise<CommonIosAppCredentialsFragment | null> {
const { account, bundleIdentifier } = appLookupParams;
@@ -126,12 +146,12 @@ export async function createOrGetExistingIosAppCredentialsWithBuildCredentialsAs
iosDistributionType: IosDistributionType;
}
): Promise<IosAppCredentialsWithBuildCredentialsQueryResult> {
const projectFullName = formatProjectFullName(appLookupParams);
const maybeIosAppCredentials = await IosAppCredentialsQuery.withBuildCredentialsByAppIdentifierIdAsync(
projectFullName,
{ appleAppIdentifierId, iosDistributionType }
const maybeIosAppCredentials = await getIosAppCredentialsWithBuildCredentialsAsync(
appLookupParams,
{
iosDistributionType,
}
);
if (maybeIosAppCredentials) {
return maybeIosAppCredentials;
} else {
@@ -144,6 +164,7 @@ export async function createOrGetExistingIosAppCredentialsWithBuildCredentialsAs
app.id,
appleAppIdentifier.id
);
const projectFullName = formatProjectFullName(appLookupParams);
return nullthrows(
await IosAppCredentialsQuery.withBuildCredentialsByAppIdentifierIdAsync(projectFullName, {
appleAppIdentifierId,
@@ -173,7 +194,7 @@ export async function createOrGetExistingAppleTeamAsync(
export async function createOrGetExistingAppleAppIdentifierAsync(
{ account, bundleIdentifier }: AppLookupParams,
appleTeam: AppleTeamFragment
appleTeam: AppleTeamFragment | null
): Promise<AppleAppIdentifierFragment> {
const appleAppIdentifier = await AppleAppIdentifierQuery.byBundleIdentifierAsync(
account.name,
@@ -182,6 +203,9 @@ export async function createOrGetExistingAppleAppIdentifierAsync(
if (appleAppIdentifier) {
return appleAppIdentifier;
} else {
if (!appleTeam) {
throw new AppleTeamMissingError();
}
return await AppleAppIdentifierMutation.createAppleAppIdentifierAsync(
{ bundleIdentifier, appleTeamId: appleTeam.id },
account.id
@@ -213,8 +237,8 @@ export async function createProvisioningProfileAsync(
export async function getProvisioningProfileAsync(
appLookupParams: AppLookupParams,
appleTeam: AppleTeamFragment,
iosDistributionType: IosDistributionType
iosDistributionType: IosDistributionType,
{ appleTeam }: { appleTeam: AppleTeamFragment | null } = { appleTeam: null }
): Promise<AppleProvisioningProfileQueryResult | null> {
const projectFullName = formatProjectFullName(appLookupParams);
const appleAppIdentifier = await createOrGetExistingAppleAppIdentifierAsync(
@@ -222,7 +246,7 @@ export async function getProvisioningProfileAsync(
appleTeam
);
return await AppleProvisioningProfileQuery.getForAppAsync(projectFullName, {
appleAppIdentifierId: appleAppIdentifier?.id,
appleAppIdentifierId: appleAppIdentifier.id,
iosDistributionType,
});
}
@@ -250,8 +274,8 @@ export async function deleteProvisioningProfilesAsync(
export async function getDistributionCertificateForAppAsync(
appLookupParams: AppLookupParams,
appleTeam: AppleTeamFragment,
iosDistributionType: IosDistributionType
iosDistributionType: IosDistributionType,
{ appleTeam }: { appleTeam: AppleTeamFragment | null } = { appleTeam: null }
): Promise<AppleDistributionCertificateFragment | null> {
const projectFullName = formatProjectFullName(appLookupParams);
const appleAppIdentifier = await createOrGetExistingAppleAppIdentifierAsync(
@@ -259,7 +283,7 @@ export async function getDistributionCertificateForAppAsync(
appleTeam
);
return await AppleDistributionCertificateQuery.getForAppAsync(projectFullName, {
appleAppIdentifierId: appleAppIdentifier?.id,
appleAppIdentifierId: appleAppIdentifier.id,
iosDistributionType,
});
}
@@ -0,0 +1,13 @@
export class AppleTeamMissingError extends Error {
constructor(message?: string) {
super(message ?? 'Apple Team is necessary to create Apple App Identifier');
}
}
export class MissingCredentialsNonInteractiveError extends Error {
constructor(message?: string) {
super(
message ?? 'Credentials are not set up. Please run this command again in interactive mode.'
);
}
}
@@ -20,6 +20,12 @@ function readProfileName(dataBase64: string): string {
return profilePlist['Name'] as string;
}
function isAdHocProfile(dataBase64: string): boolean {
const profilePlist = parse(dataBase64);
const provisionedDevices = profilePlist['ProvisionedDevices'] as string[] | undefined;
return Array.isArray(provisionedDevices);
}
function parse(dataBase64: string): PlistObject {
try {
const buffer = Buffer.from(dataBase64, 'base64');
@@ -30,4 +36,4 @@ function parse(dataBase64: string): PlistObject {
}
}
export { readAppleTeam, readProfileName, parse };
export { readAppleTeam, readProfileName, isAdHocProfile, parse };
@@ -1,5 +1,3 @@
import { DistributionType } from '@expo/eas-json';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
@@ -135,7 +133,7 @@ export class ManageIos implements Action {
}
case ActionType.SetupBuildCredentials: {
const app = this.getAppLookupParamsFromContext(ctx);
return new SetupBuildCredentials(app, DistributionType.STORE);
return new SetupBuildCredentials(app, 'store');
}
case ActionType.RemoveSpecificProvisioningProfile: {
const app = this.getAppLookupParamsFromContext(ctx);
@@ -40,7 +40,7 @@ export class ManageIosBeta implements Action {
}
if (ctx.hasProjectContext) {
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
const iosAppCredentials = await ctx.newIos.getIosAppCredentialsWithBuildCredentialsAsync(
const iosAppCredentials = await ctx.newIos.getIosAppCredentialsWithCommonFieldsAsync(
appLookupParams
);
if (!iosAppCredentials) {
+373 -22
View File
@@ -195,6 +195,8 @@ export type Account = {
applePushKeys: Array<ApplePushKey>;
appleProvisioningProfiles: Array<AppleProvisioningProfile>;
appleDevices: Array<AppleDevice>;
/** Environment secrets for an account */
environmentSecrets: Array<EnvironmentSecret>;
};
@@ -277,6 +279,15 @@ export type AccountAppleDevicesArgs = {
};
/**
* An account is a container owning projects, credentials, billing and other organization
* data and settings. Actors may own and be members of accounts.
*/
export type AccountEnvironmentSecretsArgs = {
filterNames?: Maybe<Array<Scalars['String']>>;
};
export type Offer = {
__typename?: 'Offer';
id: Scalars['ID'];
@@ -396,14 +407,16 @@ export type App = Project & {
androidAppCredentials: Array<AndroidAppCredentials>;
/** EAS channels owned by an app */
updateChannels: Array<UpdateChannel>;
/** get an EAS channel owned by the app by channelName */
updateChannelByChannelName: UpdateChannel;
/** get an EAS channel owned by the app by name */
updateChannelByName: UpdateChannel;
/** EAS branches owned by an app */
updateBranches: Array<UpdateBranch>;
/** get an EAS branch owned by the app by branchName */
updateBranchByBranchName: UpdateBranch;
/** get an EAS branch owned by the app by name */
updateBranchByName: UpdateBranch;
/** Coalesced project activity for an app. Use "createdBefore" to offset a query. */
activityTimelineProjectActivities: Array<ActivityTimelineProjectActivity>;
/** Environment secrets for an app */
environmentSecrets: Array<EnvironmentSecret>;
};
@@ -458,8 +471,8 @@ export type AppUpdateChannelsArgs = {
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppUpdateChannelByChannelNameArgs = {
channelName: Scalars['String'];
export type AppUpdateChannelByNameArgs = {
name: Scalars['String'];
};
@@ -471,8 +484,8 @@ export type AppUpdateBranchesArgs = {
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppUpdateBranchByBranchNameArgs = {
branchName: Scalars['String'];
export type AppUpdateBranchByNameArgs = {
name: Scalars['String'];
};
@@ -482,6 +495,12 @@ export type AppActivityTimelineProjectActivitiesArgs = {
createdBefore?: Maybe<Scalars['DateTime']>;
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppEnvironmentSecretsArgs = {
filterNames?: Maybe<Array<Scalars['String']>>;
};
export type AppIcon = {
__typename?: 'AppIcon';
url: Scalars['String'];
@@ -730,7 +749,8 @@ export type BuildMetrics = {
export enum DistributionType {
Store = 'STORE',
Internal = 'INTERNAL'
Internal = 'INTERNAL',
Simulator = 'SIMULATOR'
}
/** Represents an Standalone App build job */
@@ -1011,7 +1031,7 @@ export type UpdateChannel = {
__typename?: 'UpdateChannel';
id: Scalars['ID'];
appId: Scalars['ID'];
channelName: Scalars['String'];
name: Scalars['String'];
branchMapping: Scalars['String'];
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
@@ -1028,7 +1048,7 @@ export type UpdateBranch = {
__typename?: 'UpdateBranch';
id: Scalars['ID'];
appId: Scalars['ID'];
branchName: Scalars['String'];
name: Scalars['String'];
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
updates: Array<Update>;
@@ -1049,11 +1069,19 @@ export type Update = ActivityTimelineProjectActivity & {
platform: Scalars['String'];
manifestFragment: Scalars['String'];
runtimeVersion: Scalars['String'];
updateGroup: Scalars['String'];
group: Scalars['String'];
updatedAt: Scalars['DateTime'];
createdAt: Scalars['DateTime'];
updateMessage?: Maybe<Scalars['String']>;
branchName: Scalars['String'];
message?: Maybe<Scalars['String']>;
branch: UpdateBranch;
};
export type EnvironmentSecret = {
__typename?: 'EnvironmentSecret';
id: Scalars['ID'];
name: Scalars['String'];
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
};
export type UserPermission = {
@@ -1475,6 +1503,8 @@ export type RootMutation = {
me?: Maybe<MeMutation>;
/** Mutations that modify an EmailSubscription */
emailSubscription: EmailSubscriptionMutation;
/** Mutations that create and delete EnvironmentSecrets */
environmentSecret: EnvironmentSecretMutation;
};
@@ -2201,7 +2231,7 @@ export type UpdateChannelMutation = {
export type UpdateChannelMutationCreateUpdateChannelForAppArgs = {
appId: Scalars['ID'];
channelName: Scalars['String'];
name: Scalars['String'];
branchMapping?: Maybe<Scalars['String']>;
};
@@ -2243,7 +2273,7 @@ export type UpdateBranchMutation = {
createUpdateBranchForApp?: Maybe<UpdateBranch>;
/**
* Edit an EAS branch. The branch can be specified either by its ID or
* with the combination of (appId, branchName).
* with the combination of (appId, name).
*/
editUpdateBranch: UpdateBranch;
/** Delete an EAS branch and all of its updates as long as the branch is not being used by any channels */
@@ -2255,7 +2285,7 @@ export type UpdateBranchMutation = {
export type UpdateBranchMutationCreateUpdateBranchForAppArgs = {
appId: Scalars['ID'];
branchName: Scalars['String'];
name: Scalars['String'];
};
@@ -2276,8 +2306,8 @@ export type UpdateBranchMutationPublishUpdateGroupArgs = {
export type EditUpdateBranchInput = {
id?: Maybe<Scalars['ID']>;
appId?: Maybe<Scalars['ID']>;
branchName?: Maybe<Scalars['String']>;
newBranchName: Scalars['String'];
name?: Maybe<Scalars['String']>;
newName: Scalars['String'];
};
export type DeleteUpdateBranchResult = {
@@ -2289,7 +2319,7 @@ export type PublishUpdateGroupInput = {
branchId: Scalars['String'];
updateInfoGroup: UpdateInfoGroup;
runtimeVersion: Scalars['String'];
updateMessage?: Maybe<Scalars['String']>;
message?: Maybe<Scalars['String']>;
};
export type UpdateInfoGroup = {
@@ -2304,7 +2334,7 @@ export type PartialManifest = {
};
export type PartialManifestAsset = {
fileHash: Scalars['String'];
fileSHA256: Scalars['String'];
bundleKey: Scalars['String'];
contentType: Scalars['String'];
storageBucket: Scalars['String'];
@@ -2598,6 +2628,43 @@ export type MailchimpTagPayload = {
name?: Maybe<Scalars['String']>;
};
export type EnvironmentSecretMutation = {
__typename?: 'EnvironmentSecretMutation';
/** Create an environment secret for an Account */
createEnvironmentSecretForAccount: EnvironmentSecret;
/** Create an environment secret for an App */
createEnvironmentSecretForApp: EnvironmentSecret;
/** Delete an environment secret */
deleteEnvironmentSecret: DeleteEnvironmentSecretResult;
};
export type EnvironmentSecretMutationCreateEnvironmentSecretForAccountArgs = {
environmentSecretData?: Maybe<CreateEnvironmentSecretInput>;
accountId: Scalars['String'];
};
export type EnvironmentSecretMutationCreateEnvironmentSecretForAppArgs = {
environmentSecretData?: Maybe<CreateEnvironmentSecretInput>;
appId: Scalars['String'];
};
export type EnvironmentSecretMutationDeleteEnvironmentSecretArgs = {
id: Scalars['String'];
};
export type CreateEnvironmentSecretInput = {
name: Scalars['String'];
value: Scalars['String'];
};
export type DeleteEnvironmentSecretResult = {
__typename?: 'DeleteEnvironmentSecretResult';
id: Scalars['ID'];
};
export type BaseSearchResult = SearchResult & {
__typename?: 'BaseSearchResult';
/** @deprecated Use SearchResult instead */
@@ -2642,6 +2709,290 @@ export enum CacheControlScope {
}
export type CreateUpdateBranchForAppMutationVariables = Exact<{
appId: Scalars['ID'];
name: Scalars['String'];
}>;
export type CreateUpdateBranchForAppMutation = (
{ __typename?: 'RootMutation' }
& { updateBranch: (
{ __typename?: 'UpdateBranchMutation' }
& { createUpdateBranchForApp?: Maybe<(
{ __typename?: 'UpdateBranch' }
& Pick<UpdateBranch, 'id' | 'name'>
)> }
) }
);
export type GetBranchInfoQueryVariables = Exact<{
appId: Scalars['String'];
name: Scalars['String'];
}>;
export type GetBranchInfoQuery = (
{ __typename?: 'RootQuery' }
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
& Pick<App, 'id'>
& { updateBranchByName: (
{ __typename?: 'UpdateBranch' }
& Pick<UpdateBranch, 'id' | 'name'>
) }
) }
)> }
);
export type DeleteUpdateBranchMutationVariables = Exact<{
branchId: Scalars['ID'];
}>;
export type DeleteUpdateBranchMutation = (
{ __typename?: 'RootMutation' }
& { updateBranch: (
{ __typename?: 'UpdateBranchMutation' }
& { deleteUpdateBranch: (
{ __typename?: 'DeleteUpdateBranchResult' }
& Pick<DeleteUpdateBranchResult, 'id'>
) }
) }
);
export type BranchesByAppQueryVariables = Exact<{
fullName: Scalars['String'];
limit: Scalars['Int'];
}>;
export type BranchesByAppQuery = (
{ __typename?: 'RootQuery' }
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byFullName: (
{ __typename?: 'App' }
& Pick<App, 'id' | 'fullName'>
& { updateBranches: Array<(
{ __typename?: 'UpdateBranch' }
& Pick<UpdateBranch, 'id' | 'name'>
& { updates: Array<(
{ __typename?: 'Update' }
& Pick<Update, 'id' | 'updatedAt' | 'message'>
& { actor?: Maybe<(
{ __typename: 'User' }
& Pick<User, 'username' | 'id'>
) | (
{ __typename: 'Robot' }
& Pick<Robot, 'firstName' | 'id'>
)> }
)> }
)> }
) }
)> }
);
export type EditUpdateBranchMutationVariables = Exact<{
input: EditUpdateBranchInput;
}>;
export type EditUpdateBranchMutation = (
{ __typename?: 'RootMutation' }
& { updateBranch: (
{ __typename?: 'UpdateBranchMutation' }
& { editUpdateBranch: (
{ __typename?: 'UpdateBranch' }
& Pick<UpdateBranch, 'id' | 'name'>
) }
) }
);
export type ViewBranchQueryVariables = Exact<{
appId: Scalars['String'];
name: Scalars['String'];
limit: Scalars['Int'];
}>;
export type ViewBranchQuery = (
{ __typename?: 'RootQuery' }
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
& Pick<App, 'id'>
& { updateBranchByName: (
{ __typename?: 'UpdateBranch' }
& Pick<UpdateBranch, 'id' | 'name'>
& { updates: Array<(
{ __typename?: 'Update' }
& Pick<Update, 'id' | 'group' | 'message' | 'createdAt'>
& { actor?: Maybe<(
{ __typename?: 'User' }
& Pick<User, 'firstName' | 'id'>
) | (
{ __typename?: 'Robot' }
& Pick<Robot, 'firstName' | 'id'>
)> }
)> }
) }
) }
)> }
);
export type CancelBuildMutationVariables = Exact<{
buildId: Scalars['ID'];
}>;
export type CancelBuildMutation = (
{ __typename?: 'RootMutation' }
& { build?: Maybe<(
{ __typename?: 'BuildMutation' }
& { cancel: (
{ __typename?: 'Build' }
& Pick<Build, 'id' | 'status'>
) }
)> }
);
export type CreateUpdateChannelOnAppMutationVariables = Exact<{
appId: Scalars['ID'];
name: Scalars['String'];
branchMapping: Scalars['String'];
}>;
export type CreateUpdateChannelOnAppMutation = (
{ __typename?: 'RootMutation' }
& { updateChannel: (
{ __typename?: 'UpdateChannelMutation' }
& { createUpdateChannelForApp?: Maybe<(
{ __typename?: 'UpdateChannel' }
& Pick<UpdateChannel, 'id' | 'name' | 'branchMapping'>
)> }
) }
);
export type GetChannelByNameToEditQueryVariables = Exact<{
appId: Scalars['String'];
channelName: Scalars['String'];
}>;
export type GetChannelByNameToEditQuery = (
{ __typename?: 'RootQuery' }
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
& Pick<App, 'id'>
& { updateChannelByName: (
{ __typename?: 'UpdateChannel' }
& Pick<UpdateChannel, 'id' | 'name'>
& { updateBranches: Array<(
{ __typename?: 'UpdateBranch' }
& Pick<UpdateBranch, 'id' | 'name'>
)> }
) }
) }
)> }
);
export type UpdateChannelBranchMappingMutationVariables = Exact<{
channelId: Scalars['ID'];
branchMapping: Scalars['String'];
}>;
export type UpdateChannelBranchMappingMutation = (
{ __typename?: 'RootMutation' }
& { updateChannel: (
{ __typename?: 'UpdateChannelMutation' }
& { editUpdateChannel?: Maybe<(
{ __typename?: 'UpdateChannel' }
& Pick<UpdateChannel, 'id' | 'name' | 'branchMapping'>
)> }
) }
);
export type GetAllChannelsForAppQueryVariables = Exact<{
appId: Scalars['String'];
offset: Scalars['Int'];
limit: Scalars['Int'];
}>;
export type GetAllChannelsForAppQuery = (
{ __typename?: 'RootQuery' }
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
& Pick<App, 'id'>
& { updateChannels: Array<(
{ __typename?: 'UpdateChannel' }
& Pick<UpdateChannel, 'id' | 'name'>
& { updateBranches: Array<(
{ __typename?: 'UpdateBranch' }
& Pick<UpdateBranch, 'id' | 'name'>
& { updates: Array<(
{ __typename?: 'Update' }
& Pick<Update, 'id' | 'group' | 'message' | 'createdAt'>
& { actor?: Maybe<(
{ __typename?: 'User' }
& Pick<User, 'firstName' | 'id'>
) | (
{ __typename?: 'Robot' }
& Pick<Robot, 'firstName' | 'id'>
)> }
)> }
)> }
)> }
) }
)> }
);
export type GetChannelByNameForAppQueryVariables = Exact<{
appId: Scalars['String'];
channelName: Scalars['String'];
}>;
export type GetChannelByNameForAppQuery = (
{ __typename?: 'RootQuery' }
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
& Pick<App, 'id'>
& { updateChannelByName: (
{ __typename?: 'UpdateChannel' }
& Pick<UpdateChannel, 'id' | 'name' | 'createdAt'>
& { updateBranches: Array<(
{ __typename?: 'UpdateBranch' }
& Pick<UpdateBranch, 'id' | 'name'>
& { updates: Array<(
{ __typename?: 'Update' }
& Pick<Update, 'id' | 'group' | 'message' | 'createdAt'>
& { actor?: Maybe<(
{ __typename?: 'User' }
& Pick<User, 'firstName' | 'id'>
) | (
{ __typename?: 'Robot' }
& Pick<Robot, 'firstName' | 'id'>
)> }
)> }
)> }
) }
) }
)> }
);
export type CreateAppleAppIdentifierMutationVariables = Exact<{
appleAppIdentifierInput: AppleAppIdentifierInput;
accountId: Scalars['ID'];
@@ -3275,7 +3626,7 @@ export type UpdatePublishMutation = (
{ __typename?: 'UpdateBranchMutation' }
& { publishUpdateGroup: Array<Maybe<(
{ __typename?: 'Update' }
& Pick<Update, 'id' | 'updateGroup'>
& Pick<Update, 'id' | 'group'>
)>> }
) }
);
@@ -31,7 +31,7 @@ const PublishMutation = {
async publishUpdateGroupAsync(
publishUpdateGroupInput: PublishUpdateGroupInput
): Promise<Pick<Update, 'id' | 'updateGroup'>> {
): Promise<Pick<Update, 'id' | 'group'>> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<UpdatePublishMutation>(
@@ -40,7 +40,7 @@ const PublishMutation = {
updateBranch {
publishUpdateGroup(publishUpdateGroupInput: $publishUpdateGroupInput) {
id
updateGroup
group
}
}
}
@@ -140,7 +140,7 @@ describe(convertAssetToUpdateInfoGroupFormatAsync, () => {
await expect(convertAssetToUpdateInfoGroupFormatAsync(asset)).resolves.toEqual({
bundleKey: `c939e759656f577c058f445bfb19182e.${type}`,
contentType: 'image/jpeg',
fileHash: 'tzD6J-OQZaHCKnL3GHWV9RbnrpyojnagiOE7r3mSkU4',
fileSHA256: 'tzD6J-OQZaHCKnL3GHWV9RbnrpyojnagiOE7r3mSkU4',
storageBucket: 'update-assets-production',
storageKey: 'fo8Y08LktVk6qLtGbn8GRWpOUyD13ABMUnbtRCN1L7Y',
});
@@ -177,7 +177,7 @@ describe(buildUpdateInfoGroupAsync, () => {
{
bundleKey: 'c939e759656f577c058f445bfb19182e.jpg',
contentType: 'image/jpeg',
fileHash: 'tzD6J-OQZaHCKnL3GHWV9RbnrpyojnagiOE7r3mSkU4',
fileSHA256: 'tzD6J-OQZaHCKnL3GHWV9RbnrpyojnagiOE7r3mSkU4',
storageBucket: 'update-assets-production',
storageKey: 'fo8Y08LktVk6qLtGbn8GRWpOUyD13ABMUnbtRCN1L7Y',
},
@@ -186,7 +186,7 @@ describe(buildUpdateInfoGroupAsync, () => {
launchAsset: {
bundleKey: 'ec0dd14670aae108f99a810df9c1482c.bundle',
contentType: 'bundle/javascript',
fileHash: 'KEw79FnKTLOyVbRT1SlohSTjPe5e8FpULy2ST-I5BUg',
fileSHA256: 'KEw79FnKTLOyVbRT1SlohSTjPe5e8FpULy2ST-I5BUg',
storageBucket: 'update-assets-production',
storageKey: 'aC9N6RZlcHoIYjIsoJd2KUcigBKy98RHvZacDyPNjCQ',
},
@@ -206,7 +206,9 @@ describe(resolveInputDirectory, () => {
expect(() => {
resolveInputDirectory(nonExistentPath);
}).toThrow(`The input directory "${nonExistentPath}" does not exist.
You'll need to run a command to build the JS bundle first (e.g. 'expo export').
You can allow us to build it for you by not setting the --skip-bundler flag.
If you chose to build it yourself you'll need to run a command to build the JS
bundle first.
You can use '--input-dir' to specify a different input directory.`);
});
});
+12 -12
View File
@@ -71,7 +71,7 @@ export async function getAppIdentifierAsync(
): Promise<string | null> {
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
switch (platform) {
case Platform.Android: {
case Platform.ANDROID: {
const packageNameFromConfig = AndroidConfig.Package.getPackage(exp);
if (packageNameFromConfig) {
return packageNameFromConfig;
@@ -80,7 +80,7 @@ export async function getAppIdentifierAsync(
? await getAndroidApplicationIdAsync(projectDir)
: null;
}
case Platform.iOS: {
case Platform.IOS: {
return (
IOSConfig.BundleIdentifier.getBundleIdentifier(exp) ??
IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(projectDir)
@@ -96,7 +96,7 @@ export async function ensureAppIdentifierIsDefinedAsync(
const appIdentifier = await getAppIdentifierAsync(projectDir, platform);
if (!appIdentifier) {
const desc = getProjectConfigDescription(projectDir);
const fieldStr = platform === Platform.Android ? 'android.package' : 'ios.bundleIdentifier';
const fieldStr = platform === Platform.ANDROID ? 'android.package' : 'ios.bundleIdentifier';
throw new Error(`Please define "${fieldStr}" in your ${desc}.`);
}
return appIdentifier;
@@ -125,10 +125,10 @@ export function getProjectConfigDescription(projectDir: string): string {
export async function getBranchByNameAsync({
appId,
branchName,
name,
}: {
appId: string;
branchName: string;
name: string;
}): Promise<UpdateBranch> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -136,23 +136,23 @@ export async function getBranchByNameAsync({
{
app: {
byId: {
updateBranchByBranchName: UpdateBranch;
updateBranchByName: UpdateBranch;
};
};
},
{
appId: string;
branchName: string;
name: string;
}
>(
gql`
query ViewBranch($appId: String!, $branchName: String!) {
query ViewBranch($appId: String!, $name: String!) {
app {
byId(appId: $appId) {
id
updateBranchByBranchName(branchName: $branchName) {
updateBranchByName(name: $name) {
id
branchName
name
}
}
}
@@ -160,10 +160,10 @@ export async function getBranchByNameAsync({
`,
{
appId,
branchName,
name,
}
)
.toPromise()
);
return data.app.byId.updateBranchByBranchName;
return data.app.byId.updateBranchByName;
}
+47 -6
View File
@@ -1,5 +1,6 @@
import { Platform } from '@expo/config';
import JsonFile from '@expo/json-file';
import spawnAsync from '@expo/spawn-async';
import Joi from '@hapi/joi';
import crypto from 'crypto';
import fs from 'fs';
@@ -10,6 +11,7 @@ import path from 'path';
import { AssetMetadataStatus, PartialManifestAsset } from '../graphql/generated';
import { PublishMutation } from '../graphql/mutations/PublishMutation';
import { PublishQuery } from '../graphql/queries/PublishQuery';
import Log from '../log';
import { PresignedPost, uploadWithPresignedPostAsync } from '../uploads';
export const TIMEOUT_LIMIT = 60_000; // 1 minute
@@ -102,23 +104,23 @@ async function calculateFileHashAsync(filePath: string, algorithm: string): Prom
* Convenience function that computes an assets storage key starting from its buffer.
*/
export async function getStorageKeyForAssetAsync(asset: RawAsset): Promise<string> {
const fileHash = getBase64URLEncoding(await calculateFileHashAsync(asset.path, 'sha256'));
return getStorageKey(asset.contentType, fileHash);
const fileSHA256 = getBase64URLEncoding(await calculateFileHashAsync(asset.path, 'sha256'));
return getStorageKey(asset.contentType, fileSHA256);
}
export async function convertAssetToUpdateInfoGroupFormatAsync(
asset: RawAsset
): Promise<PartialManifestAsset> {
const fileHash = getBase64URLEncoding(await calculateFileHashAsync(asset.path, 'sha256'));
const fileSHA256 = getBase64URLEncoding(await calculateFileHashAsync(asset.path, 'sha256'));
const contentType = asset.contentType;
const storageKey = getStorageKey(contentType, fileHash);
const storageKey = getStorageKey(contentType, fileSHA256);
const bundleKey = [
(await calculateFileHashAsync(asset.path, 'md5')).toString('hex'),
asset.type,
].join('.');
return {
fileHash,
fileSHA256,
contentType,
storageBucket: STORAGE_BUCKET,
storageKey,
@@ -140,11 +142,50 @@ export async function buildUpdateInfoGroupAsync(assets: CollectedAssets): Promis
return updateInfoGroup as UpdateInfoGroup;
}
export async function buildBundlesAsync({
projectDir,
inputDir,
}: {
projectDir: string;
inputDir: string;
}) {
const packageJSON = JsonFile.read(path.resolve(projectDir, 'package.json'));
if (!packageJSON) {
throw new Error('Could not locate package.json');
}
Log.withTick(`Building bundle with expo-cli...`);
const spawnPromise = spawnAsync(
'yarn',
['expo', 'export', '--output-dir', inputDir, '--experimental-bundle', '--force'],
{ stdio: ['inherit', 'pipe', 'pipe'] } // inherit stdin so user can install a missing expo-cli from inside this command
);
const {
child: { stdout, stderr },
} = spawnPromise;
if (!(stdout && stderr)) {
throw new Error('Failed to spawn expo-cli');
}
stdout.on('data', data => {
for (const line of data.toString().trim().split('\n')) {
Log.log(`[expo-cli]${line}`);
}
});
stderr.on('data', data => {
for (const line of data.toString().trim().split('\n')) {
Log.warn(`[expo-cli]${line}`);
}
});
await spawnPromise;
}
export function resolveInputDirectory(customInputDirectory: string): string {
const distRoot = path.resolve(customInputDirectory);
if (!fs.existsSync(distRoot)) {
throw new Error(`The input directory "${customInputDirectory}" does not exist.
You'll need to run a command to build the JS bundle first (e.g. 'expo export').
You can allow us to build it for you by not setting the --skip-bundler flag.
If you chose to build it yourself you'll need to run a command to build the JS
bundle first.
You can use '--input-dir' to specify a different input directory.`);
}
return distRoot;
+1 -1
View File
@@ -85,7 +85,7 @@ export async function pressAnyKeyToContinueAsync() {
process.stdin.resume();
process.stdin.setEncoding('utf8');
await new Promise(res => {
await new Promise<void>(res => {
process.stdin.on('data', key => {
if (String(key) === '\u0003') {
process.exit(constants.signals.SIGINT + 128); // ctrl-c
@@ -21,7 +21,7 @@ abstract class BaseSubmitter<SubmissionContext, SubmissionOptions> {
protected async startSubmissionAsync(
submissionConfig: SubmissionConfig,
verbose: boolean = false
) {
): Promise<SubmissionStatus> {
Log.addNewLineIfNone();
const scheduleSpinner = ora('Scheduling submission').start();
let submissionId: string;
@@ -65,6 +65,7 @@ abstract class BaseSubmitter<SubmissionContext, SubmissionOptions> {
}
await displayLogs(submission, submissionStatus, verbose);
return submissionStatus ?? SubmissionStatus.ERRORED;
}
private getStatusText(status: SubmissionStatus): string {
@@ -8,7 +8,11 @@ import {
ArchiveType,
SubmissionPlatform,
} from '../types';
import { printSummary } from '../utils/summary';
import {
ArchiveSourceSummaryFields,
formatArchiveSourceSummary,
printSummary,
} from '../utils/summary';
import { AndroidPackageSource, getAndroidPackageAsync } from './AndroidPackageSource';
import { AndroidSubmissionConfig, ReleaseStatus, ReleaseTrack } from './AndroidSubmissionConfig';
import { ServiceAccountSource, getServiceAccountAsync } from './ServiceAccountSource';
@@ -35,10 +39,14 @@ class AndroidSubmitter extends BaseSubmitter<AndroidSubmissionContext, AndroidSu
async submitAsync(): Promise<void> {
const resolvedSourceOptions = await this.resolveSourceOptions();
const submissionConfig = await this.formatSubmissionConfigAndPrintSummary(
this.options,
resolvedSourceOptions
const submissionConfig = await this.formatSubmissionConfig(this.options, resolvedSourceOptions);
printSummary(
this.prepareSummaryData(this.options, resolvedSourceOptions),
SummaryHumanReadableKeys,
SummaryHumanReadableValues
);
await this.startSubmissionAsync(submissionConfig, this.ctx.commandFlags.verbose);
}
@@ -53,71 +61,66 @@ class AndroidSubmitter extends BaseSubmitter<AndroidSubmissionContext, AndroidSu
};
}
private async formatSubmissionConfigAndPrintSummary(
private async formatSubmissionConfig(
options: AndroidSubmissionOptions,
{ archive, androidPackage, serviceAccountPath }: ResolvedSourceOptions
): Promise<AndroidSubmissionConfig> {
const serviceAccount = await fs.readFile(serviceAccountPath, 'utf-8');
const { track, releaseStatus, projectId } = options;
const submissionConfig = {
// structuring order affects table rows order
return {
androidPackage,
archiveUrl: archive.location,
archiveType: archive.type as AndroidArchiveType,
track,
releaseStatus,
projectId,
serviceAccount,
};
}
printSummaryAndroid({
...submissionConfig,
private prepareSummaryData(
options: AndroidSubmissionOptions,
{ archive, androidPackage, serviceAccountPath }: ResolvedSourceOptions
): Summary {
const { projectId, track, releaseStatus } = options;
return {
projectId,
androidPackage,
track,
releaseStatus,
archiveType: archive.type as AndroidArchiveType,
...formatArchiveSourceSummary(archive),
serviceAccountPath,
});
return { ...submissionConfig, serviceAccount };
};
}
}
/**
* Log the summary as a table. Exported for testing locally.
*
* @example
* printSummaryAndroid({
* androidPackage: 'com.expo.demoapp',
* archivePath: '/Users/example/Documents/863f9337-65d2-40c6-acb3-c1054c5c09f8.apk',
* archiveUrl: 'https://turtle-v2-artifacts.s3.amazonaws.com/ios/6420592d-5b5d-439b-aed4-ccd278647138-ca4145d8468947df9ded737248a1a238.aab',
* archiveType: 'apk' as any,
* serviceAccountPath: '/Users/example/Documents/gsa.json',
* track: ReleaseTrack.production,
* releaseStatus: ReleaseStatus.completed,
* projectId: '863f9337-65d2-40c6-acb3-c1054c5c09f8',
* });
* @param submissionConfig
*/
export function printSummaryAndroid(submissionConfig: Summary) {
printSummary(submissionConfig, SummaryHumanReadableKeys, SummaryHumanReadableValues);
}
interface Summary {
type Summary = {
androidPackage: string;
archivePath?: string;
archiveUrl?: string;
archiveType: ArchiveType;
serviceAccountPath: string;
track: ReleaseTrack;
releaseStatus?: ReleaseStatus;
projectId?: string;
}
} & ArchiveSourceSummaryFields;
const SummaryHumanReadableKeys: Record<keyof Summary, string> = {
androidPackage: 'Android package',
archivePath: 'Archive path',
archiveUrl: 'Download URL',
archiveType: 'Archive type',
serviceAccountPath: 'Google Service Account',
buildId: 'Build ID',
serviceAccountPath: 'Google Service Key',
track: 'Release track',
releaseStatus: 'Release status',
projectId: 'Project ID',
};
const SummaryHumanReadableValues: Partial<Record<keyof Summary, Function>> = {};
const SummaryHumanReadableValues: Partial<Record<keyof Summary, Function>> = {
archiveType: (type: string) => type.toUpperCase(),
};
export default AndroidSubmitter;
@@ -53,6 +53,11 @@ interface ArchiveFilePromptSource extends ArchiveFileSourceBase {
projectDir: string;
}
interface ResolvedArchive {
location: string;
realSource: ArchiveFileSource;
}
export type ArchiveFileSource =
| ArchiveFileUrlSource
| ArchiveFileLatestSource
@@ -60,25 +65,31 @@ export type ArchiveFileSource =
| ArchiveFileBuildIdSource
| ArchiveFilePromptSource;
export async function getArchiveFileLocationAsync(source: ArchiveFileSource): Promise<string> {
export async function getArchiveFileLocationAsync(
source: ArchiveFileSource
): Promise<ResolvedArchive> {
switch (source.sourceType) {
case ArchiveFileSourceType.prompt:
return await handlePromptSourceAsync(source);
case ArchiveFileSourceType.url: {
const url = await handleUrlSourceAsync(source);
return await getArchiveLocationForUrlAsync(url);
const urlArchive = await handleUrlSourceAsync(source);
const resolvedUrl = await getArchiveLocationForUrlAsync(urlArchive.location);
return { ...urlArchive, location: resolvedUrl };
}
case ArchiveFileSourceType.latest: {
const url = await handleLatestSourceAsync(source);
return await getArchiveLocationForUrlAsync(url);
const urlArchive = await handleLatestSourceAsync(source);
const resolvedUrl = await getArchiveLocationForUrlAsync(urlArchive.location);
return { ...urlArchive, location: resolvedUrl };
}
case ArchiveFileSourceType.path: {
const path = await handlePathSourceAsync(source);
return getArchiveLocationForPathAsync(path);
const pathArchive = await handlePathSourceAsync(source);
const resolvedUrl = await getArchiveLocationForPathAsync(pathArchive.location);
return { ...pathArchive, location: resolvedUrl };
}
case ArchiveFileSourceType.buildId: {
const url = await handleBuildIdSourceAsync(source);
return await getArchiveLocationForUrlAsync(url);
const urlArchive = await handleBuildIdSourceAsync(source);
const resolvedUrl = await getArchiveLocationForUrlAsync(urlArchive.location);
return { ...urlArchive, location: resolvedUrl };
}
}
}
@@ -102,11 +113,14 @@ async function getArchiveLocationForPathAsync(path: string): Promise<string> {
return await uploadAppArchiveAsync(resolvedPath);
}
async function handleUrlSourceAsync(source: ArchiveFileUrlSource): Promise<string> {
return source.url;
async function handleUrlSourceAsync(source: ArchiveFileUrlSource): Promise<ResolvedArchive> {
return {
location: source.url,
realSource: source,
};
}
async function handleLatestSourceAsync(source: ArchiveFileLatestSource): Promise<string> {
async function handleLatestSourceAsync(source: ArchiveFileLatestSource): Promise<ResolvedArchive> {
try {
const artifactUrl = await getLatestBuildArtifactUrlAsync(source.platform, source.projectId);
@@ -122,14 +136,17 @@ async function handleLatestSourceAsync(source: ArchiveFileLatestSource): Promise
});
}
return artifactUrl;
return {
location: artifactUrl,
realSource: source,
};
} catch (err) {
Log.error(err);
throw err;
}
}
async function handlePathSourceAsync(source: ArchiveFilePathSource): Promise<string> {
async function handlePathSourceAsync(source: ArchiveFilePathSource): Promise<ResolvedArchive> {
if (!(await isExistingFile(source.path))) {
Log.error(chalk.bold(`${source.path} doesn't exist`));
return getArchiveFileLocationAsync({
@@ -137,12 +154,20 @@ async function handlePathSourceAsync(source: ArchiveFilePathSource): Promise<str
sourceType: ArchiveFileSourceType.prompt,
});
}
return source.path;
return {
location: source.path,
realSource: source,
};
}
async function handleBuildIdSourceAsync(source: ArchiveFileBuildIdSource): Promise<string> {
async function handleBuildIdSourceAsync(
source: ArchiveFileBuildIdSource
): Promise<ResolvedArchive> {
try {
return await getBuildArtifactUrlByIdAsync(source.platform, source.id);
return {
location: await getBuildArtifactUrlByIdAsync(source.platform, source.id),
realSource: source,
};
} catch (err) {
Log.error(chalk.bold(`Couldn't find build for id ${source.id}`));
Log.error(err);
@@ -153,7 +178,7 @@ async function handleBuildIdSourceAsync(source: ArchiveFileBuildIdSource): Promi
}
}
async function handlePromptSourceAsync(source: ArchiveFilePromptSource): Promise<string> {
async function handlePromptSourceAsync(source: ArchiveFilePromptSource): Promise<ResolvedArchive> {
const { sourceType: sourceTypeRaw } = await promptAsync({
name: 'sourceType',
type: 'select',
@@ -211,6 +236,8 @@ async function handlePromptSourceAsync(source: ArchiveFilePromptSource): Promise
}
}
/* PROMPTS */
async function askForArchiveUrlAsync(): Promise<string> {
const defaultArchiveUrl = 'https://url.to/your/archive.aab';
const { url } = await promptAsync({
@@ -14,17 +14,19 @@ export interface ArchiveSource {
export interface Archive {
location: string;
type: ArchiveType;
realFileSource: ArchiveFileSource;
}
export async function getArchiveAsync(
platform: SubmissionPlatform,
source: ArchiveSource
): Promise<Archive> {
const location = await getArchiveFileLocationAsync(source.archiveFile);
const { location, realSource } = await getArchiveFileLocationAsync(source.archiveFile);
const type = await getArchiveTypeAsync(platform, source.archiveType, location);
return {
location,
type,
realFileSource: realSource,
};
}
@@ -39,7 +39,7 @@ export async function ensureAppStoreConnectAppExistsAsync(
const { bundleIdentifier, appName, language } = ctx.commandFlags;
const resolvedBundleId =
bundleIdentifier ?? (await getAppIdentifierAsync(ctx.projectDir, Platform.iOS));
bundleIdentifier ?? (await getAppIdentifierAsync(ctx.projectDir, Platform.IOS));
if (!resolvedBundleId) {
throw new Error(
`Please define "expo.ios.bundleIdentifier" in your app config or provide it using --bundle-identifier param.
@@ -1,9 +1,15 @@
import chalk from 'chalk';
import Log, { learnMore } from '../../log';
import BaseSubmitter from '../BaseSubmitter';
import { SubmissionStatus } from '../SubmissionService.types';
import { Archive, ArchiveSource, getArchiveAsync } from '../archiveSource';
import { IosSubmissionContext, SubmissionPlatform } from '../types';
import { printSummary } from '../utils/summary';
import {
ArchiveSourceSummaryFields,
formatArchiveSourceSummary,
printSummary,
} from '../utils/summary';
import {
AppSpecificPasswordSource,
getAppSpecificPasswordAsync,
@@ -21,6 +27,9 @@ interface ResolvedSourceOptions {
appSpecificPassword: string;
}
type SummaryData = Pick<IosSubmissionOptions, 'ascAppId' | 'appleId' | 'projectId'> &
ArchiveSourceSummaryFields;
class IosSubmitter extends BaseSubmitter<IosSubmissionContext, IosSubmissionOptions> {
protected readonly appStoreName: string = 'Apple App Store';
@@ -35,9 +44,26 @@ class IosSubmitter extends BaseSubmitter<IosSubmissionContext, IosSubmissionOpti
resolvedSourceOptions
);
printSummaryIOS(submissionConfig);
printSummary(
this.prepareSummaryData(this.options, resolvedSourceOptions),
SummaryHumanReadableKeys,
SummaryHumanReadableValues
);
const result = await this.startSubmissionAsync(submissionConfig, this.ctx.commandFlags.verbose);
await this.startSubmissionAsync(submissionConfig, this.ctx.commandFlags.verbose);
if (result === SubmissionStatus.FINISHED) {
Log.addNewLineIfNone();
Log.log(
chalk.bold('Your binary has been successfully uploaded to App Store Connect!\n') +
'- It is now being processed by Apple - you will receive an e-mail when the processing finishes.\n' +
'- It usually takes about 5-10 minutes depending on how busy Apple servers are.\n' +
'- When it’s done, you can see your build here' +
learnMore(
`https://appstoreconnect.apple.com/apps/${this.options.ascAppId}/appstore/ios`,
''
)
);
}
}
private async resolveSourceOptions(): Promise<ResolvedSourceOptions> {
@@ -58,48 +84,40 @@ class IosSubmitter extends BaseSubmitter<IosSubmissionContext, IosSubmissionOpti
): Promise<IosSubmissionConfig> {
const { projectId, appleId, ascAppId } = options;
const submissionConfig = {
archiveUrl: archive.location,
appleId,
appSpecificPassword,
appAppleId: ascAppId, //ASC App ID is called "appAppleId" on server side
appleId,
projectId,
archiveUrl: archive.location,
appSpecificPassword,
};
return submissionConfig;
}
private prepareSummaryData(
options: IosSubmissionOptions,
{ archive }: ResolvedSourceOptions
): SummaryData {
const { appleId, ascAppId, projectId } = options;
// structuring order affects table rows order
return {
ascAppId,
appleId,
projectId,
...formatArchiveSourceSummary(archive),
};
}
}
/**
* Log the summary as a table. Exported for testing locally.
* @example
* printSummaryIOS({
* appleId: '[email protected]',
* appSpecificPassword: 'Apple Pie^',
* appAppleId: '1234567890',
* projectId: '863f9337-65d2-40c6-acb3-c1054c5c09f8',
* archiveUrl: 'https://turtle-v2-artifacts.s3.amazonaws.com/ios/6420592d-5b5d-439b-aed4-ccd278647138-ca4145d8468947df9ded737248a1a238.ipa',
* });
* @param submissionConfig
*/
export function printSummaryIOS(submissionConfig: IosSubmissionConfig) {
const { appSpecificPassword, projectId, archiveUrl, ...logConfig } = submissionConfig;
printSummary(
{ ...logConfig, projectId, archiveUrl },
SummaryHumanReadableKeys,
SummaryHumanReadableValues
);
}
const SummaryHumanReadableKeys: Record<keyof IosSubmissionConfig, string> = {
const SummaryHumanReadableKeys: Record<keyof SummaryData, string> = {
ascAppId: 'ASC App ID',
appleId: 'Apple ID',
archiveUrl: 'Download URL',
appSpecificPassword: 'Apple app-specific password',
appAppleId: 'ASC App ID',
projectId: 'Project ID',
archiveUrl: 'Archive URL',
archivePath: 'Archive Path',
buildId: 'Build ID',
};
const SummaryHumanReadableValues: Partial<Record<keyof IosSubmissionConfig, Function>> = {
appSpecificPassword: () => chalk.italic('[hidden]'),
};
const SummaryHumanReadableValues: Partial<Record<keyof SummaryData, Function>> = {};
export default IosSubmitter;
@@ -26,7 +26,7 @@ async function moveFileOfTypeAsync(
extension: string,
dest: string
): Promise<string> {
const [matching] = await glob(`*.${extension}`, {
const [matching] = await glob(`**/*.${extension}`, {
absolute: true,
cwd: directory,
});
@@ -1,31 +1,51 @@
import chalk from 'chalk';
import Log from '../../log';
import formatFields from '../../utils/formatFields';
import { Archive, ArchiveFileSourceType } from '../archiveSource';
export interface ArchiveSourceSummaryFields {
archiveUrl?: string;
archivePath?: string;
buildId?: string;
}
export function formatArchiveSourceSummary({
realFileSource,
}: Archive): ArchiveSourceSummaryFields {
const summarySlice: ArchiveSourceSummaryFields = {};
switch (realFileSource.sourceType) {
case ArchiveFileSourceType.path:
summarySlice.archivePath = realFileSource.path;
break;
case ArchiveFileSourceType.url:
summarySlice.archiveUrl = realFileSource.url;
break;
case ArchiveFileSourceType.buildId:
summarySlice.buildId = realFileSource.id;
break;
case ArchiveFileSourceType.latest:
// TODO: Resolve real build ID here
summarySlice.buildId = '[latest]';
break;
}
return summarySlice;
}
export function printSummary<T>(
summary: T,
keyMap: Record<keyof T, string>,
valueRemap: Partial<Record<keyof T, Function>>
): void {
const padWidth = longestStringLength(Object.keys(summary).map(key => keyMap[key as keyof T]));
const tableFormat = (name: string, msg: string) =>
`${chalk.bold.cyan(pad(`${name}:`, padWidth + 1))} ${msg}`;
const fields = [];
for (const [key, entryValue] of Object.entries(summary)) {
const label = `${keyMap[key as keyof T]}:`;
const value = valueRemap[key as keyof T]?.(entryValue) ?? entryValue;
fields.push({ label, value });
}
Log.newLine();
for (const [key, value] of Object.entries(summary)) {
const displayKey = keyMap[key as keyof T];
const displayValue = valueRemap[key as keyof T]?.(value) ?? value;
Log.log(tableFormat(displayKey, displayValue));
}
Log.log(formatFields(fields, { labelFormat: chalk.bold.cyan }));
Log.addNewLineIfNone();
}
function pad(str: string, width: number): string {
const len = Math.max(0, width - str.length);
return str + Array(len + 1).join(' ');
}
function longestStringLength(values: string[]): number {
return values.reduce((max, option) => Math.max(max, option.length), 0);
}
+29
View File
@@ -26,3 +26,32 @@ export async function maybeRenameExistingFileAsync(projectDir: string, filename:
await fs.rename(desiredFilePath, path.resolve(projectDir, getRenamedFilename(filename, num)));
}
}
export function formatBytes(bytes: number): string {
if (bytes === 0) {
return `0`;
}
let multiplier = 1;
if (bytes < 1024 * multiplier) {
return `${Math.floor(bytes)} B`;
}
multiplier *= 1024;
if (bytes < 102.4 * multiplier) {
return `${(bytes / multiplier).toFixed(1)} KB`;
}
if (bytes < 1024 * multiplier) {
return `${Math.floor(bytes / 1024)} KB`;
}
multiplier *= 1024;
if (bytes < 102.4 * multiplier) {
return `${(bytes / multiplier).toFixed(1)} MB`;
}
if (bytes < 1024 * multiplier) {
return `${Math.floor(bytes / multiplier)} MB`;
}
multiplier *= 1024;
if (bytes < 102.4 * multiplier) {
return `${(bytes / multiplier).toFixed(1)} GB`;
}
return `${Math.floor(bytes / 1024)} GB`;
}
+11 -8
View File
@@ -1,21 +1,24 @@
import chalk from 'chalk';
export default function formatFields(fields: { label: string; value: string }[]) {
type FormatFieldsOptions = {
labelFormat: (raw: string) => string;
};
export default function formatFields(
fields: { label: string; value: string }[],
options: FormatFieldsOptions = { labelFormat: chalk.dim }
) {
const columnWidth = fields.reduce((a, b) => (a.label.length > b.label.length ? a : b)).label
.length;
return fields
.map(({ label, value }) => {
let line = '';
line += chalk.dim(
// make all labels fixed-width
const formattedLabel = options.labelFormat(
label.length < columnWidth ? `${label}${' '.repeat(columnWidth - label.length)}` : label
);
line += ' ';
line += value;
return line;
return `${formattedLabel} ${value}`;
})
.join('\n');
}
+22 -21
View File
@@ -4,20 +4,25 @@ interface GitStatusOptions {
showUntracked?: boolean;
}
async function gitStatusAsync({ showUntracked }: GitStatusOptions = {}): Promise<string> {
export async function gitStatusAsync({ showUntracked }: GitStatusOptions = {}): Promise<string> {
return (await spawnAsync('git', ['status', '-s', showUntracked ? '-uall' : '-uno'])).stdout;
}
async function gitDiffAsync({ withPager = false }: { withPager?: boolean } = {}): Promise<void> {
export async function gitDiffAsync({ withPager = false }: { withPager?: boolean } = {}): Promise<
void
> {
const options = withPager ? [] : ['--no-pager'];
await spawnAsync('git', [...options, 'diff'], { stdio: ['ignore', 'inherit', 'inherit'] });
}
async function getGitDiffOutputAsync(): Promise<string> {
export async function getGitDiffOutputAsync(): Promise<string> {
return (await spawnAsync('git', ['--no-pager', 'diff'])).stdout;
}
async function gitAddAsync(file: string, options?: { intentToAdd?: boolean }): Promise<void> {
export async function gitAddAsync(
file: string,
options?: { intentToAdd?: boolean }
): Promise<void> {
if (options?.intentToAdd) {
await spawnAsync('git', ['add', '--intent-to-add', file]);
} else {
@@ -25,11 +30,11 @@ async function gitAddAsync(file: string, options?: { intentToAdd?: boolean }): P
}
}
async function gitRootDirectoryAsync(): Promise<string> {
export async function gitRootDirectoryAsync(): Promise<string> {
return (await spawnAsync('git', ['rev-parse', '--show-toplevel'])).stdout.trim();
}
async function doesGitRepoExistAsync(): Promise<boolean> {
export async function doesGitRepoExistAsync(): Promise<boolean> {
try {
await spawnAsync('git', ['rev-parse', '--git-dir']);
return true;
@@ -38,7 +43,15 @@ async function doesGitRepoExistAsync(): Promise<boolean> {
}
}
async function isGitInstalledAsync(): Promise<boolean> {
export async function gitCommitHashAsync(): Promise<string | undefined> {
try {
return (await spawnAsync('git', ['rev-parse', 'HEAD'])).stdout.trim();
} catch (err) {
return undefined;
}
}
export async function isGitInstalledAsync(): Promise<boolean> {
try {
await spawnAsync('git', ['--help']);
} catch (error) {
@@ -50,7 +63,7 @@ async function isGitInstalledAsync(): Promise<boolean> {
return true;
}
async function getBranchNameAsync(): Promise<string | null> {
export async function getBranchNameAsync(): Promise<string | null> {
try {
return (await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'])).stdout.trim();
} catch (e) {
@@ -58,22 +71,10 @@ async function getBranchNameAsync(): Promise<string | null> {
}
}
async function getLastCommitMessageAsync(): Promise<string | null> {
export async function getLastCommitMessageAsync(): Promise<string | null> {
try {
return (await spawnAsync('git', ['--no-pager', 'log', '-1', '--pretty=%B'])).stdout.trim();
} catch (e) {
return null;
}
}
export {
gitStatusAsync,
gitDiffAsync,
getGitDiffOutputAsync,
gitAddAsync,
doesGitRepoExistAsync,
gitRootDirectoryAsync,
isGitInstalledAsync,
getBranchNameAsync,
getLastCommitMessageAsync,
};
+7 -7
View File
@@ -16,8 +16,8 @@ function createProgressTracker({
completedMessage,
}: {
total?: number;
message: string;
completedMessage?: string;
message: string | ((ratio: number) => string);
completedMessage: string;
}): ProgressHandler {
let bar: ora.Ora | null = null;
let calcTotal: number = total ?? 0;
@@ -26,17 +26,17 @@ function createProgressTracker({
const timerLabel = String(Date.now());
const withPercent = (v: number) => {
const getMessage = (v: number) => {
const ratio = Math.min(Math.max(v, 0), 1);
const percent = Math.floor(ratio * 100);
return `${message} ${percent.toFixed(0)}%`;
return typeof message === 'string' ? `${message} ${percent.toFixed(0)}%` : message(ratio);
};
return ({ progress, isComplete, error }) => {
if (progress) {
if (!bar && (progress.total !== undefined || total !== undefined)) {
calcTotal = (total ?? progress.total) as number;
bar = ora(withPercent(0)).start();
bar = ora(getMessage(0)).start();
startTimer(timerLabel);
}
if (progress.total) {
@@ -51,7 +51,7 @@ function createProgressTracker({
percentage = current / calcTotal;
}
bar.text = withPercent(percentage);
bar.text = getMessage(percentage);
}
transferredSoFar = progress.transferred;
}
@@ -66,7 +66,7 @@ function createProgressTracker({
if (error) {
bar.fail();
} else if (isComplete) {
bar.succeed(`${completedMessage ?? message} ${chalk.dim(prettyTime)}`);
bar.succeed(`${completedMessage} ${chalk.dim(prettyTime)}`);
}
}
};
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with the eas.json",
"version": "0.4.0",
"version": "0.6.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/eas-build-job": "0.2.10",
"@expo/eas-build-job": "0.2.16",
"@hapi/joi": "^17.1.1",
"fs-extra": "^9.0.1",
"tslib": "^1"
+21 -17
View File
@@ -1,4 +1,4 @@
import { Android, Workflow, iOS } from '@expo/eas-build-job';
import { Android, Cache, Ios, Workflow } from '@expo/eas-build-job';
export enum CredentialsSource {
LOCAL = 'local',
@@ -6,49 +6,53 @@ export enum CredentialsSource {
AUTO = 'auto',
}
export enum DistributionType {
STORE = 'store',
INTERNAL = 'internal',
}
export type AndroidDistributionType = 'store' | 'internal';
export type iOSDistributionType = 'store' | 'internal' | 'simulator';
export type DistributionType = AndroidDistributionType | iOSDistributionType;
export type VersionAutoIncrement = boolean | 'version' | 'buildNumber';
export interface AndroidManagedBuildProfile extends Android.BuilderEnvironment {
workflow: Workflow.Managed;
workflow: Workflow.MANAGED;
credentialsSource: CredentialsSource;
buildType?: Android.ManagedBuildType;
releaseChannel?: string;
distribution?: DistributionType;
distribution?: AndroidDistributionType;
cache: Cache | null;
}
export interface AndroidGenericBuildProfile extends Android.BuilderEnvironment {
workflow: Workflow.Generic;
workflow: Workflow.GENERIC;
credentialsSource: CredentialsSource;
gradleCommand?: string;
releaseChannel?: string;
artifactPath?: string;
withoutCredentials?: boolean;
distribution?: DistributionType;
distribution?: AndroidDistributionType;
cache: Cache | null;
}
export interface iOSManagedBuildProfile extends iOS.BuilderEnvironment {
workflow: Workflow.Managed;
export interface iOSManagedBuildProfile extends Ios.BuilderEnvironment {
workflow: Workflow.MANAGED;
credentialsSource: CredentialsSource;
buildType?: iOS.ManagedBuildType;
buildType?: Ios.ManagedBuildType;
releaseChannel?: string;
distribution?: DistributionType;
distribution?: iOSDistributionType;
autoIncrement: VersionAutoIncrement;
cache: Cache | null;
}
export interface iOSGenericBuildProfile extends iOS.BuilderEnvironment {
workflow: Workflow.Generic;
export interface iOSGenericBuildProfile extends Ios.BuilderEnvironment {
workflow: Workflow.GENERIC;
credentialsSource: CredentialsSource;
scheme?: string;
schemeBuildConfiguration?: iOS.GenericSchemeBuildConfiguration;
schemeBuildConfiguration?: Ios.SchemeBuildConfiguration | 'Auto';
releaseChannel?: string;
artifactPath?: string;
distribution?: DistributionType;
distribution?: iOSDistributionType;
autoIncrement: VersionAutoIncrement;
cache: Cache | null;
disableIosBundleIdentifierValidation?: boolean;
}
export type AndroidBuildProfile = AndroidManagedBuildProfile | AndroidGenericBuildProfile;
+17 -8
View File
@@ -6,6 +6,9 @@ import { AndroidBuildProfile, BuildProfile, EasConfig, iOSBuildProfile } from '.
import { EasJsonSchema, schemaBuildProfileMap } from './EasJsonSchema';
interface EasJson {
experimental?: {
disableIosBundleIdentifierValidation?: boolean;
};
builds: {
android?: { [key: string]: BuildProfilePreValidation };
ios?: { [key: string]: BuildProfilePreValidation };
@@ -26,7 +29,7 @@ export class EasJsonReader {
let androidConfig;
if (['android', 'all'].includes(this.platform)) {
androidConfig = this.validateBuildProfile<AndroidBuildProfile>(
Platform.Android,
Platform.ANDROID,
buildProfileName,
easJson.builds?.android || {}
);
@@ -34,15 +37,21 @@ export class EasJsonReader {
let iosConfig;
if (['ios', 'all'].includes(this.platform)) {
iosConfig = this.validateBuildProfile<iOSBuildProfile>(
Platform.iOS,
Platform.IOS,
buildProfileName,
easJson.builds?.ios || {}
);
}
const iosExperimental = easJson.experimental?.disableIosBundleIdentifierValidation
? {
disableIosBundleIdentifierValidation:
easJson.experimental.disableIosBundleIdentifierValidation,
}
: {};
return {
builds: {
...(androidConfig ? { android: androidConfig } : {}),
...(iosConfig ? { ios: iosConfig } : {}),
...(iosConfig ? { ios: { ...iosConfig, ...iosExperimental } } : {}),
},
};
}
@@ -53,8 +62,8 @@ export class EasJsonReader {
const androidProfiles = easJson.builds?.android ?? {};
for (const name of Object.keys(androidProfiles)) {
try {
if (this.isWorkflowKeySpecified(Platform.Android, name, androidProfiles)) {
await this.validateBuildProfile(Platform.Android, name, androidProfiles);
if (this.isWorkflowKeySpecified(Platform.ANDROID, name, androidProfiles)) {
await this.validateBuildProfile(Platform.ANDROID, name, androidProfiles);
}
} catch (err) {
err.msg = `Failed to validate Android build profile "${name}"\n${err.msg}`;
@@ -64,8 +73,8 @@ export class EasJsonReader {
const iosProfiles = easJson.builds?.ios ?? {};
for (const name of Object.keys(iosProfiles)) {
try {
if (this.isWorkflowKeySpecified(Platform.iOS, name, iosProfiles)) {
await this.validateBuildProfile(Platform.iOS, name, iosProfiles);
if (this.isWorkflowKeySpecified(Platform.IOS, name, iosProfiles)) {
await this.validateBuildProfile(Platform.IOS, name, iosProfiles);
}
} catch (err) {
err.msg = `Failed to validate iOS build profile "${name}"\n${err.msg}`;
@@ -94,7 +103,7 @@ export class EasJsonReader {
buildProfiles: Record<string, BuildProfilePreValidation>
): T {
const buildProfile = this.resolveBuildProfile(platform, buildProfileName, buildProfiles);
if (![Workflow.Generic, Workflow.Managed].includes(buildProfile.workflow)) {
if (![Workflow.GENERIC, Workflow.MANAGED].includes(buildProfile.workflow)) {
throw new Error(
'"workflow" key is required in a build profile and has to be one of ["generic", "managed"].'
);
+29 -16
View File
@@ -1,4 +1,4 @@
import { Android, iOS } from '@expo/eas-build-job';
import { Android, Ios } from '@expo/eas-build-job';
import Joi, { CustomHelpers } from '@hapi/joi';
const semverSchemaCheck = (value: any, helpers: CustomHelpers) => {
@@ -13,21 +13,27 @@ const AndroidBuilderEnvironmentSchema = Joi.object({
image: Joi.string()
.valid(...Android.builderBaseImages)
.default('default'),
node: Joi.string().custom(semverSchemaCheck),
yarn: Joi.string().custom(semverSchemaCheck),
ndk: Joi.string(),
env: Joi.object().pattern(Joi.string(), Joi.string()).default({}),
node: Joi.string().empty(null).custom(semverSchemaCheck),
yarn: Joi.string().empty(null).custom(semverSchemaCheck),
ndk: Joi.string().empty(null).custom(semverSchemaCheck),
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)).default({}),
});
const IosBuilderEnvironmentSchema = Joi.object({
image: Joi.string()
.valid(...iOS.builderBaseImages)
.valid(...Ios.builderBaseImages)
.default('default'),
node: Joi.string().custom(semverSchemaCheck),
yarn: Joi.string().custom(semverSchemaCheck),
fastlane: Joi.string().custom(semverSchemaCheck),
cocoapods: Joi.string().custom(semverSchemaCheck),
env: Joi.object().pattern(Joi.string(), Joi.string()).default({}),
node: Joi.string().empty(null).custom(semverSchemaCheck),
yarn: Joi.string().empty(null).custom(semverSchemaCheck),
fastlane: Joi.string().empty(null).custom(semverSchemaCheck),
cocoapods: Joi.string().empty(null).custom(semverSchemaCheck),
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)).default({}),
});
const CacheSchema = Joi.object({
key: Joi.string().allow('').default('').max(128),
cacheDefaultPaths: Joi.boolean().default(true),
customPaths: Joi.array().items(Joi.string()).default([]),
});
const AndroidGenericSchema = Joi.object({
@@ -40,8 +46,9 @@ const AndroidGenericSchema = Joi.object({
}),
releaseChannel: Joi.string(),
artifactPath: Joi.string(),
withoutCredentials: Joi.boolean(),
withoutCredentials: Joi.boolean().default(false),
distribution: Joi.string().valid('store', 'internal').default('store'),
cache: CacheSchema.allow(null).default({ key: '', cacheDefaultPaths: true, customPaths: [] }),
}).concat(AndroidBuilderEnvironmentSchema);
const AndroidManagedSchema = Joi.object({
@@ -54,30 +61,33 @@ const AndroidManagedSchema = Joi.object({
otherwise: Joi.string().valid('apk', 'app-bundle', 'development-client').default('app-bundle'),
}),
distribution: Joi.string().valid('store', 'internal').default('store'),
cache: CacheSchema.allow(null).default({ key: '', cacheDefaultPaths: true, customPaths: [] }),
}).concat(AndroidBuilderEnvironmentSchema);
const iOSGenericSchema = Joi.object({
workflow: Joi.string().valid('generic').required(),
credentialsSource: Joi.string().valid('local', 'remote', 'auto').default('auto'),
scheme: Joi.string(),
schemeBuildConfiguration: Joi.string().valid('Debug', 'Release'),
schemeBuildConfiguration: Joi.string().valid('Debug', 'Release', 'Auto').default('Release'),
releaseChannel: Joi.string(),
artifactPath: Joi.string(),
distribution: Joi.string().valid('store', 'internal').default('store'),
distribution: Joi.string().valid('store', 'internal', 'simulator').default('store'),
autoIncrement: Joi.alternatives()
.try(Joi.boolean(), Joi.string().valid('version', 'buildNumber'))
.default(false),
cache: CacheSchema.allow(null).default({ key: '', cacheDefaultPaths: true, customPaths: [] }),
}).concat(IosBuilderEnvironmentSchema);
const iOSManagedSchema = Joi.object({
workflow: Joi.string().valid('managed').required(),
credentialsSource: Joi.string().valid('local', 'remote', 'auto').default('auto'),
buildType: Joi.string().valid('release', 'development-client'),
buildType: Joi.string().valid('release', 'development-client').default('release'),
releaseChannel: Joi.string(),
distribution: Joi.string().valid('store', 'internal').default('store'),
distribution: Joi.string().valid('store', 'internal', 'simulator').default('store'),
autoIncrement: Joi.alternatives()
.try(Joi.boolean(), Joi.string().valid('version', 'buildNumber'))
.default(false),
cache: CacheSchema.allow(null).default({ key: '', cacheDefaultPaths: true, customPaths: [] }),
}).concat(IosBuilderEnvironmentSchema);
const schemaBuildProfileMap: Record<string, Record<string, Joi.Schema>> = {
@@ -92,6 +102,9 @@ const schemaBuildProfileMap: Record<string, Record<string, Joi.Schema>> = {
};
const EasJsonSchema = Joi.object({
experimental: Joi.object({
disableIosBundleIdentifierValidation: Joi.boolean(),
}),
builds: Joi.object({
android: Joi.object().pattern(
Joi.string(),
@@ -28,6 +28,8 @@ test('minimal valid android eas.json', async () => {
distribution: 'store',
credentialsSource: 'auto',
env: {},
withoutCredentials: false,
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
image: 'default',
},
},
@@ -53,7 +55,9 @@ test('minimal valid ios eas.json', async () => {
workflow: 'generic',
autoIncrement: false,
env: {},
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
image: 'default',
schemeBuildConfiguration: 'Release',
},
},
}).toEqual(easJson);
@@ -80,6 +84,8 @@ test('minimal valid eas.json for both platforms', async () => {
distribution: 'store',
credentialsSource: 'auto',
env: {},
withoutCredentials: false,
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
image: 'default',
},
ios: {
@@ -88,7 +94,9 @@ test('minimal valid eas.json for both platforms', async () => {
credentialsSource: 'auto',
autoIncrement: false,
env: {},
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
image: 'default',
schemeBuildConfiguration: 'Release',
},
},
}).toEqual(easJson);
@@ -115,6 +123,8 @@ test('valid eas.json with both platform, but reading only android', async () =>
distribution: 'store',
credentialsSource: 'auto',
env: {},
withoutCredentials: false,
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
image: 'default',
},
},
@@ -148,6 +158,7 @@ test('valid eas.json for development client builds', async () => {
distribution: 'store',
env: {},
image: 'default',
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
buildType: 'development-client',
},
ios: {
@@ -157,6 +168,7 @@ test('valid eas.json for development client builds', async () => {
autoIncrement: false,
env: {},
image: 'default',
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
buildType: 'development-client',
},
},
@@ -185,6 +197,8 @@ test('valid generic profile for internal distribution on Android', async () => {
credentialsSource: 'auto',
gradleCommand: ':app:assembleRelease',
env: {},
withoutCredentials: false,
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
image: 'default',
},
},
@@ -213,6 +227,7 @@ test('valid managed profile for internal distribution on Android', async () => {
distribution: 'internal',
credentialsSource: 'auto',
env: {},
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
image: 'default',
},
},
+3 -1
View File
@@ -1,12 +1,14 @@
export {
CredentialsSource,
DistributionType,
AndroidDistributionType,
AndroidManagedBuildProfile,
AndroidGenericBuildProfile,
AndroidBuildProfile,
iOSDistributionType,
iOSManagedBuildProfile,
iOSGenericBuildProfile,
iOSBuildProfile,
DistributionType,
EasConfig,
VersionAutoIncrement,
} from './Config.types';
+21 -84
View File
@@ -1093,15 +1093,15 @@
"@babel/preset-env" "^7.4.4"
"@babel/preset-typescript" "^7.3.3"
"@expo/[email protected].18":
version "1.0.18"
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-1.0.18.tgz#f0f7d36316d60ea67b4cc6394bffef6d6904345d"
integrity sha512-8Ey+22cEAOxK+SBJY+OazaLsPyL5FXdsykfBg/QQJE2Y/DTFebUVlr5bQyeqavbASDvmDxg3Fd71A8Se8+qT1g==
"@expo/[email protected].21-alpha.0":
version "1.0.21-alpha.0"
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-1.0.21-alpha.0.tgz#cc6d428809a8b1f483de1f4ca6b8219308bb3680"
integrity sha512-npi9sttSQOxl/NWc/FSlIy+ws9HgUoX3vsbu+rgTmD2ARtF5D8x5NR5upnWWpwoq6ZXaYXwDXNcKZHWLGrCfjA==
dependencies:
"@expo/config-types" "^40.0.0-beta.2"
"@expo/configure-splash-screen" "0.3.3"
"@expo/configure-splash-screen" "0.3.4"
"@expo/image-utils" "0.3.10"
"@expo/json-file" "8.2.27"
"@expo/json-file" "8.2.28-alpha.0"
"@expo/plist" "0.0.11"
find-up "~5.0.0"
fs-extra "9.0.0"
@@ -1135,26 +1135,26 @@
semver "^7.1.3"
slugify "^1.3.4"
"@expo/[email protected].3":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@expo/configure-splash-screen/-/configure-splash-screen-0.3.3.tgz#ef44ba4d62443297f9d8a9326a71b40b8b413528"
integrity sha512-fWy6Z52Mj2a7yjdvpIJkP9G3kfkoXE79aHvTDwgggIE0KLhwnPF27v+KS0wJUf7b4JM6w0zKOlUZjQhn0kSNyA==
"@expo/[email protected].4":
version "0.3.4"
resolved "https://registry.yarnpkg.com/@expo/configure-splash-screen/-/configure-splash-screen-0.3.4.tgz#b91d8f08fd96272bd3d7aaa9b51d6189b932c7cc"
integrity sha512-HsukM03X5/EXSucVsLN/oLqyFq/1jAjpADkgU1HLaezFpkr+TOquI6yDwdDp1450kcm891PE/SYJ+mCdPxzDLw==
dependencies:
"@react-native-community/cli-platform-ios" "^4.10.0"
color-string "^1.5.3"
commander "^5.1.0"
core-js "^3.6.5"
deep-equal "^2.0.3"
fs-extra "^9.0.0"
glob "^7.1.6"
lodash "^4.17.15"
pngjs "^5.0.0"
xcode "^3.0.0"
xml-js "^1.6.11"
"@expo/[email protected]0":
version "0.2.10"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.10.tgz#ed6ac5e094e62167c0afeaae929aab6be68b48b4"
integrity sha512-AEbMlvbF/ugZFHDuMY9Bpojm8LgZIL7IIES+jevkG1ftzoPpRXP+q1Ioch+KPFdG6SyPrqvGa2NBK5Oyq/MtTA==
"@expo/[email protected]6":
version "0.2.16"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.16.tgz#490be96d32f4b9ef4501d117796f523680ce5a00"
integrity sha512-PJRYwk3phLDpqtIKTEBgyqvJsxCoa55CHYRJguRJ9lYHxqxL/litUruxu1Y5cjF+iGXw/ElpN2C75wuboUdjcw==
dependencies:
"@hapi/joi" "^17.1.1"
@@ -1186,15 +1186,14 @@
lodash "^4.17.15"
write-file-atomic "^2.3.0"
"@expo/[email protected]7":
version "8.2.27"
resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.2.27.tgz#548ff0a9989f9f789cdb2b689d17a4faf491de67"
integrity sha512-Iyqg1jbXOTg0JfCGwMrkaaRmVFjQrWDBQAhYLTdvOD3GrXYuKI1vUV+3Wqw0NnU+TYoNUpi7aB8dNzPvLj0oag==
"@expo/[email protected]8-alpha.0":
version "8.2.28-alpha.0"
resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.2.28-alpha.0.tgz#0e5280c82a17165be12c6a95cd95ec108c6dfe7b"
integrity sha512-cCQdw/Nfw8doXjN3onvUnWkuJjtVxx2iUjSOLMydvgI87YpW3x05uUXOVs4P+77YFVoFS6xbki+fmKK2JSCf8w==
dependencies:
"@babel/code-frame" "~7.10.4"
fs-extra "9.0.0"
json5 "^1.0.1"
lodash "^4.17.19"
write-file-atomic "^2.3.0"
"@expo/[email protected]":
@@ -3073,31 +3072,6 @@
dependencies:
"@types/node" ">= 8"
"@react-native-community/cli-platform-ios@^4.10.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-ios/-/cli-platform-ios-4.13.0.tgz#a738915c68cac86df54e578b59a1311ea62b1aef"
integrity sha512-6THlTu8zp62efkzimfGr3VIuQJ2514o+vScZERJCV1xgEi8XtV7mb/ZKt9o6Y9WGxKKkc0E0b/aVAtgy+L27CA==
dependencies:
"@react-native-community/cli-tools" "^4.13.0"
chalk "^3.0.0"
glob "^7.1.3"
js-yaml "^3.13.1"
lodash "^4.17.15"
plist "^3.0.1"
xcode "^2.0.0"
"@react-native-community/cli-tools@^4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@react-native-community/cli-tools/-/cli-tools-4.13.0.tgz#b406463d33af16cedc4305a9a9257ed32845cf1b"
integrity sha512-s4f489h5+EJksn4CfheLgv5PGOM0CDmK1UEBLw2t/ncWs3cW2VI7vXzndcd/WJHTv3GntJhXDcJMuL+Z2IAOgg==
dependencies:
chalk "^3.0.0"
lodash "^4.17.15"
mime "^2.4.1"
node-fetch "^2.6.0"
open "^6.2.0"
shell-quote "1.6.1"
"@samverschueren/stream-to-observable@^0.3.0":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301"
@@ -3784,11 +3758,6 @@ array-filter@^1.0.0:
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
array-filter@~0.0.0:
version "0.0.1"
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec"
integrity sha1-fajPLiZijtcygDWB/SH2fKzS7uw=
array-find-index@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
@@ -3808,16 +3777,6 @@ array-includes@^3.1.1:
es-abstract "^1.17.0"
is-string "^1.0.5"
array-map@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662"
integrity sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=
array-reduce@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b"
integrity sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=
array-union@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
@@ -7509,11 +7468,6 @@ is-windows@^1.0.0, is-windows@^1.0.1, is-windows@^1.0.2:
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
is-wsl@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
is-wsl@^2.1.1, is-wsl@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
@@ -8842,7 +8796,7 @@ mime@^1.3.4:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
mime@^2.4.1, mime@^2.4.4, mime@^2.4.7:
mime@^2.4.4, mime@^2.4.7:
version "2.4.7"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.7.tgz#962aed9be0ed19c91fd7dc2ece5d7f4e89a90d74"
integrity sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==
@@ -9450,13 +9404,6 @@ onetime@^5.1.0:
dependencies:
mimic-fn "^2.1.0"
open@^6.2.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/open/-/open-6.4.0.tgz#5c13e96d0dc894686164f18965ecfe889ecfc8a9"
integrity sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==
dependencies:
is-wsl "^1.1.0"
optionator@^0.8.1, optionator@^0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
@@ -10865,16 +10812,6 @@ shebang-regex@^3.0.0:
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
[email protected]:
version "1.6.1"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767"
integrity sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=
dependencies:
array-filter "~0.0.0"
array-map "~0.0.0"
array-reduce "~0.0.0"
jsonify "~0.0.0"
shellwords@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
@@ -12422,7 +12359,7 @@ ws@^7.2.3:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==
xcode@^2.0.0, xcode@^2.1.0:
xcode@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/xcode/-/xcode-2.1.0.tgz#bab64a7e954bb50ca8d19da7e09531c65a43ecfe"
integrity sha512-uCrmPITrqTEzhn0TtT57fJaNaw8YJs1aCzs+P/QqxsDbvPZSv7XMPPwXrKvHtD6pLjBM/NaVwraWJm8q83Y4iQ==