Skip to content

Keep runtime hydration repository neutral (#217) #527

Keep runtime hydration repository neutral (#217)

Keep runtime hydration repository neutral (#217) #527

Workflow file for this run

# Boatstack-owned control plane.
name: CI
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: boatstack-ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
flow-sdk:
name: flow-sdk
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
- uses: actions/setup-go@v7
with:
go-version-file: boatstack/go.mod
cache-dependency-path: boatstack/go.sum
- name: Build TypeScript frontends
run: npm ci && npm run build:flow-sdk
- name: Prove frontend canonical equivalence
working-directory: boatstack
env:
BOATSTACK_REQUIRE_FLOW_FRONTEND: '1'
run: go test ./controlprogram -run TestTypeScriptDSLAndRawIRHaveOneCanonicalFingerprint
component:
name: component-${{ matrix.name }}
strategy:
fail-fast: false
matrix:
include:
- name: kernel-mechanism
packages: ./kernel
- name: control-program-compiler
packages: ./controlprogram ./delivery ./core ./flow/softwaredelivery
- name: standard-flow
packages: ./flow/standard ./internal/softwaredelivery/protocol
- name: extension-conformance
packages: ./extension/... ./distribution
- name: surface-parity
packages: ./internal/softwaredelivery/surfaces ./sdk ./cmd/boatstack-helper
- name: plant-integration
packages: ./internal/softwaredelivery/plant ./internal/softwaredelivery/effects ./internal/softwaredelivery/engine
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: boatstack/go.mod
cache-dependency-path: boatstack/go.sum
- name: Test ${{ matrix.name }}
working-directory: boatstack
run: go test ${{ matrix.packages }}
# Unix runs the full suite serially (~1-2 min) and is the unsharded correctness
# reference. Windows is sharded in `test-windows` (see below).
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: boatstack/go.mod
cache-dependency-path: boatstack/go.sum
- name: Show toolchain
run: go version
working-directory: boatstack
- name: Test runtime with race detector
if: matrix.os == 'ubuntu-latest'
run: go test -race ./...
working-directory: boatstack
- name: Test runtime
if: matrix.os == 'macos-latest'
run: go test ./...
working-directory: boatstack
- name: Build helper
run: go build ./cmd/boatstack-helper
working-directory: boatstack
- name: Validate Bash installer
shell: bash
run: bash -n install.sh
race-critical:
name: race-critical
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: boatstack/go.mod
cache-dependency-path: boatstack/go.sum
- name: Test concurrency-bearing packages
working-directory: boatstack
run: go test -race ./kernel ./internal/softwaredelivery/effects
# Windows `go test` is dominated by per-process spawn latency (each test spawns
# several `git` processes; the suite is ~330 tests run serially), so the full
# suite takes ~15 min on Windows vs ~1 min on Unix. In-process t.Parallel() is
# unsafe (the package swaps ~14 global function-seams), so we shard across
# processes: N runners, each running a disjoint, balanced subset. Shard
# assignment is an LPT makespan-minimization controller — see
# .github/scripts/ci_shard.py. Target: slowest shard < 5 min.
test-windows:
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3, 4, 5]
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
# Windows `go test`/`go build` is dominated by Microsoft Defender scanning
# the many small files the Go toolchain emits during compile/link. Excluding
# the Go caches, the workspace, and go.exe is a major wall-clock lever;
# sharding on top of this is what gets the suite under 5 min.
- name: Exclude Go caches from Microsoft Defender (Windows)
shell: pwsh
run: |
$targets = @(
"$env:LOCALAPPDATA\go-build", # GOCACHE (build cache)
"$env:USERPROFILE\go", # GOPATH incl. pkg\mod (GOMODCACHE)
$env:GITHUB_WORKSPACE, # sources + compiled test binaries
$env:RUNNER_TEMP
) | Where-Object { $_ -and $_.Trim() -ne '' } | Select-Object -Unique
foreach ($t in $targets) {
try {
Add-MpPreference -ExclusionPath $t -ErrorAction Stop
Write-Host "Defender exclusion added: $t"
} catch {
Write-Host "::warning::Defender exclusion failed for $t : $($_.Exception.Message)"
}
}
try { Add-MpPreference -ExclusionProcess 'go.exe' -ErrorAction Stop } catch {}
- uses: actions/setup-go@v7
with:
go-version-file: boatstack/go.mod
cache-dependency-path: boatstack/go.sum
- name: Show toolchain
run: go version
working-directory: boatstack
# Enumerate tests, pick this shard's balanced subset, and run only those.
# `go test -list` and `go test -run` share the warm GOCACHE, so the second
# compile is a cache hit. An empty shard is a clean skip — never
# `go test -run ''`, which would run the whole suite.
- name: Test shard ${{ matrix.shard }}
shell: bash
working-directory: boatstack
run: |
regex=$(go test -list '^Test' ./... | python "${{ github.workspace }}/.github/scripts/ci_shard.py" --total 6 --index ${{ matrix.shard }})
if [ -z "$regex" ]; then
echo "Shard ${{ matrix.shard }} is empty; nothing to run."
exit 0
fi
echo "Shard ${{ matrix.shard }} selects $(( $(grep -o '|' <<<"$regex" | wc -l) + 1 )) tests"
go test -run "$regex" ./...
# Windows-only, non-test validation runs once (on shard 0), not per shard.
- name: Build helper
if: matrix.shard == '0'
working-directory: boatstack
run: go build ./cmd/boatstack-helper
- name: Validate PowerShell installer
if: matrix.shard == '0'
shell: pwsh
run: |
$tokens = $null
$errors = $null
[System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path install.ps1), [ref]$tokens, [ref]$errors) > $null
if ($errors.Count -gt 0) {
$errors | ForEach-Object { Write-Error $_ }
exit 1
}
- name: Prove PowerShell install and hydration boundaries
if: matrix.shard == '0'
shell: pwsh
run: |
$root = Join-Path $env:RUNNER_TEMP ("boatstack-installer-" + [guid]::NewGuid().ToString("N"))
$helper = (Resolve-Path boatstack/boatstack-helper.exe).Path
$version = (& $helper version).Trim()
$digest = (Get-FileHash -Algorithm SHA256 -LiteralPath $helper).Hash.ToLowerInvariant()
try {
foreach ($mode in @("hydrate", "install")) {
$repository = Join-Path $root "$mode-repository"
New-Item -ItemType Directory -Force -Path $repository | Out-Null
& git -C $repository init --initial-branch=main | Out-Null
& git -C $repository config user.email "installer-contract@example.invalid"
& git -C $repository config user.name "Installer Contract"
Set-Content -LiteralPath (Join-Path $repository "README.md") -Value "fixture"
& git -C $repository add README.md
& git -C $repository commit -m "Initialize fixture" | Out-Null
if ($mode -eq "hydrate") {
& git -C $repository checkout --detach | Out-Null
}
$runtimeHome = Join-Path $root "$mode-home"
$installDir = Join-Path $root "$mode-bin"
$launcher = Join-Path $installDir "boatstack.exe"
if ($mode -eq "hydrate") {
New-Item -ItemType Directory -Force -Path $installDir | Out-Null
[System.IO.File]::WriteAllBytes($launcher, [byte[]](1, 2, 3, 4))
$launcherBefore = (Get-FileHash -Algorithm SHA256 -LiteralPath $launcher).Hash
}
$env:BOATSTACK_REPO = $repository
$env:BOATSTACK_HOME = $runtimeHome
$env:BOATSTACK_INSTALL_DIR = $installDir
$env:BOATSTACK_MODE = $mode
$env:BOATSTACK_VERSION = $version
$env:BOATSTACK_BINARY = $helper
$env:BOATSTACK_BINARY_SHA256 = $digest
$env:BOATSTACK_EXPECTED_RUNTIME_SHA256 = $digest
& ./install.ps1
if ($LASTEXITCODE -ne 0) { throw "$mode installer failed" }
$runtime = Join-Path $runtimeHome "runtimes\$version-$digest\boatstack-runtime.exe"
if (-not (Test-Path -LiteralPath $runtime -PathType Leaf)) { throw "$mode did not stage the runtime" }
if (-not (Test-Path -LiteralPath $launcher -PathType Leaf)) { throw "$mode did not stage the launcher" }
if ($mode -eq "hydrate") {
if ((Get-FileHash -Algorithm SHA256 -LiteralPath $launcher).Hash -ne $launcherBefore) { throw "hydrate replaced the shared launcher" }
if (Test-Path -LiteralPath (Join-Path $repository ".boatstack")) { throw "hydrate changed repository state" }
if (Test-Path -LiteralPath (Join-Path $repository ".git\boatstack")) { throw "hydrate changed controller state" }
if (& git -C $repository status --porcelain) { throw "hydrate changed tracked repository files" }
} elseif (-not (Test-Path -LiteralPath (Join-Path $repository ".boatstack\runtime.json") -PathType Leaf)) {
throw "install did not initialize the repository runtime pin"
}
}
$missingEvidenceRepository = Join-Path $root "missing-evidence-repository"
New-Item -ItemType Directory -Force -Path $missingEvidenceRepository | Out-Null
& git -C $missingEvidenceRepository init --initial-branch=main | Out-Null
$missingEvidenceHome = Join-Path $root "missing-evidence-home"
$missingEvidenceBin = Join-Path $root "missing-evidence-bin"
$env:BOATSTACK_REPO = $missingEvidenceRepository
$env:BOATSTACK_HOME = $missingEvidenceHome
$env:BOATSTACK_INSTALL_DIR = $missingEvidenceBin
$env:BOATSTACK_MODE = "hydrate"
$env:BOATSTACK_VERSION = "latest"
Remove-Item Env:BOATSTACK_EXPECTED_RUNTIME_SHA256 -ErrorAction SilentlyContinue
try {
& ./install.ps1
throw "hydrate without exact evidence succeeded"
} catch {
if ($_ -notmatch "BOATSTACK_RUNTIME_PIN_INVALID") { throw }
}
if (Test-Path -LiteralPath $missingEvidenceHome) { throw "invalid hydrate changed runtime storage" }
if (Test-Path -LiteralPath $missingEvidenceBin) { throw "invalid hydrate changed launcher storage" }
} finally {
Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
}
repository-contract:
name: repository-contract
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- uses: actions/setup-go@v7
with:
go-version-file: boatstack/go.mod
cache-dependency-path: boatstack/go.sum
- name: Require an append-only release message
if: github.event_name == 'pull_request'
run: >-
python .github/scripts/release_notes.py check-policy
--repo .
--base "${{ github.event.pull_request.base.sha }}"
--head "${{ github.event.pull_request.head.sha }}"
- name: Verify repository contract
env:
PYTHONUTF8: "1"
run: python -m unittest discover -s .github/tests -p 'test_*.py' -v