198 lines
6.2 KiB
Ruby
Executable file
198 lines
6.2 KiB
Ruby
Executable file
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
|
|
# We don't have auto-loading here
|
|
require_relative '../lib/gitlab'
|
|
require_relative '../lib/gitlab/popen'
|
|
require_relative '../lib/gitlab/popen/runner'
|
|
|
|
class StaticAnalysis
|
|
ALLOWED_WARNINGS = [
|
|
# https://github.com/browserslist/browserslist/blob/d0ec62eb48c41c218478cd3ac28684df051cc865/node.js#L329
|
|
# warns if caniuse-lite package is older than 6 months. Ignore this
|
|
# warning message so that GitLab backports don't fail.
|
|
"Browserslist: caniuse-lite is outdated. Please run next command `yarn upgrade`"
|
|
].freeze
|
|
|
|
Task = Struct.new(:command, :duration) do
|
|
def cmd
|
|
command.join(' ')
|
|
end
|
|
end
|
|
NodeAssignment = Struct.new(:index, :tasks) do
|
|
def total_duration
|
|
return 0 if tasks.empty?
|
|
|
|
tasks.sum(&:duration)
|
|
end
|
|
end
|
|
|
|
def self.project_path
|
|
project_root = File.expand_path('..', __dir__)
|
|
|
|
if Gitlab.jh?
|
|
"#{project_root}/jh"
|
|
else
|
|
project_root
|
|
end
|
|
end
|
|
|
|
# `gettext:updated_check` and `gitlab:sidekiq:sidekiq_queues_yml:check` will fail on FOSS installations
|
|
# (e.g. gitlab-org/gitlab-foss) since they test against a single
|
|
# file that is generated by an EE installation, which can
|
|
# contain values that a FOSS installation won't find. To work
|
|
# around this we will only enable this task on EE installations.
|
|
TASKS_WITH_DURATIONS_SECONDS = [
|
|
(Gitlab.ee? ? Task.new(%w[bin/rake gettext:updated_check], 360) : nil),
|
|
Task.new(%w[yarn run lint:prettier], 160),
|
|
Task.new(%w[bin/rake gettext:lint], 85),
|
|
Task.new(%W[bundle exec license_finder --decisions-file config/dependency_decisions.yml --project-path #{project_path}], 20),
|
|
Task.new(%w[bin/rake lint:static_verification], 35),
|
|
Task.new(%w[scripts/rubocop-max-files-in-cache-check], 20),
|
|
Task.new(%w[bin/rake config_lint], 10),
|
|
Task.new(%w[bin/rake gitlab:sidekiq:all_queues_yml:check], 15),
|
|
(Gitlab.ee? ? Task.new(%w[bin/rake gitlab:sidekiq:sidekiq_queues_yml:check], 11) : nil),
|
|
Task.new(%w[yarn run internal:stylelint], 8),
|
|
Task.new(%w[scripts/lint-conflicts.sh], 1),
|
|
Task.new(%w[yarn run block-dependencies], 1),
|
|
Task.new(%w[scripts/lint-rugged], 1),
|
|
Task.new(%w[scripts/gemfile_lock_changed.sh], 1)
|
|
].compact.freeze
|
|
|
|
def run_tasks!(options = {})
|
|
total_nodes = (ENV['CI_NODE_TOTAL'] || 1).to_i
|
|
current_node_number = (ENV['CI_NODE_INDEX'] || 1).to_i
|
|
node_assignment = tasks_to_run(total_nodes)[current_node_number - 1]
|
|
|
|
if options[:dry_run]
|
|
puts "Dry-run mode!"
|
|
return
|
|
end
|
|
|
|
static_analysis = Gitlab::Popen::Runner.new
|
|
start_time = Time.now
|
|
static_analysis.run(node_assignment.tasks.map(&:command)) do |command, &run|
|
|
task = node_assignment.tasks.find { |task| task.command == command }
|
|
puts
|
|
puts "$ #{task.cmd}"
|
|
|
|
result = run.call
|
|
|
|
puts "==> Finished in #{result.duration} seconds (expected #{task.duration} seconds)"
|
|
puts
|
|
end
|
|
|
|
puts
|
|
puts '==================================================='
|
|
puts "Node finished running all tasks in #{Time.now - start_time} seconds (expected #{node_assignment.total_duration})"
|
|
puts
|
|
puts
|
|
|
|
if static_analysis.all_success_and_clean?
|
|
puts 'All static analyses passed successfully.'
|
|
elsif static_analysis.all_success?
|
|
puts 'All static analyses passed successfully, but we have warnings:'
|
|
puts
|
|
|
|
emit_warnings(static_analysis)
|
|
|
|
exit 2 if warning_count(static_analysis).nonzero?
|
|
else
|
|
puts 'Some static analyses failed:'
|
|
|
|
emit_warnings(static_analysis)
|
|
emit_errors(static_analysis)
|
|
|
|
exit 1
|
|
end
|
|
end
|
|
|
|
def emit_warnings(static_analysis)
|
|
static_analysis.warned_results.each do |result|
|
|
puts
|
|
puts "**** #{result.cmd.join(' ')} had the following warning(s):"
|
|
puts
|
|
puts result.stderr
|
|
puts
|
|
end
|
|
end
|
|
|
|
def emit_errors(static_analysis)
|
|
static_analysis.failed_results.each do |result|
|
|
puts
|
|
puts "**** #{result.cmd.join(' ')} failed with the following error(s):"
|
|
puts
|
|
puts result.stdout
|
|
puts result.stderr
|
|
puts
|
|
end
|
|
end
|
|
|
|
def warning_count(static_analysis)
|
|
static_analysis.warned_results
|
|
.count { |result| !ALLOWED_WARNINGS.include?(result.stderr.strip) }
|
|
end
|
|
|
|
def tasks_to_run(node_total)
|
|
total_time = TASKS_WITH_DURATIONS_SECONDS.sum(&:duration).to_f
|
|
ideal_time_per_node = total_time / node_total
|
|
tasks_by_duration_desc = TASKS_WITH_DURATIONS_SECONDS.sort_by { |a| -a.duration }
|
|
nodes = Array.new(node_total) { |i| NodeAssignment.new(i + 1, []) }
|
|
|
|
puts "Total expected time: #{total_time}; ideal time per job: #{ideal_time_per_node}.\n\n"
|
|
puts "Tasks to distribute:"
|
|
tasks_by_duration_desc.each { |task| puts "* #{task.cmd} (#{task.duration}s)" }
|
|
|
|
# Distribute tasks optimally first
|
|
puts "\nAssigning tasks optimally."
|
|
distribute_tasks(tasks_by_duration_desc, nodes, ideal_time_per_node: ideal_time_per_node)
|
|
|
|
# Distribute remaining tasks, ordered by ascending duration
|
|
leftover_tasks = tasks_by_duration_desc - nodes.flat_map(&:tasks)
|
|
|
|
if leftover_tasks.any?
|
|
puts "\n\nAssigning remaining tasks: #{leftover_tasks.flat_map(&:cmd)}"
|
|
distribute_tasks(leftover_tasks, nodes.sort_by { |node| node.total_duration })
|
|
end
|
|
|
|
nodes.each do |node|
|
|
puts "\nExpected duration for node #{node.index}: #{node.total_duration} seconds"
|
|
node.tasks.each { |task| puts "* #{task.cmd} (#{task.duration}s)" }
|
|
end
|
|
|
|
nodes
|
|
end
|
|
|
|
def distribute_tasks(tasks, nodes, ideal_time_per_node: nil)
|
|
condition =
|
|
if ideal_time_per_node
|
|
->(task, node, ideal_time_per_node) { (task.duration + node.total_duration) <= ideal_time_per_node }
|
|
else
|
|
->(*) { true }
|
|
end
|
|
|
|
tasks.each do |task|
|
|
nodes.each do |node|
|
|
if condition.call(task, node, ideal_time_per_node)
|
|
assign_task_to_node(tasks, node, task)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def assign_task_to_node(remaining_tasks, node, task)
|
|
node.tasks << task
|
|
puts "Assigning #{task.command} (#{task.duration}s) to node ##{node.index}. Node total duration: #{node.total_duration}s."
|
|
end
|
|
end
|
|
|
|
if $0 == __FILE__
|
|
options = {}
|
|
|
|
if ARGV.include?('--dry-run')
|
|
options[:dry_run] = true
|
|
end
|
|
|
|
StaticAnalysis.new.run_tasks!(options)
|
|
end
|