Skip to content
Merged
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
81 changes: 81 additions & 0 deletions .github/workflows/install-roundtrip.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# SPDX-License-Identifier: MPL-2.0
#
# install-roundtrip.yml — actual install→activate→stop→uninstall cycle
# for the cross-platform service install machinery, on a real systemd /
# launchd / Windows SCM. Complements install-tests.yml (lint-only).
#
# Each job:
# 1. Stubs `mix` and `deno` so the spawned unit doesn't need the full
# Elixir/Deno toolchain (which would make every job slow + flaky).
# 2. Runs the platform's roundtrip-*.sh / .ps1 driver.
# 3. Drivers always clean up after themselves (trap EXIT / try/finally).

name: Install round-trip

on:
push:
branches: [main, master, 'claude/**']
paths:
- 'setup.sh'
- 'setup.ps1'
- 'scripts/install-service.sh'
- 'scripts/wsl-bolt-udp-forward.ps1'
- 'assets/services/**'
- 'tests/install/**'
- '.github/workflows/install-roundtrip.yml'
pull_request:
branches: [main, master]
paths:
- 'setup.sh'
- 'setup.ps1'
- 'scripts/install-service.sh'
- 'scripts/wsl-bolt-udp-forward.ps1'
- 'assets/services/**'
- 'tests/install/**'
- '.github/workflows/install-roundtrip.yml'

permissions:
contents: read

jobs:
linux-user-roundtrip:
name: Linux --user systemd round-trip
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

- name: Enable user-systemd lingering for runner
# GH Actions runs jobs without a real login session, so the
# per-user systemd instance isn't auto-started. `enable-linger`
# makes systemd-logind start it for the runner user even with
# no session.
run: |
sudo loginctl enable-linger "$USER"
# Wait for the user instance to come up.
for i in 1 2 3 4 5 6 7 8 9 10; do
if systemctl --user list-units >/dev/null 2>&1; then
echo "user-systemd ready (attempt $i)"; exit 0
fi
sleep 1
done
echo "user-systemd never became reachable"; exit 1

- name: Run round-trip
run: tests/install/roundtrip-linux.sh

macos-roundtrip:
name: macOS launchd LaunchAgent round-trip
runs-on: macos-14
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Run round-trip
run: tests/install/roundtrip-macos.sh

windows-roundtrip:
name: Windows Service round-trip
runs-on: windows-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Run round-trip
shell: pwsh
run: tests/install/roundtrip-windows.ps1
167 changes: 167 additions & 0 deletions .github/workflows/install-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# SPDX-License-Identifier: MPL-2.0
#
# install-tests.yml — cross-platform validation for the service-install
# machinery (setup.sh, setup.ps1, scripts/install-service.sh,
# scripts/wsl-bolt-udp-forward.ps1, assets/services/*).
#
# Three jobs cover three OSes; each is the lightest validation that
# would have caught the bugs we've already hit (unsubstituted tokens,
# AmbientCapabilities-in-user-unit, inline-`;` Justfile recipes, etc.):
#
# lint-linux Render systemd units in both modes, systemd-analyze
# verify, shellcheck, plist XML validity, setup.sh
# dispatch.
# lint-macos plutil -lint on the real macOS host (catches plist
# schema issues xmllint misses) + setup.sh OS dispatch.
# lint-windows PowerShell AST parse + PSScriptAnalyzer + verify
# csc.exe is reachable so the C# service-host compile
# would succeed at install time.
#
# Round-trip (actual install/start/stop/uninstall) is intentionally
# *not* in CI yet: --user systemd in GH Actions needs `linger` gymnastics,
# and the Windows install needs a throwaway credentialed user with
# `Log on as a service`. Those are tractable but worth a follow-up PR.

name: Install tests

on:
push:
branches: [main, master, 'claude/**']
paths:
- 'setup.sh'
- 'setup.ps1'
- 'scripts/install-service.sh'
- 'scripts/wsl-bolt-udp-forward.ps1'
- 'assets/services/**'
- 'tests/install/**'
- '.github/workflows/install-tests.yml'
pull_request:
branches: [main, master]
paths:
- 'setup.sh'
- 'setup.ps1'
- 'scripts/install-service.sh'
- 'scripts/wsl-bolt-udp-forward.ps1'
- 'assets/services/**'
- 'tests/install/**'
- '.github/workflows/install-tests.yml'

permissions:
contents: read

jobs:
lint-linux:
name: Lint (Linux / systemd / shellcheck)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Install validators
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends \
shellcheck libxml2-utils systemd
# systemd is preinstalled on ubuntu-latest but we want
# systemd-analyze present specifically.
- name: Run install test suite
run: tests/install/run.sh

lint-macos:
name: Lint (macOS / launchd plists)
runs-on: macos-14
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Run install test suite
run: tests/install/run.sh

lint-windows:
name: Lint (Windows / PowerShell + csc.exe)
runs-on: windows-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

- name: Install PSScriptAnalyzer
shell: pwsh
run: |
Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser -ErrorAction Stop

- name: PowerShell AST parse
shell: pwsh
run: |
$files = @('setup.ps1', 'scripts/wsl-bolt-udp-forward.ps1')
$any = $false
foreach ($f in $files) {
$errors = $null
[System.Management.Automation.Language.Parser]::ParseFile(
(Resolve-Path $f), [ref]$null, [ref]$errors) | Out-Null
if ($errors -and $errors.Count -gt 0) {
Write-Host "FAIL parse: $f"
$errors | ForEach-Object { Write-Host " $_" }
$any = $true
} else {
Write-Host "PASS parse: $f"
}
}
if ($any) { exit 1 }

- name: PSScriptAnalyzer
shell: pwsh
run: |
$files = @('setup.ps1', 'scripts/wsl-bolt-udp-forward.ps1')
$any = $false
foreach ($f in $files) {
# Excluded rules:
# PSAvoidUsingWriteHost — Write-Host is fine for a CLI tool
# PSAvoidUsingPlainTextForPassword — we use Get-Credential, the
# -RunAsUser flow is intentional and documented
$r = Invoke-ScriptAnalyzer -Path $f -Severity Warning,Error `
-ExcludeRule PSAvoidUsingWriteHost,
PSAvoidUsingPlainTextForPassword,
PSAvoidUsingConvertToSecureStringWithPlainText
if ($r) {
Write-Host "FAIL analyzer: $f"
$r | Format-Table -AutoSize | Out-String | Write-Host
$any = $true
} else {
Write-Host "PASS analyzer: $f"
}
}
if ($any) { exit 1 }

- name: C# service-host compile check (csc.exe + ServiceBase)
shell: pwsh
run: |
# Extract the embedded C# source from wsl-bolt-udp-forward.ps1 and
# try to compile it with the in-box .NET Framework csc.exe. This
# is exactly what `-Install` does at runtime — proving it compiles
# in CI means a real install on the user's machine won't blow up
# at `Compile-ServiceHost`.
$script = Get-Content scripts/wsl-bolt-udp-forward.ps1 -Raw
# The source lives between `$script:ServiceSource = @'` and the
# closing `'@` (single-quoted here-string).
if ($script -notmatch "(?ms)\`$script:ServiceSource\s*=\s*@'\s*\r?\n(.*?)\r?\n'@") {
Write-Host "Could not locate `$script:ServiceSource here-string"; exit 1
}
$src = $Matches[1]
$tmpCs = New-TemporaryFile
$tmpCs = [System.IO.Path]::ChangeExtension($tmpCs.FullName, '.cs')
Set-Content -Path $tmpCs -Value $src -Encoding ASCII

$csc = "$env:windir\Microsoft.NET\Framework64\v4.0.30319\csc.exe"
if (-not (Test-Path $csc)) {
$csc = "$env:windir\Microsoft.NET\Framework\v4.0.30319\csc.exe"
}
if (-not (Test-Path $csc)) {
Write-Host "FAIL: csc.exe not found — install path would fail here too"; exit 1
}
Write-Host "Compiling with: $csc"

$outExe = [System.IO.Path]::ChangeExtension($tmpCs, '.exe')
& $csc /nologo /target:exe /reference:System.ServiceProcess.dll `
"/out:$outExe" $tmpCs
if ($LASTEXITCODE -ne 0) {
Write-Host "FAIL: csc.exe compile failed"; exit $LASTEXITCODE
}
if (-not (Test-Path $outExe)) {
Write-Host "FAIL: no exe produced"; exit 1
}
Write-Host "PASS: C# service host compiles cleanly ($outExe)"
3 changes: 3 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[allowlist]
description = "Allow false positive AmbientCapabilities in README"
fingerprints = ["a33392723a5f060381e5878bcc2cb88a33db3494:tests/install/README.md:generic-api-key:54"]
74 changes: 73 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- Real install→activate→stop→uninstall round-trip tests for all three
service managers (complements the lint-only tests):
- `tests/install/roundtrip-linux.sh` — `systemctl --user` round-trip.
Also kills the unit's main PID and asserts `Restart=on-failure`
respawns it within `RestartSec=5`.
- `tests/install/roundtrip-macos.sh` — `launchctl bootstrap gui/$UID`
/ `bootout` round-trip with stub `PATH=` patched into the plist
(launchd ignores the user shell's env).
- `tests/install/roundtrip-windows.ps1` — creates a throwaway local
user (`burble-ci-test`) with a random password, installs the
Windows Service non-interactively via a new `-Credential`
parameter on `wsl-bolt-udp-forward.ps1` (skips the
`Get-Credential` prompt), asserts SCM state, uninstalls, removes
the user in a `finally` block.
- `tests/install/stubs/{mix,deno}` — sleep-forever stand-ins so the
spawned units satisfy systemd/launchd's "Active" check without
needing the full Elixir/Deno toolchain in CI.
- `.github/workflows/install-roundtrip.yml` — CI matrix:
`ubuntu-latest` (with `loginctl enable-linger` for user-systemd),
`macos-14`, `windows-latest`. Path-filtered to install machinery.
- `tests/install/run.sh` — cross-platform validation for the install
machinery, safe to run anywhere. Renders systemd units in both
system and user modes and checks invariants (no unsubstituted
`@TOKEN@`s, required sections present, `AmbientCapabilities only
in system mode, `User=/Group=` stripped from user-mode renders,
`WantedBy=` rewritten correctly), `systemd-analyze verify`s them
when available, plist-lints via `plutil`/`xmllint`, AST-parses the
PowerShell scripts, runs `shellcheck` + `PSScriptAnalyzer` if
installed. Each check reports `PASS`/`FAIL`/`SKIP` and the suite
exits non-zero on any FAIL. `just test-install` shortcut.
- `.github/workflows/install-tests.yml` — three-OS CI matrix:
`lint-linux` (systemd-analyze + shellcheck + xmllint), `lint-macos`
(real `plutil -lint`), `lint-windows` (PSScriptAnalyzer + AST parse
+ actually compiles the embedded C# service host with in-box
`csc.exe`, proving the runtime install path will succeed).
Triggered on changes to install machinery only.
- One-shot OS-aware setup front doors so a fresh clone gets to a fully
installed background service in a single command per side:
- `./setup.sh` (extended) — detects Linux / macOS / WSL, runs preflight
(`mix`, `deno`, `systemctl`/`launchctl` presence), interactively
offers to install the systemd / launchd service, and on WSL prints
the *exact* elevated-PowerShell one-liner (with `\\wsl.localhost\<distro>\…`
UNC path pre-filled) for the Windows-host step. Honours
`BURBLE_INSTALL_SERVICE=yes|no` for non-interactive flows.
- `setup.ps1` (new) — Windows-host counterpart. Asserts elevation,
preflights `wsl.exe` + .NET Framework `csc.exe`, autodetects the
default WSL distro, installs the Bolt forwarder as a true Windows
Service via `scripts/wsl-bolt-udp-forward.ps1 -Install`, adds
Defender rules, then prints the `wsl -d <distro> -- ./setup.sh`
one-liner to complete the Linux side.
- Justfile: new `just setup` recipe (delegates to `./setup.sh`).
- Cross-platform background-service install path so launching Burble no
longer pops a terminal window. New `scripts/install-service.sh install`
detects the OS and installs:
- Linux/WSL: systemd `--user` units (`assets/services/burble.service`,
`burble-ai-bridge.service`) — Bolt's `udp/9` privileged bind handled
via `AmbientCapabilities=CAP_NET_BIND_SERVICE` instead of root.
via `AmbientCapabilities CAP_NET_BIND_SERVICE` instead of root.
- macOS: launchd LaunchAgents
(`assets/services/com.hyperpolymath.burble.plist`,
`…ai-bridge.plist`) in `~/Library/LaunchAgents/`.
Expand All @@ -38,6 +89,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`/tmp/burble.{out,err}.log` on macOS.
- `Burble.TestSupport.SingletonWatcher` in `test/test_helper.exs` — `Process.monitor`s each of 20 app-owned singletons (PubSub, Presence, RoomRegistry/Supervisor, PeerRegistry/Supervisor, CoprocessorRegistry/Supervisor, MessageStore, NNTPSBackend, Media.Engine, Timing.{PTP,ClockCorrelator,Alignment}, Groove + HealthMesh + Feedback, Transport.RTSP, Bolt.Listener, Endpoint), reports any mid-run death (name + pid + reason + ms-since-start) to stderr at suite end, freezes via `ExUnit.after_suite/1` before BEAM shutdown so the normal app-teardown `:DOWN` cascade is not mistaken for instability. Diagnostic for #62 Bucket B; advisory (does not fail CI).

### Fixed
- `render_unit` in `scripts/install-service.sh` left `@USER@` in user-mode
output when the token appeared in comments — caught by
`tests/install/run.sh`. Now substituted in both modes (the `User=`
*directive* is still stripped from user-mode renders since it's
invalid there).
- Linux service unit now binds `udp/9` correctly. Earlier draft used
`AmbientCapabilities CAP_NET_BIND_SERVICE` in a systemd `--user` unit,
which is silently ignored — user instances cannot grant capabilities.
`assets/services/burble.service` is now a **system** unit (User=@USER@,
installed to `/etc/systemd/system/`) where AmbientCapabilities actually
applies. `scripts/install-service.sh install` defaults to the system
mode (uses `sudo`); pass `--user` for the prior user-unit behaviour
(no sudo, but udp/9 won't bind without `--setcap`, which runs
`sudo setcap cap_net_bind_service=+eip` on the active `beam.smp`).
A new `assets/services/burble.user.service` documents the user-mode
variant; the installer also renders the user variant on the fly by
stripping `User=/Group=/AmbientCapabilities=` and rewriting
`WantedBy`. `setup.sh` prompts for system-vs-user mode interactively
(or honours `BURBLE_INSTALL_MODE=system|user`).

### Changed
- README/ROADMAP claims scoped to the shipped build per ADR-0007: QUIC & SNIF marked experimental (optional NIFs disabled by default), PTP <1µs flagged hardware-gated, Idris2 proofs flagged type-check-only (runtime enforcement = ADR-0008 Option C), latency/scale flagged unbenchmarked; added a README Status section. Closes the STATE.a2ml doc-reality-drift entries (issue #51)

Expand Down
9 changes: 9 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ server:
# users should also run scripts\wsl-bolt-udp-forward.ps1 -Install from a
# Windows PowerShell to forward Bolt udp/7373+9 into WSL — windowless.

# OS-aware first-run setup: preflight + service install + WSL handoff
setup:
./setup.sh

# Install Burble as a background service (no terminal window pops up)
service-install:
scripts/install-service.sh install
Expand All @@ -161,6 +165,11 @@ service-restart: ; scripts/install-service.sh restart
service-status: ; scripts/install-service.sh status
service-logs: ; scripts/install-service.sh logs

# Lint the cross-platform install machinery (setup.sh, .ps1, units, plists)
test-install:
tests/install/run.sh


# Start the web client dev server
client:
cd client/web && deno task dev
Expand Down
Loading
Loading