Skip to content

metadata:push: exit code does not reflect screenshot upload / delete / reorder failures #3689

@Maples7

Description

@Maples7

Build/Submit details page URL

Not applicable — this is about eas metadata:push, not a build or submit job.

Summary

eas metadata:push exits 0 even when stdout contains explicit per-asset failure markers such as:

  • Failed uploading screenshot <name> (<locale>)
  • Failed deleting screenshot <name> (<locale>)
  • Failed reordering screenshots (<locale>)
  • Failed creating screenshot set for <displayType> (<locale>)

The result is that any caller relying on $? to gate retries / CI promotion / "did the push succeed?" sees success while large parts of the screenshot phase silently failed. To work around this in a real release pipeline, callers have to write a stdout-aware retry wrapper that greps for the failure markers themselves and re-runs eas metadata:push --non-interactive until a clean run is observed.

The cause is in packages/eas-cli/src/metadata/apple/tasks/screenshots.ts. Each upload/delete/reorder is wrapped in logAsync(...) whose failure: text is rendered if the underlying promise rejects:

const newScreenshot = await logAsync(
  () =>
    AppScreenshot.uploadAsync(localization.context, {
      id: screenshotSet!.id,
      filePath: absolutePath,
      waitForProcessing: true,
    }),
  {
    pending: `Uploading screenshot ${chalk.bold(fileName)} (${locale})...`,
    success: `Uploaded screenshot ${chalk.bold(fileName)} (${locale})`,
    failure: `Failed uploading screenshot ${chalk.bold(fileName)} (${locale})`,
  }
);

logAsync either rethrows (in which case the for (const absolutePath of pathsToUpload) loop in syncScreenshotSetAsync aborts that single (locale × screenshotDisplayType) pair, but the surrounding outer loop continues to the next pair) or — more relevantly here — the outer pushAsync driver does not treat per-pair throws as a non-zero exit signal. The end result is "many pairs failed loudly, exit code says everything is fine".

Either way: when stdout has any Failed (uploading|deleting|reordering|creating) … line, the only correct exit code is non-zero.

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 = up to 34 screenshot sets, ~10 screenshots each. Apple's screenshot upload / S3 presigned URL pipeline produces transient 503 / 403 / Unexpected response errors at this scale, so per-asset retries are common and unavoidable.

Reproducible demo or steps to reproduce

  1. Configure store.config.json with a non-trivial number of (locale × displayType) pairs (e.g. 10+ locales × 2 device types × 8–10 screenshots per set).

  2. Run eas metadata:push --profile production --non-interactive 2>&1 | tee /tmp/eas_push.log.

  3. With a real Apple account at this scale, at least one of the upload / delete / reorder calls will fail transiently. Verify with:

    grep -E 'Failed (uploading|deleting|reordering|creating)' /tmp/eas_push.log
  4. Observe exit code:

    echo $?
    # 0
  5. The failures are real — repeating the push will re-attempt them, and a metadata:pull will reveal that the affected sets are inconsistent with store.config.json.

Concrete numbers from the project where I hit this

I had to wrap eas metadata:push in a retry loop that re-runs the command until stdout has zero Failed (uploading|deleting|reordering|creating) … markers, because exit 0 alone was meaningless. The loop ran 21 attempts before stdout was finally clean. Every single attempt exited 0.

Excerpts from the actual log (one of 21 attempts that exited 0 with failures in stdout):

✖ Failed uploading screenshot 07-events-2048x2732.png (es-ES)
…
✖ Failed deleting screenshot 02-add-2048x2732.png (it)
…
=== EXIT=0 ===

Suggested fix

In packages/eas-cli/src/metadata/apple/tasks/screenshots.ts (and any sibling task that uses the same logAsync failure pattern):

  1. Track per-pair failures inside syncScreenshotSetAsync. Instead of letting logAsync(...) rethrow and abort the inner for loop, capture the error, increment a counter on the task context, and continue. After the outer for (const localeCode of locales) loop completes, throw an aggregate error if the counter is non-zero.
  2. Have pushAsync (packages/eas-cli/src/metadata/apple/index.ts or wherever the task runner aggregates results) propagate that aggregate error to its caller so MetadataPush can throw new MetadataUploadError(...) and exit non-zero.
  3. Independently, add a final if (failureCount > 0) summary line and have the process exit non-zero whenever it is non-zero. This is a small, behavior-preserving change for the success path and a strict improvement for the failure path.

A rough shape of the fix in syncScreenshotSetAsync:

const failures: Error[] = [];

for (const absolutePath of pathsToUpload) {
  try {
    const newScreenshot = await logAsync(/* … */);
    screenshotIdsToKeep.push(newScreenshot.id);
  } catch (error: any) {
    failures.push(error);
  }
}

if (failures.length > 0) {
  throw new AggregateMetadataError(
    `Encountered ${failures.length} screenshot ${failures.length === 1 ? 'error' : 'errors'} for ${displayType} (${locale})`,
    failures
  );
}

…with the corresponding adjustment in the outer driver to collect / rethrow these so the process exit code is non-zero.

Workaround we are using

A bash retry wrapper that runs eas metadata:push --non-interactive, captures stdout to a log, and only treats the run as successful when both:

  1. exit code is 0; and
  2. stdout contains zero Failed (uploading|deleting|reordering|creating) … markers.

Otherwise it retries (the underlying push step is idempotent — completed assets match by filename + filesize and are skipped on the next attempt).

If it would help, we can share the wrapper as a reference, but the right fix is for eas metadata:push to make stdout-grep unnecessary.

Error output

Per-asset stdout entries like:

✖ Failed uploading screenshot 07-events-2048x2732.png (es-ES)
✖ Failed deleting screenshot 02-add-2048x2732.png (it)

…with echo $? reporting 0 on the parent process.

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