| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 0daec5f commit 3b1ede4
8 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -15,6 +15,9 @@ Lint a CI YAML configuration from a string: | |||
| 15 | 15 | ||
| 16 | 16 | To see output, you will need to use the ``-v``/``--verbose`` flag. | |
| 17 | 17 | ||
| 18 | + To exit with non-zero on YAML lint failures instead, use the ``validate`` | ||
| 19 | + subcommand shown below. | ||
| 20 | + | ||
| 18 | 21 | .. code-block:: console | |
| 19 | 22 | ||
| 20 | 23 | $ gitlab --verbose ci-lint create --content \ | |
@@ -30,12 +33,24 @@ Lint a CI YAML configuration from a file (see :ref:`cli_from_files`): | |||
| 30 | 33 | ||
| 31 | 34 | $ gitlab --verbose ci-lint create --content @.gitlab-ci.yml | |
| 32 | 35 | ||
| 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 | + | ||
| 33 | 42 | Lint a project's CI YAML configuration: | |
| 34 | 43 | ||
| 35 | 44 | .. code-block:: console | |
| 36 | 45 | ||
| 37 | 46 | $ gitlab --verbose project-ci-lint create --project-id group/my-project --content @.gitlab-ci.yml | |
| 38 | 47 | ||
| 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 | + | ||
| 39 | 54 | Lint a project's current CI YAML configuration: | |
| 40 | 55 | ||
| 41 | 56 | .. code-block:: console | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -19,7 +19,7 @@ Reference | |||
| 19 | 19 | Examples | |
| 20 | 20 | --------- | |
| 21 | 21 | ||
| 22 | - Validate a CI YAML configuration:: | ||
| 22 | + Lint a CI YAML configuration:: | ||
| 23 | 23 | ||
| 24 | 24 | gitlab_ci_yml = """.api_test: | |
| 25 | 25 | rules: | |
@@ -40,14 +40,30 @@ Validate a CI YAML configuration:: | |||
| 40 | 40 | print(lint_result.status) # Print the status of the CI YAML | |
| 41 | 41 | print(lint_result.merged_yaml) # Print the merged YAML file | |
| 42 | 42 | ||
| 43 | - Validate a project's CI configuration:: | ||
| 43 | + Lint a project's CI configuration:: | ||
| 44 | 44 | ||
| 45 | 45 | lint_result = project.ci_lint.get() | |
| 46 | 46 | assert lint_result.valid is True # Test that the .gitlab-ci.yml is valid | |
| 47 | 47 | print(lint_result.merged_yaml) # Print the merged YAML file | |
| 48 | 48 | ||
| 49 | - Validate a CI YAML configuration with a namespace:: | ||
| 49 | + Lint a CI YAML configuration with a namespace:: | ||
| 50 | 50 | ||
| 51 | 51 | lint_result = project.ci_lint.create({"content": gitlab_ci_yml}) | |
| 52 | 52 | assert lint_result.valid is True # Test that the .gitlab-ci.yml is valid | |
| 53 | 53 | 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"}) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -62,6 +62,10 @@ class GitlabParsingError(GitlabError): | |||
| 62 | 62 | pass | |
| 63 | 63 | ||
| 64 | 64 | ||
| 65 | + class GitlabCiLintError(GitlabError): | ||
| 66 | + pass | ||
| 67 | + | ||
| 68 | + | ||
| 65 | 69 | class GitlabConnectionError(GitlabError): | |
| 66 | 70 | pass | |
| 67 | 71 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -25,6 +25,7 @@ | |||
| 25 | 25 | import gitlab.base | |
| 26 | 26 | import gitlab.v4.objects | |
| 27 | 27 | from gitlab import cli | |
| 28 | + from gitlab.exceptions import GitlabCiLintError | ||
| 28 | 29 | ||
| 29 | 30 | ||
| 30 | 31 | class GitlabCLI: | |
@@ -133,6 +134,16 @@ def do_project_export_download(self) -> None: | |||
| 133 | 134 | except Exception as e: # pragma: no cover, cli.die is unit-tested | |
| 134 | 135 | cli.die("Impossible to download the export", e) | |
| 135 | 136 | ||
| 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 | + | ||
| 136 | 147 | def do_create(self) -> gitlab.base.RESTObject: | |
| 137 | 148 | if TYPE_CHECKING: | |
| 138 | 149 | assert isinstance(self.mgr, gitlab.mixins.CreateMixin) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -6,6 +6,8 @@ | |||
| 6 | 6 | from typing import Any, cast | |
| 7 | 7 | ||
| 8 | 8 | from gitlab.base import RESTManager, RESTObject | |
| 9 | + from gitlab.cli import register_custom_action | ||
| 10 | + from gitlab.exceptions import GitlabCiLintError | ||
| 9 | 11 | from gitlab.mixins import CreateMixin, GetWithoutIdMixin | |
| 10 | 12 | from gitlab.types import RequiredOptional | |
| 11 | 13 | ||
@@ -28,9 +30,24 @@ class CiLintManager(CreateMixin, RESTManager): | |||
| 28 | 30 | required=("content",), optional=("include_merged_yaml", "include_jobs") | |
| 29 | 31 | ) | |
| 30 | 32 | ||
| 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 | + | ||
| 31 | 48 | ||
| 32 | 49 | class ProjectCiLint(RESTObject): | |
| 33 | - pass | ||
| 50 | + _id_attr = None | ||
| 34 | 51 | ||
| 35 | 52 | ||
| 36 | 53 | class ProjectCiLintManager(GetWithoutIdMixin, CreateMixin, RESTManager): | |
@@ -43,3 +60,18 @@ class ProjectCiLintManager(GetWithoutIdMixin, CreateMixin, RESTManager): | |||
| 43 | 60 | ||
| 44 | 61 | def get(self, **kwargs: Any) -> ProjectCiLint: | |
| 45 | 62 | 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) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -4,3 +4,16 @@ | |||
| 4 | 4 | @pytest.fixture(scope="session") | |
| 5 | 5 | def test_dir(pytestconfig): | |
| 6 | 6 | 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" | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -22,6 +22,59 @@ def test_update_project(gitlab_cli, project): | |||
| 22 | 22 | assert description in ret.stdout | |
| 23 | 23 | ||
| 24 | 24 | ||
| 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 | + | ||
| 25 | 78 | def test_create_group(gitlab_cli): | |
| 26 | 79 | name = "test-group1" | |
| 27 | 80 | path = "group1" | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,12 +1,14 @@ | |||
| 1 | 1 | import pytest | |
| 2 | 2 | import responses | |
| 3 | 3 | ||
| 4 | - gitlab_ci_yml = """--- | ||
| 5 | - :test_job: | ||
| 6 | - :script: echo 1 | ||
| 7 | - """ | ||
| 4 | + from gitlab import exceptions | ||
| 8 | 5 | ||
| 9 | 6 | ci_lint_create_content = {"status": "valid", "errors": [], "warnings": []} | |
| 7 | + ci_lint_create_invalid_content = { | ||
| 8 | + "status": "invalid", | ||
| 9 | + "errors": ["invalid format"], | ||
| 10 | + "warnings": [], | ||
| 11 | + } | ||
| 10 | 12 | ||
| 11 | 13 | ||
| 12 | 14 | project_ci_lint_content = { | |
@@ -30,6 +32,19 @@ def resp_create_ci_lint(): | |||
| 30 | 32 | yield rsps | |
| 31 | 33 | ||
| 32 | 34 | ||
| 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 | + | ||
| 33 | 48 | @pytest.fixture | |
| 34 | 49 | def resp_get_project_ci_lint(): | |
| 35 | 50 | with responses.RequestsMock() as rsps: | |
@@ -56,16 +71,29 @@ def resp_create_project_ci_lint(): | |||
| 56 | 71 | yield rsps | |
| 57 | 72 | ||
| 58 | 73 | ||
| 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}) | ||
| 61 | 76 | assert lint_result.status == "valid" | |
| 62 | 77 | ||
| 63 | 78 | ||
| 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 | + | ||
| 64 | 90 | def test_project_ci_lint_get(project, resp_get_project_ci_lint): | |
| 65 | 91 | lint_result = project.ci_lint.get() | |
| 66 | 92 | assert lint_result.valid is True | |
| 67 | 93 | ||
| 68 | 94 | ||
| 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}) | ||
| 71 | 99 | assert lint_result.valid is True | |
| Back | FazBrowse Home | New Git URL |
0 commit comments