diff --git a/bash/aws.sh b/bash/aws.sh index d4b0a88..18fb377 100644 --- a/bash/aws.sh +++ b/bash/aws.sh @@ -123,9 +123,24 @@ function aws::wait_for_scan_complete { local repository=${1:?} local tag=${2:?} local wait_time=${3:-10} - while ! aws::is_scan_complete "$repository" "$tag"; do + local max_attempts=${4:-30} + local attempt=0 + local status + while [ "$attempt" -lt "$max_attempts" ]; do + status=$(aws::scan_status "$repository" "$tag") + if [ "$status" = "COMPLETE" ]; then + return 0 + fi + if [ "$status" = "FAILED" ]; then + log::error "ECR image scan FAILED for ${repository}:${tag}" + return 1 + fi + log::trace "Waiting for ECR scan ${repository}:${tag} status=${status} attempt=$((attempt + 1))/${max_attempts}" sleep "$wait_time" + attempt=$((attempt + 1)) done + log::error "Timed out waiting for ECR scan to complete for ${repository}:${tag} after ${max_attempts} attempts (${wait_time}s interval)" + return 1 } function aws::list_findings { diff --git a/bash/docker.sh b/bash/docker.sh index 7909de4..2018154 100644 --- a/bash/docker.sh +++ b/bash/docker.sh @@ -174,9 +174,25 @@ function docker::login() { function docker::registrycmd { local url=${1:?} local registry=${2:?} + local config="${HOME}/.docker/config.json" local basic_token - basic_token=$(docker::_jq -r ".auths.\"$registry\".auth" ~/.docker/config.json) - $(commands::use curl) -s -H "Authorization: Basic $basic_token" "https://$registry/v2/$url" + if [ ! -r "$config" ]; then + log::error "Docker config missing or unreadable: $config (try: docker login $registry)" + return 1 + fi + # shellcheck disable=SC2016 # jq filter: $reg is a jq binding from --arg, not bash. + if ! basic_token=$( + docker::_jq -r --arg reg "$registry" \ + '(.auths[$reg] // {}).auth // empty' "$config" + ); then + log::error "Could not read docker auth for registry '$registry' from $config" + return 1 + fi + if [ -z "$basic_token" ] || [ "$basic_token" = "null" ]; then + log::error "No docker credentials for registry '$registry' in $config (try: docker login $registry)" + return 1 + fi + $(commands::use curl) -fsS -H "Authorization: Basic $basic_token" "https://$registry/v2/$url" } function docker::list_repositories { diff --git a/bash/on-change b/bash/on-change index 2bbf0a1..dcd1d34 100755 --- a/bash/on-change +++ b/bash/on-change @@ -33,7 +33,7 @@ options::description "A makeshift minimal CI system. It watches a directory for\ working directory. While running, type X on stdin to force an immediate\ re-run without waiting for a filesystem change." options::add -o W -d "directory to watch for changes" -a -m -e WatchDir -options::add -o w -d "working directory" -a -m -e WorkingDir +options::add -o w -d "working directory (defaults to watch directory)" -a -e WorkingDir options::add -o t -d "interval to check for changes" -a -m -e WaitTime options::parse "$@" shift "$((OPTIND - 1))" diff --git a/bash/options.sh b/bash/options.sh index a438d44..20132f6 100644 --- a/bash/options.sh +++ b/bash/options.sh @@ -29,8 +29,30 @@ declare -g -A OPTIONS_OPTIONAL declare -g -A OPTIONS_HAS_ARGS declare -g -A OPTIONS_PARSE_FUNCS declare -g -A OPTIONS_ENVIRONMENT +declare -g -A OPTIONS_SEEN declare -g OPTIONS_DESCRIPTION +function options::_mandatory_options_validate() { + local opt + local varName + for opt in "${OPTIONS[@]}"; do + [ "${OPTIONS_OPTIONAL[$opt]}" = "true" ] && continue + if [ "${OPTIONS_SEEN[$opt]:-0}" != "1" ]; then + log::error "Missing required option: -$opt" + options::syntax_exit + fi + if [ "${OPTIONS_HAS_ARGS[$opt]}" = "true" ]; then + varName="${OPTIONS_ENVIRONMENT[$opt]:-}" + if [ -n "$varName" ]; then + if [ -z "${!varName}" ]; then + log::error "Required option -$opt cannot be empty" + options::syntax_exit + fi + fi + fi + done +} + function options::syntax_exit() { @doc Print the command syntax and exit # Use BASH_SOURCE[-1] (bottom of the call stack = entry script) instead @@ -49,6 +71,7 @@ function options::clear() { OPTIONS_HAS_ARGS=() OPTIONS_PARSE_FUNCS=() OPTIONS_ENVIRONMENT=() + OPTIONS_SEEN=() OPTIONS_DESCRIPTION="" options::add -o h -d "prints syntax and exits" -f options::syntax_exit && HELP_OPT_ADDED="true" @@ -242,20 +265,34 @@ function options::standard() { function options::parse_available() { @doc parse the options using the provided argument array @arg "$@" the provided argument array + OPTIONS_SEEN=() while options::getopts opt "$@"; do if [ "$opt" != "?" ]; then + OPTIONS_SEEN[$opt]=1 if [ -n "${OPTIONS_ENVIRONMENT[$opt]}" ]; then local varName="${OPTIONS_ENVIRONMENT[$opt]}" local val - if [ -n "${OPTARG}" ]; then + if [ "${OPTIONS_HAS_ARGS[$opt]}" = "true" ]; then + val="${OPTARG-}" + if [ "${OPTIONS_OPTIONAL[$opt]}" != "true" ] && [ -z "$val" ]; then + log::error "Required option -$opt cannot be empty" + options::syntax_exit + fi + elif [ -n "${OPTARG}" ]; then val="${OPTARG}" else val="true" fi declare -g "$varName=${val}" elif [ -n "${OPTIONS_PARSE_FUNCS[$opt]}" ]; then + if [ "${OPTIONS_OPTIONAL[$opt]}" != "true" ] && + [ "${OPTIONS_HAS_ARGS[$opt]}" = "true" ] && + [ -z "${OPTARG-}" ]; then + log::error "Required option -$opt cannot be empty" + options::syntax_exit + fi if command -v "${OPTIONS_PARSE_FUNCS[$opt]}" >/dev/null; then - ${OPTIONS_PARSE_FUNCS[$opt]} "${OPTARG}" + ${OPTIONS_PARSE_FUNCS[$opt]} "${OPTARG-}" else echo "ERROR: for option ($opt) parse_functions must be defined or left out" exit 1 @@ -265,6 +302,7 @@ function options::parse_available() { return 1 fi done + options::_mandatory_options_validate } function options::parse() { diff --git a/bash/pagerduty.sh b/bash/pagerduty.sh index 0fe1f4d..e196558 100644 --- a/bash/pagerduty.sh +++ b/bash/pagerduty.sh @@ -67,6 +67,13 @@ function pagerduty::send_incident() { local incident_key="${6}" local rc=0 + local incident_payload + incident_payload=$( + pagerduty::_incident_data "$service_id" "$alert_type" "$alert_title" "$incident_key" + ) + local xtrace_was_on=0 + case $- in *x*) xtrace_was_on=1 ;; esac + { set +x; } 2>/dev/null # --fail-with-body needs curl >= 7.76.0 (Apr 2021). On older hosts curl # exits non-zero with "option unknown" — caller still sees a non-zero rc # below, just with a less specific error body than --fail-with-body @@ -77,8 +84,11 @@ function pagerduty::send_incident() { --header 'Accept: application/vnd.pagerduty+json;version=2' \ --header "From: $alert_from" \ --header "Authorization: Token token=$alert_token" \ - --data "$(pagerduty::_incident_data "$service_id" "$alert_type" "$alert_title" "$incident_key")" \ + --data "$incident_payload" \ "${PAGERDUTY_INCIDENTS_URL:-https://api.pagerduty.com/incidents}" || rc=$? + if [ "$xtrace_was_on" -eq 1 ]; then + set -x + fi if [ "$rc" -ne 0 ]; then log::error "PagerDuty incident send failed (curl rc=$rc)" return "$rc" diff --git a/bash/release-images b/bash/release-images index 1f512ce..b293b8e 100755 --- a/bash/release-images +++ b/bash/release-images @@ -20,11 +20,6 @@ source "$(dirname "${BASH_SOURCE[0]}")/includer.sh" @include options @include docker -log::level 2 -function usage() { - true -} - additional_registries=() function _add_additional_registry { local registry=${1:?} diff --git a/bash/switch-to-branch b/bash/switch-to-branch index 62673b8..642d569 100755 --- a/bash/switch-to-branch +++ b/bash/switch-to-branch @@ -46,9 +46,12 @@ for d in "$TARGET_DIR"/*; do log::warn "Cannot cd to $d" continue fi - if [ "$NEW_BRANCH" = "true" ] && exec::hide git::cmd checkout -b \ - "$BRANCH" "$@"; then - log::info "Checked out into new branch $BRANCH in $d" + if [ "$NEW_BRANCH" = "true" ]; then + if _new_branch_err=$(git::cmd checkout -b "$BRANCH" "$@" 2>&1); then + log::info "Checked out into new branch $BRANCH in $d" + else + log::warn "Failed to create new branch $BRANCH in $d — $_new_branch_err; not falling back to checkout of an existing branch" + fi elif exec::hide git::cmd checkout "$BRANCH" "$@"; then log::info "Checked out $BRANCH in $d" else diff --git a/tests/aws.bats b/tests/aws.bats index b801772..63bd425 100644 --- a/tests/aws.bats +++ b/tests/aws.bats @@ -55,3 +55,27 @@ setup() { " [ "$status" -ne 0 ] } + +@test "aws::wait_for_scan_complete returns non-zero after bounded attempts when stuck (SUR-2340)" { + run bash -c " + export LOG_DISABLE_TRACE=true + source '$REPO_ROOT/bash/includer.sh' + @include aws + aws::scan_status() { echo 'IN_PROGRESS'; } + sleep() { :; } + aws::wait_for_scan_complete myrepo mytag 0 5 + " + [ "$status" -ne 0 ] +} + +@test "aws::wait_for_scan_complete returns non-zero promptly on FAILED status (SUR-2340)" { + run bash -c " + export LOG_DISABLE_TRACE=true + source '$REPO_ROOT/bash/includer.sh' + @include aws + aws::scan_status() { echo 'FAILED'; } + sleep() { :; } + aws::wait_for_scan_complete myrepo mytag 0 99 + " + [ "$status" -ne 0 ] +} diff --git a/tests/docker.bats b/tests/docker.bats index a2a9fb3..24679e9 100644 --- a/tests/docker.bats +++ b/tests/docker.bats @@ -144,3 +144,16 @@ run_cp() { [[ "$output" == *"jq is either not installed or not on the PATH"* ]] [[ "$output" != *"-r: command not found"* ]] } + +@test "docker::registrycmd fails with empty docker config auth for registry (SUR-2337)" { + run bash -c " + export LOGFILE_DISABLE=true + mkdir -p \"\$HOME/.docker\" + printf '%s\n' '{}' > \"\$HOME/.docker/config.json\" + source '$REPO_ROOT/bash/includer.sh' + @include docker + docker::registrycmd _catalog 'registry.example.invalid' + " + [ "$status" -ne 0 ] + [[ "$output" == *"No docker credentials"* ]] || [[ "$output" == *"docker login"* ]] +} diff --git a/tests/kind-test-environment.bats b/tests/kind-test-environment.bats index d1ded53..9a07e11 100644 --- a/tests/kind-test-environment.bats +++ b/tests/kind-test-environment.bats @@ -28,7 +28,7 @@ teardown() { "HELM_ARGV_LOG=$HELM_ARGV_LOG" \ "KIND_TEST_ENVIRONMENT_SOURCE_ONLY=true" \ bash -c " - source '$REPO_ROOT/bash/kind-test-environment' -c create + source '$REPO_ROOT/bash/kind-test-environment' -c create -f /dev/null # Reset after source so the script's own repo defaults don't # contaminate the test's controlled list. ADD_HELM_REPOS=( @@ -71,7 +71,7 @@ STUB_EOF "PATH=$PATH" \ "KIND_TEST_ENVIRONMENT_SOURCE_ONLY=true" \ bash -c " - source '$REPO_ROOT/bash/kind-test-environment' -c create + source '$REPO_ROOT/bash/kind-test-environment' -c create -f /dev/null ::asdf kind [ -z \"\${_asdf:-}\" ] || { echo '_asdf leaked' >&2; exit 1; } [ -z \"\${_where:-}\" ] || { echo '_where leaked' >&2; exit 1; } diff --git a/tests/options.bats b/tests/options.bats index ba8fe2b..8a57415 100644 --- a/tests/options.bats +++ b/tests/options.bats @@ -88,3 +88,27 @@ setup() { [[ "$output" == *"a|optional=false|env=A"* ]] [[ "$output" == *"b|optional=true|env=B"* ]] } + +@test "options::parse_available fails when mandatory -m option is absent (SUR-2322)" { + run bash -c " + source '$REPO_ROOT/bash/includer.sh' + @include options + options::clear + options::add -o l -d 'label' -a -m -e LabelSelector + NO_SYNTAX_EXIT=1 options::parse_available + " + [ "$status" -ne 0 ] + [[ "$output" == *"Missing required option: -l"* ]] +} + +@test "options::parse_available fails when mandatory option argument is empty (SUR-2322)" { + run bash -c " + source '$REPO_ROOT/bash/includer.sh' + @include options + options::clear + options::add -o l -d 'label' -a -m -e LabelSelector + NO_SYNTAX_EXIT=1 options::parse_available -l '' + " + [ "$status" -ne 0 ] + [[ "$output" == *"Required option -l cannot be empty"* ]] +} diff --git a/tests/pagerduty.bats b/tests/pagerduty.bats index feb03e3..f11ead5 100644 --- a/tests/pagerduty.bats +++ b/tests/pagerduty.bats @@ -102,3 +102,23 @@ setup() { [[ "$output" == *"svc1"* ]] [[ "$output" == *"key-1"* ]] } + +@test "pagerduty::send_incident does not emit Token token= secret in xtrace (SUR-2339)" { + run bash -c " + trace=\$(mktemp) + trap 'rm -f \"\$trace\"' EXIT + source '$REPO_ROOT/bash/includer.sh' + @include pagerduty + pagerduty::_curl() { return 0; } + exec 2>\"\$trace\" + set -x + pagerduty::send_incident SVC1 incident T from 'PD_TOKEN_SUR2339_XYZ' '' + set +x + exec 2>&1 + if grep -F 'Token token=PD_TOKEN_SUR2339_XYZ' \"\$trace\"; then + exit 1 + fi + exit 0 + " + [ "$status" -eq 0 ] +} diff --git a/tests/switch-to-branch.bats b/tests/switch-to-branch.bats index 68fd8df..3e92db6 100644 --- a/tests/switch-to-branch.bats +++ b/tests/switch-to-branch.bats @@ -39,3 +39,14 @@ setup() { [ -z "$(git -C "$HOME/git/myorg/repo1" status --porcelain)" ] [ -z "$(git -C "$HOME/git/myorg/repo2" status --porcelain)" ] } + +@test "switch-to-branch -n does not checkout existing branch when create fails (SUR-2334)" { + git -C "$HOME/git/myorg/repo1" branch -f sur2334-newb main >/dev/null + run "$SWITCH" -o myorg -n -b sur2334-newb -v + [ "$status" -eq 0 ] + branch1=$(git -C "$HOME/git/myorg/repo1" rev-parse --abbrev-ref HEAD) + [ "$branch1" = "main" ] + [[ "$output" == *"Failed to create new branch sur2334-newb"* ]] + [ ! -f "$HOME/git/myorg/repo1/exec.log" ] + [ ! -f "$HOME/git/myorg/repo2/exec.log" ] +}