From 15028ed3f7fea45d7d0a94b812f5b3bc64d606cc Mon Sep 17 00:00:00 2001 From: GPTomek <314538226+gptomek[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 21:21:05 +0000 Subject: [PATCH 1/5] feat(github): add public travnie activity --- feed_generators/github.py | 272 ++++++++++++++++++++++++- tests/test_github_requested_sources.py | 260 +++++++++++++++++++++++ tests/test_github_sources.py | 6 +- 3 files changed, 532 insertions(+), 6 deletions(-) create mode 100644 tests/test_github_requested_sources.py diff --git a/feed_generators/github.py b/feed_generators/github.py index da1b6321ae..bd711e68ab 100644 --- a/feed_generators/github.py +++ b/feed_generators/github.py @@ -14,6 +14,10 @@ * The wider Git/GitHub tooling ecosystem: Mergify, GitGuardian, GitKraken, Tower, Shields.io, git-annex, Jekyll, Travis CI and HelloGitHub. * Star History's blog (scraped index) and newsletter (native Beehiiv RSS). + The newsletter is the requested rss.beehiiv.com/feeds/BbNzf9ozGZ.xml. + * travnie organization activity via GitHub's public organization Events API. + GitHub's private organization dashboard feed requires authentication and is + deliberately not mirrored into this public Feedseek feed. * The GitHubTrendingRSS streams. Daily, weekly and monthly list the same repositories over different windows, so they share one source label: the URL dedupe collapses the overlap and the per-source quota treats trending @@ -33,10 +37,11 @@ import argparse import re import sys -from urllib.parse import urljoin, urlparse +from urllib.parse import quote, urljoin, urlparse +import requests from bs4 import BeautifulSoup -from multi_rss import get_html, parse_date, run +from multi_rss import get_html, logger, parse_date, run from utils import sanitize_xml, stable_fallback_date FEED_NAME = "github" @@ -57,6 +62,13 @@ STAR_HISTORY_NEWSLETTER_RSS_URL = "https://rss.beehiiv.com/feeds/BbNzf9ozGZ.xml" _STAR_HISTORY_ARTICLE_RE = re.compile(r"^/blog/[^/?#]+/?$") +TRAVNIE_ORG = "travnie" +TRAVNIE_ACTIVITY_LABEL = "travnie GitHub Activity" +TRAVNIE_ACTIVITY_MAX = 80 +TRAVNIE_PUBLIC_EVENTS_URL = ( + f"https://api.github.com/orgs/{TRAVNIE_ORG}/events?per_page=100" +) + SOURCES = [ ("GitHub Changelog", "https://github.blog/changelog/feed/", 40), ("GitHub Engineering", "https://github.blog/engineering/feed/", 30), @@ -93,6 +105,7 @@ AWESOME_LISTS: 30, STAR_HISTORY_BLOG_LABEL: 20, STAR_HISTORY_NEWSLETTER_LABEL: 15, + TRAVNIE_ACTIVITY_LABEL: 40, } @@ -209,7 +222,254 @@ def scrape_star_history_blog(known_links): return _parse_star_history_blog(html, known_links) -EXTRA_SCRAPERS = (scrape_beeware_news, scrape_star_history_blog) + +_EVENT_TITLE_ACTIONS = { + "BranchProtectionRuleEvent": "updated branch protection in", + "CheckRunEvent": "updated a check run in", + "CheckSuiteEvent": "updated a check suite in", + "CommitCommentEvent": "commented on a commit in", + "DeleteEvent": "deleted a ref from", + "DiscussionCommentEvent": "commented on a discussion in", + "DiscussionEvent": "updated a discussion in", + "ForkEvent": "forked", + "GollumEvent": "updated wiki pages in", + "MemberEvent": "updated repository membership in", + "MembershipEvent": "updated team membership in", + "PublicEvent": "made public", + "RepositoryEvent": "updated repository settings in", + "SponsorshipEvent": "updated sponsorship in", + "StatusEvent": "updated commit status in", + "WatchEvent": "starred", +} + + +def _travnie_api_headers(): + """Build headers for GitHub's public organization Events API.""" + return { + "Accept": "application/vnd.github+json", + "User-Agent": "feedseek/github", + "X-GitHub-Api-Version": "2026-03-10", + } + + +def _fetch_travnie_events(): + """Fetch public travnie organization events without authentication.""" + try: + response = requests.get( + TRAVNIE_PUBLIC_EVENTS_URL, + headers=_travnie_api_headers(), + timeout=30, + ) + except requests.RequestException as exc: + logger.warning("travnie public events fetch failed: %s", exc) + return [] + + if response.status_code != 200: + logger.warning( + "travnie public events returned HTTP %s", + response.status_code, + ) + return [] + + try: + payload = response.json() + except ValueError as exc: + logger.warning("travnie public events returned invalid JSON: %s", exc) + return [] + if isinstance(payload, list): + return payload + + logger.warning("travnie public events returned an unexpected payload") + return [] + + +def _event_payload_url(event): + """Prefer the page touched by an event over the repository homepage.""" + payload = event.get("payload") or {} + for key in ( + "comment", + "review", + "discussion", + "pull_request", + "issue", + "release", + "forkee", + ): + value = payload.get(key) + if not isinstance(value, dict): + continue + html_url = value.get("html_url") + if isinstance(html_url, str) and html_url.startswith("https://github.com/"): + return html_url + + repo = event.get("repo") or {} + repo_name = repo.get("name") if isinstance(repo, dict) else None + if not isinstance(repo_name, str) or "/" not in repo_name: + return "" + + base = f"https://github.com/{repo_name}" + if event.get("type") == "PushEvent": + head = payload.get("head") + if isinstance(head, str) and re.fullmatch(r"[0-9a-fA-F]{7,40}", head): + return f"{base}/commit/{head}" + + if event.get("type") == "CreateEvent": + ref = payload.get("ref") + ref_type = payload.get("ref_type") + if ( + isinstance(ref, str) + and ref + and ref_type in {"tag", "branch"} + ): + return f"{base}/tree/{quote(ref, safe='')}" + + return base + + +def _event_title(event): + """Create a compact human title from a GitHub Events API object.""" + event_type = str(event.get("type") or "GitHubEvent") + actor_obj = event.get("actor") or {} + repo_obj = event.get("repo") or {} + payload = event.get("payload") or {} + + actor = "Someone" + if isinstance(actor_obj, dict): + actor = ( + actor_obj.get("display_login") + or actor_obj.get("login") + or actor + ) + repo = TRAVNIE_ORG + if isinstance(repo_obj, dict): + repo_name = repo_obj.get("name") + if isinstance(repo_name, str) and repo_name: + repo = repo_name + + if event_type == "PullRequestEvent": + number = payload.get("number") + action = payload.get("action") or "updated" + return f"{actor} {action} PR #{number} in {repo}" + + if event_type in {"IssuesEvent", "IssueCommentEvent"}: + issue = payload.get("issue") or {} + number = issue.get("number") if isinstance(issue, dict) else None + if event_type == "IssueCommentEvent": + comment = payload.get("comment") or {} + comment_id = comment.get("id") if isinstance(comment, dict) else None + suffix = f" · comment {comment_id}" if comment_id else "" + is_pull_request = ( + isinstance(issue, dict) + and isinstance(issue.get("pull_request"), dict) + ) + target = "PR" if is_pull_request else "issue" + return f"{actor} commented on {target} #{number} in {repo}{suffix}" + verb = payload.get("action") or "updated" + return f"{actor} {verb} issue #{number} in {repo}" + + if event_type == "PullRequestReviewCommentEvent": + pull_request = payload.get("pull_request") or {} + number = ( + pull_request.get("number") + if isinstance(pull_request, dict) + else None + ) + comment = payload.get("comment") or {} + comment_id = comment.get("id") if isinstance(comment, dict) else None + suffix = f" · comment {comment_id}" if comment_id else "" + return f"{actor} commented on PR #{number} in {repo}{suffix}" + + if event_type == "PullRequestReviewEvent": + pull_request = payload.get("pull_request") or {} + number = ( + pull_request.get("number") + if isinstance(pull_request, dict) + else None + ) + review = payload.get("review") or {} + review_id = review.get("id") if isinstance(review, dict) else None + suffix = f" · review {review_id}" if review_id else "" + return f"{actor} reviewed PR #{number} in {repo}{suffix}" + + if event_type == "PushEvent": + ref = str(payload.get("ref") or "").removeprefix("refs/heads/") + head = payload.get("head") + ref_suffix = f" ({ref})" if ref else "" + head_suffix = ( + f" · {head[:7]}" + if isinstance(head, str) and re.fullmatch(r"[0-9a-fA-F]{7,40}", head) + else "" + ) + return f"{actor} pushed to {repo}{ref_suffix}{head_suffix}" + + if event_type == "ReleaseEvent": + release = payload.get("release") or {} + tag = release.get("tag_name") if isinstance(release, dict) else None + action = payload.get("action") or "published" + suffix = f" {tag}" if tag else "" + return f"{actor} {action} release{suffix} in {repo}" + + if event_type == "CreateEvent": + ref_type = payload.get("ref_type") or "ref" + ref = payload.get("ref") + suffix = f" {ref}" if ref else "" + return f"{actor} created {ref_type}{suffix} in {repo}" + + verb = _EVENT_TITLE_ACTIONS.get(event_type) + if verb: + return f"{actor} {verb} {repo}" + + event_name = re.sub(r"Event$", "", event_type) + event_name = re.sub(r"(?= TRAVNIE_ACTIVITY_MAX: + break + return entries + + +EXTRA_SCRAPERS = ( + scrape_beeware_news, + scrape_star_history_blog, + scrape_travnie_activity, +) def doc_sources(): @@ -218,6 +478,7 @@ def doc_sources(): (BEEWARE_LABEL, BEEWARE_NEWS_URL), ("Star History", STAR_HISTORY_URL), (STAR_HISTORY_BLOG_LABEL, STAR_HISTORY_BLOG_URL), + (TRAVNIE_ACTIVITY_LABEL, f"https://github.com/orgs/{TRAVNIE_ORG}"), ] @@ -234,8 +495,8 @@ def main(full=False): title="GitHub", subtitle="Combined GitHub feed: GitHub blogs and changelogs, GitHub " "Status, Mergify, BeeWare News, Star History, Komi Store, the Git " - "tooling ecosystem, Track Awesome List and deduplicated GitHub trending " - "streams.", + "tooling ecosystem, Track Awesome List, travnie organization activity " + "and deduplicated GitHub trending streams.", blog_url="https://github.blog/", author="GitHub", sources=SOURCES, @@ -243,6 +504,7 @@ def main(full=False): max_entries=400, per_source_cap=PER_SOURCE_QUOTA, cache_filter=_active_cache_entry, + dedupe_title_field=None, full=full, ) diff --git a/tests/test_github_requested_sources.py b/tests/test_github_requested_sources.py new file mode 100644 index 0000000000..0b91fe3d55 --- /dev/null +++ b/tests/test_github_requested_sources.py @@ -0,0 +1,260 @@ +import sys +import unittest +from pathlib import Path +from unittest.mock import Mock, patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "feed_generators")) + +import github + + +class GitHubRequestedSourcesTests(unittest.TestCase): + def test_requested_beehiiv_feed_is_already_registered(self): + self.assertIn( + ( + github.STAR_HISTORY_NEWSLETTER_LABEL, + "https://rss.beehiiv.com/feeds/BbNzf9ozGZ.xml", + 15, + ), + github.SOURCES, + ) + + def test_public_travnie_events_need_no_token(self): + response = Mock() + response.status_code = 200 + response.json.return_value = [] + + with patch.object(github.requests, "get", return_value=response) as get: + self.assertEqual(github._fetch_travnie_events(), []) + + self.assertEqual(get.call_args.args[0], github.TRAVNIE_PUBLIC_EVENTS_URL) + self.assertNotIn("Authorization", get.call_args.kwargs["headers"]) + + def test_private_event_is_not_published_even_if_returned_defensively(self): + private_event = { + "id": "secret-event", + "type": "PushEvent", + "public": False, + "actor": {"login": "octocat"}, + "repo": {"name": "travnie/private-repo"}, + "created_at": "2026-10-04T12:00:00Z", + "payload": { + "ref": "refs/heads/main", + "head": "a" * 40, + }, + } + + with patch.object( + github, + "_fetch_travnie_events", + return_value=[private_event], + ): + self.assertEqual(github.scrape_travnie_activity(set()), []) + + def test_push_titles_include_head_to_avoid_title_dedupe_collisions(self): + base = { + "type": "PushEvent", + "public": True, + "actor": {"login": "octocat"}, + "repo": {"name": "travnie/example"}, + "created_at": "2026-10-04T12:34:56Z", + "payload": {"ref": "refs/heads/main"}, + } + first = { + **base, + "id": "1", + "payload": {**base["payload"], "head": "a" * 40}, + } + second = { + **base, + "id": "2", + "payload": {**base["payload"], "head": "b" * 40}, + } + + first_entry = github._normalize_travnie_event(first) + second_entry = github._normalize_travnie_event(second) + + self.assertNotEqual(first_entry["title"], second_entry["title"]) + self.assertIn("aaaaaaa", first_entry["title"]) + self.assertIn("bbbbbbb", second_entry["title"]) + + def test_comment_event_links_to_specific_comment_anchor(self): + event = { + "id": "comment-event", + "type": "IssueCommentEvent", + "public": True, + "actor": {"login": "octocat"}, + "repo": {"name": "travnie/example"}, + "created_at": "2026-10-04T12:34:56Z", + "payload": { + "issue": { + "number": 7, + "html_url": "https://github.com/travnie/example/issues/7", + }, + "comment": { + "id": 123, + "html_url": ( + "https://github.com/travnie/example/issues/7" + "#issuecomment-123" + ), + }, + }, + } + + entry = github._normalize_travnie_event(event) + + self.assertEqual( + entry["link"], + "https://github.com/travnie/example/issues/7#issuecomment-123", + ) + self.assertIn("comment 123", entry["title"]) + + def test_review_event_prefers_specific_review_url(self): + event = { + "id": "review-event", + "type": "PullRequestReviewEvent", + "public": True, + "actor": {"login": "octocat"}, + "repo": {"name": "travnie/example"}, + "created_at": "2026-10-04T12:34:56Z", + "payload": { + "pull_request": { + "number": 11, + "html_url": "https://github.com/travnie/example/pull/11", + }, + "review": { + "id": 789, + "html_url": ( + "https://github.com/travnie/example/pull/11" + "#pullrequestreview-789" + ), + }, + }, + } + + entry = github._normalize_travnie_event(event) + + self.assertEqual( + entry["link"], + "https://github.com/travnie/example/pull/11#pullrequestreview-789", + ) + self.assertIn("review 789", entry["title"]) + + def test_less_common_event_type_has_readable_action(self): + event = { + "id": "commit-comment-event", + "type": "CommitCommentEvent", + "public": True, + "actor": {"login": "octocat"}, + "repo": {"name": "travnie/example"}, + "created_at": "2026-10-04T12:34:56Z", + "payload": { + "comment": { + "id": 321, + "html_url": ( + "https://github.com/travnie/example/commit/abc" + "#commitcomment-321" + ), + } + }, + } + + entry = github._normalize_travnie_event(event) + + self.assertIn("commented on a commit", entry["title"]) + self.assertNotIn("commitcomment", entry["title"]) + + def test_issue_comment_on_pull_request_is_labeled_as_pr(self): + event = { + "id": "pr-comment-event", + "type": "IssueCommentEvent", + "public": True, + "actor": {"login": "octocat"}, + "repo": {"name": "travnie/example"}, + "created_at": "2026-10-04T12:34:56Z", + "payload": { + "issue": { + "number": 9, + "pull_request": { + "url": "https://api.github.com/repos/travnie/example/pulls/9" + }, + "html_url": "https://github.com/travnie/example/pull/9", + }, + "comment": { + "id": 456, + "html_url": ( + "https://github.com/travnie/example/pull/9" + "#issuecomment-456" + ), + }, + }, + } + + entry = github._normalize_travnie_event(event) + + self.assertIn("commented on PR #9", entry["title"]) + self.assertEqual( + entry["link"], + "https://github.com/travnie/example/pull/9#issuecomment-456", + ) + + def test_tag_create_event_links_to_git_tag_tree(self): + event = { + "id": "tag-event", + "type": "CreateEvent", + "public": True, + "actor": {"login": "octocat"}, + "repo": {"name": "travnie/example"}, + "created_at": "2026-10-04T12:34:56Z", + "payload": {"ref_type": "tag", "ref": "v1.2.3"}, + } + + entry = github._normalize_travnie_event(event) + + self.assertEqual( + entry["link"], + "https://github.com/travnie/example/tree/v1.2.3#feedseek-event-tag-event", + ) + + def test_github_feed_deduplicates_by_url_not_reused_titles(self): + with patch.object(github, "run", return_value=True) as run: + self.assertTrue(github.main()) + + self.assertIsNone(run.call_args.kwargs["dedupe_title_field"]) + + def test_normalizes_pull_request_event_with_unique_identity(self): + event = { + "id": "123456789", + "type": "PullRequestEvent", + "actor": {"login": "octocat"}, + "repo": {"name": "travnie/example"}, + "created_at": "2026-10-04T12:34:56Z", + "payload": { + "action": "opened", + "number": 42, + "pull_request": { + "html_url": "https://github.com/travnie/example/pull/42" + }, + }, + } + + entry = github._normalize_travnie_event(event) + + self.assertEqual( + entry["link"], + "https://github.com/travnie/example/pull/42" + "#feedseek-event-123456789", + ) + self.assertEqual( + entry["title"], + "octocat opened PR #42 in travnie/example", + ) + self.assertEqual(entry["source"], github.TRAVNIE_ACTIVITY_LABEL) + self.assertEqual( + entry["date"].isoformat(), + "2026-10-04T12:34:56+00:00", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_github_sources.py b/tests/test_github_sources.py index c039b24c05..1eeb8b625a 100644 --- a/tests/test_github_sources.py +++ b/tests/test_github_sources.py @@ -18,7 +18,11 @@ def test_requested_engineering_sources_are_wired_in(self): self.assertNotIn("Devin Desktop", source_urls) self.assertEqual( github.EXTRA_SCRAPERS, - (github.scrape_beeware_news, github.scrape_star_history_blog), + ( + github.scrape_beeware_news, + github.scrape_star_history_blog, + github.scrape_travnie_activity, + ), ) self.assertFalse(github._active_cache_entry({"source": "Devin Desktop"})) self.assertFalse(github._active_cache_entry({"source": "Devin Release Notes"})) From e3016de9c8cfe7c6d8a91b44c270d15d62eb005b Mon Sep 17 00:00:00 2001 From: trvny <120686325+trvny@users.noreply.github.com> Date: Mon, 5 Oct 2026 00:44:21 +0200 Subject: [PATCH 2/5] fix(github): preserve activity event semantics --- feed_generators/github.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/feed_generators/github.py b/feed_generators/github.py index bd711e68ab..2095a0a227 100644 --- a/feed_generators/github.py +++ b/feed_generators/github.py @@ -37,7 +37,7 @@ import argparse import re import sys -from urllib.parse import quote, urljoin, urlparse +from urllib.parse import parse_qsl, quote, urlencode, urljoin, urlparse, urlsplit, urlunsplit import requests from bs4 import BeautifulSoup @@ -230,7 +230,6 @@ def scrape_star_history_blog(known_links): "CommitCommentEvent": "commented on a commit in", "DeleteEvent": "deleted a ref from", "DiscussionCommentEvent": "commented on a discussion in", - "DiscussionEvent": "updated a discussion in", "ForkEvent": "forked", "GollumEvent": "updated wiki pages in", "MemberEvent": "updated repository membership in", @@ -388,8 +387,13 @@ def _event_title(event): ) review = payload.get("review") or {} review_id = review.get("id") if isinstance(review, dict) else None - suffix = f" · review {review_id}" if review_id else "" - return f"{actor} reviewed PR #{number} in {repo}{suffix}" + suffix = f" {review_id}" if review_id else "" + action = payload.get("action") or "created" + return f"{actor} {action} review{suffix} on PR #{number} in {repo}" + + if event_type == "DiscussionEvent": + action = payload.get("action") or "created" + return f"{actor} {action} a discussion in {repo}" if event_type == "PushEvent": ref = str(payload.get("ref") or "").removeprefix("refs/heads/") @@ -424,6 +428,24 @@ def _event_title(event): return f"{actor} triggered {event_name} in {repo}" +def _event_identity_url(base_link, event_id): + """Keep canonical anchors while giving every GitHub event a durable identity.""" + parsed = urlsplit(base_link) + if parsed.fragment: + query = parse_qsl(parsed.query, keep_blank_values=True) + query.append(("feedseek_event", event_id)) + return urlunsplit( + ( + parsed.scheme, + parsed.netloc, + parsed.path, + urlencode(query), + parsed.fragment, + ) + ) + return f"{base_link}#feedseek-event-{quote(event_id, safe='')}" + + def _normalize_travnie_event(event): """Convert one GitHub Events API object into a Feedseek entry.""" if not isinstance(event, dict): @@ -436,10 +458,7 @@ def _normalize_travnie_event(event): base_link = _event_payload_url(event) if not base_link: return None - if "#" in base_link: - link = base_link - else: - link = f"{base_link}#feedseek-event-{event_id}" + link = _event_identity_url(base_link, event_id) title = sanitize_xml(_event_title(event)) return { "title": title, From f0dff188fb90527dcfb94aa8e8dd88bb6a004938 Mon Sep 17 00:00:00 2001 From: trvny <120686325+trvny@users.noreply.github.com> Date: Mon, 5 Oct 2026 00:44:36 +0200 Subject: [PATCH 3/5] test(github): cover review event identity --- tests/test_github_requested_sources.py | 75 ++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/tests/test_github_requested_sources.py b/tests/test_github_requested_sources.py index 0b91fe3d55..1793bc3330 100644 --- a/tests/test_github_requested_sources.py +++ b/tests/test_github_requested_sources.py @@ -105,7 +105,8 @@ def test_comment_event_links_to_specific_comment_anchor(self): self.assertEqual( entry["link"], - "https://github.com/travnie/example/issues/7#issuecomment-123", + "https://github.com/travnie/example/issues/7" + "?feedseek_event=comment-event#issuecomment-123", ) self.assertIn("comment 123", entry["title"]) @@ -122,6 +123,7 @@ def test_review_event_prefers_specific_review_url(self): "number": 11, "html_url": "https://github.com/travnie/example/pull/11", }, + "action": "created", "review": { "id": 789, "html_url": ( @@ -136,9 +138,73 @@ def test_review_event_prefers_specific_review_url(self): self.assertEqual( entry["link"], - "https://github.com/travnie/example/pull/11#pullrequestreview-789", + "https://github.com/travnie/example/pull/11" + "?feedseek_event=review-event#pullrequestreview-789", + ) + self.assertIn("created review 789 on PR #11", entry["title"]) + + def test_review_actions_keep_distinct_event_identity(self): + base = { + "type": "PullRequestReviewEvent", + "public": True, + "actor": {"login": "octocat"}, + "repo": {"name": "travnie/example"}, + "created_at": "2026-10-04T12:34:56Z", + "payload": { + "pull_request": { + "number": 11, + "html_url": "https://github.com/travnie/example/pull/11", + }, + "review": { + "id": 789, + "html_url": ( + "https://github.com/travnie/example/pull/11" + "#pullrequestreview-789" + ), + }, + }, + } + + entries = [] + for event_id, action in ( + ("review-updated", "updated"), + ("review-dismissed", "dismissed"), + ): + event = { + **base, + "id": event_id, + "payload": {**base["payload"], "action": action}, + } + entries.append(github._normalize_travnie_event(event)) + + self.assertNotEqual(entries[0]["link"], entries[1]["link"]) + self.assertTrue(entries[0]["link"].endswith("#pullrequestreview-789")) + self.assertTrue(entries[1]["link"].endswith("#pullrequestreview-789")) + self.assertIn("updated review 789 on PR #11", entries[0]["title"]) + self.assertIn("dismissed review 789 on PR #11", entries[1]["title"]) + + def test_discussion_event_preserves_created_action(self): + event = { + "id": "discussion-event", + "type": "DiscussionEvent", + "public": True, + "actor": {"login": "octocat"}, + "repo": {"name": "travnie/example"}, + "created_at": "2026-10-04T12:34:56Z", + "payload": { + "action": "created", + "discussion": { + "html_url": "https://github.com/orgs/travnie/discussions/12", + }, + }, + } + + entry = github._normalize_travnie_event(event) + + self.assertEqual( + entry["title"], + "octocat created a discussion in travnie/example", ) - self.assertIn("review 789", entry["title"]) def test_less_common_event_type_has_readable_action(self): event = { @@ -195,7 +261,8 @@ def test_issue_comment_on_pull_request_is_labeled_as_pr(self): self.assertIn("commented on PR #9", entry["title"]) self.assertEqual( entry["link"], - "https://github.com/travnie/example/pull/9#issuecomment-456", + "https://github.com/travnie/example/pull/9" + "?feedseek_event=pr-comment-event#issuecomment-456", ) def test_tag_create_event_links_to_git_tag_tree(self): From 53c1bb1d65cfd2429db4c559ff60614beee09dcd Mon Sep 17 00:00:00 2001 From: trvny <120686325+trvny@users.noreply.github.com> Date: Mon, 5 Oct 2026 00:50:18 +0200 Subject: [PATCH 4/5] fix(github): link wiki activity to pages --- feed_generators/github.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/feed_generators/github.py b/feed_generators/github.py index 2095a0a227..c36a265df6 100644 --- a/feed_generators/github.py +++ b/feed_generators/github.py @@ -285,6 +285,19 @@ def _fetch_travnie_events(): def _event_payload_url(event): """Prefer the page touched by an event over the repository homepage.""" payload = event.get("payload") or {} + if event.get("type") == "GollumEvent": + pages = payload.get("pages") + if isinstance(pages, list): + for page in pages: + if not isinstance(page, dict): + continue + html_url = page.get("html_url") + if ( + isinstance(html_url, str) + and html_url.startswith("https://github.com/") + ): + return html_url + for key in ( "comment", "review", @@ -322,6 +335,9 @@ def _event_payload_url(event): ): return f"{base}/tree/{quote(ref, safe='')}" + if event.get("type") == "GollumEvent": + return f"{base}/wiki" + return base From 91c2e77791fb27a3f86efd6bd6d653981d79ef38 Mon Sep 17 00:00:00 2001 From: trvny <120686325+trvny@users.noreply.github.com> Date: Mon, 5 Oct 2026 00:50:33 +0200 Subject: [PATCH 5/5] test(github): cover wiki activity links --- tests/test_github_requested_sources.py | 47 ++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/test_github_requested_sources.py b/tests/test_github_requested_sources.py index 1793bc3330..5df492daf4 100644 --- a/tests/test_github_requested_sources.py +++ b/tests/test_github_requested_sources.py @@ -206,6 +206,53 @@ def test_discussion_event_preserves_created_action(self): "octocat created a discussion in travnie/example", ) + def test_gollum_event_links_to_affected_wiki_page(self): + event = { + "id": "wiki-event", + "type": "GollumEvent", + "public": True, + "actor": {"login": "octocat"}, + "repo": {"name": "travnie/example"}, + "created_at": "2026-10-04T12:34:56Z", + "payload": { + "pages": [ + { + "page_name": "Home", + "title": "Home", + "action": "edited", + "html_url": "https://github.com/travnie/example/wiki/Home", + } + ] + }, + } + + entry = github._normalize_travnie_event(event) + + self.assertEqual( + entry["link"], + "https://github.com/travnie/example/wiki/Home" + "#feedseek-event-wiki-event", + ) + + def test_gollum_event_falls_back_to_repository_wiki(self): + event = { + "id": "wiki-fallback", + "type": "GollumEvent", + "public": True, + "actor": {"login": "octocat"}, + "repo": {"name": "travnie/example"}, + "created_at": "2026-10-04T12:34:56Z", + "payload": {"pages": []}, + } + + entry = github._normalize_travnie_event(event) + + self.assertEqual( + entry["link"], + "https://github.com/travnie/example/wiki" + "#feedseek-event-wiki-fallback", + ) + def test_less_common_event_type_has_readable_action(self): event = { "id": "commit-comment-event",