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)
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