mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bc7bcbf87 | ||
|
|
a1d361db1d | ||
|
|
4a36f98abe | ||
|
|
a518728bf0 | ||
|
|
482b7f8f64 | ||
|
|
639d5881f5 | ||
|
|
0fc6cf8658 | ||
|
|
c7e13c25ab | ||
|
|
77b5d63282 | ||
|
|
daded8d7d2 | ||
|
|
5a21417c8d | ||
|
|
9c3aa4c96d | ||
|
|
14481716d1 | ||
|
|
5e8b109746 | ||
|
|
996ef329eb | ||
|
|
0e93eb884f | ||
|
|
958fce59e6 | ||
|
|
595a841f21 | ||
|
|
fd01b9393b | ||
|
|
61c16674d5 | ||
|
|
838701f0d1 | ||
|
|
3ca26392b7 | ||
|
|
40c0a0a5a1 | ||
|
|
f412451aaf | ||
|
|
e2d747aadb | ||
|
|
5df5bd0aa5 | ||
|
|
583fd24e33 | ||
|
|
12c67e525c |
No files matched your search
@@ -7,6 +7,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Prompt before configuring EAS project. ([#1356](https://github.com/expo/eas-cli/pull/1356) by [@wschurman](https://github.com/wschurman))
|
||||
- Adds paginated support to device commands. Removes the ability to delete multiple devices at once. ([#1381](https://github.com/expo/eas-cli/pull/1381) by [@kgc00](https://github.com/kgc00))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
@@ -18,14 +19,25 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
- Add pagination to build commands. ([#1353](https://github.com/expo/eas-cli/pull/1353) by [@kgc00](https://github.com/kgc00))
|
||||
- Provide suggestions for developers when archive size is large. ([#1363](https://github.com/expo/eas-cli/pull/1363) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Improve `eas init` command. ([#1376](https://github.com/expo/eas-cli/pull/1376) by [@wschurman](https://github.com/wschurman))
|
||||
- Warn about outdated channel configuration before build when using eas update. ([#1397](https://github.com/expo/eas-cli/pull/1397) by [@kbrandwijk](https://github.com/kbrandwijk))
|
||||
- Validate that owner and projectId and slug all are in alignment. ([#1405](https://github.com/expo/eas-cli/pull/1405) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix automatic eas.json generation when running `eas build`. ([#1415](https://github.com/expo/eas-cli/pull/1415) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Standardize the GQL 'Update' schema using UpdateFragmentNode. ([#1348](https://github.com/expo/eas-cli/pull/1348) by [@kgc00](https://github.com/kgc00))
|
||||
- Remove 'owner' app config field dependency. ([#1368](https://github.com/expo/eas-cli/pull/1368), [#1369](https://github.com/expo/eas-cli/pull/1369), [#1371](https://github.com/expo/eas-cli/pull/1371), [#1372](https://github.com/expo/eas-cli/pull/1372), [#1373](https://github.com/expo/eas-cli/pull/1373), by [@wschurman](https://github.com/wschurman))
|
||||
- Add Xcode 14 image. ([#1365](https://github.com/expo/eas-cli/pull/1365) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Update max `versionCode` for Android. ([#1400](https://github.com/expo/eas-cli/pull/1400) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add and use command context to declare command dependencies. ([#1383](https://github.com/expo/eas-cli/pull/1383), [#1384](https://github.com/expo/eas-cli/pull/1384), [#1387](https://github.com/expo/eas-cli/pull/1387), [#1388](https://github.com/expo/eas-cli/pull/1388), [#1390](https://github.com/expo/eas-cli/pull/1390), [#1391](https://github.com/expo/eas-cli/pull/1391), [#1394](https://github.com/expo/eas-cli/pull/1394), [#1402](https://github.com/expo/eas-cli/pull/1402), [#1403](https://github.com/expo/eas-cli/pull/1403), by [@wschurman](https://github.com/wschurman))
|
||||
- Fix typo in the ad hoc build message. ([#1407](https://github.com/expo/eas-cli/pull/1407) by [@Simek](https://github.com/Simek))
|
||||
- Improve errors and error messages formatting related to **eas.json**. ([#1414](https://github.com/expo/eas-cli/pull/1414) by [@Simek](https://github.com/Simek))
|
||||
- Handle errors from platform-specific kill switches to disable free tier builds. ([#1401](https://github.com/expo/eas-cli/pull/1401) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Surface invalid eas.json errors in an optional project context. ([#1418](https://github.com/expo/eas-cli/pull/1418) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Improve message when project ID doesn't match either slug or owner. ([#1420](https://github.com/expo/eas-cli/pull/1420) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [2.1.0](https://github.com/expo/eas-cli/releases/tag/v2.1.0) - 2022-09-05
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "2.1.0"
|
||||
"version": "2.2.0"
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Generated
+188
-1
@@ -11200,7 +11200,32 @@
|
||||
{
|
||||
"name": "appleDevices",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"args": [
|
||||
{
|
||||
"name": "limit",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "offset",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
@@ -14248,6 +14273,50 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "BuildOrBuildJobQuery",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "byId",
|
||||
"description": "Look up EAS Build or Classic Build Job by ID",
|
||||
"args": [
|
||||
{
|
||||
"name": "buildOrBuildJobId",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "UNION",
|
||||
"name": "EASBuildOrClassicBuildJob",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "BuildParamsInput",
|
||||
@@ -16461,6 +16530,27 @@
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "UNION",
|
||||
"name": "EASBuildOrClassicBuildJob",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": [
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "Build",
|
||||
"ofType": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "BuildJob",
|
||||
"ofType": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "EASServiceMetric",
|
||||
@@ -17684,6 +17774,87 @@
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "searchRepositories",
|
||||
"description": "",
|
||||
"args": [
|
||||
{
|
||||
"name": "githubAppInstallationId",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": "1",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "perPage",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": "50",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "query",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "GitHubAppInstallationAccessibleRepository",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
@@ -24170,6 +24341,22 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "buildOrBuildJob",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "BuildOrBuildJobQuery",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "buildPublicData",
|
||||
"description": "Top-level query object for querying BuildPublicData publicly.",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "2.1.0",
|
||||
"version": "2.2.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -14,7 +14,7 @@
|
||||
"@expo/config-plugins": "5.0.1",
|
||||
"@expo/config-types": "46.0.2",
|
||||
"@expo/eas-build-job": "0.2.94",
|
||||
"@expo/eas-json": "2.1.0",
|
||||
"@expo/eas-json": "2.2.0",
|
||||
"@expo/json-file": "8.2.36",
|
||||
"@expo/multipart-body-parser": "1.1.0",
|
||||
"@expo/package-manager": "0.0.56",
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from '../graphql/generated';
|
||||
import { BuildResult } from '../graphql/mutations/BuildMutation';
|
||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||
import Log, { learnMore } from '../log';
|
||||
import Log, { learnMore, link } from '../log';
|
||||
import { Ora, ora } from '../ora';
|
||||
import {
|
||||
appPlatformDisplayNames,
|
||||
@@ -140,12 +140,34 @@ function handleBuildRequestError(error: any, platform: Platform): never {
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
|
||||
) {
|
||||
Log.error(
|
||||
'EAS Build is down for maintenance. Try again later. Check https://status.expo.dev/ for updates.'
|
||||
`EAS Build is down for maintenance. Try again later. Check ${link(
|
||||
'https://status.expo.dev/'
|
||||
)} for updates.`
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED') {
|
||||
Log.error(
|
||||
'EAS Build free tier is temporarily disabled. Try again later. Check https://status.expo.dev/ for updates.'
|
||||
`EAS Build free tier is temporarily disabled and we are not accepting any new builds. Try again later. ${learnMore(
|
||||
'https://expo.fyi/eas-build-queues'
|
||||
)}`
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED_IOS'
|
||||
) {
|
||||
Log.error(
|
||||
`EAS Build free tier is temporarily disabled for iOS and we are not accepting any new builds. Try again later. ${learnMore(
|
||||
'https://expo.fyi/eas-build-queues'
|
||||
)}`
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED_ANDROID'
|
||||
) {
|
||||
Log.error(
|
||||
`EAS Build free tier is temporarily disabled for Android and we are not accepting any new builds. Try again later. ${learnMore(
|
||||
'https://expo.fyi/eas-build-queues'
|
||||
)}`
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
|
||||
@@ -7,9 +7,9 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { TrackingContext } from '../analytics/common';
|
||||
import { Analytics, BuildEvent } from '../analytics/events';
|
||||
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { BuildResourceClass } from '../graphql/generated';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { Actor } from '../user/User';
|
||||
@@ -31,7 +31,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
resourceClass,
|
||||
message,
|
||||
actor,
|
||||
projectId,
|
||||
getDynamicProjectConfigAsync,
|
||||
}: {
|
||||
buildProfileName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
@@ -45,9 +45,9 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
resourceClass: BuildResourceClass;
|
||||
message?: string;
|
||||
actor: Actor;
|
||||
projectId: string;
|
||||
getDynamicProjectConfigAsync: DynamicConfigContextFn;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
|
||||
const { exp, projectId } = await getDynamicProjectConfigAsync({ env: buildProfile.env });
|
||||
|
||||
const projectName = exp.slug;
|
||||
const account = await getOwnerAccountForProjectIdAsync(projectId);
|
||||
|
||||
@@ -6,7 +6,7 @@ import semver from 'semver';
|
||||
import { ora } from '../ora';
|
||||
|
||||
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
|
||||
const PLUGIN_PACKAGE_VERSION = '0.0.99';
|
||||
const PLUGIN_PACKAGE_VERSION = '0.0.110';
|
||||
|
||||
export interface LocalBuildOptions {
|
||||
enable: boolean;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
@@ -41,6 +42,7 @@ import {
|
||||
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
|
||||
} from '../submit/submit';
|
||||
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
|
||||
import { validateBuildProfileConfigMatchesProjectConfigAsync } from '../update/utils';
|
||||
import { Actor } from '../user/User';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import { ProfileData, getProfilesAsync } from '../utils/profiles';
|
||||
@@ -92,7 +94,7 @@ export async function runBuildAndSubmitAsync(
|
||||
projectDir: string,
|
||||
flags: BuildFlags,
|
||||
actor: Actor,
|
||||
projectId: string
|
||||
getDynamicProjectConfigAsync: DynamicConfigContextFn
|
||||
): Promise<void> {
|
||||
await getVcsClient().ensureRepoExistsAsync();
|
||||
await ensureRepoIsCleanAsync(flags.nonInteractive);
|
||||
@@ -141,7 +143,7 @@ export async function runBuildAndSubmitAsync(
|
||||
],
|
||||
easJsonCliConfig,
|
||||
actor,
|
||||
projectId,
|
||||
getDynamicProjectConfigAsync,
|
||||
});
|
||||
if (maybeBuild) {
|
||||
startedBuilds.push({ build: maybeBuild, buildProfile });
|
||||
@@ -236,7 +238,7 @@ async function prepareAndStartBuildAsync({
|
||||
resourceClass,
|
||||
easJsonCliConfig,
|
||||
actor,
|
||||
projectId,
|
||||
getDynamicProjectConfigAsync,
|
||||
}: {
|
||||
projectDir: string;
|
||||
flags: BuildFlags;
|
||||
@@ -245,7 +247,7 @@ async function prepareAndStartBuildAsync({
|
||||
resourceClass: BuildResourceClass;
|
||||
easJsonCliConfig: EasJson['cli'];
|
||||
actor: Actor;
|
||||
projectId: string;
|
||||
getDynamicProjectConfigAsync: DynamicConfigContextFn;
|
||||
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
|
||||
const buildCtx = await createBuildContextAsync({
|
||||
buildProfileName: buildProfile.profileName,
|
||||
@@ -260,7 +262,7 @@ async function prepareAndStartBuildAsync({
|
||||
easJsonCliConfig,
|
||||
message: flags.message,
|
||||
actor,
|
||||
projectId,
|
||||
getDynamicProjectConfigAsync,
|
||||
});
|
||||
|
||||
if (moreBuilds) {
|
||||
@@ -272,6 +274,14 @@ async function prepareAndStartBuildAsync({
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
await validateBuildProfileConfigMatchesProjectConfigAsync(
|
||||
buildCtx.exp,
|
||||
buildProfile,
|
||||
buildCtx.projectId,
|
||||
flags.nonInteractive
|
||||
);
|
||||
|
||||
await validateAppVersionRuntimePolicySupportAsync(buildCtx.projectDir, buildCtx.exp);
|
||||
if (easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE) {
|
||||
validateAppConfigForRemoteVersionSource(buildCtx.exp, buildProfile.platform);
|
||||
@@ -325,7 +335,6 @@ async function prepareAndStartSubmissionAsync({
|
||||
const submissionCtx = await createSubmissionContextAsync({
|
||||
platform,
|
||||
projectDir,
|
||||
projectId: build.project.id,
|
||||
profile: submitProfile,
|
||||
archiveFlags: { id: build.id },
|
||||
nonInteractive,
|
||||
@@ -333,6 +342,8 @@ async function prepareAndStartSubmissionAsync({
|
||||
credentialsCtx: buildCtx.credentialsCtx,
|
||||
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
|
||||
actor: buildCtx.user,
|
||||
projectId: buildCtx.projectId,
|
||||
exp: buildCtx.exp,
|
||||
});
|
||||
|
||||
if (moreBuilds) {
|
||||
|
||||
@@ -9,20 +9,10 @@ import {
|
||||
import { getUserAsync } from '../user/User';
|
||||
import ActorContextField from './context/ActorContextField';
|
||||
import ContextField from './context/ContextField';
|
||||
import { DynamicProjectConfigContextField } from './context/DynamicProjectConfigContextField';
|
||||
import { OptionalProjectConfigContextField } from './context/OptionalProjectConfigContextField';
|
||||
import ProjectConfigContextField from './context/ProjectConfigContextField';
|
||||
import ProjectDirContextField from './context/ProjectDirContextField';
|
||||
import ProjectIdContextField from './context/ProjectIdContextField';
|
||||
|
||||
export const EASCommandProjectDirContext = {
|
||||
projectDir: new ProjectDirContextField(),
|
||||
};
|
||||
|
||||
export const EASCommandProjectIdContext = {
|
||||
projectId: new ProjectIdContextField(),
|
||||
};
|
||||
|
||||
export const EASCommandLoggedInContext = {
|
||||
actor: new ActorContextField(),
|
||||
};
|
||||
|
||||
type ContextInput<
|
||||
T extends {
|
||||
@@ -41,8 +31,55 @@ type ContextOutput<
|
||||
};
|
||||
|
||||
export default abstract class EasCommand extends Command {
|
||||
protected static readonly ContextOptions = {
|
||||
/**
|
||||
* Require this command to be run when logged-in. Returns the logged-in actor in the context.
|
||||
*/
|
||||
LoggedIn: {
|
||||
actor: new ActorContextField(),
|
||||
},
|
||||
/**
|
||||
* Require the project to be identified and registered on server if this command is being
|
||||
* run within a project directory, null otherwise.
|
||||
*/
|
||||
OptionalProjectConfig: {
|
||||
projectConfig: new OptionalProjectConfigContextField(),
|
||||
},
|
||||
/**
|
||||
* Require this command to be run in a project directory. Return the project directory in the context.
|
||||
*/
|
||||
ProjectDir: {
|
||||
projectDir: new ProjectDirContextField(),
|
||||
},
|
||||
/**
|
||||
* Provides functions to load the project config when dynamic config options are needed (custom Env for example).
|
||||
*/
|
||||
DynamicProjectConfig: {
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
getDynamicProjectConfigAsync: new DynamicProjectConfigContextField(),
|
||||
},
|
||||
/**
|
||||
* Require the project to be identified and registered on server. Returns the project config in the context.
|
||||
*/
|
||||
ProjectConfig: {
|
||||
projectConfig: new ProjectConfigContextField(),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Context allows for subclasses (commands) to declare their prerequisites in a type-safe manner.
|
||||
* These declarative definitions each output a context property that is the result of the prerequisite being
|
||||
* satisfied. These allow a unified common interface to be shared amongst commands in order to provide a more
|
||||
* consistent CLI experience.
|
||||
*
|
||||
* For example, let's say a command needs the EAS project ID to make a GraphQL mutation. It should declare that
|
||||
* it requires the `ProjectConfig` context, and then call `getContextAsync` to get the project ID.
|
||||
*/
|
||||
static contextDefinition: ContextInput = {};
|
||||
|
||||
/**
|
||||
* Execute the context in the contextDefinition to satisfy command prerequisites.
|
||||
*/
|
||||
protected async getContextAsync<
|
||||
C extends {
|
||||
[name: string]: any;
|
||||
|
||||
@@ -1,85 +1,9 @@
|
||||
import { Errors } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { ApiV2Error } from '../../api';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { Actor, getUserAsync, loginAsync } from '../../user/User';
|
||||
import { retryUsernamePasswordAuthWithOTPAsync } from '../../user/otp';
|
||||
import { Actor } from '../../user/User';
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { ensureLoggedInAsync } from './contextUtils/ensureLoggedInAsync';
|
||||
|
||||
export default class ActorContextField extends ContextField<Actor> {
|
||||
async getValueAsync({ nonInteractive }: ContextOptions): Promise<Actor> {
|
||||
return await ActorContextField.ensureLoggedInAsync({ nonInteractive });
|
||||
}
|
||||
|
||||
private static async ensureLoggedInAsync({
|
||||
nonInteractive,
|
||||
}: {
|
||||
nonInteractive: boolean;
|
||||
}): Promise<Actor> {
|
||||
let user: Actor | undefined;
|
||||
try {
|
||||
user = await getUserAsync();
|
||||
} catch {}
|
||||
|
||||
if (!user) {
|
||||
Log.warn('An Expo user account is required to proceed.');
|
||||
await this.showLoginPromptAsync({ nonInteractive, printNewLine: true });
|
||||
user = await getUserAsync();
|
||||
}
|
||||
|
||||
return nullthrows(user);
|
||||
}
|
||||
|
||||
private static async showLoginPromptAsync({
|
||||
nonInteractive = false,
|
||||
printNewLine = false,
|
||||
} = {}): Promise<void> {
|
||||
if (nonInteractive) {
|
||||
Errors.error(
|
||||
`Either log in with ${chalk.bold('eas login')} or set the ${chalk.bold(
|
||||
'EXPO_TOKEN'
|
||||
)} environment variable if you're using EAS CLI on CI (${learnMore(
|
||||
'https://docs.expo.dev/accounts/programmatic-access/',
|
||||
{ dim: false }
|
||||
)})`
|
||||
);
|
||||
}
|
||||
if (printNewLine) {
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
Log.log('Log in to EAS');
|
||||
|
||||
const { username, password } = await promptAsync([
|
||||
{
|
||||
type: 'text',
|
||||
name: 'username',
|
||||
message: 'Email or username',
|
||||
},
|
||||
{
|
||||
type: 'password',
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
},
|
||||
]);
|
||||
try {
|
||||
await loginAsync({
|
||||
username,
|
||||
password,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof ApiV2Error && e.expoApiV2ErrorCode === 'ONE_TIME_PASSWORD_REQUIRED') {
|
||||
await retryUsernamePasswordAuthWithOTPAsync(
|
||||
username,
|
||||
password,
|
||||
e.expoApiV2ErrorMetadata as any
|
||||
);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return await ensureLoggedInAsync({ nonInteractive });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ExpoConfig } from '@expo/config-types';
|
||||
|
||||
import { ExpoConfigOptions, getExpoConfig } from '../../project/expoConfig';
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
|
||||
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
|
||||
|
||||
export type DynamicConfigContextFn = (options?: ExpoConfigOptions) => Promise<{
|
||||
projectId: string;
|
||||
exp: ExpoConfig;
|
||||
projectDir: string;
|
||||
}>;
|
||||
|
||||
export class DynamicProjectConfigContextField extends ContextField<DynamicConfigContextFn> {
|
||||
async getValueAsync({ nonInteractive }: ContextOptions): Promise<DynamicConfigContextFn> {
|
||||
return async (options?: ExpoConfigOptions) => {
|
||||
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
|
||||
const expBefore = getExpoConfig(projectDir, options);
|
||||
const projectId = await getProjectIdAsync(expBefore, {
|
||||
nonInteractive,
|
||||
env: options?.env,
|
||||
});
|
||||
const exp = getExpoConfig(projectDir, options);
|
||||
return {
|
||||
exp,
|
||||
projectDir,
|
||||
projectId,
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ExpoConfig } from '@expo/config-types';
|
||||
import { InvalidEasJsonError } from '@expo/eas-json/build/errors';
|
||||
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
|
||||
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
|
||||
|
||||
export class OptionalProjectConfigContextField extends ContextField<
|
||||
| {
|
||||
projectId: string;
|
||||
exp: ExpoConfig;
|
||||
projectDir: string;
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
async getValueAsync({ nonInteractive }: ContextOptions): Promise<
|
||||
| {
|
||||
projectId: string;
|
||||
exp: ExpoConfig;
|
||||
projectDir: string;
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
let projectDir: string;
|
||||
try {
|
||||
projectDir = await findProjectDirAndVerifyProjectSetupAsync();
|
||||
if (!projectDir) {
|
||||
return undefined;
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof InvalidEasJsonError) {
|
||||
throw e;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const expBefore = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(expBefore, {
|
||||
nonInteractive,
|
||||
});
|
||||
const exp = getExpoConfig(projectDir);
|
||||
return {
|
||||
exp,
|
||||
projectDir,
|
||||
projectId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ExpoConfig } from '@expo/config-types';
|
||||
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
|
||||
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
|
||||
|
||||
export default class ProjectConfigContextField extends ContextField<{
|
||||
projectId: string;
|
||||
exp: ExpoConfig;
|
||||
projectDir: string;
|
||||
}> {
|
||||
async getValueAsync({ nonInteractive }: ContextOptions): Promise<{
|
||||
projectId: string;
|
||||
exp: ExpoConfig;
|
||||
projectDir: string;
|
||||
}> {
|
||||
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
|
||||
const expBefore = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(expBefore, {
|
||||
nonInteractive,
|
||||
});
|
||||
const exp = getExpoConfig(projectDir);
|
||||
|
||||
return {
|
||||
projectId,
|
||||
exp,
|
||||
projectDir,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,106 +1,8 @@
|
||||
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
|
||||
import * as PackageManagerUtils from '@expo/package-manager';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import pkgDir from 'pkg-dir';
|
||||
import semver from 'semver';
|
||||
|
||||
import { learnMore } from '../../log';
|
||||
import { easCliVersion } from '../../utils/easCli';
|
||||
import { getVcsClient, setVcsClient } from '../../vcs';
|
||||
import GitClient from '../../vcs/clients/git';
|
||||
import ContextField from './ContextField';
|
||||
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
|
||||
|
||||
export default class ProjectDirContextField extends ContextField<string> {
|
||||
async getValueAsync(): Promise<string> {
|
||||
return await ProjectDirContextField.findProjectDirAndVerifyProjectSetupAsync();
|
||||
}
|
||||
|
||||
private static async applyCliConfigAsync(projectDir: string): Promise<void> {
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const config = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
|
||||
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
|
||||
throw new Error(
|
||||
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
|
||||
);
|
||||
}
|
||||
if (config?.requireCommit) {
|
||||
setVcsClient(new GitClient());
|
||||
}
|
||||
}
|
||||
|
||||
private static async ensureEasCliIsNotInDependenciesAsync(projectDir: string): Promise<void> {
|
||||
let printCliVersionWarning = false;
|
||||
|
||||
const consoleWarn = (msg?: string): void => {
|
||||
if (msg) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(chalk.yellow(msg));
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn();
|
||||
}
|
||||
};
|
||||
|
||||
if (await this.isEasCliInDependenciesAsync(projectDir)) {
|
||||
printCliVersionWarning = true;
|
||||
consoleWarn(`Found ${chalk.bold('eas-cli')} in your project dependencies.`);
|
||||
}
|
||||
|
||||
const maybeRepoRoot = PackageManagerUtils.findWorkspaceRoot(projectDir) ?? projectDir;
|
||||
if (maybeRepoRoot !== projectDir && (await this.isEasCliInDependenciesAsync(maybeRepoRoot))) {
|
||||
printCliVersionWarning = true;
|
||||
consoleWarn(`Found ${chalk.bold('eas-cli')} in your monorepo dependencies.`);
|
||||
}
|
||||
|
||||
if (printCliVersionWarning) {
|
||||
consoleWarn(
|
||||
`It's recommended to use the ${chalk.bold(
|
||||
'"cli.version"'
|
||||
)} field in eas.json to enforce the ${chalk.bold('eas-cli')} version for your project.`
|
||||
);
|
||||
consoleWarn(
|
||||
learnMore('https://github.com/expo/eas-cli#enforcing-eas-cli-version-for-your-project')
|
||||
);
|
||||
consoleWarn();
|
||||
}
|
||||
}
|
||||
|
||||
private static async isEasCliInDependenciesAsync(dir: string): Promise<boolean> {
|
||||
const packageJsonPath = path.join(dir, 'package.json');
|
||||
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
||||
return (
|
||||
packageJson?.dependencies?.['eas-cli'] !== undefined ||
|
||||
packageJson?.devDependencies?.['eas-cli'] !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the project root directory.
|
||||
*/
|
||||
private static async findProjectRootAsync(): Promise<string> {
|
||||
const projectRootDir = await pkgDir();
|
||||
if (!projectRootDir) {
|
||||
throw new Error('Run this command inside a project directory.');
|
||||
} else {
|
||||
let vcsRoot;
|
||||
try {
|
||||
vcsRoot = path.normalize(await getVcsClient().getRootPathAsync());
|
||||
} catch {}
|
||||
if (vcsRoot && vcsRoot.startsWith(projectRootDir) && vcsRoot !== projectRootDir) {
|
||||
throw new Error(
|
||||
`package.json is outside of the current git repository (project root: ${projectRootDir}, git root: ${vcsRoot}).`
|
||||
);
|
||||
}
|
||||
return projectRootDir;
|
||||
}
|
||||
}
|
||||
|
||||
private static async findProjectDirAndVerifyProjectSetupAsync(): Promise<string> {
|
||||
const projectDir = await this.findProjectRootAsync();
|
||||
await this.applyCliConfigAsync(projectDir);
|
||||
await this.ensureEasCliIsNotInDependenciesAsync(projectDir);
|
||||
return projectDir;
|
||||
return await findProjectDirAndVerifyProjectSetupAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
import { getProjectConfigDescription, modifyConfigAsync } from '@expo/config';
|
||||
import { ExpoConfig } from '@expo/config-types';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { toAppPrivacy } from '../../project/projectUtils';
|
||||
import { Actor } from '../../user/User';
|
||||
import ActorContextField from './ActorContextField';
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import ProjectDirContextField from './ProjectDirContextField';
|
||||
|
||||
export default class ProjectIdContextField extends ContextField<string> {
|
||||
async getValueAsync({ nonInteractive }: ContextOptions): Promise<string> {
|
||||
const projectDir = await ProjectDirContextField['findProjectDirAndVerifyProjectSetupAsync']();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
return await ProjectIdContextField.getProjectIdAsync(exp, { nonInteractive });
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an EAS project ID to the appropriate field in the app config.
|
||||
*/
|
||||
private static async saveProjectIdToAppConfigAsync(
|
||||
projectDir: string,
|
||||
projectId: string
|
||||
): Promise<void> {
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const result = await modifyConfigAsync(projectDir, {
|
||||
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
|
||||
});
|
||||
|
||||
switch (result.type) {
|
||||
case 'success':
|
||||
break;
|
||||
case 'warn': {
|
||||
Log.warn();
|
||||
Log.warn(
|
||||
`Warning: Your project uses dynamic app configuration, and the EAS project ID can't automatically be added to it.`
|
||||
);
|
||||
Log.warn(
|
||||
chalk.dim(
|
||||
'https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs'
|
||||
)
|
||||
);
|
||||
Log.warn();
|
||||
Log.warn(
|
||||
`To complete the setup process, set the ${chalk.bold(
|
||||
'extra.eas.projectId'
|
||||
)} in your ${chalk.bold(getProjectConfigDescription(projectDir))}:`
|
||||
);
|
||||
Log.warn();
|
||||
Log.warn(chalk.bold(JSON.stringify({ expo: { extra: { eas: { projectId } } } }, null, 2)));
|
||||
Log.warn();
|
||||
throw new Error(result.message);
|
||||
}
|
||||
case 'fail':
|
||||
throw new Error(result.message);
|
||||
default:
|
||||
throw new Error('Unexpected result type from modifyConfigAsync');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the EAS project ID from the app config. If the project ID is not set in the config,
|
||||
* use the owner/slug to identify an EAS project on the server (asking for confirmation first),
|
||||
* and attempt to save the EAS project ID to the appropriate field in the app config. If unable to
|
||||
* save to the app config, throw an error.
|
||||
*/
|
||||
private static async getProjectIdAsync(
|
||||
exp: ExpoConfig,
|
||||
options: { nonInteractive: boolean }
|
||||
): Promise<string> {
|
||||
const localProjectId = exp.extra?.eas?.projectId;
|
||||
if (localProjectId) {
|
||||
return localProjectId;
|
||||
}
|
||||
|
||||
const projectDir = await ProjectDirContextField['findProjectRootAsync']();
|
||||
if (!projectDir) {
|
||||
throw new Error('This command must be run inside a project directory.');
|
||||
}
|
||||
|
||||
Log.warn('EAS project not configured.');
|
||||
|
||||
const getAccountNameForEASProjectSync = (exp: ExpoConfig, user: Actor): string => {
|
||||
if (exp.owner) {
|
||||
return exp.owner;
|
||||
}
|
||||
switch (user.__typename) {
|
||||
case 'User':
|
||||
return user.username;
|
||||
case 'Robot':
|
||||
throw new Error(
|
||||
'The "owner" manifest property is required when using robot users. See: https://docs.expo.dev/versions/latest/config/app/#owner'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const actor = await ActorContextField['ensureLoggedInAsync']({
|
||||
nonInteractive: options.nonInteractive,
|
||||
});
|
||||
const projectId = await fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync(
|
||||
{
|
||||
accountName: getAccountNameForEASProjectSync(exp, actor),
|
||||
projectName: exp.slug,
|
||||
privacy: toAppPrivacy(exp.privacy),
|
||||
},
|
||||
{
|
||||
nonInteractive: options.nonInteractive,
|
||||
},
|
||||
actor
|
||||
);
|
||||
|
||||
const spinner = ora(`Linking local project to EAS project ${projectId}`).start();
|
||||
try {
|
||||
await this.saveProjectIdToAppConfigAsync(projectDir, projectId);
|
||||
spinner.succeed(`Linked local project to EAS project ${projectId}`);
|
||||
} catch (e: any) {
|
||||
spinner.fail();
|
||||
throw e;
|
||||
}
|
||||
|
||||
return projectId;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { vol } from 'memfs';
|
||||
import pkgDir from 'pkg-dir';
|
||||
|
||||
import ProjectDirContextField from '../ProjectDirContextField';
|
||||
|
||||
jest.mock('@expo/config');
|
||||
jest.mock('fs');
|
||||
jest.mock('pkg-dir');
|
||||
|
||||
jest.mock('../../../prompts');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe(ProjectDirContextField['findProjectRootAsync'], () => {
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
it('throws if not inside the project directory', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./README.md': '1',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
await expect(ProjectDirContextField['findProjectRootAsync']()).rejects.toThrow(
|
||||
'Run this command inside a project directory.'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the root directory of the project', async () => {
|
||||
jest.mocked(pkgDir).mockResolvedValue('/app');
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./README.md': '1',
|
||||
'./package.json': '2',
|
||||
'./src/index.ts': '3',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
const projectRoot = await ProjectDirContextField['findProjectRootAsync']();
|
||||
expect(projectRoot).toBe('/app');
|
||||
});
|
||||
});
|
||||
@@ -1,104 +0,0 @@
|
||||
import { getConfig, modifyConfigAsync } from '@expo/config';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { Role } from '../../../graphql/generated';
|
||||
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import ActorContextField from '../ActorContextField';
|
||||
import ProjectDirContextField from '../ProjectDirContextField';
|
||||
import ProjectIdContextField from '../ProjectIdContextField';
|
||||
|
||||
jest.mock('@expo/config');
|
||||
jest.mock('fs');
|
||||
|
||||
jest.mock('../ActorContextField');
|
||||
jest.mock('../ProjectDirContextField');
|
||||
jest.mock('../../../user/User');
|
||||
jest.mock('../../../ora', () => ({
|
||||
ora: () => ({
|
||||
start: () => ({ succeed: () => {}, fail: () => {} }),
|
||||
}),
|
||||
}));
|
||||
jest.mock('../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
|
||||
|
||||
describe(ProjectIdContextField.name, () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(ActorContextField['ensureLoggedInAsync']).mockResolvedValue({
|
||||
__typename: 'User',
|
||||
id: 'user_id',
|
||||
username: 'notnotbrent',
|
||||
primaryAccount: {
|
||||
id: 'account_id_1',
|
||||
name: 'notnotbrent',
|
||||
users: [{ role: Role.Owner, actor: { id: 'user_id' } }],
|
||||
},
|
||||
accounts: [
|
||||
{
|
||||
id: 'account_id_1',
|
||||
name: 'notnotbrent',
|
||||
users: [{ role: Role.Owner, actor: { id: 'user_id' } }],
|
||||
},
|
||||
{
|
||||
id: 'account_id_2',
|
||||
name: 'dominik',
|
||||
users: [{ role: Role.ViewOnly, actor: { id: 'user_id' } }],
|
||||
},
|
||||
],
|
||||
isExpoAdmin: false,
|
||||
});
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./README.md': '1',
|
||||
'./package.json': '2',
|
||||
'./src/index.ts': '3',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
});
|
||||
|
||||
it('gets the project ID from app config if exists', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({
|
||||
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
|
||||
} as any);
|
||||
jest.mocked(ProjectDirContextField['findProjectRootAsync']).mockResolvedValue('/app');
|
||||
await expect(
|
||||
new ProjectIdContextField().getValueAsync({ nonInteractive: false })
|
||||
).resolves.toEqual('1234');
|
||||
});
|
||||
|
||||
it('fetches the project ID when not in app config, and sets it in the config', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
|
||||
jest.mocked(ProjectDirContextField['findProjectRootAsync']).mockResolvedValue('/app');
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({
|
||||
type: 'success',
|
||||
config: { expo: { name: 'test', slug: 'test', extra: { eas: { projectId: '2345' } } } },
|
||||
});
|
||||
jest
|
||||
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
|
||||
.mockImplementation(async () => '2345');
|
||||
|
||||
const projectId = await new ProjectIdContextField().getValueAsync({ nonInteractive: false });
|
||||
expect(projectId).toBe('2345');
|
||||
|
||||
expect(modifyConfigAsync).toHaveBeenCalledTimes(1);
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/app', {
|
||||
extra: { eas: { projectId: '2345' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if writing the ID back to the config fails', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
|
||||
jest.mocked(ProjectDirContextField['findProjectRootAsync']).mockResolvedValue('/app');
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({
|
||||
type: 'fail',
|
||||
config: null,
|
||||
});
|
||||
jest
|
||||
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
|
||||
.mockImplementation(async () => '4567');
|
||||
|
||||
await expect(
|
||||
new ProjectIdContextField().getValueAsync({ nonInteractive: false })
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
+12
-12
@@ -1,20 +1,20 @@
|
||||
import { ApiV2Error } from '../../../api';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { loginAsync } from '../../../user/User';
|
||||
import { ApiV2Error } from '../../../../api';
|
||||
import { promptAsync } from '../../../../prompts';
|
||||
import { loginAsync } from '../../../../user/User';
|
||||
import {
|
||||
UserSecondFactorDeviceMethod,
|
||||
retryUsernamePasswordAuthWithOTPAsync,
|
||||
} from '../../../user/otp';
|
||||
import ActorContextField from '../ActorContextField';
|
||||
} from '../../../../user/otp';
|
||||
import { showLoginPromptAsync } from '../../contextUtils/ensureLoggedInAsync';
|
||||
|
||||
jest.mock('../../../prompts');
|
||||
jest.mock('../../../api', () => ({
|
||||
ApiV2Error: jest.requireActual('../../../api').ApiV2Error,
|
||||
jest.mock('../../../../prompts');
|
||||
jest.mock('../../../../api', () => ({
|
||||
ApiV2Error: jest.requireActual('../../../../api').ApiV2Error,
|
||||
getExpoApiBaseUrl: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../user/otp');
|
||||
jest.mock('../../../user/User', () => ({
|
||||
jest.mock('../../../../user/otp');
|
||||
jest.mock('../../../../user/User', () => ({
|
||||
loginAsync: jest.fn(),
|
||||
}));
|
||||
|
||||
@@ -27,7 +27,7 @@ beforeEach(() => {
|
||||
jest.mocked(loginAsync).mockReset();
|
||||
});
|
||||
|
||||
describe(ActorContextField['showLoginPromptAsync'], () => {
|
||||
describe(showLoginPromptAsync, () => {
|
||||
it('prompts for OTP when 2FA is enabled', async () => {
|
||||
jest
|
||||
.mocked(promptAsync)
|
||||
@@ -57,7 +57,7 @@ describe(ActorContextField['showLoginPromptAsync'], () => {
|
||||
})
|
||||
.mockImplementation(async () => {});
|
||||
|
||||
await ActorContextField['showLoginPromptAsync']();
|
||||
await showLoginPromptAsync();
|
||||
|
||||
expect(retryUsernamePasswordAuthWithOTPAsync).toHaveBeenCalledWith('hello', 'world', {
|
||||
secondFactorDevices: [
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { findProjectRootAsync } from '../findProjectDirAndVerifyProjectSetupAsync';
|
||||
|
||||
jest.mock('@expo/config');
|
||||
jest.mock('fs');
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe(findProjectRootAsync, () => {
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
it('throws if not inside the project directory', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./README.md': '1',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
await expect(findProjectRootAsync({ cwd: '/app' })).rejects.toThrow(
|
||||
'Run this command inside a project directory.'
|
||||
);
|
||||
});
|
||||
|
||||
it('defaults to process.cwd() if defaultToProcessCwd = true', async () => {
|
||||
const spy = jest.spyOn(process, 'cwd').mockReturnValue('/this/is/fake/process/cwd');
|
||||
try {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./README.md': '1',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
const projectDir = await findProjectRootAsync({
|
||||
cwd: '/app',
|
||||
defaultToProcessCwd: true,
|
||||
});
|
||||
expect(projectDir).toBe('/this/is/fake/process/cwd');
|
||||
} finally {
|
||||
spy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('returns the root directory of the project', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./README.md': '1',
|
||||
'./package.json': '2',
|
||||
'./src/index.ts': '3',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
const projectRoot = await findProjectRootAsync({ cwd: '/app/src' });
|
||||
expect(projectRoot).toBe('/app');
|
||||
});
|
||||
});
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
import { getConfig, modifyConfigAsync } from '@expo/config';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { Role } from '../../../../graphql/generated';
|
||||
import { AppQuery } from '../../../../graphql/queries/AppQuery';
|
||||
import { learnMore } from '../../../../log';
|
||||
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { ensureLoggedInAsync } from '../../contextUtils/ensureLoggedInAsync';
|
||||
import { findProjectRootAsync } from '../findProjectDirAndVerifyProjectSetupAsync';
|
||||
import { getProjectIdAsync } from '../getProjectIdAsync';
|
||||
|
||||
jest.mock('@expo/config');
|
||||
jest.mock('fs');
|
||||
|
||||
jest.mock('../../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../contextUtils/ensureLoggedInAsync');
|
||||
jest.mock('../../contextUtils/findProjectDirAndVerifyProjectSetupAsync');
|
||||
jest.mock('../../../../user/User');
|
||||
jest.mock('../../../../ora', () => ({
|
||||
ora: () => ({
|
||||
start: () => ({ succeed: () => {}, fail: () => {} }),
|
||||
}),
|
||||
}));
|
||||
jest.mock('../../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
|
||||
|
||||
describe(getProjectIdAsync, () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(ensureLoggedInAsync).mockResolvedValue({
|
||||
__typename: 'User',
|
||||
id: 'user_id',
|
||||
username: 'notnotbrent',
|
||||
primaryAccount: {
|
||||
id: 'account_id_1',
|
||||
name: 'notnotbrent',
|
||||
users: [{ role: Role.Owner, actor: { id: 'user_id' } }],
|
||||
},
|
||||
accounts: [
|
||||
{
|
||||
id: 'account_id_1',
|
||||
name: 'notnotbrent',
|
||||
users: [{ role: Role.Owner, actor: { id: 'user_id' } }],
|
||||
},
|
||||
{
|
||||
id: 'account_id_2',
|
||||
name: 'dominik',
|
||||
users: [{ role: Role.ViewOnly, actor: { id: 'user_id' } }],
|
||||
},
|
||||
],
|
||||
isExpoAdmin: false,
|
||||
});
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./README.md': '1',
|
||||
'./package.json': '2',
|
||||
'./src/index.ts': '3',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
|
||||
jest.mocked(findProjectRootAsync).mockResolvedValue('/app');
|
||||
});
|
||||
|
||||
it('gets the project ID from app config if exists', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({
|
||||
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
|
||||
} as any);
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
fullName: '@notnotbrent/test',
|
||||
slug: 'test',
|
||||
ownerAccount: { name: 'notnotbrent' } as any,
|
||||
});
|
||||
|
||||
await expect(
|
||||
getProjectIdAsync(
|
||||
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
|
||||
{ nonInteractive: false }
|
||||
)
|
||||
).resolves.toEqual('1234');
|
||||
});
|
||||
|
||||
it('throws when the owner is out of sync', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({
|
||||
exp: { name: 'test', slug: 'test', owner: 'wat', extra: { eas: { projectId: '1234' } } },
|
||||
} as any);
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
fullName: '@notnotbrent/test',
|
||||
slug: 'test',
|
||||
ownerAccount: { name: 'notnotbrent' } as any,
|
||||
});
|
||||
|
||||
await expect(
|
||||
getProjectIdAsync(
|
||||
{ name: 'test', slug: 'test', owner: 'wat', extra: { eas: { projectId: '1234' } } },
|
||||
{ nonInteractive: false }
|
||||
)
|
||||
).rejects.toThrow(
|
||||
`Project config: Project identified by "extra.eas.projectId" (notnotbrent) is not owned by owner specified in the "owner" field (wat). ${learnMore(
|
||||
'https://expo.fyi/eas-project-id'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when the slug is out of sync', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({
|
||||
exp: { name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
|
||||
} as any);
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
fullName: '@notnotbrent/test',
|
||||
slug: 'test',
|
||||
ownerAccount: { name: 'notnotbrent' } as any,
|
||||
});
|
||||
|
||||
await expect(
|
||||
getProjectIdAsync(
|
||||
{ name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
|
||||
{ nonInteractive: false }
|
||||
)
|
||||
).rejects.toThrow(
|
||||
`Project config: Slug for project identified by "extra.eas.projectId" (test) does not match the "slug" field (wat). ${learnMore(
|
||||
'https://expo.fyi/eas-project-id'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('fetches the project ID when not in app config, and sets it in the config', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({
|
||||
type: 'success',
|
||||
config: { expo: { name: 'test', slug: 'test', extra: { eas: { projectId: '2345' } } } },
|
||||
});
|
||||
jest
|
||||
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
|
||||
.mockImplementation(async () => '2345');
|
||||
|
||||
const projectId = await getProjectIdAsync(
|
||||
{ name: 'test', slug: 'test' },
|
||||
{
|
||||
nonInteractive: false,
|
||||
}
|
||||
);
|
||||
|
||||
expect(projectId).toEqual('2345');
|
||||
|
||||
expect(modifyConfigAsync).toHaveBeenCalledTimes(1);
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/app', {
|
||||
extra: { eas: { projectId: '2345' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if writing the ID back to the config fails', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({
|
||||
type: 'fail',
|
||||
config: null,
|
||||
});
|
||||
jest
|
||||
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
|
||||
.mockImplementation(async () => '4567');
|
||||
|
||||
await expect(
|
||||
getProjectIdAsync({ name: 'test', slug: 'test' }, { nonInteractive: false })
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Errors } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { ApiV2Error } from '../../../api';
|
||||
import Log, { learnMore } from '../../../log';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { Actor, getUserAsync, loginAsync } from '../../../user/User';
|
||||
import { retryUsernamePasswordAuthWithOTPAsync } from '../../../user/otp';
|
||||
|
||||
/**
|
||||
* Ensure that there is a logged-in actor. Show a login prompt if not.
|
||||
*
|
||||
* @param nonInteractive whether the log-in prompt if logged-out should be interactive
|
||||
* @returns logged-in Actor
|
||||
*
|
||||
* @deprecated Should not be used outside of context functions.
|
||||
*/
|
||||
export async function ensureLoggedInAsync({
|
||||
nonInteractive,
|
||||
}: {
|
||||
nonInteractive: boolean;
|
||||
}): Promise<Actor> {
|
||||
let user: Actor | undefined;
|
||||
try {
|
||||
user = await getUserAsync();
|
||||
} catch {}
|
||||
|
||||
if (!user) {
|
||||
Log.warn('An Expo user account is required to proceed.');
|
||||
await showLoginPromptAsync({ nonInteractive, printNewLine: true });
|
||||
user = await getUserAsync();
|
||||
}
|
||||
|
||||
return nullthrows(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user to log in.
|
||||
*
|
||||
* @deprecated Should not be used outside of context functions, except in the AccountLogin command.
|
||||
*/
|
||||
export async function showLoginPromptAsync({
|
||||
nonInteractive = false,
|
||||
printNewLine = false,
|
||||
} = {}): Promise<void> {
|
||||
if (nonInteractive) {
|
||||
Errors.error(
|
||||
`Either log in with ${chalk.bold('eas login')} or set the ${chalk.bold(
|
||||
'EXPO_TOKEN'
|
||||
)} environment variable if you're using EAS CLI on CI (${learnMore(
|
||||
'https://docs.expo.dev/accounts/programmatic-access/',
|
||||
{ dim: false }
|
||||
)})`
|
||||
);
|
||||
}
|
||||
if (printNewLine) {
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
Log.log('Log in to EAS');
|
||||
|
||||
const { username, password } = await promptAsync([
|
||||
{
|
||||
type: 'text',
|
||||
name: 'username',
|
||||
message: 'Email or username',
|
||||
},
|
||||
{
|
||||
type: 'password',
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
},
|
||||
]);
|
||||
try {
|
||||
await loginAsync({
|
||||
username,
|
||||
password,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof ApiV2Error && e.expoApiV2ErrorCode === 'ONE_TIME_PASSWORD_REQUIRED') {
|
||||
await retryUsernamePasswordAuthWithOTPAsync(
|
||||
username,
|
||||
password,
|
||||
e.expoApiV2ErrorMetadata as any
|
||||
);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
|
||||
import * as PackageManagerUtils from '@expo/package-manager';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import pkgDir from 'pkg-dir';
|
||||
import semver from 'semver';
|
||||
|
||||
import { learnMore } from '../../../log';
|
||||
import { easCliVersion } from '../../../utils/easCli';
|
||||
import { getVcsClient, setVcsClient } from '../../../vcs';
|
||||
import GitClient from '../../../vcs/clients/git';
|
||||
|
||||
async function applyCliConfigAsync(projectDir: string): Promise<void> {
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const config = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
|
||||
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
|
||||
throw new Error(
|
||||
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
|
||||
);
|
||||
}
|
||||
if (config?.requireCommit) {
|
||||
setVcsClient(new GitClient());
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureEasCliIsNotInDependenciesAsync(projectDir: string): Promise<void> {
|
||||
let printCliVersionWarning = false;
|
||||
|
||||
const consoleWarn = (msg?: string): void => {
|
||||
if (msg) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(chalk.yellow(msg));
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn();
|
||||
}
|
||||
};
|
||||
|
||||
if (await isEasCliInDependenciesAsync(projectDir)) {
|
||||
printCliVersionWarning = true;
|
||||
consoleWarn(`Found ${chalk.bold('eas-cli')} in your project dependencies.`);
|
||||
}
|
||||
|
||||
const maybeRepoRoot = PackageManagerUtils.findWorkspaceRoot(projectDir) ?? projectDir;
|
||||
if (maybeRepoRoot !== projectDir && (await isEasCliInDependenciesAsync(maybeRepoRoot))) {
|
||||
printCliVersionWarning = true;
|
||||
consoleWarn(`Found ${chalk.bold('eas-cli')} in your monorepo dependencies.`);
|
||||
}
|
||||
|
||||
if (printCliVersionWarning) {
|
||||
consoleWarn(
|
||||
`It's recommended to use the ${chalk.bold(
|
||||
'"cli.version"'
|
||||
)} field in eas.json to enforce the ${chalk.bold('eas-cli')} version for your project.`
|
||||
);
|
||||
consoleWarn(
|
||||
learnMore('https://github.com/expo/eas-cli#enforcing-eas-cli-version-for-your-project')
|
||||
);
|
||||
consoleWarn();
|
||||
}
|
||||
}
|
||||
|
||||
async function isEasCliInDependenciesAsync(dir: string): Promise<boolean> {
|
||||
const packageJsonPath = path.join(dir, 'package.json');
|
||||
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
||||
return (
|
||||
packageJson?.dependencies?.['eas-cli'] !== undefined ||
|
||||
packageJson?.devDependencies?.['eas-cli'] !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the project root directory.
|
||||
*
|
||||
* @deprecated Should not be used outside of context functions.
|
||||
*/
|
||||
export async function findProjectRootAsync({
|
||||
cwd,
|
||||
defaultToProcessCwd = false,
|
||||
}: {
|
||||
cwd?: string;
|
||||
defaultToProcessCwd?: boolean;
|
||||
} = {}): Promise<string> {
|
||||
const projectRootDir = await pkgDir(cwd);
|
||||
if (!projectRootDir) {
|
||||
if (!defaultToProcessCwd) {
|
||||
throw new Error('Run this command inside a project directory.');
|
||||
} else {
|
||||
return process.cwd();
|
||||
}
|
||||
} else {
|
||||
let vcsRoot;
|
||||
try {
|
||||
vcsRoot = path.normalize(await getVcsClient().getRootPathAsync());
|
||||
} catch {}
|
||||
if (vcsRoot && vcsRoot.startsWith(projectRootDir) && vcsRoot !== projectRootDir) {
|
||||
throw new Error(
|
||||
`package.json is outside of the current git repository (project root: ${projectRootDir}, git root: ${vcsRoot}.`
|
||||
);
|
||||
}
|
||||
return projectRootDir;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the project root directory and ensure some constraints about the project setup
|
||||
* like CLI version and dependencies.
|
||||
* @returns the project root directory
|
||||
*
|
||||
* @deprecated Should not be used outside of context functions.
|
||||
*/
|
||||
export async function findProjectDirAndVerifyProjectSetupAsync(): Promise<string> {
|
||||
const projectDir = await findProjectRootAsync();
|
||||
await applyCliConfigAsync(projectDir);
|
||||
await ensureEasCliIsNotInDependenciesAsync(projectDir);
|
||||
return projectDir;
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { getProjectConfigDescription, modifyConfigAsync } from '@expo/config';
|
||||
import { ExpoConfig } from '@expo/config-types';
|
||||
import { Env } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import Log, { learnMore } from '../../../log';
|
||||
import { ora } from '../../../ora';
|
||||
import { getExpoConfig } from '../../../project/expoConfig';
|
||||
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { toAppPrivacy } from '../../../project/projectUtils';
|
||||
import { Actor } from '../../../user/User';
|
||||
import { ensureLoggedInAsync } from './ensureLoggedInAsync';
|
||||
import { findProjectRootAsync } from './findProjectDirAndVerifyProjectSetupAsync';
|
||||
|
||||
/**
|
||||
* Save an EAS project ID to the appropriate field in the app config.
|
||||
*
|
||||
* @deprecated Should not be used outside of context functions except in the init command.
|
||||
*/
|
||||
export async function saveProjectIdToAppConfigAsync(
|
||||
projectDir: string,
|
||||
projectId: string,
|
||||
options: { env?: Env } = {}
|
||||
): Promise<void> {
|
||||
const exp = getExpoConfig(projectDir, options);
|
||||
const result = await modifyConfigAsync(projectDir, {
|
||||
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
|
||||
});
|
||||
|
||||
switch (result.type) {
|
||||
case 'success':
|
||||
break;
|
||||
case 'warn': {
|
||||
Log.warn();
|
||||
Log.warn(
|
||||
`Warning: Your project uses dynamic app configuration, and the EAS project ID can't automatically be added to it.`
|
||||
);
|
||||
Log.warn(
|
||||
chalk.dim(
|
||||
'https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs'
|
||||
)
|
||||
);
|
||||
Log.warn();
|
||||
Log.warn(
|
||||
`To complete the setup process, set the ${chalk.bold(
|
||||
'extra.eas.projectId'
|
||||
)} in your ${chalk.bold(getProjectConfigDescription(projectDir))}:`
|
||||
);
|
||||
Log.warn();
|
||||
Log.warn(chalk.bold(JSON.stringify({ expo: { extra: { eas: { projectId } } } }, null, 2)));
|
||||
Log.warn();
|
||||
throw new Error(result.message);
|
||||
}
|
||||
case 'fail':
|
||||
throw new Error(result.message);
|
||||
default:
|
||||
throw new Error('Unexpected result type from modifyConfigAsync');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the EAS project ID from the app config. If the project ID is not set in the config,
|
||||
* use the owner/slug to identify an EAS project on the server (asking for confirmation first),
|
||||
* and attempt to save the EAS project ID to the appropriate field in the app config. If unable to
|
||||
* save to the app config, throw an error.
|
||||
*
|
||||
* @returns the EAS project ID
|
||||
*
|
||||
* @deprecated Should not be used outside of context functions.
|
||||
*/
|
||||
export async function getProjectIdAsync(
|
||||
exp: ExpoConfig,
|
||||
options: { env?: Env; nonInteractive: boolean }
|
||||
): Promise<string> {
|
||||
const localProjectId = exp.extra?.eas?.projectId;
|
||||
if (localProjectId) {
|
||||
// check that the local project ID matches account and slug
|
||||
const appForProjectId = await AppQuery.byIdAsync(localProjectId);
|
||||
if (exp.owner && exp.owner !== appForProjectId.ownerAccount.name) {
|
||||
throw new Error(
|
||||
`Project config: Project identified by "extra.eas.projectId" (${
|
||||
appForProjectId.ownerAccount.name
|
||||
}) is not owned by owner specified in the "owner" field (${exp.owner}). ${learnMore(
|
||||
'https://expo.fyi/eas-project-id'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (exp.slug && exp.slug !== appForProjectId.slug) {
|
||||
throw new Error(
|
||||
`Project config: Slug for project identified by "extra.eas.projectId" (${
|
||||
appForProjectId.slug
|
||||
}) does not match the "slug" field (${exp.slug}). ${learnMore(
|
||||
'https://expo.fyi/eas-project-id'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
return localProjectId;
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
if (!projectDir) {
|
||||
throw new Error('This command must be run inside a project directory.');
|
||||
}
|
||||
|
||||
Log.warn('EAS project not configured.');
|
||||
|
||||
const getAccountNameForEASProjectSync = (exp: ExpoConfig, user: Actor): string => {
|
||||
if (exp.owner) {
|
||||
return exp.owner;
|
||||
}
|
||||
switch (user.__typename) {
|
||||
case 'User':
|
||||
return user.username;
|
||||
case 'Robot':
|
||||
throw new Error(
|
||||
'The "owner" manifest property is required when using robot users. See: https://docs.expo.dev/versions/latest/config/app/#owner'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const actor = await ensureLoggedInAsync({
|
||||
nonInteractive: options.nonInteractive,
|
||||
});
|
||||
const projectId = await fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync(
|
||||
{
|
||||
accountName: getAccountNameForEASProjectSync(exp, actor),
|
||||
projectName: exp.slug,
|
||||
privacy: toAppPrivacy(exp.privacy),
|
||||
},
|
||||
{
|
||||
nonInteractive: options.nonInteractive,
|
||||
},
|
||||
actor
|
||||
);
|
||||
|
||||
const spinner = ora(`Linking local project to EAS project ${projectId}`).start();
|
||||
try {
|
||||
await saveProjectIdToAppConfigAsync(projectDir, projectId, options);
|
||||
spinner.succeed(`Linked local project to EAS project ${projectId}`);
|
||||
} catch (e: any) {
|
||||
spinner.fail();
|
||||
throw e;
|
||||
}
|
||||
|
||||
return projectId;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import ActorContextField from '../../commandUtils/context/ActorContextField';
|
||||
import { showLoginPromptAsync } from '../../commandUtils/context/contextUtils/ensureLoggedInAsync';
|
||||
import Log from '../../log';
|
||||
|
||||
export default class AccountLogin extends EasCommand {
|
||||
@@ -7,7 +7,7 @@ export default class AccountLogin extends EasCommand {
|
||||
static override aliases = ['login'];
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
await ActorContextField['showLoginPromptAsync']();
|
||||
await showLoginPromptAsync();
|
||||
Log.log('Logged in');
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { getDefaultBranchNameAsync } from '../../branch/utils';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
@@ -62,7 +62,7 @@ export default class BranchCreate extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -70,7 +70,9 @@ export default class BranchCreate extends EasCommand {
|
||||
args: { name },
|
||||
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(BranchCreate);
|
||||
const { projectId } = await this.getContextAsync(BranchCreate, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(BranchCreate, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
@@ -77,7 +77,7 @@ export default class BranchDelete extends EasCommand {
|
||||
static override description = 'delete a branch';
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
static override args = [
|
||||
@@ -104,7 +104,9 @@ export default class BranchDelete extends EasCommand {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const { projectId } = await this.getContextAsync(BranchDelete, { nonInteractive });
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(BranchDelete, { nonInteractive });
|
||||
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
|
||||
|
||||
if (!branchName) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { listAndRenderBranchesOnAppAsync } from '../../branch/queries';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { EasPaginatedQueryFlags, getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
@@ -13,12 +13,14 @@ export default class BranchList extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BranchList);
|
||||
const { projectId } = await this.getContextAsync(BranchList, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(BranchList, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
});
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
@@ -63,14 +63,16 @@ export default class BranchRename extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
let {
|
||||
flags: { json: jsonFlag, from: currentName, to: newName, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(BranchRename);
|
||||
const { projectId } = await this.getContextAsync(BranchRename, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(BranchRename, {
|
||||
nonInteractive,
|
||||
});
|
||||
if (jsonFlag) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasPaginatedQueryFlags,
|
||||
@@ -27,7 +27,7 @@ export default class BranchView extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -36,7 +36,9 @@ export default class BranchView extends EasCommand {
|
||||
flags,
|
||||
} = await this.parse(BranchView);
|
||||
const { 'non-interactive': nonInteractive } = flags;
|
||||
const { projectId } = await this.getContextAsync(BranchView, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(BranchView, {
|
||||
nonInteractive,
|
||||
});
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
@@ -129,7 +129,7 @@ export default class BuildCancel extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -137,7 +137,9 @@ export default class BuildCancel extends EasCommand {
|
||||
args: { BUILD_ID: buildIdFromArg },
|
||||
flags: { 'non-interactive': nonInteractive },
|
||||
} = await this.parse(BuildCancel);
|
||||
const { projectId } = await this.getContextAsync(BuildCancel, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(BuildCancel, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
|
||||
@@ -4,13 +4,9 @@ import chalk from 'chalk';
|
||||
|
||||
import { cleanUpOldEasBuildGradleScriptAsync } from '../../build/android/syncProjectConfiguration';
|
||||
import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import EasCommand, {
|
||||
EASCommandProjectDirContext,
|
||||
EASCommandProjectIdContext,
|
||||
} from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { promptAsync } from '../../prompts';
|
||||
@@ -30,13 +26,14 @@ export default class BuildConfigure extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...EASCommandProjectDirContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BuildConfigure);
|
||||
const { projectId, projectDir } = await this.getContextAsync(BuildConfigure, {
|
||||
const {
|
||||
projectConfig: { exp, projectId, projectDir },
|
||||
} = await this.getContextAsync(BuildConfigure, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
@@ -65,8 +62,6 @@ export default class BuildConfigure extends EasCommand {
|
||||
|
||||
// configure expo-updates
|
||||
if (expoUpdatesIsInstalled) {
|
||||
const exp = getExpoConfig(projectDir);
|
||||
|
||||
if ([RequestedPlatform.Android, RequestedPlatform.All].includes(platform)) {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
|
||||
@@ -3,15 +3,12 @@ import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import figures from 'figures';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
|
||||
import { UserInputResourceClass } from '../../build/types';
|
||||
import EasCommand, {
|
||||
EASCommandLoggedInContext,
|
||||
EASCommandProjectDirContext,
|
||||
EASCommandProjectIdContext,
|
||||
} from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { StatuspageServiceName } from '../../graphql/generated';
|
||||
import Log, { link } from '../../log';
|
||||
@@ -100,9 +97,9 @@ export default class Build extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandLoggedInContext,
|
||||
...EASCommandProjectIdContext,
|
||||
...EASCommandProjectDirContext,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -113,7 +110,7 @@ export default class Build extends EasCommand {
|
||||
}
|
||||
const flags = this.sanitizeFlags(rawFlags);
|
||||
|
||||
const { actor, projectId, projectDir } = await this.getContextAsync(Build, {
|
||||
const { actor, getDynamicProjectConfigAsync, projectDir } = await this.getContextAsync(Build, {
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
|
||||
@@ -129,7 +126,12 @@ export default class Build extends EasCommand {
|
||||
|
||||
const flagsWithPlatform = await this.ensurePlatformSelectedAsync(flags);
|
||||
|
||||
await runBuildAndSubmitAsync(projectDir, flagsWithPlatform, actor, projectId);
|
||||
await runBuildAndSubmitAsync(
|
||||
projectDir,
|
||||
flagsWithPlatform,
|
||||
actor,
|
||||
getDynamicProjectConfigAsync
|
||||
);
|
||||
}
|
||||
|
||||
private sanitizeFlags(
|
||||
@@ -227,6 +229,9 @@ async function handleDeprecatedEasJsonAsync(
|
||||
projectDir: string,
|
||||
nonInteractive: boolean
|
||||
): Promise<void> {
|
||||
if (!(await fs.pathExists(EasJsonAccessor.formatEasJsonPath(projectDir)))) {
|
||||
return;
|
||||
}
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const profileNames = await EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor);
|
||||
const platformAndProfileNames: [Platform, string][] = profileNames.flatMap(profileName => [
|
||||
|
||||
@@ -5,11 +5,7 @@ import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
|
||||
import EasCommand, {
|
||||
EASCommandLoggedInContext,
|
||||
EASCommandProjectDirContext,
|
||||
EASCommandProjectIdContext,
|
||||
} from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
@@ -66,16 +62,19 @@ export default class BuildInspect extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandLoggedInContext,
|
||||
...EASCommandProjectIdContext,
|
||||
...EASCommandProjectDirContext,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BuildInspect);
|
||||
const { actor, projectId, projectDir } = await this.getContextAsync(BuildInspect, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
const { actor, getDynamicProjectConfigAsync, projectDir } = await this.getContextAsync(
|
||||
BuildInspect,
|
||||
{
|
||||
nonInteractive: false,
|
||||
}
|
||||
);
|
||||
|
||||
const outputDirectory = path.resolve(process.cwd(), flags.output);
|
||||
const tmpWorkingdir = path.join(getTmpDirectory(), uuidv4());
|
||||
@@ -115,7 +114,7 @@ export default class BuildInspect extends EasCommand {
|
||||
},
|
||||
},
|
||||
actor,
|
||||
projectId
|
||||
getDynamicProjectConfigAsync
|
||||
);
|
||||
if (!flags.verbose) {
|
||||
Log.log(chalk.green('Build successful'));
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Flags } from '@oclif/core';
|
||||
|
||||
import { BUILDS_LIMIT, listAndRenderBuildsOnAppAsync } from '../../build/queries';
|
||||
import { BuildDistributionType, BuildStatus } from '../../build/types';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasPaginatedQueryFlags,
|
||||
@@ -56,7 +56,7 @@ export default class BuildList extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -69,7 +69,9 @@ export default class BuildList extends EasCommand {
|
||||
distribution: buildDistribution,
|
||||
'non-interactive': nonInteractive,
|
||||
} = flags;
|
||||
const { projectId } = await this.getContextAsync(BuildList, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(BuildList, {
|
||||
nonInteractive,
|
||||
});
|
||||
if (jsonFlag) {
|
||||
|
||||
@@ -4,11 +4,7 @@ import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand, {
|
||||
EASCommandLoggedInContext,
|
||||
EASCommandProjectDirContext,
|
||||
EASCommandProjectIdContext,
|
||||
} from '../../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../../commandUtils/EasCommand';
|
||||
import { AppVersionMutation } from '../../../graphql/mutations/AppVersionMutation';
|
||||
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
|
||||
import { toAppPlatform } from '../../../graphql/types/AppPlatform';
|
||||
@@ -16,7 +12,6 @@ import Log from '../../../log';
|
||||
import { selectPlatformAsync } from '../../../platform';
|
||||
import { VERSION_CODE_REQUIREMENTS, isValidVersionCode } from '../../../project/android/versions';
|
||||
import { getApplicationIdentifierAsync } from '../../../project/applicationIdentifier';
|
||||
import { getExpoConfig } from '../../../project/expoConfig';
|
||||
import { BUILD_NUMBER_REQUIREMENTS, isValidBuildNumber } from '../../../project/ios/versions';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../../project/projectUtils';
|
||||
import {
|
||||
@@ -43,16 +38,19 @@ export default class BuildVersionSetView extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandLoggedInContext,
|
||||
...EASCommandProjectIdContext,
|
||||
...EASCommandProjectDirContext,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
};
|
||||
|
||||
public async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BuildVersionSetView);
|
||||
const { actor, projectId, projectDir } = await this.getContextAsync(BuildVersionSetView, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
const { actor, getDynamicProjectConfigAsync, projectDir } = await this.getContextAsync(
|
||||
BuildVersionSetView,
|
||||
{
|
||||
nonInteractive: false,
|
||||
}
|
||||
);
|
||||
|
||||
const platform = await selectPlatformAsync(flags.platform);
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
@@ -63,7 +61,7 @@ export default class BuildVersionSetView extends EasCommand {
|
||||
flags.profile ?? undefined
|
||||
);
|
||||
|
||||
const exp = getExpoConfig(projectDir, { env: profile.env });
|
||||
const { exp, projectId } = await getDynamicProjectConfigAsync({ env: profile.env });
|
||||
const displayName = await getDisplayNameForProjectIdAsync(projectId);
|
||||
|
||||
validateAppConfigForRemoteVersionSource(exp, platform);
|
||||
|
||||
@@ -6,11 +6,7 @@ import chalk from 'chalk';
|
||||
|
||||
import { updateNativeVersionsAsync as updateAndroidNativeVersionsAsync } from '../../../build/android/version';
|
||||
import { updateNativeVersionsAsync as updateIosNativeVersionsAsync } from '../../../build/ios/version';
|
||||
import EasCommand, {
|
||||
EASCommandLoggedInContext,
|
||||
EASCommandProjectDirContext,
|
||||
EASCommandProjectIdContext,
|
||||
} from '../../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../../commandUtils/EasCommand';
|
||||
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
|
||||
import { toAppPlatform } from '../../../graphql/types/AppPlatform';
|
||||
import Log from '../../../log';
|
||||
@@ -22,7 +18,6 @@ import {
|
||||
import { getAppBuildGradleAsync } from '../../../project/android/gradleUtils';
|
||||
import { VERSION_CODE_REQUIREMENTS, isValidVersionCode } from '../../../project/android/versions';
|
||||
import { getApplicationIdentifierAsync } from '../../../project/applicationIdentifier';
|
||||
import { getExpoConfig } from '../../../project/expoConfig';
|
||||
import { resolveXcodeBuildContextAsync } from '../../../project/ios/scheme';
|
||||
import { resolveTargetsAsync } from '../../../project/ios/target';
|
||||
import { BUILD_NUMBER_REQUIREMENTS, isValidBuildNumber } from '../../../project/ios/versions';
|
||||
@@ -60,16 +55,19 @@ export default class BuildVersionSyncView extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandLoggedInContext,
|
||||
...EASCommandProjectIdContext,
|
||||
...EASCommandProjectDirContext,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
};
|
||||
|
||||
public async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BuildVersionSyncView);
|
||||
const { actor, projectId, projectDir } = await this.getContextAsync(BuildVersionSyncView, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
const { actor, getDynamicProjectConfigAsync, projectDir } = await this.getContextAsync(
|
||||
BuildVersionSyncView,
|
||||
{
|
||||
nonInteractive: true,
|
||||
}
|
||||
);
|
||||
|
||||
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
@@ -83,7 +81,9 @@ export default class BuildVersionSyncView extends EasCommand {
|
||||
profileName: flags.profile ?? undefined,
|
||||
});
|
||||
for (const profileInfo of buildProfiles) {
|
||||
const exp = getExpoConfig(projectDir, { env: profileInfo.profile.env });
|
||||
const { exp, projectId } = await getDynamicProjectConfigAsync({
|
||||
env: profileInfo.profile.env,
|
||||
});
|
||||
|
||||
validateAppConfigForRemoteVersionSource(exp, profileInfo.platform);
|
||||
const platformDisplayName = appPlatformDisplayNames[toAppPlatform(profileInfo.platform)];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasJsonOnlyFlag } from '../../commandUtils/flags';
|
||||
import { BuildFragment } from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
@@ -18,7 +18,7 @@ export default class BuildView extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -26,7 +26,9 @@ export default class BuildView extends EasCommand {
|
||||
args: { BUILD_ID: buildId },
|
||||
flags,
|
||||
} = await this.parse(BuildView);
|
||||
const { projectId } = await this.getContextAsync(BuildView, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(BuildView, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
if (flags.json) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { BranchNotFoundError } from '../../branch/utils';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
@@ -71,7 +71,7 @@ export default class ChannelCreate extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -79,7 +79,9 @@ export default class ChannelCreate extends EasCommand {
|
||||
args: { name: channelName },
|
||||
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(ChannelCreate);
|
||||
const { projectId } = await this.getContextAsync(ChannelCreate, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(ChannelCreate, {
|
||||
nonInteractive,
|
||||
});
|
||||
if (jsonFlag) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { selectChannelOnAppAsync } from '../../channel/queries';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
@@ -30,7 +30,7 @@ export default class ChannelDelete extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -38,7 +38,9 @@ export default class ChannelDelete extends EasCommand {
|
||||
args: { name: nameArg },
|
||||
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(ChannelDelete);
|
||||
const { projectId } = await this.getContextAsync(ChannelDelete, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(ChannelDelete, {
|
||||
nonInteractive,
|
||||
});
|
||||
if (jsonFlag) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import gql from 'graphql-tag';
|
||||
|
||||
import { selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import { selectChannelOnAppAsync } from '../../channel/queries';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
@@ -66,7 +66,7 @@ export default class ChannelEdit extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -74,7 +74,9 @@ export default class ChannelEdit extends EasCommand {
|
||||
args,
|
||||
flags: { branch: branchFlag, json, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(ChannelEdit);
|
||||
const { projectId } = await this.getContextAsync(ChannelEdit, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(ChannelEdit, {
|
||||
nonInteractive,
|
||||
});
|
||||
if (json) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CHANNELS_LIMIT, listAndRenderChannelsOnAppAsync } from '../../channel/queries';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasPaginatedQueryFlags,
|
||||
@@ -18,14 +18,16 @@ export default class ChannelList extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(ChannelList);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
const { json: jsonFlag, 'non-interactive': nonInteractive } = flags;
|
||||
const { projectId } = await this.getContextAsync(ChannelList, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(ChannelList, {
|
||||
nonInteractive,
|
||||
});
|
||||
if (jsonFlag) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { BranchMapping, getBranchMapping } from '../../channel/utils';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { UpdateBranch } from '../../graphql/generated';
|
||||
import { BranchQuery } from '../../graphql/queries/BranchQuery';
|
||||
@@ -312,7 +312,7 @@ export default class ChannelRollout extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -326,7 +326,9 @@ export default class ChannelRollout extends EasCommand {
|
||||
'non-interactive': nonInteractive,
|
||||
},
|
||||
} = await this.parse(ChannelRollout);
|
||||
const { projectId } = await this.getContextAsync(ChannelRollout, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(ChannelRollout, {
|
||||
nonInteractive,
|
||||
});
|
||||
if (jsonFlag) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
listAndRenderBranchesAndUpdatesOnChannelAsync,
|
||||
selectChannelOnAppAsync,
|
||||
} from '../../channel/queries';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { EasPaginatedQueryFlags, getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
@@ -26,7 +26,7 @@ export default class ChannelView extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -36,7 +36,9 @@ export default class ChannelView extends EasCommand {
|
||||
} = await this.parse(ChannelView);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
const { json: jsonFlag, 'non-interactive': nonInteractive } = flags;
|
||||
const { projectId } = await this.getContextAsync(ChannelView, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(ChannelView, {
|
||||
nonInteractive,
|
||||
});
|
||||
if (jsonFlag) {
|
||||
|
||||
@@ -4,18 +4,17 @@ import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand, { EASCommandProjectDirContext } from '../commandUtils/EasCommand';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { toAppPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log from '../log';
|
||||
import { appPlatformEmojis } from '../platform';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { selectAsync } from '../prompts';
|
||||
|
||||
export default class Config extends EasCommand {
|
||||
static override description = 'display project configuration (app.json + eas.json)';
|
||||
|
||||
static override flags = {
|
||||
platform: Flags.enum({ char: 'p', options: ['android', 'ios'] }),
|
||||
platform: Flags.enum<Platform>({ char: 'p', options: [Platform.ANDROID, Platform.IOS] }),
|
||||
profile: Flags.string({
|
||||
char: 'e',
|
||||
description:
|
||||
@@ -25,18 +24,16 @@ export default class Config extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectDirContext,
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(Config);
|
||||
const { projectDir } = await this.getContextAsync(Config, {
|
||||
const { platform: maybePlatform, profile: maybeProfile } = flags;
|
||||
const { getDynamicProjectConfigAsync, projectDir } = await this.getContextAsync(Config, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
const { platform: maybePlatform, profile: maybeProfile } = flags as {
|
||||
platform?: Platform;
|
||||
profile?: string;
|
||||
};
|
||||
|
||||
const accessor = new EasJsonAccessor(projectDir);
|
||||
const profileName =
|
||||
@@ -64,7 +61,10 @@ export default class Config extends EasCommand {
|
||||
]));
|
||||
|
||||
const profile = await EasJsonUtils.getBuildProfileAsync(accessor, platform, profileName);
|
||||
const config = getExpoConfig(projectDir, { env: profile.env, isPublicConfig: true });
|
||||
const { exp: config } = await getDynamicProjectConfigAsync({
|
||||
env: profile.env,
|
||||
isPublicConfig: true,
|
||||
});
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold(getProjectConfigDescription(projectDir)));
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand, {
|
||||
EASCommandLoggedInContext,
|
||||
EASCommandProjectDirContext,
|
||||
EASCommandProjectIdContext,
|
||||
} from '../commandUtils/EasCommand';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
|
||||
export default class Credentials extends EasCommand {
|
||||
static override description = 'manage credentials';
|
||||
@@ -16,19 +11,16 @@ export default class Credentials extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandLoggedInContext,
|
||||
...EASCommandProjectIdContext,
|
||||
...EASCommandProjectDirContext,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.OptionalProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(Credentials);
|
||||
const { actor, projectId, projectDir } = await this.getContextAsync(Credentials, {
|
||||
const { actor, projectConfig } = await this.getContextAsync(Credentials, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const exp = getExpoConfig(projectDir);
|
||||
|
||||
await new SelectPlatform(actor, { projectId, exp }, flags.platform).runAsync();
|
||||
await new SelectPlatform(actor, projectConfig ?? null, flags.platform).runAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
import EasCommand, {
|
||||
EASCommandLoggedInContext,
|
||||
EASCommandProjectIdContext,
|
||||
} from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import AppStoreApi from '../../credentials/ios/appstore/AppStoreApi';
|
||||
import { createContextAsync } from '../../devices/context';
|
||||
import DeviceManager from '../../devices/manager';
|
||||
@@ -10,17 +7,21 @@ export default class DeviceCreate extends EasCommand {
|
||||
static override description = 'register new Apple Devices to use for internal distribution';
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandLoggedInContext,
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.OptionalProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
// this command is interactive by design
|
||||
const { actor, projectId } = await this.getContextAsync(DeviceCreate, {
|
||||
const { actor, projectConfig } = await this.getContextAsync(DeviceCreate, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const ctx = await createContextAsync({ appStore: new AppStoreApi(), user: actor, projectId });
|
||||
const ctx = await createContextAsync({
|
||||
appStore: new AppStoreApi(),
|
||||
user: actor,
|
||||
projectId: projectConfig?.projectId,
|
||||
});
|
||||
const manager = new DeviceManager(ctx);
|
||||
await manager.createAsync();
|
||||
}
|
||||
|
||||
@@ -2,151 +2,161 @@ import { Device, DeviceStatus } from '@expo/apple-utils';
|
||||
import { Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { chooseDevicesToDeleteAsync } from '../../credentials/ios/actions/DeviceUtils';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { PaginatedQueryOptions, getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import { AppleDeviceMutation } from '../../credentials/ios/api/graphql/mutations/AppleDeviceMutation';
|
||||
import {
|
||||
AppleDeviceQuery,
|
||||
AppleDeviceQueryResult,
|
||||
AppleDevicesByTeamIdentifierQueryResult,
|
||||
} from '../../credentials/ios/api/graphql/queries/AppleDeviceQuery';
|
||||
import { AppleTeamQuery } from '../../credentials/ios/api/graphql/queries/AppleTeamQuery';
|
||||
import { authenticateAsync, getRequestContext } from '../../credentials/ios/appstore/authenticate';
|
||||
import {
|
||||
selectAppleDeviceOnAppleTeamAsync,
|
||||
selectAppleTeamOnAccountAsync,
|
||||
} from '../../devices/queries';
|
||||
import formatDevice from '../../devices/utils/formatDevice';
|
||||
import { AppleDevice, Maybe } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export default class DeviceDelete extends EasCommand {
|
||||
static override description = 'remove a registered device from your account';
|
||||
|
||||
static override flags = {
|
||||
'apple-team-id': Flags.string(),
|
||||
udid: Flags.string({ multiple: true }),
|
||||
'apple-team-id': Flags.string({ description: 'The Apple team ID on which to find the device' }),
|
||||
udid: Flags.string({ description: 'The Apple device ID to disable' }),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
let {
|
||||
flags: { 'apple-team-id': appleTeamIdentifier, udid: udids },
|
||||
} = await this.parse(DeviceDelete);
|
||||
const { projectId } = await this.getContextAsync(DeviceDelete, {
|
||||
nonInteractive: false,
|
||||
const { flags } = await this.parse(DeviceDelete);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
let { 'apple-team-id': appleTeamIdentifier, udid } = flags;
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(DeviceDelete, {
|
||||
nonInteractive: paginatedQueryOptions.nonInteractive,
|
||||
});
|
||||
|
||||
const account = await getOwnerAccountForProjectIdAsync(projectId);
|
||||
let appleTeamName;
|
||||
|
||||
if (paginatedQueryOptions.json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
if (!appleTeamIdentifier) {
|
||||
appleTeamIdentifier = await this.askForAppleTeamAsync(account.name);
|
||||
const appleTeam = await selectAppleTeamOnAccountAsync({
|
||||
accountName: account.name,
|
||||
selectionPromptTitle: `What Apple team would you like to list devices for?`,
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
appleTeamIdentifier = appleTeam.appleTeamIdentifier;
|
||||
appleTeamName = appleTeam.appleTeamName;
|
||||
}
|
||||
|
||||
assert(appleTeamIdentifier, 'No team identifier is specified');
|
||||
|
||||
const appleDevicesResult = await this.getDevicesForTeamAsync(account.name, appleTeamIdentifier);
|
||||
const chosenDevice = udid
|
||||
? await AppleDeviceQuery.getByDeviceIdentifierAsync(account.name, udid)
|
||||
: await selectAppleDeviceOnAppleTeamAsync({
|
||||
accountName: account.name,
|
||||
appleTeamIdentifier,
|
||||
selectionPromptTitle: `Which device would you like to disable?`,
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
|
||||
if (!appleDevicesResult) {
|
||||
this.logChosenDevice(chosenDevice, appleTeamName, appleTeamIdentifier, paginatedQueryOptions);
|
||||
|
||||
if (!(await this.shouldRemoveDeviceFromExpoAsync(paginatedQueryOptions))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { appleTeamName, appleDevices } = appleDevicesResult;
|
||||
await this.removeDeviceFromExpoAsync(chosenDevice);
|
||||
|
||||
const chosenDevices = await this.chooseDevicesToDeleteAsync(appleDevices, udids);
|
||||
|
||||
if (chosenDevices.length === 0) {
|
||||
Log.newLine();
|
||||
Log.warn('No devices were chosen to be removed.');
|
||||
return;
|
||||
if (await this.shouldDisableDeviceOnAppleAsync(paginatedQueryOptions)) {
|
||||
await this.disableDeviceOnAppleAsync(chosenDevice, appleTeamIdentifier);
|
||||
}
|
||||
|
||||
this.logChosenDevices(chosenDevices, appleTeamName, appleTeamIdentifier);
|
||||
|
||||
const hasRemoved = await this.askAndRemoveFromExpoAsync(chosenDevices);
|
||||
|
||||
if (!hasRemoved) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.askAndDisableOnAppleAsync(chosenDevices, appleTeamIdentifier);
|
||||
}
|
||||
|
||||
async askAndDisableOnAppleAsync(
|
||||
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[],
|
||||
async shouldDisableDeviceOnAppleAsync({
|
||||
nonInteractive,
|
||||
}: PaginatedQueryOptions): Promise<boolean> {
|
||||
if (!nonInteractive) {
|
||||
Log.newLine();
|
||||
return await toggleConfirmAsync({
|
||||
message: 'Do you want to disable this device on your Apple account as well?',
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async disableDeviceOnAppleAsync(
|
||||
device: AppleDevice | AppleDeviceQueryResult,
|
||||
appleTeamIdentifier: string
|
||||
): Promise<void> {
|
||||
Log.newLine();
|
||||
const deleteOnApple = await toggleConfirmAsync({
|
||||
message: 'Do you want to disable these devices on your Apple account as well?',
|
||||
});
|
||||
|
||||
if (!deleteOnApple) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = await authenticateAsync({ teamId: appleTeamIdentifier });
|
||||
const context = getRequestContext(ctx);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
const removeAppleSpinner = ora('Disabling devices on Apple').start();
|
||||
const removeAppleSpinner = ora('Disabling device on Apple').start();
|
||||
try {
|
||||
const chosenDeviceIdentifiers = chosenDevices.map(cd => cd.identifier);
|
||||
const allDevices = await Device.getAsync(context);
|
||||
const realDevices = allDevices.filter(d =>
|
||||
chosenDeviceIdentifiers.includes(d.attributes.udid)
|
||||
);
|
||||
|
||||
for (const device of realDevices) {
|
||||
await device.updateAsync({ status: DeviceStatus.DISABLED });
|
||||
const appleValidatedDevices = await Device.getAsync(context);
|
||||
const appleValidatedDevice = appleValidatedDevices.find(d => d.id === device.id);
|
||||
if (appleValidatedDevice) {
|
||||
await appleValidatedDevice.updateAsync({ status: DeviceStatus.DISABLED });
|
||||
}
|
||||
|
||||
removeAppleSpinner.succeed('Disabled devices on Apple');
|
||||
removeAppleSpinner.succeed('Disabled device on Apple');
|
||||
} catch (err) {
|
||||
removeAppleSpinner.fail();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async askAndRemoveFromExpoAsync(
|
||||
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[]
|
||||
): Promise<boolean> {
|
||||
Log.warn(
|
||||
`You are about to remove the Apple device${
|
||||
chosenDevices.length > 1 ? 's' : ''
|
||||
} listed above from your Expo account.`
|
||||
);
|
||||
Log.newLine();
|
||||
async shouldRemoveDeviceFromExpoAsync({
|
||||
nonInteractive,
|
||||
}: PaginatedQueryOptions): Promise<boolean> {
|
||||
if (!nonInteractive) {
|
||||
Log.warn(`You are about to remove the Apple device listed above from your Expo account.`);
|
||||
Log.newLine();
|
||||
|
||||
const confirmed = await toggleConfirmAsync({
|
||||
message: 'Are you sure you wish to proceed?',
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
const removalSpinner = ora(`Removing Apple devices on Expo`).start();
|
||||
try {
|
||||
for (const chosenDevice of chosenDevices) {
|
||||
await AppleDeviceMutation.deleteAppleDeviceAsync(chosenDevice.id);
|
||||
}
|
||||
removalSpinner.succeed('Removed Apple devices from Expo');
|
||||
} catch (err) {
|
||||
removalSpinner.fail();
|
||||
throw err;
|
||||
}
|
||||
return await toggleConfirmAsync({
|
||||
message: 'Are you sure you wish to proceed?',
|
||||
});
|
||||
}
|
||||
|
||||
return confirmed;
|
||||
return true;
|
||||
}
|
||||
|
||||
logChosenDevices(
|
||||
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[],
|
||||
async removeDeviceFromExpoAsync(
|
||||
chosenDevice: AppleDevice | AppleDeviceQueryResult
|
||||
): Promise<void> {
|
||||
const removalSpinner = ora(`Removing Apple device on Expo`).start();
|
||||
try {
|
||||
await AppleDeviceMutation.deleteAppleDeviceAsync(chosenDevice.id);
|
||||
removalSpinner.succeed('Removed Apple device from Expo');
|
||||
} catch (err) {
|
||||
removalSpinner.fail();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
logChosenDevice(
|
||||
device: AppleDevice | AppleDeviceQueryResult,
|
||||
appleTeamName: Maybe<string> | undefined,
|
||||
appleTeamIdentifier: string
|
||||
appleTeamIdentifier: string,
|
||||
{ json }: PaginatedQueryOptions
|
||||
): void {
|
||||
Log.addNewLineIfNone();
|
||||
chosenDevices.forEach(device => {
|
||||
if (json) {
|
||||
printJsonOnlyOutput(device);
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
formatDevice(device, {
|
||||
appleTeamName,
|
||||
@@ -154,98 +164,6 @@ export default class DeviceDelete extends EasCommand {
|
||||
})
|
||||
);
|
||||
Log.newLine();
|
||||
});
|
||||
}
|
||||
|
||||
async chooseDevicesToDeleteAsync(
|
||||
appleDevices: AppleDeviceQueryResult[],
|
||||
udids: string[]
|
||||
): Promise<(AppleDevice | AppleDeviceQueryResult)[]> {
|
||||
let chosenDevices: (AppleDeviceQueryResult | AppleDevice)[] = [];
|
||||
Log.newLine();
|
||||
if (udids) {
|
||||
udids.forEach(udid => {
|
||||
const foundDevice = appleDevices.find(device => device.identifier === udid);
|
||||
if (foundDevice) {
|
||||
chosenDevices.push(foundDevice);
|
||||
} else {
|
||||
Log.warn(`No device found with UDID ${udid}.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (chosenDevices.length === 0) {
|
||||
Log.addNewLineIfNone();
|
||||
chosenDevices = await chooseDevicesToDeleteAsync(appleDevices);
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
return chosenDevices;
|
||||
}
|
||||
|
||||
async getDevicesForTeamAsync(
|
||||
accountName: string,
|
||||
appleTeamIdentifier: string
|
||||
): Promise<AppleDevicesByTeamIdentifierQueryResult | undefined> {
|
||||
const devicesSpinner = ora().start('Fetching the list of devices for the team…');
|
||||
|
||||
try {
|
||||
const result = await AppleDeviceQuery.getAllForAppleTeamAsync(
|
||||
accountName,
|
||||
appleTeamIdentifier
|
||||
);
|
||||
|
||||
if (result?.appleDevices.length) {
|
||||
const { appleTeamName, appleDevices } = result;
|
||||
|
||||
devicesSpinner.succeed(
|
||||
`Found ${appleDevices.length} devices for team ${appleTeamName ?? appleTeamIdentifier}`
|
||||
);
|
||||
|
||||
return result;
|
||||
} else {
|
||||
devicesSpinner.fail(`Couldn't find any devices for the team ${appleTeamIdentifier}`);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
devicesSpinner.fail(`Something went wrong and we couldn't fetch the device list`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async askForAppleTeamAsync(accountName: string): Promise<string | undefined> {
|
||||
const teamSpinner = ora().start('Fetching the list of teams for the project…');
|
||||
|
||||
try {
|
||||
const teams = await AppleTeamQuery.getAllForAccountAsync(accountName);
|
||||
|
||||
if (teams.length > 0) {
|
||||
teamSpinner.succeed('Fetched the list of teams for the project');
|
||||
|
||||
if (teams.length === 1) {
|
||||
return teams[0].appleTeamIdentifier;
|
||||
}
|
||||
|
||||
const result = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'appleTeamIdentifier',
|
||||
message: 'What Apple Team would you like to list devices for?',
|
||||
choices: teams.map(team => ({
|
||||
title: team.appleTeamName
|
||||
? `${team.appleTeamName} (ID: ${team.appleTeamIdentifier})`
|
||||
: team.appleTeamIdentifier,
|
||||
value: team.appleTeamIdentifier,
|
||||
})),
|
||||
});
|
||||
|
||||
return result.appleTeamIdentifier;
|
||||
} else {
|
||||
teamSpinner.fail(`Couldn't find any teams for the account ${accountName}`);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
teamSpinner.fail(`Something went wrong and we couldn't fetch the list of teams`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +1,68 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { AppleDeviceQuery } from '../../credentials/ios/api/graphql/queries/AppleDeviceQuery';
|
||||
import { AppleTeamQuery } from '../../credentials/ios/api/graphql/queries/AppleTeamQuery';
|
||||
import formatDevice from '../../devices/utils/formatDevice';
|
||||
import Log from '../../log';
|
||||
import { Ora, ora } from '../../ora';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { EasPaginatedQueryFlags, getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import {
|
||||
listAndRenderAppleDevicesOnAppleTeamAsync,
|
||||
selectAppleTeamOnAccountAsync,
|
||||
} from '../../devices/queries';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
|
||||
export default class BuildList extends EasCommand {
|
||||
static override description = 'list all registered devices for your account';
|
||||
|
||||
static override flags = {
|
||||
'apple-team-id': Flags.string(),
|
||||
...EasPaginatedQueryFlags,
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
let appleTeamIdentifier = (await this.parse(BuildList)).flags['apple-team-id'];
|
||||
const { projectId } = await this.getContextAsync(BuildList, {
|
||||
nonInteractive: false,
|
||||
const { flags } = await this.parse(BuildList);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(BuildList, {
|
||||
nonInteractive: paginatedQueryOptions.nonInteractive,
|
||||
});
|
||||
let appleTeamIdentifier = flags['apple-team-id'];
|
||||
let appleTeamName;
|
||||
if (paginatedQueryOptions.json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const account = await getOwnerAccountForProjectIdAsync(projectId);
|
||||
|
||||
let spinner: Ora;
|
||||
|
||||
// if they don't provide a team id, fetch devices on their account
|
||||
if (!appleTeamIdentifier) {
|
||||
spinner = ora().start('Fetching the list of teams for the project…');
|
||||
|
||||
try {
|
||||
const teams = await AppleTeamQuery.getAllForAccountAsync(account.name);
|
||||
|
||||
if (teams.length > 0) {
|
||||
spinner.succeed();
|
||||
|
||||
if (teams.length === 1) {
|
||||
appleTeamIdentifier = teams[0].appleTeamIdentifier;
|
||||
} else {
|
||||
const result = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'appleTeamIdentifier',
|
||||
message: 'What Apple Team would you like to list devices for?',
|
||||
choices: teams.map(team => ({
|
||||
title: team.appleTeamName
|
||||
? `${team.appleTeamName} (ID: ${team.appleTeamIdentifier})`
|
||||
: team.appleTeamIdentifier,
|
||||
value: team.appleTeamIdentifier,
|
||||
})),
|
||||
});
|
||||
|
||||
appleTeamIdentifier = result.appleTeamIdentifier;
|
||||
}
|
||||
} else {
|
||||
spinner.fail(`Couldn't find any teams for the account ${account.name}`);
|
||||
}
|
||||
} catch (e) {
|
||||
spinner.fail(`Something went wrong and we couldn't fetch the list of teams`);
|
||||
throw e;
|
||||
}
|
||||
const selectedAppleTeam = await selectAppleTeamOnAccountAsync({
|
||||
accountName: account.name,
|
||||
paginatedQueryOptions: {
|
||||
...paginatedQueryOptions,
|
||||
offset: 0,
|
||||
limit: undefined,
|
||||
},
|
||||
selectionPromptTitle: 'What Apple team would you like to list devices for?',
|
||||
});
|
||||
appleTeamIdentifier = selectedAppleTeam.appleTeamIdentifier;
|
||||
appleTeamName = selectedAppleTeam.appleTeamName;
|
||||
}
|
||||
|
||||
assert(appleTeamIdentifier, 'No team identifier is specified');
|
||||
|
||||
spinner = ora().start('Fetching the list of devices for the team…');
|
||||
|
||||
try {
|
||||
const result = await AppleDeviceQuery.getAllForAppleTeamAsync(
|
||||
account.name,
|
||||
appleTeamIdentifier
|
||||
);
|
||||
|
||||
if (result?.appleDevices.length) {
|
||||
const { appleTeamName, appleDevices } = result;
|
||||
|
||||
spinner.succeed(
|
||||
`Found ${appleDevices.length} devices for team ${appleTeamName ?? appleTeamIdentifier}`
|
||||
);
|
||||
|
||||
const list = appleDevices
|
||||
.map(device =>
|
||||
formatDevice(device, { appleTeamName, appleTeamIdentifier: appleTeamIdentifier! })
|
||||
)
|
||||
.join(`\n\n${chalk.dim('———')}\n\n`);
|
||||
|
||||
Log.log(`\n${list}`);
|
||||
} else {
|
||||
spinner.fail(`Couldn't find any devices for the team ${appleTeamIdentifier}`);
|
||||
}
|
||||
} catch (e) {
|
||||
spinner.fail(`Something went wrong and we couldn't fetch the device list`);
|
||||
throw e;
|
||||
}
|
||||
await listAndRenderAppleDevicesOnAppleTeamAsync({
|
||||
accountName: account.name,
|
||||
appleTeam: {
|
||||
appleTeamIdentifier,
|
||||
appleTeamName,
|
||||
},
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { AppleDeviceQuery } from '../../credentials/ios/api/graphql/queries/AppleDeviceQuery';
|
||||
import formatDevice from '../../devices/utils/formatDevice';
|
||||
import Log from '../../log';
|
||||
@@ -11,7 +11,7 @@ export default class DeviceView extends EasCommand {
|
||||
static override args = [{ name: 'UDID' }];
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -30,7 +30,9 @@ If you are not sure what is the UDID of the device you are looking for, run:
|
||||
);
|
||||
throw new Error('Device UDID is missing');
|
||||
}
|
||||
const { projectId } = await this.getContextAsync(DeviceView, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(DeviceView, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
const account = await getOwnerAccountForProjectIdAsync(projectId);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import envinfo from 'envinfo';
|
||||
|
||||
import EasCommand, { EASCommandProjectDirContext } from '../commandUtils/EasCommand';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import Log from '../log';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
@@ -10,7 +10,7 @@ export default class Diagnostics extends EasCommand {
|
||||
static override description = 'display environment info';
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectDirContext,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import path from 'path';
|
||||
|
||||
import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import EasCommand, {
|
||||
EASCommandLoggedInContext,
|
||||
EASCommandProjectDirContext,
|
||||
EASCommandProjectIdContext,
|
||||
} from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { createMetadataContextAsync } from '../../metadata/context';
|
||||
@@ -27,21 +22,21 @@ export default class MetadataPull extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...EASCommandLoggedInContext,
|
||||
...EASCommandProjectDirContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
Log.warn('EAS Metadata is in beta and subject to breaking changes.');
|
||||
|
||||
const { flags } = await this.parse(MetadataPull);
|
||||
const { actor, projectId, projectDir } = await this.getContextAsync(MetadataPull, {
|
||||
const {
|
||||
actor,
|
||||
projectConfig: { exp, projectId, projectDir },
|
||||
} = await this.getContextAsync(MetadataPull, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
|
||||
// this command is interactive (all nonInteractive flags passed to utility functions are false)
|
||||
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
|
||||
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import EasCommand, {
|
||||
EASCommandLoggedInContext,
|
||||
EASCommandProjectDirContext,
|
||||
EASCommandProjectIdContext,
|
||||
} from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { createMetadataContextAsync } from '../../metadata/context';
|
||||
@@ -25,21 +20,21 @@ export default class MetadataPush extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...EASCommandLoggedInContext,
|
||||
...EASCommandProjectDirContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
Log.warn('EAS Metadata is in beta and subject to breaking changes.');
|
||||
|
||||
const { flags } = await this.parse(MetadataPush);
|
||||
const { actor, projectId, projectDir } = await this.getContextAsync(MetadataPush, {
|
||||
const {
|
||||
actor,
|
||||
projectConfig: { exp, projectId, projectDir },
|
||||
} = await this.getContextAsync(MetadataPush, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
|
||||
// this command is interactive (all nonInteractive flags passed to utility functions are false)
|
||||
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
|
||||
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
import openBrowserAsync from 'better-opn';
|
||||
|
||||
import { getProjectDashboardUrl } from '../build/utils/url';
|
||||
import EasCommand, {
|
||||
EASCommandProjectDirContext,
|
||||
EASCommandProjectIdContext,
|
||||
} from '../commandUtils/EasCommand';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { ora } from '../ora';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
|
||||
|
||||
export default class Open extends EasCommand {
|
||||
static override description = 'open the project page in a web browser';
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...EASCommandProjectDirContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
// this command is interactive by nature (only really run by humans in a terminal)
|
||||
const { projectId, projectDir } = await this.getContextAsync(Open, {
|
||||
const {
|
||||
projectConfig: { projectId, exp },
|
||||
} = await this.getContextAsync(Open, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
const exp = getExpoConfig(projectDir);
|
||||
|
||||
const account = await getOwnerAccountForProjectIdAsync(projectId);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { vol } from 'memfs';
|
||||
|
||||
import ActorContextField from '../../../commandUtils/context/ActorContextField';
|
||||
import ProjectDirContextField from '../../../commandUtils/context/ProjectDirContextField';
|
||||
import ProjectIdContextField from '../../../commandUtils/context/ProjectIdContextField';
|
||||
import { saveProjectIdToAppConfigAsync } from '../../../commandUtils/context/contextUtils/getProjectIdAsync';
|
||||
import { jester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { AppMutation } from '../../../graphql/mutations/AppMutation';
|
||||
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
@@ -22,7 +22,7 @@ jest.mock('../../../ora', () => ({
|
||||
}),
|
||||
}));
|
||||
jest.mock('../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
|
||||
jest.mock('../../../commandUtils/context/ProjectIdContextField');
|
||||
jest.mock('../../../commandUtils/context/contextUtils/getProjectIdAsync');
|
||||
|
||||
let originalProcessArgv: string[];
|
||||
|
||||
@@ -93,23 +93,20 @@ describe(ProjectInit.name, () => {
|
||||
describe('interactive', () => {
|
||||
it('is no-op if already configured for id', async () => {
|
||||
await new ProjectInit(['--id', '1234'], commandOptions).run();
|
||||
expect(ProjectIdContextField['saveProjectIdToAppConfigAsync']).not.toHaveBeenCalled();
|
||||
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prompts to overwrite when different', async () => {
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
await new ProjectInit(['--id', '12345'], commandOptions).run();
|
||||
expect(ProjectIdContextField['saveProjectIdToAppConfigAsync']).toHaveBeenCalledWith(
|
||||
'/test-project',
|
||||
'12345'
|
||||
);
|
||||
expect(saveProjectIdToAppConfigAsync).toHaveBeenCalledWith('/test-project', '12345');
|
||||
expect(confirmAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('aborts when prompt to overwrite is declined', async () => {
|
||||
jest.mocked(confirmAsync).mockResolvedValue(false);
|
||||
await new ProjectInit(['--id', '12345'], commandOptions).run();
|
||||
expect(ProjectIdContextField['saveProjectIdToAppConfigAsync']).not.toHaveBeenCalled();
|
||||
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
expect(confirmAsync).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -119,10 +116,7 @@ describe(ProjectInit.name, () => {
|
||||
await new ProjectInit(['--id', '12345', '--force'], {
|
||||
root: '/test-project',
|
||||
} as any).run();
|
||||
expect(ProjectIdContextField['saveProjectIdToAppConfigAsync']).toHaveBeenCalledWith(
|
||||
'/test-project',
|
||||
'12345'
|
||||
);
|
||||
expect(saveProjectIdToAppConfigAsync).toHaveBeenCalledWith('/test-project', '12345');
|
||||
expect(confirmAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -138,7 +132,7 @@ describe(ProjectInit.name, () => {
|
||||
'1234'
|
||||
)}. Use --force flag to overwrite.`
|
||||
);
|
||||
expect(ProjectIdContextField['saveProjectIdToAppConfigAsync']).not.toHaveBeenCalled();
|
||||
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -150,10 +144,7 @@ describe(ProjectInit.name, () => {
|
||||
|
||||
it('configures', async () => {
|
||||
await new ProjectInit(['--id', '1234'], commandOptions).run();
|
||||
expect(ProjectIdContextField['saveProjectIdToAppConfigAsync']).toHaveBeenCalledWith(
|
||||
'/test-project',
|
||||
'1234'
|
||||
);
|
||||
expect(saveProjectIdToAppConfigAsync).toHaveBeenCalledWith('/test-project', '1234');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -166,7 +157,7 @@ describe(ProjectInit.name, () => {
|
||||
|
||||
it('does not configure', async () => {
|
||||
await new ProjectInit([], commandOptions).run();
|
||||
expect(ProjectIdContextField['saveProjectIdToAppConfigAsync']).not.toHaveBeenCalled();
|
||||
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -184,10 +175,7 @@ describe(ProjectInit.name, () => {
|
||||
it('prompts for confirmation to link', async () => {
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
await new ProjectInit([], commandOptions).run();
|
||||
expect(ProjectIdContextField['saveProjectIdToAppConfigAsync']).toHaveBeenCalledWith(
|
||||
'/test-project',
|
||||
'123456'
|
||||
);
|
||||
expect(saveProjectIdToAppConfigAsync).toHaveBeenCalledWith('/test-project', '123456');
|
||||
expect(confirmAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -196,7 +184,7 @@ describe(ProjectInit.name, () => {
|
||||
await expect(new ProjectInit([], commandOptions).run()).rejects.toThrow(
|
||||
'Project ID configuration canceled. Re-run the command to select a different account/project.'
|
||||
);
|
||||
expect(ProjectIdContextField['saveProjectIdToAppConfigAsync']).not.toHaveBeenCalled();
|
||||
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
expect(confirmAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -206,10 +194,7 @@ describe(ProjectInit.name, () => {
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
jest.mocked(AppMutation.createAppAsync).mockResolvedValue('0129');
|
||||
await new ProjectInit([], commandOptions).run();
|
||||
expect(ProjectIdContextField['saveProjectIdToAppConfigAsync']).toHaveBeenCalledWith(
|
||||
'/test-project',
|
||||
'0129'
|
||||
);
|
||||
expect(saveProjectIdToAppConfigAsync).toHaveBeenCalledWith('/test-project', '0129');
|
||||
expect(confirmAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -218,7 +203,7 @@ describe(ProjectInit.name, () => {
|
||||
await expect(new ProjectInit([], commandOptions).run()).rejects.toThrowError(
|
||||
'Project ID configuration canceled for @jester/testing-123.'
|
||||
);
|
||||
expect(ProjectIdContextField['saveProjectIdToAppConfigAsync']).not.toHaveBeenCalled();
|
||||
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
expect(confirmAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -244,7 +229,7 @@ describe(ProjectInit.name, () => {
|
||||
await expect(new ProjectInit([], commandOptions).run()).rejects.toThrowError(
|
||||
`You don't have permission to create a new project on the other account and no matching project already exists on the account.`
|
||||
);
|
||||
expect(ProjectIdContextField['saveProjectIdToAppConfigAsync']).not.toHaveBeenCalled();
|
||||
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import { AppInfoQuery, AppInfoQueryVariables } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
@@ -33,11 +33,13 @@ export default class ProjectInfo extends EasCommand {
|
||||
static override description = 'information about the current project';
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { projectId } = await this.getContextAsync(ProjectInfo, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(ProjectInfo, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -4,11 +4,8 @@ import nullthrows from 'nullthrows';
|
||||
import terminalLink from 'terminal-link';
|
||||
|
||||
import { getProjectDashboardUrl } from '../../build/utils/url';
|
||||
import EasCommand, {
|
||||
EASCommandLoggedInContext,
|
||||
EASCommandProjectDirContext,
|
||||
} from '../../commandUtils/EasCommand';
|
||||
import ProjectIdContextField from '../../commandUtils/context/ProjectIdContextField';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { saveProjectIdToAppConfigAsync } from '../../commandUtils/context/contextUtils/getProjectIdAsync';
|
||||
import { AppPrivacy, Role } from '../../graphql/generated';
|
||||
import { AppMutation } from '../../graphql/mutations/AppMutation';
|
||||
import Log from '../../log';
|
||||
@@ -44,15 +41,15 @@ export default class ProjectInit extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandLoggedInContext,
|
||||
...EASCommandProjectDirContext,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
};
|
||||
|
||||
private static async saveProjectIdAndLogSuccessAsync(
|
||||
projectDir: string,
|
||||
projectId: string
|
||||
): Promise<void> {
|
||||
await ProjectIdContextField['saveProjectIdToAppConfigAsync'](projectDir, projectId);
|
||||
await saveProjectIdToAppConfigAsync(projectDir, projectId);
|
||||
Log.withTick(`Project successfully linked (ID: ${chalk.bold(projectId)}) (modified app.json)`);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
|
||||
import {
|
||||
@@ -50,7 +50,7 @@ export default class EnvironmentSecretCreate extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -64,7 +64,9 @@ export default class EnvironmentSecretCreate extends EasCommand {
|
||||
'non-interactive': nonInteractive,
|
||||
},
|
||||
} = await this.parse(EnvironmentSecretCreate);
|
||||
const { projectId } = await this.getContextAsync(EnvironmentSecretCreate, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(EnvironmentSecretCreate, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
|
||||
import {
|
||||
@@ -22,14 +22,16 @@ export default class EnvironmentSecretDelete extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
let {
|
||||
flags: { id, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(EnvironmentSecretDelete);
|
||||
const { projectId } = await this.getContextAsync(EnvironmentSecretDelete, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(EnvironmentSecretDelete, {
|
||||
nonInteractive,
|
||||
});
|
||||
let secret: EnvironmentSecretWithScope | undefined;
|
||||
|
||||
@@ -2,7 +2,7 @@ import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import dateFormat from 'dateformat';
|
||||
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EnvironmentSecretsQuery } from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import { EnvironmentSecretTypeToSecretType } from '../../graphql/types/EnvironmentSecret';
|
||||
import Log from '../../log';
|
||||
@@ -11,11 +11,13 @@ export default class EnvironmentSecretList extends EasCommand {
|
||||
static override description = 'list environment secrets available for your current app';
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { projectId } = await this.getContextAsync(EnvironmentSecretList, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(EnvironmentSecretList, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -2,11 +2,7 @@ import { EasJsonAccessor } from '@expo/eas-json';
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand, {
|
||||
EASCommandLoggedInContext,
|
||||
EASCommandProjectDirContext,
|
||||
EASCommandProjectIdContext,
|
||||
} from '../commandUtils/EasCommand';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { StatuspageServiceName, SubmissionFragment } from '../graphql/generated';
|
||||
import { toAppPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log from '../log';
|
||||
@@ -94,14 +90,17 @@ export default class Submit extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandLoggedInContext,
|
||||
...EASCommandProjectIdContext,
|
||||
...EASCommandProjectDirContext,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags: rawFlags } = await this.parse(Submit);
|
||||
const { actor, projectId, projectDir } = await this.getContextAsync(Submit, {
|
||||
const {
|
||||
actor,
|
||||
projectConfig: { exp, projectId, projectDir },
|
||||
} = await this.getContextAsync(Submit, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
@@ -121,14 +120,16 @@ export default class Submit extends EasCommand {
|
||||
|
||||
const submissions: SubmissionFragment[] = [];
|
||||
for (const submissionProfile of submissionProfiles) {
|
||||
// this command doesn't make use of env when getting the project config
|
||||
const ctx = await createSubmissionContextAsync({
|
||||
platform: submissionProfile.platform,
|
||||
projectDir,
|
||||
projectId,
|
||||
profile: submissionProfile.profile,
|
||||
archiveFlags: flagsWithPlatform.archiveFlags,
|
||||
nonInteractive: flagsWithPlatform.nonInteractive,
|
||||
actor,
|
||||
exp,
|
||||
projectId,
|
||||
});
|
||||
|
||||
if (submissionProfiles.length > 1) {
|
||||
|
||||
@@ -5,14 +5,10 @@ import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { getEASUpdateURL } from '../../api';
|
||||
import EasCommand, {
|
||||
EASCommandProjectDirContext,
|
||||
EASCommandProjectIdContext,
|
||||
} from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { AppPlatform } from '../../graphql/generated';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { RequestedPlatform, appPlatformDisplayNames } from '../../platform';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import {
|
||||
installExpoUpdatesAsync,
|
||||
isExpoUpdatesInstalledOrAvailable,
|
||||
@@ -37,8 +33,7 @@ export default class UpdateConfigure extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...EASCommandProjectDirContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -46,11 +41,12 @@ export default class UpdateConfigure extends EasCommand {
|
||||
'💡 The following process will configure your project to run EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
|
||||
);
|
||||
const { flags } = await this.parse(UpdateConfigure);
|
||||
const { projectId, projectDir } = await this.getContextAsync(UpdateConfigure, {
|
||||
const {
|
||||
projectConfig: { projectId, exp, projectDir },
|
||||
} = await this.getContextAsync(UpdateConfigure, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
const platform = flags.platform as RequestedPlatform;
|
||||
const exp = getExpoConfig(projectDir);
|
||||
|
||||
if (!isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion)) {
|
||||
await installExpoUpdatesAsync(projectDir);
|
||||
|
||||
@@ -10,10 +10,7 @@ import { getEASUpdateURL } from '../../api';
|
||||
import { selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import { BranchNotFoundError, getDefaultBranchNameAsync } from '../../branch/utils';
|
||||
import { getUpdateGroupUrl } from '../../build/utils/url';
|
||||
import EasCommand, {
|
||||
EASCommandProjectDirContext,
|
||||
EASCommandProjectIdContext,
|
||||
} from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import fetch from '../../fetch';
|
||||
@@ -30,7 +27,6 @@ import { BranchQuery } from '../../graphql/queries/BranchQuery';
|
||||
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
|
||||
import Log, { learnMore, link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import {
|
||||
getOwnerAccountForProjectIdAsync,
|
||||
installExpoUpdatesAsync,
|
||||
@@ -171,8 +167,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...EASCommandProjectDirContext,
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -191,7 +186,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
'non-interactive': nonInteractive,
|
||||
json: jsonFlag,
|
||||
} = flags;
|
||||
const { projectId, projectDir } = await this.getContextAsync(UpdatePublish, {
|
||||
const { getDynamicProjectConfigAsync } = await this.getContextAsync(UpdatePublish, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
@@ -203,11 +198,11 @@ export default class UpdatePublish extends EasCommand {
|
||||
// If a group was specified, that means we are republishing it.
|
||||
republish = republish || !!group;
|
||||
|
||||
const exp = getExpoConfig(projectDir, {
|
||||
const { exp, projectId, projectDir } = await getDynamicProjectConfigAsync({
|
||||
isPublicConfig: true,
|
||||
});
|
||||
|
||||
const expPrivate = getExpoConfig(projectDir, {
|
||||
const { exp: expPrivate } = await getDynamicProjectConfigAsync({
|
||||
isPublicConfig: false,
|
||||
});
|
||||
|
||||
@@ -506,7 +501,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
|
||||
publishSpinner.succeed('Published!');
|
||||
} catch (e) {
|
||||
publishSpinner.fail('Failed to published updates');
|
||||
publishSpinner.fail('Failed to publish updates');
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import { selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasPaginatedQueryFlags,
|
||||
@@ -33,13 +33,15 @@ export default class UpdateList extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(UpdateList);
|
||||
const { branch: branchFlag, all, json: jsonFlag, 'non-interactive': nonInteractive } = flags;
|
||||
const { projectId } = await this.getContextAsync(UpdateList, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(UpdateList, {
|
||||
nonInteractive,
|
||||
});
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { WebhookType } from '../../graphql/generated';
|
||||
import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
|
||||
@@ -26,12 +26,14 @@ export default class WebhookCreate extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(WebhookCreate);
|
||||
const { projectId } = await this.getContextAsync(WebhookCreate, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(WebhookCreate, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
});
|
||||
const webhookInputParams = await prepareInputParamsAsync(flags);
|
||||
|
||||
@@ -2,7 +2,7 @@ import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { WebhookFragment } from '../../graphql/generated';
|
||||
import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
|
||||
@@ -28,7 +28,7 @@ export default class WebhookDelete extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -36,7 +36,9 @@ export default class WebhookDelete extends EasCommand {
|
||||
args: { ID: webhookId },
|
||||
flags: { 'non-interactive': nonInteractive },
|
||||
} = await this.parse(WebhookDelete);
|
||||
const { projectId } = await this.getContextAsync(WebhookDelete, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(WebhookDelete, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { WebhookType } from '../../graphql/generated';
|
||||
import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
|
||||
import Log from '../../log';
|
||||
@@ -20,14 +20,16 @@ export default class WebhookList extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
flags: { event },
|
||||
} = await this.parse(WebhookList);
|
||||
const { projectId } = await this.getContextAsync(WebhookList, {
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(WebhookList, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ export async function getAppLookupParamsFromContextAsync(
|
||||
ctx: CredentialsContext,
|
||||
gradleContext?: GradleBuildContext
|
||||
): Promise<AppLookupParams> {
|
||||
ctx.ensureProjectContext();
|
||||
const projectName = ctx.exp.slug;
|
||||
const projectId = ctx.projectId;
|
||||
const account = await getOwnerAccountForProjectIdAsync(projectId);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { EasJson } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../log';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { confirmAsync } from '../prompts';
|
||||
import { Actor } from '../user/User';
|
||||
import * as AndroidGraphqlClient from './android/api/GraphqlClient';
|
||||
@@ -27,16 +28,19 @@ export class CredentialsContext {
|
||||
|
||||
private shouldAskAuthenticateAppStore: boolean = true;
|
||||
|
||||
private projectInfo: CredentialsContextProjectInfo;
|
||||
private projectInfo: CredentialsContextProjectInfo | null;
|
||||
|
||||
constructor(options: {
|
||||
projectInfo: CredentialsContextProjectInfo;
|
||||
easJsonCliConfig?: EasJson['cli'];
|
||||
nonInteractive: boolean;
|
||||
projectDir: string;
|
||||
user: Actor;
|
||||
env?: Env;
|
||||
}) {
|
||||
constructor(
|
||||
private options: {
|
||||
// if null, this implies not running in a project context
|
||||
projectInfo: CredentialsContextProjectInfo | null;
|
||||
easJsonCliConfig?: EasJson['cli'];
|
||||
nonInteractive: boolean;
|
||||
projectDir: string;
|
||||
user: Actor;
|
||||
env?: Env;
|
||||
}
|
||||
) {
|
||||
this.easJsonCliConfig = options.easJsonCliConfig;
|
||||
this.projectDir = options.projectDir;
|
||||
this.user = options.user;
|
||||
@@ -44,14 +48,40 @@ export class CredentialsContext {
|
||||
this.projectInfo = options.projectInfo;
|
||||
}
|
||||
|
||||
static getExpoConfigInProject(
|
||||
projectDir: string,
|
||||
{ env }: { env?: Env } = {}
|
||||
): ExpoConfig | null {
|
||||
try {
|
||||
return getExpoConfig(projectDir, { env });
|
||||
} catch {
|
||||
// ignore error, context might be created outside of expo project
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
get hasProjectContext(): boolean {
|
||||
return !!this.projectInfo;
|
||||
}
|
||||
|
||||
get exp(): ExpoConfig {
|
||||
this.ensureProjectContext();
|
||||
return this.projectInfo!.exp;
|
||||
}
|
||||
|
||||
get projectId(): string {
|
||||
this.ensureProjectContext();
|
||||
return this.projectInfo!.projectId;
|
||||
}
|
||||
|
||||
public ensureProjectContext(): void {
|
||||
if (this.hasProjectContext) {
|
||||
return;
|
||||
}
|
||||
// trigger getConfig error
|
||||
getExpoConfig(this.options.projectDir);
|
||||
}
|
||||
|
||||
async bestEffortAppStoreAuthenticateAsync(): Promise<void> {
|
||||
if (this.appStore.authCtx || !this.shouldAskAuthenticateAppStore) {
|
||||
// skip prompts if already have apple ctx or already asked about it
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { AppleTeamFragment } from '../../../graphql/generated';
|
||||
|
||||
export function formatAppleTeam({
|
||||
appleTeamIdentifier,
|
||||
appleTeamName,
|
||||
}: Pick<AppleTeamFragment, 'appleTeamIdentifier' | 'appleTeamName'>): string {
|
||||
return `Team ID: ${appleTeamIdentifier}${appleTeamName ? `, Team name: ${appleTeamName}` : ''}`;
|
||||
}
|
||||
@@ -14,7 +14,3 @@ export async function resolveAppleTeamIfAuthenticatedAsync(
|
||||
appleTeamName: ctx.appStore.authCtx.team.name,
|
||||
});
|
||||
}
|
||||
|
||||
export function formatAppleTeam({ appleTeamIdentifier, appleTeamName }: AppleTeamFragment): string {
|
||||
return `Team ID: ${appleTeamIdentifier}${appleTeamName ? `, Team name: ${appleTeamName}` : ''}`;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
ascApiKeyIssuerIdSchema,
|
||||
} from '../credentials';
|
||||
import { isAscApiKeyValidAndTrackedAsync } from '../validators/validateAscApiKey';
|
||||
import { formatAppleTeam } from './AppleTeamUtils';
|
||||
import { formatAppleTeam } from './AppleTeamFormatting';
|
||||
|
||||
export enum AppStoreApiKeyPurpose {
|
||||
SUBMISSION_SERVICE = 'EAS Submit',
|
||||
|
||||
@@ -82,6 +82,7 @@ export async function assignBuildCredentialsAsync(
|
||||
}
|
||||
|
||||
export async function getAppFromContextAsync(ctx: CredentialsContext): Promise<App> {
|
||||
ctx.ensureProjectContext();
|
||||
const projectName = ctx.exp.slug;
|
||||
const projectId = ctx.projectId;
|
||||
const account = await getOwnerAccountForProjectIdAsync(projectId);
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function chooseDevicesAsync(
|
||||
const { devices } = await promptAsync({
|
||||
type: 'multiselect',
|
||||
name: 'devices',
|
||||
message: 'Select devices for the adhoc build:',
|
||||
message: 'Select devices for the ad hoc build:',
|
||||
hint: '- Space to select. Return to submit',
|
||||
choices: allDevices.map(device => ({
|
||||
value: device,
|
||||
@@ -26,26 +26,11 @@ export async function chooseDevicesAsync(
|
||||
return devices;
|
||||
}
|
||||
|
||||
export async function chooseDevicesToDeleteAsync(
|
||||
allDevices: AppleDeviceFragment[]
|
||||
): Promise<AppleDevice[]> {
|
||||
const { devices } = await promptAsync({
|
||||
type: 'multiselect',
|
||||
name: 'devices',
|
||||
message: 'Which devices do you want to remove?',
|
||||
choices: allDevices.map(device => ({
|
||||
value: device,
|
||||
title: formatDeviceLabel(device),
|
||||
selected: false,
|
||||
})),
|
||||
instructions: false,
|
||||
});
|
||||
return devices;
|
||||
}
|
||||
|
||||
export function formatDeviceLabel(device: AppleDeviceFragment): string {
|
||||
const deviceDetails = formatDeviceDetails(device);
|
||||
return `${device.name ?? device.identifier}${deviceDetails !== '' ? ` ${deviceDetails}` : ''}`;
|
||||
return `${device.identifier}${deviceDetails !== '' ? ` ${deviceDetails}` : ''}${
|
||||
device.name ? ` (${device.name})` : ''
|
||||
}`;
|
||||
}
|
||||
|
||||
function formatDeviceDetails(device: AppleDeviceFragment): string {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { filterRevokedDistributionCertsFromEasServers } from '../appstore/Creden
|
||||
import { AppleTooManyCertsError } from '../appstore/distributionCertificate';
|
||||
import { distributionCertificateSchema } from '../credentials';
|
||||
import { validateDistributionCertificateAsync } from '../validators/validateDistributionCertificate';
|
||||
import { formatAppleTeam } from './AppleTeamUtils';
|
||||
import { formatAppleTeam } from './AppleTeamFormatting';
|
||||
|
||||
export function formatDistributionCertificate(
|
||||
distributionCertificate: AppleDistributionCertificateFragment,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { filterRevokedAndUntrackedPushKeysFromEasServersAsync } from '../appstor
|
||||
import { APPLE_KEYS_TOO_MANY_GENERATED_ERROR } from '../appstore/pushKey';
|
||||
import { pushKeySchema } from '../credentials';
|
||||
import { isPushKeyValidAndTrackedAsync } from '../validators/validatePushKey';
|
||||
import { formatAppleTeam } from './AppleTeamUtils';
|
||||
import { formatAppleTeam } from './AppleTeamFormatting';
|
||||
|
||||
export async function provideOrGeneratePushKeyAsync(ctx: CredentialsContext): Promise<PushKey> {
|
||||
if (!ctx.nonInteractive) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from 'assert';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { DeviceNotFoundError } from '../../../../../devices/utils/errors';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import {
|
||||
AppleDevice,
|
||||
@@ -9,10 +10,12 @@ import {
|
||||
AppleDevicesByAppleTeamQuery,
|
||||
AppleDevicesByIdentifierQuery,
|
||||
AppleDevicesByTeamIdentifierQuery,
|
||||
AppleDevicesByTeamIdentifierQueryVariables,
|
||||
AppleTeamFragment,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AppleDeviceFragmentNode } from '../../../../../graphql/types/credentials/AppleDevice';
|
||||
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
|
||||
import { formatAppleTeam } from '../../../actions/AppleTeamFormatting';
|
||||
|
||||
export type AppleDeviceFragmentWithAppleTeam = AppleDeviceFragment & {
|
||||
appleTeam: AppleTeamFragment;
|
||||
@@ -80,10 +83,12 @@ export const AppleDeviceQuery = {
|
||||
return appleDevices;
|
||||
},
|
||||
|
||||
async getAllForAppleTeamAsync(
|
||||
accountName: string,
|
||||
appleTeamIdentifier: string
|
||||
): Promise<AppleDevicesByTeamIdentifierQueryResult | null> {
|
||||
async getAllForAppleTeamAsync({
|
||||
accountName,
|
||||
appleTeamIdentifier,
|
||||
offset,
|
||||
limit,
|
||||
}: AppleDevicesByTeamIdentifierQueryVariables): Promise<AppleDeviceFragment[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<AppleDevicesByTeamIdentifierQuery>(
|
||||
@@ -91,6 +96,8 @@ export const AppleDeviceQuery = {
|
||||
query AppleDevicesByTeamIdentifier(
|
||||
$accountName: String!
|
||||
$appleTeamIdentifier: String!
|
||||
$offset: Int
|
||||
$limit: Int
|
||||
) {
|
||||
account {
|
||||
byName(accountName: $accountName) {
|
||||
@@ -99,33 +106,38 @@ export const AppleDeviceQuery = {
|
||||
id
|
||||
appleTeamIdentifier
|
||||
appleTeamName
|
||||
appleDevices {
|
||||
appleDevices(offset: $offset, limit: $limit) {
|
||||
id
|
||||
identifier
|
||||
name
|
||||
deviceClass
|
||||
enabled
|
||||
model
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ accountName, appleTeamIdentifier },
|
||||
{ accountName, appleTeamIdentifier, offset, limit },
|
||||
{
|
||||
additionalTypenames: ['AppleDevice', 'AppleTeam'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.account.byName.appleTeams[0];
|
||||
const [appleTeam] = data.account.byName.appleTeams;
|
||||
const { appleDevices } = appleTeam;
|
||||
if (!appleDevices) {
|
||||
throw new Error(`Could not find devices on Apple team -- ${formatAppleTeam(appleTeam)}`);
|
||||
}
|
||||
return appleDevices;
|
||||
},
|
||||
|
||||
async getByDeviceIdentifierAsync(
|
||||
accountName: string,
|
||||
identifier: string
|
||||
): Promise<AppleDevicesByIdentifierQueryResult | null> {
|
||||
): Promise<AppleDevicesByIdentifierQueryResult> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<AppleDevicesByIdentifierQuery>(
|
||||
@@ -136,6 +148,7 @@ export const AppleDeviceQuery = {
|
||||
id
|
||||
appleDevices(identifier: $identifier) {
|
||||
id
|
||||
model
|
||||
identifier
|
||||
name
|
||||
deviceClass
|
||||
@@ -158,6 +171,12 @@ export const AppleDeviceQuery = {
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.account.byName.appleDevices[0] ?? null;
|
||||
const device = data.account.byName.appleDevices[0];
|
||||
if (!device) {
|
||||
throw new DeviceNotFoundError(
|
||||
`Device with id ${identifier} was not found on account ${accountName}.`
|
||||
);
|
||||
}
|
||||
return device;
|
||||
},
|
||||
};
|
||||
@@ -6,29 +6,34 @@ import {
|
||||
AppleTeamByIdentifierQuery,
|
||||
AppleTeamFragment,
|
||||
AppleTeamsByAccountNameQuery,
|
||||
AppleTeamsByAccountNameQueryVariables,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
|
||||
|
||||
export const AppleTeamQuery = {
|
||||
async getAllForAccountAsync(accountName: string): Promise<AppleTeamFragment[]> {
|
||||
async getAllForAccountAsync({
|
||||
accountName,
|
||||
offset,
|
||||
limit,
|
||||
}: AppleTeamsByAccountNameQueryVariables): Promise<AppleTeamFragment[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<AppleTeamsByAccountNameQuery>(
|
||||
gql`
|
||||
query AppleTeamsByAccountName($accountName: String!) {
|
||||
query AppleTeamsByAccountName($accountName: String!, $offset: Int, $limit: Int) {
|
||||
account {
|
||||
byName(accountName: $accountName) {
|
||||
id
|
||||
appleTeams {
|
||||
appleTeams(offset: $offset, limit: $limit) {
|
||||
id
|
||||
appleTeamName
|
||||
appleTeamIdentifier
|
||||
...AppleTeamFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(AppleTeamFragmentNode)}
|
||||
`,
|
||||
{ accountName },
|
||||
{ accountName, offset, limit },
|
||||
{
|
||||
additionalTypenames: ['AppleTeam'],
|
||||
}
|
||||
@@ -36,9 +41,8 @@ export const AppleTeamQuery = {
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.account.byName.appleTeams;
|
||||
return data.account.byName.appleTeams ?? [];
|
||||
},
|
||||
|
||||
async getByAppleTeamIdentifierAsync(
|
||||
accountId: string,
|
||||
appleTeamIdentifier: string
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CredentialsContext, CredentialsContextProjectInfo } from '../context';
|
||||
|
||||
export interface Action<T = void> {
|
||||
actor: Actor;
|
||||
projectInfo: CredentialsContextProjectInfo;
|
||||
projectInfo: CredentialsContextProjectInfo | null;
|
||||
runAsync(ctx: CredentialsContext): Promise<T>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CredentialsContext } from '../context';
|
||||
import { ActionInfo, IosActionType, Scope } from './Actions';
|
||||
|
||||
export const highLevelActions: ActionInfo[] = [
|
||||
@@ -46,7 +47,7 @@ export const credentialsJsonActions: ActionInfo[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export function getPushKeyActions(): ActionInfo[] {
|
||||
export function getPushKeyActions(ctx: CredentialsContext): ActionInfo[] {
|
||||
return [
|
||||
{
|
||||
value: IosActionType.SetUpPushKey,
|
||||
@@ -56,7 +57,7 @@ export function getPushKeyActions(): ActionInfo[] {
|
||||
{
|
||||
value: IosActionType.CreatePushKey,
|
||||
title: 'Add a new push key',
|
||||
scope: Scope.Project,
|
||||
scope: ctx.hasProjectContext ? Scope.Project : Scope.Account,
|
||||
},
|
||||
{
|
||||
value: IosActionType.UseExistingPushKey,
|
||||
@@ -76,7 +77,7 @@ export function getPushKeyActions(): ActionInfo[] {
|
||||
];
|
||||
}
|
||||
|
||||
export function getAscApiKeyActions(): ActionInfo[] {
|
||||
export function getAscApiKeyActions(ctx: CredentialsContext): ActionInfo[] {
|
||||
return [
|
||||
{
|
||||
value: IosActionType.SetUpAscApiKeyForSubmissions,
|
||||
@@ -91,7 +92,7 @@ export function getAscApiKeyActions(): ActionInfo[] {
|
||||
{
|
||||
value: IosActionType.CreateAscApiKeyForSubmissions,
|
||||
title: 'Add a new API Key For EAS Submit',
|
||||
scope: Scope.Project,
|
||||
scope: ctx.hasProjectContext ? Scope.Project : Scope.Account,
|
||||
},
|
||||
{
|
||||
value: IosActionType.RemoveAscApiKey,
|
||||
@@ -106,7 +107,7 @@ export function getAscApiKeyActions(): ActionInfo[] {
|
||||
];
|
||||
}
|
||||
|
||||
export function getBuildCredentialsActions(): ActionInfo[] {
|
||||
export function getBuildCredentialsActions(ctx: CredentialsContext): ActionInfo[] {
|
||||
return [
|
||||
{
|
||||
// This command will be triggered during build to ensure all credentials are ready
|
||||
@@ -123,7 +124,7 @@ export function getBuildCredentialsActions(): ActionInfo[] {
|
||||
{
|
||||
value: IosActionType.CreateDistributionCertificate,
|
||||
title: `Distribution Certificate: Add a new one to your account`,
|
||||
scope: Scope.Project,
|
||||
scope: ctx.hasProjectContext ? Scope.Project : Scope.Account,
|
||||
},
|
||||
{
|
||||
value: IosActionType.RemoveDistributionCertificate,
|
||||
|
||||
@@ -60,27 +60,34 @@ export class ManageAndroid {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
assert(buildProfile, 'buildProfile must be defined in a project context');
|
||||
const gradleContext = await this.createProjectContextAsync(ctx, buildProfile);
|
||||
let gradleContext;
|
||||
if (ctx.hasProjectContext) {
|
||||
assert(buildProfile, 'buildProfile must be defined in a project context');
|
||||
gradleContext = await this.createProjectContextAsync(ctx, buildProfile);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx, gradleContext);
|
||||
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(
|
||||
appLookupParams
|
||||
);
|
||||
if (!appCredentials) {
|
||||
displayEmptyAndroidCredentials(appLookupParams);
|
||||
} else {
|
||||
displayAndroidAppCredentials({ appLookupParams, appCredentials });
|
||||
}
|
||||
if (ctx.hasProjectContext) {
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx, gradleContext);
|
||||
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(
|
||||
appLookupParams
|
||||
);
|
||||
if (!appCredentials) {
|
||||
displayEmptyAndroidCredentials(appLookupParams);
|
||||
} else {
|
||||
displayAndroidAppCredentials({ appLookupParams, appCredentials });
|
||||
}
|
||||
|
||||
// copy legacy credentials if user is new to EAS and has legacy credentials
|
||||
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, appLookupParams);
|
||||
if (canCopyLegacyCredentials) {
|
||||
await promptUserAndCopyLegacyCredentialsAsync(ctx, appLookupParams);
|
||||
// copy legacy credentials if user is new to EAS and has legacy credentials
|
||||
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(
|
||||
ctx,
|
||||
appLookupParams
|
||||
);
|
||||
if (canCopyLegacyCredentials) {
|
||||
await promptUserAndCopyLegacyCredentialsAsync(ctx, appLookupParams);
|
||||
}
|
||||
}
|
||||
|
||||
const { action: chosenAction } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'action',
|
||||
@@ -131,6 +138,7 @@ export class ManageAndroid {
|
||||
ctx: CredentialsContext,
|
||||
buildProfile: BuildProfile<Platform.ANDROID>
|
||||
): Promise<GradleBuildContext | undefined> {
|
||||
assert(ctx.hasProjectContext, 'createProjectContextAsync: must have project context.');
|
||||
return await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
|
||||
}
|
||||
|
||||
@@ -139,6 +147,10 @@ export class ManageAndroid {
|
||||
action: AndroidActionType,
|
||||
gradleContext?: GradleBuildContext
|
||||
): Promise<void> {
|
||||
assert(
|
||||
ctx.hasProjectContext,
|
||||
'You must be in your project directory in order to perform this action'
|
||||
);
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx, gradleContext);
|
||||
if (action === AndroidActionType.CreateKeystore) {
|
||||
const selectBuildCredentialsResult = await new SelectAndroidBuildCredentials(
|
||||
|
||||
@@ -14,6 +14,7 @@ import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
|
||||
import { resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
|
||||
import { ensureActorHasPrimaryAccount } from '../../user/actions';
|
||||
import { CredentialsContext } from '../context';
|
||||
import {
|
||||
AppStoreApiKeyPurpose,
|
||||
@@ -67,31 +68,41 @@ export class ManageIos {
|
||||
env: buildProfile?.env,
|
||||
nonInteractive: false,
|
||||
});
|
||||
const buildCredentialsActions = getBuildCredentialsActions();
|
||||
const pushKeyActions = getPushKeyActions();
|
||||
const ascApiKeyActions = getAscApiKeyActions();
|
||||
const buildCredentialsActions = getBuildCredentialsActions(ctx);
|
||||
const pushKeyActions = getPushKeyActions(ctx);
|
||||
const ascApiKeyActions = getAscApiKeyActions(ctx);
|
||||
|
||||
await ctx.bestEffortAppStoreAuthenticateAsync();
|
||||
|
||||
const account = await getOwnerAccountForProjectIdAsync(
|
||||
this.callingAction.projectInfo.projectId
|
||||
);
|
||||
const getAccountForProjectAsync = async (projectId: string): Promise<AccountFragment> => {
|
||||
return await getOwnerAccountForProjectIdAsync(projectId);
|
||||
};
|
||||
|
||||
assert(buildProfile, 'buildProfile must be defined in project context');
|
||||
const projectContext = await this.createProjectContextAsync(ctx, account, buildProfile);
|
||||
const app = projectContext.app;
|
||||
const targets = projectContext.targets;
|
||||
const account = ctx.hasProjectContext
|
||||
? await getAccountForProjectAsync(ctx.projectId)
|
||||
: ensureActorHasPrimaryAccount(ctx.user);
|
||||
|
||||
let app = null;
|
||||
let targets = null;
|
||||
if (ctx.hasProjectContext) {
|
||||
assert(buildProfile, 'buildProfile must be defined in project context');
|
||||
const projectContext = await this.createProjectContextAsync(ctx, account, buildProfile);
|
||||
app = projectContext.app;
|
||||
targets = projectContext.targets;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
assert(targets && app);
|
||||
const iosAppCredentialsMap: IosAppCredentialsMap = {};
|
||||
for (const target of targets) {
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx, target);
|
||||
iosAppCredentialsMap[target.targetName] =
|
||||
await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(appLookupParams);
|
||||
if (ctx.hasProjectContext) {
|
||||
assert(targets && app);
|
||||
const iosAppCredentialsMap: IosAppCredentialsMap = {};
|
||||
for (const target of targets) {
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx, target);
|
||||
iosAppCredentialsMap[target.targetName] =
|
||||
await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(appLookupParams);
|
||||
}
|
||||
displayIosCredentials(app, iosAppCredentialsMap, targets);
|
||||
}
|
||||
displayIosCredentials(app, iosAppCredentialsMap, targets);
|
||||
|
||||
const { action: chosenAction } = await promptAsync({
|
||||
type: 'select',
|
||||
@@ -126,6 +137,10 @@ export class ManageIos {
|
||||
return await this.callingAction.runAsync(ctx);
|
||||
}
|
||||
} else if (actionInfo.scope === Scope.Project) {
|
||||
assert(
|
||||
ctx.hasProjectContext,
|
||||
'You must be in your project directory in order to perform this action'
|
||||
);
|
||||
await this.runProjectSpecificActionAsync(
|
||||
ctx,
|
||||
nullthrows(app, 'app must be defined in project context'),
|
||||
@@ -157,6 +172,8 @@ export class ManageIos {
|
||||
app: App;
|
||||
targets: Target[];
|
||||
}> {
|
||||
assert(ctx.hasProjectContext, 'createProjectContextAsync: must have project context.');
|
||||
|
||||
const app = { account, projectName: ctx.exp.slug };
|
||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ManageIos } from './ManageIos';
|
||||
export class SelectPlatform {
|
||||
constructor(
|
||||
public readonly actor: Actor,
|
||||
public readonly projectInfo: CredentialsContextProjectInfo,
|
||||
public readonly projectInfo: CredentialsContextProjectInfo | null,
|
||||
private readonly flagPlatform?: string
|
||||
) {}
|
||||
|
||||
|
||||
@@ -84,5 +84,17 @@ describe(AccountResolver, () => {
|
||||
expect(account).toEqual(user.accounts[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when outside project dir', () => {
|
||||
it('asks the user to choose the account from his account list', async () => {
|
||||
jest.mocked(prompts).mockImplementationOnce(async () => ({
|
||||
account: user.accounts[0],
|
||||
}));
|
||||
|
||||
const resolver = new AccountResolver(null, user);
|
||||
const account = await resolver.resolveAccountAsync();
|
||||
expect(account).toEqual(user.accounts[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { Actor } from '../user/User';
|
||||
export interface DeviceManagerContext {
|
||||
appStore: AppStoreApi;
|
||||
user: Actor;
|
||||
projectId: string;
|
||||
projectId: string | null;
|
||||
}
|
||||
|
||||
export async function createContextAsync({
|
||||
@@ -14,11 +14,11 @@ export async function createContextAsync({
|
||||
}: {
|
||||
appStore: AppStoreApi;
|
||||
user: Actor;
|
||||
projectId: string;
|
||||
projectId: string | undefined;
|
||||
}): Promise<DeviceManagerContext> {
|
||||
return {
|
||||
appStore,
|
||||
user,
|
||||
projectId,
|
||||
projectId: projectId ?? null,
|
||||
};
|
||||
}
|
||||
@@ -46,9 +46,11 @@ export class AccountResolver {
|
||||
constructor(private projectId: string | null, private user: Actor) {}
|
||||
|
||||
public async resolveAccountAsync(): Promise<AccountFragment> {
|
||||
const account = await this.resolveProjectAccountAsync();
|
||||
if (account) {
|
||||
return account;
|
||||
if (this.projectId) {
|
||||
const account = await this.resolveProjectAccountAsync();
|
||||
if (account) {
|
||||
return account;
|
||||
}
|
||||
}
|
||||
return await this.promptForAccountAsync();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { PaginatedQueryOptions } from '../commandUtils/pagination';
|
||||
import { formatAppleTeam } from '../credentials/ios/actions/AppleTeamFormatting';
|
||||
import { formatDeviceLabel } from '../credentials/ios/actions/DeviceUtils';
|
||||
import { AppleDeviceQuery } from '../credentials/ios/api/graphql/queries/AppleDeviceQuery';
|
||||
import { AppleTeamQuery } from '../credentials/ios/api/graphql/queries/AppleTeamQuery';
|
||||
import { AppleDeviceFragment, AppleTeamFragment } from '../graphql/generated';
|
||||
import Log from '../log';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import {
|
||||
paginatedQueryWithConfirmPromptAsync,
|
||||
paginatedQueryWithSelectPromptAsync,
|
||||
} from '../utils/queries';
|
||||
import formatDevice, { AppleTeamIdAndName } from './utils/formatDevice';
|
||||
|
||||
export const TEAMS_LIMIT = 50;
|
||||
export const DEVICES_LIMIT = 50;
|
||||
|
||||
export async function selectAppleTeamOnAccountAsync({
|
||||
accountName,
|
||||
selectionPromptTitle,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
accountName: string;
|
||||
selectionPromptTitle: string;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}): Promise<AppleTeamFragment> {
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
throw new Error('Unable to select an Apple team in non-interactive mode.');
|
||||
} else {
|
||||
const selectedAppleTeam = await paginatedQueryWithSelectPromptAsync({
|
||||
limit: paginatedQueryOptions.limit ?? TEAMS_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
queryToPerform: (limit, offset) =>
|
||||
AppleTeamQuery.getAllForAccountAsync({
|
||||
accountName,
|
||||
limit,
|
||||
offset,
|
||||
}),
|
||||
promptOptions: {
|
||||
title: selectionPromptTitle,
|
||||
createDisplayTextForSelectionPromptListItem: appleTeam =>
|
||||
appleTeam.appleTeamName
|
||||
? `${appleTeam.appleTeamName} (ID: ${appleTeam.appleTeamIdentifier})`
|
||||
: appleTeam.appleTeamIdentifier,
|
||||
getIdentifierForQueryItem: appleTeam => appleTeam.id,
|
||||
},
|
||||
});
|
||||
if (!selectedAppleTeam) {
|
||||
throw new Error(`Couldn't find any teams for the account ${accountName}`);
|
||||
}
|
||||
return selectedAppleTeam;
|
||||
}
|
||||
}
|
||||
|
||||
export async function selectAppleDeviceOnAppleTeamAsync({
|
||||
accountName,
|
||||
appleTeamIdentifier,
|
||||
selectionPromptTitle,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
accountName: string;
|
||||
appleTeamIdentifier: string;
|
||||
selectionPromptTitle: string;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}): Promise<AppleDeviceFragment> {
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
throw new Error('Unable to select an Apple device in non-interactive mode.');
|
||||
} else {
|
||||
const selectedAppleDevice = await paginatedQueryWithSelectPromptAsync({
|
||||
limit: paginatedQueryOptions.limit ?? DEVICES_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
queryToPerform: (limit, offset) =>
|
||||
AppleDeviceQuery.getAllForAppleTeamAsync({
|
||||
accountName,
|
||||
appleTeamIdentifier,
|
||||
limit,
|
||||
offset,
|
||||
}),
|
||||
promptOptions: {
|
||||
title: selectionPromptTitle,
|
||||
createDisplayTextForSelectionPromptListItem: appleDevice => formatDeviceLabel(appleDevice),
|
||||
getIdentifierForQueryItem: appleTeam => appleTeam.id,
|
||||
},
|
||||
});
|
||||
if (!selectedAppleDevice) {
|
||||
throw new Error(
|
||||
`Couldn't find any devices on the Apple team with the id ${appleTeamIdentifier}`
|
||||
);
|
||||
}
|
||||
return selectedAppleDevice;
|
||||
}
|
||||
}
|
||||
|
||||
export async function listAndRenderAppleDevicesOnAppleTeamAsync({
|
||||
accountName,
|
||||
appleTeam,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
accountName: string;
|
||||
appleTeam: AppleTeamIdAndName;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}): Promise<void> {
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
const devices = await AppleDeviceQuery.getAllForAppleTeamAsync({
|
||||
accountName,
|
||||
appleTeamIdentifier: appleTeam.appleTeamIdentifier,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
limit: paginatedQueryOptions.limit,
|
||||
});
|
||||
renderPageOfAppleDevices({ devices, appleTeam, paginatedQueryOptions });
|
||||
} else {
|
||||
await paginatedQueryWithConfirmPromptAsync({
|
||||
limit: paginatedQueryOptions.limit ?? DEVICES_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
queryToPerform: (limit, offset) =>
|
||||
AppleDeviceQuery.getAllForAppleTeamAsync({
|
||||
accountName,
|
||||
appleTeamIdentifier: appleTeam.appleTeamIdentifier,
|
||||
limit,
|
||||
offset,
|
||||
}),
|
||||
promptOptions: {
|
||||
title: 'Load more devices?',
|
||||
renderListItems: devices =>
|
||||
renderPageOfAppleDevices({ devices, appleTeam, paginatedQueryOptions }),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderPageOfAppleDevices({
|
||||
devices,
|
||||
appleTeam,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
devices: AppleDeviceFragment[];
|
||||
appleTeam: AppleTeamIdAndName;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}): void {
|
||||
if (paginatedQueryOptions.json) {
|
||||
printJsonOnlyOutput(devices);
|
||||
} else {
|
||||
if (devices.length === 0) {
|
||||
Log.log(
|
||||
`Could not find devices on Apple team -- ${formatAppleTeam({
|
||||
appleTeamIdentifier: appleTeam.appleTeamIdentifier,
|
||||
appleTeamName: appleTeam.appleTeamName,
|
||||
})}`
|
||||
);
|
||||
} else {
|
||||
const list = devices
|
||||
.map(device => formatDevice(device, appleTeam))
|
||||
.join(`\n\n${chalk.dim('———')}\n\n`);
|
||||
|
||||
Log.log(`\n${list}`);
|
||||
Log.addNewLineIfNone();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export class DeviceNotFoundError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message ?? 'Device not found.');
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
import { AppleDevice, AppleTeam } from '../../graphql/generated';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
|
||||
type Device = Pick<AppleDevice, 'id' | 'identifier' | 'name' | 'deviceClass' | 'enabled'>;
|
||||
type Device = Pick<AppleDevice, 'id' | 'identifier' | 'name' | 'deviceClass' | 'enabled' | 'model'>;
|
||||
|
||||
type Team = Pick<AppleTeam, 'appleTeamIdentifier' | 'appleTeamName'>;
|
||||
export type AppleTeamIdAndName = Pick<AppleTeam, 'appleTeamIdentifier' | 'appleTeamName'>;
|
||||
|
||||
export default function formatDevice(device: Device, team?: Team): string {
|
||||
export default function formatDevice(device: Device, team?: AppleTeamIdAndName): string {
|
||||
const fields = [
|
||||
{ label: 'ID', value: device.id },
|
||||
{ label: 'Name', value: device.name ?? 'Unknown' },
|
||||
{ label: 'Class', value: device.deviceClass ?? 'Unknown' },
|
||||
{
|
||||
label: 'Class',
|
||||
value: device.deviceClass
|
||||
? `${device.deviceClass}${device.model ? ` ${device.model}` : ''}`
|
||||
: 'Unknown',
|
||||
},
|
||||
{ label: 'UDID', value: device.identifier },
|
||||
];
|
||||
|
||||
|
||||
+36
-3
@@ -1639,6 +1639,12 @@ export type AppleTeamAppleAppIdentifiersArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type AppleTeamAppleDevicesArgs = {
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
offset?: InputMaybe<Scalars['Int']>;
|
||||
};
|
||||
|
||||
|
||||
export type AppleTeamAppleProvisioningProfilesArgs = {
|
||||
appleAppIdentifierId?: InputMaybe<Scalars['ID']>;
|
||||
};
|
||||
@@ -2030,6 +2036,17 @@ export type BuildOrBuildJob = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type BuildOrBuildJobQuery = {
|
||||
__typename?: 'BuildOrBuildJobQuery';
|
||||
/** Look up EAS Build or Classic Build Job by ID */
|
||||
byId: EasBuildOrClassicBuildJob;
|
||||
};
|
||||
|
||||
|
||||
export type BuildOrBuildJobQueryByIdArgs = {
|
||||
buildOrBuildJobId: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type BuildParamsInput = {
|
||||
resourceClass: BuildResourceClass;
|
||||
};
|
||||
@@ -2374,6 +2391,8 @@ export enum EasBuildDeprecationInfoType {
|
||||
UserFacing = 'USER_FACING'
|
||||
}
|
||||
|
||||
export type EasBuildOrClassicBuildJob = Build | BuildJob;
|
||||
|
||||
export enum EasServiceMetric {
|
||||
AssetsRequests = 'ASSETS_REQUESTS',
|
||||
BandwidthUsage = 'BANDWIDTH_USAGE',
|
||||
@@ -2554,6 +2573,15 @@ export type GitHubAppQuery = {
|
||||
__typename?: 'GitHubAppQuery';
|
||||
appIdentifier: Scalars['String'];
|
||||
clientIdentifier: Scalars['String'];
|
||||
searchRepositories: Array<GitHubAppInstallationAccessibleRepository>;
|
||||
};
|
||||
|
||||
|
||||
export type GitHubAppQuerySearchRepositoriesArgs = {
|
||||
githubAppInstallationId: Scalars['ID'];
|
||||
page?: InputMaybe<Scalars['Int']>;
|
||||
perPage?: InputMaybe<Scalars['Int']>;
|
||||
query: Scalars['String'];
|
||||
};
|
||||
|
||||
export type GitHubRepository = {
|
||||
@@ -3467,6 +3495,7 @@ export type RootQuery = {
|
||||
appleTeam: AppleTeamQuery;
|
||||
asset: AssetQuery;
|
||||
buildJobs: BuildJobQuery;
|
||||
buildOrBuildJob: BuildOrBuildJobQuery;
|
||||
/** Top-level query object for querying BuildPublicData publicly. */
|
||||
buildPublicData: BuildPublicDataQuery;
|
||||
builds: BuildQuery;
|
||||
@@ -4797,10 +4826,12 @@ export type AppleDevicesByAppleTeamQuery = { __typename?: 'RootQuery', appleTeam
|
||||
export type AppleDevicesByTeamIdentifierQueryVariables = Exact<{
|
||||
accountName: Scalars['String'];
|
||||
appleTeamIdentifier: Scalars['String'];
|
||||
offset?: InputMaybe<Scalars['Int']>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type AppleDevicesByTeamIdentifierQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byName: { __typename?: 'Account', id: string, appleTeams: Array<{ __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null, appleDevices: Array<{ __typename?: 'AppleDevice', id: string, identifier: string, name?: string | null, deviceClass?: AppleDeviceClass | null, enabled?: boolean | null }> }> } } };
|
||||
export type AppleDevicesByTeamIdentifierQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byName: { __typename?: 'Account', id: string, appleTeams: Array<{ __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null, appleDevices: Array<{ __typename?: 'AppleDevice', id: string, identifier: string, name?: string | null, deviceClass?: AppleDeviceClass | null, enabled?: boolean | null, model?: string | null }> }> } } };
|
||||
|
||||
export type AppleDevicesByIdentifierQueryVariables = Exact<{
|
||||
accountName: Scalars['String'];
|
||||
@@ -4808,7 +4839,7 @@ export type AppleDevicesByIdentifierQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type AppleDevicesByIdentifierQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byName: { __typename?: 'Account', id: string, appleDevices: Array<{ __typename?: 'AppleDevice', id: string, identifier: string, name?: string | null, deviceClass?: AppleDeviceClass | null, enabled?: boolean | null, appleTeam: { __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null } }> } } };
|
||||
export type AppleDevicesByIdentifierQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byName: { __typename?: 'Account', id: string, appleDevices: Array<{ __typename?: 'AppleDevice', id: string, model?: string | null, identifier: string, name?: string | null, deviceClass?: AppleDeviceClass | null, enabled?: boolean | null, appleTeam: { __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null } }> } } };
|
||||
|
||||
export type AppleDistributionCertificateByAppQueryVariables = Exact<{
|
||||
projectFullName: Scalars['String'];
|
||||
@@ -4844,10 +4875,12 @@ export type ApplePushKeyByAccountQuery = { __typename?: 'RootQuery', account: {
|
||||
|
||||
export type AppleTeamsByAccountNameQueryVariables = Exact<{
|
||||
accountName: Scalars['String'];
|
||||
offset?: InputMaybe<Scalars['Int']>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type AppleTeamsByAccountNameQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byName: { __typename?: 'Account', id: string, appleTeams: Array<{ __typename?: 'AppleTeam', id: string, appleTeamName?: string | null, appleTeamIdentifier: string }> } } };
|
||||
export type AppleTeamsByAccountNameQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byName: { __typename?: 'Account', id: string, appleTeams: Array<{ __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null }> } } };
|
||||
|
||||
export type AppleTeamByIdentifierQueryVariables = Exact<{
|
||||
accountId: Scalars['ID'];
|
||||
|
||||
@@ -9,6 +9,7 @@ export const AppFragmentNode = gql`
|
||||
slug
|
||||
ownerAccount {
|
||||
id
|
||||
name
|
||||
...AccountFragment
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import assert from 'assert';
|
||||
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { getRequestContext } from '../credentials/ios/appstore/authenticate';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
|
||||
import { Actor } from '../user/User';
|
||||
|
||||
@@ -43,7 +42,7 @@ export type MetadataAppStoreAuthentication = {
|
||||
export async function createMetadataContextAsync(params: {
|
||||
projectDir: string;
|
||||
credentialsCtx: CredentialsContext;
|
||||
exp?: ExpoConfig;
|
||||
exp: ExpoConfig;
|
||||
profileName?: string;
|
||||
}): Promise<MetadataContext> {
|
||||
const easJsonAccessor = new EasJsonAccessor(params.projectDir);
|
||||
@@ -53,7 +52,7 @@ export async function createMetadataContextAsync(params: {
|
||||
params.profileName
|
||||
);
|
||||
|
||||
const exp = params.exp ?? getExpoConfig(params.projectDir);
|
||||
const exp = params.exp;
|
||||
const user = params.credentialsCtx.user;
|
||||
const bundleIdentifier =
|
||||
submitProfile.bundleIdentifier ?? (await getBundleIdentifierAsync(params.projectDir, exp));
|
||||
|
||||
@@ -5,13 +5,13 @@ describe(isValidVersionCode, () => {
|
||||
expect(isValidVersionCode(123)).toBe(true);
|
||||
expect(isValidVersionCode(12.3)).toBe(false);
|
||||
expect(isValidVersionCode(-123)).toBe(false);
|
||||
expect(isValidVersionCode(2000000000)).toBe(true);
|
||||
expect(isValidVersionCode(2000000001)).toBe(false);
|
||||
expect(isValidVersionCode(2100000000)).toBe(true);
|
||||
expect(isValidVersionCode(2100000001)).toBe(false);
|
||||
expect(isValidVersionCode('123')).toBe(true);
|
||||
expect(isValidVersionCode('12.3')).toBe(false);
|
||||
expect(isValidVersionCode('-123')).toBe(false);
|
||||
expect(isValidVersionCode('2000000000')).toBe(true);
|
||||
expect(isValidVersionCode('2000000001')).toBe(false);
|
||||
expect(isValidVersionCode('2100000000')).toBe(true);
|
||||
expect(isValidVersionCode('2100000001')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,8 +19,8 @@ describe(getNextVersionCode, () => {
|
||||
test('getNextVersionCode()', () => {
|
||||
expect(getNextVersionCode(123)).toBe(124);
|
||||
expect(() => getNextVersionCode(12.3)).toThrowError();
|
||||
expect(getNextVersionCode(1999999999)).toBe(2000000000);
|
||||
expect(() => getNextVersionCode(2000000000)).toThrowError();
|
||||
expect(getNextVersionCode(2099999999)).toBe(2100000000);
|
||||
expect(() => getNextVersionCode(2100000000)).toThrowError();
|
||||
expect(getNextVersionCode('123')).toBe(124);
|
||||
expect(() => getNextVersionCode('12.3')).toThrowError();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from 'assert';
|
||||
|
||||
const MAX_VERSION_CODE = 2000000000;
|
||||
const MAX_VERSION_CODE = 2100000000;
|
||||
export const VERSION_CODE_REQUIREMENTS = `versionCode needs to be a positive integer smaller or equal to ${MAX_VERSION_CODE}`;
|
||||
|
||||
export function isValidVersionCode(versionCode: string | number): boolean {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { ExpoConfig, getConfig } from '@expo/config';
|
||||
import { Env } from '@expo/eas-build-job';
|
||||
|
||||
interface Options {
|
||||
export interface ExpoConfigOptions {
|
||||
env?: Env;
|
||||
isPublicConfig?: boolean;
|
||||
skipSDKVersionRequirement?: boolean;
|
||||
}
|
||||
|
||||
export function getExpoConfig(projectDir: string, opts: Options = {}): ExpoConfig {
|
||||
export function getExpoConfig(projectDir: string, opts: ExpoConfigOptions = {}): ExpoConfig {
|
||||
const originalProcessEnv: NodeJS.ProcessEnv = process.env;
|
||||
try {
|
||||
process.env = {
|
||||
|
||||
@@ -64,11 +64,6 @@ describe(AndroidSubmitCommand, () => {
|
||||
...testProject.projectTree,
|
||||
...fakeFiles,
|
||||
});
|
||||
|
||||
const mockManifest = { exp: testProject.appJSON.expo };
|
||||
jest.mock('@expo/config', () => ({
|
||||
getConfig: jest.fn(() => mockManifest),
|
||||
}));
|
||||
});
|
||||
afterAll(() => {
|
||||
vol.reset();
|
||||
@@ -85,7 +80,6 @@ describe(AndroidSubmitCommand, () => {
|
||||
const ctx = await createSubmissionContextAsync({
|
||||
platform: Platform.ANDROID,
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
archiveFlags: {
|
||||
url: 'http://expo.dev/fake.apk',
|
||||
},
|
||||
@@ -96,6 +90,8 @@ describe(AndroidSubmitCommand, () => {
|
||||
},
|
||||
nonInteractive: true,
|
||||
actor: mockJester,
|
||||
exp: testProject.appJSON.expo,
|
||||
projectId,
|
||||
});
|
||||
const command = new AndroidSubmitCommand(ctx);
|
||||
await expect(command.runAsync()).rejects.toThrowError();
|
||||
@@ -109,7 +105,6 @@ describe(AndroidSubmitCommand, () => {
|
||||
const ctx = await createSubmissionContextAsync({
|
||||
platform: Platform.ANDROID,
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
archiveFlags: {
|
||||
url: 'http://expo.dev/fake.apk',
|
||||
},
|
||||
@@ -121,6 +116,8 @@ describe(AndroidSubmitCommand, () => {
|
||||
},
|
||||
nonInteractive: false,
|
||||
actor: mockJester,
|
||||
exp: testProject.appJSON.expo,
|
||||
projectId,
|
||||
});
|
||||
const command = new AndroidSubmitCommand(ctx);
|
||||
await command.runAsync();
|
||||
@@ -146,7 +143,6 @@ describe(AndroidSubmitCommand, () => {
|
||||
const ctx = await createSubmissionContextAsync({
|
||||
platform: Platform.ANDROID,
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
archiveFlags: {
|
||||
latest: true,
|
||||
},
|
||||
@@ -158,6 +154,8 @@ describe(AndroidSubmitCommand, () => {
|
||||
},
|
||||
nonInteractive: false,
|
||||
actor: mockJester,
|
||||
exp: testProject.appJSON.expo,
|
||||
projectId,
|
||||
});
|
||||
const command = new AndroidSubmitCommand(ctx);
|
||||
await command.runAsync();
|
||||
|
||||
@@ -33,10 +33,6 @@ const testProject = createTestProject(testProjectId, mockJester.accounts[0].name
|
||||
package: 'com.expo.test.project',
|
||||
},
|
||||
});
|
||||
const mockManifest = { exp: testProject.appJSON.expo };
|
||||
jest.mock('@expo/config', () => ({
|
||||
getConfig: jest.fn(() => mockManifest),
|
||||
}));
|
||||
|
||||
const projectId = uuidv4();
|
||||
const mockDetectableServiceAccountJson = JSON.stringify({
|
||||
@@ -143,7 +139,6 @@ describe(getServiceAccountKeyResultAsync, () => {
|
||||
const ctx = await createSubmissionContextAsync({
|
||||
platform: Platform.ANDROID,
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
archiveFlags: {
|
||||
url: 'http://expo.dev/fake.apk',
|
||||
},
|
||||
@@ -154,6 +149,8 @@ describe(getServiceAccountKeyResultAsync, () => {
|
||||
},
|
||||
nonInteractive: true,
|
||||
actor: mockJester,
|
||||
exp: testProject.appJSON.expo,
|
||||
projectId,
|
||||
});
|
||||
const source: ServiceAccountSource = {
|
||||
sourceType: ServiceAccountSourceType.path,
|
||||
@@ -179,7 +176,6 @@ describe(getServiceAccountKeyResultAsync, () => {
|
||||
const ctx = await createSubmissionContextAsync({
|
||||
platform: Platform.ANDROID,
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
archiveFlags: {
|
||||
url: 'http://expo.dev/fake.apk',
|
||||
},
|
||||
@@ -190,6 +186,8 @@ describe(getServiceAccountKeyResultAsync, () => {
|
||||
},
|
||||
nonInteractive: true,
|
||||
actor: mockJester,
|
||||
exp: testProject.appJSON.expo,
|
||||
projectId,
|
||||
});
|
||||
const source: ServiceAccountSource = {
|
||||
sourceType: ServiceAccountSourceType.prompt,
|
||||
@@ -211,7 +209,6 @@ describe(getServiceAccountKeyResultAsync, () => {
|
||||
const ctx = await createSubmissionContextAsync({
|
||||
platform: Platform.ANDROID,
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
archiveFlags: {
|
||||
url: 'http://expo.dev/fake.apk',
|
||||
},
|
||||
@@ -222,6 +219,8 @@ describe(getServiceAccountKeyResultAsync, () => {
|
||||
},
|
||||
nonInteractive: true,
|
||||
actor: mockJester,
|
||||
exp: testProject.appJSON.expo,
|
||||
projectId,
|
||||
});
|
||||
const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, {
|
||||
sourceType: ServiceAccountSourceType.credentialsService,
|
||||
|
||||
@@ -6,7 +6,6 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import { TrackingContext } from '../analytics/common';
|
||||
import { Analytics, SubmissionEvent } from '../analytics/events';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
|
||||
import { Actor } from '../user/User';
|
||||
|
||||
@@ -41,22 +40,22 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
|
||||
platform: T;
|
||||
profile: SubmitProfile<T>;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
applicationIdentifier?: string;
|
||||
actor: Actor;
|
||||
exp: ExpoConfig;
|
||||
projectId: string;
|
||||
}): Promise<SubmissionContext<T>> {
|
||||
const { applicationIdentifier, projectDir, nonInteractive, actor } = params;
|
||||
const exp = getExpoConfig(projectDir, { env: params.env });
|
||||
const { applicationIdentifier, projectDir, nonInteractive, actor, exp, projectId } = params;
|
||||
const { env, ...rest } = params;
|
||||
const projectName = exp.slug;
|
||||
const account = await getOwnerAccountForProjectIdAsync(params.projectId);
|
||||
const account = await getOwnerAccountForProjectIdAsync(projectId);
|
||||
const accountId = account.id;
|
||||
let credentialsCtx: CredentialsContext | undefined = params.credentialsCtx;
|
||||
if (!credentialsCtx) {
|
||||
credentialsCtx = new CredentialsContext({
|
||||
projectDir,
|
||||
user: actor,
|
||||
projectInfo: { exp, projectId: params.projectId },
|
||||
projectInfo: { exp, projectId },
|
||||
nonInteractive,
|
||||
});
|
||||
}
|
||||
@@ -65,7 +64,7 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
|
||||
tracking_id: uuidv4(),
|
||||
platform: params.platform,
|
||||
...(accountId && { account_id: accountId }),
|
||||
project_id: params.projectId,
|
||||
project_id: projectId,
|
||||
};
|
||||
|
||||
Analytics.logEvent(SubmissionEvent.SUBMIT_COMMAND, trackingCtx);
|
||||
@@ -74,7 +73,6 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
|
||||
...rest,
|
||||
accountName: account.name,
|
||||
credentialsCtx,
|
||||
exp,
|
||||
projectName,
|
||||
user: actor,
|
||||
trackingCtx,
|
||||
|
||||
@@ -32,17 +32,12 @@ const testProject = createTestProject(testProjectId, mockJester.accounts[0].name
|
||||
package: 'com.expo.test.project',
|
||||
},
|
||||
});
|
||||
const mockManifest = { exp: testProject.appJSON.expo };
|
||||
jest.mock('@expo/config', () => ({
|
||||
getConfig: jest.fn(() => mockManifest),
|
||||
}));
|
||||
const projectId = uuidv4();
|
||||
|
||||
async function getIosSubmissionContextAsync(): Promise<SubmissionContext<Platform.IOS>> {
|
||||
return await createSubmissionContextAsync({
|
||||
platform: Platform.IOS,
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
archiveFlags: {
|
||||
url: 'http://expo.dev/fake.apk',
|
||||
},
|
||||
@@ -51,6 +46,8 @@ async function getIosSubmissionContextAsync(): Promise<SubmissionContext<Platfor
|
||||
},
|
||||
nonInteractive: true,
|
||||
actor: mockJester,
|
||||
exp: testProject.appJSON.expo,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -32,10 +32,6 @@ const testProject = createTestProject(testProjectId, mockJester.accounts[0].name
|
||||
package: 'com.expo.test.project',
|
||||
},
|
||||
});
|
||||
const mockManifest = { exp: testProject.appJSON.expo };
|
||||
jest.mock('@expo/config', () => ({
|
||||
getConfig: jest.fn(() => mockManifest),
|
||||
}));
|
||||
const projectId = uuidv4();
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -51,7 +47,6 @@ describe(getFromCredentialsServiceAsync, () => {
|
||||
const ctx = await createSubmissionContextAsync({
|
||||
platform: Platform.IOS,
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
archiveFlags: {
|
||||
url: 'http://expo.dev/fake.apk',
|
||||
},
|
||||
@@ -60,6 +55,8 @@ describe(getFromCredentialsServiceAsync, () => {
|
||||
},
|
||||
nonInteractive: false,
|
||||
actor: mockJester,
|
||||
exp: testProject.appJSON.expo,
|
||||
projectId,
|
||||
});
|
||||
jest
|
||||
.spyOn(SetUpSubmissionCredentials.prototype, 'runAsync')
|
||||
@@ -79,7 +76,6 @@ describe(getFromCredentialsServiceAsync, () => {
|
||||
const ctx = await createSubmissionContextAsync({
|
||||
platform: Platform.IOS,
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
archiveFlags: {
|
||||
url: 'http://expo.dev/fake.apk',
|
||||
},
|
||||
@@ -88,6 +84,8 @@ describe(getFromCredentialsServiceAsync, () => {
|
||||
},
|
||||
nonInteractive: true,
|
||||
actor: mockJester,
|
||||
exp: testProject.appJSON.expo,
|
||||
projectId,
|
||||
});
|
||||
jest
|
||||
.spyOn(SetUpSubmissionCredentials.prototype, 'runAsync')
|
||||
|
||||
Loaded 100 of 108 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user