Skip to content

refactor: Centralize float normalization #435

refactor: Centralize float normalization

refactor: Centralize float normalization #435

Workflow file for this run

# Licensed to the Apache Software Foundation (ASF) under one

Check warning on line 1 in .github/workflows/fork_ci_status.yml

View workflow run for this annotation

GitHub Actions / Fork CI status

Workflow execution policy warning (evaluate mode)

On December 2, 2026, GitHub will restrict `pull_request_target` on public repositories by default. To continue allowing the event trigger, configure an Actions policy. Learn more: https://gh.io/securely-using-pull_request_target#default-policy-for-pull_request_target
# 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.
# # Fork CI status
#
# A pull request from a fork to `main` runs `rust.yml` on the fork. This
# workflow mirrors that run as one `Rust (fork)` check on the pull request, so
# reviewers see the result without opening the fork. It never checks out or
# runs code from the pull request: on `pull_request_target` it reads the fork's
# public run list and creates the check, and on a schedule it updates the
# active or missing runs of recently updated pull requests plus a rotating
# slice of all open pull requests. The check is informational. The merge
# queue runs the full workflow on the merged result and stays the gate.
#
# See docs/source/contributor-guide/index.md, "Pull Requests from Forks".
name: Fork CI status
on:
pull_request_target:
types: [opened, reopened, synchronize]
schedule:
- cron: "*/15 * * * *"
workflow_dispatch:
inputs:
hours:
description: "Update every pull request updated within this many hours"
type: number
default: 6
permissions:
actions: read
checks: write
pull-requests: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || 'update' }}
cancel-in-progress: ${{ github.event_name == 'pull_request_target' }}
jobs:
bridge:
name: Mirror the fork run
runs-on: ubuntu-slim
timeout-minutes: 10
steps:
- name: Create or update the check
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
UPDATE_HOURS: ${{ inputs.hours || 6 }}
with:
script: |
const CHECK = 'Rust (fork)';
const WORKFLOW = 'rust.yml';
const upstream = { owner: context.repo.owner, repo: context.repo.repo };
const GUIDE = 'https://datafusion.apache.org/contributor-guide/#pull-requests-from-forks';
// Older pull requests are revisited this many at a time, so a rerun
// or a late run on a quiet pull request is mirrored within hours.
const SWEEP_SIZE = 25;
// A check run accepts a fixed set of conclusions; a workflow run can
// report others, such as startup_failure.
const CONCLUSIONS = new Set([
'action_required', 'cancelled', 'failure', 'neutral',
'skipped', 'stale', 'success', 'timed_out',
]);
const conclusionOf = (run) =>
CONCLUSIONS.has(run.conclusion) ? run.conclusion : 'failure';
const statusOf = (run) => {
if (run.status === 'completed') return 'completed';
return run.status === 'in_progress' ? 'in_progress' : 'queued';
};
const isForkPrToMain = (pr) =>
Boolean(pr.head.repo) && pr.head.repo.fork === true && pr.base.ref === 'main';
const HOW_UPDATED =
'Active or missing runs on recently updated pull requests are checked every 15 minutes. ' +
'Completed runs and older pull requests are checked by a rotating sweep, which can take a few hours. ' +
'The merge queue runs the full workflow on the merged result before this pull request merges.';
// The check for a run. `text` is read back by the scheduled update.
const runPayload = (fork, sha, run) => {
const status = statusOf(run);
return {
status,
...(status === 'completed' ? { conclusion: conclusionOf(run) } : {}),
details_url: run.html_url,
output: {
title: `Fork run ${status === 'completed' ? run.conclusion : status}`,
summary: [
`[Rust workflow run on ${fork.owner}/${fork.repo}](${run.html_url}) for this commit.`,
'',
'If the fork run failed for reasons unrelated to this pull request, rerun it on the fork.',
HOW_UPDATED,
].join('\n'),
text: JSON.stringify({
owner: fork.owner,
repo: fork.repo,
head_sha: sha,
run_id: run.id,
run_attempt: run.run_attempt,
}),
},
};
};
// The check when the fork has no run for the commit. The workflow's
// state on the fork says whether workflows are enabled there; a
// push that changes only documentation or Markdown does not start
// rust.yml even when they are. The scheduled update keeps looking
// for a run in every case.
const missingPayload = (fork, sha, state) => {
const disabled = typeof state === 'string' && state.startsWith('disabled');
const active = state === 'active';
const advice = disabled
? [
'1. Enable workflows on your fork: open its **Actions** tab and confirm the prompt.',
'2. Push again. A push that changes only documentation or Markdown does not start',
' the Rust workflow.',
]
: active
? [
'A push that changes only documentation or Markdown does not start the Rust',
'workflow, and a run can take a moment to appear. This check keeps looking for one.',
]
: [
'GitHub reports no Rust workflow on the fork, which usually means workflows are not',
'enabled there. Enable them on the fork\'s **Actions** tab and push again. A push that',
'changes only documentation or Markdown does not start the Rust workflow.',
];
return {
status: 'completed',
conclusion: disabled ? 'action_required' : active ? 'skipped' : 'neutral',
output: {
title: disabled ? 'Workflows are disabled on the fork' : 'No fork run for this commit',
summary: [
`No Rust workflow run was found on ${fork.owner}/${fork.repo} for ${sha}.`,
'',
...advice,
'',
HOW_UPDATED,
'',
`See ${GUIDE}`,
].join('\n'),
text: JSON.stringify({ owner: fork.owner, repo: fork.repo, head_sha: sha, run_id: null }),
},
};
};
// The fork creates its run a few seconds after the push.
async function findRun(fork, sha, attempts) {
for (let attempt = 0; attempt < attempts; attempt++) {
try {
const { data } = await github.rest.actions.listWorkflowRuns({
owner: fork.owner,
repo: fork.repo,
workflow_id: WORKFLOW,
head_sha: sha,
per_page: 10,
});
// A newer manual run can replace a cancelled push run.
const run = data.workflow_runs[0];
if (run) return run;
} catch (error) {
// 404 when the fork has Actions disabled or lacks the workflow.
core.info(`No run list for ${fork.owner}/${fork.repo}: ${error.message}`);
}
if (attempt + 1 < attempts) {
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
return null;
}
// `active`, or `disabled_fork` until the owner enables workflows;
// null when the workflow cannot be read.
async function workflowState(fork) {
try {
const { data } = await github.rest.actions.getWorkflow({
owner: fork.owner,
repo: fork.repo,
workflow_id: WORKFLOW,
});
return data.state || null;
} catch (error) {
core.info(`No workflow state for ${fork.owner}/${fork.repo}: ${error.message}`);
return null;
}
}
async function createCheck(pr, attempts) {
const fork = { owner: pr.head.repo.owner.login, repo: pr.head.repo.name };
const sha = pr.head.sha;
const run = await findRun(fork, sha, attempts);
const payload = run
? runPayload(fork, sha, run)
: missingPayload(fork, sha, await workflowState(fork));
await github.rest.checks.create({ ...upstream, name: CHECK, head_sha: sha, ...payload });
core.info(`Created ${CHECK} for ${sha}: ${payload.conclusion || payload.status}`);
}
async function notify() {
const pr = context.payload.pull_request;
// Only fork PRs to main skip jobs upstream; release branches have
// no merge queue and keep the full run.
if (!isForkPrToMain(pr)) {
core.info('Not a fork pull request to main; nothing to mirror.');
return;
}
await createCheck(pr, 12);
}
// Discover the latest run again, since a manual run can replace the
// recorded run, and the fork can delete old runs.
async function syncCheck(pr, includeCompleted = true) {
const { data } = await github.rest.checks.listForRef({
...upstream,
ref: pr.head.sha,
check_name: CHECK,
per_page: 5,
});
const check = data.check_runs[0];
if (!check) {
// Recover if the initial notification failed before creating a check.
await createCheck(pr, 1);
return;
}
if (!check.output || !check.output.text) return;
let params;
try {
params = JSON.parse(check.output.text);
} catch {
return;
}
// Completed runs only need the rotating sweep to discover reruns.
// A completed check with no run must keep looking for a late run.
if (!includeCompleted && check.status === 'completed' && params.run_id) return;
const fork = { owner: pr.head.repo.owner.login, repo: pr.head.repo.name };
const sha = pr.head.sha;
const run = await findRun(fork, sha, 1);
if (!run) return;
const payload = runPayload(fork, sha, run);
if (
payload.status === check.status &&
(payload.conclusion || null) === check.conclusion &&
run.id === params.run_id &&
run.run_attempt === params.run_attempt
) {
return;
}
await github.rest.checks.update({ ...upstream, check_run_id: check.id, ...payload });
core.info(`#${pr.number}: ${check.status}/${check.conclusion} -> ${payload.status}/${payload.conclusion || ''}`);
}
const listing = { ...upstream, state: 'open', sort: 'updated', direction: 'desc' };
// The number of open pull requests, from the last-page link of a
// one-item page: one request, whatever the count.
async function openPullRequestCount() {
const response = await github.rest.pulls.list({ ...listing, per_page: 1 });
const last = /[?&]page=(\d+)>;\s*rel="last"/.exec(response.headers.link || '');
return last ? Number(last[1]) : response.data.length;
}
async function update() {
const hours = Number(process.env.UPDATE_HOURS) || 6;
const cutoff = Date.now() - hours * 3600 * 1000;
const seen = new Set();
// Select the sweep first so a recent completed PR in this slice
// is checked once, even though it also appears in the window.
const slices = Math.max(1, Math.ceil((await openPullRequestCount()) / SWEEP_SIZE));
const slot = Math.floor(Date.now() / (15 * 60 * 1000));
const page = (slot % slices) + 1;
const { data: slice } = await github.rest.pulls.list({ ...listing, per_page: SWEEP_SIZE, page });
const sweepNumbers = new Set(slice.map((pr) => pr.number));
// Pull requests updated within the window, newest first. Stop at
// the window: GITHUB_TOKEN allows 1,000 requests per hour per
// repository.
const pages = github.paginate.iterator(github.rest.pulls.list, { ...listing, per_page: 100 });
for await (const page of pages) {
let stale = false;
for (const pr of page.data) {
if (new Date(pr.updated_at).getTime() < cutoff) {
stale = true;
break;
}
seen.add(pr.number);
if (isForkPrToMain(pr)) await syncCheck(pr, sweepNumbers.has(pr.number));
}
if (stale) break;
}
// Check the rest of the slice, including older pull requests.
// A fork rerun does not update the upstream pull request.
for (const pr of slice) {
if (seen.has(pr.number) || !isForkPrToMain(pr)) continue;
await syncCheck(pr);
}
}
if (context.eventName === 'pull_request_target') {
await notify();
} else {
await update();
}