Commit df17e87e authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents dd883716 ce721bd7
...@@ -45,7 +45,7 @@ review-build-cng: ...@@ -45,7 +45,7 @@ review-build-cng:
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}" DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
GITLAB_HELM_CHART_REF: "v4.3.0" GITLAB_HELM_CHART_REF: "v4.3.0"
environment: environment:
name: review/${CI_COMMIT_REF_NAME} name: review/${CI_COMMIT_REF_SLUG}${FREQUENCY}
url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN} url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
on_stop: review-stop on_stop: review-stop
auto_stop_in: 48 hours auto_stop_in: 48 hours
...@@ -113,8 +113,8 @@ review-stop-failed-deployment: ...@@ -113,8 +113,8 @@ review-stop-failed-deployment:
review-stop: review-stop:
extends: extends:
- .review-stop-base - .review-stop-base
- .review:rules:mr-only-manual - .review:rules:review-stop
stage: review stage: post-qa
script: script:
- delete_release - delete_release
......
...@@ -753,6 +753,17 @@ ...@@ -753,6 +753,17 @@
- <<: *if-dot-com-gitlab-org-schedule - <<: *if-dot-com-gitlab-org-schedule
allow_failure: true allow_failure: true
.review:rules:review-stop:
rules:
- <<: *if-not-ee
when: never
- <<: *if-dot-com-gitlab-org-merge-request
changes: *code-qa-patterns
when: manual
allow_failure: true
- <<: *if-master-refs
allow_failure: true
.review:rules:danger: .review:rules:danger:
rules: rules:
- if: '$DANGER_GITLAB_API_TOKEN && $CI_MERGE_REQUEST_IID' - if: '$DANGER_GITLAB_API_TOKEN && $CI_MERGE_REQUEST_IID'
......
<script> <script>
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon, GlButton, GlAlert, GlPagination, GlSprintf } from '@gitlab/ui'; import { GlLoadingIcon, GlPagination, GlSprintf } from '@gitlab/ui';
import Mousetrap from 'mousetrap'; import Mousetrap from 'mousetrap';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils'; import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
...@@ -13,9 +13,12 @@ import eventHub from '../../notes/event_hub'; ...@@ -13,9 +13,12 @@ import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue'; import CompareVersions from './compare_versions.vue';
import DiffFile from './diff_file.vue'; import DiffFile from './diff_file.vue';
import NoChanges from './no_changes.vue'; import NoChanges from './no_changes.vue';
import HiddenFilesWarning from './hidden_files_warning.vue';
import CommitWidget from './commit_widget.vue'; import CommitWidget from './commit_widget.vue';
import TreeList from './tree_list.vue'; import TreeList from './tree_list.vue';
import HiddenFilesWarning from './hidden_files_warning.vue';
import MergeConflictWarning from './merge_conflict_warning.vue';
import { import {
TREE_LIST_WIDTH_STORAGE_KEY, TREE_LIST_WIDTH_STORAGE_KEY,
INITIAL_TREE_WIDTH, INITIAL_TREE_WIDTH,
...@@ -33,13 +36,12 @@ export default { ...@@ -33,13 +36,12 @@ export default {
DiffFile, DiffFile,
NoChanges, NoChanges,
HiddenFilesWarning, HiddenFilesWarning,
MergeConflictWarning,
CommitWidget, CommitWidget,
TreeList, TreeList,
GlLoadingIcon, GlLoadingIcon,
PanelResizer, PanelResizer,
GlPagination, GlPagination,
GlButton,
GlAlert,
GlSprintf, GlSprintf,
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
...@@ -422,49 +424,12 @@ export default { ...@@ -422,49 +424,12 @@ export default {
:plain-diff-path="plainDiffPath" :plain-diff-path="plainDiffPath"
:email-patch-path="emailPatchPath" :email-patch-path="emailPatchPath"
/> />
<merge-conflict-warning
<div
v-if="isDiffHead && hasConflicts" v-if="isDiffHead && hasConflicts"
:class="{ :limited="isLimitedContainer"
[CENTERED_LIMITED_CONTAINER_CLASSES]: isLimitedContainer, :resolution-path="conflictResolutionPath"
}" :mergeable="canMerge"
> />
<gl-alert
:dismissible="false"
:title="__('There are merge conflicts')"
variant="warning"
class="w-100 mb-3"
>
<p class="mb-1">
{{ __('The comparison view may be inaccurate due to merge conflicts.') }}
</p>
<p class="mb-0">
{{
__(
'Resolve these conflicts or ask someone with write access to this repository to merge it locally.',
)
}}
</p>
<template #actions>
<gl-button
v-if="conflictResolutionPath"
:href="conflictResolutionPath"
variant="info"
class="mr-3 gl-alert-action"
>
{{ __('Resolve conflicts') }}
</gl-button>
<gl-button
v-if="canMerge"
class="gl-alert-action"
data-toggle="modal"
data-target="#modal_merge_info"
>
{{ __('Merge locally') }}
</gl-button>
</template>
</gl-alert>
</div>
<div <div
:data-can-create-note="getNoteableData.current_user.can_create_note" :data-can-create-note="getNoteableData.current_user.can_create_note"
......
<script>
import { GlButton, GlAlert } from '@gitlab/ui';
import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
export default {
components: {
GlAlert,
GlButton,
},
props: {
limited: {
type: Boolean,
required: true,
},
mergeable: {
type: Boolean,
required: true,
},
resolutionPath: {
type: String,
required: true,
},
},
computed: {
containerClasses() {
return {
[CENTERED_LIMITED_CONTAINER_CLASSES]: this.limited,
};
},
},
};
</script>
<template>
<div :class="containerClasses">
<gl-alert
:dismissible="false"
:title="__('There are merge conflicts')"
variant="warning"
class="gl-mb-5"
>
<p class="gl-mb-2">
{{ __('The comparison view may be inaccurate due to merge conflicts.') }}
</p>
<p class="gl-mb-0">
{{
__(
'Resolve these conflicts or ask someone with write access to this repository to merge it locally.',
)
}}
</p>
<template #actions>
<gl-button
v-if="resolutionPath"
:href="resolutionPath"
variant="info"
class="gl-mr-5 gl-alert-action"
>
{{ __('Resolve conflicts') }}
</gl-button>
<gl-button
v-if="mergeable"
class="gl-alert-action"
data-toggle="modal"
data-target="#modal_merge_info"
>
{{ __('Merge locally') }}
</gl-button>
</template>
</gl-alert>
</div>
</template>
...@@ -1335,6 +1335,8 @@ expression string per rule, rather than an array of them. Any set of expressions ...@@ -1335,6 +1335,8 @@ expression string per rule, rather than an array of them. Any set of expressions
evaluated can be [conjoined into a single expression](../variables/README.md#conjunction--disjunction) evaluated can be [conjoined into a single expression](../variables/README.md#conjunction--disjunction)
by using `&&` or `||`, and use by using `&&` or `||`, and use
the [variable matching syntax](../variables/README.md#syntax-of-environment-variable-expressions). the [variable matching syntax](../variables/README.md#syntax-of-environment-variable-expressions).
Unlike variables in [`script`](../variables/README.md#syntax-of-environment-variables-in-job-scripts)
sections, variables in rules expressions are always formatted as `$VARIABLE`.
`if:` clauses are evaluated based on the values of [predefined environment variables](../variables/predefined_variables.md) `if:` clauses are evaluated based on the values of [predefined environment variables](../variables/predefined_variables.md)
or [custom environment variables](../variables/README.md#custom-environment-variables). or [custom environment variables](../variables/README.md#custom-environment-variables).
......
...@@ -89,8 +89,6 @@ module EE ...@@ -89,8 +89,6 @@ module EE
def self.sast_ci_configuration(project) def self.sast_ci_configuration(project)
::Security::CiConfiguration::SastParserService.new(project).configuration ::Security::CiConfiguration::SastParserService.new(project).configuration
rescue ::Gitlab::Ci::YamlProcessor::ValidationError => ex
raise ::GraphQL::ExecutionError, ex.message
end end
end end
end end
......
...@@ -66,7 +66,10 @@ module Security ...@@ -66,7 +66,10 @@ module Security
def build_sast_attributes(content) def build_sast_attributes(content)
options = { project: @project, user: current_user, sha: @project.repository.commit.sha } options = { project: @project, user: current_user, sha: @project.repository.commit.sha }
sast_attributes = Gitlab::Ci::YamlProcessor.new(content, options).build_attributes(:sast) yaml_result = Gitlab::Ci::YamlProcessor.new(content, options).execute
return {} unless yaml_result.valid?
sast_attributes = yaml_result.build_attributes(:sast)
extract_required_attributes(sast_attributes) extract_required_attributes(sast_attributes)
end end
......
...@@ -62,11 +62,11 @@ RSpec.describe Projects::FeatureFlagsController do ...@@ -62,11 +62,11 @@ RSpec.describe Projects::FeatureFlagsController do
subject { get(:index, params: view_params, format: :json) } subject { get(:index, params: view_params, format: :json) }
let!(:feature_flag_active) do let!(:feature_flag_active) do
create(:operations_feature_flag, project: project, active: true) create(:operations_feature_flag, project: project, active: true, name: 'feature_flag_a')
end end
let!(:feature_flag_inactive) do let!(:feature_flag_inactive) do
create(:operations_feature_flag, project: project, active: false) create(:operations_feature_flag, project: project, active: false, name: 'feature_flag_b')
end end
it 'returns all feature flags as json response' do it 'returns all feature flags as json response' do
...@@ -219,7 +219,7 @@ RSpec.describe Projects::FeatureFlagsController do ...@@ -219,7 +219,7 @@ RSpec.describe Projects::FeatureFlagsController do
context 'with version 1 and 2 feature flags' do context 'with version 1 and 2 feature flags' do
let!(:new_version_feature_flag) do let!(:new_version_feature_flag) do
create(:operations_feature_flag, :new_version_flag, project: project) create(:operations_feature_flag, :new_version_flag, project: project, name: 'feature_flag_c')
end end
it 'returns all feature flags as json response' do it 'returns all feature flags as json response' do
......
...@@ -25,7 +25,6 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -25,7 +25,6 @@ RSpec.describe GitlabSchema.types['Project'] do
describe 'sast_ci_configuration' do describe 'sast_ci_configuration' do
include_context 'read ci configuration for sast enabled project' include_context 'read ci configuration for sast enabled project'
let(:error_message) { "This is an error for YamlProcessor." }
let_it_be(:query) do let_it_be(:query) do
%( %(
...@@ -110,14 +109,6 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -110,14 +109,6 @@ RSpec.describe GitlabSchema.types['Project'] do
expect(analyzer['label']).to eq('Brakeman') expect(analyzer['label']).to eq('Brakeman')
expect(analyzer['enabled']).to eq(true) expect(analyzer['enabled']).to eq(true)
end end
it 'returns an error if there is an exception in YamlProcessor' do
allow_next_instance_of(::Security::CiConfiguration::SastParserService) do |service|
allow(service).to receive(:configuration).and_raise(::Gitlab::Ci::YamlProcessor::ValidationError.new(error_message))
end
expect(subject["errors"].first["message"]).to eql(error_message)
end
end end
describe 'security_scanners' do describe 'security_scanners' do
......
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe "CI YML Templates" do RSpec.describe "CI YML Templates" do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
subject { Gitlab::Ci::YamlProcessor.new(content) } subject { Gitlab::Ci::YamlProcessor.new(content).execute }
where(:template_name) do where(:template_name) do
Gitlab::Template::GitlabCiYmlTemplate.all.map(&:full_name) Gitlab::Template::GitlabCiYmlTemplate.all.map(&:full_name)
...@@ -24,7 +24,7 @@ RSpec.describe "CI YML Templates" do ...@@ -24,7 +24,7 @@ RSpec.describe "CI YML Templates" do
end end
it 'is valid' do it 'is valid' do
expect { subject }.not_to raise_error expect(subject).to be_valid
end end
it 'require default stages to be included' do it 'require default stages to be included' do
......
...@@ -11,7 +11,7 @@ RSpec.describe Gitlab::Ci::YamlProcessor do ...@@ -11,7 +11,7 @@ RSpec.describe Gitlab::Ci::YamlProcessor do
} }
end end
subject { described_class.new(YAML.dump(config)) } subject { described_class.new(YAML.dump(config)).execute }
context 'needs upstream pipeline' do context 'needs upstream pipeline' do
let(:needs) { { pipeline: 'some/project' } } let(:needs) { { pipeline: 'some/project' } }
...@@ -174,9 +174,8 @@ RSpec.describe Gitlab::Ci::YamlProcessor do ...@@ -174,9 +174,8 @@ RSpec.describe Gitlab::Ci::YamlProcessor do
end end
it 'returns errors' do it 'returns errors' do
expect { subject } expect(subject.errors).to include(
.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:bridge config should contain either a trigger or a needs:pipeline')
'jobs:bridge config should contain either a trigger or a needs:pipeline')
end end
end end
...@@ -198,9 +197,8 @@ RSpec.describe Gitlab::Ci::YamlProcessor do ...@@ -198,9 +197,8 @@ RSpec.describe Gitlab::Ci::YamlProcessor do
end end
it 'returns errors' do it 'returns errors' do
expect { subject } expect(subject.errors).to contain_exactly(
.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:test:needs:need ref should be a string')
'jobs:test:needs:need ref should be a string')
end end
end end
end end
...@@ -216,10 +214,10 @@ RSpec.describe Gitlab::Ci::YamlProcessor do ...@@ -216,10 +214,10 @@ RSpec.describe Gitlab::Ci::YamlProcessor do
let(:config) { { deploy_to_production: { stage: 'deploy', script: ['echo'], secrets: secrets } } } let(:config) { { deploy_to_production: { stage: 'deploy', script: ['echo'], secrets: secrets } } }
subject(:processor) { described_class.new(YAML.dump(config)) } subject(:result) { described_class.new(YAML.dump(config)).execute }
it "returns secrets info" do it "returns secrets info" do
secrets = processor.stage_builds_attributes('deploy').first.fetch(:secrets) secrets = result.stage_builds_attributes('deploy').first.fetch(:secrets)
expect(secrets).to eq({ expect(secrets).to eq({
DATABASE_PASSWORD: { DATABASE_PASSWORD: {
......
...@@ -62,6 +62,10 @@ module Gitlab ...@@ -62,6 +62,10 @@ module Gitlab
root.jobs_value root.jobs_value
end end
def normalized_jobs
@normalized_jobs ||= Ci::Config::Normalizer.new(jobs).normalize_jobs
end
private private
def expand_config(config) def expand_config(config)
......
...@@ -11,6 +11,7 @@ module Gitlab ...@@ -11,6 +11,7 @@ module Gitlab
end end
def normalize_jobs def normalize_jobs
return {} unless @jobs_config
return @jobs_config if parallelized_jobs.empty? return @jobs_config if parallelized_jobs.empty?
expand_parallelize_jobs do |job_name, config| expand_parallelize_jobs do |job_name, config|
......
...@@ -45,14 +45,15 @@ module Gitlab ...@@ -45,14 +45,15 @@ module Gitlab
end end
def static_validation(content) def static_validation(content)
result = Gitlab::Ci::YamlProcessor.new_with_validation_errors( result = Gitlab::Ci::YamlProcessor.new(
content, content,
project: @project, project: @project,
user: @current_user, user: @current_user,
sha: @project.repository.commit.sha) sha: @project.repository.commit.sha
).execute
Result.new( Result.new(
jobs: static_validation_convert_to_jobs(result.config&.stages, result.config&.builds), jobs: static_validation_convert_to_jobs(result),
errors: result.errors, errors: result.errors,
warnings: result.warnings warnings: result.warnings
) )
...@@ -76,12 +77,12 @@ module Gitlab ...@@ -76,12 +77,12 @@ module Gitlab
end end
end end
def static_validation_convert_to_jobs(stages, all_jobs) def static_validation_convert_to_jobs(result)
jobs = [] jobs = []
return jobs unless stages || all_jobs return jobs unless result.valid?
stages.each do |stage_name| result.stages.each do |stage_name|
all_jobs.each do |job| result.builds.each do |job|
next unless job[:stage] == stage_name next unless job[:stage] == stage_name
jobs << { jobs << {
......
...@@ -11,20 +11,23 @@ module Gitlab ...@@ -11,20 +11,23 @@ module Gitlab
def perform! def perform!
raise ArgumentError, 'missing config content' unless @command.config_content raise ArgumentError, 'missing config content' unless @command.config_content
@command.config_processor = ::Gitlab::Ci::YamlProcessor.new( result = ::Gitlab::Ci::YamlProcessor.new(
@command.config_content, { @command.config_content, {
project: project, project: project,
sha: @pipeline.sha, sha: @pipeline.sha,
user: current_user, user: current_user,
parent_pipeline: parent_pipeline parent_pipeline: parent_pipeline
} }
) ).execute
add_warnings_to_pipeline(result.warnings)
add_warnings_to_pipeline(@command.config_processor.warnings) if result.valid?
rescue Gitlab::Ci::YamlProcessor::ValidationError => ex @command.config_processor = result
add_warnings_to_pipeline(ex.warnings) else
error(result.errors.first, config_error: true)
end
error(ex.message, config_error: true)
rescue => ex rescue => ex
Gitlab::ErrorTracking.track_exception(ex, Gitlab::ErrorTracking.track_exception(ex,
project_id: project.id, project_id: project.id,
......
# frozen_string_literal: true # frozen_string_literal: true
# This is the CI Linter component that runs the syntax validations
# while parsing the YAML config into a data structure that is
# then presented to the caller as result object.
# After syntax validations (done by Ci::Config), this component also
# runs logical validation on the built data structure.
module Gitlab module Gitlab
module Ci module Ci
class YamlProcessor class YamlProcessor
# ValidationError is treated like a result object in the form of an exception. ValidationError = Class.new(StandardError)
# We can return any warnings, raised during the config validation, along with
# the error object until we support multiple messages to be returned.
class ValidationError < StandardError
attr_reader :warnings
def initialize(message, warnings: [])
@warnings = warnings
super(message)
end
end
include Gitlab::Config::Entry::LegacyValidationHelpers
attr_reader :stages, :jobs def self.validation_message(content, opts = {})
result = new(content, opts).execute
class Result result.errors.first
attr_reader :config, :errors, :warnings end
def initialize(config: nil, errors: [], warnings: []) def initialize(config_content, opts = {})
@config = config @config_content = config_content
@errors = errors @opts = opts
@warnings = warnings end
end
def valid? def execute
config.present? && errors.empty? if @config_content.blank?
return Result.new(errors: ['Please provide content of .gitlab-ci.yml'])
end end
end
def initialize(config, opts = {}) @ci_config = Gitlab::Ci::Config.new(@config_content, **@opts)
@ci_config = Gitlab::Ci::Config.new(config, **opts)
@config = @ci_config.to_hash
unless @ci_config.valid? unless @ci_config.valid?
error!(@ci_config.errors.first) return Result.new(ci_config: @ci_config, errors: @ci_config.errors, warnings: @ci_config.warnings)
end end
initial_parsing run_logical_validations!
rescue Gitlab::Ci::Config::ConfigError => e
error!(e.message)
end
def self.new_with_validation_errors(content, opts = {})
return Result.new(errors: ['Please provide content of .gitlab-ci.yml']) if content.blank?
config = Gitlab::Ci::Config.new(content, **opts) Result.new(ci_config: @ci_config, warnings: @ci_config&.warnings)
return Result.new(errors: config.errors, warnings: config.warnings) unless config.valid?
config = Gitlab::Ci::YamlProcessor.new(content, opts)
Result.new(config: config, warnings: config.warnings)
rescue ValidationError => e
Result.new(errors: [e.message], warnings: e.warnings)
rescue Gitlab::Ci::Config::ConfigError => e rescue Gitlab::Ci::Config::ConfigError => e
Result.new(errors: [e.message]) Result.new(ci_config: @ci_config, errors: [e.message], warnings: @ci_config&.warnings)
end
def warnings
@ci_config&.warnings || []
end
def builds
@jobs.map do |name, _|
build_attributes(name)
end
end
def build_attributes(name)
job = @jobs.fetch(name.to_sym, {})
{ stage_idx: @stages.index(job[:stage]),
stage: job[:stage],
tag_list: job[:tags],
name: job[:name].to_s,
allow_failure: job[:ignore],
when: job[:when] || 'on_success',
environment: job[:environment_name],
coverage_regex: job[:coverage],
yaml_variables: transform_to_yaml_variables(job[:variables]),
needs_attributes: job.dig(:needs, :job),
interruptible: job[:interruptible],
only: job[:only],
except: job[:except],
rules: job[:rules],
cache: job[:cache],
resource_group_key: job[:resource_group],
scheduling_type: job[:scheduling_type],
secrets: job[:secrets],
options: {
image: job[:image],
services: job[:services],
artifacts: job[:artifacts],
dependencies: job[:dependencies],
cross_dependencies: job.dig(:needs, :cross_dependency),
job_timeout: job[:timeout],
before_script: job[:before_script],
script: job[:script],
after_script: job[:after_script],
environment: job[:environment],
retry: job[:retry],
parallel: job[:parallel],
instance: job[:instance],
start_in: job[:start_in],
trigger: job[:trigger],
bridge_needs: job.dig(:needs, :bridge)&.first,
release: release(job)
}.compact }.compact
end
def release(job) rescue ValidationError => e
job[:release] Result.new(ci_config: @ci_config, errors: [e.message], warnings: @ci_config&.warnings)
end
def stage_builds_attributes(stage)
@jobs.values
.select { |job| job[:stage] == stage }
.map { |job| build_attributes(job[:name]) }
end
def stages_attributes
@stages.uniq.map do |stage|
seeds = stage_builds_attributes(stage)
{ name: stage, index: @stages.index(stage), builds: seeds }
end
end
def workflow_attributes
{
rules: @config.dig(:workflow, :rules),
yaml_variables: transform_to_yaml_variables(@variables)
}
end
def self.validation_message(content, opts = {})
return 'Please provide content of .gitlab-ci.yml' if content.blank?
begin
Gitlab::Ci::YamlProcessor.new(content, opts)
nil
rescue ValidationError => e
e.message
end
end end
private private
def initial_parsing def run_logical_validations!
##
# Global config
#
@variables = @ci_config.variables
@stages = @ci_config.stages @stages = @ci_config.stages
@jobs = @ci_config.normalized_jobs
##
# Jobs
#
@jobs = Ci::Config::Normalizer.new(@ci_config.jobs).normalize_jobs
@jobs.each do |name, job| @jobs.each do |name, job|
# logical validation for job validate_job!(name, job)
validate_job_stage!(name, job)
validate_job_dependencies!(name, job)
validate_job_needs!(name, job)
validate_dynamic_child_pipeline_dependencies!(name, job)
validate_job_environment!(name, job)
end end
end end
def transform_to_yaml_variables(variables) def validate_job!(name, job)
variables.to_h.map do |key, value| validate_job_stage!(name, job)
{ key: key.to_s, value: value, public: true } validate_job_dependencies!(name, job)
end validate_job_needs!(name, job)
validate_dynamic_child_pipeline_dependencies!(name, job)
validate_job_environment!(name, job)
end end
def validate_job_stage!(name, job) def validate_job_stage!(name, job)
...@@ -188,10 +70,6 @@ module Gitlab ...@@ -188,10 +70,6 @@ module Gitlab
end end
end end
def error!(message)
raise ValidationError.new(message, warnings: warnings)
end
def validate_job_dependencies!(name, job) def validate_job_dependencies!(name, job)
return unless job[:dependencies] return unless job[:dependencies]
...@@ -267,6 +145,10 @@ module Gitlab ...@@ -267,6 +145,10 @@ module Gitlab
error!("#{name} job: on_stop job #{on_stop} needs to have action stop defined") error!("#{name} job: on_stop job #{on_stop} needs to have action stop defined")
end end
end end
def error!(message)
raise ValidationError.new(message)
end
end end
end end
end end
# frozen_string_literal: true
# A data object that wraps `Ci::Config` and any messages
# (errors, warnings) generated by the YamlProcessor.
module Gitlab
module Ci
class YamlProcessor
class Result
attr_reader :errors, :warnings
def initialize(ci_config: nil, errors: [], warnings: [])
@ci_config = ci_config
@errors = errors || []
@warnings = warnings || []
end
def valid?
errors.empty?
end
def stages_attributes
stages.uniq.map do |stage|
seeds = stage_builds_attributes(stage)
{ name: stage, index: stages.index(stage), builds: seeds }
end
end
def builds
jobs.map do |name, _|
build_attributes(name)
end
end
def stage_builds_attributes(stage)
jobs.values
.select { |job| job[:stage] == stage }
.map { |job| build_attributes(job[:name]) }
end
def workflow_attributes
{
rules: hash_config.dig(:workflow, :rules),
yaml_variables: transform_to_yaml_variables(variables)
}
end
def jobs
@jobs ||= @ci_config.normalized_jobs
end
def stages
@stages ||= @ci_config.stages
end
def build_attributes(name)
job = jobs.fetch(name.to_sym, {})
{ stage_idx: stages.index(job[:stage]),
stage: job[:stage],
tag_list: job[:tags],
name: job[:name].to_s,
allow_failure: job[:ignore],
when: job[:when] || 'on_success',
environment: job[:environment_name],
coverage_regex: job[:coverage],
yaml_variables: transform_to_yaml_variables(job[:variables]),
needs_attributes: job.dig(:needs, :job),
interruptible: job[:interruptible],
only: job[:only],
except: job[:except],
rules: job[:rules],
cache: job[:cache],
resource_group_key: job[:resource_group],
scheduling_type: job[:scheduling_type],
secrets: job[:secrets],
options: {
image: job[:image],
services: job[:services],
artifacts: job[:artifacts],
dependencies: job[:dependencies],
cross_dependencies: job.dig(:needs, :cross_dependency),
job_timeout: job[:timeout],
before_script: job[:before_script],
script: job[:script],
after_script: job[:after_script],
environment: job[:environment],
retry: job[:retry],
parallel: job[:parallel],
instance: job[:instance],
start_in: job[:start_in],
trigger: job[:trigger],
bridge_needs: job.dig(:needs, :bridge)&.first,
release: release(job)
}.compact }.compact
end
private
def variables
@variables ||= @ci_config.variables
end
def hash_config
@hash_config ||= @ci_config.to_hash
end
def release(job)
job[:release]
end
def transform_to_yaml_variables(variables)
variables.to_h.map do |key, value|
{ key: key.to_s, value: value, public: true }
end
end
end
end
end
end
...@@ -98,7 +98,7 @@ RSpec.describe Projects::Ci::LintsController do ...@@ -98,7 +98,7 @@ RSpec.describe Projects::Ci::LintsController do
it_behaves_like 'returns a successful validation' it_behaves_like 'returns a successful validation'
it 'runs validations through YamlProcessor' do it 'runs validations through YamlProcessor' do
expect(Gitlab::Ci::YamlProcessor).to receive(:new_with_validation_errors).and_call_original expect(Gitlab::Ci::YamlProcessor).to receive(:new).and_call_original
subject subject
end end
...@@ -126,7 +126,7 @@ RSpec.describe Projects::Ci::LintsController do ...@@ -126,7 +126,7 @@ RSpec.describe Projects::Ci::LintsController do
it_behaves_like 'returns a successful validation' it_behaves_like 'returns a successful validation'
it 'runs validations through YamlProcessor' do it 'runs validations through YamlProcessor' do
expect(Gitlab::Ci::YamlProcessor).to receive(:new_with_validation_errors).and_call_original expect(Gitlab::Ci::YamlProcessor).to receive(:new).and_call_original
subject subject
end end
......
import { shallowMount, mount } from '@vue/test-utils';
import MergeConflictWarning from '~/diffs/components/merge_conflict_warning.vue';
import { CENTERED_LIMITED_CONTAINER_CLASSES } from '~/diffs/constants';
const propsData = {
limited: true,
mergeable: true,
resolutionPath: 'a-path',
};
const limitedClasses = CENTERED_LIMITED_CONTAINER_CLASSES.split(' ');
function findResolveButton(wrapper) {
return wrapper.find('.gl-alert-actions a.gl-button:first-child');
}
function findLocalMergeButton(wrapper) {
return wrapper.find('.gl-alert-actions button.gl-button:last-child');
}
describe('MergeConflictWarning', () => {
let wrapper;
const createComponent = (props = {}, { full } = { full: false }) => {
const mounter = full ? mount : shallowMount;
wrapper = mounter(MergeConflictWarning, {
propsData: { ...propsData, ...props },
});
};
afterEach(() => {
wrapper.destroy();
});
it.each`
limited | containerClasses
${true} | ${limitedClasses}
${false} | ${[]}
`(
'has the correct container classes when limited is $limited',
({ limited, containerClasses }) => {
createComponent({ limited });
expect(wrapper.classes()).toEqual(containerClasses);
},
);
it.each`
present | resolutionPath
${false} | ${''}
${true} | ${'some-path'}
`(
'toggles the resolve conflicts button based on the provided resolutionPath "$resolutionPath"',
({ present, resolutionPath }) => {
createComponent({ resolutionPath }, { full: true });
const resolveButton = findResolveButton(wrapper);
expect(resolveButton.exists()).toBe(present);
if (present) {
expect(resolveButton.attributes('href')).toBe(resolutionPath);
}
},
);
it.each`
present | mergeable
${false} | ${false}
${true} | ${true}
`(
'toggles the local merge button based on the provided mergeable property "$mergable"',
({ present, mergeable }) => {
createComponent({ mergeable }, { full: true });
const localMerge = findLocalMergeButton(wrapper);
expect(localMerge.exists()).toBe(present);
},
);
});
...@@ -264,5 +264,11 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do ...@@ -264,5 +264,11 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do
is_expected.to match(config) is_expected.to match(config)
end end
end end
context 'when jobs config is nil' do
let(:config) { nil }
it { is_expected.to eq({}) }
end
end end
end end
...@@ -312,7 +312,7 @@ RSpec.describe Gitlab::Ci::Config do ...@@ -312,7 +312,7 @@ RSpec.describe Gitlab::Ci::Config do
HEREDOC HEREDOC
end end
it 'raises error YamlProcessor validationError' do it 'raises ConfigError' do
expect { config }.to raise_error( expect { config }.to raise_error(
described_class::ConfigError, described_class::ConfigError,
"Included file `invalid` does not have YAML extension!" "Included file `invalid` does not have YAML extension!"
...@@ -329,7 +329,7 @@ RSpec.describe Gitlab::Ci::Config do ...@@ -329,7 +329,7 @@ RSpec.describe Gitlab::Ci::Config do
HEREDOC HEREDOC
end end
it 'raises error YamlProcessor validationError' do it 'raises ConfigError' do
expect { config }.to raise_error( expect { config }.to raise_error(
described_class::ConfigError, described_class::ConfigError,
'Include `{"remote":"http://url","local":"/local/file.yml"}` needs to match exactly one accessor!' 'Include `{"remote":"http://url","local":"/local/file.yml"}` needs to match exactly one accessor!'
......
...@@ -157,7 +157,7 @@ RSpec.describe Gitlab::Ci::Lint do ...@@ -157,7 +157,7 @@ RSpec.describe Gitlab::Ci::Lint do
it 'uses YamlProcessor' do it 'uses YamlProcessor' do
expect(Gitlab::Ci::YamlProcessor) expect(Gitlab::Ci::YamlProcessor)
.to receive(:new_with_validation_errors) .to receive(:new)
.and_call_original .and_call_original
subject subject
......
...@@ -31,20 +31,20 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do ...@@ -31,20 +31,20 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do
CI_YAML CI_YAML
end end
let(:yaml_processor) do let(:yaml_processor_result) do
::Gitlab::Ci::YamlProcessor.new( ::Gitlab::Ci::YamlProcessor.new(
ci_yaml, { ci_yaml, {
project: project, project: project,
sha: pipeline.sha, sha: pipeline.sha,
user: user user: user
} }
) ).execute
end end
let(:save_incompleted) { true } let(:save_incompleted) { true }
let(:command) do let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new( Gitlab::Ci::Pipeline::Chain::Command.new(
project: project, current_user: user, config_processor: yaml_processor, save_incompleted: save_incompleted project: project, current_user: user, config_processor: yaml_processor_result, save_incompleted: save_incompleted
) )
end end
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'CI YML Templates' do RSpec.describe 'CI YML Templates' do
subject { Gitlab::Ci::YamlProcessor.new(content) } subject { Gitlab::Ci::YamlProcessor.new(content).execute }
let(:all_templates) { Gitlab::Template::GitlabCiYmlTemplate.all.map(&:full_name) } let(:all_templates) { Gitlab::Template::GitlabCiYmlTemplate.all.map(&:full_name) }
...@@ -33,7 +33,7 @@ RSpec.describe 'CI YML Templates' do ...@@ -33,7 +33,7 @@ RSpec.describe 'CI YML Templates' do
end end
it 'is valid' do it 'is valid' do
expect { subject }.not_to raise_error expect(subject).to be_valid
end end
it 'require default stages to be included' do it 'require default stages to be included' do
......
This diff is collapsed.
...@@ -133,12 +133,6 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer do ...@@ -133,12 +133,6 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer do
expect(builds_count).to eq(1) expect(builds_count).to eq(1)
end end
it 'has no when YML attributes but only the DB column' do
expect_any_instance_of(Gitlab::Ci::YamlProcessor).not_to receive(:build_attributes)
subject
end
it 'has pipeline commits' do it 'has pipeline commits' do
expect(subject['ci_pipelines']).not_to be_empty expect(subject['ci_pipelines']).not_to be_empty
end end
......
...@@ -381,12 +381,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do ...@@ -381,12 +381,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do
expect(project_tree_saver.save).to be true expect(project_tree_saver.save).to be true
end end
it 'has no when YML attributes but only the DB column' do
expect_any_instance_of(Gitlab::Ci::YamlProcessor).not_to receive(:build_attributes)
project_tree_saver.save
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