Skip to content
代码片段 群组 项目
  • Lukas 'ai-pi' Eipert's avatar
    7bcb70e6
    Create extraction script for utility classes · 7bcb70e6
    Lukas 'ai-pi' Eipert 创作于
    This is the first step towards generating all utility classes with
    tailwind. The idea behind this is the following:
    
    Parse all utility classes being provided by @gitlab/ui into a JSON
    object where each class is represented as something like this:
    
    ```
    '.bg-gray-10': {
      'background-color': 'var(--gray-10, #fbfafd)',
    },
    ```
    
    We then let tailwind generate all the utilities and compare the class
    definitions above with the class definitions in @gitlab/ui. They can
    fall into four categories:
    
    - exact matches: The tailwind generated class matches @gitlab/ui 1:1
    - potential mismatches: The tailwind generated class mismatches
      significantly. This can be okay or we may need some adjustments to the
      tailwind config.
    - hardcoded colors: Some of the utility classes have hard coded colors
      which the conversion script is not able to resolve to a CSS variable.
    - safe to use legacy utils: These utils have no overlap with tailwind at
      all. The class names completely differ. This means we can feed the
      definitions above into tailwind, so that tailwind is able to generate
      the classes
    
    As part of the conversion certain aspects are normalized, for example
    colors like `#fbfafd` are normalized to `var(--gray-10, #fbfafd)`, but
    only if the class name contains `gray-10`.
    
    Furthermore we currently check-in two files that later will be
    git-ignored:
    1. config/helpers/tailwind/css_in_js.js – containing the css-in-js
       definitions for tailwind to generate the legacy utils with tailwind
    2. config/helpers/tailwind/all_utilities.haml – containing a list of all
       utility classes, so that tailwind is able to generate _all utilities_
       and not just the used ones
    未验证
    7bcb70e6
    历史
    Create extraction script for utility classes
    Lukas 'ai-pi' Eipert 创作于
    This is the first step towards generating all utility classes with
    tailwind. The idea behind this is the following:
    
    Parse all utility classes being provided by @gitlab/ui into a JSON
    object where each class is represented as something like this:
    
    ```
    '.bg-gray-10': {
      'background-color': 'var(--gray-10, #fbfafd)',
    },
    ```
    
    We then let tailwind generate all the utilities and compare the class
    definitions above with the class definitions in @gitlab/ui. They can
    fall into four categories:
    
    - exact matches: The tailwind generated class matches @gitlab/ui 1:1
    - potential mismatches: The tailwind generated class mismatches
      significantly. This can be okay or we may need some adjustments to the
      tailwind config.
    - hardcoded colors: Some of the utility classes have hard coded colors
      which the conversion script is not able to resolve to a CSS variable.
    - safe to use legacy utils: These utils have no overlap with tailwind at
      all. The class names completely differ. This means we can feed the
      definitions above into tailwind, so that tailwind is able to generate
      the classes
    
    As part of the conversion certain aspects are normalized, for example
    colors like `#fbfafd` are normalized to `var(--gray-10, #fbfafd)`, but
    only if the class name contains `gray-10`.
    
    Furthermore we currently check-in two files that later will be
    git-ignored:
    1. config/helpers/tailwind/css_in_js.js – containing the css-in-js
       definitions for tailwind to generate the legacy utils with tailwind
    2. config/helpers/tailwind/all_utilities.haml – containing a list of all
       utility classes, so that tailwind is able to generate _all utilities_
       and not just the used ones
代码所有者
将用户和群组指定为特定文件更改的核准人。 了解更多。
static-analysis 5.81 KiB
#!/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` moved to scripts/allowed_warnings.txt

  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 = [
    Task.new(%w[yarn run lint:prettier], 200),
    Task.new(%w[bin/rake gettext:lint], 105),
    Task.new(%W[scripts/license-check.sh #{project_path}], 200),
    (Gitlab.ee? ? Task.new(%w[bin/rake gettext:updated_check], 40) : nil),
    Task.new(%w[bin/rake lint:static_verification], 40),
    Task.new(%w[yarn run lint:tailwind-utils], 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[yarn run check-dependencies], 1),
    Task.new(%w[scripts/gemfile_lock_changed.sh], 1),
    Task.new(%w[scripts/lint-vendored-gems.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 with warnings.'
      puts

      emit_warnings(static_analysis)

      # We used to exit 2 on warnings but `fail_on_warnings` takes care of it now.
    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|
      warn
      warn "**** #{result.cmd.join(' ')} had the following warning(s):"
      warn
      warn result.stderr
      warn
    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 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 $PROGRAM_NAME == __FILE__
  options = {}

  if ARGV.include?('--dry-run')
    options[:dry_run] = true
  end

  StaticAnalysis.new.run_tasks!(options)
end