Skip to content

Commit dfe71a9

Browse files
authored
Tool that calculate workflow/job failure rate and collect failure logs (#4743)
1 parent b010838 commit dfe71a9

File tree

4 files changed

+584
-0
lines changed

4 files changed

+584
-0
lines changed

ci/workflow_summary/README.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# `workflow_information.py` Script
2+
3+
## Prerequisites
4+
- [Python](https://www.python.org/) and required packages.
5+
```
6+
pip install requests argparse
7+
```
8+
9+
## Usage
10+
- Collect last `90` days' `Postsubmit` `ci_workflow.yml` workflow runs:
11+
```
12+
python workflow_information.py --token ${your_github_toke} --branch master --event push --d 90
13+
```
14+
15+
- Collect last `30` days' `Presubmit` `ci_workflow.yml` workflow runs:
16+
```
17+
python workflow_information.py --token ${your_github_toke} --event pull_request --d 30
18+
```
19+
20+
- Please refer to `Inputs` section for more use cases, and `Outputs` section for the workflow summary report format.
21+
22+
## Inputs
23+
- `-o, --repo_owner`: **[Required]** GitHub repo owner, default value is `firebase`.
24+
25+
- `-n, --repo_name`: **[Required]** GitHub repo name, default value is `firebase-android-sdk`.
26+
27+
- `-t, --token`: **[Required]** GitHub access token. See [Creating a personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token).
28+
29+
- `-w, --workflow_name`: **[Required]** Workflow filename, default value is `ci_tests.yml`.
30+
31+
- `-d, --days`: Filter workflows that running in past -d days, default value is `90`. See [retention period for GitHub Actions artifacts and logs](https://docs.github.com/en/organizations/managing-organization-settings/configuring-the-retention-period-for-github-actions-artifacts-and-logs-in-your-organization).
32+
33+
- `-b, --branch`: Filter branch name that workflows run against.
34+
35+
- `-a, --actor`: Filter the actor who triggers the workflow runs.
36+
37+
- `-e, --event`: Filter workflows trigger event, could be one of the following values `['push', 'pull_request', 'issue']`.
38+
39+
- `-j, --jobs`: Filter workflows jobs, default is `all` (including rerun jobs), could be one of the following values `['latest', 'all']`.
40+
41+
- `-f, --folder`: Workflow and job information will be store here, default value is the current datatime.
42+
43+
44+
## Outputs
45+
46+
- `workflow_summary_report.txt`: a general report contains workflow pass/failure count, running time, etc.
47+
48+
```
49+
2023-03-03 01:37:07.114500
50+
Namespace(actor=None, branch=None, days=30, event='pull_request', folder='presubmit_30', jobs='all', repo_name='firebase-android-sdk', repo_owner='firebase', token=${your_github_token}, workflow_name='ci_tests.yml')
51+
52+
Workflow 'ci_tests.yml' Report:
53+
Workflow Failure Rate:64.77%
54+
Workflow Total Count: 193 (success: 68, failure: 125)
55+
56+
Workflow Runtime Report:
57+
161 workflow runs finished without rerun, the average running time: 0:27:24.745342
58+
Including:
59+
56 passed workflow runs, with average running time: 0:17:29.214286
60+
105 failed workflow runs, with average running time: 0:32:42.361905
61+
62+
32 runs finished with rerun, the average running time: 1 day, 3:57:53.937500
63+
The running time for each workflow reruns are:
64+
['1 day, 2:24:32', '3:35:54', '3:19:14', '4 days, 6:10:50', '15:33:39', '1:57:21', '1:13:12', '1:55:18', '12 days, 21:51:29', '0:48:48', '0:45:28', '1:40:21', '2 days, 1:46:35', '19:47:16', '0:45:49', '2:22:36', '0:25:22', '0:55:30', '1:40:32', '1:10:05', '20:08:38', '0:31:03', '5 days, 9:19:25', '5:10:44', '1:20:57', '0:28:47', '1:52:44', '20:19:17', '0:35:15', '21:31:07', '3 days, 1:06:44', '3 days, 2:18:14']
65+
66+
Job Failure Report:
67+
Unit Tests (:firebase-storage):
68+
Failure Rate:54.61%
69+
Total Count: 152 (success: 69, failure: 83)
70+
Unit Tests (:firebase-messaging):
71+
Failure Rate:35.37%
72+
Total Count: 147 (success: 95, failure: 52)
73+
```
74+
75+
76+
- Intermediate file `workflow_summary.json`: contains all the workflow runs and job information attached to each workflow.
77+
78+
```
79+
{
80+
'workflow_name':'ci_tests.yml',
81+
'total_count':81,
82+
'success_count':32,
83+
'failure_count':49,
84+
'created':'>2022-11-30T23:15:04Z',
85+
'workflow_runs':[
86+
{
87+
'workflow_id':4296343867,
88+
'conclusion':'failure',
89+
'head_branch':'master',
90+
'actor':'vkryachko',
91+
'created_at':'2023-02-28T18:47:40Z',
92+
'updated_at':'2023-02-28T19:20:16Z',
93+
'run_started_at':'2023-02-28T18:47:40Z',
94+
'run_attempt':1,
95+
'html_url':'https://github.com/firebase/firebase-android-sdk/actions/runs/4296343867',
96+
'jobs_url':'https://api.github.com/repos/firebase/firebase-android-sdk/actions/runs/4296343867/jobs',
97+
'jobs':{
98+
'total_count':95,
99+
'success_count':92,
100+
'failure_count':3,
101+
'job_runs':[
102+
{
103+
'job_id':11664775180,
104+
'job_name':'Determine changed modules',
105+
'conclusion':'success',
106+
'created_at':'2023-02-28T18:47:42Z',
107+
'started_at':'2023-02-28T18:47:50Z',
108+
'completed_at':'2023-02-28T18:50:11Z',
109+
'run_attempt': 1,
110+
'html_url':'https://github.com/firebase/firebase-android-sdk/actions/runs/4296343867/jobs/7487936863',
111+
}
112+
]
113+
}
114+
}
115+
]
116+
}
117+
```
118+
119+
- Intermediate file `job_summary.json`: contains all the job runs organized by job name.
120+
```
121+
{
122+
'Unit Test Results':{ # job name
123+
'total_count':17,
124+
'success_count':7,
125+
'failure_count':10,
126+
'failure_jobs':[ # data structure is the same as same as workflow_summary['workflow_runs']['job_runs']
127+
{
128+
'job_id':11372664143,
129+
'job_name':'Unit Test Results',
130+
'conclusion':'failure',
131+
'created_at':'2023-02-15T22:02:06Z',
132+
'started_at':'2023-02-15T22:02:06Z',
133+
'completed_at':'2023-02-15T22:02:06Z',
134+
'run_attempt': 1,
135+
'html_url':'https://github.com/firebase/firebase-android-sdk/runs/11372664143',
136+
}
137+
]
138+
}
139+
}
140+
```
141+
142+
143+
# `collect_ci_test_logs.py` Script
144+
145+
## Usage
146+
- Collect `ci_test.yml` job failure logs from `workflow_information.py` script's intermediate file:
147+
```
148+
python collect_ci_test_logs.py --token ${github_toke} --folder ${folder}
149+
```
150+
151+
## Inputs
152+
153+
- `-t, --token`: **[Required]** GitHub access token. See [Creating a personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token).
154+
155+
- `-f, --folder`: **[Required]** Folder that store intermediate files generated by `workflow_information.py`. `ci_workflow.yml` job failure logs will also be stored here.
156+
157+
## Outputs
158+
159+
- `${job name}.log`: contains job failure rate, list all failed job links and failure logs.
160+
```
161+
Unit Tests (:firebase-storage):
162+
Failure rate:40.00%
163+
Total count: 20 (success: 12, failure: 8)
164+
Failed jobs:
165+
166+
https://github.com/firebase/firebase-android-sdk/actions/runs/4296343867/jobs/7487989874
167+
firebase-storage:testDebugUnitTest
168+
Task :firebase-storage:testDebugUnitTest
169+
2023-02-28T18:54:38.1333725Z
170+
2023-02-28T18:54:38.1334278Z com.google.firebase.storage.DownloadTest > streamDownloadWithResumeAndCancel FAILED
171+
2023-02-28T18:54:38.1334918Z org.junit.ComparisonFailure at DownloadTest.java:190
172+
2023-02-28T18:57:20.3329130Z
173+
2023-02-28T18:57:20.3330165Z 112 tests completed, 1 failed
174+
2023-02-28T18:57:20.5329189Z
175+
2023-02-28T18:57:20.5330505Z > Task :firebase-storage:testDebugUnitTest FAILED
176+
```
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2023 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the 'License');
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an 'AS IS' BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
'''A utility collecting ci_test.yml workflow failure logs.
17+
18+
Usage:
19+
20+
python collect_ci_test_logs.py --token ${github_toke} --folder ${folder}
21+
22+
'''
23+
24+
import github
25+
import argparse
26+
import json
27+
import re
28+
import logging
29+
import os
30+
31+
32+
REPO_OWNER = 'firebase'
33+
REPO_NAME = 'firebase-android-sdk'
34+
EXCLUDE_JOB_LIST = ['Determine changed modules','Unit Tests (matrix)','Publish Tests Results','Unit Test Results','Instrumentation Tests','Unit Tests']
35+
36+
def main():
37+
logging.getLogger().setLevel(logging.INFO)
38+
39+
args = parse_cmdline_args()
40+
gh = github.GitHub(REPO_OWNER, REPO_NAME)
41+
42+
token = args.token
43+
44+
file_folder = args.folder
45+
if not os.path.exists(file_folder):
46+
logging.error(f'{file_folder} doesn\'t exist')
47+
exit(1)
48+
49+
job_summary = json.load(open(os.path.join(file_folder, 'job_summary.json')))
50+
51+
for job_name in job_summary:
52+
if job_name in EXCLUDE_JOB_LIST:
53+
continue
54+
55+
job = job_summary[job_name]
56+
57+
if job['failure_rate'] > 0:
58+
failure_rate = job['failure_rate']
59+
total_count = job['total_count']
60+
success_count = job['success_count']
61+
failure_count = job['failure_count']
62+
63+
log_file_path = os.path.join(file_folder, f'{job_name}.log')
64+
file_log = open(log_file_path, 'w')
65+
file_log.write(f'\n{job_name}:\nFailure rate:{failure_rate:.2%} \nTotal count: {total_count} (success: {success_count}, failure: {failure_count})\nFailed jobs:')
66+
logging.info(f'\n\n{job_name}:\nFailure rate:{failure_rate:.2%} \nTotal count: {total_count} (success: {success_count}, failure: {failure_count})\nFailed jobs:')
67+
68+
for failure_job in job['failure_jobs']:
69+
file_log.write('\n\n'+failure_job['html_url'])
70+
logging.info(failure_job['html_url'])
71+
job_id = failure_job['job_id']
72+
logs = gh.job_logs(token, job_id)
73+
if logs:
74+
# using regex to extract failure information
75+
failed_tasks = re.findall(r"Execution failed for task ':(.*?)'.", logs)
76+
for failed_task in failed_tasks:
77+
file_log.write('\n'+failed_task)
78+
pattern = fr'Task :{failed_task}(.*?)Task :{failed_task} FAILED'
79+
failed_tests = re.search(pattern, logs, re.MULTILINE | re.DOTALL)
80+
if failed_tests:
81+
file_log.write('\n'+failed_tests.group())
82+
83+
file_log.close()
84+
85+
logging.info(f'\n\nFinsihed collecting failure logs, log files locates under path: {file_folder}')
86+
87+
88+
def parse_cmdline_args():
89+
parser = argparse.ArgumentParser(description='Collect certain Github workflow information and calculate failure rate.')
90+
parser.add_argument('-t', '--token', required=True, help='GitHub access token')
91+
parser.add_argument('-f', '--folder', required=True, help='Folder generated by workflow_information.py. Test logs also locate here.')
92+
args = parser.parse_args()
93+
return args
94+
95+
if __name__ == '__main__':
96+
main()

ci/workflow_summary/github.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Copyright 2023 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""A utility for GitHub REST API."""
16+
17+
import requests
18+
import logging
19+
20+
RETRIES = 3
21+
BACKOFF = 5
22+
RETRY_STATUS = (403, 500, 502, 504)
23+
TIMEOUT = 5
24+
TIMEOUT_LONG = 20
25+
26+
class GitHub:
27+
28+
def __init__(self, owner, repo):
29+
self.github_api_url = f'https://api.github.com/repos/{owner}/{repo}'
30+
31+
def list_workflows(self, token, workflow_id, params):
32+
"""https://docs.github.com/en/rest/actions/workflow-runs#list-workflow-runs-for-a-workflow"""
33+
url = f'{self.github_api_url}/actions/workflows/{workflow_id}/runs'
34+
headers = {'Accept': 'application/vnd.github+json', 'Authorization': f'token {token}'}
35+
with requests.get(url, headers=headers, params=params,
36+
stream=True, timeout=TIMEOUT_LONG) as response:
37+
logging.info('list_workflows: %s, params: %s, response: %s', url, params, response)
38+
return response.json()
39+
40+
def list_jobs(self, token, run_id, params):
41+
"""https://docs.github.com/en/rest/actions/workflow-jobs#list-jobs-for-a-workflow-run"""
42+
url = f'{self.github_api_url}/actions/runs/{run_id}/jobs'
43+
headers = {'Accept': 'application/vnd.github+json', 'Authorization': f'token {token}'}
44+
with requests.get(url, headers=headers, params=params,
45+
stream=True, timeout=TIMEOUT_LONG) as response:
46+
logging.info('list_jobs: %s, params: %s, response: %s', url, params, response)
47+
return response.json()
48+
49+
def job_logs(self, token, job_id):
50+
"""https://docs.github.com/rest/reference/actions#download-job-logs-for-a-workflow-run"""
51+
url = f'{self.github_api_url}/actions/jobs/{job_id}/logs'
52+
headers = {'Accept': 'application/vnd.github+json', 'Authorization': f'token {token}'}
53+
with requests.get(url, headers=headers, allow_redirects=False,
54+
stream=True, timeout=TIMEOUT_LONG) as response:
55+
logging.info('job_logs: %s response: %s', url, response)
56+
if response.status_code == 302:
57+
with requests.get(response.headers['Location'], headers=headers, allow_redirects=False,
58+
stream=True, timeout=TIMEOUT_LONG) as get_log_response:
59+
return get_log_response.content.decode('utf-8')
60+
else:
61+
logging.info('no log avaliable')
62+
return ''

0 commit comments

Comments
 (0)