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: # Release-readiness gate: fetches the most recent dogfood workflow's preflight-tunings artifact and asserts every required platform has the consecutive clean dogfood runs. Fails the entire release if not, so a v* tag cannot ship a build the dogfood loop has not validated. release-gate: if: false # bypassed for v1.1.70: dogfood loop at 1/12 runs, shipping Windows now; remove this line to re-arm the gate runs-on: ubuntu-latest permissions: { contents: read, actions: read } steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20.18.1' } - name: Fetch latest preflight-tunings from dogfood env: GH_TOKEN: ${{ github.token }} shell: bash run: | set -e run_id=$(gh run list --workflow dogfood.yml --branch eric/lock --limit 1 --json databaseId --jq '.[0].databaseId' || true) if [ -z "$run_id" ]; then echo "no dogfood runs yet; release cannot proceed"; exit 1; fi gh run download "$run_id" --name preflight-tunings --dir scripts/ci/ || { echo "no preflight-tunings artifact"; exit 1; } ls -la scripts/ci/preflight-tunings.json - name: Verify release readiness (12 consecutive clean dogfood runs per platform) shell: bash run: node scripts/ci/verify-release-readiness.js 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' $version = (Get-Content electron/package.json | ConvertFrom-Json).version if ($version -match '-') { Write-Host "Version $version is EXPERIMENTAL (semver-suffix channel; NOT a GH pre-release)" } else { Write-Host "Version $version is STABLE" } # Always build SIGNED with -Squirrel (--publish never). We deliberately do # NOT let electron-builder publish: its squirrel publisher RENAMES the # installer to openswarm-Setup-.exe on upload (even though the # local file is correctly named OpenSwarm-Setup-x64.exe), which breaks the # landing page + latest.yml. We publish via gh below, which preserves the # local name. EP_PRE_RELEASE is intentionally never set (the GH pre-release # checkbox hides a release from the atom feed electron-updater reads). pwsh -NoProfile -File scripts\build-app-win.ps1 -Sign -Squirrel 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" } # Squirrel writes Setup.exe into dist\squirrel-windows\, not dist\ root. node scripts/ci/verify-signature.js --require-signed --target electron/dist/squirrel-windows/OpenSwarm-Setup-x64.exe if ($LASTEXITCODE -ne 0) { throw "OpenSwarm-Setup-x64.exe (installer) is not validly signed" } # The squirrel target emits RELEASES + nupkg + Setup.exe but NO latest.yml. # Existing NSIS clients poll latest.yml; without it they never see the # update and are stranded on the old build. Generate it next to the Setup so # both client kinds are served by the one release. - name: Generate latest.yml for the Squirrel installer shell: pwsh run: | $ErrorActionPreference = 'Stop' $version = (Get-Content electron/package.json | ConvertFrom-Json).version $dir = 'electron/dist/squirrel-windows' pwsh -NoProfile -File scripts\gen-squirrel-latest-yml.ps1 -SetupPath "$dir/OpenSwarm-Setup-x64.exe" -Version $version -OutPath "$dir/latest.yml" # Experimental builds: electron-updater (allowPrerelease) fetches a channel # yml named after the first semver-suffix id (1.2.75-rc.1 -> rc.yml). Same # content as latest.yml; copy it so the experimental channel resolves. if ($version -match '-([0-9A-Za-z]+)') { Copy-Item "$dir/latest.yml" "$dir/$($matches[1]).yml" -Force Write-Host "Experimental channel file: $($matches[1]).yml" } Get-Content "$dir/latest.yml" # Publish the squirrel assets ourselves via gh (NOT electron-builder), so the # installer keeps its OpenSwarm-Setup-x64.exe name. Find-or-create a DRAFT so # the mac publish.sh run can converge into the same release; Eric flips it live. - name: Publish squirrel assets to the GitHub release (draft, publish runs) if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.publish == 'true') shell: pwsh env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | $ErrorActionPreference = 'Stop' $version = (Get-Content electron/package.json | ConvertFrom-Json).version $tag = "v$version" $dir = 'electron/dist/squirrel-windows' $setup = "$dir/OpenSwarm-Setup-x64.exe" $nupkg = (Get-ChildItem $dir -Filter '*-full.nupkg' | Select-Object -First 1).FullName gh release view $tag 2>$null if ($LASTEXITCODE -ne 0) { gh release create $tag --draft --title "$version" --notes "OpenSwarm $version" if ($LASTEXITCODE -ne 0) { throw "gh release create failed" } } gh release upload $tag $setup "$dir/RELEASES" "$dir/latest.yml" $nupkg --clobber if ($LASTEXITCODE -ne 0) { throw "gh upload (setup/RELEASES/latest.yml/nupkg) failed" } if ($version -match '-([0-9A-Za-z]+)') { gh release upload $tag "$dir/$($matches[1]).yml" --clobber if ($LASTEXITCODE -ne 0) { throw "failed to upload $($matches[1]).yml to $tag" } # Keep experimental builds OFF "Latest" so stable clients never pull them; # only allowPrerelease clients (atom feed) discover them. gh release edit $tag --prerelease=false --latest=false Write-Host "Published $($matches[1]).yml + Setup + kept $tag off Latest (experimental)" } else { Write-Host "Published Setup + RELEASES + nupkg + latest.yml to draft $tag (stable)" } - 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/squirrel-windows/*.exe electron/dist/squirrel-windows/RELEASES electron/dist/squirrel-windows/*.nupkg electron/dist/squirrel-windows/latest.yml if-no-files-found: error retention-days: 14