Repository navigation
ci: run the Rust workflow for fork pull requests on the fork - #25591
Conversation
A pull request from a fork already runs rust.yml on the fork. Upstream ran the same 26 jobs again on GitHub-hosted runners, because the vars context is empty for a fork and every job falls back to ubuntu-latest. Skip 24 jobs upstream for a fork pull request to main and keep the two fast lint jobs. Add fork_ci_status.yml, which mirrors the fork run as one `Rust (fork)` check on the pull request without checking out or running pull request code. The merge queue still runs every job on the merged result. Fork pull requests to release branches, which have no merge queue, keep the full run. Move the skip-condition validation into check_rust_workflow_skips.py and return check_asf_yaml_status_checks.py to its original scope. Part of apache#25148
kosiew
left a comment
There was a problem hiding this comment.
Thanks for working on this. The approach looks good, and I don't see any blocking issues. I have a few non-blocking suggestions to make the fork CI status more resilient and strengthen the workflow validation.
| const fork = { owner: params.owner, repo: params.repo }; | ||
| const sha = params.head_sha || pr.head.sha; | ||
| let run = null; | ||
| if (params.run_id) { |
There was a problem hiding this comment.
Could we look for a replacement run when the recorded run is cancelled, fails, or can no longer be fetched? A workflow_dispatch run can cancel the push run for the same SHA, leaving the check showing the old result. We should select the newest run across event types rather than always preferring push runs.
| per_page: 5, | ||
| }); | ||
| const check = data.check_runs[0]; | ||
| if (!check || !check.output || !check.output.text) return; |
There was a problem hiding this comment.
If the initial notification fails before creating the check, the scheduled sweep cannot recover it. Could we let the sweep create a missing check using pr.head, or document that maintainers need to rerun the bridge job?
| return [needs] if isinstance(needs, str) else list(needs) | ||
|
|
||
|
|
||
| def check_skip_conditions(workflow, required_checks): |
There was a problem hiding this comment.
Could we add a few negative tests to confirm this validator catches regressions? For example, remove a skip guard, make an unskipped job depend on a skipped job, or remove an artifact-upload guard. This can be a follow-up.
|
Thank you @kosiew Addressed all the comments please take a look. |
kosiew
left a comment
There was a problem hiding this comment.
Thanks for addressing the review comments. I've reviewed the follow-up changes and confirmed that all three issues have been resolved.
The workflow now selects the newest fork run across event types, recovers missing checks during scheduled sweeps, and includes the requested validator negative tests.
I found no new issues. LGTM!
alamb
left a comment
There was a problem hiding this comment.
Thanks @kumarUjjawal and @kosiew this looks like it will be amazing and I think it will align costs nicely (the ASF won't pay credits to test the ever increasing number of PRs from others)
I just worry this will cause confusion with submitters when the CI doesn't run and they don't get any signal.
Maybe we can somehow post a note to the PR (or put a note in the PR template) that running the CI requires some extra steps
Also, after we merge this I think it would be helpful to post a note to dev@datafusion.apache.org to update people on the flow
| 1. Enable workflows on your fork. Open the **Actions** tab of your fork and | ||
| confirm the prompt. If the `Rust (fork)` check reports no fork run for a | ||
| commit that changed code, this step is missing; enable workflows and push | ||
| again. A push that changes only documentation or Markdown does not start |
There was a problem hiding this comment.
A push that changes only documentation or Markdown does not start
the Rust workflow; the check then reports that no fork run exists for the
commit, and the merge queue still runs the full workflow.
That seems like it describes an implementation detail and could probably be removed to make this paragraph easier to read
Sounds good! |
Added the |
alamb
left a comment
There was a problem hiding this comment.
Looks good -- thank you @kumarUjjawal .
Let's give it a try!
|
Thats a good thing, happy it moves into this direction. We had the same experiment in Comet months ago(apache/datafusion-comet#4289) and we should probably pursue it again. Let me also review it. |
|
We also need to check how this would work with Merge Queue |
comphead
left a comment
There was a problem hiding this comment.
Thanks @kumarUjjawal, this targets the right workflow. In a sample of 30 completed fork PR runs of rust.yml from Oct 18 and 19, about 136 of 138 job-minutes per run belong to the 24 jobs this PR skips. The other PR-time workflows (breaking_changes_detector, audit, docs_pr) cost about 2 to 3 minutes per run each. Fork runners should not be a slowdown here either, since most upstream PR jobs already use ubuntu-latest. My cargo test (amd64) comparison is one fork run at about 14 minutes against an upstream average of 18 minutes.
Some non-blocking suggestions, with two inline comments on the bridge.
- Fork concurrency group.
rust.ymlgroups bygithub.head_ref || github.sha. A push to a fork branch has nohead_ref, so every push gets its own group and superseded fork runs are never cancelled. The bridge only mirrors the head commit, so those runs are wasted on the contributor's account and delay the result. Spark avoids this withgithub.repository == 'apache/spark' && github.run_id || github.ref. Something likegithub.head_ref || (github.repository == 'apache/datafusion' && github.sha || github.ref)would keep upstreammainand the merge queue per SHA. I have not tested this. - Stale branches. The fork run tests the branch head, not the merge result. A green branch that is behind
maincan still fail in the queue and evict the PR, which stalls the entries behind it. Two options. The bridge could reportneutralwith a hint whencompareCommitsshows the branch is far behind. Orlinux-build-lib(about 6 minutes, roughly 4% of a run) could stay on for fork PRs as a merge-ref compile smoke test. - Fork
mainpushes. A fork's "Sync fork" push tomainstarts the full workflow on the contributor's runners. Spark skips default-branch pushes on forks. This does not affect ASF minutes, so it is optional. - Skip list. The description says two jobs stay on upstream, but
Check datafusion crates used in datafusion-examples(rust.yml,ubuntu-slim, about 0.2 minutes) is not inFORK_PR_SKIPPED_JOBSand ran in my sample. That seems fine, but the description and comment could say three. - Gate. The
Rust (fork)check is only as trustworthy as the fork'srust.yml, because the contributor controls it. Keeping it informational with the merge queue as the gate is the right call. It may be worth tracking queue evictions before and after rollout.
| jobs: | ||
| bridge: | ||
| name: Mirror the fork run | ||
| runs-on: ubuntu-latest |
There was a problem hiding this comment.
Could this use ubuntu-slim, as dev.yml and labeler.yml do? In my sample of recent fork PR runs, ubuntu-latest jobs waited about 67 to 88 minutes to start on average, while the one ubuntu-slim job waited about 2 minutes. The numbers are noisy, but if this job sits in the saturated pool, the check can appear an hour late. The script needs no more than the slim runner offers, and timeout-minutes: 10 fits its limit. Spark's notifier and updater both use ubuntu-slim.
| return last ? Number(last[1]) : response.data.length; | ||
| } | ||
|
|
||
| async function update() { |
There was a problem hiding this comment.
Every tick calls checks.listForRef and the fork's listWorkflowRuns for each fork PR updated in the last six hours, plus the 25-PR slice. With about 50 active PRs I estimate roughly 200 requests per tick, or about 800 per hour. That is unverified and close to the 1,000 per hour GITHUB_TOKEN limit mentioned above, and the pull_request_target retries add to it. The limit is per repository, so exhausting it would also hit the other workflows. Two ways to cut it. Re-poll only checks that are still queued or in_progress or have no run, and leave completed ones to the slice sweep, which is already how older PRs are handled. Or read all check states in one GraphQL query and call the fork only when needed.
|
Thank you @comphead I have addressed your review items, I’ve left stale-branch handling and queue-eviction tracking for follow-up. Fork main runs remain enabled because they populate caches used by branch runs. PTAL |
neilconway
left a comment
There was a problem hiding this comment.
Thanks @kumarUjjawal! Let's please get this merged soon 😅
| pull_request_target: | ||
| types: [opened, reopened, synchronize] |
There was a problem hiding this comment.
Apparently pull_request_target is going away next month: https://infra.apache.org/github-actions-policy.html
The ASF page suggests a workaround; will that work for what we need? But I'd be fine landing this as-is and then fixing before the end of the month.
There was a problem hiding this comment.
ouch, this is a good info
There was a problem hiding this comment.
let's merge this for now and we can find a more permanent solution in follow ups
what do you think @comphead @neilconway
There was a problem hiding this comment.
but IMO it should be fine, with user forks, the pull_request_target is called on the contributor fork
There was a problem hiding this comment.
It looks like there are security implications using pull_request_target -- are we worried about that ?
There was a problem hiding this comment.
It looks like there are security implications using
pull_request_target-- are we worried about that ?
IMO as long as it runs on user forks(which is), we are fine
comphead
left a comment
There was a problem hiding this comment.
Thanks @kumarUjjawal lets give it a try
|
Thank you all! |
|
The edge case is to check how dependabot works with user forks |
I will do some investigation and keep an eye on it so we don't miss anything. |
Which issue does this PR close?
Part of #25148.
Rationale for this change
Fork pull requests run the full Rust workflow upstream. Many contributors also run that workflow on their forks, which adds demand on shared ASF runners.
This PR uses fork runs for feedback on pull requests to
main. The fork run tests the branch head. The merge queue still runs all 27 Rust jobs on the merged result before merge.What changes are included in this PR?
main.check-fmt,check-workflow-tool-installs, andexamples-check-datafusion-cratesstill run.Rust (fork)check shows the fork run for the pull request's head commit. Scheduled updates pick up late runs and reruns.The bridge uses
pull_request_targetand the GitHub API. It never checks out or runs pull request code. The fork check is informational because contributors control their fork's workflows. It is not a required check for merge.What is the testing strategy for this PR?
Local checks passed for the job conditions, dependencies, and artifact guards. Bridge probes with stubbed API responses covered missing runs, late runs, reruns, and pagination beyond 500 open pull requests.
Hosted end-to-end validation of the new bridge remains outstanding.
pull_request_targetuses the base branch's workflow, so this pull request cannot exercise the new bridge on GitHub.Are there any user-facing changes?
Contributors must enable GitHub Actions on their forks. The contributor guide and PR template explain this requirement. Pushes that change only documentation or Markdown do not start the Rust workflow.