From fe9e9c763e3a8174c966d55cc14dce197de87d1b Mon Sep 17 00:00:00 2001
From: Shunsuke Suzuki <suzuki.shunsuke.1989@gmail.com>
Date: Mon, 30 May 2022 07:35:00 +0900
Subject: [PATCH] feat: use GitHub GraphQL API

---
 go.mod                                   |  2 +
 go.sum                                   |  4 ++
 pkg/cli/exec.go                          |  3 +-
 pkg/cli/generate.go                      |  3 +-
 pkg/cli/install.go                       |  3 +-
 pkg/cli/list.go                          |  3 +-
 pkg/cli/which.go                         |  3 +-
 pkg/controller/generate/generate.go      | 46 +++++++++-------------
 pkg/controller/generate/generate_test.go |  4 +-
 pkg/controller/wire.go                   | 24 ++++++------
 pkg/controller/wire_gen.go               | 50 +++++++++++++++---------
 pkg/github/github.go                     | 21 +++++++---
 pkg/github/github_test.go                |  2 +-
 pkg/github/mock_v4.go                    | 19 +++++++++
 pkg/github/v4.go                         | 48 +++++++++++++++++++++++
 15 files changed, 160 insertions(+), 75 deletions(-)
 create mode 100644 pkg/github/mock_v4.go
 create mode 100644 pkg/github/v4.go

diff --git a/go.mod b/go.mod
index 855b4e085..3022b28cc 100644
--- a/go.mod
+++ b/go.mod
@@ -14,6 +14,8 @@ require (
 	github.com/invopop/jsonschema v0.4.0
 	github.com/ktr0731/go-fuzzyfinder v0.6.0
 	github.com/mholt/archiver/v3 v3.5.1
+	github.com/shurcooL/githubv4 v0.0.0-20220520033151-0b4e3294ff00
+	github.com/shurcooL/graphql v0.0.0-20220520033453-bdb1221e171e // indirect
 	github.com/sirupsen/logrus v1.8.1
 	github.com/spf13/afero v1.8.2
 	github.com/suzuki-shunsuke/flute v1.0.1
diff --git a/go.sum b/go.sum
index 6feec338b..d452219d7 100644
--- a/go.sum
+++ b/go.sum
@@ -256,6 +256,10 @@ github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu
 github.com/scylladb/go-set v1.0.2/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs=
 github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
 github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/shurcooL/githubv4 v0.0.0-20220520033151-0b4e3294ff00 h1:fiFvD4lT0aWjuuAb64LlZ/67v87m+Kc9Qsu5cMFNK0w=
+github.com/shurcooL/githubv4 v0.0.0-20220520033151-0b4e3294ff00/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
+github.com/shurcooL/graphql v0.0.0-20220520033453-bdb1221e171e h1:dmM59/+RIH6bO/gjmUgaJwdyDhAvZkHgA5OJUcoUyGU=
+github.com/shurcooL/graphql v0.0.0-20220520033453-bdb1221e171e/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
 github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
 github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
diff --git a/pkg/cli/exec.go b/pkg/cli/exec.go
index 31ff2aead..d1c7afd17 100644
--- a/pkg/cli/exec.go
+++ b/pkg/cli/exec.go
@@ -3,7 +3,6 @@ package cli
 import (
 	"errors"
 	"fmt"
-	"net/http"
 	"path/filepath"
 
 	"github.com/aquaproj/aqua/pkg/config"
@@ -41,7 +40,7 @@ func (runner *Runner) execAction(c *cli.Context) error {
 	if err := runner.setParam(c, param); err != nil {
 		return fmt.Errorf("parse the command line arguments: %w", err)
 	}
-	ctrl := controller.InitializeExecCommandController(c.Context, param, http.DefaultClient, runner.Runtime)
+	ctrl := controller.InitializeExecCommandController(c.Context, param, runner.Runtime)
 	exeName, args, err := parseExecArgs(c.Args().Slice())
 	if err != nil {
 		return err
diff --git a/pkg/cli/generate.go b/pkg/cli/generate.go
index 9f4b24ef9..e1fe4a8a5 100644
--- a/pkg/cli/generate.go
+++ b/pkg/cli/generate.go
@@ -2,7 +2,6 @@ package cli
 
 import (
 	"fmt"
-	"net/http"
 
 	"github.com/aquaproj/aqua/pkg/config"
 	"github.com/aquaproj/aqua/pkg/controller"
@@ -106,6 +105,6 @@ func (runner *Runner) generateAction(c *cli.Context) error {
 	if err := runner.setParam(c, param); err != nil {
 		return fmt.Errorf("parse the command line arguments: %w", err)
 	}
-	ctrl := controller.InitializeGenerateCommandController(c.Context, param, http.DefaultClient)
+	ctrl := controller.InitializeGenerateCommandController(c.Context, param)
 	return ctrl.Generate(c.Context, runner.LogE, param, c.Args().Slice()...) //nolint:wrapcheck
 }
diff --git a/pkg/cli/install.go b/pkg/cli/install.go
index b6cf19536..5d5491632 100644
--- a/pkg/cli/install.go
+++ b/pkg/cli/install.go
@@ -2,7 +2,6 @@ package cli
 
 import (
 	"fmt"
-	"net/http"
 
 	"github.com/aquaproj/aqua/pkg/config"
 	"github.com/aquaproj/aqua/pkg/controller"
@@ -54,6 +53,6 @@ func (runner *Runner) installAction(c *cli.Context) error {
 	if err := runner.setParam(c, param); err != nil {
 		return fmt.Errorf("parse the command line arguments: %w", err)
 	}
-	ctrl := controller.InitializeInstallCommandController(c.Context, param, http.DefaultClient, runner.Runtime)
+	ctrl := controller.InitializeInstallCommandController(c.Context, param, runner.Runtime)
 	return ctrl.Install(c.Context, param, runner.LogE) //nolint:wrapcheck
 }
diff --git a/pkg/cli/list.go b/pkg/cli/list.go
index 2fb1b2c8b..6337f3206 100644
--- a/pkg/cli/list.go
+++ b/pkg/cli/list.go
@@ -2,7 +2,6 @@ package cli
 
 import (
 	"fmt"
-	"net/http"
 
 	"github.com/aquaproj/aqua/pkg/config"
 	"github.com/aquaproj/aqua/pkg/controller"
@@ -32,6 +31,6 @@ func (runner *Runner) listAction(c *cli.Context) error {
 	if err := runner.setParam(c, param); err != nil {
 		return fmt.Errorf("parse the command line arguments: %w", err)
 	}
-	ctrl := controller.InitializeListCommandController(c.Context, param, http.DefaultClient)
+	ctrl := controller.InitializeListCommandController(c.Context, param)
 	return ctrl.List(c.Context, param, runner.LogE) //nolint:wrapcheck
 }
diff --git a/pkg/cli/which.go b/pkg/cli/which.go
index 8be61fe57..4b9a6f2b3 100644
--- a/pkg/cli/which.go
+++ b/pkg/cli/which.go
@@ -2,7 +2,6 @@ package cli
 
 import (
 	"fmt"
-	"net/http"
 	"os"
 
 	"github.com/aquaproj/aqua/pkg/config"
@@ -39,7 +38,7 @@ func (runner *Runner) whichAction(c *cli.Context) error {
 	if err := runner.setParam(c, param); err != nil {
 		return fmt.Errorf("parse the command line arguments: %w", err)
 	}
-	ctrl := controller.InitializeWhichCommandController(c.Context, param, http.DefaultClient, runner.Runtime)
+	ctrl := controller.InitializeWhichCommandController(c.Context, param, runner.Runtime)
 	exeName, _, err := parseExecArgs(c.Args().Slice())
 	if err != nil {
 		return err
diff --git a/pkg/controller/generate/generate.go b/pkg/controller/generate/generate.go
index 3a15ddb94..02e49a522 100644
--- a/pkg/controller/generate/generate.go
+++ b/pkg/controller/generate/generate.go
@@ -28,6 +28,7 @@ type Controller struct {
 	stdin                   io.Reader
 	stdout                  io.Writer
 	gitHubRepositoryService githubSvc.RepositoryService
+	githubV4                githubSvc.GraphQL
 	registryInstaller       registry.Installer
 	configFinder            finder.ConfigFinder
 	configReader            reader.ConfigReader
@@ -35,7 +36,7 @@ type Controller struct {
 	fs                      afero.Fs
 }
 
-func New(configFinder finder.ConfigFinder, configReader reader.ConfigReader, registInstaller registry.Installer, gh githubSvc.RepositoryService, fs afero.Fs, fuzzyFinder FuzzyFinder) *Controller {
+func New(configFinder finder.ConfigFinder, configReader reader.ConfigReader, registInstaller registry.Installer, gh githubSvc.RepositoryService, fs afero.Fs, fuzzyFinder FuzzyFinder, githubV4 githubSvc.GraphQL) *Controller {
 	return &Controller{
 		stdin:                   os.Stdin,
 		stdout:                  os.Stdout,
@@ -43,6 +44,7 @@ func New(configFinder finder.ConfigFinder, configReader reader.ConfigReader, reg
 		configReader:            configReader,
 		registryInstaller:       registInstaller,
 		gitHubRepositoryService: gh,
+		githubV4:                githubV4,
 		fs:                      fs,
 		fuzzyFinder:             fuzzyFinder,
 	}
@@ -235,35 +237,26 @@ func (ctrl *Controller) listAndGetTagName(ctx context.Context, pkgInfo *config.P
 func (ctrl *Controller) listAndGetTagNameFromTag(ctx context.Context, pkgInfo *config.PackageInfo, logE *logrus.Entry) string {
 	repoOwner := pkgInfo.RepoOwner
 	repoName := pkgInfo.RepoName
-	opt := &github.ListOptions{
-		PerPage: 30, //nolint:gomnd
-	}
 	versionFilter, err := constraint.CompileVersionFilter(*pkgInfo.VersionFilter)
 	if err != nil {
 		return ""
 	}
-	for {
-		tags, _, err := ctrl.gitHubRepositoryService.ListTags(ctx, repoOwner, repoName, opt)
-		if err != nil {
-			logerr.WithError(logE, err).WithFields(logrus.Fields{
-				"repo_owner": repoOwner,
-				"repo_name":  repoName,
-			}).Warn("list releases")
-			return ""
-		}
-		for _, tag := range tags {
-			tagName := tag.GetName()
-			f, err := constraint.EvaluateVersionFilter(versionFilter, tagName)
-			if err != nil || !f {
-				continue
-			}
-			return tagName
-		}
-		if len(tags) != opt.PerPage {
-			return ""
+	tags, err := ctrl.githubV4.ListTags(ctx, repoOwner, repoName)
+	if err != nil {
+		logerr.WithError(logE, err).WithFields(logrus.Fields{
+			"repo_owner": repoOwner,
+			"repo_name":  repoName,
+		}).Warn("list releases")
+		return ""
+	}
+	for _, tag := range tags {
+		f, err := constraint.EvaluateVersionFilter(versionFilter, tag)
+		if err != nil || !f {
+			continue
 		}
-		opt.Page++
+		return tag
 	}
+	return ""
 }
 
 func (ctrl *Controller) getOutputtedGitHubPkgFromTag(ctx context.Context, outputPkg *config.Package, pkgInfo *config.PackageInfo, logE *logrus.Entry) {
@@ -273,7 +266,7 @@ func (ctrl *Controller) getOutputtedGitHubPkgFromTag(ctx context.Context, output
 	if pkgInfo.VersionFilter != nil {
 		tagName = ctrl.listAndGetTagNameFromTag(ctx, pkgInfo, logE)
 	} else {
-		tags, _, err := ctrl.gitHubRepositoryService.ListTags(ctx, repoOwner, repoName, nil)
+		tags, err := ctrl.githubV4.ListTags(ctx, repoOwner, repoName)
 		if err != nil {
 			logerr.WithError(logE, err).WithFields(logrus.Fields{
 				"repo_owner": repoOwner,
@@ -284,8 +277,7 @@ func (ctrl *Controller) getOutputtedGitHubPkgFromTag(ctx context.Context, output
 		if len(tags) == 0 {
 			return
 		}
-		tag := tags[0]
-		tagName = tag.GetName()
+		tagName = tags[0]
 	}
 
 	if pkgName := pkgInfo.GetName(); pkgName == repoOwner+"/"+repoName || strings.HasPrefix(pkgName, repoOwner+"/"+repoName+"/") {
diff --git a/pkg/controller/generate/generate_test.go b/pkg/controller/generate/generate_test.go
index 7128bc7c5..c1061cc22 100644
--- a/pkg/controller/generate/generate_test.go
+++ b/pkg/controller/generate/generate_test.go
@@ -37,6 +37,7 @@ func Test_controller_Generate(t *testing.T) { //nolint:funlen
 		idxs           []int
 		fuzzyFinderErr error
 		releases       []*github.RepositoryRelease
+		tags           []string
 	}{
 		{
 			name: "normal",
@@ -234,11 +235,12 @@ packages:
 			}
 			configFinder := finder.NewConfigFinder(fs)
 			gh := githubSvc.NewMock(d.releases, nil, "")
+			v4Client := githubSvc.NewMockGraphQL(d.tags, nil)
 			downloader := download.NewRegistryDownloader(gh, download.NewHTTPDownloader(http.DefaultClient))
 			registryInstaller := registry.New(d.param, downloader, fs)
 			configReader := reader.New(fs)
 			fuzzyFinder := generate.NewMockFuzzyFinder(d.idxs, d.fuzzyFinderErr)
-			ctrl := generate.New(configFinder, configReader, registryInstaller, gh, fs, fuzzyFinder)
+			ctrl := generate.New(configFinder, configReader, registryInstaller, gh, fs, fuzzyFinder, v4Client)
 			if err := ctrl.Generate(ctx, logE, d.param, d.args...); err != nil {
 				if d.isErr {
 					return
diff --git a/pkg/controller/wire.go b/pkg/controller/wire.go
index 7e3238ed2..36a356a96 100644
--- a/pkg/controller/wire.go
+++ b/pkg/controller/wire.go
@@ -5,7 +5,6 @@ package controller
 
 import (
 	"context"
-	"net/http"
 
 	"github.com/aquaproj/aqua/pkg/config"
 	finder "github.com/aquaproj/aqua/pkg/config-finder"
@@ -24,36 +23,37 @@ import (
 	"github.com/aquaproj/aqua/pkg/link"
 	"github.com/aquaproj/aqua/pkg/runtime"
 	"github.com/google/wire"
+	"github.com/shurcooL/githubv4"
 	"github.com/spf13/afero"
 	"github.com/suzuki-shunsuke/go-osenv/osenv"
 )
 
-func InitializeListCommandController(ctx context.Context, param *config.Param, httpClient *http.Client) *list.Controller {
-	wire.Build(list.NewController, finder.NewConfigFinder, github.New, registry.New, download.NewRegistryDownloader, reader.New, afero.NewOsFs, download.NewHTTPDownloader)
+func InitializeListCommandController(ctx context.Context, param *config.Param) *list.Controller {
+	wire.Build(list.NewController, finder.NewConfigFinder, github.New, github.NewHTTPClient, github.NewAccessToken, registry.New, download.NewRegistryDownloader, reader.New, afero.NewOsFs, download.NewHTTPDownloader)
 	return &list.Controller{}
 }
 
 func InitializeInitCommandController(ctx context.Context, param *config.Param) *initcmd.Controller {
-	wire.Build(initcmd.New, github.New, afero.NewOsFs)
+	wire.Build(initcmd.New, github.New, github.NewHTTPClient, github.NewAccessToken, afero.NewOsFs)
 	return &initcmd.Controller{}
 }
 
-func InitializeGenerateCommandController(ctx context.Context, param *config.Param, httpClient *http.Client) *generate.Controller {
-	wire.Build(generate.New, finder.NewConfigFinder, github.New, registry.New, download.NewRegistryDownloader, reader.New, afero.NewOsFs, generate.NewFuzzyFinder, download.NewHTTPDownloader)
+func InitializeGenerateCommandController(ctx context.Context, param *config.Param) *generate.Controller {
+	wire.Build(generate.New, finder.NewConfigFinder, github.New, github.NewHTTPClient, github.NewAccessToken, github.NewGraphQL, githubv4.NewClient, registry.New, download.NewRegistryDownloader, reader.New, afero.NewOsFs, generate.NewFuzzyFinder, download.NewHTTPDownloader)
 	return &generate.Controller{}
 }
 
-func InitializeInstallCommandController(ctx context.Context, param *config.Param, httpClient *http.Client, rt *runtime.Runtime) *install.Controller {
-	wire.Build(install.New, finder.NewConfigFinder, github.New, registry.New, download.NewRegistryDownloader, reader.New, installpackage.New, download.NewPackageDownloader, afero.NewOsFs, link.New, download.NewHTTPDownloader, exec.New)
+func InitializeInstallCommandController(ctx context.Context, param *config.Param, rt *runtime.Runtime) *install.Controller {
+	wire.Build(install.New, finder.NewConfigFinder, github.New, github.NewHTTPClient, github.NewAccessToken, registry.New, download.NewRegistryDownloader, reader.New, installpackage.New, download.NewPackageDownloader, afero.NewOsFs, link.New, download.NewHTTPDownloader, exec.New)
 	return &install.Controller{}
 }
 
-func InitializeWhichCommandController(ctx context.Context, param *config.Param, httpClient *http.Client, rt *runtime.Runtime) which.Controller {
-	wire.Build(which.New, finder.NewConfigFinder, github.New, registry.New, download.NewRegistryDownloader, reader.New, osenv.New, afero.NewOsFs, download.NewHTTPDownloader, link.New)
+func InitializeWhichCommandController(ctx context.Context, param *config.Param, rt *runtime.Runtime) which.Controller {
+	wire.Build(which.New, finder.NewConfigFinder, github.New, github.NewHTTPClient, github.NewAccessToken, registry.New, download.NewRegistryDownloader, reader.New, osenv.New, afero.NewOsFs, download.NewHTTPDownloader, link.New)
 	return nil
 }
 
-func InitializeExecCommandController(ctx context.Context, param *config.Param, httpClient *http.Client, rt *runtime.Runtime) *cexec.Controller {
-	wire.Build(cexec.New, finder.NewConfigFinder, download.NewPackageDownloader, installpackage.New, github.New, registry.New, download.NewRegistryDownloader, reader.New, which.New, exec.New, osenv.New, afero.NewOsFs, link.New, download.NewHTTPDownloader)
+func InitializeExecCommandController(ctx context.Context, param *config.Param, rt *runtime.Runtime) *cexec.Controller {
+	wire.Build(cexec.New, finder.NewConfigFinder, download.NewPackageDownloader, installpackage.New, github.New, github.NewHTTPClient, github.NewAccessToken, registry.New, download.NewRegistryDownloader, reader.New, which.New, exec.New, osenv.New, afero.NewOsFs, link.New, download.NewHTTPDownloader)
 	return &cexec.Controller{}
 }
diff --git a/pkg/controller/wire_gen.go b/pkg/controller/wire_gen.go
index c797d30c0..1d8ab9f48 100644
--- a/pkg/controller/wire_gen.go
+++ b/pkg/controller/wire_gen.go
@@ -24,19 +24,21 @@ import (
 	"github.com/aquaproj/aqua/pkg/installpackage"
 	"github.com/aquaproj/aqua/pkg/link"
 	"github.com/aquaproj/aqua/pkg/runtime"
+	"github.com/shurcooL/githubv4"
 	"github.com/spf13/afero"
 	"github.com/suzuki-shunsuke/go-osenv/osenv"
-	"net/http"
 )
 
 // Injectors from wire.go:
 
-func InitializeListCommandController(ctx context.Context, param *config.Param, httpClient *http.Client) *list.Controller {
+func InitializeListCommandController(ctx context.Context, param *config.Param) *list.Controller {
 	fs := afero.NewOsFs()
 	configFinder := finder.NewConfigFinder(fs)
 	configReader := reader.New(fs)
-	repositoryService := github.New(ctx)
-	httpDownloader := download.NewHTTPDownloader(httpClient)
+	accessToken := github.NewAccessToken()
+	client := github.NewHTTPClient(ctx, accessToken)
+	repositoryService := github.New(client)
+	httpDownloader := download.NewHTTPDownloader(client)
 	registryDownloader := download.NewRegistryDownloader(repositoryService, httpDownloader)
 	installer := registry.New(param, registryDownloader, fs)
 	controller := list.NewController(configFinder, configReader, installer)
@@ -44,31 +46,39 @@ func InitializeListCommandController(ctx context.Context, param *config.Param, h
 }
 
 func InitializeInitCommandController(ctx context.Context, param *config.Param) *initcmd.Controller {
-	repositoryService := github.New(ctx)
+	accessToken := github.NewAccessToken()
+	client := github.NewHTTPClient(ctx, accessToken)
+	repositoryService := github.New(client)
 	fs := afero.NewOsFs()
 	controller := initcmd.New(repositoryService, fs)
 	return controller
 }
 
-func InitializeGenerateCommandController(ctx context.Context, param *config.Param, httpClient *http.Client) *generate.Controller {
+func InitializeGenerateCommandController(ctx context.Context, param *config.Param) *generate.Controller {
 	fs := afero.NewOsFs()
 	configFinder := finder.NewConfigFinder(fs)
 	configReader := reader.New(fs)
-	repositoryService := github.New(ctx)
-	httpDownloader := download.NewHTTPDownloader(httpClient)
+	accessToken := github.NewAccessToken()
+	client := github.NewHTTPClient(ctx, accessToken)
+	repositoryService := github.New(client)
+	httpDownloader := download.NewHTTPDownloader(client)
 	registryDownloader := download.NewRegistryDownloader(repositoryService, httpDownloader)
 	installer := registry.New(param, registryDownloader, fs)
 	fuzzyFinder := generate.NewFuzzyFinder()
-	controller := generate.New(configFinder, configReader, installer, repositoryService, fs, fuzzyFinder)
+	githubv4Client := githubv4.NewClient(client)
+	graphQL := github.NewGraphQL(githubv4Client)
+	controller := generate.New(configFinder, configReader, installer, repositoryService, fs, fuzzyFinder, graphQL)
 	return controller
 }
 
-func InitializeInstallCommandController(ctx context.Context, param *config.Param, httpClient *http.Client, rt *runtime.Runtime) *install.Controller {
+func InitializeInstallCommandController(ctx context.Context, param *config.Param, rt *runtime.Runtime) *install.Controller {
 	fs := afero.NewOsFs()
 	configFinder := finder.NewConfigFinder(fs)
 	configReader := reader.New(fs)
-	repositoryService := github.New(ctx)
-	httpDownloader := download.NewHTTPDownloader(httpClient)
+	accessToken := github.NewAccessToken()
+	client := github.NewHTTPClient(ctx, accessToken)
+	repositoryService := github.New(client)
+	httpDownloader := download.NewHTTPDownloader(client)
 	registryDownloader := download.NewRegistryDownloader(repositoryService, httpDownloader)
 	installer := registry.New(param, registryDownloader, fs)
 	packageDownloader := download.NewPackageDownloader(repositoryService, rt, httpDownloader)
@@ -79,12 +89,14 @@ func InitializeInstallCommandController(ctx context.Context, param *config.Param
 	return controller
 }
 
-func InitializeWhichCommandController(ctx context.Context, param *config.Param, httpClient *http.Client, rt *runtime.Runtime) which.Controller {
+func InitializeWhichCommandController(ctx context.Context, param *config.Param, rt *runtime.Runtime) which.Controller {
 	fs := afero.NewOsFs()
 	configFinder := finder.NewConfigFinder(fs)
 	configReader := reader.New(fs)
-	repositoryService := github.New(ctx)
-	httpDownloader := download.NewHTTPDownloader(httpClient)
+	accessToken := github.NewAccessToken()
+	client := github.NewHTTPClient(ctx, accessToken)
+	repositoryService := github.New(client)
+	httpDownloader := download.NewHTTPDownloader(client)
 	registryDownloader := download.NewRegistryDownloader(repositoryService, httpDownloader)
 	installer := registry.New(param, registryDownloader, fs)
 	osEnv := osenv.New()
@@ -93,9 +105,11 @@ func InitializeWhichCommandController(ctx context.Context, param *config.Param,
 	return controller
 }
 
-func InitializeExecCommandController(ctx context.Context, param *config.Param, httpClient *http.Client, rt *runtime.Runtime) *exec2.Controller {
-	repositoryService := github.New(ctx)
-	httpDownloader := download.NewHTTPDownloader(httpClient)
+func InitializeExecCommandController(ctx context.Context, param *config.Param, rt *runtime.Runtime) *exec2.Controller {
+	accessToken := github.NewAccessToken()
+	client := github.NewHTTPClient(ctx, accessToken)
+	repositoryService := github.New(client)
+	httpDownloader := download.NewHTTPDownloader(client)
 	packageDownloader := download.NewPackageDownloader(repositoryService, rt, httpDownloader)
 	fs := afero.NewOsFs()
 	linker := link.New()
diff --git a/pkg/github/github.go b/pkg/github/github.go
index 514e2e479..38ccb6675 100644
--- a/pkg/github/github.go
+++ b/pkg/github/github.go
@@ -16,11 +16,20 @@ type RepositoryService interface {
 	GetReleaseByTag(ctx context.Context, owner, repoName, version string) (*github.RepositoryRelease, *github.Response, error)
 	DownloadReleaseAsset(ctx context.Context, owner, repoName string, assetID int64, httpClient *http.Client) (io.ReadCloser, string, error)
 	ListReleases(ctx context.Context, owner, repo string, opts *github.ListOptions) ([]*github.RepositoryRelease, *github.Response, error)
-	ListTags(ctx context.Context, owner string, repo string, opts *github.ListOptions) ([]*github.RepositoryTag, *github.Response, error)
 }
 
-func New(ctx context.Context) RepositoryService {
-	return github.NewClient(getHTTPClientForGitHub(ctx, getGitHubToken())).Repositories
+func New(httpClient *http.Client) RepositoryService {
+	return github.NewClient(httpClient).Repositories
+}
+
+type AccessToken struct {
+	token string
+}
+
+func NewAccessToken() *AccessToken {
+	return &AccessToken{
+		token: getGitHubToken(),
+	}
 }
 
 func getGitHubToken() string {
@@ -30,11 +39,11 @@ func getGitHubToken() string {
 	return os.Getenv("GITHUB_TOKEN")
 }
 
-func getHTTPClientForGitHub(ctx context.Context, token string) *http.Client {
-	if token == "" {
+func NewHTTPClient(ctx context.Context, token *AccessToken) *http.Client {
+	if token == nil || token.token == "" {
 		return http.DefaultClient
 	}
 	return oauth2.NewClient(ctx, oauth2.StaticTokenSource(
-		&oauth2.Token{AccessToken: token},
+		&oauth2.Token{AccessToken: token.token},
 	))
 }
diff --git a/pkg/github/github_test.go b/pkg/github/github_test.go
index 2ab6f8792..4eb70601c 100644
--- a/pkg/github/github_test.go
+++ b/pkg/github/github_test.go
@@ -9,7 +9,7 @@ import (
 
 func TestNew(t *testing.T) {
 	t.Parallel()
-	if client := github.New(context.Background()); client == nil {
+	if client := github.New(github.NewHTTPClient(context.Background(), github.NewAccessToken())); client == nil {
 		t.Fatal("client must not be nil")
 	}
 }
diff --git a/pkg/github/mock_v4.go b/pkg/github/mock_v4.go
new file mode 100644
index 000000000..97e1e7708
--- /dev/null
+++ b/pkg/github/mock_v4.go
@@ -0,0 +1,19 @@
+package github
+
+import "context"
+
+func NewMockGraphQL(tags []string, err error) GraphQL {
+	return &mockV4{
+		tags: tags,
+		err:  err,
+	}
+}
+
+type mockV4 struct {
+	tags []string
+	err  error
+}
+
+func (m *mockV4) ListTags(ctx context.Context, owner string, repo string) ([]string, error) {
+	return m.tags, m.err
+}
diff --git a/pkg/github/v4.go b/pkg/github/v4.go
new file mode 100644
index 000000000..88eeab376
--- /dev/null
+++ b/pkg/github/v4.go
@@ -0,0 +1,48 @@
+package github
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/shurcooL/githubv4"
+)
+
+type GraphQL interface {
+	ListTags(ctx context.Context, owner string, repo string) ([]string, error)
+}
+
+func NewGraphQL(v4Client *githubv4.Client) GraphQL {
+	return &graphQL{
+		client: v4Client,
+	}
+}
+
+type graphQL struct {
+	client *githubv4.Client
+}
+
+func (cl *graphQL) ListTags(ctx context.Context, owner string, repo string) ([]string, error) {
+	// Pagination isn't supported
+	var q struct {
+		Repository struct {
+			Refs struct {
+				Nodes []struct {
+					Name string
+				}
+			} `graphql:"refs(refPrefix: \"refs/tags/\", first:30, orderBy:{direction:DESC, field:TAG_COMMIT_DATE})"`
+		} `graphql:"repository(owner: $owner, name: $name)"`
+	}
+	variables := map[string]interface{}{
+		"owner": githubv4.String(owner),
+		"name":  githubv4.String(repo),
+	}
+
+	if err := cl.client.Query(ctx, &q, variables); err != nil {
+		return nil, fmt.Errorf("list tags by GitHub GraphQL API: %w", err)
+	}
+	tags := make([]string, len(q.Repository.Refs.Nodes))
+	for i, node := range q.Repository.Refs.Nodes {
+		tags[i] = node.Name
+	}
+	return tags, nil
+}