diff --git a/Library/Homebrew/dev-cmd/audit.rb b/Library/Homebrew/dev-cmd/audit.rb index c03574b4a7141aa4d01e0fa176d1cf6befdcdc8d..ebc4773f91bca73e6d3431b60e387976724dd0fe 100644 --- a/Library/Homebrew/dev-cmd/audit.rb +++ b/Library/Homebrew/dev-cmd/audit.rb @@ -117,7 +117,7 @@ module Homebrew end # Check style in a single batch run up front for performance - style_results = Style.check_style_json(style_files, options) if style_files + style_offenses = Style.check_style_json(style_files, options) if style_files # load licenses spdx_license_data = SPDX.license_data spdx_exception_data = SPDX.exception_data @@ -134,7 +134,7 @@ module Homebrew spdx_license_data: spdx_license_data, spdx_exception_data: spdx_exception_data, } - options[:style_offenses] = style_results.file_offenses(f.path) if style_results + options[:style_offenses] = style_offenses.for_path(f.path) if style_offenses options[:display_cop_names] = args.display_cop_names? options[:build_stable] = args.build_stable? diff --git a/Library/Homebrew/style.rb b/Library/Homebrew/style.rb index 6eadd257537cc8c6d6342d5d7b7e7831e03636c1..9d31a8f7245f6b7eb8cea1c602551527943641aa 100644 --- a/Library/Homebrew/style.rb +++ b/Library/Homebrew/style.rb @@ -12,10 +12,17 @@ module Homebrew # Checks style for a list of files, printing simple RuboCop output. # Returns true if violations were found, false otherwise. def check_style_and_print(files, **options) - check_style_impl(files, :print, **options) + success = check_style_impl(files, :print, **options) + + if ENV["GITHUB_ACTIONS"] && !success + offenses = check_style_json(files, **options) + puts offenses.to_github_annotations + end + + success end - # Checks style for a list of files, returning results as a RubocopResults + # Checks style for a list of files, returning results as `Offenses` # object parsed from its JSON output. def check_style_json(files, **options) check_style_impl(files, :json, **options) @@ -49,7 +56,7 @@ module Homebrew end if output_type == :json - RubocopResults.new(rubocop_result + shellcheck_result) + Offenses.new(rubocop_result + shellcheck_result) else rubocop_result && shellcheck_result end @@ -161,12 +168,12 @@ module Homebrew system shellcheck, "--format=tty", *args $CHILD_STATUS.success? when :json - result = system_command shellcheck, args: ["--format=json1", *args] + result = system_command shellcheck, args: ["--format=json", *args] json = json_result!(result) # Convert to same format as RuboCop offenses. - json["comments"].group_by { |v| v["file"] } - .map do |k, v| + json.group_by { |v| v["file"] } + .map do |k, v| { "path" => k, "offenses" => v.map do |o| @@ -208,25 +215,51 @@ module Homebrew JSON.parse(result.stdout) end - # Result of a RuboCop run. - class RubocopResults - def initialize(files) - @file_offenses = {} - files.each do |f| + # Collection of style offenses. + class Offenses + include Enumerable + + def initialize(paths) + @offenses = {} + paths.each do |f| next if f["offenses"].empty? - file = File.realpath(f["path"]) - @file_offenses[file] = f["offenses"].map { |x| RubocopOffense.new(x) } + path = Pathname(f["path"]).realpath + @offenses[path] = f["offenses"].map { |x| Offense.new(x) } end end - def file_offenses(path) - @file_offenses.fetch(path.to_s, []) + def for_path(path) + @offenses.fetch(Pathname(path), []) + end + + def each(*args, &block) + @offenses.each(*args, &block) + end + + def to_github_annotations + workspace = ENV["GITHUB_WORKSPACE"] + return [] if workspace.blank? + + workspace = Pathname(workspace).realpath + + @offenses.flat_map do |path, offenses| + relative_path = path.relative_path_from(workspace) + + # Only generate annotations for paths relative to the `GITHUB_WORKSPACE` directory. + next [] if relative_path.descend.next.to_s == ".." + + offenses.map do |o| + line = o.location.line + column = o.location.line + GitHub::Actions::Annotation.new(:error, o.message, file: relative_path, line: line, column: column) + end + end end end - # A RuboCop offense. - class RubocopOffense + # A style offense. + class Offense attr_reader :severity, :message, :corrected, :location, :cop_name def initialize(json) @@ -234,7 +267,7 @@ module Homebrew @message = json["message"] @cop_name = json["cop_name"] @corrected = json["corrected"] - @location = RubocopLineLocation.new(json["location"]) + @location = LineLocation.new(json["location"]) end def severity_code @@ -259,8 +292,8 @@ module Homebrew end end - # Source location of a RuboCop offense. - class RubocopLineLocation + # Source location of a style offense. + class LineLocation attr_reader :line, :column, :length def initialize(json) diff --git a/Library/Homebrew/test/style_spec.rb b/Library/Homebrew/test/style_spec.rb index 36f8a035e0a9a6c198dd975c8225c48668799ff4..36f9af842f2ae1eba3d105e76daea4aa89ca13d5 100644 --- a/Library/Homebrew/test/style_spec.rb +++ b/Library/Homebrew/test/style_spec.rb @@ -20,7 +20,7 @@ describe Homebrew::Style do describe ".check_style_json" do let(:dir) { mktmpdir } - it "returns RubocopResults when RuboCop reports offenses" do + it "returns offenses when RuboCop reports offenses" do formula = dir/"my-formula.rb" formula.write <<~'EOS' @@ -29,9 +29,9 @@ describe Homebrew::Style do end EOS - rubocop_result = described_class.check_style_json([formula]) + style_offenses = described_class.check_style_json([formula]) - expect(rubocop_result.file_offenses(formula.realpath.to_s).map(&:message)) + expect(style_offenses.for_path(formula.realpath).map(&:message)) .to include("Extra empty line detected at class body beginning.") end @@ -53,11 +53,11 @@ describe Homebrew::Style do end end EOS - rubocop_result = described_class.check_style_json( + style_offenses = described_class.check_style_json( [formula], fix: true, only_cops: ["FormulaAudit/DependencyOrder"], ) - offense_string = rubocop_result.file_offenses(formula.realpath).first.to_s + offense_string = style_offenses.for_path(formula.realpath).first.to_s expect(offense_string).to match(/\[Corrected\]/) end end @@ -70,9 +70,9 @@ describe Homebrew::Style do # but not regular, cop violations target_file = HOMEBREW_LIBRARY_PATH/"utils.rb" - rubocop_result = described_class.check_style_and_print([target_file]) + style_result = described_class.check_style_and_print([target_file]) - expect(rubocop_result).to eq true + expect(style_result).to eq true end end end diff --git a/Library/Homebrew/utils/github.rb b/Library/Homebrew/utils/github.rb index afb40bac3eccd89562bf81db1f5913503b071c60..2361abcb8c694fa382a16cc9824c5ae3663eaac1 100644 --- a/Library/Homebrew/utils/github.rb +++ b/Library/Homebrew/utils/github.rb @@ -2,6 +2,7 @@ require "tempfile" require "uri" +require "utils/github/actions" # Helper functions for interacting with the GitHub API. # diff --git a/Library/Homebrew/utils/github/actions.rb b/Library/Homebrew/utils/github/actions.rb new file mode 100644 index 0000000000000000000000000000000000000000..646945b4288b748a4510c2f859c3b3a2103e7c10 --- /dev/null +++ b/Library/Homebrew/utils/github/actions.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module GitHub + # Helper functions for interacting with GitHub Actions. + # + # @api private + module Actions + def self.escape(string) + string.gsub(/\r/, "%0D") + .gsub(/\n/, "%0A") + .gsub(/]/, "%5D") + .gsub(/;/, "%3B") + end + + # Helper class for formatting annotations on GitHub Actions. + class Annotation + def initialize(type, message, file: nil, line: nil, column: nil) + raise ArgumentError, "Unsupported type: #{type.inspect}" unless [:warning, :error].include?(type) + + @type = type + @message = String(message) + @file = Pathname(file) if file + @line = Integer(line) if line + @column = Integer(column) if column + end + + def to_s + file = "file=#{Actions.escape(@file.to_s)}" if @file + line = "line=#{@line}" if @line + column = "col=#{@column}" if @column + + metadata = [*file, *line, *column].join(",").presence&.prepend(" ") + + "::#{@type}#{metadata}::#{Actions.escape(@message)}" + end + end + end +end diff --git a/bin/brew b/bin/brew index 1e01a22ce581a557d338e3015d11264540c01c65..807f6ac235741df308f4957a13cdd78ccabfece2 100755 --- a/bin/brew +++ b/bin/brew @@ -94,7 +94,7 @@ then # Filter all but the specific variables. for VAR in HOME SHELL PATH TERM TERMINFO COLUMNS DISPLAY LOGNAME USER CI SSH_AUTH_SOCK SUDO_ASKPASS \ http_proxy https_proxy ftp_proxy no_proxy all_proxy HTTPS_PROXY FTP_PROXY ALL_PROXY \ - GITHUB_ACTIONS GITHUB_ACTIONS_HOMEBREW_SELF_HOSTED \ + GITHUB_ACTIONS GITHUB_WORKSPACE GITHUB_ACTIONS_HOMEBREW_SELF_HOSTED \ GITHUB_REPOSITORY GITHUB_RUN_ID GITHUB_SHA GITHUB_HEAD_REF GITHUB_BASE_REF GITHUB_REF \ "${!HOMEBREW_@}" do