From e7c989ac3f526e475c04cdd2c9aa2e6b9654a3b6 Mon Sep 17 00:00:00 2001 From: James Bennett Date: Wed, 25 Sep 2024 22:53:42 -0700 Subject: [PATCH] Actions, Hynek-style. --- .github/workflows/ci.yml | 190 ++++++++++++++++++++++++++++++++++----- docs/conf.py | 4 + noxfile.py | 43 ++++++--- pyproject.toml | 8 ++ 4 files changed, 212 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edeccf1..cef2e96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,49 +4,193 @@ name: CI on: push: branches: [trunk] - tags: ["*"] pull_request: - branches: [trunk] workflow_dispatch: env: FORCE_COLOR: "1" - PIP_DISABLE_VERSION_CHECK: "1" + PIP_DISABLE_PIP_VERSION_CHECK: "1" + PIP_NO_PYTHON_VERSION_WARNING: "1" -permissions: - contents: read +permissions: {} jobs: - tests: - name: nox on ${{ matrix.python-version }} + build-package: + name: Build and verify package runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: hynek/build-and-inspect-python-package@v2 + id: baipp + + outputs: + python-versions: ${{ steps.baipp.outputs.supported_python_classifiers_json_array }} + + tests: + name: Tests on Python ${{ matrix.python-version }} + needs: build-package + runs-on: ubuntu-latest strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ${{ fromJson(needs.build-package.outputs.python-versions) }} steps: - - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 - with: - disable-sudo: true - egress-policy: block - allowed-endpoints: > - docs.python.org:443 - files.pythonhosted.org:443 - github.com:443 - pypi.org:443 - - uses: actions/checkout@v4 + - name: Download pre-built packages + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + - run: tar xf dist/*.tar.gz --strip-components=1 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: "Install dependencies" + allow-prereleases: true + - name: Install test runner run: | python -VV python -Im site - python -Im pip install --upgrade pip setuptools wheel python -Im pip install --upgrade nox python -Im nox --version - - name: "Run CI suite with nox" - run: "python -Im nox --non-interactive --error-on-external-run --python ${{ matrix.python-version }}" + - name: Run tests + run: "python -Im nox --non-interactive --error-on-external-run --tag tests --python ${{ matrix.python-version }}" + - name: Upload coverage data + uses: actions/upload-artifact@v4 + with: + name: coverage-data-${{ matrix.python-version }} + path: .coverage.* + include-hidden-files: true + if-no-files-found: ignore + + + coverage: + name: Combine and check coverage + needs: tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - uses: actions/download-artifact@v4 + with: + pattern: coverage-data-* + merge-multiple: true + + - name: Combine coverage & fail under 100% + run: | + python -Im pip install --upgrade "coverage[toml]" + + coverage combine + coverage html --skip-covered --skip-empty + + # Report and write to summary. + coverage report --format=markdown >> $GITHUB_STEP_SUMMARY + + # Report again and fail if under 100%. + coverage report --fail-under=100 + + - name: Upload HTML report if check failed. + uses: actions/upload-artifact@v4 + with: + name: html-report + path: htmlcov + if: ${{ failure() }} + + + docs: + name: Check documentation + needs: build-package + runs-on: ubuntu-latest + steps: + - name: Download pre-built packages + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + - run: tar xf dist/*.tar.gz --strip-components=1 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Set up test runner + run: | + python -VV + python -Im site + python -Im pip install --upgrade nox + python -Im nox --version + - name: Run documentation checks + run: "python -Im nox --non-interactive --error-on-external-run --tag docs" + + + lint-format: + name: Lint code and check formatting + needs: build-package + runs-on: ubuntu-latest + steps: + - name: Download pre-built packages + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + - run: tar xf dist/*.tar.gz --strip-components=1 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Set up test runner + run: | + python -VV + python -Im site + python -Im pip install --upgrade nox + python -Im nox --version + - name: Check code formatting + run: "python -Im nox --non-interactive --error-on-external-run --tag formatters --python 3.12" + - name: Lint code + run: "python -Im nox --non-interactive --error-on-external-run --tag linters --python 3.12" + + + check-package: + name: Additional package checks + needs: build-package + runs-on: ubuntu-latest + steps: + - name: Download pre-built packages + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + - run: tar xf dist/*.tar.gz --strip-components=1 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Set up test runner + run: | + python -VV + python -Im site + python -Im pip install --upgrade nox + python -Im nox --version + - name: Check package + run: "python -Im nox --non-interactive --error-on-external-run --tag packaging --python 3.12" + + + required-checks-pass: + name: Ensure required checks pass for branch protection + if: always() + + needs: + - check-package + - coverage + - docs + - lint-format + + runs-on: ubuntu-latest + + steps: + - name: Decide whether the jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/docs/conf.py b/docs/conf.py index 5baa799..4f76bda 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,6 +53,10 @@ # Location of word list. spelling_word_list_filename = "spelling_wordlist.txt" +# The documentation does not include contributor names, so we skip this because it's +# flaky about needing to scan commit history. +spelling_ignore_contributor_names = False + # OGP metadata configuration. ogp_enable_meta_description = True ogp_site_url = "https://akismet.readthedocs.io/" diff --git a/noxfile.py b/noxfile.py index e2f7789..a03a298 100644 --- a/noxfile.py +++ b/noxfile.py @@ -24,6 +24,8 @@ PACKAGE_NAME = "akismet" +IS_CI = bool(os.getenv("CI", False)) + NOXFILE_PATH = pathlib.Path(__file__).parents[0] ARTIFACT_PATHS = ( NOXFILE_PATH / "src" / f"{PACKAGE_NAME}.egg-info", @@ -44,6 +46,10 @@ def clean(paths: typing.Iterable[os.PathLike] = ARTIFACT_PATHS) -> None: Clean up after a test run. """ + # This cleanup is only useful for the working directory of a local checkout; in CI + # we don't need it because CI environments are ephemeral anyway. + if IS_CI: + return [ shutil.rmtree(path) if path.is_dir() else path.unlink() for path in paths @@ -58,7 +64,7 @@ def clean(paths: typing.Iterable[os.PathLike] = ARTIFACT_PATHS) -> None: @nox.session(python=["3.8", "3.9", "3.10", "3.11", "3.12"], tags=["tests"]) def tests_with_coverage(session: nox.Session) -> None: """ - Run the package's unit tests, with coverage report. + Run the package's unit tests, with coverage instrumentation. """ session.install(".[tests]") @@ -75,22 +81,17 @@ def tests_with_coverage(session: nox.Session) -> None: "discover", env={"PYTHON_AKISMET_API_KEY": TEST_KEY, "PYTHON_AKISMET_BLOG_URL": TEST_URL}, ) - session.run( - f"python{session.python}", - "-Im", - "coverage", - "report", - "--show-missing", - ) clean() -@nox.session(python=["3.8", "3.9", "3.10", "3.11", "3.12"], tags=["tests", "release"]) +@nox.session(python=["3.8", "3.9", "3.10", "3.11", "3.12"], tags=["release"]) def tests_end_to_end(session: nox.Session) -> None: """ Run the end-to-end (live Akismet API) tests. """ + if IS_CI: + session.skip("Release tests do not run in CI") session.install(".[tests]") session.run( f"python{session.python}", @@ -108,6 +109,26 @@ def tests_end_to_end(session: nox.Session) -> None: clean() +@nox.session(python=["3.12"], tags=["tests"]) +def coverage_report(session: nox.Session) -> None: + """ + Combine coverage from the various test runs and output the report. + + """ + # In CI this job does not run because we substitute one that integrates with the CI + # system. + if IS_CI: + session.skip( + "Running in CI -- skipping nox coverage job in favor of CI coverage job" + ) + session.install("coverage[toml]") + session.run(f"python{session.python}", "-Im", "coverage", "combine") + session.run( + f"python{session.python}", "-Im", "coverage", "report", "--show-missing" + ) + session.run(f"python{session.python}", "-Im", "coverage", "erase") + + # Tasks which test the package's documentation. # ----------------------------------------------------------------------------------- @@ -296,7 +317,7 @@ def lint_pylint(session: nox.Session) -> None: # does not have any direct dependencies, nor does the normal test suite, but the # full conformance suite does require a few extra libraries, so they're installed # here. - session.install("httpx", "pylint") + session.install("pylint", "bs4", "html5lib", "requests") session.run(f"python{session.python}", "-Im", "pylint", "--version") session.run(f"python{session.python}", "-Im", "pylint", "src/", "tests/") clean() @@ -348,6 +369,8 @@ def package_manifest(session: nox.Session) -> None: Check that the set of files in the package matches the set under version control. """ + if IS_CI: + session.skip("check-manifest already run by earlier CI steps.") session.install("check-manifest") session.run(f"python{session.python}", "-Im", "check_manifest", "--version") session.run(f"python{session.python}", "-Im", "check_manifest", "--verbose") diff --git a/pyproject.toml b/pyproject.toml index 0a97392..2cb77b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,9 +60,17 @@ skips = ["B101"] [tool.black] target-version = ["py38", "py39", "py310", "py311", "py312"] +[tool.coverage.paths] +source = ["src", ".nox/tests_with_coverage*/**/site-packages"] + [tool.coverage.report] fail_under = 100 +[tool.coverage.run] +branch = true +parallel = true +source = ["akismet"] + [tool.interrogate] fail-under = 100 ignore-init-method = true