Commit 2654dddc authored by Fabio Pitino's avatar Fabio Pitino

Merge branch...

Merge branch '196032-create-ci-linting-api-that-allows-ci-linting-to-be-applied-across-the-application' into 'master'

Add json api endpoint that provides CI linting

See merge request gitlab-org/gitlab!37344
parents e2e3b618 3aaac9db
......@@ -14,6 +14,11 @@ class Projects::Ci::LintsController < Projects::ApplicationController
.new(project: @project, current_user: current_user)
.validate(@content, dry_run: @dry_run)
render :show
respond_to do |format|
format.html { render :show }
format.json do
render json: ::Ci::Lint::ResultSerializer.new.represent(@result)
end
end
end
end
# frozen_string_literal: true
class Ci::Lint::JobEntity < Grape::Entity
expose :name
expose :stage
expose :before_script
expose :script
expose :after_script
expose :tag_list
expose :environment
expose :when
expose :allow_failure
expose :only
expose :except
end
# frozen_string_literal: true
class Ci::Lint::ResultEntity < Grape::Entity
expose :valid?, as: :valid
expose :errors
expose :warnings
expose :jobs, using: Ci::Lint::JobEntity do |result, options|
next [] unless result.valid?
result.jobs
end
end
# frozen_string_literal: true
class Ci::Lint::ResultSerializer < BaseSerializer
entity ::Ci::Lint::ResultEntity
end
---
title: Add json api endpoint that provides CI linting
merge_request: 37344
author:
type: added
......@@ -65,10 +65,10 @@ module Gitlab
{
name: job.name,
stage: stage.name,
before_script: job.options[:before_script],
script: job.options[:script],
after_script: job.options[:after_script],
tag_list: (job.tag_list if job.is_a?(::Ci::Build)),
before_script: job.options[:before_script].to_a,
script: job.options[:script].to_a,
after_script: job.options[:after_script].to_a,
tag_list: (job.tag_list if job.is_a?(::Ci::Build)).to_a,
environment: job.options.dig(:environment, :name),
when: job.when,
allow_failure: job.allow_failure
......@@ -88,9 +88,9 @@ module Gitlab
jobs << {
name: job[:name],
stage: stage_name,
before_script: job.dig(:options, :before_script),
script: job.dig(:options, :script),
after_script: job.dig(:options, :after_script),
before_script: job.dig(:options, :before_script).to_a,
script: job.dig(:options, :script).to_a,
after_script: job.dig(:options, :after_script).to_a,
tag_list: job[:tag_list].to_a,
only: job[:only],
except: job[:except],
......
......@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe Projects::Ci::LintsController do
include StubRequests
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
before do
sign_in(user)
......@@ -20,7 +20,7 @@ RSpec.describe Projects::Ci::LintsController do
get :show, params: { namespace_id: project.namespace, project_id: project }
end
it { expect(response).to be_successful }
it { expect(response).to have_gitlab_http_status(:ok) }
it 'renders show page' do
expect(response).to render_template :show
......@@ -47,7 +47,8 @@ RSpec.describe Projects::Ci::LintsController do
describe 'POST #create' do
subject { post :create, params: params }
let(:params) { { namespace_id: project.namespace, project_id: project, content: content } }
let(:format) { :html }
let(:params) { { namespace_id: project.namespace, project_id: project, content: content, format: format } }
let(:remote_file_path) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' }
let(:remote_file_content) do
......@@ -71,6 +72,20 @@ RSpec.describe Projects::Ci::LintsController do
HEREDOC
end
shared_examples 'successful request with format json' do
context 'with format json' do
let(:format) { :json }
let(:parsed_body) { Gitlab::Json.parse(response.body) }
it 'renders json' do
expect(response).to have_gitlab_http_status :ok
expect(response.content_type).to eq 'application/json'
expect(parsed_body).to include('errors', 'warnings', 'jobs', 'valid')
expect(parsed_body).to match_schema('entities/lint_result_entity')
end
end
end
context 'with a valid gitlab-ci.yml' do
before do
stub_full_request(remote_file_path).to_return(body: remote_file_content)
......@@ -78,20 +93,23 @@ RSpec.describe Projects::Ci::LintsController do
end
shared_examples 'returns a successful validation' do
it 'returns successfully' do
before do
subject
expect(response).to be_successful
end
it 'render show page' do
subject
it 'returns successfully' do
expect(response).to have_gitlab_http_status :ok
end
it 'renders show page' do
expect(response).to render_template :show
end
it 'retrieves project' do
subject
expect(assigns(:project)).to eq(project)
end
it_behaves_like 'successful request with format json'
end
context 'using legacy validation (YamlProcessor)' do
......@@ -145,25 +163,30 @@ RSpec.describe Projects::Ci::LintsController do
before do
project.add_developer(user)
subject
end
it 'assigns result with errors' do
subject
expect(assigns[:result].errors).to match_array([
'jobs rubocop config should implement a script: or a trigger: keyword',
'jobs config should contain at least one visible job'
])
end
it 'render show page' do
expect(response).to render_template :show
end
it_behaves_like 'successful request with format json'
context 'with dry_run mode' do
subject { post :create, params: params.merge(dry_run: 'true') }
it 'assigns result with errors' do
subject
expect(assigns[:result].errors).to eq(['jobs rubocop config should implement a script: or a trigger: keyword'])
end
it_behaves_like 'successful request with format json'
end
end
......@@ -177,6 +200,14 @@ RSpec.describe Projects::Ci::LintsController do
it 'responds with 404' do
expect(response).to have_gitlab_http_status(:not_found)
end
context 'with format json' do
let(:format) { :json }
it 'responds with 404' do
expect(response).to have_gitlab_http_status :not_found
end
end
end
end
end
{
"type": "object",
"required": [
"name",
"stage",
"before_script",
"script",
"after_script",
"tag_list",
"environment",
"when",
"allow_failure",
"only",
"except"
],
"properties": {
"name": {
"type": ["string"]
},
"stage": {
"type": ["string"]
},
"before_script": {
"type": ["array"],
"items": { "type": "string" }
},
"script": {
"type": ["array"],
"items": { "type": "string" }
},
"after_script": {
"type": ["array"],
"items": { "type": "string" }
},
"when": {
"items": { "type": ["string"] }
},
"allow_failure": {
"type": ["boolean"]
},
"environment": {
"type": ["string", null]
},
"tag_list": {
"type": ["array"],
"items": { "type": "string" }
},
"only": {
"type": ["array", "object", null],
"items": { "type": ["string", "array"]}
},
"except": {
"type": ["array", "object", null],
"items": { "type": ["string", "array"]}
}
},
"additionalProperties": false
}
{
"type": "object",
"required": ["valid", "errors", "jobs", "warnings"],
"properties": {
"errors": {
"type": "array",
"items": { "type": "string" }
},
"warnings": {
"type": "array",
"items": { "type": "string" }
},
"valid": {
"type": "boolean"
},
"jobs": {
"type": ["array", null],
"items": {
"type": "object",
"$ref": "lint_job_entity.json"
}
}
},
"additionalProperties": false
}
......@@ -42,7 +42,7 @@ RSpec.describe Gitlab::Ci::Lint do
expect(build_job[:stage]).to eq('build')
expect(build_job[:before_script]).to eq(['before_build'])
expect(build_job[:script]).to eq(['echo'])
expect(build_job.fetch(:after_script)).to be_nil
expect(build_job.fetch(:after_script)).to eq([])
expect(build_job[:tag_list]).to eq([])
expect(build_job[:environment]).to eq('staging')
expect(build_job[:when]).to eq('manual')
......@@ -51,7 +51,7 @@ RSpec.describe Gitlab::Ci::Lint do
rspec_job = subject.jobs.last
expect(rspec_job[:name]).to eq('rspec')
expect(rspec_job[:stage]).to eq('test')
expect(rspec_job.fetch(:before_script)).to be_nil
expect(rspec_job.fetch(:before_script)).to eq([])
expect(rspec_job[:script]).to eq(['rspec'])
expect(rspec_job[:after_script]).to eq(['after_rspec'])
expect(rspec_job[:tag_list]).to eq(['docker'])
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::Lint::JobEntity, :aggregate_failures do
describe '#represent' do
let(:job) do
{
name: 'rspec',
stage: 'test',
before_script: ['bundle install', 'bundle exec rake db:create'],
script: ["rake spec"],
after_script: ["rake spec"],
tag_list: %w[ruby postgres],
environment: { name: 'hello', url: 'world' },
when: 'on_success',
allow_failure: false,
except: { refs: ["branches"] },
only: { refs: ["branches"] },
variables: { hello: 'world' }
}
end
subject(:serialized_job_result) { described_class.new(job).as_json }
it 'exposes job data' do
expect(serialized_job_result.keys).to contain_exactly(
:name,
:stage,
:before_script,
:script,
:after_script,
:tag_list,
:environment,
:when,
:allow_failure,
:only,
:except
)
expect(serialized_job_result.keys).not_to include(:variables)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::Lint::ResultEntity do
describe '#represent' do
let(:yaml_content) { YAML.dump({ rspec: { script: 'test', tags: 'mysql' } }) }
let(:result) { Gitlab::Ci::YamlProcessor.new(yaml_content).execute }
subject(:serialized_linting_result) { described_class.new(result).as_json }
it 'serializes with lint result entity' do
expect(serialized_linting_result.keys).to include(:valid, :errors, :jobs, :warnings)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::Lint::ResultSerializer, :aggregate_failures do
let_it_be(:project) { create(:project, :repository) }
let(:result) do
Gitlab::Ci::Lint
.new(project: project, current_user: project.owner)
.validate(yaml_content, dry_run: false)
end
let(:first_job) { linting_result[:jobs].first }
let(:serialized_linting_result) { linting_result.to_json }
subject(:linting_result) { described_class.new.represent(result) }
shared_examples 'matches schema' do
it { expect(serialized_linting_result).to match_schema('entities/lint_result_entity') }
end
context 'when config is invalid' do
let(:yaml_content) { YAML.dump({ rspec: { script: 'test', tags: 'mysql' } }) }
it_behaves_like 'matches schema'
it 'returns expected validity' do
expect(linting_result[:valid]).to eq(false)
expect(linting_result[:errors]).to eq(['jobs:rspec:tags config should be an array of strings'])
expect(linting_result[:warnings]).to eq([])
end
it 'returns job data' do
expect(linting_result[:jobs]).to eq([])
end
end
context 'when config is valid' do
let(:yaml_content) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
it_behaves_like 'matches schema'
it 'returns expected validity' do
expect(linting_result[:valid]).to eq(true)
expect(linting_result[:errors]).to eq([])
expect(linting_result[:warnings]).to eq([])
end
it 'returns job data' do
expect(first_job[:name]).to eq('rspec')
expect(first_job[:stage]).to eq('test')
expect(first_job[:before_script]).to eq(['bundle install', 'bundle exec rake db:create'])
expect(first_job[:script]).to eq(['rake spec'])
expect(first_job[:after_script]).to eq([])
expect(first_job[:tag_list]).to eq(%w[ruby postgres])
expect(first_job[:environment]).to eq(nil)
expect(first_job[:when]).to eq('on_success')
expect(first_job[:allow_failure]).to eq(false)
expect(first_job[:only]).to eq(refs: ['branches'])
expect(first_job[:except]).to eq(nil)
end
context 'when dry run is enabled' do
let(:result) do
Gitlab::Ci::Lint
.new(project: project, current_user: project.owner)
.validate(yaml_content, dry_run: true)
end
it_behaves_like 'matches schema'
it 'returns expected validity' do
expect(linting_result[:valid]).to eq(true)
expect(linting_result[:errors]).to eq([])
expect(linting_result[:warnings]).to eq([])
end
it 'returns job data' do
expect(first_job[:name]).to eq('rspec')
expect(first_job[:stage]).to eq('test')
expect(first_job[:before_script]).to eq(['bundle install', 'bundle exec rake db:create'])
expect(first_job[:script]).to eq(['rake spec'])
expect(first_job[:after_script]).to eq([])
expect(first_job[:tag_list]).to eq(%w[ruby postgres])
expect(first_job[:environment]).to eq(nil)
expect(first_job[:when]).to eq('on_success')
expect(first_job[:allow_failure]).to eq(false)
expect(first_job[:only]).to eq(nil)
expect(first_job[:except]).to eq(nil)
end
end
context 'when only is not nil in the yaml' do
context 'when only: is hash' do
let(:yaml_content) do
<<~YAML
build:
stage: build
script: echo
only:
refs:
- branches
YAML
end
it_behaves_like 'matches schema'
it 'renders only:refs as hash' do
expect(first_job[:only]).to eq(refs: ['branches'])
end
end
context 'when only is an array of strings in the yaml' do
let(:yaml_content) do
<<~YAML
build:
stage: build
script: echo
only:
- pushes
YAML
end
it_behaves_like 'matches schema'
it 'renders only: list as hash' do
expect(first_job[:only]).to eq(refs: ['pushes'])
end
end
end
context 'when except is not nil in the yaml' do
context 'when except: is hash' do
let(:yaml_content) do
<<~YAML
build:
stage: build
script: echo
except:
refs:
- branches
YAML
end
it_behaves_like 'matches schema'
it 'renders except as hash' do
expect(first_job[:except]).to eq(refs: ['branches'])
end
end
context 'when except is an array of strings in the yaml' do
let(:yaml_content) do
<<~YAML
build:
stage: build
script: echo
except:
- pushes
YAML
end
it_behaves_like 'matches schema'
it 'renders only: list as hash' do
expect(first_job[:except]).to eq(refs: ['pushes'])
end
end
context 'with minimal job configuration' do
let(:yaml_content) do
<<~YAML
build:
stage: build
script: echo
YAML
end
it_behaves_like 'matches schema'
it 'renders the job with defaults' do
expect(first_job[:name]).to eq('build')
expect(first_job[:stage]).to eq('build')
expect(first_job[:before_script]).to eq([])
expect(first_job[:script]).to eq(['echo'])
expect(first_job[:after_script]).to eq([])
expect(first_job[:tag_list]).to eq([])
expect(first_job[:environment]).to eq(nil)
expect(first_job[:when]).to eq('on_success')
expect(first_job[:allow_failure]).to eq(false)
expect(first_job[:only]).to eq(refs: %w[branches tags])
expect(first_job[:except]).to eq(nil)
end
end
context 'with environment defined' do
context 'when formatted as a hash in yaml' do
let(:yaml_content) do
<<~YAML
build:
stage: build
script: echo
environment:
name: production
url: https://example.com
YAML
end
it_behaves_like 'matches schema'
it 'renders the environment as a string' do
expect(first_job[:environment]).to eq('production')
end
end
context 'when formatted as a string in yaml' do
let(:yaml_content) do
<<~YAML
build:
stage: build
script: echo
environment: production
YAML
end
it_behaves_like 'matches schema'
it 'renders the environment as a string' do
expect(first_job[:environment]).to eq('production')
end
end
end
context 'when script values are formatted as arrays in the yaml' do
let(:yaml_content) do
<<~YAML
build:
stage: build
before_script:
- echo
- cat '~/.zshrc'
script:
- echo
- cat '~/.zshrc'
after_script:
- echo
- cat '~/.zshrc'
YAML
end
it_behaves_like 'matches schema'
it 'renders the scripts as arrays' do
expect(first_job[:before_script]).to eq(['echo', "cat '~/.zshrc'"])
expect(first_job[:script]).to eq(['echo', "cat '~/.zshrc'"])
expect(first_job[:after_script]).to eq(['echo', "cat '~/.zshrc'"])
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment