name: CI on: push: branches: [ "**" ] pull_request: branches: [ "**" ] schedule: - cron: '0 3 * * *' jobs: # Regenerating the lockfile and diffing it proves the committed file really # came from src/requirements.txt. That is one question about one artifact, # so it is asked once here rather than once per matrix entry. # # WHY uv AND NOT pip-compile # # This lockfile has to serve Linux, macOS and Windows across Python # 3.10-3.12, and pip-compile resolves for the single environment it runs on # and emits no markers. Two dependencies make that impossible: aiohttp # needs async-timeout only below 3.11, and portalocker needs pywin32 only # on Windows. So every pip-compile output was wrong somewhere -- a # Linux/3.11 lockfile made the Windows job fail with "must have their # versions pinned" for pywin32, and a Windows/3.10 one could never match # the diff elsewhere. The Windows entry of this job had never been able to # install from this file. # # `uv pip compile --universal` resolves across all of them and emits the # markers, so ONE lockfile covers every target. The output is still an # ordinary pip requirements file: nothing downstream changes, the build # jobs still install it with plain `pip install --require-hashes`. Only the # generator is different, and it is pinned so this diff stays deterministic. lockfile: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.10" - name: Verify the lockfile matches src/requirements.txt run: | python -m pip install --upgrade pip pip install uv==0.10.4 # --python-version must equal the minimum in pyproject's # `python = ">=3.10,<3.13"`. Raising that floor means regenerating # requirements.lock with the new value. uv pip compile --universal --generate-hashes --python-version 3.10 \ -o requirements.lock src/requirements.txt git diff --exit-code requirements.lock secret-scan: name: Secret Scan runs-on: ubuntu-latest if: github.event_name == 'pull_request' || github.event_name == 'schedule' steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Run gitleaks uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITLEAKS_CONFIG: .gitleaks.toml build: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.11"] exclude: - os: windows-latest python-version: "3.11" include: - os: windows-latest python-version: "3.10" runs-on: ${{ matrix.os }} env: HYPOTHESIS_SEED: 123456 steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install build tools (Linux/macOS) if: runner.os != 'Windows' shell: bash run: | if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get update sudo apt-get install -y pkg-config build-essential else brew install pkg-config || brew upgrade pkg-config || true fi - name: Install MSYS2 toolchain if: runner.os == 'Windows' id: msys uses: msys2/setup-msys2@v2 with: update: true install: >- mingw-w64-x86_64-toolchain base-devel - name: Set PKG_CONFIG_PATH if: runner.os == 'Windows' shell: bash run: echo "PKG_CONFIG_PATH=${{ steps.msys.outputs.msys2-location }}/mingw64/lib/pkgconfig" >> $GITHUB_ENV - name: Add MSYS2 tools to PATH if: runner.os == 'Windows' shell: bash run: echo "${{ steps.msys.outputs.msys2-location }}/mingw64/bin" >> $GITHUB_PATH - name: Cache pip uses: actions/cache@v3 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('requirements.lock') }} restore-keys: | ${{ runner.os }}-pip- - name: Install dependencies # `shell: bash` so a failure in any command fails the step -- under # pwsh only the last one counts (see tests.yml). Under bash on Windows # a bare `python` then resolves to MSYS2's, which has no pip, so the # interpreter is chosen the same way run_ci_tests.sh chooses one: by # what it can actually do, falling back to the pythonLocation that # setup-python exports but does not put first on PATH. shell: bash run: | PY="" for candidate in python python3 "${pythonLocation:-}/python" "${pythonLocation:-}/python.exe"; do if "$candidate" -m pip --version >/dev/null 2>&1; then PY="$candidate" break fi done if [[ -z "$PY" ]]; then echo "no interpreter with pip found (pythonLocation=${pythonLocation:-unset})" >&2 exit 1 fi echo "install interpreter: $PY" "$PY" -m pip install --upgrade pip "$PY" -m pip install --require-hashes -r requirements.lock - name: Run dependency scan run: scripts/dependency_scan.sh --ignore-vuln GHSA-wj6h-64fc-37mp --ignore-vuln CVE-2025-62727 - name: Determine stress args shell: bash run: | if [ "${{ github.event_name }}" = "schedule" ]; then echo "STRESS_ARGS=--stress" >> $GITHUB_ENV fi - name: Enable Nostr network tests on main branch or nightly if: github.ref == 'refs/heads/main' || github.event_name == 'schedule' run: echo "NOSTR_E2E=1" >> $GITHUB_ENV - name: Run deterministic regression tests shell: bash run: pytest -q src/tests/test_deterministic_artifact_regression.py - name: Run tests with coverage timeout-minutes: 16 shell: bash run: scripts/run_ci_tests.sh - name: Run desktop tests timeout-minutes: 10 shell: bash env: TOGA_BACKEND: toga_dummy run: scripts/run_gui_tests.sh - name: Upload pytest log if: always() uses: actions/upload-artifact@v4 with: name: pytest-log-${{ matrix.os }} path: pytest.log - name: Upload GUI pytest log if: always() uses: actions/upload-artifact@v4 with: name: gui-pytest-log-${{ matrix.os }} path: pytest_gui.log - name: Upload coverage report uses: actions/upload-artifact@v4 with: name: coverage-xml-${{ matrix.os }} path: coverage.xml