Skip to content

GitHub Actions and reports

Use nredd/pytest-airflow-in-a-box/action@v0 to create a reproducible Airflow test environment from Apache Airflow's published constraints. The action provisions the environment; your workflow still runs pytest and uploads its reports.

name: Airflow tests

on: [pull_request]

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: nredd/pytest-airflow-in-a-box/action@v0
        id: airflow-env
        with:
          airflow-version: "3.3.1"
          python-version: "3.13"
      - run: ${{ steps.airflow-env.outputs.python-path }} -m pytest

Always invoke pytest through python-path; the action does not add its virtual environment to PATH. It also does not run tests, cache packages, upload artifacts, or start Docker.

The action targets POSIX runners. Use GitHub-hosted Linux or macOS, or a self-hosted runner with Bash, curl, and network access to GitHub, PyPI, and astral.sh. Native Windows is not supported.

Across a version matrix

Put each exact Airflow and Python pair in the matrix and pass both values to the action:

jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        include:
          - airflow-version: "3.2.2"
            python-version: "3.12"
          - airflow-version: "3.3.1"
            python-version: "3.13"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: nredd/pytest-airflow-in-a-box/action@v0
        id: airflow-env
        with:
          airflow-version: ${{ matrix.airflow-version }}
          python-version: ${{ matrix.python-version }}
      - run: ${{ steps.airflow-env.outputs.python-path }} -m pytest

Choose pairs from Compatibility and certification. The action validates input syntax, not whether Airflow publishes constraints for the pair. An unsupported pair therefore fails while downloading constraints or resolving dependencies.

Inputs

Input Required Default What it does
airflow-version yes -- Installs an exact X.Y.Z release and selects its constraints-<version> branch.
python-version yes -- Provisions an X.Y interpreter and selects constraints-<python-version>.txt.
extra no airflow3 Selects the plugin's airflow3 or airflow2 extra. Other values fail validation.
plugin-version no latest on PyPI Pins pytest-airflow-in-a-box to an exact version.
uv-version no 0.12.2 Selects the uv release installed into RUNNER_TEMP.
working-directory no . Directory containing the virtual environment and resolving requirements-file. It must already exist.
requirements-file no (none) Installs additional operators, providers, and test dependencies after the constrained environment. The path is relative to working-directory.
report-dir no (none) Creates a report directory and configures pytest to write pytest.log and pytest.xml. Relative paths resolve from working-directory.

airflow-version, python-version, and extra are checked before installation. Invalid syntax produces a GitHub ::error:: annotation immediately.

Outputs

Output What it holds
python-path Absolute path to the environment's Python interpreter. Use it for pytest.
venv-path Absolute path to the virtual environment. Use it to reach installed console scripts.
report-dir Absolute report-directory path, or an empty string when report-dir was not set.

What gets installed

The action creates <working-directory>/.venv-airflow-in-a-box and downloads the constraints file for the requested Airflow and Python versions.

For extra: airflow3, one constrained transaction installs the plugin, the exact apache-airflow-core release, and apache-airflow-providers-sqlite>=4.1,<5. Installing core instead of the apache-airflow meta-package avoids the default provider set while retaining the SQLite provider required by the default metadata backend.

For extra: airflow2, Airflow is installed under its constraints first. The plugin and pytest>=8,<9 are then installed without those constraints because Airflow 2 constraints can pin pytest below the plugin's pytest 8 floor.

The optional requirements-file is installed last and unconstrained. Use it for your Dag repository's providers and test tools, but pin it deliberately: its requirements can replace versions chosen by the Airflow constraints.

The action's extra input accepts only an Airflow family. To run with xdist or the disposable Postgres backend, install their dependencies through requirements-file, then invoke pytest with -n auto --dist loadgroup or --airflow-db-backend=postgres. Postgres also requires an available Docker daemon. See Dependencies and extras.

Report artifacts

Set report-dir to append --airflow-report-dir=<absolute path> to PYTEST_ADDOPTS. Upload that directory in a separate always() step:

      - uses: nredd/pytest-airflow-in-a-box/action@v0
        id: airflow-env
        with:
          airflow-version: "3.3.1"
          python-version: "3.13"
          report-dir: ${{ github.workspace }}/reports
      - run: ${{ steps.airflow-env.outputs.python-path }} -m pytest --airflow-smoke
      - if: always()
        uses: actions/upload-artifact@v7
        with:
          name: airflow-test-reports
          path: ${{ github.workspace }}/reports
          if-no-files-found: warn

Use the same literal report path for upload. If provisioning fails, the action never sets its report-dir output; passing that empty output to upload-artifact can obscure the original failure.

The plugin writes pytest.log at DEBUG and pytest.xml in xunit2 format. Explicit --log-file, log-level, or --junit-xml settings take precedence. Under xdist, each worker writes a suffixed log such as pytest.gw0.log, while the controller writes JUnit XML. airflow_isolated children suffix both artifacts so they cannot overwrite the parent files. When COVERAGE_FILE is set, isolated children also receive suffixed coverage filenames unless pytest-cov is loaded and already owns them.

An older plugin-version may not support --airflow-report-dir. The action probes pytest --help; if the option is absent or the probe fails, it emits a warning and leaves PYTEST_ADDOPTS unchanged.

DEBUG logging can change caplog

Pytest implements --log-file-level through the session's root logger. Tests that assert exact caplog counts may capture more records with report generation enabled. Set an explicit --log-file-level or --log-level when the threshold is part of the test.

Migration runs

Use venv-path for the installed airflow-migration-diff script. Migration recording and comparison flags run through python-path like any other pytest invocation. See Running both families in CI.

Pinning

Use @v0 to follow the newest stable 0.x action release without crossing a major version. Use a full tag such as @v0.12.0 for an immutable release pin. Moving major tags advance only after a stable release is published; prereleases do not move them.

The action's inputs and outputs are a published interface and follow the plugin's major-version compatibility promise.