Repository navigation
perf: cut redundant work from the steady cross-session rebuild (#90) #38
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: Install checks | |
| # Installs the backend on clean macOS, Linux, and Windows hosts, checks the | |
| # running service, then uninstalls; on Linux it also runs a real update. Tray | |
| # companions need a desktop session that hosted runners do not reliably | |
| # provide, so they are not covered here. | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| # Superseded pull-request runs are cancelled. Every other run gets its own | |
| # group (keyed by commit), so each commit on main is checked to completion. | |
| concurrency: | |
| group: install-checks-${{ github.event_name == 'pull_request' && github.ref || github.sha }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| linux: | |
| name: Linux install | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| env: | |
| TOKEN_METER_READINESS_TIMEOUT_SECONDS: "600" | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| fetch-depth: 0 | |
| # Hosted runners have no logged-in user session; lingering starts the | |
| # systemd user manager the installer expects from a desktop login. | |
| - name: Start a systemd user session | |
| run: | | |
| sudo loginctl enable-linger "$USER" | |
| runtime_dir="/run/user/$(id -u)" | |
| export XDG_RUNTIME_DIR="$runtime_dir" | |
| export DBUS_SESSION_BUS_ADDRESS="unix:path=$runtime_dir/bus" | |
| for _ in $(seq 1 30); do | |
| systemctl --user show-environment >/dev/null 2>&1 && break | |
| sleep 1 | |
| done | |
| systemctl --user show-environment >/dev/null | |
| echo "XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR" >> "$GITHUB_ENV" | |
| echo "DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS" >> "$GITHUB_ENV" | |
| - name: Add a sample session | |
| run: python3 .github/scripts/write_sample_trace.py | |
| - name: Install | |
| run: ./scripts/install --backend-only | |
| - name: Check the installation | |
| run: | | |
| runtime="${XDG_DATA_HOME:-$HOME/.local/share}/token-meter/runtime" | |
| systemctl --user is-active --quiet token-meter-server.service | |
| test "$(cat "$runtime/INSTALL_MODE")" = "backend-only" | |
| # The installer waits for state_ready; the session inventory can | |
| # finish a moment later, so allow it up to a minute. | |
| python3 - <<'PY' | |
| import json, time, urllib.request | |
| deadline = time.monotonic() + 60 | |
| while True: | |
| with urllib.request.urlopen("http://127.0.0.1:8722/health", timeout=10) as response: | |
| health = json.load(response) | |
| if health.get("inventory_ready") or time.monotonic() > deadline: | |
| break | |
| time.sleep(1) | |
| assert health.get("ok") and health.get("state_ready"), health | |
| assert health.get("inventory_ready"), health | |
| assert (health.get("sources") or 0) >= 1, health | |
| assert not health.get("runtime_adapter_failures"), health | |
| print("health ok:", health["sources"], "session(s) indexed") | |
| PY | |
| curl -fsS --max-time 10 http://127.0.0.1:8722/menubar | python3 -c 'import json, sys; json.load(sys.stdin); print("menubar ok")' | |
| - name: Uninstall | |
| run: | | |
| runtime="${XDG_DATA_HOME:-$HOME/.local/share}/token-meter/runtime" | |
| "$runtime/scripts/uninstall-systemd-user" | |
| if systemctl --user is-active --quiet token-meter-server.service; then | |
| echo "token-meter-server.service is still running" >&2 | |
| exit 1 | |
| fi | |
| if curl -fsS --max-time 3 http://127.0.0.1:8722/health >/dev/null 2>&1; then | |
| echo "the server still answers after uninstall" >&2 | |
| exit 1 | |
| fi | |
| echo "uninstall ok" | |
| - name: Show server logs | |
| if: failure() | |
| run: journalctl --user -u token-meter-server.service -n 200 --no-pager || true | |
| # Installs from a local clone whose origin is a local bare remote, publishes a | |
| # newer commit there, then drives Install through the dashboard API. The | |
| # installer restarts the server mid-update, so this proves the helper | |
| # outlives that restart and the server comes back on the new revision. | |
| linux-update: | |
| name: Linux update | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| env: | |
| TOKEN_METER_READINESS_TIMEOUT_SECONDS: "600" | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Start a systemd user session | |
| run: | | |
| sudo loginctl enable-linger "$USER" | |
| runtime_dir="/run/user/$(id -u)" | |
| export XDG_RUNTIME_DIR="$runtime_dir" | |
| export DBUS_SESSION_BUS_ADDRESS="unix:path=$runtime_dir/bus" | |
| for _ in $(seq 1 30); do | |
| systemctl --user show-environment >/dev/null 2>&1 && break | |
| sleep 1 | |
| done | |
| systemctl --user show-environment >/dev/null | |
| echo "XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR" >> "$GITHUB_ENV" | |
| echo "DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS" >> "$GITHUB_ENV" | |
| - name: Publish this revision to a local remote | |
| run: | | |
| remote="$RUNNER_TEMP/update-remote.git" | |
| source="$RUNNER_TEMP/update-source" | |
| git init --quiet --bare --initial-branch=main "$remote" | |
| git push --quiet "$remote" HEAD:refs/heads/main | |
| git clone --quiet "$remote" "$source" | |
| echo "UPDATE_SOURCE=$source" >> "$GITHUB_ENV" | |
| - name: Add a sample session and turn off automatic installs | |
| run: | | |
| python3 .github/scripts/write_sample_trace.py | |
| mkdir -p "$HOME/.token-meter" | |
| printf '%s\n' '{"updates": {"enabled": true, "auto_install": false}}' \ | |
| > "$HOME/.token-meter/settings.json" | |
| - name: Install | |
| run: | | |
| cd "$UPDATE_SOURCE" | |
| ./scripts/install --backend-only | |
| systemctl --user show -p MainPID --value token-meter-server.service \ | |
| > "$RUNNER_TEMP/server-pid-before" | |
| - name: Publish a newer revision | |
| run: | | |
| cd "$UPDATE_SOURCE" | |
| git -c user.name="Token Meter CI" -c user.email="ci@token-meter.invalid" \ | |
| commit --quiet --allow-empty -m "ci: update bump" | |
| git push --quiet origin main | |
| echo "UPDATE_TARGET=$(git rev-parse HEAD)" >> "$GITHUB_ENV" | |
| - name: Check for and install the update | |
| run: | | |
| python3 - <<'PY' | |
| import json, os, time, urllib.request | |
| base = "http://127.0.0.1:8722" | |
| target = os.environ["UPDATE_TARGET"] | |
| def get(path): | |
| with urllib.request.urlopen(base + path, timeout=10) as response: | |
| return json.load(response) | |
| def post(path, token): | |
| request = urllib.request.Request( | |
| base + path, data=b"{}", method="POST", | |
| headers={"Content-Type": "application/json", "X-Token-Meter-Action": token}, | |
| ) | |
| with urllib.request.urlopen(request, timeout=10) as response: | |
| return response.status, json.load(response) | |
| token = get("/updates/status")["actions"]["token"] | |
| deadline = time.monotonic() + 180 | |
| # A check already running from server startup ignores a new request, | |
| # so keep asking until one sees the published revision. | |
| while True: | |
| status = get("/updates/status") | |
| if status.get("can_update") and target.startswith(status.get("latest_revision") or "-"): | |
| break | |
| assert time.monotonic() < deadline, status | |
| if not status.get("checking"): | |
| post("/updates/check", token) | |
| time.sleep(2) | |
| code, result = post("/updates/install", token) | |
| assert code == 202 and result.get("ok"), (code, result) | |
| print("update started for", target[:12]) | |
| PY | |
| - name: Wait for the update to finish | |
| run: | | |
| status_file="$HOME/.token-meter/update-status.json" | |
| deadline=$((SECONDS + 720)) | |
| while (( SECONDS < deadline )); do | |
| phase="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("phase",""))' "$status_file" 2>/dev/null || true)" | |
| case "$phase" in | |
| complete) echo "update complete"; break ;; | |
| failed) cat "$status_file"; exit 1 ;; | |
| esac | |
| sleep 2 | |
| done | |
| test "$phase" = complete || { echo "update stuck in phase: $phase" >&2; cat "$status_file"; exit 1; } | |
| - name: Check the updated installation | |
| run: | | |
| runtime="${XDG_DATA_HOME:-$HOME/.local/share}/token-meter/runtime" | |
| installed="$(cat "$runtime/INSTALLED_REVISION")" | |
| case "$UPDATE_TARGET" in | |
| "$installed"*) echo "installed revision ok: $installed" ;; | |
| *) echo "installed $installed, expected $UPDATE_TARGET" >&2; exit 1 ;; | |
| esac | |
| systemctl --user is-active --quiet token-meter-server.service | |
| before="$(cat "$RUNNER_TEMP/server-pid-before")" | |
| after="$(systemctl --user show -p MainPID --value token-meter-server.service)" | |
| test "$before" != "$after" || { echo "the server was not restarted" >&2; exit 1; } | |
| curl -fsS --max-time 10 http://127.0.0.1:8722/health | python3 -c 'import json, sys; h = json.load(sys.stdin); assert h.get("ok"), h; print("health ok")' | |
| - name: Show update and server logs | |
| if: failure() | |
| run: | | |
| cat "$HOME/.token-meter/update-status.json" || true | |
| journalctl --user -u token-meter-update.service -n 200 --no-pager || true | |
| journalctl --user -u token-meter-server.service -n 200 --no-pager || true | |
| macos: | |
| name: macOS install | |
| runs-on: macos-latest | |
| timeout-minutes: 20 | |
| env: | |
| TOKEN_METER_READINESS_TIMEOUT_SECONDS: "600" | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Add a sample session | |
| run: python3 .github/scripts/write_sample_trace.py | |
| - name: Install | |
| run: ./scripts/install --backend-only | |
| - name: Check the installation | |
| run: | | |
| runtime="$HOME/Library/Application Support/Token Meter/runtime" | |
| launchctl print "gui/$UID/com.token-meter.server" >/dev/null | |
| test "$(cat "$runtime/INSTALL_MODE")" = "backend-only" | |
| python3 - <<'PY' | |
| import json, time, urllib.request | |
| deadline = time.monotonic() + 60 | |
| while True: | |
| with urllib.request.urlopen("http://127.0.0.1:8722/health", timeout=10) as response: | |
| health = json.load(response) | |
| if health.get("inventory_ready") or time.monotonic() > deadline: | |
| break | |
| time.sleep(1) | |
| assert health.get("ok") and health.get("state_ready"), health | |
| assert health.get("inventory_ready"), health | |
| assert (health.get("sources") or 0) >= 1, health | |
| assert not health.get("runtime_adapter_failures"), health | |
| print("health ok:", health["sources"], "session(s) indexed") | |
| PY | |
| curl -fsS --max-time 10 http://127.0.0.1:8722/menubar | python3 -c 'import json, sys; json.load(sys.stdin); print("menubar ok")' | |
| - name: Uninstall | |
| run: | | |
| runtime="$HOME/Library/Application Support/Token Meter/runtime" | |
| "$runtime/scripts/uninstall-launch-agent" | |
| if launchctl print "gui/$UID/com.token-meter.server" >/dev/null 2>&1; then | |
| echo "com.token-meter.server is still loaded" >&2 | |
| exit 1 | |
| fi | |
| for _ in $(seq 1 10); do | |
| curl -fsS --max-time 3 http://127.0.0.1:8722/health >/dev/null 2>&1 || { echo "uninstall ok"; exit 0; } | |
| sleep 1 | |
| done | |
| echo "the server still answers after uninstall" >&2 | |
| exit 1 | |
| - name: Show server logs | |
| if: failure() | |
| run: | | |
| runtime="$HOME/Library/Application Support/Token Meter/runtime" | |
| for name in meter.err.log meter.log; do | |
| [[ -f "$runtime/$name" ]] && { echo "== $name"; tail -n 200 "$runtime/$name"; } | |
| done | |
| true | |
| windows: | |
| name: Windows install | |
| runs-on: windows-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Add a sample session | |
| run: python .github/scripts/write_sample_trace.py | |
| - name: Install | |
| shell: powershell | |
| run: | | |
| & powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass ` | |
| -File scripts\install-windows.ps1 -BackendOnly -ReadinessTimeoutSeconds 600 | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| - name: Check the installation | |
| shell: powershell | |
| run: | | |
| $Runtime = Join-Path $env:LOCALAPPDATA "Token Meter\runtime" | |
| # The session inventory can finish a moment after state_ready. | |
| $Deadline = (Get-Date).AddSeconds(60) | |
| do { | |
| $Health = Invoke-RestMethod -Uri "http://127.0.0.1:8722/health" -TimeoutSec 10 | |
| if ($Health.inventory_ready) { break } | |
| Start-Sleep -Seconds 1 | |
| } while ((Get-Date) -lt $Deadline) | |
| if (-not $Health.ok -or -not $Health.state_ready -or -not $Health.inventory_ready) { throw "server is not ready: $($Health | ConvertTo-Json -Compress)" } | |
| if ([int]$Health.sources -lt 1) { throw "no sessions were indexed: $($Health | ConvertTo-Json -Compress)" } | |
| if (@($Health.runtime_adapter_failures).Count -gt 0) { throw "runtime adapters failed: $($Health | ConvertTo-Json -Compress)" } | |
| Write-Host "health ok: $($Health.sources) session(s) indexed" | |
| $null = Invoke-RestMethod -Uri "http://127.0.0.1:8722/menubar" -TimeoutSec 10 | |
| Write-Host "menubar ok" | |
| $Mode = (Get-Content -LiteralPath (Join-Path $Runtime "INSTALL_MODE") -Raw).Trim() | |
| if ($Mode -ne "backend-only") { throw "unexpected install mode: $Mode" } | |
| $RunKey = "Registry::HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run" | |
| if (-not (Get-ItemProperty -LiteralPath $RunKey -Name "Token Meter" -ErrorAction SilentlyContinue)) { | |
| throw "the login startup entry is missing" | |
| } | |
| Write-Host "startup entry ok" | |
| - name: Uninstall | |
| shell: powershell | |
| run: | | |
| $Runtime = Join-Path $env:LOCALAPPDATA "Token Meter\runtime" | |
| & powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File (Join-Path $Runtime "scripts\uninstall-windows.ps1") | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| $RunKey = "Registry::HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run" | |
| if (Get-ItemProperty -LiteralPath $RunKey -Name "Token Meter" -ErrorAction SilentlyContinue) { | |
| throw "the login startup entry is still present" | |
| } | |
| $StillServing = $true | |
| try { $null = Invoke-RestMethod -Uri "http://127.0.0.1:8722/health" -TimeoutSec 3 } catch { $StillServing = $false } | |
| if ($StillServing) { throw "the server still answers after uninstall" } | |
| Write-Host "uninstall ok" | |
| - name: Show server logs | |
| if: failure() | |
| shell: powershell | |
| run: | | |
| $Runtime = Join-Path $env:LOCALAPPDATA "Token Meter\runtime" | |
| foreach ($Name in @("meter.err.log", "meter.log")) { | |
| $Path = Join-Path $Runtime $Name | |
| if (Test-Path -LiteralPath $Path) { Write-Host "== $Name"; Get-Content -LiteralPath $Path -Tail 200 } | |
| } |