Wails in Practice #6 CI/CD Automated Releases — Three-Platform Delivery with GitHub Actions

5 min read

The final post of Part 1. With signing and notarization learned in #5, we now automate the whole process. The goal is clear: push a single Git tag and signed releases for macOS, Windows, and Linux build themselves. We hand the job of building and signing three times by hand to GitHub Actions.

This series is ten posts in two parts, and this is the last of Part 1.

Why a matrix: macOS cannot be cross-compiled #

As noted in intro #6, a desktop app is hard to produce for three OSes from one. In particular, a macOS app can only be built on macOS, because the signing and notarization tools (codesign, notarytool) are macOS-only. So instead of cross-compiling on one runner, we use a matrix that builds each platform on that OS’s runner. GitHub Actions provides macos, windows, and ubuntu runners, so this structure is natural.

Here #2’s choice pays off. Because SQLite was picked as the pure-Go driver modernc.org/sqlite, it just builds on each runner with no CGO. Had you picked the CGO version, the workflow would be far more complex, wrangling a C toolchain on each runner.

The workflow: a matrix that reacts to a tag #

When a tag starting with v is pushed, three runners build in parallel.

.github/workflows/release.yml — matrix skeleton
on:
  push:
    tags: ["v*"]

jobs:
  build:
    strategy:
      matrix:
        include:
          - os: macos-latest
            platform: darwin/universal
          - os: windows-latest
            platform: windows/amd64
          - os: ubuntu-latest
            platform: linux/amd64
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with: { go-version: "1.23" }
      - name: Wails build
        uses: dAppServer/wails-build-action@main
        with:
          build-platform: ${{ matrix.platform }}

dAppServer/wails-build-action is a community action that bundles Wails CLI installation, frontend dependency install, and the build. You can replace it with steps that call wails build directly, but this action optionally handles signing and installer creation too, reducing initial setup.

Signing in CI: restore from Secrets #

#5’s decision not to keep secrets locally continues here. Base64-encode the certificate into a GitHub Secret, and restore it in the workflow to use for signing.

restore certificate and sign/notarize on the macOS runner
      - name: Import certificate (macOS)
        if: matrix.os == 'macos-latest'
        env:
          CERT_P12: ${{ secrets.MACOS_CERT_P12 }}       # base64-encoded .p12
          CERT_PW: ${{ secrets.MACOS_CERT_PASSWORD }}
        run: |
          echo "$CERT_P12" | base64 --decode > cert.p12
          security create-keychain -p "" build.keychain
          security import cert.p12 -k build.keychain -P "$CERT_PW" -T /usr/bin/codesign
          # then codesign → notarytool submit --wait → stapler staple (see #5)

The Windows runner restores the code-signing certificate the same way and signs with signtool. All sensitive values come only from secrets.*, and take care they are not printed to the log.

Bundling into a release #

Collect each runner’s output into a single GitHub Release. Upload the build outputs as artifacts, then create the release in a final job and attach them, or have each job add assets to the release for the same tag.

attach build outputs to the release
      - name: Upload assets to the release
        uses: softprops/action-gh-release@v2
        with:
          files: build/bin/*

Now the cycle is this. Commit your code and run git tag v1.0.0 && git push --tags, and the three runners each build and sign, and the finished installers land on the v1.0.0 release page. Users download the file for their OS and install with no warning.

Auto-update: what you can use #

The next question after shipping is auto-update. Wails has no official built-in updater, so you pick a strategy.

  • The light approach: on startup, query the latest tag on GitHub Releases, and if it is higher than the current version, open the release page and let the user download it themselves. Simple to implement and it does not collide with the signing flow.
  • Fully automatic: download the new binary and replace itself. You have to handle signature verification, permissions, and rollback yourself — complex, and done wrong it can break the trust of a signed app.

For individual and small-scale distribution, the light approach (notify that an update exists and send to the release) is the safe default. Consider fully automatic replacement when your user base grows enough that manual update prompts become a burden.

Note
Wails v3 is refining its tooling along with multi-window and a built-in tray. This post’s concepts — matrix build, Secrets-based signing, release automation — remain valid in v3; what changes is the detail of CLI commands and actions. Check the migration guide at the official release.

Closing Part 1 — and Part 2 #

Through Part 1, we grew a memory-based skeleton into a shippable notes app. We split the layers (#1), attached pure-Go SQLite (#2), settled search and data flow (#3), added background operation (#4), signed and notarized (#5), and automated with CI (#6), completing the full cycle of shipping an idea as signed installers for three platforms.

But a shippable app and an app users reopen every day are different things. What remains is the front half we deferred while leaning on the backend (editor and UX), the testing we only promised, the auto-update we only discussed as strategy, and finishing like settings, dark mode, and data safety. Part 2 (#7#10) raises this app to the polish that earns repeat readers.

Summary #

  • A macOS app builds and signs only on macOS, so use a per-platform runner matrix instead of one-runner cross-compilation. Pure-Go SQLite keeps each runner’s build simple.
  • The workflow reacts to a v* tag and builds in parallel on three runners, with wails-build-action bundling the build and installer creation.
  • Put signing secrets in GitHub Secrets as base64 and restore them on the runner. #5’s rule of not keeping them locally completes in CI.
  • Wails has no official updater. For individuals and small scale, “notify that an update exists and send to the release” is the safe default; consider fully automatic replacement as scale grows.
  • The point where a single tag generates signed releases for three platforms is the completion of the release automation this series aimed for.
X