Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ details there and keep this file focused on rules for editing `.github/`.
qualification job promotes it. Incus and registry publishers are callable
gates that complete before promotion and are covered by aggregate
reconciliation.
- CI runs on Linux and macOS only; do not add Windows runner lanes or manual
Windows CI inputs.
- Preserve hosted Windows native workspace and installer qualification lanes.
The manual `run_windows` input selects only advisory desktop checks.
- External actions and reusable workflows are pinned to full commit SHAs.
- Fleet contract callers must pass the same exact workflows commit as
`implementation-ref`.
Expand Down Expand Up @@ -101,7 +101,7 @@ Labby through the pinned fleet policy and repository contract. Keep that opt-in
visible when adding ARM64 jobs or artifacts; QEMU and cross-platform emulation
still require a deliberate implementation and verification plan.
The supported release binary artifacts are Linux x86_64, Linux arm64, and macOS arm64.
Windows is neither a CI runner nor a release target.
Windows has native CI qualification lanes but is not a release target.
Keep each release target native to its GitHub-hosted runner; do not add
emulation, cross-platform image matrices, or QEMU setup.

Expand Down
276 changes: 275 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ name: CI

on:
workflow_dispatch:
inputs:
run_windows:
description: Run advisory Windows desktop checks (required native checks follow changed paths)
required: false
type: boolean
default: false
schedule:
- cron: '23 9 * * 1'
push:
Expand Down Expand Up @@ -314,7 +320,7 @@ jobs:
- name: Test CI policy helpers
run: |
python3 -m pip install --disable-pip-version-check PyYAML==6.0.2
python3 -m unittest scripts/ci/test_check_rust_coverage.py scripts/ci/test_ci_platform_policy.py scripts/ci/test_protected_doc_guard.py scripts/ci/test_release_hardening.py scripts/ci/test_release_observer.py scripts/ci/test_multi_user_migration_rehearsal.py
python3 -m unittest scripts/ci/test_check_rust_coverage.py scripts/ci/test_ci_platform_policy.py scripts/ci/test_windows_ci_policy.py scripts/ci/test_protected_doc_guard.py scripts/ci/test_release_hardening.py scripts/ci/test_release_observer.py scripts/ci/test_multi_user_migration_rehearsal.py
python3 -m unittest scripts/ci/test_default_web_build.py

lifecycle-static-analysis:
Expand All @@ -337,6 +343,36 @@ jobs:
package-manager-cache: false
- name: Analyze every shipped shell lifecycle script
run: scripts/ci/check-lifecycle-scripts.sh
- name: Install pinned PowerShell validation modules
shell: pwsh
run: |
Install-Module PSScriptAnalyzer -RequiredVersion 1.24.0 -Scope CurrentUser -Force
Install-Module Pester -RequiredVersion 5.7.1 -Scope CurrentUser -Force
- name: Analyze every shipped PowerShell lifecycle script
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$inventory = Get-Content scripts/ci/lifecycle-scripts.json -Raw | ConvertFrom-Json
$results = @($inventory.powershell | ForEach-Object { Invoke-ScriptAnalyzer -Path $_ -Severity Warning,Error })
$results | Format-Table -AutoSize
if ($results.Count -ne 0) { throw "PSScriptAnalyzer found $($results.Count) issue(s)" }

windows-installer:
name: Windows installer behavioral contracts
runs-on: windows-latest
timeout-minutes: 15
needs: changes
if: ${{ needs.changes.outputs.workflow == 'true' || needs.changes.outputs.release == 'true' }}
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
persist-credentials: false
- name: Install pinned Pester
shell: pwsh
run: Install-Module Pester -RequiredVersion 5.7.1 -Scope CurrentUser -Force
- name: Run installer contracts on Windows
shell: pwsh
run: Invoke-Pester scripts/tests/install.ps1.Tests.ps1 -CI -Output Detailed

macos-installer:
name: macOS installer behavioral contracts
Expand Down Expand Up @@ -1363,6 +1399,202 @@ jobs:
if-no-files-found: warn
retention-days: 14

test-windows:
name: Test (Windows)
runs-on: windows-latest
needs: [changes, frontend-assets]
# Native filesystem and process-containment coverage is required whenever
# changed-path routing enables Rust tests, including fork pull requests.
if: ${{ needs.changes.outputs.rust_test == 'true' }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
env:
CARGO_TARGET_DIR: ${{ github.workspace }}/target
LIBCLANG_PATH: C:\Program Files\LLVM\bin
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
persist-credentials: false
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
name: gateway-admin-out
path: apps/web/out
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: "22"
- name: Install Rust
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
with:
toolchain: "1.97.1"
- name: Neutralize rustc wrapper (native Windows)
shell: pwsh
run: |
"RUSTC_WRAPPER=" >> $env:GITHUB_ENV
"CARGO_BUILD_RUSTC_WRAPPER=" >> $env:GITHUB_ENV
- name: Restore Windows Cargo cache
uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae
with:
workspaces: . -> target
key: workspace-native-windows-v2
cache-on-failure: true
- name: Prepare hosted Windows build cache
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$targetDir = $env:CARGO_TARGET_DIR
$targetMaxBytes = 12GB

function Get-TreeSizeBytes($Path) {
if (-not (Test-Path $Path)) {
return 0
}
$sum = Get-ChildItem -LiteralPath $Path -Recurse -Force -File -ErrorAction SilentlyContinue |
Measure-Object -Property Length -Sum
if ($null -eq $sum.Sum) {
return 0
}
return [int64]$sum.Sum
}

function Remove-IfExists($Path) {
if (Test-Path $Path) {
Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue
}
}

function Enforce-HardCap($Path, $MaxBytes, $Label) {
$size = Get-TreeSizeBytes $Path
if ($size -gt $MaxBytes) {
Write-Warning "$Label is $([math]::Round($size / 1GB, 2)) GiB, above $([math]::Round($MaxBytes / 1GB, 2)) GiB; wiping it to enforce the hard cap."
Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $Path | Out-Null
$size = Get-TreeSizeBytes $Path
}
Write-Host "$Label ready at $Path ($([math]::Round($size / 1GB, 2)) GiB, hard cap $([math]::Round($MaxBytes / 1GB, 2)) GiB)."
}

function Remove-TargetBuildDirs($TargetDir, $Patterns) {
$buildDir = Join-Path $TargetDir "debug\build"
if (-not (Test-Path $buildDir)) {
return
}
foreach ($pattern in $Patterns) {
Get-ChildItem -LiteralPath $buildDir -Directory -Filter $pattern -ErrorAction SilentlyContinue |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
}
}

New-Item -ItemType Directory -Force -Path $targetDir | Out-Null

# Native bindgen outputs can be left half-written by interrupted runs,
# and Cargo may then reuse the stale build dir until the include! fails.
Remove-TargetBuildDirs $targetDir @("libsqlite3-sys-*")

$size = Get-TreeSizeBytes $targetDir
if ($size -gt $targetMaxBytes) {
Write-Warning "Hosted Cargo target dir is $([math]::Round($size / 1GB, 2)) GiB, above $([math]::Round($targetMaxBytes / 1GB, 2)) GiB; pruning volatile caches."
Remove-IfExists (Join-Path $targetDir "debug\incremental")
Remove-IfExists (Join-Path $targetDir "debug\.fingerprint")
Remove-IfExists (Join-Path $targetDir "debug\deps")
}

Enforce-HardCap $targetDir $targetMaxBytes "Hosted Cargo target dir"
- name: Install cargo-nextest
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$version = "0.9.138"
$current = $null
try {
$current = (& cargo nextest --version 2>$null)
} catch {
$current = $null
}
if ($current -like "*$version*") {
Write-Host "cargo-nextest already installed: $current"
exit 0
}

$target = "x86_64-pc-windows-msvc"
$base = "https://github.com/nextest-rs/nextest/releases/download/cargo-nextest-$version"
$archive = Join-Path $env:RUNNER_TEMP "cargo-nextest.tar.gz"
$shaFile = Join-Path $env:RUNNER_TEMP "cargo-nextest.sha256"
$extractDir = Join-Path $env:RUNNER_TEMP "cargo-nextest"
$cargoBin = Join-Path $env:USERPROFILE ".cargo\bin"

Invoke-WebRequest "$base/cargo-nextest-$version-$target.tar.gz" -OutFile $archive -TimeoutSec 300
Invoke-WebRequest "$base/cargo-nextest-$version-$target.sha256" -OutFile $shaFile -TimeoutSec 300

$expected = (Get-Content $shaFile -Raw).Split(" ")[0].Trim().ToLowerInvariant()
$actual = (Get-FileHash $archive -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actual -ne $expected) {
throw "cargo-nextest checksum mismatch: expected $expected, got $actual"
}

if (Test-Path $extractDir) {
Remove-Item -Recurse -Force $extractDir
}
New-Item -ItemType Directory -Force -Path $extractDir | Out-Null
tar -xzf $archive -C $extractDir
$exe = Get-ChildItem -Path $extractDir -Recurse -Filter cargo-nextest.exe | Select-Object -First 1
if (-not $exe) {
throw "cargo-nextest.exe not found in release archive"
}

New-Item -ItemType Directory -Force -Path $cargoBin | Out-Null
Copy-Item $exe.FullName (Join-Path $cargoBin "cargo-nextest.exe") -Force
$cargoBin | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
& (Join-Path $cargoBin "cargo-nextest.exe") --version
- name: Check Windows Job Object helper crate
shell: pwsh
run: cargo check -p labby-winjob --all-targets --locked
- name: Compile every Windows workspace target
shell: pwsh
run: cargo test --workspace --all-features --locked --no-run
- name: Run native Windows workspace tests (shard ${{ matrix.shard }}/4)
shell: pwsh
run: cargo nextest run --workspace --all-features --locked --profile ci --test-threads 4 --partition hash:${{ matrix.shard }}/4
- name: Run Windows Job Object reaping integration test
if: matrix.shard == 1
shell: pwsh
run: cargo nextest run -p labby --test windows_job_object_reaping --all-features --locked --profile ci --run-ignored ignored-only
- name: Enforce Windows build cache caps
if: always()
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$targetDir = $env:CARGO_TARGET_DIR
$targetMaxBytes = 12GB

function Get-TreeSizeBytes($Path) {
if (-not (Test-Path $Path)) {
return 0
}
$sum = Get-ChildItem -LiteralPath $Path -Recurse -Force -File -ErrorAction SilentlyContinue |
Measure-Object -Property Length -Sum
if ($null -eq $sum.Sum) {
return 0
}
return [int64]$sum.Sum
}

function Enforce-HardCap($Path, $MaxBytes, $Label) {
New-Item -ItemType Directory -Force -Path $Path | Out-Null
$size = Get-TreeSizeBytes $Path
if ($size -gt $MaxBytes) {
Write-Warning "$Label grew to $([math]::Round($size / 1GB, 2)) GiB, above $([math]::Round($MaxBytes / 1GB, 2)) GiB; wiping it to enforce the hard cap."
Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $Path | Out-Null
$size = Get-TreeSizeBytes $Path
}
Write-Host "$Label final size: $([math]::Round($size / 1GB, 2)) GiB (hard cap $([math]::Round($MaxBytes / 1GB, 2)) GiB)."
}

Enforce-HardCap $targetDir $targetMaxBytes "Persistent Cargo target dir"

release-contract:
name: Release metadata contract
runs-on: ubuntu-24.04
Expand Down Expand Up @@ -1463,6 +1695,34 @@ jobs:
--config apps/tauri/src-tauri/deny.toml \
check advisories

desktop-windows:
name: Labby desktop Tauri (Windows advisory)
runs-on: windows-latest
needs: changes
# Advisory, excluded from ci-gate, and only runs when explicitly selected
# from workflow_dispatch.
if: ${{ github.event_name == 'workflow_dispatch' && inputs.run_windows == true && needs.changes.outputs.desktop == 'true' }}
timeout-minutes: 60
# GitHub-hosted job: do not inherit an ambient compiler wrapper. Cache jobs
# configure kache explicitly through setup-rust-kache.
env:
RUSTC_WRAPPER: ""
CARGO_BUILD_RUSTC_WRAPPER: ""
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
with:
toolchain: "1.97.1"
- uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae
with:
workspaces: apps/tauri/src-tauri
key: labby-desktop-windows-v1
cache-on-failure: true
- run: cargo test --manifest-path apps/tauri/src-tauri/Cargo.toml --locked
- run: cargo build --manifest-path apps/tauri/src-tauri/Cargo.toml --locked

verification-t0:
name: Verification T0
uses: ./.github/workflows/verification-t0.yml
Expand Down Expand Up @@ -1495,6 +1755,7 @@ jobs:
- actionlint
- lifecycle-static-analysis
- macos-installer
- windows-installer
- frontend-assets
- gateway-admin-browser
- browser-extension
Expand All @@ -1511,6 +1772,7 @@ jobs:
- verification
- clippy
- test-fork
- test-windows
- test
- codemode-runner-smoke
- feature-slices
Expand Down Expand Up @@ -1546,6 +1808,8 @@ jobs:
RUST_TEST_ROUTED: ${{ needs.changes.outputs.rust_test }}
WEB_ROUTED: ${{ needs.changes.outputs.web }}
TAILCAT_ROUTED: ${{ needs.changes.outputs.tailcat }}
WORKFLOW_ROUTED: ${{ needs.changes.outputs.workflow }}
RELEASE_ROUTED: ${{ needs.changes.outputs.release }}
run: |
set -euo pipefail
require_success_or_skipped() {
Expand Down Expand Up @@ -1574,6 +1838,11 @@ jobs:
require_success_or_skipped unraid-plugin-check "${{ needs.unraid-plugin-check.result }}"
require_success_or_skipped actionlint "${{ needs.actionlint.result }}"
require_success_or_skipped lifecycle-static-analysis "${{ needs.lifecycle-static-analysis.result }}"
if [[ "$WORKFLOW_ROUTED" == true || "$RELEASE_ROUTED" == true ]]; then
require_success windows-installer "${{ needs.windows-installer.result }}"
else
require_success_or_skipped windows-installer "${{ needs.windows-installer.result }}"
fi
require_success_or_skipped macos-installer "${{ needs.macos-installer.result }}"
require_success_or_skipped frontend-assets "${{ needs.frontend-assets.result }}"
if [[ "$WEB_ROUTED" == true ]]; then
Expand All @@ -1598,6 +1867,11 @@ jobs:
require_success_or_skipped rustdoc "${{ needs.rustdoc.result }}"
require_success_or_skipped verification "${{ needs.verification.result }}"
require_success_or_skipped clippy "${{ needs.clippy.result }}"
if [[ "$RUST_TEST_ROUTED" == true ]]; then
require_success test-windows "${{ needs.test-windows.result }}"
else
require_success_or_skipped test-windows "${{ needs.test-windows.result }}"
fi
if [[ "$RUST_TEST_ROUTED" == true && "$EVENT_NAME" == pull_request && "$HEAD_REPOSITORY" != "$BASE_REPOSITORY" ]]; then
require_success test-fork "${{ needs.test-fork.result }}"
else
Expand Down
Loading
Loading