Skip to content

πŸ› Fix GitHub repo browsing in Mission Explorer β€” use Contents API#4047

Merged
clubanderson merged 1 commit intomainfrom
fix/github-repo-browse-api
Apr 1, 2026
Merged

πŸ› Fix GitHub repo browsing in Mission Explorer β€” use Contents API#4047
clubanderson merged 1 commit intomainfrom
fix/github-repo-browse-api

Conversation

@clubanderson
Copy link
Copy Markdown
Collaborator

Summary

  • Fix Mission Explorer failing to list files from user GitHub repos (showing "Empty")
  • The browser was calling phantom endpoints (/api/github/missions?repo=... and /api/github/missions/file?path=...) that went through the generic proxy to invalid GitHub API paths
  • Now uses the GitHub Contents API: repos/{owner}/{repo}/contents/ for listing, with base64 content decoding for file fetch
  • Filters listings to mission file extensions (.json, .yaml, .yml, .md)

Test plan

  • Add clubanderson/sample-runbooks as a watched repo in Mission Explorer
  • Click to expand β€” should show 9 files (YAML + MD)
  • Click a YAML file β€” should show parsed mission or unstructured preview with detected CNCF projects
  • Click a Markdown file β€” should show parsed steps from headings

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>
Copilot AI review requested due to automatic review settings April 1, 2026 00:48
@kubestellar-prow kubestellar-prow bot added the dco-signoff: yes Indicates the PR's author has signed the DCO. label Apr 1, 2026
@kubestellar-prow
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign clubanderson for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 1, 2026

βœ… Deploy Preview for kubestellarconsole ready!

Name Link
πŸ”¨ Latest commit 125365d
πŸ” Latest deploy log https://app.netlify.com/projects/kubestellarconsole/deploys/69cc6b75d90069000803dd24
😎 Deploy Preview https://deploy-preview-4047.console-deploy-preview.kubestellar.io
πŸ“± Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@clubanderson clubanderson merged commit 1211cf5 into main Apr 1, 2026
19 of 20 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

πŸ‘‹ Hey @clubanderson β€” thanks for opening this PR!

πŸ€– This project is developed exclusively using AI coding assistants.

Please do not attempt to code anything for this project manually.
All contributions should be authored using an AI coding tool such as:

This ensures consistency in code style, architecture patterns, test coverage,
and commit quality across the entire codebase.


This is an automated message.

@kubestellar-prow kubestellar-prow bot added the size/M Denotes a PR that changes 30-99 lines, ignoring generated files. label Apr 1, 2026
@kubestellar-prow kubestellar-prow bot deleted the fix/github-repo-browse-api branch April 1, 2026 00:49
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

Thank you for your contribution! Your PR has been merged.

Check out what's new:

Stay connected: Slack #kubestellar-dev | Multi-Cluster Survey

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/missions endpoints.
  • 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).

Comment on lines +671 to +672
const { data: ghEntries } = await api.get<Array<{ name: string; path: string; type: string; size?: number }>>(
`/api/github/repos/${repoPath}/contents/`
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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

Copilot uses AI. Check for mistakes.
const { data: ghEntries } = await api.get<Array<{ name: string; path: string; type: string; size?: number }>>(
`/api/github/repos/${repoPath}/contents/`
)
const entries: BrowseEntry[] = (ghEntries || [])
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
const entries: BrowseEntry[] = (ghEntries || [])
const ghArray = Array.isArray(ghEntries) ? ghEntries : []
const entries: BrowseEntry[] = ghArray

Copilot uses AI. Check for mistakes.
content = data
// GitHub returns base64-encoded content for files
if (ghFile.content && ghFile.encoding === 'base64') {
content = atob(ghFile.content.replace(/\n/g, ''))
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)

Copilot uses AI. Check for mistakes.
Comment on lines +714 to +717
} else if (ghFile.download_url) {
const rawResp = await fetch(ghFile.download_url)
content = await rawResp.text()
} else {
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +708 to +709
const { data: ghFile } = await api.get<{ content?: string; encoding?: string; download_url?: string }>(
`/api/github/repos/${owner}/${repo}/contents/${filePath}`
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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}`

Copilot uses AI. Check for mistakes.
@clubanderson
Copy link
Copy Markdown
Collaborator Author

πŸ”„ Auto-Applying Copilot Code Review

Copilot code review found 4 code suggestion(s) and 1 general comment(s).

@copilot Please apply all of the following code review suggestions:

  • web/src/components/missions/MissionBrowser.tsx (line 672): const [owner, repo, ...subParts] = repoPath.split('/') const subPath =...
  • web/src/components/missions/MissionBrowser.tsx (line 674): const ghArray = Array.isArray(ghEntries) ? ghEntries : [] const entrie...
  • web/src/components/missions/MissionBrowser.tsx (line 713): const binaryString = atob(ghFile.content.replace(/\n/g, '')) const b...
  • web/src/components/missions/MissionBrowser.tsx (line 709): const encodedOwner = encodeURIComponent(owner) const encodedRepo = enc...

Also address these general comments:

  • web/src/components/missions/MissionBrowser.tsx (line 717): The download_url fallback uses a direct fetch() to raw.githubusercontent.com with no timeout, no status check, and

Push all fixes in a single commit. Run cd web && npm run build && npm run lint before committing.


Auto-generated by copilot-review-apply workflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: yes Indicates the PR's author has signed the DCO. size/M Denotes a PR that changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants