ado-git-repo-insights 1.2.1__py3-none-any.whl → 2.7.4__py3-none-any.whl
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.
- ado_git_repo_insights/__init__.py +3 -3
- ado_git_repo_insights/cli.py +703 -354
- ado_git_repo_insights/config.py +186 -186
- ado_git_repo_insights/extractor/__init__.py +1 -1
- ado_git_repo_insights/extractor/ado_client.py +452 -246
- ado_git_repo_insights/extractor/pr_extractor.py +239 -239
- ado_git_repo_insights/ml/__init__.py +13 -0
- ado_git_repo_insights/ml/date_utils.py +70 -0
- ado_git_repo_insights/ml/forecaster.py +288 -0
- ado_git_repo_insights/ml/insights.py +497 -0
- ado_git_repo_insights/persistence/__init__.py +1 -1
- ado_git_repo_insights/persistence/database.py +193 -193
- ado_git_repo_insights/persistence/models.py +207 -145
- ado_git_repo_insights/persistence/repository.py +662 -376
- ado_git_repo_insights/transform/__init__.py +1 -1
- ado_git_repo_insights/transform/aggregators.py +950 -0
- ado_git_repo_insights/transform/csv_generator.py +132 -132
- ado_git_repo_insights/utils/__init__.py +1 -1
- ado_git_repo_insights/utils/datetime_utils.py +101 -101
- ado_git_repo_insights/utils/logging_config.py +172 -172
- ado_git_repo_insights/utils/run_summary.py +207 -206
- {ado_git_repo_insights-1.2.1.dist-info → ado_git_repo_insights-2.7.4.dist-info}/METADATA +56 -15
- ado_git_repo_insights-2.7.4.dist-info/RECORD +27 -0
- {ado_git_repo_insights-1.2.1.dist-info → ado_git_repo_insights-2.7.4.dist-info}/licenses/LICENSE +21 -21
- ado_git_repo_insights-1.2.1.dist-info/RECORD +0 -22
- {ado_git_repo_insights-1.2.1.dist-info → ado_git_repo_insights-2.7.4.dist-info}/WHEEL +0 -0
- {ado_git_repo_insights-1.2.1.dist-info → ado_git_repo_insights-2.7.4.dist-info}/entry_points.txt +0 -0
- {ado_git_repo_insights-1.2.1.dist-info → ado_git_repo_insights-2.7.4.dist-info}/top_level.txt +0 -0
|
@@ -1,206 +1,207 @@
|
|
|
1
|
-
"""Run summary tracking with enriched error diagnostics.
|
|
2
|
-
|
|
3
|
-
Captures comprehensive run telemetry including per-project status and first fatal error.
|
|
4
|
-
"""
|
|
5
|
-
# ruff: noqa: S603, S607
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import json
|
|
10
|
-
import os
|
|
11
|
-
import re
|
|
12
|
-
import subprocess
|
|
13
|
-
from dataclasses import dataclass, field
|
|
14
|
-
from datetime import date
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
from typing import Any, Literal
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def normalize_error_message(error: str, max_length: int = 500) -> str:
|
|
20
|
-
"""Normalize and bound error messages to prevent secret leakage.
|
|
21
|
-
|
|
22
|
-
Args:
|
|
23
|
-
error: Raw error message.
|
|
24
|
-
max_length: Maximum length for bounded message.
|
|
25
|
-
|
|
26
|
-
Returns:
|
|
27
|
-
Normalized error message.
|
|
28
|
-
"""
|
|
29
|
-
# Strip URLs with query strings (can contain secrets)
|
|
30
|
-
error = re.sub(r"https?://[^\s]+\?[^\s]+", "[URL_WITH_PARAMS]", error)
|
|
31
|
-
|
|
32
|
-
# Strip full URLs (can contain hostnames/paths)
|
|
33
|
-
error = re.sub(r"https?://[^\s]+", "[URL]", error)
|
|
34
|
-
|
|
35
|
-
# Truncate to max length
|
|
36
|
-
if len(error) > max_length:
|
|
37
|
-
error = error[:max_length] + "...[truncated]"
|
|
38
|
-
|
|
39
|
-
return error
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@dataclass
|
|
43
|
-
class RunCounts:
|
|
44
|
-
"""Counts of extracted/generated items."""
|
|
45
|
-
|
|
46
|
-
prs_fetched: int = 0
|
|
47
|
-
prs_updated: int = 0
|
|
48
|
-
rows_per_csv: dict[str, int] = field(default_factory=dict)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@dataclass
|
|
52
|
-
class RunTimings:
|
|
53
|
-
"""Timing information for run phases."""
|
|
54
|
-
|
|
55
|
-
total_seconds: float = 0.0
|
|
56
|
-
extract_seconds: float = 0.0
|
|
57
|
-
persist_seconds: float = 0.0
|
|
58
|
-
export_seconds: float = 0.0
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@dataclass
|
|
62
|
-
class RunSummary:
|
|
63
|
-
"""Comprehensive run summary with forensic diagnostics."""
|
|
64
|
-
|
|
65
|
-
tool_version: str
|
|
66
|
-
git_sha: str | None
|
|
67
|
-
organization: str
|
|
68
|
-
projects: list[str]
|
|
69
|
-
date_range_start: str # ISO format date
|
|
70
|
-
date_range_end: str # ISO format date
|
|
71
|
-
counts: RunCounts
|
|
72
|
-
timings: RunTimings
|
|
73
|
-
warnings: list[str]
|
|
74
|
-
final_status: Literal["success", "failed"]
|
|
75
|
-
per_project_status: dict[str, str] = field(default_factory=dict)
|
|
76
|
-
first_fatal_error: str | None = None
|
|
77
|
-
|
|
78
|
-
def __post_init__(self) -> None:
|
|
79
|
-
"""Normalize error message on initialization."""
|
|
80
|
-
if self.first_fatal_error:
|
|
81
|
-
self.first_fatal_error = normalize_error_message(self.first_fatal_error)
|
|
82
|
-
|
|
83
|
-
def to_dict(self) -> dict[str, Any]:
|
|
84
|
-
"""Convert to dictionary for JSON serialization."""
|
|
85
|
-
return {
|
|
86
|
-
"tool_version": self.tool_version,
|
|
87
|
-
"git_sha": self.git_sha,
|
|
88
|
-
"organization": self.organization,
|
|
89
|
-
"projects": self.projects,
|
|
90
|
-
"date_range": {
|
|
91
|
-
"start": self.date_range_start,
|
|
92
|
-
"end": self.date_range_end,
|
|
93
|
-
},
|
|
94
|
-
"counts": {
|
|
95
|
-
"prs_fetched": self.counts.prs_fetched,
|
|
96
|
-
"prs_updated": self.counts.prs_updated,
|
|
97
|
-
"rows_per_csv": self.counts.rows_per_csv,
|
|
98
|
-
},
|
|
99
|
-
"timings": {
|
|
100
|
-
"total_seconds": self.timings.total_seconds,
|
|
101
|
-
"extract_seconds": self.timings.extract_seconds,
|
|
102
|
-
"persist_seconds": self.timings.persist_seconds,
|
|
103
|
-
"export_seconds": self.timings.export_seconds,
|
|
104
|
-
},
|
|
105
|
-
"warnings": self.warnings,
|
|
106
|
-
"final_status": self.final_status,
|
|
107
|
-
"per_project_status": self.per_project_status,
|
|
108
|
-
"first_fatal_error": self.first_fatal_error,
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
def write(self, path: Path) -> None:
|
|
112
|
-
"""Write summary to JSON file.
|
|
113
|
-
|
|
114
|
-
Args:
|
|
115
|
-
path: Path to write summary file.
|
|
116
|
-
"""
|
|
117
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
118
|
-
with path.open("w", encoding="utf-8") as f:
|
|
119
|
-
json.dump(self.to_dict(), f, indent=2)
|
|
120
|
-
|
|
121
|
-
def print_final_line(self) -> None:
|
|
122
|
-
"""Print one-liner summary to stdout."""
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
f"{self.
|
|
127
|
-
f"{
|
|
128
|
-
f"(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
1
|
+
"""Run summary tracking with enriched error diagnostics.
|
|
2
|
+
|
|
3
|
+
Captures comprehensive run telemetry including per-project status and first fatal error.
|
|
4
|
+
"""
|
|
5
|
+
# ruff: noqa: S603, S607
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import subprocess
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from datetime import date
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Literal
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def normalize_error_message(error: str, max_length: int = 500) -> str:
|
|
20
|
+
"""Normalize and bound error messages to prevent secret leakage.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
error: Raw error message.
|
|
24
|
+
max_length: Maximum length for bounded message.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Normalized error message.
|
|
28
|
+
"""
|
|
29
|
+
# Strip URLs with query strings (can contain secrets)
|
|
30
|
+
error = re.sub(r"https?://[^\s]+\?[^\s]+", "[URL_WITH_PARAMS]", error)
|
|
31
|
+
|
|
32
|
+
# Strip full URLs (can contain hostnames/paths)
|
|
33
|
+
error = re.sub(r"https?://[^\s]+", "[URL]", error)
|
|
34
|
+
|
|
35
|
+
# Truncate to max length
|
|
36
|
+
if len(error) > max_length:
|
|
37
|
+
error = error[:max_length] + "...[truncated]"
|
|
38
|
+
|
|
39
|
+
return error
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class RunCounts:
|
|
44
|
+
"""Counts of extracted/generated items."""
|
|
45
|
+
|
|
46
|
+
prs_fetched: int = 0
|
|
47
|
+
prs_updated: int = 0
|
|
48
|
+
rows_per_csv: dict[str, int] = field(default_factory=dict)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class RunTimings:
|
|
53
|
+
"""Timing information for run phases."""
|
|
54
|
+
|
|
55
|
+
total_seconds: float = 0.0
|
|
56
|
+
extract_seconds: float = 0.0
|
|
57
|
+
persist_seconds: float = 0.0
|
|
58
|
+
export_seconds: float = 0.0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class RunSummary:
|
|
63
|
+
"""Comprehensive run summary with forensic diagnostics."""
|
|
64
|
+
|
|
65
|
+
tool_version: str
|
|
66
|
+
git_sha: str | None
|
|
67
|
+
organization: str
|
|
68
|
+
projects: list[str]
|
|
69
|
+
date_range_start: str # ISO format date
|
|
70
|
+
date_range_end: str # ISO format date
|
|
71
|
+
counts: RunCounts
|
|
72
|
+
timings: RunTimings
|
|
73
|
+
warnings: list[str]
|
|
74
|
+
final_status: Literal["success", "failed"]
|
|
75
|
+
per_project_status: dict[str, str] = field(default_factory=dict)
|
|
76
|
+
first_fatal_error: str | None = None
|
|
77
|
+
|
|
78
|
+
def __post_init__(self) -> None:
|
|
79
|
+
"""Normalize error message on initialization."""
|
|
80
|
+
if self.first_fatal_error:
|
|
81
|
+
self.first_fatal_error = normalize_error_message(self.first_fatal_error)
|
|
82
|
+
|
|
83
|
+
def to_dict(self) -> dict[str, Any]:
|
|
84
|
+
"""Convert to dictionary for JSON serialization."""
|
|
85
|
+
return {
|
|
86
|
+
"tool_version": self.tool_version,
|
|
87
|
+
"git_sha": self.git_sha,
|
|
88
|
+
"organization": self.organization,
|
|
89
|
+
"projects": self.projects,
|
|
90
|
+
"date_range": {
|
|
91
|
+
"start": self.date_range_start,
|
|
92
|
+
"end": self.date_range_end,
|
|
93
|
+
},
|
|
94
|
+
"counts": {
|
|
95
|
+
"prs_fetched": self.counts.prs_fetched,
|
|
96
|
+
"prs_updated": self.counts.prs_updated,
|
|
97
|
+
"rows_per_csv": self.counts.rows_per_csv,
|
|
98
|
+
},
|
|
99
|
+
"timings": {
|
|
100
|
+
"total_seconds": self.timings.total_seconds,
|
|
101
|
+
"extract_seconds": self.timings.extract_seconds,
|
|
102
|
+
"persist_seconds": self.timings.persist_seconds,
|
|
103
|
+
"export_seconds": self.timings.export_seconds,
|
|
104
|
+
},
|
|
105
|
+
"warnings": self.warnings,
|
|
106
|
+
"final_status": self.final_status,
|
|
107
|
+
"per_project_status": self.per_project_status,
|
|
108
|
+
"first_fatal_error": self.first_fatal_error,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
def write(self, path: Path) -> None:
|
|
112
|
+
"""Write summary to JSON file.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
path: Path to write summary file.
|
|
116
|
+
"""
|
|
117
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
with path.open("w", encoding="utf-8") as f:
|
|
119
|
+
json.dump(self.to_dict(), f, indent=2)
|
|
120
|
+
|
|
121
|
+
def print_final_line(self) -> None:
|
|
122
|
+
"""Print one-liner summary to stdout."""
|
|
123
|
+
# Use ASCII symbols for Windows cp1252 compatibility
|
|
124
|
+
status_symbol = "[OK]" if self.final_status == "success" else "[FAIL]"
|
|
125
|
+
print(
|
|
126
|
+
f"{status_symbol} {self.final_status.upper()}: "
|
|
127
|
+
f"{self.counts.prs_fetched} PRs extracted, "
|
|
128
|
+
f"{len(self.counts.rows_per_csv)} CSVs written "
|
|
129
|
+
f"({self.timings.total_seconds:.1f}s)"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def emit_ado_commands(self) -> None:
|
|
133
|
+
"""Emit Azure Pipelines logging commands."""
|
|
134
|
+
# Only emit if running in Azure Pipelines
|
|
135
|
+
if os.environ.get("TF_BUILD") != "true":
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
if self.final_status == "failed":
|
|
139
|
+
if self.first_fatal_error:
|
|
140
|
+
print(f"##vso[task.logissue type=error]{self.first_fatal_error}")
|
|
141
|
+
print("##vso[task.complete result=Failed]")
|
|
142
|
+
elif self.warnings:
|
|
143
|
+
for warning in self.warnings:
|
|
144
|
+
print(f"##vso[task.logissue type=warning]{warning}")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_tool_version() -> str:
|
|
148
|
+
"""Get tool version from VERSION file."""
|
|
149
|
+
version_file = Path(__file__).parent.parent.parent.parent / "VERSION"
|
|
150
|
+
if version_file.exists():
|
|
151
|
+
return version_file.read_text().strip()
|
|
152
|
+
return "unknown"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_git_sha() -> str | None:
|
|
156
|
+
"""Get Git SHA from VERSION file or git command.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Git SHA or None if unavailable.
|
|
160
|
+
"""
|
|
161
|
+
# Try VERSION file first
|
|
162
|
+
version_file = Path(__file__).parent.parent.parent.parent / "VERSION"
|
|
163
|
+
if version_file.exists():
|
|
164
|
+
version = version_file.read_text().strip()
|
|
165
|
+
if "+" in version: # Version format like "1.0.7+8d88fb4"
|
|
166
|
+
return version.split("+")[1]
|
|
167
|
+
|
|
168
|
+
# Fallback to git command
|
|
169
|
+
try:
|
|
170
|
+
result = subprocess.run( # noqa: S603, S607
|
|
171
|
+
["git", "rev-parse", "--short", "HEAD"],
|
|
172
|
+
capture_output=True,
|
|
173
|
+
text=True,
|
|
174
|
+
check=True,
|
|
175
|
+
timeout=5,
|
|
176
|
+
)
|
|
177
|
+
return result.stdout.strip()
|
|
178
|
+
except Exception:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def create_minimal_summary(
|
|
183
|
+
error_message: str,
|
|
184
|
+
artifacts_dir: Path = Path("run_artifacts"),
|
|
185
|
+
) -> RunSummary:
|
|
186
|
+
"""Create a partial summary for early failures.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
error_message: Error message describing the failure.
|
|
190
|
+
artifacts_dir: Directory for artifacts.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Minimal RunSummary with failure status.
|
|
194
|
+
"""
|
|
195
|
+
return RunSummary(
|
|
196
|
+
tool_version=get_tool_version(),
|
|
197
|
+
git_sha=get_git_sha(),
|
|
198
|
+
organization="unknown",
|
|
199
|
+
projects=[],
|
|
200
|
+
date_range_start=str(date.today()),
|
|
201
|
+
date_range_end=str(date.today()),
|
|
202
|
+
counts=RunCounts(),
|
|
203
|
+
timings=RunTimings(),
|
|
204
|
+
warnings=[],
|
|
205
|
+
final_status="failed",
|
|
206
|
+
first_fatal_error=normalize_error_message(error_message),
|
|
207
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ado-git-repo-insights
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.7.4
|
|
4
4
|
Summary: Extract Azure DevOps Pull Request metrics to SQLite and generate PowerBI-compatible CSVs.
|
|
5
5
|
Author-email: "Odd Essentials, LLC" <admin@oddessentials.com>
|
|
6
6
|
License: MIT
|
|
@@ -21,12 +21,15 @@ Requires-Dist: azure-storage-blob>=12.0.0
|
|
|
21
21
|
Provides-Extra: dev
|
|
22
22
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
23
23
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
24
|
-
Requires-Dist: ruff
|
|
24
|
+
Requires-Dist: ruff==0.14.11; extra == "dev"
|
|
25
25
|
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
26
26
|
Requires-Dist: pre-commit>=3.0; extra == "dev"
|
|
27
27
|
Requires-Dist: types-requests>=2.28.0; extra == "dev"
|
|
28
28
|
Requires-Dist: types-PyYAML>=6.0; extra == "dev"
|
|
29
29
|
Requires-Dist: pandas-stubs>=2.0.0; extra == "dev"
|
|
30
|
+
Provides-Extra: ml
|
|
31
|
+
Requires-Dist: prophet>=1.1.0; extra == "ml"
|
|
32
|
+
Requires-Dist: openai>=1.0.0; extra == "ml"
|
|
30
33
|
Dynamic: license-file
|
|
31
34
|
|
|
32
35
|
# ado-git-repo-insights
|
|
@@ -110,7 +113,7 @@ Best for teams that prefer the ADO pipeline editor UI or want a self-contained t
|
|
|
110
113
|
|
|
111
114
|
```yaml
|
|
112
115
|
steps:
|
|
113
|
-
- task: ExtractPullRequests@
|
|
116
|
+
- task: ExtractPullRequests@2
|
|
114
117
|
inputs:
|
|
115
118
|
organization: 'MyOrg'
|
|
116
119
|
projects: 'Project1,Project2'
|
|
@@ -123,6 +126,16 @@ steps:
|
|
|
123
126
|
1. Download the `.vsix` from [GitHub Releases](https://github.com/oddessentials/ado-git-repo-insights/releases)
|
|
124
127
|
2. Install in your ADO organization: Organization Settings → Extensions → Browse local extensions
|
|
125
128
|
|
|
129
|
+
### PR Insights Dashboard
|
|
130
|
+
|
|
131
|
+
Once the extension is installed and a pipeline runs successfully with the `aggregates` artifact published, the **PR Insights** hub appears in the project navigation menu. The dashboard auto-discovers pipelines that publish aggregates.
|
|
132
|
+
|
|
133
|
+
**Configuration precedence:**
|
|
134
|
+
1. `?dataset=<url>` — Direct URL (dev/testing only)
|
|
135
|
+
2. `?pipelineId=<id>` — Query parameter override
|
|
136
|
+
3. Extension settings — User-scoped saved preference (Project Settings → PR Insights Settings)
|
|
137
|
+
4. Auto-discovery — Find pipelines with 'aggregates' artifact
|
|
138
|
+
|
|
126
139
|
## Configuration
|
|
127
140
|
|
|
128
141
|
Create a `config.yaml` file:
|
|
@@ -156,9 +169,16 @@ ado-insights extract --config config.yaml --pat $ADO_PAT
|
|
|
156
169
|
|
|
157
170
|
## Azure DevOps Pipeline Integration
|
|
158
171
|
|
|
159
|
-
|
|
172
|
+
Use [pr-insights-pipeline.yml](pr-insights-pipeline.yml) for a production-ready template that includes:
|
|
173
|
+
- Daily incremental extraction
|
|
174
|
+
- Sunday backfill for data convergence
|
|
175
|
+
- Dashboard-compatible `aggregates` artifact
|
|
176
|
+
|
|
177
|
+
See [sample-pipeline.yml](sample-pipeline.yml) for additional reference.
|
|
160
178
|
|
|
161
|
-
###
|
|
179
|
+
### Daily Schedule with Sunday Backfill
|
|
180
|
+
|
|
181
|
+
The production template uses a single daily schedule that detects Sundays for backfill:
|
|
162
182
|
|
|
163
183
|
```yaml
|
|
164
184
|
schedules:
|
|
@@ -169,16 +189,7 @@ schedules:
|
|
|
169
189
|
always: true
|
|
170
190
|
```
|
|
171
191
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
```yaml
|
|
175
|
-
schedules:
|
|
176
|
-
- cron: "0 6 * * 0" # Weekly on Sunday
|
|
177
|
-
displayName: "Weekly Backfill"
|
|
178
|
-
branches:
|
|
179
|
-
include: [main]
|
|
180
|
-
always: true
|
|
181
|
-
```
|
|
192
|
+
On Sundays, the pipeline automatically performs a 60-day backfill for data convergence.
|
|
182
193
|
|
|
183
194
|
## CSV Output Contract
|
|
184
195
|
|
|
@@ -193,6 +204,20 @@ The following CSVs are generated with **exact schema and column order** for Powe
|
|
|
193
204
|
| `users.csv` | `user_id`, `display_name`, `email` |
|
|
194
205
|
| `reviewers.csv` | `pull_request_uid`, `user_id`, `vote`, `repository_id` |
|
|
195
206
|
|
|
207
|
+
## Security & Permissions
|
|
208
|
+
|
|
209
|
+
### PR Insights Dashboard (Phase 3)
|
|
210
|
+
|
|
211
|
+
The PR Insights dashboard reads data from pipeline-produced artifacts. **Users must have Build Read permission** on the analytics pipeline to view dashboard data.
|
|
212
|
+
|
|
213
|
+
| Requirement | Details |
|
|
214
|
+
|-------------|---------|
|
|
215
|
+
| **Permission scope** | Build → Read on the pipeline that produces artifacts |
|
|
216
|
+
| **No special redaction** | Data is not filtered per-user; access is all-or-nothing |
|
|
217
|
+
| **Artifact retention** | Operators must configure retention for their desired analytics window |
|
|
218
|
+
|
|
219
|
+
If a user lacks permissions, the dashboard displays: *"No access to analytics pipeline artifacts. Ask an admin for Build Read on pipeline X."*
|
|
220
|
+
|
|
196
221
|
## Governance
|
|
197
222
|
|
|
198
223
|
This project is governed by authoritative documents in `agents/`:
|
|
@@ -220,6 +245,22 @@ mypy src/
|
|
|
220
245
|
pytest
|
|
221
246
|
```
|
|
222
247
|
|
|
248
|
+
## Contributing
|
|
249
|
+
|
|
250
|
+
### Line Endings (Windows Developers)
|
|
251
|
+
|
|
252
|
+
This repo uses LF line endings for cross-platform compatibility. The `.gitattributes` file handles this automatically, but for best results:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
# Recommended: Let .gitattributes be the source of truth
|
|
256
|
+
git config core.autocrlf false
|
|
257
|
+
|
|
258
|
+
# Alternative: Convert on commit (but not checkout)
|
|
259
|
+
git config core.autocrlf input
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
If you see "CRLF will be replaced by LF" warnings, that's expected behavior.
|
|
263
|
+
|
|
223
264
|
## License
|
|
224
265
|
|
|
225
266
|
MIT
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
ado_git_repo_insights/__init__.py,sha256=730M8o7DUvF6VGejYhP6U9G5kF-Vk05sHmuIbsz8szk,136
|
|
2
|
+
ado_git_repo_insights/cli.py,sha256=RQl9Le8zQsPj1-iZAcqF1bi6v91ducJt1tzJ4BmNrLY,23676
|
|
3
|
+
ado_git_repo_insights/config.py,sha256=0oeB7IQ6srCCCY9qFU0EEj_F1svGI9q-j0xIHcLmKpA,6185
|
|
4
|
+
ado_git_repo_insights/extractor/__init__.py,sha256=H3y-GwGtlsAoZMt0brBL1WqbP9cjY6Kn9TnSwFaLy2U,58
|
|
5
|
+
ado_git_repo_insights/extractor/ado_client.py,sha256=sDNCH0okayjHDdeTrjUNA48OI5trgD3XY_DsAAgu-ck,14471
|
|
6
|
+
ado_git_repo_insights/extractor/pr_extractor.py,sha256=1SSRtjQGp_FkX_ONrJaKJWRMvy67PPgPKH7h2tyPeC4,7562
|
|
7
|
+
ado_git_repo_insights/ml/__init__.py,sha256=zPtv9Ca0QruQ4w6N_p9mTRYD95Lz6Vr9HxnWyqXNacw,472
|
|
8
|
+
ado_git_repo_insights/ml/date_utils.py,sha256=FlHqDPU9D5KxJMnkBPrqU5Dx9IPvPm2-1Xg_DB4w8bk,1981
|
|
9
|
+
ado_git_repo_insights/ml/forecaster.py,sha256=Z217j6BgBUl4_VOrEuHDm2FOg_u3Jzg3nX41nQffGkM,8895
|
|
10
|
+
ado_git_repo_insights/ml/insights.py,sha256=ecqKy_VApEROa88AAu4S6I0Xh2ryk4nfgvemifpaMxY,17530
|
|
11
|
+
ado_git_repo_insights/persistence/__init__.py,sha256=jpVCJOia8ZuZmFA_9kpTz_5u8Gwl0SXJRXS-E81w79w,56
|
|
12
|
+
ado_git_repo_insights/persistence/database.py,sha256=dUp1buCyrUrxN3QP8rmM8LgX9nTsontIDqhH6RqXviw,5930
|
|
13
|
+
ado_git_repo_insights/persistence/models.py,sha256=klQhCUcxjKKxe-_qsCyRfcncafL8utRUEXCiwSnwqeA,7766
|
|
14
|
+
ado_git_repo_insights/persistence/repository.py,sha256=iJ4f2u3GgFB_meKOAB4ElkVAez6BsRwzAbDbnksiDDY,20896
|
|
15
|
+
ado_git_repo_insights/transform/__init__.py,sha256=lDjE3oMQQFBgpNSw-iC7Lyi1tcjvNgGTWrVxOx7stAQ,43
|
|
16
|
+
ado_git_repo_insights/transform/aggregators.py,sha256=rjnfWD79imVKtbmBjkfCsQ9l_d1-bFq68KAsxgpjPhA,34793
|
|
17
|
+
ado_git_repo_insights/transform/csv_generator.py,sha256=zfhS93EXQtAm9gEAZcTMjDHj31bjBqO3cqEVv9TYGNI,3957
|
|
18
|
+
ado_git_repo_insights/utils/__init__.py,sha256=NEwaQ6FcwYVzrvJ8es3X-vaOc7a5MCD_3rIYDAwn-z4,52
|
|
19
|
+
ado_git_repo_insights/utils/datetime_utils.py,sha256=L556SFQjaUBaPmNlnU6atIZIINI5SuCOZiheGoDSv7c,2936
|
|
20
|
+
ado_git_repo_insights/utils/logging_config.py,sha256=o2RG8fhX6SeM4qSleGSZDBKJmUhwd-3kvCBCljje5qA,5521
|
|
21
|
+
ado_git_repo_insights/utils/run_summary.py,sha256=bcDo-7w1kplxjhG8Agoqb_FysAL3QeYQKLMSI87Ulrw,6492
|
|
22
|
+
ado_git_repo_insights-2.7.4.dist-info/licenses/LICENSE,sha256=cjgHmK9h1hMSh7DdPI3FNFU132QQAv7OOgLX7xCcX44,1069
|
|
23
|
+
ado_git_repo_insights-2.7.4.dist-info/METADATA,sha256=dP0BUHRkNnP_QOA_0adXefdsXJ1xqf8lQ4WrVz9fAhE,8430
|
|
24
|
+
ado_git_repo_insights-2.7.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
25
|
+
ado_git_repo_insights-2.7.4.dist-info/entry_points.txt,sha256=xG4WnncN88TiGSTdm-aQc_coA9oo0ygX8XxaPSM0wGk,64
|
|
26
|
+
ado_git_repo_insights-2.7.4.dist-info/top_level.txt,sha256=g27zDliEsKELgsQvn9TPdqbGAj-cX2CQ8nCaqyx4PbM,22
|
|
27
|
+
ado_git_repo_insights-2.7.4.dist-info/RECORD,,
|
{ado_git_repo_insights-1.2.1.dist-info → ado_git_repo_insights-2.7.4.dist-info}/licenses/LICENSE
RENAMED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
ado_git_repo_insights/__init__.py,sha256=GdPeAjnS_8m6gBWAFFGd-roF1ol9Qov9CtUOjaPJ3bY,139
|
|
2
|
-
ado_git_repo_insights/cli.py,sha256=Ioxi3Kp3KwJ0dWoHNiMmXPQtRXH1XdJAqk1MjOJi4-E,11685
|
|
3
|
-
ado_git_repo_insights/config.py,sha256=RoRULJ2CvHhYc9NGBhc6awKheTZrapU8-VADvYeLgmw,6371
|
|
4
|
-
ado_git_repo_insights/extractor/__init__.py,sha256=c1iaZJEmUWA4NhCG9WN1WDR417loNJ562wbUphfvI68,59
|
|
5
|
-
ado_git_repo_insights/extractor/ado_client.py,sha256=72tKnI0DrfHRANPjltKBCP06r6xV5thjxOxn76Pf4jk,8005
|
|
6
|
-
ado_git_repo_insights/extractor/pr_extractor.py,sha256=HN9XJJ5B5x_MuGcUUX-4mSrId6uAy07X7kL5c-0hFmE,7801
|
|
7
|
-
ado_git_repo_insights/persistence/__init__.py,sha256=Gk7TqvLoqx1-vwTCcPrXgzh4pMWKyZxA-NTEOr1VDB8,57
|
|
8
|
-
ado_git_repo_insights/persistence/database.py,sha256=mShHYtIiWNKqpK2LBKERbt1ZVPKQYNVTPMqdMY7ev7s,6123
|
|
9
|
-
ado_git_repo_insights/persistence/models.py,sha256=-Ioa4qkTbhEbKfLIiVkz1xB3QBkZrux2S0U4rzBNuDI,5323
|
|
10
|
-
ado_git_repo_insights/persistence/repository.py,sha256=A_JpOsuyzCzzfAklcwq13j9abkMVSoW1WYl8iTBHFTQ,12077
|
|
11
|
-
ado_git_repo_insights/transform/__init__.py,sha256=jBn6xWAeoNWwMzGaialhQAv9bhrVKc6fPP3uI5sPPmQ,44
|
|
12
|
-
ado_git_repo_insights/transform/csv_generator.py,sha256=yquM3nbYJt9Yxs0pSevvGPcHkGHnJjkxy2ZqONpWNuE,4089
|
|
13
|
-
ado_git_repo_insights/utils/__init__.py,sha256=brlDxZd5bEDX3QguzN1Q-FEol9NvlT0hbxHVEh8QIeA,53
|
|
14
|
-
ado_git_repo_insights/utils/datetime_utils.py,sha256=I51MLZxpG8F18JnrI4mHQYGkXgIfKsGyPCCSVrZs7xk,3037
|
|
15
|
-
ado_git_repo_insights/utils/logging_config.py,sha256=hzhYL0NDsfgpEIlyZei0HjJU2etlV3SrhqtqJSqP5MA,5693
|
|
16
|
-
ado_git_repo_insights/utils/run_summary.py,sha256=VPcg4M-DWKdJDtDcbbrsq9Pe1Hu3vAo9HKHbDpLgwns,6633
|
|
17
|
-
ado_git_repo_insights-1.2.1.dist-info/licenses/LICENSE,sha256=1L7ghNYfOgApbwgAkiJ4AQ6HHt0VpIGXXELsaKYSfIo,1090
|
|
18
|
-
ado_git_repo_insights-1.2.1.dist-info/METADATA,sha256=pTX0MtoXRNcp70S1JycdlPXoW7XjwiTchOlwfb5hCBc,6370
|
|
19
|
-
ado_git_repo_insights-1.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
20
|
-
ado_git_repo_insights-1.2.1.dist-info/entry_points.txt,sha256=xG4WnncN88TiGSTdm-aQc_coA9oo0ygX8XxaPSM0wGk,64
|
|
21
|
-
ado_git_repo_insights-1.2.1.dist-info/top_level.txt,sha256=g27zDliEsKELgsQvn9TPdqbGAj-cX2CQ8nCaqyx4PbM,22
|
|
22
|
-
ado_git_repo_insights-1.2.1.dist-info/RECORD,,
|
|
File without changes
|
{ado_git_repo_insights-1.2.1.dist-info → ado_git_repo_insights-2.7.4.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{ado_git_repo_insights-1.2.1.dist-info → ado_git_repo_insights-2.7.4.dist-info}/top_level.txt
RENAMED
|
File without changes
|