Skip to content

Fix template URL for watched repositories backup#521

Merged
josegonzalez merged 2 commits into
josegonzalez:masterfrom
PeterGabaldon:fix-deprecation-/users/{username}/subscriptions
Jul 7, 2026
Merged

Fix template URL for watched repositories backup#521
josegonzalez merged 2 commits into
josegonzalez:masterfrom
PeterGabaldon:fix-deprecation-/users/{username}/subscriptions

Conversation

@PeterGabaldon

Copy link
Copy Markdown
Contributor

Summary: github-backup --watched currently fails for personal account backups because GitHub has started returning empty responses from:

GET /users/{username}/subscriptions

This is now expected behavior per GitHub’s June 30, 2026 changelog:

GitHub states that /users/{username}/subscriptions is being deprecated and that, during the deprecation period, it may return empty responses or 403 Forbidden.

Reproduction

github-backup PeterGabaldon\
  --output-directory /tmp/github-backup-test \
  --log-level info \
  --incremental \
  --token <REDACTED_PAT> \
  --watched

Current result:

Requesting https://api.github.com/users/PeterGabaldon/subscriptions?per_page=100

Exception: Unexpected HTTP 204 from https://api.github.com/users/PeterGabaldon/subscriptions
(expected non-2xx to raise HTTPError)

The token is valid and includes the relevant permissions. The same token works with the authenticated-user endpoint:

curl -i \
  -H "Authorization: Bearer <REDACTED_PAT>" \
  -H "Accept: application/vnd.github+json" \
  "https://api.github.com/user/subscriptions?per_page=100"

That returns 200 OK with the watched repository JSON array.

Root Cause

--watched uses:

/users/{username}/subscriptions

That endpoint is no longer reliable for retrieving watched repositories, even when {username} is the authenticated user.

For backing up the authenticated user’s own watched repositories, GitHub now expects:

/user/subscriptions

Fix

Changed endpoint

@andrewferrier

Copy link
Copy Markdown

Thanks for this, I just opened issue #522 not realising you'd already created a PR!

@josegonzalez

Copy link
Copy Markdown
Owner

Mind fixing the lint issue and maybe adding a test if possible? /cc @Iamrodos

@Iamrodos

Iamrodos commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

The endpoint change looks right — I verified the changelog and can reproduce the 204 today, so --watched is broken for everyone and this is worth landing.

One thing worth flagging: this is potentially a breaking change for anyone backing up an account other than their own — through no fault of this PR. GitHub removed the ability to read another user's subscriptions entirely, so --watched for a different user simply can't work anymore, whatever we do. But the way the new endpoint fails is nasty: /user/subscriptions always returns the authenticated user's subscriptions, silently ignoring the username argument. So github-backup someoneelse --watched --token <mine> would write my watched repos into someoneelse/account/watched.json — wrong data in a backup, with no indication anything went wrong. Since GitHub has forced the behavior change on us, I think we should at least make it loud rather than silent. The codebase already handles this exact situation for starred gists (/gists/starred is also an authenticated-user-only endpoint): warn and skip when the requested user isn't the authenticated user.

The diff at the end of this comment follows that existing pattern. It also fixes the lint failure as a side effect — flake8 is flagging the now-unused args.user argument still being passed to the format string. I've tested it locally: backing up my own user saves watched repos via the new endpoint, a different username warns and skips instead of saving the wrong data, --as-app (no user login) also takes the skip path, and flake8 plus the full test suite pass.

Feel free to apply the diff to this PR directly — or take whatever pieces of it you find useful. If it's easier, I'm also happy to open a PR against your fork's branch so the changes land here with your authorship intact.

On tests: the useful one here is a regression guard that pins the endpoint choice, using the suite's existing mocked-urlopen pattern — (1) username matches the authenticated user → the request goes to /user/subscriptions (not the deprecated /users/{username}/subscriptions, so nobody "fixes" it back later), and (2) username doesn't match → no request is made and a warning is logged. Worth being honest that no mocked test would have caught GitHub flipping this endpoint in the first place — it can only protect the URL choice on our side. Happy to write those tests too if you'd like.

--- a/github_backup/cli.py
+++ b/github_backup/cli.py
@@ -81,7 +81,7 @@ def main():
     repositories = retrieve_repositories(args, authenticated_user)
     repositories = filter_repositories(args, repositories)
     backup_repositories(args, output_directory, repositories)
-    backup_account(args, output_directory)
+    backup_account(args, output_directory, authenticated_user)
 
 
 if __name__ == "__main__":
--- a/github_backup/github_backup.py
+++ b/github_backup/github_backup.py
@@ -3063,8 +3063,10 @@ def fetch_repository(
                 logging_subprocess(git_command, cwd=local_dir)
 
 
-def backup_account(args, output_directory):
+def backup_account(args, output_directory, authenticated_user=None):
     account_cwd = os.path.join(output_directory, "account")
+    if authenticated_user is None:
+        authenticated_user = {"login": None}
 
     if args.include_starred or args.include_everything:
         output_file = "{0}/starred.json".format(account_cwd)
@@ -3074,11 +3076,25 @@ def backup_account(args, output_directory):
         _backup_data(args, "starred repositories", template, output_file, account_cwd)
 
     if args.include_watched or args.include_everything:
-        output_file = "{0}/watched.json".format(account_cwd)
-        template = "https://{0}/users/{1}/subscriptions".format(
-            get_github_api_host(args), args.user
-        )
-        _backup_data(args, "watched repositories", template, output_file, account_cwd)
+        # GitHub deprecated /users/{username}/subscriptions (June 2026 changelog);
+        # it now returns empty responses. Watched repositories remain available
+        # only for the authenticated user, via /user/subscriptions.
+        if (
+            not authenticated_user.get("login")
+            or args.user.lower() != authenticated_user["login"].lower()
+        ):
+            logger.warning(
+                "Cannot retrieve watched repositories for '%s'. GitHub only allows access to the authenticated user's watched repositories.",
+                args.user,
+            )
+        else:
+            output_file = "{0}/watched.json".format(account_cwd)
+            template = "https://{0}/user/subscriptions".format(
+                get_github_api_host(args)
+            )
+            _backup_data(
+                args, "watched repositories", template, output_file, account_cwd
+            )
 
     if args.include_followers or args.include_everything:
         output_file = "{0}/followers.json".format(account_cwd)

@Iamrodos

Iamrodos commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Two more nit picky things to consider, both of which also apply to the original one-line fix — flagging, not blocking:

  1. Failure mode for under-permissioned tokens changes. The README already lists "watching" read access among the required fine-grained token permissions, but previously --watched worked even without it (public endpoint, no scope needed). Now a token missing that permission gets a 403 on /user/subscriptions, which propagates and aborts the run partway through the account backup (starred completes, followers/following never run). Probably acceptable — just a new failure mode to be aware of.

  2. GitHub Enterprise Server. The deprecation is a github.com change, and GHES being versioned on-prem software means existing installs keep serving /users/{username}/subscriptions until an upgrade removes it (the GHES 3.17 docs still list it with no deprecation notice — though I can't test a live instance to be certain). Might be worth keeping the old endpoint when a custom --github-host is configured, so GHE users don't lose cross-user watched backups before their server actually drops the endpoint. @josegonzalez your call on whether that's worth the complexity.

@josegonzalez

Copy link
Copy Markdown
Owner

If a GHE user complains, they can send us money to buy pizza or something to fix it :D

@PeterGabaldon

PeterGabaldon commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Applied the diff and fixed the lint issue.

Sorry about not noticing about Github's breaking change of reading subscriptions of any other user. I should have noticed that when applied the change.

@josegonzalez josegonzalez merged commit 3ecae56 into josegonzalez:master Jul 7, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants