Commit 92769ba5 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'mc/feature/project-ci-lint-api' into 'master'

Add project scope CI lint API endpoint

See merge request gitlab-org/gitlab!42998
parents e8308bfc 3d4159e6
......@@ -27,6 +27,13 @@ module Ci
sha_attribute :source_sha
sha_attribute :target_sha
# Ci::CreatePipelineService returns Ci::Pipeline so this is the only place
# where we can pass additional information from the service. This accessor
# is used for storing the processed CI YAML contents for linting purposes.
# There is an open issue to address this:
# https://gitlab.com/gitlab-org/gitlab/-/issues/259010
attr_accessor :merged_yaml
belongs_to :project, inverse_of: :all_pipelines
belongs_to :user
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
......
---
title: Add project scoped CI lint API endpoint.
merge_request: 42998
author:
type: added
......@@ -94,3 +94,51 @@ Example response:
"merged_config": "---\n:another_test:\n :stage: test\n :script: echo 2\n:test:\n :stage: test\n :script: echo 1\n"
}
```
## Validate a project's CI configuration
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/231352) in GitLab 13.5.
Checks if a project's latest (`HEAD` of the project's default branch)
`.gitlab-ci.yml` configuration is valid. This endpoint uses all namespace
specific data available, including variables, local includes, and so on.
```plaintext
GET /projects/:id/ci/lint
```
| Attribute | Type | Required | Description |
| ---------- | ------- | -------- | -------- |
| `dry_run` | boolean | no | Run pipeline creation simulation, or only do static check. |
Example request:
```shell
curl "https://gitlab.example.com/api/v4/projects/:id/ci/lint"
```
Example responses:
- Valid config:
```json
{
"valid": true,
"merged_yaml": "---\n:test_job:\n :script: echo 1\n",
"errors": [],
"warnings": []
}
```
- Invalid config:
```json
{
"valid": false,
"merged_yaml": "---\n:test_job:\n :script: echo 1\n",
"errors": [
"jobs config should contain at least one visible job"
],
"warnings": []
}
```
# frozen_string_literal: true
module API
module Entities
module Ci
module Lint
class Result < Grape::Entity
expose :valid?, as: :valid
expose :errors
expose :warnings
expose :merged_yaml
end
end
end
end
end
......@@ -25,5 +25,24 @@ module API
end
end
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Validation of .gitlab-ci.yml content' do
detail 'This feature was introduced in GitLab 13.5.'
end
params do
optional :dry_run, type: Boolean, default: false, desc: 'Run pipeline creation simulation, or only do static check.'
end
get ':id/ci/lint' do
authorize! :download_code, user_project
content = user_project.repository.gitlab_ci_yml_for(user_project.commit.id, user_project.ci_config_path_or_default)
result = Gitlab::Ci::Lint
.new(project: user_project, current_user: current_user)
.validate(content, dry_run: params[:dry_run])
present result, with: Entities::Ci::Lint::Result, current_user: current_user
end
end
end
end
......@@ -4,10 +4,11 @@ module Gitlab
module Ci
class Lint
class Result
attr_reader :jobs, :errors, :warnings
attr_reader :jobs, :merged_yaml, :errors, :warnings
def initialize(jobs:, errors:, warnings:)
def initialize(jobs:, merged_yaml:, errors:, warnings:)
@jobs = jobs
@merged_yaml = merged_yaml
@errors = errors
@warnings = warnings
end
......@@ -39,6 +40,7 @@ module Gitlab
Result.new(
jobs: dry_run_convert_to_jobs(pipeline.stages),
merged_yaml: pipeline.merged_yaml,
errors: pipeline.error_messages.map(&:content),
warnings: pipeline.warning_messages(limit: ::Gitlab::Ci::Warnings::MAX_LIMIT).map(&:content)
)
......@@ -54,6 +56,7 @@ module Gitlab
Result.new(
jobs: static_validation_convert_to_jobs(result),
merged_yaml: result.merged_yaml,
errors: result.errors,
warnings: result.warnings.take(::Gitlab::Ci::Warnings::MAX_LIMIT) # rubocop: disable CodeReuse/ActiveRecord
)
......
......@@ -28,6 +28,8 @@ module Gitlab
error(result.errors.first, config_error: true)
end
@pipeline.merged_yaml = result.merged_yaml
rescue => ex
Gitlab::ErrorTracking.track_exception(ex,
project_id: project.id,
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Lint do
let_it_be(:project) { create(:project, :repository) }
let(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let(:lint) { described_class.new(project: project, current_user: user) }
......@@ -61,6 +61,43 @@ RSpec.describe Gitlab::Ci::Lint do
end
end
shared_examples 'sets merged yaml' do
let(:content) do
<<~YAML
:include:
:local: another-gitlab-ci.yml
:test_job:
:stage: test
:script: echo
YAML
end
let(:included_content) do
<<~YAML
:another_job:
:script: echo
YAML
end
before do
project.repository.create_file(
project.creator,
'another-gitlab-ci.yml',
included_content,
message: 'Automatically created another-gitlab-ci.yml',
branch_name: 'master'
)
end
it 'sets merged_config' do
root_config = YAML.safe_load(content, [Symbol])
included_config = YAML.safe_load(included_content, [Symbol])
expected_config = included_config.merge(root_config).except(:include)
expect(subject.merged_yaml).to eq(expected_config.to_yaml)
end
end
shared_examples 'content with errors and warnings' do
context 'when content has errors' do
let(:content) do
......@@ -173,6 +210,8 @@ RSpec.describe Gitlab::Ci::Lint do
end
end
it_behaves_like 'sets merged yaml'
include_context 'advanced validations' do
it 'does not catch advanced logical errors' do
expect(subject).to be_valid
......@@ -203,6 +242,8 @@ RSpec.describe Gitlab::Ci::Lint do
end
end
it_behaves_like 'sets merged yaml'
include_context 'advanced validations' do
it 'runs advanced logical validations' do
expect(subject).not_to be_valid
......
......@@ -75,4 +75,115 @@ RSpec.describe API::Lint do
end
end
end
describe 'GET /projects/:id/ci/lint' do
subject(:ci_lint) { get api("/projects/#{project.id}/ci/lint", api_user), params: { dry_run: dry_run } }
let_it_be(:api_user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:dry_run) { nil }
RSpec.shared_examples 'valid config' do
it 'passes validation' do
ci_lint
included_config = YAML.safe_load(included_content, [Symbol])
root_config = YAML.safe_load(yaml_content, [Symbol])
expected_yaml = included_config.merge(root_config).except(:include).to_yaml
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Hash
expect(json_response['merged_yaml']).to eq(expected_yaml)
expect(json_response['valid']).to eq(true)
expect(json_response['errors']).to eq([])
end
end
RSpec.shared_examples 'invalid config' do
it 'responds with errors about invalid configuration' do
ci_lint
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['merged_yaml']).to eq(yaml_content)
expect(json_response['valid']).to eq(false)
expect(json_response['errors']).to eq(['jobs config should contain at least one visible job'])
end
end
context 'when unauthenticated' do
it 'returns authentication error' do
ci_lint
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when authenticated as project member' do
before do
project.add_developer(api_user)
end
context 'with valid .gitlab-ci.yml content' do
let(:yaml_content) do
{ include: { local: 'another-gitlab-ci.yml' }, test: { stage: 'test', script: 'echo 1' } }.to_yaml
end
let(:included_content) do
{ another_test: { stage: 'test', script: 'echo 1' } }.to_yaml
end
before do
project.repository.create_file(
project.creator,
'.gitlab-ci.yml',
yaml_content,
message: 'Automatically created .gitlab-ci.yml',
branch_name: 'master'
)
project.repository.create_file(
project.creator,
'another-gitlab-ci.yml',
included_content,
message: 'Automatically created another-gitlab-ci.yml',
branch_name: 'master'
)
end
context 'when running as dry run' do
let(:dry_run) { true }
it_behaves_like 'valid config'
end
context 'when running static validation' do
let(:dry_run) { false }
it_behaves_like 'valid config'
end
end
context 'with invalid .gitlab-ci.yml content' do
let(:yaml_content) do
{ image: 'ruby:2.7', services: ['postgres'] }.to_yaml
end
before do
stub_ci_pipeline_yaml_file(yaml_content)
end
context 'when running as dry run' do
let(:dry_run) { true }
it_behaves_like 'invalid config'
end
context 'when running static validation' do
let(:dry_run) { false }
it_behaves_like 'invalid config'
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