From de070f8f20b531cc2061561af8c462ed41c87e82 Mon Sep 17 00:00:00 2001 From: Chaitanya Chandurkar Date: Mon, 28 Sep 2026 13:30:54 -0400 Subject: [PATCH 01/12] feat(ci): add notarized release workflow --- .github/workflows/release.yml | 131 ++++++++++++++++++++++++++++++++++ README.md | 65 ++++++++++++----- 2 files changed, 178 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e816f3e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,131 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + build: + name: Build (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - 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 + + - 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 Node.js 24 + uses: actions/setup-node@v7 + with: + node-version: '24' + cache: 'npm' + + # See the "Cache Electron/electron-builder downloads" step in ci.yml for why this exists + # and what each path covers. + - 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 + + # 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. + - 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 || '' }} + APPLE_TEAM_ID: ${{ runner.os == 'macOS' && secrets.APPLE_TEAM_ID || '' }} + run: npm run release + + - name: Upload macOS artifacts + if: runner.os == 'macOS' + uses: actions/upload-artifact@v4 + with: + name: release-macos + path: build/*.dmg + if-no-files-found: error + + - name: Upload Windows artifacts + if: runner.os == 'Windows' + uses: actions/upload-artifact@v4 + with: + name: release-windows + path: build/*.exe + if-no-files-found: error + + - name: Upload Linux artifacts + if: runner.os == 'Linux' + uses: actions/upload-artifact@v4 + with: + name: release-linux + path: | + build/*.AppImage + build/*.deb + build/*.flatpak + if-no-files-found: error + + publish: + name: Publish GitHub release + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + + - name: Create or update release + env: + GH_TOKEN: ${{ github.token }} + run: | + tag="${{ github.ref_name }}" + if gh release view "$tag" --repo "${{ github.repository }}" >/dev/null 2>&1; then + gh release upload "$tag" artifacts/* --repo "${{ github.repository }}" --clobber + else + gh release create "$tag" artifacts/* --repo "${{ github.repository }}" --title "$tag" --generate-notes + fi diff --git a/README.md b/README.md index 4508a6d..7c96b5f 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ An Electron starter for developers who want to build a desktop app with Angular. - [Project Layout](#%EF%B8%8F-project-layout) - [Add an IPC Channel](#-add-an-ipc-channel) - [Make It Yours](#-make-it-yours) +- [Releasing (macOS notarization)](#-releasing-macos-notarization) - [Available Scripts](#%EF%B8%8F-available-scripts) - [Troubleshooting](#-troubleshooting) - [How This Compares](#-how-this-compares) @@ -146,7 +147,7 @@ It updates package and repository metadata, the app's display name and ID, the i Then replace the app icons in [`packages/main/assets/icons`](packages/main/assets/icons), the renderer's [`favicon.ico`](packages/renderer/public/favicon.ico), and the generic HTML title in [`packages/renderer/src/index.html`](packages/renderer/src/index.html). Remove the note example once you've used it to understand the wiring. -Before distributing an app, set the identity and release settings you need, including signing and notarization where applicable. The template intentionally does not configure a signing identity, an update server, or automatic updates. +Before distributing an app, set the identity and release settings you need, including signing and notarization where applicable. The template intentionally does not configure a signing identity, an update server, or automatic updates β€” see [Releasing](#-releasing-macos-notarization) below for the macOS signing/notarization workflow this template ships with. ### What is example, what is template @@ -162,28 +163,56 @@ Everything about notes exists to demonstrate the wiring and can be deleted: The frameless header with custom window controls is also a design choice, not a requirement. If you prefer a native title bar, set `frame: true` in `window.ts` and drop the header component. -Before distributing an app, set the identity and release settings you need, including signing and notarization where applicable. The template intentionally does not configure a signing identity, an update server, or automatic updates. +## πŸš€ Releasing (macOS notarization) + +[`.github/workflows/release.yml`](.github/workflows/release.yml) builds installers for macOS, Windows, and Linux and attaches them to a GitHub Release whenever you push a tag matching `v*.*.*`: + +```bash +git tag v0.2.0 +git push origin v0.2.0 +``` + +It runs `npm run release` on each OS (clean + build + `electron-builder --publish never`), uploads the installers as workflow artifacts, then a `publish` job downloads them all and creates (or updates) the GitHub Release for that tag via `gh release create`/`gh release upload`. + +The macOS leg additionally signs with a Developer ID Application certificate and notarizes with Apple's `notarytool`, gated on these repository secrets (Settings β†’ Secrets and variables β†’ Actions). Without them the macOS build step fails; Linux and Windows builds don't need them and succeed regardless: + +| Secret | What it is | +| ---------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| `MAC_CERTIFICATE_P12_BASE64` | Your Developer ID Application certificate + key, exported as `.p12`, base64-encoded (`base64 -i cert.p12 \| pbcopy`) | +| `MAC_CERTIFICATE_PASSWORD` | The export password for that `.p12` | +| `APPLE_API_KEY_BASE64` | An App Store Connect API key (`.p8`), base64-encoded | +| `APPLE_API_KEY_ID` | The key ID shown next to that API key in App Store Connect | +| `APPLE_API_ISSUER` | Your App Store Connect issuer ID | +| `APPLE_TEAM_ID` | Your Apple Developer Team ID | + +Notes: + +- You need an active [Apple Developer Program](https://developer.apple.com/programs/) membership to create the certificate and API key above. +- `electron-builder.json` keeps `mac.notarize: false` so local `npm run package` stays fast and unsigned for smoke-testing installers. `packages/main/package.json`'s `package:release` script overrides that to `true` via `-c.mac.notarize=true` β€” you don't need to edit the config file. +- Windows and Linux builds are unsigned in this template (no `win.certificateFile`/Authenticode setup) β€” add that separately if you need it. +- This only uploads artifacts to this repository's GitHub Releases; it doesn't set `electron-builder`'s `publish`/auto-update feed, which stays `null`. ## πŸ› οΈ Available Scripts Run these commands from the repository root: -| Command | Description | -| ---------------------- | -------------------------------------------------------- | -| `npm start` | Start development mode (Angular + Electron) | -| `npm run build` | Compile all packages (no installers) | -| `npm run package` | Compile + package into installers (`.dmg`/`.exe`/etc.) | -| `npm run clean` | Clean all build artifacts | -| `npm run lint` | Lint all packages | -| `npm run lint:fix` | Fix linting issues in all packages | -| `npm run format` | Format code with Prettier | -| `npm run format:check` | Check code formatting | -| `npm run test` | Run tests in all packages | -| `npm run typecheck` | Type-check all packages | -| `npm run verify` | Run format:check + lint + typecheck + tests β€” same as CI | -| `npm run dev:debug` | Start development mode with remote debugging (port 9222) | - -The CI workflow runs checks and packaging on macOS, Windows, and Linux. +| Command | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------------ | +| `npm start` | Start development mode (Angular + Electron) | +| `npm run build` | Compile all packages (no installers) | +| `npm run package` | Compile + package into installers (`.dmg`/`.exe`/etc.) | +| `npm run release` | Like `package`, plus macOS notarization (used by CI β€” see [Releasing](#-releasing-macos-notarization)) | +| `npm run clean` | Clean all build artifacts | +| `npm run lint` | Lint all packages | +| `npm run lint:fix` | Fix linting issues in all packages | +| `npm run format` | Format code with Prettier | +| `npm run format:check` | Check code formatting | +| `npm run test` | Run tests in all packages | +| `npm run typecheck` | Type-check all packages | +| `npm run verify` | Run format:check + lint + typecheck + tests β€” same as CI | +| `npm run dev:debug` | Start development mode with remote debugging (port 9222) | + +The CI workflow runs checks and packaging on macOS, Windows, and Linux. The release workflow (tag push) builds and notarizes installers and attaches them to a GitHub Release β€” see [Releasing](#-releasing-macos-notarization). ## 🧯 Troubleshooting From c75870a86922a24d3654b9ef8202980a1701f354 Mon Sep 17 00:00:00 2001 From: Chaitanya Chandurkar Date: Mon, 28 Sep 2026 15:06:19 -0400 Subject: [PATCH 02/12] refactor(ci): share build env setup via composite action --- .github/actions/setup-build-env/action.yml | 50 +++++++++++++++ .github/workflows/ci.yml | 38 +---------- .github/workflows/release.yml | 75 ++++++---------------- 3 files changed, 70 insertions(+), 93 deletions(-) create mode 100644 .github/actions/setup-build-env/action.yml diff --git a/.github/actions/setup-build-env/action.yml b/.github/actions/setup-build-env/action.yml new file mode 100644 index 0000000..5da26ad --- /dev/null +++ b/.github/actions/setup-build-env/action.yml @@ -0,0 +1,50 @@ +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. + - 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 + + - 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- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77f3ca4..3687513 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e816f3e..710cb1a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,45 +21,26 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + include: + - os: ubuntu-latest + artifact-name: release-linux + artifact-path: | + build/*.AppImage + build/*.deb + build/*.flatpak + - os: macos-latest + artifact-name: release-macos + artifact-path: build/*.dmg + - os: windows-latest + artifact-name: release-windows + artifact-path: build/*.exe steps: - 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 - - - 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 Node.js 24 - uses: actions/setup-node@v7 - with: - node-version: '24' - cache: 'npm' - - # See the "Cache Electron/electron-builder downloads" step in ci.yml for why this exists - # and what each path covers. - - 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: Setup build environment (Node.js, Linux native deps, caches) + uses: ./.github/actions/setup-build-env - name: Install dependencies run: npm ci @@ -78,31 +59,11 @@ jobs: APPLE_TEAM_ID: ${{ runner.os == 'macOS' && secrets.APPLE_TEAM_ID || '' }} run: npm run release - - name: Upload macOS artifacts - if: runner.os == 'macOS' - uses: actions/upload-artifact@v4 - with: - name: release-macos - path: build/*.dmg - if-no-files-found: error - - - name: Upload Windows artifacts - if: runner.os == 'Windows' - uses: actions/upload-artifact@v4 - with: - name: release-windows - path: build/*.exe - if-no-files-found: error - - - name: Upload Linux artifacts - if: runner.os == 'Linux' + - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: release-linux - path: | - build/*.AppImage - build/*.deb - build/*.flatpak + name: ${{ matrix.artifact-name }} + path: ${{ matrix.artifact-path }} if-no-files-found: error publish: From ace5926cfcba8e779946e4a483601357bfb9a4f3 Mon Sep 17 00:00:00 2001 From: Chaitanya Chandurkar Date: Mon, 28 Sep 2026 15:21:34 -0400 Subject: [PATCH 03/12] feat(build): add mac pkg/zip and linux rpm/tar.gz targets --- .github/actions/setup-build-env/action.yml | 4 +++- .github/workflows/release.yml | 11 ++++++++++- README.md | 23 +++++++++++++--------- packages/main/electron-builder.json | 4 ++-- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/.github/actions/setup-build-env/action.yml b/.github/actions/setup-build-env/action.yml index 5da26ad..0379a7f 100644 --- a/.github/actions/setup-build-env/action.yml +++ b/.github/actions/setup-build-env/action.yml @@ -10,12 +10,14 @@ runs: # 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 + 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' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 710cb1a..fb08452 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,10 +27,15 @@ jobs: artifact-path: | build/*.AppImage build/*.deb + build/*.rpm + build/*.tar.gz build/*.flatpak - os: macos-latest artifact-name: release-macos - artifact-path: build/*.dmg + artifact-path: | + build/*.dmg + build/*.zip + build/*.pkg - os: windows-latest artifact-name: release-windows artifact-path: build/*.exe @@ -53,6 +58,10 @@ jobs: env: CSC_LINK: ${{ runner.os == 'macOS' && secrets.MAC_CERTIFICATE_P12_BASE64 || '' }} CSC_KEY_PASSWORD: ${{ runner.os == 'macOS' && secrets.MAC_CERTIFICATE_PASSWORD || '' }} + # pkg installers sign with a separate "Developer ID Installer" certificate β€” the + # app-signing cert above (CSC_LINK) can't sign .pkg. See README > Releasing. + CSC_INSTALLER_LINK: ${{ runner.os == 'macOS' && secrets.MAC_INSTALLER_CERTIFICATE_P12_BASE64 || '' }} + CSC_INSTALLER_KEY_PASSWORD: ${{ runner.os == 'macOS' && secrets.MAC_INSTALLER_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 || '' }} diff --git a/README.md b/README.md index 7c96b5f..d7e80bc 100644 --- a/README.md +++ b/README.md @@ -174,21 +174,26 @@ git push origin v0.2.0 It runs `npm run release` on each OS (clean + build + `electron-builder --publish never`), uploads the installers as workflow artifacts, then a `publish` job downloads them all and creates (or updates) the GitHub Release for that tag via `gh release create`/`gh release upload`. +Artifacts produced: macOS `.dmg` + `.zip` + `.pkg`, Windows `.exe` (NSIS installer + portable), Linux `.deb` + `.AppImage` + `.rpm` + `.tar.gz` + `.flatpak`. + The macOS leg additionally signs with a Developer ID Application certificate and notarizes with Apple's `notarytool`, gated on these repository secrets (Settings β†’ Secrets and variables β†’ Actions). Without them the macOS build step fails; Linux and Windows builds don't need them and succeed regardless: -| Secret | What it is | -| ---------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| `MAC_CERTIFICATE_P12_BASE64` | Your Developer ID Application certificate + key, exported as `.p12`, base64-encoded (`base64 -i cert.p12 \| pbcopy`) | -| `MAC_CERTIFICATE_PASSWORD` | The export password for that `.p12` | -| `APPLE_API_KEY_BASE64` | An App Store Connect API key (`.p8`), base64-encoded | -| `APPLE_API_KEY_ID` | The key ID shown next to that API key in App Store Connect | -| `APPLE_API_ISSUER` | Your App Store Connect issuer ID | -| `APPLE_TEAM_ID` | Your Apple Developer Team ID | +| Secret | What it is | +| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MAC_CERTIFICATE_P12_BASE64` | Your Developer ID **Application** certificate + key (signs the `.app`/`.dmg`/`.zip`), exported as `.p12`, base64-encoded (`base64 -i cert.p12 \| pbcopy`) | +| `MAC_CERTIFICATE_PASSWORD` | The export password for that `.p12` | +| `MAC_INSTALLER_CERTIFICATE_P12_BASE64` | A **separate** Developer ID **Installer** certificate β€” required to sign the `.pkg` target specifically; the Application cert above can't sign it | +| `MAC_INSTALLER_CERTIFICATE_PASSWORD` | The export password for that Installer `.p12` | +| `APPLE_API_KEY_BASE64` | An App Store Connect API key (`.p8`), base64-encoded | +| `APPLE_API_KEY_ID` | The key ID shown next to that API key in App Store Connect | +| `APPLE_API_ISSUER` | Your App Store Connect issuer ID | +| `APPLE_TEAM_ID` | Your Apple Developer Team ID | Notes: -- You need an active [Apple Developer Program](https://developer.apple.com/programs/) membership to create the certificate and API key above. +- You need an active [Apple Developer Program](https://developer.apple.com/programs/) membership to create the certificates and API key above. Developer ID **Application** and Developer ID **Installer** are two distinct certificate types in the same account β€” you need both if you want a signed `.pkg`. - `electron-builder.json` keeps `mac.notarize: false` so local `npm run package` stays fast and unsigned for smoke-testing installers. `packages/main/package.json`'s `package:release` script overrides that to `true` via `-c.mac.notarize=true` β€” you don't need to edit the config file. +- The `rpm` Linux target needs `rpm`/`rpmbuild` on the runner; the shared [`setup-build-env`](.github/actions/setup-build-env/action.yml) composite action installs it alongside the other Linux native deps. - Windows and Linux builds are unsigned in this template (no `win.certificateFile`/Authenticode setup) β€” add that separately if you need it. - This only uploads artifacts to this repository's GitHub Releases; it doesn't set `electron-builder`'s `publish`/auto-update feed, which stays `null`. diff --git a/packages/main/electron-builder.json b/packages/main/electron-builder.json index 97709b4..b42dbbb 100644 --- a/packages/main/electron-builder.json +++ b/packages/main/electron-builder.json @@ -17,7 +17,7 @@ "artifactName": "${productName}-v${version}-Setup.${ext}" }, "linux": { - "target": ["deb", "AppImage", { "target": "flatpak", "arch": ["x64"] }] + "target": ["deb", "AppImage", "rpm", "tar.gz", { "target": "flatpak", "arch": ["x64"] }] }, "flatpak": { "runtime": "org.freedesktop.Platform", @@ -29,7 +29,7 @@ "mac": { "hardenedRuntime": true, "gatekeeperAssess": false, - "target": ["dmg"], + "target": ["dmg", "zip", "pkg"], "notarize": false } } From bca8c5f85b11f868e7e11708650f5bfc0012cf9b Mon Sep 17 00:00:00 2001 From: Chaitanya Chandurkar Date: Mon, 28 Sep 2026 18:14:25 -0400 Subject: [PATCH 04/12] revert(build): drop mac pkg target --- .github/workflows/release.yml | 5 ----- README.md | 22 ++++++++++------------ packages/main/electron-builder.json | 2 +- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb08452..dfde579 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,6 @@ jobs: artifact-path: | build/*.dmg build/*.zip - build/*.pkg - os: windows-latest artifact-name: release-windows artifact-path: build/*.exe @@ -58,10 +57,6 @@ jobs: env: CSC_LINK: ${{ runner.os == 'macOS' && secrets.MAC_CERTIFICATE_P12_BASE64 || '' }} CSC_KEY_PASSWORD: ${{ runner.os == 'macOS' && secrets.MAC_CERTIFICATE_PASSWORD || '' }} - # pkg installers sign with a separate "Developer ID Installer" certificate β€” the - # app-signing cert above (CSC_LINK) can't sign .pkg. See README > Releasing. - CSC_INSTALLER_LINK: ${{ runner.os == 'macOS' && secrets.MAC_INSTALLER_CERTIFICATE_P12_BASE64 || '' }} - CSC_INSTALLER_KEY_PASSWORD: ${{ runner.os == 'macOS' && secrets.MAC_INSTALLER_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 || '' }} diff --git a/README.md b/README.md index d7e80bc..af9b253 100644 --- a/README.md +++ b/README.md @@ -174,24 +174,22 @@ git push origin v0.2.0 It runs `npm run release` on each OS (clean + build + `electron-builder --publish never`), uploads the installers as workflow artifacts, then a `publish` job downloads them all and creates (or updates) the GitHub Release for that tag via `gh release create`/`gh release upload`. -Artifacts produced: macOS `.dmg` + `.zip` + `.pkg`, Windows `.exe` (NSIS installer + portable), Linux `.deb` + `.AppImage` + `.rpm` + `.tar.gz` + `.flatpak`. +Artifacts produced: macOS `.dmg` + `.zip`, Windows `.exe` (NSIS installer + portable), Linux `.deb` + `.AppImage` + `.rpm` + `.tar.gz` + `.flatpak`. The macOS leg additionally signs with a Developer ID Application certificate and notarizes with Apple's `notarytool`, gated on these repository secrets (Settings β†’ Secrets and variables β†’ Actions). Without them the macOS build step fails; Linux and Windows builds don't need them and succeed regardless: -| Secret | What it is | -| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `MAC_CERTIFICATE_P12_BASE64` | Your Developer ID **Application** certificate + key (signs the `.app`/`.dmg`/`.zip`), exported as `.p12`, base64-encoded (`base64 -i cert.p12 \| pbcopy`) | -| `MAC_CERTIFICATE_PASSWORD` | The export password for that `.p12` | -| `MAC_INSTALLER_CERTIFICATE_P12_BASE64` | A **separate** Developer ID **Installer** certificate β€” required to sign the `.pkg` target specifically; the Application cert above can't sign it | -| `MAC_INSTALLER_CERTIFICATE_PASSWORD` | The export password for that Installer `.p12` | -| `APPLE_API_KEY_BASE64` | An App Store Connect API key (`.p8`), base64-encoded | -| `APPLE_API_KEY_ID` | The key ID shown next to that API key in App Store Connect | -| `APPLE_API_ISSUER` | Your App Store Connect issuer ID | -| `APPLE_TEAM_ID` | Your Apple Developer Team ID | +| Secret | What it is | +| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MAC_CERTIFICATE_P12_BASE64` | Your Developer ID Application certificate + key (signs the `.app`/`.dmg`/`.zip`), exported as `.p12`, base64-encoded (`base64 -i cert.p12 \| pbcopy`) | +| `MAC_CERTIFICATE_PASSWORD` | The export password for that `.p12` | +| `APPLE_API_KEY_BASE64` | An App Store Connect API key (`.p8`), base64-encoded | +| `APPLE_API_KEY_ID` | The key ID shown next to that API key in App Store Connect | +| `APPLE_API_ISSUER` | Your App Store Connect issuer ID | +| `APPLE_TEAM_ID` | Your Apple Developer Team ID | Notes: -- You need an active [Apple Developer Program](https://developer.apple.com/programs/) membership to create the certificates and API key above. Developer ID **Application** and Developer ID **Installer** are two distinct certificate types in the same account β€” you need both if you want a signed `.pkg`. +- You need an active [Apple Developer Program](https://developer.apple.com/programs/) membership to create the certificate and API key above. - `electron-builder.json` keeps `mac.notarize: false` so local `npm run package` stays fast and unsigned for smoke-testing installers. `packages/main/package.json`'s `package:release` script overrides that to `true` via `-c.mac.notarize=true` β€” you don't need to edit the config file. - The `rpm` Linux target needs `rpm`/`rpmbuild` on the runner; the shared [`setup-build-env`](.github/actions/setup-build-env/action.yml) composite action installs it alongside the other Linux native deps. - Windows and Linux builds are unsigned in this template (no `win.certificateFile`/Authenticode setup) β€” add that separately if you need it. diff --git a/packages/main/electron-builder.json b/packages/main/electron-builder.json index b42dbbb..b762b80 100644 --- a/packages/main/electron-builder.json +++ b/packages/main/electron-builder.json @@ -29,7 +29,7 @@ "mac": { "hardenedRuntime": true, "gatekeeperAssess": false, - "target": ["dmg", "zip", "pkg"], + "target": ["dmg", "zip"], "notarize": false } } From 0f035e63ad97ceac0a535a33946c985da4751da6 Mon Sep 17 00:00:00 2001 From: Chaitanya Chandurkar Date: Mon, 28 Sep 2026 18:55:41 -0400 Subject: [PATCH 05/12] refactor(ci): drop unused APPLE_TEAM_ID from release workflow --- .github/workflows/release.yml | 1 - README.md | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dfde579..1c821c0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,7 +60,6 @@ jobs: 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 || '' }} - APPLE_TEAM_ID: ${{ runner.os == 'macOS' && secrets.APPLE_TEAM_ID || '' }} run: npm run release - name: Upload artifacts diff --git a/README.md b/README.md index af9b253..ee6bd13 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,6 @@ The macOS leg additionally signs with a Developer ID Application certificate and | `APPLE_API_KEY_BASE64` | An App Store Connect API key (`.p8`), base64-encoded | | `APPLE_API_KEY_ID` | The key ID shown next to that API key in App Store Connect | | `APPLE_API_ISSUER` | Your App Store Connect issuer ID | -| `APPLE_TEAM_ID` | Your Apple Developer Team ID | Notes: From f24576b4c817bad8dc9238fabf795111edfc632b Mon Sep 17 00:00:00 2001 From: Chaitanya Chandurkar Date: Mon, 28 Sep 2026 19:46:29 -0400 Subject: [PATCH 06/12] feat(ci): publish releases via electron-builder github provider --- .github/workflows/release.yml | 59 +++++------------------------ AGENTS.md | 2 +- README.md | 9 +++-- packages/main/electron-builder.json | 4 +- packages/main/package.json | 2 +- 5 files changed, 20 insertions(+), 56 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1c821c0..f23c352 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,29 +15,15 @@ concurrency: jobs: build: - name: Build (${{ matrix.os }}) + name: Build & publish (${{ matrix.os }}) runs-on: ${{ matrix.os }} + permissions: + contents: write strategy: fail-fast: false matrix: - include: - - os: ubuntu-latest - artifact-name: release-linux - artifact-path: | - build/*.AppImage - build/*.deb - build/*.rpm - build/*.tar.gz - build/*.flatpak - - os: macos-latest - artifact-name: release-macos - artifact-path: | - build/*.dmg - build/*.zip - - os: windows-latest - artifact-name: release-windows - artifact-path: build/*.exe + os: [ubuntu-latest, windows-latest, macos-latest] steps: - name: Checkout code @@ -53,6 +39,10 @@ jobs: # 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 || '' }} @@ -60,36 +50,5 @@ jobs: 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 || '' }} - run: npm run release - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.artifact-name }} - path: ${{ matrix.artifact-path }} - if-no-files-found: error - - publish: - name: Publish GitHub release - needs: build - runs-on: ubuntu-latest - permissions: - contents: write - - steps: - - name: Download artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - merge-multiple: true - - - name: Create or update release - env: GH_TOKEN: ${{ github.token }} - run: | - tag="${{ github.ref_name }}" - if gh release view "$tag" --repo "${{ github.repository }}" >/dev/null 2>&1; then - gh release upload "$tag" artifacts/* --repo "${{ github.repository }}" --clobber - else - gh release create "$tag" artifacts/* --repo "${{ github.repository }}" --title "$tag" --generate-notes - fi + run: npm run release diff --git a/AGENTS.md b/AGENTS.md index c7f0f2b..12a070f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,6 +26,6 @@ Package-specific rules live next to the code they govern. Read the relevant one - Keep `package-lock.json` in sync with dependency changes (`npm install`, not hand edits). Do not bump Electron, Angular, or TypeScript majors as a side effect of another change; TypeScript is pinned to `~6.0.3` across all packages on purpose. - Commit messages follow Conventional Commits: `type(scope): description`, where scope is usually `main`, `renderer`, `shared`, `e2e`, or `docs`. See [CONTRIBUTING.md](CONTRIBUTING.md#commit-guidelines). - Formatting is Prettier; run `npm run format` before committing so `format:check` passes in CI. Files use kebab-case, types PascalCase with no `I` prefix. -- Do not hardcode this template repository as an app's publishing destination or update feed; adopters configure their own. `electron-builder.json` keeps `publish: null`. +- `electron-builder.json` publishes to GitHub Releases (`publish.provider: "github"`, no hardcoded `owner`/`repo` β€” electron-builder auto-detects these from the repo's own git remote, so each fork publishes to its own releases). Don't add a literal `owner`/`repo` pointing at a specific fork, and don't wire actual auto-update consumption (`electron-updater` in `packages/main`) without the user asking for it β€” the release pipeline only builds and publishes installers today; see README > Releasing. - When you change behavior, add or update a test at the boundary that proves it (unit test in the owning package, E2E for cross-process flows). Tests should assert behavior, not only that something can be constructed. - If you touch the example (note editor, `note:*` channels, `NoteData`), keep it minimal and removable. Do not grow the example into a product; prefer improving the template's structure, docs, or defaults. diff --git a/README.md b/README.md index ee6bd13..27b8909 100644 --- a/README.md +++ b/README.md @@ -165,14 +165,16 @@ The frameless header with custom window controls is also a design choice, not a ## πŸš€ Releasing (macOS notarization) -[`.github/workflows/release.yml`](.github/workflows/release.yml) builds installers for macOS, Windows, and Linux and attaches them to a GitHub Release whenever you push a tag matching `v*.*.*`: +[`.github/workflows/release.yml`](.github/workflows/release.yml) builds installers for macOS, Windows, and Linux and publishes them to a GitHub Release whenever you push a tag matching `v*.*.*`: ```bash +# packages/main/package.json's "version" must match the tag (electron-builder computes the +# release tag from it, not from the git ref) β€” bump it first, then tag and push. git tag v0.2.0 git push origin v0.2.0 ``` -It runs `npm run release` on each OS (clean + build + `electron-builder --publish never`), uploads the installers as workflow artifacts, then a `publish` job downloads them all and creates (or updates) the GitHub Release for that tag via `gh release create`/`gh release upload`. +Each OS runs `npm run release` (clean + build + `electron-builder --publish always`), which builds its installers **and** publishes them straight to the GitHub Release for that tag β€” no separate publish/upload job. `electron-builder.json`'s `publish.provider: "github"` doesn't hardcode an `owner`/`repo`; electron-builder auto-detects them from this repo's own git remote, so a fork publishes to _its own_ releases automatically, no config edit needed. Artifacts produced: macOS `.dmg` + `.zip`, Windows `.exe` (NSIS installer + portable), Linux `.deb` + `.AppImage` + `.rpm` + `.tar.gz` + `.flatpak`. @@ -192,7 +194,8 @@ Notes: - `electron-builder.json` keeps `mac.notarize: false` so local `npm run package` stays fast and unsigned for smoke-testing installers. `packages/main/package.json`'s `package:release` script overrides that to `true` via `-c.mac.notarize=true` β€” you don't need to edit the config file. - The `rpm` Linux target needs `rpm`/`rpmbuild` on the runner; the shared [`setup-build-env`](.github/actions/setup-build-env/action.yml) composite action installs it alongside the other Linux native deps. - Windows and Linux builds are unsigned in this template (no `win.certificateFile`/Authenticode setup) β€” add that separately if you need it. -- This only uploads artifacts to this repository's GitHub Releases; it doesn't set `electron-builder`'s `publish`/auto-update feed, which stays `null`. +- `GH_TOKEN`/`GITHUB_TOKEN` for the publish step is the workflow's automatic built-in token (`${{ github.token }}`) β€” no extra secret needed for that part. +- **This publishes installers to GitHub Releases; it does not make the app auto-update itself.** `electron-builder` generates the `latest.yml`/`latest-mac.yml`/`latest-linux.yml` update-feed metadata these releases need, but nothing in `packages/main` reads it yet β€” the `updater:status` push channel is still a placeholder (see [Add an IPC Channel](#-add-an-ipc-channel)). Wiring `electron-updater` into the main process to actually check/download/apply updates is tracked as a follow-up, not included yet. ## πŸ› οΈ Available Scripts diff --git a/packages/main/electron-builder.json b/packages/main/electron-builder.json index b762b80..a4d1aed 100644 --- a/packages/main/electron-builder.json +++ b/packages/main/electron-builder.json @@ -4,7 +4,9 @@ "output": "../../build/", "buildResources": "assets/icons" }, - "publish": null, + "publish": { + "provider": "github" + }, "artifactName": "${productName}-v${version}.${ext}", "asar": true, "forceCodeSigning": false, diff --git a/packages/main/package.json b/packages/main/package.json index 4ac7ef8..da3a4fb 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -9,7 +9,7 @@ "scripts": { "build": "npm run tsc:build && npm run preload:build", "package": "npm run build && electron-builder --publish never", - "package:release": "npm run build && electron-builder --publish never -c.mac.notarize=true", + "package:release": "npm run build && electron-builder --publish always -c.mac.notarize=true", "serve": "wait-on tcp:4200 && npm-run-all --parallel tsc:watch preload:watch electron:launch", "electron:launch": "wait-on dist/main/index.js dist/main/preload/index.js && electron . --serve", "tsc:watch": "tsc -p tsconfig.json --outDir ./dist/main --watch", From 2e457772b9f05744941dfc257c8d28b5e82e1a87 Mon Sep 17 00:00:00 2001 From: Chaitanya Chandurkar Date: Tue, 29 Sep 2026 11:40:37 -0400 Subject: [PATCH 07/12] feat(ci): add create-release workflow for automated version bump --- .github/scripts/generate-changelog.mjs | 113 +++++++++++++++++++++++++ .github/workflows/create-release.yml | 91 ++++++++++++++++++++ .github/workflows/release.yml | 17 ++++ AGENTS.md | 1 + README.md | 21 +++-- 5 files changed, 235 insertions(+), 8 deletions(-) create mode 100644 .github/scripts/generate-changelog.mjs create mode 100644 .github/workflows/create-release.yml diff --git a/.github/scripts/generate-changelog.mjs b/.github/scripts/generate-changelog.mjs new file mode 100644 index 0000000..16b5f82 --- /dev/null +++ b/.github/scripts/generate-changelog.mjs @@ -0,0 +1,113 @@ +#!/usr/bin/env node +// Generates a changelog section 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. Prepends the section to CHANGELOG.md and writes the same body to +// CHANGELOG_BODY.md for use as the GitHub release notes. +// +// Usage: node generate-changelog.mjs +// 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 { existsSync, readFileSync, writeFileSync } from 'node:fs'; + +const [, , newVersion, repoSlug] = process.argv; +if (!newVersion || !repoSlug) { + console.error('Usage: generate-changelog.mjs '); + process.exit(1); +} + +// Safety valve for a degenerate case (e.g. a dormant fork cutting its first release after +// years of history) β€” not a normal limit for this manually-triggered release flow, where +// per-release commit counts are expected to be small. See README > Releasing. +const MAX_ENTRIES = 150; + +const TYPES = [ + ['feat', 'Features'], + ['fix', 'Bug Fixes'], + ['refactor', 'Refactoring'], + ['perf', 'Performance'], + ['revert', 'Reverts'] +]; +const typeLabel = new Map(TYPES); + +function sh(cmd) { + return execSync(cmd, { encoding: 'utf8' }).trim(); +} + +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}`; + +const body = + (sections || '_No user-facing changes recorded since the last release._') + + truncatedNote + + `\n\n${compareLink}\n`; + +const date = new Date().toISOString().slice(0, 10); +const newSection = `## v${newVersion} β€” ${date}\n\n${body}\n`; + +const changelogPath = 'CHANGELOG.md'; +const existing = existsSync(changelogPath) + ? readFileSync(changelogPath, 'utf8') + : '# Changelog\n\n'; +// Insert the new section right after the top-level "# Changelog" heading, above prior entries. +const headingEnd = existing.indexOf('\n\n') + 2; +const updated = existing.slice(0, headingEnd) + newSection + '\n' + existing.slice(headingEnd); +writeFileSync(changelogPath, updated); +writeFileSync('CHANGELOG_BODY.md', body); + +console.log( + `Changelog updated for v${newVersion} (${matchedCount} matched commit(s), lastTag=${lastTag || ''}).` +); diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml new file mode 100644 index 0000000..0ae9e2f --- /dev/null +++ b/.github/workflows/create-release.yml @@ -0,0 +1,91 @@ +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" + + # All 4 package.json files (root + 3 workspaces) share one version. electron-builder + # reads packages/main/package.json specifically β€” that's the one that actually drives + # the release tag and artifact names β€” but we keep every package.json in lockstep so + # there's a single number to reason about across the repo. See README > Releasing. + - name: Bump version (root + all workspaces) + id: version + env: + BUMP: ${{ inputs.bump }} + run: | + npm version "$BUMP" --no-git-tag-version + NEW_VERSION=$(node -p "require('./package.json').version") + npm pkg set version="$NEW_VERSION" -w packages/main -w packages/renderer -w packages/shared + 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 packages/main/package.json packages/renderer/package.json packages/shared/package.json CHANGELOG.md + 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 }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f23c352..ac3587f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,3 +52,20 @@ jobs: 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 }}" diff --git a/AGENTS.md b/AGENTS.md index 12a070f..8c31743 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,6 +26,7 @@ Package-specific rules live next to the code they govern. Read the relevant one - Keep `package-lock.json` in sync with dependency changes (`npm install`, not hand edits). Do not bump Electron, Angular, or TypeScript majors as a side effect of another change; TypeScript is pinned to `~6.0.3` across all packages on purpose. - Commit messages follow Conventional Commits: `type(scope): description`, where scope is usually `main`, `renderer`, `shared`, `e2e`, or `docs`. See [CONTRIBUTING.md](CONTRIBUTING.md#commit-guidelines). - Formatting is Prettier; run `npm run format` before committing so `format:check` passes in CI. Files use kebab-case, types PascalCase with no `I` prefix. +- Releases are cut via `.github/workflows/create-release.yml` (`workflow_dispatch`, owner-only guard): it bumps all 4 `package.json` versions (root + 3 workspaces) in lockstep, regenerates `CHANGELOG.md`, commits, tags, pushes, creates a draft GitHub release, then dispatches `.github/workflows/release.yml` (a plain tag push can't β€” GITHUB_TOKEN-authored pushes don't trigger other workflows). `release.yml` builds/signs/notarizes/publishes per OS and its `finalize` job un-drafts the release once all 3 legs succeed. Don't hand-edit `CHANGELOG.md` or package versions outside that workflow. - `electron-builder.json` publishes to GitHub Releases (`publish.provider: "github"`, no hardcoded `owner`/`repo` β€” electron-builder auto-detects these from the repo's own git remote, so each fork publishes to its own releases). Don't add a literal `owner`/`repo` pointing at a specific fork, and don't wire actual auto-update consumption (`electron-updater` in `packages/main`) without the user asking for it β€” the release pipeline only builds and publishes installers today; see README > Releasing. - When you change behavior, add or update a test at the boundary that proves it (unit test in the owning package, E2E for cross-process flows). Tests should assert behavior, not only that something can be constructed. - If you touch the example (note editor, `note:*` channels, `NoteData`), keep it minimal and removable. Do not grow the example into a product; prefer improving the template's structure, docs, or defaults. diff --git a/README.md b/README.md index 27b8909..9c8258a 100644 --- a/README.md +++ b/README.md @@ -165,16 +165,20 @@ The frameless header with custom window controls is also a design choice, not a ## πŸš€ Releasing (macOS notarization) -[`.github/workflows/release.yml`](.github/workflows/release.yml) builds installers for macOS, Windows, and Linux and publishes them to a GitHub Release whenever you push a tag matching `v*.*.*`: +Cutting a release is one manual step; everything after that is automatic: -```bash -# packages/main/package.json's "version" must match the tag (electron-builder computes the -# release tag from it, not from the git ref) β€” bump it first, then tag and push. -git tag v0.2.0 -git push origin v0.2.0 -``` +**Actions tab β†’ [Create Release](../../actions/workflows/create-release.yml) β†’ Run workflow β†’ pick `patch`/`minor`/`major`.** + +That workflow ([`.github/workflows/create-release.yml`](.github/workflows/create-release.yml)): + +1. Bumps the version (via `npm version`) in **all 4** `package.json` files (root + `packages/main`, `packages/renderer`, `packages/shared`) so they stay in lockstep β€” `packages/main`'s is the one that actually matters (electron-builder reads it for the release tag and every artifact filename), the other 3 are cosmetic/internal but kept in sync for consistency. +2. Refreshes `package-lock.json` (`npm install`). +3. Generates a changelog from conventional-commit subjects since the last tag (`feat`/`fix`/`refactor`/`perf`/`revert` only β€” noise like `chore`/`ci`/`docs` is filtered out), prepends it to `CHANGELOG.md`, capped at 150 entries with a "…and N more" note plus a `compare` link if a release window is ever unusually large. +4. Commits, tags `vX.Y.Z`, and pushes both to `main`. +5. Creates the GitHub release as a **draft** with that changelog as its notes. +6. Explicitly dispatches [`release.yml`](.github/workflows/release.yml) (`gh workflow run --ref vX.Y.Z`) β€” required because a push made with the default `GITHUB_TOKEN` doesn't trigger other workflows' `push` events; `workflow_dispatch` is exempt from that rule. -Each OS runs `npm run release` (clean + build + `electron-builder --publish always`), which builds its installers **and** publishes them straight to the GitHub Release for that tag β€” no separate publish/upload job. `electron-builder.json`'s `publish.provider: "github"` doesn't hardcode an `owner`/`repo`; electron-builder auto-detects them from this repo's own git remote, so a fork publishes to _its own_ releases automatically, no config edit needed. +[`release.yml`](.github/workflows/release.yml) then builds installers for macOS, Windows, and Linux. Each OS runs `npm run release` (clean + build + `electron-builder --publish always`), which builds its installers **and** uploads them straight to that same draft release β€” no separate publish/upload job, and no `owner`/`repo` hardcoded (`electron-builder.json`'s `publish.provider: "github"` auto-detects both from the repo's own git remote, so a fork publishes to _its own_ releases with no config edit). Once all 3 OS legs succeed, a final `finalize` job flips the release from draft to published, so a partially-built release is never visible. Artifacts produced: macOS `.dmg` + `.zip`, Windows `.exe` (NSIS installer + portable), Linux `.deb` + `.AppImage` + `.rpm` + `.tar.gz` + `.flatpak`. @@ -190,6 +194,7 @@ The macOS leg additionally signs with a Developer ID Application certificate and Notes: +- `create-release.yml` only runs for `github.repository_owner` β€” anyone else with write access who triggers it fails fast at the guard step. Adjust or remove that check if other collaborators on your fork should be able to cut releases too. - You need an active [Apple Developer Program](https://developer.apple.com/programs/) membership to create the certificate and API key above. - `electron-builder.json` keeps `mac.notarize: false` so local `npm run package` stays fast and unsigned for smoke-testing installers. `packages/main/package.json`'s `package:release` script overrides that to `true` via `-c.mac.notarize=true` β€” you don't need to edit the config file. - The `rpm` Linux target needs `rpm`/`rpmbuild` on the runner; the shared [`setup-build-env`](.github/actions/setup-build-env/action.yml) composite action installs it alongside the other Linux native deps. From bb1f8af9a456f43bf289b73ae33c569e167effa7 Mon Sep 17 00:00:00 2001 From: Chaitanya Chandurkar Date: Tue, 29 Sep 2026 12:21:57 -0400 Subject: [PATCH 08/12] refactor(ci): drop persisted CHANGELOG.md, keep release notes only --- .github/scripts/generate-changelog.mjs | 23 ++++++----------------- .github/workflows/create-release.yml | 2 +- AGENTS.md | 2 +- README.md | 2 +- 4 files changed, 9 insertions(+), 20 deletions(-) diff --git a/.github/scripts/generate-changelog.mjs b/.github/scripts/generate-changelog.mjs index 16b5f82..f71978c 100644 --- a/.github/scripts/generate-changelog.mjs +++ b/.github/scripts/generate-changelog.mjs @@ -1,15 +1,15 @@ #!/usr/bin/env node -// Generates a changelog section 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. Prepends the section to CHANGELOG.md and writes the same body to -// CHANGELOG_BODY.md for use as the GitHub release notes. +// 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 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 { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { writeFileSync } from 'node:fs'; const [, , newVersion, repoSlug] = process.argv; if (!newVersion || !repoSlug) { @@ -95,19 +95,8 @@ const body = truncatedNote + `\n\n${compareLink}\n`; -const date = new Date().toISOString().slice(0, 10); -const newSection = `## v${newVersion} β€” ${date}\n\n${body}\n`; - -const changelogPath = 'CHANGELOG.md'; -const existing = existsSync(changelogPath) - ? readFileSync(changelogPath, 'utf8') - : '# Changelog\n\n'; -// Insert the new section right after the top-level "# Changelog" heading, above prior entries. -const headingEnd = existing.indexOf('\n\n') + 2; -const updated = existing.slice(0, headingEnd) + newSection + '\n' + existing.slice(headingEnd); -writeFileSync(changelogPath, updated); writeFileSync('CHANGELOG_BODY.md', body); console.log( - `Changelog updated for v${newVersion} (${matchedCount} matched commit(s), lastTag=${lastTag || ''}).` + `Release notes generated for v${newVersion} (${matchedCount} matched commit(s), lastTag=${lastTag || ''}).` ); diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 0ae9e2f..82f1c06 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -66,7 +66,7 @@ jobs: env: VERSION: ${{ steps.version.outputs.version }} run: | - git add package.json package-lock.json packages/main/package.json packages/renderer/package.json packages/shared/package.json CHANGELOG.md + git add package.json package-lock.json packages/main/package.json packages/renderer/package.json packages/shared/package.json git commit -m "chore(release): v${VERSION}" git tag "v${VERSION}" git push origin HEAD:main diff --git a/AGENTS.md b/AGENTS.md index 8c31743..395af9b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,7 +26,7 @@ Package-specific rules live next to the code they govern. Read the relevant one - Keep `package-lock.json` in sync with dependency changes (`npm install`, not hand edits). Do not bump Electron, Angular, or TypeScript majors as a side effect of another change; TypeScript is pinned to `~6.0.3` across all packages on purpose. - Commit messages follow Conventional Commits: `type(scope): description`, where scope is usually `main`, `renderer`, `shared`, `e2e`, or `docs`. See [CONTRIBUTING.md](CONTRIBUTING.md#commit-guidelines). - Formatting is Prettier; run `npm run format` before committing so `format:check` passes in CI. Files use kebab-case, types PascalCase with no `I` prefix. -- Releases are cut via `.github/workflows/create-release.yml` (`workflow_dispatch`, owner-only guard): it bumps all 4 `package.json` versions (root + 3 workspaces) in lockstep, regenerates `CHANGELOG.md`, commits, tags, pushes, creates a draft GitHub release, then dispatches `.github/workflows/release.yml` (a plain tag push can't β€” GITHUB_TOKEN-authored pushes don't trigger other workflows). `release.yml` builds/signs/notarizes/publishes per OS and its `finalize` job un-drafts the release once all 3 legs succeed. Don't hand-edit `CHANGELOG.md` or package versions outside that workflow. +- Releases are cut via `.github/workflows/create-release.yml` (`workflow_dispatch`, owner-only guard): it bumps all 4 `package.json` versions (root + 3 workspaces) in lockstep, generates release notes from conventional commits, commits, tags, pushes, creates a draft GitHub release, then dispatches `.github/workflows/release.yml` (a plain tag push can't β€” GITHUB_TOKEN-authored pushes don't trigger other workflows). `release.yml` builds/signs/notarizes/publishes per OS and its `finalize` job un-drafts the release once all 3 legs succeed. No `CHANGELOG.md` is maintained in the repo β€” the GitHub release notes are the changelog. Don't hand-edit package versions outside that workflow. - `electron-builder.json` publishes to GitHub Releases (`publish.provider: "github"`, no hardcoded `owner`/`repo` β€” electron-builder auto-detects these from the repo's own git remote, so each fork publishes to its own releases). Don't add a literal `owner`/`repo` pointing at a specific fork, and don't wire actual auto-update consumption (`electron-updater` in `packages/main`) without the user asking for it β€” the release pipeline only builds and publishes installers today; see README > Releasing. - When you change behavior, add or update a test at the boundary that proves it (unit test in the owning package, E2E for cross-process flows). Tests should assert behavior, not only that something can be constructed. - If you touch the example (note editor, `note:*` channels, `NoteData`), keep it minimal and removable. Do not grow the example into a product; prefer improving the template's structure, docs, or defaults. diff --git a/README.md b/README.md index 9c8258a..6765c3b 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ That workflow ([`.github/workflows/create-release.yml`](.github/workflows/create 1. Bumps the version (via `npm version`) in **all 4** `package.json` files (root + `packages/main`, `packages/renderer`, `packages/shared`) so they stay in lockstep β€” `packages/main`'s is the one that actually matters (electron-builder reads it for the release tag and every artifact filename), the other 3 are cosmetic/internal but kept in sync for consistency. 2. Refreshes `package-lock.json` (`npm install`). -3. Generates a changelog from conventional-commit subjects since the last tag (`feat`/`fix`/`refactor`/`perf`/`revert` only β€” noise like `chore`/`ci`/`docs` is filtered out), prepends it to `CHANGELOG.md`, capped at 150 entries with a "…and N more" note plus a `compare` link if a release window is ever unusually large. +3. Generates release notes from conventional-commit subjects since the last tag (`feat`/`fix`/`refactor`/`perf`/`revert` only β€” noise like `chore`/`ci`/`docs` is filtered out), capped at 150 entries with a "…and N more" note plus a `compare` link if a release window is ever unusually large. Nothing is committed to the repo for this β€” the GitHub release itself is the changelog's home. 4. Commits, tags `vX.Y.Z`, and pushes both to `main`. 5. Creates the GitHub release as a **draft** with that changelog as its notes. 6. Explicitly dispatches [`release.yml`](.github/workflows/release.yml) (`gh workflow run --ref vX.Y.Z`) β€” required because a push made with the default `GITHUB_TOKEN` doesn't trigger other workflows' `push` events; `workflow_dispatch` is exempt from that rule. From a4421cc5283bc605b91b02aebab7d7ca6ac43442 Mon Sep 17 00:00:00 2001 From: Chaitanya Chandurkar Date: Tue, 29 Sep 2026 12:38:21 -0400 Subject: [PATCH 09/12] feat(ci): fall back to raw commit list for non-conventional history --- .github/scripts/generate-changelog.mjs | 31 +++++++++++++++++--------- CHANGELOG_BODY.md | 27 ++++++++++++++++++++++ 2 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 CHANGELOG_BODY.md diff --git a/.github/scripts/generate-changelog.mjs b/.github/scripts/generate-changelog.mjs index f71978c..2ea67ff 100644 --- a/.github/scripts/generate-changelog.mjs +++ b/.github/scripts/generate-changelog.mjs @@ -17,11 +17,11 @@ if (!newVersion || !repoSlug) { process.exit(1); } -// Safety valve for a degenerate case (e.g. a dormant fork cutting its first release after -// years of history) β€” not a normal limit for this manually-triggered release flow, where -// per-release commit counts are expected to be small. See README > Releasing. -const MAX_ENTRIES = 150; +function sh(cmd) { + return execSync(cmd, { encoding: 'utf8' }).trim(); +} +const MAX_ENTRIES = 150; const TYPES = [ ['feat', 'Features'], ['fix', 'Bug Fixes'], @@ -29,11 +29,8 @@ const TYPES = [ ['perf', 'Performance'], ['revert', 'Reverts'] ]; -const typeLabel = new Map(TYPES); -function sh(cmd) { - return execSync(cmd, { encoding: 'utf8' }).trim(); -} +const typeLabel = new Map(TYPES); let lastTag = ''; try { @@ -90,9 +87,23 @@ 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 = - (sections || '_No user-facing changes recorded since the last release._') + - truncatedNote + + (sectionsBody || '_No user-facing changes recorded since the last release._') + + noteSuffix + `\n\n${compareLink}\n`; writeFileSync('CHANGELOG_BODY.md', body); diff --git a/CHANGELOG_BODY.md b/CHANGELOG_BODY.md new file mode 100644 index 0000000..6cdc88b --- /dev/null +++ b/CHANGELOG_BODY.md @@ -0,0 +1,27 @@ +### Features + +- **ci**: add create-release workflow for automated version bump +- **ci**: publish releases via electron-builder github provider +- **build**: add mac pkg/zip and linux rpm/tar.gz targets +- **ci**: add notarized release workflow +- **main**: support optional macOS notarization and split CI validation +- **renderer**: add tiptap editor and modernize scrollbars +- **renderer**: show note saved timestamp in footer + +### Bug Fixes + +- **scripts**: rebrand renderer and fix cross-platform prettier spawn +- **scripts**: Fix scripts/rebrand.mjs windows compatibility +- **renderer**: Fix window buttons height and width + +### Refactoring + +- **ci**: drop persisted CHANGELOG.md, keep release notes only +- **ci**: drop unused APPLE_TEAM_ID from release workflow +- **ci**: share build env setup via composite action + +### Reverts + +- **build**: drop mac pkg target + +**Full Changelog**: https://github.com/cchandurkar/electron-angular-template/commits/v0.2.0 From 59ec28857d6bd921193ad910ed356e1cf286b12a Mon Sep 17 00:00:00 2001 From: Chaitanya Chandurkar <chaitanya.chandurkar@gopuff.com> Date: Tue, 29 Sep 2026 12:40:01 -0400 Subject: [PATCH 10/12] fix(ci): remove accidentally committed changelog test artifact --- .gitignore | 5 ++++- CHANGELOG_BODY.md | 27 --------------------------- 2 files changed, 4 insertions(+), 28 deletions(-) delete mode 100644 CHANGELOG_BODY.md diff --git a/.gitignore b/.gitignore index a615ea9..6bac981 100644 --- a/.gitignore +++ b/.gitignore @@ -148,4 +148,7 @@ opencode.json # OS .DS_Store -Thumbs.db \ No newline at end of file +Thumbs.db + +# Generated by .github/scripts/generate-changelog.mjs β€” ephemeral release-notes body, never committed +CHANGELOG_BODY.md \ No newline at end of file diff --git a/CHANGELOG_BODY.md b/CHANGELOG_BODY.md deleted file mode 100644 index 6cdc88b..0000000 --- a/CHANGELOG_BODY.md +++ /dev/null @@ -1,27 +0,0 @@ -### Features - -- **ci**: add create-release workflow for automated version bump -- **ci**: publish releases via electron-builder github provider -- **build**: add mac pkg/zip and linux rpm/tar.gz targets -- **ci**: add notarized release workflow -- **main**: support optional macOS notarization and split CI validation -- **renderer**: add tiptap editor and modernize scrollbars -- **renderer**: show note saved timestamp in footer - -### Bug Fixes - -- **scripts**: rebrand renderer <title> and fix cross-platform prettier spawn -- **scripts**: Fix scripts/rebrand.mjs windows compatibility -- **renderer**: Fix window buttons height and width - -### Refactoring - -- **ci**: drop persisted CHANGELOG.md, keep release notes only -- **ci**: drop unused APPLE_TEAM_ID from release workflow -- **ci**: share build env setup via composite action - -### Reverts - -- **build**: drop mac pkg target - -**Full Changelog**: https://github.com/cchandurkar/electron-angular-template/commits/v0.2.0 From 1bc372e02a264809e2f6e7d06b3cfca0a985488a Mon Sep 17 00:00:00 2001 From: Chaitanya Chandurkar <chaitanya.chandurkar@gopuff.com> Date: Tue, 29 Sep 2026 14:09:53 -0400 Subject: [PATCH 11/12] fix(ci): release workflow now only bumps the root package.json version --- .github/workflows/create-release.yml | 13 +++--- AGENTS.md | 2 +- README.md | 32 ++++++--------- packages/main/electron-builder.config.js | 52 ++++++++++++++++++++++++ packages/main/electron-builder.json | 37 ----------------- packages/main/package.json | 6 +-- packages/renderer/package.json | 2 +- packages/shared/package.json | 2 +- scripts/rebrand.mjs | 6 +-- 9 files changed, 80 insertions(+), 72 deletions(-) create mode 100644 packages/main/electron-builder.config.js delete mode 100644 packages/main/electron-builder.json diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 82f1c06..eb7e956 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -44,18 +44,17 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - # All 4 package.json files (root + 3 workspaces) share one version. electron-builder - # reads packages/main/package.json specifically β€” that's the one that actually drives - # the release tag and artifact names β€” but we keep every package.json in lockstep so - # there's a single number to reason about across the repo. See README > Releasing. - - name: Bump version (root + all workspaces) + # 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 pkg set version="$NEW_VERSION" -w packages/main -w packages/renderer -w packages/shared npm install echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT" @@ -66,7 +65,7 @@ jobs: env: VERSION: ${{ steps.version.outputs.version }} run: | - git add package.json package-lock.json packages/main/package.json packages/renderer/package.json packages/shared/package.json + git add package.json package-lock.json git commit -m "chore(release): v${VERSION}" git tag "v${VERSION}" git push origin HEAD:main diff --git a/AGENTS.md b/AGENTS.md index 395af9b..45a8063 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,6 @@ Package-specific rules live next to the code they govern. Read the relevant one - Commit messages follow Conventional Commits: `type(scope): description`, where scope is usually `main`, `renderer`, `shared`, `e2e`, or `docs`. See [CONTRIBUTING.md](CONTRIBUTING.md#commit-guidelines). - Formatting is Prettier; run `npm run format` before committing so `format:check` passes in CI. Files use kebab-case, types PascalCase with no `I` prefix. - Releases are cut via `.github/workflows/create-release.yml` (`workflow_dispatch`, owner-only guard): it bumps all 4 `package.json` versions (root + 3 workspaces) in lockstep, generates release notes from conventional commits, commits, tags, pushes, creates a draft GitHub release, then dispatches `.github/workflows/release.yml` (a plain tag push can't β€” GITHUB_TOKEN-authored pushes don't trigger other workflows). `release.yml` builds/signs/notarizes/publishes per OS and its `finalize` job un-drafts the release once all 3 legs succeed. No `CHANGELOG.md` is maintained in the repo β€” the GitHub release notes are the changelog. Don't hand-edit package versions outside that workflow. -- `electron-builder.json` publishes to GitHub Releases (`publish.provider: "github"`, no hardcoded `owner`/`repo` β€” electron-builder auto-detects these from the repo's own git remote, so each fork publishes to its own releases). Don't add a literal `owner`/`repo` pointing at a specific fork, and don't wire actual auto-update consumption (`electron-updater` in `packages/main`) without the user asking for it β€” the release pipeline only builds and publishes installers today; see README > Releasing. +- `electron-builder.config.js` publishes to GitHub Releases (`publish.provider: "github"`, no hardcoded `owner`/`repo` β€” electron-builder auto-detects these from the repo's own git remote, so each fork publishes to its own releases). It's JS, not JSON, specifically so `extraMetadata.version` can read the root `package.json` version at build time β€” `packages/main/package.json`'s own version is unused (fixed at `0.0.1`), so don't "fix" that drift. Don't add a literal `owner`/`repo` pointing at a specific fork, and don't wire actual auto-update consumption (`electron-updater` in `packages/main`) without the user asking for it β€” the release pipeline only builds and publishes installers today; see README > Releasing. - When you change behavior, add or update a test at the boundary that proves it (unit test in the owning package, E2E for cross-process flows). Tests should assert behavior, not only that something can be constructed. - If you touch the example (note editor, `note:*` channels, `NoteData`), keep it minimal and removable. Do not grow the example into a product; prefer improving the template's structure, docs, or defaults. diff --git a/README.md b/README.md index 6765c3b..703ae02 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ packages/ β”‚ β”‚ β”œβ”€β”€ storage.ts userData file storage β”‚ β”‚ └── preload/ contextBridge script, bundled to CJS for the sandbox β”‚ β”œβ”€β”€ assets/icons/ App icons for electron-builder -β”‚ └── electron-builder.json +β”‚ └── electron-builder.config.js β”œβ”€β”€ renderer/ Angular app (browser sandbox, no Node access) β”‚ └── src/app/ β”‚ β”œβ”€β”€ components/ Standalone components (note editor, header) @@ -163,24 +163,23 @@ Everything about notes exists to demonstrate the wiring and can be deleted: The frameless header with custom window controls is also a design choice, not a requirement. If you prefer a native title bar, set `frame: true` in `window.ts` and drop the header component. -## πŸš€ Releasing (macOS notarization) +## πŸš€ Versioning and Release Management -Cutting a release is one manual step; everything after that is automatic: +This template ships with the release management workflow. Cutting a release is one manual step; everything after that is automatic: **Actions tab β†’ [Create Release](../../actions/workflows/create-release.yml) β†’ Run workflow β†’ pick `patch`/`minor`/`major`.** -That workflow ([`.github/workflows/create-release.yml`](.github/workflows/create-release.yml)): +That workflow ( [`.github/workflows/create-release.yml`](.github/workflows/create-release.yml) ) bumps the version, generates changelog and creates the Github release as draft. Then it explicitely dispatches [`release.yml`](.github/workflows/release.yml) workflow that builds installers for macOS, Windows, and Linux and publishes them to the release. -1. Bumps the version (via `npm version`) in **all 4** `package.json` files (root + `packages/main`, `packages/renderer`, `packages/shared`) so they stay in lockstep β€” `packages/main`'s is the one that actually matters (electron-builder reads it for the release tag and every artifact filename), the other 3 are cosmetic/internal but kept in sync for consistency. -2. Refreshes `package-lock.json` (`npm install`). -3. Generates release notes from conventional-commit subjects since the last tag (`feat`/`fix`/`refactor`/`perf`/`revert` only β€” noise like `chore`/`ci`/`docs` is filtered out), capped at 150 entries with a "…and N more" note plus a `compare` link if a release window is ever unusually large. Nothing is committed to the repo for this β€” the GitHub release itself is the changelog's home. -4. Commits, tags `vX.Y.Z`, and pushes both to `main`. -5. Creates the GitHub release as a **draft** with that changelog as its notes. -6. Explicitly dispatches [`release.yml`](.github/workflows/release.yml) (`gh workflow run --ref vX.Y.Z`) β€” required because a push made with the default `GITHUB_TOKEN` doesn't trigger other workflows' `push` events; `workflow_dispatch` is exempt from that rule. +This release workflow only bumps the root [`package.json`](package.json) version. All `packages/*` versions remain unchanged as internal-only. [`./packages/main/electron-builder.config.js`](./packages/main/electron-builder.config.js) reads the root package version. -[`release.yml`](.github/workflows/release.yml) then builds installers for macOS, Windows, and Linux. Each OS runs `npm run release` (clean + build + `electron-builder --publish always`), which builds its installers **and** uploads them straight to that same draft release β€” no separate publish/upload job, and no `owner`/`repo` hardcoded (`electron-builder.json`'s `publish.provider: "github"` auto-detects both from the repo's own git remote, so a fork publishes to _its own_ releases with no config edit). Once all 3 OS legs succeed, a final `finalize` job flips the release from draft to published, so a partially-built release is never visible. +Artifacts produced: -Artifacts produced: macOS `.dmg` + `.zip`, Windows `.exe` (NSIS installer + portable), Linux `.deb` + `.AppImage` + `.rpm` + `.tar.gz` + `.flatpak`. +| OS | Installers | +| ------- | ------------------------------------------------------ | +| MacOs | `.dmg` + `.zip` | +| Windows | `.exe` (NSIS installer + portable), | +| Linux | `.deb` + `.AppImage` + `.rpm` + `.tar.gz` + `.flatpak` | The macOS leg additionally signs with a Developer ID Application certificate and notarizes with Apple's `notarytool`, gated on these repository secrets (Settings β†’ Secrets and variables β†’ Actions). Without them the macOS build step fails; Linux and Windows builds don't need them and succeed regardless: @@ -194,13 +193,8 @@ The macOS leg additionally signs with a Developer ID Application certificate and Notes: -- `create-release.yml` only runs for `github.repository_owner` β€” anyone else with write access who triggers it fails fast at the guard step. Adjust or remove that check if other collaborators on your fork should be able to cut releases too. -- You need an active [Apple Developer Program](https://developer.apple.com/programs/) membership to create the certificate and API key above. +- You need an active [Apple Developer Program](https://developer.apple.com/programs/) membership to create the certificate and API key above. Read more about [macOs Notarization](https://www.electron.build/v26/docs/features/code-signing/notarization/). - `electron-builder.json` keeps `mac.notarize: false` so local `npm run package` stays fast and unsigned for smoke-testing installers. `packages/main/package.json`'s `package:release` script overrides that to `true` via `-c.mac.notarize=true` β€” you don't need to edit the config file. -- The `rpm` Linux target needs `rpm`/`rpmbuild` on the runner; the shared [`setup-build-env`](.github/actions/setup-build-env/action.yml) composite action installs it alongside the other Linux native deps. -- Windows and Linux builds are unsigned in this template (no `win.certificateFile`/Authenticode setup) β€” add that separately if you need it. -- `GH_TOKEN`/`GITHUB_TOKEN` for the publish step is the workflow's automatic built-in token (`${{ github.token }}`) β€” no extra secret needed for that part. -- **This publishes installers to GitHub Releases; it does not make the app auto-update itself.** `electron-builder` generates the `latest.yml`/`latest-mac.yml`/`latest-linux.yml` update-feed metadata these releases need, but nothing in `packages/main` reads it yet β€” the `updater:status` push channel is still a placeholder (see [Add an IPC Channel](#-add-an-ipc-channel)). Wiring `electron-updater` into the main process to actually check/download/apply updates is tracked as a follow-up, not included yet. ## πŸ› οΈ Available Scripts @@ -254,7 +248,7 @@ Launch Electron with the `packages/main` directory, not the compiled `index.js` #### `npm run package` fails on Linux -The default Linux targets include Flatpak, which needs `flatpak`, `flatpak-builder`, and the `org.freedesktop.Platform` 25.08 runtime installed. See the Linux steps in [`.github/workflows/ci.yml`](.github/workflows/ci.yml) for the exact commands, or remove the `flatpak` target from `packages/main/electron-builder.json` if you don't need it. +The default Linux targets include Flatpak, which needs `flatpak`, `flatpak-builder`, and the `org.freedesktop.Platform` 25.08 runtime installed. See the Linux steps in [`.github/workflows/ci.yml`](.github/workflows/ci.yml) for the exact commands, or remove the `flatpak` target from `packages/main/electron-builder.config.js` if you don't need it. #### Default Electron icon in the macOS Dock during development diff --git a/packages/main/electron-builder.config.js b/packages/main/electron-builder.config.js new file mode 100644 index 0000000..85903c0 --- /dev/null +++ b/packages/main/electron-builder.config.js @@ -0,0 +1,52 @@ +// Electron-builder config as JS (not JSON) so `extraMetadata.version` can read the version +// straight from the repo root's package.json at build time. + +import { path } from 'node:path'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const rootPackageJson = JSON.parse( + readFileSync(path.join(__dirname, '../../package.json'), 'utf8') +); + +export default { + productName: 'Electron Angular Template', + directories: { + output: '../../build/', + buildResources: 'assets/icons' + }, + publish: { + provider: 'github' + }, + extraMetadata: { + version: rootPackageJson.version + }, + artifactName: '${productName}-v${version}.${ext}', + asar: true, + forceCodeSigning: false, + npmRebuild: false, + files: ['./dist/**/*', './assets/**/*', './package.json'], + win: { + target: ['nsis', 'portable'] + }, + nsis: { + artifactName: '${productName}-v${version}-Setup.${ext}' + }, + linux: { + target: ['deb', 'AppImage', 'rpm', 'tar.gz', { target: 'flatpak', arch: ['x64'] }] + }, + flatpak: { + runtime: 'org.freedesktop.Platform', + runtimeVersion: '25.08', + sdk: 'org.freedesktop.Sdk', + base: 'org.electronjs.Electron2.BaseApp', + baseVersion: '25.08' + }, + mac: { + hardenedRuntime: true, + gatekeeperAssess: false, + target: ['dmg', 'zip'], + notarize: false + } +}; diff --git a/packages/main/electron-builder.json b/packages/main/electron-builder.json deleted file mode 100644 index a4d1aed..0000000 --- a/packages/main/electron-builder.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "productName": "Electron Angular Template", - "directories": { - "output": "../../build/", - "buildResources": "assets/icons" - }, - "publish": { - "provider": "github" - }, - "artifactName": "${productName}-v${version}.${ext}", - "asar": true, - "forceCodeSigning": false, - "npmRebuild": false, - "files": ["./dist/**/*", "./assets/**/*", "./package.json"], - "win": { - "target": ["nsis", "portable"] - }, - "nsis": { - "artifactName": "${productName}-v${version}-Setup.${ext}" - }, - "linux": { - "target": ["deb", "AppImage", "rpm", "tar.gz", { "target": "flatpak", "arch": ["x64"] }] - }, - "flatpak": { - "runtime": "org.freedesktop.Platform", - "runtimeVersion": "25.08", - "sdk": "org.freedesktop.Sdk", - "base": "org.electronjs.Electron2.BaseApp", - "baseVersion": "25.08" - }, - "mac": { - "hardenedRuntime": true, - "gatekeeperAssess": false, - "target": ["dmg", "zip"], - "notarize": false - } -} diff --git a/packages/main/package.json b/packages/main/package.json index da3a4fb..6ff2ac0 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -1,15 +1,15 @@ { "name": "main", "productName": "Electron Angular Template", - "version": "0.1.0", + "version": "0.0.1", "private": true, "description": "A template for building modern Electron apps with Angular", "main": "dist/main/index.js", "type": "module", "scripts": { "build": "npm run tsc:build && npm run preload:build", - "package": "npm run build && electron-builder --publish never", - "package:release": "npm run build && electron-builder --publish always -c.mac.notarize=true", + "package": "npm run build && electron-builder --config electron-builder.config.js --publish never", + "package:release": "npm run build && electron-builder --config electron-builder.config.js --publish always -c.mac.notarize=true", "serve": "wait-on tcp:4200 && npm-run-all --parallel tsc:watch preload:watch electron:launch", "electron:launch": "wait-on dist/main/index.js dist/main/preload/index.js && electron . --serve", "tsc:watch": "tsc -p tsconfig.json --outDir ./dist/main --watch", diff --git a/packages/renderer/package.json b/packages/renderer/package.json index 6d0abcf..737eac5 100644 --- a/packages/renderer/package.json +++ b/packages/renderer/package.json @@ -1,6 +1,6 @@ { "name": "renderer", - "version": "0.0.0", + "version": "0.0.1", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/packages/shared/package.json b/packages/shared/package.json index 54cf2c0..7f1d489 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@local/shared", - "version": "0.1.0", + "version": "0.0.1", "private": true, "description": "Utility functions and models shared between main and renderer", "license": "MIT", diff --git a/scripts/rebrand.mjs b/scripts/rebrand.mjs index c02b4ed..0b1e5fb 100644 --- a/scripts/rebrand.mjs +++ b/scripts/rebrand.mjs @@ -201,13 +201,13 @@ export async function applyRebrand(answers, root = ROOT) { changed.push('packages/shared/package.json'); } - // packages/main/electron-builder.json β€” productName + a real appId (was deliberately unset) + // packages/main/electron-builder.config.js β€” productName + a real appId (was deliberately unset) { - const { full, data } = await readJson(root, 'packages/main/electron-builder.json'); + const { full, data } = await readJson(root, 'packages/main/electron-builder.config.js'); data.productName = productName; const ordered = { appId, ...data }; await writeJson(full, ordered); - changed.push('packages/main/electron-builder.json'); + changed.push('packages/main/electron-builder.config.js'); } // In-app title bar text From 60521744859888e86d440ad0bb9c66b1014c841c Mon Sep 17 00:00:00 2001 From: Chaitanya Chandurkar <chaitanya.chandurkar@gopuff.com> Date: Tue, 29 Sep 2026 14:19:05 -0400 Subject: [PATCH 12/12] fix(ci): Fix the import path in electron-builder.config.js --- packages/main/electron-builder.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/main/electron-builder.config.js b/packages/main/electron-builder.config.js index 85903c0..8b0daf0 100644 --- a/packages/main/electron-builder.config.js +++ b/packages/main/electron-builder.config.js @@ -1,7 +1,7 @@ // Electron-builder config as JS (not JSON) so `extraMetadata.version` can read the version // straight from the repo root's package.json at build time. -import { path } from 'node:path'; +import path from 'node:path'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url';