blob: eaeee11ebeca6e2a1e57f000bb5de0007f654d23 [file]
#!/usr/bin/env bash
# Copyright 2026 The etcd Authors
#
# 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.
set -euo pipefail
ETCD_ROOT_DIR=${ETCD_ROOT_DIR:-$(git rev-parse --show-toplevel)}
source "${ETCD_ROOT_DIR}/scripts/test_lib.sh"
# dump_module_deps outputs dependency records in CSV format:
# <dep_path>,<version>,<direct|indirect>,<module_dir>
# Similar to scripts/test.sh but records the relative directory instead of
# the Go module path, so we can use it directly with run_for_module.
# shellcheck disable=SC2317 # invoked indirectly via run_for_workspace_modules
function dump_module_deps {
local mod_dir
mod_dir=$(realpath --relative-to="${ETCD_ROOT_DIR}" .)
local json_mod
json_mod=$(run go mod edit -json)
local require
require=$(echo "${json_mod}" | jq -r '.Require')
if [ "$require" == "null" ]; then
return 0
fi
echo "$require" | jq -r '.[] | .Path+","+.Version+","+if .Indirect then " (indirect)" else "" end+",'"${mod_dir}"'"'
}
function collect_all_deps {
log_callout "Collecting dependencies from all modules..."
run_for_workspace_modules dump_module_deps | sort
}
# find_inconsistencies outputs dependency paths that have version conflicts
# Input: collected dependency records (CSV output of collect_all_deps)
# Output: one dependency path per line for each inconsistent dependency
function find_inconsistencies {
local all_deps="$1"
echo "${all_deps}" | cut -d ',' -f 1,2 | sort | uniq | cut -d ',' -f 1 | sort | uniq -d
}
# select_latest_version asks the Go workspace for the version of a dependency
# selected across all workspace modules (minimal version selection picks the
# highest version required by any module, pre-releases included).
# Arguments:
# $1 - dependency path
# Output: the selected version string
function select_latest_version {
local dep="$1"
local version
if ! version=$(run_for_module . go list -m -f '{{.Version}}' "${dep}"); then
log_error "FAIL: unable to resolve the selected version for ${dep}"
return 1
fi
if [[ -z "${version}" ]]; then
log_error "FAIL: ${dep} is a workspace module; automatic version alignment is not supported"
return 1
fi
printf '%s\n' "${version}"
}
# fix_inconsistencies resolves dependency version conflicts by running
# `go get dep@latest_version` in each affected module, then `go mod tidy`.
# Arguments:
# $1 - all dependency records
# $2 - list of inconsistent dependency paths
# Returns non-zero on first failure
function fix_inconsistencies {
local all_deps="$1"
local inconsistent_deps="$2"
for dup in ${inconsistent_deps}; do
local latest
latest=$(select_latest_version "${dup}") || return 1
log_callout "Fixing ${dup} -> ${latest}"
# Find which module dirs have this dep (at any version)
local affected_dirs
affected_dirs=$(echo "${all_deps}" | grep "^${dup}," | cut -d ',' -f 4 | sort -u)
for mod_dir in ${affected_dirs}; do
if ! run_for_module "${mod_dir}" run go get "${dup}@${latest}"; then
log_error "FAIL: go get ${dup}@${latest} failed in module ${mod_dir}"
return 1
fi
done
done
# After all go get operations, tidy each affected module.
local all_affected_dirs
all_affected_dirs=$(for dup in ${inconsistent_deps}; do
echo "${all_deps}" | grep "^${dup}," | cut -d ',' -f 4 | sort -u
done | sort -u)
for mod_dir in ${all_affected_dirs}; do
if ! run_for_module "${mod_dir}" run go mod tidy; then
log_error "FAIL: go mod tidy failed in module ${mod_dir}"
return 1
fi
done
}
# verify_dep runs the same check as `make verify-dep`.
function verify_dep {
PASSES="dep" "${ETCD_ROOT_DIR}/scripts/test.sh" &> /dev/null
}
# resolve_inconsistencies runs one collect + fix round.
# Returns 0 when there was nothing left to fix or the fixes were applied,
# non-zero on failure.
function resolve_inconsistencies {
local all_deps
all_deps=$(collect_all_deps)
if [[ -z "${all_deps}" ]]; then
log_error "FAIL: dependency collection returned no results"
return 1
fi
local inconsistent_deps
inconsistent_deps=$(find_inconsistencies "${all_deps}")
if [[ -z "${inconsistent_deps}" ]]; then
return 0
fi
local inconsistent_count
inconsistent_count=$(echo "${inconsistent_deps}" | wc -l)
log_callout "Found ${inconsistent_count} inconsistent dependencies. Resolving..."
fix_inconsistencies "${all_deps}" "${inconsistent_deps}"
}
# Nothing to do when verify-dep already passes. Otherwise fix and re-verify:
# fixing one dependency can pull in newer versions of others, so we repeat
# until consistent or MAX_ATTEMPTS is reached.
function main {
local max_attempts="${MAX_ATTEMPTS:-5}"
local attempt
for ((attempt = 1; attempt <= max_attempts; attempt++)); do
if verify_dep; then
if [[ "${attempt}" == 1 ]]; then
log_success "No dependencies with inconsistencies. Nothing to do"
else
log_success "All dependency inconsistencies resolved"
fi
exit 0
fi
log_callout "Attempt ${attempt}/${max_attempts}: resolving dependency inconsistencies..."
if ! resolve_inconsistencies; then
log_error "FAIL: error while trying to fix inconsistencies"
exit 1
fi
done
# Verify the result of the last attempt.
if verify_dep; then
log_success "All dependency inconsistencies resolved"
exit 0
fi
log_error "FAIL: dependencies still inconsistent after ${max_attempts} attempts"
exit 1
}
main