blob: ed34e729116913c756daf1184c3c8f02f95a7604 [file]
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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: Update build status workflow
on:
schedule:
- cron: "*/15 * * * *"
jobs:
update:
name: Update build status
runs-on: ubuntu-latest
permissions:
actions: read
checks: write
steps:
- name: "Update build status"
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const SEARCH_WINDOW_DAYS = 7;
const BATCH_SIZE = 10;
function getSearchSince() {
const windowStart = new Date(Date.now() - SEARCH_WINDOW_DAYS * 24 * 60 * 60 * 1000);
return windowStart.toISOString().replace(/\.\d{3}Z$/, 'Z');
}
async function searchActivePullRequests() {
const since = getSearchSince();
const query =
`repo:${context.repo.owner}/${context.repo.repo} is:pr is:open updated:>=${since}`;
const items = [];
let page = 1;
console.log(`Searching active PRs with query: ${query}`);
while (true) {
const response = await github.request('GET /search/issues', {
q: query,
page,
per_page: 100,
sort: 'updated',
order: 'desc'
});
const pageItems = response.data.items || [];
items.push(...pageItems);
console.log(`Fetched ${pageItems.length} PRs from search page ${page}`);
if (pageItems.length < 100) {
break;
}
page += 1;
}
return items;
}
function shouldSkipPatch(checkRun, workflowRun) {
const sameStatus = checkRun.status === workflowRun.status;
const sameConclusion =
workflowRun.status !== 'completed' || checkRun.conclusion === workflowRun.conclusion;
const sameDetailsUrl = checkRun.details_url === workflowRun.details_url;
return sameStatus && sameConclusion && sameDetailsUrl;
}
async function processPullRequest(searchItem) {
const prNumber = searchItem.number;
try {
const pr = (
await github.request('GET /repos/{owner}/{repo}/pulls/{pull_number}', {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
})
).data;
console.log(`PR #${pr.number} SHA: ${pr.head.sha}`);
console.log(` Mergeable status: ${pr.mergeable_state}`);
const checkRuns = await github.request(
'GET /repos/{owner}/{repo}/commits/{ref}/check-runs',
{
owner: context.repo.owner,
repo: context.repo.repo,
ref: pr.head.sha
}
);
const buildCheckRun = checkRuns.data.check_runs.find(
(checkRun) =>
checkRun.name === 'Build' && checkRun.conclusion !== 'action_required'
);
if (!buildCheckRun) {
console.log(` Skip PR #${pr.number}: no eligible Build check run`);
return;
}
if (!buildCheckRun.output || !buildCheckRun.output.text) {
console.log(` Skip PR #${pr.number}: Build check run ${buildCheckRun.id} has no run metadata`);
return;
}
let workflowRunParams;
try {
workflowRunParams = JSON.parse(buildCheckRun.output.text);
} catch (error) {
console.error(` Skip PR #${pr.number}: invalid JSON in check run ${buildCheckRun.id}`, error);
return;
}
let workflowRun;
try {
workflowRun = (
await github.request(
'GET /repos/{owner}/{repo}/actions/runs/{run_id}',
workflowRunParams
)
).data;
} catch (error) {
console.error(` Skip PR #${pr.number}: workflow run lookup failed`, error);
return;
}
if (shouldSkipPatch(buildCheckRun, workflowRun)) {
console.log(
` Skip PR #${pr.number}: Build check run ${buildCheckRun.id} already matches ${workflowRun.status}/${workflowRun.conclusion}`
);
return;
}
const patchParams = {
owner: context.repo.owner,
repo: context.repo.repo,
check_run_id: buildCheckRun.id,
output: buildCheckRun.output,
status: workflowRun.status,
details_url: workflowRun.details_url
};
if (workflowRun.status === 'completed') {
patchParams.conclusion = workflowRun.conclusion;
console.log(
` Patch PR #${pr.number} check run ${buildCheckRun.id}: ${buildCheckRun.status}/${buildCheckRun.conclusion} -> ${workflowRun.status}/${workflowRun.conclusion}`
);
} else {
console.log(
` Patch PR #${pr.number} check run ${buildCheckRun.id}: ${buildCheckRun.status} -> ${workflowRun.status}`
);
}
await github.request(
'PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}',
patchParams
);
} catch (error) {
console.error(`Failed to sync PR #${prNumber}`, error);
}
}
async function processInBatches(items) {
for (let index = 0; index < items.length; index += BATCH_SIZE) {
const batch = items.slice(index, index + BATCH_SIZE);
console.log(
`Processing batch ${Math.floor(index / BATCH_SIZE) + 1} with ${batch.length} PRs`
);
await Promise.all(batch.map((item) => processPullRequest(item)));
}
}
try {
const activePullRequests = await searchActivePullRequests();
console.log(`Found ${activePullRequests.length} active PRs to evaluate`);
if (activePullRequests.length === 0) {
console.log('No active PRs matched the search window');
return;
}
await processInBatches(activePullRequests);
} catch (error) {
console.error('Update build status workflow failed', error);
throw error;
}