From f3a53aa0d15994184bc83fbb83924b75f89356f5 Mon Sep 17 00:00:00 2001 From: Nejc Habjan Date: Sat, 9 Jul 2022 13:56:33 +0200 Subject: [PATCH] feat: support validating CI lint results --- docs/cli-examples.rst | 15 ++++++++ docs/gl_objects/ci_lint.rst | 22 ++++++++++-- gitlab/exceptions.py | 4 +++ gitlab/v4/cli.py | 11 ++++++ gitlab/v4/objects/ci_lint.py | 34 +++++++++++++++++- tests/conftest.py | 13 +++++++ tests/functional/cli/test_cli_v4.py | 53 +++++++++++++++++++++++++++++ tests/unit/objects/test_ci_lint.py | 44 +++++++++++++++++++----- 8 files changed, 184 insertions(+), 12 deletions(-) diff --git a/docs/cli-examples.rst b/docs/cli-examples.rst index 7f21f0308..6c4364f8a 100644 --- a/docs/cli-examples.rst +++ b/docs/cli-examples.rst @@ -15,6 +15,9 @@ Lint a CI YAML configuration from a string: To see output, you will need to use the ``-v``/``--verbose`` flag. + To exit with non-zero on YAML lint failures instead, use the ``validate`` + subcommand shown below. + .. code-block:: console $ gitlab --verbose ci-lint create --content \ @@ -30,12 +33,24 @@ Lint a CI YAML configuration from a file (see :ref:`cli_from_files`): $ gitlab --verbose ci-lint create --content @.gitlab-ci.yml +Validate a CI YAML configuration from a file (lints and exits with non-zero on failure): + +.. code-block:: console + + $ gitlab ci-lint validate --content @.gitlab-ci.yml + Lint a project's CI YAML configuration: .. code-block:: console $ gitlab --verbose project-ci-lint create --project-id group/my-project --content @.gitlab-ci.yml +Validate a project's CI YAML configuration (lints and exits with non-zero on failure): + +.. code-block:: console + + $ gitlab project-ci-lint validate --project-id group/my-project --content @.gitlab-ci.yml + Lint a project's current CI YAML configuration: .. code-block:: console diff --git a/docs/gl_objects/ci_lint.rst b/docs/gl_objects/ci_lint.rst index 6533db310..ad2d875e9 100644 --- a/docs/gl_objects/ci_lint.rst +++ b/docs/gl_objects/ci_lint.rst @@ -19,7 +19,7 @@ Reference Examples --------- -Validate a CI YAML configuration:: +Lint a CI YAML configuration:: gitlab_ci_yml = """.api_test: rules: @@ -40,14 +40,30 @@ Validate a CI YAML configuration:: print(lint_result.status) # Print the status of the CI YAML print(lint_result.merged_yaml) # Print the merged YAML file -Validate a project's CI configuration:: +Lint a project's CI configuration:: lint_result = project.ci_lint.get() assert lint_result.valid is True # Test that the .gitlab-ci.yml is valid print(lint_result.merged_yaml) # Print the merged YAML file -Validate a CI YAML configuration with a namespace:: +Lint a CI YAML configuration with a namespace:: lint_result = project.ci_lint.create({"content": gitlab_ci_yml}) assert lint_result.valid is True # Test that the .gitlab-ci.yml is valid print(lint_result.merged_yaml) # Print the merged YAML file + +Validate a CI YAML configuration (raises ``GitlabCiLintError`` on failures):: + + # returns None + gl.ci_lint.validate({"content": gitlab_ci_yml}) + + # raises GitlabCiLintError + gl.ci_lint.validate({"content": "invalid"}) + +Validate a CI YAML configuration with a namespace:: + + # returns None + project.ci_lint.validate({"content": gitlab_ci_yml}) + + # raises GitlabCiLintError + project.ci_lint.validate({"content": "invalid"}) diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py index 8465838e2..4a2f1dc6d 100644 --- a/gitlab/exceptions.py +++ b/gitlab/exceptions.py @@ -62,6 +62,10 @@ class GitlabParsingError(GitlabError): pass +class GitlabCiLintError(GitlabError): + pass + + class GitlabConnectionError(GitlabError): pass diff --git a/gitlab/v4/cli.py b/gitlab/v4/cli.py index d9035278e..b496669e4 100644 --- a/gitlab/v4/cli.py +++ b/gitlab/v4/cli.py @@ -25,6 +25,7 @@ import gitlab.base import gitlab.v4.objects from gitlab import cli +from gitlab.exceptions import GitlabCiLintError class GitlabCLI: @@ -133,6 +134,16 @@ def do_project_export_download(self) -> None: except Exception as e: # pragma: no cover, cli.die is unit-tested cli.die("Impossible to download the export", e) + def do_validate(self) -> None: + if TYPE_CHECKING: + assert isinstance(self.mgr, gitlab.v4.objects.CiLintManager) + try: + self.mgr.validate(self.args) + except GitlabCiLintError as e: # pragma: no cover, cli.die is unit-tested + cli.die("CI YAML Lint failed", e) + except Exception as e: # pragma: no cover, cli.die is unit-tested + cli.die("Cannot validate CI YAML", e) + def do_create(self) -> gitlab.base.RESTObject: if TYPE_CHECKING: assert isinstance(self.mgr, gitlab.mixins.CreateMixin) diff --git a/gitlab/v4/objects/ci_lint.py b/gitlab/v4/objects/ci_lint.py index 73f9d4d9d..e6b459ccd 100644 --- a/gitlab/v4/objects/ci_lint.py +++ b/gitlab/v4/objects/ci_lint.py @@ -6,6 +6,8 @@ from typing import Any, cast from gitlab.base import RESTManager, RESTObject +from gitlab.cli import register_custom_action +from gitlab.exceptions import GitlabCiLintError from gitlab.mixins import CreateMixin, GetWithoutIdMixin from gitlab.types import RequiredOptional @@ -28,9 +30,24 @@ class CiLintManager(CreateMixin, RESTManager): required=("content",), optional=("include_merged_yaml", "include_jobs") ) + @register_custom_action( + "CiLintManager", + ("content",), + optional=("include_merged_yaml", "include_jobs"), + ) + def validate(self, *args: Any, **kwargs: Any) -> None: + """Raise an error if the CI Lint results are not valid. + + This is a custom python-gitlab method to wrap lint endpoints.""" + result = self.create(*args, **kwargs) + + if result.status != "valid": + message = ",\n".join(result.errors) + raise GitlabCiLintError(message) + class ProjectCiLint(RESTObject): - pass + _id_attr = None class ProjectCiLintManager(GetWithoutIdMixin, CreateMixin, RESTManager): @@ -43,3 +60,18 @@ class ProjectCiLintManager(GetWithoutIdMixin, CreateMixin, RESTManager): def get(self, **kwargs: Any) -> ProjectCiLint: return cast(ProjectCiLint, super().get(**kwargs)) + + @register_custom_action( + "ProjectCiLintManager", + ("content",), + optional=("dry_run", "include_jobs", "ref"), + ) + def validate(self, *args: Any, **kwargs: Any) -> None: + """Raise an error if the Project CI Lint results are not valid. + + This is a custom python-gitlab method to wrap lint endpoints.""" + result = self.create(*args, **kwargs) + + if not result.valid: + message = ",\n".join(result.errors) + raise GitlabCiLintError(message) diff --git a/tests/conftest.py b/tests/conftest.py index 12b573f60..fdcafee7a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,3 +4,16 @@ @pytest.fixture(scope="session") def test_dir(pytestconfig): return pytestconfig.rootdir / "tests" + + +@pytest.fixture +def valid_gitlab_ci_yml(): + return """--- +:test_job: + :script: echo 1 +""" + + +@pytest.fixture +def invalid_gitlab_ci_yml(): + return "invalid" diff --git a/tests/functional/cli/test_cli_v4.py b/tests/functional/cli/test_cli_v4.py index da649577b..6b4373062 100644 --- a/tests/functional/cli/test_cli_v4.py +++ b/tests/functional/cli/test_cli_v4.py @@ -22,6 +22,59 @@ def test_update_project(gitlab_cli, project): assert description in ret.stdout +def test_create_ci_lint(gitlab_cli, valid_gitlab_ci_yml): + cmd = ["ci-lint", "create", "--content", valid_gitlab_ci_yml] + ret = gitlab_cli(cmd) + + assert ret.success + + +def test_validate_ci_lint(gitlab_cli, valid_gitlab_ci_yml): + cmd = ["ci-lint", "validate", "--content", valid_gitlab_ci_yml] + ret = gitlab_cli(cmd) + + assert ret.success + + +def test_validate_ci_lint_invalid_exits_non_zero(gitlab_cli, invalid_gitlab_ci_yml): + cmd = ["ci-lint", "validate", "--content", invalid_gitlab_ci_yml] + ret = gitlab_cli(cmd) + + assert not ret.success + assert "CI YAML Lint failed (Invalid configuration format)" in ret.stderr + + +def test_validate_project_ci_lint(gitlab_cli, project, valid_gitlab_ci_yml): + cmd = [ + "project-ci-lint", + "validate", + "--project-id", + project.id, + "--content", + valid_gitlab_ci_yml, + ] + ret = gitlab_cli(cmd) + + assert ret.success + + +def test_validate_project_ci_lint_invalid_exits_non_zero( + gitlab_cli, project, invalid_gitlab_ci_yml +): + cmd = [ + "project-ci-lint", + "validate", + "--project-id", + project.id, + "--content", + invalid_gitlab_ci_yml, + ] + ret = gitlab_cli(cmd) + + assert not ret.success + assert "CI YAML Lint failed (Invalid configuration format)" in ret.stderr + + def test_create_group(gitlab_cli): name = "test-group1" path = "group1" diff --git a/tests/unit/objects/test_ci_lint.py b/tests/unit/objects/test_ci_lint.py index 509a5ed1b..76281f1e2 100644 --- a/tests/unit/objects/test_ci_lint.py +++ b/tests/unit/objects/test_ci_lint.py @@ -1,12 +1,14 @@ import pytest import responses -gitlab_ci_yml = """--- -:test_job: - :script: echo 1 -""" +from gitlab import exceptions ci_lint_create_content = {"status": "valid", "errors": [], "warnings": []} +ci_lint_create_invalid_content = { + "status": "invalid", + "errors": ["invalid format"], + "warnings": [], +} project_ci_lint_content = { @@ -30,6 +32,19 @@ def resp_create_ci_lint(): yield rsps +@pytest.fixture +def resp_create_ci_lint_invalid(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/ci/lint", + json=ci_lint_create_invalid_content, + content_type="application/json", + status=200, + ) + yield rsps + + @pytest.fixture def resp_get_project_ci_lint(): with responses.RequestsMock() as rsps: @@ -56,16 +71,29 @@ def resp_create_project_ci_lint(): yield rsps -def test_ci_lint_create(gl, resp_create_ci_lint): - lint_result = gl.ci_lint.create({"content": gitlab_ci_yml}) +def test_ci_lint_create(gl, resp_create_ci_lint, valid_gitlab_ci_yml): + lint_result = gl.ci_lint.create({"content": valid_gitlab_ci_yml}) assert lint_result.status == "valid" +def test_ci_lint_validate(gl, resp_create_ci_lint, valid_gitlab_ci_yml): + gl.ci_lint.validate({"content": valid_gitlab_ci_yml}) + + +def test_ci_lint_validate_invalid_raises( + gl, resp_create_ci_lint_invalid, invalid_gitlab_ci_yml +): + with pytest.raises(exceptions.GitlabCiLintError, match="invalid format"): + gl.ci_lint.validate({"content": invalid_gitlab_ci_yml}) + + def test_project_ci_lint_get(project, resp_get_project_ci_lint): lint_result = project.ci_lint.get() assert lint_result.valid is True -def test_project_ci_lint_create(project, resp_create_project_ci_lint): - lint_result = project.ci_lint.create({"content": gitlab_ci_yml}) +def test_project_ci_lint_create( + project, resp_create_project_ci_lint, valid_gitlab_ci_yml +): + lint_result = project.ci_lint.create({"content": valid_gitlab_ci_yml}) assert lint_result.valid is True pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy