blob: 20f2eedf8083f01798343a5de675190ee37b1c4d [file] [view]
<!--
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.
-->
# Continuous Integration
Comet runs CI through GitHub Actions, and merges to `main` go through GitHub's merge queue. This
page is the contributor's view of both: what runs on a pull request, what runs later in the queue,
how to opt a pull request into a suite the PR tier skips, and what to do when a run fails. The
mechanics behind the configuration are documented in
[.github/workflows/README.md](https://github.com/apache/datafusion-comet/blob/main/.github/workflows/README.md).
## Three tiers
A single umbrella workflow, `.github/workflows/ci.yml`, orchestrates everything. It runs cheap
preflight checks first (license headers, Markdown formatting, workflow linting, the CI config
checks), computes which heavy jobs the changed files are relevant to, and fans out to those jobs.
Which jobs run also depends on the event:
| Suite | Pull request | Merge queue | Nightly |
| ------------------------------------------------- | ------------ | ----------- | ------- |
| Linux build, lint, Rust tests, TPC-H/TPC-DS | yes | yes | no |
| Comet test suites, Spark 4.1 | yes | yes | no |
| Spark SQL tests, Spark 4.1, catalyst and sql_core | with label | yes | no |
| Spark SQL tests, Spark 4.1, sql_hive | with label | yes | no |
| Iceberg Spark SQL tests, Iceberg 1.11 | with label | yes | no |
| macOS build and Comet test suites | with label | yes | no |
| Benchmark compile and lint check | with label | yes | no |
| Delta contrib build gate | with label | yes | no |
| PyArrow UDF tests, Spark 4.0 / 4.1 / 4.2 | with label | yes | no |
| Comet test suites, Spark 3.4 / 3.5 / 4.0 / 4.2 | with label | no | yes |
| Spark SQL tests, Spark 3.5 / 4.0 | with label | no | yes |
| Iceberg Spark SQL tests, Iceberg 1.8 / 1.9 / 1.10 | with label | no | yes |
| Spark SQL tests, Spark 3.4 | with label | no | no |
The **PR tier** is the fast feedback loop while a change is being iterated on. The **queue
tier** is the authoritative gate: everything the PR tier runs plus the suites that must pass before
a change lands, evaluated against the merge result rather than the pull request head. The queue
runs one Spark version and one Iceberg version, the ones the default build profile targets. The
**nightly tier** runs the other Spark and Iceberg versions once a day against `main` as it stands;
see [Nightly runs](#nightly-runs) below. Nothing in the queue tier runs again
on push to `main`, because the queue already tested the exact tree that landed. The one exception
is the Linux build, which also runs on push so that the dependency caches on `main` stay fresh: a
pull request can only restore caches saved on its own branch or on `main`, and the queue's
temporary branch takes its caches with it when it is deleted.
That push run is for the caches and nothing else, so it runs in **cache-refresh-only** mode: only
the four jobs that own a cache entry (the native CI build, the Rust tests, and the two TPC-H/TPC-DS
jobs, the last two stopping before their query passes), plus the short `Lint` job the native jobs
depend on. The lints and the Comet test matrix are skipped, which is the difference between 587
runner-minutes a push and about 73. If you add a job to `pr_build_linux.yml`, give it
`if: ${{ !inputs.cache-refresh-only }}` unless it writes a cache that `main` needs;
`dev/ci/check-ci-config.py` fails the build if you forget.
Spark 3.4 is the one suite in neither tier. [Spark 3.4 support is deprecated](../user-guide/latest/compatibility/spark-versions.md#spark-34),
so its Spark SQL suite no longer gates a merge. It remains available on demand: apply the
`run-spark-3.4-tests` label to run it against a pull request, or trigger `ci.yml` from the Actions
page with **Run workflow**, which runs every suite regardless of tier. A contributor touching Spark
3.4 code can still get a verdict before merging, but applying the label is only half of that: the
run it starts is advisory. Label the pull request and then push, and Spark 3.4 joins the required
verdict like any other suite. See
[Opting a pull request into a suite the PR tier skips](#opting-a-pull-request-into-a-suite-the-pr-tier-skips)
for the mechanics.
Every job's result feeds one flat job named `Required Checks`, and that is the only status check
`main` requires. A job that is skipped because the change did not touch its inputs counts as a
pass. A job that fails or is cancelled turns `Required Checks` red.
Which tier a job belongs to is the `POLICY` table in `dev/ci/compute-changes.py`. Each job's path
filters are the `FILTERS` table in the same file, and `dev/ci/check-ci-config.py` holds the test
cases that pin both down.
## Opting a pull request into a suite the PR tier skips
Each suite outside the PR tier has a label that runs it on a pull request:
| Label | Runs |
| -------------------------- | ----------------------------------------------------- |
| `run-macos-tests` | macOS build and Comet test suites |
| `run-all-spark-profiles` | Comet test suites against Spark 3.4 / 3.5 / 4.0 / 4.2 |
| `run-benchmark-check` | Benchmark compile and lint check |
| `run-delta-build-gate` | Delta contrib build gate |
| `run-pyarrow-udf-tests` | PyArrow UDF tests against Spark 4.0/4.1/4.2 |
| `run-spark-4.1-tests` | Spark SQL tests against Spark 4.1, every module |
| `run-spark-4.1-hive-tests` | Spark SQL tests against Spark 4.1, sql_hive only |
| `run-spark-3.4-tests` | Spark SQL tests against Spark 3.4 |
| `run-spark-3.5-tests` | Spark SQL tests against Spark 3.5 |
| `run-spark-4.0-tests` | Spark SQL tests against Spark 4.0 |
| `run-iceberg-tests` | Iceberg Spark SQL tests against every Iceberg version |
For a queue-tier suite the label only brings the run forward; the queue would have run it anyway
before the change landed. For a nightly-tier suite the label is what gets a verdict _before_ the
change lands: without it, the first run is the nightly after the merge. For Spark 3.4 the label is
the only way the suite runs at all.
Apply a label from the pull request sidebar, or from the command line:
```sh
gh pr edit <number> --add-label run-spark-3.5-tests
```
Applying a label starts a new run immediately at the pull request's current commit. That run
executes only the suite the label gates; the PR tier already ran at that commit and is not
repeated. Its aggregate verdict is published as `Required Checks (label run)` rather than
`Required Checks`, so it can be read alongside the commit run without replacing it.
For a queue-tier suite, that separate name costs nothing: the merge queue runs the suite again
before the change lands, so a failure a label run surfaced still blocks the merge later. A
nightly-tier suite and Spark 3.4 have no queue run behind them. A red `Required Checks (label run)`
leaves an earlier green `Required Checks` in place and the pull request mergeable, so treat a label
run as feedback to read, not as a gate. For one of those suites to count toward the required
verdict the label has to already be on the pull request when a commit is pushed, which is what the
next push gives you, since the label stays applied. A nightly-tier regression that does slip
through is caught by the next nightly run and filed as an issue; a Spark 3.4 one is not.
The label stays on the pull request, so every later push runs the suite as part of the normal PR
run. Remove the label once it has served its purpose. To re-run the suite at the same commit,
re-run the failed jobs from the Actions page, or remove and re-apply the label.
Use a label when a change is likely to behave differently on a version or platform the PR tier
does not cover. Some examples:
- a change to the serde, the planner, or a native operator, where the Comet test suites pass but
Spark's own SQL suite is the thing that would catch a behavior difference; `run-spark-4.1-tests`
runs it against the default profile
- code under `spark/src/main/spark-3.4/`, `spark-3.5/`, `spark-4.0/` or the shared `spark-3.x/`
directory, or any change to `CometExprShim` and friends; `run-all-spark-profiles` runs the Comet
test suites against every Spark version rather than 4.1 alone (the Lint Java matrix already
compiles the 3.4/3.5/4.0 profiles on every pull request, so this is for runtime differences)
- a change to a Spark SQL diff under `dev/diffs/`
- anything that touches Hive table support, `InsertIntoHiveTable`, or the `sql/hive` parts of
the 4.1 diff
- anything touching Iceberg reflection, the Iceberg scan or write path, or the Iceberg diffs
- native code with platform-specific behavior, or a dependency bump that changes what is
compiled on macOS
- a change to the benchmark sources under `spark/src/test/scala/org/apache/spark/sql/benchmark`
or to `native/*/benches`
Reach for `run-spark-3.4-tests` deliberately, since nothing else will run it: a change to
`spark/src/main/spark-3.4/`, to `dev/diffs/3.4.3.diff`, or to shared code whose Spark 3.4 behavior
you are unsure of. Without the label, a Spark 3.4 regression will not be caught before the change
lands.
Labels that gate nothing, such as `dependencies` or the type labels, also start a run. That run
executes nothing and finishes in a minute. It is a consequence of GitHub firing the `labeled`
event for every label; see the workflows README for why it is handled this way.
## Merging through the queue
Once a pull request is approved, a committer queues it with **Merge when ready** in the GitHub
UI, or from the command line:
```sh
gh pr merge <number> --squash --auto
```
The pull request's own checks do not have to be finished, though it is polite not to queue a pull
request whose PR tier is red.
GitHub then builds a temporary branch named `gh-readonly-queue/main/...` containing the pull
request's commits squashed on top of the current `main`, batched with up to four other queued pull
requests, and runs `ci.yml` against it with a `merge_group` event. When `Required Checks` on that
branch is green, every pull request in the batch merges. If it is red, GitHub removes the pull
request whose entry failed, rebuilds the remaining entries without it, and records the removal on
the pull request's timeline.
The queue tests the merge result rather than the pull request head. That is the point of it: a
semantic conflict between two pull requests that each pass in isolation is caught before either
lands. It also means a pull request can be evicted for a failure it did not cause on its own,
because `main` moved or because another entry in the batch broke the combined tree.
## When a queue run fails
A queue failure removes the pull request from the queue but does not otherwise change it. Work
through these in order:
1. **Find the run.** On the Actions page, filter by the `merge_group` event, or:
```sh
gh run list --event merge_group --limit 10
gh run view <run-id> --log-failed
```
The pull request's timeline links to the run as well.
2. **Decide whether it is infrastructure.** A job that dies in under a minute on an artifact
download, a Maven download, or a runner setup step is not a test failure. Re-run the failed
jobs and, if it passes, re-queue. Comet already retries the common cases, so if the same step
fails repeatedly, open an issue with the `area:ci` label.
3. **Reproduce it on the pull request.** Merge `main` into the branch so the pull request head
matches what the queue tested, then apply the label for the suite that failed. If the labeled
run passes, the failure came from the batch, not from this change, and re-queuing is the right
next step. If it fails, fix it on the branch like any other CI failure.
4. **Check for flakiness.** A test that is flaky in the queue tier blocks everyone's merges, not
just one pull request. If a queue failure looks like a flake, do not just re-queue: file or
update an issue naming the test, so it can be fixed or excluded.
A pull request evicted from the queue has to be queued again by hand. Merge when ready is not
re-armed automatically.
## Nightly runs
`ci.yml` also runs on a schedule, at 06:00 UTC every day, against what landed on `main` since the
last successful scheduled run. That run executes only the nightly tier: the Comet test suites against the Spark
profiles other than 4.1, the Spark SQL suites for Spark 3.5 and 4.0, and the Iceberg suites for
1.8, 1.9 and 1.10, each only when the day's changes touched files it covers. The queue already ran
everything else against the same tree, so nothing in the queue tier is repeated. A day with no
merges, or with only documentation changes, runs nothing.
If the previous run cannot be found — the first nightly, an unreachable Actions API, or a base that
is no longer on `main` — the run has nothing to diff against and runs the whole nightly tier
instead of guessing at a range.
A red nightly has no pull request to show up on, so the run opens an issue labelled
`ci-nightly-failure` that links the run and lists the jobs that failed. If one of those issues is
already open, the run comments on it instead, so a failure that persists across several nights
stays in one place. When you pick up a nightly failure:
1. **Find the commit.** The issue names the `main` commit the run tested, and the run's
`Detect changes` log names the base it diffed against, which is the commit the previous green
nightly tested. The regression is in that range.
2. **Reproduce it on a pull request.** Open the fix as a pull request and apply the label for
the suite that failed, so the same suite runs against the fix before it lands.
3. **Close the issue** once the failure is understood, whether it was fixed, excluded, or found to
be infrastructure. The next red nightly opens a new issue.
Dispatching `ci.yml` from the Actions page with **Run workflow** runs every tier, including the
nightly suites, if you need a result before the next scheduled run.
## Reproducing a suite failure locally
The Spark SQL suites outside the PR tier run Spark's own test suite against Comet, with the
version's diff from `dev/diffs/` applied. See [Spark SQL Tests](spark-sql-tests.md) for how to run
one locally, and [Iceberg Spark Tests](iceberg-spark-tests.md) for the Iceberg equivalents. For the
Comet test suites that run on macOS, `make test-jvm` on a Mac runs the same suites the workflow
does; the macOS job differs from Linux only in the platform.
## Changing CI itself
The umbrella workflow, the reusable workflows it calls, and the routing tables are checked by
`dev/ci/check-ci-config.py`, which runs in preflight. It enforces that every job feeds
`Required Checks`, that the required check name in `.asf.yaml` matches the job that publishes it,
that artifact names are unique per producer, that the routing policy matches its test cases, and
that every job in `pr_build_linux.yml` is either a cache writer or skipped on push. Run it locally
before pushing a CI change:
```sh
python3 dev/ci/check-ci-config.py
actionlint --shellcheck=off
```
A new test suite has to be registered in the workflow files by hand; see
[Register New Test Suites in CI](development.md#5-register-new-test-suites-in-ci). A new job in
`ci.yml` needs an entry in both `FILTERS` and `POLICY` in `dev/ci/compute-changes.py`, a case in
`dev/ci/check-ci-config.py`, and a line in `required_checks.needs`. The check will tell you which
of those is missing.
Moving a suite between tiers is a one-line change to `POLICY` plus the matching test case, but it
is a change to what every contributor sees on their pull requests, so raise it on the dev mailing
list first.