cloud_build_failure_reporter.yml•7.91 kB
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Cloud Build Failure Reporter
on:
workflow_call:
inputs:
trigger_names:
required: true
type: string
workflow_dispatch:
inputs:
trigger_names:
description: 'Cloud Build trigger names separated by comma.'
required: true
default: ''
jobs:
report:
permissions:
issues: 'write'
checks: 'read'
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd' # v8
with:
script: |-
// parse test names
const testNameSubstring = '${{ inputs.trigger_names }}';
const testNameFound = new Map(); //keeps track of whether each test is found
testNameSubstring.split(',').forEach(testName => {
testNameFound.set(testName, false);
});
// label for all issues opened by reporter
const periodicLabel = 'periodic-failure';
// check if any reporter opened any issues previously
const prevIssues = await github.paginate(github.rest.issues.listForRepo, {
...context.repo,
state: 'open',
creator: 'github-actions[bot]',
labels: [periodicLabel]
});
// createOrCommentIssue creates a new issue or comments on an existing issue.
const createOrCommentIssue = async function (title, txt) {
if (prevIssues.length < 1) {
console.log('no previous issues found, creating one');
await github.rest.issues.create({
...context.repo,
title: title,
body: txt,
labels: [periodicLabel]
});
return;
}
// only comment on issue related to the current test
for (const prevIssue of prevIssues) {
if (prevIssue.title.includes(title)){
console.log(
`found previous issue ${prevIssue.html_url}, adding comment`
);
await github.rest.issues.createComment({
...context.repo,
issue_number: prevIssue.number,
body: txt
});
return;
}
}
};
// updateIssues comments on any existing issues. No-op if no issue exists.
const updateIssues = async function (checkName, txt) {
if (prevIssues.length < 1) {
console.log('no previous issues found.');
return;
}
// only comment on issue related to the current test
for (const prevIssue of prevIssues) {
if (prevIssue.title.includes(checkName)){
console.log(`found previous issue ${prevIssue.html_url}, adding comment`);
await github.rest.issues.createComment({
...context.repo,
issue_number: prevIssue.number,
body: txt
});
}
}
};
// Find status of check runs.
// We will find check runs for each commit and then filter for the periodic.
// Checks API only allows for ref and if we use main there could be edge cases where
// the check run happened on a SHA that is different from head.
const commits = await github.paginate(github.rest.repos.listCommits, {
...context.repo
});
const relevantChecks = new Map();
for (const commit of commits) {
console.log(
`checking runs at ${commit.html_url}: ${commit.commit.message}`
);
const checks = await github.rest.checks.listForRef({
...context.repo,
ref: commit.sha
});
// Iterate through each check and find matching names
for (const check of checks.data.check_runs) {
console.log(`Handling test name ${check.name}`);
for (const testName of testNameFound.keys()) {
if (testNameFound.get(testName) === true){
//skip if a check is already found for this name
continue;
}
if (check.name.includes(testName)) {
relevantChecks.set(check, commit);
testNameFound.set(testName, true);
}
}
}
// Break out of the loop early if all tests are found
const allTestsFound = Array.from(testNameFound.values()).every(value => value === true);
if (allTestsFound){
break;
}
}
// Handle each relevant check
relevantChecks.forEach((commit, check) => {
if (
check.status === 'completed' &&
check.conclusion === 'success'
) {
updateIssues(
check.name,
`[Tests are passing](${check.html_url}) for commit [${commit.sha}](${commit.html_url}).`
);
} else if (check.status === 'in_progress') {
console.log(
`Check is pending ${check.html_url} for ${commit.html_url}. Retry again later.`
);
} else {
createOrCommentIssue(
`Cloud Build Failure Reporter: ${check.name} failed`,
`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.`
);
}
});
// no periodic checks found across all commits, report it
const noTestFound = Array.from(testNameFound.values()).every(value => value === false);
if (noTestFound){
createOrCommentIssue(
'Missing periodic tests: ${{ inputs.trigger_names }}',
`No periodic test is found for triggers: ${{ inputs.trigger_names }}. Last checked from ${
commits[0].html_url
} to ${commits[commits.length - 1].html_url}.`
);
}