From 3af0d009a80ea39907fa781bbd0c6466d46f260e Mon Sep 17 00:00:00 2001 From: wozz Date: Thu, 21 Dec 2023 08:37:36 -0500 Subject: [PATCH 1/5] singleflight go commands (#1877) * singleflight go commands * Apply suggestions from code review Co-authored-by: Brendan Le Glaunec --------- Co-authored-by: michael-wozniak Co-authored-by: Brendan Le Glaunec --- pkg/module/go_get_fetcher.go | 99 ++++++++++++++++++---------------- pkg/module/go_vcs_lister.go | 100 +++++++++++++++++++++-------------- 2 files changed, 113 insertions(+), 86 deletions(-) diff --git a/pkg/module/go_get_fetcher.go b/pkg/module/go_get_fetcher.go index e43d3529d..c9e59d292 100644 --- a/pkg/module/go_get_fetcher.go +++ b/pkg/module/go_get_fetcher.go @@ -14,6 +14,7 @@ import ( "github.com/gomods/athens/pkg/observ" "github.com/gomods/athens/pkg/storage" "github.com/spf13/afero" + "golang.org/x/sync/singleflight" ) type goGetFetcher struct { @@ -21,6 +22,7 @@ type goGetFetcher struct { goBinaryName string envVars []string gogetDir string + sfg *singleflight.Group } type goModule struct { @@ -46,6 +48,7 @@ func NewGoGetFetcher(goBinaryName, gogetDir string, envVars []string, fs afero.F goBinaryName: goBinaryName, envVars: envVars, gogetDir: gogetDir, + sfg: &singleflight.Group{}, }, nil } @@ -56,57 +59,63 @@ func (g *goGetFetcher) Fetch(ctx context.Context, mod, ver string) (*storage.Ver ctx, span := observ.StartSpan(ctx, op.String()) defer span.End() - // setup the GOPATH - goPathRoot, err := afero.TempDir(g.fs, g.gogetDir, "athens") - if err != nil { - return nil, errors.E(op, err) - } - sourcePath := filepath.Join(goPathRoot, "src") - modPath := filepath.Join(sourcePath, getRepoDirName(mod, ver)) - if err := g.fs.MkdirAll(modPath, os.ModeDir|os.ModePerm); err != nil { - _ = clearFiles(g.fs, goPathRoot) - return nil, errors.E(op, err) - } + resp, err, _ := g.sfg.Do(mod+"###"+ver, func() (any, error) { + // setup the GOPATH + goPathRoot, err := afero.TempDir(g.fs, g.gogetDir, "athens") + if err != nil { + return nil, errors.E(op, err) + } + sourcePath := filepath.Join(goPathRoot, "src") + modPath := filepath.Join(sourcePath, getRepoDirName(mod, ver)) + if err := g.fs.MkdirAll(modPath, os.ModeDir|os.ModePerm); err != nil { + _ = clearFiles(g.fs, goPathRoot) + return nil, errors.E(op, err) + } - m, err := downloadModule( - ctx, - g.goBinaryName, - g.envVars, - goPathRoot, - modPath, - mod, - ver, - ) - if err != nil { - _ = clearFiles(g.fs, goPathRoot) - return nil, errors.E(op, err) - } + m, err := downloadModule( + ctx, + g.goBinaryName, + g.envVars, + goPathRoot, + modPath, + mod, + ver, + ) + if err != nil { + _ = clearFiles(g.fs, goPathRoot) + return nil, errors.E(op, err) + } - var storageVer storage.Version - storageVer.Semver = m.Version - info, err := afero.ReadFile(g.fs, m.Info) - if err != nil { - return nil, errors.E(op, err) - } - storageVer.Info = info + var storageVer storage.Version + storageVer.Semver = m.Version + info, err := afero.ReadFile(g.fs, m.Info) + if err != nil { + return nil, errors.E(op, err) + } + storageVer.Info = info - gomod, err := afero.ReadFile(g.fs, m.GoMod) - if err != nil { - return nil, errors.E(op, err) - } - storageVer.Mod = gomod + gomod, err := afero.ReadFile(g.fs, m.GoMod) + if err != nil { + return nil, errors.E(op, err) + } + storageVer.Mod = gomod - zip, err := g.fs.Open(m.Zip) + zip, err := g.fs.Open(m.Zip) + if err != nil { + return nil, errors.E(op, err) + } + // note: don't close zip here so that the caller can read directly from disk. + // + // if we close, then the caller will panic, and the alternative to make this work is + // that we read into memory and return an io.ReadCloser that reads out of memory + storageVer.Zip = &zipReadCloser{zip, g.fs, goPathRoot} + + return &storageVer, nil + }) if err != nil { - return nil, errors.E(op, err) + return nil, err } - // note: don't close zip here so that the caller can read directly from disk. - // - // if we close, then the caller will panic, and the alternative to make this work is - // that we read into memory and return an io.ReadCloser that reads out of memory - storageVer.Zip = &zipReadCloser{zip, g.fs, goPathRoot} - - return &storageVer, nil + return resp.(*storage.Version), nil } // given a filesystem, gopath, repository root, module and version, runs 'go mod download -json' diff --git a/pkg/module/go_vcs_lister.go b/pkg/module/go_vcs_lister.go index a6c1c611a..86abcc38c 100644 --- a/pkg/module/go_vcs_lister.go +++ b/pkg/module/go_vcs_lister.go @@ -13,6 +13,7 @@ import ( "github.com/gomods/athens/pkg/observ" "github.com/gomods/athens/pkg/storage" "github.com/spf13/afero" + "golang.org/x/sync/singleflight" ) type listResp struct { @@ -26,6 +27,7 @@ type vcsLister struct { goBinPath string env []string fs afero.Fs + sfg *singleflight.Group } // NewVCSLister creates an UpstreamLister which uses VCS to fetch a list of available versions. @@ -34,58 +36,74 @@ func NewVCSLister(goBinPath string, env []string, fs afero.Fs) UpstreamLister { goBinPath: goBinPath, env: env, fs: fs, + sfg: &singleflight.Group{}, } } +type listSFResp struct { + rev *storage.RevInfo + versions []string +} + func (l *vcsLister) List(ctx context.Context, module string) (*storage.RevInfo, []string, error) { const op errors.Op = "vcsLister.List" _, span := observ.StartSpan(ctx, op.String()) defer span.End() - tmpDir, err := afero.TempDir(l.fs, "", "go-list") - if err != nil { - return nil, nil, errors.E(op, err) - } - defer func() { _ = l.fs.RemoveAll(tmpDir) }() + sfResp, err, _ := l.sfg.Do(module, func() (any, error) { + tmpDir, err := afero.TempDir(l.fs, "", "go-list") + if err != nil { + return nil, errors.E(op, err) + } + defer func() { _ = l.fs.RemoveAll(tmpDir) }() - cmd := exec.Command( - l.goBinPath, - "list", "-m", "-versions", "-json", - config.FmtModVer(module, "latest"), - ) - cmd.Dir = tmpDir - stdout := &bytes.Buffer{} - stderr := &bytes.Buffer{} - cmd.Stdout = stdout - cmd.Stderr = stderr + cmd := exec.Command( + l.goBinPath, + "list", "-m", "-versions", "-json", + config.FmtModVer(module, "latest"), + ) + cmd.Dir = tmpDir + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + cmd.Stdout = stdout + cmd.Stderr = stderr - gopath, err := afero.TempDir(l.fs, "", "athens") - if err != nil { - return nil, nil, errors.E(op, err) - } - defer func() { _ = clearFiles(l.fs, gopath) }() - cmd.Env = prepareEnv(gopath, l.env) + gopath, err := afero.TempDir(l.fs, "", "athens") + if err != nil { + return nil, errors.E(op, err) + } + defer func() { _ = clearFiles(l.fs, gopath) }() + cmd.Env = prepareEnv(gopath, l.env) - err = cmd.Run() - if err != nil { - err = fmt.Errorf("%w: %s", err, stderr) - // as of now, we can't recognize between a true NotFound - // and an unexpected error, so we choose the more - // hopeful path of NotFound. This way the Go command - // will not log en error and we still get to log - // what happened here if someone wants to dig in more. - // Once, https://github.com/golang/go/issues/30134 is - // resolved, we can hopefully differentiate. - return nil, nil, errors.E(op, err, errors.KindNotFound) - } + err = cmd.Run() + if err != nil { + err = fmt.Errorf("%w: %s", err, stderr) + // as of now, we can't recognize between a true NotFound + // and an unexpected error, so we choose the more + // hopeful path of NotFound. This way the Go command + // will not log en error and we still get to log + // what happened here if someone wants to dig in more. + // Once, https://github.com/golang/go/issues/30134 is + // resolved, we can hopefully differentiate. + return nil, errors.E(op, err, errors.KindNotFound) + } - var lr listResp - err = json.NewDecoder(stdout).Decode(&lr) + var lr listResp + err = json.NewDecoder(stdout).Decode(&lr) + if err != nil { + return nil, errors.E(op, err) + } + rev := storage.RevInfo{ + Time: lr.Time, + Version: lr.Version, + } + return listSFResp{ + rev: &rev, + versions: lr.Versions, + }, nil + }) if err != nil { - return nil, nil, errors.E(op, err) - } - rev := storage.RevInfo{ - Time: lr.Time, - Version: lr.Version, + return nil, nil, err } - return &rev, lr.Versions, nil + ret := sfResp.(listSFResp) + return ret.rev, ret.versions, nil } From 43d56f07f67ce30051de1e5345ff8a5a5e3078b7 Mon Sep 17 00:00:00 2001 From: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> Date: Wed, 3 Jan 2024 16:16:08 +0100 Subject: [PATCH 2/5] fix: arm64 build (#1911) --- cmd/proxy/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/proxy/Dockerfile b/cmd/proxy/Dockerfile index 00e0671f5..7a33221cc 100644 --- a/cmd/proxy/Dockerfile +++ b/cmd/proxy/Dockerfile @@ -8,7 +8,7 @@ ARG GOLANG_VERSION=1.20 ARG ALPINE_VERSION=3.17 -FROM --platform=$BUILDPLATFORM golang:${GOLANG_VERSION}-alpine AS builder +FROM golang:${GOLANG_VERSION}-alpine AS builder ARG VERSION="unset" \ TARGETARCH From 7284004d0529c13fc52f0309a47689e9b48cff97 Mon Sep 17 00:00:00 2001 From: Mike Seplowitz Date: Thu, 4 Jan 2024 05:11:29 -0500 Subject: [PATCH 3/5] Set up and use logrus logger in main (#1819) * Set up and use logrus logger in main Also return errors more consistently from other functions. * Updated wording styles * Prefer human-readable descriptions to method names * Wrapped errors use gerund forms, e.g. "doing x: %w" * Log traces start with a capital letter * Fix style on standard log failure cases --------- Co-authored-by: Manu Gupta --- cmd/proxy/actions/app.go | 50 ++++++++++++++------------------------- cmd/proxy/actions/auth.go | 20 +++++++++------- cmd/proxy/main.go | 40 ++++++++++++++++++++----------- 3 files changed, 55 insertions(+), 55 deletions(-) diff --git a/cmd/proxy/actions/app.go b/cmd/proxy/actions/app.go index e37022469..74df44abf 100644 --- a/cmd/proxy/actions/app.go +++ b/cmd/proxy/actions/app.go @@ -3,7 +3,6 @@ package actions import ( "fmt" "net/http" - "os" "github.com/gomods/athens/pkg/config" "github.com/gomods/athens/pkg/log" @@ -11,7 +10,6 @@ import ( "github.com/gomods/athens/pkg/module" "github.com/gomods/athens/pkg/observ" "github.com/gorilla/mux" - "github.com/sirupsen/logrus" "github.com/unrolled/secure" "go.opencensus.io/plugin/ochttp" ) @@ -22,38 +20,33 @@ const Service = "proxy" // App is where all routes and middleware for the proxy // should be defined. This is the nerve center of your // application. -func App(conf *config.Config) (http.Handler, error) { - // ENV is used to help switch settings based on where the - // application is being run. Default is "development". - ENV := conf.GoEnv - +func App(logger *log.Logger, conf *config.Config) (http.Handler, error) { if conf.GithubToken != "" { if conf.NETRCPath != "" { - fmt.Println("Cannot provide both GithubToken and NETRCPath. Only provide one.") - os.Exit(1) + return nil, fmt.Errorf("cannot provide both GithubToken and NETRCPath") } - netrcFromToken(conf.GithubToken) + if err := netrcFromToken(conf.GithubToken); err != nil { + return nil, fmt.Errorf("creating netrc from token: %w", err) + } } // mount .netrc to home dir // to have access to private repos. - initializeAuthFile(conf.NETRCPath) + if err := initializeAuthFile(conf.NETRCPath); err != nil { + return nil, fmt.Errorf("initializing auth file from netrc: %w", err) + } // mount .hgrc to home dir // to have access to private repos. - initializeAuthFile(conf.HGRCPath) - - logLvl, err := logrus.ParseLevel(conf.LogLevel) - if err != nil { - return nil, err + if err := initializeAuthFile(conf.HGRCPath); err != nil { + return nil, fmt.Errorf("initializing auth file from hgrc: %w", err) } - lggr := log.New(conf.CloudRuntime, logLvl) r := mux.NewRouter() r.Use( mw.WithRequestID, - mw.LogEntryMiddleware(lggr), + mw.LogEntryMiddleware(logger), mw.RequestLogger, secure.New(secure.Options{ SSLRedirect: conf.ForceSSL, @@ -82,10 +75,10 @@ func App(conf *config.Config) (http.Handler, error) { conf.TraceExporter, conf.TraceExporterURL, Service, - ENV, + conf.GoEnv, ) if err != nil { - lggr.Infof("%s", err) + logger.Info(err) } else { defer flushTraces() } @@ -95,7 +88,7 @@ func App(conf *config.Config) (http.Handler, error) { // was specified by the user. flushStats, err := observ.RegisterStatsExporter(r, conf.StatsExporter, Service) if err != nil { - lggr.Infof("%s", err) + logger.Info(err) } else { defer flushStats() } @@ -108,7 +101,7 @@ func App(conf *config.Config) (http.Handler, error) { if !conf.FilterOff() { mf, err := module.NewFilter(conf.FilterFile) if err != nil { - lggr.Fatal(err) + return nil, fmt.Errorf("creating new filter: %w", err) } r.Use(mw.NewFilterMiddleware(mf, conf.GlobalEndpoint)) } @@ -126,22 +119,15 @@ func App(conf *config.Config) (http.Handler, error) { store, err := GetStorage(conf.StorageType, conf.Storage, conf.TimeoutDuration(), client) if err != nil { - err = fmt.Errorf("error getting storage configuration: %w", err) - return nil, err + return nil, fmt.Errorf("getting storage configuration: %w", err) } proxyRouter := r if subRouter != nil { proxyRouter = subRouter } - if err := addProxyRoutes( - proxyRouter, - store, - lggr, - conf, - ); err != nil { - err = fmt.Errorf("error adding proxy routes: %w", err) - return nil, err + if err := addProxyRoutes(proxyRouter, store, logger, conf); err != nil { + return nil, fmt.Errorf("adding proxy routes: %w", err) } h := &ochttp.Handler{ diff --git a/cmd/proxy/actions/auth.go b/cmd/proxy/actions/auth.go index c4daedc6b..455d2febf 100644 --- a/cmd/proxy/actions/auth.go +++ b/cmd/proxy/actions/auth.go @@ -2,7 +2,6 @@ package actions import ( "fmt" - "log" "os" "path/filepath" "runtime" @@ -14,40 +13,43 @@ import ( // initializeAuthFile checks if provided auth file is at a pre-configured path // and moves to home directory -- note that this will override whatever // .netrc/.hgrc file you have in your home directory. -func initializeAuthFile(path string) { +func initializeAuthFile(path string) error { if path == "" { - return + return nil } fileBts, err := os.ReadFile(filepath.Clean(path)) if err != nil { - log.Fatalf("could not read %s: %v", path, err) + return fmt.Errorf("reading %s: %w", path, err) } hdir, err := homedir.Dir() if err != nil { - log.Fatalf("could not get homedir: %v", err) + return fmt.Errorf("getting home dir: %w", err) } fileName := transformAuthFileName(filepath.Base(path)) rcp := filepath.Join(hdir, fileName) if err := os.WriteFile(rcp, fileBts, 0o600); err != nil { - log.Fatalf("could not write to file: %v", err) + return fmt.Errorf("writing to auth file: %w", err) } + + return nil } // netrcFromToken takes a github token and creates a .netrc // file for you, overriding whatever might be already there. -func netrcFromToken(tok string) { +func netrcFromToken(tok string) error { fileContent := fmt.Sprintf("machine github.com login %s\n", tok) hdir, err := homedir.Dir() if err != nil { - log.Fatalf("netrcFromToken: could not get homedir: %v", err) + return fmt.Errorf("getting homedir: %w", err) } rcp := filepath.Join(hdir, getNETRCFilename()) if err := os.WriteFile(rcp, []byte(fileContent), 0o600); err != nil { - log.Fatalf("netrcFromToken: could not write to file: %v", err) + return fmt.Errorf("writing to netrc file: %w", err) } + return nil } func transformAuthFileName(authFileName string) string { diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index ffbc327a3..3b55c0d05 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -5,7 +5,7 @@ import ( "errors" "flag" "fmt" - "log" + stdlog "log" "net" "net/http" _ "net/http/pprof" @@ -17,6 +17,8 @@ import ( "github.com/gomods/athens/internal/shutdown" "github.com/gomods/athens/pkg/build" "github.com/gomods/athens/pkg/config" + athenslog "github.com/gomods/athens/pkg/log" + "github.com/sirupsen/logrus" ) var ( @@ -32,12 +34,19 @@ func main() { } conf, err := config.Load(*configFile) if err != nil { - log.Fatalf("could not load config file: %v", err) + stdlog.Fatalf("Could not load config file: %v", err) } - handler, err := actions.App(conf) + logLvl, err := logrus.ParseLevel(conf.LogLevel) if err != nil { - log.Fatal(err) + stdlog.Fatalf("Could not parse log level %q: %v", conf.LogLevel, err) + } + + logger := athenslog.New(conf.CloudRuntime, logLvl) + + handler, err := actions.App(logger, conf) + if err != nil { + logger.WithError(err).Fatal("Could not create App") } srv := &http.Server{ @@ -50,23 +59,24 @@ func main() { sigint := make(chan os.Signal, 1) signal.Notify(sigint, shutdown.GetSignals()...) s := <-sigint - log.Printf("Received signal (%s): Shutting down server", s) + logger.WithField("signal", s).Infof("Received signal, shutting down server") // We received an interrupt signal, shut down. ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(conf.ShutdownTimeout)) defer cancel() if err := srv.Shutdown(ctx); err != nil { - log.Fatal(err) + logger.WithError(err).Fatal("Could not shut down server") } close(idleConnsClosed) }() if conf.EnablePprof { go func() { - // pprof to be exposed on a different port than the application for security matters, not to expose profiling data and avoid DoS attacks (profiling slows down the service) + // pprof to be exposed on a different port than the application for security matters, + // not to expose profiling data and avoid DoS attacks (profiling slows down the service) // https://www.farsightsecurity.com/txt-record/2016/10/28/cmikk-go-remote-profiling/ - log.Printf("Starting `pprof` at port %v", conf.PprofPort) - log.Fatal(http.ListenAndServe(conf.PprofPort, nil)) //nolint:gosec // This should not be exposed to the world. + logger.WithField("port", conf.PprofPort).Infof("starting pprof") + logger.Fatal(http.ListenAndServe(conf.PprofPort, nil)) //nolint:gosec // This should not be exposed to the world. }() } @@ -74,18 +84,20 @@ func main() { var ln net.Listener if conf.UnixSocket != "" { - log.Printf("Starting application at Unix domain socket %v", conf.UnixSocket) + logger := logger.WithField("unixSocket", conf.UnixSocket) + logger.Info("Starting application") ln, err = net.Listen("unix", conf.UnixSocket) if err != nil { - log.Fatalf("error listening on Unix domain socket %v: %v", conf.UnixSocket, err) + logger.WithError(err).Fatal("Could not listen on Unix domain socket") } } else { - log.Printf("Starting application at port %v", conf.Port) + logger := logger.WithField("tcpPort", conf.Port) + logger.Info("Starting application") ln, err = net.Listen("tcp", conf.Port) if err != nil { - log.Fatalf("error listening on TCP port %v: %v", conf.Port, err) + logger.WithError(err).Fatal("Could not listen on TCP port") } } @@ -96,7 +108,7 @@ func main() { } if !errors.Is(err, http.ErrServerClosed) { - log.Fatal(err) + logger.WithError(err).Fatal("Could not start server") } <-idleConnsClosed From f4239d5b2835f85634b2d55c42d11e720f388524 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jan 2024 12:09:44 +0100 Subject: [PATCH 4/5] update-github-action(deps): bump github/codeql-action from 2 to 3 (#1905) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v2...v3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: DrPsychick --- .github/workflows/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4ca511522..3d28880a4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -22,12 +22,12 @@ jobs: uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: go - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 3164fe691424ba82dee0518bfaa82b0d0c4fe596 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jan 2024 12:44:11 +0100 Subject: [PATCH 5/5] update-github-action(deps): bump docker/build-push-action from 4 to 5 (#1906) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v4...v5) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: DrPsychick --- .github/workflows/release.canary.yml | 2 +- .github/workflows/release.latest.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.canary.yml b/.github/workflows/release.canary.yml index 9f375cee4..1d335b9fb 100644 --- a/.github/workflows/release.canary.yml +++ b/.github/workflows/release.canary.yml @@ -28,7 +28,7 @@ jobs: run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - name: Build and push images - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: file: cmd/proxy/Dockerfile build-args: VERSION=${{ github.sha }} diff --git a/.github/workflows/release.latest.yml b/.github/workflows/release.latest.yml index b2f89bedb..7b82e3b21 100644 --- a/.github/workflows/release.latest.yml +++ b/.github/workflows/release.latest.yml @@ -34,7 +34,7 @@ jobs: type=raw,value=latest - name: Build and push images - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: file: cmd/proxy/Dockerfile build-args: VERSION=${{ github.ref_name }}