Add --issue-timeline flag and refresh timelines when cross-references change (#168) - #528
Open
Iamrodos wants to merge 2 commits into
Open
Add --issue-timeline flag and refresh timelines when cross-references change (#168)#528Iamrodos wants to merge 2 commits into
Iamrodos wants to merge 2 commits into
Conversation
…alez#168) The issue events endpoint never returns cross-references, so an issue mentioned from another issue or pull request leaves no trace in the backup. GitHub only exposes those through the timeline endpoint. Full backups miss them as well as incremental ones. Adds --issue-timeline, which stores the timeline under a separate timeline_data key alongside the unmodified event_data. The two endpoints are complementary rather than nested: the timeline adds cross-references, commits and reviews, while events carries referenced entries the timeline drops, so neither replaces the other. Timeline entries for comments are filtered out, since --issue-comments already saves the bodies. Everything else is stored verbatim. The flag is included in --all. This makes the data obtainable, but does not change incremental selection: a cross-reference does not bump the referenced issue's updated_at, so an incremental run still never lists it. That half of the issue remains open. References josegonzalez#168
Iamrodos
force-pushed
the
fix/168-issue-timeline
branch
from
July 26, 2026 09:15
76a8443 to
b29e948
Compare
Cross-references do not change an issue's updated_at, so an incremental run never lists the issue and never re-fetches its timeline. An issue can be referenced from another repository years after its last activity and the backup would never see it. On incremental runs with --issue-timeline, ask GraphQL for each item's cross-reference count and newest timestamp, compare both against what is already saved, and re-fetch the timeline of anything that differs. Items the since listing did not return are fetched individually. Both values are needed: the count alone would miss a reference being added and another deleted between runs, and the timestamp alone would miss a deletion. Pull requests are swept too when they are being stored as issues, which is the case unless --pulls is used. They need their own query because the issues connection excludes them, and because totalCount ignores the itemTypes filter on a pull request although it honours it on an issue, so their cross-references are counted from the nodes instead. Where that count may have been truncated, the comparison falls back to the timestamp alone rather than re-fetching on every run. This costs one rate-limit point per 100 items against the GraphQL quota, measured at 6 requests for a repository with 528 issues and pull requests, and stores no new state: the comparison is against the cross-references already in each item's timeline_data. A GraphQL failure warns and leaves the rest of the backup unaffected. Closes josegonzalez#168
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #168.
Adds an opt-in
--issue-timelineflag, which backs up GitHub's issue timeline under a separatetimeline_datakey on each issue, alongside the existing and unmodifiedevent_data.This captures cross-references, where one issue or pull request mentions another.
--issue-eventsreads/issues/{number}/events, which returns a restricted set of event types and never includes cross-references; GitHub only exposes those through/issues/{number}/timeline. So an issue mentioned from elsewhere shows no trace of it in the backup today.There are two faults behind the report and this fixes both. The data was not obtainable at all, so full backups missed it too, not just incremental ones. Separately, the reporter's own diagnosis about
sincewas correct: a cross-reference does not bump the referenced issue'supdated_at, so the issue is never listed and never revisited. The two are independent, and fixing only the endpoint would leave dormant issues stale forever.The reporter's own issue, eclipse-sumo/sumo#21, shows the scale of what is lost.
/eventsreturns 310 entries and not one of them is a cross-reference./timelinereturns 2,130 entries for the same issue, including 193 cross-references that the tool cannot see today.Details
--issue-timelinecomplements--issue-events, it does not replace it. Neither endpoint contains the other. The timeline adds cross-references, commits and reviews, but/eventscarriesreferencedentries the timeline drops, 130 of 254 across a full backup of this repository. Run both for complete coverage.event_datais untouched. Same endpoint, same contents, same shape, whether or not the timeline is enabled. Nothing that already reads it is affected.commentedentries are filtered out before storing. They embed the full comment body, which--issue-commentsalready backs up, and two copies invite divergence. On sumo#21 that is 1,627 of 2,130 entries.source.issueobject on cross-references, consistent with howevent_dataandreview_dataare stored.cross-referencedandcommittedcarry noidfield, so consumers oftimeline_datamust not assume one. Documented in the README.--all, on the grounds that an "everything" mode silently omitting a whole class of GitHub activity is the bug being reported. It is not marked[*]; the cost is bytes and requests, not clones.Impact
On cross-reference-heavy issues the timeline is materially heavier than events, because each
cross-referencedentry embeds the referencing issue in full. Measured through the tool on sumo#21, writing the same issue both ways:--issue-events--issue-timeline5.5x the requests and 9x the disk for one issue. The requests are unavoidable: the endpoint has no server-side type filter, so the 1,627 comments are paginated through and then discarded. sumo#21 is the worst case I could find; ordinary issues are unaffected, and a 512-item private repository of mine never exceeded 62 timeline entries on any issue.
The refresh check described below adds a GraphQL sweep on each incremental run, costing one rate-limit point per 100 items, against a quota separate from REST. Measured at 6 requests for this repository's 528 issues and pull requests. It is cheap in quota terms even at size, but not instant: sweeping eclipse-sumo/sumo's 18,219 items took around 200 points and five minutes.
The check also parses each stored issue JSON to read two values, so a large backup does one parse per item per run. That is local I/O rather than network, and caching the values would mean a sidecar checkpoint file, which is exactly what this design avoids. A faster JSON parser would help, but the project keeps its runtime dependencies minimal and this does not seem worth adding one for.
Keeping timelines current
Fetching the timeline is only half of it. Because being referenced does not change
updated_at, an incremental run never lists the issue and so never re-fetches its timeline. An issue can be mentioned from another repository years after its last activity.On incremental runs, github-backup now asks GraphQL for each item's cross-reference count and newest timestamp, compares both against what is already saved in
timeline_data, and re-fetches the timeline of anything that differs. Items thesincelisting did not return are fetched individually.Both values are compared because the count alone would miss a reference added and another deleted between runs, while the timestamp alone would miss a deletion.
Pull requests are checked too when they are stored as issues, which is the case unless
--pullsis used. They need a separate query for two reasons: GraphQL'sissuesconnection excludes them, andtotalCountignores theitemTypesfilter on a pull request although it honours it on an issue. Their cross-references are therefore counted from the nodes instead, falling back to comparing timestamps alone where that count may have been truncated. Without this, 390 of this repository's 526 stored items would never be checked.This needs no new state on disk — no checkpoint file, no interaction with the existing
last_updatemachinery — because the comparison is against the cross-references already stored. It self-heals after an interrupted run and picks up deletions. If the GraphQL call fails it warns and the backup continues, and it is skipped entirely when no token is configured, since GraphQL always requires one.Scope note
Backfill is partial when enabling the flag on an existing incremental backup. Cross-references on older issues are picked up automatically by the check above, since their stored count is zero and GitHub's is not. Timeline entries of other kinds, commits and reviews, are only fetched when the issue itself is updated, so those need one run without
--incrementalto fill in historically. Those kinds do moveupdated_at, so they stay current from then on.Testing
tests/test_issue_timeline.py, 14 tests. For the flag: wiring throughparse_args, thecommentedfilter combined with verbatim storage of everything else, no fetch without the flag,event_dataunaffected when both flags are set, inclusion in--all. For the refresh check: an added reference, an add paired with a delete where the count is unchanged, no false positive when nothing changed, a GraphQL failure warning rather than aborting, an itemsinceexcludes still being refreshed, multi-page cursor following, pull requests counted from nodes rather than the misleadingtotalCount, pull requests skipped when--pullshandles them, and a truncated count falling back to timestamp comparison.updated_atat2024-07-02, two years before the checkpoint, so thesincelisting could not return it. The incremental run reportedRefreshing 1 issues with new cross-referencesand Add an option to prefer checkouts over SSH #6 gained the reference. This is the exact failure reported in Incremental backup skips some new events #168.backup_issuesagainst sumo#21 with only the issue listing stubbed: 22 paginated requests, 2,130 entries fetched, 1,627commentedfiltered, 504 stored including all 194 cross-references. Neither full-repository run reached 100 entries on a single item, so this is the only test of multi-page timelines.--ignore=E501,E203,W503);rst-lint README.rstclean;twine checkclean on sdist and wheel; new code black-clean; README options block matches--helpcharacter for character.