From c2e7a41446b6235f6c3f95d21e093370159390bf Mon Sep 17 00:00:00 2001 From: Jash Parekh Date: Sat, 4 Dec 2021 17:22:12 -0500 Subject: [PATCH] Add support for running the action locally and linting py files (#4) --- .bandit | 2 + .flake8 | 7 + .../actions/install-dependencies/action.sh | 14 ++ .../actions/install-dependencies/action.yml | 20 +++ .github/workflows/integration-test.yml | 3 - .github/workflows/lint.yml | 91 +++++++++++++ CHANGELOG.md | 11 ++ CODE_OF_CONDUCT.md | 126 ++++++++++++++++++ CONTRIBUTING.md | 13 ++ Dockerfile | 1 + MAINTAINERS.md | 3 + README.md | 58 +++++++- docker-compose.yml | 10 +- docker/devbox.dockerfile | 28 ++++ docker/run_linting.sh | 50 +++++++ entrypoint.sh | 11 +- github.py | 45 +++++++ requirements-test.txt | 8 ++ setup.cfg | 12 ++ 19 files changed, 505 insertions(+), 8 deletions(-) create mode 100644 .bandit create mode 100644 .flake8 create mode 100755 .github/actions/install-dependencies/action.sh create mode 100644 .github/actions/install-dependencies/action.yml create mode 100644 .github/workflows/lint.yml create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 MAINTAINERS.md create mode 100644 docker/devbox.dockerfile create mode 100755 docker/run_linting.sh create mode 100644 github.py create mode 100644 requirements-test.txt create mode 100644 setup.cfg diff --git a/.bandit b/.bandit new file mode 100644 index 0000000..7cad8d5 --- /dev/null +++ b/.bandit @@ -0,0 +1,2 @@ +[bandit] +exclude: *venv*,*env*,*scratch* diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..b349232 --- /dev/null +++ b/.flake8 @@ -0,0 +1,7 @@ +[flake8] +max-line-length = 88 +max-complexity = 8 +ignore = W503,E501 +builtins = unicode +tee = True +exclude = venv,env,.venv,scratch diff --git a/.github/actions/install-dependencies/action.sh b/.github/actions/install-dependencies/action.sh new file mode 100755 index 0000000..bcce892 --- /dev/null +++ b/.github/actions/install-dependencies/action.sh @@ -0,0 +1,14 @@ +set -euo pipefail + +echo "Ensuring pip is up to date" +python -m pip install --upgrade pip + +if [[ "${INSTALL_REQUIREMENTS}" == "true" ]]; then + echo "Installing code requirements" + pip install -r requirements.lock +fi + +if [[ "${INSTALL_TEST_REQUIREMENTS}" == "true" ]]; then + echo "Installing test requirements" + pip install -r requirements-test.txt +fi diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml new file mode 100644 index 0000000..b482550 --- /dev/null +++ b/.github/actions/install-dependencies/action.yml @@ -0,0 +1,20 @@ +name: "Install Dependencies" +description: "Install Python dependencies" +inputs: + requirements: + description: "Should requirements.lock be installed" + default: "false" + required: false + test-requirements: + description: "Should requirements-test.txt be installed" + default: "false" + required: false +runs: + using: "composite" + steps: + - name: "Install Python dependencies" + run: "$GITHUB_ACTION_PATH/action.sh" + shell: "bash" + env: + INSTALL_REQUIREMENTS: ${{ inputs.requirements }} + INSTALL_TEST_REQUIREMENTS: ${{ inputs.test-requirements }} diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 748f057..2abf3d7 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -24,6 +24,3 @@ jobs: uses: actions/checkout@v2 - name: Mypy Lint uses: ./ # Uses an action in the root directory - with: - mypy_version: '0.901' - mypy_flags: '--verbose' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..7ed9de5 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,91 @@ +name: "Lint" +on: + pull_request: {} + push: + branches: ["main"] + +env: + PYTHON_VERSION: "3.10" + +jobs: + + bandit: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2.3.4 + - uses: actions/setup-python@v2.3.1 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Install dependencies + uses: ./.github/actions/install-dependencies + with: + test-requirements: "true" + + - name: Run bandit + run: bandit --ini .bandit -r github.py + + black: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2.3.4 + - name: Set up Python + uses: actions/setup-python@v2.3.1 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Install dependencies + uses: ./.github/actions/install-dependencies + with: + test-requirements: "true" + + - name: Run black + run: black --check github.py + + flake8: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2.3.4 + - uses: actions/setup-python@v2.3.1 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Install dependencies + uses: ./.github/actions/install-dependencies + with: + test-requirements: "true" + + - name: Run flake8 + run: flake8 github.py + + isort: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2.3.4 + - uses: actions/setup-python@v2.3.1 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Install dependencies + uses: ./.github/actions/install-dependencies + with: + test-requirements: "true" + + - name: Run isort + run: isort --recursive --check-only github.py + + mypy: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2.3.4 + - uses: actions/setup-python@v2.3.1 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Install dependencies + uses: ./.github/actions/install-dependencies + with: + test-requirements: "true" + + - name: Run mypy + run: mypy github.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fa7f10c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [v1] - 2021-09-12 + +### Added + +- Initial Release diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..18ee183 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,126 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement by contacting [a +project maintainer][maintainers]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[maintainers]: MAINTAINERS.md +[homepage]: https://www.contributor-covenant.org diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fe7b803 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,13 @@ +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via issue before making a change. + +Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. + +## Pull Request Process + +1. Ensure the CI pipeline and all checks are passing. +2. Update documentation with details of changes. +3. Add an entry to the [change log](CHANGELOG.md). +4. You may merge the Pull Request in once it has approvals of two other developers. If you do not have permission to do + that, you may request the second reviewer to merge it for you. diff --git a/Dockerfile b/Dockerfile index 75109f7..00b6267 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ FROM python:3.10-alpine LABEL "maintainer"="Jash Parekh " ADD entrypoint.sh /entrypoint.sh +ADD github.py /github.py RUN apk add bash gcc musl-dev diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000..c2d8662 --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,3 @@ +# Maintainers + +* Jash Parekh - me [at] jashparekh.com diff --git a/README.md b/README.md index 9bdccf7..ade1f35 100644 --- a/README.md +++ b/README.md @@ -1 +1,57 @@ -# mypy-action \ No newline at end of file +# Mypy Github Action + +GitHub Action for [mypy](https://mypy.readthedocs.io/en/master/). + +Influenced by [jpetrucciani/mypy-check](https://github.com/jpetrucciani/mypy-check). + +### Simple + +```yaml +name: "Mypy" +on: + pull_request: {} + push: + branches: ["main"] + +jobs: + deploy_schemas: + runs-on: ubuntu-latest + name: Mypy + steps: + # To use this repository's private action, + # you must check out the repository + - name: Checkout + uses: actions/checkout@v2 + - name: Deploy schemas to BigQuery + uses: jashparekh/mypy-action@v2 + with: + path: '.' + mypy_version: '0.910' + mypy_flags: '--verbose' +``` + +## Configuration + +### `path` (optional, string) + +File or directory to run mypy on. + +Default: `.` + +### `mypy_version` (optional, string) + +Version of mypy library to use for linting. + +Default: `0.910` + +### `mypy_config_file` (optional, string) + +Location of mypy config file in the repository. + +Default: `mypy.ini` + +### `mypy_flags` (optional, string) + +Optional mypy flags (refer to `mypy --help`) + +Default: `mypy.ini` diff --git a/docker-compose.yml b/docker-compose.yml index 7da6c02..62eb530 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,9 @@ version: '3.5' services: - devbox: + devbox: &devbox build: - dockerfile: "./Dockerfile" + dockerfile: "./docker/devbox.dockerfile" context: "." stdin_open: "true" # docker run -i tty: "true" # docker run -t @@ -11,6 +11,12 @@ services: volumes: - "./:/app" + # run all the linting locally + # - black & isort will format code to address issues + lint: + <<: *devbox + command: "docker/run_linting.sh --format-code" + volumes: home: env: diff --git a/docker/devbox.dockerfile b/docker/devbox.dockerfile new file mode 100644 index 0000000..2ade006 --- /dev/null +++ b/docker/devbox.dockerfile @@ -0,0 +1,28 @@ +FROM python:3.10.0-buster + +ARG _USER="lilchz" +ARG _UID="1001" +ARG _GID="100" +ARG _SHELL="/bin/bash" + +ARG BUILD_DATE + +RUN useradd -m -s "${_SHELL}" -N -u "${_UID}" "${_USER}" + +ENV USER ${_USER} +ENV UID ${_UID} +ENV GID ${_GID} +ENV HOME /home/${_USER} +ENV PATH "${HOME}/.local/bin/:${PATH}" +ENV PIP_NO_CACHE_DIR "true" + +RUN mkdir /app && chown ${UID}:${GID} /app + +USER ${_USER} + +COPY --chown=${UID}:${GID} ./requirements* /app/ +WORKDIR /app + +RUN pip install -r requirements-test.txt + +CMD bash diff --git a/docker/run_linting.sh b/docker/run_linting.sh new file mode 100755 index 0000000..162b71c --- /dev/null +++ b/docker/run_linting.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +set -eo pipefail + +BLACK_ACTION="--check" +ISORT_ACTION="--check-only" + +function usage +{ + echo "usage: run_linting.sh [--format-code]" + echo "" + echo " --format-code : Format the code instead of checking formatting." + exit 1 +} + +while [[ $# -gt 0 ]]; do + arg="$1" + case $arg in + --format-code) + BLACK_ACTION="--quiet" + ISORT_ACTION="" + ;; + -h|--help) + usage + ;; + "") + # ignore + ;; + *) + echo "Unexpected argument: ${arg}" + usage + ;; + esac + shift +done + +echo "Running MyPy..." +mypy github.py + +echo "Running black..." +black ${BLACK_ACTION} github.py + +echo "Running iSort..." +isort ${ISORT_ACTION} github.py + +echo "Running flake8..." +flake8 github.py + +echo "Running bandit..." +bandit --ini .bandit --quiet -r github.py diff --git a/entrypoint.sh b/entrypoint.sh index c3a8845..b7ef045 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -28,5 +28,12 @@ then mypy_args += " $flags" fi -# run mypy -mypy --show-column-numbers --hide-error-context ${mypy_args} ${lint_path} +# run mypy, tee output to file +# shellcheck disable=2086 +mypy --show-column-numbers --hide-error-context ${mypy_args} ${lint_path} | tee "${output_file}" +exit_code="${PIPESTATUS[0]}" + +# analyze output +python /github.py "${output_file}" + +exit "$exit_code" diff --git a/github.py b/github.py new file mode 100644 index 0000000..893c148 --- /dev/null +++ b/github.py @@ -0,0 +1,45 @@ +""" +quick script to write mypy errors to github comments +""" +import re +import sys + +MYPY_REGEX = ( + r"^(\w:)?(?P[^:]+):(?P\d+):((?P\d+):)?" + r"\s*(?P[^:]+):\s*(?P.+)" +) +MYPY = re.compile(MYPY_REGEX) + + +def error( + file: str, line: int = 0, col: int = 0, message: str = "error", warn: bool = False +) -> None: + """write an error to stdout""" + kind = "warning" if warn else "error" + print(f"::{kind} file={file},line={line},col={col}::{message}") + + +def main(file_path: str) -> None: + """read the given file, and print errors""" + data = "" + with open(file_path) as file: + data = file.read() + + for line in data.split("\n"): + match = MYPY.match(line) + if match: + values = match.groupdict() + error( + values["file"], + line=int(values["line"]), + col=int(values["col"]), + message=values["msg"], + ) + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("you must specify a json file path!") + sys.exit(1) + file_path = sys.argv[1] + main(file_path) diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..e5c7aaf --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,8 @@ +bandit==1.7.1 +black==21.11b1 +flake8==4.0.1 +isort==5.10.1 +mypy==0.910 +pytest==6.2.5 +pytest-cov==3.0.0 +pytest-mock==3.6.1 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b41dde6 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,12 @@ +[mypy] +python_version = 3.9 +disallow_untyped_defs = True +ignore_missing_imports = True + +[flake8] +ignore = N802,N807,W503,E203 +max-line-length = 100 +max-complexity = 30 + +[tool:pytest] +log_print = False