mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8d4262aa7 | ||
|
|
b22d89e41f | ||
|
|
81fe0f237f | ||
|
|
b9ddc04cf7 | ||
|
|
d6a8a4f694 |
No files matched your search
+9
-3
@@ -8,13 +8,19 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [build-tools] Add `eas/read_package_json` and `eas/read_app_config` functions ([#3585](https://github.com/expo/eas-cli/pull/3585) by [@gwdp](https://github.com/gwdp))
|
||||
- [eas-cli] Add `--skip-bundler` to `eas build:dev` to install and run a development build without starting the local Expo/Metro server. ([#3604](https://github.com/expo/eas-cli/pull/3604) by [@mmichels-brex](https://github.com/mmichels-brex))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [eas-cli] Fixes for `observe` commands, including an issue for apps with many update IDs. ([#3609](https://github.com/expo/eas-cli/pull/3609) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [18.7.0](https://github.com/expo/eas-cli/releases/tag/v18.7.0) - 2026-04-14
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [build-tools] Add `eas/read_package_json` and `eas/read_app_config` functions ([#3585](https://github.com/expo/eas-cli/pull/3585) by [@gwdp](https://github.com/gwdp))
|
||||
- [eas-cli] Add `--skip-bundler` to `eas build:dev` to install and run a development build without starting the local Expo/Metro server. ([#3604](https://github.com/expo/eas-cli/pull/3604) by [@mmichels-brex](https://github.com/mmichels-brex))
|
||||
|
||||
## [18.6.0](https://github.com/expo/eas-cli/releases/tag/v18.6.0) - 2026-04-10
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
@@ -200,7 +200,7 @@ export async function restoreGradleCacheAsync({
|
||||
env: Record<string, string | undefined>;
|
||||
secrets?: { robotAccessToken?: string };
|
||||
}): Promise<void> {
|
||||
if (env.EXPERIMENTAL_EAS_GRADLE_CACHE !== '1') {
|
||||
if (env.EAS_GRADLE_CACHE !== '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ export async function saveGradleCacheAsync({
|
||||
env: Record<string, string | undefined>;
|
||||
secrets?: { robotAccessToken?: string };
|
||||
}): Promise<void> {
|
||||
if (env.EXPERIMENTAL_EAS_GRADLE_CACHE !== '1') {
|
||||
if (env.EAS_GRADLE_CACHE !== '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,110 @@ describe('StaticWorkflowInterpolationContextZ', () => {
|
||||
expect(StaticWorkflowInterpolationContextZ.parse(context)).toEqual(context);
|
||||
});
|
||||
|
||||
it('accepts app_store_connect build_upload with cf_bundle_version', () => {
|
||||
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: '123e4567-e89b-42d3-a456-426614174000',
|
||||
state: 'processing',
|
||||
cf_bundle_version: '42',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(StaticWorkflowInterpolationContextZ.parse(context)).toEqual(context);
|
||||
});
|
||||
|
||||
it('accepts app_store_connect build_upload with build.id', () => {
|
||||
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: '123e4567-e89b-42d3-a456-426614174000',
|
||||
state: 'complete',
|
||||
build: {
|
||||
id: 'build-abc123',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(StaticWorkflowInterpolationContextZ.parse(context)).toEqual(context);
|
||||
});
|
||||
|
||||
it('accepts app_store_connect build_upload with both cf_bundle_version and build.id', () => {
|
||||
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: '123e4567-e89b-42d3-a456-426614174000',
|
||||
state: 'complete',
|
||||
cf_bundle_version: '42',
|
||||
build: {
|
||||
id: 'build-abc123',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(StaticWorkflowInterpolationContextZ.parse(context)).toEqual(context);
|
||||
});
|
||||
|
||||
it('accepts app_store_connect context without build_upload', () => {
|
||||
const context = {
|
||||
after: {},
|
||||
|
||||
@@ -256,6 +256,12 @@ const AppStoreConnectContextZ = z.looseObject({
|
||||
.looseObject({
|
||||
id: z.string(),
|
||||
state: z.enum(['awaiting_upload', 'processing', 'failed', 'complete']),
|
||||
cf_bundle_version: z.string().optional(),
|
||||
build: z
|
||||
.looseObject({
|
||||
id: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
external_beta: z
|
||||
|
||||
@@ -199,6 +199,8 @@ describe(ObserveMetrics, () => {
|
||||
expect.any(Map),
|
||||
expect.any(Array),
|
||||
['min', 'average'],
|
||||
expect.any(Map),
|
||||
expect.any(Map),
|
||||
expect.any(Map)
|
||||
);
|
||||
expect(mockPrintJsonOnlyOutput).toHaveBeenCalled();
|
||||
|
||||
@@ -81,16 +81,29 @@ export default class ObserveEvents extends EasCommand {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
private static loggedInOnlyContextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags, args } = await this.parse(ObserveEvents);
|
||||
const {
|
||||
projectId: contextProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(ObserveEvents, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
});
|
||||
|
||||
const projectId = flags['project-id'] ?? contextProjectId;
|
||||
let projectId: string;
|
||||
let graphqlClient;
|
||||
if (flags['project-id']) {
|
||||
projectId = flags['project-id'];
|
||||
const ctx = await this.getContextAsync(
|
||||
{ contextDefinition: ObserveEvents.loggedInOnlyContextDefinition },
|
||||
{ nonInteractive: flags['non-interactive'] }
|
||||
);
|
||||
graphqlClient = ctx.loggedIn.graphqlClient;
|
||||
} else {
|
||||
const ctx = await this.getContextAsync(ObserveEvents, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
});
|
||||
projectId = ctx.projectId;
|
||||
graphqlClient = ctx.loggedIn.graphqlClient;
|
||||
}
|
||||
|
||||
if (flags.json) {
|
||||
enableJsonOutput();
|
||||
|
||||
@@ -78,16 +78,29 @@ export default class ObserveMetrics extends EasCommand {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
private static loggedInOnlyContextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(ObserveMetrics);
|
||||
const {
|
||||
projectId: contextProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(ObserveMetrics, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
});
|
||||
|
||||
const projectId = flags['project-id'] ?? contextProjectId;
|
||||
let projectId: string;
|
||||
let graphqlClient;
|
||||
if (flags['project-id']) {
|
||||
projectId = flags['project-id'];
|
||||
const ctx = await this.getContextAsync(
|
||||
{ contextDefinition: ObserveMetrics.loggedInOnlyContextDefinition },
|
||||
{ nonInteractive: flags['non-interactive'] }
|
||||
);
|
||||
graphqlClient = ctx.loggedIn.graphqlClient;
|
||||
} else {
|
||||
const ctx = await this.getContextAsync(ObserveMetrics, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
});
|
||||
projectId = ctx.projectId;
|
||||
graphqlClient = ctx.loggedIn.graphqlClient;
|
||||
}
|
||||
|
||||
if (flags.json) {
|
||||
enableJsonOutput();
|
||||
@@ -122,7 +135,14 @@ export default class ObserveMetrics extends EasCommand {
|
||||
if (flags.json) {
|
||||
const stats: StatisticKey[] = argumentsStat ?? DEFAULT_STATS_JSON;
|
||||
printJsonOnlyOutput(
|
||||
buildObserveMetricsJson(metricsMap, metricNames, stats, totalEventCounts)
|
||||
buildObserveMetricsJson(
|
||||
metricsMap,
|
||||
metricNames,
|
||||
stats,
|
||||
totalEventCounts,
|
||||
buildNumbersMap,
|
||||
updateIdsMap
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const stats: StatisticKey[] = argumentsStat ?? DEFAULT_STATS_TABLE;
|
||||
@@ -131,7 +151,6 @@ export default class ObserveMetrics extends EasCommand {
|
||||
buildObserveMetricsTable(metricsMap, metricNames, stats, {
|
||||
daysBack,
|
||||
buildNumbersMap,
|
||||
updateIdsMap,
|
||||
totalEventCounts,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -42,16 +42,29 @@ export default class ObserveVersions extends EasCommand {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
private static loggedInOnlyContextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(ObserveVersions);
|
||||
const {
|
||||
projectId: contextProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(ObserveVersions, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
});
|
||||
|
||||
const projectId = flags['project-id'] ?? contextProjectId;
|
||||
let projectId: string;
|
||||
let graphqlClient;
|
||||
if (flags['project-id']) {
|
||||
projectId = flags['project-id'];
|
||||
const ctx = await this.getContextAsync(
|
||||
{ contextDefinition: ObserveVersions.loggedInOnlyContextDefinition },
|
||||
{ nonInteractive: flags['non-interactive'] }
|
||||
);
|
||||
graphqlClient = ctx.loggedIn.graphqlClient;
|
||||
} else {
|
||||
const ctx = await this.getContextAsync(ObserveVersions, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
});
|
||||
projectId = ctx.projectId;
|
||||
graphqlClient = ctx.loggedIn.graphqlClient;
|
||||
}
|
||||
|
||||
if (flags.json) {
|
||||
enableJsonOutput();
|
||||
|
||||
@@ -5,4 +5,4 @@ import { getAdapter } from 'axios';
|
||||
// see: https://github.com/nock/nock#axios
|
||||
|
||||
export { default } from 'nock';
|
||||
getRequestClient().defaults.adapter = getAdapter('http');
|
||||
getRequestClient().defaults.adapter = getAdapter('http') as any;
|
||||
@@ -443,5 +443,54 @@ describe(PreviewsTask, () => {
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('deletes remote previews when preview type is removed from config', async () => {
|
||||
const config = new AppleConfigReader({
|
||||
info: {
|
||||
'en-US': {
|
||||
title: 'My App',
|
||||
// No previews configured — the IPHONE_67 preview should be deleted
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
const existingPreview = new AppPreview(requestContext, 'PV_1', {
|
||||
fileName: 'demo.mp4',
|
||||
fileSize: 10240,
|
||||
previewFrameTimeCode: null,
|
||||
assetDeliveryState: { state: 'COMPLETE', errors: [], warnings: [] },
|
||||
} as any);
|
||||
|
||||
const deleteScope = nock('https://api.appstoreconnect.apple.com')
|
||||
.delete(`/v1/${AppPreview.type}/PV_1`)
|
||||
.reply(204);
|
||||
|
||||
const previewTypeMap = new Map<PreviewType, AppPreviewSet>();
|
||||
previewTypeMap.set(
|
||||
PreviewType.IPHONE_67,
|
||||
new AppPreviewSet(requestContext, 'PSET_1', {
|
||||
previewType: PreviewType.IPHONE_67,
|
||||
appPreviews: [existingPreview],
|
||||
} as any)
|
||||
);
|
||||
|
||||
const previewSets = new Map();
|
||||
previewSets.set('en-US', previewTypeMap);
|
||||
|
||||
await new PreviewsTask().uploadAsync({
|
||||
config,
|
||||
context: {
|
||||
previewSets,
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(deleteScope.isDone()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -136,9 +136,7 @@ export class PreviewsTask extends AppleTask {
|
||||
|
||||
for (const localeCode of locales) {
|
||||
const previews = config.getPreviews(localeCode);
|
||||
if (!previews || Object.keys(previews).length === 0) {
|
||||
continue;
|
||||
}
|
||||
const existingSets = context.previewSets.get(localeCode);
|
||||
|
||||
const localization = context.versionLocales.find(l => l.attributes.locale === localeCode);
|
||||
if (!localization) {
|
||||
@@ -146,18 +144,39 @@ export class PreviewsTask extends AppleTask {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [previewType, previewConfig] of Object.entries(previews)) {
|
||||
if (!previewConfig) {
|
||||
continue;
|
||||
}
|
||||
// Upload/sync configured previews
|
||||
if (previews) {
|
||||
for (const [previewType, previewConfig] of Object.entries(previews)) {
|
||||
if (!previewConfig) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await syncPreviewSetAsync(
|
||||
context.projectDir,
|
||||
localization,
|
||||
previewType as PreviewType,
|
||||
normalizePreviewConfig(previewConfig),
|
||||
context.previewSets.get(localeCode)
|
||||
);
|
||||
await syncPreviewSetAsync(
|
||||
context.projectDir,
|
||||
localization,
|
||||
previewType as PreviewType,
|
||||
normalizePreviewConfig(previewConfig),
|
||||
existingSets
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete remote previews that are no longer in config
|
||||
if (existingSets) {
|
||||
for (const [previewType, previewSet] of existingSets) {
|
||||
if (previews?.[previewType]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingPreviews = previewSet.attributes.appPreviews || [];
|
||||
for (const preview of existingPreviews) {
|
||||
await logAsync(() => preview.deleteAsync(), {
|
||||
pending: `Deleting video preview ${chalk.bold(preview.attributes.fileName)} (${localeCode})...`,
|
||||
success: `Deleted video preview ${chalk.bold(preview.attributes.fileName)} (${localeCode})`,
|
||||
failure: `Failed deleting video preview ${chalk.bold(preview.attributes.fileName)} (${localeCode})`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,8 @@ describe(buildObserveMetricsJson, () => {
|
||||
expect(result.versions[0]).toEqual({
|
||||
appVersion: '1.0.0',
|
||||
platform: AppPlatform.Ios,
|
||||
buildNumbers: [],
|
||||
updateIds: [],
|
||||
metrics: {
|
||||
'expo.app_startup.tti': {
|
||||
min: 0.1,
|
||||
|
||||
@@ -105,6 +105,8 @@ export type MetricValuesJson = Partial<Record<StatisticKey, number | null>>;
|
||||
export interface ObserveMetricsVersionResult {
|
||||
appVersion: string;
|
||||
platform: AppPlatform;
|
||||
buildNumbers: string[];
|
||||
updateIds: string[];
|
||||
metrics: Record<string, MetricValuesJson>;
|
||||
}
|
||||
|
||||
@@ -117,7 +119,9 @@ export function buildObserveMetricsJson(
|
||||
metricsMap: ObserveMetricsMap,
|
||||
metricNames: string[],
|
||||
stats: StatisticKey[],
|
||||
totalEventCounts?: Map<string, number>
|
||||
totalEventCounts?: Map<string, number>,
|
||||
buildNumbersMap?: BuildNumbersMap,
|
||||
updateIdsMap?: UpdateIdsMap
|
||||
): ObserveMetricsJsonOutput {
|
||||
const versions: ObserveMetricsVersionResult[] = [];
|
||||
|
||||
@@ -134,7 +138,13 @@ export function buildObserveMetricsJson(
|
||||
metrics[metricName] = statValues;
|
||||
}
|
||||
|
||||
versions.push({ appVersion, platform, metrics });
|
||||
versions.push({
|
||||
appVersion,
|
||||
platform,
|
||||
buildNumbers: buildNumbersMap?.get(key) ?? [],
|
||||
updateIds: updateIdsMap?.get(key) ?? [],
|
||||
metrics,
|
||||
});
|
||||
}
|
||||
|
||||
// Group total event counts by metric → platform
|
||||
@@ -188,7 +198,6 @@ export function buildObserveMetricsTable(
|
||||
options?: {
|
||||
daysBack?: number;
|
||||
buildNumbersMap?: BuildNumbersMap;
|
||||
updateIdsMap?: UpdateIdsMap;
|
||||
totalEventCounts?: Map<string, number>;
|
||||
}
|
||||
): string {
|
||||
@@ -232,12 +241,7 @@ export function buildObserveMetricsTable(
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if any version has updates
|
||||
const hasUpdates = options?.updateIdsMap
|
||||
? Array.from(options.updateIdsMap.values()).some(ids => ids.length > 0)
|
||||
: false;
|
||||
|
||||
const headers = ['App Version', ...(hasUpdates ? ['Updates'] : []), ...metricHeaders];
|
||||
const headers = ['App Version', ...metricHeaders];
|
||||
|
||||
const sections: string[] = [chalk.bold(summaryLine)];
|
||||
|
||||
@@ -245,16 +249,14 @@ export function buildObserveMetricsTable(
|
||||
sections.push('');
|
||||
sections.push(chalk.bold(appPlatformDisplayNames[platform]));
|
||||
|
||||
const rows: string[][] = platformResults.map(result => {
|
||||
const rows: string[][] = [];
|
||||
for (const result of platformResults) {
|
||||
const key = makeMetricsKey(result.appVersion, result.platform);
|
||||
const buildNumbers = options?.buildNumbersMap?.get(key);
|
||||
const versionLabel = buildNumbers?.length
|
||||
? `${result.appVersion} (${buildNumbers.join(', ')})`
|
||||
: result.appVersion;
|
||||
|
||||
const updateIds = options?.updateIdsMap?.get(key);
|
||||
const updatesLabel = updateIds?.length ? updateIds.join(', ') : '';
|
||||
|
||||
const metricCells: string[] = [];
|
||||
for (const m of metricNames) {
|
||||
const values = result.metrics[m];
|
||||
@@ -272,8 +274,9 @@ export function buildObserveMetricsTable(
|
||||
}
|
||||
}
|
||||
}
|
||||
return [versionLabel, ...(hasUpdates ? [updatesLabel] : []), ...metricCells];
|
||||
});
|
||||
|
||||
rows.push([versionLabel, ...metricCells]);
|
||||
}
|
||||
|
||||
let footerRow: string[] | undefined;
|
||||
if (options?.totalEventCounts) {
|
||||
@@ -283,7 +286,7 @@ export function buildObserveMetricsTable(
|
||||
countCells.push(count != null ? count.toLocaleString() : '-');
|
||||
}
|
||||
if (countCells.some(c => c !== '-')) {
|
||||
footerRow = ['Total events', ...(hasUpdates ? [''] : []), ...countCells];
|
||||
footerRow = ['Total events', ...countCells];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user