# .github/workflows/release.yml # Build and package the Windows release of BoxPilot. # # GPUI's Windows backend (DirectX 11 + MSVC `windows` crate bindings) does not # support MinGW cross-compilation; Zed upstream rejects MinGW builds. Production # runs natively on `windows-latest` with MSVC. name: Build Windows Release on: push: branches: - main - master - dev # Version tags (v1.13.0) publish an immutable versioned release — the # winget manifest points at those assets, so they must never be # overwritten. Branch pushes keep feeding the rolling ci-latest build. tags: - "v*" pull_request: branches: - main - master workflow_dispatch: inputs: singbox_version: description: "sing-box version to bundle, without leading v (e.g. 1.11.0). Blank = use repo variable SINGBOX_VERSION." required: false default: "" # sing-box version bundled into the MSI. Pinned explicitly (no implicit # "latest") so builds are reproducible and never silently grab the wrong # version. Set the repo/org Actions variable SINGBOX_VERSION, or override # per-run via the workflow_dispatch input above. The download step fails fast # if neither is set. Optionally set SINGBOX_SHA256 to the .zip asset digest # shown on the sing-box release page to pin the archive contents. env: SINGBOX_VERSION: ${{ inputs.singbox_version || vars.SINGBOX_VERSION }} # Needed so the built-in GITHUB_TOKEN can create/update the rolling CI release # (and its tag). Release assets do NOT count against the Actions artifact # storage quota, which is why we publish there instead of upload-artifact. permissions: contents: write jobs: release: runs-on: windows-latest steps: - name: Checkout repository uses: actions/checkout@v4 # On version-tag builds, fail fast if the tag doesn't match the crate # version — the MSI's ProductVersion (and the winget manifest that will # point at it) comes from Cargo.toml, so a mismatched tag would publish # a release whose assets claim a different version than the tag says. # The tag name is read via the runner-provided env var, never # interpolated into the script. - name: Verify tag matches Cargo.toml version if: startsWith(github.ref, 'refs/tags/v') run: | $tag = "$env:GITHUB_REF_NAME".TrimStart('v') $m = Select-String -Path Cargo.toml -Pattern '^version\s*=\s*"([^"]+)"' | Select-Object -First 1 $cargo = $m.Matches[0].Groups[1].Value if ($tag -ne $cargo) { Write-Error "Tag 'v$tag' does not match Cargo.toml version '$cargo'. Bump Cargo.toml (and Cargo.lock) before tagging." exit 1 } Write-Host "Tag v$tag matches Cargo.toml version $cargo" - name: Install Rust toolchain (MSVC) uses: dtolnay/rust-toolchain@stable with: toolchain: stable targets: x86_64-pc-windows-msvc - name: Cache cargo registry and target uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-cargo-msvc-${{ hashFiles('**/Cargo.lock', '**/Cargo.toml') }} - name: Test run: cargo test --target x86_64-pc-windows-msvc - name: Build run: cargo build --release --target x86_64-pc-windows-msvc - name: Download sing-box from official GitHub release run: | # Read the version from the env var (set at workflow level), never # interpolated into the script — avoids workflow injection. $ver = "$env:SINGBOX_VERSION".Trim().TrimStart('v') if ([string]::IsNullOrWhiteSpace($ver)) { Write-Error "SINGBOX_VERSION is not set. Set the repo variable SINGBOX_VERSION (e.g. 1.11.0) or pass 'singbox_version' via Run workflow." exit 1 } # Validate the format up front: digits + dots, optional pre-release # suffix. Rejects anything that isn't a plausible sing-box tag. if ($ver -notmatch '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$') { Write-Error "SINGBOX_VERSION '$ver' is not a valid version (expected e.g. 1.11.0)." exit 1 } $asset = "sing-box-$ver-windows-amd64" $url = "https://github.com/SagerNet/sing-box/releases/download/v$ver/$asset.zip" Write-Host "Downloading sing-box $ver from $url" Invoke-WebRequest -Uri $url -OutFile "sing-box.zip" # Verify the archive as downloaded, before anything touches it. The # expected hash is the asset digest GitHub shows on the sing-box release # page — copy it into the SINGBOX_SHA256 variable when bumping # SINGBOX_VERSION (it hashes the .zip, not the inner exe). - name: Verify sing-box archive integrity if: vars.SINGBOX_SHA256 != '' env: SINGBOX_SHA256: ${{ vars.SINGBOX_SHA256 }} run: | $expected = "$env:SINGBOX_SHA256".Trim() -replace '^sha256:', '' $hash = (Get-FileHash -Algorithm SHA256 "sing-box.zip").Hash if ($hash -ne $expected) { Write-Error "sing-box.zip SHA256 mismatch! Expected: $expected, Got: $hash" exit 1 } Write-Host "sing-box.zip SHA256 verified: $hash" - name: Extract and stage sing-box run: | New-Item -ItemType Directory -Force -Path "release" Expand-Archive -Path "sing-box.zip" -DestinationPath "singbox-extract" -Force # Robust against future folder-layout changes: just find the exe. $exe = Get-ChildItem -Path "singbox-extract" -Recurse -Filter "sing-box.exe" | Select-Object -First 1 if ($null -eq $exe) { Write-Error "sing-box.exe not found inside the downloaded archive." exit 1 } Copy-Item $exe.FullName -Destination "release/sing-box.exe" Write-Host "Bundled sing-box from $($exe.FullName)" - name: Install WiX Toolset run: choco install wixtoolset -y - name: Add WiX to PATH run: echo "C:\Program Files (x86)\WiX Toolset v3.14\bin" >> $env:GITHUB_PATH - name: Install cargo-wix run: cargo install cargo-wix@0.3.9 - name: Stage sing-box for MSI build run: | Copy-Item "release/sing-box.exe" -Destination "target/x86_64-pc-windows-msvc/release/sing-box.exe" - name: Build MSI installer run: cargo wix --no-build --nocapture --target x86_64-pc-windows-msvc -o release/ # Publish to a single rolling pre-release ("ci-latest") instead of # actions/upload-artifact — release assets don't count against the # Actions artifact storage quota, and each push overwrites the assets so # there's always one current build to download. Skipped on pull_request # (PRs only build/test; fork PRs lack write access anyway). Both exes # are bundled inside the MSI, so the MSI is the only attached asset. - name: Publish to rolling CI release if: github.event_name != 'pull_request' && !startsWith(github.ref, 'refs/tags/') # Third-party action holding contents:write — pinned to a full commit # SHA (not the mutable @v2 tag) to prevent a moved/compromised tag from # running with our release token. Bump deliberately (or via Dependabot). uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 with: tag_name: ci-latest name: CI build (latest) prerelease: true body: | Rolling CI build from `${{ github.ref_name }}` @ `${{ github.sha }}`. sing-box `${{ env.SINGBOX_VERSION }}` bundled in the MSI. files: release/*.msi # Version tags publish a real (non-prerelease) release with immutable # assets. The winget manifest records this URL + the MSI's SHA256, so # never re-push a version tag or replace its assets — publish a new # version instead. - name: Publish versioned release if: startsWith(github.ref, 'refs/tags/v') uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 with: name: BoxPilot ${{ github.ref_name }} body: | BoxPilot ${{ github.ref_name }} — sing-box `${{ env.SINGBOX_VERSION }}` bundled in the MSI. Install via winget: `winget install Glide01.BoxPilot` BoxPilot is MIT licensed. The bundled `sing-box.exe` is the official [SagerNet/sing-box](https://github.com/SagerNet/sing-box) release binary, distributed under its own GPL-3.0-or-later license. files: release/*.msi