Compare commits

...
9 Commits
Author SHA1 Message Date
Wojciech Kozyra 6e1c7dd91e v0.29.0 2021-09-28 12:12:28 +02:00
Wojciech KozyraandDominik Sokal 9812406c18 [eas-cli] update eas-build-job + add xcode 13 support (#651)
* update eas-build-job adds xcode 13 support

* update CHANGELOG

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-09-28 12:04:44 +02:00
Quinlan Jung cf11ae93af [eas-cli] create GoogleServiceAccountKey (#642)
* [eas-cli] create GoogleServiceAccountKey

* pr feedback

* changelog stuff

* pr feedback
2021-09-27 17:02:02 -07:00
Wojciech Kozyra 14756ad39b support target specific Info.plist (#635) 2021-09-27 14:08:10 +02:00
Dominik Sokal 1486b481f6 use asMock instead of 'as jest.Mock' 2021-09-27 10:23:51 +02:00
Quinlan Jung 84d82e9a59 [eas-cli] change export style (#647)
* [eas-cli] change export style

* changelog stuff
2021-09-27 09:37:41 +02:00
Quinlan Jung c7ce4a8670 [eas-cli] creds manager: stop prompting every iteration (#641)
* [eas-cli] creds manager: stop prompting every iteration

* changelog stuff
2021-09-24 12:49:40 -07:00
Dominik Sokal fe0a3b4c41 fix experience with validating metro config in non-interactive mode (#644)
* fix experience with validating metro config in non-interactive mode

* update CHANGELOG
2021-09-24 11:35:28 +02:00
Dominik Sokal 0bce278bd4 update CHANGELOG 2021-09-23 11:42:56 +02:00
84 changed files with 2704 additions and 963 deletions

No files matched your search

+15 -2
View File
@@ -8,13 +8,26 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Added upload support for Google Service Account Keys. ([#642](https://github.com/expo/eas-cli/pull/642) by [@quinlanj](https://github.com/quinlanj))
- Add Xcode 13 image. ([#651](https://github.com/expo/eas-cli/pull/651) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Credentials manager: stop prompting for an Android build profile every iteration. ([#641](https://github.com/expo/eas-cli/pull/641) by [@quinlanj](https://github.com/quinlanj))
- Don't display prompt in non-interactive mode when the metro config seems invalid. ([#644](https://github.com/expo/eas-cli/pull/644) by [@dsokal](https://github.com/dsokal))
- Read versions from correct `Info.plist`. ([#635](https://github.com/expo/eas-cli/pull/635) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Move away from weird convention of exports. ([#647](https://github.com/expo/eas-cli/pull/647) by [@quinlanj](https://github.com/quinlanj))
## [0.28.2](https://github.com/expo/eas-cli/releases/tag/v0.28.2) - 2021-09-23
### 🐛 Bug fixes
- Use `log-symbols` to unify green ticks style. ([#639](https://github.com/expo/eas-cli/pull/639) by [@dsokal](https://github.com/dsokal))
- Fix `eas build --auto-submit` in fresh projects. ([#640](https://github.com/expo/eas-cli/pull/640) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
## [0.28.1](https://github.com/expo/eas-cli/releases/tag/v0.28.1) - 2021-09-22
### 🐛 Bug fixes
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.28.2"
"version": "0.29.0"
}
+26 -26
View File
@@ -73,7 +73,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/account/login.js)_
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/account/login.js)_
## `eas account:logout`
@@ -87,7 +87,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/account/logout.js)_
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/account/logout.js)_
## `eas account:view`
@@ -101,7 +101,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/account/view.js)_
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/account/view.js)_
## `eas analytics [STATUS]`
@@ -112,7 +112,7 @@ USAGE
$ eas analytics [STATUS]
```
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/analytics.js)_
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/analytics.js)_
## `eas build`
@@ -145,7 +145,7 @@ OPTIONS
--[no-]wait Wait for build(s) to complete
```
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/build/index.js)_
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/build/index.js)_
## `eas build:cancel [BUILD_ID]`
@@ -156,7 +156,7 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/build/cancel.js)_
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/build/cancel.js)_
## `eas build:configure`
@@ -170,7 +170,7 @@ OPTIONS
-p, --platform=(android|ios|all) Platform to configure
```
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/build/configure.js)_
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/build/configure.js)_
## `eas build:list`
@@ -203,7 +203,7 @@ OPTIONS
--status=(new|in-queue|in-progress|errored|finished|canceled)
```
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/build/list.js)_
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/build/list.js)_
## `eas build:view [BUILD_ID]`
@@ -217,7 +217,7 @@ OPTIONS
--json Enable JSON output, non-JSON messages will be printed to stderr
```
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/build/view.js)_
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/build/view.js)_
## `eas config`
@@ -232,7 +232,7 @@ OPTIONS
--profile=profile
```
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/config.js)_
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/config.js)_
## `eas credentials`
@@ -243,7 +243,7 @@ USAGE
$ eas credentials
```
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/credentials.js)_
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/credentials.js)_
## `eas device:create`
@@ -254,7 +254,7 @@ USAGE
$ eas device:create
```
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/device/create.js)_
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/device/create.js)_
## `eas device:list`
@@ -268,7 +268,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/device/list.js)_
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/device/list.js)_
## `eas device:view [UDID]`
@@ -279,7 +279,7 @@ USAGE
$ eas device:view [UDID]
```
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/device/view.js)_
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/device/view.js)_
## `eas diagnostics`
@@ -290,7 +290,7 @@ USAGE
$ eas diagnostics
```
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/diagnostics.js)_
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/diagnostics.js)_
## `eas help [COMMAND]`
@@ -318,7 +318,7 @@ USAGE
$ eas project:info
```
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/project/info.js)_
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/project/info.js)_
## `eas project:init`
@@ -332,7 +332,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/project/init.js)_
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/project/init.js)_
## `eas secret:create`
@@ -349,7 +349,7 @@ OPTIONS
--value=value Value of the secret
```
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/secret/create.js)_
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/secret/create.js)_
## `eas secret:delete`
@@ -366,7 +366,7 @@ DESCRIPTION
Unsure where to find the secret's ID? Run eas secrets:list
```
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/secret/delete.js)_
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/secret/delete.js)_
## `eas secret:list`
@@ -377,7 +377,7 @@ USAGE
$ eas secret:list
```
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/secret/list.js)_
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/secret/list.js)_
## `eas submit`
@@ -410,7 +410,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/submit.js)_
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/submit.js)_
## `eas webhook:create`
@@ -429,7 +429,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/webhook/create.js)_
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/webhook/create.js)_
## `eas webhook:delete [ID]`
@@ -443,7 +443,7 @@ ARGUMENTS
ID ID of the webhook to delete
```
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/webhook/delete.js)_
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/webhook/delete.js)_
## `eas webhook:list`
@@ -457,7 +457,7 @@ OPTIONS
--event=(BUILD) Event type that triggers the webhook
```
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/webhook/list.js)_
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/webhook/list.js)_
## `eas webhook:update`
@@ -477,7 +477,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/webhook/update.js)_
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/webhook/update.js)_
## `eas webhook:view ID`
@@ -491,5 +491,5 @@ ARGUMENTS
ID ID of the webhook to view
```
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.28.2/packages/eas-cli/src/commands/webhook/view.js)_
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.29.0/packages/eas-cli/src/commands/webhook/view.js)_
<!-- commandsstop -->
+2007 -607
View File
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.28.2",
"version": "0.29.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -11,8 +11,8 @@
"@expo/apple-utils": "0.0.0-alpha.25",
"@expo/config": "5.0.9",
"@expo/config-plugins": "3.1.0",
"@expo/eas-build-job": "0.2.46",
"@expo/eas-json": "^0.28.2",
"@expo/eas-build-job": "0.2.48",
"@expo/eas-json": "^0.29.0",
"@expo/json-file": "8.2.33",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.14",
@@ -25,7 +25,7 @@ interface AppJSON {
};
}
function createTestProject(
export function createTestProject(
user: Actor,
appJSONExtraData?: Record<string, object | string>
): MockProject {
@@ -58,5 +58,3 @@ function createTestProject(
},
};
}
export { createTestProject };
@@ -10,6 +10,7 @@ import {
bumpVersionAsync,
bumpVersionInAppJsonAsync,
evaluateTemplateString,
getInfoPlistPath,
readBuildNumberAsync,
readShortVersionAsync,
} from '../version';
@@ -30,9 +31,22 @@ beforeEach(() => {
});
describe(evaluateTemplateString, () => {
it('evaluates the template string', () => {
it('evaluates the template string when value is a number', () => {
expect(evaluateTemplateString('$(BLAH_BLAH)', { BLAH_BLAH: 123 })).toBe('123');
});
it('evaluates the template string when value is a string', () => {
expect(evaluateTemplateString('$(BLAH_BLAH)', { BLAH_BLAH: '123' })).toBe('123');
});
it('evaluates the template string when template is not the only element', () => {
expect(evaluateTemplateString('before$(BLAH_BLAH)after', { BLAH_BLAH: '123' })).toBe(
'before123after'
);
});
it('evaluates the template string when template value is double quoted', () => {
expect(evaluateTemplateString('before$(BLAH_BLAH)after', { BLAH_BLAH: '"123"' })).toBe(
'before123after'
);
});
});
// generic workflow
@@ -44,6 +58,7 @@ describe(bumpVersionAsync, () => {
bumpStrategy: BumpStrategy.BUILD_NUMBER,
projectDir: '/repo',
exp: fakeExp,
buildSettings: {},
});
const appJSON = await fs.readJSON('/repo/app.json');
@@ -58,6 +73,30 @@ describe(bumpVersionAsync, () => {
expect(infoPlist['CFBundleVersion']).toBe('2');
});
it('bumps expo.ios.buildNumber and CFBundleVersion for non default Info.plist location', async () => {
const fakeExp = initGenericProject({ infoPlistName: 'Info2.plist' });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
projectDir: '/repo',
exp: fakeExp,
buildSettings: {
INFOPLIST_FILE: '$(SRCROOT)/myproject/Info2.plist',
},
});
const appJSON = await fs.readJSON('/repo/app.json');
const infoPlist = (await readPlistAsync(
'/repo/ios/myproject/Info2.plist'
)) as IOSConfig.InfoPlist;
expect(fakeExp.version).toBe('1.0.0');
expect(fakeExp.ios?.buildNumber).toBe('2');
expect(appJSON.expo.version).toBe('1.0.0');
expect(appJSON.expo.ios.buildNumber).toBe('2');
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
expect(infoPlist['CFBundleVersion']).toBe('2');
});
it('bumps expo.version and CFBundleShortVersionString when strategy = BumpStrategy.SHORT_VERSION', async () => {
const fakeExp = initGenericProject();
@@ -65,6 +104,7 @@ describe(bumpVersionAsync, () => {
bumpStrategy: BumpStrategy.SHORT_VERSION,
projectDir: '/repo',
exp: fakeExp,
buildSettings: {},
});
const appJSON = await fs.readJSON('/repo/app.json');
@@ -86,6 +126,7 @@ describe(bumpVersionAsync, () => {
bumpStrategy: BumpStrategy.NOOP,
projectDir: '/repo',
exp: fakeExp,
buildSettings: {},
});
const appJSON = await fs.readJSON('/repo/app.json');
@@ -197,10 +238,59 @@ describe(readShortVersionAsync, () => {
});
});
describe(getInfoPlistPath, () => {
it('returns default path if INFOPLIST_FILE is not specified', () => {
vol.fromJSON(
{
'./ios/testapp/Info.plist': '',
},
'/repo'
);
const plistPath = getInfoPlistPath('/repo', {});
expect(plistPath).toBe('/repo/ios/testapp/Info.plist');
});
it('returns INFOPLIST_FILE if specified', () => {
vol.fromJSON(
{
'./ios/testapp/Info.plist': '',
},
'/repo'
);
const plistPath = getInfoPlistPath('/repo', { INFOPLIST_FILE: './qwert/NotInfo.plist' });
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
});
it('evaluates SRCROOT in Info.plist', () => {
vol.fromJSON(
{
'./ios/testapp/Info.plist': '',
},
'/repo'
);
const plistPath = getInfoPlistPath('/repo', {
INFOPLIST_FILE: '$(SRCROOT)/qwert/NotInfo.plist',
});
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
});
it('evaluates BuildSettings in Info.plist', () => {
vol.fromJSON(
{
'./ios/testapp/Info.plist': '',
},
'/repo'
);
const plistPath = getInfoPlistPath('/repo', {
INFOPLIST_FILE: '$(SRCROOT)/qwert/$(TARGET_NAME).plist',
TARGET_NAME: 'NotInfo',
});
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
});
});
function initGenericProject({
shortVersion = '1.0.0',
version = '1',
}: { shortVersion?: string; version?: string } = {}): ExpoConfig {
infoPlistName = 'Info.plist',
}: { shortVersion?: string; version?: string; infoPlistName?: string } = {}): ExpoConfig {
vol.fromJSON(
{
'./app.json': JSON.stringify({
@@ -211,7 +301,7 @@ function initGenericProject({
},
},
}),
'./ios/myproject/Info.plist': `<?xml version="1.0" encoding="UTF-8"?>
[`./ios/myproject/${infoPlistName}`]: `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
+32 -16
View File
@@ -1,7 +1,8 @@
import { IOSConfig } from '@expo/config-plugins';
import { Ios, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import type { XCBuildConfiguration } from 'xcode';
import { IosCredentials } from '../../credentials/ios/types';
import { IosCredentials, Target } from '../../credentials/ios/types';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
@@ -16,6 +17,12 @@ import { ensureIosCredentialsAsync } from './credentials';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
interface BuildConfiguration {
targetName?: string;
buildConfiguration?: string;
buildSettings: XCBuildConfiguration['buildSettings'];
}
export async function prepareIosBuildAsync(
ctx: BuildContext<Platform.IOS>
): Promise<BuildRequestSender> {
@@ -43,6 +50,7 @@ export async function prepareIosBuildAsync(
},
xcodeBuildContext
);
const buildConfiguration = resolveBuildConfiguration(ctx, targets);
return await prepareBuildRequestForPlatformAsync({
ctx,
@@ -54,24 +62,11 @@ export async function prepareIosBuildAsync(
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile,
buildSettings: buildConfiguration.buildSettings,
});
},
getMetadataContext: (): IosMetadataContext => {
if (ctx.workflow === Workflow.MANAGED) {
return { buildSettings: {} };
}
const applicationTarget = findApplicationTarget(targets);
const project = IOSConfig.XcodeUtils.getPbxproj(ctx.projectDir);
const xcBuildConfiguration = IOSConfig.Target.getXCBuildConfigurationFromPbxproj(project, {
targetName: applicationTarget.targetName,
buildConfiguration: applicationTarget.buildConfiguration,
});
const buildSettings = xcBuildConfiguration?.buildSettings ?? {};
return {
buildSettings,
targetName: applicationTarget.targetName,
buildConfiguration: applicationTarget.buildConfiguration,
};
return buildConfiguration;
},
prepareJobAsync: async (
ctx: BuildContext<Platform.IOS>,
@@ -94,3 +89,24 @@ export async function prepareIosBuildAsync(
},
});
}
function resolveBuildConfiguration(
ctx: BuildContext<Platform.IOS>,
targets: Target[]
): BuildConfiguration {
if (ctx.workflow === Workflow.MANAGED) {
return { buildSettings: {} };
}
const applicationTarget = findApplicationTarget(targets);
const project = IOSConfig.XcodeUtils.getPbxproj(ctx.projectDir);
const xcBuildConfiguration = IOSConfig.Target.getXCBuildConfigurationFromPbxproj(project, {
targetName: applicationTarget.targetName,
buildConfiguration: applicationTarget.buildConfiguration,
});
const buildSettings = xcBuildConfiguration?.buildSettings ?? {};
return {
buildSettings,
targetName: applicationTarget.targetName,
buildConfiguration: applicationTarget.buildConfiguration,
};
}
+4 -1
View File
@@ -1,6 +1,7 @@
import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { IosBuildProfile, VersionAutoIncrement } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import Log from '../../log';
import {
@@ -31,10 +32,12 @@ export async function validateAndSyncProjectConfigurationAsync({
projectDir,
exp,
buildProfile,
buildSettings,
}: {
projectDir: string;
exp: ExpoConfig;
buildProfile: IosBuildProfile;
buildSettings: XCBuildConfiguration['buildSettings'];
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const { autoIncrement } = buildProfile;
@@ -44,7 +47,7 @@ export async function validateAndSyncProjectConfigurationAsync({
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
}
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, buildSettings });
} else {
await bumpVersionInAppJsonAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
}
+45 -9
View File
@@ -3,6 +3,7 @@ import { IOSConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import path from 'path';
import semver from 'semver';
import type { XCBuildConfiguration } from 'xcode';
@@ -22,19 +23,21 @@ export async function bumpVersionAsync({
bumpStrategy,
projectDir,
exp,
buildSettings,
}: {
projectDir: string;
exp: ExpoConfig;
bumpStrategy: BumpStrategy;
buildSettings: XCBuildConfiguration['buildSettings'];
}): Promise<void> {
if (bumpStrategy === BumpStrategy.NOOP) {
return;
}
ensureStaticConfigExists(projectDir);
const infoPlist = await readInfoPlistAsync(projectDir);
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
Log.log('Updated versions in app.json');
await writeVersionsToInfoPlistAsync({ projectDir, exp, infoPlist });
await writeVersionsToInfoPlistAsync({ projectDir, exp, infoPlist, buildSettings });
Log.log('Synchronized versions with Info.plist');
}
@@ -110,7 +113,7 @@ export async function readShortVersionAsync(
): Promise<string | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir);
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
return (
infoPlist.CFBundleShortVersionString &&
evaluateTemplateString(infoPlist.CFBundleShortVersionString, buildSettings)
@@ -127,7 +130,7 @@ export async function readBuildNumberAsync(
): Promise<string | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir);
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
return (
infoPlist.CFBundleVersion && evaluateTemplateString(infoPlist.CFBundleVersion, buildSettings)
);
@@ -155,30 +158,56 @@ async function writeVersionsToInfoPlistAsync({
projectDir,
exp,
infoPlist,
buildSettings,
}: {
projectDir: string;
exp: ExpoConfig;
infoPlist: IOSConfig.InfoPlist;
buildSettings: XCBuildConfiguration['buildSettings'];
}): Promise<IOSConfig.InfoPlist> {
let updatedInfoPlist = IOSConfig.Version.setVersion(exp, infoPlist);
updatedInfoPlist = IOSConfig.Version.setBuildNumber(exp, updatedInfoPlist);
await writeInfoPlistAsync({ projectDir, infoPlist: updatedInfoPlist });
await writeInfoPlistAsync({ projectDir, infoPlist: updatedInfoPlist, buildSettings });
return updatedInfoPlist;
}
async function readInfoPlistAsync(projectDir: string): Promise<IOSConfig.InfoPlist> {
const infoPlistPath = IOSConfig.Paths.getInfoPlistPath(projectDir);
export function getInfoPlistPath(
projectDir: string,
buildSettings: XCBuildConfiguration['buildSettings']
): string {
if (buildSettings.INFOPLIST_FILE) {
const infoPlistFile = buildSettings.INFOPLIST_FILE.startsWith('"')
? buildSettings.INFOPLIST_FILE.slice(1, -1)
: buildSettings.INFOPLIST_FILE;
const iosDir = path.join(projectDir, 'ios');
const plistPath = evaluateTemplateString(infoPlistFile, {
...buildSettings,
SRCROOT: iosDir,
});
return path.isAbsolute(plistPath) ? plistPath : path.resolve(iosDir, plistPath);
} else {
return IOSConfig.Paths.getInfoPlistPath(projectDir);
}
}
async function readInfoPlistAsync(
projectDir: string,
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<IOSConfig.InfoPlist> {
const infoPlistPath = getInfoPlistPath(projectDir, buildSettings);
return (await readPlistAsync(infoPlistPath)) as IOSConfig.InfoPlist;
}
async function writeInfoPlistAsync({
projectDir,
infoPlist,
buildSettings,
}: {
projectDir: string;
infoPlist: IOSConfig.InfoPlist;
buildSettings: XCBuildConfiguration['buildSettings'];
}): Promise<void> {
const infoPlistPath = IOSConfig.Paths.getInfoPlistPath(projectDir);
const infoPlistPath = getInfoPlistPath(projectDir, buildSettings);
await writePlistAsync(infoPlistPath, infoPlist);
}
@@ -190,5 +219,12 @@ function ensureStaticConfigExists(projectDir: string): void {
}
export function evaluateTemplateString(s: string, vars: Record<string, any>): string {
return s.replace(/\$\((\w+)\)/g, (match, key) => (vars.hasOwnProperty(key) ? vars[key] : match));
return s.replace(/\$\((\w+)\)/g, (match, key) => {
if (vars.hasOwnProperty(key)) {
const value = String(vars[key]);
return value.startsWith('"') ? value.slice(1, -1) : value;
} else {
return match;
}
});
}
@@ -1,3 +1,4 @@
import { asMock } from '../../__tests__/utils';
import { flushAsync, initAsync, logEvent } from '../../analytics';
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
import { getUserAsync } from '../../user/User';
@@ -31,11 +32,15 @@ afterAll(() => {
});
beforeEach(() => {
(getUserAsync as jest.Mock).mockReset().mockImplementation(() => mockJester);
(ensureLoggedInAsync as jest.Mock).mockReset().mockImplementation(() => mockJester);
(initAsync as jest.Mock).mockReset();
(flushAsync as jest.Mock).mockReset();
(logEvent as jest.Mock).mockReset();
asMock(getUserAsync)
.mockReset()
.mockImplementation(() => mockJester);
asMock(ensureLoggedInAsync)
.mockReset()
.mockImplementation(() => mockJester);
asMock(initAsync).mockReset();
asMock(flushAsync).mockReset();
asMock(logEvent).mockReset();
});
describe(EasCommand.name, () => {
@@ -54,7 +59,7 @@ describe(EasCommand.name, () => {
}, 15_000);
it('ensures the user data is read from cache', async () => {
(TestEasCommand.prototype.authValue as jest.Mock).mockImplementationOnce(() => false);
asMock(TestEasCommand.prototype.authValue).mockImplementationOnce(() => false);
await TestEasCommand.run();
@@ -130,6 +130,7 @@ export function getNewAndroidApiMock(): { [key in keyof typeof AndroidGraphqlCli
getAndroidAppBuildCredentialsByNameAsync: jest.fn(),
createOrUpdateAndroidAppBuildCredentialsByNameAsync: jest.fn(),
createKeystoreAsync: jest.fn(),
createGoogleServiceAccountKeyAsync: jest.fn(),
createFcmAsync: jest.fn(),
deleteKeystoreAsync: jest.fn(),
deleteFcmAsync: jest.fn(),
@@ -0,0 +1,41 @@
import { GoogleServiceAccountKeyFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { promptAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { Context } from '../../context';
import { GoogleServiceAccountKey } from '../credentials';
import { readAndValidateServiceAccountKey } from '../utils/googleServiceAccountKey';
export class CreateGoogleServiceAccountKey {
constructor(private account: Account) {}
public async runAsync(ctx: Context): Promise<GoogleServiceAccountKeyFragment> {
if (ctx.nonInteractive) {
throw new Error(`New Google Service Account Key cannot be created in non-interactive mode.`);
}
const jsonKeyObject = await this.provideAsync();
const gsaKeyFragment = await ctx.android.createGoogleServiceAccountKeyAsync(
this.account,
jsonKeyObject
);
Log.succeed('Uploaded Google Service Account Key.');
return gsaKeyFragment;
}
private async provideAsync(): Promise<GoogleServiceAccountKey> {
try {
const { keyJsonPath } = await promptAsync([
{
type: 'text',
name: 'keyJsonPath',
message: 'Path to Google Service Account Key JSON file:',
validate: (value: string) => value.length > 0 || "Path can't be empty",
},
]);
return readAndValidateServiceAccountKey(keyJsonPath);
} catch (e) {
Log.error(e);
return await this.provideAsync();
}
}
}
@@ -14,9 +14,9 @@ describe(AssignFcm, () => {
// expect app credentials to be fetched/created, then updated
expect(
ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync as any
ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync
).toHaveBeenCalledTimes(1);
expect(ctx.android.updateAndroidAppCredentialsAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.updateAndroidAppCredentialsAsync).toHaveBeenCalledTimes(1);
});
it('works in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync, promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -16,7 +17,7 @@ import {
jest.mock('../../../../ora');
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
asMock(confirmAsync).mockImplementation(() => true);
describe('BuildCredentialsUtils', () => {
describe(canCopyLegacyCredentialsAsync, () => {
@@ -83,7 +84,7 @@ describe('BuildCredentialsUtils', () => {
});
describe(promptUserAndCopyLegacyCredentialsAsync, () => {
it('copies all legacy credentials to EAS if the user is eligible', async () => {
(promptAsync as jest.Mock).mockImplementation(() => ({ providedName: 'test-provided-name' }));
asMock(promptAsync).mockImplementation(() => ({ providedName: 'test-provided-name' }));
const ctx = createCtxMock({
nonInteractive: false,
android: {
@@ -103,12 +104,12 @@ describe('BuildCredentialsUtils', () => {
await promptUserAndCopyLegacyCredentialsAsync(ctx, appLookupParams);
expect(
ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync as any
ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync
).toHaveBeenCalledTimes(1);
expect(ctx.android.createFcmAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.updateAndroidAppCredentialsAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.createAndroidAppBuildCredentialsAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.createFcmAsync).toHaveBeenCalledTimes(1);
expect(ctx.android.updateAndroidAppCredentialsAsync).toHaveBeenCalledTimes(1);
expect(ctx.android.createKeystoreAsync).toHaveBeenCalledTimes(1);
expect(ctx.android.createAndroidAppBuildCredentialsAsync).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -1,10 +1,11 @@
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateFcm } from '../CreateFcm';
jest.mock('../../../../prompts');
(promptAsync as jest.Mock).mockImplementation(() => ({ fcmApiKey: 'blah' }));
asMock(promptAsync).mockImplementation(() => ({ fcmApiKey: 'blah' }));
describe(CreateFcm, () => {
it('creates an fcm api key in Interactive Mode', async () => {
@@ -14,7 +15,7 @@ describe(CreateFcm, () => {
await createFcmAction.runAsync(ctx);
// expect fcm api key to be created on expo servers
expect(ctx.android.createFcmAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.createFcmAsync).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
@@ -0,0 +1,41 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateGoogleServiceAccountKey } from '../CreateGoogleServiceAccountKey';
jest.mock('../../../../prompts');
jest.mock('fs');
asMock(promptAsync).mockImplementation(() => ({
keyJsonPath: '/google-service-account-key.json',
}));
beforeEach(() => {
vol.reset();
});
describe(CreateGoogleServiceAccountKey, () => {
it('creates a Google Service Account Key in Interactive Mode', async () => {
vol.fromJSON({
'/google-service-account-key.json': JSON.stringify({ private_key: 'super secret' }),
});
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createGsaKeyAction = new CreateGoogleServiceAccountKey(appLookupParams.account);
await createGsaKeyAction.runAsync(ctx);
// expect fcm api key to be created on expo servers
expect(ctx.android.createGoogleServiceAccountKeyAsync).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createGsaKeyAction = new CreateGoogleServiceAccountKey(appLookupParams.account);
// fail if users are running in non-interactive mode
await expect(createGsaKeyAction.runAsync(ctx)).rejects.toThrowError();
});
});
@@ -16,8 +16,8 @@ describe(DownloadKeystore, () => {
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const downloadKeystoreAction = new DownloadKeystore({ app: appLookupParams });
await downloadKeystoreAction.runAsync(ctx, testAndroidBuildCredentialsFragment);
expect(fsWriteFileSpy as any).toHaveBeenCalledTimes(1);
expect(fsWriteFileSpy as any).toHaveBeenCalledWith(
expect(fsWriteFileSpy).toHaveBeenCalledTimes(1);
expect(fsWriteFileSpy).toHaveBeenCalledWith(
`@${appLookupParams.account.name}__${appLookupParams.projectName}.jks`,
testAndroidBuildCredentialsFragment.androidKeystore?.keystore,
'base64'
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -8,7 +9,7 @@ import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { RemoveFcm } from '../RemoveFcm';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
asMock(confirmAsync).mockImplementation(() => true);
describe(RemoveFcm, () => {
it('removes an FCM Api Key', async () => {
@@ -24,7 +25,7 @@ describe(RemoveFcm, () => {
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const removeFcmApiKeyAction = new RemoveFcm(appLookupParams);
await removeFcmApiKeyAction.runAsync(ctx);
expect(ctx.android.deleteFcmAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.deleteFcmAsync).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import { testAndroidBuildCredentialsFragment } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -6,7 +7,7 @@ import { RemoveKeystore } from '../RemoveKeystore';
jest.mock('fs-extra');
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
asMock(confirmAsync).mockImplementation(() => true);
describe(RemoveKeystore, () => {
it('removes a keystore', async () => {
@@ -16,7 +17,7 @@ describe(RemoveKeystore, () => {
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const removeKeystoreAction = new RemoveKeystore(appLookupParams);
await removeKeystoreAction.runAsync(ctx, testAndroidBuildCredentialsFragment);
expect(ctx.android.deleteKeystoreAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.deleteKeystoreAsync).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
@@ -1,5 +1,6 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { AndroidKeystoreType } from '../../../../graphql/generated';
import { confirmAsync } from '../../../../prompts';
import {
@@ -18,7 +19,7 @@ import { SetupBuildCredentialsFromCredentialsJson } from '../SetupBuildCredentia
jest.mock('fs');
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
asMock(confirmAsync).mockImplementation(() => true);
jest.mock('../../../manager/SelectAndroidBuildCredentials');
beforeEach(() => {
@@ -27,7 +28,7 @@ beforeEach(() => {
describe(SetupBuildCredentialsFromCredentialsJson, () => {
it('sets up a new build configuration from credentials.json upon user request', async () => {
(SelectAndroidBuildCredentials as any).mockImplementation(() => {
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
return {
runAsync: () => {
return {
@@ -64,8 +65,8 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
await setupBuildCredentialsAction.runAsync(ctx);
// expect keystore to be created with credentials.json content
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.createKeystoreAsync as any).toBeCalledWith(appLookupParams.account, {
expect(ctx.android.createKeystoreAsync).toHaveBeenCalledTimes(1);
expect(ctx.android.createKeystoreAsync).toBeCalledWith(appLookupParams.account, {
keystorePassword: testJksAndroidKeystoreFragment.keystorePassword,
keyAlias: testJksAndroidKeystoreFragment.keyAlias,
keyPassword: testJksAndroidKeystoreFragment.keyPassword,
@@ -74,10 +75,10 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
});
// expect new build credentials to be created
expect(ctx.android.createAndroidAppBuildCredentialsAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.createAndroidAppBuildCredentialsAsync).toHaveBeenCalledTimes(1);
});
it('uses an existing build configuration upon user request', async () => {
(SelectAndroidBuildCredentials as any).mockImplementation(() => {
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
return {
runAsync: () => {
return {
@@ -114,8 +115,8 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
await setupBuildCredentialsAction.runAsync(ctx);
// expect keystore to be created with credentials.json content
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.createKeystoreAsync as any).toBeCalledWith(appLookupParams.account, {
expect(ctx.android.createKeystoreAsync).toHaveBeenCalledTimes(1);
expect(ctx.android.createKeystoreAsync).toBeCalledWith(appLookupParams.account, {
keystorePassword: testJksAndroidKeystoreFragment.keystorePassword,
keyAlias: testJksAndroidKeystoreFragment.keyAlias,
keyPassword: testJksAndroidKeystoreFragment.keyPassword,
@@ -124,7 +125,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
});
// expect existing build credentials to be updated
expect(ctx.android.updateAndroidAppBuildCredentialsAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.updateAndroidAppBuildCredentialsAsync).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -5,14 +5,16 @@ import {
AndroidKeystoreFragment,
AppFragment,
CommonAndroidAppCredentialsFragment,
GoogleServiceAccountKeyFragment,
} from '../../../graphql/generated';
import { Account } from '../../../user/Account';
import { AppQuery } from '../../ios/api/graphql/queries/AppQuery';
import { KeystoreWithType } from '../credentials';
import { GoogleServiceAccountKey, KeystoreWithType } from '../credentials';
import { AndroidAppBuildCredentialsMutation } from './graphql/mutations/AndroidAppBuildCredentialsMutation';
import { AndroidAppCredentialsMutation } from './graphql/mutations/AndroidAppCredentialsMutation';
import { AndroidFcmMutation } from './graphql/mutations/AndroidFcmMutation';
import { AndroidKeystoreMutation } from './graphql/mutations/AndroidKeystoreMutation';
import { GoogleServiceAccountKeyMutation } from './graphql/mutations/GoogleServiceAccountKeyMutation';
import { AndroidAppCredentialsQuery } from './graphql/queries/AndroidAppCredentialsQuery';
export interface AppLookupParams {
@@ -223,6 +225,16 @@ export async function deleteFcmAsync(fcm: AndroidFcmFragment): Promise<void> {
return await AndroidFcmMutation.deleteAndroidFcmAsync(fcm.id);
}
export async function createGoogleServiceAccountKeyAsync(
account: Account,
jsonKey: GoogleServiceAccountKey
): Promise<GoogleServiceAccountKeyFragment> {
return await GoogleServiceAccountKeyMutation.createGoogleServiceAccountKeyAsync(
{ jsonKey },
account.id
);
}
async function getAppAsync(appLookupParams: AppLookupParams): Promise<AppFragment> {
const projectFullName = formatProjectFullName(appLookupParams);
return await AppQuery.byFullNameAsync(projectFullName);
@@ -15,7 +15,7 @@ export type AndroidAppBuildCredentialsMetadataInput = Omit<
AndroidAppBuildCredentialsInput,
'keystoreId'
>;
const AndroidAppBuildCredentialsMutation = {
export const AndroidAppBuildCredentialsMutation = {
async createAndroidAppBuildCredentialsAsync(
androidAppBuildCredentialsInput: AndroidAppBuildCredentialsInput,
androidAppCredentialsId: string
@@ -85,5 +85,3 @@ const AndroidAppBuildCredentialsMutation = {
return data.androidAppBuildCredentials.setKeystore;
},
};
export { AndroidAppBuildCredentialsMutation };
@@ -10,7 +10,7 @@ import {
} from '../../../../../graphql/generated';
import { CommonAndroidAppCredentialsFragmentNode } from '../../../../../graphql/types/credentials/AndroidAppCredentials';
const AndroidAppCredentialsMutation = {
export const AndroidAppCredentialsMutation = {
async createAndroidAppCredentialsAsync(
androidAppCredentialsInput: {
fcmId?: string;
@@ -83,5 +83,3 @@ const AndroidAppCredentialsMutation = {
return data.androidAppCredentials.setFcm;
},
};
export { AndroidAppCredentialsMutation };
@@ -11,7 +11,7 @@ import {
} from '../../../../../graphql/generated';
import { AndroidFcmFragmentNode } from '../../../../../graphql/types/credentials/AndroidFcm';
const AndroidFcmMutation = {
export const AndroidFcmMutation = {
async createAndroidFcmAsync(
androidFcmInput: AndroidFcmInput,
accountId: string
@@ -67,5 +67,3 @@ const AndroidFcmMutation = {
);
},
};
export { AndroidFcmMutation };
@@ -11,7 +11,7 @@ import {
} from '../../../../../graphql/generated';
import { AndroidKeystoreFragmentNode } from '../../../../../graphql/types/credentials/AndroidKeystore';
const AndroidKeystoreMutation = {
export const AndroidKeystoreMutation = {
async createAndroidKeystoreAsync(
androidKeystoreInput: AndroidKeystoreInput,
accountId: string
@@ -73,5 +73,3 @@ const AndroidKeystoreMutation = {
);
},
};
export { AndroidKeystoreMutation };
@@ -0,0 +1,51 @@
import assert from 'assert';
import { print } from 'graphql';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
import {
CreateGoogleServiceAccountKeyMutation,
GoogleServiceAccountKeyFragment,
GoogleServiceAccountKeyInput,
} from '../../../../../graphql/generated';
import { GoogleServiceAccountKeyFragmentNode } from '../../../../../graphql/types/credentials/GoogleServiceAccountKey';
export const GoogleServiceAccountKeyMutation = {
async createGoogleServiceAccountKeyAsync(
googleServiceAccountKeyInput: GoogleServiceAccountKeyInput,
accountId: string
): Promise<GoogleServiceAccountKeyFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateGoogleServiceAccountKeyMutation>(
gql`
mutation CreateGoogleServiceAccountKeyMutation(
$googleServiceAccountKeyInput: GoogleServiceAccountKeyInput!
$accountId: ID!
) {
googleServiceAccountKey {
createGoogleServiceAccountKey(
googleServiceAccountKeyInput: $googleServiceAccountKeyInput
accountId: $accountId
) {
id
...GoogleServiceAccountKeyFragment
}
}
}
${print(GoogleServiceAccountKeyFragmentNode)}
`,
{
googleServiceAccountKeyInput,
accountId,
}
)
.toPromise()
);
assert(
data.googleServiceAccountKey.createGoogleServiceAccountKey,
'GraphQL: `createAndroidFcm` not defined in server response'
);
return data.googleServiceAccountKey.createGoogleServiceAccountKey;
},
};
@@ -9,7 +9,7 @@ import {
} from '../../../../../graphql/generated';
import { CommonAndroidAppCredentialsFragmentNode } from '../../../../../graphql/types/credentials/AndroidAppCredentials';
const AndroidAppCredentialsQuery = {
export const AndroidAppCredentialsQuery = {
async withCommonFieldsByApplicationIdentifierAsync(
projectFullName: string,
{
@@ -66,5 +66,3 @@ const AndroidAppCredentialsQuery = {
return data.app.byFullName.androidAppCredentials[0] ?? null;
},
};
export { AndroidAppCredentialsQuery };
@@ -16,6 +16,11 @@ export type KeystoreWithType = Keystore & {
type: AndroidKeystoreType;
};
export type GoogleServiceAccountKey = {
[key: string]: any;
private_key: string;
};
export type AndroidCredentials = {
experienceName: string;
keystore: Keystore | null;
@@ -1,5 +1,6 @@
import mockdate from 'mockdate';
import { asMock } from '../../../../__tests__/utils';
import Log from '../../../../log';
import {
testAndroidAppCredentialsFragment,
@@ -22,9 +23,7 @@ describe('print credentials', () => {
appLookupParams,
appCredentials: testAndroidAppCredentialsFragment,
});
const loggedSoFar = (Log.log as jest.Mock).mock.calls.reduce(
(acc, mockValue) => acc + mockValue
);
const loggedSoFar = asMock(Log.log).mock.calls.reduce((acc, mockValue) => acc + mockValue);
expect(loggedSoFar).toMatchSnapshot();
});
});
@@ -0,0 +1,36 @@
import JsonFile from '@expo/json-file';
import Joi from 'joi';
import Log, { learnMore } from '../../../log';
import { GoogleServiceAccountKey } from '../credentials';
export const MinimalGoogleServiceAccountKeySchema = Joi.object({
private_key: Joi.string().required(),
});
export function readAndValidateServiceAccountKey(keyJsonPath: string): GoogleServiceAccountKey {
try {
const jsonKeyObject = JsonFile.read(keyJsonPath);
const { error, value } = MinimalGoogleServiceAccountKeySchema.validate(jsonKeyObject, {
abortEarly: false,
allowUnknown: true,
});
if (error) {
const maybeGoogleServicesJson = keyJsonPath.includes('google-services');
if (maybeGoogleServicesJson) {
Log.error(
`Oops! Looks like you uploaded a google-services.json instead of your service account key. ${learnMore(
'https://expo.fyi/creating-google-service-account'
)}`
);
}
throw new Error(`Google Service Account JSON Key is not valid [${error.toString()}].`);
}
return value as unknown as GoogleServiceAccountKey;
} catch (err: any) {
if (err.code === 'EJSONPARSE') {
err.message = `Found invalid JSON in Google Service Account JSON Key. ${err.message}`;
}
throw err;
}
}
@@ -14,10 +14,8 @@ describe(AssignPushKey, () => {
await assignPushKeyAction.runAsync(ctx, testPushKey);
// expect app credentials to be fetched/created, then updated
expect(ctx.ios.createOrGetIosAppCredentialsWithCommonFieldsAsync as any).toHaveBeenCalledTimes(
1
);
expect(ctx.ios.updateIosAppCredentialsAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.ios.createOrGetIosAppCredentialsWithCommonFieldsAsync).toHaveBeenCalledTimes(1);
expect(ctx.ios.updateIosAppCredentialsAsync).toHaveBeenCalledTimes(1);
});
it('works in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -14,7 +15,7 @@ import { ConfigureProvisioningProfile } from '../ConfigureProvisioningProfile';
jest.mock('../../../../ora');
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
asMock(confirmAsync).mockImplementation(() => true);
describe('ConfigureProvisioningProfile', () => {
it('configures a Provisioning Profile in Interactive Mode', async () => {
@@ -42,9 +43,9 @@ describe('ConfigureProvisioningProfile', () => {
await provProfConfigurator.runAsync(ctx);
// expect provisioning profile not to be updated on expo servers
expect((ctx.ios.updateProvisioningProfileAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(1);
// expect provisioning profile not to be updated on apple portal
expect((ctx.appStore.useExistingProvisioningProfileAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -61,8 +62,8 @@ describe('ConfigureProvisioningProfile', () => {
);
// expect provisioning profile not to be updated on expo servers
expect((ctx.ios.updateProvisioningProfileAsync as any).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be updated on apple portal
expect((ctx.appStore.useExistingProvisioningProfileAsync as any).mock.calls.length).toBe(0);
expect(asMock(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(0);
});
});
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -10,8 +11,9 @@ import {
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { CreateProvisioningProfile } from '../CreateProvisioningProfile';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
asMock(confirmAsync).mockImplementation(() => true);
describe('CreateProvisioningProfile', () => {
it('creates a Provisioning Profile in Interactive Mode', async () => {
@@ -32,9 +34,9 @@ describe('CreateProvisioningProfile', () => {
await createProvProfAction.runAsync(ctx);
// expect provisioning profile to be created on expo servers
expect((ctx.ios.createProvisioningProfileAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
// expect provisioning profile to be created on apple portal
expect((ctx.appStore.createProvisioningProfileAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.appStore.createProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -50,8 +52,8 @@ describe('CreateProvisioningProfile', () => {
);
// expect provisioning profile not to be created on expo servers
expect((ctx.ios.createProvisioningProfileAsync as any).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be created on apple portal
expect((ctx.appStore.createProvisioningProfileAsync as any).mock.calls.length).toBe(0);
expect(asMock(ctx.appStore.createProvisioningProfileAsync).mock.calls.length).toBe(0);
});
});
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -7,7 +8,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { CreatePushKey } from '../CreatePushKey';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
asMock(confirmAsync).mockImplementation(() => true);
describe(CreatePushKey, () => {
it('creates a Push Key in Interactive Mode', async () => {
@@ -24,9 +25,9 @@ describe(CreatePushKey, () => {
await createPushKeyAction.runAsync(ctx);
// expect push key to be created on expo servers
expect((ctx.ios.createPushKeyAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(1);
// expect push key to be created on apple portal
expect((ctx.appStore.createPushKeyAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.appStore.createPushKeyAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -10,7 +11,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { RemoveDistributionCertificate } from '../RemoveDistributionCertificate';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
asMock(confirmAsync).mockImplementation(() => true);
describe('RemoveDistributionCertificate', () => {
it('deletes the distribution certificate on Expo and Apple servers when there are no App Dependencies in Interactive Mode', async () => {
@@ -23,11 +24,11 @@ describe('RemoveDistributionCertificate', () => {
await removeDistCertAction.runAsync(ctx);
// expect dist cert to be deleted on expo servers
expect((ctx.ios.deleteDistributionCertificateAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect dist cert to be deleted on apple portal
expect((ctx.appStore.revokeDistributionCertificateAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile deletion to be skipped because there arent any associated with the dist cert
expect((ctx.ios.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('deletes the distribution certificate on Expo servers when there are no App Dependencies in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
@@ -39,11 +40,11 @@ describe('RemoveDistributionCertificate', () => {
await removeDistCertAction.runAsync(ctx);
// expect dist cert to be deleted on expo servers
expect((ctx.ios.deleteDistributionCertificateAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
// not supported in non-interactive mode
expect((ctx.appStore.revokeDistributionCertificateAsync as any).mock.calls.length).toBe(0);
expect(asMock(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(0);
// expect provisioning profile deletion to be skipped because there arent any associated with the dist cert
expect((ctx.ios.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('deletes the distribution certificate and its provisioning profile on Expo and Apple servers when there are App Dependencies in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
@@ -55,11 +56,11 @@ describe('RemoveDistributionCertificate', () => {
await removeDistCertAction.runAsync(ctx);
// expect dist cert to be deleted on expo servers
expect((ctx.ios.deleteDistributionCertificateAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect dist cert to be deleted on apple portal
expect((ctx.appStore.revokeDistributionCertificateAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile deletion to be invoked on expo servers
expect((ctx.ios.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
});
it('errors when the distribution certificate has App Dependencies in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testTargets } from '../../../__tests__/fixtures-ios';
@@ -19,6 +20,6 @@ describe('RemoveProvisioningProfile', () => {
await removeProfilesAction.runAsync(ctx);
// expect provisioning profile to be deleted from servers
expect((ctx.ios.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
});
});
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -6,7 +7,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { RemovePushKey } from '../RemovePushKey';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
asMock(confirmAsync).mockImplementation(() => true);
describe(RemovePushKey, () => {
it('deletes the push key on Expo and Apple servers in Interactive Mode', async () => {
@@ -16,9 +17,9 @@ describe(RemovePushKey, () => {
await removePushKeyAction.runAsync(ctx);
// expect push key to be deleted on expo servers
expect((ctx.ios.deletePushKeyAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.deletePushKeyAsync).mock.calls.length).toBe(1);
// expect push key to be revoked on apple portal
expect((ctx.appStore.revokePushKeyAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.appStore.revokePushKeyAsync).mock.calls.length).toBe(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
@@ -1,5 +1,6 @@
import nullthrows from 'nullthrows';
import { asMock } from '../../../../__tests__/utils';
import { IosDistributionType } from '../../../../graphql/generated';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
@@ -16,8 +17,9 @@ import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { validateProvisioningProfileAsync } from '../../validators/validateProvisioningProfile';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetupProvisioningProfile } from '../SetupProvisioningProfile';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
asMock(confirmAsync).mockImplementation(() => true);
jest.mock('../SetupDistributionCertificate');
jest.mock('../ConfigureProvisioningProfile');
jest.mock('../CreateProvisioningProfile');
@@ -25,7 +27,7 @@ jest.mock('../../validators/validateProvisioningProfile');
describe('SetupProvisioningProfile', () => {
it('repairs existing Provisioning Profile with bad build credentials in Interactive Mode', async () => {
(validateProvisioningProfileAsync as jest.Mock).mockImplementation(() => false);
asMock(validateProvisioningProfileAsync).mockImplementation(() => false);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -59,12 +61,12 @@ describe('SetupProvisioningProfile', () => {
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect((ctx.ios.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
// expect provisioning profile not to be deleted on expo servers
expect((ctx.ios.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('sets up a new Provisioning Profile with bad build credentials in Interactive Mode', async () => {
(validateProvisioningProfileAsync as jest.Mock).mockImplementation(() => false);
asMock(validateProvisioningProfileAsync).mockImplementation(() => false);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -89,12 +91,12 @@ describe('SetupProvisioningProfile', () => {
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect((ctx.ios.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
// expect provisioning profile to be deleted on expo servers
expect((ctx.ios.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
});
it('skips setting up a Provisioning Profile with prior build credentials configured properly in Interactive Mode', async () => {
(validateProvisioningProfileAsync as jest.Mock).mockImplementation(() => true);
asMock(validateProvisioningProfileAsync).mockImplementation(() => true);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -119,12 +121,12 @@ describe('SetupProvisioningProfile', () => {
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials not to be created or updated on expo servers
expect((ctx.ios.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be deleted on expo servers
expect((ctx.ios.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('sets up a Provisioning Profile with no prior build credentials configured in Interactive Mode', async () => {
(validateProvisioningProfileAsync as jest.Mock).mockImplementation(() => false);
asMock(validateProvisioningProfileAsync).mockImplementation(() => false);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -145,12 +147,12 @@ describe('SetupProvisioningProfile', () => {
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect((ctx.ios.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
// expect provisioning profile not to be deleted on expo servers
expect((ctx.ios.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('errors in Non Interactive Mode', async () => {
(validateProvisioningProfileAsync as jest.Mock).mockImplementation(() => false);
asMock(validateProvisioningProfileAsync).mockImplementation(() => false);
const ctx = createCtxMock({
nonInteractive: true,
});
@@ -164,8 +166,8 @@ describe('SetupProvisioningProfile', () => {
);
// expect build credentials not to be created or updated on expo servers
expect((ctx.ios.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be deleted on expo servers
expect((ctx.ios.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
});
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -7,7 +8,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetupPushKey } from '../SetupPushKey';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
asMock(confirmAsync).mockImplementation(() => true);
describe(SetupPushKey, () => {
it('skips setting up a Push Key if it is already configured', async () => {
@@ -23,9 +24,9 @@ describe(SetupPushKey, () => {
await setupPushKeyAction.runAsync(ctx);
// expect not to create a new push key on expo servers
expect((ctx.ios.createPushKeyAsync as any).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(0);
// expect configuration not to be updated with a new push key
expect((ctx.ios.updateIosAppCredentialsAsync as any).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
});
it('sets up a Push Key, creating a new one if there are no existing push keys', async () => {
const ctx = createCtxMock({
@@ -41,9 +42,9 @@ describe(SetupPushKey, () => {
await setupPushKeyAction.runAsync(ctx);
// expect to create a new push key on expo servers
expect((ctx.ios.createPushKeyAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(1);
// expect configuration to be updated with a new push key
expect((ctx.ios.updateIosAppCredentialsAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('sets up a Push Key, use autoselected push key when there are existing keys', async () => {
const ctx = createCtxMock({
@@ -65,9 +66,9 @@ describe(SetupPushKey, () => {
await setupPushKeyAction.runAsync(ctx);
// expect not to create a new push key on expo servers, we are using the existing one
expect((ctx.ios.createPushKeyAsync as any).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(0);
// expect configuration not to be updated with a new push key
expect((ctx.ios.updateIosAppCredentialsAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -22,7 +22,7 @@ jest.mock('../../../credentialsJson/read');
beforeEach(() => {
asMock(confirmAsync).mockReset();
(confirmAsync as jest.Mock).mockImplementation(() => true);
asMock(confirmAsync).mockImplementation(() => true);
});
describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
@@ -61,11 +61,11 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials not to be created or updated on expo servers
expect((ctx.ios.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(0);
// expect distribution certificate not to be uploaded on expo servers
expect((ctx.ios.createDistributionCertificateAsync as any).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be uploaded on expo servers
expect((ctx.ios.createProvisioningProfileAsync as any).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(0);
});
it('sets up build credentials with no prior configuration in Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -90,11 +90,11 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect((ctx.ios.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
// expect distribution certificate to be uploaded on expo servers
expect((ctx.ios.createDistributionCertificateAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile to be uploaded on expo servers
expect((ctx.ios.createProvisioningProfileAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('sets up build credentials with different prior configuration in Interactive Mode', async () => {
// create deep clone the quick and dirty way
@@ -123,11 +123,11 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect((ctx.ios.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
// expect distribution certificate to be uploaded on expo servers
expect((ctx.ios.createDistributionCertificateAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile to be uploaded on expo servers
expect((ctx.ios.createProvisioningProfileAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('works in Non Interactive Mode - sets up build credentials with different prior configuration', async () => {
// create deep clone the quick and dirty way
@@ -157,12 +157,12 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect no prompts to be called
expect((confirmAsync as any).mock.calls.length).toBe(0);
expect(asMock(confirmAsync).mock.calls.length).toBe(0);
// expect build credentials to be created or updated on expo servers
expect((ctx.ios.createOrUpdateIosAppBuildCredentialsAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
// expect distribution certificate to be uploaded on expo servers
expect((ctx.ios.createDistributionCertificateAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile to be uploaded on expo servers
expect((ctx.ios.createProvisioningProfileAsync as any).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
});
});
@@ -10,7 +10,7 @@ import {
} from '../../../../../graphql/generated';
import { AppleAppIdentifierFragmentNode } from '../../../../../graphql/types/credentials/AppleAppIdentifier';
const AppleAppIdentifierMutation = {
export const AppleAppIdentifierMutation = {
async createAppleAppIdentifierAsync(
appleAppIdentifierInput: AppleAppIdentifierInput,
accountId: string
@@ -49,5 +49,3 @@ const AppleAppIdentifierMutation = {
return data.appleAppIdentifier.createAppleAppIdentifier;
},
};
export { AppleAppIdentifierMutation };
@@ -9,7 +9,7 @@ import {
} from '../../../../../graphql/generated';
import { AppleDeviceFragmentNode } from '../../../../../graphql/types/credentials/AppleDevice';
const AppleDeviceMutation = {
export const AppleDeviceMutation = {
async createAppleDeviceAsync(
appleDeviceInput: AppleDeviceInput,
accountId: string
@@ -41,5 +41,3 @@ const AppleDeviceMutation = {
return data.appleDevice.createAppleDevice;
},
};
export { AppleDeviceMutation };
@@ -8,7 +8,7 @@ import {
} from '../../../../../graphql/generated';
import { AppleDeviceRegistrationRequestFragmentNode } from '../../../../../graphql/types/credentials/AppleDeviceRegistrationRequest';
const AppleDeviceRegistrationRequestMutation = {
export const AppleDeviceRegistrationRequestMutation = {
async createAppleDeviceRegistrationRequestAsync(
appleTeamId: string,
accountId: string
@@ -43,5 +43,3 @@ const AppleDeviceRegistrationRequestMutation = {
return data.appleDeviceRegistrationRequest.createAppleDeviceRegistrationRequest;
},
};
export { AppleDeviceRegistrationRequestMutation };
@@ -17,7 +17,7 @@ export type AppleDistributionCertificateMutationResult = AppleDistributionCertif
appleTeam?: AppleTeamFragment | null;
};
const AppleDistributionCertificateMutation = {
export const AppleDistributionCertificateMutation = {
async createAppleDistributionCertificateAsync(
appleDistributionCertificateInput: AppleDistributionCertificateInput,
accountId: string
@@ -88,5 +88,3 @@ const AppleDistributionCertificateMutation = {
);
},
};
export { AppleDistributionCertificateMutation };
@@ -16,7 +16,7 @@ export type AppleProvisioningProfileMutationResult = AppleProvisioningProfileFra
appleTeam?: AppleTeamFragment | null;
};
const AppleProvisioningProfileMutation = {
export const AppleProvisioningProfileMutation = {
async createAppleProvisioningProfileAsync(
appleProvisioningProfileInput: AppleProvisioningProfileInput,
accountId: string,
@@ -124,5 +124,3 @@ const AppleProvisioningProfileMutation = {
);
},
};
export { AppleProvisioningProfileMutation };
@@ -11,7 +11,7 @@ import {
} from '../../../../../graphql/generated';
import { ApplePushKeyFragmentNode } from '../../../../../graphql/types/credentials/ApplePushKey';
const ApplePushKeyMutation = {
export const ApplePushKeyMutation = {
async createApplePushKeyAsync(
applePushKeyInput: ApplePushKeyInput,
accountId: string
@@ -70,5 +70,3 @@ const ApplePushKeyMutation = {
);
},
};
export { ApplePushKeyMutation };
@@ -14,7 +14,7 @@ export type AppleTeamMutationResult = AppleTeamFragment & {
account: Account;
};
const AppleTeamMutation = {
export const AppleTeamMutation = {
async createAppleTeamAsync(
appleTeamInput: AppleTeamInput,
accountId: string
@@ -47,5 +47,3 @@ const AppleTeamMutation = {
return data.appleTeam.createAppleTeam;
},
};
export { AppleTeamMutation };
@@ -11,7 +11,7 @@ import {
} from '../../../../../graphql/generated';
import { IosAppBuildCredentialsFragmentNode } from '../../../../../graphql/types/credentials/IosAppBuildCredentials';
const IosAppBuildCredentialsMutation = {
export const IosAppBuildCredentialsMutation = {
async createIosAppBuildCredentialsAsync(
iosAppBuildCredentialsInput: IosAppBuildCredentialsInput,
iosAppCredentialsId: string
@@ -120,5 +120,3 @@ const IosAppBuildCredentialsMutation = {
return data.iosAppBuildCredentials.setProvisioningProfile;
},
};
export { IosAppBuildCredentialsMutation };
@@ -11,7 +11,7 @@ import {
} from '../../../../../graphql/generated';
import { CommonIosAppCredentialsFragmentNode } from '../../../../../graphql/types/credentials/IosAppCredentials';
const IosAppCredentialsMutation = {
export const IosAppCredentialsMutation = {
async createIosAppCredentialsAsync(
iosAppCredentialsInput: IosAppCredentialsInput,
appId: string,
@@ -81,5 +81,3 @@ const IosAppCredentialsMutation = {
return data.iosAppCredentials.setPushKey;
},
};
export { IosAppCredentialsMutation };
@@ -1,6 +1,6 @@
import { AppleDevice } from '../../../../../../graphql/generated';
const AppleDeviceMutation = {
export const AppleDeviceMutation = {
createAppleDeviceAsync: jest.fn().mockImplementation(() => {
const appleDevice: Pick<AppleDevice, 'id' | 'identifier'> = {
id: 'apple-device-id',
@@ -9,5 +9,3 @@ const AppleDeviceMutation = {
return appleDevice;
}),
};
export { AppleDeviceMutation };
@@ -6,7 +6,7 @@ import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/cl
import { AppByFullNameQuery, AppFragment } from '../../../../../graphql/generated';
import { AppFragmentNode } from '../../../../../graphql/types/App';
const AppQuery = {
export const AppQuery = {
async byFullNameAsync(fullName: string): Promise<AppFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -31,5 +31,3 @@ const AppQuery = {
return data.app.byFullName;
},
};
export { AppQuery };
@@ -8,7 +8,7 @@ import {
} from '../../../../../graphql/generated';
import { AppleAppIdentifierFragmentNode } from '../../../../../graphql/types/credentials/AppleAppIdentifier';
const AppleAppIdentifierQuery = {
export const AppleAppIdentifierQuery = {
async byBundleIdentifierAsync(
accountName: string,
bundleIdentifier: string
@@ -46,5 +46,3 @@ const AppleAppIdentifierQuery = {
return data.account.byName.appleAppIdentifiers[0];
},
};
export { AppleAppIdentifierQuery };
@@ -31,7 +31,7 @@ export type AppleDevicesByIdentifierQueryResult = AppleDeviceQueryResult & {
appleTeam: AppleTeamFragment;
};
const AppleDeviceQuery = {
export const AppleDeviceQuery = {
async getAllByAppleTeamIdentifierAsync(
accountId: string,
appleTeamIdentifier: string,
@@ -155,5 +155,3 @@ const AppleDeviceQuery = {
return data.account.byName.appleDevices[0] ?? null;
},
};
export { AppleDeviceQuery };
@@ -12,7 +12,7 @@ import {
import { AppleDistributionCertificateFragmentNode } from '../../../../../graphql/types/credentials/AppleDistributionCertificate';
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
const AppleDistributionCertificateQuery = {
export const AppleDistributionCertificateQuery = {
async getForAppAsync(
projectFullName: string,
{
@@ -100,5 +100,3 @@ const AppleDistributionCertificateQuery = {
return data.account.byName.appleDistributionCertificates;
},
};
export { AppleDistributionCertificateQuery };
@@ -19,7 +19,7 @@ import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/
export type AppleProvisioningProfileQueryResult = AppleProvisioningProfileFragment & {
appleTeam?: AppleTeamFragment | null;
} & { appleDevices: AppleDeviceFragment[] } & { appleAppIdentifier: AppleAppIdentifierFragment };
const AppleProvisioningProfileQuery = {
export const AppleProvisioningProfileQuery = {
async getForAppAsync(
projectFullName: string,
{
@@ -89,5 +89,3 @@ const AppleProvisioningProfileQuery = {
);
},
};
export { AppleProvisioningProfileQuery };
@@ -5,7 +5,7 @@ import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/cl
import { ApplePushKeyByAccountQuery, ApplePushKeyFragment } from '../../../../../graphql/generated';
import { ApplePushKeyFragmentNode } from '../../../../../graphql/types/credentials/ApplePushKey';
const ApplePushKeyQuery = {
export const ApplePushKeyQuery = {
async getAllForAccountAsync(accountName: string): Promise<ApplePushKeyFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -33,5 +33,3 @@ const ApplePushKeyQuery = {
return data.account.byName.applePushKeys;
},
};
export { ApplePushKeyQuery };
@@ -9,7 +9,7 @@ import {
} from '../../../../../graphql/generated';
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
const AppleTeamQuery = {
export const AppleTeamQuery = {
async getAllForAccountAsync(accountName: string): Promise<AppleTeamFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -64,5 +64,3 @@ const AppleTeamQuery = {
return data.appleTeam.byAppleTeamIdentifier ?? null;
},
};
export { AppleTeamQuery };
@@ -10,7 +10,7 @@ import {
} from '../../../../../graphql/generated';
import { IosAppBuildCredentialsFragmentNode } from '../../../../../graphql/types/credentials/IosAppBuildCredentials';
const IosAppBuildCredentialsQuery = {
export const IosAppBuildCredentialsQuery = {
async byAppIdentifierIdAndDistributionTypeAsync(
projectFullName: string,
{
@@ -59,5 +59,3 @@ const IosAppBuildCredentialsQuery = {
return data.app.byFullName.iosAppCredentials[0]?.iosAppBuildCredentialsList[0] ?? null;
},
};
export { IosAppBuildCredentialsQuery };
@@ -15,7 +15,7 @@ import {
CommonIosAppCredentialsWithoutBuildCredentialsFragmentNode,
} from '../../../../../graphql/types/credentials/IosAppCredentials';
const IosAppCredentialsQuery = {
export const IosAppCredentialsQuery = {
async withBuildCredentialsByAppIdentifierIdAsync(
projectFullName: string,
{
@@ -110,5 +110,3 @@ const IosAppCredentialsQuery = {
return data.app.byFullName.iosAppCredentials[0] ?? null;
},
};
export { IosAppCredentialsQuery };
@@ -1,4 +1,4 @@
const AppleDistributionCertificateQuery = {
export const AppleDistributionCertificateQuery = {
getAllForAccountAsync: jest.fn().mockImplementation(() => {
return [
{
@@ -40,5 +40,3 @@ const AppleDistributionCertificateQuery = {
];
}),
};
export { AppleDistributionCertificateQuery };
@@ -1,6 +1,6 @@
import { AppleTeam } from '../../../../../../graphql/generated';
const AppleTeamQuery = {
export const AppleTeamQuery = {
byAppleTeamIdentifierAsync: jest.fn().mockImplementation(() => {
const appleTeam: Pick<AppleTeam, 'id' | 'appleTeamIdentifier' | 'appleTeamName'> = {
id: 'apple-team-id',
@@ -10,5 +10,3 @@ const AppleTeamQuery = {
return appleTeam;
}),
};
export { AppleTeamQuery };
@@ -1,4 +1,4 @@
const IosAppCredentialsQuery = {
export const IosAppCredentialsQuery = {
withCommonFieldsByAppIdentifierIdAsync: jest.fn().mockImplementation(() => {
return {
id: '48d0cd34-96d8-447c-9de8-40dd2c3ca0f8',
@@ -112,5 +112,3 @@ const IosAppCredentialsQuery = {
};
}),
};
export { IosAppCredentialsQuery };
@@ -1,6 +1,7 @@
import mockdate from 'mockdate';
import nullthrows from 'nullthrows';
import { asMock } from '../../../../__tests__/utils';
import Log from '../../../../log';
import { IosAppCredentialsQuery } from '../../api/graphql/queries/IosAppCredentialsQuery';
import { App, Target } from '../../types';
@@ -30,9 +31,7 @@ describe('print credentials', () => {
};
const targets: Target[] = [{ targetName: 'test52', bundleIdentifier: 'com.quinlanj.test52' }];
displayIosCredentials(app, appCredentials, targets);
const loggedSoFar = (Log.log as jest.Mock).mock.calls.reduce(
(acc, mockValue) => acc + mockValue
);
const loggedSoFar = asMock(Log.log).mock.calls.reduce((acc, mockValue) => acc + mockValue);
expect(loggedSoFar).toMatchSnapshot();
});
});
@@ -5,7 +5,7 @@ interface AppleTeam {
teamName: string;
}
function readAppleTeam(dataBase64: string): AppleTeam {
export function readAppleTeam(dataBase64: string): AppleTeam {
const profilePlist = parse(dataBase64);
const teamId = (profilePlist['TeamIdentifier'] as PlistArray)?.[0] as string;
const teamName = profilePlist['TeamName'] as string;
@@ -15,18 +15,18 @@ function readAppleTeam(dataBase64: string): AppleTeam {
return { teamId, teamName };
}
function readProfileName(dataBase64: string): string {
export function readProfileName(dataBase64: string): string {
const profilePlist = parse(dataBase64);
return profilePlist['Name'] as string;
}
function isAdHocProfile(dataBase64: string): boolean {
export function isAdHocProfile(dataBase64: string): boolean {
const profilePlist = parse(dataBase64);
const provisionedDevices = profilePlist['ProvisionedDevices'] as string[] | undefined;
return Array.isArray(provisionedDevices);
}
function parse(dataBase64: string): PlistObject {
export function parse(dataBase64: string): PlistObject {
try {
const buffer = Buffer.from(dataBase64, 'base64');
const profile = buffer.toString('utf8');
@@ -35,5 +35,3 @@ function parse(dataBase64: string): PlistObject {
throw new Error('Provisioning profile is malformed');
}
}
export { readAppleTeam, readProfileName, isAdHocProfile, parse };
@@ -145,17 +145,17 @@ export class ManageAndroid {
constructor(private callingAction: Action) {}
async runAsync(ctx: Context, currentActions: ActionInfo[] = highLevelActions): Promise<void> {
const accountName = ctx.hasProjectContext
? getProjectAccountName(ctx.exp, ctx.user)
: ensureActorHasUsername(ctx.user);
const account = findAccountByName(ctx.user.accounts, accountName);
if (!account) {
throw new Error(`You do not have access to account: ${accountName}`);
}
const { gradleContext } = await this.createProjectContextAsync(ctx);
while (true) {
try {
const accountName = ctx.hasProjectContext
? getProjectAccountName(ctx.exp, ctx.user)
: ensureActorHasUsername(ctx.user);
const account = findAccountByName(ctx.user.accounts, accountName);
if (!account) {
throw new Error(`You do not have access to account: ${accountName}`);
}
const { gradleContext } = await this.createProjectContextAsync(ctx);
if (ctx.hasProjectContext) {
const maybeProjectId = await promptToCreateProjectIfNotExistsAsync(ctx.exp);
if (!maybeProjectId) {
@@ -1,3 +1,4 @@
import { asMock } from '../../../__tests__/utils';
import { promptAsync } from '../../../prompts';
import {
getNewAndroidApiMock,
@@ -14,12 +15,12 @@ const TEST_STRING = 'TEST_STRING';
jest.mock('../../../prompts');
beforeEach(() => {
(promptAsync as jest.Mock).mockReset();
asMock(promptAsync).mockReset();
});
describe(SelectAndroidBuildCredentials, () => {
it('returns a request to make default build credentials when there are no credentials', async () => {
(promptAsync as jest.Mock).mockImplementation(() => ({
asMock(promptAsync).mockImplementation(() => ({
providedName: TEST_STRING,
}));
const ctx = createCtxMock({
@@ -37,7 +38,7 @@ describe(SelectAndroidBuildCredentials, () => {
});
});
it('returns a request to make build credentials when the user chooses to make a new one', async () => {
(promptAsync as jest.Mock).mockImplementation(() => ({
asMock(promptAsync).mockImplementation(() => ({
buildCredentialsResultOrRequestToCreateNew:
SelectAndroidBuildCredentialsResultType.CREATE_REQUEST,
providedName: TEST_STRING,
@@ -59,7 +60,7 @@ describe(SelectAndroidBuildCredentials, () => {
});
});
it('returns a request to make default build credentials when the user chooses to make a new one, and if they have no existing credentials', async () => {
(promptAsync as jest.Mock).mockImplementation(() => ({
asMock(promptAsync).mockImplementation(() => ({
buildCredentialsResultOrRequestToCreateNew:
SelectAndroidBuildCredentialsResultType.CREATE_REQUEST,
providedName: TEST_STRING,
@@ -79,7 +80,7 @@ describe(SelectAndroidBuildCredentials, () => {
});
});
it('returns buildCredentials of the users choice', async () => {
(promptAsync as jest.Mock).mockImplementation(() => ({
asMock(promptAsync).mockImplementation(() => ({
buildCredentialsResultOrRequestToCreateNew: testAndroidBuildCredentialsFragment,
}));
const ctx = createCtxMock({
@@ -50,7 +50,7 @@ export async function askForUserProvidedAsync<T>(
export async function getCredentialsFromUserAsync<T>(
credentialsSchema: CredentialSchema<T>,
initialValues: Partial<T>
): Promise<T | null> {
): Promise<T> {
const results: any = {};
for (const question of credentialsSchema.questions) {
results[question.field] = await askQuestionAndProcessAnswerAsync(
@@ -1,3 +1 @@
const runInputMethodAsync = jest.fn();
export { runInputMethodAsync };
export const runInputMethodAsync = jest.fn();
@@ -1,3 +1 @@
const runRegistrationUrlMethodAsync = jest.fn();
export { runRegistrationUrlMethodAsync };
export const runRegistrationUrlMethodAsync = jest.fn();
+47
View File
@@ -213,6 +213,8 @@ export type Account = {
appleProvisioningProfiles: Array<AppleProvisioningProfile>;
appleDevices: Array<AppleDevice>;
appleAppSpecificPasswords: Array<AppleAppSpecificPassword>;
/** Android credentials for account */
googleServiceAccountKeys: Array<GoogleServiceAccountKey>;
/** Environment secrets for an account */
environmentSecrets: Array<EnvironmentSecret>;
/** @deprecated Legacy access tokens are deprecated */
@@ -1816,6 +1818,8 @@ export type RootMutation = {
androidFcm: AndroidFcmMutation;
/** Mutations that modify a Keystore */
androidKeystore: AndroidKeystoreMutation;
/** Mutations that modify a Google Service Account Key */
googleServiceAccountKey: GoogleServiceAccountKeyMutation;
/** Mutations that modify an Identifier for an iOS App */
appleAppIdentifier: AppleAppIdentifierMutation;
/** Mutations that modify an App Specific Password for an Apple User Account */
@@ -2074,6 +2078,8 @@ export type AndroidAppCredentialsMutation = {
createAndroidAppCredentials?: Maybe<AndroidAppCredentials>;
/** Set the FCM push key to be used in an Android app */
setFcm?: Maybe<AndroidAppCredentials>;
/** Set the Google Service Account Key to be used for submitting an Android app */
setGoogleServiceAccountKeyForSubmissions?: Maybe<AndroidAppCredentials>;
};
@@ -2089,8 +2095,15 @@ export type AndroidAppCredentialsMutationSetFcmArgs = {
fcmId: Scalars['ID'];
};
export type AndroidAppCredentialsMutationSetGoogleServiceAccountKeyForSubmissionsArgs = {
id: Scalars['ID'];
googleServiceAccountKeyId: Scalars['ID'];
};
export type AndroidAppCredentialsInput = {
fcmId?: Maybe<Scalars['ID']>;
googleServiceAccountKeyForSubmissionsId?: Maybe<Scalars['ID']>;
};
export type AndroidFcmMutation = {
@@ -2154,6 +2167,22 @@ export type DeleteAndroidKeystoreResult = {
id: Scalars['ID'];
};
export type GoogleServiceAccountKeyMutation = {
__typename?: 'GoogleServiceAccountKeyMutation';
/** Create an Google Service Account Key */
createGoogleServiceAccountKey: GoogleServiceAccountKey;
};
export type GoogleServiceAccountKeyMutationCreateGoogleServiceAccountKeyArgs = {
googleServiceAccountKeyInput: GoogleServiceAccountKeyInput;
accountId: Scalars['ID'];
};
export type GoogleServiceAccountKeyInput = {
jsonKey: Scalars['JSONObject'];
};
export type AppleAppIdentifierMutation = {
__typename?: 'AppleAppIdentifierMutation';
/** Create an Identifier for an iOS App */
@@ -3957,6 +3986,24 @@ export type DeleteAndroidKeystoreMutation = (
) }
);
export type CreateGoogleServiceAccountKeyMutationVariables = Exact<{
googleServiceAccountKeyInput: GoogleServiceAccountKeyInput;
accountId: Scalars['ID'];
}>;
export type CreateGoogleServiceAccountKeyMutation = (
{ __typename?: 'RootMutation' }
& { googleServiceAccountKey: (
{ __typename?: 'GoogleServiceAccountKeyMutation' }
& { createGoogleServiceAccountKey: (
{ __typename?: 'GoogleServiceAccountKey' }
& Pick<GoogleServiceAccountKey, 'id'>
& GoogleServiceAccountKeyFragment
) }
) }
);
export type CommonAndroidAppCredentialsWithBuildCredentialsByApplicationIdentifierQueryVariables = Exact<{
projectFullName: Scalars['String'];
applicationIdentifier?: Maybe<Scalars['String']>;
@@ -4,7 +4,7 @@ import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import { AppPrivacy, CreateAppMutation, CreateAppMutationVariables } from '../generated';
const AppMutation = {
export const AppMutation = {
async createAppAsync(appInput: {
accountId: string;
projectName: string;
@@ -33,5 +33,3 @@ const AppMutation = {
return appId;
},
};
export { AppMutation };
@@ -21,7 +21,7 @@ export interface BuildResult {
deprecationInfo?: EasBuildDeprecationInfo | null;
}
const BuildMutation = {
export const BuildMutation = {
async createAndroidBuildAsync(input: {
appId: string;
job: AndroidJobInput;
@@ -93,5 +93,3 @@ const BuildMutation = {
return nullthrows(data.build?.createIosBuild);
},
};
export { BuildMutation };
@@ -3,7 +3,7 @@ import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import { PublishUpdateGroupInput, UpdatePublishMutation } from '../generated';
const PublishMutation = {
export const PublishMutation = {
async getUploadURLsAsync(contentTypes: string[]): Promise<{ specifications: string[] }> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -54,5 +54,3 @@ const PublishMutation = {
return data.updateBranch.publishUpdateGroups;
},
};
export { PublishMutation };
@@ -5,7 +5,7 @@ import { Project, ProjectByUsernameAndSlugQuery } from '../generated';
type ProjectQueryResult = Pick<Project, 'id'>;
const ProjectQuery = {
export const ProjectQuery = {
async byUsernameAndSlugAsync(username: string, slug: string): Promise<ProjectQueryResult> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -27,5 +27,3 @@ const ProjectQuery = {
return data.project.byUsernameAndSlug;
},
};
export { ProjectQuery };
@@ -3,7 +3,7 @@ import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import { AssetMetadataResult, GetAssetMetadataQuery } from '../generated';
const PublishQuery = {
export const PublishQuery = {
async getAssetMetadataAsync(storageKeys: string[]): Promise<AssetMetadataResult[]> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -28,5 +28,3 @@ const PublishQuery = {
return data.asset.metadata;
},
};
export { PublishQuery };
@@ -3,7 +3,7 @@ import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import { CurrentUserQuery } from '../generated';
const UserQuery = {
export const UserQuery = {
async currentUserAsync(): Promise<CurrentUserQuery['meActor']> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -34,5 +34,3 @@ const UserQuery = {
return data.meActor;
},
};
export { UserQuery };
@@ -25,7 +25,7 @@ interface AppJSON {
};
}
function createTestProject(
export function createTestProject(
user: Actor,
appJSONExtraData?: Record<string, object | string>
): MockProject {
@@ -58,5 +58,3 @@ function createTestProject(
},
};
}
export { createTestProject };
@@ -192,7 +192,7 @@ describe(promptToCreateProjectIfNotExistsAsync, () => {
asMock(ensureProjectExistsAsync).mockImplementation(() => 'new-project-id');
// user agrees to make new project
(confirmAsync as jest.Mock).mockImplementation(() => true);
asMock(confirmAsync).mockImplementation(() => true);
const exp: any = {};
const projectId = await promptToCreateProjectIfNotExistsAsync(exp);
expect(projectId).toBe('new-project-id');
@@ -210,7 +210,7 @@ describe(promptToCreateProjectIfNotExistsAsync, () => {
asMock(ensureProjectExistsAsync).mockImplementation(() => 'new-project-id');
// user doesnt agree to make new project
(confirmAsync as jest.Mock).mockImplementation(() => false);
asMock(confirmAsync).mockImplementation(() => false);
const exp: any = {};
const projectId = await promptToCreateProjectIfNotExistsAsync(exp);
expect(projectId).toBe(null);
+13 -3
View File
@@ -1,4 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import { exit } from '@oclif/errors';
import chalk from 'chalk';
import type MetroConfig from 'metro-config';
import resolveFrom from 'resolve-from';
@@ -27,19 +28,28 @@ export async function validateMetroConfigForManagedWorkflowAsync(
Log.warn(
'This can result in unexpected and hard to debug issues, like missing assets in the production bundle.'
);
Log.warn(`We recommend you to abort, fix the ${chalk.bold('metro.config.js')}, and try again.`);
if (!ctx.nonInteractive) {
Log.warn(
`We recommend you to abort, fix the ${chalk.bold('metro.config.js')}, and try again.`
);
}
Log.warn(
learnMore('https://docs.expo.dev/guides/customizing-metro/', {
learnMoreMessage: 'Learn more on customizing Metro',
})
);
if (ctx.nonInteractive) {
Log.warn("You've run EAS CLI in non-interactive mode, proceeding...");
return;
}
const shouldAbort = await confirmAsync({
message: 'Would you like to abort?',
});
if (shouldAbort) {
Log.log('Aborting...');
process.exit(1);
Log.error('Aborting...');
exit(1);
}
}
}
@@ -1,4 +1,5 @@
import ApiV2Error from '../../ApiV2Error';
import { asMock } from '../../__tests__/utils';
import { promptAsync } from '../../prompts';
import { Actor, loginAsync } from '../User';
import { ensureActorHasUsername, showLoginPromptAsync } from '../actions';
@@ -12,12 +13,12 @@ jest.mock('../User', () => ({
}));
beforeEach(() => {
(promptAsync as jest.Mock).mockReset();
(promptAsync as jest.Mock).mockImplementation(() => {
asMock(promptAsync).mockReset();
asMock(promptAsync).mockImplementation(() => {
throw new Error('Should not be called');
});
(loginAsync as jest.Mock).mockReset();
asMock(loginAsync).mockReset();
});
const userStub: Actor = {
@@ -48,13 +49,13 @@ describe('ensureActorHasUsername', () => {
describe(showLoginPromptAsync, () => {
it('prompts for OTP when 2FA is enabled', async () => {
(promptAsync as jest.Mock)
asMock(promptAsync)
.mockImplementationOnce(() => ({ username: 'hello', password: 'world' }))
.mockImplementationOnce(() => ({ otp: '123456' }))
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
(loginAsync as jest.Mock)
asMock(loginAsync)
.mockImplementationOnce(async () => {
throw new ApiV2Error({ code: 'testcode', request: {} } as any, {
message: 'An OTP is required',
@@ -76,20 +77,16 @@ describe(showLoginPromptAsync, () => {
await showLoginPromptAsync();
expect(retryUsernamePasswordAuthWithOTPAsync as jest.Mock).toHaveBeenCalledWith(
'hello',
'world',
{
secondFactorDevices: [
{
id: 'p0',
is_primary: true,
method: UserSecondFactorDeviceMethod.SMS,
sms_phone_number: 'testphone',
},
],
smsAutomaticallySent: true,
}
);
expect(retryUsernamePasswordAuthWithOTPAsync).toHaveBeenCalledWith('hello', 'world', {
secondFactorDevices: [
{
id: 'p0',
is_primary: true,
method: UserSecondFactorDeviceMethod.SMS,
sms_phone_number: 'testphone',
},
],
smsAutomaticallySent: true,
});
});
});
+26 -25
View File
@@ -1,3 +1,4 @@
import { asMock } from '../../__tests__/utils';
import { apiClient } from '../../api';
import Log from '../../log';
import { promptAsync, selectAsync } from '../../prompts';
@@ -12,23 +13,23 @@ jest.mock('../User', () => ({
jest.mock('../../log');
beforeEach(() => {
(promptAsync as jest.Mock).mockReset();
(promptAsync as jest.Mock).mockImplementation(() => {
asMock(promptAsync).mockReset();
asMock(promptAsync).mockImplementation(() => {
throw new Error('Should not be called');
});
(selectAsync as jest.Mock).mockReset();
(selectAsync as jest.Mock).mockImplementation(() => {
asMock(selectAsync).mockReset();
asMock(selectAsync).mockImplementation(() => {
throw new Error('Should not be called');
});
(loginAsync as jest.Mock).mockReset();
(Log.log as jest.Mock).mockReset();
asMock(loginAsync).mockReset();
asMock(Log.log).mockReset();
});
describe(retryUsernamePasswordAuthWithOTPAsync, () => {
it('shows SMS OTP prompt when SMS is primary and code was automatically sent', async () => {
(promptAsync as jest.Mock)
asMock(promptAsync)
.mockImplementationOnce(() => ({ otp: 'hello' }))
.mockImplementation(() => {
throw new Error("shouldn't happen");
@@ -50,11 +51,11 @@ describe(retryUsernamePasswordAuthWithOTPAsync, () => {
'One-time password was sent to the phone number ending in testphone.'
);
expect(loginAsync as jest.Mock).toHaveBeenCalledTimes(1);
expect(asMock(loginAsync)).toHaveBeenCalledTimes(1);
});
it('shows authenticator OTP prompt when authenticator is primary', async () => {
(promptAsync as jest.Mock)
asMock(promptAsync)
.mockImplementationOnce(() => ({ otp: 'hello' }))
.mockImplementation(() => {
throw new Error("shouldn't happen");
@@ -73,18 +74,18 @@ describe(retryUsernamePasswordAuthWithOTPAsync, () => {
});
expect(Log.log).toHaveBeenCalledWith('One-time password from authenticator required.');
expect(loginAsync as jest.Mock).toHaveBeenCalledTimes(1);
expect(asMock(loginAsync)).toHaveBeenCalledTimes(1);
});
it('shows menu when user bails on primary', async () => {
(promptAsync as jest.Mock)
asMock(promptAsync)
.mockImplementationOnce(() => ({ otp: null }))
.mockImplementationOnce(() => ({ otp: 'hello' })) // second time it is prompted after selecting backup code
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
(selectAsync as any)
asMock(selectAsync)
.mockImplementationOnce(() => -1)
.mockImplementation(() => {
throw new Error("shouldn't happen");
@@ -108,12 +109,12 @@ describe(retryUsernamePasswordAuthWithOTPAsync, () => {
smsAutomaticallySent: false,
});
expect((selectAsync as any).mock.calls.length).toEqual(1);
expect(loginAsync as jest.Mock).toHaveBeenCalledTimes(1);
expect(asMock(selectAsync).mock.calls.length).toEqual(1);
expect(asMock(loginAsync)).toHaveBeenCalledTimes(1);
});
it('shows a warning when when user bails on primary and does not have any secondary set up', async () => {
(promptAsync as jest.Mock)
asMock(promptAsync)
.mockImplementationOnce(() => ({ otp: null }))
.mockImplementation(() => {
throw new Error("shouldn't happen");
@@ -137,14 +138,14 @@ describe(retryUsernamePasswordAuthWithOTPAsync, () => {
});
it('prompts for authenticator OTP when user selects authenticator secondary', async () => {
(promptAsync as jest.Mock)
asMock(promptAsync)
.mockImplementationOnce(() => ({ otp: null }))
.mockImplementationOnce(() => ({ otp: 'hello' })) // second time it is prompted after selecting backup code
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
(selectAsync as any)
asMock(selectAsync)
.mockImplementationOnce(() => -1)
.mockImplementation(() => {
throw new Error("shouldn't happen");
@@ -168,24 +169,24 @@ describe(retryUsernamePasswordAuthWithOTPAsync, () => {
smsAutomaticallySent: false,
});
expect((promptAsync as jest.Mock).mock.calls.length).toBe(2); // first OTP, second OTP
expect(asMock(promptAsync).mock.calls.length).toBe(2); // first OTP, second OTP
});
it('requests SMS OTP and prompts for SMS OTP when user selects SMS secondary', async () => {
(promptAsync as jest.Mock)
asMock(promptAsync)
.mockImplementationOnce(() => ({ otp: null }))
.mockImplementationOnce(() => ({ otp: 'hello' })) // second time it is prompted after selecting backup code
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
(selectAsync as any)
asMock(selectAsync)
.mockImplementationOnce(() => 0)
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
(apiClient.post as jest.Mock).mockReturnValueOnce({
asMock(apiClient.post).mockReturnValueOnce({
json: () => Promise.resolve({ data: { sessionSecret: 'SESSION_SECRET' } }),
});
@@ -207,8 +208,8 @@ describe(retryUsernamePasswordAuthWithOTPAsync, () => {
smsAutomaticallySent: false,
});
expect((promptAsync as jest.Mock).mock.calls.length).toBe(2); // first OTP, second OTP
expect((apiClient.post as jest.Mock).mock.calls[0]).toEqual([
expect(asMock(promptAsync).mock.calls.length).toBe(2); // first OTP, second OTP
expect(asMock(apiClient.post).mock.calls[0]).toEqual([
'auth/send-sms-otp',
{
json: {
@@ -221,13 +222,13 @@ describe(retryUsernamePasswordAuthWithOTPAsync, () => {
});
it('exits when user bails on primary and backup', async () => {
(promptAsync as jest.Mock)
asMock(promptAsync)
.mockImplementationOnce(() => ({ otp: null }))
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
(selectAsync as any)
asMock(selectAsync)
.mockImplementationOnce(() => -2)
.mockImplementation(() => {
throw new Error("shouldn't happen");
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with the eas.json",
"version": "0.28.2",
"version": "0.29.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/eas-build-job": "0.2.46",
"@expo/eas-build-job": "0.2.48",
"@expo/json-file": "8.2.33",
"env-string": "1.0.1",
"fs-extra": "10.0.0",
Generated
+4 -4
View File
@@ -889,10 +889,10 @@
slugify "^1.3.4"
sucrase "^3.20.0"
"@expo/[email protected]6":
version "0.2.46"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.46.tgz#2742fd21a0fb0fdd2857705f931a4c10c0ad45f7"
integrity sha512-D3/RcwlLdywmT7UCtOOylLGy47W+o4UwGxrLH1j8IVYTJxyT+s/n1ss5ipbLkssEPPUfibTBysgZfwVGdCSWkQ==
"@expo/[email protected]8":
version "0.2.48"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.48.tgz#ebc2abea02cb3f23be9714318bea04515980b03d"
integrity sha512-QWg44uXndNw3ZKrP+1FD2Li1VSQoSWn94iQ6j6lwMfVfFpp6DiiZ73HXf4Bxix52cl5DTmpaE7hxYyhxSvV0yA==
dependencies:
joi "^17.4.2"