Commit ef0f1584 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'use-dry-run-on-ci-lint' into 'master'

Optionally use new dry_run mode on CI Lint page

See merge request gitlab-org/gitlab!37828
parents 2f94aec4 bac1b6b8
...@@ -8,17 +8,30 @@ class Projects::Ci::LintsController < Projects::ApplicationController ...@@ -8,17 +8,30 @@ class Projects::Ci::LintsController < Projects::ApplicationController
def create def create
@content = params[:content] @content = params[:content]
result = Gitlab::Ci::YamlProcessor.new_with_validation_errors(@content, yaml_processor_options) @dry_run = params[:dry_run]
@status = result.valid? if @dry_run && Gitlab::Ci::Features.lint_creates_pipeline_with_dry_run?(@project)
@errors = result.errors pipeline = Ci::CreatePipelineService
@warnings = result.warnings .new(@project, current_user, ref: @project.default_branch)
.execute(:push, dry_run: true, content: @content)
if result.valid?
@config_processor = result.config @status = pipeline.error_messages.empty?
@stages = @config_processor.stages @stages = pipeline.stages
@builds = @config_processor.builds @errors = pipeline.error_messages.map(&:content)
@jobs = @config_processor.jobs @warnings = pipeline.warning_messages.map(&:content)
else
result = Gitlab::Ci::YamlProcessor.new_with_validation_errors(@content, yaml_processor_options)
@status = result.valid?
@errors = result.errors
@warnings = result.warnings
if result.valid?
@config_processor = result.config
@stages = @config_processor.stages
@builds = @config_processor.builds
@jobs = @config_processor.jobs
end
end end
render :show render :show
......
...@@ -13,33 +13,54 @@ ...@@ -13,33 +13,54 @@
%th= _("Parameter") %th= _("Parameter")
%th= _("Value") %th= _("Value")
%tbody %tbody
- @stages.each do |stage| - if @dry_run
- @builds.select { |build| build[:stage] == stage }.each do |build| - @stages.each do |stage|
- job = @jobs[build[:name].to_sym] - stage.statuses.each do |job|
%tr %tr
%td #{stage.capitalize} Job - #{build[:name]} %td #{stage.name.capitalize} Job - #{job.name}
%td %td
%pre= job[:before_script].to_a.join('\n') %pre= job.options[:before_script].to_a.join('\n')
%pre= job[:script].to_a.join('\n') %pre= job.options[:script].to_a.join('\n')
%pre= job[:after_script].to_a.join('\n') %pre= job.options[:after_script].to_a.join('\n')
%br
%b= _("Tag list:")
= job.tag_list.to_a.join(", ") if job.is_a?(Ci::Build)
%br
%b= _("Environment:")
= job.options.dig(:environment, :name)
%br
%b= _("When:")
= job.when
- if job.allow_failure
%b= _("Allowed to fail")
%br - else
%b= _("Tag list:") - @stages.each do |stage|
= build[:tag_list].to_a.join(", ") - @builds.select { |build| build[:stage] == stage }.each do |build|
%br - job = @jobs[build[:name].to_sym]
%b= _("Only policy:") %tr
= job[:only].to_a.join(", ") %td #{stage.capitalize} Job - #{build[:name]}
%br %td
%b= _("Except policy:") %pre= job[:before_script].to_a.join('\n')
= job[:except].to_a.join(", ") %pre= job[:script].to_a.join('\n')
%br %pre= job[:after_script].to_a.join('\n')
%b= _("Environment:") %br
= build[:environment] %b= _("Tag list:")
%br = build[:tag_list].to_a.join(", ")
%b= _("When:") %br
= build[:when] %b= _("Only policy:")
- if build[:allow_failure] = job[:only].to_a.join(", ")
%b= _("Allowed to fail") %br
%b= _("Except policy:")
= job[:except].to_a.join(", ")
%br
%b= _("Environment:")
= build[:environment]
%br
%b= _("When:")
= build[:when]
- if build[:allow_failure]
%b= _("Allowed to fail")
- else - else
.bs-callout.bs-callout-danger .bs-callout.bs-callout-danger
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- content_for :library_javascripts do - content_for :library_javascripts do
= page_specific_javascript_tag('lib/ace.js') = page_specific_javascript_tag('lib/ace.js')
%h2.pt-3.pb-3= _("Check your .gitlab-ci.yml") %h2.pt-3.pb-3= _("Validate your GitLab CI configuration")
.project-ci-linter .project-ci-linter
= form_tag project_ci_lint_path(@project), method: :post do = form_tag project_ci_lint_path(@project), method: :post do
...@@ -17,7 +17,11 @@ ...@@ -17,7 +17,11 @@
.col-sm-12 .col-sm-12
.float-left.gl-mt-3 .float-left.gl-mt-3
= submit_tag(_('Validate'), class: 'btn btn-success submit-yml') = submit_tag(_('Validate'), class: 'btn btn-success submit-yml')
.float-right.gl-mt-3 - if Gitlab::Ci::Features.lint_creates_pipeline_with_dry_run?(@project)
= check_box_tag(:dry_run, 'true', params[:dry_run])
= label_tag(:dry_run, _('Simulate a pipeline created for the default branch'))
= link_to icon('question-circle'), help_page_path('ci/lint', anchor: 'pipeline-simulation'), target: '_blank', rel: 'noopener noreferrer'
.float-right.prepend-top-10
= button_tag(_('Clear'), type: 'button', class: 'btn btn-default clear-yml') = button_tag(_('Clear'), type: 'button', class: 'btn btn-default clear-yml')
.row.prepend-top-20 .row.prepend-top-20
......
---
title: Allow user to simulate pipeline creation via CI Lint and go beyond syntax checks
merge_request: 37828
author:
type: added
# CI Lint
If you want to test the validity of your GitLab CI/CD configuration before committing
the changes, you can use the CI Lint tool. This tool checks for syntax and logical
errors by default, and can simulate pipeline creation to try to find more complicated
issues as well.
To access the CI Lint tool, navigate to **CI/CD > Pipelines** or **CI/CD > Jobs**
in your project and click **CI lint**.
## Validate basic logic and syntax
By default, the CI lint checks the syntax of your CI YAML configuration and also runs
some basic logical validations.
To use the CI lint, paste a complete CI configuration (`.gitlab-ci.yml` for example)
into the text box and click **Validate**:
![CI Lint](img/ci_lint.png)
## Pipeline simulation
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/229794) in GitLab 13.3.
Not all pipeline configuration issues can be found by the [basic CI lint validation](#validate-basic-logic-and-syntax).
You can simulate the creation of a pipeline for deeper validation that can discover
more complicated issues.
To validate the configuration by running a pipeline simulation:
1. Paste the GitLab CI configuration to verify into the text box.
1. Click the **Simulate pipeline creation for the default branch** checkbox.
1. Click **Validate**.
![Dry run](img/ci_lint_dry_run.png)
### Pipeline simulation limitations
Simulations run as `git push` events against the default branch. You must have
[permissions](../user/permissions.md#project-members-permissions) to create pipelines
on this branch to validate with a simulation.
...@@ -114,9 +114,7 @@ Jobs are used to create jobs, which are then picked by ...@@ -114,9 +114,7 @@ Jobs are used to create jobs, which are then picked by
What is important is that each job is run independently from each other. What is important is that each job is run independently from each other.
If you want to check whether the `.gitlab-ci.yml` of your project is valid, there is a If you want to check whether the `.gitlab-ci.yml` of your project is valid, there is a
Lint tool under the page `/-/ci/lint` of your project namespace. You can also find [CI Lint tool](../lint.md) available in every project.
a "CI Lint" button to go to this page under **CI/CD ➔ Pipelines** and
**Pipelines ➔ Jobs** in your project.
For more information and a complete `.gitlab-ci.yml` syntax, please read For more information and a complete `.gitlab-ci.yml` syntax, please read
[the reference documentation on `.gitlab-ci.yml`](../yaml/README.md). [the reference documentation on `.gitlab-ci.yml`](../yaml/README.md).
......
...@@ -76,6 +76,10 @@ module Gitlab ...@@ -76,6 +76,10 @@ module Gitlab
::Feature.enabled?(:ci_job_entry_matches_all_keys) ::Feature.enabled?(:ci_job_entry_matches_all_keys)
end end
def self.lint_creates_pipeline_with_dry_run?(project)
::Feature.enabled?(:ci_lint_creates_pipeline_with_dry_run, project)
end
def self.reset_ci_minutes_for_all_namespaces? def self.reset_ci_minutes_for_all_namespaces?
::Feature.enabled?(:reset_ci_minutes_for_all_namespaces, default_enabled: false) ::Feature.enabled?(:reset_ci_minutes_for_all_namespaces, default_enabled: false)
end end
......
...@@ -45,6 +45,15 @@ module Gitlab ...@@ -45,6 +45,15 @@ module Gitlab
Gitlab::Metrics.counter(name, comment) Gitlab::Metrics.counter(name, comment)
end end
end end
def pipelines_created_counter
strong_memoize(:pipelines_created_count) do
name = :pipelines_created_total
comment = 'Counter of pipelines created'
Gitlab::Metrics.counter(name, comment)
end
end
end end
end end
end end
......
...@@ -4550,9 +4550,6 @@ msgstr "" ...@@ -4550,9 +4550,6 @@ msgstr ""
msgid "Check the %{docs_link_start}documentation%{docs_link_end}." msgid "Check the %{docs_link_start}documentation%{docs_link_end}."
msgstr "" msgstr ""
msgid "Check your .gitlab-ci.yml"
msgstr ""
msgid "Check your Docker images for known vulnerabilities." msgid "Check your Docker images for known vulnerabilities."
msgstr "" msgstr ""
...@@ -22459,6 +22456,9 @@ msgstr "" ...@@ -22459,6 +22456,9 @@ msgstr ""
msgid "Similar issues" msgid "Similar issues"
msgstr "" msgstr ""
msgid "Simulate a pipeline created for the default branch"
msgstr ""
msgid "Single or combined queries" msgid "Single or combined queries"
msgstr "" msgstr ""
...@@ -26754,6 +26754,9 @@ msgstr "" ...@@ -26754,6 +26754,9 @@ msgstr ""
msgid "Validate" msgid "Validate"
msgstr "" msgstr ""
msgid "Validate your GitLab CI configuration"
msgstr ""
msgid "Validate your GitLab CI configuration file" msgid "Validate your GitLab CI configuration file"
msgstr "" msgstr ""
......
...@@ -45,6 +45,9 @@ RSpec.describe Projects::Ci::LintsController do ...@@ -45,6 +45,9 @@ RSpec.describe Projects::Ci::LintsController do
end end
describe 'POST #create' do describe 'POST #create' do
subject { post :create, params: params }
let(:params) { { namespace_id: project.namespace, project_id: project, content: content } }
let(:remote_file_path) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' } let(:remote_file_path) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' }
let(:remote_file_content) do let(:remote_file_content) do
...@@ -72,18 +75,62 @@ RSpec.describe Projects::Ci::LintsController do ...@@ -72,18 +75,62 @@ RSpec.describe Projects::Ci::LintsController do
before do before do
stub_full_request(remote_file_path).to_return(body: remote_file_content) stub_full_request(remote_file_path).to_return(body: remote_file_content)
project.add_developer(user) project.add_developer(user)
end
post :create, params: { namespace_id: project.namespace, project_id: project, content: content } shared_examples 'returns a successful validation' do
it 'returns successfully' do
subject
expect(response).to be_successful
end
it 'render show page' do
subject
expect(response).to render_template :show
end
it 'retrieves project' do
subject
expect(assigns(:project)).to eq(project)
end
end end
it { expect(response).to be_successful } context 'using legacy validation (YamlProcessor)' do
it_behaves_like 'returns a successful validation'
it 'render show page' do it 'runs validations through YamlProcessor' do
expect(response).to render_template :show expect(Gitlab::Ci::YamlProcessor).to receive(:new_with_validation_errors).and_call_original
subject
end
end end
it 'retrieves project' do context 'using dry_run mode' do
expect(assigns(:project)).to eq(project) subject { post :create, params: params.merge(dry_run: 'true') }
it_behaves_like 'returns a successful validation'
it 'runs validations through Ci::CreatePipelineService' do
expect(Ci::CreatePipelineService)
.to receive(:new)
.with(project, user, ref: 'master')
.and_call_original
subject
end
context 'when dry_run feature flag is disabled' do
before do
stub_feature_flags(ci_lint_creates_pipeline_with_dry_run: false)
end
it_behaves_like 'returns a successful validation'
it 'runs validations through YamlProcessor' do
expect(Gitlab::Ci::YamlProcessor).to receive(:new_with_validation_errors).and_call_original
subject
end
end
end end
end end
...@@ -98,13 +145,23 @@ RSpec.describe Projects::Ci::LintsController do ...@@ -98,13 +145,23 @@ RSpec.describe Projects::Ci::LintsController do
before do before do
project.add_developer(user) project.add_developer(user)
post :create, params: { namespace_id: project.namespace, project_id: project, content: content }
end end
it 'assigns errors' do it 'assigns errors' do
subject
expect(assigns[:errors]).to eq(['root config contains unknown keys: rubocop']) expect(assigns[:errors]).to eq(['root config contains unknown keys: rubocop'])
end end
context 'with dry_run mode' do
subject { post :create, params: params.merge(dry_run: 'true') }
it 'assigns errors' do
subject
expect(assigns[:errors]).to eq(['root config contains unknown keys: rubocop'])
end
end
end end
context 'without enough privileges' do context 'without enough privileges' do
......
...@@ -21,32 +21,48 @@ RSpec.describe 'CI Lint', :js do ...@@ -21,32 +21,48 @@ RSpec.describe 'CI Lint', :js do
end end
describe 'YAML parsing' do describe 'YAML parsing' do
before do shared_examples 'validates the YAML' do
click_on 'Validate' before do
end click_on 'Validate'
end
context 'YAML is correct' do context 'YAML is correct' do
let(:yaml_content) do let(:yaml_content) do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
end
it 'parses Yaml and displays the jobs' do
expect(page).to have_content('Status: syntax is correct')
within "table" do
aggregate_failures do
expect(page).to have_content('Job - rspec')
expect(page).to have_content('Job - spinach')
expect(page).to have_content('Deploy Job - staging')
expect(page).to have_content('Deploy Job - production')
end
end
end
end end
it 'parses Yaml' do context 'YAML is incorrect' do
within "table" do let(:yaml_content) { 'value: cannot have :' }
expect(page).to have_content('Job - rspec')
expect(page).to have_content('Job - spinach') it 'displays information about an error' do
expect(page).to have_content('Deploy Job - staging') expect(page).to have_content('Status: syntax is incorrect')
expect(page).to have_content('Deploy Job - production') expect(page).to have_selector('.ace_content', text: yaml_content)
end end
end end
end end
context 'YAML is incorrect' do it_behaves_like 'validates the YAML'
let(:yaml_content) { 'value: cannot have :' }
it 'displays information about an error' do context 'when Dry Run is checked' do
expect(page).to have_content('Status: syntax is incorrect') before do
expect(page).to have_selector('.ace_content', text: yaml_content) check 'Simulate a pipeline created for the default branch'
end end
it_behaves_like 'validates the YAML'
end end
describe 'YAML revalidate' do describe 'YAML revalidate' do
......
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