Compare commits

...
18 Commits
Author SHA1 Message Date
Dominik Sokal 558c367038 v0.37.0 2021-11-18 10:42:38 +01:00
Dominik Sokal 4b88d00379 upgrade eas-cli-local-build-plugin to 0.0.50 2021-11-18 10:42:16 +01:00
Dominik Sokal aad3b5ccd5 interactively configure git user.name and user.email (#772)
* interactively configure git user.name and user.email

* update CHANGELOG.md

* address PR feedback
2021-11-18 10:30:51 +01:00
Quinlan Jung 8b50806093 [eas-cli] make the missing eas.json messages better (#770)
* [eas-cli] make the missing eas.json messages better

* make error upstream

* revert

* fix tests and stuff

* update CHANGELOG.md
2021-11-17 13:39:44 -08:00
Dominik Sokal 45c9bda2e9 remove eas availability checks (#771) 2021-11-17 13:26:36 +01:00
Dominik Sokal 23237210d1 Update CHANGELOG.md 2021-11-17 09:58:36 +01:00
Dominik Sokal e34382e6a3 show plist path when parsing failed (#768)
* show plist path when parsing failed

* Update packages/eas-cli/src/build/ios/version.ts
2021-11-17 09:57:59 +01:00
jkh b8ad2ee29a [eas-cli] auto create channel on publish (#766) 2021-11-16 13:23:39 -05:00
Wojciech KozyraandDominik Sokal 6bd62bac64 [eas-cli] read correct identifiers for multi-scheme/multi-flavor projects (#765)
* [eas-cli] read correct appIdentifier for multi-scheme/multi-flavor project

* review feedback

* Update CHANGELOG.md

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-16 15:52:39 +01:00
Dominik Sokal 8b959fc6d8 make the missing build/submit profile error message more descriptive (#761) 2021-11-15 20:04:34 +01:00
Bartosz Kaszubowski 6e615e20d6 [workflows] update setup-node action to v2 (#763) 2021-11-15 19:59:29 +01:00
Bartosz Kaszubowski efbc5fc4bc [workspace] update Prettier (#762) 2021-11-15 19:59:12 +01:00
Dominik Sokal 79c77ae835 validate release channel in eas.json (#764)
* validate release channel in eas.json

* update CHANGELOG.md
2021-11-15 16:52:12 +01:00
Expo CI 618b6b1caf update CHANGELOG.md 2021-11-15 11:50:31 +00:00
Dominik Sokal 864658df80 v0.36.1 2021-11-15 12:26:39 +01:00
Dominik Sokal 4510e86b64 update CHANGELOG.md 2021-11-15 12:24:53 +01:00
Dominik Sokal 4af4490abc upgrade eas-cli-local-build-plugin to 0.0.48 2021-11-15 12:23:18 +01:00
Expo CI 37c4040203 update CHANGELOG.md 2021-11-15 10:48:35 +00:00
54 changed files with 609 additions and 392 deletions

No files matched your search

-1
View File
@@ -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': [
+1 -1
View File
@@ -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
+5 -5
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1
View File
@@ -12,3 +12,4 @@ node_modules
coverage
.history
.DS_Store
.idea
+1 -1
View File
@@ -2,7 +2,7 @@
"printWidth": 100,
"tabWidth": 2,
"singleQuote": true,
"jsxBracketSameLine": true,
"bracketSameLine": true,
"trailingComma": "es5",
"arrowParens": "avoid"
}
+27
View File
@@ -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
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.36.0"
"version": "0.37.0"
}
+2 -2
View File
@@ -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
View File
@@ -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 -->
+2 -2
View File
@@ -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,
+6 -3
View File
@@ -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({
+1 -1
View File
@@ -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: [] };
}
+6 -16
View File
@@ -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");
}
+2 -12
View File
@@ -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;
}
}
+38 -12
View File
@@ -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>;
}
}
+90 -5
View File
@@ -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}
+4 -3
View File
@@ -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 -1
View File
@@ -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": {
+11 -5
View File
@@ -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`);
}
}
+4 -2
View File
@@ -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({
+2
View File
@@ -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: {
+2
View File
@@ -1 +1,3 @@
export class InvalidEasJsonError extends Error {}
export class MissingEasJsonError extends Error {}
Generated
+4 -4
View File
@@ -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"