07 · Release & Versioning Workflow
This project ships macOS / Windows / Linux installers via GitHub Releases, driven entirely by three manual GitHub Actions workflows. There is no automatic trigger — every publish is a deliberate human action taken on the GitHub web UI. No local command line is required.
The three workflows
| Workflow | File | Purpose |
|---|---|---|
| List releases | .github/workflows/list-releases.yml | Read-only — prints what already exists on GitHub. Run this first before doing anything else. |
| Set version & tag | .github/workflows/set-version.yml | Writes a release vX.Y.Z commit, pushes the vX.Y.Z tag. Upgrade, downgrade, or arbitrary version. |
| Release Electron App | .github/workflows/release.yml | Builds macOS / Windows / Linux installers and publishes the GitHub Release. Manual-only. |
The publish cycle is always two steps:
Step 1: Set version & tag → creates commit + tag (no build)
Step 2: Release Electron App → builds + publishes GitHub ReleaseYou can repeat Step 1 many times before doing Step 2 if you want — the workflows are fully decoupled.
One-time setup
In your GitHub repo:
- Settings → Actions → General → Workflow permissions → choose Read and write permissions
- Click Save
Without this, the runner cannot push commits / tags back to the repo and every Step 1 will fail at the git push.
See what already exists
Before bumping, downgrading, or re-publishing, check what's already on the server.
Actions → List releases → Run workflow → wait → open the run → expand "Print releases + tags"
Output is two lists:
================================================================
GitHub Releases (most recent 30)
================================================================
v1.0.0 v1.0.0 Published 2026-08-17
v0.9.5 v0.9.5 Draft 2026-08-15
================================================================
Tags on origin (most recent 30)
================================================================
refs/tags/v1.0.0
refs/tags/v0.9.5
refs/tags/v0.9.0The diff between the two lists is informative: a tag that appears in the second list but not the first means its GitHub Release was deleted — that tag can be re-published via the Release Electron App workflow.
Bump up (auto, patch / minor / major)
Actions → Set version & tag → Run workflow, with:
| Input | Value |
|---|---|
mode | auto |
bump | patch (or minor / major) |
version | (leave blank) |
What it does:
- Reads the current version from
package.json - Bumps it (e.g.
1.0.0+patch→1.0.1) - Writes the new version into
package.json,pnpm-lock.yaml, and the version label inSettingsPanel.vue - Commits
release v1.0.1 - Tags
v1.0.1and pushes both commit + tag toorigin/main
Nothing is built yet — go to Step 2.
Set to an explicit version (upgrade OR downgrade)
Actions → Set version & tag → Run workflow, with:
| Input | Value |
|---|---|
mode | set |
bump | (ignored) |
version | 2.0.0 (or anything — a value lower than current = downgrade, e.g. 0.9.6) |
Same effect as above, but the target version is whatever you typed. Downgrade is non-destructive — the old tag and its Release stay in place.
If the tag you typed already exists on origin, the workflow aborts with a clear message: either pick a different version, or use Step 2 with that tag to re-publish.
Build & publish the GitHub Release
Actions → Release Electron App → Run workflow, with:
| Input | Value |
|---|---|
version | (leave empty — picks the highest semver tag on origin) |
When version is empty, the workflow lists every semver tag on origin, picks the highest one, checks it out, and runs pnpm build + electron-builder --publish always. macOS / Windows / Linux build in parallel via matrix strategy.
Draft → published, automatically
electron-builder's releaseType defaults to draft — each of the three parallel builds uploads its artifacts into the same draft release. A dedicated publish job then runs only after all three builds have succeeded and flips the draft to published (gh release edit <tag> --draft=false).
Why keep the draft step at all:
- The release never goes public with a partial artifact set — e.g. the
.dmguploaded while the.exeis still building - If any platform fails, the release stays a draft (visible only to maintainers) — fix and re-run, nothing half-public ever appears
- Published releases are what
releases/latestand the download page point to, so they must always be complete
A release stuck in Draft = at least one platform's build failed. Check the Actions run, re-run the failed jobs, and the
publishjob will promote it.
If you want a specific (not-the-latest) tag — say to skip past v1.1.0 and ship v1.0.3 instead, or re-publish a tag whose Release got corrupted — fill version=1.0.3 (or v1.0.3). Either form works.
The workflow always:
- Uses a tag (never silently builds HEAD)
- Errors out if the resolved tag doesn't exist on origin — and tells you to run
Set version & tagfirst git checkouts the tag, so the published artifacts always match the tagged commit byte-for-byte
Re-publish an existing tag
Same workflow, with version filled in:
| Input | Value |
|---|---|
version | 1.0.0 (or v1.0.0) |
Use this when:
- You accidentally deleted a GitHub Release
- The previous build had a bad artifact and you want a fresh build at the same tag
- You want to add a new OS target to an old release
The runner checks out the same tag, rebuilds, and electron-builder --publish always overwrites the existing Release with fresh artifacts.
Deleting releases or tags
None of the workflows ever delete anything. To clean up, use the GitHub web UI (repo → Releases → trash icon on the release) or a local terminal with the gh CLI:
# Delete just the Release (keep the tag — re-publishable via the flow above)
gh release delete v1.0.0
# Delete Release AND tag
gh release delete v1.0.0 --yes
git push origin --delete v1.0.0Or mark the Release as Draft in the web UI to hide it without losing anything — the tag, artifacts, and download links all stay live.
Why everything is manual
No push: tags: v* trigger exists anywhere. Every publish needs an explicit Run workflow click. Reasons:
- Decoupled: bump a version without immediately building (e.g. batch a few version bumps before triggering builds)
- Predictable: nothing happens while you're iterating on
main - Recoverable: any commit/tag state is reproducible from the web UI alone, no local git history needed
- Auditable: every release has a clear human-initiated action in the Actions log
End-to-end examples
Hotfix release to an old minor
1. Actions → Set version & tag → Run workflow
mode=set, version=0.9.6
2. Actions → Release Electron App → Run workflow
tag=(leave blank)The current main HEAD is tagged v0.9.6 and published. The previous v1.0.0 tag and Release stay untouched.
Replace a corrupted release
1. Don't touch package.json — just push a build fix to main:
git commit --allow-empty -m "trigger rebuild" && git push
2. Actions → Release Electron App → Run workflow
tag=v1.0.1The v1.0.1 tag already exists, so Step 1's version-bump workflow would refuse. Going directly to Step 2 with the existing tag checks out that exact commit and re-publishes.
Promote a draft to a real release
The workflow's publish job promotes drafts automatically once all three builds succeed — a release left in Draft means a build failed (or the publish job was skipped). Normal recovery is: open the failed Actions run, re-run the failed jobs, and the publish job promotes the release.
If you ever need to do it by hand (e.g. all artifacts are present but the publish job itself failed):
gh release edit v0.9.5 --draft=falseor open the draft on GitHub and click Publish release.
