Skip to content

Commit 366e9a6

Browse files
Avery FeltsAvery Felts
authored andcommitted
feat: add first-class Windows release support
1 parent 1bac136 commit 366e9a6

15 files changed

Lines changed: 783 additions & 127 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ jobs:
3131
- target: aarch64-unknown-linux-gnu
3232
os: ubuntu-22.04
3333
artifact: graphiq-aarch64-unknown-linux-gnu
34+
- target: x86_64-pc-windows-msvc
35+
os: windows-2022
36+
artifact: graphiq-x86_64-pc-windows-msvc
3437

3538
runs-on: ${{ matrix.os }}
3639

@@ -61,23 +64,44 @@ jobs:
6164
- name: Build release
6265
run: cargo build --release --target ${{ matrix.target }} --features gpu
6366

64-
- name: Package
67+
- name: Package (Unix)
68+
if: runner.os != 'Windows'
6569
run: |
70+
set -euo pipefail
6671
mkdir -p dist
6772
for bin in graphiq graphiq-mcp graphiq-bench; do
68-
if [ -f "target/${{ matrix.target }}/release/$bin" ]; then
69-
cp "target/${{ matrix.target }}/release/$bin" dist/
70-
fi
73+
test -f "target/${{ matrix.target }}/release/$bin"
74+
cp "target/${{ matrix.target }}/release/$bin" dist/
7175
done
7276
cd dist && tar czf "${{ matrix.artifact }}.tar.gz" -- *
7377
shasum -a 256 "${{ matrix.artifact }}.tar.gz" > "${{ matrix.artifact }}.sha256"
7478
79+
- name: Package (Windows)
80+
if: runner.os == 'Windows'
81+
shell: pwsh
82+
run: |
83+
$ErrorActionPreference = 'Stop'
84+
$dist = Join-Path $PWD 'dist'
85+
New-Item -ItemType Directory -Path $dist -Force | Out-Null
86+
$binaries = @('graphiq.exe', 'graphiq-mcp.exe', 'graphiq-bench.exe')
87+
foreach ($binary in $binaries) {
88+
$source = Join-Path $PWD "target\${{ matrix.target }}\release\$binary"
89+
if (-not (Test-Path -LiteralPath $source -PathType Leaf)) {
90+
throw "missing release binary: $source"
91+
}
92+
Copy-Item -LiteralPath $source -Destination (Join-Path $dist $binary)
93+
}
94+
$archiveName = '${{ matrix.artifact }}.zip'
95+
$archive = Join-Path $dist $archiveName
96+
Compress-Archive -Path (Join-Path $dist 'graphiq.exe'), (Join-Path $dist 'graphiq-mcp.exe'), (Join-Path $dist 'graphiq-bench.exe') -DestinationPath $archive -Force
97+
$hash = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant()
98+
"$hash $archiveName" | Set-Content -LiteralPath (Join-Path $dist '${{ matrix.artifact }}.sha256')
99+
75100
- uses: actions/upload-artifact@v4
76101
with:
77102
name: ${{ matrix.artifact }}
78-
path: |
79-
dist/${{ matrix.artifact }}.tar.gz
80-
dist/${{ matrix.artifact }}.sha256
103+
path: dist/*
104+
if-no-files-found: error
81105

82106
release:
83107
needs: [build]
@@ -94,4 +118,6 @@ jobs:
94118
with:
95119
files: |
96120
artifacts/**/*.tar.gz
121+
artifacts/**/*.zip
122+
artifacts/**/*.sha256
97123
generate_release_notes: true
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
name: Windows smoke
2+
3+
# Validates the native Windows build, Windows path handling, setup/update
4+
# integration points, and the MCP stdio protocol. GPU availability is not a
5+
# property of the hosted runner, so the deterministic smoke path forces the
6+
# documented CPU fallback while the release build still includes the gpu feature.
7+
8+
on:
9+
push:
10+
branches: [main]
11+
pull_request:
12+
branches: [main]
13+
14+
permissions:
15+
contents: read
16+
17+
jobs:
18+
smoke:
19+
runs-on: windows-2022
20+
timeout-minutes: 30
21+
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- name: Install Rust
26+
uses: dtolnay/rust-toolchain@stable
27+
28+
- name: Build release (gpu feature)
29+
run: cargo build --workspace --release --features gpu
30+
31+
- name: Run workspace tests
32+
run: cargo test --workspace --features gpu
33+
34+
- name: Run Windows CLI and setup smoke
35+
shell: pwsh
36+
run: |
37+
$ErrorActionPreference = 'Stop'
38+
$root = (Get-Location).Path
39+
$graphiq = Join-Path $root 'target\release\graphiq.exe'
40+
$mcp = Join-Path $root 'target\release\graphiq-mcp.exe'
41+
$env:GRAPHIQ_DISABLE_GPU = '1'
42+
43+
$version = & $graphiq --version
44+
if ($LASTEXITCODE -ne 0 -or $version -notmatch '^graphiq ') {
45+
throw "graphiq --version failed: $version"
46+
}
47+
48+
$fixture = Join-Path $env:RUNNER_TEMP 'graphiq Windows fixture with spaces'
49+
Remove-Item -LiteralPath $fixture -Recurse -Force -ErrorAction SilentlyContinue
50+
New-Item -ItemType Directory -Path (Join-Path $fixture 'src') -Force | Out-Null
51+
@'
52+
pub struct RateLimiter { max: u32 }
53+
impl RateLimiter {
54+
pub fn check_rate(&self) -> bool { true }
55+
pub fn reset(&self) {}
56+
}
57+
pub fn handle_request(rl: &RateLimiter) -> bool { rl.check_rate() }
58+
'@ | Set-Content -LiteralPath (Join-Path $fixture 'src\lib.rs') -Encoding utf8
59+
60+
& git -C $fixture init --quiet
61+
& git -C $fixture config user.email ci@example.invalid
62+
& git -C $fixture config user.name 'GraphIQ CI'
63+
& git -C $fixture add --all
64+
& git -C $fixture commit --quiet -m init
65+
if ($LASTEXITCODE -ne 0) { throw 'failed to create the Windows fixture repository' }
66+
67+
$db = Join-Path $fixture '.graphiq\graphiq.db'
68+
& $graphiq index $fixture --db $db
69+
if ($LASTEXITCODE -ne 0) { throw 'graphiq index failed on Windows' }
70+
71+
$search = (& $graphiq search 'rate limit' --db $db --top 3 | Out-String)
72+
if ($LASTEXITCODE -ne 0 -or $search -notmatch 'RateLimiter') {
73+
throw "Windows search did not find RateLimiter: $search"
74+
}
75+
76+
$status = (& $graphiq status --db $db | Out-String)
77+
if ($LASTEXITCODE -ne 0 -or $status -notmatch 'GPU: unavailable \(disabled by GRAPHIQ_DISABLE_GPU\)') {
78+
throw "Windows status did not report the CPU fallback: $status"
79+
}
80+
81+
# This exercises sibling graphiq-mcp.exe discovery, Windows command
82+
# lookup, and the no-Unix-package-manager setup path.
83+
& $graphiq setup --project $fixture --skip-index --persistent --harness claude-code
84+
if ($LASTEXITCODE -ne 0) { throw 'graphiq setup failed on Windows' }
85+
$configPath = Join-Path $fixture '.claude\.mcp.json'
86+
$config = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json
87+
if ($null -eq $config.mcpServers.graphiq) { throw 'setup did not write the Claude Code MCP entry' }
88+
if ($config.mcpServers.graphiq.args[0] -ne $fixture) { throw 'setup wrote the wrong Windows project path' }
89+
90+
$sync = (& $graphiq sync --project $fixture --harness claude-code | Out-String)
91+
if ($LASTEXITCODE -ne 0 -or $sync -notmatch 'connected:') {
92+
throw "graphiq sync did not verify the Windows MCP entry: $sync"
93+
}
94+
95+
function Read-McpJsonLine([System.Diagnostics.Process]$Process) {
96+
$read = $Process.StandardOutput.ReadLineAsync()
97+
if (-not $read.Wait(10000)) { throw 'timed out waiting for MCP response' }
98+
if ($null -eq $read.Result) { throw 'MCP exited before returning a response' }
99+
return ($read.Result | ConvertFrom-Json)
100+
}
101+
102+
$psi = New-Object System.Diagnostics.ProcessStartInfo
103+
$psi.FileName = $mcp
104+
$quotedFixture = '"' + $fixture.Replace('"', '\"') + '"'
105+
$quotedDb = '"' + $db.Replace('"', '\"') + '"'
106+
$psi.Arguments = "$quotedFixture --db $quotedDb"
107+
$psi.WorkingDirectory = $root
108+
$psi.UseShellExecute = $false
109+
$psi.CreateNoWindow = $true
110+
$psi.RedirectStandardInput = $true
111+
$psi.RedirectStandardOutput = $true
112+
$psi.RedirectStandardError = $true
113+
$mcpProcess = New-Object System.Diagnostics.Process
114+
$mcpProcess.StartInfo = $psi
115+
[void]$mcpProcess.Start()
116+
try {
117+
$mcpProcess.StandardInput.WriteLine('{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci","version":"1"}}}')
118+
$mcpProcess.StandardInput.WriteLine('{"jsonrpc":"2.0","method":"notifications/initialized"}')
119+
$mcpProcess.StandardInput.WriteLine('{"jsonrpc":"2.0","id":2,"method":"ping"}')
120+
$mcpProcess.StandardInput.WriteLine('{"jsonrpc":"2.0","id":3,"method":"shutdown"}')
121+
122+
$initialize = Read-McpJsonLine $mcpProcess
123+
$ping = Read-McpJsonLine $mcpProcess
124+
$shutdown = Read-McpJsonLine $mcpProcess
125+
if ($initialize.id -ne 1 -or $initialize.result.serverInfo.name -ne 'graphiq') { throw 'invalid MCP initialize response on Windows' }
126+
if ($ping.id -ne 2 -or $null -eq $ping.result) { throw 'invalid MCP ping response on Windows' }
127+
if ($shutdown.id -ne 3) { throw 'invalid MCP shutdown response on Windows' }
128+
} finally {
129+
$mcpProcess.StandardInput.Close()
130+
if (-not $mcpProcess.WaitForExit(10000)) {
131+
$mcpProcess.Kill()
132+
throw 'MCP process did not exit after stdin closed'
133+
}
134+
$mcpProcess.Dispose()
135+
}
136+
137+
- name: Test PowerShell installer with the local release archive
138+
shell: pwsh
139+
run: |
140+
$ErrorActionPreference = 'Stop'
141+
$root = (Get-Location).Path
142+
$stage = Join-Path $env:RUNNER_TEMP 'graphiq Windows package'
143+
$installDir = Join-Path $env:RUNNER_TEMP 'graphiq Windows install'
144+
Remove-Item -LiteralPath $stage -Recurse -Force -ErrorAction SilentlyContinue
145+
Remove-Item -LiteralPath $installDir -Recurse -Force -ErrorAction SilentlyContinue
146+
New-Item -ItemType Directory -Path $stage -Force | Out-Null
147+
foreach ($binary in @('graphiq.exe', 'graphiq-mcp.exe', 'graphiq-bench.exe')) {
148+
Copy-Item -LiteralPath (Join-Path $root "target\release\$binary") -Destination (Join-Path $stage $binary)
149+
}
150+
$archiveName = 'graphiq-x86_64-pc-windows-msvc.zip'
151+
$archive = Join-Path $stage $archiveName
152+
Compress-Archive -Path (Join-Path $stage 'graphiq.exe'), (Join-Path $stage 'graphiq-mcp.exe'), (Join-Path $stage 'graphiq-bench.exe') -DestinationPath $archive -Force
153+
$hash = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant()
154+
"$hash $archiveName" | Set-Content -LiteralPath ([IO.Path]::ChangeExtension($archive, 'sha256'))
155+
156+
& (Join-Path $root 'install.ps1') -ArchivePath $archive -InstallDir $installDir -NoPathUpdate -Version 'ci'
157+
if (-not (Test-Path -LiteralPath (Join-Path $installDir 'graphiq.exe'))) {
158+
throw 'PowerShell installer did not install graphiq.exe'
159+
}
160+
$installedVersion = & (Join-Path $installDir 'graphiq.exe') --version
161+
if ($LASTEXITCODE -ne 0 -or $installedVersion -notmatch '^graphiq ') {
162+
throw "installed Windows binary failed: $installedVersion"
163+
}
164+
165+
& (Join-Path $root 'install.ps1') -Uninstall -InstallDir $installDir -NoPathUpdate
166+
if (Test-Path -LiteralPath (Join-Path $installDir 'graphiq.exe')) {
167+
throw 'PowerShell installer did not uninstall graphiq.exe'
168+
}

‎Cargo.lock‎

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎README.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge" alt="MIT License"></a>
1414
<a href="https://github.com/aaf2tbz/graphiq/actions/workflows/linux-smoke.yml"><img src="https://img.shields.io/github/actions/workflow/status/aaf2tbz/graphiq/linux-smoke.yml?branch=main&label=linux&style=for-the-badge" alt="Linux smoke"></a>
1515
<a href="https://github.com/aaf2tbz/graphiq/actions/workflows/macos-smoke.yml"><img src="https://img.shields.io/github/actions/workflow/status/aaf2tbz/graphiq/macos-smoke.yml?branch=main&label=macOS&style=for-the-badge" alt="macOS smoke"></a>
16+
<a href="https://github.com/aaf2tbz/graphiq/actions/workflows/windows-smoke.yml"><img src="https://img.shields.io/github/actions/workflow/status/aaf2tbz/graphiq/windows-smoke.yml?branch=main&label=Windows&style=for-the-badge" alt="Windows smoke"></a>
1617
</p>
1718

1819

@@ -35,6 +36,18 @@ Or install a prebuilt binary on macOS or Linux:
3536
curl -fsSL https://raw.githubusercontent.com/aaf2tbz/graphiq/main/install.sh | bash
3637
```
3738

39+
On Windows, run the PowerShell installer:
40+
41+
```powershell
42+
$installer = Join-Path $env:TEMP "graphiq-install.ps1"
43+
Invoke-WebRequest https://raw.githubusercontent.com/aaf2tbz/graphiq/main/install.ps1 -OutFile $installer
44+
powershell -ExecutionPolicy Bypass -File $installer
45+
Remove-Item $installer
46+
```
47+
48+
The Windows installer places `graphiq.exe` and `graphiq-mcp.exe` in
49+
`%LOCALAPPDATA%\GraphIQ\bin` and adds that directory to the user PATH.
50+
3851
### Index and search
3952

4053
```bash

‎crates/graphiq-bench/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "graphiq-bench"
3-
version = "4.4.0"
3+
version = "4.5.0"
44
edition = "2021"
55

66
[[bin]]

‎crates/graphiq-cli/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "graphiq-cli"
3-
version = "4.4.0"
3+
version = "4.5.0"
44
edition = "2021"
55

66
[[bin]]

0 commit comments

Comments
 (0)