merge master into ariadne hygiene branch
This commit is contained in:
commit
7f284007eb
@ -57,12 +57,18 @@ class KeycloakOIDC:
|
|||||||
def _decode_claims(self, token: str, key: dict[str, Any]) -> dict[str, Any]:
|
def _decode_claims(self, token: str, key: dict[str, Any]) -> dict[str, Any]:
|
||||||
return jwt.decode(
|
return jwt.decode(
|
||||||
token,
|
token,
|
||||||
key=jwt.algorithms.RSAAlgorithm.from_jwk(key),
|
key=self._key_from_jwk(key),
|
||||||
algorithms=["RS256"],
|
algorithms=["RS256"],
|
||||||
options={"verify_aud": False},
|
options={"verify_aud": False},
|
||||||
issuer=self._issuer,
|
issuer=self._issuer,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _key_from_jwk(self, key: dict[str, Any]) -> Any:
|
||||||
|
algorithm = getattr(jwt.algorithms, "RSAAlgorithm", None)
|
||||||
|
if algorithm and hasattr(algorithm, "from_jwk"):
|
||||||
|
return algorithm.from_jwk(key)
|
||||||
|
return jwt.PyJWK.from_dict(key).key
|
||||||
|
|
||||||
def _validate_audience(self, claims: dict[str, Any]) -> None:
|
def _validate_audience(self, claims: dict[str, Any]) -> None:
|
||||||
azp = claims.get("azp")
|
azp = claims.get("azp")
|
||||||
aud = claims.get("aud")
|
aud = claims.get("aud")
|
||||||
|
|||||||
@ -8,29 +8,45 @@ import ast
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def _is_dataclass_class(node: ast.ClassDef) -> bool:
|
||||||
|
"""Return whether a class uses the dataclass decorator."""
|
||||||
|
|
||||||
|
return any(
|
||||||
|
(isinstance(dec, ast.Name) and dec.id == "dataclass")
|
||||||
|
or (isinstance(dec, ast.Call) and isinstance(dec.func, ast.Name) and dec.func.id == "dataclass")
|
||||||
|
for dec in node.decorator_list
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _base_names(node: ast.ClassDef) -> set[str]:
|
||||||
|
"""Return simple base class names used by a class definition."""
|
||||||
|
|
||||||
|
return {base.id for base in node.bases if isinstance(base, ast.Name)}
|
||||||
|
|
||||||
|
|
||||||
|
def _needs_function_docstring(node: ast.FunctionDef | ast.AsyncFunctionDef, parent_class: str | None) -> bool:
|
||||||
|
"""Return whether a public function-like node needs a docstring."""
|
||||||
|
|
||||||
|
if node.name.startswith("_") and node.name != "__init__":
|
||||||
|
return False
|
||||||
|
return not (parent_class and node.name.startswith("_"))
|
||||||
|
|
||||||
|
|
||||||
|
def _needs_class_docstring(node: ast.ClassDef) -> bool:
|
||||||
|
"""Return whether a public class-like node needs a docstring."""
|
||||||
|
|
||||||
|
bases = _base_names(node)
|
||||||
|
skipped_bases = {"Exception", "RuntimeError", "BaseException", "BaseModel"}
|
||||||
|
return not (node.name.startswith("_") or _is_dataclass_class(node) or bool(bases.intersection(skipped_bases)))
|
||||||
|
|
||||||
|
|
||||||
def _needs_docstring(node: ast.AST, *, parent_class: str | None = None) -> bool:
|
def _needs_docstring(node: ast.AST, *, parent_class: str | None = None) -> bool:
|
||||||
"""Return whether `node` should carry an API contract docstring."""
|
"""Return whether `node` should carry an API contract docstring."""
|
||||||
|
|
||||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||||
name = node.name
|
return _needs_function_docstring(node, parent_class)
|
||||||
if name.startswith("_") and name != "__init__":
|
|
||||||
return False
|
|
||||||
return not (parent_class and name.startswith("_"))
|
|
||||||
if isinstance(node, ast.ClassDef):
|
if isinstance(node, ast.ClassDef):
|
||||||
if node.name.startswith("_"):
|
return _needs_class_docstring(node)
|
||||||
return False
|
|
||||||
if any(
|
|
||||||
(isinstance(dec, ast.Name) and dec.id == "dataclass")
|
|
||||||
or (isinstance(dec, ast.Call) and isinstance(dec.func, ast.Name) and dec.func.id == "dataclass")
|
|
||||||
for dec in node.decorator_list
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
if any(
|
|
||||||
isinstance(base, ast.Name) and base.id in {"Exception", "RuntimeError", "BaseException"}
|
|
||||||
for base in node.bases
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
return not any(isinstance(base, ast.Name) and base.id == "BaseModel" for base in node.bases)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -204,11 +204,31 @@ def _supply_chain_check_status(build_dir: Path) -> str:
|
|||||||
return "failed"
|
return "failed"
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_artifact_paths(repo_root: Path) -> tuple[Path, Path]:
|
||||||
|
"""Find coverage and JUnit artifacts even when a test runner uses fallback names."""
|
||||||
|
|
||||||
|
coverage_path = Path(os.getenv("COVERAGE_JSON", "build/coverage.json"))
|
||||||
|
junit_path = Path(os.getenv("JUNIT_XML", "build/junit.xml"))
|
||||||
|
if not coverage_path.exists():
|
||||||
|
for candidate in (
|
||||||
|
repo_root / "build" / "coverage.json",
|
||||||
|
repo_root / "build" / "coverage-summary.json",
|
||||||
|
repo_root / "build" / "coverage" / "coverage-summary.json",
|
||||||
|
):
|
||||||
|
if candidate.exists():
|
||||||
|
coverage_path = candidate
|
||||||
|
break
|
||||||
|
if not junit_path.exists():
|
||||||
|
junit_candidates = sorted((repo_root / "build").glob("junit*.xml"))
|
||||||
|
if junit_candidates:
|
||||||
|
junit_path = junit_candidates[0]
|
||||||
|
return coverage_path, junit_path
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
repo_root = Path(__file__).resolve().parents[1]
|
repo_root = Path(__file__).resolve().parents[1]
|
||||||
build_dir = repo_root / "build"
|
build_dir = repo_root / "build"
|
||||||
coverage_path = os.getenv("COVERAGE_JSON", "build/coverage.json")
|
coverage_path, junit_path = _resolve_artifact_paths(repo_root)
|
||||||
junit_path = os.getenv("JUNIT_XML", "build/junit.xml")
|
|
||||||
pushgateway_url = os.getenv(
|
pushgateway_url = os.getenv(
|
||||||
"PUSHGATEWAY_URL", "http://platform-quality-gateway.monitoring.svc.cluster.local:9091"
|
"PUSHGATEWAY_URL", "http://platform-quality-gateway.monitoring.svc.cluster.local:9091"
|
||||||
).strip()
|
).strip()
|
||||||
@ -217,16 +237,19 @@ def main() -> int:
|
|||||||
build_number = os.getenv("BUILD_NUMBER", "")
|
build_number = os.getenv("BUILD_NUMBER", "")
|
||||||
commit = os.getenv("GIT_COMMIT", "")
|
commit = os.getenv("GIT_COMMIT", "")
|
||||||
|
|
||||||
|
print(f"[metrics] coverage_path={coverage_path} exists={coverage_path.exists()}")
|
||||||
|
print(f"[metrics] junit_path={junit_path} exists={junit_path.exists()}")
|
||||||
|
|
||||||
coverage = 0.0
|
coverage = 0.0
|
||||||
if os.path.exists(coverage_path):
|
if coverage_path.exists():
|
||||||
coverage = _load_coverage(coverage_path)
|
coverage = _load_coverage(str(coverage_path))
|
||||||
docs_gate_rc = _load_gate_rc(Path(os.getenv("QUALITY_GATE_DOCS_RC_PATH", str(build_dir / "docs-naming.rc"))))
|
docs_gate_rc = _load_gate_rc(Path(os.getenv("QUALITY_GATE_DOCS_RC_PATH", str(build_dir / "docs-naming.rc"))))
|
||||||
source_lines_over_500 = _count_source_files_over_limit(repo_root, max_lines=500)
|
source_lines_over_500 = _count_source_files_over_limit(repo_root, max_lines=500)
|
||||||
totals = {"tests": 0, "failures": 0, "errors": 0, "skipped": 0}
|
totals = {"tests": 0, "failures": 0, "errors": 0, "skipped": 0}
|
||||||
test_cases: list[tuple[str, str]] = []
|
test_cases: list[tuple[str, str]] = []
|
||||||
if os.path.exists(junit_path):
|
if junit_path.exists():
|
||||||
totals = _load_junit(junit_path)
|
totals = _load_junit(str(junit_path))
|
||||||
test_cases = _load_junit_cases(junit_path)
|
test_cases = _load_junit_cases(str(junit_path))
|
||||||
passed = max(totals["tests"] - totals["failures"] - totals["errors"] - totals["skipped"], 0)
|
passed = max(totals["tests"] - totals["failures"] - totals["errors"] - totals["skipped"], 0)
|
||||||
|
|
||||||
outcome = "ok"
|
outcome = "ok"
|
||||||
@ -284,10 +307,15 @@ def main() -> int:
|
|||||||
"# TYPE ariadne_quality_gate_build_info gauge",
|
"# TYPE ariadne_quality_gate_build_info gauge",
|
||||||
f"ariadne_quality_gate_build_info{_label_str(labels)} 1",
|
f"ariadne_quality_gate_build_info{_label_str(labels)} 1",
|
||||||
]
|
]
|
||||||
payload_lines.extend(
|
if test_cases:
|
||||||
f'platform_quality_gate_test_case_result{{suite="{suite}",test="{_escape_label(test_name)}",status="{_escape_label(test_status)}"}} 1'
|
payload_lines.extend(
|
||||||
for test_name, test_status in test_cases
|
f'platform_quality_gate_test_case_result{{suite="{suite}",test="{_escape_label(test_name)}",status="{_escape_label(test_status)}"}} 1'
|
||||||
)
|
for test_name, test_status in test_cases
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
payload_lines.append(
|
||||||
|
f'platform_quality_gate_test_case_result{{suite="{suite}",test="__no_test_cases__",status="skipped"}} 1'
|
||||||
|
)
|
||||||
payload_lines.extend(
|
payload_lines.extend(
|
||||||
f'ariadne_quality_gate_checks_total{{suite="{suite}",check="{check_name}",result="{check_status}"}} 1'
|
f'ariadne_quality_gate_checks_total{{suite="{suite}",check="{check_name}",result="{check_status}"}} 1'
|
||||||
for check_name, check_status in checks.items()
|
for check_name, check_status in checks.items()
|
||||||
|
|||||||
@ -20,7 +20,7 @@ def test_keycloak_verify_accepts_matching_audience(monkeypatch) -> None:
|
|||||||
kc = KeycloakOIDC("https://jwks", "https://issuer", "portal")
|
kc = KeycloakOIDC("https://jwks", "https://issuer", "portal")
|
||||||
|
|
||||||
monkeypatch.setattr(kc, "_get_jwks", lambda force=False: {"keys": [{"kid": "test"}]})
|
monkeypatch.setattr(kc, "_get_jwks", lambda force=False: {"keys": [{"kid": "test"}]})
|
||||||
monkeypatch.setattr(jwt.algorithms.RSAAlgorithm, "from_jwk", lambda key: "dummy")
|
monkeypatch.setattr(kc, "_key_from_jwk", lambda key: "dummy")
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
jwt,
|
jwt,
|
||||||
"decode",
|
"decode",
|
||||||
@ -36,7 +36,7 @@ def test_keycloak_verify_rejects_wrong_audience(monkeypatch) -> None:
|
|||||||
kc = KeycloakOIDC("https://jwks", "https://issuer", "portal")
|
kc = KeycloakOIDC("https://jwks", "https://issuer", "portal")
|
||||||
|
|
||||||
monkeypatch.setattr(kc, "_get_jwks", lambda force=False: {"keys": [{"kid": "test"}]})
|
monkeypatch.setattr(kc, "_get_jwks", lambda force=False: {"keys": [{"kid": "test"}]})
|
||||||
monkeypatch.setattr(jwt.algorithms.RSAAlgorithm, "from_jwk", lambda key: "dummy")
|
monkeypatch.setattr(kc, "_key_from_jwk", lambda key: "dummy")
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
jwt,
|
jwt,
|
||||||
"decode",
|
"decode",
|
||||||
@ -73,7 +73,7 @@ def test_keycloak_verify_refreshes_jwks(monkeypatch) -> None:
|
|||||||
return {"keys": [{"kid": "test"}]}
|
return {"keys": [{"kid": "test"}]}
|
||||||
|
|
||||||
monkeypatch.setattr(kc, "_get_jwks", fake_get_jwks)
|
monkeypatch.setattr(kc, "_get_jwks", fake_get_jwks)
|
||||||
monkeypatch.setattr(jwt.algorithms.RSAAlgorithm, "from_jwk", lambda key: "dummy")
|
monkeypatch.setattr(kc, "_key_from_jwk", lambda key: "dummy")
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
jwt,
|
jwt,
|
||||||
"decode",
|
"decode",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user