Repository: lindell/multi-gitter
Pull request #320 (2023-02-02 08:52:59)

feat: filter repositories using topics
# What does this change
feat: filter github repositories using topics

# What issue does it fix
We have hundreds of repositories and targeting them all or providing a list of repositories is annoying to do so.
It would be nicer just to target repos filtered by topic name. 
For example I'd like to add a fix targeting all repos labeled backend.

# Checklist
- [x] Made sure the PR follows the [CONTRIBUTING.md](CONTRIBUTING.md) guidelines
- [x] Tests if something new is added


raffis: >     1. Are topics supported on other platforms? Should be an easy implementation in that case, and I'm happy to test it out if needed.

I did not check yet as I don' use any other platform atm. But might have a look if I find the time.


>     2. Multiple topics to be filtered are supported in the current implementation, which should be the case. But I'm unsure if it should be that all topics specified must exist, or if any of the topics has to exist. (The latter is implemented). Any input here? :)

I asked myself the same question. I came to the conclusion that it makes more sense to match if any exists. Although this might be a matter of taste. But it should not get complicated for a user to configure it so I went with the later. Might just be a matter of documentation.
lindell: Taking a quick look, it seems that topic exist in:

✔️ GitHub
✔️ GitLab
❌ BitBucket server
✔️ Gitea

If you are unfamiliar with GitLab and GitLab, I could add it in this PR.
raffis: > Taking a quick look, it seems that topic exist in:
> 
> heavy_check_mark GitHub heavy_check_mark GitLab x BitBucket server heavy_check_mark Gitea
> 

I will have a look 👍🏻 


raffis: Added support for gitlab and gita. Although I doubt topics is a widely used feature in gitea since I didn't even see the option in the ui to set topics.
Will add a story test once everything else is confirmed.

Patch:
diff --git a/.github/workflows/readme.yaml b/.github/workflows/readme.yaml
index 12b4975..13397da 100755
--- a/.github/workflows/readme.yaml
+++ b/.github/workflows/readme.yaml
@@ -26,10 +26,11 @@ jobs:
         continue-on-error: true
         run: |
           git checkout master
-          git config user.email "johan@lindell.me"
-          git config user.name "Johan Lindell"
+          git config user.email "github-actions[bot]@users.noreply.github.com"
+          git config user.name "github-actions[bot]"
           git add README.md
-          git commit -m "docs: updated readme"
+          COAUTHOR=`git log -1 --pretty=format:'Co-authored-by: %an <%ae>'`
+          git commit -m "docs: updated readme" -m "$COAUTHOR"
 
       - name: Push changes
         if: ${{ success() }}
diff --git a/README.md b/README.md
index 5838995..7ec5c97 100755
--- a/README.md
+++ b/README.md
@@ -112,7 +112,7 @@ author-name:
 # The branch which the changes will be based on.
 base-branch:
 
-# Base URL of the GitHub API, needs to be changed if GitHub enterprise is used. Or the url to a self-hosted GitLab instance.
+# Base URL of the target platform, needs to be changed for GitHub enterprise, a self-hosted GitLab instance, Gitea or BitBucket.
 base-url:
 
 # The name of the branch where changes are committed.
@@ -219,7 +219,7 @@ skip-repo:
 # Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.
 ssh-auth: false
 
-# The GitHub/GitLab personal access token. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
+# The personal access token for the targeting platform. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
 token:
 
 # The name of a user. All repositories owned by that user will be used.
@@ -236,7 +236,7 @@ username:
   <summary>All available merge options</summary>
 
 ```yaml
-# Base URL of the GitHub API, needs to be changed if GitHub enterprise is used. Or the url to a self-hosted GitLab instance.
+# Base URL of the target platform, needs to be changed for GitHub enterprise, a self-hosted GitLab instance, Gitea or BitBucket.
 base-url:
 
 # The name of the branch where changes are committed.
@@ -292,7 +292,7 @@ repo:
 # Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.
 ssh-auth: false
 
-# The GitHub/GitLab personal access token. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
+# The personal access token for the targeting platform. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
 token:
 
 # The name of a user. All repositories owned by that user will be used.
@@ -309,7 +309,7 @@ username:
   <summary>All available status options</summary>
 
 ```yaml
-# Base URL of the GitHub API, needs to be changed if GitHub enterprise is used. Or the url to a self-hosted GitLab instance.
+# Base URL of the target platform, needs to be changed for GitHub enterprise, a self-hosted GitLab instance, Gitea or BitBucket.
 base-url:
 
 # The name of the branch where changes are committed.
@@ -362,7 +362,7 @@ repo:
 # Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.
 ssh-auth: false
 
-# The GitHub/GitLab personal access token. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
+# The personal access token for the targeting platform. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
 token:
 
 # The name of a user. All repositories owned by that user will be used.
@@ -379,7 +379,7 @@ username:
   <summary>All available close options</summary>
 
 ```yaml
-# Base URL of the GitHub API, needs to be changed if GitHub enterprise is used. Or the url to a self-hosted GitLab instance.
+# Base URL of the target platform, needs to be changed for GitHub enterprise, a self-hosted GitLab instance, Gitea or BitBucket.
 base-url:
 
 # The name of the branch where changes are committed.
@@ -429,7 +429,7 @@ repo:
 # Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.
 ssh-auth: false
 
-# The GitHub/GitLab personal access token. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
+# The personal access token for the targeting platform. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
 token:
 
 # The name of a user. All repositories owned by that user will be used.
@@ -446,7 +446,7 @@ username:
   <summary>All available print options</summary>
 
 ```yaml
-# Base URL of the GitHub API, needs to be changed if GitHub enterprise is used. Or the url to a self-hosted GitLab instance.
+# Base URL of the target platform, needs to be changed for GitHub enterprise, a self-hosted GitLab instance, Gitea or BitBucket.
 base-url:
 
 # The maximum number of concurrent runs.
@@ -505,7 +505,7 @@ repo:
 # Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.
 ssh-auth: false
 
-# The GitHub/GitLab personal access token. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
+# The personal access token for the targeting platform. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
 token:
 
 # The name of a user. All repositories owned by that user will be used.
@@ -542,7 +542,7 @@ Flags:
       --author-email string        Email of the committer. If not set, the global git config setting will be used.
       --author-name string         Name of the committer. If not set, the global git config setting will be used.
       --base-branch string         The branch which the changes will be based on.
-  -g, --base-url string            Base URL of the GitHub API, needs to be changed if GitHub enterprise is used. Or the url to a self-hosted GitLab instance.
+  -g, --base-url string            Base URL of the target platform, needs to be changed for GitHub enterprise, a self-hosted GitLab instance, Gitea or BitBucket.
   -B, --branch string              The name of the branch where changes are committed. (default "multi-gitter-branch")
   -m, --commit-message string      The commit message. Will default to title + body if none is set.
   -C, --concurrent int             The maximum number of concurrent runs. (default 1)
@@ -582,7 +582,7 @@ Flags:
       --skip-pr                    Skip pull request and directly push to the branch.
   -s, --skip-repo strings          Skip changes on specified repositories, the name is including the owner of repository in the format "ownerName/repoName".
       --ssh-auth                   Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.
-  -T, --token string               The GitHub/GitLab personal access token. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
+  -T, --token string               The personal access token for the targeting platform. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
   -U, --user strings               The name of a user. All repositories owned by that user will be used.
   -u, --username string            The Bitbucket server username.
 ```
@@ -595,7 +595,7 @@ Usage:
   multi-gitter merge [flags]
 
 Flags:
-  -g, --base-url string      Base URL of the GitHub API, needs to be changed if GitHub enterprise is used. Or the url to a self-hosted GitLab instance.
+  -g, --base-url string      Base URL of the target platform, needs to be changed for GitHub enterprise, a self-hosted GitLab instance, Gitea or BitBucket.
   -B, --branch string        The name of the branch where changes are committed. (default "multi-gitter-branch")
       --config string        Path of the config file.
       --fork                 Use pull requests made from forks instead of from the same repository.
@@ -612,7 +612,7 @@ Flags:
   -P, --project strings      The name, including owner of a GitLab project in the format "ownerName/repoName".
   -R, --repo strings         The name, including owner of a GitHub repository in the format "ownerName/repoName".
       --ssh-auth             Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.
-  -T, --token string         The GitHub/GitLab personal access token. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
+  -T, --token string         The personal access token for the targeting platform. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
   -U, --user strings         The name of a user. All repositories owned by that user will be used.
   -u, --username string      The Bitbucket server username.
 ```
@@ -625,7 +625,7 @@ Usage:
   multi-gitter status [flags]
 
 Flags:
-  -g, --base-url string     Base URL of the GitHub API, needs to be changed if GitHub enterprise is used. Or the url to a self-hosted GitLab instance.
+  -g, --base-url string     Base URL of the target platform, needs to be changed for GitHub enterprise, a self-hosted GitLab instance, Gitea or BitBucket.
   -B, --branch string       The name of the branch where changes are committed. (default "multi-gitter-branch")
       --config string       Path of the config file.
       --fork                Use pull requests made from forks instead of from the same repository.
@@ -642,7 +642,7 @@ Flags:
   -P, --project strings     The name, including owner of a GitLab project in the format "ownerName/repoName".
   -R, --repo strings        The name, including owner of a GitHub repository in the format "ownerName/repoName".
       --ssh-auth            Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.
-  -T, --token string        The GitHub/GitLab personal access token. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
+  -T, --token string        The personal access token for the targeting platform. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
   -U, --user strings        The name of a user. All repositories owned by that user will be used.
   -u, --username string     The Bitbucket server username.
 ```
@@ -655,7 +655,7 @@ Usage:
   multi-gitter close [flags]
 
 Flags:
-  -g, --base-url string     Base URL of the GitHub API, needs to be changed if GitHub enterprise is used. Or the url to a self-hosted GitLab instance.
+  -g, --base-url string     Base URL of the target platform, needs to be changed for GitHub enterprise, a self-hosted GitLab instance, Gitea or BitBucket.
   -B, --branch string       The name of the branch where changes are committed. (default "multi-gitter-branch")
       --config string       Path of the config file.
       --fork                Use pull requests made from forks instead of from the same repository.
@@ -671,7 +671,7 @@ Flags:
   -P, --project strings     The name, including owner of a GitLab project in the format "ownerName/repoName".
   -R, --repo strings        The name, including owner of a GitHub repository in the format "ownerName/repoName".
       --ssh-auth            Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.
-  -T, --token string        The GitHub/GitLab personal access token. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
+  -T, --token string        The personal access token for the targeting platform. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
   -U, --user strings        The name of a user. All repositories owned by that user will be used.
   -u, --username string     The Bitbucket server username.
 ```
@@ -688,7 +688,7 @@ Usage:
   multi-gitter print [script path] [flags]
 
 Flags:
-  -g, --base-url string       Base URL of the GitHub API, needs to be changed if GitHub enterprise is used. Or the url to a self-hosted GitLab instance.
+  -g, --base-url string       Base URL of the target platform, needs to be changed for GitHub enterprise, a self-hosted GitLab instance, Gitea or BitBucket.
   -C, --concurrent int        The maximum number of concurrent runs. (default 1)
       --config string         Path of the config file.
   -E, --error-output string   The file that the output of the script should be outputted to. "-" means stderr. (default "-")
@@ -710,7 +710,7 @@ Flags:
   -P, --project strings       The name, including owner of a GitLab project in the format "ownerName/repoName".
   -R, --repo strings          The name, including owner of a GitHub repository in the format "ownerName/repoName".
       --ssh-auth              Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.
-  -T, --token string          The GitHub/GitLab personal access token. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
+  -T, --token string          The personal access token for the targeting platform. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.
   -U, --user strings          The name of a user. All repositories owned by that user will be used.
   -u, --username string       The Bitbucket server username.
 ```
diff --git a/cmd/platform.go b/cmd/platform.go
index 48edf6c..7e408f1 100644
--- a/cmd/platform.go
+++ b/cmd/platform.go
@@ -18,15 +18,16 @@ import (
 func configurePlatform(cmd *cobra.Command) {
 	flags := cmd.Flags()
 
-	flags.StringP("base-url", "g", "", "Base URL of the GitHub API, needs to be changed if GitHub enterprise is used. Or the url to a self-hosted GitLab instance.")
+	flags.StringP("base-url", "g", "", "Base URL of the target platform, needs to be changed for GitHub enterprise, a self-hosted GitLab instance, Gitea or BitBucket.")
 	flags.BoolP("insecure", "", false, "Insecure controls whether a client verifies the server certificate chain and host name. Used only for Bitbucket server.")
 	flags.StringP("username", "u", "", "The Bitbucket server username.")
-	flags.StringP("token", "T", "", "The GitHub/GitLab personal access token. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.")
+	flags.StringP("token", "T", "", "The personal access token for the targeting platform. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN environment variable.")
 
 	flags.StringSliceP("org", "O", nil, "The name of a GitHub organization. All repositories in that organization will be used.")
 	flags.StringSliceP("group", "G", nil, "The name of a GitLab organization. All repositories in that group will be used.")
 	flags.StringSliceP("user", "U", nil, "The name of a user. All repositories owned by that user will be used.")
 	flags.StringSliceP("repo", "R", nil, "The name, including owner of a GitHub repository in the format \"ownerName/repoName\".")
+	flags.StringSliceP("topic", "", nil, "The topic of a GitHub/GitLab/Gitea repository. All repositories having at least one matching topic are targeted.")
 	flags.StringSliceP("project", "P", nil, "The name, including owner of a GitLab project in the format \"ownerName/repoName\".")
 	flags.BoolP("include-subgroups", "", false, "Include GitLab subgroups when using the --group flag.")
 	flags.BoolP("ssh-auth", "", false, `Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.`)
@@ -119,6 +120,7 @@ func createGithubClient(flag *flag.FlagSet, verifyFlags bool, readOnly bool) (mu
 	orgs, _ := flag.GetStringSlice("org")
 	users, _ := flag.GetStringSlice("user")
 	repos, _ := flag.GetStringSlice("repo")
+	topics, _ := flag.GetStringSlice("topic")
 	forkMode, _ := flag.GetBool("fork")
 	forkOwner, _ := flag.GetString("fork-owner")
 	sshAuth, _ := flag.GetBool("ssh-auth")
@@ -149,6 +151,7 @@ func createGithubClient(flag *flag.FlagSet, verifyFlags bool, readOnly bool) (mu
 		Organizations: orgs,
 		Users:         users,
 		Repositories:  repoRefs,
+		Topics:        topics,
 	}, mergeTypes, forkMode, forkOwner, sshAuth, readOnly)
 	if err != nil {
 		return nil, err
@@ -162,6 +165,7 @@ func createGitlabClient(flag *flag.FlagSet, verifyFlags bool) (multigitter.Versi
 	groups, _ := flag.GetStringSlice("group")
 	users, _ := flag.GetStringSlice("user")
 	projects, _ := flag.GetStringSlice("project")
+	topics, _ := flag.GetStringSlice("topic")
 	includeSubgroups, _ := flag.GetBool("include-subgroups")
 	sshAuth, _ := flag.GetBool("ssh-auth")
 
@@ -186,6 +190,7 @@ func createGitlabClient(flag *flag.FlagSet, verifyFlags bool) (multigitter.Versi
 		Groups:   groups,
 		Users:    users,
 		Projects: projRefs,
+		Topics:   topics,
 	}, gitlab.Config{
 		IncludeSubgroups: includeSubgroups,
 		SSHAuth:          sshAuth,
@@ -202,6 +207,7 @@ func createGiteaClient(flag *flag.FlagSet, verifyFlags bool) (multigitter.Versio
 	orgs, _ := flag.GetStringSlice("org")
 	users, _ := flag.GetStringSlice("user")
 	repos, _ := flag.GetStringSlice("repo")
+	topics, _ := flag.GetStringSlice("topic")
 	sshAuth, _ := flag.GetBool("ssh-auth")
 
 	if verifyFlags && len(orgs) == 0 && len(users) == 0 && len(repos) == 0 {
@@ -234,6 +240,7 @@ func createGiteaClient(flag *flag.FlagSet, verifyFlags bool) (multigitter.Versio
 		Organizations: orgs,
 		Users:         users,
 		Repositories:  repoRefs,
+		Topics:        topics,
 	}, mergeTypes, sshAuth)
 	if err != nil {
 		return nil, err
diff --git a/internal/scm/gitea/gitea.go b/internal/scm/gitea/gitea.go
index 40e8045..69739a4 100644
--- a/internal/scm/gitea/gitea.go
+++ b/internal/scm/gitea/gitea.go
@@ -10,6 +10,7 @@ import (
 	"code.gitea.io/sdk/gitea"
 	"github.com/lindell/multi-gitter/internal/scm"
 	"github.com/pkg/errors"
+	log "github.com/sirupsen/logrus"
 
 	internalHTTP "github.com/lindell/multi-gitter/internal/http"
 )
@@ -67,6 +68,7 @@ type RepositoryListing struct {
 	Organizations []string
 	Users         []string
 	Repositories  []RepositoryReference
+	Topics        []string
 }
 
 // RepositoryReference contains information to be able to reference a repository
@@ -96,6 +98,20 @@ func (g *Gitea) GetRepositories(ctx context.Context) ([]scm.Repository, error) {
 
 	repos := make([]scm.Repository, 0, len(allRepos))
 	for _, repo := range allRepos {
+		log := log.WithField("repo", repo.FullName)
+
+		if len(g.Topics) != 0 {
+			topics, err := g.getRepoTopics(ctx, repo)
+			if err != nil {
+				return repos, fmt.Errorf("could not fetch repository topics: %w", err)
+			}
+
+			if !scm.RepoContainsTopic(topics, g.Topics) {
+				log.Debug("Skipping repository since it does not match repository topics")
+				continue
+			}
+		}
+
 		convertedRepo, err := g.convertRepository(repo)
 		if err != nil {
 			return nil, err
@@ -106,6 +122,11 @@ func (g *Gitea) GetRepositories(ctx context.Context) ([]scm.Repository, error) {
 	return repos, nil
 }
 
+func (g *Gitea) getRepoTopics(ctx context.Context, repo *gitea.Repository) ([]string, error) {
+	topics, _, err := g.giteaClient(ctx).ListRepoTopics(repo.Owner.UserName, repo.Name, gitea.ListRepoTopicsOptions{})
+	return topics, err
+}
+
 func (g *Gitea) getRepositories(ctx context.Context) ([]*gitea.Repository, error) {
 	allRepos := []*gitea.Repository{}
 
diff --git a/internal/scm/github/github.go b/internal/scm/github/github.go
index b738955..f9a549a 100755
--- a/internal/scm/github/github.go
+++ b/internal/scm/github/github.go
@@ -99,6 +99,7 @@ type RepositoryListing struct {
 	Organizations []string
 	Users         []string
 	Repositories  []RepositoryReference
+	Topics        []string
 }
 
 // RepositoryReference contains information to be able to reference a repository
@@ -151,6 +152,9 @@ func (g *Github) GetRepositories(ctx context.Context) ([]scm.Repository, error)
 		case !g.Fork && !g.ReadOnly && !permissions["push"]:
 			log.Debug("Skipping repository since the token does not have push permissions and the run will not fork")
 			continue
+		case len(g.Topics) != 0 && !scm.RepoContainsTopic(r.Topics, g.Topics):
+			log.Debug("Skipping repository since it does not match repository topics")
+			continue
 		}
 
 		newRepo, err := g.convertRepo(r)
diff --git a/internal/scm/gitlab/gitlab.go b/internal/scm/gitlab/gitlab.go
index 4bba751..a0498b8 100644
--- a/internal/scm/gitlab/gitlab.go
+++ b/internal/scm/gitlab/gitlab.go
@@ -11,6 +11,7 @@ import (
 
 	internalHTTP "github.com/lindell/multi-gitter/internal/http"
 	"github.com/lindell/multi-gitter/internal/scm"
+	log "github.com/sirupsen/logrus"
 	"github.com/xanzy/go-gitlab"
 )
 
@@ -54,6 +55,7 @@ type RepositoryListing struct {
 	Groups   []string
 	Users    []string
 	Projects []ProjectReference
+	Topics   []string
 }
 
 // Config includes extra config parameters for the GitLab client
@@ -89,6 +91,12 @@ func (g *Gitlab) GetRepositories(ctx context.Context) ([]scm.Repository, error)
 
 	repos := make([]scm.Repository, 0, len(allProjects))
 	for _, project := range allProjects {
+		log := log.WithField("repo", project.NameWithNamespace)
+		if len(g.Topics) != 0 && !scm.RepoContainsTopic(project.Topics, g.Topics) {
+			log.Debug("Skipping repository since it does not match repository topics")
+			continue
+		}
+
 		p, err := g.convertProject(project)
 		if err != nil {
 			return nil, err
@@ -124,6 +132,7 @@ func (g *Gitlab) getProjects(ctx context.Context) ([]*gitlab.Project, error) {
 		if err != nil {
 			return nil, err
 		}
+
 		allProjects = append(allProjects, project)
 	}
 
diff --git a/internal/scm/repository.go b/internal/scm/repository.go
index d915b7c..657f392 100755
--- a/internal/scm/repository.go
+++ b/internal/scm/repository.go
@@ -9,3 +9,18 @@ type Repository interface {
 	// FullName returns the full id of the repository, usually ownerName/repoName
 	FullName() string
 }
+
+func RepoContainsTopic(repoTopics []string, filterTopics []string) bool {
+	repoTopicsMap := map[string]struct{}{}
+	for _, v := range repoTopics {
+		repoTopicsMap[v] = struct{}{}
+	}
+
+	for _, v := range filterTopics {
+		if _, ok := repoTopicsMap[v]; ok {
+			return true
+		}
+	}
+
+	return false
+}


Test patch:
diff --git a/internal/scm/github/github_test.go b/internal/scm/github/github_test.go
index c4d7639..e16bcfe 100644
--- a/internal/scm/github/github_test.go
+++ b/internal/scm/github/github_test.go
@@ -49,6 +49,9 @@ func Test_GetRepositories(t *testing.T) {
 					"name": "test1",
 					"full_name": "test-org/test1",
 					"private": false,
+					"topics": [
+						"frontend"
+					],
 					"owner": {
 						"login": "test-org",
 						"type": "Organization",
@@ -93,6 +96,10 @@ func Test_GetRepositories(t *testing.T) {
 					"name": "test2",
 					"full_name": "lindell/test2",
 					"private": false,
+					"topics": [
+						"backend",
+						"go"
+					],
 					"owner": {
 						"login": "lindell",
 						"type": "User",
@@ -163,6 +170,22 @@ func Test_GetRepositories(t *testing.T) {
 		}
 	}
 
+	// Topics
+	{
+		gh, err := github.New("", "", transport.Wrapper, github.RepositoryListing{
+			Organizations: []string{"test-org"},
+			Topics:        []string{"frontend", "backend"},
+		}, []scm.MergeType{scm.MergeTypeMerge}, false, "", false, false)
+		require.NoError(t, err)
+
+		repos, err := gh.GetRepositories(context.Background())
+		assert.NoError(t, err)
+		if assert.Len(t, repos, 1) {
+			assert.Equal(t, "master", repos[0].DefaultBranch())
+			assert.Equal(t, "test-org/test1", repos[0].FullName())
+		}
+	}
+
 	// Multiple
 	{
 		gh, err := github.New("", "", transport.Wrapper, github.RepositoryListing{