How to cut releases, what happens automatically, and how to fix things when they break.
Before cutting a release you need:
- Push access to the
milady-ai/miladyrepository (to create tags) - The following GitHub Secrets configured on the repo:
| Secret | Purpose |
|---|---|
NPM_TOKEN |
Publish to npm |
PYPI_API_TOKEN |
Publish to PyPI |
CSC_LINK |
macOS code signing certificate (.p12, base64) |
CSC_KEY_PASSWORD |
Password for the .p12 certificate |
APPLE_ID |
Apple ID email for notarization |
APPLE_APP_SPECIFIC_PASSWORD |
App-specific password from appleid.apple.com |
APPLE_TEAM_ID |
10-char Apple Developer Team ID |
WINDOWS_SIGN_CERT_BASE64 |
Windows code signing certificate |
WINDOWS_SIGN_CERT_PASSWORD |
Password for Windows signing cert |
SNAP_STORE_CREDENTIALS |
Snap Store upload credentials |
HOMEBREW_TAP_TOKEN |
PAT for milady-ai/homebrew-tap repo |
RELEASE_UPLOAD_KEY |
SSH key for uploading update files to milady.ai |
RELEASE_HOST_FINGERPRINT |
SSH host key for milady.ai |
The root package.json holds the canonical version (currently uses semver with pre-release identifiers like 2.0.0-alpha.92).
Scripts:
| Script | What it does |
|---|---|
scripts/set-package-version.mjs |
Sets version in root package.json. Reads RELEASE_VERSION env var. |
scripts/align-electrobun-version.mjs |
Syncs version across package.json, apps/app/package.json, apps/app/electrobun/package.json, and electrobun.config.ts. Also reads RELEASE_VERSION env var. |
Both scripts are called automatically by CI during the release workflow. You don't need to run them manually unless doing a local dry-run.
Version format determines the release channel:
| Version pattern | npm dist-tag | Build env |
|---|---|---|
2.0.0 |
latest |
stable |
2.0.0-beta.1 |
beta |
canary |
2.0.0-alpha.7 |
next |
canary |
2.0.0-rc.1 |
next |
canary |
2.0.0-nightly.20260320 |
nightly |
canary |
bun run build
bun run release:check # validates dist contents, forbidden paths, package integritygit tag v2.0.0-alpha.93
git push origin v2.0.0-alpha.93That's it. Pushing a v* tag triggers the entire release pipeline.
Go to Actions > Build & Release (Electrobun) and click "Run workflow". You can specify a tag and optionally create as a draft release.
Pushing a v* tag triggers this pipeline:
v* tag push
│
├─ release-electrobun.yml
│ ├─ prepare ─── Determine version, channel (stable/canary)
│ ├─ validate-release ─── bun run release:check
│ ├─ build ─── Build desktop apps (4 platforms in parallel)
│ │ ├─ macOS ARM64 (signed + notarized)
│ │ ├─ macOS Intel (signed + notarized)
│ │ ├─ Windows x64 (signed, Inno Setup installer, smoke tested)
│ │ └─ Linux x64
│ ├─ release ─── Create GitHub Release with installers + checksums
│ ├─ publish-docker ─── Build + push ghcr.io/milady-ai/agent
│ └─ (upload update channel files to milady.ai/releases/)
│
└─ On GitHub Release "published" event:
├─ publish-npm.yml ─── Publish to npm with auto-detected dist-tag
└─ publish-packages.yml ─── Publish to all package managers
├─ PyPI
├─ Homebrew tap (stable releases only)
├─ Snap Store
├─ Debian .deb (+ trigger APT repo update)
└─ Flatpak (stable releases only)
| Artifact | Where | Install command |
|---|---|---|
npm package (miladyai) |
npmjs.com | npm install -g miladyai@latest |
PyPI package (milady) |
pypi.org | pip install milady |
| Homebrew formula | milady-ai/homebrew-tap |
brew install milady-ai/tap/milady |
| Snap | Snap Store | sudo snap install milady --classic |
| Debian .deb | GitHub Release + APT repo | sudo dpkg -i milady_*.deb |
| Flatpak | GitHub Release | flatpak install milady.flatpak |
| Docker image | ghcr.io/milady-ai/agent |
docker pull ghcr.io/milady-ai/agent:latest |
| macOS app (.dmg) | GitHub Release | Download from release page |
| Windows installer (.exe) | GitHub Release | Download from release page |
| Linux desktop (.tar.zst) | GitHub Release | Download from release page |
Docker tags:
:v{version}— exact version:latest— latest tagged release:dev— latestdevelopbranch push
Schedule: Daily at 04:00 UTC, from the main branch.
Behavior:
- Skips if no new commits since the last nightly tag
- Runs build + unit tests before publishing
- Version format:
{base}-nightly.{YYYYMMDD}(e.g.2.0.0-nightly.20260320) - Published to npm with
nightlydist-tag - Creates a GitHub pre-release with auto-generated release notes
- Old nightly releases are cleaned up (keeps last 14)
Force a nightly: Actions > Nightly Build > Run workflow > check "Force nightly build"
Install nightly:
npm install -g miladyai@nightlyThe script (scripts/release-check.ts) validates that required files exist in dist/ and forbidden paths (like dist/Milady.app/) are absent. Fix whatever it complains about and re-tag.
- Check that
NPM_TOKENis valid and has publish access tomiladyai - Verify the version doesn't already exist:
npm view miladyai@{version} - You can re-run the publish manually: Actions > Publish npm > Run workflow
- Verify
CSC_LINKcontains a valid, non-expired Developer ID Application certificate - Check
APPLE_APP_SPECIFIC_PASSWORDhasn't been revoked at appleid.apple.com - If
APPLE_TEAM_IDis wrong, notarization silently fails
The workflow pre-extracts the Electrobun CLI using C:\Windows\System32\tar.exe (BSD tar) because GNU tar misinterprets C: drive letters as user@host. If this step fails, check the Electrobun release exists for the expected version.
The Docker build has a 90-minute timeout. If it's timing out, check for cache misses on the bun-store or Docker layer caches.
These only run for stable releases (no alpha, beta, or rc in the version). This is intentional.
If a release partially failed:
- Fix the issue
- Delete the GitHub Release (if created) and the tag
- Re-tag and push, or use manual dispatch with the same tag
The publish-npm.yml verify step warns (not errors) if the registry hasn't propagated yet. Wait a few minutes and check manually: npm view miladyai@{dist-tag} version.