I spent a good chunk of the last month streamlining Wilderplace’s build and release process - read on to learn how to do the same for your HTML game.
What is Wilderplace? It’s an HTML app built with Snowpack and published to Steam and packaged into a desktop app using Electron. While it’s possible to manually build Electron distributables and upload them to Steam, the process is fraught, and I wanted no-stress deployments.
My goal was to be able to trigger a workflow that publishes new versions of my game on Windows, Linux, and Mac by pushing a git tag to my game’s Github repo. After many roadblocks, I managed to pull it off. In this post, I’ll first share my solution, and then talk through some of the obstacles I encountered.
Before I dive in, I want to give some credit to Drew Conley, author of “How to package your web project for platforms like Steam”, for providing a starting point for my own workflow.
Here’s how Wilderplace’s repo is set up:
wilderplace/
├─ package.json
├─ .github/
│ ├─ publish.yml
├─ scripts/
│ ├─ prepare-electron.js
├─ src/
├─ build/
│ ├─ index.html
│ ├─ (css, js, ect.)
├─ electron/
│ ├─ package.json
│ ├─ app-icons/
│ │ ├─ icon.icns
│ │ ├─ icon.ico
│ ├─ out-resources/
│ ├─ src/
│ │ ├─ index.js
You’ll notice I have a second JavaScript project nested inside my root project under electron/
. Electron Forge, the bundling tool I use, is rigid about repo structure. I want to be able to run my game in the browser during development, using Snowpack’s dev server, and only package for Electron as part of my release process. I chose to use a separate directory for all things Electron. I could have worked directly with Electron Packager, which would give me more flexibility about repo structure, but I like the simplicity of Electron Forge.
Important takeaways from this diagram:
build/
. It doesn’t matter if you use a bundler or rely on Chrome’s JavaScript module system. Both styles of HTML apps work with Electron./build
to /electron/out-resources
before running the Electron make command. See Preparing electron/
for the make step for details on how I do this.electron/src/index.js
is my “main” Electron process file. It must be in this location to conform to Electron Forge. Learn how to create your main file from the Electron Quick Start guide.The Electron Forge build system lives in electron/
, and this build system has it’s own package.json file. A couple comments about my forge config:
.zip
maker for all platforms.icon
option takes a url template, and will look for platform-specific file endings (icns on windows, ico on mac).Here’s a minimal version of electron/package.json
:
{
"name": "wilderplace",
"productName": "wilderplace",
"version": "0.0.0-alpha.10",
"description": "A divine garden adventure",
"main": "src/index.js",
"scripts": {
"make": "electron-forge make"
},
"config": {
"forge": {
"packagerConfig": { "icon": "./app-icons/icon" },
"makers": [{ "name": "@electron-forge/maker-zip" }]
}
},
"devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.63",
"@electron-forge/maker-zip": "^6.0.0-beta.63",
"electron": "^17.0.1",
}
}
electron/
for the make stepI need to be able to copy files from the root project into the Electron project, and overwrite the Electron project package.json version key, before we make our distributables. To do this, I used a node script located at scripts/prepare-electron.js
:
const fs = require('fs');
const path = require('path');
const rimraf = require('rimraf');
const copydir = require('copy-dir');
const electronPkgJson = require('../electron/package.json');
const parentPkgJson = require('../package.json');
const src = path.join(__dirname, '..', 'build');
const dest = path.join(__dirname, '..', 'electron', 'out-resources');
// Copy build dir to electron out-resources, which is where electron-forge
// looks for source files.
rimraf.sync(dest);
copydir(src, dest, {});
// Overwrite nested package.json version, because this is what electron-forge
// uses to set the version in the Electron app.
electronPkgJson.version = parentPkgJson.version;
fs.writeFileSync(
path.join(__dirname, '..', 'electron', 'package.json'),
JSON.stringify(electronPkgJson, null, 2)
);
I’m using Github Actions for release automation. Here’s my workflow file, located at ./github/publish.yml
:
name: Build/release
# Run this workflow whenever a tag that starts with v is pushed to github.
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v1
- name: Install Node.js & NPM
uses: actions/setup-node@v2
with:
node-version: '14.2.0'
# Wine is required in order to generate an Electron build for Windows from
# an Ubuntu machine.
- name: Install Wine
run: |
sudo dpkg --add-architecture i386
sudo apt update
sudo apt install -y wine-stable
# Move all the game files into a build/ directory.
- name: Install and build the HTML app
run: |
npm ci
npm run build
# npm run make uses Electron Forge, see package.json above for details.
# We have to run make three times, once for each platform. This step
# generates three directories, corresponding to each platform.
# Notice the working-directory is now ./electron!
- name: Copy HTML app to electron/, and make Electron packages
run: |
node ../scripts/prepare-electron.js
npm run make
npm run make -- --platform=win32
npm run make -- --platform=darwin
working-directory: ./electron
# View this action's source code and documentation at
# https://github.com/game-ci/steam-deploy. Note that this action only
# works in ubuntu environments. If you want to run in Mac OS or Windows
# environments, you'll need to write your own deploy script.
# The depot{number}Path parameters correspond to the directories created
# in the previous step by npm run make.
- name: Deploy to Steam
uses: game-ci/steam-deploy@v1.1.0
with:
username: ${{ secrets.STEAM_USERNAME }}
password: ${{ secrets.STEAM_PASSWORD }}
configVdf: ${{ secrets.STEAM_CONFIG_VDF}}
ssfnFileName: ${{ secrets.STEAM_SSFN_FILE_NAME }}
ssfnFileContents: ${{ secrets.STEAM_SSFN_FILE_CONTENTS }}
appId: your app id goes here
buildDescription: ${{ github.ref }}
rootPath: electron/out
depot1Path: ${{ github.repository }}-darwin-x64
depot2Path: ${{ github.repository }}-win32-x64
depot3Path: ${{ github.repository }}-linux-x64
releaseBranch: prerelease
That covers my basic setup. As long as your repo is structured similarly to mine, and you’ve got a build step in your root app that places all source files including an index.html
file in build/
, this setup should work for you.
An earlier version of my build script ran on a build matrix including macos-latest
and win32-latest
, which I assumed was the only way to build for each platform, until I read the Electron Packager docs more closely. Turns out you can build for every platform from Ubuntu as long as you install Wine. This allowed me to simplify and speed up my action, and fully rely on game-ci/steam-deploy
rather than writing my own platform-specific scripts for running steamcmd
. Getting the Wine install instructions just right took a few iterations, but the command in the workflow file above has been working reliably.
In order to run steamcmd as part of a Github action, you need to be authenticated with Steam’s two factor authentication system, Steam Guard. The contributors behind steam-deploy
came up with a good solution to authenticating on Github Actions machines, but the documentation is from the point of view of a Linux user. If you’re running Mac OS, config/config.vdf
and ssfn<numbers>
will be located at ~/Library/Application\ Support/Steam/
. Besides these file locations, steam-deploy’s documentation should apply to Mac OS users.
For more details on authenticating steamcmd with Steam Guard, check out Steam’s build script documentation.
Every platform came with its quirks. Some gotchas:
steamcmd
. const mainWindow = new BrowserWindow({
autoHideMenuBar: true,
titleBarStyle: 'hidden',
fullscreen: true,
/*...other settings not related to window sizing*/
});
--no-sandbox
launch option argument.Steamworks is Steam’s publishing toolkit. It’s pretty confusing. Plus, as far as I can tell there’s no way to totally automate working with builds, depots, installation options, or publishing. This Reddit comment from u/PirateHearts is the best explanation I found of how things work, give it a read.
My basic workflow in the Steamworks UI is as follows:
One thing I haven’t dove into is integration with Steam-specific APIs like acheivements and the Steam overlay. I briefly looked at https://github.com/greenheartgames/greenworks but was put off by the install instructions, it’s implementation as a Node.js addon, and some of the bug reports I’ve seen from Electron users. I’d love to hear how other game devs managed to work with Steam APIs in Electron apps.
Now that Wilderplace has a great CD system up and running, we’ll be shifting from Alpha to Beta, with all future pre-release testing happening through Steam. Stay tuned for more Beta news soon!
I hope this overview of Wilderplace’s publishing setup is useful for other folks who want to release HTML games on Steam! I’d love to hear feedback or ideas for how to improve or generalize my solution. Let me know on Twitter at @samanbb.
Wilderplace can now be wishlisted on Steam! The best way to keep up with the game’s development or request access to the Alpha is to join the discord.