diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81721f2..1a47fc9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,40 +1,199 @@ 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: + # --- 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: 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 + + - 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' }} + 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 - 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' }} + 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' }} + 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')"