From db8a6c4ec2ec08b9c5c2ac081a0db37f4fbb2726 Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 7 May 2025 13:29:46 +0200 Subject: [PATCH 1/2] Extract ghmcp internal package This commit cleanly separates config parsing, stdio server execution and mcp server construction. Aside from significant clarity improvements, it allows for direct construction of the mcp server in e2e tests to allow for breakpoint debugging. --- cmd/github-mcp-server/main.go | 188 ++++------------------------- internal/ghmcp/server.go | 216 ++++++++++++++++++++++++++++++++++ 2 files changed, 238 insertions(+), 166 deletions(-) create mode 100644 internal/ghmcp/server.go diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index cf459f47..fb716f78 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -1,25 +1,17 @@ package main import ( - "context" + "errors" "fmt" - "io" - stdlog "log" "os" - "os/signal" - "syscall" + "github.com/github/github-mcp-server/internal/ghmcp" "github.com/github/github-mcp-server/pkg/github" - iolog "github.com/github/github-mcp-server/pkg/log" - "github.com/github/github-mcp-server/pkg/translations" - gogithub "github.com/google/go-github/v69/github" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" ) +// These variables are set by the build process using ldflags. var version = "version" var commit = "commit" var date = "date" @@ -36,13 +28,10 @@ var ( Use: "stdio", Short: "Start stdio server", Long: `Start a server that communicates via standard input/output streams using JSON-RPC messages.`, - Run: func(_ *cobra.Command, _ []string) { - logFile := viper.GetString("log-file") - readOnly := viper.GetBool("read-only") - exportTranslations := viper.GetBool("export-translations") - logger, err := initLogger(logFile) - if err != nil { - stdlog.Fatal("Failed to initialize logger:", err) + RunE: func(_ *cobra.Command, _ []string) error { + token := viper.GetString("personal_access_token") + if token == "" { + return errors.New("GITHUB_PERSONAL_ACCESS_TOKEN not set") } // If you're wondering why we're not using viper.GetStringSlice("toolsets"), @@ -50,22 +39,23 @@ var ( // vars when using GetStringSlice. // https://github.com/spf13/viper/issues/380 var enabledToolsets []string - err = viper.UnmarshalKey("toolsets", &enabledToolsets) - if err != nil { - stdlog.Fatal("Failed to unmarshal toolsets:", err) + if err := viper.UnmarshalKey("toolsets", &enabledToolsets); err != nil { + return fmt.Errorf("failed to unmarshal toolsets: %w", err) } - logCommands := viper.GetBool("enable-command-logging") - cfg := runConfig{ - readOnly: readOnly, - logger: logger, - logCommands: logCommands, - exportTranslations: exportTranslations, - enabledToolsets: enabledToolsets, - } - if err := runStdioServer(cfg); err != nil { - stdlog.Fatal("failed to run stdio server:", err) + stdioServerConfig := ghmcp.StdioServerConfig{ + Version: version, + Host: viper.GetString("host"), + Token: token, + EnabledToolsets: enabledToolsets, + DynamicToolsets: viper.GetBool("dynamic_toolsets"), + ReadOnly: viper.GetBool("read-only"), + ExportTranslations: viper.GetBool("export-translations"), + EnableCommandLogging: viper.GetBool("enable-command-logging"), + LogFilePath: viper.GetString("log-file"), } + + return ghmcp.RunStdioServer(stdioServerConfig) }, } ) @@ -103,143 +93,9 @@ func initConfig() { viper.AutomaticEnv() } -func initLogger(outPath string) (*log.Logger, error) { - if outPath == "" { - return log.New(), nil - } - - file, err := os.OpenFile(outPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) - if err != nil { - return nil, fmt.Errorf("failed to open log file: %w", err) - } - - logger := log.New() - logger.SetLevel(log.DebugLevel) - logger.SetOutput(file) - - return logger, nil -} - -type runConfig struct { - readOnly bool - logger *log.Logger - logCommands bool - exportTranslations bool - enabledToolsets []string -} - -func runStdioServer(cfg runConfig) error { - // Create app context - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) - defer stop() - - // Create GH client - token := viper.GetString("personal_access_token") - if token == "" { - cfg.logger.Fatal("GITHUB_PERSONAL_ACCESS_TOKEN not set") - } - ghClient := gogithub.NewClient(nil).WithAuthToken(token) - ghClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", version) - - host := viper.GetString("host") - - if host != "" { - var err error - ghClient, err = ghClient.WithEnterpriseURLs(host, host) - if err != nil { - return fmt.Errorf("failed to create GitHub client with host: %w", err) - } - } - - t, dumpTranslations := translations.TranslationHelper() - - beforeInit := func(_ context.Context, _ any, message *mcp.InitializeRequest) { - ghClient.UserAgent = fmt.Sprintf("github-mcp-server/%s (%s/%s)", version, message.Params.ClientInfo.Name, message.Params.ClientInfo.Version) - } - - getClient := func(_ context.Context) (*gogithub.Client, error) { - return ghClient, nil // closing over client - } - - hooks := &server.Hooks{ - OnBeforeInitialize: []server.OnBeforeInitializeFunc{beforeInit}, - } - // Create server - ghServer := github.NewServer(version, server.WithHooks(hooks)) - - enabled := cfg.enabledToolsets - dynamic := viper.GetBool("dynamic_toolsets") - if dynamic { - // filter "all" from the enabled toolsets - enabled = make([]string, 0, len(cfg.enabledToolsets)) - for _, toolset := range cfg.enabledToolsets { - if toolset != "all" { - enabled = append(enabled, toolset) - } - } - } - - // Create default toolsets - toolsets, err := github.InitToolsets(enabled, cfg.readOnly, getClient, t) - context := github.InitContextToolset(getClient, t) - - if err != nil { - stdlog.Fatal("Failed to initialize toolsets:", err) - } - - // Register resources with the server - github.RegisterResources(ghServer, getClient, t) - // Register the tools with the server - toolsets.RegisterTools(ghServer) - context.RegisterTools(ghServer) - - if dynamic { - dynamic := github.InitDynamicToolset(ghServer, toolsets, t) - dynamic.RegisterTools(ghServer) - } - - stdioServer := server.NewStdioServer(ghServer) - - stdLogger := stdlog.New(cfg.logger.Writer(), "stdioserver", 0) - stdioServer.SetErrorLogger(stdLogger) - - if cfg.exportTranslations { - // Once server is initialized, all translations are loaded - dumpTranslations() - } - - // Start listening for messages - errC := make(chan error, 1) - go func() { - in, out := io.Reader(os.Stdin), io.Writer(os.Stdout) - - if cfg.logCommands { - loggedIO := iolog.NewIOLogger(in, out, cfg.logger) - in, out = loggedIO, loggedIO - } - - errC <- stdioServer.Listen(ctx, in, out) - }() - - // Output github-mcp-server string - _, _ = fmt.Fprintf(os.Stderr, "GitHub MCP Server running on stdio\n") - - // Wait for shutdown signal - select { - case <-ctx.Done(): - cfg.logger.Infof("shutting down server...") - case err := <-errC: - if err != nil { - return fmt.Errorf("error running server: %w", err) - } - } - - return nil -} - func main() { if err := rootCmd.Execute(); err != nil { - fmt.Println(err) + fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } } diff --git a/internal/ghmcp/server.go b/internal/ghmcp/server.go new file mode 100644 index 00000000..f75119ad --- /dev/null +++ b/internal/ghmcp/server.go @@ -0,0 +1,216 @@ +package ghmcp + +import ( + "context" + "fmt" + "io" + "log" + "os" + "os/signal" + "syscall" + + "github.com/github/github-mcp-server/pkg/github" + mcplog "github.com/github/github-mcp-server/pkg/log" + "github.com/github/github-mcp-server/pkg/translations" + gogithub "github.com/google/go-github/v69/github" + "github.com/mark3labs/mcp-go/mcp" + + "github.com/mark3labs/mcp-go/server" + "github.com/sirupsen/logrus" +) + +type MCPServerConfig struct { + // Version of the server + Version string + + // GitHub Host to target for API requests (e.g. github.com or github.enterprise.com) + Host string + + // GitHub Token to authenticate with the GitHub API + Token string + + // EnabledToolsets is a list of toolsets to enable + // See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration + EnabledToolsets []string + + // Whether to enable dynamic toolsets + // See: https://github.com/github/github-mcp-server?tab=readme-ov-file#dynamic-tool-discovery + DynamicToolsets bool + + // ReadOnly indicates if we should only offer read-only tools + ReadOnly bool + + // Translator provides translated text for the server tooling + Translator translations.TranslationHelperFunc +} + +func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) { + ghClient := gogithub.NewClient(nil).WithAuthToken(cfg.Token) + ghClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", cfg.Version) + + if cfg.Host != "" { + var err error + ghClient, err = ghClient.WithEnterpriseURLs(cfg.Host, cfg.Host) + if err != nil { + return nil, fmt.Errorf("failed to create GitHub client with host: %w", err) + } + } + + // When a client send an initialize request, update the user agent to include the client info. + beforeInit := func(_ context.Context, _ any, message *mcp.InitializeRequest) { + ghClient.UserAgent = fmt.Sprintf( + "github-mcp-server/%s (%s/%s)", + cfg.Version, + message.Params.ClientInfo.Name, + message.Params.ClientInfo.Version, + ) + } + + hooks := &server.Hooks{ + OnBeforeInitialize: []server.OnBeforeInitializeFunc{beforeInit}, + } + + ghServer := github.NewServer(cfg.Version, server.WithHooks(hooks)) + + enabledToolsets := cfg.EnabledToolsets + if cfg.DynamicToolsets { + // filter "all" from the enabled toolsets + enabledToolsets = make([]string, 0, len(cfg.EnabledToolsets)) + for _, toolset := range cfg.EnabledToolsets { + if toolset != "all" { + enabledToolsets = append(enabledToolsets, toolset) + } + } + } + + getClient := func(_ context.Context) (*gogithub.Client, error) { + return ghClient, nil // closing over client + } + + // Create default toolsets + toolsets, err := github.InitToolsets( + enabledToolsets, + cfg.ReadOnly, + getClient, + cfg.Translator, + ) + if err != nil { + return nil, fmt.Errorf("failed to initialize toolsets: %w", err) + } + + context := github.InitContextToolset(getClient, cfg.Translator) + github.RegisterResources(ghServer, getClient, cfg.Translator) + + // Register the tools with the server + toolsets.RegisterTools(ghServer) + context.RegisterTools(ghServer) + + if cfg.DynamicToolsets { + dynamic := github.InitDynamicToolset(ghServer, toolsets, cfg.Translator) + dynamic.RegisterTools(ghServer) + } + + return ghServer, nil +} + +type StdioServerConfig struct { + // Version of the server + Version string + + // GitHub Host to target for API requests (e.g. github.com or github.enterprise.com) + Host string + + // GitHub Token to authenticate with the GitHub API + Token string + + // EnabledToolsets is a list of toolsets to enable + // See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration + EnabledToolsets []string + + // Whether to enable dynamic toolsets + // See: https://github.com/github/github-mcp-server?tab=readme-ov-file#dynamic-tool-discovery + DynamicToolsets bool + + // ReadOnly indicates if we should only register read-only tools + ReadOnly bool + + // ExportTranslations indicates if we should export translations + // See: https://github.com/github/github-mcp-server?tab=readme-ov-file#i18n--overriding-descriptions + ExportTranslations bool + + // EnableCommandLogging indicates if we should log commands + EnableCommandLogging bool + + // Path to the log file if not stderr + LogFilePath string +} + +// RunStdioServer is not concurrent safe. +func RunStdioServer(cfg StdioServerConfig) error { + // Create app context + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + t, dumpTranslations := translations.TranslationHelper() + + ghServer, err := NewMCPServer(MCPServerConfig{ + Version: cfg.Version, + Host: cfg.Host, + Token: cfg.Token, + EnabledToolsets: cfg.EnabledToolsets, + DynamicToolsets: cfg.DynamicToolsets, + ReadOnly: cfg.ReadOnly, + Translator: t, + }) + if err != nil { + return fmt.Errorf("failed to create MCP server: %w", err) + } + + stdioServer := server.NewStdioServer(ghServer) + + logrusLogger := logrus.New() + if cfg.LogFilePath != "" { + file, err := os.OpenFile(cfg.LogFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + return fmt.Errorf("failed to open log file: %w", err) + } + + logrusLogger.SetLevel(logrus.DebugLevel) + logrusLogger.SetOutput(file) + } + stdLogger := log.New(logrusLogger.Writer(), "stdioserver", 0) + stdioServer.SetErrorLogger(stdLogger) + + if cfg.ExportTranslations { + // Once server is initialized, all translations are loaded + dumpTranslations() + } + + // Start listening for messages + errC := make(chan error, 1) + go func() { + in, out := io.Reader(os.Stdin), io.Writer(os.Stdout) + + if cfg.EnableCommandLogging { + loggedIO := mcplog.NewIOLogger(in, out, logrusLogger) + in, out = loggedIO, loggedIO + } + + errC <- stdioServer.Listen(ctx, in, out) + }() + + // Output github-mcp-server string + _, _ = fmt.Fprintf(os.Stderr, "GitHub MCP Server running on stdio\n") + + // Wait for shutdown signal + select { + case <-ctx.Done(): + logrusLogger.Infof("shutting down server...") + case err := <-errC: + if err != nil { + return fmt.Errorf("error running server: %w", err) + } + } + + return nil +} From bd1df79a20e2dff6f927b7bff473a830f4b2e819 Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 7 May 2025 13:55:09 +0200 Subject: [PATCH 2/2] Support breakpoint debugging e2e tests --- e2e/README.md | 6 +++ e2e/e2e_test.go | 139 +++++++++++++++++++++++++++++------------------- 2 files changed, 89 insertions(+), 56 deletions(-) diff --git a/e2e/README.md b/e2e/README.md index bb93b32c..82de966b 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -77,6 +77,12 @@ FAIL github.com/github/github-mcp-server/e2e 1.433s FAIL ``` +## Debugging the Tests + +It is possible to provide `GITHUB_MCP_SERVER_E2E_DEBUG=true` to run the e2e tests with an in-process version of the MCP server. This has slightly reduced coverage as it doesn't integrate with Docker, or make use of the cobra/viper configuration parsing. However, it allows for placing breakpoints in the MCP Server internals, supporting much better debugging flows than the fully black-box tests. + +One might argue that the lack of visibility into failures for the black box tests also indicates a product need, but this solves for the immediate pain point felt as a maintainer. + ## Limitations The current test suite is intentionally very limited in scope. This is because the maintenance costs on e2e tests tend to increase significantly over time. To read about some challenges with GitHub integration tests, see [go-github integration tests README](https://github.com/google/go-github/blob/5b75aa86dba5cf4af2923afa0938774f37fa0a67/test/README.md). We will expand this suite circumspectly! diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index 5da6379c..b6637191 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -9,11 +9,15 @@ import ( "os" "os/exec" "slices" + "strings" "sync" "testing" "time" - "github.com/google/go-github/v69/github" + "github.com/github/github-mcp-server/internal/ghmcp" + "github.com/github/github-mcp-server/pkg/github" + "github.com/github/github-mcp-server/pkg/translations" + gogithub "github.com/google/go-github/v69/github" mcpClient "github.com/mark3labs/mcp-go/client" "github.com/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/require" @@ -56,68 +60,91 @@ func ensureDockerImageBuilt(t *testing.T) { require.NoError(t, buildError, "expected to build Docker image successfully") } -// ClientOpts holds configuration options for the MCP client setup -type ClientOpts struct { - // Environment variables to set before starting the client - EnvVars map[string]string +// clientOpts holds configuration options for the MCP client setup +type clientOpts struct { + // Toolsets to enable in the MCP server + enabledToolsets []string } -// ClientOption defines a function type for configuring ClientOpts -type ClientOption func(*ClientOpts) +// clientOption defines a function type for configuring ClientOpts +type clientOption func(*clientOpts) -// WithEnvVars returns an option that adds environment variables to the client options -func WithEnvVars(envVars map[string]string) ClientOption { - return func(opts *ClientOpts) { - opts.EnvVars = envVars +// withToolsets returns an option that either sets an Env Var when executing in docker, +// or sets the toolsets in the MCP server when running in-process. +func withToolsets(toolsets []string) clientOption { + return func(opts *clientOpts) { + opts.enabledToolsets = toolsets } } -// setupMCPClient sets up the test environment and returns an initialized MCP client -// It handles token retrieval, Docker image building, and applying the provided options -func setupMCPClient(t *testing.T, options ...ClientOption) *mcpClient.Client { +func setupMCPClient(t *testing.T, options ...clientOption) *mcpClient.Client { // Get token and ensure Docker image is built token := getE2EToken(t) - ensureDockerImageBuilt(t) // Create and configure options - opts := &ClientOpts{ - EnvVars: make(map[string]string), - } + opts := &clientOpts{} // Apply all options to configure the opts struct for _, option := range options { option(opts) } - // Prepare Docker arguments - args := []string{ - "docker", - "run", - "-i", - "--rm", - "-e", - "GITHUB_PERSONAL_ACCESS_TOKEN", // Personal access token is all required - } + // By default, we run the tests including the Docker image, but with DEBUG + // enabled, we run the server in-process, allowing for easier debugging. + var client *mcpClient.Client + if os.Getenv("GITHUB_MCP_SERVER_E2E_DEBUG") == "" { + ensureDockerImageBuilt(t) + + // Prepare Docker arguments + args := []string{ + "docker", + "run", + "-i", + "--rm", + "-e", + "GITHUB_PERSONAL_ACCESS_TOKEN", // Personal access token is all required + } - // Add all environment variables to the Docker arguments - for key := range opts.EnvVars { - args = append(args, "-e", key) - } + // Add toolsets environment variable to the Docker arguments + if len(opts.enabledToolsets) > 0 { + args = append(args, "-e", "GITHUB_TOOLSETS") + } + + // Add the image name + args = append(args, "github/e2e-github-mcp-server") - // Add the image name - args = append(args, "github/e2e-github-mcp-server") + // Construct the env vars for the MCP Client to execute docker with + dockerEnvVars := []string{ + fmt.Sprintf("GITHUB_PERSONAL_ACCESS_TOKEN=%s", token), + fmt.Sprintf("GITHUB_TOOLSETS=%s", strings.Join(opts.enabledToolsets, ",")), + } - // Construct the env vars for the MCP Client to execute docker with - dockerEnvVars := make([]string, 0, len(opts.EnvVars)+1) - dockerEnvVars = append(dockerEnvVars, fmt.Sprintf("GITHUB_PERSONAL_ACCESS_TOKEN=%s", token)) - for key, value := range opts.EnvVars { - dockerEnvVars = append(dockerEnvVars, fmt.Sprintf("%s=%s", key, value)) + // Create the client + t.Log("Starting Stdio MCP client...") + var err error + client, err = mcpClient.NewStdioMCPClient(args[0], dockerEnvVars, args[1:]...) + require.NoError(t, err, "expected to create client successfully") + } else { + // We need this because the fully compiled server has a default for the viper config, which is + // not in scope for using the MCP server directly. This probably indicates that we should refactor + // so that there is a shared setup mechanism, but let's wait till we feel more friction. + enabledToolsets := opts.enabledToolsets + if enabledToolsets == nil { + enabledToolsets = github.DefaultTools + } + + ghServer, err := ghmcp.NewMCPServer(ghmcp.MCPServerConfig{ + Token: token, + EnabledToolsets: enabledToolsets, + Translator: translations.NullTranslationHelper, + }) + require.NoError(t, err, "expected to construct MCP server successfully") + + t.Log("Starting In Process MCP client...") + client, err = mcpClient.NewInProcessClient(ghServer) + require.NoError(t, err, "expected to create in-process client successfully") } - // Create the client - t.Log("Starting Stdio MCP client...") - client, err := mcpClient.NewStdioMCPClient(args[0], dockerEnvVars, args[1:]...) - require.NoError(t, err, "expected to create client successfully") t.Cleanup(func() { require.NoError(t, client.Close(), "expected to close client successfully") }) @@ -169,7 +196,7 @@ func TestGetMe(t *testing.T) { // Then the login in the response should match the login obtained via the same // token using the GitHub API. - ghClient := github.NewClient(nil).WithAuthToken(getE2EToken(t)) + ghClient := gogithub.NewClient(nil).WithAuthToken(getE2EToken(t)) user, _, err := ghClient.Users.Get(context.Background(), "") require.NoError(t, err, "expected to get user successfully") require.Equal(t, trimmedContent.Login, *user.Login, "expected login to match") @@ -181,9 +208,7 @@ func TestToolsets(t *testing.T) { mcpClient := setupMCPClient( t, - WithEnvVars(map[string]string{ - "GITHUB_TOOLSETS": "repos,issues", - }), + withToolsets([]string{"repos", "issues"}), ) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) @@ -208,6 +233,8 @@ func TestToolsets(t *testing.T) { } func TestTags(t *testing.T) { + t.Parallel() + mcpClient := setupMCPClient(t) ctx := context.Background() @@ -253,7 +280,7 @@ func TestTags(t *testing.T) { // Cleanup the repository after the test t.Cleanup(func() { // MCP Server doesn't support deletions, but we can use the GitHub Client - ghClient := github.NewClient(nil).WithAuthToken(getE2EToken(t)) + ghClient := gogithub.NewClient(nil).WithAuthToken(getE2EToken(t)) t.Logf("Deleting repository %s/%s...", currentOwner, repoName) _, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName) require.NoError(t, err, "expected to delete repository successfully") @@ -261,24 +288,24 @@ func TestTags(t *testing.T) { // Then create a tag // MCP Server doesn't support tag creation, but we can use the GitHub Client - ghClient := github.NewClient(nil).WithAuthToken(getE2EToken(t)) + ghClient := gogithub.NewClient(nil).WithAuthToken(getE2EToken(t)) t.Logf("Creating tag %s/%s:%s...", currentOwner, repoName, "v0.0.1") ref, _, err := ghClient.Git.GetRef(context.Background(), currentOwner, repoName, "refs/heads/main") require.NoError(t, err, "expected to get ref successfully") - tagObj, _, err := ghClient.Git.CreateTag(context.Background(), currentOwner, repoName, &github.Tag{ - Tag: github.Ptr("v0.0.1"), - Message: github.Ptr("v0.0.1"), - Object: &github.GitObject{ + tagObj, _, err := ghClient.Git.CreateTag(context.Background(), currentOwner, repoName, &gogithub.Tag{ + Tag: gogithub.Ptr("v0.0.1"), + Message: gogithub.Ptr("v0.0.1"), + Object: &gogithub.GitObject{ SHA: ref.Object.SHA, - Type: github.Ptr("commit"), + Type: gogithub.Ptr("commit"), }, }) require.NoError(t, err, "expected to create tag object successfully") - _, _, err = ghClient.Git.CreateRef(context.Background(), currentOwner, repoName, &github.Reference{ - Ref: github.Ptr("refs/tags/v0.0.1"), - Object: &github.GitObject{ + _, _, err = ghClient.Git.CreateRef(context.Background(), currentOwner, repoName, &gogithub.Reference{ + Ref: gogithub.Ptr("refs/tags/v0.0.1"), + Object: &gogithub.GitObject{ SHA: tagObj.SHA, }, })