@kernel-sig/console 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +50 -0
- package/LICENSE +127 -0
- package/Makefile +66 -0
- package/README.md +279 -0
- package/backend/.dockerignore +12 -0
- package/backend/Dockerfile +43 -0
- package/backend/alembic/env.py +75 -0
- package/backend/alembic/script.py.mako +25 -0
- package/backend/alembic/versions/0001_initial.py +88 -0
- package/backend/alembic/versions/350e2d9b6553_add_pr_comment_table.py +48 -0
- package/backend/alembic/versions/4f8342e727fe_sig_info_roster_fields_and_branch_.py +39 -0
- package/backend/alembic/versions/5a31ade90136_add_repository_credential_pull_request_.py +293 -0
- package/backend/alembic/versions/87a008142f17_add_classification_attention_and_ai_.py +436 -0
- package/backend/alembic/versions/a1c7d90e4b52_branch_belongs_to_a_repository.py +72 -0
- package/backend/alembic/versions/b41c9d7e5f28_extend_pr_kind_categories.py +43 -0
- package/backend/alembic/versions/c8e4f2a71b93_add_release_kind.py +40 -0
- package/backend/alembic/versions/d1275091dfdc_add_needs_detail_flag_to_pull_request.py +44 -0
- package/backend/alembic/versions/e5b27c9d3a41_downgrade_cve_without_ids.py +54 -0
- package/backend/alembic/versions/ee3159a4eff0_add_sig_meeting_member_and_release_.py +164 -0
- package/backend/alembic/versions/fcf3c186d63b_attention_rule_subscriptions.py +42 -0
- package/backend/alembic.ini +40 -0
- package/backend/app/__init__.py +0 -0
- package/backend/app/api/__init__.py +0 -0
- package/backend/app/api/deps.py +74 -0
- package/backend/app/api/v1/__init__.py +0 -0
- package/backend/app/api/v1/ai.py +386 -0
- package/backend/app/api/v1/analytics.py +48 -0
- package/backend/app/api/v1/attention.py +142 -0
- package/backend/app/api/v1/auth.py +106 -0
- package/backend/app/api/v1/classification.py +192 -0
- package/backend/app/api/v1/health.py +29 -0
- package/backend/app/api/v1/issues.py +143 -0
- package/backend/app/api/v1/pulls.py +388 -0
- package/backend/app/api/v1/repositories.py +273 -0
- package/backend/app/api/v1/router.py +32 -0
- package/backend/app/api/v1/sig.py +565 -0
- package/backend/app/api/v1/users.py +87 -0
- package/backend/app/api/v1/webhooks.py +135 -0
- package/backend/app/core/__init__.py +0 -0
- package/backend/app/core/config.py +72 -0
- package/backend/app/core/crypto.py +74 -0
- package/backend/app/core/db.py +79 -0
- package/backend/app/core/exceptions.py +60 -0
- package/backend/app/core/logging.py +69 -0
- package/backend/app/core/permissions.py +87 -0
- package/backend/app/core/queue.py +57 -0
- package/backend/app/core/security.py +69 -0
- package/backend/app/domain/__init__.py +0 -0
- package/backend/app/domain/attention.py +618 -0
- package/backend/app/domain/classification.py +689 -0
- package/backend/app/domain/meeting.py +408 -0
- package/backend/app/domain/release.py +163 -0
- package/backend/app/domain/review.py +416 -0
- package/backend/app/domain/sig.py +178 -0
- package/backend/app/domain/sig_info.py +201 -0
- package/backend/app/integrations/__init__.py +0 -0
- package/backend/app/integrations/atomgit/__init__.py +0 -0
- package/backend/app/integrations/atomgit/client.py +505 -0
- package/backend/app/integrations/atomgit/models.py +311 -0
- package/backend/app/integrations/llm/__init__.py +0 -0
- package/backend/app/integrations/llm/prompts.py +222 -0
- package/backend/app/integrations/llm/provider.py +326 -0
- package/backend/app/main.py +242 -0
- package/backend/app/middleware/__init__.py +0 -0
- package/backend/app/middleware/audit.py +129 -0
- package/backend/app/middleware/request_context.py +42 -0
- package/backend/app/models/__init__.py +112 -0
- package/backend/app/models/ai.py +214 -0
- package/backend/app/models/attention.py +131 -0
- package/backend/app/models/attention_settings.py +61 -0
- package/backend/app/models/audit.py +53 -0
- package/backend/app/models/base.py +35 -0
- package/backend/app/models/classification.py +144 -0
- package/backend/app/models/credential.py +56 -0
- package/backend/app/models/issue.py +132 -0
- package/backend/app/models/meeting.py +189 -0
- package/backend/app/models/pull_request.py +288 -0
- package/backend/app/models/repository.py +196 -0
- package/backend/app/models/sig.py +194 -0
- package/backend/app/models/user.py +44 -0
- package/backend/app/schemas/__init__.py +0 -0
- package/backend/app/schemas/ai.py +120 -0
- package/backend/app/schemas/attention.py +43 -0
- package/backend/app/schemas/auth.py +26 -0
- package/backend/app/schemas/classification.py +58 -0
- package/backend/app/schemas/common.py +43 -0
- package/backend/app/schemas/pull_request.py +214 -0
- package/backend/app/schemas/repository.py +94 -0
- package/backend/app/schemas/sig.py +246 -0
- package/backend/app/schemas/user.py +79 -0
- package/backend/app/services/__init__.py +0 -0
- package/backend/app/services/ai_service.py +569 -0
- package/backend/app/services/analytics_service.py +390 -0
- package/backend/app/services/attention_queue.py +451 -0
- package/backend/app/services/attention_service.py +432 -0
- package/backend/app/services/auth_service.py +74 -0
- package/backend/app/services/classification_service.py +658 -0
- package/backend/app/services/credential_service.py +105 -0
- package/backend/app/services/pull_query.py +273 -0
- package/backend/app/services/release_service.py +446 -0
- package/backend/app/services/repository_service.py +132 -0
- package/backend/app/services/sig_service.py +385 -0
- package/backend/app/services/sync_service.py +752 -0
- package/backend/app/services/user_service.py +68 -0
- package/backend/app/worker.py +389 -0
- package/backend/entrypoint.sh +10 -0
- package/backend/pyproject.toml +68 -0
- package/backend/tests/test_analysis_api.py +154 -0
- package/backend/tests/test_atomgit_client.py +360 -0
- package/backend/tests/test_atomgit_models.py +257 -0
- package/backend/tests/test_attention.py +291 -0
- package/backend/tests/test_audit_middleware.py +135 -0
- package/backend/tests/test_classification.py +498 -0
- package/backend/tests/test_config.py +41 -0
- package/backend/tests/test_crypto.py +68 -0
- package/backend/tests/test_exceptions.py +62 -0
- package/backend/tests/test_health.py +63 -0
- package/backend/tests/test_llm_provider.py +320 -0
- package/backend/tests/test_meeting_domain.py +169 -0
- package/backend/tests/test_permissions.py +69 -0
- package/backend/tests/test_pull_query_wiring.py +66 -0
- package/backend/tests/test_pull_schemas.py +82 -0
- package/backend/tests/test_release_domain.py +82 -0
- package/backend/tests/test_review_parser.py +301 -0
- package/backend/tests/test_schemas_user.py +97 -0
- package/backend/tests/test_security.py +92 -0
- package/cli/index.js +338 -0
- package/compose.yaml +105 -0
- package/frontend/.dockerignore +4 -0
- package/frontend/Dockerfile +27 -0
- package/frontend/index.html +14 -0
- package/frontend/nginx.conf +47 -0
- package/frontend/package-lock.json +5020 -0
- package/frontend/package.json +35 -0
- package/frontend/src/api/ai.ts +124 -0
- package/frontend/src/api/analytics.ts +66 -0
- package/frontend/src/api/attention.ts +181 -0
- package/frontend/src/api/auth.ts +74 -0
- package/frontend/src/api/classification.ts +170 -0
- package/frontend/src/api/issues.ts +90 -0
- package/frontend/src/api/pulls.ts +233 -0
- package/frontend/src/api/repositories.ts +95 -0
- package/frontend/src/api/sig.ts +232 -0
- package/frontend/src/app/antd-theme.ts +94 -0
- package/frontend/src/app/providers.tsx +59 -0
- package/frontend/src/app/router.tsx +411 -0
- package/frontend/src/app/search.ts +30 -0
- package/frontend/src/components/ClassificationBadge.tsx +58 -0
- package/frontend/src/components/GateBadge.tsx +13 -0
- package/frontend/src/components/SeverityBadge.tsx +20 -0
- package/frontend/src/components/layout/AppShell.tsx +16 -0
- package/frontend/src/components/layout/AuthLayout.tsx +32 -0
- package/frontend/src/components/layout/Sidebar.tsx +223 -0
- package/frontend/src/components/layout/TopBar.tsx +47 -0
- package/frontend/src/components/pulls/DiscussionTimeline.tsx +145 -0
- package/frontend/src/components/pulls/FacetRail.tsx +199 -0
- package/frontend/src/components/pulls/LabelChips.tsx +87 -0
- package/frontend/src/components/ui/alert.tsx +30 -0
- package/frontend/src/components/ui/badge.tsx +53 -0
- package/frontend/src/components/ui/button.tsx +51 -0
- package/frontend/src/components/ui/card.tsx +64 -0
- package/frontend/src/components/ui/chart-theme.ts +65 -0
- package/frontend/src/components/ui/data-table.tsx +39 -0
- package/frontend/src/components/ui/echart.tsx +70 -0
- package/frontend/src/components/ui/empty-state.tsx +22 -0
- package/frontend/src/components/ui/input.tsx +39 -0
- package/frontend/src/components/ui/lazy-chart.tsx +21 -0
- package/frontend/src/components/ui/skeleton.tsx +19 -0
- package/frontend/src/hooks/use-current-repository.ts +44 -0
- package/frontend/src/hooks/use-current-user.ts +38 -0
- package/frontend/src/lib/api-client.ts +93 -0
- package/frontend/src/lib/css-color.ts +60 -0
- package/frontend/src/lib/utils.ts +45 -0
- package/frontend/src/main.tsx +22 -0
- package/frontend/src/pages/attention/AttentionQueuePage.tsx +316 -0
- package/frontend/src/pages/attention/RuleSettingsPanel.tsx +177 -0
- package/frontend/src/pages/branches/BranchDetailPage.tsx +632 -0
- package/frontend/src/pages/branches/BranchListPage.tsx +308 -0
- package/frontend/src/pages/dashboard/DashboardPage.tsx +657 -0
- package/frontend/src/pages/issues/IssueListPage.tsx +284 -0
- package/frontend/src/pages/login/LoginPage.tsx +95 -0
- package/frontend/src/pages/meetings/MeetingDetailPage.tsx +403 -0
- package/frontend/src/pages/meetings/MeetingListPage.tsx +264 -0
- package/frontend/src/pages/members/MembersPage.tsx +534 -0
- package/frontend/src/pages/pulls/PullDetailPage.tsx +833 -0
- package/frontend/src/pages/pulls/PullListPage.tsx +399 -0
- package/frontend/src/pages/settings/AiSettingsPage.tsx +449 -0
- package/frontend/src/pages/settings/SettingsPage.tsx +321 -0
- package/frontend/src/pages/setup/SetupPage.tsx +144 -0
- package/frontend/src/styles/globals.css +147 -0
- package/frontend/tsconfig.json +22 -0
- package/frontend/vite.config.ts +57 -0
- package/package.json +48 -0
- package/scripts/e2e-auth-flow.py +131 -0
- package/scripts/e2e-pr-detail.py +128 -0
- package/scripts/e2e-verify.py +773 -0
- package/scripts/verify-ai-pipeline.py +616 -0
- package/scripts/verify-analysis-pipeline.py +303 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""密码哈希与 JWT 签发/校验。"""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime, timedelta
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import jwt
|
|
8
|
+
from argon2 import PasswordHasher
|
|
9
|
+
from argon2.exceptions import InvalidHashError, VerifyMismatchError
|
|
10
|
+
|
|
11
|
+
_ALGORITHM = "HS256"
|
|
12
|
+
_hasher = PasswordHasher()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TokenType(StrEnum):
|
|
16
|
+
ACCESS = "access"
|
|
17
|
+
REFRESH = "refresh"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def hash_password(password: str) -> str:
|
|
21
|
+
return _hasher.hash(password)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def verify_password(password: str, password_hash: str) -> bool:
|
|
25
|
+
try:
|
|
26
|
+
return _hasher.verify(password_hash, password)
|
|
27
|
+
except (VerifyMismatchError, InvalidHashError, ValueError):
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def needs_rehash(password_hash: str) -> bool:
|
|
32
|
+
try:
|
|
33
|
+
return _hasher.check_needs_rehash(password_hash)
|
|
34
|
+
except (InvalidHashError, ValueError):
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def create_token(
|
|
39
|
+
subject: str,
|
|
40
|
+
token_type: TokenType,
|
|
41
|
+
secret: str,
|
|
42
|
+
expires_delta: timedelta | None = None,
|
|
43
|
+
extra_claims: dict[str, Any] | None = None,
|
|
44
|
+
) -> str:
|
|
45
|
+
now = datetime.now(UTC)
|
|
46
|
+
if expires_delta is None:
|
|
47
|
+
expires_delta = (
|
|
48
|
+
timedelta(minutes=30) if token_type is TokenType.ACCESS else timedelta(days=7)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
payload: dict[str, Any] = {
|
|
52
|
+
"sub": subject,
|
|
53
|
+
"type": token_type.value,
|
|
54
|
+
"iat": int(now.timestamp()),
|
|
55
|
+
"exp": int((now + expires_delta).timestamp()),
|
|
56
|
+
}
|
|
57
|
+
if extra_claims:
|
|
58
|
+
payload.update(extra_claims)
|
|
59
|
+
|
|
60
|
+
return jwt.encode(payload, secret, algorithm=_ALGORITHM)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def decode_token(token: str, secret: str, expected_type: TokenType) -> dict[str, Any]:
|
|
64
|
+
payload = jwt.decode(token, secret, algorithms=[_ALGORITHM])
|
|
65
|
+
if payload.get("type") != expected_type.value:
|
|
66
|
+
raise jwt.InvalidTokenError(
|
|
67
|
+
f"令牌类型不匹配:期望 {expected_type.value},实际 {payload.get('type')}"
|
|
68
|
+
)
|
|
69
|
+
return payload
|
|
File without changes
|
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
"""关注项规则。
|
|
2
|
+
|
|
3
|
+
把散落在上千个 PR 上的问题收敛成一份可操作的待办清单。
|
|
4
|
+
规则是纯函数:输入一个 PR 的快照,输出命中与否及原因。
|
|
5
|
+
|
|
6
|
+
**可解释优先**:每条命中都带上证据(等了几天、哪个文件是二进制、
|
|
7
|
+
缺什么签名),而不是只给一个"有问题"的结论。维护者需要据此判断
|
|
8
|
+
"这条要不要理"、"该怎么回"。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Callable, Sequence
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from datetime import UTC, datetime
|
|
16
|
+
from enum import StrEnum
|
|
17
|
+
|
|
18
|
+
from app.domain.review import GateStage
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Severity(StrEnum):
|
|
22
|
+
BLOCKER = "blocker"
|
|
23
|
+
CRITICAL = "critical"
|
|
24
|
+
WARNING = "warning"
|
|
25
|
+
INFO = "info"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
SEVERITY_ORDER: dict[Severity, int] = {
|
|
29
|
+
Severity.BLOCKER: 0,
|
|
30
|
+
Severity.CRITICAL: 1,
|
|
31
|
+
Severity.WARNING: 2,
|
|
32
|
+
Severity.INFO: 3,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Rule(StrEnum):
|
|
37
|
+
"""规则标识。
|
|
38
|
+
|
|
39
|
+
"持续超时"不单列为规则,而是同一规则内升级 severity ——
|
|
40
|
+
否则同一条 PR 会同时出现"CI 失败"与"CI 长期失败"两条待办,
|
|
41
|
+
使用者还得自己判断哪条更要紧。
|
|
42
|
+
|
|
43
|
+
数据库枚举(app.models.attention.AttentionRule)必须与此保持一致。
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
STALE = "stale"
|
|
47
|
+
REVIEW_SLA_BREACH = "review_sla_breach"
|
|
48
|
+
CI_FAILED = "ci_failed"
|
|
49
|
+
MERGE_CONFLICT = "merge_conflict"
|
|
50
|
+
NEEDS_ISSUE = "needs_issue"
|
|
51
|
+
CLA_DENIED = "cla_denied"
|
|
52
|
+
CLA_PENDING_LONG = "cla_pending_long"
|
|
53
|
+
REJECTED = "rejected"
|
|
54
|
+
BINARY_FILE = "binary_file"
|
|
55
|
+
MISSING_SIGNED_OFF = "missing_signed_off"
|
|
56
|
+
CVE_AGING = "cve_aging"
|
|
57
|
+
ISSUE_UNASSIGNED = "issue_unassigned"
|
|
58
|
+
ISSUE_STALE = "issue_stale"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class Thresholds:
|
|
63
|
+
"""各规则的阈值。集中一处便于按团队节奏调整。"""
|
|
64
|
+
|
|
65
|
+
stale_days: int = 30
|
|
66
|
+
review_sla_days: int = 7
|
|
67
|
+
ci_failed_days: int = 3
|
|
68
|
+
conflict_days: int = 2
|
|
69
|
+
cla_pending_days: int = 1
|
|
70
|
+
cve_aging_hours: int = 24
|
|
71
|
+
# Issue 的时限宽于 PR:Issue 常需长时间讨论,催得太急反而制造噪音
|
|
72
|
+
issue_unassigned_days: int = 7
|
|
73
|
+
issue_stale_days: int = 90
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class PullSnapshot:
|
|
78
|
+
"""规则求值所需的 PR 快照。
|
|
79
|
+
|
|
80
|
+
刻意只取必要字段:规则不该依赖完整 ORM 对象,
|
|
81
|
+
这样它们既能用于实时查询,也能用于批处理,且易于测试。
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
number: int
|
|
85
|
+
title: str
|
|
86
|
+
state: str
|
|
87
|
+
gate_stage: str
|
|
88
|
+
created_at: datetime | None
|
|
89
|
+
updated_at: datetime | None
|
|
90
|
+
is_draft: bool = False
|
|
91
|
+
merge_conflict: bool = False
|
|
92
|
+
label_names: Sequence[str] = field(default_factory=list)
|
|
93
|
+
kind: str = "unknown"
|
|
94
|
+
cve_ids: Sequence[str] = field(default_factory=list)
|
|
95
|
+
binary_files: Sequence[str] = field(default_factory=list)
|
|
96
|
+
commits_without_signed_off: int = 0
|
|
97
|
+
total_commits: int = 0
|
|
98
|
+
changed_files: Sequence[str] = field(default_factory=list)
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def is_open(self) -> bool:
|
|
102
|
+
return self.state == "open"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class IssueSnapshot:
|
|
107
|
+
"""规则求值所需的 Issue 快照。字段取法与 PullSnapshot 同理。"""
|
|
108
|
+
|
|
109
|
+
number: int
|
|
110
|
+
title: str
|
|
111
|
+
state: str
|
|
112
|
+
created_at: datetime | None
|
|
113
|
+
updated_at: datetime | None
|
|
114
|
+
issue_type: str | None = None
|
|
115
|
+
priority: str | None = None
|
|
116
|
+
assignee_login: str | None = None
|
|
117
|
+
label_names: Sequence[str] = field(default_factory=list)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def is_open(self) -> bool:
|
|
121
|
+
return self.state == "open"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class AttentionHit:
|
|
126
|
+
rule: Rule
|
|
127
|
+
severity: Severity
|
|
128
|
+
title: str
|
|
129
|
+
detail: str
|
|
130
|
+
evidence: dict
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _days_since(moment: datetime | None, now: datetime) -> float:
|
|
134
|
+
if moment is None:
|
|
135
|
+
return 0.0
|
|
136
|
+
if moment.tzinfo is None:
|
|
137
|
+
moment = moment.replace(tzinfo=UTC)
|
|
138
|
+
return max(0.0, (now - moment).total_seconds() / 86400)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# 规则实现
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
KABI_BREAKING_MARKERS = ("-EXPORT_SYMBOL", "-EXPORT_SYMBOL_GPL", "-EXPORT_UNWIND_FIRST")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def rule_ci_failed(pull: PullSnapshot, now: datetime, t: Thresholds) -> AttentionHit | None:
|
|
149
|
+
"""CI 失败。这是明确的阻塞信号,优先于"未完成"展示。"""
|
|
150
|
+
if pull.gate_stage not in (GateStage.CI_FAILED.value,):
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
days = _days_since(pull.updated_at, now)
|
|
154
|
+
severity = Severity.CRITICAL if days >= t.ci_failed_days else Severity.WARNING
|
|
155
|
+
return AttentionHit(
|
|
156
|
+
rule=Rule.CI_FAILED,
|
|
157
|
+
severity=severity,
|
|
158
|
+
title=f"CI 未通过(已 {int(days)} 天)" if days >= 1 else "CI 未通过",
|
|
159
|
+
detail="门禁检查失败会阻塞合入,需修复后重新触发。",
|
|
160
|
+
evidence={"days_since_update": round(days, 1), "gate_stage": pull.gate_stage},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def rule_merge_conflict(pull: PullSnapshot, now: datetime, t: Thresholds) -> AttentionHit | None:
|
|
165
|
+
if not pull.merge_conflict or not pull.is_open:
|
|
166
|
+
return None
|
|
167
|
+
days = _days_since(pull.updated_at, now)
|
|
168
|
+
severity = Severity.CRITICAL if days >= t.conflict_days else Severity.WARNING
|
|
169
|
+
return AttentionHit(
|
|
170
|
+
rule=Rule.MERGE_CONFLICT,
|
|
171
|
+
severity=severity,
|
|
172
|
+
title="存在合并冲突",
|
|
173
|
+
detail="目标分支已前进,需要 rebase 或合并后再评审。",
|
|
174
|
+
evidence={"days_since_update": round(days, 1), "target_branch_changed": True},
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def rule_review_sla_breach(pull: PullSnapshot, now: datetime, t: Thresholds) -> AttentionHit | None:
|
|
179
|
+
"""待评审超期。
|
|
180
|
+
|
|
181
|
+
只看"仍在等待评审"的 PR —— 已经拿到 LGTM 的不算超期,
|
|
182
|
+
否则会把已完成的评审也算成欠账。
|
|
183
|
+
"""
|
|
184
|
+
if not pull.is_open or pull.gate_stage != GateStage.REVIEW_PENDING.value:
|
|
185
|
+
return None
|
|
186
|
+
days = _days_since(pull.created_at, now)
|
|
187
|
+
if days < t.review_sla_days:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
severity = Severity.CRITICAL if days >= t.review_sla_days * 3 else Severity.WARNING
|
|
191
|
+
return AttentionHit(
|
|
192
|
+
rule=Rule.REVIEW_SLA_BREACH,
|
|
193
|
+
severity=severity,
|
|
194
|
+
title=f"等待评审已 {int(days)} 天",
|
|
195
|
+
detail=f"超过 {t.review_sla_days} 天的评审时限,建议推进或明确拒绝。",
|
|
196
|
+
evidence={"waiting_days": round(days, 1), "sla_days": t.review_sla_days},
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def rule_cve_aging(pull: PullSnapshot, now: datetime, t: Thresholds) -> AttentionHit | None:
|
|
201
|
+
"""CVE 修复久未推进。安全问题的处理时限远紧于普通 PR。"""
|
|
202
|
+
if not pull.is_open or not pull.cve_ids:
|
|
203
|
+
return None
|
|
204
|
+
if pull.gate_stage in (GateStage.MERGED.value, GateStage.CLOSED.value):
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
hours = _days_since(pull.created_at, now) * 24
|
|
208
|
+
if hours < t.cve_aging_hours:
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
return AttentionHit(
|
|
212
|
+
rule=Rule.CVE_AGING,
|
|
213
|
+
severity=Severity.CRITICAL,
|
|
214
|
+
title=f"CVE 修复已 {int(hours)} 小时未推进",
|
|
215
|
+
# 实测有单次涉及 30+ 个 CVE 的补丁,全量列出会把这条待办本身淹掉。
|
|
216
|
+
# 完整列表在 evidence 里,需要时再取。
|
|
217
|
+
detail=f"涉及 {_cve_preview(pull.cve_ids)},安全修复通常要求 24 小时内响应。",
|
|
218
|
+
evidence={"cve_ids": list(pull.cve_ids), "hours": round(hours, 1)},
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
CVE_PREVIEW_COUNT = 3
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _cve_preview(cve_ids: Sequence[str]) -> str:
|
|
226
|
+
shown = ", ".join(cve_ids[:CVE_PREVIEW_COUNT])
|
|
227
|
+
if len(cve_ids) <= CVE_PREVIEW_COUNT:
|
|
228
|
+
return shown
|
|
229
|
+
return f"{shown} 等 {len(cve_ids)} 项"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def rule_needs_issue(pull: PullSnapshot, now: datetime, t: Thresholds) -> AttentionHit | None:
|
|
233
|
+
"""未关联 Issue。openEuler 的硬性门禁。"""
|
|
234
|
+
if not pull.is_open or pull.gate_stage != GateStage.NEEDS_ISSUE.value:
|
|
235
|
+
return None
|
|
236
|
+
return AttentionHit(
|
|
237
|
+
rule=Rule.NEEDS_ISSUE,
|
|
238
|
+
severity=Severity.WARNING,
|
|
239
|
+
title="未关联 Issue",
|
|
240
|
+
detail="openEuler 要求 PR 至少关联一个 Issue,需在描述中补充 fixes #<编号>。",
|
|
241
|
+
evidence={"gate_stage": pull.gate_stage},
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def rule_cla(pull: PullSnapshot, now: datetime, t: Thresholds) -> AttentionHit | None:
|
|
246
|
+
if not pull.is_open:
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
if pull.gate_stage == GateStage.CLA_DENIED.value:
|
|
250
|
+
return AttentionHit(
|
|
251
|
+
rule=Rule.CLA_DENIED,
|
|
252
|
+
severity=Severity.CRITICAL,
|
|
253
|
+
title="CLA 未通过",
|
|
254
|
+
detail="贡献者许可协议校验失败,无法合入。需贡献者完成签署。",
|
|
255
|
+
evidence={"gate_stage": pull.gate_stage},
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if pull.gate_stage == GateStage.CLA_PENDING.value:
|
|
259
|
+
days = _days_since(pull.created_at, now)
|
|
260
|
+
if days < t.cla_pending_days:
|
|
261
|
+
return None
|
|
262
|
+
return AttentionHit(
|
|
263
|
+
rule=Rule.CLA_PENDING_LONG,
|
|
264
|
+
severity=Severity.WARNING,
|
|
265
|
+
title=f"CLA 已等待 {int(days)} 天",
|
|
266
|
+
detail="CLA 校验长时间未完成,通常意味着签署环节被卡住。",
|
|
267
|
+
evidence={"days": round(days, 1)},
|
|
268
|
+
)
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def rule_rejected(pull: PullSnapshot, now: datetime, t: Thresholds) -> AttentionHit | None:
|
|
273
|
+
if not pull.is_open or pull.gate_stage != GateStage.REJECTED.value:
|
|
274
|
+
return None
|
|
275
|
+
return AttentionHit(
|
|
276
|
+
rule=Rule.REJECTED,
|
|
277
|
+
severity=Severity.CRITICAL,
|
|
278
|
+
title="已被 NACK",
|
|
279
|
+
detail="评审人明确否决,需处理反馈后重新申请评审。",
|
|
280
|
+
evidence={"labels": [n for n in pull.label_names if n == "NACK"]},
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def rule_stale(pull: PullSnapshot, now: datetime, t: Thresholds) -> AttentionHit | None:
|
|
285
|
+
"""长期无活动。"""
|
|
286
|
+
if not pull.is_open:
|
|
287
|
+
return None
|
|
288
|
+
days = _days_since(pull.updated_at, now)
|
|
289
|
+
if days < t.stale_days:
|
|
290
|
+
return None
|
|
291
|
+
return AttentionHit(
|
|
292
|
+
rule=Rule.STALE,
|
|
293
|
+
severity=Severity.WARNING,
|
|
294
|
+
title=f"已 {int(days)} 天无活动",
|
|
295
|
+
detail="长期无更新,考虑推进、明确拒绝或关闭。",
|
|
296
|
+
evidence={"days_since_update": round(days, 1), "threshold_days": t.stale_days},
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def rule_binary_file(pull: PullSnapshot, now: datetime, t: Thresholds) -> AttentionHit | None:
|
|
301
|
+
"""二进制文件。内核仓库明令禁止提交。"""
|
|
302
|
+
if not pull.is_open or not pull.binary_files:
|
|
303
|
+
return None
|
|
304
|
+
preview = list(pull.binary_files[:5])
|
|
305
|
+
return AttentionHit(
|
|
306
|
+
rule=Rule.BINARY_FILE,
|
|
307
|
+
severity=Severity.BLOCKER,
|
|
308
|
+
title=f"含 {len(pull.binary_files)} 个二进制文件",
|
|
309
|
+
detail="内核仓库禁止提交二进制文件:无法审查、增大仓库体积、难以追踪变更。"
|
|
310
|
+
"固件应提交到 linux-firmware,工具应提供源码与构建脚本。",
|
|
311
|
+
evidence={"files": preview, "total": len(pull.binary_files)},
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def rule_missing_signed_off(
|
|
316
|
+
pull: PullSnapshot, now: datetime, t: Thresholds
|
|
317
|
+
) -> AttentionHit | None:
|
|
318
|
+
"""缺少 Signed-off-by。openEuler 要求签名链完整。"""
|
|
319
|
+
if not pull.is_open or pull.total_commits == 0:
|
|
320
|
+
return None
|
|
321
|
+
if pull.commits_without_signed_off == 0:
|
|
322
|
+
return None
|
|
323
|
+
return AttentionHit(
|
|
324
|
+
rule=Rule.MISSING_SIGNED_OFF,
|
|
325
|
+
severity=Severity.WARNING,
|
|
326
|
+
title=f"{pull.commits_without_signed_off} 个提交缺少 Signed-off-by",
|
|
327
|
+
detail="签名链不完整会阻塞合入,需作者用 git commit -s 补签。",
|
|
328
|
+
evidence={
|
|
329
|
+
"missing": pull.commits_without_signed_off,
|
|
330
|
+
"total": pull.total_commits,
|
|
331
|
+
},
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# ---------------------------------------------------------------------------
|
|
336
|
+
# Issue 规则
|
|
337
|
+
# ---------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# 未指派且长期无人认领的 Issue 是社区协作里的黑洞:
|
|
341
|
+
# 报告者等不到回应,维护者也不知道有这条。只有超过宽限期才提示,
|
|
342
|
+
# 避免新提交的 Issue 一进来就出现在待办里。
|
|
343
|
+
def rule_issue_unassigned(
|
|
344
|
+
issue: IssueSnapshot, now: datetime, t: Thresholds
|
|
345
|
+
) -> AttentionHit | None:
|
|
346
|
+
if not issue.is_open or issue.assignee_login:
|
|
347
|
+
return None
|
|
348
|
+
days = _days_since(issue.created_at, now)
|
|
349
|
+
if days < t.issue_unassigned_days:
|
|
350
|
+
return None
|
|
351
|
+
return AttentionHit(
|
|
352
|
+
rule=Rule.ISSUE_UNASSIGNED,
|
|
353
|
+
severity=Severity.WARNING,
|
|
354
|
+
title=f"Issue 已 {int(days)} 天无人认领",
|
|
355
|
+
detail="长期未指派通常意味着报告得不到回应,建议指派或明确排除。",
|
|
356
|
+
evidence={"days_open": round(days, 1), "threshold_days": t.issue_unassigned_days},
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def rule_issue_stale(issue: IssueSnapshot, now: datetime, t: Thresholds) -> AttentionHit | None:
|
|
361
|
+
if not issue.is_open:
|
|
362
|
+
return None
|
|
363
|
+
days = _days_since(issue.updated_at, now)
|
|
364
|
+
if days < t.issue_stale_days:
|
|
365
|
+
return None
|
|
366
|
+
return AttentionHit(
|
|
367
|
+
rule=Rule.ISSUE_STALE,
|
|
368
|
+
severity=Severity.INFO,
|
|
369
|
+
title=f"Issue 已 {int(days)} 天无更新",
|
|
370
|
+
detail="长期停滞,考虑推动、明确结论或关闭。",
|
|
371
|
+
evidence={"days_since_update": round(days, 1), "threshold_days": t.issue_stale_days},
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
ISSUE_RULES: tuple[Callable[[IssueSnapshot, datetime, Thresholds], AttentionHit | None], ...] = (
|
|
376
|
+
rule_issue_unassigned,
|
|
377
|
+
rule_issue_stale,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def evaluate_issue(
|
|
382
|
+
issue: IssueSnapshot,
|
|
383
|
+
now: datetime | None = None,
|
|
384
|
+
thresholds: Thresholds | None = None,
|
|
385
|
+
) -> list[AttentionHit]:
|
|
386
|
+
"""对一个 Issue 求值全部 Issue 规则。已关闭的 Issue 返回空。"""
|
|
387
|
+
if not issue.is_open:
|
|
388
|
+
return []
|
|
389
|
+
|
|
390
|
+
now = now or datetime.now(UTC)
|
|
391
|
+
thresholds = thresholds or Thresholds()
|
|
392
|
+
|
|
393
|
+
hits = [hit for rule in ISSUE_RULES if (hit := rule(issue, now, thresholds)) is not None]
|
|
394
|
+
hits.sort(key=lambda h: (SEVERITY_ORDER[h.severity], h.rule.value))
|
|
395
|
+
return hits
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
ALL_RULES: tuple[Callable[[PullSnapshot, datetime, Thresholds], AttentionHit | None], ...] = (
|
|
399
|
+
rule_binary_file,
|
|
400
|
+
rule_ci_failed,
|
|
401
|
+
rule_merge_conflict,
|
|
402
|
+
rule_cve_aging,
|
|
403
|
+
rule_rejected,
|
|
404
|
+
rule_cla,
|
|
405
|
+
rule_needs_issue,
|
|
406
|
+
rule_missing_signed_off,
|
|
407
|
+
rule_review_sla_breach,
|
|
408
|
+
rule_stale,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def evaluate(
|
|
413
|
+
pull: PullSnapshot,
|
|
414
|
+
now: datetime | None = None,
|
|
415
|
+
thresholds: Thresholds | None = None,
|
|
416
|
+
) -> list[AttentionHit]:
|
|
417
|
+
"""对一个 PR 求值全部规则,按严重程度排序返回。
|
|
418
|
+
|
|
419
|
+
已合并/已关闭的 PR 直接返回空 —— 对它们提"待处理"是噪音。
|
|
420
|
+
"""
|
|
421
|
+
if pull.state in ("merged", "closed"):
|
|
422
|
+
return []
|
|
423
|
+
|
|
424
|
+
now = now or datetime.now(UTC)
|
|
425
|
+
thresholds = thresholds or Thresholds()
|
|
426
|
+
|
|
427
|
+
hits: list[AttentionHit] = []
|
|
428
|
+
for rule in ALL_RULES:
|
|
429
|
+
hit = rule(pull, now, thresholds)
|
|
430
|
+
if hit is not None:
|
|
431
|
+
hits.append(hit)
|
|
432
|
+
|
|
433
|
+
hits.sort(key=lambda h: (SEVERITY_ORDER[h.severity], h.rule.value))
|
|
434
|
+
return hits
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# ---------------------------------------------------------------------------
|
|
438
|
+
# 规则说明(供界面展示"这条是什么意思")
|
|
439
|
+
# ---------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
RULE_DESCRIPTIONS: dict[Rule, str] = {
|
|
442
|
+
Rule.BINARY_FILE: "内核仓库禁止提交二进制文件,属阻塞性问题",
|
|
443
|
+
Rule.CI_FAILED: "门禁检查失败,无法合入",
|
|
444
|
+
Rule.MERGE_CONFLICT: "与目标分支存在冲突",
|
|
445
|
+
Rule.CVE_AGING: "安全修复响应超时",
|
|
446
|
+
Rule.REJECTED: "评审人明确否决",
|
|
447
|
+
Rule.CLA_DENIED: "贡献者许可协议未通过",
|
|
448
|
+
Rule.CLA_PENDING_LONG: "CLA 校验长时间未完成",
|
|
449
|
+
Rule.NEEDS_ISSUE: "未按要求关联 Issue",
|
|
450
|
+
Rule.MISSING_SIGNED_OFF: "补丁缺少 Signed-off-by",
|
|
451
|
+
Rule.REVIEW_SLA_BREACH: "等待评审超出时限",
|
|
452
|
+
Rule.STALE: "长期无活动",
|
|
453
|
+
Rule.ISSUE_UNASSIGNED: "Issue 长期未指派",
|
|
454
|
+
Rule.ISSUE_STALE: "Issue 长期无更新",
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
# ---------------------------------------------------------------------------
|
|
459
|
+
# 规则元信息
|
|
460
|
+
# ---------------------------------------------------------------------------
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@dataclass(frozen=True)
|
|
464
|
+
class RuleInfo:
|
|
465
|
+
"""一条规则对使用者意味着什么。
|
|
466
|
+
|
|
467
|
+
集中一处,供三处共用:关注设置页(说明与阈值)、队列分组(标题与说明)、
|
|
468
|
+
首页摘要。分散写会让同一规则在三处显示成三种说法。
|
|
469
|
+
"""
|
|
470
|
+
|
|
471
|
+
rule: Rule
|
|
472
|
+
label: str
|
|
473
|
+
"""短名,用于标签与分组标题。"""
|
|
474
|
+
|
|
475
|
+
meaning: str
|
|
476
|
+
"""这条规则在提醒什么。使用者要能由此判断"与我有关系吗"。"""
|
|
477
|
+
|
|
478
|
+
action: str
|
|
479
|
+
"""看到之后该做什么。只说问题不给出路,队列就只是噪音来源。"""
|
|
480
|
+
|
|
481
|
+
subject: str
|
|
482
|
+
"""``pull_request`` 或 ``issue``。"""
|
|
483
|
+
|
|
484
|
+
default_days: int
|
|
485
|
+
threshold_field: str
|
|
486
|
+
"""``Thresholds`` 中对应的字段名。设置页改的就是它。"""
|
|
487
|
+
|
|
488
|
+
# 命中的对象通常很多(每个未推进的 CVE PR 都算一条)。
|
|
489
|
+
# 这类规则必须聚合展示,逐条列等于没筛选。
|
|
490
|
+
high_volume: bool = False
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
RULE_INFO: tuple[RuleInfo, ...] = (
|
|
494
|
+
RuleInfo(
|
|
495
|
+
rule=Rule.CI_FAILED,
|
|
496
|
+
label="CI 失败",
|
|
497
|
+
meaning="门禁检查未通过,且已超过阈值天数没有恢复。",
|
|
498
|
+
action="看构建日志定位失败原因;若为偶发失败,重新触发一次。",
|
|
499
|
+
subject="pull_request",
|
|
500
|
+
default_days=3,
|
|
501
|
+
threshold_field="ci_failed_days",
|
|
502
|
+
),
|
|
503
|
+
RuleInfo(
|
|
504
|
+
rule=Rule.MERGE_CONFLICT,
|
|
505
|
+
label="合并冲突",
|
|
506
|
+
meaning="与目标分支产生冲突,无法直接合入。",
|
|
507
|
+
action="请作者 rebase 到目标分支最新提交。",
|
|
508
|
+
subject="pull_request",
|
|
509
|
+
default_days=2,
|
|
510
|
+
threshold_field="conflict_days",
|
|
511
|
+
),
|
|
512
|
+
RuleInfo(
|
|
513
|
+
rule=Rule.CLA_DENIED,
|
|
514
|
+
label="CLA 未通过",
|
|
515
|
+
meaning="签署检查未通过,通常是提交缺少 Signed-off-by 或签署账号不匹配。",
|
|
516
|
+
action="请作者补签并确认提交邮箱与签署账号一致。",
|
|
517
|
+
subject="pull_request",
|
|
518
|
+
default_days=0,
|
|
519
|
+
threshold_field="cla_pending_days",
|
|
520
|
+
),
|
|
521
|
+
RuleInfo(
|
|
522
|
+
rule=Rule.CLA_PENDING_LONG,
|
|
523
|
+
label="CLA 停滞",
|
|
524
|
+
meaning="签署长时间停留在待处理状态。",
|
|
525
|
+
action="确认是否卡在签署流程上,必要时联系作者。",
|
|
526
|
+
subject="pull_request",
|
|
527
|
+
default_days=1,
|
|
528
|
+
threshold_field="cla_pending_days",
|
|
529
|
+
),
|
|
530
|
+
RuleInfo(
|
|
531
|
+
rule=Rule.REJECTED,
|
|
532
|
+
label="已被 NACK",
|
|
533
|
+
meaning="有评审人明确反对合入。",
|
|
534
|
+
action="确认反对意见是否已被处理;已处理则请评审人撤回。",
|
|
535
|
+
subject="pull_request",
|
|
536
|
+
default_days=0,
|
|
537
|
+
threshold_field="review_sla_days",
|
|
538
|
+
),
|
|
539
|
+
RuleInfo(
|
|
540
|
+
rule=Rule.NEEDS_ISSUE,
|
|
541
|
+
label="缺关联 Issue",
|
|
542
|
+
meaning="openEuler 要求 PR 关联 Issue,这条没有。",
|
|
543
|
+
action="请作者在描述中补 Fixes #编号。",
|
|
544
|
+
subject="pull_request",
|
|
545
|
+
default_days=0,
|
|
546
|
+
threshold_field="review_sla_days",
|
|
547
|
+
),
|
|
548
|
+
RuleInfo(
|
|
549
|
+
rule=Rule.MISSING_SIGNED_OFF,
|
|
550
|
+
label="缺签名",
|
|
551
|
+
meaning="提交缺少 Signed-off-by,不符合 DCO 要求。",
|
|
552
|
+
action="请作者用 git commit -s 补签。",
|
|
553
|
+
subject="pull_request",
|
|
554
|
+
default_days=0,
|
|
555
|
+
threshold_field="review_sla_days",
|
|
556
|
+
),
|
|
557
|
+
RuleInfo(
|
|
558
|
+
rule=Rule.BINARY_FILE,
|
|
559
|
+
label="含二进制文件",
|
|
560
|
+
meaning="补丁中包含二进制文件,无法逐行评审。",
|
|
561
|
+
action="确认该二进制是必要的;固件之类通常需要说明来源与许可。",
|
|
562
|
+
subject="pull_request",
|
|
563
|
+
default_days=0,
|
|
564
|
+
threshold_field="review_sla_days",
|
|
565
|
+
),
|
|
566
|
+
RuleInfo(
|
|
567
|
+
rule=Rule.REVIEW_SLA_BREACH,
|
|
568
|
+
label="评审超期",
|
|
569
|
+
meaning="已过评审时限仍无人评审。",
|
|
570
|
+
action="按 committers.md 的模块归属指定评审人。",
|
|
571
|
+
subject="pull_request",
|
|
572
|
+
default_days=7,
|
|
573
|
+
threshold_field="review_sla_days",
|
|
574
|
+
high_volume=True,
|
|
575
|
+
),
|
|
576
|
+
RuleInfo(
|
|
577
|
+
rule=Rule.STALE,
|
|
578
|
+
label="长期无活动",
|
|
579
|
+
meaning="超过阈值天数没有任何更新。",
|
|
580
|
+
action="确认是否已被放弃;放弃则应关闭,否则催促推进。",
|
|
581
|
+
subject="pull_request",
|
|
582
|
+
default_days=30,
|
|
583
|
+
threshold_field="stale_days",
|
|
584
|
+
high_volume=True,
|
|
585
|
+
),
|
|
586
|
+
RuleInfo(
|
|
587
|
+
rule=Rule.CVE_AGING,
|
|
588
|
+
label="CVE 未推进",
|
|
589
|
+
meaning="安全补丁在时限内没有推进。这是唯一有外部时限的一类。",
|
|
590
|
+
action="优先安排评审;确认是否缺少上游补丁或需要回合。",
|
|
591
|
+
subject="pull_request",
|
|
592
|
+
default_days=1,
|
|
593
|
+
threshold_field="cve_aging_hours",
|
|
594
|
+
high_volume=True,
|
|
595
|
+
),
|
|
596
|
+
RuleInfo(
|
|
597
|
+
rule=Rule.ISSUE_UNASSIGNED,
|
|
598
|
+
label="Issue 未指派",
|
|
599
|
+
meaning="Issue 没有指派给任何人,因此没有人会去处理它。",
|
|
600
|
+
action="按模块归属指派处理人。",
|
|
601
|
+
subject="issue",
|
|
602
|
+
default_days=7,
|
|
603
|
+
threshold_field="issue_unassigned_days",
|
|
604
|
+
high_volume=True,
|
|
605
|
+
),
|
|
606
|
+
RuleInfo(
|
|
607
|
+
rule=Rule.ISSUE_STALE,
|
|
608
|
+
label="Issue 长期无活动",
|
|
609
|
+
meaning="Issue 长时间没有更新,通常意味着被遗忘了。",
|
|
610
|
+
action="确认是否仍然有效;无效则关闭,有效则重新指派。",
|
|
611
|
+
subject="issue",
|
|
612
|
+
default_days=90,
|
|
613
|
+
threshold_field="issue_stale_days",
|
|
614
|
+
high_volume=True,
|
|
615
|
+
),
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
RULE_INFO_BY_KEY: dict[str, RuleInfo] = {item.rule.value: item for item in RULE_INFO}
|