| #!/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 "$@" |