blob: 4710b751b142aea033eaed4ddce004ce265ef17b [file] [log] [blame]
#!/bin/bash
# -------------------------------------------------------------------------
# Copyright 2023-2024 Open Networking Foundation Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http:#www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# -----------------------------------------------------------------------
# SPDX-FileCopyrightText: 2023-2024 Open Networking Foundation Contributors
# SPDX-License-Identifier: Apache-2.0
## --------------------------------------------------------------------
## Intent: Construct a jira ticket query with attributes
## --------------------------------------------------------------------
{ # loader
declare pgm=''
pgm="$(realpath --canonicalize-existing "$0")"
# stack-trace-on-error
# interrupt handler
# mkdir with auto-cleanup at exit
declare root=''
root="${pgm%%/jira/bin/jira-search.sh}"
source "$root/lf/onf-common/common.sh" '--common-args-begin--'
pgm_lib="${root}/jira/jira-search"
readonly pgm_lib
pgm_bin="${root}/bin"
readonly pgm_bin
pgm_help="${root}/jira/jira-search/help"
readonly pgm_help
}
# set -euo pipefail
#source ~/.sandbox/trainlab-common/common.sh '--common-args-begin--'
##-------------------##
##---] GLOBALS [---##
##-------------------##
declare -g -a is_empty=() # label
declare -g -a text=()
declare -g -a text_and=()
declare -g -a text_or=()
declare -g -a urls_raw=()
declare -g -a urls_filt=()
declare -g -a labels_incl=()
declare -g -a labels_excl=()
declare -g -a projects=()
path="$(realpath $0 --canonicalize-existing)"
source "${pgm_lib}/utils.sh"
source "${pgm_lib}/include.sh"
source "${pgm_lib}/fixversion.sh"
source "${pgm_lib}/resolved.sh"
source "${pgm_lib}/help/utils.sh"
## --------------------------------------------------------------------
## --------------------------------------------------------------------
function error()
{
cat <<ERROR
** -----------------------------------------------------------------------
** IAM: ${FUNCNAME[1]}
** ERROR: $@
** -----------------------------------------------------------------------
ERROR
echo
exit 1
}
## --------------------------------------------------------------------
## --------------------------------------------------------------------
function banner()
{
cat <<MSG
** -----------------------------------------------------------------------
** $@
** -----------------------------------------------------------------------
MSG
return
}
## -----------------------------------------------------------------------
## Intent: Append a conditional token in the list based on context
## -----------------------------------------------------------------------
function and_or()
{
local -n ref=$1; shift
local val="$1" ; shift
if [[ ${#ref[@]} -gt 0 ]]; then
if [[ -v bool_or ]]; then
ref+=('OR')
else
ref+=('AND')
fi
fi
ref+=("$val")
return
}
## -----------------------------------------------------------------------
## -----------------------------------------------------------------------
function html_encode()
{
local -n ref=$1; shift
local tmp="$ref"
tmp="${tmp//[[:space:]]/%20}"
tmp="${tmp//\"/%22}"
tmp="${tmp//\'/%27}"
ref="$tmp"
return
}
## -----------------------------------------------------------------------
## Intent: Insert a conjunction into the stream when prior statements exist
## -----------------------------------------------------------------------
function conjunction()
{
return
local -n ref=$1; shift
[[ $# -gt 0 ]] && { local literal="$1"; shift; }
## -------------------------------
## Conjunction if prior statements
## -------------------------------
if [ ${#ref[@]} -gt 0 ]; then
if [[ -v literal ]]; then
ref+=("$literal")
elif [[ -v bool_and ]]; then
ref+=('AND')
else
ref+=('OR')
fi
fi
return
}
## -----------------------------------------------------------------------
## Intent: Helper method
## -----------------------------------------------------------------------
## Usage : local path="$(join_by '/' 'lib' "${fields[@]}")"
## -----------------------------------------------------------------------
function join_by()
{
local d=${1-} f=${2-}; if shift 2; then printf %s "$f" "${@/#/$d}"; fi;
}
## --------------------------------------------------------------------
## Intent: Query by component name filter
## --------------------------------------------------------------------
## Value: helm-charts
## --------------------------------------------------------------------
function do_components()
{
declare -n args=$1; shift
declare -n ans=$1; shift
# [ -z ${args+word} ] && { args=(); }
if [[ ${#args[@]} -gt 0 ]]; then
local modifier
if [[ -v bool_not ]]; then
modifier='NOT IN'
else
modifier='IN'
fi
ans+=("component ${modifier} (${args[@]})")
# alt: comp='foo' OR comp='bar'
fi
return
}
## --------------------------------------------------------------------
## Intent: Query filter by labels assigned to a ticket:
## o pods, failing, testing
## --------------------------------------------------------------------
## Perform a few string joins
## - elements {in,not-in} label {excl, incl}
## - OR: is-empty + in-label
## - AND: excl + incl
## --------------------------------------------------------------------
# "project in (UKSCR, COMPRG) AND issuetype = Bug AND labels in (BAT)" and
## --------------------------------------------------------------------
function do_labels()
{
local -n incl=$1; shift # was args=
local -n excl=$1; shift
local -n ans=$1; shift
## -------------------------------
## Join #1: is-empty + labels-incl
## -------------------------------
local -a tokens=()
if [[ " ${is_empty[*]} " =~ ' label ' ]]; then
tokens+=('(labels IS EMPTY)')
fi
## ------------------------------
## Construct label include filter
## ------------------------------
if [[ ${#incl[@]} -gt 0 ]]; then
local modifier
if [[ -v bool_not ]]; then
modifier='NOT IN'
else
modifier='IN'
fi
local labels=$(join_by ',' "${incl[@]}")
tokens+=("(labels ${modifier} ($labels))")
fi
# ------------------------------
# JOIN[OR]: is-empty + in-labels
# ------------------------------
if [[ ${#tokens[@]} -gt 1 ]]; then
local combine
combine=("$(join_by ' OR ' "${tokens[@]}")")
tokens=("$combine")
fi
## ------------------------------
## Construct label exclude filter
## ------------------------------
if [[ ${#excl[@]} -gt 0 ]]; then
local labels=$(join_by ',' "${excl[@]}")
tokens+=("(labels NOT IN ($labels))")
fi
# ------------------------------------
# JOIN[AND]: labels-excl + labels-incl
# ------------------------------------
if [[ ${#tokens[@]} -gt 1 ]]; then
tokens=("$(join_by ' AND ' "${tokens[@]}")")
fi
ans+=("${tokens[@]}")
return
}
## --------------------------------------------------------------------
## Intent: Modify search query by project type (SEBA, VOL)
## --------------------------------------------------------------------
function do_projects()
{
local -n ref=$1; shift
[[ ${#projects[@]} -eq 0 ]] && { return; }
local terms="$(join_by ',' "${projects[@]}")"
# local -a buffer=('(' 'project' 'IN' "($terms)" ')')
# ref+=("$(join_by '%20' "${buffer[@]}")")
ref+=("(project IN ($terms))")
return
}
## --------------------------------------------------------------------
## Intent: Construct query using text field filters
## --------------------------------------------------------------------
function do_text()
{
local -n ref=$1; shift
local -n ans=$1; shift
local val
## Accumulate
if [[ ${#ref[@]} -gt 0 ]]; then
if [[ -v bool_and ]]; then
text_and+=("${ref[@]}")
else
text_or+=("${ref[@]}")
fi
fi
## Append terms: AND
if [[ ${#text_and[@]} -gt 0 ]]; then
local -a term=()
for val in "${text_and[@]}";
do
term+=("text ~ \"$val\"")
done
val=$(join_by ' AND ' "${term[@]}")
ans+=("($val)")
fi
## Append terms: OR
if [[ ${#text_or[@]} -gt 0 ]]; then
local -a term=()
for val in "${text_or[@]}";
do
term+=("text ~ \"$val\"")
done
val=$(join_by ' OR ' "${term[@]}")
ans+=("($val)")
fi
return
}
## --------------------------------------------------------------------
## Intent: Query by assigned or requestor
## --------------------------------------------------------------------
## Note: Simple for now but support query by a list of suers
## --------------------------------------------------------------------
function do_user()
{
local -n ref=$1; shift
[[ -v argv_nobody ]] && return
local -a buffer=()
local user='currentUser()'
[[ ! -v argv_users ]] && { declare -a argv_users=('currentUser()'); }
local users
users="$(join_by ',' "${argv_users[@]}")"
if [[ -v argv_assigned ]]; then
if [[ ${#argv_users[@]} -eq 1 ]]; then
buffer+=("assignee=${user}")
else
buffer+=("(asignee in ($users))")
fi
fi
if [[ -v argv_reported ]]; then
if [[ ${#argv_users[@]} -eq 1 ]]; then
buffer+=("reporter=${user}")
else
buffer+=("(reporter in ($users))")
fi
fi
## --------------------------
## Construct resolved queries
## --------------------------
local query="$(join_by ' OR ' "${buffer[@]}")"
## ----------------------------------------
## Normalize parens for the composite query
## ----------------------------------------
if [[ ${#ref[@]} -eq 0 ]]; then
ref+=("$query")
else
local buffer
buffer="$(join_by ' AND ' "(${ref[@]})" "($query)")"
ref=("$buffer")
fi
return
}
## --------------------------------------------------------------------
## Intent: Combine filter arguments into a search query
## --------------------------------------------------------------------
function gen_filter()
{
local -n ans=$1 ; shift
local -n args=$1 ; shift
## -----------------------------------
## Begin by joining major search terms
## -----------------------------------
local -a _tmp=()
local val
local -i is_paren=0
local -a buffer=()
for val in "${args[@]}";
do
case "$val" in
'(') _tmp+=("$val"); continue ;;
')') _tmp+=("$val"); continue ;;
esac
and_or _tmp "$val"
done
## ----------------------------------------------------------------
# This was used to remove AND term when a non-query or non-argument
# query was needed. Any lingering logic dependent on it ?
## ----------------------------------------------------------------
# if [[ ${#_tmp[@]} -gt 0 ]]; then
# unset _tmp[-1]
# fi
## -----------------------
## Massage with html codes
## -----------------------
ans="$(join_by '%20' "${_tmp[@]}")"
return
}
## --------------------------------------------------------------------
## Intent: Combine filter arguments into a search query
## --------------------------------------------------------------------
function gen_url()
{
local -n ans=$1; shift
local -n args=$1; shift
## Which jira server to query (?)
[[ ! -v server ]] && local -g server='jira.opennetworking.org'
tmp_url="https://${server}/issues/?jql="
tmp="${tmp_url}${args}"
ans="${tmp// /%20}"
return
}
## --------------------------------------------------------------------
## Intent: Dispaly command usage
## --------------------------------------------------------------------
function usage()
{
cat <<EOH
Usage: $0 VOL-xxxx
--debug Enable script debug mode
--dry-run Simulate
--todo Display future enhancements
VOL-{xxxx} View a jira ticket by ID
[SERVER]
--server {cord,onf}
--onf jira.opennetworking.org
--opencord jira.opencord.org
[WHAT]
--component Search by component name assigned to ticket
--label Search by label name assigned to ticket.
--text Search string(s)
EOH
# local -a topics=()
# topics+=('fixversion.switches')
# topics+=('resolved.switches')
#
# help_switch_show "${topics[@]}"#
#
#[USER(s)]
# --me Tickets assigned to or reported by me.
# --user [u] Tickets assigned to this user.
# --nobody Raw query, no filtering by user
cat <<EOH
[BOOL]
--and Join terms using 'AND'
--or Join terms using 'OR'
[MEMBER]
--in (default) Items belong (--component IN)
--not-in Negate item set (--component NOT IN)
[Contains] (join modifer: --and, --or, --is-empty)
--text [t]
--text-and [t] All list items
--text-or [t] Any list items
--label [l] Match label field based on modifier criteria
--text-and [t] All list items
--text-or [t] Any list items
[MODIFIERS]
--and Combine query terms using AND keyword
--or Combine query terms using OR keyword
--in Include ticket field if list item(s) match
--not Negate a query
--is-empty Include ticket if field is empty
--unresolved Query for open/unresolved jira tickets.
[FILTER]
--excl Exclude tickets whose fields match this string (filter-out)
--incl Include tickets whose fields match this string
[--resolved]
--resolved-is-empty
--resolved-start Query by date range
--resolved-end Query by date range
--resolved(-not)-empty Query for (closed)/open tickets
--resolved-excl Types to exclude
--resolved-incl Types to include
--resolved-in {start} {end} Helps resolve join({and,or}) ambiguity
[RANGE]
--newer [d] Search for tickets created < [n] days ago.
--older [d] Search for tickets created > [n] days ago.
[ALIASES]
--all Query for all unresolved tickets
--unresolved Alias for --resolved-is-empty
--wip Alias for --resolved-is-empty
[TOPIC]
--fixversion Query by field: fixedversion
--resolved Query by field: resolved
--user Query by owner, requestor or 'my' jira tickets.
[USER]
--user [u] Login of user to query for
--assigned Query for tickets assigned to the user
--reported Query for tickets reported by the user
[HELP]
--help This message
--help-{topic} Display switch help and use case. (--help-resolved)
--usage-{topic} Display use cases for a given switch (--usage-user)
[USAGE]
$0 --opencord --assigned --unresolved
o Display all tickets assigned to my login
$0 --reported --or --text 'bbsim' --text 'release'
o Search for tickets that contain strings bbsim or release
$0 --cord --text-and 'release' --text-and 'voltctl'
o Search jira.opencord for tickets that contain release and voltctl
$0 --cord --label failing --label pod
o Search jira.opencord for tests failing due to pod/hardware issuses.
$0 --proj VOL --fixversion "VOLTHA v2.12" --resolved-is-empty
o Query for unresolved release tickets
EOH
return
}
## --------------------------------------------------------------------
# classpath=$(join_by ':' "${mypath[@]}")
## --------------------------------------------------------------------
function join_by()
{
local d=${1-} f=${2-}; if shift 2; then printf %s "$f" "${@/#/$d}"; fi;
}
##----------------##
##---] MAIN [---##
##----------------##
declare -a suffix0=() # accumulated terms to join
declare server='jira.opencord.org'
# declare -g -i debug=1
unset attrs
while [ $# -gt 0 ]; do
arg="$1"; shift
banner "ARG=[$arg], \$@=[$@]"
[[ -v debug ]] && echo "** argv=[$arg] [$*]"
getopt_detect_modifiers "$arg"
if getopts_switch__not arg; then
echo "DETECTED: NOT"
declare -i modifier_found=1
elif getopts_switch__empty arg; then
echo "DETECTED: EMPTY"
declare -i modifier_found=1
else
declare -i modifier_found=0
fi
if [[ $modifier_found -eq 1 ]]; then
echo "*ATTRS MODIFIER (LINENO:$LINENO): $(declare -p attrs)"
declare -a args=()
[[ ${#arg} -gt 0 ]] && { args+=("$arg"); }
[[ $# -gt 0 ]] && { args+=("$@"); }
set -- "${args[@]}"
continue
fi
case "$arg" in
'--help') usage; exit 0 ;;
'--help-'*) help_with "${arg/--help-/}" ;;
'--usage-'*) help_usage_show "${arg/--usage-/}" ;;
##-----------------##
##---] MODES [---##
##-----------------##
-*debug) declare -g -i debug=1 ;;
--dry-run) declare -g -i dry_run=1 ;;
##------------------------##
##---] SWITCH ALIAS [---##
##------------------------##
'--unresolved'|'--wip')
declare -a args=()
args+=('--resolved-is-empty')
[[ $# -gt 0 ]] && { args+=("$@"); }
set -- "${args[@]}"
;;
##-------------------##
##---] BY USER [---##
##-------------------##
'--assigned') declare -g -i argv_assigned=1 ;;
'--reported') declare -g -i argv_reported=1 ;;
'--me')
declare -a args=()
args+=('--user' 'currentUser()')
args+=('--assigned')
args+=('--reported')
[[ $# -gt 0 ]] && { args+=("$@"); }
set -- "${args[@]}"
;;
--nobody) declare -g -i argv_nobody=1 ;;
'--user')
arg="$1"; shift
declare -g argv_user="$arg"
[[ ! -v argv_users ]] && { declare -g -a argv_users=(); }
argv_users+=("$arg")
declare -p argv_users
;;
##------------------##
##---] SERVER [---##
##------------------##
--serv*)
arg="$1"; shift
case "$arg" in
*cord*) server='jira.opencord.org' ;;
*onf*) server='jira.opennetworking.org' ;;
*) error "--server [$arg] expected opencord or onf" ;;
esac
;;
--onf) declare server='jira.opennetworking.org' ;;
--opencord) declare server='jira.opencord.org' ;;
##---------------------##
##---] SEARCH-BY [---##
##---------------------##
--component|--comp*)
arg="$1"; shift
[[ ! -v components ]] && declare -g -a components=()
components+=("$arg")
;;
'--label-is-empty') is_empty+=('label') ;;
'--label'|'--lab'*)
val="$1"; shift
if [[ -v getopt_argv_EXCL ]]; then
labels_excl+=("$val")
elif [[ -v getopt_argv_INCL ]]; then
labels_incl+=("$val")
else
labels_incl+=("$val")
fi
;;
##-----------------------##
##---] Text Search [---##
##-----------------------##
# jsearch.sh --text-and bbsim --text-and release
'--tex'*)
[[ $# -gt 0 ]] && { val="$1"; }
case "$arg" in
'--text-and') text_and+=("$val") ;;
'--text-or') text_or+=("$val") ;;
# % js --and --text jenkins --text cord
# text ~ "Jira Software" # [WORDs]
# text ~ "\"Jira Software\"" # [STRING]
'--text')
if [[ ! -v bool_and_or ]]; then
error "Qualify [$arg] using --text-{and,or}"
elif [[ bool_and_or -eq 1 ]]; then
text_and+=("$1"); shift
elif [[ bool_and_or -eq 0 ]]; then
text_or+=("$1"); shift
else
error "Qualify [$arg] using --text-{and,or}"
fi
;;
*) error "Qualify [$arg] using --text-{and,or}" ;;
esac
shift # $val
;;
--all) set -- '--resolved-is-none' "$@" ;; # alias: --[un-]resolved
--todo) source "${pgm_lib}/todo.sh" ;;
--proj*) projects+=("$1"); shift ;;
--fixversion-*)
# function get_jql_fixversion()
case "$arg" in
*excl)
[[ ! -v fixversion_excl ]] && { declare -g -a fixversion_excl=(); }
val="\"$1\""; shift
html_encode val
fixversion_excl+=("$val");
;;
*incl)
[[ ! -v fixversion_incl ]] && { declare -g -a fixversion_incl=(); }
val="\"$1\""; shift
html_encode val
fixversion_incl+=("$val");
;;
*not-empty) declare -g -i fixversion_not_empty=1 ;;
*is-empty) declare -g -i fixversion_is_empty=1 ;;
*) error "Detected invalid --fixversion-* modifier" ;;
esac
;;
'--resolved'*) # [-+]
gen_attrs__empty 'resolved'
# [[ -v resolved_not_empty ]] && { declare -p resolved_not_empty; }
# [[ -v resolved_is_empty ]] && { declare -p resolved_is_empty; }
# function get_jql_reasons()
case "$arg" in
'--resolved') ;;
*'start') declare -g resolved_start="$1"; shift ;;
*'end') declare -g resolved_end="$1"; shift ;;
*'excl')
[[ ! -v resolved_excl ]] && { declare -g -a resolved_excl=(); }
val="\"$1\""; shift
html_encode val
resolved_excl+=("$val");
;;
*'incl')
[[ ! -v resolved_incl ]] && { declare -g -a resolved_incl=(); }
val="\"$1\""; shift
html_encode val
resolved_incl+=("$val");
;;
*) error "Detected invalid --resolved-* modifier arg=[$arg]" ;;
esac
;;
-*newer)
arg="$1"; shift
suffix0+=("created <= '-${arg}d'") ;;
-*older)
arg="$1"; shift
suffix0+=("created >= '-${arg}d'") ;;
##----------------##
##---] BOOL [---##
##----------------##
--[aA][nN][dD])
declare -g -i bool_and=1
declare -g -i bool_and_or=1 # ! -v else toggle
;;
--[oO][rR])
declare -g -i bool_or=1
declare -g -i bool_and_or=0
;;
##------------------##
##---] MEMBER [---##
##------------------##
--[iI][nN]) declare -g -i bool_in=1 ;;
--[nN][oO][tT]) declare -g -i bool_not=1 ;;
[A-Z][A-Z][A-Z]-[0-9]*) # VOL-xxxx (jira ticket)
case "$arg" in
CORD-[0-9]*)
url="https://jira.opencord.org/browse/${arg}"
urls_raw+=('--new-window' "$url")
;;
INF-[0-9]*)
url="https://jira.opennetworking.org/browse/${arg}"
urls_raw+=('--new-window' "$url")
;;
VOL-[0-9]*)
url="https://jira.opencord.org/browse/${arg}"
urls_raw+=('--new-window' "$url")
;;
*) error "Detected invalid ticket [$arg]" ;;
esac
;;
## ---------------------------
## Search all fields for value
## ---------------------------
[[:word:]]*)
echo "MATCHED: [$arg] (LINENO: $LINENO)"
declare -a args=()
args+=('--OR')
args+=('--component' "$arg")
args+=('--label-incl' "$arg")
args+=('--text-or' "$arg")
[[ $# -gt 0 ]] && { args+=("$@"); }
set -- "${args[@]}"
;;
-*) error "Detected unknown switch [$arg]" ;;
# -----------------------------------------------------------------------
# https://support.atlassian.com/jira-software-cloud/docs/search-syntax-for-text-fields/
# -----------------------------------------------------------------------
# +jira atlassian -- must contain jira, atlassian is optional
# -japan -- exclude term
# [STEM] summary ~ "customize" -- finds stem 'custom' in the Summary field
*)
echo "MATCHED: [$arg] (LINENO: $LINENO)"
declare -p text_and
error "Detected unknown argument $arg"
;;
esac
if [[ -v resolved ]]; then
clear_attrs__empty
fi
done
## --------------
## Required check
## --------------
[[ ! -v server ]] && { error "--server={cord,onf} is required"; }
## ----------------------
## Construct query filter
## ----------------------
do_user suffix0
do_projects suffix0
[[ -v components ]] && { do_components components suffix0; }
do_labels labels_incl labels_excl suffix0
do_text text suffix0
get_jcl_resolved suffix0
do_fixversion suffix0
[[ -v getopt_argv_any_OR ]] && { declare -p getopt_argv_any_OR; }
if [[ -v getopt_argv_any_OR ]]; then
query="$(join_by ' OR ' "${suffix0[0]}")"
elif [[ -v getopt_argv_any_AND ]]; then
query="$(join_by ' AND ' "${suffix0[0]}")"
else
# query="$(join_by ' OR ' "${suffix0[0]}")"
error "Ambiguous query [argv needs: --and or --or], argv=[$*]"
fi
filter=''
gen_filter filter suffix0
if [[ ! -v urls_raw ]]; then
url=''
gen_url url filter
urls_filt+=("$url")
elif [ ${#urls_raw} -eq 0 ]; then
url=''
gen_url url filter
urls_filt+=("$url")
fi
[[ -v debug ]] && [[ -v url ]] && echo "URL: $url"
browser="${BROWSER:-firefox}"
echo "$browser ${urls_filt[@]} ${urls_raw[@]}"
if [[ ! -v dry_run ]]; then
"$browser" "${urls_filt[@]}" "${urls_raw[@]}" >/dev/null 2>/dev/null &
fi
# [SEE ALSO]
# o https://support.atlassian.com/jira-software-cloud/docs/advanced-search-reference-jql-fields/
# [EOF]