I have been working on a personal desktop project (Deskaide) for about one year and finished some basic features recently. So I decided to publish an alpha version to GitHub. Instead of upgrading the version number and publishing it manually, I was looking for a way to automate the process.
The template I was following to structure the project uses electron builder to build the project. It uses the current date as the upcoming version number. It seems okay, but the version number does not indicate what type of changes it includes. I believe semantic-release has the solution, and I used it in some of my other projects.
I started exploring a solution. I found an electron release plugin for the semantic release package but found it complicated. I decided to hack the semantic release process and use it only to update the version number (it can publish) and push the tag to GitHub. Electron builder can publish the release to GitHub, so I don’t need any extra hacking to publish the release. The process looks like this:
- Configure semantic release
- Prepare the npm script
- Configure GitHub action
Let’s explore the process in detail.
Configure semantic release
First, we will install semantic-release
as a dev dependency with other related plugins.
npm i -D semantic-release @semantic-release/commit-analyzer @semantic-release/npm @semantic-release/git semantic-release-export-data
Now we need to configure it in such a way that it will calculate the upcoming version number from the commit message and expose it to the GitHub workflow environment. We need to add the following code in a file named release.config.js
at the root of our project folder.
module.exports = {
branches: [
'main',
'next',
'next-major',
{ name: 'beta', prerelease: true },
{ name: 'alpha', prerelease: true },
],
plugins: [
'@semantic-release/commit-analyzer',
'semantic-release-export-data',
'@semantic-release/npm',
[
'@semantic-release/git',
{
assets: ['package.json', 'package-lock.json'],
},
],
],
};
Here semantic-release-export-data plugin will help us to expose the next release version to the GitHub action environment. We will use that version number to name our releases in the build step.
Prepare the npm script
We need an npm script to run the semantic-release
command. In our package.json
in the scripts section, we will add the following commands:
"release": "electron-builder --config .electron-builder.config.js --publish always",
"updateVersion": "semantic-release"
In the release
script, we added --publish always
to tell electron builder to publish it to GitHub.
Configure GitHub action
Now we will configure our GitHub workflow to combine all the steps.
- First, we will set up a job to run semantic-release in dry mode to expose the upcoming version number (if releasable) to the GitHub action environment (job name
get-next-version
) - Then, we will create a release draft with a release note (job name
draft
) - Finally, we will build the production release and upload the artifacts to GitHub (job name
upload_artifacts
).
Before building the production release, we will run the updateVersion
script we defined earlier. It will generate the upcoming version number and tag and push the version tag to GitHub. Then we will put the version number in a file named meta.json
inside our buildResources
folder at the root of our project. Finally, we will build our project for production and run the release
script to release the app to GitHub. Following is the complete workflow configuration:
name: Release A New Version
on: [workflow_call, workflow_dispatch]
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: 'bash'
env:
GH_TOKEN: ${{ secrets.github_token }}
jobs:
get-next-version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm ci
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npx semantic-release --dry-run
id: get-next-version
env:
GITHUB_TOKEN: ${{ secrets.github_token }}
outputs:
new-release-published: ${{ steps.get-next-version.outputs.new-release-published }}
new-release-version: ${{ steps.get-next-version.outputs.new-release-version }}
draft:
needs: [get-next-version]
runs-on: ubuntu-latest
if: needs.get-next-version.outputs.new-release-published == 'true'
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
with:
node-version: 16
- name: Show version
run: echo "Next Version is ${{ needs.get-next-version.outputs.new-release-version }} (${{ github.ref_name }})"
- name: Get last git tag
id: tag
run: echo "last-tag=$(git describe --tags --abbrev=0 || git rev-list --max-parents=0 ${{github.ref}})" >> $GITHUB_OUTPUT
- name: Generate release notes
uses: ./.github/actions/release-notes
id: release-note
with:
from: ${{ steps.tag.outputs.last-tag }}
to: ${{ github.ref }}
include-commit-body: true
include-abbreviated-commit: true
- name: Delete outdated drafts
uses: hugo19941994/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Release Draft
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.github_token }}
with:
prerelease: true
draft: true
tag_name: v${{ needs.get-next-version.outputs.new-release-version }}
name: v${{ needs.get-next-version.outputs.new-release-version }}
body: ${{ steps.release-note.outputs.release-note }}
outputs:
release-note: ${{ steps.release-note.outputs.release-note }}
version: ${{ needs.get-next-version.outputs.new-release-version }}
upload_artifacts:
needs: [draft]
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'npm'
- run: npm ci
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- name: Prepare release notes
env:
RELEASE_NOTE: ${{ needs.draft.outputs.release-note }}
run: echo "$RELEASE_NOTE" >> ./buildResources/release-notes.md
- name: Show version
run: echo "Next Version is ${{ needs.draft.outputs.version }} (${{ github.ref_name }})"
- name: Update and push version tag
run: npm run updateVersion
- name: Write next version to meta.json
run: echo "{\"version\":\"${{ needs.draft.outputs.version }}\"}" >| ./buildResources/meta.json
- name: Build the app
env:
MODE: 'production'
run: npm run build
- name: Compile & release Electron app
id: release-to-github
env:
VITE_APP_VERSION: ${{ needs.draft.outputs.version }}
run: npm run release
- name: Delete tag for failed release
if: steps.release-to-github.outcome == 'failure'
run: git push --delete origin v${{ needs.draft.outputs.version }}```
If you want to see the complete process in action or want to contribute to the open source Deskaide
project, please check out the repository here.
Hard Lesson
If you use modular GitHub workflows (different jobs in different files) and any lower-level actions need to access any secret value, you must tell the root workflow to pass the secret value to that action.
In my ci.yaml
file:
# This workflow is the entry point for all CI processes.
# It is from here that all other workflows launch.
on:
push:
branches:
- main
- alpha
- beta
paths-ignore:
- '**.md'
- .editorconfig
- .gitignore
pull_request:
paths-ignore:
- '**.md'
- .editorconfig
- .gitignore
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
uses: ./.github/workflows/lint.yml
typechecking:
uses: ./.github/workflows/typechecking.yml
tests:
uses: ./.github/workflows/tests.yml
draft_release:
if: github.event_name == 'push' && (github.ref_name == 'alpha' || github.ref_name == 'beta' || github.ref_name == 'main')
needs: [typechecking, tests]
uses: ./.github/workflows/release.yml
secrets: inherit # <- THIS IS WHAT I AM TALKING ABOUT