Keyman Build Summary #153086
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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]); | |