diff --git a/.github/boring-cyborg.yml b/.github/boring-cyborg.yml new file mode 100644 index 000000000000..8789531ae077 --- /dev/null +++ b/.github/boring-cyborg.yml @@ -0,0 +1,77 @@ +# 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. + +labelPRBasedOnFilePath: + "Component: C++": + - "cpp/**/*" + + "Component: GLib": + - "c_glib/**/*" + + "Component: MATLAB": + - "matlab/**/*" + + "Component: Python": + - "python/**/*" + + "Component: R": + - "r/**/*" + + "Component: Ruby": + - "ruby/**/*" + + "Component: FlightRPC": + - "cpp/src/arrow/flight/**/*" + - "r/R/flight.*" + - "python/pyarrow/*flight.*" + + "Component: Gandiva": + - "c_glib/gandiva-glib/**/*" + - "cpp/src/gandiva/**/*" + - "python/pyarrow/gandiva.*" + - "ruby/red-gandiva/**/*" + + "Component: Parquet": + - "c_glib/parquet-glib/**/*" + - "cpp/src/parquet/**/*" + - "r/R/parquet.*" + - "ruby/red-parquet/**/*" + + "Component: Documentation": + - "docs/**/*" + - "**/*.md" + - "**/*.rst" + - "**/*.Rmd" + - "**/*.Rd" + +verifyTitles: + titleRegexp: '^(WIP:?\s*)?GH-[0-9]+|^MINOR: ' + alwaysUsePrTitle: true + validateEitherPrOrSingleCommitTitle: false + statusTitle: "PR Title" + successMessage: "PR title format is valid" + failureMessage: "Title must start with 'GH-: [Component]' or 'MINOR: [Component]'" + +insertIssueLinkInPrDescription: + descriptionIssuePlaceholderRegexp: '^\* GitHub Issue: (.*)$' + matchers: + githubIssue: + titleIssueIdRegexp: '^(?:WIP:?\s*)?GH-([0-9]+)' + descriptionIssueLink: '#${1}' + minor: + titleIssueIdRegexp: '^(MINOR): ' + descriptionIssueLink: 'N/A (MINOR)' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0187ca2dc907..dd7509e2605a 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -33,3 +33,5 @@ In accordance to the [AI generation guidelines](https://arrow.apache.org/docs/de - [ ] Human - [ ] AI - [ ] Not reviewed + +* GitHub Issue: (filled in automatically from the PR title) diff --git a/.github/workflows/dev_pr.yml b/.github/workflows/dev_pr.yml deleted file mode 100644 index 83a98540ed1f..000000000000 --- a/.github/workflows/dev_pr.yml +++ /dev/null @@ -1,107 +0,0 @@ -# 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: Dev PR - -on: - # TODO: Enable this when eps1lon/actions-label-merge-conflict is available. - # push: - # branches: - # - master - # - main - pull_request_target: - types: - - opened - - edited - - synchronize - -concurrency: - group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.number }} - cancel-in-progress: true - -permissions: - # Required for the GraphQL convertPullRequestToDraft mutation. - contents: write - pull-requests: write - issues: write - -jobs: - process: - name: Process - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - repository: apache/arrow - ref: main - persist-credentials: false - - - name: Add Issue link - if: | - (github.event.action == 'opened' || - github.event.action == 'edited') - uses: actions/github-script@v9 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const script = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/dev_pr/link.js`); - script({github, context}); - - - name: Check title - if: | - (github.event.action == 'opened' || - github.event.action == 'edited') - uses: actions/github-script@v9 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const script = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/dev_pr/title_check.js`); - script({github, context}); - - - name: Check Issue - if: | - (github.event.action == 'opened' || - github.event.action == 'edited') - uses: actions/github-script@v9 - with: - debug: true - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const script = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/dev_pr/issue_check.js`); - script({github, context}); - - - name: Assign GitHub labels - if: | - (github.event.action == 'opened' || - github.event.action == 'synchronize') - uses: actions/labeler@v7 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - configuration-path: .github/workflows/dev_pr/labeler.yml - sync-labels: true - - # TODO: Enable this when eps1lon/actions-label-merge-conflict is available. - # - name: Checks if PR needs rebase - # if: | - # github.event_name == 'push' || - # (github.event_name == 'pull_request_target' && - # (github.event.action == 'opened' || - # github.event.action == 'synchronize')) - # uses: eps1lon/actions-label-merge-conflict@releases/2.x - # with: - # dirtyLabel: "needs-rebase" - # repoToken: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/dev_pr/helpers.js b/.github/workflows/dev_pr/helpers.js deleted file mode 100644 index 222d2213771c..000000000000 --- a/.github/workflows/dev_pr/helpers.js +++ /dev/null @@ -1,66 +0,0 @@ -// 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. - -const https = require('https'); - -/** - * Given the title of a PullRequest return the Issue - * - * @param {String} title - * @returns {Issue} or null if no issue detected. - * - * @typedef {Object} Issue - * @property {string} kind - The kind of issue: minor or github - * @property {string} id - The numeric issue id of the issue - */ -function detectIssue(title) { - if (!title) { - return null; - } - if (title.startsWith("MINOR: ")) { - return {"kind": "minor"}; - } - const matched_gh = /^(WIP:?\s*)?GH-(\d+)/.exec(title); - if (matched_gh) { - return {"kind": "github", "id": matched_gh[2]}; - } - return null; -} - -/** - * Retrieves information about a GitHub issue. - * @param {String} issueID - * @returns {Object} the information about a GitHub issue. - */ - async function getGitHubInfo(github, context, issueID, pullRequestNumber) { - try { - const response = await github.rest.issues.get({ - issue_number: issueID, - owner: context.repo.owner, - repo: context.repo.repo, - }) - return response.data - } catch (error) { - console.log(`${error.name}: ${error.code}`); - return false - } -} - -module.exports = { - detectIssue, - getGitHubInfo -}; diff --git a/.github/workflows/dev_pr/issue_check.js b/.github/workflows/dev_pr/issue_check.js deleted file mode 100644 index 176349cdd051..000000000000 --- a/.github/workflows/dev_pr/issue_check.js +++ /dev/null @@ -1,84 +0,0 @@ -// 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. - -const helpers = require("./helpers.js"); - -/** - * Assigns the GitHub Issue to the PR creator. - * - * @param {Object} github - * @param {Object} context - * @param {String} pullRequestNumber - * @param {Object} issueInfo - */ -async function assignGitHubIssue(github, context, pullRequestNumber, issueInfo) { - await github.rest.issues.addAssignees({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueInfo.number, - assignees: context.payload.pull_request.user.login - }); - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pullRequestNumber, - body: ":warning: GitHub issue #" + issueInfo.number + " **has been automatically assigned in GitHub** to PR creator." - }); -} - -/** - * Performs checks on the GitHub Issue: - * - The issue is assigned to someone. If not assign it gets automatically - * assigned to the PR creator. - * - The issue contains any label. - * - * @param {Object} github - * @param {Object} context - * @param {String} pullRequestNumber - * @param {String} issueID - */ -async function verifyGitHubIssue(github, context, pullRequestNumber, issueID) { - const issueInfo = await helpers.getGitHubInfo(github, context, issueID, pullRequestNumber); - if (!issueInfo) { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pullRequestNumber, - body: ":x: GitHub issue #" + issueID + " could not be retrieved." - }) - } - if (!issueInfo.assignees.length) { - await assignGitHubIssue(github, context, pullRequestNumber, issueInfo); - } - if(!issueInfo.labels.filter((label) => label.name.startsWith("Component:")).length) { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pullRequestNumber, - body: ":warning: GitHub issue #" + issueID + " **has no components**, please add labels for components." - }) - } -} - -module.exports = async ({github, context}) => { - const pullRequestNumber = context.payload.number; - const title = context.payload.pull_request.title; - const issue = helpers.detectIssue(title) - if (issue && issue.kind === "github") { - await verifyGitHubIssue(github, context, pullRequestNumber, issue.id); - } -}; diff --git a/.github/workflows/dev_pr/labeler.yml b/.github/workflows/dev_pr/labeler.yml deleted file mode 100644 index 0950cacc2b9f..000000000000 --- a/.github/workflows/dev_pr/labeler.yml +++ /dev/null @@ -1,75 +0,0 @@ -# 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. - -"Component: C++": -- changed-files: - - any-glob-to-any-file: - - cpp/**/* - -"Component: GLib": -- changed-files: - - any-glob-to-any-file: - - c_glib/**/* - -"Component: MATLAB": -- changed-files: - - any-glob-to-any-file: - - matlab/**/* - -"Component: Python": -- changed-files: - - any-glob-to-any-file: - - python/**/* - -"Component: R": -- changed-files: - - any-glob-to-any-file: - - r/**/* - -"Component: Ruby": -- changed-files: - - any-glob-to-any-file: - - ruby/**/* - -"Component: FlightRPC": -- changed-files: - - any-glob-to-any-file: - - cpp/src/arrow/flight/**/* - - r/R/flight.* - - python/pyarrow/*flight.* - -"Component: Gandiva": -- changed-files: - - any-glob-to-any-file: - - c_glib/gandiva-glib/**/* - - cpp/src/gandiva/**/* - - python/pyarrow/gandiva.* - - ruby/red-gandiva/**/* - -"Component: Parquet": -- changed-files: - - any-glob-to-any-file: - - c_glib/parquet-glib/**/* - - cpp/src/parquet/**/* - - r/R/parquet.* - - ruby/red-parquet/**/* - -"Component: Documentation": -- changed-files: - - any-glob-to-any-file: - - docs/**/* - - "**/*.{md, rst, Rmd, Rd}" diff --git a/.github/workflows/dev_pr/link.js b/.github/workflows/dev_pr/link.js deleted file mode 100644 index 3f50a5b9b9d4..000000000000 --- a/.github/workflows/dev_pr/link.js +++ /dev/null @@ -1,83 +0,0 @@ -// 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. - -const helpers = require("./helpers.js"); - - -/** - * Checks whether message is present on Pull Request list of comments. - * - * @param {Object} github - * @param {Object} context - * @param {String} pullRequestNumber - * @param {String} message - * @returns {Boolean} true if message was found. - */ -async function haveComment(github, context, pullRequestNumber, message) { - const options = { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pullRequestNumber, - page: 1 - }; - while (true) { - const response = await github.rest.issues.listComments(options); - if (response.data.some(comment => comment.body === message)) { - return true; - } - if (!/;\s*rel="next"/.test(response.headers.link || "")) { - break; - } - options.page++; - } - return false; -} - -/** - * Adds a comment on the Pull Request linking the GitHub issue. - * - * @param {Object} github - * @param {Object} context - * @param {String} pullRequestNumber - String containing numeric id of PR - * @param {String} issueID - String containing numeric id of the github issue - */ -async function commentGitHubURL(github, context, pullRequestNumber, issueID) { - // Make the call to ensure issue exists before adding comment - const issueInfo = await helpers.getGitHubInfo(github, context, issueID, pullRequestNumber); - const message = "* GitHub Issue: #" + issueInfo.number - if (issueInfo) { - const body = context.payload.pull_request.body || ""; - if (body.includes(message)) { - return; - } - await github.rest.pulls.update({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: pullRequestNumber, - body: body + "\n" + message - }); - } -} - -module.exports = async ({github, context}) => { - const pullRequestNumber = context.payload.number; - const title = context.payload.pull_request.title; - const issue = helpers.detectIssue(title); - if (issue && issue.kind === "github") { - await commentGitHubURL(github, context, pullRequestNumber, issue.id); - } -}; diff --git a/.github/workflows/dev_pr/title_check.js b/.github/workflows/dev_pr/title_check.js deleted file mode 100644 index a986c8bc5f03..000000000000 --- a/.github/workflows/dev_pr/title_check.js +++ /dev/null @@ -1,78 +0,0 @@ -// 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. - -const fs = require("fs"); -const helpers = require("./helpers.js"); - -async function commentOpenGitHubIssue(github, context, pullRequestNumber) { - const {data: comments} = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pullRequestNumber, - per_page: 1 - }); - if (comments.length > 0) { - return; - } - const commentPath = ".github/workflows/dev_pr/title_check.md"; - const comment = fs.readFileSync(commentPath).toString(); - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pullRequestNumber, - body: comment - }); -} - -/** - * Converts the pull request to draft. - * - * @param {Object} github - * @param {Object} context - */ -async function convertToDraft(github, context) { - if (context.payload.pull_request.draft) { - return; - } - - await github.graphql( - ` - mutation ($pullRequestId: ID!) { - convertPullRequestToDraft( - input: {pullRequestId: $pullRequestId} - ) { - pullRequest { - id - } - } - } - `, - { - pullRequestId: context.payload.pull_request.node_id, - }, - ); -} - -module.exports = async ({github, context}) => { - const pullRequestNumber = context.payload.number; - const title = context.payload.pull_request.title; - const issue = helpers.detectIssue(title) - if (!issue) { - await commentOpenGitHubIssue(github, context, pullRequestNumber); - await convertToDraft(github, context); - } -}; diff --git a/.github/workflows/dev_pr/title_check.md b/.github/workflows/dev_pr/title_check.md deleted file mode 100644 index 1919e0cadb4a..000000000000 --- a/.github/workflows/dev_pr/title_check.md +++ /dev/null @@ -1,41 +0,0 @@ - - -Thanks for opening a pull request! - -**This pull request has been automatically converted to a draft because its title doesn't match Arrow's required format.** - -If this is not a [minor PR](https://github.com/apache/arrow/blob/main/CONTRIBUTING.md#Minor-Fixes), could you open an issue for this pull request on GitHub? https://github.com/apache/arrow/issues/new/choose - -Opening GitHub issues ahead of time contributes to the [Openness](http://theapacheway.com/open/#:~:text=Openness%20allows%20new%20users%20the,must%20happen%20in%20the%20open.) of the Apache Arrow project. - -Then could you also rename the pull request title in the following format? - - GH-${GITHUB_ISSUE_ID}: [${COMPONENT}] ${SUMMARY} - -or - - MINOR: [${COMPONENT}] ${SUMMARY} - -**After updating the title, you can mark the pull request as ready for review.** - -See also: - - * [Other pull requests](https://github.com/apache/arrow/pulls/) - * [Contribution Guidelines - Contributing Overview](https://arrow.apache.org/docs/developers/overview.html) diff --git a/docs/source/developers/continuous_integration/overview.rst b/docs/source/developers/continuous_integration/overview.rst index 71bc53ef59a7..4ae0e97297c4 100644 --- a/docs/source/developers/continuous_integration/overview.rst +++ b/docs/source/developers/continuous_integration/overview.rst @@ -53,7 +53,8 @@ The ``.yml`` files in ``.github/workflows`` are workflows which are run on GitHu - ``@github-actions crossbow submit ...`` - runs the specified Crossbow command - ``@github-actions rebase`` - rebases the PR onto the main branch - ``dev.yml`` - runs any time there is activity on a PR, or a PR is merged; it runs the linter and tests that the PR can be merged -- ``dev_pr.yml`` - runs any time a PR is opened or updated; checks the formatting of the PR title, adds assignee to the appropriate GitHub issue if needed (or adds a comment requesting the user to include the issue id in the title), and adds any relevant GitHub labels + +PR housekeeping (component labels, PR title validation and the GitHub issue link in the PR description) is handled by the `Boring Cyborg `_ GitHub App, configured in ``.github/boring-cyborg.yml``. Extended builds -----------------------