Skip to content

Use the Markdown API workaround for GitHub App tokens and repo-scoped image URLs - #546

Open
gaupon wants to merge 1 commit into
josegonzalez:masterfrom
gaupon:app-token-jwt-attachments
Open

gaupon wants to merge 1 commit into
josegonzalez:masterfrom
gaupon:app-token-jwt-attachments

Conversation

@gaupon

@gaupon gaupon commented Nov 14, 2026

Copy link
Copy Markdown

Follows up on #477 / #478.

GitHub App installation tokens hit the same limitation as fine-grained PATs: downloading github.com/user-attachments/assets/* from a private repository returns 404. The Markdown API workaround from #478 works for them too, but it is only used when -f is given, so with --as-app / --app-id every image in a private repository is skipped.

The older repo-scoped image URLs (github.com/{owner}/{repo}/assets/{user_id}/{uuid}) have the same problem (the original report in #477 lists them as well) and are never routed through the workaround either. The Markdown API renders them to the same private-user-images.githubusercontent.com JWT-signed URLs.

Changes

  • Move the check into needs_jwt_workaround(args, repository, url). It is true for fine-grained or App tokens, on private repositories, for user-attachments/assets/* or repo-scoped /assets/ URLs. /files/ URLs still go the normal path, since the Markdown API does not sign them.
  • Pass get_auth(args, encode=False) to the Markdown API instead of args.token_fine. That is the raw token for both token types, and with --app-id it is the current (refreshed) installation token.
  • The skip message and README now mention App tokens.
  • The existing needs_jwt test re-implemented the condition inline, so it would not have caught this. It is replaced by a parametrized test of the real function, plus a test that download_attachments sends the installation token to the Markdown API and downloads the JWT URL without it.

Testing

  • pytest: 221 passed. flake8 --ignore=E501,E203,W503 and rst-lint README.rst are clean.
  • Live, against a private organization:
    • Same installation token (contents, issues and metadata read), same two private repositories, --issues --issue-comments --attachments --as-app -t <ghs_...>:

      0.68.0 this branch
      user-attachments/assets/* (7) 7 × 404 7 × 200, jwt_workaround: true
      {owner}/{repo}/assets/* (5) 5 × 404 5 × 200, jwt_workaround: true

      Every file has the same size as a direct download with an OAuth user token.

    • With a token that only has issues and metadata read, POST /markdown returns 200 but the image URLs in the HTML are not signed, so the workaround fails. The README now says the token needs read access to contents (already in the recommended permissions).

    • In a full backup of two organizations with --app-id, 71 of the 77 failed attachments were images in one of these two formats. The other 6 are /files/ (PDF, audio, markdown), which stay unavailable as documented.

Notes

  • Attachments that already failed with 404 are recorded in manifest.json and not retried, so existing backups only pick up the fixed images after the manifest entries are removed (or from a fresh backup directory). I left that alone rather than adding retry logic for one class of 404.
  • GitHub Enterprise Server is unaffected in practice. extract_attachment_urls() only matches github.com / githubusercontent.com URLs, so attachments hosted on a GHES instance are never picked up, with or without this change. The only URLs that reach this code on GHES are github.com attachments pasted into GHES issues, which a GHES token cannot download directly either.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JefeUY9THfz2WzRtBGEP3U

… image URLs

GitHub App installation tokens get the same 404 as fine-grained PATs when
downloading attachments from private repositories (josegonzalez#477), but the workaround
was only used with -f. Older github.com/{owner}/{repo}/assets/* image URLs
also 404 with both token types and render to the same JWT-signed URLs.

Move the check into needs_jwt_workaround() so it can be tested directly,
and pass the current token from get_auth() so App tokens are refreshed.

The README now also notes that the workaround needs read access to
contents: without it, POST /markdown returns 200 but leaves the image URLs
unsigned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JefeUY9THfz2WzRtBGEP3U
@Iamrodos

Copy link
Copy Markdown
Contributor

Looks good to merge. This fixes the same image download problem we had with fine-grained tokens, but for App tokens and the older image URLs. Thanks for the contribution.

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.

2 participants