mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 17:50:32 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31020047ca | ||
|
|
26778ec52b | ||
|
|
b27590f4f5 | ||
|
|
ea2af5af04 | ||
|
|
3f1a7a0da0 | ||
|
|
8184a13bf3 | ||
|
|
2f7be56bef | ||
|
|
5c166bb3b2 | ||
|
|
4e76526843 | ||
|
|
0e6fae0632 | ||
|
|
62a8fd1b02 | ||
|
|
5a8e252790 | ||
|
|
78613cd7c0 | ||
|
|
8f6f7f7e1c | ||
|
|
f46ad6b898 | ||
|
|
f153b08a1e | ||
|
|
6b57f00d7d | ||
|
|
de65ab702b | ||
|
|
a87b735832 | ||
|
|
afc6022529 | ||
|
|
2d3bb21f60 | ||
|
|
c26f478b45 | ||
|
|
f958a0db3e | ||
|
|
6df1130140 | ||
|
|
260a771070 | ||
|
|
6659ed97ca | ||
|
|
4eae49fe58 | ||
|
|
41fef5c0e4 | ||
|
|
8c4b45a439 | ||
|
|
e35b5e78d2 | ||
|
|
b04c1a2c20 | ||
|
|
25d75b6f94 | ||
|
|
a2adacf5a3 | ||
|
|
ca718d8da9 | ||
|
|
3e7b849ad1 | ||
|
|
2ec646659d | ||
|
|
c33e3d8a4c | ||
|
|
d65c180746 | ||
|
|
754af79e0c | ||
|
|
3733cc4cf8 | ||
|
|
9e5c1740b4 |
No files matched your search
@@ -1,11 +1,6 @@
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
// Only use this when running tests
|
||||
env: {
|
||||
test: {
|
||||
presets: ['@expo/babel-preset-cli'],
|
||||
},
|
||||
},
|
||||
presets: ['@expo/babel-preset-cli'],
|
||||
};
|
||||
};
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.1.1"
|
||||
"version": "0.2.1"
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
"@types/jest": "^26.0.13",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-config-universe": "^4.0.0",
|
||||
"eslint-plugin-graphql": "^4.0.0",
|
||||
"jest": "^26.4.2",
|
||||
"jest-watch-typeahead": "^0.6.1",
|
||||
"lerna": "^3.22.1",
|
||||
|
||||
@@ -1,9 +1,37 @@
|
||||
module.exports = {
|
||||
rules: {
|
||||
'import/no-cycle': 'error',
|
||||
'import/no-extraneous-dependencies': ['error', { devDependencies: [
|
||||
'**/__tests__/**/*',
|
||||
'**/__mocks__/**/*'
|
||||
] }],
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{ devDependencies: ['**/__tests__/**/*', '**/__mocks__/**/*'] },
|
||||
],
|
||||
'graphql/template-strings': [
|
||||
'error',
|
||||
{
|
||||
env: 'apollo',
|
||||
schemaJson: require('./graphql.schema.json'),
|
||||
},
|
||||
],
|
||||
'graphql/named-operations': [
|
||||
'error',
|
||||
{
|
||||
schemaJson: require('./graphql.schema.json'),
|
||||
},
|
||||
],
|
||||
'graphql/required-fields': [
|
||||
'error',
|
||||
{
|
||||
env: 'apollo',
|
||||
schemaJson: require('./graphql.schema.json'),
|
||||
requiredFields: ['id'],
|
||||
},
|
||||
],
|
||||
'graphql/capitalized-type-name': [
|
||||
'error',
|
||||
{
|
||||
schemaJson: require('./graphql.schema.json'),
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: ['graphql'],
|
||||
};
|
||||
+13
-13
@@ -21,7 +21,7 @@ $ npm install -g eas-cli
|
||||
$ eas COMMAND
|
||||
running command...
|
||||
$ eas (-v|--version|version)
|
||||
eas-cli/0.1.1 darwin-x64 node-v14.15.0
|
||||
eas-cli/0.2.1 darwin-x64 node-v12.18.4
|
||||
$ eas --help [COMMAND]
|
||||
USAGE
|
||||
$ eas COMMAND
|
||||
@@ -58,7 +58,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/account/login.ts)_
|
||||
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.2.1/build/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -72,7 +72,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/account/logout.ts)_
|
||||
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.2.1/build/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -86,7 +86,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/account/view.ts)_
|
||||
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.2.1/build/commands/account/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -105,7 +105,7 @@ OPTIONS
|
||||
--wait Wait for build(s) to complete
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/build/index.ts)_
|
||||
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.2.1/build/commands/build/index.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -120,7 +120,7 @@ OPTIONS
|
||||
--allow-experimental Enable experimental configuration steps.
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/build/configure.ts)_
|
||||
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.2.1/build/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -136,7 +136,7 @@ OPTIONS
|
||||
--status=(in-queue|in-progress|errored|finished)
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/build/list.ts)_
|
||||
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.2.1/build/commands/build/list.ts)_
|
||||
|
||||
## `eas build:view [BUILDID]`
|
||||
|
||||
@@ -147,7 +147,7 @@ USAGE
|
||||
$ eas build:view [BUILDID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/build/view.ts)_
|
||||
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.2.1/build/commands/build/view.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -158,7 +158,7 @@ USAGE
|
||||
$ eas credentials
|
||||
```
|
||||
|
||||
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/credentials.ts)_
|
||||
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.2.1/build/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -169,7 +169,7 @@ USAGE
|
||||
$ eas device:create
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/device/create.ts)_
|
||||
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.2.1/build/commands/device/create.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -183,7 +183,7 @@ OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/device/list.ts)_
|
||||
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.2.1/build/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -194,7 +194,7 @@ USAGE
|
||||
$ eas device:view [UDID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/device/view.ts)_
|
||||
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.2.1/build/commands/device/view.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -291,5 +291,5 @@ EXAMPLES
|
||||
and provide its App ID
|
||||
```
|
||||
|
||||
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/submit.ts)_
|
||||
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.2.1/build/commands/submit.ts)_
|
||||
<!-- commandsstop -->
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = async () => {
|
||||
process.env.TZ = 'UTC';
|
||||
};
|
||||
@@ -2,11 +2,17 @@ overwrite: true
|
||||
schema: 'https://staging.exp.host/--/graphql'
|
||||
documents:
|
||||
- 'src/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
|
||||
generates:
|
||||
src/graphql/generated.ts:
|
||||
plugins:
|
||||
- 'typescript'
|
||||
- 'typescript-operations'
|
||||
config:
|
||||
dedupeOperationSuffix: true
|
||||
hooks:
|
||||
afterOneFileWrite:
|
||||
- ./annotate-graphql-codegen.sh
|
||||
./graphql.schema.json:
|
||||
plugins:
|
||||
- 'introspection'
|
||||
File diff suppressed because it is too large.
Load diff
@@ -2,4 +2,5 @@ module.exports = {
|
||||
...require('../../jest/jest.config.js'),
|
||||
rootDir: __dirname,
|
||||
roots: ['src', '__mocks__'],
|
||||
globalSetup: './global-setup.ts',
|
||||
};
|
||||
@@ -1,26 +1,27 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "0.0.0-alpha.16",
|
||||
"@expo/apple-utils": "0.0.0-alpha.17",
|
||||
"@expo/config": "~3.3.19",
|
||||
"@expo/config-plugins": "~1.0.9",
|
||||
"@expo/eas-build-job": "0.1.4",
|
||||
"@expo/eas-json": "^0.1.0-alpha.20",
|
||||
"@expo/config-plugins": "^1.0.14",
|
||||
"@expo/eas-build-job": "^0.2.0",
|
||||
"@expo/eas-json": "^0.2.0",
|
||||
"@expo/json-file": "^8.2.24",
|
||||
"@expo/plist": "^0.0.10",
|
||||
"@expo/plist": "^0.0.11",
|
||||
"@expo/plugin-warn-if-update-available": "^1.7.0",
|
||||
"@expo/results": "^1.0.0",
|
||||
"@expo/spawn-async": "^1.5.0",
|
||||
"@hapi/joi": "^17.1.1",
|
||||
"@oclif/command": "^1",
|
||||
"@oclif/config": "^1",
|
||||
"@oclif/errors": "^1.3.3",
|
||||
"@oclif/plugin-help": "^3",
|
||||
"@urql/core": "^1.13.0",
|
||||
"chalk": "^4.1.0",
|
||||
@@ -39,6 +40,7 @@
|
||||
"keychain": "^1.3.0",
|
||||
"lodash": "^4.17.20",
|
||||
"md5-file": "^5.0.0",
|
||||
"mime": "^2.4.7",
|
||||
"minimatch": "^3.0.4",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-forge": "^0.10.0",
|
||||
@@ -51,10 +53,13 @@
|
||||
"strip-ansi": "^6.0.0",
|
||||
"tar": "^6.0.5",
|
||||
"tempy": "^0.7.0",
|
||||
"terminal-link": "^2.1.1",
|
||||
"timeago.js": "^4.0.2",
|
||||
"tslib": "^1",
|
||||
"turndown": "^7.0.0",
|
||||
"untildify": "^4.0.0",
|
||||
"uuid": "^8.3.0",
|
||||
"wordwrap": "^1.0.0"
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
@@ -62,6 +67,7 @@
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@expo/babel-preset-cli": "^0.2.17",
|
||||
"@graphql-codegen/cli": "^1.19.2",
|
||||
"@graphql-codegen/introspection": "^1.18.0",
|
||||
"@graphql-codegen/typescript": "^1.17.11",
|
||||
"@graphql-codegen/typescript-operations": "^1.17.10",
|
||||
"@types/color-string": "^1.5.0",
|
||||
@@ -70,6 +76,7 @@
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/getenv": "^1.0.0",
|
||||
"@types/lodash": "^4.14.161",
|
||||
"@types/mime": "^2.0.3",
|
||||
"@types/node": "^12",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/node-forge": "^0.9.5",
|
||||
@@ -78,11 +85,12 @@
|
||||
"@types/tar": "^4.0.3",
|
||||
"@types/tough-cookie": "^4.0.0",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/wordwrap": "^1.0.0",
|
||||
"@types/wrap-ansi": "^3.0.0",
|
||||
"@vercel/ncc": "^0.23.0",
|
||||
"axios": "^0.21.0",
|
||||
"babel-jest": "^26.3.0",
|
||||
"memfs": "^3.2.0",
|
||||
"mockdate": "^3.0.2",
|
||||
"ts-node": "^8",
|
||||
"typescript": "^3.9.7",
|
||||
"wonka": "^4.0.14"
|
||||
|
||||
@@ -130,7 +130,7 @@ describe(ensureCredentialsAsync, () => {
|
||||
throw new Error('ensureCredentialsAsync should throw an Error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
'Contents of your local credentials.json for Android are not the same as credentials on Expo servers'
|
||||
'Contents of your local credentials.json for Android are not the same as credentials on EAS servers'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -112,8 +112,9 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.code === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
log.error('EAS Build API changed, upgrade to latest expo-cli');
|
||||
const body = error?.response?.body;
|
||||
if (body && JSON.parse(body)?.errors?.[0]?.code === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
log.error('EAS Build API has changed, please upgrade to the latest eas-cli');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -34,11 +34,11 @@ async function ensureCredentialsAutoAsync(
|
||||
if (!(await provider.isLocalSyncedAsync())) {
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
`Contents of your local credentials.json for ${platform} are not the same as credentials on Expo servers. To use the desired credentials, set the "builds.${platform}.{profile}.credentialsSource" field in the credentials.json file to one of the following: "local", "remote".`
|
||||
`Contents of your local credentials.json for ${platform} are not the same as credentials on EAS servers. To use the desired credentials, set the "builds.${platform}.{profile}.credentialsSource" field in the credentials.json file to one of the following: "local", "remote".`
|
||||
);
|
||||
} else {
|
||||
log(
|
||||
`Contents of your local credentials.json for ${platform} are not the same as credentials on Expo servers`
|
||||
`Contents of your local credentials.json for ${platform} are not the same as credentials on EAS servers`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ async function ensureCredentialsAutoAsync(
|
||||
message: 'Which credentials do you want to use for this build?',
|
||||
choices: [
|
||||
{ title: 'Local credentials.json', value: CredentialsSource.LOCAL },
|
||||
{ title: 'Credentials stored on Expo servers.', value: CredentialsSource.REMOTE },
|
||||
{ title: 'Credentials stored on EAS servers.', value: CredentialsSource.REMOTE },
|
||||
],
|
||||
});
|
||||
return select;
|
||||
|
||||
@@ -38,15 +38,22 @@ export async function prepareIosBuildAsync(
|
||||
}
|
||||
|
||||
let iosNativeProjectScheme: string | undefined;
|
||||
let iosApplicationNativeTarget: string | undefined;
|
||||
if (buildCtx.buildProfile.workflow === Workflow.Generic) {
|
||||
iosNativeProjectScheme = buildCtx.buildProfile.scheme ?? (await resolveSchemeAsync(buildCtx));
|
||||
iosApplicationNativeTarget = await IOSConfig.Target.getApplicationTargetForSchemeAsync(
|
||||
buildCtx.commandCtx.projectDir,
|
||||
iosNativeProjectScheme
|
||||
);
|
||||
}
|
||||
|
||||
await ensureBundleIdentifierIsValidAsync(commandCtx.projectDir);
|
||||
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx: buildCtx,
|
||||
projectConfiguration: {
|
||||
iosNativeProjectScheme,
|
||||
iosApplicationNativeTarget,
|
||||
},
|
||||
ensureCredentialsAsync: ensureIosCredentialsAsync,
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
|
||||
@@ -7,10 +7,15 @@ import {
|
||||
sanitizeJob,
|
||||
} from '@expo/eas-build-job';
|
||||
import { iOSGenericBuildProfile, iOSManagedBuildProfile } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import path from 'path';
|
||||
|
||||
import { readSecretEnvsAsync } from '../../credentials/credentialsJson/read';
|
||||
import { IosCredentials } from '../../credentials/ios/IosCredentialsProvider';
|
||||
import {
|
||||
IosCredentials,
|
||||
IosTargetCredentials,
|
||||
isCredentialsMap,
|
||||
readSecretEnvsAsync,
|
||||
} from '../../credentials/credentialsJson/read';
|
||||
import { getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { gitRootDirectoryAsync } from '../../utils/git';
|
||||
import { BuildContext } from '../context';
|
||||
@@ -21,6 +26,7 @@ interface JobData {
|
||||
credentials?: IosCredentials;
|
||||
projectConfiguration: {
|
||||
iosNativeProjectScheme?: string;
|
||||
iosApplicationNativeTarget?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,43 +50,56 @@ interface CommonJobProperties {
|
||||
projectArchive: ArchiveSource;
|
||||
releaseChannel: string;
|
||||
secrets: {
|
||||
buildCredentials?: {
|
||||
provisioningProfileBase64: string;
|
||||
distributionCertificate: {
|
||||
dataBase64: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
buildCredentials: iOS.BuildCredentials;
|
||||
secretEnvs?: Record<string, string>;
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareJobCommonAsync(
|
||||
ctx: BuildContext<Platform.iOS>,
|
||||
jobData: JobData
|
||||
{
|
||||
archiveBucketKey,
|
||||
credentials,
|
||||
targetName,
|
||||
}: { archiveBucketKey: string; credentials?: IosCredentials; targetName?: string }
|
||||
): Promise<Partial<CommonJobProperties>> {
|
||||
const secretEnvs = await readSecretEnvsAsync(ctx.commandCtx.projectDir);
|
||||
const buildCredentials = jobData.credentials
|
||||
? {
|
||||
buildCredentials: {
|
||||
provisioningProfileBase64: jobData.credentials.provisioningProfile,
|
||||
distributionCertificate: {
|
||||
dataBase64: jobData.credentials.distributionCertificate.certP12,
|
||||
password: jobData.credentials.distributionCertificate.certPassword,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
let buildCredentials: CommonJobProperties['secrets']['buildCredentials'] = {};
|
||||
if (credentials && isCredentialsMap(credentials)) {
|
||||
const targets = Object.keys(credentials);
|
||||
for (const target of targets) {
|
||||
buildCredentials[target] = prepareTargetCredentials(credentials[target]);
|
||||
}
|
||||
} else if (credentials) {
|
||||
// targetName
|
||||
// - for managed projects: sanitized .name from the app config
|
||||
// - for generic projects: name of the main application target
|
||||
assert(targetName, 'target name should be defined');
|
||||
buildCredentials = {
|
||||
[targetName]: prepareTargetCredentials(credentials),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
platform: Platform.iOS,
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.S3,
|
||||
bucketKey: jobData.archiveBucketKey,
|
||||
bucketKey: archiveBucketKey,
|
||||
},
|
||||
secrets: {
|
||||
...(secretEnvs ? { secretEnvs } : {}),
|
||||
...buildCredentials,
|
||||
buildCredentials,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function prepareTargetCredentials(targetCredentials: IosTargetCredentials): iOS.TargetCredentials {
|
||||
return {
|
||||
provisioningProfileBase64: targetCredentials.provisioningProfile,
|
||||
distributionCertificate: {
|
||||
dataBase64: targetCredentials.distributionCertificate.certP12,
|
||||
password: targetCredentials.distributionCertificate.certPassword,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -92,7 +111,11 @@ async function prepareGenericJobAsync(
|
||||
): Promise<Partial<iOS.GenericJob>> {
|
||||
const projectRootDirectory = path.relative(await gitRootDirectoryAsync(), process.cwd()) || '.';
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, jobData)),
|
||||
...(await prepareJobCommonAsync(ctx, {
|
||||
archiveBucketKey: jobData.archiveBucketKey,
|
||||
credentials: jobData.credentials,
|
||||
targetName: jobData.projectConfiguration.iosApplicationNativeTarget,
|
||||
})),
|
||||
type: Workflow.Generic,
|
||||
scheme: jobData.projectConfiguration.iosNativeProjectScheme,
|
||||
artifactPath: buildProfile.artifactPath,
|
||||
@@ -108,11 +131,25 @@ async function prepareManagedJobAsync(
|
||||
): Promise<Partial<iOS.ManagedJob>> {
|
||||
const projectRootDirectory = path.relative(await gitRootDirectoryAsync(), process.cwd()) || '.';
|
||||
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.projectDir);
|
||||
const targetName = sanitizedName(ctx.commandCtx.exp.name);
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, jobData)),
|
||||
...(await prepareJobCommonAsync(ctx, {
|
||||
archiveBucketKey: jobData.archiveBucketKey,
|
||||
credentials: jobData.credentials,
|
||||
targetName,
|
||||
})),
|
||||
type: Workflow.Managed,
|
||||
username: accountName,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
projectRootDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
// copy-pasted from expo-cli/packages/xdl/src/Exp.ts
|
||||
// it's used in eject
|
||||
function sanitizedName(name: string) {
|
||||
return name
|
||||
.replace(/[\W_]+/g, '')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '');
|
||||
}
|
||||
@@ -57,7 +57,7 @@ export type BuildMetadata = {
|
||||
|
||||
/**
|
||||
* Credentials source
|
||||
* Credentials could be obtained either from credential.json or Expo servers.
|
||||
* Credentials could be obtained either from credential.json or EAS servers.
|
||||
*/
|
||||
credentialsSource?: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ export default function formatBuild(build: Build, { accountName }: Options) {
|
||||
label: 'Status',
|
||||
get value() {
|
||||
switch (build.status) {
|
||||
case BuildStatus.IN_QUEUE:
|
||||
return chalk.blue('in queue');
|
||||
case BuildStatus.IN_PROGRESS:
|
||||
return chalk.blue('in progress');
|
||||
case BuildStatus.FINISHED:
|
||||
@@ -38,7 +40,7 @@ export default function formatBuild(build: Build, { accountName }: Options) {
|
||||
{
|
||||
label: 'Artifact',
|
||||
get value() {
|
||||
if (build.status === BuildStatus.IN_PROGRESS) {
|
||||
if (build.status === BuildStatus.IN_QUEUE || build.status === BuildStatus.IN_PROGRESS) {
|
||||
return '<in progress>';
|
||||
}
|
||||
|
||||
@@ -55,7 +57,7 @@ export default function formatBuild(build: Build, { accountName }: Options) {
|
||||
{
|
||||
label: 'Finished at',
|
||||
value:
|
||||
build.status === BuildStatus.IN_PROGRESS
|
||||
build.status === BuildStatus.IN_QUEUE || build.status === BuildStatus.IN_PROGRESS
|
||||
? '<in progress>'
|
||||
: new Date(build.updatedAt).toLocaleString(),
|
||||
},
|
||||
|
||||
@@ -74,13 +74,13 @@ export function printDeprecationWarnings(deprecationInfo?: DeprecationInfo): voi
|
||||
return;
|
||||
}
|
||||
if (deprecationInfo.type === 'internal') {
|
||||
log.warn('This command is using API that soon will be deprecated, please update expo-cli.');
|
||||
log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
|
||||
log.warn("Changes won't affect your project config.");
|
||||
log.warn(deprecationInfo.message);
|
||||
} else if (deprecationInfo.type === 'user-facing') {
|
||||
log.warn('This command is using API that soon will be deprecated, please update expo-cli.');
|
||||
log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
|
||||
log.warn(
|
||||
'There might be some changes necessary to your project config, latest expo-cli will provide more specific error messages.'
|
||||
'There might be some changes necessary to your project config, latest eas-cli will provide more specific error messages.'
|
||||
);
|
||||
log.warn(deprecationInfo.message);
|
||||
} else {
|
||||
|
||||
@@ -3,6 +3,7 @@ import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import ora from 'ora';
|
||||
import path from 'path';
|
||||
import tar from 'tar';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import log from '../../log';
|
||||
@@ -99,6 +100,7 @@ async function makeProjectTarballAsync(): Promise<{ path: string; size: number }
|
||||
const spinner = ora('Compressing project files');
|
||||
|
||||
await fs.mkdirp(getTmpDirectory());
|
||||
const shallowClonePath = path.join(getTmpDirectory(), `${uuidv4()}-shallow-clone`);
|
||||
const tarPath = path.join(getTmpDirectory(), `${uuidv4()}.tar.gz`);
|
||||
|
||||
// If the compression takes longer then a second, show the spinner.
|
||||
@@ -116,14 +118,22 @@ async function makeProjectTarballAsync(): Promise<{ path: string; size: number }
|
||||
startTimer(compressTimerLabel);
|
||||
|
||||
try {
|
||||
await spawnAsync(
|
||||
'git',
|
||||
['archive', '--format=tar.gz', '--prefix', 'project/', '-o', tarPath, 'HEAD'],
|
||||
{ cwd: await gitRootDirectoryAsync() }
|
||||
);
|
||||
await spawnAsync('git', [
|
||||
'clone',
|
||||
'--local',
|
||||
'--no-hardlinks',
|
||||
'--depth',
|
||||
'1',
|
||||
`file://${await gitRootDirectoryAsync()}`,
|
||||
shallowClonePath,
|
||||
]);
|
||||
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
|
||||
'.',
|
||||
]);
|
||||
} finally {
|
||||
// Stop the timer
|
||||
clearTimeout(timer);
|
||||
await fs.remove(shallowClonePath);
|
||||
|
||||
const duration = endTimer(compressTimerLabel);
|
||||
if (spinner.isSpinning) {
|
||||
|
||||
@@ -4,19 +4,28 @@ import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { apiClient } from '../../api';
|
||||
import { configureAsync } from '../../build/configure';
|
||||
import { createCommandContextAsync } from '../../build/context';
|
||||
import { buildAsync } from '../../build/create';
|
||||
import { AnalyticsEvent, RequestedPlatform } from '../../build/types';
|
||||
import { AnalyticsEvent, Build as BuildType, RequestedPlatform } from '../../build/types';
|
||||
import Analytics from '../../build/utils/analytics';
|
||||
import formatBuild from '../../build/utils/formatBuild';
|
||||
import { isGitStatusCleanAsync } from '../../build/utils/repository';
|
||||
import { learnMore } from '../../log';
|
||||
import { AppPlatform } from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import log, { learnMore } from '../../log';
|
||||
import {
|
||||
isEasEnabledForProjectAsync,
|
||||
warnEasUnavailable,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { Actor } from '../../user/User';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
export default class Build extends Command {
|
||||
@@ -48,7 +57,7 @@ export default class Build extends Command {
|
||||
|
||||
async run(): Promise<void> {
|
||||
const { flags } = this.parse(Build);
|
||||
await ensureLoggedInAsync();
|
||||
const user = await ensureLoggedInAsync();
|
||||
|
||||
const nonInteractive = flags['non-interactive'];
|
||||
if (!flags.platform && nonInteractive) {
|
||||
@@ -66,6 +75,17 @@ export default class Build extends Command {
|
||||
return;
|
||||
}
|
||||
|
||||
const accountName = await getProjectAccountNameAsync(projectDir);
|
||||
const accountHasPendingBuilds = await ensureNoPendingBuildsExistAsync({
|
||||
accountName,
|
||||
platform,
|
||||
projectId,
|
||||
user,
|
||||
});
|
||||
if (accountHasPendingBuilds) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureProjectConfiguredAsync(projectDir);
|
||||
|
||||
const trackingCtx = {
|
||||
@@ -89,6 +109,64 @@ export default class Build extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureNoPendingBuildsExistAsync({
|
||||
user,
|
||||
platform,
|
||||
accountName,
|
||||
projectId,
|
||||
}: {
|
||||
user: Actor;
|
||||
platform: RequestedPlatform;
|
||||
accountName: string;
|
||||
projectId: string;
|
||||
}): Promise<boolean> {
|
||||
// allow expo admins to run as many builds as they wish
|
||||
if (user.isExpoAdmin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const appPlatforms = toAppPlatforms(platform);
|
||||
const maybePendingBuilds = await Promise.all(
|
||||
appPlatforms.map(appPlatform => BuildQuery.getPendingBuildIdAsync(accountName, appPlatform))
|
||||
);
|
||||
const pendingBuilds = maybePendingBuilds.filter(i => i !== null);
|
||||
if (pendingBuilds.length > 0) {
|
||||
log.newLine();
|
||||
log.error(
|
||||
'Your other builds are still pending. Wait for them to complete before running this command again.'
|
||||
);
|
||||
log.newLine();
|
||||
const results = await Promise.all(
|
||||
pendingBuilds.map(pendingBuild => {
|
||||
return apiClient.get<{ data: BuildType }>(
|
||||
`projects/${projectId}/builds/${pendingBuild?.id}`,
|
||||
{
|
||||
responseType: 'json',
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
for (const result of results) {
|
||||
log(formatBuild(result.body.data, { accountName }));
|
||||
}
|
||||
|
||||
process.exitCode = 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function toAppPlatforms(requestedPlatform: RequestedPlatform): AppPlatform[] {
|
||||
if (requestedPlatform === RequestedPlatform.All) {
|
||||
return [AppPlatform.Android, AppPlatform.Ios];
|
||||
} else if (requestedPlatform === RequestedPlatform.Android) {
|
||||
return [AppPlatform.Android];
|
||||
} else {
|
||||
return [AppPlatform.Ios];
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForPlatformAsync(): Promise<RequestedPlatform> {
|
||||
const { platform } = await promptAsync({
|
||||
type: 'select',
|
||||
@@ -117,7 +195,7 @@ async function ensureProjectConfiguredAsync(projectDir: string): Promise<void> {
|
||||
return;
|
||||
}
|
||||
const confirm = await confirmAsync({
|
||||
message: "This app isn't setup for building with EAS. Set it up now?",
|
||||
message: 'This app is not set up for building with EAS. Set it up now?',
|
||||
});
|
||||
if (confirm) {
|
||||
await configureAsync({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Command } from '@oclif/command';
|
||||
|
||||
import { runStandaloneCredentialsManagerAsync } from '../credentials/CredentialsManager';
|
||||
import { CredentialsManager } from '../credentials/CredentialsManager';
|
||||
import { createCredentialsContextAsync } from '../credentials/context';
|
||||
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
|
||||
|
||||
@@ -9,6 +9,6 @@ export default class Credentials extends Command {
|
||||
|
||||
async run() {
|
||||
const ctx = await createCredentialsContextAsync(process.cwd(), {});
|
||||
await runStandaloneCredentialsManagerAsync(ctx, new SelectPlatform());
|
||||
new CredentialsManager(ctx).runActionAsync(new SelectPlatform());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { CLIError } from '@oclif/errors';
|
||||
import CliTable from 'cli-table3';
|
||||
import gql from 'graphql-tag';
|
||||
import { format } from 'timeago.js';
|
||||
|
||||
import { graphqlClient } from '../../graphql/client';
|
||||
import { RootQuery, Update, UpdateRelease } from '../../graphql/generated';
|
||||
import log from '../../log';
|
||||
import { findProjectRootAsync, getProjectFullNameAsync } from '../../project/projectUtils';
|
||||
import { getActorDisplayName } from '../../user/actions';
|
||||
|
||||
const RELEASES_LIMIT = 10_000;
|
||||
|
||||
export default class ReleaseList extends Command {
|
||||
static hidden = true;
|
||||
|
||||
static description = 'List all releases on this project.';
|
||||
|
||||
static flags = {
|
||||
json: flags.boolean({
|
||||
description: 'return output as JSON',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
const { flags } = this.parse(ReleaseList);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const fullName = await getProjectFullNameAsync(projectDir);
|
||||
const releases = await this.listReleasesAsync({ fullName });
|
||||
if (flags.json) {
|
||||
log(JSON.stringify(releases, null, 2));
|
||||
} else {
|
||||
const table = new CliTable({ head: ['Release', 'Latest update'] });
|
||||
table.push(
|
||||
...releases.map((release: UpdateRelease) => [
|
||||
release.releaseName,
|
||||
formatUpdate(release.updates[0]),
|
||||
])
|
||||
);
|
||||
log(table.toString());
|
||||
if (releases.length >= RELEASES_LIMIT) {
|
||||
log.warn(`Showing first ${RELEASES_LIMIT} releases, some results might be omitted.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async listReleasesAsync({ fullName }: { fullName: string }): Promise<UpdateRelease[]> {
|
||||
const { data, error } = await graphqlClient
|
||||
.query<RootQuery>(
|
||||
gql`
|
||||
query ReleasesByAppQuery($fullName: String!, $limit: Int!) {
|
||||
app {
|
||||
byFullName(fullName: $fullName) {
|
||||
id
|
||||
fullName
|
||||
updateReleases(offset: 0, limit: $limit) {
|
||||
id
|
||||
releaseName
|
||||
updates(offset: 0, limit: 1) {
|
||||
id
|
||||
actor {
|
||||
__typename
|
||||
id
|
||||
... on User {
|
||||
username
|
||||
}
|
||||
... on Robot {
|
||||
firstName
|
||||
}
|
||||
}
|
||||
updatedAt
|
||||
updateMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
fullName,
|
||||
limit: RELEASES_LIMIT,
|
||||
}
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
if (error) {
|
||||
if (error.networkError) {
|
||||
throw new CLIError(`Fetching releases failed: ${error.networkError.message}`);
|
||||
} else {
|
||||
throw new CLIError(error.graphQLErrors.map(e => e.message).join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
return data?.app?.byFullName.updateReleases ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
function formatUpdate(update: Update | undefined): string {
|
||||
if (!update) {
|
||||
return 'N/A';
|
||||
}
|
||||
const message = update.updateMessage ? `"${update.updateMessage}" ` : '';
|
||||
return `${message}(${format(update.updatedAt, 'en_US')} by ${getActorDisplayName(
|
||||
update.actor as any
|
||||
)})`;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import ora from 'ora';
|
||||
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getReleaseByNameAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { buildUpdateInfoGroupAsync, collectAssets, uploadAssetsAsync } from '../../project/publish';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { getLastCommitMessageAsync } from '../../utils/git';
|
||||
|
||||
export default class ReleasePublish extends Command {
|
||||
static hidden = true;
|
||||
static description = 'Publish an update group to a release.';
|
||||
|
||||
static flags = {
|
||||
'input-dir': flags.string({
|
||||
description: 'location of the bundle',
|
||||
default: 'dist',
|
||||
required: false,
|
||||
}),
|
||||
release: flags.string({
|
||||
description: 'current name of the release.',
|
||||
}),
|
||||
message: flags.string({
|
||||
description: 'Short message describing the updates.',
|
||||
required: false,
|
||||
}),
|
||||
json: flags.boolean({
|
||||
description: `return a json with the edited release's ID and name.`,
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
let {
|
||||
flags: {
|
||||
json: jsonFlag,
|
||||
release: releaseName,
|
||||
message: updateMessage,
|
||||
'input-dir': inputDir,
|
||||
},
|
||||
} = this.parse(ReleasePublish);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
|
||||
const accountName = await getProjectAccountNameAsync(projectDir);
|
||||
const {
|
||||
exp: { slug, runtimeVersion },
|
||||
} = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
if (!runtimeVersion) {
|
||||
throw new Error(
|
||||
"Couldn't find 'runtimeVersion'. Please specify it under the 'expo' key in 'app.json'"
|
||||
);
|
||||
}
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
|
||||
if (!releaseName) {
|
||||
const validationMessage = 'release name may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ releaseName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'releaseName',
|
||||
message: 'Please enter the name of the release to publish on:',
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
if (!updateMessage) {
|
||||
const validationMessage = 'publish message may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ publishMessage: updateMessage } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'publishMessage',
|
||||
message: `Please enter a publication message.`,
|
||||
initial: (await getLastCommitMessageAsync())?.trim(),
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
const { id: releaseId } = await getReleaseByNameAsync({
|
||||
appId: projectId,
|
||||
releaseName: releaseName!,
|
||||
});
|
||||
|
||||
let updateInfoGroup;
|
||||
const assetSpinner = ora('Uploading assets...').start();
|
||||
try {
|
||||
const assets = collectAssets(inputDir!);
|
||||
await uploadAssetsAsync(assets);
|
||||
updateInfoGroup = await buildUpdateInfoGroupAsync(assets);
|
||||
assetSpinner.succeed('Uploaded assets!');
|
||||
} catch (e) {
|
||||
assetSpinner.fail('Failed to upload assets');
|
||||
throw e;
|
||||
}
|
||||
|
||||
let newUpdateGroup;
|
||||
const publishSpinner = ora('Publishing...').start();
|
||||
try {
|
||||
newUpdateGroup = await PublishMutation.publishUpdateGroupAsync({
|
||||
releaseId,
|
||||
updateInfoGroup,
|
||||
runtimeVersion,
|
||||
updateMessage,
|
||||
});
|
||||
publishSpinner.succeed('Published!');
|
||||
} catch (e) {
|
||||
publishSpinner.fail('Failed to published updates');
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
log(newUpdateGroup);
|
||||
} else {
|
||||
const outputMessage = new Table({
|
||||
wordWrap: true,
|
||||
chars: {
|
||||
top: '',
|
||||
'top-mid': '',
|
||||
'top-left': '',
|
||||
'top-right': '',
|
||||
bottom: '',
|
||||
'bottom-mid': '',
|
||||
'bottom-left': '',
|
||||
'bottom-right': '',
|
||||
left: '',
|
||||
'left-mid': '',
|
||||
mid: '',
|
||||
'mid-mid': '',
|
||||
right: '',
|
||||
'right-mid': '',
|
||||
middle: ' ',
|
||||
},
|
||||
style: { 'padding-left': 0, 'padding-right': 0 },
|
||||
});
|
||||
outputMessage.push(
|
||||
[chalk.dim('project'), `@${accountName}/${slug}`],
|
||||
[chalk.dim('release'), releaseName],
|
||||
[chalk.dim('runtimeVersion'), runtimeVersion],
|
||||
[chalk.dim('updateGroupID'), newUpdateGroup.updateGroup],
|
||||
[chalk.dim('message'), updateMessage]
|
||||
);
|
||||
log(outputMessage.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,20 +44,24 @@ async function viewUpdateReleaseAsync({
|
||||
{
|
||||
appId: string;
|
||||
releaseName: string;
|
||||
limit: number;
|
||||
}
|
||||
>(
|
||||
gql`
|
||||
query ViewRelease($appId: String!, $releaseName: String!) {
|
||||
query ViewRelease($appId: String!, $releaseName: String!, $limit: Int!) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
updateReleaseByReleaseName(releaseName: $releaseName) {
|
||||
id
|
||||
releaseName
|
||||
updates(offset: 0, limit: ${PAGE_LIMIT}) {
|
||||
updates(offset: 0, limit: $limit) {
|
||||
id
|
||||
updateGroup
|
||||
updateMessage
|
||||
createdAt
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
firstName
|
||||
}
|
||||
@@ -74,6 +78,7 @@ async function viewUpdateReleaseAsync({
|
||||
{
|
||||
appId,
|
||||
releaseName,
|
||||
limit: PAGE_LIMIT,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
|
||||
@@ -1,78 +1,13 @@
|
||||
import log from '../log';
|
||||
import { Context } from './context';
|
||||
|
||||
export interface CredentialsManager {
|
||||
runActionAsync(action: Action): Promise<void>;
|
||||
|
||||
pushNextAction(action: Action): void;
|
||||
|
||||
popAction(): Action | null;
|
||||
export interface Action<T = void> {
|
||||
runAsync(manager: CredentialsManager, ctx: Context): Promise<T>;
|
||||
}
|
||||
|
||||
export interface Action {
|
||||
runAsync(manager: CredentialsManager, ctx: Context): Promise<void>;
|
||||
}
|
||||
export class CredentialsManager {
|
||||
constructor(private ctx: Context) {}
|
||||
|
||||
export class QuitError extends Error {}
|
||||
|
||||
export async function runStandaloneCredentialsManagerAsync(
|
||||
ctx: Context,
|
||||
startAction: Action
|
||||
): Promise<void> {
|
||||
const manager = new CredentialsManagerImpl(ctx, startAction);
|
||||
await manager.runManagerAsync(true);
|
||||
}
|
||||
|
||||
export async function runCredentialsManagerAsync(ctx: Context, startAction: Action): Promise<void> {
|
||||
const manager = new CredentialsManagerImpl(ctx, startAction);
|
||||
await manager.runManagerAsync();
|
||||
}
|
||||
|
||||
class CredentialsManagerImpl implements CredentialsManager {
|
||||
private actionStack: Action[] = [];
|
||||
|
||||
constructor(private ctx: Context, startAction: Action) {
|
||||
this.actionStack.push(startAction);
|
||||
}
|
||||
|
||||
public pushNextAction(action: Action): void {
|
||||
this.actionStack.push(action);
|
||||
}
|
||||
|
||||
public popAction(): Action | null {
|
||||
return this.actionStack.pop() ?? null;
|
||||
}
|
||||
|
||||
public async runActionAsync(action: Action) {
|
||||
await new CredentialsManagerImpl(this.ctx, action).doRunManagerAsync();
|
||||
}
|
||||
|
||||
public async runManagerAsync(isStandaloneManager: boolean = false): Promise<void> {
|
||||
try {
|
||||
await this.doRunManagerAsync(isStandaloneManager);
|
||||
} catch (error) {
|
||||
if (error instanceof QuitError) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async doRunManagerAsync(isStandaloneManager: boolean = false): Promise<void> {
|
||||
while (true) {
|
||||
try {
|
||||
const currentAction = this.popAction();
|
||||
if (!currentAction) {
|
||||
return;
|
||||
}
|
||||
await currentAction.runAsync(this, this.ctx);
|
||||
} catch (error) {
|
||||
if (!isStandaloneManager || error instanceof QuitError) {
|
||||
throw error;
|
||||
} else {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
public async runActionAsync<T>(action: Action<T>): Promise<T> {
|
||||
return await action.runAsync(this, this.ctx);
|
||||
}
|
||||
}
|
||||
@@ -29,10 +29,6 @@ export function createManagerMock(mockOverride: Record<string, any> = {}): Crede
|
||||
runActionAsync: jest.fn(() => {
|
||||
throw new Error('unexpected call'); // should be implemented in test directly
|
||||
}),
|
||||
pushNextAction: jest.fn(() => {
|
||||
throw new Error('unexpected call'); // should be implemented in test directly
|
||||
}),
|
||||
popAction: jest.fn(),
|
||||
},
|
||||
mockOverride
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
|
||||
import log from '../../log';
|
||||
import { runCredentialsManagerAsync } from '../CredentialsManager';
|
||||
import { CredentialsManager } from '../CredentialsManager';
|
||||
import { CredentialsProvider } from '../CredentialsProvider';
|
||||
import { Context } from '../context';
|
||||
import * as credentialsJsonReader from '../credentialsJson/read';
|
||||
@@ -85,10 +85,12 @@ export default class AndroidCredentialsProvider implements CredentialsProvider {
|
||||
}
|
||||
|
||||
private async getRemoteAsync(): Promise<AndroidCredentials> {
|
||||
await runCredentialsManagerAsync(this.ctx, new SetupBuildCredentials(this.projectFullName));
|
||||
await new CredentialsManager(this.ctx).runActionAsync(
|
||||
new SetupBuildCredentials(this.projectFullName)
|
||||
);
|
||||
const keystore = await this.ctx.android.fetchKeystoreAsync(this.projectFullName);
|
||||
if (!keystore) {
|
||||
throw new Error('Unable to set up credentials, failed to fetch keystore from Expo servers');
|
||||
throw new Error('Unable to set up credentials, failed to fetch keystore from EAS servers');
|
||||
}
|
||||
return { keystore };
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export class SetupBuildCredentials implements Action {
|
||||
throw new Error('Generating a new Keystore is not supported in --non-interactive mode');
|
||||
}
|
||||
|
||||
manager.pushNextAction(new UpdateKeystore(this.projectFullName));
|
||||
await manager.runActionAsync(new UpdateKeystore(this.projectFullName));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export class UpdateCredentialsJson implements Action {
|
||||
log('Updating Android credentials in credentials.json');
|
||||
await updateAndroidCredentialsAsync(ctx);
|
||||
log.succeed(
|
||||
'Android part of your local credentials.json is synced with values stored on Expo servers.'
|
||||
'Android part of your local credentials.json is synced with values stored on EAS servers.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ describe('run RemoveKeystore when keystore does exist', () => {
|
||||
|
||||
await new RemoveKeystore(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.pushNextAction).not.toHaveBeenCalled();
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.removeKeystoreAsync).not.toHaveBeenCalled();
|
||||
@@ -59,7 +59,6 @@ describe('run RemoveKeystore when keystore does exist', () => {
|
||||
|
||||
await new RemoveKeystore(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.pushNextAction).not.toHaveBeenCalled();
|
||||
expect(manager.runActionAsync).toHaveBeenCalledTimes(1);
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
@@ -83,7 +82,7 @@ describe('run RemoveKeystore when keystore does exist', () => {
|
||||
}
|
||||
|
||||
expect(prompts).not.toHaveBeenCalled();
|
||||
expect(manager.pushNextAction).not.toHaveBeenCalled();
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.removeKeystoreAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -100,7 +99,7 @@ describe('run RemoveKeystore when keystore does not exist', () => {
|
||||
|
||||
await new RemoveKeystore(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.pushNextAction).not.toHaveBeenCalled();
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(prompts).not.toHaveBeenCalled();
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.removeKeystoreAsync).not.toHaveBeenCalled();
|
||||
@@ -115,7 +114,7 @@ describe('run RemoveKeystore when keystore does not exist', () => {
|
||||
|
||||
await new RemoveKeystore(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.pushNextAction).not.toHaveBeenCalled();
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(prompts).not.toHaveBeenCalled();
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.removeKeystoreAsync).not.toHaveBeenCalled();
|
||||
|
||||
+4
-4
@@ -38,7 +38,7 @@ describe('run SetupBuildCredentials when www has valid credentials', () => {
|
||||
|
||||
await new SetupBuildCredentials(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.pushNextAction).not.toHaveBeenCalled();
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateKeystoreAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -54,7 +54,7 @@ describe('run SetupBuildCredentials when www has valid credentials', () => {
|
||||
|
||||
await new SetupBuildCredentials(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.pushNextAction).not.toHaveBeenCalled();
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateKeystoreAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -69,12 +69,12 @@ describe('run SetupBuildCredentials when www has no credentials', () => {
|
||||
});
|
||||
const manager = createManagerMock();
|
||||
|
||||
(manager.pushNextAction as any).mockImplementationOnce((action: Action) => {
|
||||
(manager.runActionAsync as any).mockImplementationOnce((action: Action) => {
|
||||
expect(action).toBeInstanceOf(UpdateKeystore);
|
||||
});
|
||||
await new SetupBuildCredentials(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.pushNextAction).toHaveBeenCalledTimes(1);
|
||||
expect(manager.runActionAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateKeystoreAsync).toHaveBeenCalledTimes(0); // will be called in UpdateKeystore
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('run UpdateKeystore when keystore exist on www', () => {
|
||||
|
||||
await new UpdateKeystore(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.pushNextAction).not.toHaveBeenCalled();
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
@@ -66,7 +66,7 @@ describe('run UpdateKeystore when keystore exist on www', () => {
|
||||
|
||||
await new UpdateKeystore(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.pushNextAction).not.toHaveBeenCalled();
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(prompts).toHaveBeenCalledTimes(5);
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
@@ -86,7 +86,7 @@ describe('run UpdateKeystore when keystore does not exist on www', () => {
|
||||
|
||||
await new UpdateKeystore(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.pushNextAction).not.toHaveBeenCalled();
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
@@ -110,7 +110,7 @@ describe('run UpdateKeystore when keystore does not exist on www', () => {
|
||||
|
||||
await new UpdateKeystore(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
expect(manager.pushNextAction).not.toHaveBeenCalled();
|
||||
expect(manager.runActionAsync).not.toHaveBeenCalled();
|
||||
expect(prompts).toHaveBeenCalledTimes(5);
|
||||
expect(ctx.android.fetchKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.updateKeystoreAsync).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -90,93 +90,140 @@ describe('credentialsJson', () => {
|
||||
});
|
||||
|
||||
describe('readIosAsync', () => {
|
||||
it('should read ios credentials if everything is correct', async () => {
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({
|
||||
ios: {
|
||||
provisioningProfilePath: 'pprofile',
|
||||
describe('single target credentials', () => {
|
||||
it('should read ios credentials if everything is correct', async () => {
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({
|
||||
ios: {
|
||||
provisioningProfilePath: 'pprofile',
|
||||
distributionCertificate: {
|
||||
path: 'cert.p12',
|
||||
password: 'certPass',
|
||||
},
|
||||
},
|
||||
}),
|
||||
'./pprofile': 'somebinarycontent',
|
||||
'./cert.p12': 'somebinarycontent2',
|
||||
});
|
||||
const result = await credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
expect(result).toEqual({
|
||||
provisioningProfile: 'c29tZWJpbmFyeWNvbnRlbnQ=',
|
||||
distributionCertificate: {
|
||||
certP12: 'c29tZWJpbmFyeWNvbnRlbnQy',
|
||||
certPassword: 'certPass',
|
||||
},
|
||||
});
|
||||
});
|
||||
it('should throw error when credentials.json is missing', async () => {
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
await expect(promise).rejects.toThrow(
|
||||
'credentials.json must exist in the project root directory and contain a valid JSON'
|
||||
);
|
||||
});
|
||||
it('should throw error if ios field is missing', async () => {
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({}),
|
||||
'./pprofile': 'somebinarycontent',
|
||||
'./cert.p12': 'somebinarycontent2',
|
||||
});
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
await expect(promise).rejects.toThrow('iOS credentials are missing from credentials.json');
|
||||
});
|
||||
it('should throw error if some field is missing', async () => {
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({
|
||||
ios: {
|
||||
distributionCertificate: {
|
||||
path: 'cert.p12',
|
||||
password: 'certPass',
|
||||
},
|
||||
},
|
||||
}),
|
||||
'./pprofile': 'somebinarycontent',
|
||||
'./cert.p12': 'somebinarycontent2',
|
||||
});
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
await expect(promise).rejects.toThrow(
|
||||
'credentials.json is not valid [ValidationError: "ios" does not match any of the allowed types]'
|
||||
);
|
||||
});
|
||||
it('should throw error if dist cert file is missing', async () => {
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({
|
||||
ios: {
|
||||
provisioningProfilePath: 'pprofile',
|
||||
distributionCertificate: {
|
||||
path: 'cert.p12',
|
||||
password: 'certPass',
|
||||
},
|
||||
},
|
||||
}),
|
||||
'./pprofile': 'somebinarycontent',
|
||||
});
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
await expect(promise).rejects.toThrow("ENOENT: no such file or directory, open 'cert.p12'");
|
||||
});
|
||||
it('should throw error if provisioningProfile file is missing', async () => {
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({
|
||||
ios: {
|
||||
provisioningProfilePath: 'pprofile',
|
||||
distributionCertificate: {
|
||||
path: 'cert.p12',
|
||||
password: 'certPass',
|
||||
},
|
||||
},
|
||||
}),
|
||||
'./cert.p12': 'somebinarycontent2',
|
||||
});
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
await expect(promise).rejects.toThrow("ENOENT: no such file or directory, open 'pprofile'");
|
||||
});
|
||||
});
|
||||
describe('multi-target credentials', () => {
|
||||
it('should read ios credentials if everything is correct', async () => {
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({
|
||||
ios: {
|
||||
target1: {
|
||||
provisioningProfilePath: 'pprofile-1.mobileprovision',
|
||||
distributionCertificate: {
|
||||
path: 'dist-cert-1.p12',
|
||||
password: 'cert-pass-1',
|
||||
},
|
||||
},
|
||||
target2: {
|
||||
provisioningProfilePath: 'pprofile-2.mobileprovision',
|
||||
distributionCertificate: {
|
||||
path: 'dist-cert-2.p12',
|
||||
password: 'cert-pass-2',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
'./pprofile-1.mobileprovision': 'pprofile-1-somebinarycontent',
|
||||
'./pprofile-2.mobileprovision': 'pprofile-2-somebinarycontent',
|
||||
'./dist-cert-1.p12': 'cert-1-somebinarycontent',
|
||||
'./dist-cert-2.p12': 'cert-2-somebinarycontent',
|
||||
});
|
||||
const result = await credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
expect(result).toEqual({
|
||||
target1: {
|
||||
provisioningProfile: 'cHByb2ZpbGUtMS1zb21lYmluYXJ5Y29udGVudA==',
|
||||
distributionCertificate: {
|
||||
path: 'cert.p12',
|
||||
password: 'certPass',
|
||||
certP12: 'Y2VydC0xLXNvbWViaW5hcnljb250ZW50',
|
||||
certPassword: 'cert-pass-1',
|
||||
},
|
||||
},
|
||||
}),
|
||||
'./pprofile': 'somebinarycontent',
|
||||
'./cert.p12': 'somebinarycontent2',
|
||||
});
|
||||
const result = await credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
expect(result).toEqual({
|
||||
provisioningProfile: 'c29tZWJpbmFyeWNvbnRlbnQ=',
|
||||
distributionCertificate: {
|
||||
certP12: 'c29tZWJpbmFyeWNvbnRlbnQy',
|
||||
certPassword: 'certPass',
|
||||
},
|
||||
});
|
||||
});
|
||||
it('should throw error when credentials.json is missing', async () => {
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
await expect(promise).rejects.toThrow(
|
||||
'credentials.json must exist in the project root directory and contain a valid JSON'
|
||||
);
|
||||
});
|
||||
it('should throw error if ios field is missing', async () => {
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({}),
|
||||
'./pprofile': 'somebinarycontent',
|
||||
'./cert.p12': 'somebinarycontent2',
|
||||
});
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
await expect(promise).rejects.toThrow('iOS credentials are missing from credentials.json');
|
||||
});
|
||||
it('should throw error if some field is missing', async () => {
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({
|
||||
ios: {
|
||||
target2: {
|
||||
provisioningProfile: 'cHByb2ZpbGUtMi1zb21lYmluYXJ5Y29udGVudA==',
|
||||
distributionCertificate: {
|
||||
path: 'cert.p12',
|
||||
password: 'certPass',
|
||||
certP12: 'Y2VydC0yLXNvbWViaW5hcnljb250ZW50',
|
||||
certPassword: 'cert-pass-2',
|
||||
},
|
||||
},
|
||||
}),
|
||||
'./pprofile': 'somebinarycontent',
|
||||
'./cert.p12': 'somebinarycontent2',
|
||||
});
|
||||
});
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
await expect(promise).rejects.toThrow(
|
||||
'credentials.json is not valid [ValidationError: "ios.provisioningProfilePath" is required]'
|
||||
);
|
||||
});
|
||||
it('should throw error if dist cert file is missing', async () => {
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({
|
||||
ios: {
|
||||
provisioningProfilePath: 'pprofile',
|
||||
distributionCertificate: {
|
||||
path: 'cert.p12',
|
||||
password: 'certPass',
|
||||
},
|
||||
},
|
||||
}),
|
||||
'./pprofile': 'somebinarycontent',
|
||||
});
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
await expect(promise).rejects.toThrow("ENOENT: no such file or directory, open 'cert.p12'");
|
||||
});
|
||||
it('should throw error if provisioningProfile file is missing', async () => {
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({
|
||||
ios: {
|
||||
provisioningProfilePath: 'pprofile',
|
||||
distributionCertificate: {
|
||||
path: 'cert.p12',
|
||||
password: 'certPass',
|
||||
},
|
||||
},
|
||||
}),
|
||||
'./cert.p12': 'somebinarycontent2',
|
||||
});
|
||||
const promise = credentialsJsonReader.readIosCredentialsAsync('.');
|
||||
await expect(promise).rejects.toThrow("ENOENT: no such file or directory, open 'pprofile'");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -17,15 +17,18 @@ jest.mock('prompts');
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
const originalConsoleError = console.error;
|
||||
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
console.error = jest.fn();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
console.log = originalConsoleLog;
|
||||
console.warn = originalConsoleWarn;
|
||||
console.error = originalConsoleError;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -114,7 +117,7 @@ describe('update credentials.json', () => {
|
||||
throw new Error('updateAndroidCredentialsAsync should throw an error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
'There are no credentials configured for this project on Expo servers'
|
||||
'There are no credentials configured for this project on EAS servers'
|
||||
);
|
||||
}
|
||||
const keystore = await fs.readFile('./keystore.jks', 'base64');
|
||||
@@ -277,7 +280,7 @@ describe('update credentials.json', () => {
|
||||
throw new Error('updateIosCredentialsAsync should throw na error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
'There are no credentials configured for this project on Expo servers'
|
||||
'There are no credentials configured for this project on EAS servers'
|
||||
);
|
||||
}
|
||||
const certP12 = await fs.readFile('./cert.p12', 'base64');
|
||||
@@ -331,5 +334,67 @@ describe('update credentials.json', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
it('should throw an error if credentials.json contains credentials for multi-target projects', async () => {
|
||||
const ctx = createCtxMock({
|
||||
ios: {
|
||||
getAppCredentialsAsync: jest.fn(() => testAllCredentialsForApp),
|
||||
getDistributionCertificateAsync: jest.fn(() => testIosDistCredential),
|
||||
getProvisioningProfileAsync: jest.fn(() => testProvisioningProfile),
|
||||
},
|
||||
});
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({
|
||||
ios: {
|
||||
target1: {
|
||||
provisioningProfilePath: 'pprofile-1.mobileprovision',
|
||||
distributionCertificate: {
|
||||
path: 'dist-cert-1.p12',
|
||||
password: 'cert-pass-1',
|
||||
},
|
||||
},
|
||||
target2: {
|
||||
provisioningProfilePath: 'pprofile-2.mobileprovision',
|
||||
distributionCertificate: {
|
||||
path: 'dist-cert-2.p12',
|
||||
password: 'cert-pass-2',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
'./pprofile-1.mobileprovision': 'pprofile-1-somebinarycontent',
|
||||
'./pprofile-2.mobileprovision': 'pprofile-2-somebinarycontent',
|
||||
'./dist-cert-1.p12': 'cert-1-somebinarycontent',
|
||||
'./dist-cert-2.p12': 'cert-2-somebinarycontent',
|
||||
});
|
||||
await expect(() => updateIosCredentialsAsync(ctx, 'bundleIdentifier')).rejects.toThrow(
|
||||
/Updating credentials.json failed/
|
||||
);
|
||||
});
|
||||
it(`should throw an error if there is a typo in credentials.json (because we can't tell if this is a target name or not)`, async () => {
|
||||
const ctx = createCtxMock({
|
||||
ios: {
|
||||
getAppCredentialsAsync: jest.fn(() => testAllCredentialsForApp),
|
||||
getDistributionCertificateAsync: jest.fn(() => testIosDistCredential),
|
||||
getProvisioningProfileAsync: jest.fn(() => testProvisioningProfile),
|
||||
},
|
||||
});
|
||||
vol.fromJSON({
|
||||
'./credentials.json': JSON.stringify({
|
||||
ios: {
|
||||
// typo!!!
|
||||
xprovisioningProfilePath: 'pprofile-1.mobileprovision',
|
||||
distributionCertificate: {
|
||||
path: 'dist-cert-1.p12',
|
||||
password: 'cert-pass-1',
|
||||
},
|
||||
},
|
||||
}),
|
||||
'./pprofile-1.mobileprovision': 'pprofile-1-somebinarycontent',
|
||||
'./dist-cert-1.p12': 'cert-1-somebinarycontent',
|
||||
});
|
||||
await expect(() => updateIosCredentialsAsync(ctx, 'bundleIdentifier')).rejects.toThrow(
|
||||
/Updating credentials.json failed/
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,13 @@ import path from 'path';
|
||||
|
||||
import { Keystore } from '../android/credentials';
|
||||
|
||||
interface CredentialsJsonIosCredentials {
|
||||
provisioningProfilePath: string;
|
||||
distributionCertificate: {
|
||||
path: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
export interface CredentialsJson {
|
||||
android?: {
|
||||
keystore: {
|
||||
@@ -13,18 +20,20 @@ export interface CredentialsJson {
|
||||
keyPassword: string;
|
||||
};
|
||||
};
|
||||
ios?: {
|
||||
provisioningProfilePath: string;
|
||||
distributionCertificate: {
|
||||
path: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
ios?: CredentialsJsonIosCredentials | Record<string, CredentialsJsonIosCredentials>;
|
||||
experimental?: {
|
||||
npmToken?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const IosTargetCredentials = Joi.object({
|
||||
provisioningProfilePath: Joi.string().required(),
|
||||
distributionCertificate: Joi.object({
|
||||
path: Joi.string().required(),
|
||||
password: Joi.string().required(),
|
||||
}).required(),
|
||||
});
|
||||
|
||||
const CredentialsJsonSchema = Joi.object({
|
||||
android: Joi.object({
|
||||
keystore: Joi.object({
|
||||
@@ -34,13 +43,10 @@ const CredentialsJsonSchema = Joi.object({
|
||||
keyPassword: Joi.string().required(),
|
||||
}).required(),
|
||||
}),
|
||||
ios: Joi.object({
|
||||
provisioningProfilePath: Joi.string().required(),
|
||||
distributionCertificate: Joi.object({
|
||||
path: Joi.string().required(),
|
||||
password: Joi.string().required(),
|
||||
}).required(),
|
||||
}),
|
||||
ios: [
|
||||
IosTargetCredentials,
|
||||
Joi.object().pattern(Joi.string().required(), IosTargetCredentials.required()),
|
||||
],
|
||||
experimental: Joi.object({
|
||||
npmToken: Joi.string(),
|
||||
}),
|
||||
@@ -50,13 +56,15 @@ interface AndroidCredentials {
|
||||
keystore: Keystore;
|
||||
}
|
||||
|
||||
interface iOSCredentials {
|
||||
export interface IosTargetCredentials {
|
||||
provisioningProfile: string;
|
||||
distributionCertificate: {
|
||||
certP12: string;
|
||||
certPassword: string;
|
||||
};
|
||||
}
|
||||
export type IosTargetCredentialsMap = Record<string, IosTargetCredentials>;
|
||||
export type IosCredentials = IosTargetCredentials | IosTargetCredentialsMap;
|
||||
|
||||
export async function fileExistsAsync(projectDir: string): Promise<boolean> {
|
||||
return await fs.pathExists(path.join(projectDir, 'credentials.json'));
|
||||
@@ -78,22 +86,52 @@ export async function readAndroidCredentialsAsync(projectDir: string): Promise<A
|
||||
};
|
||||
}
|
||||
|
||||
export async function readIosCredentialsAsync(projectDir: string): Promise<iOSCredentials> {
|
||||
export async function readIosCredentialsAsync(projectDir: string): Promise<IosCredentials> {
|
||||
const credentialsJson = await readAsync(projectDir);
|
||||
if (!credentialsJson.ios) {
|
||||
throw new Error('iOS credentials are missing from credentials.json');
|
||||
}
|
||||
|
||||
if (!isInternalCredentialsMap(credentialsJson.ios)) {
|
||||
return await readCredentialsForTargetAsync(projectDir, credentialsJson.ios);
|
||||
} else {
|
||||
const targets = Object.keys(credentialsJson.ios);
|
||||
const targetCredentialsMap: IosTargetCredentialsMap = {};
|
||||
for (const target of targets) {
|
||||
targetCredentialsMap[target] = await readCredentialsForTargetAsync(
|
||||
projectDir,
|
||||
credentialsJson.ios[target]
|
||||
);
|
||||
}
|
||||
return targetCredentialsMap;
|
||||
}
|
||||
}
|
||||
|
||||
function isInternalCredentialsMap(
|
||||
ios: CredentialsJsonIosCredentials | Record<string, CredentialsJsonIosCredentials>
|
||||
): ios is Record<string, CredentialsJsonIosCredentials> {
|
||||
return typeof ios.provisioningProfilePath !== 'string';
|
||||
}
|
||||
|
||||
export function isCredentialsMap(ios: IosCredentials): ios is IosTargetCredentialsMap {
|
||||
return typeof ios.provisioningProfile !== 'string';
|
||||
}
|
||||
|
||||
async function readCredentialsForTargetAsync(
|
||||
projectDir: string,
|
||||
targetCredentials: CredentialsJsonIosCredentials
|
||||
): Promise<IosTargetCredentials> {
|
||||
return {
|
||||
provisioningProfile: await fs.readFile(
|
||||
getAbsolutePath(projectDir, credentialsJson.ios.provisioningProfilePath),
|
||||
getAbsolutePath(projectDir, targetCredentials.provisioningProfilePath),
|
||||
'base64'
|
||||
),
|
||||
distributionCertificate: {
|
||||
certP12: await fs.readFile(
|
||||
getAbsolutePath(projectDir, credentialsJson.ios.distributionCertificate.path),
|
||||
getAbsolutePath(projectDir, targetCredentials.distributionCertificate.path),
|
||||
'base64'
|
||||
),
|
||||
certPassword: credentialsJson.ios.distributionCertificate.password,
|
||||
certPassword: targetCredentials.distributionCertificate.password,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function updateAndroidCredentialsAsync(ctx: Context): Promise<void>
|
||||
const experienceName = `@${accountName}/${ctx.exp.slug}`;
|
||||
const keystore = await ctx.android.fetchKeystoreAsync(experienceName);
|
||||
if (!keystore) {
|
||||
throw new Error('There are no credentials configured for this project on Expo servers');
|
||||
throw new Error('There are no credentials configured for this project on EAS servers');
|
||||
}
|
||||
|
||||
const isKeystoreComplete =
|
||||
@@ -39,7 +39,7 @@ export async function updateAndroidCredentialsAsync(ctx: Context): Promise<void>
|
||||
if (!isKeystoreComplete) {
|
||||
const confirm = await confirmAsync({
|
||||
message:
|
||||
'Credentials on Expo servers might be invalid or incomplete. Are you sure you want to continue?',
|
||||
'Credentials on EAS servers might be invalid or incomplete. Are you sure you want to continue?',
|
||||
});
|
||||
if (!confirm) {
|
||||
log.warn('Aborting...');
|
||||
@@ -98,9 +98,27 @@ export async function updateIosCredentialsAsync(
|
||||
log.error('Make sure that file is correct (or remove it) and rerun this command.');
|
||||
throw error;
|
||||
}
|
||||
|
||||
// TODO: implement updating credentials.json with multi-target credentials from EAS servers
|
||||
if (rawCredentialsJsonObject?.ios) {
|
||||
const keys = Object.keys(rawCredentialsJsonObject.ios);
|
||||
const maybeUnknownKey = keys.find(
|
||||
key => key !== 'provisioningProfilePath' && key !== 'distributionCertificate'
|
||||
);
|
||||
if (maybeUnknownKey) {
|
||||
log.error(
|
||||
`It looks like your credentials.json either contains multi-target credentials or you've made a typo.`
|
||||
);
|
||||
log.error(`- The key in 'ios' object that EAS CLI encountered is: ${maybeUnknownKey}`);
|
||||
log.error(
|
||||
`- Updating credentials.json (for multi-target iOS projects) with credentials stored on EAS servers is not supported at the moment, sorry!`
|
||||
);
|
||||
throw new Error('Updating credentials.json failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const accountName = await getProjectAccountName(ctx.exp, ctx.user);
|
||||
const accountName = getProjectAccountName(ctx.exp, ctx.user);
|
||||
const appLookupParams = {
|
||||
accountName,
|
||||
projectName: ctx.exp.slug,
|
||||
@@ -113,7 +131,7 @@ export async function updateIosCredentialsAsync(
|
||||
const appCredentials = await ctx.ios.getAppCredentialsAsync(appLookupParams);
|
||||
const distCredentials = await ctx.ios.getDistributionCertificateAsync(appLookupParams);
|
||||
if (!appCredentials?.credentials?.provisioningProfile && !distCredentials) {
|
||||
throw new Error('There are no credentials configured for this project on Expo servers');
|
||||
throw new Error('There are no credentials configured for this project on EAS servers');
|
||||
}
|
||||
|
||||
const areCredentialsComplete =
|
||||
@@ -124,7 +142,7 @@ export async function updateIosCredentialsAsync(
|
||||
if (!areCredentialsComplete) {
|
||||
const confirm = await confirmAsync({
|
||||
message:
|
||||
'Credentials on Expo servers might be invalid or incomplete. Are you sure you want to continue?',
|
||||
'Credentials on EAS servers might be invalid or incomplete. Are you sure you want to continue?',
|
||||
});
|
||||
if (!confirm) {
|
||||
log.warn('Aborting...');
|
||||
|
||||
@@ -3,20 +3,15 @@ import { CredentialsSource, DistributionType } from '@expo/eas-json';
|
||||
|
||||
import { IosDistributionType } from '../../graphql/generated';
|
||||
import log from '../../log';
|
||||
import { ensureAccounts, findAccountByName } from '../../user/Account';
|
||||
import { runCredentialsManagerAsync } from '../CredentialsManager';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { CredentialsManager } from '../CredentialsManager';
|
||||
import { CredentialsProvider } from '../CredentialsProvider';
|
||||
import { Context } from '../context';
|
||||
import * as credentialsJsonReader from '../credentialsJson/read';
|
||||
import type { IosCredentials } from '../credentialsJson/read';
|
||||
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
|
||||
|
||||
export interface IosCredentials {
|
||||
provisioningProfile: string;
|
||||
distributionCertificate: {
|
||||
certP12: string;
|
||||
certPassword: string;
|
||||
};
|
||||
}
|
||||
export { IosCredentials };
|
||||
|
||||
interface PartialIosCredentials {
|
||||
provisioningProfile?: string;
|
||||
@@ -74,6 +69,15 @@ export default class IosCredentialsProvider implements CredentialsProvider {
|
||||
const [remote, local] = await Promise.all([this.fetchRemoteAsync(), this.getLocalAsync()]);
|
||||
const r = remote;
|
||||
const l = local;
|
||||
|
||||
// TODO
|
||||
// For now, when credentials.json contains credentials for multi-target project
|
||||
// assume they are synced with the Expo servers.
|
||||
// Change this behavior when we figure out how to store multi-target project credentials in the db.
|
||||
if (credentialsJsonReader.isCredentialsMap(l)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !!(
|
||||
r &&
|
||||
l &&
|
||||
@@ -109,8 +113,7 @@ export default class IosCredentialsProvider implements CredentialsProvider {
|
||||
if (this.options.skipCredentialsCheck) {
|
||||
log('Skipping credentials check');
|
||||
} else {
|
||||
await runCredentialsManagerAsync(
|
||||
this.ctx,
|
||||
await new CredentialsManager(this.ctx).runActionAsync(
|
||||
new SetupBuildCredentials(this.options.app, this.options.distribution)
|
||||
);
|
||||
}
|
||||
@@ -146,7 +149,7 @@ export default class IosCredentialsProvider implements CredentialsProvider {
|
||||
private async fetchRemoteAsync(): Promise<PartialIosCredentials> {
|
||||
if (this.options.distribution === DistributionType.INTERNAL) {
|
||||
const { app } = this.options;
|
||||
const account = findAccountByName(ensureAccounts(this.ctx.user.accounts), app.accountName);
|
||||
const account = findAccountByName(this.ctx.user.accounts, app.accountName);
|
||||
if (!account) {
|
||||
throw new Error(`You do not have access to the ${app.accountName} account`);
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export class ConfigureProvisioningProfile implements Action {
|
||||
|
||||
const spinner = ora(`Updating Expo profile for ${projectTag}`).start();
|
||||
try {
|
||||
// Update profile on expo servers
|
||||
// Update profile on EAS servers
|
||||
await ctx.ios.updateProvisioningProfileAsync(app, updatedProfile);
|
||||
spinner.succeed(`Updated Expo profile for ${projectTag}`);
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,13 +2,13 @@ import { DistributionType } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { AppleDevice, IosAppBuildCredentials } from '../../../graphql/generated';
|
||||
import { AppleDevice, IosAppBuildCredentialsFragment } from '../../../graphql/generated';
|
||||
import log from '../../../log';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { ensureAccounts, findAccountByName } from '../../../user/Account';
|
||||
import { findAccountByName } from '../../../user/Account';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
import { Context } from '../../context';
|
||||
import { readIosCredentialsAsync } from '../../credentialsJson/read';
|
||||
import { isCredentialsMap, readIosCredentialsAsync } from '../../credentialsJson/read';
|
||||
import { AppLookupParams, IosAppCredentials, IosDistCredentials } from '../credentials';
|
||||
import { displayProjectCredentials } from '../utils/printCredentials';
|
||||
import { readAppleTeam } from '../utils/provisioningProfile';
|
||||
@@ -30,10 +30,10 @@ export class SetupBuildCredentials implements Action {
|
||||
await ctx.appStore.ensureBundleIdExistsAsync(this.app, { enablePushNotifications: true });
|
||||
}
|
||||
|
||||
let iosAppBuildCredentials: IosAppBuildCredentials | null = null;
|
||||
let iosAppBuildCredentials: IosAppBuildCredentialsFragment | null = null;
|
||||
try {
|
||||
if (this.distribution === DistributionType.INTERNAL) {
|
||||
const account = findAccountByName(ensureAccounts(ctx.user.accounts), this.app.accountName);
|
||||
const account = findAccountByName(ctx.user.accounts, this.app.accountName);
|
||||
if (!account) {
|
||||
throw new Error(`You do not have access to the ${this.app.accountName} account`);
|
||||
}
|
||||
@@ -70,7 +70,7 @@ export class SetupBuildCredentials implements Action {
|
||||
|
||||
async unifyCredentialsFormatAsync(
|
||||
ctx: Context,
|
||||
iosAppBuildCredentials: IosAppBuildCredentials | null
|
||||
iosAppBuildCredentials: IosAppBuildCredentialsFragment | null
|
||||
): Promise<AppCredentialsAndDistCert> {
|
||||
if (!iosAppBuildCredentials) {
|
||||
const [appCredentials, distCert] = await Promise.all([
|
||||
@@ -127,6 +127,13 @@ export class SetupBuildCredentialsFromCredentialsJson implements Action {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// TODO: implement storing multi-target credentials on EAS servers
|
||||
if (isCredentialsMap(localCredentials)) {
|
||||
throw new Error(
|
||||
'Storing multi-target iOS credentials from credentials.json on EAS servers is not yet supported.'
|
||||
);
|
||||
}
|
||||
|
||||
const team = readAppleTeam(localCredentials.provisioningProfile);
|
||||
await ctx.ios.updateProvisioningProfileAsync(this.app, {
|
||||
...team,
|
||||
|
||||
@@ -35,7 +35,7 @@ export class SetupProvisioningProfile implements Action {
|
||||
}
|
||||
|
||||
if (!ctx.appStore.authCtx) {
|
||||
manager.pushNextAction(new CreateProvisioningProfile(this.app));
|
||||
await manager.runActionAsync(new CreateProvisioningProfile(this.app));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export class SetupProvisioningProfile implements Action {
|
||||
);
|
||||
|
||||
if (existingProfiles.length === 0) {
|
||||
manager.pushNextAction(new CreateProvisioningProfile(this.app));
|
||||
await manager.runActionAsync(new CreateProvisioningProfile(this.app));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -144,9 +144,9 @@ export class SetupProvisioningProfile implements Action {
|
||||
});
|
||||
|
||||
if (action === 'GENERATE') {
|
||||
manager.pushNextAction(new CreateProvisioningProfile(this.app));
|
||||
await manager.runActionAsync(new CreateProvisioningProfile(this.app));
|
||||
} else if (action === 'CHOOSE_EXISTING') {
|
||||
manager.pushNextAction(new UseExistingProvisioningProfile(this.app));
|
||||
await manager.runActionAsync(new UseExistingProvisioningProfile(this.app));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export class UpdateCredentialsJson implements Action {
|
||||
log('Updating iOS credentials in credentials.json');
|
||||
await updateIosCredentialsAsync(ctx, bundleIdentifer);
|
||||
log.succeed(
|
||||
'iOS part of your local credentials.json is synced with values stored on Expo servers.'
|
||||
'iOS part of your local credentials.json is synced with values stored on EAS servers.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
import assert from 'assert';
|
||||
|
||||
import { AppleDistributionCertificate } from '../../../../graphql/generated';
|
||||
import log from '../../../../log';
|
||||
import { Action, CredentialsManager } from '../../../CredentialsManager';
|
||||
import { Context } from '../../../context';
|
||||
import { AppLookupParams } from '../../api/GraphqlClient';
|
||||
import { AppleDistributionCertificateMutationResult } from '../../api/graphql/mutations/AppleDistributionCertificateMutation';
|
||||
import { provideOrGenerateDistributionCertificateAsync } from '../DistributionCertificateUtils';
|
||||
|
||||
export class CreateDistributionCertificate implements Action {
|
||||
private _distributionCertificate?: AppleDistributionCertificate;
|
||||
private _distributionCertificate?: AppleDistributionCertificateMutationResult;
|
||||
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
public get distributionCertificate(): AppleDistributionCertificate {
|
||||
public get distributionCertificate(): AppleDistributionCertificateMutationResult {
|
||||
assert(
|
||||
this._distributionCertificate,
|
||||
'distributionCertificate can be accessed only after calling .runAsync()'
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { AppleDevice } from '../../../../graphql/generated';
|
||||
import { APPLE_DEVICE_CLASS_LABELS } from '../../../../graphql/types/credentials/AppleDevice';
|
||||
import { promptAsync } from '../../.././../prompts';
|
||||
import { AppleDeviceFragmentWithAppleTeam } from '../../api/graphql/queries/AppleDeviceQuery';
|
||||
|
||||
export async function chooseDevices(
|
||||
allDevices: AppleDevice[],
|
||||
allDevices: AppleDeviceFragmentWithAppleTeam[],
|
||||
preselectedDeviceIdentifiers: string[] = []
|
||||
): Promise<AppleDevice[]> {
|
||||
const preselectedDeviceIdentifierSet = new Set(preselectedDeviceIdentifiers);
|
||||
const isSelected = (device: AppleDevice) =>
|
||||
const isSelected = (device: AppleDeviceFragmentWithAppleTeam) =>
|
||||
preselectedDeviceIdentifierSet.size === 0 ||
|
||||
preselectedDeviceIdentifierSet.has(device.identifier);
|
||||
const { devices } = await promptAsync({
|
||||
@@ -26,12 +27,12 @@ export async function chooseDevices(
|
||||
return devices;
|
||||
}
|
||||
|
||||
function formatDeviceLabel(device: AppleDevice): string {
|
||||
function formatDeviceLabel(device: AppleDeviceFragmentWithAppleTeam): string {
|
||||
const deviceDetails = formatDeviceDetails(device);
|
||||
return `${device.name ?? device.identifier}${deviceDetails !== '' ? ` ${deviceDetails}` : ''}`;
|
||||
}
|
||||
|
||||
function formatDeviceDetails(device: AppleDevice): string {
|
||||
function formatDeviceDetails(device: AppleDeviceFragmentWithAppleTeam): string {
|
||||
let details = '';
|
||||
if (device.deviceClass) {
|
||||
details += APPLE_DEVICE_CLASS_LABELS[device.deviceClass];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AppleDistributionCertificate, AppleTeam } from '../../../../graphql/generated';
|
||||
import { AppleTeamFragment } from '../../../../graphql/generated';
|
||||
import { AppleDistributionCertificateQueryResult } from '../../api/graphql/queries/AppleDistributionCertificateQuery';
|
||||
|
||||
export function formatDistributionCertificate({
|
||||
developerPortalIdentifier,
|
||||
@@ -6,7 +7,7 @@ export function formatDistributionCertificate({
|
||||
appleTeam,
|
||||
validityNotBefore,
|
||||
validityNotAfter,
|
||||
}: AppleDistributionCertificate): string {
|
||||
}: AppleDistributionCertificateQueryResult): string {
|
||||
let line: string = '';
|
||||
if (developerPortalIdentifier) {
|
||||
line += `Cert ID: ${developerPortalIdentifier}`;
|
||||
@@ -17,6 +18,6 @@ export function formatDistributionCertificate({
|
||||
return line;
|
||||
}
|
||||
|
||||
function formatAppleTeam({ appleTeamIdentifier, appleTeamName }: AppleTeam): string {
|
||||
function formatAppleTeam({ appleTeamIdentifier, appleTeamName }: AppleTeamFragment): string {
|
||||
return `Team ID: ${appleTeamIdentifier}${appleTeamName ? `, Team name: ${appleTeamName}` : ''}`;
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
import { AppleDevice, AppleDistributionCertificate } from '../../../../graphql/generated';
|
||||
import log from '../../../../log';
|
||||
import { AppleDeviceFragmentWithAppleTeam } from '../../api/graphql/queries/AppleDeviceQuery';
|
||||
import { AppleDistributionCertificateQueryResult } from '../../api/graphql/queries/AppleDistributionCertificateQuery';
|
||||
import { ProvisioningProfileStoreInfo } from '../../appstore/Credentials.types';
|
||||
|
||||
export function isDevPortalAdhocProfileValid(
|
||||
profileFromDevPortal: ProvisioningProfileStoreInfo | null,
|
||||
distCert: AppleDistributionCertificate,
|
||||
expectedDevices: AppleDevice[]
|
||||
distCert: AppleDistributionCertificateQueryResult,
|
||||
expectedDevices: AppleDeviceFragmentWithAppleTeam[]
|
||||
): boolean {
|
||||
if (!profileFromDevPortal) {
|
||||
return false;
|
||||
|
||||
@@ -3,13 +3,16 @@ import nullthrows from 'nullthrows';
|
||||
|
||||
import {
|
||||
AppleDevice,
|
||||
IosAppBuildCredentials,
|
||||
IosAppBuildCredentialsFragment,
|
||||
IosDistributionType,
|
||||
} from '../../../../graphql/generated';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import { Action, CredentialsManager } from '../../../CredentialsManager';
|
||||
import { Context } from '../../../context';
|
||||
import { AppLookupParams } from '../../api/GraphqlClient';
|
||||
import { AppleProvisioningProfileMutationResult } from '../../api/graphql/mutations/AppleProvisioningProfileMutation';
|
||||
import { AppleDeviceFragmentWithAppleTeam } from '../../api/graphql/queries/AppleDeviceQuery';
|
||||
import { AppleProvisioningProfileQueryResult } from '../../api/graphql/queries/AppleProvisioningProfileQuery';
|
||||
import { ProvisioningProfileStoreInfo } from '../../appstore/Credentials.types';
|
||||
import { ProfileClass } from '../../appstore/provisioningProfile';
|
||||
import { chooseDevices } from './DeviceUtils';
|
||||
@@ -17,11 +20,11 @@ import { doUDIDsMatch, isDevPortalAdhocProfileValid } from './ProvisioningProfil
|
||||
import { SetupDistributionCertificate } from './SetupDistributionCertificate';
|
||||
|
||||
export class SetupAdhocProvisioningProfile implements Action {
|
||||
private _iosAppBuildCredentials?: IosAppBuildCredentials;
|
||||
private _iosAppBuildCredentials?: IosAppBuildCredentialsFragment;
|
||||
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
public get iosAppBuildCredentials(): IosAppBuildCredentials {
|
||||
public get iosAppBuildCredentials(): IosAppBuildCredentialsFragment {
|
||||
assert(
|
||||
this._iosAppBuildCredentials,
|
||||
'iosAppBuildCredentials can be accessed only after calling .runAsync()'
|
||||
@@ -38,7 +41,7 @@ export class SetupAdhocProvisioningProfile implements Action {
|
||||
appleTeamName: authCtx.team.name,
|
||||
});
|
||||
|
||||
// 1. Fetch devices registered on Expo servers.
|
||||
// 1. Fetch devices registered on EAS servers.
|
||||
const registeredAppleDevices = await ctx.newIos.getDevicesForAppleTeamAsync(
|
||||
this.app,
|
||||
appleTeam
|
||||
@@ -55,15 +58,15 @@ export class SetupAdhocProvisioningProfile implements Action {
|
||||
await manager.runActionAsync(distCertAction);
|
||||
const distCert = distCertAction.distributionCertificate;
|
||||
|
||||
// 3. Fetch profile from Expo servers
|
||||
let currentProfileFromExpoServers = await ctx.newIos.getProvisioningProfileAsync(
|
||||
// 3. Fetch profile from EAS servers
|
||||
let currentProfileFromExpoServers = (await ctx.newIos.getProvisioningProfileAsync(
|
||||
this.app,
|
||||
appleTeam,
|
||||
IosDistributionType.AdHoc
|
||||
);
|
||||
)) as AppleProvisioningProfileQueryResult | AppleProvisioningProfileMutationResult | null;
|
||||
|
||||
// 4. Choose devices for internal distribution
|
||||
let chosenDevices: AppleDevice[];
|
||||
let chosenDevices: AppleDeviceFragmentWithAppleTeam[];
|
||||
if (currentProfileFromExpoServers) {
|
||||
const appleDeviceIdentifiersFromProfile = ((currentProfileFromExpoServers.appleDevices ??
|
||||
[]) as AppleDevice[]).map(({ identifier }) => identifier);
|
||||
|
||||
@@ -7,17 +7,19 @@ import { confirmAsync, promptAsync } from '../../../../prompts';
|
||||
import { Action, CredentialsManager } from '../../../CredentialsManager';
|
||||
import { Context } from '../../../context';
|
||||
import { AppLookupParams } from '../../api/GraphqlClient';
|
||||
import { AppleDistributionCertificateMutationResult } from '../../api/graphql/mutations/AppleDistributionCertificateMutation';
|
||||
import { AppleDistributionCertificateQueryResult } from '../../api/graphql/queries/AppleDistributionCertificateQuery';
|
||||
import { getValidCertSerialNumbers } from '../../appstore/CredentialsUtils';
|
||||
import { CreateDistributionCertificate } from './CreateDistributionCertificate';
|
||||
import { formatDistributionCertificate } from './DistributionCertificateUtils';
|
||||
|
||||
export class SetupDistributionCertificate implements Action {
|
||||
private validDistCerts?: AppleDistributionCertificate[];
|
||||
private _distributionCertificate?: AppleDistributionCertificate;
|
||||
private validDistCerts?: AppleDistributionCertificateQueryResult[];
|
||||
private _distributionCertificate?: AppleDistributionCertificateQueryResult;
|
||||
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
public get distributionCertificate(): AppleDistributionCertificate {
|
||||
public get distributionCertificate(): AppleDistributionCertificateQueryResult {
|
||||
assert(
|
||||
this._distributionCertificate,
|
||||
'distributionCertificate can be accessed only after calling .runAsync()'
|
||||
@@ -56,7 +58,7 @@ export class SetupDistributionCertificate implements Action {
|
||||
|
||||
private async isCurrentCertificateValidAsync(
|
||||
ctx: Context,
|
||||
currentCertificate: AppleDistributionCertificate | null
|
||||
currentCertificate: AppleDistributionCertificateQueryResult | null
|
||||
): Promise<boolean> {
|
||||
if (!currentCertificate) {
|
||||
return false;
|
||||
@@ -74,7 +76,7 @@ export class SetupDistributionCertificate implements Action {
|
||||
private async createOrReuseDistCert(
|
||||
manager: CredentialsManager,
|
||||
ctx: Context
|
||||
): Promise<AppleDistributionCertificate> {
|
||||
): Promise<AppleDistributionCertificateQueryResult> {
|
||||
const validDistCerts = await this.getValidDistCertsAsync(ctx);
|
||||
const autoselectedDistCert = validDistCerts[0];
|
||||
|
||||
@@ -111,7 +113,7 @@ export class SetupDistributionCertificate implements Action {
|
||||
|
||||
private async createNewDistCertAsync(
|
||||
manager: CredentialsManager
|
||||
): Promise<AppleDistributionCertificate> {
|
||||
): Promise<AppleDistributionCertificateMutationResult> {
|
||||
const action = new CreateDistributionCertificate(this.app);
|
||||
await manager.runActionAsync(action);
|
||||
return action.distributionCertificate;
|
||||
@@ -131,7 +133,9 @@ export class SetupDistributionCertificate implements Action {
|
||||
return distCert;
|
||||
}
|
||||
|
||||
private async getValidDistCertsAsync(ctx: Context): Promise<AppleDistributionCertificate[]> {
|
||||
private async getValidDistCertsAsync(
|
||||
ctx: Context
|
||||
): Promise<AppleDistributionCertificateQueryResult[]> {
|
||||
if (this.validDistCerts) {
|
||||
return this.validDistCerts;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,46 @@
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import {
|
||||
App,
|
||||
AppleAppIdentifier,
|
||||
AppleDevice,
|
||||
AppleDistributionCertificate,
|
||||
AppleProvisioningProfile,
|
||||
AppleTeam,
|
||||
IosAppBuildCredentials,
|
||||
IosAppCredentials,
|
||||
AppFragment,
|
||||
AppleAppIdentifierFragment,
|
||||
AppleTeamFragment,
|
||||
CommonIosAppCredentialsFragment,
|
||||
IosAppBuildCredentialsFragment,
|
||||
IosDistributionType,
|
||||
} from '../../../graphql/generated';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { DistributionCertificate } from '../appstore/Credentials.types';
|
||||
import { AppleAppIdentifierMutation } from './graphql/mutations/AppleAppIdentifierMutation';
|
||||
import { AppleDistributionCertificateMutation } from './graphql/mutations/AppleDistributionCertificateMutation';
|
||||
import { AppleProvisioningProfileMutation } from './graphql/mutations/AppleProvisioningProfileMutation';
|
||||
import {
|
||||
AppleDistributionCertificateMutation,
|
||||
AppleDistributionCertificateMutationResult,
|
||||
} from './graphql/mutations/AppleDistributionCertificateMutation';
|
||||
import {
|
||||
AppleProvisioningProfileMutation,
|
||||
AppleProvisioningProfileMutationResult,
|
||||
} from './graphql/mutations/AppleProvisioningProfileMutation';
|
||||
import { AppleTeamMutation } from './graphql/mutations/AppleTeamMutation';
|
||||
import { IosAppBuildCredentialsMutation } from './graphql/mutations/IosAppBuildCredentialsMutation';
|
||||
import { IosAppCredentialsMutation } from './graphql/mutations/IosAppCredentialsMutation';
|
||||
import { AppQuery } from './graphql/queries/AppQuery';
|
||||
import { AppleAppIdentifierQuery } from './graphql/queries/AppleAppIdentifierQuery';
|
||||
import { AppleDeviceQuery } from './graphql/queries/AppleDeviceQuery';
|
||||
import { AppleDistributionCertificateQuery } from './graphql/queries/AppleDistributionCertificateQuery';
|
||||
import { AppleProvisioningProfileQuery } from './graphql/queries/AppleProvisioningProfileQuery';
|
||||
import {
|
||||
AppleDeviceFragmentWithAppleTeam,
|
||||
AppleDeviceQuery,
|
||||
} from './graphql/queries/AppleDeviceQuery';
|
||||
import {
|
||||
AppleDistributionCertificateQuery,
|
||||
AppleDistributionCertificateQueryResult,
|
||||
} from './graphql/queries/AppleDistributionCertificateQuery';
|
||||
import {
|
||||
AppleProvisioningProfileQuery,
|
||||
AppleProvisioningProfileQueryResult,
|
||||
} from './graphql/queries/AppleProvisioningProfileQuery';
|
||||
import { AppleTeamQuery } from './graphql/queries/AppleTeamQuery';
|
||||
import { IosAppCredentialsQuery } from './graphql/queries/IosAppCredentialsQuery';
|
||||
import {
|
||||
IosAppCredentialsQuery,
|
||||
IosAppCredentialsWithBuildCredentialsQueryResult,
|
||||
} from './graphql/queries/IosAppCredentialsQuery';
|
||||
|
||||
export interface AppLookupParams {
|
||||
account: Account;
|
||||
@@ -33,7 +48,7 @@ export interface AppLookupParams {
|
||||
bundleIdentifier: string;
|
||||
}
|
||||
|
||||
export async function getAppAsync(appLookupParams: AppLookupParams): Promise<App> {
|
||||
export async function getAppAsync(appLookupParams: AppLookupParams): Promise<AppFragment> {
|
||||
const projectFullName = formatProjectFullName(appLookupParams);
|
||||
return await AppQuery.byFullNameAsync(projectFullName);
|
||||
}
|
||||
@@ -47,13 +62,13 @@ export async function createOrUpdateIosAppBuildCredentialsAsync(
|
||||
appleProvisioningProfileId,
|
||||
appleDistributionCertificateId,
|
||||
}: {
|
||||
appleTeam: AppleTeam;
|
||||
appleTeam: AppleTeamFragment;
|
||||
appleAppIdentifierId: string;
|
||||
iosDistributionType: IosDistributionType;
|
||||
appleProvisioningProfileId: string;
|
||||
appleDistributionCertificateId: string;
|
||||
}
|
||||
): Promise<IosAppBuildCredentials> {
|
||||
): Promise<IosAppBuildCredentialsFragment> {
|
||||
const iosAppCredentials = await createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync(
|
||||
appLookupParams,
|
||||
{
|
||||
@@ -84,6 +99,23 @@ export async function createOrUpdateIosAppBuildCredentialsAsync(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getIosAppCredentialsWithBuildCredentialsAsync(
|
||||
appLookupParams: AppLookupParams
|
||||
): Promise<CommonIosAppCredentialsFragment | null> {
|
||||
const { account, bundleIdentifier } = appLookupParams;
|
||||
const appleAppIdentifier = await AppleAppIdentifierQuery.byBundleIdentifierAsync(
|
||||
account.name,
|
||||
bundleIdentifier
|
||||
);
|
||||
if (!appleAppIdentifier) {
|
||||
return null;
|
||||
}
|
||||
const projectFullName = formatProjectFullName(appLookupParams);
|
||||
return await IosAppCredentialsQuery.withCommonFieldsByAppIdentifierIdAsync(projectFullName, {
|
||||
appleAppIdentifierId: appleAppIdentifier.id,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync(
|
||||
appLookupParams: AppLookupParams,
|
||||
{
|
||||
@@ -91,11 +123,11 @@ export async function createOrGetExistingIosAppCredentialsWithBuildCredentialsAs
|
||||
appleAppIdentifierId,
|
||||
iosDistributionType,
|
||||
}: {
|
||||
appleTeam: AppleTeam;
|
||||
appleTeam: AppleTeamFragment;
|
||||
appleAppIdentifierId: string;
|
||||
iosDistributionType: IosDistributionType;
|
||||
}
|
||||
): Promise<IosAppCredentials> {
|
||||
): Promise<IosAppCredentialsWithBuildCredentialsQueryResult> {
|
||||
const projectFullName = formatProjectFullName(appLookupParams);
|
||||
const maybeIosAppCredentials = await IosAppCredentialsQuery.withBuildCredentialsByAppIdentifierIdAsync(
|
||||
projectFullName,
|
||||
@@ -126,7 +158,7 @@ export async function createOrGetExistingIosAppCredentialsWithBuildCredentialsAs
|
||||
export async function createOrGetExistingAppleTeamAsync(
|
||||
{ account }: AppLookupParams,
|
||||
{ appleTeamIdentifier, appleTeamName }: { appleTeamIdentifier: string; appleTeamName?: string }
|
||||
): Promise<AppleTeam> {
|
||||
): Promise<AppleTeamFragment> {
|
||||
const appleTeam = await AppleTeamQuery.getByAppleTeamIdentifierAsync(
|
||||
account.id,
|
||||
appleTeamIdentifier
|
||||
@@ -143,8 +175,8 @@ export async function createOrGetExistingAppleTeamAsync(
|
||||
|
||||
export async function createOrGetExistingAppleAppIdentifierAsync(
|
||||
{ account, bundleIdentifier }: AppLookupParams,
|
||||
appleTeam: AppleTeam
|
||||
): Promise<AppleAppIdentifier> {
|
||||
appleTeam: AppleTeamFragment
|
||||
): Promise<AppleAppIdentifierFragment> {
|
||||
const appleAppIdentifier = await AppleAppIdentifierQuery.byBundleIdentifierAsync(
|
||||
account.name,
|
||||
bundleIdentifier
|
||||
@@ -161,19 +193,19 @@ export async function createOrGetExistingAppleAppIdentifierAsync(
|
||||
|
||||
export async function getDevicesForAppleTeamAsync(
|
||||
{ account }: AppLookupParams,
|
||||
{ appleTeamIdentifier }: AppleTeam
|
||||
): Promise<AppleDevice[]> {
|
||||
{ appleTeamIdentifier }: AppleTeamFragment
|
||||
): Promise<AppleDeviceFragmentWithAppleTeam[]> {
|
||||
return await AppleDeviceQuery.getAllByAppleTeamIdentifierAsync(account.id, appleTeamIdentifier);
|
||||
}
|
||||
|
||||
export async function createProvisioningProfileAsync(
|
||||
{ account }: AppLookupParams,
|
||||
appleAppIdentifier: AppleAppIdentifier,
|
||||
appleAppIdentifier: AppleAppIdentifierFragment,
|
||||
appleProvisioningProfileInput: {
|
||||
appleProvisioningProfile: string;
|
||||
developerPortalIdentifier?: string;
|
||||
}
|
||||
): Promise<AppleProvisioningProfile> {
|
||||
): Promise<AppleProvisioningProfileMutationResult> {
|
||||
return await AppleProvisioningProfileMutation.createAppleProvisioningProfileAsync(
|
||||
appleProvisioningProfileInput,
|
||||
account.id,
|
||||
@@ -183,9 +215,9 @@ export async function createProvisioningProfileAsync(
|
||||
|
||||
export async function getProvisioningProfileAsync(
|
||||
appLookupParams: AppLookupParams,
|
||||
appleTeam: AppleTeam,
|
||||
appleTeam: AppleTeamFragment,
|
||||
iosDistributionType: IosDistributionType
|
||||
): Promise<AppleProvisioningProfile | null> {
|
||||
): Promise<AppleProvisioningProfileQueryResult | null> {
|
||||
const projectFullName = formatProjectFullName(appLookupParams);
|
||||
const appleAppIdentifier = await createOrGetExistingAppleAppIdentifierAsync(
|
||||
appLookupParams,
|
||||
@@ -203,7 +235,7 @@ export async function updateProvisioningProfileAsync(
|
||||
appleProvisioningProfile: string;
|
||||
developerPortalIdentifier?: string;
|
||||
}
|
||||
): Promise<AppleProvisioningProfile> {
|
||||
): Promise<AppleProvisioningProfileMutationResult> {
|
||||
return await AppleProvisioningProfileMutation.updateAppleProvisioningProfileAsync(
|
||||
appleProvisioningProfileId,
|
||||
appleProvisioningProfileInput
|
||||
@@ -212,9 +244,9 @@ export async function updateProvisioningProfileAsync(
|
||||
|
||||
export async function getDistributionCertificateForAppAsync(
|
||||
appLookupParams: AppLookupParams,
|
||||
appleTeam: AppleTeam,
|
||||
appleTeam: AppleTeamFragment,
|
||||
iosDistributionType: IosDistributionType
|
||||
): Promise<AppleDistributionCertificate | null> {
|
||||
): Promise<AppleDistributionCertificateQueryResult | null> {
|
||||
const projectFullName = formatProjectFullName(appLookupParams);
|
||||
const appleAppIdentifier = await createOrGetExistingAppleAppIdentifierAsync(
|
||||
appLookupParams,
|
||||
@@ -228,14 +260,14 @@ export async function getDistributionCertificateForAppAsync(
|
||||
|
||||
export async function getDistributionCertificatesForAccountAsync({
|
||||
account,
|
||||
}: AppLookupParams): Promise<AppleDistributionCertificate[]> {
|
||||
}: AppLookupParams): Promise<AppleDistributionCertificateQueryResult[]> {
|
||||
return await AppleDistributionCertificateQuery.getAllForAccount(account.name);
|
||||
}
|
||||
|
||||
export async function createDistributionCertificateAsync(
|
||||
appLookupParams: AppLookupParams,
|
||||
distCert: DistributionCertificate
|
||||
): Promise<AppleDistributionCertificate> {
|
||||
): Promise<AppleDistributionCertificateMutationResult> {
|
||||
const appleTeam = await createOrGetExistingAppleTeamAsync(appLookupParams, {
|
||||
appleTeamIdentifier: distCert.teamId,
|
||||
appleTeamName: distCert.teamName,
|
||||
|
||||
+24
-8
@@ -1,8 +1,13 @@
|
||||
import assert from 'assert';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import { AppleAppIdentifier } from '../../../../../graphql/generated';
|
||||
import { AppleAppIdentifierFragment } from '../../../../../graphql/types/credentials/AppleAppIdentifier';
|
||||
import {
|
||||
AppleAppIdentifierFragment,
|
||||
CreateAppleAppIdentifierMutation,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AppleAppIdentifierFragmentNode } from '../../../../../graphql/types/credentials/AppleAppIdentifier';
|
||||
|
||||
const AppleAppIdentifierMutation = {
|
||||
async createAppleAppIdentifierAsync(
|
||||
@@ -11,19 +16,26 @@ const AppleAppIdentifierMutation = {
|
||||
appleTeamId?: string;
|
||||
},
|
||||
accountId: string
|
||||
): Promise<AppleAppIdentifier> {
|
||||
): Promise<AppleAppIdentifierFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<{ appleAppIdentifier: { createAppleAppIdentifier: AppleAppIdentifier } }>(
|
||||
.mutation<CreateAppleAppIdentifierMutation>(
|
||||
gql`
|
||||
mutation AppleAppIdentifierMutation($appleAppIdentifierInput: AppleAppIdentifierInput!, $accountId: ID!) {
|
||||
mutation CreateAppleAppIdentifierMutation(
|
||||
$appleAppIdentifierInput: AppleAppIdentifierInput!
|
||||
$accountId: ID!
|
||||
) {
|
||||
appleAppIdentifier {
|
||||
createAppleAppIdentifier(appleAppIdentifierInput: $appleAppIdentifierInput, accountId: $accountId) {
|
||||
...${AppleAppIdentifierFragment.name}
|
||||
createAppleAppIdentifier(
|
||||
appleAppIdentifierInput: $appleAppIdentifierInput
|
||||
accountId: $accountId
|
||||
) {
|
||||
id
|
||||
...AppleAppIdentifierFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${AppleAppIdentifierFragment.definition}
|
||||
${print(AppleAppIdentifierFragmentNode)}
|
||||
`,
|
||||
{
|
||||
appleAppIdentifierInput,
|
||||
@@ -32,6 +44,10 @@ const AppleAppIdentifierMutation = {
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(
|
||||
data.appleAppIdentifier.createAppleAppIdentifier,
|
||||
'GraphQL: `createAppleAppIdentifier` not defined in server response'
|
||||
);
|
||||
return data.appleAppIdentifier.createAppleAppIdentifier;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import { AppleDevice, AppleDeviceClass } from '../../../../../graphql/generated';
|
||||
import { AppleDeviceFragment } from '../../../../../graphql/types/credentials/AppleDevice';
|
||||
import {
|
||||
AppleDeviceClass,
|
||||
AppleDeviceFragment,
|
||||
CreateAppleDeviceMutation,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AppleDeviceFragmentNode } from '../../../../../graphql/types/credentials/AppleDevice';
|
||||
|
||||
const AppleDeviceMutation = {
|
||||
async createAppleDeviceAsync(
|
||||
@@ -13,19 +18,23 @@ const AppleDeviceMutation = {
|
||||
deviceClass?: AppleDeviceClass;
|
||||
},
|
||||
accountId: string
|
||||
): Promise<AppleDevice> {
|
||||
): Promise<AppleDeviceFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<{ appleDevice: { createAppleDevice: AppleDevice } }>(
|
||||
.mutation<CreateAppleDeviceMutation>(
|
||||
gql`
|
||||
mutation AppleDeviceMutation($appleDeviceInput: AppleDeviceInput!, $accountId: ID!) {
|
||||
mutation CreateAppleDeviceMutation(
|
||||
$appleDeviceInput: AppleDeviceInput!
|
||||
$accountId: ID!
|
||||
) {
|
||||
appleDevice {
|
||||
createAppleDevice(appleDeviceInput: $appleDeviceInput, accountId: $accountId) {
|
||||
...${AppleDeviceFragment.name}
|
||||
id
|
||||
...AppleDeviceFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${AppleDeviceFragment.definition}
|
||||
${print(AppleDeviceFragmentNode)}
|
||||
`,
|
||||
{
|
||||
appleDeviceInput,
|
||||
|
||||
+15
-11
@@ -1,33 +1,37 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import { AppleDeviceRegistrationRequest } from '../../../../../graphql/generated';
|
||||
import { AppleDeviceRegistrationRequestFragment } from '../../../../../graphql/types/credentials/AppleDeviceRegistrationRequest';
|
||||
import {
|
||||
AppleDeviceRegistrationRequestFragment,
|
||||
CreateAppleDeviceRegistrationRequestMutation,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AppleDeviceRegistrationRequestFragmentNode } from '../../../../../graphql/types/credentials/AppleDeviceRegistrationRequest';
|
||||
|
||||
const AppleDeviceRegistrationRequestMutation = {
|
||||
async createAppleDeviceRegistrationRequestAsync(
|
||||
appleTeamId: string,
|
||||
accountId: string
|
||||
): Promise<AppleDeviceRegistrationRequest> {
|
||||
): Promise<AppleDeviceRegistrationRequestFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<{
|
||||
appleDeviceRegistrationRequest: {
|
||||
createAppleDeviceRegistrationRequest: AppleDeviceRegistrationRequest;
|
||||
};
|
||||
}>(
|
||||
.mutation<CreateAppleDeviceRegistrationRequestMutation>(
|
||||
gql`
|
||||
mutation AppleDeviceRegistrationRequestMutation($appleTeamId: ID!, $accountId: ID!) {
|
||||
mutation CreateAppleDeviceRegistrationRequestMutation(
|
||||
$appleTeamId: ID!
|
||||
$accountId: ID!
|
||||
) {
|
||||
appleDeviceRegistrationRequest {
|
||||
createAppleDeviceRegistrationRequest(
|
||||
appleTeamId: $appleTeamId
|
||||
accountId: $accountId
|
||||
) {
|
||||
...${AppleDeviceRegistrationRequestFragment.name}
|
||||
id
|
||||
...AppleDeviceRegistrationRequestFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${AppleDeviceRegistrationRequestFragment.definition}
|
||||
${print(AppleDeviceRegistrationRequestFragmentNode)}
|
||||
`,
|
||||
{
|
||||
appleTeamId,
|
||||
|
||||
+26
-14
@@ -1,9 +1,19 @@
|
||||
import assert from 'assert';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import { AppleDistributionCertificate } from '../../../../../graphql/generated';
|
||||
import { AppleDistributionCertificateFragment } from '../../../../../graphql/types/credentials/AppleDistributionCertificate';
|
||||
import { AppleTeamFragment } from '../../../../../graphql/types/credentials/AppleTeam';
|
||||
import {
|
||||
AppleDistributionCertificateFragment,
|
||||
AppleTeamFragment,
|
||||
CreateAppleDistributionCertificateMutation,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AppleDistributionCertificateFragmentNode } from '../../../../../graphql/types/credentials/AppleDistributionCertificate';
|
||||
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
|
||||
|
||||
export type AppleDistributionCertificateMutationResult = AppleDistributionCertificateFragment & {
|
||||
appleTeam?: AppleTeamFragment | null;
|
||||
};
|
||||
|
||||
const AppleDistributionCertificateMutation = {
|
||||
async createAppleDistributionCertificate(
|
||||
@@ -15,16 +25,12 @@ const AppleDistributionCertificateMutation = {
|
||||
appleTeamId?: string;
|
||||
},
|
||||
accountId: string
|
||||
): Promise<AppleDistributionCertificate> {
|
||||
): Promise<AppleDistributionCertificateMutationResult> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<{
|
||||
appleDistributionCertificate: {
|
||||
createAppleDistributionCertificate: AppleDistributionCertificate;
|
||||
};
|
||||
}>(
|
||||
.mutation<CreateAppleDistributionCertificateMutation>(
|
||||
gql`
|
||||
mutation AppleDistributionCertificateMutation(
|
||||
mutation CreateAppleDistributionCertificateMutation(
|
||||
$appleDistributionCertificateInput: AppleDistributionCertificateInput!
|
||||
$accountId: ID!
|
||||
) {
|
||||
@@ -33,15 +39,17 @@ const AppleDistributionCertificateMutation = {
|
||||
appleDistributionCertificateInput: $appleDistributionCertificateInput
|
||||
accountId: $accountId
|
||||
) {
|
||||
...${AppleDistributionCertificateFragment.name}
|
||||
id
|
||||
...AppleDistributionCertificateFragment
|
||||
appleTeam {
|
||||
...${AppleTeamFragment.name}
|
||||
id
|
||||
...AppleTeamFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${AppleDistributionCertificateFragment.definition}
|
||||
${AppleTeamFragment.definition}
|
||||
${print(AppleDistributionCertificateFragmentNode)}
|
||||
${print(AppleTeamFragmentNode)}
|
||||
`,
|
||||
{
|
||||
appleDistributionCertificateInput,
|
||||
@@ -50,6 +58,10 @@ const AppleDistributionCertificateMutation = {
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(
|
||||
data.appleDistributionCertificate.createAppleDistributionCertificate,
|
||||
'GraphQL: `createAppleDistributionCertificate` not defined in server response'
|
||||
);
|
||||
return data.appleDistributionCertificate.createAppleDistributionCertificate;
|
||||
},
|
||||
};
|
||||
|
||||
+31
-21
@@ -1,9 +1,19 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import { AppleProvisioningProfile } from '../../../../../graphql/generated';
|
||||
import { AppleProvisioningProfileFragment } from '../../../../../graphql/types/credentials/AppleProvisioningProfile';
|
||||
import { AppleTeamFragment } from '../../../../../graphql/types/credentials/AppleTeam';
|
||||
import {
|
||||
AppleProvisioningProfileFragment,
|
||||
AppleTeamFragment,
|
||||
CreateAppleProvisioningProfileMutation,
|
||||
UpdateAppleProvisioningProfileMutation,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AppleProvisioningProfileFragmentNode } from '../../../../../graphql/types/credentials/AppleProvisioningProfile';
|
||||
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
|
||||
|
||||
export type AppleProvisioningProfileMutationResult = AppleProvisioningProfileFragment & {
|
||||
appleTeam?: AppleTeamFragment | null;
|
||||
};
|
||||
|
||||
const AppleProvisioningProfileMutation = {
|
||||
async createAppleProvisioningProfileAsync(
|
||||
@@ -13,14 +23,12 @@ const AppleProvisioningProfileMutation = {
|
||||
},
|
||||
accountId: string,
|
||||
appleAppIdentifierId: string
|
||||
): Promise<AppleProvisioningProfile> {
|
||||
): Promise<AppleProvisioningProfileMutationResult> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<{
|
||||
appleProvisioningProfile: { createAppleProvisioningProfile: AppleProvisioningProfile };
|
||||
}>(
|
||||
.mutation<CreateAppleProvisioningProfileMutation>(
|
||||
gql`
|
||||
mutation AppleProvisioningProfileMutation(
|
||||
mutation CreateAppleProvisioningProfileMutation(
|
||||
$appleProvisioningProfileInput: AppleProvisioningProfileInput!
|
||||
$accountId: ID!
|
||||
$appleAppIdentifierId: ID!
|
||||
@@ -31,15 +39,17 @@ const AppleProvisioningProfileMutation = {
|
||||
accountId: $accountId
|
||||
appleAppIdentifierId: $appleAppIdentifierId
|
||||
) {
|
||||
...${AppleProvisioningProfileFragment.name}
|
||||
id
|
||||
...AppleProvisioningProfileFragment
|
||||
appleTeam {
|
||||
...${AppleTeamFragment.name}
|
||||
id
|
||||
...AppleTeamFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${AppleProvisioningProfileFragment.definition}
|
||||
${AppleTeamFragment.definition}
|
||||
${print(AppleProvisioningProfileFragmentNode)}
|
||||
${print(AppleTeamFragmentNode)}
|
||||
`,
|
||||
{
|
||||
appleProvisioningProfileInput,
|
||||
@@ -57,14 +67,12 @@ const AppleProvisioningProfileMutation = {
|
||||
appleProvisioningProfile: string;
|
||||
developerPortalIdentifier?: string;
|
||||
}
|
||||
): Promise<AppleProvisioningProfile> {
|
||||
): Promise<AppleProvisioningProfileMutationResult> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<{
|
||||
appleProvisioningProfile: { updateAppleProvisioningProfile: AppleProvisioningProfile };
|
||||
}>(
|
||||
.mutation<UpdateAppleProvisioningProfileMutation>(
|
||||
gql`
|
||||
mutation AppleProvisioningProfileMutation(
|
||||
mutation UpdateAppleProvisioningProfileMutation(
|
||||
$appleProvisioningProfileId: ID!
|
||||
$appleProvisioningProfileInput: AppleProvisioningProfileInput!
|
||||
) {
|
||||
@@ -73,15 +81,17 @@ const AppleProvisioningProfileMutation = {
|
||||
id: $appleProvisioningProfileId
|
||||
appleProvisioningProfileInput: $appleProvisioningProfileInput
|
||||
) {
|
||||
...${AppleProvisioningProfileFragment.name}
|
||||
id
|
||||
...AppleProvisioningProfileFragment
|
||||
appleTeam {
|
||||
...${AppleTeamFragment.name}
|
||||
id
|
||||
...AppleTeamFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${AppleProvisioningProfileFragment.definition}
|
||||
${AppleTeamFragment.definition}
|
||||
${print(AppleProvisioningProfileFragmentNode)}
|
||||
${print(AppleTeamFragmentNode)}
|
||||
`,
|
||||
{
|
||||
appleProvisioningProfileId,
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import { AppleTeam } from '../../../../../graphql/generated';
|
||||
import { AppleTeamFragment } from '../../../../../graphql/types/credentials/AppleTeam';
|
||||
import { AppleTeamFragment, CreateAppleTeamMutation } from '../../../../../graphql/generated';
|
||||
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
|
||||
import { Account } from '../../../../../user/Account';
|
||||
|
||||
export type AppleTeamMutationResult = AppleTeamFragment & {
|
||||
account: Account;
|
||||
};
|
||||
|
||||
const AppleTeamMutation = {
|
||||
async createAppleTeamAsync(
|
||||
@@ -11,15 +17,16 @@ const AppleTeamMutation = {
|
||||
appleTeamName?: string;
|
||||
},
|
||||
accountId: string
|
||||
): Promise<AppleTeam> {
|
||||
): Promise<AppleTeamMutationResult> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<{ appleTeam: { createAppleTeam: AppleTeam } }>(
|
||||
.mutation<CreateAppleTeamMutation>(
|
||||
gql`
|
||||
mutation AppleTeamMutation($appleTeamInput: AppleTeamInput!, $accountId: ID!) {
|
||||
mutation CreateAppleTeamMutation($appleTeamInput: AppleTeamInput!, $accountId: ID!) {
|
||||
appleTeam {
|
||||
createAppleTeam(appleTeamInput: $appleTeamInput, accountId: $accountId) {
|
||||
...${AppleTeamFragment.name}
|
||||
id
|
||||
...AppleTeamFragment
|
||||
account {
|
||||
id
|
||||
name
|
||||
@@ -27,7 +34,7 @@ const AppleTeamMutation = {
|
||||
}
|
||||
}
|
||||
}
|
||||
${AppleTeamFragment.definition}
|
||||
${print(AppleTeamFragmentNode)}
|
||||
`,
|
||||
{
|
||||
appleTeamInput,
|
||||
|
||||
+48
-18
@@ -1,8 +1,15 @@
|
||||
import assert from 'assert';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import { IosAppBuildCredentials, IosDistributionType } from '../../../../../graphql/generated';
|
||||
import { IosAppBuildCredentialsFragment } from '../../../../../graphql/types/credentials/IosAppBuildCredentials';
|
||||
import {
|
||||
IosAppBuildCredentials,
|
||||
IosAppBuildCredentialsFragment,
|
||||
IosDistributionType,
|
||||
SetProvisioningProfileMutation,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { IosAppBuildCredentialsFragmentNode } from '../../../../../graphql/types/credentials/IosAppBuildCredentials';
|
||||
|
||||
const IosAppBuildCredentialsMutation = {
|
||||
async createIosAppBuildCredentialsAsync(
|
||||
@@ -19,14 +26,21 @@ const IosAppBuildCredentialsMutation = {
|
||||
iosAppBuildCredentials: { createIosAppBuildCredentials: IosAppBuildCredentials };
|
||||
}>(
|
||||
gql`
|
||||
mutation IosAppBuildCredentialsMutation($iosAppBuildCredentialsInput: IosAppBuildCredentialsInput!, $iosAppCredentialsId: ID!) {
|
||||
mutation CreateIosAppBuildCredentialsMutation(
|
||||
$iosAppBuildCredentialsInput: IosAppBuildCredentialsInput!
|
||||
$iosAppCredentialsId: ID!
|
||||
) {
|
||||
iosAppBuildCredentials {
|
||||
createIosAppBuildCredentials(iosAppBuildCredentialsInput: $iosAppBuildCredentialsInput, iosAppCredentialsId: $iosAppCredentialsId) {
|
||||
...${IosAppBuildCredentialsFragment.name}
|
||||
createIosAppBuildCredentials(
|
||||
iosAppBuildCredentialsInput: $iosAppBuildCredentialsInput
|
||||
iosAppCredentialsId: $iosAppCredentialsId
|
||||
) {
|
||||
id
|
||||
...IosAppBuildCredentialsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${IosAppBuildCredentialsFragment.definition}
|
||||
${print(IosAppBuildCredentialsFragmentNode)}
|
||||
`,
|
||||
{
|
||||
iosAppBuildCredentialsInput,
|
||||
@@ -47,14 +61,21 @@ const IosAppBuildCredentialsMutation = {
|
||||
iosAppBuildCredentials: { setDistributionCertificate: IosAppBuildCredentials };
|
||||
}>(
|
||||
gql`
|
||||
mutation IosAppBuildCredentialsMutation($iosAppBuildCredentialsId: ID!, $distributionCertificateId: ID!) {
|
||||
mutation SetDistributionCertificateMutation(
|
||||
$iosAppBuildCredentialsId: ID!
|
||||
$distributionCertificateId: ID!
|
||||
) {
|
||||
iosAppBuildCredentials {
|
||||
setDistributionCertificate(id: $iosAppBuildCredentialsId, distributionCertificateId: $distributionCertificateId) {
|
||||
...${IosAppBuildCredentialsFragment.name}
|
||||
setDistributionCertificate(
|
||||
id: $iosAppBuildCredentialsId
|
||||
distributionCertificateId: $distributionCertificateId
|
||||
) {
|
||||
id
|
||||
...IosAppBuildCredentialsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${IosAppBuildCredentialsFragment.definition}
|
||||
${print(IosAppBuildCredentialsFragmentNode)}
|
||||
`,
|
||||
{
|
||||
iosAppBuildCredentialsId,
|
||||
@@ -68,21 +89,26 @@ const IosAppBuildCredentialsMutation = {
|
||||
async setProvisioningProfileAsync(
|
||||
iosAppBuildCredentialsId: string,
|
||||
provisioningProfileId: string
|
||||
): Promise<IosAppBuildCredentials> {
|
||||
): Promise<IosAppBuildCredentialsFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<{
|
||||
iosAppBuildCredentials: { setProvisioningProfile: IosAppBuildCredentials };
|
||||
}>(
|
||||
.mutation<SetProvisioningProfileMutation>(
|
||||
gql`
|
||||
mutation IosAppBuildCredentialsMutation($iosAppBuildCredentialsId: ID!, $provisioningProfileId: ID!) {
|
||||
mutation SetProvisioningProfileMutation(
|
||||
$iosAppBuildCredentialsId: ID!
|
||||
$provisioningProfileId: ID!
|
||||
) {
|
||||
iosAppBuildCredentials {
|
||||
setProvisioningProfile(id: $iosAppBuildCredentialsId, provisioningProfileId: $provisioningProfileId) {
|
||||
...${IosAppBuildCredentialsFragment.name}
|
||||
setProvisioningProfile(
|
||||
id: $iosAppBuildCredentialsId
|
||||
provisioningProfileId: $provisioningProfileId
|
||||
) {
|
||||
id
|
||||
...IosAppBuildCredentialsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${IosAppBuildCredentialsFragment.definition}
|
||||
${print(IosAppBuildCredentialsFragmentNode)}
|
||||
`,
|
||||
{
|
||||
iosAppBuildCredentialsId,
|
||||
@@ -91,6 +117,10 @@ const IosAppBuildCredentialsMutation = {
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(
|
||||
data.iosAppBuildCredentials.setProvisioningProfile,
|
||||
'GraphQL: `setProvisioningProfile` not defined in server response'
|
||||
);
|
||||
return data.iosAppBuildCredentials.setProvisioningProfile;
|
||||
},
|
||||
};
|
||||
|
||||
+26
-8
@@ -1,8 +1,13 @@
|
||||
import assert from 'assert';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import { IosAppCredentials } from '../../../../../graphql/generated';
|
||||
import { IosAppCredentialsFragment } from '../../../../../graphql/types/credentials/IosAppCredentials';
|
||||
import {
|
||||
CreateIosAppCredentialsMutation,
|
||||
IosAppCredentialsFragment,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { IosAppCredentialsFragmentNode } from '../../../../../graphql/types/credentials/IosAppCredentials';
|
||||
|
||||
const IosAppCredentialsMutation = {
|
||||
async createIosAppCredentialsAsync(
|
||||
@@ -12,19 +17,28 @@ const IosAppCredentialsMutation = {
|
||||
},
|
||||
appId: string,
|
||||
appleAppIdentifierId: string
|
||||
): Promise<IosAppCredentials> {
|
||||
): Promise<IosAppCredentialsFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<{ iosAppCredentials: { createIosAppCredentials: IosAppCredentials } }>(
|
||||
.mutation<CreateIosAppCredentialsMutation>(
|
||||
gql`
|
||||
mutation IosAppCredentialsMutation($iosAppCredentialsInput: IosAppCredentialsInput!, $appId: ID!, $appleAppIdentifierId: ID!) {
|
||||
mutation CreateIosAppCredentialsMutation(
|
||||
$iosAppCredentialsInput: IosAppCredentialsInput!
|
||||
$appId: ID!
|
||||
$appleAppIdentifierId: ID!
|
||||
) {
|
||||
iosAppCredentials {
|
||||
createIosAppCredentials(iosAppCredentialsInput: $iosAppCredentialsInput, appId: $appId, appleAppIdentifierId: $appleAppIdentifierId) {
|
||||
...${IosAppCredentialsFragment.name}
|
||||
createIosAppCredentials(
|
||||
iosAppCredentialsInput: $iosAppCredentialsInput
|
||||
appId: $appId
|
||||
appleAppIdentifierId: $appleAppIdentifierId
|
||||
) {
|
||||
id
|
||||
...IosAppCredentialsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${IosAppCredentialsFragment.definition}
|
||||
${print(IosAppCredentialsFragmentNode)}
|
||||
`,
|
||||
{
|
||||
iosAppCredentialsInput,
|
||||
@@ -34,6 +48,10 @@ const IosAppCredentialsMutation = {
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(
|
||||
data.iosAppCredentials.createIosAppCredentials,
|
||||
'GraphQL: `createIosAppCredentials` not defined in server response'
|
||||
);
|
||||
return data.iosAppCredentials.createIosAppCredentials;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
import assert from 'assert';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import { App } from '../../../../../graphql/generated';
|
||||
import { AppFragment } from '../../../../../graphql/types/App';
|
||||
import { AppByFullNameQuery, AppFragment } from '../../../../../graphql/generated';
|
||||
import { AppFragmentNode } from '../../../../../graphql/types/App';
|
||||
|
||||
const AppQuery = {
|
||||
async byFullNameAsync(fullName: string): Promise<App> {
|
||||
async byFullNameAsync(fullName: string): Promise<AppFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{ app: { byFullName: App } }>(
|
||||
.query<AppByFullNameQuery>(
|
||||
gql`
|
||||
query($fullName: String!) {
|
||||
query AppByFullNameQuery($fullName: String!) {
|
||||
app {
|
||||
byFullName(fullName: $fullName) {
|
||||
...${AppFragment.name}
|
||||
id
|
||||
...AppFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${AppFragment.definition}
|
||||
${print(AppFragmentNode)}
|
||||
`,
|
||||
{ fullName }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
assert(data.app, 'GraphQL: `app` not defined in server response');
|
||||
return data.app.byFullName;
|
||||
},
|
||||
};
|
||||
|
||||
+16
-7
@@ -1,28 +1,37 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import { AppleAppIdentifier } from '../../../../../graphql/generated';
|
||||
import { AppleAppIdentifierFragment } from '../../../../../graphql/types/credentials/AppleAppIdentifier';
|
||||
import {
|
||||
AppleAppIdentifierByBundleIdQuery,
|
||||
AppleAppIdentifierFragment,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AppleAppIdentifierFragmentNode } from '../../../../../graphql/types/credentials/AppleAppIdentifier';
|
||||
|
||||
const AppleAppIdentifierQuery = {
|
||||
async byBundleIdentifierAsync(
|
||||
accountName: string,
|
||||
bundleIdentifier: string
|
||||
): Promise<AppleAppIdentifier | null> {
|
||||
): Promise<AppleAppIdentifierFragment | null> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{ account: { byName: { appleAppIdentifiers: AppleAppIdentifier[] } } }>(
|
||||
.query<AppleAppIdentifierByBundleIdQuery>(
|
||||
gql`
|
||||
query($accountName: String!, $bundleIdentifier: String!) {
|
||||
query AppleAppIdentifierByBundleIdQuery(
|
||||
$accountName: String!
|
||||
$bundleIdentifier: String!
|
||||
) {
|
||||
account {
|
||||
byName(accountName: $accountName) {
|
||||
id
|
||||
appleAppIdentifiers(bundleIdentifier: $bundleIdentifier) {
|
||||
...${AppleAppIdentifierFragment.name}
|
||||
id
|
||||
...AppleAppIdentifierFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${AppleAppIdentifierFragment.definition}
|
||||
${print(AppleAppIdentifierFragmentNode)}
|
||||
`,
|
||||
{
|
||||
accountName,
|
||||
|
||||
@@ -1,50 +1,63 @@
|
||||
import assert from 'assert';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import { AppleDevice, AppleTeam } from '../../../../../graphql/generated';
|
||||
import { AppleDeviceFragment } from '../../../../../graphql/types/credentials/AppleDevice';
|
||||
import { AppleTeamFragment } from '../../../../../graphql/types/credentials/AppleTeam';
|
||||
import {
|
||||
AppleDevice,
|
||||
AppleDeviceFragment,
|
||||
AppleDevicesByAppleTeamQuery,
|
||||
AppleDevicesByIdentifierQuery,
|
||||
AppleDevicesByTeamIdentifierQuery,
|
||||
AppleTeamFragment,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AppleDeviceFragmentNode } from '../../../../../graphql/types/credentials/AppleDevice';
|
||||
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
|
||||
|
||||
export type AppleTeamQueryResult = Pick<AppleTeam, 'id' | 'appleTeamIdentifier' | 'appleTeamName'>;
|
||||
export type AppleDeviceFragmentWithAppleTeam = AppleDeviceFragment & {
|
||||
appleTeam: AppleTeamFragment;
|
||||
};
|
||||
|
||||
export type AppleDeviceQueryResult = Pick<
|
||||
AppleDevice,
|
||||
'id' | 'identifier' | 'name' | 'deviceClass' | 'enabled'
|
||||
>;
|
||||
|
||||
export type AppleDevicesByTeamIdentifierQueryResult = AppleTeamQueryResult & {
|
||||
export type AppleDevicesByTeamIdentifierQueryResult = AppleTeamFragment & {
|
||||
appleDevices: AppleDeviceQueryResult[];
|
||||
};
|
||||
|
||||
export type AppleDevicesByIdentifierQueryResult = AppleDeviceQueryResult & {
|
||||
appleTeam: AppleTeamQueryResult;
|
||||
appleTeam: AppleTeamFragment;
|
||||
};
|
||||
|
||||
const AppleDeviceQuery = {
|
||||
async getAllByAppleTeamIdentifierAsync(
|
||||
accountId: string,
|
||||
appleTeamIdentifier: string
|
||||
): Promise<AppleDevice[]> {
|
||||
): Promise<AppleDeviceFragmentWithAppleTeam[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{ appleTeam: { byAppleTeamIdentifier: AppleTeam } }>(
|
||||
.query<AppleDevicesByAppleTeamQuery>(
|
||||
gql`
|
||||
query($accountId: ID!, $appleTeamIdentifier: String!) {
|
||||
query AppleDevicesByAppleTeamQuery($accountId: ID!, $appleTeamIdentifier: String!) {
|
||||
appleTeam {
|
||||
byAppleTeamIdentifier(accountId: $accountId, identifier: $appleTeamIdentifier) {
|
||||
...${AppleTeamFragment.name}
|
||||
id
|
||||
...AppleTeamFragment
|
||||
appleDevices {
|
||||
...${AppleDeviceFragment.name}
|
||||
id
|
||||
...AppleDeviceFragment
|
||||
appleTeam {
|
||||
...${AppleTeamFragment.name}
|
||||
id
|
||||
...AppleTeamFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${AppleTeamFragment.definition}
|
||||
${AppleDeviceFragment.definition}
|
||||
${print(AppleTeamFragmentNode)}
|
||||
${print(AppleDeviceFragmentNode)}
|
||||
`,
|
||||
{
|
||||
accountId,
|
||||
@@ -54,9 +67,13 @@ const AppleDeviceQuery = {
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(
|
||||
data.appleTeam.byAppleTeamIdentifier,
|
||||
'byAppleTeamIdentifier should be defined in this context - enforced by GraphQL'
|
||||
);
|
||||
const { appleDevices } = data.appleTeam.byAppleTeamIdentifier;
|
||||
assert(appleDevices, 'Apple Devices should be defined in this context - enforced by GraphQL');
|
||||
return appleDevices.filter(device => device) as AppleDevice[];
|
||||
return appleDevices;
|
||||
},
|
||||
|
||||
async getAllForAppleTeamAsync(
|
||||
@@ -65,7 +82,7 @@ const AppleDeviceQuery = {
|
||||
): Promise<AppleDevicesByTeamIdentifierQueryResult | null> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{ account: { byName: { appleTeams: AppleDevicesByTeamIdentifierQueryResult[] } } }>(
|
||||
.query<AppleDevicesByTeamIdentifierQuery>(
|
||||
gql`
|
||||
query AppleDevicesByTeamIdentifier(
|
||||
$accountName: String!
|
||||
@@ -73,6 +90,7 @@ const AppleDeviceQuery = {
|
||||
) {
|
||||
account {
|
||||
byName(accountName: $accountName) {
|
||||
id
|
||||
appleTeams(appleTeamIdentifier: $appleTeamIdentifier) {
|
||||
id
|
||||
appleTeamIdentifier
|
||||
@@ -103,7 +121,7 @@ const AppleDeviceQuery = {
|
||||
): Promise<AppleDevicesByIdentifierQueryResult | null> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{ account: { byName: { appleDevices: AppleDevicesByIdentifierQueryResult[] } } }>(
|
||||
.query<AppleDevicesByIdentifierQuery>(
|
||||
gql`
|
||||
query AppleDevicesByIdentifier($accountName: String!, $identifier: String!) {
|
||||
account {
|
||||
|
||||
+34
-33
@@ -1,13 +1,21 @@
|
||||
import assert from 'assert';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import {
|
||||
AppleDistributionCertificate,
|
||||
AppleDistributionCertificateByAccountQuery,
|
||||
AppleDistributionCertificateByAppQuery,
|
||||
AppleDistributionCertificateFragment,
|
||||
AppleTeamFragment,
|
||||
IosDistributionType,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AppleDistributionCertificateFragment } from '../../../../../graphql/types/credentials/AppleDistributionCertificate';
|
||||
import { AppleTeamFragment } from '../../../../../graphql/types/credentials/AppleTeam';
|
||||
import { AppleDistributionCertificateFragmentNode } from '../../../../../graphql/types/credentials/AppleDistributionCertificate';
|
||||
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
|
||||
|
||||
export type AppleDistributionCertificateQueryResult = AppleDistributionCertificateFragment & {
|
||||
appleTeam?: AppleTeamFragment | null;
|
||||
};
|
||||
const AppleDistributionCertificateQuery = {
|
||||
async getForAppAsync(
|
||||
projectFullName: string,
|
||||
@@ -15,36 +23,31 @@ const AppleDistributionCertificateQuery = {
|
||||
appleAppIdentifierId,
|
||||
iosDistributionType,
|
||||
}: { appleAppIdentifierId: string; iosDistributionType: IosDistributionType }
|
||||
): Promise<AppleDistributionCertificate | null> {
|
||||
): Promise<AppleDistributionCertificateQueryResult | null> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{
|
||||
app: {
|
||||
byFullName: {
|
||||
iosAppCredentials: {
|
||||
iosAppBuildCredentialsArray: {
|
||||
distributionCertificate?: AppleDistributionCertificate;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
}>(
|
||||
.query<AppleDistributionCertificateByAppQuery>(
|
||||
gql`
|
||||
query(
|
||||
query AppleDistributionCertificateByAppQuery(
|
||||
$projectFullName: String!
|
||||
$appleAppIdentifierId: String!
|
||||
$iosDistributionType: IosDistributionType!
|
||||
) {
|
||||
app {
|
||||
byFullName(fullName: $projectFullName) {
|
||||
id
|
||||
iosAppCredentials(filter: { appleAppIdentifierId: $appleAppIdentifierId }) {
|
||||
id
|
||||
iosAppBuildCredentialsArray(
|
||||
filter: { iosDistributionType: $iosDistributionType }
|
||||
) {
|
||||
id
|
||||
distributionCertificate {
|
||||
...${AppleDistributionCertificateFragment.name}
|
||||
id
|
||||
...AppleDistributionCertificateFragment
|
||||
appleTeam {
|
||||
...${AppleTeamFragment.name}
|
||||
id
|
||||
...AppleTeamFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,8 +55,8 @@ const AppleDistributionCertificateQuery = {
|
||||
}
|
||||
}
|
||||
}
|
||||
${AppleDistributionCertificateFragment.definition}
|
||||
${AppleTeamFragment.definition}
|
||||
${print(AppleDistributionCertificateFragmentNode)}
|
||||
${print(AppleTeamFragmentNode)}
|
||||
`,
|
||||
{
|
||||
projectFullName,
|
||||
@@ -66,36 +69,34 @@ const AppleDistributionCertificateQuery = {
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(data.app, 'GraphQL: `app` not defined in server response');
|
||||
return (
|
||||
data.app.byFullName.iosAppCredentials[0]?.iosAppBuildCredentialsArray[0]
|
||||
?.distributionCertificate ?? null
|
||||
);
|
||||
},
|
||||
async getAllForAccount(accountName: string): Promise<AppleDistributionCertificate[]> {
|
||||
async getAllForAccount(accountName: string): Promise<AppleDistributionCertificateQueryResult[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{
|
||||
account: {
|
||||
byName: {
|
||||
appleDistributionCertificates: AppleDistributionCertificate[];
|
||||
};
|
||||
};
|
||||
}>(
|
||||
.query<AppleDistributionCertificateByAccountQuery>(
|
||||
gql`
|
||||
query($accountName: String!) {
|
||||
query AppleDistributionCertificateByAccountQuery($accountName: String!) {
|
||||
account {
|
||||
byName(accountName: $accountName) {
|
||||
id
|
||||
appleDistributionCertificates {
|
||||
...${AppleDistributionCertificateFragment.name}
|
||||
id
|
||||
...AppleDistributionCertificateFragment
|
||||
appleTeam {
|
||||
...${AppleTeamFragment.name}
|
||||
id
|
||||
...AppleTeamFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${AppleDistributionCertificateFragment.definition}
|
||||
${AppleTeamFragment.definition}
|
||||
${print(AppleDistributionCertificateFragmentNode)}
|
||||
${print(AppleTeamFragmentNode)}
|
||||
`,
|
||||
{
|
||||
accountName,
|
||||
|
||||
+36
-26
@@ -1,12 +1,24 @@
|
||||
import assert from 'assert';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import { AppleProvisioningProfile, IosDistributionType } from '../../../../../graphql/generated';
|
||||
import { AppleAppIdentifierFragment } from '../../../../../graphql/types/credentials/AppleAppIdentifier';
|
||||
import { AppleDeviceFragment } from '../../../../../graphql/types/credentials/AppleDevice';
|
||||
import { AppleProvisioningProfileFragment } from '../../../../../graphql/types/credentials/AppleProvisioningProfile';
|
||||
import { AppleTeamFragment } from '../../../../../graphql/types/credentials/AppleTeam';
|
||||
import {
|
||||
AppleAppIdentifierFragment,
|
||||
AppleDeviceFragment,
|
||||
AppleProvisioningProfileFragment,
|
||||
AppleProvisioningProfilesByAppQuery,
|
||||
AppleTeamFragment,
|
||||
IosDistributionType,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AppleAppIdentifierFragmentNode } from '../../../../../graphql/types/credentials/AppleAppIdentifier';
|
||||
import { AppleDeviceFragmentNode } from '../../../../../graphql/types/credentials/AppleDevice';
|
||||
import { AppleProvisioningProfileFragmentNode } from '../../../../../graphql/types/credentials/AppleProvisioningProfile';
|
||||
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
|
||||
|
||||
export type AppleProvisioningProfileQueryResult = AppleProvisioningProfileFragment & {
|
||||
appleTeam?: AppleTeamFragment | null;
|
||||
} & { appleDevices: AppleDeviceFragment[] } & { appleAppIdentifier: AppleAppIdentifierFragment };
|
||||
const AppleProvisioningProfileQuery = {
|
||||
async getForAppAsync(
|
||||
projectFullName: string,
|
||||
@@ -14,42 +26,39 @@ const AppleProvisioningProfileQuery = {
|
||||
appleAppIdentifierId,
|
||||
iosDistributionType,
|
||||
}: { appleAppIdentifierId: string; iosDistributionType: IosDistributionType }
|
||||
): Promise<AppleProvisioningProfile | null> {
|
||||
): Promise<AppleProvisioningProfileQueryResult | null> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{
|
||||
app: {
|
||||
byFullName: {
|
||||
iosAppCredentials: {
|
||||
iosAppBuildCredentialsArray: {
|
||||
provisioningProfile?: AppleProvisioningProfile;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
}>(
|
||||
.query<AppleProvisioningProfilesByAppQuery>(
|
||||
gql`
|
||||
query(
|
||||
query AppleProvisioningProfilesByAppQuery(
|
||||
$projectFullName: String!
|
||||
$appleAppIdentifierId: String!
|
||||
$iosDistributionType: IosDistributionType!
|
||||
) {
|
||||
app {
|
||||
byFullName(fullName: $projectFullName) {
|
||||
id
|
||||
iosAppCredentials(filter: { appleAppIdentifierId: $appleAppIdentifierId }) {
|
||||
id
|
||||
iosAppBuildCredentialsArray(
|
||||
filter: { iosDistributionType: $iosDistributionType }
|
||||
) {
|
||||
id
|
||||
provisioningProfile {
|
||||
...${AppleProvisioningProfileFragment.name}
|
||||
id
|
||||
...AppleProvisioningProfileFragment
|
||||
appleTeam {
|
||||
...${AppleTeamFragment.name}
|
||||
id
|
||||
...AppleTeamFragment
|
||||
}
|
||||
appleDevices {
|
||||
...${AppleDeviceFragment.name}
|
||||
id
|
||||
...AppleDeviceFragment
|
||||
}
|
||||
appleAppIdentifier {
|
||||
...${AppleAppIdentifierFragment.name}
|
||||
id
|
||||
...AppleAppIdentifierFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,10 +66,10 @@ const AppleProvisioningProfileQuery = {
|
||||
}
|
||||
}
|
||||
}
|
||||
${AppleProvisioningProfileFragment.definition}
|
||||
${AppleTeamFragment.definition}
|
||||
${AppleDeviceFragment.definition}
|
||||
${AppleAppIdentifierFragment.definition}
|
||||
${print(AppleProvisioningProfileFragmentNode)}
|
||||
${print(AppleTeamFragmentNode)}
|
||||
${print(AppleDeviceFragmentNode)}
|
||||
${print(AppleAppIdentifierFragmentNode)}
|
||||
`,
|
||||
{
|
||||
projectFullName,
|
||||
@@ -73,6 +82,7 @@ const AppleProvisioningProfileQuery = {
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(data.app, 'GraphQL: `app` not defined in server response');
|
||||
return (
|
||||
data.app.byFullName.iosAppCredentials[0]?.iosAppBuildCredentialsArray[0]
|
||||
?.provisioningProfile ?? null
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import { AppleTeam } from '../../../../../graphql/generated';
|
||||
import { AppleTeamFragment } from '../../../../../graphql/types/credentials/AppleTeam';
|
||||
|
||||
type AppleTeamQueryResult = Pick<AppleTeam, 'id' | 'appleTeamIdentifier' | 'appleTeamName'>;
|
||||
import {
|
||||
AppleTeamByIdentifierQuery,
|
||||
AppleTeamFragment,
|
||||
AppleTeamsByAccountNameQuery,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
|
||||
|
||||
const AppleTeamQuery = {
|
||||
async getAllForAccountAsync(accountName: string): Promise<AppleTeamQueryResult[]> {
|
||||
async getAllForAccountAsync(accountName: string): Promise<AppleTeamFragment[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{ account: { byName: { appleTeams: AppleTeamQueryResult[] } } }>(
|
||||
.query<AppleTeamsByAccountNameQuery>(
|
||||
gql`
|
||||
query AppleTeamsByAccountName($accountName: String!) {
|
||||
account {
|
||||
byName(accountName: $accountName) {
|
||||
id
|
||||
appleTeams {
|
||||
id
|
||||
appleTeamName
|
||||
@@ -35,19 +39,20 @@ const AppleTeamQuery = {
|
||||
async getByAppleTeamIdentifierAsync(
|
||||
accountId: string,
|
||||
appleTeamIdentifier: string
|
||||
): Promise<AppleTeam | null> {
|
||||
): Promise<AppleTeamFragment | null> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{ appleTeam: { byAppleTeamIdentifier: AppleTeam | null } }>(
|
||||
.query<AppleTeamByIdentifierQuery>(
|
||||
gql`
|
||||
query($accountId: ID!, $appleTeamIdentifier: String!) {
|
||||
query AppleTeamByIdentifierQuery($accountId: ID!, $appleTeamIdentifier: String!) {
|
||||
appleTeam {
|
||||
byAppleTeamIdentifier(accountId: $accountId, identifier: $appleTeamIdentifier) {
|
||||
...${AppleTeamFragment.name}
|
||||
id
|
||||
...AppleTeamFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${AppleTeamFragment.definition}
|
||||
${print(AppleTeamFragmentNode)}
|
||||
`,
|
||||
{
|
||||
accountId,
|
||||
@@ -56,7 +61,7 @@ const AppleTeamQuery = {
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.appleTeam.byAppleTeamIdentifier;
|
||||
return data.appleTeam.byAppleTeamIdentifier ?? null;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+21
-19
@@ -1,8 +1,14 @@
|
||||
import assert from 'assert';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import { IosAppBuildCredentials, IosDistributionType } from '../../../../../graphql/generated';
|
||||
import { IosAppBuildCredentialsFragment } from '../../../../../graphql/types/credentials/IosAppBuildCredentials';
|
||||
import {
|
||||
IosAppBuildCredentialsByAppleAppIdentiferAndDistributionQuery,
|
||||
IosAppBuildCredentialsFragment,
|
||||
IosDistributionType,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { IosAppBuildCredentialsFragmentNode } from '../../../../../graphql/types/credentials/IosAppBuildCredentials';
|
||||
|
||||
const IosAppBuildCredentialsQuery = {
|
||||
async byAppIdentifierIdAndDistributionTypeAsync(
|
||||
@@ -11,37 +17,32 @@ const IosAppBuildCredentialsQuery = {
|
||||
appleAppIdentifierId,
|
||||
iosDistributionType,
|
||||
}: { appleAppIdentifierId: string; iosDistributionType: IosDistributionType }
|
||||
): Promise<IosAppBuildCredentials | null> {
|
||||
): Promise<IosAppBuildCredentialsFragment | null> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{
|
||||
app: {
|
||||
byFullName: {
|
||||
iosAppCredentials: {
|
||||
iosAppBuildCredentialsArray: IosAppBuildCredentials[];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
}>(
|
||||
.query<IosAppBuildCredentialsByAppleAppIdentiferAndDistributionQuery>(
|
||||
gql`
|
||||
query(
|
||||
$projectFullName: String!
|
||||
$appleAppIdentifierId: String!
|
||||
$iosDistributionType: IosDistributionType!
|
||||
) {
|
||||
query IosAppBuildCredentialsByAppleAppIdentiferAndDistributionQuery(
|
||||
$projectFullName: String!
|
||||
$appleAppIdentifierId: String!
|
||||
$iosDistributionType: IosDistributionType!
|
||||
) {
|
||||
app {
|
||||
byFullName(fullName: $projectFullName) {
|
||||
id
|
||||
iosAppCredentials(filter: { appleAppIdentifierId: $appleAppIdentifierId }) {
|
||||
id
|
||||
iosAppBuildCredentialsArray(
|
||||
filter: { iosDistributionType: $iosDistributionType }
|
||||
) {
|
||||
...${IosAppBuildCredentialsFragment.name}
|
||||
id
|
||||
...IosAppBuildCredentialsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${IosAppBuildCredentialsFragment.definition}
|
||||
${print(IosAppBuildCredentialsFragmentNode)}
|
||||
`,
|
||||
{
|
||||
projectFullName,
|
||||
@@ -54,6 +55,7 @@ const IosAppBuildCredentialsQuery = {
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(data.app, 'GraphQL: `app` not defined in server response');
|
||||
return data.app.byFullName.iosAppCredentials[0]?.iosAppBuildCredentialsArray[0] ?? null;
|
||||
},
|
||||
};
|
||||
|
||||
+94
-23
@@ -1,29 +1,50 @@
|
||||
import assert from 'assert';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import { IosAppCredentials, IosDistributionType } from '../../../../../graphql/generated';
|
||||
import { IosAppBuildCredentialsFragment } from '../../../../../graphql/types/credentials/IosAppBuildCredentials';
|
||||
import { IosAppCredentialsFragment } from '../../../../../graphql/types/credentials/IosAppCredentials';
|
||||
import {
|
||||
CommonIosAppCredentialsFragment,
|
||||
CommonIosAppCredentialsWithBuildCredentialsByAppIdentifierIdQuery,
|
||||
IosAppBuildCredentialsFragment,
|
||||
IosAppCredentialsByAppIdentifierIdQuery,
|
||||
IosAppCredentialsFragment,
|
||||
IosAppCredentialsWithBuildCredentialsByAppIdentifierIdQuery,
|
||||
IosDistributionType,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { IosAppBuildCredentialsFragmentNode } from '../../../../../graphql/types/credentials/IosAppBuildCredentials';
|
||||
import {
|
||||
CommonIosAppCredentialsFragmentNode,
|
||||
IosAppCredentialsFragmentNode,
|
||||
} from '../../../../../graphql/types/credentials/IosAppCredentials';
|
||||
|
||||
export type IosAppCredentialsWithBuildCredentialsQueryResult = IosAppCredentialsFragment & {
|
||||
iosAppBuildCredentialsArray: IosAppBuildCredentialsFragment[];
|
||||
};
|
||||
const IosAppCredentialsQuery = {
|
||||
async byAppIdentifierIdAsync(
|
||||
projectFullName: string,
|
||||
appleAppIdentifierId: string
|
||||
): Promise<IosAppCredentials | null> {
|
||||
): Promise<IosAppCredentialsFragment | null> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{ app: { byFullName: { iosAppCredentials: IosAppCredentials[] } } }>(
|
||||
.query<IosAppCredentialsByAppIdentifierIdQuery>(
|
||||
gql`
|
||||
query($projectFullName: String!, $appleAppIdentifierId: String!) {
|
||||
query IosAppCredentialsByAppIdentifierIdQuery(
|
||||
$projectFullName: String!
|
||||
$appleAppIdentifierId: String!
|
||||
) {
|
||||
app {
|
||||
byFullName(fullName: $projectFullName) {
|
||||
id
|
||||
iosAppCredentials(filter: { appleAppIdentifierId: $appleAppIdentifierId }) {
|
||||
...${IosAppCredentialsFragment.name}
|
||||
id
|
||||
...IosAppCredentialsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${IosAppCredentialsFragment.definition}
|
||||
${print(IosAppCredentialsFragmentNode)}
|
||||
`,
|
||||
{
|
||||
projectFullName,
|
||||
@@ -35,6 +56,7 @@ const IosAppCredentialsQuery = {
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(data.app, 'GraphQL: `app` not defined in server response');
|
||||
return data.app.byFullName.iosAppCredentials[0] ?? null;
|
||||
},
|
||||
async withBuildCredentialsByAppIdentifierIdAsync(
|
||||
@@ -46,28 +68,35 @@ const IosAppCredentialsQuery = {
|
||||
appleAppIdentifierId: string;
|
||||
iosDistributionType: IosDistributionType;
|
||||
}
|
||||
): Promise<IosAppCredentials | null> {
|
||||
): Promise<IosAppCredentialsWithBuildCredentialsQueryResult | null> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{ app: { byFullName: { iosAppCredentials: IosAppCredentials[] } } }>(
|
||||
.query<IosAppCredentialsWithBuildCredentialsByAppIdentifierIdQuery>(
|
||||
gql`
|
||||
query($projectFullName: String!, $appleAppIdentifierId: String!, $iosDistributionType: IosDistributionType!) {
|
||||
app {
|
||||
byFullName(fullName: $projectFullName) {
|
||||
iosAppCredentials(filter: { appleAppIdentifierId: $appleAppIdentifierId }) {
|
||||
...${IosAppCredentialsFragment.name}
|
||||
iosAppBuildCredentialsArray(
|
||||
filter: { iosDistributionType: $iosDistributionType }
|
||||
) {
|
||||
...${IosAppBuildCredentialsFragment.name}
|
||||
}
|
||||
query IosAppCredentialsWithBuildCredentialsByAppIdentifierIdQuery(
|
||||
$projectFullName: String!
|
||||
$appleAppIdentifierId: String!
|
||||
$iosDistributionType: IosDistributionType!
|
||||
) {
|
||||
app {
|
||||
byFullName(fullName: $projectFullName) {
|
||||
id
|
||||
iosAppCredentials(filter: { appleAppIdentifierId: $appleAppIdentifierId }) {
|
||||
id
|
||||
...IosAppCredentialsFragment
|
||||
iosAppBuildCredentialsArray(
|
||||
filter: { iosDistributionType: $iosDistributionType }
|
||||
) {
|
||||
id
|
||||
...IosAppBuildCredentialsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${IosAppCredentialsFragment.definition}
|
||||
${IosAppBuildCredentialsFragment.definition}
|
||||
`,
|
||||
}
|
||||
${print(IosAppCredentialsFragmentNode)}
|
||||
${print(IosAppBuildCredentialsFragmentNode)}
|
||||
`,
|
||||
{
|
||||
projectFullName,
|
||||
appleAppIdentifierId,
|
||||
@@ -79,6 +108,48 @@ const IosAppCredentialsQuery = {
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(data.app, 'GraphQL: `app` not defined in server response');
|
||||
return data.app.byFullName.iosAppCredentials[0] ?? null;
|
||||
},
|
||||
async withCommonFieldsByAppIdentifierIdAsync(
|
||||
projectFullName: string,
|
||||
{
|
||||
appleAppIdentifierId,
|
||||
}: {
|
||||
appleAppIdentifierId: string;
|
||||
}
|
||||
): Promise<CommonIosAppCredentialsFragment | null> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<CommonIosAppCredentialsWithBuildCredentialsByAppIdentifierIdQuery>(
|
||||
gql`
|
||||
query CommonIosAppCredentialsWithBuildCredentialsByAppIdentifierIdQuery(
|
||||
$projectFullName: String!
|
||||
$appleAppIdentifierId: String!
|
||||
) {
|
||||
app {
|
||||
byFullName(fullName: $projectFullName) {
|
||||
id
|
||||
iosAppCredentials(filter: { appleAppIdentifierId: $appleAppIdentifierId }) {
|
||||
id
|
||||
...CommonIosAppCredentialsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${CommonIosAppCredentialsFragmentNode}
|
||||
`,
|
||||
{
|
||||
projectFullName,
|
||||
appleAppIdentifierId,
|
||||
},
|
||||
{
|
||||
additionalTypenames: ['IosAppCredentials', 'IosAppBuildCredentials', 'App'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(data.app, 'GraphQL: `app` not defined in server response');
|
||||
return data.app.byFullName.iosAppCredentials[0] ?? null;
|
||||
},
|
||||
};
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
const IosAppCredentialsQuery = {
|
||||
withCommonFieldsByAppIdentifierIdAsync: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
id: '48d0cd34-96d8-447c-9de8-40dd2c3ca0f8',
|
||||
app: {
|
||||
id: '033134f3-e85c-4bde-9065-41a9c066ec31',
|
||||
fullName: '@quinlanj/test52',
|
||||
__typename: 'App',
|
||||
},
|
||||
appleTeam: {
|
||||
id: '6d39dbfe-a30a-44a4-bc36-d562e5963fa3',
|
||||
appleTeamIdentifier: 'test-apple-identifier',
|
||||
appleTeamName: null,
|
||||
__typename: 'AppleTeam',
|
||||
},
|
||||
appleAppIdentifier: {
|
||||
id: 'bd9c3609-26b9-451e-8ba6-e0604736c7fc',
|
||||
bundleIdentifier: 'com.quinlanj.test52',
|
||||
__typename: 'AppleAppIdentifier',
|
||||
},
|
||||
iosAppBuildCredentialsArray: [
|
||||
{
|
||||
id: '6f8d0175-0caf-4922-b09b-eae757b83144',
|
||||
iosDistributionType: 'APP_STORE',
|
||||
distributionCertificate: {
|
||||
id: '8b3baa53-b149-4a1b-842b-75ff6e0bcb1e',
|
||||
certificateP12: 'test-cert-p12',
|
||||
certificatePassword: 'test-cert-password',
|
||||
serialNumber: 'test-serial',
|
||||
developerPortalIdentifier: 'test-portal-id',
|
||||
validityNotBefore: '2020-10-13T18:28:44.000Z',
|
||||
validityNotAfter: '2021-10-13T18:28:44.000Z',
|
||||
updatedAt: '2020-10-13T18:38:45.275Z',
|
||||
appleTeam: {
|
||||
id: '6d39dbfe-a30a-44a4-bc36-d562e5963fa3',
|
||||
appleTeamIdentifier: 'test-apple-identifier',
|
||||
appleTeamName: null,
|
||||
__typename: 'AppleTeam',
|
||||
},
|
||||
__typename: 'AppleDistributionCertificate',
|
||||
},
|
||||
provisioningProfile: {
|
||||
id: '01517f14-b4cf-489e-b1fd-3f5a61cb8df8',
|
||||
expiration: '2021-10-13T18:28:44.000Z',
|
||||
developerPortalIdentifier: 'test-profile-id',
|
||||
provisioningProfile: 'test-profile',
|
||||
updatedAt: '2020-10-29T00:18:32.130Z',
|
||||
status: 'active',
|
||||
appleDevices: [],
|
||||
appleTeam: {
|
||||
id: '6d39dbfe-a30a-44a4-bc36-d562e5963fa3',
|
||||
appleTeamIdentifier: 'test-apple-identifier',
|
||||
appleTeamName: null,
|
||||
__typename: 'AppleTeam',
|
||||
},
|
||||
__typename: 'AppleProvisioningProfile',
|
||||
},
|
||||
__typename: 'IosAppBuildCredentials',
|
||||
},
|
||||
{
|
||||
id: '3e1907a8-7114-4f14-9964-73467bc010ea',
|
||||
iosDistributionType: 'AD_HOC',
|
||||
distributionCertificate: {
|
||||
id: '8b3baa53-b149-4a1b-842b-75ff6e0bcb1e',
|
||||
certificateP12: 'test-cert-p12',
|
||||
certificatePassword: 'test-cert-password',
|
||||
serialNumber: 'test-serial',
|
||||
developerPortalIdentifier: 'test-portal-id',
|
||||
validityNotBefore: '2020-10-13T18:28:44.000Z',
|
||||
validityNotAfter: '2021-10-13T18:28:44.000Z',
|
||||
updatedAt: '2020-10-13T18:38:45.275Z',
|
||||
appleTeam: {
|
||||
id: '6d39dbfe-a30a-44a4-bc36-d562e5963fa3',
|
||||
appleTeamIdentifier: 'test-apple-identifier',
|
||||
appleTeamName: null,
|
||||
__typename: 'AppleTeam',
|
||||
},
|
||||
__typename: 'AppleDistributionCertificate',
|
||||
},
|
||||
provisioningProfile: {
|
||||
id: '01517f14-b4cf-489e-b1fd-3f5a61cb8df8',
|
||||
expiration: '2021-10-13T18:28:44.000Z',
|
||||
developerPortalIdentifier: 'test-profile-id',
|
||||
provisioningProfile: 'test-profile',
|
||||
updatedAt: '2020-10-29T00:18:32.130Z',
|
||||
status: 'active',
|
||||
appleDevices: [],
|
||||
appleTeam: {
|
||||
id: '6d39dbfe-a30a-44a4-bc36-d562e5963fa3',
|
||||
appleTeamIdentifier: 'test-apple-identifier',
|
||||
appleTeamName: null,
|
||||
__typename: 'AppleTeam',
|
||||
},
|
||||
__typename: 'AppleProvisioningProfile',
|
||||
},
|
||||
__typename: 'IosAppBuildCredentials',
|
||||
},
|
||||
],
|
||||
__typename: 'IosAppCredentials',
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
export { IosAppCredentialsQuery };
|
||||
@@ -0,0 +1,45 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { formatContractMessage } from '../contractMessages';
|
||||
|
||||
const expiredMembership = {
|
||||
subject: 'Developer Program Membership Expired',
|
||||
message:
|
||||
"Your membership has expired, and your apps have been removed from the App Store until you renew your membership. To renew, a user with the Account Holder role must <a href='http://developer.apple.com/membercenter/index.action' target='_blank'>sign in</a> and renew the membership on the Apple Developer website. <a href='https://developer.apple.com/support/renewal/' target='_blank'>Learn More</a>.",
|
||||
};
|
||||
const paymentReturned = {
|
||||
subject: 'Payment Returned',
|
||||
message:
|
||||
"Your payment was returned by your bank. The reason your payment was returned is:<br/><br/>Your bank account number is formatted incorrectly.<br/><br/>You can review your banking information and make any necessary changes in <a href='/agreements/#' target='_self'>Agreements, Tax, and Banking</a> for: <br/>CHASE BANK - ****1234 <br/>Bank Payment Reference Number: 123456789",
|
||||
};
|
||||
const licenseAgreementUpdated = {
|
||||
subject: 'Apple Developer Program License Agreement Updated',
|
||||
message:
|
||||
'The updated Apple Developer Program License Agreement needs to be reviewed. In order to update your existing apps and submit new apps to the App Store, the Account Holder must review and accept the updated agreement by signing in to their <a href="https://developer.apple.com/account" target="_blank">account</a> on the Apple Developer website.',
|
||||
};
|
||||
const paidLicenseAgreementUpdated = {
|
||||
subject: '',
|
||||
message:
|
||||
'<b>Review the updated Paid Applications Schedule.</b><br />In order to update your existing apps, create new in-app purchases, and submit new apps to the App Store, the user with the Legal role (Account Holder) must review and accept the Paid Applications Schedule (Schedule 2 to the Apple Developer Program License Agreement) in the Agreements, Tax, and Banking module.<br /><br /> To accept this agreement, they must have already accepted the latest version of the Apple Developer Program License Agreement in their <a href="http://developer.apple.com/membercenter/index.action">account on the developer website<a/>.<br />',
|
||||
};
|
||||
|
||||
describe(formatContractMessage, () => {
|
||||
it(`formats expired message`, () => {
|
||||
expect(formatContractMessage(expiredMembership as any)).toMatch(
|
||||
chalk.bold(`Developer Program Membership Expired`)
|
||||
);
|
||||
});
|
||||
it(`formats payment message`, () => {
|
||||
expect(formatContractMessage(paymentReturned as any)).toMatch(chalk.bold(`Payment Returned`));
|
||||
});
|
||||
it(`formats license message`, () => {
|
||||
expect(formatContractMessage(licenseAgreementUpdated as any)).toMatch(
|
||||
chalk.bold`Apple Developer Program License Agreement Updated`
|
||||
);
|
||||
});
|
||||
it(`formats paid license message`, () => {
|
||||
expect(formatContractMessage(paidLicenseAgreementUpdated as any)).toMatch(
|
||||
chalk.bold`Review the updated Paid Applications Schedule.`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
import { ITCAgreements, RequestContext } from '@expo/apple-utils';
|
||||
import chalk from 'chalk';
|
||||
import { Ora } from 'ora';
|
||||
|
||||
import log from '../../../log';
|
||||
import { convertHTMLToASCII } from '../utils/convertHTMLToASCII';
|
||||
|
||||
async function getContractStatusAsync(
|
||||
context: RequestContext
|
||||
): Promise<ITCAgreements.ITCContractStatus | null> {
|
||||
try {
|
||||
const capabilities = await ITCAgreements.getCapabilitiesAsync(context);
|
||||
return capabilities?.contractStatus ?? null;
|
||||
} catch (error) {
|
||||
log.warn(`Failed to get iTunes contract status: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getContractMessagesAsync(context: RequestContext) {
|
||||
try {
|
||||
return await ITCAgreements.getContractMessagesAsync(context);
|
||||
} catch (error) {
|
||||
log.warn(`Failed to get iTunes contract messages: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getRequiredContractMessagesAsync(
|
||||
context: RequestContext
|
||||
): Promise<{ messages: ITCAgreements.ITCContractMessage[]; isFatal: boolean }> {
|
||||
// This emulates the check that's performed on the ASC website's "apps"
|
||||
// page before presenting the (+) create app button.
|
||||
const status = await getContractStatusAsync(context);
|
||||
|
||||
if (status) {
|
||||
if (['FREE_APP_AGREEMENT_ACTIVE', 'PAID_APP_AGREEMENT_ACTIVE'].includes(status)) {
|
||||
// The user can freely create an app, no contracts need to be accepted.
|
||||
// No need to check for messages because afaict no vital messages will be present.
|
||||
return { messages: [], isFatal: false };
|
||||
} else if (
|
||||
['FREE_APP_AGREEMENT_OUTDATED', 'PAID_APP_AGREEMENT_OUTDATED', 'EXPIRED_MEMBERSHIP'].includes(
|
||||
status
|
||||
)
|
||||
) {
|
||||
// The user cannot create an app until they've reviewed, and agreed to the updated agreements
|
||||
// or renewed their membership on ASC.
|
||||
// Get the exact messages from ASC to show the user a useful message.
|
||||
return { messages: (await getContractMessagesAsync(context)) ?? [], isFatal: true };
|
||||
}
|
||||
}
|
||||
// The contract messages aren't documented so if a new one is present we cannot be sure if it's fatal or not.
|
||||
// This will check for messages, if none exist, then the process will continue.
|
||||
// Otherwise messages will be present and the process will stop.
|
||||
// There is a small chance that this could result in a false positive if the messages are extraneous, so we'll also
|
||||
// prompt the user to open an issue so we can address the new contract state if it ever appears.
|
||||
// TODO: Maybe a silent analytic would be better
|
||||
log.error(
|
||||
`\nUnexpected Apple developer contract status "${status}". Please open an issue on https://github.com/expo/eas-cli`
|
||||
);
|
||||
log.newLine();
|
||||
return { messages: (await getContractMessagesAsync(context)) ?? [], isFatal: false };
|
||||
}
|
||||
|
||||
const rootUrl = 'https://appstoreconnect.apple.com';
|
||||
|
||||
export function formatContractMessage(message: ITCAgreements.ITCContractMessage): string {
|
||||
return convertHTMLToASCII({
|
||||
content:
|
||||
'\u203A ' +
|
||||
[message.subject && `<b>${message.subject}</b>`, message.message]
|
||||
.filter(Boolean)
|
||||
.join('<br />'),
|
||||
rootUrl,
|
||||
});
|
||||
}
|
||||
|
||||
export async function assertContractMessagesAsync(context: RequestContext, spinner?: Ora) {
|
||||
const { messages, isFatal } = await getRequiredContractMessagesAsync(context);
|
||||
|
||||
if (Array.isArray(messages) && messages.length) {
|
||||
if (spinner) {
|
||||
spinner.stop();
|
||||
}
|
||||
log.newLine();
|
||||
log(chalk.yellow.bold('Messages from App Store Connect:'));
|
||||
log.newLine();
|
||||
for (const message of messages) {
|
||||
if (log.isDebug) {
|
||||
log(JSON.stringify(message, null, 2));
|
||||
log.newLine();
|
||||
}
|
||||
log.addNewLineIfNone();
|
||||
log(formatContractMessage(message));
|
||||
}
|
||||
log.addNewLineIfNone();
|
||||
// Only throw an error if we know that the status is fatal, otherwise attempt to finish the process.
|
||||
if (isFatal) {
|
||||
throw new Error('App Store Connect has agreement updates that must be resolved');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
|
||||
import { AuthCtx, getRequestContext } from './authenticate';
|
||||
import { assertContractMessagesAsync } from './contractMessages';
|
||||
|
||||
export interface EnsureAppExistsOptions {
|
||||
enablePushNotifications?: boolean;
|
||||
@@ -60,7 +61,13 @@ export async function ensureBundleIdExistsWithNameAsync(
|
||||
);
|
||||
} else {
|
||||
spinner.fail(`Failed to register bundle identifier ${chalk.dim(bundleIdentifier)}`);
|
||||
|
||||
// Assert contract errors for easier resolution when the user has an expired developer account.
|
||||
if (err.message.match(/forbidden for security reasons/)) {
|
||||
await assertContractMessagesAsync(context);
|
||||
}
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -102,6 +109,9 @@ export async function ensureAppExistsAsync(
|
||||
if (!app) {
|
||||
spinner.text = `Creating App Store app ${chalk.bold(name)} ${chalk.dim(bundleIdentifier)}`;
|
||||
try {
|
||||
// Assert contract errors when the user needs to create an app.
|
||||
await assertContractMessagesAsync(context, spinner);
|
||||
|
||||
app = await App.createAsync(context, {
|
||||
bundleId: bundleIdentifier,
|
||||
name,
|
||||
@@ -117,7 +127,8 @@ export async function ensureAppExistsAsync(
|
||||
}
|
||||
|
||||
spinner.fail(`Failed to create App Store app ${chalk.dim(name)}`);
|
||||
|
||||
error.message +=
|
||||
'\nPlease visit https://appstoreconnect.apple.com and resolve any warnings, then try again.';
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Auth, JsonFileCache } from '@expo/apple-utils';
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'fs-extra';
|
||||
import wordwrap from 'wordwrap';
|
||||
import wrapAnsi from 'wrap-ansi';
|
||||
|
||||
import log, { learnMore } from '../../../log';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
@@ -85,14 +85,13 @@ export async function promptPasswordAsync({
|
||||
return cachedPassword;
|
||||
}
|
||||
|
||||
const wrap = wordwrap(process.stdout.columns || 80);
|
||||
|
||||
// https://docs.expo.io/distribution/security/#apple-developer-account-credentials
|
||||
log(
|
||||
wrap(
|
||||
wrapAnsi(
|
||||
chalk.bold(
|
||||
`\u203A The password is only used to authenticate with Apple and never stored on Expo servers`
|
||||
)
|
||||
`\u203A The password is only used to authenticate with Apple and never stored on EAS servers`
|
||||
),
|
||||
process.stdout.columns || 80
|
||||
)
|
||||
);
|
||||
log(` ${learnMore('https://bit.ly/2VtGWhU')}`);
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`print credentials prints the IosAppCredentials fragment 1`] = `"iOS Credentials Project: @quinlanj/test52 Bundle Identifier: com.quinlanj.test52 Apple Team: test-apple-identifier Configuration: App Store Distribution Certificate: Serial Number: test-serial Expiration Date: Wed, 13 Oct 2021 18:28:44 GMT+0000 Apple Team: test-apple-identifier Updated 6 months ago Provisioning Profile: Developer Portal ID: test-profile-id Status: active Expiration Date: Wed, 13 Oct 2021 18:28:44 GMT+0000 Apple Team: test-apple-identifier Updated 5 months ago Configuration: Ad Hoc Distribution Certificate: Serial Number: test-serial Expiration Date: Wed, 13 Oct 2021 18:28:44 GMT+0000 Apple Team: test-apple-identifier Updated 6 months ago Provisioning Profile: Developer Portal ID: test-profile-id Status: active Expiration Date: Wed, 13 Oct 2021 18:28:44 GMT+0000 Apple Team: test-apple-identifier Updated 5 months ago"`;
|
||||
@@ -0,0 +1,34 @@
|
||||
import mockdate from 'mockdate';
|
||||
|
||||
import { IosAppCredentialsQuery } from '../../api/graphql/queries/IosAppCredentialsQuery';
|
||||
import { displayIosAppCredentials } from '../printCredentialsBeta';
|
||||
const mockLog = {
|
||||
__esModule: true, // this property makes it work
|
||||
default: jest.fn(toLog => toLog),
|
||||
};
|
||||
(mockLog.default as any).newLine = jest.fn();
|
||||
jest.mock('../../../../log', () => mockLog);
|
||||
jest.mock('chalk', () => {
|
||||
return {
|
||||
__esModule: true, // this property makes it work
|
||||
default: { bold: jest.fn(log => log) },
|
||||
};
|
||||
});
|
||||
jest.mock('../../api/graphql/queries/IosAppCredentialsQuery');
|
||||
|
||||
mockdate.set(new Date('4/20/2021'));
|
||||
describe('print credentials', () => {
|
||||
it('prints the IosAppCredentials fragment', async () => {
|
||||
const testIosAppCredentialsData = await IosAppCredentialsQuery.withCommonFieldsByAppIdentifierIdAsync(
|
||||
'@foo/bar',
|
||||
{
|
||||
appleAppIdentifierId: 'test-id',
|
||||
}
|
||||
);
|
||||
displayIosAppCredentials(testIosAppCredentialsData);
|
||||
const loggedSoFar = mockLog.default.mock.results
|
||||
.map(mockCall => mockCall.value)
|
||||
.reduce((acc, mockValue) => acc + mockValue);
|
||||
expect(loggedSoFar).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import chalk from 'chalk';
|
||||
import terminalLink from 'terminal-link';
|
||||
// @ts-ignore
|
||||
import TurndownService from 'turndown';
|
||||
import wrapAnsi from 'wrap-ansi';
|
||||
|
||||
const turndownServices: Record<string, any> = {};
|
||||
|
||||
function getService(rootUrl: string) {
|
||||
if (turndownServices[rootUrl]) {
|
||||
return turndownServices[rootUrl];
|
||||
}
|
||||
const turndownService = new TurndownService();
|
||||
turndownService.addRule('strikethrough', {
|
||||
filter: ['del', 's', 'strike'],
|
||||
replacement(content: string): string {
|
||||
return chalk.strikethrough(content);
|
||||
},
|
||||
});
|
||||
turndownService.addRule('strong', {
|
||||
filter: ['strong', 'b'],
|
||||
replacement(content: string): string {
|
||||
return chalk.bold(content);
|
||||
},
|
||||
});
|
||||
turndownService.addRule('emphasis', {
|
||||
filter: ['em', 'i'],
|
||||
replacement(content: string): string {
|
||||
return chalk.italic(content);
|
||||
},
|
||||
});
|
||||
turndownService.addRule('heading', {
|
||||
filter: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
|
||||
replacement(content: string): string {
|
||||
return '\n' + chalk.bold(content) + '\n';
|
||||
},
|
||||
});
|
||||
turndownService.addRule('inlineLink', {
|
||||
filter(node: any, options: any) {
|
||||
return options.linkStyle === 'inlined' && node.nodeName === 'A' && node.getAttribute('href');
|
||||
},
|
||||
replacement(content: string, node: any) {
|
||||
let href = node.getAttribute('href');
|
||||
if (href.startsWith('/')) href = `${rootUrl}${href}`;
|
||||
if (terminalLink.isSupported) {
|
||||
return chalk.cyan(terminalLink(content, href));
|
||||
}
|
||||
return `${chalk.cyan(content)} (${chalk.underline(href)})`;
|
||||
},
|
||||
});
|
||||
|
||||
turndownServices[rootUrl] = turndownService;
|
||||
return turndownService;
|
||||
}
|
||||
|
||||
export function convertHTMLToASCII({
|
||||
content,
|
||||
rootUrl,
|
||||
}: {
|
||||
content: string;
|
||||
rootUrl: string;
|
||||
}): string {
|
||||
const service = getService(rootUrl);
|
||||
return wrapAnsi(service.turndown(content), process.stdout.columns || 80);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import chalk from 'chalk';
|
||||
import dateformat from 'dateformat';
|
||||
|
||||
import {
|
||||
CommonIosAppCredentialsFragment,
|
||||
IosAppBuildCredentialsFragment,
|
||||
IosDistributionType,
|
||||
} from '../../../graphql/generated';
|
||||
import log from '../../../log';
|
||||
import { fromNow } from '../../../utils/date';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
|
||||
function prettyIosDistributionType(distributionType: IosDistributionType): string {
|
||||
switch (distributionType) {
|
||||
case IosDistributionType.AppStore:
|
||||
return 'App Store';
|
||||
case IosDistributionType.AdHoc:
|
||||
return 'Ad Hoc';
|
||||
case IosDistributionType.Development:
|
||||
return 'Development';
|
||||
case IosDistributionType.Enterprise:
|
||||
return 'Enterprise';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
export function displayEmptyIosCredentials(appLookupParams: AppLookupParams): void {
|
||||
const { projectName, bundleIdentifier } = appLookupParams;
|
||||
log(chalk.bold(`iOS Credentials`));
|
||||
log(` Project: ${projectName}`);
|
||||
log(` Bundle Identifier: ${bundleIdentifier}`);
|
||||
log(` No credentials set up yet!`);
|
||||
}
|
||||
|
||||
/**
|
||||
* sort a build credentials array in descending order of preference
|
||||
*/
|
||||
function sortBuildCredentialsByDistributionType(
|
||||
iosAppBuildCredentialsArray: IosAppBuildCredentialsFragment[]
|
||||
): IosAppBuildCredentialsFragment[] {
|
||||
// The order in which we choose the distribution type from least to most preferred
|
||||
const typePriority = [
|
||||
IosDistributionType.Development,
|
||||
IosDistributionType.AdHoc,
|
||||
IosDistributionType.Enterprise,
|
||||
IosDistributionType.AppStore,
|
||||
];
|
||||
return iosAppBuildCredentialsArray
|
||||
.sort(
|
||||
(buildCredentialsA, buildCredentialsB) =>
|
||||
typePriority.indexOf(buildCredentialsA.iosDistributionType) -
|
||||
typePriority.indexOf(buildCredentialsB.iosDistributionType)
|
||||
)
|
||||
.reverse();
|
||||
}
|
||||
|
||||
export function displayIosAppCredentials(credentials: CommonIosAppCredentialsFragment): void {
|
||||
log(chalk.bold(`iOS Credentials`));
|
||||
log(` Project: ${credentials.app.fullName}`);
|
||||
log(` Bundle Identifier: ${credentials.appleAppIdentifier.bundleIdentifier}`);
|
||||
const appleTeam = credentials.appleTeam;
|
||||
if (appleTeam) {
|
||||
const { appleTeamIdentifier, appleTeamName } = appleTeam;
|
||||
log(` Apple Team: ${appleTeamIdentifier} ${appleTeamName ? `(${appleTeamName})` : ''}`);
|
||||
}
|
||||
log.newLine();
|
||||
|
||||
if (credentials.iosAppBuildCredentialsArray.length === 0) {
|
||||
log(` Configuration: None setup yet`);
|
||||
log.newLine();
|
||||
return;
|
||||
}
|
||||
const sortedIosAppBuildCredentialsArray = sortBuildCredentialsByDistributionType(
|
||||
credentials.iosAppBuildCredentialsArray
|
||||
);
|
||||
for (const iosAppBuildCredentials of sortedIosAppBuildCredentialsArray) {
|
||||
displayIosAppBuildCredentials(iosAppBuildCredentials);
|
||||
}
|
||||
}
|
||||
|
||||
function displayIosAppBuildCredentials(buildCredentials: IosAppBuildCredentialsFragment): void {
|
||||
log(
|
||||
chalk.bold(
|
||||
` Configuration: ${prettyIosDistributionType(buildCredentials.iosDistributionType)}`
|
||||
)
|
||||
);
|
||||
const maybeDistCert = buildCredentials.distributionCertificate;
|
||||
log(` Distribution Certificate:`);
|
||||
if (maybeDistCert) {
|
||||
const { serialNumber, updatedAt, validityNotAfter, appleTeam } = maybeDistCert;
|
||||
log(` Serial Number: ${serialNumber}`);
|
||||
log(` Expiration Date: ${dateformat(validityNotAfter, 'expiresHeaderFormat')}`);
|
||||
if (appleTeam) {
|
||||
const { appleTeamIdentifier, appleTeamName } = appleTeam;
|
||||
log(` Apple Team: ${appleTeamIdentifier} ${appleTeamName ? `(${appleTeamName})` : ''}`);
|
||||
}
|
||||
log(` Updated ${fromNow(new Date(updatedAt))} ago`);
|
||||
} else {
|
||||
log(` None assigned yet`);
|
||||
}
|
||||
log.newLine();
|
||||
|
||||
const maybeProvProf = buildCredentials.provisioningProfile;
|
||||
log(` Provisioning Profile:`);
|
||||
if (maybeProvProf) {
|
||||
const { expiration, updatedAt, status, developerPortalIdentifier, appleTeam } = maybeProvProf;
|
||||
if (developerPortalIdentifier) {
|
||||
log(` Developer Portal ID: ${developerPortalIdentifier}`);
|
||||
}
|
||||
log(` Status: ${status}`);
|
||||
log(` Expiration Date: ${dateformat(expiration, 'expiresHeaderFormat')}`);
|
||||
if (appleTeam) {
|
||||
const { appleTeamIdentifier, appleTeamName } = appleTeam;
|
||||
log(` Apple Team: ${appleTeamIdentifier} ${appleTeamName ? `(${appleTeamName})` : ''}`);
|
||||
}
|
||||
log(` Updated ${fromNow(new Date(updatedAt))} ago`);
|
||||
} else {
|
||||
log(` None assigned yet`);
|
||||
}
|
||||
log.newLine();
|
||||
}
|
||||
@@ -27,46 +27,58 @@ export class ManageAndroidApp implements Action {
|
||||
constructor(private projectFullName: string) {}
|
||||
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
manager.pushNextAction(this);
|
||||
const credentials = await ctx.android.fetchCredentialsAsync(this.projectFullName);
|
||||
while (true) {
|
||||
try {
|
||||
const credentials = await ctx.android.fetchCredentialsAsync(this.projectFullName);
|
||||
|
||||
if (isEmpty(credentials.keystore) && isEmpty(credentials.pushCredentials)) {
|
||||
log(`No credentials available for ${this.projectFullName}.\n`);
|
||||
} else {
|
||||
log.newLine();
|
||||
await printAndroidAppCredentials(credentials);
|
||||
}
|
||||
if (isEmpty(credentials.keystore) && isEmpty(credentials.pushCredentials)) {
|
||||
log(`No credentials available for ${this.projectFullName}.\n`);
|
||||
} else {
|
||||
log.newLine();
|
||||
await printAndroidAppCredentials(credentials);
|
||||
}
|
||||
|
||||
const { action } = await promptAsync([
|
||||
{
|
||||
type: 'select',
|
||||
name: 'action',
|
||||
message: 'What do you want to do?',
|
||||
choices: [
|
||||
const { action } = await promptAsync([
|
||||
{
|
||||
value: ActionType.UpdateCredentialsJson,
|
||||
title: 'Update credentials.json with values from Expo servers',
|
||||
type: 'select',
|
||||
name: 'action',
|
||||
message: 'What do you want to do?',
|
||||
choices: [
|
||||
{
|
||||
value: ActionType.UpdateCredentialsJson,
|
||||
title: 'Update credentials.json with values from EAS servers',
|
||||
},
|
||||
{
|
||||
value: ActionType.SetupBuildCredentialsFromCredentialsJson,
|
||||
title: 'Update credentials on EAS servers with values from credentials.json',
|
||||
},
|
||||
{ value: ActionType.UpdateKeystore, title: 'Update Keystore' },
|
||||
{ value: ActionType.RemoveKeystore, title: 'Remove Keystore' },
|
||||
{
|
||||
value: ActionType.DownloadKeystore,
|
||||
title: 'Download Keystore from the EAS servers',
|
||||
},
|
||||
{ value: ActionType.UpdateFcmKey, title: 'Update FCM API Key' },
|
||||
{ value: ActionType.GoBack, title: 'Go back to project list' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: ActionType.SetupBuildCredentialsFromCredentialsJson,
|
||||
title: 'Update credentials on Expo servers with values from credentials.json',
|
||||
},
|
||||
{ value: ActionType.UpdateKeystore, title: 'Update Keystore' },
|
||||
{ value: ActionType.RemoveKeystore, title: 'Remove Keystore' },
|
||||
{ value: ActionType.DownloadKeystore, title: 'Download Keystore from the Expo servers' },
|
||||
{ value: ActionType.UpdateFcmKey, title: 'Update FCM API Key' },
|
||||
{ value: ActionType.GoBack, title: 'Go back to project list' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
]);
|
||||
|
||||
if (action === ActionType.GoBack) {
|
||||
manager.popAction();
|
||||
return;
|
||||
if (action === ActionType.GoBack) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await manager.runActionAsync(this.getAction(ctx, action));
|
||||
} catch (err) {
|
||||
log.error(err);
|
||||
}
|
||||
await manager.runActionAsync(new PressAnyKeyToContinue());
|
||||
} catch (err) {
|
||||
log.error(err);
|
||||
await manager.runActionAsync(new PressAnyKeyToContinue());
|
||||
}
|
||||
}
|
||||
|
||||
manager.pushNextAction(new PressAnyKeyToContinue());
|
||||
manager.pushNextAction(this.getAction(ctx, action));
|
||||
}
|
||||
|
||||
private getAction(context: Context, selected: ActionType): Action {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DistributionType } from '@expo/eas-json';
|
||||
|
||||
import log from '../../log';
|
||||
import { getProjectAccountName } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { ensureActorHasUsername } from '../../user/actions';
|
||||
@@ -32,66 +33,76 @@ enum ActionType {
|
||||
|
||||
export class ManageIos implements Action {
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
manager.pushNextAction(this);
|
||||
await ctx.bestEffortAppStoreAuthenticateAsync();
|
||||
while (true) {
|
||||
try {
|
||||
await ctx.bestEffortAppStoreAuthenticateAsync();
|
||||
|
||||
const accountName = ctx.hasProjectContext
|
||||
? getProjectAccountName(ctx.exp, ctx.user)
|
||||
: ensureActorHasUsername(ctx.user);
|
||||
const iosCredentials = await ctx.ios.getAllCredentialsAsync(accountName);
|
||||
const accountName = ctx.hasProjectContext
|
||||
? getProjectAccountName(ctx.exp, ctx.user)
|
||||
: ensureActorHasUsername(ctx.user);
|
||||
const iosCredentials = await ctx.ios.getAllCredentialsAsync(accountName);
|
||||
|
||||
await displayAllIosCredentials(iosCredentials);
|
||||
await displayAllIosCredentials(iosCredentials);
|
||||
|
||||
const projectSpecificActions: { value: ActionType; title: string }[] = ctx.hasProjectContext
|
||||
? [
|
||||
{
|
||||
// This command will be triggered during build to ensure all credentials are ready
|
||||
// I'm leaving it here for now to simplify testing
|
||||
value: ActionType.SetupBuildCredentials,
|
||||
title: 'Ensure all credentials for project are valid',
|
||||
},
|
||||
{
|
||||
value: ActionType.UpdateCredentialsJson,
|
||||
title: 'Update credentials.json with values from Expo servers',
|
||||
},
|
||||
{
|
||||
value: ActionType.SetupBuildCredentialsFromCredentialsJson,
|
||||
title: 'Update credentials on Expo servers with values from credentials.json',
|
||||
},
|
||||
{
|
||||
value: ActionType.UseExistingDistributionCertificate,
|
||||
title: 'Use existing Distribution Certificate in current project',
|
||||
},
|
||||
{
|
||||
value: ActionType.RemoveSpecificProvisioningProfile,
|
||||
title: 'Remove Provisioning Profile from current project',
|
||||
},
|
||||
]
|
||||
: [];
|
||||
const projectSpecificActions: { value: ActionType; title: string }[] = ctx.hasProjectContext
|
||||
? [
|
||||
{
|
||||
// This command will be triggered during build to ensure all credentials are ready
|
||||
// I'm leaving it here for now to simplify testing
|
||||
value: ActionType.SetupBuildCredentials,
|
||||
title: 'Ensure all credentials for project are valid',
|
||||
},
|
||||
{
|
||||
value: ActionType.UpdateCredentialsJson,
|
||||
title: 'Update credentials.json with values from EAS servers',
|
||||
},
|
||||
{
|
||||
value: ActionType.SetupBuildCredentialsFromCredentialsJson,
|
||||
title: 'Update credentials on EAS servers with values from credentials.json',
|
||||
},
|
||||
{
|
||||
value: ActionType.UseExistingDistributionCertificate,
|
||||
title: 'Use existing Distribution Certificate in current project',
|
||||
},
|
||||
{
|
||||
value: ActionType.RemoveSpecificProvisioningProfile,
|
||||
title: 'Remove Provisioning Profile from current project',
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const { action } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'action',
|
||||
message: 'What do you want to do?',
|
||||
choices: [
|
||||
...projectSpecificActions,
|
||||
{
|
||||
value: ActionType.CreateDistributionCertificate,
|
||||
title: 'Add new Distribution Certificate',
|
||||
},
|
||||
{
|
||||
value: ActionType.RemoveDistributionCertificate,
|
||||
title: 'Remove Distribution Certificate',
|
||||
},
|
||||
{
|
||||
value: ActionType.UpdateDistributionCertificate,
|
||||
title: 'Update Distribution Certificate',
|
||||
},
|
||||
],
|
||||
});
|
||||
const { action } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'action',
|
||||
message: 'What do you want to do?',
|
||||
choices: [
|
||||
...projectSpecificActions,
|
||||
{
|
||||
value: ActionType.CreateDistributionCertificate,
|
||||
title: 'Add new Distribution Certificate',
|
||||
},
|
||||
{
|
||||
value: ActionType.RemoveDistributionCertificate,
|
||||
title: 'Remove Distribution Certificate',
|
||||
},
|
||||
{
|
||||
value: ActionType.UpdateDistributionCertificate,
|
||||
title: 'Update Distribution Certificate',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
manager.pushNextAction(new PressAnyKeyToContinue());
|
||||
manager.pushNextAction(this.getAction(manager, ctx, accountName, action));
|
||||
try {
|
||||
await manager.runActionAsync(this.getAction(manager, ctx, accountName, action));
|
||||
} catch (err) {
|
||||
log.error(err);
|
||||
}
|
||||
await manager.runActionAsync(new PressAnyKeyToContinue());
|
||||
} catch (err) {
|
||||
log.error(err);
|
||||
await manager.runActionAsync(new PressAnyKeyToContinue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getAppLookupParamsFromContext(ctx: Context): AppLookupParams {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import log from '../../log';
|
||||
import { getProjectAccountName, getProjectConfigDescription } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { ensureActorHasUsername } from '../../user/actions';
|
||||
import { Action, CredentialsManager } from '../CredentialsManager';
|
||||
import { Context } from '../context';
|
||||
//import { RemoveDistributionCertificateBeta } from '../ios/actions/RemoveDistributionCertificateBeta';
|
||||
import { AppLookupParams } from '../ios/api/GraphqlClient';
|
||||
import {
|
||||
displayEmptyIosCredentials,
|
||||
displayIosAppCredentials,
|
||||
} from '../ios/utils/printCredentialsBeta';
|
||||
import { PressAnyKeyToContinue } from './HelperActions';
|
||||
|
||||
enum ActionType {
|
||||
SetupBuildCredentials,
|
||||
SetupBuildCredentialsFromCredentialsJson,
|
||||
UpdateCredentialsJson,
|
||||
UseExistingDistributionCertificate,
|
||||
RemoveSpecificProvisioningProfile,
|
||||
CreateDistributionCertificate,
|
||||
UpdateDistributionCertificate,
|
||||
RemoveDistributionCertificate,
|
||||
}
|
||||
|
||||
export class ManageIosBeta implements Action {
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
while (true) {
|
||||
try {
|
||||
await ctx.bestEffortAppStoreAuthenticateAsync();
|
||||
|
||||
const accountName = ctx.hasProjectContext
|
||||
? getProjectAccountName(ctx.exp, ctx.user)
|
||||
: ensureActorHasUsername(ctx.user);
|
||||
|
||||
const account = findAccountByName(ctx.user.accounts, accountName);
|
||||
if (!account) {
|
||||
throw new Error(`You do not have access to account: ${accountName}`);
|
||||
}
|
||||
if (ctx.hasProjectContext) {
|
||||
const appLookupParams = this.getAppLookupParamsFromContext(ctx);
|
||||
const iosAppCredentials = await ctx.newIos.getIosAppCredentialsWithBuildCredentialsAsync(
|
||||
appLookupParams
|
||||
);
|
||||
if (!iosAppCredentials) {
|
||||
displayEmptyIosCredentials(appLookupParams);
|
||||
} else {
|
||||
displayIosAppCredentials(iosAppCredentials);
|
||||
}
|
||||
}
|
||||
|
||||
const projectSpecificActions: { value: ActionType; title: string }[] = ctx.hasProjectContext
|
||||
? []
|
||||
: [];
|
||||
|
||||
const { action } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'action',
|
||||
message: 'What do you want to do?',
|
||||
choices: [
|
||||
...projectSpecificActions,
|
||||
{
|
||||
value: ActionType.RemoveDistributionCertificate,
|
||||
title: 'Remove Distribution Certificate',
|
||||
},
|
||||
],
|
||||
});
|
||||
try {
|
||||
await manager.runActionAsync(this.getAction(ctx, accountName, action));
|
||||
} catch (err) {
|
||||
log.error(err);
|
||||
}
|
||||
await manager.runActionAsync(new PressAnyKeyToContinue());
|
||||
} catch (err) {
|
||||
log.error(err);
|
||||
await manager.runActionAsync(new PressAnyKeyToContinue());
|
||||
} finally {
|
||||
// TODO remove finally block before committing!
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getAppLookupParamsFromContext(ctx: Context): AppLookupParams {
|
||||
ctx.ensureProjectContext();
|
||||
const projectName = ctx.exp.slug;
|
||||
const accountName = getProjectAccountName(ctx.exp, ctx.user);
|
||||
|
||||
const account = findAccountByName(ctx.user.accounts, accountName);
|
||||
if (!account) {
|
||||
throw new Error(`You do not have access to account: ${accountName}`);
|
||||
}
|
||||
|
||||
const bundleIdentifier = ctx.exp.ios?.bundleIdentifier;
|
||||
if (!bundleIdentifier) {
|
||||
throw new Error(
|
||||
`ios.bundleIdentifier needs to be defined in your ${getProjectConfigDescription(
|
||||
ctx.projectDir
|
||||
)} file`
|
||||
);
|
||||
}
|
||||
|
||||
return { account, projectName, bundleIdentifier };
|
||||
}
|
||||
|
||||
private getAction(ctx: Context, accountName: string, action: ActionType): Action {
|
||||
switch (action) {
|
||||
/* case ActionType.RemoveDistributionCertificate:
|
||||
return new RemoveDistributionCertificateBeta(accountName); */
|
||||
default:
|
||||
throw new Error('Unknown action selected');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import log from '../../log';
|
||||
import { getProjectAccountName } from '../../project/projectUtils';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { Action, CredentialsManager, QuitError } from '../CredentialsManager';
|
||||
import { Action, CredentialsManager } from '../CredentialsManager';
|
||||
import { AndroidCredentials } from '../android/credentials';
|
||||
import { printAndroidCredentials } from '../android/utils/printCredentials';
|
||||
import { Context } from '../context';
|
||||
@@ -13,41 +14,46 @@ export class SelectAndroidApp implements Action {
|
||||
constructor(private askAboutProjectMode = true) {}
|
||||
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
manager.pushNextAction(this);
|
||||
const firstRun = this.firstRun;
|
||||
this.firstRun = false;
|
||||
while (true) {
|
||||
try {
|
||||
const firstRun = this.firstRun;
|
||||
this.firstRun = false;
|
||||
|
||||
if (ctx.hasProjectContext && this.askAboutProjectMode && firstRun) {
|
||||
const projectFullName = `@${getProjectAccountName(ctx.exp, ctx.user)}/${ctx.exp.slug}`;
|
||||
const runProjectContext = await confirmAsync({
|
||||
message: `You are currently in a directory with project ${chalk.green(
|
||||
projectFullName
|
||||
)}. Do you want to select it?`,
|
||||
});
|
||||
if (ctx.hasProjectContext && this.askAboutProjectMode && firstRun) {
|
||||
const projectFullName = `@${getProjectAccountName(ctx.exp, ctx.user)}/${ctx.exp.slug}`;
|
||||
const runProjectContext = await confirmAsync({
|
||||
message: `You are currently in a directory with project ${chalk.green(
|
||||
projectFullName
|
||||
)}. Do you want to select it?`,
|
||||
});
|
||||
|
||||
if (runProjectContext) {
|
||||
manager.pushNextAction(new ManageAndroidApp(projectFullName));
|
||||
return;
|
||||
if (runProjectContext) {
|
||||
await manager.runActionAsync(new ManageAndroidApp(projectFullName));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const credentials = await ctx.android.fetchAllAsync();
|
||||
await printAndroidCredentials(Object.values(credentials));
|
||||
|
||||
const appChoices = Object.values(credentials).map((cred: AndroidCredentials) => ({
|
||||
title: cred.experienceName,
|
||||
value: cred.experienceName,
|
||||
}));
|
||||
const { projectFullName } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'projectFullName',
|
||||
message: 'Select application',
|
||||
choices: [...appChoices, { title: '[Quit]', value: 'quit' }],
|
||||
});
|
||||
|
||||
if (projectFullName === 'quit') {
|
||||
return;
|
||||
}
|
||||
await manager.runActionAsync(new ManageAndroidApp(projectFullName));
|
||||
} catch (err) {
|
||||
log.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
const credentials = await ctx.android.fetchAllAsync();
|
||||
await printAndroidCredentials(Object.values(credentials));
|
||||
|
||||
const appChoices = Object.values(credentials).map((cred: AndroidCredentials) => ({
|
||||
title: cred.experienceName,
|
||||
value: cred.experienceName,
|
||||
}));
|
||||
const { projectFullName } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'projectFullName',
|
||||
message: 'Select application',
|
||||
choices: [...appChoices, { title: '[Quit]', value: 'quit' }],
|
||||
});
|
||||
|
||||
if (projectFullName === 'quit') {
|
||||
throw new QuitError();
|
||||
}
|
||||
manager.pushNextAction(new ManageAndroidApp(projectFullName));
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,6 @@ export class SelectPlatform implements Action {
|
||||
],
|
||||
});
|
||||
const action = platform === 'ios' ? new ManageIos() : new SelectAndroidApp();
|
||||
manager.pushNextAction(action);
|
||||
await manager.runActionAsync(action);
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,11 @@ import chalk from 'chalk';
|
||||
|
||||
import { AppleTeamMutation } from '../credentials/ios/api/graphql/mutations/AppleTeamMutation';
|
||||
import { AppleTeamQuery } from '../credentials/ios/api/graphql/queries/AppleTeamQuery';
|
||||
import { AppleTeam } from '../graphql/generated';
|
||||
import { AppleTeamFragment } from '../graphql/generated';
|
||||
import log from '../log';
|
||||
import { getProjectAccountNameAsync } from '../project/projectUtils';
|
||||
import { Choice, confirmAsync, promptAsync } from '../prompts';
|
||||
import { Account, ensureAccounts, findAccountByName } from '../user/Account';
|
||||
import { Account, findAccountByName } from '../user/Account';
|
||||
import { Actor } from '../user/User';
|
||||
import { getActorDisplayName } from '../user/actions';
|
||||
import DeviceCreateAction from './actions/create/action';
|
||||
@@ -61,10 +61,7 @@ export class AccountResolver {
|
||||
assert(this.projectDir, 'project directory is not set ');
|
||||
|
||||
const projectAccountName = await getProjectAccountNameAsync(this.projectDir);
|
||||
const projectAccount = findAccountByName(
|
||||
ensureAccounts(this.user.accounts),
|
||||
projectAccountName
|
||||
);
|
||||
const projectAccount = findAccountByName(this.user.accounts, projectAccountName);
|
||||
if (!projectAccount) {
|
||||
log.warn(
|
||||
`Your account (${getActorDisplayName(this.user)}) doesn't have access to the ${chalk.bold(
|
||||
@@ -80,13 +77,11 @@ export class AccountResolver {
|
||||
)} account?`,
|
||||
});
|
||||
|
||||
if (useProjectAccount) {
|
||||
return projectAccount;
|
||||
}
|
||||
return useProjectAccount ? projectAccount : undefined;
|
||||
}
|
||||
|
||||
private async promptForAccountAsync(): Promise<Account> {
|
||||
const choices: Choice[] = ensureAccounts(this.user.accounts).map(account => ({
|
||||
const choices: Choice[] = this.user.accounts.map(account => ({
|
||||
title: account.name,
|
||||
value: account,
|
||||
}));
|
||||
@@ -103,7 +98,7 @@ export class AccountResolver {
|
||||
async function ensureAppleTeamExistsAsync(
|
||||
accountId: string,
|
||||
{ appleTeamIdentifier, appleTeamName }: { appleTeamIdentifier: string; appleTeamName: string }
|
||||
): Promise<AppleTeam> {
|
||||
): Promise<AppleTeamFragment> {
|
||||
const appleTeam = await AppleTeamQuery.getByAppleTeamIdentifierAsync(
|
||||
accountId,
|
||||
appleTeamIdentifier
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface Fragment {
|
||||
name: string;
|
||||
definition: string;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,56 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import { PublishUpdateGroupInput, Update, UpdatePublishMutation } from '../generated';
|
||||
|
||||
const PublishMutation = {
|
||||
async getUploadURLsAsync(contentTypes: string[]): Promise<{ specifications: string[] }> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<
|
||||
{ asset: { getSignedAssetUploadSpecifications: { specifications: string[] } } },
|
||||
{ contentTypes: string[] }
|
||||
>(
|
||||
gql`
|
||||
mutation GetSignedUploadMutation($contentTypes: [String!]!) {
|
||||
asset {
|
||||
getSignedAssetUploadSpecifications(assetContentTypes: $contentTypes) {
|
||||
specifications
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
contentTypes,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.asset.getSignedAssetUploadSpecifications;
|
||||
},
|
||||
|
||||
async publishUpdateGroupAsync(
|
||||
publishUpdateGroupInput: PublishUpdateGroupInput
|
||||
): Promise<Pick<Update, 'id' | 'updateGroup'>> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<UpdatePublishMutation>(
|
||||
gql`
|
||||
mutation UpdatePublishMutation($publishUpdateGroupInput: PublishUpdateGroupInput) {
|
||||
updateRelease {
|
||||
publishUpdateGroup(publishUpdateGroupInput: $publishUpdateGroupInput) {
|
||||
id
|
||||
updateGroup
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ publishUpdateGroupInput }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.updateRelease.publishUpdateGroup[0]!;
|
||||
},
|
||||
};
|
||||
|
||||
export { PublishMutation };
|
||||
@@ -1,24 +1,32 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import { Build } from '../generated';
|
||||
import {
|
||||
AppPlatform,
|
||||
Build,
|
||||
BuildsByIdQuery,
|
||||
BuildsForAppQuery,
|
||||
PendingBuildsForAccountAndPlatformQuery,
|
||||
} from '../generated';
|
||||
|
||||
type Filters = Partial<Pick<Build, 'platform' | 'status'>> & {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
type BuildQueryResult = Pick<Build, 'platform' | 'artifacts'>;
|
||||
type BuildQueryResult = Pick<Build, 'id' | 'platform' | 'artifacts'>;
|
||||
type PendingBuildQueryResult = Pick<Build, 'id' | 'platform'>;
|
||||
|
||||
const BuildQuery = {
|
||||
async byIdAsync(buildId: string): Promise<BuildQueryResult> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{ builds: { byId: BuildQueryResult } }>(
|
||||
.query<BuildsByIdQuery>(
|
||||
gql`
|
||||
query BuildsByIdQuery($buildId: ID!) {
|
||||
builds {
|
||||
byId(buildId: $buildId) {
|
||||
id
|
||||
platform
|
||||
artifacts {
|
||||
buildUrl
|
||||
@@ -37,7 +45,7 @@ const BuildQuery = {
|
||||
async allForAppAsync(appId: string, filters?: Filters): Promise<BuildQueryResult[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{ builds: { allForApp: BuildQueryResult[] } }>(
|
||||
.query<BuildsForAppQuery>(
|
||||
// TODO: Change $appId: String! to ID! when fixed server-side schema
|
||||
gql`
|
||||
query BuildsForAppQuery(
|
||||
@@ -55,6 +63,7 @@ const BuildQuery = {
|
||||
platform: $platform
|
||||
status: $status
|
||||
) {
|
||||
id
|
||||
platform
|
||||
artifacts {
|
||||
buildUrl
|
||||
@@ -68,7 +77,55 @@ const BuildQuery = {
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.builds.allForApp;
|
||||
return data.builds.allForApp as BuildQueryResult[];
|
||||
},
|
||||
|
||||
async getPendingBuildIdAsync(
|
||||
accountName: string,
|
||||
platform: AppPlatform
|
||||
): Promise<PendingBuildQueryResult | null> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<PendingBuildsForAccountAndPlatformQuery>(
|
||||
gql`
|
||||
query PendingBuildsForAccountAndPlatform(
|
||||
$accountName: String!
|
||||
$platform: AppPlatform!
|
||||
) {
|
||||
account {
|
||||
byName(accountName: $accountName) {
|
||||
id
|
||||
inQueueBuilds: builds(
|
||||
offset: 0
|
||||
limit: 1
|
||||
platform: $platform
|
||||
status: IN_QUEUE
|
||||
) {
|
||||
id
|
||||
platform
|
||||
}
|
||||
inProgressBuilds: builds(
|
||||
offset: 0
|
||||
limit: 1
|
||||
platform: $platform
|
||||
status: IN_PROGRESS
|
||||
) {
|
||||
id
|
||||
platform
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ accountName, platform }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const pendingBuilds = [
|
||||
...data.account.byName.inProgressBuilds,
|
||||
...data.account.byName.inQueueBuilds,
|
||||
];
|
||||
return pendingBuilds.length > 0 ? pendingBuilds[0] : null;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import { Project } from '../generated';
|
||||
import { Project, ProjectByUsernameAndSlugQuery } from '../generated';
|
||||
|
||||
type ProjectQueryResult = Pick<Project, 'id'>;
|
||||
|
||||
@@ -9,9 +9,9 @@ const ProjectQuery = {
|
||||
async byUsernameAndSlugAsync(username: string, slug: string): Promise<ProjectQueryResult> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{ project: { byUsernameAndSlug: ProjectQueryResult } }>(
|
||||
.query<ProjectByUsernameAndSlugQuery>(
|
||||
gql`
|
||||
query($username: String!, $slug: String!) {
|
||||
query ProjectByUsernameAndSlugQuery($username: String!, $slug: String!) {
|
||||
project {
|
||||
byUsernameAndSlug(username: $username, slug: $slug) {
|
||||
id
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import { AssetMetadataResult, GetAssetMetadataQuery } from '../generated';
|
||||
|
||||
const PublishQuery = {
|
||||
async getAssetMetadataAsync(storageKeys: string[]): Promise<AssetMetadataResult[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<GetAssetMetadataQuery>(
|
||||
gql`
|
||||
query GetAssetMetadataQuery($storageKeys: [String!]!) {
|
||||
asset {
|
||||
metadata(storageKeys: $storageKeys) {
|
||||
storageKey
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
storageKeys,
|
||||
},
|
||||
{ requestPolicy: 'network-only' } // Since we reptitively query this to monitor the asset upload, we need to ensure it is not cached.
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.asset.metadata;
|
||||
},
|
||||
};
|
||||
|
||||
export { PublishQuery };
|
||||
@@ -23,6 +23,7 @@ const UserQuery = {
|
||||
id
|
||||
name
|
||||
}
|
||||
isExpoAdmin
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Fragment } from '../fragment';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const AppFragment: Fragment = {
|
||||
name: 'app',
|
||||
definition: `
|
||||
fragment app on App {
|
||||
id
|
||||
}
|
||||
`,
|
||||
};
|
||||
export const AppFragmentNode = gql`
|
||||
fragment AppFragment on App {
|
||||
id
|
||||
fullName
|
||||
}
|
||||
`;
|
||||
@@ -1,11 +1,8 @@
|
||||
import { Fragment } from '../../fragment';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const AppleAppIdentifierFragment: Fragment = {
|
||||
name: 'appleAppIdentifier',
|
||||
definition: `
|
||||
fragment appleAppIdentifier on AppleAppIdentifier {
|
||||
id
|
||||
bundleIdentifier
|
||||
}
|
||||
`,
|
||||
};
|
||||
export const AppleAppIdentifierFragmentNode = gql`
|
||||
fragment AppleAppIdentifierFragment on AppleAppIdentifier {
|
||||
id
|
||||
bundleIdentifier
|
||||
}
|
||||
`;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Fragment } from '../../fragment';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { AppleDeviceClass } from '../../generated';
|
||||
|
||||
export const APPLE_DEVICE_CLASS_LABELS: Record<AppleDeviceClass, string> = {
|
||||
@@ -6,15 +7,12 @@ export const APPLE_DEVICE_CLASS_LABELS: Record<AppleDeviceClass, string> = {
|
||||
[AppleDeviceClass.Iphone]: 'iPhone',
|
||||
};
|
||||
|
||||
export const AppleDeviceFragment: Fragment = {
|
||||
name: 'appleDevice',
|
||||
definition: `
|
||||
fragment appleDevice on AppleDevice {
|
||||
id
|
||||
identifier
|
||||
name
|
||||
model
|
||||
deviceClass
|
||||
}
|
||||
`,
|
||||
};
|
||||
export const AppleDeviceFragmentNode = gql`
|
||||
fragment AppleDeviceFragment on AppleDevice {
|
||||
id
|
||||
identifier
|
||||
name
|
||||
model
|
||||
deviceClass
|
||||
}
|
||||
`;
|
||||
@@ -1,10 +1,7 @@
|
||||
import { Fragment } from '../../fragment';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const AppleDeviceRegistrationRequestFragment: Fragment = {
|
||||
name: 'appleDeviceRegistrationRequest',
|
||||
definition: `
|
||||
fragment appleDeviceRegistrationRequest on AppleDeviceRegistrationRequest {
|
||||
id
|
||||
}
|
||||
`,
|
||||
};
|
||||
export const AppleDeviceRegistrationRequestFragmentNode = gql`
|
||||
fragment AppleDeviceRegistrationRequestFragment on AppleDeviceRegistrationRequest {
|
||||
id
|
||||
}
|
||||
`;
|
||||
@@ -1,21 +1,18 @@
|
||||
import { Fragment } from '../../fragment';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const AppleDistributionCertificateFragment: Fragment = {
|
||||
name: 'appleDistCert',
|
||||
definition: `
|
||||
fragment appleDistCert on AppleDistributionCertificate {
|
||||
export const AppleDistributionCertificateFragmentNode = gql`
|
||||
fragment AppleDistributionCertificateFragment on AppleDistributionCertificate {
|
||||
id
|
||||
certificateP12
|
||||
certificatePassword
|
||||
serialNumber
|
||||
developerPortalIdentifier
|
||||
validityNotBefore
|
||||
validityNotAfter
|
||||
appleTeam {
|
||||
id
|
||||
certificateP12
|
||||
certificatePassword
|
||||
serialNumber
|
||||
developerPortalIdentifier
|
||||
validityNotBefore
|
||||
validityNotAfter
|
||||
appleTeam {
|
||||
id
|
||||
appleTeamIdentifier
|
||||
appleTeamName
|
||||
}
|
||||
appleTeamIdentifier
|
||||
appleTeamName
|
||||
}
|
||||
`,
|
||||
};
|
||||
}
|
||||
`;
|
||||
@@ -1,25 +1,22 @@
|
||||
import { Fragment } from '../../fragment';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const AppleProvisioningProfileFragment: Fragment = {
|
||||
name: 'appleProvisioningProfile',
|
||||
definition: `
|
||||
fragment appleProvisioningProfile on AppleProvisioningProfile {
|
||||
export const AppleProvisioningProfileFragmentNode = gql`
|
||||
fragment AppleProvisioningProfileFragment on AppleProvisioningProfile {
|
||||
id
|
||||
expiration
|
||||
developerPortalIdentifier
|
||||
provisioningProfile
|
||||
appleTeam {
|
||||
id
|
||||
expiration
|
||||
developerPortalIdentifier
|
||||
provisioningProfile
|
||||
appleTeam {
|
||||
id
|
||||
appleTeamIdentifier
|
||||
appleTeamName
|
||||
}
|
||||
appleDevices {
|
||||
id
|
||||
identifier
|
||||
name
|
||||
model
|
||||
deviceClass
|
||||
}
|
||||
appleTeamIdentifier
|
||||
appleTeamName
|
||||
}
|
||||
`,
|
||||
};
|
||||
appleDevices {
|
||||
id
|
||||
identifier
|
||||
name
|
||||
model
|
||||
deviceClass
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,12 +1,9 @@
|
||||
import { Fragment } from '../../fragment';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const AppleTeamFragment: Fragment = {
|
||||
name: 'appleTeam',
|
||||
definition: `
|
||||
fragment appleTeam on AppleTeam {
|
||||
id
|
||||
appleTeamIdentifier
|
||||
appleTeamName
|
||||
}
|
||||
`,
|
||||
};
|
||||
export const AppleTeamFragmentNode = gql`
|
||||
fragment AppleTeamFragment on AppleTeam {
|
||||
id
|
||||
appleTeamIdentifier
|
||||
appleTeamName
|
||||
}
|
||||
`;
|
||||
@@ -1,55 +1,43 @@
|
||||
import { Fragment } from '../../fragment';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const IosAppBuildCredentialsFragment: Fragment = {
|
||||
name: 'iosAppBuildCredentials',
|
||||
definition: `
|
||||
fragment iosAppBuildCredentials on IosAppBuildCredentials {
|
||||
export const IosAppBuildCredentialsFragmentNode = gql`
|
||||
fragment IosAppBuildCredentialsFragment on IosAppBuildCredentials {
|
||||
id
|
||||
iosDistributionType
|
||||
distributionCertificate {
|
||||
id
|
||||
iosDistributionType
|
||||
distributionCertificate {
|
||||
certificateP12
|
||||
certificatePassword
|
||||
serialNumber
|
||||
developerPortalIdentifier
|
||||
validityNotBefore
|
||||
validityNotAfter
|
||||
updatedAt
|
||||
appleTeam {
|
||||
id
|
||||
certificateP12
|
||||
certificatePassword
|
||||
serialNumber
|
||||
developerPortalIdentifier
|
||||
validityNotBefore
|
||||
validityNotAfter
|
||||
appleTeam {
|
||||
id
|
||||
appleTeamIdentifier
|
||||
appleTeamName
|
||||
}
|
||||
}
|
||||
provisioningProfile {
|
||||
id
|
||||
expiration
|
||||
developerPortalIdentifier
|
||||
provisioningProfile
|
||||
appleDevices {
|
||||
id
|
||||
identifier
|
||||
name
|
||||
model
|
||||
deviceClass
|
||||
}
|
||||
appleTeam {
|
||||
id
|
||||
appleTeamIdentifier
|
||||
appleTeamName
|
||||
}
|
||||
appleTeamIdentifier
|
||||
appleTeamName
|
||||
}
|
||||
}
|
||||
provisioningProfile {
|
||||
id
|
||||
expiration
|
||||
developerPortalIdentifier
|
||||
provisioningProfile
|
||||
updatedAt
|
||||
status
|
||||
appleDevices {
|
||||
id
|
||||
identifier
|
||||
name
|
||||
model
|
||||
deviceClass
|
||||
appleTeam {
|
||||
id
|
||||
appleTeamIdentifier
|
||||
appleTeamName
|
||||
}
|
||||
}
|
||||
appleTeam {
|
||||
id
|
||||
appleTeamIdentifier
|
||||
appleTeamName
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
}
|
||||
`;
|
||||
@@ -1,10 +1,38 @@
|
||||
import { Fragment } from '../../fragment';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const IosAppCredentialsFragment: Fragment = {
|
||||
name: 'iosAppCredentials',
|
||||
definition: `
|
||||
fragment iosAppCredentials on IosAppCredentials {
|
||||
import { AppFragmentNode } from '../App';
|
||||
import { AppleAppIdentifierFragmentNode } from './AppleAppIdentifier';
|
||||
import { AppleTeamFragmentNode } from './AppleTeam';
|
||||
import { IosAppBuildCredentialsFragmentNode } from './IosAppBuildCredentials';
|
||||
|
||||
export const IosAppCredentialsFragmentNode = gql`
|
||||
fragment IosAppCredentialsFragment on IosAppCredentials {
|
||||
id
|
||||
}
|
||||
`;
|
||||
|
||||
export const CommonIosAppCredentialsFragmentNode = gql`
|
||||
fragment CommonIosAppCredentialsFragment on IosAppCredentials {
|
||||
id
|
||||
app {
|
||||
id
|
||||
...AppFragment
|
||||
}
|
||||
`,
|
||||
};
|
||||
appleTeam {
|
||||
id
|
||||
...AppleTeamFragment
|
||||
}
|
||||
appleAppIdentifier {
|
||||
id
|
||||
...AppleAppIdentifierFragment
|
||||
}
|
||||
iosAppBuildCredentialsArray {
|
||||
id
|
||||
...IosAppBuildCredentialsFragment
|
||||
}
|
||||
}
|
||||
${AppFragmentNode}
|
||||
${AppleTeamFragmentNode}
|
||||
${AppleAppIdentifierFragmentNode}
|
||||
${IosAppBuildCredentialsFragmentNode}
|
||||
`;
|
||||
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
||||
import figures from 'figures';
|
||||
import { boolish } from 'getenv';
|
||||
import ora from 'ora';
|
||||
import terminalLink from 'terminal-link';
|
||||
|
||||
type Color = (...text: string[]) => string;
|
||||
|
||||
@@ -86,6 +87,10 @@ log.isDebug = IS_DEBUG;
|
||||
* @param url
|
||||
*/
|
||||
export function learnMore(url: string, learnMoreMessage?: string): string {
|
||||
// Links can be disabled via env variables https://github.com/jamestalmage/supports-hyperlinks/blob/master/index.js
|
||||
if (terminalLink.isSupported) {
|
||||
return chalk.dim(terminalLink(learnMoreMessage ?? 'Learn more.', url));
|
||||
}
|
||||
return chalk.dim(`${learnMoreMessage ?? 'Learn more'}: ${chalk.underline(url)}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,17 @@ jest.mock('fs');
|
||||
|
||||
jest.mock('../../user/User');
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.log = originalConsoleLog;
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
|
||||
describe(findProjectRootAsync, () => {
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
|
||||
@@ -0,0 +1,444 @@
|
||||
import fs from 'fs';
|
||||
import mockdate from 'mockdate';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { AssetMetadataStatus } from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import { PublishQuery } from '../../graphql/queries/PublishQuery';
|
||||
import {
|
||||
MetadataJoi,
|
||||
Platforms,
|
||||
TIMEOUT_LIMIT,
|
||||
buildUpdateInfoGroupAsync,
|
||||
collectAssets,
|
||||
convertAssetToUpdateInfoGroupFormatAsync,
|
||||
filterOutAssetsThatAlreadyExistAsync,
|
||||
getBase64URLEncoding,
|
||||
getStorageKey,
|
||||
getStorageKeyForAssetAsync,
|
||||
guessContentTypeFromExtension,
|
||||
resolveInputDirectory,
|
||||
uploadAssetsAsync,
|
||||
} from '../publish';
|
||||
|
||||
jest.mock('../../uploads');
|
||||
jest.mock('fs');
|
||||
|
||||
const dummyFileBuffer = Buffer.from('dummy-file');
|
||||
fs.mkdirSync(path.resolve(), { recursive: true });
|
||||
fs.writeFileSync(path.resolve('md5-hash-of-file'), dummyFileBuffer);
|
||||
|
||||
describe('MetadataJoi', () => {
|
||||
it('passes correctly structured metadata', () => {
|
||||
const { error } = MetadataJoi.validate({
|
||||
version: 0,
|
||||
bundler: 'metro',
|
||||
fileMetadata: {
|
||||
android: {
|
||||
assets: [{ path: 'assets/3261e570d51777be1e99116562280926', ext: 'png' }],
|
||||
bundle: 'bundles/android.js',
|
||||
},
|
||||
ios: {
|
||||
assets: [{ path: 'assets/3261e570d51777be1e99116562280926', ext: 'png' }],
|
||||
bundle: 'bundles/ios.js',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(error).toBe(undefined);
|
||||
});
|
||||
it('fails if a bundle is missing', () => {
|
||||
const { error } = MetadataJoi.validate({
|
||||
version: 0,
|
||||
bundler: 'metro',
|
||||
fileMetadata: {
|
||||
android: {
|
||||
assets: [{ path: 'assets/3261e570d51777be1e99116562280926', ext: 'png' }],
|
||||
bundle: 'bundles/android.js',
|
||||
},
|
||||
ios: {
|
||||
assets: [{ path: 'assets/3261e570d51777be1e99116562280926', ext: 'png' }],
|
||||
bundle: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(error).toBeDefined();
|
||||
});
|
||||
it('passes metadata with no assets', () => {
|
||||
const { error } = MetadataJoi.validate({
|
||||
version: 0,
|
||||
bundler: 'metro',
|
||||
fileMetadata: {
|
||||
android: {
|
||||
assets: [],
|
||||
bundle: 'bundles/android.js',
|
||||
},
|
||||
ios: {
|
||||
assets: [],
|
||||
bundle: 'bundles/ios.js',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(error).toBe(undefined);
|
||||
});
|
||||
});
|
||||
describe(guessContentTypeFromExtension, () => {
|
||||
it('returns the correct content type for jpg', () => {
|
||||
expect(guessContentTypeFromExtension('jpg')).toBe('image/jpeg');
|
||||
});
|
||||
it('returns application/octet-stream when an ext is not recognized', () => {
|
||||
expect(guessContentTypeFromExtension('does-not-exist')).toBe('application/octet-stream');
|
||||
});
|
||||
it('returns application/octet-stream when an ext is undefined', () => {
|
||||
expect(guessContentTypeFromExtension(undefined)).toBe('application/octet-stream');
|
||||
});
|
||||
});
|
||||
|
||||
describe(getBase64URLEncoding, () => {
|
||||
it('computes the correct encoding', () => {
|
||||
expect(getBase64URLEncoding(Buffer.from('test-string'))).toBe('dGVzdC1zdHJpbmc');
|
||||
});
|
||||
});
|
||||
|
||||
describe(getStorageKey, () => {
|
||||
it('computes the correct key', () => {
|
||||
const key = getStorageKey('image/jpeg', 'blibblab');
|
||||
expect(key).toBe('j0iiW9hDbR2HKoH1nCxsKRM6QIZVtZ__2ssOiOcxlAs');
|
||||
});
|
||||
it('uses the null separator to distinguish unequal keys', () => {
|
||||
const keyOne = getStorageKey('image/jpeg', 'blibblab');
|
||||
const keyTwo = getStorageKey('image', '/jpegblibblab');
|
||||
expect(keyOne).not.toBe(keyTwo);
|
||||
});
|
||||
});
|
||||
|
||||
describe(getStorageKeyForAssetAsync, () => {
|
||||
const pathLocation = uuidv4();
|
||||
fs.writeFileSync(pathLocation, Buffer.from('I am pretending to be a jpeg'));
|
||||
it('returns the correct key', async () => {
|
||||
const asset = {
|
||||
type: 'jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: pathLocation,
|
||||
};
|
||||
expect(await getStorageKeyForAssetAsync(asset)).toBe(
|
||||
'fo8Y08LktVk6qLtGbn8GRWpOUyD13ABMUnbtRCN1L7Y'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(convertAssetToUpdateInfoGroupFormatAsync, () => {
|
||||
it('resolves to the correct value', async () => {
|
||||
const path = uuidv4();
|
||||
fs.writeFileSync(path, 'I am pretending to be a jpeg');
|
||||
const type = 'jpg';
|
||||
const asset = {
|
||||
type,
|
||||
contentType: 'image/jpeg',
|
||||
path,
|
||||
};
|
||||
await expect(convertAssetToUpdateInfoGroupFormatAsync(asset)).resolves.toEqual({
|
||||
bundleKey: `c939e759656f577c058f445bfb19182e.${type}`,
|
||||
contentType: 'image/jpeg',
|
||||
fileHash: 'tzD6J-OQZaHCKnL3GHWV9RbnrpyojnagiOE7r3mSkU4',
|
||||
storageBucket: 'update-assets-production',
|
||||
storageKey: 'fo8Y08LktVk6qLtGbn8GRWpOUyD13ABMUnbtRCN1L7Y',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(buildUpdateInfoGroupAsync, () => {
|
||||
const androidBundlePath = uuidv4();
|
||||
const assetPath = uuidv4();
|
||||
fs.writeFileSync(androidBundlePath, 'I am a js bundle');
|
||||
fs.writeFileSync(assetPath, 'I am pretending to be a jpeg');
|
||||
|
||||
it('returns the correct value', async () => {
|
||||
await expect(
|
||||
buildUpdateInfoGroupAsync({
|
||||
android: {
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
contentType: 'bundle/javascript',
|
||||
path: androidBundlePath,
|
||||
},
|
||||
assets: [
|
||||
{
|
||||
type: 'jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: assetPath,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
).resolves.toEqual({
|
||||
android: {
|
||||
assets: [
|
||||
{
|
||||
bundleKey: 'c939e759656f577c058f445bfb19182e.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
fileHash: 'tzD6J-OQZaHCKnL3GHWV9RbnrpyojnagiOE7r3mSkU4',
|
||||
storageBucket: 'update-assets-production',
|
||||
storageKey: 'fo8Y08LktVk6qLtGbn8GRWpOUyD13ABMUnbtRCN1L7Y',
|
||||
},
|
||||
],
|
||||
|
||||
launchAsset: {
|
||||
bundleKey: 'ec0dd14670aae108f99a810df9c1482c.bundle',
|
||||
contentType: 'bundle/javascript',
|
||||
fileHash: 'KEw79FnKTLOyVbRT1SlohSTjPe5e8FpULy2ST-I5BUg',
|
||||
storageBucket: 'update-assets-production',
|
||||
storageKey: 'aC9N6RZlcHoIYjIsoJd2KUcigBKy98RHvZacDyPNjCQ',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(resolveInputDirectory, () => {
|
||||
it('returns the correct distRoot path', () => {
|
||||
const customDirectoryName = path.resolve(uuidv4());
|
||||
fs.mkdirSync(customDirectoryName, { recursive: true });
|
||||
expect(resolveInputDirectory(customDirectoryName)).toBe(customDirectoryName);
|
||||
});
|
||||
it('throws an error if the path does not exist', () => {
|
||||
const nonExistentPath = path.resolve(uuidv4());
|
||||
expect(() => {
|
||||
resolveInputDirectory(nonExistentPath);
|
||||
}).toThrow(`The input directory "${nonExistentPath}" does not exist.
|
||||
You'll need to run a command to build the JS bundle first (e.g. 'expo export').
|
||||
You can use '--input-dir' to specify a different input directory.`);
|
||||
});
|
||||
});
|
||||
|
||||
describe(collectAssets, () => {
|
||||
it('builds an update info group', () => {
|
||||
const fakeHash = 'md5-hash-of-jpg';
|
||||
const bundles = { android: 'android-bundle-code', ios: 'ios-bundle-code' };
|
||||
const inputDir = uuidv4();
|
||||
|
||||
const userDefinedAssets = [
|
||||
{
|
||||
type: 'jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: path.resolve(`${inputDir}/assets/${fakeHash}`),
|
||||
},
|
||||
];
|
||||
|
||||
const bundleDir = path.resolve(`${inputDir}/bundles`);
|
||||
const assetDir = path.resolve(`${inputDir}/assets`);
|
||||
fs.mkdirSync(bundleDir, { recursive: true });
|
||||
fs.mkdirSync(assetDir, { recursive: true });
|
||||
Platforms.forEach(platform => {
|
||||
fs.writeFileSync(path.resolve(inputDir, `bundles/${platform}.js`), bundles[platform]);
|
||||
});
|
||||
fs.writeFileSync(path.resolve(`${inputDir}/assets/${fakeHash}`), dummyFileBuffer);
|
||||
fs.writeFileSync(
|
||||
path.resolve(inputDir, 'metadata.json'),
|
||||
JSON.stringify({
|
||||
version: 0,
|
||||
bundler: 'metro',
|
||||
fileMetadata: {
|
||||
android: {
|
||||
assets: [{ path: `assets/${fakeHash}`, ext: 'jpg' }],
|
||||
bundle: 'bundles/android.js',
|
||||
},
|
||||
ios: {
|
||||
assets: [{ path: `assets/${fakeHash}`, ext: 'jpg' }],
|
||||
bundle: 'bundles/ios.js',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(collectAssets(inputDir)).toEqual({
|
||||
android: {
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: path.resolve(`${inputDir}/bundles/android.js`),
|
||||
},
|
||||
assets: userDefinedAssets,
|
||||
},
|
||||
ios: {
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: path.resolve(`${inputDir}/bundles/ios.js`),
|
||||
},
|
||||
assets: userDefinedAssets,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(filterOutAssetsThatAlreadyExistAsync, () => {
|
||||
it('gets a missing asset', async () => {
|
||||
jest.spyOn(PublishQuery, 'getAssetMetadataAsync').mockImplementation(async () => {
|
||||
return [
|
||||
{
|
||||
storageKey: 'blah',
|
||||
status: AssetMetadataStatus.DoesNotExist,
|
||||
__typename: 'AssetMetadataResult',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
expect(
|
||||
await (await filterOutAssetsThatAlreadyExistAsync([{ storageKey: 'blah' } as any])).length
|
||||
).toBe(1);
|
||||
});
|
||||
it('ignores an asset that exists', async () => {
|
||||
jest.spyOn(PublishQuery, 'getAssetMetadataAsync').mockImplementation(async () => {
|
||||
return [
|
||||
{
|
||||
storageKey: 'blah',
|
||||
status: AssetMetadataStatus.Exists,
|
||||
__typename: 'AssetMetadataResult',
|
||||
},
|
||||
];
|
||||
});
|
||||
expect(
|
||||
await (await filterOutAssetsThatAlreadyExistAsync([{ storageKey: 'blah' } as any])).length
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe(uploadAssetsAsync, () => {
|
||||
const publishBundles = {
|
||||
android: {
|
||||
code: 'android bundle code',
|
||||
assets: [{ files: ['md5-hash-of-file'], type: 'jpg' }],
|
||||
map: 'dummy-string',
|
||||
},
|
||||
ios: {
|
||||
code: 'ios bundle code',
|
||||
assets: [{ files: ['md5-hash-of-file'], type: 'jpg' }],
|
||||
map: 'dummy-string',
|
||||
},
|
||||
};
|
||||
|
||||
const androidBundlePath = uuidv4();
|
||||
const iosBundlePath = uuidv4();
|
||||
const dummyFilePath = uuidv4();
|
||||
const userDefinedPath = uuidv4();
|
||||
fs.writeFileSync(androidBundlePath, publishBundles.android.code);
|
||||
fs.writeFileSync(iosBundlePath, publishBundles.ios.code);
|
||||
fs.writeFileSync(dummyFilePath, dummyFileBuffer);
|
||||
fs.writeFileSync(userDefinedPath, 'I am an octet stream');
|
||||
|
||||
const userDefinedAsset = {
|
||||
type: 'bundle',
|
||||
contentType: 'application/octet-stream',
|
||||
path: userDefinedPath,
|
||||
};
|
||||
|
||||
const assetsForUpdateInfoGroup = {
|
||||
android: {
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: androidBundlePath,
|
||||
},
|
||||
assets: [userDefinedAsset, { type: 'jpg', contentType: 'image/jpeg', path: dummyFilePath }],
|
||||
},
|
||||
ios: {
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: androidBundlePath,
|
||||
},
|
||||
assets: [userDefinedAsset, { type: 'jpg', contentType: 'image/jpeg', path: dummyFilePath }],
|
||||
},
|
||||
};
|
||||
jest.spyOn(PublishMutation, 'getUploadURLsAsync').mockImplementation(async () => {
|
||||
return { specifications: ['{}', '{}', '{}'] };
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
it('throws an error if the upload exceeds TIMEOUT_LIMIT', async () => {
|
||||
jest.spyOn(PublishQuery, 'getAssetMetadataAsync').mockImplementation(async () => {
|
||||
const status = AssetMetadataStatus.DoesNotExist;
|
||||
mockdate.set(Date.now() + TIMEOUT_LIMIT + 1);
|
||||
jest.runAllTimers();
|
||||
return [
|
||||
{
|
||||
storageKey: 'qbgckgkgfdjnNuf9dQd7FDTWUmlEEzg7l1m1sKzQaq0',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // userDefinedAsset
|
||||
{
|
||||
storageKey: 'bbjgXFSIXtjviGwkaPFY0HG4dVVIGiXHAboRFQEqVa4',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // android.code
|
||||
{
|
||||
storageKey: 'dP-nC8EJXKz42XKh_Rc9tYxiGAT-ilpkRltEi6HIKeQ',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // ios.code
|
||||
];
|
||||
});
|
||||
|
||||
mockdate.set(0);
|
||||
await expect(uploadAssetsAsync(assetsForUpdateInfoGroup)).rejects.toThrow(
|
||||
'Asset upload timed out. Please try again.'
|
||||
);
|
||||
});
|
||||
it('resolves if the assets are already uploaded', async () => {
|
||||
jest.spyOn(PublishQuery, 'getAssetMetadataAsync').mockImplementation(async () => {
|
||||
const status = AssetMetadataStatus.Exists;
|
||||
jest.runAllTimers();
|
||||
return [
|
||||
{
|
||||
storageKey: 'qbgckgkgfdjnNuf9dQd7FDTWUmlEEzg7l1m1sKzQaq0',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // userDefinedAsset
|
||||
{
|
||||
storageKey: 'bbjgXFSIXtjviGwkaPFY0HG4dVVIGiXHAboRFQEqVa4',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // android.code
|
||||
{
|
||||
storageKey: 'dP-nC8EJXKz42XKh_Rc9tYxiGAT-ilpkRltEi6HIKeQ',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // ios.code
|
||||
];
|
||||
});
|
||||
|
||||
mockdate.set(0);
|
||||
await expect(uploadAssetsAsync(assetsForUpdateInfoGroup)).resolves.toBe(undefined);
|
||||
});
|
||||
it('resolves if the assets are eventually uploaded', async () => {
|
||||
jest.spyOn(PublishQuery, 'getAssetMetadataAsync').mockImplementation(async () => {
|
||||
const status =
|
||||
Date.now() === 0 ? AssetMetadataStatus.DoesNotExist : AssetMetadataStatus.Exists;
|
||||
mockdate.set(Date.now() + 1);
|
||||
jest.runAllTimers();
|
||||
return [
|
||||
{
|
||||
storageKey: 'qbgckgkgfdjnNuf9dQd7FDTWUmlEEzg7l1m1sKzQaq0',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // userDefinedAsset
|
||||
{
|
||||
storageKey: 'bbjgXFSIXtjviGwkaPFY0HG4dVVIGiXHAboRFQEqVa4',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // android.code
|
||||
{
|
||||
storageKey: 'dP-nC8EJXKz42XKh_Rc9tYxiGAT-ilpkRltEi6HIKeQ',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // ios.code
|
||||
];
|
||||
});
|
||||
|
||||
mockdate.set(0);
|
||||
await expect(uploadAssetsAsync(assetsForUpdateInfoGroup)).resolves.toBe(undefined);
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,7 @@ interface ProjectInfo {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures project exists on Expo servers. Registers it when it doesn't
|
||||
* Ensures project exists on EAS servers. Registers it when it doesn't
|
||||
* @returns The project ID
|
||||
*/
|
||||
export async function ensureProjectExistsAsync(projectInfo: ProjectInfo): Promise<string> {
|
||||
@@ -58,7 +58,7 @@ async function findProjectIdByAccountNameAndSlugAsync(
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers new project on Expo servers
|
||||
* Registers new project on EAS servers
|
||||
* @returns Created project's ID
|
||||
*/
|
||||
async function registerNewProjectAsync({
|
||||
|
||||
@@ -149,6 +149,7 @@ export async function getReleaseByNameAsync({
|
||||
query ViewRelease($appId: String!, $releaseName: String!) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
updateReleaseByReleaseName(releaseName: $releaseName) {
|
||||
id
|
||||
releaseName
|
||||
|
||||
Loaded 100 of 114 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user