Compare commits

..
2 Commits
Author SHA1 Message Date
Dominik Sokal 9b36ae0ef8 convert to ESM 2021-10-11 16:15:21 +02:00
Dominik Sokal 1a4cb107af add clean script 2021-10-11 16:12:42 +02:00
30 changed files with 88 additions and 1319 deletions

No files matched your search

-5
View File
@@ -8,17 +8,12 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Add App Store Connect Api Key support to iOS submissions. ([#678](https://github.com/expo/eas-cli/pull/678) by [@quinlanj](https://github.com/quinlanj))
- Create/list/revoke App Store Connect Api keys via Apple apis. ([#687](https://github.com/expo/eas-cli/pull/687) by [@quinlanj](https://github.com/quinlanj))
- Add ability to select a build from a list in `eas submit` interactive mode. ([#688](https://github.com/expo/eas-cli/pull/688) by [@barthap](https://github.com/barthap))
### 🐛 Bug fixes
- Fix printing App Store Connect URL after submission. ([#683](https://github.com/expo/eas-cli/pull/683) by [@brentvatne](https://github.com/brentvatne))
### 🧹 Chores
- Add App Store Connect Api Key fields to `eas.json`. ([#684](https://github.com/expo/eas-cli/pull/684) by [@quinlanj](https://github.com/quinlanj))
- Enable no-underscore-dangle eslint rule. ([#686](https://github.com/expo/eas-cli/pull/686) by [@dsokal](https://github.com/dsokal))
## [0.31.1](https://github.com/expo/eas-cli/releases/tag/0.31.1) - 2021-10-08
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.32.0"
"version": "0.31.1"
}
+2 -1
View File
@@ -10,7 +10,8 @@
"eas": "packages/eas-cli/bin/run",
"lint": "eslint . --ext .ts --ext .js",
"release": "lerna version",
"test": "jest"
"test": "jest",
"clean": "lerna run clean"
},
"devDependencies": {
"@expo/oclif-dev-cli": "1.24.0-expo",
+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.32.0/packages/eas-cli/src/commands/account/login.js)_
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/account/logout.js)_
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/account/view.js)_
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/analytics.js)_
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/build/index.js)_
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/build/cancel.js)_
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/build/configure.js)_
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/build/list.js)_
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/build/view.js)_
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/config.js)_
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/credentials.js)_
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/device/create.js)_
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/device/list.js)_
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/device/view.js)_
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/diagnostics.js)_
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/project/info.js)_
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/project/init.js)_
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/secret/create.js)_
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/secret/delete.js)_
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/secret/list.js)_
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/submit.js)_
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/webhook/create.js)_
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/webhook/delete.js)_
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/webhook/list.js)_
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/webhook/update.js)_
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.31.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.32.0/packages/eas-cli/src/commands/webhook/view.js)_
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.31.1/packages/eas-cli/src/commands/webhook/view.js)_
<!-- commandsstop -->
-251
View File
@@ -2160,18 +2160,6 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "prerequisite",
"description": "",
"args": [],
"type": {
"kind": "OBJECT",
"name": "OfferPrerequisite",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@@ -2198,12 +2186,6 @@
"description": "Advanced Purchase of Paid Resource",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "ADDON",
"description": "Addon, or supplementary subscription",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
@@ -2243,57 +2225,6 @@
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "OfferPrerequisite",
"description": "",
"fields": [
{
"name": "type",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "stripeIds",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Snack",
@@ -10566,22 +10497,6 @@
"name": "Billing",
"description": "",
"fields": [
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "payment",
"description": "",
@@ -10842,42 +10757,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "planId",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "addons",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "AddonDetails",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "",
@@ -10968,65 +10847,6 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AddonDetails",
"description": "",
"fields": [
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "planId",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Charge",
@@ -20519,18 +20339,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "ascApiKey",
"description": "",
"type": {
"kind": "INPUT_OBJECT",
"name": "AscApiKeyInput",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "archiveUrl",
"description": "",
@@ -20580,65 +20388,6 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AscApiKeyInput",
"description": "",
"fields": null,
"inputFields": [
{
"name": "keyP8",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "keyIdentifier",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "issuerIdentifier",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "CreateAndroidSubmissionInput",
+5 -4
View File
@@ -1,7 +1,8 @@
{
"name": "eas-cli",
"type": "module",
"description": "EAS command line tool",
"version": "0.32.0",
"version": "0.31.1",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -12,7 +13,7 @@
"@expo/config": "6.0.2",
"@expo/config-plugins": "4.0.2",
"@expo/eas-build-job": "0.2.52",
"@expo/eas-json": "^0.32.0",
"@expo/eas-json": "^0.31.0",
"@expo/json-file": "8.2.33",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.14",
@@ -113,7 +114,6 @@
"cli"
],
"license": "MIT",
"main": "build/index.js",
"oclif": {
"bin": "eas",
"commands": "./build/commands",
@@ -177,6 +177,7 @@
"typecheck": "tsc",
"test": "jest",
"version": "yarn oclif-dev readme && node patch-readme && git add README.md",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml"
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
"clean": "rm -rf build node_modules"
}
}
@@ -5,9 +5,3 @@ export class MissingCredentialsNonInteractiveError extends Error {
);
}
}
export class MissingCredentialsError extends Error {
constructor(message?: string) {
super(message ?? 'Credentials are not set up.');
}
}
@@ -1,71 +0,0 @@
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import * as uuid from 'uuid';
import Log, { learnMore } from '../../../log';
import { promptAsync } from '../../../prompts';
import { getCredentialsFromUserAsync } from '../../utils/promptForCredentials';
import { AscApiKey } from '../appstore/Credentials.types';
import { AscApiKeyPath, MinimalAscApiKey, ascApiKeyMetadataSchema } from '../credentials';
export async function promptForAscApiKeyAsync(): Promise<AscApiKeyPath> {
Log.log(
chalk.bold('An App Store Connect Api key is required to upload your app to the Apple App Store')
);
Log.log(
`If you're not sure what this is or how to create one, ${learnMore(
'https://expo.fyi/creating-asc-api-key'
)}`
);
const { keyP8Path } = await promptAsync({
type: 'text',
name: 'keyP8Path',
message: 'Path to App Store Connect Api Key:',
initial: 'AuthKey_ABCD.p8',
// eslint-disable-next-line async-protect/async-suffix
validate: async (filePath: string) => {
try {
const stats = await fs.stat(filePath);
if (stats.isFile()) {
return true;
}
return 'Input is not a file.';
} catch {
return 'File does not exist.';
}
},
});
const regex = /^AuthKey_(?<keyId>\w+)\.p8$/; // Common ASC Api file name downloaded from Apple
const bestEffortKeyId = path.basename(keyP8Path).match(regex)?.groups?.keyId;
const ascApiKeyMetadata = await getCredentialsFromUserAsync(ascApiKeyMetadataSchema, {
keyId: bestEffortKeyId,
});
return { ...ascApiKeyMetadata, keyP8Path };
}
export async function promptForIssuerIdAsync(): Promise<string> {
Log.log(chalk.bold('An App Store Connect Issuer ID is required'));
Log.log(
`If you're not sure what this is or how to find yours, ${learnMore(
'https://expo.fyi/asc-issuer-id'
)}`
);
const { issuerId } = await promptAsync({
type: 'text',
name: 'issuerId',
message: 'App Store Connect Issuer ID:',
validate: (input: string) => uuid.validate(input),
});
return issuerId;
}
export async function getMinimalAscApiKeyAsync(ascApiKey: AscApiKey): Promise<MinimalAscApiKey> {
return {
...ascApiKey,
issuerId: ascApiKey.issuerId ?? (await promptForIssuerIdAsync()),
};
}
@@ -1,6 +1,4 @@
import {
AscApiKey,
AscApiKeyInfo,
DistributionCertificate,
DistributionCertificateStoreInfo,
ProvisioningProfile,
@@ -8,12 +6,6 @@ import {
PushKey,
PushKeyStoreInfo,
} from './Credentials.types';
import {
createAscApiKeyAsync,
getAscApiKeyAsync,
listAscApiKeysAsync,
revokeAscApiKeyAsync,
} from './ascApiKey';
import { AuthCtx, Options as AuthenticateOptions, authenticateAsync } from './authenticate';
import {
createDistributionCertificateAsync,
@@ -144,24 +136,4 @@ export default class AppStoreApi {
distCertSerialNumber
);
}
public async listAscApiKeysAsync(): Promise<AscApiKeyInfo[]> {
const ctx = await this.ensureAuthenticatedAsync();
return await listAscApiKeysAsync(ctx);
}
public async getAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo> {
const ctx = await this.ensureAuthenticatedAsync();
return await getAscApiKeyAsync(ctx, keyId);
}
public async createAscApiKeyAsync({ nickname }: { nickname: string }): Promise<AscApiKey> {
const ctx = await this.ensureAuthenticatedAsync();
return await createAscApiKeyAsync(ctx, { nickname });
}
public async revokeAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo> {
const ctx = await this.ensureAuthenticatedAsync();
return await revokeAscApiKeyAsync(ctx, keyId);
}
}
@@ -1,5 +1,3 @@
import { UserRole } from '@expo/apple-utils';
export interface Device {
id: string;
teamId: string;
@@ -68,16 +66,3 @@ export interface PushKey {
teamId: string;
teamName?: string;
}
export type AscApiKeyInfo = {
keyId: string;
issuerId?: string;
teamId: string;
name: string;
teamName?: string;
roles: UserRole[];
};
export type AscApiKey = AscApiKeyInfo & {
keyP8: string;
};
@@ -1,98 +0,0 @@
import { ApiKey, ApiKeyType, UserRole } from '@expo/apple-utils';
import { asMock } from '../../../../__tests__/utils';
import {
createAscApiKeyAsync,
getAscApiKeyAsync,
listAscApiKeysAsync,
revokeAscApiKeyAsync,
} from '../ascApiKey';
import { getRequestContext } from '../authenticate';
jest.mock('@expo/apple-utils');
jest.mock('../authenticate');
jest.mock('../../../../ora');
const mockTeam = {
id: 'test-id',
name: 'test-name',
};
const mockAuthCtx = {
appleId: 'test-appleId',
team: mockTeam,
};
const mockApiKey = {
type: 'apiKeys',
id: 'NL67AN9Q6Q',
attributes: {
nickname: 'ejb-apple-utils-admin',
lastUsed: '2021-06-28T12:30:41-07:00',
revokingDate: null,
isActive: true,
canDownload: false,
privateKey: null,
roles: ['ADMIN'],
allAppsVisible: true,
keyType: 'PUBLIC_API',
},
downloadAsync: jest.fn(() => 'super secret'),
revokeAsync: jest.fn(() => mockApiKey),
} as unknown as ApiKey;
const mockAscApiKeyInfo = {
issuerId: undefined,
keyId: 'NL67AN9Q6Q',
name: 'ejb-apple-utils-admin',
roles: ['ADMIN'],
teamId: 'test-id',
teamName: 'test-name',
};
beforeEach(() => {
jest.clearAllMocks();
});
test(`listAscApiKeysAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'getAsync').mockImplementation(async () => []);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await listAscApiKeysAsync(mockAuthCtx);
expect(ApiKey.getAsync).toHaveBeenLastCalledWith(mockRequestContext);
expect(result).toEqual([]);
});
test(`getAscApiKeyAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'infoAsync').mockImplementation(async () => mockApiKey);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await getAscApiKeyAsync(mockAuthCtx, 'test-key-id');
expect(ApiKey.infoAsync).toHaveBeenLastCalledWith(mockRequestContext, { id: 'test-key-id' });
expect(result).toEqual(mockAscApiKeyInfo);
});
test(`createAscApiKeyAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'createAsync').mockImplementation(async () => mockApiKey);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await createAscApiKeyAsync(mockAuthCtx, { nickname: 'test-name' });
expect(ApiKey.createAsync).toHaveBeenLastCalledWith(mockRequestContext, {
nickname: 'test-name',
allAppsVisible: true,
roles: [UserRole.ADMIN],
keyType: ApiKeyType.PUBLIC_API,
});
expect(result).toEqual({ ...mockAscApiKeyInfo, keyP8: 'super secret' });
});
test(`revokeAscApiKeyAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'infoAsync').mockImplementation(async () => mockApiKey);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await revokeAscApiKeyAsync(mockAuthCtx, 'test-key-id');
expect(ApiKey.infoAsync).toHaveBeenLastCalledWith(mockRequestContext, {
id: 'test-key-id',
});
expect(mockApiKey.revokeAsync).toBeCalled();
expect(result).toEqual(mockAscApiKeyInfo);
});
@@ -1,104 +0,0 @@
import { ApiKey, ApiKeyProps, ApiKeyType, UserRole } from '@expo/apple-utils';
import Log from '../../../log';
import { ora } from '../../../ora';
import { AscApiKey, AscApiKeyInfo } from './Credentials.types';
import { AuthCtx, getRequestContext } from './authenticate';
export async function listAscApiKeysAsync(authCtx: AuthCtx): Promise<AscApiKeyInfo[]> {
const spinner = ora(`Fetching App Store Connect API Keys.`).start();
try {
const context = getRequestContext(authCtx);
const keys = await ApiKey.getAsync(context);
spinner.succeed(`Fetched App Store Connect API Keys.`);
return keys.map(key => getAscApiKeyInfo(key, authCtx));
} catch (error) {
spinner.fail(`Failed to fetch App Store Connect API Keys.`);
throw error;
}
}
export async function getAscApiKeyAsync(authCtx: AuthCtx, keyId: string): Promise<AscApiKeyInfo> {
const spinner = ora(`Fetching App Store Connect API Key.`).start();
try {
const context = getRequestContext(authCtx);
const apiKey = await ApiKey.infoAsync(context, { id: keyId });
spinner.succeed(`Fetched App Store Connect API Key (ID: ${keyId}).`);
return getAscApiKeyInfo(apiKey, authCtx);
} catch (error) {
Log.error(error);
spinner.fail(`Failed to fetch App Store Connect API Key.`);
throw error;
}
}
export async function createAscApiKeyAsync(
authCtx: AuthCtx,
{
nickname,
allAppsVisible,
roles,
keyType,
}: Partial<Pick<ApiKeyProps, 'nickname' | 'roles' | 'allAppsVisible' | 'keyType'>>
): Promise<AscApiKey> {
const spinner = ora(`Creating App Store Connect API Key.`).start();
try {
const context = getRequestContext(authCtx);
const key = await ApiKey.createAsync(context, {
nickname: nickname ?? `[expo] ${new Date().getTime()}`,
allAppsVisible: allAppsVisible ?? true,
roles: roles ?? [UserRole.ADMIN],
keyType: keyType ?? ApiKeyType.PUBLIC_API,
});
const keyP8 = await key.downloadAsync();
if (!keyP8) {
const { nickname, roles } = key.attributes;
const humanReadableKey = `App Store Connect Key '${nickname}' (${
key.id
}) with roles {${roles.join(',')}}`;
if (!key.attributes.canDownload) {
// this case would be unexpected because we just created the key
throw new Error(`${humanReadableKey} is not available for download from Apple.`);
} else if (!key.attributes.isActive) {
throw new Error(`${humanReadableKey} is inactive and could not be downloaded.`);
}
throw new Error(`Failed to download .p8 file of ${humanReadableKey}.`);
}
return {
...getAscApiKeyInfo(key, authCtx),
keyP8,
};
} catch (err: any) {
spinner.fail('Failed to create App Store Connect API Key.');
throw err;
}
}
export async function revokeAscApiKeyAsync(
authCtx: AuthCtx,
keyId: string
): Promise<AscApiKeyInfo> {
const spinner = ora(`Revoking App Store Connect API Key.`).start();
try {
const context = getRequestContext(authCtx);
const apiKey = await ApiKey.infoAsync(context, { id: keyId });
const revokedKey = await apiKey.revokeAsync();
spinner.succeed(`Revoked App Store Connect API Key.`);
return getAscApiKeyInfo(revokedKey, authCtx);
} catch (error) {
Log.error(error);
spinner.fail(`Failed to revoke App Store Connect API Key.`);
throw error;
}
}
export function getAscApiKeyInfo(apiKey: ApiKey, authCtx: AuthCtx): AscApiKeyInfo {
return {
name: apiKey.attributes.nickname,
keyId: apiKey.id,
issuerId: apiKey.attributes.provider?.id,
teamId: authCtx.team.id,
teamName: authCtx.team.name,
roles: apiKey.attributes.roles,
};
}
@@ -1,5 +1,3 @@
import { UserRole } from '@expo/apple-utils';
import { AppleDevice } from '../../graphql/generated';
import Log from '../../log';
import { CredentialSchema } from '../utils/promptForCredentials';
@@ -102,38 +100,6 @@ export const distributionCertificateSchema: CredentialSchema<DistributionCertifi
},
};
export type MinimalAscApiKey = {
keyP8: string;
keyId: string;
issuerId: string;
teamId?: string;
teamName?: string;
roles?: UserRole[];
name?: string;
};
export interface AscApiKeyPath {
keyP8Path: string;
keyId: string;
issuerId: string;
}
export const ascApiKeyMetadataSchema: CredentialSchema<Omit<MinimalAscApiKey, 'keyP8'>> = {
name: 'App Store Connect API Key',
questions: [
{
field: 'keyId',
type: 'string',
question: 'Key ID:',
},
{
field: 'issuerId',
type: 'string',
question: 'Issuer ID:',
},
],
};
export const pushKeySchema: CredentialSchema<PushKey> = {
name: 'Apple Push Notifications service key',
questions: [
+1 -27
View File
@@ -350,16 +350,13 @@ export type Offer = {
trialLength?: Maybe<Scalars['Int']>;
type: OfferType;
features?: Maybe<Array<Maybe<Feature>>>;
prerequisite?: Maybe<OfferPrerequisite>;
};
export enum OfferType {
/** Term subscription */
Subscription = 'SUBSCRIPTION',
/** Advanced Purchase of Paid Resource */
Prepaid = 'PREPAID',
/** Addon, or supplementary subscription */
Addon = 'ADDON'
Prepaid = 'PREPAID'
}
export enum Feature {
@@ -373,12 +370,6 @@ export enum Feature {
OpenSource = 'OPEN_SOURCE'
}
export type OfferPrerequisite = {
__typename?: 'OfferPrerequisite';
type: Scalars['String'];
stripeIds: Array<Scalars['String']>;
};
export type Snack = Project & {
__typename?: 'Snack';
id: Scalars['ID'];
@@ -1388,7 +1379,6 @@ export type UserPermission = {
export type Billing = {
__typename?: 'Billing';
id: Scalars['ID'];
payment?: Maybe<PaymentDetails>;
subscription?: Maybe<SubscriptionDetails>;
/** History of invoices */
@@ -1423,8 +1413,6 @@ export type Address = {
export type SubscriptionDetails = {
__typename?: 'SubscriptionDetails';
id: Scalars['ID'];
planId?: Maybe<Scalars['String']>;
addons: Array<AddonDetails>;
name?: Maybe<Scalars['String']>;
nextInvoice?: Maybe<Scalars['DateTime']>;
cancelledAt?: Maybe<Scalars['DateTime']>;
@@ -1434,13 +1422,6 @@ export type SubscriptionDetails = {
status?: Maybe<Scalars['String']>;
};
export type AddonDetails = {
__typename?: 'AddonDetails';
id: Scalars['ID'];
planId: Scalars['String'];
name: Scalars['String'];
};
export type Charge = {
__typename?: 'Charge';
id: Scalars['ID'];
@@ -2975,18 +2956,11 @@ export type CreateIosSubmissionInput = {
export type IosSubmissionConfigInput = {
appleAppSpecificPasswordId?: Maybe<Scalars['String']>;
appleAppSpecificPassword?: Maybe<Scalars['String']>;
ascApiKey?: Maybe<AscApiKeyInput>;
archiveUrl?: Maybe<Scalars['String']>;
appleIdUsername: Scalars['String'];
ascAppIdentifier: Scalars['String'];
};
export type AscApiKeyInput = {
keyP8: Scalars['String'];
keyIdentifier: Scalars['String'];
issuerIdentifier: Scalars['String'];
};
export type CreateAndroidSubmissionInput = {
appId: Scalars['ID'];
config: AndroidSubmissionConfigInput;
+8 -130
View File
@@ -1,26 +1,22 @@
import { Platform } from '@expo/eas-build-job';
import chalk from 'chalk';
import prompts from 'prompts';
import { URL } from 'url';
import * as uuid from 'uuid';
import { AppPlatform, BuildFragment } from '../graphql/generated';
import { BuildFragment } from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log, { learnMore } from '../log';
import { appPlatformDisplayNames } from '../platform';
import { confirmAsync, promptAsync } from '../prompts';
import { getRecentBuildsForSubmissionAsync } from './utils/builds';
import { getLatestBuildForSubmissionAsync } from './utils/builds';
import { isExistingFileAsync, uploadAppArchiveAsync } from './utils/files';
export const BUILD_LIST_ITEM_COUNT = 4;
export enum ArchiveSourceType {
url,
latest,
path,
buildId,
buildList,
prompt,
}
@@ -50,10 +46,6 @@ interface ArchiveBuildIdSource extends ArchiveSourceBase {
id: string;
}
interface ArchiveBuildListSource extends ArchiveSourceBase {
sourceType: ArchiveSourceType.buildList;
}
interface ArchivePromptSource extends ArchiveSourceBase {
sourceType: ArchiveSourceType.prompt;
}
@@ -69,7 +61,6 @@ export type ArchiveSource =
| ArchiveLatestSource
| ArchivePathSource
| ArchiveBuildIdSource
| ArchiveBuildListSource
| ArchivePromptSource;
export async function getArchiveAsync(source: ArchiveSource): Promise<Archive> {
@@ -89,9 +80,6 @@ export async function getArchiveAsync(source: ArchiveSource): Promise<Archive> {
case ArchiveSourceType.buildId: {
return await handleBuildIdSourceAsync(source);
}
case ArchiveSourceType.buildList: {
return await handleBuildListSourceAsync(source);
}
}
}
@@ -125,7 +113,7 @@ async function handleUrlSourceAsync(source: ArchiveUrlSource): Promise<Archive>
async function handleLatestSourceAsync(source: ArchiveLatestSource): Promise<Archive> {
try {
const [latestBuild] = await getRecentBuildsForSubmissionAsync(
const latestBuild = await getLatestBuildForSubmissionAsync(
toAppPlatform(source.platform),
source.projectId
);
@@ -209,116 +197,6 @@ async function handleBuildIdSourceAsync(source: ArchiveBuildIdSource): Promise<A
}
}
async function handleBuildListSourceAsync(source: ArchiveBuildListSource): Promise<Archive> {
try {
const appPlatform = toAppPlatform(source.platform);
const expiryDate = new Date(); // artifacts expire after 30 days
expiryDate.setDate(expiryDate.getDate() - 30);
const recentBuilds = await getRecentBuildsForSubmissionAsync(appPlatform, source.projectId, {
limit: BUILD_LIST_ITEM_COUNT,
});
if (recentBuilds.length < 1) {
Log.error(
chalk.bold(
`Couldn't find any ${appPlatformDisplayNames[appPlatform]} builds for this project on EAS servers. ` +
"It looks like you haven't run 'eas build' yet."
)
);
return getArchiveAsync({
...source,
sourceType: ArchiveSourceType.prompt,
});
}
if (recentBuilds.every(it => new Date(it.updatedAt) < expiryDate)) {
Log.error(
chalk.bold(
'It looks like all of your build artifacts have expired. ' +
'EAS keeps your build artifacts only for 30 days.'
)
);
return getArchiveAsync({
...source,
sourceType: ArchiveSourceType.prompt,
});
}
const choices = recentBuilds.map(build => formatBuildChoice(build, expiryDate));
choices.push({
title: 'None of the above (select another option)',
value: null,
});
const { selectedBuild } = await promptAsync({
name: 'selectedBuild',
type: 'select',
message: 'Which build would you like to submit?',
choices: choices.map(choice => ({ ...choice, title: `- ${choice.title}` })),
// @ts-expect-error field documented in npm, but not defined in typescript
warn: 'This artifact has expired',
});
if (selectedBuild == null) {
return getArchiveAsync({
...source,
sourceType: ArchiveSourceType.prompt,
});
}
return {
build: selectedBuild,
source,
};
} catch (err) {
Log.error(err);
throw err;
}
}
function formatBuildChoice(build: BuildFragment, expiryDate: Date): prompts.Choice {
const {
id,
platform,
updatedAt,
appVersion,
sdkVersion,
runtimeVersion,
buildProfile,
appBuildVersion,
releaseChannel,
} = build;
const formatValue = (field?: string | null): string =>
field ? chalk.bold(field) : chalk.dim('Unknown');
const buildDate = new Date(updatedAt);
const maybeRuntimeVersion = runtimeVersion ? `Runtime: ${formatValue(runtimeVersion)}` : null;
const maybeSdkVersion = sdkVersion ? `SDK: ${formatValue(sdkVersion)}` : null;
const appBuildVersionString = `${
platform === AppPlatform.Android ? 'Version code' : 'Build number'
}: ${formatValue(appBuildVersion)}`;
const title = [
`ID: ${chalk.dim(id)}, Finished at: ${chalk.bold(buildDate.toLocaleString())}`,
[
`\tApp version: ${formatValue(appVersion)}, ${appBuildVersionString}`,
maybeRuntimeVersion,
maybeSdkVersion,
]
.filter(it => it != null)
.join(', '),
`\tProfile: ${formatValue(buildProfile)}, Release channel: ${formatValue(releaseChannel)}`,
].join('\n');
return {
title,
value: build,
disabled: buildDate < expiryDate,
};
}
async function handlePromptSourceAsync(source: ArchivePromptSource): Promise<Archive> {
const { sourceType: sourceTypeRaw } = await promptAsync({
name: 'sourceType',
@@ -326,8 +204,8 @@ async function handlePromptSourceAsync(source: ArchivePromptSource): Promise<Arc
message: 'What would you like to submit?',
choices: [
{
title: 'Selected build from EAS',
value: ArchiveSourceType.buildList,
title: 'Latest finished build from EAS',
value: ArchiveSourceType.latest,
},
{ title: 'I have a url to the app archive', value: ArchiveSourceType.url },
{
@@ -358,10 +236,10 @@ async function handlePromptSourceAsync(source: ArchivePromptSource): Promise<Arc
path,
});
}
case ArchiveSourceType.buildList: {
case ArchiveSourceType.latest: {
return getArchiveAsync({
...source,
sourceType: ArchiveSourceType.buildList,
sourceType: ArchiveSourceType.latest,
});
}
case ArchiveSourceType.buildId: {
@@ -372,7 +250,7 @@ async function handlePromptSourceAsync(source: ArchivePromptSource): Promise<Arc
id,
});
}
default:
case ArchiveSourceType.prompt:
throw new Error('This should never happen');
}
}
@@ -9,13 +9,8 @@ import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { toAppPlatform } from '../../graphql/types/AppPlatform';
import { confirmAsync, promptAsync } from '../../prompts';
import { uploadAsync } from '../../uploads';
import {
Archive,
ArchiveSourceType,
BUILD_LIST_ITEM_COUNT,
getArchiveAsync,
} from '../ArchiveSource';
import { getRecentBuildsForSubmissionAsync } from '../utils/builds';
import { Archive, ArchiveSourceType, getArchiveAsync } from '../ArchiveSource';
import { getLatestBuildForSubmissionAsync } from '../utils/builds';
jest.mock('fs');
jest.mock('../../log');
@@ -37,7 +32,6 @@ const MOCK_BUILD_FRAGMENT: Partial<BuildFragment> = {
},
appVersion: '1.2.3',
platform: AppPlatform.Android,
updatedAt: Date.now(),
};
const SOURCE_STUB_INPUT = {
@@ -146,7 +140,7 @@ describe(getArchiveAsync, () => {
it('handles latest build source', async () => {
const projectId = uuidv4();
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([MOCK_BUILD_FRAGMENT]);
asMock(getLatestBuildForSubmissionAsync).mockResolvedValueOnce(MOCK_BUILD_FRAGMENT);
const archive = await getArchiveAsync({
...SOURCE_STUB_INPUT,
@@ -154,7 +148,7 @@ describe(getArchiveAsync, () => {
sourceType: ArchiveSourceType.latest,
});
expect(getRecentBuildsForSubmissionAsync).toBeCalledWith(
expect(getLatestBuildForSubmissionAsync).toBeCalledWith(
toAppPlatform(SOURCE_STUB_INPUT.platform),
projectId
);
@@ -162,7 +156,7 @@ describe(getArchiveAsync, () => {
});
it('prompts again if no builds exists when selected latest', async () => {
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([]);
asMock(getLatestBuildForSubmissionAsync).mockResolvedValueOnce(null);
asMock(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
@@ -175,59 +169,6 @@ describe(getArchiveAsync, () => {
assertArchiveResult(archive, ArchiveSourceType.url);
});
it('handles build-list-select source', async () => {
const projectId = uuidv4();
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([MOCK_BUILD_FRAGMENT]);
asMock(promptAsync).mockResolvedValueOnce({ selectedBuild: MOCK_BUILD_FRAGMENT });
const archive = await getArchiveAsync({
...SOURCE_STUB_INPUT,
projectId,
sourceType: ArchiveSourceType.buildList,
});
expect(getRecentBuildsForSubmissionAsync).toBeCalledWith(
toAppPlatform(SOURCE_STUB_INPUT.platform),
projectId,
{ limit: BUILD_LIST_ITEM_COUNT }
);
assertArchiveResult(archive, ArchiveSourceType.buildList);
});
it('prompts again if all builds have expired', async () => {
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([
{
...MOCK_BUILD_FRAGMENT,
updatedAt: new Date(Date.now() - 31 * 24 * 3600 * 1000),
},
]);
asMock(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
const archive = await getArchiveAsync({
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.buildList,
});
assertArchiveResult(archive, ArchiveSourceType.url);
});
it('falls back to prompt if user selected "None of the above"', async () => {
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([MOCK_BUILD_FRAGMENT]);
asMock(promptAsync)
.mockResolvedValueOnce({ selectedBuild: null })
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
const archive = await getArchiveAsync({
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.buildList,
});
assertArchiveResult(archive, ArchiveSourceType.url);
});
it('handles path source', async () => {
const path = '/archive.apk';
await fs.writeFile(path, 'some content');
@@ -15,7 +15,7 @@ import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutatio
import { createTestProject } from '../../../project/__tests__/project-utils';
import { getProjectIdAsync } from '../../../project/projectUtils';
import { createSubmissionContextAsync } from '../../context';
import { getRecentBuildsForSubmissionAsync } from '../../utils/builds';
import { getLatestBuildForSubmissionAsync } from '../../utils/builds';
import AndroidSubmitCommand from '../AndroidSubmitCommand';
jest.mock('fs');
@@ -137,7 +137,7 @@ describe(AndroidSubmitCommand, () => {
it('assigns the build ID to submission', async () => {
const projectId = uuidv4();
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([fakeBuildFragment]);
asMock(getLatestBuildForSubmissionAsync).mockResolvedValueOnce(fakeBuildFragment);
const ctx = await createSubmissionContextAsync({
platform: Platform.ANDROID,
@@ -1,78 +0,0 @@
import fs from 'fs-extra';
import { promptForAscApiKeyAsync } from '../../credentials/ios/actions/AscApiKeyUtils';
import { AscApiKeyPath, MinimalAscApiKey } from '../../credentials/ios/credentials';
import Log from '../../log';
import { isExistingFileAsync } from '../utils/files';
export enum AscApiKeySourceType {
path,
prompt,
}
interface AscApiKeySourceBase {
sourceType: AscApiKeySourceType;
}
interface AscApiKeyPromptSource extends AscApiKeySourceBase {
sourceType: AscApiKeySourceType.prompt;
}
interface AscApiKeyEnvVarSource extends AscApiKeySourceBase {
sourceType: AscApiKeySourceType.path;
path: AscApiKeyPath;
}
export type AscApiKeySource = AscApiKeyEnvVarSource | AscApiKeyPromptSource;
type AscApiKeySummary = {
source: 'local' | 'EAS servers';
path?: string;
keyId: string;
};
export type AscApiKeyResult = {
result: MinimalAscApiKey;
summary: AscApiKeySummary;
};
export async function getAscApiKeyLocallyAsync(source: AscApiKeySource): Promise<AscApiKeyResult> {
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
const { keyP8Path, keyId, issuerId } = ascApiKeyPath;
const keyP8 = await fs.readFile(keyP8Path, 'utf-8');
return {
result: { keyP8, keyId, issuerId },
summary: {
source: 'local',
path: keyP8Path,
keyId,
},
};
}
export async function getAscApiKeyPathAsync(source: AscApiKeySource): Promise<AscApiKeyPath> {
switch (source.sourceType) {
case AscApiKeySourceType.path:
return await handlePathSourceAsync(source);
case AscApiKeySourceType.prompt:
return await handlePromptSourceAsync(source);
}
}
async function handlePathSourceAsync(source: AscApiKeyEnvVarSource): Promise<AscApiKeyPath> {
const { keyP8Path } = source.path;
if (!(await isExistingFileAsync(keyP8Path))) {
Log.warn(`File ${keyP8Path} doesn't exist.`);
return await getAscApiKeyPathAsync({ sourceType: AscApiKeySourceType.prompt });
}
return source.path;
}
async function handlePromptSourceAsync(_source: AscApiKeyPromptSource): Promise<AscApiKeyPath> {
const ascApiKeyPath = await promptForAscApiKeyAsync();
return await getAscApiKeyPathAsync({
sourceType: AscApiKeySourceType.path,
path: ascApiKeyPath,
});
}
@@ -4,7 +4,6 @@ import chalk from 'chalk';
import getenv from 'getenv';
import wrapAnsi from 'wrap-ansi';
import { MissingCredentialsError } from '../../credentials/errors';
import { SubmissionFragment } from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import { promptAsync } from '../../prompts';
@@ -17,7 +16,6 @@ import {
AppSpecificPasswordSource,
AppSpecificPasswordSourceType,
} from './AppSpecificPasswordSource';
import { AscApiKeySource, AscApiKeySourceType } from './AscApiKeySource';
import IosSubmitter, { IosSubmissionOptions } from './IosSubmitter';
export default class IosSubmitCommand {
@@ -30,37 +28,15 @@ export default class IosSubmitCommand {
return await submitter.submitAsync();
}
private async resolveCredentialSubmissionOptionsAsync(): Promise<
| { appSpecificPasswordSource: Result<AppSpecificPasswordSource> }
| { ascApiKeySource: Result<AscApiKeySource> }
> {
// Fall back to app specific password if no ascApiKey defined
const ascApiKeySource = this.resolveAscApiKeySource();
const shouldUseAppSpecificPassword =
!ascApiKeySource.ok && ascApiKeySource.enforceError() instanceof MissingCredentialsError;
if (shouldUseAppSpecificPassword) {
return { appSpecificPasswordSource: this.resolveAppSpecificPasswordSource() };
} else {
return { ascApiKeySource };
}
}
private async resolveSubmissionOptionsAsync(): Promise<IosSubmissionOptions> {
const archiveSource = this.resolveArchiveSource();
const credentialsSource = await this.resolveCredentialSubmissionOptionsAsync();
const maybeAppSpecificPasswordSource =
'appSpecificPasswordSource' in credentialsSource
? credentialsSource.appSpecificPasswordSource
: null;
const maybeAscApiKeySource =
'ascApiKeySource' in credentialsSource ? credentialsSource.ascApiKeySource : null;
const appSpecificPasswordSource = this.resolveAppSpecificPasswordSource();
const ascAppIdentifier = await this.resolveAscAppIdentifierAsync();
const appleIdUsername = await this.resolveAppleIdUsernameAsync();
const errored = [
archiveSource,
...(maybeAppSpecificPasswordSource ? [maybeAppSpecificPasswordSource] : []),
...(maybeAscApiKeySource ? [maybeAscApiKeySource] : []),
appSpecificPasswordSource,
ascAppIdentifier,
appleIdUsername,
].filter(r => !r.ok);
@@ -75,16 +51,7 @@ export default class IosSubmitCommand {
appleIdUsername: appleIdUsername.enforceValue(),
ascAppIdentifier: ascAppIdentifier.enforceValue(),
archiveSource: archiveSource.enforceValue(),
...(maybeAppSpecificPasswordSource
? {
appSpecificPasswordSource: maybeAppSpecificPasswordSource.enforceValue(),
}
: null),
...(maybeAscApiKeySource
? {
ascApiKeySource: maybeAscApiKeySource.enforceValue(),
}
: null),
appSpecificPasswordSource: appSpecificPasswordSource.enforceValue(),
};
}
@@ -105,35 +72,6 @@ export default class IosSubmitCommand {
}
}
private resolveAscApiKeySource(): Result<AscApiKeySource> {
const { ascApiKeyPath, ascApiKeyIssuerId, ascApiKeyId } = this.ctx.profile;
if (ascApiKeyPath && ascApiKeyIssuerId && ascApiKeyId) {
return result({
sourceType: AscApiKeySourceType.path,
path: {
keyP8Path: ascApiKeyPath,
issuerId: ascApiKeyIssuerId,
keyId: ascApiKeyId,
},
});
}
// interpret this to mean the user had some intention of passing in ASC Api key
if (ascApiKeyPath || ascApiKeyIssuerId || ascApiKeyId) {
Log.warn(`ascApiKeyPath, ascApiKeyIssuerId and ascApiKeyId must all be defined in eas.json`);
return result({
sourceType: AscApiKeySourceType.prompt,
});
}
return result(
new MissingCredentialsError(
'Set the ascApiKeyPath, ascApiKeyIssuerId and ascApiKeyId fields in eas.json.'
)
);
}
private resolveArchiveSource(): Result<ArchiveSource> {
try {
return result(resolveArchiveSource(this.ctx, Platform.IOS));
@@ -1,9 +1,7 @@
import { Platform } from '@expo/eas-build-job';
import chalk from 'chalk';
import { IosSubmissionConfigInput, SubmissionFragment } from '../../graphql/generated';
import { SubmissionMutation } from '../../graphql/mutations/SubmissionMutation';
import formatFields from '../../utils/formatFields';
import { Archive, ArchiveSource, getArchiveAsync } from '../ArchiveSource';
import BaseSubmitter, { SubmissionInput } from '../BaseSubmitter';
import {
@@ -15,20 +13,17 @@ import {
AppSpecificPasswordSource,
getAppSpecificPasswordAsync,
} from './AppSpecificPasswordSource';
import { AscApiKeyResult, AscApiKeySource, getAscApiKeyLocallyAsync } from './AscApiKeySource';
export interface IosSubmissionOptions
extends Pick<IosSubmissionConfigInput, 'appleIdUsername' | 'ascAppIdentifier'> {
projectId: string;
archiveSource: ArchiveSource;
appSpecificPasswordSource?: AppSpecificPasswordSource;
ascApiKeySource?: AscApiKeySource;
appSpecificPasswordSource: AppSpecificPasswordSource;
}
interface ResolvedSourceOptions {
archive: Archive;
appSpecificPassword?: string;
ascApiKeyResult?: AscApiKeyResult;
appSpecificPassword: string;
}
export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmissionOptions> {
@@ -65,22 +60,18 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
private async resolveSourceOptionsAsync(): Promise<ResolvedSourceOptions> {
const archive = await getArchiveAsync(this.options.archiveSource);
const maybeAppSpecificPassword = this.options.appSpecificPasswordSource
? await getAppSpecificPasswordAsync(this.options.appSpecificPasswordSource)
: null;
const maybeAppStoreConnectApiKey = this.options.ascApiKeySource
? await getAscApiKeyLocallyAsync(this.options.ascApiKeySource)
: null;
const appSpecificPassword = await getAppSpecificPasswordAsync(
this.options.appSpecificPasswordSource
);
return {
archive,
...(maybeAppSpecificPassword ? { appSpecificPassword: maybeAppSpecificPassword } : null),
...(maybeAppStoreConnectApiKey ? { ascApiKeyResult: maybeAppStoreConnectApiKey } : null),
appSpecificPassword,
};
}
private async formatSubmissionConfigAsync(
options: IosSubmissionOptions,
{ archive, appSpecificPassword, ascApiKeyResult }: ResolvedSourceOptions
{ archive, appSpecificPassword }: ResolvedSourceOptions
): Promise<IosSubmissionConfigInput> {
const { appleIdUsername, ascAppIdentifier } = options;
return {
@@ -88,21 +79,12 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
appleIdUsername,
archiveUrl: archive.url,
appleAppSpecificPassword: appSpecificPassword,
...(ascApiKeyResult?.result
? {
ascApiKey: {
keyP8: ascApiKeyResult?.result.keyP8,
keyIdentifier: ascApiKeyResult?.result.keyId,
issuerIdentifier: ascApiKeyResult?.result.issuerId,
},
}
: null),
};
}
private prepareSummaryData(
options: IosSubmissionOptions,
{ archive, ascApiKeyResult }: ResolvedSourceOptions
{ archive }: ResolvedSourceOptions
): SummaryData {
const { appleIdUsername, ascAppIdentifier, projectId } = options;
@@ -111,9 +93,6 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
ascAppIdentifier,
appleIdUsername,
projectId,
...(ascApiKeyResult
? { formattedAscApiKey: formatServiceAccountSummary(ascApiKeyResult) }
: null),
...formatArchiveSourceSummary(archive),
};
}
@@ -123,7 +102,6 @@ type SummaryData = {
ascAppIdentifier: string;
appleIdUsername: string;
projectId: string;
formattedAscApiKey?: string;
} & ArchiveSourceSummaryFields;
const SummaryHumanReadableKeys: Record<keyof SummaryData, string> = {
@@ -133,36 +111,4 @@ const SummaryHumanReadableKeys: Record<keyof SummaryData, string> = {
archiveUrl: 'Archive URL',
archivePath: 'Archive Path',
formattedBuild: 'Build',
formattedAscApiKey: 'App Store Connect Api Key',
};
function formatServiceAccountSummary({ summary }: AscApiKeyResult): string {
const { source, path, keyId } = summary;
const fields = [
{
label: 'Key Source',
value: source,
},
{
label: 'Key Path',
value: path,
},
{
label: 'Key ID',
value: keyId,
},
];
const filteredFields = fields.filter(({ value }) => value !== undefined && value !== null) as {
label: string;
value: string;
}[];
return (
'\n' +
formatFields(filteredFields, {
labelFormat: label => ` ${chalk.dim(label)}:`,
})
);
}
@@ -1,174 +0,0 @@
import { vol } from 'memfs';
import { asMock } from '../../../__tests__/utils';
import { getCredentialsFromUserAsync } from '../../../credentials/utils/promptForCredentials';
import { promptAsync } from '../../../prompts';
import {
AscApiKeySource,
AscApiKeySourceType,
getAscApiKeyLocallyAsync,
getAscApiKeyPathAsync,
} from '../AscApiKeySource';
jest.mock('fs');
jest.mock('../../../prompts');
jest.mock('../../../credentials/utils/promptForCredentials');
beforeAll(() => {
vol.fromJSON({
'/asc-api-key.p8': 'super secret',
'/project_dir/subdir/asc-api-key.p8': 'super secret',
'/project_dir/another-asc-api-key.p8': 'super secret',
});
});
afterAll(() => {
vol.reset();
});
afterEach(() => {
asMock(promptAsync).mockClear();
});
describe(getAscApiKeyPathAsync, () => {
describe('when source is AscApiKeySourceType.path', () => {
it("prompts for path if the provided file doesn't exist", async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementationOnce(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/doesnt-exist.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
expect(promptAsync).toHaveBeenCalled();
expect(ascApiKeyPath).toEqual({
keyP8Path: '/asc-api-key.p8',
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
});
});
it("doesn't prompt for path if the provided file exists", async () => {
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/asc-api-key.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
await getAscApiKeyPathAsync(source);
expect(promptAsync).not.toHaveBeenCalled();
});
it('returns the provided file path if the file exists', async () => {
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/asc-api-key.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
expect(ascApiKeyPath).toEqual({
issuerId: 'test-issuer-id',
keyId: 'test-key-id',
keyP8Path: '/asc-api-key.p8',
});
});
});
describe('when source is AscApiKeySourceType.prompt', () => {
it('prompts for path', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementationOnce(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.prompt,
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
expect(promptAsync).toHaveBeenCalled();
expect(ascApiKeyPath).toEqual({
keyP8Path: '/asc-api-key.p8',
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
});
});
it('prompts for path until the user provides an existing file', async () => {
asMock(promptAsync)
.mockImplementationOnce(() => ({
keyP8Path: '/doesnt-exist.p8',
}))
.mockImplementationOnce(() => ({
keyP8Path: '/blah.p8',
}))
.mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.prompt,
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
expect(promptAsync).toHaveBeenCalledTimes(3);
expect(ascApiKeyPath).toEqual({
keyP8Path: '/asc-api-key.p8',
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
});
});
});
});
describe(getAscApiKeyLocallyAsync, () => {
it('returns a local Asc Api Key file with a AscApiKeySourceType.path source', async () => {
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/asc-api-key.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
const ascApiKeyResult = await getAscApiKeyLocallyAsync(source);
expect(ascApiKeyResult).toMatchObject({
result: {
keyP8: 'super secret',
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
},
summary: {
source: 'local',
path: '/asc-api-key.p8',
keyId: 'test-key-id',
},
});
});
it('returns a local Asc Api Key file with a AscApiKeySourceType.prompt source', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementationOnce(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.prompt,
};
const serviceAccountResult = await getAscApiKeyLocallyAsync(source);
expect(serviceAccountResult).toMatchObject({
result: {
keyP8: 'super secret',
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
},
summary: {
source: 'local',
path: '/asc-api-key.p8',
keyId: 'test-key-id',
},
});
});
});
+7 -8
View File
@@ -1,17 +1,16 @@
import { AppPlatform, BuildFragment, BuildStatus, DistributionType } from '../../graphql/generated';
import { AppPlatform, BuildFragment, BuildStatus } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
export async function getRecentBuildsForSubmissionAsync(
export async function getLatestBuildForSubmissionAsync(
platform: AppPlatform,
appId: string,
{ limit = 1 }: { limit?: number } = {}
): Promise<BuildFragment[]> {
return await BuildQuery.allForAppAsync(appId, {
limit,
appId: string
): Promise<BuildFragment | null> {
const [build] = await BuildQuery.allForAppAsync(appId, {
limit: 1,
filter: {
platform,
distribution: DistributionType.Store,
status: BuildStatus.Finished,
},
});
return build;
}
+4 -2
View File
@@ -2,13 +2,15 @@
"compilerOptions": {
"declaration": true,
"importHelpers": true,
"module": "commonjs",
"esModuleInterop": true,
"module": "ES2020",
"noImplicitReturns": true,
"strict": true,
"target": "ES2019",
"outDir": "build",
"rootDir": "src",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"typeRoots": [
"../../node_modules/@types",
"../../ts-declarations",
+6 -3
View File
@@ -1,7 +1,8 @@
{
"name": "@expo/eas-json",
"type": "module",
"description": "A library for interacting with the eas.json",
"version": "0.32.0",
"version": "0.31.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
@@ -21,14 +22,16 @@
},
"homepage": "https://github.com/expo/eas-cli",
"license": "MIT",
"main": "build/index.js",
"exports": "./build/index.js",
"types": "./build/index.d.ts",
"repository": "expo/eas-cli",
"scripts": {
"build": "tsc --project tsconfig.build.json",
"watch": "yarn build --watch --preserveWatchOutput",
"typecheck": "tsc",
"prepack": "rm -rf build && yarn build",
"test": "jest"
"test": "jest",
"clean": "rm -rf build node_modules"
},
"publishConfig": {
"access": "public"
-3
View File
@@ -90,9 +90,6 @@ export const AndroidSubmitProfileSchema = Joi.object({
});
export const IosSubmitProfileSchema = Joi.object({
ascApiKeyPath: Joi.string(),
ascApiKeyId: Joi.string(),
ascApiKeyIssuerId: Joi.string(),
appleId: Joi.string(),
ascAppId: Joi.string(),
appleTeamId: Joi.string(),
+1 -8
View File
@@ -26,9 +26,6 @@ export const AndroidSubmitProfileFieldsToEvaluate: (keyof AndroidSubmitProfile)[
];
export interface IosSubmitProfile {
ascApiKeyPath?: string;
ascApiKeyIssuerId?: string;
ascApiKeyId?: string;
appleId?: string;
ascAppId?: string;
appleTeamId?: string;
@@ -38,11 +35,7 @@ export interface IosSubmitProfile {
appName?: string;
}
export const IosSubmitProfileFieldsToEvaluate: (keyof IosSubmitProfile)[] = [
'ascApiKeyPath',
'ascApiKeyIssuerId',
'ascApiKeyId',
];
export const IosSubmitProfileFieldsToEvaluate: (keyof IosSubmitProfile)[] = [];
export type SubmitProfile<TPlatform extends Platform = Platform> =
TPlatform extends Platform.ANDROID
@@ -93,9 +93,6 @@ test('ios config with all required values', async () => {
appleId: '[email protected]',
ascAppId: '1223423523',
appleTeamId: 'QWERTY',
ascApiKeyPath: './path-ABCD.p8',
ascApiKeyIssuerId: 'abc-123-def-456',
ascApiKeyId: 'ABCD',
},
},
},
@@ -108,48 +105,6 @@ test('ios config with all required values', async () => {
appleId: '[email protected]',
appleTeamId: 'QWERTY',
ascAppId: '1223423523',
ascApiKeyPath: './path-ABCD.p8',
ascApiKeyIssuerId: 'abc-123-def-456',
ascApiKeyId: 'ABCD',
language: 'en-US',
});
});
test('ios config with ascApiKey fields set to env var', async () => {
await fs.writeJson('/project/eas.json', {
submit: {
release: {
ios: {
appleId: '[email protected]',
ascAppId: '1223423523',
appleTeamId: 'QWERTY',
ascApiKeyPath: '$ASC_API_KEY_PATH',
ascApiKeyIssuerId: '$ASC_API_KEY_ISSUER_ID',
ascApiKeyId: '$ASC_API_KEY_ID',
},
},
},
});
try {
process.env.ASC_API_KEY_PATH = './path-ABCD.p8';
process.env.ASC_API_KEY_ISSUER_ID = 'abc-123-def-456';
process.env.ASC_API_KEY_ID = 'ABCD';
const reader = new EasJsonReader('/project');
const iosProfile = await reader.readSubmitProfileAsync(Platform.IOS, 'release');
expect(iosProfile).toEqual({
appleId: '[email protected]',
ascAppId: '1223423523',
appleTeamId: 'QWERTY',
ascApiKeyPath: './path-ABCD.p8',
ascApiKeyIssuerId: 'abc-123-def-456',
ascApiKeyId: 'ABCD',
language: 'en-US',
});
} finally {
process.env.ASC_API_KEY_PATH = undefined;
process.env.ASC_API_KEY_ISSUER_ID = undefined;
process.env.ASC_API_KEY_ID = undefined;
}
});
+4 -2
View File
@@ -2,13 +2,15 @@
"compilerOptions": {
"declaration": true,
"importHelpers": true,
"module": "commonjs",
"esModuleInterop": true,
"module": "ES2020",
"noImplicitReturns": true,
"strict": true,
"target": "ES2019",
"outDir": "build",
"rootDir": "src",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"typeRoots": [
"../../node_modules/@types",
"../../ts-declarations",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "scripts",
"private": true,
"type": "module",
"version": "0.31.0",
"scripts": {
"changelog-entry": "ts-node src/changelogEntry.ts",
+4 -2
View File
@@ -2,13 +2,15 @@
"compilerOptions": {
"declaration": true,
"importHelpers": true,
"module": "commonjs",
"esModuleInterop": true,
"module": "ES2020",
"noImplicitReturns": true,
"strict": true,
"target": "ES2019",
"outDir": "build",
"rootDir": "src",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"typeRoots": [
"../ts-declarations",
"./node_modules/@types"