fix(dashboard): fail closed on remote state reads #868
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: Bun Parity | |
| # Verifies bash-vs-bun byte-for-byte parity for the commands ported in | |
| # Phase 2/3 of the bash->Bun migration (ADR-001). Codifies the parity | |
| # invariant that has been hand-checked since v7.3.0. | |
| # | |
| # For every command in the matrix below this job: | |
| # 1. Captures stdout from the Bun route (bin/loki <cmd>) | |
| # 2. Captures stdout from the bash route (LOKI_LEGACY_BASH=1 bin/loki <cmd>) | |
| # 3. For --json variants: jq -S sort then diff | |
| # For text variants: direct diff | |
| # 4. Fails the job if ANY command differs. | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| bun-parity: | |
| name: bun-parity on ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: 1.3.13 | |
| - name: Setup Python (for bash route fallthroughs) | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install jq (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| if ! command -v jq >/dev/null 2>&1; then | |
| sudo apt-get update | |
| sudo apt-get install -y jq | |
| fi | |
| jq --version | |
| - name: Install jq (macOS) | |
| if: runner.os == 'macOS' | |
| run: | | |
| if ! command -v jq >/dev/null 2>&1; then | |
| brew install jq | |
| fi | |
| jq --version | |
| - name: Build loki-ts (Bun) | |
| working-directory: loki-ts | |
| run: | | |
| bun install | |
| bun run build | |
| - name: Verify both routes are invokable | |
| run: | | |
| test -x bin/loki || { echo "FATAL: bin/loki missing or not executable"; exit 1; } | |
| test -x autonomy/loki || { echo "FATAL: autonomy/loki missing or not executable"; exit 1; } | |
| test -f loki-ts/dist/loki.js || { echo "FATAL: loki-ts/dist/loki.js missing after build"; exit 1; } | |
| - name: Run parity matrix | |
| id: parity | |
| shell: bash | |
| run: | | |
| set -uo pipefail | |
| # Command matrix. JSON variants get jq -S normalization; text | |
| # variants get straight diff. | |
| # Format: "<label>|<args>|<mode:text|json>" | |
| MATRIX=( | |
| "version|--version|text" | |
| "provider-show|provider show|text" | |
| "provider-list|provider list|text" | |
| "memory-list|memory list|text" | |
| "status|status|text" | |
| "status-json|status --json|json" | |
| "stats|stats|text" | |
| "stats-json|stats --json|json" | |
| # v7.4.10: re-enabled doctor text parity now that v7.4.9 | |
| # added Bun probe to bash cmd_doctor and confirmed both routes | |
| # print Disk space + Summary + missing-prereq trailer. | |
| "doctor|doctor|text" | |
| "doctor-json|doctor --json|json" | |
| ) | |
| mkdir -p /tmp/parity | |
| FAILED=() | |
| PASSED=() | |
| for entry in "${MATRIX[@]}"; do | |
| label="${entry%%|*}" | |
| rest="${entry#*|}" | |
| args="${rest%%|*}" | |
| mode="${rest##*|}" | |
| bun_out="/tmp/parity/${label}.bun" | |
| bash_out="/tmp/parity/${label}.bash" | |
| diff_out="/tmp/parity/${label}.diff" | |
| # Capture both routes. Do not let a non-zero exit kill the job | |
| # before we can diff -- record exit codes separately. | |
| set +e | |
| bin/loki $args >"$bun_out" 2>/dev/null | |
| bun_ec=$? | |
| LOKI_LEGACY_BASH=1 bin/loki $args >"$bash_out" 2>/dev/null | |
| bash_ec=$? | |
| set -e | |
| if [ "$mode" = "json" ]; then | |
| # Sort keys recursively before diffing so key-order changes | |
| # do not register as parity violations. | |
| # v7.4.12 also floors disk.available_gb because Python | |
| # json.dumps emits 58.0 while JS JSON.stringify emits 58 -- | |
| # plus the underlying df read can drift by 1GB between two | |
| # near-simultaneous calls on a busy CI runner. | |
| JQ_NORM='if .disk?.available_gb? != null then .disk.available_gb = (.disk.available_gb | floor) else . end' | |
| if ! jq -S "$JQ_NORM" "$bun_out" >"${bun_out}.sorted" 2>/dev/null; then | |
| echo "FAIL [$label]: bun output is not valid JSON" | |
| FAILED+=("$label (invalid bun JSON)") | |
| continue | |
| fi | |
| if ! jq -S "$JQ_NORM" "$bash_out" >"${bash_out}.sorted" 2>/dev/null; then | |
| echo "FAIL [$label]: bash output is not valid JSON" | |
| FAILED+=("$label (invalid bash JSON)") | |
| continue | |
| fi | |
| if diff -u "${bash_out}.sorted" "${bun_out}.sorted" >"$diff_out"; then | |
| PASSED+=("$label") | |
| else | |
| echo "FAIL [$label]: JSON parity diff (bash exit=$bash_ec, bun exit=$bun_ec):" | |
| cat "$diff_out" | |
| FAILED+=("$label") | |
| fi | |
| else | |
| # v7.4.12+ -- before diff, normalize jittery numeric values | |
| # that drift between two near-simultaneous reads on busy CI: | |
| # - Disk space: 88GB available -> Disk space: NGB available | |
| # bash + Bun read `df` independently a few ms apart; on a CI | |
| # runner where logs are being written, the value can drift by | |
| # 1GB between the two reads and trigger a false parity fail. | |
| # v7.5.6 (council R3 #1, 5-runs-red): mirror the | |
| # scripts/local-ci.sh normalization that was added in | |
| # v7.5.1 + v7.5.5 but never ported here: | |
| # - strip the "Runtime route:" block (route-dependent; | |
| # reports "Bash" on the bash route and "Bun" on Bun) | |
| # - strip the "Phase 1 artifacts:" block (#204) | |
| # Without these, doctor parity failed every release since | |
| # v7.5.1 -- the local-only normalization masked it on | |
| # developer Macs while CI was red. | |
| # | |
| # The summary "N passed/N failed/N warnings" COUNTS are | |
| # intentionally KEPT (not blanked), matching parity-drift.yml. | |
| # A real divergence that shifts a doctor check between | |
| # pass/fail/warn between routes MUST surface as a parity | |
| # failure here -- this is the exact class of bug that broke | |
| # v7.58.0 (doctor crash off-TTY). The Runtime-route block drop | |
| # already removes the one benign count-shifter (the | |
| # LOKI_LEGACY_BASH-only WARN line), so counts match for | |
| # genuinely-equivalent runs. | |
| for src in "$bash_out" "$bun_out"; do | |
| sed -E 's/Disk space: [0-9]+GB/Disk space: NGB/g' "$src" \ | |
| | sed -E '/Runtime route:/,/^$/d' \ | |
| | sed -E '/Phase 1 artifacts:/,/^$/d' \ | |
| | sed -E '/Cockpit:/,/^$/d' \ | |
| > "${src}.norm" | |
| done | |
| # COMMON-MODE FAILURE GUARD. Parity is a diff, and two routes that | |
| # break IDENTICALLY diff clean. bun_ec/bash_ec were captured but | |
| # never asserted, and stderr is discarded, so a command that exits | |
| # 127 with empty stdout on BOTH routes was recorded as PASS and the | |
| # job printed "All commands parity-verified". Both routes share the | |
| # bin/loki shim, so a shim-level break is exactly the common-mode | |
| # case this would wave through. | |
| # | |
| # Identical output is only evidence of parity when the commands | |
| # actually RAN. The json branch is already protected -- an empty or | |
| # invalid capture fails `jq -S` and lands in FAILED -- so this guard | |
| # is the text branch's equivalent, not a duplicate. | |
| # | |
| # Deliberately narrow: only a matching-nonzero-exit-AND-empty-output | |
| # pair fails here. A command that legitimately exits nonzero while | |
| # still printing (a doctor run with blockers) keeps working, which | |
| # is why the emptiness half of the condition is load-bearing. | |
| if [ "$bun_ec" -ne 0 ] && [ "$bash_ec" -ne 0 ] \ | |
| && [ ! -s "${bash_out}.norm" ] && [ ! -s "${bun_out}.norm" ]; then | |
| echo "FAIL [$label]: BOTH routes failed identically (bash exit=$bash_ec, bun exit=$bun_ec) with no output -- identical breakage is not parity" | |
| FAILED+=("$label (common-mode failure, exit $bash_ec/$bun_ec)") | |
| continue | |
| fi | |
| if diff -u "${bash_out}.norm" "${bun_out}.norm" >"$diff_out"; then | |
| PASSED+=("$label") | |
| else | |
| echo "FAIL [$label]: text parity diff (bash exit=$bash_ec, bun exit=$bun_ec):" | |
| cat "$diff_out" | |
| FAILED+=("$label") | |
| fi | |
| fi | |
| done | |
| echo "" | |
| echo "================================================================" | |
| echo "Bun Parity Summary" | |
| echo "================================================================" | |
| printf '%-20s %s\n' "COMMAND" "RESULT" | |
| printf '%-20s %s\n' "-------" "------" | |
| # bash 3.2 on macos triggers `unbound variable` under `set -u` when | |
| # iterating an empty array, so guard with the :- expansion. | |
| for p in "${PASSED[@]:-}"; do | |
| [ -z "$p" ] && continue | |
| printf '%-20s %s\n' "$p" "PASS" | |
| done | |
| for f in "${FAILED[@]:-}"; do | |
| [ -z "$f" ] && continue | |
| printf '%-20s %s\n' "$f" "FAIL" | |
| done | |
| echo "================================================================" | |
| echo "Passed: ${#PASSED[@]} Failed: ${#FAILED[@]} Total: ${#MATRIX[@]}" | |
| echo "================================================================" | |
| if [ "${#FAILED[@]}" -gt 0 ]; then | |
| echo "Bun parity check FAILED for ${#FAILED[@]} command(s)." | |
| exit 1 | |
| fi | |
| echo "All commands parity-verified." |