Skip to content

Commit 3b1ede4

Browse files
nejchJohnVillalovos
authored andcommitted
feat: support validating CI lint results
1 parent 0daec5f commit 3b1ede4

File tree

8 files changed

+184
-12
lines changed

8 files changed

+184
-12
lines changed

docs/cli-examples.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ Lint a CI YAML configuration from a string:
1515

1616
To see output, you will need to use the ``-v``/``--verbose`` flag.
1717

18+
To exit with non-zero on YAML lint failures instead, use the ``validate``
19+
subcommand shown below.
20+
1821
.. code-block:: console
1922
2023
$ gitlab --verbose ci-lint create --content \
@@ -30,12 +33,24 @@ Lint a CI YAML configuration from a file (see :ref:`cli_from_files`):
3033
3134
$ gitlab --verbose ci-lint create --content @.gitlab-ci.yml
3235
36+
Validate a CI YAML configuration from a file (lints and exits with non-zero on failure):
37+
38+
.. code-block:: console
39+
40+
$ gitlab ci-lint validate --content @.gitlab-ci.yml
41+
3342
Lint a project's CI YAML configuration:
3443

3544
.. code-block:: console
3645
3746
$ gitlab --verbose project-ci-lint create --project-id group/my-project --content @.gitlab-ci.yml
3847
48+
Validate a project's CI YAML configuration (lints and exits with non-zero on failure):
49+
50+
.. code-block:: console
51+
52+
$ gitlab project-ci-lint validate --project-id group/my-project --content @.gitlab-ci.yml
53+
3954
Lint a project's current CI YAML configuration:
4055

4156
.. code-block:: console

docs/gl_objects/ci_lint.rst

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Reference
1919
Examples
2020
---------
2121

22-
Validate a CI YAML configuration::
22+
Lint a CI YAML configuration::
2323

2424
gitlab_ci_yml = """.api_test:
2525
rules:
@@ -40,14 +40,30 @@ Validate a CI YAML configuration::
4040
print(lint_result.status) # Print the status of the CI YAML
4141
print(lint_result.merged_yaml) # Print the merged YAML file
4242

43-
Validate a project's CI configuration::
43+
Lint a project's CI configuration::
4444

4545
lint_result = project.ci_lint.get()
4646
assert lint_result.valid is True # Test that the .gitlab-ci.yml is valid
4747
print(lint_result.merged_yaml) # Print the merged YAML file
4848

49-
Validate a CI YAML configuration with a namespace::
49+
Lint a CI YAML configuration with a namespace::
5050

5151
lint_result = project.ci_lint.create({"content": gitlab_ci_yml})
5252
assert lint_result.valid is True # Test that the .gitlab-ci.yml is valid
5353
print(lint_result.merged_yaml) # Print the merged YAML file
54+
55+
Validate a CI YAML configuration (raises ``GitlabCiLintError`` on failures)::
56+
57+
# returns None
58+
gl.ci_lint.validate({"content": gitlab_ci_yml})
59+
60+
# raises GitlabCiLintError
61+
gl.ci_lint.validate({"content": "invalid"})
62+
63+
Validate a CI YAML configuration with a namespace::
64+
65+
# returns None
66+
project.ci_lint.validate({"content": gitlab_ci_yml})
67+
68+
# raises GitlabCiLintError
69+
project.ci_lint.validate({"content": "invalid"})

gitlab/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ class GitlabParsingError(GitlabError):
6262
pass
6363

6464

65+
class GitlabCiLintError(GitlabError):
66+
pass
67+
68+
6569
class GitlabConnectionError(GitlabError):
6670
pass
6771

gitlab/v4/cli.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import gitlab.base
2626
import gitlab.v4.objects
2727
from gitlab import cli
28+
from gitlab.exceptions import GitlabCiLintError
2829

2930

3031
class GitlabCLI:
@@ -133,6 +134,16 @@ def do_project_export_download(self) -> None:
133134
except Exception as e: # pragma: no cover, cli.die is unit-tested
134135
cli.die("Impossible to download the export", e)
135136

137+
def do_validate(self) -> None:
138+
if TYPE_CHECKING:
139+
assert isinstance(self.mgr, gitlab.v4.objects.CiLintManager)
140+
try:
141+
self.mgr.validate(self.args)
142+
except GitlabCiLintError as e: # pragma: no cover, cli.die is unit-tested
143+
cli.die("CI YAML Lint failed", e)
144+
except Exception as e: # pragma: no cover, cli.die is unit-tested
145+
cli.die("Cannot validate CI YAML", e)
146+
136147
def do_create(self) -> gitlab.base.RESTObject:
137148
if TYPE_CHECKING:
138149
assert isinstance(self.mgr, gitlab.mixins.CreateMixin)

gitlab/v4/objects/ci_lint.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from typing import Any, cast
77

88
from gitlab.base import RESTManager, RESTObject
9+
from gitlab.cli import register_custom_action
10+
from gitlab.exceptions import GitlabCiLintError
911
from gitlab.mixins import CreateMixin, GetWithoutIdMixin
1012
from gitlab.types import RequiredOptional
1113

@@ -28,9 +30,24 @@ class CiLintManager(CreateMixin, RESTManager):
2830
required=("content",), optional=("include_merged_yaml", "include_jobs")
2931
)
3032

33+
@register_custom_action(
34+
"CiLintManager",
35+
("content",),
36+
optional=("include_merged_yaml", "include_jobs"),
37+
)
38+
def validate(self, *args: Any, **kwargs: Any) -> None:
39+
"""Raise an error if the CI Lint results are not valid.
40+
41+
This is a custom python-gitlab method to wrap lint endpoints."""
42+
result = self.create(*args, **kwargs)
43+
44+
if result.status != "valid":
45+
message = ",\n".join(result.errors)
46+
raise GitlabCiLintError(message)
47+
3148

3249
class ProjectCiLint(RESTObject):
33-
pass
50+
_id_attr = None
3451

3552

3653
class ProjectCiLintManager(GetWithoutIdMixin, CreateMixin, RESTManager):
@@ -43,3 +60,18 @@ class ProjectCiLintManager(GetWithoutIdMixin, CreateMixin, RESTManager):
4360

4461
def get(self, **kwargs: Any) -> ProjectCiLint:
4562
return cast(ProjectCiLint, super().get(**kwargs))
63+
64+
@register_custom_action(
65+
"ProjectCiLintManager",
66+
("content",),
67+
optional=("dry_run", "include_jobs", "ref"),
68+
)
69+
def validate(self, *args: Any, **kwargs: Any) -> None:
70+
"""Raise an error if the Project CI Lint results are not valid.
71+
72+
This is a custom python-gitlab method to wrap lint endpoints."""
73+
result = self.create(*args, **kwargs)
74+
75+
if not result.valid:
76+
message = ",\n".join(result.errors)
77+
raise GitlabCiLintError(message)

tests/conftest.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,16 @@
44
@pytest.fixture(scope="session")
55
def test_dir(pytestconfig):
66
return pytestconfig.rootdir / "tests"
7+
8+
9+
@pytest.fixture
10+
def valid_gitlab_ci_yml():
11+
return """---
12+
:test_job:
13+
:script: echo 1
14+
"""
15+
16+
17+
@pytest.fixture
18+
def invalid_gitlab_ci_yml():
19+
return "invalid"

tests/functional/cli/test_cli_v4.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,59 @@ def test_update_project(gitlab_cli, project):
2222
assert description in ret.stdout
2323

2424

25+
def test_create_ci_lint(gitlab_cli, valid_gitlab_ci_yml):
26+
cmd = ["ci-lint", "create", "--content", valid_gitlab_ci_yml]
27+
ret = gitlab_cli(cmd)
28+
29+
assert ret.success
30+
31+
32+
def test_validate_ci_lint(gitlab_cli, valid_gitlab_ci_yml):
33+
cmd = ["ci-lint", "validate", "--content", valid_gitlab_ci_yml]
34+
ret = gitlab_cli(cmd)
35+
36+
assert ret.success
37+
38+
39+
def test_validate_ci_lint_invalid_exits_non_zero(gitlab_cli, invalid_gitlab_ci_yml):
40+
cmd = ["ci-lint", "validate", "--content", invalid_gitlab_ci_yml]
41+
ret = gitlab_cli(cmd)
42+
43+
assert not ret.success
44+
assert "CI YAML Lint failed (Invalid configuration format)" in ret.stderr
45+
46+
47+
def test_validate_project_ci_lint(gitlab_cli, project, valid_gitlab_ci_yml):
48+
cmd = [
49+
"project-ci-lint",
50+
"validate",
51+
"--project-id",
52+
project.id,
53+
"--content",
54+
valid_gitlab_ci_yml,
55+
]
56+
ret = gitlab_cli(cmd)
57+
58+
assert ret.success
59+
60+
61+
def test_validate_project_ci_lint_invalid_exits_non_zero(
62+
gitlab_cli, project, invalid_gitlab_ci_yml
63+
):
64+
cmd = [
65+
"project-ci-lint",
66+
"validate",
67+
"--project-id",
68+
project.id,
69+
"--content",
70+
invalid_gitlab_ci_yml,
71+
]
72+
ret = gitlab_cli(cmd)
73+
74+
assert not ret.success
75+
assert "CI YAML Lint failed (Invalid configuration format)" in ret.stderr
76+
77+
2578
def test_create_group(gitlab_cli):
2679
name = "test-group1"
2780
path = "group1"

tests/unit/objects/test_ci_lint.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import pytest
22
import responses
33

4-
gitlab_ci_yml = """---
5-
:test_job:
6-
:script: echo 1
7-
"""
4+
from gitlab import exceptions
85

96
ci_lint_create_content = {"status": "valid", "errors": [], "warnings": []}
7+
ci_lint_create_invalid_content = {
8+
"status": "invalid",
9+
"errors": ["invalid format"],
10+
"warnings": [],
11+
}
1012

1113

1214
project_ci_lint_content = {
@@ -30,6 +32,19 @@ def resp_create_ci_lint():
3032
yield rsps
3133

3234

35+
@pytest.fixture
36+
def resp_create_ci_lint_invalid():
37+
with responses.RequestsMock() as rsps:
38+
rsps.add(
39+
method=responses.POST,
40+
url="http://localhost/api/v4/ci/lint",
41+
json=ci_lint_create_invalid_content,
42+
content_type="application/json",
43+
status=200,
44+
)
45+
yield rsps
46+
47+
3348
@pytest.fixture
3449
def resp_get_project_ci_lint():
3550
with responses.RequestsMock() as rsps:
@@ -56,16 +71,29 @@ def resp_create_project_ci_lint():
5671
yield rsps
5772

5873

59-
def test_ci_lint_create(gl, resp_create_ci_lint):
60-
lint_result = gl.ci_lint.create({"content": gitlab_ci_yml})
74+
def test_ci_lint_create(gl, resp_create_ci_lint, valid_gitlab_ci_yml):
75+
lint_result = gl.ci_lint.create({"content": valid_gitlab_ci_yml})
6176
assert lint_result.status == "valid"
6277

6378

79+
def test_ci_lint_validate(gl, resp_create_ci_lint, valid_gitlab_ci_yml):
80+
gl.ci_lint.validate({"content": valid_gitlab_ci_yml})
81+
82+
83+
def test_ci_lint_validate_invalid_raises(
84+
gl, resp_create_ci_lint_invalid, invalid_gitlab_ci_yml
85+
):
86+
with pytest.raises(exceptions.GitlabCiLintError, match="invalid format"):
87+
gl.ci_lint.validate({"content": invalid_gitlab_ci_yml})
88+
89+
6490
def test_project_ci_lint_get(project, resp_get_project_ci_lint):
6591
lint_result = project.ci_lint.get()
6692
assert lint_result.valid is True
6793

6894

69-
def test_project_ci_lint_create(project, resp_create_project_ci_lint):
70-
lint_result = project.ci_lint.create({"content": gitlab_ci_yml})
95+
def test_project_ci_lint_create(
96+
project, resp_create_project_ci_lint, valid_gitlab_ci_yml
97+
):
98+
lint_result = project.ci_lint.create({"content": valid_gitlab_ci_yml})
7199
assert lint_result.valid is True

0 commit comments

Comments
 (0)