mirror of
https://github.com/lukaszraczylo/ghtracker.git
synced 2026-10-12 00:45:38 +00:00
feat(alerts): raise one alert per open issue and pull request
Each open issue now has its own alert. An issue with recent activity is issue_fresh. An issue idle for issue_stale_after or longer is issue_stale. This replaces the single issues_stale line per repo. A non-draft pull request younger than pr_warn_after now raises pr_fresh. Older pull requests keep pr_waiting. All new alerts have warn severity, so a repo with open work is no longer green. The pr_open alert for auto actions is unchanged.
This commit is contained in:
@@ -17,11 +17,11 @@ Release archives and the image checksums are signed with cosign.
|
|||||||
The dashboard lists only what needs attention, grouped by project. The project with the newest problem comes first:
|
The dashboard lists only what needs attention, grouped by project. The project with the newest problem comes first:
|
||||||
|
|
||||||
- Failing workflows, including scheduled and other background ones, with a link to the run.
|
- Failing workflows, including scheduled and other background ones, with a link to the run.
|
||||||
- Pull requests that wait too long or fail their checks, with links and age.
|
- Pull requests, one line each: new ones (`pr_fresh`), ones that wait too long (`pr_waiting`) and ones that fail their checks.
|
||||||
- Stale issues, as one line per project.
|
- Open issues, one line each: new ones (`issue_fresh`) and ones without recent activity (`issue_stale`).
|
||||||
- Projects that failed to refresh, or loaded only in part.
|
- Projects that failed to refresh, or loaded only in part.
|
||||||
|
|
||||||
A side panel lists every project with its latest release (or newest tag), a link to the release, and counts of workflows, open pull requests and issues. Healthy items do not appear anywhere else.
|
A side panel lists every project with its latest release (or newest tag), a link to the release, and counts of workflows, open pull requests and issues. Every open non-draft pull request and every open issue raises a warning, so a project with open work is never green.
|
||||||
|
|
||||||
### Search and filters
|
### Search and filters
|
||||||
|
|
||||||
@@ -48,7 +48,9 @@ A side panel lists every project with its latest release (or newest tag), a link
|
|||||||
| Non-draft PR open longer than `pr_warn_after` | warn |
|
| Non-draft PR open longer than `pr_warn_after` | warn |
|
||||||
| PR checks failing | warn |
|
| PR checks failing | warn |
|
||||||
| Workflow queued or running longer than `workflow_stuck_after` | warn |
|
| Workflow queued or running longer than `workflow_stuck_after` | warn |
|
||||||
| Open issues without activity for `issue_stale_after` (one alert per repo) | warn |
|
| Non-draft PR open less than `pr_warn_after` (`pr_fresh`) | warn |
|
||||||
|
| Open issue without activity for `issue_stale_after` (`issue_stale`, one alert per issue) | warn |
|
||||||
|
| Open issue with recent activity (`issue_fresh`, one alert per issue) | warn |
|
||||||
| A section failed to load, for example a missing App permission | warn |
|
| A section failed to load, for example a missing App permission | warn |
|
||||||
| Data older than `data_stale_after` | warn |
|
| Data older than `data_stale_after` | warn |
|
||||||
|
|
||||||
@@ -121,17 +123,17 @@ actions:
|
|||||||
|
|
||||||
All metrics use the `ghtracker_` prefix. Gauges are computed at scrape time.
|
All metrics use the `ghtracker_` prefix. Gauges are computed at scrape time.
|
||||||
|
|
||||||
| Metric | Labels |
|
| Metric | Labels |
|
||||||
| -------------------------------------------------------------------------------------------------------------- | -------------------------- |
|
| ----------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
|
||||||
| `repo_up`, `repo_health` (0 ok, 1 warn, 2 crit) | repo |
|
| `repo_up`, `repo_health` (0 ok, 1 warn, 2 crit) | repo |
|
||||||
| `repo_open_issues`, `repo_open_prs`, `repo_open_prs_failing_checks`, `repo_stale_issues` | repo |
|
| `repo_open_issues`, `repo_open_prs`, `repo_open_prs_failing_checks`, `repo_stale_issues` | repo |
|
||||||
| `repo_oldest_pr_age_seconds`, `repo_last_refresh_timestamp_seconds` | repo |
|
| `repo_oldest_pr_age_seconds`, `repo_last_refresh_timestamp_seconds` | repo |
|
||||||
| `pr_age_seconds`, `pr_checks_failing` | repo, number |
|
| `pr_age_seconds`, `pr_checks_failing` | repo, number |
|
||||||
| `workflow_failing`, `workflow_running`, `workflow_last_run_timestamp_seconds` | repo, workflow, background |
|
| `workflow_failing`, `workflow_running`, `workflow_last_run_timestamp_seconds` | repo, workflow, background |
|
||||||
| `release_info`, `release_published_timestamp_seconds` | repo, version, url |
|
| `release_info`, `release_published_timestamp_seconds` | repo, version, url |
|
||||||
| `alerts` | severity |
|
| `alerts` | severity |
|
||||||
| `refreshes_total`, `repo_refresh_errors_total`, `last_refresh_duration_seconds`, `github_rate_limit_remaining` | none |
|
| `refreshes_total`, `repo_refresh_errors_total`, `last_refresh_duration_seconds`, `github_rate_limit_remaining` | none |
|
||||||
| `action_requests_total` (counter; result is `ok`, `rejected` or `error`, plus `auto_*` for auto actions; only with `actions`) | action, result |
|
| `action_requests_total` (counter; result is `ok`, `rejected` or `error`, plus `auto_*` for auto actions; only with `actions`) | action, result |
|
||||||
|
|
||||||
Example alert rules:
|
Example alert rules:
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func TestEvaluate(t *testing.T) {
|
|||||||
}, map[string]model.Severity{KindWorkflowStuck: model.Warn}, model.Warn},
|
}, map[string]model.Severity{KindWorkflowStuck: model.Warn}, model.Warn},
|
||||||
"pr fresh": {func(r *model.Repo) {
|
"pr fresh": {func(r *model.Repo) {
|
||||||
r.PRs = []model.PullRequest{{Number: 1, Fork: true, CreatedAt: t0.Add(-47 * time.Hour)}}
|
r.PRs = []model.PullRequest{{Number: 1, Fork: true, CreatedAt: t0.Add(-47 * time.Hour)}}
|
||||||
}, map[string]model.Severity{}, model.OK},
|
}, map[string]model.Severity{KindPRFresh: model.Warn}, model.Warn},
|
||||||
"pr waiting warn": {func(r *model.Repo) {
|
"pr waiting warn": {func(r *model.Repo) {
|
||||||
r.PRs = []model.PullRequest{{Number: 1, Fork: true, CreatedAt: t0.Add(-49 * time.Hour)}}
|
r.PRs = []model.PullRequest{{Number: 1, Fork: true, CreatedAt: t0.Add(-49 * time.Hour)}}
|
||||||
}, map[string]model.Severity{KindPRWaiting: model.Warn}, model.Warn},
|
}, map[string]model.Severity{KindPRWaiting: model.Warn}, model.Warn},
|
||||||
@@ -79,11 +79,13 @@ func TestEvaluate(t *testing.T) {
|
|||||||
}, map[string]model.Severity{}, model.OK},
|
}, map[string]model.Severity{}, model.OK},
|
||||||
"pr checks failing": {func(r *model.Repo) {
|
"pr checks failing": {func(r *model.Repo) {
|
||||||
r.PRs = []model.PullRequest{{Number: 1, Fork: true, CreatedAt: t0, Checks: model.CheckSummary{State: model.ChecksFail, Failing: []string{"build"}}}}
|
r.PRs = []model.PullRequest{{Number: 1, Fork: true, CreatedAt: t0, Checks: model.CheckSummary{State: model.ChecksFail, Failing: []string{"build"}}}}
|
||||||
}, map[string]model.Severity{KindPRChecks: model.Warn}, model.Warn},
|
}, map[string]model.Severity{KindPRFresh: model.Warn, KindPRChecks: model.Warn}, model.Warn},
|
||||||
"issues stale aggregated": {func(r *model.Repo) {
|
"issue stale": {func(r *model.Repo) {
|
||||||
old := t0.Add(-40 * 24 * time.Hour)
|
r.Issues = []model.Issue{{Number: 1, CreatedAt: t0.Add(-60 * 24 * time.Hour), UpdatedAt: t0.Add(-40 * 24 * time.Hour)}}
|
||||||
r.Issues = []model.Issue{{Number: 1, UpdatedAt: old}, {Number: 2, UpdatedAt: old}, {Number: 3, UpdatedAt: t0}}
|
}, map[string]model.Severity{KindIssueStale: model.Warn}, model.Warn},
|
||||||
}, map[string]model.Severity{KindIssuesStale: model.Warn}, model.Warn},
|
"issue fresh": {func(r *model.Repo) {
|
||||||
|
r.Issues = []model.Issue{{Number: 1, CreatedAt: t0.Add(-time.Hour), UpdatedAt: t0}}
|
||||||
|
}, map[string]model.Severity{KindIssueFresh: model.Warn}, model.Warn},
|
||||||
"archived repo skips work alerts": {func(r *model.Repo) {
|
"archived repo skips work alerts": {func(r *model.Repo) {
|
||||||
r.Archived = true
|
r.Archived = true
|
||||||
r.PRs = []model.PullRequest{{Number: 1, Fork: true, CreatedAt: t0.Add(-30 * 24 * time.Hour)}}
|
r.PRs = []model.PullRequest{{Number: 1, Fork: true, CreatedAt: t0.Add(-30 * 24 * time.Hour)}}
|
||||||
@@ -125,8 +127,10 @@ func TestEvaluateDisabledThresholds(t *testing.T) {
|
|||||||
r := model.Repo{FullName: "o/r", RefreshedAt: t0, PRs: []model.PullRequest{{Number: 1, Fork: true, CreatedAt: t0.Add(-100 * 24 * time.Hour)}},
|
r := model.Repo{FullName: "o/r", RefreshedAt: t0, PRs: []model.PullRequest{{Number: 1, Fork: true, CreatedAt: t0.Add(-100 * 24 * time.Hour)}},
|
||||||
Issues: []model.Issue{{UpdatedAt: t0.Add(-100 * 24 * time.Hour)}}}
|
Issues: []model.Issue{{UpdatedAt: t0.Add(-100 * 24 * time.Hour)}}}
|
||||||
off := config.Thresholds{PRWarnAfter: -1, PRCritAfter: -1, IssueStaleAfter: -1, WorkflowStuck: -1, DataStaleAfter: -1}
|
off := config.Thresholds{PRWarnAfter: -1, PRCritAfter: -1, IssueStaleAfter: -1, WorkflowStuck: -1, DataStaleAfter: -1}
|
||||||
if a := withoutPROpen(Evaluate(&r, t0, off)); len(a) != 0 {
|
for _, a := range withoutPROpen(Evaluate(&r, t0, off)) {
|
||||||
t.Fatalf("disabled checks produced %+v", a)
|
if a.Kind != KindPRFresh && a.Kind != KindIssueFresh {
|
||||||
|
t.Fatalf("disabled checks produced %+v", a)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,11 +138,11 @@ func TestAgeAdvancesWithoutRefresh(t *testing.T) {
|
|||||||
r := model.Repo{FullName: "o/r", RefreshedAt: t0, PRs: []model.PullRequest{{Number: 1, Fork: true, CreatedAt: t0.Add(-40 * time.Hour)}}}
|
r := model.Repo{FullName: "o/r", RefreshedAt: t0, PRs: []model.PullRequest{{Number: 1, Fork: true, CreatedAt: t0.Add(-40 * time.Hour)}}}
|
||||||
cfg := th()
|
cfg := th()
|
||||||
cfg.DataStaleAfter = -1
|
cfg.DataStaleAfter = -1
|
||||||
if a := withoutPROpen(Evaluate(&r, t0, cfg)); len(a) != 0 {
|
if a := withoutPROpen(Evaluate(&r, t0, cfg)); len(a) != 1 || a[0].Kind != KindPRFresh {
|
||||||
t.Fatalf("unexpected %+v", a)
|
t.Fatalf("a PR below the warn threshold must be fresh: %+v", a)
|
||||||
}
|
}
|
||||||
if a := withoutPROpen(Evaluate(&r, t0.Add(10*time.Hour), cfg)); len(a) != 1 || a[0].Severity != model.Warn {
|
if a := withoutPROpen(Evaluate(&r, t0.Add(10*time.Hour), cfg)); len(a) != 1 || a[0].Kind != KindPRWaiting {
|
||||||
t.Fatalf("PR must cross the warn threshold as time passes: %+v", a)
|
t.Fatalf("PR must turn from fresh to waiting as time passes: %+v", a)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,9 +494,6 @@ func TestEvaluatePROpen(t *testing.T) {
|
|||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
r := model.Repo{FullName: "o/r", Up: true, RefreshedAt: t0, Archived: tc.archived, PRs: []model.PullRequest{tc.pr}}
|
r := model.Repo{FullName: "o/r", Up: true, RefreshedAt: t0, Archived: tc.archived, PRs: []model.PullRequest{tc.pr}}
|
||||||
alerts := Evaluate(&r, t0, th())
|
alerts := Evaluate(&r, t0, th())
|
||||||
if r.Health != model.OK {
|
|
||||||
t.Fatalf("health = %v, want ok", r.Health)
|
|
||||||
}
|
|
||||||
if !tc.want {
|
if !tc.want {
|
||||||
if len(alerts) != 0 {
|
if len(alerts) != 0 {
|
||||||
t.Fatalf("alerts = %+v, want none", alerts)
|
t.Fatalf("alerts = %+v, want none", alerts)
|
||||||
@@ -501,21 +502,31 @@ func TestEvaluatePROpen(t *testing.T) {
|
|||||||
}
|
}
|
||||||
want := model.Alert{Severity: model.OK, Repo: "o/r", Kind: KindPROpen, Subject: "#7 fix", Detail: "open for 5h",
|
want := model.Alert{Severity: model.OK, Repo: "o/r", Kind: KindPROpen, Subject: "#7 fix", Detail: "open for 5h",
|
||||||
URL: tc.pr.URL, Ref: "abc123", Author: "alice", Since: tc.pr.CreatedAt}
|
URL: tc.pr.URL, Ref: "abc123", Author: "alice", Since: tc.pr.CreatedAt}
|
||||||
if len(alerts) != 1 || alerts[0] != want {
|
if alerts = onlyPROpen(alerts); len(alerts) != 1 || alerts[0] != want {
|
||||||
t.Fatalf("alerts = %+v, want [%+v]", alerts, want)
|
t.Fatalf("alerts = %+v, want [%+v]", alerts, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPROpenDoesNotChangeCounts(t *testing.T) {
|
func onlyPROpen(in []model.Alert) []model.Alert {
|
||||||
|
var out []model.Alert
|
||||||
|
for _, a := range in {
|
||||||
|
if a.Kind == KindPROpen {
|
||||||
|
out = append(out, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPROpenHasNoSeverityOfItsOwn(t *testing.T) {
|
||||||
f := &fakeFetcher{fn: func(name string) (model.Repo, error) {
|
f := &fakeFetcher{fn: func(name string) (model.Repo, error) {
|
||||||
return model.Repo{FullName: name, PRs: []model.PullRequest{{Number: 1, HeadSHA: "s", CreatedAt: t0}}}, nil
|
return model.Repo{FullName: name, PRs: []model.PullRequest{{Number: 1, HeadSHA: "s", CreatedAt: t0}}}, nil
|
||||||
}}
|
}}
|
||||||
c := newCollector(f, "o/a")
|
c := newCollector(f, "o/a")
|
||||||
c.Refresh(context.Background())
|
c.Refresh(context.Background())
|
||||||
v := c.View()
|
v := c.View()
|
||||||
if len(v.Alerts) != 1 || v.Alerts[0].Kind != KindPROpen || v.Crit != 0 || v.Warn != 0 || v.Repos[0].Health != model.OK {
|
if len(onlyPROpen(v.Alerts)) != 1 || v.Alerts[0].Severity != model.Warn || v.Crit != 0 || v.Warn != 1 {
|
||||||
t.Fatalf("view = %+v", v)
|
t.Fatalf("view = %+v", v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ const (
|
|||||||
KindWorkflowStuck = "workflow_stuck"
|
KindWorkflowStuck = "workflow_stuck"
|
||||||
KindPRWaiting = "pr_waiting"
|
KindPRWaiting = "pr_waiting"
|
||||||
KindPRChecks = "pr_checks_failing"
|
KindPRChecks = "pr_checks_failing"
|
||||||
KindIssuesStale = "issues_stale"
|
KindPRFresh = "pr_fresh"
|
||||||
|
KindIssueStale = "issue_stale"
|
||||||
|
KindIssueFresh = "issue_fresh"
|
||||||
// KindPROpen is informational plumbing for automatic actions; it has OK severity so it never moves health or counts.
|
// KindPROpen is informational plumbing for automatic actions; it has OK severity so it never moves health or counts.
|
||||||
KindPROpen = "pr_open"
|
KindPROpen = "pr_open"
|
||||||
)
|
)
|
||||||
@@ -71,27 +73,20 @@ func Evaluate(r *model.Repo, now time.Time, th config.Thresholds) []model.Alert
|
|||||||
add(model.Crit, KindPRWaiting, subject, "open "+Age(age), pr.URL, pr.CreatedAt)
|
add(model.Crit, KindPRWaiting, subject, "open "+Age(age), pr.URL, pr.CreatedAt)
|
||||||
case th.PRWarnAfter > 0 && age >= th.PRWarnAfter:
|
case th.PRWarnAfter > 0 && age >= th.PRWarnAfter:
|
||||||
add(model.Warn, KindPRWaiting, subject, "open "+Age(age), pr.URL, pr.CreatedAt)
|
add(model.Warn, KindPRWaiting, subject, "open "+Age(age), pr.URL, pr.CreatedAt)
|
||||||
|
default:
|
||||||
|
add(model.Warn, KindPRFresh, subject, "new, open "+Age(age), pr.URL, pr.CreatedAt)
|
||||||
}
|
}
|
||||||
if pr.Checks.State == model.ChecksFail {
|
if pr.Checks.State == model.ChecksFail {
|
||||||
add(model.Warn, KindPRChecks, subject, summarizeChecks(pr.Checks), pr.URL, pr.UpdatedAt)
|
add(model.Warn, KindPRChecks, subject, summarizeChecks(pr.Checks), pr.URL, pr.UpdatedAt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if th.IssueStaleAfter > 0 {
|
for _, is := range r.Issues {
|
||||||
var stale int
|
subject := fmt.Sprintf("#%d %s", is.Number, is.Title)
|
||||||
var oldest time.Time
|
if idle := now.Sub(is.UpdatedAt); th.IssueStaleAfter > 0 && idle >= th.IssueStaleAfter {
|
||||||
for _, is := range r.Issues {
|
add(model.Warn, KindIssueStale, subject, "no activity for "+Age(idle), is.URL, is.UpdatedAt)
|
||||||
if now.Sub(is.UpdatedAt) >= th.IssueStaleAfter {
|
} else {
|
||||||
stale++
|
add(model.Warn, KindIssueFresh, subject, "new, open "+Age(now.Sub(is.CreatedAt)), is.URL, is.CreatedAt)
|
||||||
if oldest.IsZero() || is.UpdatedAt.Before(oldest) {
|
|
||||||
oldest = is.UpdatedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if stale > 0 {
|
|
||||||
add(model.Warn, KindIssuesStale, fmt.Sprintf("%d stale issues", stale),
|
|
||||||
"no activity for "+Age(th.IssueStaleAfter)+" or more",
|
|
||||||
r.URL+"/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-asc", oldest)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return alerts
|
return alerts
|
||||||
|
|||||||
@@ -138,10 +138,10 @@ func TestNewAutoRunnerNilWithoutAutoActions(t *testing.T) {
|
|||||||
|
|
||||||
func TestStateCarriesPROpenRef(t *testing.T) {
|
func TestStateCarriesPROpenRef(t *testing.T) {
|
||||||
st := buildState(autoView(autoPR(1, "abc", "x", nil)), true)
|
st := buildState(autoView(autoPR(1, "abc", "x", nil)), true)
|
||||||
if len(st.Alerts) != 1 || st.Alerts[0].Kind != collector.KindPROpen || st.Alerts[0].Ref != "abc" || st.Alerts[0].Severity != "ok" {
|
if len(st.Alerts) != 2 || st.Alerts[0].Kind != collector.KindPROpen || st.Alerts[0].Ref != "abc" || st.Alerts[0].Severity != "ok" || st.Alerts[1].Kind != collector.KindPRFresh {
|
||||||
t.Fatalf("alerts = %+v", st.Alerts)
|
t.Fatalf("alerts = %+v", st.Alerts)
|
||||||
}
|
}
|
||||||
if st.Counts.Crit != 0 || st.Counts.Warn != 0 || st.Repos[0].Health != "ok" {
|
if st.Repos[0].Health != "warn" {
|
||||||
t.Fatalf("counts/health changed: %+v %s", st.Counts, st.Repos[0].Health)
|
t.Fatalf("health = %s, want warn", st.Repos[0].Health)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ describe('AttentionRow', () => {
|
|||||||
it('omits the age for an unknown timestamp', () => {
|
it('omits the age for an unknown timestamp', () => {
|
||||||
const w = mount(AttentionRow, {
|
const w = mount(AttentionRow, {
|
||||||
props: {
|
props: {
|
||||||
row: row({ since: '0001-01-01T00:00:00Z', kind: 'issues_stale' }),
|
row: row({ since: '0001-01-01T00:00:00Z', kind: 'issue_stale' }),
|
||||||
nowMs,
|
nowMs,
|
||||||
critSeconds: CRIT_SECONDS,
|
critSeconds: CRIT_SECONDS,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const fleet = () =>
|
|||||||
}),
|
}),
|
||||||
alert({
|
alert({
|
||||||
repo: 'b/two',
|
repo: 'b/two',
|
||||||
kind: 'issues_stale',
|
kind: 'issue_stale',
|
||||||
severity: 'warn',
|
severity: 'warn',
|
||||||
subject: '4 stale issues',
|
subject: '4 stale issues',
|
||||||
url: '3',
|
url: '3',
|
||||||
@@ -62,7 +62,7 @@ describe('kindGroup', () => {
|
|||||||
['workflow_stuck', 'workflow'],
|
['workflow_stuck', 'workflow'],
|
||||||
['pr_waiting', 'pr'],
|
['pr_waiting', 'pr'],
|
||||||
['pr_checks_failing', 'pr'],
|
['pr_checks_failing', 'pr'],
|
||||||
['issues_stale', 'issue'],
|
['issue_stale', 'issue'],
|
||||||
['refresh_failed', 'data'],
|
['refresh_failed', 'data'],
|
||||||
['data_stale', 'data'],
|
['data_stale', 'data'],
|
||||||
['partial_data', 'data'],
|
['partial_data', 'data'],
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ export const emptyFilters = (): Filters => ({
|
|||||||
export function kindGroup(kind: string): KindGroup {
|
export function kindGroup(kind: string): KindGroup {
|
||||||
if (kind.startsWith('workflow_')) return 'workflow'
|
if (kind.startsWith('workflow_')) return 'workflow'
|
||||||
if (kind.startsWith('pr_')) return 'pr'
|
if (kind.startsWith('pr_')) return 'pr'
|
||||||
if (kind === 'issues_stale') return 'issue'
|
if (kind.startsWith('issue_')) return 'issue'
|
||||||
return 'data'
|
return 'data'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -14,7 +14,9 @@ export const KIND_ICON: Record<string, IconDefinition> = {
|
|||||||
workflow_stuck: faGears,
|
workflow_stuck: faGears,
|
||||||
pr_waiting: faCodePullRequest,
|
pr_waiting: faCodePullRequest,
|
||||||
pr_checks_failing: faCodePullRequest,
|
pr_checks_failing: faCodePullRequest,
|
||||||
issues_stale: faCircleDot,
|
pr_fresh: faCodePullRequest,
|
||||||
|
issue_stale: faCircleDot,
|
||||||
|
issue_fresh: faCircleDot,
|
||||||
refresh_failed: faPlugCircleXmark,
|
refresh_failed: faPlugCircleXmark,
|
||||||
data_stale: faClockRotateLeft,
|
data_stale: faClockRotateLeft,
|
||||||
partial_data: faTriangleExclamation,
|
partial_data: faTriangleExclamation,
|
||||||
|
|||||||
Reference in New Issue
Block a user