Read both JSON documents altool prints with --wait
With --wait, Xcode 27's altool writes two JSON documents to stdout: the build's processing status, then the upload result. Parsing that as one value failed, so the output came back as a plain string and delivery_id was null even on a successful upload. That happened on the first real upload, smbcloud-mailx visionOS 1.1.0 (1791527508). The output is now read as a stream of documents and returned as an array when there is more than one, so delivery_id is found again. The delivery UUID is also the build's App Store Connect id, and the field docs now say so. The new test uses the document shape from that upload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seto Elkahfi committed
Oct 9, 2026 at 08:36 UTC
d8342adbed4c601fe10f48bbd466f31d8e26e6dd
1 file changed
+48
-6
crates/frontend/src/upload.rs
+48
-6
@@ -37,10 +37,11 @@ pub struct UploadOutcome {
37
/// altool's exit status; `None` if it was killed by a signal.
38
pub exit_code: Option<i32>,
39
/// The delivery UUID altool reports for a successful upload, if its
40
- /// output carried one. `altool --build-status --delivery-id` takes it.
40
+ /// output carried one. It is also the build's App Store Connect id, so
41
+ /// `ascapi apps build <delivery_id>` reads the build back.
42
pub delivery_id: Option<String>,
42
- /// altool's `--output-format json` body when it parsed, otherwise its
43
- /// stdout as a string.
43
+ /// altool's `--output-format json` body when it parsed (an array of
44
+ /// both documents with `--wait`), otherwise its stdout as a string.
45
pub output: serde_json::Value,
46
}
47
@@ -122,9 +123,7 @@ pub fn upload_package(
123
}
124
125
fn outcome(package: &Path, exit_code: Option<i32>, stdout: &[u8]) -> UploadOutcome {
125
- let output = serde_json::from_slice::<serde_json::Value>(stdout).unwrap_or_else(|_| {
126
- serde_json::Value::String(String::from_utf8_lossy(stdout).trim().to_string())
127
- });
126
+ let output = parse_output(stdout);
127
UploadOutcome {
128
package: package.display().to_string(),
129
success: exit_code == Some(0),
@@ -134,6 +133,21 @@ fn outcome(package: &Path, exit_code: Option<i32>, stdout: &[u8]) -> UploadOutco
133
}
134
}
135
136
+/// With `--wait`, altool writes two JSON documents back to back: the build's
137
+/// processing status, then the upload result. Read them as a stream and
138
+/// return an array when there is more than one. Anything that isn't JSON
139
+/// is kept as text.
140
+fn parse_output(stdout: &[u8]) -> serde_json::Value {
141
+ let documents: Result<Vec<serde_json::Value>, _> = serde_json::Deserializer::from_slice(stdout)
142
+ .into_iter()
143
+ .collect();
144
+ match documents {
145
+ Ok(mut documents) if documents.len() == 1 => documents.remove(0),
146
+ Ok(documents) if !documents.is_empty() => serde_json::Value::Array(documents),
147
+ _ => serde_json::Value::String(String::from_utf8_lossy(stdout).trim().to_string()),
148
+ }
149
+}
150
+
151
/// altool has spelled this key differently across releases
152
/// (`delivery-uuid` in the legacy tool), so search the body for any of
153
/// the spellings rather than pinning one path.
@@ -209,6 +223,34 @@ mod tests {
223
assert_eq!(outcome.delivery_id.as_deref(), Some("d1e2f3"));
224
}
225
226
+ /// The shape Xcode 27's altool prints with `--wait`, trimmed from a real
227
+ /// smbcloud-mailx visionOS upload: a status document, then the upload
228
+ /// document, with no separator between them.
229
+ #[test]
230
+ fn reads_both_documents_altool_prints_with_wait() {
231
+ let body = br#"{
232
+ "build-status" : "VALID",
233
+ "bundle-short-version-string" : "1.1.0",
234
+ "bundle-version" : "1791527508",
235
+ "delivery-uuid" : "3a12861d-5afd-4fc1-8b2d-96c80e055b1c"
236
+}
237
+{
238
+ "details" : {
239
+ "delivery-uuid" : "3a12861d-5afd-4fc1-8b2d-96c80e055b1c"
240
+ },
241
+ "success-message" : "No errors uploading archive"
242
+}
243
+"#;
244
+ let outcome = outcome(Path::new("/out/MailXVision.ipa"), Some(0), body);
245
+ assert_eq!(
246
+ outcome.delivery_id.as_deref(),
247
+ Some("3a12861d-5afd-4fc1-8b2d-96c80e055b1c")
248
+ );
249
+ let documents = outcome.output.as_array().expect("two documents");
250
+ assert_eq!(documents.len(), 2);
251
+ assert_eq!(documents[0]["build-status"], "VALID");
252
+ }
253
+
254
#[test]
255
fn keeps_non_json_output_as_text_and_reports_failure() {
256
let outcome = outcome(