mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
068a56b375 | ||
|
|
ce4b4d58a6 | ||
|
|
639541c12b | ||
|
|
900ad765bf |
No files matched your search
+8
-2
@@ -8,6 +8,14 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [18.8.0](https://github.com/expo/eas-cli/releases/tag/v18.8.0) - 2026-04-22
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [eas-cli] Support custom params in `observe:events` results. ([#3620](https://github.com/expo/eas-cli/pull/3620) by [@douglowder](https://github.com/douglowder))
|
||||
- [build-tools] Add `eas/deploy` function for EAS Hosting web deployments. ([#3598](https://github.com/expo/eas-cli/pull/3598) by [@gwdp](https://github.com/gwdp))
|
||||
- [build-tools] Add `eas/export` function for Expo web exports. ([#3598](https://github.com/expo/eas-cli/pull/3598) by [@gwdp](https://github.com/gwdp))
|
||||
@@ -23,8 +31,6 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
- [eas-cli] Fixes for `observe` commands, including an issue for apps with many update IDs. ([#3609](https://github.com/expo/eas-cli/pull/3609) by [@douglowder](https://github.com/douglowder))
|
||||
- [eas-cli] Add existing capability identifiers. ([#3615](https://github.com/expo/eas-cli/pull/3615) by [@jakex7](https://github.com/jakex7))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [18.7.0](https://github.com/expo/eas-cli/releases/tag/v18.7.0) - 2026-04-14
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/downloader": "18.5.0",
|
||||
"@expo/eas-build-job": "18.8.0",
|
||||
"@expo/eas-json": "18.8.0",
|
||||
"@expo/env": "^0.4.0",
|
||||
"@expo/logger": "18.5.0",
|
||||
"@expo/package-manager": "1.9.10",
|
||||
|
||||
@@ -27,6 +27,7 @@ import { createRepackBuildFunction } from './functions/repack';
|
||||
import { createReportMaestroTestResultsFunction } from './functions/reportMaestroTestResults';
|
||||
import { resolveAppleTeamIdFromCredentialsFunction } from './functions/resolveAppleTeamIdFromCredentials';
|
||||
import { createResolveBuildConfigBuildFunction } from './functions/resolveBuildConfig';
|
||||
import { createResolveSubmitConfigBuildFunction } from './functions/resolveSubmitConfig';
|
||||
import {
|
||||
createCacheStatsBuildFunction,
|
||||
createRestoreBuildCacheFunction,
|
||||
@@ -88,6 +89,7 @@ export function getEasFunctions(ctx: CustomBuildContext): BuildFunction[] {
|
||||
calculateEASUpdateRuntimeVersionFunction(),
|
||||
|
||||
createSubmissionEntityFunction(),
|
||||
createResolveSubmitConfigBuildFunction(ctx),
|
||||
createUploadToAscBuildFunction(),
|
||||
|
||||
createReportMaestroTestResultsFunction(ctx),
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { Platform, SubmissionConfig, SystemError, UserError } from '@expo/eas-build-job';
|
||||
import { AndroidReleaseStatus, SubmitProfile } from '@expo/eas-json';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { asyncResult } from '@expo/results';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
import { BuildStepEnv } from '@expo/steps';
|
||||
import fs from 'fs-extra';
|
||||
import { graphql } from 'gql.tada';
|
||||
import path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { BuildInfo, ResolvedSubmitConfig } from './common';
|
||||
import { CustomBuildContext } from '../../../customBuildContext';
|
||||
|
||||
const ANDROID_APP_CREDENTIALS_QUERY = graphql(`
|
||||
query ResolveSubmitConfigAndroidAppCredentials(
|
||||
$appId: String!
|
||||
$applicationIdentifier: String
|
||||
) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
androidAppCredentials(filter: { applicationIdentifier: $applicationIdentifier }) {
|
||||
id
|
||||
googleServiceAccountKeyForSubmissions {
|
||||
id
|
||||
keyJson
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const AndroidReleaseStatusToSubmissionReleaseStatus: Record<
|
||||
AndroidReleaseStatus,
|
||||
SubmissionConfig.Android.ReleaseStatus
|
||||
> = {
|
||||
[AndroidReleaseStatus.completed]: SubmissionConfig.Android.ReleaseStatus.COMPLETED,
|
||||
[AndroidReleaseStatus.draft]: SubmissionConfig.Android.ReleaseStatus.DRAFT,
|
||||
[AndroidReleaseStatus.halted]: SubmissionConfig.Android.ReleaseStatus.HALTED,
|
||||
[AndroidReleaseStatus.inProgress]: SubmissionConfig.Android.ReleaseStatus.IN_PROGRESS,
|
||||
};
|
||||
|
||||
export async function resolveAndroidSubmitConfigAsync({
|
||||
artifactPath,
|
||||
build,
|
||||
ctx,
|
||||
env,
|
||||
logger,
|
||||
profile,
|
||||
workingDirectory,
|
||||
}: {
|
||||
artifactPath: string;
|
||||
build: BuildInfo;
|
||||
ctx: CustomBuildContext;
|
||||
env: BuildStepEnv;
|
||||
logger: bunyan;
|
||||
profile: SubmitProfile<Platform.ANDROID>;
|
||||
workingDirectory: string;
|
||||
}): Promise<ResolvedSubmitConfig> {
|
||||
const applicationId =
|
||||
build.appIdentifier ??
|
||||
profile.applicationId ??
|
||||
(await readAndroidApplicationIdAsync(artifactPath, env, logger));
|
||||
if (!applicationId) {
|
||||
throw new UserError(
|
||||
'EAS_RESOLVE_SUBMIT_CONFIG_ANDROID_APPLICATION_ID_NOT_FOUND',
|
||||
'Could not resolve Android applicationId from build metadata or artifact. Set applicationId in the Android submit profile, or pass an APK/AAB artifact with a readable package name.'
|
||||
);
|
||||
}
|
||||
|
||||
const googleServiceAccountKeyJson = profile.serviceAccountKeyPath
|
||||
? await fs.readFile(path.resolve(workingDirectory, profile.serviceAccountKeyPath), 'utf8')
|
||||
: await getGoogleServiceAccountKeyJsonAsync({
|
||||
applicationId,
|
||||
appId: build.appId,
|
||||
ctx,
|
||||
});
|
||||
|
||||
const releaseStatus = AndroidReleaseStatusToSubmissionReleaseStatus[profile.releaseStatus];
|
||||
const baseConfigInput = {
|
||||
changesNotSentForReview: profile.changesNotSentForReview,
|
||||
googleServiceAccountKeyJson,
|
||||
track: profile.track,
|
||||
};
|
||||
const configInput: z.input<typeof SubmissionConfig.Android.SchemaZ> =
|
||||
releaseStatus === SubmissionConfig.Android.ReleaseStatus.IN_PROGRESS
|
||||
? {
|
||||
...baseConfigInput,
|
||||
releaseStatus,
|
||||
rollout: profile.rollout,
|
||||
}
|
||||
: {
|
||||
...baseConfigInput,
|
||||
releaseStatus,
|
||||
};
|
||||
|
||||
return {
|
||||
appIdentifier: applicationId,
|
||||
config: SubmissionConfig.Android.SchemaZ.parse(configInput),
|
||||
platform: Platform.ANDROID,
|
||||
};
|
||||
}
|
||||
|
||||
async function getGoogleServiceAccountKeyJsonAsync({
|
||||
applicationId,
|
||||
appId,
|
||||
ctx,
|
||||
}: {
|
||||
applicationId: string;
|
||||
appId: string;
|
||||
ctx: CustomBuildContext;
|
||||
}): Promise<string> {
|
||||
const credentialsResult = await ctx.graphqlClient
|
||||
.query(ANDROID_APP_CREDENTIALS_QUERY, {
|
||||
appId,
|
||||
applicationIdentifier: applicationId,
|
||||
})
|
||||
.toPromise();
|
||||
if (credentialsResult.error) {
|
||||
throw credentialsResult.error;
|
||||
}
|
||||
|
||||
const key =
|
||||
credentialsResult.data?.app.byId.androidAppCredentials[0]
|
||||
?.googleServiceAccountKeyForSubmissions;
|
||||
if (!key) {
|
||||
throw new UserError(
|
||||
'EAS_RESOLVE_SUBMIT_CONFIG_ANDROID_SERVICE_ACCOUNT_KEY_NOT_CONFIGURED',
|
||||
`Google Service Account Key for submissions is not configured for ${applicationId}. Configure a Google Service Account Key for submissions in EAS credentials, or set serviceAccountKeyPath in the Android submit profile.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!key.keyJson) {
|
||||
throw new SystemError(`Google Service Account Key ${key.id} could not be resolved.`);
|
||||
}
|
||||
return key.keyJson;
|
||||
}
|
||||
|
||||
async function readAndroidApplicationIdAsync(
|
||||
artifactPath: string,
|
||||
env: BuildStepEnv,
|
||||
logger: bunyan
|
||||
): Promise<string | undefined> {
|
||||
if (artifactPath.endsWith('.apk')) {
|
||||
return await readAndroidApplicationIdFromApkAsync(artifactPath, env, logger);
|
||||
} else if (artifactPath.endsWith('.aab')) {
|
||||
return await readAndroidApplicationIdFromAabAsync(artifactPath, env, logger);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function readAndroidApplicationIdFromApkAsync(
|
||||
apkPath: string,
|
||||
env: BuildStepEnv,
|
||||
logger: bunyan
|
||||
): Promise<string | undefined> {
|
||||
const aapt2Result = await asyncResult(spawn('aapt2', ['dump', 'packagename', apkPath], { env }));
|
||||
if (aapt2Result.ok) {
|
||||
return aapt2Result.value.stdout.trim();
|
||||
}
|
||||
logger.warn('Failed to read APK package name with aapt2.');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function readAndroidApplicationIdFromAabAsync(
|
||||
aabPath: string,
|
||||
env: BuildStepEnv,
|
||||
logger: bunyan
|
||||
): Promise<string | undefined> {
|
||||
const result = await asyncResult(
|
||||
spawn('bundletool', ['dump', 'manifest', `--bundle=${aabPath}`, '--xpath=/manifest/@package'], {
|
||||
env,
|
||||
})
|
||||
);
|
||||
if (!result.ok) {
|
||||
logger.warn('Failed to read AAB package name with bundletool.');
|
||||
return undefined;
|
||||
}
|
||||
return result.value.stdout.trim();
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Platform, SubmissionConfig, UserError } from '@expo/eas-build-job';
|
||||
import { EasJsonAccessor, EasJsonUtils, SubmitProfile } from '@expo/eas-json';
|
||||
import { graphql } from 'gql.tada';
|
||||
|
||||
import { CustomBuildContext } from '../../../customBuildContext';
|
||||
|
||||
export type AppPlatform = 'ANDROID' | 'IOS';
|
||||
|
||||
export const AppPlatformToPlatform: Record<AppPlatform, Platform> = {
|
||||
ANDROID: Platform.ANDROID,
|
||||
IOS: Platform.IOS,
|
||||
};
|
||||
|
||||
export type BuildInfo = {
|
||||
appIdentifier?: string | null;
|
||||
appId: string;
|
||||
buildId: string;
|
||||
projectOwnerAccountId: string;
|
||||
platform: Platform;
|
||||
};
|
||||
|
||||
export type ResolvedSubmitConfig = {
|
||||
appIdentifier?: string;
|
||||
config: SubmissionConfig.Android | SubmissionConfig.Ios;
|
||||
platform: Platform;
|
||||
};
|
||||
|
||||
const BUILD_BY_ID_QUERY = graphql(`
|
||||
query ResolveSubmitConfigBuildById($buildId: ID!) {
|
||||
builds {
|
||||
byId(buildId: $buildId) {
|
||||
id
|
||||
platform
|
||||
appIdentifier
|
||||
app {
|
||||
id
|
||||
ownerAccount {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export async function getBuildInfoAsync(
|
||||
ctx: CustomBuildContext,
|
||||
buildId: string
|
||||
): Promise<BuildInfo> {
|
||||
const result = await ctx.graphqlClient.query(BUILD_BY_ID_QUERY, { buildId }).toPromise();
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
const build = result.data?.builds.byId;
|
||||
if (!build) {
|
||||
throw new UserError(
|
||||
'EAS_RESOLVE_SUBMIT_CONFIG_BUILD_NOT_FOUND',
|
||||
`Could not find build ${buildId}.`
|
||||
);
|
||||
}
|
||||
return {
|
||||
appIdentifier: build.appIdentifier,
|
||||
appId: build.app.id,
|
||||
buildId: build.id,
|
||||
projectOwnerAccountId: build.app.ownerAccount.id,
|
||||
platform: AppPlatformToPlatform[build.platform],
|
||||
};
|
||||
}
|
||||
|
||||
export async function getSubmitProfileAsync<T extends Platform>({
|
||||
platform,
|
||||
profileName,
|
||||
workingDirectory,
|
||||
}: {
|
||||
platform: T;
|
||||
profileName: string;
|
||||
workingDirectory: string;
|
||||
}): Promise<SubmitProfile<T>> {
|
||||
return await EasJsonUtils.getSubmitProfileAsync(
|
||||
EasJsonAccessor.fromProjectPath(workingDirectory),
|
||||
platform,
|
||||
profileName
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { Platform, SystemError } from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import {
|
||||
BuildFunction,
|
||||
BuildStepEnv,
|
||||
BuildStepInput,
|
||||
BuildStepInputValueTypeName,
|
||||
BuildStepOutput,
|
||||
} from '@expo/steps';
|
||||
import path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { resolveAndroidSubmitConfigAsync } from './android';
|
||||
import { ResolvedSubmitConfig, getBuildInfoAsync, getSubmitProfileAsync } from './common';
|
||||
import { resolveIosSubmitConfigAsync } from './ios';
|
||||
import { CustomBuildContext } from '../../../customBuildContext';
|
||||
|
||||
export function createResolveSubmitConfigBuildFunction(ctx: CustomBuildContext): BuildFunction {
|
||||
return new BuildFunction({
|
||||
namespace: 'eas',
|
||||
id: 'resolve_submit_config',
|
||||
name: 'Resolve submit config',
|
||||
__metricsId: 'eas/resolve_submit_config',
|
||||
inputProviders: [
|
||||
BuildStepInput.createProvider({
|
||||
id: 'build_id',
|
||||
required: true,
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.STRING,
|
||||
}),
|
||||
BuildStepInput.createProvider({
|
||||
id: 'profile',
|
||||
required: true,
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.STRING,
|
||||
}),
|
||||
BuildStepInput.createProvider({
|
||||
id: 'artifact_path',
|
||||
required: true,
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.STRING,
|
||||
}),
|
||||
],
|
||||
outputProviders: [
|
||||
BuildStepOutput.createProvider({
|
||||
id: 'config_json',
|
||||
required: true,
|
||||
}),
|
||||
BuildStepOutput.createProvider({
|
||||
id: 'platform',
|
||||
required: true,
|
||||
}),
|
||||
BuildStepOutput.createProvider({
|
||||
id: 'app_identifier',
|
||||
required: false,
|
||||
}),
|
||||
],
|
||||
fn: async (stepsCtx, { env, inputs, outputs }) => {
|
||||
const buildId = z.string().uuid().parse(inputs.build_id.value);
|
||||
const profileName = z.string().parse(inputs.profile.value);
|
||||
const artifactPath = path.resolve(
|
||||
stepsCtx.workingDirectory,
|
||||
z.string().parse(inputs.artifact_path.value)
|
||||
);
|
||||
|
||||
const { config, platform, appIdentifier } = await resolveSubmitConfigAsync({
|
||||
artifactPath,
|
||||
buildId,
|
||||
ctx,
|
||||
env,
|
||||
logger: stepsCtx.logger,
|
||||
profileName,
|
||||
workingDirectory: stepsCtx.workingDirectory,
|
||||
});
|
||||
|
||||
outputs.config_json.set(JSON.stringify(config));
|
||||
outputs.platform.set(platform);
|
||||
if (appIdentifier) {
|
||||
outputs.app_identifier.set(appIdentifier);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveSubmitConfigAsync({
|
||||
artifactPath,
|
||||
buildId,
|
||||
ctx,
|
||||
env,
|
||||
logger,
|
||||
profileName,
|
||||
workingDirectory,
|
||||
}: {
|
||||
artifactPath: string;
|
||||
buildId: string;
|
||||
ctx: CustomBuildContext;
|
||||
env: BuildStepEnv;
|
||||
logger: bunyan;
|
||||
profileName: string;
|
||||
workingDirectory: string;
|
||||
}): Promise<ResolvedSubmitConfig> {
|
||||
logger.info('Resolving submit config...');
|
||||
const build = await getBuildInfoAsync(ctx, buildId);
|
||||
|
||||
switch (build.platform) {
|
||||
case Platform.ANDROID: {
|
||||
const profile = await getSubmitProfileAsync({
|
||||
platform: build.platform,
|
||||
profileName,
|
||||
workingDirectory,
|
||||
});
|
||||
return await resolveAndroidSubmitConfigAsync({
|
||||
artifactPath,
|
||||
build,
|
||||
ctx,
|
||||
env,
|
||||
logger,
|
||||
profile,
|
||||
workingDirectory,
|
||||
});
|
||||
}
|
||||
case Platform.IOS: {
|
||||
const profile = await getSubmitProfileAsync({
|
||||
platform: build.platform,
|
||||
profileName,
|
||||
workingDirectory,
|
||||
});
|
||||
return await resolveIosSubmitConfigAsync({
|
||||
artifactPath,
|
||||
appId: build.appId,
|
||||
buildAppIdentifier: build.appIdentifier,
|
||||
ctx,
|
||||
env,
|
||||
profile,
|
||||
projectOwnerAccountId: build.projectOwnerAccountId,
|
||||
workingDirectory,
|
||||
});
|
||||
}
|
||||
default:
|
||||
throw new SystemError(`Unsupported platform: ${build.platform}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import { Platform, SubmissionConfig, UserError } from '@expo/eas-build-job';
|
||||
import { SubmitProfile } from '@expo/eas-json';
|
||||
import { BuildStepEnv } from '@expo/steps';
|
||||
import fs from 'fs-extra';
|
||||
import { graphql } from 'gql.tada';
|
||||
import path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { readIpaInfoAsync } from '../readIpaInfo';
|
||||
import { ResolvedSubmitConfig } from './common';
|
||||
import { CustomBuildContext } from '../../../customBuildContext';
|
||||
|
||||
const APPLE_APP_IDENTIFIER_QUERY = graphql(`
|
||||
query ResolveSubmitConfigAppleAppIdentifier(
|
||||
$accountId: String!
|
||||
$bundleIdentifier: String!
|
||||
) {
|
||||
account {
|
||||
byId(accountId: $accountId) {
|
||||
id
|
||||
appleAppIdentifiers(bundleIdentifier: $bundleIdentifier) {
|
||||
id
|
||||
bundleIdentifier
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const IOS_APP_CREDENTIALS_QUERY = graphql(`
|
||||
query ResolveSubmitConfigIosAppCredentials(
|
||||
$appId: String!
|
||||
$appleAppIdentifierId: String!
|
||||
) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
iosAppCredentials(filter: { appleAppIdentifierId: $appleAppIdentifierId }) {
|
||||
id
|
||||
appStoreConnectApiKeyForSubmissions {
|
||||
id
|
||||
issuerIdentifier
|
||||
keyIdentifier
|
||||
keyP8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export async function resolveIosSubmitConfigAsync({
|
||||
appId,
|
||||
artifactPath,
|
||||
buildAppIdentifier,
|
||||
ctx,
|
||||
env,
|
||||
profile,
|
||||
projectOwnerAccountId,
|
||||
workingDirectory,
|
||||
}: {
|
||||
appId: string;
|
||||
artifactPath: string;
|
||||
buildAppIdentifier?: string | null;
|
||||
ctx: CustomBuildContext;
|
||||
env: BuildStepEnv;
|
||||
profile: SubmitProfile<Platform.IOS>;
|
||||
projectOwnerAccountId: string;
|
||||
workingDirectory: string;
|
||||
}): Promise<ResolvedSubmitConfig> {
|
||||
const bundleIdentifier =
|
||||
buildAppIdentifier ??
|
||||
profile.bundleIdentifier ??
|
||||
(await readIpaInfoAsync(artifactPath)).bundleIdentifier;
|
||||
|
||||
if (!profile.ascAppId) {
|
||||
throw new UserError(
|
||||
'EAS_RESOLVE_SUBMIT_CONFIG_IOS_ASC_APP_ID_NOT_CONFIGURED',
|
||||
'Set ascAppId in the submit profile in eas.json.'
|
||||
);
|
||||
}
|
||||
|
||||
const appSpecificPassword = env.EXPO_APPLE_APP_SPECIFIC_PASSWORD;
|
||||
const configInput: z.input<typeof SubmissionConfig.Ios.SchemaZ> = {
|
||||
ascAppIdentifier: profile.ascAppId,
|
||||
groups: profile.groups,
|
||||
...(appSpecificPassword
|
||||
? {
|
||||
appleAppSpecificPassword: validateAppSpecificPassword(appSpecificPassword),
|
||||
appleIdUsername: requireAppleIdUsername(profile.appleId ?? env.EXPO_APPLE_ID),
|
||||
}
|
||||
: {
|
||||
ascApiJsonKey: await getAscApiJsonKeyAsync({
|
||||
accountId: projectOwnerAccountId,
|
||||
appId,
|
||||
bundleIdentifier,
|
||||
ctx,
|
||||
profile,
|
||||
workingDirectory,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
return {
|
||||
appIdentifier: bundleIdentifier,
|
||||
config: SubmissionConfig.Ios.SchemaZ.parse(configInput),
|
||||
platform: Platform.IOS,
|
||||
};
|
||||
}
|
||||
|
||||
async function getAscApiJsonKeyAsync({
|
||||
accountId,
|
||||
appId,
|
||||
bundleIdentifier,
|
||||
ctx,
|
||||
profile,
|
||||
workingDirectory,
|
||||
}: {
|
||||
accountId: string;
|
||||
appId: string;
|
||||
bundleIdentifier?: string;
|
||||
ctx: CustomBuildContext;
|
||||
profile: SubmitProfile<Platform.IOS>;
|
||||
workingDirectory: string;
|
||||
}): Promise<string> {
|
||||
// Prefer explicit local ASC API key fields from eas.json when all are present.
|
||||
if (profile.ascApiKeyPath && profile.ascApiKeyIssuerId && profile.ascApiKeyId) {
|
||||
return JSON.stringify({
|
||||
issuer_id: profile.ascApiKeyIssuerId,
|
||||
key: await fs.readFile(path.resolve(workingDirectory, profile.ascApiKeyPath), 'utf8'),
|
||||
key_id: profile.ascApiKeyId,
|
||||
});
|
||||
}
|
||||
|
||||
if (profile.ascApiKeyPath || profile.ascApiKeyIssuerId || profile.ascApiKeyId) {
|
||||
throw new UserError(
|
||||
'EAS_RESOLVE_SUBMIT_CONFIG_IOS_ASC_API_KEY_INCOMPLETE',
|
||||
'ascApiKeyPath, ascApiKeyIssuerId and ascApiKeyId must all be defined in eas.json.'
|
||||
);
|
||||
}
|
||||
|
||||
if (!bundleIdentifier) {
|
||||
throw new UserError(
|
||||
'EAS_RESOLVE_SUBMIT_CONFIG_IOS_BUNDLE_IDENTIFIER_NOT_FOUND',
|
||||
'Could not resolve iOS bundleIdentifier from build metadata or artifact. Set bundleIdentifier in the iOS submit profile, or pass an IPA artifact with a readable bundle identifier.'
|
||||
);
|
||||
}
|
||||
|
||||
// Server credentials are keyed by the Apple App Identifier, so first map the bundle id to it.
|
||||
const appleAppIdentifierResult = await ctx.graphqlClient
|
||||
.query(APPLE_APP_IDENTIFIER_QUERY, {
|
||||
accountId,
|
||||
bundleIdentifier,
|
||||
})
|
||||
.toPromise();
|
||||
if (appleAppIdentifierResult.error) {
|
||||
throw appleAppIdentifierResult.error;
|
||||
}
|
||||
|
||||
const appleAppIdentifierId =
|
||||
appleAppIdentifierResult.data?.account.byId.appleAppIdentifiers[0]?.id;
|
||||
if (!appleAppIdentifierId) {
|
||||
throw new UserError(
|
||||
'EAS_RESOLVE_SUBMIT_CONFIG_IOS_APP_IDENTIFIER_NOT_CONFIGURED',
|
||||
`Apple App Identifier is not configured for ${bundleIdentifier}. Configure the bundle identifier in EAS credentials for this account.`
|
||||
);
|
||||
}
|
||||
|
||||
// The app credentials record points at the ASC API key selected for submissions.
|
||||
const credentialsResult = await ctx.graphqlClient
|
||||
.query(IOS_APP_CREDENTIALS_QUERY, {
|
||||
appId,
|
||||
appleAppIdentifierId,
|
||||
})
|
||||
.toPromise();
|
||||
if (credentialsResult.error) {
|
||||
throw credentialsResult.error;
|
||||
}
|
||||
|
||||
const ascApiKey =
|
||||
credentialsResult.data?.app.byId.iosAppCredentials[0]?.appStoreConnectApiKeyForSubmissions;
|
||||
if (!ascApiKey) {
|
||||
throw new UserError(
|
||||
'EAS_RESOLVE_SUBMIT_CONFIG_IOS_ASC_API_KEY_NOT_CONFIGURED',
|
||||
`App Store Connect API Key for submissions is not configured for ${bundleIdentifier}. Configure an App Store Connect API Key for submissions in EAS credentials, or set ascApiKeyPath, ascApiKeyIssuerId, and ascApiKeyId in the iOS submit profile.`
|
||||
);
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
issuer_id: ascApiKey.issuerIdentifier,
|
||||
key: ascApiKey.keyP8,
|
||||
key_id: ascApiKey.keyIdentifier,
|
||||
});
|
||||
}
|
||||
|
||||
function validateAppSpecificPassword(password: string): string {
|
||||
if (!/^[a-z]{4}-[a-z]{4}-[a-z]{4}-[a-z]{4}$/.test(password)) {
|
||||
throw new UserError(
|
||||
'EAS_RESOLVE_SUBMIT_CONFIG_IOS_APP_SPECIFIC_PASSWORD_INVALID',
|
||||
'EXPO_APPLE_APP_SPECIFIC_PASSWORD must be in the format xxxx-xxxx-xxxx-xxxx, where x is a lowercase letter.'
|
||||
);
|
||||
}
|
||||
return password;
|
||||
}
|
||||
|
||||
function requireAppleIdUsername(appleIdUsername?: string): string {
|
||||
if (!appleIdUsername) {
|
||||
throw new UserError(
|
||||
'EAS_RESOLVE_SUBMIT_CONFIG_IOS_APPLE_ID_NOT_CONFIGURED',
|
||||
'Set appleId in the submit profile or EXPO_APPLE_ID when using EXPO_APPLE_APP_SPECIFIC_PASSWORD.'
|
||||
);
|
||||
}
|
||||
return appleIdUsername;
|
||||
}
|
||||
Reference in new issue
Block a user