diff --git a/jenkins/__init__.py b/jenkins/__init__.py index 6bafcac..a1fb535 100755 --- a/jenkins/__init__.py +++ b/jenkins/__init__.py @@ -110,6 +110,7 @@ CONFIG_JOB = '%(folder_url)sjob/%(short_name)s/config.xml' DELETE_JOB = '%(folder_url)sjob/%(short_name)s/doDelete' ENABLE_JOB = '%(folder_url)sjob/%(short_name)s/enable' DISABLE_JOB = '%(folder_url)sjob/%(short_name)s/disable' +CHECK_JENKINSFILE_SYNTAX = 'pipeline-model-converter/validateJenkinsfile' SET_JOB_BUILD_NUMBER = '%(folder_url)sjob/%(short_name)s/nextbuildnumber/submit' COPY_JOB = '%(from_folder_url)screateItem?name=%(to_short_name)s&mode=copy&from=%(from_short_name)s' RENAME_JOB = '%(from_folder_url)sjob/%(from_short_name)s/doRename?newName=%(to_short_name)s' @@ -1182,6 +1183,22 @@ class Jenkins(object): else: self.create_job(name, config_xml) + def check_jenkinsfile_syntax(self, jenkinsfile): + '''Checks if a Pipeline Jenkinsfile has a valid syntax + + :param jenkinsfile: Jenkinsfile text, ``str`` + :returns: List of errors in the Jenkinsfile. Empty list if no errors. + ''' + # https://jenkins.io/doc/book/pipeline/development/#linter + # JENKINS_CRUMB=`curl "$JENKINS_URL/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,\":\",//crumb)" + # curl -X POST -H $JENKINS_CRUMB -F "jenkinsfile=<Jenkinsfile" $JENKINS_URL/pipeline-model-converter/val + url = self._build_url(CHECK_JENKINSFILE_SYNTAX, locals()) + the_data = { + "jenkinsfile": jenkinsfile + } + response = self.jenkins_request(requests.Request('POST', url, data=the_data)) + return response.json().get("data", {}).get("errors", []) + def create_job(self, name, config_xml): '''Create a new Jenkins job diff --git a/tests/helper.py b/tests/helper.py index f6f57cc..5511c54 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -104,5 +104,6 @@ def build_response_mock(status_code, json_body=None, headers=None, # and accessed by key response.headers = real_response.headers response.content = text + response.json = lambda: json_body return response diff --git a/tests/test_check_jenkinsfile_syntax.py b/tests/test_check_jenkinsfile_syntax.py new file mode 100644 index 0000000..700d745 --- /dev/null +++ b/tests/test_check_jenkinsfile_syntax.py @@ -0,0 +1,65 @@ +from mock import patch + +from tests.base import JenkinsTestBase +from tests.helper import build_response_mock + + +class JenkinsCheckJenkinsfileSyntax(JenkinsTestBase): + + @patch('jenkins.requests.Session.send', autospec=True) + def test_check_syntax_of_a_valid_file(self, session_send_mock): + + valid_jenkinsfile_content = """ + pipeline { + agent any ; + stages { + stage("blah") + { + stexps { + sh("pwd") + } + } + } + } + """ + + valid_response = { + "status": "ok", + "data": { + "result": "success" + } + } + session_send_mock.side_effect = iter([ + build_response_mock(200, self.crumb_data), # crumb + build_response_mock(200, valid_response) # request + ]) + response = self.j.check_jenkinsfile_syntax(valid_jenkinsfile_content) + self.assertEqual(response, []) + + @patch('jenkins.requests.Session.send', autospec=True) + def test_check_syntax_of_an_invalid_file(self, session_send_mock): + + invalid_jenkinsfile_content = """node { "pwd" } """ + + invalid_response = { + "status": "ok", + "data": { + "result": "failure", + "errors": [ + { + "error": [ + "Unknown stage section \"stexps\". Starting with version 0.5, steps in a stage must be" + + " in a \"steps\" block. @ line 5, column 9.", + "Expected one of \"steps\", \"stages\", or \"parallel\" for stage \"blah\" @ line 5, column 9." + ] + } + ] + } + } + + session_send_mock.side_effect = iter([ + build_response_mock(200, self.crumb_data), # crumb + build_response_mock(200, invalid_response) # request + ]) + response = self.j.check_jenkinsfile_syntax(invalid_jenkinsfile_content) + self.assertEqual(response, invalid_response["data"]["errors"])