Skip to content

Desktop Release Automation

ButtonsCLI can build Windows, macOS, and Linux desktop bundles automatically from GitHub Actions.

This repo now includes .github/workflows/release-desktop.yml for that.

What the workflow does

The workflow runs on:

  • pushes to main
  • manual workflow_dispatch

It reads the version from package.json.

If a matching Git tag already exists, for example buttonscli-v0.5.0, the workflow stops without publishing another desktop release.

If the version is new, the workflow:

  • builds Linux bundles on Ubuntu
  • builds Windows bundles on Windows
  • builds a universal macOS bundle on macOS
  • attaches the artifacts to a GitHub release draft
  • uploads the installers as workflow artifacts named desktop-<platform>-<version>
  • skips updater metadata and platform signing by default
  • writes a server handoff Markdown artifact that can be given to a server-side agent after the desktop artifacts are reviewed
  • can manually upload the built desktop artifacts to the VPS and trigger the server deploy script when deploy_to_server is enabled

That draft-first setup is safer when you are still getting used to the release flow. Once you trust it, you can flip releaseDraft: true to false in the workflow file and let it publish immediately.

One-time GitHub setup

1. Commit the workflow

Make sure .github/workflows/release-desktop.yml is committed on the branch you plan to merge into main.

2. Optional updater-signing secrets

You can skip this while you are just trying to get downloadable installers. Unsigned desktop artifacts are still useful for testing and manual release downloads.

Open your GitHub repository, then go to:

Settings -> Secrets and variables -> Actions

Add these secrets:

  • TAURI_SIGNING_PRIVATE_KEY
  • TAURI_SIGNING_PRIVATE_KEY_PASSWORD

If you generated your updater key locally with:

pnpm updater:keygen

then the private key lives at ~/.buttonscli-signing/updater.key.

This repo's local helper generates that key with an empty password by default, so TAURI_SIGNING_PRIVATE_KEY_PASSWORD can stay blank.

Only enable the signed_release workflow input after these secrets are known to work. If TAURI_SIGNING_PRIVATE_KEY is malformed or the password is wrong, Tauri can finish compiling and then fail while creating updater signatures.

3. Optional Windows signing secrets

If you already have a real Windows code-signing certificate, also add:

  • BUTTONSCLI_WINDOWS_CERT_THUMBPRINT
  • BUTTONSCLI_WINDOWS_TIMESTAMP_URL
  • BUTTONSCLI_WINDOWS_DIGEST_ALGORITHM

If you do not have a Windows certificate yet, that is okay. The workflow builds unsigned Windows bundles by default.

4. Optional macOS signing secrets

If you want proper macOS public distribution instead of ad-hoc local-style signing, add the usual Apple secrets too:

  • APPLE_SIGNING_IDENTITY
  • APPLE_CERTIFICATE
  • APPLE_CERTIFICATE_PASSWORD
  • APPLE_ID
  • APPLE_PASSWORD
  • APPLE_TEAM_ID

If you skip these for now, the repo can still build, but macOS public distribution trust and notarization will not be fully set up yet.

Do not enable signed_release until the Apple certificate secrets are valid. If the certificate is missing, malformed, or protected by the wrong password, macOS can finish compiling and then fail at the security import step.

5. Optional VPS deploy secrets

For the VPS deploy lane, add these repository secrets under:

Settings -> Secrets and variables -> Actions

  • DEPLOY_SSH_KEY: the private deploy key
  • DEPLOY_HOST: the VPS IP address or DNS name
  • DEPLOY_USER: the SSH user, currently deploy

Do not paste the IP address into DEPLOY_SSH_KEY. The IP address belongs only in DEPLOY_HOST.

The current server deploy key is a forced-command key. That is intentional: it can run only the deploy wrapper on the server. Because of that, normal scp commands will not work unless the wrapper explicitly supports an upload command. The GitHub workflow expects the server wrapper to support:

ssh deploy@host "upload 0.8.5" < release.tar.gz
ssh deploy@host "0.8.5"

The first command streams the release files into the server's incoming release folder. The second command promotes that incoming folder to the live release.

Normal release routine

1. Bump the version once

Change the version in package.json.

That is the source of truth now.

pnpm run branding:sync mirrors it into:

  • src-tauri/tauri.conf.json
  • src-tauri/Cargo.toml

2. Push or merge to main

When that new version lands on main, GitHub Actions will build the three OS artifacts automatically.

3. Find the built files

The files go to two GitHub places:

  • the draft Release, if the release upload step finishes cleanly
  • the workflow run's Artifacts section, always named like desktop-linux-x64-0.8.3, desktop-windows-x64-0.8.3, and desktop-macos-universal-0.8.3

In GitHub, open:

Actions -> desktop-release -> the run -> Artifacts

Download the artifact zip for the platform you want. Inside it, expect files like:

  • Linux: .AppImage and .deb
  • Windows: .exe from the NSIS bundle
  • macOS: .dmg

The runner's raw build directories are temporary. Anything not uploaded as a Release asset or Actions artifact disappears when the run ends.

4. Open the draft release

After the workflow finishes, open the new GitHub release draft and check that the expected bundles are attached.

5. Publish the draft when ready

When the artifacts look correct, publish the draft release from GitHub's Releases page.

6. Optional: deploy to the VPS

Manual runs have a deploy_to_server checkbox.

When enabled, the workflow:

  • downloads the ButtonsCLI_<version>_* assets from the matching GitHub release
  • streams those files to the VPS with the forced-command deploy key
  • triggers the server deploy script for the same version

Leave this off while you are only testing compile/release packaging. Once the release draft/assets exist and the server-side wrapper has upload <version> support, you can run the workflow manually with deploy_to_server enabled.

Local build vs GitHub build

For local machine builds, keep using:

pnpm release:desktop

That is still the fastest path when you only want a Windows bundle from your own machine.

GitHub Actions is the cross-platform path that creates all three operating-system builds without you needing three separate computers.

Compile checks without releasing

The repo also has .github/workflows/desktop-ci.yml.

That workflow runs on pull requests, pushes to main, and manual dispatch. It does not publish releases. It installs dependencies, builds the frontend, and runs cargo check for the Tauri backend on:

  • Ubuntu
  • Windows
  • macOS

Use this as the normal "does it compile everywhere?" lane. Use release-desktop.yml only when you are ready to produce release artifacts.

Server handoff lane

release-desktop.yml no longer tries to rsync a half-defined site folder after desktop builds. Instead, it creates a server handoff .md artifact with the version, tag, release checklist, and server-agent instructions.

On manual dispatch, you can also enable send_server_handoff. That uploads the handoff file to the server over SSH.

Do not use send_server_handoff with the forced-command deploy key. That key is intentionally limited to the deploy wrapper and cannot run arbitrary mkdir or scp commands. Use a normal SSH account for handoff uploads, or leave this off and use the workflow artifact directly.

Required secrets for handoff upload:

  • DEPLOY_SSH_KEY
  • DEPLOY_HOST
  • DEPLOY_USER

The default remote directory is:

/home/ubuntu/buttonscli-release-requests

That is intentionally just a handoff. Let Codex on the server read the file, inspect the actual server paths, and propose the concrete deploy commands before anything syncs or restarts.

First things to check when a workflow fails

  • the version in package.json was actually bumped
  • if the log says it built one version but looked for another version, run pnpm run branding:sync and commit the updated src-tauri metadata
  • the workflow run has platform artifacts under Actions -> run -> Artifacts
  • TAURI_SIGNING_PRIVATE_KEY exists only if signed_release was enabled
  • Linux system packages installed correctly on the runner
  • macOS signing secrets are present only if signed_release was enabled
  • the repo does not already have a tag for the same version

If you want more aggressive automation later, the main toggle is simple:

  • keep releaseDraft: true for safer review-first releases
  • change it to false for auto-published releases