9494 echo "::error::Published macOS releases require signing and App Store Connect notarization secrets."
9595 exit 1
9696 fi
97- if [[ "$RUNNER_OS" == "Windows" && -n "$WINDOWS_CERTIFICATE_INPUT" && -z "$WINDOWS_CERTIFICATE_PASSWORD_INPUT" ]]; then
98- echo "::error::A configured Windows certificate requires its password."
97+ if [[ "$RUNNER_OS" == "Windows" && ( -z "$WINDOWS_CERTIFICATE_INPUT" || -z "$WINDOWS_CERTIFICATE_PASSWORD_INPUT" ) ]]; then
98+ echo "::error::Published Windows releases require an Authenticode certificate and password."
9999 exit 1
100100 fi
101101
@@ -113,23 +113,42 @@ jobs:
113113 OPENWORK_DESKTOP_TARGET : ' ${{ matrix.target }}'
114114 run : ' npm run build:runtime --prefix packages/desktop-shell --workspaces=false'
115115
116+ - name : ' Run desktop tests'
117+ run : ' npm test --prefix packages/desktop-shell --workspaces=false'
118+
119+ - name : ' Run desktop release tests'
120+ run : ' npm run test:release --prefix packages/desktop-shell --workspaces=false'
121+
116122 - name : ' Configure macOS signing and notarization'
117123 if : " runner.os == 'macOS' && inputs.publish"
118124 shell : ' bash'
119125 env :
120126 APPLE_API_KEY : ' ${{ secrets.APPLE_API_KEY || secrets.APPLE_NOTARY_KEY_ID }}'
121127 APPLE_API_KEY_P8_INPUT : ' ${{ secrets.APPLE_API_KEY_P8_BASE64 || secrets.APPLE_NOTARY_API_KEY_P8_BASE64 }}'
122128 APPLE_CERTIFICATE_INPUT : ' ${{ secrets.APPLE_CERTIFICATE || secrets.MAC_CSC_LINK }}'
129+ APPLE_CERTIFICATE_PASSWORD : ' ${{ secrets.APPLE_CERTIFICATE_PASSWORD || secrets.MAC_CSC_KEY_PASSWORD }}'
123130 run : |
124131 set -euo pipefail
132+ certificate_path="$RUNNER_TEMP/openwork-signing.p12"
125133 if [[ "$APPLE_CERTIFICATE_INPUT" =~ ^https?:// ]]; then
126- certificate_path="$RUNNER_TEMP/openwork-signing.p12"
127134 curl --fail --silent --show-error --location "$APPLE_CERTIFICATE_INPUT" --output "$certificate_path"
128- certificate="$(base64 < "$certificate_path" | tr -d '\n')"
129135 else
130- certificate="${APPLE_CERTIFICATE_INPUT#*base64,}"
136+ CERTIFICATE_PATH="$certificate_path" node -e "require('node:fs').writeFileSync(process.env.CERTIFICATE_PATH, Buffer.from(process.env.APPLE_CERTIFICATE_INPUT.replace(/^.*base64,/, ''), 'base64'))"
137+ fi
138+ keychain="$RUNNER_TEMP/openwork-signing.keychain-db"
139+ keychain_password="$(openssl rand -hex 32)"
140+ security create-keychain -p "$keychain_password" "$keychain"
141+ security set-keychain-settings -lut 21600 "$keychain"
142+ security unlock-keychain -p "$keychain_password" "$keychain"
143+ security import "$certificate_path" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain"
144+ security list-keychains -d user -s "$keychain" login.keychain-db
145+ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain"
146+ identity="$(security find-identity -v -p codesigning "$keychain" | sed -n 's/.*"\(Developer ID Application:.*\)"/\1/p' | head -n 1)"
147+ if [[ -z "$identity" ]]; then
148+ echo "::error::Developer ID Application identity was not found."
149+ exit 1
131150 fi
132- echo "APPLE_CERTIFICATE=$certificate " >> "$GITHUB_ENV"
151+ echo "APPLE_SIGNING_IDENTITY=$identity " >> "$GITHUB_ENV"
133152 key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY}.p8"
134153 APPLE_KEY_PATH="$key_path" node -e "require('node:fs').writeFileSync(process.env.APPLE_KEY_PATH, Buffer.from(process.env.APPLE_API_KEY_P8_INPUT, 'base64'), { mode: 0o600 })"
135154 echo "APPLE_API_KEY_PATH=$key_path" >> "$GITHUB_ENV"
@@ -141,7 +160,6 @@ jobs:
141160 WINDOWS_CERTIFICATE_INPUT : ' ${{ secrets.WINDOWS_CERTIFICATE || secrets.WIN_CSC_LINK }}'
142161 WINDOWS_CERTIFICATE_PASSWORD_INPUT : ' ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD || secrets.WIN_CSC_KEY_PASSWORD }}'
143162 run : |
144- if (-not $env:WINDOWS_CERTIFICATE_INPUT) { exit 0 }
145163 $certificatePath = Join-Path $env:RUNNER_TEMP 'openwork-signing.pfx'
146164 if ($env:WINDOWS_CERTIFICATE_INPUT -match '^https?://') {
147165 Invoke-WebRequest -Uri $env:WINDOWS_CERTIFICATE_INPUT -OutFile $certificatePath
@@ -154,6 +172,27 @@ jobs:
154172 if (-not $certificate.HasPrivateKey) { throw 'The Windows certificate has no private key.' }
155173 "WINDOWS_CERTIFICATE_THUMBPRINT=$($certificate.Thumbprint)" | Out-File -FilePath $env:GITHUB_ENV -Append
156174
175+ - name : ' Sign bundled runtime binaries (macOS)'
176+ if : " runner.os == 'macOS' && inputs.publish"
177+ shell : ' bash'
178+ run : |
179+ set -euo pipefail
180+ while IFS= read -r -d '' binary; do
181+ if ! file "$binary" | grep -q 'Mach-O'; then continue; fi
182+ args=(--force --sign "$APPLE_SIGNING_IDENTITY" --options runtime --timestamp)
183+ if [[ "$binary" == */node/bin/node ]]; then
184+ args+=(--entitlements packages/desktop-shell/src-tauri/NodeEntitlements.plist)
185+ fi
186+ codesign "${args[@]}" "$binary"
187+ done < <(find packages/desktop-shell/runtime/openwork -type f -print0)
188+
189+ - name : ' Refresh bundled runtime checksums (macOS)'
190+ if : " runner.os == 'macOS' && inputs.publish"
191+ run : ' node packages/desktop-shell/scripts/prepare-runtime.js --refresh-checksums'
192+
193+ - name : ' Verify bundled runtime'
194+ run : ' npm run smoke:runtime --prefix packages/desktop-shell --workspaces=false'
195+
157196 - name : ' Configure platform signing'
158197 shell : ' bash'
159198 env :
@@ -165,22 +204,161 @@ jobs:
165204 - name : ' Build desktop artifacts'
166205 uses : ' tauri-apps/tauri-action@1deb371b0cd8bd54025b384f1cd735e725c4060f' # v1.0.0
167206 env :
168- GITHUB_TOKEN : " ${{ inputs.publish && secrets.GITHUB_TOKEN || '' }}"
169207 APPLE_API_ISSUER : " ${{ runner.os == 'macOS' && inputs.publish && (secrets.APPLE_API_ISSUER || secrets.APPLE_NOTARY_ISSUER_ID) || '' }}"
170208 APPLE_API_KEY : " ${{ runner.os == 'macOS' && inputs.publish && (secrets.APPLE_API_KEY || secrets.APPLE_NOTARY_KEY_ID) || '' }}"
171209 APPLE_CERTIFICATE_PASSWORD : " ${{ runner.os == 'macOS' && inputs.publish && (secrets.APPLE_CERTIFICATE_PASSWORD || secrets.MAC_CSC_KEY_PASSWORD) || '' }}"
172- APPLE_SIGNING_IDENTITY : " ${{ runner.os == 'macOS' && inputs.publish && secrets.APPLE_SIGNING_IDENTITY || '' }}"
173210 TAURI_SIGNING_PRIVATE_KEY : " ${{ inputs.publish && secrets.TAURI_SIGNING_PRIVATE_KEY || '' }}"
174211 TAURI_SIGNING_PRIVATE_KEY_PASSWORD : " ${{ inputs.publish && secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD || '' }}"
175212 with :
176213 projectPath : ' packages/desktop-shell'
177214 args : ' --config src-tauri/release.conf.json --target ${{ matrix.target }}'
178- tagName : " ${{ inputs.publish && inputs.tag || '' }}"
179- releaseName : " ${{ inputs.publish && inputs.release_name || '' }}"
180- releaseDraft : ' ${{ inputs.draft }}'
181- prerelease : ' ${{ inputs.prerelease }}'
182- generateReleaseNotes : true
183- uploadUpdaterJson : ' ${{ inputs.publish }}'
184- uploadUpdaterSignatures : ' ${{ inputs.publish }}'
215+ uploadUpdaterJson : false
216+ uploadUpdaterSignatures : false
185217 updaterJsonPreferNsis : true
186- uploadWorkflowArtifacts : true
218+ uploadWorkflowArtifacts : false
219+
220+ - name : ' Verify macOS signature'
221+ if : " runner.os == 'macOS' && inputs.publish"
222+ shell : ' bash'
223+ run : |
224+ set -euo pipefail
225+ app="$(find packages/desktop-shell/src-tauri/target/${{ matrix.target }}/release/bundle/macos -maxdepth 1 -name '*.app' -print -quit)"
226+ codesign --verify --deep --strict --verbose=2 "$app"
227+ spctl --assess --type execute --verbose=2 "$app"
228+
229+ - name : ' Verify Windows signature'
230+ if : " runner.os == 'Windows' && inputs.publish"
231+ shell : ' pwsh'
232+ run : |
233+ $installer = Get-ChildItem packages/desktop-shell/src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*.exe | Select-Object -First 1
234+ $signature = Get-AuthenticodeSignature $installer.FullName
235+ if ($signature.Status -ne 'Valid') { throw "Invalid Authenticode signature: $($signature.Status)" }
236+
237+ - name : ' Smoke packaged application (macOS)'
238+ if : " runner.os == 'macOS'"
239+ shell : ' bash'
240+ working-directory : ' packages/desktop-shell'
241+ run : |
242+ set -euo pipefail
243+ executable="$(find src-tauri/target/${{ matrix.target }}/release/bundle/macos -path '*.app/Contents/MacOS/*' -type f -perm -111 -print -quit)"
244+ npm run smoke:packaged -- "$executable"
245+
246+ - name : ' Smoke packaged application (Windows)'
247+ if : " runner.os == 'Windows'"
248+ shell : ' pwsh'
249+ working-directory : ' packages/desktop-shell'
250+ run : |
251+ $executable = Get-ChildItem src-tauri/target/${{ matrix.target }}/release/openwork-desktop.exe | Select-Object -First 1
252+ npm run smoke:packaged -- $executable.FullName
253+
254+ - name : ' Smoke packaged application (Linux)'
255+ if : " runner.os == 'Linux'"
256+ shell : ' bash'
257+ working-directory : ' packages/desktop-shell'
258+ run : ' xvfb-run -a npm run smoke:packaged -- src-tauri/target/${{ matrix.target }}/release/openwork-desktop'
259+
260+ - name : ' Collect verified artifacts'
261+ shell : ' bash'
262+ run : |
263+ set -euo pipefail
264+ destination="$RUNNER_TEMP/openwork-desktop-artifacts"
265+ mkdir -p "$destination"
266+ bundle_root="packages/desktop-shell/src-tauri/target/${{ matrix.target }}/release/bundle"
267+ while IFS= read -r -d '' artifact; do
268+ name="$(basename "$artifact")"
269+ if [[ "$RUNNER_OS" == 'macOS' ]]; then
270+ case "$name" in
271+ *.app.tar.gz.sig) name="${name%.app.tar.gz.sig}-${{ matrix.target }}.app.tar.gz.sig" ;;
272+ *.app.tar.gz) name="${name%.app.tar.gz}-${{ matrix.target }}.app.tar.gz" ;;
273+ *.dmg) name="OpenWork-${{ matrix.target }}.dmg" ;;
274+ *) continue ;;
275+ esac
276+ elif [[ "$RUNNER_OS" == 'Windows' ]]; then
277+ case "$name" in *-setup.exe|*-setup.exe.sig) ;; *) continue ;; esac
278+ else
279+ case "$name" in *.AppImage|*.AppImage.sig|*.deb|*.deb.sig) ;; *) continue ;; esac
280+ fi
281+ cp "$artifact" "$destination/${name// /-}"
282+ done < <(find "$bundle_root" -mindepth 2 -maxdepth 2 -type f \( -name '*.dmg' -o -name '*.AppImage' -o -name '*.deb' -o -name '*.exe' -o -name '*.app.tar.gz' -o -name '*.sig' \) -print0)
283+ if [[ -z "$(find "$destination" -type f -print -quit)" ]]; then
284+ echo '::error::No desktop artifacts were produced.'
285+ exit 1
286+ fi
287+
288+ - name : ' Upload verified artifacts'
289+ uses : ' actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # v4
290+ with :
291+ name : ' openwork-desktop-${{ matrix.target }}'
292+ path : ' ${{ runner.temp }}/openwork-desktop-artifacts/*'
293+ if-no-files-found : ' error'
294+ retention-days : 14
295+
296+ publish :
297+ name : ' Publish verified release'
298+ if : ' inputs.publish'
299+ needs : ' desktop'
300+ runs-on : ' ubuntu-latest'
301+ permissions :
302+ contents : ' write'
303+ steps :
304+ - name : ' Check out source'
305+ uses : ' actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
306+ with :
307+ persist-credentials : false
308+
309+ - name : ' Download verified artifacts'
310+ uses : ' actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093' # v4
311+ with :
312+ pattern : ' openwork-desktop-*'
313+ path : ' release-assets'
314+ merge-multiple : true
315+
316+ - name : ' Generate updater manifest and checksums'
317+ shell : ' bash'
318+ env :
319+ RELEASE_TAG : ' ${{ inputs.tag }}'
320+ RELEASE_VERSION : ' ${{ inputs.version }}'
321+ run : |
322+ set -euo pipefail
323+ node .github/scripts/create-desktop-update-manifest.mjs --assets release-assets --repository "$GITHUB_REPOSITORY" --tag "$RELEASE_TAG" --version "$RELEASE_VERSION" --output release-assets/latest.json
324+ (cd release-assets && sha256sum -- * > SHA256SUMS.txt)
325+
326+ - name : ' Create GitHub release'
327+ env :
328+ GH_TOKEN : ' ${{ github.token }}'
329+ RELEASE_DRAFT : ' ${{ inputs.draft }}'
330+ RELEASE_NAME : ' ${{ inputs.release_name }}'
331+ RELEASE_PRERELEASE : ' ${{ inputs.prerelease }}'
332+ RELEASE_TAG : ' ${{ inputs.tag }}'
333+ run : |
334+ set -euo pipefail
335+ args=("$RELEASE_TAG" release-assets/* --target "$GITHUB_SHA" --title "$RELEASE_NAME" --generate-notes --latest=false)
336+ if [[ "$RELEASE_DRAFT" == 'true' ]]; then args+=(--draft); fi
337+ if [[ "$RELEASE_PRERELEASE" == 'true' ]]; then args+=(--prerelease); fi
338+ gh release create "${args[@]}"
339+
340+ - name : ' Update stable updater feed'
341+ if : ' inputs.draft == false && inputs.prerelease == false'
342+ env :
343+ GH_TOKEN : ' ${{ github.token }}'
344+ RELEASE_VERSION : ' ${{ inputs.version }}'
345+ run : |
346+ set -euo pipefail
347+ if gh release view desktop-latest >/dev/null 2>&1; then
348+ directory="$(mktemp -d)"
349+ trap 'rm -rf "$directory"' EXIT
350+ gh release download desktop-latest --dir "$directory" --pattern 'latest.json'
351+ current="$(jq -r '.version' "$directory/latest.json")"
352+ if [[ ! "$current" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
353+ echo "::error::Current Desktop stable feed has an invalid version: $current"
354+ exit 1
355+ fi
356+ newest="$(printf '%s\n%s\n' "$RELEASE_VERSION" "$current" | sort -V | tail -n 1)"
357+ if [[ "$current" != "$RELEASE_VERSION" && "$newest" == "$current" ]]; then
358+ echo "::notice::Desktop $RELEASE_VERSION will not replace newer stable feed $current."
359+ exit 0
360+ fi
361+ gh release upload desktop-latest release-assets/latest.json --clobber
362+ else
363+ gh release create desktop-latest release-assets/latest.json --title 'OpenWork Desktop latest' --notes 'Stable desktop updater feed.' --latest=false
364+ fi
0 commit comments