metadata:push: exit code does not reflect screenshot upload / delete / reorder failures
- 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
`eas metadata:push` exits 0 even when stdout contains explicit per-asset failure markers such as:
- `Failed uploading screenshot ()`
- `Failed deleting screenshot ()`
- `Failed reordering screenshots ()`
- `Failed creating screenshot set for ()`
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:
```ts
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:
```bash
grep -E 'Failed (uploading|deleting|reordering|creating)' /tmp/eas_push.log
```
4. Observe exit code:
```bash
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`:
```ts
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.
Contributor guide
Assessment
This issue has not been assessed yet.