Repository: py-cov-action/python-coverage-comment-action
Pull request #360 (2024-02-15 13:57:47)

Fix cases where the datafile is huge
When the json datafile is >1MB, the [default mode](https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#get-repository-content) of the content API is to skip the content and provide a link to it.

If we use header `Accept: application/vnd.github.raw+json`, the application will:
- Return the raw file instead of a payload (it's good, we only want the file) as long as it's <100MB
- Return `Content-Type: application/vnd.github.raw+json`

This PR:
- Adds code to the GitHub client to send custom headers (surprisingly, it's the first time we needed to do it)
- Add the said `Accept` header to the datafile function
- Setup the client so that the said `Content-Type` response header makes return text instead of bytes. (I'm not 100% sure it's what we should do but we can always revert this later).

github-actions[bot]: [End-to-end public repo](https://github.com/mihcaojwe/python-coverage-comment-action-end-to-end-360-public)

Admin commands cheatsheet:

- `/e2e` (in approved PR review body): Trigger end-to-end tests on external contributions
- `/invite` (in comment): Invite the author & admins to the end-to-end private repo


Patch:
diff --git a/coverage_comment/github_client.py b/coverage_comment/github_client.py
index 19165b6..3057e1e 100644
--- a/coverage_comment/github_client.py
+++ b/coverage_comment/github_client.py
@@ -47,7 +47,6 @@ class _Callable:
 
 
 class GitHub:
-
     """
     GitHub client.
     """
@@ -58,9 +57,18 @@ class GitHub:
     def __getattr__(self, attr):
         return _Callable(self, "/%s" % attr)
 
-    def _http(self, method, path, *, bytes=False, **kw):
+    def _http(
+        self,
+        method: str,
+        path: str,
+        *,
+        bytes: bool = False,
+        headers: dict[str, str] | None = None,
+        **kw,
+    ):
         _method = method.lower()
         requests_kwargs = {}
+        header_kwargs = {"headers": headers} if headers else {}
         if _method == "get" and kw:
             requests_kwargs = {"params": kw}
 
@@ -71,6 +79,7 @@ class GitHub:
             _method.upper(),
             path,
             timeout=TIMEOUT,
+            **header_kwargs,
             **requests_kwargs,
         )
         if bytes:
@@ -93,9 +102,13 @@ class GitHub:
 
 def response_contents(
     response: httpx.Response,
-) -> JsonObject | bytes:
+) -> JsonObject | str | bytes:
     if response.headers.get("content-type", "").startswith("application/json"):
         return response.json(object_hook=JsonObject)
+    if response.headers.get("content-type", "").startswith(
+        "application/vnd.github.raw+json"
+    ):
+        return response.text
     return response.content
 
 
diff --git a/coverage_comment/storage.py b/coverage_comment/storage.py
index 7235764..eb8e49a 100644
--- a/coverage_comment/storage.py
+++ b/coverage_comment/storage.py
@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-import base64
 import contextlib
 import pathlib
 
@@ -120,11 +119,16 @@ def get_datafile_contents(
 ) -> str | None:
     contents_path = github.repos(repository).contents(str(files.DATA_PATH))
     try:
-        response = contents_path.get(ref=branch)
+        response = contents_path.get(
+            ref=branch,
+            # If we don't pass this header, the format of the answer will depend on
+            # the size of the file. With the header, we're sure to get the raw content.
+            headers={"Accept": "application/vnd.github.raw+json"},
+        )
     except github_client.NotFound:
         return None
 
-    return base64.b64decode(response.content).decode()
+    return response
 
 
 def get_raw_file_url(


Test patch:
diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py
index a2419e9..10e563f 100644
--- a/tests/integration/test_main.py
+++ b/tests/integration/test_main.py
@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-import base64
 import json
 import os
 import pathlib
@@ -232,7 +231,7 @@ def test_action__pull_request__store_comment_not_targeting_default(
     session.register(
         "GET",
         "/repos/py-cov-action/foobar/contents/data.json",
-    )(json={"content": base64.b64encode(payload.encode()).decode()})
+    )(text=payload, headers={"content-type": "application/vnd.github.raw+json"})
 
     # Who am I
     session.register("GET", "/user")(json={"login": "foo"})
@@ -293,7 +292,7 @@ def test_action__pull_request__post_comment(
     session.register(
         "GET",
         "/repos/py-cov-action/foobar/contents/data.json",
-    )(json={"content": base64.b64encode(payload.encode()).decode()})
+    )(text=payload, headers={"content-type": "application/vnd.github.raw+json"})
 
     # Who am I
     session.register("GET", "/user")(json={"login": "foo"})
@@ -355,7 +354,7 @@ def test_action__push__non_default_branch(
     session.register(
         "GET",
         "/repos/py-cov-action/foobar/contents/data.json",
-    )(json={"content": base64.b64encode(payload.encode()).decode()})
+    )(text=payload, headers={"content-type": "application/vnd.github.raw+json"})
 
     session.register(
         "GET",
@@ -444,7 +443,7 @@ def test_action__push__non_default_branch__no_pr(
     session.register(
         "GET",
         "/repos/py-cov-action/foobar/contents/data.json",
-    )(json={"content": base64.b64encode(payload.encode()).decode()})
+    )(text=payload, headers={"content-type": "application/vnd.github.raw+json"})
 
     session.register(
         "GET",
@@ -498,7 +497,7 @@ def test_action__pull_request__force_store_comment(
     session.register(
         "GET",
         "/repos/py-cov-action/foobar/contents/data.json",
-    )(json={"content": base64.b64encode(payload.encode()).decode()})
+    )(text=payload, headers={"content-type": "application/vnd.github.raw+json"})
 
     git.register("git fetch origin main --depth=1000")()
     git.register("git diff --unified=0 FETCH_HEAD -- .")(stdout=DIFF_STDOUT)
diff --git a/tests/unit/test_github_client.py b/tests/unit/test_github_client.py
index 8aa147e..3f07b27 100644
--- a/tests/unit/test_github_client.py
+++ b/tests/unit/test_github_client.py
@@ -13,6 +13,31 @@ def test_github_client__get(session, gh):
     assert gh.repos("a/b").issues().get(a=1) == {"foo": "bar"}
 
 
+def test_github_client__get_text(session, gh):
+    session.register("GET", "/repos/a/b/issues", timeout=60, params={"a": 1})(
+        text="foobar", headers={"content-type": "application/vnd.github.raw+json"}
+    )
+
+    assert gh.repos("a/b").issues().get(a=1) == "foobar"
+
+
+def test_github_client__get_bytes(session, gh):
+    session.register("GET", "/repos/a/b/issues", timeout=60, params={"a": 1})(
+        text="foobar", headers={"content-type": "application/vnd.github.raw+json"}
+    )
+
+    assert gh.repos("a/b").issues().get(a=1, bytes=True) == b"foobar"
+
+
+def test_github_client__get_headers(session, gh):
+    session.register("GET", "/repos/a/b/issues", timeout=60, params={"a": 1})(
+        json={"foo": "bar"},
+        headers={"X-foo": "yay"},
+    )
+
+    assert gh.repos("a/b").issues().get(a=1, headers={"X-foo": "yay"}) == {"foo": "bar"}
+
+
 def test_github_client__post_non_json(session, gh):
     session.register("POST", "/repos/a/b/issues", timeout=60, json={"a": 1})()
 
diff --git a/tests/unit/test_storage.py b/tests/unit/test_storage.py
index 357dad5..2ba72a8 100644
--- a/tests/unit/test_storage.py
+++ b/tests/unit/test_storage.py
@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-import base64
 import pathlib
 
 import pytest
@@ -142,9 +141,8 @@ def test_get_datafile_contents__not_found(gh, session):
 
 
 def test_get_datafile_contents(gh, session):
-    payload = base64.b64encode(b"yay").decode()
     session.register("GET", "/repos/foo/bar/contents/data.json", params={"ref": "baz"})(
-        json={"content": payload}
+        text="yay", headers={"content-type": "application/vnd.github.raw+json"}
     )
 
     result = storage.get_datafile_contents(