diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 0000000000..5543242a5a --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,195 @@ +--- +name: Integration Tests + +on: + push: + tags: + - v* + branches: + - master + - main + pull_request: + +# Cancel in-progress jobs if a new commit is pushed to the same branch +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build Validation + runs-on: ubuntu-latest + container: + image: goalert/build-env:go1.24.2 + volumes: + - /var/cache/arc:/var/cache/arc + steps: + - uses: actions/checkout@v4 + - name: Set git safe directory + run: git config --global --add safe.directory $GITHUB_WORKSPACE + - uses: actions/cache@v4 + with: + key: cache-${{ hashFiles('go.mod','go.sum','package.json','bun.lock','*.version')}} + path: | + /go/pkg/mod + $HOME/.cache/goalert-gettool + $HOME/.cache/Cypress + $HOME/.cache/ms-playwright + $HOME/.bun/install/cache + - name: Build all binaries + run: make cache + + check: + name: make check + # You need to use the INSTALLATION_NAME from the previous step + runs-on: ubuntu-latest + needs: build + container: + image: goalert/build-env:go1.24.2 + volumes: + - /var/cache/arc:/var/cache/arc + steps: + - uses: actions/checkout@v4 + - name: Set git safe directory + run: git config --global --add safe.directory $GITHUB_WORKSPACE + - uses: actions/cache@v4 + with: + key: cache-${{ hashFiles('go.mod','go.sum','package.json','bun.lock','*.version')}} + path: | + /go/pkg/mod + $HOME/.cache/goalert-gettool + $HOME/.cache/Cypress + $HOME/.cache/ms-playwright + $HOME/.bun/install/cache + + # Run repo & linting checks + - name: Run Repo & Linting Checks + run: make check self-test + + unit: + name: Unit Tests + # You need to use the INSTALLATION_NAME from the previous step + runs-on: ubuntu-latest + needs: build + container: + image: goalert/build-env:go1.24.2 + volumes: + - /var/cache/arc:/var/cache/arc + steps: + - uses: actions/checkout@v4 + - name: Set git safe directory + run: git config --global --add safe.directory $GITHUB_WORKSPACE + + - uses: actions/cache@v4 + with: + key: cache-${{ hashFiles('go.mod','go.sum','package.json','bun.lock','*.version')}} + path: | + /go/pkg/mod + $HOME/.cache/goalert-gettool + $HOME/.cache/Cypress + $HOME/.cache/ms-playwright + $HOME/.bun/install/cache + + # Run repo & linting checks + - name: Run Tests + run: make test-unit test-components + + # smoke: + # name: Smoke Tests (behavioral tests) + # # You need to use the INSTALLATION_NAME from the previous step + # runs-on: arc-runner-set + # needs: build + # strategy: + # matrix: + # postgres_version: [13, 17] + # container: + # image: goalert/build-env:go1.24.2 + # volumes: + # - /var/cache/arc:/var/cache/arc + # steps: + # - uses: actions/checkout@v4 + # - name: Set git safe directory + # run: git config --global --add safe.directory $GITHUB_WORKSPACE + + # - name: Restore cache + # run: bash ./devtools/scripts/ci-cache.sh restore + # env: { CI: '1' } + + # # Run repo & linting checks + # - name: Run Smoke Tests + # run: start_postgres ${{ matrix.postgres_version }} make smoketest + + # playwright: + # name: Playwright Tests + # # You need to use the INSTALLATION_NAME from the previous step + # runs-on: arc-runner-set + # needs: build + # strategy: + # matrix: + # postgres_version: [13, 17] + # container: + # image: goalert/build-env:go1.24.2 + # volumes: + # - /var/cache/arc:/var/cache/arc + # steps: + # - uses: actions/checkout@v4 + # - name: Set git safe directory + # run: git config --global --add safe.directory $GITHUB_WORKSPACE + + # - name: Restore cache + # run: bash ./devtools/scripts/ci-cache.sh restore + # env: { CI: '1' } + + # # Run repo & linting checks + # - name: Run Smoke Tests + # run: bash -c 'start_postgres ${{ matrix.postgres_version }} make playwright-run' + + # cy-wide: + # name: Cypess Widescreen Tests + # # You need to use the INSTALLATION_NAME from the previous step + # runs-on: arc-runner-set + # needs: build + # strategy: + # matrix: + # postgres_version: [13, 17] + # container: + # image: goalert/build-env:go1.24.2 + # volumes: + # - /var/cache/arc:/var/cache/arc + # steps: + # - uses: actions/checkout@v4 + # - name: Set git safe directory + # run: git config --global --add safe.directory $GITHUB_WORKSPACE + + # - name: Restore cache + # run: bash ./devtools/scripts/ci-cache.sh restore + # env: { CI: '1' } + + # # Run repo & linting checks + # - name: Run Smoke Tests + # run: bash -c 'start_postgres ${{ matrix.postgres_version }} make cy-wide-prod-run' + + # cy-mobile: + # name: Cypess Mobile Tests + # # You need to use the INSTALLATION_NAME from the previous step + # runs-on: arc-runner-set + # needs: build + # strategy: + # matrix: + # postgres_version: [13, 17] + # container: + # image: goalert/build-env:go1.24.2 + # volumes: + # - /var/cache/arc:/var/cache/arc + # steps: + # - uses: actions/checkout@v4 + # - name: Set git safe directory + # run: git config --global --add safe.directory $GITHUB_WORKSPACE + + # - name: Restore cache + # run: bash ./devtools/scripts/ci-cache.sh restore + # env: { CI: '1' } + + # # Run repo & linting checks + # - name: Run Smoke Tests + # run: bash -c 'start_postgres ${{ matrix.postgres_version }} make cy-mobile-prod-run' diff --git a/Makefile b/Makefile index b97ca31c5a..caada66ed3 100644 --- a/Makefile +++ b/Makefile @@ -97,6 +97,25 @@ $(BIN_DIR)/tools/mailpit: mailpit.version $(BIN_DIR)/tools/bun: bun.version go tool gettool -t bun -v $(shell cat bun.version) -o $@ +.PHONY: setup-cypress +setup-cypress: $(BIN_DIR)/tools/bun + bun x cypress install + +.PHONY: setup-playwright +setup-playwright: $(BIN_DIR)/tools/bun + bun x playwright install + +.PHONY: deps +deps: setup-cypress setup-playwright ## Install all dependencies + go get tool ./... + +.PHONY: cache +cache: deps + $(MAKE) -j4 binaries + +.PHONY: binaries +binaries: bin/goalert bin/goalert.cover bin/goalert-linux-amd64.tgz bin/goalert-linux-arm.tgz bin/goalert-linux-arm64.tgz bin/goalert-darwin-amd64.tgz bin/goalert-windows-amd64.zip ## Prime the build cache by building all binaries + bun.lock: $(BIN_DIR)/tools/bun $(BIN_DIR)/tools/bun install touch "$@" @@ -226,6 +245,10 @@ generate: $(NODE_DEPS) pkg/sysapi/sysapi.pb.go pkg/sysapi/sysapi_grpc.pb.go $(SQLC) generate go generate ./... +.PHONY: self-test +self-test: + $(MAKE) bin/goalert BUNDLE=1 + ./bin/goalert self-test --offline test-all: test-unit test-components test-smoke test-integration test-integration: playwright-run cy-wide-prod-run cy-mobile-prod-run @@ -321,6 +344,7 @@ resetdb: config.json.bak ## Recreate the database leaving it empty (no migration go tool resetdb --no-migrate clean: ## Clean up build artifacts + chmod +w -f -R bin || true rm -rf bin node_modules web/src/node_modules .pnp.cjs .pnp.loader.mjs web/src/build/static .yarn storybook-static new-migration: diff --git a/bun.version b/bun.version index c813fe116c..9d4f8239dc 100644 --- a/bun.version +++ b/bun.version @@ -1 +1 @@ -1.2.5 +1.2.9 diff --git a/devtools/ci/dockerfiles/build-env/Dockerfile b/devtools/ci/dockerfiles/build-env/Dockerfile index 7e53f75c4c..d42fa6e044 100644 --- a/devtools/ci/dockerfiles/build-env/Dockerfile +++ b/devtools/ci/dockerfiles/build-env/Dockerfile @@ -4,6 +4,7 @@ RUN apt-get update && apt-get install -y \ apt-transport-https \ ca-certificates \ curl \ + unzip \ gnupg-agent \ software-properties-common \ build-essential \ @@ -26,7 +27,7 @@ RUN \ rm -rf /var/lib/apt/lists/* # Postgres -ENV PGDATA=/var/lib/postgresql/data PGUSER=postgres DB_URL=postgresql://postgres@?client_encoding=UTF8 +ENV PGDATA=/var/lib/postgresql/data PGUSER=postgres DB_URL=postgresql://postgres@?client_encoding=UTF8&sslmode=disable RUN mkdir -p ${PGDATA} /run/postgresql /var/log/postgresql &&\ chown postgres ${PGDATA} /run/postgresql /var/log/postgresql &&\ su postgres -c "/usr/lib/postgresql/13/bin/initdb $PGDATA" &&\ diff --git a/devtools/gettool/fetchfile.go b/devtools/gettool/fetchfile.go index e6eb4bd985..472edcade1 100644 --- a/devtools/gettool/fetchfile.go +++ b/devtools/gettool/fetchfile.go @@ -1,14 +1,65 @@ package main import ( + "crypto/sha256" + "encoding/hex" + "errors" "fmt" "io" + "log" "net/http" "os" + "path/filepath" ) +var errMiss = fmt.Errorf("file not found in cache") + +func hash256(in string) string { + sum := sha256.Sum256([]byte(in)) + return hex.EncodeToString(sum[:]) +} + +func getCacheFile(url string) (*os.File, int64, error) { + if cacheDir == "" { + return nil, 0, errMiss + } + + file := filepath.Join(cacheDir, hash256(url)+".data") + fd, err := os.Open(file) + if errors.Is(err, os.ErrNotExist) { + return nil, 0, errMiss + } + info, err := fd.Stat() + if err != nil { + return nil, 0, fmt.Errorf("stat: %w", err) + } + return fd, info.Size(), nil +} + +func mkCacheFile(url string) (*os.File, error) { + file := filepath.Join(cacheDir, hash256(url)+".data.tmp") + err := os.MkdirAll(cacheDir, 0o755) + if err != nil { + return nil, fmt.Errorf("create cache dir: %w", err) + } + + fd, err := os.Create(file) + if err != nil { + return nil, fmt.Errorf("create cache file: %w", err) + } + return fd, nil +} + // fetchFile will download and open a file with the contents of `url`. func fetchFile(url string) (*os.File, int64, error) { + file, size, err := getCacheFile(url) + if err == nil { + return file, size, nil + } + if err != errMiss { + return nil, 0, fmt.Errorf("getCacheFile: %w", err) + } + resp, err := http.Get(url) if err != nil { return nil, 0, fmt.Errorf("fetch: %w", err) @@ -18,9 +69,21 @@ func fetchFile(url string) (*os.File, int64, error) { return nil, 0, fmt.Errorf("non-200 response: %s", resp.Status) } - fd, err := os.CreateTemp("", "*.zip") - if err != nil { - return nil, 0, fmt.Errorf("create temp file: %w", err) + var fd *os.File + var cacheFailed bool + if cacheDir != "" { + fd, err = mkCacheFile(url) + if err != nil { + log.Println("failed to update cache:", err) + } + } + + if fd == nil { + fd, err = os.CreateTemp("", "*.zip") + if err != nil { + return nil, 0, fmt.Errorf("create temp file: %w", err) + } + cacheFailed = true } n, err := io.Copy(fd, resp.Body) @@ -28,6 +91,15 @@ func fetchFile(url string) (*os.File, int64, error) { fd.Close() return nil, 0, fmt.Errorf("download file '%s': %w", url, err) } + + if !cacheFailed { + err = os.Rename(fd.Name(), filepath.Join(cacheDir, hash256(url)+".data")) + if err != nil { + fd.Close() + return nil, 0, fmt.Errorf("rename cache file: %w", err) + } + } + _, err = fd.Seek(0, 0) if err != nil { fd.Close() diff --git a/devtools/gettool/run.go b/devtools/gettool/main.go similarity index 66% rename from devtools/gettool/run.go rename to devtools/gettool/main.go index 57372fefe7..eeac5af5c2 100644 --- a/devtools/gettool/run.go +++ b/devtools/gettool/main.go @@ -3,15 +3,30 @@ package main import ( "flag" "log" + "os" + "path/filepath" ) +var cacheDir string + func main() { tool := flag.String("t", "", "Tool to fetch.") version := flag.String("v", "", "Version of the tool to fetch.") output := flag.String("o", "", "Output file/dir.") + flag.StringVar(&cacheDir, "c", os.Getenv("GETTOOL_CACHE"), "Cache dir.") flag.Parse() log.SetFlags(log.Lshortfile) + if cacheDir != "" { + // do nothing, already set + } else if base := os.Getenv("XDG_CACHE_HOME"); base != "" { + // use the XDG_CACHE_HOME + cacheDir = filepath.Join(base, "goalert-gettool") + } else if home := os.Getenv("HOME"); home != "" { + // use the HOME dir + cacheDir = filepath.Join(home, ".cache", "goalert-gettool") + } + if *tool == "" { log.Fatal("-t flag is required") } diff --git a/devtools/scripts/ci-cache.sh b/devtools/scripts/ci-cache.sh new file mode 100755 index 0000000000..1b768e84f0 --- /dev/null +++ b/devtools/scripts/ci-cache.sh @@ -0,0 +1,103 @@ +#!/bin/bash +set -e + +if [ "$CI" != "1" ]; then + echo "This script is only intended to be run in CI." + exit 0 +fi + +GLOBAL_CACHE_PATH=/var/cache/arc + +ACTION="$1" +if [ "$ACTION" != "restore" ] && [ "$ACTION" != "save" ]; then + echo "Usage: $0 " + exit 1 +fi + +KEY="goalert-cache-$(uname -s)-$(uname -m)-$(cat devtools/scripts/ci-cache.sh go.mod go.sum package.json bun.lock *.version | sha256sum | awk '{print $1}')" + +if [ -z "$GLOBAL_CACHE_PATH" ]; then + echo "GLOBAL_CACHE_PATH is not set. Skipping cache management." + exit 0 +fi + +echo "Cache key: $KEY" + +path_key() { + echo "$1" | sha256sum | awk '{print $1}' +} +resolve_path() { + echo "$GLOBAL_CACHE_PATH/$KEY/$(path_key "$1")" +} + +resolve_tmp_path() { + local hash=$(echo "$1" | sha256sum | awk '{print $1}') + echo "$1/$hash" +} + +PATHS=( + "$(go env GOMODCACHE)" + "$(go env GOCACHE)" + "$HOME/.cache/goalert-gettool" + "$HOME/.cache/Cypress" + "$HOME/.cache/ms-playwright" + "$HOME/.bun/install/cache" +) + +if [ "$ACTION" == "restore" ]; then + echo "Restoring cache..." + for path in "${PATHS[@]}"; do + SRC=$(resolve_path "$path") + if [ -d "$SRC" ]; then + echo "Restoring $path" + mkdir -p "$path" + cp -r "$SRC/"* "$path/" + else + echo "No cache found for $path" + fi + done + if [ -d "$GLOBAL_CACHE_PATH/$KEY" ]; then + touch "$GLOBAL_CACHE_PATH/$KEY/.last_used" + else + echo "No cache found for $KEY" + fi +else + # Skip saving if the cache already exists + if [ -d "$GLOBAL_CACHE_PATH/$KEY" ]; then + echo "Cache already exists. Skipping save." + exit 0 + fi + echo "Saving cache..." + TMP_DIR=$(mktemp -d -p "$GLOBAL_CACHE_PATH") + if [ ! -d "$TMP_DIR" ]; then + echo "Failed to create temporary directory for cache." + exit 1 + fi + for path in "${PATHS[@]}"; do + if [ -d "$path" ]; then + echo "Saving $path" + DST="$TMP_DIR/$(path_key "$path")" + mkdir "$DST" + cp -r "$path/"* "$DST/" + else + echo "No cache to save for $path" + fi + done + touch "$TMP_DIR/.last_used" + mv "$TMP_DIR" "$GLOBAL_CACHE_PATH/$KEY" +fi + +MONTH_AGO=$(date -d '1 month ago' +%s) +if [ -z "$(ls -A "$GLOBAL_CACHE_PATH")" ]; then + echo "GLOBAL_CACHE_PATH is empty. Skipping old cache cleanup." +else + for last_used in "$GLOBAL_CACHE_PATH"/*/.last_used; do + TIME=$(stat -c %Y "$last_used") + if [ "$TIME" -lt "$MONTH_AGO" ]; then + echo "Deleting old cache $(dirname "$last_used")" + rm -rf "$(dirname "$last_used")" + fi + done +fi + +echo "Cache $ACTION complete." diff --git a/go.mod b/go.mod index dca8d01a23..1238efa5d3 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 github.com/pelletier/go-toml/v2 v2.2.4 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.22.0 + github.com/prometheus/client_golang v1.21.1 github.com/riverqueue/river v0.20.2 github.com/riverqueue/river/riverdriver/riverdatabasesql v0.20.2 github.com/riverqueue/river/riverdriver/riverpgxv5 v0.20.2 @@ -188,6 +188,7 @@ require ( github.com/kffl/speedbump v1.1.0 // indirect github.com/kisielk/errcheck v1.9.0 // indirect github.com/kkHAIKE/contextcheck v1.1.6 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/kulti/thelper v0.6.3 // indirect github.com/kunwardeep/paralleltest v1.0.10 // indirect github.com/lasiar/canonicalheader v1.1.2 // indirect @@ -308,10 +309,10 @@ require ( golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.38.0 // indirect + golang.org/x/net v0.39.0 // indirect golang.org/x/sync v0.13.0 // indirect golang.org/x/text v0.24.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/tools v0.32.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect diff --git a/go.sum b/go.sum index ff0b8643c2..970fc46943 100644 --- a/go.sum +++ b/go.sum @@ -438,8 +438,8 @@ github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE= github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -581,8 +581,8 @@ github.com/polyfloyd/go-errorlint v1.7.1 h1:RyLVXIbosq1gBdk/pChWA8zWYLsq9UEw7a1L github.com/polyfloyd/go-errorlint v1.7.1/go.mod h1:aXjNb1x2TNhoLsk26iv1yl7a+zTnXPhwEMtEXukiLR8= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= +github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= @@ -905,8 +905,8 @@ golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1020,8 +1020,8 @@ golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=