Repository: lindell/multi-gitter (go)

Issue:
Use repository metadata search as input for target repository list
I mostly use GitHub, so my suggestion is based around that ecosystem.

At the moment, the source of the list of repositories to alter is either (a) everything owned by a user/organisation, or (b) a specific set of repositories.

What I often do is use a GitHub code or repository search to find the set of target repositories first, then feed that into `multi-gitter`.

I propose adding a `--repo-search` flag (GitHub only, initially) that would use the flag as an SCM-specific search query that would gather the set of target repositories. The syntax accepted by the flag would be that supported by the underlying SCM, and would be passed through directly.

Example searches:

- partial names with boolean queries: `owner:buildkite-plugins in:name docker AND plugin`
- one or multiple owners `owner:cultureamp owner:jamestelfer fork:true in:name plugin`

Key drawback to this approach is that it is possible that it is not consistent across executions, making it potentially tricky for commands like `merge` or `status`. Ultimately there is some level of similar potential for instability in existing options like `--topic`, but it's worth calling out.

In my use I tend to work with this by ensuring that the PRs created have a set of tags to make status and merging easy to query by other means. In GitHub, searching by the PR's base branch or tags is quite straightforward, allowing PRs created by multiple runs of `multi-gitter` to be tracked and updated as a group.

Relevant GitHub documentation:

- [GitHub repository search API](https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-repositories)
- [GitHub repo search qualifiers](https://docs.github.com/articles/searching-for-repositories/)
- [GitHub search query syntax](https://docs.github.com/rest/search/search#constructing-a-search-query) (this is the legacy search, as the API does not yet use the new search engine).

The [initial discussion](https://github.com/lindell/multi-gitter/discussions/386) centred around using code search. This is a common use case for me (finding which repositories reference a certain library, or use a method/idiom that we're targeting for evolution).

Pull request description:
feat: use repo search to select repositories
# What does this change

Using the `--repo-search` option, it is possible to supply a GitHub **repository** search query that is used to generate the set of repositories to process.

This feature is GItHub-only at the moment.

This allows for searches that reference:

- partial names with boolean queries: `owner:buildkite-plugins in:name docker AND plugin`
- one or multiple owners `owner:cultureamp owner:jamestelfer fork:true in:name plugin`
- README contents
- license

I initially started this as a "warm up" to implementing something similar with code search, but the partial name search and license search are potentially quite useful in themselves.

Obviously this means that search syntax and results depend on the underlying source being queried, so implementations for GitLab et al would be required to see this feature properly take root.

References:

- [GitHub repository search API](https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-repositories)
- [GitHub repo search qualifiers](https://docs.github.com/articles/searching-for-repositories/)
- [GitHub search query syntax](https://docs.github.com/rest/search/search#constructing-a-search-query) (this is the legacy search, as the API does not yet use the new search engine).

```console
$ multi-gitter run examples/general/test.sh --repo-search "user:jamestelfer fork:only" --dry-run --commit-message "chore: foo"

<log output elided />
INFO[0102] Cloning and running script                    repo=jamestelfer/multi-gitter
INFO[0103] Script output: jamestelfer/multi-gitter       repo=jamestelfer/multi-gitter
DEBU[0103] diff --git a/.features.yaml b/.features.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..73311e25f5ad0766cd0576c7302b96f9bc76a4b7
--- /dev/null
+++ b/.features.yaml
@@ -0,0 +1,3 @@
+
+features: ~
+
INFO[0103] Skipping pushing changes because of dry run   repo=jamestelfer/multi-gitter
Repositories with a successful run:
  jamestelfer/jenkins-inheritance-plugin #0
  jamestelfer/slow-cheetah.xdt #0
  jamestelfer/global-build-stats-plugin #0
  jamestelfer/wheel-of-misfortune #0
  jamestelfer/blast-1 #0
  jamestelfer/cloudformation-cli #0
  jamestelfer/step-templates-buildkite-plugin #0
  jamestelfer/fusionauth-site #0
  jamestelfer/aws-lambda-go #0
  jamestelfer/testza #0
  jamestelfer/avro #0
  jamestelfer/ca-go #0
  jamestelfer/rocketcrab #0
  jamestelfer/terraform-cdk #0
  jamestelfer/aws-cdk #0
  jamestelfer/multi-gitter #0
```

# What issue does it fix

Closes #398

Refer also to [original discussion](https://github.com/lindell/multi-gitter/discussions/386).

# Notes for the reviewer

Happy for feedback/changes of direction.

# Checklist
- [X] Made sure the PR follows the [CONTRIBUTING.md](CONTRIBUTING.md) guidelines
- [x] Tests if something new is added


Patch:
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index bfc8c8f..5035413 100755
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -11,7 +11,7 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v4
         with:
-          go-version: "^1.15.5"
+          go-version-file: "go.mod"
 
       - name: Build
         run: go build main.go
diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml
index 992aa4f..8d845be 100755
--- a/.github/workflows/coverage.yaml
+++ b/.github/workflows/coverage.yaml
@@ -13,7 +13,7 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v4
         with:
-          go-version: "^1.15.5"
+          go-version-file: "go.mod"
 
       - name: Prepare coverage
         run: mkdir coverage
diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
index 043bd6d..2466fd0 100755
--- a/.github/workflows/docs.yaml
+++ b/.github/workflows/docs.yaml
@@ -16,7 +16,7 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v4
         with:
-          go-version: "^1.15.5"
+          go-version-file: "go.mod"
         id: go
 
       - name: Generate docs
diff --git a/.github/workflows/goreleaser-check.yaml b/.github/workflows/goreleaser-check.yaml
index b3856de..c0e7cc7 100644
--- a/.github/workflows/goreleaser-check.yaml
+++ b/.github/workflows/goreleaser-check.yaml
@@ -16,6 +16,8 @@ jobs:
 
       - name: Set up Go
         uses: actions/setup-go@v4
+        with:
+          go-version-file: "go.mod"
 
       - name: Check GoReleaser config
         uses: goreleaser/goreleaser-action@v4
diff --git a/.github/workflows/linting.yaml b/.github/workflows/linting.yaml
index ac4d962..be1a571 100644
--- a/.github/workflows/linting.yaml
+++ b/.github/workflows/linting.yaml
@@ -10,4 +10,5 @@ jobs:
       - name: golangci-lint
         uses: reviewdog/action-golangci-lint@v2
         with:
+          go_version_file: "go.mod"
           level: warning
diff --git a/.github/workflows/readme.yaml b/.github/workflows/readme.yaml
index b1c07fe..04e6814 100755
--- a/.github/workflows/readme.yaml
+++ b/.github/workflows/readme.yaml
@@ -16,7 +16,7 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v4
         with:
-          go-version: "^1.15.5"
+          go-version-file: "go.mod"
         id: go
 
       - name: Generate readme
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 131c0ba..3c60ef8 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -24,7 +24,7 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v4
         with:
-          go-version: "^1.16.0"
+          go-version-file: "go.mod"
         if: ${{ steps.release.outputs.release_created }}
 
       - name: Run GoReleaser
diff --git a/cmd/platform.go b/cmd/platform.go
index a8018c4..e63824d 100644
--- a/cmd/platform.go
+++ b/cmd/platform.go
@@ -28,6 +28,7 @@ func configurePlatform(cmd *cobra.Command) {
 	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.StringP("repo-search", "", "", "Use a repository search to find repositories to target (GitHub only). Forks are NOT included by default, use `fork:true` to include them. See the GitHub documentation for full syntax: https://docs.github.com/en/search-github/searching-on-github/searching-for-repositories")
 	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.")
@@ -122,14 +123,15 @@ func createGithubClient(flag *flag.FlagSet, verifyFlags bool, readOnly bool) (mu
 	orgs, _ := flag.GetStringSlice("org")
 	users, _ := flag.GetStringSlice("user")
 	repos, _ := flag.GetStringSlice("repo")
+	repoSearch, _ := flag.GetString("repo-search")
 	topics, _ := flag.GetStringSlice("topic")
 	forkMode, _ := flag.GetBool("fork")
 	forkOwner, _ := flag.GetString("fork-owner")
 	sshAuth, _ := flag.GetBool("ssh-auth")
 	skipForks, _ := flag.GetBool("skip-forks")
 
-	if verifyFlags && len(orgs) == 0 && len(users) == 0 && len(repos) == 0 {
-		return nil, errors.New("no organization, user or repo set")
+	if verifyFlags && len(orgs) == 0 && len(users) == 0 && len(repos) == 0 && repoSearch == "" {
+		return nil, errors.New("no organization, user, repo or repo-search set")
 	}
 
 	token, err := getToken(flag)
@@ -164,11 +166,12 @@ func createGithubClient(flag *flag.FlagSet, verifyFlags bool, readOnly bool) (mu
 		BaseURL:             gitBaseURL,
 		TransportMiddleware: http.NewLoggingRoundTripper,
 		RepoListing: github.RepositoryListing{
-			Organizations: orgs,
-			Users:         users,
-			Repositories:  repoRefs,
-			Topics:        topics,
-			SkipForks:     skipForks,
+			Organizations:    orgs,
+			Users:            users,
+			Repositories:     repoRefs,
+			RepositorySearch: repoSearch,
+			Topics:           topics,
+			SkipForks:        skipForks,
 		},
 		MergeTypes:       mergeTypes,
 		ForkMode:         forkMode,
diff --git a/go.mod b/go.mod
index 93decdc..783dda9 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/lindell/multi-gitter
 
-go 1.19
+go 1.21
 
 require (
 	code.gitea.io/sdk/gitea v0.16.0
diff --git a/go.sum b/go.sum
index 4c190fb..3b2e6f6 100644
--- a/go.sum
+++ b/go.sum
@@ -50,7 +50,9 @@ github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjA
 github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
 github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
 github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@@ -74,6 +76,7 @@ github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9e
 github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg=
 github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg=
 github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
+github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
 github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -83,12 +86,15 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
 github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
+github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
 github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
+github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
 github.com/gfleury/go-bitbucket-v1 v0.0.0-20230626192437-8d7be5866751 h1:Ea58sAu8LX/NS7z3aeL+YX/7+FSd9jsxq6Ecpz7QEDM=
 github.com/gfleury/go-bitbucket-v1 v0.0.0-20230626192437-8d7be5866751/go.mod h1:IqOZzks2wlWCIai0esXnZPdPwxF2yOz0HcCYw5I4pCg=
 github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
+github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
 github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
 github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
@@ -96,6 +102,7 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmS
 github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
 github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
 github.com/go-git/go-git/v5 v5.9.0 h1:cD9SFA7sHVRdJ7AYck1ZaAa/yeuBvGPxwXDL8cxrObY=
 github.com/go-git/go-git/v5 v5.9.0/go.mod h1:RKIqga24sWdMGZF+1Ekv9kylsDz6LzdTSI2s/OsZWE0=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@@ -144,6 +151,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-github/v55 v55.0.0 h1:4pp/1tNMB9X/LuAhs5i0KQAE40NmiR/y6prLNb9x9cg=
 github.com/google/go-github/v55 v55.0.0/go.mod h1:JLahOTA1DnXzhxEymmFF5PP2tSS9JVNj68mSZNDwskA=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
@@ -170,6 +178,7 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n
 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
 github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
 github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
+github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
 github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0=
 github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
 github.com/hashicorp/go-version v1.5.0 h1:O293SZ2Eg+AAYijkVK3jR786Am1bhDEh2GHT0tIVE5E=
@@ -192,19 +201,24 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
 github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
 github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
 github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
 github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
+github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
 github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
+github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
 github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
 github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
 github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
@@ -217,6 +231,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
+github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
@@ -382,6 +397,7 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -435,6 +451,7 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
 golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -605,6 +622,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
diff --git a/internal/scm/github/github.go b/internal/scm/github/github.go
index a2391b0..be47903 100755
--- a/internal/scm/github/github.go
+++ b/internal/scm/github/github.go
@@ -105,11 +105,12 @@ type Github struct {
 
 // RepositoryListing contains information about which repositories that should be fetched
 type RepositoryListing struct {
-	Organizations []string
-	Users         []string
-	Repositories  []RepositoryReference
-	Topics        []string
-	SkipForks     bool
+	Organizations    []string
+	Users            []string
+	Repositories     []RepositoryReference
+	RepositorySearch string
+	Topics           []string
+	SkipForks        bool
 }
 
 // RepositoryReference contains information to be able to reference a repository
@@ -213,6 +214,14 @@ func (g *Github) getRepositories(ctx context.Context) ([]*github.Repository, err
 		allRepos = append(allRepos, repo)
 	}
 
+	if g.RepositorySearch != "" {
+		repos, err := g.getSearchRepositories(ctx, g.RepositorySearch)
+		if err != nil {
+			return nil, errors.Wrapf(err, "could not get repository search results for '%s'", g.RepositorySearch)
+		}
+		allRepos = append(allRepos, repos...)
+	}
+
 	// Remove duplicate repos
 	repoMap := map[string]*github.Repository{}
 	for _, repo := range allRepos {
@@ -282,6 +291,48 @@ func (g *Github) getUserRepositories(ctx context.Context, user string) ([]*githu
 	return repos, nil
 }
 
+func (g *Github) getSearchRepositories(ctx context.Context, search string) ([]*github.Repository, error) {
+	var repos []*github.Repository
+	i := 1
+	for {
+		rr, _, err := retry(ctx, func() ([]*github.Repository, *github.Response, error) {
+			rr, resp, err := g.ghClient.Search.Repositories(ctx, search, &github.SearchOptions{
+				ListOptions: github.ListOptions{
+					Page:    i,
+					PerPage: 100,
+				},
+			})
+
+			if err != nil {
+				return nil, nil, err
+			}
+
+			if rr.GetIncompleteResults() {
+				// can occur when search times out on the server: for now, fail instead
+				// of handling the issue
+				return nil, nil, fmt.Errorf("search timed out on GitHub and was marked incomplete: try refining the search to return fewer results or be less complex")
+			}
+
+			if rr.GetTotal() > 1000 {
+				return nil, nil, fmt.Errorf("%d results for this search, but only the first 1000 results will be returned: try refining your search terms", rr.GetTotal())
+			}
+
+			return rr.Repositories, resp, nil
+		})
+
+		if err != nil {
+			return nil, err
+		}
+		repos = append(repos, rr...)
+		if len(rr) != 100 {
+			break
+		}
+		i++
+	}
+
+	return repos, nil
+}
+
 func (g *Github) getRepository(ctx context.Context, repoRef RepositoryReference) (*github.Repository, error) {
 	repo, _, err := retry(ctx, func() (*github.Repository, *github.Response, error) {
 		return g.ghClient.Repositories.Get(ctx, repoRef.OwnerName, repoRef.Name)


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index c566d3f..42d796e 100755
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -17,7 +17,7 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v4
         with:
-          go-version: "^1.15.5"
+          go-version-file: "go.mod"
 
       # Because of a bug in go-git that that makes cloning of a folder in another Windows drive letter,
       # the test has to be moved and run in another folder on Windows (until the bug is fixed)
diff --git a/internal/scm/github/github_test.go b/internal/scm/github/github_test.go
index 38f65fa..0be69fc 100644
--- a/internal/scm/github/github_test.go
+++ b/internal/scm/github/github_test.go
@@ -90,7 +90,7 @@ func Test_GetRepositories(t *testing.T) {
 					"push": true,
 					"pull": true
 				},
-				"created_at": "2020-01-02T16:49:16Z"
+				"created_at": "2020-01-02T16:49:17Z"
 			}`,
 			"/users/test-user/repos": `[
 				{
@@ -117,9 +117,41 @@ func Test_GetRepositories(t *testing.T) {
 						"push": true,
 						"pull": true
 					},
-					"created_at": "2020-01-03T16:49:16Z"
+					"created_at": "2020-01-03T16:49:18Z"
 				}
 			]`,
+			"/search/repositories": `{
+					"total_count": 1,
+					"incomplete_results": false,
+					"items": [
+					{
+						"id": 3,
+						"name": "search-repo1",
+						"full_name": "lindell/search-repo1",
+						"private": false,
+						"topics": [
+							"backend",
+							"go"
+						],
+						"owner": {
+							"login": "lindell",
+							"type": "User",
+							"site_admin": false
+						},
+						"html_url": "https://github.com/lindell/search-repo1",
+						"fork": true,
+						"archived": false,
+						"disabled": false,
+						"default_branch": "main",
+						"permissions": {
+							"admin": true,
+							"push": true,
+							"pull": true
+						},
+						"created_at": "2020-01-03T16:49:19Z"
+					}
+				]
+			}`,
 		},
 	}
 
@@ -205,6 +237,25 @@ func Test_GetRepositories(t *testing.T) {
 		}
 	}
 
+	// Repo search
+	{
+		gh, err := github.New(github.Config{
+			TransportMiddleware: transport.Wrapper,
+			RepoListing: github.RepositoryListing{
+				RepositorySearch: "search-string",
+			},
+			MergeTypes: []scm.MergeType{scm.MergeTypeMerge},
+		})
+		require.NoError(t, err)
+
+		repos, err := gh.GetRepositories(context.Background())
+		assert.NoError(t, err)
+		if assert.Len(t, repos, 1) {
+			assert.Equal(t, "main", repos[0].DefaultBranch())
+			assert.Equal(t, "lindell/search-repo1", repos[0].FullName())
+		}
+	}
+
 	// Multiple
 	{
 		gh, err := github.New(github.Config{
@@ -218,6 +269,7 @@ func Test_GetRepositories(t *testing.T) {
 						Name:      "test1",
 					},
 				},
+				RepositorySearch: "test-search",
 			},
 			MergeTypes: []scm.MergeType{scm.MergeTypeMerge},
 		})
@@ -225,11 +277,12 @@ func Test_GetRepositories(t *testing.T) {
 
 		repos, err := gh.GetRepositories(context.Background())
 		assert.NoError(t, err)
-		if assert.Len(t, repos, 2) {
+		if assert.Len(t, repos, 3) {
 			assert.Equal(t, "master", repos[0].DefaultBranch())
 			assert.Equal(t, "test-org/test1", repos[0].FullName())
 			assert.Equal(t, "main", repos[1].DefaultBranch())
 			assert.Equal(t, "lindell/test2", repos[1].FullName())
+			assert.Equal(t, "lindell/search-repo1", repos[2].FullName())
 		}
 	}
 
@@ -259,3 +312,107 @@ func Test_GetRepositories(t *testing.T) {
 		}
 	}
 }
+
+func Test_GetSearchRepository_Incomplete(t *testing.T) {
+	transport := testTransport{
+		pathBodies: map[string]string{
+			"/search/repositories": `{
+					"total_count": 1,
+					"incomplete_results": true,
+					"items": [
+					{
+						"id": 3,
+						"name": "search-repo1",
+						"full_name": "lindell/search-repo1",
+						"private": false,
+						"topics": [
+							"backend",
+							"go"
+						],
+						"owner": {
+							"login": "lindell",
+							"type": "User",
+							"site_admin": false
+						},
+						"html_url": "https://github.com/lindell/search-repo1",
+						"fork": true,
+						"archived": false,
+						"disabled": false,
+						"default_branch": "main",
+						"permissions": {
+							"admin": true,
+							"push": true,
+							"pull": true
+						},
+						"created_at": "2020-01-03T16:49:16Z"
+					}
+				]
+			}`,
+		},
+	}
+
+	gh, err := github.New(github.Config{
+		TransportMiddleware: transport.Wrapper,
+		RepoListing: github.RepositoryListing{
+			RepositorySearch: "search-string",
+		},
+		MergeTypes: []scm.MergeType{scm.MergeTypeMerge},
+	})
+	require.NoError(t, err)
+
+	repos, err := gh.GetRepositories(context.Background())
+	assert.ErrorContains(t, err, "search timed out on GitHub and was marked incomplete")
+	assert.Len(t, repos, 0)
+}
+
+func Test_GetSearchRepository_TooManyResults(t *testing.T) {
+	transport := testTransport{
+		pathBodies: map[string]string{
+			"/search/repositories": `{
+					"total_count": 2054,
+					"incomplete_results": false,
+					"items": [
+					{
+						"id": 3,
+						"name": "search-repo1",
+						"full_name": "lindell/search-repo1",
+						"private": false,
+						"topics": [
+							"backend",
+							"go"
+						],
+						"owner": {
+							"login": "lindell",
+							"type": "User",
+							"site_admin": false
+						},
+						"html_url": "https://github.com/lindell/search-repo1",
+						"fork": true,
+						"archived": false,
+						"disabled": false,
+						"default_branch": "main",
+						"permissions": {
+							"admin": true,
+							"push": true,
+							"pull": true
+						},
+						"created_at": "2020-01-03T16:49:16Z"
+					}
+				]
+			}`,
+		},
+	}
+
+	gh, err := github.New(github.Config{
+		TransportMiddleware: transport.Wrapper,
+		RepoListing: github.RepositoryListing{
+			RepositorySearch: "search-string",
+		},
+		MergeTypes: []scm.MergeType{scm.MergeTypeMerge},
+	})
+	require.NoError(t, err)
+
+	repos, err := gh.GetRepositories(context.Background())
+	assert.ErrorContains(t, err, "only the first 1000 results will be returned")
+	assert.Len(t, repos, 0)
+}