code-coordinator 0.5.46__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_coordinator-0.5.46.dist-info/METADATA +625 -0
- code_coordinator-0.5.46.dist-info/RECORD +295 -0
- code_coordinator-0.5.46.dist-info/WHEEL +5 -0
- code_coordinator-0.5.46.dist-info/entry_points.txt +2 -0
- code_coordinator-0.5.46.dist-info/licenses/LICENSE +110 -0
- code_coordinator-0.5.46.dist-info/top_level.txt +1 -0
- coord/__init__.py +176 -0
- coord/_board_mapping.py +229 -0
- coord/acceptance.py +468 -0
- coord/acceptance_drivers.py +632 -0
- coord/agent.py +7517 -0
- coord/agent_app.py +1555 -0
- coord/agent_update.py +417 -0
- coord/agents/opencode/.gitignore +13 -0
- coord/agents/opencode/agents/work.md +129 -0
- coord/agents/opencode/routing.jsonc +49 -0
- coord/audit.py +301 -0
- coord/auto_loop.py +1440 -0
- coord/board_bool_guard.py +72 -0
- coord/board_service.py +141 -0
- coord/board_wire.py +309 -0
- coord/brain.py +581 -0
- coord/branch_model.py +214 -0
- coord/cargo_cache.py +258 -0
- coord/ci_github.py +386 -0
- coord/ci_store.py +560 -0
- coord/claim.py +353 -0
- coord/cli.py +454 -0
- coord/client.py +610 -0
- coord/commands/__init__.py +1 -0
- coord/commands/_common.py +329 -0
- coord/commands/acceptance.py +916 -0
- coord/commands/agent_ops.py +1339 -0
- coord/commands/audit.py +131 -0
- coord/commands/chat.py +320 -0
- coord/commands/dispatch.py +1780 -0
- coord/commands/dispatch_workers.py +4894 -0
- coord/commands/drive.py +616 -0
- coord/commands/drive_queue.py +1203 -0
- coord/commands/gate_a.py +217 -0
- coord/commands/gates.py +89 -0
- coord/commands/issues.py +681 -0
- coord/commands/lifecycle.py +513 -0
- coord/commands/merge.py +1900 -0
- coord/commands/milestone.py +2081 -0
- coord/commands/plan_followup.py +1243 -0
- coord/commands/plans.py +156 -0
- coord/commands/release.py +2232 -0
- coord/commands/report.py +341 -0
- coord/commands/review.py +1523 -0
- coord/commands/scorecard.py +252 -0
- coord/commands/sessions.py +1930 -0
- coord/commands/setup.py +576 -0
- coord/commands/status.py +2089 -0
- coord/commands/terminal.py +385 -0
- coord/commands/test_gate.py +775 -0
- coord/commands/tui.py +288 -0
- coord/comments.py +718 -0
- coord/config.py +3032 -0
- coord/conflict_fix.py +633 -0
- coord/dao.py +483 -0
- coord/dashboard/__init__.py +0 -0
- coord/dashboard/fixture.py +376 -0
- coord/dashboard/index.html +658 -0
- coord/dashboard/server.py +1894 -0
- coord/dashboard/terminal.py +382 -0
- coord/dashboard/webapp/.gitignore +9 -0
- coord/dashboard/webapp/components.json +17 -0
- coord/dashboard/webapp/dist/assets/Gallery-da3qNiIw.js +71 -0
- coord/dashboard/webapp/dist/assets/Terminal-9CEnUXvW.css +32 -0
- coord/dashboard/webapp/dist/assets/Terminal-skVFCxPU.js +63 -0
- coord/dashboard/webapp/dist/assets/index-DltfZR5f.js +184 -0
- coord/dashboard/webapp/dist/assets/index-Dq4kwTdw.css +1 -0
- coord/dashboard/webapp/dist/assets/workbox-window.prod.es5-BqEJf4Xk.js +2 -0
- coord/dashboard/webapp/dist/icons/icon-192.png +0 -0
- coord/dashboard/webapp/dist/icons/icon-512.png +0 -0
- coord/dashboard/webapp/dist/icons/icon.svg +5 -0
- coord/dashboard/webapp/dist/index.html +38 -0
- coord/dashboard/webapp/dist/manifest.webmanifest +1 -0
- coord/dashboard/webapp/dist/sw.js +1 -0
- coord/dashboard/webapp/dist/workbox-e4022e15.js +1 -0
- coord/dashboard/webapp/e2e/available-gates-terminal.spec.ts +75 -0
- coord/dashboard/webapp/e2e/deep-link.spec.ts +172 -0
- coord/dashboard/webapp/e2e/fixtureServer.ts +155 -0
- coord/dashboard/webapp/e2e/live-update-fixture.spec.ts +113 -0
- coord/dashboard/webapp/e2e/realtime.spec.ts +238 -0
- coord/dashboard/webapp/e2e/shell.spec.ts +309 -0
- coord/dashboard/webapp/e2e/smoke.spec.ts +191 -0
- coord/dashboard/webapp/e2e/terminal.spec.ts +420 -0
- coord/dashboard/webapp/e2e/theme.spec.ts +138 -0
- coord/dashboard/webapp/eslint.config.js +20 -0
- coord/dashboard/webapp/index.html +37 -0
- coord/dashboard/webapp/node_modules/flatted/python/flatted.py +144 -0
- coord/dashboard/webapp/package-lock.json +10584 -0
- coord/dashboard/webapp/package.json +63 -0
- coord/dashboard/webapp/playwright.acceptance.config.ts +166 -0
- coord/dashboard/webapp/playwright.config.ts +93 -0
- coord/dashboard/webapp/postcss.config.js +6 -0
- coord/dashboard/webapp/public/icons/icon-192.png +0 -0
- coord/dashboard/webapp/public/icons/icon-512.png +0 -0
- coord/dashboard/webapp/public/icons/icon.svg +5 -0
- coord/dashboard/webapp/src/App.tsx +140 -0
- coord/dashboard/webapp/src/api/client.ts +199 -0
- coord/dashboard/webapp/src/api/generated.ts +176 -0
- coord/dashboard/webapp/src/components/ConnectionBadge.tsx +52 -0
- coord/dashboard/webapp/src/components/Detail.tsx +800 -0
- coord/dashboard/webapp/src/components/Gallery.tsx +341 -0
- coord/dashboard/webapp/src/components/Home.tsx +435 -0
- coord/dashboard/webapp/src/components/MobileKeyBar.tsx +280 -0
- coord/dashboard/webapp/src/components/PanelHeader.tsx +59 -0
- coord/dashboard/webapp/src/components/PipelineCard.tsx +168 -0
- coord/dashboard/webapp/src/components/SessionCard.tsx +99 -0
- coord/dashboard/webapp/src/components/SessionDetail.tsx +140 -0
- coord/dashboard/webapp/src/components/SessionsList.tsx +81 -0
- coord/dashboard/webapp/src/components/Terminal.tsx +376 -0
- coord/dashboard/webapp/src/components/__tests__/ConnectionBadge.test.tsx +81 -0
- coord/dashboard/webapp/src/components/__tests__/Detail.test.tsx +680 -0
- coord/dashboard/webapp/src/components/__tests__/Gallery.test.tsx +83 -0
- coord/dashboard/webapp/src/components/__tests__/Home.test.tsx +271 -0
- coord/dashboard/webapp/src/components/__tests__/MobileKeyBar.test.tsx +197 -0
- coord/dashboard/webapp/src/components/__tests__/PipelineCard.test.tsx +143 -0
- coord/dashboard/webapp/src/components/__tests__/SessionCard.test.tsx +106 -0
- coord/dashboard/webapp/src/components/__tests__/Terminal.test.tsx +504 -0
- coord/dashboard/webapp/src/components/ui/badge.tsx +41 -0
- coord/dashboard/webapp/src/components/ui/button.tsx +54 -0
- coord/dashboard/webapp/src/components/ui/card.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/dialog.tsx +99 -0
- coord/dashboard/webapp/src/components/ui/dropdown-menu.tsx +189 -0
- coord/dashboard/webapp/src/components/ui/empty-state.tsx +35 -0
- coord/dashboard/webapp/src/components/ui/sheet.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/skeleton.tsx +9 -0
- coord/dashboard/webapp/src/components/ui/tabs.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/theme-provider.tsx +78 -0
- coord/dashboard/webapp/src/components/ui/theme-toggle.tsx +20 -0
- coord/dashboard/webapp/src/components/ui/toast.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/toaster.tsx +30 -0
- coord/dashboard/webapp/src/components/ui/tooltip.tsx +26 -0
- coord/dashboard/webapp/src/components/ui/use-toast.ts +134 -0
- coord/dashboard/webapp/src/index.css +210 -0
- coord/dashboard/webapp/src/lib/pipeline.ts +29 -0
- coord/dashboard/webapp/src/lib/utils.ts +6 -0
- coord/dashboard/webapp/src/main.tsx +46 -0
- coord/dashboard/webapp/src/realtime/RealtimeProvider.tsx +112 -0
- coord/dashboard/webapp/src/realtime/__tests__/RealtimeProvider.test.tsx +189 -0
- coord/dashboard/webapp/src/realtime/__tests__/connection.test.ts +255 -0
- coord/dashboard/webapp/src/realtime/connection.ts +227 -0
- coord/dashboard/webapp/src/realtime/events.ts +100 -0
- coord/dashboard/webapp/src/routes/__tests__/paths.test.ts +92 -0
- coord/dashboard/webapp/src/routes/paths.ts +92 -0
- coord/dashboard/webapp/src/shell/ActivityRail.tsx +335 -0
- coord/dashboard/webapp/src/shell/AppShell.tsx +276 -0
- coord/dashboard/webapp/src/shell/ComingSoon.tsx +33 -0
- coord/dashboard/webapp/src/shell/EmptyDetail.tsx +26 -0
- coord/dashboard/webapp/src/shell/RouteNotFound.tsx +33 -0
- coord/dashboard/webapp/src/shell/ShellLayout.tsx +147 -0
- coord/dashboard/webapp/src/shell/StatusBar.tsx +46 -0
- coord/dashboard/webapp/src/shell/__tests__/ShellLayout.test.tsx +520 -0
- coord/dashboard/webapp/src/shell/__tests__/shellState.test.ts +95 -0
- coord/dashboard/webapp/src/shell/__tests__/stubViewport.ts +40 -0
- coord/dashboard/webapp/src/shell/breakpoints.ts +87 -0
- coord/dashboard/webapp/src/shell/railItems.ts +105 -0
- coord/dashboard/webapp/src/shell/shellState.ts +174 -0
- coord/dashboard/webapp/src/shell/useRegionFocus.ts +95 -0
- coord/dashboard/webapp/src/test-setup.ts +41 -0
- coord/dashboard/webapp/src/vite-env.d.ts +2 -0
- coord/dashboard/webapp/tailwind.config.js +140 -0
- coord/dashboard/webapp/tsconfig.json +25 -0
- coord/dashboard/webapp/tsconfig.node.json +11 -0
- coord/dashboard/webapp/vite.config.ts +71 -0
- coord/db.py +1076 -0
- coord/dead_end.py +332 -0
- coord/deploy/README.md +33 -0
- coord/deploy/coord-agent.service +89 -0
- coord/deploy/coord-db-backup.service +60 -0
- coord/deploy/coord-db-backup.sh +74 -0
- coord/deploy/coord-db-backup.timer +18 -0
- coord/deploy/coord-drive-queue.service +117 -0
- coord/deploy/coord-drive-queue.timer +39 -0
- coord/deploy/coord-notify.service +48 -0
- coord/deploy/coord-notify.timer +24 -0
- coord/deploy/coord-release-propagate.service +83 -0
- coord/deploy/coord-release-propagate.timer +38 -0
- coord/deploy/coord-release-window.service +119 -0
- coord/deploy/coord-release-window.timer +36 -0
- coord/deploy/coord-serve.service +82 -0
- coord/deploy/coord-web-dist-build.service +43 -0
- coord/deploy/coord-web-dist-build.timer +36 -0
- coord/deploy/coord-web.service +125 -0
- coord/deploy_manifest.py +80 -0
- coord/deploy_units.py +384 -0
- coord/deps.py +115 -0
- coord/diagnose.py +1623 -0
- coord/dispatch.py +1009 -0
- coord/dist_name.py +123 -0
- coord/drive.py +3101 -0
- coord/drive_queue.py +2298 -0
- coord/drive_state.py +870 -0
- coord/events.py +381 -0
- coord/failure_class.py +914 -0
- coord/filelock.py +168 -0
- coord/fleet_config_health.py +300 -0
- coord/freshness.py +206 -0
- coord/gate_a.py +469 -0
- coord/gate_b.py +411 -0
- coord/gate_snapshot.py +385 -0
- coord/gates.py +582 -0
- coord/github_ops.py +1954 -0
- coord/goal.py +125 -0
- coord/graph_health.py +348 -0
- coord/health/__init__.py +69 -0
- coord/health/aggregate.py +129 -0
- coord/health/checks/__init__.py +13 -0
- coord/health/checks/agent_install.py +280 -0
- coord/health/checks/cargo_targets.py +171 -0
- coord/health/checks/claude_binary.py +65 -0
- coord/health/checks/deploy_lane_facts.py +458 -0
- coord/health/checks/disk.py +99 -0
- coord/health/checks/fleet_board.py +89 -0
- coord/health/checks/fleet_deploy_lanes.py +469 -0
- coord/health/checks/fleet_phantom.py +69 -0
- coord/health/checks/fleet_unit_drift.py +151 -0
- coord/health/checks/graph.py +192 -0
- coord/health/checks/plan_usage.py +88 -0
- coord/health/checks/repo_state.py +161 -0
- coord/health/checks/spawned_coord.py +465 -0
- coord/health/checks/timer_active.py +254 -0
- coord/health/checks/toolchain.py +547 -0
- coord/health/checks/unit_drift.py +648 -0
- coord/health/checks/unit_enablement.py +171 -0
- coord/health/checks/worktrees.py +96 -0
- coord/health/cli.py +121 -0
- coord/health/context.py +106 -0
- coord/health/fleet_snapshot.py +477 -0
- coord/health/models.py +250 -0
- coord/health/pypi.py +231 -0
- coord/health/registry.py +240 -0
- coord/health/render.py +82 -0
- coord/health/units.py +60 -0
- coord/hooks.py +106 -0
- coord/housekeeping.py +204 -0
- coord/interactive.py +4286 -0
- coord/issue_store.py +1496 -0
- coord/liveness_auditor.py +293 -0
- coord/machine_pause.py +755 -0
- coord/merge_queue.py +4681 -0
- coord/milestone_chat.py +600 -0
- coord/milestone_dispatch.py +943 -0
- coord/milestone_gate.py +709 -0
- coord/milestone_order.py +840 -0
- coord/mock_author.py +334 -0
- coord/models.py +891 -0
- coord/network.py +269 -0
- coord/new_issue_chat.py +229 -0
- coord/notify.py +3226 -0
- coord/openapi.py +404 -0
- coord/overlap_fence.py +133 -0
- coord/parentage.py +200 -0
- coord/parentage_github.py +58 -0
- coord/pipeline.py +481 -0
- coord/plan_parser.py +266 -0
- coord/plans.py +543 -0
- coord/platform_paths.py +43 -0
- coord/pr_body_lint.py +67 -0
- coord/prereqs.py +533 -0
- coord/progress.py +425 -0
- coord/providers/__init__.py +683 -0
- coord/providers/base.py +218 -0
- coord/providers/claude.py +284 -0
- coord/providers/claude_pty.py +610 -0
- coord/providers/opencode.py +896 -0
- coord/reconcile.py +2233 -0
- coord/refine_chat.py +485 -0
- coord/release_cordon.py +525 -0
- coord/release_propagate.py +1176 -0
- coord/release_verify.py +777 -0
- coord/release_window.py +322 -0
- coord/reports.py +1643 -0
- coord/revalidate.py +1101 -0
- coord/review.py +3317 -0
- coord/scorecard.py +484 -0
- coord/serve_app.py +7192 -0
- coord/skills/update-issue/SKILL.md +93 -0
- coord/smoke.py +1030 -0
- coord/split_work.py +210 -0
- coord/stage_projection.py +650 -0
- coord/state.py +5720 -0
- coord/test_author.py +1064 -0
- coord/test_chat.py +352 -0
- coord/test_orchestrator.py +494 -0
- coord/test_report.py +178 -0
- coord/tui_release.py +271 -0
- coord/usage.py +753 -0
- coord/usage_limits.py +358 -0
- coord/usage_rollup.py +709 -0
- coord/worker_events.py +954 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
"""Toolchain versions: per-machine facts, and fleet-scope skew (#1629, H-2).
|
|
2
|
+
|
|
3
|
+
**The finding.** Measured 2026-07-30 diagnosing vimcode#615: CI
|
|
4
|
+
(``dtolnay/rust-toolchain@stable``) built with rustc 1.97.1 while the three
|
|
5
|
+
fleet machines that can run vimcode's tests were on 1.95.0, 1.95.0, and
|
|
6
|
+
1.93.1 — five months stale. Six ``insta`` render snapshots passed on every
|
|
7
|
+
fleet machine and failed in CI. The recorded Test verdict was ``passed``;
|
|
8
|
+
the truth was "the code does not build on a current toolchain," and the gate
|
|
9
|
+
had been silently green for months because nothing measured *which*
|
|
10
|
+
toolchain produced a `passed`.
|
|
11
|
+
|
|
12
|
+
**Annotate, don't gate — locked 2026-07-31 (operator).** This module does
|
|
13
|
+
two independent things, and neither one blocks a dispatch, a routing
|
|
14
|
+
decision, or a merge:
|
|
15
|
+
|
|
16
|
+
1. :func:`probe_toolchain_versions` (machine scope) — what's actually
|
|
17
|
+
installed on THIS box for the toolchains its local checkouts need. Pure
|
|
18
|
+
fact-gathering, like ``agent_venv``: always OK when detected, UNKNOWN
|
|
19
|
+
when it can't be.
|
|
20
|
+
2. :func:`probe_toolchain_skew` (fleet scope) — reads every machine's #1
|
|
21
|
+
results (fed through the daemon's health-poll tick, same "gather in the
|
|
22
|
+
context, judge in the probe" split as ``fleet_deploy_lanes``) plus the CI
|
|
23
|
+
pin the daemon-host facts read out of the repo's own workflow YAML, and
|
|
24
|
+
turns disagreement into a severity: all equal = OK, any machine differs
|
|
25
|
+
from another = WARN, a machine differs from CI = CRIT.
|
|
26
|
+
|
|
27
|
+
Gating on this is explicitly out of scope (a follow-up issue, once the data
|
|
28
|
+
says something) — see ``tests/test_fleet_health_probes.py``'s
|
|
29
|
+
``test_toolchain_skew_crit_never_touches_dispatch_routing_or_merge`` and the
|
|
30
|
+
structural guarantee in ``tests/test_fleet_health_snapshot.py``.
|
|
31
|
+
|
|
32
|
+
**Table-driven, not ``rustc --version`` hardcoded (scope item 3).** A new
|
|
33
|
+
interpreter/SDK a repo's tests depend on is a new :data:`TOOLCHAIN_SPECS`
|
|
34
|
+
entry — the detection, the CI-pin parser, and the skew comparison all key
|
|
35
|
+
off the table, not off a toolchain-specific code path.
|
|
36
|
+
|
|
37
|
+
Everything here is fail-soft toward UNKNOWN: a version this module could not
|
|
38
|
+
determine is "we don't know," never fabricated as "matches" or "differs."
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import re
|
|
44
|
+
import subprocess
|
|
45
|
+
from dataclasses import dataclass
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
|
|
48
|
+
import yaml
|
|
49
|
+
|
|
50
|
+
from coord.health.models import CheckResult, HealthContext, Severity
|
|
51
|
+
from coord.health.registry import check
|
|
52
|
+
|
|
53
|
+
VERSIONS_CHECK_ID = "toolchain_versions"
|
|
54
|
+
SKEW_CHECK_ID = "fleet_toolchain_skew"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class ToolchainSpec:
|
|
59
|
+
"""One row of the table #1629 scope item 3 asks for."""
|
|
60
|
+
|
|
61
|
+
kind: str
|
|
62
|
+
# argv to run locally to get a version string, e.g. ("rustc", "--version").
|
|
63
|
+
version_argv: tuple[str, ...]
|
|
64
|
+
# Regex whose first group is the dotted version number.
|
|
65
|
+
version_regex: str
|
|
66
|
+
# A checkout needs this kind if any of these subdirectories exist...
|
|
67
|
+
dir_markers: tuple[str, ...] = ()
|
|
68
|
+
# ...or the checkout ROOT has any of these marker files (a repo that IS
|
|
69
|
+
# that language, e.g. vimcode/quadraui's root Cargo.toml, vs.
|
|
70
|
+
# claude-coordinator's nested tui/ subdirectory).
|
|
71
|
+
root_markers: tuple[str, ...] = ()
|
|
72
|
+
# GitHub Actions step(s) (the `uses:` name, before the `@`) that install
|
|
73
|
+
# this toolchain in CI.
|
|
74
|
+
ci_actions: tuple[str, ...] = ()
|
|
75
|
+
# The `with:` key on that action carrying a literal version pin, e.g.
|
|
76
|
+
# "python-version" / "node-version" / "toolchain".
|
|
77
|
+
ci_version_key: str = ""
|
|
78
|
+
# `with:`/`@ref` values that mean "CI floats, no literal pin to read" —
|
|
79
|
+
# dtolnay/rust-toolchain@stable being the textbook case from #1629 itself.
|
|
80
|
+
ci_floating_values: tuple[str, ...] = ("stable", "beta", "nightly")
|
|
81
|
+
# True only for actions where the `@ref` itself IS the toolchain version
|
|
82
|
+
# (``dtolnay/rust-toolchain@1.97.1``) — the ref-as-version fallback in
|
|
83
|
+
# ``_parse_ci_pin`` is gated on this. Actions like ``actions/setup-python``
|
|
84
|
+
# / ``actions/setup-node`` version their OWN release tag in `@ref`
|
|
85
|
+
# (``actions/setup-node@v4``), which has nothing to do with the
|
|
86
|
+
# interpreter version installed — falling back to it fabricates a pin
|
|
87
|
+
# that was never configured. Default False; only the rust spec sets it.
|
|
88
|
+
ci_ref_is_version: bool = False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Scope item 1's starting table: "rustc/cargo for tui/** and the Rust repos,
|
|
92
|
+
# python for the coord suite, node for coord/dashboard/webapp/**".
|
|
93
|
+
TOOLCHAIN_SPECS: tuple[ToolchainSpec, ...] = (
|
|
94
|
+
ToolchainSpec(
|
|
95
|
+
kind="rustc",
|
|
96
|
+
version_argv=("rustc", "--version"),
|
|
97
|
+
version_regex=r"rustc (\d+(?:\.\d+){1,2})",
|
|
98
|
+
dir_markers=("tui",),
|
|
99
|
+
root_markers=("Cargo.toml",),
|
|
100
|
+
ci_actions=("dtolnay/rust-toolchain", "actions-rs/toolchain"),
|
|
101
|
+
ci_version_key="toolchain",
|
|
102
|
+
ci_ref_is_version=True,
|
|
103
|
+
),
|
|
104
|
+
ToolchainSpec(
|
|
105
|
+
kind="python",
|
|
106
|
+
version_argv=("python3", "--version"),
|
|
107
|
+
version_regex=r"Python (\d+(?:\.\d+){1,2})",
|
|
108
|
+
dir_markers=("coord",),
|
|
109
|
+
root_markers=("pyproject.toml", "setup.py"),
|
|
110
|
+
ci_actions=("actions/setup-python",),
|
|
111
|
+
ci_version_key="python-version",
|
|
112
|
+
ci_floating_values=(),
|
|
113
|
+
),
|
|
114
|
+
ToolchainSpec(
|
|
115
|
+
kind="node",
|
|
116
|
+
version_argv=("node", "--version"),
|
|
117
|
+
version_regex=r"v?(\d+(?:\.\d+){0,2})",
|
|
118
|
+
dir_markers=("coord/dashboard/webapp",),
|
|
119
|
+
root_markers=("package.json",),
|
|
120
|
+
ci_actions=("actions/setup-node",),
|
|
121
|
+
ci_version_key="node-version",
|
|
122
|
+
ci_floating_values=(),
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ── applicability: does a checkout's test suite depend on this kind? ────────
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _spec_applies(checkout_path: Path, spec: ToolchainSpec) -> bool:
|
|
131
|
+
for marker in spec.dir_markers:
|
|
132
|
+
if (checkout_path / marker).is_dir():
|
|
133
|
+
return True
|
|
134
|
+
for marker in spec.root_markers:
|
|
135
|
+
if (checkout_path / marker).is_file():
|
|
136
|
+
return True
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def repo_toolchain_kinds(checkout_path: Path) -> list[str]:
|
|
141
|
+
"""Which :data:`TOOLCHAIN_SPECS` kinds *checkout_path*'s tests plausibly
|
|
142
|
+
depend on — table-driven, per #1629 scope item 3."""
|
|
143
|
+
return [spec.kind for spec in TOOLCHAIN_SPECS if _spec_applies(checkout_path, spec)]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ── local version detection (machine-scope fact gathering) ──────────────────
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _parse_version(raw: str, regex: str) -> str | None:
|
|
150
|
+
m = re.search(regex, raw)
|
|
151
|
+
return m.group(1) if m else None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def detect_local_version(spec: ToolchainSpec, *, timeout: float = 5.0) -> str | None:
|
|
155
|
+
"""Run ``<spec.version_argv>`` on THIS machine and extract a version.
|
|
156
|
+
|
|
157
|
+
None — never raised, never fabricated — for a missing binary, a
|
|
158
|
+
non-zero exit, unparseable output, or a timeout.
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
result = subprocess.run(
|
|
162
|
+
list(spec.version_argv),
|
|
163
|
+
capture_output=True,
|
|
164
|
+
text=True,
|
|
165
|
+
timeout=timeout,
|
|
166
|
+
)
|
|
167
|
+
except (OSError, subprocess.SubprocessError):
|
|
168
|
+
return None
|
|
169
|
+
if result.returncode != 0:
|
|
170
|
+
return None
|
|
171
|
+
return _parse_version(result.stdout + result.stderr, spec.version_regex)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def local_toolchain_label(checkout_path: Path) -> str | None:
|
|
175
|
+
"""``"rustc 1.95.0"`` / ``"python 3.12.4, node 20.11.0"`` for whatever
|
|
176
|
+
*checkout_path*'s tests depend on — used to annotate a Test verdict
|
|
177
|
+
recorded on THIS machine (#1629 scope item 2). ``None`` when no known
|
|
178
|
+
toolchain applies, or none could be detected; a caller records the
|
|
179
|
+
verdict either way, just without this optional annotation.
|
|
180
|
+
"""
|
|
181
|
+
parts: list[str] = []
|
|
182
|
+
for spec in TOOLCHAIN_SPECS:
|
|
183
|
+
if not _spec_applies(checkout_path, spec):
|
|
184
|
+
continue
|
|
185
|
+
version = detect_local_version(spec)
|
|
186
|
+
if version:
|
|
187
|
+
parts.append(f"{spec.kind} {version}")
|
|
188
|
+
return ", ".join(parts) or None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def toolchain_label_for_machine(
|
|
192
|
+
health_block: dict | None, kinds: "list[str] | None" = None
|
|
193
|
+
) -> str | None:
|
|
194
|
+
"""Compact label built from one machine's last-known H-1 report block
|
|
195
|
+
(``{"results": [...]}``, the same shape ``fleet_snapshot`` stores per
|
|
196
|
+
machine) — restricted to *kinds* when given. Used by a caller recording
|
|
197
|
+
a verdict for a REMOTE machine's test run, reusing the H-1 tick's
|
|
198
|
+
already-gathered facts instead of shelling out again.
|
|
199
|
+
"""
|
|
200
|
+
if not health_block:
|
|
201
|
+
return None
|
|
202
|
+
results = health_block.get("results") or []
|
|
203
|
+
parts: list[str] = []
|
|
204
|
+
for r in results:
|
|
205
|
+
if not isinstance(r, dict) or r.get("check_id") != VERSIONS_CHECK_ID:
|
|
206
|
+
continue
|
|
207
|
+
subject = r.get("subject")
|
|
208
|
+
if kinds is not None and subject not in kinds:
|
|
209
|
+
continue
|
|
210
|
+
version = (r.get("values") or {}).get("version")
|
|
211
|
+
if subject and version:
|
|
212
|
+
parts.append(f"{subject} {version}")
|
|
213
|
+
return ", ".join(parts) or None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@check(
|
|
217
|
+
id=VERSIONS_CHECK_ID,
|
|
218
|
+
scope="machine",
|
|
219
|
+
title="toolchain",
|
|
220
|
+
order=45,
|
|
221
|
+
description=(
|
|
222
|
+
"Installed versions of the toolchains this machine's local "
|
|
223
|
+
"checkouts need (rustc/python/node...). Pure fact-gathering — feeds "
|
|
224
|
+
"the fleet_toolchain_skew judgement, says nothing about skew itself."
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
def probe_toolchain_versions(ctx: HealthContext) -> list[CheckResult] | None:
|
|
228
|
+
needed: dict[str, ToolchainSpec] = {}
|
|
229
|
+
for checkout in ctx.checkouts:
|
|
230
|
+
for spec in TOOLCHAIN_SPECS:
|
|
231
|
+
if spec.kind not in needed and _spec_applies(checkout.path, spec):
|
|
232
|
+
needed[spec.kind] = spec
|
|
233
|
+
if not needed:
|
|
234
|
+
return None # nothing this machine's checkouts need — silence beats a green line
|
|
235
|
+
|
|
236
|
+
results: list[CheckResult] = []
|
|
237
|
+
for kind, spec in sorted(needed.items()):
|
|
238
|
+
version = detect_local_version(spec)
|
|
239
|
+
if version is None:
|
|
240
|
+
results.append(
|
|
241
|
+
CheckResult(
|
|
242
|
+
check_id=VERSIONS_CHECK_ID,
|
|
243
|
+
scope="machine",
|
|
244
|
+
severity=Severity.UNKNOWN,
|
|
245
|
+
subject=kind,
|
|
246
|
+
headroom=f"{' '.join(spec.version_argv)} could not be run",
|
|
247
|
+
detail=(
|
|
248
|
+
"a local checkout needs this toolchain but it could "
|
|
249
|
+
"not be detected on this machine"
|
|
250
|
+
),
|
|
251
|
+
values={"argv": list(spec.version_argv)},
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
else:
|
|
255
|
+
results.append(
|
|
256
|
+
CheckResult(
|
|
257
|
+
check_id=VERSIONS_CHECK_ID,
|
|
258
|
+
scope="machine",
|
|
259
|
+
severity=Severity.OK,
|
|
260
|
+
subject=kind,
|
|
261
|
+
headroom=version,
|
|
262
|
+
values={"version": version},
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
return results
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ── CI facts: parse pinned versions out of workflow YAML (daemon-host-local,
|
|
269
|
+
# no network — see coord.health.fleet_snapshot._daemon_host_facts) ───────
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _iter_workflow_steps(checkout_path: Path):
|
|
273
|
+
workflows_dir = checkout_path / ".github" / "workflows"
|
|
274
|
+
if not workflows_dir.is_dir():
|
|
275
|
+
return
|
|
276
|
+
try:
|
|
277
|
+
files = sorted(workflows_dir.glob("*.yml")) + sorted(workflows_dir.glob("*.yaml"))
|
|
278
|
+
except OSError:
|
|
279
|
+
return
|
|
280
|
+
for wf_file in files:
|
|
281
|
+
try:
|
|
282
|
+
doc = yaml.safe_load(wf_file.read_text(encoding="utf-8", errors="replace"))
|
|
283
|
+
except (OSError, yaml.YAMLError):
|
|
284
|
+
continue
|
|
285
|
+
if not isinstance(doc, dict):
|
|
286
|
+
continue
|
|
287
|
+
jobs = doc.get("jobs")
|
|
288
|
+
if not isinstance(jobs, dict):
|
|
289
|
+
continue
|
|
290
|
+
for job in jobs.values():
|
|
291
|
+
if not isinstance(job, dict):
|
|
292
|
+
continue
|
|
293
|
+
for step in job.get("steps") or []:
|
|
294
|
+
if isinstance(step, dict):
|
|
295
|
+
yield step
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _version_tuple(s: str) -> tuple[int, ...] | None:
|
|
299
|
+
parts = re.findall(r"\d+", s or "")
|
|
300
|
+
return tuple(int(p) for p in parts) if parts else None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def ci_matches_machine(ci_version: str, machine_version: str) -> bool:
|
|
304
|
+
"""True when *machine_version* satisfies a (possibly less precise)
|
|
305
|
+
*ci_version* pin.
|
|
306
|
+
|
|
307
|
+
CI configs commonly name a MINOR version (``python-version: '3.12'``)
|
|
308
|
+
while a local ``--version`` reports the full patch triplet
|
|
309
|
+
(``"3.12.4"``) — that's still "running what CI runs", so the comparison
|
|
310
|
+
is a component-wise PREFIX match at CI's own precision, not string
|
|
311
|
+
equality. A toolchain CI pins to full precision (rustc's 3-part
|
|
312
|
+
``1.97.1``) still requires every one of those components to match.
|
|
313
|
+
"""
|
|
314
|
+
ci_t = _version_tuple(ci_version)
|
|
315
|
+
m_t = _version_tuple(machine_version)
|
|
316
|
+
if not ci_t or not m_t or len(m_t) < len(ci_t):
|
|
317
|
+
return False
|
|
318
|
+
return m_t[: len(ci_t)] == ci_t
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _parse_ci_pin(spec: ToolchainSpec, step: dict) -> str | None:
|
|
322
|
+
uses = str(step.get("uses") or "")
|
|
323
|
+
if "@" not in uses:
|
|
324
|
+
return None
|
|
325
|
+
action_name, action_ref = uses.split("@", 1)
|
|
326
|
+
if action_name not in spec.ci_actions:
|
|
327
|
+
return None
|
|
328
|
+
with_block = step.get("with")
|
|
329
|
+
pinned = with_block.get(spec.ci_version_key) if isinstance(with_block, dict) else None
|
|
330
|
+
if pinned is not None:
|
|
331
|
+
pinned_s = str(pinned).strip().strip("'\"")
|
|
332
|
+
if pinned_s and pinned_s not in spec.ci_floating_values and _version_tuple(pinned_s):
|
|
333
|
+
return pinned_s
|
|
334
|
+
return None # explicit but floating ("stable") or unparseable — honestly unknown
|
|
335
|
+
if not spec.ci_ref_is_version:
|
|
336
|
+
# No `with:` pin, and this action's `@ref` is the ACTION's own release
|
|
337
|
+
# tag (e.g. actions/setup-node@v4), not the toolchain version — that's
|
|
338
|
+
# the common "default runner version" / ".nvmrc-driven" CI pattern,
|
|
339
|
+
# honestly unknown, never fabricated from the action's tag (#1629 fix).
|
|
340
|
+
return None
|
|
341
|
+
action_ref = action_ref.strip()
|
|
342
|
+
if action_ref and action_ref not in spec.ci_floating_values and _version_tuple(action_ref):
|
|
343
|
+
return action_ref
|
|
344
|
+
return None # no `with:` pin and the @ref is a floating channel (or a SHA) — unknown
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def ci_toolchain_versions(checkout_path: Path) -> dict[str, str | None]:
|
|
348
|
+
"""kind -> the version CI pins, or None when CI floats / doesn't
|
|
349
|
+
configure that toolchain at all.
|
|
350
|
+
|
|
351
|
+
Local-filesystem-only (workflow YAML already on disk) — no ``gh`` call,
|
|
352
|
+
cheap enough for the daemon's health-poll tick. A floating pin
|
|
353
|
+
(``dtolnay/rust-toolchain@stable``, #1629's own motivating example) is
|
|
354
|
+
honestly ``None``: the version it actually resolves to on a given day
|
|
355
|
+
can only be read from a live CI run, which this function does not fetch.
|
|
356
|
+
"""
|
|
357
|
+
out: dict[str, str | None] = {spec.kind: None for spec in TOOLCHAIN_SPECS}
|
|
358
|
+
for step in _iter_workflow_steps(checkout_path):
|
|
359
|
+
for spec in TOOLCHAIN_SPECS:
|
|
360
|
+
if out[spec.kind] is not None:
|
|
361
|
+
continue
|
|
362
|
+
pin = _parse_ci_pin(spec, step)
|
|
363
|
+
if pin:
|
|
364
|
+
out[spec.kind] = pin
|
|
365
|
+
return out
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# ── fleet-scope skew judgement ───────────────────────────────────────────────
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _machine_toolchain_versions(ctx: HealthContext) -> dict[str, dict[str, str]]:
|
|
372
|
+
"""machine_name -> {kind: version}, read from each machine's last-known
|
|
373
|
+
H-1 report (the same ``checks.results`` block ``fleet_deploy_lanes``
|
|
374
|
+
reads for ``agent_venv``)."""
|
|
375
|
+
out: dict[str, dict[str, str]] = {}
|
|
376
|
+
if ctx.fleet is None:
|
|
377
|
+
return out
|
|
378
|
+
for name, entry in ctx.fleet.machines.items():
|
|
379
|
+
checks = (entry or {}).get("checks") or {}
|
|
380
|
+
versions: dict[str, str] = {}
|
|
381
|
+
for r in checks.get("results", []) or []:
|
|
382
|
+
if not isinstance(r, dict) or r.get("check_id") != VERSIONS_CHECK_ID:
|
|
383
|
+
continue
|
|
384
|
+
if r.get("error"):
|
|
385
|
+
continue
|
|
386
|
+
subject = r.get("subject")
|
|
387
|
+
version = (r.get("values") or {}).get("version")
|
|
388
|
+
if subject and version:
|
|
389
|
+
versions[subject] = version
|
|
390
|
+
out[name] = versions
|
|
391
|
+
return out
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _by_version(versions: dict[str, str]) -> dict[str, list[str]]:
|
|
395
|
+
out: dict[str, list[str]] = {}
|
|
396
|
+
for machine, v in versions.items():
|
|
397
|
+
out.setdefault(v, []).append(machine)
|
|
398
|
+
return out
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _format_by_version(by_version: dict[str, list[str]]) -> str:
|
|
402
|
+
return "; ".join(f"{v}: {', '.join(sorted(ms))}" for v, ms in sorted(by_version.items()))
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@check(
|
|
406
|
+
id=SKEW_CHECK_ID,
|
|
407
|
+
scope="fleet",
|
|
408
|
+
title="toolchain skew",
|
|
409
|
+
order=15,
|
|
410
|
+
description=(
|
|
411
|
+
"Do all machines that can run a repo's tests agree on its "
|
|
412
|
+
"toolchain, and does that agree with CI? #1629: annotate-only — "
|
|
413
|
+
"this NEVER gates dispatch, routing, or merge."
|
|
414
|
+
),
|
|
415
|
+
)
|
|
416
|
+
def probe_toolchain_skew(ctx: HealthContext) -> list[CheckResult] | CheckResult | None:
|
|
417
|
+
if ctx.fleet is None:
|
|
418
|
+
return CheckResult(
|
|
419
|
+
check_id=SKEW_CHECK_ID,
|
|
420
|
+
scope="fleet",
|
|
421
|
+
severity=Severity.UNKNOWN,
|
|
422
|
+
headroom="no fleet snapshot (fleet checks only run on the daemon)",
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
dh = ctx.fleet.daemon_host or {}
|
|
426
|
+
repo_kinds: dict[str, list[str]] = dh.get("repo_toolchain_kinds") or {}
|
|
427
|
+
ci_versions: dict[str, dict[str, str | None]] = dh.get("ci_toolchains") or {}
|
|
428
|
+
if not repo_kinds:
|
|
429
|
+
return CheckResult(
|
|
430
|
+
check_id=SKEW_CHECK_ID,
|
|
431
|
+
scope="fleet",
|
|
432
|
+
severity=Severity.UNKNOWN,
|
|
433
|
+
headroom="no repo has a resolvable toolchain from the daemon host's local checkouts",
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
machine_versions = _machine_toolchain_versions(ctx)
|
|
437
|
+
config = ctx.config
|
|
438
|
+
machines_by_repo: dict[str, list[str]] = {}
|
|
439
|
+
for m in getattr(config, "machines", ()) or ():
|
|
440
|
+
for repo_name in getattr(m, "repo_paths", None) or {}:
|
|
441
|
+
machines_by_repo.setdefault(repo_name, []).append(m.name)
|
|
442
|
+
|
|
443
|
+
results: list[CheckResult] = []
|
|
444
|
+
for repo_name, kinds in sorted(repo_kinds.items()):
|
|
445
|
+
candidate_machines = sorted(set(machines_by_repo.get(repo_name, ())))
|
|
446
|
+
for kind in kinds:
|
|
447
|
+
subject = f"{repo_name}:{kind}"
|
|
448
|
+
|
|
449
|
+
if not candidate_machines:
|
|
450
|
+
results.append(
|
|
451
|
+
CheckResult(
|
|
452
|
+
check_id=SKEW_CHECK_ID,
|
|
453
|
+
scope="fleet",
|
|
454
|
+
severity=Severity.UNKNOWN,
|
|
455
|
+
subject=subject,
|
|
456
|
+
headroom="no machine advertises a checkout of this repo",
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
continue
|
|
460
|
+
|
|
461
|
+
versions_by_machine = {
|
|
462
|
+
m: machine_versions.get(m, {}).get(kind) for m in candidate_machines
|
|
463
|
+
}
|
|
464
|
+
known = {m: v for m, v in versions_by_machine.items() if v}
|
|
465
|
+
missing = sorted(m for m, v in versions_by_machine.items() if not v)
|
|
466
|
+
ci_version = (ci_versions.get(repo_name) or {}).get(kind)
|
|
467
|
+
|
|
468
|
+
if not known:
|
|
469
|
+
results.append(
|
|
470
|
+
CheckResult(
|
|
471
|
+
check_id=SKEW_CHECK_ID,
|
|
472
|
+
scope="fleet",
|
|
473
|
+
severity=Severity.UNKNOWN,
|
|
474
|
+
subject=subject,
|
|
475
|
+
headroom=f"no {kind} version data yet",
|
|
476
|
+
detail=f"no data for: {', '.join(missing)}" if missing else "",
|
|
477
|
+
)
|
|
478
|
+
)
|
|
479
|
+
continue
|
|
480
|
+
|
|
481
|
+
distinct = sorted(set(known.values()))
|
|
482
|
+
ci_mismatches = (
|
|
483
|
+
sorted(m for m, v in known.items() if not ci_matches_machine(ci_version, v))
|
|
484
|
+
if ci_version
|
|
485
|
+
else []
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
if ci_version and ci_mismatches:
|
|
489
|
+
results.append(
|
|
490
|
+
CheckResult(
|
|
491
|
+
check_id=SKEW_CHECK_ID,
|
|
492
|
+
scope="fleet",
|
|
493
|
+
severity=Severity.CRIT,
|
|
494
|
+
subject=subject,
|
|
495
|
+
headroom=(
|
|
496
|
+
f"CI pins {kind} {ci_version}; "
|
|
497
|
+
f"{', '.join(ci_mismatches)} disagree"
|
|
498
|
+
),
|
|
499
|
+
detail=_format_by_version(_by_version(known)),
|
|
500
|
+
threshold=f"crit when a machine's {kind} != CI's {ci_version}",
|
|
501
|
+
values={
|
|
502
|
+
"versions": known,
|
|
503
|
+
"ci_version": ci_version,
|
|
504
|
+
"missing": missing,
|
|
505
|
+
},
|
|
506
|
+
)
|
|
507
|
+
)
|
|
508
|
+
elif len(distinct) > 1:
|
|
509
|
+
results.append(
|
|
510
|
+
CheckResult(
|
|
511
|
+
check_id=SKEW_CHECK_ID,
|
|
512
|
+
scope="fleet",
|
|
513
|
+
severity=Severity.WARN,
|
|
514
|
+
subject=subject,
|
|
515
|
+
headroom=f"{len(distinct)} {kind} versions across the fleet",
|
|
516
|
+
detail=_format_by_version(_by_version(known)),
|
|
517
|
+
threshold="warn when machines disagree",
|
|
518
|
+
values={
|
|
519
|
+
"versions": known,
|
|
520
|
+
"ci_version": ci_version,
|
|
521
|
+
"missing": missing,
|
|
522
|
+
},
|
|
523
|
+
)
|
|
524
|
+
)
|
|
525
|
+
else:
|
|
526
|
+
(version,) = distinct
|
|
527
|
+
headroom = f"all machines on {kind} {version}"
|
|
528
|
+
if ci_version:
|
|
529
|
+
headroom += f" (matches CI's {ci_version})"
|
|
530
|
+
if missing:
|
|
531
|
+
headroom += f" ({len(missing)} machine(s) with no data)"
|
|
532
|
+
results.append(
|
|
533
|
+
CheckResult(
|
|
534
|
+
check_id=SKEW_CHECK_ID,
|
|
535
|
+
scope="fleet",
|
|
536
|
+
severity=Severity.OK if not missing else Severity.UNKNOWN,
|
|
537
|
+
subject=subject,
|
|
538
|
+
headroom=headroom,
|
|
539
|
+
detail=f"no data for: {', '.join(missing)}" if missing else "",
|
|
540
|
+
values={
|
|
541
|
+
"versions": known,
|
|
542
|
+
"ci_version": ci_version,
|
|
543
|
+
"missing": missing,
|
|
544
|
+
},
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
return results or None
|