Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/actions/setup-build-env/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Setup build environment
description: >
Install OS-native electron-builder dependencies (Linux), Node.js, and restore the
Electron/electron-builder download caches. Shared by ci.yml and release.yml so the
Linux package list, Flatpak runtime version, and cache paths have one source of truth.

runs:
using: composite
steps:
# xvfb is installed unconditionally here (even though only ci.yml's E2E step uses it)
# so both workflows share one Linux package list instead of two lists that can drift.
# It's a tiny package; installing it unused in release.yml costs nothing.
# rpm is electron-builder's packaging tool for the `rpm` Linux target — ubuntu-latest
# doesn't ship it by default (see electron-builder's multi-platform-build docs).
- name: Install Linux native dependencies (electron-builder, xvfb for headless E2E)
if: runner.os == 'Linux'
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf flatpak flatpak-builder xvfb rpm

- name: Install Flatpak runtime (electron-builder flatpak target)
if: runner.os == 'Linux'
shell: bash
run: |
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
sudo flatpak install --system -y flathub org.freedesktop.Platform//25.08 org.freedesktop.Sdk//25.08 org.electronjs.Electron2.BaseApp//25.08

- name: Setup Node.js 24
uses: actions/setup-node@v7
with:
node-version: '24'
cache: 'npm'

# actions/setup-node's cache:npm only covers the npm registry cache — it doesn't touch
# the Electron binary zip (~100-200MB) or electron-builder's own toolchain downloads
# (NSIS, winCodeSign, etc.), which live in a separate cache dir and get re-downloaded on
# every run otherwise. Cache path list covers all 3 OSes; actions/cache skips whichever
# paths don't exist on the current runner, and the OS-prefixed key keeps them separate.
- name: Cache Electron/electron-builder downloads
uses: actions/cache@v6
with:
path: |
~/.cache/electron
~/.cache/electron-builder
~/Library/Caches/electron
~/Library/Caches/electron-builder
~\AppData\Local\electron\Cache
~\AppData\Local\electron-builder\Cache
key: ${{ runner.os }}-electron-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-electron-
113 changes: 113 additions & 0 deletions .github/scripts/generate-changelog.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/usr/bin/env node
// Generates the GitHub release notes body for the range between the last git tag (or the
// start of history, if this is the first release) and HEAD, grouped by conventional-commit
// type. Writes CHANGELOG_BODY.md for use as the release notes — nothing is persisted to the
// repo (no CHANGELOG.md); the GitHub release itself is the changelog's home.
//
// Usage: node generate-changelog.mjs <newVersion> <owner/repo>
// newVersion e.g. "1.2.3" (no leading "v")
// owner/repo e.g. "cchandurkar/electron-angular-template" (for the compare link)

import { execSync } from 'node:child_process';
import { writeFileSync } from 'node:fs';

const [, , newVersion, repoSlug] = process.argv;
if (!newVersion || !repoSlug) {
console.error('Usage: generate-changelog.mjs <newVersion> <owner/repo>');
process.exit(1);
}

function sh(cmd) {
return execSync(cmd, { encoding: 'utf8' }).trim();
}

const MAX_ENTRIES = 150;
const TYPES = [
['feat', 'Features'],
['fix', 'Bug Fixes'],
['refactor', 'Refactoring'],
['perf', 'Performance'],
['revert', 'Reverts']
];

const typeLabel = new Map(TYPES);

let lastTag = '';
try {
lastTag = sh('git describe --tags --abbrev=0');
} catch {
// No tags yet — this is the first release; changelog covers full history.
}

const range = lastTag ? `${lastTag}..HEAD` : '';
const log = sh(`git log ${range} --format=%s`.trim());
const subjects = log ? log.split('\n') : [];

// Matches this repo's commit convention: `type(scope): description` (see root AGENTS.md).
const pattern = /^(\w+)(\(.+?\))?(!)?: (.+)$/;
const groups = new Map();
let matchedCount = 0;

for (const subject of subjects) {
const m = subject.match(pattern);
if (!m) continue;
const [, type, scopeRaw, , message] = m;
if (!typeLabel.has(type)) continue;
matchedCount++;
const scope = scopeRaw ? scopeRaw.slice(1, -1) : null;
const line = scope ? `- **${scope}**: ${message}` : `- ${message}`;
if (!groups.has(type)) groups.set(type, []);
groups.get(type).push(line);
}

let truncatedNote = '';
if (matchedCount > MAX_ENTRIES) {
let kept = 0;
for (const [type] of TYPES) {
const lines = groups.get(type);
if (!lines) continue;
if (kept >= MAX_ENTRIES) {
groups.delete(type);
continue;
}
const remaining = MAX_ENTRIES - kept;
if (lines.length > remaining) {
groups.set(type, lines.slice(0, remaining));
}
kept += groups.get(type).length;
}
truncatedNote = `\n_…and ${matchedCount - MAX_ENTRIES} more change(s) not shown here — see the full changelog link below._\n`;
}

const sections = TYPES.filter(([type]) => groups.has(type))
.map(([type, label]) => `### ${label}\n\n${groups.get(type).join('\n')}`)
.join('\n\n');

const compareLink = lastTag
? `**Full Changelog**: https://github.com/${repoSlug}/compare/${lastTag}...v${newVersion}`
: `**Full Changelog**: https://github.com/${repoSlug}/commits/v${newVersion}`;

// Fallback for forks that don't use Conventional Commits: `sections` is only empty here when
// zero commits matched the type-prefixed pattern above. If commits exist but none matched,
// list them verbatim instead of silently claiming "no changes" when real work happened.
let sectionsBody = sections;
let noteSuffix = truncatedNote;
if (!sections && subjects.length > 0) {
let rawList = subjects;
if (rawList.length > MAX_ENTRIES) {
noteSuffix = `\n_…and ${rawList.length - MAX_ENTRIES} more change(s) not shown here — see the full changelog link below._\n`;
rawList = rawList.slice(0, MAX_ENTRIES);
}
sectionsBody = `### Changes\n\n${rawList.map(subject => `- ${subject}`).join('\n')}`;
}

const body =
(sectionsBody || '_No user-facing changes recorded since the last release._') +
noteSuffix +
`\n\n${compareLink}\n`;

writeFileSync('CHANGELOG_BODY.md', body);

console.log(
`Release notes generated for v${newVersion} (${matchedCount} matched commit(s), lastTag=${lastTag || '<none>'}).`
);
38 changes: 2 additions & 36 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,8 @@ jobs:
- name: Checkout code
uses: actions/checkout@v7

- name: Install Linux native dependencies (electron-builder)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf flatpak flatpak-builder xvfb

- name: Install Flatpak runtime (electron-builder flatpak target)
if: runner.os == 'Linux'
run: |
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
sudo flatpak install --system -y flathub org.freedesktop.Platform//25.08 org.freedesktop.Sdk//25.08 org.electronjs.Electron2.BaseApp//25.08
- name: Setup build environment (Node.js, Linux native deps, caches)
uses: ./.github/actions/setup-build-env

# ubuntu-latest (Ubuntu 24.04+) restricts unprivileged user namespaces via AppArmor by
# default, which breaks Electron's sandbox init entirely (electron.launch() throws before
Expand All @@ -77,31 +68,6 @@ jobs:
if: runner.os == 'Linux'
run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0

- name: Setup Node.js 24
uses: actions/setup-node@v7
with:
node-version: '24'
cache: 'npm'

# actions/setup-node's cache:npm only covers the npm registry cache — it doesn't touch
# the Electron binary zip (~100-200MB) or electron-builder's own toolchain downloads
# (NSIS, winCodeSign, etc.), which live in a separate cache dir and get re-downloaded on
# every run otherwise. Cache path list covers all 3 OSes; actions/cache skips whichever
# paths don't exist on the current runner, and the OS-prefixed key keeps them separate.
- name: Cache Electron/electron-builder downloads
uses: actions/cache@v6
with:
path: |
~/.cache/electron
~/.cache/electron-builder
~/Library/Caches/electron
~/Library/Caches/electron-builder
~\AppData\Local\electron\Cache
~\AppData\Local\electron-builder\Cache
key: ${{ runner.os }}-electron-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-electron-

- name: Install dependencies
run: npm ci

Expand Down
90 changes: 90 additions & 0 deletions .github/workflows/create-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: Create Release

on:
workflow_dispatch:
inputs:
bump:
description: 'Version bump type'
required: true
type: choice
options: [patch, minor, major]

permissions:
contents: write
actions: write

jobs:
create-release:
name: Bump, tag, and kick off release build
runs-on: ubuntu-latest

steps:
# Anyone with write access can trigger workflow_dispatch; this adds a second gate
# since cutting a release also pushes to main and dispatches the build/publish
# pipeline. Adjust or drop if your team wants other collaborators to release too.
- name: Guard — owner only
if: github.actor != github.repository_owner
run: |
echo "Only the repository owner (${{ github.repository_owner }}) can trigger releases."
exit 1

- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Setup Node.js 24
uses: actions/setup-node@v7
with:
node-version: '24'
cache: 'npm'

- name: Configure git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

# Root package.json is the single source of truth for the app version.
# packages/main/electron-builder.config.js reads it directly (via `extraMetadata.version`)
# at build time, so packages/main, packages/renderer, and packages/shared keep their own
# fixed internal version (0.0.1) — nothing reads those for versioning. See README > Releasing.
- name: Bump root version
id: version
env:
BUMP: ${{ inputs.bump }}
run: |
npm version "$BUMP" --no-git-tag-version
NEW_VERSION=$(node -p "require('./package.json').version")
npm install
echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT"

- name: Generate changelog
run: node .github/scripts/generate-changelog.mjs "${{ steps.version.outputs.version }}" "${{ github.repository }}"

- name: Commit and tag
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
git add package.json package-lock.json
git commit -m "chore(release): v${VERSION}"
git tag "v${VERSION}"
git push origin HEAD:main
git push origin "v${VERSION}"

# Created as a draft: electron-builder's own GitHub publish step (triggered below)
# reuses an existing draft release unconditionally and just uploads its assets to it,
# so our changelog stays as the notes instead of being overwritten. release.yml's
# `finalize` job flips it to published once all 3 OS legs have uploaded successfully.
- name: Create draft GitHub release with changelog notes
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.version.outputs.version }}
run: gh release create "v${VERSION}" --draft --title "v${VERSION}" --notes-file CHANGELOG_BODY.md --repo "${{ github.repository }}"

# A push made with the default GITHUB_TOKEN does not trigger other workflows' `push`
# events (GitHub's anti-recursion rule) — so the tag push above won't start release.yml
# on its own. workflow_dispatch is exempt from that rule, so we call it explicitly.
- name: Trigger release build
env:
GH_TOKEN: ${{ github.token }}
run: gh workflow run release.yml --ref "v${{ steps.version.outputs.version }}" --repo "${{ github.repository }}"
71 changes: 71 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Release

on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
build:
name: Build & publish (${{ matrix.os }})
runs-on: ${{ matrix.os }}
permissions:
contents: write

strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]

steps:
- name: Checkout code
uses: actions/checkout@v7

- name: Setup build environment (Node.js, Linux native deps, caches)
uses: ./.github/actions/setup-build-env

- name: Install dependencies
run: npm ci

# Code signing / notarization secrets are scoped to the macOS leg only (via the
# runner.os ternary). CSC_LINK is a *shared* electron-builder var also read on Windows;
# leaving it unset there prevents a mac .p12 from being fed into Windows Authenticode
# signing by mistake. See README > Releasing for what each secret is and how to set it up.
#
# GH_TOKEN authenticates electron-builder's own GitHub publish step (package:release runs
# with --publish always): each OS leg builds its installers and uploads them directly to
# the release matching this repo's package.json version — no separate publish job needed.
- name: Build & package (release)
env:
CSC_LINK: ${{ runner.os == 'macOS' && secrets.MAC_CERTIFICATE_P12_BASE64 || '' }}
CSC_KEY_PASSWORD: ${{ runner.os == 'macOS' && secrets.MAC_CERTIFICATE_PASSWORD || '' }}
APPLE_API_KEY: ${{ runner.os == 'macOS' && secrets.APPLE_API_KEY_BASE64 || '' }}
APPLE_API_KEY_ID: ${{ runner.os == 'macOS' && secrets.APPLE_API_KEY_ID || '' }}
APPLE_API_ISSUER: ${{ runner.os == 'macOS' && secrets.APPLE_API_ISSUER || '' }}
GH_TOKEN: ${{ github.token }}
run: npm run release

finalize:
name: Publish release (undraft)
needs: build
runs-on: ubuntu-latest
permissions:
contents: write

steps:
# electron-builder.json intentionally leaves `publish.releaseType` unset, which
# electron-builder defaults to "draft" — each of the 3 OS legs above reuses that same
# draft and uploads its own assets to it. Only once all 3 have succeeded (this job's
# `needs: build`) do we flip it to published, so a release never goes live half-built.
- name: Publish the release created by create-release.yml
env:
GH_TOKEN: ${{ github.token }}
run: gh release edit "${{ github.ref_name }}" --draft=false --repo "${{ github.repository }}"
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,7 @@ opencode.json

# OS
.DS_Store
Thumbs.db
Thumbs.db

# Generated by .github/scripts/generate-changelog.mjs — ephemeral release-notes body, never committed
CHANGELOG_BODY.md
Loading
Loading