Skip to content

Commit 8185cdf

Browse files
author
mars
committed
chore: align release readiness commands
1 parent f11fcbb commit 8185cdf

7 files changed

Lines changed: 189 additions & 8 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ help:
3333
@printf " make test-api [runtime mutating] Run the API pytest suite\n"
3434
@printf " make test-browser [runtime mutating] Run the Playwright smoke test\n"
3535
@printf " make verify-persistence [runtime mutating] Verify saved settings survive restart and rebuild\n"
36-
@printf " make doctor [tracked-source read-only] Probe the supported upstream checkout and local tooling\n"
36+
@printf " make doctor [tracked-source read-only] Probe supported upstream checkouts and local tooling\n"
3737

3838
.PHONY: help validate validate-lite validate-config secret-scan local-state-report drift-check patch-audit audit release-evidence sync-upstream build run dashboard e2e verify-submit-replay verify-release patch-check patch-check-upstream test-ci test-api test-browser verify-persistence doctor
3939

docs/command-contract.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ right gate without guessing.
5454
| `make test-api` | runtime mutating | Runs API validation against a local virtual runtime. |
5555
| `make test-browser` | runtime mutating | Runs browser smoke validation against a local virtual runtime. |
5656
| `make verify-persistence` | runtime mutating | Verifies settings persistence across restart/rebuild. |
57-
| `make doctor` | tracked-source read-only | Probes configured source and local tool availability. |
57+
| `make doctor` | tracked-source read-only | Probes canonical configured sources and local tool availability. |
5858

5959
## Release Rule
6060

scripts/drift-check.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,40 @@ def git_output(args: list[str], cwd: Path = ROOT_DIR) -> tuple[int, str, str]:
5050
return result.returncode, result.stdout.strip(), result.stderr.strip()
5151

5252

53+
def resolve_source_ref(source_dir: Path, ref: str) -> tuple[str, str]:
54+
if not (source_dir / ".git").is_dir():
55+
return "", "local source cache missing"
56+
rc, resolved, error = git_output(["git", "-C", str(source_dir), "rev-parse", f"{ref}^{{commit}}"])
57+
if rc != 0:
58+
return "", error or f"git rev-parse failed for {ref}"
59+
return resolved, ""
60+
61+
62+
def sync_source_cache(source_name: str, ref: str, *, reason: str) -> dict[str, Any]:
63+
env = {
64+
**os.environ,
65+
"SOURCE": source_name,
66+
"SOURCE_NAME": source_name,
67+
"UPSTREAM_REF": ref,
68+
}
69+
result = subprocess.run(
70+
[str(ROOT_DIR / "scripts" / "sync-upstream.sh")],
71+
cwd=ROOT_DIR,
72+
env=env,
73+
text=True,
74+
capture_output=True,
75+
check=False,
76+
)
77+
return {
78+
"status": "passed" if result.returncode == 0 else "failed",
79+
"classification": "ok" if result.returncode == 0 else "external source sync failed",
80+
"reason": reason,
81+
"returncode": result.returncode,
82+
"stdout": result.stdout.strip(),
83+
"stderr": result.stderr.strip(),
84+
}
85+
86+
5387
def run_patch_apply(source_name: str, source_dir: Path, ref: str) -> dict[str, Any]:
5488
if not (source_dir / ".git").is_dir():
5589
return {
@@ -117,9 +151,25 @@ def drift_check(include_upstream_head: bool = False) -> dict[str, Any]:
117151
entry = sources["sources"][source_name]
118152
ref = entry["ref"]
119153
source_dir = ROOT_DIR / ".sources" / source_name
120-
_rc, resolved, ref_error = git_output(["git", "-C", str(source_dir), "rev-parse", f"{ref}^{{commit}}"]) if source_dir.exists() else (1, "", "local source cache missing")
154+
resolved, ref_error = resolve_source_ref(source_dir, ref)
155+
source_sync = {
156+
"status": "skipped",
157+
"classification": "ok",
158+
"reason": "configured source ref already resolved locally",
159+
}
160+
if not resolved:
161+
source_sync = sync_source_cache(source_name, ref, reason=ref_error)
162+
if source_sync["status"] == "passed":
163+
resolved, ref_error = resolve_source_ref(source_dir, ref)
121164
series_digest = patch_series_hash(source_name)
122-
configured_pin = run_patch_apply(source_name, source_dir, ref)
165+
if source_sync["status"] == "failed":
166+
configured_pin = {
167+
"status": "skipped",
168+
"classification": "external check skipped",
169+
"reason": "source sync failed before patch apply",
170+
}
171+
else:
172+
configured_pin = run_patch_apply(source_name, source_dir, ref)
123173
upstream_head = {"status": "skipped", "classification": "external check skipped", "reason": "run with --upstream-head"}
124174
if include_upstream_head:
125175
upstream_ref = "origin/master"
@@ -148,13 +198,16 @@ def drift_check(include_upstream_head: bool = False) -> dict[str, Any]:
148198
release_blockers = []
149199
if configured_pin["classification"] == "release-blocking drift":
150200
release_blockers.append("configured pin patch stack does not apply")
201+
if source_sync["status"] == "failed":
202+
release_blockers.append("configured source sync failed")
151203
if not resolved:
152204
release_blockers.append(f"configured source ref is not resolved locally: {ref_error}")
153205
return {
154206
"status": "passed" if not release_blockers else "failed",
155207
"source": source_name,
156208
"configuredRef": ref,
157209
"resolvedConfiguredRef": resolved,
210+
"sourceSync": source_sync,
158211
"patchSeriesSha256": series_digest,
159212
"patchCount": len(series(source_name)),
160213
"configuredPinPatchCheck": configured_pin,

scripts/virtualaxe.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1059,7 +1059,7 @@ def ensure_matching_build(
10591059

10601060

10611061
def command_doctor(args: argparse.Namespace) -> int:
1062-
sources = load_sources()
1062+
sources = source_registry().as_legacy_payload(include_aliases=False)
10631063
payload: dict[str, Any] = {"sources": {}, "profiles": sorted(p.stem for p in PROFILES_DIR.glob("*.json"))}
10641064
for name, entry in sources["sources"].items():
10651065
try:

tests/api/test_settings_update.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
SOURCE_NAME = os.environ.get("SOURCE_NAME", "bitaxe")
1111

1212

13+
def require_explicit_settings_api():
14+
if not os.environ.get("BASE_URL"):
15+
pytest.skip("set BASE_URL to run mutating API settings checks")
16+
require_virtualaxe_api(BASE_URL)
17+
18+
1319
def require_bitaxe_settings_api():
1420
if SOURCE_NAME != "bitaxe":
1521
pytest.skip(f"{SOURCE_NAME} exposes a source-specific settings API")
@@ -37,7 +43,7 @@ def setting_matches(actual: object, expected: object) -> bool:
3743

3844

3945
def test_system_settings_patch_roundtrip():
40-
require_virtualaxe_api(BASE_URL, env_vars=("BASE_URL",))
46+
require_explicit_settings_api()
4147
require_bitaxe_settings_api()
4248

4349
system_info = get_json(f"{BASE_URL}/api/system/info", timeout=10)
@@ -66,7 +72,7 @@ def test_system_settings_patch_roundtrip():
6672

6773

6874
def test_system_settings_patch_accepts_large_form_payload():
69-
require_virtualaxe_api(BASE_URL, env_vars=("BASE_URL",))
75+
require_explicit_settings_api()
7076
require_bitaxe_settings_api()
7177

7278
system_info = get_json(f"{BASE_URL}/api/system/info", timeout=10)
@@ -96,7 +102,7 @@ def test_system_settings_patch_accepts_large_form_payload():
96102

97103

98104
def test_pool_settings_patch_keeps_system_info_responsive():
99-
require_virtualaxe_api(BASE_URL, env_vars=("BASE_URL",))
105+
require_explicit_settings_api()
100106
require_bitaxe_settings_api()
101107

102108
system_info = get_json(f"{BASE_URL}/api/system/info", timeout=10)

tests/test_repo_readiness_tools.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,91 @@ def test_local_state_report_is_read_only_and_classifies_known_paths():
5656
assert by_path["out"]["canAffectBuildRunReview"] is True
5757

5858

59+
def test_drift_check_prepares_missing_source_cache(monkeypatch):
60+
module = load_script("drift-check.py")
61+
resolved_ref = "c" * 40
62+
resolve_calls = iter(
63+
[
64+
("", "local source cache missing"),
65+
(resolved_ref, ""),
66+
]
67+
)
68+
sync_calls = []
69+
70+
def fake_resolve(_source_dir, ref):
71+
assert ref == "ce44b2bbfef60ef8830ab17b321cc295e0c0edc8"
72+
return next(resolve_calls)
73+
74+
def fake_sync(source_name, ref, *, reason):
75+
sync_calls.append((source_name, ref, reason))
76+
return {
77+
"status": "passed",
78+
"classification": "ok",
79+
"reason": reason,
80+
"returncode": 0,
81+
"stdout": ".sources/bitaxe",
82+
"stderr": "",
83+
}
84+
85+
def fake_patch_apply(source_name, _source_dir, ref):
86+
return {
87+
"status": "passed",
88+
"classification": "ok",
89+
"source": source_name,
90+
"requestedRef": ref,
91+
}
92+
93+
monkeypatch.setattr(module, "resolve_source_ref", fake_resolve)
94+
monkeypatch.setattr(module, "sync_source_cache", fake_sync)
95+
monkeypatch.setattr(module, "run_patch_apply", fake_patch_apply)
96+
monkeypatch.setattr(module, "manifest_status", lambda _source, _digest: {"status": "missing"})
97+
98+
payload = module.drift_check()
99+
100+
assert payload["status"] == "passed"
101+
assert payload["sourceSync"]["status"] == "passed"
102+
assert payload["resolvedConfiguredRef"] == resolved_ref
103+
assert sync_calls == [
104+
("bitaxe", "ce44b2bbfef60ef8830ab17b321cc295e0c0edc8", "local source cache missing")
105+
]
106+
107+
108+
def test_drift_check_reports_source_sync_failure(monkeypatch):
109+
module = load_script("drift-check.py")
110+
111+
monkeypatch.setattr(
112+
module,
113+
"resolve_source_ref",
114+
lambda _source_dir, _ref: ("", "local source cache missing"),
115+
)
116+
monkeypatch.setattr(
117+
module,
118+
"sync_source_cache",
119+
lambda _source, _ref, *, reason: {
120+
"status": "failed",
121+
"classification": "external source sync failed",
122+
"reason": reason,
123+
"returncode": 128,
124+
"stdout": "",
125+
"stderr": "network unavailable",
126+
},
127+
)
128+
129+
def fail_patch_apply(*_args, **_kwargs):
130+
raise AssertionError("patch apply should not run when source sync fails")
131+
132+
monkeypatch.setattr(module, "run_patch_apply", fail_patch_apply)
133+
monkeypatch.setattr(module, "manifest_status", lambda _source, _digest: {"status": "missing"})
134+
135+
payload = module.drift_check()
136+
137+
assert payload["status"] == "failed"
138+
assert payload["sourceSync"]["status"] == "failed"
139+
assert payload["configuredPinPatchCheck"]["status"] == "skipped"
140+
assert "configured source sync failed" in payload["releaseBlockers"]
141+
assert any("configured source ref is not resolved locally" in blocker for blocker in payload["releaseBlockers"])
142+
143+
59144
def test_validate_full_gate_prepares_local_state_and_avoids_live_pools():
60145
module = load_script("validate.py")
61146

tests/test_vaxe.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,6 +1191,43 @@ def test_virtualaxe_patch_check_metadata_supports_nerdnos_submit_replay_series()
11911191
assert metadata[5]["patch"] == "0006-nerdnos-brand-virtualaxe-header.patch"
11921192

11931193

1194+
def test_virtualaxe_doctor_reports_canonical_sources_only(monkeypatch: pytest.MonkeyPatch, capsys):
1195+
module = load_virtualaxe_module()
1196+
1197+
class Registry:
1198+
def as_legacy_payload(self, *, include_aliases: bool = True):
1199+
assert include_aliases is False
1200+
return {
1201+
"sources": {
1202+
"bitaxe": {"ref": "bitaxe-pin"},
1203+
"nerdnos": {"ref": "nerdnos-pin"},
1204+
}
1205+
}
1206+
1207+
monkeypatch.setattr(module, "source_registry", lambda: Registry())
1208+
monkeypatch.setattr(module, "ensure_git_source", lambda name, entry, **_kwargs: (Path(f"/tmp/{name}"), entry))
1209+
monkeypatch.setattr(
1210+
module,
1211+
"source_probe",
1212+
lambda _source_dir: {
1213+
"capabilities": {"supportsGammaProfiles": False},
1214+
"missingRequiredCapabilities": [],
1215+
},
1216+
)
1217+
monkeypatch.setattr(
1218+
module,
1219+
"run",
1220+
lambda command, **_kwargs: subprocess.CompletedProcess(command, 0, "tool version\n", ""),
1221+
)
1222+
1223+
args = type("Args", (), {"json": True})()
1224+
1225+
assert module.command_doctor(args) == 0
1226+
1227+
payload = json.loads(capsys.readouterr().out)
1228+
assert set(payload["sources"]) == {"bitaxe", "nerdnos"}
1229+
1230+
11941231
def test_virtualaxe_patch_check_applies_resolved_upstream_commit(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys):
11951232
module = load_virtualaxe_module()
11961233
source_dir = tmp_path / "source"

0 commit comments

Comments
 (0)