Build and Publish a Multi-Platform Electron App on GitHub
Tags: electron, github, javascript, tutorial
Published: 2020-12-31 15:50:18

* [Introduction](#intro)
* [Create Electron Application](#create-app)
* [Build on Linux, Windows and macOS](#build)
* [Publish Release to GitHub](#publish)
* [Draft Releases](#draft)
* [Outlook](#outlook)

#Introduction <a name="intro"></a>
I recently dived into [Electron](https://www.electronjs.org/) for a hobby project. I like the approach of using web technologies to create the UI and shipping a native application to all platforms.

For a Proof of Concept, I wanted to actually set up the process of building and distributing such an application on GitHub. It was a lot of trial and error to finally get there. Because of this, I documented my learnings in this tutorial. The final setup I came up with is actually surprisingly simple.

You can find the complete example here:

{% github erikhofer/electron-publish-example no-readme %}

#Create Electron Application <a name="create-app"></a>

We are going to use [Electron Forge](https://www.electronforge.io/) for creating the example application. 

```
npx create-electron-app electron-publish-example
cd electron-publish-example
npm start
```

We can now see our example application in a native window.

![Example App](https://dev-to-uploads.s3.amazonaws.com/i/cfdnolemnidbsm6my3h3.png)

The distributable packages can be built with the following command.

```
npm run make
```

This will only build package formats that are supported by your operating system. For more information see [Electron Forge Makers](https://www.electronforge.io/config/makers).

#Build on Linux, Windows and macOS <a name="build"></a>

Ok, so how do we build the application for other platforms? Luckily, [GitHub](https://github.com) provides a free and easy way to do this. We start by creating an empty repository and pushing our example code. I assume that you know the basics of Git. Everything we do from now on needs to be pushed to GitHub.

```
git init
git add .
git commit -m "Create example app"
git branch -M main
git remote add origin https://github.com/erikhofer/electron-publish-example.git
git push -u origin main
```

We then create a new file `.github/workflows/build.yml` with the follwoing content.

```yaml
name: Build
on: [push, pull_request]

jobs:
  build_on_linux:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-node@master
      with:
        node-version: 14
    - name: install dependencies
      run: npm install
    - name: build
      run: npm run make

  build_on_mac:
    runs-on: macos-latest
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-node@master
      with:
        node-version: 14
    - name: install dependencies
      run: npm install
    - name: build
      run: npm run make
      
  build_on_win:
    runs-on: windows-latest
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-node@master
      with:
        node-version: 14
    - name: install dependencies
      run: npm install
    - name: build
      run: npm run make
```

It basically defines the same job three times for different operating systems. The workflow is executed for all branches and for pull requests to verify that the application can still be built after making changes.

After pushing the file, we go to the "Actions" tab of the repository ([example](https://github.com/erikhofer/electron-publish-example/actions)). We can see our newly created workflow running. Click on it to see the individual tasks and wait for them to finish.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ovls2lpibcdl46jm3d77.png)

That's it! 🚀 We are now building a native application on Windows, Linux and macOS.

#Publish Release to GitHub <a name="publish"></a>

Fine, now how do we get access to the distributable files? We could set up [artifact uploading](https://docs.github.com/en/free-pro-team@latest/actions/guides/storing-workflow-data-as-artifacts) for this. While this is useful for developers, it is not sufficient for providing the application to users. For that, we want to utilize [GitHub releases](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/managing-releases-in-a-repository).

<img src="https://dev-to-uploads.s3.amazonaws.com/i/meu46l91dzprj6xqwjel.png" alt="Shipping" width="250"/>

A release is based on a [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). It has a descriptive text (e.g. changelog) and can have files attached to it. That's exactly what we need.

Electron Forge provides a [GitHub publisher](https://www.electronforge.io/config/publishers/github) that does all the work for us. We need to install it in our example project.

```
npm install -D @electron-forge/publisher-github
```

Then we add the following configuration to the `package.json` file (make sure to adapt this to your repository).

```json
{
...
  "config": {
    "forge": {
    ...
      "publishers": [
        {
          "name": "@electron-forge/publisher-github",
          "config": {
            "repository": {
              "owner": "erikhofer",
              "name": "electron-publish-example"
            }
          }
        }
      ]
    }
  }
}
```

Finally, we create a second workflow `.github/workflows/release.yml` with the following content.

```yaml
name: Release
on:
  release:
    types:
      - created

jobs:

  publish_on_linux:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2       
    - uses: actions/setup-node@master
      with:
        node-version: 14
    - name: install dependencies
      run: npm install
    - name: publish
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: npm run publish

  publish_on_mac:
    runs-on: macos-latest
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-node@master
      with:
        node-version: 14
    - name: install dependencies
      run: npm install
    - name: publish
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: npm run publish
      
  publish_on_win:
    runs-on: windows-latest
    steps:
    - uses: actions/checkout@v2       
    - uses: actions/setup-node@master
      with:
        node-version: 14
    - name: install dependencies
      run: npm install
    - name: publish
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: npm run publish
```

It is basically the same as `build.yaml` but uses the `publish` script (`make` is not needed, this would build the app twice). It also accesses the `GITHUB_TOKEN` and is only executed for created releases.

After pushing all changes, we can go to the "Releases" section in the "Code" tab of the repository and click "Create a new release". As "Tag version" we chose "v1.0.0" and click "Publish release". In the "Actions" tab we can now see that our newly created workflow is executed.

After it has finished, we go back to our release ([example](https://github.com/erikhofer/electron-publish-example/releases/tag/v1.0.0)). It should now have the packaged application for all platforms attached.

![Release](https://dev-to-uploads.s3.amazonaws.com/i/c9ch2ztfju7b9atb04f9.png)

#Draft Releases <a name="draft"></a>

So far we pushed a tag and then manually created a release via the GitHub website. With this approach, the release is published immediately and the files are attached later&mdash;assuming the action is executed successfully. If something goes wrong, watchers have already been notified about the release via email.

Ideally, we want to draft a release, attach the application files and then publish, if everything looks good. There is, however, a caveat.

> The `release` event is not triggered for draft releases. ([Source](https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows#release))

That means that if we create a draft release and then publish it, the `created` activity is never detected. If we use `published` instead, we still have the same behavior as before.

We can solve this by letting Electron Forge create the release. For that, we first change the workflow so that it is executed for all version tags.

```yaml
name: Release
on:
  push:
    tags:
      - 'v*'
```

This is sufficient for automatically creating releases. Additionally, we can now configure the publisher to create a draft instead.

```json
"publishers": [
  {
    "name": "@electron-forge/publisher-github",
    "config": {
      "repository": {
        "owner": "erikhofer",
        "name": "hello-electron"
      },
      "draft": true
    }
  }
]
```

To create tags, we can utilize the [`npm version`](https://docs.npmjs.com/cli/v6/commands/npm-version) command. It automatically updates the `package.json` and `package-lock.json`. Let's create a new version `1.1.0`.

```
npm version minor
git push --follow-tags
```

⚠ Make sure to push the created tag to GitHub. By default, Git does not push tags.

After the workflow has finished, we go to the releases page again. We can now see the automatically created draft release.

![Draft Release](https://dev-to-uploads.s3.amazonaws.com/i/g6css3hjjhq6r5ohseg6.png)

If everything looks fine, click on "Edit", enter a description and click "Publish release". We now have a proper setup for distributing our multi-platform Electron application. ✨

#Outlook <a name="outlook"></a>

If we actually want to pubslish a software that is used by the public, we need to address [code signing](https://www.electronjs.org/docs/tutorial/code-signing) as the next step. It is a security mechanism and at least on macOS, there is no practical way around it. It is also needed for auto-updating the application on other platforms.

Electron Forge and other tools have convenient built-in support for the code signing process. But be aware that certificates need to be purchased with an anual fee.

Speaking of auto-updating, this is an interesting topic to look into next. Now that we have a setup to host our binaries via GitHub releases, we can also use this for update distribution. There is even a completely free service for open source applications. For more information, take a look into the [Electron Forge docs](https://www.electronforge.io/advanced/auto-update).