diff --git a/CHANGELOG b/CHANGELOG index 2af2056979d6fe3909903d2cddb3e81c4ec61fd2..bf1136afd03ef51f431cfa1de2445383d1e8025b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -92,6 +92,7 @@ v 8.12.0 (unreleased) - Refactor the triggers page and documentation !6217 - Show values of CI trigger variables only when clicked (Katarzyna Kobierska Ula Budziszewska) - Use default clone protocol on "check out, review, and merge locally" help page URL + - API for Ci Lint !5953 (Katarzyna Kobierska Urszula Budziszewska) v 8.11.5 (unreleased) - Optimize branch lookups and force a repository reload for Repository#find_branch diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb index a7af3cb83450b1e6d5fc23559345d7f4accee53f..e06d12cfce16ce5fe0784fb7566045a28341630e 100644 --- a/app/controllers/ci/lints_controller.rb +++ b/app/controllers/ci/lints_controller.rb @@ -7,19 +7,14 @@ module Ci def create @content = params[:content] + @error = Ci::GitlabCiYamlProcessor.validation_message(@content) + @status = @error.blank? - if @content.blank? - @status = false - @error = "Please provide content of .gitlab-ci.yml" - else + if @error.blank? @config_processor = Ci::GitlabCiYamlProcessor.new(@content) @stages = @config_processor.stages @builds = @config_processor.builds - @status = true end - rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e - @error = e.message - @status = false rescue @error = 'Undefined error' @status = false diff --git a/doc/api/README.md b/doc/api/README.md index 96d94e08487a35be96d788cde79a5f9ab13e037f..e12070dc1cebb28e96992bff365c42f402c85fe4 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -41,8 +41,9 @@ following locations: - [Sidekiq metrics](sidekiq_metrics.md) - [System Hooks](system_hooks.md) - [Tags](tags.md) -- [Users](users.md) - [Todos](todos.md) +- [Users](users.md) +- [Validate CI configuration](ci/lint.md) ### Internal CI API diff --git a/doc/api/ci/lint.md b/doc/api/ci/lint.md new file mode 100644 index 0000000000000000000000000000000000000000..0c96b3ee335778cb1d7c11fb14d86098fde4aec4 --- /dev/null +++ b/doc/api/ci/lint.md @@ -0,0 +1,49 @@ +# Validate the .gitlab-ci.yml + +> [Introduced][ce-5953] in GitLab 8.12. + +Checks if your .gitlab-ci.yml file is valid. + +``` +POST ci/lint +``` + +| Attribute | Type | Required | Description | +| ---------- | ------- | -------- | -------- | +| `content` | string | yes | the .gitlab-ci.yaml content| + +```bash +curl --header "Content-Type: application/json" https://gitlab.example.com/api/v3/ci/lint --data '{"content": "{ \"image\": \"ruby:2.1\", \"services\": [\"postgres\"], \"before_script\": [\"gem install bundler\", \"bundle install\", \"bundle exec rake db:create\"], \"variables\": {\"DB_NAME\": \"postgres\"}, \"types\": [\"test\", \"deploy\", \"notify\"], \"rspec\": { \"script\": \"rake spec\", \"tags\": [\"ruby\", \"postgres\"], \"only\": [\"branches\"]}}"}' +``` + +Be sure to copy paste the exact contents of `.gitlab-ci.yml` as YAML is very picky about indentation and spaces. + +Example responses: + +* Valid content: + + ```json + { + "status": "valid", + "errors": [] + } + ``` + +* Invalid content: + + ```json + { + "status": "invalid", + "errors": [ + "variables config should be a hash of key value pairs" + ] + } + ``` + +* Without the content attribute: + + ```json + { + "error": "content is missing" + } + ``` diff --git a/lib/api/api.rb b/lib/api/api.rb index e14464c1b0d05847a67c8c22cd1181fb6a9f9e6d..a08fb056049b8fb53b09c4b2101e0f26789caa96 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -45,6 +45,7 @@ module API mount ::API::Keys mount ::API::Labels mount ::API::LicenseTemplates + mount ::API::Lint mount ::API::Members mount ::API::MergeRequests mount ::API::Milestones diff --git a/lib/api/lint.rb b/lib/api/lint.rb new file mode 100644 index 0000000000000000000000000000000000000000..ae43a4a32376bcee210bb07c72ed683352707090 --- /dev/null +++ b/lib/api/lint.rb @@ -0,0 +1,21 @@ +module API + class Lint < Grape::API + namespace :ci do + desc 'Validation of .gitlab-ci.yml content' + params do + requires :content, type: String, desc: 'Content of .gitlab-ci.yml' + end + post '/lint' do + error = Ci::GitlabCiYamlProcessor.validation_message(params[:content]) + + status 200 + + if error.blank? + { status: 'valid', errors: [] } + else + { status: 'invalid', errors: [error] } + end + end + end + end +end diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 47efd5bd9f264eb3685d0f07e252bff49dedb8f5..e8c863493396cf93b69af8f0226f55da4595391f 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -78,6 +78,17 @@ module Ci } end + def self.validation_message(content) + return 'Please provide content of .gitlab-ci.yml' if content.blank? + + begin + Ci::GitlabCiYamlProcessor.new(content) + nil + rescue ValidationError, Psych::SyntaxError => e + e.message + end + end + private def initial_parsing diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index be51d942af7c140d00f5e5459e7fece59469b04c..af192664b3305e5745a7dcaefc0d8f77be82266c 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -1250,5 +1250,40 @@ EOT end end end + + describe "#validation_message" do + context "when the YAML could not be parsed" do + it "returns an error about invalid configutaion" do + content = YAML.dump("invalid: yaml: test") + + expect(GitlabCiYamlProcessor.validation_message(content)) + .to eq "Invalid configuration format" + end + end + + context "when the tags parameter is invalid" do + it "returns an error about invalid tags" do + content = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) + + expect(GitlabCiYamlProcessor.validation_message(content)) + .to eq "jobs:rspec tags should be an array of strings" + end + end + + context "when YAML content is empty" do + it "returns an error about missing content" do + expect(GitlabCiYamlProcessor.validation_message('')) + .to eq "Please provide content of .gitlab-ci.yml" + end + end + + context "when the YAML is valid" do + it "does not return any errors" do + content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + + expect(GitlabCiYamlProcessor.validation_message(content)).to be_nil + end + end + end end end diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..391fc13a380f56ff2fd997f221685e1f166d4146 --- /dev/null +++ b/spec/requests/api/lint_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe API::Lint, api: true do + include ApiHelpers + + describe 'POST /ci/lint' do + context 'with valid .gitlab-ci.yaml content' do + let(:yaml_content) do + File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + end + + it 'passes validation' do + post api('/ci/lint'), { content: yaml_content } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Hash + expect(json_response['status']).to eq('valid') + expect(json_response['errors']).to eq([]) + end + end + + context 'with an invalid .gitlab_ci.yml' do + it 'responds with errors about invalid syntax' do + post api('/ci/lint'), { content: 'invalid content' } + + expect(response).to have_http_status(200) + expect(json_response['status']).to eq('invalid') + expect(json_response['errors']).to eq(['Invalid configuration format']) + end + + it "responds with errors about invalid configuration" do + post api('/ci/lint'), { content: '{ image: "ruby:2.1", services: ["postgres"] }' } + + expect(response).to have_http_status(200) + expect(json_response['status']).to eq('invalid') + expect(json_response['errors']).to eq(['jobs config should contain at least one visible job']) + end + end + + context 'without the content parameter' do + it 'responds with validation error about missing content' do + post api('/ci/lint') + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('content is missing') + end + end + end +end