Ilan Firsov

Adding An Auto Update Feature In A GO Application

About 5 minutes

I’m posting this for my own reference – which I’ll probably forget that I even posted at some point and have to Google again and maybe, hopefully, find this post.

So I wanted to add an automatic update feature in my clock dashboard application (which actually is one of the more useful thing I made recently), so that I don’t have to manually run a git pull and rebuild the project each time I make a small change.

First thing we have to add is a Github action which will build my project for each release I publish.
This turned out to be a whole day of debugging weird errors in my action, but eventually I prevailed and with the help from ChatGPT (which for once actually helped with something 🙌) made it work…

Compiling the application

You can find the action I settled on for my Github workflow here: https://github.com/IlanF/clock-dashboard/blob/main/app.go

I’ll brief over some of the steps…

First thing I’m doing is setting some variables based on the platform I’m building for, I will use them in the following steps
1. Extracting the binary name from the repository name.
2. Based on the platform get the binary name we need – for Windows we have to add a .exe at the end, that’s pretty much it for the binary.
3. Get the tag name from the release I publish, I’ll use that later to inject the version number into the code.
These will make the action reusable for different applications without much edits.

- name: Set App Vars
        id: vars
        shell: bash
        env:
          PLATFORM: ${{ matrix.build.platform }}
        run: |
          if [ -z "${APP_NAME}" ]; then
            APP_NAME="${{ github.event.repository.name }}"
          fi
          GOOS=$(echo "$PLATFORM" | cut -d'/' -f1)
          GOARCH=$(echo "$PLATFORM" | cut -d'/' -f2)

          BIN_NAME="$APP_NAME"
          if [ "$GOOS" == "windows" ]; then
            BIN_NAME="${APP_NAME}.exe"
          fi

          echo "app_name=${APP_NAME}" >> "$GITHUB_OUTPUT"
          echo "bin_name=${BIN_NAME}" >> "$GITHUB_OUTPUT"
          echo "goos=${GOOS}" >> "$GITHUB_OUTPUT"
          echo "goarch=${GOARCH}" >> "$GITHUB_OUTPUT"
          echo "tag=${GITHUB_REF##*/}" >> "$GITHUB_OUTPUT"

Next main step is the actual build step:

- name: Wails Build
        shell: bash
        env:
          APP_NAME: ${{ steps.vars.outputs.app_name }}
          BIN_NAME: ${{ steps.vars.outputs.bin_name }}
          GOOS: ${{ steps.vars.outputs.goos }}
          GOARCH: ${{ steps.vars.outputs.goarch }}
          # Optional build tags (if needed, e.g., webkit2_41)
          BUILD_TAGS: ""
        run: |
          export PATH=$PATH:$(go env GOPATH)/bin
          echo "Building for $GOOS/$GOARCH"
          wails build -tags "$BUILD_TAGS" -ldflags="-X 'main.version=${{ steps.vars.outputs.tag }}'" -o "${{ steps.vars.outputs.bin_name }}"

Generally I’ll go for dAppServer/wails-build-action but this time because I want to inject the version number while running the build I can’t use it, so I used the normal build process – this requires me to install Go, NodeJS and Wails.io which I don’t go over in this post but you can see in the final action.
In my main.go file I have a variable I named version which I use later. The -ldflags flag will update that variable at build time and set it based on the release tag name from the published release.

Next steps are rename the binary, zip it and upload it as an artifact to the release

- name: Rename binary
        shell: bash
        env:
          APP_NAME: ${{ steps.vars.outputs.app_name }}
          BIN_NAME: ${{ steps.vars.outputs.bin_name }}
          GOOS: ${{ steps.vars.outputs.goos }}
          GOARCH: ${{ steps.vars.outputs.goarch }}
        run: |
          mkdir -p out
          mv build/bin/${BIN_NAME} ./out/${BIN_NAME}
          ZIP_NAME="${APP_NAME}_${GOOS}_${GOARCH}.zip"

          echo "zip_name=$ZIP_NAME" >> $GITHUB_ENV

          if [ "$GOOS" == "windows" ]; then
            powershell.exe -Command "Compress-Archive -Path out/${BIN_NAME} -DestinationPath ${ZIP_NAME}"
          else
            zip -j "$ZIP_NAME" "out/${BIN_NAME}"
          fi

- name: Upload Release Asset
        if: github.event_name == 'release'
        uses: softprops/action-gh-release@v1
        with:
          files: ${{ env.zip_name }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

There is not much to that, rename the binary file based on a specific pattern, zip it and then upload.
Only gotchas here are that we have to use Powershell in Windows instead of the zip command, and in order for the upload to run we have to allow write permissions with:

permissions:
  contents: write

Adding the update functionality

In the Go code I have to update the main.go file and add a version variable I’ll use for the update checker.
This is the variable that -ldflags "-X 'main.version=XXXXX'" updates.

package main

// ... imports 

var version = "dev" // a placeholder value

func main() {
	app := NewApp(version)

    // ... rest of the code
}

Notice that I pass the version number to the app. This is something specific to Wails.io in other applications you may use that directly inside the main function, and in my app.go file I add the actual update check mechanism.
I’ll use the rhysd/go-github-selfupdate package, this package takes care of all the nitty gritty of the update procedure.
All I have to do is invoke the selfupdate function at the right time, this function takes the current version and your repository name as arguments.

v, err := semver.Parse(app.version)
if err != nil {
	log.Printf("Could not parse version string '%s'\n", app.version)
	return
}

latest, err := selfupdate.UpdateSelf(v, "user/repository")
if err != nil {
	log.Println("Binary update failed:", err)
	return
}

if latest.Version.Equals(v) {
	// latest version is the same as current version. It means current binary is up to date.
	log.Println("Current binary is the latest version", app.version)

	return
}

log.Println("Successfully updated to version", latest.Version)
log.Println("Release note:\n", latest.ReleaseNotes)

And that actually it.
The selfupdate function takes your binary name, OS, and architecture then goes to the Github releases of the repository you provided and check if it has a newer release for your OS. It checks the version against the tag name the release published for, and searches for a specific file name to download which needs to be in the following format: {cmd}_{goos}_{goarch}{.ext}
for my application this looks like: clock-dashboard_linux_arm64.zip (linux/arm64 since I’m running this on a Raspberry Pi) and that what the rename step in the Github action does.

{cmd} is a name of command. {goos} and {goarch} are the platform and the arch type of the binary. {.ext} is a file extension. go-github-selfupdate supports .zip, .gzip, .tar.gz and .tar.xz. You can also use blank and it means binary is not compressed.

The selfupdate function runs at app startup, checks for updates and if has any update will download and replace the binary file, only step left is to restart the application from the new binary file.