
We already do this for deprecations but these may make warnings and errors from Homebrew easier to spot in GitHub Actions logs. While we're here, cleanup other cases that should have used `GitHub::Actions::Annotation` but didn't and provide some helpers and tweaks there necessary for our use case here.
128 lines
3.9 KiB
Ruby
128 lines
3.9 KiB
Ruby
# typed: true
|
|
# frozen_string_literal: true
|
|
|
|
require "securerandom"
|
|
require "utils/tty"
|
|
|
|
module GitHub
|
|
# Helper functions for interacting with GitHub Actions.
|
|
#
|
|
# @api internal
|
|
module Actions
|
|
sig { params(string: String).returns(String) }
|
|
def self.escape(string)
|
|
# See https://github.community/t/set-output-truncates-multiline-strings/16852/3.
|
|
string.gsub("%", "%25")
|
|
.gsub("\n", "%0A")
|
|
.gsub("\r", "%0D")
|
|
end
|
|
|
|
sig { params(name: String, value: String).returns(String) }
|
|
def self.format_multiline_string(name, value)
|
|
# Format multiline strings for environment files
|
|
# See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
|
|
|
|
delimiter = "ghadelimiter_#{SecureRandom.uuid}"
|
|
|
|
if name.include?(delimiter) || value.include?(delimiter)
|
|
raise "`name` and `value` must not contain the delimiter"
|
|
end
|
|
|
|
<<~EOS
|
|
#{name}<<#{delimiter}
|
|
#{value}
|
|
#{delimiter}
|
|
EOS
|
|
end
|
|
|
|
sig { returns(T::Boolean) }
|
|
def self.env_set?
|
|
ENV.fetch("GITHUB_ACTIONS", false).present?
|
|
end
|
|
|
|
sig {
|
|
params(
|
|
type: Symbol, message: String,
|
|
file: T.nilable(T.any(String, Pathname)),
|
|
line: T.nilable(Integer)
|
|
).void
|
|
}
|
|
def self.puts_annotation_if_env_set(type, message, file: nil, line: nil)
|
|
# Don't print annotations during tests, too messy to handle these.
|
|
return if ENV.fetch("HOMEBREW_TESTS", false)
|
|
|
|
puts Annotation.new(type, message) if env_set?
|
|
end
|
|
|
|
# Helper class for formatting annotations on GitHub Actions.
|
|
class Annotation
|
|
ANNOTATION_TYPES = [:notice, :warning, :error].freeze
|
|
|
|
sig { params(path: T.any(String, Pathname)).returns(T.nilable(Pathname)) }
|
|
def self.path_relative_to_workspace(path)
|
|
workspace = Pathname(ENV.fetch("GITHUB_WORKSPACE", Dir.pwd)).realpath
|
|
path = Pathname(path)
|
|
return path unless path.exist?
|
|
|
|
path.realpath.relative_path_from(workspace)
|
|
end
|
|
|
|
sig {
|
|
params(
|
|
type: Symbol,
|
|
message: String,
|
|
file: T.nilable(T.any(String, Pathname)),
|
|
title: T.nilable(String),
|
|
line: T.nilable(Integer),
|
|
end_line: T.nilable(Integer),
|
|
column: T.nilable(Integer),
|
|
end_column: T.nilable(Integer),
|
|
).void
|
|
}
|
|
def initialize(type, message, file: nil, title: nil, line: nil, end_line: nil, column: nil, end_column: nil)
|
|
raise ArgumentError, "Unsupported type: #{type.inspect}" if ANNOTATION_TYPES.exclude?(type)
|
|
|
|
@type = type
|
|
@message = Tty.strip_ansi(message)
|
|
@file = self.class.path_relative_to_workspace(file) if file.present?
|
|
@title = Tty.strip_ansi(title) if title
|
|
@line = Integer(line) if line
|
|
@end_line = Integer(end_line) if end_line
|
|
@column = Integer(column) if column
|
|
@end_column = Integer(end_column) if end_column
|
|
end
|
|
|
|
sig { returns(String) }
|
|
def to_s
|
|
metadata = @type.to_s
|
|
if @file
|
|
metadata << " file=#{Actions.escape(@file.to_s)}"
|
|
|
|
if @line
|
|
metadata << ",line=#{@line}"
|
|
metadata << ",endLine=#{@end_line}" if @end_line
|
|
|
|
if @column
|
|
metadata << ",col=#{@column}"
|
|
metadata << ",endColumn=#{@end_column}" if @end_column
|
|
end
|
|
end
|
|
end
|
|
|
|
metadata << ",title=#{Actions.escape(@title)}" if @title
|
|
|
|
"::#{metadata}::#{Actions.escape(@message)}"
|
|
end
|
|
|
|
# An annotation is only relevant if the corresponding `file` is relative to
|
|
# the `GITHUB_WORKSPACE` directory or if no `file` is specified.
|
|
sig { returns(T::Boolean) }
|
|
def relevant?
|
|
return true if @file.blank?
|
|
|
|
@file.descend.next.to_s != ".."
|
|
end
|
|
end
|
|
end
|
|
end
|