Compare commits

...
3 Commits
Author SHA1 Message Date
Quinlan Jung 0bcd94e83f [eas-cli] eas updates: warn users if they near plan limits 2023-04-19 16:55:35 -07:00
Brent Vatne da4b56a58d Fix changelog for 3.9.3 2023-04-18 15:59:05 -07:00
Expo CI 1080600af6 update CHANGELOG.md 2023-04-18 22:47:47 +00:00
7 changed files with 1317 additions and 235 deletions

No files matched your search

+6
View File
@@ -12,6 +12,12 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 Chores
## [3.9.3](https://github.com/expo/eas-cli/releases/tag/v3.9.3) - 2023-04-18
### 🐛 Bug fixes
- Fix prompts in `eas update`. ([#1797](https://github.com/expo/eas-cli/pull/1797) by [@wschurman](https://github.com/wschurman))
## [3.9.2](https://github.com/expo/eas-cli/releases/tag/v3.9.2) - 2023-04-13
### 🐛 Bug fixes
+1006 -208
View File
File diff suppressed because it is too large. Load diff
@@ -39,6 +39,7 @@ import {
} from '../../project/publish';
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
import { getUpdateGroupJsonInfo } from '../../update/utils';
import { maybeWarnAboutEasPlanOverageAsync } from '../../utils/billing';
import {
checkManifestBodyAgainstUpdateInfoGroup,
getCodeSigningInfoAsync,
@@ -176,6 +177,7 @@ export default class UpdatePublish extends EasCommand {
isPublicConfig: true,
});
await maybeWarnAboutEasPlanOverageAsync(graphqlClient, projectId);
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
+164 -27
View File
@@ -87,6 +87,8 @@ export type Account = {
appleTeams: Array<AppleTeam>;
/** Apps associated with this account */
apps: Array<App>;
/** Paginated list of apps associated with this account. By default sorted by name. Use filter to adjust the sorting order. */
appsPaginated: AccountAppsConnection;
/** @deprecated Build packs are no longer supported */
availableBuilds?: Maybe<Scalars['Int']>;
/** Billing information. Only visible to members with the ADMIN or OWNER role. */
@@ -211,6 +213,19 @@ export type AccountAppsArgs = {
};
/**
* An account is a container owning projects, credentials, billing and other organization
* data and settings. Actors may own and be members of accounts.
*/
export type AccountAppsPaginatedArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
filter?: InputMaybe<AccountAppsFilterInput>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
};
/**
* An account is a container owning projects, credentials, billing and other organization
* data and settings. Actors may own and be members of accounts.
@@ -284,6 +299,31 @@ export type AccountTimelineActivityArgs = {
last?: InputMaybe<Scalars['Int']>;
};
export type AccountAppsConnection = {
__typename?: 'AccountAppsConnection';
edges: Array<AccountAppsEdge>;
pageInfo: PageInfo;
};
export type AccountAppsEdge = {
__typename?: 'AccountAppsEdge';
cursor: Scalars['String'];
node: App;
};
export type AccountAppsFilterInput = {
sortByField: AccountAppsSortByField;
};
export enum AccountAppsSortByField {
LatestActivityTime = 'LATEST_ACTIVITY_TIME',
/**
* Name prefers the display name but falls back to full_name with @account/
* part stripped.
*/
Name = 'NAME'
}
export type AccountDataInput = {
name: Scalars['String'];
};
@@ -295,6 +335,8 @@ export type AccountMutation = {
* @deprecated Build packs are no longer supported
*/
buyProduct?: Maybe<Account>;
/** Cancels all subscriptions immediately */
cancelAllSubscriptionsImmediately: Account;
/** Cancel scheduled subscription change */
cancelScheduledSubscriptionChange: Account;
/** Cancels the active subscription */
@@ -329,6 +371,11 @@ export type AccountMutationBuyProductArgs = {
};
export type AccountMutationCancelAllSubscriptionsImmediatelyArgs = {
accountID: Scalars['ID'];
};
export type AccountMutationCancelScheduledSubscriptionChangeArgs = {
accountID: Scalars['ID'];
};
@@ -502,6 +549,7 @@ export type AccountSsoConfigurationPublicDataQueryPublicDataByAccountNameArgs =
export type AccountUsageEasBuildMetadata = {
__typename?: 'AccountUsageEASBuildMetadata';
billingResourceClass: EasBuildBillingResourceClass;
platform: AppPlatform;
};
@@ -976,6 +1024,8 @@ export type App = Project & {
isLikedByMe: Scalars['Boolean'];
/** @deprecated No longer supported */
lastPublishedTime: Scalars['DateTime'];
/** Time of the last user activity (update, branch, submission). */
latestActivity: Scalars['DateTime'];
latestAppVersionByPlatformAndApplicationIdentifier?: Maybe<AppVersion>;
latestReleaseForReleaseChannel?: Maybe<AppRelease>;
/** ID of latest classic update release */
@@ -1002,6 +1052,7 @@ export type App = Project & {
releaseChannels: Array<Scalars['String']>;
/** @deprecated Legacy access tokens are deprecated */
requiresAccessTokenForPushSecurity: Scalars['Boolean'];
scopeKey: Scalars['String'];
/** SDK version of the latest classic update publish, 0.0.0 otherwise */
sdkVersion: Scalars['String'];
slug: Scalars['String'];
@@ -1593,6 +1644,8 @@ export type AppleDeviceMutation = {
createAppleDevice: AppleDevice;
/** Delete an Apple Device */
deleteAppleDevice: DeleteAppleDeviceResult;
/** Update an Apple Device */
updateAppleDevice: AppleDevice;
};
@@ -1606,6 +1659,12 @@ export type AppleDeviceMutationDeleteAppleDeviceArgs = {
id: Scalars['ID'];
};
export type AppleDeviceMutationUpdateAppleDeviceArgs = {
appleDeviceUpdateInput: AppleDeviceUpdateInput;
id: Scalars['ID'];
};
export type AppleDeviceRegistrationRequest = {
__typename?: 'AppleDeviceRegistrationRequest';
account: Account;
@@ -1635,6 +1694,10 @@ export type AppleDeviceRegistrationRequestQueryByIdArgs = {
id: Scalars['ID'];
};
export type AppleDeviceUpdateInput = {
name?: InputMaybe<Scalars['String']>;
};
export type AppleDistributionCertificate = {
__typename?: 'AppleDistributionCertificate';
account: Account;
@@ -1924,6 +1987,8 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
expirationDate?: Maybe<Scalars['DateTime']>;
gitCommitHash?: Maybe<Scalars['String']>;
gitCommitMessage?: Maybe<Scalars['String']>;
gitRef?: Maybe<Scalars['String']>;
githubRepositoryOwnerAndName?: Maybe<Scalars['String']>;
id: Scalars['ID'];
/** Queue position is 1-indexed */
initialQueuePosition?: Maybe<Scalars['Int']>;
@@ -1942,6 +2007,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
platform: AppPlatform;
priority: BuildPriority;
project: Project;
projectRootDirectory?: Maybe<Scalars['String']>;
provisioningStartedAt?: Maybe<Scalars['DateTime']>;
/** Queue position is 1-indexed */
queuePosition?: Maybe<Scalars['Int']>;
@@ -1984,11 +2050,10 @@ export type BuildArtifacts = {
};
export type BuildCacheInput = {
cacheDefaultPaths?: InputMaybe<Scalars['Boolean']>;
clear?: InputMaybe<Scalars['Boolean']>;
customPaths?: InputMaybe<Array<InputMaybe<Scalars['String']>>>;
disabled?: InputMaybe<Scalars['Boolean']>;
key?: InputMaybe<Scalars['String']>;
paths?: InputMaybe<Array<Scalars['String']>>;
};
export enum BuildCredentialsSource {
@@ -2058,9 +2123,7 @@ export enum BuildJobLogsFormat {
export type BuildJobMutation = {
__typename?: 'BuildJobMutation';
cancel: BuildJob;
del?: Maybe<BuildJob>;
restart: BuildJob;
};
export type BuildJobQuery = {
@@ -2375,27 +2438,6 @@ export type Charge = {
wasRefunded: Scalars['Boolean'];
};
/** Represents a client build request */
export type ClientBuild = {
__typename?: 'ClientBuild';
buildJobId?: Maybe<Scalars['String']>;
id: Scalars['ID'];
manifestPlistUrl?: Maybe<Scalars['String']>;
status?: Maybe<Scalars['String']>;
userFacingErrorMessage?: Maybe<Scalars['String']>;
userId?: Maybe<Scalars['String']>;
};
export type ClientBuildQuery = {
__typename?: 'ClientBuildQuery';
byId: ClientBuild;
};
export type ClientBuildQueryByIdArgs = {
requestId: Scalars['ID'];
};
export type CodeSigningInfo = {
__typename?: 'CodeSigningInfo';
alg: Scalars['String'];
@@ -2536,6 +2578,11 @@ export type DeleteEnvironmentSecretResult = {
id: Scalars['ID'];
};
export type DeleteGitHubUserResult = {
__typename?: 'DeleteGitHubUserResult';
id: Scalars['ID'];
};
export type DeleteGoogleServiceAccountKeyResult = {
__typename?: 'DeleteGoogleServiceAccountKeyResult';
id: Scalars['ID'];
@@ -2657,6 +2704,11 @@ export enum DistributionType {
Store = 'STORE'
}
export enum EasBuildBillingResourceClass {
Large = 'LARGE',
Medium = 'MEDIUM'
}
export type EasBuildDeprecationInfo = {
__typename?: 'EASBuildDeprecationInfo';
message: Scalars['String'];
@@ -2897,6 +2949,17 @@ export enum GitHubAppInstallationStatus {
Suspended = 'SUSPENDED'
}
export type GitHubAppMutation = {
__typename?: 'GitHubAppMutation';
/** Create a GitHub build for an app */
createGitHubBuild: Scalars['Boolean'];
};
export type GitHubAppMutationCreateGitHubBuildArgs = {
buildInput: GitHubBuildInput;
};
export type GitHubAppQuery = {
__typename?: 'GitHubAppQuery';
appIdentifier: Scalars['String'];
@@ -2918,6 +2981,14 @@ export type GitHubAppQuerySearchRepositoriesArgs = {
query: Scalars['String'];
};
export type GitHubBuildInput = {
appId: Scalars['ID'];
baseDirectory?: InputMaybe<Scalars['String']>;
buildProfile: Scalars['String'];
gitRef: Scalars['String'];
platform: AppPlatform;
};
export type GitHubRepository = {
__typename?: 'GitHubRepository';
app: App;
@@ -3007,6 +3078,33 @@ export type GitHubRepositorySettingsMutationUpdateGitHubRepositorySettingsArgs =
githubRepositorySettingsId: Scalars['ID'];
};
export type GitHubUser = {
__typename?: 'GitHubUser';
githubUserIdentifier: Scalars['String'];
id: Scalars['ID'];
metadata?: Maybe<GitHubUserMetadata>;
userActor: UserActor;
};
export type GitHubUserMetadata = {
__typename?: 'GitHubUserMetadata';
avatarUrl: Scalars['String'];
login: Scalars['String'];
name?: Maybe<Scalars['String']>;
url: Scalars['String'];
};
export type GitHubUserMutation = {
__typename?: 'GitHubUserMutation';
/** Delete a GitHub User by ID */
deleteGitHubUser: DeleteGitHubUserResult;
};
export type GitHubUserMutationDeleteGitHubUserArgs = {
id: Scalars['ID'];
};
export type GoogleServiceAccountKey = {
__typename?: 'GoogleServiceAccountKey';
account: Account;
@@ -3443,8 +3541,10 @@ export type MeMutation = {
unpublishApp: App;
/** Update an App that the current user owns */
updateApp: App;
/** Update the current user's data */
/** Update the current regular user's data */
updateProfile: User;
/** Update the current SSO user's data */
updateSSOProfile: SsoUser;
};
@@ -3531,6 +3631,11 @@ export type MeMutationUpdateProfileArgs = {
userData: UserDataInput;
};
export type MeMutationUpdateSsoProfileArgs = {
userData: SsoUserDataInput;
};
export type MeteredBillingStatus = {
__typename?: 'MeteredBillingStatus';
EAS_BUILD: Scalars['Boolean'];
@@ -3863,12 +3968,16 @@ export type RootMutation = {
emailSubscription: EmailSubscriptionMutation;
/** Mutations that create and delete EnvironmentSecrets */
environmentSecret: EnvironmentSecretMutation;
/** Mutations that utilize services facilitated by the GitHub App */
githubApp: GitHubAppMutation;
/** Mutations for GitHub App installations */
githubAppInstallation: GitHubAppInstallationMutation;
/** Mutations for GitHub repositories */
githubRepository: GitHubRepositoryMutation;
/** Mutations for GitHub repository settings */
githubRepositorySettings: GitHubRepositorySettingsMutation;
/** Mutations for GitHub users */
githubUser: GitHubUserMutation;
/** Mutations that modify a Google Service Account Key */
googleServiceAccountKey: GoogleServiceAccountKeyMutation;
/** Mutations that modify the build credentials for an iOS app */
@@ -3949,7 +4058,6 @@ export type RootQuery = {
/** Top-level query object for querying BuildPublicData publicly. */
buildPublicData: BuildPublicDataQuery;
builds: BuildQuery;
clientBuilds: ClientBuildQuery;
/** Top-level query object for querying Experimentation configuration. */
experimentation: ExperimentationQuery;
/** Top-level query object for querying GitHub App information and resources it has access to. */
@@ -4051,6 +4159,8 @@ export type SsoUser = Actor & UserActor & {
apps: Array<App>;
bestContactEmail?: Maybe<Scalars['String']>;
created: Scalars['DateTime'];
/** Discord account linked to a user */
discordUser?: Maybe<DiscordUser>;
displayName: Scalars['String'];
/**
* Server feature gate values for this actor, optionally filtering by desired gates.
@@ -4059,6 +4169,8 @@ export type SsoUser = Actor & UserActor & {
featureGates: Scalars['JSONObject'];
firstName?: Maybe<Scalars['String']>;
fullName?: Maybe<Scalars['String']>;
/** GitHub account linked to a user */
githubUser?: Maybe<GitHubUser>;
githubUsername?: Maybe<Scalars['String']>;
id: Scalars['ID'];
industry?: Maybe<Scalars['String']>;
@@ -4110,6 +4222,15 @@ export type SsoUserSnacksArgs = {
offset: Scalars['Int'];
};
export type SsoUserDataInput = {
firstName?: InputMaybe<Scalars['String']>;
githubUsername?: InputMaybe<Scalars['String']>;
industry?: InputMaybe<Scalars['String']>;
lastName?: InputMaybe<Scalars['String']>;
location?: InputMaybe<Scalars['String']>;
twitterUsername?: InputMaybe<Scalars['String']>;
};
export type SsoUserQuery = {
__typename?: 'SSOUserQuery';
/** Query an SSOUser by ID */
@@ -4782,6 +4903,8 @@ export type User = Actor & UserActor & {
featureGates: Scalars['JSONObject'];
firstName?: Maybe<Scalars['String']>;
fullName?: Maybe<Scalars['String']>;
/** GitHub account linked to a user */
githubUser?: Maybe<GitHubUser>;
githubUsername?: Maybe<Scalars['String']>;
/** Whether this user has any pending user invitations. Only resolves for the viewer. */
hasPendingUserInvitations: Scalars['Boolean'];
@@ -4858,6 +4981,8 @@ export type UserActor = {
apps: Array<App>;
bestContactEmail?: Maybe<Scalars['String']>;
created: Scalars['DateTime'];
/** Discord account linked to a user */
discordUser?: Maybe<DiscordUser>;
/**
* Best-effort human readable name for this human actor for use in user interfaces during action attribution.
* For example, when displaying a sentence indicating that actor X created a build or published an update.
@@ -4870,6 +4995,8 @@ export type UserActor = {
featureGates: Scalars['JSONObject'];
firstName?: Maybe<Scalars['String']>;
fullName?: Maybe<Scalars['String']>;
/** GitHub account linked to a user */
githubUser?: Maybe<GitHubUser>;
githubUsername?: Maybe<Scalars['String']>;
id: Scalars['ID'];
industry?: Maybe<Scalars['String']>;
@@ -5802,6 +5929,14 @@ export type LatestAppVersionQueryVariables = Exact<{
export type LatestAppVersionQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, latestAppVersionByPlatformAndApplicationIdentifier?: { __typename?: 'AppVersion', id: string, storeVersion: string, buildVersion: string } | null } } };
export type BillingUpdateByAccountQueryVariables = Exact<{
accountId: Scalars['String'];
date: Scalars['DateTime'];
}>;
export type BillingUpdateByAccountQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byId: { __typename?: 'Account', id: string, billing?: { __typename?: 'Billing', id: string, subscription?: { __typename?: 'SubscriptionDetails', id: string, name?: string | null, meteredBillingStatus: { __typename?: 'MeteredBillingStatus', EAS_UPDATE: boolean } } | null } | null, usageMetrics: { __typename?: 'AccountUsageMetrics', byBillingPeriod: { __typename?: 'UsageMetricTotal', id: string, planMetrics: Array<{ __typename?: 'EstimatedUsage', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number }> } } } } };
export type ViewBranchQueryVariables = Exact<{
appId: Scalars['String'];
name: Scalars['String'];
@@ -5966,6 +6101,8 @@ export type WebhookByIdQuery = { __typename?: 'RootQuery', webhook: { __typename
export type AccountFragment = { __typename?: 'Account', id: string, name: string, users: Array<{ __typename?: 'UserPermission', role?: Role | null, actor: { __typename?: 'Robot', id: string } | { __typename?: 'SSOUser', id: string } | { __typename?: 'User', id: string } }> };
export type AccountUpdatesUsageFragment = { __typename?: 'Account', id: string, billing?: { __typename?: 'Billing', id: string, subscription?: { __typename?: 'SubscriptionDetails', id: string, name?: string | null, meteredBillingStatus: { __typename?: 'MeteredBillingStatus', EAS_UPDATE: boolean } } | null } | null, usageMetrics: { __typename?: 'AccountUsageMetrics', byBillingPeriod: { __typename?: 'UsageMetricTotal', id: string, planMetrics: Array<{ __typename?: 'EstimatedUsage', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number }> } } };
export type AppFragment = { __typename?: 'App', id: string, fullName: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string, users: Array<{ __typename?: 'UserPermission', role?: Role | null, actor: { __typename?: 'Robot', id: string } | { __typename?: 'SSOUser', id: string } | { __typename?: 'User', id: string } }> } };
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: 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, resourceClass: BuildResourceClass, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __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 } };
@@ -0,0 +1,66 @@
import assert from 'assert';
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import { BillingUpdateByAccountQuery } from '../generated';
export const BillingUpdateQuery = {
async byAccountIdAsync(
graphqlClient: ExpoGraphqlClient,
accountId: string,
date: Date
): Promise<BillingUpdateByAccountQuery['account']['byId']> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<BillingUpdateByAccountQuery>(
gql`
query BillingUpdateByAccountQuery($accountId: String!, $date: DateTime!) {
account {
byId(accountId: $accountId) {
id
billing {
id
subscription {
id
name
meteredBillingStatus {
EAS_UPDATE
}
}
}
usageMetrics {
byBillingPeriod(date: $date, service: UPDATES) {
id
planMetrics {
id
serviceMetric
metricType
value
limit
}
}
}
}
}
}
`,
{ accountId, date },
{
additionalTypenames: [
'Account',
'Billing',
'SubscriptionDetails',
'AccountUsageMetrics',
'UsageMetricTotal',
'EstimatedUsage',
],
}
)
.toPromise()
);
assert(data.account, 'GraphQL: `account` not defined in server response');
return data.account.byId;
},
};
@@ -0,0 +1,29 @@
import gql from 'graphql-tag';
export const AccountUpdatesUsageFragmentNode = gql`
fragment AccountUpdatesUsageFragment on Account {
id
billing {
id
subscription {
id
name
meteredBillingStatus {
EAS_UPDATE
}
}
}
usageMetrics {
byBillingPeriod(date: $date, service: UPDATES) {
id
planMetrics {
id
serviceMetric
metricType
value
limit
}
}
}
}
`;
+44
View File
@@ -0,0 +1,44 @@
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { BillingUpdateQuery } from '../graphql/queries/BillingUpdateQuery';
import Log from '../log';
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
const WARNING_THRESHOLD = 0.85;
export async function maybeWarnAboutEasPlanOverageAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string
): Promise<void> {
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const billingUpdateQuery = await BillingUpdateQuery.byAccountIdAsync(
graphqlClient,
account.id,
new Date()
);
const isFree = !billingUpdateQuery.billing; // TODO: no billing means free?
const isMetered = !!billingUpdateQuery.billing?.subscription?.meteredBillingStatus?.EAS_UPDATE;
// TODO: verify correctness
if (!isFree || isMetered) {
return;
}
const planMetrics = billingUpdateQuery.usageMetrics.byBillingPeriod.planMetrics;
for (const metric of planMetrics) {
const { value, limit, serviceMetric } = metric;
const percentageUsed = value / limit;
if (percentageUsed >= 1) {
Log.error(
'You have exceeded your EAS Update plan limit. You can create updates but they will no longer be distributed to users.'
);
Log.error('Please upgrade your plan to continue receiving updates.');
return;
} else if (percentageUsed >= WARNING_THRESHOLD && percentageUsed < 1) {
Log.warn(
`You are approaching your EAS Update plan limit. You have used ${Math.round(
percentageUsed * 100
)}% of your plan's ${serviceMetric}.`
);
Log.warn('Please upgrade your plan to continue receiving updates.');
}
}
}