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:
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
- 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.
- Re-run
eas metadata:push --profile production --non-interactive until the run completes with no Failed … markers in stdout.
- 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.
- 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[<displayType>] arrays come back in a different order from what's in the local file.
- Hit App Store Connect directly with the same
.p8 key:
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.
- PATCH the relationship in the desired order:
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
- 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.
- 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.
- 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:
The relevant logic for verifying drift looks roughly like:
# 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.
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:pushruns in which the screenshot set on App Store Connect ends up matching the localstore.config.jsonby filename + filesize but in a different order, EAS CLI no longer reorders the set. Subsequent "clean" pushes never callscreenshotSet.reorderScreenshotsAsync, never print anyUpdating screenshots …/Uploaded screenshot …/Deleted screenshot …lines, and exit 0 — yet the App Store Connect dashboard continues to show the wrong order. A directPATCH /v1/appScreenshotSets/{setId}/relationships/appScreenshotsagainst 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.tsis:Empirically
currentIdshere 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 thatAppScreenshotSet.infoAsync({ id })(in@expo/apple-utils) returnsattributes.appScreenshotsin 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 furthereas metadata:pushwill ever fix it.Managed or bare?
bare (a native Swift / SwiftUI iOS project that uses EAS only for
metadata:*)Environment
store.config.jsonhas 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
eas metadata:pushhas previously had transientFailed uploading screenshot …orFailed deleting screenshot …lines for one or more(locale × screenshotDisplayType)pairs.eas metadata:push --profile production --non-interactiveuntil the run completes with noFailed …markers in stdout.Updating screenshots …/Uploading screenshot …/Deleted screenshot …/Reordering …lines, onlyUpdating localized info …/Updating localized version …/Updating age rating declaration/Updating store review details for …/Updating version and release info …. Exit code: 0.eas metadata:pull --profile production --non-interactiveand diff against a saved copy of the localstore.config.json. The diff will show that some pairs'screenshots[<displayType>]arrays come back in a different order from what's in the local file..p8key: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]=fileNameThe 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.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
eas metadata:push --non-interactiveinvocations, all exit 0, all reaching the end of the run with noFailed …markers.screenshot|reorder|uploaded|deletedlines in stdout (only the localization / age-rating / store-review / version steps).(locale × screenshotDisplayType)pairs still had the wrong order. Locales affected:en-US,es-ES,fr-FR,it,ja,ko,ru,zh-Hans,zh-Hant— forAPP_IPAD_PRO_3GEN_129and/orAPP_IPHONE_67.PATCH /v1/appScreenshotSets/{setId}/relationships/appScreenshotsrepaired all 14 pairs in one pass; a follow-up read-only check showeddrift=0.Suspected root cause
In
syncScreenshotSetAsync(packages/eas-cli/src/metadata/apple/tasks/screenshots.ts),currentIdsis read fromrefreshedSet.attributes.appScreenshotsafterAppScreenshotSet.infoAsync({ id: screenshotSet.id }). If@expo/apple-utilsresolves thatappScreenshotsto-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 ordereddataarray used to render the dashboard) and compare against that.Suggested fix directions
syncScreenshotSetAsync, fetch the canonical screenshot order from the relationship endpoint (/v1/appScreenshotSets/{setId}/relationships/appScreenshots) instead of trusting the include order oninfoAsync. CompareorderedIdsagainst that canonical order.reorderScreenshotsAsyncwhenever 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.--verify-screenshotsfollow-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 +
cryptographyonly — uses the same.p8key already configured ineas.json) that:store.config.jsonEAS already consumes;--check(read-only diff against the liveappScreenshotSetsrelationship; 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:
The relevant logic for verifying drift looks roughly like:
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.jsonorder was definitely the ground truth andeas metadata:push's reorder check was a false negative.The right fix is for
eas metadata:pushto 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 forand exits 0.