Skip to content

Commit

Permalink
feat: support validating CI lint results
Browse files Browse the repository at this point in the history
  • Loading branch information
nejch authored and JohnVillalovos committed Jul 9, 2022
1 parent 0daec5f commit 3b1ede4
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 12 deletions.
15 changes: 15 additions & 0 deletions docs/cli-examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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
Expand Down
22 changes: 19 additions & 3 deletions docs/gl_objects/ci_lint.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Reference
Examples
---------

Validate a CI YAML configuration::
Lint a CI YAML configuration::

gitlab_ci_yml = """.api_test:
rules:
Expand All @@ -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"})
4 changes: 4 additions & 0 deletions gitlab/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ class GitlabParsingError(GitlabError):
pass


class GitlabCiLintError(GitlabError):
pass


class GitlabConnectionError(GitlabError):
pass

Expand Down
11 changes: 11 additions & 0 deletions gitlab/v4/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import gitlab.base
import gitlab.v4.objects
from gitlab import cli
from gitlab.exceptions import GitlabCiLintError


class GitlabCLI:
Expand Down Expand Up @@ -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)
Expand Down
34 changes: 33 additions & 1 deletion gitlab/v4/objects/ci_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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):
Expand All @@ -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)
13 changes: 13 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
53 changes: 53 additions & 0 deletions tests/functional/cli/test_cli_v4.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
44 changes: 36 additions & 8 deletions tests/unit/objects/test_ci_lint.py
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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:
Expand All @@ -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

0 comments on commit 3b1ede4

Please sign in to comment.