forked from kata-containers/kata-containers.github.io
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
Merge pull request kata-containers#16 from a1icja/main
migration: Move from vanilla JS to Next.JS
Showing
24 changed files
with
6,393 additions
and
302 deletions.
There are no files selected for viewing
This file contains 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,3 @@ | ||
{ | ||
"extends": ["next/core-web-vitals", "next/typescript"] | ||
} |
This file contains 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,100 @@ | ||
# Reference: https://github.com/actions/starter-workflows/blob/main/pages/nextjs.yml | ||
|
||
# Sample workflow for building and deploying a Next.js site to GitHub Pages | ||
# | ||
# To get started with Next.js see: https://nextjs.org/docs/getting-started | ||
# | ||
name: Deploy Next.js site to Pages | ||
|
||
on: | ||
# When a commit is made to the main branch | ||
push: | ||
branches: | ||
- main | ||
|
||
# Allows you to run this workflow manually from the Actions tab | ||
workflow_dispatch: | ||
|
||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages | ||
permissions: | ||
contents: read | ||
pages: write | ||
id-token: write | ||
|
||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. | ||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. | ||
concurrency: | ||
group: "pages" | ||
cancel-in-progress: false | ||
|
||
jobs: | ||
# Build job | ||
build: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v4 | ||
|
||
- name: Detect package manager | ||
id: detect-package-manager | ||
run: | | ||
if [ -f "${{ github.workspace }}/yarn.lock" ]; then | ||
echo "manager=yarn" >> $GITHUB_OUTPUT | ||
echo "command=install" >> $GITHUB_OUTPUT | ||
echo "runner=yarn" >> $GITHUB_OUTPUT | ||
exit 0 | ||
elif [ -f "${{ github.workspace }}/package.json" ]; then | ||
echo "manager=npm" >> $GITHUB_OUTPUT | ||
echo "command=ci" >> $GITHUB_OUTPUT | ||
echo "runner=npx --no-install" >> $GITHUB_OUTPUT | ||
exit 0 | ||
else | ||
echo "Unable to determine package manager" | ||
exit 1 | ||
fi | ||
- name: Setup Node | ||
uses: actions/setup-node@v4 | ||
with: | ||
node-version: "lts/*" | ||
cache: ${{ steps.detect-package-manager.outputs.manager }} | ||
|
||
- name: Setup Pages | ||
uses: actions/configure-pages@v4 | ||
|
||
- name: Restore cache | ||
uses: actions/cache@v4 | ||
with: | ||
path: | | ||
.next/cache | ||
# Generate a new cache whenever packages or source files change. | ||
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} | ||
# If source files changed but packages didn't, rebuild from a prior cache. | ||
restore-keys: | | ||
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}- | ||
- name: Install dependencies | ||
run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} | ||
|
||
- name: Build with Next.js | ||
run: ${{ steps.detect-package-manager.outputs.runner }} next build | ||
|
||
- name: Add .nojekyll to out directory | ||
run: touch out/.nojekyll | ||
|
||
- name: Upload artifact | ||
uses: actions/upload-pages-artifact@v3 | ||
with: | ||
path: ./out | ||
|
||
# Deployment job | ||
deploy: | ||
environment: | ||
name: github-pages | ||
url: ${{ steps.deployment.outputs.page_url }} | ||
runs-on: ubuntu-latest | ||
needs: build | ||
steps: | ||
- name: Deploy to GitHub Pages | ||
id: deployment | ||
uses: actions/deploy-pages@v4 |
This file contains 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
This file contains 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 |
---|---|---|
@@ -1,2 +1,39 @@ | ||
_site/ | ||
*.swp | ||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
||
# dependencies | ||
/node_modules | ||
/.pnp | ||
.pnp.js | ||
.yarn/install-state.gz | ||
|
||
# testing | ||
/coverage | ||
|
||
# next.js | ||
/.next/ | ||
/out/ | ||
|
||
# production | ||
/build | ||
|
||
# misc | ||
.DS_Store | ||
*.pem | ||
.vscode/* | ||
|
||
# debug | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
|
||
# local env files | ||
.env*.local | ||
|
||
# vercel | ||
.vercel | ||
|
||
# typescript | ||
*.tsbuildinfo | ||
next-env.d.ts | ||
|
||
/job_stats.json |
This file contains 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,101 @@ | ||
# Kata Containers Test Dashboard | ||
|
||
This repository contains the **Kata Containers Test Dashboard**, a web application that visualizes data for the nightly tests run by the Kata Containers repository. Built using **Next.js** and styled with **TailwindCSS**, this dashboard provides a simple and efficient interface to monitor test results, leveraging modern frontend technologies to ensure responsive and scalable performance. | ||
|
||
## Features | ||
- Fetches nightly CI test data using custom scripts. | ||
- Displays weather-like icons to reflect test statuses (e.g., sunny for success, stormy for failures). | ||
- Utilizes **Next.js** for server-side rendering and optimized builds. | ||
- **TailwindCSS** for responsive, utility-first styling. | ||
- Integration of **PrimeReact** components for UI elements. | ||
|
||
--- | ||
|
||
## Project Structure | ||
|
||
```bash | ||
. | ||
├── next.config.js # Next.js configuration | ||
├── package.json # Project dependencies and scripts | ||
├── package-lock.json # Dependency lock file | ||
├── pages | ||
│ ├── _app.js # Application wrapper for global setup | ||
│ └── index.js # Main dashboard page | ||
├── postcss.config.js # PostCSS configuration for TailwindCSS | ||
├── public | ||
│ ├── cloudy.svg # Weather icons for test statuses | ||
│ ├── favicon.ico | ||
│ ├── partially-sunny.svg | ||
│ ├── rainy.svg | ||
│ ├── stormy.svg | ||
│ └── sunny.svg | ||
├── README.md # Project documentation (this file) | ||
├── scripts | ||
│ └── fetch-ci-nightly-data.js # Script to fetch nightly test data | ||
├── styles | ||
│ └── globals.css # Global CSS imports | ||
└── tailwind.config.js # TailwindCSS configuration | ||
``` | ||
|
||
### Key Files | ||
- **`pages/index.js`**: The main entry point of the dashboard, displaying test results and their statuses. | ||
- **`scripts/fetch-ci-nightly-data.js`**: Custom script to retrieve CI data for the dashboard. | ||
- **`styles/globals.css`**: Custom global styles, mainly extending the TailwindCSS base. | ||
- **`public/`**: Contains static assets like icons representing different test statuses. | ||
|
||
--- | ||
|
||
## Setup Instructions | ||
|
||
Follow these steps to set up the development environment for the Kata Containers Test Dashboard: | ||
|
||
### Prerequisites | ||
- [**Node.js**](https://nodejs.org/en/download) (version 18.x or later recommended) | ||
- **npm** (comes with Node.js) | ||
|
||
### Installation | ||
|
||
1. **Clone the repository**: | ||
```bash | ||
git clone https://github.com/kata-containers/kata-containers.github.io.git | ||
cd kata-containers.github.io | ||
``` | ||
|
||
2. **Install dependencies**: | ||
Run the following command to install both the project dependencies and development dependencies. | ||
```bash | ||
npm install | ||
``` | ||
|
||
### Development | ||
|
||
3. **Run the development server**: | ||
Start the Next.js development server with hot-reloading enabled. | ||
```bash | ||
node scripts/fetch-ci-nightly-data.js > job_stats.json | ||
npm run dev | ||
``` | ||
|
||
The app will be available at [http://localhost:3000](http://localhost:3000). | ||
|
||
### Production | ||
|
||
4. **Build for production**: | ||
To create an optimized production build, run: | ||
```bash | ||
npm run build | ||
``` | ||
|
||
5. **Start the production server**: | ||
After building, you can start the server: | ||
```bash | ||
npm start | ||
``` | ||
|
||
### Scripts | ||
|
||
- **Fetch CI Nightly Data**: | ||
The `fetch-ci-nightly-data.js` script can be executed manually to pull the latest CI test data from the Kata Containers repository: | ||
```bash | ||
node scripts/fetch-ci-nightly-data.js > job_stats.json | ||
``` |
This file contains 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,50 @@ | ||
import Image from "next/image"; | ||
import { basePath } from "../next.config.js"; | ||
|
||
const icons = [ | ||
"sunny.svg", | ||
"partially-sunny.svg", | ||
"cloudy.svg", | ||
"rainy.svg", | ||
"stormy.svg", | ||
]; | ||
|
||
export const getWeatherIndex = (stat) => { | ||
let fail_rate = 0; | ||
fail_rate = (stat["fails"] + stat["skips"]) / stat["runs"]; | ||
// e.g. failing 3/9 runs is .33, or idx=1 | ||
var idx = Math.floor((fail_rate * 10) / 2); | ||
if (idx == icons.length) { | ||
// edge case: if 100% failures, then we go past the end of icons[] | ||
// back the idx down by 1 | ||
console.assert(fail_rate == 1.0); | ||
idx -= 1; | ||
} | ||
|
||
// This error checks if there are zero runs. | ||
// Currently, will display stormy weather. | ||
if (isNaN(idx)) { | ||
idx = 4; | ||
} | ||
return idx; | ||
}; | ||
|
||
const getWeatherIcon = (stat) => { | ||
const idx = getWeatherIndex(stat); | ||
return icons[idx]; | ||
}; | ||
|
||
export const weatherTemplate = (data) => { | ||
const icon = getWeatherIcon(data); | ||
return ( | ||
<div> | ||
<Image | ||
src={`${basePath}/${icon}`} | ||
alt="weather" | ||
width={32} | ||
height={32} | ||
// priority | ||
/> | ||
</div> | ||
); | ||
}; |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains 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,14 @@ | ||
module.exports = { | ||
reactStrictMode: true, | ||
output: 'export', | ||
basePath: "", | ||
images: { | ||
unoptimized: true, | ||
}, | ||
webpack: (config, { dev }) => { | ||
if (dev) { | ||
config.devtool = false; | ||
} | ||
return config; | ||
}, | ||
} |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains 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,26 @@ | ||
{ | ||
"name": "nextjs-basic", | ||
"private": true, | ||
"scripts": { | ||
"dev": "NODE_ENV=development next dev", | ||
"build": "next build", | ||
"start": "next start", | ||
"lint": "next lint" | ||
}, | ||
"dependencies": { | ||
"next": "^14.2.13", | ||
"primeflex": "^3.3.1", | ||
"primeicons": "^6.0.1", | ||
"primereact": "^10.8.3", | ||
"react": "^18.3.1", | ||
"react-dom": "^18.3.1", | ||
"react-transition-group": "^4.4.5" | ||
}, | ||
"devDependencies": { | ||
"autoprefixer": "^10.4.20", | ||
"eslint": "8.57.0", | ||
"eslint-config-next": "^14.2.13", | ||
"postcss": "^8.4.47", | ||
"tailwindcss": "^3.4.13" | ||
} | ||
} |
This file contains 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,11 @@ | ||
import '../styles/globals.css'; | ||
import 'primereact/resources/themes/saga-blue/theme.css'; | ||
import 'primereact/resources/primereact.min.css'; | ||
import 'primeicons/primeicons.css'; | ||
import 'primeflex/primeflex.css'; | ||
|
||
function MyApp({ Component, pageProps }) { | ||
return <Component {...pageProps} /> | ||
} | ||
|
||
export default MyApp |
This file contains 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,186 @@ | ||
import { useEffect, useState } from "react"; | ||
import { DataTable } from "primereact/datatable"; | ||
import { Column } from "primereact/column"; | ||
import { weatherTemplate, getWeatherIndex } from "../components/weatherTemplate"; | ||
|
||
|
||
export default function Home() { | ||
const [loading, setLoading] = useState(true); | ||
const [jobs, setJobs] = useState([]); | ||
const [rows, setRows] = useState([]); | ||
const [expandedRows, setExpandedRows] = useState([]); | ||
|
||
useEffect(() => { | ||
const fetchData = async () => { | ||
let data = {}; | ||
|
||
if (process.env.NODE_ENV === "development") { | ||
data = (await import("../job_stats.json")).default; | ||
} else { | ||
const response = await fetch( | ||
"https://raw.githubusercontent.com/kata-containers/kata-containers.github.io" + | ||
"/refs/heads/latest-dashboard-data/data/job_stats.json" | ||
); | ||
data = await response.json(); | ||
} | ||
|
||
try { | ||
const jobData = Object.keys(data).map((key) => { | ||
const job = data[key]; | ||
return { name: key, ...job }; | ||
}); | ||
setJobs(jobData); | ||
} catch (error) { | ||
// TODO: Add pop-up/toast message for error | ||
console.error("Error fetching data:", error); | ||
} finally { | ||
setLoading(false); | ||
} | ||
}; | ||
|
||
fetchData(); | ||
}, []); | ||
|
||
useEffect(() => { | ||
setLoading(true); | ||
|
||
// Create rows to set into table. | ||
const rows = jobs.map((job) => ({ | ||
...job, | ||
weather: getWeatherIndex(job), | ||
})); | ||
setRows(rows); | ||
setLoading(false); | ||
}, [jobs]); | ||
|
||
const toggleRow = (rowData) => { | ||
const isRowExpanded = expandedRows.includes(rowData); | ||
|
||
let updatedExpandedRows; | ||
if (isRowExpanded) { | ||
updatedExpandedRows = expandedRows.filter((r) => r !== rowData); | ||
} else { | ||
updatedExpandedRows = [...expandedRows, rowData]; | ||
} | ||
|
||
setExpandedRows(updatedExpandedRows); | ||
}; | ||
|
||
// Template for rendering the Name column as a clickable item | ||
const nameTemplate = (rowData) => { | ||
return ( | ||
<span onClick={() => toggleRow(rowData)} style={{ cursor: "pointer" }}> | ||
{rowData.name} | ||
</span> | ||
); | ||
}; | ||
|
||
const rowExpansionTemplate = (data) => { | ||
const job = jobs.find((job) => job.name === data.name); | ||
|
||
// Prepare run data | ||
const runs = []; | ||
for (let i = 0; i < job.runs; i++) { | ||
runs.push({ | ||
run_num: job.run_nums[i], | ||
result: job.results[i], | ||
url: job.urls[i], | ||
}); | ||
} | ||
|
||
return ( | ||
<div | ||
key={`${job.name}-runs`} | ||
className="p-3 bg-gray-100" | ||
style={{ marginLeft: "4.5rem", marginTop: "-2.0rem" }} | ||
> | ||
<div> | ||
{runs.length > 0 ? ( | ||
runs.map((run) => { | ||
const emoji = | ||
run.result === "Pass" | ||
? "✅" | ||
: run.result === "Fail" | ||
? "❌" | ||
: "⚠️"; | ||
return ( | ||
<span key={`${job.name}-runs-${run.run_num}`}> | ||
<a href={run.url}> | ||
{emoji} {run.run_num} | ||
</a> | ||
| ||
</span> | ||
); | ||
}) | ||
) : ( | ||
<div>No Nightly Runs associated with this job</div> | ||
)} | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
const renderTable = () => ( | ||
<DataTable | ||
value={rows} | ||
expandedRows={expandedRows} | ||
stripedRows | ||
rowExpansionTemplate={rowExpansionTemplate} | ||
onRowToggle={(e) => setExpandedRows(e.data)} | ||
loading={loading} | ||
emptyMessage="No results found." | ||
> | ||
<Column expander style={{ width: "5rem" }} /> | ||
<Column | ||
field="name" | ||
header="Name" | ||
body={nameTemplate} | ||
filter | ||
sortable | ||
maxConstraints={4} | ||
filterHeader="Filter by Name" | ||
filterPlaceholder="Search..." | ||
/> | ||
<Column field="required" header="Required" sortable /> | ||
<Column field="runs" header="Runs" sortable /> | ||
<Column field="fails" header="Fails" sortable /> | ||
<Column field="skips" header="Skips" sortable /> | ||
<Column | ||
field="weather" | ||
header="Weather" | ||
body={weatherTemplate} | ||
sortable | ||
/> | ||
</DataTable> | ||
); | ||
|
||
return ( | ||
<div className="text-center"> | ||
<h1 | ||
className={ | ||
"text-4xl mt-4 mb-0 underline text-inherit hover:text-blue-500" | ||
} | ||
> | ||
<a | ||
href={ | ||
"https://github.com/kata-containers/kata-containers/" + | ||
"actions/workflows/ci-nightly.yaml" | ||
} | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
> | ||
Kata CI Dashboard | ||
</a> | ||
</h1> | ||
|
||
<main | ||
className={ | ||
"m-0 h-full p-4 overflow-x-hidden overflow-y-auto bg-surface-ground font-normal text-text-color antialiased select-text" | ||
} | ||
> | ||
<div>{renderTable()}</div> | ||
<div className="mt-4 text-lg">Total Rows: {rows.length}</div> | ||
</main> | ||
</div> | ||
); | ||
} |
This file contains 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,6 @@ | ||
module.exports = { | ||
plugins: { | ||
tailwindcss: {}, | ||
autoprefixer: {}, | ||
}, | ||
} |
File renamed without changes
File renamed without changes.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
This file contains 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,322 @@ | ||
// | ||
// This script is designed to query the github API for a useful summary | ||
// of recent nightly CI results (though this could be expanded later). | ||
// | ||
// The general flow is as follows: | ||
// - It queries the github API for the workflow runs for the nightly CI (e.g. | ||
// the last 10 nights/runs). | ||
// - For each of those runs, it queries the API for all the jobs data (e.g. | ||
// data on the tdx or snp jobs in each run). | ||
// - It reorganizes and summarizes those results in an array, where each | ||
// entry is information about a job and how it has performed over the last | ||
// few runs (e.g. pass or fail). | ||
// | ||
// Count of API calls: | ||
// 1 for the batch of last 10 nightly run | ||
// 20 for 2 batches of >100 jobs for each of the 10 runs | ||
// 1 for the main branch details (for list of 'Required' jobs) | ||
// 1 for the last 10 closed pull requests | ||
// 20 for all 4 batches of checks (max of a hundred each) for each of the 5 PRs | ||
// X Other? | ||
// TOTAL: 43? | ||
// LIMIT: 60 per hour // curl https://api.github.com/rate_limit | ||
// TODO: Further explore using the GraphQL API, which permits more narrowly targeted queries | ||
|
||
// Github API URL for the kata-container ci-nightly workflow's runs. This | ||
// will only get the most recent 10 runs ('page' is empty, and 'per_page=10'). | ||
const ci_nightly_runs_url = | ||
"https://api.github.com/repos/" + | ||
"kata-containers/kata-containers/actions/workflows/" + | ||
"ci-nightly.yaml/runs?per_page=10"; | ||
// NOTE: For checks run on main after push/merge, do similar call with: payload-after-push.yaml | ||
|
||
// NOTE: pull_requests attribute often empty if commit/branch from a fork: https://github.com/orgs/community/discussions/25220 | ||
// Current approach (there may be better alternatives) is to: | ||
// 1. retrieve the last 10 closed PRs | ||
// 2. fetch the checks for each PR (using the head commit SHA) | ||
const pull_requests_url = | ||
"https://api.github.com/repos/" + | ||
"kata-containers/kata-containers/pulls?state=closed&per_page="; | ||
const pr_checks_url = // for our purposes, 'check' refers to a job in the context of a PR | ||
"https://api.github.com/repos/" + | ||
"kata-containers/kata-containers/commits/"; // will be followed by {commit_sha}/check-runs | ||
// Max of 100 per page, w/ little *over* 300 checks total, so that's 40 calls total for 10 PRs | ||
|
||
// Github API URL for the main branch of the kata-containers repo. | ||
// Used to get the list of required jobs. | ||
const main_branch_url = | ||
"https://api.github.com/repos/" + | ||
"kata-containers/kata-containers/branches/main"; | ||
|
||
// The number of jobs to fetch from the github API on each paged request. | ||
const jobs_per_request = 100; | ||
// The last X closed PRs to retrieve | ||
const pr_count = 5; | ||
// Complete list of jobs (from the CI nightly run) | ||
const job_names = new Set(); | ||
// Count of the number of fetches | ||
let fetch_count = 0; | ||
|
||
// Perform a github API request for workflow runs. | ||
async function fetch_workflow_runs() { | ||
fetch_count++; | ||
return fetch(ci_nightly_runs_url, { | ||
headers: { | ||
Accept: "application/vnd.github+json", | ||
"X-GitHub-Api-Version": "2022-11-28", | ||
}, | ||
}).then(function (response) { | ||
return response.json(); | ||
}); | ||
} | ||
|
||
// Perform a github API request for the last pr_count closed PRs | ||
async function fetch_pull_requests() { | ||
fetch_count++; | ||
const prs_url = `${pull_requests_url}${pr_count}`; | ||
return fetch(prs_url, { | ||
headers: { | ||
Accept: "application/vnd.github+json", | ||
"X-GitHub-Api-Version": "2022-11-28", | ||
}, | ||
}).then(function (response) { | ||
return response.json(); | ||
}); | ||
} | ||
|
||
// Perform a github API request for a list of "Required" jobs | ||
async function fetch_main_branch() { | ||
return fetch(main_branch_url, { | ||
headers: { | ||
Accept: "application/vnd.github+json", | ||
"X-GitHub-Api-Version": "2022-11-28", | ||
}, | ||
}).then(function (response) { | ||
return response.json(); | ||
}); | ||
} | ||
|
||
// Get job data about a workflow run | ||
// Returns a map that has information about a run, e.g. | ||
// ID assigned by github | ||
// run number assigned by github | ||
// 'jobs' array, which has some details about each job from that run | ||
function get_job_data(run) { | ||
// Perform the actual (paged) request | ||
async function fetch_jobs_by_page(which_page) { | ||
fetch_count++; | ||
const jobs_url = | ||
run["jobs_url"] + "?per_page=" + jobs_per_request + "&page=" + which_page; | ||
return fetch(jobs_url, { | ||
headers: { | ||
Accept: "application/vnd.github+json", | ||
"X-GitHub-Api-Version": "2022-11-28", | ||
}, | ||
}).then(function (response) { | ||
return response.json(); | ||
}); | ||
} | ||
|
||
// Fetch the jobs for a run. Extract a few details from the response, | ||
// including the job name and whether it concluded successfully. | ||
function fetch_jobs(p) { | ||
return fetch_jobs_by_page(p).then(function (jobs_request) { | ||
for (const job of jobs_request["jobs"]) { | ||
if (!job_names.has(job["name"])) { | ||
job_names.add(job["name"]) | ||
}; | ||
run_with_job_data["jobs"].push({ | ||
name: job["name"], | ||
run_id: job["run_id"], | ||
html_url: job["html_url"], | ||
conclusion: job["conclusion"], | ||
}); | ||
} | ||
if (p * jobs_per_request >= jobs_request["total_count"]) { | ||
return run_with_job_data; | ||
} | ||
return fetch_jobs(p + 1); | ||
}); | ||
} | ||
|
||
const run_with_job_data = { | ||
id: run["id"], | ||
run_number: run["run_number"], | ||
created_at: run["created_at"], | ||
conclusion: null, | ||
jobs: [], | ||
}; | ||
if (run["status"] == "in_progress") { | ||
return new Promise((resolve) => { | ||
resolve(run_with_job_data); | ||
}); | ||
} | ||
run_with_job_data["conclusion"] = run["conclusion"]; | ||
return fetch_jobs(1); | ||
} | ||
|
||
function get_check_data(pr) { | ||
// Perform a github API request for a list of commits for a PR (takes in the PR's head commit SHA) | ||
async function fetch_checks_by_page(which_page) { | ||
fetch_count++; | ||
const checks_url = | ||
pr_checks_url + prs_with_check_data["commit_sha"] + "/check-runs" + "?per_page=" + jobs_per_request + "&page=" + which_page; | ||
return fetch(checks_url, { | ||
headers: { | ||
Accept: "application/vnd.github+json", | ||
"X-GitHub-Api-Version": "2022-11-28", | ||
}, | ||
}).then(function (response) { | ||
return response.json(); | ||
}); | ||
} | ||
|
||
// Fetch the checks for a PR. | ||
function fetch_checks(p) { | ||
if (p > 60) { | ||
throw new Error(`Attempting to make too many API calls: ${p}`) | ||
} | ||
return fetch_checks_by_page(p) | ||
.then(function (checks_request) { | ||
for (const check of checks_request["check_runs"]) { | ||
// NOTE: For now, excluding checks that are not also run in CI Nightly | ||
if (job_names.has(check["name"])) { | ||
prs_with_check_data["checks"].push({ | ||
name: check["name"], | ||
conclusion: check["conclusion"] | ||
}); | ||
} | ||
} | ||
if (p * jobs_per_request >= checks_request["total_count"]) { | ||
return prs_with_check_data; | ||
} | ||
return fetch_checks(p + 1); | ||
}) | ||
.catch(function (error) { | ||
console.error("Error fetching checks:", error); | ||
throw error; | ||
}); | ||
} | ||
|
||
// Extract list of objects with PR commit SHAs, PR URLs, and PR number (i.e. id) | ||
const prs_with_check_data = { | ||
html_url: pr["html_url"], // link to PR page | ||
number: pr["number"], // PR number (used as PR id); displayed on dashboard | ||
commit_sha: pr["head"]["sha"], // For getting checks run on PR branch | ||
// NOTE: using for now b/c we'll be linking to the PR page, where these checks are listed... | ||
checks: [], // will be populated later with fetch_checks | ||
}; | ||
|
||
return fetch_checks(1); | ||
} | ||
|
||
// Extract list of required jobs (i.e. main branch details: protection: required_status_checks: contexts) | ||
function get_required_jobs(main_branch) { | ||
const required_jobs = main_branch["protection"]["required_status_checks"]["contexts"]; | ||
return required_jobs; | ||
} | ||
|
||
// Calculate and return job stats across all runs | ||
function compute_job_stats(runs_with_job_data, prs_with_check_data, required_jobs) { | ||
const job_stats = {}; | ||
for (const run of runs_with_job_data) { | ||
for (const job of run["jobs"]) { | ||
if (!(job["name"] in job_stats)) { | ||
job_stats[job["name"]] = { | ||
runs: 0, // e.g. 10, if it ran 10 times | ||
fails: 0, // e.g. 3, if it failed 3 out of 10 times | ||
skips: 0, // e.g. 7, if it got skipped the other 7 times | ||
urls: [], // ordered list of URLs associated w/ each run | ||
results: [], // an array of strings, e.g. 'Pass', 'Fail', ... | ||
run_nums: [], // ordered list of github-assigned run numbers | ||
|
||
pr_runs: 0, | ||
pr_fails: 0, | ||
pr_skips: 0, | ||
pr_urls: [], // list of PR URLs that this job is associated with | ||
pr_results: [], // list of job statuses for the PRs in which the job was run | ||
pr_nums: [], // list of PR numbers that this job is associated with | ||
}; | ||
} | ||
const job_stat = job_stats[job["name"]]; | ||
job_stat["runs"] += 1; | ||
job_stat["run_nums"].push(run["run_number"]); | ||
job_stat["urls"].push(job["html_url"]); | ||
if (job["conclusion"] != "success") { | ||
if (job["conclusion"] == "skipped") { | ||
job_stat["skips"] += 1; | ||
job_stat["results"].push("Skip"); | ||
} else { | ||
// failed or cancelled | ||
job_stat["fails"] += 1; | ||
job_stat["results"].push("Fail"); | ||
} | ||
} else { | ||
job_stat["results"].push("Pass"); | ||
} | ||
job_stat["required"] = required_jobs.includes(job["name"]); | ||
} | ||
} | ||
for (const pr of prs_with_check_data) { | ||
for (const check of pr["checks"]) { | ||
if ((check["name"] in job_stats)) { | ||
const job_stat = job_stats[check["name"]]; | ||
job_stat["pr_runs"] += 1; | ||
job_stat["pr_urls"].push(pr["html_url"]) | ||
job_stat["pr_nums"].push(pr["number"]) | ||
if (check["conclusion"] != "success") { | ||
if (check["conclusion"] == "skipped") { | ||
job_stat["pr_skips"] += 1; | ||
job_stat["pr_results"].push("Skip"); | ||
} else { | ||
// failed or cancelled | ||
job_stat["pr_fails"] += 1; | ||
job_stat["pr_results"].push("Fail"); | ||
} | ||
} else { | ||
job_stat["pr_results"].push("Pass"); | ||
} | ||
} | ||
} | ||
} | ||
return job_stats; | ||
} | ||
|
||
async function main() { | ||
// Fetch recent workflow runs via the github API | ||
const workflow_runs = await fetch_workflow_runs(); | ||
|
||
// Fetch required jobs from main branch | ||
const main_branch = await fetch_main_branch(); | ||
const required_jobs = get_required_jobs(main_branch); | ||
|
||
// Fetch job data for each of the runs. | ||
// Store all of this in an array of maps, runs_with_job_data. | ||
const promises_buf = []; | ||
for (const run of workflow_runs["workflow_runs"]) { | ||
promises_buf.push(get_job_data(run)); | ||
} | ||
runs_with_job_data = await Promise.all(promises_buf); | ||
|
||
// Fetch recent pull requests via the github API | ||
const pull_requests = await fetch_pull_requests(); | ||
|
||
// Fetch last pr_count closed PRs | ||
// Store all of this in an array of maps, prs_with_check_data. | ||
const promises_buffer = []; | ||
for (const pr of pull_requests) { | ||
promises_buffer.push(get_check_data(pr)); | ||
} | ||
prs_with_check_data = await Promise.all(promises_buffer); | ||
|
||
// Transform the raw details of each run and its jobs' results into a | ||
// an array of just the jobs and their overall results (e.g. pass or fail, | ||
// and the URLs associated with them). | ||
const job_stats = compute_job_stats(runs_with_job_data, prs_with_check_data, required_jobs); | ||
|
||
// Write the job_stats to console as a JSON object | ||
console.log(JSON.stringify(job_stats)); | ||
} | ||
|
||
|
||
main(); |
This file contains 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,3 @@ | ||
@tailwind base; | ||
@tailwind components; | ||
@tailwind utilities; |
This file contains 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,15 @@ | ||
/** @type {import('tailwindcss').Config} */ | ||
module.exports = { | ||
content: [ | ||
"./app/**/*.{js,ts,jsx,tsx,mdx}", | ||
"./pages/**/*.{js,ts,jsx,tsx,mdx}", | ||
"./components/**/*.{js,ts,jsx,tsx,mdx}", | ||
|
||
// Or if using `src` directory: | ||
"./src/**/*.{js,ts,jsx,tsx,mdx}", | ||
], | ||
theme: { | ||
extend: {}, | ||
}, | ||
plugins: [], | ||
} |