diff --git a/src/cli.py b/src/cli.py index 09f5550..fa1d4cb 100644 --- a/src/cli.py +++ b/src/cli.py @@ -307,11 +307,11 @@ def cmd_serve(args: argparse.Namespace, db_path: str) -> None: port = args.port or DEFAULT_SERVER_PORT from src.server import create_handler - from http.server import HTTPServer + from http.server import ThreadingHTTPServer handler = create_handler(db_path) try: - server = HTTPServer((host, port), handler) + server = ThreadingHTTPServer((host, port), handler) except OSError as e: if e.errno == 98: # Address already in use error(f"Port {port} is already in use") diff --git a/src/github.py b/src/github.py index eddb7ac..57b5af7 100644 --- a/src/github.py +++ b/src/github.py @@ -117,40 +117,30 @@ class GitHubClient: result = [] for tag in data: tag_name = tag["name"] - published_at = self._get_tag_date(owner_repo, tag_name) + commit_sha = tag.get("commit", {}).get("sha") + published_at = self._get_tag_date(owner_repo, commit_sha) result.append({ "tag_name": tag_name, "title": tag_name, "body": "", "published_at": published_at, - "html_url": f"https://github.com/{owner_repo}/tags/{tag_name}", + "html_url": f"https://github.com/{owner_repo}/releases/tag/{tag_name}", }) return result - def _get_tag_date(self, owner_repo: str, tag_name: str) -> str: - """Get the commit date for a tag by resolving the git ref.""" + def _get_tag_date(self, owner_repo: str, commit_sha: str | None) -> str: + """Get the commit date for a tag using the commit SHA from tags API.""" + if not commit_sha: + return datetime.now(timezone.utc).isoformat() try: resp = self._request( "GET", - f"{self.BASE_URL}/repos/{owner_repo}/git/ref/tags/{tag_name}", + f"{self.BASE_URL}/repos/{owner_repo}/commits/{commit_sha}", ) data = resp.json() - # The ref may point to a commit directly or to a tag object - obj = data.get("object", {}) - obj_type = obj.get("type") - if obj_type == "commit": - author_date = obj.get("author", {}).get("date") - if author_date: - return author_date - elif obj_type == "tag": - # Follow the tag object to get the commit - commit_sha = obj.get("sha") - if commit_sha: - commit_resp = self._request("GET", f"{self.BASE_URL}/repos/{owner_repo}/git/commits/{commit_sha}") - commit_data = commit_resp.json() - author_date = commit_data.get("author", {}).get("date") - if author_date: - return author_date + author_date = data.get("commit", {}).get("author", {}).get("date") + if author_date: + return author_date except Exception: pass return datetime.now(timezone.utc).isoformat() diff --git a/src/server.py b/src/server.py index fb152e4..c01608c 100644 --- a/src/server.py +++ b/src/server.py @@ -39,20 +39,29 @@ def create_handler(db_path: str): self.send_response(200) self.send_header("Content-Type", "application/rss+xml; charset=utf-8") self.end_headers() - self.wfile.write(feed_xml.encode("utf-8")) + try: + self.wfile.write(feed_xml.encode("utf-8")) + except BrokenPipeError: + pass def _serve_index(self): html = generate_index_html(self._base_url()) self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.end_headers() - self.wfile.write(html.encode("utf-8")) + try: + self.wfile.write(html.encode("utf-8")) + except BrokenPipeError: + pass def _serve_health(self): self.send_response(200) self.send_header("Content-Type", "text/plain") self.end_headers() - self.wfile.write(b"OK") + try: + self.wfile.write(b"OK") + except BrokenPipeError: + pass def _base_url(self) -> str: host = self.server.server_address[0] diff --git a/tests/test_github.py b/tests/test_github.py index 3490f2f..3531c70 100644 --- a/tests/test_github.py +++ b/tests/test_github.py @@ -61,24 +61,18 @@ class TestFetchTags: def test_fetch_tags(self, mock_api): mock_api.get("https://api.github.com/repos/owner/repo/tags").mock( return_value=httpx.Response(200, json=[ - {"name": "v1.0.0", "zipball_url": "https://api.github.com/repos/owner/repo/zipball/v1.0.0"}, - {"name": "v0.9.0", "zipball_url": "https://api.github.com/repos/owner/repo/zipball/v0.9.0"}, + {"name": "v1.0.0", "commit": {"sha": "aaa111"}}, + {"name": "v0.9.0", "commit": {"sha": "bbb222"}}, ]) ) - mock_api.get("https://api.github.com/repos/owner/repo/git/ref/tags/v1.0.0").mock( + mock_api.get("https://api.github.com/repos/owner/repo/commits/aaa111").mock( return_value=httpx.Response(200, json={ - "object": { - "type": "commit", - "author": {"date": "2025-01-15T10:30:00Z"}, - } + "commit": {"author": {"date": "2025-01-15T10:30:00Z"}} }) ) - mock_api.get("https://api.github.com/repos/owner/repo/git/ref/tags/v0.9.0").mock( + mock_api.get("https://api.github.com/repos/owner/repo/commits/bbb222").mock( return_value=httpx.Response(200, json={ - "object": { - "type": "commit", - "author": {"date": "2025-01-10T08:00:00Z"}, - } + "commit": {"author": {"date": "2025-01-10T08:00:00Z"}} }) ) @@ -91,26 +85,18 @@ class TestFetchTags: assert tags[0]["title"] == "v1.0.0" assert tags[0]["body"] == "" assert tags[0]["published_at"] == "2025-01-15T10:30:00Z" - assert tags[0]["html_url"] == "https://github.com/owner/repo/tags/v1.0.0" + assert tags[0]["html_url"] == "https://github.com/owner/repo/releases/tag/v1.0.0" def test_fetch_tags_with_tag_object(self, mock_api): - """When a tag points to a tag object, follow to the commit.""" + """Commit date is fetched directly from commit SHA in tags API.""" mock_api.get("https://api.github.com/repos/owner/repo/tags").mock( return_value=httpx.Response(200, json=[ - {"name": "v2.0.0", "zipball_url": "https://api.github.com/..."}, + {"name": "v2.0.0", "commit": {"sha": "abc123"}}, ]) ) - mock_api.get("https://api.github.com/repos/owner/repo/git/ref/tags/v2.0.0").mock( + mock_api.get("https://api.github.com/repos/owner/repo/commits/abc123").mock( return_value=httpx.Response(200, json={ - "object": { - "type": "tag", - "sha": "abc123", - } - }) - ) - mock_api.get("https://api.github.com/repos/owner/repo/git/commits/abc123").mock( - return_value=httpx.Response(200, json={ - "author": {"date": "2025-02-01T12:00:00Z"} + "commit": {"author": {"date": "2025-02-01T12:00:00Z"}} }) ) @@ -121,13 +107,13 @@ class TestFetchTags: assert tags[0]["published_at"] == "2025-02-01T12:00:00Z" def test_fetch_tags_fallback_date(self, mock_api): - """When git ref fails, use current time as fallback.""" + """When commit API fails, use current time as fallback.""" mock_api.get("https://api.github.com/repos/owner/repo/tags").mock( return_value=httpx.Response(200, json=[ - {"name": "v1.0.0", "zipball_url": "https://api.github.com/..."}, + {"name": "v1.0.0", "commit": {"sha": "xyz789"}}, ]) ) - mock_api.get("https://api.github.com/repos/owner/repo/git/ref/tags/v1.0.0").mock( + mock_api.get("https://api.github.com/repos/owner/repo/commits/xyz789").mock( return_value=httpx.Response(404) )