204 lines
6.0 KiB
Go
204 lines
6.0 KiB
Go
package github
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
workflowAttentionCacheMaxEntries = 512
|
|
workflowAttentionShortTTL = 30 * time.Second
|
|
workflowAttentionLongTTL = 5 * time.Minute
|
|
)
|
|
|
|
func newWorkflowAttentionCache() *ttlCache {
|
|
c := newTTLCache()
|
|
c.ttl = workflowAttentionShortTTL
|
|
c.maxSize = workflowAttentionCacheMaxEntries
|
|
return c
|
|
}
|
|
|
|
func workflowRunsCacheKey(cacheScope, owner, repo, headSHA string) string {
|
|
return scopedCacheKey(cacheScope, fmt.Sprintf(
|
|
"workflow-runs:%s/%s@%s", strings.ToLower(owner), strings.ToLower(repo), strings.ToLower(headSHA),
|
|
))
|
|
}
|
|
|
|
func workflowJobsCacheKey(cacheScope, owner, repo string, runID int64, attempt int) string {
|
|
return scopedCacheKey(cacheScope, fmt.Sprintf(
|
|
"workflow-jobs:%s/%s#%d/%d", strings.ToLower(owner), strings.ToLower(repo), runID, attempt,
|
|
))
|
|
}
|
|
|
|
func workflowRunsCachePrefix(cacheScope, owner, repo string) string {
|
|
return scopedCacheKey(cacheScope, fmt.Sprintf(
|
|
"workflow-runs:%s/%s@", strings.ToLower(owner), strings.ToLower(repo),
|
|
))
|
|
}
|
|
|
|
func workflowJobsCachePrefix(cacheScope, owner, repo string) string {
|
|
return scopedCacheKey(cacheScope, fmt.Sprintf(
|
|
"workflow-jobs:%s/%s#", strings.ToLower(owner), strings.ToLower(repo),
|
|
))
|
|
}
|
|
|
|
func workflowRunsCacheTTL(value any) time.Duration {
|
|
runs, ok := value.([]WorkflowRun)
|
|
if !ok || len(runs) == 0 {
|
|
return workflowAttentionShortTTL
|
|
}
|
|
for _, run := range runs {
|
|
if !strings.EqualFold(run.Status, workflowStatusCompleted) ||
|
|
run.Conclusion == "" || strings.EqualFold(run.Conclusion, workflowConclusionActionRequired) {
|
|
return workflowAttentionShortTTL
|
|
}
|
|
}
|
|
return workflowAttentionLongTTL
|
|
}
|
|
|
|
func workflowJobsCacheTTL(value any) time.Duration {
|
|
jobs, ok := value.([]WorkflowJob)
|
|
if !ok || len(jobs) == 0 {
|
|
return workflowAttentionShortTTL
|
|
}
|
|
for _, job := range jobs {
|
|
if !strings.EqualFold(job.Status, workflowStatusCompleted) || job.Conclusion == "" {
|
|
return workflowAttentionShortTTL
|
|
}
|
|
}
|
|
return workflowAttentionLongTTL
|
|
}
|
|
|
|
func (s *Service) cachedWorkflowRuns(
|
|
ctx context.Context, client Client, cacheScope, owner, repo, headSHA string,
|
|
) ([]WorkflowRun, error) {
|
|
if s == nil || s.workflowRunsCache == nil {
|
|
return client.ListWorkflowRuns(ctx, owner, repo, headSHA)
|
|
}
|
|
key := workflowRunsCacheKey(cacheScope, owner, repo, headSHA)
|
|
value, err := s.workflowRunsCache.doOrFetchWithTTL(key, func() (any, error) {
|
|
return client.ListWorkflowRuns(ctx, owner, repo, headSHA)
|
|
}, workflowRunsCacheTTL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
runs, ok := value.([]WorkflowRun)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid cached workflow runs value")
|
|
}
|
|
return runs, nil
|
|
}
|
|
|
|
func (s *Service) cachedWorkflowJobs(
|
|
ctx context.Context,
|
|
client Client,
|
|
cacheScope, owner, repo string,
|
|
runID int64, attempt int, shortLived bool,
|
|
) ([]WorkflowJob, error) {
|
|
if s == nil || s.workflowJobsCache == nil {
|
|
return client.ListWorkflowRunJobs(ctx, owner, repo, runID, attempt)
|
|
}
|
|
key := workflowJobsCacheKey(cacheScope, owner, repo, runID, attempt)
|
|
ttlFor := workflowJobsCacheTTL
|
|
if shortLived {
|
|
ttlFor = func(any) time.Duration { return workflowAttentionShortTTL }
|
|
}
|
|
value, err := s.workflowJobsCache.doOrFetchWithTTL(key, func() (any, error) {
|
|
return client.ListWorkflowRunJobs(ctx, owner, repo, runID, attempt)
|
|
}, ttlFor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
jobs, ok := value.([]WorkflowJob)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid cached workflow jobs value")
|
|
}
|
|
return jobs, nil
|
|
}
|
|
|
|
func (s *Service) collectWorkflowAttention(
|
|
ctx context.Context, client Client, cacheScope, owner, repo string, pr *PR,
|
|
) (*WorkflowAttention, error) {
|
|
if pr == nil {
|
|
return workflowAttentionUnknown(""), nil
|
|
}
|
|
if isTerminalPR(pr) {
|
|
return workflowAttentionNone(pr.HeadSHA), nil
|
|
}
|
|
if pr.HeadSHA == "" {
|
|
return workflowAttentionUnknown(""), nil
|
|
}
|
|
|
|
runs, err := s.cachedWorkflowRuns(ctx, client, cacheScope, owner, repo, pr.HeadSHA)
|
|
if err != nil {
|
|
return workflowAttentionUnknown(pr.HeadSHA), err
|
|
}
|
|
return classifyWorkflowAttentionWithJobs(ctx, owner, repo, pr, runs,
|
|
func(jobCtx context.Context, runID int64, attempt int) ([]WorkflowJob, error) {
|
|
return s.cachedWorkflowJobs(jobCtx, client, cacheScope, owner, repo, runID, attempt, true)
|
|
},
|
|
), nil
|
|
}
|
|
|
|
func (s *Service) invalidateWorkflowAttentionForPR(
|
|
cacheScope, owner, repo string, number int, headSHA string,
|
|
) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
key := scopedCacheKey(cacheScope, prStatusCacheKey(owner, repo, number))
|
|
if number > 0 {
|
|
if s.prStatusCache != nil {
|
|
s.prStatusCache.invalidateKey(key)
|
|
}
|
|
if s.prFeedbackCache != nil {
|
|
s.prFeedbackCache.invalidateKey(key)
|
|
}
|
|
}
|
|
if s.workflowRunsCache != nil {
|
|
if headSHA == "" {
|
|
s.workflowRunsCache.invalidatePrefix(workflowRunsCachePrefix(cacheScope, owner, repo))
|
|
} else {
|
|
s.workflowRunsCache.invalidateKey(workflowRunsCacheKey(cacheScope, owner, repo, headSHA))
|
|
}
|
|
}
|
|
if s.workflowJobsCache != nil {
|
|
s.workflowJobsCache.invalidatePrefix(workflowJobsCachePrefix(cacheScope, owner, repo))
|
|
}
|
|
}
|
|
|
|
// invalidateWorkflowAttentionForResolvedPR clears the automation namespace
|
|
// and the personal-read namespace for the same resolved credential. The
|
|
// status synchronizer uses the former, while the feedback endpoint uses the
|
|
// latter, so an explicit refresh must invalidate both views together.
|
|
func (s *Service) invalidateWorkflowAttentionForResolvedPR(
|
|
resolved *resolvedServiceClient, owner, repo string, number int, headSHA string,
|
|
) {
|
|
if resolved == nil {
|
|
return
|
|
}
|
|
s.invalidateWorkflowAttentionForPR(resolved.CacheScope, owner, repo, number, headSHA)
|
|
personalScope := resolved.cacheScopeForPurpose(CredentialPurposePersonalRead)
|
|
if personalScope != "" && personalScope != resolved.CacheScope {
|
|
s.invalidateWorkflowAttentionForPR(personalScope, owner, repo, number, headSHA)
|
|
}
|
|
}
|
|
|
|
func (s *Service) invalidateWorkflowAttentionForResolvedWatches(
|
|
resolved *resolvedServiceClient, watches []*PRWatch,
|
|
) {
|
|
if resolved == nil {
|
|
return
|
|
}
|
|
for _, watch := range watches {
|
|
if watch == nil {
|
|
continue
|
|
}
|
|
s.invalidateWorkflowAttentionForResolvedPR(
|
|
resolved, watch.Owner, watch.Repo, watch.PRNumber, "",
|
|
)
|
|
}
|
|
}
|