mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 03:38:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
558c367038 | ||
|
|
4b88d00379 | ||
|
|
aad3b5ccd5 | ||
|
|
8b50806093 | ||
|
|
45c9bda2e9 | ||
|
|
23237210d1 | ||
|
|
e34382e6a3 | ||
|
|
b8ad2ee29a | ||
|
|
6bd62bac64 | ||
|
|
8b959fc6d8 | ||
|
|
6e615e20d6 | ||
|
|
efbc5fc4bc | ||
|
|
79c77ae835 | ||
|
|
618b6b1caf | ||
|
|
864658df80 | ||
|
|
4510e86b64 | ||
|
|
4af4490abc | ||
|
|
37c4040203 |
No files matched your search
@@ -11,7 +11,6 @@ module.exports = {
|
||||
ignoreDeclarationSort: true,
|
||||
},
|
||||
],
|
||||
'flowtype/no-types-missing-file-annotation': 'off',
|
||||
curly: 'warn',
|
||||
'import/no-cycle': 'error',
|
||||
'import/no-extraneous-dependencies': [
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
if: ${{ github.event.issue.pull_request && github.event.issue.user.login == github.event.comment.user.login && startsWith(github.event.comment.body, '/changelog-entry ') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Checkout Pull Request
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
needs: release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
needs: release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
needs: release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
needs: [build-linux, build-mac, build-windows]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
scope: 'expo'
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
needs: publish-to-npm
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- uses: preactjs/compressed-size-action@v2
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
|
||||
@@ -12,3 +12,4 @@ node_modules
|
||||
coverage
|
||||
.history
|
||||
.DS_Store
|
||||
.idea
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"jsxBracketSameLine": true,
|
||||
"bracketSameLine": true,
|
||||
"trailingComma": "es5",
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
@@ -6,11 +6,38 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Require explicitly defined applicationId/bundleIdentifier for EAS Submit in case of bare projects consisting of multiple product flavors on Android or multiple schemes/targets on iOS. ([#765](https://github.com/expo/eas-cli/pull/765) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Auto create channel on publish. ([#766](https://github.com/expo/eas-cli/pull/766) by [@jkhales](https://github.com/jkhales))
|
||||
- Interactively configure Git `user.name` and `user.email`. ([#772](https://github.com/expo/eas-cli/pull/772) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Validate release channel in eas.json. ([#764](https://github.com/expo/eas-cli/pull/764) by [@dsokal](https://github.com/dsokal))
|
||||
- Make the missing profile error message more descriptive. ([#761](https://github.com/expo/eas-cli/pull/761) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Better error message when missing eas.json. ([#770](https://github.com/expo/eas-cli/pull/770) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.36.1](https://github.com/expo/eas-cli/releases/tag/v0.36.1) - 2021-11-15
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `dateformat` import issue when building with `--local` flag. ([a520d](https://github.com/expo/eas-cli/commit/a520d1750736d61d6489da6487879f35ed3deb23) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.36.0](https://github.com/expo/eas-cli/releases/tag/v0.36.0) - 2021-11-15
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Do not require manual `eas-cli-local-build-plugin` installation. An existing global installation (either with `yarn global add` or `npm install -g`) will not be used anymore. ([#753](https://github.com/expo/eas-cli/pull/753) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Use Keystore Service when `keytool` is not installed. ([#754](https://github.com/expo/eas-cli/pull/754) by [@dsokal](https://github.com/dsokal))
|
||||
- Add applicationId/bundleIdentifier fields to the submit profile. ([#765](https://github.com/expo/eas-cli/pull/765) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.36.0"
|
||||
"version": "0.37.0"
|
||||
}
|
||||
+2
-2
@@ -8,7 +8,7 @@
|
||||
"start": "lerna run watch --parallel",
|
||||
"watch": "yarn start",
|
||||
"eas": "packages/eas-cli/bin/run",
|
||||
"lint": "eslint . --ext .ts --ext .js",
|
||||
"lint": "eslint .",
|
||||
"release": "./scripts/bin/run update-local-plugin && lerna version",
|
||||
"test": "jest",
|
||||
"clean": "lerna run clean && rm -rf node_modules coverage"
|
||||
@@ -24,7 +24,7 @@
|
||||
"jest": "27.1.0",
|
||||
"jest-watch-typeahead": "0.6.4",
|
||||
"lerna": "4.0.0",
|
||||
"prettier": "2.3.2",
|
||||
"prettier": "2.4.1",
|
||||
"ts-jest": "27.0.5",
|
||||
"typescript": "4.4.2"
|
||||
},
|
||||
|
||||
+26
-26
@@ -73,7 +73,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/account/login.js)_
|
||||
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/account/login.js)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -87,7 +87,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/account/logout.js)_
|
||||
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/account/logout.js)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -101,7 +101,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/account/view.js)_
|
||||
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/account/view.js)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -112,7 +112,7 @@ USAGE
|
||||
$ eas analytics [STATUS]
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/analytics.js)_
|
||||
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/analytics.js)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -146,7 +146,7 @@ OPTIONS
|
||||
--[no-]wait Wait for build(s) to complete
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/build/index.js)_
|
||||
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/build/index.js)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -157,7 +157,7 @@ USAGE
|
||||
$ eas build:cancel [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/build/cancel.js)_
|
||||
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/build/cancel.js)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -171,7 +171,7 @@ OPTIONS
|
||||
-p, --platform=(android|ios|all) Platform to configure
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/build/configure.js)_
|
||||
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/build/configure.js)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -204,7 +204,7 @@ OPTIONS
|
||||
--status=(new|in-queue|in-progress|errored|finished|canceled)
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/build/list.js)_
|
||||
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/build/list.js)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -218,7 +218,7 @@ OPTIONS
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/build/view.js)_
|
||||
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/build/view.js)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -233,7 +233,7 @@ OPTIONS
|
||||
--profile=profile
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/config.js)_
|
||||
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/config.js)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -244,7 +244,7 @@ USAGE
|
||||
$ eas credentials
|
||||
```
|
||||
|
||||
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/credentials.js)_
|
||||
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/credentials.js)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -255,7 +255,7 @@ USAGE
|
||||
$ eas device:create
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/device/create.js)_
|
||||
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/device/create.js)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -269,7 +269,7 @@ OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/device/list.js)_
|
||||
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/device/list.js)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -280,7 +280,7 @@ USAGE
|
||||
$ eas device:view [UDID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/device/view.js)_
|
||||
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/device/view.js)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -291,7 +291,7 @@ USAGE
|
||||
$ eas diagnostics
|
||||
```
|
||||
|
||||
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/diagnostics.js)_
|
||||
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/diagnostics.js)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -319,7 +319,7 @@ USAGE
|
||||
$ eas project:info
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/project/info.js)_
|
||||
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/project/info.js)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -333,7 +333,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/project/init.js)_
|
||||
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/project/init.js)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
@@ -350,7 +350,7 @@ OPTIONS
|
||||
--value=value Value of the secret
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/secret/create.js)_
|
||||
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/secret/create.js)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
@@ -367,7 +367,7 @@ DESCRIPTION
|
||||
Unsure where to find the secret's ID? Run eas secret:list
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/secret/delete.js)_
|
||||
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/secret/delete.js)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
@@ -378,7 +378,7 @@ USAGE
|
||||
$ eas secret:list
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/secret/list.js)_
|
||||
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/secret/list.js)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -411,7 +411,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/submit.js)_
|
||||
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/submit.js)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -430,7 +430,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/webhook/create.js)_
|
||||
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/webhook/create.js)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -444,7 +444,7 @@ ARGUMENTS
|
||||
ID ID of the webhook to delete
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/webhook/delete.js)_
|
||||
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/webhook/delete.js)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -458,7 +458,7 @@ OPTIONS
|
||||
--event=(BUILD) Event type that triggers the webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/webhook/list.js)_
|
||||
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/webhook/list.js)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -478,7 +478,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/webhook/update.js)_
|
||||
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/webhook/update.js)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -492,5 +492,5 @@ ARGUMENTS
|
||||
ID ID of the webhook to view
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/webhook/view.js)_
|
||||
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/webhook/view.js)_
|
||||
<!-- commandsstop -->
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.36.0",
|
||||
"version": "0.37.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -12,7 +12,7 @@
|
||||
"@expo/config": "6.0.3",
|
||||
"@expo/config-plugins": "4.0.3",
|
||||
"@expo/eas-build-job": "0.2.57",
|
||||
"@expo/eas-json": "^0.36.0",
|
||||
"@expo/eas-json": "^0.37.0",
|
||||
"@expo/json-file": "8.2.33",
|
||||
"@expo/pkcs12": "0.0.4",
|
||||
"@expo/plist": "0.0.14",
|
||||
|
||||
@@ -4,9 +4,9 @@ import { IOSConfig } from '@expo/config-plugins';
|
||||
import Log from '../../log';
|
||||
import { getProjectAccountName } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { ensureValidVersions } from '../utils/updates';
|
||||
import { readPlistAsync, writePlistAsync } from './plist';
|
||||
|
||||
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
@@ -59,7 +59,7 @@ async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
|
||||
|
||||
async function readExpoPlistAsync(projectDir: string): Promise<IOSConfig.ExpoPlist> {
|
||||
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
|
||||
return (await readPlistAsync(expoPlistPath)) as IOSConfig.ExpoPlist;
|
||||
return ((await readPlistAsync(expoPlistPath)) ?? {}) as IOSConfig.ExpoPlist;
|
||||
}
|
||||
|
||||
async function writeExpoPlistAsync(
|
||||
|
||||
@@ -4,7 +4,7 @@ import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
|
||||
import { readPlistAsync } from '../plist';
|
||||
import { readPlistAsync } from '../../../utils/plist';
|
||||
import {
|
||||
BumpStrategy,
|
||||
bumpVersionAsync,
|
||||
|
||||
@@ -8,9 +8,9 @@ import type { XCBuildConfiguration } from 'xcode';
|
||||
import Log from '../../log';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
|
||||
import { updateAppJsonConfigAsync } from '../utils/appJson';
|
||||
import { bumpAppVersionAsync } from '../utils/version';
|
||||
import { readPlistAsync, writePlistAsync } from './plist';
|
||||
|
||||
export enum BumpStrategy {
|
||||
APP_VERSION,
|
||||
@@ -128,7 +128,10 @@ export async function maybeResolveVersionsAsync(
|
||||
appBuildVersion: await readBuildNumberAsync(projectDir, exp, buildSettings),
|
||||
appVersion: await readShortVersionAsync(projectDir, exp, buildSettings),
|
||||
};
|
||||
} catch {
|
||||
} catch (err: any) {
|
||||
Log.warn('Failed to read app versions.');
|
||||
Log.warn(err.message);
|
||||
Log.warn('Proceeding anyway...');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -174,7 +177,7 @@ async function readInfoPlistAsync(
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): Promise<IOSConfig.InfoPlist> {
|
||||
const infoPlistPath = getInfoPlistPath(projectDir, buildSettings);
|
||||
return (await readPlistAsync(infoPlistPath)) as IOSConfig.InfoPlist;
|
||||
return ((await readPlistAsync(infoPlistPath)) ?? {}) as IOSConfig.InfoPlist;
|
||||
}
|
||||
|
||||
async function writeInfoPlistAsync({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Job } from '@expo/eas-build-job';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
|
||||
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
|
||||
const PLUGIN_PACKAGE_VERSION = '0.0.47';
|
||||
const PLUGIN_PACKAGE_VERSION = '0.0.50';
|
||||
|
||||
export async function runLocalBuildAsync(job: Job): Promise<void> {
|
||||
const { command, args } = getCommandAndArgs(job);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import { error } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
import resolveFrom from 'resolve-from';
|
||||
@@ -21,7 +20,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
}: {
|
||||
projectDir: string;
|
||||
nonInteractive?: boolean;
|
||||
buildProfiles?: ProfileData<BuildProfile>[];
|
||||
buildProfiles?: ProfileData<'build'>[];
|
||||
}): Promise<void> {
|
||||
if (await isExpoDevClientInstalledAsync(projectDir)) {
|
||||
return;
|
||||
|
||||
@@ -136,6 +136,7 @@ export async function reviewAndCommitChangesAsync(
|
||||
await getVcsClient().commitAsync({
|
||||
commitMessage: initialCommitMessage,
|
||||
commitAllFiles: false,
|
||||
nonInteractive,
|
||||
});
|
||||
Log.withTick('Committed changes.');
|
||||
return;
|
||||
|
||||
@@ -35,6 +35,7 @@ import { formatUpdate } from '../../update/utils';
|
||||
import uniqBy from '../../utils/expodash/uniqBy';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { createUpdateChannelOnAppAsync } from '../channel/create';
|
||||
import { createUpdateBranchOnAppAsync } from './create';
|
||||
import { listBranchesAsync } from './list';
|
||||
import { viewUpdateBranchAsync } from './view';
|
||||
@@ -70,6 +71,34 @@ async function getUpdateGroupAsync({
|
||||
return updatesByGroup;
|
||||
}
|
||||
|
||||
async function ensureChannelExistsAsync({
|
||||
appId,
|
||||
branchId,
|
||||
channelName,
|
||||
}: {
|
||||
appId: string;
|
||||
branchId: string;
|
||||
channelName: string;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
await createUpdateChannelOnAppAsync({
|
||||
appId,
|
||||
channelName,
|
||||
branchId,
|
||||
});
|
||||
Log.withTick(
|
||||
`Created a channel: ${chalk.bold(channelName)} pointed at branch: ${chalk.bold(channelName)}.`
|
||||
);
|
||||
} catch (e: any) {
|
||||
const isIgnorableError =
|
||||
e.graphQLErrors?.length === 1 &&
|
||||
e.graphQLErrors[0].extensions.errorCode === 'CHANNEL_ALREADY_EXISTS';
|
||||
if (!isIgnorableError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureBranchExistsAsync({
|
||||
appId,
|
||||
name: branchName,
|
||||
@@ -90,10 +119,13 @@ async function ensureBranchExistsAsync({
|
||||
const updateBranch = app?.byId.updateBranchByName;
|
||||
if (updateBranch) {
|
||||
const { id, updates } = updateBranch;
|
||||
await ensureChannelExistsAsync({ appId, branchId: id, channelName: branchName });
|
||||
return { id, updates };
|
||||
}
|
||||
|
||||
const newUpdateBranch = await createUpdateBranchOnAppAsync({ appId, name: branchName });
|
||||
Log.withTick(`Created branch: ${chalk.bold(branchName)}`);
|
||||
await ensureChannelExistsAsync({ appId, branchId: newUpdateBranch.id, channelName: branchName });
|
||||
return { id: newUpdateBranch.id, updates: [] };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile, EasJsonReader } from '@expo/eas-json';
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { flags } from '@oclif/command';
|
||||
import { error } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
@@ -32,10 +32,6 @@ import {
|
||||
selectRequestedPlatformAsync,
|
||||
toPlatforms,
|
||||
} from '../../platform';
|
||||
import {
|
||||
EAS_UNAVAILABLE_MESSAGE,
|
||||
isEasEnabledForProjectAsync,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { selectAsync } from '../../prompts';
|
||||
@@ -152,13 +148,11 @@ export default class Build extends EasCommand {
|
||||
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
|
||||
|
||||
const platforms = toPlatforms(requestedPlatform);
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const buildProfiles = await getProfilesAsync({
|
||||
type: 'build',
|
||||
projectDir,
|
||||
platforms,
|
||||
profileName: flags.profile,
|
||||
async readProfileAsync(platform, profileName) {
|
||||
return await easJsonReader.readBuildProfileAsync(platform, profileName);
|
||||
},
|
||||
});
|
||||
|
||||
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
@@ -169,7 +163,7 @@ export default class Build extends EasCommand {
|
||||
|
||||
const startedBuilds: {
|
||||
build: BuildFragment;
|
||||
buildProfile: ProfileData<BuildProfile>;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
}[] = [];
|
||||
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
|
||||
|
||||
@@ -288,7 +282,7 @@ export default class Build extends EasCommand {
|
||||
projectDir: string;
|
||||
flags: BuildFlags;
|
||||
moreBuilds: boolean;
|
||||
buildProfile: ProfileData<BuildProfile>;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
|
||||
const buildCtx = await createBuildContextAsync({
|
||||
buildProfileName: buildProfile.profileName,
|
||||
@@ -316,10 +310,6 @@ export default class Build extends EasCommand {
|
||||
this.metroConfigValidated = true;
|
||||
}
|
||||
|
||||
if (!buildCtx.local && !(await isEasEnabledForProjectAsync(buildCtx.projectId))) {
|
||||
error(EAS_UNAVAILABLE_MESSAGE, { exit: 1 });
|
||||
}
|
||||
|
||||
const build = await this.startBuildAsync(buildCtx);
|
||||
return {
|
||||
build,
|
||||
@@ -350,7 +340,7 @@ export default class Build extends EasCommand {
|
||||
flags: BuildFlags;
|
||||
moreBuilds: boolean;
|
||||
projectDir: string;
|
||||
buildProfile: ProfileData<BuildProfile>;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
}): Promise<SubmissionFragment> {
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const platform = toPlatform(build.platform);
|
||||
|
||||
@@ -20,7 +20,7 @@ import { promptAsync } from '../../prompts';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { createUpdateBranchOnAppAsync } from '../branch/create';
|
||||
|
||||
async function createUpdateChannelOnAppAsync({
|
||||
export async function createUpdateChannelOnAppAsync({
|
||||
appId,
|
||||
channelName,
|
||||
branchId,
|
||||
|
||||
@@ -9,10 +9,6 @@ import {
|
||||
EnvironmentSecretsQuery,
|
||||
} from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
isEasEnabledForProjectAsync,
|
||||
warnEasUnavailable,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
@@ -57,12 +53,6 @@ export default class EnvironmentSecretCreate extends EasCommand {
|
||||
const { slug } = exp;
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!scope) {
|
||||
const validationMessage = 'Secret scope may not be empty.';
|
||||
|
||||
|
||||
@@ -10,10 +10,6 @@ import {
|
||||
EnvironmentSecretsQuery,
|
||||
} from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
isEasEnabledForProjectAsync,
|
||||
warnEasUnavailable,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
@@ -37,12 +33,6 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secret:list')}`;
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectAccountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
let {
|
||||
flags: { id },
|
||||
} = this.parse(EnvironmentSecretDelete);
|
||||
|
||||
@@ -6,10 +6,6 @@ import dateFormat from 'dateformat';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EnvironmentSecretsQuery } from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
isEasEnabledForProjectAsync,
|
||||
warnEasUnavailable,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
@@ -25,12 +21,6 @@ export default class EnvironmentSecretList extends EasCommand {
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectAccountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!projectDir) {
|
||||
throw new Error("Please run this command inside your project's directory");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { flags } from '@oclif/command';
|
||||
import { error } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
@@ -15,7 +14,6 @@ import {
|
||||
selectRequestedPlatformAsync,
|
||||
toPlatforms,
|
||||
} from '../platform';
|
||||
import { isEasEnabledForProjectAsync, warnEasUnavailable } from '../project/isEasEnabledForProject';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { SubmitArchiveFlags, createSubmissionContextAsync } from '../submit/context';
|
||||
import { submitAsync, waitToCompleteAsync } from '../submit/submit';
|
||||
@@ -96,20 +94,12 @@ See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const platforms = toPlatforms(flags.requestedPlatform);
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const submissionProfiles = await getProfilesAsync({
|
||||
type: 'submit',
|
||||
projectDir,
|
||||
platforms,
|
||||
profileName: flags.profile,
|
||||
async readProfileAsync(platform, profileName) {
|
||||
return await easJsonReader.readSubmitProfileAsync(platform, profileName);
|
||||
},
|
||||
});
|
||||
|
||||
const submissions: SubmissionFragment[] = [];
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { IOSConfig, compileModsAsync } from '@expo/config-plugins';
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { JSONObject } from '@expo/json-file';
|
||||
import plist from '@expo/plist';
|
||||
import { getPrebuildConfig } from '@expo/prebuild-config';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { readPlistAsync } from '../../../utils/plist';
|
||||
|
||||
export async function getManagedEntitlementsJsonAsync(
|
||||
projectDir: string,
|
||||
@@ -43,12 +43,11 @@ export async function resolveEntitlementsJsonAsync(
|
||||
}
|
||||
|
||||
async function getEntitlementsJsonAsync(projectDir: string): Promise<JSONObject | null> {
|
||||
try {
|
||||
const entitlementsPath = IOSConfig.Paths.getEntitlementsPath(projectDir);
|
||||
if (entitlementsPath) {
|
||||
const entitlementsContents = await fs.readFile(entitlementsPath, 'utf8');
|
||||
return plist.parse(entitlementsContents);
|
||||
}
|
||||
} catch {}
|
||||
return null;
|
||||
const entitlementsPath = IOSConfig.Paths.getEntitlementsPath(projectDir);
|
||||
if (entitlementsPath) {
|
||||
const plist = await readPlistAsync(entitlementsPath);
|
||||
return plist ? (plist as unknown as JSONObject) : null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ describe(getApplicationIdAsync, () => {
|
||||
'/app'
|
||||
);
|
||||
await expect(getApplicationIdAsync('/app', {} as any, undefined)).rejects.toThrowError(
|
||||
/Could not read application id/
|
||||
/Failed to find 'build.gradle' /
|
||||
);
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ describe(getApplicationIdAsync, () => {
|
||||
|
||||
await expect(
|
||||
getApplicationIdAsync('/app', {} as any, { moduleName: 'app' })
|
||||
).rejects.toThrowError(/Could not read application id/);
|
||||
).rejects.toThrowError(/Could not read applicationId/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,7 +112,7 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
|
||||
);
|
||||
await expect(
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync('/app', {} as any)
|
||||
).rejects.toThrowError(/we can't update this file programatically/);
|
||||
).rejects.toThrowError(/we can't update this file programmatically/);
|
||||
});
|
||||
it('prompts for the Android package if using app.json', async () => {
|
||||
vol.fromJSON(
|
||||
|
||||
@@ -15,7 +15,7 @@ import { resolveWorkflowAsync } from '../workflow';
|
||||
import { GradleBuildContext } from './gradle';
|
||||
import * as gradleUtils from './gradleUtils';
|
||||
|
||||
const INVALID_APPLICATION_ID_MESSAGE = `Invalid format of Android applicationId. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.`;
|
||||
export const INVALID_APPLICATION_ID_MESSAGE = `Invalid format of Android applicationId. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.`;
|
||||
|
||||
export async function ensureApplicationIdIsDefinedForManagedProjectAsync(
|
||||
projectDir: string,
|
||||
@@ -33,6 +33,51 @@ export async function ensureApplicationIdIsDefinedForManagedProjectAsync(
|
||||
}
|
||||
}
|
||||
|
||||
export class AmbiguousApplicationIdError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message ?? 'Could not resolve applicationId.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getApplicationIdFromBareAsync(
|
||||
projectDir: string,
|
||||
gradleContext?: GradleBuildContext
|
||||
): Promise<string> {
|
||||
const errorMessage = 'Could not read applicationId from Android project.';
|
||||
|
||||
if (gradleContext) {
|
||||
const buildGradle = await gradleUtils.getAppBuildGradleAsync(projectDir);
|
||||
const applicationIdSuffix = gradleUtils.resolveConfigValue(
|
||||
buildGradle,
|
||||
'applicationIdSuffix',
|
||||
gradleContext.flavor
|
||||
);
|
||||
if (applicationIdSuffix) {
|
||||
throw new Error('"applicationIdSuffix" in app/build.gradle is not supported.');
|
||||
}
|
||||
const applicationId = gradleUtils.resolveConfigValue(
|
||||
buildGradle,
|
||||
'applicationId',
|
||||
gradleContext.flavor
|
||||
);
|
||||
return nullthrows(applicationId, errorMessage);
|
||||
} else {
|
||||
// should return value only if productFlavors are not used
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
const buildGradle = await fs.readFile(buildGradlePath, 'utf8');
|
||||
const matchResult = buildGradle.match(/applicationId ['"](.*)['"]/);
|
||||
if (buildGradle.match(/applicationIdSuffix/)) {
|
||||
throw new Error('"applicationIdSuffix" in app/build.gradle is not supported.');
|
||||
}
|
||||
if (buildGradle.match(/productFlavors/)) {
|
||||
throw new AmbiguousApplicationIdError(
|
||||
'Failed to autodetect applicationId in multi-flavor project.'
|
||||
);
|
||||
}
|
||||
return nullthrows(matchResult?.[1], errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getApplicationIdAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
@@ -42,33 +87,7 @@ export async function getApplicationIdAsync(
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(projectDir, exp);
|
||||
|
||||
const errorMessage = 'Could not read application id from Android project.';
|
||||
if (gradleContext) {
|
||||
const buildGradle = await gradleUtils.getAppBuildGradleAsync(projectDir);
|
||||
const applicationId = gradleUtils.resolveConfigValue(
|
||||
buildGradle,
|
||||
'applicationId',
|
||||
gradleContext.flavor
|
||||
);
|
||||
return nullthrows(applicationId, errorMessage);
|
||||
} else {
|
||||
// fallback to best effort approach, this logic can be dropped when we start supporting
|
||||
// modules different than 'app' and 'flavorDimensions'
|
||||
let buildGradlePath = null;
|
||||
try {
|
||||
buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
} catch {}
|
||||
if (!buildGradlePath || !(await fs.pathExists(buildGradlePath))) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
const buildGradle = await fs.readFile(buildGradlePath, 'utf8');
|
||||
const matchResult = buildGradle.match(/applicationId ['"](.*)['"]/);
|
||||
const applicationId = nullthrows(matchResult?.[1], errorMessage);
|
||||
|
||||
Log.warn(`Unable to detect applicationId`);
|
||||
Log.warn(`Falling back to best effort approach, using applicationId ${applicationId}`);
|
||||
return applicationId;
|
||||
}
|
||||
return getApplicationIdFromBareAsync(projectDir, gradleContext);
|
||||
} else {
|
||||
const applicationId = AndroidConfig.Package.getPackage(exp);
|
||||
if (!applicationId || !isApplicationIdValid(applicationId)) {
|
||||
@@ -91,7 +110,7 @@ async function configureApplicationIdAsync(projectDir: string, exp: ExpoConfig):
|
||||
// we can't automatically update app.config.js
|
||||
if (paths.dynamicConfigPath) {
|
||||
throw new Error(
|
||||
`"android.package" is not defined in your app.config.js and we can't update this file programatically. Add the value on your own and run this command again.`
|
||||
`"android.package" is not defined in your app.config.js and we can't update this file programmatically. Add the value on your own and run this command again.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,7 +144,7 @@ async function configureApplicationIdAsync(projectDir: string, exp: ExpoConfig):
|
||||
return packageName;
|
||||
}
|
||||
|
||||
function isApplicationIdValid(applicationId: string): boolean {
|
||||
export function isApplicationIdValid(applicationId: string): boolean {
|
||||
return /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(applicationId);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function resolveGradleBuildContextAsync(
|
||||
parsedGradleCommand.moduleName !== gradleUtils.DEFAULT_MODULE_NAME
|
||||
) {
|
||||
Log.warn(
|
||||
`Building modules different than "${gradleUtils.DEFAULT_MODULE_NAME}" might result in unexpected behavior`
|
||||
`Building modules different than "${gradleUtils.DEFAULT_MODULE_NAME}" might result in unexpected behavior.`
|
||||
);
|
||||
}
|
||||
return {
|
||||
@@ -38,7 +38,8 @@ export async function resolveGradleBuildContextAsync(
|
||||
return { moduleName: gradleUtils.DEFAULT_MODULE_NAME };
|
||||
}
|
||||
} catch (err: any) {
|
||||
Log.warn(`Unable to read project config from app/build.gradle: ${err.message}`);
|
||||
Log.warn(`Unable to read gradle project config: ${err.message}.`);
|
||||
Log.warn('Values from app/build.gradle might be resolved incorrectly.');
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -12,6 +12,7 @@ interface GradleCommand {
|
||||
|
||||
interface Config {
|
||||
applicationId?: string;
|
||||
applicationIdSuffix?: string; // unsupported value
|
||||
versionCode?: string;
|
||||
versionName?: string;
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
|
||||
);
|
||||
await expect(
|
||||
ensureBundleIdentifierIsDefinedForManagedProjectAsync('/app', {} as any)
|
||||
).rejects.toThrowError(/we can't update this file programatically/);
|
||||
).rejects.toThrowError(/we can't update this file programmatically/);
|
||||
});
|
||||
it('prompts for the bundle identifier if using app.json', async () => {
|
||||
vol.fromJSON(
|
||||
|
||||
@@ -12,7 +12,7 @@ import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { getProjectConfigDescription, getUsername } from '../projectUtils';
|
||||
import { resolveWorkflowAsync } from '../workflow';
|
||||
|
||||
const INVALID_BUNDLE_IDENTIFIER_MESSAGE = `Invalid format of iOS bundle identifier. Only alphanumeric characters, '.' and '-' are allowed, and each '.' must be followed by a letter.`;
|
||||
export const INVALID_BUNDLE_IDENTIFIER_MESSAGE = `Invalid format of iOS bundle identifier. Only alphanumeric characters, '.' and '-' are allowed, and each '.' must be followed by a letter.`;
|
||||
|
||||
export async function ensureBundleIdentifierIsDefinedForManagedProjectAsync(
|
||||
projectDir: string,
|
||||
@@ -28,22 +28,39 @@ export async function ensureBundleIdentifierIsDefinedForManagedProjectAsync(
|
||||
}
|
||||
}
|
||||
|
||||
export class AmbiguousBundleIdentifierError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message ?? 'Could not resolve bundle identifier.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBundleIdentifierAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
{ targetName, buildConfiguration }: { targetName?: string; buildConfiguration?: string } = {}
|
||||
xcodeContext?: { targetName?: string; buildConfiguration?: string }
|
||||
): Promise<string> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(projectDir, exp);
|
||||
|
||||
const bundleIdentifier = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(projectDir, {
|
||||
targetName,
|
||||
buildConfiguration,
|
||||
});
|
||||
const xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
|
||||
const isMultiScheme = IOSConfig.BuildScheme.getSchemesFromXcodeproj(projectDir).length > 1;
|
||||
const isMultiTarget =
|
||||
IOSConfig.Target.getNativeTargets(xcodeProject).filter(([, target]) =>
|
||||
IOSConfig.Target.isTargetOfType(target, IOSConfig.Target.TargetType.APPLICATION)
|
||||
).length > 1;
|
||||
if (!xcodeContext && isMultiScheme && isMultiTarget) {
|
||||
throw new AmbiguousBundleIdentifierError(
|
||||
"Multiple schemes and targets found in Xcode project, bundle identifier couldn't be resolved."
|
||||
);
|
||||
}
|
||||
const bundleIdentifier = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(
|
||||
projectDir,
|
||||
xcodeContext ?? {}
|
||||
);
|
||||
const buildConfigurationDesc =
|
||||
targetName && buildConfiguration
|
||||
? ` (target = ${targetName}, build configuration = ${buildConfiguration})`
|
||||
xcodeContext?.targetName && xcodeContext?.buildConfiguration
|
||||
? ` (target = ${xcodeContext.targetName}, build configuration = ${xcodeContext.buildConfiguration})`
|
||||
: '';
|
||||
assert(
|
||||
bundleIdentifier,
|
||||
@@ -58,13 +75,17 @@ export async function getBundleIdentifierAsync(
|
||||
} else {
|
||||
// TODO: the following asserts are only temporary until we support app extensions in managed projects
|
||||
assert(
|
||||
!targetName || targetName === IOSConfig.XcodeUtils.sanitizedName(exp.name),
|
||||
'targetName cannot be set to an arbitrary value for managed projects'
|
||||
!xcodeContext?.targetName ||
|
||||
xcodeContext?.targetName === IOSConfig.XcodeUtils.sanitizedName(exp.name),
|
||||
'targetName cannot be set to an arbitrary value for managed projects.'
|
||||
);
|
||||
assert(!buildConfiguration, 'buildConfiguration cannot be passed for managed projects');
|
||||
const bundleIdentifer = IOSConfig.BundleIdentifier.getBundleIdentifier(exp);
|
||||
if (!bundleIdentifer || !isBundleIdentifierValid(bundleIdentifer)) {
|
||||
if (bundleIdentifer) {
|
||||
assert(
|
||||
!xcodeContext?.buildConfiguration,
|
||||
'buildConfiguration cannot be passed for managed projects.'
|
||||
);
|
||||
const bundleIdentifier = IOSConfig.BundleIdentifier.getBundleIdentifier(exp);
|
||||
if (!bundleIdentifier || !isBundleIdentifierValid(bundleIdentifier)) {
|
||||
if (bundleIdentifier) {
|
||||
Log.warn(INVALID_BUNDLE_IDENTIFIER_MESSAGE);
|
||||
}
|
||||
throw new Error(
|
||||
@@ -73,7 +94,7 @@ export async function getBundleIdentifierAsync(
|
||||
)} and run this command again.`
|
||||
);
|
||||
} else {
|
||||
return bundleIdentifer;
|
||||
return bundleIdentifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,7 +107,7 @@ async function configureBundleIdentifierAsync(
|
||||
// we can't automatically update app.config.js
|
||||
if (paths.dynamicConfigPath) {
|
||||
throw new Error(
|
||||
`"ios.bundleIdentifier" is not defined in your app.config.js and we can't update this file programatically. Add the value on your own and run this command again.`
|
||||
`"ios.bundleIdentifier" is not defined in your app.config.js and we can't update this file programmatically. Add the value on your own and run this command again.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -120,7 +141,7 @@ async function configureBundleIdentifierAsync(
|
||||
return bundleIdentifier;
|
||||
}
|
||||
|
||||
function isBundleIdentifierValid(bundleIdentifier: string): boolean {
|
||||
export function isBundleIdentifierValid(bundleIdentifier: string): boolean {
|
||||
return /^[a-zA-Z0-9-.]+$/.test(bundleIdentifier);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { apiClient } from '../api';
|
||||
import Log from '../log';
|
||||
|
||||
/**
|
||||
* Checks if the project is allowed for using EAS services.
|
||||
* THIS IS A TEMPORARY STEP NEEDED FOR OPEN PREVIEW
|
||||
* @returns boolean
|
||||
*/
|
||||
export async function isEasEnabledForProjectAsync(projectId: string): Promise<boolean> {
|
||||
try {
|
||||
const {
|
||||
data: { enabled },
|
||||
} = await apiClient.get(`projects/${projectId}/eas-enabled`).json();
|
||||
return enabled;
|
||||
} catch (error: any) {
|
||||
if (error.response?.statusCode === 404) {
|
||||
return true;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const EAS_UNAVAILABLE_MESSAGE = `Your account doesn't have access to Expo Application Services (EAS) features. Enroll in EAS to give it a try: ${chalk.underline(
|
||||
'https://expo.dev/eas'
|
||||
)}`;
|
||||
|
||||
export function warnEasUnavailable(): void {
|
||||
Log.warn(EAS_UNAVAILABLE_MESSAGE);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { promptAsync } from '../../prompts';
|
||||
|
||||
export enum AndroidPackageSourceType {
|
||||
userDefined,
|
||||
prompt,
|
||||
}
|
||||
|
||||
interface AndroidPackageSourceBase {
|
||||
sourceType: AndroidPackageSourceType;
|
||||
}
|
||||
|
||||
interface AndroidPackageUserDefinedSource extends AndroidPackageSourceBase {
|
||||
sourceType: AndroidPackageSourceType.userDefined;
|
||||
androidPackage: string;
|
||||
}
|
||||
|
||||
interface AndroidPackagePromptSource extends AndroidPackageSourceBase {
|
||||
sourceType: AndroidPackageSourceType.prompt;
|
||||
}
|
||||
|
||||
export type AndroidPackageSource = AndroidPackageUserDefinedSource | AndroidPackagePromptSource;
|
||||
|
||||
export async function getAndroidPackageAsync(source: AndroidPackageSource): Promise<string> {
|
||||
if (source.sourceType === AndroidPackageSourceType.userDefined) {
|
||||
return source.androidPackage;
|
||||
} else if (source.sourceType === AndroidPackageSourceType.prompt) {
|
||||
const { androidPackage } = await promptAsync({
|
||||
name: 'androidPackage',
|
||||
message: 'Android package name:',
|
||||
type: 'text',
|
||||
validate: (val: string) => val !== '' || 'Package name cannot be empty!',
|
||||
});
|
||||
return androidPackage;
|
||||
} else {
|
||||
throw new Error('This should never happen');
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,14 @@ import {
|
||||
SubmissionFragment,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { getApplicationIdAsync } from '../../project/android/applicationId';
|
||||
import * as gradleUtils from '../../project/android/gradleUtils';
|
||||
import {
|
||||
AmbiguousApplicationIdError,
|
||||
getApplicationIdAsync,
|
||||
} from '../../project/android/applicationId';
|
||||
import capitalize from '../../utils/expodash/capitalize';
|
||||
import { ArchiveSource } from '../ArchiveSource';
|
||||
import { resolveArchiveSource } from '../commons';
|
||||
import { SubmissionContext } from '../context';
|
||||
import { AndroidPackageSource, AndroidPackageSourceType } from './AndroidPackageSource';
|
||||
import AndroidSubmitter, { AndroidSubmissionOptions } from './AndroidSubmitter';
|
||||
import { ServiceAccountSource, ServiceAccountSourceType } from './ServiceAccountSource';
|
||||
|
||||
@@ -29,19 +30,12 @@ export default class AndroidSubmitCommand {
|
||||
}
|
||||
|
||||
private async getAndroidSubmissionOptionsAsync(): Promise<AndroidSubmissionOptions> {
|
||||
const androidPackageSource = await this.resolveAndroidPackageSourceAsync();
|
||||
const track = this.resolveTrack();
|
||||
const releaseStatus = this.resolveReleaseStatus();
|
||||
const archiveSource = this.resolveArchiveSource();
|
||||
const serviceAccountSource = this.resolveServiceAccountSource();
|
||||
const serviceAccountSource = await this.resolveServiceAccountSourceAsync();
|
||||
|
||||
const errored = [
|
||||
androidPackageSource,
|
||||
track,
|
||||
releaseStatus,
|
||||
archiveSource,
|
||||
serviceAccountSource,
|
||||
].filter(r => !r.ok);
|
||||
const errored = [track, releaseStatus, archiveSource, serviceAccountSource].filter(r => !r.ok);
|
||||
if (errored.length > 0) {
|
||||
const message = errored.map(err => err.reason?.message).join('\n');
|
||||
Log.error(message);
|
||||
@@ -50,7 +44,6 @@ export default class AndroidSubmitCommand {
|
||||
|
||||
return {
|
||||
projectId: this.ctx.projectId,
|
||||
androidPackageSource: androidPackageSource.enforceValue(),
|
||||
track: track.enforceValue(),
|
||||
releaseStatus: releaseStatus.enforceValue(),
|
||||
archiveSource: archiveSource.enforceValue(),
|
||||
@@ -59,29 +52,19 @@ export default class AndroidSubmitCommand {
|
||||
};
|
||||
}
|
||||
|
||||
private async maybeGetAndroidPackageFromCurrentProjectAsync(): Promise<string | undefined> {
|
||||
private async maybeGetAndroidPackageFromCurrentProjectAsync(): Promise<Result<string | null>> {
|
||||
try {
|
||||
return await getApplicationIdAsync(this.ctx.projectDir, this.ctx.exp, {
|
||||
moduleName: gradleUtils.DEFAULT_MODULE_NAME,
|
||||
});
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveAndroidPackageSourceAsync(): Promise<Result<AndroidPackageSource>> {
|
||||
const androidPackage = await this.maybeGetAndroidPackageFromCurrentProjectAsync();
|
||||
if (androidPackage) {
|
||||
return result({
|
||||
sourceType: AndroidPackageSourceType.userDefined,
|
||||
androidPackage,
|
||||
});
|
||||
} else if (this.ctx.nonInteractive) {
|
||||
return result(new Error("Couldn't resolve the Android package."));
|
||||
} else {
|
||||
return result({
|
||||
sourceType: AndroidPackageSourceType.prompt,
|
||||
});
|
||||
return result(await getApplicationIdAsync(this.ctx.projectDir, this.ctx.exp));
|
||||
} catch (error: any) {
|
||||
if (error instanceof AmbiguousApplicationIdError) {
|
||||
Log.warn(
|
||||
'"applicationId" is ambiguous, specify it via "applicationId" field in the submit profile in the eas.json.'
|
||||
);
|
||||
return result(null);
|
||||
}
|
||||
return result(
|
||||
new Error(`Failed to resolve applicationId in Android project: ${error.message}.`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +120,7 @@ export default class AndroidSubmitCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private resolveServiceAccountSource(): Result<ServiceAccountSource> {
|
||||
private async resolveServiceAccountSourceAsync(): Promise<Result<ServiceAccountSource>> {
|
||||
const { serviceAccountKeyPath } = this.ctx.profile;
|
||||
if (serviceAccountKeyPath) {
|
||||
return result({
|
||||
@@ -145,8 +128,21 @@ export default class AndroidSubmitCommand {
|
||||
path: serviceAccountKeyPath,
|
||||
});
|
||||
}
|
||||
let androidApplicationIdentifier: string | undefined = this.ctx.profile.applicationId;
|
||||
if (!androidApplicationIdentifier) {
|
||||
const androidApplicationIdentifierResult =
|
||||
await this.maybeGetAndroidPackageFromCurrentProjectAsync();
|
||||
if (!androidApplicationIdentifierResult.ok) {
|
||||
return result(androidApplicationIdentifierResult.reason);
|
||||
}
|
||||
const androidApplicationIdentifierValue = androidApplicationIdentifierResult.enforceValue();
|
||||
if (androidApplicationIdentifierValue) {
|
||||
androidApplicationIdentifier = androidApplicationIdentifierValue;
|
||||
}
|
||||
}
|
||||
return result({
|
||||
sourceType: ServiceAccountSourceType.credentialsService,
|
||||
androidApplicationIdentifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -18,19 +18,18 @@ import {
|
||||
formatArchiveSourceSummary,
|
||||
printSummary,
|
||||
} from '../utils/summary';
|
||||
import { AndroidPackageSource, getAndroidPackageAsync } from './AndroidPackageSource';
|
||||
import {
|
||||
ServiceAccountKeyResult,
|
||||
ServiceAccountSource,
|
||||
getServiceAccountKeyResultAsync,
|
||||
} from './ServiceAccountSource';
|
||||
|
||||
export interface AndroidSubmissionOptions
|
||||
extends Pick<
|
||||
AndroidSubmissionConfigInput,
|
||||
'track' | 'releaseStatus' | 'changesNotSentForReview'
|
||||
> {
|
||||
projectId: string;
|
||||
androidPackageSource: AndroidPackageSource;
|
||||
archiveSource: ArchiveSource;
|
||||
serviceAccountSource: ServiceAccountSource;
|
||||
}
|
||||
@@ -51,12 +50,7 @@ export default class AndroidSubmitter extends BaseSubmitter<
|
||||
archive: async () => await getArchiveAsync(this.options.archiveSource),
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
serviceAccountKeyResult: async () => {
|
||||
const androidPackage = await getAndroidPackageAsync(this.options.androidPackageSource);
|
||||
return await getServiceAccountKeyResultAsync(
|
||||
this.ctx,
|
||||
this.options.serviceAccountSource,
|
||||
androidPackage
|
||||
);
|
||||
return await getServiceAccountKeyResultAsync(this.ctx, this.options.serviceAccountSource);
|
||||
},
|
||||
};
|
||||
const sourceOptionsAnalytics = {
|
||||
|
||||
@@ -6,6 +6,10 @@ import nullthrows from 'nullthrows';
|
||||
import { SetUpGoogleServiceAccountKey } from '../../credentials/android/actions/SetUpGoogleServiceAccountKey';
|
||||
import { readAndValidateServiceAccountKey } from '../../credentials/android/utils/googleServiceAccountKey';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import {
|
||||
INVALID_APPLICATION_ID_MESSAGE,
|
||||
isApplicationIdValid,
|
||||
} from '../../project/android/applicationId';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { SubmissionContext } from '../context';
|
||||
@@ -32,6 +36,7 @@ interface ServiceAccountPromptSource extends ServiceAccountSourceBase {
|
||||
|
||||
export interface ServiceAccountCredentialsServiceSource extends ServiceAccountSourceBase {
|
||||
sourceType: ServiceAccountSourceType.credentialsService;
|
||||
androidApplicationIdentifier?: string;
|
||||
}
|
||||
|
||||
export type ServiceAccountKeyResult = {
|
||||
@@ -60,11 +65,10 @@ export type ServiceAccountSource =
|
||||
|
||||
export async function getServiceAccountKeyResultAsync(
|
||||
ctx: SubmissionContext<Platform.ANDROID>,
|
||||
source: ServiceAccountSource,
|
||||
androidApplicationIdentifier: string
|
||||
source: ServiceAccountSource
|
||||
): Promise<ServiceAccountKeyResult> {
|
||||
if (source.sourceType === ServiceAccountSourceType.credentialsService) {
|
||||
return await getServiceAccountFromCredentialsServiceAsync(ctx, androidApplicationIdentifier);
|
||||
return await getServiceAccountFromCredentialsServiceAsync(ctx, source);
|
||||
} else {
|
||||
return await getServiceAccountLocallyAsync(source);
|
||||
}
|
||||
@@ -97,9 +101,19 @@ export async function getServiceAccountKeyPathAsync(source: ServiceAccountSource
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForApplicationIdAsync(): Promise<string> {
|
||||
const { androidPackage } = await promptAsync({
|
||||
name: 'androidPackage',
|
||||
message: 'Android package name:',
|
||||
type: 'text',
|
||||
validate: value => (isApplicationIdValid(value) ? true : INVALID_APPLICATION_ID_MESSAGE),
|
||||
});
|
||||
return androidPackage;
|
||||
}
|
||||
|
||||
export async function getServiceAccountFromCredentialsServiceAsync(
|
||||
ctx: SubmissionContext<Platform.ANDROID>,
|
||||
androidApplicationIdentifier: string
|
||||
source: ServiceAccountCredentialsServiceSource
|
||||
): Promise<ServiceAccountKeyResult> {
|
||||
const appLookupParams = {
|
||||
account: nullthrows(
|
||||
@@ -107,8 +121,12 @@ export async function getServiceAccountFromCredentialsServiceAsync(
|
||||
`You do not have access to account: ${ctx.accountName}`
|
||||
),
|
||||
projectName: ctx.projectName,
|
||||
androidApplicationIdentifier,
|
||||
androidApplicationIdentifier:
|
||||
source.androidApplicationIdentifier ?? (await promptForApplicationIdAsync()),
|
||||
};
|
||||
Log.log(
|
||||
`Looking up credentials configuration for ${appLookupParams.androidApplicationIdentifier}...`
|
||||
);
|
||||
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
|
||||
const androidAppCredentials = await setupGoogleServiceAccountKeyAction.runAsync(
|
||||
ctx.credentialsCtx
|
||||
|
||||
@@ -152,7 +152,7 @@ describe(getServiceAccountKeyResultAsync, () => {
|
||||
sourceType: ServiceAccountSourceType.path,
|
||||
path: '/project_dir/subdir/service-account.json',
|
||||
};
|
||||
const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, source, 'test.sdf');
|
||||
const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, source);
|
||||
expect(serviceAccountResult).toMatchObject({
|
||||
result: {
|
||||
googleServiceAccountKeyJson: mockDetectableServiceAccountJson,
|
||||
@@ -186,7 +186,7 @@ describe(getServiceAccountKeyResultAsync, () => {
|
||||
const source: ServiceAccountSource = {
|
||||
sourceType: ServiceAccountSourceType.prompt,
|
||||
};
|
||||
const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, source, 'test.sdf');
|
||||
const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, source);
|
||||
expect(serviceAccountResult).toMatchObject({
|
||||
result: {
|
||||
googleServiceAccountKeyJson: mockDetectableServiceAccountJson,
|
||||
@@ -214,13 +214,10 @@ describe(getServiceAccountKeyResultAsync, () => {
|
||||
},
|
||||
nonInteractive: true,
|
||||
});
|
||||
const serviceAccountResult = await getServiceAccountKeyResultAsync(
|
||||
ctx,
|
||||
{
|
||||
sourceType: ServiceAccountSourceType.credentialsService,
|
||||
},
|
||||
'test.application.identiifer'
|
||||
);
|
||||
const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, {
|
||||
sourceType: ServiceAccountSourceType.credentialsService,
|
||||
androidApplicationIdentifier: 'test.application.identiifer',
|
||||
});
|
||||
expect(serviceAccountResult).toMatchObject({
|
||||
result: { googleServiceAccountKeyId: 'test-id' },
|
||||
summary: { source: 'EAS servers', email: '[email protected]' },
|
||||
|
||||
@@ -3,7 +3,11 @@ import nullthrows from 'nullthrows';
|
||||
|
||||
import { SetUpSubmissionCredentials } from '../../credentials/ios/actions/SetUpSubmissionCredentials';
|
||||
import Log from '../../log';
|
||||
import { getBundleIdentifierAsync } from '../../project/ios/bundleIdentifier';
|
||||
import {
|
||||
INVALID_BUNDLE_IDENTIFIER_MESSAGE,
|
||||
isBundleIdentifierValid,
|
||||
} from '../../project/ios/bundleIdentifier';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { SubmissionContext } from '../context';
|
||||
import {
|
||||
@@ -16,15 +20,29 @@ import { AscApiKeyResult } from './AscApiKeySource';
|
||||
* The Credentials Service will either return an ASC API Key or an App Specific Password
|
||||
* When we no longer support the App Specific Password user prompt, refactor this into the AscApiKeySource
|
||||
*/
|
||||
export const CREDENTIALS_SERVICE_SOURCE = 'CREDENTIALS_SERVICE_SOURCE';
|
||||
export type CredentialsServiceSource = typeof CREDENTIALS_SERVICE_SOURCE;
|
||||
export const CREDENTIALS_SERVICE_SOURCE = 'CREDENTIALS_SERVICE_SOURCE' as const;
|
||||
export interface CredentialsServiceSource {
|
||||
sourceType: typeof CREDENTIALS_SERVICE_SOURCE;
|
||||
bundleIdentifier?: string;
|
||||
}
|
||||
|
||||
async function promptForBundleIdentifierAsync(): Promise<string> {
|
||||
const { bundleIdentifier } = await promptAsync({
|
||||
name: 'bundleIdentifier',
|
||||
message: 'Bundle identifier:',
|
||||
type: 'text',
|
||||
validate: value => (isBundleIdentifierValid(value) ? true : INVALID_BUNDLE_IDENTIFIER_MESSAGE),
|
||||
});
|
||||
return bundleIdentifier;
|
||||
}
|
||||
|
||||
export async function getFromCredentialsServiceAsync(
|
||||
ctx: SubmissionContext<Platform.IOS>
|
||||
ctx: SubmissionContext<Platform.IOS>,
|
||||
source: CredentialsServiceSource
|
||||
): Promise<
|
||||
{ appSpecificPassword: AppSpecificPasswordCredentials } | { ascApiKeyResult: AscApiKeyResult }
|
||||
> {
|
||||
const bundleIdentifier = await getBundleIdentifierAsync(ctx.projectDir, ctx.exp);
|
||||
const bundleIdentifier = source.bundleIdentifier ?? (await promptForBundleIdentifierAsync());
|
||||
Log.log(`Looking up credentials configuration for ${bundleIdentifier}...`);
|
||||
|
||||
const appLookupParams = {
|
||||
|
||||
@@ -7,6 +7,10 @@ import wrapAnsi from 'wrap-ansi';
|
||||
import { MissingCredentialsError } from '../../credentials/errors';
|
||||
import { SubmissionFragment } from '../../graphql/generated';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import {
|
||||
AmbiguousBundleIdentifierError,
|
||||
getBundleIdentifierAsync,
|
||||
} from '../../project/ios/bundleIdentifier';
|
||||
import { ArchiveSource } from '../ArchiveSource';
|
||||
import { resolveArchiveSource } from '../commons';
|
||||
import { SubmissionContext } from '../context';
|
||||
@@ -29,33 +33,9 @@ export default class IosSubmitCommand {
|
||||
return await submitter.submitAsync();
|
||||
}
|
||||
|
||||
private resolveCredentialSubmissionOptions():
|
||||
| { appSpecificPasswordSource: Result<AppSpecificPasswordSource> }
|
||||
| { ascApiKeySource: Result<AscApiKeySource> }
|
||||
| { credentialsServiceSource: Result<CredentialsServiceSource> } {
|
||||
const ascApiKeySource = this.resolveAscApiKeySource();
|
||||
const shouldSkipAscApiKeySource =
|
||||
!ascApiKeySource.ok && ascApiKeySource.enforceError() instanceof MissingCredentialsError;
|
||||
if (!shouldSkipAscApiKeySource) {
|
||||
return { ascApiKeySource };
|
||||
}
|
||||
|
||||
const appSpecificPasswordSource = this.resolveAppSpecificPasswordSource();
|
||||
const shouldSkipAppSpecificPasswordSource =
|
||||
!appSpecificPasswordSource.ok &&
|
||||
appSpecificPasswordSource.enforceError() instanceof MissingCredentialsError;
|
||||
if (!shouldSkipAppSpecificPasswordSource) {
|
||||
return { appSpecificPasswordSource: this.resolveAppSpecificPasswordSource() };
|
||||
}
|
||||
|
||||
return {
|
||||
credentialsServiceSource: result(CREDENTIALS_SERVICE_SOURCE),
|
||||
};
|
||||
}
|
||||
|
||||
private async resolveSubmissionOptionsAsync(): Promise<IosSubmissionOptions> {
|
||||
const archiveSource = this.resolveArchiveSource();
|
||||
const credentialsSource = this.resolveCredentialSubmissionOptions();
|
||||
const credentialsSource = await this.resolveCredentialSubmissionOptionsAsync();
|
||||
const maybeAppSpecificPasswordSource =
|
||||
'appSpecificPasswordSource' in credentialsSource
|
||||
? credentialsSource.appSpecificPasswordSource
|
||||
@@ -103,6 +83,63 @@ export default class IosSubmitCommand {
|
||||
};
|
||||
}
|
||||
|
||||
private async maybeGetIosBundleIdentifierAsync(): Promise<Result<string | null>> {
|
||||
try {
|
||||
return result(await getBundleIdentifierAsync(this.ctx.projectDir, this.ctx.exp));
|
||||
} catch (error: any) {
|
||||
if (error instanceof AmbiguousBundleIdentifierError) {
|
||||
Log.warn(
|
||||
'bundleIdentifier in the Xcode project is ambiguous, specify it via "bundleIdentifier" field in the submit profile in the eas.json.'
|
||||
);
|
||||
return result(null);
|
||||
}
|
||||
return result(
|
||||
new Error(`Failed to resolve bundleIdentifier in the Xcode project: ${error.message}.`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveCredentialSubmissionOptionsAsync(): Promise<
|
||||
| { appSpecificPasswordSource: Result<AppSpecificPasswordSource> }
|
||||
| { ascApiKeySource: Result<AscApiKeySource> }
|
||||
| { credentialsServiceSource: Result<CredentialsServiceSource> }
|
||||
> {
|
||||
const ascApiKeySource = this.resolveAscApiKeySource();
|
||||
const shouldSkipAscApiKeySource =
|
||||
!ascApiKeySource.ok && ascApiKeySource.enforceError() instanceof MissingCredentialsError;
|
||||
if (!shouldSkipAscApiKeySource) {
|
||||
return { ascApiKeySource };
|
||||
}
|
||||
|
||||
const appSpecificPasswordSource = this.resolveAppSpecificPasswordSource();
|
||||
const shouldSkipAppSpecificPasswordSource =
|
||||
!appSpecificPasswordSource.ok &&
|
||||
appSpecificPasswordSource.enforceError() instanceof MissingCredentialsError;
|
||||
if (!shouldSkipAppSpecificPasswordSource) {
|
||||
return { appSpecificPasswordSource: this.resolveAppSpecificPasswordSource() };
|
||||
}
|
||||
|
||||
let bundleIdentifier = this.ctx.profile.bundleIdentifier;
|
||||
if (!bundleIdentifier) {
|
||||
const bundleIdentifierResult = await this.maybeGetIosBundleIdentifierAsync();
|
||||
if (!bundleIdentifierResult.ok) {
|
||||
return {
|
||||
credentialsServiceSource: result(bundleIdentifierResult.reason),
|
||||
};
|
||||
}
|
||||
const bundleIdentifierValue = bundleIdentifierResult.enforceValue();
|
||||
if (bundleIdentifierValue) {
|
||||
bundleIdentifier = bundleIdentifierValue;
|
||||
}
|
||||
}
|
||||
return {
|
||||
credentialsServiceSource: result({
|
||||
sourceType: CREDENTIALS_SERVICE_SOURCE,
|
||||
bundleIdentifier,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private resolveAppSpecificPasswordSource(): Result<AppSpecificPasswordSource> {
|
||||
const envAppSpecificPassword = getenv.string('EXPO_APPLE_APP_SPECIFIC_PASSWORD', '');
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ export default class IosSubmitter extends BaseSubmitter<
|
||||
? await getAscApiKeyLocallyAsync(this.ctx, this.options.ascApiKeySource)
|
||||
: null;
|
||||
const maybeAscOrAspFromCredentialsService = this.options.credentialsServiceSource
|
||||
? await getFromCredentialsServiceAsync(this.ctx)
|
||||
? await getFromCredentialsServiceAsync(this.ctx, this.options.credentialsServiceSource)
|
||||
: null;
|
||||
return {
|
||||
...(maybeAppSpecificPassword ? { appSpecificPassword: maybeAppSpecificPassword } : null),
|
||||
|
||||
@@ -9,7 +9,10 @@ import { createTestProject } from '../../../project/__tests__/project-utils';
|
||||
import { getBundleIdentifierAsync } from '../../../project/ios/bundleIdentifier';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { createSubmissionContextAsync } from '../../context';
|
||||
import { getFromCredentialsServiceAsync } from '../CredentialsServiceSource';
|
||||
import {
|
||||
CREDENTIALS_SERVICE_SOURCE,
|
||||
getFromCredentialsServiceAsync,
|
||||
} from '../CredentialsServiceSource';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('../../../prompts');
|
||||
@@ -57,7 +60,10 @@ describe(getFromCredentialsServiceAsync, () => {
|
||||
asMock(getBundleIdentifierAsync).mockImplementation(() => 'com.hello.world');
|
||||
asMock(promptAsync).mockImplementationOnce(() => ({ appleId: '[email protected]' }));
|
||||
|
||||
const result = await getFromCredentialsServiceAsync(ctx);
|
||||
const result = await getFromCredentialsServiceAsync(ctx, {
|
||||
sourceType: CREDENTIALS_SERVICE_SOURCE,
|
||||
bundleIdentifier: 'com.expo.test.project',
|
||||
});
|
||||
expect(result).toEqual({
|
||||
appSpecificPassword: { password: 'super secret', appleIdUsername: '[email protected]' },
|
||||
});
|
||||
@@ -80,7 +86,10 @@ describe(getFromCredentialsServiceAsync, () => {
|
||||
.mockImplementation(async _ctx => testCommonIosAppCredentialsFragment);
|
||||
asMock(getBundleIdentifierAsync).mockImplementation(() => 'com.hello.world');
|
||||
|
||||
const result = await getFromCredentialsServiceAsync(ctx);
|
||||
const result = await getFromCredentialsServiceAsync(ctx, {
|
||||
sourceType: CREDENTIALS_SERVICE_SOURCE,
|
||||
bundleIdentifier: 'com.expo.test.project',
|
||||
});
|
||||
expect(result).toEqual({
|
||||
ascApiKeyResult: {
|
||||
result: {
|
||||
|
||||
@@ -1,26 +1,45 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { errors } from '@expo/eas-json';
|
||||
import { EasJsonReader, errors } from '@expo/eas-json';
|
||||
|
||||
import { getProfilesAsync } from '../profiles';
|
||||
|
||||
jest.mock('@expo/eas-json', () => {
|
||||
const actual = jest.requireActual('@expo/eas-json');
|
||||
|
||||
const EasJsonReaderMock = jest.fn();
|
||||
EasJsonReaderMock.prototype = {
|
||||
readBuildProfileAsync: jest.fn(),
|
||||
readSubmitProfileAsync: jest.fn(),
|
||||
};
|
||||
return {
|
||||
...actual,
|
||||
EasJsonReader: EasJsonReaderMock,
|
||||
};
|
||||
});
|
||||
|
||||
const readBuildProfileAsync = jest.spyOn(EasJsonReader.prototype, 'readBuildProfileAsync');
|
||||
|
||||
describe(getProfilesAsync, () => {
|
||||
afterEach(() => {
|
||||
readBuildProfileAsync.mockReset();
|
||||
});
|
||||
|
||||
it('defaults to production profile', async () => {
|
||||
const readProfileAsync = jest.fn();
|
||||
const result = await getProfilesAsync({
|
||||
projectDir: '/fake',
|
||||
platforms: [Platform.ANDROID, Platform.IOS],
|
||||
profileName: undefined,
|
||||
readProfileAsync,
|
||||
type: 'build',
|
||||
});
|
||||
|
||||
expect(result[0].profileName).toBe('production');
|
||||
expect(result[1].profileName).toBe('production');
|
||||
expect(readProfileAsync).toBeCalledWith(Platform.ANDROID, 'production');
|
||||
expect(readProfileAsync).toBeCalledWith(Platform.IOS, 'production');
|
||||
expect(readBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'production');
|
||||
expect(readBuildProfileAsync).toBeCalledWith(Platform.IOS, 'production');
|
||||
});
|
||||
|
||||
it('defaults to release profile when production profile is non-existent', async () => {
|
||||
const readProfileAsync = jest
|
||||
.fn()
|
||||
readBuildProfileAsync
|
||||
.mockRejectedValueOnce(() => {
|
||||
throw new Error();
|
||||
})
|
||||
@@ -29,22 +48,22 @@ describe(getProfilesAsync, () => {
|
||||
});
|
||||
|
||||
const result = await getProfilesAsync({
|
||||
projectDir: '/fake',
|
||||
platforms: [Platform.ANDROID, Platform.IOS],
|
||||
profileName: undefined,
|
||||
readProfileAsync,
|
||||
type: 'build',
|
||||
});
|
||||
|
||||
expect(result[0].profileName).toBe('release');
|
||||
expect(result[1].profileName).toBe('release');
|
||||
expect(readProfileAsync).toBeCalledWith(Platform.ANDROID, 'production');
|
||||
expect(readProfileAsync).toBeCalledWith(Platform.IOS, 'production');
|
||||
expect(readProfileAsync).toBeCalledWith(Platform.ANDROID, 'release');
|
||||
expect(readProfileAsync).toBeCalledWith(Platform.IOS, 'release');
|
||||
expect(readBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'production');
|
||||
expect(readBuildProfileAsync).toBeCalledWith(Platform.IOS, 'production');
|
||||
expect(readBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'release');
|
||||
expect(readBuildProfileAsync).toBeCalledWith(Platform.IOS, 'release');
|
||||
});
|
||||
|
||||
it('fails when neither production or release profiles are present', async () => {
|
||||
const readProfileAsync = jest
|
||||
.fn()
|
||||
readBuildProfileAsync
|
||||
.mockRejectedValueOnce(() => {
|
||||
throw new Error();
|
||||
})
|
||||
@@ -54,37 +73,39 @@ describe(getProfilesAsync, () => {
|
||||
|
||||
await expect(
|
||||
getProfilesAsync({
|
||||
projectDir: '/fake',
|
||||
platforms: [Platform.ANDROID],
|
||||
profileName: undefined,
|
||||
readProfileAsync,
|
||||
type: 'build',
|
||||
})
|
||||
).rejects.toThrowError(/There is no profile named "production" in eas.json/);
|
||||
).rejects.toThrowError(/There is no build profile named "production" in eas.json/);
|
||||
});
|
||||
|
||||
it('gets a specific profile', async () => {
|
||||
const readProfileAsync = jest.fn();
|
||||
const result = await getProfilesAsync({
|
||||
projectDir: '/fake',
|
||||
platforms: [Platform.ANDROID, Platform.IOS],
|
||||
profileName: 'custom-profile',
|
||||
readProfileAsync,
|
||||
type: 'build',
|
||||
});
|
||||
|
||||
expect(result[0].profileName).toBe('custom-profile');
|
||||
expect(result[1].profileName).toBe('custom-profile');
|
||||
expect(readProfileAsync).toBeCalledWith(Platform.ANDROID, 'custom-profile');
|
||||
expect(readProfileAsync).toBeCalledWith(Platform.IOS, 'custom-profile');
|
||||
expect(readBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'custom-profile');
|
||||
expect(readBuildProfileAsync).toBeCalledWith(Platform.IOS, 'custom-profile');
|
||||
});
|
||||
|
||||
it('throws validation error if eas.json is invalid', async () => {
|
||||
const readProfileAsync = jest.fn().mockImplementation(() => {
|
||||
readBuildProfileAsync.mockImplementation(() => {
|
||||
throw new errors.InvalidEasJsonError('eas.json is not valid');
|
||||
});
|
||||
|
||||
await expect(
|
||||
getProfilesAsync({
|
||||
projectDir: '/fake',
|
||||
platforms: [Platform.ANDROID, Platform.IOS],
|
||||
profileName: undefined,
|
||||
readProfileAsync,
|
||||
type: 'build',
|
||||
})
|
||||
).rejects.toThrowError(/eas.json is not valid/);
|
||||
});
|
||||
|
||||
@@ -3,12 +3,17 @@ import plist from '@expo/plist';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
export async function readPlistAsync(plistPath: string): Promise<object> {
|
||||
export async function readPlistAsync(plistPath: string): Promise<object | null> {
|
||||
if (await fs.pathExists(plistPath)) {
|
||||
const expoPlistContent = await fs.readFile(plistPath, 'utf8');
|
||||
return plist.parse(expoPlistContent);
|
||||
try {
|
||||
return plist.parse(expoPlistContent);
|
||||
} catch (err: any) {
|
||||
err.message = `Failed to parse ${plistPath}. ${err.message}`;
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
return {};
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,45 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { errors } from '@expo/eas-json';
|
||||
import { BuildProfile, EasJsonReader, SubmitProfile, errors } from '@expo/eas-json';
|
||||
|
||||
import Log from '../log';
|
||||
|
||||
export type ProfileData<T> = {
|
||||
profile: T;
|
||||
type ProfileType = 'build' | 'submit';
|
||||
|
||||
type EasProfile<T extends ProfileType> = T extends 'build'
|
||||
? BuildProfile<Platform>
|
||||
: SubmitProfile<Platform>;
|
||||
|
||||
export type ProfileData<T extends ProfileType> = {
|
||||
profile: EasProfile<T>;
|
||||
platform: Platform;
|
||||
profileName: string;
|
||||
};
|
||||
|
||||
export async function getProfilesAsync<T>({
|
||||
export async function getProfilesAsync<T extends ProfileType>({
|
||||
projectDir,
|
||||
platforms,
|
||||
profileName: profileNameArg,
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
readProfileAsync,
|
||||
type,
|
||||
}: {
|
||||
projectDir: string;
|
||||
platforms: Platform[];
|
||||
profileName?: string | null;
|
||||
readProfileAsync: (platform: Platform, profileName: string) => Promise<T>;
|
||||
type: T;
|
||||
}): Promise<ProfileData<T>[]> {
|
||||
const results = platforms.map(async function (platform) {
|
||||
let profile;
|
||||
let profile: EasProfile<T>;
|
||||
let profileName = profileNameArg;
|
||||
|
||||
if (!profileName) {
|
||||
try {
|
||||
profile = await readProfileAsync(platform, 'production');
|
||||
profile = await readProfileAsync({ projectDir, platform, type, profileName: 'production' });
|
||||
profileName = 'production';
|
||||
} catch (errorOuter) {
|
||||
if (errorOuter instanceof errors.InvalidEasJsonError) {
|
||||
throw errorOuter;
|
||||
}
|
||||
try {
|
||||
profile = await readProfileAsync(platform, 'release');
|
||||
profile = await readProfileAsync({ projectDir, platform, type, profileName: 'release' });
|
||||
profileName = 'release';
|
||||
Log.warn(
|
||||
'The default profile changed from "release" to "production". We detected that you still have a "release" build profile, so we are using it. Update eas.json to have a profile named "production" under the `build` key, or specify which profile you\'d like to use with the --profile flag. This fallback behavior will be removed in the next major version of EAS CLI.'
|
||||
@@ -41,11 +48,11 @@ export async function getProfilesAsync<T>({
|
||||
if (errorInner instanceof errors.InvalidEasJsonError) {
|
||||
throw errorInner;
|
||||
}
|
||||
throw new Error('There is no profile named "production" in eas.json');
|
||||
throw new Error(`There is no ${type} profile named "production" in eas.json`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
profile = await readProfileAsync(platform, profileName);
|
||||
profile = await readProfileAsync({ projectDir, platform, type, profileName });
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -57,3 +64,22 @@ export async function getProfilesAsync<T>({
|
||||
|
||||
return await Promise.all(results);
|
||||
}
|
||||
|
||||
async function readProfileAsync<T extends ProfileType>({
|
||||
projectDir,
|
||||
platform,
|
||||
type,
|
||||
profileName,
|
||||
}: {
|
||||
projectDir: string;
|
||||
platform: Platform;
|
||||
type: ProfileType;
|
||||
profileName: string;
|
||||
}): Promise<EasProfile<T>> {
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
if (type === 'build') {
|
||||
return (await easJsonReader.readBuildProfileAsync(platform, profileName)) as EasProfile<T>;
|
||||
} else {
|
||||
return (await easJsonReader.readSubmitProfileAsync(platform, profileName)) as EasProfile<T>;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import {
|
||||
doesGitRepoExistAsync,
|
||||
@@ -49,16 +51,29 @@ export default class GitClient extends Client {
|
||||
public async commitAsync({
|
||||
commitMessage,
|
||||
commitAllFiles,
|
||||
nonInteractive = false,
|
||||
}: {
|
||||
commitMessage: string;
|
||||
commitAllFiles?: boolean;
|
||||
nonInteractive?: boolean;
|
||||
}): Promise<void> {
|
||||
if (commitAllFiles) {
|
||||
await spawnAsync('git', ['add', '-A']);
|
||||
}
|
||||
await ensureGitConfiguredAsync({ nonInteractive });
|
||||
|
||||
await spawnAsync('git', ['add', '-u']);
|
||||
await spawnAsync('git', ['commit', '-m', commitMessage]);
|
||||
try {
|
||||
if (commitAllFiles) {
|
||||
await spawnAsync('git', ['add', '-A']);
|
||||
}
|
||||
await spawnAsync('git', ['add', '-u']);
|
||||
await spawnAsync('git', ['commit', '-m', commitMessage]);
|
||||
} catch (err: any) {
|
||||
if (err?.stdout) {
|
||||
Log.error(err.stdout);
|
||||
}
|
||||
if (err?.stderr) {
|
||||
Log.error(err.stderr);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public async isCommitRequiredAsync(): Promise<boolean> {
|
||||
@@ -171,6 +186,76 @@ export default class GitClient extends Client {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureGitConfiguredAsync({
|
||||
nonInteractive,
|
||||
}: {
|
||||
nonInteractive: boolean;
|
||||
}): Promise<void> {
|
||||
let usernameConfigured = true;
|
||||
let emailConfigured = true;
|
||||
try {
|
||||
await spawnAsync('git', ['config', '--get', 'user.name']);
|
||||
} catch (err: any) {
|
||||
Log.debug(err);
|
||||
usernameConfigured = false;
|
||||
}
|
||||
try {
|
||||
await spawnAsync('git', ['config', '--get', 'user.email']);
|
||||
} catch (err: any) {
|
||||
Log.debug(err);
|
||||
emailConfigured = false;
|
||||
}
|
||||
if (usernameConfigured && emailConfigured) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.warn(
|
||||
`You need to configure Git with your ${[
|
||||
!usernameConfigured && 'username (user.name)',
|
||||
!emailConfigured && 'email address (user.email)',
|
||||
]
|
||||
.filter(i => i)
|
||||
.join(' and ')}`
|
||||
);
|
||||
if (nonInteractive) {
|
||||
throw new Error('Git cannot be configured automatically in non-interactive mode');
|
||||
}
|
||||
if (!usernameConfigured) {
|
||||
const { username } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'username',
|
||||
message: 'Username:',
|
||||
validate: (input: string) => input !== '',
|
||||
});
|
||||
const spinner = ora(
|
||||
`Running ${chalk.bold(`git config --local user.name ${username}`)}`
|
||||
).start();
|
||||
try {
|
||||
await spawnAsync('git', ['config', '--local', 'user.name', username]);
|
||||
spinner.succeed();
|
||||
} catch (err: any) {
|
||||
spinner.fail();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
if (!emailConfigured) {
|
||||
const { email } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'email',
|
||||
message: 'Email address:',
|
||||
validate: (input: string) => input !== '',
|
||||
});
|
||||
const spinner = ora(`Running ${chalk.bold(`git config --local user.email ${email}`)}`).start();
|
||||
try {
|
||||
await spawnAsync('git', ['config', '--local', 'user.email', email]);
|
||||
spinner.succeed();
|
||||
} catch (err: any) {
|
||||
spinner.fail();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if git is configured to be case sensitive
|
||||
* @returns {boolean | undefined}
|
||||
|
||||
@@ -34,9 +34,10 @@ export abstract class Client {
|
||||
// - Should be implemented if hasUncommittedChangesAsync is implemented
|
||||
// - If it's not implemented method `makeShallowCopyAsync` needs to be able to include uncommitted changes
|
||||
// in project copy
|
||||
public async commitAsync(arg: {
|
||||
public async commitAsync(_arg: {
|
||||
commitMessage: string;
|
||||
commitAllFiles?: boolean;
|
||||
nonInteractive?: boolean;
|
||||
}): Promise<void> {
|
||||
// it should not be called unless hasUncommittedChangesAsync is implemented
|
||||
throw new Error('commitAsync is not implemented');
|
||||
@@ -44,7 +45,7 @@ export abstract class Client {
|
||||
|
||||
// (optional) mark file as tracked, if this method is called on file, the next call to
|
||||
// `commitAsync({ commitAllFiles: false })` should commit that file
|
||||
public async trackFileAsync(file: string): Promise<void> {}
|
||||
public async trackFileAsync(_file: string): Promise<void> {}
|
||||
|
||||
// (optional) print diff of the changes that will be commited in the next call to
|
||||
// `commitAsync({ commitAllFiles: false })`
|
||||
@@ -74,7 +75,7 @@ export abstract class Client {
|
||||
//
|
||||
// @param filePath has to be a relative normalized path pointing to a file
|
||||
// located under the root of the repository
|
||||
public async isFileIgnoredAsync(filePath: string): Promise<boolean> {
|
||||
public async isFileIgnoredAsync(_filePath: string): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@expo/eas-json",
|
||||
"description": "A library for interacting with eas.json",
|
||||
"version": "0.36.0",
|
||||
"version": "0.37.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import envString from 'env-string';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { BuildProfile } from './EasBuild.types';
|
||||
@@ -17,7 +18,7 @@ import {
|
||||
IosSubmitProfileFieldsToEvaluate,
|
||||
SubmitProfile,
|
||||
} from './EasSubmit.types';
|
||||
import { InvalidEasJsonError } from './errors';
|
||||
import { InvalidEasJsonError, MissingEasJsonError } from './errors';
|
||||
|
||||
interface EasJsonPreValidation {
|
||||
cli?: object;
|
||||
@@ -71,7 +72,7 @@ export class EasJsonReader {
|
||||
}
|
||||
return value as CliConfig;
|
||||
} catch (err: any) {
|
||||
if (err.code === 'ENOENT') {
|
||||
if (err instanceof MissingEasJsonError) {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
@@ -141,7 +142,7 @@ export class EasJsonReader {
|
||||
const easJson = await this.readAndValidateAsync();
|
||||
const profile = easJson?.submit?.[profileName];
|
||||
if (!profile) {
|
||||
throw new Error(`There is no profile named ${profileName} in eas.json`);
|
||||
throw new Error(`There is no submit profile named ${profileName} in eas.json`);
|
||||
}
|
||||
const platformProfile = profile[platform];
|
||||
if (platformProfile) {
|
||||
@@ -168,6 +169,11 @@ export class EasJsonReader {
|
||||
public async readRawAsync(): Promise<EasJsonPreValidation> {
|
||||
try {
|
||||
const easJsonPath = EasJsonReader.formatEasJsonPath(this.projectDir);
|
||||
if (!(await fs.pathExists(easJsonPath))) {
|
||||
throw new MissingEasJsonError(
|
||||
`An eas.json file could not be found at ${easJsonPath}. You must make one in order to proceed. Learn more at https://expo.fyi/eas-json`
|
||||
);
|
||||
}
|
||||
const rawEasJson = JsonFile.read(easJsonPath);
|
||||
const { value, error } = MinimalEasJsonSchema.validate(rawEasJson, { abortEarly: false });
|
||||
if (error) {
|
||||
@@ -194,7 +200,7 @@ export class EasJsonReader {
|
||||
}
|
||||
const buildProfile = easJson.build[profileName];
|
||||
if (!buildProfile) {
|
||||
throw new Error(`There is no profile named ${profileName} in eas.json`);
|
||||
throw new Error(`There is no build profile named ${profileName} in eas.json`);
|
||||
}
|
||||
const { extends: baseProfileName, ...buildProfileRest } = buildProfile;
|
||||
if (baseProfileName) {
|
||||
@@ -209,7 +215,7 @@ export class EasJsonReader {
|
||||
|
||||
private ensureBuildProfileExists(easJson: EasJson, profileName: string): void {
|
||||
if (!easJson.build || !easJson.build[profileName]) {
|
||||
throw new Error(`There is no profile named ${profileName} in eas.json`);
|
||||
throw new Error(`There is no build profile named ${profileName} in eas.json`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ const CommonBuildProfileSchema = Joi.object({
|
||||
credentialsSource: Joi.string().valid('local', 'remote'),
|
||||
distribution: Joi.string().valid('store', 'internal'),
|
||||
cache: CacheSchema,
|
||||
releaseChannel: Joi.string(),
|
||||
channel: Joi.string(),
|
||||
releaseChannel: Joi.string().regex(/^[a-z\d][a-z\d._-]*$/),
|
||||
channel: Joi.string().regex(/^[a-z\d][a-z\d._-]*$/),
|
||||
developmentClient: Joi.boolean(),
|
||||
|
||||
node: Joi.string().empty(null).custom(semverSchemaCheck),
|
||||
@@ -87,6 +87,7 @@ export const AndroidSubmitProfileSchema = Joi.object({
|
||||
.valid(...Object.values(AndroidReleaseStatus))
|
||||
.default(AndroidReleaseStatus.completed),
|
||||
changesNotSentForReview: Joi.boolean().default(false),
|
||||
applicationId: Joi.string(),
|
||||
});
|
||||
|
||||
export const IosSubmitProfileSchema = Joi.object({
|
||||
@@ -100,6 +101,7 @@ export const IosSubmitProfileSchema = Joi.object({
|
||||
language: Joi.string().default('en-US'),
|
||||
companyName: Joi.string(),
|
||||
appName: Joi.string(),
|
||||
bundleIdentifier: Joi.string(),
|
||||
});
|
||||
|
||||
const EasJsonSubmitConfigurationSchema = Joi.object({
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface AndroidSubmitProfile {
|
||||
track: AndroidReleaseTrack;
|
||||
releaseStatus: AndroidReleaseStatus;
|
||||
changesNotSentForReview: boolean;
|
||||
applicationId?: string;
|
||||
}
|
||||
|
||||
export const AndroidSubmitProfileFieldsToEvaluate: (keyof AndroidSubmitProfile)[] = [
|
||||
@@ -36,6 +37,7 @@ export interface IosSubmitProfile {
|
||||
language: string;
|
||||
companyName?: string;
|
||||
appName?: string;
|
||||
bundleIdentifier?: string;
|
||||
}
|
||||
|
||||
export const IosSubmitProfileFieldsToEvaluate: (keyof IosSubmitProfile)[] = [
|
||||
|
||||
@@ -225,7 +225,7 @@ test('valid eas.json with missing profile', async () => {
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'debug');
|
||||
await expect(promise).rejects.toThrowError('There is no profile named debug in eas.json');
|
||||
await expect(promise).rejects.toThrowError('There is no build profile named debug in eas.json');
|
||||
});
|
||||
|
||||
test('invalid eas.json when using wrong buildType', async () => {
|
||||
@@ -247,7 +247,9 @@ test('empty json', async () => {
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'production');
|
||||
await expect(promise).rejects.toThrowError('There is no profile named production in eas.json');
|
||||
await expect(promise).rejects.toThrowError(
|
||||
'There is no build profile named production in eas.json'
|
||||
);
|
||||
});
|
||||
|
||||
test('invalid semver value', async () => {
|
||||
@@ -264,6 +266,18 @@ test('invalid semver value', async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('invalid release channel', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
build: {
|
||||
production: { releaseChannel: 'feature/myfeature' },
|
||||
},
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'production');
|
||||
await expect(promise).rejects.toThrowError(/fails to match the required pattern/);
|
||||
});
|
||||
|
||||
test('get profile names', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
build: {
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export class InvalidEasJsonError extends Error {}
|
||||
|
||||
export class MissingEasJsonError extends Error {}
|
||||
@@ -8882,10 +8882,10 @@ prettier-linter-helpers@^1.0.0:
|
||||
dependencies:
|
||||
fast-diff "^1.1.2"
|
||||
|
||||
prettier@2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d"
|
||||
integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==
|
||||
prettier@2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.4.1.tgz#671e11c89c14a4cfc876ce564106c4a6726c9f5c"
|
||||
integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==
|
||||
|
||||
pretty-format@^27.0.0, pretty-format@^27.1.0:
|
||||
version "27.1.0"
|
||||
|
||||
Reference in new issue
Block a user