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