diff --git a/README.md b/README.md index 4320b81..3dc1d3e 100644 --- a/README.md +++ b/README.md @@ -62,11 +62,33 @@ See the `examples/` directory: ## Developer workflow -- `./scripts/setup-dev.sh` - verify required local tools +- `./scripts/setup-dev.sh` - validate local Go/Terraform toolchains and environment health (no changes) +- `./scripts/setup-dev.sh --fix` - attempt safe remediation with installed version managers (`mise`, `asdf`, `tfenv`, `tenv`) +- `./scripts/setup-dev.sh --strict` - require Terraform to be installed - `./scripts/check.sh` - formatting, vet, and unit tests (Tier A) - `./scripts/test-integration.sh` - local mock-backed provider integration tests (Tier B) - `./scripts/testacc.sh` - default: Tier B; live mode available with `--live` (Tier C) +### setup-dev behavior + +`scripts/setup-dev.sh` now performs robust validation and troubleshooting checks: + +- Always reports resolved paths for `bash`, `git`, `go`, and `terraform` (Terraform is warning-only unless `--strict` is used). +- Enforces minimum versions: + - Go `>= 1.23` (error if missing or too old) + - Terraform `>= 1.5` (warning if missing by default, error if present but too old) +- Detects common broken Go setups: + - manual `GOROOT` conflicting with `go env GOROOT` + - stale stdlib tree mismatches (for example missing `slices`, `maps`, `math/rand/v2`) +- Detects malformed `GOPROXY` and prints the recommended non-destructive fix: + - `go env -w GOPROXY=https://proxy.golang.org,direct` +- Supports optional safe auto-fix mode: + - only uses already-installed user-space managers + - does **not** run distro package managers, `sudo`, or shell-profile edits + - re-validates tools after attempted remediation + +The script is safe for both normal shells and VS Code integrated terminals, and includes guidance when terminal/session restart may be needed after changing versions. + ### Standalone repository guarantee This repository is intentionally self-contained: @@ -74,6 +96,25 @@ This repository is intentionally self-contained: - no dependency on external helper scripts (for example `services-up.sh`) - no hardcoded local paths (for example `/workspace/...` or `/home/...`) +### setup-dev troubleshooting quick reference + +- **Malformed GOPROXY** + - Symptom: warning that GOPROXY has no valid entries. + - Fix: `go env -w GOPROXY=https://proxy.golang.org,direct` + +- **Broken GOROOT** + - Symptom: `GOROOT` environment value differs from `go env GOROOT`, or stdlib package checks fail. + - Fix: usually remove manual override with `unset GOROOT`, then ensure the intended `go` binary is first in `PATH`. + +- **Go version / stdlib path mismatch** + - Symptom: `go version` looks modern but build errors mention missing stdlib packages (for example `slices`, `maps`, `math/rand/v2`). + - Cause: stale or mismatched Go installation path. + - Fix: re-select/install Go via your version manager and re-run `./scripts/setup-dev.sh`. + +- **VS Code integrated terminal stale environment** + - Symptom: command paths or versions do not match your expected shell setup. + - Fix: restart the integrated terminal (or reload the VS Code window) and run `./scripts/setup-dev.sh` again. + ### Build ```bash diff --git a/scripts/setup-dev.sh b/scripts/setup-dev.sh index e482353..c354916 100755 --- a/scripts/setup-dev.sh +++ b/scripts/setup-dev.sh @@ -1,20 +1,450 @@ #!/usr/bin/env bash set -euo pipefail -echo "[setup-dev] Validating local toolchain" +MIN_GO_VERSION="1.23.0" +MIN_TERRAFORM_VERSION="1.5.0" -for cmd in bash git go; do - if ! command -v "$cmd" >/dev/null 2>&1; then - echo "[setup-dev][error] Missing required command: $cmd" >&2 - exit 1 - fi - echo "[setup-dev] $cmd: $(command -v "$cmd")" +FIX_MODE=0 +STRICT_MODE=0 + +log() { + echo "[setup-dev] $*" +} + +warn() { + echo "[setup-dev][warn] $*" +} + +error() { + echo "[setup-dev][error] $*" >&2 +} + +usage() { + cat </dev/null 2>&1; then - echo "[setup-dev] terraform: $(command -v terraform)" -else - echo "[setup-dev][warn] terraform not found (only required for terraform validate and acceptance workflows)" -fi +is_vscode_terminal() { + [[ "${TERM_PROGRAM:-}" == "vscode" || -n "${VSCODE_PID:-}" || -n "${VSCODE_CWD:-}" ]] +} -echo "[setup-dev] Toolchain validation complete" +normalize_version() { + local raw="$1" + raw="${raw#v}" + raw="${raw%%-*}" + raw="${raw%%+*}" + local major minor patch + IFS='.' read -r major minor patch <<<"$raw" + major="${major:-0}" + minor="${minor:-0}" + patch="${patch:-0}" + printf '%s.%s.%s\n' "$major" "$minor" "$patch" +} + +version_ge() { + local a b + a="$(normalize_version "$1")" + b="$(normalize_version "$2")" + local a1 a2 a3 b1 b2 b3 + IFS='.' read -r a1 a2 a3 <<<"$a" + IFS='.' read -r b1 b2 b3 <<<"$b" + + if (( a1 > b1 )); then return 0; fi + if (( a1 < b1 )); then return 1; fi + if (( a2 > b2 )); then return 0; fi + if (( a2 < b2 )); then return 1; fi + if (( a3 >= b3 )); then return 0; fi + return 1 +} + +command_path() { + command -v "$1" 2>/dev/null || true +} + +check_required_command() { + local cmd="$1" + local path + path="$(command_path "$cmd")" + if [[ -z "$path" ]]; then + error "Missing required command: $cmd" + return 1 + fi + log "$cmd path: $path" + return 0 +} + +parse_go_version() { + local output="$1" + sed -nE 's/^go version go([0-9]+(\.[0-9]+){1,2}).*/\1/p' <<<"$output" +} + +parse_tf_version_text() { + local output="$1" + sed -nE 's/^Terraform v([0-9]+(\.[0-9]+){1,2}).*/\1/p' <<<"$output" | head -n1 +} + +parse_tf_version_json() { + local output="$1" + sed -nE 's/.*"terraform_version"[[:space:]]*:[[:space:]]*"([0-9]+(\.[0-9]+){1,2})".*/\1/p' <<<"$output" | head -n1 +} + +has_go_stdlib_package() { + local goroot="$1" + local pkg="$2" + [[ -d "$goroot/src/$pkg" ]] +} + +validate_goproxy_entry() { + local entry="$1" + if [[ -z "$entry" ]]; then + return 1 + fi + case "$entry" in + off|direct) + return 0 + ;; + http://*|https://*|file://*) + return 0 + ;; + esac + return 1 +} + +check_goproxy() { + local bad=0 + local env_goproxy goenv_goproxy + env_goproxy="${GOPROXY:-}" + goenv_goproxy="$(go env GOPROXY 2>/dev/null || true)" + + if [[ -n "$env_goproxy" ]]; then + log "GOPROXY env: $env_goproxy" + fi + if [[ -n "$goenv_goproxy" ]]; then + log "go env GOPROXY: $goenv_goproxy" + fi + + local effective="$goenv_goproxy" + if [[ -z "$effective" ]]; then + return 0 + fi + + local valid=0 + IFS=',' read -r -a entries <<<"$effective" + for entry in "${entries[@]}"; do + entry="$(echo "$entry" | xargs)" + if validate_goproxy_entry "$entry"; then + valid=1 + break + fi + done + + if (( valid == 0 )); then + warn "GOPROXY appears malformed: '$effective'" + warn "Recommended fix: go env -w GOPROXY=https://proxy.golang.org,direct" + bad=1 + fi + + return "$bad" +} + +get_go_state() { + GO_PATH="$(command_path go)" + GO_RAW_VERSION="" + GO_VERSION="" + GO_GOROOT="" + + if [[ -n "$GO_PATH" ]]; then + GO_RAW_VERSION="$(go version 2>/dev/null || true)" + GO_VERSION="$(parse_go_version "$GO_RAW_VERSION")" + GO_GOROOT="$(go env GOROOT 2>/dev/null || true)" + fi +} + +validate_go_environment() { + local bad=0 + get_go_state + + if [[ -z "$GO_PATH" ]]; then + error "go not found in PATH" + bad=1 + return "$bad" + fi + + log "go path: $GO_PATH" + if [[ -n "$GO_RAW_VERSION" ]]; then + log "go version: $GO_RAW_VERSION" + fi + if [[ -n "$GO_GOROOT" ]]; then + log "go env GOROOT: $GO_GOROOT" + fi + if [[ -n "${GOROOT:-}" ]]; then + log "GOROOT env: ${GOROOT}" + fi + + if [[ -z "$GO_VERSION" ]]; then + error "Unable to parse Go version from: '${GO_RAW_VERSION:-}'" + bad=1 + elif version_ge "$GO_VERSION" "$MIN_GO_VERSION"; then + log "Go version check: PASS (detected $GO_VERSION, required >= $MIN_GO_VERSION)" + else + error "Go version check: FAIL (detected $GO_VERSION, required >= $MIN_GO_VERSION)" + bad=1 + fi + + if [[ -n "${GOROOT:-}" && -n "$GO_GOROOT" && "$GOROOT" != "$GO_GOROOT" ]]; then + error "GOROOT mismatch: GOROOT env differs from 'go env GOROOT'." + warn "Usually GOROOT should not be set manually. Try: unset GOROOT" + bad=1 + fi + + if [[ -n "$GO_GOROOT" ]]; then + local missing_std=0 + local pkg + for pkg in "slices" "maps" "math/rand/v2"; do + if ! has_go_stdlib_package "$GO_GOROOT" "$pkg"; then + warn "Stdlib package missing from GOROOT ($GO_GOROOT): $pkg" + missing_std=1 + fi + done + + if (( missing_std == 1 )); then + error "Detected a possibly stale or mismatched GOROOT. This can cause build failures on modern Go versions." + warn "Likely cause: manual GOROOT override or a broken Go installation path precedence." + warn "Next steps: unset GOROOT, ensure correct 'go' is first in PATH, then re-open shell/terminal." + bad=1 + fi + fi + + check_goproxy || true + + return "$bad" +} + +get_terraform_state() { + TERRAFORM_PATH="$(command_path terraform)" + TERRAFORM_VERSION="" + TERRAFORM_RAW_VERSION="" + + if [[ -n "$TERRAFORM_PATH" ]]; then + local json_output + json_output="$(terraform version -json 2>/dev/null || true)" + if [[ -n "$json_output" ]]; then + TERRAFORM_VERSION="$(parse_tf_version_json "$json_output")" + fi + + TERRAFORM_RAW_VERSION="$(terraform version 2>/dev/null || true)" + if [[ -z "$TERRAFORM_VERSION" ]]; then + TERRAFORM_VERSION="$(parse_tf_version_text "$TERRAFORM_RAW_VERSION")" + fi + fi +} + +validate_terraform() { + local bad=0 + get_terraform_state + + if [[ -z "$TERRAFORM_PATH" ]]; then + if (( STRICT_MODE == 1 )); then + error "terraform not found in PATH (required in --strict mode)" + bad=1 + else + warn "terraform not found in PATH (required for validate/acceptance workflows)" + fi + return "$bad" + fi + + log "terraform path: $TERRAFORM_PATH" + if [[ -n "$TERRAFORM_VERSION" ]]; then + log "terraform version: $TERRAFORM_VERSION" + else + warn "Unable to parse Terraform version from output: ${TERRAFORM_RAW_VERSION//$'\n'/ | }" + fi + + if [[ -z "$TERRAFORM_VERSION" ]]; then + bad=1 + elif version_ge "$TERRAFORM_VERSION" "$MIN_TERRAFORM_VERSION"; then + log "Terraform version check: PASS (detected $TERRAFORM_VERSION, required >= $MIN_TERRAFORM_VERSION)" + else + error "Terraform version check: FAIL (detected $TERRAFORM_VERSION, required >= $MIN_TERRAFORM_VERSION)" + bad=1 + fi + + return "$bad" +} + +has_manager() { + command -v "$1" >/dev/null 2>&1 +} + +attempt_fix_go() { + local target="${MIN_GO_VERSION%.*}" + log "Attempting Go remediation (minimum: $MIN_GO_VERSION)" + + if has_manager mise; then + log "Using mise for Go remediation" + if mise install "go@$target" >/dev/null 2>&1 && mise use -q "go@$target" >/dev/null 2>&1; then + hash -r + log "mise applied go@$target for this repository" + return 0 + fi + warn "mise was detected but automatic Go remediation failed" + fi + + if has_manager asdf; then + log "Using asdf for Go remediation" + if asdf plugin list 2>/dev/null | grep -qx 'golang'; then + if asdf install golang "$target" >/dev/null 2>&1 && asdf set golang "$target" >/dev/null 2>&1; then + hash -r + log "asdf applied golang $target for this repository" + return 0 + fi + warn "asdf golang plugin exists but install/set failed" + else + warn "asdf detected, but golang plugin is not installed" + warn "Install plugin manually: asdf plugin add golang https://github.com/asdf-community/asdf-golang.git" + fi + fi + + warn "Could not auto-remediate Go with installed managers." + warn "Manual options:" + warn " - mise install go@$target && mise use go@$target" + warn " - asdf install golang $target && asdf set golang $target" + return 1 +} + +attempt_fix_terraform() { + local target="${MIN_TERRAFORM_VERSION%.*}" + log "Attempting Terraform remediation (minimum: $MIN_TERRAFORM_VERSION)" + + if has_manager mise; then + log "Using mise for Terraform remediation" + if mise install "terraform@$target" >/dev/null 2>&1 && mise use -q "terraform@$target" >/dev/null 2>&1; then + hash -r + log "mise applied terraform@$target for this repository" + return 0 + fi + warn "mise was detected but automatic Terraform remediation failed" + fi + + if has_manager asdf; then + log "Using asdf for Terraform remediation" + if asdf plugin list 2>/dev/null | grep -qx 'terraform'; then + if asdf install terraform "$target" >/dev/null 2>&1 && asdf set terraform "$target" >/dev/null 2>&1; then + hash -r + log "asdf applied terraform $target for this repository" + return 0 + fi + warn "asdf terraform plugin exists but install/set failed" + else + warn "asdf detected, but terraform plugin is not installed" + warn "Install plugin manually: asdf plugin add terraform https://github.com/asdf-community/asdf-hashicorp.git" + fi + fi + + if has_manager tfenv; then + log "Using tfenv for Terraform remediation" + if tfenv install "$target" >/dev/null 2>&1 && tfenv use "$target" >/dev/null 2>&1; then + hash -r + log "tfenv selected Terraform $target" + return 0 + fi + warn "tfenv was detected but install/use failed" + fi + + if has_manager tenv; then + log "Using tenv for Terraform remediation" + if tenv tf install "$target" >/dev/null 2>&1 && tenv tf use "$target" >/dev/null 2>&1; then + hash -r + log "tenv selected Terraform $target" + return 0 + fi + warn "tenv was detected but install/use failed" + fi + + warn "Could not auto-remediate Terraform with installed managers." + warn "Manual options:" + warn " - mise install terraform@$target && mise use terraform@$target" + warn " - asdf install terraform $target && asdf set terraform $target" + warn " - tfenv install $target && tfenv use $target" + warn " - tenv tf install $target && tenv tf use $target" + return 1 +} + +print_environment_context() { + log "Validating local toolchain (mode: fix=$FIX_MODE strict=$STRICT_MODE)" + if is_vscode_terminal; then + warn "VS Code integrated terminal detected; inherited environment variables may be stale." + warn "If paths/versions look incorrect after changes, restart the integrated terminal or reload VS Code window." + fi + + if [[ -n "${IN_NIX_SHELL:-}" || -n "${NIX_PROFILES:-}" ]]; then + warn "Nix shell/develop environment detected. This script will only report/advice and not mutate Nix configs." + fi +} + +main() { + local failed=0 + + print_environment_context + + check_required_command bash || failed=1 + check_required_command git || failed=1 + + if ! validate_go_environment; then + failed=1 + if (( FIX_MODE == 1 )); then + attempt_fix_go || true + log "Re-validating Go after remediation attempt" + validate_go_environment || failed=1 + fi + fi + + if ! validate_terraform; then + failed=1 + if (( FIX_MODE == 1 )); then + attempt_fix_terraform || true + log "Re-validating Terraform after remediation attempt" + validate_terraform || failed=1 + fi + fi + + if (( failed == 0 )); then + log "Toolchain validation complete: PASS" + exit 0 + fi + + error "Toolchain validation complete: FAIL" + if is_vscode_terminal; then + warn "If you changed tool versions during this run, restart VS Code terminal and re-run ./scripts/setup-dev.sh" + fi + exit 1 +} + +main "$@"