1
+ # Copyright 2025 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
+ name : Cloud Build Failure Reporter
16
+
17
+ on :
18
+ workflow_call :
19
+ inputs :
20
+ trigger_names :
21
+ required : true
22
+ type : string
23
+ workflow_dispatch :
24
+ inputs :
25
+ trigger_names :
26
+ description : ' Cloud Build trigger names separated by comma.'
27
+ required : true
28
+ default : ' '
29
+
30
+ jobs :
31
+ report :
32
+
33
+ permissions :
34
+ issues : ' write'
35
+ checks : ' read'
36
+
37
+ runs-on : ' ubuntu-latest'
38
+
39
+ steps :
40
+ - uses : ' actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' # v7
41
+ with :
42
+ script : |-
43
+ // parse test names
44
+ const testNameSubstring = '${{ inputs.trigger_names }}';
45
+ const testNameFound = new Map(); //keeps track of whether each test is found
46
+ testNameSubstring.split(',').forEach(testName => {
47
+ testNameFound.set(testName, false);
48
+ });
49
+
50
+ // label for all issues opened by reporter
51
+ const periodicLabel = 'periodic-failure';
52
+
53
+ // check if any reporter opened any issues previously
54
+ const prevIssues = await github.paginate(github.rest.issues.listForRepo, {
55
+ ...context.repo,
56
+ state: 'open',
57
+ creator: 'github-actions[bot]',
58
+ labels: [periodicLabel]
59
+ });
60
+
61
+ // createOrCommentIssue creates a new issue or comments on an existing issue.
62
+ const createOrCommentIssue = async function (title, txt) {
63
+ if (prevIssues.length < 1) {
64
+ console.log('no previous issues found, creating one');
65
+ await github.rest.issues.create({
66
+ ...context.repo,
67
+ title: title,
68
+ body: txt,
69
+ labels: [periodicLabel]
70
+ });
71
+ return;
72
+ }
73
+ // only comment on issue related to the current test
74
+ for (const prevIssue of prevIssues) {
75
+ if (prevIssue.title.includes(title)){
76
+ console.log(
77
+ `found previous issue ${prevIssue.html_url}, adding comment`
78
+ );
79
+
80
+ await github.rest.issues.createComment({
81
+ ...context.repo,
82
+ issue_number: prevIssue.number,
83
+ body: txt
84
+ });
85
+ return;
86
+ }
87
+ }
88
+ };
89
+
90
+ // updateIssues comments on any existing issues. No-op if no issue exists.
91
+ const updateIssues = async function (checkName, txt) {
92
+ if (prevIssues.length < 1) {
93
+ console.log('no previous issues found.');
94
+ return;
95
+ }
96
+ // only comment on issue related to the current test
97
+ for (const prevIssue of prevIssues) {
98
+ if (prevIssue.title.includes(checkName)){
99
+ console.log(`found previous issue ${prevIssue.html_url}, adding comment`);
100
+ await github.rest.issues.createComment({
101
+ ...context.repo,
102
+ issue_number: prevIssue.number,
103
+ body: txt
104
+ });
105
+ }
106
+ }
107
+ };
108
+
109
+ // Find status of check runs.
110
+ // We will find check runs for each commit and then filter for the periodic.
111
+ // Checks API only allows for ref and if we use main there could be edge cases where
112
+ // the check run happened on a SHA that is different from head.
113
+ const commits = await github.paginate(github.rest.repos.listCommits, {
114
+ ...context.repo
115
+ });
116
+
117
+ const relevantChecks = new Map();
118
+ for (const commit of commits) {
119
+ console.log(
120
+ `checking runs at ${commit.html_url}: ${commit.commit.message}`
121
+ );
122
+ const checks = await github.rest.checks.listForRef({
123
+ ...context.repo,
124
+ ref: commit.sha
125
+ });
126
+
127
+ // Iterate through each check and find matching names
128
+ for (const check of checks.data.check_runs) {
129
+ console.log(`Handling test name ${check.name}`);
130
+ for (const testName of testNameFound.keys()) {
131
+ if (testNameFound.get(testName) === true){
132
+ //skip if a check is already found for this name
133
+ continue;
134
+ }
135
+ if (check.name.includes(testName)) {
136
+ relevantChecks.set(check, commit);
137
+ testNameFound.set(testName, true);
138
+ }
139
+ }
140
+ }
141
+ // Break out of the loop early if all tests are found
142
+ const allTestsFound = Array.from(testNameFound.values()).every(value => value === true);
143
+ if (allTestsFound){
144
+ break;
145
+ }
146
+ }
147
+
148
+ // Handle each relevant check
149
+ relevantChecks.forEach((commit, check) => {
150
+ if (
151
+ check.status === 'completed' &&
152
+ check.conclusion === 'success'
153
+ ) {
154
+ updateIssues(
155
+ check.name,
156
+ `[Tests are passing](${check.html_url}) for commit [${commit.sha}](${commit.html_url}).`
157
+ );
158
+ } else if (check.status === 'in_progress') {
159
+ console.log(
160
+ `Check is pending ${check.html_url} for ${commit.html_url}. Retry again later.`
161
+ );
162
+ } else {
163
+ createOrCommentIssue(
164
+ `Cloud Build Failure Reporter: ${check.name} failed`,
165
+ `Cloud Build Failure Reporter found test failure for [**${check.name}** ](${check.html_url}) at [${commit.sha}](${commit.html_url}). Please fix the error and then close the issue after the **${check.name}** test passes.`
166
+ );
167
+ }
168
+ });
169
+
170
+ // no periodic checks found across all commits, report it
171
+ const noTestFound = Array.from(testNameFound.values()).every(value => value === false);
172
+ if (noTestFound){
173
+ createOrCommentIssue(
174
+ 'Missing periodic tests: ${{ inputs.trigger_names }}',
175
+ `No periodic test is found for triggers: ${{ inputs.trigger_names }}. Last checked from ${
176
+ commits[0].html_url
177
+ } to ${commits[commits.length - 1].html_url}.`
178
+ );
179
+ }
0 commit comments