mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
608bb669c2 | ||
|
|
8c170af829 | ||
|
|
ad4c5299e3 | ||
|
|
dee07ae0a2 | ||
|
|
4ad12340f2 | ||
|
|
c6d4faefe7 | ||
|
|
2f8712a0f3 | ||
|
|
a439721054 | ||
|
|
2e2dddb59a | ||
|
|
b7df3ff4f8 |
No files matched your search
@@ -8,6 +8,24 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [eas-cli] Add missing Apple metadata attributes for age ratings and content descriptions. ([#3584](https://github.com/expo/eas-cli/pull/3584) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- [eas-cli] Add App Clip metadata support to `metadata:push` and `metadata:pull` (default experience action, per-locale subtitle and header image, App Store review invocation URLs). ([#3590](https://github.com/expo/eas-cli/pull/3590) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- [eas-cli] Allow up to 3 video previews per device type in `metadata:push` and `metadata:pull`. Each preview type now accepts an array of preview configs in addition to the existing single-config form (which remains supported for backwards compatibility). ([@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [build-tools][eas-cli] Detect iOS Development provisioning profiles and set correct code signing identity instead of treating them as Ad Hoc. ([#3496](https://github.com/expo/eas-cli/pull/3496) by [@qwertey6](https://github.com/qwertey6))
|
||||
- [build-tools] Prevent detecting Yarn Modern as Classic based on lockfile ([#3572](https://github.com/expo/eas-cli/pull/3572) by [@kitten](https://github.com/kitten))
|
||||
- [eas-cli] Bump `@expo/apple-utils` to `2.1.18` to fix `metadata:push` failing on `ageRatingDeclarations` due to the removed `gamblingAndContests` attribute. ([#3588](https://github.com/expo/eas-cli/pull/3588) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- [eas-cli] Bump `@expo/apple-utils` to `2.1.19` to fix image and video uploads via `metadata:push` getting stuck in `AWAITING_UPLOAD` state. The asset client was inheriting Bearer token injection from the App Store Connect API client, which caused S3 presigned URL uploads to be silently mishandled by Apple's CDN. Fixes screenshots, previews, and App Clip header image uploads. ([#3590](https://github.com/expo/eas-cli/pull/3590) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- [eas-cli] `metadata:pull` now preserves screenshot, video preview, and App Clip header image entries with placeholder paths when the asset is in an unrendered state, so users can recover broken records by replacing the file or removing the entry instead of having entries silently dropped from `store.config.json`. ([#3590](https://github.com/expo/eas-cli/pull/3590) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [18.5.0](https://github.com/expo/eas-cli/releases/tag/v18.5.0) - 2026-04-02
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [eas-cli] Add screenshots and previews support to `metadata:push` and `metadata:pull`. ([#3301](https://github.com/expo/eas-cli/pull/3301) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- [eas-cli] Add `--non-interactive` flag to `metadata:push` and `metadata:pull` commands with ASC API Key auth support. ([#3548](https://github.com/expo/eas-cli/pull/3548) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "18.4.0",
|
||||
"version": "18.5.0",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/build-tools",
|
||||
"version": "18.4.0",
|
||||
"version": "18.5.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
@@ -37,17 +37,17 @@
|
||||
"dependencies": {
|
||||
"@expo/config": "55.0.10",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/downloader": "18.0.1",
|
||||
"@expo/eas-build-job": "18.4.0",
|
||||
"@expo/downloader": "18.5.0",
|
||||
"@expo/eas-build-job": "18.5.0",
|
||||
"@expo/env": "^0.4.0",
|
||||
"@expo/logger": "18.0.1",
|
||||
"@expo/logger": "18.5.0",
|
||||
"@expo/package-manager": "1.9.10",
|
||||
"@expo/plist": "^0.2.0",
|
||||
"@expo/results": "^1.0.0",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "18.4.0",
|
||||
"@expo/template-file": "18.0.1",
|
||||
"@expo/turtle-spawn": "18.0.1",
|
||||
"@expo/steps": "18.5.0",
|
||||
"@expo/template-file": "18.5.0",
|
||||
"@expo/turtle-spawn": "18.5.0",
|
||||
"@expo/xcpretty": "^4.3.1",
|
||||
"@google-cloud/storage": "^7.11.2",
|
||||
"@urql/core": "^6.0.1",
|
||||
|
||||
@@ -165,6 +165,52 @@ describe(configureXcodeProject, () => {
|
||||
'Info.plist application target'
|
||||
);
|
||||
});
|
||||
it('configures credentials with Apple Development identity for development profile', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'ios/testapp.xcodeproj/project.pbxproj': originalFs.readFileSync(
|
||||
path.join(__dirname, 'fixtures/simple-project.pbxproj'),
|
||||
'utf-8'
|
||||
),
|
||||
'ios/testapp/AppDelegate.m': 'placeholder',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
const options = {
|
||||
credentials: {
|
||||
keychainPath: 'fake/path',
|
||||
targetProvisioningProfiles: {
|
||||
testapp: {
|
||||
path: 'fake/path.mobileprovision',
|
||||
target: 'testapp',
|
||||
bundleIdentifier: 'abc',
|
||||
teamId: 'ABCDEFGH',
|
||||
uuid: 'abc',
|
||||
name: 'profile name',
|
||||
developerCertificate: Buffer.from('test'),
|
||||
certificateCommonName: 'Apple Development: Test User',
|
||||
distributionType: DistributionType.DEVELOPMENT,
|
||||
},
|
||||
},
|
||||
distributionType: DistributionType.DEVELOPMENT,
|
||||
teamId: 'ABCDEFGH',
|
||||
applicationTargetProvisioningProfile: {} as ProvisioningProfile<Ios.Job>,
|
||||
},
|
||||
buildConfiguration: 'Release',
|
||||
};
|
||||
const ctx = {
|
||||
getReactNativeProjectDirectory: () => '/app',
|
||||
logger: { info: jest.fn() },
|
||||
job: {},
|
||||
};
|
||||
await configureXcodeProject(ctx as any, options);
|
||||
const pbxproj = vol.readFileSync(
|
||||
'/app/ios/testapp.xcodeproj/project.pbxproj',
|
||||
'utf-8'
|
||||
) as string;
|
||||
expect(pbxproj).toContain('CODE_SIGN_IDENTITY = "Apple Development: Test User"');
|
||||
expect(pbxproj).not.toContain('"iPhone Distribution"');
|
||||
});
|
||||
it('configures credentials and versions for multi target project', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ import uniq from 'lodash/uniq';
|
||||
import path from 'path';
|
||||
|
||||
import { Credentials } from './credentials/manager';
|
||||
import { DistributionType } from './credentials/provisioningProfile';
|
||||
import { BuildContext } from '../context';
|
||||
|
||||
async function configureXcodeProject(
|
||||
@@ -56,6 +57,9 @@ async function configureCredentialsAsync(
|
||||
profileName: profile.name,
|
||||
appleTeamId: profile.teamId,
|
||||
buildConfiguration,
|
||||
...(credentials.distributionType === DistributionType.DEVELOPMENT && {
|
||||
codeSignIdentity: profile.certificateCommonName,
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface ProvisioningProfileData {
|
||||
export enum DistributionType {
|
||||
AD_HOC = 'ad-hoc',
|
||||
APP_STORE = 'app-store',
|
||||
DEVELOPMENT = 'development',
|
||||
ENTERPRISE = 'enterprise',
|
||||
}
|
||||
|
||||
@@ -137,6 +138,10 @@ export default class ProvisioningProfile<TJob extends Ios.Job> {
|
||||
if (plistData.ProvisionsAllDevices) {
|
||||
return DistributionType.ENTERPRISE;
|
||||
} else if (plistData.ProvisionedDevices) {
|
||||
const entitlements = plistData.Entitlements as plist.PlistObject | undefined;
|
||||
if (entitlements?.['get-task-allow']) {
|
||||
return DistributionType.DEVELOPMENT;
|
||||
}
|
||||
return DistributionType.AD_HOC;
|
||||
} else {
|
||||
return DistributionType.APP_STORE;
|
||||
|
||||
@@ -50,6 +50,47 @@ describe(configureCredentialsAsync, () => {
|
||||
vol.readFileSync('/app/ios/testapp.xcodeproj/project.pbxproj', 'utf-8')
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
it('configures credentials with Apple Development identity for development profile', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'ios/testapp.xcodeproj/project.pbxproj': originalFs.readFileSync(
|
||||
path.join(__dirname, 'fixtures/simple-project.pbxproj'),
|
||||
'utf-8'
|
||||
),
|
||||
'ios/testapp/AppDelegate.m': 'placeholder',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
const options = {
|
||||
credentials: {
|
||||
keychainPath: 'fake/path',
|
||||
targetProvisioningProfiles: {
|
||||
testapp: {
|
||||
path: 'fake/path.mobileprovision',
|
||||
target: 'testapp',
|
||||
bundleIdentifier: 'abc',
|
||||
teamId: 'ABCDEFGH',
|
||||
uuid: 'abc',
|
||||
name: 'profile name',
|
||||
developerCertificate: Buffer.from('test'),
|
||||
certificateCommonName: 'Apple Development: Test User',
|
||||
distributionType: DistributionType.DEVELOPMENT,
|
||||
},
|
||||
},
|
||||
distributionType: DistributionType.DEVELOPMENT,
|
||||
teamId: 'ABCDEFGH',
|
||||
applicationTargetProvisioningProfile: {} as ProvisioningProfile,
|
||||
},
|
||||
buildConfiguration: 'Release',
|
||||
};
|
||||
await configureCredentialsAsync({ info: jest.fn() } as any, '/app', options);
|
||||
const pbxproj = vol.readFileSync(
|
||||
'/app/ios/testapp.xcodeproj/project.pbxproj',
|
||||
'utf-8'
|
||||
) as string;
|
||||
expect(pbxproj).toContain('CODE_SIGN_IDENTITY = "Apple Development: Test User"');
|
||||
expect(pbxproj).not.toContain('"iPhone Distribution"');
|
||||
});
|
||||
it('configures credentials for multi target project', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ import uniq from 'lodash/uniq';
|
||||
import path from 'path';
|
||||
|
||||
import { Credentials } from './credentials/manager';
|
||||
import { DistributionType } from './credentials/provisioningProfile';
|
||||
|
||||
export async function configureCredentialsAsync(
|
||||
logger: bunyan,
|
||||
@@ -29,6 +30,9 @@ export async function configureCredentialsAsync(
|
||||
profileName: profile.name,
|
||||
appleTeamId: profile.teamId,
|
||||
buildConfiguration,
|
||||
...(credentials.distributionType === DistributionType.DEVELOPMENT && {
|
||||
codeSignIdentity: profile.certificateCommonName,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface ProvisioningProfileData {
|
||||
export enum DistributionType {
|
||||
AD_HOC = 'ad-hoc',
|
||||
APP_STORE = 'app-store',
|
||||
DEVELOPMENT = 'development',
|
||||
ENTERPRISE = 'enterprise',
|
||||
}
|
||||
|
||||
@@ -133,6 +134,10 @@ export default class ProvisioningProfile {
|
||||
if (plistData.ProvisionsAllDevices) {
|
||||
return DistributionType.ENTERPRISE;
|
||||
} else if (plistData.ProvisionedDevices) {
|
||||
const entitlements = plistData.Entitlements as plist.PlistObject | undefined;
|
||||
if (entitlements?.['get-task-allow']) {
|
||||
return DistributionType.DEVELOPMENT;
|
||||
}
|
||||
return DistributionType.AD_HOC;
|
||||
} else {
|
||||
return DistributionType.APP_STORE;
|
||||
|
||||
@@ -2,18 +2,24 @@ import { ExpoConfig } from '@expo/config';
|
||||
import { Android } from '@expo/eas-build-job';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import path from 'path';
|
||||
import { Readable } from 'stream';
|
||||
import { instance, mock, when } from 'ts-mockito';
|
||||
|
||||
import { BuildContext } from '../../context';
|
||||
import { PackageManager } from '../packageManager';
|
||||
import { readEasJsonContents, runExpoCliCommand } from '../project';
|
||||
import { PackageManager, findPackagerRootDir } from '../packageManager';
|
||||
import { isUsingModernYarnVersion, readEasJsonContents, runExpoCliCommand } from '../project';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('@expo/turtle-spawn', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mock('../packageManager', () => ({
|
||||
...jest.requireActual('../packageManager'),
|
||||
findPackagerRootDir: jest.fn((dir: string) => dir),
|
||||
}));
|
||||
|
||||
describe(runExpoCliCommand, () => {
|
||||
describe('Expo SDK >= 46', () => {
|
||||
@@ -75,6 +81,61 @@ describe(runExpoCliCommand, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe(isUsingModernYarnVersion, () => {
|
||||
const projectDir = '/project';
|
||||
const mockedFindPackagerRootDir = findPackagerRootDir as jest.MockedFunction<
|
||||
typeof findPackagerRootDir
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedFindPackagerRootDir.mockReturnValue(projectDir);
|
||||
jest.spyOn(fs, 'createReadStream').mockImplementation(((
|
||||
filePath: string,
|
||||
options?: { start?: number; end?: number }
|
||||
) => {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const start = options?.start ?? 0;
|
||||
const end = options?.end != null ? options.end + 1 : content.length;
|
||||
return Readable.from(Buffer.from(content.slice(start, end)));
|
||||
}) as any);
|
||||
});
|
||||
|
||||
it('returns true when .yarnrc.yml exists in project directory', async () => {
|
||||
vol.fromJSON({ [path.join(projectDir, '.yarnrc.yml')]: '' });
|
||||
expect(await isUsingModernYarnVersion(projectDir)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when .yarnrc.yml exists in workspace root directory', async () => {
|
||||
const workspaceRoot = '/workspace-root';
|
||||
mockedFindPackagerRootDir.mockReturnValue(workspaceRoot);
|
||||
vol.fromJSON({ [path.join(workspaceRoot, '.yarnrc.yml')]: '' });
|
||||
expect(await isUsingModernYarnVersion(projectDir)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when no .yarnrc.yml and no yarn.lock exist', async () => {
|
||||
vol.fromJSON({ [projectDir]: null });
|
||||
expect(await isUsingModernYarnVersion(projectDir)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when yarn.lock contains classic v1 header', async () => {
|
||||
vol.fromJSON({ [path.join(projectDir, 'yarn.lock')]: '# yarn lockfile v1\n' });
|
||||
expect(await isUsingModernYarnVersion(projectDir)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when yarn.lock does not contain classic v1 header', async () => {
|
||||
vol.fromJSON({ [path.join(projectDir, 'yarn.lock')]: '__metadata:\n version: 8\n' });
|
||||
expect(await isUsingModernYarnVersion(projectDir)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when .yarnrc.yml exists even with classic yarn.lock', async () => {
|
||||
vol.fromJSON({
|
||||
[path.join(projectDir, '.yarnrc.yml')]: '',
|
||||
[path.join(projectDir, 'yarn.lock')]: '# yarn lockfile v1\n',
|
||||
});
|
||||
expect(await isUsingModernYarnVersion(projectDir)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe(readEasJsonContents, () => {
|
||||
it('returns the raw eas.json contents even when it is not valid JSON', async () => {
|
||||
const projectDir = '/project';
|
||||
|
||||
@@ -4,13 +4,43 @@ import path from 'path';
|
||||
|
||||
import { PackageManager, findPackagerRootDir } from '../utils/packageManager';
|
||||
|
||||
async function readFirstChars(filePath: string, chars: number): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Uint8Array[] = [];
|
||||
const stream = fs.createReadStream(filePath, {
|
||||
start: 0,
|
||||
end: chars - 1,
|
||||
});
|
||||
stream.on('error', reject);
|
||||
stream.on('data', chunk => {
|
||||
chunks.push(chunk as Uint8Array);
|
||||
});
|
||||
stream.on('end', () => {
|
||||
resolve(Buffer.concat(chunks).toString('utf8'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* check if .yarnrc.yml exists in the project dir or in the workspace root dir
|
||||
* check if .yarnrc.yml exists in the project dir or in the workspace root dir,
|
||||
* or if the `yarn.lock` file is classic one, not a modern one
|
||||
*/
|
||||
export async function isUsingModernYarnVersion(projectDir: string): Promise<boolean> {
|
||||
const rootDir = findPackagerRootDir(projectDir);
|
||||
const yarnrcPath = path.join(projectDir, '.yarnrc.yml');
|
||||
const yarnrcRootPath = path.join(findPackagerRootDir(projectDir), '.yarnrc.yml');
|
||||
return (await fs.pathExists(yarnrcPath)) || (await fs.pathExists(yarnrcRootPath));
|
||||
if ((await fs.pathExists(yarnrcPath)) || (await fs.pathExists(yarnrcRootPath))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const yarnlockPath = path.join(rootDir, 'yarn.lock');
|
||||
if (!(await fs.pathExists(yarnlockPath))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The yarn.lock file is for Yarn Classic, not Modern, if it contains "# yarn lockfile v1"
|
||||
const startOfLockfile = await readFirstChars(yarnlockPath, 100);
|
||||
return !/yarn lockfile v1/i.test(startOfLockfile);
|
||||
}
|
||||
|
||||
export function runExpoCliCommand({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-eas-build-function",
|
||||
"version": "18.4.0",
|
||||
"version": "18.5.0",
|
||||
"description": "Create functions for use in EAS Build custom builds.",
|
||||
"keywords": [
|
||||
"expo",
|
||||
@@ -35,7 +35,7 @@
|
||||
"clean": "rimraf ./build/ \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/steps": "18.4.0"
|
||||
"@expo/steps": "18.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/package-manager": "1.9.10",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/downloader",
|
||||
"version": "18.0.1",
|
||||
"version": "18.5.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-build-cache-provider",
|
||||
"version": "18.0.2",
|
||||
"version": "18.5.0",
|
||||
"description": "A build cache provider plugin for the Expo CLI",
|
||||
"homepage": "https://github.com/expo/eas-cli",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/eas-build-job",
|
||||
"version": "18.4.0",
|
||||
"version": "18.5.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "MIT",
|
||||
"author": "Expo <[email protected]>",
|
||||
@@ -25,7 +25,7 @@
|
||||
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/logger": "18.0.1",
|
||||
"@expo/logger": "18.5.0",
|
||||
"joi": "^17.13.1",
|
||||
"semver": "^7.6.2",
|
||||
"zod": "^4.3.5"
|
||||
|
||||
@@ -46,12 +46,76 @@ describe('StaticWorkflowInterpolationContextZ', () => {
|
||||
app: {
|
||||
id: '1234567890',
|
||||
},
|
||||
build_upload: {
|
||||
id: '123e4567-e89b-42d3-a456-426614174000',
|
||||
state: 'processing',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(StaticWorkflowInterpolationContextZ.parse(context)).toEqual(context);
|
||||
});
|
||||
|
||||
it('accepts app_store_connect context without build_upload', () => {
|
||||
const context = {
|
||||
after: {},
|
||||
needs: {},
|
||||
workflow: {
|
||||
id: 'workflow-id',
|
||||
name: 'workflow-name',
|
||||
filename: 'workflow.yml',
|
||||
url: 'https://expo.dev/accounts/example/workflows/workflow-id',
|
||||
},
|
||||
app: {
|
||||
id: 'app-id',
|
||||
slug: 'app-slug',
|
||||
},
|
||||
account: {
|
||||
id: 'account-id',
|
||||
name: 'account-name',
|
||||
},
|
||||
app_store_connect: {
|
||||
app: {
|
||||
id: '1234567890',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(StaticWorkflowInterpolationContextZ.parse(context)).toEqual(context);
|
||||
});
|
||||
|
||||
it('rejects invalid app_store_connect build_upload context', () => {
|
||||
const context = {
|
||||
after: {},
|
||||
needs: {},
|
||||
workflow: {
|
||||
id: 'workflow-id',
|
||||
name: 'workflow-name',
|
||||
filename: 'workflow.yml',
|
||||
url: 'https://expo.dev/accounts/example/workflows/workflow-id',
|
||||
},
|
||||
app: {
|
||||
id: 'app-id',
|
||||
slug: 'app-slug',
|
||||
},
|
||||
account: {
|
||||
id: 'account-id',
|
||||
name: 'account-name',
|
||||
},
|
||||
app_store_connect: {
|
||||
app: {
|
||||
id: '1234567890',
|
||||
},
|
||||
build_upload: {
|
||||
id: 'not-a-uuid',
|
||||
state: 'invalid-state',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => StaticWorkflowInterpolationContextZ.parse(context)).toThrow();
|
||||
});
|
||||
|
||||
it('rejects invalid app and account context', () => {
|
||||
const context = {
|
||||
after: {},
|
||||
|
||||
@@ -264,6 +264,12 @@ export const StaticWorkflowInterpolationContextZ = z.object({
|
||||
app: z.looseObject({
|
||||
id: z.string(),
|
||||
}),
|
||||
build_upload: z
|
||||
.looseObject({
|
||||
id: z.string(),
|
||||
state: z.enum(['awaiting_upload', 'processing', 'failed', 'complete']),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
+92
-90
@@ -178,7 +178,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -195,7 +195,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:usage [ACCOUNT_NAME]`
|
||||
|
||||
@@ -217,7 +217,7 @@ DESCRIPTION
|
||||
view account usage and billing for the current cycle
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/account/usage.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/account/usage.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -234,7 +234,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -248,7 +248,7 @@ DESCRIPTION
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
@@ -300,7 +300,7 @@ DESCRIPTION
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
@@ -321,7 +321,7 @@ DESCRIPTION
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
@@ -341,7 +341,7 @@ DESCRIPTION
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
@@ -361,7 +361,7 @@ DESCRIPTION
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
@@ -384,7 +384,7 @@ DESCRIPTION
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -422,7 +422,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -442,7 +442,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -460,7 +460,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:delete [BUILD_ID]`
|
||||
|
||||
@@ -480,7 +480,7 @@ DESCRIPTION
|
||||
delete a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
|
||||
## `eas build:dev`
|
||||
|
||||
@@ -501,7 +501,7 @@ DESCRIPTION
|
||||
run dev client simulator/emulator build with matching fingerprint or create a new one
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
|
||||
## `eas build:download`
|
||||
|
||||
@@ -522,7 +522,7 @@ DESCRIPTION
|
||||
download simulator/emulator builds for a given fingerprint hash
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/download.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/download.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -560,7 +560,7 @@ DESCRIPTION
|
||||
inspect the state of the project at specific build stages, useful for troubleshooting
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -600,7 +600,7 @@ DESCRIPTION
|
||||
list all builds for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:resign`
|
||||
|
||||
@@ -630,7 +630,7 @@ DESCRIPTION
|
||||
re-sign a build archive
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
|
||||
## `eas build:run`
|
||||
|
||||
@@ -656,7 +656,7 @@ DESCRIPTION
|
||||
run simulator/emulator builds from eas-cli
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/run.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/run.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -708,7 +708,7 @@ DESCRIPTION
|
||||
get the latest version from EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
|
||||
## `eas build:version:set`
|
||||
|
||||
@@ -726,7 +726,7 @@ DESCRIPTION
|
||||
update version of an app
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
|
||||
## `eas build:version:sync`
|
||||
|
||||
@@ -744,7 +744,7 @@ DESCRIPTION
|
||||
update a version in native code with a value stored on EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -761,7 +761,7 @@ DESCRIPTION
|
||||
view a build for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -782,7 +782,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:delete [NAME]`
|
||||
|
||||
@@ -803,7 +803,7 @@ DESCRIPTION
|
||||
Delete a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -825,7 +825,7 @@ DESCRIPTION
|
||||
point a channel at a new branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -845,7 +845,7 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:pause [NAME]`
|
||||
|
||||
@@ -867,7 +867,7 @@ DESCRIPTION
|
||||
pause a channel to stop it from sending updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
|
||||
## `eas channel:resume [NAME]`
|
||||
|
||||
@@ -889,7 +889,7 @@ DESCRIPTION
|
||||
resume a channel to start sending updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
|
||||
## `eas channel:rollout [CHANNEL]`
|
||||
|
||||
@@ -924,7 +924,7 @@ DESCRIPTION
|
||||
Roll a new branch out on a channel incrementally.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -947,7 +947,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -968,7 +968,7 @@ DESCRIPTION
|
||||
display project configuration (app.json + eas.json)
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -985,7 +985,7 @@ DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
|
||||
## `eas credentials:configure-build`
|
||||
|
||||
@@ -1003,7 +1003,7 @@ DESCRIPTION
|
||||
Set up credentials for building your project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
|
||||
## `eas deploy [options]`
|
||||
|
||||
@@ -1032,7 +1032,7 @@ ALIASES
|
||||
$ eas worker:deploy
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
|
||||
## `eas deploy:alias`
|
||||
|
||||
@@ -1057,7 +1057,7 @@ ALIASES
|
||||
$ eas deploy:promote
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
|
||||
## `eas deploy:alias:delete [ALIAS_NAME]`
|
||||
|
||||
@@ -1078,7 +1078,7 @@ ALIASES
|
||||
$ eas worker:alias:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
|
||||
## `eas deploy:delete [DEPLOYMENT_ID]`
|
||||
|
||||
@@ -1099,7 +1099,7 @@ ALIASES
|
||||
$ eas worker:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
|
||||
## `eas deploy:promote`
|
||||
|
||||
@@ -1136,7 +1136,7 @@ DESCRIPTION
|
||||
register new Apple Devices to use for internal distribution
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -1156,7 +1156,7 @@ DESCRIPTION
|
||||
remove a registered device from your account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -1177,7 +1177,7 @@ DESCRIPTION
|
||||
list all registered devices for your account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:rename`
|
||||
|
||||
@@ -1198,7 +1198,7 @@ DESCRIPTION
|
||||
rename a registered device
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -1212,7 +1212,7 @@ DESCRIPTION
|
||||
view a device for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -1226,7 +1226,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas env:create [ENVIRONMENT]`
|
||||
|
||||
@@ -1258,7 +1258,7 @@ DESCRIPTION
|
||||
create an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/env/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/env/create.ts)_
|
||||
|
||||
## `eas env:delete [ENVIRONMENT]`
|
||||
|
||||
@@ -1284,7 +1284,7 @@ DESCRIPTION
|
||||
delete an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
|
||||
## `eas env:exec ENVIRONMENT BASH_COMMAND`
|
||||
|
||||
@@ -1306,7 +1306,7 @@ DESCRIPTION
|
||||
execute a command with environment variables from the selected environment
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
|
||||
## `eas env:get [ENVIRONMENT]`
|
||||
|
||||
@@ -1334,7 +1334,7 @@ DESCRIPTION
|
||||
view an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/env/get.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/env/get.ts)_
|
||||
|
||||
## `eas env:list [ENVIRONMENT]`
|
||||
|
||||
@@ -1362,7 +1362,7 @@ DESCRIPTION
|
||||
list environment variables for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/env/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/env/list.ts)_
|
||||
|
||||
## `eas env:pull [ENVIRONMENT]`
|
||||
|
||||
@@ -1385,7 +1385,7 @@ DESCRIPTION
|
||||
pull environment variables for the selected environment to .env file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
|
||||
## `eas env:push [ENVIRONMENT]`
|
||||
|
||||
@@ -1407,7 +1407,7 @@ DESCRIPTION
|
||||
push environment variables from .env file to the selected environment
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/env/push.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/env/push.ts)_
|
||||
|
||||
## `eas env:update [ENVIRONMENT]`
|
||||
|
||||
@@ -1441,7 +1441,7 @@ DESCRIPTION
|
||||
update an environment variable on the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/env/update.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/env/update.ts)_
|
||||
|
||||
## `eas fingerprint:compare [HASH1] [HASH2]`
|
||||
|
||||
@@ -1485,7 +1485,7 @@ EXAMPLES
|
||||
$ eas fingerprint:compare <FINGERPRINT-HASH> --update-id <UPDATE-ID> # Compare fingerprint from update against provided fingerprint
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
|
||||
## `eas fingerprint:generate`
|
||||
|
||||
@@ -1516,7 +1516,7 @@ EXAMPLES
|
||||
$ eas fingerprint:generate --json --non-interactive --platform android # Output fingerprint json to stdout
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -1625,7 +1625,7 @@ DESCRIPTION
|
||||
validate the local store configuration
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
|
||||
## `eas metadata:pull`
|
||||
|
||||
@@ -1633,16 +1633,17 @@ generate the local store configuration from the app stores
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas metadata:pull [-e <value>]
|
||||
$ eas metadata:pull [-e <value>] [--non-interactive]
|
||||
|
||||
FLAGS
|
||||
-e, --profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
|
||||
DESCRIPTION
|
||||
generate the local store configuration from the app stores
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
@@ -1650,16 +1651,17 @@ sync the local store configuration to the app stores
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas metadata:push [-e <value>]
|
||||
$ eas metadata:push [-e <value>] [--non-interactive]
|
||||
|
||||
FLAGS
|
||||
-e, --profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
|
||||
DESCRIPTION
|
||||
sync the local store configuration to the app stores
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas new [PATH]`
|
||||
|
||||
@@ -1711,7 +1713,7 @@ DESCRIPTION
|
||||
open the project page in a web browser
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/open.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/open.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -1725,7 +1727,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -1748,7 +1750,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas project:new [PATH]`
|
||||
|
||||
@@ -1772,7 +1774,7 @@ ALIASES
|
||||
$ eas new
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/project/new.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/project/new.ts)_
|
||||
|
||||
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
|
||||
|
||||
@@ -1790,7 +1792,7 @@ ALIASES
|
||||
$ eas onboarding
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/project/onboarding.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/project/onboarding.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -1823,7 +1825,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -1866,7 +1868,7 @@ DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -1887,7 +1889,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -1908,7 +1910,7 @@ DESCRIPTION
|
||||
delete all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:edit [GROUPID]`
|
||||
|
||||
@@ -1933,7 +1935,7 @@ DESCRIPTION
|
||||
edit all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -1960,7 +1962,7 @@ DESCRIPTION
|
||||
view the recent updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:republish`
|
||||
|
||||
@@ -1998,7 +2000,7 @@ DESCRIPTION
|
||||
roll back to an existing update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
|
||||
## `eas update:revert-update-rollout`
|
||||
|
||||
@@ -2026,7 +2028,7 @@ DESCRIPTION
|
||||
revert a rollout update for a project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
|
||||
## `eas update:roll-back-to-embedded`
|
||||
|
||||
@@ -2056,7 +2058,7 @@ DESCRIPTION
|
||||
roll back to the embedded update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
|
||||
## `eas update:rollback`
|
||||
|
||||
@@ -2077,7 +2079,7 @@ DESCRIPTION
|
||||
instead execute "eas update:republish" or "eas update:roll-back-to-embedded".
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -2097,7 +2099,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas upload`
|
||||
|
||||
@@ -2118,7 +2120,7 @@ DESCRIPTION
|
||||
upload a local build and generate a sharable link
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/upload.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/upload.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -2140,7 +2142,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -2160,7 +2162,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -2179,7 +2181,7 @@ DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -2202,7 +2204,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -2219,7 +2221,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
@@ -2340,7 +2342,7 @@ DESCRIPTION
|
||||
to cancel.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
|
||||
## `eas workflow:create [NAME]`
|
||||
|
||||
@@ -2360,7 +2362,7 @@ DESCRIPTION
|
||||
create a new workflow configuration YAML file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
|
||||
## `eas workflow:logs [ID]`
|
||||
|
||||
@@ -2384,7 +2386,7 @@ DESCRIPTION
|
||||
If no ID is passed in, you will be prompted to select from recent workflow runs for the current project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
|
||||
## `eas workflow:run FILE`
|
||||
|
||||
@@ -2424,7 +2426,7 @@ FLAG DESCRIPTIONS
|
||||
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
|
||||
## `eas workflow:runs`
|
||||
|
||||
@@ -2446,7 +2448,7 @@ DESCRIPTION
|
||||
list recent workflow runs for this project, with their IDs, statuses, and timestamps
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
|
||||
## `eas workflow:status [WORKFLOW_RUN_ID]`
|
||||
|
||||
@@ -2474,7 +2476,7 @@ FLAG DESCRIPTIONS
|
||||
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/workflow/status.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/workflow/status.ts)_
|
||||
|
||||
## `eas workflow:validate PATH`
|
||||
|
||||
@@ -2494,7 +2496,7 @@ DESCRIPTION
|
||||
validate a workflow configuration yaml file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
|
||||
## `eas workflow:view [ID]`
|
||||
|
||||
@@ -2516,5 +2518,5 @@ DESCRIPTION
|
||||
workflow runs for the current project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"version": "18.4.0",
|
||||
"version": "18.5.0",
|
||||
"description": "EAS command line tool",
|
||||
"keywords": [
|
||||
"cli",
|
||||
@@ -43,15 +43,15 @@
|
||||
"copy-new-templates": "rimraf build/commandUtils/new/templates && mkdir -p build/commandUtils/new && cp -r src/commandUtils/new/templates build/commandUtils/new"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "2.1.16",
|
||||
"@expo/apple-utils": "2.1.19",
|
||||
"@expo/code-signing-certificates": "0.0.5",
|
||||
"@expo/config": "55.0.10",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/eas-build-job": "18.4.0",
|
||||
"@expo/eas-json": "18.4.0",
|
||||
"@expo/eas-build-job": "18.5.0",
|
||||
"@expo/eas-json": "18.5.0",
|
||||
"@expo/env": "^1.0.0",
|
||||
"@expo/json-file": "8.3.3",
|
||||
"@expo/logger": "18.0.1",
|
||||
"@expo/logger": "18.5.0",
|
||||
"@expo/multipart-body-parser": "2.0.0",
|
||||
"@expo/osascript": "2.1.4",
|
||||
"@expo/package-manager": "1.9.10",
|
||||
@@ -63,7 +63,7 @@
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "18.4.0",
|
||||
"@expo/steps": "18.5.0",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "^4.8.3",
|
||||
"@oclif/plugin-autocomplete": "^3.2.40",
|
||||
|
||||
@@ -328,7 +328,7 @@
|
||||
},
|
||||
"previews": {
|
||||
"type": "object",
|
||||
"description": "Video previews for this locale, organized by display type (e.g., IPHONE_67, IPAD_PRO_129)",
|
||||
"description": "Video previews for this locale, organized by display type (e.g., IPHONE_67, IPAD_PRO_129). Each device type accepts a single preview config (string or object) or an array of up to 3 preview configs.",
|
||||
"additionalProperties": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -348,6 +348,31 @@
|
||||
},
|
||||
"required": ["path"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Video file path (relative to project root)"
|
||||
},
|
||||
"previewFrameTimeCode": {
|
||||
"type": "string",
|
||||
"description": "Optional preview frame time code (e.g., '00:05:00' for 5 seconds)"
|
||||
}
|
||||
},
|
||||
"required": ["path"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -515,6 +540,57 @@
|
||||
"lootBox": {
|
||||
"type": "boolean",
|
||||
"description": "Does your app contain purchasable loot boxes? These virtual containers provide players with in-game items, including functional cards or cosmetic modifications, based on random chance."
|
||||
},
|
||||
"advertising": {
|
||||
"type": "boolean",
|
||||
"description": "Does your app display advertising?"
|
||||
},
|
||||
"ageAssurance": {
|
||||
"type": "boolean",
|
||||
"description": "Does your app implement age assurance mechanisms?"
|
||||
},
|
||||
"ageRatingOverrideV2": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/apple/AppleAgeRatingOverrideV2"
|
||||
}
|
||||
],
|
||||
"description": "Override the age rating using the V2 rating system. Allows setting a more granular age rating than the auto-calculated value."
|
||||
},
|
||||
"developerAgeRatingInfoUrl": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
}
|
||||
],
|
||||
"description": "URL to additional developer-provided information about the app's age rating."
|
||||
},
|
||||
"gunsOrOtherWeapons": {
|
||||
"$ref": "#/definitions/apple/AppleRating",
|
||||
"description": "Does the app contain depictions of guns or other weapons?"
|
||||
},
|
||||
"healthOrWellnessTopics": {
|
||||
"type": "boolean",
|
||||
"description": "Does your app contain health or wellness topics?"
|
||||
},
|
||||
"messagingAndChat": {
|
||||
"type": "boolean",
|
||||
"description": "Does your app contain messaging or chat functionality?"
|
||||
},
|
||||
"parentalControls": {
|
||||
"type": "boolean",
|
||||
"description": "Does your app implement parental controls?"
|
||||
},
|
||||
"userGeneratedContent": {
|
||||
"type": "boolean",
|
||||
"description": "Does your app contain or display user-generated content?"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -637,6 +713,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"AppleAppClip": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "App Clip metadata. Only applies to apps that ship an App Clip target.",
|
||||
"properties": {
|
||||
"defaultExperience": {
|
||||
"$ref": "#/definitions/apple/AppleAppClipDefaultExperience",
|
||||
"description": "The default experience for this App Clip. There is exactly one per app."
|
||||
}
|
||||
}
|
||||
},
|
||||
"AppleAppClipDefaultExperience": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Default experience for an App Clip - the configuration shown when invoked without a custom experience.",
|
||||
"properties": {
|
||||
"action": {
|
||||
"enum": ["OPEN", "VIEW", "PLAY"],
|
||||
"description": "Action button shown in the App Clip card."
|
||||
},
|
||||
"releaseWithAppStoreVersion": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to release this default experience alongside the next App Store version."
|
||||
},
|
||||
"reviewDetail": {
|
||||
"$ref": "#/definitions/apple/AppleAppClipReviewDetail",
|
||||
"description": "App Store review information for the App Clip default experience."
|
||||
},
|
||||
"info": {
|
||||
"$ref": "#/definitions/apple/AppleAppClipInfo",
|
||||
"description": "Localized subtitle and header image for the App Clip default experience."
|
||||
}
|
||||
}
|
||||
},
|
||||
"AppleAppClipReviewDetail": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "App Store review detail for an App Clip default experience.",
|
||||
"required": ["invocationUrls"],
|
||||
"properties": {
|
||||
"invocationUrls": {
|
||||
"type": "array",
|
||||
"description": "Invocation URLs that App Review will use to launch the App Clip during review.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"AppleAppClipLocalizedInfo": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Localized subtitle and header image for an App Clip default experience.",
|
||||
"properties": {
|
||||
"subtitle": {
|
||||
"type": "string",
|
||||
"description": "Subtitle shown in the App Clip card. Apple limits this to 43 characters.",
|
||||
"maxLength": 43
|
||||
},
|
||||
"headerImage": {
|
||||
"type": "string",
|
||||
"description": "Relative path (from project root) to the App Clip header image PNG."
|
||||
}
|
||||
}
|
||||
},
|
||||
"AppleAppClipInfo": {
|
||||
"type": "object",
|
||||
"description": "Localized App Clip information, keyed by locale.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"ar-SA": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Arabic" },
|
||||
"ca": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Catalan" },
|
||||
"zh-Hans": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Chinese (Simplified)" },
|
||||
"zh-Hant": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Chinese (Traditional)" },
|
||||
"hr": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Croatian" },
|
||||
"cs": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Czech" },
|
||||
"da": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Danish" },
|
||||
"nl-NL": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Dutch" },
|
||||
"en-AU": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "English (Australia)" },
|
||||
"en-CA": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "English (Canada)" },
|
||||
"en-GB": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "English (U.K.)" },
|
||||
"en-US": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "English (U.S.)" },
|
||||
"fi": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Finnish" },
|
||||
"fr-FR": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "French" },
|
||||
"fr-CA": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "French (Canada)" },
|
||||
"de-DE": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "German" },
|
||||
"el": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Greek" },
|
||||
"he": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Hebrew" },
|
||||
"hi": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Hindi" },
|
||||
"hu": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Hungarian" },
|
||||
"id": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Indonesian" },
|
||||
"it": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Italian" },
|
||||
"ja": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Japanese" },
|
||||
"ko": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Korean" },
|
||||
"ms": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Malay" },
|
||||
"no": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Norwegian" },
|
||||
"pl": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Polish" },
|
||||
"pt-BR": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Portuguese (Brazil)" },
|
||||
"pt-PT": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Portuguese (Portugal)" },
|
||||
"ro": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Romanian" },
|
||||
"ru": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Russian" },
|
||||
"sk": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Slovak" },
|
||||
"es-MX": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Spanish (Mexico)" },
|
||||
"es-ES": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Spanish (Spain)" },
|
||||
"sv": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Swedish" },
|
||||
"th": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Thai" },
|
||||
"tr": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Turkish" },
|
||||
"uk": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Ukrainian" },
|
||||
"vi": { "$ref": "#/definitions/apple/AppleAppClipLocalizedInfo", "description": "Vietnamese" }
|
||||
}
|
||||
},
|
||||
"AppleAgeRatingOverride": {
|
||||
"enum": [
|
||||
"NONE",
|
||||
@@ -650,6 +838,16 @@
|
||||
"FIFTEEN_PLUS",
|
||||
"NINETEEN_PLUS"
|
||||
]
|
||||
},
|
||||
"AppleAgeRatingOverrideV2": {
|
||||
"enum": [
|
||||
"NONE",
|
||||
"NINE_PLUS",
|
||||
"THIRTEEN_PLUS",
|
||||
"SIXTEEN_PLUS",
|
||||
"EIGHTEEN_PLUS",
|
||||
"UNRATED"
|
||||
]
|
||||
}
|
||||
},
|
||||
"AppleConfig": {
|
||||
@@ -857,6 +1055,10 @@
|
||||
"review": {
|
||||
"$ref": "#/definitions/apple/AppleReview",
|
||||
"description": "Info for the App Store reviewer"
|
||||
},
|
||||
"appClip": {
|
||||
"$ref": "#/definitions/apple/AppleAppClip",
|
||||
"description": "App Clip metadata. Only applies to apps that ship an App Clip target."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# CLAUDE.md - metadata commands
|
||||
|
||||
`metadata:push` validates `store.config.json` against `packages/eas-cli/schema/metadata-0.json`. The schema can drift from the apple config reader/writer, and when it does `metadata:pull` happily writes fields that `metadata:push` then rejects — and the failure looks like the user's fault.
|
||||
|
||||
**Whenever you touch `src/metadata/apple/config/{reader,writer}.ts`, update `metadata-0.json` in the same change.** For new advisory fields:
|
||||
|
||||
- Add the property under `definitions.apple.AppleAdvisory.properties`.
|
||||
- For enums from `@expo/apple-utils`, add a matching `Apple<Name>` enum definition with the exact string values and reference it via `$ref`. Wrap in `oneOf` with `{ "type": "null" }` if the writer ever persists `null`.
|
||||
- Don't add to `required` — that breaks existing user configs. Optional with a sensible default in the writer is the convention.
|
||||
|
||||
The drift is caught by `src/metadata/apple/config/__tests__/writer.test.ts` ("writer output for ... passes JSON schema validation"). If you change apple-utils types or fixtures, that test will tell you exactly what the schema is missing.
|
||||
@@ -6,6 +6,7 @@ import path from 'path';
|
||||
|
||||
import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { downloadMetadataAsync } from '../../metadata/download';
|
||||
@@ -21,10 +22,7 @@ export default class MetadataPull extends EasCommand {
|
||||
description:
|
||||
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
|
||||
}),
|
||||
'non-interactive': Flags.boolean({
|
||||
default: false,
|
||||
description: 'Run the command in non-interactive mode.',
|
||||
}),
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Flags } from '@oclif/core';
|
||||
|
||||
import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { handleMetadataError } from '../../metadata/errors';
|
||||
@@ -19,10 +20,7 @@ export default class MetadataPush extends EasCommand {
|
||||
description:
|
||||
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
|
||||
}),
|
||||
'non-interactive': Flags.boolean({
|
||||
default: false,
|
||||
description: 'Run the command in non-interactive mode.',
|
||||
}),
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
|
||||
@@ -10,7 +10,11 @@ import { getAppFromContextAsync } from './actions/BuildCredentialsUtils';
|
||||
import { SetUpBuildCredentials } from './actions/SetUpBuildCredentials';
|
||||
import { SetUpPushKey } from './actions/SetUpPushKey';
|
||||
import { App, IosCredentials, Target } from './types';
|
||||
import { isAdHocProfile, isEnterpriseUniversalProfile } from './utils/provisioningProfile';
|
||||
import {
|
||||
isAdHocProfile,
|
||||
isDevelopmentProfile,
|
||||
isEnterpriseUniversalProfile,
|
||||
} from './utils/provisioningProfile';
|
||||
import { CommonIosAppCredentialsFragment } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { findApplicationTarget } from '../../project/ios/target';
|
||||
@@ -150,6 +154,7 @@ export default class IosCredentialsProvider {
|
||||
|
||||
private assertProvisioningProfileType(provisioningProfile: string, targetName?: string): void {
|
||||
const isAdHoc = isAdHocProfile(provisioningProfile);
|
||||
const isDevelopment = isDevelopmentProfile(provisioningProfile);
|
||||
const isEnterprise = isEnterpriseUniversalProfile(provisioningProfile);
|
||||
if (this.options.distribution === 'internal') {
|
||||
if (this.options.enterpriseProvisioning === 'universal' && !isEnterprise) {
|
||||
@@ -164,16 +169,21 @@ export default class IosCredentialsProvider {
|
||||
targetName ? ` (target '${targetName})'` : ''
|
||||
} for internal distribution if you specified "enterpriseProvisioning": "adhoc" in eas.json`
|
||||
);
|
||||
} else if (!this.options.enterpriseProvisioning && !isEnterprise && !isAdHoc) {
|
||||
} else if (
|
||||
!this.options.enterpriseProvisioning &&
|
||||
!isEnterprise &&
|
||||
!isAdHoc &&
|
||||
!isDevelopment
|
||||
) {
|
||||
throw new Error(
|
||||
`You must use an adhoc provisioning profile${
|
||||
`You must use an adhoc or development provisioning profile${
|
||||
targetName ? ` (target '${targetName})'` : ''
|
||||
} for internal distribution.`
|
||||
);
|
||||
}
|
||||
} else if (isAdHoc) {
|
||||
} else if (isAdHoc || isDevelopment) {
|
||||
throw new Error(
|
||||
`You can't use an adhoc provisioning profile${
|
||||
`You can't use ${isAdHoc ? 'an adhoc' : 'a development'} provisioning profile${
|
||||
targetName ? ` (target '${targetName}')` : ''
|
||||
} for app store distribution.`
|
||||
);
|
||||
|
||||
@@ -23,7 +23,21 @@ export function readProfileName(dataBase64: string): string {
|
||||
export function isAdHocProfile(dataBase64: string): boolean {
|
||||
const profilePlist = parse(dataBase64);
|
||||
const provisionedDevices = profilePlist['ProvisionedDevices'] as string[] | undefined;
|
||||
return Array.isArray(provisionedDevices);
|
||||
if (!Array.isArray(provisionedDevices)) {
|
||||
return false;
|
||||
}
|
||||
const entitlements = profilePlist['Entitlements'] as PlistObject | undefined;
|
||||
return !entitlements?.['get-task-allow'];
|
||||
}
|
||||
|
||||
export function isDevelopmentProfile(dataBase64: string): boolean {
|
||||
const profilePlist = parse(dataBase64);
|
||||
const provisionedDevices = profilePlist['ProvisionedDevices'] as string[] | undefined;
|
||||
if (!Array.isArray(provisionedDevices)) {
|
||||
return false;
|
||||
}
|
||||
const entitlements = profilePlist['Entitlements'] as PlistObject | undefined;
|
||||
return !!entitlements?.['get-task-allow'];
|
||||
}
|
||||
|
||||
export function isEnterpriseUniversalProfile(dataBase64: string): boolean {
|
||||
|
||||
@@ -34,6 +34,15 @@ export const emptyAdvisory: AgeRatingDeclarationProps = {
|
||||
ageRatingOverride: null,
|
||||
koreaAgeRatingOverride: null,
|
||||
lootBox: null,
|
||||
advertising: null,
|
||||
ageAssurance: null,
|
||||
ageRatingOverrideV2: null,
|
||||
developerAgeRatingInfoUrl: null,
|
||||
gunsOrOtherWeapons: null,
|
||||
healthOrWellnessTopics: null,
|
||||
messagingAndChat: null,
|
||||
parentalControls: null,
|
||||
userGeneratedContent: null,
|
||||
};
|
||||
|
||||
export const leastRestrictiveAdvisory: AgeRatingDeclarationProps = {
|
||||
@@ -55,6 +64,15 @@ export const leastRestrictiveAdvisory: AgeRatingDeclarationProps = {
|
||||
ageRatingOverride: RatingOverride.NONE,
|
||||
koreaAgeRatingOverride: KoreaRatingOverride.NONE,
|
||||
lootBox: false,
|
||||
advertising: false,
|
||||
ageAssurance: false,
|
||||
ageRatingOverrideV2: null,
|
||||
developerAgeRatingInfoUrl: null,
|
||||
gunsOrOtherWeapons: Rating.NONE,
|
||||
healthOrWellnessTopics: false,
|
||||
messagingAndChat: false,
|
||||
parentalControls: false,
|
||||
userGeneratedContent: false,
|
||||
};
|
||||
|
||||
export const mostRestrictiveAdvisory: AgeRatingDeclarationProps = {
|
||||
@@ -76,6 +94,15 @@ export const mostRestrictiveAdvisory: AgeRatingDeclarationProps = {
|
||||
ageRatingOverride: RatingOverride.SEVENTEEN_PLUS,
|
||||
koreaAgeRatingOverride: KoreaRatingOverride.NINETEEN_PLUS,
|
||||
lootBox: true,
|
||||
advertising: true,
|
||||
ageAssurance: true,
|
||||
ageRatingOverrideV2: null,
|
||||
developerAgeRatingInfoUrl: null,
|
||||
gunsOrOtherWeapons: Rating.FREQUENT_OR_INTENSE,
|
||||
healthOrWellnessTopics: true,
|
||||
messagingAndChat: true,
|
||||
parentalControls: true,
|
||||
userGeneratedContent: true,
|
||||
};
|
||||
|
||||
export const kidsSixToEightAdvisory: AgeRatingDeclarationProps = {
|
||||
@@ -97,4 +124,13 @@ export const kidsSixToEightAdvisory: AgeRatingDeclarationProps = {
|
||||
ageRatingOverride: RatingOverride.NONE,
|
||||
koreaAgeRatingOverride: KoreaRatingOverride.NONE,
|
||||
lootBox: false,
|
||||
advertising: false,
|
||||
ageAssurance: false,
|
||||
ageRatingOverrideV2: null,
|
||||
developerAgeRatingInfoUrl: null,
|
||||
gunsOrOtherWeapons: Rating.NONE,
|
||||
healthOrWellnessTopics: false,
|
||||
messagingAndChat: false,
|
||||
parentalControls: false,
|
||||
userGeneratedContent: false,
|
||||
};
|
||||
@@ -176,3 +176,43 @@ describe('getVersion', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAppClip', () => {
|
||||
it('returns null when no app clip is configured', () => {
|
||||
const reader = new AppleConfigReader({});
|
||||
expect(reader.getAppClip()).toBeNull();
|
||||
expect(reader.getAppClipDefaultExperience()).toBeNull();
|
||||
expect(reader.getAppClipLocales()).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns the configured app clip block', () => {
|
||||
const reader = new AppleConfigReader({
|
||||
appClip: {
|
||||
defaultExperience: {
|
||||
action: 'OPEN',
|
||||
releaseWithAppStoreVersion: true,
|
||||
reviewDetail: { invocationUrls: ['https://example.com/clip'] },
|
||||
info: {
|
||||
'en-US': { subtitle: 'Quick experience', headerImage: 'header.png' },
|
||||
'fr-FR': { subtitle: 'Expérience rapide' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(reader.getAppClipDefaultExperience()).toMatchObject({
|
||||
action: 'OPEN',
|
||||
releaseWithAppStoreVersion: true,
|
||||
reviewDetail: { invocationUrls: ['https://example.com/clip'] },
|
||||
});
|
||||
expect(reader.getAppClipLocales().sort()).toEqual(['en-US', 'fr-FR']);
|
||||
expect(reader.getAppClipLocalizedInfo('en-US')).toEqual({
|
||||
subtitle: 'Quick experience',
|
||||
headerImage: 'header.png',
|
||||
});
|
||||
expect(reader.getAppClipLocalizedInfo('fr-FR')).toEqual({
|
||||
subtitle: 'Expérience rapide',
|
||||
});
|
||||
expect(reader.getAppClipLocalizedInfo('de-DE')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import { nameAndDemoReviewDetails, nameOnlyReviewDetails } from './fixtures/appS
|
||||
import { automaticRelease, manualRelease, scheduledRelease } from './fixtures/appStoreVersion';
|
||||
import { dutchVersion, englishVersion } from './fixtures/appStoreVersionLocalization';
|
||||
import { phasedRelease } from './fixtures/appStoreVersionPhasedRelease';
|
||||
import { validateConfig } from '../../../config/validate';
|
||||
import { AppleConfigWriter } from '../writer';
|
||||
|
||||
describe('toSchema', () => {
|
||||
@@ -42,6 +43,22 @@ describe('setAgeRating', () => {
|
||||
writer.setAgeRating(mostRestrictiveAdvisory);
|
||||
expect(writer.schema.advisory).toMatchObject(mostRestrictiveAdvisory);
|
||||
});
|
||||
|
||||
// Drift guard: every advisory field that the writer emits must be allowed by
|
||||
// metadata-0.json. If you add a field to writer.ts (or apple-utils surfaces a
|
||||
// new one upstream), update the schema in the same change. Without this
|
||||
// test, the drift only shows up at `metadata:push` time as a confusing
|
||||
// validation error against the user's freshly pulled config.
|
||||
it.each([
|
||||
['emptyAdvisory', emptyAdvisory],
|
||||
['leastRestrictiveAdvisory', leastRestrictiveAdvisory],
|
||||
['mostRestrictiveAdvisory', mostRestrictiveAdvisory],
|
||||
['kidsSixToEightAdvisory', kidsSixToEightAdvisory],
|
||||
])('writer output for %s passes JSON schema validation', (_name, advisory) => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setAgeRating(advisory);
|
||||
expect(validateConfig(writer.toSchema())).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setInfoLocale', () => {
|
||||
@@ -329,3 +346,72 @@ describe('setReviewDetails', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAppClipDefaultExperience', () => {
|
||||
it('sets default experience attributes', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setAppClipDefaultExperience({
|
||||
action: 'OPEN',
|
||||
releaseWithAppStoreVersion: true,
|
||||
});
|
||||
expect(writer.schema.appClip?.defaultExperience).toMatchObject({
|
||||
action: 'OPEN',
|
||||
releaseWithAppStoreVersion: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAppClipReviewDetail', () => {
|
||||
it('sets invocation urls', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setAppClipReviewDetail({ invocationUrls: ['https://example.com/clip'] });
|
||||
expect(writer.schema.appClip?.defaultExperience?.reviewDetail).toEqual({
|
||||
invocationUrls: ['https://example.com/clip'],
|
||||
});
|
||||
});
|
||||
|
||||
it('removes review detail when null', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setAppClipReviewDetail({ invocationUrls: ['https://example.com/clip'] });
|
||||
writer.setAppClipReviewDetail(null);
|
||||
expect(writer.schema.appClip?.defaultExperience?.reviewDetail).toBeUndefined();
|
||||
});
|
||||
|
||||
it('removes review detail when invocationUrls is empty', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setAppClipReviewDetail({ invocationUrls: ['https://example.com/clip'] });
|
||||
writer.setAppClipReviewDetail({ invocationUrls: [] });
|
||||
expect(writer.schema.appClip?.defaultExperience?.reviewDetail).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAppClipLocalizedInfo', () => {
|
||||
it('writes localized subtitle and header image', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setAppClipLocalizedInfo('en-US', {
|
||||
subtitle: 'Quick experience',
|
||||
headerImage: 'store/apple/app-clip/en-US/header.png',
|
||||
});
|
||||
expect(writer.schema.appClip?.defaultExperience?.info?.['en-US']).toEqual({
|
||||
subtitle: 'Quick experience',
|
||||
headerImage: 'store/apple/app-clip/en-US/header.png',
|
||||
});
|
||||
});
|
||||
|
||||
it('omits empty locale entries', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setAppClipLocalizedInfo('en-US', { subtitle: 'Quick' });
|
||||
writer.setAppClipLocalizedInfo('en-US', {});
|
||||
expect(writer.schema.appClip?.defaultExperience?.info?.['en-US']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps multiple locales independent', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setAppClipLocalizedInfo('en-US', { subtitle: 'Quick' });
|
||||
writer.setAppClipLocalizedInfo('fr-FR', { subtitle: 'Rapide' });
|
||||
expect(writer.schema.appClip?.defaultExperience?.info).toMatchObject({
|
||||
'en-US': { subtitle: 'Quick' },
|
||||
'fr-FR': { subtitle: 'Rapide' },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,14 @@ import {
|
||||
import uniq from '../../../utils/expodash/uniq';
|
||||
import { AttributesOf } from '../../utils/asc';
|
||||
import { removeDatePrecision } from '../../utils/date';
|
||||
import { AppleMetadata, ApplePreviews, AppleScreenshots } from '../types';
|
||||
import {
|
||||
AppleAppClip,
|
||||
AppleAppClipDefaultExperience,
|
||||
AppleAppClipLocalizedInfo,
|
||||
AppleMetadata,
|
||||
ApplePreviews,
|
||||
AppleScreenshots,
|
||||
} from '../types';
|
||||
|
||||
type PartialExcept<T, K extends keyof T> = Pick<T, K> & Partial<Omit<T, K>>;
|
||||
|
||||
@@ -57,6 +64,15 @@ export class AppleConfigReader {
|
||||
violenceRealistic: attributes.violenceRealistic ?? Rating.NONE,
|
||||
violenceRealisticProlongedGraphicOrSadistic:
|
||||
attributes.violenceRealisticProlongedGraphicOrSadistic ?? Rating.NONE,
|
||||
advertising: attributes.advertising ?? false,
|
||||
ageAssurance: attributes.ageAssurance ?? false,
|
||||
ageRatingOverrideV2: attributes.ageRatingOverrideV2 ?? null,
|
||||
developerAgeRatingInfoUrl: attributes.developerAgeRatingInfoUrl ?? null,
|
||||
gunsOrOtherWeapons: attributes.gunsOrOtherWeapons ?? Rating.NONE,
|
||||
healthOrWellnessTopics: attributes.healthOrWellnessTopics ?? false,
|
||||
messagingAndChat: attributes.messagingAndChat ?? false,
|
||||
parentalControls: attributes.parentalControls ?? false,
|
||||
userGeneratedContent: attributes.userGeneratedContent ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -221,4 +237,24 @@ export class AppleConfigReader {
|
||||
public getPreviews(locale: string): ApplePreviews | null {
|
||||
return this.schema.info?.[locale]?.previews ?? null;
|
||||
}
|
||||
|
||||
/** Get the App Clip configuration block, or null if no App Clip is configured. */
|
||||
public getAppClip(): AppleAppClip | null {
|
||||
return this.schema.appClip ?? null;
|
||||
}
|
||||
|
||||
/** Get the App Clip default experience, or null if not configured. */
|
||||
public getAppClipDefaultExperience(): AppleAppClipDefaultExperience | null {
|
||||
return this.schema.appClip?.defaultExperience ?? null;
|
||||
}
|
||||
|
||||
/** Locales that have App Clip default-experience info configured. */
|
||||
public getAppClipLocales(): string[] {
|
||||
return uniq(Object.keys(this.schema.appClip?.defaultExperience?.info ?? {}));
|
||||
}
|
||||
|
||||
/** Get the localized App Clip info (subtitle + header image) for a specific locale. */
|
||||
public getAppClipLocalizedInfo(locale: string): AppleAppClipLocalizedInfo | null {
|
||||
return this.schema.appClip?.defaultExperience?.info?.[locale] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,14 @@ import {
|
||||
} from '@expo/apple-utils';
|
||||
|
||||
import { AttributesOf } from '../../utils/asc';
|
||||
import { AppleMetadata, ApplePreviews, AppleScreenshots } from '../types';
|
||||
import {
|
||||
AppleAppClipDefaultExperience,
|
||||
AppleAppClipLocalizedInfo,
|
||||
AppleAppClipReviewDetail,
|
||||
AppleMetadata,
|
||||
ApplePreviews,
|
||||
AppleScreenshots,
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* Serializes the Apple ASC entities into the metadata configuration schema.
|
||||
@@ -55,6 +62,15 @@ export class AppleConfigWriter {
|
||||
violenceRealistic: attributes.violenceRealistic ?? Rating.NONE,
|
||||
violenceRealisticProlongedGraphicOrSadistic:
|
||||
attributes.violenceRealisticProlongedGraphicOrSadistic ?? Rating.NONE,
|
||||
advertising: attributes.advertising ?? false,
|
||||
ageAssurance: attributes.ageAssurance ?? false,
|
||||
ageRatingOverrideV2: attributes.ageRatingOverrideV2 ?? null,
|
||||
developerAgeRatingInfoUrl: attributes.developerAgeRatingInfoUrl ?? null,
|
||||
gunsOrOtherWeapons: attributes.gunsOrOtherWeapons ?? Rating.NONE,
|
||||
healthOrWellnessTopics: attributes.healthOrWellnessTopics ?? false,
|
||||
messagingAndChat: attributes.messagingAndChat ?? false,
|
||||
parentalControls: attributes.parentalControls ?? false,
|
||||
userGeneratedContent: attributes.userGeneratedContent ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -195,11 +211,80 @@ export class AppleConfigWriter {
|
||||
Object.keys(screenshots).length > 0 ? screenshots : undefined;
|
||||
}
|
||||
|
||||
/** Set video previews for a specific locale */
|
||||
/**
|
||||
* Set video previews for a specific locale.
|
||||
*
|
||||
* Accepts either the legacy single-config form or the multi-preview array form
|
||||
* for each preview type. When an array contains exactly one entry, it is
|
||||
* unwrapped to the legacy single-object form for stable, minimal config diffs.
|
||||
*/
|
||||
public setPreviews(locale: string, previews: ApplePreviews): void {
|
||||
this.schema.info = this.schema.info ?? {};
|
||||
this.schema.info[locale] = this.schema.info[locale] ?? { title: '' };
|
||||
this.schema.info[locale].previews = Object.keys(previews).length > 0 ? previews : undefined;
|
||||
|
||||
if (Object.keys(previews).length === 0) {
|
||||
this.schema.info[locale].previews = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized: ApplePreviews = {};
|
||||
for (const [previewType, value] of Object.entries(previews)) {
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
continue;
|
||||
}
|
||||
if (value.length === 1) {
|
||||
normalized[previewType as keyof ApplePreviews] = value[0];
|
||||
} else {
|
||||
normalized[previewType as keyof ApplePreviews] = value;
|
||||
}
|
||||
} else {
|
||||
normalized[previewType as keyof ApplePreviews] = value;
|
||||
}
|
||||
}
|
||||
|
||||
this.schema.info[locale].previews = Object.keys(normalized).length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
/** Set the App Clip default experience attributes (action, releaseWithAppStoreVersion). */
|
||||
public setAppClipDefaultExperience(
|
||||
attributes: Pick<AppleAppClipDefaultExperience, 'action' | 'releaseWithAppStoreVersion'>
|
||||
): void {
|
||||
this.schema.appClip = this.schema.appClip ?? {};
|
||||
this.schema.appClip.defaultExperience = {
|
||||
...this.schema.appClip.defaultExperience,
|
||||
action: attributes.action,
|
||||
releaseWithAppStoreVersion: attributes.releaseWithAppStoreVersion,
|
||||
};
|
||||
}
|
||||
|
||||
/** Set the App Clip App Store review detail (invocation URLs). */
|
||||
public setAppClipReviewDetail(reviewDetail: AppleAppClipReviewDetail | null): void {
|
||||
this.schema.appClip = this.schema.appClip ?? {};
|
||||
this.schema.appClip.defaultExperience = this.schema.appClip.defaultExperience ?? {};
|
||||
if (reviewDetail && reviewDetail.invocationUrls.length > 0) {
|
||||
this.schema.appClip.defaultExperience.reviewDetail = reviewDetail;
|
||||
} else {
|
||||
delete this.schema.appClip.defaultExperience.reviewDetail;
|
||||
}
|
||||
}
|
||||
|
||||
/** Set per-locale App Clip info (subtitle + header image). */
|
||||
public setAppClipLocalizedInfo(locale: string, info: AppleAppClipLocalizedInfo): void {
|
||||
this.schema.appClip = this.schema.appClip ?? {};
|
||||
this.schema.appClip.defaultExperience = this.schema.appClip.defaultExperience ?? {};
|
||||
this.schema.appClip.defaultExperience.info = this.schema.appClip.defaultExperience.info ?? {};
|
||||
if (info.subtitle || info.headerImage) {
|
||||
this.schema.appClip.defaultExperience.info[locale] = {
|
||||
subtitle: optional(info.subtitle ?? null),
|
||||
headerImage: optional(info.headerImage ?? null),
|
||||
};
|
||||
} else {
|
||||
delete this.schema.appClip.defaultExperience.info[locale];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { App } from '@expo/apple-utils';
|
||||
|
||||
import type { AgeRatingData } from './tasks/age-rating';
|
||||
import type { AppClipData } from './tasks/app-clip';
|
||||
import type { AppInfoData } from './tasks/app-info';
|
||||
import type { AppReviewData } from './tasks/app-review-detail';
|
||||
import type { AppVersionData } from './tasks/app-version';
|
||||
@@ -16,7 +17,8 @@ export type AppleData = { app: App; projectDir: string } & AppInfoData &
|
||||
AgeRatingData &
|
||||
AppReviewData &
|
||||
ScreenshotsData &
|
||||
PreviewsData;
|
||||
PreviewsData &
|
||||
AppClipData;
|
||||
|
||||
/**
|
||||
* The unprepared partial apple data, used within the `prepareAsync` tasks.
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
import {
|
||||
App,
|
||||
AppClip,
|
||||
AppClipAppStoreReviewDetail,
|
||||
AppClipDefaultExperience,
|
||||
AppClipDefaultExperienceLocalization,
|
||||
AppStoreVersion,
|
||||
} from '@expo/apple-utils';
|
||||
import nock from 'nock';
|
||||
|
||||
import { requestContext } from './fixtures/requestContext';
|
||||
import { AppleConfigReader } from '../../config/reader';
|
||||
import { AppleConfigWriter } from '../../config/writer';
|
||||
import { AppleData, PartialAppleData } from '../../data';
|
||||
import { AppClipTask } from '../app-clip';
|
||||
|
||||
jest.mock('../../../../ora');
|
||||
jest.mock('../../config/writer');
|
||||
|
||||
function makeExperience(
|
||||
id: string,
|
||||
attrs: Partial<AppClipDefaultExperience['attributes']> = {}
|
||||
): AppClipDefaultExperience {
|
||||
return new AppClipDefaultExperience(requestContext, id, {
|
||||
action: null,
|
||||
...attrs,
|
||||
} as any);
|
||||
}
|
||||
|
||||
function makeLocalization(
|
||||
id: string,
|
||||
locale: string,
|
||||
subtitle: string | null = null
|
||||
): AppClipDefaultExperienceLocalization {
|
||||
return new AppClipDefaultExperienceLocalization(requestContext, id, {
|
||||
locale,
|
||||
subtitle,
|
||||
} as any);
|
||||
}
|
||||
|
||||
function makeVersion(id: string): AppStoreVersion {
|
||||
return new AppStoreVersion(requestContext, id, { versionString: '1.0.0' } as any);
|
||||
}
|
||||
|
||||
const APP_CLIPS_EMPTY_RESPONSE = {
|
||||
data: [],
|
||||
links: { self: 'https://api.appstoreconnect.apple.com/v1/apps/stub-id/appClips' },
|
||||
meta: { paging: { total: 0, limit: 50 } },
|
||||
};
|
||||
|
||||
describe(AppClipTask, () => {
|
||||
describe('prepareAsync', () => {
|
||||
it('initializes empty state when the app has no app clips', async () => {
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.get(uri => uri.startsWith(`/v1/${App.type}/stub-id/${AppClip.type}`))
|
||||
.reply(200, APP_CLIPS_EMPTY_RESPONSE);
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
};
|
||||
|
||||
await new AppClipTask().prepareAsync({ context });
|
||||
|
||||
expect(context.appClip).toBeNull();
|
||||
expect(context.appClipDefaultExperience).toBeNull();
|
||||
expect(context.appClipLocalizations?.size).toBe(0);
|
||||
expect(context.appClipHeaderImages?.size).toBe(0);
|
||||
expect(context.appClipReviewDetail).toBeNull();
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadAsync', () => {
|
||||
it('skips when no default experience is loaded', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
await new AppClipTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
appClip: null,
|
||||
appClipDefaultExperience: null,
|
||||
appClipTemplateExperience: null,
|
||||
appClipLocalizations: new Map(),
|
||||
appClipHeaderImages: new Map(),
|
||||
appClipReviewDetail: null,
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setAppClipDefaultExperience).not.toBeCalled();
|
||||
expect(writer.setAppClipLocalizedInfo).not.toBeCalled();
|
||||
expect(writer.setAppClipReviewDetail).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('writes from current version experience when present', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
const experience = makeExperience('exp-current', { action: 'PLAY' as any });
|
||||
const localization = makeLocalization('loc-1', 'en-US', 'Quick play');
|
||||
|
||||
await new AppClipTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
appClip: new AppClip(requestContext, 'clip-1', { bundleId: 'com.x.Clip' }),
|
||||
appClipDefaultExperience: experience,
|
||||
appClipTemplateExperience: null,
|
||||
appClipLocalizations: new Map([['en-US', localization]]),
|
||||
appClipHeaderImages: new Map(),
|
||||
appClipReviewDetail: null,
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setAppClipDefaultExperience).toBeCalledWith(
|
||||
expect.objectContaining({ action: 'PLAY' })
|
||||
);
|
||||
expect(writer.setAppClipLocalizedInfo).toBeCalledWith(
|
||||
'en-US',
|
||||
expect.objectContaining({ subtitle: 'Quick play' })
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to template experience when current version has none', async () => {
|
||||
// Regression test: when bumping the app version, the new version has no
|
||||
// default experience yet. Pull should still populate config from the
|
||||
// most recent prior experience (the template) so the user has data to
|
||||
// edit, instead of writing nothing.
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
const template = makeExperience('exp-template', { action: 'OPEN' as any });
|
||||
const localization = makeLocalization('loc-1', 'en-US', 'Old subtitle');
|
||||
|
||||
await new AppClipTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
appClip: new AppClip(requestContext, 'clip-1', { bundleId: 'com.x.Clip' }),
|
||||
appClipDefaultExperience: null,
|
||||
appClipTemplateExperience: template,
|
||||
appClipLocalizations: new Map([['en-US', localization]]),
|
||||
appClipHeaderImages: new Map(),
|
||||
appClipReviewDetail: null,
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setAppClipDefaultExperience).toBeCalledWith(
|
||||
expect.objectContaining({ action: 'OPEN' })
|
||||
);
|
||||
expect(writer.setAppClipLocalizedInfo).toBeCalledWith(
|
||||
'en-US',
|
||||
expect.objectContaining({ subtitle: 'Old subtitle' })
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves expected local path when header image is not yet downloadable', async () => {
|
||||
// Regression test for the App Clip header image race condition: ASC
|
||||
// hasn't yet rendered the imageAsset (typically right after a fresh
|
||||
// upload), so the download URL is null. We should still preserve the
|
||||
// headerImage path in config so subsequent pushes don't try to delete
|
||||
// the in-progress upload.
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
const experience = makeExperience('exp-1', { action: 'OPEN' as any });
|
||||
const localization = makeLocalization('loc-1', 'en-US', 'Sub');
|
||||
// Header image with no rendered imageAsset (still processing).
|
||||
const headerImage = {
|
||||
attributes: { fileName: 'header.png', imageAsset: null },
|
||||
getImageAssetUrl: () => null,
|
||||
} as any;
|
||||
|
||||
await new AppClipTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
appClip: new AppClip(requestContext, 'clip-1', { bundleId: 'com.x.Clip' }),
|
||||
appClipDefaultExperience: experience,
|
||||
appClipTemplateExperience: null,
|
||||
appClipLocalizations: new Map([['en-US', localization]]),
|
||||
appClipHeaderImages: new Map([['en-US', headerImage]]),
|
||||
appClipReviewDetail: null,
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setAppClipLocalizedInfo).toBeCalledWith(
|
||||
'en-US',
|
||||
expect.objectContaining({
|
||||
subtitle: 'Sub',
|
||||
headerImage: 'store/apple/app-clip/en-US/header.png',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadAsync', () => {
|
||||
it('skips when no app clip is configured in store config', async () => {
|
||||
const reader = new AppleConfigReader({});
|
||||
|
||||
await new AppClipTask().uploadAsync({
|
||||
config: reader,
|
||||
context: {
|
||||
appClip: null,
|
||||
appClipDefaultExperience: null,
|
||||
appClipTemplateExperience: null,
|
||||
appClipLocalizations: new Map(),
|
||||
appClipHeaderImages: new Map(),
|
||||
appClipReviewDetail: null,
|
||||
} as AppleData,
|
||||
});
|
||||
|
||||
// No HTTP calls should be made when nothing is configured.
|
||||
expect(nock.pendingMocks()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('warns and aborts when ASC has no app clip but config does', async () => {
|
||||
const reader = new AppleConfigReader({
|
||||
appClip: {
|
||||
defaultExperience: { action: 'OPEN' },
|
||||
},
|
||||
});
|
||||
|
||||
await new AppClipTask().uploadAsync({
|
||||
config: reader,
|
||||
context: {
|
||||
appClip: null,
|
||||
appClipDefaultExperience: null,
|
||||
appClipTemplateExperience: null,
|
||||
appClipLocalizations: new Map(),
|
||||
appClipHeaderImages: new Map(),
|
||||
appClipReviewDetail: null,
|
||||
} as AppleData,
|
||||
});
|
||||
|
||||
// No HTTP calls when there's no clip in ASC to update.
|
||||
expect(nock.pendingMocks()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('warns and aborts when no editable app store version is loaded', async () => {
|
||||
const reader = new AppleConfigReader({
|
||||
appClip: { defaultExperience: { action: 'OPEN' } },
|
||||
});
|
||||
|
||||
await new AppClipTask().uploadAsync({
|
||||
config: reader,
|
||||
context: {
|
||||
appClip: new AppClip(requestContext, 'clip-1', { bundleId: 'com.x.Clip' }),
|
||||
version: null,
|
||||
appClipDefaultExperience: null,
|
||||
appClipTemplateExperience: null,
|
||||
appClipLocalizations: new Map(),
|
||||
appClipHeaderImages: new Map(),
|
||||
appClipReviewDetail: null,
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(nock.pendingMocks()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('skips PATCH when default experience action already matches config', async () => {
|
||||
// Regression test: Apple rejects PATCHes on `action` once the linked
|
||||
// version is locked (in review/released), so we must skip the call when
|
||||
// nothing changed to avoid spurious failures on round-trip pushes.
|
||||
const reader = new AppleConfigReader({
|
||||
appClip: { defaultExperience: { action: 'PLAY' } },
|
||||
});
|
||||
const existing = makeExperience('exp-current', { action: 'PLAY' as any });
|
||||
// Spy on updateAsync to assert it's not invoked.
|
||||
const updateSpy = jest.spyOn(existing, 'updateAsync');
|
||||
|
||||
await new AppClipTask().uploadAsync({
|
||||
config: reader,
|
||||
context: {
|
||||
app: new App(requestContext, 'app-1', {} as any),
|
||||
appClip: new AppClip(requestContext, 'clip-1', { bundleId: 'com.x.Clip' }),
|
||||
version: makeVersion('ver-1'),
|
||||
appClipDefaultExperience: existing,
|
||||
appClipTemplateExperience: null,
|
||||
appClipLocalizations: new Map(),
|
||||
appClipHeaderImages: new Map(),
|
||||
appClipReviewDetail: null,
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(updateSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('skips review detail PATCH when invocation URLs already match', async () => {
|
||||
const reader = new AppleConfigReader({
|
||||
appClip: {
|
||||
defaultExperience: {
|
||||
action: 'OPEN',
|
||||
reviewDetail: { invocationUrls: ['https://a.example/', 'https://b.example/'] },
|
||||
},
|
||||
},
|
||||
});
|
||||
const existing = makeExperience('exp-current', { action: 'OPEN' as any });
|
||||
const review = new AppClipAppStoreReviewDetail(requestContext, 'rev-1', {
|
||||
invocationUrls: ['https://a.example/', 'https://b.example/'],
|
||||
} as any);
|
||||
const updateSpy = jest.spyOn(review, 'updateAsync');
|
||||
|
||||
await new AppClipTask().uploadAsync({
|
||||
config: reader,
|
||||
context: {
|
||||
app: new App(requestContext, 'app-1', {} as any),
|
||||
appClip: new AppClip(requestContext, 'clip-1', { bundleId: 'com.x.Clip' }),
|
||||
version: makeVersion('ver-1'),
|
||||
appClipDefaultExperience: existing,
|
||||
appClipTemplateExperience: null,
|
||||
appClipLocalizations: new Map(),
|
||||
appClipHeaderImages: new Map(),
|
||||
appClipReviewDetail: review,
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(updateSpy).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -219,6 +219,48 @@ describe(PreviewsTask, () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves entries with placeholder paths when videoUrl is null (broken state)', async () => {
|
||||
// Same regression as for screenshots: previews stuck in AWAITING_UPLOAD
|
||||
// with no rendered videoUrl used to be dropped from config. Now we
|
||||
// preserve the entry so the user can recover.
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
const broken = new AppPreview(requestContext, 'PV_BROKEN', {
|
||||
fileName: 'demo.mp4',
|
||||
fileSize: 12345,
|
||||
videoUrl: null,
|
||||
assetDeliveryState: { state: 'AWAITING_UPLOAD', errors: [], warnings: [] },
|
||||
} as any);
|
||||
jest.spyOn(broken, 'getVideoUrl').mockReturnValue(null);
|
||||
|
||||
const previewTypeMap = new Map<PreviewType, AppPreviewSet>();
|
||||
previewTypeMap.set(
|
||||
PreviewType.IPHONE_67,
|
||||
new AppPreviewSet(requestContext, 'PSET_1', {
|
||||
previewType: PreviewType.IPHONE_67,
|
||||
appPreviews: [broken],
|
||||
} as any)
|
||||
);
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
await new PreviewsTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
previewSets: new Map([['en-US', previewTypeMap]]),
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(mockFetch).not.toBeCalled();
|
||||
expect(writer.setPreviews).toBeCalledWith('en-US', {
|
||||
[PreviewType.IPHONE_67]: 'store/apple/preview/en-US/IPHONE_67/demo.mp4',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadAsync', () => {
|
||||
@@ -312,6 +354,17 @@ describe(PreviewsTask, () => {
|
||||
// Mock AppPreview.uploadAsync to avoid real file access
|
||||
jest.spyOn(AppPreview, 'uploadAsync').mockResolvedValue(newPreview);
|
||||
|
||||
// Mock AppPreviewSet.infoAsync for the reorder step
|
||||
jest.spyOn(AppPreviewSet, 'infoAsync').mockResolvedValue(
|
||||
new AppPreviewSet(requestContext, 'NEW_PSET_1', {
|
||||
previewType: PreviewType.IPHONE_67,
|
||||
appPreviews: [newPreview],
|
||||
} as any)
|
||||
);
|
||||
|
||||
const reorderMock = jest.fn().mockResolvedValue([]);
|
||||
AppPreviewSet.prototype.reorderPreviewsAsync = reorderMock;
|
||||
|
||||
const previewSets = new Map();
|
||||
previewSets.set('en-US', new Map());
|
||||
|
||||
@@ -333,6 +386,214 @@ describe(PreviewsTask, () => {
|
||||
waitForProcessing: true,
|
||||
})
|
||||
);
|
||||
// Single preview, current order matches: reorder should be skipped.
|
||||
expect(reorderMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uploads multiple previews when configured as an array', async () => {
|
||||
const config = new AppleConfigReader({
|
||||
info: {
|
||||
'en-US': {
|
||||
title: 'My App',
|
||||
previews: {
|
||||
IPHONE_67: [
|
||||
'./previews/intro.mp4',
|
||||
{ path: './previews/features.mp4', previewFrameTimeCode: '00:02:00' },
|
||||
'./previews/outro.mp4',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
const intro = new AppPreview(requestContext, 'NEW_PV_INTRO', {
|
||||
fileName: 'intro.mp4',
|
||||
fileSize: 10240,
|
||||
previewFrameTimeCode: null,
|
||||
assetDeliveryState: { state: 'COMPLETE', errors: [], warnings: [] },
|
||||
} as any);
|
||||
const features = new AppPreview(requestContext, 'NEW_PV_FEATURES', {
|
||||
fileName: 'features.mp4',
|
||||
fileSize: 10240,
|
||||
previewFrameTimeCode: '00:02:00',
|
||||
assetDeliveryState: { state: 'COMPLETE', errors: [], warnings: [] },
|
||||
} as any);
|
||||
const outro = new AppPreview(requestContext, 'NEW_PV_OUTRO', {
|
||||
fileName: 'outro.mp4',
|
||||
fileSize: 10240,
|
||||
previewFrameTimeCode: null,
|
||||
assetDeliveryState: { state: 'COMPLETE', errors: [], warnings: [] },
|
||||
} as any);
|
||||
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.post(`/v1/${AppPreviewSet.type}`)
|
||||
.reply(201, {
|
||||
data: {
|
||||
id: 'NEW_PSET_1',
|
||||
type: AppPreviewSet.type,
|
||||
attributes: {
|
||||
previewType: PreviewType.IPHONE_67,
|
||||
appPreviews: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const uploadSpy = jest
|
||||
.spyOn(AppPreview, 'uploadAsync')
|
||||
.mockResolvedValueOnce(intro)
|
||||
.mockResolvedValueOnce(features)
|
||||
.mockResolvedValueOnce(outro);
|
||||
|
||||
// After upload Apple returns the previews in upload order, which already matches config.
|
||||
jest.spyOn(AppPreviewSet, 'infoAsync').mockResolvedValue(
|
||||
new AppPreviewSet(requestContext, 'NEW_PSET_1', {
|
||||
previewType: PreviewType.IPHONE_67,
|
||||
appPreviews: [intro, features, outro],
|
||||
} as any)
|
||||
);
|
||||
|
||||
const reorderMock = jest.fn().mockResolvedValue([]);
|
||||
AppPreviewSet.prototype.reorderPreviewsAsync = reorderMock;
|
||||
|
||||
const previewSets = new Map();
|
||||
previewSets.set('en-US', new Map());
|
||||
|
||||
await new PreviewsTask().uploadAsync({
|
||||
config,
|
||||
context: {
|
||||
previewSets,
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
expect(uploadSpy).toHaveBeenCalledTimes(3);
|
||||
expect(uploadSpy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
requestContext,
|
||||
expect.objectContaining({ filePath: '/test/project/previews/intro.mp4' })
|
||||
);
|
||||
expect(uploadSpy).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
requestContext,
|
||||
expect.objectContaining({
|
||||
filePath: '/test/project/previews/features.mp4',
|
||||
previewFrameTimeCode: '00:02:00',
|
||||
})
|
||||
);
|
||||
expect(uploadSpy).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
requestContext,
|
||||
expect.objectContaining({ filePath: '/test/project/previews/outro.mp4' })
|
||||
);
|
||||
// Order already matches config order, no reorder call expected.
|
||||
expect(reorderMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reorders previews when ASC order differs from config order', async () => {
|
||||
const config = new AppleConfigReader({
|
||||
info: {
|
||||
'en-US': {
|
||||
title: 'My App',
|
||||
previews: {
|
||||
IPHONE_67: ['./previews/a.mp4', './previews/b.mp4'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
// Both previews already exist (matching filename + size), so no upload happens.
|
||||
const previewA = new AppPreview(requestContext, 'PV_A', {
|
||||
fileName: 'a.mp4',
|
||||
fileSize: 10240,
|
||||
previewFrameTimeCode: null,
|
||||
assetDeliveryState: { state: 'COMPLETE', errors: [], warnings: [] },
|
||||
} as any);
|
||||
const previewB = new AppPreview(requestContext, 'PV_B', {
|
||||
fileName: 'b.mp4',
|
||||
fileSize: 10240,
|
||||
previewFrameTimeCode: null,
|
||||
assetDeliveryState: { state: 'COMPLETE', errors: [], warnings: [] },
|
||||
} as any);
|
||||
|
||||
const previewTypeMap = new Map<PreviewType, AppPreviewSet>();
|
||||
// ASC reports them in [b, a] order — config wants [a, b].
|
||||
previewTypeMap.set(
|
||||
PreviewType.IPHONE_67,
|
||||
new AppPreviewSet(requestContext, 'PSET_1', {
|
||||
previewType: PreviewType.IPHONE_67,
|
||||
appPreviews: [previewB, previewA],
|
||||
} as any)
|
||||
);
|
||||
|
||||
jest.spyOn(AppPreview, 'uploadAsync').mockResolvedValue(previewA);
|
||||
|
||||
jest.spyOn(AppPreviewSet, 'infoAsync').mockResolvedValue(
|
||||
new AppPreviewSet(requestContext, 'PSET_1', {
|
||||
previewType: PreviewType.IPHONE_67,
|
||||
appPreviews: [previewB, previewA],
|
||||
} as any)
|
||||
);
|
||||
|
||||
const reorderMock = jest.fn().mockResolvedValue([]);
|
||||
AppPreviewSet.prototype.reorderPreviewsAsync = reorderMock;
|
||||
|
||||
const previewSets = new Map();
|
||||
previewSets.set('en-US', previewTypeMap);
|
||||
|
||||
await new PreviewsTask().uploadAsync({
|
||||
config,
|
||||
context: {
|
||||
previewSets,
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(AppPreview.uploadAsync).not.toHaveBeenCalled();
|
||||
expect(reorderMock).toHaveBeenCalledWith({ appPreviews: ['PV_A', 'PV_B'] });
|
||||
});
|
||||
|
||||
it('skips a preview type whose array is empty', async () => {
|
||||
const config = new AppleConfigReader({
|
||||
info: {
|
||||
'en-US': {
|
||||
title: 'My App',
|
||||
previews: {
|
||||
IPHONE_67: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
jest.spyOn(AppPreview, 'uploadAsync');
|
||||
|
||||
const previewSets = new Map();
|
||||
previewSets.set('en-US', new Map());
|
||||
|
||||
await new PreviewsTask().uploadAsync({
|
||||
config,
|
||||
context: {
|
||||
previewSets,
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(AppPreview.uploadAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uploads new preview with object config including previewFrameTimeCode', async () => {
|
||||
@@ -378,6 +639,16 @@ describe(PreviewsTask, () => {
|
||||
// Mock AppPreview.uploadAsync to avoid real file access
|
||||
jest.spyOn(AppPreview, 'uploadAsync').mockResolvedValue(newPreview);
|
||||
|
||||
jest.spyOn(AppPreviewSet, 'infoAsync').mockResolvedValue(
|
||||
new AppPreviewSet(requestContext, 'NEW_PSET_1', {
|
||||
previewType: PreviewType.IPHONE_67,
|
||||
appPreviews: [newPreview],
|
||||
} as any)
|
||||
);
|
||||
|
||||
const reorderMock = jest.fn().mockResolvedValue([]);
|
||||
AppPreviewSet.prototype.reorderPreviewsAsync = reorderMock;
|
||||
|
||||
const previewSets = new Map();
|
||||
previewSets.set('en-US', new Map());
|
||||
|
||||
@@ -402,4 +673,67 @@ describe(PreviewsTask, () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadAsync (multi-preview)', () => {
|
||||
it('emits an array when there are multiple previews in a set', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
const previews = [
|
||||
new AppPreview(requestContext, 'PV_1', {
|
||||
fileName: 'intro.mp4',
|
||||
fileSize: 10240,
|
||||
previewFrameTimeCode: null,
|
||||
videoUrl: 'https://example.com/intro.mp4',
|
||||
assetDeliveryState: { state: 'COMPLETE', errors: [], warnings: [] },
|
||||
} as any),
|
||||
new AppPreview(requestContext, 'PV_2', {
|
||||
fileName: 'features.mp4',
|
||||
fileSize: 10240,
|
||||
previewFrameTimeCode: '00:01:00',
|
||||
videoUrl: 'https://example.com/features.mp4',
|
||||
assetDeliveryState: { state: 'COMPLETE', errors: [], warnings: [] },
|
||||
} as any),
|
||||
];
|
||||
|
||||
jest.spyOn(previews[0], 'getVideoUrl').mockReturnValue('https://example.com/intro.mp4');
|
||||
jest.spyOn(previews[1], 'getVideoUrl').mockReturnValue('https://example.com/features.mp4');
|
||||
|
||||
const previewTypeMap = new Map<PreviewType, AppPreviewSet>();
|
||||
previewTypeMap.set(
|
||||
PreviewType.IPHONE_67,
|
||||
new AppPreviewSet(requestContext, 'PSET_1', {
|
||||
previewType: PreviewType.IPHONE_67,
|
||||
appPreviews: previews,
|
||||
} as any)
|
||||
);
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
buffer: () => Promise.resolve(Buffer.from('fake-video-data')),
|
||||
});
|
||||
|
||||
await new PreviewsTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
previewSets: new Map([['en-US', previewTypeMap]]),
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setPreviews).toBeCalledWith('en-US', {
|
||||
[PreviewType.IPHONE_67]: [
|
||||
'store/apple/preview/en-US/IPHONE_67/intro.mp4',
|
||||
{
|
||||
path: 'store/apple/preview/en-US/IPHONE_67/features.mp4',
|
||||
previewFrameTimeCode: '00:01:00',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -170,6 +170,95 @@ describe(ScreenshotsTask, () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves entries with placeholder paths when imageAsset is null (broken state)', async () => {
|
||||
// Regression test for screenshots stuck in AWAITING_UPLOAD with no
|
||||
// rendered imageAsset. Pull used to drop these from config entirely,
|
||||
// which made it impossible to recover via push (since push only acts on
|
||||
// entries present in config). Now pull writes a placeholder path so the
|
||||
// user can drop in a replacement file or remove the entry to delete the
|
||||
// broken ASC record.
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
const broken = new AppScreenshot(requestContext, 'SS_BROKEN', {
|
||||
fileName: '01.png',
|
||||
fileSize: 599307,
|
||||
imageAsset: null,
|
||||
assetDeliveryState: { state: 'AWAITING_UPLOAD', errors: [], warnings: [] },
|
||||
} as any);
|
||||
// getImageAssetUrl returns null when imageAsset is null.
|
||||
jest.spyOn(broken, 'getImageAssetUrl').mockReturnValue(null);
|
||||
|
||||
const displayTypeMap = new Map<ScreenshotDisplayType, AppScreenshotSet>();
|
||||
const screenshotSet = new AppScreenshotSet(requestContext, 'SET_1', {
|
||||
screenshotDisplayType: ScreenshotDisplayType.APP_IPHONE_67,
|
||||
appScreenshots: [broken],
|
||||
} as any);
|
||||
displayTypeMap.set(ScreenshotDisplayType.APP_IPHONE_67, screenshotSet);
|
||||
|
||||
const screenshotSets = new Map([['en-US', displayTypeMap]]);
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
await new ScreenshotsTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
screenshotSets,
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
// No fetch should have been attempted (URL was null).
|
||||
expect(mockFetch).not.toBeCalled();
|
||||
// Entry should still be present in config with the original filename.
|
||||
expect(writer.setScreenshots).toBeCalledWith('en-US', {
|
||||
[ScreenshotDisplayType.APP_IPHONE_67]: [
|
||||
'store/apple/screenshot/en-US/APP_IPHONE_67/01.png',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('uses index-based fallback filename when fileName is also null', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
const broken = new AppScreenshot(requestContext, 'SS_BROKEN', {
|
||||
fileName: null,
|
||||
fileSize: 0,
|
||||
imageAsset: null,
|
||||
assetDeliveryState: { state: 'AWAITING_UPLOAD', errors: [], warnings: [] },
|
||||
} as any);
|
||||
jest.spyOn(broken, 'getImageAssetUrl').mockReturnValue(null);
|
||||
|
||||
const displayTypeMap = new Map<ScreenshotDisplayType, AppScreenshotSet>();
|
||||
displayTypeMap.set(
|
||||
ScreenshotDisplayType.APP_IPAD_PRO_3GEN_129,
|
||||
new AppScreenshotSet(requestContext, 'SET_2', {
|
||||
screenshotDisplayType: ScreenshotDisplayType.APP_IPAD_PRO_3GEN_129,
|
||||
appScreenshots: [broken],
|
||||
} as any)
|
||||
);
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
await new ScreenshotsTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
screenshotSets: new Map([['en-US', displayTypeMap]]),
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setScreenshots).toBeCalledWith('en-US', {
|
||||
[ScreenshotDisplayType.APP_IPAD_PRO_3GEN_129]: [
|
||||
'store/apple/screenshot/en-US/APP_IPAD_PRO_3GEN_129/01.png',
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadAsync', () => {
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
import {
|
||||
AppClip,
|
||||
AppClipAppStoreReviewDetail,
|
||||
AppClipDefaultExperience,
|
||||
AppClipDefaultExperienceLocalization,
|
||||
AppClipHeaderImage,
|
||||
} from '@expo/apple-utils';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import fetch from '../../../fetch';
|
||||
import Log from '../../../log';
|
||||
import { logAsync } from '../../utils/log';
|
||||
import { AppleTask, TaskDownloadOptions, TaskPrepareOptions, TaskUploadOptions } from '../task';
|
||||
|
||||
export type AppClipData = {
|
||||
/** The App Clip resource for the current app, if one exists. */
|
||||
appClip: AppClip | null;
|
||||
/**
|
||||
* The default experience attached to the *current editable* app store
|
||||
* version, if one exists. App Clip default experiences are versioned per
|
||||
* app store version, so this is null when the user has bumped the version
|
||||
* but not yet pushed an App Clip configuration for it.
|
||||
*/
|
||||
appClipDefaultExperience: AppClipDefaultExperience | null;
|
||||
/**
|
||||
* The most recent default experience from any other version, used as a
|
||||
* template when creating a new default experience for the current version
|
||||
* (and as the source for `metadata:pull` when the current version has none).
|
||||
*/
|
||||
appClipTemplateExperience: AppClipDefaultExperience | null;
|
||||
/** Existing localizations keyed by locale (e.g. `en-US`). */
|
||||
appClipLocalizations: Map<string, AppClipDefaultExperienceLocalization>;
|
||||
/** Existing header images keyed by locale. */
|
||||
appClipHeaderImages: Map<string, AppClipHeaderImage>;
|
||||
/** App Store review detail (invocation URLs), if one exists. */
|
||||
appClipReviewDetail: AppClipAppStoreReviewDetail | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Task for managing App Clip metadata (default experience, localized
|
||||
* subtitles + header images, App Store review invocation URLs).
|
||||
*
|
||||
* No-op when the app does not have an App Clip target.
|
||||
*/
|
||||
export class AppClipTask extends AppleTask {
|
||||
public name = (): string => 'app clip';
|
||||
|
||||
public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
|
||||
context.appClip = null;
|
||||
context.appClipDefaultExperience = null;
|
||||
context.appClipTemplateExperience = null;
|
||||
context.appClipLocalizations = new Map();
|
||||
context.appClipHeaderImages = new Map();
|
||||
context.appClipReviewDetail = null;
|
||||
|
||||
const appClips = await context.app.getAppClipsAsync();
|
||||
if (appClips.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apps may have multiple App Clip bundles registered. Pick the first
|
||||
// clip that has any default experiences attached.
|
||||
let chosenClip: AppClip | null = null;
|
||||
let allExperiences: AppClipDefaultExperience[] = [];
|
||||
for (const clip of appClips) {
|
||||
const experiences = await clip.getAppClipDefaultExperiencesAsync();
|
||||
if (experiences.length > 0) {
|
||||
chosenClip = clip;
|
||||
allExperiences = experiences;
|
||||
break;
|
||||
}
|
||||
}
|
||||
chosenClip ??= appClips[0];
|
||||
context.appClip = chosenClip;
|
||||
|
||||
if (allExperiences.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// App Clip default experiences are versioned per app store version. Find
|
||||
// the experience linked to the current editable version. Fall back to the
|
||||
// first experience as a template (used as the source of truth on pull
|
||||
// when the current version has no experience yet, and as a template when
|
||||
// creating a new experience on push).
|
||||
const currentVersionId = context.version?.id;
|
||||
const currentVersionExperience = currentVersionId
|
||||
? (allExperiences.find(
|
||||
exp => exp.attributes.releaseWithAppStoreVersion?.id === currentVersionId
|
||||
) ?? null)
|
||||
: null;
|
||||
|
||||
context.appClipDefaultExperience = currentVersionExperience;
|
||||
// The template is only used when we need to CREATE a new default
|
||||
// experience for the current version on push — pick any other existing
|
||||
// experience (most recent first by sort order). When the current version
|
||||
// already has an experience, no template is needed.
|
||||
context.appClipTemplateExperience = currentVersionExperience
|
||||
? null
|
||||
: (allExperiences[0] ?? null);
|
||||
|
||||
// Use the current version's experience for downstream data when present;
|
||||
// otherwise fall back to the template so `metadata:pull` still produces a
|
||||
// populated `store.config.json`.
|
||||
const sourceExperience = currentVersionExperience ?? context.appClipTemplateExperience;
|
||||
if (!sourceExperience) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [localizations, reviewDetail] = await Promise.all([
|
||||
sourceExperience.getAppClipDefaultExperienceLocalizationsAsync(),
|
||||
sourceExperience.getAppClipAppStoreReviewDetailAsync(),
|
||||
]);
|
||||
|
||||
for (const localization of localizations) {
|
||||
context.appClipLocalizations.set(localization.attributes.locale, localization);
|
||||
}
|
||||
context.appClipReviewDetail = reviewDetail;
|
||||
|
||||
// Header images may be missing from the included payload depending on
|
||||
// ASC's whims; fetch any that weren't pre-populated.
|
||||
await Promise.all(
|
||||
localizations.map(async localization => {
|
||||
const included = localization.attributes.appClipHeaderImage;
|
||||
if (included) {
|
||||
context.appClipHeaderImages!.set(localization.attributes.locale, included);
|
||||
return;
|
||||
}
|
||||
const headerImage = await localization.getAppClipHeaderImageAsync();
|
||||
if (headerImage) {
|
||||
context.appClipHeaderImages!.set(localization.attributes.locale, headerImage);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
|
||||
// Pull data from the current version's experience when present, otherwise
|
||||
// fall back to the template experience. The localizations and review
|
||||
// detail in `context` were populated from whichever was used.
|
||||
const experience = context.appClipDefaultExperience ?? context.appClipTemplateExperience;
|
||||
if (!experience) {
|
||||
return;
|
||||
}
|
||||
|
||||
config.setAppClipDefaultExperience({
|
||||
action: experience.attributes.action ?? undefined,
|
||||
// ASC does not expose `releaseWithAppStoreVersion` directly on the
|
||||
// attributes — it's a relationship. We treat presence of an included
|
||||
// version as `true`. When unset we leave the field undefined so the
|
||||
// serializer omits it.
|
||||
releaseWithAppStoreVersion:
|
||||
experience.attributes.releaseWithAppStoreVersion != null ? true : undefined,
|
||||
});
|
||||
|
||||
const reviewDetail = context.appClipReviewDetail;
|
||||
if (reviewDetail?.attributes.invocationUrls?.length) {
|
||||
config.setAppClipReviewDetail({
|
||||
invocationUrls: reviewDetail.attributes.invocationUrls,
|
||||
});
|
||||
} else {
|
||||
config.setAppClipReviewDetail(null);
|
||||
}
|
||||
|
||||
for (const [locale, localization] of context.appClipLocalizations) {
|
||||
const headerImage = context.appClipHeaderImages.get(locale);
|
||||
let headerImagePath: string | undefined;
|
||||
if (headerImage) {
|
||||
const downloaded = await downloadAppClipHeaderImageAsync(
|
||||
context.projectDir,
|
||||
locale,
|
||||
headerImage
|
||||
);
|
||||
if (downloaded) {
|
||||
headerImagePath = downloaded;
|
||||
} else {
|
||||
// Image exists in ASC but isn't downloadable yet (still processing,
|
||||
// or Apple's CDN hasn't caught up). Preserve the expected local path
|
||||
// so subsequent pushes don't try to delete the in-progress upload.
|
||||
// Filename match in syncAppClipHeaderImageAsync will skip re-upload.
|
||||
const fileName = headerImage.attributes.fileName || 'header.png';
|
||||
headerImagePath = path.join('store', 'apple', 'app-clip', locale, fileName);
|
||||
}
|
||||
}
|
||||
config.setAppClipLocalizedInfo(locale, {
|
||||
subtitle: localization.attributes.subtitle ?? undefined,
|
||||
headerImage: headerImagePath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async uploadAsync({ config, context }: TaskUploadOptions): Promise<void> {
|
||||
const desired = config.getAppClipDefaultExperience();
|
||||
if (!desired) {
|
||||
Log.log(chalk`{dim - Skipped app clip, not configured}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.appClip) {
|
||||
Log.warn(
|
||||
chalk`{yellow Skipping app clip - no App Clip is registered for this app in App Store Connect}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.version) {
|
||||
Log.warn(chalk`{yellow Skipping app clip - no editable app store version available}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// App Clip default experiences are versioned per app store version. If
|
||||
// the current editable version doesn't have one yet, create a new one,
|
||||
// optionally cloning the most recent prior experience as a template.
|
||||
let experience = context.appClipDefaultExperience;
|
||||
// We always link the default experience to the current editable version,
|
||||
// regardless of whether the user opted into `releaseWithAppStoreVersion`
|
||||
// — every default experience must belong to a version in ASC.
|
||||
const releaseWithAppStoreVersionId = context.version.id;
|
||||
|
||||
if (!experience) {
|
||||
const appClipId = context.appClip.id;
|
||||
const templateId = context.appClipTemplateExperience?.id;
|
||||
experience = await logAsync(
|
||||
() =>
|
||||
AppClipDefaultExperience.createAsync(context.app.context, {
|
||||
appClipId,
|
||||
releaseWithAppStoreVersionId,
|
||||
appClipDefaultExperienceTemplateId: templateId,
|
||||
attributes: { action: (desired.action as any) ?? null },
|
||||
}),
|
||||
{
|
||||
pending: templateId
|
||||
? `Creating App Clip default experience for ${chalk.bold(context.version.attributes.versionString)} (cloned from previous version)...`
|
||||
: `Creating App Clip default experience for ${chalk.bold(context.version.attributes.versionString)}...`,
|
||||
success: `Created App Clip default experience for ${chalk.bold(context.version.attributes.versionString)}`,
|
||||
failure: `Failed creating App Clip default experience for ${chalk.bold(context.version.attributes.versionString)}`,
|
||||
}
|
||||
);
|
||||
context.appClipDefaultExperience = experience;
|
||||
|
||||
// Apple cloned the previous experience's localizations + review detail
|
||||
// into the new experience. Re-fetch them so subsequent diff/sync logic
|
||||
// operates on the new resource ids.
|
||||
const [localizations, reviewDetail] = await Promise.all([
|
||||
experience.getAppClipDefaultExperienceLocalizationsAsync(),
|
||||
experience.getAppClipAppStoreReviewDetailAsync(),
|
||||
]);
|
||||
context.appClipLocalizations = new Map();
|
||||
context.appClipHeaderImages = new Map();
|
||||
for (const localization of localizations) {
|
||||
context.appClipLocalizations.set(localization.attributes.locale, localization);
|
||||
const included = localization.attributes.appClipHeaderImage;
|
||||
if (included) {
|
||||
context.appClipHeaderImages.set(localization.attributes.locale, included);
|
||||
} else {
|
||||
const headerImage = await localization.getAppClipHeaderImageAsync();
|
||||
if (headerImage) {
|
||||
context.appClipHeaderImages.set(localization.attributes.locale, headerImage);
|
||||
}
|
||||
}
|
||||
}
|
||||
context.appClipReviewDetail = reviewDetail;
|
||||
} else {
|
||||
const currentAction = experience.attributes.action ?? null;
|
||||
const desiredAction = (desired.action as any) ?? null;
|
||||
// Apple rejects PATCHes on `action` once the linked app store version is
|
||||
// locked (in review / released). Skip the call when nothing actually
|
||||
// changed to avoid spurious failures on round-trip pushes.
|
||||
if (currentAction !== desiredAction) {
|
||||
const existing = experience;
|
||||
experience = await logAsync(
|
||||
() =>
|
||||
existing.updateAsync({
|
||||
action: desiredAction,
|
||||
releaseWithAppStoreVersionId: desired.releaseWithAppStoreVersion
|
||||
? (releaseWithAppStoreVersionId ?? null)
|
||||
: null,
|
||||
}),
|
||||
{
|
||||
pending: 'Updating App Clip default experience...',
|
||||
success: 'Updated App Clip default experience',
|
||||
failure: 'Failed updating App Clip default experience',
|
||||
}
|
||||
);
|
||||
context.appClipDefaultExperience = experience;
|
||||
} else {
|
||||
Log.log(chalk`{dim - Skipped App Clip default experience, no changes}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Sync App Store review detail (invocation URLs).
|
||||
const desiredReview = desired.reviewDetail;
|
||||
if (desiredReview && desiredReview.invocationUrls.length > 0) {
|
||||
if (context.appClipReviewDetail) {
|
||||
const currentUrls = context.appClipReviewDetail.attributes.invocationUrls ?? [];
|
||||
const desiredUrls = desiredReview.invocationUrls;
|
||||
const unchanged =
|
||||
currentUrls.length === desiredUrls.length &&
|
||||
currentUrls.every((url, i) => url === desiredUrls[i]);
|
||||
if (!unchanged) {
|
||||
const existingReview = context.appClipReviewDetail;
|
||||
context.appClipReviewDetail = await logAsync(
|
||||
() =>
|
||||
existingReview.updateAsync({
|
||||
invocationUrls: desiredUrls,
|
||||
}),
|
||||
{
|
||||
pending: 'Updating App Clip review invocation URLs...',
|
||||
success: 'Updated App Clip review invocation URLs',
|
||||
failure: 'Failed updating App Clip review invocation URLs',
|
||||
}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
context.appClipReviewDetail = await logAsync(
|
||||
() =>
|
||||
AppClipAppStoreReviewDetail.createAsync(context.app.context, {
|
||||
appClipDefaultExperienceId: experience.id,
|
||||
attributes: { invocationUrls: desiredReview.invocationUrls },
|
||||
}),
|
||||
{
|
||||
pending: 'Creating App Clip review invocation URLs...',
|
||||
success: 'Created App Clip review invocation URLs',
|
||||
failure: 'Failed creating App Clip review invocation URLs',
|
||||
}
|
||||
);
|
||||
}
|
||||
} else if (context.appClipReviewDetail) {
|
||||
await logAsync(() => context.appClipReviewDetail!.deleteAsync(), {
|
||||
pending: 'Removing App Clip review invocation URLs...',
|
||||
success: 'Removed App Clip review invocation URLs',
|
||||
failure: 'Failed removing App Clip review invocation URLs',
|
||||
});
|
||||
context.appClipReviewDetail = null;
|
||||
}
|
||||
|
||||
// Sync localizations: create/update from config, delete locales that
|
||||
// exist in ASC but were removed from the config.
|
||||
const desiredLocales = config.getAppClipLocales();
|
||||
const desiredLocaleSet = new Set(desiredLocales);
|
||||
|
||||
for (const locale of desiredLocales) {
|
||||
const desiredInfo = config.getAppClipLocalizedInfo(locale);
|
||||
if (!desiredInfo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let localization = context.appClipLocalizations.get(locale);
|
||||
if (!localization) {
|
||||
localization = await logAsync(
|
||||
() =>
|
||||
experience.createAppClipDefaultExperienceLocalizationAsync({
|
||||
locale,
|
||||
subtitle: desiredInfo.subtitle ?? null,
|
||||
}),
|
||||
{
|
||||
pending: `Creating App Clip localization for ${chalk.bold(locale)}...`,
|
||||
success: `Created App Clip localization for ${chalk.bold(locale)}`,
|
||||
failure: `Failed creating App Clip localization for ${chalk.bold(locale)}`,
|
||||
}
|
||||
);
|
||||
context.appClipLocalizations.set(locale, localization);
|
||||
} else if (localization.attributes.subtitle !== (desiredInfo.subtitle ?? null)) {
|
||||
localization = await logAsync(
|
||||
() => localization!.updateAsync({ subtitle: desiredInfo.subtitle ?? null }),
|
||||
{
|
||||
pending: `Updating App Clip localization for ${chalk.bold(locale)}...`,
|
||||
success: `Updated App Clip localization for ${chalk.bold(locale)}`,
|
||||
failure: `Failed updating App Clip localization for ${chalk.bold(locale)}`,
|
||||
}
|
||||
);
|
||||
context.appClipLocalizations.set(locale, localization);
|
||||
}
|
||||
|
||||
// Sync header image.
|
||||
await syncAppClipHeaderImageAsync({
|
||||
projectDir: context.projectDir,
|
||||
localization,
|
||||
existing: context.appClipHeaderImages.get(locale),
|
||||
desiredPath: desiredInfo.headerImage,
|
||||
onUploaded: image => context.appClipHeaderImages.set(locale, image),
|
||||
onDeleted: () => context.appClipHeaderImages.delete(locale),
|
||||
});
|
||||
}
|
||||
|
||||
// Delete localizations no longer in config.
|
||||
for (const [locale, localization] of context.appClipLocalizations) {
|
||||
if (!desiredLocaleSet.has(locale)) {
|
||||
await logAsync(() => localization.deleteAsync(), {
|
||||
pending: `Deleting App Clip localization for ${chalk.bold(locale)}...`,
|
||||
success: `Deleted App Clip localization for ${chalk.bold(locale)}`,
|
||||
failure: `Failed deleting App Clip localization for ${chalk.bold(locale)}`,
|
||||
});
|
||||
context.appClipLocalizations.delete(locale);
|
||||
context.appClipHeaderImages.delete(locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload, replace, or delete the header image for a single App Clip
|
||||
* localization based on the desired config path.
|
||||
*/
|
||||
async function syncAppClipHeaderImageAsync({
|
||||
projectDir,
|
||||
localization,
|
||||
existing,
|
||||
desiredPath,
|
||||
onUploaded,
|
||||
onDeleted,
|
||||
}: {
|
||||
projectDir: string;
|
||||
localization: AppClipDefaultExperienceLocalization;
|
||||
existing: AppClipHeaderImage | undefined;
|
||||
desiredPath: string | undefined;
|
||||
onUploaded: (image: AppClipHeaderImage) => void;
|
||||
onDeleted: () => void;
|
||||
}): Promise<void> {
|
||||
const locale = localization.attributes.locale;
|
||||
|
||||
if (!desiredPath) {
|
||||
if (existing) {
|
||||
await logAsync(() => existing.deleteAsync(), {
|
||||
pending: `Deleting App Clip header image for ${chalk.bold(locale)}...`,
|
||||
success: `Deleted App Clip header image for ${chalk.bold(locale)}`,
|
||||
failure: `Failed deleting App Clip header image for ${chalk.bold(locale)}`,
|
||||
});
|
||||
onDeleted();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const absolutePath = path.resolve(projectDir, desiredPath);
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
Log.warn(chalk`{yellow App Clip header image not found: ${absolutePath}}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = path.basename(absolutePath);
|
||||
|
||||
// Skip upload if the existing image already has the same filename and is
|
||||
// either fully processed or still being processed (i.e. anything that
|
||||
// isn't AWAITING_UPLOAD or FAILED). Apple does not expose the original
|
||||
// source bytes (only re-rendered copies), so we can't reliably compare
|
||||
// file size or checksum after a round-trip pull — filename is the only
|
||||
// stable identity available.
|
||||
if (
|
||||
existing &&
|
||||
!existing.isAwaitingUpload() &&
|
||||
!existing.isFailed() &&
|
||||
existing.attributes.fileName === fileName
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
await logAsync(() => existing.deleteAsync(), {
|
||||
pending: `Replacing App Clip header image for ${chalk.bold(locale)}...`,
|
||||
success: `Removed previous App Clip header image for ${chalk.bold(locale)}`,
|
||||
failure: `Failed removing previous App Clip header image for ${chalk.bold(locale)}`,
|
||||
});
|
||||
onDeleted();
|
||||
}
|
||||
|
||||
const uploaded = await logAsync(
|
||||
() =>
|
||||
AppClipHeaderImage.uploadAsync(localization.context, {
|
||||
id: localization.id,
|
||||
filePath: absolutePath,
|
||||
waitForProcessing: true,
|
||||
}),
|
||||
{
|
||||
pending: `Uploading App Clip header image ${chalk.bold(fileName)} (${locale})...`,
|
||||
success: `Uploaded App Clip header image ${chalk.bold(fileName)} (${locale})`,
|
||||
failure: `Failed uploading App Clip header image ${chalk.bold(fileName)} (${locale})`,
|
||||
}
|
||||
);
|
||||
onUploaded(uploaded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download an App Clip header image to the local filesystem.
|
||||
* Returns the relative path to the downloaded file, or null on failure.
|
||||
*/
|
||||
async function downloadAppClipHeaderImageAsync(
|
||||
projectDir: string,
|
||||
locale: string,
|
||||
headerImage: AppClipHeaderImage
|
||||
): Promise<string | null> {
|
||||
const imageUrl = headerImage.getImageAssetUrl({ type: 'png' });
|
||||
if (!imageUrl) {
|
||||
Log.warn(chalk`{yellow Could not get download URL for App Clip header image (${locale})}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetDir = path.join(projectDir, 'store', 'apple', 'app-clip', locale);
|
||||
await fs.promises.mkdir(targetDir, { recursive: true });
|
||||
|
||||
const fileName = headerImage.attributes.fileName || 'header.png';
|
||||
const outputPath = path.join(targetDir, fileName);
|
||||
const relativePath = path.relative(projectDir, outputPath);
|
||||
|
||||
try {
|
||||
const response = await fetch(imageUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const buffer = await response.buffer();
|
||||
await fs.promises.writeFile(outputPath, buffer);
|
||||
|
||||
Log.log(chalk`{dim Downloaded App Clip header image: ${relativePath}}`);
|
||||
return relativePath;
|
||||
} catch (error: any) {
|
||||
Log.warn(
|
||||
chalk`{yellow Failed to download App Clip header image (${locale}): ${error.message}}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AgeRatingTask } from './age-rating';
|
||||
import { AppClipTask } from './app-clip';
|
||||
import { AppInfoTask } from './app-info';
|
||||
import { AppReviewDetailTask } from './app-review-detail';
|
||||
import { AppVersionOptions, AppVersionTask } from './app-version';
|
||||
@@ -21,5 +22,6 @@ export function createAppleTasks({ version }: AppleTaskOptions = {}): AppleTask[
|
||||
new AppReviewDetailTask(),
|
||||
new ScreenshotsTask(),
|
||||
new PreviewsTask(),
|
||||
new AppClipTask(),
|
||||
];
|
||||
}
|
||||
@@ -22,19 +22,35 @@ export type PreviewsData = {
|
||||
previewSets: PreviewSetsMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize preview config to always return an object with path and optional previewFrameTimeCode.
|
||||
*/
|
||||
function normalizePreviewConfig(config: ApplePreviewConfig): {
|
||||
/** Normalized preview entry used internally by the upload pipeline. */
|
||||
type NormalizedPreviewConfig = {
|
||||
path: string;
|
||||
previewFrameTimeCode?: string;
|
||||
} {
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize a single preview config into an object form with path and optional previewFrameTimeCode.
|
||||
*/
|
||||
function normalizePreviewConfig(config: ApplePreviewConfig): NormalizedPreviewConfig {
|
||||
if (typeof config === 'string') {
|
||||
return { path: config };
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the value stored in `ApplePreviews[previewType]` (which can be a single
|
||||
* config OR an array of configs) into an array of normalized configs.
|
||||
*/
|
||||
function normalizePreviewConfigs(
|
||||
value: ApplePreviewConfig | ApplePreviewConfig[]
|
||||
): NormalizedPreviewConfig[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(normalizePreviewConfig);
|
||||
}
|
||||
return [normalizePreviewConfig(value)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Task for managing App Store video previews.
|
||||
* Downloads existing previews and uploads new ones based on store configuration.
|
||||
@@ -86,28 +102,44 @@ export class PreviewsTask extends AppleTask {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For now, we only handle the first preview per set (App Store allows up to 3)
|
||||
// We can extend this later to support multiple previews
|
||||
const preview = previewModels[0];
|
||||
const relativePath = await downloadPreviewAsync(
|
||||
context.projectDir,
|
||||
localeCode,
|
||||
previewType,
|
||||
preview,
|
||||
0
|
||||
);
|
||||
// Download all previews in this set, preserving order. Mirrors how
|
||||
// screenshots are handled: when a preview is in a broken state
|
||||
// (AWAITING_UPLOAD with no rendered videoUrl) the download will fail,
|
||||
// but we still preserve the entry pointing at its expected local path
|
||||
// so users can either drop in a replacement file or remove the entry
|
||||
// to delete the broken ASC record.
|
||||
const entries: ApplePreviewConfig[] = [];
|
||||
for (let i = 0; i < previewModels.length; i++) {
|
||||
const preview = previewModels[i];
|
||||
const downloaded = await downloadPreviewAsync(
|
||||
context.projectDir,
|
||||
localeCode,
|
||||
previewType,
|
||||
preview,
|
||||
i
|
||||
);
|
||||
|
||||
const fileName = preview.attributes.fileName || `${String(i + 1).padStart(2, '0')}.mp4`;
|
||||
const relativePath =
|
||||
downloaded || path.join('store', 'apple', 'preview', localeCode, previewType, fileName);
|
||||
|
||||
if (relativePath) {
|
||||
// Include preview frame time code if available
|
||||
if (preview.attributes.previewFrameTimeCode) {
|
||||
previews[previewType] = {
|
||||
entries.push({
|
||||
path: relativePath,
|
||||
previewFrameTimeCode: preview.attributes.previewFrameTimeCode,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
previews[previewType] = relativePath;
|
||||
entries.push(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For backwards compatibility with existing single-preview configs,
|
||||
// emit the legacy single-object form when there's exactly one entry.
|
||||
previews[previewType] = entries.length === 1 ? entries[0] : entries;
|
||||
}
|
||||
|
||||
if (Object.keys(previews).length > 0) {
|
||||
@@ -140,8 +172,13 @@ export class PreviewsTask extends AppleTask {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [previewType, previewConfig] of Object.entries(previews)) {
|
||||
if (!previewConfig) {
|
||||
for (const [previewType, previewValue] of Object.entries(previews)) {
|
||||
if (!previewValue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalized = normalizePreviewConfigs(previewValue);
|
||||
if (normalized.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -149,7 +186,7 @@ export class PreviewsTask extends AppleTask {
|
||||
context.projectDir,
|
||||
localization,
|
||||
previewType as PreviewType,
|
||||
normalizePreviewConfig(previewConfig),
|
||||
normalized,
|
||||
context.previewSets.get(localeCode)
|
||||
);
|
||||
}
|
||||
@@ -158,23 +195,20 @@ export class PreviewsTask extends AppleTask {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a preview set - upload new preview, delete old one if changed.
|
||||
* Sync a preview set against the configured previews.
|
||||
*
|
||||
* Mirrors the screenshots task: keeps unchanged previews (matched by filename
|
||||
* + size + completion state), uploads any new/changed previews, deletes
|
||||
* removed entries, and finally reorders the set to match config order.
|
||||
*/
|
||||
async function syncPreviewSetAsync(
|
||||
projectDir: string,
|
||||
localization: AppStoreVersionLocalization,
|
||||
previewType: PreviewType,
|
||||
previewConfig: { path: string; previewFrameTimeCode?: string },
|
||||
previewConfigs: NormalizedPreviewConfig[],
|
||||
existingSets: Map<PreviewType, AppPreviewSet> | undefined
|
||||
): Promise<void> {
|
||||
const locale = localization.attributes.locale;
|
||||
const absolutePath = path.resolve(projectDir, previewConfig.path);
|
||||
const fileName = path.basename(absolutePath);
|
||||
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
Log.warn(chalk`{yellow Video preview not found: ${absolutePath}}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get or create the preview set
|
||||
let previewSet = existingSets?.get(previewType);
|
||||
@@ -195,62 +229,127 @@ async function syncPreviewSetAsync(
|
||||
|
||||
const existingPreviews = previewSet.attributes.appPreviews || [];
|
||||
|
||||
// Check if we need to update (different filename, size, or no existing preview)
|
||||
const existingPreview = existingPreviews.find(p => p.attributes.fileName === fileName);
|
||||
const localSize = fs.statSync(absolutePath).size;
|
||||
|
||||
if (
|
||||
existingPreview &&
|
||||
existingPreview.isComplete() &&
|
||||
existingPreview.attributes.fileSize === localSize
|
||||
) {
|
||||
// Preview with same filename exists, check if we need to update preview frame time code
|
||||
if (
|
||||
previewConfig.previewFrameTimeCode &&
|
||||
existingPreview.attributes.previewFrameTimeCode !== previewConfig.previewFrameTimeCode
|
||||
) {
|
||||
await logAsync(
|
||||
() =>
|
||||
existingPreview.updateAsync({
|
||||
previewFrameTimeCode: previewConfig.previewFrameTimeCode,
|
||||
}),
|
||||
{
|
||||
pending: `Updating preview frame time code for ${chalk.bold(fileName)} (${locale})...`,
|
||||
success: `Updated preview frame time code for ${chalk.bold(fileName)} (${locale})`,
|
||||
failure: `Failed updating preview frame time code for ${chalk.bold(fileName)} (${locale})`,
|
||||
}
|
||||
);
|
||||
}
|
||||
Log.log(chalk`{dim Preview ${fileName} already exists, skipping upload}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete existing previews that don't match
|
||||
// Build a map of existing previews by filename for comparison.
|
||||
const existingByFilename = new Map<string, AppPreview>();
|
||||
for (const preview of existingPreviews) {
|
||||
if (preview.attributes.fileName !== fileName) {
|
||||
await logAsync(() => preview.deleteAsync(), {
|
||||
pending: `Deleting old preview ${chalk.bold(preview.attributes.fileName)} (${locale})...`,
|
||||
success: `Deleted old preview ${chalk.bold(preview.attributes.fileName)} (${locale})`,
|
||||
failure: `Failed deleting old preview ${chalk.bold(preview.attributes.fileName)} (${locale})`,
|
||||
});
|
||||
}
|
||||
existingByFilename.set(preview.attributes.fileName, preview);
|
||||
}
|
||||
|
||||
// Upload new preview
|
||||
await logAsync(
|
||||
() =>
|
||||
AppPreview.uploadAsync(localization.context, {
|
||||
id: previewSet!.id,
|
||||
filePath: absolutePath,
|
||||
waitForProcessing: true,
|
||||
previewFrameTimeCode: previewConfig.previewFrameTimeCode,
|
||||
}),
|
||||
{
|
||||
pending: `Uploading video preview ${chalk.bold(fileName)} (${locale})...`,
|
||||
success: `Uploaded video preview ${chalk.bold(fileName)} (${locale})`,
|
||||
failure: `Failed uploading video preview ${chalk.bold(fileName)} (${locale})`,
|
||||
// Track which previews to keep (by id), and which configs need uploading.
|
||||
const keptPreviewIds = new Set<string>();
|
||||
type UploadEntry = {
|
||||
absolutePath: string;
|
||||
fileName: string;
|
||||
previewFrameTimeCode?: string;
|
||||
};
|
||||
const toUpload: UploadEntry[] = [];
|
||||
|
||||
for (const previewConfig of previewConfigs) {
|
||||
const absolutePath = path.resolve(projectDir, previewConfig.path);
|
||||
const fileName = path.basename(absolutePath);
|
||||
|
||||
const existing = existingByFilename.get(fileName);
|
||||
const localSize = fs.existsSync(absolutePath) ? fs.statSync(absolutePath).size : null;
|
||||
|
||||
if (
|
||||
existing &&
|
||||
existing.isComplete() &&
|
||||
(localSize === null || existing.attributes.fileSize === localSize)
|
||||
) {
|
||||
keptPreviewIds.add(existing.id);
|
||||
existingByFilename.delete(fileName);
|
||||
|
||||
// If only the previewFrameTimeCode changed, patch it in place.
|
||||
if (
|
||||
previewConfig.previewFrameTimeCode &&
|
||||
existing.attributes.previewFrameTimeCode !== previewConfig.previewFrameTimeCode
|
||||
) {
|
||||
await logAsync(
|
||||
() =>
|
||||
existing.updateAsync({
|
||||
previewFrameTimeCode: previewConfig.previewFrameTimeCode,
|
||||
}),
|
||||
{
|
||||
pending: `Updating preview frame time code for ${chalk.bold(fileName)} (${locale})...`,
|
||||
success: `Updated preview frame time code for ${chalk.bold(fileName)} (${locale})`,
|
||||
failure: `Failed updating preview frame time code for ${chalk.bold(fileName)} (${locale})`,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
Log.log(chalk`{dim Preview ${fileName} already exists, skipping upload}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
);
|
||||
|
||||
toUpload.push({
|
||||
absolutePath,
|
||||
fileName,
|
||||
previewFrameTimeCode: previewConfig.previewFrameTimeCode,
|
||||
});
|
||||
}
|
||||
|
||||
// Delete previews that are no longer in config.
|
||||
for (const preview of existingByFilename.values()) {
|
||||
await logAsync(() => preview.deleteAsync(), {
|
||||
pending: `Deleting old preview ${chalk.bold(preview.attributes.fileName)} (${locale})...`,
|
||||
success: `Deleted old preview ${chalk.bold(preview.attributes.fileName)} (${locale})`,
|
||||
failure: `Failed deleting old preview ${chalk.bold(preview.attributes.fileName)} (${locale})`,
|
||||
});
|
||||
}
|
||||
|
||||
// Upload new previews.
|
||||
for (const entry of toUpload) {
|
||||
if (!fs.existsSync(entry.absolutePath)) {
|
||||
Log.warn(chalk`{yellow Video preview not found: ${entry.absolutePath}}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newPreview = await logAsync(
|
||||
() =>
|
||||
AppPreview.uploadAsync(localization.context, {
|
||||
id: previewSet!.id,
|
||||
filePath: entry.absolutePath,
|
||||
waitForProcessing: true,
|
||||
previewFrameTimeCode: entry.previewFrameTimeCode,
|
||||
}),
|
||||
{
|
||||
pending: `Uploading video preview ${chalk.bold(entry.fileName)} (${locale})...`,
|
||||
success: `Uploaded video preview ${chalk.bold(entry.fileName)} (${locale})`,
|
||||
failure: `Failed uploading video preview ${chalk.bold(entry.fileName)} (${locale})`,
|
||||
}
|
||||
);
|
||||
|
||||
keptPreviewIds.add(newPreview.id);
|
||||
}
|
||||
|
||||
// Reorder previews to match config order (mirrors screenshots).
|
||||
if (keptPreviewIds.size > 0) {
|
||||
const refreshedSet = await AppPreviewSet.infoAsync(localization.context, {
|
||||
id: previewSet.id,
|
||||
});
|
||||
const refreshedPreviews = refreshedSet.attributes.appPreviews || [];
|
||||
const previewsByFilename = new Map<string, AppPreview>();
|
||||
for (const p of refreshedPreviews) {
|
||||
previewsByFilename.set(p.attributes.fileName, p);
|
||||
}
|
||||
|
||||
const orderedIds: string[] = [];
|
||||
for (const previewConfig of previewConfigs) {
|
||||
const fileName = path.basename(previewConfig.path);
|
||||
const preview = previewsByFilename.get(fileName);
|
||||
if (preview) {
|
||||
orderedIds.push(preview.id);
|
||||
}
|
||||
}
|
||||
|
||||
const currentIds = refreshedPreviews.map(p => p.id);
|
||||
if (
|
||||
orderedIds.length > 0 &&
|
||||
(orderedIds.length !== currentIds.length || orderedIds.some((id, i) => id !== currentIds[i]))
|
||||
) {
|
||||
await previewSet.reorderPreviewsAsync({ appPreviews: orderedIds });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -73,20 +73,32 @@ export class ScreenshotsTask extends AppleTask {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Download screenshots and save to local filesystem
|
||||
// Download screenshots and save to local filesystem. When a screenshot
|
||||
// is in a broken state (AWAITING_UPLOAD with no rendered imageAsset)
|
||||
// the download will fail, but we still preserve the entry pointing at
|
||||
// its expected local path so users can either drop in a replacement
|
||||
// file or remove the entry to delete the broken ASC record.
|
||||
const paths: string[] = [];
|
||||
for (let i = 0; i < screenshotModels.length; i++) {
|
||||
const screenshot = screenshotModels[i];
|
||||
const relativePath = await downloadScreenshotAsync(
|
||||
const downloaded = await downloadScreenshotAsync(
|
||||
context.projectDir,
|
||||
localeCode,
|
||||
displayType,
|
||||
screenshot,
|
||||
i
|
||||
);
|
||||
if (relativePath) {
|
||||
paths.push(relativePath);
|
||||
if (downloaded) {
|
||||
paths.push(downloaded);
|
||||
continue;
|
||||
}
|
||||
// Fall back to a placeholder path so the entry isn't lost from
|
||||
// config. Push will detect that the existing screenshot isn't
|
||||
// complete and either re-upload (if a local file exists at this
|
||||
// path) or warn and skip (if it doesn't).
|
||||
const fileName =
|
||||
screenshot.attributes.fileName || `${String(i + 1).padStart(2, '0')}.png`;
|
||||
paths.push(path.join('store', 'apple', 'screenshot', localeCode, displayType, fileName));
|
||||
}
|
||||
|
||||
if (paths.length > 0) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
AgeRatingDeclarationProps,
|
||||
AppClipAction,
|
||||
PreviewType,
|
||||
ScreenshotDisplayType,
|
||||
} from '@expo/apple-utils';
|
||||
@@ -35,11 +36,16 @@ export type ApplePreviewConfig =
|
||||
|
||||
/**
|
||||
* Video previews organized by display type.
|
||||
* Key is the display type (e.g., 'IPHONE_67'), value is the preview config.
|
||||
* Key is the display type (e.g., 'IPHONE_67'), value is the preview config or an array of preview configs.
|
||||
* Apple supports up to 3 previews per device type. A single object/string is still
|
||||
* accepted for backwards compatibility with existing store configs.
|
||||
* @example { "IPHONE_67": "./previews/demo.mp4" }
|
||||
* @example { "IPHONE_67": { path: "./previews/demo.mp4", previewFrameTimeCode: "00:05:00" } }
|
||||
* @example { "IPHONE_67": ["./previews/intro.mp4", "./previews/features.mp4"] }
|
||||
*/
|
||||
export type ApplePreviews = Partial<Record<ApplePreviewType, ApplePreviewConfig>>;
|
||||
export type ApplePreviews = Partial<
|
||||
Record<ApplePreviewType, ApplePreviewConfig | ApplePreviewConfig[]>
|
||||
>;
|
||||
|
||||
export interface AppleMetadata {
|
||||
version?: string;
|
||||
@@ -51,6 +57,38 @@ export interface AppleMetadata {
|
||||
/** @deprecated Use screenshots/previews in AppleInfo instead */
|
||||
preview?: Record<string, string[]>;
|
||||
review?: AppleReview;
|
||||
/** App Clip metadata. Only applies to apps that ship an App Clip target. */
|
||||
appClip?: AppleAppClip;
|
||||
}
|
||||
|
||||
/** App Clip action enum values from App Store Connect API */
|
||||
export type AppleAppClipAction = `${AppClipAction}`;
|
||||
|
||||
export interface AppleAppClip {
|
||||
/** The default experience for this App Clip. There is exactly one per app. */
|
||||
defaultExperience?: AppleAppClipDefaultExperience;
|
||||
}
|
||||
|
||||
export interface AppleAppClipDefaultExperience {
|
||||
/** Action button shown in the App Clip card. Defaults to OPEN if unset. */
|
||||
action?: AppleAppClipAction;
|
||||
/** Whether to release this default experience alongside the next App Store version. */
|
||||
releaseWithAppStoreVersion?: boolean;
|
||||
/** App Store review invocation URLs (used by App Review to launch the clip). */
|
||||
reviewDetail?: AppleAppClipReviewDetail;
|
||||
/** Per-locale subtitle and header image. */
|
||||
info?: Record<AppleLocale, AppleAppClipLocalizedInfo>;
|
||||
}
|
||||
|
||||
export interface AppleAppClipReviewDetail {
|
||||
invocationUrls: string[];
|
||||
}
|
||||
|
||||
export interface AppleAppClipLocalizedInfo {
|
||||
/** Subtitle shown in the App Clip card. Apple limits this to 43 characters. */
|
||||
subtitle?: string;
|
||||
/** Relative path (from project root) to the App Clip header image PNG. */
|
||||
headerImage?: string;
|
||||
}
|
||||
|
||||
// The omited properties are deprecated
|
||||
|
||||
@@ -85,6 +85,16 @@ export default class SessionManager {
|
||||
}
|
||||
|
||||
public async logoutAsync(): Promise<void> {
|
||||
const sessionSecret = this.getSessionSecret();
|
||||
if (sessionSecret) {
|
||||
const apiV2Client = new ApiV2Client({ accessToken: null, sessionSecret });
|
||||
try {
|
||||
await apiV2Client.postAsync('auth/logout', { body: {} });
|
||||
} catch (e) {
|
||||
// Best-effort: clear the local session even if the server request fails
|
||||
Log.debug('Failed to invalidate session secret on server:', e);
|
||||
}
|
||||
}
|
||||
this.currentActor = undefined;
|
||||
await this.setSessionAsync(undefined);
|
||||
}
|
||||
|
||||
@@ -219,6 +219,38 @@ describe(SessionManager, () => {
|
||||
await sessionManager.logoutAsync();
|
||||
expect(sessionManager['getSessionSecret']()).toBe(null);
|
||||
});
|
||||
|
||||
it('calls the server logout endpoint when session secret exists', async () => {
|
||||
jest.mocked(fetchSessionSecretAndUserAsync).mockResolvedValue({
|
||||
sessionSecret: 'SESSION_SECRET',
|
||||
id: 'USER_ID',
|
||||
username: 'USERNAME',
|
||||
});
|
||||
const apiV2PostSpy = jest.spyOn(ApiV2Client.prototype, 'postAsync');
|
||||
|
||||
const sessionManager = new SessionManager(analytics);
|
||||
await sessionManager['loginAsync']({ username: 'USERNAME', password: 'PASSWORD' });
|
||||
await sessionManager.logoutAsync();
|
||||
|
||||
expect(apiV2PostSpy).toHaveBeenCalledWith('auth/logout', { body: {} });
|
||||
});
|
||||
|
||||
it('clears the local session even if the server logout call fails', async () => {
|
||||
jest.mocked(fetchSessionSecretAndUserAsync).mockResolvedValue({
|
||||
sessionSecret: 'SESSION_SECRET',
|
||||
id: 'USER_ID',
|
||||
username: 'USERNAME',
|
||||
});
|
||||
jest
|
||||
.spyOn(ApiV2Client.prototype, 'postAsync')
|
||||
.mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
const sessionManager = new SessionManager(analytics);
|
||||
await sessionManager['loginAsync']({ username: 'USERNAME', password: 'PASSWORD' });
|
||||
await sessionManager.logoutAsync();
|
||||
|
||||
expect(sessionManager['getSessionSecret']()).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showLoginPromptAsync', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/eas-json",
|
||||
"version": "18.4.0",
|
||||
"version": "18.5.0",
|
||||
"description": "A library for interacting with eas.json",
|
||||
"homepage": "https://github.com/expo/eas-cli",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "7.23.5",
|
||||
"@expo/eas-build-job": "18.4.0",
|
||||
"@expo/eas-build-job": "18.5.0",
|
||||
"chalk": "4.1.2",
|
||||
"env-string": "1.0.1",
|
||||
"fs-extra": "11.2.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli-local-build-plugin",
|
||||
"version": "18.4.0",
|
||||
"version": "18.5.0",
|
||||
"description": "Tool for running EAS compatible builds on a local machine.",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
@@ -26,11 +26,11 @@
|
||||
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/build-tools": "18.4.0",
|
||||
"@expo/eas-build-job": "18.4.0",
|
||||
"@expo/logger": "18.0.1",
|
||||
"@expo/build-tools": "18.5.0",
|
||||
"@expo/eas-build-job": "18.5.0",
|
||||
"@expo/logger": "18.5.0",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
"@expo/turtle-spawn": "18.0.1",
|
||||
"@expo/turtle-spawn": "18.5.0",
|
||||
"bunyan": "^1.8.15",
|
||||
"chalk": "4.1.2",
|
||||
"env-paths": "^2.2.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/logger",
|
||||
"version": "18.0.1",
|
||||
"version": "18.5.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/steps",
|
||||
"version": "18.4.0",
|
||||
"version": "18.5.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
@@ -27,8 +27,8 @@
|
||||
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/eas-build-job": "18.4.0",
|
||||
"@expo/logger": "18.0.1",
|
||||
"@expo/eas-build-job": "18.5.0",
|
||||
"@expo/logger": "18.5.0",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
"arg": "^5.0.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/template-file",
|
||||
"version": "18.0.1",
|
||||
"version": "18.5.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/turtle-spawn",
|
||||
"version": "18.0.1",
|
||||
"version": "18.5.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
@@ -25,7 +25,7 @@
|
||||
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/logger": "18.0.1",
|
||||
"@expo/logger": "18.5.0",
|
||||
"@expo/spawn-async": "^1.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/worker",
|
||||
"version": "18.4.0",
|
||||
"version": "18.5.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "yarn watch",
|
||||
@@ -19,18 +19,18 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@expo/build-tools": "18.4.0",
|
||||
"@expo/build-tools": "18.5.0",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/config-types": "55.0.5",
|
||||
"@expo/downloader": "18.0.1",
|
||||
"@expo/eas-build-job": "18.4.0",
|
||||
"@expo/logger": "18.0.1",
|
||||
"@expo/downloader": "18.5.0",
|
||||
"@expo/eas-build-job": "18.5.0",
|
||||
"@expo/logger": "18.5.0",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "18.4.0",
|
||||
"@expo/template-file": "18.0.1",
|
||||
"@expo/turtle-spawn": "18.0.1",
|
||||
"@expo/steps": "18.5.0",
|
||||
"@expo/template-file": "18.5.0",
|
||||
"@expo/turtle-spawn": "18.5.0",
|
||||
"@hapi/boom": "10.0.1",
|
||||
"@sentry/node": "7.77.0",
|
||||
"check-disk-space": "3.4.0",
|
||||
|
||||
@@ -2847,33 +2847,33 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/apple-utils@npm:2.1.16":
|
||||
version: 2.1.16
|
||||
resolution: "@expo/apple-utils@npm:2.1.16"
|
||||
"@expo/apple-utils@npm:2.1.19":
|
||||
version: 2.1.19
|
||||
resolution: "@expo/apple-utils@npm:2.1.19"
|
||||
bin:
|
||||
apple-utils: bin.js
|
||||
checksum: 10c0/9d3915f3f25c32e1f1ab323676cabccc2998046be6f30deebc68a0706836bd50bc5145662fa13441e0283f82c56e50f55002d50dcc43aa78e9fa7327d1676a54
|
||||
checksum: 10c0/42eb1038bd0906d53b37d830499c7a8ee7ec9f9b9ef4078ff898ca84cb5af1e6ac39d3a392818825151aa2ea237a44698f539b973936733350f314efff020858
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/build-tools@npm:18.4.0, @expo/build-tools@workspace:packages/build-tools":
|
||||
"@expo/build-tools@npm:18.5.0, @expo/build-tools@workspace:packages/build-tools":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/build-tools@workspace:packages/build-tools"
|
||||
dependencies:
|
||||
"@expo/config": "npm:55.0.10"
|
||||
"@expo/config-plugins": "npm:55.0.7"
|
||||
"@expo/downloader": "npm:18.0.1"
|
||||
"@expo/eas-build-job": "npm:18.4.0"
|
||||
"@expo/downloader": "npm:18.5.0"
|
||||
"@expo/eas-build-job": "npm:18.5.0"
|
||||
"@expo/env": "npm:^0.4.0"
|
||||
"@expo/logger": "npm:18.0.1"
|
||||
"@expo/logger": "npm:18.5.0"
|
||||
"@expo/package-manager": "npm:1.9.10"
|
||||
"@expo/plist": "npm:^0.2.0"
|
||||
"@expo/repack-app": "npm:~0.4.1"
|
||||
"@expo/results": "npm:^1.0.0"
|
||||
"@expo/spawn-async": "npm:1.7.2"
|
||||
"@expo/steps": "npm:18.4.0"
|
||||
"@expo/template-file": "npm:18.0.1"
|
||||
"@expo/turtle-spawn": "npm:18.0.1"
|
||||
"@expo/steps": "npm:18.5.0"
|
||||
"@expo/template-file": "npm:18.5.0"
|
||||
"@expo/turtle-spawn": "npm:18.5.0"
|
||||
"@expo/xcpretty": "npm:^4.3.1"
|
||||
"@google-cloud/storage": "npm:^7.11.2"
|
||||
"@types/fs-extra": "npm:^11.0.4"
|
||||
@@ -3095,7 +3095,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/downloader@npm:18.0.1, @expo/downloader@workspace:packages/downloader":
|
||||
"@expo/downloader@npm:18.5.0, @expo/downloader@workspace:packages/downloader":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/downloader@workspace:packages/downloader"
|
||||
dependencies:
|
||||
@@ -3111,11 +3111,11 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@expo/eas-build-job@npm:18.4.0, @expo/eas-build-job@workspace:packages/eas-build-job":
|
||||
"@expo/eas-build-job@npm:18.5.0, @expo/eas-build-job@workspace:packages/eas-build-job":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/eas-build-job@workspace:packages/eas-build-job"
|
||||
dependencies:
|
||||
"@expo/logger": "npm:18.0.1"
|
||||
"@expo/logger": "npm:18.5.0"
|
||||
"@types/jest": "npm:^29.5.12"
|
||||
"@types/node": "npm:20.14.2"
|
||||
jest: "npm:^29.7.0"
|
||||
@@ -3128,12 +3128,12 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@expo/eas-json@npm:18.4.0, @expo/eas-json@workspace:packages/eas-json":
|
||||
"@expo/eas-json@npm:18.5.0, @expo/eas-json@workspace:packages/eas-json":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/eas-json@workspace:packages/eas-json"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:7.23.5"
|
||||
"@expo/eas-build-job": "npm:18.4.0"
|
||||
"@expo/eas-build-job": "npm:18.5.0"
|
||||
"@tsconfig/node20": "npm:20.1.8"
|
||||
"@types/babel__code-frame": "npm:7.0.3"
|
||||
"@types/fs-extra": "npm:11.0.4"
|
||||
@@ -3270,7 +3270,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/logger@npm:18.0.1, @expo/logger@workspace:packages/logger":
|
||||
"@expo/logger@npm:18.5.0, @expo/logger@workspace:packages/logger":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/logger@workspace:packages/logger"
|
||||
dependencies:
|
||||
@@ -3481,12 +3481,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/steps@npm:18.4.0, @expo/steps@workspace:packages/steps":
|
||||
"@expo/steps@npm:18.5.0, @expo/steps@workspace:packages/steps":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/steps@workspace:packages/steps"
|
||||
dependencies:
|
||||
"@expo/eas-build-job": "npm:18.4.0"
|
||||
"@expo/logger": "npm:18.0.1"
|
||||
"@expo/eas-build-job": "npm:18.5.0"
|
||||
"@expo/logger": "npm:18.5.0"
|
||||
"@expo/spawn-async": "npm:^1.7.2"
|
||||
"@jest/globals": "npm:^29.7.0"
|
||||
"@types/jest": "npm:^29.5.12"
|
||||
@@ -3511,7 +3511,7 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@expo/template-file@npm:18.0.1, @expo/template-file@workspace:packages/template-file":
|
||||
"@expo/template-file@npm:18.5.0, @expo/template-file@workspace:packages/template-file":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/template-file@workspace:packages/template-file"
|
||||
dependencies:
|
||||
@@ -3534,11 +3534,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/turtle-spawn@npm:18.0.1, @expo/turtle-spawn@workspace:packages/turtle-spawn":
|
||||
"@expo/turtle-spawn@npm:18.5.0, @expo/turtle-spawn@workspace:packages/turtle-spawn":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/turtle-spawn@workspace:packages/turtle-spawn"
|
||||
dependencies:
|
||||
"@expo/logger": "npm:18.0.1"
|
||||
"@expo/logger": "npm:18.5.0"
|
||||
"@expo/spawn-async": "npm:^1.7.2"
|
||||
"@types/jest": "npm:^29.5.12"
|
||||
"@types/node": "npm:20.14.2"
|
||||
@@ -3554,18 +3554,18 @@ __metadata:
|
||||
resolution: "@expo/worker@workspace:packages/worker"
|
||||
dependencies:
|
||||
"@babel/core": "npm:7.24.7"
|
||||
"@expo/build-tools": "npm:18.4.0"
|
||||
"@expo/build-tools": "npm:18.5.0"
|
||||
"@expo/config-plugins": "npm:55.0.7"
|
||||
"@expo/config-types": "npm:55.0.5"
|
||||
"@expo/downloader": "npm:18.0.1"
|
||||
"@expo/eas-build-job": "npm:18.4.0"
|
||||
"@expo/logger": "npm:18.0.1"
|
||||
"@expo/downloader": "npm:18.5.0"
|
||||
"@expo/eas-build-job": "npm:18.5.0"
|
||||
"@expo/logger": "npm:18.5.0"
|
||||
"@expo/results": "npm:1.0.0"
|
||||
"@expo/rudder-sdk-node": "npm:1.1.1"
|
||||
"@expo/spawn-async": "npm:1.7.2"
|
||||
"@expo/steps": "npm:18.4.0"
|
||||
"@expo/template-file": "npm:18.0.1"
|
||||
"@expo/turtle-spawn": "npm:18.0.1"
|
||||
"@expo/steps": "npm:18.5.0"
|
||||
"@expo/template-file": "npm:18.5.0"
|
||||
"@expo/turtle-spawn": "npm:18.5.0"
|
||||
"@hapi/boom": "npm:10.0.1"
|
||||
"@sentry/node": "npm:7.77.0"
|
||||
"@types/babel__core": "npm:7.20.5"
|
||||
@@ -10331,7 +10331,7 @@ __metadata:
|
||||
resolution: "create-eas-build-function@workspace:packages/create-eas-build-function"
|
||||
dependencies:
|
||||
"@expo/package-manager": "npm:1.9.10"
|
||||
"@expo/steps": "npm:18.4.0"
|
||||
"@expo/steps": "npm:18.5.0"
|
||||
"@jest/globals": "npm:^29.7.0"
|
||||
"@types/jest": "npm:^29.5.12"
|
||||
"@types/node": "npm:20.14.2"
|
||||
@@ -10944,11 +10944,11 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "eas-cli-local-build-plugin@workspace:packages/local-build-plugin"
|
||||
dependencies:
|
||||
"@expo/build-tools": "npm:18.4.0"
|
||||
"@expo/eas-build-job": "npm:18.4.0"
|
||||
"@expo/logger": "npm:18.0.1"
|
||||
"@expo/build-tools": "npm:18.5.0"
|
||||
"@expo/eas-build-job": "npm:18.5.0"
|
||||
"@expo/logger": "npm:18.5.0"
|
||||
"@expo/spawn-async": "npm:^1.7.2"
|
||||
"@expo/turtle-spawn": "npm:18.0.1"
|
||||
"@expo/turtle-spawn": "npm:18.5.0"
|
||||
"@types/bunyan": "npm:^1.8.11"
|
||||
"@types/fs-extra": "npm:^11.0.4"
|
||||
"@types/hapi__joi": "npm:^17.1.14"
|
||||
@@ -10998,15 +10998,15 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "eas-cli@workspace:packages/eas-cli"
|
||||
dependencies:
|
||||
"@expo/apple-utils": "npm:2.1.16"
|
||||
"@expo/apple-utils": "npm:2.1.19"
|
||||
"@expo/code-signing-certificates": "npm:0.0.5"
|
||||
"@expo/config": "npm:55.0.10"
|
||||
"@expo/config-plugins": "npm:55.0.7"
|
||||
"@expo/eas-build-job": "npm:18.4.0"
|
||||
"@expo/eas-json": "npm:18.4.0"
|
||||
"@expo/eas-build-job": "npm:18.5.0"
|
||||
"@expo/eas-json": "npm:18.5.0"
|
||||
"@expo/env": "npm:^1.0.0"
|
||||
"@expo/json-file": "npm:8.3.3"
|
||||
"@expo/logger": "npm:18.0.1"
|
||||
"@expo/logger": "npm:18.5.0"
|
||||
"@expo/multipart-body-parser": "npm:2.0.0"
|
||||
"@expo/osascript": "npm:2.1.4"
|
||||
"@expo/package-manager": "npm:1.9.10"
|
||||
@@ -11018,7 +11018,7 @@ __metadata:
|
||||
"@expo/results": "npm:1.0.0"
|
||||
"@expo/rudder-sdk-node": "npm:1.1.1"
|
||||
"@expo/spawn-async": "npm:1.7.2"
|
||||
"@expo/steps": "npm:18.4.0"
|
||||
"@expo/steps": "npm:18.5.0"
|
||||
"@expo/timeago.js": "npm:1.0.0"
|
||||
"@graphql-codegen/cli": "npm:5.0.0"
|
||||
"@graphql-codegen/introspection": "npm:4.0.0"
|
||||
|
||||
Reference in new issue
Block a user