Skip to content

Commit

Permalink
Merge pull request #10 from dailymotion/repos-discovery
Browse files Browse the repository at this point in the history
feat: discover repos from github query or env var
  • Loading branch information
vbehar authored Jun 7, 2020
2 parents c2193d9 + e4b3994 commit 710ea8b
Show file tree
Hide file tree
Showing 21 changed files with 1,341 additions and 22 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/dsnet/compress v0.0.1 // indirect
github.com/golang/protobuf v1.3.5 // indirect
github.com/google/go-github/v28 v28.0.2
github.com/imdario/mergo v0.3.9
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/mholt/archiver v3.1.1+incompatible
github.com/mikefarah/yq/v3 v3.0.0-20200418141808-3ccd32a47e54
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKe
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0=
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
Expand Down
8 changes: 3 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ func init() {
}

func main() {
ctx := context.Background()
pflag.Parse()
printHelpOrVersion()
setLogLevel()
Expand All @@ -98,7 +99,7 @@ func main() {
logrus.WithField("updaters", updaters).Debug("Updaters ready")

logrus.WithField("repos", options.repos).Trace("Parsing repositories")
repositories, err := repository.Parse(options.repos)
repositories, err := repository.Parse(ctx, options.repos, options.GitHub.Token)
if err != nil {
logrus.
WithError(err).
Expand All @@ -108,10 +109,7 @@ func main() {
logrus.WithField("repositories", repositories).Debug("Repositories ready")

logrus.WithField("repositories-count", len(repositories)).Trace("Starting updates")
var (
ctx = context.Background()
wg sync.WaitGroup
)
var wg sync.WaitGroup
for _, repo := range repositories {
wg.Add(1)
go func(repo repository.Repository) {
Expand Down
42 changes: 42 additions & 0 deletions repository/discover_from_env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package repository

import (
"context"
"fmt"
"os"
"strings"

"github.com/imdario/mergo"
)

func discoverRepositoriesFromEnvironment(ctx context.Context, envVar string, params map[string]string, githubToken string) ([]Repository, error) {
separator := params["sep"]
if len(separator) == 0 {
separator = " "
}
delete(params, "sep")

envValue := os.Getenv(envVar)
repoNames := strings.Split(envValue, separator)

if len(repoNames) == 0 {
return nil, nil
}
if len(repoNames) == 1 && len(repoNames[0]) == 0 {
return nil, nil
}

repos, err := Parse(ctx, repoNames, githubToken)
if err != nil {
return nil, fmt.Errorf("failed to parse %v: %w", repoNames, err)
}

for i := range repos {
err = mergo.Merge(&repos[i].Params, params)
if err != nil {
return nil, fmt.Errorf("failed to merge params for repo %v: %w", repos[i], err)
}
}

return repos, nil
}
154 changes: 154 additions & 0 deletions repository/discover_from_env_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package repository

import (
"context"
"fmt"
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestDiscoverRepositoriesFromEnvironment(t *testing.T) {
t.Parallel()
tests := []struct {
name string
envVarValue string
params map[string]string
expected []Repository
expectedErrorMsg string
}{
{
name: "empty input",
},
{
name: "invalid input",
envVarValue: "whatever",
expectedErrorMsg: "failed to parse [whatever]: invalid syntax for whatever: found 0 matches instead of 4: []",
},
{
name: "single repository without parameters",
envVarValue: "dailymotion/octopilot",
expected: []Repository{
{
Owner: "dailymotion",
Name: "octopilot",
Params: map[string]string{},
},
},
},
{
name: "multiple repositories without parameters",
envVarValue: "dailymotion/octopilot some-owner/MyGreatRepo",
expected: []Repository{
{
Owner: "dailymotion",
Name: "octopilot",
Params: map[string]string{},
},
{
Owner: "some-owner",
Name: "MyGreatRepo",
Params: map[string]string{},
},
},
},
{
name: "multiple repositories with custom separator",
envVarValue: "dailymotion/octopilot|some-owner/MyGreatRepo",
params: map[string]string{
"sep": "|",
},
expected: []Repository{
{
Owner: "dailymotion",
Name: "octopilot",
Params: map[string]string{},
},
{
Owner: "some-owner",
Name: "MyGreatRepo",
Params: map[string]string{},
},
},
},
{
name: "single repository with a single parameter",
envVarValue: "dailymotion/octopilot(draft=true)",
expected: []Repository{
{
Owner: "dailymotion",
Name: "octopilot",
Params: map[string]string{
"draft": "true",
},
},
},
},
{
name: "multiple repositories with multiple parameters",
envVarValue: "dailymotion/octopilot(draft=true,merge=true) some-owner/MyGreatRepo(merge=false)",
expected: []Repository{
{
Owner: "dailymotion",
Name: "octopilot",
Params: map[string]string{
"draft": "true",
"merge": "true",
},
},
{
Owner: "some-owner",
Name: "MyGreatRepo",
Params: map[string]string{
"merge": "false",
},
},
},
},
{
name: "multiple repositories with multiple parameters with overrides",
envVarValue: "dailymotion/octopilot(draft=true,merge=false) some-owner/MyGreatRepo(draft=true)",
params: map[string]string{
"merge": "true",
},
expected: []Repository{
{
Owner: "dailymotion",
Name: "octopilot",
Params: map[string]string{
"draft": "true",
"merge": "false",
},
},
{
Owner: "some-owner",
Name: "MyGreatRepo",
Params: map[string]string{
"draft": "true",
"merge": "true",
},
},
},
},
}

for i := range tests {
test := tests[i]
t.Run(test.name, func(t *testing.T) {
envVar := fmt.Sprintf("DISCOVER_FROM_ENV_%v", i)
os.Setenv(envVar, test.envVarValue)
defer os.Unsetenv(envVar)

actual, err := discoverRepositoriesFromEnvironment(context.Background(), envVar, test.params, "")
if len(test.expectedErrorMsg) > 0 {
require.EqualError(t, err, test.expectedErrorMsg)
assert.Empty(t, actual)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, actual)
}
})
}
}
42 changes: 42 additions & 0 deletions repository/discover_from_query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package repository

import (
"context"
"fmt"

"github.com/google/go-github/v28/github"
)

func discoverRepositoriesFromQuery(ctx context.Context, query string, params map[string]string, githubToken string) ([]Repository, error) {
repos := []Repository{}
ghClient := githubClient(ctx, githubToken)

page := 1
for {
opts := &github.SearchOptions{
ListOptions: github.ListOptions{
Page: page,
PerPage: 100,
},
}
result, resp, err := ghClient.Search.Repositories(ctx, query, opts)
if err != nil {
return nil, fmt.Errorf("failed to list all repositories matching query %s on GitHub (page %d): %w", query, page, err)
}

for _, ghRepo := range result.Repositories {
repos = append(repos, Repository{
Owner: ghRepo.Owner.GetLogin(),
Name: ghRepo.GetName(),
Params: params,
})
}

page = resp.NextPage
if resp.NextPage == 0 {
break
}
}

return repos, nil
}
48 changes: 38 additions & 10 deletions repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ import (
)

var (
// type(params)
repoRegexp = regexp.MustCompile(`^(?P<type>[A-Za-z0-9_\-/]+)(?:\((?P<params>.+)\))?$`)

// owner/name(params)
repoRegexp = regexp.MustCompile(`^(?P<owner>[A-Za-z0-9_\-]+)/(?P<name>[A-Za-z0-9_\-]+)(?:\((?P<params>.+)\))?$`)
repoWithNameRegexp = regexp.MustCompile(`^(?P<owner>[A-Za-z0-9_\-]+)/(?P<name>[A-Za-z0-9_\-]+)(?:\((?P<params>.+)\))?$`)
)

type Repository struct {
Expand All @@ -25,25 +28,50 @@ type Repository struct {
Params map[string]string
}

func Parse(repos []string) ([]Repository, error) {
func Parse(ctx context.Context, repos []string, githubToken string) ([]Repository, error) {
var repositories []Repository
for _, repo := range repos {
matches := repoRegexp.FindStringSubmatch(repo)
if len(matches) < 4 {
return nil, fmt.Errorf("invalid syntax for %s: found %d matches instead of 4: %v", repo, len(matches), matches)
if len(matches) < 2 {
return nil, fmt.Errorf("invalid syntax for %s: missing repo type or name", repo)
}

r := Repository{
Owner: matches[1],
Name: matches[2],
Params: parameters.Parse(matches[3]),
}
switch matches[1] {
case "discover-from":
discoveredRepos, err := discoverRepositoriesFrom(ctx, parameters.Parse(matches[2]), githubToken)
if err != nil {
return nil, fmt.Errorf("failed to discover repositories: %w", err)
}
repositories = append(repositories, discoveredRepos...)
default:
matches := repoWithNameRegexp.FindStringSubmatch(repo)
if len(matches) < 4 {
return nil, fmt.Errorf("invalid syntax for %s: found %d matches instead of 4: %v", repo, len(matches), matches)
}

repositories = append(repositories, r)
repositories = append(repositories, Repository{
Owner: matches[1],
Name: matches[2],
Params: parameters.Parse(matches[3]),
})
}
}
return repositories, nil
}

func discoverRepositoriesFrom(ctx context.Context, params map[string]string, githubToken string) ([]Repository, error) {
if query, ok := params["query"]; ok {
return discoverRepositoriesFromQuery(ctx, query, params, githubToken)
}

if envVar, ok := params["env"]; ok {
delete(params, "env")
return discoverRepositoriesFromEnvironment(ctx, envVar, params, githubToken)
}

return nil, fmt.Errorf("can't discover repositories from params %v: missing either query or env param", params)
}

func (r Repository) Update(ctx context.Context, updaters []update.Updater, options UpdateOptions) (bool, error) {
r.adjustOptionsFromParams(&options)

Expand Down
Loading

0 comments on commit 710ea8b

Please sign in to comment.