├── 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 | 
18 |
19 | Screenshot:
20 | 
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 |
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 |
--------------------------------------------------------------------------------