# PR 3523: Address Upload Error Masking

## Analysis

### Impact

| Area | Current behavior | Intended behavior | Verification |
|---|---|---|---|
| Manifest upload error reporting | A producer-side pipe error is returned before the S3 uploader error, even when the producer error was caused by the failed upload. | Join uploader and producer failures, with the uploader error first, so neither root cause is hidden. | Targeted and app-wide tests. |
| Backup-export workflow | The workflow receives whichever error `GenerateAndUploadManifest` selects. | The workflow receives an actionable S3 error for upload failures without changing success, empty-manifest, or dry-run behavior. | Existing AWS facade and workflow action tests. |

### Symbols to Change

- Join uploader and producer errors at the AWS Batch Operations facade boundary.

### Dead Code Cascade

None. No interface, generated code, or workflow symbol is removed.

## Execution Steps

1. Join uploader and producer failures after both sides complete.
   - Put the uploader error first so the actionable S3 failure leads the message.
   - Preserve both errors through `errors.Join` and `%w`.
   - Success criterion: neither error is dropped, and nil errors are ignored.
2. Run workflow and broader app verification.
   - Run `go test ./app/workflow/action/backup_export/aws`.
   - Run `go test ./app/...` if the targeted packages pass.
   - Run `gofmt` on edited Go files and `git diff --check`.
   - Expected result: all commands exit successfully with no formatting errors.
3. Re-fetch review threads and confirm the actionable thread remains resolved.

## Risks

- Joined errors can contain a secondary pipe error as well as the uploader failure, but placing the uploader error first keeps the actionable cause prominent.
- Error wrapping must retain `errors.Is` behavior for callers.
