Deploying Node.js apps to Railway
May 16, 2026Railway is probably one of the fastest ways to get a Node.js API live. GitHub Integration Connect your GitHub repository to Railway…
read more →
Railway is probably one of the fastest ways to get a Node.js API live. GitHub Integration Connect your GitHub repository to Railway…
read more →
Releasing Electron desktop apps can be automated with Electron Forge and GitHub Actions. This post covers the main steps for automation.
repo and write:packages permissions) as a GitHub Action secret (GH_TOKEN)Run the following commands to configure Electron Forge for the app release.
npm i @electron-forge/cli @electron-forge/publisher-github -Dnpm i electron-squirrel-startupnpx electron-forge import
The last command should install the necessary dependencies and add a configuration file.
Update the forge.config.js file with the bin field containing the app name and ensure the GitHub publisher points to the right repository.
Put Windows and MacOS icons paths in the packagerConfig.icon field, Windows supports ico files with 256x256 resolution, and MacOS supports icns icons with 512x512 resolution (1024x1024 for Retina displays). Linux supports png icons with 512x512 resolution, also include its path in the BrowserWindow constructor config within the icon field.
// forge.config.jsconst path = require('path');module.exports = {packagerConfig: {asar: true,icon: path.join(process.cwd(), 'main', 'build', 'icon')},rebuildConfig: {},makers: [{name: '@electron-forge/maker-squirrel',config: {bin: 'Electron Starter'}},{name: '@electron-forge/maker-dmg',config: {bin: 'Electron Starter'}},{name: '@electron-forge/maker-deb',config: {bin: 'Electron Starter',options: {icon: path.join(process.cwd(), 'main', 'build', 'icon.png')}}},{name: '@electron-forge/maker-rpm',config: {bin: 'Electron Starter',icon: path.join(process.cwd(), 'main', 'build', 'icon.png')}}],plugins: [{name: '@electron-forge/plugin-auto-unpack-natives',config: {}}],publishers: [{name: '@electron-forge/publisher-github',config: {repository: {owner: 'delimitertech',name: 'electron-starter'},prerelease: true}}]};
Upgrade the package version before releasing the app. The npm script for publishing should use publish command. Set productName field to the app name.
// package.json{// ..."version": "1.0.1","scripts": {// ..."publish": "electron-forge publish"},"productName": "Electron Starter"}
GitHub Action workflow for manually releasing the app for Linux, Windows, and MacOS should contain the below configuration.
# .github/workflows/release.ymlname: Release appon:workflow_dispatch:jobs:build:strategy:matrix:os:[{ name: 'linux', image: 'ubuntu-latest' },{ name: 'windows', image: 'windows-latest' },{ name: 'macos', image: 'macos-latest' },]runs-on: ${{ matrix.os.image }}steps:- name: Github checkoutuses: actions/checkout@v4- name: Use Node.jsuses: actions/setup-node@v4with:node-version: 20- run: npm ci- name: Publish appenv:GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}run: npm run publish
Add the following code in the main process to prevent Squirrel.Windows launches your app multiple times during the installation/updating/uninstallation.
// main/index.jsif (require('electron-squirrel-startup') === true) app.quit();
GitHub action is a CI/CD tool integrated within GitHub repositories that can run different kinds of jobs (building, testing, deployment). Store workflow files in .github/workflows inside the repository, which will be triggered based on specified conditions.
This post covers GitHub actions basics, from specifying the workflow name to configuring different jobs.
Specify the name of the workflow with the name field.
# .github/workflows/config.ymlname: CI/CD pipeline
on field specifies when the workflow should be running.
The following configuration runs on every push to a specific branch.
# .github/workflows/config.ymlon:push:branches:- main
The following configuration runs on every push to every branch.
# .github/workflows/config.ymlon:push:branches:- '*'
The following configuration runs at a specified interval (e.g., every hour).
# .github/workflows/config.ymlon:schedule:- cron: '0 * * * *'
The following configuration enables manual triggering. Trigger it on the Actions tab by selecting the workflow and clicking the Run workflow button.
Use a manual trigger to upload apps to the Google Play console or update the GitHub profile Readme.
# .github/workflows/config.ymlon:workflow_dispatch:
Specify with the env field. Set repository secrets in Settings → Secrets and variables → Actions page.
# .github/workflows/config.ymlenv:API_KEY: ${{ secrets.API_KEY }}
Specify the job name with the name field. Otherwise, the workflow will use the jobs item as the job name.
Every job should have a runs-on field specified for the machine, which will be running on (e.g., ubuntu-latest) or container field with set Docker image (e.g., node:20.9.0-alpine3.17)
Every job can have a separate working directory in case you have multiple subdirectories, and you want to run a different job in a different subdirectory so you can specify it within the defaults field
jobs:job-name:defaults:run:working-directory: directory-name
You can specify multiple tasks inside one job. Every task can have the following fields
name - task nameuses - GitHub action path from GitHub Marketplacewith - parameters for the specified GitHub actionrun - bash commandsenv - environment variables# .github/workflows/config.ymljobs:build:name: Custom build jobruns-on: ubuntu-latest# container: node:20.9.0-alpine3.17steps:- name: Checkoutuses: actions/checkout@v4- name: Configure Node.jsuses: actions/setup-node@v4with:node-version: 20- name: Install and buildrun: |npm cinpm run build
Use the needs field for running jobs sequentially. It specifies a job that has to be finished before starting the next one. Otherwise, jobs will run in parallel.
# .github/workflows/config.ymljobs:build:# ...deploy:name: Custom deploy jobruns-on: ubuntu-latestneeds: buildsteps:- name: Deployrun: |npm run deploy
Every job can run multiple times with different versions using a matrix strategy (e.g., Node versions 18 and 20 or multiple OS versions inside an array of objects).
# .github/workflows/config.ymljobs:build:name: Custom build jobstrategy:matrix:node-version: [18, 20]os:[{ name: 'linux', image: 'ubuntu-latest' },{ name: 'windows', image: 'windows-latest' },{ name: 'macos', image: 'macos-latest' },]runs-on: ${{ matrix.os.image }}steps:- name: Checkoutuses: actions/checkout@v4- name: Configure Node.js ${{ matrix.node-version }}uses: actions/setup-node@v4with:node-version: ${{ matrix.node-version }}- name: Install and buildrun: |npm cinpm run build
Every job can provision databases for e2e tests with a services field like Postgres in the following example.
# .github/workflows/config.ymljobs:build:name: Custom build jobruns-on: ubuntu-lateststrategy:matrix:database-name:- test-dbdatabase-user:- usernamedatabase-password:- passworddatabase-host:- postgresdatabase-port:- 5432services:postgres:image: postgres:latestenv:POSTGRES_DB: ${{ matrix.database-name }}POSTGRES_USER: ${{ matrix.database-user }}POSTGRES_PASSWORD: ${{ matrix.database-password }}ports:- ${{ matrix.database-port }}:${{ matrix.database-port }}# Set health checks to wait until postgres has startedoptions: --health-cmd pg_isready--health-interval 10s--health-timeout 5s--health-retries 5steps:# ...- run: npm run test:e2eenv:DATABASE_URL: postgres://${{ matrix.database-user }}:${{ matrix.database-password }}@${{ matrix.database-host }}:${{ matrix.database-port }}/${{ matrix.database-name }}
Passing artifacts between jobs can be done with uploading (actions/upload-artifact) and downloading (actions/download-artifact) actions.
# .github/workflows/config.ymljobs:build:runs-on: ubuntu-lateststeps:- name: Checkoutuses: actions/checkout@v4- name: Configure Node.jsuses: actions/setup-node@v4with:node-version: 20- name: Install and buildrun: |npm cinpm run build- name: Upload artifactuses: actions/upload-artifact@v3with:name: artifactpath: publicdeploy:needs: buildruns-on: ubuntu-lateststeps:- name: Checkoutuses: actions/checkout@v4- name: Download artifactuses: actions/download-artifact@v3with:name: artifact# ...
You can use act to run GitHub actions locally.
Install it and run it with the following commands.
curl -s https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bashcp ./bin/act /usr/local/bin/actact
This post covers instructions for deploying to GitHub pages and domain setup. GitHub Pages GitHub provides a way to host a static website (e…
read more →
Internal testing on Google Play Console is used for testing new app versions before releasing them to the end users. This post covers the main notes from setting up the app (on the Google Play Console) to automatic uploads.
Create an app with the essential details, such as app name, default language, and app type, and choose if it is paid or free.
Add email addresses to the email list.
Generate a private signing key with a password using keytool
sudo keytool -genkey -v -keystore my-upload-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000
Move the generated file to android/app directory.
Edit ~/.gradle/gradle.properties to add the following keys and replace the alias and password with the correct values.
MYAPP_UPLOAD_STORE_FILE=my-upload-key.keystoreMYAPP_UPLOAD_KEY_ALIAS=my-key-aliasMYAPP_UPLOAD_STORE_PASSWORD=*****MYAPP_UPLOAD_KEY_PASSWORD=*****
Edit android/app/build.gradle to add the release signing config, which uses the generated key.
android {...defaultConfig { ... }signingConfigs {...release {if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) {storeFile file(MYAPP_UPLOAD_STORE_FILE)storePassword MYAPP_UPLOAD_STORE_PASSWORDkeyAlias MYAPP_UPLOAD_KEY_ALIASkeyPassword MYAPP_UPLOAD_KEY_PASSWORD}}}buildTypes {...release {...signingConfig signingConfigs.release}}}
Update versionCode and versionName fields in android/app/build.gradle file before generating the bundle. The version code should be incremented by 1.
android {...defaultConfig {...versionCode 2versionName "1.1.0"...}...
Generate the Android app bundle with the following command.
cd android./gradlew bundleRelease
android/app/build/outputs/bundle/release/app-release.aab is the path for the generated file.
Upload the aab file and write the release name and notes. The link for downloading the app should be available on the Testers tab.
This section configures the necessary steps for the pipeline with Github actions.
Version apps with np and react-native-version packages running np script.
// package.json{"scripts": {"np": "np --no-publish","postversion": "react-native-version -t android"},"repository": {"type": "git","url": "<REPOSITORY_URL>"}}
Remove the release signing config from android/app/build.gradle file. An app will be bundled first and signed after that.
Get the signing key with the following command and set it as ANDROID_SIGNING_KEY Github action secret in the Settings → Secrets → Actions page.
cd android/appopenssl base64 < my-upload-key.keystore | tr -d '\n' | tee my-upload-key.keystore.base64.txt
Reuse credentials from ~/.gradle/gradle.properties file and set them as Github secrets.
For automatic uploads, create a service account by following the next steps.
Learn how to create service accounts, follow the mentioned stepsANDROID_SERVICE_ACCOUNT_JSON_TEXT Github secretThe following pipeline sets up the necessary tools, does CI checks (linting, testing, audit), generates the app bundle, signs it, and uploads it to the Google Play console.
name: Android Buildon:push:branches:- releasejobs:android-build:name: Android Buildruns-on: ubuntu-lateststeps:- name: Check out Git repositoryuses: actions/checkout@v4- name: Set up JDKuses: actions/setup-java@v3with:java-version: 18distribution: temurin- name: Set up Android SDKuses: android-actions/setup-android@v3- name: Use Node.jsuses: actions/setup-node@v4with:node-version: 20- run: npm ci- run: npm run lint- run: npm test- run: npm audit- name: Make Gradlew Executablerun: cd android && chmod +x ./gradlew- name: Generate App Bundlerun: |cd android && ./gradlew clean && \./gradlew bundleRelease --no-daemon- name: Sign App Bundleid: sign_aabuses: r0adkll/sign-android-release@v1with:releaseDirectory: android/app/build/outputs/bundle/releasesigningKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }}alias: ${{ secrets.ANDROID_ALIAS }}keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}- name: Upload App Bundle to Google Playuses: r0adkll/upload-google-play@v1with:serviceAccountJsonPlainText: ${{ secrets.ANDROID_SERVICE_ACCOUNT_JSON_TEXT }}packageName: com.flatmeappreleaseFiles: android/app/build/outputs/bundle/release/*.aabtrack: internalstatus: draftinAppUpdatePriority: 2
Edit the draft release and roll it out.
In case of a problem with signatures not matching the previously installed version, uninstall the app with the following commands.
adb devices# adb -s <DEVICE_KEY> uninstall <PACKAGE_NAME>adb -s emulator-5554 uninstall "com.yourapp"
If the link for downloading the app installs some old version, clear the cache and data of the Google Play Store app on your device
This post covers the main notes from setting up the Heroku CLI tool to CI/CD pipeline with Github actions . Prerequisites Run the following…
read more →
This post covers the main notes from setting up the Fly.io CLI tool to CI/CD pipeline with Github actions . Prerequisites Run the following…
read more →
End-to-end tests should use a real database connection, and provisioning container service for the Postgres database can be automated using Github actions. The environment variable for the connection string for the newly created database can be set in the step for running e2e tests. The same goes for the Redis instance.
# ...jobs:build:# Container must run in Linux-based operating systemsruns-on: ubuntu-latest# Image from Docker hubcontainer: node:20.9.0-alpine3.17# ...strategy:matrix:# ...database-name:- e2e-testing-dbdatabase-user:- usernamedatabase-password:- passworddatabase-host:- postgresdatabase-port:- 5432redis-host:- redisredis-port:- 6379services:postgres:image: postgres:latestenv:POSTGRES_DB: ${{ matrix.database-name }}POSTGRES_USER: ${{ matrix.database-user }}POSTGRES_PASSWORD: ${{ matrix.database-password }}ports:- ${{ matrix.database-port }}:${{ matrix.database-port }}# Set health checks to wait until postgres has startedoptions: --health-cmd pg_isready--health-interval 10s--health-timeout 5s--health-retries 5redis:image: redis# Set health checks to wait until redis has startedoptions: >---health-cmd "redis-cli ping"--health-interval 10s--health-timeout 5s--health-retries 5steps:# ...- run: npm run test:e2eenv:DATABASE_URL: postgres://${{ matrix.database-user }}:${{ matrix.database-password }}@${{ matrix.database-host }}:${{ matrix.database-port }}/${{ matrix.database-name }}REDIS_URL: redis://${{ matrix.redis-host }}:${{ matrix.redis-port }}# ...
This blog post covers what you need to know to automate publishing an npm package to Npm and Github package registries. It can also be…
read more →