metadata:push: screenshot reorder is skipped while App Store Connect order still differs from store.config.json
- Dominant language
- TypeScript
- Stars
- 1.4k
- Forks
- 236
- Avg merge
- 3d 1h
- Merged PRs (30d)
- 91
Description
### Build/Submit details page URL
_Not applicable — this is about `eas metadata:push`, not a build or submit job._
### Summary
After a sequence of `eas metadata:push` runs in which the screenshot set on App Store Connect ends up matching the local `store.config.json` by **filename + filesize** but in a **different order**, EAS CLI no longer reorders the set. Subsequent "clean" pushes never call `screenshotSet.reorderScreenshotsAsync`, never print any `Updating screenshots …` / `Uploaded screenshot …` / `Deleted screenshot …` lines, and exit 0 — yet the App Store Connect dashboard continues to show the wrong order. A direct `PATCH /v1/appScreenshotSets/{setId}/relationships/appScreenshots` against the same App Store Connect API key fixes it instantly, which proves the desired vs current order really do disagree on App Store Connect's side.
The reorder logic in `packages/eas-cli/src/metadata/apple/tasks/screenshots.ts` is:
```ts
const refreshedSet = await AppScreenshotSet.infoAsync(localization.context, {
id: screenshotSet.id,
});
const refreshedScreenshots = refreshedSet.attributes.appScreenshots || [];
…
const currentIds = refreshedScreenshots.map(s => s.id);
if (
orderedIds.length > 0 &&
(orderedIds.length !== currentIds.length || orderedIds.some((id, i) => id !== currentIds[i]))
) {
await screenshotSet.reorderScreenshotsAsync({ appScreenshots: orderedIds });
}
```
Empirically `currentIds` here does not always match the order shown in App Store Connect, so the equality check returns true (no reorder) while App Store Connect itself still has the old order. My best guess is that `AppScreenshotSet.infoAsync({ id })` (in `@expo/apple-utils`) returns `attributes.appScreenshots` in App Store Connect's natural / default order rather than the ordered relationship order. Whatever the root cause, the practical effect is that once a partial run has scrambled a `(locale × screenshotDisplayType)` pair, no further `eas metadata:push` will ever fix it.
### Managed or bare?
bare (a native Swift / SwiftUI iOS project that uses EAS only for `metadata:*`)
### Environment
```
eas-cli: 18.11.0 darwin-arm64 node-v22.13.1
node: v22.13.1
macOS: 26.4.1 (arm64)
```
`store.config.json` has 17 localizations × 2 device types (iPhone 6.9", iPad Pro 13") = up to 34 screenshot sets, ~10 screenshots each.
### Reproducible demo or steps to reproduce
1. On a project where `eas metadata:push` has previously had transient `Failed uploading screenshot …` or `Failed deleting screenshot …` lines for one or more `(locale × screenshotDisplayType)` pairs.
2. Re-run `eas metadata:push --profile production --non-interactive` until the run completes with no `Failed …` markers in stdout.
3. Re-run it again. Observe that the second run has **zero** `Updating screenshots …` / `Uploading screenshot …` / `Deleted screenshot …` / `Reordering …` lines, only `Updating localized info …` / `Updating localized version …` / `Updating age rating declaration` / `Updating store review details for …` / `Updating version and release info …`. Exit code: 0.
4. Run `eas metadata:pull --profile production --non-interactive` and diff against a saved copy of the local `store.config.json`. The diff will show that some pairs' `screenshots[]` arrays come back in a different order from what's in the local file.
5. Hit App Store Connect directly with the same `.p8` key:
```bash
GET /v1/apps/{appId}/appStoreVersions
?filter[platform]=IOS
&filter[appStoreState]=PREPARE_FOR_SUBMISSION,READY_FOR_REVIEW,…
GET /v1/appStoreVersions/{versionId}/appStoreVersionLocalizations
GET /v1/appStoreVersionLocalizations/{locId}/appScreenshotSets
GET /v1/appScreenshotSets/{setId}/appScreenshots?fields[appScreenshots]=fileName
```
The screenshot order returned by the last call is the App Store Connect-displayed order, and on affected pairs it does not match `store.config.json`.
6. PATCH the relationship in the desired order:
```bash
PATCH /v1/appScreenshotSets/{setId}/relationships/appScreenshots
{ "data": [ {"type":"appScreenshots","id":"…"}, … ] }
```
The dashboard immediately reflects the new order. No re-upload required.
### Concrete numbers from the project where I hit this
- 21 consecutive `eas metadata:push --non-interactive` invocations, all exit 0, all reaching the end of the run with no `Failed …` markers.
- Last 4 runs: zero `screenshot|reorder|uploaded|deleted` lines in stdout (only the localization / age-rating / store-review / version steps).
- Independent verification via App Store Connect API: 14 `(locale × screenshotDisplayType)` pairs still had the wrong order. Locales affected: `en-US`, `es-ES`, `fr-FR`, `it`, `ja`, `ko`, `ru`, `zh-Hans`, `zh-Hant` — for `APP_IPAD_PRO_3GEN_129` and/or `APP_IPHONE_67`.
- Direct `PATCH /v1/appScreenshotSets/{setId}/relationships/appScreenshots` repaired all 14 pairs in one pass; a follow-up read-only check showed `drift=0`.
### Suspected root cause
In `syncScreenshotSetAsync` (`packages/eas-cli/src/metadata/apple/tasks/screenshots.ts`), `currentIds` is read from `refreshedSet.attributes.appScreenshots` after `AppScreenshotSet.infoAsync({ id: screenshotSet.id })`. If `@expo/apple-utils` resolves that `appScreenshots` to-many relationship with App Store Connect's default ordering rather than the explicit relationship ordering, the array order will not always reflect what users see in App Store Connect, and the equality check will short-circuit reorder when it shouldn't.
A safer signal is to read the relationship explicitly via `GET /v1/appScreenshotSets/{setId}/relationships/appScreenshots` (which returns the canonical ordered `data` array used to render the dashboard) and compare against that.
### Suggested fix directions
1. In `syncScreenshotSetAsync`, fetch the canonical screenshot order from the **relationship endpoint** (`/v1/appScreenshotSets/{setId}/relationships/appScreenshots`) instead of trusting the include order on `infoAsync`. Compare `orderedIds` against that canonical order.
2. As a safer baseline, always call `reorderScreenshotsAsync` whenever the set is non-empty, even if the local order looks like it already matches. The endpoint is idempotent and the cost is one PATCH per affected pair.
3. Independently, consider adding a `--verify-screenshots` follow-up step (or a non-zero exit when the post-run order check still mismatches) so this class of drift surfaces without users having to write their own App Store Connect API helpers.
### Workaround we are using
A small App Store Connect API helper (Python, stdlib + `cryptography` only — uses the same `.p8` key already configured in `eas.json`) that:
- reads desired order from the same `store.config.json` EAS already consumes;
- supports `--check` (read-only diff against the live `appScreenshotSets` relationship; exits non-zero on drift) and `--fix` (PATCH the relationship in the desired order).
It is open-sourced as part of an unrelated skills repo and is fully generic — no project-specific identifiers:
- Code:
The relevant logic for verifying drift looks roughly like:
```python
# Pull the canonical relationship order, not the include order.
current = client.get_paginated(
f"/v1/appScreenshotSets/{set_id}/appScreenshots",
params={"fields[appScreenshots]": "fileName"},
)
current_order = [s["attributes"]["fileName"] for s in current]
if current_order != desired_order:
client.patch(
f"/v1/appScreenshotSets/{set_id}/relationships/appScreenshots",
json={"data": [{"type": "appScreenshots", "id": _id_for(name)} for name in desired_order]},
)
```
In our project this repaired all 14 drifted pairs in a single pass with no re-uploads, which is also what convinced me the local `store.config.json` order was definitely the ground truth and `eas metadata:push`'s reorder check was a false negative.
The right fix is for `eas metadata:push` to make this helper unnecessary.
### Error output
_Not applicable — this is a silent false-negative; there is no error output. The run prints only `Updated localized info …` / `Updated localized version …` / `Updated age rating declaration` / `Updated app categories` / `Updated store review details for` / `Updated version and release info for` and exits 0._
Contributor guide
Assessment
This issue has not been assessed yet.