From d59c6c3b92199e6b76ab8749c3b0ae8251333e95 Mon Sep 17 00:00:00 2001 From: anon-d Date: Mon, 27 Apr 2026 23:01:16 +0300 Subject: [PATCH 1/2] added check user rule methods --- config.example.json | 3 +- internal/app/app.go | 37 ++++++++++++-------- internal/config/flag/config.go | 7 ++++ internal/handler/example_test.go | 4 +++ internal/handler/handler.go | 23 ++++++++++++ internal/handler/handler_test.go | 12 +++++++ internal/middleware/middleware.go | 26 ++++++++++++++ internal/repository/db/postgres/postgres.go | 19 ++++++++++ internal/repository/models.go | 6 ++++ internal/repository/storage.go | 27 ++++++++++++++ internal/service/service.go | 8 +++++ profiles/base.pprof | Bin 1649 -> 1679 bytes 12 files changed, 157 insertions(+), 15 deletions(-) diff --git a/config.example.json b/config.example.json index 8017685..efcacc4 100644 --- a/config.example.json +++ b/config.example.json @@ -11,5 +11,6 @@ "audit_url": "", "enable_https": false, "cert_file": "cert.pem", - "key_file": "key.pem" + "key_file": "key.pem", + "trusted_subnet": "" } diff --git a/internal/app/app.go b/internal/app/app.go index ca16d4a..3936fab 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -26,13 +26,14 @@ import ( // App — корневая структура приложения, содержащая HTTP-сервер, роутер и фоновые воркеры. type App struct { - server *http.Server - router *gin.Engine - urlHandler *handler.URLHandler - deleteWorker *worker.DeleteWorker - enableTLS bool - certFile string - keyFile string + server *http.Server + router *gin.Engine + urlHandler *handler.URLHandler + deleteWorker *worker.DeleteWorker + enableTLS bool + certFile string + keyFile string + trustedSubnet string } // New создаёт и настраивает новое приложение: @@ -151,13 +152,14 @@ func New() (*App, error) { } return &App{ - server: httpServer, - router: router, - urlHandler: urlHandler, - deleteWorker: deleteWorker, - enableTLS: cfg.Enable_HTTPS, - certFile: cfg.CertFile, - keyFile: cfg.KeyFile, + server: httpServer, + router: router, + urlHandler: urlHandler, + deleteWorker: deleteWorker, + enableTLS: cfg.Enable_HTTPS, + certFile: cfg.CertFile, + keyFile: cfg.KeyFile, + trustedSubnet: cfg.TrustedSubnet, }, nil } @@ -200,6 +202,13 @@ func (a *App) SetupRoutes() { a.router.POST("/api/shorten/batch", a.urlHandler.BatchShorten) a.router.GET("/api/user/urls", a.urlHandler.GetUserURLs) a.router.DELETE("/api/user/urls", a.urlHandler.DeleteURLs) + + // Внутренние эндпоинты: доступ только из доверенной подсети. + internalAPI := a.router.Group("/api/internal", middleware.TrustedSubnet(a.trustedSubnet)) + { + internalAPI.GET("/stats", a.urlHandler.GetStats) + } + a.router.NoMethod(a.urlHandler.NotAllowed) a.router.NoRoute(a.urlHandler.NotFound) diff --git a/internal/config/flag/config.go b/internal/config/flag/config.go index 5604066..2707ef4 100644 --- a/internal/config/flag/config.go +++ b/internal/config/flag/config.go @@ -28,6 +28,7 @@ type ServerConfig struct { CertFile string `env:"CERT_FILE"` KeyFile string `env:"KEY_FILE"` ConfigJSON string `env:"CONFIG"` + TrustedSubnet string `env:"TRUSTED_SUBNET"` } // JSONFileConfig — структура JSON-файла конфигурации. @@ -37,6 +38,7 @@ type JSONFileConfig struct { FileStoragePath string `json:"file_storage_path"` DatabaseDSN string `json:"database_dsn"` EnableHTTPS *bool `json:"enable_https"` + TrustedSubnet string `json:"trusted_subnet"` } var ( @@ -60,6 +62,7 @@ func initFlags() { fs.String("cert", "cert.pem", "path to TLS certificate file") fs.String("key", "key.pem", "path to TLS private key file") fs.StringP("config", "c", "", "path to JSON config file") + fs.StringP("trusted-subnet", "t", "", "trusted subnet in CIDR notation") // Ошибки разбора флагов (например, неизвестные флаги go test) намеренно игнорируются. _ = fs.Parse(os.Args[1:]) } @@ -76,6 +79,7 @@ func loadJSONConfig(path string) (*JSONFileConfig, error) { BaseURL: v.GetString("base_url"), FileStoragePath: v.GetString("file_storage_path"), DatabaseDSN: v.GetString("database_dsn"), + TrustedSubnet: v.GetString("trusted_subnet"), } if v.IsSet("enable_https") { b := v.GetBool("enable_https") @@ -105,6 +109,7 @@ func NewServerConfig() *ServerConfig { v.SetDefault("enable_https", false) v.SetDefault("cert_file", "cert.pem") v.SetDefault("key_file", "key.pem") + v.SetDefault("trusted_subnet", "") // Привязка флагов командной строки к ключам Viper _ = v.BindPFlag("server_address", fs.Lookup("a")) @@ -121,6 +126,7 @@ func NewServerConfig() *ServerConfig { _ = v.BindPFlag("cert_file", fs.Lookup("cert")) _ = v.BindPFlag("key_file", fs.Lookup("key")) _ = v.BindPFlag("config", fs.Lookup("config")) + _ = v.BindPFlag("trusted_subnet", fs.Lookup("trusted-subnet")) // Переменные окружения: ключ автоматически преобразуется в верхний регистр // (server_address → SERVER_ADDRESS, base_url → BASE_URL и т.д.) @@ -148,6 +154,7 @@ func NewServerConfig() *ServerConfig { CertFile: v.GetString("cert_file"), KeyFile: v.GetString("key_file"), ConfigJSON: configPath, + TrustedSubnet: v.GetString("trusted_subnet"), } } diff --git a/internal/handler/example_test.go b/internal/handler/example_test.go index 086bb3e..45e3c25 100644 --- a/internal/handler/example_test.go +++ b/internal/handler/example_test.go @@ -11,6 +11,7 @@ import ( "github.com/anon-d/urlshortener/internal/handler" "github.com/anon-d/urlshortener/internal/model" + "github.com/anon-d/urlshortener/internal/repository" "github.com/anon-d/urlshortener/internal/service" "github.com/anon-d/urlshortener/internal/worker" "github.com/gin-gonic/gin" @@ -54,6 +55,9 @@ func (m *mockStorageEx) BatchMarkAsDeleted(ctx context.Context, r []worker.Delet return nil } func (m *mockStorageEx) Ping(ctx context.Context) error { return nil } +func (m *mockStorageEx) GetStats(ctx context.Context) (repository.Stats, error) { + return repository.Stats{}, nil +} func init() { gin.SetMode(gin.TestMode) diff --git a/internal/handler/handler.go b/internal/handler/handler.go index e3c15a8..6bc3c85 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -50,6 +50,13 @@ type UserURLResponse struct { OriginalURL string `json:"original_url"` } +// StatsResponse — тело ответа для GET /api/internal/stats. +// Содержит количество сокращённых URL и пользователей в сервисе. +type StatsResponse struct { + URLs int `json:"urls"` + Users int `json:"users"` +} + // URLHandler содержит зависимости и методы для обработки HTTP-запросов // к сервису сокращения URL. type URLHandler struct { @@ -354,6 +361,22 @@ func (u *URLHandler) DeleteURLs(c *gin.Context) { c.Status(http.StatusAccepted) } +// GetStats возвращает сводную статистику сервиса: +// количество сокращённых URL и количество пользователей. +// Доступ ограничивается доверенной подсетью (см. middleware TrustedSubnet). +func (u *URLHandler) GetStats(c *gin.Context) { + stats, err := u.Service.GetStats(c) + if err != nil { + u.logger.Errorw("failed to get stats", "error", err) + c.String(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + return + } + c.JSON(http.StatusOK, StatsResponse{ + URLs: stats.URLs, + Users: stats.Users, + }) +} + // publishAudit отправляет событие аудита во все зарегистрированные приёмники. func (u *URLHandler) publishAudit(action, userID, originalURL string) { if u.audit == nil { diff --git a/internal/handler/handler_test.go b/internal/handler/handler_test.go index f033230..319d120 100644 --- a/internal/handler/handler_test.go +++ b/internal/handler/handler_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/anon-d/urlshortener/internal/model" + "github.com/anon-d/urlshortener/internal/repository" "github.com/anon-d/urlshortener/internal/service" "github.com/anon-d/urlshortener/internal/worker" "github.com/gin-gonic/gin" @@ -120,6 +121,13 @@ func (m *mockStorage) Ping(ctx context.Context) error { return nil } +func (m *mockStorage) GetStats(ctx context.Context) (repository.Stats, error) { + if m.shouldFail { + return repository.Stats{}, errors.New("get stats error") + } + return repository.Stats{URLs: 2, Users: 1}, nil +} + func TestPostURL_Success(t *testing.T) { testLogger := zap.NewNop().Sugar() @@ -747,3 +755,7 @@ func (m *mockStorageEmpty) UpdateBatch(ctx context.Context, urls []string) error func (m *mockStorageEmpty) Ping(ctx context.Context) error { return nil } + +func (m *mockStorageEmpty) GetStats(ctx context.Context) (repository.Stats, error) { + return repository.Stats{}, nil +} diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 71ef7ef..8d9916d 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -7,6 +7,7 @@ import ( "crypto/rand" "crypto/sha256" "encoding/base64" + "net" "net/http" "strings" "sync" @@ -208,6 +209,31 @@ func signValue(value string, secretKey string) string { return value + "." + signature } +// TrustedSubnet проверяет, что IP-адрес клиента из заголовка X-Real-IP +// входит в доверенную подсеть (CIDR). Если строка подсети пустая или +// невалидна, доступ запрещён для всех запросов (403 Forbidden). +func TrustedSubnet(subnet string) gin.HandlerFunc { + var ipNet *net.IPNet + if subnet != "" { + if _, parsed, err := net.ParseCIDR(subnet); err == nil { + ipNet = parsed + } + } + return func(c *gin.Context) { + if ipNet == nil { + c.AbortWithStatus(http.StatusForbidden) + return + } + realIP := strings.TrimSpace(c.GetHeader("X-Real-IP")) + ip := net.ParseIP(realIP) + if ip == nil || !ipNet.Contains(ip) { + c.AbortWithStatus(http.StatusForbidden) + return + } + c.Next() + } +} + // validateSignedValue проверяет подпись и возвращает оригинальное значение func validateSignedValue(signedValue string, secretKey string) (string, bool) { parts := strings.Split(signedValue, ".") diff --git a/internal/repository/db/postgres/postgres.go b/internal/repository/db/postgres/postgres.go index 5cf5c84..4555d3f 100644 --- a/internal/repository/db/postgres/postgres.go +++ b/internal/repository/db/postgres/postgres.go @@ -236,6 +236,25 @@ func (r *Repository) BatchMarkAsDeleted(ctx context.Context, requests []worker.D return nil } +// GetStats возвращает количество сокращённых URL и уникальных пользователей из БД. +func (r *Repository) GetStats(ctx context.Context) (repository.Stats, error) { + query := ` + SELECT + COUNT(*) FILTER (WHERE COALESCE(is_deleted, false) = false) AS urls, + COUNT(DISTINCT user_id) FILTER (WHERE COALESCE(is_deleted, false) = false AND user_id IS NOT NULL AND user_id <> '') AS users + FROM urls + ` + var stats repository.Stats + err := r.db.QueryRowContext(ctx, query).Scan(&stats.URLs, &stats.Users) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return repository.Stats{}, nil + } + return repository.Stats{}, fmt.Errorf("failed to get stats in GetStats: %w", err) + } + return stats, nil +} + // GetURLByShortURL получает URL по короткой ссылке func (r *Repository) GetURLByShortURL(ctx context.Context, shortURL string) (repository.Data, error) { query := "SELECT id, short_url, original_url, COALESCE(user_id, ''), COALESCE(is_deleted, false) FROM urls WHERE short_url = $1" diff --git a/internal/repository/models.go b/internal/repository/models.go index f59fb48..b607101 100644 --- a/internal/repository/models.go +++ b/internal/repository/models.go @@ -13,3 +13,9 @@ type Data struct { UserID string IsDeleted bool } + +// Stats — сводная статистика сервиса. +type Stats struct { + URLs int + Users int +} diff --git a/internal/repository/storage.go b/internal/repository/storage.go index 55a6304..88a8f55 100644 --- a/internal/repository/storage.go +++ b/internal/repository/storage.go @@ -18,6 +18,7 @@ type Storage interface { GetURLsByUser(ctx context.Context, userID string) ([]model.Data, error) GetURLByShortURL(ctx context.Context, shortURL string) (model.Data, error) BatchMarkAsDeleted(ctx context.Context, requests []worker.DeleteRequest) error + GetStats(ctx context.Context) (Stats, error) Ping(ctx context.Context) error } @@ -118,6 +119,11 @@ func (d *DBAdapter) Ping(ctx context.Context) error { return d.db.Ping(ctx) } +// GetStats возвращает количество сокращённых URL и уникальных пользователей из БД. +func (d *DBAdapter) GetStats(ctx context.Context) (Stats, error) { + return d.db.GetStats(ctx) +} + // LocalAdapter адаптирует Local файловое хранилище к интерфейсу Storage type LocalAdapter struct { local Local @@ -221,6 +227,26 @@ func (l *LocalAdapter) Ping(ctx context.Context) error { return nil } +// GetStats возвращает количество URL и уникальных пользователей в локальном файле. +func (l *LocalAdapter) GetStats(ctx context.Context) (Stats, error) { + data, err := l.local.Load() + if err != nil { + return Stats{}, err + } + users := make(map[string]struct{}) + urls := 0 + for _, item := range data { + if item.IsDeleted { + continue + } + urls++ + if item.UserID != "" { + users[item.UserID] = struct{}{} + } + } + return Stats{URLs: urls, Users: len(users)}, nil +} + // DB — интерфейс работы с реляционной базой данных. type DB interface { InsertURL(ctx context.Context, id, shortURL, originalURL, userID string) error @@ -230,6 +256,7 @@ type DB interface { GetURLsByUser(ctx context.Context, userID string) ([]Data, error) GetURLByShortURL(ctx context.Context, shortURL string) (Data, error) BatchMarkAsDeleted(ctx context.Context, requests []worker.DeleteRequest) error + GetStats(ctx context.Context) (Stats, error) Ping(ctx context.Context) error } diff --git a/internal/service/service.go b/internal/service/service.go index e6934b4..6c1d761 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -110,6 +110,14 @@ func (s *Service) GetURLsByUser(ctx context.Context, userID string) ([]model.Dat return s.Storage.GetURLsByUser(ctx, userID) } +// GetStats возвращает количество сокращённых URL и уникальных пользователей в сервисе. +func (s *Service) GetStats(ctx context.Context) (repository.Stats, error) { + if s.Storage == nil { + return repository.Stats{}, errors.New("storage is not available") + } + return s.Storage.GetStats(ctx) +} + // GetURLByShortURL получает полные данные URL по короткой ссылке func (s *Service) GetURLByShortURL(ctx context.Context, shortURL string) (model.Data, error) { // Сначала проверяем кэш diff --git a/profiles/base.pprof b/profiles/base.pprof index a7313f57c54e97f8deef19dd8ae65641b06d4199..c72c86af33a6b9bae12843b0ee45d0ba152ad9bf 100644 GIT binary patch literal 1679 zcmV;A25|WwiwFP!00004|Fl$HY#h}U_N;g9>;3bY@$UMpug8f3u|rWrlX21s@noDNAeM}i zE^+RLn6459Klt05CqYjT1um2}~$vlj$^7ra;$co+0yFHIYd7Il?$n=d@|1n3Epf~OVW7o37T z=4rt=spvq`_|6*~kCQZf29#2gq@f@CDH>zCN-}uquUAF-4E&>4q|ZPRi*&&FT~^C! z-B0cLaYQ>hlAWn^W*|;tct8h|#m%2e&Mf2wXBKwg4vI!jS4l6f{e+`j(Q^tYT0!f| z9Di=4qazv5_NFrfU0nOGVmc7OW9PXm7dHb6Pxzq{JwiJ=2#|~{QoCimN>qI7c^=PG zJ;_t$weCnc`j}9xrdtfW4;p^w@jr<|dtpOfQ!fnSAl+%at>`N0!?)HY6Tn%C0}SC1 zEf}wGJcsXHyXI3oQ@=){DE6?poP^ewYh4d-NgiMQ`$dsUg^wkd3d1-|sd1Ha74XWY z#5H(d;u?(L2&KkvIo^-I{ENi<;F!ewKu4WYV~yj8YuB{C%C!`^miV`XmhOC^AKPk( zu9702+IUx}$-yU*EeE4GO2>?k`2!iikAB80=RLR^6fMuml*a^FXK_Gwo$5e#;FZ(T zF%MrBUC+Zfj?;p1j}BxIk6n9b)=f>{F?fB`cjCX#NuD(9mI)t`;dnyKM?V;7&^wJs5*ljB%cb>*il1bh#zu58 zBZqXAjN*IWmWv1ByK=G+CUAnLjZd<=O2+W&qY^K|AG3nF2qi4hv~iK+<9On-Tnqzn zTq+xYUAT+pjH5A)-mn-_?Zq%UHZF$-zVQ3EH%~nC{)x*tpb-V4==&!Dqv!`ug7K++ ziq&Byf`;udD=~j0WPz2jYcC+Sn^ugwn-4xt&v;vG<;96 zq}w4bJ6zu)2twNr>sF?X9=4ZR!^*cmeHO4VXjEOd>Q=T22%IIhuu@}NJPq4lp1Y2x z?%1`OHGXSdc2J+Q{rdD&Ik3J|sfJ4{^JT}Yo3`t@cP*GJel1w?{E)fKH>+;QeAlj- z0rOX@4l5g_Std>s-z7yWq127ukXrdaLGs7A%Ke!)(6X z{6+Ij^R?#rjfdu%&um1^_2&7Fqs`Zw=Qq}xKR5qpTC<(L;#v3HuH=I(2oLz)VztJ; z%Icnf#9I14Ds41;Z*eYULCD4S@}$7Mqfx4R&T^~D z&)d!t+iDXFtrBw`Z=vc|%y~Ot(^F-mbZ^TtQ5Gk5(3=f?{?vADS9nu&o224dpT1q8 z_wi?ChwO{{tz2-#b;?HRfaldF%8jZ!Tk{TEL)+7HKeOR^HJ%ap*Y#01S?*(@m0GNa z<=KW`b;HH~Mz~lHO@VU4dvCXd?IXZn|4@5yjZ)y)tIVx1e>Sv3Hc_s+)zC7w*VT$J z>lLRR!Ubt9dwIUnHe0=ITV1S;-8UO1xAuYbu)e`7T(v`HuG+r2=+zeZY&I56|C>wm zGn3PMci%lTvv1m3o}R8-Q-QT_a&mIA;+hA|ig(xiO0~9NP4BVxPVJeV+GozsOzt%U Z=C4*ARt{MZ{wDwc|Nr{wEs9(V003boOzQvu literal 1649 zcmV-%29Eh3iwFP!00004|Fl$Hj2lNCzxCIRz5e*`-q{}eoMf{3=)`GueVgNQfu}Y} zY553IE`32l$KKx9w~f7P?XE8kPi_-XsVWjF1)-!8mkL4JlSm|*QbLLl5^Vvg^{GXD z02Cys0!WqmfPe>t(9GIR)ypB}p)cO`|M&6#|7K?A%Lybw?5q7g$s|x55}5=_LM)R& zDe*oIF%6J}&;07mRnRzh2BdlhNq{gSY1+`rD2CsC;w^#3;INiNarj1BkYbR*8Iq=l z>N1LhjAfFgXVcm)6vsdQ**ij!fP$7pN%(q9OQIC?`C}Z;#)LirIh-STnysOe3QFKx-{;Orgy6Y2NOc(@n8cH$NH4~WOQJNKk825(fro*!(on)BQl#hN^$f~D8J7vBJ2eHR@Y{cTZgq`9(}qkc=;_|3M_k!w1nsdDx1#l5O;TK2a9GeuG=_i4Ka1 zP`@vo?~9S-Y^s_-F-$ZCM9z=keNAZj?j-KZBE14T@D5U=4`_fCeETO6 zngJ6eby6L_xc`dqiGvck9Mb@)_|oNwr@-f`==3JgqKSKFTx(Yhfw-LNmEc6|Le5~FTr=Qz)MiabwcUy%bJ2PzV!01 zg?|~=DgrOVUc8r3`b@XKui$8bNspHO;zo2H;De*Fg#UV+<0;1$@1 z_Yq2e$oF;%zx%DIe-*wH9rG&aSSOTzkk=Gc!?&J^@G1CHUg)R5zy_i8HI8q=7k>8} z(Z2>y^UqgCHE7@lnWj(1RHA13vHnv;{AA>b$zrLDE7hsmmgv00!&ma`AM?0yAf|+n&APEk4HVFffz0*KL?{(%w-*H>!PNNt2M%%Nkw$bv8z_*Pc zqK}?>%QGt~KJEp&$HluhioL*S zI&HSV@r9o2JkABBq2M8>&5n9)ucbG8u03sT-&{YEf*0sc%k_BnBaa+Caop547j?ZL zYRFZ5f)5*-vKNTJykgciwcyjHy+A+YGb?1~11%?9?k(!J*D);DbN4M7J-;0+dw$4V z<{OS1GT*h@M!@_P$7cEo7KF!quj#be7g@*iPn*leqXF{+!*Z8=_E?iGd7-yr9A?4E z(CZq5tAkeuFAc5_Zmd7CF!P?i_PKk_NbB=a+CBRGvRH`+tx_DzJyy3^Xcca(G)FYFYXMXx#Q+<}X%(p`J$YJxyy=pgZBHusodv54-nBEb0 zRm(PaY)mlZ7!ik0t_(}M%T7f%nwcM!gi+^ipZbttbbU{_WQPUr(z#<0gq9z6%-je) zWu0VQvpD+evw(#`*Ku8bJAN1-u$S3Vug%QDh}aQE(;qK1?sSS6=B~{nTR~@d4F={{ z_>ow#LT0R3zR~pBOUw_9Zqx7|UtT=WnEllB!2<{8X3dkcvmJ9LFy|VLMx*5#CybW2 vZ?WgJm!{`t_BRgBO&{EEEY37$jDYzoj?MIt1>ye!00960Pi@GO`3e94=DSKG From 204f932d6203c0e0e597f05f2df822c033b173d2 Mon Sep 17 00:00:00 2001 From: anon-d Date: Mon, 27 Apr 2026 23:08:49 +0300 Subject: [PATCH 2/2] update tag in tests --- .github/workflows/statictest.yml | 51 ++++++++++++++++---------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/.github/workflows/statictest.yml b/.github/workflows/statictest.yml index 935f9ad..bdc4547 100644 --- a/.github/workflows/statictest.yml +++ b/.github/workflows/statictest.yml @@ -1,33 +1,34 @@ name: go vet test on: - pull_request: - push: - branches: - - main + pull_request: + push: + branches: + - main jobs: - statictest: - runs-on: ubuntu-latest - container: golang:1.26 - steps: - - name: Checkout code - uses: actions/checkout@v2 + statictest: + runs-on: ubuntu-latest + container: golang:1.26 + steps: + - name: Checkout code + uses: actions/checkout@v2 - - name: Download statictest binary - uses: robinraju/release-downloader@v1.8 - with: - repository: Yandex-Practicum/go-autotests - latest: true - fileName: statictest - out-file-path: .tools - token: ${{ secrets.GITHUB_TOKEN }} + - name: Download statictest binary + uses: robinraju/release-downloader@v1.8 + with: + repository: Yandex-Practicum/go-autotests + latest: false + tag: v0.13.4 + fileName: statictest + out-file-path: .tools + token: ${{ secrets.GITHUB_TOKEN }} - - name: Setup autotest binary - run: | - chmod -R +x $GITHUB_WORKSPACE/.tools/statictest - mv $GITHUB_WORKSPACE/.tools/statictest /usr/local/bin/statictest + - name: Setup autotest binary + run: | + chmod -R +x $GITHUB_WORKSPACE/.tools/statictest + mv $GITHUB_WORKSPACE/.tools/statictest /usr/local/bin/statictest - - name: Run statictest - run: | - go vet -vettool=$(which statictest) ./... + - name: Run statictest + run: | + go vet -vettool=$(which statictest) ./...