Skip to content

Commit c2ddca8

Browse files
committed
feat: add scripts for circleci analysis
0 parents  commit c2ddca8

5 files changed

+413
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.json
2+
*.txt
3+
*.csv

deno.lock

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

get-circleci-insights.ts

+321
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
import { transformToCSV } from "./transform-to-csv.ts"
2+
3+
const TOKEN = Deno.env.get("CIRCLECI_TOKEN")!
4+
5+
const ORGS = Deno.env.get("GITHUB_ORG")!.split(",")
6+
7+
const DEBUG = Deno.env.get("DEBUG") === "true"
8+
9+
async function getJSON<T>(url: string, params: URLSearchParams = new URLSearchParams()): Promise<T> {
10+
if (DEBUG) {
11+
console.log(`GET ${url}?${params}`)
12+
}
13+
const res = await fetch(`${url}?${params}`, {
14+
headers: {
15+
"Circle-Token": TOKEN
16+
}
17+
})
18+
const json = await res.json()
19+
if (DEBUG) {
20+
console.log(JSON.stringify(json, null, 2))
21+
}
22+
return json
23+
}
24+
25+
async function getPagedJSON<T>(url: string, params: URLSearchParams = new URLSearchParams()): Promise<T[]> {
26+
const res: T[] = []
27+
let page: Paged<T> | undefined
28+
do {
29+
if (page?.next_page_token) {
30+
params.set("page-token", page?.next_page_token)
31+
}
32+
page = await getJSON<Paged<T>>(url, params)
33+
res.push(...page.items)
34+
} while (page?.next_page_token)
35+
return res
36+
}
37+
38+
39+
40+
type TrendsOrMetrics = {
41+
total_credits_used: number
42+
total_duration_secs: number
43+
throughput: number
44+
total_runs: number
45+
success_rate: number
46+
}
47+
48+
type Project = {
49+
project_name: string
50+
}
51+
52+
type TrendsAndMetrics = {
53+
trends: TrendsOrMetrics
54+
metrics: TrendsOrMetrics
55+
}
56+
57+
type OrgSummaryData = {
58+
org_data: TrendsAndMetrics
59+
all_projects: string[]
60+
org_project_data: (Project & TrendsAndMetrics)[]
61+
}
62+
63+
enum ReportingWindow {
64+
LAST_90_DAYS = "last-90-days",
65+
LAST_60_DAYS = "last-60-days",
66+
LAST_30_DAYS = "last-30-days",
67+
LAST_7_DAYS = "last-7-days",
68+
LAST_24_HOURS = "last-24-hours"
69+
}
70+
71+
// https://circleci.com/docs/api/v2/index.html#operation/getOrgSummaryData
72+
function getOrgSummaryData(org: string, reportingWindow: ReportingWindow = ReportingWindow.LAST_90_DAYS): Promise<OrgSummaryData> {
73+
return getJSON(`https://circleci.com/api/v2/insights/gh/${org}/summary`, new URLSearchParams({"reporting-window": reportingWindow}))
74+
}
75+
76+
type Workflow = {
77+
workflow_name: string
78+
}
79+
80+
type Branch = {
81+
branch: string
82+
}
83+
84+
type ProjectWorkflowsPageData = {
85+
project_workflow_branch_data: (Workflow & Branch & TrendsAndMetrics)[]
86+
all_workflows: string[]
87+
org_id: string
88+
all_branches: string[]
89+
project_workflow_data: (Workflow & TrendsAndMetrics)[]
90+
project_id: string
91+
project_data: TrendsAndMetrics
92+
}
93+
94+
// https://circleci.com/docs/api/v2/index.html#operation/getProjectWorkflowsPageData
95+
function getProjectWorkflowsPageData(org: string, repo: string, reportingWindow: ReportingWindow = ReportingWindow.LAST_90_DAYS): Promise<ProjectWorkflowsPageData> {
96+
return getJSON(`https://circleci.com/api/v2/insights/pages/gh/${org}/${repo}/summary`, new URLSearchParams({"reporting-window": reportingWindow}))
97+
}
98+
99+
type Paged<T> = {
100+
items: T[]
101+
next_page_token?: string
102+
}
103+
104+
type Metrics = {
105+
name: string
106+
metrics: {
107+
total_runs: number
108+
successful_runs: number
109+
mttr: number
110+
total_credits_used: number
111+
failed_runs: number
112+
median_credits_used: number
113+
success_rate: number
114+
duration_metrics: {
115+
min: number
116+
mean: number
117+
median: number
118+
p95: number
119+
max: number
120+
standard_deviation: number
121+
total_duration: number
122+
}
123+
total_recoveries: number
124+
throughput: number
125+
}
126+
window_start: string
127+
window_end: string
128+
}
129+
130+
type ProjectWorkflowMetrics = {
131+
project_id: string
132+
} & Metrics
133+
134+
// https://circleci.com/docs/api/v2/index.html#operation/getProjectWorkflowMetrics
135+
function getProjectWorkflowMetrics(org: string, repo: string, allBranches = false, reportingWindow: ReportingWindow = ReportingWindow.LAST_90_DAYS): Promise<ProjectWorkflowMetrics[]> {
136+
return getPagedJSON(`https://circleci.com/api/v2/insights/gh/${org}/${repo}/workflows`, new URLSearchParams({"reporting-window": reportingWindow, "all-branches": allBranches.toString()}))
137+
}
138+
139+
type ProjectWorkflowRuns = {
140+
id: string
141+
duration: number
142+
status: string
143+
created_at: string
144+
stopped_at: string
145+
credits_used: number
146+
branch: string
147+
is_approval: boolean
148+
}
149+
150+
// https://circleci.com/docs/api/v2/index.html#operation/getProjectWorkflowRuns
151+
async function getProjectWorkflowRuns(org: string, repo: string, workflow: string, allBranches = false, reportingWindow: ReportingWindow = ReportingWindow.LAST_90_DAYS): Promise<ProjectWorkflowRuns[]> {
152+
const [startDate, endDate] = ((reportingWindow: ReportingWindow) => {
153+
const now = Date.now()
154+
switch (reportingWindow) {
155+
case ReportingWindow.LAST_90_DAYS:
156+
return [new Date(now - 90 * 24 * 60 * 60 * 1000).toISOString(), new Date(now).toISOString()]
157+
case ReportingWindow.LAST_60_DAYS:
158+
return [new Date(now - 60 * 24 * 60 * 60 * 1000).toISOString(), new Date(now).toISOString()]
159+
case ReportingWindow.LAST_30_DAYS:
160+
return [new Date(now - 30 * 24 * 60 * 60 * 1000).toISOString(), new Date(now).toISOString()]
161+
case ReportingWindow.LAST_7_DAYS:
162+
return [new Date(now - 7 * 24 * 60 * 60 * 1000).toISOString(), new Date(now).toISOString()]
163+
case ReportingWindow.LAST_24_HOURS:
164+
return [new Date(now - 24 * 60 * 60 * 1000).toISOString(), new Date(now).toISOString()]
165+
}
166+
})(reportingWindow)
167+
const json = await getJSON<Paged<ProjectWorkflowRuns>>(`https://circleci.com/api/v2/insights/gh/${org}/${repo}/workflows/${workflow}`, new URLSearchParams({"all-branches": `${allBranches}`, "start-date": startDate, "end-date": endDate}))
168+
return json.items
169+
}
170+
171+
type ProjectWorkflowJobMetrics = Metrics
172+
173+
// https://circleci.com/docs/api/v2/index.html#operation/getProjectWorkflowJobMetrics
174+
function getProjectWorkflowJobMetrics(org: string, repo: string, workflow: string, allBranches = false, reportingWindow: ReportingWindow = ReportingWindow.LAST_90_DAYS): Promise<ProjectWorkflowJobMetrics[]> {
175+
return getPagedJSON(`https://circleci.com/api/v2/insights/gh/${org}/${repo}/workflows/${workflow}/jobs`, new URLSearchParams({"reporting-window": reportingWindow, "all-branches": allBranches.toString()}))
176+
}
177+
178+
type WorkflowJob = {
179+
dependencies: string[]
180+
job_number: number
181+
id: string
182+
started_at: string
183+
name: string
184+
project_slug: string
185+
status: string
186+
type: string
187+
stopped_at: string
188+
}
189+
190+
// https://circleci.com/docs/api/v2/index.html#operation/listWorkflowJobs
191+
function listWorkflowJobs(id: string): Promise<WorkflowJob[]> {
192+
return getPagedJSON(`https://circleci.com/api/v2/workflow/${id}/job`)
193+
}
194+
195+
type JobDetails = {
196+
created_at: string
197+
duration: number
198+
executor: {
199+
resource_class: string
200+
type: string
201+
}
202+
messages: string[]
203+
queued_at: string
204+
started_at: string
205+
parallel_runs: {
206+
index: number
207+
status: string
208+
}[]
209+
contexts: string[]
210+
latest_workflow: {
211+
id: string
212+
name: string
213+
}
214+
name: string
215+
number: number
216+
organization: {
217+
name: string
218+
}
219+
parallelism: number
220+
pipeline: {
221+
id: string
222+
}
223+
project: {
224+
external_url: string
225+
id: string
226+
name: string
227+
slug: string
228+
}
229+
web_url: string
230+
}
231+
232+
// https://circleci.com/docs/api/v2/index.html#operation/getJobDetails
233+
function getJobDetails(org: string, repo: string, number: string): Promise<JobDetails> {
234+
return getJSON(`https://circleci.com/api/v2/project/gh/${org}/${repo}/job/${number}`)
235+
}
236+
237+
type ProjectBySlug = {
238+
slug: string
239+
name: string
240+
id: string
241+
organization_name: string
242+
organization_slug: string
243+
organization_id: string
244+
vcs_info: {
245+
vcs_url: string
246+
provider: string
247+
default_branch: string
248+
}
249+
}
250+
251+
// https://circleci.com/docs/api/v2/index.html#operation/getProjectBySlug
252+
function getProjectBySlug(org: string, repo: string): Promise<ProjectBySlug> {
253+
return getJSON(`https://circleci.com/api/v2/project/gh/${org}/${repo}`)
254+
}
255+
256+
async function getCircleCIInsights() {
257+
const worfklowData = []
258+
const jobData = []
259+
for (const org of ORGS) {
260+
console.log(`# ${org}`)
261+
const orgSummaryData = await getOrgSummaryData(org, ReportingWindow.LAST_90_DAYS)
262+
const projects = orgSummaryData['all_projects']
263+
for (const project of projects) {
264+
console.log(`## ${project}`)
265+
const projectBySlug = await getProjectBySlug(org, project)
266+
for (const reportingWindow of [ReportingWindow.LAST_30_DAYS, ReportingWindow.LAST_60_DAYS, ReportingWindow.LAST_90_DAYS]) {
267+
console.log(`### ${reportingWindow}`)
268+
for (const allBranches of [true, false]) {
269+
console.log(`#### ${allBranches ? "All Branches" : "Default Branch"}`)
270+
const projectWorkflowMetrics = await getProjectWorkflowMetrics(org, project, allBranches, reportingWindow)
271+
for (const {name: workflow, metrics} of projectWorkflowMetrics) {
272+
console.log(`##### ${workflow}`)
273+
// Workflow data
274+
if (metrics !== undefined) {
275+
worfklowData.push({
276+
org,
277+
project,
278+
workflow,
279+
metrics,
280+
resourceUsage: `https://bff.circleci.com/private/insights/resource-usage/${projectBySlug.organization_id}/${projectBySlug.id}/workflows/${workflow}/jobs/?allBranches=${allBranches}&reporting-window=${reportingWindow}`,
281+
allBranches,
282+
reportingWindow
283+
})
284+
}
285+
// Job data
286+
const projectWorkflowRuns = await getProjectWorkflowRuns(org, project, workflow, allBranches, reportingWindow)
287+
const projectWorkflowJobMetrics = await getProjectWorkflowJobMetrics(org, project, workflow, allBranches, reportingWindow)
288+
const workflowJobs: WorkflowJob[] = []
289+
const run = projectWorkflowRuns.find(run => run.status === 'success')
290+
if (run != undefined) {
291+
workflowJobs.push(...(await listWorkflowJobs(run.id)))
292+
}
293+
for (const {name: job, metrics} of projectWorkflowJobMetrics) {
294+
console.log(`###### ${job}`)
295+
const jobDetails: JobDetails[] = await Promise.all(workflowJobs.filter(workflowJob => workflowJob.name === job).map(workflowJob => workflowJob.job_number).map(jobNumber => getJobDetails(org, project, `${jobNumber}`)))
296+
const executor = jobDetails.pop()?.executor
297+
jobData.push({
298+
org,
299+
project,
300+
workflow,
301+
job,
302+
executor,
303+
metrics,
304+
allBranches,
305+
reportingWindow
306+
})
307+
}
308+
}
309+
}
310+
}
311+
}
312+
}
313+
console.log(`# Workflow Data`)
314+
console.log(transformToCSV(worfklowData))
315+
console.log(`# Job Data`)
316+
console.log(transformToCSV(jobData))
317+
}
318+
319+
if (import.meta.main) {
320+
getCircleCIInsights()
321+
}

0 commit comments

Comments
 (0)