From ba4c7f64d6abc673793ecdd0f2c673339a56041d Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Mon, 13 Jul 2026 13:02:40 -0400 Subject: [PATCH 1/4] ci: fast required gate (e2e-excluded unit test), PR-only trigger, concurrency-cancel, caching Extracted from the engineering-hygiene branch so the merge queue benefits now. Required 'test' context stays a single ubuntu/py3.12 job (fast unit suite, e2e excluded); full matrix + e2e run post-merge/nightly. Halves runner load (drops the push+pull_request double-trigger) and cancels superseded runs. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ --- .github/workflows/ci.yml | 208 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 200 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81721f2..5b548de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,40 +1,232 @@ name: CI +# Triggers are deliberately asymmetric to keep PR feedback fast and cheap: +# - pull_request: run the FAST required gate (lint + unit `test`) on every PR. +# - push to main: re-validate main AND run the heavy post-merge suites +# (full Python matrix + macOS + windows-mock + wheel + slow e2e/browser). +# - schedule: a nightly run of the same heavy suites, so long-lived branches +# and environment drift are caught even without a merge. +# There is intentionally NO bare `push:` -- a bare push trigger fires a SECOND +# identical run alongside `pull_request` on every PR-branch push (double the +# runner spend for zero extra signal). on: - push: pull_request: + push: + branches: [main] + schedule: + - cron: "0 7 * * *" # 07:00 UTC nightly full matrix + +# Cancel any superseded in-flight run for the same ref (e.g. a force-push or a +# rapid second push to a PR branch) instead of letting both run to completion. +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# GitHub Actions are pinned to full commit SHAs (not mutable tags) as a +# supply-chain control; the trailing comment records the human-readable +# version. Dependabot (.github/dependabot.yml) proposes SHA bumps. jobs: + # --- Lint + format + type-check (fast, single interpreter) --------------- + # Runs on every event. On PRs it is chained INTO the merge gate: `test` + # `needs: lint`, so a lint failure skips `test` and the required check never + # goes green -> the PR cannot merge with failing lint. + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: pyproject.toml + + - name: Install + run: pip install -e .[dev] + + - name: Ruff lint + run: ruff check openadapt_flow + + - name: Ruff format check + run: ruff format --check openadapt_flow + + - name: Mypy + run: mypy + + # --- Required merge gate: FAST unit suite (ubuntu, single Python) --------- + # This is the ONE required status check (branch protection context: "test"). + # It MUST stay a single, non-matrix job named exactly `test` so its reported + # context is exactly `test` -- a matrix would report `test (3.12)` etc. and + # the required `test` check would never report, permanently blocking merges. + # It runs the bulk of the unit suite but EXCLUDES the slow end-to-end + # browser/OCR record->compile->replay suite (tests/e2e), which is exercised + # post-merge by `test-matrix`. Keeps PR feedback quick. test: + needs: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" + cache: pip + cache-dependency-path: pyproject.toml - name: Install run: pip install -e .[dev] + # Cache the Playwright browser binaries (chromium) so the lightweight + # browser-backed unit tests don't re-download ~150MB every run. Keyed on + # the resolved playwright version so a dependency bump busts the cache. + - name: Resolve Playwright version + id: pw + run: echo "version=$(python -c 'import importlib.metadata as m; print(m.version(\"playwright\"))')" >> "$GITHUB_OUTPUT" + + - name: Cache Playwright browsers + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ steps.pw.outputs.version }} + - name: Install Playwright browser run: playwright install --with-deps chromium # --basetemp pins pytest's tmp_path factory inside the workspace so # run dirs (REPORT.md + step/heal PNGs) survive for artifact upload; # by default they land in the system temp dir and are unrecoverable - # when a drift/heal test fails in CI. - - name: Test - run: mkdir -p runs && pytest -q --basetemp=runs/ci + # when a drift/heal test fails in CI. --cov reports coverage as a + # visibility number (no hard floor yet). --ignore=tests/e2e drops the + # slow browser/OCR end-to-end suite from the fast required gate. + - name: Test (fast unit suite) + run: | + mkdir -p runs + pytest -q --ignore=tests/e2e --basetemp=runs/ci \ + --cov=openadapt_flow --cov-report=term-missing:skip-covered + + - name: Upload run artifacts + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: runs-test + path: | + runs/**/REPORT.md + runs/**/BENCH.md + runs/**/report.json + runs/**/*.png + if-no-files-found: warn + + # --- Post-merge / nightly: full Python matrix + macOS, FULL suite -------- + # NOT a required check and NOT run on PRs (so merges never block on it). Runs + # the COMPLETE suite including the slow tests/e2e browser/OCR leg across + # Python 3.10-3.13 on Linux plus a macOS leg, on push to main and nightly. + test-matrix: + if: ${{ github.event_name != 'pull_request' }} + needs: lint + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python-version: ["3.10", "3.11", "3.12", "3.13"] + include: + # At least one non-Linux leg for the browser/OCR runtime. + - os: macos-latest + python-version: "3.12" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: pyproject.toml + + - name: Install + run: pip install -e .[dev] + + - name: Resolve Playwright version + id: pw + run: echo "version=$(python -c 'import importlib.metadata as m; print(m.version(\"playwright\"))')" >> "$GITHUB_OUTPUT" + + - name: Cache Playwright browsers + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ steps.pw.outputs.version }} + + - name: Install Playwright browser + run: playwright install --with-deps chromium + + - name: Test (full suite incl. e2e) + run: | + mkdir -p runs + pytest -q --basetemp=runs/ci \ + --cov=openadapt_flow --cov-report=term-missing:skip-covered - name: Upload run artifacts if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: runs + name: runs-${{ matrix.os }}-py${{ matrix.python-version }} path: | runs/**/REPORT.md runs/**/BENCH.md runs/**/report.json runs/**/*.png if-no-files-found: warn + + # --- Windows: pure-Python / mock backend tests (no live VM, no browser) -- + # Post-merge / nightly only (not a required check, not run on PRs). + windows-mock: + if: ${{ github.event_name != 'pull_request' }} + needs: lint + runs-on: windows-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: pyproject.toml + + - name: Install + run: pip install -e .[dev] + + # The WindowsBackend + RDP backend suites mock the WAA/RDP servers with + # stdlib HTTP + fakes (no live VM, no Playwright browser), so they run + # anywhere. This proves the 4-method Backend protocol on Windows. + - name: Windows mock backend tests + run: pytest -q tests/test_windows_backend.py tests/test_rdp_backend.py + + # --- Clean-wheel install: build, install in a fresh venv, import --------- + # Post-merge / nightly only (not a required check, not run on PRs). + wheel: + if: ${{ github.event_name != 'pull_request' }} + needs: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: pyproject.toml + + - name: Build wheel + run: | + pip install build + python -m build --wheel + + - name: Install the wheel in a clean venv and import + run: | + python -m venv /tmp/clean + /tmp/clean/bin/pip install dist/*.whl + # Import from a dir without the source tree; assert __version__ equals + # the installed distribution metadata (the drift bug this PR fixes -- + # version-agnostic so it keeps holding across releases) and that the + # py.typed marker shipped inside the wheel. + cd /tmp && /tmp/clean/bin/python -c "import os, importlib.metadata as m, openadapt_flow; v = openadapt_flow.__version__; dist = m.version('openadapt-flow'); print('attr', v, 'dist', dist); assert v == dist, (v, dist); p = os.path.join(os.path.dirname(openadapt_flow.__file__), 'py.typed'); assert os.path.exists(p), 'py.typed missing from installed package'; print('py.typed present, import OK')" From 2362e2077f454b8e22147d72687b31a7e2c6e3bb Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Mon, 13 Jul 2026 13:05:41 -0400 Subject: [PATCH 2/4] ci: decouple test gate from lint (main not yet ruff-formatted; #62 restores lint) --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b548de..ab07c46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,6 @@ jobs: # browser/OCR record->compile->replay suite (tests/e2e), which is exercised # post-merge by `test-matrix`. Keeps PR feedback quick. test: - needs: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 @@ -123,7 +122,6 @@ jobs: # Python 3.10-3.13 on Linux plus a macOS leg, on push to main and nightly. test-matrix: if: ${{ github.event_name != 'pull_request' }} - needs: lint strategy: fail-fast: false matrix: @@ -181,7 +179,6 @@ jobs: # Post-merge / nightly only (not a required check, not run on PRs). windows-mock: if: ${{ github.event_name != 'pull_request' }} - needs: lint runs-on: windows-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 @@ -205,7 +202,6 @@ jobs: # Post-merge / nightly only (not a required check, not run on PRs). wheel: if: ${{ github.event_name != 'pull_request' }} - needs: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 From edde05fa998aacd56e6a44a98a6c8018b6206302 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Mon, 13 Jul 2026 13:08:20 -0400 Subject: [PATCH 3/4] ci: drop --cov (pytest-cov not on main until #62); keep fast e2e-excluded gate --- .github/workflows/ci.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab07c46..b4be6cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,8 +101,7 @@ jobs: - name: Test (fast unit suite) run: | mkdir -p runs - pytest -q --ignore=tests/e2e --basetemp=runs/ci \ - --cov=openadapt_flow --cov-report=term-missing:skip-covered + pytest -q --ignore=tests/e2e --basetemp=runs/ci - name: Upload run artifacts if: always() @@ -160,8 +159,7 @@ jobs: - name: Test (full suite incl. e2e) run: | mkdir -p runs - pytest -q --basetemp=runs/ci \ - --cov=openadapt_flow --cov-report=term-missing:skip-covered + pytest -q --basetemp=runs/ci - name: Upload run artifacts if: always() From c984267d23c05c0bf24ad3aad3cdca9d44c4f96b Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Mon, 13 Jul 2026 13:27:20 -0400 Subject: [PATCH 4/4] ci: drop lint job (ruff/mypy + reformat land with #62); keep fast test gate only --- .github/workflows/ci.yml | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4be6cf..1a47fc9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,33 +27,6 @@ concurrency: # version. Dependabot (.github/dependabot.yml) proposes SHA bumps. jobs: - # --- Lint + format + type-check (fast, single interpreter) --------------- - # Runs on every event. On PRs it is chained INTO the merge gate: `test` - # `needs: lint`, so a lint failure skips `test` and the required check never - # goes green -> the PR cannot merge with failing lint. - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: "3.12" - cache: pip - cache-dependency-path: pyproject.toml - - - name: Install - run: pip install -e .[dev] - - - name: Ruff lint - run: ruff check openadapt_flow - - - name: Ruff format check - run: ruff format --check openadapt_flow - - - name: Mypy - run: mypy - # --- Required merge gate: FAST unit suite (ubuntu, single Python) --------- # This is the ONE required status check (branch protection context: "test"). # It MUST stay a single, non-matrix job named exactly `test` so its reported