Compare commits

...
19 Commits
Author SHA1 Message Date
Dominik Sokal f15beea454 v0.20.0 2021-07-09 10:31:16 +02:00
Wojciech Kozyra 16e384be76 [eas-cli] Error message for eas build downtime (#502) 2021-07-09 10:04:17 +02:00
jkh e74e6d8fc5 [eas-cli] import UI in branch:* channel:* commands (#503) 2021-07-08 18:05:51 -07:00
jkh b6f4603107 [eas-cli] remove shameful console.log (#501) 2021-07-07 18:37:41 -07:00
Wojciech Kozyra 0e7d7f765a [eas-cli] unify generic and managed workflows (#497) 2021-07-07 12:30:06 +02:00
jkh 33b4d8d719 [eas-cli] add project:info command (#498) 2021-07-06 19:25:21 -07:00
Wojciech KozyraandBartłomiej Klocek 0d765748e3 [eas-cli] override value from app.json with param (#494)
* [eas-cli] override value from app.json with param

* update changelog

* Apply suggestions from code review

Co-authored-by: Bartłomiej Klocek <[email protected]>

Co-authored-by: Bartłomiej Klocek <[email protected]>
2021-07-05 15:42:13 +02:00
Dominik Sokal 93953f1424 bump @expo/config-plugins to 3.0.3 (#495)
* bump @expo/config-plugins to 3.0.3

* update CHANGELOG

* fix type mismatch
2021-07-05 14:15:17 +02:00
Quinlan Jung e9b67db05c update chagelog 2021-07-02 11:18:30 -07:00
Quinlan Jung 09bf912b9e v0.19.1 2021-07-02 11:02:09 -07:00
Quinlan Jung e701bacdaa [eas-cli] better debugging experience (#492)
* [eas-cli] better debugging

* fyi

* changelog
2021-07-02 10:59:16 -07:00
Quinlan JungandDominik Sokal cc22542031 [eas-cli] prompt to create project if not exists, more credentials guardrails (#491)
* [eas-cli] prompt to create project if not exists

* [eas-cli] more credentials manager guardrails

* Update packages/eas-cli/src/credentials/manager/ManageAndroid.ts

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

* Update packages/eas-cli/src/credentials/manager/ManageIos.ts

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

* Update packages/eas-cli/src/project/projectUtils.ts

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

* pr feedback

Co-authored-by: Dominik Sokal <[email protected]>
2021-07-02 10:53:26 -07:00
Dominik Sokal 6c53262b5b [ENG-1444] improve workflow detection (#490)
* improve workflow detection

* update CHANGELOG

* address PR feedback
2021-07-02 10:24:05 +02:00
Wojciech Kozyra 7d7d4995d8 update CHANGELOG 2021-07-01 12:23:13 +02:00
Wojciech Kozyra 977546c8c9 v0.19.0 2021-07-01 12:05:57 +02:00
Brent Vatne 53dcd4cb43 [eas-cli] Support configuring one platform at a time when running eas build (#483) 2021-06-30 17:52:37 -07:00
Wojciech Kozyra 9e18e738eb [eas-cli] use new build mutation (#485) 2021-06-29 11:54:11 +02:00
Dominik Sokal 252c7187d5 [ENG-1377] auto-suggest application id and bundle identifier (#487)
* Auto-suggest application id and bundle identifier

* update CHANGELOG

* address PR feedback
2021-06-29 10:51:48 +02:00
Quinlan Jung 90852b9557 update changelog 2021-06-28 16:01:48 -07:00
100 changed files with 2291 additions and 2331 deletions

No files matched your search

+1
View File
@@ -10,6 +10,7 @@ module.exports = {
jasmine: false,
},
rules: {
'no-console':'warn',
'no-constant-condition': ['warn', { checkLoops: false }],
'sort-imports': [
'warn',
+32 -3
View File
@@ -6,16 +6,46 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
- Unifify generic and managed workflow, deprecate `workflow` field. ([#497](https://github.com/expo/eas-cli/pull/497) by [@wkozyra95](https://github.com/wkozyra95))
### 🎉 New features
### 🐛 Bug fixes
- Fix runtime version checks. ([#495](https://github.com/expo/eas-cli/pull/495) by [@dsokal](https://github.com/dsokal))
- Resolve `--android-package` correctly in `eas submit` command. ([#494](https://github.com/expo/eas-cli/pull/494) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
## [0.19.1](https://github.com/expo/eas-cli/releases/tag/v0.19.1) - 2021-07-02
### 🎉 New features
- Improve project workflow detection (fixes the case where the `android` and/or `ios` directories are git-ignored). ([#490](https://github.com/expo/eas-cli/pull/490) by [@dsokal](https://github.com/dsokal))
- Improve credentials workflow with project creation and current working directories ([#491](https://github.com/expo/eas-cli/pull/491) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Better debugging experience ([#492](https://github.com/expo/eas-cli/pull/492) by [@quinlanj](https://github.com/quinlanj))
## [0.19.0](https://github.com/expo/eas-cli/releases/tag/v0.19.0) - 2021-07-01
### 🎉 New features
- Auto-suggest application id and bundle identifier when running `eas build:configure` for a managed project. ([#487](https://github.com/expo/eas-cli/pull/487) by [@dsokal](https://github.com/dsokal))
- Support configuring one platform at a time when running `eas build`. ([#483](https://github.com/expo/eas-cli/pull/483) by [@brentvatne](https://github.com/brentvatne))
## [0.18.3](https://github.com/expo/eas-cli/releases/tag/v0.18.3) - 2021-06-28
### 🎉 New features
- Support bundle identifiers that begin with `.`. ([#459](https://github.com/expo/eas-cli/pull/459) by [@EvanBacon](https://github.com/EvanBacon))
- Push Key setup integrated with ios build command ([#473](https://github.com/expo/eas-cli/pull/473) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Fix android builds for PKCS12 keystores. ([#484](https://github.com/expo/eas-cli/pull/484) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
## [0.18.2](https://github.com/expo/eas-cli/releases/tag/v0.18.2) - 2021-06-25
### 🎉 New features
@@ -47,7 +77,6 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Support bundle identifiers that begin with `.`. ([#459](https://github.com/expo/eas-cli/pull/459) by [@EvanBacon](https://github.com/EvanBacon))
- Make credentials manager work with multi-target iOS projects. ([#441](https://github.com/expo/eas-cli/pull/441) by [@dsokal](https://github.com/dsokal))
- Copy over credentials from Expo Classic to EAS. ([#445](https://github.com/expo/eas-cli/pull/445) by [@quinlanj](https://github.com/quinlanj))
- Add Big Sur image for iOS builds. ([#457](https://github.com/expo/eas-cli/pull/457) by [@wkozyra95](https://github.com/wkozyra95))
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.18.3"
"version": "0.20.0"
}
+22 -22
View File
@@ -69,7 +69,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -83,7 +83,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -97,7 +97,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -108,7 +108,7 @@ USAGE
$ eas analytics [STATUS]
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/analytics.ts)_
## `eas build`
@@ -128,7 +128,7 @@ OPTIONS
--[no-]wait Wait for build(s) to complete
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -139,7 +139,7 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -153,7 +153,7 @@ OPTIONS
-p, --platform=(android|ios|all) Platform to configure
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:list`
@@ -169,7 +169,7 @@ OPTIONS
--status=(new|in-queue|in-progress|errored|finished|canceled)
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/build/list.ts)_
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:view [BUILD_ID]`
@@ -180,7 +180,7 @@ USAGE
$ eas build:view [BUILD_ID]
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/build/view.ts)_
## `eas credentials`
@@ -191,7 +191,7 @@ USAGE
$ eas credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -202,7 +202,7 @@ USAGE
$ eas device:create
```
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:list`
@@ -216,7 +216,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -227,7 +227,7 @@ USAGE
$ eas device:view [UDID]
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/device/view.ts)_
## `eas help [COMMAND]`
@@ -260,7 +260,7 @@ OPTIONS
--value=value Value of the secret
```
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -277,7 +277,7 @@ DESCRIPTION
Unsure where to find the secret's ID? Run eas secrets:list
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -288,7 +288,7 @@ USAGE
$ eas secret:list
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/secret/list.ts)_
## `eas submit --platform=(android|ios)`
@@ -368,7 +368,7 @@ EXAMPLES
and provide its App ID
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/submit.ts)_
## `eas webhook:create`
@@ -387,7 +387,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -401,7 +401,7 @@ ARGUMENTS
ID ID of the webhook to delete
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -415,7 +415,7 @@ OPTIONS
--event=(BUILD) Event type that triggers the webhook
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -435,7 +435,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -449,5 +449,5 @@ ARGUMENTS
ID ID of the webhook to view
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.18.3/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.20.0/packages/eas-cli/src/commands/webhook/view.ts)_
<!-- commandsstop -->
+7
View File
@@ -1,2 +1,9 @@
import { fs } from 'memfs';
// needed because of a weird bug with tempy (dependency of @expo/config-plugins)
fs.mkdirSync('/tmp');
if (process.env.TMPDIR) {
fs.mkdirSync(process.env.TMPDIR, { recursive: true });
}
module.exports = fs;
-3
View File
@@ -1,3 +0,0 @@
module.exports = async () => {
process.env.TZ = 'UTC';
};
File diff suppressed because it is too large. Load diff
+2 -1
View File
@@ -2,5 +2,6 @@ module.exports = {
...require('../../jest/jest.config.js'),
rootDir: __dirname,
roots: ['src', '__mocks__'],
globalSetup: './global-setup.ts',
globalSetup: './jest/global-setup.ts',
setupFilesAfterEnv: ['./jest/setup-after-env.ts'],
};
+5
View File
@@ -0,0 +1,5 @@
module.exports = async () => {
process.env.TZ = 'UTC';
process.env.EAS_NO_VCS = '1';
process.env.EAS_PROJECT_ROOT = '/app';
};
+6
View File
@@ -0,0 +1,6 @@
/* eslint-disable no-console */
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
console.error = jest.fn();
});
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.18.3",
"version": "0.20.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -12,9 +12,9 @@
"@amplitude/node": "1.5.0",
"@expo/apple-utils": "0.0.0-alpha.20",
"@expo/config": "3.3.42",
"@expo/config-plugins": "2.0.2",
"@expo/eas-build-job": "0.2.37",
"@expo/eas-json": "^0.18.2",
"@expo/config-plugins": "3.0.3",
"@expo/eas-build-job": "0.2.41",
"@expo/eas-json": "^0.20.0",
"@expo/json-file": "8.2.25",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.11",
+1
View File
@@ -6,4 +6,5 @@ readmeContent = readmeContent.replace(/\[build\//g, '[src/');
readmeContent = readmeContent.replace(/build\/commands/g, 'packages/eas-cli/src/commands');
fs.writeFileSync('README.md', readmeContent);
// eslint-disable-next-line no-console
console.log('Patched README path generation');
@@ -101,7 +101,6 @@ export async function readChannelSafelyAsync(projectDir: string): Promise<string
}
return JSON.parse(stringifiedRequestHeaders)['expo-channel-name'] ?? null;
} catch (err) {
console.log({ err });
return null;
}
}
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { readVersionCode, readVersionName } from '../version';
import { readVersionCodeAsync, readVersionNameAsync } from '../version';
jest.mock('fs');
@@ -20,35 +20,35 @@ beforeEach(() => {
fs.mkdirpSync(os.tmpdir());
});
describe(readVersionCode, () => {
describe(readVersionCodeAsync, () => {
describe('generic project', () => {
it('reads the version code from native code', () => {
it('reads the version code from native code', async () => {
const exp = initGenericProject();
const versionCode = readVersionCode('/repo', exp);
const versionCode = await readVersionCodeAsync('/repo', exp);
expect(versionCode).toBe(123);
});
});
describe('managed project', () => {
it('reads the version code from expo config', () => {
it('reads the version code from expo config', async () => {
const exp = initManagedProject();
const versionCode = readVersionCode('/repo', exp);
const versionCode = await readVersionCodeAsync('/repo', exp);
expect(versionCode).toBe(123);
});
});
});
describe(readVersionName, () => {
describe(readVersionNameAsync, () => {
describe('generic project', () => {
it('reads the version name from native code', () => {
it('reads the version name from native code', async () => {
const exp = initGenericProject();
const versionName = readVersionName('/repo', exp);
const versionName = await readVersionNameAsync('/repo', exp);
expect(versionName).toBe('1.0');
});
});
describe('managed project', () => {
it('reads the version from expo config', () => {
it('reads the version from expo config', async () => {
const exp = initManagedProject();
const versionName = readVersionName('/repo', exp);
const versionName = await readVersionNameAsync('/repo', exp);
expect(versionName).toBe('1.0.0');
});
});
@@ -72,6 +72,7 @@ function initGenericProject(): ExpoConfig {
versionName "1.0"
}
}`,
'./android/app/src/main/AndroidManifest.xml': 'fake',
},
'/repo'
);
+15 -44
View File
@@ -1,9 +1,7 @@
import { Android, Metadata, Workflow } from '@expo/eas-build-job';
import { EasConfig } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import path from 'path';
import AndroidCredentialsProvider, {
AndroidCredentials,
@@ -13,46 +11,31 @@ import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutatio
import Log from '../../log';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
getApplicationId,
getApplicationIdAsync,
} from '../../project/android/applicationId';
import { toggleConfirmAsync } from '../../prompts';
import { findAccountByName } from '../../user/Account';
import { CredentialsResult, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext, CommandContext, createBuildContext } from '../context';
import { BuildContext, CommandContext, createBuildContextAsync } from '../context';
import { transformMetadata } from '../graphql';
import { Platform } from '../types';
import { logCredentialsSource } from '../utils/credentials';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { transformGenericJob, transformManagedJob } from './graphql';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
export async function prepareAndroidBuildAsync(
commandCtx: CommandContext,
easConfig: EasConfig
): Promise<() => Promise<string | undefined>> {
const buildCtx = createBuildContext<Platform.ANDROID>({
const buildCtx = await createBuildContextAsync<Platform.ANDROID>({
commandCtx,
platform: Platform.ANDROID,
easConfig,
});
const { buildProfile } = buildCtx;
if (
buildProfile.workflow === Workflow.GENERIC &&
!(await fs.pathExists(path.join(commandCtx.projectDir, 'android')))
) {
throw new Error(
`"android" directory not found. If you're trying to build a managed project, set ${chalk.bold(
`builds.android.${commandCtx.profile}.workflow`
)} in "eas.json" to "managed".`
);
}
if (
buildProfile.workflow === Workflow.GENERIC &&
buildProfile.distribution === 'internal' &&
buildProfile.gradleCommand?.match(/bundle/)
) {
if (buildProfile.distribution === 'internal' && buildProfile.gradleCommand?.match(/bundle/)) {
Log.addNewLineIfNone();
Log.warn(
`You're building your Android app for internal distribution. However, we've detected that the Gradle command you defined (${chalk.underline(
@@ -68,7 +51,7 @@ This means that it will most likely produce an AAB and you will not be able to i
}
}
if (buildCtx.buildProfile.workflow === Workflow.MANAGED) {
if (buildCtx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(commandCtx.projectDir, commandCtx.exp);
}
@@ -76,7 +59,7 @@ This means that it will most likely produce an AAB and you will not be able to i
ctx: buildCtx,
ensureCredentialsAsync: ensureAndroidCredentialsAsync,
ensureProjectConfiguredAsync: async () => {
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
if (buildCtx.workflow === Workflow.GENERIC) {
await validateAndSyncProjectConfigurationAsync(commandCtx.projectDir, commandCtx.exp);
}
},
@@ -87,30 +70,18 @@ This means that it will most likely produce an AAB and you will not be able to i
metadata: Metadata
): Promise<BuildResult> => {
const graphqlMetadata = transformMetadata(metadata);
if (job.type === Workflow.GENERIC) {
const graphqlJob = transformGenericJob(job);
return await BuildMutation.createAndroidGenericBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
} else {
const graphqlJob = transformManagedJob(job);
return await BuildMutation.createAndroidManagedBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
}
const graphqlJob = transformJob(job);
return await BuildMutation.createAndroidBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
},
});
}
function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean {
return (
ctx.buildProfile.workflow === Workflow.MANAGED ||
(ctx.buildProfile.workflow === Workflow.GENERIC && !ctx.buildProfile.withoutCredentials)
);
return !ctx.buildProfile.withoutCredentials;
}
async function ensureAndroidCredentialsAsync(
@@ -119,7 +90,7 @@ async function ensureAndroidCredentialsAsync(
if (!shouldProvideCredentials(ctx)) {
return;
}
const androidApplicationIdentifier = getApplicationId(
const androidApplicationIdentifier = await getApplicationIdAsync(
ctx.commandCtx.projectDir,
ctx.commandCtx.exp
);
+11 -26
View File
@@ -1,14 +1,11 @@
import { Android } from '@expo/eas-build-job';
import {
AndroidGenericJobInput,
AndroidManagedBuildType,
AndroidManagedJobInput,
} from '../../graphql/generated';
import { transformProjectArchive } from '../graphql';
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
import { transformProjectArchive, transformWorkflow } from '../graphql';
export function transformGenericJob(job: Android.GenericJob): AndroidGenericJobInput {
export function transformJob(job: Android.Job): AndroidJobInput {
return {
type: transformWorkflow(job.type),
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
@@ -18,29 +15,17 @@ export function transformGenericJob(job: Android.GenericJob): AndroidGenericJobI
cache: job.cache,
gradleCommand: job.gradleCommand,
artifactPath: job.artifactPath,
};
}
export function transformManagedJob(job: Android.ManagedJob): AndroidManagedJobInput {
return {
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
updates: job.updates,
secrets: job.secrets,
builderEnvironment: job.builderEnvironment,
cache: job.cache,
username: job.username,
buildType: transformBuildType(job.buildType),
buildType: job.buildType && transformBuildType(job.buildType),
};
}
function transformBuildType(buildType: Android.ManagedBuildType): AndroidManagedBuildType {
if (buildType === Android.ManagedBuildType.APK) {
return AndroidManagedBuildType.Apk;
} else if (buildType === Android.ManagedBuildType.APP_BUNDLE) {
return AndroidManagedBuildType.AppBundle;
function transformBuildType(buildType: Android.BuildType): AndroidBuildType {
if (buildType === Android.BuildType.APK) {
return AndroidBuildType.Apk;
} else if (buildType === Android.BuildType.APP_BUNDLE) {
return AndroidBuildType.AppBundle;
} else {
return AndroidManagedBuildType.DevelopmentClient;
return AndroidBuildType.DevelopmentClient;
}
}
@@ -1,5 +1,4 @@
import { Android, ArchiveSource, Cache, Job, Workflow, sanitizeJob } from '@expo/eas-build-job';
import { AndroidGenericBuildProfile, AndroidManagedBuildProfile } from '@expo/eas-json';
import { ArchiveSource, Job, sanitizeJob } from '@expo/eas-build-job';
import path from 'path';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
@@ -18,33 +17,9 @@ export async function prepareJobAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData
): Promise<Job> {
if (ctx.buildProfile.workflow === Workflow.GENERIC) {
const partialJob = await prepareGenericJobAsync(ctx, jobData, ctx.buildProfile);
return sanitizeJob(partialJob);
} else if (ctx.buildProfile.workflow === Workflow.MANAGED) {
const partialJob = await prepareManagedJobAsync(ctx, jobData, ctx.buildProfile);
return sanitizeJob(partialJob);
} else {
throw new Error("Unknown workflow. Shouldn't happen");
}
}
interface CommonJobProperties {
platform: Platform.ANDROID;
projectArchive: ArchiveSource;
builderEnvironment: Android.BuilderEnvironment;
cache: Cache;
secrets: {
buildCredentials?: {
keystore: Android.Keystore;
};
};
}
async function prepareJobCommonAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData
): Promise<Partial<CommonJobProperties>> {
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
const projectRootDirectory =
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
const { credentials } = jobData;
const buildCredentials = credentials
? {
@@ -59,8 +34,10 @@ async function prepareJobCommonAsync(
}
: {};
return {
const job = {
type: ctx.workflow,
platform: Platform.ANDROID,
projectRootDirectory,
projectArchive: jobData.projectArchive,
builderEnvironment: {
image: ctx.buildProfile.image,
@@ -77,42 +54,15 @@ async function prepareJobCommonAsync(
secrets: {
...buildCredentials,
},
};
}
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
async function prepareGenericJobAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData,
buildProfile: AndroidGenericBuildProfile
): Promise<Partial<Android.GenericJob>> {
const projectRootDirectory =
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
return {
...(await prepareJobCommonAsync(ctx, jobData)),
type: Workflow.GENERIC,
gradleCommand: buildProfile.gradleCommand,
artifactPath: buildProfile.artifactPath,
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
projectRootDirectory,
};
}
gradleCommand: ctx.buildProfile.gradleCommand,
artifactPath: ctx.buildProfile.artifactPath,
async function prepareManagedJobAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData,
buildProfile: AndroidManagedBuildProfile
): Promise<Partial<Android.ManagedJob>> {
const projectRootDirectory =
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
return {
...(await prepareJobCommonAsync(ctx, jobData)),
type: Workflow.MANAGED,
username,
buildType: buildProfile.buildType,
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
projectRootDirectory,
buildType: ctx.buildProfile.buildType,
};
return sanitizeJob(job);
}
+11 -5
View File
@@ -3,10 +3,13 @@ import { AndroidConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import fs from 'fs-extra';
import { resolveWorkflow } from '../../project/workflow';
import { resolveWorkflowAsync } from '../../project/workflow';
export function readVersionCode(projectDir: string, exp: ExpoConfig): number | undefined {
const workflow = resolveWorkflow(projectDir, Platform.ANDROID);
export async function readVersionCodeAsync(
projectDir: string,
exp: ExpoConfig
): Promise<number | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
const buildGradle = readBuildGradle(projectDir);
const matchResult = buildGradle?.match(/versionCode (.*)/);
@@ -20,8 +23,11 @@ export function readVersionCode(projectDir: string, exp: ExpoConfig): number | u
}
}
export function readVersionName(projectDir: string, exp: ExpoConfig): string | undefined {
const workflow = resolveWorkflow(projectDir, Platform.ANDROID);
export async function readVersionNameAsync(
projectDir: string,
exp: ExpoConfig
): Promise<string | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
const buildGradle = readBuildGradle(projectDir);
const matchResult = buildGradle?.match(/versionName ["'](.*)["']/);
+7
View File
@@ -102,6 +102,13 @@ export async function prepareBuildRequestForPlatformAsync<
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli version.');
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
) {
Log.error(
'EAS Build is down for maintenance, please try again later. Check https://status.expo.dev/ for updates.'
);
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS'
) {
+27 -13
View File
@@ -1,9 +1,11 @@
import { getConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import fs from 'fs-extra';
import path from 'path';
import Log from '../log';
import { resolveWorkflowAsync } from '../project/workflow';
import { promptAsync } from '../prompts';
import { ensureLoggedInAsync } from '../user/actions';
import vcs from '../vcs';
@@ -37,8 +39,10 @@ export async function configureAsync(options: {
options.platform
),
shouldConfigureIos: [RequestedPlatform.All, RequestedPlatform.Ios].includes(options.platform),
hasAndroidNativeProject: await fs.pathExists(path.join(options.projectDir, 'android')),
hasIosNativeProject: await fs.pathExists(path.join(options.projectDir, 'ios')),
hasAndroidNativeProject:
(await resolveWorkflowAsync(options.projectDir, Platform.ANDROID)) === Workflow.GENERIC,
hasIosNativeProject:
(await resolveWorkflowAsync(options.projectDir, Platform.IOS)) === Workflow.GENERIC,
};
Log.newLine();
@@ -59,32 +63,40 @@ export async function configureAsync(options: {
}
}
const MANAGED_DEFAULTS = {
const ANDROID_MANAGED_DEFAULTS = {
release: {
workflow: 'managed',
buildType: 'app-bundle',
},
development: {
workflow: 'managed',
buildType: 'development-client',
distribution: 'internal',
},
};
const ANDROID_GENERIC_DEFAULTS = {
release: {
workflow: 'generic',
gradleCommand: ':app:bundleRelease',
},
development: {
workflow: 'generic',
gradleCommand: ':app:assembleDebug',
distribution: 'internal',
},
};
const IOS_GENERIC_DEFAULTS = {
const IOS_MANAGED_DEFAULTS = {
release: {
workflow: 'generic',
buildType: 'release',
},
development: {
buildType: 'development-client',
distribution: 'internal',
},
};
const IOS_GENERIC_DEFAULTS = {
release: {
schemeBuildConfiguration: 'Release',
},
development: {
workflow: 'generic',
schemeBuildConfiguration: 'Debug',
distribution: 'internal',
},
@@ -121,15 +133,17 @@ export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<v
const easJson = {
builds: {
...existingEasJson,
...existingEasJson?.builds,
...(shouldInitAndroid
? {
android: ctx.hasAndroidNativeProject ? ANDROID_GENERIC_DEFAULTS : MANAGED_DEFAULTS,
android: ctx.hasAndroidNativeProject
? ANDROID_GENERIC_DEFAULTS
: ANDROID_MANAGED_DEFAULTS,
}
: null),
...(shouldInitIOS
? {
ios: ctx.hasIosNativeProject ? IOS_GENERIC_DEFAULTS : MANAGED_DEFAULTS,
ios: ctx.hasIosNativeProject ? IOS_GENERIC_DEFAULTS : IOS_MANAGED_DEFAULTS,
}
: null),
},
+28 -13
View File
@@ -6,6 +6,7 @@ import resolveFrom from 'resolve-from';
import { v4 as uuidv4 } from 'uuid';
import { getProjectAccountName } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
import { findAccountByName } from '../user/Account';
import { Actor } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
@@ -92,9 +93,10 @@ export interface BuildContext<T extends Platform> {
trackingCtx: TrackingContext;
platform: T;
buildProfile: PlatformBuildProfile<T>;
workflow: Workflow;
}
export function createBuildContext<T extends Platform>({
export async function createBuildContextAsync<T extends Platform>({
platform,
easConfig,
commandCtx,
@@ -102,45 +104,58 @@ export function createBuildContext<T extends Platform>({
platform: T;
easConfig: EasConfig;
commandCtx: CommandContext;
}): BuildContext<T> {
}): Promise<BuildContext<T>> {
const buildProfile = easConfig.builds[platform] as PlatformBuildProfile<T> | undefined;
if (!buildProfile) {
throw new Error(`${platform} build profile does not exist`);
}
const workflow = await resolveWorkflowAsync(commandCtx.projectDir, platform);
const accountId = findAccountByName(commandCtx.user.accounts, commandCtx.accountName)?.id;
const devClientProperties = getDevClientEventProperties(platform, commandCtx, buildProfile);
const devClientProperties = getDevClientEventProperties({
platform,
projectDir: commandCtx.projectDir,
buildProfile,
workflow,
});
const trackingCtx = {
tracking_id: uuidv4(),
platform,
...(accountId && { account_id: accountId }),
account_name: commandCtx.accountName,
project_id: commandCtx.projectId,
project_type: buildProfile.workflow,
project_type: workflow,
...devClientProperties,
};
Analytics.logEvent(Event.BUILD_COMMAND, trackingCtx);
return {
commandCtx,
workflow,
trackingCtx,
platform,
buildProfile,
};
}
function getDevClientEventProperties(
platform: Platform,
commandCtx: CommandContext,
buildProfile: AndroidBuildProfile | IosBuildProfile
): Partial<TrackingContext> {
function getDevClientEventProperties({
platform,
projectDir,
buildProfile,
workflow,
}: {
platform: Platform;
projectDir: string;
buildProfile: AndroidBuildProfile | IosBuildProfile;
workflow: Workflow;
}): Partial<TrackingContext> {
let includesDevClient;
const version = tryGetDevClientVersion(commandCtx.projectDir);
if (buildProfile.workflow === Workflow.MANAGED) {
includesDevClient = buildProfile.buildType === 'development-client';
} else if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
const version = tryGetDevClientVersion(projectDir);
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
includesDevClient = Boolean(version && buildProfile.gradleCommand?.includes('Debug'));
} else if (platform === Platform.IOS && 'schemeBuildConfiguration' in buildProfile) {
includesDevClient = Boolean(version && buildProfile.schemeBuildConfiguration === 'Debug');
} else if ('buildType' in buildProfile) {
includesDevClient = buildProfile.buildType === 'development-client';
} else {
includesDevClient = false;
}
+1 -1
View File
@@ -55,7 +55,7 @@ function transformDistribution(distribution: Metadata['distribution']): Distribu
}
}
function transformWorkflow(workflow: Metadata['workflow']): BuildWorkflow {
export function transformWorkflow(workflow: Workflow): BuildWorkflow {
if (workflow === Workflow.GENERIC) {
return BuildWorkflow.Generic;
} else {
@@ -75,7 +75,7 @@ async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig)
async function readExpoPlistAsync(projectDir: string): Promise<IOSConfig.ExpoPlist> {
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
return await readPlistAsync(expoPlistPath);
return (await readPlistAsync(expoPlistPath)) as IOSConfig.ExpoPlist;
}
async function writeExpoPlistAsync(
@@ -15,12 +15,7 @@ import {
jest.mock('fs');
const originalConsoleLog = console.log;
beforeAll(() => {
console.log = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
fs.removeSync(os.tmpdir());
+10 -33
View File
@@ -1,8 +1,5 @@
import { Ios, Job, Metadata, Workflow } from '@expo/eas-build-job';
import { EasConfig } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { IosCredentials } from '../../credentials/ios/types';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
@@ -10,37 +7,26 @@ import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../pro
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { resolveTargetsAsync } from '../../project/ios/target';
import { JobData, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext, CommandContext, createBuildContext } from '../context';
import { BuildContext, CommandContext, createBuildContextAsync } from '../context';
import { transformMetadata } from '../graphql';
import { Platform } from '../types';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { ensureIosCredentialsAsync } from './credentials';
import { transformGenericJob, transformManagedJob } from './graphql';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
export async function prepareIosBuildAsync(
commandCtx: CommandContext,
easConfig: EasConfig
): Promise<() => Promise<string | undefined>> {
const buildCtx = createBuildContext<Platform.IOS>({
const buildCtx = await createBuildContextAsync<Platform.IOS>({
commandCtx,
platform: Platform.IOS,
easConfig,
});
const { buildProfile } = buildCtx;
if (
buildProfile.workflow === Workflow.GENERIC &&
!(await fs.pathExists(path.join(commandCtx.projectDir, 'ios')))
) {
throw new Error(
`"ios" directory not found. If you're trying to build a managed project, set ${chalk.bold(
`builds.ios.${commandCtx.profile}.workflow`
)} in "eas.json" to "managed".`
);
}
if (buildProfile.workflow === Workflow.MANAGED) {
if (buildCtx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(
commandCtx.projectDir,
commandCtx.exp
@@ -87,21 +73,12 @@ export async function prepareIosBuildAsync(
metadata: Metadata
): Promise<BuildResult> => {
const graphqlMetadata = transformMetadata(metadata);
if (job.type === Workflow.GENERIC) {
const graphqlJob = transformGenericJob(job);
return await BuildMutation.createIosGenericBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
} else {
const graphqlJob = transformManagedJob(job);
return await BuildMutation.createIosManagedBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
}
const graphqlJob = transformJob(job);
return await BuildMutation.createIosBuildAsync({
appId,
job: graphqlJob,
metadata: graphqlMetadata,
});
},
});
}
+4 -2
View File
@@ -1,5 +1,5 @@
import { ExpoConfig } from '@expo/config';
import { Workflow } from '@expo/eas-build-job';
import { Platform, Workflow } from '@expo/eas-build-job';
import { IosBuildProfile, VersionAutoIncrement } from '@expo/eas-json';
import Log from '../../log';
@@ -7,6 +7,7 @@ import {
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
warnIfBundleIdentifierDefinedInAppConfigForGenericProject,
} from '../../project/ios/bundleIdentifier';
import { resolveWorkflowAsync } from '../../project/workflow';
import { ConfigureContext } from '../context';
import { isExpoUpdatesInstalled } from '../utils/updates';
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
@@ -35,7 +36,8 @@ export async function validateAndSyncProjectConfigurationAsync({
exp: ExpoConfig;
buildProfile: IosBuildProfile;
}): Promise<void> {
const { workflow, autoIncrement } = buildProfile;
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const { autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement);
if (workflow === Workflow.GENERIC) {
@@ -28,7 +28,7 @@ export async function ensureIosCredentialsAsync(
iosCapabilitiesOptions: {
entitlements: await resolveEntitlementsJsonAsync(
buildCtx.commandCtx.projectDir,
buildCtx.buildProfile.workflow
buildCtx.workflow
),
},
distribution: buildCtx.buildProfile.distribution ?? 'store',
+10 -21
View File
@@ -3,15 +3,15 @@ import nullthrows from 'nullthrows';
import {
DistributionType,
IosGenericJobInput,
IosBuildType,
IosJobInput,
IosJobSecretsInput,
IosManagedBuildType,
IosManagedJobInput,
} from '../../graphql/generated';
import { transformProjectArchive } from '../graphql';
import { transformProjectArchive, transformWorkflow } from '../graphql';
export function transformGenericJob(job: Ios.GenericJob): IosGenericJobInput {
export function transformJob(job: Ios.Job): IosJobInput {
return {
type: transformWorkflow(job.type),
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
@@ -20,22 +20,11 @@ export function transformGenericJob(job: Ios.GenericJob): IosGenericJobInput {
secrets: transformIosSecrets(job.secrets),
builderEnvironment: job.builderEnvironment,
cache: job.cache,
scheme: job.scheme,
buildConfiguration: job.buildConfiguration,
artifactPath: job.artifactPath,
};
}
export function transformManagedJob(job: Ios.ManagedJob): IosManagedJobInput {
return {
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
updates: job.updates,
distribution: job.distribution && transformDistributionType(job.distribution),
secrets: transformIosSecrets(job.secrets),
builderEnvironment: job.builderEnvironment,
cache: job.cache,
buildType: job.buildType && transformBuildType(job.buildType),
username: job.username,
};
@@ -73,10 +62,10 @@ function transformIosSecrets(secrets: {
};
}
function transformBuildType(buildType: Ios.ManagedBuildType): IosManagedBuildType {
if (buildType === Ios.ManagedBuildType.DEVELOPMENT_CLIENT) {
return IosManagedBuildType.DevelopmentClient;
function transformBuildType(buildType: Ios.BuildType): IosBuildType {
if (buildType === Ios.BuildType.DEVELOPMENT_CLIENT) {
return IosBuildType.DevelopmentClient;
} else {
return IosManagedBuildType.Release;
return IosBuildType.Release;
}
}
+23 -79
View File
@@ -1,5 +1,4 @@
import { ArchiveSource, Cache, Ios, Job, Workflow, sanitizeJob } from '@expo/eas-build-job';
import { IosGenericBuildProfile, IosManagedBuildProfile } from '@expo/eas-json';
import { ArchiveSource, Ios, Job, sanitizeJob } from '@expo/eas-build-job';
import path from 'path';
import semver from 'semver';
@@ -20,44 +19,22 @@ export async function prepareJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData
): Promise<Job> {
if (ctx.buildProfile.workflow === Workflow.GENERIC) {
const partialJob = await prepareGenericJobAsync(ctx, jobData, ctx.buildProfile);
return sanitizeJob(partialJob);
} else if (ctx.buildProfile.workflow === Workflow.MANAGED) {
const partialJob = await prepareManagedJobAsync(ctx, jobData, ctx.buildProfile);
return sanitizeJob(partialJob);
} else {
throw new Error("Unknown workflow. Shouldn't happen");
}
}
interface CommonJobProperties {
platform: Platform.IOS;
projectArchive: ArchiveSource;
builderEnvironment: Ios.BuilderEnvironment;
distribution?: Ios.DistributionType;
cache: Cache;
secrets: {
buildCredentials: Ios.BuildCredentials;
environmentSecrets?: Record<string, string>;
};
}
async function prepareJobCommonAsync(
ctx: BuildContext<Platform.IOS>,
{ credentials, projectArchive }: { credentials?: IosCredentials; projectArchive: ArchiveSource }
): Promise<Partial<CommonJobProperties>> {
const buildCredentials: CommonJobProperties['secrets']['buildCredentials'] = {};
if (credentials) {
const targetNames = Object.keys(credentials);
const projectRootDirectory =
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
if (jobData.credentials) {
const targetNames = Object.keys(jobData.credentials);
for (const targetName of targetNames) {
buildCredentials[targetName] = prepareTargetCredentials(credentials[targetName]);
buildCredentials[targetName] = prepareTargetCredentials(jobData.credentials[targetName]);
}
}
return {
const job = {
type: ctx.workflow,
platform: Platform.IOS,
projectArchive,
projectArchive: jobData.projectArchive,
projectRootDirectory,
distribution: ctx.buildProfile.distribution,
builderEnvironment: {
image: resolveImage(ctx),
@@ -76,7 +53,18 @@ async function prepareJobCommonAsync(
secrets: {
buildCredentials,
},
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
scheme: jobData.buildScheme,
buildConfiguration: ctx.buildProfile.schemeBuildConfiguration,
artifactPath: ctx.buildProfile.artifactPath,
buildType: ctx.buildProfile.buildType,
username,
};
return sanitizeJob(job);
}
function resolveImage(ctx: BuildContext<Platform.IOS>): Ios.BuilderEnvironment['image'] {
@@ -99,47 +87,3 @@ function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.Tar
},
};
}
async function prepareGenericJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData,
buildProfile: IosGenericBuildProfile
): Promise<Partial<Ios.GenericJob>> {
const projectRootDirectory =
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
return {
...(await prepareJobCommonAsync(ctx, {
credentials: jobData.credentials,
projectArchive: jobData.projectArchive,
})),
type: Workflow.GENERIC,
scheme: jobData.buildScheme,
buildConfiguration: buildProfile.schemeBuildConfiguration,
artifactPath: buildProfile.artifactPath,
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
projectRootDirectory,
};
}
async function prepareManagedJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData,
buildProfile: IosManagedBuildProfile
): Promise<Partial<Ios.ManagedJob>> {
const projectRootDirectory =
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
return {
...(await prepareJobCommonAsync(ctx, {
credentials: jobData.credentials,
projectArchive: jobData.projectArchive,
})),
type: Workflow.MANAGED,
buildType: buildProfile.buildType,
username,
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
projectRootDirectory,
};
}
+3 -3
View File
@@ -6,7 +6,7 @@ import nullthrows from 'nullthrows';
import semver from 'semver';
import Log from '../../log';
import { resolveWorkflow } from '../../project/workflow';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { readPlistAsync, writePlistAsync } from './plist';
@@ -106,7 +106,7 @@ export async function readShortVersionAsync(
projectDir: string,
exp: ExpoConfig
): Promise<string | undefined> {
const workflow = resolveWorkflow(projectDir, Platform.IOS);
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir);
return infoPlist.CFBundleShortVersionString;
@@ -119,7 +119,7 @@ export async function readBuildNumberAsync(
projectDir: string,
exp: ExpoConfig
): Promise<string | undefined> {
const workflow = resolveWorkflow(projectDir, Platform.IOS);
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir);
return infoPlist.CFBundleVersion;
+12 -13
View File
@@ -1,8 +1,8 @@
import { Metadata } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import { getApplicationId } from '../project/android/applicationId';
import { getBundleIdentifier } from '../project/ios/bundleIdentifier';
import { getApplicationIdAsync } from '../project/android/applicationId';
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
import { getUsername } from '../project/projectUtils';
import { ensureLoggedInAsync } from '../user/actions';
import vcs from '../vcs';
@@ -10,7 +10,7 @@ import {
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
} from './android/UpdatesModule';
import { readVersionCode, readVersionName } from './android/version';
import { readVersionCodeAsync, readVersionNameAsync } from './android/version';
import { BuildContext } from './context';
import {
readChannelSafelyAsync as readIosChannelSafelyAsync,
@@ -37,20 +37,17 @@ export async function collectMetadata<T extends Platform>(
): Promise<Metadata> {
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
return {
// TODO: type error fixed in https://github.com/expo/eas-build/pull/34
// remove @ts-expect-error after upgrading @expo/eas-build-job
// @ts-expect-error error TS2322: not assignable to type 'Record<string, string | number>'
trackingContext: ctx.trackingCtx,
appVersion: await resolveAppVersionAsync(ctx),
appBuildVersion: await resolveAppBuildVersionAsync(ctx),
cliVersion: packageJSON.version,
workflow: ctx.buildProfile.workflow,
workflow: ctx.workflow,
credentialsSource,
sdkVersion: ctx.commandCtx.exp.sdkVersion,
...channelOrReleaseChannel,
distribution: ctx.buildProfile.distribution ?? 'store',
appName: ctx.commandCtx.exp.name,
appIdentifier: resolveAppIdentifier(ctx),
appIdentifier: await resolveAppIdentifierAsync(ctx),
buildProfile: ctx.commandCtx.profile,
gitCommitHash: await vcs.getCommitHashAsync(),
username: getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync()),
@@ -63,7 +60,7 @@ async function resolveAppVersionAsync<T extends Platform>(
if (ctx.platform === Platform.IOS) {
return await readShortVersionAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
} else {
return readVersionName(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return await readVersionNameAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
}
}
@@ -73,16 +70,18 @@ async function resolveAppBuildVersionAsync<T extends Platform>(
if (ctx.platform === Platform.IOS) {
return await readBuildNumberAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
} else {
const versionCode = readVersionCode(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
const versionCode = await readVersionCodeAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return versionCode !== undefined ? String(versionCode) : undefined;
}
}
function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string {
async function resolveAppIdentifierAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<string> {
if (ctx.platform === Platform.IOS) {
return getBundleIdentifier(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return await getBundleIdentifierAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
} else {
return getApplicationId(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return await getApplicationIdAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
}
}
@@ -85,7 +85,7 @@ function printBuildResult(accountName: string, build: BuildFragment): void {
account: accountName,
});
const installUrl = getInternalDistributionInstallUrl(build);
qrcodeTerminal.generate(installUrl, code => console.log(`${indentString(code, 2)}\n`));
qrcodeTerminal.generate(installUrl, code => Log.log(`${indentString(code, 2)}\n`));
Log.log(
`${appPlatformEmojis[build.platform]} Open this link on your ${
appPlatformDisplayNames[build.platform]
@@ -22,7 +22,7 @@ import { promptAsync, toggleConfirmAsync } from '../../prompts';
async function getBranchInfoAsync({
appId,
name,
}: GetBranchInfoQueryVariables): Promise<{ branchId: string }> {
}: GetBranchInfoQueryVariables): Promise<GetBranchInfoQuery> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<GetBranchInfoQuery, GetBranchInfoQueryVariables>(
@@ -46,11 +46,7 @@ async function getBranchInfoAsync({
)
.toPromise()
);
const branchId = data.app?.byId.updateBranchByName?.id;
if (!branchId) {
throw new Error(`Could not find branch ${name} on ${appId}`);
}
return { branchId };
return data;
}
async function deleteBranchOnAppAsync({
@@ -122,7 +118,11 @@ export default class BranchDelete extends Command {
}));
}
const { branchId } = await getBranchInfoAsync({ appId: projectId, name });
const data = await getBranchInfoAsync({ appId: projectId, name });
const branchId = data.app?.byId.updateBranchByName?.id;
if (!branchId) {
throw new Error(`Could not find branch ${name} on ${fullName}`);
}
if (!jsonFlag) {
Log.addNewLineIfNone();
@@ -100,6 +100,8 @@ export default class BranchList extends Command {
}),
])
);
Log.addNewLineIfNone();
Log.log(chalk.bold('Branches with their most recent update group:'));
Log.log(table.toString());
if (branches.length >= BRANCHES_LIMIT) {
+10 -36
View File
@@ -3,7 +3,6 @@ import { getRuntimeVersionForSDKVersion } from '@expo/sdk-runtime-versions';
import { Command, flags } from '@oclif/command';
import assert from 'assert';
import chalk from 'chalk';
import Table from 'cli-table3';
import dateFormat from 'dateformat';
import gql from 'graphql-tag';
import { uniqBy } from 'lodash';
@@ -19,11 +18,7 @@ import {
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import Log from '../../log';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import {
PublishPlatform,
buildBundlesAsync,
@@ -33,6 +28,7 @@ import {
} from '../../project/publish';
import { promptAsync, selectAsync } from '../../prompts';
import { formatUpdate } from '../../update/utils';
import formatFields from '../../utils/formatFields';
import vcs from '../../vcs';
import { listBranchesAsync } from './list';
import { viewUpdateBranchAsync } from './view';
@@ -135,8 +131,7 @@ export default class BranchPublish extends Command {
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = await getProjectAccountNameAsync(exp);
let { slug, runtimeVersion, sdkVersion } = exp;
let { runtimeVersion, sdkVersion } = exp;
// When a SDK version is supplied instead of a runtime version and we're in the managed workflow
// construct the runtimeVersion with special meaning indicating that the runtime is an
@@ -307,35 +302,14 @@ export default class BranchPublish extends Command {
if (jsonFlag) {
Log.log(JSON.stringify(newUpdateGroup));
} else {
const outputMessage = new Table({
wordWrap: true,
chars: {
top: '',
'top-mid': '',
'top-left': '',
'top-right': '',
bottom: '',
'bottom-mid': '',
'bottom-left': '',
'bottom-right': '',
left: '',
'left-mid': '',
mid: '',
'mid-mid': '',
right: '',
'right-mid': '',
middle: ' ',
},
style: { 'padding-left': 0, 'padding-right': 0 },
});
outputMessage.push(
[chalk.dim('project'), `@${accountName}/${slug}`],
[chalk.dim('branch'), name],
[chalk.dim('runtimeVersion'), runtimeVersion],
[chalk.dim('groupID'), newUpdateGroup.group],
[chalk.dim('message'), message]
Log.log(
formatFields([
{ label: 'branch', value: name },
{ label: 'runtime version', value: runtimeVersion },
{ label: 'update group ID', value: newUpdateGroup.group },
{ label: 'message', value: message },
])
);
Log.log(outputMessage.toString());
}
}
}
+11 -11
View File
@@ -16,13 +16,10 @@ import {
ViewBranchQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
import formatFields from '../../utils/formatFields';
const PAGE_LIMIT = 10_000;
@@ -121,7 +118,6 @@ export default class BranchView extends Command {
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
if (!name) {
@@ -168,12 +164,16 @@ export default class BranchView extends Command {
]);
}
Log.withTick(
`️Branch: ${chalk.bold(UpdateBranch.name)} on project ${chalk.bold(
fullName
)}. Branch ID: ${chalk.bold(UpdateBranch.id)}`
Log.addNewLineIfNone();
Log.log(chalk.bold('Branch:'));
Log.log(
formatFields([
{ label: 'Name', value: UpdateBranch.name },
{ label: 'ID', value: UpdateBranch.id },
])
);
Log.log(chalk.bold('Recent update groups published on this branch:'));
Log.addNewLineIfNone();
Log.log(chalk.bold('Recently published update groups:'));
Log.log(groupTable.toString());
}
}
+48 -7
View File
@@ -1,4 +1,5 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { EasJsonReader } from '@expo/eas-json';
import { flags } from '@oclif/command';
import chalk from 'chalk';
import fs from 'fs-extra';
@@ -87,7 +88,7 @@ export default class Build extends EasCommand {
return;
}
exp = (await ensureProjectConfiguredAsync(projectDir)) ?? exp;
exp = (await ensureProjectConfiguredAsync(projectDir, platform)) ?? exp;
const commandCtx = await createCommandContextAsync({
requestedPlatform: platform,
@@ -101,6 +102,7 @@ export default class Build extends EasCommand {
skipProjectConfiguration: flags['skip-project-configuration'],
waitForBuildEnd: flags.wait,
});
await buildAsync(commandCtx);
}
}
@@ -140,17 +142,29 @@ async function promptForPlatformAsync(): Promise<RequestedPlatform> {
return platform;
}
async function ensureProjectConfiguredAsync(projectDir: string): Promise<ExpoConfig | null> {
if (await fs.pathExists(path.join(projectDir, 'eas.json'))) {
async function ensureProjectConfiguredAsync(
projectDir: string,
platform: RequestedPlatform
): Promise<ExpoConfig | null> {
const platformsToConfigure = await getPlatformsToConfigureAsync(projectDir, platform);
if (!platformsToConfigure) {
return null;
}
const confirm = await confirmAsync({
message: 'This app is not set up for building with EAS. Set it up now?',
});
// Ensure the prompt is consistent with the platforms we need to configure
let message = 'This project is not configured to build with EAS. Set it up now?';
if (platformsToConfigure === RequestedPlatform.Ios) {
message = 'Your iOS project is not configured to build with EAS. Set it up now?';
} else if (platformsToConfigure === RequestedPlatform.Android) {
message = 'Your Android project is not configured to build with EAS. Set it up now?';
}
const confirm = await confirmAsync({ message });
if (confirm) {
await configureAsync({
projectDir,
platform: RequestedPlatform.All,
platform: platformsToConfigure,
});
if (await vcs.hasUncommittedChangesAsync()) {
throw new Error(
@@ -167,3 +181,30 @@ async function ensureProjectConfiguredAsync(projectDir: string): Promise<ExpoCon
);
}
}
async function getPlatformsToConfigureAsync(
projectDir: string,
platform: RequestedPlatform
): Promise<RequestedPlatform | null> {
if (!(await fs.pathExists(path.join(projectDir, 'eas.json')))) {
return platform;
}
const easConfig = await new EasJsonReader(projectDir, platform).readRawAsync();
if (platform === RequestedPlatform.All) {
if (easConfig.builds?.android && easConfig.builds?.ios) {
return null;
} else if (easConfig.builds?.ios) {
return RequestedPlatform.Android;
} else if (easConfig.builds?.android) {
return RequestedPlatform.Ios;
}
} else if (
(platform === RequestedPlatform.Android || platform === RequestedPlatform.Ios) &&
easConfig.builds?.[platform]
) {
return null;
}
return platform;
}
@@ -16,6 +16,7 @@ import {
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import formatFields from '../../utils/formatFields';
import { createUpdateBranchOnAppAsync } from '../branch/create';
async function createUpdateChannelOnAppAsync({
@@ -87,7 +88,6 @@ export default class ChannelCreate extends Command {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
if (!channelName) {
@@ -140,10 +140,26 @@ export default class ChannelCreate extends Command {
return;
}
Log.addNewLineIfNone();
Log.withTick(
`️Created a new channel ${chalk.bold(newChannel.name)} on project ${chalk.bold(
fullName
)}. ${branchMessage} and have pointed the channel at it. You can now update your app by publishing!`
`Created a new channel on project ${chalk.bold(await getProjectFullNameAsync(exp))}`
);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: newChannel.id },
])
);
Log.addNewLineIfNone();
Log.withTick(`${branchMessage} and have pointed the channel at it.`);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: branchId },
])
);
Log.addNewLineIfNone();
Log.log(chalk.bold('You can now update your app by publishing!'));
}
}
@@ -146,13 +146,6 @@ export default class ChannelEdit extends Command {
'There is a rollout in progress. Please manage it with "channel:rollout" instead.'
);
}
const existingBranch = existingChannel.updateBranches[0];
Log.addNewLineIfNone();
Log.log(
chalk`Channel {bold ${existingChannel.name}} is currently set to branch {bold ${existingBranch.name}}`
);
Log.addNewLineIfNone();
if (!branchName) {
const validationMessage = 'branch name may not be empty.';
@@ -185,6 +178,7 @@ export default class ChannelEdit extends Command {
Log.withTick(
chalk`Channel {bold ${channel.name}} is now set to branch {bold ${branch.name}}.\n`
);
Log.addNewLineIfNone();
Log.log(
chalk`Users with builds on channel {bold ${channel.name}} will now receive the active update on {bold ${branch.name}}.`
);
@@ -1,6 +1,5 @@
import { getConfig } from '@expo/config';
import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
@@ -10,6 +9,7 @@ import {
} from '../../graphql/generated';
import Log from '../../log';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import formatFields from '../../utils/formatFields';
import { logChannelDetails } from './view';
const CHANNEL_LIMIT = 10_000;
@@ -99,9 +99,14 @@ export default class ChannelList extends Command {
return;
}
Log.log(); // spacing
for (const channel of channels) {
Log.log(`Details of channel ${chalk.bold(chalk.cyan(channel.name))}:`);
Log.addNewLineIfNone();
Log.log(
formatFields([
{ label: 'Name', value: channel.name },
{ label: 'ID', value: channel.id },
])
);
logChannelDetails(channel);
}
}
+10 -8
View File
@@ -11,11 +11,7 @@ import {
GetChannelByNameForAppQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import {
FormatUpdateParameter,
@@ -23,6 +19,7 @@ import {
formatUpdate,
getPlatformsForGroup,
} from '../../update/utils';
import formatFields from '../../utils/formatFields';
export type BranchMapping = {
version: number;
@@ -220,7 +217,6 @@ export default class ChannelView extends Command {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
if (!channelName) {
@@ -250,9 +246,15 @@ export default class ChannelView extends Command {
return;
}
Log.withTick(
chalk`Channel: {bold ${channel.name}} on project {bold ${fullName}}. Channel ID: {bold ${channel.id}}`
Log.addNewLineIfNone();
Log.log(chalk.bold('Channel:'));
Log.log(
formatFields([
{ label: 'Name', value: channel.name },
{ label: 'ID', value: channel.id },
])
);
Log.addNewLineIfNone();
Log.log(
chalk`{bold Branches, pointed at by this channel, and their most recent update group:}`
);
@@ -45,7 +45,6 @@ If you are not sure what is the UDID of the device you are looking for, run:
spinner.fail(`Couldn't find a device with the UDID ${UDID}`);
}
} catch (e) {
console.log(e);
spinner.fail(`Something went wrong and we couldn't fetch the device with UDID ${UDID}`);
throw e;
}
@@ -0,0 +1,57 @@
import { getConfig } from '@expo/config';
import { Command } from '@oclif/command';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { AppInfoQuery, AppInfoQueryVariables } from '../../graphql/generated';
import Log from '../../log';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import formatFields from '../../utils/formatFields';
async function projectInfoByIdAsync(appId: string): Promise<AppInfoQuery> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppInfoQuery, AppInfoQueryVariables>(
gql`
query AppInfo($appId: String!) {
app {
byId(appId: $appId) {
id
fullName
}
}
}
`,
{ appId }
)
.toPromise()
);
return data;
}
export default class ProjectInfo extends Command {
static hidden = true;
static description = 'information about the current project';
async run() {
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const { app } = await projectInfoByIdAsync(projectId);
if (!app) {
throw new Error(`Could not find project with ID: ${projectId}`);
}
Log.addNewLineIfNone();
Log.log(
formatFields([
{ label: 'ID', value: projectId },
{ label: 'fullName', value: app.byId.fullName },
])
);
}
}
@@ -2,12 +2,13 @@ import { getConfig } from '@expo/config';
import { Command } from '@oclif/command';
import chalk from 'chalk';
import Log from '../log';
import { findProjectRootAsync, setProjectIdAsync } from '../project/projectUtils';
import Log from '../../log';
import { findProjectRootAsync, setProjectIdAsync } from '../../project/projectUtils';
export default class InitView extends Command {
export default class ProjectInit extends Command {
static hidden = true;
static description = 'Create or link an EAS project';
static description = 'create or link an EAS project';
static aliases = ['init'];
async run() {
const projectDir = await findProjectRootAsync(process.cwd());
@@ -3,7 +3,7 @@ import { nanoid } from 'nanoid';
import ora from 'ora';
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
import { getApplicationId } from '../../../project/android/applicationId';
import { getApplicationIdAsync } from '../../../project/android/applicationId';
import { getProjectAccountName, getProjectConfigDescription } from '../../../project/projectUtils';
import { promptAsync } from '../../../prompts';
import { findAccountByName } from '../../../user/Account';
@@ -86,7 +86,7 @@ export async function promptUserAndCopyLegacyCredentialsAsync(
spinner.succeed('Credentials copied to EAS.');
}
export function getAppLookupParamsFromContext(ctx: Context): AppLookupParams {
export async function getAppLookupParamsFromContextAsync(ctx: Context): Promise<AppLookupParams> {
ctx.ensureProjectContext();
const projectName = ctx.exp.slug;
const accountName = getProjectAccountName(ctx.exp, ctx.user);
@@ -95,7 +95,7 @@ export function getAppLookupParamsFromContext(ctx: Context): AppLookupParams {
throw new Error(`You do not have access to account: ${accountName}`);
}
const androidApplicationIdentifier = getApplicationId(ctx.projectDir, ctx.exp);
const androidApplicationIdentifier = await getApplicationIdAsync(ctx.projectDir, ctx.exp);
if (!androidApplicationIdentifier) {
throw new Error(
`android.package needs to be defined in your ${getProjectConfigDescription(
@@ -1,14 +1,14 @@
import { testLegacyAndroidFcmFragment } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { AssignFcm } from '../AssignFcm';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
describe(AssignFcm, () => {
it('assigns an fcm api key in Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: false,
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const assignFcmAction = new AssignFcm(appLookupParams);
await assignFcmAction.runAsync(ctx, testLegacyAndroidFcmFragment);
@@ -22,7 +22,7 @@ describe(AssignFcm, () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const assignFcmAction = new AssignFcm(appLookupParams);
// dont fail if users are running in non-interactive mode
@@ -10,24 +10,13 @@ import {
import { createCtxMock } from '../../../__tests__/fixtures-context';
import {
canCopyLegacyCredentialsAsync,
getAppLookupParamsFromContext,
getAppLookupParamsFromContextAsync,
promptUserAndCopyLegacyCredentialsAsync,
} from '../BuildCredentialsUtils';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
describe('BuildCredentialsUtils', () => {
describe(canCopyLegacyCredentialsAsync, () => {
it('returns true if the user has legacy credentials and no modern ones', async () => {
@@ -41,7 +30,7 @@ describe('BuildCredentialsUtils', () => {
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, appLookupParams);
expect(canCopyLegacyCredentials).toBe(true);
});
@@ -58,7 +47,7 @@ describe('BuildCredentialsUtils', () => {
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, appLookupParams);
expect(canCopyLegacyCredentials).toBe(false);
});
@@ -71,7 +60,7 @@ describe('BuildCredentialsUtils', () => {
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(() => null),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, appLookupParams);
expect(canCopyLegacyCredentials).toBe(false);
});
@@ -86,7 +75,7 @@ describe('BuildCredentialsUtils', () => {
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, appLookupParams);
expect(canCopyLegacyCredentials).toBe(true);
});
@@ -109,7 +98,7 @@ describe('BuildCredentialsUtils', () => {
createFcmAsync: jest.fn(() => testLegacyAndroidFcmFragment),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
await promptUserAndCopyLegacyCredentialsAsync(ctx, appLookupParams);
expect(
@@ -131,7 +120,7 @@ describe('BuildCredentialsUtils', () => {
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
await expect(
promptUserAndCopyLegacyCredentialsAsync(ctx, appLookupParams)
).rejects.toThrowError();
@@ -1,6 +1,6 @@
import { promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateFcm } from '../CreateFcm';
jest.mock('../../../../prompts');
@@ -9,7 +9,7 @@ jest.mock('../../../../prompts');
describe(CreateFcm, () => {
it('creates an fcm api key in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createFcmAction = new CreateFcm(appLookupParams.account);
await createFcmAction.runAsync(ctx);
@@ -18,7 +18,7 @@ describe(CreateFcm, () => {
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createFcmAction = new CreateFcm(appLookupParams.account);
// fail if users are running in non-interactive mode
@@ -1,6 +1,6 @@
import { confirmAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateKeystore } from '../CreateKeystore';
jest.mock('../../../../prompts');
@@ -9,7 +9,7 @@ jest.mock('../../../../prompts');
describe('CreateKeystore', () => {
it('creates a keystore in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createKeystoreAction = new CreateKeystore(appLookupParams.account);
await createKeystoreAction.runAsync(ctx);
@@ -18,7 +18,7 @@ describe('CreateKeystore', () => {
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createKeystoreAction = new CreateKeystore(appLookupParams.account);
// fail if users are running in non-interactive mode
@@ -2,28 +2,18 @@ import fs from 'fs-extra';
import { testAndroidBuildCredentialsFragment } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { DownloadKeystore } from '../DownloadKeystore';
jest.mock('fs-extra');
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
describe(DownloadKeystore, () => {
it('downloads a keystore', async () => {
const fsWriteFileSpy = jest.spyOn(fs, 'writeFile');
const ctx = createCtxMock({
nonInteractive: false,
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const downloadKeystoreAction = new DownloadKeystore({ app: appLookupParams });
await downloadKeystoreAction.runAsync(ctx, testAndroidBuildCredentialsFragment);
expect(fsWriteFileSpy as any).toHaveBeenCalledTimes(1);
@@ -36,7 +26,7 @@ describe(DownloadKeystore, () => {
});
it('works in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const downloadKeystoreAction = new DownloadKeystore({ app: appLookupParams });
await expect(
downloadKeystoreAction.runAsync(ctx, testAndroidBuildCredentialsFragment)
@@ -4,22 +4,12 @@ import {
testAndroidAppCredentialsFragment,
} from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { RemoveFcm } from '../RemoveFcm';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
describe(RemoveFcm, () => {
it('removes an FCM Api Key', async () => {
const ctx = createCtxMock({
@@ -31,14 +21,14 @@ describe(RemoveFcm, () => {
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const removeFcmApiKeyAction = new RemoveFcm(appLookupParams);
await removeFcmApiKeyAction.runAsync(ctx);
expect(ctx.android.deleteFcmAsync as any).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const removeFcmApiKeyAction = new RemoveFcm(appLookupParams);
await expect(removeFcmApiKeyAction.runAsync(ctx)).rejects.toThrowError();
});
@@ -1,36 +1,26 @@
import { confirmAsync } from '../../../../prompts';
import { testAndroidBuildCredentialsFragment } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { RemoveKeystore } from '../RemoveKeystore';
jest.mock('fs-extra');
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
describe(RemoveKeystore, () => {
it('removes a keystore', async () => {
const ctx = createCtxMock({
nonInteractive: false,
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const removeKeystoreAction = new RemoveKeystore(appLookupParams);
await removeKeystoreAction.runAsync(ctx, testAndroidBuildCredentialsFragment);
expect(ctx.android.deleteKeystoreAsync as any).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const removeKeystoreAction = new RemoveKeystore(appLookupParams);
await expect(
removeKeystoreAction.runAsync(ctx, testAndroidBuildCredentialsFragment)
@@ -6,23 +6,12 @@ import {
} from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetupBuildCredentials } from '../SetupBuildCredentials';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
describe('SetupBuildCredentials', () => {
it('skips setup when there are prior credentials', async () => {
const ctx = createCtxMock({
@@ -34,7 +23,7 @@ describe('SetupBuildCredentials', () => {
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
await setupBuildCredentialsAction.runAsync(ctx);
@@ -50,7 +39,7 @@ describe('SetupBuildCredentials', () => {
createKeystoreAsync: jest.fn(() => testJksAndroidKeystoreFragment),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
await setupBuildCredentialsAction.runAsync(ctx);
@@ -65,7 +54,7 @@ describe('SetupBuildCredentials', () => {
...getNewAndroidApiMock(),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
await expect(setupBuildCredentialsAction.runAsync(ctx)).rejects.toThrowError(
MissingCredentialsNonInteractiveError
@@ -12,7 +12,7 @@ import {
SelectAndroidBuildCredentials,
SelectAndroidBuildCredentialsResultType,
} from '../../../manager/SelectAndroidBuildCredentials';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetupBuildCredentialsFromCredentialsJson } from '../SetupBuildCredentialsFromCredentialsJson';
jest.mock('fs');
@@ -21,16 +21,6 @@ jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
jest.mock('../../../manager/SelectAndroidBuildCredentials');
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
beforeEach(() => {
vol.reset();
});
@@ -67,7 +57,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
}),
'keystore.jks': 'some-binary-content',
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
appLookupParams
);
@@ -117,7 +107,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
}),
'keystore.jks': 'some-binary-content',
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
appLookupParams
);
@@ -140,7 +130,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
appLookupParams
);
@@ -3,7 +3,7 @@ import mockdate from 'mockdate';
import Log from '../../../../log';
import { testAndroidAppCredentialsFragment } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContext } from '../../actions/BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../../actions/BuildCredentialsUtils';
import { displayAndroidAppCredentials } from '../printCredentials';
jest.mock('../../../../log');
@@ -14,7 +14,7 @@ mockdate.set(new Date('4/20/2021'));
describe('print credentials', () => {
it('prints the AndroidAppCredentials fragment', async () => {
const ctx = createCtxMock();
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
displayAndroidAppCredentials({
appLookupParams,
appCredentials: testAndroidAppCredentialsFragment,
@@ -22,22 +22,6 @@ import { updateAndroidCredentialsAsync, updateIosCredentialsAsync } from '../upd
jest.mock('fs');
jest.mock('prompts');
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
const originalConsoleError = console.error;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
console.error = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
console.error = originalConsoleError;
});
beforeEach(() => {
vol.reset();
asMock(prompts).mockReset();
@@ -29,20 +29,6 @@ jest.mock('../validators/validateProvisioningProfile', () => ({
}));
jest.mock('../../../project/ios/bundleIdentifier');
const originalConsoleLog = console.log;
const originalConsoleError = console.error;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.error = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.error = originalConsoleError;
console.warn = originalConsoleWarn;
});
beforeEach(() => {
vol.reset();
});
@@ -12,14 +12,6 @@ import { RemoveDistributionCertificate } from '../RemoveDistributionCertificate'
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
const originalConsoleLog = console.log;
beforeAll(() => {
console.log = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
});
describe('RemoveDistributionCertificate', () => {
it('deletes the distribution certificate on Expo and Apple servers when there are no App Dependencies in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
@@ -8,14 +8,6 @@ import { RemovePushKey } from '../RemovePushKey';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
const originalConsoleLog = console.log;
beforeAll(() => {
console.log = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
});
describe(RemovePushKey, () => {
it('deletes the push key on Expo and Apple servers in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
@@ -13,14 +13,6 @@ jest.mock('../SetupAdhocProvisioningProfile');
jest.mock('../SetupProvisioningProfile');
jest.mock('../BuildCredentialsUtils', () => ({ getAllBuildCredentialsAsync: jest.fn() }));
const originalConsoleLog = console.log;
beforeAll(() => {
console.log = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
});
beforeEach(() => {
asMock(promptAsync).mockReset();
@@ -9,14 +9,6 @@ import { SetupPushKey } from '../SetupPushKey';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
const originalConsoleLog = console.log;
beforeAll(() => {
console.log = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
});
describe(SetupPushKey, () => {
it('skips setting up a Push Key if it is already configured', async () => {
const ctx = createCtxMock({
@@ -20,17 +20,6 @@ import { SetupTargetBuildCredentialsFromCredentialsJson } from '../SetupTargetBu
jest.mock('../../../../prompts');
jest.mock('../../../credentialsJson/read');
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
beforeEach(() => {
asMock(confirmAsync).mockReset();
(confirmAsync as jest.Mock).mockImplementation(() => true);
@@ -1,12 +1,17 @@
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
import assert from 'assert';
import Log, { learnMore } from '../../log';
import {
getProjectAccountName,
promptToCreateProjectIfNotExistsAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { findAccountByName } from '../../user/Account';
import { ensureActorHasUsername } from '../../user/actions';
import { AssignFcm } from '../android/actions/AssignFcm';
import {
canCopyLegacyCredentialsAsync,
getAppLookupParamsFromContext,
getAppLookupParamsFromContextAsync,
promptUserAndCopyLegacyCredentialsAsync,
} from '../android/actions/BuildCredentialsUtils';
import { CreateFcm } from '../android/actions/CreateFcm';
@@ -147,7 +152,13 @@ export class ManageAndroid {
throw new Error(`You do not have access to account: ${accountName}`);
}
if (ctx.hasProjectContext) {
const appLookupParams = getAppLookupParamsFromContext(ctx);
const maybeProjectId = await promptToCreateProjectIfNotExistsAsync(ctx.exp);
if (!maybeProjectId) {
throw new Error(
'Your project must be registered with EAS in order to use the credentials manager.'
);
}
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(
appLookupParams
);
@@ -196,61 +207,74 @@ export class ManageAndroid {
return await this.callingAction.runAsync(ctx);
}
}
const appLookupParams = getAppLookupParamsFromContext(ctx);
if (chosenAction === ActionType.CreateKeystore) {
const selectBuildCredentialsResult = await new SelectAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
const keystore = await new CreateKeystore(appLookupParams.account).runAsync(ctx);
if (
selectBuildCredentialsResult.resultType ===
SelectAndroidBuildCredentialsResultType.CREATE_REQUEST
) {
await ctx.android.createAndroidAppBuildCredentialsAsync(appLookupParams, {
...selectBuildCredentialsResult.result,
androidKeystoreId: keystore.id,
});
} else {
await ctx.android.updateAndroidAppBuildCredentialsAsync(
selectBuildCredentialsResult.result,
{
androidKeystoreId: keystore.id,
}
);
}
} else if (chosenAction === ActionType.DownloadKeystore) {
const buildCredentials = await new SelectExistingAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
if (buildCredentials) {
await new DownloadKeystore({ app: appLookupParams }).runAsync(ctx, buildCredentials);
}
} else if (chosenAction === ActionType.RemoveKeystore) {
const buildCredentials = await new SelectExistingAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
if (buildCredentials) {
await new RemoveKeystore(appLookupParams).runAsync(ctx, buildCredentials);
}
} else if (chosenAction === ActionType.CreateFcm) {
const fcm = await new CreateFcm(appLookupParams.account).runAsync(ctx);
await new AssignFcm(appLookupParams).runAsync(ctx, fcm);
} else if (chosenAction === ActionType.RemoveFcm) {
await new RemoveFcm(appLookupParams).runAsync(ctx);
} else if (chosenAction === ActionType.UpdateCredentialsJson) {
const buildCredentials = await new SelectExistingAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
if (buildCredentials) {
await new UpdateCredentialsJson().runAsync(ctx, buildCredentials);
}
} else if (chosenAction === ActionType.SetupBuildCredentialsFromCredentialsJson) {
await new SetupBuildCredentialsFromCredentialsJson(appLookupParams).runAsync(ctx);
}
await this.runProjectSpecificActionAsync(ctx, chosenAction);
} catch (err) {
Log.error(err);
if (process.env.DEBUG) {
throw err; // breaks out of the loop so we can get stack trace
}
Log.error(`Cryptic error? ${learnMore('https://expo.fyi/cryptic-error-eas')}`);
}
await new PressAnyKeyToContinue().runAsync();
}
}
private async runProjectSpecificActionAsync(ctx: Context, action: ActionType): Promise<void> {
assert(
ctx.hasProjectContext,
'You must be in your project directory in order to perform this action'
);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
if (action === ActionType.CreateKeystore) {
const selectBuildCredentialsResult = await new SelectAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
const keystore = await new CreateKeystore(appLookupParams.account).runAsync(ctx);
if (
selectBuildCredentialsResult.resultType ===
SelectAndroidBuildCredentialsResultType.CREATE_REQUEST
) {
await ctx.android.createAndroidAppBuildCredentialsAsync(appLookupParams, {
...selectBuildCredentialsResult.result,
androidKeystoreId: keystore.id,
});
} else {
await ctx.android.updateAndroidAppBuildCredentialsAsync(
selectBuildCredentialsResult.result,
{
androidKeystoreId: keystore.id,
}
);
}
} else if (action === ActionType.DownloadKeystore) {
const buildCredentials = await new SelectExistingAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
if (buildCredentials) {
await new DownloadKeystore({ app: appLookupParams }).runAsync(ctx, buildCredentials);
}
} else if (action === ActionType.RemoveKeystore) {
const buildCredentials = await new SelectExistingAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
if (buildCredentials) {
await new RemoveKeystore(appLookupParams).runAsync(ctx, buildCredentials);
}
} else if (action === ActionType.CreateFcm) {
const fcm = await new CreateFcm(appLookupParams.account).runAsync(ctx);
await new AssignFcm(appLookupParams).runAsync(ctx, fcm);
} else if (action === ActionType.RemoveFcm) {
await new RemoveFcm(appLookupParams).runAsync(ctx);
} else if (action === ActionType.UpdateCredentialsJson) {
const buildCredentials = await new SelectExistingAndroidBuildCredentials(
appLookupParams
).runAsync(ctx);
if (buildCredentials) {
await new UpdateCredentialsJson().runAsync(ctx, buildCredentials);
}
} else if (action === ActionType.SetupBuildCredentialsFromCredentialsJson) {
await new SetupBuildCredentialsFromCredentialsJson(appLookupParams).runAsync(ctx);
}
}
}
@@ -7,10 +7,13 @@ import {
IosAppBuildCredentialsFragment,
IosDistributionType as IosDistributionTypeGraphql,
} from '../../graphql/generated';
import Log from '../../log';
import Log, { learnMore } from '../../log';
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { resolveTargetsAsync } from '../../project/ios/target';
import { getProjectAccountName } from '../../project/projectUtils';
import {
getProjectAccountName,
promptToCreateProjectIfNotExistsAsync,
} from '../../project/projectUtils';
import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
import { Account, findAccountByName } from '../../user/Account';
import { ensureActorHasUsername } from '../../user/actions';
@@ -235,11 +238,15 @@ export class ManageIos {
return await this.callingAction.runAsync(ctx);
}
} else if (actionInfo.scope === Scope.Project) {
assert(
ctx.hasProjectContext,
'You must be in your project directory in order to perform this action'
);
await this.runProjectSpecificActionAsync(
ctx,
nullthrows(app),
nullthrows(targets),
nullthrows(buildProfile),
nullthrows(app, 'app must be defined in project context'),
nullthrows(targets, 'targets must be defined in project context'),
nullthrows(buildProfile, 'buildProfile must be defined in project context'),
chosenAction
);
} else if (actionInfo.scope === Scope.Account) {
@@ -249,6 +256,10 @@ export class ManageIos {
}
} catch (err) {
Log.error(err);
if (process.env.DEBUG) {
throw err; // breaks out of the loop so we can get stack trace
}
Log.error(`Cryptic error? ${learnMore('https://expo.fyi/cryptic-error-eas')}`);
}
await new PressAnyKeyToContinue().runAsync();
}
@@ -266,6 +277,13 @@ export class ManageIos {
};
}
const maybeProjectId = await promptToCreateProjectIfNotExistsAsync(ctx.exp);
if (!maybeProjectId) {
throw new Error(
'Your project must be registered with EAS in order to use the credentials manager.'
);
}
const app = { account, projectName: ctx.exp.slug };
const easJsonReader = new EasJsonReader(ctx.projectDir, 'ios');
const easConfig = await new SelectBuildProfileFromEasJson(easJsonReader).runAsync(ctx);
@@ -4,7 +4,7 @@ import {
testAndroidBuildCredentialsFragment,
} from '../../__tests__/fixtures-android';
import { createCtxMock } from '../../__tests__/fixtures-context';
import { getAppLookupParamsFromContext } from '../../android/actions/BuildCredentialsUtils';
import { getAppLookupParamsFromContextAsync } from '../../android/actions/BuildCredentialsUtils';
import {
SelectAndroidBuildCredentials,
SelectAndroidBuildCredentialsResultType,
@@ -28,7 +28,7 @@ describe(SelectAndroidBuildCredentials, () => {
getAndroidAppBuildCredentialsListAsync: jest.fn(() => []),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const selectAndroidBuildCredentialsAction = new SelectAndroidBuildCredentials(appLookupParams);
const buildCredentialsMetadataInput = await selectAndroidBuildCredentialsAction.runAsync(ctx);
expect(buildCredentialsMetadataInput).toMatchObject({
@@ -50,7 +50,7 @@ describe(SelectAndroidBuildCredentials, () => {
]),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const selectAndroidBuildCredentialsAction = new SelectAndroidBuildCredentials(appLookupParams);
const buildCredentialsMetadataInput = await selectAndroidBuildCredentialsAction.runAsync(ctx);
expect(buildCredentialsMetadataInput).toMatchObject({
@@ -70,7 +70,7 @@ describe(SelectAndroidBuildCredentials, () => {
getAndroidAppBuildCredentialsListAsync: jest.fn(() => []),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const selectAndroidBuildCredentialsAction = new SelectAndroidBuildCredentials(appLookupParams);
const buildCredentialsMetadataInput = await selectAndroidBuildCredentialsAction.runAsync(ctx);
expect(buildCredentialsMetadataInput).toMatchObject({
@@ -90,7 +90,7 @@ describe(SelectAndroidBuildCredentials, () => {
]),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const selectAndroidBuildCredentialsAction = new SelectAndroidBuildCredentials(appLookupParams);
const buildCredentialsMetadataInput = await selectAndroidBuildCredentialsAction.runAsync(ctx);
expect(buildCredentialsMetadataInput).toMatchObject({
@@ -1,6 +1,7 @@
import prompts from 'prompts';
import { asMock } from '../../__tests__/utils';
import Log from '../../log';
import { Actor } from '../../user/User';
import { AccountResolver } from '../manager';
@@ -69,15 +70,11 @@ describe(AccountResolver, () => {
accounts: [user.accounts[0]],
};
const originalConsoleWarn = console.warn;
console.warn = jest.fn();
jest.spyOn(Log, 'warn');
const resolver = new AccountResolver(exp, userWithAccessToProjectAccount);
const account = await resolver.resolveAccountAsync();
expect(account).toEqual(user.accounts[0]);
expect(console.warn).toBeCalledWith(expect.stringMatching(/doesn't have access to the/));
console.warn = originalConsoleWarn;
expect(Log.warn).toBeCalledWith(expect.stringMatching(/doesn't have access to the/));
});
});
@@ -6,14 +6,6 @@ import DeviceCreateAction, { RegistrationMethod } from '../action';
import { runInputMethodAsync } from '../inputMethod';
import { runRegistrationUrlMethodAsync } from '../registrationUrlMethod';
const originalConsoleLog = console.log;
beforeAll(() => {
console.log = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
});
jest.mock('prompts');
jest.mock('../registrationUrlMethod');
jest.mock('../inputMethod');
@@ -8,17 +8,6 @@ import { runInputMethodAsync } from '../inputMethod';
jest.mock('prompts');
jest.mock('../../../../credentials/ios/api/graphql/mutations/AppleDeviceMutation');
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
beforeEach(() => {
asMock(prompts).mockReset();
asMock(prompts).mockImplementation(() => {
@@ -14,7 +14,7 @@ export async function runRegistrationUrlMethodAsync(
): Promise<void> {
const registrationURL = await generateDeviceRegistrationURLAsync(accountId, appleTeam);
Log.newLine();
qrcodeTerminal.generate(registrationURL, code => console.log(`${indentString(code, 2)}\n`));
qrcodeTerminal.generate(registrationURL, code => Log.log(`${indentString(code, 2)}\n`));
Log.log(
'Open the following link on your iOS devices (or scan the QR code) and follow the instructions to install the development profile:'
);
+164 -194
View File
@@ -61,10 +61,6 @@ export type RootQuery = {
/** Top-level query object for querying Experimentation configuration. */
experimentation: ExperimentationQuery;
project: ProjectQuery;
/** Search for Snacks */
search: Array<SearchResult>;
/** @deprecated Use 'search' root field. */
searchUsersAndApps: Array<Maybe<SearchResult>>;
snack: SnackQuery;
submissions: SubmissionQuery;
/** Top-level query object for querying UserInvitationPublicData publicly. */
@@ -113,22 +109,6 @@ export type RootQueryAllPublicAppsArgs = {
};
export type RootQuerySearchArgs = {
type: SearchType;
query: Scalars['String'];
offset: Scalars['Int'];
limit: Scalars['Int'];
};
export type RootQuerySearchUsersAndAppsArgs = {
type: SearchType;
query: Scalars['String'];
offset: Scalars['Int'];
limit: Scalars['Int'];
};
export type RootQueryUserByUserIdArgs = {
userId: Scalars['String'];
};
@@ -637,6 +617,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
appVersion?: Maybe<Scalars['String']>;
appBuildVersion?: Maybe<Scalars['String']>;
sdkVersion?: Maybe<Scalars['String']>;
runtimeVersion?: Maybe<Scalars['String']>;
releaseChannel?: Maybe<Scalars['String']>;
channel?: Maybe<Scalars['String']>;
metrics?: Maybe<BuildMetrics>;
@@ -644,6 +625,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
buildProfile?: Maybe<Scalars['String']>;
gitCommitHash?: Maybe<Scalars['String']>;
error?: Maybe<BuildError>;
submissions: Array<Submission>;
};
export type BuildOrBuildJob = {
@@ -825,6 +807,72 @@ export type BuildError = {
docsUrl?: Maybe<Scalars['String']>;
};
/** Represents an EAS Submission */
export type Submission = ActivityTimelineProjectActivity & {
__typename?: 'Submission';
id: Scalars['ID'];
actor?: Maybe<Actor>;
activityTimestamp: Scalars['DateTime'];
app?: Maybe<App>;
initiatingActor?: Maybe<Actor>;
submittedBuild?: Maybe<Build>;
platform: AppPlatform;
status: SubmissionStatus;
androidConfig?: Maybe<AndroidSubmissionConfig>;
iosConfig?: Maybe<IosSubmissionConfig>;
logsUrl?: Maybe<Scalars['String']>;
error?: Maybe<SubmissionError>;
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
};
export enum SubmissionStatus {
InQueue = 'IN_QUEUE',
InProgress = 'IN_PROGRESS',
Finished = 'FINISHED',
Errored = 'ERRORED'
}
export type AndroidSubmissionConfig = {
__typename?: 'AndroidSubmissionConfig';
applicationIdentifier: Scalars['String'];
archiveType: SubmissionAndroidArchiveType;
track: SubmissionAndroidTrack;
releaseStatus?: Maybe<SubmissionAndroidReleaseStatus>;
};
export enum SubmissionAndroidArchiveType {
Apk = 'APK',
Aab = 'AAB'
}
export enum SubmissionAndroidTrack {
Production = 'PRODUCTION',
Internal = 'INTERNAL',
Alpha = 'ALPHA',
Beta = 'BETA'
}
export enum SubmissionAndroidReleaseStatus {
Draft = 'DRAFT',
InProgress = 'IN_PROGRESS',
Halted = 'HALTED',
Completed = 'COMPLETED'
}
export type IosSubmissionConfig = {
__typename?: 'IosSubmissionConfig';
ascAppIdentifier: Scalars['String'];
appleIdUsername: Scalars['String'];
appleAppSpecificPasswordId?: Maybe<Scalars['String']>;
};
export type SubmissionError = {
__typename?: 'SubmissionError';
errorCode?: Maybe<Scalars['String']>;
message?: Maybe<Scalars['String']>;
};
/** Represents an Standalone App build job */
export type BuildJob = ActivityTimelineProjectActivity & BuildOrBuildJob & {
__typename?: 'BuildJob';
@@ -891,71 +939,6 @@ export type SubmissionFilter = {
status?: Maybe<SubmissionStatus>;
};
export enum SubmissionStatus {
InQueue = 'IN_QUEUE',
InProgress = 'IN_PROGRESS',
Finished = 'FINISHED',
Errored = 'ERRORED'
}
/** Represents an EAS Submission */
export type Submission = ActivityTimelineProjectActivity & {
__typename?: 'Submission';
id: Scalars['ID'];
actor?: Maybe<Actor>;
activityTimestamp: Scalars['DateTime'];
app?: Maybe<App>;
initiatingActor?: Maybe<Actor>;
platform: AppPlatform;
status: SubmissionStatus;
androidConfig?: Maybe<AndroidSubmissionConfig>;
iosConfig?: Maybe<IosSubmissionConfig>;
logsUrl?: Maybe<Scalars['String']>;
error?: Maybe<SubmissionError>;
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
};
export type AndroidSubmissionConfig = {
__typename?: 'AndroidSubmissionConfig';
applicationIdentifier: Scalars['String'];
archiveType: SubmissionAndroidArchiveType;
track: SubmissionAndroidTrack;
releaseStatus?: Maybe<SubmissionAndroidReleaseStatus>;
};
export enum SubmissionAndroidArchiveType {
Apk = 'APK',
Aab = 'AAB'
}
export enum SubmissionAndroidTrack {
Production = 'PRODUCTION',
Internal = 'INTERNAL',
Alpha = 'ALPHA',
Beta = 'BETA'
}
export enum SubmissionAndroidReleaseStatus {
Draft = 'DRAFT',
InProgress = 'IN_PROGRESS',
Halted = 'HALTED',
Completed = 'COMPLETED'
}
export type IosSubmissionConfig = {
__typename?: 'IosSubmissionConfig';
ascAppIdentifier: Scalars['String'];
appleIdUsername: Scalars['String'];
appleAppSpecificPasswordId?: Maybe<Scalars['String']>;
};
export type SubmissionError = {
__typename?: 'SubmissionError';
errorCode?: Maybe<Scalars['String']>;
message?: Maybe<Scalars['String']>;
};
export type IosAppCredentialsFilter = {
appleAppIdentifierId?: Maybe<Scalars['String']>;
};
@@ -1590,19 +1573,6 @@ export type ProjectQueryByPathsArgs = {
paths?: Maybe<Array<Maybe<Scalars['String']>>>;
};
export enum SearchType {
All = 'ALL',
Users = 'USERS',
Apps = 'APPS',
Snacks = 'SNACKS'
}
/** Represents a search result for an app */
export type SearchResult = {
id: Scalars['ID'];
rank?: Maybe<Scalars['Int']>;
};
export type SnackQuery = {
__typename?: 'SnackQuery';
/**
@@ -2352,14 +2322,24 @@ export type BuildMutation = {
* @deprecated Use cancelBuild instead
*/
cancel: Build;
/** Create an Android generic build */
/**
* Create an Android generic build
* @deprecated Use createAndroidBuild instead
*/
createAndroidGenericBuild: CreateBuildResult;
/** Create an Android managed build */
/**
* Create an Android managed build
* @deprecated Use createAndroidBuild instead
*/
createAndroidManagedBuild: CreateBuildResult;
/** Create an Android build */
createAndroidBuild: CreateBuildResult;
/** Create an iOS generic build */
createIosGenericBuild: CreateBuildResult;
/** Create an iOS managed build */
createIosManagedBuild: CreateBuildResult;
/** Create an iOS build */
createIosBuild: CreateBuildResult;
};
@@ -2387,6 +2367,13 @@ export type BuildMutationCreateAndroidManagedBuildArgs = {
};
export type BuildMutationCreateAndroidBuildArgs = {
appId: Scalars['ID'];
job: AndroidJobInput;
metadata?: Maybe<BuildMetadataInput>;
};
export type BuildMutationCreateIosGenericBuildArgs = {
appId: Scalars['ID'];
job: IosGenericJobInput;
@@ -2400,6 +2387,13 @@ export type BuildMutationCreateIosManagedBuildArgs = {
metadata?: Maybe<BuildMetadataInput>;
};
export type BuildMutationCreateIosBuildArgs = {
appId: Scalars['ID'];
job: IosJobInput;
metadata?: Maybe<BuildMetadataInput>;
};
export type AndroidGenericJobInput = {
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
@@ -2468,6 +2462,7 @@ export type BuildMetadataInput = {
workflow?: Maybe<BuildWorkflow>;
credentialsSource?: Maybe<BuildCredentialsSource>;
sdkVersion?: Maybe<Scalars['String']>;
runtimeVersion?: Maybe<Scalars['String']>;
releaseChannel?: Maybe<Scalars['String']>;
channel?: Maybe<Scalars['String']>;
distribution?: Maybe<DistributionType>;
@@ -2523,6 +2518,27 @@ export enum AndroidManagedBuildType {
DevelopmentClient = 'DEVELOPMENT_CLIENT'
}
export type AndroidJobInput = {
type: BuildWorkflow;
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: Maybe<Scalars['String']>;
updates?: Maybe<BuildUpdatesInput>;
secrets?: Maybe<AndroidJobSecretsInput>;
builderEnvironment?: Maybe<AndroidBuilderEnvironmentInput>;
cache?: Maybe<BuildCacheInput>;
gradleCommand?: Maybe<Scalars['String']>;
artifactPath?: Maybe<Scalars['String']>;
buildType?: Maybe<AndroidBuildType>;
username?: Maybe<Scalars['String']>;
};
export enum AndroidBuildType {
Apk = 'APK',
AppBundle = 'APP_BUNDLE',
DevelopmentClient = 'DEVELOPMENT_CLIENT'
}
export type IosGenericJobInput = {
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
@@ -2588,6 +2604,28 @@ export enum IosManagedBuildType {
DevelopmentClient = 'DEVELOPMENT_CLIENT'
}
export type IosJobInput = {
type: BuildWorkflow;
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: Maybe<Scalars['String']>;
updates?: Maybe<BuildUpdatesInput>;
distribution?: Maybe<DistributionType>;
secrets?: Maybe<IosJobSecretsInput>;
builderEnvironment?: Maybe<IosBuilderEnvironmentInput>;
cache?: Maybe<BuildCacheInput>;
scheme?: Maybe<Scalars['String']>;
buildConfiguration?: Maybe<Scalars['String']>;
artifactPath?: Maybe<Scalars['String']>;
buildType?: Maybe<IosBuildType>;
username?: Maybe<Scalars['String']>;
};
export enum IosBuildType {
Release = 'RELEASE',
DevelopmentClient = 'DEVELOPMENT_CLIENT'
}
export type IosAppBuildCredentialsMutation = {
__typename?: 'IosAppBuildCredentialsMutation';
/** Create a set of build credentials for an iOS app */
@@ -2740,6 +2778,7 @@ export type CreateSubmissionInput = {
appId: Scalars['ID'];
platform: AppPlatform;
config: Scalars['JSONObject'];
submittedBuildId?: Maybe<Scalars['ID']>;
};
export type CreateSubmissionResult = {
@@ -2751,6 +2790,7 @@ export type CreateSubmissionResult = {
export type CreateIosSubmissionInput = {
appId: Scalars['ID'];
config: NewIosSubmissionConfig;
submittedBuildId?: Maybe<Scalars['ID']>;
};
export type NewIosSubmissionConfig = {
@@ -3283,44 +3323,6 @@ export type DeleteWebhookResult = {
id: Scalars['ID'];
};
export type BaseSearchResult = SearchResult & {
__typename?: 'BaseSearchResult';
/** @deprecated Use SearchResult instead */
id: Scalars['ID'];
/** @deprecated Use SearchResult instead */
rank?: Maybe<Scalars['Int']>;
};
/** Represents a search result for an app */
export type AppSearchResult = SearchResult & {
__typename?: 'AppSearchResult';
/** @deprecated Field no longer supported */
id: Scalars['ID'];
/** @deprecated Field no longer supported */
rank?: Maybe<Scalars['Int']>;
/** @deprecated Field no longer supported */
app: App;
};
/** Represents a search result for a user */
export type UserSearchResult = SearchResult & {
__typename?: 'UserSearchResult';
/** @deprecated Field no longer supported */
id: Scalars['ID'];
/** @deprecated Field no longer supported */
rank?: Maybe<Scalars['Int']>;
/** @deprecated Field no longer supported */
user: User;
};
/** Represents a search result for a snack */
export type SnackSearchResult = SearchResult & {
__typename?: 'SnackSearchResult';
id: Scalars['ID'];
rank?: Maybe<Scalars['Int']>;
snack: Snack;
};
export enum CacheControlScope {
Public = 'PUBLIC',
Private = 'PRIVATE'
@@ -3624,6 +3626,22 @@ export type GetChannelByNameForAppQuery = (
)> }
);
export type AppInfoQueryVariables = Exact<{
appId: Scalars['String'];
}>;
export type AppInfoQuery = (
{ __typename?: 'RootQuery' }
& { app?: Maybe<(
{ __typename?: 'AppQuery' }
& { byId: (
{ __typename?: 'App' }
& Pick<App, 'id' | 'fullName'>
) }
)> }
);
export type DeleteUpdateGroupMutationVariables = Exact<{
group: Scalars['ID'];
}>;
@@ -4497,18 +4515,18 @@ export type CreateAppMutation = (
)> }
);
export type CreateAndroidGenericBuildMutationVariables = Exact<{
export type CreateAndroidBuildMutationVariables = Exact<{
appId: Scalars['ID'];
job: AndroidGenericJobInput;
job: AndroidJobInput;
metadata?: Maybe<BuildMetadataInput>;
}>;
export type CreateAndroidGenericBuildMutation = (
export type CreateAndroidBuildMutation = (
{ __typename?: 'RootMutation' }
& { build?: Maybe<(
{ __typename?: 'BuildMutation' }
& { createAndroidGenericBuild: (
& { createAndroidBuild: (
{ __typename?: 'CreateBuildResult' }
& { build: (
{ __typename?: 'Build' }
@@ -4521,66 +4539,18 @@ export type CreateAndroidGenericBuildMutation = (
)> }
);
export type CreateAndroidManagedBuildMutationVariables = Exact<{
export type CreateIosBuildMutationVariables = Exact<{
appId: Scalars['ID'];
job: AndroidManagedJobInput;
job: IosJobInput;
metadata?: Maybe<BuildMetadataInput>;
}>;
export type CreateAndroidManagedBuildMutation = (
export type CreateIosBuildMutation = (
{ __typename?: 'RootMutation' }
& { build?: Maybe<(
{ __typename?: 'BuildMutation' }
& { createAndroidManagedBuild: (
{ __typename?: 'CreateBuildResult' }
& { build: (
{ __typename?: 'Build' }
& Pick<Build, 'id'>
), deprecationInfo?: Maybe<(
{ __typename?: 'EASBuildDeprecationInfo' }
& Pick<EasBuildDeprecationInfo, 'type' | 'message'>
)> }
) }
)> }
);
export type CreateIosGenericBuildMutationVariables = Exact<{
appId: Scalars['ID'];
job: IosGenericJobInput;
metadata?: Maybe<BuildMetadataInput>;
}>;
export type CreateIosGenericBuildMutation = (
{ __typename?: 'RootMutation' }
& { build?: Maybe<(
{ __typename?: 'BuildMutation' }
& { createIosGenericBuild: (
{ __typename?: 'CreateBuildResult' }
& { build: (
{ __typename?: 'Build' }
& Pick<Build, 'id'>
), deprecationInfo?: Maybe<(
{ __typename?: 'EASBuildDeprecationInfo' }
& Pick<EasBuildDeprecationInfo, 'type' | 'message'>
)> }
) }
)> }
);
export type CreateIosManagedBuildMutationVariables = Exact<{
appId: Scalars['ID'];
job: IosManagedJobInput;
metadata?: Maybe<BuildMetadataInput>;
}>;
export type CreateIosManagedBuildMutation = (
{ __typename?: 'RootMutation' }
& { build?: Maybe<(
{ __typename?: 'BuildMutation' }
& { createIosManagedBuild: (
& { createIosBuild: (
{ __typename?: 'CreateBuildResult' }
& { build: (
{ __typename?: 'Build' }
@@ -3,20 +3,14 @@ import nullthrows from 'nullthrows';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import {
AndroidGenericJobInput,
AndroidManagedJobInput,
AndroidJobInput,
BuildMetadataInput,
CreateAndroidGenericBuildMutation,
CreateAndroidGenericBuildMutationVariables,
CreateAndroidManagedBuildMutation,
CreateAndroidManagedBuildMutationVariables,
CreateIosGenericBuildMutation,
CreateIosGenericBuildMutationVariables,
CreateIosManagedBuildMutation,
CreateIosManagedBuildMutationVariables,
CreateAndroidBuildMutation,
CreateAndroidBuildMutationVariables,
CreateIosBuildMutation,
CreateIosBuildMutationVariables,
EasBuildDeprecationInfo,
IosGenericJobInput,
IosManagedJobInput,
IosJobInput,
} from '../generated';
export interface BuildResult {
@@ -25,22 +19,22 @@ export interface BuildResult {
}
const BuildMutation = {
async createAndroidGenericBuildAsync(input: {
async createAndroidBuildAsync(input: {
appId: string;
job: AndroidGenericJobInput;
job: AndroidJobInput;
metadata: BuildMetadataInput;
}): Promise<BuildResult> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateAndroidGenericBuildMutation, CreateAndroidGenericBuildMutationVariables>(
.mutation<CreateAndroidBuildMutation, CreateAndroidBuildMutationVariables>(
gql`
mutation CreateAndroidGenericBuildMutation(
mutation CreateAndroidBuildMutation(
$appId: ID!
$job: AndroidGenericJobInput!
$job: AndroidJobInput!
$metadata: BuildMetadataInput
) {
build {
createAndroidGenericBuild(appId: $appId, job: $job, metadata: $metadata) {
createAndroidBuild(appId: $appId, job: $job, metadata: $metadata) {
build {
id
}
@@ -56,24 +50,24 @@ const BuildMutation = {
)
.toPromise()
);
return nullthrows(data.build?.createAndroidGenericBuild);
return nullthrows(data.build?.createAndroidBuild);
},
async createAndroidManagedBuildAsync(input: {
async createIosBuildAsync(input: {
appId: string;
job: AndroidManagedJobInput;
job: IosJobInput;
metadata: BuildMetadataInput;
}): Promise<BuildResult> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateAndroidManagedBuildMutation, CreateAndroidManagedBuildMutationVariables>(
.mutation<CreateIosBuildMutation, CreateIosBuildMutationVariables>(
gql`
mutation CreateAndroidManagedBuildMutation(
mutation CreateIosBuildMutation(
$appId: ID!
$job: AndroidManagedJobInput!
$job: IosJobInput!
$metadata: BuildMetadataInput
) {
build {
createAndroidManagedBuild(appId: $appId, job: $job, metadata: $metadata) {
createIosBuild(appId: $appId, job: $job, metadata: $metadata) {
build {
id
}
@@ -89,73 +83,7 @@ const BuildMutation = {
)
.toPromise()
);
return nullthrows(data.build?.createAndroidManagedBuild);
},
async createIosGenericBuildAsync(input: {
appId: string;
job: IosGenericJobInput;
metadata: BuildMetadataInput;
}): Promise<BuildResult> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateIosGenericBuildMutation, CreateIosGenericBuildMutationVariables>(
gql`
mutation CreateIosGenericBuildMutation(
$appId: ID!
$job: IosGenericJobInput!
$metadata: BuildMetadataInput
) {
build {
createIosGenericBuild(appId: $appId, job: $job, metadata: $metadata) {
build {
id
}
deprecationInfo {
type
message
}
}
}
}
`,
input
)
.toPromise()
);
return nullthrows(data.build?.createIosGenericBuild);
},
async createIosManagedBuildAsync(input: {
appId: string;
job: IosManagedJobInput;
metadata: BuildMetadataInput;
}): Promise<BuildResult> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateIosManagedBuildMutation, CreateIosManagedBuildMutationVariables>(
gql`
mutation CreateIosManagedBuildMutation(
$appId: ID!
$job: IosManagedJobInput!
$metadata: BuildMetadataInput
) {
build {
createIosManagedBuild(appId: $appId, job: $job, metadata: $metadata) {
build {
id
}
deprecationInfo {
type
message
}
}
}
}
`,
input
)
.toPromise()
);
return nullthrows(data.build?.createIosManagedBuild);
return nullthrows(data.build?.createIosBuild);
},
};
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import chalk from 'chalk';
import figures from 'figures';
import { boolish } from 'getenv';
@@ -1,28 +1,24 @@
import { vol } from 'memfs';
import { asMock } from '../../__tests__/utils';
import { confirmAsync } from '../../prompts';
import { Actor, getUserAsync } from '../../user/User';
import {
ensureProjectExistsAsync,
findProjectIdByAccountNameAndSlugNullableAsync,
} from '../ensureProjectExists';
import {
findProjectRootAsync,
getProjectAccountName,
getProjectAccountNameAsync,
promptToCreateProjectIfNotExistsAsync,
} from '../projectUtils';
jest.mock('@expo/config');
jest.mock('fs');
jest.mock('../../user/User');
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
jest.mock('../ensureProjectExists');
describe(findProjectRootAsync, () => {
beforeEach(() => {
@@ -168,3 +164,62 @@ describe(getProjectAccountNameAsync, () => {
);
});
});
describe(promptToCreateProjectIfNotExistsAsync, () => {
beforeEach(() => {
asMock(getUserAsync).mockReset();
asMock(findProjectIdByAccountNameAndSlugNullableAsync).mockReset();
});
it('returns the existing project', async () => {
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
__typename: 'User',
id: 'user_id',
username: 'quin',
accounts: [{ id: 'account_id_1', name: 'quin' }],
isExpoAdmin: false,
}));
asMock(findProjectIdByAccountNameAndSlugNullableAsync).mockImplementation(
() => 'already-existing-project-id'
);
const exp: any = {};
const projectId = await promptToCreateProjectIfNotExistsAsync(exp);
expect(projectId).toBe('already-existing-project-id');
});
it('makes a new project if none exists', async () => {
jest.mock('../../prompts');
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
__typename: 'User',
id: 'user_id',
username: 'quin',
accounts: [{ id: 'account_id_1', name: 'quin' }],
isExpoAdmin: false,
}));
asMock(findProjectIdByAccountNameAndSlugNullableAsync).mockImplementation(() => null);
asMock(ensureProjectExistsAsync).mockImplementation(() => 'new-project-id');
// user agrees to make new project
(confirmAsync as jest.Mock).mockImplementation(() => true);
const exp: any = {};
const projectId = await promptToCreateProjectIfNotExistsAsync(exp);
expect(projectId).toBe('new-project-id');
});
it('aborts making a new project if the user declines', async () => {
jest.mock('../../prompts');
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
__typename: 'User',
id: 'user_id',
username: 'quin',
accounts: [{ id: 'account_id_1', name: 'quin' }],
isExpoAdmin: false,
}));
asMock(findProjectIdByAccountNameAndSlugNullableAsync).mockImplementation(() => null);
asMock(ensureProjectExistsAsync).mockImplementation(() => 'new-project-id');
// user doesnt agree to make new project
(confirmAsync as jest.Mock).mockImplementation(() => false);
const exp: any = {};
const projectId = await promptToCreateProjectIfNotExistsAsync(exp);
expect(projectId).toBe(null);
});
});
@@ -0,0 +1,59 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { vol } from 'memfs';
import { resolveWorkflowAsync } from '../workflow';
jest.mock('fs');
describe(resolveWorkflowAsync, () => {
beforeEach(() => {
vol.reset();
});
const projectDir = '/app';
test('generic workflow for both platforms', async () => {
vol.fromJSON(
{
'./ios/helloworld.xcodeproj/project.pbxproj': 'fake',
'./android/app/src/main/AndroidManifest.xml': 'fake',
},
projectDir
);
await expect(resolveWorkflowAsync(projectDir, Platform.ANDROID)).resolves.toBe(
Workflow.GENERIC
);
await expect(resolveWorkflowAsync(projectDir, Platform.IOS)).resolves.toBe(Workflow.GENERIC);
});
test('generic workflow for single platform', async () => {
vol.fromJSON(
{
'./ios/helloworld.xcodeproj/project.pbxproj': 'fake',
},
projectDir
);
await expect(resolveWorkflowAsync(projectDir, Platform.ANDROID)).resolves.toBe(
Workflow.MANAGED
);
await expect(resolveWorkflowAsync(projectDir, Platform.IOS)).resolves.toBe(Workflow.GENERIC);
});
test('android/ios directories are ignored', async () => {
vol.fromJSON(
{
'./ios/helloworld.xcodeproj/project.pbxproj': 'fake',
'./android/app/src/main/AndroidManifest.xml': 'fake',
'./.easignore': 'android/\nios/',
},
projectDir
);
await expect(resolveWorkflowAsync(projectDir, Platform.ANDROID)).resolves.toBe(
Workflow.MANAGED
);
await expect(resolveWorkflowAsync(projectDir, Platform.IOS)).resolves.toBe(Workflow.MANAGED);
});
});
@@ -3,19 +3,16 @@ import { vol } from 'memfs';
import os from 'os';
import { asMock } from '../../../__tests__/utils';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { promptAsync } from '../../../prompts';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
getApplicationId,
getApplicationIdAsync,
} from '../applicationId';
jest.mock('fs');
jest.mock('../../../prompts');
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.warn = jest.fn();
});
jest.mock('../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
beforeEach(() => {
vol.reset();
@@ -26,71 +23,69 @@ beforeEach(() => {
asMock(promptAsync).mockReset();
});
afterAll(() => {
fs.removeSync(os.tmpdir());
console.warn = originalConsoleWarn;
});
describe(getApplicationId, () => {
describe(getApplicationIdAsync, () => {
describe('generic projects', () => {
it('reads applicationId from build.gradle', () => {
it('reads applicationId from build.gradle', async () => {
vol.fromJSON(
{
'android/app/build.gradle': `
applicationId "com.expo.notdominik"
`,
'android/app/src/main/AndroidManifest.xml': 'fake',
},
'/app'
);
const applicationId = getApplicationId('/app', {} as any);
const applicationId = await getApplicationIdAsync('/app', {} as any);
expect(applicationId).toBe('com.expo.notdominik');
});
it('throws an error if build.gradle does not exist', () => {
it('throws an error if build.gradle does not exist', async () => {
vol.fromJSON(
{
'android/gradle.properties': 'fake file',
'android/app/src/main/AndroidManifest.xml': 'fake',
},
'/app'
);
expect(() => {
getApplicationId('/app', {} as any);
}).toThrowError(/Could not read application id/);
await expect(getApplicationIdAsync('/app', {} as any)).rejects.toThrowError(
/Could not read application id/
);
});
it('throws an error if the project does not have applicationId defined in build.gradle', () => {
it('throws an error if the project does not have applicationId defined in build.gradle', async () => {
vol.fromJSON(
{
'android/app/build.gradle': `fake build.gradle`,
'android/app/src/main/AndroidManifest.xml': 'fake',
},
'/app'
);
expect(() => {
getApplicationId('/app', {} as any);
}).toThrowError(/Could not read application id/);
await expect(getApplicationIdAsync('/app', {} as any)).rejects.toThrowError(
/Could not read application id/
);
});
});
describe('managed projects', () => {
it('reads applicationId (Android package) from app config', () => {
const applicationId = getApplicationId('/app', {
it('reads applicationId (Android package) from app config', async () => {
const applicationId = await getApplicationIdAsync('/app', {
android: { package: 'com.expo.notdominik' },
} as any);
expect(applicationId).toBe('com.expo.notdominik');
});
it('throws an error if Android package is not defined in app config', () => {
expect(() => {
getApplicationId('/app', {} as any);
}).toThrowError(/Specify "android.package"/);
it('throws an error if Android package is not defined in app config', async () => {
await expect(getApplicationIdAsync('/app', {} as any)).rejects.toThrowError(
/Specify "android.package"/
);
});
it('throws an error if Android package in app config is invalid', () => {
expect(() => {
getApplicationId('/app', { android: { package: '1com.expo.notdominik' } } as any);
}).toThrowError(/Specify "android.package"/);
it('throws an error if Android package in app config is invalid', async () => {
await expect(
getApplicationIdAsync('/app', { android: { package: '1com.expo.notdominik' } } as any)
).rejects.toThrowError(/Specify "android.package"/);
});
});
});
@@ -1,15 +1,17 @@
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import { AndroidConfig, IOSConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import assert from 'assert';
import chalk from 'chalk';
import fs from 'fs-extra';
import once from 'lodash/once';
import nullthrows from 'nullthrows';
import Log from '../../log';
import { getProjectConfigDescription } from '../../project/projectUtils';
import Log, { learnMore } from '../../log';
import { getProjectConfigDescription, getUsername } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { resolveWorkflow } from '../workflow';
import { ensureLoggedInAsync } from '../../user/actions';
import { resolveWorkflowAsync } from '../workflow';
const INVALID_APPLICATION_ID_MESSAGE = `Invalid format of Android applicationId. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.`;
@@ -17,18 +19,18 @@ export async function ensureApplicationIdIsDefinedForManagedProjectAsync(
projectDir: string,
exp: ExpoConfig
): Promise<string> {
const workflow = resolveWorkflow(projectDir, Platform.ANDROID);
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
assert(workflow === Workflow.MANAGED, 'This function should be called only for managed projects');
try {
return getApplicationId(projectDir, exp);
return await getApplicationIdAsync(projectDir, exp);
} catch (err) {
return await configureApplicationIdAsync(projectDir, exp);
}
}
export function getApplicationId(projectDir: string, exp: ExpoConfig): string {
const workflow = resolveWorkflow(projectDir, Platform.ANDROID);
export async function getApplicationIdAsync(projectDir: string, exp: ExpoConfig): Promise<string> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
warnIfAndroidPackageDefinedInAppConfigForGenericProject(projectDir, exp);
@@ -71,10 +73,19 @@ async function configureApplicationIdAsync(projectDir: string, exp: ExpoConfig):
assert(paths.staticConfigPath, 'app.json must exist');
Log.addNewLineIfNone();
Log.log(
`${chalk.bold(`📝 Android application id`)} ${chalk.dim(
learnMore('https://expo.fyi/android-package')
)}`
);
const suggestedAndroidApplicationId = await getSuggestedApplicationIdAsync(exp);
const { packageName } = await promptAsync({
name: 'packageName',
type: 'text',
message: `What would you like your Android application id to be?`,
initial: suggestedAndroidApplicationId,
validate: value => (isApplicationIdValid(value) ? true : INVALID_APPLICATION_ID_MESSAGE),
});
@@ -111,3 +122,19 @@ function _warnIfAndroidPackageDefinedInAppConfigForGenericProject(
export const warnIfAndroidPackageDefinedInAppConfigForGenericProject = once(
_warnIfAndroidPackageDefinedInAppConfigForGenericProject
);
async function getSuggestedApplicationIdAsync(exp: ExpoConfig): Promise<string | undefined> {
// Attempt to use the ios bundle id first since it's convenient to have them aligned.
const maybeBundleId = IOSConfig.BundleIdentifier.getBundleIdentifier(exp);
if (maybeBundleId && isApplicationIdValid(maybeBundleId)) {
return maybeBundleId;
} else {
const username = getUsername(exp, await ensureLoggedInAsync());
// It's common to use dashes in your node project name, strip them from the suggested package name.
const possibleId = `com.${username}.${exp.slug}`.split('-').join('');
if (isApplicationIdValid(possibleId)) {
return possibleId;
}
}
return undefined;
}
@@ -29,15 +29,10 @@ export async function ensureProjectExistsAsync(projectInfo: ProjectInfo): Promis
const spinner = ora(`Linking to project ${chalk.bold(projectFullName)}`).start();
try {
const id = await findProjectIdByAccountNameAndSlugAsync(accountName, projectName);
const maybeId = await findProjectIdByAccountNameAndSlugNullableAsync(accountName, projectName);
if (maybeId) {
spinner.succeed(`Linked to project ${chalk.bold(projectFullName)}`);
return id;
} catch (err) {
if (err.graphQLErrors?.some((it: any) => it.extensions?.errorCode !== 'EXPERIENCE_NOT_FOUND')) {
spinner.fail(`Something went wrong while looking for ${chalk.bold(projectFullName)} on Expo`);
throw err;
}
return maybeId;
}
try {
@@ -55,6 +50,26 @@ export async function ensureProjectExistsAsync(projectInfo: ProjectInfo): Promis
}
}
/**
* Finds project by `@accountName/slug` and returns its ID, return null if the project does not exist
* @param accountName account name
* @param slug project slug
* @returns A promise resolving to Project ID, null if it doesn't exist
*/
export async function findProjectIdByAccountNameAndSlugNullableAsync(
accountName: string,
slug: string
): Promise<string | null> {
try {
return await findProjectIdByAccountNameAndSlugAsync(accountName, slug);
} catch (err) {
if (err.graphQLErrors?.some((it: any) => it.extensions?.errorCode !== 'EXPERIENCE_NOT_FOUND')) {
throw err;
}
return null;
}
}
/**
* Finds project by `@accountName/slug` and returns its ID
* @param accountName account name
@@ -73,7 +88,7 @@ async function findProjectIdByAccountNameAndSlugAsync(
* Registers new project on EAS servers
* @returns Created project's ID
*/
async function registerNewProjectAsync({
export async function registerNewProjectAsync({
accountId,
projectName,
privacy,
@@ -4,20 +4,17 @@ import os from 'os';
import path from 'path';
import { asMock } from '../../../__tests__/utils';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { promptAsync } from '../../../prompts';
import {
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
getBundleIdentifier,
getBundleIdentifierAsync,
isWildcardBundleIdentifier,
} from '../bundleIdentifier';
jest.mock('fs');
jest.mock('../../../prompts');
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.warn = jest.fn();
});
jest.mock('../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
beforeEach(() => {
vol.reset();
@@ -28,16 +25,11 @@ beforeEach(() => {
asMock(promptAsync).mockReset();
});
afterAll(() => {
fs.removeSync(os.tmpdir());
console.warn = originalConsoleWarn;
});
const originalFs = jest.requireActual('fs');
describe(getBundleIdentifier, () => {
describe(getBundleIdentifierAsync, () => {
describe('generic projects', () => {
it('reads bundle identifier from project.', () => {
it('reads bundle identifier from project.', async () => {
vol.fromJSON(
{
'ios/myproject.xcodeproj/project.pbxproj': originalFs.readFileSync(
@@ -48,11 +40,11 @@ describe(getBundleIdentifier, () => {
'/app'
);
const bundleIdentifier = getBundleIdentifier('/app', {} as any);
const bundleIdentifier = await getBundleIdentifierAsync('/app', {} as any);
expect(bundleIdentifier).toBe('org.name.testproject');
});
it('throws an error if the pbxproj is not configured with bundle id', () => {
it('throws an error if the pbxproj is not configured with bundle id', async () => {
vol.fromJSON(
{
'ios/myproject.xcodeproj/project.pbxproj': originalFs.readFileSync(
@@ -63,30 +55,30 @@ describe(getBundleIdentifier, () => {
'/app'
);
expect(() => {
getBundleIdentifier('/app', {} as any);
}).toThrowError(/Could not read bundle identifier/);
await expect(getBundleIdentifierAsync('/app', {} as any)).rejects.toThrowError(
/Could not read bundle identifier/
);
});
});
describe('managed projects', () => {
it('reads bundleIdentifier from app config', () => {
const applicationId = getBundleIdentifier('/app', {
it('reads bundleIdentifier from app config', async () => {
const applicationId = await getBundleIdentifierAsync('/app', {
ios: { bundleIdentifier: 'com.expo.notdominik' },
} as any);
expect(applicationId).toBe('com.expo.notdominik');
});
it('throws an error if bundleIdentifier is not defined in app config', () => {
expect(() => {
getBundleIdentifier('/app', {} as any);
}).toThrowError(/Specify "ios.bundleIdentifier"/);
it('throws an error if bundleIdentifier is not defined in app config', async () => {
await expect(getBundleIdentifierAsync('/app', {} as any)).rejects.toThrowError(
/Specify "ios.bundleIdentifier"/
);
});
it('throws an error if bundleIdentifier in app config is invalid', () => {
expect(() => {
getBundleIdentifier('/app', { ios: { bundleIdentifier: '' } } as any);
}).toThrowError(/Specify "ios.bundleIdentifier"/);
it('throws an error if bundleIdentifier in app config is invalid', async () => {
await expect(
getBundleIdentifierAsync('/app', { ios: { bundleIdentifier: '' } } as any)
).rejects.toThrowError(/Specify "ios.bundleIdentifier"/);
});
});
});
@@ -9,11 +9,6 @@ import { selectSchemeAsync } from '../scheme';
jest.mock('fs');
jest.mock('../../../prompts');
const originalConsoleLog = console.log;
beforeAll(() => {
console.log = jest.fn();
});
beforeEach(() => {
vol.reset();
// do not remove the following line
@@ -23,11 +18,6 @@ beforeEach(() => {
asMock(promptAsync).mockReset();
});
afterAll(() => {
fs.removeSync(os.tmpdir());
console.log = originalConsoleLog;
});
describe(selectSchemeAsync, () => {
const projectDir = '/app';
@@ -1,14 +1,16 @@
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import { AndroidConfig, IOSConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import assert from 'assert';
import chalk from 'chalk';
import fs from 'fs-extra';
import once from 'lodash/once';
import Log from '../../log';
import Log, { learnMore } from '../../log';
import { promptAsync } from '../../prompts';
import { getProjectConfigDescription, sanitizedProjectName } from '../projectUtils';
import { resolveWorkflow } from '../workflow';
import { ensureLoggedInAsync } from '../../user/actions';
import { getProjectConfigDescription, getUsername, sanitizedProjectName } 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.`;
@@ -16,22 +18,22 @@ export async function ensureBundleIdentifierIsDefinedForManagedProjectAsync(
projectDir: string,
exp: ExpoConfig
): Promise<string> {
const workflow = resolveWorkflow(projectDir, Platform.IOS);
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
assert(workflow === Workflow.MANAGED, 'This function should be called only for managed projects');
try {
return getBundleIdentifier(projectDir, exp);
return await getBundleIdentifierAsync(projectDir, exp);
} catch (err) {
return await configureBundleIdentifierAsync(projectDir, exp);
}
}
export function getBundleIdentifier(
export async function getBundleIdentifierAsync(
projectDir: string,
exp: ExpoConfig,
{ targetName, buildConfiguration }: { targetName?: string; buildConfiguration?: string } = {}
): string {
const workflow = resolveWorkflow(projectDir, Platform.IOS);
): Promise<string> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
warnIfBundleIdentifierDefinedInAppConfigForGenericProject(projectDir, exp);
@@ -90,10 +92,19 @@ async function configureBundleIdentifierAsync(
assert(paths.staticConfigPath, 'app.json must exist');
Log.addNewLineIfNone();
Log.log(
`${chalk.bold(`📝 iOS Bundle Identifier`)} ${chalk.dim(
learnMore('https://expo.fyi/bundle-identifier')
)}`
);
const suggestedBundleIdentifier = await getSuggestedBundleIdentifierAsync(exp);
const { bundleIdentifier } = await promptAsync({
name: 'bundleIdentifier',
type: 'text',
message: `What would you like your iOS bundle identifier to be?`,
initial: suggestedBundleIdentifier,
validate: value => (isBundleIdentifierValid(value) ? true : INVALID_BUNDLE_IDENTIFIER_MESSAGE),
});
@@ -135,3 +146,19 @@ export function isWildcardBundleIdentifier(bundleIdentifier: string): boolean {
const wildcardRegex = /^[A-Za-z0-9.-]+\*$/;
return wildcardRegex.test(bundleIdentifier);
}
async function getSuggestedBundleIdentifierAsync(exp: ExpoConfig): Promise<string | undefined> {
// Attempt to use the android package name first since it's convenient to have them aligned.
const maybeAndroidPackage = AndroidConfig.Package.getPackage(exp);
if (maybeAndroidPackage && isBundleIdentifierValid(maybeAndroidPackage)) {
return maybeAndroidPackage;
} else {
const username = getUsername(exp, await ensureLoggedInAsync());
// It's common to use dashes in your node project name, strip them from the suggested package name.
const possibleId = `com.${username}.${exp.slug}`.split('-').join('');
if (isBundleIdentifierValid(possibleId)) {
return possibleId;
}
}
return undefined;
}
+4 -2
View File
@@ -1,6 +1,6 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import { Workflow } from '@expo/eas-build-job';
import { Platform, Workflow } from '@expo/eas-build-job';
import { IosBuildProfile } from '@expo/eas-json';
import chalk from 'chalk';
import sortBy from 'lodash/sortBy';
@@ -8,6 +8,7 @@ import sortBy from 'lodash/sortBy';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { sanitizedProjectName } from '../projectUtils';
import { resolveWorkflowAsync } from '../workflow';
export interface XcodeBuildContext {
buildScheme: string;
@@ -22,7 +23,8 @@ export async function resolveXcodeBuildContextAsync(
}: { exp: ExpoConfig; projectDir: string; nonInteractive: boolean },
buildProfile: IosBuildProfile
): Promise<XcodeBuildContext> {
if (buildProfile.workflow === Workflow.GENERIC) {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const buildScheme =
buildProfile.scheme ??
(await selectSchemeAsync({
+5 -5
View File
@@ -3,8 +3,8 @@ import { IOSConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Target } from '../../credentials/ios/types';
import { resolveWorkflow } from '../workflow';
import { getBundleIdentifier } from './bundleIdentifier';
import { resolveWorkflowAsync } from '../workflow';
import { getBundleIdentifierAsync } from './bundleIdentifier';
import { XcodeBuildContext } from './scheme';
export async function resolveTargetsAsync(
@@ -14,7 +14,7 @@ export async function resolveTargetsAsync(
const result: Target[] = [];
const applicationTarget = await readApplicationTargetForSchemeAsync(projectDir, buildScheme);
const bundleIdentifier = getBundleIdentifier(projectDir, exp, {
const bundleIdentifier = await getBundleIdentifierAsync(projectDir, exp, {
targetName: applicationTarget.name,
buildConfiguration,
});
@@ -27,7 +27,7 @@ export async function resolveTargetsAsync(
for (const dependency of applicationTarget.dependencies) {
result.push({
targetName: dependency.name,
bundleIdentifier: getBundleIdentifier(projectDir, exp, {
bundleIdentifier: await getBundleIdentifierAsync(projectDir, exp, {
targetName: dependency.name,
buildConfiguration,
}),
@@ -43,7 +43,7 @@ async function readApplicationTargetForSchemeAsync(
projectDir: string,
scheme: string
): Promise<IOSConfig.Target.Target> {
const workflow = resolveWorkflow(projectDir, Platform.IOS);
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
return await IOSConfig.Target.findApplicationTargetWithDependenciesAsync(projectDir, scheme);
} else {
+32 -1
View File
@@ -13,9 +13,13 @@ import pkgDir from 'pkg-dir';
import { graphqlClient, withErrorHandlingAsync } from '../graphql/client';
import { AppPrivacy, UpdateBranch } from '../graphql/generated';
import Log from '../log';
import { confirmAsync } from '../prompts';
import { Actor } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
import { ensureProjectExistsAsync } from './ensureProjectExists';
import {
ensureProjectExistsAsync,
findProjectIdByAccountNameAndSlugNullableAsync,
} from './ensureProjectExists';
export function getProjectAccountName(exp: ExpoConfig, user: Actor): string {
switch (user.__typename) {
@@ -225,3 +229,30 @@ export function sanitizedProjectName(name: string) {
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '');
}
// return project id of existing/newly created project, or null if user declines
export async function promptToCreateProjectIfNotExistsAsync(
exp: ExpoConfig
): Promise<string | null> {
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const maybeProjectId = await findProjectIdByAccountNameAndSlugNullableAsync(
accountName,
exp.slug
);
if (maybeProjectId) {
return maybeProjectId;
}
const fullName = await getProjectFullNameAsync(exp);
const shouldCreateProject = await confirmAsync({
message: `Looks like ${fullName} is new. Register it with EAS?`,
});
if (!shouldCreateProject) {
return null;
}
const privacy = toAppPrivacy(exp.privacy);
return await ensureProjectExistsAsync({
accountName,
projectName: exp.slug,
privacy,
});
}
+24 -2
View File
@@ -1,7 +1,29 @@
import { AndroidConfig, IOSConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import fs from 'fs-extra';
import path from 'path';
export function resolveWorkflow(projectDir: string, platform: Platform): Workflow {
return fs.pathExistsSync(path.join(projectDir, platform)) ? Workflow.GENERIC : Workflow.MANAGED;
import vcs from '../vcs';
export async function resolveWorkflowAsync(
projectDir: string,
platform: Platform
): Promise<Workflow> {
let platformWorkflowMarker;
try {
platformWorkflowMarker =
platform === Platform.ANDROID
? await AndroidConfig.Paths.getAndroidManifestAsync(projectDir)
: IOSConfig.Paths.getXcodeProjectPath(projectDir);
} catch {
return Workflow.MANAGED;
}
if (await fs.pathExists(platformWorkflowMarker)) {
return (await vcs.isFileIgnoredAsync(path.relative(projectDir, platformWorkflowMarker)))
? Workflow.MANAGED
: Workflow.GENERIC;
} else {
return Workflow.MANAGED;
}
}
@@ -65,11 +65,10 @@ class AndroidSubmitCommand {
private resolveAndroidPackageSource(): Result<AndroidPackageSource> {
let androidPackage: string | undefined;
const { exp } = getConfig(this.ctx.projectDir, { skipSDKVersionRequirement: true });
if (this.ctx.commandFlags.androidPackage) {
androidPackage = this.ctx.commandFlags.androidPackage;
}
const { exp } = getConfig(this.ctx.projectDir, { skipSDKVersionRequirement: true });
if (exp.android?.package) {
} else if (exp.android?.package) {
androidPackage = exp.android.package;
}
if (androidPackage) {
@@ -35,10 +35,7 @@ describe(AndroidSubmitCommand, () => {
'/google-service-account.json': JSON.stringify({ service: 'account' }),
};
const originalConsoleLog = console.log;
beforeAll(() => {
console.log = jest.fn();
vol.fromJSON({
...testProject.projectTree,
...fakeFiles,
@@ -53,8 +50,6 @@ describe(AndroidSubmitCommand, () => {
vol.reset();
jest.unmock('@expo/config');
console.log = originalConsoleLog;
});
afterEach(() => {
@@ -12,22 +12,13 @@ jest.mock('fs');
jest.mock('../../../prompts');
describe(getServiceAccountAsync, () => {
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
vol.fromJSON({
'/google-service-account.json': JSON.stringify({ service: 'account' }),
});
});
afterAll(() => {
vol.reset();
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
});
afterEach(() => {
@@ -8,7 +8,7 @@ import {
ensureBundleIdExistsWithNameAsync,
} from '../../credentials/ios/appstore/ensureAppExists';
import Log from '../../log';
import { getBundleIdentifier } from '../../project/ios/bundleIdentifier';
import { getBundleIdentifierAsync } from '../../project/ios/bundleIdentifier';
import { promptAsync } from '../../prompts';
import { IosSubmissionContext } from '../types';
import { sanitizeLanguage } from './utils/language';
@@ -41,7 +41,8 @@ export async function ensureAppStoreConnectAppExistsAsync(
// - for builds from the database, read bundled identifier from metadata
// - for builds uploaded from file system, prompt for the bundle identifier
// this is necessary to make submit work outside the project directory
const resolvedBundleId = bundleIdentifier ?? getBundleIdentifier(ctx.projectDir, exp);
const resolvedBundleId =
bundleIdentifier ?? (await getBundleIdentifierAsync(ctx.projectDir, exp));
const options = {
...ctx.commandFlags,
@@ -30,10 +30,7 @@ describe(IosSubmitCommand, () => {
'/artifacts/fake.ipa': 'fake ipa',
};
const originalConsoleLog = console.log;
beforeAll(() => {
console.log = jest.fn();
vol.fromJSON({
...testProject.projectTree,
...fakeFiles,
@@ -48,8 +45,6 @@ describe(IosSubmitCommand, () => {
vol.reset();
jest.unmock('@expo/config');
console.log = originalConsoleLog;
});
afterEach(() => {
@@ -1,13 +1,5 @@
import { sanitizeLanguage } from '../language';
const originalConsoleLog = console.log;
beforeAll(() => {
console.log = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
});
describe(sanitizeLanguage, () => {
it('throws when language not found', () => {
expect(() => sanitizeLanguage('Ponglish')).toThrowError();
+9
View File
@@ -127,6 +127,15 @@ export default class GitClient extends Client {
!trackedFiles.includes(pathWithoutLeadingDot)
);
}
public async isFileIgnoredAsync(filePath: string): Promise<boolean> {
try {
await spawnAsync('git', ['check-ignore', '-q', filePath]);
return true;
} catch (e) {
return false;
}
}
}
async function isGitInstalledAsync(): Promise<boolean> {
+32 -20
View File
@@ -1,5 +1,5 @@
import fs from 'fs-extra';
import ignore from 'ignore';
import ignore, { Ignore } from 'ignore';
import path from 'path';
import { Client } from './vcs';
@@ -10,34 +10,46 @@ node_modules
export default class LocalClient extends Client {
public async getRootPathAsync(): Promise<string> {
const rootPath = process.env.EAS_PROJECT_ROOT ?? process.cwd();
if (!path.isAbsolute(rootPath)) {
return path.resolve(process.cwd(), rootPath);
}
return rootPath;
return getRootPath();
}
public async makeShallowCopyAsync(destinationPath: string): Promise<void> {
const srcPath = await this.getRootPathAsync();
const easIgnorePath = path.join(srcPath, '.easignore');
const gitIgnorePath = path.join(srcPath, '.gitignore');
let ignoreFile = DEFAULT_IGNORE;
if (await fs.pathExists(easIgnorePath)) {
ignoreFile = await fs.readFile(easIgnorePath, 'utf8');
} else if (await fs.pathExists(gitIgnorePath)) {
ignoreFile = await fs.readFile(gitIgnorePath, 'utf8');
}
const match = ignore().add(ignoreFile);
const srcPath = getRootPath();
const ignore = initIgnore();
await fs.copy(srcPath, destinationPath, {
filter: (srcFilePath: string) => {
if (srcFilePath === srcPath) {
return true;
}
return !match.ignores(path.relative(srcPath, srcFilePath));
return !ignore.ignores(path.relative(srcPath, srcFilePath));
},
});
}
public async isFileIgnoredAsync(filePath: string): Promise<boolean> {
return initIgnore().ignores(filePath);
}
}
function getRootPath(): string {
const rootPath = process.env.EAS_PROJECT_ROOT ?? process.cwd();
if (!path.isAbsolute(rootPath)) {
return path.resolve(process.cwd(), rootPath);
}
return rootPath;
}
function initIgnore(): Ignore {
const srcPath = getRootPath();
const easIgnorePath = path.join(srcPath, '.easignore');
const gitIgnorePath = path.join(srcPath, '.gitignore');
let ignoreFile = DEFAULT_IGNORE;
if (fs.pathExistsSync(easIgnorePath)) {
ignoreFile = fs.readFileSync(easIgnorePath, 'utf8');
} else if (fs.pathExistsSync(gitIgnorePath)) {
ignoreFile = fs.readFileSync(gitIgnorePath, 'utf8');
}
return ignore().add(ignoreFile);
}
+8 -1
View File
@@ -2,7 +2,7 @@ export abstract class Client {
// makeShallowCopyAsync should copy current project (result of getRootPathAsync()) to the specified
// destination, folder created this way will be uploaded "as is", so implementation should skip
// anything that is not commited to the repository. Most optimal solution is to create shallow clone
// using tooling provided by specific VCS, that respect all ignore rules
// using tooling provided by specific VCS, that respects all ignore rules
public abstract makeShallowCopyAsync(destinationPath: string): Promise<void>;
// Find root of the repository.
@@ -58,4 +58,11 @@ export abstract class Client {
public async getLastCommitMessageAsync(): Promise<string | null> {
return null;
}
// (optional) checks if the file is ignored, an implementation should ensure
// that if file exists and `isFileIgnoredAsync` returns true, then that file
// should not be included in the project tarball.
public async isFileIgnoredAsync(filePath: string): Promise<boolean> {
return false;
}
}
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with the eas.json",
"version": "0.18.2",
"version": "0.20.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/eas-build-job": "0.2.37",
"@expo/eas-build-job": "0.2.41",
"@hapi/joi": "17.1.1",
"fs-extra": "9.0.1",
"tslib": "1.14.1"
+17 -35
View File
@@ -1,4 +1,4 @@
import { Android, Cache, Ios, Workflow } from '@expo/eas-build-job';
import { Android, Cache, Ios } from '@expo/eas-build-job';
export enum CredentialsSource {
LOCAL = 'local',
@@ -13,64 +13,46 @@ export type IosEnterpriseProvisioning = 'adhoc' | 'universal';
export type VersionAutoIncrement = boolean | 'version' | 'buildNumber';
export interface AndroidManagedBuildProfile extends Android.BuilderEnvironment {
workflow: Workflow.MANAGED;
interface IosBuilderEnvironment extends Omit<Ios.BuilderEnvironment, 'image'> {
image?: Ios.BuilderEnvironment['image'];
}
export interface AndroidBuildProfile extends Android.BuilderEnvironment {
credentialsSource: CredentialsSource;
buildType?: Android.ManagedBuildType;
releaseChannel?: string;
channel?: string;
distribution: AndroidDistributionType;
cache: Cache;
}
export interface AndroidGenericBuildProfile extends Android.BuilderEnvironment {
workflow: Workflow.GENERIC;
credentialsSource: CredentialsSource;
gradleCommand?: string;
releaseChannel?: string;
channel?: string;
artifactPath?: string;
withoutCredentials?: boolean;
distribution: AndroidDistributionType;
cache: Cache;
buildType?: Android.BuildType;
gradleCommand?: string;
artifactPath?: string;
}
export interface IosManagedBuildProfile extends Omit<Ios.BuilderEnvironment, 'image'> {
workflow: Workflow.MANAGED;
export interface IosBuildProfile extends IosBuilderEnvironment {
credentialsSource: CredentialsSource;
buildType?: Ios.ManagedBuildType;
releaseChannel?: string;
channel?: string;
distribution: IosDistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
autoIncrement: VersionAutoIncrement;
cache: Cache;
image?: Ios.BuilderEnvironment['image'];
}
export interface IosGenericBuildProfile extends Omit<Ios.BuilderEnvironment, 'image'> {
workflow: Workflow.GENERIC;
credentialsSource: CredentialsSource;
artifactPath?: string;
scheme?: string;
schemeBuildConfiguration?: string;
releaseChannel?: string;
channel?: string;
artifactPath?: string;
distribution: IosDistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
autoIncrement: VersionAutoIncrement;
cache: Cache;
image?: Ios.BuilderEnvironment['image'];
buildType?: Ios.BuildType;
}
export type AndroidBuildProfile = AndroidManagedBuildProfile | AndroidGenericBuildProfile;
export type IosBuildProfile = IosManagedBuildProfile | IosGenericBuildProfile;
export type BuildProfile = AndroidBuildProfile | IosBuildProfile;
// EasConfig represents eas.json with one specific profile
export interface EasConfig {
builds: {
android?: AndroidManagedBuildProfile | AndroidGenericBuildProfile;
ios?: IosManagedBuildProfile | IosGenericBuildProfile;
android?: AndroidBuildProfile;
ios?: IosBuildProfile;
};
}
+3 -24
View File
@@ -76,9 +76,7 @@ export class EasJsonReader {
const androidProfiles = easJson.builds?.android ?? {};
for (const name of Object.keys(androidProfiles)) {
try {
if (this.isWorkflowKeySpecified(Platform.ANDROID, name, androidProfiles)) {
this.validateBuildProfile(Platform.ANDROID, name, androidProfiles);
}
this.validateBuildProfile(Platform.ANDROID, name, androidProfiles);
} catch (err) {
err.msg = `Failed to validate Android build profile "${name}"\n${err.msg}`;
throw err;
@@ -87,9 +85,7 @@ export class EasJsonReader {
const iosProfiles = easJson.builds?.ios ?? {};
for (const name of Object.keys(iosProfiles)) {
try {
if (this.isWorkflowKeySpecified(Platform.IOS, name, iosProfiles)) {
this.validateBuildProfile(Platform.IOS, name, iosProfiles);
}
this.validateBuildProfile(Platform.IOS, name, iosProfiles);
} catch (err) {
err.msg = `Failed to validate iOS build profile "${name}"\n${err.msg}`;
throw err;
@@ -117,15 +113,7 @@ export class EasJsonReader {
buildProfiles: Record<string, BuildProfilePreValidation>
): T {
const buildProfile = this.resolveBuildProfile(platform, buildProfileName, buildProfiles);
if (![Workflow.GENERIC, Workflow.MANAGED].includes(buildProfile.workflow)) {
throw new Error(
'"workflow" key is required in a build profile and has to be one of ["generic", "managed"].'
);
}
const schema = schemaBuildProfileMap[platform][buildProfile.workflow];
if (!schema) {
throw new Error('invalid workflow'); // this should be validated earlier
}
const schema = schemaBuildProfileMap[platform];
const { value, error } = schema.validate(buildProfile, {
stripUnknown: true,
convert: true,
@@ -140,15 +128,6 @@ export class EasJsonReader {
return value;
}
private isWorkflowKeySpecified(
platform: Platform,
buildProfileName: string,
buildProfiles: Record<string, BuildProfilePreValidation>
): boolean {
const buildProfile = this.resolveBuildProfile(platform, buildProfileName, buildProfiles);
return !!buildProfile.workflow;
}
private resolveBuildProfile(
platform: Platform,
buildProfileName: string,
+27 -59
View File
@@ -38,90 +38,58 @@ const CacheSchema = Joi.object({
customPaths: Joi.array().items(Joi.string()).default([]),
});
const AndroidGenericSchema = Joi.object({
workflow: Joi.string().valid('generic').required(),
const AndroidSchema = Joi.object({
workflow: Joi.string(),
credentialsSource: Joi.string().valid('local', 'remote').default('remote'),
gradleCommand: Joi.alternatives().conditional('distribution', {
is: 'internal',
then: Joi.string().default(':app:assembleRelease'),
otherwise: Joi.string(),
}),
releaseChannel: Joi.string(),
channel: Joi.string(),
artifactPath: Joi.string(),
withoutCredentials: Joi.boolean().default(false),
distribution: Joi.string().valid('store', 'internal').default('store'),
cache: CacheSchema.default(),
}).concat(AndroidBuilderEnvironmentSchema);
withoutCredentials: Joi.boolean().default(false),
artifactPath: Joi.string(),
gradleCommand: Joi.string(),
const AndroidManagedSchema = Joi.object({
workflow: Joi.string().valid('managed').required(),
credentialsSource: Joi.string().valid('local', 'remote').default('remote'),
releaseChannel: Joi.string(),
channel: Joi.string(),
buildType: Joi.alternatives().conditional('distribution', {
is: 'internal',
then: Joi.string().valid('apk', 'development-client').default('apk'),
otherwise: Joi.string().valid('apk', 'app-bundle', 'development-client').default('app-bundle'),
then: Joi.string().valid('apk', 'development-client'),
otherwise: Joi.string().valid('apk', 'app-bundle', 'development-client'),
}),
distribution: Joi.string().valid('store', 'internal').default('store'),
cache: CacheSchema.default(),
}).concat(AndroidBuilderEnvironmentSchema);
const IosGenericSchema = Joi.object({
workflow: Joi.string().valid('generic').required(),
const IosSchema = Joi.object({
workflow: Joi.string(),
credentialsSource: Joi.string().valid('local', 'remote').default('remote'),
releaseChannel: Joi.string(),
channel: Joi.string(),
distribution: Joi.string().valid('store', 'internal', 'simulator').default('store'),
enterpriseProvisioning: Joi.string().valid('adhoc', 'universal'),
autoIncrement: Joi.alternatives()
.try(Joi.boolean(), Joi.string().valid('version', 'buildNumber'))
.default(false),
cache: CacheSchema.default(),
artifactPath: Joi.string(),
scheme: Joi.string(),
schemeBuildConfiguration: Joi.string(),
releaseChannel: Joi.string(),
channel: Joi.string(),
artifactPath: Joi.string(),
distribution: Joi.string().valid('store', 'internal', 'simulator').default('store'),
enterpriseProvisioning: Joi.string().valid('adhoc', 'universal'),
autoIncrement: Joi.alternatives()
.try(Joi.boolean(), Joi.string().valid('version', 'buildNumber'))
.default(false),
cache: CacheSchema.default(),
buildType: Joi.string().valid('release', 'development-client'),
}).concat(IosBuilderEnvironmentSchema);
const IosManagedSchema = Joi.object({
workflow: Joi.string().valid('managed').required(),
credentialsSource: Joi.string().valid('local', 'remote').default('remote'),
buildType: Joi.string().valid('release', 'development-client').default('release'),
releaseChannel: Joi.string(),
channel: Joi.string(),
distribution: Joi.string().valid('store', 'internal', 'simulator').default('store'),
enterpriseProvisioning: Joi.string().valid('adhoc', 'universal'),
autoIncrement: Joi.alternatives()
.try(Joi.boolean(), Joi.string().valid('version', 'buildNumber'))
.default(false),
cache: CacheSchema.default(),
}).concat(IosBuilderEnvironmentSchema);
export const schemaBuildProfileMap: Record<string, Record<string, Joi.Schema>> = {
android: {
generic: AndroidGenericSchema,
managed: AndroidManagedSchema,
},
ios: {
managed: IosManagedSchema,
generic: IosGenericSchema,
},
export const schemaBuildProfileMap: Record<string, Joi.Schema> = {
android: AndroidSchema,
ios: IosSchema,
};
export const EasJsonSchema = Joi.object({
builds: Joi.object({
android: Joi.object().pattern(
Joi.string(),
Joi.object({
workflow: Joi.string().valid('generic', 'managed'),
}).unknown(true) // profile is validated further only if build is for that platform
Joi.object({}).unknown(true) // profile is validated further only if build is for that platform
),
ios: Joi.object().pattern(
Joi.string(),
Joi.object({
workflow: Joi.string().valid('generic', 'managed'),
}).unknown(true) // profile is validated further only if build is for that platform
Joi.object({}).unknown(true) // profile is validated further only if build is for that platform
),
}),
});
@@ -14,7 +14,7 @@ test('minimal valid android eas.json', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
release: { workflow: 'generic' },
release: {},
},
},
});
@@ -24,7 +24,6 @@ test('minimal valid android eas.json', async () => {
expect({
builds: {
android: {
workflow: 'generic',
distribution: 'store',
credentialsSource: 'remote',
env: {},
@@ -40,7 +39,7 @@ test('minimal valid ios eas.json', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
ios: {
release: { workflow: 'generic' },
release: {},
},
},
});
@@ -52,7 +51,6 @@ test('minimal valid ios eas.json', async () => {
ios: {
credentialsSource: 'remote',
distribution: 'store',
workflow: 'generic',
autoIncrement: false,
env: {},
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
@@ -65,10 +63,10 @@ test('minimal valid eas.json for both platforms', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
release: { workflow: 'generic' },
release: {},
},
ios: {
release: { workflow: 'generic' },
release: {},
},
},
});
@@ -78,7 +76,6 @@ test('minimal valid eas.json for both platforms', async () => {
expect({
builds: {
android: {
workflow: 'generic',
distribution: 'store',
credentialsSource: 'remote',
env: {},
@@ -87,7 +84,6 @@ test('minimal valid eas.json for both platforms', async () => {
image: 'default',
},
ios: {
workflow: 'generic',
distribution: 'store',
credentialsSource: 'remote',
autoIncrement: false,
@@ -102,10 +98,10 @@ test('valid eas.json with both platform, but reading only android', async () =>
await fs.writeJson('/project/eas.json', {
builds: {
ios: {
release: { workflow: 'generic' },
release: {},
},
android: {
release: { workflow: 'generic' },
release: {},
},
},
});
@@ -115,7 +111,6 @@ test('valid eas.json with both platform, but reading only android', async () =>
expect({
builds: {
android: {
workflow: 'generic',
distribution: 'store',
credentialsSource: 'remote',
env: {},
@@ -131,13 +126,13 @@ test('valid eas.json for development client builds', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
ios: {
release: { workflow: 'managed' },
debug: { workflow: 'managed', buildType: 'development-client' },
release: {},
debug: { buildType: 'development-client' },
},
android: {
release: { workflow: 'managed' },
release: {},
debug: {
workflow: 'managed',
withoutCredentials: false,
buildType: 'development-client',
},
},
@@ -150,16 +145,15 @@ test('valid eas.json for development client builds', async () => {
builds: {
android: {
credentialsSource: 'remote',
workflow: 'managed',
distribution: 'store',
env: {},
image: 'default',
withoutCredentials: false,
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
buildType: 'development-client',
},
ios: {
credentialsSource: 'remote',
workflow: 'managed',
distribution: 'store',
autoIncrement: false,
env: {},
@@ -175,7 +169,6 @@ test('valid generic profile for internal distribution on Android', async () => {
builds: {
android: {
internal: {
workflow: 'generic',
distribution: 'internal',
},
},
@@ -187,10 +180,8 @@ test('valid generic profile for internal distribution on Android', async () => {
expect({
builds: {
android: {
workflow: 'generic',
distribution: 'internal',
credentialsSource: 'remote',
gradleCommand: ':app:assembleRelease',
env: {},
withoutCredentials: false,
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
@@ -205,7 +196,6 @@ test('valid managed profile for internal distribution on Android', async () => {
builds: {
android: {
internal: {
workflow: 'managed',
distribution: 'internal',
},
},
@@ -217,10 +207,9 @@ test('valid managed profile for internal distribution on Android', async () => {
expect({
builds: {
android: {
workflow: 'managed',
buildType: 'apk',
distribution: 'internal',
credentialsSource: 'remote',
withoutCredentials: false,
env: {},
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
image: 'default',
@@ -234,7 +223,6 @@ test('invalid managed profile for internal distribution on Android', async () =>
builds: {
android: {
internal: {
workflow: 'managed',
buildType: 'aab',
distribution: 'internal',
},
@@ -253,9 +241,7 @@ test('invalid eas.json with missing preset', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
release: {
workflow: 'generic',
},
release: {},
},
},
});
@@ -271,7 +257,7 @@ test('invalid eas.json when using buildType for wrong platform', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
release: { workflow: 'managed', buildType: 'archive' },
release: { buildType: 'archive' },
},
},
});
@@ -283,22 +269,6 @@ test('invalid eas.json when using buildType for wrong platform', async () => {
);
});
test('invalid eas.json when missing workflow', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
release: { buildType: 'apk' },
},
},
});
const reader = new EasJsonReader('/project', 'android');
const promise = reader.readAsync('release');
await expect(promise).rejects.toThrowError(
'"workflow" key is required in a build profile and has to be one of ["generic", "managed"].'
);
});
test('empty json', async () => {
await fs.writeJson('/project/eas.json', {});
@@ -313,7 +283,7 @@ test('invalid semver value', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
release: { workflow: 'generic', node: '12.0.0-alpha' },
release: { node: '12.0.0-alpha' },
},
},
});
@@ -329,12 +299,12 @@ test('get profile names', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
release: { workflow: 'generic', node: '12.0.0-alpha' },
blah: { workflow: 'generic', node: '12.0.0-alpha' },
release: { node: '12.0.0-alpha' },
blah: { node: '12.0.0-alpha' },
},
ios: {
test: { workflow: 'generic', node: '12.0.0-alpha' },
blah: { workflow: 'generic', node: '12.0.0-alpha' },
test: { node: '12.0.0-alpha' },
blah: { node: '12.0.0-alpha' },
},
},
});
-4
View File
@@ -1,13 +1,9 @@
export {
CredentialsSource,
AndroidDistributionType,
AndroidManagedBuildProfile,
AndroidGenericBuildProfile,
AndroidBuildProfile,
BuildProfile,
IosDistributionType,
IosManagedBuildProfile,
IosGenericBuildProfile,
IosBuildProfile,
IosEnterpriseProvisioning,
DistributionType,
+16 -8
View File
@@ -1856,14 +1856,15 @@
xcode "^3.0.1"
xml2js "^0.4.23"
"@expo/config-plugins@2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-2.0.2.tgz#5abf85ecbf81ea16d040c2c84b1b57d1111bce86"
integrity sha512-kaTfzLfg9sjy9uAHq708FVqC2hlVQyzCmrCsnfRguk2d+5V9ZyCVdMPiDdAIKHtWCPygIPNmlIP4FYQ093RNzQ==
"@expo/config-plugins@3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-3.0.3.tgz#4aff484fd2adedbd12a1cd73daabf07b1a5af0e5"
integrity sha512-+DWHd/2L3O+njVercxGSzcBUXMm5CSzdvIK+7dU+FRhoiF57y2NzBhfNQcReYx/EY1coBrYOsMtENPhV1tWoWQ==
dependencies:
"@expo/config-types" "^41.0.0"
"@expo/json-file" "8.2.30"
"@expo/plist" "0.0.13"
debug "^4.3.1"
find-up "~5.0.0"
fs-extra "9.0.0"
getenv "^1.0.0"
@@ -1917,10 +1918,10 @@
xcode "^3.0.0"
xml-js "^1.6.11"
"@expo/[email protected].37":
version "0.2.37"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.37.tgz#fd6cb911e33774239d1ab7662c0ccc8efa22f7a2"
integrity sha512-B9gahwTTYs0pWQP2HnfYWcbsqmUIAdnr+iwounVqpcxHhERxGJby2JEw+Vxtl2+VMARM4w9RLgNduP5Vk/zeQw==
"@expo/[email protected].41":
version "0.2.41"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.41.tgz#d44ce7f5e7a1f08caa415913fed43f875fce4d52"
integrity sha512-tp3i7EZRMGvlJ3NyhpBpLdIxg6+CI6z6P3JgZVh57s2skN5zjzHcfjXPcw0gVH9fCouUnvhSSye8O+g2Zo1oaA==
dependencies:
"@hapi/joi" "^17.1.1"
@@ -5896,6 +5897,13 @@ debug@^3.1.0:
dependencies:
ms "^2.1.1"
debug@^4.3.1:
version "4.3.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
dependencies:
ms "2.1.2"
debuglog@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"