A fresh Windows install can run init: name the missing VC++ runtime #198
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: CI | |
| on: | |
| push: | |
| pull_request: | |
| jobs: | |
| test-offline: | |
| # The full suite - including init, seed-pack install, selftest, store, | |
| # search, shred, lock, unlock - runs with the runtime offline guard | |
| # active: any attempt to create a network socket aborts the process. | |
| # This is the executable proof of the zero-network claim. | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| python: ["3.11", "3.12"] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python }} | |
| - name: Install (network allowed for dependency download only) | |
| run: pip install -e .[dev] | |
| - name: Test suite with offline guard active | |
| env: | |
| COMPARTMENT_ASSERT_OFFLINE: "1" | |
| run: pytest tests/ -q --cov=compartment --cov-report=xml --cov-append | |
| - name: Dashboard tests (loopback socket; offline guard intentionally off) | |
| # `compartment dash` binds 127.0.0.1 by design and is never run under the | |
| # offline guard, so its tests run here with the guard off. | |
| run: pytest tests/test_dash.py -q --cov=compartment --cov-report=xml --cov-append | |
| - name: Coverage to Codecov | |
| # Never fail the build on a reporting service: the offline guard and the | |
| # test suite are the gate here, not the upload. | |
| uses: codecov/codecov-action@v5 | |
| continue-on-error: true | |
| with: | |
| files: ./coverage.xml | |
| flags: ${{ matrix.os }}-py${{ matrix.python }} | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| fail_ci_if_error: false | |
| linux-panel: | |
| # Linux has no notification area worth relying on, so it draws the same | |
| # panel as an ordinary window. "It opens and stays open" is the failure | |
| # unit tests cannot see, and this is a real Linux box, so it is checked | |
| # here on a virtual display. | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: The Tk package, which pip cannot install | |
| run: sudo apt-get update && sudo apt-get install -y python3-tk xvfb | |
| - name: Install | |
| run: pip install -e .[dev] | |
| - name: A vault to look at | |
| run: | | |
| compartment init --passphrase ci-only-not-a-secret --no-app | |
| compartment store --source "from chat" "Deploys go out Tuesday and Thursday mornings." | |
| compartment store --source "from chat" "Staging runs Postgres 16 behind pgbouncer." | |
| - name: The panel, without a display (what it would say) | |
| run: compartment panel --self-check | |
| - name: It opens and stays open | |
| run: | | |
| xvfb-run -a --server-args="-screen 0 1280x1024x24" \ | |
| compartment panel --show >/dev/null 2>&1 & | |
| PID=$! | |
| sleep 18 | |
| if ! kill -0 $PID 2>/dev/null; then | |
| echo "the panel exited on its own"; exit 1 | |
| fi | |
| echo "still running after 18s" | |
| kill $PID 2>/dev/null || true | |
| pkill -f "compartment.*panel" || true | |
| sleep 2 | |
| - name: A second copy stands down instead of opening a second window | |
| run: | | |
| # Detached from the step's stdout, or the runner waits on it. | |
| xvfb-run -a --server-args="-screen 0 1280x1024x24" \ | |
| compartment panel --show >/dev/null 2>&1 & | |
| sleep 12 | |
| set +e | |
| out=$(timeout 30 xvfb-run -a compartment panel --show 2>&1) | |
| rc=$? | |
| set -e | |
| echo "second copy rc=$rc said: $out" | |
| pkill -f "compartment.*panel" || true | |
| if [ "$rc" -ne 0 ]; then | |
| echo "the second copy did not exit; it opened a window of its own" | |
| exit 1 | |
| fi | |
| echo "$out" | grep -q "already open" || { | |
| echo "the second copy exited without saying why"; exit 1; } | |
| - name: It puts itself in the applications menu | |
| run: | | |
| compartment panel --login on | |
| test -f "$HOME/.local/share/applications/compartment.desktop" \ | |
| || { echo "no desktop entry"; exit 1; } | |
| cat "$HOME/.local/share/applications/compartment.desktop" | |
| grep -q "^Type=Application" "$HOME/.local/share/applications/compartment.desktop" | |
| grep -q "^Exec=" "$HOME/.local/share/applications/compartment.desktop" | |
| desktop-file-validate "$HOME/.local/share/applications/compartment.desktop" \ | |
| || echo "note: desktop-file-validate not installed or reported style warnings" | |
| compartment panel --login off | |
| test ! -f "$HOME/.local/share/applications/compartment.desktop" | |
| windows-tray: | |
| # The tray app has to actually start on Windows, and "it starts" is the | |
| # failure the unit tests cannot see: the macOS app once launched, ran, and | |
| # showed nothing at all. The runner has a desktop session, so the app can | |
| # really be drawn here - the screenshot is uploaded so a human can look at | |
| # the panel without owning a Windows machine. | |
| runs-on: windows-latest | |
| # Nothing here should take twenty minutes. Without a cap, one command | |
| # waiting on a passphrase prompt that no one can answer held a runner | |
| # for 2h40m: init crashed on a non-ASCII character, left the vault | |
| # locked, and the next command sat asking for input. | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install with the tray extra | |
| run: pip install -e .[tray] | |
| - name: A vault to look at | |
| # Five ordinary-looking memories, so the screenshot shows the panel | |
| # doing its job instead of an empty list. | |
| run: | | |
| compartment init --passphrase ci-only-not-a-secret | |
| compartment store --source "from chat" "Deploys go out Tuesday and Thursday mornings, never on a Friday." | |
| compartment store --source "from chat" "Approved (answered OK): send the release notes to the team now?" | |
| compartment store --source "from chat" "Staging runs Postgres 16 behind pgbouncer on port 6432." | |
| compartment store --source "from chat" "Prefers spaces over tabs everywhere except Go." | |
| compartment store --source "from chat" "This laptop has 16 GB, so keep parallel jobs at 4." | |
| python -c "from compartment import menubar as m; m.set_setting(m.default_vault(), 'capture_hook', True)" | |
| - name: The panel, without a window (what it would say) | |
| run: compartment tray --self-check | |
| - name: Changing the passphrase has to work here too | |
| # The apps grew a Change password button, and Windows is the side | |
| # with no one sitting in front of it. Prove the whole round trip: | |
| # the new passphrase opens the vault and the old one does not. | |
| shell: pwsh | |
| run: | | |
| "Locksmith-ci-only" | compartment rekey --new-passphrase-stdin | |
| if ($LASTEXITCODE -ne 0) { Write-Error "rekey failed"; exit 1 } | |
| compartment lock | |
| $old = "ci-only-not-a-secret" | compartment unlock --passphrase-stdin 2>&1 | |
| if ($LASTEXITCODE -eq 0) { Write-Error "the OLD passphrase still opens it"; exit 1 } | |
| Write-Host "old passphrase correctly rejected" | |
| "Locksmith-ci-only" | compartment unlock --passphrase-stdin | |
| if ($LASTEXITCODE -ne 0) { Write-Error "the NEW passphrase does not open it"; exit 1 } | |
| Write-Host "new passphrase accepted" | |
| - name: A bigger screen than the 1024x768 default | |
| shell: pwsh | |
| continue-on-error: true | |
| run: | | |
| Set-DisplayResolution -Width 1920 -Height 1080 -Force | |
| Start-Sleep -Seconds 3 | |
| - name: A second copy stands down instead of adding another icon | |
| # `compartment init` above already started the tray app, which is the | |
| # whole point of it. So a launch now must hand off and exit 0, and | |
| # the copy that was already there must still be running afterwards. | |
| shell: pwsh | |
| run: | | |
| $before = @(Get-Process compartment -ErrorAction SilentlyContinue).Count | |
| if ($before -lt 1) { Write-Error "init did not start the tray app"; exit 1 } | |
| compartment tray --show | |
| if ($LASTEXITCODE -ne 0) { Write-Error "the second copy failed instead of standing down"; exit 1 } | |
| $after = @(Get-Process compartment -ErrorAction SilentlyContinue).Count | |
| if ($after -ne $before) { Write-Error "second copy changed the count: $before -> $after"; exit 1 } | |
| Write-Host "one copy before, one copy after" | |
| Get-Process compartment -ErrorAction SilentlyContinue | Stop-Process -Force | |
| Start-Sleep -Seconds 2 | |
| - name: Start the tray app and prove it is still alive | |
| # At the DEFAULT scale, which is what nearly everyone runs. The | |
| # high-DPI path below must not be the only one exercised. | |
| shell: pwsh | |
| run: | | |
| $p = Start-Process -PassThru -FilePath compartment -ArgumentList 'tray','--show' | |
| Start-Sleep -Seconds 25 | |
| if ($p.HasExited) { | |
| Write-Error "the tray app exited on its own with $($p.ExitCode)" | |
| exit 1 | |
| } | |
| Write-Host "still running after 25s (pid $($p.Id))" | |
| Stop-Process -Id $p.Id -Force | |
| Start-Sleep -Seconds 2 | |
| - name: Start it again at 2x, the way a high-DPI laptop draws it | |
| # The runner's display is 96 DPI, so this is the only way to see the | |
| # scaled path render here - and it is what produces a screenshot | |
| # sharp enough to sit beside the Retina macOS one. | |
| shell: pwsh | |
| env: | |
| COMPARTMENT_UI_SCALE: "2" | |
| run: | | |
| $p = Start-Process -PassThru -FilePath compartment -ArgumentList 'tray','--show' | |
| Start-Sleep -Seconds 25 | |
| if ($p.HasExited) { | |
| Write-Error "the tray app exited at 2x scale with $($p.ExitCode)" | |
| exit 1 | |
| } | |
| Write-Host "still running at 2x after 25s (pid $($p.Id))" | |
| - name: Screenshot the panel, cropped by Windows itself | |
| shell: pwsh | |
| run: | | |
| Add-Type -AssemblyName System.Windows.Forms,System.Drawing | |
| Add-Type @" | |
| using System; | |
| using System.Runtime.InteropServices; | |
| public class NativeWin { | |
| [DllImport("user32.dll", CharSet=CharSet.Unicode)] | |
| public static extern IntPtr FindWindow(string cls, string name); | |
| [DllImport("user32.dll")] | |
| public static extern bool SetForegroundWindow(IntPtr h); | |
| [StructLayout(LayoutKind.Sequential)] | |
| public struct RECT { public int Left, Top, Right, Bottom; } | |
| [DllImport("dwmapi.dll")] | |
| public static extern int DwmGetWindowAttribute( | |
| IntPtr h, int attr, out RECT r, int size); | |
| } | |
| "@ | |
| # The whole desktop first, so this step always produces something | |
| # even if the precise capture below cannot find the window. | |
| $b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds | |
| Write-Host "desktop: $($b.Width)x$($b.Height)" | |
| $full = New-Object System.Drawing.Bitmap $b.Width, $b.Height | |
| $g2 = [System.Drawing.Graphics]::FromImage($full) | |
| $g2.CopyFromScreen($b.Location, [System.Drawing.Point]::Empty, $b.Size) | |
| $full.Save("$PWD\tray.png", [System.Drawing.Imaging.ImageFormat]::Png) | |
| # Ask the process for its own window rather than FindWindow: passing | |
| # $null for a string parameter through P/Invoke marshals as "" in | |
| # PowerShell, which then matches a window class literally named | |
| # empty and finds nothing. | |
| Write-Host "windows with a title:" | |
| Get-Process | Where-Object { $_.MainWindowHandle -ne 0 } | | |
| ForEach-Object { Write-Host " $($_.ProcessName): '$($_.MainWindowTitle)'" } | |
| $proc = Get-Process | | |
| Where-Object { $_.MainWindowHandle -ne 0 -and | |
| ($_.ProcessName -like "*compartment*" -or | |
| $_.MainWindowTitle -eq "Compartment") } | | |
| Select-Object -First 1 | |
| if (-not $proc) { | |
| Write-Warning "no Compartment window found; desktop shot only" | |
| exit 0 | |
| } | |
| $h = $proc.MainWindowHandle | |
| [void][NativeWin]::SetForegroundWindow($h) | |
| Start-Sleep -Milliseconds 900 | |
| $r = New-Object NativeWin+RECT | |
| # 9 = DWMWA_EXTENDED_FRAME_BOUNDS: the real frame, without the | |
| # invisible resize border a plain GetWindowRect includes. | |
| [void][NativeWin]::DwmGetWindowAttribute($h, 9, [ref]$r, 16) | |
| $w = $r.Right - $r.Left; $ht = $r.Bottom - $r.Top | |
| if ($w -lt 50 -or $ht -lt 50) { | |
| Write-Warning "implausible window rect ${w}x${ht}; desktop shot only" | |
| exit 0 | |
| } | |
| $bmp = New-Object System.Drawing.Bitmap $w, $ht | |
| $g = [System.Drawing.Graphics]::FromImage($bmp) | |
| $g.CopyFromScreen($r.Left, $r.Top, 0, 0, | |
| (New-Object System.Drawing.Size $w, $ht)) | |
| $bmp.Save("$PWD\compartment-windows-panel.png", | |
| [System.Drawing.Imaging.ImageFormat]::Png) | |
| Write-Host "panel: ${w}x${ht}" | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: windows-tray-screenshot | |
| path: | | |
| compartment-windows-panel.png | |
| tray.png |