Skip to content

Keyman Build Summary #153086

Keyman Build Summary

Keyman Build Summary #153086

# GENERATED FILE - DO NOT EDIT!
#
# Keyman is copyright (C) SIL Global. MIT License.
#
# Do not modify the script in .github/workflows/pr-build-status.yml directly;
# instead work on the sources in resources/build/pr-build-status and use the
# build.sh script to rebuild the .github/workflows/pr-build-status.yml file from
# them.
#
# build.sh will append the relevant portions of
# resources/build/pr-build-status/pr-build-status.mjs to the content in
# resources/build/pr-build-status/pr-build-status.prefix.yml to form the .github
# workflow file.
#
name: Keyman Build Summary
on:
status:
push:
branches-ignore:
- master
- beta
- stable-*
workflow_dispatch:
inputs:
commit:
description: 'Commit sha'
required: true
type: string
jobs:
run_pr_build_status:
name: Summarize build status checks
runs-on: ubuntu-latest
steps:
- name: Check PR build status
id: run_pr_build_status_script
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
// This code is copied out of resources/build/pr-build-status/pr-build-status.mjs
// where it is tested. It is copied inline here in order to avoid requiring the
// repository to be checked out, which dramatically reduces the run time of the
// check.
//
// Note: we don't currently look at check runs, only statuses
//
// Verify the following statuses:
// 'user_testing'
// 'API Verification' (github-actions[bot])
//
// At least 1 of the following statuses must be found:
// 'Test*' (keyman-server), e.g. 'Test Build (Keyman)'
// 'Ubuntu Packaging' (github-actions[bot])
//
// Ignore the following statuses:
// check/web/file-size
//
function addLog(message) {
console.log(message);
return message + String.fromCharCode(10);
}
function addStatus(o, type, context, state) {
if(!o[context]) {
o[context] = {type, state};
return addLog(`Found ${context}: ${state}`);
} else {
return addLog(`Skipping repeated ${context}: ${state}`);
}
}
function reduceStatuses(statuses) {
let summary = '';
const filtered_statuses = statuses.reduce((o, status) => {
if(status.creator?.login == 'keyman-server' && status.context.startsWith('Test')) {
summary += addStatus(o, 'build', status.context, status.state);
} else if(status.creator?.login == 'keymanapp-test-bot[bot]' && status.context == 'user_testing') {
summary += addStatus(o, 'user-test', status.context, status.state);
} else if(status.context == 'API Verification') {
summary += addStatus(o, 'check', status.context, status.state);
} else if(status.context == 'Ubuntu Packaging') {
summary += addStatus(o, 'build', status.context, status.state);
} else if(status.context == 'npm pack/publish') {
summary += addStatus(o, 'build', status.context, status.state);
} else if(status.context == 'Keyman Core - ARM64 test') {
summary += addStatus(o, 'build', status.context, status.state);
} else if(status.context == 'check/web/file-size') {
// Ignore check/web/file-size -- we won't block automerge for this at this point
summary += addLog(`Skipping ${status.context}`);
} else {
// We fail with an 'unknown status' response if we get a new status check
// so we can be sure we are not skipping known status checks
summary += addLog(`Found unknown ${status.context}: ${status.state}`);
o[status.context] = {type: 'unknown', state: status.state};
}
return o;
}, {});
return { filtered_statuses, summary };
}
//
// Given the collection of status checks we care about, return
// an aggregate status -- error, failure, pending, or success,
// and a summary description
//
function calculateFinalStatus(filtered_statuses, summary) {
const counts = {};
let hasBuilds = false;
for(const context of Object.keys(filtered_statuses)) {
const { state, type } = filtered_statuses[context];
if(type == 'unknown') {
// We special-case for unknown status checks, and never permit them
// Note: function reduceStatuses() is where new status checks can be added
console.error(`An unknown context "${context}" was found, cannot calculate build status.`);
return [
'error', `An unknown context "${context}" was found, cannot calculate build status.`, summary
];
}
if(type == 'build') {
hasBuilds = true;
}
counts[state] = counts[state] ? counts[state] + 1 : 1;
}
// If we do not have any statuses yet, we wait
if(Object.keys(filtered_statuses).length == 0 || !hasBuilds) {
console.log(`Builds not yet triggered`);
return ['pending', 'Builds have not yet been triggered ⌛', summary];
}
const state =
counts.error ? 'error' :
counts.failure ? 'failure' :
counts.pending ? 'pending' :
'success';
let description = '';
function appendDescription(count, state) {
if(!count) return;
if(description != '') description += '; ';
description += `${count} check${count == 1 ? '' : 's'} ${state}`;
}
appendDescription(counts.error, 'in an error state ❌');
appendDescription(counts.failure, 'failed ❌');
appendDescription(counts.pending, 'pending ⌛');
appendDescription(counts.success, 'completed successfully ✅');
console.log(`Finished filtering and reviewing status checks: ${state}: ${description}`);
return [ state, description, summary ];
}
async function getCommitStatuses(github, owner, repo, sha) {
console.log(`Getting commit statuses for ${sha}`);
const statuses = await github.paginate('GET /repos/{owner}/{repo}/commits/{sha}/statuses', {
owner,
repo,
sha,
headers: {
'X-GitHub-Api-Version': '2022-11-28'
}
});
return statuses;
}
async function getCommitCheckRuns(github, owner, repo, sha) {
console.log(`Getting commit check runs for ${sha}`);
const statuses = await github.paginate('GET /repos/{owner}/{repo}/commits/{sha}/check-runs', {
owner,
repo,
sha,
headers: {
'X-GitHub-Api-Version': '2022-11-28'
}
});
return statuses;
}
function calculateCheckResult(statuses) {
if(!Array.isArray(statuses)) {
return ['error', 'Failed to retrieve status checks from GitHub ❌', 'statuses is not an array'];
}
const { filtered_statuses, summary } = reduceStatuses(statuses);
return calculateFinalStatus(filtered_statuses, summary);
}
async function test(github, owner, repo, sha) {
// Get statuses from sha
const statuses = await getCommitStatuses(github, owner, repo, sha);
return calculateCheckResult(statuses);
}
async function createCheck(github, owner, repo, sha) {
const check = await github.rest.checks.create({
owner,
repo,
head_sha: sha,
name: 'Build Outcome',
status: 'in_progress',
});
return check.data.id;
}
async function updateCheck(github, owner, repo, checkRunId, status, description, summary) {
const checkStatus = status == 'pending' ? 'in_progress' : 'completed';
const conclusion = checkStatus == 'in_progress' ? undefined : (status == 'success' ? 'success' : 'failure');
await github.rest.checks.update({
owner,
repo,
check_run_id: checkRunId,
status: checkStatus,
conclusion,
output: {
title: description,
summary
}
});
}
const { owner, repo } = context.repo;
const sha =
context.payload?.check_suite?.sha || /* check run completed */
context.payload?.inputs?.commit || /* manual run */
context.payload?.after || /* push */
context.payload?.commit?.sha || /* status */
context.sha; /* probably 'master'! */
const checkRunId = await createCheck(github, owner, repo, sha);
const res = await test(github, owner, repo, sha);
await updateCheck(github, owner, repo, checkRunId, res[0], res[1], res[2]);