├── src ├── react-app-env.d.ts ├── setupTests.ts ├── reportWebVitals.ts ├── index.tsx └── pages │ └── WorkflowStats.tsx ├── .yarnrc.yml ├── public ├── robots.txt ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── manifest.json └── index.html ├── assets ├── first-screenshot.png ├── second-screenshot.png └── github-actions-stats-demo.gif ├── package.json ├── index.ts ├── README.md ├── .gitignore └── tsconfig.json /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-2.4.1.cjs 2 | nodeLinker: "node-modules" 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amir-bio/github-actions-stats/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amir-bio/github-actions-stats/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amir-bio/github-actions-stats/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /assets/first-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amir-bio/github-actions-stats/HEAD/assets/first-screenshot.png -------------------------------------------------------------------------------- /assets/second-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amir-bio/github-actions-stats/HEAD/assets/second-screenshot.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amir-bio/github-actions-stats/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amir-bio/github-actions-stats/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amir-bio/github-actions-stats/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /assets/github-actions-stats-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amir-bio/github-actions-stats/HEAD/assets/github-actions-stats-demo.gif -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Github Actions Stat Visualiser", 3 | "short_name": "Github Actions Stat Visualiser", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-actions-stats", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:amirha97/github-actions-stats.git", 6 | "author": "Amirhossein Andohkosh <4315615+amirha97@users.noreply.github.com>", 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "execute": "ts-node index.ts", 10 | "start": "react-scripts start", 11 | "build": "react-scripts build", 12 | "test": "react-scripts test", 13 | "eject": "react-scripts eject" 14 | }, 15 | "eslintConfig": { 16 | "extends": [ 17 | "react-app" 18 | ] 19 | }, 20 | "browserslist": { 21 | "production": [ 22 | ">0.2%", 23 | "not dead", 24 | "not op_mini all" 25 | ], 26 | "development": [ 27 | "last 1 chrome version", 28 | "last 1 firefox version", 29 | "last 1 safari version" 30 | ] 31 | }, 32 | "dependencies": { 33 | "@chakra-ui/react": "^1.4.0", 34 | "@emotion/react": "^11", 35 | "@emotion/styled": "^11", 36 | "@octokit/rest": "^18.3.5", 37 | "focus-visible": "^5.2.0", 38 | "framer-motion": "^4", 39 | "node": "^15.10.0", 40 | "react": "^17.0.1", 41 | "react-dom": "^17.0.1", 42 | "react-query": "^3.13.0", 43 | "react-scripts": "^4.0.3", 44 | "typescript": "^4.2.3", 45 | "victory": "^35.4.12", 46 | "web-vitals": "^1.1.1" 47 | }, 48 | "devDependencies": { 49 | "@types/node": "^14.14.35", 50 | "@types/react": "^17.0.3", 51 | "@types/react-dom": "^17.0.2", 52 | "ts-node": "^9.1.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | const { Octokit } = require("@octokit/rest"); 2 | const octokit = new Octokit({ 3 | auth: process.env.GITHUB_TOKEN 4 | }); 5 | 6 | 7 | // octokit.actions.listRepoWorkflows({ 8 | // owner: process.env.OWNER, 9 | // repo: process.env.REPO, 10 | // }).then(({data}) => console.log(data)) 11 | 12 | const main = async () => { 13 | const { data: specificWorkflowRuns } = await octokit.actions.listWorkflowRuns({ 14 | owner: process.env.OWNER, 15 | repo: process.env.REPO, 16 | workflow_id: process.env.WORKFLOW_NAME, 17 | per_page: 100, 18 | }) 19 | 20 | console.log(specificWorkflowRuns) 21 | console.log("\n\n\n---------\n\n\n") 22 | const stats = { 23 | totalRuns: specificWorkflowRuns.total_count, 24 | conclusion: { 25 | success: 0, 26 | failure: 0, 27 | cancelled: 0, 28 | startup_failure: 0 29 | }, 30 | // list of duration of runs in seconds for each conclusion 31 | durations: { 32 | success: [] as number[], 33 | failure: [] as number[], 34 | cancelled: [] as number[], 35 | startup_failure: [] as number[], 36 | } 37 | } 38 | // only count completed runs 39 | for (const run of specificWorkflowRuns.workflow_runs) { 40 | if (run.status != "completed") continue 41 | stats.conclusion[run.conclusion] += 1 42 | 43 | const durationMs = Date.parse(run.updated_at)- Date.parse(run.created_at) 44 | stats.durations[run.conclusion].push(durationMs/1000) 45 | } 46 | 47 | console.log(stats) 48 | 49 | } 50 | 51 | main() 52 | 53 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Github Actions Stat Visualiser 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Github Actions Stat Visualiser 2 | 3 | Statistics for your Github Actions to help analyse and optimise your workflows. 4 | 5 | This project provides a CLI that can be used to analyse your github actions over time. 6 | The resulting analysis can be used to monitor the duration of workflows over time and bea valuable tool for measuring 7 | effect of changes to the pipeline, thereby helping to find which optimisations are optimal for your actions. 8 | 9 | ## 🌟 Features 10 | - React App to visualise and graph stats 11 | - List all workflows under a repo - clicking on them takes one to the analysis page of that workflow 12 | - List all runs for a workflow (up to last 100) 13 | - Histogram of the duration of successful runs along with basic stats about that workflows runs 14 | - Provide stats for different workflows conclusion, e.g. success, failure, cancelled, etc 15 | 16 | Gif of current state of the app: 17 | ![Latest GIF of the App](./assets/github-actions-stats-demo.gif) 18 | 19 | Screenshot: 20 | ![Screenshot of the App](./assets/second-screenshot.png) 21 | 22 | ## Development 23 | 24 | Create a .env file at the root with `REACT_APP_GITHUB_TOKEN` env var. Its value should be a 25 | personal token you create via github (it must have the necessary permission - list of these coming) . 26 | 27 | [Soon this app will become a github app and will automatically authenticate with github] 28 | 29 | ## How it works? 30 | 31 | Long term: cache all fetched data and only retrieve again if not already cached. 32 | 33 | ## 🌟 Upcoming Features 34 | - Use Formik for form logic 35 | - Hydrate form input from localstorage 36 | - More detailed explanation of workflow conclusion e.g. failed after how long? histograms for each different conclusion 37 | - Provide basic stats for the given runs for a workflow, e.g. min, max, mean, 80th, 90th, 95th, 99th percentile 38 | - List all runs for a workflow with a variable time based start window 39 | - Number of workflows run per day (with possibility of filtering by a given workflow) 40 | - Filtering based on the branch that workflows ran on? 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Yarn 2 (not using zero-install) 107 | .yarn/* 108 | !.yarn/patches 109 | !.yarn/releases 110 | !.yarn/plugins 111 | !.yarn/sdks 112 | !.yarn/versions 113 | .pnp.* 114 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { 4 | Box, Button, 5 | Center, 6 | ChakraProvider, 7 | Flex, 8 | Grid, 9 | GridItem, 10 | HStack, Input, 11 | Spinner, 12 | Tab, 13 | TabList, 14 | TabPanel, 15 | TabPanels, 16 | Tabs, 17 | } from "@chakra-ui/react" 18 | import { extendTheme } from "@chakra-ui/react" 19 | import { WorkflowStats } from './pages/WorkflowStats' 20 | import { Octokit } from "@octokit/rest"; 21 | import { useToast } from "@chakra-ui/react" 22 | import "focus-visible/dist/focus-visible" 23 | 24 | const colors = { 25 | brand: { 26 | activeBlue: "#0566d6", 27 | 900: "#1a365d", 28 | 800: "#153e75", 29 | 700: "#2a69ac", 30 | }, 31 | } 32 | 33 | const theme = extendTheme({colors}) 34 | 35 | const tabStyle = { 36 | borderRadius: "5px", 37 | justifyContent: "left", 38 | } 39 | 40 | const selectedTabStyle = { 41 | color: "white", 42 | bg: "brand.activeBlue", 43 | borderRadius: "5px", 44 | justifyContent: "left", 45 | } 46 | 47 | const octokit = new Octokit({ 48 | auth: process.env.REACT_APP_GITHUB_TOKEN 49 | }); 50 | 51 | 52 | const App = () => { 53 | const [owner, setOwner] = useState("") 54 | const [repo, setRepo] = useState("") 55 | const [workflowsList, setWorkflowsList] = useState([]) 56 | const [loading, setLoading] = useState(false) 57 | const toast = useToast() 58 | 59 | // TODO: type properly 60 | const handleSubmit = async (event: any) => { 61 | event.preventDefault() 62 | try { 63 | setLoading(true) 64 | const {data} = await octokit.actions.listRepoWorkflows({ 65 | owner: event.target.owner.value, 66 | repo: event.target.repo.value, 67 | }) 68 | setOwner(event.target.owner.value) 69 | setRepo(event.target.repo.value) 70 | setWorkflowsList(data.workflows) 71 | } catch (e: any) { 72 | console.error("error while getting list of repo workflows from github", e) 73 | toast({ 74 | title: "Retrieval of workflows list failed.", 75 | description: e.toString(), 76 | status: "error", 77 | duration: 9000, 78 | isClosable: true, 79 | }) 80 | } finally { 81 | setLoading(false) 82 | } 83 | } 84 | 85 | return ( 86 | <> 87 | 94 | 95 | 96 |
97 | 98 | 99 | 100 | 101 | 102 | 103 |
104 |
105 | 106 | {loading && 107 |
108 | 116 |
} 117 | 118 | {!loading && !!workflowsList && 119 | 120 | ( 122 | 127 | 128 | 129 | 130 | {workflowsList.map(workflow => ( 131 | 136 | {workflow.name} 137 | 138 | ))} 139 | 140 | 141 | 142 | 143 | 144 | {workflowsList.map(workflow => ( 145 | 146 | 147 | 148 | ))} 149 | 150 | 151 | 152 | 153 | 154 | )} 155 |
156 | 157 | ) 158 | } 159 | 160 | 161 | ReactDOM.render( 162 | 163 | 164 | 165 | 166 | , 167 | document.getElementById('root') 168 | ); 169 | 170 | // If you want to start measuring performance in your app, pass a function 171 | // to log results (for example: reportWebVitals(console.log)) 172 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 173 | // reportWebVitals(); 174 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "noImplicitAny": false 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | // old configs - kept as reference for other options 28 | // "compilerOptions": { 29 | // /* Visit https://aka.ms/tsconfig.json to read more about this file */ 30 | // 31 | // /* Basic Options */ 32 | // // "incremental": true, /* Enable incremental compilation */ 33 | // "target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 34 | // "module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 35 | // // "lib": [], /* Specify library files to be included in the compilation. */ 36 | // // "allowJs": true, /* Allow javascript files to be compiled. */ 37 | // // "checkJs": true, /* Report errors in .js files. */ 38 | // // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 39 | // // "declaration": true, /* Generates corresponding '.d.ts' file. */ 40 | // // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 41 | // // "sourceMap": true, /* Generates corresponding '.map' file. */ 42 | // // "outFile": "./", /* Concatenate and emit output to single file. */ 43 | // // "outDir": "./", /* Redirect output structure to the directory. */ 44 | // // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 45 | // // "composite": true, /* Enable project compilation */ 46 | // // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 47 | // // "removeComments": true, /* Do not emit comments to output. */ 48 | // // "noEmit": true, /* Do not emit outputs. */ 49 | // // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 50 | // // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 51 | // // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 52 | // 53 | // /* Strict Type-Checking Options */ 54 | // "strict": true, /* Enable all strict type-checking options. */ 55 | // "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 56 | // // "strictNullChecks": true, /* Enable strict null checks. */ 57 | // // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 58 | // // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 59 | // // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 60 | // // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 61 | // // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 62 | // 63 | // /* Additional Checks */ 64 | // // "noUnusedLocals": true, /* Report errors on unused locals. */ 65 | // // "noUnusedParameters": true, /* Report errors on unused parameters. */ 66 | // // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 67 | // // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 68 | // // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 69 | // // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 70 | // 71 | // /* Module Resolution Options */ 72 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 73 | // // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 74 | // // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 75 | // // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 76 | // // "typeRoots": [], /* List of folders to include type definitions from. */ 77 | // // "types": [], /* Type declaration files to be included in compilation. */ 78 | // // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 79 | // "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 80 | // // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 81 | // // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 82 | // 83 | // /* Source Map Options */ 84 | // // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 85 | // // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 86 | // // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 87 | // // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 88 | // 89 | // /* Experimental Options */ 90 | // // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 91 | // // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 92 | // 93 | // /* Advanced Options */ 94 | // "skipLibCheck": true, /* Skip type checking of declaration files. */ 95 | // "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 96 | // } 97 | } 98 | -------------------------------------------------------------------------------- /src/pages/WorkflowStats.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Box, Center, Flex, HStack, Spinner, Tag } from "@chakra-ui/react" 3 | import { Octokit } from "@octokit/rest"; 4 | import { 5 | VictoryAxis, 6 | VictoryChart, 7 | VictoryHistogram, 8 | VictoryLabel, 9 | VictoryTooltip, 10 | VictoryVoronoiContainer 11 | } from 'victory'; 12 | 13 | const octokit = new Octokit({ 14 | auth: process.env.REACT_APP_GITHUB_TOKEN 15 | }); 16 | 17 | type Props = { 18 | owner: string 19 | repo: string 20 | workflowId: number 21 | } 22 | 23 | export const WorkflowStats = ({owner, repo, workflowId}: Props) => { 24 | const [workflowRunsStats, setWorkflowRunsStats] = useState({}) 25 | const [loading, setLoading] = useState(true) 26 | 27 | useEffect(() => { 28 | // TODO: setup proper pagination (potentially with a request limit of 10/20 ?) 29 | setWorkflowRunsStats({}) 30 | setLoading(true) 31 | octokit.actions.listWorkflowRuns({ 32 | owner: owner, 33 | repo: repo, 34 | workflow_id: workflowId, 35 | per_page: 100, 36 | }).then(({data: specificWorkflowRuns}) => { 37 | 38 | const stats = { 39 | totalRuns: specificWorkflowRuns.total_count, 40 | conclusion: { 41 | success: 0, 42 | failure: 0, 43 | cancelled: 0, 44 | startup_failure: 0 45 | }, 46 | // list of duration of runs in seconds for each conclusion 47 | durations: { 48 | success: [] as number[], 49 | failure: [] as number[], 50 | cancelled: [] as number[], 51 | startup_failure: [] as number[], 52 | }, 53 | earliestRun: new Date(8640000000000000).getTime(), 54 | latestRun: new Date(-8640000000000000).getTime() 55 | } 56 | // only count completed runs 57 | for (const run of specificWorkflowRuns.workflow_runs) { 58 | if (!run.conclusion || run.status !== "completed") continue 59 | stats.conclusion[run.conclusion] += 1 60 | 61 | const createdAtTime = Date.parse(run.created_at) 62 | const updatedAtTime = Date.parse(run.updated_at) 63 | const durationMs = updatedAtTime - createdAtTime 64 | stats.durations[run.conclusion].push(durationMs / 1000) 65 | 66 | stats.earliestRun = Math.min(stats.earliestRun, createdAtTime) 67 | stats.latestRun = Math.max(stats.latestRun, createdAtTime) 68 | } 69 | 70 | console.log("stats", stats) 71 | setLoading(false) 72 | setWorkflowRunsStats(stats) 73 | } 74 | ).catch(e => { 75 | setLoading(false) 76 | console.error("error while getting runs in a workflow from github", e) 77 | }) 78 | }, [owner, repo, workflowId]) 79 | 80 | return ( 81 | <> 82 | {loading && 83 | ( 84 |
85 | 93 |
94 | ) 95 | } 96 | 97 | { 98 | !loading && !!workflowRunsStats?.durations && ( 99 | 106 | <> 107 | 108 | Total Runs: {workflowRunsStats.totalRuns} 109 | 110 | 111 | 112 | {workflowRunsStats.conclusion.success} Successes 113 | 114 | 115 | {workflowRunsStats.conclusion.failure} Failures 116 | 117 | 118 | {workflowRunsStats.conclusion.cancelled} Cancelled 119 | 120 | 121 | {workflowRunsStats.conclusion.startup_failure} Startup Failures 122 | 123 | 124 | 125 | 126 | 127 | 128 |
129 | Latest Run: {new Date(workflowRunsStats.latestRun).toLocaleDateString()}
130 | Earliest Run: {new Date(workflowRunsStats.earliestRun).toLocaleDateString()}
131 | 132 | 133 | 134 | 135 | `${datum.y} (${(datum.x.toFixed(1))} minutes)`} 142 | labelComponent={} 144 | />} 145 | 146 | > 147 | 148 | 154 | 155 | 156 | 157 | 158 | ({ 166 | x: successDuration / 60 167 | })) 168 | 169 | } 170 | /> 171 | 172 | 173 |
174 | ) 175 | } 176 | 177 | ) 178 | } 179 | --------------------------------------------------------------------------------