blob: 4052986d8baf3953930afe0cecdfa79fd30f7cd0 [file]
contributing-docs/05_pull_requests.rst
.. 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.
Pull Requests
=============
This document describes how you can create Pull Requests (PRs) and describes coding standards we use when
implementing them.
.. contents:: Table of Contents
:depth: 2
:local:
Protect your commit identity
----------------------------
Once a PR is merged, the commit author's (and any co-author's) name and email address become
**permanently public** as part of the commit history and **cannot be changed or removed afterward**.
GitHub provides a **built-in option to anonymize your commit email address**
(`Setting your commit email address <https://docs.github.com/en/account-and-profile/how-tos/email-preferences/setting-your-commit-email-address>`__)
by using a GitHub-provided email; however, **this setting applies only to future commits and cannot be
applied retrospectively**.
On computers used with **multiple GitHub accounts** (for example, shared or work computers), the local Git
configuration may be set to a different name or email than expected. This can result in unintended personal
or work-related details being permanently published. If you are using the Git CLI, you can verify your
current configuration by running the following commands in your terminal within your local Airflow repository:
.. code-block:: bash
git config --get user.name
git config --get user.email
To keep your email address private on GitHub, enable *Keep my email addresses private* in your GitHub
settings and configure git to use your GitHub-provided ``noreply`` address (typically
``<id>+<username>@users.noreply.github.com``) as ``user.email`` for future commits.
If you use the Git CLI, you can set the appropriate configuration by running the following commands:
.. code-block:: bash
# Replace <id> and <username> accordingly with your GitHub user ID and username (from the GitHub settings page)
git config user.email "<id>+<username>@users.noreply.github.com"
# Replace "Your Name" with your desired commit author name
git config user.name "Your Name"
Notes:
* GitHub's anonymization does **not** apply to co-author entries or to email addresses explicitly included
in the commit message.
* GitHub Desktop and other third-party Git clients manage author and email settings separately; verify
them using the tool's own configuration or preferences.
Before opening a pull request, please ensure you are comfortable with the author and co-author details that
will be publicly visible. Maintainers might point out unexpected personal information before merging,
but they are not obliged to do so.
Pull Request guidelines
-----------------------
Before you submit a Pull Request (PR) from your forked repo, check that it meets
these guidelines:
- Mind the **limit on open PRs**: contributors without write access can have at most 5 open PRs
at a time. See `Limit on open Pull Requests <32_open_pull_request_limit.rst>`__ for why, and what
to do when you reach it.
- Start with **Draft**: Until you are sure that your PR passes all the quality checks and tests, keep it
in **Draft** status. This will signal to maintainers that the PR is not yet ready
for review and it will prevent maintainers from accidentally merging it before
it's ready. Once you are sure that your PR is ready for review, you can mark it as
"Ready for review" in the GitHub UI. Our regular check will convert all PRs from
non-collaborators that do not pass our quality gates to Draft status, so if you see
that your PR is in Draft status and you haven't set it to Draft. Check the
comments to see what needs to be fixed.
- Include tests, either as doctests, unit tests, or both, to your pull request.
The Airflow repo uses `GitHub Actions <https://help.github.com/en/actions>`_ to
run the tests and `codecov <https://codecov.io/gh/apache/airflow>`_ to track
coverage. You can set up both for free on your fork. It will help you make sure you do not
break the build with your PR and that you help increase coverage.
Also we advise to install locally `prek hooks <08_static_code_checks.rst#prek-hooks>`_ to
apply various checks, code generation and formatting at the time you make a local commit - which
gives you near-immediate feedback on things you need to fix before you push your code to the PR, or in
many case it will even fix it for you locally so that you can add and commit it straight away.
- Follow our project's `Coding style and best practices`_. Usually we attempt to enforce the practices by
having appropriate prek hooks. There are checks amongst them that aren't currently enforced
programmatically (either because they are too hard or just not yet done).
- Maintainers will not merge a PR that regresses linting or does not pass CI tests (unless you have good
justification that it is a transient error or something that is being fixed in other PR).
- Maintainers will not merge PRs that have unresolved conversation.
- We prefer that you ``rebase`` your PR (and do it quite often) rather than merge. It leads to
easier reviews and cleaner changes where you know exactly what changes you've done. You can learn more
about rebase vs. merge workflow in `Rebase and merge your pull request <https://github.blog/2016-09-26-rebase-and-merge-pull-requests/>`_
and `Rebase your fork <http://stackoverflow.com/a/7244456/1110993>`_. Make sure to resolve all conflicts
during rebase.
- When merging PRs, Maintainer will use **Squash and Merge** which means then your PR will be merged as one
commit, regardless of the number of commits in your PR. During the review cycle, you can keep a commit
history for easier review, but if you need to, you can also squash all commits to reduce the
maintenance burden during rebase.
- Add an `Apache License <http://www.apache.org/legal/src-headers.html>`_ header to all new files. If you
have ``prek`` installed, prek will do it automatically for you. If you hesitate to install
prek for your local repository - for example because it takes a few seconds to commit your changes,
this one thing might be a good reason to convince anyone to install prek.
- If your PR adds functionality, make sure to update the docs as part of the same PR, not only
code and tests. Docstring is often sufficient. Make sure to follow the Sphinx compatible standards.
- Make sure your code fulfills all the
`static code checks <08_static_code_checks.rst#static-code-checks>`_ we have in our code. The easiest way
to make sure of that is - again - to install `prek hooks <08_static_code_checks.rst#prek-hooks>`_.
- Make sure your PR is small and focused on one change only - avoid adding unrelated changes, mixing
adding features and refactoring. Keeping to that rule will make it easier to review your PR and will make
it easier for release managers if they decide that your change should be cherry-picked to release it in a
bug-fix release of Airflow. If you want to add a new feature and refactor the code, it's better to split the
PR to several smaller PRs. It's also quite a good and common idea to keep a big ``Draft`` PR if you have
a bigger change that you want to make and then create smaller PRs from it that are easier to review and
merge and cherry-pick. It takes a long time (and a lot of attention and focus of a reviewer to review
big PRs so by splitting it to smaller PRs you actually speed up the review process and make it easier
for your change to be eventually merged.
- To learn more about ``cherry-pick`` of PRs, refer to the `Airflow 3 development documentation <../blob/main/dev/README_AIRFLOW3_DEV.md>`_.
- Run relevant tests locally before opening PR. Often tests are placed in the files that are corresponding
to the changed code (for example for ``airflow/cli/cli_parser.py`` changes you have tests in
``tests/cli/test_cli_parser.py``). However there are a number of cases where the tests that should run
are placed elsewhere - you can either run tests for the whole ``TEST_TYPE`` that is relevant (see
``breeze testing core-tests --help`` or ``breeze testing providers-tests --help`` output for
available test types for each of the testing commands) or you can run all tests, or eventually
you can push your code to PR and see results of the tests in the CI.
- You can use any supported python version to run the tests, but the best is to check
if it works for the oldest supported version (Python 3.11 currently). In rare cases
tests might fail with the oldest version when you use features that are available in newer Python
versions. For that purpose we have ``airflow.compat`` package where we keep back-ported
useful features from newer versions.
- Adhere to guidelines for commit messages described in this `article <https://cbea.ms/git-commit/>`_.
This makes the lives of those who come after you (and your future self) a lot easier.
.. _pull-request-quality-criteria:
Pull Request quality criteria
-----------------------------
Every open PR must meet the following minimum quality criteria before maintainers will review it.
PRs that do not meet these criteria may be automatically converted to **draft** status by project
tooling, with a comment explaining what needs to be fixed.
1. **Descriptive title** — The PR title must clearly describe the change.
Generic titles such as "Fix bug", "Update code", "Changes", single-word titles, or titles
that only reference an issue number (e.g. "Fixes #12345") do not meet this bar.
2. **Golden Rule** - in text descriptions - always use imperative mode, never use past
tense or conventional commits or any prefixes in the title take precious space and do not add any value, so they should be
avoided. For example, instead of "feat: add new feature" or "Added this new feature"
use "Add new feature". This is a common convention in many open source projects
and it makes the commit history more readable and consistent.
3. **Meaningful description** — The PR body must contain a meaningful description of *what* the
PR does and *why*. An empty body, a body consisting only of the PR template
checkboxes/headers with no added text, or a body that merely repeats the title is not
sufficient.
4. **Passing static checks** — The PR's static checks (pre-commit / ruff / mypy) must pass.
You can run them locally with ``prek run --from-ref main`` before pushing.
5. **Gen-AI disclosure** — If the PR was created with the assistance of generative AI tools,
the description must include a disclosure (see `Gen-AI Assisted contributions`_ below).
6. **Coherent changes** — The PR should contain related changes only. Completely unrelated
changes bundled together will be flagged.
**What happens when a PR is converted to draft?**
- The comment informs you what you need to do.
- Fix each issue, then mark the PR as "Ready for review" in the GitHub UI - but only after making sure that all the issues are fixed.
- There is no rush — take your time and work at your own pace. We appreciate your contribution and are happy to wait for updates.
- Maintainers will then proceed with a normal review.
Converting a PR to draft is **not** a rejection — it is an invitation to bring the PR up to
the project's standards so that maintainer review time is spent productively.
For details on how maintainers use tooling to triage and review PRs, see
`Maintainer PR Triage and Review <25_maintainer_pr_triage.md>`__.
**What happens when a PR is closed for quality violations?**
If a contributor has more than 3 open PRs that are flagged for quality issues, maintainers may
choose to **close** the PR instead of converting it to draft. Closed PRs receive the
``closed because of multiple quality violations`` label and a comment listing the violations.
Contributors are welcome to open a new PR that addresses the issues listed in the comment.
**What happens when suspicious changes are detected?**
When maintainers review a PR's diff before approving CI workflow runs and determine that it
contains suspicious changes (e.g. attempts to exfiltrate secrets, modify CI pipelines
maliciously, or inject harmful code), **all open PRs by the same author** will be closed
and labeled ``suspicious changes detected``. A comment is posted on each PR explaining that
the closure was triggered by suspicious changes found in the flagged PR.
**What happens when a PR is closed because of the open PR limit?**
When the `limit on open PRs <32_open_pull_request_limit.rst>`__ was introduced, maintainers ran a
one-time closure of the PRs of contributors who had more than 5 open PRs, except PRs where a
maintainer had already engaged. Those PRs are labeled ``closed because of open PR limit`` and can be
reopened once you have a free slot.
If a contributor believes any closure described above was applied in error, the appeal channel
is the PMC private list — see the
`Community escalation process <../COMMUNITY_ESCALATION.md>`_ for how to email
``private@airflow.apache.org``. That document also covers the further steps the project may
take when the same behaviour persists across PRs.
Gen-AI Assisted contributions
-----------------------------
Generally, it's fine to use Gen-AI tools to help you create Pull Requests for Apache Airflow as long as you
adhere to the following guidelines:
* Follow the `Apache Software Foundation (ASF) Generative Tooling guidelines <https://www.apache.org/legal/generative-tooling.html>`__.
* Ensure that you review and understand all code generated by Gen-AI tools before including it in your PR -
do not blindly trust the generated code.
* Make sure that the generated code adheres to the project's coding standards and best practices described
above
* Make sure to run all relevant static checks and tests locally to verify that the generated code works as
intended and does not introduce any issues.
* State in your PR description that you have used Gen-AI tools to assist in creating the PR.
* Be prepared to explain and justify the use of Gen-AI tools if asked by project maintainers or reviewers.
* Remember that the final responsibility for the code in your PR lies with you, regardless of whether
it was generated by a tool or written by you.
* By contributing code generated by Gen-AI tools, you agree to comply with the project's licensing terms
and any applicable laws and regulations.
* Blindly copy & pasting code from Gen-AI tools is detrimental as it might introduce security and
stability risks to the project. Maintainers that spot such behaviours will have no choice but to
close the related PRs. It adds extra burden on the maintainers and doesn't help the project.
The contributor reputation is also impacted as maintainers will lose confidence and might block
the user from making further contributions.
* Review your code to make sure it does not contain unrelated changes. Often Gen-AI tools
might introduce changes that are not related to the problem you are trying to solve. Such
unrelated changes should be removed from the PR before it is submitted for review. Relying on
maintainers to spot such unrelated changes is unfair and adds extra burden on them.
When a contributor does not follow these guidelines, maintainers may close the PR (or all of
that contributor's open PRs) with a comment pointing to this section of the documentation, so
that maintainer review time is not spent on unvetted Gen-AI-generated code. Repeated disregard
of the Gen-AI guidelines is treated the same way as any other sustained disruptive behaviour
toward the project: it is handled under the project-wide
`Community escalation process <../COMMUNITY_ESCALATION.md>`_, which describes the further
steps that may follow (PMC-level blocking, ASF-Infrastructure-level blocking, reporting the
account to GitHub) and how a contributor can appeal any such decision by emailing the PMC at
``private@airflow.apache.org``.
Requirement to resolve all conversations
----------------------------------------
We have decided to enable the requirement to resolve all the open conversations in a
PR in order to make it merge-able. You will see in the status of the PR that it needs to have all the
conversations resolved before it can be merged.
The goal is to make it easier to see when there are some conversations that are not
resolved for everyone involved in the PR - author, reviewers and maintainers who try to figure out if
the PR is ready to merge and - eventually - merge it. The goal is also to use conversations more as a "soft" way
to request changes and limit the use of ``Request changes`` status to only those cases when the maintainer
is sure that the PR should not be merged in the current state. This leads to faster review/merge
cycle and less problems with stalled PRs that have ``Request changes`` status but all the issues are
already solved.
.. _coding_style:
Coding style and best practices
-------------------------------
Most of our coding style rules are enforced programmatically by ruff and mypy, which are run automatically
with static checks and on every Pull Request (PR), but there are some rules that are not yet automated and
are more Airflow specific or semantic than style.
Don't Use Asserts Outside Tests
...............................
Our community agreed that to various reasons we do not use ``assert`` in production code of Apache Airflow.
For details check the relevant `mailing list thread <https://lists.apache.org/thread.html/bcf2d23fcd79e21b3aac9f32914e1bf656e05ffbcb8aa282af497a2d%40%3Cdev.airflow.apache.org%3E>`_.
In other words instead of doing:
.. code-block:: python
assert some_predicate()
you should do:
.. code-block:: python
if not some_predicate():
handle_the_case()
The one exception to this is if you need to make an assert for type checking (which should be almost a last resort) you can do this:
.. code-block:: python
if TYPE_CHECKING:
assert isinstance(x, MyClass)
Database Session Handling
.........................
**Explicit is better than implicit.** If a function accepts a ``session`` parameter it should not commit the
transaction itself. Session management is up to the caller.
To make this easier, there is the ``create_session`` helper:
.. code-block:: python
from sqlalchemy.orm import Session
from airflow.utils.session import create_session
def my_call(x, y, *, session: Session):
...
# You MUST not commit the session here.
with create_session() as session:
my_call(x, y, session=session)
.. warning::
**DO NOT** add a default to the ``session`` argument **unless** ``@provide_session`` is used.
If this function is designed to be called by "end-users" (i.e. Dag authors) then using the ``@provide_session`` wrapper is okay:
.. code-block:: python
from sqlalchemy.orm import Session
from airflow.utils.session import NEW_SESSION, provide_session
@provide_session
def my_method(arg, *, session: Session = NEW_SESSION):
...
# You SHOULD not commit the session here. The wrapper will take care of commit()/rollback() if exception
In both cases, the ``session`` argument is a `keyword-only argument`_. This is the most preferred form if
possible, although there are some exceptions in the code base where this cannot be used, due to backward
compatibility considerations. In most cases, ``session`` argument should be last in the argument list.
.. _`keyword-only argument`: https://www.python.org/dev/peps/pep-3102/
Don't use time() for duration calculations
..........................................
If you wish to compute the time difference between two events with in the same process, use
``time.monotonic()``, not ``time.time()`` nor ``timezone.utcnow()``.
If you are measuring duration for performance reasons, then ``time.perf_counter()`` should be used. (On many
platforms, this uses the same underlying clock mechanism as monotonic, but ``perf_counter`` is guaranteed to be
the highest accuracy clock on the system, monotonic is simply "guaranteed" to not go backwards.)
If you wish to time how long a block of code takes, use ``Stats.timer()`` -- either with a metric name, which
will be timed and submitted automatically:
.. code-block:: python
from airflow.sdk.observability.stats import Stats
...
with Stats.timer("my_timer_metric"):
...
or to time but not send a metric:
.. code-block:: python
from airflow.sdk.observability.stats import Stats
...
with Stats.timer() as timer:
...
log.info("Code took %.3f ms", timer.duration)
For full docs on ``timer()`` check out `shared/observability/src/airflow_shared/observability/metrics/base_stats_logger.py`_.
If the start_date of a duration calculation needs to be stored in a database, then this has to be done using
datetime objects. In all other cases, using datetime for duration calculation MUST be avoided as creating and
diffing datetime operations are (comparatively) slow.
Templated fields in Operator's __init__ method
..............................................
Airflow Operators might have some fields added to the list of ``template_fields``. Such fields should be
set in the constructor (``__init__`` method) of the operator and usually their values should
come from the ``__init__`` method arguments. The reason for that is that the templated fields
are evaluated at the time of the operator execution and when you pass arguments to the operator
in the Dag, the fields that are set on the class just before the ``execute`` method is called
are processed through templating engine and the fields values are set to the result of applying the
templating engine to the fields (in case the field is a structure such as dict or list, the templating
engine is applied to all the values of the structure).
That's why we expect two things in case of ``template fields``:
* with a few exceptions, only self.field = field should be happening in the operator's constructor
* validation of the fields should be done in the ``execute`` method, not in the constructor because in
the constructor, the field value might be a templated value, not the final value.
The exceptions are cases where we want to assign empty default value to a mutable field (list or dict)
or when we have a more complex structure which we want to convert into a different format (say dict or list)
but where we want to keep the original strings in the converted structure.
In such cases we can usually do something like this
.. code-block:: python
def __init__(self, *, my_field: list[str] = None, **kwargs):
super().__init__(**kwargs)
my_field = my_field or []
self.my_field = my_field
For such a mutable default the ``if my_field is None: my_field = []`` spelling is equally fine.
Checks that only ask **whether an argument was passed** are the other exception, and they belong in
the constructor — it is the only place that can answer the question. With
``render_template_as_native_obj=True`` a field that *was* passed can render to
``None``, so in ``execute`` a supplied argument is indistinguishable from a missing one. Write the
check as ``is None`` / ``is not None``, never as a truthiness test — a supplied argument can itself
be ``""``, ``0`` or an empty list:
.. code-block:: python
from airflow.utils.helpers import exactly_one
def __init__(self, *, command: str | None = None, powershell: str | None = None, **kwargs):
if not exactly_one(command is not None, powershell is not None):
raise ValueError("Must provide exactly one of 'command' or 'powershell'")
super().__init__(**kwargs)
self.command = command
self.powershell = powershell
``self.field is None`` works the same way, after the assignment. Anything that looks at the *value*
still has to move to ``execute``, and the operand has to be the field itself — indirection such as
``all(v is None for v in [a, b])`` is still flagged.
Converting an existing truthiness check is not neutral: a guard that raises when two arguments are
*both* set only gets stricter, but ``if not field: raise`` gets looser — ``""`` and ``0`` start
passing, and that half is a value check. ``not exactly_one(a, b)`` carries both, since it also
requires at least one.
Operators that support ``start_from_trigger`` are the third exception. The scheduler sends the task
straight to the triggerer and ``execute`` never runs; the triggerer renders the ``trigger_kwargs``
entries whose key is both an operator template field and an attribute of the trigger. Copying such
a field verbatim into ``start_trigger_args`` under its own name is therefore fine in the
constructor:
.. code-block:: python
def __init__(self, *, job: dict, start_from_trigger: bool = False, **kwargs):
super().__init__(**kwargs)
self.job = job
self.start_from_trigger = start_from_trigger
if start_from_trigger:
self.start_trigger_args = dataclasses.replace(
self.start_trigger_args, trigger_kwargs={"job": self.job}
)
A transformed value, a copy under a key that is not a template field of the operator, and a template
field passed as ``timeout`` or ``next_kwargs`` are never rendered and are still flagged. A copy under
the name of a *different* template field is rendered, under that key, but is flagged as well — the
entry no longer holds the field its key names.
The hook only verifies the operator half of that pair: it cannot tell whether the key is also an
attribute of the trigger class, so a passing hook is not confirmation that the kwarg will be rendered.
The reason for doing it is that we are working on a cleaning up our code to have
`prek hook <../scripts/ci/prek/validate_operators_init.py>`_
that will make sure all the cases where logic (such as validation and complex conversion)
is not done in the constructor are detected in PRs.
Don't raise AirflowException directly
..............................................
Our community has decided to stop adding new ``raise AirflowException`` and to adopt the following practices when an exception is necessary. For details check the relevant `mailing list thread <https://lists.apache.org/thread/t8bnhyqy77kq4fk7fj3fmjd5wo9kv6w0>`_.
1. In most cases, we should prioritize using Python's standard exceptions (e.g., ``ValueError``, ``TypeError``, ``OSError``)
instead of wrapping everything in ``AirflowException``.
2. Within ``airflow-core``, we should define and utilize more specific exception classes under ``airflow-core/src/airflow/exceptions.py``.
3. For provider-specific implementations, exceptions should be defined within ``providers/<provider>/src/airflow/providers/<provider>/exceptions.py``.
The use of points 2 and 3 should only be considered when point 1 is inappropriate, which should be a rare occurrence.
In other words instead of doing:
.. code-block:: python
if key not in conf:
raise AirflowException(f"Required key {key} is missing")
you should do:
.. code-block:: python
if key not in conf:
raise ValueError(f"Required key {key} is missing")
-----------
If you want to learn what are the options for your development environment, follow to the
`Development environments <06_development_environments.rst>`_ document.