Compare commits

...
17 Commits
Author SHA1 Message Date
Dominik Sokal 5a730d70d6 v0.29.1 2021-09-29 13:21:17 +02:00
Dominik Sokal 41f58e5e52 fix double prompt for apple credentials (#654)
* add credentials context to build context

* add credentials context to submission context

* use credentials context when authenticating with apple

* don't use chalk.italic

* update CHANGELOG
2021-09-29 11:50:07 +02:00
kgc00 34af57f585 upgraded analytics client (#655) 2021-09-29 09:15:17 +02:00
Quinlan Jung 4674cbebc5 [eas-cli] use existing google service account key (#650)
* [eas-cli] use existing google service account key

* changelog

* pr feedback

* update snap
2021-09-28 15:07:10 -07:00
Quinlan Jung 69add6f523 fix changelog 2021-09-28 14:55:45 -07:00
Quinlan Jung b70e874135 [eas-cli] assign google service account key (#649)
* [eas-cli] assign google service account key

* changelog

* pr feedback
2021-09-28 14:54:41 -07:00
Dominik Sokal 03fa29dd5a make eslint disallow use of sync fs functions (#652)
* make eslint disallow use of sync fs functions

* update CHANGELOG
2021-09-28 13:41:38 +02:00
Wojciech Kozyra 9e467b900e Update CHANGELOG 2021-09-28 12:30:37 +02:00
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
176 changed files with 3826 additions and 1819 deletions

No files matched your search

+2 -1
View File
@@ -1,7 +1,7 @@
module.exports = {
root: true,
extends: ['universe/node'],
plugins: ['async-protect'],
plugins: ['async-protect', 'node'],
rules: {
'no-console': 'warn',
'no-constant-condition': ['warn', { checkLoops: false }],
@@ -41,6 +41,7 @@ module.exports = {
},
],
'async-protect/async-suffix': 'error',
'node/no-sync': 'error',
},
overrides: [
{
+30 -2
View File
@@ -8,13 +8,41 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- More upload support for Google Service Account Keys. ([#649](https://github.com/expo/eas-cli/pull/649) by [@quinlanj](https://github.com/quinlanj))
- Allow the user to assign an existing Google Service Account Key to their project. ([#650](https://github.com/expo/eas-cli/pull/650) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Fix double prompt for Apple credentials when running `eas build --auto-submit`. ([#654](https://github.com/expo/eas-cli/pull/654) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Always use async `fs` functions. ([#652](https://github.com/expo/eas-cli/pull/652) by [@dsokal](https://github.com/dsokal))
## [0.29.0](https://github.com/expo/eas-cli/releases/tag/v0.29.0) - 2021-09-28
### 🎉 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.1"
}
+1
View File
@@ -21,6 +21,7 @@
"eslint": "7.32.0",
"eslint-config-universe": "8.0.0",
"eslint-plugin-async-protect": "1.1.0",
"eslint-plugin-node": "11.1.0",
"jest": "27.1.0",
"jest-watch-typeahead": "0.6.4",
"lerna": "4.0.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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/packages/eas-cli/src/commands/webhook/view.js)_
<!-- commandsstop -->
+2
View File
@@ -1,8 +1,10 @@
import { fs } from 'memfs';
// needed because of a weird bug with tempy (dependency of @expo/config-plugins)
// eslint-disable-next-line node/no-sync
fs.mkdirSync('/tmp');
if (process.env.TMPDIR) {
// eslint-disable-next-line node/no-sync
fs.mkdirSync(process.env.TMPDIR, { recursive: true });
}
+2067 -607
View File
File diff suppressed because it is too large. Load diff
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.28.2",
"version": "0.29.1",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -11,15 +11,15 @@
"@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",
"@expo/plugin-warn-if-update-available": "1.7.0",
"@expo/prebuild-config": "2.1.0",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.0.7",
"@expo/rudder-sdk-node": "1.1.0",
"@expo/sdk-runtime-versions": "1.0.0",
"@expo/spawn-async": "1.5.0",
"@expo/timeago.js": "1.0.0",
+10 -8
View File
@@ -1,10 +1,12 @@
const fs = require('fs');
const { promises: fs } = require('fs');
// Patch `oclif-dev readme` path and link generation
let readmeContent = fs.readFileSync('README.md', 'utf8');
readmeContent = readmeContent.replace(/\[build\//g, '[src/');
readmeContent = readmeContent.replace(/build\/commands/g, 'packages/eas-cli/src/commands');
fs.writeFileSync('README.md', readmeContent);
(async () => {
// Patch `oclif-dev readme` path and link generation
let readmeContent = await fs.readFile('README.md', 'utf8');
readmeContent = readmeContent.replace(/\[build\//g, '[src/');
readmeContent = readmeContent.replace(/build\/commands/g, 'packages/eas-cli/src/commands');
await fs.writeFile('README.md', readmeContent);
// eslint-disable-next-line no-console
console.log('Patched README path generation');
// eslint-disable-next-line no-console
console.log('Patched README path generation');
})();
@@ -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 };
+6 -9
View File
@@ -65,7 +65,6 @@ export async function initAsync(): Promise<void> {
flushInterval: 300,
}
);
rudderstackClient.logger.silent = true;
}
}
@@ -87,7 +86,7 @@ export async function setUserDataAsync(userId: string, traits: Record<string, an
export async function flushAsync(): Promise<void> {
if (rudderstackClient) {
rudderstackClient.flush();
await rudderstackClient.flush();
}
}
@@ -114,13 +113,11 @@ function ensureUserIdentified(): void {
return;
}
if (rudderstackClient) {
rudderstackClient.identify({
userId: identifyData.userId,
anonymousId: identifyData.deviceId,
traits: identifyData.traits,
});
}
rudderstackClient.identify({
userId: identifyData.userId,
anonymousId: identifyData.deviceId,
traits: identifyData.traits,
});
userIdentified = true;
}
@@ -6,17 +6,17 @@ import { readChannelSafelyAsync } from '../UpdatesModule';
jest.mock('fs');
afterAll(() => {
afterAll(async () => {
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
fs.removeSync(os.tmpdir());
await fs.remove(os.tmpdir());
});
beforeEach(() => {
beforeEach(async () => {
vol.reset();
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
fs.mkdirpSync(os.tmpdir());
await fs.mkdirp(os.tmpdir());
});
const testChannel = 'test-channel';
@@ -9,17 +9,17 @@ import { maybeResolveVersionsAsync } from '../version';
jest.mock('fs');
afterAll(() => {
afterAll(async () => {
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
fs.removeSync(os.tmpdir());
await fs.remove(os.tmpdir());
});
beforeEach(() => {
beforeEach(async () => {
vol.reset();
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
fs.mkdirpSync(os.tmpdir());
await fs.mkdirp(os.tmpdir());
});
describe(maybeResolveVersionsAsync, () => {
+10 -17
View File
@@ -5,7 +5,6 @@ import nullthrows from 'nullthrows';
import AndroidCredentialsProvider, {
AndroidCredentials,
} from '../../credentials/android/AndroidCredentialsProvider';
import { createCredentialsContextAsync } from '../../credentials/context';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import Log from '../../log';
import {
@@ -110,22 +109,16 @@ async function ensureAndroidCredentialsAsync(
ctx.exp,
gradleContext
);
const provider = new AndroidCredentialsProvider(
await createCredentialsContextAsync(ctx.projectDir, {
exp: ctx.exp,
nonInteractive: ctx.nonInteractive,
}),
{
app: {
account: nullthrows(
findAccountByName(ctx.user.accounts, ctx.accountName),
`You do not have access to account: ${ctx.accountName}`
),
projectName: ctx.projectName,
androidApplicationIdentifier,
},
}
);
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
app: {
account: nullthrows(
findAccountByName(ctx.user.accounts, ctx.accountName),
`You do not have access to account: ${ctx.accountName}`
),
projectName: ctx.projectName,
androidApplicationIdentifier,
},
});
const { credentialsSource } = ctx.buildProfile;
logCredentialsSource(credentialsSource, Platform.ANDROID);
return {
+12 -1
View File
@@ -5,6 +5,7 @@ import JsonFile from '@expo/json-file';
import resolveFrom from 'resolve-from';
import { v4 as uuidv4 } from 'uuid';
import { CredentialsContext } from '../credentials/context';
import { RequestedPlatform } from '../platform';
import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
@@ -31,6 +32,7 @@ export interface BuildContext<T extends Platform> {
buildProfile: BuildProfile<T>;
buildProfileName: string;
clearCache: boolean;
credentialsCtx: CredentialsContext;
exp: ExpoConfig;
local: boolean;
nonInteractive: boolean;
@@ -69,9 +71,16 @@ export async function createBuildContextAsync<T extends Platform>({
const accountName = getProjectAccountName(exp, user);
const projectName = exp.slug;
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
const workflow = await resolveWorkflowAsync(projectDir, platform);
const accountId = findAccountByName(user.accounts, accountName)?.id;
const credentialsCtx = new CredentialsContext({
exp,
nonInteractive,
projectDir,
user,
});
const devClientProperties = getDevClientEventProperties({
platform,
projectDir,
@@ -87,11 +96,13 @@ export async function createBuildContextAsync<T extends Platform>({
...devClientProperties,
};
Analytics.logEvent(Event.BUILD_COMMAND, trackingCtx);
return {
accountName,
buildProfile,
buildProfileName,
clearCache,
credentialsCtx,
exp,
local,
nonInteractive,
@@ -19,7 +19,11 @@ export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig)
projectDir,
xcodeProject
);
await fs.writeFile(IOSConfig.Paths.getPBXProjectPath(projectDir), xcodeProject.writeSync());
await fs.writeFile(
IOSConfig.Paths.getPBXProjectPath(projectDir),
// eslint-disable-next-line node/no-sync
xcodeProject.writeSync()
);
}
let expoPlist = await readExpoPlistAsync(projectDir);
@@ -6,17 +6,17 @@ import { readChannelSafelyAsync } from '../UpdatesModule';
jest.mock('fs');
afterAll(() => {
afterAll(async () => {
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
fs.removeSync(os.tmpdir());
await fs.remove(os.tmpdir());
});
beforeEach(() => {
beforeEach(async () => {
vol.reset();
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
fs.mkdirpSync(os.tmpdir());
await fs.mkdirp(os.tmpdir());
});
const testChannel = 'test-channel';
@@ -10,29 +10,43 @@ import {
bumpVersionAsync,
bumpVersionInAppJsonAsync,
evaluateTemplateString,
getInfoPlistPath,
readBuildNumberAsync,
readShortVersionAsync,
} from '../version';
jest.mock('fs');
afterAll(() => {
afterAll(async () => {
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
fs.removeSync(os.tmpdir());
await fs.remove(os.tmpdir());
});
beforeEach(() => {
beforeEach(async () => {
vol.reset();
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
fs.mkdirpSync(os.tmpdir());
await fs.mkdirp(os.tmpdir());
});
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 });
}
@@ -1,6 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import { createCredentialsContextAsync } from '../../credentials/context';
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
import { getAppFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
import { resolveEntitlementsJsonAsync } from '../../credentials/ios/appstore/entitlements';
@@ -17,13 +16,8 @@ export async function ensureIosCredentialsAsync(
return;
}
const credentialsCtx = await createCredentialsContextAsync(buildCtx.projectDir, {
exp: buildCtx.exp,
nonInteractive: buildCtx.nonInteractive,
});
const provider = new IosCredentialsProvider(credentialsCtx, {
app: getAppFromContext(credentialsCtx),
const provider = new IosCredentialsProvider(buildCtx.credentialsCtx, {
app: getAppFromContext(buildCtx.credentialsCtx),
targets,
iosCapabilitiesOptions: {
entitlements: await resolveEntitlementsJsonAsync(buildCtx.projectDir, buildCtx.workflow),
+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();
@@ -25,7 +25,7 @@ import {
PublishPlatform,
buildBundlesAsync,
buildUnsortedUpdateInfoGroupAsync,
collectAssets,
collectAssetsAsync,
uploadAssetsAsync,
} from '../../project/publish';
import { promptAsync, selectAsync } from '../../prompts';
@@ -290,7 +290,7 @@ export default class BranchPublish extends EasCommand {
const assetSpinner = ora('Uploading assets...').start();
try {
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
const assets = collectAssets({ inputDir: inputDir!, platforms });
const assets = await collectAssetsAsync({ inputDir: inputDir!, platforms });
await uploadAssetsAsync(assets);
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
assetSpinner.succeed('Uploaded assets!');
+28 -11
View File
@@ -8,6 +8,7 @@ import {
import { flags } from '@oclif/command';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { prepareAndroidBuildAsync } from '../../build/android/build';
import { BuildRequestSender, waitForBuildEndAsync } from '../../build/build';
@@ -18,7 +19,13 @@ import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from '../../build
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { BuildFragment, BuildStatus, SubmissionFragment } from '../../graphql/generated';
import { CredentialsContext } from '../../credentials/context';
import {
AppPlatform,
BuildFragment,
BuildStatus,
SubmissionFragment,
} from '../../graphql/generated';
import { toAppPlatform, toPlatform } from '../../graphql/types/AppPlatform';
import Log from '../../log';
import {
@@ -35,7 +42,7 @@ import {
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
import { findProjectRootAsync } from '../../project/projectUtils';
import { confirmAsync } from '../../prompts';
import { createSubmissionContext } from '../../submit/context';
import { createSubmissionContextAsync } from '../../submit/context';
import {
submitAsync,
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
@@ -153,8 +160,9 @@ export default class Build extends EasCommand {
});
const startedBuilds: BuildFragment[] = [];
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
for (const platform of platforms) {
const maybeBuild = await this.prepareAndStartBuildAsync({
const { build: maybeBuild, buildCtx } = await this.prepareAndStartBuildAsync({
projectDir,
platform,
flags,
@@ -163,6 +171,7 @@ export default class Build extends EasCommand {
if (maybeBuild) {
startedBuilds.push(maybeBuild);
}
buildCtxByPlatform[toAppPlatform(platform)] = buildCtx;
}
if (flags.local) {
return;
@@ -176,10 +185,11 @@ export default class Build extends EasCommand {
if (flags.autoSubmit) {
for (const build of startedBuilds) {
const submission = await this.prepareAndStartSubmissionAsync({
projectDir,
build,
credentialsCtx: nullthrows(buildCtxByPlatform[build.platform]?.credentialsCtx),
flags,
moreBuilds: startedBuilds.length > 1,
projectDir,
});
submissions.push(submission);
}
@@ -253,17 +263,17 @@ export default class Build extends EasCommand {
};
}
private async prepareAndStartBuildAsync({
private async prepareAndStartBuildAsync<T extends Platform>({
projectDir,
platform,
flags,
moreBuilds,
}: {
projectDir: string;
platform: Platform;
platform: T;
flags: BuildFlags;
moreBuilds: boolean;
}): Promise<BuildFragment | undefined> {
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<T> }> {
const easJsonReader = new EasJsonReader(projectDir);
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, flags.profile);
@@ -297,7 +307,11 @@ export default class Build extends EasCommand {
error(EAS_UNAVAILABLE_MESSAGE, { exit: 1 });
}
return await this.startBuildAsync(buildCtx);
const build = await this.startBuildAsync(buildCtx);
return {
build,
buildCtx,
};
}
private async startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragment | undefined> {
@@ -311,21 +325,23 @@ export default class Build extends EasCommand {
}
private async prepareAndStartSubmissionAsync({
projectDir,
build,
credentialsCtx,
flags,
moreBuilds,
projectDir,
}: {
projectDir: string;
build: BuildFragment;
credentialsCtx: CredentialsContext;
flags: BuildFlags;
moreBuilds: boolean;
projectDir: string;
}): Promise<SubmissionFragment> {
const easJsonReader = new EasJsonReader(projectDir);
const platform = toPlatform(build.platform);
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, flags.profile);
const submitProfile = await easJsonReader.readSubmitProfileAsync(platform, flags.submitProfile);
const submissionCtx = createSubmissionContext({
const submissionCtx = await createSubmissionContextAsync({
platform,
projectDir,
projectId: build.project.id,
@@ -333,6 +349,7 @@ export default class Build extends EasCommand {
archiveFlags: { id: build.id },
nonInteractive: flags.nonInteractive,
env: buildProfile.env,
credentialsCtx,
});
if (moreBuilds) {
+6 -2
View File
@@ -1,12 +1,16 @@
import EasCommand from '../commandUtils/EasCommand';
import { createCredentialsContextAsync } from '../credentials/context';
import { CredentialsContext } from '../credentials/context';
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
import { ensureLoggedInAsync } from '../user/actions';
export default class Credentials extends EasCommand {
static description = 'manage your credentials';
async runAsync(): Promise<void> {
const ctx = await createCredentialsContextAsync(process.cwd(), {});
const ctx = new CredentialsContext({
projectDir: process.cwd(),
user: await ensureLoggedInAsync(),
});
await new SelectPlatform().runAsync(ctx);
}
}
+2 -2
View File
@@ -17,7 +17,7 @@ import {
} from '../platform';
import { isEasEnabledForProjectAsync, warnEasUnavailable } from '../project/isEasEnabledForProject';
import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils';
import { SubmitArchiveFlags, createSubmissionContext } from '../submit/context';
import { SubmitArchiveFlags, createSubmissionContextAsync } from '../submit/context';
import { submitAsync, waitToCompleteAsync } from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
@@ -108,7 +108,7 @@ See how to configure submits with eas.json: ${learnMore('https://docs.expo.dev/s
const submissions: SubmissionFragment[] = [];
for (const platform of platforms) {
const profile = await easJsonReader.readSubmitProfileAsync(platform, flags.profile);
const ctx = createSubmissionContext({
const ctx = await createSubmissionContextAsync({
platform,
projectDir,
projectId,
@@ -6,6 +6,7 @@ import {
AndroidKeystoreType,
AppFragment,
CommonAndroidAppCredentialsFragment,
GoogleServiceAccountKeyFragment,
} from '../../graphql/generated';
import * as AndroidGraphqlClient from '../android/api/GraphqlClient';
import { Keystore } from '../android/credentials';
@@ -68,6 +69,16 @@ export const testLegacyAndroidFcmFragment: AndroidFcmFragment = {
updatedAt: now,
};
export const testGoogleServiceAccountKeyFragment: GoogleServiceAccountKeyFragment = {
id: 'test-id',
projectIdentifier: 'sdf.sdf.sdf',
privateKeyIdentifier: 'test-private-key-identifier',
clientEmail: '[email protected]',
clientIdentifier: 'test-client-identifier',
createdAt: now,
updatedAt: now,
};
export const testJksAndroidKeystoreFragment: AndroidKeystoreFragment = {
id: 'test-id',
type: AndroidKeystoreType.Jks,
@@ -113,6 +124,7 @@ export const testAndroidAppCredentialsFragment: CommonAndroidAppCredentialsFragm
isLegacy: false,
app: testAppFragment,
androidFcm: testLegacyAndroidFcmFragment,
googleServiceAccountKeyForSubmissions: testGoogleServiceAccountKeyFragment,
androidAppBuildCredentialsList: [testLegacyAndroidBuildCredentialsFragment],
};
@@ -130,6 +142,8 @@ export function getNewAndroidApiMock(): { [key in keyof typeof AndroidGraphqlCli
getAndroidAppBuildCredentialsByNameAsync: jest.fn(),
createOrUpdateAndroidAppBuildCredentialsByNameAsync: jest.fn(),
createKeystoreAsync: jest.fn(),
createGoogleServiceAccountKeyAsync: jest.fn(),
getGoogleServiceAccountKeysForAccountAsync: jest.fn(),
createFcmAsync: jest.fn(),
deleteKeystoreAsync: jest.fn(),
deleteFcmAsync: jest.fn(),
@@ -1,12 +1,12 @@
import merge from 'ts-deepmerge';
import { Context } from '../context';
import { CredentialsContext } from '../context';
import { getNewAndroidApiMock } from './fixtures-android';
import { getAppstoreMock } from './fixtures-appstore';
import { testAppJson, testUsername } from './fixtures-constants';
import { getNewIosApiMock } from './fixtures-ios';
export function createCtxMock(mockOverride: Record<string, any> = {}): Context {
export function createCtxMock(mockOverride: Record<string, any> = {}): CredentialsContext {
const defaultMock = {
ios: getNewIosApiMock(),
android: getNewAndroidApiMock(),
@@ -3,7 +3,7 @@ import { CredentialsSource } from '@expo/eas-json';
import nullthrows from 'nullthrows';
import { AndroidAppBuildCredentialsFragment } from '../../graphql/generated';
import { Context } from '../context';
import { CredentialsContext } from '../context';
import * as credentialsJsonReader from '../credentialsJson/read';
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
import { AppLookupParams } from './api/GraphqlClient';
@@ -20,7 +20,7 @@ interface Options {
export default class AndroidCredentialsProvider {
public readonly platform = Platform.ANDROID;
constructor(private ctx: Context, private options: Options) {}
constructor(private ctx: CredentialsContext, private options: Options) {}
public async getCredentialsAsync(
src: CredentialsSource.LOCAL | CredentialsSource.REMOTE
@@ -3,14 +3,14 @@ import {
CommonAndroidAppCredentialsFragment,
} from '../../../graphql/generated';
import Log from '../../../log';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
export class AssignFcm {
constructor(private app: AppLookupParams) {}
public async runAsync(
ctx: Context,
ctx: CredentialsContext,
fcm: AndroidFcmFragment
): Promise<CommonAndroidAppCredentialsFragment> {
const appCredentials =
@@ -0,0 +1,29 @@
import {
CommonAndroidAppCredentialsFragment,
GoogleServiceAccountKeyFragment,
} from '../../../graphql/generated';
import Log from '../../../log';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
export class AssignGoogleServiceAccountKey {
constructor(private app: AppLookupParams) {}
public async runAsync(
ctx: CredentialsContext,
googleServiceAccountKey: GoogleServiceAccountKeyFragment
): Promise<CommonAndroidAppCredentialsFragment> {
const appCredentials =
await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(this.app);
const updatedAppCredentials = await ctx.android.updateAndroidAppCredentialsAsync(
appCredentials,
{
googleServiceAccountKeyForSubmissionsId: googleServiceAccountKey.id,
}
);
Log.succeed(
`Google Service Account Key assigned to ${this.app.androidApplicationIdentifier} for submissions`
);
return updatedAppCredentials;
}
}
@@ -8,7 +8,7 @@ import { GradleBuildContext } from '../../../project/android/gradle';
import { getProjectAccountName, getProjectConfigDescription } from '../../../project/projectUtils';
import { promptAsync } from '../../../prompts';
import { findAccountByName } from '../../../user/Account';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
/**
@@ -16,7 +16,7 @@ import { AppLookupParams } from '../api/GraphqlClient';
* EAS credentials set up yet
*/
export async function canCopyLegacyCredentialsAsync(
ctx: Context,
ctx: CredentialsContext,
app: AppLookupParams
): Promise<boolean> {
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(app);
@@ -30,7 +30,7 @@ export async function canCopyLegacyCredentialsAsync(
}
export async function promptUserAndCopyLegacyCredentialsAsync(
ctx: Context,
ctx: CredentialsContext,
app: AppLookupParams
): Promise<void> {
assert(
@@ -85,7 +85,7 @@ export async function promptUserAndCopyLegacyCredentialsAsync(
}
export async function getAppLookupParamsFromContextAsync(
ctx: Context,
ctx: CredentialsContext,
gradleContext?: GradleBuildContext
): Promise<AppLookupParams> {
ctx.ensureProjectContext();
@@ -113,7 +113,7 @@ export async function getAppLookupParamsFromContextAsync(
}
export async function createOrUpdateDefaultAndroidAppBuildCredentialsAsync(
ctx: Context,
ctx: CredentialsContext,
appLookupParams: AppLookupParams,
{
androidKeystoreId,
@@ -2,12 +2,12 @@ import { AndroidFcmFragment, AndroidFcmVersion } from '../../../graphql/generate
import Log from '../../../log';
import { promptAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
export class CreateFcm {
constructor(private account: Account) {}
public async runAsync(ctx: Context): Promise<AndroidFcmFragment> {
public async runAsync(ctx: CredentialsContext): Promise<AndroidFcmFragment> {
if (ctx.nonInteractive) {
throw new Error(`FCM API Key cannot be uploaded in non-interactive mode.`);
}
@@ -0,0 +1,41 @@
import { GoogleServiceAccountKeyFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { promptAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { CredentialsContext } from '../../context';
import { GoogleServiceAccountKey } from '../credentials';
import { readAndValidateServiceAccountKey } from '../utils/googleServiceAccountKey';
export class CreateGoogleServiceAccountKey {
constructor(private account: Account) {}
public async runAsync(ctx: CredentialsContext): 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();
}
}
}
@@ -2,7 +2,7 @@ import { AndroidKeystoreFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { getProjectIdAsync } from '../../../project/projectUtils';
import { Account } from '../../../user/Account';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
import { KeystoreWithType, keystoreSchema } from '../credentials';
import { generateRandomKeystoreAsync } from '../utils/keystore';
@@ -11,7 +11,7 @@ import { getKeystoreWithType, validateKeystore } from '../utils/keystoreNew';
export class CreateKeystore {
constructor(private account: Account) {}
public async runAsync(ctx: Context): Promise<AndroidKeystoreFragment> {
public async runAsync(ctx: CredentialsContext): Promise<AndroidKeystoreFragment> {
if (ctx.nonInteractive) {
throw new Error(`New keystore cannot be created in non-interactive mode.`);
}
@@ -5,7 +5,7 @@ import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { maybeRenameExistingFileAsync } from '../../../utils/files';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { AppLookupParams, formatProjectFullName } from '../api/GraphqlClient';
import { displayAndroidKeystore } from '../utils/printCredentials';
@@ -19,7 +19,7 @@ export class DownloadKeystore {
constructor(private options: DownloadKeystoreOptions) {}
public async runAsync(
ctx: Context,
ctx: CredentialsContext,
buildCredentials: AndroidAppBuildCredentialsFragment
): Promise<void> {
const keystore = buildCredentials.androidKeystore;
@@ -65,7 +65,7 @@ export class BackupKeystore {
constructor(private app: AppLookupParams) {}
public async runAsync(
ctx: Context,
ctx: CredentialsContext,
buildCredentials: AndroidAppBuildCredentialsFragment
): Promise<void> {
if (!buildCredentials.androidKeystore) {
@@ -1,12 +1,12 @@
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { AppLookupParams, formatProjectFullName } from '../api/GraphqlClient';
export class RemoveFcm {
constructor(private app: AppLookupParams) {}
async runAsync(ctx: Context): Promise<void> {
async runAsync(ctx: CredentialsContext): Promise<void> {
if (ctx.nonInteractive) {
throw new Error(
"Deleting an FCM Api Key is a destructive operation. Start the CLI without the '--non-interactive' flag to delete the credentials."
@@ -3,7 +3,7 @@ import chalk from 'chalk';
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
import { BackupKeystore } from './DownloadKeystore';
@@ -11,7 +11,7 @@ export class RemoveKeystore {
constructor(private app: AppLookupParams) {}
async runAsync(
ctx: Context,
ctx: CredentialsContext,
buildCredentials: AndroidAppBuildCredentialsFragment
): Promise<void> {
if (ctx.nonInteractive) {
@@ -6,7 +6,7 @@ import {
} from '../../../graphql/generated';
import Log from '../../../log';
import { ora } from '../../../ora';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import { AppLookupParams } from '../api/GraphqlClient';
import {
@@ -28,7 +28,7 @@ interface Options {
export class SetupBuildCredentials {
constructor(private options: Options) {}
async runAsync(ctx: Context): Promise<AndroidAppBuildCredentialsFragment> {
async runAsync(ctx: CredentialsContext): Promise<AndroidAppBuildCredentialsFragment> {
const { app, name: maybeName } = this.options;
if (!ctx.nonInteractive) {
@@ -62,7 +62,7 @@ export class SetupBuildCredentials {
name,
keystore,
}: {
ctx: Context;
ctx: CredentialsContext;
app: AppLookupParams;
name?: string;
keystore: AndroidKeystoreFragment;
@@ -82,7 +82,7 @@ export class SetupBuildCredentials {
app,
name,
}: {
ctx: Context;
ctx: CredentialsContext;
app: AppLookupParams;
name?: string;
}): Promise<AndroidAppBuildCredentialsFragment | null> {
@@ -108,7 +108,7 @@ export class SetupBuildCredentials {
app,
name,
}: {
ctx: Context;
ctx: CredentialsContext;
app: AppLookupParams;
name: string;
}): Promise<AndroidAppBuildCredentialsFragment | null> {
@@ -4,7 +4,7 @@ import {
} from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { readAndroidCredentialsAsync } from '../../credentialsJson/read';
import {
SelectAndroidBuildCredentials,
@@ -17,7 +17,7 @@ import { BackupKeystore } from './DownloadKeystore';
export class SetupBuildCredentialsFromCredentialsJson {
constructor(private app: AppLookupParams) {}
async runAsync(ctx: Context): Promise<AndroidAppBuildCredentialsFragment | null> {
async runAsync(ctx: CredentialsContext): Promise<AndroidAppBuildCredentialsFragment | null> {
if (ctx.nonInteractive) {
throw new Error(
'Setting up build credentials from credentials.json is only available in interactive mode'
@@ -1,11 +1,11 @@
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { updateAndroidCredentialsAsync } from '../../credentialsJson/update';
export class UpdateCredentialsJson {
async runAsync(
ctx: Context,
ctx: CredentialsContext,
buildCredentials: AndroidAppBuildCredentialsFragment
): Promise<void> {
Log.log('Updating Android credentials in credentials.json');
@@ -0,0 +1,69 @@
import chalk from 'chalk';
import { GoogleServiceAccountKeyFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { promptAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { fromNow } from '../../../utils/date';
import { CredentialsContext } from '../../context';
export class UseExistingGoogleServiceAccountKey {
constructor(private account: Account) {}
public async runAsync(ctx: CredentialsContext): Promise<GoogleServiceAccountKeyFragment | null> {
if (ctx.nonInteractive) {
throw new Error(
`Existing Google Service Account Key cannot be chosen in non-interactive mode.`
);
}
const gsaKeyFragments = await ctx.android.getGoogleServiceAccountKeysForAccountAsync(
this.account
);
if (gsaKeyFragments.length === 0) {
Log.error("There aren't any Google Service Account Keys associated with your account.");
return null;
}
return await this.selectGoogleServiceAccountKeyAsync(gsaKeyFragments);
}
private async selectGoogleServiceAccountKeyAsync(
keys: GoogleServiceAccountKeyFragment[]
): Promise<GoogleServiceAccountKeyFragment | null> {
const sortedKeys = this.sortGoogleServiceAccountKeysByUpdatedAtDesc(keys);
const { chosenKey } = await promptAsync({
type: 'select',
name: 'chosenKey',
message: 'Select a Google Service Account Key:',
choices: sortedKeys.map(key => ({
title: this.formatGoogleServiceAccountKey(key),
value: key,
})),
});
return chosenKey;
}
private sortGoogleServiceAccountKeysByUpdatedAtDesc(
keys: GoogleServiceAccountKeyFragment[]
): GoogleServiceAccountKeyFragment[] {
return keys.sort(
(keyA, keyB) =>
new Date(keyB.updatedAt).getMilliseconds() - new Date(keyA.updatedAt).getMilliseconds()
);
}
private formatGoogleServiceAccountKey({
projectIdentifier,
privateKeyIdentifier,
clientEmail,
clientIdentifier,
updatedAt,
}: GoogleServiceAccountKeyFragment): string {
let line: string = '';
line += `Client Email: ${clientEmail}, Project Id: ${projectIdentifier}`;
line += chalk.gray(
`\n Client Id: ${clientIdentifier}, Private Key Id: ${privateKeyIdentifier}`
);
line += chalk.gray(`\n Updated: ${fromNow(new Date(updatedAt))} ago,`);
return line;
}
}
@@ -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({
@@ -0,0 +1,33 @@
import { testGoogleServiceAccountKeyFragment } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { AssignGoogleServiceAccountKey } from '../AssignGoogleServiceAccountKey';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
describe(AssignGoogleServiceAccountKey, () => {
it('assigns a Google Service Account key in Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: false,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const assignGoogleServiceAccountKeyAction = new AssignGoogleServiceAccountKey(appLookupParams);
await assignGoogleServiceAccountKeyAction.runAsync(ctx, testGoogleServiceAccountKeyFragment);
// expect app credentials to be fetched/created, then updated
expect(
ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync
).toHaveBeenCalledTimes(1);
expect(ctx.android.updateAndroidAppCredentialsAsync).toHaveBeenCalledTimes(1);
});
it('works in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const assignGoogleServiceAccountKeyAction = new AssignGoogleServiceAccountKey(appLookupParams);
// dont fail if users are running in non-interactive mode
await expect(
assignGoogleServiceAccountKeyAction.runAsync(ctx, testGoogleServiceAccountKeyFragment)
).resolves.not.toThrowError();
});
});
@@ -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({
@@ -0,0 +1,59 @@
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
testGoogleServiceAccountKeyFragment,
} from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { UseExistingGoogleServiceAccountKey } from '../UseExistingGoogleServiceAccountKey';
jest.mock('../../../../prompts');
asMock(promptAsync).mockImplementation(() => ({ chosenKey: testGoogleServiceAccountKeyFragment }));
describe(UseExistingGoogleServiceAccountKey, () => {
it('uses an existing Google Service Account Key in Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: false,
android: {
...getNewAndroidApiMock(),
getGoogleServiceAccountKeysForAccountAsync: jest.fn(() => [
testGoogleServiceAccountKeyFragment,
]),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const useExistingGoogleServiceAccountKeyAction = new UseExistingGoogleServiceAccountKey(
appLookupParams.account
);
const selectedKey = await useExistingGoogleServiceAccountKeyAction.runAsync(ctx);
expect(ctx.android.getGoogleServiceAccountKeysForAccountAsync).toHaveBeenCalledTimes(1);
expect(selectedKey).toMatchObject(testGoogleServiceAccountKeyFragment);
});
it("returns null if the account doesn't have any Google Service Account Keys", async () => {
const ctx = createCtxMock({
nonInteractive: false,
android: {
...getNewAndroidApiMock(),
getGoogleServiceAccountKeysForAccountAsync: jest.fn(() => []),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const useExistingGoogleServiceAccountKeyAction = new UseExistingGoogleServiceAccountKey(
appLookupParams.account
);
const selectedKey = await useExistingGoogleServiceAccountKeyAction.runAsync(ctx);
expect(ctx.android.getGoogleServiceAccountKeysForAccountAsync).toHaveBeenCalledTimes(1);
expect(selectedKey).toBe(null);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const useExistingGoogleServiceAccountKeyAction = new UseExistingGoogleServiceAccountKey(
appLookupParams.account
);
// fail if users are running in non-interactive mode
await expect(useExistingGoogleServiceAccountKeyAction.runAsync(ctx)).rejects.toThrowError();
});
});
@@ -5,15 +5,18 @@ 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';
import { GoogleServiceAccountKeyQuery } from './graphql/queries/GoogleServiceAccountKeyQuery';
export interface AppLookupParams {
account: Account;
@@ -87,11 +90,27 @@ export async function updateAndroidAppCredentialsAsync(
appCredentials: CommonAndroidAppCredentialsFragment,
{
androidFcmId,
googleServiceAccountKeyForSubmissionsId,
}: {
androidFcmId: string;
androidFcmId?: string;
googleServiceAccountKeyForSubmissionsId?: string;
}
): Promise<CommonAndroidAppCredentialsFragment> {
return await AndroidAppCredentialsMutation.setFcmKeyAsync(appCredentials.id, androidFcmId);
let updatedAppCredentials = appCredentials;
if (androidFcmId) {
updatedAppCredentials = await AndroidAppCredentialsMutation.setFcmKeyAsync(
appCredentials.id,
androidFcmId
);
}
if (googleServiceAccountKeyForSubmissionsId) {
updatedAppCredentials =
await AndroidAppCredentialsMutation.setGoogleServiceAccountKeyForSubmissionsAsync(
appCredentials.id,
googleServiceAccountKeyForSubmissionsId
);
}
return updatedAppCredentials;
}
export async function updateAndroidAppBuildCredentialsAsync(
@@ -223,6 +242,22 @@ 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
);
}
export async function getGoogleServiceAccountKeysForAccountAsync(
account: Account
): Promise<GoogleServiceAccountKeyFragment[]> {
return await GoogleServiceAccountKeyQuery.getAllForAccountAsync(account.name);
}
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 };
@@ -7,10 +7,11 @@ import {
CommonAndroidAppCredentialsFragment,
CreateAndroidAppCredentialsMutation,
SetFcmMutation,
SetGoogleServiceAccountKeyForSubmissionsMutation,
} from '../../../../../graphql/generated';
import { CommonAndroidAppCredentialsFragmentNode } from '../../../../../graphql/types/credentials/AndroidAppCredentials';
const AndroidAppCredentialsMutation = {
export const AndroidAppCredentialsMutation = {
async createAndroidAppCredentialsAsync(
androidAppCredentialsInput: {
fcmId?: string;
@@ -82,6 +83,41 @@ const AndroidAppCredentialsMutation = {
assert(data.androidAppCredentials.setFcm, 'GraphQL: `setFcm` not defined in server response');
return data.androidAppCredentials.setFcm;
},
async setGoogleServiceAccountKeyForSubmissionsAsync(
androidAppCredentialsId: string,
googleServiceAccountKeyId: string
): Promise<CommonAndroidAppCredentialsFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<SetGoogleServiceAccountKeyForSubmissionsMutation>(
gql`
mutation SetGoogleServiceAccountKeyForSubmissionsMutation(
$androidAppCredentialsId: ID!
$googleServiceAccountKeyId: ID!
) {
androidAppCredentials {
setGoogleServiceAccountKeyForSubmissions(
id: $androidAppCredentialsId
googleServiceAccountKeyId: $googleServiceAccountKeyId
) {
id
...CommonAndroidAppCredentialsFragment
}
}
}
${print(CommonAndroidAppCredentialsFragmentNode)}
`,
{
androidAppCredentialsId,
googleServiceAccountKeyId,
}
)
.toPromise()
);
assert(
data.androidAppCredentials.setGoogleServiceAccountKeyForSubmissions,
'GraphQL: `setGoogleServiceAccountKeyForSubmissions` not defined in server response'
);
return data.androidAppCredentials.setGoogleServiceAccountKeyForSubmissions;
},
};
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 };
@@ -0,0 +1,38 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
import {
GoogleServiceAccountKeyByAccountQuery,
GoogleServiceAccountKeyFragment,
} from '../../../../../graphql/generated';
import { GoogleServiceAccountKeyFragmentNode } from '../../../../../graphql/types/credentials/GoogleServiceAccountKey';
export const GoogleServiceAccountKeyQuery = {
async getAllForAccountAsync(accountName: string): Promise<GoogleServiceAccountKeyFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<GoogleServiceAccountKeyByAccountQuery>(
gql`
query GoogleServiceAccountKeyByAccountQuery($accountName: String!) {
account {
byName(accountName: $accountName) {
id
googleServiceAccountKeys {
id
...GoogleServiceAccountKeyFragment
}
}
}
}
${print(GoogleServiceAccountKeyFragmentNode)}
`,
{
accountName,
}
)
.toPromise()
);
return data.account.byName.googleServiceAccountKeys;
},
};
@@ -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,3 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`print credentials prints the AndroidAppCredentials fragment 1`] = `"Android Credentials Project: testApp Application Identifier: test.com.app Push Notifications (FCM): Key: abcd...efgh Updated 0 second ago Configuration: legacy (Default) Keystore: Type: JKS Key Alias: QHdrb3p5cmEvY3JlZGVudGlhbHMtdGVzdA== MD5 Fingerprint: TE:ST:-M:D5 SHA1 Fingerprint: TE:ST:-S:HA:1 SHA256 Fingerprint: TE:ST:-S:HA:25:6 Updated 0 second ago"`;
exports[`print credentials prints the AndroidAppCredentials fragment 1`] = `
"Android Credentials Project: testApp Application Identifier: test.com.app Push Notifications (FCM): Key: abcd...efgh Updated 0 second agoGoogle Service Account Key For Submissions Project ID sdf.sdf.sdf
Client Email [email protected]
Client ID test-client-identifier
Private Key ID test-private-key-identifier
Updated 0 second ago Configuration: legacy (Default) Keystore: Type: JKS Key Alias: QHdrb3p5cmEvY3JlZGVudGlhbHMtdGVzdA== MD5 Fingerprint: TE:ST:-M:D5 SHA1 Fingerprint: TE:ST:-S:HA:1 SHA256 Fingerprint: TE:ST:-S:HA:25:6 Updated 0 second ago"
`;
@@ -1,8 +1,10 @@
import mockdate from 'mockdate';
import { asMock } from '../../../../__tests__/utils';
import Log from '../../../../log';
import {
testAndroidAppCredentialsFragment,
testGoogleServiceAccountKeyFragment,
testLegacyAndroidFcmFragment,
} from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -10,9 +12,10 @@ import { getAppLookupParamsFromContextAsync } from '../../actions/BuildCredentia
import { displayAndroidAppCredentials } from '../printCredentials';
jest.mock('../../../../log');
jest.mock('chalk', () => ({ bold: jest.fn(log => log) }));
jest.mock('chalk', () => ({ bold: jest.fn(log => log), cyan: { bold: jest.fn(log => log) } }));
mockdate.set(testLegacyAndroidFcmFragment.updatedAt);
mockdate.set(testGoogleServiceAccountKeyFragment.updatedAt);
describe('print credentials', () => {
it('prints the AndroidAppCredentials fragment', async () => {
@@ -22,9 +25,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;
}
}
@@ -10,6 +10,7 @@ import {
} from '../../../graphql/generated';
import Log from '../../../log';
import { fromNow } from '../../../utils/date';
import formatFields from '../../../utils/formatFields';
import { sortBuildCredentials } from '../actions/BuildCredentialsUtils';
import { AppLookupParams } from '../api/GraphqlClient';
@@ -44,10 +45,43 @@ function displayAndroidFcmCredentials(appCredentials: CommonAndroidAppCredential
Log.newLine();
}
function displayGoogleServiceAccountKeyForSubmissions(
appCredentials: CommonAndroidAppCredentialsFragment
): void {
const maybeGsaKey = appCredentials.googleServiceAccountKeyForSubmissions;
Log.log(
formatFields([{ label: 'Google Service Account Key For Submissions', value: '' }], {
labelFormat: chalk.cyan.bold,
})
);
if (!maybeGsaKey) {
Log.log(
formatFields([{ label: '', value: 'None assigned yet' }], {
labelFormat: chalk.cyan.bold,
})
);
Log.newLine();
return;
}
const { projectIdentifier, privateKeyIdentifier, clientEmail, clientIdentifier, updatedAt } =
maybeGsaKey;
const fields = [
{ label: 'Project ID', value: projectIdentifier },
{ label: 'Client Email', value: clientEmail },
{ label: 'Client ID', value: clientIdentifier },
{ label: 'Private Key ID', value: privateKeyIdentifier },
{ label: 'Updated', value: `${fromNow(new Date(updatedAt))} ago` },
];
Log.log(formatFields(fields, { labelFormat: chalk.cyan.bold }));
Log.newLine();
}
function displayEASAndroidAppCredentials(
appCredentials: CommonAndroidAppCredentialsFragment
): void {
displayAndroidFcmCredentials(appCredentials);
displayGoogleServiceAccountKeyForSubmissions(appCredentials);
const sortedBuildCredentialsList = sortBuildCredentials(
appCredentials.androidAppBuildCredentialsList
);
+28 -60
View File
@@ -6,74 +6,41 @@ import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName } from '../project/projectUtils';
import { confirmAsync } from '../prompts';
import { Actor, getActorDisplayName } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
import pick from '../utils/expodash/pick';
import * as AndroidGraphqlClient from './android/api/GraphqlClient';
import * as IosGraphqlClient from './ios/api/GraphqlClient';
import AppStoreApi from './ios/appstore/AppStoreApi';
export interface Action<T = void> {
runAsync(ctx: Context): Promise<T>;
}
interface AppleCtxOptions {
appleId?: string;
appleIdPassword?: string;
teamId?: string;
}
interface Options extends AppleCtxOptions {
nonInteractive?: boolean;
exp?: ExpoConfig;
}
export interface Context {
readonly projectDir: string;
readonly user: Actor;
readonly nonInteractive: boolean;
readonly android: typeof AndroidGraphqlClient;
readonly ios: typeof IosGraphqlClient;
readonly appStore: AppStoreApi;
readonly hasProjectContext: boolean;
readonly exp: ExpoConfig;
ensureProjectContext(): void;
bestEffortAppStoreAuthenticateAsync(): Promise<void>;
}
export async function createCredentialsContextAsync(
projectDir: string,
options: Options
): Promise<Context> {
const user = await ensureLoggedInAsync();
let expoConfig: ExpoConfig | undefined = options.exp;
if (!expoConfig) {
try {
expoConfig = getExpoConfig(projectDir);
} catch (error) {
// ignore error, context might be created outside of expo project
}
}
return new CredentialsContext(projectDir, user, expoConfig, options);
}
class CredentialsContext implements Context {
export class CredentialsContext {
public readonly android = AndroidGraphqlClient;
public readonly appStore = new AppStoreApi();
public readonly ios = IosGraphqlClient;
public readonly appStore: AppStoreApi;
public readonly nonInteractive: boolean;
public readonly projectDir: string;
public readonly user: Actor;
private shouldAskAuthenticateAppStore: boolean = true;
private _exp?: ExpoConfig;
constructor(
public readonly projectDir: string,
public readonly user: Actor,
private _exp: ExpoConfig | undefined,
options: Options
private options: {
exp?: ExpoConfig;
nonInteractive?: boolean;
projectDir: string;
user: Actor;
}
) {
this.appStore = new AppStoreApi(pick(options, ['appleId', 'appleIdPassword', 'teamId']));
this.projectDir = options.projectDir;
this.user = options.user;
this.nonInteractive = options.nonInteractive ?? false;
this._exp = options.exp;
if (!this._exp) {
try {
this._exp = getExpoConfig(options.projectDir);
} catch (error) {
// ignore error, context might be created outside of expo project
}
}
}
get hasProjectContext(): boolean {
@@ -90,14 +57,15 @@ class CredentialsContext implements Context {
return;
}
// trigger getConfig error
getConfig(this.projectDir, { skipSDKVersionRequirement: true });
getConfig(this.options.projectDir, { skipSDKVersionRequirement: true });
}
public logOwnerAndProject(): void {
const { user } = this.options;
if (this.hasProjectContext) {
const owner = getProjectAccountName(this.exp, this.user);
const owner = getProjectAccountName(this.exp, user);
// Figure out if User A is configuring credentials as admin for User B's project
const isProxyUser = this.user.__typename === 'Robot' || owner !== this.user.username;
const isProxyUser = user.__typename === 'Robot' || owner !== user.username;
Log.log(
`Accessing credentials ${isProxyUser ? 'on behalf of' : 'for'} ${owner} in project ${
@@ -105,7 +73,7 @@ class CredentialsContext implements Context {
}`
);
} else {
Log.log(`Accessing credentials for ${this.exp.owner ?? getActorDisplayName(this.user)}`);
Log.log(`Accessing credentials for ${this.exp.owner ?? getActorDisplayName(user)}`);
}
}
@@ -8,7 +8,7 @@ import { findApplicationTarget, findTargetByName } from '../../project/ios/targe
import zipObject from '../../utils/expodash/zipObject';
import vcs from '../../vcs';
import GitClient from '../../vcs/git';
import { Context } from '../context';
import { CredentialsContext } from '../context';
import { App, Target, TargetCredentials } from '../ios/types';
import { readRawAsync } from './read';
import {
@@ -23,7 +23,7 @@ import { getCredentialsJsonPath } from './utils';
* is not validated
*/
export async function updateAndroidCredentialsAsync(
ctx: Context,
ctx: CredentialsContext,
buildCredentials: AndroidAppBuildCredentialsFragment
): Promise<void> {
const rawCredentialsJson: any =
@@ -70,7 +70,7 @@ export async function updateAndroidCredentialsAsync(
* credentials.json will be updated partially
*/
export async function updateIosCredentialsAsync(
ctx: Context,
ctx: CredentialsContext,
app: App,
targets: Target[],
distributionType: IosDistributionType
@@ -205,7 +205,7 @@ function createTargetCredentialsPathsMap(
}
async function getTargetBuildCredentialsAsync(
ctx: Context,
ctx: CredentialsContext,
app: App,
target: Target,
iosDistributionType: IosDistributionType
@@ -244,7 +244,7 @@ async function getTargetBuildCredentialsAsync(
}
async function backupTargetCredentialsAsync(
ctx: Context,
ctx: CredentialsContext,
{
targetCredentials,
targetCredentialsPaths,
@@ -5,7 +5,7 @@ import { CommonIosAppCredentialsFragment } from '../../graphql/generated';
import Log from '../../log';
import { findApplicationTarget } from '../../project/ios/target';
import { confirmAsync } from '../../prompts';
import { Context } from '../context';
import { CredentialsContext } from '../context';
import * as credentialsJsonReader from '../credentialsJson/read';
import { ensureAllTargetsAreConfigured } from '../credentialsJson/utils';
import { getAppFromContext } from './actions/BuildCredentialsUtils';
@@ -26,7 +26,7 @@ interface Options {
export default class IosCredentialsProvider {
public readonly platform = Platform.IOS;
constructor(private ctx: Context, private options: Options) {}
constructor(private ctx: CredentialsContext, private options: Options) {}
public async getCredentialsAsync(
src: CredentialsSource.LOCAL | CredentialsSource.REMOTE
@@ -68,7 +68,7 @@ export default class IosCredentialsProvider {
}
private async getPushKeyAsync(
ctx: Context,
ctx: CredentialsContext,
targets: Target[]
): Promise<CommonIosAppCredentialsFragment | null> {
if (ctx.nonInteractive) {
@@ -1,9 +1,9 @@
import { AppleTeamFragment } from '../../../graphql/generated';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
export async function resolveAppleTeamIfAuthenticatedAsync(
ctx: Context,
ctx: CredentialsContext,
app: AppLookupParams
): Promise<AppleTeamFragment | null> {
if (!ctx.appStore.authCtx) {
@@ -1,6 +1,6 @@
import { ApplePushKeyFragment, CommonIosAppCredentialsFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
@@ -8,7 +8,7 @@ export class AssignPushKey {
constructor(private app: AppLookupParams) {}
public async runAsync(
ctx: Context,
ctx: CredentialsContext,
pushKey: ApplePushKeyFragment
): Promise<CommonIosAppCredentialsFragment> {
const appleTeam =
@@ -9,13 +9,13 @@ import {
} from '../../../graphql/generated';
import { getProjectAccountName } from '../../../project/projectUtils';
import { findAccountByName } from '../../../user/Account';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
import { App, Target } from '../types';
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
export async function getAllBuildCredentialsAsync(
ctx: Context,
ctx: CredentialsContext,
app: AppLookupParams
): Promise<IosAppBuildCredentialsFragment[]> {
const appCredentials = await ctx.ios.getIosAppCredentialsWithBuildCredentialsAsync(app, {});
@@ -26,7 +26,7 @@ export async function getAllBuildCredentialsAsync(
}
export async function getBuildCredentialsAsync(
ctx: Context,
ctx: CredentialsContext,
app: AppLookupParams,
iosDistributionType: GraphQLIosDistributionType
): Promise<IosAppBuildCredentialsFragment | null> {
@@ -41,7 +41,7 @@ export async function getBuildCredentialsAsync(
}
export async function getProvisioningProfileAsync(
ctx: Context,
ctx: CredentialsContext,
app: AppLookupParams,
iosDistributionType: GraphQLIosDistributionType
): Promise<AppleProvisioningProfileFragment | null> {
@@ -50,7 +50,7 @@ export async function getProvisioningProfileAsync(
}
export async function getDistributionCertificateAsync(
ctx: Context,
ctx: CredentialsContext,
app: AppLookupParams,
iosDistributionType: GraphQLIosDistributionType
): Promise<AppleDistributionCertificateFragment | null> {
@@ -59,7 +59,7 @@ export async function getDistributionCertificateAsync(
}
export async function assignBuildCredentialsAsync(
ctx: Context,
ctx: CredentialsContext,
app: AppLookupParams,
iosDistributionType: GraphQLIosDistributionType,
distCert: AppleDistributionCertificateFragment,
@@ -82,7 +82,7 @@ export async function assignBuildCredentialsAsync(
});
}
export function getAppFromContext(ctx: Context): App {
export function getAppFromContext(ctx: CredentialsContext): App {
ctx.ensureProjectContext();
const projectName = ctx.exp.slug;
const accountName = getProjectAccountName(ctx.exp, ctx.user);
@@ -96,7 +96,10 @@ export function getAppFromContext(ctx: Context): App {
};
}
export function getAppLookupParamsFromContext(ctx: Context, target: Target): AppLookupParams {
export function getAppLookupParamsFromContext(
ctx: CredentialsContext,
target: Target
): AppLookupParams {
const app = getAppFromContext(ctx);
return { ...app, bundleIdentifier: target.bundleIdentifier };
}
@@ -7,7 +7,7 @@ import {
} from '../../../graphql/generated';
import Log from '../../../log';
import { ora } from '../../../ora';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import { AppLookupParams } from '../api/GraphqlClient';
import { AppleProvisioningProfileMutationResult } from '../api/graphql/mutations/AppleProvisioningProfileMutation';
@@ -21,7 +21,9 @@ export class ConfigureProvisioningProfile {
private originalProvisioningProfile: AppleProvisioningProfileFragment
) {}
public async runAsync(ctx: Context): Promise<AppleProvisioningProfileMutationResult | null> {
public async runAsync(
ctx: CredentialsContext
): Promise<AppleProvisioningProfileMutationResult | null> {
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'Configuring Provisioning Profiles is only supported in interactive mode.'
@@ -62,7 +64,7 @@ export class ConfigureProvisioningProfile {
}
private async configureAndUpdateAsync(
ctx: Context,
ctx: CredentialsContext,
authCtx: AuthCtx,
app: AppLookupParams,
profileFromApple: ProvisioningProfileStoreInfo
@@ -1,13 +1,15 @@
import Log from '../../../log';
import { Account } from '../../../user/Account';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { AppleDistributionCertificateMutationResult } from '../api/graphql/mutations/AppleDistributionCertificateMutation';
import { provideOrGenerateDistributionCertificateAsync } from './DistributionCertificateUtils';
export class CreateDistributionCertificate {
constructor(private account: Account) {}
public async runAsync(ctx: Context): Promise<AppleDistributionCertificateMutationResult> {
public async runAsync(
ctx: CredentialsContext
): Promise<AppleDistributionCertificateMutationResult> {
const distCert = await provideOrGenerateDistributionCertificateAsync(ctx, this.account.name);
const result = await ctx.ios.createDistributionCertificateAsync(this.account, distCert);
Log.succeed('Created distribution certificate');
@@ -3,7 +3,7 @@ import nullthrows from 'nullthrows';
import { AppleDistributionCertificateFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
import { AppLookupParams } from '../api/GraphqlClient';
@@ -20,7 +20,7 @@ export class CreateProvisioningProfile {
private distributionCertificate: AppleDistributionCertificateFragment
) {}
async runAsync(ctx: Context): Promise<AppleProvisioningProfileMutationResult> {
async runAsync(ctx: CredentialsContext): Promise<AppleProvisioningProfileMutationResult> {
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'Creating Provisioning Profiles is only supported in interactive mode.'
@@ -46,7 +46,7 @@ export class CreateProvisioningProfile {
}
private async provideOrGenerateAsync(
ctx: Context,
ctx: CredentialsContext,
appleAuthCtx: AuthCtx
): Promise<ProvisioningProfile> {
const userProvided = await askForUserProvidedAsync(provisioningProfileSchema);
@@ -1,13 +1,13 @@
import { ApplePushKeyFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { Account } from '../../../user/Account';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { provideOrGeneratePushKeyAsync } from './PushKeyUtils';
export class CreatePushKey {
constructor(private account: Account) {}
public async runAsync(ctx: Context): Promise<ApplePushKeyFragment> {
public async runAsync(ctx: CredentialsContext): Promise<ApplePushKeyFragment> {
if (ctx.nonInteractive) {
throw new Error(`A new push key cannot be created in non-interactive mode.`);
}
@@ -7,15 +7,15 @@ import Log, { learnMore } from '../../../log';
import { promptAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { fromNow } from '../../../utils/date';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
import { AppLookupParams } from '../api/GraphqlClient';
import { AppleTooManyCertsError } from '../appstore/AppStoreApi';
import {
DistributionCertificate,
DistributionCertificateStoreInfo,
} from '../appstore/Credentials.types';
import { filterRevokedDistributionCertsFromEasServers } from '../appstore/CredentialsUtils';
import { AppleTooManyCertsError } from '../appstore/distributionCertificate';
import { distributionCertificateSchema } from '../credentials';
import { validateDistributionCertificateAsync } from '../validators/validateDistributionCertificate';
import { formatAppleTeam } from './AppleTeamUtils';
@@ -84,7 +84,7 @@ async function _selectDistributionCertificateAsync(
* select a distribution certificate from an account (validity status shown on a best effort basis)
* */
export async function selectDistributionCertificateWithDependenciesAsync(
ctx: Context,
ctx: CredentialsContext,
account: Account
): Promise<AppleDistributionCertificateFragment | null> {
const distCertsForAccount = await ctx.ios.getDistributionCertificatesForAccountAsync(account);
@@ -110,7 +110,7 @@ export async function selectDistributionCertificateWithDependenciesAsync(
* select a distribution certificate from a valid set (curated on a best effort basis)
* */
export async function selectValidDistributionCertificateAsync(
ctx: Context,
ctx: CredentialsContext,
appLookupParams: AppLookupParams
): Promise<AppleDistributionCertificateFragment | null> {
const distCertsForAccount = await ctx.ios.getDistributionCertificatesForAccountAsync(
@@ -133,7 +133,7 @@ export async function selectValidDistributionCertificateAsync(
// filter by valid certs on the developer portal
const certInfoFromApple = await ctx.appStore.listDistributionCertificatesAsync();
const validDistCerts = await filterRevokedDistributionCertsFromEasServers(
const validDistCerts = filterRevokedDistributionCertsFromEasServers(
distCertsForAppleTeam,
certInfoFromApple
);
@@ -152,7 +152,7 @@ Please remember that Apple Distribution Certificates are not application specifi
`;
export async function provideOrGenerateDistributionCertificateAsync(
ctx: Context,
ctx: CredentialsContext,
accountName: string
): Promise<DistributionCertificate> {
if (!ctx.nonInteractive) {
@@ -177,7 +177,9 @@ export async function provideOrGenerateDistributionCertificateAsync(
return await generateDistributionCertificateAsync(ctx, accountName);
}
async function promptForDistCertAsync(ctx: Context): Promise<DistributionCertificate | null> {
async function promptForDistCertAsync(
ctx: CredentialsContext
): Promise<DistributionCertificate | null> {
let initialValues: { teamId?: string } = {};
if (ctx.appStore.authCtx) {
initialValues = {
@@ -198,7 +200,7 @@ async function promptForDistCertAsync(ctx: Context): Promise<DistributionCertifi
}
async function generateDistributionCertificateAsync(
ctx: Context,
ctx: CredentialsContext,
accountName: string
): Promise<DistributionCertificate> {
await ctx.appStore.ensureAuthenticatedAsync();
@@ -1,6 +1,6 @@
import chalk from 'chalk';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import {
DistributionCertificate,
ProvisioningProfile,
@@ -19,7 +19,7 @@ export function formatProvisioningProfileFromApple(
}
export async function generateProvisioningProfileAsync(
ctx: Context,
ctx: CredentialsContext,
bundleIdentifier: string,
distCert: DistributionCertificate
): Promise<ProvisioningProfile> {
@@ -5,7 +5,7 @@ import Log, { learnMore } from '../../../log';
import { confirmAsync, promptAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { fromNow } from '../../../utils/date';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
import { PushKey, PushKeyStoreInfo } from '../appstore/Credentials.types';
import { filterRevokedAndUntrackedPushKeysFromEasServersAsync } from '../appstore/CredentialsUtils';
@@ -15,7 +15,7 @@ import { isPushKeyValidAndTrackedAsync } from '../validators/validatePushKey';
import { formatAppleTeam } from './AppleTeamUtils';
export async function provideOrGeneratePushKeyAsync(
ctx: Context,
ctx: CredentialsContext,
accountName: string
): Promise<PushKey> {
if (!ctx.nonInteractive) {
@@ -43,7 +43,7 @@ export async function provideOrGeneratePushKeyAsync(
return await generatePushKeyAsync(ctx, accountName);
}
async function promptForPushKeyAsync(ctx: Context): Promise<PushKey | null> {
async function promptForPushKeyAsync(ctx: CredentialsContext): Promise<PushKey | null> {
let initialValues: { teamId?: string } = {};
if (ctx.appStore.authCtx) {
initialValues = {
@@ -63,7 +63,10 @@ async function promptForPushKeyAsync(ctx: Context): Promise<PushKey | null> {
return userProvided;
}
async function generatePushKeyAsync(ctx: Context, accountName: string): Promise<PushKey> {
async function generatePushKeyAsync(
ctx: CredentialsContext,
accountName: string
): Promise<PushKey> {
await ctx.appStore.ensureAuthenticatedAsync();
try {
return await ctx.appStore.createPushKeyAsync();
@@ -116,7 +119,7 @@ function formatPushKeyFromApple(pushKey: PushKeyStoreInfo): string {
* select a push key from an account (validity status shown on a best effort basis)
* */
export async function selectPushKeyAsync(
ctx: Context,
ctx: CredentialsContext,
account: Account
): Promise<ApplePushKeyFragment | null> {
const pushKeysForAccount = await ctx.ios.getPushKeysForAccountAsync(account);
@@ -133,7 +136,7 @@ export async function selectPushKeyAsync(
}
export async function getValidAndTrackedPushKeysOnEasServersAsync(
ctx: Context,
ctx: CredentialsContext,
pushKeysForAccount: ApplePushKeyFragment[]
): Promise<ApplePushKeyFragment[]> {
const pushInfoFromApple = await ctx.appStore.listPushKeysAsync();
@@ -5,7 +5,7 @@ import {
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
import { selectDistributionCertificateWithDependenciesAsync } from './DistributionCertificateUtils';
import { RemoveProvisioningProfiles } from './RemoveProvisioningProfile';
@@ -13,7 +13,7 @@ import { RemoveProvisioningProfiles } from './RemoveProvisioningProfile';
export class SelectAndRemoveDistributionCertificate {
constructor(private account: Account) {}
async runAsync(ctx: Context): Promise<void> {
async runAsync(ctx: CredentialsContext): Promise<void> {
const selected = await selectDistributionCertificateWithDependenciesAsync(ctx, this.account);
if (selected) {
await new RemoveDistributionCertificate(this.account, selected).runAsync(ctx);
@@ -29,7 +29,7 @@ export class RemoveDistributionCertificate {
private distributionCertificate: AppleDistributionCertificateFragment
) {}
public async runAsync(ctx: Context): Promise<void> {
public async runAsync(ctx: CredentialsContext): Promise<void> {
const apps = this.distributionCertificate.iosAppBuildCredentialsList.map(
buildCredentials => buildCredentials.iosAppCredentials.app
);
@@ -71,7 +71,7 @@ export class RemoveDistributionCertificate {
await this.removeInvalidProvisioningProfilesAsync(ctx);
}
private async removeInvalidProvisioningProfilesAsync(ctx: Context): Promise<void> {
private async removeInvalidProvisioningProfilesAsync(ctx: CredentialsContext): Promise<void> {
const buildCredentialsList = this.distributionCertificate.iosAppBuildCredentialsList;
const appsWithProfilesToRemove: AppLookupParams[] = [];
const profilesToRemove: AppleProvisioningProfileIdentifiersFragment[] = [];
@@ -2,7 +2,7 @@ import assert from 'assert';
import { AppleProvisioningProfileIdentifiersFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
export class RemoveProvisioningProfiles {
@@ -16,7 +16,7 @@ export class RemoveProvisioningProfiles {
);
}
async runAsync(ctx: Context): Promise<void> {
async runAsync(ctx: CredentialsContext): Promise<void> {
if (this.provisioningProfiles.length === 0) {
Log.log(`Skipping deletion of Provisioning Profiles`);
return;
@@ -2,13 +2,13 @@ import { ApplePushKeyFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { selectPushKeyAsync } from './PushKeyUtils';
export class SelectAndRemovePushKey {
constructor(private account: Account) {}
async runAsync(ctx: Context): Promise<void> {
async runAsync(ctx: CredentialsContext): Promise<void> {
const selected = await selectPushKeyAsync(ctx, this.account);
if (selected) {
await new RemovePushKey(this.account, selected).runAsync(ctx);
@@ -21,7 +21,7 @@ export class SelectAndRemovePushKey {
export class RemovePushKey {
constructor(private account: Account, private pushKey: ApplePushKeyFragment) {}
public async runAsync(ctx: Context): Promise<void> {
public async runAsync(ctx: CredentialsContext): Promise<void> {
if (ctx.nonInteractive) {
throw new Error(`Cannot remove push keys in non-interactive mode`);
}
@@ -14,7 +14,7 @@ import {
import Log from '../../../log';
import { confirmAsync, pressAnyKeyToContinueAsync } from '../../../prompts';
import differenceBy from '../../../utils/expodash/differenceBy';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import { AppLookupParams } from '../api/GraphqlClient';
import { validateProvisioningProfileAsync } from '../validators/validateProvisioningProfile';
@@ -26,7 +26,7 @@ import { SetupDistributionCertificate } from './SetupDistributionCertificate';
export class SetupAdhocProvisioningProfile {
constructor(private app: AppLookupParams) {}
async runAsync(ctx: Context): Promise<IosAppBuildCredentialsFragment> {
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
const distCert = await new SetupDistributionCertificate(
this.app,
IosDistributionType.AdHoc
@@ -60,7 +60,7 @@ export class SetupAdhocProvisioningProfile {
}
async runWithDistributionCertificateAsync(
ctx: Context,
ctx: CredentialsContext,
distCert: AppleDistributionCertificateFragment
): Promise<IosAppBuildCredentialsFragment> {
const currentBuildCredentials = await getBuildCredentialsAsync(
@@ -158,7 +158,7 @@ export class SetupAdhocProvisioningProfile {
);
}
private async areBuildCredentialsSetupAsync(ctx: Context): Promise<boolean> {
private async areBuildCredentialsSetupAsync(ctx: CredentialsContext): Promise<boolean> {
const buildCredentials = await getBuildCredentialsAsync(
ctx,
this.app,
@@ -168,7 +168,7 @@ export class SetupAdhocProvisioningProfile {
}
private async shouldUseExistingProfileAsync(
ctx: Context,
ctx: CredentialsContext,
buildCredentials: IosAppBuildCredentialsFragment
): Promise<boolean> {
const provisioningProfile = nullthrows(buildCredentials.provisioningProfile);
@@ -202,7 +202,7 @@ export class SetupAdhocProvisioningProfile {
}
private async registerDevicesAsync(
ctx: Context,
ctx: CredentialsContext,
appleTeam: AppleTeamFragment
): Promise<AppleDeviceFragment[]> {
const action = new DeviceCreateAction(this.app.account, appleTeam);
@@ -3,7 +3,7 @@ import chalk from 'chalk';
import nullthrows from 'nullthrows';
import Log from '../../../log';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { IosCapabilitiesOptions } from '../appstore/ensureAppExists';
import { App, IosAppBuildCredentialsMap, IosCredentials, Target } from '../types';
import { displayProjectCredentials } from '../utils/printCredentials';
@@ -20,7 +20,7 @@ interface Options {
export class SetupBuildCredentials {
constructor(private options: Options) {}
async runAsync(ctx: Context): Promise<IosCredentials> {
async runAsync(ctx: CredentialsContext): Promise<IosCredentials> {
const hasManyTargets = this.options.targets.length > 1;
const iosAppBuildCredentialsMap: IosAppBuildCredentialsMap = {};
if (hasManyTargets) {
@@ -3,7 +3,7 @@ import chalk from 'chalk';
import { IosDistributionType } from '../../../graphql/generated';
import Log from '../../../log';
import { findApplicationTarget } from '../../../project/ios/target';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { readIosCredentialsAsync } from '../../credentialsJson/read';
import { IosCredentials } from '../../credentialsJson/types';
import { ensureAllTargetsAreConfigured } from '../../credentialsJson/utils';
@@ -17,7 +17,7 @@ export class SetupBuildCredentialsFromCredentialsJson {
private distributionType: IosDistributionType
) {}
async runAsync(ctx: Context): Promise<void> {
async runAsync(ctx: CredentialsContext): Promise<void> {
const credentialsJson = await this.readCredentialsJsonAsync(ctx);
ensureAllTargetsAreConfigured(this.targets, credentialsJson);
@@ -45,7 +45,7 @@ export class SetupBuildCredentialsFromCredentialsJson {
}
}
private async readCredentialsJsonAsync(ctx: Context): Promise<IosCredentials> {
private async readCredentialsJsonAsync(ctx: CredentialsContext): Promise<IosCredentials> {
const applicationTarget = findApplicationTarget(this.targets);
try {
return await readIosCredentialsAsync(ctx.projectDir, applicationTarget);
@@ -8,7 +8,7 @@ import {
import Log from '../../../log';
import { confirmAsync, promptAsync } from '../../../prompts';
import sortBy from '../../../utils/expodash/sortBy';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import { AppLookupParams } from '../api/GraphqlClient';
import { AppleDistributionCertificateMutationResult } from '../api/graphql/mutations/AppleDistributionCertificateMutation';
@@ -23,7 +23,7 @@ export class SetupDistributionCertificate {
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
public async runAsync(ctx: Context): Promise<AppleDistributionCertificateFragment> {
public async runAsync(ctx: CredentialsContext): Promise<AppleDistributionCertificateFragment> {
const appleTeam = await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app);
try {
@@ -47,7 +47,7 @@ export class SetupDistributionCertificate {
}
private async runNonInteractiveAsync(
ctx: Context,
ctx: CredentialsContext,
currentCertificate: AppleDistributionCertificateFragment | null
): Promise<AppleDistributionCertificateFragment> {
// TODO: implement validation
@@ -60,7 +60,7 @@ export class SetupDistributionCertificate {
}
private async runInteractiveAsync(
ctx: Context,
ctx: CredentialsContext,
currentCertificate: AppleDistributionCertificateFragment | null
): Promise<AppleDistributionCertificateFragment> {
if (await this.isCurrentCertificateValidAsync(ctx, currentCertificate)) {
@@ -75,7 +75,7 @@ export class SetupDistributionCertificate {
}
private async isCurrentCertificateValidAsync(
ctx: Context,
ctx: CredentialsContext,
currentCertificate: AppleDistributionCertificateFragment | null
): Promise<boolean> {
if (!currentCertificate) {
@@ -108,7 +108,7 @@ export class SetupDistributionCertificate {
}
private async createOrReuseDistCertAsync(
ctx: Context
ctx: CredentialsContext
): Promise<AppleDistributionCertificateFragment> {
const validDistCerts = await this.getValidDistCertsAsync(ctx);
const autoselectedDistCert = validDistCerts[0];
@@ -147,12 +147,12 @@ export class SetupDistributionCertificate {
}
private async createNewDistCertAsync(
ctx: Context
ctx: CredentialsContext
): Promise<AppleDistributionCertificateMutationResult> {
return new CreateDistributionCertificate(this.app.account).runAsync(ctx);
}
async reuseDistCertAsync(ctx: Context): Promise<AppleDistributionCertificate> {
async reuseDistCertAsync(ctx: CredentialsContext): Promise<AppleDistributionCertificate> {
const validDistCerts = await this.getValidDistCertsAsync(ctx);
const { distCert } = await promptAsync({
type: 'select',
@@ -167,7 +167,7 @@ export class SetupDistributionCertificate {
}
private async getValidDistCertsAsync(
ctx: Context
ctx: CredentialsContext
): Promise<AppleDistributionCertificateFragment[]> {
if (this.validDistCerts) {
return this.validDistCerts;
@@ -1,7 +1,7 @@
import { IosAppBuildCredentialsFragment, IosDistributionType } from '../../../graphql/generated';
import Log from '../../../log';
import { promptAsync } from '../../../prompts';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
import { getAllBuildCredentialsAsync } from './BuildCredentialsUtils';
import { SetupAdhocProvisioningProfile } from './SetupAdhocProvisioningProfile';
@@ -17,7 +17,7 @@ import { SetupProvisioningProfile } from './SetupProvisioningProfile';
export class SetupInternalProvisioningProfile {
constructor(private app: AppLookupParams) {}
async runAsync(ctx: Context): Promise<IosAppBuildCredentialsFragment> {
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
const buildCredentials = await getAllBuildCredentialsAsync(ctx, this.app);
const adhocBuildCredentialsExist =
@@ -80,13 +80,13 @@ export class SetupInternalProvisioningProfile {
}
private async setupAdhocProvisioningProfileAsync(
ctx: Context
ctx: CredentialsContext
): Promise<IosAppBuildCredentialsFragment> {
return await new SetupAdhocProvisioningProfile(this.app).runAsync(ctx);
}
private async setupUniversalProvisioningProfileAsync(
ctx: Context
ctx: CredentialsContext
): Promise<IosAppBuildCredentialsFragment> {
return await new SetupProvisioningProfile(this.app, IosDistributionType.Enterprise).runAsync(
ctx
@@ -94,7 +94,7 @@ export class SetupInternalProvisioningProfile {
}
private async askForDistributionTypeAndSetupAsync(
ctx: Context,
ctx: CredentialsContext,
message: string
): Promise<IosAppBuildCredentialsFragment> {
const { distributionType } = await promptAsync({
@@ -7,7 +7,7 @@ import {
IosDistributionType,
} from '../../../graphql/generated';
import { confirmAsync } from '../../../prompts';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import { AppLookupParams } from '../api/GraphqlClient';
import { ProvisioningProfileStoreInfo } from '../appstore/Credentials.types';
@@ -28,13 +28,13 @@ import { SetupDistributionCertificate } from './SetupDistributionCertificate';
export class SetupProvisioningProfile {
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
async areBuildCredentialsSetupAsync(ctx: Context): Promise<boolean> {
async areBuildCredentialsSetupAsync(ctx: CredentialsContext): Promise<boolean> {
const buildCredentials = await getBuildCredentialsAsync(ctx, this.app, this.distributionType);
return await validateProvisioningProfileAsync(ctx, this.app, buildCredentials);
}
async assignNewAndDeleteOldProfileAsync(
ctx: Context,
ctx: CredentialsContext,
distCert: AppleDistributionCertificateFragment,
currentProfile: AppleProvisioningProfileFragment
): Promise<IosAppBuildCredentialsFragment> {
@@ -45,7 +45,7 @@ export class SetupProvisioningProfile {
}
async createAndAssignProfileAsync(
ctx: Context,
ctx: CredentialsContext,
distCert: AppleDistributionCertificateFragment
): Promise<IosAppBuildCredentialsFragment> {
const provisioningProfile = await new CreateProvisioningProfile(this.app, distCert).runAsync(
@@ -61,7 +61,7 @@ export class SetupProvisioningProfile {
}
async configureAndAssignProfileAsync(
ctx: Context,
ctx: CredentialsContext,
distCert: AppleDistributionCertificateFragment,
originalProvisioningProfile: AppleProvisioningProfileFragment
): Promise<IosAppBuildCredentialsFragment | null> {
@@ -83,7 +83,7 @@ export class SetupProvisioningProfile {
);
}
async runAsync(ctx: Context): Promise<IosAppBuildCredentialsFragment> {
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
const distCert = await new SetupDistributionCertificate(
this.app,
this.distributionType
@@ -4,7 +4,7 @@ import nullthrows from 'nullthrows';
import { ApplePushKeyFragment, CommonIosAppCredentialsFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync, promptAsync } from '../../../prompts';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
import { AssignPushKey } from './AssignPushKey';
import { CreatePushKey } from './CreatePushKey';
@@ -17,12 +17,12 @@ import {
export class SetupPushKey {
constructor(private app: AppLookupParams) {}
async isPushKeySetupAsync(ctx: Context): Promise<boolean> {
async isPushKeySetupAsync(ctx: CredentialsContext): Promise<boolean> {
const pushKey = await ctx.ios.getPushKeyForAppAsync(this.app);
return !!pushKey;
}
public async runAsync(ctx: Context): Promise<CommonIosAppCredentialsFragment | null> {
public async runAsync(ctx: CredentialsContext): Promise<CommonIosAppCredentialsFragment | null> {
if (ctx.nonInteractive) {
throw new Error(`Push keys cannot be setup in non-interactive mode.`);
}
@@ -42,7 +42,7 @@ export class SetupPushKey {
return await new AssignPushKey(this.app).runAsync(ctx, pushKey);
}
private async createOrReusePushKeyAsync(ctx: Context): Promise<ApplePushKeyFragment> {
private async createOrReusePushKeyAsync(ctx: CredentialsContext): Promise<ApplePushKeyFragment> {
const pushKeysForAccount = await ctx.ios.getPushKeysForAccountAsync(this.app.account);
assert(
pushKeysForAccount.length > 0,
@@ -5,7 +5,7 @@ import {
IosAppBuildCredentialsFragment,
} from '../../../graphql/generated';
import Log from '../../../log';
import { Action, Context } from '../../context';
import { CredentialsContext } from '../../context';
import { AppLookupParams as GraphQLAppLookupParams } from '../api/GraphqlClient';
import { IosCapabilitiesOptions } from '../appstore/ensureAppExists';
import { SetupAdhocProvisioningProfile } from './SetupAdhocProvisioningProfile';
@@ -18,10 +18,10 @@ interface Options {
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
export class SetupTargetBuildCredentials implements Action<IosAppBuildCredentialsFragment> {
export class SetupTargetBuildCredentials {
constructor(private options: Options) {}
async runAsync(ctx: Context): Promise<IosAppBuildCredentialsFragment> {
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
const { app, iosCapabilitiesOptions } = this.options;
await ctx.bestEffortAppStoreAuthenticateAsync();
@@ -44,7 +44,9 @@ export class SetupTargetBuildCredentials implements Action<IosAppBuildCredential
}
}
async setupBuildCredentialsAsync(ctx: Context): Promise<IosAppBuildCredentialsFragment> {
async setupBuildCredentialsAsync(
ctx: CredentialsContext
): Promise<IosAppBuildCredentialsFragment> {
const { app, distribution, enterpriseProvisioning } = this.options;
if (distribution === 'internal') {
if (enterpriseProvisioning === 'adhoc') {
@@ -10,7 +10,7 @@ import {
} from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { IosTargetCredentials } from '../../credentialsJson/types';
import { AppLookupParams } from '../api/GraphqlClient';
import { displayProjectCredentials } from '../utils/printCredentials';
@@ -29,7 +29,7 @@ export class SetupTargetBuildCredentialsFromCredentialsJson {
private targetCredentials: IosTargetCredentials
) {}
async runAsync(ctx: Context): Promise<IosAppBuildCredentialsFragment> {
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
// currently configured credentials
const buildCredentials = await getBuildCredentialsAsync(ctx, this.app, this.distributionType);
const currentDistributionCertificate = await getDistributionCertificateAsync(
@@ -102,7 +102,7 @@ export class SetupTargetBuildCredentialsFromCredentialsJson {
}
async getDistributionCertificateToAssignAsync(
ctx: Context,
ctx: CredentialsContext,
appleTeam: AppleTeamFragment,
currentDistributionCertificate: AppleDistributionCertificateFragment | null
): Promise<AppleDistributionCertificateFragment> {
@@ -132,7 +132,7 @@ export class SetupTargetBuildCredentialsFromCredentialsJson {
}
async getProvisioningProfileToAssignAsync(
ctx: Context,
ctx: CredentialsContext,
appleTeam: AppleTeamFragment,
currentProvisioningProfile: AppleProvisioningProfileFragment | null
): Promise<AppleProvisioningProfileFragment> {
@@ -152,7 +152,7 @@ export class SetupTargetBuildCredentialsFromCredentialsJson {
}
async createNewProvisioningProfileAsync(
ctx: Context,
ctx: CredentialsContext,
appleTeam: AppleTeamFragment
): Promise<AppleProvisioningProfileFragment> {
const { provisioningProfile } = this.targetCredentials;
@@ -1,6 +1,6 @@
import { IosDistributionType } from '../../../graphql/generated';
import Log from '../../../log';
import { Context } from '../../context';
import { CredentialsContext } from '../../context';
import { updateIosCredentialsAsync } from '../../credentialsJson/update';
import { App, Target } from '../types';
@@ -11,7 +11,7 @@ export class UpdateCredentialsJson {
private distributionType: IosDistributionType
) {}
async runAsync(ctx: Context): Promise<void> {
async runAsync(ctx: CredentialsContext): Promise<void> {
Log.log('Updating iOS credentials in credentials.json');
await updateIosCredentialsAsync(ctx, this.app, this.targets, this.distributionType);
Log.succeed(
@@ -3,7 +3,7 @@ import {
AppleProvisioningProfileFragment,
} from '../../../../graphql/generated';
import { testProvisioningProfileFragment } from '../../../__tests__/fixtures-ios';
import { Context } from '../../../context';
import { CredentialsContext } from '../../../context';
import { AppLookupParams } from '../../api/GraphqlClient';
import { AppleProvisioningProfileMutationResult } from '../../api/graphql/mutations/AppleProvisioningProfileMutation';
@@ -14,7 +14,9 @@ export class ConfigureProvisioningProfile {
private originalProvisioningProfile: AppleProvisioningProfileFragment
) {}
public async runAsync(ctx: Context): Promise<AppleProvisioningProfileMutationResult | null> {
public async runAsync(
ctx: CredentialsContext
): Promise<AppleProvisioningProfileMutationResult | null> {
return testProvisioningProfileFragment;
}
}
@@ -1,6 +1,6 @@
import { AppleDistributionCertificateFragment } from '../../../../graphql/generated';
import { testProvisioningProfileFragment } from '../../../__tests__/fixtures-ios';
import { Context } from '../../../context';
import { CredentialsContext } from '../../../context';
import { AppLookupParams } from '../../api/GraphqlClient';
import { AppleProvisioningProfileMutationResult } from '../../api/graphql/mutations/AppleProvisioningProfileMutation';
@@ -10,7 +10,7 @@ export class CreateProvisioningProfile {
private distributionCertificate: AppleDistributionCertificateFragment
) {}
async runAsync(ctx: Context): Promise<AppleProvisioningProfileMutationResult> {
async runAsync(ctx: CredentialsContext): Promise<AppleProvisioningProfileMutationResult> {
return testProvisioningProfileFragment;
}
}
@@ -3,12 +3,12 @@ import {
IosDistributionType,
} from '../../../../graphql/generated';
import { testDistCertFragmentOneDependency } from '../../../__tests__/fixtures-ios';
import { Context } from '../../../context';
import { CredentialsContext } from '../../../context';
import { AppLookupParams } from '../../api/GraphqlClient';
export class SetupDistributionCertificate {
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
public async runAsync(_ctx: Context): Promise<AppleDistributionCertificateFragment> {
public async runAsync(_ctx: CredentialsContext): Promise<AppleDistributionCertificateFragment> {
return testDistCertFragmentOneDependency;
}
}
@@ -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 });
Loaded 100 of 176 files, more files were not shown because too many files have changed in this diff. Show more