Repository navigation
Conversation
pull_request_target is disabled by ASF infra policy and GitHub is adding a default policy that blocks it. Add pull_request_signal.yml and trigger labeler.yml and fork_ci_status.yml from its workflow_run. Label merged pull requests from the push to main in release_version_labeler.yaml.
|
Thank you for working on this |
All the credits to Apache Spark community, lets see if it works for us |
|
Nice! Good job :) . World is better with every single |
|
Glad my examples were helpful and useful to replicate :D |
Label every pull request that matches the head commit, and mirror the fork check only for the pull request to main.
The merged pull request can lag the push to main by a few seconds. Retry up to three times, and fail the job when no merged pull request is found.
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| commit_sha: context.sha | ||
| }); |
There was a problem hiding this comment.
On push, the script looks up PRs only for context.sha, so every other PR merged by the same push never gets the release version label.
There was a problem hiding this comment.
Fixed in 4c3c2e9. The script now looks up every commit in context.payload.commits (or context.sha when the payload has none) and labels each merged PR once. In the last 40 merges every merge commit was the head of its own push, so this is robustness and not a current gap. test_release_version_labeler.cjs covers a push with two commits that share a PR.
| }); | ||
| pulls = data.filter((pull) => pull.merged_at && pull.base.ref === 'main'); | ||
| } | ||
| if (pulls.length === 0) { |
There was a problem hiding this comment.
The script now throws when a push to main has no merged PR, which turns ordinary direct pushes into failed workflow runs on main.
There was a problem hiding this comment.
Agreed. After the three attempts it now logs No merged pull request found for <sha> and the run succeeds. All of the last 100 commits on main had a merged PR, but a failed run on main is too noisy for a cosmetic label. A test covers it.
| # - labeler.yml | ||
| # - fork_ci_status.yml | ||
| name: Pull request signal | ||
|
|
There was a problem hiding this comment.
The signal workflow runs from the PR's own version of the file, so a PR that renames, edits or deletes it turns off labeling and the Rust (fork) check for itself without any error.
There was a problem hiding this comment.
True, a PR runs its own copy of the file. It can only skip the labels and the immediate Rust (fork) check for itself, and it gains nothing else. The signal run has permissions: {}, and the privileged workflows only read the head repository, branch and SHA of the run, which the PR author controls anyway. The scheduled update in fork_ci_status.yml still creates a missing Rust (fork) check, and the merge queue stays the gate. I documented this in the header of the signal workflow in 4c3c2e9.
|
|
||
| async function notify() { | ||
| const pr = context.payload.pull_request; | ||
| // An outdated commit matches nothing, since a newer run mirrors the new head. |
There was a problem hiding this comment.
The script dereferences run.head_repository.owner.login without a null check; head_repository is null when the fork was deleted or is unavailable.
There was a problem hiding this comment.
Fixed in 4c3c2e9. The shared lookup returns no pull requests when head_repository is null, so neither workflow dereferences it. Tests cover the lookup and the fork_ci_status notification.
| # find the pull requests by their head branch and commit. The same branch | ||
| # can target `main` and a release branch, so label every match. An | ||
| # outdated commit matches nothing, since a newer run labels the new head. | ||
| - name: Find the pull requests |
There was a problem hiding this comment.
The PR lookup by head branch plus SHA is written twice: inline in labeler.yml and in pullRequestOf in fork_ci_status.yml. Only the second copy has tests.
There was a problem hiding this comment.
Fixed in 4c3c2e9. Both workflows now call ci/scripts/pull_requests_of_run.js, after a sparse checkout of ci/scripts from the default branch (never the pull request). The tests in test_fork_ci_status.cjs cover it directly, including the no-base case that the labeler uses.
| # default policy that blocks it. The `workflow_run` workflows below run from the | ||
| # default branch with a write token and never check out or run pull request | ||
| # code. They trigger on the `requested` event of this workflow, which GitHub | ||
| # sends at once for every run, even for a first-time contributor whose run waits |
There was a problem hiding this comment.
The design relies on GitHub sending workflow_run: requested for a run that is waiting for first-time-contributor approval. That is not documented; GitHub may hold the event until a maintainer approves the run.
There was a problem hiding this comment.
You are right that it is not documented. It is observed behavior. In apache/spark-kubernetes-operator two signal runs from an outside fork stayed completed/action_required (never approved), and each still started its workflow_run consumer 2 to 3 seconds later. I noted this in the header of the signal workflow in 4c3c2e9. If GitHub changes it, the scheduled update in fork_ci_status.yml still creates a missing Rust (fork) check, but labeling would need a follow-up.
| @@ -11,16 +11,15 @@ concurrency: | |||
| group: ${{ github.repository }}-${{ github.head_ref || github.sha }}-${{ github.workflow }} | |||
There was a problem hiding this comment.
The concurrency group still uses github.head_ref || github.sha. With a push trigger head_ref is always empty and every SHA is unique, so the group and cancel-in-progress no longer do anything.
There was a problem hiding this comment.
Fixed in 4c3c2e9. I removed the concurrency block. Each push needs its own run, so there is nothing to cancel or serialize.
| # `pull_request_signal.yml` it reads the fork's public run list and creates the | ||
| # check (`pull_request_target` is disabled by ASF infra policy), 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 |
There was a problem hiding this comment.
The rewritten header comment is not re-wrapped; the line "requests plus a rotating slice ... The merge" runs far past the wrap width of the lines around it.
There was a problem hiding this comment.
Fixed in 4c3c2e9. Re-wrapped, and I dropped the parenthetical that the header of the signal workflow already explains.
|
regarding labelling, have we considered using a bot? it was suggested on an infra confluence page and seems to be used by airflow too
one note is we'd need an infra ticket to enable the permissions, e.g. |
- Share the pull request lookup of the labeler and fork_ci_status workflows in ci/scripts/pull_requests_of_run.js, guard against a deleted fork, and test it. - Label every merged pull request in a push, and log instead of failing when a push has none. - Drop the concurrency group of the release version labeler, which never applied to a push. - Document the signal workflow limits and re-wrap the fork_ci_status header.
|
This PR keeps the current |
| runs: [run(1)], | ||
| }); | ||
|
|
||
| test("notification skips a run whose fork was deleted", async () => { |
There was a problem hiding this comment.
A misplaced closing brace leaves five new tests, plus the findPullRequests/lookup helpers, inside the body of the test "notification skips a commit that is no longer a pull request head"; that test's own assertion ends up at line 266, after them.
Failure scenario: The inner test() calls (deleted fork, three lookup tests) become unawaited subtests of the stale-head test instead of top-level tests. If the parent finishes or fails first, node:test cancels them or reports them as failures under the wrong name. Whether they run at all depends on the Node version's subtest handling. The stale-head check itself only runs after these extra subtests have been registered.
There was a problem hiding this comment.
Good catch. The }); I matched was the end of the execute(...) call, so the new tests ran as subtests of the stale-head test. Fixed in f64546b. They are top-level tests now (the runner output has no indented test lines), and the stale-head test asserts on its own again.
| pull_request_target: | ||
| types: | ||
| - closed | ||
| push: |
There was a problem hiding this comment.
The new push: main trigger has no if: github.repository == 'apache/datafusion' guard, so it now runs in every fork that has Actions enabled. Forks were affected before only when PRs were closed inside the fork itself.
Failure scenario: The contributor guide asks fork owners to enable workflows. A contributor who syncs their fork's main then triggers this job on each sync. It creates a v57.0.0 label in the fork, waits through the retries, and calls addLabels on the fork for any PR numbers the lookup returns. That spends the fork's minutes and can label the wrong fork issue or fail with a 404. merge_queue_cleanup.yml and draft_inactive_prs.yml already use this guard.
There was a problem hiding this comment.
Fixed in f64546b. The job now has if: github.repository == 'apache/datafusion', like merge_queue_cleanup.yml and draft_inactive_prs.yml.
| # and the merge queue stays the gate. | ||
| name: Pull request signal | ||
|
|
||
| on: |
There was a problem hiding this comment.
The signal uses pull_request, which GitHub does not run while a PR has merge conflicts (there is no merge ref). The old pull_request_target triggers ran either way.
Failure scenario: A contributor pushes to a PR that conflicts with main. No signal run starts, so labeler.yml never re-syncs labels for the new files and fork_ci_status.yml never creates the immediate Rust (fork) check. Labels stay stale until the conflict is fixed and another push lands, and the header comment does not mention this gap.
There was a problem hiding this comment.
Right, this is a known gap. I noted it in the header of the signal workflow in f64546b. A PR with conflicts gets its labels after the push that resolves them. The scheduled update in fork_ci_status.yml creates the Rust (fork) check regardless, because it selects PRs by their head and base and not by mergeability. The Rust workflow has the same limit, since it also runs on pull_request.
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || 'update' }} | ||
| cancel-in-progress: ${{ github.event_name == 'pull_request_target' }} | ||
| group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name && format('{0}-{1}', github.event.workflow_run.head_repository.full_name, github.event.workflow_run.head_branch) || 'update' }} |
There was a problem hiding this comment.
When workflow_run.head_repository is null (deleted fork), the concurrency expression falls back to the update group used by the scheduled sweep, while cancel-in-progress is still true because the event is workflow_run.
A signal run, or a rerun, from a PR whose fork was deleted triggers this workflow. It joins group Fork CI status-update and cancels the scheduled or manually dispatched sweep that is running, even though its own notify() finds nothing to do.
There was a problem hiding this comment.
Confirmed and fixed in f64546b. The per-branch group is now chosen by github.event_name == 'workflow_run', so a signal run from a deleted fork gets its own group and no longer cancels the sweep. Two tests cover the groups, and the one for the deleted fork fails with the old expression.
| // A push can include several merged pull requests. | ||
| const shas = (context.payload.commits || []).map((commit) => commit.id); | ||
| const numbers = new Set(); | ||
| for (const sha of shas.length ? shas : [context.sha]) { |
There was a problem hiding this comment.
Each commit in the push is looked up one after another, and every commit without a merged PR costs two 5-second waits.
A direct push to main with N commits that have no PR blocks the job for about 10s × N (20 commits is about 200s of runner time) before it gives up. Retrying only the head commit, or looking up commits in parallel, would avoid most of that.
There was a problem hiding this comment.
Fixed in f64546b. The commits are looked up in parallel with Promise.all, each with its own retries, so a push whose commits have no PR waits about 10 seconds in total and not 10 seconds per commit. A test checks that the lookups overlap and that only the lagging commit is retried.
| } | ||
| } | ||
| if (numbers.size === 0) { | ||
| console.log(`No merged pull request found for ${context.sha}`); |
There was a problem hiding this comment.
The "No merged pull request found" message prints only context.sha, although the script searched every commit in payload.commits.
A multi-commit push where none of the commits maps to a merged PR logs only the head SHA, so whoever debugs a missing label can't tell which commits were checked.
There was a problem hiding this comment.
Fixed in f64546b. The message lists every commit that was searched. The old test had locked in the head SHA, so it now checks a push with two commits.
- Close the stale-head test before the new tests, which were nested in it. - Skip the release version labeler outside apache/datafusion. - Keep a signal run from a deleted fork out of the sweep concurrency group. - Look up the commits of a push in parallel and name them all in the log. - Note in the signal workflow that pull requests with conflicts are skipped.
| // parallel, so the retries of commits without one overlap. | ||
| const pushed = (context.payload.commits || []).map((commit) => commit.id); | ||
| const shas = pushed.length ? pushed : [context.sha]; | ||
| const found = await Promise.all(shas.map((sha) => mergedPulls({ github, context, sha }))); |
There was a problem hiding this comment.
All commits of the push are looked up at the same time with no limit, and one failed lookup makes Promise.all reject, so no pull request in the push gets the label.
A push to main can hold many commits, for example a branch merged with a merge commit or a large merge queue group. Each commit starts its own listPullRequestsAssociatedWithCommit call at once. A burst of up to 2048 calls can hit GitHub's secondary rate limit (about 100 requests at the same time). Then one call fails with 403/429, or one call fails with a normal 5xx. Promise.all rejects, the job fails before addLabels runs, and no pull request in that push gets the label. The old pull_request_target flow labeled each pull request in its own run, so one error never cost other pull requests their label. Fix: look the commits up in small batches, and catch the error for each commit (for example with Promise.allSettled) so that the lookups that worked still lead to labels.
There was a problem hiding this comment.
Sound, and fixed in aa555be. The commits are looked up ten at a time with Promise.allSettled, so a failed lookup no longer drops the labels of the other pull requests. The script labels what it found and then rethrows the first error, so the run is red and a rerun can finish the job. A failed addLabels is isolated the same way. The docs allow up to 2048 commits in the push payload, so the bound is not only theoretical, although each of the last 40 merges to main was a single commit. Tests: 25 commits never have more than 10 lookups in flight, and a failed lookup or a failed label still labels the others.
| # code of a pull request. See that file for details. | ||
| concurrency: | ||
| group: ${{ github.repository }}-${{ github.head_ref || github.sha }}-${{ github.workflow }} | ||
| group: ${{ github.repository }}-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}-${{ github.workflow }} |
There was a problem hiding this comment.
The concurrency group is the head repository and branch with cancel-in-progress, but the lookup only matches the run's own head SHA. If an older run is queued after a newer one, it cancels the newer run and then labels nothing.
Failure scenario: A contributor pushes twice quickly to a fork branch. If the workflow_run requested event for push 1 is queued after the event for push 2, the push 1 run cancels the push 2 run. pull_requests_of_run.js then drops the pull request, because pull.head.sha no longer equals run.head_sha. Neither run labels the new head, and labels stay stale until the next push. fork_ci_status.yml has the same group shape, but its scheduled sweep fixes the check later. The labeler has no such recovery. Fix: let the run that lost the race use the current head (drop the SHA filter for the labeler), or put the SHA in the group and turn off cancel-in-progress.
There was a problem hiding this comment.
Sound, and fixed in aa555be with your first suggestion. pull_requests_of_run.js no longer compares head.sha with the run's commit, so the run that survives a cancellation labels the current head. That is safe because actions/labeler computes labels from the files of the PR as it is now, so a delayed run for an older commit gives the same result as the newest one. I kept the group and cancel-in-progress as they are, since putting the SHA in the group would only multiply runs. A test covers a PR whose head moved after the run started.
| jobs: | ||
| bridge: | ||
| name: Mirror the fork run | ||
| if: github.event_name != 'workflow_run' || github.event.workflow_run.event == 'pull_request' |
There was a problem hiding this comment.
The job runs a runner, a checkout and a pulls.list call for every signal run, even when the payload already shows that the head repository is not a fork or was deleted.
Failure scenario: Every push to a pull request from an apache/datafusion branch starts the signal. The bridge job then gets a runner, does the sparse checkout and calls the API, and only after that notify() says 'Not a fork pull request to main'. The same happens for a deleted fork (head_repository is null). Fix: add github.event.workflow_run.head_repository.fork == true to the job-level if for the workflow_run event. That skips these runs before any runner starts, and the release-branch check stays in notify().
There was a problem hiding this comment.
Fixed in aa555be. For workflow_run events the job if now also requires github.event.workflow_run.head_repository.fork == true. Branches of this repository (Dependabot among them) and deleted forks no longer start a runner, and notify() still checks the base. Tests evaluate the expression for a fork, a branch of this repository, a deleted fork, another triggering event and the scheduled and manual runs.
| head: `${run.head_repository.owner.login}:${run.head_branch}`, | ||
| ...(base && { base }) | ||
| }); | ||
| return data.filter((pull) => pull.head.sha === run.head_sha); |
There was a problem hiding this comment.
Prevent stale signals from cancelling the current label update. For rapid successive pushes, GitHub does not guarantee execution order within a concurrency group. If the older signal's consumer arrives last, labeler.yml cancels the newer consumer because both share the branch-scoped group. This filter then discards the older SHA, so neither run updates the labels, and there is no scheduled label reconciliation. Reconcile the current PR head or ensure obsolete signals cannot cancel its update.
There was a problem hiding this comment.
Same fix as in the labeler thread. The lookup now returns the open pull requests of the branch as they are, so whichever signal run survives labels the current head. fork_ci_status.yml still compares the commit in notify(). A late run that created the check for the current head could add a second Rust (fork) check to a commit that already has one, and its scheduled sweep covers the cancelled case.
| // parallel, so the retries of commits without one overlap. | ||
| const pushed = (context.payload.commits || []).map((commit) => commit.id); | ||
| const shas = pushed.length ? pushed : [context.sha]; | ||
| const found = await Promise.all(shas.map((sha) => mergedPulls({ github, context, sha }))); |
There was a problem hiding this comment.
Very unlikely but for a push containing more than 100 commits, this launches enough simultaneous requests to exceed GitHub's secondary limit of 100 concurrent API requests. A rejected lookup rejects the entire Promise.all before any labels are applied, including for successfully resolved PRs. Use bounded parallelism rather than launching one request per commit; a 101-commit stub reproduces 101 concurrent requests and zero labels after a rate-limit rejection.
There was a problem hiding this comment.
Covered by the same change in aa555be. Lookups run ten at a time, and a rejected lookup no longer drops the labels of the others. A 25-commit stub never has more than 10 requests in flight.
should be fine to keep it as actions; i'll probably test it out on arrow-rs instead |
- Look up the commits of a push ten at a time, label the pull requests that were found when a lookup or a label fails, and fail the run afterwards. - Return the current pull requests of the branch from the shared lookup, so a delayed run for an older commit still labels the latest head. fork_ci_status keeps comparing the commit. - Skip the fork_ci_status job for runs that are not from a fork before a runner starts.
kumarUjjawal
left a comment
There was a problem hiding this comment.
@comphead sorry about all the revisions asks 😬
| }); | ||
| // A run for an older commit finds nothing, and the scheduled | ||
| // update creates the check of the new head. | ||
| const pr = pulls.find( |
There was a problem hiding this comment.
When a second pull request from the same fork branch targets a release branch, its signal run creates a second Rust (fork) check on the pull request to main
There was a problem hiding this comment.
Confirmed. The old pull_request_target run of the release-branch PR returned early, but the new lookup goes through the branch, so it finds the main PR at the same commit and creates a second check. Fixed in 6ce320a: notify() first lists the Rust (fork) checks of the commit, and when one exists it updates it through the existing syncCheck() instead of creating another. That also covers a reopened PR, which got a second check in the old flow too. A test covers a release-branch PR next to the main PR. It fails both without the lookup and when the existing check is skipped instead of updated.
| name: Pull request signal | ||
|
|
||
| on: | ||
| pull_request: |
There was a problem hiding this comment.
The signal uses pull_request, and GitHub skips that event when the head commit message contains [skip ci] (or [ci skip], [no ci] and similar). The old pull_request_target triggers ran anyway, and the header comment does not list this gap.**
There was a problem hiding this comment.
Confirmed. GitHub documents that the markers skip pull_request workflows when they are in the head commit, and that they do not stop pull_request_target. I listed it next to merge conflicts in the header in 6ce320a. The fork's own rust.yml push run is skipped by the same marker, so the scheduled update creates a Rust (fork) check that reports no fork run for the commit, which matches what happened. The labels follow with the next push that has no marker. I also noted that a re-run of the signal does not send requested, which GitHub documents, and how to trigger it again.
| errors.push(error); | ||
| } | ||
| } | ||
| if (errors.length > 0) { |
There was a problem hiding this comment.
Only the first error is thrown. The other lookup and label errors are counted but their messages are never logged.
There was a problem hiding this comment.
Fixed in 6ce320a. Each failed lookup is logged with its commit, and each failed label with its pull request, when it happens. A summary line follows, and the first error is still thrown so the run is red. A test checks the log lines for a run with one failed lookup and one failed label.
- Update the Rust (fork) check a commit already has instead of adding a second one, when a pull request of the same branch to a release branch or a reopened pull request signals that commit again. - Log every failed lookup and label of the release version labeler with its commit or pull request. - Note in the signal workflow that skip markers in the head commit skip it, and that a re-run of it does not send requested.
|
@kumarUjjawal no need to apologize. Every round found something real, and the PR is better for it. The three points from this round are fixed in 6ce320a. I also confirmed that Dependabot PRs keep their labels, which the description listed as unverified. The |
💯 |
| on: | ||
| pull_request_target: | ||
| types: | ||
| - closed | ||
| push: | ||
| branches: | ||
| - main |
There was a problem hiding this comment.
Recover release labels for merges that skip push workflows. If a squash-merge commit contains [skip ci], [ci skip], or another supported skip marker, GitHub suppresses this push workflow. Unlike the previous pull_request_target: closed trigger, the merged PR then never receives its release-version label. Subsequent pushes only inspect their own commits, so they cannot recover the missing label. Add a reconciliation path that does not depend on the merge's push workflow running.
There was a problem hiding this comment.
Sound, and it applies more widely than it looks. The merge queue squashes with the PR body as the commit message, so a skip marker anywhere in a PR description reaches main. None of the last 3000 commits on main has one, but this PR's own description had two until today, which I removed. Fixed in c24e43a: each push run now labels every PR merged to main in the last 7 days that has no release label, with the version that release_version_labeler.js names at the PR's merge commit. So a later push labels a merge whose own push skipped the workflow, without depending on that push. This replaces the lookup of the pushed commits. A test checks that dev/update_datafusion_versions.py still rewrites only the version constant, since the script now also reads that constant back at older commits.
| // A pull request of the same branch to a release branch, or a | ||
| // reopened pull request, signals a commit that can already have | ||
| // the check. Update that check instead of adding a second one. | ||
| const { data } = await github.rest.checks.listForRef({ |
There was a problem hiding this comment.
notify() checks for an existing Rust (fork) check before createCheck() starts its wait for the fork run, which can last up to 60 s (12 attempts x 5 s). A check created during that wait is not seen, so the new dedup fails exactly when it is needed.
There was a problem hiding this comment.
Sound. The overlap that matters is with the scheduled update, which has its own concurrency group. Two signals for the same branch share one, so only the newer runs. Fixed in cb97e0b: createCheck() looks for the commit's check only after the wait, and updates it if one exists. The scheduled update's recovery path calls createCheck() too, so it is covered from that side as well. A test creates the check while the run looks for the fork run, and it fails with the earlier order.
| commit_sha: sha | ||
| }); | ||
| const pulls = data.filter((pull) => pull.merged_at && pull.base.ref === 'main'); | ||
| if (pulls.length > 0 || attempt === 3) { |
There was a problem hiding this comment.
The lookup gives up about 10 s after the push (3 attempts, 5 s apart) and then only logs, so the run stays green. The old pull_request_target: closed trigger fired only after the PR was marked merged and always carried the PR number.
There was a problem hiding this comment.
Fixed by the same change in c24e43a. The run no longer looks up the pushed commits, so there is nothing to wait for. It lists the recently merged PRs, and for the last 40 merges merged_at was always set before the push run was created. A PR that a run still misses, or a run that fails, is labeled by the next push.
| // A pull request of the same branch to a release branch, or a | ||
| // reopened pull request, signals a commit that can already have | ||
| // the check. Update that check instead of adding a second one. | ||
| const { data } = await github.rest.checks.listForRef({ |
There was a problem hiding this comment.
notify() calls checks.listForRef(check_name: CHECK) and then syncCheck(pr), which immediately makes the same listForRef call again. That is a redundant API request, and the existence check lives in two places.
There was a problem hiding this comment.
Fixed in cb97e0b. The existence check now lives only in createCheck(), and notify() calls it directly, so there is one lookup.
| } | ||
| // Only fork PRs to main skip jobs upstream; release branches have | ||
| // no merge queue and keep the full run. | ||
| if (!isForkPrToMain(pr)) { |
There was a problem hiding this comment.
In notify(), the isForkPrToMain(pr) guard is now dead code. pulls.list is called with base 'main', and the job-level if already requires head_repository.fork == true.
There was a problem hiding this comment.
Right, fixed in cb97e0b. I removed the guard and moved its reason into the comment on the base: 'main' lookup. The scheduled update still uses isForkPrToMain().
| # This checks out the default branch, never the pull request. | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| sparse-checkout: ci/scripts |
There was a problem hiding this comment.
Every labeler run makes an extra contents API request (and depends on the API fallback path) for a file that the checkout already does could provide. Adding .github/workflows/labeler to sparse-checkout would let the action read the config locally.
There was a problem hiding this comment.
Confirmed in the source of the pinned actions/labeler: it reads the config from the file when the file exists, and falls back to the contents API otherwise. Fixed in cb97e0b by adding .github/workflows/labeler to the sparse checkout. The checkout is the default branch, so the content is the same.
| ## CI Workflow Tests | ||
|
|
||
| The [Rust workflow validator tests] and [fork status bridge tests] run in CI and in `./dev/rust_lint.sh`. | ||
| The [Rust workflow validator tests], [fork status bridge tests] and [release version labeler tests] run in CI and in `./dev/rust_lint.sh`. |
There was a problem hiding this comment.
nit: should we also mention test_cancel_merge_queue_runs.cjs?
There was a problem hiding this comment.
Creating the label is a non-atomic getLabel/createLabel pair, and the push-triggered runs have no concurrency group. Two runs close together can both get a 404 and both call createLabel; the loser throws 422 before labeling anything.
Treating 422 from createLabel as success would fix it.
There was a problem hiding this comment.
Every push to main writes the write-scoped GITHUB_TOKEN into the workspace's git config, where any later step or action can read it. It also clones the whole repository when only ci/scripts is needed, which is inconsistent with the hardening this PR applies to labeler.yml and fork_ci_status.yml.
There was a problem hiding this comment.
Fixed in cb97e0b: a sparse checkout of ci/scripts with persist-credentials: false, like the other two workflows.
There was a problem hiding this comment.
should this also use commit SHA like others?
There was a problem hiding this comment.
Yes, fixed in cb97e0b. It was the only use of actions/github-script by tag on main.
- Look for the commit's Rust (fork) check after waiting for the fork run, in createCheck, so a check created meanwhile is updated instead of duplicated, and drop the earlier lookup in notify() along with its dead fork guard. - Check out the labeler config so actions/labeler reads it from the file. - Treat a 422 from createLabel as success, since another run can create the release label first. - Use a sparse checkout without persisted credentials in the release version labeler, and pin actions/github-script by commit. - List the merge queue cleanup tests with the other CI workflow tests.
Each run of the release version labeler now labels the pull requests merged to main in the last week that have no release label, with the version that the script names at their merge commit. A later push thereby labels a merge whose own push did not run the workflow, for example because of a skip marker in the commit message, or whose run failed. This replaces the lookup of the pushed commits and its retries.
Which issue does this PR close?
pull_request_target#26406Rationale for this change
Three workflows use
pull_request_target:labeler.yml,fork_ci_status.ymlandrelease_version_labeler.yaml. ASF infra disables the trigger from November 2026. GitHub also adds a default policy that blocks it on public repositories, enforced from 2026-12-02. Without this change, PRs stop getting labels, theRust (fork)check and the release version label.What changes are included in this PR?
pull_request_signal.yml, an unprivilegedpull_requestworkflow withpermissions: {}and oneechostep. Its header lists what it does not cover: a PR that edits its own copy of the file, a PR with merge conflicts, a head commit with a skip marker, and re-runs, which do not sendrequested.labeler.ymlandfork_ci_status.ymlrun onworkflow_runof that workflow withtypes: [requested]. GitHub runs them from the default branch, so they never run PR code.requestedis sent at once, even for a first-time contributor whose run waits for approval.workflow_run.pull_requestsis empty for fork PRs. Both workflows find the PRs with the sharedci/scripts/pull_requests_of_run.js, after a sparse checkout ofci/scriptsfrom the default branch (never the PR). The labeler also checks out its config, soactions/labelerreads it from the file. It lists the open PRs of the run's head branch and returns none when the fork was deleted.labeler.ymllabels every match, since the same branch can targetmainand a release branch, and passes the numbers toactions/labeleraspr-number. The matches are the current PRs, so a delayed run for an older commit still labels the latest head.fork_ci_status.ymllooks only at PRs tomainand skips a commit that is no longer the PR head, because its scheduled update creates the check of the new head. When the commit already has aRust (fork)check, for example after a signal from a PR of the same branch to a release branch or from a reopened PR, it updates that check instead of adding a second one. It looks for that check only after waiting for the fork run, so a check that the scheduled update created meanwhile is updated too. Its job also skipsworkflow_runevents that are not from a fork before a runner starts.workflow_runhead repository and branch, sincegithub.head_refand the PR number are not set. Infork_ci_status.ymlonlyworkflow_runevents use that group, so a run from a deleted fork cannot cancel the scheduled sweep.release_version_labeler.yamlruns onpushtomaininapache/datafusiononly, so a fork that syncs itsmaindoes nothing. Each run labels every PR merged tomainin the last 7 days that has no release label yet, with the version thatrelease_version_labeler.jsnames at the PR's merge commit. A later push therefore labels a merge whose own push did not run the workflow, for example because of a skip marker in the squash commit message, which comes from the PR body, or whose run failed. A failed read or label is logged with its PR, the other PRs are labeled, and the run then fails. A 422 fromcreateLabelcounts as success, since another run can create the label first. The checkout is sparse and does not persist credentials,actions/github-scriptis pinned by commit, and the concurrency group is removed, since it never applied to a push.test_fork_ci_status.cjsnow drives the notification with aworkflow_runpayload and covers the shared lookup. The newtest_release_version_labeler.cjscovers the release labeler. Both run indev.ymlanddev/rust_lint.sh, andtesting_infrastructure.mdlists the new one with the other CI workflow tests, including the merge queue cleanup tests it did not list yet.What is the testing strategy for this PR?
node --test ci/scripts/test_fork_ci_status.cjs ci/scripts/test_release_version_labeler.cjspasses (45 tests). They include a newer release-branch PR next to an oldermainPR from the same branch, a PR whose head moved after the run, a deleted fork, a stale commit, a commit that already has the check or gets it while the run waits, the concurrency groups and the jobif. For the release labeler they cover the version at an earlier merge commit, the window and its paging, existing release labels, a missing or concurrently created label, a failure that is logged and still labels the other PRs, and a check thatdev/update_datafusion_versions.pyrewrites only the version constant. Each fix was also checked by breaking it and seeing its test fail.actionlint,hawkeyeandtyposreport nothing on a merge withmain.main. Each resolved to the right PR.datafusion-ballistalabeler, migrated the same way, labeled Dependabot PRs Eliminate multi limit-offset nodes to emptyRelation #2823 and Add Python examples for reading CSV and query by SQL in Doc #2824 fromworkflow_runruns triggered by Dependabot on 2026-11-09. The push of a merged Dependabot PR tomaincomes fromgithub-merge-queue[bot], so the release version labeler gets a normal token.workflow_runflow only takes effect after the merge tomain, because GitHub runs these workflows from the default branch. It is not tested end to end. Things to check after the merge: that a PR from a fork gets labels and theRust (fork)check, and that merge queue commits get the release label.actions/labelerreadslabeler-config.ymlatgithub.context.sha, which is the default branch underworkflow_run(read from the action source, not run).Are there any user-facing changes?
No. A PR with merge conflicts, or whose head commit has a skip marker, does not start
pull_requestworkflows, so it is labeled with a later push, and the scheduled update creates itsRust (fork)check. Theeditedlabel trigger was dropped, since the old workflow skipped that action already.