Releases: IKrysanov/airflow-pytest-operator
v0.5.0
0.5.0 - 2026-06-16
Headline: three complementary ways to re-run only the failed tests instead
of the whole suite, plus multiple test targets and a parser-owned report
location.
Added
PytestOperator(rerun_failed=N)-- in-process re-run of only the failed
tests. Runs the full suite once, then re-runs the still-failing tests (via
node_id_to_pytest_args) up toNmore times within the same
execute(), stopping early once none fail. Needs no.pytest_cache, no
XCom and notry_number, so it is robust on any executor and
deterministic. The XCom summary keeps the first run's counts and adds
rerun_rounds,recovered_node_idsandstill_failing_node_ids.
Ignored indry_run. Default0(unchanged behaviour).PytestOperator(test_retry_strategy="failed_only")-- Airflow-retry
driven re-run of only the previous attempt's failures, in a single task. The
failed node-ids are carried between attempts in an Airflow Variable keyed
by(dag_id, task_id, run_id, map_index)(not the task's own XCom, which
Airflow clears on retry; a Variable survives and works on Airflow 2.x/3.x).
map_indexis part of the key so the dynamically-mapped instances of one
task (.expand(...)) never clobber each other's failed set. The Variable
lifecycle is crash-safe: consumed on read (deleted before any test runs)
and (re)written only when a further retry will read it -- never on the
success/final attempt, and never whenfail_on_test_failure=False(the task
then succeeds, so no retry will ever read it) -- so a killed worker cannot
orphan it. Narrowing is driven by the stored set, nottry_number(a reused
run_idmay narrow earlier). Best-effort: falls back to the full suite if
the backend or the context ids are unavailable;pytest_argsare never
mutated; ignored indry_run. Default"all"(unchanged behaviour). If
the final-attempt status can't be determined from the task context, the
operator logs a warning to the task log (it then writes the set forward,
which could otherwise leave a Variable behind on what was really the last
attempt).LastFailedStore(Protocol),VariableLastFailedStoreand
last_failed_var_keyback thefailed_onlystore. Inject a custom backend
withPytestOperator(..., store=...)-- any object with
read(key)/write(key, ids)/delete(key)satisfies the structural
protocol (validated at init). The Variable class is resolved through the
compat shim, so importing the package stays Airflow-free.node_id_to_pytest_args(node_ids, *, class_prefix="Test")-- converts the
dottedfailed_node_ids(from XCom) back into pytest CLI selectors, for the
two-taskrun_all -> run_failedDAG pattern (documented in the README).
Idempotent; leaves malformed/slash-form input untouched.- Multiple test targets:
PytestOperator(test_path=...)and
PytestRunner.runnow accept a single string or a sequence of strings,
all passed to pytest as positional selectors. With no explicitcwd, the
working directory is derived as the closest shared parent of the targets. - Parser-owned report location: parsers accept
report_dir, e.g.
JUnitResultParser(report_dir="/opt/airflow/artifacts")(also
JSONResultParser). The location travels with the parser, so it applies to
any runner. When unset, the runner writes to a temp dir it cleans up. - On a pytest timeout, the raised
TestExecutionErrornow carries the
capturedstdout/stderras attributes (not just in the worker log). - Task-log lines for the report directory, run outcome, and cleanup /
cancellation decisions, so the report location and lifecycle are visible.
Changed
- Breaking (pre-1.0):
SubprocessPytestRunnerno longer takes a
report_dirargument -- set the location on the parser instead
(JUnitResultParser(report_dir=X)). The runner owns only the temp-dir
fallback and itscleanuppolicy. - Empty / whitespace-only test targets and pytest args (e.g. a Jinja
expression that rendered to"") are dropped with a warning. A run with no
usable target fails fast; an empty arg list is fine. - Constructor validation follows the Python convention: a wrong type raises
TypeError(rerun_failednot an int,storenot aLastFailedStore),
a valid type with a wrong value raisesValueError(unknown
test_retry_strategy, negativererun_failed).
Fixed
- Relative targets and a relative parser
report_dirnow resolve correctly
under the runner's derived cwd (previously a relative target could
double-join,"tests"->"tests/tests", and a relative report path went
missing). Targets and report paths are now absolutised. - The working directory is now derived from node-id selectors too, by
anchoring on their path portion (tests/test_x.py::test_a->tests/).
Previously any::target fell back to the worker's inherited cwd, so a
failed_onlyretry -- whose targets are all node-ids -- ran from the wrong
directory and relativeaddopts(e.g. Allure's--alluredir) broke on
every retry. The path portion is absolutised in lock-step, so pytest never
double-joins. cleanup()is idempotent: the operator cleans up twice on a kill (from
execute()andon_kill); it no longer logs the decision twice._resolve_cwdfalls back gracefully (with a warning) when targets share no
common anchor (e.g. different Windows drives) instead of raising.- JSON parser edge cases: a skip raised from a fixture finalizer
(pytest.skip()in teardown) keeps its reason; the pluralerrorssummary
key is counted (not only the singularerror); and parametrized node ids
whose value contains::(e.g.test_param[a::b]) are split correctly. - Process-tree termination no longer leaks an
OSError/PermissionError
(child changed gid, or a cancel/timeout race) — it falls back to killing the
direct child. The auto-created temp report dir is also removed if the parser's
report_requestcallback raises, andreport_dirownership now resolves
symlinks so a symlinked path isn't mistaken for outside the runner's temp dir. SubprocessPytestRunnervalidates itstimeout(must be positive) and
grace_period(must be non-negative) at construction instead of failing
obscurely later.
v0.4.2
0.4.2 - 2026-06-06
Added
PytestOperator(dry_run=True)-- new optional argument that switches
the run to pytest's--collect-onlymode. Test bodies do NOT
execute; pytest only collects them (imports modules, runs collection-
time fixtures, walks the test tree). Intended as a pre-flight task in
a DAG: verifies the test path resolves on the worker, that imports
succeed (so the worker has all required deps installed), and that
collection itself succeeds (no syntax errors, no missing fixtures).
Collection errors surface the same way normal test failures do --
the task fails withTestsFailedErrorunder the default
fail_on_test_failure=True. The user'spytest_argsare not
mutated; the--collect-onlyflag is appended to a per-call
effective list atexecute()time, so retries see the original
configuration and downstream introspection of the operator is honest.
Defaultdry_run=False-- behaviour unchanged for existing tasks.JSONResultParsernow reportsTestRunResult.totalfrom
summary.collectedwhensummary.totalis 0 and no per-case
entries were parsed. This is exactly the shape pytest-json-report
writes in--collect-onlymode (zero "ran" tests, N collected).
Normal runs are unaffected -- the fallback only kicks in when there
is no other signal. The JUnit XML for--collect-onlycontains
no<testcase>entries at all (<testsuite tests="0">), so the
JUnit parser cannot report a count for dry-run; the operator
docstring notes this limitation.
v0.4.1
0.4.1 - 2026-06-06
Fixed
TestRunResult.casesis now atuple[CaseResult, ...]instead of a
list, so thefrozen=Trueclaim on the dataclass is honest.
Before this change,frozen=Trueonly blocked attribute
reassignment (result.cases = [...]); the list itself remained
mutable soresult.cases.append(...)silently modified a
"frozen" instance. A__post_init__coerces any iterable the
caller passes (list, generator, etc.) into a tuple, so the existing
parsers that accumulate cases via.appendon a local list and
pass it through continue to work without change. Breaking only
for external code that depended on the mutability ofresult.cases
-- which was the bug we're fixing.JSONResultParserandJUnitResultParsernow produce identical
failed_node_idsfor the same suite. The JSON parser previously
emitted pytest's native form ("tests/test_x.py::test_y") while
the JUnit parser emitted the dotted JUnit-XML form
("tests.test_x::test_y"), making downstream consumers (Airflow
branches reading XCom, alerting that diffs the list across runs)
silently parser-dependent. The JSON parser is now normalised to the
same dotted form as JUnit. Breaking for any consumer that pinned
on the slash form coming out ofJSONResultParsersince 0.4.0 --
they get the new format starting with this release. The conversion
direction was chosen for information-preservation: from the slash
path with.pywe can always derive the dotted module, but the
reverse from JUnit's classname alone is ambiguous (module.Class
vsmodule.subname).JUnitResultParser.parseno longer catchesExceptionfrom the
underlying XML parser. The handler now lists exactly the exception
types that a JUnit parse can legitimately fail with:
xml.etree.ElementTree.ParseError(malformed XML),
ValueError(covers everydefusedxmlsecurity exception via
DefusedXmlException), andOSError(file became unreadable
between ourexists()check and the actual read). Anything else
--MemoryError,AttributeErrorfrom a bug in our code,
RecursionError-- now escapes the parser uncaught so the worker
logs the real problem instead of seeing a misleading
"Failed to parse JUnit report" message.JSONResultParsernow reports per-casetimeas the sum of the
setup+call+teardownphase durations from
pytest-json-report, instead of reading only thecallphase.
Previously, a case that errored duringsetup(e.g. a fixture
raised) had nocallsection in the JSON document and ended up
withtime=0.0-- making per-case timings misleading exactly for
the failures users most want to investigate. The new behaviour
matches what pytest's own JUnit XML writer reports as the case
timeand so restores parity with :class:JUnitResultParser.
Malformed durations on individual phases are still tolerated: that
phase contributes0and the others are summed as usual.
Changed
SubprocessPytestRunnerdrainer threads now count the per-stream
output cap vialen(chunk)(character count) instead of
len(chunk.encode("utf-8", errors="replace")). The previous
implementation allocated a throwawaybytesobject on every
readline()-- tens of thousands of allocations on a verbose
suite -- just to get a precise byte count.len(chunk)is an
O(1) cached lookup onstr. The trade-off is that the cap is now
an approximate byte count: exact for ASCII output (which is what
pytest emits in practically all cases), under-counting bytes by up
to 4x for UTF-8 multi-byte content. The cap parameter is still
namedmax_output_bytesfor back-compat; the docstring
documents the approximation. Microbench on a 10k-line ASCII/Cyrillic
mix: 41ms -> 14ms per 50 iterations (~3x speedup) with a 1.002x
under-count ratio.TestRunResult.to_xcomno longer goes throughdataclasses.asdict.
The previous implementation recursively converted every nested
:class:CaseResultinto a dict tree before discarding the whole
casesentry; for suites with thousands of tests that's a real
amount of CPU and short-lived garbage on the worker. The new path
builds the payload field-by-field, ~30x faster on a 5k-case suite
(~227ms -> ~7ms in a microbench; bigger speedup on larger suites
due to the per-case dataclass-to-dict overhead). The wire format is
unchanged. A new structural test pins the set of XCom keys against
the :class:TestRunResultschema so any future field addition is a
conscious choice rather than a silent omission.
v0.4.0
0.4.0 - 2026-06-04
Breaking changes
This release removes the runner's hardcoded knowledge of the JUnit format.
Parsers now declare which pytest CLI flags they need and where their report
will land; the runner just splices those args verbatim and reports back the
declared path. No deprecation aliases are provided -- breakage is intentional
and loud, because the silent-fallback alternative (a JSONResultParser that
secretly gets a JUnit XML file) is much harder to diagnose than a TypeError
at startup.
Migration matrix (was -> is):
RunArtifacts.junit_xml_path->RunArtifacts.report_path.ResultParser-- subclasses now MUST implementreport_request(report_dir)
in addition toparse(...). A class that overrides onlyparsewill raise
TypeErrorat instantiation. The new method returns aReportRequestwith
the CLI flags and the report path the parser wants pytest to produce.PytestRunner.run(...)-- new required keyword-only argument
report_request: Callable[[str], ReportRequest]. Operators pass
parser.report_request; custom runners receive the callback and must call
it on the prepared report directory before launching pytest.SubprocessPytestRunnerno longer adds--junitxml=...or
-o junit_logging=allof its own accord. Those flags live in
JUnitResultParser.report_requestnow.TestExecutionErrorraised on a missing report previously read
"pytest produced no JUnit report"; it now names the configured parser
class ("pytest produced no report for JUnitResultParser (exit code N)",
"pytest produced no report for JSONResultParser ...", ...). Tests
asserting against the old wording must update the match string.
Added
ReportRequestdataclass inairflow_pytest_operator.models(also
re-exported from the package root). Frozen, withpytest_args: tuple[str, ...]andreport_path: str | None.report_path=None
documents "no report file expected"; the type is kept permissive so a
future format that produces no file needs no model change.JSONResultParserinairflow_pytest_operator.reporters.json_parser,
parsing output produced by thepytest-json-reportplugin. Same
contract asJUnitResultParser: counts, durations, per-case results,
andfailed_node_ids. Available from the package root.[json-report]extra wiringpytest-json-report>=1.5. Install on
workers configured to use the JSON parser:
pip install airflow-pytest-operator[json-report]
The parser itself has no runtime dependency on the plugin -- it just
parses whatever JSON it is handed -- so this extra only needs to be
on the side where pytest runs.- The
[dev]extra now also pulls inpytest-json-report, so
tests/test_json_parser.pyruns as part of the normal test suite. - The
TestExecutionErrorraised when pytest writes no report truncates
very long captured stderr at 4096 chars, keeping Airflow task logs and
XCom payloads bounded. (The parser-class naming in that same message is
covered under Breaking changes above.) SubprocessPytestRunnergained amax_output_bytesconstructor
parameter (default 10 MiB) that caps capturedstdout/stderrper
stream. A pytest run that writes unbounded output to a pipe (e.g.
-swith a chatty or looping test) could otherwise grow the in-memory
capture without limit and bloat the Airflow task log / XCom payload.
Once a stream reaches the cap, further chunks from it are dropped and
the captured text is suffixed with a one-line marker
(...(stdout truncated at N bytes; ...)); the underlying pipe keeps
being drained so the child never blocks on a full OS buffer. Pass
Noneto restore unbounded capture; a non-positive value raises
ValueError.- Two new worker-oriented extras:
[pytest](pytest>=7.0) and
[pytest-allure](pytest>=7.0, allure-pytest>=2.13). These let workers
pull in pytest (and optionally the Allure plugin) as part of a single
pip install airflow-pytest-operator[pytest]command without manually
tracking a separate requirement. The[dev]extra is unchanged and
continues to includepytestalongside the development toolchain.
Changed
SubprocessPytestRunneris now format-agnostic. It receives a
report_requestcallback from the operator, invokes it on the prepared
report directory, splices the returned CLI args into the pytest command,
and returns the declared report path inRunArtifacts. Adding a new
report format is now strictly a matter of writing a new parser; the
runner needs no changes. (This closes the gap between the OCP claim in
the README and what the code actually allowed.)SubprocessPytestRunnerno longer collects stdout/stderr via
communicate(). Two background threads drain each pipe from the moment
Popen returns, accumulating chunks until EOF. The main thread waits via
proc.wait(timeout=...)and then joins the drainers with a bounded
timeout. This removes a documented race: previously, the post-timeout
tail was collected by a secondcommunicate()call, which CPython
documents as best-effort and which races SIGKILL against the kernel's
pipe-flush -- on a saturated pipe the tail could come back empty even
when bytes were waiting in the buffer. The new design captures every
byte the child wrote before the kill, plus also covers the cancel()
path that previously dropped the tail entirely.JSONResultParsernow treats unknownoutcomevalues as"skipped"
instead of"error". The previous default would flip a clean run to
failed if a future pytest-json-report version introduced a new state
(e.g."deselected","warned") and raiseTestsFailedErroron a
suite that actually passed."skipped"is non-fatal and honest: we did
not classify the case as a real pass or failure. To prevent silent
drift, the parser logs a singleWARNINGper report listing every
unknown outcome it encountered, so schema changes still show up in
worker logs rather than being papered over forever.JSONResultParserhardening pass:- Non-list
testsfield now raisesReportParseErrorinstead of
crashing withTypeErrorfrom afor-loop. Callers can catch a
single exception type for "report is malformed". - Non-numeric values in
summarycounters are still coerced to 0 (to
keep the parse going), but trigger a singleWARNINGper report
listing every offending key. Silent structural errors no longer
produce misleading zero counts. - Skipped-case message extraction now returns just the reason string
instead of the repr of the(filename, lineno, 'Skipped: reason')
tuple pytest-json-report stores inlongrepr. Falls back to the raw
text on schema drift. Usesast.literal_evalso report content is
never executed. - Per-parser docstrings now document that the report filename is fixed
and that reusing the samereport_diroverwrites prior reports --
callers needing history retention must give the runner a fresh dir
per run (the default temp-dir behavior already does this).
- Non-list
- DCO check now skips automated bot commits (Dependabot, github-actions,
etc.), identified by their…[bot]@users.noreply.github.comauthor
email. Bots cannot rungit commit -s, and their provenance comes from
GitHub's bot identity rather than a DCO sign-off, so requiring one only
blocked dependency-update PRs. - CI and CodeQL workflows now trigger on
pull_requestonly (plus the
weekly schedule for CodeQL), not onpush: main. Under branch protection
every change reachesmainthrough a PR, so the PR run is the
authoritative gate; the post-mergepushrun re-tested identical code
and roughly doubled CI usage per change. Addedconcurrencygroups with
cancel-in-progressto CI, CodeQL, and DCO so superseded runs on the
same ref are cancelled rather than left to finish.
v0.3.1
0.3.1 - 2026-05-31
Security
- Completed SHA-pinning of GitHub Actions across all workflows. 0.3.0
pinnedrelease.ymlandtestpypi.yml; this release also pinsci.yml
(actions/checkout,actions/setup-python,codecov/codecov-action)
anddco.yml(actions/checkout). Closes the remaining Pinned-
Dependencies findings from OpenSSF Scorecard for GitHub-owned and
third-party actions. - Added a CodeQL static-analysis workflow (
.github/workflows/codeql.yml)
running on push, pull request, and a weekly schedule, publishing results
to the Security tab. Satisfies the Scorecard SAST check. - Added a Dependabot configuration (
.github/dependabot.yml) that keeps
GitHub Actions (SHA pins plus their version comments) and Python dev
dependencies current, so upstream security fixes surface as pull requests
rather than sitting behind frozen hashes. Satisfies the Scorecard
Dependency-Update-Tool check. release.ymlnow also signs and attaches the distributions to the
GitHub Release. A singlebuildjob produces the artifacts; both the
PyPI publish job and a newattest-and-attachjob consume the SAME
distartifact, so the*.intoto.jsonlSigstore attestation attached
to the GitHub Release is a real attestation of the bytes uploaded to
PyPI -- not a separate rebuild made just to satisfy supply-chain
scanners.
Documentation
- README: new "Passing values from upstream tasks into your tests" section
documenting how to forward XCom values into a test run via per-value
templatedenv(the template goes inside each dict value, not around the
wholeenv), with aDataIngester→parametrizeend-to-end example and
a note onrender_template_as_native_obj.
v0.3.0
0.3.0 - 2026-05-30
Added
- Coverage measurement wired into the project:
pytest-covis now a dev
dependency and[tool.coverage]configuration lives inpyproject.toml
(branch coverage on,airflow_pytest_operatoras the source). Coverage is
opt-in viapytest --cov=airflow_pytest_operatorso the integration CI
job, which installs a bare pytest, is unaffected. - Codecov integration in CI: the unit job uploads a coverage report
(coverage.xml) on Python 3.12 viacodecov/codecov-action, and the
README now carries a coverage badge. - The CI integration matrix now also runs against Airflow 3.2.1 (py3.12),
the release where the 0.2.1 provider-discovery startup crash first
appeared, guarding the lazy-import fix against regression. - Substantial test additions bringing measured coverage to ~99.5% on the
test stub and on real Airflow 2.10.3, 3.0.6, and 3.2.1. The single
uncovered line is arun()/cancel()race-guard with no deterministic
test, left uncovered by design rather than chased with a flaky timing
test (afail_under = 85gate guards against real regressions):tests/test_models.py— node-id reconstruction (with and without a
classname),success/failed_node_idsderivation, and the XCom
projection that drops per-case detail.tests/test_base_interfaces.py— the abstractPytestRunner/
ResultParserdefaults (no-opcancel/cleanup, abstract-method
contracts), proving Liskov-substitutability of minimal implementations.tests/test_compat.py— everyBaseOperatorresolution branch of the
compatibility shim, driven deterministically by injecting fake modules
intosys.modules, plusget_airflow_versionparsing and the
apply_defaultspassthrough.- Operator logging of child stdout/stderr and failed node ids, asserted
by spying on the logger's methods rather thancaplog(robust across
Airflow versions, which route task logging differently). SubprocessPytestRunneredge paths:_terminatewhen the process is
already dead or disappears mid-signal (ProcessLookupErroron SIGTERM
and SIGKILL) and the cleanup race-guard that prevents a doublermtree.- A malformed
timeattribute in a JUnit report now has an explicit test
confirming it degrades to0.0rather than failing the parse.
Changed
- License headers on all source files now use a collective copyright
("the airflow-pytest-operator contributors") instead of an individual
name, so contributors never have to edit copyright lines per file. A
NOTICEfile records project-level authorship, and each contributor
retains copyright over their own contributions. - OS-specific Windows-only branches in
SubprocessPytestRunnerare marked
# pragma: no cover; they cannot execute on the Linux CI runners and are
excluded from the coverage measurement rather than left as phantom gaps.
Security
- A
SECURITY.mdsecurity policy documents supported versions, the
preferred reporting channel (GitHub Private Vulnerability Reporting),
response-time expectations (acknowledgement within 72 hours, initial
assessment within 7 days, 90-day coordinated disclosure), out-of-scope
cases, and hardening recommendations for users (including the
[secure-xml]extra and how to verify release attestations). Closes the
Security-Policy criterion in supply-chain audits such as OpenSSF
Scorecard. - A weekly OpenSSF Scorecard analysis (
.github/workflows/scorecard.yml)
scans the repository for supply-chain best practices and publishes its
result as a signed SARIF report to the GitHub Security tab and to the
public Scorecard API. The score badge is linked from the README so
consumers can see the project's supply-chain posture at a glance. - Release and TestPyPI workflows now pin every third-party GitHub Action by
immutable commit SHA (with a trailing comment naming the version) rather
than by floating tag, so a compromise of an upstream action repository
cannot silently substitute new code into the workflow that holds our
PyPI OIDC token. - Release artifacts ship with PEP 740
Sigstore attestations, produced automatically by
pypa/gh-action-pypi-publishv1.10+ for any Trusted-Publishing release.
PyPI verifies them at upload and surfaces the source repository in the
release's Verified details. README documents how end users can verify
individual artifacts against this repository withpypi-attestations.
Contributor experience
CONTRIBUTING.mdnow documents the license header to copy into new files,
the project's GitHub Flow branching model (PRs targetmain; there is no
developbranch), a Developer Certificate of Origin (DCO) sign-off
workflow, and a maintainer review/merge checklist.- Added a
DCOGitHub Actions workflow that verifies every pull-request
commit carries aSigned-off-bytrailer.
v0.2.1
0.2.1 - 2026-05-24
Fixed
- Prevent an Airflow worker startup crash on Airflow 3.2.x. Provider
discovery imports this package during Airflow's own config
initialization, before the Task SDK is ready; eagerly importing the
operator (and thusairflow.sdk.bases.operator) at that point raised
ImportError: cannot import name 'conf' from 'airflow.sdk.configuration'
and aborted startup. Two changes break the chain: the provider-discovery
entry point now lives in an import-light module
(airflow_pytest_operator.provider_info), andPytestOperatoris
exposed lazily via module__getattr__, so importing the package no
longer triggers the Airflow import chain._import_base_operatoralso
now raises a single diagnosticImportErrorlisting all attempted paths
instead of leaking Airflow's internal deprecation traceback.
v.0.2.0
0.2.0 - 2026-05-24
Fixed
- Resolve
BaseOperatorfromairflow.sdk.bases.operatoron Airflow 3,
eliminating theDeprecatedImportWarningthat came from the legacy
airflow.models.baseoperatorpath. Import resolution now prefers the
canonical Task SDK location and only falls back to the Airflow 2 path on
Airflow 2. - Reject concurrent
run()calls on a singleSubprocessPytestRunner
instance (fail-fast withTestExecutionError) to prevent a race on the
per-run temporary directory and process handle. Separate runner instances
remain fully independent, and the same instance can be reused for
sequential runs (e.g. task retries). cancel()no longer holds the internal lock during its graceful
termination wait (up tograce_periodseconds). The lock is now held only
to snapshot the process handle, soon_killno longer serializes the
run's teardown orcleanup().- Wrap report-directory preparation so a user-supplied
report_dirpointing
at a file (or an unwritable location) raisesTestExecutionErrorper the
runner contract, instead of leaking a bareOSError.
Changed
- Breaking: the test summary is now exposed only via the standard XCom
return_valuekey. The previously-duplicated custompytest_resultkey
has been removed. Downstream tasks that readxcom_pull(key="pytest_result")
must switch toxcom_pull(task_ids="<task>")(the defaultreturn_value). - Breaking: removed the custom
push_resultparameter. XCom output is
now controlled solely by Airflow's standarddo_xcom_push(defaultTrue);
passdo_xcom_push=Falseto disable. Replacepush_result=Falsewith
do_xcom_push=False.
Added
- Documentation on installing the package in Docker/constrained environments
using Airflow's official constraint files.
v0.1.0
Changelog
All notable changes to this project are documented here.
The format is based on Keep a Changelog,
and this project adheres to Semantic Versioning.
Unreleased
0.1.0 - 2026-05-23
Initial release.
Added
PytestOperator— runs a pytest suite as an Airflow task, parses the JUnit
report into a structured result, optionally pushes a summary to XCom under the
pytest_resultkey, and fails the task on test failure (configurable via
fail_on_test_failure).PytestRunnerabstraction with a defaultSubprocessPytestRunnerthat runs
python -m pytestin a child process using the worker's own interpreter and
virtualenv.ResultParserabstraction with a defaultJUnitResultParser(uses
defusedxmlwhen available via thesecure-xmlextra).- Airflow 2.x / 3.x compatibility through a single
compat.airflowshim. - Graceful cancellation:
on_killterminates the whole pytest process tree
(SIGTERM→grace_period→SIGKILL), coveringxdistworkers and nested
processes. - Automatic working-directory resolution so relative paths in pytest
addopts
(e.g. Allure's--alluredir) resolve next to the tests. - Temporary report-directory cleanup with
cleanuppolicy
("always"|"on_success"|"never"); user-suppliedreport_diris never
removed; cleanup also runs on task kill. runtimeout support and clearTestExecutionErrorwhen pytest produces no
report (collection error, crash, wrong path).push_result=Falsesuppresses all XCom output, including Airflow's automatic
return-value push.- Packaged as an Airflow provider (
get_provider_infoentry point), Apache-2.0
licensed.