Skip to content

metadata:push: screenshot reorder is skipped while App Store Connect order still differs from store.config.json #3690

@Maples7

Description

@Maples7

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

  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[<displayType>] 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:
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.

  1. 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

  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:

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions