Compare commits

...
21 Commits
Author SHA1 Message Date
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
55 changed files with 1486 additions and 341 deletions

No files matched your search

+27 -2
View File
@@ -8,9 +8,34 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 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
- Fix detecting application target (iOS builds). ([#238](https://github.com/expo/eas-cli/pull/2238) by [@dsokal](https://github.com/dsokal))
- 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
@@ -29,5 +54,5 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 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 `--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.2"
"version": "0.5.0"
}
+14 -14
View File
@@ -21,7 +21,7 @@ $ npm install -g eas-cli
$ eas COMMAND
running command...
$ eas (-v|--version|version)
eas-cli/0.4.2 darwin-x64 node-v14.15.4
eas-cli/0.5.0 darwin-x64 node-v14.15.4
$ eas --help [COMMAND]
USAGE
$ eas COMMAND
@@ -59,7 +59,7 @@ ALIASES
$ eas login
```
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/account/login.ts)_
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.5.0/build/commands/account/login.ts)_
## `eas account:logout`
@@ -73,7 +73,7 @@ ALIASES
$ eas logout
```
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/account/logout.ts)_
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.5.0/build/commands/account/logout.ts)_
## `eas account:view`
@@ -87,7 +87,7 @@ ALIASES
$ eas whoami
```
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/account/view.ts)_
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.5.0/build/commands/account/view.ts)_
## `eas build`
@@ -106,7 +106,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.2/build/commands/build/index.ts)_
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.5.0/build/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -117,7 +117,7 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [build/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/build/cancel.ts)_
_See code: [build/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.5.0/build/commands/build/cancel.ts)_
## `eas build:configure`
@@ -132,7 +132,7 @@ OPTIONS
--allow-experimental Enable experimental configuration steps.
```
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/build/configure.ts)_
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.5.0/build/commands/build/configure.ts)_
## `eas build:list`
@@ -148,7 +148,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.2/build/commands/build/list.ts)_
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.5.0/build/commands/build/list.ts)_
## `eas build:view [BUILD_ID]`
@@ -159,7 +159,7 @@ USAGE
$ eas build:view [BUILD_ID]
```
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/build/view.ts)_
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.5.0/build/commands/build/view.ts)_
## `eas credentials`
@@ -170,7 +170,7 @@ USAGE
$ eas credentials
```
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/credentials.ts)_
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.5.0/build/commands/credentials.ts)_
## `eas device:create`
@@ -181,7 +181,7 @@ USAGE
$ eas device:create
```
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/device/create.ts)_
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.5.0/build/commands/device/create.ts)_
## `eas device:list`
@@ -195,7 +195,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/device/list.ts)_
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.5.0/build/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -206,7 +206,7 @@ USAGE
$ eas device:view [UDID]
```
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/device/view.ts)_
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.5.0/build/commands/device/view.ts)_
## `eas help [COMMAND]`
@@ -303,5 +303,5 @@ EXAMPLES
and provide its App ID
```
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/submit.ts)_
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.5.0/build/commands/submit.ts)_
<!-- commandsstop -->
+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",
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.4.2",
"version": "0.5.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -11,8 +11,8 @@
"@expo/apple-utils": "0.0.0-alpha.17",
"@expo/config": "~3.3.19",
"@expo/config-plugins": "1.0.19-alpha.0",
"@expo/eas-build-job": "0.2.11",
"@expo/eas-json": "^0.4.1",
"@expo/eas-build-job": "0.2.14",
"@expo/eas-json": "^0.5.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
);
@@ -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,
+7 -5
View File
@@ -112,8 +112,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;
@@ -183,7 +182,7 @@ enum ShouldCommitChanges {
}
async function reviewAndCommitChangesAsync(
commitMessage: string,
initialCommitMessage: string,
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
): Promise<void> {
if (nonInteractive) {
@@ -212,10 +211,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) {
+5 -5
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,7 +39,7 @@ 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.BuildScheme.getApplicationTargetForSchemeAsync(
buildCtx.commandCtx.projectDir,
@@ -67,7 +67,7 @@ export async function prepareIosBuildAsync(
});
}
async function resolveSchemeAsync(ctx: BuildContext<Platform.iOS>): Promise<string> {
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(
+1 -1
View File
@@ -38,7 +38,7 @@ export async function validateAndSyncProjectConfigurationAsync({
const { workflow, autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement);
if (workflow === Workflow.Generic) {
if (workflow === Workflow.GENERIC) {
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(
projectDir
);
@@ -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';
}
+22 -17
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,7 +131,7 @@ async function prepareGenericJobAsync(
credentials: jobData.credentials,
targetName: jobData.projectConfiguration.iosApplicationTarget,
})),
type: Workflow.Generic,
type: Workflow.GENERIC,
scheme: jobData.projectConfiguration.iosBuildScheme,
schemeBuildConfiguration: buildProfile.schemeBuildConfiguration,
artifactPath: buildProfile.artifactPath,
@@ -136,10 +141,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 +155,7 @@ async function prepareManagedJobAsync(
credentials: jobData.credentials,
targetName,
})),
type: Workflow.Managed,
type: Workflow.MANAGED,
buildType: buildProfile.buildType,
username: accountName,
releaseChannel: buildProfile.releaseChannel,
+2 -2
View File
@@ -1,4 +1,4 @@
import { CredentialsSource, DistributionType } from '@expo/eas-json';
import { CredentialsSource } from '@expo/eas-json';
import { getAppIdentifierAsync } from '../project/projectUtils';
import { BuildContext } from './context';
@@ -26,7 +26,7 @@ export async function collectMetadata<T extends Platform>(
credentialsSource,
sdkVersion: ctx.commandCtx.exp.sdkVersion,
trackingContext: ctx.trackingCtx,
distribution: ctx.buildProfile.distribution ?? DistributionType.STORE,
distribution: ctx.buildProfile.distribution ?? 'store',
appName: ctx.commandCtx.exp.name,
appIdentifier:
(await getAppIdentifierAsync(ctx.commandCtx.projectDir, ctx.platform)) ?? undefined,
+2 -2
View File
@@ -83,7 +83,7 @@ export type BuildMetadata = {
/**
* Distribution type
* Indicates whether this is a build for store or internal distribution.
* Indicates whether this is a build for store, internal distribution, or simulator (iOS).
*/
distribution: DistributionType;
@@ -100,7 +100,7 @@ export type BuildMetadata = {
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,
+13 -5
View File
@@ -43,12 +43,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 +89,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...');
}
@@ -151,7 +150,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 +164,9 @@ async function commitPromptAsync(initialCommitMessage?: string): Promise<void> {
initial: initialCommitMessage,
validate: (input: string) => input !== '',
});
if (commitAllFiles) {
await spawnAsync('git', ['add', '-A']);
}
await commitChangedFilesAsync(message);
}
+16 -16
View File
@@ -14,30 +14,30 @@ import { getBranchNameAsync } from '../../utils/git';
export async function createUpdateBranchOnAppAsync({
appId,
branchName,
name,
}: {
appId: string;
branchName: string;
}): Promise<Pick<UpdateBranch, 'id' | 'branchName'>> {
name: string;
}): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<
{ updateBranch: { createUpdateBranchForApp: Pick<UpdateBranch, 'id' | 'branchName'> } },
{ appId: string; branchName: string }
{ updateBranch: { createUpdateBranchForApp: Pick<UpdateBranch, 'id' | 'name'> } },
{ appId: string; name: string }
>(
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()
@@ -51,7 +51,7 @@ export default class BranchCreate extends Command {
static args = [
{
name: 'branchName',
name: 'name',
required: false,
description: 'Name of the branch to create',
},
@@ -66,7 +66,7 @@ export default class BranchCreate extends Command {
async run() {
let {
args: { branchName },
args: { name },
flags,
} = this.parse(BranchCreate);
@@ -82,14 +82,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 +97,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 +105,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}`
)}.`
);
+4 -7
View File
@@ -38,10 +38,7 @@ export default class BranchList extends Command {
} else {
const table = new CliTable({ head: ['Branch', 'Latest update'] });
table.push(
...branches.map((branch: UpdateBranch) => [
branch.branchName,
formatUpdate(branch.updates[0]),
])
...branches.map((branch: UpdateBranch) => [branch.name, formatUpdate(branch.updates[0])])
);
Log.log(table.toString());
if (branches.length >= BRANCHES_LIMIT) {
@@ -61,7 +58,7 @@ export default class BranchList extends Command {
fullName
updateBranches(offset: 0, limit: $limit) {
id
branchName
name
updates(offset: 0, limit: 1) {
id
actor {
@@ -75,7 +72,7 @@ export default class BranchList extends Command {
}
}
updatedAt
updateMessage
message
}
}
}
@@ -105,7 +102,7 @@ function formatUpdate(update: Update | undefined): 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());
}
@@ -33,7 +33,7 @@ async function renameUpdateBranchOnAppAsync({
updateBranch {
editUpdateBranch(input: $input) {
id
branchName
name
}
}
}
@@ -41,8 +41,8 @@ async function renameUpdateBranchOnAppAsync({
{
input: {
appId,
branchName: currentName,
newBranchName: newName,
name: currentName,
newName,
},
}
)
@@ -119,13 +119,13 @@ export default class BranchRename extends Command {
});
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}`)}.`
);
}
+30 -30
View File
@@ -14,17 +14,17 @@ 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,
name,
}: {
appId: string;
branchName: string;
name: string;
}): Promise<{
id: string;
branchName: string;
name: string;
updates: TruncatedUpdate[];
}> {
const data = await withErrorHandlingAsync(
@@ -33,9 +33,9 @@ async function viewUpdateBranchAsync({
{
app: {
byId: {
updateBranchByBranchName: {
updateBranchByName: {
id: string;
branchName: string;
name: string;
updates: TruncatedUpdate[];
};
};
@@ -43,22 +43,22 @@ async function viewUpdateBranchAsync({
},
{
appId: string;
branchName: string;
name: string;
limit: number;
}
>(
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 +77,13 @@ async function viewUpdateBranchAsync({
`,
{
appId,
branchName,
name,
limit: PAGE_LIMIT,
}
)
.toPromise()
);
return data.app.byId.updateBranchByBranchName;
return data.app.byId.updateBranchByName;
}
export default class BranchView extends Command {
@@ -92,7 +92,7 @@ export default class BranchView extends Command {
static args = [
{
name: 'branchName',
name: 'name',
required: false,
description: 'Name of the branch to view',
},
@@ -107,7 +107,7 @@ export default class BranchView extends Command {
async run() {
let {
args: { branchName },
args: { name },
flags: { json: jsonFlag },
} = this.parse(BranchView);
@@ -124,14 +124,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 +139,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());
}
}
@@ -38,8 +38,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(
+13 -21
View File
@@ -33,22 +33,14 @@ async function createUpdateChannelOnAppAsync({
graphqlClient
.mutation<
{ updateChannel: { createUpdateChannelForApp: UpdateChannel } },
{ appId: string; channelName: string; branchMapping: string }
{ appId: string; name: string; branchMapping: string }
>(
gql`
mutation CreateUpdateChannelForApp(
$appId: ID!
$channelName: String!
$branchMapping: String!
) {
mutation CreateUpdateChannelForApp($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,7 +48,7 @@ async function createUpdateChannelOnAppAsync({
`,
{
appId,
channelName,
name: channelName,
branchMapping,
}
)
@@ -71,7 +63,7 @@ export default class ChannelCreate extends Command {
static args = [
{
name: 'channelName',
name: 'name',
required: false,
description: 'Name of the channel to create',
},
@@ -87,7 +79,7 @@ export default class ChannelCreate extends Command {
async run() {
let {
args: { channelName },
args: { name: channelName },
flags: { json: jsonFlag },
} = this.parse(ChannelCreate);
@@ -109,9 +101,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,14 +114,14 @@ 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`;
@@ -142,12 +134,12 @@ export default class ChannelCreate extends Command {
});
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,181 @@
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 { UpdateChannel } 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(variables: {
appId: string;
channelName: string;
}): Promise<UpdateChannel> {
const data = await withErrorHandlingAsync(
graphqlClient
.query(
gql`
query GetChannelByNameForApp($appId: String!, $channelName: String!) {
app {
byId(appId: $appId) {
id
updateChannelByName(name: $channelName) {
id
name
updateBranches(offset: 0, limit: 1) {
id
name
}
}
}
}
}
`,
variables
)
.toPromise()
);
return data.app.byId.updateChannelByName;
}
async function updateChannelBranchMappingAsync(variables: {
channelId: string;
branchMapping: string;
}): Promise<UpdateChannel> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation(
gql`
mutation UpdateChannelBranchMapping($channelId: ID!, $branchMapping: String!) {
updateChannel {
editUpdateChannel(channelId: $channelId, branchMapping: $branchMapping) {
id
name
createdAt
branchMapping
updateBranches(offset: 0, limit: 1) {
id
name
}
}
}
}
`,
variables
)
.toPromise()
);
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,121 @@
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 { UpdateChannel } 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<UpdateChannel[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query(
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, limit: CHANNEL_LIMIT }
)
.toPromise()
);
return data.app.byId.updateChannels;
}
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 channels = await getAllUpdateChannelForAppAsync({ appId: 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,143 @@
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 { UpdateChannel } 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(variables: {
appId: string;
channelName: string;
}): Promise<UpdateChannel> {
const data = await withErrorHandlingAsync(
graphqlClient
.query(
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
}
}
}
}
}
}
}
}
`,
variables
)
.toPromise()
);
return data.app.byId.updateChannelByName;
}
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 channel = await getUpdateChannelByNameForAppAsync({ appId: projectId, 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());
}
}
@@ -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,5 +1,5 @@
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 Log from '../../log';
@@ -10,6 +10,7 @@ import { Context } from '../context';
import * as credentialsJsonReader from '../credentialsJson/read';
import type { IosCredentials } from '../credentialsJson/read';
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
import { isAdHocProfile } from './utils/provisioningProfile';
export { IosCredentials };
@@ -23,7 +24,7 @@ interface PartialIosCredentials {
interface Options {
app: AppLookupParams;
distribution: DistributionType;
distribution: iOSDistributionType;
skipCredentialsCheck?: boolean;
}
@@ -34,11 +35,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 +54,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 +104,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 +170,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) {
@@ -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,7 +32,7 @@ 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`);
@@ -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(
@@ -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);
+201 -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 */
@@ -3248,6 +3315,45 @@ export type CommonIosAppCredentialsWithBuildCredentialsByAppIdentifierIdQuery =
)> }
);
export type CreateUpdateChannelForAppMutationVariables = Exact<{
appId: Scalars['ID'];
name: Scalars['String'];
branchMapping: Scalars['String'];
}>;
export type CreateUpdateChannelForAppMutation = (
{ __typename?: 'RootMutation' }
& { updateChannel: (
{ __typename?: 'UpdateChannelMutation' }
& { createUpdateChannelForApp?: Maybe<(
{ __typename?: 'UpdateChannel' }
& Pick<UpdateChannel, 'id' | 'name' | 'branchMapping'>
)> }
) }
);
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' | 'createdAt' | 'branchMapping'>
& { updateBranches: Array<(
{ __typename?: 'UpdateBranch' }
& Pick<UpdateBranch, 'id' | 'name'>
)> }
)> }
) }
);
export type GetSignedUploadMutationVariables = Exact<{
contentTypes: Array<Scalars['String']>;
}>;
@@ -3275,7 +3381,7 @@ export type UpdatePublishMutation = (
{ __typename?: 'UpdateBranchMutation' }
& { publishUpdateGroup: Array<Maybe<(
{ __typename?: 'Update' }
& Pick<Update, 'id' | 'updateGroup'>
& Pick<Update, 'id' | 'group'>
)>> }
) }
);
@@ -3348,6 +3454,79 @@ export type PendingBuildsForAccountAndPlatformQuery = (
) }
);
export type GetChannelByNameForApp123213QueryVariables = Exact<{
appId: Scalars['String'];
name: Scalars['String'];
}>;
export type GetChannelByNameForApp123213Query = (
{ __typename?: 'RootQuery' }
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
& Pick<App, 'id' | 'name'>
& { 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 ListChannelsForAppQueryVariables = Exact<{
appId: Scalars['String'];
offset: Scalars['Int'];
limit: Scalars['Int'];
}>;
export type ListChannelsForAppQuery = (
{ __typename?: 'RootQuery' }
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
& Pick<App, 'id' | 'name'>
& { 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 ProjectByUsernameAndSlugQueryVariables = Exact<{
username: Scalars['String'];
slug: Scalars['String'];
@@ -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
@@ -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.
@@ -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,
});
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with the eas.json",
"version": "0.4.1",
"version": "0.5.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/eas-build-job": "0.2.11",
"@expo/eas-build-job": "0.2.14",
"@hapi/joi": "^17.1.1",
"fs-extra": "^9.0.1",
"tslib": "^1"
+20 -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,52 @@ 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;
releaseChannel?: string;
artifactPath?: string;
distribution?: DistributionType;
distribution?: iOSDistributionType;
autoIncrement: VersionAutoIncrement;
cache: Cache | null;
}
export type AndroidBuildProfile = AndroidManagedBuildProfile | AndroidGenericBuildProfile;
+7 -7
View File
@@ -26,7 +26,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,7 +34,7 @@ export class EasJsonReader {
let iosConfig;
if (['ios', 'all'].includes(this.platform)) {
iosConfig = this.validateBuildProfile<iOSBuildProfile>(
Platform.iOS,
Platform.IOS,
buildProfileName,
easJson.builds?.ios || {}
);
@@ -53,8 +53,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 +64,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 +94,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"].'
);
+25 -15
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,6 +61,7 @@ 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({
@@ -63,21 +71,23 @@ const iOSGenericSchema = Joi.object({
schemeBuildConfiguration: Joi.string().valid('Debug', '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>> = {
@@ -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,6 +55,7 @@ test('minimal valid ios eas.json', async () => {
workflow: 'generic',
autoIncrement: false,
env: {},
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
image: 'default',
},
},
@@ -80,6 +83,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,6 +93,7 @@ test('minimal valid eas.json for both platforms', async () => {
credentialsSource: 'auto',
autoIncrement: false,
env: {},
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
image: 'default',
},
},
@@ -115,6 +121,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 +156,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 +166,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 +195,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 +225,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';
+4 -4
View File
@@ -1151,10 +1151,10 @@
xcode "^3.0.0"
xml-js "^1.6.11"
"@expo/[email protected]1":
version "0.2.11"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.11.tgz#cc4c9816c1e3cea29bca7b2eac4de99c6e6b804d"
integrity sha512-IeG37+10OteIG2RpKTA0ClxFxQVuRx9QYu8qbesMpmroKSDyrRZQFD9DdAho2CFr+WCUG5vLVfJpGuw17LrQ+Q==
"@expo/[email protected]4":
version "0.2.14"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.14.tgz#10402f8c2cdd3e8c7ebc6f8773270295e7993771"
integrity sha512-+msVy5CgyQwZrUJeiV6hvg3Sjx4Fc1CHDF0jY0LxXSsRDGQDM7qde6ukHRyPvGuGxv/+PzwlWolv2kOP6GFcFQ==
dependencies:
"@hapi/joi" "^17.1.1"