@@ -14,6 +14,7 @@ import (
1414 "path/filepath"
1515 "sort"
1616 "strings"
17+ "sync"
1718 "time"
1819 "unicode/utf8"
1920
4647
4748const runnerTokenRefreshedAtAnnotation = "ambient-code.io/token-refreshed-at"
4849
50+ // ootbWorkflowsCache provides in-memory caching for OOTB workflows to avoid GitHub API rate limits.
51+ // The cache stores workflows by repo URL key and expires after ootbCacheTTL.
52+ type ootbWorkflowsCache struct {
53+ mu sync.RWMutex
54+ workflows []OOTBWorkflow
55+ cachedAt time.Time
56+ cacheKey string // repo+branch+path combination
57+ }
58+
59+ var (
60+ ootbCache = & ootbWorkflowsCache {}
61+ ootbCacheTTL = 5 * time .Minute // Cache OOTB workflows for 5 minutes
62+ )
63+
4964// isBinaryContentType checks if a MIME type represents binary content that should be base64 encoded.
5065// This includes images, archives, documents, executables, and other non-text formats.
5166func isBinaryContentType (contentType string ) bool {
@@ -1718,35 +1733,10 @@ type OOTBWorkflow struct {
17181733}
17191734
17201735// ListOOTBWorkflows returns the list of out-of-the-box workflows dynamically discovered from GitHub
1721- // Attempts to use user's GitHub token for better rate limits, falls back to unauthenticated for public repos
1736+ // Uses in-memory caching (5 min TTL) to avoid GitHub API rate limits.
1737+ // Attempts to use user's GitHub token for better rate limits when cache miss occurs.
17221738// GET /api/workflows/ootb?project=<projectName>
17231739func ListOOTBWorkflows (c * gin.Context ) {
1724- // Try to get user's GitHub token (best effort - not required)
1725- // This gives better rate limits (5000/hr vs 60/hr) and supports private repos
1726- // Project is optional - if provided, we'll try to get the user's token
1727- token := ""
1728- project := c .Query ("project" ) // Optional query parameter
1729- if project != "" {
1730- usrID , _ := c .Get ("userID" )
1731- k8sClt , sessDyn := GetK8sClientsForRequest (c )
1732- if k8sClt == nil || sessDyn == nil {
1733- c .JSON (http .StatusUnauthorized , gin.H {"error" : "Invalid or missing token" })
1734- c .Abort ()
1735- return
1736- }
1737- if userIDStr , ok := usrID .(string ); ok && userIDStr != "" {
1738- if githubToken , err := GetGitHubToken (c .Request .Context (), k8sClt , sessDyn , project , userIDStr ); err == nil {
1739- token = githubToken
1740- log .Printf ("ListOOTBWorkflows: using user's GitHub token for project %s (better rate limits)" , project )
1741- } else {
1742- log .Printf ("ListOOTBWorkflows: failed to get GitHub token for project %s: %v" , project , err )
1743- }
1744- }
1745- }
1746- if token == "" {
1747- log .Printf ("ListOOTBWorkflows: proceeding without GitHub token (public repo, lower rate limits)" )
1748- }
1749-
17501740 // Read OOTB repo configuration from environment
17511741 ootbRepo := strings .TrimSpace (os .Getenv ("OOTB_WORKFLOWS_REPO" ))
17521742 if ootbRepo == "" {
@@ -1763,6 +1753,43 @@ func ListOOTBWorkflows(c *gin.Context) {
17631753 ootbWorkflowsPath = "workflows"
17641754 }
17651755
1756+ // Build cache key from repo configuration
1757+ cacheKey := fmt .Sprintf ("%s|%s|%s" , ootbRepo , ootbBranch , ootbWorkflowsPath )
1758+
1759+ // Check cache first (read lock)
1760+ ootbCache .mu .RLock ()
1761+ if ootbCache .cacheKey == cacheKey && time .Since (ootbCache .cachedAt ) < ootbCacheTTL && len (ootbCache .workflows ) > 0 {
1762+ workflows := ootbCache .workflows
1763+ ootbCache .mu .RUnlock ()
1764+ log .Printf ("ListOOTBWorkflows: returning %d cached workflows (age: %v)" , len (workflows ), time .Since (ootbCache .cachedAt ).Round (time .Second ))
1765+ c .JSON (http .StatusOK , gin.H {"workflows" : workflows })
1766+ return
1767+ }
1768+ ootbCache .mu .RUnlock ()
1769+
1770+ // Cache miss - need to fetch from GitHub
1771+ // Try to get user's GitHub token (best effort - not required)
1772+ // This gives better rate limits (5000/hr vs 60/hr) and supports private repos
1773+ token := ""
1774+ project := c .Query ("project" ) // Optional query parameter
1775+ if project != "" {
1776+ usrID , _ := c .Get ("userID" )
1777+ k8sClt , sessDyn := GetK8sClientsForRequest (c )
1778+ if k8sClt != nil && sessDyn != nil {
1779+ if userIDStr , ok := usrID .(string ); ok && userIDStr != "" {
1780+ if githubToken , err := GetGitHubToken (c .Request .Context (), k8sClt , sessDyn , project , userIDStr ); err == nil {
1781+ token = githubToken
1782+ log .Printf ("ListOOTBWorkflows: using user's GitHub token for project %s (better rate limits)" , project )
1783+ } else {
1784+ log .Printf ("ListOOTBWorkflows: failed to get GitHub token for project %s: %v" , project , err )
1785+ }
1786+ }
1787+ }
1788+ }
1789+ if token == "" {
1790+ log .Printf ("ListOOTBWorkflows: proceeding without GitHub token (public repo, lower rate limits)" )
1791+ }
1792+
17661793 // Parse GitHub URL
17671794 owner , repoName , err := git .ParseGitHubURL (ootbRepo )
17681795 if err != nil {
@@ -1775,6 +1802,16 @@ func ListOOTBWorkflows(c *gin.Context) {
17751802 entries , err := fetchGitHubDirectoryListing (c .Request .Context (), owner , repoName , ootbBranch , ootbWorkflowsPath , token )
17761803 if err != nil {
17771804 log .Printf ("ListOOTBWorkflows: failed to list workflows directory: %v" , err )
1805+ // On error, try to return stale cache if available
1806+ ootbCache .mu .RLock ()
1807+ if len (ootbCache .workflows ) > 0 && ootbCache .cacheKey == cacheKey {
1808+ workflows := ootbCache .workflows
1809+ ootbCache .mu .RUnlock ()
1810+ log .Printf ("ListOOTBWorkflows: returning stale cached workflows due to GitHub error" )
1811+ c .JSON (http .StatusOK , gin.H {"workflows" : workflows })
1812+ return
1813+ }
1814+ ootbCache .mu .RUnlock ()
17781815 c .JSON (http .StatusInternalServerError , gin.H {"error" : "Failed to discover OOTB workflows" })
17791816 return
17801817 }
@@ -1822,7 +1859,14 @@ func ListOOTBWorkflows(c *gin.Context) {
18221859 })
18231860 }
18241861
1825- log .Printf ("ListOOTBWorkflows: discovered %d workflows from %s" , len (workflows ), ootbRepo )
1862+ // Update cache (write lock)
1863+ ootbCache .mu .Lock ()
1864+ ootbCache .workflows = workflows
1865+ ootbCache .cachedAt = time .Now ()
1866+ ootbCache .cacheKey = cacheKey
1867+ ootbCache .mu .Unlock ()
1868+
1869+ log .Printf ("ListOOTBWorkflows: discovered %d workflows from %s (cached for %v)" , len (workflows ), ootbRepo , ootbCacheTTL )
18261870 c .JSON (http .StatusOK , gin.H {"workflows" : workflows })
18271871}
18281872
0 commit comments