@@ -68,6 +68,18 @@ type GHCRVersionResponse struct {
6868 } `json:"package_files"`
6969}
7070
71+ // GitHubRepository represents a GitHub repository
72+ type GitHubRepository struct {
73+ ID int `json:"id"`
74+ NodeID string `json:"node_id"`
75+ Name string `json:"name"`
76+ FullName string `json:"full_name"`
77+ Private bool `json:"private"`
78+ Owner struct {
79+ Login string `json:"login"`
80+ } `json:"owner"`
81+ }
82+
7183func NewGHCRCollector (cfg * config.Config , registry * metrics.Registry ) * GHCRCollector {
7284 return & GHCRCollector {
7385 config : cfg ,
@@ -94,25 +106,31 @@ func (gc *GHCRCollector) run(ctx context.Context) {
94106 }()
95107
96108 // Start individual tickers for each package
97- for groupName , group := range gc .config .Packages {
109+ for _ , group := range gc .config .Packages {
98110 interval := gc .config .GetPackageInterval (group )
99111 ticker := time .NewTicker (time .Duration (interval ) * time .Second )
100- tickers [groupName ] = ticker
101-
102- // Initial collection for this package
103- gc .collectSinglePackage (ctx , group .Repo , group )
104-
105- // Start goroutine for this package
106- go func (name string , pkg config.PackageGroup ) {
107- for {
108- select {
109- case <- ctx .Done ():
110- return
111- case <- ticker .C :
112- gc .collectSinglePackage (ctx , pkg .Repo , pkg )
112+ tickers [group .Name ] = ticker
113+
114+ // If repo is specified, collect for that specific repo
115+ if group .Repo != "" {
116+ // Initial collection for this package
117+ gc .collectSinglePackage (ctx , group .Repo , group )
118+
119+ // Start goroutine for this package
120+ go func (pkg config.PackageGroup ) {
121+ for {
122+ select {
123+ case <- ctx .Done ():
124+ return
125+ case <- ticker .C :
126+ gc .collectSinglePackage (ctx , pkg .Repo , pkg )
127+ }
113128 }
114- }
115- }(groupName , group )
129+ }(group )
130+ } else {
131+ // If no repo specified, discover all repos for the owner
132+ gc .collectAllReposForOwner (ctx , group , ticker )
133+ }
116134 }
117135
118136 // Wait for context cancellation
@@ -179,7 +197,7 @@ func (gc *GHCRCollector) collectPackageMetrics(ctx context.Context, repo string,
179197}
180198
181199func (gc * GHCRCollector ) getPackageInfo (ctx context.Context , owner , repo , packageName string ) (* GHCRPackageResponse , error ) {
182- resp , err := gc .makeGitHubAPIRequest (ctx , fmt .Sprintf ("/users/%s/packages/container/%s" , owner , packageName ))
200+ resp , err := gc .MakeGitHubAPIRequest (ctx , fmt .Sprintf ("/users/%s/packages/container/%s" , owner , packageName ))
183201 if err != nil {
184202 return nil , err
185203 }
@@ -198,8 +216,8 @@ func (gc *GHCRCollector) getPackageInfo(ctx context.Context, owner, repo, packag
198216 return & packageInfo , nil
199217}
200218
201- // makeGitHubAPIRequest makes a request to GitHub API, trying user endpoint first, then org endpoint
202- func (gc * GHCRCollector ) makeGitHubAPIRequest (ctx context.Context , path string ) (* http.Response , error ) {
219+ // MakeGitHubAPIRequest makes a request to GitHub API, trying user endpoint first, then org endpoint
220+ func (gc * GHCRCollector ) MakeGitHubAPIRequest (ctx context.Context , path string ) (* http.Response , error ) {
203221 // Try user endpoint first
204222 userURL := fmt .Sprintf ("https://api.github.com%s" , path )
205223
@@ -271,7 +289,7 @@ func (gc *GHCRCollector) makeGitHubAPIRequest(ctx context.Context, path string)
271289}
272290
273291func (gc * GHCRCollector ) getPackageVersions (ctx context.Context , owner , repo , packageName string ) ([]GHCRVersionResponse , error ) {
274- resp , err := gc .makeGitHubAPIRequest (ctx , fmt .Sprintf ("/users/%s/packages/container/%s/versions" , owner , packageName ))
292+ resp , err := gc .MakeGitHubAPIRequest (ctx , fmt .Sprintf ("/users/%s/packages/container/%s/versions" , owner , packageName ))
275293 if err != nil {
276294 return nil , err
277295 }
@@ -290,6 +308,96 @@ func (gc *GHCRCollector) getPackageVersions(ctx context.Context, owner, repo, pa
290308 return versions , nil
291309}
292310
311+ // getRepositoriesForOwner lists all repositories for a given owner
312+ func (gc * GHCRCollector ) getRepositoriesForOwner (ctx context.Context , owner string ) ([]GitHubRepository , error ) {
313+ var allRepos []GitHubRepository
314+
315+ page := 1
316+ perPage := 100
317+
318+ for {
319+ // Try user endpoint first
320+ userPath := fmt .Sprintf ("/users/%s/repos?page=%d&per_page=%d&type=all" , owner , page , perPage )
321+
322+ resp , err := gc .MakeGitHubAPIRequest (ctx , userPath )
323+ if err != nil {
324+ return nil , err
325+ }
326+
327+ defer func () {
328+ if err := resp .Body .Close (); err != nil {
329+ slog .Error ("Error closing response body" , "error" , err )
330+ }
331+ }()
332+
333+ var repos []GitHubRepository
334+ if err := json .NewDecoder (resp .Body ).Decode (& repos ); err != nil {
335+ return nil , err
336+ }
337+
338+ // If we got no repos, we've reached the end
339+ if len (repos ) == 0 {
340+ break
341+ }
342+
343+ allRepos = append (allRepos , repos ... )
344+
345+ // If we got fewer repos than requested, we've reached the end
346+ if len (repos ) < perPage {
347+ break
348+ }
349+
350+ page ++
351+ }
352+
353+ slog .Info ("Discovered repositories for owner" , "owner" , owner , "count" , len (allRepos ))
354+
355+ return allRepos , nil
356+ }
357+
358+ // collectAllReposForOwner discovers and collects metrics for all repositories of an owner
359+ func (gc * GHCRCollector ) collectAllReposForOwner (ctx context.Context , group config.PackageGroup , ticker * time.Ticker ) {
360+ // Initial discovery and collection
361+ gc .discoverAndCollectRepos (ctx , group )
362+
363+ // Start goroutine for periodic discovery and collection
364+ go func (pkg config.PackageGroup ) {
365+ for {
366+ select {
367+ case <- ctx .Done ():
368+ return
369+ case <- ticker .C :
370+ gc .discoverAndCollectRepos (ctx , pkg )
371+ }
372+ }
373+ }(group )
374+ }
375+
376+ // discoverAndCollectRepos discovers repositories and collects metrics for each
377+ func (gc * GHCRCollector ) discoverAndCollectRepos (ctx context.Context , group config.PackageGroup ) {
378+ slog .Info ("Discovering repositories for owner" , "owner" , group .Owner , "group" , group .Name )
379+
380+ // Get all repositories for the owner
381+ repos , err := gc .getRepositoriesForOwner (ctx , group .Owner )
382+ if err != nil {
383+ slog .Error ("Failed to discover repositories" , "owner" , group .Owner , "group" , group .Name , "error" , err )
384+ return
385+ }
386+
387+ // Collect metrics for each repository
388+ for _ , repo := range repos {
389+ // Create a package group for this specific repository
390+ repoPackage := config.PackageGroup {
391+ Name : group .Name ,
392+ Owner : group .Owner ,
393+ Repo : repo .Name ,
394+ }
395+
396+ // Collect metrics for this repository
397+ gc .collectSinglePackage (ctx , repo .Name , repoPackage )
398+ }
399+ }
400+
293401func (gc * GHCRCollector ) updatePackageMetrics (ctx context.Context , pkg config.PackageGroup , packageInfo * GHCRPackageResponse , versions []GHCRVersionResponse ) {
294402 // Update package-level metrics with real data
295403 // Note: GitHub API doesn't provide download statistics for packages
0 commit comments