-
Notifications
You must be signed in to change notification settings - Fork 3
Add CI status check #55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
63997b7
Add CI status check
amrit110 1f9666d
Fix metrics
amrit110 0827318
Merge branch 'main' into add_ci_status
amrit110 bd135ea
Potential fix for code scanning alert no. 2: Server-side request forgery
amrit110 b9f377d
Potential fix for code scanning alert no. 3: Server-side request forgery
amrit110 fd51030
Potential fix for code scanning alert no. 1: Use of externally-contro…
amrit110 00f111d
Fixes to logic
amrit110 032133d
Merge branch 'add_ci_status' of github.com:VectorInstitute/implementa…
amrit110 c3277aa
Fixes to security issues identified
amrit110 3239949
Fix security vulnerabilities in dependencies
amrit110 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
Large diffs are not rendered by default.
Oops, something went wrong.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,231 @@ | ||
| import { NextResponse } from 'next/server'; | ||
|
|
||
| export const dynamic = 'force-dynamic'; | ||
|
|
||
| interface CIStatusRequest { | ||
| repositories: string[]; // Array of repo_ids like "VectorInstitute/cyclops" | ||
| } | ||
|
|
||
| interface CIStatus { | ||
| repo_id: string; | ||
| state: 'success' | 'failure' | 'pending' | 'error' | 'unknown'; | ||
| total_checks: number; | ||
| updated_at: string; | ||
| details?: string; | ||
| } | ||
|
|
||
| function isValidRepoId(repo_id: string): boolean { | ||
| // Expect GitHub-style "owner/repo" with safe characters only | ||
| const trimmed = repo_id.trim(); | ||
| const repoPattern = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/; | ||
| return repoPattern.test(trimmed); | ||
| } | ||
|
|
||
| function isValidCommitSha(sha: string): boolean { | ||
| // Git SHAs are 40-character hexadecimal strings | ||
| const shaPattern = /^[a-f0-9]{40}$/; | ||
| return shaPattern.test(sha); | ||
| } | ||
|
|
||
| function buildGitHubApiUrl(path: string): URL { | ||
| // Always use the official GitHub API base URL to prevent SSRF | ||
| const baseUrl = 'https://api.github.com'; | ||
| // URL constructor will throw if path is malformed | ||
| return new URL(path, baseUrl); | ||
| } | ||
|
|
||
| export async function POST(request: Request) { | ||
| try { | ||
| const { repositories }: CIStatusRequest = await request.json(); | ||
|
|
||
| if (!Array.isArray(repositories)) { | ||
| return NextResponse.json( | ||
| { error: 'Invalid request: "repositories" must be an array of strings' }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| const token = process.env.GH_TOKEN || process.env.CATALOG_GITHUB_TOKEN || process.env.GITHUB_TOKEN || process.env.METRICS_GITHUB_TOKEN; | ||
|
|
||
| if (!token) { | ||
| // Return unknown status for all repos if token is not configured | ||
| const statusMap = repositories.reduce((acc, repo_id) => { | ||
| acc[repo_id] = { | ||
| repo_id, | ||
| state: 'unknown' as const, | ||
| total_checks: 0, | ||
| updated_at: new Date().toISOString(), | ||
| details: 'GitHub token not configured', | ||
| }; | ||
| return acc; | ||
| }, {} as Record<string, CIStatus>); | ||
|
|
||
| return NextResponse.json(statusMap); | ||
| } | ||
|
|
||
| // Fetch CI status for all repos in parallel | ||
| const statusPromises = repositories.map(async (repo_id) => { | ||
| // Validate repo_id before using it in an outbound request | ||
| const repoIdStr = String(repo_id).trim(); | ||
| if (!isValidRepoId(repoIdStr)) { | ||
| return { | ||
| repo_id: repoIdStr, | ||
| state: 'unknown' as const, | ||
| total_checks: 0, | ||
| updated_at: new Date().toISOString(), | ||
| details: 'Invalid repository identifier', | ||
| }; | ||
| } | ||
|
|
||
| try { | ||
| // First, get the latest commit SHA on main branch | ||
| // Use URL constructor to prevent SSRF | ||
| const branchUrl = buildGitHubApiUrl(`/repos/${encodeURIComponent(repoIdStr)}/branches/main`); | ||
| const branchResponse = await fetch( | ||
| branchUrl.toString(), | ||
| { | ||
| headers: { | ||
| 'Authorization': `Bearer ${token}`, | ||
| 'Accept': 'application/vnd.github+json', | ||
| 'X-GitHub-Api-Version': '2022-11-28', | ||
| }, | ||
| } | ||
| ); | ||
|
|
||
| if (!branchResponse.ok) { | ||
| if (branchResponse.status === 404) { | ||
| return { | ||
| repo_id, | ||
| state: 'unknown' as const, | ||
| total_checks: 0, | ||
| updated_at: new Date().toISOString(), | ||
| details: 'Main branch not found', | ||
| }; | ||
| } | ||
| throw new Error(`GitHub API error: ${branchResponse.status}`); | ||
| } | ||
|
|
||
| const branchData = await branchResponse.json(); | ||
| const latestCommitSha = branchData.commit.sha; | ||
|
|
||
| // Validate the commit SHA to prevent SSRF | ||
| if (!isValidCommitSha(latestCommitSha)) { | ||
| return { | ||
| repo_id, | ||
| state: 'unknown' as const, | ||
| total_checks: 0, | ||
| updated_at: new Date().toISOString(), | ||
| details: 'Invalid commit SHA received from API', | ||
| }; | ||
| } | ||
|
|
||
| // Now get check runs for this specific commit | ||
| // Use URL constructor to prevent SSRF | ||
| const checksUrl = buildGitHubApiUrl(`/repos/${encodeURIComponent(repoIdStr)}/commits/${latestCommitSha}/check-runs`); | ||
| const checksResponse = await fetch( | ||
| checksUrl.toString(), | ||
| { | ||
| headers: { | ||
| 'Authorization': `Bearer ${token}`, | ||
| 'Accept': 'application/vnd.github+json', | ||
| 'X-GitHub-Api-Version': '2022-11-28', | ||
| }, | ||
| } | ||
| ); | ||
|
||
|
|
||
| if (!checksResponse.ok) { | ||
| throw new Error(`GitHub API error: ${checksResponse.status}`); | ||
| } | ||
|
|
||
| const data = await checksResponse.json(); | ||
| const checkRuns = data.check_runs || []; | ||
|
|
||
| if (checkRuns.length === 0) { | ||
| return { | ||
| repo_id, | ||
| state: 'unknown' as const, | ||
| total_checks: 0, | ||
| updated_at: new Date().toISOString(), | ||
| details: 'No CI configured', | ||
| }; | ||
| } | ||
|
|
||
| // Check if any workflow runs failed | ||
| // Status: completed, in_progress, queued, waiting, requested, pending | ||
| // Conclusion (when completed): success, failure, neutral, cancelled, skipped, timed_out, action_required, startup_failure, stale | ||
| let hasFailure = false; | ||
| let hasPending = false; | ||
| let mostRecentUpdate = ''; | ||
|
|
||
| for (const check of checkRuns) { | ||
| // Skip Dependabot checks - they mark as "failure" for dependency conflicts which aren't CI failures | ||
| if (check.app?.slug === 'dependabot' || check.name === 'Dependabot') { | ||
| continue; | ||
| } | ||
|
|
||
| // If the check hasn't completed yet, mark as pending | ||
| if (check.status !== 'completed') { | ||
| hasPending = true; | ||
| } | ||
| // If completed, check the conclusion | ||
| else if (check.conclusion === 'failure' || | ||
| check.conclusion === 'timed_out' || | ||
| check.conclusion === 'action_required' || | ||
| check.conclusion === 'startup_failure') { | ||
| hasFailure = true; | ||
| } | ||
|
|
||
| // Track most recent update | ||
| const updateTime = check.completed_at || check.started_at; | ||
| if (updateTime && (!mostRecentUpdate || updateTime > mostRecentUpdate)) { | ||
| mostRecentUpdate = updateTime; | ||
| } | ||
| } | ||
|
|
||
| // Determine overall state based on the checks | ||
| let state: 'success' | 'failure' | 'pending' | 'error' | 'unknown'; | ||
| if (hasFailure) { | ||
| state = 'failure'; | ||
| } else if (hasPending) { | ||
| state = 'pending'; | ||
| } else { | ||
| // All checks completed without failure | ||
| state = 'success'; | ||
| } | ||
|
|
||
| return { | ||
| repo_id, | ||
| state, | ||
| total_checks: checkRuns.length, | ||
| updated_at: mostRecentUpdate || new Date().toISOString(), | ||
| details: `${checkRuns.length} check(s)`, | ||
| }; | ||
| } catch (error) { | ||
| console.error('Error fetching CI status for %s:', repo_id, error); | ||
| return { | ||
| repo_id, | ||
| state: 'unknown' as const, | ||
| total_checks: 0, | ||
| updated_at: new Date().toISOString(), | ||
| details: 'Error fetching status', | ||
| }; | ||
| } | ||
| }); | ||
|
|
||
| const results = await Promise.all(statusPromises); | ||
|
|
||
| // Convert array to object keyed by repo_id for easy lookup | ||
| const statusMap = results.reduce((acc, status) => { | ||
| acc[status.repo_id] = status; | ||
| return acc; | ||
| }, {} as Record<string, CIStatus>); | ||
|
|
||
| return NextResponse.json(statusMap); | ||
| } catch (error) { | ||
| console.error('CI status API error:', error); | ||
| return NextResponse.json( | ||
| { error: 'Failed to fetch CI statuses' }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
| } | ||
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.