name: Release (Windows) # Builds + signs + uploads the Windows installer to the GitHub Release matching # the app version in electron/package.json. Mac builds stay on the local # publish.sh flow — this workflow is Windows-only on purpose. # # Triggers: # - Push a tag `v*` (e.g. v1.0.25) → full signed release build, uploaded to # the release of that tag (creates it in draft if absent). # - Manual dispatch (workflow_dispatch) with `publish: false` → signed build # as an artifact, no release upload. Useful for smoke-tests. # # Required repository secrets (Settings → Secrets and variables → Actions): # AZURE_TENANT_ID # AZURE_CLIENT_ID # AZURE_CLIENT_SECRET # AZURE_SIGNING_ENDPOINT e.g. https://wus2.codesigning.azure.net/ # AZURE_SIGNING_ACCOUNT mist-code-signing # AZURE_SIGNING_CERT_PROFILE Mist-Windows-Signing # GOOGLE_OAUTH_CLIENT_ID shipped in production .env (Google OAuth) # GOOGLE_OAUTH_CLIENT_SECRET shipped in production .env (Google OAuth) # v1.0.29 cloud-proxied the OAuth flow itself, # but the bundled google_workspace_mcp still # requires CLIENT_SECRET at startup. v1.0.30 # plans to remove this dependency. on: push: tags: - 'v*' workflow_dispatch: inputs: publish: description: 'Publish to GitHub Releases (otherwise artifact only)' required: true default: 'false' type: choice options: - 'false' - 'true' permissions: contents: write jobs: build-windows: runs-on: windows-latest timeout-minutes: 60 env: AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SIGNING_ENDPOINT: ${{ secrets.AZURE_SIGNING_ENDPOINT }} AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }} AZURE_SIGNING_CERT_PROFILE: ${{ secrets.AZURE_SIGNING_CERT_PROFILE }} PUBLISH_INPUT: ${{ github.event.inputs.publish }} steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: # Exact pin (not '20'): the build bundles Node v20.18.1 as the runtime # for 9router + MCP servers (see build-app-win.ps1 step 3b), so the # toolchain that packages the app must match the runtime that ships. node-version: '20.18.1' - name: Setup Python (for building bundled python-env) uses: actions/setup-python@v5 with: python-version: '3.13' # The signing hook calls `signtool.exe` directly. signtool ships in the # Windows 10 SDK, preinstalled on windows-latest runners — we just need # the dlib for Azure Trusted Signing, pulled via NuGet. - name: Install Microsoft.Trusted.Signing.Client (dlib for signtool) shell: pwsh run: | $ErrorActionPreference = 'Stop' $dlibDir = Join-Path $env:GITHUB_WORKSPACE 'trusted-signing-client' New-Item -ItemType Directory -Force -Path $dlibDir | Out-Null nuget install Microsoft.Trusted.Signing.Client -Version 1.0.60 -OutputDirectory $dlibDir -ExcludeVersion $dlib = Join-Path $dlibDir 'Microsoft.Trusted.Signing.Client\bin\x64\Azure.CodeSigning.Dlib.dll' if (-not (Test-Path $dlib)) { Get-ChildItem -Path $dlibDir -Recurse -Filter 'Azure.CodeSigning.Dlib.dll' | ForEach-Object { Write-Host "Found: $($_.FullName)" } throw "Azure.CodeSigning.Dlib.dll not found after NuGet install" } "AZURE_SIGNING_DLIB=$dlib" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 Write-Host "AZURE_SIGNING_DLIB=$dlib" - name: Locate signtool.exe on the runner shell: pwsh run: | $ErrorActionPreference = 'Stop' $candidates = Get-ChildItem -Path 'C:\Program Files (x86)\Windows Kits\10\bin' -Recurse -Filter 'signtool.exe' -ErrorAction SilentlyContinue ` | Where-Object { $_.FullName -match '\\x64\\signtool\.exe$' } ` | Sort-Object FullName -Descending if (-not $candidates) { throw "signtool.exe not found on runner" } $signtool = $candidates[0].FullName "SIGNTOOL_PATH=$signtool" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 Write-Host "SIGNTOOL_PATH=$signtool" - name: Build app shell: pwsh env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GOOGLE_OAUTH_CLIENT_ID: ${{ secrets.GOOGLE_OAUTH_CLIENT_ID }} GOOGLE_OAUTH_CLIENT_SECRET: ${{ secrets.GOOGLE_OAUTH_CLIENT_SECRET }} run: | $ErrorActionPreference = 'Stop' $shouldPublish = ($env:GITHUB_EVENT_NAME -eq 'push') -or ` ($env:GITHUB_EVENT_NAME -eq 'workflow_dispatch' -and $env:PUBLISH_INPUT -eq 'true') # electron-builder auto-detects prerelease from semver suffix in electron/package.json, # but EP_PRE_RELEASE forces the GitHub Releases publisher to mark it Pre-release even # when the runner's environment differs from local. Set it whenever the version has a "-" suffix. $version = (Get-Content electron/package.json | ConvertFrom-Json).version if ($version -match '-') { $env:EP_PRE_RELEASE = 'true' Write-Host "Version $version is EXPERIMENTAL; setting EP_PRE_RELEASE=true" } else { Write-Host "Version $version is STABLE" } if ($shouldPublish) { Write-Host "Build mode: PUBLISH" pwsh -NoProfile -File scripts\build-app-win.ps1 -Publish } else { Write-Host "Build mode: SIGN (artifact only)" pwsh -NoProfile -File scripts\build-app-win.ps1 -Sign } if ($LASTEXITCODE -ne 0) { throw "build-app-win.ps1 failed ($LASTEXITCODE)" } # SmartScreen gate: after electron-builder + the Azure sign hook run, prove # the bits we are about to ship are ACTUALLY Authenticode-Valid. An unsigned # installer trips SmartScreen on every user's first launch, so a release that # silently didn't sign (missing secrets, hook skip) must fail here, not ship. # verify-signature.js --require-signed exits non-zero unless Status == Valid. - name: Verify the shipped artifact is signed shell: pwsh run: | node scripts/ci/verify-signature.js --require-signed --target electron/dist/win-unpacked/OpenSwarm.exe if ($LASTEXITCODE -ne 0) { throw "inner OpenSwarm.exe is not validly signed" } node scripts/ci/verify-signature.js --require-signed --target electron/dist/OpenSwarm-Setup-x64.exe if ($LASTEXITCODE -ne 0) { throw "OpenSwarm-Setup-x64.exe (installer) is not validly signed" } - name: Upload artifact (non-publish runs) if: github.event_name == 'workflow_dispatch' && github.event.inputs.publish != 'true' uses: actions/upload-artifact@v4 with: name: openswarm-windows-x64 path: | electron/dist/*.exe electron/dist/latest.yml if-no-files-found: error retention-days: 14