feat: multi-repo patterns + directory recursion
CI / test (pull_request) Successful in 14s
CI / review (gpt-5-mini, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m57s
CI / review (gpt-5, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 2m2s

patterns-repo now accepts a comma-separated list of repos:
  PATTERNS_REPO="rodin/elixir-patterns,rodin/phoenix-conventions"

patterns-files accepts files AND directories:
  PATTERNS_FILES="README.md,docs/"

When a path is a directory, all files within it are fetched
recursively via the Gitea contents API. Only .md, .txt, .yml,
and .yaml files are included as pattern content.

New API methods:
- ListContents: list files/dirs at a path via contents API
- GetAllFilesInPath: recursively fetch all file contents

This allows a single review action to pull idioms from multiple
pattern repos (e.g. elixir-patterns + phoenix-conventions) and
include entire directories of documentation as review criteria.
This commit is contained in:
Rodin
2026-05-01 12:14:19 -07:00
parent e234dca474
commit 56f5abda3c
3 changed files with 160 additions and 15 deletions
+45 -15
View File
@@ -184,31 +184,61 @@ func fetchFileContext(client *gitea.Client, owner, repo, ref string, files []git
return sb.String()
}
// fetchPatterns fetches pattern files from an external repo.
// fetchPatterns fetches pattern files from one or more external repos.
// patternsRepo is comma-separated list of owner/name repos.
// patternsFiles is comma-separated list of file paths or directories.
// If a path ends with / or is a directory, all files within it are fetched recursively.
func fetchPatterns(client *gitea.Client, patternsRepo, patternsFiles string) string {
parts := strings.SplitN(patternsRepo, "/", 2)
if len(parts) != 2 {
log.Printf("Warning: invalid patterns-repo format %q, expected owner/name", patternsRepo)
return ""
}
owner, repo := parts[0], parts[1]
var sb strings.Builder
for _, filepath := range strings.Split(patternsFiles, ",") {
filepath = strings.TrimSpace(filepath)
if filepath == "" {
repos := strings.Split(patternsRepo, ",")
paths := strings.Split(patternsFiles, ",")
for _, repoRef := range repos {
repoRef = strings.TrimSpace(repoRef)
if repoRef == "" {
continue
}
content, err := client.GetFileContent(owner, repo, filepath)
if err != nil {
log.Printf("Warning: could not fetch pattern file %s from %s: %v", filepath, patternsRepo, err)
parts := strings.SplitN(repoRef, "/", 2)
if len(parts) != 2 {
log.Printf("Warning: invalid patterns-repo format %q, expected owner/name", repoRef)
continue
}
sb.WriteString(fmt.Sprintf("### %s/%s\n\n%s\n\n", patternsRepo, filepath, content))
owner, repo := parts[0], parts[1]
for _, path := range paths {
path = strings.TrimSpace(path)
if path == "" {
continue
}
files, err := client.GetAllFilesInPath(owner, repo, path)
if err != nil {
log.Printf("Warning: could not fetch %s from %s: %v", path, repoRef, err)
continue
}
for filepath, content := range files {
// Only include markdown and text files as patterns
if !isPatternFile(filepath) {
continue
}
sb.WriteString(fmt.Sprintf("### %s/%s\n\n%s\n\n", repoRef, filepath, content))
}
}
}
return sb.String()
}
// isPatternFile returns true if the file should be included as pattern content.
func isPatternFile(path string) bool {
lower := strings.ToLower(path)
return strings.HasSuffix(lower, ".md") ||
strings.HasSuffix(lower, ".txt") ||
strings.HasSuffix(lower, ".yml") ||
strings.HasSuffix(lower, ".yaml")
}
// evaluateCIStatus checks if all CI statuses indicate success.
func evaluateCIStatus(statuses []gitea.CommitStatus) (passed bool, details string) {
if len(statuses) == 0 {