88permissions :
99 contents : write
1010 actions : read
11+ id-token : write # Required for SignPath Trusted Build System authentication
1112
1213jobs :
13- # ───────────────────────────────────────────────────────────────────────────
14- # JOB 1 — BUILD (matrix across Windows + macOS + Linux)
15- # Each runner compiles its own native artifacts and publishes them to the
16- # GitHub release. electron-updater needs the per-platform manifest files
17- # (latest.yml, latest-mac.yml, latest-linux.yml) AND the artifacts to sit
18- # on the same release — `--publish always` handles both in one step.
19- # ───────────────────────────────────────────────────────────────────────────
14+ # ─────────────────────────────────────────────────────────────────────────────
15+ # JOB 1 — BUILD (matrix: Windows, macOS, Linux)
16+ # Produces unsigned installers. Does NOT publish to GitHub — signing job
17+ # owns the final upload so the release only ever contains signed artifacts.
18+ # ─────────────────────────────────────────────────────────────────────────────
2019 build :
2120 name : Build (${{ matrix.os }})
2221 strategy :
23- fail-fast : false # one platform's failure shouldn't block the others
22+ fail-fast : false
2423 matrix :
2524 include :
2625 - os : windows-latest
2726 target_flag : --win
28- artifact_glob : |
29- dist/*.exe
30- dist/*.zip
31- dist/latest.yml
32- dist/*.blockmap
3327 - os : macos-latest
3428 target_flag : --mac
35- artifact_glob : |
36- dist/*.dmg
37- dist/*.zip
38- dist/latest-mac.yml
39- dist/*.blockmap
4029 - os : ubuntu-latest
4130 target_flag : --linux
42- artifact_glob : |
43- dist/*.AppImage
44- dist/*.deb
45- dist/latest-linux.yml
46- dist/*.blockmap
4731 runs-on : ${{ matrix.os }}
48- outputs :
49- version : ${{ steps.version.outputs.VERSION }}
5032
5133 steps :
5234 - name : Checkout code
@@ -58,17 +40,10 @@ jobs:
5840 node-version : ' 20'
5941 cache : ' npm'
6042
61- # Linux-only: AppImage builds need libfuse2 on the GH runner image.
6243 - name : Install libfuse2 (Linux only)
6344 if : matrix.os == 'ubuntu-latest'
6445 run : sudo apt-get update && sudo apt-get install -y libfuse2
6546
66- # Electron's postinstall downloads a ~100 MB binary from GitHub which
67- # flakes on CI runners with "socket hang up" once in a while (saw it
68- # on macos-latest during v1.6.1). Retry up to 3 times with backoff,
69- # but stop retrying as soon as it succeeds. We deliberately run
70- # postinstall scripts (no --ignore-scripts) because electron-builder
71- # needs the Electron binary that install.js fetches.
7247 - name : Install dependencies (with retry on network flakes)
7348 shell : bash
7449 run : |
@@ -87,62 +62,258 @@ jobs:
8762 echo "npm ci failed after 3 attempts"
8863 exit 1
8964
90- # Run build:wb explicitly so a postinstall-time failure here is
91- # immediately distinguishable from a dependency-install failure.
92- # (The postinstall hook in package.json runs this too — re-running
93- # it is cheap and idempotent.)
9465 - name : Build whiteboard iframe bundle
9566 run : npm run build:wb
9667
97- - name : Build & publish installer (electron-builder publishes to GitHub release )
98- run : npx electron-builder ${{ matrix.target_flag }} --publish always
68+ - name : Build unsigned installers (no publish )
69+ run : npx electron-builder ${{ matrix.target_flag }} --publish never
9970 env :
100- GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
101- # Skip Mac code-signing in CI — we ship unsigned builds for now.
102- # Users on macOS will need to right-click → Open the first time.
71+ # Skip local code-signing — SignPath handles signing in the next job.
10372 CSC_IDENTITY_AUTO_DISCOVERY : false
10473
105- - name : Upload platform artifacts (for the release job)
74+ - name : Upload unsigned Windows artifacts
75+ if : matrix.os == 'windows-latest'
10676 uses : actions/upload-artifact@v4
10777 with :
108- name : dist-${{ matrix.os }}
109- path : ${{ matrix.artifact_glob }}
110- if-no-files-found : warn
78+ name : unsigned-windows
79+ path : |
80+ dist/*.exe
81+ dist/*.zip
82+ if-no-files-found : error
11183
112- - name : Get version from tag
113- id : version
114- shell : bash
115- run : echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
116-
117- # ───────────────────────────────────────────────────────────────────────────
118- # JOB 2 — FINALISE RELEASE
119- # electron-builder --publish always has already uploaded every artifact
120- # in the build job. This job's ONLY purpose is to finalise the GitHub
121- # release: set a friendly title, generate the auto-changelog, and flip
122- # the 'Latest' badge.
84+ - name : Upload unsigned macOS artifacts
85+ if : matrix.os == 'macos-latest'
86+ uses : actions/upload-artifact@v4
87+ with :
88+ name : unsigned-macos
89+ path : |
90+ dist/*.dmg
91+ dist/*.zip
92+ if-no-files-found : error
93+
94+ - name : Upload unsigned Linux artifacts
95+ if : matrix.os == 'ubuntu-latest'
96+ uses : actions/upload-artifact@v4
97+ with :
98+ name : unsigned-linux
99+ path : |
100+ dist/*.AppImage
101+ dist/*.deb
102+ if-no-files-found : error
103+
104+ # ─────────────────────────────────────────────────────────────────────────────
105+ # JOB 2 — SIGN (SignPath)
106+ # Downloads unsigned artifacts, submits each platform to SignPath, and
107+ # re-uploads the signed versions for the release job to consume.
123108 #
124- # IMPORTANT: we do NOT pass `files:` to softprops. If we did, it would
125- # re-upload everything in dist/ with its OWN filename sanitization
126- # (which strips '+' chars from "Note++" and produces mangled
127- # `Note.-1.7.0-x64.zip.blockmap` style names), bloating the release
128- # to 27+ assets with confusing duplicates. Electron-builder is the
129- # sole authoritative uploader.
130- # ───────────────────────────────────────────────────────────────────────────
131- release :
132- name : Finalise GitHub Release
109+ # Prerequisites — configure once in GitHub repository settings:
110+ # Secrets → SIGNPATH_API_TOKEN (Settings → Secrets → Actions)
111+ # Variables → SIGNPATH_ORGANIZATION_ID (Settings → Variables → Actions)
112+ # SIGNPATH_PROJECT_SLUG
113+ #
114+ # In the SignPath dashboard you must also create:
115+ # • One Artifact Configuration per platform (windows-installer, macos-dmg,
116+ # linux-packages) that selects the files to sign and the certificate.
117+ # • One Signing Policy per platform that references those configurations.
118+ # • Optional: add this repo as a Trusted Build System (uses the
119+ # id-token: write permission above for keyless OIDC auth, eliminating
120+ # the need for SIGNPATH_API_TOKEN).
121+ #
122+ # macOS note: SignPath handles the Developer ID code-signing step. Apple
123+ # notarization (xcrun notarytool) is a separate process — add it here
124+ # after signing if you want full Gatekeeper trust without right-click → Open.
125+ # ─────────────────────────────────────────────────────────────────────────────
126+ sign :
127+ name : Sign (SignPath)
133128 needs : build
134129 runs-on : ubuntu-latest
135130
136131 steps :
137- - name : Finalise release (title + changelog + 'Latest' badge)
132+ - name : Download unsigned Windows artifacts
133+ uses : actions/download-artifact@v4
134+ with :
135+ name : unsigned-windows
136+ path : ./unsigned/windows/
137+
138+ - name : Download unsigned macOS artifacts
139+ uses : actions/download-artifact@v4
140+ with :
141+ name : unsigned-macos
142+ path : ./unsigned/macos/
143+
144+ - name : Download unsigned Linux artifacts
145+ uses : actions/download-artifact@v4
146+ with :
147+ name : unsigned-linux
148+ path : ./unsigned/linux/
149+
150+ # ── Windows (Authenticode) ───────────────────────────────────────────────
151+ - name : Sign Windows installer
152+ uses : SignPath/github-action-submit-signing-request@v1
153+ with :
154+ api-token : ${{ secrets.SIGNPATH_API_TOKEN }}
155+ organization-id : ${{ vars.SIGNPATH_ORGANIZATION_ID }}
156+ project-slug : ${{ vars.SIGNPATH_PROJECT_SLUG }}
157+ signing-policy-slug : release-signing
158+ artifact-configuration-slug : windows-installer
159+ input-artifact-path : ./unsigned/windows/
160+ wait-for-completion : true
161+ output-artifact-directory : ./signed/windows/
162+
163+ # ── macOS (Apple Developer ID) ───────────────────────────────────────────
164+ # Requires an Apple Developer ID Application certificate loaded in SignPath.
165+ - name : Sign macOS disk image
166+ uses : SignPath/github-action-submit-signing-request@v1
167+ with :
168+ api-token : ${{ secrets.SIGNPATH_API_TOKEN }}
169+ organization-id : ${{ vars.SIGNPATH_ORGANIZATION_ID }}
170+ project-slug : ${{ vars.SIGNPATH_PROJECT_SLUG }}
171+ signing-policy-slug : release-signing
172+ artifact-configuration-slug : macos-dmg
173+ input-artifact-path : ./unsigned/macos/
174+ wait-for-completion : true
175+ output-artifact-directory : ./signed/macos/
176+
177+ # ── Linux (GPG) ──────────────────────────────────────────────────────────
178+ # Requires a GPG key configured in SignPath.
179+ # To sign .deb and .AppImage separately, duplicate this step with a
180+ # different artifact-configuration-slug for each format.
181+ - name : Sign Linux packages
182+ uses : SignPath/github-action-submit-signing-request@v1
183+ with :
184+ api-token : ${{ secrets.SIGNPATH_API_TOKEN }}
185+ organization-id : ${{ vars.SIGNPATH_ORGANIZATION_ID }}
186+ project-slug : ${{ vars.SIGNPATH_PROJECT_SLUG }}
187+ signing-policy-slug : release-signing
188+ artifact-configuration-slug : linux-packages
189+ input-artifact-path : ./unsigned/linux/
190+ wait-for-completion : true
191+ output-artifact-directory : ./signed/linux/
192+
193+ - name : Upload signed Windows artifacts
194+ uses : actions/upload-artifact@v4
195+ with :
196+ name : signed-windows
197+ path : ./signed/windows/
198+ if-no-files-found : error
199+
200+ - name : Upload signed macOS artifacts
201+ uses : actions/upload-artifact@v4
202+ with :
203+ name : signed-macos
204+ path : ./signed/macos/
205+ if-no-files-found : error
206+
207+ - name : Upload signed Linux artifacts
208+ uses : actions/upload-artifact@v4
209+ with :
210+ name : signed-linux
211+ path : ./signed/linux/
212+ if-no-files-found : error
213+
214+ # ─────────────────────────────────────────────────────────────────────────────
215+ # JOB 3 — PUBLISH & FINALISE RELEASE
216+ # Downloads signed artifacts, regenerates the electron-updater manifests
217+ # (latest*.yml) with correct sha512 hashes for the signed files, then
218+ # uploads everything to GitHub and marks the release as Latest.
219+ #
220+ # Why regenerate manifests? Signing changes file content, so the sha512
221+ # hashes electron-builder embedded in the original latest*.yml no longer
222+ # match. electron-updater would reject a signed update whose hash differs
223+ # from the manifest — regenerating here keeps auto-update working correctly.
224+ #
225+ # Blockmaps: signing invalidates existing .blockmap files. We omit blockmap
226+ # references from the regenerated manifests; electron-updater gracefully
227+ # falls back to a full download instead of a binary delta.
228+ # ─────────────────────────────────────────────────────────────────────────────
229+ release :
230+ name : Publish & Finalise Release
231+ needs : sign
232+ runs-on : ubuntu-latest
233+
234+ steps :
235+ - name : Download signed Windows artifacts
236+ uses : actions/download-artifact@v4
237+ with :
238+ name : signed-windows
239+ path : ./signed/windows/
240+
241+ - name : Download signed macOS artifacts
242+ uses : actions/download-artifact@v4
243+ with :
244+ name : signed-macos
245+ path : ./signed/macos/
246+
247+ - name : Download signed Linux artifacts
248+ uses : actions/download-artifact@v4
249+ with :
250+ name : signed-linux
251+ path : ./signed/linux/
252+
253+ - name : Set up Node.js (for manifest generation)
254+ uses : actions/setup-node@v4
255+ with :
256+ node-version : ' 20'
257+
258+ - name : Regenerate electron-updater manifests from signed files
259+ shell : node {0}
260+ run : |
261+ const crypto = require('crypto');
262+ const fs = require('fs');
263+ const path = require('path');
264+
265+ const version = process.env.GITHUB_REF_NAME;
266+ const releaseDate = new Date().toISOString();
267+
268+ function sha512b64(p) {
269+ return crypto.createHash('sha512').update(fs.readFileSync(p)).digest('base64');
270+ }
271+ function byExt(dir, ext) {
272+ if (!fs.existsSync(dir)) return [];
273+ return fs.readdirSync(dir).filter(f => f.endsWith(ext)).map(f => path.join(dir, f));
274+ }
275+ function entry(p) {
276+ return ` - url: ${path.basename(p)}\n sha512: ${sha512b64(p)}\n size: ${fs.statSync(p).size}\n`;
277+ }
278+ function write(file, primary, files) {
279+ const entries = files.map(entry).join('');
280+ fs.writeFileSync(file,
281+ `version: ${version}\nfiles:\n${entries}` +
282+ `path: ${path.basename(primary)}\nsha512: ${sha512b64(primary)}\nreleaseDate: '${releaseDate}'\n`
283+ );
284+ console.log(`Generated ${file}`);
285+ }
286+
287+ // latest.yml — Windows (.exe)
288+ const exes = byExt('./signed/windows', '.exe');
289+ if (exes.length) write('latest.yml', exes[0], exes);
290+
291+ // latest-mac.yml — macOS (.dmg + .zip)
292+ const dmgs = byExt('./signed/macos', '.dmg');
293+ if (dmgs.length) write('latest-mac.yml', dmgs[0], [...dmgs, ...byExt('./signed/macos', '.zip')]);
294+
295+ // latest-linux.yml — Linux (.AppImage; .deb listed as supplementary)
296+ const imgs = byExt('./signed/linux', '.AppImage');
297+ if (imgs.length) write('latest-linux.yml', imgs[0], [...imgs, ...byExt('./signed/linux', '.deb')]);
298+
299+ - name : Publish signed artifacts + manifests to GitHub release
138300 uses : softprops/action-gh-release@v2
139301 with :
140- name : Note++ ${{ needs.build.outputs.VERSION }}
141- tag_name : ${{ needs.build.outputs.VERSION }}
302+ name : Note++ ${{ github.ref_name }}
303+ tag_name : ${{ github.ref_name }}
142304 draft : false
143305 prerelease : false
144306 make_latest : ' true'
145307 generate_release_notes : true
146- # no `files:` — electron-builder is the sole uploader
308+ files : |
309+ signed/windows/*.exe
310+ signed/windows/*.zip
311+ signed/macos/*.dmg
312+ signed/macos/*.zip
313+ signed/linux/*.AppImage
314+ signed/linux/*.deb
315+ latest.yml
316+ latest-mac.yml
317+ latest-linux.yml
147318 env :
148319 GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
0 commit comments