@@ -2,10 +2,16 @@ name: CI
22
33on :
44 push :
5- branches : [ main ]
5+ branches : [main]
66 pull_request :
7- branches : [ main ]
8- workflow_dispatch : # Manually run the signed-release flow from any branch (creates a draft release)
7+ branches : [main]
8+ workflow_dispatch :
9+ inputs :
10+ should_release :
11+ description : Build, sign, and publish a draft release
12+ required : true
13+ type : boolean
14+ default : false
915
1016permissions :
1117 contents : write
@@ -15,103 +21,236 @@ jobs:
1521 runs-on : windows-latest
1622
1723 steps :
18- - uses : actions/checkout@v6
19- with :
20- fetch-depth : 2 # Fetch the last 2 commits to compare changes
21-
22- - name : Setup Node.js
23- uses : actions/setup-node@v6
24- with :
25- node-version : ' 24'
26- cache : ' npm'
27-
28- - name : Cache node_modules and native builds
29- uses : actions/cache@v5
30- id : cache-deps
31- with :
32- path : |
33- node_modules
34- .vite
35- **/node_modules
36- **/*.node
37- **/build/Release
38- key : ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/binding.gyp', '**/node_modules/**/*.node') }}
39- restore-keys : |
40- ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}-
41- ${{ runner.os }}-node-
42-
43- - name : Install dependencies
44- if : steps.cache-deps.outputs.cache-hit != 'true'
45- run : npm ci
46-
47- - name : Run linting
48- run : npm run lint
49-
50- - name : Build Storybook
51- run : npm run build-storybook
52-
53- - name : Run tests with coverage
54- run : npm test
55-
56- - name : Upload coverage report
57- uses : actions/upload-artifact@v7
58- with :
59- name : coverage-report
60- path : coverage
61-
62- - name : Check if version changed
63- id : check_version
64- shell : pwsh
65- run : |
66- $currentVersion = (Get-Content package.json | ConvertFrom-Json).version
67- $previousVersion = (git show HEAD^:package.json | ConvertFrom-Json).version
68- if ($currentVersion -ne $previousVersion) {
69- echo "version_changed=true" >> $env:GITHUB_OUTPUT
70- echo "Version changed from $previousVersion to $currentVersion"
71- }
72-
73- # --- Signed release (only when the version bumps) ---------------------
74- # Flow: build unsigned (dry-run) -> sign Setup.exe via SignPath -> swap the
75- # signed installer back in place -> publish from the saved dry-run.
76- # The dry-run manifest stores paths to out/make/... (no content hashing), so
77- # replacing the Setup.exe file is picked up by --from-dry-run. RELEASES and
78- # the .nupkg are never touched, keeping auto-updates valid.
79-
80- - name : Build unsigned artifacts (publish dry-run)
81- if : steps.check_version.outputs.version_changed == 'true' || github.event_name == 'workflow_dispatch'
82- run : npx electron-forge publish --dry-run
83- env :
84- POSTHOG_KEY : ${{ secrets.POSTHOG_KEY }}
85-
86- - name : Upload unsigned installer for signing
87- id : upload-unsigned
88- if : steps.check_version.outputs.version_changed == 'true' || github.event_name == 'workflow_dispatch'
89- uses : actions/upload-artifact@v7
90- with :
91- name : unsigned-installer
92- if-no-files-found : error
93- path : out/make/squirrel.windows/x64/*Setup.exe
94-
95- - name : Sign installer with SignPath
96- if : steps.check_version.outputs.version_changed == 'true' || github.event_name == 'workflow_dispatch'
97- uses : signpath/github-action-submit-signing-request@v2
98- with :
99- api-token : ${{ secrets.SIGNPATH_API_TOKEN }}
100- organization-id : ${{ vars.SIGNPATH_ORGANIZATION_ID }}
101- project-slug : irdashies
102- signing-policy-slug : release-signing
103- artifact-configuration-slug : initial
104- github-artifact-id : ${{ steps.upload-unsigned.outputs.artifact-id }}
105- wait-for-completion : true
106- output-artifact-directory : signed-installer
107-
108- - name : Swap in the signed installer
109- if : steps.check_version.outputs.version_changed == 'true' || github.event_name == 'workflow_dispatch'
110- shell : pwsh
111- run : Copy-Item signed-installer/*Setup.exe out/make/squirrel.windows/x64/ -Force
112-
113- - name : Publish signed release
114- if : steps.check_version.outputs.version_changed == 'true' || github.event_name == 'workflow_dispatch'
115- run : npx electron-forge publish --from-dry-run
116- env :
117- GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
24+ - uses : actions/checkout@v6
25+ with :
26+ fetch-depth : 2 # Fetch the last 2 commits to compare changes
27+ persist-credentials : false
28+
29+ - name : Setup Node.js
30+ uses : actions/setup-node@v6
31+ with :
32+ node-version : ' 24'
33+ cache : ' npm'
34+
35+ - name : Cache node_modules and native builds
36+ uses : actions/cache@v5
37+ id : cache-deps
38+ with :
39+ path : |
40+ node_modules
41+ .vite
42+ **/node_modules
43+ **/*.node
44+ **/build/Release
45+ key : ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/binding.gyp', '**/node_modules/**/*.node') }}
46+ restore-keys : |
47+ ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}-
48+ ${{ runner.os }}-node-
49+
50+ - name : Install dependencies
51+ if : steps.cache-deps.outputs.cache-hit != 'true'
52+ run : npm ci
53+
54+ - name : Run linting
55+ run : npm run lint
56+
57+ - name : Build Storybook
58+ run : npm run build-storybook
59+
60+ - name : Run tests with coverage
61+ run : npm test
62+
63+ - name : Upload coverage report
64+ uses : actions/upload-artifact@v7
65+ with :
66+ name : coverage-report
67+ path : coverage
68+
69+ - name : Determine release eligibility
70+ id : check_version
71+ shell : pwsh
72+ env :
73+ DISPATCH_SHOULD_RELEASE : ${{ inputs.should_release }}
74+ run : |
75+ $currentVersion = (Get-Content package.json | ConvertFrom-Json).version
76+ $previousVersion = (git show HEAD^:package.json | ConvertFrom-Json).version
77+ $versionChanged = $currentVersion -ne $previousVersion
78+ $shouldRelease = (
79+ ($env:GITHUB_EVENT_NAME -eq 'push' -and $versionChanged) -or
80+ ($env:GITHUB_EVENT_NAME -eq 'workflow_dispatch' -and
81+ $env:DISPATCH_SHOULD_RELEASE -eq 'true')
82+ )
83+
84+ echo "version_changed=$($versionChanged.ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT
85+ echo "should_release=$($shouldRelease.ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT
86+
87+ if ($versionChanged) {
88+ echo "Version changed from $previousVersion to $currentVersion"
89+ }
90+
91+ # --- Signed release ----------------------------------------------------
92+ # Pushes release only when the version changes. Manual runs release only
93+ # when the should_release workflow-dispatch input is enabled. Pull
94+ # requests never enter this flow.
95+ # Flow: build unsigned (dry-run) -> sign the packaged application via
96+ # SignPath -> rebuild Squirrel artifacts from the signed package -> sign
97+ # Setup.exe via SignPath -> publish from the saved dry-run.
98+ # The dry-run manifest stores paths to out/make/... (no content hashing), so
99+ # rebuilding the artifacts and replacing Setup.exe are picked up by
100+ # --from-dry-run. Rebuilding all Squirrel artifacts together keeps RELEASES
101+ # and the .nupkg checksums consistent for auto-updates.
102+
103+ - name : Build unsigned artifacts (publish dry-run)
104+ if : steps.check_version.outputs.should_release == 'true'
105+ run : npx electron-forge publish --dry-run
106+ env :
107+ POSTHOG_KEY : ${{ secrets.POSTHOG_KEY }}
108+
109+ - name : Archive packaged Windows application
110+ if : steps.check_version.outputs.should_release == 'true'
111+ shell : pwsh
112+ run : |
113+ $packageDirectory = 'out/irdashies-win32-x64'
114+ if (-not (Test-Path -LiteralPath $packageDirectory -PathType Container)) {
115+ throw "Packaged application not found: $packageDirectory"
116+ }
117+ Compress-Archive `
118+ -Path "$packageDirectory/*" `
119+ -DestinationPath packaged-windows-app.zip `
120+ -CompressionLevel Optimal
121+
122+ - name : Upload packaged application for signing
123+ id : upload-packaged-app
124+ if : steps.check_version.outputs.should_release == 'true'
125+ uses : actions/upload-artifact@v7
126+ with :
127+ name : unsigned-packaged-windows-app
128+ if-no-files-found : error
129+ path : packaged-windows-app.zip
130+
131+ - name : Sign packaged application
132+ if : steps.check_version.outputs.should_release == 'true'
133+ uses : signpath/github-action-submit-signing-request@v2
134+ with :
135+ api-token : ${{ secrets.SIGNPATH_API_TOKEN }}
136+ organization-id : ${{ vars.SIGNPATH_ORGANIZATION_ID }}
137+ project-slug : irdashies
138+ signing-policy-slug : release-signing
139+ artifact-configuration-slug : packaged-app
140+ github-artifact-id : ${{ steps.upload-packaged-app.outputs.artifact-id }}
141+ wait-for-completion : true
142+ output-artifact-directory : signed-packaged-app
143+
144+ - name : Restore and verify signed packaged application
145+ if : steps.check_version.outputs.should_release == 'true'
146+ shell : pwsh
147+ run : |
148+ $signedArchive = Get-ChildItem `
149+ -LiteralPath signed-packaged-app `
150+ -Filter '*.zip' `
151+ -File `
152+ -Recurse |
153+ Select-Object -First 1
154+ if (-not $signedArchive) {
155+ throw 'SignPath did not return the signed packaged application ZIP.'
156+ }
157+
158+ Expand-Archive `
159+ -LiteralPath $signedArchive.FullName `
160+ -DestinationPath out/irdashies-win32-x64 `
161+ -Force
162+
163+ $appExecutable = 'out/irdashies-win32-x64/irdashies.exe'
164+ $signature = Get-AuthenticodeSignature -LiteralPath $appExecutable
165+ if ($signature.Status -ne 'Valid') {
166+ throw "Packaged application signature is $($signature.Status): $($signature.StatusMessage)"
167+ }
168+ Write-Output "Verified signed packaged application: $($signature.SignerCertificate.Subject)"
169+
170+ - name : Rebuild Squirrel artifacts from signed application
171+ if : steps.check_version.outputs.should_release == 'true'
172+ run : npx electron-forge make --skip-package
173+
174+ - name : Verify signed application in Squirrel package
175+ if : steps.check_version.outputs.should_release == 'true'
176+ shell : pwsh
177+ run : |
178+ $package = Get-ChildItem `
179+ -Path out/make/squirrel.windows/x64 `
180+ -Filter '*.nupkg' `
181+ -File |
182+ Where-Object { $_.Name -notlike '*-delta.nupkg' } |
183+ Select-Object -First 1
184+ if (-not $package) {
185+ throw 'Squirrel full package was not generated.'
186+ }
187+
188+ $extractDirectory = Join-Path $env:RUNNER_TEMP 'irdashies-nupkg'
189+ New-Item -ItemType Directory -Path $extractDirectory -Force | Out-Null
190+ $packageArchive = Join-Path $env:RUNNER_TEMP 'irdashies-nupkg.zip'
191+ Copy-Item -LiteralPath $package.FullName -Destination $packageArchive -Force
192+ Expand-Archive `
193+ -LiteralPath $packageArchive `
194+ -DestinationPath $extractDirectory `
195+ -Force
196+
197+ $packagedExecutable = Get-ChildItem `
198+ -LiteralPath $extractDirectory `
199+ -Recurse `
200+ -Filter 'irdashies.exe' `
201+ -File |
202+ Select-Object -First 1
203+ if (-not $packagedExecutable) {
204+ throw 'irdashies.exe was not found in the Squirrel package.'
205+ }
206+
207+ $signature = Get-AuthenticodeSignature `
208+ -LiteralPath $packagedExecutable.FullName
209+ if ($signature.Status -ne 'Valid') {
210+ throw "Squirrel package contains an application with signature status $($signature.Status)."
211+ }
212+ Write-Output "Verified signed application in $($package.Name)."
213+
214+ - name : Upload unsigned installer for signing
215+ id : upload-unsigned
216+ if : steps.check_version.outputs.should_release == 'true'
217+ uses : actions/upload-artifact@v7
218+ with :
219+ name : unsigned-installer
220+ if-no-files-found : error
221+ path : out/make/squirrel.windows/x64/*Setup.exe
222+
223+ - name : Sign installer with SignPath
224+ if : steps.check_version.outputs.should_release == 'true'
225+ uses : signpath/github-action-submit-signing-request@v2
226+ with :
227+ api-token : ${{ secrets.SIGNPATH_API_TOKEN }}
228+ organization-id : ${{ vars.SIGNPATH_ORGANIZATION_ID }}
229+ project-slug : irdashies
230+ signing-policy-slug : release-signing
231+ artifact-configuration-slug : initial
232+ github-artifact-id : ${{ steps.upload-unsigned.outputs.artifact-id }}
233+ wait-for-completion : true
234+ output-artifact-directory : signed-installer
235+
236+ - name : Swap in the signed installer
237+ if : steps.check_version.outputs.should_release == 'true'
238+ shell : pwsh
239+ run : |
240+ Copy-Item signed-installer/*Setup.exe out/make/squirrel.windows/x64/ -Force
241+ $installer = Get-ChildItem `
242+ -Path out/make/squirrel.windows/x64 `
243+ -Filter '*Setup.exe' `
244+ -File |
245+ Select-Object -First 1
246+ $signature = Get-AuthenticodeSignature -LiteralPath $installer.FullName
247+ if ($signature.Status -ne 'Valid') {
248+ throw "Installer signature is $($signature.Status): $($signature.StatusMessage)"
249+ }
250+ Write-Output "Verified signed installer: $($signature.SignerCertificate.Subject)"
251+
252+ - name : Publish signed release
253+ if : steps.check_version.outputs.should_release == 'true'
254+ run : npx electron-forge publish --from-dry-run
255+ env :
256+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
0 commit comments