Skip to content

Commit

Permalink
refactor: use our own Github client library (#73)
Browse files Browse the repository at this point in the history
Starting to slowly move away from `gobox`, first stop is using our own
Github client library. The folder structure is to allow us to support
other VCS providers if needed at some point.

I also removed `gitauth` because it appeared to be dead code.

Added a `cmdexec` package for the purposes of mocking command output.
  • Loading branch information
jaredallard committed May 31, 2024
1 parent eec1af4 commit 220bcf8
Show file tree
Hide file tree
Showing 17 changed files with 588 additions and 82 deletions.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/go-git/go-billy/v5 v5.5.0
github.com/go-git/go-git/v5 v5.12.0
github.com/google/go-cmp v0.6.0
github.com/google/go-github/v62 v62.0.0
github.com/hashicorp/go-hclog v1.6.3
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-plugin v1.6.1
Expand All @@ -25,6 +26,7 @@ require (
github.com/sirupsen/logrus v1.9.3
github.com/urfave/cli/v2 v2.27.2
github.com/whilp/git-urls v1.0.0
golang.org/x/oauth2 v0.20.0
golang.org/x/term v0.20.0
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.1
Expand Down Expand Up @@ -56,7 +58,6 @@ require (
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-github/v62 v62.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
Expand Down Expand Up @@ -101,7 +102,6 @@ require (
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/oauth2 v0.20.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
Expand Down
2 changes: 0 additions & 2 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 4 additions & 5 deletions internal/cmd/stencil/stencil.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/blang/semver/v4"
"github.com/charmbracelet/glamour"
"github.com/getoutreach/gobox/pkg/cfg"
"github.com/getoutreach/gobox/pkg/cli/github"
"github.com/pkg/errors"
"go.rgst.io/stencil/internal/codegen"
"go.rgst.io/stencil/internal/git/vcs/github"
"go.rgst.io/stencil/internal/modules"
"go.rgst.io/stencil/internal/slogext"
"go.rgst.io/stencil/pkg/configuration"
Expand Down Expand Up @@ -54,7 +53,7 @@ type Command struct {
allowMajorVersionUpgrades bool

// token is the github token used for fetching modules
token cfg.SecretData
token string
}

// NewCommand creates a new stencil command
Expand All @@ -64,7 +63,7 @@ func NewCommand(log slogext.Logger, s *configuration.Manifest,
if err != nil && !errors.Is(err, os.ErrNotExist) {
log.WithError(err).Warn("failed to load lockfile")
}
token, err := github.GetToken()
token, err := github.Token()
if err != nil {
log.Warn("failed to get github token, using anonymous access")
}
Expand Down Expand Up @@ -272,7 +271,7 @@ func (c *Command) promptMajorVersion(ctx context.Context, m *modules.Module, las
)
}

gh, err := github.NewClient(github.WithAllowUnauthenticated())
gh, err := github.New()
if err != nil {
return errors.Wrap(err, "failed to fetch release notes (create github client)")
}
Expand Down
33 changes: 33 additions & 0 deletions internal/git/vcs/github/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (C) 2024 stencil contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package github

import "errors"

// ErrNoToken is returned when no token is found in the configured
// credential providers.
type ErrNoToken struct {
errs []error
}

// Unwrap returns the errors that caused the ErrNoToken error.
func (e ErrNoToken) Unwrap() []error {
return e.errs
}

// Error returns the error message for ErrNoToken.
func (e ErrNoToken) Error() string {
return errors.Join(e.errs...).Error()
}
78 changes: 78 additions & 0 deletions internal/git/vcs/github/github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (C) 2024 stencil contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package github provides methods for creating a Github client for the
// purposes of interacting with the API. Provides support for retrieving
// authentication tokens from the following sources:
//
// - Environment variables ($GITHUB_TOKEN)
// - Github CLI
package github

import (
"context"
"net/http"

"github.com/google/go-github/v62/github"
"golang.org/x/oauth2"
)

// defaultProviders is a list of credential providers that are used to
// retrieve a token by default.
var defaultProviders = []provider{
&envProvider{},
&ghProvider{},
}

// Token returns a valid token from one of the configured credential
// providers. If no token is found, ErrNoToken is returned.
func Token() (string, error) {
token := ""
errors := []error{}
for _, p := range defaultProviders {
var err error
token, err = p.Token()
if err != nil {
errors = append(errors, err)
continue
}

// Got a token, break out of the loop.
if token != "" {
break
}
}
if token == "" {
return "", ErrNoToken{errors}
}
return token, nil
}

// New returns a new [github.Client] using credentials from one of the
// configured credential providers. If no token is found, an
// unauthenticated client is returned.
func New() (*github.Client, error) {
token, err := Token()
if err != nil {
return github.NewClient(http.DefaultClient), nil
}

// Note: background ctx is used here because we don't want the oauth2
// client to pick up credentials from a provided context.
return github.NewClient(oauth2.NewClient(context.Background(),
oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)),
), nil
}
67 changes: 67 additions & 0 deletions internal/git/vcs/github/github_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (C) 2024 stencil contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package github

import (
"errors"
"fmt"
"testing"

"go.rgst.io/stencil/internal/testing/cmdexec"
"golang.org/x/oauth2"
"gotest.tools/v3/assert"
)

// TestEnvOverGH ensures that the GITHUB_TOKEN environment variable
// takes precedence over the token returned by the gh CLI.
func TestEnvOverGH(t *testing.T) {
t.Setenv("GITHUB_TOKEN", "github_token")

cmdexec.UseMockExecutor(t, cmdexec.NewMockExecutor(&cmdexec.MockCommand{
Name: "gh",
Args: []string{"auth", "token"},
Stdout: []byte("gh_token\n"),
}))

cli, err := New()
assert.NilError(t, err)
token, err := cli.Client().Transport.(*oauth2.Transport).Source.Token()
assert.NilError(t, err)
assert.Equal(t, "github_token", token.AccessToken)
}

// TestReturnsErrors ensures that New returns underlying provider errors
// and that they can be found.
func TestReturnsErrors(t *testing.T) {
cmdexec.UseMockExecutor(t, cmdexec.NewMockExecutor(&cmdexec.MockCommand{
Name: "gh",
Args: []string{"auth", "token"},
Err: fmt.Errorf("bad things happened"),
}))

token, err := Token()
var tokenErr ErrNoToken
assert.Assert(t, errors.As(err, &tokenErr))
assert.Assert(t, token == "", "expected token to be empty")

// Find a GH cli error
var found bool
for _, e := range tokenErr.errs {
if e.Error() == "gh failed: bad things happened (no stderr)" {
found = true
}
}
assert.Assert(t, found, "expected error not found")
}
36 changes: 36 additions & 0 deletions internal/git/vcs/github/provider_env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (C) 2024 stencil contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package github

import (
"fmt"
"os"
)

// envProvider implements the provider interface using the environment
// variables to retrieve a token.
type envProvider struct{}

// Token returns a valid token or an error if no token is found.
func (p *envProvider) Token() (string, error) {
envVars := []string{"GITHUB_TOKEN"}
for _, env := range envVars {
if token := os.Getenv(env); token != "" {
return token, nil
}
}

return "", fmt.Errorf("no token found in environment variables: %v", envVars)
}
30 changes: 30 additions & 0 deletions internal/git/vcs/github/provider_env_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (C) 2024 stencil contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package github

import (
"testing"

"gotest.tools/v3/assert"
)

func TestEnvProviderReadsCorrectEnvVar(t *testing.T) {
t.Setenv("GITHUB_TOKEN", "token")

p := &envProvider{}
token, err := p.Token()
assert.NilError(t, err)
assert.Equal(t, "token", token)
}
44 changes: 44 additions & 0 deletions internal/git/vcs/github/provider_gh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (C) 2024 stencil contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package github

import (
"errors"
"fmt"
"os/exec"
"strings"

"go.rgst.io/stencil/internal/testing/cmdexec"
)

// ghProvider implements the provider interface using the Github CLI to
// retrieve a token.
type ghProvider struct{}

// Token returns a valid token or an error if no token is found.
func (p *ghProvider) Token() (string, error) {
cmd := cmdexec.Command("gh", "auth", "token")
token, err := cmd.Output()
if err != nil {
var execErr *exec.ExitError
if errors.As(err, &execErr) {
return "", fmt.Errorf("gh failed: %s (%w)", string(execErr.Stderr), execErr)
}

return "", fmt.Errorf("gh failed: %w (no stderr)", err)
}

return strings.TrimSpace(string(token)), nil
}
Loading

0 comments on commit 220bcf8

Please sign in to comment.