Skip to content

Commit 97f2e3e

Browse files
committed
feat(release): enhance appcast generation and delta upload handling
1 parent f95cac0 commit 97f2e3e

3 files changed

Lines changed: 104 additions & 32 deletions

File tree

scripts/commands/release.ts

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { Command } from "commander";
22
import { spawn, spawnSync } from "node:child_process";
3-
import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
3+
import {
4+
copyFileSync,
5+
existsSync,
6+
readFileSync,
7+
readdirSync,
8+
writeFileSync,
9+
} from "node:fs";
410
import path from "node:path";
511
import chalk from "chalk";
612
import {
@@ -346,11 +352,23 @@ export const releaseCommand = new Command("release")
346352
"bin",
347353
"generate_appcast"
348354
);
355+
// --download-url-prefix pins the host used in every <enclosure url=...>
356+
// generate_appcast writes (for both the full .dmg and every .delta it
357+
// synthesizes). Without this, generate_appcast inherits the prefix
358+
// from the previous enclosure in the seeded appcast, which means a
359+
// single bad legacy entry (or a host migration) silently propagates
360+
// forward. Artifacts ship from dao-release.msgbyte.com — keep this
361+
// in sync with website/public/appcast.xml and the R2 bucket's
362+
// custom-domain binding.
349363
await runStep(
350364
opts.dryRun,
351365
"Generating Sparkle appcast",
352366
generateAppcast,
353-
[path.join(ROOT_DIR, "dist")]
367+
[
368+
"--download-url-prefix",
369+
"https://dao-release.msgbyte.com/",
370+
path.join(ROOT_DIR, "dist"),
371+
]
354372
);
355373

356374
// ------------------------------------------------------------------
@@ -411,15 +429,64 @@ export const releaseCommand = new Command("release")
411429
}
412430

413431
// ------------------------------------------------------------------
414-
// Step 6 — upload .dmg to R2
432+
// Step 6 — upload .dmg + every .delta in dist/ to R2
433+
//
434+
// generate_appcast writes signed <enclosure> entries for delta
435+
// patches alongside the full dmg; the appcast advertises both. If
436+
// we don't upload the .delta files, clients fetch the appcast,
437+
// pick a delta matching their current version, and hit a 404 — at
438+
// which point Sparkle reports "Update Error" instead of silently
439+
// falling back to the full dmg. So delta upload is not optional.
415440
// ------------------------------------------------------------------
416441
if (willUpload) {
417-
const uploadArgs = ["tsx", "scripts/cli.ts", "upload", dmgPath];
442+
const distDir = path.join(ROOT_DIR, "dist");
443+
444+
// Whitelist delta uploads against the freshly-regenerated appcast.
445+
// generate_appcast does prune unreferenced delta files into
446+
// dist/old_updates/, but a resumed/partial release can leave stray
447+
// .delta files in dist/ root from a previous run. Without this
448+
// filter we'd silently re-upload those orphans to R2. Parse the
449+
// appcast we just wrote and only ship delta filenames it actually
450+
// references.
451+
const referencedDeltas = !opts.dryRun
452+
? collectReferencedDeltaBasenames(appcastDest)
453+
: new Set<string>();
454+
const allDeltas = existsSync(distDir)
455+
? readdirSync(distDir).filter((f) => f.endsWith(".delta"))
456+
: [];
457+
const deltaPaths: string[] = [];
458+
const skippedDeltas: string[] = [];
459+
for (const name of allDeltas) {
460+
if (opts.dryRun || referencedDeltas.has(name)) {
461+
deltaPaths.push(path.join(distDir, name));
462+
} else {
463+
skippedDeltas.push(name);
464+
}
465+
}
466+
if (skippedDeltas.length > 0) {
467+
warn(
468+
`Skipping ${skippedDeltas.length} unreferenced delta file(s) in ` +
469+
`dist/ (not present in appcast): ${skippedDeltas.join(", ")}`
470+
);
471+
}
472+
473+
const uploadArgs = [
474+
"tsx",
475+
"scripts/cli.ts",
476+
"upload",
477+
dmgPath,
478+
...deltaPaths,
479+
];
418480
if (opts.bucket) uploadArgs.push("--bucket", opts.bucket);
419481
if (opts.prefix) uploadArgs.push("--prefix", opts.prefix);
482+
483+
const deltaSummary =
484+
deltaPaths.length > 0
485+
? ` + ${deltaPaths.length} delta(s)`
486+
: " (no referenced delta files in dist/)";
420487
await runStep(
421488
opts.dryRun,
422-
`Uploading ${dmgName} to R2`,
489+
`Uploading ${dmgName}${deltaSummary} to R2`,
423490
"npx",
424491
uploadArgs
425492
);
@@ -728,6 +795,28 @@ function injectGitCommitIntoAppcast(
728795
success(`appcast stamped with git commit ${gitCommit.slice(0, 7)}`);
729796
}
730797

798+
// Parse the just-regenerated appcast and return the set of .delta basenames
799+
// it references in any <enclosure url="...">. Used by Step 6 to whitelist
800+
// delta uploads so stale orphan files left in dist/ from a previous run
801+
// aren't shipped to R2 alongside the current release.
802+
//
803+
// Same string-surgery rationale as injectGitCommitIntoAppcast — Sparkle's
804+
// output shape is stable enough that a regex over <enclosure> tags is
805+
// sufficient without taking on an XML parser dependency.
806+
function collectReferencedDeltaBasenames(appcastPath: string): Set<string> {
807+
const referenced = new Set<string>();
808+
if (!existsSync(appcastPath)) return referenced;
809+
const xml = readFileSync(appcastPath, "utf-8");
810+
for (const m of xml.matchAll(/<enclosure\b[^>]*\burl="([^"]+)"/g)) {
811+
const url = m[1];
812+
const tail = url.split("/").pop() || "";
813+
if (tail.endsWith(".delta")) {
814+
referenced.add(tail);
815+
}
816+
}
817+
return referenced;
818+
}
819+
731820
// ---------------------------------------------------------------------------
732821
// Notarize + staple, with a useful recovery path on keychain failures
733822
// ---------------------------------------------------------------------------

scripts/commands/upload.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,10 @@ const CONTENT_TYPES: Record<string, string> = {
270270
".gz": "application/gzip",
271271
".tgz": "application/gzip",
272272
".sig": "application/octet-stream",
273+
// Sparkle binary delta patches (BinaryDelta archives produced by
274+
// generate_appcast). Served as octet-stream so browsers / proxies
275+
// don't try to sniff or transform them.
276+
".delta": "application/octet-stream",
273277
};
274278

275279
function guessContentType(filePath: string): string | null {

website/public/appcast.xml

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@
2626
bundle Info.plist. Sparkle uses this to gate updates so users on
2727
older macOS don't end up with an app they can't launch.
2828
-->
29-
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dao="https://dao.msgbyte.com/xml-namespaces/dao" version="2.0">
29+
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dao="https://dao-release.msgbyte.com/xml-namespaces/dao" version="2.0">
3030
<channel>
3131
<title>Dao Browser</title>
32-
<link>https://dao.msgbyte.com/appcast.xml</link>
32+
<link>https://dao-release.msgbyte.com/appcast.xml</link>
3333
<description>Dao Browser auto-update feed</description>
3434
<language>en</language>
3535
<!--
@@ -60,14 +60,7 @@
6060
<sparkle:version>16.0</sparkle:version>
6161
<sparkle:shortVersionString>1.0.16.0</sparkle:shortVersionString>
6262
<sparkle:minimumSystemVersion>12.0</sparkle:minimumSystemVersion>
63-
<enclosure url="https://dao.msgbyte.com/dao-browser-1.0.16-mac-arm64.dmg" length="154004604" type="application/octet-stream" sparkle:edSignature="Qjyy4k8J7r1erRXo4Z40F2mMiq5cUWnaCPIDM+0sz36QYBB9dArVAUU0QOVAF0xdWJTumFEkO1sx7fLx/6FJAw=="/>
64-
<sparkle:deltas>
65-
<enclosure url="https://dao.msgbyte.com/Dao16.0-15.0.delta" sparkle:deltaFrom="15.0" length="2765042" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="862416" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="dwQtAOOSD90K6ohbPm2+KZ3umhedRMxdvVr8OOUQahT4hQwwtfxMM8PZdLwms7kKMdUDS+OHGNeSHyPBiI7SAw=="/>
66-
<enclosure url="https://dao.msgbyte.com/Dao16.0-14.0.delta" sparkle:deltaFrom="14.0" length="5787058" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="862416" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="uDQMSfP68Dp/9O1WDHcmD6uM1PArkwrxwT6D2FAwFNriUf7546qi8ZmvfbPGwqIle2aqg2WlO6IM3t8BXR5TCw=="/>
67-
<enclosure url="https://dao.msgbyte.com/Dao16.0-13.0.delta" sparkle:deltaFrom="13.0" length="6061454" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="862416" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="YybcTgcSAPACsFfMU8M/PM+44FY1X3c/mAvkIVriEMZXMxRijkAxtZR044Eozn2NzCHvS+tm5UAfE/j3wUzDCQ=="/>
68-
<enclosure url="https://dao.msgbyte.com/Dao16.0-12.0.delta" sparkle:deltaFrom="12.0" length="6060574" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="862416" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="RrGAmg7WRDzfhndo4FrCooH+DVu2NPXHrcaitxIvayaDwcI8C1JD6zr8TsXYBC5pvOMoEc2hd2MBkEvv5ewKAQ=="/>
69-
<enclosure url="https://dao.msgbyte.com/Dao16.0-11.0.delta" sparkle:deltaFrom="11.0" length="6058858" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="862416" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="yaGaBDex1Gpxrt2/YXpvyRjE3QVRFbPwNXcxy3t0SzLLhNixH1V4O6MjWxdaNRpOBAoNyQc8+6nxfWc0RF/9DQ=="/>
70-
</sparkle:deltas>
63+
<enclosure url="https://dao-release.msgbyte.com/dao-browser-1.0.16-mac-arm64.dmg" length="154004604" type="application/octet-stream" sparkle:edSignature="Qjyy4k8J7r1erRXo4Z40F2mMiq5cUWnaCPIDM+0sz36QYBB9dArVAUU0QOVAF0xdWJTumFEkO1sx7fLx/6FJAw=="/>
7164
<dao:gitCommit>62bc43411e03eb92bd47283df3ec23f074f9495b</dao:gitCommit>
7265
</item>
7366
<item>
@@ -76,14 +69,7 @@
7669
<sparkle:version>15.0</sparkle:version>
7770
<sparkle:shortVersionString>1.0.15.0</sparkle:shortVersionString>
7871
<sparkle:minimumSystemVersion>12.0</sparkle:minimumSystemVersion>
79-
<enclosure url="https://dao.msgbyte.com/dao-browser-1.0.15-mac-arm64.dmg" length="153804003" type="application/octet-stream" sparkle:edSignature="3fkAs59bRdIC/r0OejaKdJEVUfEJ23RmzH22Tk0i/x0XSVNFMFIBee7h08kqXVS1tm+tqea4muDmCZw9SZ1QBw=="/>
80-
<sparkle:deltas>
81-
<enclosure url="https://dao.msgbyte.com/Dao15.0-14.0.delta" sparkle:deltaFrom="14.0" length="5506894" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="862416" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="5Tortf0eXxNFwEEJ6a0lLq44GAJMRGsbGtClkhXQN7DKxw/waQuAPT07Nj7Xcf3IOYn9va3L0VOeb9+UX8M0AQ=="/>
82-
<enclosure url="https://dao.msgbyte.com/Dao15.0-13.0.delta" sparkle:deltaFrom="13.0" length="5714262" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="862416" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="YOMrwKR8r5QDa/G4XBw4H2hWIarpq3OOps/gD1np9CjQs7TM9exFVtkxq/zHiaLTL5A5Qxal7rNf8eXpimeODA=="/>
83-
<enclosure url="https://dao.msgbyte.com/Dao15.0-12.0.delta" sparkle:deltaFrom="12.0" length="5713890" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="862416" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="687DUGNqfAWgAYTdar9Jttazro0W17Maw0ogvFtPQScInI8jutD/p2G477ZCWEGI8JT3ea0eRoi/tomIHOZvCQ=="/>
84-
<enclosure url="https://dao.msgbyte.com/Dao15.0-11.0.delta" sparkle:deltaFrom="11.0" length="5713814" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="862416" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="V1K4BQi8MGtYy/qGceFtZ0k10y76UHsh65WvJsjX7NfEzE1kLWaYx3wRt0+XJFtvwcfS2DHrZ62TeciqlbzgAg=="/>
85-
<enclosure url="https://dao.msgbyte.com/Dao15.0-10.0.delta" sparkle:deltaFrom="10.0" length="5919998" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="862416" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="6deSYmrJAdBuM8gQPjdwCAT+MUudxl6OsX15wLZ9r4KWcBGJhFgHFmyifH0hmX5YMZoyc4yVATwFEtFvHzO/BA=="/>
86-
</sparkle:deltas>
72+
<enclosure url="https://dao-release.msgbyte.com/dao-browser-1.0.15-mac-arm64.dmg" length="153804003" type="application/octet-stream" sparkle:edSignature="3fkAs59bRdIC/r0OejaKdJEVUfEJ23RmzH22Tk0i/x0XSVNFMFIBee7h08kqXVS1tm+tqea4muDmCZw9SZ1QBw=="/>
8773
<dao:gitCommit>9cde7fa5143d165d120749d6e6baf9ee68163b13</dao:gitCommit>
8874
</item>
8975
<item>
@@ -92,14 +78,7 @@
9278
<sparkle:version>14.0</sparkle:version>
9379
<sparkle:shortVersionString>1.0.14.0</sparkle:shortVersionString>
9480
<sparkle:minimumSystemVersion>12.0</sparkle:minimumSystemVersion>
95-
<enclosure url="https://dao.msgbyte.com/dao-browser-1.0.14-mac-arm64.dmg" length="153771120" type="application/octet-stream" sparkle:edSignature="L9Shk7E+ZvXQ1wuE4Iz08dUZEfId6ezSQp3UIq003CYeM8qifdL5NT4i2Mal7yX1UaDQCd8Ba6MH/X7BOxHtDA=="/>
96-
<sparkle:deltas>
97-
<enclosure url="https://dao.msgbyte.com/Dao14.0-13.0.delta" sparkle:deltaFrom="13.0" length="4789110" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="862416" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="+Hn6LUjNU4Q63bHeumgjuqo7LtCyvv4kqvHbYp5RNC/C4KKeF6+CfAU3LnqB9b1xuD8EkDehoM8hpigStzw3Ag=="/>
98-
<enclosure url="https://dao.msgbyte.com/Dao14.0-12.0.delta" sparkle:deltaFrom="12.0" length="4788974" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="862416" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="m6L6l1tLm7i+KYooC/urKgX3MRckJtQu6jq6e1XXszIWVPd0HRfdqU8f3BMH23LN4NYGL7MNA2SbBwiwMvQ1CQ=="/>
99-
<enclosure url="https://dao.msgbyte.com/Dao14.0-11.0.delta" sparkle:deltaFrom="11.0" length="4788126" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="862416" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="KJmhHxg6CG+fANenuZDw2ed0AkjgQzjDDbqRyN94bnHAyMuVyHSeP6/siEh+8moyezaWiAOZ9nLhSgwLN5/HAA=="/>
100-
<enclosure url="https://dao.msgbyte.com/Dao14.0-10.0.delta" sparkle:deltaFrom="10.0" length="5340758" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="862416" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="yKWFtUUF5s0yPh+IaogkeCAtlX/QzUXjMtPLdsnD/mvZl3xCICgrK4VaGQqWycM3ATmNn0l+FBv9qm2WICNICw=="/>
101-
<enclosure url="https://dao.msgbyte.com/Dao14.0-9.0.delta" sparkle:deltaFrom="9.0" length="5766338" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="862416" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="Can97V1s4gpdilfxTXui9wRPGOaeYBFRCea2F+haAMOnaUlVE9czqnXZYxL90+O2FBTHAKa+DU9hHuX5l8eeDA=="/>
102-
</sparkle:deltas>
81+
<enclosure url="https://dao-release.msgbyte.com/dao-browser-1.0.14-mac-arm64.dmg" length="153771120" type="application/octet-stream" sparkle:edSignature="L9Shk7E+ZvXQ1wuE4Iz08dUZEfId6ezSQp3UIq003CYeM8qifdL5NT4i2Mal7yX1UaDQCd8Ba6MH/X7BOxHtDA=="/>
10382
</item>
10483
</channel>
105-
</rss>
84+
</rss>

0 commit comments

Comments
 (0)