Repository navigation
chore(release): bump to 2.10.4 #152
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: ['v*'] | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Tag to release (e.g. v0.5.6). Leave empty to use current package.json version.' | |
| required: false | |
| permissions: | |
| contents: write | |
| jobs: | |
| build-sign-release: | |
| runs-on: windows-latest | |
| # SignPath Authenticode signing (issue #71). The signing steps run only when | |
| # both secrets are configured, so releases keep working while SignPath OSS | |
| # approval is pending — and turn on automatically once the secrets land. | |
| env: | |
| SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} | |
| SIGNPATH_ORGANIZATION_ID: ${{ secrets.SIGNPATH_ORGANIZATION_ID }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| - name: Fix Python distutils for node-gyp | |
| run: pip install setuptools | |
| - name: Install dependencies | |
| run: npm ci | |
| # ── Gate ─────────────────────────────────────────────── | |
| # A tag push used to go straight from `npm ci` to a published release with | |
| # no test or typecheck run anywhere in between. 0.35.0 shipped a renderer | |
| # that threw on first paint for every user (a TDZ read in a useMemo deps | |
| # array, which `tsc` reports as TS2448 in under two seconds) because | |
| # nothing here ever asked. Failing the release is the point: a tag that | |
| # cannot pass its own tests must not become a download. | |
| - name: Test | |
| run: npm test | |
| # ── Build ────────────────────────────────────────────── | |
| - name: Build main + preload + CLI | |
| run: npm run build:main | |
| # resources/cli/*.js is a checked-in copy of this build's output, and | |
| # nothing kept it in step — it was ~1700 diff lines behind its own source | |
| # when #168 measured it. Nothing shipped reads it (extraResources maps | |
| # dist/cli), but dev-mode Claude hook registration does, so a stale copy | |
| # means dev behaves unlike production for no visible reason. | |
| # | |
| # Runs AFTER build:main because that is what makes dist/cli exist; the | |
| # build-free half of the same invariant (shell-integration) is covered by | |
| # npm test above, so it gates a release even if this step is ever moved. | |
| - name: Verify checked-in resources match their sources | |
| run: npm run verify:resources | |
| - name: Build renderer | |
| run: npx vite build | |
| # ── Package (NSIS installer) ─────────────────────────── | |
| # Build the NSIS installer so electron-updater can actually install the | |
| # update on Windows. The previous `--dir` build shipped a zip, which | |
| # electron-updater's NsisUpdater can download and verify but has no way to | |
| # install — quitAndInstall() no-ops and the app relaunches unchanged | |
| # (issue #96). --publish never: we upload manually below so latest.yml is | |
| # generated AFTER signing (see the note on that step). | |
| - name: Package with electron-builder | |
| run: npx electron-builder --win --publish never | |
| - name: Extract version | |
| id: version | |
| shell: bash | |
| run: | | |
| VER=$(node -p "require('./package.json').version") | |
| echo "version=$VER" >> "$GITHUB_OUTPUT" | |
| if [ "${{ github.event_name }}" = "push" ]; then | |
| echo "tag=${{ github.ref_name }}" >> "$GITHUB_OUTPUT" | |
| elif [ -n "${{ github.event.inputs.tag }}" ]; then | |
| echo "tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "tag=v$VER" >> "$GITHUB_OUTPUT" | |
| fi | |
| # ── Icon + version metadata ──────────────────────────── | |
| # electron-builder embeds win.icon and the package.json version into the | |
| # packaged exe during the NSIS build above, so the previous manual rcedit | |
| # pass is no longer needed — it targeted release/win-unpacked/wmux.exe, | |
| # which is already baked into the installer by the time a separate step | |
| # could run, so it had no effect on the shipped artifact. | |
| # ── Sign exe with SignPath (runs only when secrets are set, #71) ── | |
| # continue-on-error: a rejected submission (e.g. SignPath OSS yearly | |
| # quota exceeded) must not block the release — we ship unsigned, as | |
| # before, and signing resumes automatically once quota is available. | |
| # The verify step below stays fatal: if a SIGNED artifact comes back | |
| # invalid, the release fails rather than shipping it. | |
| - name: Submit exe to SignPath for Authenticode signing | |
| id: sign-submit | |
| if: env.SIGNPATH_API_TOKEN != '' && env.SIGNPATH_ORGANIZATION_ID != '' | |
| continue-on-error: true | |
| shell: bash | |
| run: | | |
| HTTP_RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" \ | |
| -X POST \ | |
| -H "Authorization: Bearer $SIGNPATH_API_TOKEN" \ | |
| -F "projectSlug=wmux" \ | |
| -F "signingPolicySlug=wmux" \ | |
| -F "artifactConfigurationSlug=initial" \ | |
| -F "description=CI release v${{ steps.version.outputs.version }}" \ | |
| -F "artifact=@release/wmux-${{ steps.version.outputs.version }}-setup.exe" \ | |
| -D headers.txt \ | |
| "https://app.signpath.io/api/v1/$SIGNPATH_ORGANIZATION_ID/SigningRequests/SubmitWithArtifact") | |
| HTTP_CODE=$(echo "$HTTP_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2) | |
| echo "HTTP status: $HTTP_CODE" | |
| if [ "$HTTP_CODE" != "201" ]; then | |
| echo "::warning::SignPath submission failed (HTTP $HTTP_CODE) — releasing unsigned" | |
| echo "$HTTP_RESPONSE" | |
| exit 1 | |
| fi | |
| LOCATION=$(grep -i "^location:" headers.txt | tr -d '\r') | |
| SIGNING_REQUEST_ID=$(echo "$LOCATION" | grep -oE '[0-9a-f-]{36}$') | |
| echo "Signing request ID: $SIGNING_REQUEST_ID" | |
| echo "signing_request_id=$SIGNING_REQUEST_ID" >> "$GITHUB_OUTPUT" | |
| - name: Wait for signing to complete | |
| if: steps.sign-submit.outcome == 'success' && steps.sign-submit.outputs.signing_request_id != '' | |
| shell: bash | |
| run: | | |
| REQ_ID="${{ steps.sign-submit.outputs.signing_request_id }}" | |
| API="https://app.signpath.io/api/v1/$SIGNPATH_ORGANIZATION_ID/SigningRequests/$REQ_ID" | |
| for i in $(seq 1 60); do | |
| RESPONSE=$(curl -s -H "Authorization: Bearer $SIGNPATH_API_TOKEN" "$API") | |
| STATUS=$(echo "$RESPONSE" | jq -r '.status') | |
| echo "[$i/60] Status: $STATUS" | |
| if [ "$STATUS" = "Completed" ]; then | |
| echo "Signing completed successfully!" | |
| exit 0 | |
| elif [ "$STATUS" = "Failed" ] || [ "$STATUS" = "Denied" ] || [ "$STATUS" = "Canceled" ]; then | |
| echo "::error::Signing request $STATUS" | |
| echo "$RESPONSE" | jq . | |
| exit 1 | |
| fi | |
| sleep 10 | |
| done | |
| echo "::error::Signing timed out after 10 minutes" | |
| exit 1 | |
| - name: Download signed exe and verify signature | |
| id: sign-verify | |
| if: steps.sign-submit.outcome == 'success' && steps.sign-submit.outputs.signing_request_id != '' | |
| shell: bash | |
| run: | | |
| curl -s \ | |
| -H "Authorization: Bearer $SIGNPATH_API_TOKEN" \ | |
| -o "release/wmux-${{ steps.version.outputs.version }}-setup.exe" \ | |
| "https://app.signpath.io/api/v1/$SIGNPATH_ORGANIZATION_ID/SigningRequests/${{ steps.sign-submit.outputs.signing_request_id }}/SignedArtifact" | |
| echo "Signed installer downloaded, size: $(wc -c < release/wmux-${{ steps.version.outputs.version }}-setup.exe) bytes" | |
| # trusted=true only for a chain-valid Authenticode signature. A | |
| # SignPath project can also sign with a SELF-SIGNED certificate | |
| # (e.g. while the OSS "SignPath Foundation" cert is unavailable, | |
| # issue #71): Get-AuthenticodeSignature then reports UnknownError | |
| # (untrusted root). That artifact is fine to ship for manual | |
| # download, but it must NOT be advertised to the auto-updater — | |
| # electron-updater requires a Valid signature on the user's machine, | |
| # so a self-signed installer downloads and then fails to install, | |
| # which is exactly the endless-update loop of issue #96. | |
| powershell -NoProfile -Command " | |
| \$sig = Get-AuthenticodeSignature 'release/wmux-${{ steps.version.outputs.version }}-setup.exe'; | |
| \$subject = if (\$sig.SignerCertificate) { \$sig.SignerCertificate.Subject } else { '(none)' }; | |
| Write-Host ('Status: ' + \$sig.Status + ' — Subject: ' + \$subject); | |
| \$trusted = if (\$sig.Status -eq 'Valid') { 'true' } else { 'false' }; | |
| Add-Content -Path \$env:GITHUB_OUTPUT -Value ('trusted=' + \$trusted); | |
| if (\$trusted -eq 'false') { | |
| Write-Host '::warning::Installer signature is not chain-trusted (self-signed cert?). Shipping anyway — clients no longer pin publisherName, so auto-update still works (integrity via latest.yml sha512).' | |
| } | |
| " | |
| # ── Portable zip ─────────────────────────────────────── | |
| # Keep shipping the portable zip alongside the NSIS installer: the docs | |
| # and wmux.org tell users to grab wmux-X.Y.Z-win-x64.zip, and unsigned | |
| # zips trip SmartScreen far less than unsigned installers. The NSIS build | |
| # above still emits release/win-unpacked/ as an intermediate, so this is | |
| # free. Auto-update (latest.yml) points at the installer, not this zip — | |
| # a zip can be downloaded but never installed by NsisUpdater (issue #96). | |
| - name: Create portable zip | |
| shell: pwsh | |
| run: | | |
| Compress-Archive -Path "release/win-unpacked/*" ` | |
| -DestinationPath "wmux-${{ steps.version.outputs.version }}-win-x64.zip" | |
| # ── Generate latest.yml for electron-updater ────────── | |
| # ALWAYS published. The `win.publisherName` pin was removed from | |
| # electron-builder.json: with no pin, NsisUpdater skips Authenticode | |
| # verification, so unsigned/self-signed installers install fine — | |
| # integrity comes from the sha512 below, and the client-side quarantine | |
| # window + explicit install dialog (updater.ts) remain the safety net. | |
| # History: while the pin existed, latest.yml was gated on a chain-trusted | |
| # signature; since SignPath fell back to a self-signed cert, that gate | |
| # silently stranded every install on its current version (no latest.yml | |
| # → 404 → no update ever). If chain-trusted signing returns, re-add the | |
| # pin AND re-gate this step together. | |
| # Installs ≤0.31 still carry the pin in their local app-update.yml and | |
| # will fail verification silently; the notify-only checker | |
| # (update-checker.ts) tells those users to update manually once. | |
| # | |
| # MUST run last, AFTER signing: SignPath rewrites the installer PE, so a | |
| # hash computed earlier would not match the shipped file. Points at the | |
| # NSIS installer (a zip can be downloaded but never installed — issue | |
| # #96). No blockmap is emitted, so electron-updater does a full | |
| # (non-differential) download verified by the top-level sha512. | |
| - name: Generate latest.yml | |
| shell: bash | |
| run: | | |
| node -e " | |
| const crypto = require('crypto'); | |
| const fs = require('fs'); | |
| const version = '${{ steps.version.outputs.version }}'; | |
| const installer = 'wmux-' + version + '-setup.exe'; | |
| const data = fs.readFileSync('release/' + installer); | |
| const sha512 = crypto.createHash('sha512').update(data).digest('base64'); | |
| const size = data.length; | |
| const date = new Date().toISOString(); | |
| const yaml = [ | |
| 'version: ' + version, | |
| 'files:', | |
| ' - url: ' + installer, | |
| ' sha512: ' + sha512, | |
| ' size: ' + size, | |
| 'path: ' + installer, | |
| 'sha512: ' + sha512, | |
| \"releaseDate: '\" + date + \"'\", | |
| '' | |
| ].join('\n'); | |
| fs.writeFileSync('latest.yml', yaml); | |
| console.log('latest.yml generated for', installer, 'size=' + size, 'sha512=' + sha512.substring(0, 20) + '...'); | |
| " | |
| - name: Upload release artifacts as workflow artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: wmux-release | |
| path: | | |
| release/wmux-${{ steps.version.outputs.version }}-setup.exe | |
| wmux-${{ steps.version.outputs.version }}-win-x64.zip | |
| latest.yml | |
| # ── GitHub Release ───────────────────────────────────── | |
| - name: Create or update GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.version.outputs.tag }} | |
| name: wmux ${{ steps.version.outputs.tag }} | |
| files: | | |
| release/wmux-${{ steps.version.outputs.version }}-setup.exe | |
| wmux-${{ steps.version.outputs.version }}-win-x64.zip | |
| latest.yml | |
| draft: false | |
| generate_release_notes: true |