diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 00000000000..7a0ee982131 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,137 @@ +# 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. + +# Path rules for the pull request labeler (.github/workflows/label_prs.yml). +# A label is added when a changed file matches one of its globs; labels are never removed, +# so hand-applied labels stay. area:ci and documentation apply only when every changed file +# matches, since most PRs touch a workflow or a doc in passing. + +area:shuffle: + - changed-files: + - any-glob-to-any-file: + - 'native/shuffle/**' + - 'spark/src/main/**/shuffle/**' + - 'spark/src/main/scala/org/apache/comet/shuffle/**' + - 'common/src/main/**/*Shuffle*' + +area:scan: + - changed-files: + - any-glob-to-any-file: + - 'native/core/src/parquet/**' + - 'native/core/src/cloud/**' + - 'native/core/src/execution/operators/scan.rs' + - 'native/core/src/execution/operators/csv_scan.rs' + - 'native/core/src/execution/operators/iceberg_scan.rs' + - 'native/core/src/execution/operators/iceberg_common.rs' + - 'spark/src/main/**/parquet/**' + - 'common/src/main/**/parquet/**' + - 'spark/src/main/scala/org/apache/comet/rules/CometScanRule.scala' + - 'spark/src/main/scala/org/apache/comet/rules/CometScanContrib.scala' + - 'spark/src/main/scala/org/apache/spark/sql/comet/*Scan*.scala' + - 'spark/src/main/scala/org/apache/comet/objectstore/**' + - 'spark/src/main/scala/org/apache/comet/serde/operator/*Scan*.scala' + - 'contrib/**' + +area:writer: + - changed-files: + - any-glob-to-any-file: + - 'native/core/src/execution/operators/parquet_writer.rs' + - 'native/core/src/execution/operators/iceberg_write.rs' + - 'spark/src/main/scala/org/apache/spark/sql/comet/*Write*.scala' + - 'spark/src/main/scala/org/apache/spark/sql/comet/*Commit*.scala' + - 'spark/src/main/scala/org/apache/comet/serde/operator/*Writ*.scala' + - 'spark/src/main/scala/org/apache/comet/serde/operator/CometSink.scala' + - 'spark/src/main/scala/org/apache/comet/serde/operator/CometDataWritingCommand.scala' + - 'spark/src/main/**/*WriteFiles*' + +area:aggregation: + - changed-files: + - any-glob-to-any-file: + - 'native/spark-expr/src/agg_funcs/**' + - 'spark/src/main/scala/org/apache/comet/serde/aggregates.scala' + - 'spark/src/main/scala/org/apache/comet/serde/CometAggregateExpressionSerde.scala' + - 'native/core/src/execution/merge_as_partial.rs' + +area:expressions: + - changed-files: + - all-globs-to-any-file: + - 'native/spark-expr/src/**' + - '!native/spark-expr/src/agg_funcs/**' + - '!native/spark-expr/src/jvm_udf/**' + - changed-files: + - all-globs-to-any-file: + - 'spark/src/main/scala/org/apache/comet/serde/*.scala' + - '!spark/src/main/scala/org/apache/comet/serde/aggregates.scala' + - '!spark/src/main/scala/org/apache/comet/serde/CometAggregateExpressionSerde.scala' + - changed-files: + - any-glob-to-any-file: + - 'native/core/src/execution/expressions/**' + - 'spark/src/main/scala/org/apache/comet/expressions/**' + - 'spark/src/main/scala/org/apache/comet/codegen/**' + +area:joins: + - changed-files: + - any-glob-to-any-file: + - 'native/core/src/execution/operators/dynamic_filter.rs' + - 'native/core/src/execution/operators/dynamic_filter/**' + - 'spark/src/main/scala/org/apache/comet/rules/RewriteJoin.scala' + - 'spark/src/main/scala/org/apache/comet/rules/CometPlanAdaptiveDynamicPruningFilters.scala' + - 'spark/src/main/**/*Join*' + +area:ffi: + - changed-files: + - any-glob-to-any-file: + - 'native/jni-bridge/**' + - 'native/core/src/execution/columnar_to_row.rs' + - 'spark/src/main/**/vector/**' + - 'common/src/main/**/vector/**' + - 'spark/src/main/scala/org/apache/comet/NativeColumnarToRowConverter.scala' + - 'spark/src/main/scala/org/apache/spark/sql/comet/CometColumnarToRowExec.scala' + - 'spark/src/main/scala/org/apache/spark/sql/comet/CometNativeColumnarToRowExec.scala' + - 'spark/src/main/scala/org/apache/spark/sql/comet/CometSparkToColumnarExec.scala' + +area:memory: + - changed-files: + - any-glob-to-any-file: + - 'native/core/src/execution/memory_pools/**' + - 'spark/src/main/**/*MemoryManager*' + - 'spark/src/main/**/*MemoryAllocator*' + - 'common/src/main/**/*MemoryAllocator*' + - 'common/src/main/**/*OutOfMemory*' + +area:udf: + - changed-files: + - any-glob-to-any-file: + - 'native/spark-expr/src/jvm_udf/**' + - 'spark/src/main/scala/org/apache/comet/udf/**' + - 'spark/src/main/**/python/**' + - 'spark/src/main/**/*MapInBatch*' + - '.github/workflows/pyarrow_udf_test.yml' + +area:Iceberg: + - changed-files: + - any-glob-to-any-file: + - '**/*[Ii]ceberg*' + - '**/*[Ii]ceberg*/**' + +area:ci: + - changed-files: + - any-glob-to-all-files: '{.github/**,dev/ci/**,dev/release/**,dev/*.sh,dev/*.py,dev/*.xml,dev/*.toml}' + +documentation: + - changed-files: + - any-glob-to-all-files: '{docs/**,**/*.md}' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a07ce889dd5..e8eb9973117 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,6 +88,9 @@ jobs: - name: Check micro benchmark runner run: python3 dev/ci/check-benchmark-runner.py + - name: Check pull request type labeling + run: node --test dev/ci/pr-type-label.test.mjs + - name: Install actionlint run: | curl -sSfL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash | bash diff --git a/.github/workflows/label_prs.yml b/.github/workflows/label_prs.yml new file mode 100644 index 00000000000..7eaff2baf8f --- /dev/null +++ b/.github/workflows/label_prs.yml @@ -0,0 +1,87 @@ +# 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: Label pull requests + +# Runs on pull_request_target so it can label pull requests from forks. Nothing from the pull +# request is checked out or executed: the labeler reads the changed file list through the API +# and the title step reads the event payload. +on: + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +# One group per pull request with cancellation, so at most one run writes labels at a time: +# the labeler action reads the label set and writes it back whole, and two runs writing at +# once can drop a label the other just added. A cancelled or replaced run loses nothing +# because every step below is safe to repeat on the next event. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + label: + runs-on: ubuntu-slim + steps: + # Checks out the base branch, never the pull request's code: under pull_request_target the + # default ref is the base commit, and only dev/ci/pr-type-label.mjs is read from it. + - uses: actions/checkout@v7 + with: + persist-credentials: false + + # The type label follows the conventional commit prefix in the title, matching how + # maintainers label by hand. The decision reads the pull request's current labels, not the + # event payload, so a type label a maintainer applied before this run started stays and + # a run cut short by a newer push is completed by that push. Dependabot pull requests + # already carry their label. + - name: Label by title prefix + if: github.actor != 'dependabot[bot]' + uses: actions/github-script@v9 + with: + script: | + const { typeLabelsToAdd } = await import( + `${process.env.GITHUB_WORKSPACE}/dev/ci/pr-type-label.mjs` + ); + const { data: current } = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + per_page: 100, + }); + const labels = typeLabelsToAdd( + context.payload.pull_request.title, + current.map((label) => label.name), + ); + if (labels.length === 0) { + core.info('No type label to add'); + return; + } + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels, + }); + + - name: Label by changed paths + uses: actions/labeler@v7 + with: + configuration-path: .github/labeler.yml + sync-labels: false diff --git a/dev/ci/pr-type-label.mjs b/dev/ci/pr-type-label.mjs new file mode 100644 index 00000000000..96a5ceb8016 --- /dev/null +++ b/dev/ci/pr-type-label.mjs @@ -0,0 +1,53 @@ +// 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. + +// Type label for a pull request from its conventional commit prefix, mirroring how +// maintainers label by hand. Used by .github/workflows/label_prs.yml and tested by +// pr-type-label.test.mjs. + +export const LABELS_BY_PREFIX = { + feat: ['enhancement'], + refactor: ['enhancement'], + chore: ['enhancement'], + fix: ['bug'], + perf: ['enhancement', 'performance'], + test: ['enhancement', 'test'], + doc: ['documentation'], + docs: ['documentation'], + ci: ['enhancement', 'build'], + build: ['enhancement', 'build'], + deps: ['dependencies'], +}; + +const TYPE_LABELS = new Set(Object.values(LABELS_BY_PREFIX).flat()); + +const PREFIX = /^(feat|fix|perf|test|docs?|chore|refactor|ci|build|deps)(\([^)]*\))?!?:/i; + +// Labels to add for `title` given the pull request's current labels. Empty when the title has +// no conventional prefix or when any type label is already present, so a choice a maintainer +// has made stays as it is. +export function typeLabelsToAdd(title, currentLabels) { + const match = PREFIX.exec(title.trim()); + if (!match) { + return []; + } + const present = new Set(currentLabels); + if ([...present].some((label) => TYPE_LABELS.has(label))) { + return []; + } + return LABELS_BY_PREFIX[match[1].toLowerCase()].filter((label) => !present.has(label)); +} diff --git a/dev/ci/pr-type-label.test.mjs b/dev/ci/pr-type-label.test.mjs new file mode 100644 index 00000000000..784f1d76e06 --- /dev/null +++ b/dev/ci/pr-type-label.test.mjs @@ -0,0 +1,44 @@ +// 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. + +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { typeLabelsToAdd } from './pr-type-label.mjs'; + +test('adds the type label for a conventional title with no labels', () => { + assert.deepEqual(typeLabelsToAdd('fix: keep the entry alive', []), ['bug']); + assert.deepEqual(typeLabelsToAdd('perf(shuffle)!: reuse contexts', ['area:shuffle']), [ + 'enhancement', + 'performance', + ]); + assert.deepEqual(typeLabelsToAdd('chore(deps): bump actions', []), ['enhancement']); +}); + +test('leaves a type label a maintainer already applied, whatever the event carried', () => { + // The event payload showed no labels, but the pull request has one by the time the run starts. + assert.deepEqual(typeLabelsToAdd('fix: keep the entry alive', ['enhancement']), []); + assert.deepEqual(typeLabelsToAdd('perf: reuse contexts', ['performance']), []); +}); + +test('adds nothing for a title without a conventional prefix', () => { + assert.deepEqual(typeLabelsToAdd('Add Lance contrib build gate', []), []); + assert.deepEqual(typeLabelsToAdd('[draft] ci: pinned image', []), []); +}); + +test('does not repeat labels that are already present', () => { + assert.deepEqual(typeLabelsToAdd('test: cover struct columns', ['test']), []); +});