From 9eab92513a7e32609f2c1e6302e99351ae488785 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 5 Jul 2020 20:20:29 +0200 Subject: [PATCH] Add QA/CI to Supervisor (#1814) --- .devcontainer/devcontainer.json | 1 + .github/workflows/ci.yaml | 433 ++++++++++++++++++ .../check-executables-have-shebangs.json | 14 + .github/workflows/matchers/check-json.json | 16 + .github/workflows/matchers/flake8.json | 30 ++ .github/workflows/matchers/hadolint.json | 16 + .github/workflows/matchers/pylint.json | 32 ++ .github/workflows/matchers/python.json | 18 + .pre-commit-config.yaml | 32 ++ codecov.yaml | 9 + requirements_tests.txt | 17 +- supervisor/addons/addon.py | 2 +- tests/utils/test_tarfile.py | 5 +- 13 files changed, 617 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/matchers/check-executables-have-shebangs.json create mode 100644 .github/workflows/matchers/check-json.json create mode 100644 .github/workflows/matchers/flake8.json create mode 100644 .github/workflows/matchers/hadolint.json create mode 100644 .github/workflows/matchers/pylint.json create mode 100644 .github/workflows/matchers/python.json create mode 100644 .pre-commit-config.yaml create mode 100644 codecov.yaml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ac8f55e48..f6dba9204 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,6 +3,7 @@ "context": "..", "dockerFile": "Dockerfile", "appPort": "9123:8123", + "postCreateCommand": "pre-commit install", "runArgs": ["-e", "GIT_EDITOR=code --wait", "--privileged"], "extensions": [ "ms-python.python", diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 000000000..371218b10 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,433 @@ +name: CI + +# yamllint disable-line rule:truthy +on: + push: + branches: + - dev + - master + pull_request: ~ + +env: + DEFAULT_PYTHON: 3.7 + PRE_COMMIT_HOME: ~/.cache/pre-commit + +jobs: + # Separate job to pre-populate the base dependency cache + # This prevent upcoming jobs to do the same individually + prepare: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7, 3.8] + name: Prepare Python ${{ matrix.python-version }} dependencies + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: | + ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} + restore-keys: | + ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }} + ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}- + ${{ runner.os }}-venv- + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + python -m venv venv + . venv/bin/activate + pip install -U pip setuptools + pip install -r requirements.txt -r requirements_tests.txt + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: | + ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: | + ${{ runner.os }}-pre-commit- + - name: Install pre-commit dependencies + if: steps.cache-precommit.outputs.cache-hit != 'true' + run: | + . venv/bin/activate + pre-commit install-hooks + + lint-black: + name: Check black + runs-on: ubuntu-latest + needs: prepare + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: | + ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Run black + run: | + . venv/bin/activate + black --target-version py37 --check supervisor tests setup.py + + lint-dockerfile: + name: Check Dockerfile + runs-on: ubuntu-latest + needs: prepare + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Register hadolint problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/hadolint.json" + - name: Check Dockerfile + uses: docker://hadolint/hadolint:v1.18.0 + with: + args: hadolint Dockerfile + + lint-executable-shebangs: + name: Check executables + runs-on: ubuntu-latest + needs: prepare + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: | + ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: | + ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + - name: Fail job if cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Register check executables problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json" + - name: Run executables check + run: | + . venv/bin/activate + pre-commit run --hook-stage manual check-executables-have-shebangs --all-files + + lint-flake8: + name: Check flake8 + runs-on: ubuntu-latest + needs: prepare + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: | + ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Register flake8 problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/flake8.json" + - name: Run flake8 + run: | + . venv/bin/activate + flake8 supervisor tests + + lint-isort: + name: Check isort + runs-on: ubuntu-latest + needs: prepare + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: | + ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: | + ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + - name: Fail job if cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Run isort + run: | + . venv/bin/activate + pre-commit run --hook-stage manual isort --all-files --show-diff-on-failure + + lint-json: + name: Check JSON + runs-on: ubuntu-latest + needs: prepare + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: | + ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: | + ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + - name: Fail job if cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Register check-json problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/check-json.json" + - name: Run check-json + run: | + . venv/bin/activate + pre-commit run --hook-stage manual check-json --all-files + + lint-pylint: + name: Check pylint + runs-on: ubuntu-latest + needs: prepare + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: | + ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Register pylint problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/pylint.json" + - name: Run pylint + run: | + . venv/bin/activate + pylint supervisor tests + + lint-pyupgrade: + name: Check pyupgrade + runs-on: ubuntu-latest + needs: prepare + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: | + ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: | + ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + - name: Fail job if cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Run pyupgrade + run: | + . venv/bin/activate + pre-commit run --hook-stage manual pyupgrade --all-files --show-diff-on-failure + + pytest: + runs-on: ubuntu-latest + needs: prepare + strategy: + matrix: + python-version: [3.7, 3.8] + name: Run tests Python ${{ matrix.python-version }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ matrix.python-version }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: | + ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Install additional system dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends libpulse0 libudev1 + - name: Register Python problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Install Pytest Annotation plugin + run: | + . venv/bin/activate + # Ideally this should be part of our dependencies + # However this plugin is fairly new and doesn't run correctly + # on a non-GitHub environment. + pip install pytest-github-actions-annotate-failures + - name: Run pytest + run: | + . venv/bin/activate + pytest \ + -qq \ + --timeout=10 \ + --durations=10 \ + --cov supervisor \ + -o console_output_style=count \ + tests + - name: Upload coverage artifact + uses: actions/upload-artifact@v1 + with: + name: coverage-${{ matrix.python-version }} + path: .coverage + + coverage: + name: Process test coverage + runs-on: ubuntu-latest + needs: pytest + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: | + ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Download all coverage artifacts + uses: actions/download-artifact@v2 + - name: Combine coverage results + run: | + . venv/bin/activate + coverage combine coverage*/.coverage* + coverage report + coverage xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 diff --git a/.github/workflows/matchers/check-executables-have-shebangs.json b/.github/workflows/matchers/check-executables-have-shebangs.json new file mode 100644 index 000000000..667ef7956 --- /dev/null +++ b/.github/workflows/matchers/check-executables-have-shebangs.json @@ -0,0 +1,14 @@ +{ + "problemMatcher": [ + { + "owner": "check-executables-have-shebangs", + "pattern": [ + { + "regexp": "^(.+):\\s(.+)$", + "file": 1, + "message": 2 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/check-json.json b/.github/workflows/matchers/check-json.json new file mode 100644 index 000000000..390d63d02 --- /dev/null +++ b/.github/workflows/matchers/check-json.json @@ -0,0 +1,16 @@ +{ + "problemMatcher": [ + { + "owner": "check-json", + "pattern": [ + { + "regexp": "^(.+):\\s(.+\\sline\\s(\\d+)\\scolumn\\s(\\d+).+)$", + "file": 1, + "message": 2, + "line": 3, + "column": 4 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/flake8.json b/.github/workflows/matchers/flake8.json new file mode 100644 index 000000000..83163e728 --- /dev/null +++ b/.github/workflows/matchers/flake8.json @@ -0,0 +1,30 @@ +{ + "problemMatcher": [ + { + "owner": "flake8-error", + "severity": "error", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s(E\\d{3}\\s.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + } + ] + }, + { + "owner": "flake8-warning", + "severity": "warning", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s([CDFNW]\\d{3}\\s.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/hadolint.json b/.github/workflows/matchers/hadolint.json new file mode 100644 index 000000000..5a2f1846c --- /dev/null +++ b/.github/workflows/matchers/hadolint.json @@ -0,0 +1,16 @@ +{ + "problemMatcher": [ + { + "owner": "hadolint", + "pattern": [ + { + "regexp": "^(.+):(\\d+)\\s+((DL\\d{4}).+)$", + "file": 1, + "line": 2, + "message": 3, + "code": 4 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/pylint.json b/.github/workflows/matchers/pylint.json new file mode 100644 index 000000000..5624ca695 --- /dev/null +++ b/.github/workflows/matchers/pylint.json @@ -0,0 +1,32 @@ +{ + "problemMatcher": [ + { + "owner": "pylint-error", + "severity": "error", + "pattern": [ + { + "regexp": "^(.+):(\\d+):(\\d+):\\s(([EF]\\d{4}):\\s.+)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4, + "code": 5 + } + ] + }, + { + "owner": "pylint-warning", + "severity": "warning", + "pattern": [ + { + "regexp": "^(.+):(\\d+):(\\d+):\\s(([CRW]\\d{4}):\\s.+)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4, + "code": 5 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/python.json b/.github/workflows/matchers/python.json new file mode 100644 index 000000000..3e5d8d5b8 --- /dev/null +++ b/.github/workflows/matchers/python.json @@ -0,0 +1,18 @@ +{ + "problemMatcher": [ + { + "owner": "python", + "pattern": [ + { + "regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$", + "file": 1, + "line": 2 + }, + { + "regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$", + "message": 2 + } + ] + } + ] +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..77a2089b2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +repos: + - repo: https://github.com/psf/black + rev: 19.10b0 + hooks: + - id: black + args: + - --safe + - --quiet + files: ^((supervisor|tests)/.+)?[^/]+\.py$ + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.3 + hooks: + - id: flake8 + additional_dependencies: + - flake8-docstrings==1.5.0 + - pydocstyle==5.0.2 + files: ^(supervisor|script|tests)/.+\.py$ + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.1.0 + hooks: + - id: check-executables-have-shebangs + stages: [manual] + - id: check-json + - repo: https://github.com/pre-commit/mirrors-isort + rev: v4.3.21 + hooks: + - id: isort + - repo: https://github.com/asottile/pyupgrade + rev: v2.6.2 + hooks: + - id: pyupgrade + args: [--py37-plus] diff --git a/codecov.yaml b/codecov.yaml new file mode 100644 index 000000000..e8f6de918 --- /dev/null +++ b/codecov.yaml @@ -0,0 +1,9 @@ +codecov: + branch: dev +coverage: + status: + project: + default: + target: 40 + threshold: 0.09 +comment: false diff --git a/requirements_tests.txt b/requirements_tests.txt index 64f0adc75..7d931b79a 100644 --- a/requirements_tests.txt +++ b/requirements_tests.txt @@ -1,6 +1,13 @@ -flake8==3.8.3 -pylint==2.5.3 -pytest==5.4.3 -pytest-timeout==1.4.1 -pytest-aiohttp==0.3.0 black==19.10b0 +codecov==2.1.0 +coverage==5.1 +flake8-docstrings==1.5.0 +flake8==3.8.3 +pre-commit==2.6.0 +pydocstyle==5.0.2 +pylint==2.5.3 +pytest-aiohttp==0.3.0 +pytest-cov==2.10.0 +pytest-timeout==1.4.1 +pytest==5.4.3 +pyupgrade==2.6.2 diff --git a/supervisor/addons/addon.py b/supervisor/addons/addon.py index f859717af..bdfbbce2f 100644 --- a/supervisor/addons/addon.py +++ b/supervisor/addons/addon.py @@ -51,7 +51,7 @@ from ..exceptions import ( ) from ..utils.apparmor import adjust_profile from ..utils.json import read_json_file, write_json_file -from ..utils.tar import secure_path, atomic_contents_add +from ..utils.tar import atomic_contents_add, secure_path from .model import AddonModel, Data from .utils import remove_data from .validate import SCHEMA_ADDON_SNAPSHOT, validate_options diff --git a/tests/utils/test_tarfile.py b/tests/utils/test_tarfile.py index 907282985..50838b5ea 100644 --- a/tests/utils/test_tarfile.py +++ b/tests/utils/test_tarfile.py @@ -1,9 +1,10 @@ """Test Tarfile functions.""" +from pathlib import PurePath + import attr -from pathlib import PurePath -from supervisor.utils.tar import secure_path, _is_excluded_by_filter +from supervisor.utils.tar import _is_excluded_by_filter, secure_path @attr.s