Repository navigation
ci: migrate pull_request_target workflows to workflow_run #26683
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2e5546d
078a414
2f56678
cb1509a
bb9fddd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,18 +7,20 @@ | |
| # 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 | ||
| # runs code from the pull request: on `workflow_run` of | ||
| # `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 | ||
| # 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] | ||
| workflow_run: | ||
| workflows: ["Pull request signal"] | ||
| types: [requested] | ||
| schedule: | ||
| - cron: "*/15 * * * *" | ||
| workflow_dispatch: | ||
|
|
@@ -34,12 +36,13 @@ permissions: | |
| pull-requests: read | ||
|
|
||
| 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' }} | ||
| cancel-in-progress: ${{ github.event_name == 'workflow_run' }} | ||
|
|
||
| jobs: | ||
| bridge: | ||
| name: Mirror the fork run | ||
| if: github.event_name != 'workflow_run' || github.event.workflow_run.event == 'pull_request' | ||
| runs-on: ubuntu-slim | ||
| timeout-minutes: 10 | ||
| steps: | ||
|
|
@@ -198,8 +201,27 @@ jobs: | |
| core.info(`Created ${CHECK} for ${sha}: ${payload.conclusion || payload.status}`); | ||
| } | ||
|
|
||
| // `workflow_run.pull_requests` is empty for pull requests from | ||
| // forks, so find the pull request by its head branch and commit. | ||
| // The same branch can also target a release branch, which gets no | ||
| // mirrored check, so list only the pull requests to `main`. | ||
| async function pullRequestOf(run) { | ||
| const { data } = await github.rest.pulls.list({ | ||
| ...upstream, | ||
| state: 'open', | ||
| base: 'main', | ||
| head: `${run.head_repository.owner.login}:${run.head_branch}`, | ||
| }); | ||
| return data.find((pr) => pr.head.sha === run.head_sha); | ||
| } | ||
|
|
||
| async function notify() { | ||
| const pr = context.payload.pull_request; | ||
| // An outdated commit matches nothing, since a newer run mirrors the new head. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The script dereferences |
||
| const pr = await pullRequestOf(context.payload.workflow_run); | ||
| if (!pr) { | ||
| core.info('No open pull request for this commit; nothing to mirror.'); | ||
| return; | ||
| } | ||
| // Only fork PRs to main skip jobs upstream; release branches have | ||
| // no merge queue and keep the full run. | ||
| if (!isForkPrToMain(pr)) { | ||
|
|
@@ -299,7 +321,7 @@ jobs: | |
| } | ||
| } | ||
|
|
||
| if (context.eventName === 'pull_request_target') { | ||
| if (context.eventName === 'workflow_run') { | ||
| await notify(); | ||
| } else { | ||
| await update(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,36 +4,60 @@ | |
|
|
||
| name: Labeler | ||
|
|
||
| # `pull_request_target` is disabled by ASF infra policy. This workflow runs from | ||
| # the default branch when `pull_request_signal.yml` starts, so it never runs the | ||
| # 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 }} | ||
| cancel-in-progress: true | ||
|
|
||
| on: | ||
| pull_request_target: | ||
| types: | ||
| - opened | ||
| - edited | ||
| - synchronize | ||
| workflow_run: | ||
| workflows: ["Pull request signal"] | ||
| types: [requested] | ||
|
|
||
| permissions: {} | ||
|
|
||
| jobs: | ||
| process: | ||
| name: Process | ||
| runs-on: ubuntu-slim | ||
| # only run for users whose permissions allow them to update PRs | ||
| # otherwise labeler is failing: | ||
| if: github.event.workflow_run.event == 'pull_request' | ||
| # The token of a `workflow_run` workflow can update PRs from forks. | ||
| # Previously `pull_request_target` was used for the same reason, see | ||
| # https://github.com/apache/datafusion/issues/3743 | ||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
| steps: | ||
| # `workflow_run.pull_requests` is empty for pull requests from forks, so | ||
| # 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The PR lookup by head branch plus SHA is written twice: inline in |
||
| id: pr | ||
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | ||
| with: | ||
| script: | | ||
| const run = context.payload.workflow_run; | ||
| const { data } = await github.rest.pulls.list({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| state: 'open', | ||
| head: `${run.head_repository.owner.login}:${run.head_branch}`, | ||
| }); | ||
| const numbers = data | ||
| .filter((pr) => pr.head.sha === run.head_sha) | ||
| .map((pr) => String(pr.number)); | ||
| if (numbers.length) core.setOutput('numbers', numbers.join('\n')); | ||
| else core.info(`No open pull request for ${run.head_sha}`); | ||
|
|
||
| - name: Assign GitHub labels | ||
| if: | | ||
| github.event_name == 'pull_request_target' && | ||
| (github.event.action == 'opened' || | ||
| github.event.action == 'synchronize') | ||
| if: steps.pr.outputs.numbers != '' | ||
| uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0 | ||
| with: | ||
| repo-token: ${{ secrets.GITHUB_TOKEN }} | ||
| pr-number: ${{ steps.pr.outputs.numbers }} | ||
| configuration-path: .github/workflows/labeler/labeler-config.yml | ||
| sync-labels: true | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: See the NOTICE file distributed with this work for additional information regarding copyright ownership | ||
| # SPDX-FileContributor: Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements | ||
|
|
||
| # Signal for the workflows that need a write token on a pull request. | ||
| # | ||
| # ASF infra policy disables `pull_request_target` | ||
| # (https://infra.apache.org/github-actions-policy.html), and GitHub is adding a | ||
| # 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The design relies on GitHub sending |
||
| # for approval: | ||
| # - labeler.yml | ||
| # - fork_ci_status.yml | ||
| name: Pull request signal | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 |
||
| on: | ||
| pull_request: | ||
| types: [opened, reopened, synchronize] | ||
|
|
||
| permissions: {} | ||
|
|
||
| jobs: | ||
| signal: | ||
| name: Signal pull request update | ||
| runs-on: ubuntu-slim | ||
| steps: | ||
| - run: echo "This run triggers the workflow_run workflows that handle the pull request." | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,16 +11,15 @@ concurrency: | |
| group: ${{ github.repository }}-${{ github.head_ref || github.sha }}-${{ github.workflow }} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The concurrency group still uses |
||
| cancel-in-progress: true | ||
|
|
||
| # `pull_request_target` is disabled by ASF infra policy, so label on the push of | ||
| # the merge commit to `main` and look up the merged pull request from it. | ||
| on: | ||
| pull_request_target: | ||
| types: | ||
| - closed | ||
| push: | ||
| branches: | ||
| - main | ||
|
|
||
| jobs: | ||
| label: | ||
| if: github.event.pull_request.merged == true | ||
| runs-on: ubuntu-slim | ||
| permissions: | ||
| pull-requests: write | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,10 +27,29 @@ module.exports = async ({ github, context }) => { | |
| } | ||
| } | ||
|
|
||
| await github.rest.issues.addLabels({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.payload.pull_request.number, | ||
| labels: [target_version] | ||
| }); | ||
| // A squash merge or a merge queue commit maps to the merged pull request. | ||
| // The pull request can lag the push by a few seconds, so retry. | ||
| let pulls = []; | ||
| for (let attempt = 0; attempt < 3 && pulls.length === 0; attempt++) { | ||
| if (attempt > 0) { | ||
| await new Promise((resolve) => setTimeout(resolve, 5000)); | ||
| } | ||
| const { data } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| commit_sha: context.sha | ||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On push, the script looks up PRs only for |
||
| pulls = data.filter((pull) => pull.merged_at && pull.base.ref === 'main'); | ||
| } | ||
| if (pulls.length === 0) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The script now throws when a push to main has no merged PR, which turns ordinary direct pushes into failed workflow runs on main. |
||
| throw new Error(`No merged pull request found for ${context.sha}`); | ||
| } | ||
| for (const pull of pulls) { | ||
| await github.rest.issues.addLabels({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: pull.number, | ||
| labels: [target_version] | ||
| }); | ||
| } | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.