Compare commits

...
Author SHA1 Message Date
Brent Vatne 4a3563c963 [eas-cli] Set PR number in changelog entry 2026-08-01 11:21:11 -07:00
Brent Vatne 98a20ac841 [eas-cli] Add submit:list command
List submissions for a project, filterable by platform and status,
with interactive pagination and --json output. Each row includes the
runtime version and fingerprint of the submitted build when the
submission is associated with an EAS build.
2026-08-01 11:18:11 -07:00
8 changed files with 1878 additions and 23 deletions

No files matched your search

+1
View File
@@ -8,6 +8,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- [eas-cli] Add `eas submit:list` command to list submissions, including the runtime version and fingerprint of the submitted build. ([#4132](https://github.com/expo/eas-cli/pull/4132) by [@brentvatne](https://github.com/brentvatne))
- [eas-build-job] Add optional `ssh` field on build/job payloads. ([#4083](https://github.com/expo/eas-cli/pull/4083) by [@gwdp](https://github.com/gwdp))
- [eas-build-job] Add an `SSH_SESSION` build phase for upcoming worker SSH support. ([#4029](https://github.com/expo/eas-cli/pull/4029) by [@gwdp](https://github.com/gwdp))
- [build-tools] Add `ref` input to the `eas/checkout` step to check out a different git ref (branch, tag, or commit SHA) than the one that triggered the job. ([#4035](https://github.com/expo/eas-cli/pull/4035) by [@sswrk](https://github.com/sswrk))
+1348 -15
View File
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,109 @@
import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand';
import {
EasNonInteractiveAndJsonFlags,
resolveNonInteractiveAndJsonFlags,
} from '../../commandUtils/flags';
import {
EasPaginatedQueryFlags,
getLimitFlagWithCustomValues,
getPaginatedQueryOptions,
} from '../../commandUtils/pagination';
import { AppPlatform, SubmissionStatus as GraphQLSubmissionStatus } from '../../graphql/generated';
import { RequestedPlatform } from '../../platform';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { SUBMISSIONS_LIMIT, listAndRenderSubmissionsOnAppAsync } from '../../submit/queries';
import { enableJsonOutput } from '../../utils/json';
enum SubmissionStatus {
AWAITING_BUILD = 'awaiting-build',
IN_QUEUE = 'in-queue',
IN_PROGRESS = 'in-progress',
FINISHED = 'finished',
ERRORED = 'errored',
CANCELED = 'canceled',
}
export default class SubmissionList extends EasCommand {
static override description = 'list submissions for your project';
static override flags = {
platform: Flags.option({
options: Object.values(RequestedPlatform),
char: 'p',
})(),
status: Flags.option({
options: Object.values(SubmissionStatus),
description: 'Filter only submissions with the specified status',
})(),
...EasPaginatedQueryFlags,
limit: getLimitFlagWithCustomValues({ defaultTo: 10, limit: SUBMISSIONS_LIMIT }),
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(SubmissionList);
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
const { platform: requestedPlatform, status: submissionStatus } = flags;
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(SubmissionList, {
nonInteractive,
});
if (jsonFlag) {
enableJsonOutput();
}
const platform = toAppPlatform(requestedPlatform);
const graphqlSubmissionStatus = toGraphQLSubmissionStatus(submissionStatus);
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
await listAndRenderSubmissionsOnAppAsync(graphqlClient, {
projectId,
projectDisplayName: displayName,
filter: {
platform,
status: graphqlSubmissionStatus,
},
paginatedQueryOptions,
});
}
}
const toAppPlatform = (requestedPlatform?: RequestedPlatform): AppPlatform | undefined => {
if (!requestedPlatform || requestedPlatform === RequestedPlatform.All) {
return undefined;
} else if (requestedPlatform === RequestedPlatform.Android) {
return AppPlatform.Android;
} else {
return AppPlatform.Ios;
}
};
const toGraphQLSubmissionStatus = (
submissionStatus?: SubmissionStatus
): GraphQLSubmissionStatus | undefined => {
if (!submissionStatus) {
return undefined;
} else if (submissionStatus === SubmissionStatus.AWAITING_BUILD) {
return GraphQLSubmissionStatus.AwaitingBuild;
} else if (submissionStatus === SubmissionStatus.IN_QUEUE) {
return GraphQLSubmissionStatus.InQueue;
} else if (submissionStatus === SubmissionStatus.IN_PROGRESS) {
return GraphQLSubmissionStatus.InProgress;
} else if (submissionStatus === SubmissionStatus.FINISHED) {
return GraphQLSubmissionStatus.Finished;
} else if (submissionStatus === SubmissionStatus.ERRORED) {
return GraphQLSubmissionStatus.Errored;
} else {
return GraphQLSubmissionStatus.Canceled;
}
};
+233 -5
View File
@@ -180,6 +180,8 @@ export type Account = {
ssoConfiguration?: Maybe<AccountSsoConfiguration>;
/** Subscription info visible to members that have VIEWER role */
subscription?: Maybe<SubscriptionDetails>;
/** Supabase connection for this account */
supabaseConnection?: Maybe<SupabaseConnection>;
/** Coalesced project activity for an app using pagination */
timelineActivity: TimelineActivityConnection;
updatedAt: Scalars['DateTime']['output'];
@@ -1494,6 +1496,7 @@ export type App = Project & {
submissions: Array<Submission>;
submissionsPaginated: AppSubmissionsConnection;
suggestedDevDomainName: Scalars['String']['output'];
supabaseProject?: Maybe<SupabaseProject>;
/** Coalesced project activity for an app using pagination */
timelineActivity: TimelineActivityConnection;
turtleBrownfieldArtifactsPaginated: BrownfieldArtifactsConnection;
@@ -2875,15 +2878,16 @@ export type AppObserveOverviewUpdate = {
__typename?: 'AppObserveOverviewUpdate';
/** Null for the embedded bundle. */
appUpdateId?: Maybe<Scalars['ID']['output']>;
/** Downloads recorded in range; null when none were recorded (and always for the embedded bundle). */
/** Downloads recorded in range across platforms; null when none were recorded (and always for the embedded bundle). */
downloadCount?: Maybe<Scalars['Int']['output']>;
/** One entry per platform with downloads in range; empty for the embedded bundle. */
downloads: Array<AppObserveOverviewUpdateDownload>;
eventCount: Scalars['Int']['output'];
firstSeenAt: Scalars['DateTime']['output'];
isEmbedded: Scalars['Boolean']['output'];
medianDownloadTime?: Maybe<Scalars['Float']['output']>;
/** EAS update message; null for the embedded bundle or when the update row no longer exists. */
message?: Maybe<Scalars['String']['output']>;
/** One entry per metric with data in range, all platforms combined. */
/** One entry per (metric, platform) with data in range. */
metrics: Array<AppObserveOverviewUpdateMetric>;
/** When the update was published; null for the embedded bundle. */
publishedAt?: Maybe<Scalars['DateTime']['output']>;
@@ -2906,10 +2910,18 @@ export type AppObserveOverviewUpdateComparisonInput = {
startTime: Scalars['DateTime']['input'];
};
export type AppObserveOverviewUpdateDownload = {
__typename?: 'AppObserveOverviewUpdateDownload';
downloadCount: Scalars['Int']['output'];
medianDownloadTime: Scalars['Float']['output'];
platform: AppObservePlatform;
};
export type AppObserveOverviewUpdateMetric = {
__typename?: 'AppObserveOverviewUpdateMetric';
eventCount: Scalars['Int']['output'];
metricName: Scalars['String']['output'];
platform: AppObservePlatform;
statistics: AppObserveVersionMarkerStatistics;
};
@@ -4177,6 +4189,10 @@ export enum BackgroundJobState {
Success = 'SUCCESS'
}
export type BeginSupabaseOAuthInput = {
accountId: Scalars['ID']['input'];
};
export type Billing = {
__typename?: 'Billing';
/** History of invoices */
@@ -4894,6 +4910,11 @@ export type CompletePostHogConnectionInput = {
state: Scalars['ID']['input'];
};
export type CompleteSupabaseOAuthInput = {
code: Scalars['String']['input'];
state: Scalars['ID']['input'];
};
export type Concurrencies = {
__typename?: 'Concurrencies';
android: Scalars['Int']['output'];
@@ -5897,6 +5918,8 @@ export type DeviceRunSessionEventLogUploadSession = {
};
export type DeviceRunSessionFilterInput = {
/** Case-insensitive prefix match on the session name. */
name?: InputMaybe<Scalars['String']['input']>;
platforms?: InputMaybe<Array<AppPlatform>>;
statuses?: InputMaybe<Array<DeviceRunSessionStatus>>;
types?: InputMaybe<Array<DeviceRunSessionType>>;
@@ -6856,12 +6879,15 @@ export enum EntityTypeName {
CustomerEntity = 'CustomerEntity',
EchoProjectEntity = 'EchoProjectEntity',
EchoVersionEntity = 'EchoVersionEntity',
EnvironmentVariableEntity = 'EnvironmentVariableEntity',
GoogleServiceAccountKeyEntity = 'GoogleServiceAccountKeyEntity',
IosAppCredentialsEntity = 'IosAppCredentialsEntity',
LogRocketOrganizationEntity = 'LogRocketOrganizationEntity',
LogRocketProjectEntity = 'LogRocketProjectEntity',
PostHogOrganizationConnectionEntity = 'PostHogOrganizationConnectionEntity',
PostHogProjectEntity = 'PostHogProjectEntity',
SupabaseConnectionEntity = 'SupabaseConnectionEntity',
SupabaseProjectEntity = 'SupabaseProjectEntity',
UserInvitationEntity = 'UserInvitationEntity',
UserPermissionEntity = 'UserPermissionEntity',
VexoAccountConnectionEntity = 'VexoAccountConnectionEntity',
@@ -8254,10 +8280,18 @@ export type JobRunError = {
message: Scalars['String']['output'];
};
export type JobRunLogsCentrifugoSubscriptionToken = {
__typename?: 'JobRunLogsCentrifugoSubscriptionToken';
channel: Scalars['String']['output'];
token: Scalars['String']['output'];
};
export type JobRunMutation = {
__typename?: 'JobRunMutation';
/** Cancel an EAS Job Run */
cancelJobRun: JobRun;
/** Generate a token for subscribing to an EAS Job Run log channel */
generateLogsCentrifugoSubscriptionToken: JobRunLogsCentrifugoSubscriptionToken;
};
@@ -8265,6 +8299,12 @@ export type JobRunMutationCancelJobRunArgs = {
jobRunId: Scalars['ID']['input'];
};
export type JobRunMutationGenerateLogsCentrifugoSubscriptionTokenArgs = {
jobRunId: Scalars['ID']['input'];
thread?: InputMaybe<Scalars['String']['input']>;
};
export enum JobRunPriority {
High = 'HIGH',
Normal = 'NORMAL'
@@ -8350,6 +8390,11 @@ export type LinkSentryInstallationToExpoAccountInput = {
sentryOrgSlug: Scalars['String']['input'];
};
export type LinkSupabaseProjectInput = {
appId: Scalars['ID']['input'];
supabaseProjectRef: Scalars['String']['input'];
};
export type LocalBuildArchiveSourceInput = {
bucketKey: Scalars['String']['input'];
type: LocalBuildArchiveSourceType;
@@ -8952,6 +8997,20 @@ export type ProjectPublicData = {
id: Scalars['ID']['output'];
};
export type ProvisionAdditionalSupabaseProjectInput = {
appId: Scalars['ID']['input'];
/** Suffix appended to the app name for the new Supabase project (e.g. preview). */
projectNameSuffix: Scalars['String']['input'];
/** A Supabase region code (e.g. us-east-1) or smart-group (americas, emea, apac). */
region: Scalars['String']['input'];
};
export type ProvisionSupabaseProjectInput = {
appId: Scalars['ID']['input'];
/** A Supabase region code (e.g. us-east-1) or smart-group (americas, emea, apac). */
region: Scalars['String']['input'];
};
export type PublicArtifacts = {
__typename?: 'PublicArtifacts';
applicationArchiveUrl?: Maybe<Scalars['String']['output']>;
@@ -8977,6 +9036,17 @@ export type PublishUpdateGroupInput = {
updateInfoGroup?: InputMaybe<UpdateInfoGroup>;
};
export type RealtimeLogsCentrifugoConnectionToken = {
__typename?: 'RealtimeLogsCentrifugoConnectionToken';
token: Scalars['String']['output'];
};
export type RealtimeLogsMutation = {
__typename?: 'RealtimeLogsMutation';
/** Generate a token for connecting to EAS Logs Centrifugo */
generateCentrifugoConnectionToken: RealtimeLogsCentrifugoConnectionToken;
};
export type RemoteAppStoreConnectApp = {
__typename?: 'RemoteAppStoreConnectApp';
appStoreIconUrl?: Maybe<Scalars['String']['output']>;
@@ -9257,6 +9327,7 @@ export type RootMutation = {
notificationPreference: NotificationPreferenceMutation;
posthogOrganizationConnection: PostHogOrganizationConnectionMutation;
posthogProject: PostHogProjectMutation;
realtimeLogs: RealtimeLogsMutation;
/** Mutations that create, update, and delete Robots */
robot: RobotMutation;
/** Mutations for Sentry installations */
@@ -9265,6 +9336,8 @@ export type RootMutation = {
sentryProject: SentryProjectMutation;
/** Mutations that modify an EAS Submit submission */
submission: SubmissionMutation;
supabaseConnection: SupabaseConnectionMutation;
supabaseProject: SupabaseProjectMutation;
tunnels: TunnelsMutation;
turtleBrownfieldArtifacts: TurtleBrownfieldArtifactMutation;
update: UpdateMutation;
@@ -9757,6 +9830,11 @@ export type ServeSimRunSessionRemoteConfig = {
streamUrl?: Maybe<Scalars['String']['output']>;
};
export type SetSupabaseConnectionOrganizationInput = {
organizationSlug: Scalars['String']['input'];
supabaseConnectionId: Scalars['ID']['input'];
};
export type SetupConvexProjectInput = {
appId: Scalars['ID']['input'];
convexTeamConnectionId: Scalars['ID']['input'];
@@ -10103,6 +10181,138 @@ export type SubscriptionDetailsPlanEnablementArgs = {
serviceMetric: EasServiceMetric;
};
export type SupabaseConnection = {
__typename?: 'SupabaseConnection';
account: Account;
createdAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
supabaseOrganizationName: Scalars['String']['output'];
/** The organization new projects are provisioned under. Chosen at connect time; defaults to the first. */
supabaseOrganizationSlug: Scalars['String']['output'];
supabaseProjects: Array<SupabaseProject>;
updatedAt: Scalars['DateTime']['output'];
};
export type SupabaseConnectionMutation = {
__typename?: 'SupabaseConnectionMutation';
/** Begins a Supabase connection: creates the PKCE handoff and returns the authorize URL. */
beginSupabaseOAuth: SupabaseOAuthStart;
/**
* Completes a connection from the browser callback: exchanges the authorization code with the
* stored PKCE verifier and persists the connection.
*/
completeSupabaseOAuth: SupabaseConnection;
/** Removes the Expo-side connection only; the Supabase organization and its projects are preserved. */
disconnectSupabase: Scalars['ID']['output'];
/**
* Live list of Supabase organizations this connection can access (Management API). Used once at
* connect-time for the org picker — not a type field, so casual connection reads stay cheap.
*/
listSupabaseOrganizations: Array<SupabaseOrganization>;
/** Sets which Supabase organization new projects are provisioned under (the connect-time picker). */
setSupabaseConnectionOrganization: SupabaseConnection;
};
export type SupabaseConnectionMutationBeginSupabaseOAuthArgs = {
input: BeginSupabaseOAuthInput;
};
export type SupabaseConnectionMutationCompleteSupabaseOAuthArgs = {
input: CompleteSupabaseOAuthInput;
};
export type SupabaseConnectionMutationDisconnectSupabaseArgs = {
id: Scalars['ID']['input'];
};
export type SupabaseConnectionMutationListSupabaseOrganizationsArgs = {
accountId: Scalars['ID']['input'];
};
export type SupabaseConnectionMutationSetSupabaseConnectionOrganizationArgs = {
input: SetSupabaseConnectionOrganizationInput;
};
/**
* Handoff for the browser OAuth step. The CLI opens `url`, and once the user authorizes,
* the website callback completes the connection; the CLI polls for it with `state`.
*/
export type SupabaseOAuthStart = {
__typename?: 'SupabaseOAuthStart';
state: Scalars['ID']['output'];
url: Scalars['String']['output'];
};
export type SupabaseOrganization = {
__typename?: 'SupabaseOrganization';
id: Scalars['ID']['output'];
name: Scalars['String']['output'];
slug: Scalars['String']['output'];
};
export type SupabaseProject = {
__typename?: 'SupabaseProject';
app: App;
createdAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
supabaseConnection: SupabaseConnection;
supabaseProjectName: Scalars['String']['output'];
supabaseProjectRef: Scalars['String']['output'];
supabaseProjectUrl: Scalars['String']['output'];
supabaseRegion: Scalars['String']['output'];
updatedAt: Scalars['DateTime']['output'];
};
export type SupabaseProjectMutation = {
__typename?: 'SupabaseProjectMutation';
/** Removes the Expo-side project link only; the Supabase project is preserved. */
deleteSupabaseProject: Scalars['ID']['output'];
/**
* Live publishable (anon) key for the app's linked Supabase project (Management API). Never
* stored — null while the project is still provisioning. Used by the CLI to write EAS env vars.
*/
fetchSupabasePublishableKey?: Maybe<Scalars['String']['output']>;
/** Links an existing Supabase project (by ref) to the app. */
linkSupabaseProject: SupabaseProject;
/**
* Schedules an additional hosted Supabase project for selected EAS environments. Does not replace
* the app's primary linked project — poll the receipt for ref/url/publishableKey in resultData.
*/
provisionAdditionalSupabaseProject: BackgroundJobReceipt;
/** Schedules provisioning of a new Supabase project for the app. Poll the returned receipt until success, then read app.supabaseProject. */
provisionSupabaseProject: BackgroundJobReceipt;
};
export type SupabaseProjectMutationDeleteSupabaseProjectArgs = {
id: Scalars['ID']['input'];
};
export type SupabaseProjectMutationFetchSupabasePublishableKeyArgs = {
appId: Scalars['ID']['input'];
};
export type SupabaseProjectMutationLinkSupabaseProjectArgs = {
input: LinkSupabaseProjectInput;
};
export type SupabaseProjectMutationProvisionAdditionalSupabaseProjectArgs = {
input: ProvisionAdditionalSupabaseProjectInput;
};
export type SupabaseProjectMutationProvisionSupabaseProjectArgs = {
input: ProvisionSupabaseProjectInput;
};
export enum TargetEntityMutationType {
Create = 'CREATE',
Delete = 'DELETE',
@@ -12400,7 +12610,7 @@ export type WorkflowDeviceTestCaseResult = {
export type WorkflowDeviceTestCaseResultInput = {
/** Execution time in milliseconds. Must be non-negative. */
duration?: InputMaybe<Scalars['Int']['input']>;
/** Error message if failed. Max 4096 characters. */
/** Error message if failed. Truncated to 4096 characters. */
errorMessage?: InputMaybe<Scalars['String']['input']>;
/** Test case name (e.g., "login", "checkout"). Max 255 characters. */
name: Scalars['String']['input'];
@@ -14820,7 +15030,15 @@ export type GetAllSubmissionsForAppQueryVariables = Exact<{
}>;
export type GetAllSubmissionsForAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }> } } };
export type GetAllSubmissionsForAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, submissions: Array<{ __typename?: 'Submission', id: string, createdAt: any, completedAt?: any | null, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, submittedBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?:
| { __typename: 'PartnerActor', id: string, displayName: string }
| { __typename: 'Robot', id: string, displayName: string }
| { __typename: 'SSOUser', id: string, displayName: string }
| { __typename: 'User', id: string, displayName: string }
| null, project:
| { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }
| { __typename: 'Snack', id: string, name: string, slug: string }
, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }> } } };
export type ViewUpdateGroupInsightsQueryVariables = Exact<{
groupId: Scalars['ID']['input'];
@@ -15158,6 +15376,16 @@ export type StatuspageServiceFragment = { __typename?: 'StatuspageService', id:
export type SubmissionFragment = { __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null };
export type SubmissionWithSubmittedBuildFragment = { __typename?: 'Submission', id: string, createdAt: any, completedAt?: any | null, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, submittedBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?:
| { __typename: 'PartnerActor', id: string, displayName: string }
| { __typename: 'Robot', id: string, displayName: string }
| { __typename: 'SSOUser', id: string, displayName: string }
| { __typename: 'User', id: string, displayName: string }
| null, project:
| { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }
| { __typename: 'Snack', id: string, name: string, slug: string }
, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null };
export type UpdateFragment = { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: any | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?:
| { __typename: 'PartnerActor', username: string, id: string }
| { __typename: 'Robot', firstName?: string | null, id: string }
@@ -9,10 +9,12 @@ import {
GetAllSubmissionsForAppQueryVariables,
SubmissionFragment,
SubmissionStatus,
SubmissionWithSubmittedBuildFragment,
SubmissionsByIdQuery,
SubmissionsByIdQueryVariables,
} from '../generated';
import { SubmissionFragmentNode } from '../types/Submission';
import { SubmissionWithSubmittedBuildFragmentNode } from '../types/SubmissionWithSubmittedBuild';
type Filters = {
platform?: AppPlatform;
@@ -56,7 +58,7 @@ export const SubmissionQuery = {
graphqlClient: ExpoGraphqlClient,
appId: string,
{ limit = 10, offset = 0, status, platform }: Filters
): Promise<SubmissionFragment[]> {
): Promise<SubmissionWithSubmittedBuildFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<GetAllSubmissionsForAppQuery, GetAllSubmissionsForAppQueryVariables>(
@@ -77,12 +79,12 @@ export const SubmissionQuery = {
limit: $limit
) {
id
...SubmissionFragment
...SubmissionWithSubmittedBuildFragment
}
}
}
}
${print(SubmissionFragmentNode)}
${print(SubmissionWithSubmittedBuildFragmentNode)}
`,
{ appId, offset, limit, status, platform },
{ additionalTypenames: ['Submission'] }
@@ -0,0 +1,21 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { BuildFragmentNode } from './Build';
import { SubmissionFragmentNode } from './Submission';
export const SubmissionWithSubmittedBuildFragmentNode = gql`
${print(BuildFragmentNode)}
${print(SubmissionFragmentNode)}
fragment SubmissionWithSubmittedBuildFragment on Submission {
id
...SubmissionFragment
createdAt
completedAt
submittedBuild {
id
...BuildFragment
}
}
`;
+84
View File
@@ -0,0 +1,84 @@
import chalk from 'chalk';
import { formatGraphQLSubmission } from './utils/formatSubmission';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import {
AppPlatform,
SubmissionStatus,
SubmissionWithSubmittedBuildFragment,
} from '../graphql/generated';
import { SubmissionQuery } from '../graphql/queries/SubmissionQuery';
import Log from '../log';
import { printJsonOnlyOutput } from '../utils/json';
import { paginatedQueryWithConfirmPromptAsync } from '../utils/queries';
export const SUBMISSIONS_LIMIT = 50;
export type SubmissionFilter = {
platform?: AppPlatform;
status?: SubmissionStatus;
};
export async function listAndRenderSubmissionsOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
projectDisplayName,
filter,
paginatedQueryOptions,
}: {
projectId: string;
projectDisplayName: string;
filter?: SubmissionFilter;
paginatedQueryOptions: PaginatedQueryOptions;
}
): Promise<void> {
if (paginatedQueryOptions.nonInteractive) {
const submissions = await SubmissionQuery.allForAppAsync(graphqlClient, projectId, {
limit: paginatedQueryOptions.limit ?? SUBMISSIONS_LIMIT,
offset: paginatedQueryOptions.offset,
...filter,
});
renderPageOfSubmissions({ submissions, projectDisplayName, paginatedQueryOptions });
} else {
await paginatedQueryWithConfirmPromptAsync({
limit: paginatedQueryOptions.limit ?? SUBMISSIONS_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) =>
SubmissionQuery.allForAppAsync(graphqlClient, projectId, {
limit,
offset,
...filter,
}),
promptOptions: {
title: 'Load more submissions?',
renderListItems: submissions => {
renderPageOfSubmissions({ submissions, projectDisplayName, paginatedQueryOptions });
},
},
});
}
}
function renderPageOfSubmissions({
submissions,
projectDisplayName,
paginatedQueryOptions,
}: {
submissions: SubmissionWithSubmittedBuildFragment[];
projectDisplayName: string;
paginatedQueryOptions: PaginatedQueryOptions;
}): void {
if (paginatedQueryOptions.json) {
printJsonOnlyOutput(submissions);
} else {
const list = submissions
.map(submission => formatGraphQLSubmission(submission))
.join(`\n\n${chalk.dim('———')}\n\n`);
Log.addNewLineIfNone();
Log.log(chalk.bold(`Submissions for ${projectDisplayName}:`));
Log.log(`\n${list}`);
}
}
@@ -0,0 +1,77 @@
import chalk from 'chalk';
import { getSubmissionDetailsUrl } from './urls';
import {
AppPlatform,
SubmissionStatus,
SubmissionWithSubmittedBuildFragment,
} from '../../graphql/generated';
import { link } from '../../log';
import { appPlatformDisplayNames } from '../../platform';
import formatFields from '../../utils/formatFields';
export function formatGraphQLSubmission(submission: SubmissionWithSubmittedBuildFragment): string {
const { submittedBuild } = submission;
const fields: { label: string; value?: string | null }[] = [
{ label: 'ID', value: submission.id },
{
label: 'Platform',
value: appPlatformDisplayNames[submission.platform],
},
{
label: 'Status',
get value() {
switch (submission.status) {
case SubmissionStatus.AwaitingBuild:
return chalk.blue('awaiting build');
case SubmissionStatus.InQueue:
return chalk.blue('in queue');
case SubmissionStatus.InProgress:
return chalk.blue('in progress');
case SubmissionStatus.Finished:
return chalk.green('finished');
case SubmissionStatus.Errored:
return chalk.red('errored');
case SubmissionStatus.Canceled:
return chalk.gray('canceled');
default:
return 'unknown';
}
},
},
...(submission.platform === AppPlatform.Android
? [
{
label: 'Application Identifier',
value: submission.androidConfig?.applicationIdentifier,
},
{ label: 'Track', value: submission.androidConfig?.track?.toLowerCase() },
{ label: 'Release Status', value: submission.androidConfig?.releaseStatus?.toLowerCase() },
]
: [{ label: 'ASC App ID', value: submission.iosConfig?.ascAppIdentifier }]),
{ label: 'Build ID', value: submittedBuild?.id },
{ label: 'Build Profile', value: submittedBuild?.buildProfile },
{ label: 'App Version', value: submittedBuild?.appVersion },
{
label: submission.platform === AppPlatform.Android ? 'Version code' : 'Build number',
value: submittedBuild?.appBuildVersion,
},
{ label: 'Runtime Version', value: submittedBuild?.runtimeVersion },
{ label: 'Fingerprint', value: submittedBuild?.fingerprint?.hash },
{ label: 'Commit', value: submittedBuild?.gitCommitHash },
{ label: 'Error Code', value: submission.error?.errorCode },
{ label: 'Error Message', value: submission.error?.message },
{ label: 'Started at', value: new Date(submission.createdAt).toLocaleString() },
{
label: 'Finished at',
value: submission.completedAt ? new Date(submission.completedAt).toLocaleString() : null,
},
{ label: 'Submission Details', value: link(getSubmissionDetailsUrl(submission)) },
];
const filteredFields = fields.filter(({ value }) => value !== undefined && value !== null) as {
label: string;
value: string;
}[];
return formatFields(filteredFields);
}