Docs
Cloudflare Release Hosting
aetower.dev is served by two independent Cloudflare deploy units so a website edit can never take the release payload (and Sparkle auto-updates) down:
- Pages project
aetower-devโ the website only (/,/assets/*). Holds theaetower.devcustom domain. - Pages project
aetower-releasesโ the release payload (/releases/,/homebrew/,/third-party-notices.md). - Worker
aetower-release-router(infra/release-router/) โ routesaetower.dev/releases/,aetower.dev/homebrew/, andaetower.dev/third-party-notices.mdin front of the Pages custom domain and proxies them toaetower-releases.pages.dev, byte-exact. It fronts the origin with the Cache API so ranged requests (Sparkle resume) return 206 and appcast revalidation returns 304. All other paths fall through to the website project.
Public URLs are unchanged from the single-project era โ the Sparkle appcast baked into shipped apps keeps working at:
https://aetower.dev/releases/appcast.xml
URL contract
/and/assets/*โ website (Pagesaetower-dev)./releases/appcast.xmlโ Sparkle feed./releases/Aetower.{zip,dmg,pkg}and/releases/Aetower-source.tar.gzโ latest aliases (max-age=120)./releases/Aetower-<version>-<build>.{zip,dmg,pkg}and/releases/Aetower-<version>-<build>-source.tar.gzโ immutable artifacts./releases/Aetower<build>-<oldbuild>.deltaโ Sparkle deltas (immutable)./homebrew/Casks/aetower.rbโ downloadable mirror of the generated Homebrew cask. The installable Homebrew distribution is theAeptus/homebrew-aetowertap./third-party-notices.mdโ dependency/license inventory.
Header rules live next to what they govern: site/_headers (security block only) ships with the website; infra/releases-pages/_headers (content types + cache policy, using ! detachment so latest aliases do not inherit the immutable policy) ships with the payload.
Deploy the website (fast path, safe any time)
sh scripts/deploy-website.sh
Assembles dist/cloudflare-site from site/ (plus the brand icon), deploys it to aetower-dev, and runs a smoke check that ends with an appcast tripwire: the deploy fails loudly if /releases/appcast.xml is not being served by the release router.
Deploy the release payload (release pipeline)
Payload deploys are full snapshots, so they must contain the complete history (immutable archives, Sparkle deltas), not just the newest release. scripts/prepare-release-payload.sh therefore always syncs the live payload down first (via scripts/mirror-live-release-payload.sh, which verifies the cask sha256, appcast enclosure lengths, and live Content-Lengths), then overlays the new release's dist/ artifacts.
The release orchestrator does all of this:
sh scripts/release-public-preview.sh # full build
sh scripts/release-public-preview.sh --prepare-only # reuse dist artifacts
sh scripts/release-public-preview.sh --prepare-only --publish-cloudflare
--publish-cloudflare deploys the payload to aetower-releases, runs scripts/verify-published-release.sh against the public URLs, then deploys the website. Payload-only deploys are also available directly:
sh scripts/prepare-release-payload.sh
sh scripts/deploy-release-payload.sh
Worker deploy
Only needed when infra/release-router/src/worker.js changes:
cd infra/release-router && npx wrangler@4 deploy
Configuration
All values have working defaults; override in .env.release.local if the project names or dirs ever change:
AETOWER_CLOUDFLARE_PROJECT=aetower-dev
AETOWER_CLOUDFLARE_SITE_DIR=dist/cloudflare-site
AETOWER_CLOUDFLARE_RELEASES_PROJECT=aetower-releases
AETOWER_CLOUDFLARE_RELEASES_DIR=dist/releases-payload
AETOWER_CLOUDFLARE_BRANCH=master
AETOWER_RELEASES_ORIGIN=https://aetower-releases.pages.dev
Rollback
- Website: Cloudflare dashboard โ Pages โ
aetower-devโ Deployments โ rollback to a previous deployment. - Payload: same, on
aetower-releases; or re-runmirror-live-release-payload.sh+deploy-release-payload.shfrom a known-good state. - Router:
npx wrangler@4 delete --config infra/release-router/wrangler.jsoncremoves the worker and its routes. NOTE: after the split, the website project no longer contains the payload, so deleting the router makes/releases/*404 โ only do this while immediately redeploying the worker or restoring a pre-split full snapshot onaetower-dev.
After deploy
curl -I https://aetower.dev/
curl -I https://aetower.dev/third-party-notices.md
curl -I https://aetower.dev/releases/appcast.xml
curl -I https://aetower.dev/releases/Aetower.dmg
curl -I https://aetower.dev/releases/Aetower.zip
curl -I https://aetower.dev/releases/Aetower.pkg
curl -I https://aetower.dev/releases/Aetower-source.tar.gz
curl -I https://aetower.dev/homebrew/Casks/aetower.rb
Release payload responses carry x-aetower-release-router: 1; website responses must not. Then run the Sparkle N-1 -> N update verification before sharing the link.
Notes
aetower-releases.pages.devmust stay publicly reachable โ it is the router's origin (no regression: the payload was equally reachable onaetower-dev.pages.devbefore the split). Payload responses carryX-Robots-Tag: noindex.- Bare
/releases(no trailing slash) is not on a route and falls through to the website project, where it 404s. Nothing links to it. - The Sparkle URL baked into packaged builds remains
https://aetower.dev/releases/appcast.xml; appcast archive URLs remain underhttps://aetower.dev/releases/.
Aetower is a free early-alpha download for macOS 14+ (Apple silicon).
Download for macOS