Skip to main content

max / makenotwork

ota: add --release-id to mnw-cli ota publish (resume failed upload) When an artifact upload fails after the release row is already created (e.g. storage was misconfigured), re-running publish would 409 on the duplicate version. --release-id <uuid> skips create_release and registers/uploads the artifact to the existing release instead. Adds the uuid dep. Used to recover the goingson 0.4.1 release after the prod SyncKit-S3 config was added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-08 03:54 UTC
Commit: b0590376587a52e66545c2e1de61f366c325ff72
Parent: 3befb8c
3 files changed, +32 insertions, -7 deletions
@@ -1995,6 +1995,7 @@ dependencies = [
1995 1995 "tokio",
1996 1996 "tracing",
1997 1997 "tracing-subscriber",
1998 + "uuid",
1998 1999 ]
1999 2000
2000 2001 [[package]]
@@ -17,3 +17,4 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
17 17 anyhow = "1"
18 18 bytes = "1"
19 19 synckit-client = { path = "../shared/synckit-client", default-features = false }
20 + uuid = "1"
@@ -53,6 +53,8 @@ fn print_usage() {
53 53 Optional:\n\
54 54 \x20 --notes Release notes (default: empty)\n\
55 55 \x20 --signature Minisign signature for Tauri verification (REQUIRED for a working update)\n\
56 + \x20 --release-id Attach to an existing release UUID instead of creating one\n\
57 + \x20 (resume a publish whose artifact upload failed)\n\
56 58 \x20 --email MNW account email (env MNW_OTA_EMAIL; password auth only)\n\
57 59 \x20 --password MNW account password (env MNW_OTA_PASSWORD; enables password auth)\n\
58 60 \x20 --server Server URL (env MNW_OTA_SERVER, default {DEFAULT_SERVER})",
@@ -69,6 +71,9 @@ struct PublishArgs {
69 71 artifact: PathBuf,
70 72 notes: String,
71 73 signature: String,
74 + // When set, attach the artifact to this existing release instead of creating
75 + // one (resume a failed upload; create would 409 on a duplicate version).
76 + release_id: Option<String>,
72 77 // email + password drive the legacy password auth path. When absent, the
73 78 // publisher uses the interactive MNW OAuth flow (the default; required for
74 79 // accounts with 2FA, which the password endpoint rejects).
@@ -91,6 +96,7 @@ impl std::fmt::Debug for PublishArgs {
91 96 .field("artifact", &self.artifact)
92 97 .field("notes", &self.notes)
93 98 .field("signature", &self.signature)
99 + .field("release_id", &self.release_id)
94 100 .field("email", &self.email)
95 101 .field("password", &self.password.as_ref().map(|_| "<redacted>"))
96 102 .field("api_key", &"<redacted>")
@@ -109,6 +115,7 @@ fn parse_args(flags: &[String]) -> Result<PublishArgs> {
109 115 let mut artifact = None;
110 116 let mut notes = String::new();
111 117 let mut signature = String::new();
118 + let mut release_id = None;
112 119 let mut email = std::env::var("MNW_OTA_EMAIL").ok();
113 120 let mut password = std::env::var("MNW_OTA_PASSWORD").ok();
114 121 let mut api_key = std::env::var("MNW_OTA_API_KEY").ok();
@@ -130,6 +137,7 @@ fn parse_args(flags: &[String]) -> Result<PublishArgs> {
130 137 "--artifact" => artifact = Some(PathBuf::from(take("--artifact")?)),
131 138 "--notes" => notes = take("--notes")?,
132 139 "--signature" => signature = take("--signature")?,
140 + "--release-id" => release_id = Some(take("--release-id")?),
133 141 "--email" => email = Some(take("--email")?),
134 142 "--password" => password = Some(take("--password")?),
135 143 "--api-key" => api_key = Some(take("--api-key")?),
@@ -162,6 +170,7 @@ fn parse_args(flags: &[String]) -> Result<PublishArgs> {
162 170 artifact: artifact.ok_or_else(|| missing("--artifact"))?,
163 171 notes,
164 172 signature,
173 + release_id,
165 174 email, // optional: only used by the password auth path
166 175 password,
167 176 api_key: api_key.ok_or_else(|| missing("--api-key / MNW_OTA_API_KEY"))?,
@@ -227,16 +236,30 @@ async fn publish(flags: &[String]) -> Result<()> {
227 236 .unwrap_or_default();
228 237 println!(" authenticated (app {app_id})");
229 238
230 - print!(" creating release v{}... ", args.version);
231 - let release = client
232 - .ota_create_release(&args.version, &args.notes, &args.signature)
233 - .await
234 - .context("create release failed")?;
235 - println!("ok (release {})", release.id);
239 + // With --release-id, attach to an existing release (resume a failed upload)
240 + // instead of creating one — create would 409 on a duplicate version.
241 + let release_id = match &args.release_id {
242 + Some(rid) => {
243 + let id = rid
244 + .parse::<uuid::Uuid>()
245 + .context("--release-id must be a UUID")?;
246 + println!(" using existing release {id} (skipping create)");
247 + id
248 + }
249 + None => {
250 + print!(" creating release v{}... ", args.version);
251 + let release = client
252 + .ota_create_release(&args.version, &args.notes, &args.signature)
253 + .await
254 + .context("create release failed")?;
255 + println!("ok (release {})", release.id);
256 + release.id
257 + }
258 + };
236 259
237 260 print!(" registering artifact... ");
238 261 let upload = client
239 - .ota_register_artifact(release.id, &args.target, &args.arch, file_size)
262 + .ota_register_artifact(release_id, &args.target, &args.arch, file_size)
240 263 .await
241 264 .context("register artifact failed")?;
242 265 println!("ok ({})", upload.s3_key);