Hmbown/CodeWhale · error

The downloaded app has a different version.

Error message

The downloaded app has a different version.

What it means

After extraction, prepareUpdate() reads CFBundleShortVersionString from the staged app bundle's Info.plist via /usr/libexec/PlistBuddy and requires it to equal update.version. A mismatch (or PlistBuddy failing) means the bundle inside the verified archive does not carry the version the release metadata promised, so the update is aborted and the staging directory removed.

Solutions

  1. Re-run the update check and retry — if the release was repaired and re-uploaded, the new archive will match its declared version.
  2. Verify on the release page that the asset's bundle Info.plist CFBundleShortVersionString equals the tag version; if not, the release itself is mis-cut and must be re-published.
  3. If testing with a locally built zip, rebuild it so the bundle's CFBundleShortVersionString matches the version you pass in the descriptor.
  4. Confirm the zip contains `<APP_NAME>.app` at its root with a valid Contents/Info.plist.
Defensive patterns

Strategy: validation

Try / catch

try { await prepareUpdate(update); } catch (e) { if (/different version/.test(e.message)) { /* re-check for updates; if it persists, the published release is mis-cut — report it */ } else throw e; }

Prevention

When it happens

Trigger: The release zip contains a bundle built from a different commit than the tagged version (mis-cut release); update.version was bumped in metadata without rebuilding the app; PlistBuddy exits non-zero because Info.plist is missing or malformed inside the bundle; the zip's top-level app name differs from APP_NAME so verifyReleaseBundle/pickup picked an unexpected bundle.

Common situations: A release pipeline that re-tagged a version without re-archiving; CI caching a stale .app artifact into the release zip; hand-repacked update zips for testing; a partially uploaded asset from an interrupted release run.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/8d06b533c19ae78f. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/plugins/computer-use/app/updates.mjs:107

  for(let redirects=0;redirects<4;redirects++) {
    response=await fetch(url,{redirect:"manual",signal:AbortSignal.timeout(60_000)});
    if(![301,302,303,307,308].includes(response.status)) break;
    const next=new URL(response.headers.get("location"),url);
    if(next.protocol!=="https:"||!["github.com","release-assets.githubusercontent.com","objects.githubusercontent.com"].includes(next.hostname)) throw new Error("The update download redirected to an unexpected host.");
    url=next.href;
  }
  if(!response?.ok) throw new Error("The update could not be downloaded. Your current app is unchanged.");
  const bytes=await responseBytes(response,update.size);
  if(bytes.length!==update.size||crypto.createHash("sha256").update(bytes).digest("hex")!==update.sha256) throw new Error("The update checksum did not match. Your current app is unchanged.");
  validateReleaseZip(bytes);
  const stage=fs.mkdtempSync(path.join(os.tmpdir(),"codewhale-cu-release-"));
  try {
    const archive=path.join(stage,"release.zip"); fs.writeFileSync(archive,bytes,{mode:0o600});
    const result=spawnSync("ditto",["-x","-k",archive,stage],{encoding:"utf8"});
    if(result.status!==0) throw new Error("The update could not be unpacked.");
    const bundle=path.join(stage,`${APP_NAME}.app`); verifyReleaseBundle(bundle);
    const version=spawnSync("/usr/libexec/PlistBuddy",["-c","Print :CFBundleShortVersionString",path.join(bundle,"Contents","Info.plist")],{encoding:"utf8"});
    if(version.status!==0||version.stdout.trim()!==update.version) throw new Error("The downloaded app has a different version.");
    return {stage,bundle};
  } catch(error) { fs.rmSync(stage,{recursive:true,force:true}); throw error; }
}

export async function restartWithUpdate(prepared,destination) {
  const logDir=path.join(os.homedir(),"Library","Logs",APP_NAME); fs.mkdirSync(logDir,{recursive:true});
  const log=fs.openSync(path.join(logDir,"update.log"),"a",0o600);
  const child=spawn(process.execPath,[fileURLToPath(import.meta.url),"--apply",prepared.bundle,destination,String(process.pid),String(process.ppid)],{detached:true,stdio:["ignore",log,log]});
  try { await new Promise((resolve,reject)=>{child.once("spawn",resolve);child.once("error",reject);}); child.unref(); }
  finally { fs.closeSync(log); }
}

if(process.argv[1]===fileURLToPath(import.meta.url)&&process.argv[2]==="--apply") {
  const [source,destination,owner,launcher]=process.argv.slice(3);
  let result;
  try {
    verifyReleaseBundle(source);
    process.kill(Number(owner),"SIGTERM");

View on GitHub (pinned to 73e0f67d83)