diff --git a/lib/elixir_funcs.sh b/lib/elixir_funcs.sh index 0c399c4..5000bf1 100644 --- a/lib/elixir_funcs.sh +++ b/lib/elixir_funcs.sh @@ -95,7 +95,57 @@ function backup_mix() { function install_hex() { output_section "Installing Hex" - mix local.hex --force + + # Capture the output so we can detect and explain the known OTP 27.x TLS + # regression (erlang/otp#9208) that breaks Hex installation. Without this, + # customers only see an opaque certificate error and have to open a ticket. + local hex_log + local status=0 + hex_log=$(mix local.hex --force 2>&1) || status=$? + + echo "${hex_log}" + + if [ "${status}" -ne 0 ]; then + diagnose_hex_tls_regression "${hex_log}" + exit "${status}" + fi +} + +# Detects the Erlang/OTP TLS regression (erlang/otp#9208) that makes Hex +# installation fail with a "key_usage_mismatch" certificate error when Mix +# downloads from builds.hex.pm. Introduced on the OTP 27 line and fixed in +# OTP 27.2.2. Prints guidance tailored to whichever version-config file the +# app actually uses, so support does not have to triage these one by one. +function diagnose_hex_tls_regression() { + local hex_log=$1 + + # "key_usage_mismatch" is the distinctive marker of this regression; gate on + # the OTP 27 line so we never misattribute an unrelated cert error to it. + echo "${hex_log}" | grep -q "key_usage_mismatch" || return 0 + [ "$(otp_version "${erlang_version}")" = "27" ] || return 0 + + output_line "" + output_warning "Hex could not be installed because of a known TLS regression in Erlang/OTP ${erlang_version}." + output_warning "This is erlang/otp#9208, fixed in OTP 27.2.2." + output_line "" + output_line "Fix: set your Erlang/OTP version to 27.2.2 or newer." + + if [ -n "$(extract_asdf_version erlang)" ]; then + output_line "Update the 'erlang' line in your .tool-versions file, e.g.:" + output_line " erlang 27.2.2" + elif [ -f "${build_path}/elixir_buildpack.config" ] && grep -q "^erlang_version=" "${build_path}/elixir_buildpack.config"; then + output_line "Update erlang_version in your elixir_buildpack.config, e.g.:" + output_line " erlang_version=27.2.2" + else + output_line "Set erlang_version in your elixir_buildpack.config, e.g.:" + output_line " erlang_version=27.2.2" + output_line "or add an 'erlang' line to a .tool-versions file, e.g.:" + output_line " erlang 27.2.2" + fi + + output_line "" + output_line "Reference: https://github.com/erlang/otp/issues/9208" + output_line "" } function install_rebar() { diff --git a/test/elixir_funcs.sh b/test/elixir_funcs.sh index 83b9142..04abb0e 100755 --- a/test/elixir_funcs.sh +++ b/test/elixir_funcs.sh @@ -7,6 +7,19 @@ source $SCRIPT_DIR/.test_support.sh # include source file source $SCRIPT_DIR/../lib/elixir_funcs.sh +# diagnose_hex_tls_regression uses extract_asdf_version from misc_funcs.sh +source $SCRIPT_DIR/../lib/misc_funcs.sh + +# A real "mix local.hex --force" failure on an affected OTP 27.x release. +TLS_REGRESSION_LOG='** (Mix) httpc request failed with: {:failed_connect, [{:to_address, {~c"builds.hex.pm", 443}}, {:inet, [:inet], {:tls_alert, {:unsupported_certificate, ~c"TLS client: ... CLIENT ALERT: Fatal - Unsupported Certificate\n {key_usage_mismatch,{{Extension,{2,5,29,15},true,[keyCertSign,cRLSign]}}}"}}}]}' + +# Capture guidance emitted via output_line/output_warning (stubbed silent by +# the framework) so tests can assert on the message. +capture_guidance() { + GUIDANCE="" + output_line() { GUIDANCE+="$1"$'\n'; } + output_warning() { GUIDANCE+="$1"$'\n'; } +} # TESTS @@ -61,4 +74,60 @@ suite "elixir_download_file" [ "$result" == "elixir-v1.14.5-otp-25.zip" ] +suite "diagnose_hex_tls_regression" + + test "explains regression and points at .tool-versions when used" + + erlang_version="27.2" + printf 'erlang 27.2\nelixir 1.18.1-otp-27\n' > ${build_path}/.tool-versions + rm -f ${build_path}/elixir_buildpack.config + capture_guidance + diagnose_hex_tls_regression "$TLS_REGRESSION_LOG" + + echo "$GUIDANCE" | grep -q "27.2.2" && + echo "$GUIDANCE" | grep -q "erlang/otp#9208" && + echo "$GUIDANCE" | grep -q ".tool-versions" + + + test "points at elixir_buildpack.config when used" + + erlang_version="27.2" + rm -f ${build_path}/.tool-versions + printf 'erlang_version=27.2\nelixir_version=1.18.1\n' > ${build_path}/elixir_buildpack.config + capture_guidance + diagnose_hex_tls_regression "$TLS_REGRESSION_LOG" + + echo "$GUIDANCE" | grep -q "elixir_buildpack.config" && + echo "$GUIDANCE" | grep -q "27.2.2" + + + test "gives generic guidance when neither config file sets a version" + + erlang_version="27.2" + rm -f ${build_path}/.tool-versions ${build_path}/elixir_buildpack.config + capture_guidance + diagnose_hex_tls_regression "$TLS_REGRESSION_LOG" + + echo "$GUIDANCE" | grep -q "elixir_buildpack.config" && + echo "$GUIDANCE" | grep -q ".tool-versions" + + + test "stays silent for unrelated hex failures" + + erlang_version="27.2" + capture_guidance + diagnose_hex_tls_regression "** (Mix) some other unrelated error" + + [ -z "$GUIDANCE" ] + + + test "stays silent when OTP line is not 27" + + erlang_version="26.2.1" + capture_guidance + diagnose_hex_tls_regression "$TLS_REGRESSION_LOG" + + [ -z "$GUIDANCE" ] + + PASSED_ALL_TESTS=true