├── .circleci ├── config.yml ├── scripts.yml ├── src.yml └── tests.yml ├── .github ├── FUNDING.yml └── settings.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pre-commit-hooks.yaml ├── .pylintrc ├── .pypirc ├── .tool-versions ├── LICENSE ├── README.md ├── examples ├── gitlab-ci │ ├── .gitlab-ci-1-FAIL.yml │ └── .gitlab-ci-1-PASS.yml └── gitlabci-lint-conf │ ├── .gitlabci-lint-defaults.toml │ ├── .gitlabci-lint-self-hosted-quiet.toml │ └── .gitlabci-lint-self-hosted.toml ├── package.json ├── poetry.lock ├── pyproject.toml ├── renovate.json ├── requirements.txt ├── scripts ├── dependencies.sh ├── pylint.sh └── tests.sh ├── src ├── __init__.py └── gitlabci_lint │ ├── __init__.py │ └── cli.py └── tests ├── .gitlab-ci-example-1-FAIL.yml ├── .gitlab-ci-example-1-PASS.yml ├── .gitlab-ci-example-2-FAIL.yml ├── .gitlab-ci-example-3-FAIL.yml └── run-tests.sh /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | setup: true 4 | 5 | 6 | orbs: 7 | dynamic: bjd2385/dynamic-continuation@3.7.1 8 | general: premiscale/general@1.0.12 9 | slack: circleci/slack@4.12.5 10 | 11 | 12 | workflows: 13 | gitlabci-lint: 14 | jobs: 15 | - dynamic/continue: 16 | context: orb-publishing 17 | 18 | - slack/on-hold: 19 | context: slack 20 | filters: 21 | branches: 22 | ignore: /.*/ 23 | tags: 24 | only: /^v?[0-9]+\.[0-9]+\.[0-9]+$/ 25 | 26 | - request-approval: 27 | requires: 28 | - slack/on-hold 29 | type: approval 30 | filters: 31 | branches: 32 | ignore: /.*/ 33 | tags: 34 | only: /^v?[0-9]+\.[0-9]+\.[0-9]+$/ 35 | 36 | - general/github-release: 37 | name: Create GitHub release from tag 38 | context: 39 | - github 40 | - orb-publishing 41 | requires: 42 | - request-approval 43 | github-organization: $CIRCLE_PROJECT_USERNAME 44 | filters: 45 | branches: 46 | ignore: /.*/ 47 | tags: 48 | only: /^v?[0-9]+\.[0-9]+\.[0-9]+$/ 49 | 50 | - general/python-release-poetry: 51 | name: gitlabci-lint upload [python] 52 | context: nexus 53 | repository: python 54 | requires: 55 | - request-approval 56 | filters: 57 | branches: 58 | ignore: /.*/ 59 | tags: 60 | only: /^v?[0-9]+\.[0-9]+\.[0-9]+$/ 61 | 62 | - general/python-release-poetry: 63 | name: pypi gitlabci-lint upload [pypi] 64 | context: pypi 65 | repository: pypi 66 | username: $PYPI_USERNAME 67 | password: $API_TOKEN 68 | requires: 69 | - request-approval 70 | filters: 71 | branches: 72 | ignore: /.*/ 73 | tags: 74 | only: /^v?[0-9]+\.[0-9]+\.[0-9]+$/ -------------------------------------------------------------------------------- /.circleci/scripts.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | 4 | orbs: 5 | shellcheck: circleci/shellcheck@3.1.1 6 | 7 | 8 | workflows: 9 | scripts: 10 | jobs: 11 | - shellcheck/check -------------------------------------------------------------------------------- /.circleci/src.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | general: premiscale/general@1.0.9 5 | 6 | 7 | executors: 8 | python3-10: 9 | docker: 10 | - image: cimg/python:3.10.6 11 | 12 | python3-9: 13 | docker: 14 | - image: cimg/python:3.9.12 15 | 16 | python3-8: 17 | docker: 18 | - image: cimg/python:3.8.13 19 | 20 | 21 | workflows: 22 | src: 23 | jobs: 24 | - general/python-pylint: 25 | name: pylint 26 | modules_path: src/ 27 | context: nexus 28 | 29 | - general/python-mypy: 30 | name: mypy [<< matrix.executor >>] 31 | configuration_file: pyproject.toml 32 | mypy-args: >- 33 | --ignore-missing-imports --install-types --non-interactive 34 | matrix: 35 | parameters: 36 | executor: 37 | - python3-10 38 | - python3-9 39 | - python3-8 40 | alias: mypy 41 | 42 | # develop 43 | 44 | - general/python-release-poetry: 45 | name: twine upload [python-develop] 46 | context: nexus 47 | repository: python-develop 48 | version: 0.0.<< pipeline.number >> 49 | pypirc-config: .pypirc 50 | requires: 51 | - pylint 52 | - mypy 53 | filters: 54 | branches: 55 | ignore: 56 | - main 57 | 58 | # master 59 | 60 | - general/python-release-poetry: 61 | name: twine upload [python-master] 62 | context: pypi 63 | repository: python-master 64 | version: 0.0.<< pipeline.number >> 65 | pypirc-config: .pypirc 66 | requires: 67 | - pylint 68 | - mypy 69 | filters: 70 | branches: 71 | only: 72 | - main -------------------------------------------------------------------------------- /.circleci/tests.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | 4 | executors: 5 | python3-10: 6 | docker: 7 | - image: cimg/python:3.10.6 8 | 9 | 10 | jobs: 11 | test-bats: 12 | executor: python3-10 13 | resource_class: small 14 | parameters: 15 | executable: 16 | type: string 17 | description: Location of the executable to run tests. 18 | default: ./scripts/tests.sh 19 | steps: 20 | - checkout 21 | - run: 22 | name: Install development package 23 | command: | 24 | pip install --upgrade poetry 25 | poetry install 26 | poetry run gitlabci-lint --version 27 | - run: 28 | name: Run bats tests 29 | command: | 30 | sudo apt update && sudo apt install -y bats 31 | << parameters.executable >> 32 | 33 | 34 | workflows: 35 | tests: 36 | jobs: 37 | - test-bats: 38 | context: gitlab -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | ko_fi: bjd2385 4 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | name: gitlabci-lint-pre-commit-hook 3 | description: A pre-commit hook that lints Gitlab CI configurations 4 | homepage: https://pypi.org/project/pre-commit-gitlabci-lint/ 5 | topics: gitlab, git, pre-commit, lint 6 | private: false 7 | has_issues: true 8 | has_projects: false 9 | has_wiki: false 10 | has_downloads: true 11 | 12 | default_branch: master 13 | 14 | allow_squash_merge: true 15 | allow_merge_commit: true 16 | allow_rebase_merge: true 17 | delete_branch_on_merge: true 18 | enable_automated_security_fixes: false 19 | enable_vulnerability_alerts: true 20 | 21 | # Labels: define labels for Issues and Pull Requests 22 | labels: 23 | - name: Bug 24 | color: CC0000 25 | description: An issue with the system 🐛. 26 | 27 | - name: Feature 28 | color: "#336699" 29 | description: New functionality. 30 | 31 | - name: Help Wanted 32 | new_name: first-timers-only 33 | 34 | branches: 35 | - name: main 36 | protection: 37 | required_pull_request_reviews: 38 | required_approving_review_count: 1 39 | dismiss_stale_reviews: true 40 | require_code_owner_reviews: true 41 | dismissal_restrictions: 42 | users: [] 43 | teams: [] 44 | required_status_checks: 45 | strict: true 46 | contexts: 47 | - on-commit 48 | enforce_admins: true 49 | required_linear_history: true 50 | restrictions: 51 | apps: [] 52 | users: [] 53 | teams: [] 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | #.idea/ 165 | 166 | ### Python Patch ### 167 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 168 | poetry.toml 169 | 170 | # ruff 171 | .ruff_cache/ 172 | 173 | # LSP config files 174 | pyrightconfig.json 175 | 176 | # End of https://www.toptal.com/developers/gitignore/api/python 177 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: true 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.5.0 5 | hooks: 6 | - id: trailing-whitespace 7 | - id: check-added-large-files 8 | args: [--maxkb=10000, --enforce-all] 9 | - id: check-executables-have-shebangs 10 | - id: check-shebang-scripts-are-executable 11 | - id: mixed-line-ending 12 | 13 | - repo: https://github.com/pre-commit/mirrors-mypy 14 | rev: v1.7.1 15 | hooks: 16 | - id: mypy 17 | args: 18 | - --install-types 19 | - --non-interactive 20 | - --config-file=pyproject.toml 21 | 22 | - repo: https://github.com/jumanjihouse/pre-commit-hooks 23 | rev: 3.0.0 24 | hooks: 25 | - id: shellcheck 26 | args: 27 | - -x 28 | 29 | - repo: https://github.com/bjd2385/dynamic-continuation-orb 30 | rev: v3.7.1 31 | hooks: 32 | - id: circleci-config-validate 33 | 34 | - repo: https://github.com/python-poetry/poetry 35 | rev: 1.7.0 36 | hooks: 37 | - id: poetry-check 38 | - id: poetry-lock 39 | - id: poetry-export 40 | args: ["-f", "requirements.txt", "-o", "requirements.txt"] 41 | 42 | - repo: https://github.com/PyCQA/pylint 43 | rev: v3.0.3 44 | hooks: 45 | - id: pylint 46 | args: 47 | - --rcfile=.pylintrc 48 | - premiscale/ 49 | 50 | - repo: https://github.com/abravalheri/validate-pyproject 51 | rev: v0.15 52 | hooks: 53 | - id: validate-pyproject 54 | 55 | - repo: https://github.com/premiscale/pre-commit-hooks 56 | rev: v0.0.9 57 | hooks: 58 | - id: msg-issue-prefix -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: gitlabci-lint 2 | name: Linting GitLab CI configs 3 | description: | 4 | This hooks verifies that your Gitlab CI configuration is valid. You can 5 | pass in a self-hosted GitLab URL in your .pre-commit-config.yaml (see 6 | README.md). 7 | entry: gitlabci-lint 8 | language: python 9 | #files: .gitlab-ci.yml 10 | pass_filenames: false 11 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | 3 | disable=invalid-name,superfluous-parens,line-too-long,missing-final-newline,logging-fstring-interpolation,too-many-instance-attributes 4 | fail-under=6 5 | 6 | [MASTER] 7 | init-hook='import sys; sys.path.append(".")' -------------------------------------------------------------------------------- /.pypirc: -------------------------------------------------------------------------------- 1 | [distutils] 2 | index-servers = 3 | pypi 4 | python 5 | python-develop 6 | python-master 7 | 8 | [pypi] 9 | repository = https://upload.pypi.org/legacy/ 10 | 11 | [python] 12 | repository = https://repo.ops.premiscale.com/repository/python/ 13 | 14 | [python-develop] 15 | repository = https://repo.ops.premiscale.com/repository/python-develop/ 16 | 17 | [python-master] 18 | repository = https://repo.ops.premiscale.com/repository/python-master/ -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 20.10.0 2 | yarn 1.22.19 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Emma Doyle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `gitlabci-lint` pre-commit hook 2 | 3 | [![PyPI version](https://img.shields.io/pypi/v/pre-commit-gitlabci-lint.svg?logo=pypi&style=flat-square)](https://pypi.org/project/pre-commit-gitlabci-lint/) 4 | [![PyPI downloads](https://img.shields.io/pypi/dm/pre-commit-gitlabci-lint?style=flat-square)](https://pypistats.org/packages/pre-commit-gitlabci-lint) 5 | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/emmeowzing/gitlabci-lint-pre-commit-hook/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/emmeowzing/gitlabci-lint-pre-commit-hook/tree/main) 6 | 7 | This is a [pre-commit](https://pre-commit.com/) hook that uses GitLab's `/api/v4/ci/lint` linting endpoint to validate the contents of `.gitlab-ci.yml` files. This is similar to CircleCI pre-commit hooks that validate that product's configuration files. 8 | 9 | ```text 10 | $ gitlabci-lint --help 11 | usage: gitlabci-lint [-h] [-c CONFIGS] [-C GITLABCI_LINT_CONFIG] [-b [BASE_URL]] [-p PROJECT_ID] [--version] [-q] 12 | 13 | Validate your GitLab CI with GitLab's API endpoint. 14 | 15 | optional arguments: 16 | -h, --help show this help message and exit 17 | -c CONFIGS, --configs CONFIGS 18 | CI Config files to check. (default: .gitlab-ci.yml) 19 | -C GITLABCI_LINT_CONFIG, --gitlabci-lint-config GITLABCI_LINT_CONFIG 20 | Pass parameters via config file. Looks first at '.gitlabci-lint.toml', then '$HOME/.config/gitlabci-lint/config.toml', unless otherwise specified. 21 | -b [BASE_URL], -B [BASE_URL], --base-url [BASE_URL] 22 | Base GitLab URL. (default: https://gitlab.com/) 23 | -p PROJECT_ID, -P PROJECT_ID, --project-id PROJECT_ID 24 | Project ID to use with the lint API. 25 | --version show program's version number and exit 26 | -q, -Q, --quiet Silently fail and pass, without output, unless improperly configured. (default: False) 27 | ``` 28 | 29 | ## Install 30 | 31 | ```shell 32 | pip install pre-commit-gitlabci-lint 33 | ``` 34 | 35 | ## Use 36 | 37 | ### Setup 38 | 39 | 1. [Create an access token](https://gitlab.com/-/profile/personal_access_tokens) with `api` scope. 40 | 2. Set access token value in an environment variable named `GITLAB_TOKEN` or `GITLABCI_LINT_TOKEN`. 41 | 3. Add the projectId for your gitlab project as a command line argument, or set it in the config file. 42 | 4. Ensure the virtualenv Python version is 3.8 or later. 43 | 44 | ### Configuration 45 | 46 | A configuration file is not required for use. However, if you'd rather specify settings in a file that is checked into your project's VCS, you may create a config file located at `/root/of/repo/.gitlabci-lint.toml`, or `$HOME/.config/.gitlabci-lint/config.toml`, such as the following. 47 | 48 | ```toml 49 | [gitlabci-lint] 50 | quiet = false 51 | base-url = "https://gitlab.com" 52 | project-id = "12345678" 53 | configs = [ ".gitlab-ci.yml" ] 54 | token = "$GITLAB_TOKEN" 55 | ``` 56 | 57 | ## Examples 58 | 59 | ### Shell 60 | 61 | ```console 62 | $ export GITLAB_TOKEN="$(pass show gitlab-api-key)" 63 | $ gitlabci-lint -p 64 | Config file at '.gitlab-ci.yml' is valid. 65 | ``` 66 | 67 | ### pre-commit 68 | 69 | An example `.pre-commit-config.yaml`: 70 | 71 | ```yaml 72 | --- 73 | repos: 74 | - repo: https://github.com/bjd2385/pre-commit-gitlabci-lint 75 | rev: 76 | hooks: 77 | - id: gitlabci-lint 78 | # args: [-b, 'https://custom.gitlab.host.com', '-p', '12345678'] 79 | ``` 80 | 81 | ### GitLab CI 82 | 83 | Here is an example Gitlab CI job that lints all GitLab CI files in a project on merge requests with naming conventions matching the regex `.*.gitlab-ci.yml`. 84 | 85 | ```yaml 86 | gitlab-ci-lint: 87 | stage: test 88 | image: cimg/base:2023.12 89 | variables: 90 | GITLAB_CI_LINT_VERSION: 91 | YQ_VERSION: 4.40.5 92 | before_script: 93 | - set -eo pipefail 94 | - sudo apt update && sudo apt install -y python3-pip 95 | - pip install -q --disable-pip-version-check --no-python-version-warning pre-commit-gitlabci-lint=="$GITLAB_CI_LINT_VERSION" 96 | - | 97 | wget https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_linux_amd64 -O yq 98 | sudo install yq /usr/local/bin/yq 99 | script: 100 | - |+ 101 | mapfile -t _TEMPLATES < <(find . -type f -regex ".*.gitlab-ci.yml") 102 | 103 | for template in "${_TEMPLATES[@]}"; do 104 | printf "INFO: Considering \"%s\"\\n" "$template" 105 | 106 | # Count the number of jobs present in a GitLab CI template. Job templates aren't counted as jobs. 107 | _JOB_COUNT="$(yq '... comments="" | to_entries | filter(.key != "include" and .key != "default" and .key != "stages" and .key != "variables" and .key != "workflow" and (.key != ".*") and .key != "cache") | from_entries | length' "$template")" 108 | 109 | if [ "$_JOB_COUNT" -ne "0" ]; then 110 | printf "INFO: Linting \"%s\"\\n" "$template" 111 | gitlabci-lint -p "$CI_PROJECT_ID" -b https://"$CI_SERVER_HOST" -c "$template" 112 | else 113 | printf "INFO: Skipping \"%s\": no defined jobs.\\n" "$template" 114 | fi 115 | done 116 | tags: 117 | - small 118 | rules: 119 | - $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^(develop|main)$/ 120 | ``` 121 | -------------------------------------------------------------------------------- /examples/gitlab-ci/.gitlab-ci-1-FAIL.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - validate 3 | - build 4 | - deploy 5 | -------------------------------------------------------------------------------- /examples/gitlab-ci/.gitlab-ci-1-PASS.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - validate 3 | - build 4 | - deploy 5 | 6 | .install-dependencies: 7 | stage: build 8 | script: 9 | - apt update && apt install htop 10 | 11 | setup: 12 | extends: .install-dependencies 13 | -------------------------------------------------------------------------------- /examples/gitlabci-lint-conf/.gitlabci-lint-defaults.toml: -------------------------------------------------------------------------------- 1 | [gitlabci-lint] 2 | quiet = false 3 | base-url = https://gitlab.com 4 | configs = [ .gitlab-ci.yml ] 5 | token = "$GITLAB_TOKEN" -------------------------------------------------------------------------------- /examples/gitlabci-lint-conf/.gitlabci-lint-self-hosted-quiet.toml: -------------------------------------------------------------------------------- 1 | [gitlabci-lint] 2 | quiet = true 3 | base-url = https://git.ops.sbe-vision.com 4 | configs = [ .gitlab-ci.yml ] 5 | -------------------------------------------------------------------------------- /examples/gitlabci-lint-conf/.gitlabci-lint-self-hosted.toml: -------------------------------------------------------------------------------- 1 | [gitlabci-lint] 2 | quiet = false 3 | base-url = https://git.ops.sbe-vision.com 4 | configs = [ .gitlab-ci.yml ] 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pre-commit-gitlabci-lint", 3 | "description": "pre-commit hook for GitLab CI validation", 4 | "repository": "git@github.com:bjd2385/pre-commit-gitlabci-lint.git", 5 | "author": "Emma Doyle ", 6 | "license": "MIT", 7 | "scripts": { 8 | "install:dependencies": "./scripts/dependencies.sh", 9 | "install:deps": "yarn install:dependencies", 10 | "pylint": "./scripts/pylint.sh", 11 | "freeze": "pip freeze | grep -v \"file:///\" | grep -v \"ssh://\" > \"$(git rev-parse --show-toplevel)\"/requirements.txt", 12 | "test": "./scripts/tests.sh", 13 | "tests": "./scripts/tests.sh", 14 | "run:tests": "./scripts/tests.sh" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "astroid" 5 | version = "3.0.2" 6 | description = "An abstract syntax tree for Python with inference support." 7 | optional = false 8 | python-versions = ">=3.8.0" 9 | files = [ 10 | {file = "astroid-3.0.2-py3-none-any.whl", hash = "sha256:d6e62862355f60e716164082d6b4b041d38e2a8cf1c7cd953ded5108bac8ff5c"}, 11 | {file = "astroid-3.0.2.tar.gz", hash = "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91"}, 12 | ] 13 | 14 | [package.dependencies] 15 | typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} 16 | 17 | [[package]] 18 | name = "cfgv" 19 | version = "3.4.0" 20 | description = "Validate configuration and produce human readable error messages." 21 | optional = false 22 | python-versions = ">=3.8" 23 | files = [ 24 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 25 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 26 | ] 27 | 28 | [[package]] 29 | name = "colorama" 30 | version = "0.4.6" 31 | description = "Cross-platform colored terminal text." 32 | optional = false 33 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 34 | files = [ 35 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 36 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 37 | ] 38 | 39 | [[package]] 40 | name = "dill" 41 | version = "0.3.7" 42 | description = "serialize all of Python" 43 | optional = false 44 | python-versions = ">=3.7" 45 | files = [ 46 | {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, 47 | {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, 48 | ] 49 | 50 | [package.extras] 51 | graph = ["objgraph (>=1.7.2)"] 52 | 53 | [[package]] 54 | name = "distlib" 55 | version = "0.3.8" 56 | description = "Distribution utilities" 57 | optional = false 58 | python-versions = "*" 59 | files = [ 60 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 61 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 62 | ] 63 | 64 | [[package]] 65 | name = "filelock" 66 | version = "3.13.1" 67 | description = "A platform independent file lock." 68 | optional = false 69 | python-versions = ">=3.8" 70 | files = [ 71 | {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, 72 | {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, 73 | ] 74 | 75 | [package.extras] 76 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] 77 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] 78 | typing = ["typing-extensions (>=4.8)"] 79 | 80 | [[package]] 81 | name = "identify" 82 | version = "2.5.33" 83 | description = "File identification library for Python" 84 | optional = false 85 | python-versions = ">=3.8" 86 | files = [ 87 | {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, 88 | {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, 89 | ] 90 | 91 | [package.extras] 92 | license = ["ukkonen"] 93 | 94 | [[package]] 95 | name = "isort" 96 | version = "5.13.2" 97 | description = "A Python utility / library to sort Python imports." 98 | optional = false 99 | python-versions = ">=3.8.0" 100 | files = [ 101 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 102 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 103 | ] 104 | 105 | [package.extras] 106 | colors = ["colorama (>=0.4.6)"] 107 | 108 | [[package]] 109 | name = "mccabe" 110 | version = "0.7.0" 111 | description = "McCabe checker, plugin for flake8" 112 | optional = false 113 | python-versions = ">=3.6" 114 | files = [ 115 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 116 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 117 | ] 118 | 119 | [[package]] 120 | name = "mypy" 121 | version = "1.7.1" 122 | description = "Optional static typing for Python" 123 | optional = false 124 | python-versions = ">=3.8" 125 | files = [ 126 | {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, 127 | {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, 128 | {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, 129 | {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, 130 | {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, 131 | {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, 132 | {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, 133 | {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, 134 | {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, 135 | {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, 136 | {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, 137 | {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, 138 | {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, 139 | {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, 140 | {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, 141 | {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, 142 | {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, 143 | {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, 144 | {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, 145 | {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, 146 | {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, 147 | {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, 148 | {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, 149 | {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, 150 | {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, 151 | {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, 152 | {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, 153 | ] 154 | 155 | [package.dependencies] 156 | mypy-extensions = ">=1.0.0" 157 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 158 | typing-extensions = ">=4.1.0" 159 | 160 | [package.extras] 161 | dmypy = ["psutil (>=4.0)"] 162 | install-types = ["pip"] 163 | mypyc = ["setuptools (>=50)"] 164 | reports = ["lxml"] 165 | 166 | [[package]] 167 | name = "mypy-extensions" 168 | version = "1.0.0" 169 | description = "Type system extensions for programs checked with the mypy type checker." 170 | optional = false 171 | python-versions = ">=3.5" 172 | files = [ 173 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 174 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 175 | ] 176 | 177 | [[package]] 178 | name = "nodeenv" 179 | version = "1.8.0" 180 | description = "Node.js virtual environment builder" 181 | optional = false 182 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" 183 | files = [ 184 | {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, 185 | {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, 186 | ] 187 | 188 | [package.dependencies] 189 | setuptools = "*" 190 | 191 | [[package]] 192 | name = "platformdirs" 193 | version = "4.1.0" 194 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 195 | optional = false 196 | python-versions = ">=3.8" 197 | files = [ 198 | {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, 199 | {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, 200 | ] 201 | 202 | [package.extras] 203 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] 204 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] 205 | 206 | [[package]] 207 | name = "pre-commit" 208 | version = "3.5.0" 209 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 210 | optional = false 211 | python-versions = ">=3.8" 212 | files = [ 213 | {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, 214 | {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, 215 | ] 216 | 217 | [package.dependencies] 218 | cfgv = ">=2.0.0" 219 | identify = ">=1.0.0" 220 | nodeenv = ">=0.11.1" 221 | pyyaml = ">=5.1" 222 | virtualenv = ">=20.10.0" 223 | 224 | [[package]] 225 | name = "pylint" 226 | version = "3.0.3" 227 | description = "python code static checker" 228 | optional = false 229 | python-versions = ">=3.8.0" 230 | files = [ 231 | {file = "pylint-3.0.3-py3-none-any.whl", hash = "sha256:7a1585285aefc5165db81083c3e06363a27448f6b467b3b0f30dbd0ac1f73810"}, 232 | {file = "pylint-3.0.3.tar.gz", hash = "sha256:58c2398b0301e049609a8429789ec6edf3aabe9b6c5fec916acd18639c16de8b"}, 233 | ] 234 | 235 | [package.dependencies] 236 | astroid = ">=3.0.1,<=3.1.0-dev0" 237 | colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} 238 | dill = [ 239 | {version = ">=0.2", markers = "python_version < \"3.11\""}, 240 | {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, 241 | {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, 242 | ] 243 | isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" 244 | mccabe = ">=0.6,<0.8" 245 | platformdirs = ">=2.2.0" 246 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 247 | tomlkit = ">=0.10.1" 248 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 249 | 250 | [package.extras] 251 | spelling = ["pyenchant (>=3.2,<4.0)"] 252 | testutils = ["gitpython (>3)"] 253 | 254 | [[package]] 255 | name = "pyyaml" 256 | version = "6.0.1" 257 | description = "YAML parser and emitter for Python" 258 | optional = false 259 | python-versions = ">=3.6" 260 | files = [ 261 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 262 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 263 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 264 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 265 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 266 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 267 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 268 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 269 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 270 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 271 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 272 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 273 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 274 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 275 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 276 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 277 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 278 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 279 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 280 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 281 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 282 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 283 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 284 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 285 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 286 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 287 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 288 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 289 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 290 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 291 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 292 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 293 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 294 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 295 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 296 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 297 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 298 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 299 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 300 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 301 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 302 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 303 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 304 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 305 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 306 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 307 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 308 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 309 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 310 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 311 | ] 312 | 313 | [[package]] 314 | name = "setuptools" 315 | version = "69.0.2" 316 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 317 | optional = false 318 | python-versions = ">=3.8" 319 | files = [ 320 | {file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"}, 321 | {file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"}, 322 | ] 323 | 324 | [package.extras] 325 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 326 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 327 | testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 328 | 329 | [[package]] 330 | name = "toml" 331 | version = "0.10.2" 332 | description = "Python Library for Tom's Obvious, Minimal Language" 333 | optional = false 334 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 335 | files = [ 336 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 337 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 338 | ] 339 | 340 | [[package]] 341 | name = "tomli" 342 | version = "2.0.1" 343 | description = "A lil' TOML parser" 344 | optional = false 345 | python-versions = ">=3.7" 346 | files = [ 347 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 348 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 349 | ] 350 | 351 | [[package]] 352 | name = "tomlkit" 353 | version = "0.12.3" 354 | description = "Style preserving TOML library" 355 | optional = false 356 | python-versions = ">=3.7" 357 | files = [ 358 | {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, 359 | {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, 360 | ] 361 | 362 | [[package]] 363 | name = "typing-extensions" 364 | version = "4.9.0" 365 | description = "Backported and Experimental Type Hints for Python 3.8+" 366 | optional = false 367 | python-versions = ">=3.8" 368 | files = [ 369 | {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, 370 | {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, 371 | ] 372 | 373 | [[package]] 374 | name = "virtualenv" 375 | version = "20.25.0" 376 | description = "Virtual Python Environment builder" 377 | optional = false 378 | python-versions = ">=3.7" 379 | files = [ 380 | {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, 381 | {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, 382 | ] 383 | 384 | [package.dependencies] 385 | distlib = ">=0.3.7,<1" 386 | filelock = ">=3.12.2,<4" 387 | platformdirs = ">=3.9.1,<5" 388 | 389 | [package.extras] 390 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 391 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 392 | 393 | [metadata] 394 | lock-version = "2.0" 395 | python-versions = "^3.8" 396 | content-hash = "1d88a7776a160434c15be1beedc8da1b7fa67b70a776c2f25cc7d3040ae07c1f" 397 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pre-commit-gitlabci-lint" 3 | version = "1.3.0" 4 | description = "Validate GitLab CI templates with a GitLab instance's API endpoint" 5 | authors = ["Emma Doyle "] 6 | license = "MIT" 7 | readme = "README.md" 8 | packages = [{include = "src"}] 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.8" 12 | toml = "^0.10.2" 13 | 14 | [tool.poetry.group.dev.dependencies] 15 | pylint = "^3.0.2" 16 | mypy = "^1.7.1" 17 | pre-commit = "^3.5.0" 18 | 19 | [tool.poetry.scripts] 20 | gitlabci-lint = "src.gitlabci_lint.cli:main" 21 | 22 | [build-system] 23 | requires = ["poetry-core"] 24 | build-backend = "poetry.core.masonry.api" 25 | 26 | [tool.mypy] 27 | python_version = "3.8" 28 | strict_optional = true 29 | ignore_missing_imports = true -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | toml==0.10.2 ; python_version >= "3.8" and python_version < "4.0" \ 2 | --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ 3 | --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f 4 | -------------------------------------------------------------------------------- /scripts/dependencies.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | asdf plugin add nodejs https://github.com/asdf-vm/asdf-nodejs.git 4 | asdf plugin-add yarn 5 | 6 | asdf install 7 | 8 | if [ "$(conda info --envs --json | jq -r '.envs[]' | awk '/(gitlab-ci-lint)$/')" = "" ]; then 9 | conda create -y -n gitlab-ci-lint python=3.10 10 | fi 11 | 12 | sudo apt install -y bats 13 | 14 | #conda activate gitlab-ci-lint 15 | #pip install setupext_janitor pylint mypy 16 | -------------------------------------------------------------------------------- /scripts/pylint.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # Execute pylint against this package's modules. 3 | 4 | pylint --rcfile .pylintrc src/ 5 | -------------------------------------------------------------------------------- /scripts/tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | ./tests/run-tests.sh 4 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmeowzing/gitlabci-lint-pre-commit-hook/899ef12c7cf78124a1d38b9be7bd682d12041a77/src/__init__.py -------------------------------------------------------------------------------- /src/gitlabci_lint/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmeowzing/gitlabci-lint-pre-commit-hook/899ef12c7cf78124a1d38b9be7bd682d12041a77/src/gitlabci_lint/__init__.py -------------------------------------------------------------------------------- /src/gitlabci_lint/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Validate your GitLab CI with GitLab's API endpoint. 3 | """ 4 | 5 | 6 | import argparse 7 | import json 8 | import os 9 | import sys 10 | import toml 11 | import importlib.metadata as meta 12 | import pathlib 13 | 14 | from urllib.error import HTTPError 15 | from urllib.parse import urljoin 16 | from urllib.request import Request, urlopen 17 | from http import HTTPStatus 18 | from functools import partial 19 | from typing import List, Optional 20 | 21 | 22 | __version__ = meta.version('pre-commit-gitlabci-lint') 23 | 24 | errprint = partial(print, file=sys.stderr) 25 | 26 | default_config_section = 'gitlabci-lint' 27 | default_base_url: str = 'https://gitlab.com/' 28 | default_quiet: bool = False 29 | default_configs: List[str] = list() 30 | 31 | 32 | def validateCiConfig(token: str, baseUrl: str, project_id: str, configs: List[str], silent: bool) -> int: 33 | """ 34 | Validate the input GitLab CI config against the validation API endpoint. 35 | 36 | Args: 37 | baseUrl: The location of the GitLab instance. 38 | project_id: A gitlab project id. 39 | configFile: The GitLab CI file to validate. 40 | silent: Whether or not to output text on success or failure, unless improperly configured. 41 | Allows the use of exit codes in scripts without redirecting stdout. 42 | 43 | Returns: 44 | An exit code, zero if successful and the CI config is valid. 45 | """ 46 | returnValue = 0 47 | 48 | for config in configs: 49 | try: 50 | with open(config, 'r', encoding='utf-8') as f: 51 | data = json.dumps( 52 | { 53 | 'content': f.read() 54 | } 55 | ) 56 | except (FileNotFoundError, PermissionError): 57 | errprint(f'Cannot open {config}') 58 | returnValue = 1 59 | else: 60 | url = urljoin(baseUrl, f'/api/v4/projects/{project_id}/ci/lint') 61 | headers = { 62 | 'Content-Type': 'application/json', 63 | 'Content-Length': str(len(data)) 64 | } 65 | 66 | # Get around mypy typing issue with if statement. 67 | if token: 68 | headers['PRIVATE-TOKEN'] = token 69 | 70 | try: 71 | request = Request( 72 | url, 73 | data.encode('utf-8'), 74 | headers=headers, 75 | ) 76 | 77 | if silent: 78 | # Lint quietly. 79 | with urlopen(request) as response: 80 | lint_output = json.loads(response.read()) 81 | 82 | if lint_output['status'] == 'invalid': 83 | returnValue = 1 84 | else: 85 | # Lint verbosely. 86 | with urlopen(request) as response: 87 | lint_output = json.loads(response.read()) 88 | 89 | if not lint_output['valid']: 90 | errprint('=======') 91 | for error in lint_output['errors']: 92 | errprint(error) 93 | returnValue = 1 94 | errprint('=======') 95 | elif lint_output['valid'] and lint_output['warnings']: 96 | print(f'Config file at \'{config}\' is valid, with warnings:', end=' ') 97 | for warning in lint_output['warnings']: 98 | errprint(warning) 99 | else: 100 | print(f'Config file at \'{config}\' is valid.') 101 | 102 | except HTTPError as exc: 103 | errprint(f'Error connecting to Gitlab: {exc}') 104 | 105 | if exc.code == HTTPStatus.UNAUTHORIZED: 106 | errprint( 107 | 'The lint endpoint requires authentication. ' 108 | 'Please check value of \'GITLAB_TOKEN\' environment variable.' 109 | ) 110 | else: 111 | errprint(f'Failed with reason \'{exc.reason}\'') 112 | 113 | returnValue = 1 114 | if returnValue == 1: 115 | break 116 | 117 | return returnValue 118 | 119 | 120 | def config(conf: Optional[str] =None) -> dict: 121 | """ 122 | Read a config file, if it exists, from standard locations. 123 | 124 | Returns: 125 | dict: The parsed config file. 126 | """ 127 | config_locations = [conf] if conf else [ 128 | '.gitlabci-lint.toml', 129 | os.path.expandvars('$HOME/.config/gitlabci-lint/config.toml') 130 | ] 131 | 132 | for loc in config_locations: 133 | try: 134 | if pathlib.Path(loc).exists(): 135 | return toml.load(pathlib.Path(loc)) 136 | elif conf: 137 | errprint(f'Could not locate config file at {loc}, please ensure this file exists.') 138 | sys.exit(1) 139 | except PermissionError: 140 | errprint(f'Could not access config file at {loc}, check permissions.') 141 | sys.exit(1) 142 | 143 | return {} 144 | 145 | 146 | def main() -> None: 147 | """ 148 | Set up CLI for gitlabci-lint pre-commit hook. 149 | """ 150 | parser = argparse.ArgumentParser( 151 | description=__doc__, 152 | formatter_class=argparse.RawDescriptionHelpFormatter 153 | ) 154 | 155 | parser.add_argument( 156 | '-c', '--configs', action='append', default=list(), 157 | help='CI Config files to check. (default: .gitlab-ci.yml)' 158 | ) 159 | 160 | parser.add_argument( 161 | '-C', '--gitlabci-lint-config', nargs=1, default=None, 162 | help='Pass parameters via config file. Looks first at \'.gitlabci-lint.toml\', then \'$HOME/.config/gitlabci-lint/config.toml\', unless otherwise specified.' 163 | ) 164 | 165 | parser.add_argument( 166 | '-b', '-B', '--base-url', nargs='?', default=default_base_url, 167 | help=f'Base GitLab URL. (default: {default_base_url})' 168 | ) 169 | 170 | parser.add_argument( 171 | '-p', '-P', '--project-id', 172 | help='Project ID to use with the lint API.' 173 | ) 174 | 175 | parser.add_argument( 176 | '--version', action='version', 177 | version=f'%(prog)s {__version__}' 178 | ) 179 | 180 | parser.add_argument( 181 | '-q', '-Q', '--quiet', action='store_true', default=False, 182 | help='Silently fail and pass, without output, unless improperly configured. ' 183 | '(default: False)' 184 | ) 185 | 186 | args = parser.parse_args() 187 | 188 | # If a gitlabci-lint config was specified via CLI, override the default search locations. 189 | hook_config_file_CLI = args.gitlabci_lint_config 190 | filesystem_config = config(*hook_config_file_CLI) if hook_config_file_CLI else config() 191 | 192 | # Parse a potential config file, with defaults / fallback values matching the CLI's defaults. 193 | gitlabci_lint_section = filesystem_config.get('gitlabci-lint', {}) 194 | quiet_CONF = gitlabci_lint_section.get('quiet', default_quiet) 195 | base_url_CONF = os.path.expandvars(gitlabci_lint_section.get('base-url', default_base_url)) 196 | project_id_CONF = os.path.expandvars(gitlabci_lint_section.get('project-id', '')) 197 | configs_CONF = list(map(os.path.expandvars, gitlabci_lint_section.get('configs', default_configs))) 198 | token_CONF = os.path.expandvars(gitlabci_lint_section.get('token', '')) 199 | 200 | quiet_CLI = args.quiet 201 | base_url_CLI = args.base_url 202 | configs_CLI = args.configs 203 | project_id_CLI = args.project_id 204 | 205 | # Decide which vars, configuration file or cli, take precendence. Basically how this logic works is, if a setting is 206 | # enabled in either the config file or via CLI args, it takes precedence. 207 | quiet = quiet_CONF == 'true' or quiet_CLI 208 | base_url = base_url_CLI if (base_url_CLI != default_base_url) else (base_url_CONF if (base_url_CONF != default_base_url) else base_url_CLI) 209 | configs = configs_CLI if (configs_CLI != default_configs) else (configs_CONF if (configs_CONF != default_configs) else configs_CLI) 210 | project_id = project_id_CLI if project_id_CLI else project_id_CONF 211 | if not project_id: 212 | errprint('ERROR: No project ID specified. Please specify a project ID as a command line argument or in the config file.') 213 | sys.exit(1) 214 | 215 | if not configs: 216 | # Set default, since there's a bug in argparse that I can't seem to overcome with specifying 217 | # multiple flags and defaults. 218 | configs = ['.gitlab-ci.yml'] 219 | 220 | if token_CONF: 221 | token = os.path.expandvars(token_CONF) 222 | elif not ((token := os.getenv('GITLABCI_LINT_TOKEN')) or (token := os.getenv('GITLAB_TOKEN'))): 223 | errprint('ERROR: Neither \'GITLABCI_LINT_TOKEN\' nor \'GITLAB_TOKEN\' set.') 224 | sys.exit(1) 225 | 226 | sys.exit(validateCiConfig(token, base_url, project_id, configs, quiet)) 227 | 228 | 229 | if __name__ == '__main__': 230 | main() -------------------------------------------------------------------------------- /tests/.gitlab-ci-example-1-FAIL.yml: -------------------------------------------------------------------------------- 1 | stages: [] 2 | 3 | # test 4 | 5 | hello-world: 6 | image: ${TEST_IMAGE} 7 | stage: test 8 | script: 9 | - printf "Hello, world!\\n" 10 | tags: 11 | - docker -------------------------------------------------------------------------------- /tests/.gitlab-ci-example-1-PASS.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - test 3 | 4 | # test 5 | 6 | hello-world: 7 | image: ${TEST_IMAGE} 8 | stage: test 9 | script: 10 | - printf "Hello, world!\\n" 11 | tags: 12 | - docker 13 | -------------------------------------------------------------------------------- /tests/.gitlab-ci-example-2-FAIL.yml: -------------------------------------------------------------------------------- 1 | stages: [stage] 2 | 3 | # test 4 | 5 | hello-world: 6 | image: ${TEST_IMAGE} 7 | stage: test 8 | script: 9 | - printf "Hello, world!\\n" 10 | tags: 11 | - docker 12 | -------------------------------------------------------------------------------- /tests/.gitlab-ci-example-3-FAIL.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - test 3 | 4 | # test 5 | 6 | hello-world: 7 | image: ${TEST_IMAGE} 8 | stage: test1 9 | script: 10 | - printf "Hello, world!\\n" 11 | tags: 12 | - docker 13 | -------------------------------------------------------------------------------- /tests/run-tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # Test all test cases in this directory. Files should end in PASS or FAIL to indicate the expected outcome. 3 | 4 | 5 | set -e 6 | 7 | 8 | ## 9 | # Generate all test bats files/cases to execute upon. 10 | generate_tests() 11 | { 12 | local tests 13 | mapfile -t tests < <(find ./tests/ -type f -name "*.yml") 14 | 15 | for f in "${tests[@]}"; do 16 | printf "%s\\n" "$f" 17 | f_base="$(basename "$f")" 18 | f_base_ext="${f_base%.*}" 19 | f_path="$(dirname "$f")" 20 | f_bats="${f_path}/${f_base_ext}.bats" 21 | 22 | if [ "$(echo "$f_base_ext" | awk '/(PASS)$/')" ]; then 23 | # PASS 24 | cat > "$f_bats" << EOFI 25 | #! /usr/bin/env bats 26 | 27 | @test "$f_base" { 28 | poetry run gitlabci-lint -c "$f" -p 53075218 29 | } 30 | EOFI 31 | else 32 | # FAIL 33 | cat > "$f_bats" << EOFI 34 | #! /usr/bin/env bats 35 | 36 | @test "$f_base" { 37 | ! poetry run gitlabci-lint -c "$f" -p 53075218 38 | } 39 | EOFI 40 | fi 41 | done 42 | } 43 | 44 | 45 | ## 46 | # Clean up generated bats test files from YAML examples. 47 | cleanup_tests() 48 | { 49 | rm ./tests/.*.bats 50 | } 51 | 52 | 53 | main() 54 | { 55 | generate_tests 56 | 57 | bats ./tests/.*.bats 58 | 59 | cleanup_tests 60 | } 61 | 62 | 63 | main 64 | unset -f main generate_tests cleanup_tests --------------------------------------------------------------------------------