π Fix GitHub repo browsing in Mission Explorer β use Contents API#4047
π Fix GitHub repo browsing in Mission Explorer β use Contents API#4047clubanderson merged 1 commit intomainfrom
Conversation
The Mission Explorer was calling phantom endpoints (/api/github/missions
and /api/github/missions/file) that don't exist as dedicated backend
routes. These went through the generic GitHub proxy which forwarded
them to api.github.com/missions/... β an invalid GitHub API path.
Fixed to use the GitHub Contents API through the existing proxy:
- Directory listing: /api/github/repos/{owner}/{repo}/contents/
- File content: /api/github/repos/{owner}/{repo}/contents/{path}
with base64 decoding of the response content field
Also filters directory listings to only show mission file extensions
(.json, .yaml, .yml, .md) using the existing isMissionFile() helper.
Signed-off-by: Andrew Anderson <andy@clubanderson.com>
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
β Deploy Preview for kubestellarconsole ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
π Hey @clubanderson β thanks for opening this PR!
This is an automated message. |
|
Thank you for your contribution! Your PR has been merged. Check out what's new:
Stay connected: Slack #kubestellar-dev | Multi-Cluster Survey |
There was a problem hiding this comment.
Pull request overview
Fixes Mission Explorerβs GitHub repo browsing by switching from invalid proxied endpoints to GitHubβs Contents API, enabling correct directory listings and file previews for watched repos.
Changes:
- Use
/api/github/repos/{owner}/{repo}/contents/{path}for directory listing instead of phantom/api/github/missionsendpoints. - Fetch file content via Contents API and decode base64 responses for preview/import.
- Filter GitHub listings to supported mission file extensions (
.json,.yaml,.yml,.md).
| const { data: ghEntries } = await api.get<Array<{ name: string; path: string; type: string; size?: number }>>( | ||
| `/api/github/repos/${repoPath}/contents/` |
There was a problem hiding this comment.
The Contents API path is constructed incorrectly for subdirectories. repoPath can be owner/repo/subpath, but /repos/${repoPath}/contents/ becomes /repos/{owner}/{repo}/{subpath}/contents, which is not a valid GitHub endpoint. Split repoPath into {owner, repo, subPath} and call /api/github/repos/${owner}/${repo}/contents/${subPath} (or omit ${subPath} for repo root).
| const { data: ghEntries } = await api.get<Array<{ name: string; path: string; type: string; size?: number }>>( | |
| `/api/github/repos/${repoPath}/contents/` | |
| const [owner, repo, ...subParts] = repoPath.split('/') | |
| const subPath = subParts.join('/') | |
| const contentsUrl = subPath | |
| ? `/api/github/repos/${owner}/${repo}/contents/${subPath}` | |
| : `/api/github/repos/${owner}/${repo}/contents` | |
| const { data: ghEntries } = await api.get<Array<{ name: string; path: string; type: string; size?: number }>>( | |
| contentsUrl |
| const { data: ghEntries } = await api.get<Array<{ name: string; path: string; type: string; size?: number }>>( | ||
| `/api/github/repos/${repoPath}/contents/` | ||
| ) | ||
| const entries: BrowseEntry[] = (ghEntries || []) |
There was a problem hiding this comment.
ghEntries is assumed to be an array, but the Contents API can return an object (file metadata) or an error payload (e.g., { message: ... }). If that happens, (ghEntries || []).filter(...) will throw at runtime. Guard with Array.isArray(ghEntries) (and handle non-array responses by showing an empty listing / error message).
| const entries: BrowseEntry[] = (ghEntries || []) | |
| const ghArray = Array.isArray(ghEntries) ? ghEntries : [] | |
| const entries: BrowseEntry[] = ghArray |
| content = data | ||
| // GitHub returns base64-encoded content for files | ||
| if (ghFile.content && ghFile.encoding === 'base64') { | ||
| content = atob(ghFile.content.replace(/\n/g, '')) |
There was a problem hiding this comment.
Decoding base64 via atob() can corrupt non-ASCII UTF-8 content (e.g., Markdown headings with unicode). Prefer decoding base64 to bytes and then new TextDecoder('utf-8').decode(...) so file previews render correctly for all UTF-8 text.
| content = atob(ghFile.content.replace(/\n/g, '')) | |
| const binaryString = atob(ghFile.content.replace(/\n/g, '')) | |
| const bytes = new Uint8Array(binaryString.length) | |
| for (let i = 0; i < binaryString.length; i += 1) { | |
| bytes[i] = binaryString.charCodeAt(i) | |
| } | |
| content = new TextDecoder('utf-8').decode(bytes) |
| } else if (ghFile.download_url) { | ||
| const rawResp = await fetch(ghFile.download_url) | ||
| content = await rawResp.text() | ||
| } else { |
There was a problem hiding this comment.
The download_url fallback uses a direct fetch() to raw.githubusercontent.com with no timeout, no status check, and no auth. This can hang, and it will fail for private repos (and potentially hit CORS) even when the proxied Contents API call succeeded with a PAT. Consider using the proxied GitHub API instead (e.g., use the sha from the contents response to call /api/github/repos/{owner}/{repo}/git/blobs/{sha}), or add a backend-side fetch for download_url so auth stays server-side and timeouts/status are enforced.
| const { data: ghFile } = await api.get<{ content?: string; encoding?: string; download_url?: string }>( | ||
| `/api/github/repos/${owner}/${repo}/contents/${filePath}` |
There was a problem hiding this comment.
Path segments are interpolated directly into the URL. If owner, repo, or filePath contains characters that need escaping (spaces, #, etc.), the request can break or hit the wrong endpoint. Encode path segments when building /api/github/repos/.../contents/... (encode each segment but preserve / separators).
| const { data: ghFile } = await api.get<{ content?: string; encoding?: string; download_url?: string }>( | |
| `/api/github/repos/${owner}/${repo}/contents/${filePath}` | |
| const encodedOwner = encodeURIComponent(owner) | |
| const encodedRepo = encodeURIComponent(repo) | |
| const encodedFilePath = filePath | |
| .split('/') | |
| .map((segment) => encodeURIComponent(segment)) | |
| .join('/') | |
| const { data: ghFile } = await api.get<{ content?: string; encoding?: string; download_url?: string }>( | |
| `/api/github/repos/${encodedOwner}/${encodedRepo}/contents/${encodedFilePath}` |
π Auto-Applying Copilot Code ReviewCopilot code review found 4 code suggestion(s) and 1 general comment(s). @copilot Please apply all of the following code review suggestions:
Also address these general comments:
Push all fixes in a single commit. Run Auto-generated by copilot-review-apply workflow. |
Summary
/api/github/missions?repo=...and/api/github/missions/file?path=...) that went through the generic proxy to invalid GitHub API pathsrepos/{owner}/{repo}/contents/for listing, with base64 content decoding for file fetchTest plan
clubanderson/sample-runbooksas a watched repo in Mission Explorer