mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9147ae631c |
No files matched your search
@@ -6,27 +6,15 @@ 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))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `-e` as a shortcut for `--profile` flag. ([#1342](https://github.com/expo/eas-cli/pull/1342) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Support JSON5 in **eas.json**. ([#1350](https://github.com/expo/eas-cli/pull/1350) by [@dsokal](https://github.com/dsokal))
|
||||
- Add pagination and interactivity to update commands. ([#1323](https://github.com/expo/eas-cli/pull/1323) by [@kgc00](https://github.com/kgc00))
|
||||
- Add support for uploading files to EAS Secret. ([#1354](https://github.com/expo/eas-cli/pull/1354) by [@dsokal](https://github.com/dsokal))
|
||||
- Add pagination to channel commands. ([#1352](https://github.com/expo/eas-cli/pull/1352) by [@kgc00](https://github.com/kgc00))
|
||||
- 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))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
### 🧹 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))
|
||||
|
||||
## [2.1.0](https://github.com/expo/eas-cli/releases/tag/v2.1.0) - 2022-09-05
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
Generated
+2
-1093
File diff suppressed because it is too large.
Load diff
@@ -13,7 +13,7 @@
|
||||
"@expo/config": "7.0.1",
|
||||
"@expo/config-plugins": "5.0.1",
|
||||
"@expo/config-types": "46.0.2",
|
||||
"@expo/eas-build-job": "0.2.94",
|
||||
"@expo/eas-build-job": "0.2.90",
|
||||
"@expo/eas-json": "2.1.0",
|
||||
"@expo/json-file": "8.2.36",
|
||||
"@expo/multipart-body-parser": "1.1.0",
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Actor } from '../user/User';
|
||||
|
||||
interface MockProject {
|
||||
projectRoot: string;
|
||||
projectTree: Record<string, string>;
|
||||
appJSON: AppJSON;
|
||||
packageJSON: PackageJSON;
|
||||
}
|
||||
|
||||
interface PackageJSON {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
main: string;
|
||||
}
|
||||
|
||||
interface AppJSON {
|
||||
expo: {
|
||||
name: string;
|
||||
version: string;
|
||||
slug: string;
|
||||
sdkVersion: string;
|
||||
owner?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export function createTestProject(
|
||||
user: Actor,
|
||||
appJSONExtraData?: Record<string, object | string>
|
||||
): MockProject {
|
||||
const projectRoot = '/test-project';
|
||||
const packageJSON: PackageJSON = {
|
||||
name: 'testing123',
|
||||
version: '0.1.0',
|
||||
description: 'fake description',
|
||||
main: 'index.js',
|
||||
};
|
||||
|
||||
const owner = appJSONExtraData?.owner as string | undefined;
|
||||
const appJSON: AppJSON = {
|
||||
expo: {
|
||||
name: 'testing 123',
|
||||
version: '0.1.0',
|
||||
slug: 'testing-123',
|
||||
sdkVersion: '33.0.0',
|
||||
owner: owner ?? (user.__typename === 'User' ? user.username : undefined),
|
||||
...appJSONExtraData,
|
||||
},
|
||||
};
|
||||
return {
|
||||
appJSON,
|
||||
packageJSON,
|
||||
projectRoot,
|
||||
projectTree: {
|
||||
[projectRoot + '/package.json']: JSON.stringify(packageJSON, null, 2),
|
||||
[projectRoot + '/app.json']: JSON.stringify(appJSON, null, 2),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { PaginatedQueryOptions } from '../commandUtils/pagination';
|
||||
import { UpdateBranchFragment } from '../graphql/generated';
|
||||
import { BranchQuery } from '../graphql/queries/BranchQuery';
|
||||
import Log from '../log';
|
||||
import { UPDATE_COLUMNS, formatUpdateMessage, getPlatformsForGroup } from '../update/utils';
|
||||
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../update/utils';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import {
|
||||
paginatedQueryWithConfirmPromptAsync,
|
||||
@@ -14,29 +14,24 @@ import {
|
||||
|
||||
export const BRANCHES_LIMIT = 50;
|
||||
|
||||
export async function selectBranchOnAppAsync({
|
||||
projectId,
|
||||
promptTitle,
|
||||
displayTextForListItem,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
projectId: string;
|
||||
displayTextForListItem: (queryItem: UpdateBranchFragment) => string;
|
||||
promptTitle: string;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}): Promise<UpdateBranchFragment> {
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
export async function selectBranchFromPaginatedQueryAsync(
|
||||
projectId: string,
|
||||
promptTitle: string,
|
||||
options: PaginatedQueryOptions
|
||||
): Promise<UpdateBranchFragment> {
|
||||
if (options.nonInteractive) {
|
||||
throw new Error('Unable to select a branch in non-interactive mode.');
|
||||
}
|
||||
|
||||
const selectedBranch = await paginatedQueryWithSelectPromptAsync({
|
||||
limit: paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
queryToPerform: (limit, offset) => queryBranchesOnProjectAsync(limit, offset, projectId),
|
||||
limit: options.limit ?? BRANCHES_LIMIT,
|
||||
offset: options.offset,
|
||||
queryToPerform: (limit, offset) => queryBranchesForProjectAsync(limit, offset, projectId),
|
||||
promptOptions: {
|
||||
title: promptTitle,
|
||||
getIdentifierForQueryItem: updateBranchFragment => updateBranchFragment.id,
|
||||
createDisplayTextForSelectionPromptListItem: displayTextForListItem,
|
||||
createDisplayTextForSelectionPromptListItem: updateBranchFragment =>
|
||||
updateBranchFragment.name,
|
||||
},
|
||||
});
|
||||
if (!selectedBranch) {
|
||||
@@ -45,39 +40,36 @@ export async function selectBranchOnAppAsync({
|
||||
return selectedBranch;
|
||||
}
|
||||
|
||||
export async function listAndRenderBranchesOnAppAsync({
|
||||
projectId,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
projectId: string;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}): Promise<void> {
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
const branches = await queryBranchesOnProjectAsync(
|
||||
paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
|
||||
paginatedQueryOptions.offset,
|
||||
export async function listAndRenderPaginatedBranchesAsync(
|
||||
projectId: string,
|
||||
options: PaginatedQueryOptions
|
||||
): Promise<void> {
|
||||
if (options.nonInteractive) {
|
||||
const branches = await queryBranchesForProjectAsync(
|
||||
options.limit ?? BRANCHES_LIMIT,
|
||||
options.offset,
|
||||
projectId
|
||||
);
|
||||
renderPageOfBranches(branches, paginatedQueryOptions);
|
||||
renderPageOfBranches(branches, options);
|
||||
} else {
|
||||
await paginatedQueryWithConfirmPromptAsync({
|
||||
limit: paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
queryToPerform: (limit, offset) => queryBranchesOnProjectAsync(limit, offset, projectId),
|
||||
limit: options.limit ?? BRANCHES_LIMIT,
|
||||
offset: options.offset,
|
||||
queryToPerform: (limit, offset) => queryBranchesForProjectAsync(limit, offset, projectId),
|
||||
promptOptions: {
|
||||
title: 'Load more branches?',
|
||||
renderListItems: branches => renderPageOfBranches(branches, paginatedQueryOptions),
|
||||
renderListItems: branches => renderPageOfBranches(branches, options),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function queryBranchesOnProjectAsync(
|
||||
async function queryBranchesForProjectAsync(
|
||||
limit: number,
|
||||
offset: number,
|
||||
projectId: string
|
||||
): Promise<UpdateBranchFragment[]> {
|
||||
return await BranchQuery.listBranchesOnAppAsync({
|
||||
return await BranchQuery.listBranchesAsync({
|
||||
appId: projectId,
|
||||
limit,
|
||||
offset,
|
||||
@@ -98,20 +90,18 @@ function renderPageOfBranches(
|
||||
|
||||
table.push(
|
||||
...currentPage.map(branch => {
|
||||
if (branch.updates.length === 0) {
|
||||
return [branch.name, 'N/A', 'N/A', 'N/A', 'N/A'];
|
||||
}
|
||||
|
||||
const latestUpdateOnBranch = branch.updates[0];
|
||||
const update: UpdateBranchFragment['updates'][0] | undefined = branch.updates[0];
|
||||
return [
|
||||
branch.name,
|
||||
formatUpdateMessage(latestUpdateOnBranch),
|
||||
latestUpdateOnBranch.runtimeVersion,
|
||||
latestUpdateOnBranch.group,
|
||||
getPlatformsForGroup({
|
||||
group: latestUpdateOnBranch.group,
|
||||
updates: branch.updates,
|
||||
}),
|
||||
formatUpdate(update),
|
||||
update?.runtimeVersion ?? 'N/A',
|
||||
update?.group ?? 'N/A',
|
||||
update?.group
|
||||
? getPlatformsForGroup({
|
||||
updates: branch.updates,
|
||||
group: update.group,
|
||||
})
|
||||
: 'N/A',
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { getVcsClient } from '../vcs';
|
||||
|
||||
export async function getDefaultBranchNameAsync(): Promise<string> {
|
||||
return (
|
||||
(await getVcsClient().getBranchNameAsync()) ||
|
||||
`branch-${Math.random().toString(36).substring(2, 4)}`
|
||||
);
|
||||
}
|
||||
|
||||
export class BranchNotFoundError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message ?? 'Branch not found.');
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { selectBuildToCancelAsync } from '../../commands/build/cancel';
|
||||
import { AppPlatform, BuildFragment, BuildPriority, BuildStatus } from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import { confirmAsync, selectAsync } from '../../prompts';
|
||||
|
||||
jest.mock('../../ora', () => ({
|
||||
ora: jest.fn().mockImplementation(() => ({
|
||||
start: jest.fn().mockImplementation(() => ({
|
||||
stop: jest.fn(),
|
||||
fail: jest.fn(),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
jest.mock('../../prompts', () => {
|
||||
return {
|
||||
selectAsync: jest.fn(),
|
||||
confirmAsync: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../graphql/queries/BuildQuery', () => {
|
||||
const actual = jest.requireActual('../../graphql/queries/BuildQuery');
|
||||
return {
|
||||
BuildQuery: {
|
||||
...actual.BuildQuery,
|
||||
viewBuildsOnAppAsync: jest.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe(selectBuildToCancelAsync.name, () => {
|
||||
const selectedBuildId = uuid();
|
||||
const projectId = uuid();
|
||||
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.mocked(BuildQuery.viewBuildsOnAppAsync)
|
||||
.mockImplementation(async () => [
|
||||
createMockBuildFragment({ projectId, buildId: selectedBuildId }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not return build id when confirmation is rejected', async () => {
|
||||
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
|
||||
expect(selectBuildToCancelAsync(projectId, 'blah')).resolves.toEqual(null);
|
||||
});
|
||||
|
||||
it('returns build id when confirmation is confirmed', async () => {
|
||||
jest.mocked(selectAsync).mockResolvedValueOnce(selectedBuildId);
|
||||
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
|
||||
expect(selectBuildToCancelAsync(projectId, 'blah')).resolves.toEqual(selectedBuildId);
|
||||
});
|
||||
});
|
||||
|
||||
function createMockBuildFragment({
|
||||
projectId,
|
||||
buildId,
|
||||
}: {
|
||||
projectId: string;
|
||||
buildId?: string;
|
||||
}): BuildFragment {
|
||||
return {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
platform: AppPlatform.Android,
|
||||
id: buildId ?? uuid(),
|
||||
priority: BuildPriority.Normal,
|
||||
project: {
|
||||
__typename: 'App',
|
||||
slug: 'test-project',
|
||||
id: projectId,
|
||||
name: 'test-project',
|
||||
ownerAccount: {
|
||||
__typename: 'Account',
|
||||
id: uuid(),
|
||||
name: 'test-account',
|
||||
},
|
||||
},
|
||||
status: BuildStatus.InQueue,
|
||||
};
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '../../project/android/applicationId';
|
||||
import { resolveGradleBuildContextAsync } from '../../project/android/gradle';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import {
|
||||
BuildRequestSender,
|
||||
CredentialsResult,
|
||||
@@ -57,7 +58,7 @@ This means that it will most likely produce an AAB and you will not be able to i
|
||||
const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp, ctx.user);
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
|
||||
const applicationId = await getApplicationIdAsync(ctx.projectDir, ctx.exp, gradleContext);
|
||||
@@ -91,7 +92,6 @@ export async function prepareAndroidBuildAsync(
|
||||
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
|
||||
? false
|
||||
: ctx.buildProfile.autoIncrement,
|
||||
projectId: ctx.projectId,
|
||||
});
|
||||
},
|
||||
prepareJobAsync: async (
|
||||
@@ -136,7 +136,7 @@ async function ensureAndroidCredentialsAsync(
|
||||
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
|
||||
app: {
|
||||
account: nullthrows(
|
||||
ctx.user.accounts.find(a => a.name === ctx.accountName),
|
||||
findAccountByName(ctx.user.accounts, ctx.accountName),
|
||||
`You do not have access to account: ${ctx.accountName}`
|
||||
),
|
||||
projectName: ctx.projectName,
|
||||
|
||||
@@ -5,6 +5,7 @@ import slash from 'slash';
|
||||
|
||||
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { BuildContext } from '../context';
|
||||
|
||||
@@ -23,7 +24,7 @@ export async function prepareJobAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData
|
||||
): Promise<Job> {
|
||||
const username = getUsername(ctx.exp, ctx.user);
|
||||
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
|
||||
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
|
||||
const projectRootDirectory =
|
||||
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
|
||||
|
||||
@@ -16,12 +16,10 @@ export async function syncProjectConfigurationAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
localAutoIncrement,
|
||||
projectId,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
localAutoIncrement?: AndroidVersionAutoIncrement;
|
||||
projectId: string;
|
||||
}): Promise<void> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
|
||||
@@ -29,7 +27,7 @@ export async function syncProjectConfigurationAsync({
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp, projectId);
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
|
||||
} else {
|
||||
|
||||
@@ -177,17 +177,6 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
)}`
|
||||
);
|
||||
const projectTarball = await makeProjectTarballAsync();
|
||||
|
||||
if (projectTarball.size > 1024 * 1024 * 100) {
|
||||
Log.warn(
|
||||
`Your project archive is ${formatBytes(
|
||||
projectTarball.size
|
||||
)}. You can reduce its size and the time it takes to upload by excluding files that are unnecessary for the build process in ${chalk.bold(
|
||||
'.easignore'
|
||||
)} file. ${learnMore('https://expo.fyi/eas-build-archive')}`
|
||||
);
|
||||
}
|
||||
|
||||
projectTarballPath = projectTarball.path;
|
||||
|
||||
const { bucketKey } = await uploadFileAtPathToS3Async(
|
||||
|
||||
@@ -10,9 +10,10 @@ import { Analytics, BuildEvent } from '../analytics/events';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { BuildResourceClass } from '../graphql/generated';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
|
||||
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { Actor } from '../user/User';
|
||||
import { findAccountByName } from '../user/Account';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { createAndroidContextAsync } from './android/build';
|
||||
import { BuildContext, CommonContext } from './context';
|
||||
import { createIosContextAsync } from './ios/build';
|
||||
@@ -30,8 +31,6 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
projectDir,
|
||||
resourceClass,
|
||||
message,
|
||||
actor,
|
||||
projectId,
|
||||
}: {
|
||||
buildProfileName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
@@ -44,22 +43,22 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
projectDir: string;
|
||||
resourceClass: BuildResourceClass;
|
||||
message?: string;
|
||||
actor: Actor;
|
||||
projectId: string;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
|
||||
|
||||
const user = await ensureLoggedInAsync();
|
||||
const accountName = getProjectAccountName(exp, user);
|
||||
const projectName = exp.slug;
|
||||
const account = await getOwnerAccountForProjectIdAsync(projectId);
|
||||
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
|
||||
const workflow = await resolveWorkflowAsync(projectDir, platform);
|
||||
const accountId = account.id;
|
||||
const accountId = findAccountByName(user.accounts, accountName)?.id;
|
||||
const runFromCI = getenv.boolish('CI', false);
|
||||
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
projectInfo: { exp, projectId },
|
||||
exp,
|
||||
nonInteractive,
|
||||
projectDir,
|
||||
user: actor,
|
||||
user,
|
||||
env: buildProfile.env,
|
||||
easJsonCliConfig,
|
||||
});
|
||||
@@ -82,7 +81,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
|
||||
|
||||
const commonContext: CommonContext<T> = {
|
||||
accountName: account.name,
|
||||
accountName,
|
||||
buildProfile,
|
||||
buildProfileName,
|
||||
resourceClass,
|
||||
@@ -98,7 +97,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
projectId,
|
||||
projectName,
|
||||
trackingCtx,
|
||||
user: actor,
|
||||
user,
|
||||
workflow,
|
||||
message,
|
||||
runFromCI,
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function createIosContextAsync(
|
||||
const { buildProfile } = ctx;
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp, ctx.user);
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
|
||||
checkNodeEnvVariable(ctx);
|
||||
@@ -80,7 +80,6 @@ export async function prepareIosBuildAsync(
|
||||
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
|
||||
? false
|
||||
: ctx.buildProfile.autoIncrement,
|
||||
projectId: ctx.projectId,
|
||||
});
|
||||
},
|
||||
prepareJobAsync: async (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
|
||||
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
|
||||
import { getAppFromContextAsync } from '../../credentials/ios/actions/BuildCredentialsUtils';
|
||||
import { getAppFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
|
||||
import { IosCredentials, Target } from '../../credentials/ios/types';
|
||||
import { CredentialsResult } from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
@@ -16,7 +16,7 @@ export async function ensureIosCredentialsAsync(
|
||||
}
|
||||
|
||||
const provider = new IosCredentialsProvider(buildCtx.credentialsCtx, {
|
||||
app: await getAppFromContextAsync(buildCtx.credentialsCtx),
|
||||
app: getAppFromContext(buildCtx.credentialsCtx),
|
||||
targets,
|
||||
distribution: buildCtx.buildProfile.distribution ?? 'store',
|
||||
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
|
||||
|
||||
@@ -4,6 +4,7 @@ import slash from 'slash';
|
||||
|
||||
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { BuildContext } from '../context';
|
||||
|
||||
@@ -25,7 +26,7 @@ export async function prepareJobAsync(
|
||||
): Promise<Job> {
|
||||
const projectRootDirectory =
|
||||
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
|
||||
const username = getUsername(ctx.exp, ctx.user);
|
||||
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
|
||||
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
|
||||
if (jobData.credentials) {
|
||||
const targetNames = Object.keys(jobData.credentials);
|
||||
|
||||
@@ -13,20 +13,18 @@ export async function syncProjectConfigurationAsync({
|
||||
exp,
|
||||
targets,
|
||||
localAutoIncrement,
|
||||
projectId,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
targets: Target[];
|
||||
localAutoIncrement?: IosVersionAutoIncrement;
|
||||
projectId: string;
|
||||
}): Promise<void> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
|
||||
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp, projectId);
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, targets });
|
||||
} else {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
readChannelSafelyAsync as readIosChannelSafelyAsync,
|
||||
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
|
||||
} from '../update/ios/UpdatesModule';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
|
||||
@@ -48,7 +49,7 @@ export async function collectMetadataAsync<T extends Platform>(
|
||||
(await vcsClient.getLastCommitMessageAsync()) ?? undefined
|
||||
),
|
||||
isGitWorkingTreeDirty: await vcsClient.hasUncommittedChangesAsync(),
|
||||
username: getUsername(ctx.exp, ctx.user),
|
||||
username: getUsername(ctx.exp, await ensureLoggedInAsync()),
|
||||
message: ctx.message,
|
||||
...(ctx.platform === Platform.IOS && {
|
||||
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { PaginatedQueryOptions } from '../commandUtils/pagination';
|
||||
import { BuildFilter, BuildFragment } from '../graphql/generated';
|
||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||
import Log from '../log';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import { paginatedQueryWithConfirmPromptAsync } from '../utils/queries';
|
||||
import { formatGraphQLBuild } from './utils/formatBuild';
|
||||
|
||||
export const BUILDS_LIMIT = 50;
|
||||
|
||||
export async function listAndRenderBuildsOnAppAsync({
|
||||
projectId,
|
||||
projectDisplayName,
|
||||
filter,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
projectId: string;
|
||||
projectDisplayName: string;
|
||||
filter?: BuildFilter;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}): Promise<void> {
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
const builds = await BuildQuery.viewBuildsOnAppAsync({
|
||||
appId: projectId,
|
||||
limit: paginatedQueryOptions.limit ?? BUILDS_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
filter,
|
||||
});
|
||||
renderPageOfBuilds({ builds, projectDisplayName, paginatedQueryOptions });
|
||||
} else {
|
||||
await paginatedQueryWithConfirmPromptAsync({
|
||||
limit: paginatedQueryOptions.limit ?? BUILDS_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
queryToPerform: (limit, offset) =>
|
||||
BuildQuery.viewBuildsOnAppAsync({
|
||||
appId: projectId,
|
||||
limit,
|
||||
offset,
|
||||
filter,
|
||||
}),
|
||||
promptOptions: {
|
||||
title: 'Load more builds?',
|
||||
renderListItems: builds =>
|
||||
renderPageOfBuilds({ builds, projectDisplayName, paginatedQueryOptions }),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderPageOfBuilds({
|
||||
builds,
|
||||
projectDisplayName,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
builds: BuildFragment[];
|
||||
projectDisplayName: string;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}): void {
|
||||
if (paginatedQueryOptions.json) {
|
||||
printJsonOnlyOutput(builds);
|
||||
} else {
|
||||
const list = builds.map(build => formatGraphQLBuild(build)).join(`\n\n${chalk.dim('———')}\n\n`);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold(`Builds for ${projectDisplayName}:`));
|
||||
Log.log(`\n${list}`);
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,6 @@ import {
|
||||
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
|
||||
} from '../submit/submit';
|
||||
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
|
||||
import { Actor } from '../user/User';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import { ProfileData, getProfilesAsync } from '../utils/profiles';
|
||||
import { getVcsClient } from '../vcs';
|
||||
@@ -88,12 +87,7 @@ const platformToGraphQLResourceClassMapping: Record<
|
||||
},
|
||||
};
|
||||
|
||||
export async function runBuildAndSubmitAsync(
|
||||
projectDir: string,
|
||||
flags: BuildFlags,
|
||||
actor: Actor,
|
||||
projectId: string
|
||||
): Promise<void> {
|
||||
export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFlags): Promise<void> {
|
||||
await getVcsClient().ensureRepoExistsAsync();
|
||||
await ensureRepoIsCleanAsync(flags.nonInteractive);
|
||||
|
||||
@@ -140,8 +134,6 @@ export async function runBuildAndSubmitAsync(
|
||||
flags.userInputResourceClass ?? UserInputResourceClass.DEFAULT
|
||||
],
|
||||
easJsonCliConfig,
|
||||
actor,
|
||||
projectId,
|
||||
});
|
||||
if (maybeBuild) {
|
||||
startedBuilds.push({ build: maybeBuild, buildProfile });
|
||||
@@ -235,8 +227,6 @@ async function prepareAndStartBuildAsync({
|
||||
buildProfile,
|
||||
resourceClass,
|
||||
easJsonCliConfig,
|
||||
actor,
|
||||
projectId,
|
||||
}: {
|
||||
projectDir: string;
|
||||
flags: BuildFlags;
|
||||
@@ -244,8 +234,6 @@ async function prepareAndStartBuildAsync({
|
||||
buildProfile: ProfileData<'build'>;
|
||||
resourceClass: BuildResourceClass;
|
||||
easJsonCliConfig: EasJson['cli'];
|
||||
actor: Actor;
|
||||
projectId: string;
|
||||
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
|
||||
const buildCtx = await createBuildContextAsync({
|
||||
buildProfileName: buildProfile.profileName,
|
||||
@@ -259,8 +247,6 @@ async function prepareAndStartBuildAsync({
|
||||
localBuildOptions: flags.localBuildOptions,
|
||||
easJsonCliConfig,
|
||||
message: flags.message,
|
||||
actor,
|
||||
projectId,
|
||||
});
|
||||
|
||||
if (moreBuilds) {
|
||||
@@ -332,7 +318,6 @@ async function prepareAndStartSubmissionAsync({
|
||||
env: buildProfile.env,
|
||||
credentialsCtx: buildCtx.credentialsCtx,
|
||||
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
|
||||
actor: buildCtx.user,
|
||||
});
|
||||
|
||||
if (moreBuilds) {
|
||||
|
||||
@@ -70,11 +70,7 @@ export async function commitPromptAsync({
|
||||
initial: initialCommitMessage,
|
||||
validate: (input: string) => input !== '',
|
||||
});
|
||||
await getVcsClient().commitAsync({
|
||||
commitAllFiles,
|
||||
commitMessage: message,
|
||||
nonInteractive: false,
|
||||
});
|
||||
await getVcsClient().commitAsync({ commitAllFiles, commitMessage: message });
|
||||
}
|
||||
|
||||
export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { PaginatedQueryOptions } from '../commandUtils/pagination';
|
||||
import {
|
||||
ViewBranchesOnUpdateChannelQueryVariables,
|
||||
ViewUpdateChannelsOnAppQueryVariables,
|
||||
} from '../graphql/generated';
|
||||
import { BranchQuery, UpdateBranchOnChannelObject } from '../graphql/queries/BranchQuery';
|
||||
import { ChannelQuery, UpdateChannelObject } from '../graphql/queries/ChannelQuery';
|
||||
import Log from '../log';
|
||||
import formatFields from '../utils/formatFields';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import {
|
||||
paginatedQueryWithConfirmPromptAsync,
|
||||
paginatedQueryWithSelectPromptAsync,
|
||||
} from '../utils/queries';
|
||||
import { logChannelDetails } from './utils';
|
||||
|
||||
export const CHANNELS_LIMIT = 25;
|
||||
|
||||
export async function selectChannelOnAppAsync({
|
||||
projectId,
|
||||
selectionPromptTitle,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
projectId: string;
|
||||
selectionPromptTitle: string;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}): Promise<UpdateChannelObject> {
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
throw new Error('Unable to select a channel in non-interactive mode.');
|
||||
}
|
||||
|
||||
const updateChannel = await paginatedQueryWithSelectPromptAsync({
|
||||
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
queryToPerform: (limit, offset) => queryChannelsOnAppAsync({ appId: projectId, limit, offset }),
|
||||
promptOptions: {
|
||||
title: selectionPromptTitle,
|
||||
createDisplayTextForSelectionPromptListItem: updateChannel => updateChannel.name,
|
||||
getIdentifierForQueryItem: updateChannel => updateChannel.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!updateChannel) {
|
||||
throw new Error(`Could not find any channels for app "${projectId}"`);
|
||||
}
|
||||
return updateChannel;
|
||||
}
|
||||
|
||||
export async function listAndRenderChannelsOnAppAsync({
|
||||
projectId,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
projectId: string;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}): Promise<void> {
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
const channels = await queryChannelsOnAppAsync({
|
||||
appId: projectId,
|
||||
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
});
|
||||
renderPageOfChannels(channels, paginatedQueryOptions);
|
||||
} else {
|
||||
await paginatedQueryWithConfirmPromptAsync({
|
||||
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
queryToPerform: (limit, offset) =>
|
||||
queryChannelsOnAppAsync({ limit, offset, appId: projectId }),
|
||||
promptOptions: {
|
||||
title: 'Load more channels?',
|
||||
renderListItems: channels => renderPageOfChannels(channels, paginatedQueryOptions),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function listAndRenderBranchesAndUpdatesOnChannelAsync({
|
||||
projectId: appId,
|
||||
channelName,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
projectId: string;
|
||||
channelName: string;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}): Promise<void> {
|
||||
const channel = await ChannelQuery.viewUpdateChannelAsync({ appId, channelName });
|
||||
renderChannelHeaderContent({ channelName: channel.name, channelId: channel.id });
|
||||
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
const branches = await queryBranchesAndUpdateGroupsOnChannelAsync({
|
||||
appId,
|
||||
channelName,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
|
||||
});
|
||||
renderPageOfBranchesOnChannel(channel, branches, paginatedQueryOptions);
|
||||
} else {
|
||||
await paginatedQueryWithConfirmPromptAsync({
|
||||
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
queryToPerform: (limit, offset) =>
|
||||
queryBranchesAndUpdateGroupsOnChannelAsync({
|
||||
channelName,
|
||||
appId,
|
||||
offset,
|
||||
limit,
|
||||
}),
|
||||
promptOptions: {
|
||||
title: 'Load more channels?',
|
||||
renderListItems: branches =>
|
||||
renderPageOfBranchesOnChannel(channel, branches, paginatedQueryOptions),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function queryChannelsOnAppAsync({
|
||||
appId,
|
||||
offset,
|
||||
limit,
|
||||
}: ViewUpdateChannelsOnAppQueryVariables): Promise<UpdateChannelObject[]> {
|
||||
return await ChannelQuery.viewUpdateChannelsOnAppAsync({
|
||||
appId,
|
||||
offset,
|
||||
limit,
|
||||
});
|
||||
}
|
||||
|
||||
async function queryBranchesAndUpdateGroupsOnChannelAsync(
|
||||
args: ViewBranchesOnUpdateChannelQueryVariables
|
||||
): Promise<UpdateBranchOnChannelObject[]> {
|
||||
return await BranchQuery.listBranchesOnChannelAsync(args);
|
||||
}
|
||||
|
||||
function renderPageOfChannels(
|
||||
currentPage: UpdateChannelObject[],
|
||||
{ json }: PaginatedQueryOptions
|
||||
): void {
|
||||
if (json) {
|
||||
printJsonOnlyOutput({ currentPage });
|
||||
} else {
|
||||
for (const channel of currentPage) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: channel.name },
|
||||
{ label: 'ID', value: channel.id },
|
||||
])
|
||||
);
|
||||
logChannelDetails(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderPageOfBranchesOnChannel(
|
||||
channel: UpdateChannelObject,
|
||||
currentPage: UpdateBranchOnChannelObject[],
|
||||
{ json }: PaginatedQueryOptions
|
||||
): void {
|
||||
const channelWithNewBranches = { ...channel, updateBranches: currentPage };
|
||||
if (json) {
|
||||
printJsonOnlyOutput({ currentPage: channelWithNewBranches });
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: channel.name },
|
||||
{ label: 'ID', value: channel.id },
|
||||
])
|
||||
);
|
||||
logChannelDetails(channelWithNewBranches);
|
||||
}
|
||||
}
|
||||
|
||||
function renderChannelHeaderContent({
|
||||
channelName,
|
||||
channelId,
|
||||
}: {
|
||||
channelName: string;
|
||||
channelId: string;
|
||||
}): void {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Channel:'));
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: channelName },
|
||||
{ label: 'ID', value: channelId },
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk`{bold Branches pointed at this channel and their most recent update group:}`);
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import assert from 'assert';
|
||||
import CliTable from 'cli-table3';
|
||||
|
||||
import { UpdateChannelObject } from '../graphql/queries/ChannelQuery';
|
||||
import Log from '../log';
|
||||
import { UPDATE_COLUMNS, getUpdateGroupDescriptionsWithBranch } from '../update/utils';
|
||||
|
||||
export type BranchMapping = {
|
||||
version: number;
|
||||
data: {
|
||||
branchId: string;
|
||||
branchMappingLogic: {
|
||||
operand: number;
|
||||
clientKey: string;
|
||||
branchMappingOperator: string;
|
||||
} & string;
|
||||
}[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the branch mapping and determine whether it is a rollout.
|
||||
* Ensure that the branch mapping is properly formatted.
|
||||
*/
|
||||
export function getBranchMapping(branchMappingString?: string): {
|
||||
branchMapping: BranchMapping;
|
||||
isRollout: boolean;
|
||||
rolloutPercent?: number;
|
||||
} {
|
||||
if (!branchMappingString) {
|
||||
throw new Error('Missing branch mapping.');
|
||||
}
|
||||
let branchMapping: BranchMapping;
|
||||
try {
|
||||
branchMapping = JSON.parse(branchMappingString);
|
||||
} catch {
|
||||
throw new Error(`Could not parse branchMapping string into a JSON: "${branchMappingString}"`);
|
||||
}
|
||||
assert(branchMapping, 'Branch Mapping must be defined.');
|
||||
|
||||
if (branchMapping.version !== 0) {
|
||||
throw new Error('Branch mapping must be version 0.');
|
||||
}
|
||||
|
||||
const isRollout = branchMapping.data.length === 2;
|
||||
const rolloutPercent = branchMapping.data[0].branchMappingLogic.operand;
|
||||
|
||||
switch (branchMapping.data.length) {
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
if (branchMapping.data[0].branchMappingLogic !== 'true') {
|
||||
throw new Error('Branch mapping logic for a single branch must be "true"');
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if (branchMapping.data[0].branchMappingLogic.clientKey !== 'rolloutToken') {
|
||||
throw new Error('Client key of initial branch mapping must be "rolloutToken"');
|
||||
}
|
||||
if (branchMapping.data[0].branchMappingLogic.branchMappingOperator !== 'hash_lt') {
|
||||
throw new Error('Branch mapping operator of initial branch mapping must be "hash_lt"');
|
||||
}
|
||||
if (rolloutPercent == null) {
|
||||
throw new Error('Branch mapping is missing a "rolloutPercent"');
|
||||
}
|
||||
if (branchMapping.data[1].branchMappingLogic !== 'true') {
|
||||
throw new Error('Branch mapping logic for a the second branch of a rollout must be "true"');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error('Branch mapping data must have length less than or equal to 2.');
|
||||
}
|
||||
|
||||
return { branchMapping, isRollout, rolloutPercent };
|
||||
}
|
||||
|
||||
export function logChannelDetails(channel: UpdateChannelObject): void {
|
||||
const { branchMapping, isRollout, rolloutPercent } = getBranchMapping(channel.branchMapping);
|
||||
|
||||
const updateChannelsTable = new CliTable({
|
||||
head: ['branch', ...(isRollout ? ['rollout percent'] : []), ...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
if (branchMapping.data.length > 2) {
|
||||
throw new Error('Branch Mapping data must have length less than or equal to 2.');
|
||||
}
|
||||
|
||||
const rolloutBranchIds = branchMapping.data.map(data => data.branchId);
|
||||
|
||||
for (const currentBranch of channel.updateBranches) {
|
||||
const updateGroupDescriptions = getUpdateGroupDescriptionsWithBranch(
|
||||
currentBranch.updateGroups
|
||||
);
|
||||
|
||||
const isRolloutBranch = isRollout && rolloutBranchIds.includes(currentBranch.id);
|
||||
const isBaseBranch = rolloutBranchIds.length > 0 && rolloutBranchIds[0] === currentBranch.id;
|
||||
|
||||
updateGroupDescriptions.forEach(({ branch, message, runtimeVersion, group, platforms }) => {
|
||||
updateChannelsTable.push([
|
||||
branch,
|
||||
...(isRolloutBranch
|
||||
? [isBaseBranch ? `${rolloutPercent! * 100}%` : `${(1 - rolloutPercent!) * 100}%`]
|
||||
: []),
|
||||
message,
|
||||
runtimeVersion,
|
||||
group,
|
||||
platforms,
|
||||
]);
|
||||
});
|
||||
}
|
||||
Log.log(updateChannelsTable.toString());
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
|
||||
import * as PackageManagerUtils from '@expo/package-manager';
|
||||
import { Command } from '@oclif/core';
|
||||
import { Command, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Options, Ora } from 'ora';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
|
||||
@@ -12,142 +14,105 @@ import {
|
||||
initAsync as initAnalyticsAsync,
|
||||
logEvent,
|
||||
} from '../analytics/rudderstackClient';
|
||||
import { learnMore } from '../log';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { Actor, getUserAsync } from '../user/User';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { interactiveOra, nonInteractiveOra } from '../ora';
|
||||
import { findProjectRootAsync } from '../project/projectUtils';
|
||||
import { InteractivePrompts, NonInteractivePrompts, Prompts } from '../prompts';
|
||||
import { getUserAsync } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { setVcsClient } from '../vcs';
|
||||
import GitClient from '../vcs/clients/git';
|
||||
|
||||
export interface CommandConfiguration {
|
||||
/**
|
||||
* When user data is unavailable locally, determines if the command will
|
||||
* force the user to log in. By default, all commands require authentication.
|
||||
*/
|
||||
allowUnauthenticated?: boolean;
|
||||
|
||||
/**
|
||||
* Whether a command can be run outside of an Expo project directory.
|
||||
* By default, all commands must be run inside an Expo project directory.
|
||||
*/
|
||||
canRunOutsideProject?: boolean;
|
||||
}
|
||||
|
||||
export interface ContextOptions {
|
||||
nonInteractive: boolean;
|
||||
}
|
||||
|
||||
export abstract class ContextField<T> {
|
||||
abstract getValueAsync(options: ContextOptions): Promise<T>;
|
||||
}
|
||||
|
||||
export class ProjectIdContextField extends ContextField<string> {
|
||||
async getValueAsync({ nonInteractive }: ContextOptions): Promise<string> {
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
return await getProjectIdAsync(exp, { nonInteractive });
|
||||
}
|
||||
}
|
||||
|
||||
export class OptionalProjectIdContextField extends ContextField<string | undefined> {
|
||||
async getValueAsync({ nonInteractive }: ContextOptions): Promise<string | undefined> {
|
||||
const projectDir = await findProjectRootAsync();
|
||||
if (!projectDir) {
|
||||
return undefined;
|
||||
type CommandOptions =
|
||||
| {
|
||||
nonInteractive: true;
|
||||
json: true;
|
||||
}
|
||||
| {
|
||||
nonInteractive: boolean;
|
||||
json: false;
|
||||
};
|
||||
|
||||
const exp = getExpoConfig(projectDir);
|
||||
return await getProjectIdAsync(exp, { nonInteractive });
|
||||
export type CommandContext = Readonly<
|
||||
CommandOptions & {
|
||||
logger: Log;
|
||||
prompts: Prompts;
|
||||
ora: (options?: Options | string) => Ora;
|
||||
}
|
||||
}
|
||||
|
||||
export class ActorContextField extends ContextField<Actor> {
|
||||
async getValueAsync({ nonInteractive }: ContextOptions): Promise<Actor> {
|
||||
return await ensureLoggedInAsync({ nonInteractive });
|
||||
}
|
||||
}
|
||||
|
||||
export const EASCommandProjectIdContext = {
|
||||
projectId: new ProjectIdContextField(),
|
||||
};
|
||||
|
||||
export const EASCommandProjectIdIfProjectDirContext = {
|
||||
projectId: new OptionalProjectIdContextField(),
|
||||
};
|
||||
|
||||
export const EASCommandLoggedInContext = {
|
||||
actor: new ActorContextField(),
|
||||
};
|
||||
|
||||
type ContextInput<
|
||||
T extends {
|
||||
[name: string]: any;
|
||||
} = object
|
||||
> = {
|
||||
[P in keyof T]: ContextField<T[P]>;
|
||||
};
|
||||
|
||||
type ContextOutput<
|
||||
T extends {
|
||||
[name: string]: any;
|
||||
} = object
|
||||
> = {
|
||||
[P in keyof T]: T[P];
|
||||
};
|
||||
>;
|
||||
|
||||
export default abstract class EasCommand extends Command {
|
||||
static contextDefinition: ContextInput = {};
|
||||
static override globalFlags = {
|
||||
json: Flags.boolean({
|
||||
description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
|
||||
dependsOn: ['non-interactive'],
|
||||
}),
|
||||
'non-interactive': Flags.boolean({
|
||||
description: 'Run the command in non-interactive mode.',
|
||||
}),
|
||||
};
|
||||
|
||||
protected commandConfiguration: CommandConfiguration = {};
|
||||
// disable built in oclif json stuff
|
||||
enableJsonFlag = false;
|
||||
|
||||
protected async getContextAsync<
|
||||
C extends {
|
||||
[name: string]: any;
|
||||
} = object
|
||||
>(
|
||||
commandClass: { contextDefinition: ContextInput<C> },
|
||||
{ nonInteractive }: { nonInteractive: boolean }
|
||||
): Promise<ContextOutput<C>> {
|
||||
const contextDefinition = commandClass.contextDefinition;
|
||||
/**
|
||||
* When user data is unavailable locally, determines if the command will
|
||||
* force the user to log in
|
||||
*/
|
||||
protected requiresAuthentication = true;
|
||||
protected mustBeRunInsideProject = true;
|
||||
|
||||
const contextValuePairs = await Promise.all(
|
||||
Object.keys(contextDefinition).map(async contextKey => {
|
||||
return [contextKey, await contextDefinition[contextKey].getValueAsync({ nonInteractive })];
|
||||
})
|
||||
);
|
||||
|
||||
return Object.fromEntries(contextValuePairs);
|
||||
}
|
||||
|
||||
protected abstract runAsync(): Promise<any>;
|
||||
protected abstract runAsync({
|
||||
nonInteractive,
|
||||
json,
|
||||
logger,
|
||||
prompts: {
|
||||
promptAsync,
|
||||
confirmAsync,
|
||||
selectAsync,
|
||||
toggleConfirmAsync,
|
||||
pressAnyKeyToContinueAsync,
|
||||
},
|
||||
ora,
|
||||
}: CommandContext): Promise<{ jsonOutput: object }>;
|
||||
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
async run(): Promise<any> {
|
||||
const { flags } = await this.parse();
|
||||
const json = flags.json ?? false;
|
||||
const nonInteractive = (flags as any)['non-interactive'] ?? false;
|
||||
|
||||
await initAnalyticsAsync();
|
||||
|
||||
if (!this.commandConfiguration.canRunOutsideProject) {
|
||||
if (this.mustBeRunInsideProject) {
|
||||
const projectDir = await findProjectRootAsync();
|
||||
await this.applyCliConfigAsync(projectDir);
|
||||
await this.ensureEasCliIsNotInDependenciesAsync(projectDir);
|
||||
}
|
||||
|
||||
if (!this.commandConfiguration.allowUnauthenticated) {
|
||||
const { flags } = await this.parse();
|
||||
const nonInteractive = (flags as any)['non-interactive'] ?? false;
|
||||
if (this.requiresAuthentication) {
|
||||
await ensureLoggedInAsync({ nonInteractive });
|
||||
} else {
|
||||
await getUserAsync();
|
||||
}
|
||||
|
||||
logEvent(AnalyticsEvent.ACTION, {
|
||||
// id is assigned by oclif in constructor based on the filepath:
|
||||
// commands/submit === submit, commands/build/list === build:list
|
||||
action: `eas ${this.id}`,
|
||||
});
|
||||
|
||||
return this.runAsync();
|
||||
const jsonOutput = await this.runAsync({
|
||||
nonInteractive,
|
||||
json: json ?? false,
|
||||
logger: new Log(json),
|
||||
prompts: nonInteractive ? new InteractivePrompts() : new NonInteractivePrompts(),
|
||||
ora: nonInteractive ? nonInteractiveOra : interactiveOra,
|
||||
});
|
||||
|
||||
if (json) {
|
||||
this.printJsonOnlyOutput(jsonOutput);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
@@ -214,4 +179,26 @@ export default abstract class EasCommand extends Command {
|
||||
packageJson?.devDependencies?.['eas-cli'] !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
private printJsonOnlyOutput(value: object): void {
|
||||
try {
|
||||
new Log(false).log(JSON.stringify(this.sanitizeValue(value), null, 2));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
private sanitizeValue(value: any): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(val => this.sanitizeValue(val));
|
||||
} else if (value && typeof value === 'object') {
|
||||
const result: Record<string, any> = {};
|
||||
Object.keys(value).forEach(key => {
|
||||
if (key !== '__typename' && value[key] !== null) {
|
||||
result[key] = this.sanitizeValue(value[key]);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { flushAsync, initAsync, logEvent } from '../../analytics/rudderstackClie
|
||||
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
|
||||
import { getUserAsync } from '../../user/User';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import EasCommand, { CommandConfiguration } from '../EasCommand';
|
||||
import EasCommand from '../EasCommand';
|
||||
|
||||
jest.mock('../../user/User');
|
||||
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() }));
|
||||
@@ -43,9 +43,7 @@ beforeEach(() => {
|
||||
|
||||
const createTestEasCommand = (authValue: boolean): typeof EasCommand => {
|
||||
class TestEasCommand extends EasCommand {
|
||||
protected override commandConfiguration: CommandConfiguration = {
|
||||
allowUnauthenticated: !authValue,
|
||||
};
|
||||
override requiresAuthentication = authValue;
|
||||
|
||||
async runAsync(): Promise<void> {}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
export const EasNonInteractiveAndJsonFlags = {
|
||||
json: Flags.boolean({
|
||||
description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
|
||||
dependsOn: ['non-interactive'],
|
||||
}),
|
||||
'non-interactive': Flags.boolean({
|
||||
description: 'Run the command in non-interactive mode.',
|
||||
}),
|
||||
};
|
||||
|
||||
export const EASNonInteractiveFlag = {
|
||||
'non-interactive': Flags.boolean({
|
||||
description: 'Run the command in non-interactive mode.',
|
||||
}),
|
||||
};
|
||||
|
||||
export const EasJsonOnlyFlag = {
|
||||
json: Flags.boolean({
|
||||
description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
|
||||
}),
|
||||
};
|
||||
@@ -1,17 +1,16 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import { EasNonInteractiveAndJsonFlags } from './flags';
|
||||
|
||||
export const getPaginatedQueryOptions = (
|
||||
flags: Partial<
|
||||
Record<keyof typeof EasPaginatedQueryFlags | keyof typeof EasNonInteractiveAndJsonFlags, any>
|
||||
>
|
||||
): PaginatedQueryOptions => {
|
||||
export const getPaginatedQueryOptions = (flags: {
|
||||
json: boolean;
|
||||
'non-interactive': boolean;
|
||||
offset: number | undefined;
|
||||
limit: number | undefined;
|
||||
}): PaginatedQueryOptions => {
|
||||
return {
|
||||
...('limit' in flags && { limit: flags.limit }),
|
||||
json: flags.json,
|
||||
offset: flags.offset ?? 0,
|
||||
nonInteractive: flags['non-interactive'] ?? false,
|
||||
json: flags.json ?? false,
|
||||
nonInteractive: flags['non-interactive'],
|
||||
...('limit' in flags && { limit: flags.limit }),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -31,19 +30,6 @@ const parseFlagInputStringAsInteger = (
|
||||
return inputAsNumber;
|
||||
};
|
||||
|
||||
export const getLimitFlagWithCustomValues = ({
|
||||
defaultTo,
|
||||
limit,
|
||||
}: {
|
||||
defaultTo: number;
|
||||
limit: number;
|
||||
}): any =>
|
||||
Flags.integer({
|
||||
description: `The number of items to fetch each query. Defaults to ${defaultTo} and is capped at ${limit}.`,
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
parse: async input => parseFlagInputStringAsInteger(input, 'limit', 1, limit),
|
||||
});
|
||||
|
||||
export const EasPaginatedQueryFlags = {
|
||||
offset: Flags.integer({
|
||||
description: 'Start queries from specified index. Use for paginating results. Defaults to 0.',
|
||||
@@ -51,7 +37,12 @@ export const EasPaginatedQueryFlags = {
|
||||
parse: async input =>
|
||||
parseFlagInputStringAsInteger(input, 'offset', 0, Number.MAX_SAFE_INTEGER),
|
||||
}),
|
||||
limit: getLimitFlagWithCustomValues({ defaultTo: 50, limit: 100 }),
|
||||
limit: Flags.integer({
|
||||
description:
|
||||
'The number of query items to list at once. The default value is 50 (the maximum is 100). Using a lower value may help increase command speed.',
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
parse: async input => parseFlagInputStringAsInteger(input, 'limit', 1, 100),
|
||||
}),
|
||||
};
|
||||
|
||||
// options required to control a paginated query
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import EasCommand, { CommandConfiguration } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log from '../../log';
|
||||
import { showLoginPromptAsync } from '../../user/actions';
|
||||
|
||||
@@ -6,10 +6,8 @@ export default class AccountLogin extends EasCommand {
|
||||
static override description = 'log in with your Expo account';
|
||||
static override aliases = ['login'];
|
||||
|
||||
protected override commandConfiguration: CommandConfiguration = {
|
||||
allowUnauthenticated: true,
|
||||
canRunOutsideProject: true,
|
||||
};
|
||||
protected override mustBeRunInsideProject = false;
|
||||
protected override requiresAuthentication = false;
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
await showLoginPromptAsync();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import EasCommand, { CommandConfiguration } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log from '../../log';
|
||||
import { logoutAsync } from '../../user/User';
|
||||
|
||||
@@ -6,10 +6,8 @@ export default class AccountLogout extends EasCommand {
|
||||
static override description = 'log out';
|
||||
static override aliases = ['logout'];
|
||||
|
||||
protected override commandConfiguration: CommandConfiguration = {
|
||||
allowUnauthenticated: true,
|
||||
canRunOutsideProject: true,
|
||||
};
|
||||
protected override mustBeRunInsideProject = false;
|
||||
protected override requiresAuthentication = false;
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
await logoutAsync();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand, { CommandConfiguration } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log from '../../log';
|
||||
import { getActorDisplayName, getUserAsync } from '../../user/User';
|
||||
|
||||
@@ -8,10 +8,8 @@ export default class AccountView extends EasCommand {
|
||||
static override description = 'show the username you are logged in as';
|
||||
static override aliases = ['whoami'];
|
||||
|
||||
protected override commandConfiguration: CommandConfiguration = {
|
||||
allowUnauthenticated: true,
|
||||
canRunOutsideProject: true,
|
||||
};
|
||||
protected override mustBeRunInsideProject = false;
|
||||
protected override requiresAuthentication = false;
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const user = await getUserAsync();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import EasCommand, { CommandConfiguration } from '../commandUtils/EasCommand';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import Log from '../log';
|
||||
import UserSettings from '../user/UserSettings';
|
||||
|
||||
@@ -7,9 +7,7 @@ export default class AnalyticsView extends EasCommand {
|
||||
|
||||
static override args = [{ name: 'STATUS', options: ['on', 'off'] }];
|
||||
|
||||
protected override commandConfiguration: CommandConfiguration = {
|
||||
allowUnauthenticated: true,
|
||||
};
|
||||
protected override requiresAuthentication = false;
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { STATUS: status } = (await this.parse(AnalyticsView)).args;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { getDefaultBranchNameAsync } from '../../branch/utils';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
CreateUpdateBranchForAppMutation,
|
||||
@@ -11,9 +10,15 @@ import {
|
||||
UpdateBranch,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
|
||||
export async function createUpdateBranchOnAppAsync({
|
||||
appId,
|
||||
@@ -58,51 +63,49 @@ export default class BranchCreate extends EasCommand {
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
json: Flags.boolean({
|
||||
description: 'return a json with the new branch ID and name.',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
let {
|
||||
args: { name },
|
||||
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
|
||||
flags,
|
||||
} = await this.parse(BranchCreate);
|
||||
const { projectId } = await this.getContextAsync(BranchCreate, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
if (jsonFlag) {
|
||||
if (flags.json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!name) {
|
||||
const validationMessage = 'Branch name may not be empty.';
|
||||
if (nonInteractive) {
|
||||
if (flags.json) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ name } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Provide a branch name:',
|
||||
initial: await getDefaultBranchNameAsync(),
|
||||
initial:
|
||||
(await getVcsClient().getBranchNameAsync()) ||
|
||||
`branch-${Math.random().toString(36).substr(2, 4)}`,
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
const newBranch = await createUpdateBranchOnAppAsync({ appId: projectId, name });
|
||||
|
||||
if (jsonFlag) {
|
||||
if (flags.json) {
|
||||
printJsonOnlyOutput(newBranch);
|
||||
} else {
|
||||
Log.withTick(
|
||||
`️Created a new branch: ${chalk.bold(newBranch.name)} on project ${chalk.bold(
|
||||
projectDisplayName
|
||||
)}.`
|
||||
`️Created a new branch: ${chalk.bold(newBranch.name)} on project ${chalk.bold(fullName)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import EasCommand, { CommandContext } from '../../commandUtils/EasCommand';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
DeleteUpdateBranchMutation,
|
||||
@@ -13,10 +10,12 @@ import {
|
||||
GetBranchInfoQuery,
|
||||
GetBranchInfoQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
|
||||
async function getBranchInfoAsync({
|
||||
appId,
|
||||
@@ -76,10 +75,6 @@ async function deleteBranchOnAppAsync({
|
||||
export default class BranchDelete extends EasCommand {
|
||||
static override description = 'delete a branch';
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'name',
|
||||
@@ -88,54 +83,49 @@ export default class BranchDelete extends EasCommand {
|
||||
},
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
protected async runAsync({
|
||||
nonInteractive,
|
||||
logger,
|
||||
prompts: { promptAsync, toggleConfirmAsync },
|
||||
}: CommandContext): Promise<{ jsonOutput: object }> {
|
||||
let {
|
||||
args: { name: branchName },
|
||||
flags,
|
||||
args: { name },
|
||||
} = await this.parse(BranchDelete);
|
||||
const { json: jsonFlag, 'non-interactive': nonInteractive } = flags;
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
const { projectId } = await this.getContextAsync(BranchDelete, { nonInteractive });
|
||||
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
|
||||
|
||||
if (!branchName) {
|
||||
if (!name) {
|
||||
const validationMessage = 'branch name may not be empty.';
|
||||
if (nonInteractive) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ name: branchName } = await selectBranchOnAppAsync({
|
||||
projectId,
|
||||
displayTextForListItem: updateBranch => updateBranch.name,
|
||||
promptTitle: 'Which branch would you like to delete?',
|
||||
paginatedQueryOptions,
|
||||
({ name } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Provide the name of the branch to delete:',
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
const data = await getBranchInfoAsync({ appId: projectId, name: branchName });
|
||||
const data = await getBranchInfoAsync({ appId: projectId, name });
|
||||
const branchId = data.app?.byId.updateBranchByName?.id;
|
||||
if (!branchId) {
|
||||
throw new Error(`Could not find branch ${branchName} on ${projectDisplayName}`);
|
||||
throw new Error(`Could not find branch ${name} on ${fullName}`);
|
||||
}
|
||||
|
||||
if (!nonInteractive) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`You are about to permanently delete branch: "${branchName}" and all of the updates published on it.` +
|
||||
logger.addNewLineIfNone();
|
||||
logger.warn(
|
||||
`You are about to permanently delete branch: "${name}" and all of the updates published on it.` +
|
||||
`\nThis action is irreversible.`
|
||||
);
|
||||
Log.newLine();
|
||||
logger.newLine();
|
||||
const confirmed = await toggleConfirmAsync({ message: 'Are you sure you wish to proceed?' });
|
||||
if (!confirmed) {
|
||||
Log.error(`Cancelled deletion of branch: "${branchName}".`);
|
||||
logger.error(`Cancelled deletion of branch: "${name}".`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -144,14 +134,10 @@ export default class BranchDelete extends EasCommand {
|
||||
branchId,
|
||||
});
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput(deletionResult);
|
||||
} else {
|
||||
Log.withTick(
|
||||
`️Deleted branch "${branchName}" and all of its updates on project ${chalk.bold(
|
||||
projectDisplayName
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
logger.withTick(
|
||||
`Deleted branch "${name}" and all of its updates on project ${chalk.bold(fullName)}.`
|
||||
);
|
||||
|
||||
return { jsonOutput: deletionResult };
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { listAndRenderBranchesOnAppAsync } from '../../branch/queries';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { listAndRenderPaginatedBranchesAsync } from '../../branch/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasPaginatedQueryFlags, getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
|
||||
export default class BranchList extends EasCommand {
|
||||
@@ -9,24 +10,19 @@ export default class BranchList extends EasCommand {
|
||||
|
||||
static override flags = {
|
||||
...EasPaginatedQueryFlags,
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BranchList);
|
||||
const { projectId } = await this.getContextAsync(BranchList, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
});
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
const options = getPaginatedQueryOptions(flags);
|
||||
|
||||
if (paginatedQueryOptions.json) {
|
||||
if (options.json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
await listAndRenderBranchesOnAppAsync({ projectId, paginatedQueryOptions });
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
await listAndRenderPaginatedBranchesAsync(projectId, options);
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,7 @@ import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
EditUpdateBranchInput,
|
||||
@@ -12,7 +11,12 @@ import {
|
||||
UpdateBranch,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
@@ -59,29 +63,28 @@ export default class BranchRename extends EasCommand {
|
||||
description: 'new name of the branch.',
|
||||
required: false,
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
json: Flags.boolean({
|
||||
description: `return a json with the edited branch's ID and name.`,
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
let {
|
||||
flags: { json: jsonFlag, from: currentName, to: newName, 'non-interactive': nonInteractive },
|
||||
flags: { json: jsonFlag, from: currentName, to: newName },
|
||||
} = await this.parse(BranchRename);
|
||||
const { projectId } = await this.getContextAsync(BranchRename, {
|
||||
nonInteractive,
|
||||
});
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!currentName) {
|
||||
const validationMessage = 'current name may not be empty.';
|
||||
if (nonInteractive) {
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ currentName } = await promptAsync({
|
||||
@@ -94,7 +97,7 @@ export default class BranchRename extends EasCommand {
|
||||
|
||||
if (!newName) {
|
||||
const validationMessage = 'new name may not be empty.';
|
||||
if (nonInteractive) {
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ newName } = await promptAsync({
|
||||
@@ -117,7 +120,7 @@ export default class BranchRename extends EasCommand {
|
||||
Log.withTick(
|
||||
`️Renamed branch from ${currentName} to ${chalk.bold(
|
||||
editedBranch.name
|
||||
)} on project ${chalk.bold(projectDisplayName)}.`
|
||||
)} on project ${chalk.bold(fullName)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasPaginatedQueryFlags,
|
||||
getLimitFlagWithCustomValues,
|
||||
getPaginatedQueryOptions,
|
||||
} from '../../commandUtils/pagination';
|
||||
import { listAndRenderUpdateGroupsOnBranchAsync } from '../../update/queries';
|
||||
import { selectBranchFromPaginatedQueryAsync } from '../../branch/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasPaginatedQueryFlags, getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { listAndRenderUpdatesOnBranchByNameAsync } from '../../update/queries';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
|
||||
export default class BranchView extends EasCommand {
|
||||
@@ -22,12 +19,6 @@ export default class BranchView extends EasCommand {
|
||||
|
||||
static override flags = {
|
||||
...EasPaginatedQueryFlags,
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
limit: getLimitFlagWithCustomValues({ defaultTo: 25, limit: 50 }),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -35,38 +26,30 @@ export default class BranchView extends EasCommand {
|
||||
args: { name: branchName },
|
||||
flags,
|
||||
} = await this.parse(BranchView);
|
||||
const { 'non-interactive': nonInteractive } = flags;
|
||||
const { projectId } = await this.getContextAsync(BranchView, {
|
||||
nonInteractive,
|
||||
});
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
const options = getPaginatedQueryOptions(flags);
|
||||
|
||||
if (paginatedQueryOptions.json) {
|
||||
if (options.json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
// provide help to a user if they ran the command with missing args
|
||||
if (!branchName) {
|
||||
if (nonInteractive) {
|
||||
if (options.nonInteractive) {
|
||||
throw new Error('Branch name may not be empty.');
|
||||
}
|
||||
|
||||
({ name: branchName } = await selectBranchOnAppAsync({
|
||||
({ name: branchName } = await selectBranchFromPaginatedQueryAsync(
|
||||
projectId,
|
||||
promptTitle: 'Which branch would you like to view?',
|
||||
displayTextForListItem: updateBranch => updateBranch.name,
|
||||
// discard limit and offset because this query is not their intended target
|
||||
paginatedQueryOptions: {
|
||||
json: paginatedQueryOptions.json,
|
||||
nonInteractive,
|
||||
offset: 0,
|
||||
},
|
||||
}));
|
||||
'Which branch would you like to view?',
|
||||
// discard limit and offset because this query is not those flag's intended target
|
||||
{ json: options.json, nonInteractive: options.nonInteractive, offset: 0 }
|
||||
));
|
||||
}
|
||||
|
||||
await listAndRenderUpdateGroupsOnBranchAsync({
|
||||
projectId,
|
||||
branchName,
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
await listAndRenderUpdatesOnBranchByNameAsync(projectId, branchName, options);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
Build,
|
||||
@@ -14,7 +13,12 @@ import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { appPlatformEmojis } from '../../platform';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { confirmAsync, selectAsync } from '../../prompts';
|
||||
|
||||
async function cancelBuildAsync(buildId: string): Promise<Pick<Build, 'id' | 'status'>> {
|
||||
@@ -55,45 +59,36 @@ function formatUnfinishedBuild(
|
||||
return `${platform} Started at: ${startTime}, Status: ${status}, Id: ${build.id}`;
|
||||
}
|
||||
|
||||
export async function selectBuildToCancelAsync(
|
||||
async function selectBuildToCancelAsync(
|
||||
projectId: string,
|
||||
projectDisplayName: string
|
||||
projectFullName: string
|
||||
): Promise<string | null> {
|
||||
const spinner = ora().start('Fetching the uncompleted builds…');
|
||||
|
||||
let builds;
|
||||
try {
|
||||
const [newBuilds, inQueueBuilds, inProgressBuilds] = await Promise.all([
|
||||
BuildQuery.viewBuildsOnAppAsync({
|
||||
appId: projectId,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
filter: { status: BuildStatus.New },
|
||||
}),
|
||||
BuildQuery.viewBuildsOnAppAsync({
|
||||
appId: projectId,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
filter: { status: BuildStatus.InQueue },
|
||||
}),
|
||||
BuildQuery.viewBuildsOnAppAsync({
|
||||
appId: projectId,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
filter: { status: BuildStatus.InProgress },
|
||||
}),
|
||||
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.New } }),
|
||||
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.InQueue } }),
|
||||
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.InProgress } }),
|
||||
]);
|
||||
spinner.stop();
|
||||
builds = [...newBuilds, ...inQueueBuilds, ...inProgressBuilds];
|
||||
} catch (error) {
|
||||
spinner.fail(
|
||||
`Something went wrong and we couldn't fetch the builds for the project ${projectDisplayName}.`
|
||||
`Something went wrong and we couldn't fetch the builds for the project ${projectFullName}.`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
if (builds.length === 0) {
|
||||
Log.warn(`There aren't any uncompleted builds for the project ${projectDisplayName}.`);
|
||||
Log.warn(`There aren't any uncompleted builds for the project ${projectFullName}.`);
|
||||
return null;
|
||||
} else if (builds.length === 1) {
|
||||
Log.log('Found one build');
|
||||
Log.log(formatUnfinishedBuild(builds[0]));
|
||||
await confirmAsync({
|
||||
message: 'Do you want to cancel it?',
|
||||
});
|
||||
return builds[0].id;
|
||||
} else {
|
||||
const buildId = await selectAsync<string>(
|
||||
'Which build do you want to cancel?',
|
||||
@@ -102,12 +97,7 @@ export async function selectBuildToCancelAsync(
|
||||
value: build.id,
|
||||
}))
|
||||
);
|
||||
|
||||
return (await confirmAsync({
|
||||
message: 'Are you sure you want to cancel it?',
|
||||
}))
|
||||
? buildId
|
||||
: null;
|
||||
return buildId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,39 +114,21 @@ export default class BuildCancel extends EasCommand {
|
||||
|
||||
static override args = [{ name: 'BUILD_ID' }];
|
||||
|
||||
static override flags = {
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
args: { BUILD_ID: buildIdFromArg },
|
||||
flags: { 'non-interactive': nonInteractive },
|
||||
} = await this.parse(BuildCancel);
|
||||
const { projectId } = await this.getContextAsync(BuildCancel, {
|
||||
nonInteractive,
|
||||
});
|
||||
const { BUILD_ID: buildIdFromArg } = (await this.parse(BuildCancel)).args;
|
||||
|
||||
const displayName = await getDisplayNameForProjectIdAsync(projectId);
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectFullName = await getProjectFullNameAsync(exp);
|
||||
|
||||
if (buildIdFromArg) {
|
||||
await ensureBuildExistsAsync(buildIdFromArg);
|
||||
}
|
||||
|
||||
let buildId: string | null = buildIdFromArg;
|
||||
const buildId = buildIdFromArg || (await selectBuildToCancelAsync(projectId, projectFullName));
|
||||
if (!buildId) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('BUILD_ID must not be empty in non-interactive mode');
|
||||
}
|
||||
|
||||
buildId = await selectBuildToCancelAsync(projectId, displayName);
|
||||
if (!buildId) {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const spinner = ora().start('Canceling the build…');
|
||||
|
||||
@@ -4,7 +4,7 @@ import chalk from 'chalk';
|
||||
|
||||
import { cleanUpOldEasBuildGradleScriptAsync } from '../../build/android/syncProjectConfiguration';
|
||||
import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
@@ -26,15 +26,8 @@ export default class BuildConfigure extends EasCommand {
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BuildConfigure);
|
||||
const { projectId } = await this.getContextAsync(BuildConfigure, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
Log.log(
|
||||
'💡 The following process will configure your iOS and/or Android project to be compatible with EAS Build. These changes only apply to your local project files and you can safely revert them at any time.'
|
||||
@@ -67,14 +60,14 @@ export default class BuildConfigure extends EasCommand {
|
||||
if ([RequestedPlatform.Android, RequestedPlatform.All].includes(platform)) {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
await syncAndroidUpdatesConfigurationAsync(projectDir, exp, projectId);
|
||||
await syncAndroidUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
}
|
||||
|
||||
if ([RequestedPlatform.Ios, RequestedPlatform.All].includes(platform)) {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
await syncIosUpdatesConfigurationAsync(projectDir, exp, projectId);
|
||||
await syncIosUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,7 @@ import path from 'path';
|
||||
|
||||
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
|
||||
import { UserInputResourceClass } from '../../build/types';
|
||||
import EasCommand, {
|
||||
EASCommandLoggedInContext,
|
||||
EASCommandProjectIdContext,
|
||||
} from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { StatuspageServiceName } from '../../graphql/generated';
|
||||
import Log, { link } from '../../log';
|
||||
import { RequestedPlatform, selectRequestedPlatformAsync } from '../../platform';
|
||||
@@ -50,6 +46,10 @@ export default class Build extends EasCommand {
|
||||
default: false,
|
||||
hidden: true,
|
||||
}),
|
||||
json: Flags.boolean({
|
||||
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
|
||||
default: false,
|
||||
}),
|
||||
'skip-project-configuration': Flags.boolean({
|
||||
default: false,
|
||||
hidden: true,
|
||||
@@ -60,6 +60,10 @@ export default class Build extends EasCommand {
|
||||
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
|
||||
helpValue: 'PROFILE_NAME',
|
||||
}),
|
||||
'non-interactive': Flags.boolean({
|
||||
default: false,
|
||||
description: 'Run command in non-interactive mode',
|
||||
}),
|
||||
local: Flags.boolean({
|
||||
default: false,
|
||||
description: 'Run build locally [experimental]',
|
||||
@@ -96,12 +100,6 @@ export default class Build extends EasCommand {
|
||||
char: 'm',
|
||||
description: 'A short message describing the build',
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandLoggedInContext,
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -112,10 +110,6 @@ export default class Build extends EasCommand {
|
||||
}
|
||||
const flags = this.sanitizeFlags(rawFlags);
|
||||
|
||||
const { actor, projectId } = await this.getContextAsync(Build, {
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
|
||||
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
|
||||
@@ -130,7 +124,7 @@ export default class Build extends EasCommand {
|
||||
|
||||
const flagsWithPlatform = await this.ensurePlatformSelectedAsync(flags);
|
||||
|
||||
await runBuildAndSubmitAsync(projectDir, flagsWithPlatform, actor, projectId);
|
||||
await runBuildAndSubmitAsync(projectDir, flagsWithPlatform);
|
||||
}
|
||||
|
||||
private sanitizeFlags(
|
||||
|
||||
@@ -5,10 +5,7 @@ import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
|
||||
import EasCommand, {
|
||||
EASCommandLoggedInContext,
|
||||
EASCommandProjectIdContext,
|
||||
} from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
@@ -65,17 +62,8 @@ export default class BuildInspect extends EasCommand {
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandLoggedInContext,
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BuildInspect);
|
||||
const { actor, projectId } = await this.getContextAsync(BuildInspect, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const outputDirectory = path.resolve(process.cwd(), flags.output);
|
||||
const tmpWorkingdir = path.join(getTmpDirectory(), uuidv4());
|
||||
|
||||
@@ -95,28 +83,23 @@ export default class BuildInspect extends EasCommand {
|
||||
} else {
|
||||
const projectDir = await findProjectRootAsync();
|
||||
try {
|
||||
await runBuildAndSubmitAsync(
|
||||
projectDir,
|
||||
{
|
||||
nonInteractive: false,
|
||||
wait: true,
|
||||
clearCache: false,
|
||||
json: false,
|
||||
autoSubmit: false,
|
||||
requestedPlatform: flags.platform,
|
||||
profile: flags.profile,
|
||||
localBuildOptions: {
|
||||
enable: true,
|
||||
...(flags.stage === InspectStage.PRE_BUILD ? { skipNativeBuild: true } : {}),
|
||||
...(flags.stage === InspectStage.POST_BUILD ? { skipCleanup: true } : {}),
|
||||
verbose: flags.verbose,
|
||||
workingdir: tmpWorkingdir,
|
||||
artifactsDir: path.join(tmpWorkingdir, 'artifacts'),
|
||||
},
|
||||
await runBuildAndSubmitAsync(projectDir, {
|
||||
nonInteractive: false,
|
||||
wait: true,
|
||||
clearCache: false,
|
||||
json: false,
|
||||
autoSubmit: false,
|
||||
requestedPlatform: flags.platform,
|
||||
profile: flags.profile,
|
||||
localBuildOptions: {
|
||||
enable: true,
|
||||
...(flags.stage === InspectStage.PRE_BUILD ? { skipNativeBuild: true } : {}),
|
||||
...(flags.stage === InspectStage.POST_BUILD ? { skipCleanup: true } : {}),
|
||||
verbose: flags.verbose,
|
||||
workingdir: tmpWorkingdir,
|
||||
artifactsDir: path.join(tmpWorkingdir, 'artifacts'),
|
||||
},
|
||||
actor,
|
||||
projectId
|
||||
);
|
||||
});
|
||||
if (!flags.verbose) {
|
||||
Log.log(chalk.green('Build successful'));
|
||||
}
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { BUILDS_LIMIT, listAndRenderBuildsOnAppAsync } from '../../build/queries';
|
||||
import { BuildDistributionType, BuildStatus } from '../../build/types';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasPaginatedQueryFlags,
|
||||
getLimitFlagWithCustomValues,
|
||||
getPaginatedQueryOptions,
|
||||
} from '../../commandUtils/pagination';
|
||||
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import {
|
||||
AppPlatform,
|
||||
DistributionType,
|
||||
BuildStatus as GraphQLBuildStatus,
|
||||
} from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export default class BuildList extends EasCommand {
|
||||
static override description = 'list all builds for your project';
|
||||
@@ -25,6 +28,9 @@ export default class BuildList extends EasCommand {
|
||||
platform: Flags.enum({
|
||||
options: [RequestedPlatform.All, RequestedPlatform.Android, RequestedPlatform.Ios],
|
||||
}),
|
||||
json: Flags.boolean({
|
||||
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
|
||||
}),
|
||||
status: Flags.enum({
|
||||
options: [
|
||||
BuildStatus.NEW,
|
||||
@@ -50,55 +56,76 @@ export default class BuildList extends EasCommand {
|
||||
appIdentifier: Flags.string(),
|
||||
buildProfile: Flags.string(),
|
||||
gitCommitHash: Flags.string(),
|
||||
...EasPaginatedQueryFlags,
|
||||
limit: getLimitFlagWithCustomValues({ defaultTo: 10, limit: BUILDS_LIMIT }),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
limit: Flags.integer(),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BuildList);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
const {
|
||||
json: jsonFlag,
|
||||
json,
|
||||
platform: requestedPlatform,
|
||||
status: buildStatus,
|
||||
distribution: buildDistribution,
|
||||
'non-interactive': nonInteractive,
|
||||
limit = 10,
|
||||
} = flags;
|
||||
const { projectId } = await this.getContextAsync(BuildList, {
|
||||
nonInteractive,
|
||||
});
|
||||
if (jsonFlag) {
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const platform = toAppPlatform(requestedPlatform);
|
||||
const graphqlBuildStatus = toGraphQLBuildStatus(buildStatus);
|
||||
const graphqlBuildDistribution = toGraphQLBuildDistribution(buildDistribution);
|
||||
const displayName = await getDisplayNameForProjectIdAsync(projectId);
|
||||
|
||||
await listAndRenderBuildsOnAppAsync({
|
||||
projectId,
|
||||
projectDisplayName: displayName,
|
||||
filter: {
|
||||
platform,
|
||||
status: graphqlBuildStatus,
|
||||
distribution: graphqlBuildDistribution,
|
||||
channel: flags.channel,
|
||||
appVersion: flags.appVersion,
|
||||
appBuildVersion: flags.appBuildVersion,
|
||||
sdkVersion: flags.sdkVersion,
|
||||
runtimeVersion: flags.runtimeVersion,
|
||||
appIdentifier: flags.appIdentifier,
|
||||
buildProfile: flags.buildProfile,
|
||||
gitCommitHash: flags.gitCommitHash,
|
||||
},
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectName = await getProjectFullNameAsync(exp);
|
||||
|
||||
const spinner = ora().start('Fetching the build list for the project…');
|
||||
|
||||
try {
|
||||
const builds = await BuildQuery.allForAppAsync(projectId, {
|
||||
limit,
|
||||
filter: {
|
||||
platform,
|
||||
status: graphqlBuildStatus,
|
||||
distribution: graphqlBuildDistribution,
|
||||
channel: flags.channel,
|
||||
appVersion: flags.appVersion,
|
||||
appBuildVersion: flags.appBuildVersion,
|
||||
sdkVersion: flags.sdkVersion,
|
||||
runtimeVersion: flags.runtimeVersion,
|
||||
appIdentifier: flags.appIdentifier,
|
||||
buildProfile: flags.buildProfile,
|
||||
gitCommitHash: flags.gitCommitHash,
|
||||
},
|
||||
});
|
||||
|
||||
if (builds.length) {
|
||||
if (platform || graphqlBuildStatus) {
|
||||
spinner.succeed(
|
||||
`Showing ${builds.length} matching builds for the project ${projectName}`
|
||||
);
|
||||
} else {
|
||||
spinner.succeed(`Showing last ${builds.length} builds for the project ${projectName}`);
|
||||
}
|
||||
|
||||
if (json) {
|
||||
printJsonOnlyOutput(builds);
|
||||
} else {
|
||||
const list = builds
|
||||
.map(build => formatGraphQLBuild(build))
|
||||
.join(`\n\n${chalk.dim('———')}\n\n`);
|
||||
|
||||
Log.log(`\n${list}`);
|
||||
}
|
||||
} else {
|
||||
spinner.fail(`Couldn't find any builds for the project ${projectName}`);
|
||||
}
|
||||
} catch (e) {
|
||||
spinner.fail(`Something went wrong and we couldn't fetch the build list ${projectName}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,7 @@ import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand, {
|
||||
EASCommandLoggedInContext,
|
||||
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';
|
||||
@@ -19,7 +16,8 @@ import { getExpoConfig } from '../../../project/expoConfig';
|
||||
import { BUILD_NUMBER_REQUIREMENTS, isValidBuildNumber } from '../../../project/ios/versions';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getDisplayNameForProjectIdAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../../project/projectUtils';
|
||||
import {
|
||||
ensureVersionSourceIsRemoteAsync,
|
||||
@@ -44,16 +42,8 @@ export default class BuildVersionSetView extends EasCommand {
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandLoggedInContext,
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
public async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BuildVersionSetView);
|
||||
const { actor, projectId } = await this.getContextAsync(BuildVersionSetView, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
|
||||
@@ -67,16 +57,15 @@ export default class BuildVersionSetView extends EasCommand {
|
||||
);
|
||||
|
||||
const exp = getExpoConfig(projectDir, { env: profile.env });
|
||||
const displayName = await getDisplayNameForProjectIdAsync(projectId);
|
||||
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectFullName = await getProjectFullNameAsync(exp);
|
||||
validateAppConfigForRemoteVersionSource(exp, platform);
|
||||
|
||||
const applicationIdentifier = await getApplicationIdentifierAsync(
|
||||
projectDir,
|
||||
exp,
|
||||
profile,
|
||||
platform,
|
||||
actor
|
||||
platform
|
||||
);
|
||||
const remoteVersions = await AppVersionQuery.latestVersionAsync(
|
||||
projectId,
|
||||
@@ -84,12 +73,12 @@ export default class BuildVersionSetView extends EasCommand {
|
||||
applicationIdentifier
|
||||
);
|
||||
const currentStateMessage = remoteVersions?.buildVersion
|
||||
? `Project ${chalk.bold(displayName)} with ${getApplicationIdentifierName(
|
||||
? `Project ${chalk.bold(projectFullName)} with ${getApplicationIdentifierName(
|
||||
platform
|
||||
)} "${applicationIdentifier}" is configured with ${getBuildVersionName(platform)} ${
|
||||
remoteVersions.buildVersion
|
||||
}.`
|
||||
: `Project ${chalk.bold(displayName)} with ${getApplicationIdentifierName(
|
||||
: `Project ${chalk.bold(projectFullName)} with ${getApplicationIdentifierName(
|
||||
platform
|
||||
)} "${applicationIdentifier}" does not have any ${getBuildVersionName(
|
||||
platform
|
||||
|
||||
@@ -6,10 +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,
|
||||
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';
|
||||
@@ -25,7 +22,7 @@ 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';
|
||||
import { findProjectRootAsync } from '../../../project/projectUtils';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../../project/projectUtils';
|
||||
import {
|
||||
ensureVersionSourceIsRemoteAsync,
|
||||
getBuildVersionName,
|
||||
@@ -59,16 +56,8 @@ export default class BuildVersionSyncView extends EasCommand {
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandLoggedInContext,
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
public async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BuildVersionSyncView);
|
||||
const { actor, projectId } = await this.getContextAsync(BuildVersionSyncView, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
const projectDir = await findProjectRootAsync();
|
||||
|
||||
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
|
||||
@@ -84,7 +73,7 @@ export default class BuildVersionSyncView extends EasCommand {
|
||||
});
|
||||
for (const profileInfo of buildProfiles) {
|
||||
const exp = getExpoConfig(projectDir, { env: profileInfo.profile.env });
|
||||
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
validateAppConfigForRemoteVersionSource(exp, profileInfo.platform);
|
||||
const platformDisplayName = appPlatformDisplayNames[toAppPlatform(profileInfo.platform)];
|
||||
|
||||
@@ -92,8 +81,7 @@ export default class BuildVersionSyncView extends EasCommand {
|
||||
projectDir,
|
||||
exp,
|
||||
profileInfo.profile,
|
||||
profileInfo.platform,
|
||||
actor
|
||||
profileInfo.platform
|
||||
);
|
||||
const remoteVersions = await AppVersionQuery.latestVersionAsync(
|
||||
projectId,
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EasJsonOnlyFlag } from '../../commandUtils/flags';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { BuildFragment } from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export default class BuildView extends EasCommand {
|
||||
@@ -14,11 +20,9 @@ export default class BuildView extends EasCommand {
|
||||
static override args = [{ name: 'BUILD_ID' }];
|
||||
|
||||
static override flags = {
|
||||
...EasJsonOnlyFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
json: Flags.boolean({
|
||||
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -26,14 +30,14 @@ export default class BuildView extends EasCommand {
|
||||
args: { BUILD_ID: buildId },
|
||||
flags,
|
||||
} = await this.parse(BuildView);
|
||||
const { projectId } = await this.getContextAsync(BuildView, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
if (flags.json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const displayName = await getDisplayNameForProjectIdAsync(projectId);
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectName = await getProjectFullNameAsync(exp);
|
||||
|
||||
const spinner = ora().start('Fetching the build…');
|
||||
|
||||
@@ -43,22 +47,18 @@ export default class BuildView extends EasCommand {
|
||||
if (buildId) {
|
||||
build = await BuildQuery.byIdAsync(buildId);
|
||||
} else {
|
||||
const builds = await BuildQuery.viewBuildsOnAppAsync({
|
||||
appId: projectId,
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
});
|
||||
const builds = await BuildQuery.allForAppAsync(projectId, { limit: 1 });
|
||||
if (builds.length === 0) {
|
||||
spinner.fail(`Couldn't find any builds for the project ${displayName}`);
|
||||
spinner.fail(`Couldn't find any builds for the project ${projectName}`);
|
||||
return;
|
||||
}
|
||||
build = builds[0];
|
||||
}
|
||||
|
||||
if (buildId) {
|
||||
spinner.succeed(`Found a matching build for the project ${displayName}`);
|
||||
spinner.succeed(`Found a matching build for the project ${projectName}`);
|
||||
} else {
|
||||
spinner.succeed(`Showing the last build for the project ${displayName}`);
|
||||
spinner.succeed(`Showing the last build for the project ${projectName}`);
|
||||
}
|
||||
|
||||
if (flags.json) {
|
||||
@@ -71,7 +71,7 @@ export default class BuildView extends EasCommand {
|
||||
spinner.fail(`Something went wrong and we couldn't fetch the build with id ${buildId}`);
|
||||
} else {
|
||||
spinner.fail(
|
||||
`Something went wrong and we couldn't fetch the last build for the project ${displayName}`
|
||||
`Something went wrong and we couldn't fetch the last build for the project ${projectName}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { BranchNotFoundError } from '../../branch/utils';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
CreateUpdateChannelOnAppMutation,
|
||||
@@ -11,7 +10,12 @@ import {
|
||||
} from '../../graphql/generated';
|
||||
import { BranchQuery } from '../../graphql/queries/BranchQuery';
|
||||
import Log from '../../log';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
@@ -67,28 +71,29 @@ export default class ChannelCreate extends EasCommand {
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
json: Flags.boolean({
|
||||
description:
|
||||
'print output as a JSON object with the new channel ID, name and branch mapping.',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
let {
|
||||
args: { name: channelName },
|
||||
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
|
||||
flags: { json: jsonFlag },
|
||||
} = await this.parse(ChannelCreate);
|
||||
const { projectId } = await this.getContextAsync(ChannelCreate, {
|
||||
nonInteractive,
|
||||
});
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!channelName) {
|
||||
const validationMessage = 'Channel name may not be empty.';
|
||||
if (nonInteractive) {
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ name: channelName } = await promptAsync({
|
||||
@@ -101,25 +106,21 @@ export default class ChannelCreate extends EasCommand {
|
||||
|
||||
let branchId: string;
|
||||
let branchMessage: string;
|
||||
const existingBranch = await BranchQuery.getBranchByNameAsync({
|
||||
appId: projectId,
|
||||
name: channelName,
|
||||
});
|
||||
|
||||
try {
|
||||
const branch = await BranchQuery.getBranchByNameAsync({
|
||||
if (existingBranch) {
|
||||
branchId = existingBranch.id;
|
||||
branchMessage = `We found a branch with the same name`;
|
||||
} else {
|
||||
const newBranch = await createUpdateBranchOnAppAsync({
|
||||
appId: projectId,
|
||||
name: channelName,
|
||||
});
|
||||
branchId = branch.id;
|
||||
branchMessage = `We found a branch with the same name`;
|
||||
} catch (error) {
|
||||
if (error instanceof BranchNotFoundError) {
|
||||
const newBranch = await createUpdateBranchOnAppAsync({
|
||||
appId: projectId,
|
||||
name: channelName,
|
||||
});
|
||||
branchId = newBranch.id;
|
||||
branchMessage = `We also went ahead and made a branch with the same name`;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
branchId = newBranch.id;
|
||||
branchMessage = `We also went ahead and made a branch with the same name`;
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -132,7 +133,7 @@ export default class ChannelCreate extends EasCommand {
|
||||
|
||||
if (!newChannel) {
|
||||
throw new Error(
|
||||
`Could not create channel with name ${channelName} on project with id ${projectId}`
|
||||
`Could not create channel with name ${channelName} on app with id ${projectId}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -141,9 +142,7 @@ export default class ChannelCreate extends EasCommand {
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.withTick(
|
||||
`Created a new channel on project ${chalk.bold(
|
||||
await getDisplayNameForProjectIdAsync(projectId)
|
||||
)}`
|
||||
`Created a new channel on project ${chalk.bold(await getProjectFullNameAsync(exp))}`
|
||||
);
|
||||
Log.log(
|
||||
formatFields([
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { selectChannelOnAppAsync } from '../../channel/queries';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
DeleteUpdateChannelMutation,
|
||||
DeleteUpdateChannelMutationVariables,
|
||||
DeleteUpdateChannelResult,
|
||||
GetChannelInfoQuery,
|
||||
GetChannelInfoQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
|
||||
import Log from '../../log';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export default class ChannelDelete extends EasCommand {
|
||||
@@ -26,59 +32,65 @@ export default class ChannelDelete extends EasCommand {
|
||||
},
|
||||
];
|
||||
static override flags = {
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
json: Flags.boolean({
|
||||
description: 'print output as a JSON object',
|
||||
default: false,
|
||||
}),
|
||||
'non-interactive': Flags.boolean({
|
||||
default: false,
|
||||
description: 'Run command in non-interactive mode',
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
args: { name: nameArg },
|
||||
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
|
||||
flags: { json: jsonFlag, 'non-interactive': nonInteractiveFlag },
|
||||
} = await this.parse(ChannelDelete);
|
||||
const { projectId } = await this.getContextAsync(ChannelDelete, {
|
||||
nonInteractive,
|
||||
});
|
||||
if (jsonFlag && !nonInteractiveFlag) {
|
||||
throw new Error('--json is allowed only in non-interactive mode');
|
||||
}
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
let channelId, channelName;
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
let name;
|
||||
if (nameArg) {
|
||||
const { id, name } = await ChannelQuery.viewUpdateChannelAsync({
|
||||
appId: projectId,
|
||||
channelName: nameArg,
|
||||
});
|
||||
channelId = id;
|
||||
channelName = name;
|
||||
name = nameArg;
|
||||
} else {
|
||||
if (nonInteractive) {
|
||||
if (nonInteractiveFlag) {
|
||||
throw new Error('Channel name must be set when running in non-interactive mode');
|
||||
}
|
||||
const { id, name } = await selectChannelOnAppAsync({
|
||||
projectId,
|
||||
selectionPromptTitle: 'Select a channel to delete',
|
||||
paginatedQueryOptions: {
|
||||
json: jsonFlag,
|
||||
nonInteractive,
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
channelId = id;
|
||||
channelName = name;
|
||||
name = (
|
||||
await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Provide the name of the channel to delete:',
|
||||
validate: (value: any) => (value ? true : 'Channel name may not be empty.'),
|
||||
})
|
||||
).name;
|
||||
}
|
||||
|
||||
if (!nonInteractive) {
|
||||
const data = await getChannelInfoAsync({ appId: projectId, name });
|
||||
const channelId = data.app?.byId.updateChannelByName?.id;
|
||||
if (!channelId) {
|
||||
throw new Error(`Could not find channel ${name} on ${fullName}`);
|
||||
}
|
||||
|
||||
if (!nonInteractiveFlag) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`You are about to permanently delete channel: "${channelName}".\nThis action is irreversible.`
|
||||
`You are about to permanently delete channel: "${name}".\nThis action is irreversible.`
|
||||
);
|
||||
Log.newLine();
|
||||
const confirmed = await toggleConfirmAsync({ message: 'Are you sure you wish to proceed?' });
|
||||
if (!confirmed) {
|
||||
Log.error(`Canceled deletion of channel: "${channelName}".`);
|
||||
Log.error(`Canceled deletion of channel: "${name}".`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -90,11 +102,42 @@ export default class ChannelDelete extends EasCommand {
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput(deletionResult);
|
||||
} else {
|
||||
Log.withTick(`️Deleted channel "${channelName}".`);
|
||||
Log.withTick(`️Deleted channel "${name}".`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getChannelInfoAsync({
|
||||
appId,
|
||||
name,
|
||||
}: GetChannelInfoQueryVariables): Promise<GetChannelInfoQuery> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<GetChannelInfoQuery, GetChannelInfoQueryVariables>(
|
||||
gql`
|
||||
query GetChannelInfo($appId: String!, $name: String!) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
updateChannelByName(name: $name) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
appId,
|
||||
name,
|
||||
},
|
||||
{ additionalTypenames: ['UpdateChannel'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
async function deleteChannelOnAppAsync({
|
||||
channelId,
|
||||
}: DeleteUpdateChannelMutationVariables): Promise<DeleteUpdateChannelResult> {
|
||||
|
||||
@@ -2,26 +2,71 @@ import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import { selectChannelOnAppAsync } from '../../channel/queries';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
GetChannelByNameToEditQuery,
|
||||
GetChannelByNameToEditQueryVariables,
|
||||
UpdateBranch,
|
||||
UpdateChannel,
|
||||
UpdateChannelBranchMappingMutation,
|
||||
UpdateChannelBranchMappingMutationVariables,
|
||||
} from '../../graphql/generated';
|
||||
import { BranchQuery } from '../../graphql/queries/BranchQuery';
|
||||
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
|
||||
import Log from '../../log';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
async function getChannelByNameForAppAsync({
|
||||
appId,
|
||||
channelName,
|
||||
}: GetChannelByNameToEditQueryVariables): Promise<
|
||||
Pick<UpdateChannel, 'id' | 'name'> & {
|
||||
updateBranches: Pick<UpdateBranch, 'id' | 'name'>[];
|
||||
}
|
||||
> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<GetChannelByNameToEditQuery, GetChannelByNameToEditQueryVariables>(
|
||||
gql`
|
||||
query GetChannelByNameToEdit($appId: String!, $channelName: String!) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
updateChannelByName(name: $channelName) {
|
||||
id
|
||||
name
|
||||
updateBranches(offset: 0, limit: 1) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appId, channelName },
|
||||
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const updateChannelByNameResult = data.app?.byId.updateChannelByName;
|
||||
if (!updateChannelByNameResult) {
|
||||
throw new Error(`Could not find a channel named ${channelName} on app with id ${appId}`);
|
||||
}
|
||||
return updateChannelByNameResult;
|
||||
}
|
||||
|
||||
export async function updateChannelBranchMappingAsync({
|
||||
channelId,
|
||||
branchMapping,
|
||||
}: UpdateChannelBranchMappingMutationVariables): Promise<
|
||||
UpdateChannelBranchMappingMutation['updateChannel']['editUpdateChannel']
|
||||
> {
|
||||
}: UpdateChannelBranchMappingMutationVariables): Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
branchMapping: string;
|
||||
}> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<UpdateChannelBranchMappingMutation, UpdateChannelBranchMappingMutationVariables>(
|
||||
@@ -44,7 +89,7 @@ export async function updateChannelBranchMappingAsync({
|
||||
if (!channel) {
|
||||
throw new Error(`Could not find a channel with id: ${channelId}`);
|
||||
}
|
||||
return channel;
|
||||
return data.updateChannel.editUpdateChannel!;
|
||||
}
|
||||
|
||||
export default class ChannelEdit extends EasCommand {
|
||||
@@ -62,53 +107,42 @@ export default class ChannelEdit extends EasCommand {
|
||||
branch: Flags.string({
|
||||
description: 'Name of the branch to point to',
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
json: Flags.boolean({
|
||||
description: 'Print output as a JSON object with the channel ID, name and branch mapping',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
args,
|
||||
flags: { branch: branchFlag, json, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(ChannelEdit);
|
||||
const { projectId } = await this.getContextAsync(ChannelEdit, {
|
||||
nonInteractive,
|
||||
});
|
||||
if (json) {
|
||||
const { args, flags } = await this.parse(ChannelEdit);
|
||||
if (flags.json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const existingChannel = args.name
|
||||
? await ChannelQuery.viewUpdateChannelAsync({ appId: projectId, channelName: args.name })
|
||||
: await selectChannelOnAppAsync({
|
||||
projectId,
|
||||
selectionPromptTitle: 'Select a channel to edit',
|
||||
paginatedQueryOptions: { json, nonInteractive, offset: 0 },
|
||||
});
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
const channelName = args.name ?? (await promptForChannelAsync());
|
||||
|
||||
const existingChannel = await getChannelByNameForAppAsync({ appId: projectId, channelName });
|
||||
if (existingChannel.updateBranches.length > 1) {
|
||||
throw new Error('There is a rollout in progress. Manage it with "channel:rollout" instead.');
|
||||
}
|
||||
|
||||
const branch = branchFlag
|
||||
? await BranchQuery.getBranchByNameAsync({
|
||||
appId: projectId,
|
||||
name: branchFlag,
|
||||
})
|
||||
: await selectBranchOnAppAsync({
|
||||
projectId,
|
||||
promptTitle: `Which branch would you like ${existingChannel.name} to point at?`,
|
||||
displayTextForListItem: updateBranch => updateBranch.name,
|
||||
paginatedQueryOptions: {
|
||||
json,
|
||||
nonInteractive,
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
const branchName = flags.branch ?? (await promptForBranchAsync());
|
||||
|
||||
const branch = await BranchQuery.getBranchByNameAsync({
|
||||
appId: projectId,
|
||||
name: branchName,
|
||||
});
|
||||
if (!branch) {
|
||||
throw new Error(
|
||||
`Could not find a branch named "${branchName}". Check what branches exist on this project with ${chalk.bold(
|
||||
'eas branch:list'
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
const channel = await updateChannelBranchMappingAsync({
|
||||
channelId: existingChannel.id,
|
||||
// todo: move branch mapping logic to utility
|
||||
@@ -118,7 +152,7 @@ export default class ChannelEdit extends EasCommand {
|
||||
}),
|
||||
});
|
||||
|
||||
if (json) {
|
||||
if (flags.json) {
|
||||
printJsonOnlyOutput(channel);
|
||||
} else {
|
||||
Log.withTick(
|
||||
@@ -131,3 +165,25 @@ export default class ChannelEdit extends EasCommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForChannelAsync(): Promise<string> {
|
||||
Log.addNewLineIfNone();
|
||||
const { name } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Provide the name of the channel to edit:',
|
||||
validate: value => (value ? true : 'The channel name may not be empty.'),
|
||||
});
|
||||
return name;
|
||||
}
|
||||
|
||||
async function promptForBranchAsync(): Promise<string> {
|
||||
Log.addNewLineIfNone();
|
||||
const { name } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: `To which branch should the channel link?`,
|
||||
validate: value => (value ? true : 'The branch name may not be empty.'),
|
||||
});
|
||||
return name;
|
||||
}
|
||||
@@ -1,40 +1,114 @@
|
||||
import { CHANNELS_LIMIT, listAndRenderChannelsOnAppAsync } from '../../channel/queries';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { Flags } from '@oclif/core';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
EasPaginatedQueryFlags,
|
||||
getLimitFlagWithCustomValues,
|
||||
getPaginatedQueryOptions,
|
||||
} from '../../commandUtils/pagination';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
GetAllChannelsForAppQuery,
|
||||
GetAllChannelsForAppQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { logChannelDetails } from './view';
|
||||
|
||||
const CHANNEL_LIMIT = 10_000;
|
||||
|
||||
async function getAllUpdateChannelForAppAsync({
|
||||
appId,
|
||||
}: {
|
||||
appId: string;
|
||||
}): Promise<GetAllChannelsForAppQuery> {
|
||||
return await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<GetAllChannelsForAppQuery, GetAllChannelsForAppQueryVariables>(
|
||||
gql`
|
||||
query GetAllChannelsForApp($appId: String!, $offset: Int!, $limit: Int!) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
updateChannels(offset: $offset, limit: $limit) {
|
||||
id
|
||||
name
|
||||
branchMapping
|
||||
updateBranches(offset: 0, limit: $limit) {
|
||||
id
|
||||
name
|
||||
updates(offset: 0, limit: 10) {
|
||||
id
|
||||
group
|
||||
message
|
||||
runtimeVersion
|
||||
createdAt
|
||||
platform
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
username
|
||||
}
|
||||
... on Robot {
|
||||
firstName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appId, offset: 0, limit: CHANNEL_LIMIT },
|
||||
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch', 'Update'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
export default class ChannelList extends EasCommand {
|
||||
static override description = 'list all channels';
|
||||
|
||||
static override flags = {
|
||||
...EasPaginatedQueryFlags,
|
||||
limit: getLimitFlagWithCustomValues({ defaultTo: 10, limit: CHANNELS_LIMIT }),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
json: Flags.boolean({
|
||||
description: 'print output as a JSON object with the channel ID, name and branch mapping.',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
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, {
|
||||
nonInteractive,
|
||||
});
|
||||
const {
|
||||
flags: { json: jsonFlag },
|
||||
} = await this.parse(ChannelList);
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
await listAndRenderChannelsOnAppAsync({
|
||||
projectId,
|
||||
paginatedQueryOptions,
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
const getAllUpdateChannelForAppResult = await getAllUpdateChannelForAppAsync({
|
||||
appId: projectId,
|
||||
});
|
||||
const channels = getAllUpdateChannelForAppResult.app?.byId.updateChannels;
|
||||
if (!channels) {
|
||||
throw new Error(`Could not find channels on project with id ${projectId}`);
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput(channels);
|
||||
} else {
|
||||
for (const channel of channels) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: channel.name },
|
||||
{ label: 'ID', value: channel.id },
|
||||
])
|
||||
);
|
||||
logChannelDetails(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,21 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { BranchMapping, getBranchMapping } from '../../channel/utils';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { UpdateBranch } from '../../graphql/generated';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { GetChannelByNameForAppQuery, UpdateBranch } from '../../graphql/generated';
|
||||
import { BranchQuery } from '../../graphql/queries/BranchQuery';
|
||||
import { ChannelQuery, UpdateChannelByNameObject } from '../../graphql/queries/ChannelQuery';
|
||||
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
|
||||
import Log from '../../log';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync, selectAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { updateChannelBranchMappingAsync } from './edit';
|
||||
import { BranchMapping, getBranchMapping } from './view';
|
||||
|
||||
async function promptForRolloutPercentAsync({
|
||||
promptMessage,
|
||||
@@ -36,7 +40,9 @@ async function promptForRolloutPercentAsync({
|
||||
return rolloutPercent;
|
||||
}
|
||||
|
||||
function getRolloutInfo(channel: UpdateChannelByNameObject): {
|
||||
function getRolloutInfo(
|
||||
channel: NonNullable<GetChannelByNameForAppQuery['app']['byId']['updateChannelByName']>
|
||||
): {
|
||||
newBranch: Pick<UpdateBranch, 'name' | 'id'>;
|
||||
oldBranch: Pick<UpdateBranch, 'name' | 'id'>;
|
||||
currentPercent: number;
|
||||
@@ -58,20 +64,20 @@ async function startRolloutAsync({
|
||||
channelName,
|
||||
branchName,
|
||||
percent,
|
||||
jsonFlag,
|
||||
projectId,
|
||||
displayName,
|
||||
fullName,
|
||||
currentBranchMapping,
|
||||
channel,
|
||||
nonInteractive,
|
||||
}: {
|
||||
channelName?: string;
|
||||
branchName: string;
|
||||
percent?: number;
|
||||
jsonFlag: boolean;
|
||||
projectId: string;
|
||||
displayName: string;
|
||||
fullName: string;
|
||||
currentBranchMapping: BranchMapping;
|
||||
channel: UpdateChannelByNameObject;
|
||||
nonInteractive: boolean;
|
||||
channel: NonNullable<GetChannelByNameForAppQuery['app']['byId']['updateChannelByName']>;
|
||||
}): Promise<{
|
||||
newChannelInfo: {
|
||||
id: string;
|
||||
@@ -84,6 +90,13 @@ async function startRolloutAsync({
|
||||
appId: projectId,
|
||||
name: branchName,
|
||||
});
|
||||
if (!branch) {
|
||||
throw new Error(
|
||||
`Could not find a branch named "${branchName}". Check which branches exist on this project with ${chalk.bold(
|
||||
'eas branch:list'
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
|
||||
const oldBranchId = currentBranchMapping.data[0].branchId;
|
||||
if (branch.id === oldBranchId) {
|
||||
@@ -93,9 +106,9 @@ async function startRolloutAsync({
|
||||
}
|
||||
|
||||
if (percent == null) {
|
||||
if (nonInteractive) {
|
||||
if (jsonFlag) {
|
||||
throw new Error(
|
||||
'You must specify a percent with the --percent flag when initiating a rollout with the --non-interactive flag.'
|
||||
'You must specify a percent with the --percent flag when initiating a rollout with the --json flag.'
|
||||
);
|
||||
}
|
||||
const promptMessage = `What percent of users should be directed to the branch "${branchName}"?`;
|
||||
@@ -124,7 +137,7 @@ async function startRolloutAsync({
|
||||
const oldBranch = channel.updateBranches.filter(branch => branch.id === oldBranchId)[0];
|
||||
if (!oldBranch) {
|
||||
throw new Error(
|
||||
`Branch mapping is missing its only branch for channel "${channelName}" on app "${displayName}"`
|
||||
`Branch mapping is missing its only branch for channel "${channelName}" on app "${fullName}"`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -142,15 +155,15 @@ async function startRolloutAsync({
|
||||
async function editRolloutAsync({
|
||||
channelName,
|
||||
percent,
|
||||
nonInteractive,
|
||||
jsonFlag,
|
||||
currentBranchMapping,
|
||||
channel,
|
||||
}: {
|
||||
channelName?: string;
|
||||
percent?: number;
|
||||
nonInteractive: boolean;
|
||||
jsonFlag: boolean;
|
||||
currentBranchMapping: BranchMapping;
|
||||
channel: UpdateChannelByNameObject;
|
||||
channel: NonNullable<GetChannelByNameForAppQuery['app']['byId']['updateChannelByName']>;
|
||||
}): Promise<{
|
||||
newChannelInfo: {
|
||||
id: string;
|
||||
@@ -162,7 +175,7 @@ async function editRolloutAsync({
|
||||
const { newBranch, oldBranch, currentPercent } = getRolloutInfo(channel);
|
||||
|
||||
if (percent == null) {
|
||||
if (nonInteractive) {
|
||||
if (jsonFlag) {
|
||||
throw new Error(
|
||||
'A rollout is already in progress. If you wish to modify it you must use specify the new rollout percentage with the --percent flag.'
|
||||
);
|
||||
@@ -197,15 +210,15 @@ async function editRolloutAsync({
|
||||
async function endRolloutAsync({
|
||||
channelName,
|
||||
branchName,
|
||||
nonInteractive,
|
||||
jsonFlag,
|
||||
projectId,
|
||||
channel,
|
||||
}: {
|
||||
channelName?: string;
|
||||
branchName?: string;
|
||||
nonInteractive: boolean;
|
||||
jsonFlag: boolean;
|
||||
projectId: string;
|
||||
channel: UpdateChannelByNameObject;
|
||||
channel: NonNullable<GetChannelByNameForAppQuery['app']['byId']['updateChannelByName']>;
|
||||
}): Promise<{
|
||||
newChannelInfo: {
|
||||
id: string;
|
||||
@@ -223,6 +236,13 @@ async function endRolloutAsync({
|
||||
appId: projectId,
|
||||
name: branchName,
|
||||
});
|
||||
if (!branch) {
|
||||
throw new Error(
|
||||
`Could not find a branch named "${branchName}". Check which branches exist on this project with ${chalk.bold(
|
||||
'eas branch:list'
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
|
||||
switch (branch.id) {
|
||||
case newBranch.id:
|
||||
@@ -237,9 +257,9 @@ async function endRolloutAsync({
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (nonInteractive) {
|
||||
if (jsonFlag) {
|
||||
throw new Error(
|
||||
'Branch name must be specified with the --branch flag when both the --end and --non-interactive flag are true.'
|
||||
'Branch name must be specified with the --branch flag when both the --end and --json flag are true.'
|
||||
);
|
||||
}
|
||||
endOnNewBranch = await selectAsync<boolean>(
|
||||
@@ -308,34 +328,27 @@ export default class ChannelRollout extends EasCommand {
|
||||
description: 'end the rollout',
|
||||
default: false,
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
json: Flags.boolean({
|
||||
description: 'print output as a JSON object with the new channel ID, name and branch mapping',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
args: { channel: channelName },
|
||||
flags: {
|
||||
json: jsonFlag,
|
||||
end: endFlag,
|
||||
branch: branchName,
|
||||
percent,
|
||||
'non-interactive': nonInteractive,
|
||||
},
|
||||
flags: { json: jsonFlag, end: endFlag, branch: branchName, percent },
|
||||
} = await this.parse(ChannelRollout);
|
||||
const { projectId } = await this.getContextAsync(ChannelRollout, {
|
||||
nonInteractive,
|
||||
});
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
const channel = await ChannelQuery.viewUpdateChannelAsync({
|
||||
const channel = await ChannelQuery.getUpdateChannelByNameForAppAsync({
|
||||
appId: projectId,
|
||||
channelName: channelName!,
|
||||
});
|
||||
@@ -382,11 +395,11 @@ export default class ChannelRollout extends EasCommand {
|
||||
if (!isRollout) {
|
||||
rolloutMutationResult = await startRolloutAsync({
|
||||
channelName,
|
||||
branchName: branchName ?? (await promptForBranchNameAsync(channelName, nonInteractive)),
|
||||
branchName: branchName ?? (await promptForBranchNameAsync(channelName)),
|
||||
percent,
|
||||
nonInteractive,
|
||||
jsonFlag,
|
||||
projectId,
|
||||
displayName: projectDisplayName,
|
||||
fullName,
|
||||
currentBranchMapping,
|
||||
channel,
|
||||
});
|
||||
@@ -394,7 +407,7 @@ export default class ChannelRollout extends EasCommand {
|
||||
rolloutMutationResult = await endRolloutAsync({
|
||||
channelName,
|
||||
branchName,
|
||||
nonInteractive,
|
||||
jsonFlag,
|
||||
projectId,
|
||||
channel,
|
||||
});
|
||||
@@ -402,7 +415,7 @@ export default class ChannelRollout extends EasCommand {
|
||||
rolloutMutationResult = await editRolloutAsync({
|
||||
channelName,
|
||||
percent,
|
||||
nonInteractive,
|
||||
jsonFlag,
|
||||
currentBranchMapping,
|
||||
channel,
|
||||
});
|
||||
@@ -419,13 +432,7 @@ export default class ChannelRollout extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForBranchNameAsync(
|
||||
channelName: string,
|
||||
nonInteractive: boolean
|
||||
): Promise<string> {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply branch flag in non-interactive mode');
|
||||
}
|
||||
async function promptForBranchNameAsync(channelName: string): Promise<string> {
|
||||
const { name } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
|
||||
@@ -1,13 +1,141 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
|
||||
import Log from '../../log';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import {
|
||||
listAndRenderBranchesAndUpdatesOnChannelAsync,
|
||||
selectChannelOnAppAsync,
|
||||
} from '../../channel/queries';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { EasPaginatedQueryFlags, getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
FormatUpdateParameter,
|
||||
UPDATE_COLUMNS,
|
||||
formatUpdate,
|
||||
getPlatformsForGroup,
|
||||
} from '../../update/utils';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export type BranchMapping = {
|
||||
version: number;
|
||||
data: {
|
||||
branchId: string;
|
||||
branchMappingLogic: {
|
||||
operand: number;
|
||||
clientKey: string;
|
||||
branchMappingOperator: string;
|
||||
} & string;
|
||||
}[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the branch mapping and determine whether it is a rollout.
|
||||
* Ensure that the branch mapping is properly formatted.
|
||||
*/
|
||||
export function getBranchMapping(branchMappingString?: string): {
|
||||
branchMapping: BranchMapping;
|
||||
isRollout: boolean;
|
||||
rolloutPercent?: number;
|
||||
} {
|
||||
if (!branchMappingString) {
|
||||
throw new Error('Missing branch mapping.');
|
||||
}
|
||||
let branchMapping: BranchMapping;
|
||||
try {
|
||||
branchMapping = JSON.parse(branchMappingString);
|
||||
} catch {
|
||||
throw new Error(`Could not parse branchMapping string into a JSON: "${branchMappingString}"`);
|
||||
}
|
||||
assert(branchMapping, 'Branch Mapping must be defined.');
|
||||
|
||||
if (branchMapping.version !== 0) {
|
||||
throw new Error('Branch mapping must be version 0.');
|
||||
}
|
||||
|
||||
const isRollout = branchMapping.data.length === 2;
|
||||
const rolloutPercent = branchMapping.data[0].branchMappingLogic.operand;
|
||||
|
||||
switch (branchMapping.data.length) {
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
if (branchMapping.data[0].branchMappingLogic !== 'true') {
|
||||
throw new Error('Branch mapping logic for a single branch must be "true"');
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if (branchMapping.data[0].branchMappingLogic.clientKey !== 'rolloutToken') {
|
||||
throw new Error('Client key of initial branch mapping must be "rolloutToken"');
|
||||
}
|
||||
if (branchMapping.data[0].branchMappingLogic.branchMappingOperator !== 'hash_lt') {
|
||||
throw new Error('Branch mapping operator of initial branch mapping must be "hash_lt"');
|
||||
}
|
||||
if (rolloutPercent == null) {
|
||||
throw new Error('Branch mapping is missing a "rolloutPercent"');
|
||||
}
|
||||
if (branchMapping.data[1].branchMappingLogic !== 'true') {
|
||||
throw new Error('Branch mapping logic for a the second branch of a rollout must be "true"');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error('Branch mapping data must have length less than or equal to 2.');
|
||||
}
|
||||
|
||||
return { branchMapping, isRollout, rolloutPercent };
|
||||
}
|
||||
|
||||
export function logChannelDetails(channel: {
|
||||
branchMapping: string;
|
||||
updateBranches: {
|
||||
updates: (FormatUpdateParameter & {
|
||||
runtimeVersion: string;
|
||||
group: string;
|
||||
platform: string;
|
||||
})[];
|
||||
name: string;
|
||||
id: string;
|
||||
}[];
|
||||
}): void {
|
||||
const { branchMapping, isRollout, rolloutPercent } = getBranchMapping(channel.branchMapping);
|
||||
|
||||
const table = new Table({
|
||||
head: ['branch', ...(isRollout ? ['rollout percent'] : []), ...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const index in branchMapping.data) {
|
||||
if (parseInt(index, 10) > 1) {
|
||||
throw new Error('Branch Mapping data must have length less than or equal to 2.');
|
||||
}
|
||||
|
||||
const { branchId } = branchMapping.data[index];
|
||||
const branch = channel.updateBranches.filter(branch => branch.id === branchId)[0];
|
||||
if (!branch) {
|
||||
throw new Error('Branch mapping is pointing at a missing branch.');
|
||||
}
|
||||
const update = branch.updates[0];
|
||||
table.push([
|
||||
branch.name,
|
||||
...(isRollout
|
||||
? [
|
||||
parseInt(index, 10) === 0
|
||||
? `${rolloutPercent! * 100}%`
|
||||
: `${(1 - rolloutPercent!) * 100}%`,
|
||||
]
|
||||
: []),
|
||||
formatUpdate(update),
|
||||
update?.runtimeVersion ?? 'N/A',
|
||||
update?.group ?? 'N/A',
|
||||
getPlatformsForGroup({
|
||||
updates: branch.updates,
|
||||
group: branch.updates[0]?.group,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
Log.log(table.toString());
|
||||
}
|
||||
|
||||
export default class ChannelView extends EasCommand {
|
||||
static override description = 'view a channel';
|
||||
@@ -21,50 +149,60 @@ export default class ChannelView extends EasCommand {
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
...EasPaginatedQueryFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
json: Flags.boolean({
|
||||
description: 'print output as a JSON object with the channel ID, name and branch mapping.',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
let {
|
||||
args: { name: channelName },
|
||||
flags,
|
||||
flags: { json: jsonFlag },
|
||||
} = await this.parse(ChannelView);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
const { json: jsonFlag, 'non-interactive': nonInteractive } = flags;
|
||||
const { projectId } = await this.getContextAsync(ChannelView, {
|
||||
nonInteractive,
|
||||
});
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!channelName) {
|
||||
const validationMessage = 'A channel name is required to view a specific channel.';
|
||||
if (nonInteractive) {
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
const selectedUpdateChannel = await selectChannelOnAppAsync({
|
||||
projectId,
|
||||
selectionPromptTitle: 'Select a channel to view',
|
||||
paginatedQueryOptions: {
|
||||
json: jsonFlag,
|
||||
nonInteractive,
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
channelName = selectedUpdateChannel.name;
|
||||
assert(channelName, `A channel must be selected.`);
|
||||
({ name: channelName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Provide a channel name:',
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
await listAndRenderBranchesAndUpdatesOnChannelAsync({
|
||||
projectId,
|
||||
const channel = await ChannelQuery.getUpdateChannelByNameForAppAsync({
|
||||
appId: projectId,
|
||||
channelName,
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
if (!channel) {
|
||||
throw new Error(`Could not find a channel with name: ${channelName}`);
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput(channel);
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Channel:'));
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: channel.name },
|
||||
{ label: 'ID', value: channel.id },
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk`{bold Branches pointed at this channel and their most recent update group:}`);
|
||||
logChannelDetails(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand, { CommandConfiguration } from '../commandUtils/EasCommand';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { toAppPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log from '../log';
|
||||
import { appPlatformEmojis } from '../platform';
|
||||
@@ -25,9 +25,7 @@ export default class Config extends EasCommand {
|
||||
}),
|
||||
};
|
||||
|
||||
protected override commandConfiguration: CommandConfiguration = {
|
||||
allowUnauthenticated: true,
|
||||
};
|
||||
protected override requiresAuthentication = false;
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(Config);
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand, {
|
||||
EASCommandLoggedInContext,
|
||||
EASCommandProjectIdContext,
|
||||
} from '../commandUtils/EasCommand';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { findProjectRootAsync } from '../project/projectUtils';
|
||||
|
||||
export default class Credentials extends EasCommand {
|
||||
static override description = 'manage credentials';
|
||||
@@ -15,18 +10,8 @@ export default class Credentials extends EasCommand {
|
||||
platform: Flags.enum({ char: 'p', options: ['android', 'ios'] }),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandLoggedInContext,
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(Credentials);
|
||||
const { actor, projectId } = await this.getContextAsync(Credentials, { nonInteractive: false });
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
|
||||
await new SelectPlatform(actor, { projectId, exp }, flags.platform).runAsync();
|
||||
await new SelectPlatform(flags.platform).runAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,16 @@
|
||||
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';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
export default class DeviceCreate extends EasCommand {
|
||||
static override description = 'register new Apple Devices to use for internal distribution';
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandLoggedInContext,
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
// this command is interactive by design
|
||||
const { actor, projectId } = await this.getContextAsync(DeviceCreate, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
const user = await ensureLoggedInAsync();
|
||||
|
||||
const ctx = await createContextAsync({ appStore: new AppStoreApi(), user: actor, projectId });
|
||||
const ctx = await createContextAsync({ appStore: new AppStoreApi(), user });
|
||||
const manager = new DeviceManager(ctx);
|
||||
await manager.createAsync();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Device, DeviceStatus } from '@expo/apple-utils';
|
||||
import { Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand, { CommandContext } from '../../commandUtils/EasCommand';
|
||||
import { chooseDevicesToDeleteAsync } from '../../credentials/ios/actions/DeviceUtils';
|
||||
import { AppleDeviceMutation } from '../../credentials/ios/api/graphql/mutations/AppleDeviceMutation';
|
||||
import {
|
||||
@@ -14,10 +14,8 @@ import { AppleTeamQuery } from '../../credentials/ios/api/graphql/queries/AppleT
|
||||
import { authenticateAsync, getRequestContext } from '../../credentials/ios/appstore/authenticate';
|
||||
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 { getExpoConfig } from '../../project/expoConfig';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
|
||||
export default class DeviceDelete extends EasCommand {
|
||||
static override description = 'remove a registered device from your account';
|
||||
@@ -27,61 +25,71 @@ export default class DeviceDelete extends EasCommand {
|
||||
udid: Flags.string({ multiple: true }),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
protected async runAsync(commandContext: CommandContext): Promise<{ jsonOutput: object }> {
|
||||
let {
|
||||
flags: { 'apple-team-id': appleTeamIdentifier, udid: udids },
|
||||
} = await this.parse(DeviceDelete);
|
||||
const { projectId } = await this.getContextAsync(DeviceDelete, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const account = await getOwnerAccountForProjectIdAsync(projectId);
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
if (!appleTeamIdentifier) {
|
||||
appleTeamIdentifier = await this.askForAppleTeamAsync(account.name);
|
||||
if (commandContext.nonInteractive) {
|
||||
throw new Error('Must supply `apple-team-id` in non-interactive mode');
|
||||
}
|
||||
appleTeamIdentifier = await this.askForAppleTeamAsync(commandContext, accountName);
|
||||
}
|
||||
|
||||
assert(appleTeamIdentifier, 'No team identifier is specified');
|
||||
|
||||
const appleDevicesResult = await this.getDevicesForTeamAsync(account.name, appleTeamIdentifier);
|
||||
const appleDevicesResult = await this.getDevicesForTeamAsync(
|
||||
commandContext,
|
||||
accountName,
|
||||
appleTeamIdentifier
|
||||
);
|
||||
|
||||
if (!appleDevicesResult) {
|
||||
return;
|
||||
return { jsonOutput: [] };
|
||||
}
|
||||
|
||||
const { appleTeamName, appleDevices } = appleDevicesResult;
|
||||
|
||||
const chosenDevices = await this.chooseDevicesToDeleteAsync(appleDevices, udids);
|
||||
const chosenDevices = await this.chooseDevicesToDeleteAsync(
|
||||
commandContext,
|
||||
appleDevices,
|
||||
udids
|
||||
);
|
||||
|
||||
if (chosenDevices.length === 0) {
|
||||
Log.newLine();
|
||||
Log.warn('No devices were chosen to be removed.');
|
||||
return;
|
||||
commandContext.logger.newLine();
|
||||
commandContext.logger.warn('No devices were chosen to be removed.');
|
||||
return { jsonOutput: [] };
|
||||
}
|
||||
|
||||
this.logChosenDevices(chosenDevices, appleTeamName, appleTeamIdentifier);
|
||||
|
||||
const hasRemoved = await this.askAndRemoveFromExpoAsync(chosenDevices);
|
||||
this.logChosenDevices(commandContext, chosenDevices, appleTeamName, appleTeamIdentifier);
|
||||
|
||||
const hasRemoved = await this.askAndRemoveFromExpoAsync(commandContext, chosenDevices);
|
||||
if (!hasRemoved) {
|
||||
return;
|
||||
return { jsonOutput: [] };
|
||||
}
|
||||
|
||||
await this.askAndDisableOnAppleAsync(chosenDevices, appleTeamIdentifier);
|
||||
await this.askAndDisableOnAppleAsync(commandContext, chosenDevices, appleTeamIdentifier);
|
||||
|
||||
return { jsonOutput: chosenDevices.map(it => it.id) };
|
||||
}
|
||||
|
||||
async askAndDisableOnAppleAsync(
|
||||
{ nonInteractive, logger, prompts: { toggleConfirmAsync }, ora }: CommandContext,
|
||||
chosenDevices: (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?',
|
||||
});
|
||||
logger.newLine();
|
||||
const deleteOnApple = nonInteractive
|
||||
? true
|
||||
: await toggleConfirmAsync({
|
||||
message: 'Do you want to disable these devices on your Apple account as well?',
|
||||
});
|
||||
|
||||
if (!deleteOnApple) {
|
||||
return;
|
||||
@@ -90,7 +98,7 @@ export default class DeviceDelete extends EasCommand {
|
||||
const ctx = await authenticateAsync({ teamId: appleTeamIdentifier });
|
||||
const context = getRequestContext(ctx);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
logger.addNewLineIfNone();
|
||||
const removeAppleSpinner = ora('Disabling devices on Apple').start();
|
||||
try {
|
||||
const chosenDeviceIdentifiers = chosenDevices.map(cd => cd.identifier);
|
||||
@@ -111,18 +119,21 @@ export default class DeviceDelete extends EasCommand {
|
||||
}
|
||||
|
||||
async askAndRemoveFromExpoAsync(
|
||||
{ nonInteractive, logger, prompts: { toggleConfirmAsync }, ora }: CommandContext,
|
||||
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[]
|
||||
): Promise<boolean> {
|
||||
Log.warn(
|
||||
logger.warn(
|
||||
`You are about to remove the Apple device${
|
||||
chosenDevices.length > 1 ? 's' : ''
|
||||
} listed above from your Expo account.`
|
||||
);
|
||||
Log.newLine();
|
||||
logger.newLine();
|
||||
|
||||
const confirmed = await toggleConfirmAsync({
|
||||
message: 'Are you sure you wish to proceed?',
|
||||
});
|
||||
const confirmed = nonInteractive
|
||||
? true
|
||||
: await toggleConfirmAsync({
|
||||
message: 'Are you sure you wish to proceed?',
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
const removalSpinner = ora(`Removing Apple devices on Expo`).start();
|
||||
@@ -141,49 +152,55 @@ export default class DeviceDelete extends EasCommand {
|
||||
}
|
||||
|
||||
logChosenDevices(
|
||||
{ logger }: CommandContext,
|
||||
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[],
|
||||
appleTeamName: Maybe<string> | undefined,
|
||||
appleTeamIdentifier: string
|
||||
): void {
|
||||
Log.addNewLineIfNone();
|
||||
logger.addNewLineIfNone();
|
||||
chosenDevices.forEach(device => {
|
||||
Log.log(
|
||||
logger.log(
|
||||
formatDevice(device, {
|
||||
appleTeamName,
|
||||
appleTeamIdentifier: appleTeamIdentifier!,
|
||||
})
|
||||
);
|
||||
Log.newLine();
|
||||
logger.newLine();
|
||||
});
|
||||
}
|
||||
|
||||
async chooseDevicesToDeleteAsync(
|
||||
{ nonInteractive, logger }: CommandContext,
|
||||
appleDevices: AppleDeviceQueryResult[],
|
||||
udids: string[]
|
||||
): Promise<(AppleDevice | AppleDeviceQueryResult)[]> {
|
||||
let chosenDevices: (AppleDeviceQueryResult | AppleDevice)[] = [];
|
||||
Log.newLine();
|
||||
logger.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}.`);
|
||||
logger.warn(`No device found with UDID ${udid}.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (chosenDevices.length === 0) {
|
||||
Log.addNewLineIfNone();
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply `udid` paramter in non-interactive mode');
|
||||
}
|
||||
logger.addNewLineIfNone();
|
||||
chosenDevices = await chooseDevicesToDeleteAsync(appleDevices);
|
||||
Log.newLine();
|
||||
logger.newLine();
|
||||
}
|
||||
|
||||
return chosenDevices;
|
||||
}
|
||||
|
||||
async getDevicesForTeamAsync(
|
||||
{ ora }: CommandContext,
|
||||
accountName: string,
|
||||
appleTeamIdentifier: string
|
||||
): Promise<AppleDevicesByTeamIdentifierQueryResult | undefined> {
|
||||
@@ -213,7 +230,10 @@ export default class DeviceDelete extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
async askForAppleTeamAsync(accountName: string): Promise<string | undefined> {
|
||||
async askForAppleTeamAsync(
|
||||
{ prompts: { promptAsync }, ora }: CommandContext,
|
||||
accountName: string
|
||||
): Promise<string | undefined> {
|
||||
const teamSpinner = ora().start('Fetching the list of teams for the project…');
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,13 +2,14 @@ import { Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import EasCommand 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 { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
|
||||
export default class BuildList extends EasCommand {
|
||||
@@ -18,17 +19,12 @@ export default class BuildList extends EasCommand {
|
||||
'apple-team-id': Flags.string(),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
let appleTeamIdentifier = (await this.parse(BuildList)).flags['apple-team-id'];
|
||||
const { projectId } = await this.getContextAsync(BuildList, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const account = await getOwnerAccountForProjectIdAsync(projectId);
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
let spinner: Ora;
|
||||
|
||||
@@ -36,7 +32,7 @@ export default class BuildList extends EasCommand {
|
||||
spinner = ora().start('Fetching the list of teams for the project…');
|
||||
|
||||
try {
|
||||
const teams = await AppleTeamQuery.getAllForAccountAsync(account.name);
|
||||
const teams = await AppleTeamQuery.getAllForAccountAsync(accountName);
|
||||
|
||||
if (teams.length > 0) {
|
||||
spinner.succeed();
|
||||
@@ -59,7 +55,7 @@ export default class BuildList extends EasCommand {
|
||||
appleTeamIdentifier = result.appleTeamIdentifier;
|
||||
}
|
||||
} else {
|
||||
spinner.fail(`Couldn't find any teams for the account ${account.name}`);
|
||||
spinner.fail(`Couldn't find any teams for the account ${accountName}`);
|
||||
}
|
||||
} catch (e) {
|
||||
spinner.fail(`Something went wrong and we couldn't fetch the list of teams`);
|
||||
@@ -73,7 +69,7 @@ export default class BuildList extends EasCommand {
|
||||
|
||||
try {
|
||||
const result = await AppleDeviceQuery.getAllForAppleTeamAsync(
|
||||
account.name,
|
||||
accountName,
|
||||
appleTeamIdentifier
|
||||
);
|
||||
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
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';
|
||||
import { ora } from '../../ora';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
|
||||
export default class DeviceView extends EasCommand {
|
||||
static override description = 'view a device for your project';
|
||||
|
||||
static override args = [{ name: 'UDID' }];
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { UDID } = (await this.parse(DeviceView)).args;
|
||||
|
||||
@@ -30,15 +27,15 @@ 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, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
const account = await getOwnerAccountForProjectIdAsync(projectId);
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
const spinner = ora().start(`Fetching device details for ${UDID}…`);
|
||||
|
||||
try {
|
||||
const device = await AppleDeviceQuery.getByDeviceIdentifierAsync(account.name, UDID);
|
||||
const device = await AppleDeviceQuery.getByDeviceIdentifierAsync(accountName, UDID);
|
||||
|
||||
if (device) {
|
||||
spinner.succeed('Fetched device details');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import envinfo from 'envinfo';
|
||||
|
||||
import EasCommand, { CommandConfiguration } from '../commandUtils/EasCommand';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import Log from '../log';
|
||||
import { findProjectRootAsync } from '../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
@@ -10,9 +10,7 @@ import { easCliVersion } from '../utils/easCli';
|
||||
export default class Diagnostics extends EasCommand {
|
||||
static override description = 'display environment info';
|
||||
|
||||
protected override commandConfiguration: CommandConfiguration = {
|
||||
allowUnauthenticated: true,
|
||||
};
|
||||
protected override requiresAuthentication = false;
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const info = await envinfo.run(
|
||||
|
||||
@@ -4,16 +4,14 @@ import chalk from 'chalk';
|
||||
import path from 'path';
|
||||
|
||||
import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import EasCommand, {
|
||||
EASCommandLoggedInContext,
|
||||
EASCommandProjectIdContext,
|
||||
} from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { createMetadataContextAsync } from '../../metadata/context';
|
||||
import { downloadMetadataAsync } from '../../metadata/download';
|
||||
import { handleMetadataError } from '../../metadata/errors';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
export default class MetadataPull extends EasCommand {
|
||||
static override description = 'generate the local store configuration from the app stores';
|
||||
@@ -26,29 +24,19 @@ export default class MetadataPull extends EasCommand {
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...EASCommandLoggedInContext,
|
||||
};
|
||||
|
||||
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 } = await this.getContextAsync(MetadataPull, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
|
||||
// this command is interactive (all nonInteractive flags passed to utility functions are false)
|
||||
await getProjectIdAsync(exp);
|
||||
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
|
||||
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
projectInfo: { exp, projectId },
|
||||
exp,
|
||||
projectDir,
|
||||
user: actor,
|
||||
user: await ensureLoggedInAsync(),
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -2,16 +2,14 @@ import { getConfig } from '@expo/config';
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import EasCommand, {
|
||||
EASCommandLoggedInContext,
|
||||
EASCommandProjectIdContext,
|
||||
} from '../../commandUtils/EasCommand';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { createMetadataContextAsync } from '../../metadata/context';
|
||||
import { handleMetadataError } from '../../metadata/errors';
|
||||
import { uploadMetadataAsync } from '../../metadata/upload';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
export default class MetadataPush extends EasCommand {
|
||||
static override description = 'sync the local store configuration to the app stores';
|
||||
@@ -24,29 +22,19 @@ export default class MetadataPush extends EasCommand {
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
...EASCommandLoggedInContext,
|
||||
};
|
||||
|
||||
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 } = await this.getContextAsync(MetadataPush, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
|
||||
// this command is interactive (all nonInteractive flags passed to utility functions are false)
|
||||
await getProjectIdAsync(exp);
|
||||
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
|
||||
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
projectInfo: { exp, projectId },
|
||||
exp,
|
||||
projectDir,
|
||||
user: actor,
|
||||
user: await ensureLoggedInAsync(),
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import openBrowserAsync from 'better-opn';
|
||||
|
||||
import { getProjectDashboardUrl } from '../build/utils/url';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../commandUtils/EasCommand';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { ora } from '../ora';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { findProjectRootAsync, getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
|
||||
import {
|
||||
fetchProjectIdFromServerAsync,
|
||||
findProjectRootAsync,
|
||||
getProjectAccountName,
|
||||
} from '../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
|
||||
export default class Open extends EasCommand {
|
||||
static override description = 'open the project page in a web browser';
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
// this command is interactive by nature (only really run by humans in a terminal)
|
||||
const { projectId } = await this.getContextAsync(Open, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
|
||||
const account = await getOwnerAccountForProjectIdAsync(projectId);
|
||||
// this ensures the project exists
|
||||
await fetchProjectIdFromServerAsync(exp);
|
||||
|
||||
const user = await ensureLoggedInAsync();
|
||||
const accountName = getProjectAccountName(exp, user);
|
||||
const projectName = exp.slug;
|
||||
|
||||
const projectDashboardUrl = getProjectDashboardUrl(account.name, projectName);
|
||||
const projectDashboardUrl = getProjectDashboardUrl(accountName, projectName);
|
||||
const failedMessage = `Unable to open a web browser. Project page is available at: ${projectDashboardUrl}`;
|
||||
const spinner = ora(`Opening ${projectDashboardUrl}`).start();
|
||||
try {
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
import { AppJSONConfig, PackageJSONConfig, getConfig } from '@expo/config';
|
||||
import chalk from 'chalk';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { jester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { AppMutation } from '../../../graphql/mutations/AppMutation';
|
||||
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { findProjectRootAsync, saveProjectIdToAppConfigAsync } from '../../../project/projectUtils';
|
||||
import { confirmAsync, promptAsync } from '../../../prompts';
|
||||
import { ensureLoggedInAsync } from '../../../user/actions';
|
||||
import ProjectInit from '../init';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('@expo/config');
|
||||
jest.mock('../../../project/projectUtils');
|
||||
jest.mock('../../../prompts');
|
||||
jest.mock('../../../user/actions');
|
||||
jest.mock('../../../graphql/mutations/AppMutation');
|
||||
jest.mock('../../../ora', () => ({
|
||||
ora: () => ({
|
||||
start: () => ({ succeed: () => {}, fail: () => {} }),
|
||||
}),
|
||||
}));
|
||||
jest.mock('../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
|
||||
|
||||
let originalProcessArgv: string[];
|
||||
|
||||
beforeAll(() => {
|
||||
originalProcessArgv = process.argv;
|
||||
process.argv = [];
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.argv = originalProcessArgv;
|
||||
});
|
||||
|
||||
function mockTestProject(options: {
|
||||
configuredProjectId?: string;
|
||||
configuredOwner?: string;
|
||||
}): void {
|
||||
const projectRoot = '/test-project';
|
||||
const packageJSON: PackageJSONConfig = {
|
||||
name: 'testing123',
|
||||
version: '0.1.0',
|
||||
description: 'fake description',
|
||||
main: 'index.js',
|
||||
};
|
||||
|
||||
const appJSON: AppJSONConfig = {
|
||||
expo: {
|
||||
name: 'testing 123',
|
||||
version: '0.1.0',
|
||||
slug: 'testing-123',
|
||||
sdkVersion: '33.0.0',
|
||||
owner: options.configuredOwner,
|
||||
extra: {
|
||||
eas: {
|
||||
projectId: options.configuredProjectId,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
[projectRoot + '/package.json']: JSON.stringify(packageJSON, null, 2),
|
||||
[projectRoot + '/app.json']: JSON.stringify(appJSON, null, 2),
|
||||
},
|
||||
'/'
|
||||
);
|
||||
|
||||
const mockManifest = { exp: appJSON.expo };
|
||||
|
||||
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
|
||||
jest.mocked(findProjectRootAsync).mockResolvedValue('/test-project');
|
||||
}
|
||||
|
||||
const commandOptions = { root: '/test-project' } as any;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
jest.mocked(ensureLoggedInAsync).mockResolvedValue(jester);
|
||||
});
|
||||
|
||||
describe(ProjectInit.name, () => {
|
||||
describe('when id flag is provided', () => {
|
||||
describe('when it is already configured', () => {
|
||||
beforeEach(() => {
|
||||
mockTestProject({ configuredProjectId: '1234', configuredOwner: jester.accounts[0].name });
|
||||
});
|
||||
|
||||
describe('interactive', () => {
|
||||
it('is no-op if already configured for id', async () => {
|
||||
await new ProjectInit(['--id', '1234'], commandOptions).run();
|
||||
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prompts to overwrite when different', async () => {
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
await new ProjectInit(['--id', '12345'], commandOptions).run();
|
||||
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(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
expect(confirmAsync).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('force', () => {
|
||||
it('does not prompt to overwrite when different', async () => {
|
||||
await new ProjectInit(['--id', '12345', '--force'], {
|
||||
root: '/test-project',
|
||||
} as any).run();
|
||||
expect(saveProjectIdToAppConfigAsync).toHaveBeenCalledWith('/test-project', '12345');
|
||||
expect(confirmAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('non-interactive', () => {
|
||||
it('aborts when different', async () => {
|
||||
await expect(
|
||||
new ProjectInit(['--id', '12345', '--non-interactive'], {
|
||||
root: '/test-project',
|
||||
} as any).run()
|
||||
).rejects.toThrowError(
|
||||
`Project is already linked to a different ID: ${chalk.bold(
|
||||
'1234'
|
||||
)}. Use --force flag to overwrite.`
|
||||
);
|
||||
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when it is not yet configured', () => {
|
||||
beforeEach(() => {
|
||||
mockTestProject({});
|
||||
});
|
||||
|
||||
it('configures', async () => {
|
||||
await new ProjectInit(['--id', '1234'], commandOptions).run();
|
||||
expect(saveProjectIdToAppConfigAsync).toHaveBeenCalledWith('/test-project', '1234');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when no arguments are provided', () => {
|
||||
describe('when it is already configured', () => {
|
||||
beforeEach(() => {
|
||||
mockTestProject({ configuredProjectId: '1234', configuredOwner: jester.accounts[0].name });
|
||||
});
|
||||
|
||||
it('does not configure', async () => {
|
||||
await new ProjectInit([], commandOptions).run();
|
||||
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when it is not yet configured', () => {
|
||||
describe('when owner is specified', () => {
|
||||
beforeEach(() => {
|
||||
mockTestProject({ configuredOwner: jester.accounts[0].name });
|
||||
});
|
||||
|
||||
describe('when project exists on server', () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue('123456');
|
||||
});
|
||||
|
||||
it('prompts for confirmation to link', async () => {
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
await new ProjectInit([], commandOptions).run();
|
||||
expect(saveProjectIdToAppConfigAsync).toHaveBeenCalledWith('/test-project', '123456');
|
||||
expect(confirmAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not save when confirmation to link is denied', async () => {
|
||||
jest.mocked(confirmAsync).mockResolvedValue(false);
|
||||
await expect(new ProjectInit([], commandOptions).run()).rejects.toThrow(
|
||||
'Project ID configuration canceled. Re-run the command to select a different account/project.'
|
||||
);
|
||||
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
expect(confirmAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when project does not exist on server', () => {
|
||||
it('asks to create it', async () => {
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
jest.mocked(AppMutation.createAppAsync).mockResolvedValue('0129');
|
||||
await new ProjectInit([], commandOptions).run();
|
||||
expect(saveProjectIdToAppConfigAsync).toHaveBeenCalledWith('/test-project', '0129');
|
||||
expect(confirmAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not create it if declined', async () => {
|
||||
jest.mocked(confirmAsync).mockResolvedValue(false);
|
||||
await expect(new ProjectInit([], commandOptions).run()).rejects.toThrowError(
|
||||
'Project ID configuration canceled for @jester/testing-123.'
|
||||
);
|
||||
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
expect(confirmAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when owner is not specified', () => {
|
||||
beforeEach(() => {
|
||||
mockTestProject({});
|
||||
});
|
||||
|
||||
it('prompts for an account', async () => {
|
||||
jest.mocked(promptAsync).mockResolvedValue({ account: { name: 'other' } });
|
||||
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(promptAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when the user does not have permission to create projects on account', async () => {
|
||||
mockTestProject({ configuredOwner: jester.accounts[1].name });
|
||||
jest.mocked(AppMutation.createAppAsync).mockResolvedValue('0129');
|
||||
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(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,11 @@
|
||||
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';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
|
||||
async function projectInfoByIdAsync(appId: string): Promise<AppInfoQuery> {
|
||||
@@ -32,18 +34,13 @@ async function projectInfoByIdAsync(appId: string): Promise<AppInfoQuery> {
|
||||
export default class ProjectInfo extends EasCommand {
|
||||
static override description = 'information about the current project';
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { projectId } = await this.getContextAsync(ProjectInfo, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const { app } = await projectInfoByIdAsync(projectId);
|
||||
if (!app) {
|
||||
throw new Error(`Could not find project with id: ${projectId}`);
|
||||
throw new Error(`Could not find project with ID: ${projectId}`);
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
@@ -1,229 +1,32 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
import terminalLink from 'terminal-link';
|
||||
|
||||
import { getProjectDashboardUrl } from '../../build/utils/url';
|
||||
import EasCommand, { EASCommandLoggedInContext } from '../../commandUtils/EasCommand';
|
||||
import { AppPrivacy, Role } from '../../graphql/generated';
|
||||
import { AppMutation } from '../../graphql/mutations/AppMutation';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import {
|
||||
fetchProjectIdFromServerAsync,
|
||||
findProjectRootAsync,
|
||||
saveProjectIdToAppConfigAsync,
|
||||
toAppPrivacy,
|
||||
} from '../../project/projectUtils';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { Actor } from '../../user/User';
|
||||
|
||||
type InitializeMethodOptions = {
|
||||
force: boolean;
|
||||
nonInteractive: boolean;
|
||||
};
|
||||
|
||||
export default class ProjectInit extends EasCommand {
|
||||
static override description = 'create or link an EAS project';
|
||||
static override aliases = ['init'];
|
||||
|
||||
static override flags = {
|
||||
id: Flags.string({
|
||||
description: 'ID of the EAS project to link',
|
||||
}),
|
||||
force: Flags.boolean({
|
||||
description: 'Whether to overwrite any existing project ID',
|
||||
dependsOn: ['id'],
|
||||
}),
|
||||
// this is the same as EASNonInteractiveFlag but with the dependsOn
|
||||
'non-interactive': Flags.boolean({
|
||||
description: 'Run the command in non-interactive mode.',
|
||||
dependsOn: ['id'],
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandLoggedInContext,
|
||||
};
|
||||
|
||||
private static async saveProjectIdAndLogSuccessAsync(
|
||||
projectDir: string,
|
||||
projectId: string
|
||||
): Promise<void> {
|
||||
await saveProjectIdToAppConfigAsync(projectDir, projectId);
|
||||
Log.withTick(`Project successfully linked (ID: ${chalk.bold(projectId)}) (modified app.json)`);
|
||||
}
|
||||
|
||||
private static async initializeWithExplicitIDAsync(
|
||||
projectId: string,
|
||||
{ force, nonInteractive }: InitializeMethodOptions
|
||||
): Promise<void> {
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const existingProjectId = exp.extra?.eas?.projectId;
|
||||
|
||||
if (projectId === existingProjectId) {
|
||||
Log.succeed(`Project already linked (ID: ${chalk.bold(existingProjectId)})`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!existingProjectId) {
|
||||
await ProjectInit.saveProjectIdAndLogSuccessAsync(projectDir, projectId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (projectId !== existingProjectId) {
|
||||
if (force) {
|
||||
await ProjectInit.saveProjectIdAndLogSuccessAsync(projectDir, projectId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
`Project is already linked to a different ID: ${chalk.bold(
|
||||
existingProjectId
|
||||
)}. Use --force flag to overwrite.`
|
||||
);
|
||||
}
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: `Project is already linked to a different ID: ${chalk.bold(
|
||||
existingProjectId
|
||||
)}. Do you wish to overwrite it?`,
|
||||
});
|
||||
if (!confirm) {
|
||||
Log.log('Aborting');
|
||||
return;
|
||||
}
|
||||
|
||||
await ProjectInit.saveProjectIdAndLogSuccessAsync(projectDir, projectId);
|
||||
}
|
||||
}
|
||||
|
||||
private static async initializeWithInteractiveSelectionAsync(actor: Actor): Promise<void> {
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const existingProjectId = exp.extra?.eas?.projectId;
|
||||
|
||||
if (existingProjectId) {
|
||||
Log.succeed(
|
||||
`Project already linked (ID: ${chalk.bold(
|
||||
existingProjectId
|
||||
)}). To re-configure, remove the extra.eas.projectId field from your app config.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const allAccounts = actor.accounts;
|
||||
const accountNamesWhereUserHasSufficientPermissionsToCreateApp = new Set(
|
||||
allAccounts
|
||||
.filter(a => a.users.find(it => it.actor.id === actor.id)?.role !== Role.ViewOnly)
|
||||
.map(it => it.name)
|
||||
);
|
||||
|
||||
// if no owner field, ask the user which account they want to use to create/link the project
|
||||
let accountName = exp.owner;
|
||||
if (!accountName) {
|
||||
if (allAccounts.length === 1) {
|
||||
accountName = allAccounts[0].name;
|
||||
} else {
|
||||
// if regular user, put primary account first
|
||||
const sortedAccounts =
|
||||
actor.__typename === 'Robot'
|
||||
? allAccounts
|
||||
: [...allAccounts].sort((a, _b) => (a.name === actor.username ? -1 : 1));
|
||||
|
||||
const choices = sortedAccounts.map(account => ({
|
||||
title: account.name,
|
||||
value: account,
|
||||
description: !accountNamesWhereUserHasSufficientPermissionsToCreateApp.has(account.name)
|
||||
? '(Viewer Role)'
|
||||
: undefined,
|
||||
}));
|
||||
accountName = (
|
||||
await promptAsync({
|
||||
type: 'select',
|
||||
name: 'account',
|
||||
message: 'Which account should own this project?',
|
||||
choices,
|
||||
})
|
||||
).account.name;
|
||||
}
|
||||
}
|
||||
|
||||
if (!accountName) {
|
||||
throw new Error('No account selected for project. Canceling.');
|
||||
}
|
||||
|
||||
const projectName = exp.slug;
|
||||
const projectFullName = `@${accountName}/${projectName}`;
|
||||
const existingProjectIdOnServer = await findProjectIdByAccountNameAndSlugNullableAsync(
|
||||
accountName,
|
||||
projectName
|
||||
);
|
||||
if (existingProjectIdOnServer) {
|
||||
const affirmedLink = await confirmAsync({
|
||||
message: `Existing project found: ${projectFullName} (ID: ${existingProjectIdOnServer}). Link this project?`,
|
||||
});
|
||||
if (!affirmedLink) {
|
||||
throw new Error(
|
||||
`Project ID configuration canceled. Re-run the command to select a different account/project.`
|
||||
);
|
||||
}
|
||||
|
||||
await ProjectInit.saveProjectIdAndLogSuccessAsync(projectDir, existingProjectIdOnServer);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!accountNamesWhereUserHasSufficientPermissionsToCreateApp.has(accountName)) {
|
||||
throw new Error(
|
||||
`You don't have permission to create a new project on the ${accountName} account and no matching project already exists on the account.`
|
||||
);
|
||||
}
|
||||
|
||||
const affirmedCreate = await confirmAsync({
|
||||
message: `Would you like to create a project for ${projectFullName}?`,
|
||||
});
|
||||
if (!affirmedCreate) {
|
||||
throw new Error(`Project ID configuration canceled for ${projectFullName}.`);
|
||||
}
|
||||
|
||||
const projectDashboardUrl = getProjectDashboardUrl(accountName, projectName);
|
||||
const projectLink = terminalLink(projectFullName, projectDashboardUrl, {
|
||||
// https://github.com/sindresorhus/terminal-link/issues/18#issuecomment-1068020361
|
||||
fallback: () => `${projectFullName} (${projectDashboardUrl})`,
|
||||
});
|
||||
|
||||
const account = nullthrows(allAccounts.find(a => a.name === accountName));
|
||||
|
||||
const spinner = ora(`Creating ${chalk.bold(projectFullName)}`).start();
|
||||
let createdProjectId: string;
|
||||
try {
|
||||
createdProjectId = await AppMutation.createAppAsync({
|
||||
accountId: account.id,
|
||||
projectName,
|
||||
privacy: toAppPrivacy(exp.privacy) ?? AppPrivacy.Public,
|
||||
});
|
||||
spinner.succeed(`Created ${chalk.bold(projectLink)}`);
|
||||
} catch (err) {
|
||||
spinner.fail();
|
||||
throw err;
|
||||
}
|
||||
|
||||
await ProjectInit.saveProjectIdAndLogSuccessAsync(projectDir, createdProjectId);
|
||||
}
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
flags: { id, force, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(ProjectInit);
|
||||
const { actor } = await this.getContextAsync(ProjectInit, { nonInteractive });
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
|
||||
if (id) {
|
||||
await ProjectInit.initializeWithExplicitIDAsync(id, { force, nonInteractive });
|
||||
} else {
|
||||
await ProjectInit.initializeWithInteractiveSelectionAsync(actor);
|
||||
if (exp.extra?.eas?.projectId) {
|
||||
Log.error(
|
||||
`app.json is already linked to project with ID: ${chalk.bold(exp.extra?.eas?.projectId)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const projectId = await fetchProjectIdFromServerAsync(exp);
|
||||
await saveProjectIdToAppConfigAsync(projectDir, projectId);
|
||||
|
||||
Log.withTick(`Linked app.json to project with ID ${chalk.bold(projectId)}`);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,23 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
|
||||
import {
|
||||
EnvironmentSecretScope,
|
||||
EnvironmentSecretsQuery,
|
||||
} from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import {
|
||||
SecretType,
|
||||
SecretTypeToEnvironmentSecretType,
|
||||
} from '../../graphql/types/EnvironmentSecret';
|
||||
import Log from '../../log';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import {
|
||||
getDisplayNameForProjectIdAsync,
|
||||
getOwnerAccountForProjectIdAsync,
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync, selectAsync } from '../../prompts';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { getActorDisplayName } from '../../user/User';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
export default class EnvironmentSecretCreate extends EasCommand {
|
||||
static override description =
|
||||
@@ -36,46 +33,29 @@ export default class EnvironmentSecretCreate extends EasCommand {
|
||||
description: 'Name of the secret',
|
||||
}),
|
||||
value: Flags.string({
|
||||
description: 'Text value or path to a file to store in the secret',
|
||||
}),
|
||||
type: Flags.enum({
|
||||
description: 'The type of secret',
|
||||
options: [SecretType.STRING, SecretType.FILE],
|
||||
description: 'Value of the secret',
|
||||
}),
|
||||
force: Flags.boolean({
|
||||
description: 'Delete and recreate existing secrets',
|
||||
default: false,
|
||||
}),
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const actor = await ensureLoggedInAsync();
|
||||
let {
|
||||
flags: {
|
||||
name,
|
||||
value: secretValue,
|
||||
scope,
|
||||
force,
|
||||
type: secretType,
|
||||
'non-interactive': nonInteractive,
|
||||
},
|
||||
flags: { name, value: secretValue, scope, force },
|
||||
} = await this.parse(EnvironmentSecretCreate);
|
||||
const { projectId } = await this.getContextAsync(EnvironmentSecretCreate, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
|
||||
const ownerAccount = await getOwnerAccountForProjectIdAsync(projectId);
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
const { slug } = exp;
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!scope) {
|
||||
const validationMessage = 'Secret scope may not be empty.';
|
||||
if (nonInteractive) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
|
||||
({ scope } = await promptAsync({
|
||||
type: 'select',
|
||||
@@ -90,18 +70,13 @@ export default class EnvironmentSecretCreate extends EasCommand {
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
const validationMessage = 'Secret name may not be empty.';
|
||||
if (nonInteractive) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
|
||||
({ name } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: `Secret name:`,
|
||||
validate: value => {
|
||||
if (!value) {
|
||||
return validationMessage;
|
||||
return 'Secret name may not be empty.';
|
||||
}
|
||||
|
||||
// this validation regex here is just to shorten the feedback loop
|
||||
@@ -115,82 +90,42 @@ export default class EnvironmentSecretCreate extends EasCommand {
|
||||
}));
|
||||
|
||||
if (!name) {
|
||||
throw new Error(validationMessage);
|
||||
throw new Error('Secret name may not be empty.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!secretType) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Secret type may not be empty in non-interactive mode');
|
||||
}
|
||||
|
||||
secretType = await selectAsync('Select secret type', [
|
||||
{
|
||||
title: 'string',
|
||||
value: SecretType.STRING,
|
||||
},
|
||||
{
|
||||
title: 'file',
|
||||
value: SecretType.FILE,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
if (!secretValue) {
|
||||
const validationMessage = 'Secret value may not be empty.';
|
||||
if (nonInteractive) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
|
||||
({ secretValue } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'secretValue',
|
||||
message: secretType === SecretType.STRING ? 'Secret value:' : 'Local file path:',
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
validate: async secretValue => {
|
||||
if (!secretValue) {
|
||||
return 'Secret value may not be empty.';
|
||||
}
|
||||
if (secretType === SecretType.FILE) {
|
||||
const secretFilePath = path.resolve(secretValue);
|
||||
if (!(await fs.pathExists(secretFilePath))) {
|
||||
return `File "${secretValue}" does not exist.`;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
message: 'Secret value:',
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
assert(secretValue);
|
||||
|
||||
let secretFilePath: string | undefined;
|
||||
if (secretType === SecretType.FILE) {
|
||||
secretFilePath = path.resolve(secretValue);
|
||||
if (!(await fs.pathExists(secretFilePath))) {
|
||||
throw new Error(`File "${secretValue}" does not exist`);
|
||||
if (!secretValue) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (scope === EnvironmentSecretScope.PROJECT) {
|
||||
if (force) {
|
||||
const { appSecrets: existingSecrets } = await EnvironmentSecretsQuery.byAppIdAsync(
|
||||
projectId
|
||||
);
|
||||
const existingSecrets = await EnvironmentSecretsQuery.byAppIdAsync(projectId);
|
||||
const existingSecret = existingSecrets.find(secret => secret.name === name);
|
||||
|
||||
if (existingSecret) {
|
||||
await EnvironmentSecretMutation.deleteAsync(existingSecret.id);
|
||||
Log.withTick(
|
||||
`Deleting existing secret ${chalk.bold(name)} on project ${chalk.bold(
|
||||
projectDisplayName
|
||||
`@${accountName}/${slug}`
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const secret = await EnvironmentSecretMutation.createForAppAsync(
|
||||
{ name, value: secretValue, type: SecretTypeToEnvironmentSecretType[secretType] },
|
||||
{ name, value: secretValue },
|
||||
projectId
|
||||
);
|
||||
if (!secret) {
|
||||
@@ -199,24 +134,25 @@ export default class EnvironmentSecretCreate extends EasCommand {
|
||||
);
|
||||
}
|
||||
|
||||
if (secretType === SecretType.STRING) {
|
||||
Log.withTick(
|
||||
`️Created a new secret ${chalk.bold(name)} with value ${chalk.bold(
|
||||
secretValue
|
||||
)} on project ${chalk.bold(projectDisplayName)}.`
|
||||
);
|
||||
} else {
|
||||
Log.withTick(
|
||||
`️Created a new secret ${chalk.bold(name)} from file ${chalk.bold(
|
||||
secretFilePath
|
||||
)} on project ${chalk.bold(projectDisplayName)}.`
|
||||
);
|
||||
}
|
||||
Log.withTick(
|
||||
`️Created a new secret ${chalk.bold(name)} on project ${chalk.bold(
|
||||
`@${accountName}/${slug}`
|
||||
)}.`
|
||||
);
|
||||
} else if (scope === EnvironmentSecretScope.ACCOUNT) {
|
||||
if (force) {
|
||||
const { accountSecrets: existingSecrets } = await EnvironmentSecretsQuery.byAppIdAsync(
|
||||
projectId
|
||||
const ownerAccount = findAccountByName(actor.accounts, accountName);
|
||||
|
||||
if (!ownerAccount) {
|
||||
Log.warn(
|
||||
`Your account (${getActorDisplayName(actor)}) doesn't have access to the ${chalk.bold(
|
||||
accountName
|
||||
)} account`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (force) {
|
||||
const existingSecrets = await EnvironmentSecretsQuery.byAccountNameAsync(ownerAccount.name);
|
||||
const existingSecret = existingSecrets.find(secret => secret.name === name);
|
||||
|
||||
if (existingSecret) {
|
||||
@@ -231,7 +167,7 @@ export default class EnvironmentSecretCreate extends EasCommand {
|
||||
}
|
||||
|
||||
const secret = await EnvironmentSecretMutation.createForAccountAsync(
|
||||
{ name, value: secretValue, type: SecretTypeToEnvironmentSecretType[secretType] },
|
||||
{ name, value: secretValue },
|
||||
ownerAccount.id
|
||||
);
|
||||
|
||||
@@ -241,19 +177,9 @@ export default class EnvironmentSecretCreate extends EasCommand {
|
||||
);
|
||||
}
|
||||
|
||||
if (secretType === SecretType.STRING) {
|
||||
Log.withTick(
|
||||
`️Created a new secret ${chalk.bold(name)} with value ${chalk.bold(
|
||||
secretValue
|
||||
)} on account ${chalk.bold(ownerAccount.name)}.`
|
||||
);
|
||||
} else {
|
||||
Log.withTick(
|
||||
`️Created a new secret ${chalk.bold(name)} from file ${chalk.bold(
|
||||
secretFilePath
|
||||
)} on account ${chalk.bold(ownerAccount.name)}.`
|
||||
);
|
||||
}
|
||||
Log.withTick(
|
||||
`️Created a new secret ${chalk.bold(name)} on account ${chalk.bold(ownerAccount.name)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
|
||||
import {
|
||||
EnvironmentSecretScope,
|
||||
@@ -9,6 +8,12 @@ import {
|
||||
EnvironmentSecretsQuery,
|
||||
} from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import Log from '../../log';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
|
||||
export default class EnvironmentSecretDelete extends EasCommand {
|
||||
@@ -18,29 +23,24 @@ export default class EnvironmentSecretDelete extends EasCommand {
|
||||
id: Flags.string({
|
||||
description: 'ID of the secret to delete',
|
||||
}),
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectAccountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
let {
|
||||
flags: { id, 'non-interactive': nonInteractive },
|
||||
flags: { id },
|
||||
} = await this.parse(EnvironmentSecretDelete);
|
||||
const { projectId } = await this.getContextAsync(EnvironmentSecretDelete, {
|
||||
nonInteractive,
|
||||
});
|
||||
let secret: EnvironmentSecretWithScope | undefined;
|
||||
|
||||
if (!id) {
|
||||
const validationMessage = 'You must select which secret to delete.';
|
||||
if (nonInteractive) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
|
||||
const secrets = await EnvironmentSecretsQuery.allAsync(projectId);
|
||||
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectId);
|
||||
|
||||
({ secret } = await promptAsync({
|
||||
type: 'autocomplete',
|
||||
name: 'secret',
|
||||
@@ -58,27 +58,25 @@ export default class EnvironmentSecretDelete extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
if (!nonInteractive) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`You are about to permanently delete secret${
|
||||
secret?.name ? ` "${secret?.name}"` : ''
|
||||
} with id: "${id}".\nThis action is irreversible.`
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`You are about to permanently delete secret${
|
||||
secret?.name ? ` "${secret?.name}"` : ''
|
||||
} with id: "${id}".\nThis action is irreversible.`
|
||||
);
|
||||
Log.newLine();
|
||||
const confirmed = await toggleConfirmAsync({
|
||||
message: `Are you sure you wish to proceed?${
|
||||
secret?.scope === EnvironmentSecretScope.ACCOUNT
|
||||
? ' This secret is applied across your whole account and may affect multiple apps.'
|
||||
: ''
|
||||
}`,
|
||||
});
|
||||
if (!confirmed) {
|
||||
Log.error(
|
||||
`Canceled deletion of secret${secret?.name ? ` "${secret?.name}"` : ''} with id: "${id}".`
|
||||
);
|
||||
Log.newLine();
|
||||
const confirmed = await toggleConfirmAsync({
|
||||
message: `Are you sure you wish to proceed?${
|
||||
secret?.scope === EnvironmentSecretScope.ACCOUNT
|
||||
? ' This secret is applied across your whole account and may affect multiple apps.'
|
||||
: ''
|
||||
}`,
|
||||
});
|
||||
if (!confirmed) {
|
||||
Log.error(
|
||||
`Canceled deletion of secret${secret?.name ? ` "${secret?.name}"` : ''} with id: "${id}".`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await EnvironmentSecretMutation.deleteAsync(id);
|
||||
|
||||
@@ -2,39 +2,39 @@ 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';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
|
||||
export default class EnvironmentSecretList extends EasCommand {
|
||||
static override description = 'list environment secrets available for your current app';
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { projectId } = await this.getContextAsync(EnvironmentSecretList, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectAccountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
const secrets = await EnvironmentSecretsQuery.allAsync(projectId);
|
||||
if (!projectDir) {
|
||||
throw new Error("Run this command inside your project's directory");
|
||||
}
|
||||
|
||||
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectId);
|
||||
|
||||
const table = new Table({
|
||||
head: ['Name', 'Type', 'Scope', 'ID', 'Updated at'],
|
||||
head: ['Name', 'Scope', 'ID', 'Updated at'],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const secret of secrets) {
|
||||
const { name, createdAt: updatedAt, scope, id, type } = secret;
|
||||
table.push([
|
||||
name,
|
||||
EnvironmentSecretTypeToSecretType[type],
|
||||
scope,
|
||||
id,
|
||||
dateFormat(updatedAt, 'mmm dd HH:MM:ss'),
|
||||
]);
|
||||
const { name, createdAt: updatedAt, scope, id } = secret;
|
||||
table.push([name, scope, id, dateFormat(updatedAt, 'mmm dd HH:MM:ss')]);
|
||||
}
|
||||
|
||||
Log.log(chalk`{bold Secrets for this account and project:}`);
|
||||
|
||||
@@ -2,10 +2,7 @@ import { EasJsonAccessor } from '@expo/eas-json';
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand, {
|
||||
EASCommandLoggedInContext,
|
||||
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';
|
||||
@@ -16,7 +13,8 @@ import {
|
||||
selectRequestedPlatformAsync,
|
||||
toPlatforms,
|
||||
} from '../platform';
|
||||
import { findProjectRootAsync } from '../project/projectUtils';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { SubmitArchiveFlags, createSubmissionContextAsync } from '../submit/context';
|
||||
import {
|
||||
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish,
|
||||
@@ -93,18 +91,14 @@ export default class Submit extends EasCommand {
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandLoggedInContext,
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags: rawFlags } = await this.parse(Submit);
|
||||
const { actor, projectId } = await this.getContextAsync(Submit, { nonInteractive: false });
|
||||
|
||||
const flags = this.sanitizeFlags(rawFlags);
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
await maybeWarnAboutEasOutagesAsync([StatuspageServiceName.EasSubmit]);
|
||||
|
||||
@@ -127,7 +121,6 @@ export default class Submit extends EasCommand {
|
||||
profile: submissionProfile.profile,
|
||||
archiveFlags: flagsWithPlatform.archiveFlags,
|
||||
nonInteractive: flagsWithPlatform.nonInteractive,
|
||||
actor,
|
||||
});
|
||||
|
||||
if (submissionProfiles.length > 1) {
|
||||
|
||||
@@ -5,13 +5,14 @@ import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { getEASUpdateURL } from '../../api';
|
||||
import EasCommand, { 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 {
|
||||
findProjectRootAsync,
|
||||
getProjectIdAsync,
|
||||
installExpoUpdatesAsync,
|
||||
isExpoUpdatesInstalledOrAvailable,
|
||||
} from '../../project/projectUtils';
|
||||
@@ -34,18 +35,11 @@ export default class UpdateConfigure extends EasCommand {
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
Log.log(
|
||||
'💡 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 } = await this.getContextAsync(UpdateConfigure, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
const platform = flags.platform as RequestedPlatform;
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
@@ -67,7 +61,6 @@ export default class UpdateConfigure extends EasCommand {
|
||||
android: androidWorkflow,
|
||||
ios: iosWorkflow,
|
||||
},
|
||||
projectId,
|
||||
});
|
||||
Log.withTick(`Configured ${chalk.bold('app.json')} for EAS Update`);
|
||||
|
||||
@@ -76,14 +69,14 @@ export default class UpdateConfigure extends EasCommand {
|
||||
[RequestedPlatform.Android, RequestedPlatform.All].includes(platform) &&
|
||||
androidWorkflow === Workflow.GENERIC
|
||||
) {
|
||||
await syncAndroidUpdatesConfigurationAsync(projectDir, updatedExp, projectId);
|
||||
await syncAndroidUpdatesConfigurationAsync(projectDir, updatedExp);
|
||||
Log.withTick(`Configured ${chalk.bold('AndroidManifest.xml')} for EAS Update`);
|
||||
}
|
||||
if (
|
||||
[RequestedPlatform.Ios, RequestedPlatform.All].includes(platform) &&
|
||||
iosWorkflow === Workflow.GENERIC
|
||||
) {
|
||||
await syncIosUpdatesConfigurationAsync(projectDir, updatedExp, projectId);
|
||||
await syncIosUpdatesConfigurationAsync(projectDir, updatedExp);
|
||||
Log.withTick(`Configured ${chalk.bold('Expo.plist')} for EAS Update`);
|
||||
}
|
||||
|
||||
@@ -97,7 +90,6 @@ async function configureAppJSONForEASUpdateAsync({
|
||||
exp,
|
||||
platform,
|
||||
workflows,
|
||||
projectId,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
@@ -105,9 +97,8 @@ async function configureAppJSONForEASUpdateAsync({
|
||||
workflows: {
|
||||
[key in RequestedPlatform.Android | RequestedPlatform.Ios]: Workflow;
|
||||
};
|
||||
projectId: string;
|
||||
}): Promise<ExpoConfig> {
|
||||
// this command is non-interactive in the way it was designed
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const easUpdateURL = getEASUpdateURL(projectId);
|
||||
const updates = { ...exp.updates, url: easUpdateURL };
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
DeleteUpdateGroupMutation,
|
||||
@@ -47,20 +47,21 @@ export default class UpdateDelete extends EasCommand {
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
json: Flags.boolean({
|
||||
description: `Return a json with the group ID of the deleted updates.`,
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
args: { groupId: group },
|
||||
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
|
||||
flags: { json: jsonFlag },
|
||||
} = await this.parse(UpdateDelete);
|
||||
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
if (!nonInteractive) {
|
||||
} else {
|
||||
const shouldAbort = await confirmAsync({
|
||||
message:
|
||||
`🚨${chalk.red('CAUTION')}🚨\n\n` +
|
||||
|
||||
@@ -4,23 +4,27 @@ import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import dateFormat from 'dateformat';
|
||||
import gql from 'graphql-tag';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { getEASUpdateURL } from '../../api';
|
||||
import { selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import { BranchNotFoundError, getDefaultBranchNameAsync } from '../../branch/utils';
|
||||
import { BRANCHES_LIMIT } from '../../branch/queries';
|
||||
import { getUpdateGroupUrl } from '../../build/utils/url';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import fetch from '../../fetch';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
GetUpdateGroupAsyncQuery,
|
||||
PublishUpdateGroupInput,
|
||||
Robot,
|
||||
RootQueryUpdatesByGroupArgs,
|
||||
StatuspageServiceName,
|
||||
Update,
|
||||
UpdateInfoGroup,
|
||||
UpdatePublishMutation,
|
||||
ViewBranchQueryVariables,
|
||||
User,
|
||||
ViewBranchUpdatesQuery,
|
||||
} from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import { BranchQuery } from '../../graphql/queries/BranchQuery';
|
||||
@@ -30,7 +34,8 @@ import { ora } from '../../ora';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getOwnerAccountForProjectIdAsync,
|
||||
getProjectAccountName,
|
||||
getProjectIdAsync,
|
||||
installExpoUpdatesAsync,
|
||||
isExpoUpdatesInstalledOrAvailable,
|
||||
} from '../../project/projectUtils';
|
||||
@@ -43,15 +48,16 @@ import {
|
||||
uploadAssetsAsync,
|
||||
} from '../../project/publish';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
|
||||
import { formatUpdateMessage } from '../../update/utils';
|
||||
import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
|
||||
import { formatUpdate } from '../../update/utils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import {
|
||||
checkManifestBodyAgainstUpdateInfoGroup,
|
||||
getCodeSigningInfoAsync,
|
||||
getManifestBodyAsync,
|
||||
signManifestBody,
|
||||
} from '../../utils/code-signing';
|
||||
import uniqBy from '../../utils/expodash/uniqBy';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
|
||||
@@ -62,6 +68,34 @@ import { createUpdateChannelOnAppAsync } from '../channel/create';
|
||||
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
|
||||
export type PublishPlatformFlag = PublishPlatform | 'all';
|
||||
|
||||
async function getUpdateGroupAsync({
|
||||
group,
|
||||
}: RootQueryUpdatesByGroupArgs): Promise<GetUpdateGroupAsyncQuery['updatesByGroup']> {
|
||||
const { updatesByGroup } = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<GetUpdateGroupAsyncQuery, RootQueryUpdatesByGroupArgs>(
|
||||
gql`
|
||||
query getUpdateGroupAsync($group: ID!) {
|
||||
updatesByGroup(group: $group) {
|
||||
id
|
||||
group
|
||||
runtimeVersion
|
||||
manifestFragment
|
||||
platform
|
||||
message
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
group,
|
||||
},
|
||||
{ additionalTypenames: ['Update'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return updatesByGroup;
|
||||
}
|
||||
|
||||
async function ensureChannelExistsAsync({
|
||||
appId,
|
||||
branchId,
|
||||
@@ -93,32 +127,37 @@ async function ensureChannelExistsAsync({
|
||||
export async function ensureBranchExistsAsync({
|
||||
appId,
|
||||
name: branchName,
|
||||
}: ViewBranchQueryVariables): Promise<{
|
||||
branchId: string;
|
||||
limit,
|
||||
offset,
|
||||
}: {
|
||||
appId: string;
|
||||
name: string;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}): Promise<{
|
||||
id: string;
|
||||
updates: Exclude<
|
||||
Exclude<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
|
||||
null | undefined
|
||||
>['updates'];
|
||||
}> {
|
||||
try {
|
||||
const updateBranch = await BranchQuery.getBranchByNameAsync({
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
|
||||
const { id } = updateBranch;
|
||||
const { app } = await UpdateQuery.viewBranchAsync({
|
||||
appId,
|
||||
name: branchName,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
const updateBranch = app?.byId.updateBranchByName;
|
||||
if (updateBranch) {
|
||||
const { id, updates } = updateBranch;
|
||||
await ensureChannelExistsAsync({ appId, branchId: id, channelName: branchName });
|
||||
return { branchId: id };
|
||||
} catch (error) {
|
||||
if (error instanceof BranchNotFoundError) {
|
||||
const newUpdateBranch = await createUpdateBranchOnAppAsync({ appId, name: branchName });
|
||||
Log.withTick(`Created branch: ${chalk.bold(branchName)}`);
|
||||
await ensureChannelExistsAsync({
|
||||
appId,
|
||||
branchId: newUpdateBranch.id,
|
||||
channelName: branchName,
|
||||
});
|
||||
return { branchId: newUpdateBranch.id };
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
return { id, updates };
|
||||
}
|
||||
|
||||
const newUpdateBranch = await createUpdateBranchOnAppAsync({ appId, name: branchName });
|
||||
Log.withTick(`Created branch: ${chalk.bold(branchName)}`);
|
||||
await ensureChannelExistsAsync({ appId, branchId: newUpdateBranch.id, channelName: branchName });
|
||||
return { id: newUpdateBranch.id, updates: [] };
|
||||
}
|
||||
|
||||
export default class UpdatePublish extends EasCommand {
|
||||
@@ -156,6 +195,10 @@ export default class UpdatePublish extends EasCommand {
|
||||
default: 'all',
|
||||
required: false,
|
||||
}),
|
||||
json: Flags.boolean({
|
||||
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
|
||||
default: false,
|
||||
}),
|
||||
auto: Flags.boolean({
|
||||
description:
|
||||
'Use the current git branch and commit message for the EAS branch and update message',
|
||||
@@ -165,32 +208,28 @@ export default class UpdatePublish extends EasCommand {
|
||||
description: `File containing the PEM-encoded private key corresponding to the certificate in expo-updates' configuration. Defaults to a file named "private-key.pem" in the certificate's directory.`,
|
||||
required: false,
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
'non-interactive': Flags.boolean({
|
||||
default: false,
|
||||
description: 'Run command in non-interactive mode',
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(UpdatePublish);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
let {
|
||||
branch: branchName,
|
||||
auto: autoFlag,
|
||||
message,
|
||||
republish,
|
||||
group,
|
||||
'input-dir': inputDir,
|
||||
'skip-bundler': skipBundler,
|
||||
platform,
|
||||
'private-key-path': privateKeyPath,
|
||||
'non-interactive': nonInteractive,
|
||||
json: jsonFlag,
|
||||
} = flags;
|
||||
const { projectId } = await this.getContextAsync(UpdatePublish, {
|
||||
nonInteractive,
|
||||
});
|
||||
flags: {
|
||||
branch: branchName,
|
||||
json: jsonFlag,
|
||||
auto: autoFlag,
|
||||
message,
|
||||
republish,
|
||||
group,
|
||||
'input-dir': inputDir,
|
||||
'skip-bundler': skipBundler,
|
||||
platform,
|
||||
'private-key-path': privateKeyPath,
|
||||
'non-interactive': nonInteractive,
|
||||
},
|
||||
} = await this.parse(UpdatePublish);
|
||||
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
@@ -198,7 +237,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
|
||||
const platformFlag = platform as PublishPlatformFlag;
|
||||
// If a group was specified, that means we are republishing it.
|
||||
republish = republish || !!group;
|
||||
republish = group ? true : republish;
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir, {
|
||||
@@ -240,40 +279,54 @@ export default class UpdatePublish extends EasCommand {
|
||||
}
|
||||
|
||||
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
|
||||
await checkEASUpdateURLIsSetAsync(exp, projectId);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
await checkEASUpdateURLIsSetAsync(exp);
|
||||
|
||||
if (!branchName && autoFlag) {
|
||||
branchName =
|
||||
(await getVcsClient().getBranchNameAsync()) ||
|
||||
`branch-${Math.random().toString(36).substr(2, 4)}`;
|
||||
}
|
||||
|
||||
if (!branchName) {
|
||||
if (autoFlag) {
|
||||
branchName = await getDefaultBranchNameAsync();
|
||||
} else if (nonInteractive) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --branch or use --auto when in non-interactive mode');
|
||||
} else {
|
||||
try {
|
||||
const branch = await selectBranchOnAppAsync({
|
||||
projectId,
|
||||
promptTitle: `Which branch would you like to ${
|
||||
republish ? 'republish' : 'publish'
|
||||
} on?`,
|
||||
displayTextForListItem: updateBranch =>
|
||||
`${updateBranch.name} ${chalk.grey(
|
||||
`- current update: ${formatUpdateMessage(updateBranch.updates[0])}`
|
||||
)}`,
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
branchName = branch.name;
|
||||
} catch {
|
||||
// unable to select a branch (network error or no branches for project)
|
||||
({ name: branchName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'No branches found. Provide a branch name:',
|
||||
initial: await getDefaultBranchNameAsync(),
|
||||
validate: value => (value ? true : 'Branch name may not be empty.'),
|
||||
}));
|
||||
}
|
||||
|
||||
assert(branchName, 'Branch name must be specified.');
|
||||
}
|
||||
|
||||
const validationMessage = 'Branch name may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
|
||||
const branches = await BranchQuery.listBranchesAsync({
|
||||
appId: projectId,
|
||||
limit: BRANCHES_LIMIT,
|
||||
offset: 0,
|
||||
});
|
||||
if (branches.length === 0) {
|
||||
({ name: branchName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'No branches found. Provide a branch name:',
|
||||
initial:
|
||||
(await getVcsClient().getBranchNameAsync()) ||
|
||||
`branch-${Math.random().toString(36).substr(2, 4)}`,
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
} else {
|
||||
branchName = await selectAsync<string>(
|
||||
'Which branch would you like to publish on?',
|
||||
branches.map(branch => {
|
||||
return {
|
||||
title: `${branch.name} ${chalk.grey(
|
||||
`- current update: ${formatUpdate(branch.updates[0])}`
|
||||
)}`,
|
||||
value: branch.name,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
assert(branchName, 'Branch name must be specified.');
|
||||
}
|
||||
|
||||
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
|
||||
@@ -289,18 +342,23 @@ export default class UpdatePublish extends EasCommand {
|
||||
'group' | 'message' | 'runtimeVersion' | 'manifestFragment' | 'platform'
|
||||
>[];
|
||||
if (group) {
|
||||
const updatesByGroup = await UpdateQuery.viewUpdateGroupAsync({ groupId: group });
|
||||
updatesToRepublish = updatesByGroup;
|
||||
updatesToRepublish = await getUpdateGroupAsync({ group });
|
||||
} else {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --group when in non-interactive mode');
|
||||
}
|
||||
|
||||
updatesToRepublish = await selectUpdateGroupOnBranchAsync({
|
||||
// Drop into interactive mode if the user has not specified an update group to republish.
|
||||
if (jsonFlag) {
|
||||
throw new Error('You must specify the update group to republish.');
|
||||
}
|
||||
|
||||
updatesToRepublish = await getUpdatesToRepublishInteractiveAsync(
|
||||
projectId,
|
||||
branchName,
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
platformFlag,
|
||||
50
|
||||
);
|
||||
}
|
||||
const updatesToRepublishFilteredByPlatform = updatesToRepublish.filter(
|
||||
// Only republish to the specified platforms
|
||||
@@ -426,9 +484,11 @@ export default class UpdatePublish extends EasCommand {
|
||||
.map(pair => pair[0]);
|
||||
}
|
||||
|
||||
const { branchId } = await ensureBranchExistsAsync({
|
||||
const { id: branchId } = await ensureBranchExistsAsync({
|
||||
appId: projectId,
|
||||
name: branchName,
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
// Sort the updates into different groups based on their platform specific runtime versions
|
||||
@@ -534,7 +594,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
const updateGroupId = newUpdatesForRuntimeVersion[0].group;
|
||||
|
||||
const projectName = exp.slug;
|
||||
const accountName = (await getOwnerAccountForProjectIdAsync(projectId)).name;
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
const updateGroupUrl = getUpdateGroupUrl(accountName, projectName, updateGroupId);
|
||||
const updateGroupLink = link(updateGroupUrl, { dim: false });
|
||||
|
||||
@@ -570,6 +630,73 @@ export default class UpdatePublish extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUpdatesToRepublishInteractiveAsync(
|
||||
projectId: string,
|
||||
branchName: string,
|
||||
platformFlag: string,
|
||||
pageSize: number,
|
||||
offset: number = 0,
|
||||
cumulativeUpdates: Exclude<
|
||||
Exclude<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
|
||||
null | undefined
|
||||
>['updates'] = []
|
||||
): Promise<
|
||||
Exclude<
|
||||
Exclude<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
|
||||
null | undefined
|
||||
>['updates']
|
||||
> {
|
||||
const fetchMoreValue = '_fetchMore';
|
||||
|
||||
const { updates } = await ensureBranchExistsAsync({
|
||||
appId: projectId,
|
||||
name: branchName,
|
||||
limit: pageSize + 1, // fetch an extra item so we know if there are additional pages
|
||||
offset,
|
||||
});
|
||||
cumulativeUpdates = [
|
||||
...cumulativeUpdates,
|
||||
// drop that extra item used for pagination from our render logic
|
||||
...updates.slice(0, updates.length - 1),
|
||||
];
|
||||
const cumulativeUpdatesForTargetPlatforms =
|
||||
platformFlag === 'all'
|
||||
? cumulativeUpdates
|
||||
: cumulativeUpdates.filter(update => {
|
||||
// Only show groups that have updates on the specified platform(s).
|
||||
return update.platform === platformFlag;
|
||||
});
|
||||
const updateGroups = uniqBy(cumulativeUpdatesForTargetPlatforms, u => u.group).map(update => ({
|
||||
title: formatUpdateTitle(update),
|
||||
value: update.group,
|
||||
}));
|
||||
if (updateGroups.length === 0) {
|
||||
throw new Error(
|
||||
`There are no updates on branch "${branchName}" published for the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
|
||||
);
|
||||
}
|
||||
|
||||
if (updates.length > pageSize) {
|
||||
updateGroups.push({ title: 'Next page...', value: fetchMoreValue });
|
||||
}
|
||||
|
||||
const selectedUpdateGroup = await selectAsync<string>(
|
||||
'Which update would you like to republish?',
|
||||
updateGroups
|
||||
);
|
||||
if (selectedUpdateGroup === fetchMoreValue) {
|
||||
return await getUpdatesToRepublishInteractiveAsync(
|
||||
projectId,
|
||||
branchName,
|
||||
platformFlag,
|
||||
pageSize,
|
||||
offset + pageSize,
|
||||
cumulativeUpdates
|
||||
);
|
||||
}
|
||||
return cumulativeUpdates.filter(update => update.group === selectedUpdateGroup);
|
||||
}
|
||||
|
||||
async function getRuntimeVersionObjectAsync(
|
||||
exp: ExpoConfig,
|
||||
platformFlag: PublishPlatformFlag,
|
||||
@@ -600,8 +727,37 @@ async function getRuntimeVersionObjectAsync(
|
||||
);
|
||||
}
|
||||
|
||||
async function checkEASUpdateURLIsSetAsync(exp: ExpoConfig, projectId: string): Promise<void> {
|
||||
function formatUpdateTitle(
|
||||
update: Exclude<
|
||||
Exclude<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
|
||||
null | undefined
|
||||
>['updates'][number]
|
||||
): string {
|
||||
const { message, createdAt, actor, runtimeVersion } = update;
|
||||
|
||||
let actorName: string;
|
||||
switch (actor?.__typename) {
|
||||
case 'User': {
|
||||
actorName = (actor as Pick<User, 'username' | 'id'>).username;
|
||||
break;
|
||||
}
|
||||
case 'Robot': {
|
||||
const { firstName, id } = actor as Pick<Robot, 'firstName' | 'id'>;
|
||||
actorName = firstName ?? `robot: ${id.slice(0, 4)}...`;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
actorName = 'unknown';
|
||||
}
|
||||
return `[${dateFormat(
|
||||
createdAt,
|
||||
'mmm dd HH:MM'
|
||||
)} by ${actorName}, runtimeVersion: ${runtimeVersion}] ${message}`;
|
||||
}
|
||||
|
||||
async function checkEASUpdateURLIsSetAsync(exp: ExpoConfig): Promise<void> {
|
||||
const configuredURL = exp.updates?.url;
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const expectedURL = getEASUpdateURL(projectId);
|
||||
|
||||
if (configuredURL !== expectedURL) {
|
||||
|
||||
@@ -1,86 +1,124 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import CliTable from 'cli-table3';
|
||||
|
||||
import { selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
|
||||
import Log from '../../log';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { UPDATES_LIMIT } from '../../update/queries';
|
||||
import {
|
||||
EasPaginatedQueryFlags,
|
||||
getLimitFlagWithCustomValues,
|
||||
getPaginatedQueryOptions,
|
||||
} from '../../commandUtils/pagination';
|
||||
import {
|
||||
listAndRenderUpdateGroupsOnAppAsync,
|
||||
listAndRenderUpdateGroupsOnBranchAsync,
|
||||
} from '../../update/queries';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
UPDATE_COLUMNS,
|
||||
UpdateGroupDescription,
|
||||
formatUpdate,
|
||||
getUpdateGroupsWithPlatforms,
|
||||
} from '../../update/utils';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
|
||||
export default class UpdateList extends EasCommand {
|
||||
static override description = 'view the recent updates';
|
||||
export default class BranchView extends EasCommand {
|
||||
static override description = 'view the recent updates for a branch';
|
||||
|
||||
static override flags = {
|
||||
branch: Flags.string({
|
||||
description: 'List updates only on this branch',
|
||||
description: 'List all updates on this branch',
|
||||
exclusive: ['all'],
|
||||
}),
|
||||
all: Flags.boolean({
|
||||
description: 'List updates on all branches',
|
||||
description: 'List all updates associated with this project',
|
||||
exclusive: ['branch'],
|
||||
default: false,
|
||||
}),
|
||||
...EasPaginatedQueryFlags,
|
||||
limit: getLimitFlagWithCustomValues({ defaultTo: 25, limit: 50 }),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
json: Flags.boolean({
|
||||
description: `Return a json with all of the recent update groups.`,
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
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, {
|
||||
nonInteractive,
|
||||
});
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
|
||||
const {
|
||||
flags: { branch: branchFlag, all, json: jsonFlag },
|
||||
} = await this.parse(BranchView);
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
let updateGroupDescriptions: UpdateGroupDescription[];
|
||||
if (all) {
|
||||
listAndRenderUpdateGroupsOnAppAsync({ projectId, paginatedQueryOptions });
|
||||
const branchesAndUpdates = await UpdateQuery.viewAllAsync({
|
||||
appId: projectId,
|
||||
limit: UPDATES_LIMIT,
|
||||
offset: 0,
|
||||
});
|
||||
updateGroupDescriptions = getUpdateGroupsWithPlatforms(
|
||||
branchesAndUpdates.app.byId.updates.map(update => ({
|
||||
...update,
|
||||
branch: update.branch.name,
|
||||
}))
|
||||
).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
} else {
|
||||
if (branchFlag) {
|
||||
listAndRenderUpdateGroupsOnBranchAsync({
|
||||
projectId,
|
||||
branchName: branchFlag,
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
} else {
|
||||
let branchInteractive: string | undefined;
|
||||
if (!branchFlag) {
|
||||
const validationMessage = 'Branch name may not be empty.';
|
||||
if (nonInteractive) {
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
|
||||
const selectedBranch = await selectBranchOnAppAsync({
|
||||
projectId,
|
||||
promptTitle: 'Which branch would you like to view?',
|
||||
displayTextForListItem: updateBranch => updateBranch.name,
|
||||
paginatedQueryOptions:
|
||||
// discard limit and offset because this query is not those flag's intended target
|
||||
{
|
||||
json: paginatedQueryOptions.json,
|
||||
nonInteractive: paginatedQueryOptions.nonInteractive,
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
listAndRenderUpdateGroupsOnBranchAsync({
|
||||
projectId,
|
||||
branchName: selectedBranch.name,
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
({ name: branchInteractive } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Provide the name of the branch whose updates you wish to view:',
|
||||
initial: (await getVcsClient().getBranchNameAsync()) ?? undefined,
|
||||
validate: (value: any) => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
const branch = branchFlag ?? branchInteractive;
|
||||
assert(branch, 'Branch name may not be empty.');
|
||||
|
||||
const branchesAndUpdates = await UpdateQuery.viewBranchAsync({
|
||||
appId: projectId,
|
||||
name: branch,
|
||||
limit: UPDATES_LIMIT,
|
||||
offset: 0,
|
||||
});
|
||||
const UpdateBranch = branchesAndUpdates.app?.byId.updateBranchByName;
|
||||
if (!UpdateBranch) {
|
||||
throw new Error(`Could not find branch "${branch}"`);
|
||||
}
|
||||
updateGroupDescriptions = getUpdateGroupsWithPlatforms(
|
||||
UpdateBranch.updates.map(update => ({ ...update, branch }))
|
||||
).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput(updateGroupDescriptions);
|
||||
} else {
|
||||
logAsTable(updateGroupDescriptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function logAsTable(updateGroupDescriptions: UpdateGroupDescription[]): void {
|
||||
const table = new CliTable({
|
||||
head: ['Branch', ...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
table.push(
|
||||
...updateGroupDescriptions.map(updateGroupDescription => [
|
||||
updateGroupDescription.branch,
|
||||
formatUpdate(updateGroupDescription),
|
||||
updateGroupDescription.runtimeVersion,
|
||||
updateGroupDescription.group,
|
||||
updateGroupDescription.platforms,
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Recently published update groups:'));
|
||||
Log.log(table.toString());
|
||||
}
|
||||
@@ -1,14 +1,55 @@
|
||||
import chalk from 'chalk';
|
||||
import { Flags } from '@oclif/core';
|
||||
import Table from 'cli-table3';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasJsonOnlyFlag } from '../../commandUtils/flags';
|
||||
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import { UpdatesByGroupQuery, UpdatesByGroupQueryVariables } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { UPDATE_COLUMNS, getUpdateGroupDescriptions } from '../../update/utils';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export async function viewUpdateAsync({
|
||||
groupId,
|
||||
}: {
|
||||
groupId: string;
|
||||
}): Promise<UpdatesByGroupQuery> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<UpdatesByGroupQuery, UpdatesByGroupQueryVariables>(
|
||||
gql`
|
||||
query UpdatesByGroup($groupId: ID!) {
|
||||
updatesByGroup(group: $groupId) {
|
||||
id
|
||||
group
|
||||
runtimeVersion
|
||||
platform
|
||||
message
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
username
|
||||
}
|
||||
... on Robot {
|
||||
firstName
|
||||
}
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
groupId,
|
||||
},
|
||||
{ additionalTypenames: ['Update'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
if (data.updatesByGroup.length === 0) {
|
||||
throw new Error(`Could not find any updates with group ID: "${groupId}"`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
export default class UpdateView extends EasCommand {
|
||||
static override description = 'update group details';
|
||||
|
||||
@@ -21,7 +62,10 @@ export default class UpdateView extends EasCommand {
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
...EasJsonOnlyFlag,
|
||||
json: Flags.boolean({
|
||||
description: `Return a json with the updates belonging to the group.`,
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -33,25 +77,27 @@ export default class UpdateView extends EasCommand {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const updatesByGroup = await UpdateQuery.viewUpdateGroupAsync({ groupId });
|
||||
const [updateGroupDescription] = getUpdateGroupDescriptions([updatesByGroup]);
|
||||
const { updatesByGroup } = await viewUpdateAsync({ groupId });
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput(updateGroupDescription);
|
||||
printJsonOnlyOutput(updatesByGroup);
|
||||
} else {
|
||||
const groupTable = new Table({
|
||||
head: [...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
Log.log(chalk.bold('Update group:'));
|
||||
Log.log(formatFields([{ label: 'ID', value: updateGroupDescription.group }]));
|
||||
const representativeUpdate = updatesByGroup[0];
|
||||
groupTable.push([
|
||||
updateGroupDescription.message,
|
||||
updateGroupDescription.runtimeVersion,
|
||||
updateGroupDescription.group,
|
||||
updateGroupDescription.platforms,
|
||||
formatUpdate(representativeUpdate),
|
||||
representativeUpdate.runtimeVersion,
|
||||
representativeUpdate.group,
|
||||
getPlatformsForGroup({
|
||||
updates: updatesByGroup,
|
||||
group: updatesByGroup[0]?.group,
|
||||
}),
|
||||
]);
|
||||
|
||||
Log.log(groupTable.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { WebhookType } from '../../graphql/generated';
|
||||
import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
|
||||
import { ora } from '../../ora';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { prepareInputParamsAsync } from '../../webhooks/input';
|
||||
|
||||
export default class WebhookCreate extends EasCommand {
|
||||
@@ -22,20 +23,16 @@ export default class WebhookCreate extends EasCommand {
|
||||
description:
|
||||
"Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature' header.",
|
||||
}),
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(WebhookCreate);
|
||||
const { projectId } = await this.getContextAsync(WebhookCreate, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
});
|
||||
const webhookInputParams = await prepareInputParamsAsync(flags);
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
const spinner = ora('Creating a webhook').start();
|
||||
try {
|
||||
await WebhookMutation.createWebhookAsync(projectId, webhookInputParams);
|
||||
|
||||
@@ -2,13 +2,14 @@ import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { WebhookFragment } from '../../graphql/generated';
|
||||
import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
|
||||
import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
import { formatWebhook } from '../../webhooks/formatWebhook';
|
||||
|
||||
@@ -23,22 +24,14 @@ export default class WebhookDelete extends EasCommand {
|
||||
},
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
let {
|
||||
args: { ID: webhookId },
|
||||
flags: { 'non-interactive': nonInteractive },
|
||||
} = await this.parse(WebhookDelete);
|
||||
const { projectId } = await this.getContextAsync(WebhookDelete, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
let webhook: WebhookFragment | undefined =
|
||||
webhookId && (await WebhookQuery.byIdAsync(webhookId));
|
||||
@@ -47,11 +40,6 @@ export default class WebhookDelete extends EasCommand {
|
||||
if (webhooks.length === 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply ID argument in non-interactive mode');
|
||||
}
|
||||
|
||||
({ webhook } = await promptAsync({
|
||||
type: 'autocomplete',
|
||||
name: 'webhook',
|
||||
@@ -69,17 +57,15 @@ export default class WebhookDelete extends EasCommand {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(formatWebhook(webhook));
|
||||
Log.newLine();
|
||||
Log.warn(`You are about to permanently delete this webhook.\nThis action is irreversible.`);
|
||||
Log.newLine();
|
||||
const confirmed = await toggleConfirmAsync({
|
||||
message: 'Are you sure you wish to proceed?',
|
||||
});
|
||||
|
||||
if (!nonInteractive) {
|
||||
Log.warn(`You are about to permanently delete this webhook.\nThis action is irreversible.`);
|
||||
Log.newLine();
|
||||
const confirmed = await toggleConfirmAsync({
|
||||
message: 'Are you sure you wish to proceed?',
|
||||
});
|
||||
if (!confirmed) {
|
||||
Log.error(`Canceled deletion of the webhook`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!confirmed) {
|
||||
Log.error(`Canceled deletion of the webhook`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const spinner = ora('Deleting the webhook').start();
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
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';
|
||||
import { ora } from '../../ora';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { formatWebhook } from '../../webhooks/formatWebhook';
|
||||
|
||||
export default class WebhookList extends EasCommand {
|
||||
@@ -19,27 +24,23 @@ export default class WebhookList extends EasCommand {
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...EASCommandProjectIdContext,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
flags: { event },
|
||||
} = await this.parse(WebhookList);
|
||||
const { projectId } = await this.getContextAsync(WebhookList, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectFullName = await getProjectFullNameAsync(exp);
|
||||
|
||||
const spinner = ora(`Fetching the list of webhook on project ${projectDisplayName}`).start();
|
||||
const spinner = ora(`Fetching the list of webhook on project ${projectFullName}`).start();
|
||||
try {
|
||||
const webhooks = await WebhookQuery.byAppIdAsync(projectId, event && { event });
|
||||
if (webhooks.length === 0) {
|
||||
spinner.fail(`There are no webhooks on project ${projectDisplayName}`);
|
||||
spinner.fail(`There are no webhooks on project ${projectFullName}`);
|
||||
} else {
|
||||
spinner.succeed(`Found ${webhooks.length} webhooks on project ${projectDisplayName}`);
|
||||
spinner.succeed(`Found ${webhooks.length} webhooks on project ${projectFullName}`);
|
||||
const list = webhooks
|
||||
.map(webhook => formatWebhook(webhook))
|
||||
.join(`\n\n${chalk.dim('———')}\n\n`);
|
||||
@@ -47,7 +48,7 @@ export default class WebhookList extends EasCommand {
|
||||
}
|
||||
} catch (err) {
|
||||
spinner.fail(
|
||||
`Something went wrong and we couldn't fetch the webhook list for the project ${projectDisplayName}`
|
||||
`Something went wrong and we couldn't fetch the webhook list for the project ${projectFullName}`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { WebhookType } from '../../graphql/generated';
|
||||
import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
|
||||
import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
|
||||
@@ -28,7 +27,6 @@ export default class WebhookUpdate extends EasCommand {
|
||||
description:
|
||||
"Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature' header.",
|
||||
}),
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -38,7 +36,7 @@ export default class WebhookUpdate extends EasCommand {
|
||||
|
||||
const webhook = await WebhookQuery.byIdAsync(webhookId);
|
||||
const webhookInputParams = await prepareInputParamsAsync(
|
||||
pick(flags, ['event', 'url', 'secret', 'non-interactive']),
|
||||
pick(flags, ['event', 'url', 'secret']),
|
||||
webhook
|
||||
);
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
AppFragment,
|
||||
CommonAndroidAppCredentialsFragment,
|
||||
GoogleServiceAccountKeyFragment,
|
||||
Role,
|
||||
} from '../../graphql/generated';
|
||||
import * as AndroidGraphqlClient from '../android/api/GraphqlClient';
|
||||
import { Keystore } from '../android/credentials';
|
||||
@@ -56,18 +55,6 @@ export const testAppFragment: AppFragment = {
|
||||
id: 'test-app-id',
|
||||
fullName: '@testuser/testapp',
|
||||
slug: 'testapp',
|
||||
ownerAccount: {
|
||||
id: 'test-account-id',
|
||||
name: 'testuser',
|
||||
users: [
|
||||
{
|
||||
role: Role.Owner,
|
||||
actor: {
|
||||
id: 'test-user-id',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const testLegacyAndroidFcmFragment: AndroidFcmFragment = {
|
||||
|
||||
@@ -1,40 +1,18 @@
|
||||
import { AppFragment, Role } from '../../graphql/generated';
|
||||
import { Actor } from '../../user/User';
|
||||
|
||||
export const jester = {
|
||||
__typename: 'User' as const,
|
||||
export const jester: Actor = {
|
||||
__typename: 'User',
|
||||
id: 'jester-id',
|
||||
username: 'jester',
|
||||
primaryAccount: {
|
||||
id: 'jester-account-id',
|
||||
name: 'jester',
|
||||
users: [{ role: Role.Admin, actor: { id: 'jester-id' } }],
|
||||
},
|
||||
accounts: [
|
||||
{
|
||||
id: 'jester-account-id',
|
||||
name: 'jester',
|
||||
users: [{ role: Role.Admin, actor: { id: 'jester-id' } }],
|
||||
},
|
||||
{
|
||||
id: 'other-account-id',
|
||||
name: 'other',
|
||||
users: [{ role: Role.ViewOnly, actor: { id: 'jester-id' } }],
|
||||
},
|
||||
],
|
||||
accounts: [{ id: 'jester-account-id', name: 'jester' }],
|
||||
isExpoAdmin: false,
|
||||
};
|
||||
|
||||
export const jester2 = {
|
||||
export const jester2: Actor = {
|
||||
__typename: 'User',
|
||||
id: 'jester2-id',
|
||||
username: 'jester2',
|
||||
accounts: [
|
||||
{
|
||||
id: 'jester2-account-id',
|
||||
name: 'jester2',
|
||||
users: [{ role: Role.Admin, actor: { id: 'jester2-id' } }],
|
||||
},
|
||||
],
|
||||
accounts: [{ id: 'jester2-account-id', name: 'jester2' }],
|
||||
isExpoAdmin: false,
|
||||
};
|
||||
|
||||
@@ -69,10 +47,3 @@ export const testAppJsonWithDifferentOwner = {
|
||||
...testAppJson,
|
||||
owner: jester2.username,
|
||||
};
|
||||
|
||||
export const testAppQueryByIdResponse: AppFragment = {
|
||||
id: testProjectId,
|
||||
slug: testSlug,
|
||||
fullName: testExperienceName,
|
||||
ownerAccount: jester.accounts[0],
|
||||
};
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
CommonIosAppCredentialsFragment,
|
||||
IosAppBuildCredentialsFragment,
|
||||
IosDistributionType,
|
||||
Role,
|
||||
} from '../../graphql/generated';
|
||||
import * as IosGraphqlClient from '../ios/api/GraphqlClient';
|
||||
import {
|
||||
@@ -56,18 +55,6 @@ export const testAppFragment: AppFragment = {
|
||||
id: 'test-app-id',
|
||||
fullName: '@testuser/testapp',
|
||||
slug: 'testapp',
|
||||
ownerAccount: {
|
||||
id: 'test-account-id',
|
||||
name: 'testuser',
|
||||
users: [
|
||||
{
|
||||
role: Role.Owner,
|
||||
actor: {
|
||||
id: 'test-user-id',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const testAscApiKeyFragment: AppStoreConnectApiKeyFragment = {
|
||||
|
||||
@@ -5,11 +5,9 @@ import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
|
||||
import { ora } from '../../../ora';
|
||||
import { getApplicationIdAsync } from '../../../project/android/applicationId';
|
||||
import { GradleBuildContext } from '../../../project/android/gradle';
|
||||
import {
|
||||
getOwnerAccountForProjectIdAsync,
|
||||
getProjectConfigDescription,
|
||||
} from '../../../project/projectUtils';
|
||||
import { getProjectAccountName, getProjectConfigDescription } from '../../../project/projectUtils';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { findAccountByName } from '../../../user/Account';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
|
||||
@@ -90,9 +88,13 @@ 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);
|
||||
const accountName = getProjectAccountName(ctx.exp, ctx.user);
|
||||
const account = findAccountByName(ctx.user.accounts, accountName);
|
||||
if (!account) {
|
||||
throw new Error(`You do not have access to account: ${accountName}`);
|
||||
}
|
||||
|
||||
const androidApplicationIdentifier = await getApplicationIdAsync(
|
||||
ctx.projectDir,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { AccountFragment, AndroidFcmFragment, AndroidFcmVersion } from '../../../graphql/generated';
|
||||
import { AndroidFcmFragment, AndroidFcmVersion } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { CredentialsContext } from '../../context';
|
||||
|
||||
export class CreateFcm {
|
||||
constructor(private account: AccountFragment) {}
|
||||
constructor(private account: Account) {}
|
||||
|
||||
public async runAsync(ctx: CredentialsContext): Promise<AndroidFcmFragment> {
|
||||
if (ctx.nonInteractive) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { AccountFragment, GoogleServiceAccountKeyFragment } from '../../../graphql/generated';
|
||||
import { GoogleServiceAccountKeyFragment } from '../../../graphql/generated';
|
||||
import Log, { learnMore } from '../../../log';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { GoogleServiceAccountKey } from '../credentials';
|
||||
import {
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
} from '../utils/googleServiceAccountKey';
|
||||
|
||||
export class CreateGoogleServiceAccountKey {
|
||||
constructor(private account: AccountFragment) {}
|
||||
constructor(private account: Account) {}
|
||||
|
||||
public async runAsync(ctx: CredentialsContext): Promise<GoogleServiceAccountKeyFragment> {
|
||||
if (ctx.nonInteractive) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { AccountFragment, AndroidKeystoreFragment } from '../../../graphql/generated';
|
||||
import { AndroidKeystoreFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { getProjectIdAsync } from '../../../project/projectUtils';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
|
||||
import { KeystoreWithType, keystoreSchema } from '../credentials';
|
||||
@@ -7,14 +9,14 @@ import { generateRandomKeystoreAsync } from '../utils/keystore';
|
||||
import { getKeystoreWithType, validateKeystore } from '../utils/keystoreNew';
|
||||
|
||||
export class CreateKeystore {
|
||||
constructor(private account: AccountFragment) {}
|
||||
constructor(private account: Account) {}
|
||||
|
||||
public async runAsync(ctx: CredentialsContext): Promise<AndroidKeystoreFragment> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(`New keystore cannot be created in non-interactive mode.`);
|
||||
}
|
||||
|
||||
const projectId = ctx.projectId;
|
||||
const projectId = await getProjectIdAsync(ctx.exp);
|
||||
const keystore = await this.provideOrGenerateAsync(projectId);
|
||||
const keystoreFragment = await ctx.android.createKeystoreAsync(this.account, keystore);
|
||||
Log.succeed('Created keystore');
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { AccountFragment, GoogleServiceAccountKeyFragment } from '../../../graphql/generated';
|
||||
import { GoogleServiceAccountKeyFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync } from '../../../prompts';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { selectGoogleServiceAccountKeyAsync } from '../utils/googleServiceAccountKey';
|
||||
|
||||
export class SelectAndRemoveGoogleServiceAccountKey {
|
||||
constructor(private account: AccountFragment) {}
|
||||
constructor(private account: Account) {}
|
||||
|
||||
async runAsync(ctx: CredentialsContext): Promise<void> {
|
||||
if (ctx.nonInteractive) {
|
||||
|
||||
+3
-2
@@ -1,10 +1,11 @@
|
||||
import { AccountFragment, GoogleServiceAccountKeyFragment } from '../../../graphql/generated';
|
||||
import { GoogleServiceAccountKeyFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { selectGoogleServiceAccountKeyAsync } from '../utils/googleServiceAccountKey';
|
||||
|
||||
export class UseExistingGoogleServiceAccountKey {
|
||||
constructor(private account: AccountFragment) {}
|
||||
constructor(private account: Account) {}
|
||||
|
||||
public async runAsync(ctx: CredentialsContext): Promise<GoogleServiceAccountKeyFragment | null> {
|
||||
if (ctx.nonInteractive) {
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import { AppQuery } from '../../../../graphql/queries/AppQuery';
|
||||
import { testLegacyAndroidFcmFragment } from '../../../__tests__/fixtures-android';
|
||||
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { AssignFcm } from '../AssignFcm';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
|
||||
jest.mock('../../../../graphql/queries/AppQuery');
|
||||
|
||||
describe(AssignFcm, () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
|
||||
});
|
||||
it('assigns an fcm api key in Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
|
||||
-7
@@ -1,16 +1,9 @@
|
||||
import { AppQuery } from '../../../../graphql/queries/AppQuery';
|
||||
import { testGoogleServiceAccountKeyFragment } from '../../../__tests__/fixtures-android';
|
||||
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { AssignGoogleServiceAccountKey } from '../AssignGoogleServiceAccountKey';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
|
||||
jest.mock('../../../../graphql/queries/AppQuery');
|
||||
|
||||
describe(AssignGoogleServiceAccountKey, () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
|
||||
});
|
||||
it('assigns a Google Service Account key in Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
|
||||
-6
@@ -1,4 +1,3 @@
|
||||
import { AppQuery } from '../../../../graphql/queries/AppQuery';
|
||||
import { confirmAsync, promptAsync } from '../../../../prompts';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
@@ -8,7 +7,6 @@ import {
|
||||
testLegacyAndroidBuildCredentialsFragment,
|
||||
testLegacyAndroidFcmFragment,
|
||||
} from '../../../__tests__/fixtures-android';
|
||||
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import {
|
||||
canCopyLegacyCredentialsAsync,
|
||||
@@ -19,12 +17,8 @@ import {
|
||||
jest.mock('../../../../ora');
|
||||
jest.mock('../../../../prompts');
|
||||
jest.mocked(confirmAsync).mockImplementation(async () => true);
|
||||
jest.mock('../../../../graphql/queries/AppQuery');
|
||||
|
||||
describe('BuildCredentialsUtils', () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
|
||||
});
|
||||
describe(canCopyLegacyCredentialsAsync, () => {
|
||||
it('returns true if the user has legacy credentials and no modern ones', async () => {
|
||||
const ctx = createCtxMock({
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import { AppQuery } from '../../../../graphql/queries/AppQuery';
|
||||
import { promptAsync } from '../../../../prompts';
|
||||
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { CreateFcm } from '../CreateFcm';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
jest.mocked(promptAsync).mockImplementation(async () => ({ fcmApiKey: 'blah' }));
|
||||
jest.mock('../../../../graphql/queries/AppQuery');
|
||||
|
||||
describe(CreateFcm, () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
|
||||
});
|
||||
|
||||
it('creates an fcm api key in Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: false });
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
|
||||
|
||||
-7
@@ -1,8 +1,6 @@
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { AppQuery } from '../../../../graphql/queries/AppQuery';
|
||||
import { promptAsync } from '../../../../prompts';
|
||||
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { CreateGoogleServiceAccountKey } from '../CreateGoogleServiceAccountKey';
|
||||
@@ -12,17 +10,12 @@ jest.mock('fs');
|
||||
jest.mocked(promptAsync).mockImplementation(async () => ({
|
||||
filePath: '/google-service-account-key.json',
|
||||
}));
|
||||
jest.mock('../../../../graphql/queries/AppQuery');
|
||||
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
describe(CreateGoogleServiceAccountKey, () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
|
||||
});
|
||||
|
||||
it('creates a Google Service Account Key in Interactive Mode', async () => {
|
||||
vol.fromJSON({
|
||||
'/google-service-account-key.json': JSON.stringify({
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
jester as mockJester,
|
||||
testAppQueryByIdResponse,
|
||||
} from '../../../../credentials/__tests__/fixtures-constants';
|
||||
import { AppQuery } from '../../../../graphql/queries/AppQuery';
|
||||
import { jester as mockJester } from '../../../../credentials/__tests__/fixtures-constants';
|
||||
import { testKeystore } from '../../../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { askForUserProvidedAsync } from '../../../utils/promptForCredentials';
|
||||
@@ -10,16 +6,15 @@ import { generateRandomKeystoreAsync } from '../../utils/keystore';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { CreateKeystore } from '../CreateKeystore';
|
||||
|
||||
jest.mock('../../../../project/ensureProjectExists');
|
||||
jest.mock('../../../../prompts', () => ({ confirmAsync: jest.fn(() => true) }));
|
||||
jest.mock('../../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
|
||||
jest.mock('../../../utils/promptForCredentials');
|
||||
jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn() }));
|
||||
jest.mock('../../../../graphql/queries/AppQuery');
|
||||
|
||||
describe('CreateKeystore', () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(generateRandomKeystoreAsync).mockReset();
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
|
||||
});
|
||||
it('creates a keystore in Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: false });
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { AppQuery } from '../../../../graphql/queries/AppQuery';
|
||||
import { testAndroidBuildCredentialsFragment } from '../../../__tests__/fixtures-android';
|
||||
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { DownloadKeystore } from '../DownloadKeystore';
|
||||
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('../../../../graphql/queries/AppQuery');
|
||||
|
||||
describe(DownloadKeystore, () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
|
||||
});
|
||||
it('downloads a keystore', async () => {
|
||||
const fsWriteFileSpy = jest.spyOn(fs, 'writeFile');
|
||||
const ctx = createCtxMock({
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import { AppQuery } from '../../../../graphql/queries/AppQuery';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
testAndroidAppCredentialsFragment,
|
||||
} from '../../../__tests__/fixtures-android';
|
||||
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { RemoveFcm } from '../RemoveFcm';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
jest.mocked(confirmAsync).mockImplementation(async () => true);
|
||||
jest.mock('../../../../graphql/queries/AppQuery');
|
||||
|
||||
describe(RemoveFcm, () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
|
||||
});
|
||||
it('removes an FCM API Key', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { AppQuery } from '../../../../graphql/queries/AppQuery';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import { testAndroidBuildCredentialsFragment } from '../../../__tests__/fixtures-android';
|
||||
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { RemoveKeystore } from '../RemoveKeystore';
|
||||
@@ -9,13 +7,8 @@ import { RemoveKeystore } from '../RemoveKeystore';
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('../../../../prompts');
|
||||
jest.mocked(confirmAsync).mockImplementation(async () => true);
|
||||
jest.mock('../../../../graphql/queries/AppQuery');
|
||||
|
||||
describe(RemoveKeystore, () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
|
||||
});
|
||||
|
||||
it('removes a keystore', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
|
||||
+2
-7
@@ -1,13 +1,9 @@
|
||||
import { AppQuery } from '../../../../graphql/queries/AppQuery';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
testAndroidBuildCredentialsFragment,
|
||||
testJksAndroidKeystoreFragment,
|
||||
} from '../../../__tests__/fixtures-android';
|
||||
import {
|
||||
jester as mockJester,
|
||||
testAppQueryByIdResponse,
|
||||
} from '../../../__tests__/fixtures-constants';
|
||||
import { jester as mockJester } from '../../../__tests__/fixtures-constants';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../../errors';
|
||||
import { generateRandomKeystoreAsync } from '../../utils/keystore';
|
||||
@@ -15,15 +11,14 @@ import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { SetUpBuildCredentials } from '../SetUpBuildCredentials';
|
||||
|
||||
jest.mock('../../../../ora');
|
||||
jest.mock('../../../../project/ensureProjectExists');
|
||||
jest.mock('../../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
|
||||
jest.mock('../../../../prompts', () => ({ confirmAsync: jest.fn(() => true) }));
|
||||
jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn() }));
|
||||
jest.mock('../../../../graphql/queries/AppQuery');
|
||||
|
||||
describe('SetUpBuildCredentials', () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(generateRandomKeystoreAsync).mockReset();
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
|
||||
});
|
||||
|
||||
it('skips setup when there are prior credentials', async () => {
|
||||
|
||||
-6
@@ -1,14 +1,12 @@
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { AndroidKeystoreType } from '../../../../graphql/generated';
|
||||
import { AppQuery } from '../../../../graphql/queries/AppQuery';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
testAndroidBuildCredentialsFragment,
|
||||
testJksAndroidKeystoreFragment,
|
||||
} from '../../../__tests__/fixtures-android';
|
||||
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import {
|
||||
SelectAndroidBuildCredentials,
|
||||
@@ -23,16 +21,12 @@ jest.mock('fs');
|
||||
jest.mock('../../../../prompts');
|
||||
jest.mocked(confirmAsync).mockImplementation(async () => true);
|
||||
jest.mock('../../../manager/SelectAndroidBuildCredentials');
|
||||
jest.mock('../../../../graphql/queries/AppQuery');
|
||||
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
describe(SetUpBuildCredentialsFromCredentialsJson, () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
|
||||
});
|
||||
it('sets up a new build configuration from credentials.json upon user request', async () => {
|
||||
jest.mocked(SelectAndroidBuildCredentials).mockImplementation((() => {
|
||||
return {
|
||||
|
||||
-6
@@ -1,13 +1,11 @@
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { AppQuery } from '../../../../graphql/queries/AppQuery';
|
||||
import { promptAsync } from '../../../../prompts';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
testAndroidAppCredentialsFragment,
|
||||
testGoogleServiceAccountKeyFragment,
|
||||
} from '../../../__tests__/fixtures-android';
|
||||
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../../errors';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
@@ -18,16 +16,12 @@ jest.mock('fs');
|
||||
jest.mocked(promptAsync).mockImplementation(async () => ({
|
||||
filePath: '/google-service-account-key.json',
|
||||
}));
|
||||
jest.mock('../../../../graphql/queries/AppQuery');
|
||||
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
describe(SetUpGoogleServiceAccountKey, () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
|
||||
});
|
||||
it('skips setup when there is a Google Service Account Key already assigned to the project', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
|
||||
-6
@@ -1,10 +1,8 @@
|
||||
import { AppQuery } from '../../../../graphql/queries/AppQuery';
|
||||
import { promptAsync } from '../../../../prompts';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
testGoogleServiceAccountKeyFragment,
|
||||
} from '../../../__tests__/fixtures-android';
|
||||
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { UseExistingGoogleServiceAccountKey } from '../UseExistingGoogleServiceAccountKey';
|
||||
@@ -13,12 +11,8 @@ jest.mock('../../../../prompts');
|
||||
jest
|
||||
.mocked(promptAsync)
|
||||
.mockImplementation(async () => ({ chosenKey: testGoogleServiceAccountKeyFragment }));
|
||||
jest.mock('../../../../graphql/queries/AppQuery');
|
||||
|
||||
describe(UseExistingGoogleServiceAccountKey, () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
|
||||
});
|
||||
it('uses an existing Google Service Account Key in Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
AccountFragment,
|
||||
AndroidAppBuildCredentialsFragment,
|
||||
AndroidFcmFragment,
|
||||
AndroidFcmVersion,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
GoogleServiceAccountKeyFragment,
|
||||
} from '../../../graphql/generated';
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { GoogleServiceAccountKey, KeystoreWithType } from '../credentials';
|
||||
import { AndroidAppBuildCredentialsMutation } from './graphql/mutations/AndroidAppBuildCredentialsMutation';
|
||||
import { AndroidAppCredentialsMutation } from './graphql/mutations/AndroidAppCredentialsMutation';
|
||||
@@ -19,7 +19,7 @@ import { AndroidAppCredentialsQuery } from './graphql/queries/AndroidAppCredenti
|
||||
import { GoogleServiceAccountKeyQuery } from './graphql/queries/GoogleServiceAccountKeyQuery';
|
||||
|
||||
export interface AppLookupParams {
|
||||
account: AccountFragment;
|
||||
account: Account;
|
||||
projectName: string;
|
||||
androidApplicationIdentifier: string; // 'android.package' field in app.json
|
||||
}
|
||||
@@ -208,7 +208,7 @@ export async function createOrUpdateDefaultIosAppBuildCredentialsAsync(): Promis
|
||||
}
|
||||
|
||||
export async function createKeystoreAsync(
|
||||
account: AccountFragment,
|
||||
account: Account,
|
||||
keystore: KeystoreWithType
|
||||
): Promise<AndroidKeystoreFragment> {
|
||||
return await AndroidKeystoreMutation.createAndroidKeystoreAsync(
|
||||
@@ -228,7 +228,7 @@ export async function deleteKeystoreAsync(keystore: AndroidKeystoreFragment): Pr
|
||||
}
|
||||
|
||||
export async function createFcmAsync(
|
||||
account: AccountFragment,
|
||||
account: Account,
|
||||
fcmApiKey: string,
|
||||
version: AndroidFcmVersion
|
||||
): Promise<AndroidFcmFragment> {
|
||||
@@ -243,7 +243,7 @@ export async function deleteFcmAsync(fcm: AndroidFcmFragment): Promise<void> {
|
||||
}
|
||||
|
||||
export async function createGoogleServiceAccountKeyAsync(
|
||||
account: AccountFragment,
|
||||
account: Account,
|
||||
jsonKey: GoogleServiceAccountKey
|
||||
): Promise<GoogleServiceAccountKeyFragment> {
|
||||
return await GoogleServiceAccountKeyMutation.createGoogleServiceAccountKeyAsync(
|
||||
@@ -261,7 +261,7 @@ export async function deleteGoogleServiceAccountKeyAsync(
|
||||
}
|
||||
|
||||
export async function getGoogleServiceAccountKeysForAccountAsync(
|
||||
account: AccountFragment
|
||||
account: Account
|
||||
): Promise<GoogleServiceAccountKeyFragment[]> {
|
||||
return await GoogleServiceAccountKeyQuery.getAllForAccountAsync(account.name);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
import mockdate from 'mockdate';
|
||||
|
||||
import { AppQuery } from '../../../../graphql/queries/AppQuery';
|
||||
import Log from '../../../../log';
|
||||
import {
|
||||
testAndroidAppCredentialsFragment,
|
||||
testGoogleServiceAccountKeyFragment,
|
||||
testLegacyAndroidFcmFragment,
|
||||
} from '../../../__tests__/fixtures-android';
|
||||
import { testAppQueryByIdResponse } from '../../../__tests__/fixtures-constants';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { getAppLookupParamsFromContextAsync } from '../../actions/BuildCredentialsUtils';
|
||||
import { displayAndroidAppCredentials } from '../printCredentials';
|
||||
|
||||
jest.mock('../../../../log');
|
||||
jest.mock('chalk', () => ({ bold: jest.fn(log => log), cyan: { bold: jest.fn(log => log) } }));
|
||||
jest.mock('../../../../graphql/queries/AppQuery');
|
||||
|
||||
mockdate.set(testLegacyAndroidFcmFragment.updatedAt);
|
||||
mockdate.set(testGoogleServiceAccountKeyFragment.updatedAt);
|
||||
|
||||
describe('print credentials', () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
|
||||
});
|
||||
it('prints the AndroidAppCredentials fragment', async () => {
|
||||
const ctx = createCtxMock();
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
|
||||
|
||||
@@ -4,18 +4,15 @@ import { EasJson } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../log';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getProjectAccountName } from '../project/projectUtils';
|
||||
import { confirmAsync } from '../prompts';
|
||||
import { Actor } from '../user/User';
|
||||
import { Actor, getActorDisplayName } from '../user/User';
|
||||
import * as AndroidGraphqlClient from './android/api/GraphqlClient';
|
||||
import * as IosGraphqlClient from './ios/api/GraphqlClient';
|
||||
import AppStoreApi from './ios/appstore/AppStoreApi';
|
||||
import { AuthenticationMode } from './ios/appstore/authenticateTypes';
|
||||
|
||||
export type CredentialsContextProjectInfo = {
|
||||
exp: ExpoConfig;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export class CredentialsContext {
|
||||
public readonly android = AndroidGraphqlClient;
|
||||
public readonly appStore = new AppStoreApi();
|
||||
@@ -26,30 +23,75 @@ export class CredentialsContext {
|
||||
public readonly easJsonCliConfig?: EasJson['cli'];
|
||||
|
||||
private shouldAskAuthenticateAppStore: boolean = true;
|
||||
private resolvedExp?: ExpoConfig;
|
||||
|
||||
private projectInfo: CredentialsContextProjectInfo;
|
||||
|
||||
constructor(options: {
|
||||
projectInfo: CredentialsContextProjectInfo;
|
||||
easJsonCliConfig?: EasJson['cli'];
|
||||
nonInteractive: boolean;
|
||||
projectDir: string;
|
||||
user: Actor;
|
||||
env?: Env;
|
||||
}) {
|
||||
constructor(
|
||||
private options: {
|
||||
exp?: ExpoConfig;
|
||||
easJsonCliConfig?: EasJson['cli'];
|
||||
nonInteractive?: boolean;
|
||||
projectDir: string;
|
||||
user: Actor;
|
||||
env?: Env;
|
||||
}
|
||||
) {
|
||||
this.easJsonCliConfig = options.easJsonCliConfig;
|
||||
this.projectDir = options.projectDir;
|
||||
this.user = options.user;
|
||||
this.nonInteractive = options.nonInteractive ?? false;
|
||||
this.projectInfo = options.projectInfo;
|
||||
|
||||
this.resolvedExp = options.exp;
|
||||
if (!this.resolvedExp) {
|
||||
this.resolvedExp =
|
||||
CredentialsContext.getExpoConfigInProject(this.projectDir, { env: options.env }) ??
|
||||
undefined;
|
||||
}
|
||||
}
|
||||
|
||||
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.resolvedExp;
|
||||
}
|
||||
|
||||
get exp(): ExpoConfig {
|
||||
return this.projectInfo!.exp;
|
||||
this.ensureProjectContext();
|
||||
return this.resolvedExp!;
|
||||
}
|
||||
|
||||
get projectId(): string {
|
||||
return this.projectInfo!.projectId;
|
||||
public ensureProjectContext(): void {
|
||||
if (this.hasProjectContext) {
|
||||
return;
|
||||
}
|
||||
// trigger getConfig error
|
||||
getExpoConfig(this.options.projectDir);
|
||||
}
|
||||
|
||||
public logOwnerAndProject(): void {
|
||||
const { user } = this.options;
|
||||
if (this.hasProjectContext) {
|
||||
const owner = getProjectAccountName(this.exp, user);
|
||||
// Figure out if User A is configuring credentials as admin for User B's project
|
||||
const isProxyUser = user.__typename === 'Robot' || owner !== user.username;
|
||||
|
||||
Log.log(
|
||||
`Accessing credentials ${isProxyUser ? 'on behalf of' : 'for'} ${owner} in project ${
|
||||
this.exp.slug
|
||||
}`
|
||||
);
|
||||
} else {
|
||||
Log.log(`Accessing credentials for ${this.exp.owner ?? getActorDisplayName(user)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async bestEffortAppStoreAuthenticateAsync(): Promise<void> {
|
||||
|
||||
Loaded 100 of 220 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user