Commit 6aeb9d708b52
tools/daily-plan/cmd/daily-plan/main.go
@@ -11,11 +11,13 @@ import (
"fmt"
"os"
"os/exec"
+ "path/filepath"
"regexp"
"strconv"
"strings"
"time"
+ "github.com/vdemeester/home/tools/daily-plan/internal/cache"
"github.com/vdemeester/home/tools/daily-plan/internal/config"
"github.com/vdemeester/home/tools/daily-plan/internal/display"
"github.com/vdemeester/home/tools/daily-plan/internal/github"
@@ -27,6 +29,7 @@ var (
outputJSON bool
noDiscussions bool
noComments bool
+ apiCache *cache.Cache
)
func main() {
@@ -38,6 +41,7 @@ func main() {
func run(args []string) error {
cfg := config.DefaultConfig()
+ apiCache = cache.New(filepath.Join(cfg.StateDir, "cache"), 10*time.Minute)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
@@ -223,12 +227,25 @@ func cmdShow(ctx context.Context, cfg *config.Config) error {
today := time.Now().Format("2006-01-02")
agenda, _ := org.TodayItems(cfg.Org.File)
- inprog, _ := jira.FetchByStatus(ctx, cfg.Jira.User, []string{"In Progress", "Code Review", "On QA"})
- todo, _ := jira.FetchByStatus(ctx, cfg.Jira.User, []string{"To Do", "New"})
- ghIssues, _ := github.FetchAssignedIssues(ctx, cfg.GitHub.Username, cfg.GitHub.Owners)
- reviews, _ := github.FetchReviewRequests(ctx, cfg.GitHub.Username, cfg.GitHub.Owners)
- reviews = github.FilterBots(reviews, cfg.GitHub.BotFilters)
- myPRs, _ := github.FetchMyPRs(ctx, cfg.GitHub.Username, cfg.GitHub.Owners)
+ inprog, _ := cache.GetOrFetch(apiCache, "show:inprog:"+today, func() ([]jira.Issue, error) {
+ return jira.FetchByStatus(ctx, cfg.Jira.User, []string{"In Progress", "Code Review", "On QA"})
+ })
+ todo, _ := cache.GetOrFetch(apiCache, "show:todo:"+today, func() ([]jira.Issue, error) {
+ return jira.FetchByStatus(ctx, cfg.Jira.User, []string{"To Do", "New"})
+ })
+ ghIssues, _ := cache.GetOrFetch(apiCache, "show:assigned:"+today, func() ([]github.Item, error) {
+ return github.FetchAssignedIssues(ctx, cfg.GitHub.Username, cfg.GitHub.Owners)
+ })
+ reviews, _ := cache.GetOrFetch(apiCache, "show:reviews:"+today, func() ([]github.Item, error) {
+ r, err := github.FetchReviewRequests(ctx, cfg.GitHub.Username, cfg.GitHub.Owners)
+ if err != nil {
+ return nil, err
+ }
+ return github.FilterBots(r, cfg.GitHub.BotFilters), nil
+ })
+ myPRs, _ := cache.GetOrFetch(apiCache, "show:myprs:"+today, func() ([]github.Item, error) {
+ return github.FetchMyPRs(ctx, cfg.GitHub.Username, cfg.GitHub.Owners)
+ })
if outputJSON {
orgItems := make([]jsonOrg, 0, len(agenda))
@@ -274,16 +291,37 @@ func cmdInbox(ctx context.Context, cfg *config.Config, sinceStr string) error {
return fmt.Errorf("invalid date: %w", err)
}
- cves, _ := jira.FetchSecuritySince(ctx, since)
+ sinceKey := since.Format("2006-01-02")
+ cves, _ := cache.GetOrFetch(apiCache, "inbox:cves:"+sinceKey, func() ([]jira.Issue, error) {
+ return jira.FetchSecuritySince(ctx, since)
+ })
grouped, total := jira.GroupByCVE(cves)
- updated, _ := jira.FetchUpdatedSince(ctx, since)
- advisories, _ := github.FetchSecurityAdvisories(ctx, cfg.GitHub.SecurityRepos, []string{"triage", "draft"})
- dependabot, _ := github.FetchDependabotAlerts(ctx, cfg.GitHub.Owners, "critical,high")
- newIssues, _ := github.FetchNewIssuesSince(ctx, since, cfg.GitHub.Owners)
- newPRs, _ := github.FetchNewPRsSince(ctx, since, cfg.GitHub.Owners)
- newPRs = github.FilterBots(newPRs, cfg.GitHub.BotFilters)
- reviews, _ := github.FetchReviewRequests(ctx, cfg.GitHub.Username, cfg.GitHub.Owners)
- reviews = github.FilterBots(reviews, cfg.GitHub.BotFilters)
+ updated, _ := cache.GetOrFetch(apiCache, "inbox:updated:"+sinceKey, func() ([]jira.Issue, error) {
+ return jira.FetchUpdatedSince(ctx, since)
+ })
+ advisories, _ := cache.GetOrFetch(apiCache, "inbox:advisories:"+sinceKey, func() ([]github.SecurityAdvisory, error) {
+ return github.FetchSecurityAdvisories(ctx, cfg.GitHub.SecurityRepos, []string{"triage", "draft"})
+ })
+ dependabot, _ := cache.GetOrFetch(apiCache, "inbox:dependabot:"+sinceKey, func() ([]github.DependabotAlert, error) {
+ return github.FetchDependabotAlerts(ctx, cfg.GitHub.Owners, "critical,high")
+ })
+ newIssues, _ := cache.GetOrFetch(apiCache, "inbox:issues:"+sinceKey, func() ([]github.Item, error) {
+ return github.FetchNewIssuesSince(ctx, since, cfg.GitHub.Owners)
+ })
+ newPRs, _ := cache.GetOrFetch(apiCache, "inbox:prs:"+sinceKey, func() ([]github.Item, error) {
+ prs, err := github.FetchNewPRsSince(ctx, since, cfg.GitHub.Owners)
+ if err != nil {
+ return nil, err
+ }
+ return github.FilterBots(prs, cfg.GitHub.BotFilters), nil
+ })
+ reviews, _ := cache.GetOrFetch(apiCache, "inbox:reviews:"+sinceKey, func() ([]github.Item, error) {
+ r, err := github.FetchReviewRequests(ctx, cfg.GitHub.Username, cfg.GitHub.Owners)
+ if err != nil {
+ return nil, err
+ }
+ return github.FilterBots(r, cfg.GitHub.BotFilters), nil
+ })
if outputJSON {
updateLastCheck(cfg)
@@ -504,21 +542,41 @@ func cmdWeekly(ctx context.Context, cfg *config.Config) error {
monday := now.AddDate(0, 0, -daysFromMonday)
nextMonday := monday.AddDate(0, 0, 7)
- completed, _ := jira.FetchCompletedSince(ctx, cfg.Jira.User, monday)
- merged, _ := github.FetchMergedPRsSince(ctx, cfg.GitHub.Username, monday, cfg.GitHub.Owners)
- reviewed, _ := github.FetchReviewsGivenSince(ctx, cfg.GitHub.Username, monday, cfg.GitHub.Owners)
- issuesCreated, _ := github.FetchIssuesCreatedSince(ctx, cfg.GitHub.Username, monday, cfg.GitHub.Owners)
- inprog, _ := jira.FetchByStatus(ctx, cfg.Jira.User, []string{"In Progress", "Code Review", "On QA"})
- todo, _ := jira.FetchByStatus(ctx, cfg.Jira.User, []string{"To Do", "New"})
- ghIssues, _ := github.FetchAssignedIssues(ctx, cfg.GitHub.Username, cfg.GitHub.Owners)
+ mondayStr := monday.Format("2006-01-02")
+
+ completed, _ := cache.GetOrFetch(apiCache, "jira:completed:"+mondayStr, func() ([]jira.Issue, error) {
+ return jira.FetchCompletedSince(ctx, cfg.Jira.User, monday)
+ })
+ merged, _ := cache.GetOrFetch(apiCache, "gh:merged:"+mondayStr, func() ([]github.Item, error) {
+ return github.FetchMergedPRsSince(ctx, cfg.GitHub.Username, monday, cfg.GitHub.Owners)
+ })
+ reviewed, _ := cache.GetOrFetch(apiCache, "gh:reviewed:"+mondayStr, func() ([]github.Item, error) {
+ return github.FetchReviewsGivenSince(ctx, cfg.GitHub.Username, monday, cfg.GitHub.Owners)
+ })
+ issuesCreated, _ := cache.GetOrFetch(apiCache, "gh:issues-created:"+mondayStr, func() ([]github.Item, error) {
+ return github.FetchIssuesCreatedSince(ctx, cfg.GitHub.Username, monday, cfg.GitHub.Owners)
+ })
+ inprog, _ := cache.GetOrFetch(apiCache, "jira:inprog:"+mondayStr, func() ([]jira.Issue, error) {
+ return jira.FetchByStatus(ctx, cfg.Jira.User, []string{"In Progress", "Code Review", "On QA"})
+ })
+ todo, _ := cache.GetOrFetch(apiCache, "jira:todo:"+mondayStr, func() ([]jira.Issue, error) {
+ return jira.FetchByStatus(ctx, cfg.Jira.User, []string{"To Do", "New"})
+ })
+ ghIssues, _ := cache.GetOrFetch(apiCache, "gh:assigned:"+mondayStr, func() ([]github.Item, error) {
+ return github.FetchAssignedIssues(ctx, cfg.GitHub.Username, cfg.GitHub.Owners)
+ })
var discussions []github.DiscussionItem
var comments []github.CommentItem
if !noDiscussions {
- discussions, _ = github.FetchDiscussionsSince(ctx, monday)
+ discussions, _ = cache.GetOrFetch(apiCache, "gh:discussions:"+mondayStr, func() ([]github.DiscussionItem, error) {
+ return github.FetchDiscussionsSince(ctx, monday)
+ })
}
if !noComments {
- comments, _ = github.FetchCommentsSince(ctx, monday)
+ comments, _ = cache.GetOrFetch(apiCache, "gh:comments:"+mondayStr, func() ([]github.CommentItem, error) {
+ return github.FetchCommentsSince(ctx, monday)
+ })
}
if outputJSON {
tools/daily-plan/internal/cache/cache.go
@@ -0,0 +1,96 @@
+// Package cache provides file-based TTL caching for API responses.
+package cache
+
+import (
+ "crypto/sha256"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+)
+
+// Cache provides file-based TTL caching.
+type Cache struct {
+ dir string
+ ttl time.Duration
+}
+
+type entry struct {
+ ExpiresAt time.Time `json:"expires_at"`
+ Data json.RawMessage `json:"data"`
+}
+
+// New creates a cache with the given directory and TTL.
+func New(dir string, ttl time.Duration) *Cache {
+ return &Cache{dir: dir, ttl: ttl}
+}
+
+// Get retrieves a cached value. Returns false if missing or expired.
+func (c *Cache) Get(key string, dest any) bool {
+ path := c.path(key)
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return false
+ }
+
+ var e entry
+ if err := json.Unmarshal(data, &e); err != nil {
+ os.Remove(path)
+ return false
+ }
+
+ if time.Now().After(e.ExpiresAt) {
+ os.Remove(path)
+ return false
+ }
+
+ return json.Unmarshal(e.Data, dest) == nil
+}
+
+// Set stores a value in the cache.
+func (c *Cache) Set(key string, value any) error {
+ data, err := json.Marshal(value)
+ if err != nil {
+ return err
+ }
+
+ e := entry{
+ ExpiresAt: time.Now().Add(c.ttl),
+ Data: data,
+ }
+
+ out, err := json.Marshal(e)
+ if err != nil {
+ return err
+ }
+
+ if err := os.MkdirAll(c.dir, 0755); err != nil {
+ return err
+ }
+
+ return os.WriteFile(c.path(key), out, 0644)
+}
+
+// GetOrFetch retrieves from cache, or calls fetch and caches the result.
+func GetOrFetch[T any](c *Cache, key string, fetch func() (T, error)) (T, error) {
+ var result T
+ if c != nil && c.Get(key, &result) {
+ return result, nil
+ }
+
+ result, err := fetch()
+ if err != nil {
+ return result, err
+ }
+
+ if c != nil {
+ c.Set(key, result)
+ }
+ return result, nil
+}
+
+func (c *Cache) path(key string) string {
+ hash := sha256.Sum256([]byte(key))
+ return filepath.Join(c.dir, fmt.Sprintf("%x.json", hash[:12]))
+}