|
1 | 1 | name: Install checks |
2 | 2 |
|
3 | | -# Installs the backend on clean Linux and Windows hosts, checks the running |
4 | | -# service, then uninstalls. Tray companions need a desktop session that hosted |
5 | | -# runners do not reliably provide, so they are not covered here. |
| 3 | +# Installs the backend on clean macOS, Linux, and Windows hosts, checks the |
| 4 | +# running service, then uninstalls; on Linux it also runs a real update. Tray |
| 5 | +# companions need a desktop session that hosted runners do not reliably |
| 6 | +# provide, so they are not covered here. |
6 | 7 | on: |
7 | 8 | push: |
8 | 9 | branches: [main] |
@@ -95,6 +96,200 @@ jobs: |
95 | 96 | if: failure() |
96 | 97 | run: journalctl --user -u token-meter-server.service -n 200 --no-pager || true |
97 | 98 |
|
| 99 | + # Installs from a local clone whose origin is a local bare remote, publishes a |
| 100 | + # newer commit there, then drives Install through the dashboard API. The |
| 101 | + # installer restarts the server mid-update, so this proves the helper |
| 102 | + # outlives that restart and the server comes back on the new revision. |
| 103 | + linux-update: |
| 104 | + name: Linux update |
| 105 | + runs-on: ubuntu-latest |
| 106 | + timeout-minutes: 25 |
| 107 | + env: |
| 108 | + TOKEN_METER_READINESS_TIMEOUT_SECONDS: "600" |
| 109 | + steps: |
| 110 | + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 |
| 111 | + with: |
| 112 | + fetch-depth: 0 |
| 113 | + |
| 114 | + - name: Start a systemd user session |
| 115 | + run: | |
| 116 | + sudo loginctl enable-linger "$USER" |
| 117 | + runtime_dir="/run/user/$(id -u)" |
| 118 | + export XDG_RUNTIME_DIR="$runtime_dir" |
| 119 | + export DBUS_SESSION_BUS_ADDRESS="unix:path=$runtime_dir/bus" |
| 120 | + for _ in $(seq 1 30); do |
| 121 | + systemctl --user show-environment >/dev/null 2>&1 && break |
| 122 | + sleep 1 |
| 123 | + done |
| 124 | + systemctl --user show-environment >/dev/null |
| 125 | + echo "XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR" >> "$GITHUB_ENV" |
| 126 | + echo "DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS" >> "$GITHUB_ENV" |
| 127 | +
|
| 128 | + - name: Publish this revision to a local remote |
| 129 | + run: | |
| 130 | + remote="$RUNNER_TEMP/update-remote.git" |
| 131 | + source="$RUNNER_TEMP/update-source" |
| 132 | + git init --quiet --bare --initial-branch=main "$remote" |
| 133 | + git push --quiet "$remote" HEAD:refs/heads/main |
| 134 | + git clone --quiet "$remote" "$source" |
| 135 | + echo "UPDATE_SOURCE=$source" >> "$GITHUB_ENV" |
| 136 | +
|
| 137 | + - name: Add a sample session and turn off automatic installs |
| 138 | + run: | |
| 139 | + python3 .github/scripts/write_sample_trace.py |
| 140 | + mkdir -p "$HOME/.token-meter" |
| 141 | + printf '%s\n' '{"updates": {"enabled": true, "auto_install": false}}' \ |
| 142 | + > "$HOME/.token-meter/settings.json" |
| 143 | +
|
| 144 | + - name: Install |
| 145 | + run: | |
| 146 | + cd "$UPDATE_SOURCE" |
| 147 | + ./scripts/install --backend-only |
| 148 | + systemctl --user show -p MainPID --value token-meter-server.service \ |
| 149 | + > "$RUNNER_TEMP/server-pid-before" |
| 150 | +
|
| 151 | + - name: Publish a newer revision |
| 152 | + run: | |
| 153 | + cd "$UPDATE_SOURCE" |
| 154 | + git -c user.name="Token Meter CI" -c user.email="ci@token-meter.invalid" \ |
| 155 | + commit --quiet --allow-empty -m "ci: update bump" |
| 156 | + git push --quiet origin main |
| 157 | + echo "UPDATE_TARGET=$(git rev-parse HEAD)" >> "$GITHUB_ENV" |
| 158 | +
|
| 159 | + - name: Check for and install the update |
| 160 | + run: | |
| 161 | + python3 - <<'PY' |
| 162 | + import json, os, time, urllib.request |
| 163 | + base = "http://127.0.0.1:8722" |
| 164 | + target = os.environ["UPDATE_TARGET"] |
| 165 | +
|
| 166 | + def get(path): |
| 167 | + with urllib.request.urlopen(base + path, timeout=10) as response: |
| 168 | + return json.load(response) |
| 169 | +
|
| 170 | + def post(path, token): |
| 171 | + request = urllib.request.Request( |
| 172 | + base + path, data=b"{}", method="POST", |
| 173 | + headers={"Content-Type": "application/json", "X-Token-Meter-Action": token}, |
| 174 | + ) |
| 175 | + with urllib.request.urlopen(request, timeout=10) as response: |
| 176 | + return response.status, json.load(response) |
| 177 | +
|
| 178 | + token = get("/updates/status")["actions"]["token"] |
| 179 | + deadline = time.monotonic() + 180 |
| 180 | + # A check already running from server startup ignores a new request, |
| 181 | + # so keep asking until one sees the published revision. |
| 182 | + while True: |
| 183 | + status = get("/updates/status") |
| 184 | + if status.get("can_update") and target.startswith(status.get("latest_revision") or "-"): |
| 185 | + break |
| 186 | + assert time.monotonic() < deadline, status |
| 187 | + if not status.get("checking"): |
| 188 | + post("/updates/check", token) |
| 189 | + time.sleep(2) |
| 190 | + code, result = post("/updates/install", token) |
| 191 | + assert code == 202 and result.get("ok"), (code, result) |
| 192 | + print("update started for", target[:12]) |
| 193 | + PY |
| 194 | +
|
| 195 | + - name: Wait for the update to finish |
| 196 | + run: | |
| 197 | + status_file="$HOME/.token-meter/update-status.json" |
| 198 | + deadline=$((SECONDS + 720)) |
| 199 | + while (( SECONDS < deadline )); do |
| 200 | + phase="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("phase",""))' "$status_file" 2>/dev/null || true)" |
| 201 | + case "$phase" in |
| 202 | + complete) echo "update complete"; break ;; |
| 203 | + failed) cat "$status_file"; exit 1 ;; |
| 204 | + esac |
| 205 | + sleep 2 |
| 206 | + done |
| 207 | + test "$phase" = complete || { echo "update stuck in phase: $phase" >&2; cat "$status_file"; exit 1; } |
| 208 | +
|
| 209 | + - name: Check the updated installation |
| 210 | + run: | |
| 211 | + runtime="${XDG_DATA_HOME:-$HOME/.local/share}/token-meter/runtime" |
| 212 | + installed="$(cat "$runtime/INSTALLED_REVISION")" |
| 213 | + case "$UPDATE_TARGET" in |
| 214 | + "$installed"*) echo "installed revision ok: $installed" ;; |
| 215 | + *) echo "installed $installed, expected $UPDATE_TARGET" >&2; exit 1 ;; |
| 216 | + esac |
| 217 | + systemctl --user is-active --quiet token-meter-server.service |
| 218 | + before="$(cat "$RUNNER_TEMP/server-pid-before")" |
| 219 | + after="$(systemctl --user show -p MainPID --value token-meter-server.service)" |
| 220 | + test "$before" != "$after" || { echo "the server was not restarted" >&2; exit 1; } |
| 221 | + 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")' |
| 222 | +
|
| 223 | + - name: Show update and server logs |
| 224 | + if: failure() |
| 225 | + run: | |
| 226 | + cat "$HOME/.token-meter/update-status.json" || true |
| 227 | + journalctl --user -u token-meter-update.service -n 200 --no-pager || true |
| 228 | + journalctl --user -u token-meter-server.service -n 200 --no-pager || true |
| 229 | +
|
| 230 | + macos: |
| 231 | + name: macOS install |
| 232 | + runs-on: macos-latest |
| 233 | + timeout-minutes: 20 |
| 234 | + env: |
| 235 | + TOKEN_METER_READINESS_TIMEOUT_SECONDS: "600" |
| 236 | + steps: |
| 237 | + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 |
| 238 | + with: |
| 239 | + fetch-depth: 0 |
| 240 | + |
| 241 | + - name: Add a sample session |
| 242 | + run: python3 .github/scripts/write_sample_trace.py |
| 243 | + |
| 244 | + - name: Install |
| 245 | + run: ./scripts/install --backend-only |
| 246 | + |
| 247 | + - name: Check the installation |
| 248 | + run: | |
| 249 | + runtime="$HOME/Library/Application Support/Token Meter/runtime" |
| 250 | + launchctl print "gui/$UID/com.token-meter.server" >/dev/null |
| 251 | + test "$(cat "$runtime/INSTALL_MODE")" = "backend-only" |
| 252 | + python3 - <<'PY' |
| 253 | + import json, time, urllib.request |
| 254 | + deadline = time.monotonic() + 60 |
| 255 | + while True: |
| 256 | + with urllib.request.urlopen("http://127.0.0.1:8722/health", timeout=10) as response: |
| 257 | + health = json.load(response) |
| 258 | + if health.get("inventory_ready") or time.monotonic() > deadline: |
| 259 | + break |
| 260 | + time.sleep(1) |
| 261 | + assert health.get("ok") and health.get("state_ready"), health |
| 262 | + assert health.get("inventory_ready"), health |
| 263 | + assert (health.get("sources") or 0) >= 1, health |
| 264 | + assert not health.get("runtime_adapter_failures"), health |
| 265 | + print("health ok:", health["sources"], "session(s) indexed") |
| 266 | + PY |
| 267 | + curl -fsS --max-time 10 http://127.0.0.1:8722/menubar | python3 -c 'import json, sys; json.load(sys.stdin); print("menubar ok")' |
| 268 | +
|
| 269 | + - name: Uninstall |
| 270 | + run: | |
| 271 | + runtime="$HOME/Library/Application Support/Token Meter/runtime" |
| 272 | + "$runtime/scripts/uninstall-launch-agent" |
| 273 | + if launchctl print "gui/$UID/com.token-meter.server" >/dev/null 2>&1; then |
| 274 | + echo "com.token-meter.server is still loaded" >&2 |
| 275 | + exit 1 |
| 276 | + fi |
| 277 | + for _ in $(seq 1 10); do |
| 278 | + curl -fsS --max-time 3 http://127.0.0.1:8722/health >/dev/null 2>&1 || { echo "uninstall ok"; exit 0; } |
| 279 | + sleep 1 |
| 280 | + done |
| 281 | + echo "the server still answers after uninstall" >&2 |
| 282 | + exit 1 |
| 283 | +
|
| 284 | + - name: Show server logs |
| 285 | + if: failure() |
| 286 | + run: | |
| 287 | + runtime="$HOME/Library/Application Support/Token Meter/runtime" |
| 288 | + for name in meter.err.log meter.log; do |
| 289 | + [[ -f "$runtime/$name" ]] && { echo "== $name"; tail -n 200 "$runtime/$name"; } |
| 290 | + done |
| 291 | + true |
| 292 | +
|
98 | 293 | windows: |
99 | 294 | name: Windows install |
100 | 295 | runs-on: windows-latest |
|
0 commit comments