471d8a427a
Aligns the star ranking with the rule the skills index already follows: GitHub is consulted only by the twice-daily skills-index.yml schedule, whose artifact every docs deploy reuses. deploy-site.yml now runs fetch-plugin-stars.py without --probe (reuse-only: artifact → live site copy → disk → empty) and cannot call the API at all, so a same-day merge train adds zero requests regardless of cache age. The probe itself collapses from one REST call per repo (~26 today, growing with the catalog) to a single GraphQL query with aliased repository fields, so the scheduled run costs one request no matter how big the catalog gets. A failed probe (rate limit, renamed repo, bad token) keeps the previous counts.
81 lines
3.3 KiB
Python
81 lines
3.3 KiB
Python
"""fetch-plugin-stars.py: plugin-catalog star counts, GitHub consulted only from the scheduled run.
|
|
|
|
The contract under test is rate-limit discipline, not the numbers: a deploy (no ``--probe``)
|
|
must never reach GitHub, the scheduled probe must be ONE request for every repo, and a failed
|
|
probe must keep the previous counts rather than zeroing them.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import json
|
|
import urllib.error
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
SCRIPT = REPO_ROOT / "website" / "scripts" / "fetch-plugin-stars.py"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def mod():
|
|
spec = importlib.util.spec_from_file_location("fetch_plugin_stars", SCRIPT)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def _catalog(tmp_path: Path, *repos: str) -> Path:
|
|
import yaml
|
|
|
|
cat = tmp_path / "plugin-catalog"
|
|
cat.mkdir()
|
|
for i, repo in enumerate(repos):
|
|
(cat / f"p{i}.yaml").write_text(yaml.safe_dump({
|
|
"name": f"p{i}", "repo": repo, "sha": "38fe0fb53eff98d477f807432e965429e665ca33",
|
|
"description": "d", "maintainer": "m"}), encoding="utf-8")
|
|
return cat
|
|
|
|
|
|
def test_deploy_reuses_the_cache_without_any_github_call(mod, tmp_path, monkeypatch):
|
|
cat = _catalog(tmp_path, "https://github.com/a/one")
|
|
out = tmp_path / "plugin-stars.json"
|
|
out.write_text(json.dumps({"fetched_at": "2026-01-01T00:00:00+00:00", "stars": {"a/one": 7}}), encoding="utf-8")
|
|
|
|
def boom(*a, **k):
|
|
raise AssertionError("GitHub must not be called without --probe")
|
|
monkeypatch.setattr(mod, "_graphql", boom)
|
|
monkeypatch.setattr(mod, "_http_json", boom)
|
|
|
|
assert mod.main(catalog_dir=cat, output=out, probe=False, live_url=None) == 0
|
|
assert json.loads(out.read_text())["stars"] == {"a/one": 7}
|
|
|
|
|
|
def test_probe_is_one_graphql_request_and_a_failure_keeps_previous_counts(mod, tmp_path, monkeypatch):
|
|
cat = _catalog(tmp_path, "https://github.com/a/one", "https://github.com/b/two", "https://gitlab.com/c/three")
|
|
out = tmp_path / "plugin-stars.json"
|
|
out.write_text(json.dumps({"fetched_at": "2026-01-01T00:00:00+00:00", "stars": {"a/one": 7, "b/two": 9}}),
|
|
encoding="utf-8")
|
|
calls: list[str] = []
|
|
|
|
def one_request(query, token):
|
|
calls.append(query)
|
|
# b/two errored (renamed repo): its node is null, previous count must survive.
|
|
return {"data": {"r0": {"stargazerCount": 42}, "r1": None},
|
|
"errors": [{"message": "Could not resolve to a Repository"}]}
|
|
monkeypatch.setattr(mod, "_graphql", one_request)
|
|
|
|
assert mod.main(catalog_dir=cat, output=out, probe=True, live_url=None, token="t") == 0
|
|
data = json.loads(out.read_text())
|
|
assert data["stars"] == {"a/one": 42, "b/two": 9}
|
|
assert len(calls) == 1 and "gitlab" not in calls[0] and 'owner: "a"' in calls[0] and 'owner: "b"' in calls[0]
|
|
assert data["fetched_at"] > "2026-01-01"
|
|
|
|
# A rate-limited / failed probe keeps everything as it was.
|
|
def limited(query, token):
|
|
raise urllib.error.HTTPError("u", 403, "rate limited", hdrs=None, fp=None)
|
|
monkeypatch.setattr(mod, "_graphql", limited)
|
|
assert mod.main(catalog_dir=cat, output=out, probe=True, live_url=None, token="t") == 0
|
|
assert json.loads(out.read_text())["stars"] == {"a/one": 42, "b/two": 9}
|