gitlab-ci-yaml_lint 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +8 -0
  6. data/LICENSE +20 -0
  7. data/README.md +50 -0
  8. data/Rakefile +6 -0
  9. data/bin/gitlab-ci-yaml_lint +13 -0
  10. data/gitlab-ci-yaml_lint.gemspec +31 -0
  11. data/gitlab_lib/LICENSE +27 -0
  12. data/gitlab_lib/gitlab/ci/config.rb +62 -0
  13. data/gitlab_lib/gitlab/ci/config/entry/artifacts.rb +35 -0
  14. data/gitlab_lib/gitlab/ci/config/entry/attributable.rb +27 -0
  15. data/gitlab_lib/gitlab/ci/config/entry/boolean.rb +18 -0
  16. data/gitlab_lib/gitlab/ci/config/entry/cache.rb +45 -0
  17. data/gitlab_lib/gitlab/ci/config/entry/commands.rb +33 -0
  18. data/gitlab_lib/gitlab/ci/config/entry/configurable.rb +77 -0
  19. data/gitlab_lib/gitlab/ci/config/entry/coverage.rb +22 -0
  20. data/gitlab_lib/gitlab/ci/config/entry/environment.rb +83 -0
  21. data/gitlab_lib/gitlab/ci/config/entry/factory.rb +73 -0
  22. data/gitlab_lib/gitlab/ci/config/entry/global.rb +72 -0
  23. data/gitlab_lib/gitlab/ci/config/entry/hidden.rb +22 -0
  24. data/gitlab_lib/gitlab/ci/config/entry/image.rb +47 -0
  25. data/gitlab_lib/gitlab/ci/config/entry/job.rb +157 -0
  26. data/gitlab_lib/gitlab/ci/config/entry/jobs.rb +52 -0
  27. data/gitlab_lib/gitlab/ci/config/entry/key.rb +22 -0
  28. data/gitlab_lib/gitlab/ci/config/entry/legacy_validation_helpers.rb +61 -0
  29. data/gitlab_lib/gitlab/ci/config/entry/node.rb +101 -0
  30. data/gitlab_lib/gitlab/ci/config/entry/paths.rb +18 -0
  31. data/gitlab_lib/gitlab/ci/config/entry/policy.rb +53 -0
  32. data/gitlab_lib/gitlab/ci/config/entry/script.rb +18 -0
  33. data/gitlab_lib/gitlab/ci/config/entry/service.rb +34 -0
  34. data/gitlab_lib/gitlab/ci/config/entry/services.rb +41 -0
  35. data/gitlab_lib/gitlab/ci/config/entry/simplifiable.rb +43 -0
  36. data/gitlab_lib/gitlab/ci/config/entry/stage.rb +22 -0
  37. data/gitlab_lib/gitlab/ci/config/entry/stages.rb +22 -0
  38. data/gitlab_lib/gitlab/ci/config/entry/undefined.rb +40 -0
  39. data/gitlab_lib/gitlab/ci/config/entry/unspecified.rb +19 -0
  40. data/gitlab_lib/gitlab/ci/config/entry/validatable.rb +38 -0
  41. data/gitlab_lib/gitlab/ci/config/entry/validator.rb +26 -0
  42. data/gitlab_lib/gitlab/ci/config/entry/validators.rb +144 -0
  43. data/gitlab_lib/gitlab/ci/config/entry/variables.rb +26 -0
  44. data/gitlab_lib/gitlab/ci/config/loader.rb +25 -0
  45. data/gitlab_lib/gitlab/ci/yaml_processor.rb +189 -0
  46. data/lib/gitlab/ci/yaml_lint.rb +73 -0
  47. data/lib/gitlab/ci/yaml_lint/rake/tasks.rb +28 -0
  48. data/lib/gitlab/ci/yaml_lint/version.rb +7 -0
  49. metadata +203 -0
@@ -0,0 +1,22 @@
1
+ module Gitlab
2
+ module Ci
3
+ class Config
4
+ module Entry
5
+ ##
6
+ # Entry that represents a stage for a job.
7
+ #
8
+ class Stage < Node
9
+ include Validatable
10
+
11
+ validations do
12
+ validates :config, type: String
13
+ end
14
+
15
+ def self.default
16
+ 'test'
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ module Gitlab
2
+ module Ci
3
+ class Config
4
+ module Entry
5
+ ##
6
+ # Entry that represents a configuration for pipeline stages.
7
+ #
8
+ class Stages < Node
9
+ include Validatable
10
+
11
+ validations do
12
+ validates :config, array_of_strings: true
13
+ end
14
+
15
+ def self.default
16
+ %w[build test deploy]
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,40 @@
1
+ module Gitlab
2
+ module Ci
3
+ class Config
4
+ module Entry
5
+ ##
6
+ # This class represents an undefined entry.
7
+ #
8
+ class Undefined < Node
9
+ def initialize(*)
10
+ super(nil)
11
+ end
12
+
13
+ def value
14
+ nil
15
+ end
16
+
17
+ def valid?
18
+ true
19
+ end
20
+
21
+ def errors
22
+ []
23
+ end
24
+
25
+ def specified?
26
+ false
27
+ end
28
+
29
+ def relevant?
30
+ false
31
+ end
32
+
33
+ def inspect
34
+ "#<#{self.class.name}>"
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,19 @@
1
+ module Gitlab
2
+ module Ci
3
+ class Config
4
+ module Entry
5
+ ##
6
+ # This class represents an unspecified entry.
7
+ #
8
+ # It decorates original entry adding method that indicates it is
9
+ # unspecified.
10
+ #
11
+ class Unspecified < SimpleDelegator
12
+ def specified?
13
+ false
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,38 @@
1
+ module Gitlab
2
+ module Ci
3
+ class Config
4
+ module Entry
5
+ module Validatable
6
+ extend ActiveSupport::Concern
7
+
8
+ def self.included(node)
9
+ node.aspects.append -> do
10
+ @validator = self.class.validator.new(self)
11
+ @validator.validate(:new)
12
+ end
13
+ end
14
+
15
+ def errors
16
+ @validator.messages + descendants.flat_map(&:errors) # rubocop:disable Gitlab/ModuleWithInstanceVariables
17
+ end
18
+
19
+ class_methods do
20
+ def validator
21
+ @validator ||= Class.new(Entry::Validator).tap do |validator|
22
+ if defined?(@validations)
23
+ @validations.each { |rules| validator.class_eval(&rules) }
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def validations(&block)
31
+ (@validations ||= []).append(block)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,26 @@
1
+ module Gitlab
2
+ module Ci
3
+ class Config
4
+ module Entry
5
+ class Validator < SimpleDelegator
6
+ include ActiveModel::Validations
7
+ include Entry::Validators
8
+
9
+ def initialize(entry)
10
+ super(entry)
11
+ end
12
+
13
+ def messages
14
+ errors.full_messages.map do |error|
15
+ "#{location} #{error}".downcase
16
+ end
17
+ end
18
+
19
+ def self.name
20
+ 'Validator'
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,144 @@
1
+ module Gitlab
2
+ module Ci
3
+ class Config
4
+ module Entry
5
+ module Validators
6
+ class AllowedKeysValidator < ActiveModel::EachValidator
7
+ def validate_each(record, attribute, value)
8
+ unknown_keys = record.config.try(:keys).to_a - options[:in]
9
+
10
+ if unknown_keys.any?
11
+ record.errors.add(:config, 'contains unknown keys: ' +
12
+ unknown_keys.join(', '))
13
+ end
14
+ end
15
+ end
16
+
17
+ class AllowedValuesValidator < ActiveModel::EachValidator
18
+ def validate_each(record, attribute, value)
19
+ unless options[:in].include?(value.to_s)
20
+ record.errors.add(attribute, "unknown value: #{value}")
21
+ end
22
+ end
23
+ end
24
+
25
+ class ArrayOfStringsValidator < ActiveModel::EachValidator
26
+ include LegacyValidationHelpers
27
+
28
+ def validate_each(record, attribute, value)
29
+ unless validate_array_of_strings(value)
30
+ record.errors.add(attribute, 'should be an array of strings')
31
+ end
32
+ end
33
+ end
34
+
35
+ class BooleanValidator < ActiveModel::EachValidator
36
+ include LegacyValidationHelpers
37
+
38
+ def validate_each(record, attribute, value)
39
+ unless validate_boolean(value)
40
+ record.errors.add(attribute, 'should be a boolean value')
41
+ end
42
+ end
43
+ end
44
+
45
+ class DurationValidator < ActiveModel::EachValidator
46
+ include LegacyValidationHelpers
47
+
48
+ def validate_each(record, attribute, value)
49
+ unless validate_duration(value)
50
+ record.errors.add(attribute, 'should be a duration')
51
+ end
52
+ end
53
+ end
54
+
55
+ class HashOrStringValidator < ActiveModel::EachValidator
56
+ def validate_each(record, attribute, value)
57
+ unless value.is_a?(Hash) || value.is_a?(String)
58
+ record.errors.add(attribute, 'should be a hash or a string')
59
+ end
60
+ end
61
+ end
62
+
63
+ class KeyValidator < ActiveModel::EachValidator
64
+ include LegacyValidationHelpers
65
+
66
+ def validate_each(record, attribute, value)
67
+ unless validate_string(value)
68
+ record.errors.add(attribute, 'should be a string or symbol')
69
+ end
70
+ end
71
+ end
72
+
73
+ class RegexpValidator < ActiveModel::EachValidator
74
+ include LegacyValidationHelpers
75
+
76
+ def validate_each(record, attribute, value)
77
+ unless validate_regexp(value)
78
+ record.errors.add(attribute, 'must be a regular expression')
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def look_like_regexp?(value)
85
+ value.is_a?(String) && value.start_with?('/') &&
86
+ value.end_with?('/')
87
+ end
88
+
89
+ def validate_regexp(value)
90
+ look_like_regexp?(value) &&
91
+ Regexp.new(value.to_s[1...-1]) &&
92
+ true
93
+ rescue RegexpError
94
+ false
95
+ end
96
+ end
97
+
98
+ class ArrayOfStringsOrRegexpsValidator < RegexpValidator
99
+ def validate_each(record, attribute, value)
100
+ unless validate_array_of_strings_or_regexps(value)
101
+ record.errors.add(attribute, 'should be an array of strings or regexps')
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ def validate_array_of_strings_or_regexps(values)
108
+ values.is_a?(Array) && values.all?(&method(:validate_string_or_regexp))
109
+ end
110
+
111
+ def validate_string_or_regexp(value)
112
+ return false unless value.is_a?(String)
113
+ return validate_regexp(value) if look_like_regexp?(value)
114
+
115
+ true
116
+ end
117
+ end
118
+
119
+ class TypeValidator < ActiveModel::EachValidator
120
+ def validate_each(record, attribute, value)
121
+ type = options[:with]
122
+ raise unless type.is_a?(Class)
123
+
124
+ unless value.is_a?(type)
125
+ message = options[:message] || "should be a #{type.name}"
126
+ record.errors.add(attribute, message)
127
+ end
128
+ end
129
+ end
130
+
131
+ class VariablesValidator < ActiveModel::EachValidator
132
+ include LegacyValidationHelpers
133
+
134
+ def validate_each(record, attribute, value)
135
+ unless validate_variables(value)
136
+ record.errors.add(attribute, 'should be a hash of key value pairs')
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,26 @@
1
+ module Gitlab
2
+ module Ci
3
+ class Config
4
+ module Entry
5
+ ##
6
+ # Entry that represents environment variables.
7
+ #
8
+ class Variables < Node
9
+ include Validatable
10
+
11
+ validations do
12
+ validates :config, variables: true
13
+ end
14
+
15
+ def self.default
16
+ {}
17
+ end
18
+
19
+ def value
20
+ Hash[@config.map { |key, value| [key.to_s, value.to_s] }]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ module Gitlab
2
+ module Ci
3
+ class Config
4
+ class Loader
5
+ FormatError = Class.new(StandardError)
6
+
7
+ def initialize(config)
8
+ @config = YAML.safe_load(config, [Symbol], [], true)
9
+ end
10
+
11
+ def valid?
12
+ @config.is_a?(Hash)
13
+ end
14
+
15
+ def load!
16
+ unless valid?
17
+ raise FormatError, 'Invalid configuration format'
18
+ end
19
+
20
+ @config.deep_symbolize_keys
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,189 @@
1
+ module Gitlab
2
+ module Ci
3
+ class YamlProcessor
4
+ ValidationError = Class.new(StandardError)
5
+
6
+ include Gitlab::Ci::Config::Entry::LegacyValidationHelpers
7
+
8
+ attr_reader :cache, :stages, :jobs
9
+
10
+ def initialize(config)
11
+ @ci_config = Gitlab::Ci::Config.new(config)
12
+ @config = @ci_config.to_hash
13
+
14
+ unless @ci_config.valid?
15
+ raise ValidationError, @ci_config.errors.first
16
+ end
17
+
18
+ initial_parsing
19
+ rescue Gitlab::Ci::Config::Loader::FormatError => e
20
+ raise ValidationError, e.message
21
+ end
22
+
23
+ def builds
24
+ @jobs.map do |name, _|
25
+ build_attributes(name)
26
+ end
27
+ end
28
+
29
+ def build_attributes(name)
30
+ job = @jobs[name.to_sym] || {}
31
+
32
+ { stage_idx: @stages.index(job[:stage]),
33
+ stage: job[:stage],
34
+ commands: job[:commands],
35
+ tag_list: job[:tags] || [],
36
+ name: job[:name].to_s,
37
+ allow_failure: job[:ignore],
38
+ when: job[:when] || 'on_success',
39
+ environment: job[:environment_name],
40
+ coverage_regex: job[:coverage],
41
+ yaml_variables: yaml_variables(name),
42
+ options: {
43
+ image: job[:image],
44
+ services: job[:services],
45
+ artifacts: job[:artifacts],
46
+ cache: job[:cache],
47
+ dependencies: job[:dependencies],
48
+ before_script: job[:before_script],
49
+ script: job[:script],
50
+ after_script: job[:after_script],
51
+ environment: job[:environment],
52
+ retry: job[:retry]
53
+ }.compact }
54
+ end
55
+
56
+ def pipeline_stage_builds(stage, pipeline)
57
+ selected_jobs = @jobs.select do |_, job|
58
+ next unless job[:stage] == stage
59
+
60
+ only_specs = Gitlab::Ci::Build::Policy
61
+ .fabricate(job.fetch(:only, {}))
62
+ except_specs = Gitlab::Ci::Build::Policy
63
+ .fabricate(job.fetch(:except, {}))
64
+
65
+ only_specs.all? { |spec| spec.satisfied_by?(pipeline) } &&
66
+ except_specs.none? { |spec| spec.satisfied_by?(pipeline) }
67
+ end
68
+
69
+ selected_jobs.map { |_, job| build_attributes(job[:name]) }
70
+ end
71
+
72
+ def stage_seeds(pipeline)
73
+ seeds = @stages.uniq.map do |stage|
74
+ builds = pipeline_stage_builds(stage, pipeline)
75
+
76
+ Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any?
77
+ end
78
+
79
+ seeds.compact
80
+ end
81
+
82
+ def self.validation_message(content)
83
+ return 'Please provide content of .gitlab-ci.yml' if content.blank?
84
+
85
+ begin
86
+ Gitlab::Ci::YamlProcessor.new(content)
87
+ nil
88
+ rescue ValidationError, Psych::SyntaxError => e
89
+ e.message
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def initial_parsing
96
+ ##
97
+ # Global config
98
+ #
99
+ @before_script = @ci_config.before_script
100
+ @image = @ci_config.image
101
+ @after_script = @ci_config.after_script
102
+ @services = @ci_config.services
103
+ @variables = @ci_config.variables
104
+ @stages = @ci_config.stages
105
+ @cache = @ci_config.cache
106
+
107
+ ##
108
+ # Jobs
109
+ #
110
+ @jobs = @ci_config.jobs
111
+
112
+ @jobs.each do |name, job|
113
+ # logical validation for job
114
+
115
+ validate_job_stage!(name, job)
116
+ validate_job_dependencies!(name, job)
117
+ validate_job_environment!(name, job)
118
+ end
119
+ end
120
+
121
+ def yaml_variables(name)
122
+ variables = (@variables || {})
123
+ .merge(job_variables(name))
124
+
125
+ variables.map do |key, value|
126
+ { key: key.to_s, value: value, public: true }
127
+ end
128
+ end
129
+
130
+ def job_variables(name)
131
+ job = @jobs[name.to_sym]
132
+ return {} unless job
133
+
134
+ job[:variables] || {}
135
+ end
136
+
137
+ def validate_job_stage!(name, job)
138
+ return unless job[:stage]
139
+
140
+ unless job[:stage].is_a?(String) && job[:stage].in?(@stages)
141
+ raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}"
142
+ end
143
+ end
144
+
145
+ def validate_job_dependencies!(name, job)
146
+ return unless job[:dependencies]
147
+
148
+ stage_index = @stages.index(job[:stage])
149
+
150
+ job[:dependencies].each do |dependency|
151
+ raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym]
152
+
153
+ unless @stages.index(@jobs[dependency.to_sym][:stage]) < stage_index
154
+ raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
155
+ end
156
+ end
157
+ end
158
+
159
+ def validate_job_environment!(name, job)
160
+ return unless job[:environment]
161
+ return unless job[:environment].is_a?(Hash)
162
+
163
+ environment = job[:environment]
164
+ validate_on_stop_job!(name, environment, environment[:on_stop])
165
+ end
166
+
167
+ def validate_on_stop_job!(name, environment, on_stop)
168
+ return unless on_stop
169
+
170
+ on_stop_job = @jobs[on_stop.to_sym]
171
+ unless on_stop_job
172
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} is not defined"
173
+ end
174
+
175
+ unless on_stop_job[:environment]
176
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} does not have environment defined"
177
+ end
178
+
179
+ unless on_stop_job[:environment][:name] == environment[:name]
180
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} have different environment name"
181
+ end
182
+
183
+ unless on_stop_job[:environment][:action] == 'stop'
184
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined"
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end