From 20ea80135cd19f285894ff4604284789fdcfb62e Mon Sep 17 00:00:00 2001 From: emil Date: Sun, 5 Jul 2026 14:46:33 +0000 Subject: [PATCH] DEVX-112: fix: build images after post-merge publish, not on push --- .gitea/workflows/build-images.yml | 18 +++++++++++------- src/devx/tools/check_test_speed.py | 5 ++++- tests/unit/test_check_test_speed.py | 10 +++++----- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/.gitea/workflows/build-images.yml b/.gitea/workflows/build-images.yml index 49903f5..212c042 100644 --- a/.gitea/workflows/build-images.yml +++ b/.gitea/workflows/build-images.yml @@ -5,7 +5,9 @@ name: Build Images # devx and all dependencies into the image. # # Triggers: -# - On push to master (after post-merge release completes) +# - After post-merge workflow completes successfully (workflow_run) +# This ensures images are only rebuilt AFTER the release is published +# to PyPI, so the image always has the latest released version. # - Manually via workflow_dispatch # # The workflow builds 3 tier images in sequence: @@ -15,12 +17,10 @@ name: Build Images # After pushing, a cleanup job removes old versions (keeps last 2 + latest). on: - push: + workflow_run: + workflows: ["Post-merge"] + types: [completed] branches: [master] - paths: - - docker/** - - pyproject.toml - - src/devx/** workflow_dispatch: concurrency: @@ -49,7 +49,11 @@ jobs: build-and-push: needs: [detect-type] - if: needs.detect-type.outputs.is-release == 'false' + if: >- + needs.detect-type.outputs.is-release == 'false' && ( + github.event_name == 'workflow_dispatch' || + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') + ) runs-on: docker timeout-minutes: 30 steps: diff --git a/src/devx/tools/check_test_speed.py b/src/devx/tools/check_test_speed.py index f7b49ed..47ac69e 100644 --- a/src/devx/tools/check_test_speed.py +++ b/src/devx/tools/check_test_speed.py @@ -32,7 +32,10 @@ _TIMING_RE = re.compile(r"(\d+) passed.* in ([0-9.]+)s") # Matches per-test duration lines from --durations=0: # 0.51s call tests/test_foo.py::test_bar -_DURATION_LINE_RE = re.compile(r"^(\d+\.?\d*)s\s+(?:setup|call|teardown)\s+(.+)$") +# Only "call" duration is counted — "setup" includes import/collection +# overhead (coverage init, module imports) which is environment-dependent +# and not a test quality signal. +_DURATION_LINE_RE = re.compile(r"^(\d+\.?\d*)s\s+call\s+(.+)$") def run_tests() -> tuple[str, str]: diff --git a/tests/unit/test_check_test_speed.py b/tests/unit/test_check_test_speed.py index d58f3c4..ba34d28 100644 --- a/tests/unit/test_check_test_speed.py +++ b/tests/unit/test_check_test_speed.py @@ -69,16 +69,16 @@ class TestParsePerTestDurations: assert len(durations) == 1 assert durations[0] == ("tests/test_foo.py::test_bar", 0.01) - def test_parses_setup_and_teardown(self) -> None: + def test_ignores_setup_and_teardown(self) -> None: + """Only 'call' durations are counted — setup includes import overhead.""" output = ( - "0.02s setup tests/test_foo.py::test_bar\n" + "0.68s setup tests/test_foo.py::test_bar\n" "0.01s call tests/test_foo.py::test_bar\n" "0.00s teardown tests/test_foo.py::test_bar\n" ) durations = parse_per_test_durations(output) - assert len(durations) == 3 - names = [d[0] for d in durations] - assert "tests/test_foo.py::test_bar" in names + assert len(durations) == 1 + assert durations[0] == ("tests/test_foo.py::test_bar", 0.01) def test_sorted_slowest_first(self) -> None: output = "0.01s call tests/test_a.py::test_slow\n0.50s call tests/test_b.py::test_fast\n"