fix: doctor validates GitHub tokens against /rate_limit so Actions installation tokens pass (review follow-up)
GET /user answers 403 "Resource not accessible by integration" for App installation tokens (ghs_, the GITHUB_TOKEN every Actions job exports), so `hermes doctor` in CI painted a valid token red. /rate_limit is reachable by every token type, still 401s on a bad token, and does not count against the quota. Stand-in replays GitHub's documented 403 for ghs_ on /user; new test red before.
This commit is contained in:
@@ -368,7 +368,10 @@ def _probe_ipv6_path() -> ProbeResult:
|
||||
return _row(name, "ok", f"(IPv6 path to {host} reachable)") # refused/reset also prove a live path
|
||||
|
||||
|
||||
GITHUB_API_USER_URL = "https://api.github.com/user"
|
||||
# /rate_limit is reachable by EVERY token type and does not count against the quota. /user answers
|
||||
# 403 "Resource not accessible by integration" for App installation tokens (the GITHUB_TOKEN every
|
||||
# Actions job exports), which would paint a valid token red.
|
||||
GITHUB_API_PROBE_URL = "https://api.github.com/rate_limit"
|
||||
|
||||
|
||||
def _probe_github_token() -> ProbeResult:
|
||||
@@ -387,7 +390,7 @@ def _probe_github_token() -> ProbeResult:
|
||||
where = f"{_DHH}/.env" if var in load_env() else "the environment"
|
||||
try:
|
||||
import httpx
|
||||
r = httpx.get(GITHUB_API_USER_URL, timeout=10, headers={
|
||||
r = httpx.get(GITHUB_API_PROBE_URL, timeout=10, headers={
|
||||
"Authorization": f"Bearer {get_env_value(var)}", "User-Agent": _HERMES_USER_AGENT,
|
||||
"Accept": "application/vnd.github+json"})
|
||||
except Exception as e:
|
||||
|
||||
@@ -17,6 +17,7 @@ from hermes_cli import doctor_connectivity as dc
|
||||
|
||||
# GitHub's documented 401 body for a bad/expired token (REST API "Authentication" docs).
|
||||
_BAD_CREDENTIALS = {"message": "Bad credentials", "documentation_url": "https://docs.github.com/rest"}
|
||||
_NOT_ACCESSIBLE = {"message": "Resource not accessible by integration", "documentation_url": "https://docs.github.com/rest"}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -27,9 +28,15 @@ def github_stand_in(monkeypatch):
|
||||
status = 401
|
||||
|
||||
def do_GET(self): # noqa: N802 - http.server API
|
||||
seen.append({"path": self.path, "authorization": self.headers.get("Authorization")})
|
||||
body = json.dumps(_BAD_CREDENTIALS if self.status == 401 else {"login": "octocat"}).encode()
|
||||
self.send_response(self.status)
|
||||
auth = self.headers.get("Authorization") or ""
|
||||
seen.append({"path": self.path, "authorization": auth})
|
||||
status = self.status
|
||||
if status == 200 and self.path == "/user" and auth.startswith("Bearer ghs_"):
|
||||
# GitHub's documented answer for an App installation token on /user.
|
||||
status, body = 403, json.dumps(_NOT_ACCESSIBLE).encode()
|
||||
else:
|
||||
body = json.dumps(_BAD_CREDENTIALS if status == 401 else {"login": "octocat"}).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
@@ -40,7 +47,10 @@ def github_stand_in(monkeypatch):
|
||||
|
||||
srv = HTTPServer(("127.0.0.1", 0), Handler)
|
||||
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
||||
monkeypatch.setattr(dc, "GITHUB_API_USER_URL", f"http://127.0.0.1:{srv.server_port}/user")
|
||||
# Keep the production path so the stand-in can tell /user from /rate_limit.
|
||||
from urllib.parse import urlsplit
|
||||
path = urlsplit(dc.GITHUB_API_PROBE_URL).path
|
||||
monkeypatch.setattr(dc, "GITHUB_API_PROBE_URL", f"http://127.0.0.1:{srv.server_port}{path}")
|
||||
yield Handler, seen
|
||||
srv.shutdown()
|
||||
|
||||
@@ -84,3 +94,13 @@ def test_valid_token_is_ok_and_no_token_is_skipped(monkeypatch, tmp_path, github
|
||||
seen.clear()
|
||||
result = _github_probe_row(monkeypatch, tmp_path, None, name="home-without-token")
|
||||
assert result.lines == [] and result.issues == [] and seen == [] # nothing configured: no request, no row
|
||||
|
||||
|
||||
def test_actions_installation_token_is_accepted(monkeypatch, tmp_path, github_stand_in):
|
||||
"""A ``ghs_`` App installation token (the GITHUB_TOKEN every Actions job exports) is valid yet
|
||||
GitHub answers 403 on /user; the doctor must probe an endpoint every token type can reach."""
|
||||
handler, seen = github_stand_in
|
||||
handler.status = 200
|
||||
result = _github_probe_row(monkeypatch, tmp_path, "GITHUB_TOKEN=ghs_install000000000000000000000000000000\n")
|
||||
((glyph, _label, detail),) = result.lines
|
||||
assert "✓" in glyph and "GITHUB_TOKEN" in detail and not result.issues, (detail, seen)
|
||||
|
||||
Reference in New Issue
Block a user