blob: 202a82deb5ae5770f75bf7dc99ba6f80d08443c7 [file] [log] [blame]
#!/bin/bash
# Copyright 2021 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#### CATEGORY=Software delivery
### report a bug for an fx command
## usage:
## fx report-bug [<flags>] <command> [<args>]
## Report an issue on an fx <command>.
##
## <flags>: [optional] include:
## -v, --verbose: verbose output
## -h, --help : provide this help.
##
## <command>: any fx command.
##
## <args>: [optional] the arguments passed to the fx <command> that
## caused the issue being reported, for inclusion in the bug
## report.
##
## This tool will help file a bug on bugs.fuchsia.dev for the given command. It
## parses the COMPONENT and OWNERS lines of the command's metadata or, if those
## aren't present, the `per-file` and `# COMPONENT:` lines of their OWNERS
## files. This parsing allows `report-bug` to specify which bugs.fuchsia.dev
## component the bug should be filed in and who should be added to the "Cc:"
## field.
##
## `fx report-bug` will print out a URL that can be pasted into a browser to
## create a bug with the appropriate fields pre-filled. Just add the details
## and submit it.
##
## Be sure to include the <args> so the the bug report can show the exact
## command invocation that exhibited the reported issue.
set -e
# Globals
readonly DEFAULT_COMPONENT="Tools>fx"
verbose=false
# shellcheck source=/dev/null
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"/lib/vars.sh || exit $?
fx-config-read
# Look for an `include <filename>` line in an OWNERS file. Return the filename.
function parse_owners_file_for_includes() {
local file=$1
grep -E "^include " "${file}" | # Find lines with the token followed by a space or colon.
cut -d" " -f2 | # Use only the part after the =.
cut -d"#" -f1 | # Strip any additional comments.
xargs -n 1 # Trim whitespace at beginning and end.
}
# Look for the email addresses listed in an OWNERS file. Return the addresses as
# a comma-separated string.
function parse_owners_file_for_owners() {
local file=$1
grep -E -o "^\s*\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}\b" "${file}" | # Find email addresses at line starts.
tr -d ' ' | # Remove spaces.
xargs -n 1 | # Trim whitespace at beginning and end.
sort | uniq | # Remove duplicates.
paste -sd "," - # Recombine into a comma-separated list.
}
# Look for "<token> <command> = <address> [, <address>]*" lines in OWNERS files
# and return the addresses. The token is a regular expression expected at the
# start of a line. Example tokens include "per-file" and
# "#[[:space:]]*per-file-COMPONENT". The addresses are not checked for format
# so may contain a `file: <filename>`.
function parse_owners_file_for_per_file() {
local file=$1
local token=$2
local cmd_name=$3
# Note that there can be multiple per-file entries for a command. The sequence
# below will include and combine them.
grep -E "^${token}[ \:]+" "${file}" | # Find lines with the token followed by a space or colon.
grep -E "[, ]+(${cmd_name}|${cmd_name}.fx)[, =]+" | # Find the command as a whole word (+/- '.fx') only.
grep -E "(${cmd_name}|${cmd_name}.fx).*=" | # Only lines with the cmd (+/- '.fx') *followed* by =.
cut -d"=" -f2 | # Use only the part after the =.
cut -d"#" -f1 | # Strip any additional comments.
tr -d ' ' | # Remove spaces.
tr ',' '\n' | # Split into separate lines.
xargs -n 1 | # Trim whitespace at beginning and end.
sort | uniq | # Remove duplicates.
paste -sd "," - # Recombine into a comma-separated list.
}
# Look for "# COMPONENT: <component> [, <component>]*" lines in OWNERS files and
# return the components.
function parse_owners_file_for_COMPONENTS() {
local file=$1
# Note that there could be multiple entries. The sequence below will include
# and combine them.
grep -E "^#[[:space:]]*COMPONENT[ \:]+" "${file}" | # Find lines with the token followed by a space or colon.
cut -d":" -f2 | # Use only the part after the :.
cut -d"#" -f1 | # Strip any additional comments.
tr -d ' ' | # Remove spaces.
tr ',' '\n' | # Split into separate lines.
xargs -n 1 | # Trim whitespace at beginning and end.
sort | uniq | # Remove duplicates.
paste -sd "," - # Recombine into a comma-separated list.
}
rawurlencode() {
local string=$1
local strlen=${#string}
local encoded=""
local pos c o
for ((pos = 0; pos < strlen; pos++)); do
c=${string:$pos:1}
case "$c" in [-_.~a-zA-Z0-9]) o="${c}" ;;
*) printf -v o '%%%02x' "'$c" ;;
esac
encoded+="${o}"
done
echo "${encoded}"
}
# Given a command and arguments, file a bug in the given component and cc the
# given owners.
function file_bug() {
local cmd_name=$1
local cmd_args=$2
local component=$3
local owners=$4 # comma-separated string
local base_url="https://bugs.fuchsia.dev/p/fuchsia/issues/entry"
local summary_escd
summary_escd=$(rawurlencode "[fx ${cmd_name}] one-line summary")
local description="The following issue was found with the \`fx ${cmd_name}\` command"
if [[ -n "${cmd_args}" ]]; then
description="${description} when run as follows:
$ fx ${cmd_name} ${cmd_args}"
else
description="${description}."
fi
description="${description}
Steps to complete this bug report:
1. Correct the one-line summary above.
2. Describe the issue.
3. Provide the steps to reproduce the issue.
[This bug report was generated using \`fx report-bug <cmd> <args>\` and
modified by the reporter. If you are listed in the initial Cc: list,
please add or update the \`per-file-COMPONENT\` line in the OWNERS file
to ensure bugs are filed to the correct component.]
"
description_escd=$(rawurlencode "${description}")
local url
url="${base_url}?summary=${summary_escd}&description=${description_escd}"
if [[ -n "$component" ]]; then
url="${url}&components=$(rawurlencode "$component")"
fi
# Put the file owners in the "Cc:" field.
if [[ -n "$owners" ]]; then
url="${url}&cc=$(rawurlencode "$owners")"
fi
fx-info "Use the following URL to report your issue. [Hint: Try control-clicking or command-clicking to make the URL clickable.]"
fx-info "${url}"
}
# Find a bugs.fuchsia.dev component or command owners in a given OWNERS file.
# Return 0 if found else 1 if the search should continue.
#
# This is a recursive algorithm that follows `file:` directives in `per-file`
# and `per-file_COMPONENT` lines. To keep it understandable, it works simply: It
# recurses one-way on one path, carrying the parent call's variables as globals.
# When sufficient owners and component data is found, a bug is created and
# written to stdout. It stops as soon as it has sufficient data, thus keeping
# the data as close to the given command as possible.
#
# The algorithm prefers `file:` directives over includes and will not trace both
# `file:` and `include` directives in the same file. This is usually correct
# because `file:` directives override `include` directives. It will traverse
# multiple `file:` directives if they occur in both per-file and
# per-file-COMPONENT lines.
function check_an_owners_file() {
local cmd_name=$1
local cmd_args=$2
local file=$3
if [[ ! -f "${file}" ]]; then
return 1
fi
"${verbose}" && echo "looking for a component and owners in ${file}"
# 1. The list of email addresses in the file.
local new_listed_owners
new_listed_owners=$(parse_owners_file_for_owners "${file}")
# Note that here and below, the global value is used if one is not found
# locally.
local listed_owners="${new_listed_owners:-$listed_owners}"
[[ -n "${listed_owners}" ]] && "${verbose}" && echo "found owners: ${listed_owners}"
# 2. `# COMPONENT: <string>` line in the OWNERS file.
local new_component_in_owners_file
new_component_in_owners_file=$(parse_owners_file_for_COMPONENTS "${file}")
local component_in_owners_file="${new_component_in_owners_file:-$component_in_owners_file}"
[[ -n "${component_in_owners_file}" ]] && "${verbose}" && echo "found components: ${component_in_owners_file}"
# 3. `per-file-COMPONENT` line.
local new_per_file_component
new_per_file_component=$(parse_owners_file_for_per_file "${file}" "#[[:space:]]*per-file-COMPONENT" "${cmd_name}")
local per_file_component="${per_file_component:-$new_per_file_component}"
[[ -n "${per_file_component}" ]] && "${verbose}" && echo "found per_file components: ${per_file_component}"
# 4. `per-file` owners line.
local new_per_file_owners
new_per_file_owners=$(parse_owners_file_for_per_file "${file}" "per-file" "${cmd_name}")
if [[ -n "${new_per_file_owners}" ]]; then
listed_owners=""
fi
local per_file_owners="${per_file_owners:-$new_per_file_owners}"
# If there's a per-file `file:` directive, follow it, carrying the above
# values into the call as global variables.
if [[ "${per_file_owners}" =~ ^\s*file\s*:.* ]]; then
local next_file=${FUCHSIA_DIR}${per_file_owners#"file:"}
"${verbose}" && echo "per-file file to search: ${next_file}"
per_file_owners="" # clear so it isn't re-expanded
if check_an_owners_file "${cmd_name}" "${cmd_args}" "${next_file}"; then
return 0 # Success.
fi
fi
# Prefer the per_file_owners over any listed owners.
local owners="${per_file_owners:-${listed_owners}}"
# If we don't have any owners, then follow include files, carrying the above
# values into the call as global variables.
local include_file
include_file=$(parse_owners_file_for_includes "${file}")
# We prefer the owner listed directly in a file over those in a remote
# included directory. This preference ensures that we don't accummulate whole
# team lists as we recurse farther and farther from the fx command files.
if [[ -n "${include_file}" ]] && [[ -z "${listed_owners}" ]]; then
"${verbose}" && echo "found an 'include' directive': ${include_file}"
fullpath="${FUCHSIA_DIR}${include_file}"
if check_an_owners_file "${cmd_name}" "${cmd_args}" "${fullpath}"; then
return 0 # Success.
fi
fi
# Found enough to file a bug. As the component may have come from several
# sources, the preferred order is:
# per_file_component > component_in_owners_file > DEFAULT_COMPONENT.
local component="${per_file_component:-${component_in_owners_file}}"
component="${component:-${DEFAULT_COMPONENT}}"
file_bug "${cmd_name}" "${cmd_args}" "${component}" "${owners}"
return 0
}
# Climb the command's filepath, checking OWNERS files, looking for "per-file"
# owners or COMPONENT lines. File a bug when either is found.
function parse_owners_files() {
local cmd_name=$1
local cmd_args=$2
local cmd_path=$3
local parent
parent="${cmd_path%/*}"
while [[ -n "${parent}" ]] && [[ "${parent}" != "${FUCHSIA_DIR}" ]]; do
if check_an_owners_file "${cmd_name}" "${cmd_args}" "${parent}/OWNERS"; then
return 0 # Success.
fi
parent="${parent%/*}"
done
# Nothing found; file against the default component.
file_bug "${cmd_name}" "${cmd_args}" "${DEFAULT_COMPONENT}"
}
function analyze_command_and_report_issue() {
local cmd_name=$1
local cmd_args=$2
local cmd_path
cmd_path="$(commands "${cmd_name}" | head -1)"
if [[ -z "${cmd_path}" ]]; then
fx-error "cannot find command: ${cmd_name}"
exit 1
fi
parse_owners_files "${cmd_name}" "${cmd_args}" "${cmd_path}"
}
function main() {
[[ $# == 0 ]] && fx-command-help && exit 0
while [[ $# -ne 0 ]]; do
case $1 in
-h | --help | "")
fx-command-help
exit 0
;;
-v | --verbose)
verbose=true
shift
;;
-*)
fx-error "Unknown argument $1"
fx-command-help
exit 1
;;
*)
break
;;
esac
done
local cmd_name=$1
shift
local cmd_args="$*"
if [[ -z "${cmd_name}" ]]; then
fx-error "error parsing command name"
exit 1
fi
analyze_command_and_report_issue "${cmd_name}" "${cmd_args}"
}
main "$@"