codepulse-cli 0.1.0__tar.gz
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.
- codepulse_cli-0.1.0/.github/workflows/codepulse.yml +52 -0
- codepulse_cli-0.1.0/.github/workflows/tests.yml +19 -0
- codepulse_cli-0.1.0/.gitignore +13 -0
- codepulse_cli-0.1.0/LICENSE +21 -0
- codepulse_cli-0.1.0/PKG-INFO +227 -0
- codepulse_cli-0.1.0/README.md +201 -0
- codepulse_cli-0.1.0/action.yml +69 -0
- codepulse_cli-0.1.0/demo.tape +28 -0
- codepulse_cli-0.1.0/pyproject.toml +41 -0
- codepulse_cli-0.1.0/src/codepulse/__init__.py +3 -0
- codepulse_cli-0.1.0/src/codepulse/__main__.py +3 -0
- codepulse_cli-0.1.0/src/codepulse/analyzers.py +468 -0
- codepulse_cli-0.1.0/src/codepulse/charts.py +126 -0
- codepulse_cli-0.1.0/src/codepulse/cli.py +250 -0
- codepulse_cli-0.1.0/src/codepulse/complexity.py +39 -0
- codepulse_cli-0.1.0/src/codepulse/git_data.py +121 -0
- codepulse_cli-0.1.0/src/codepulse/report.py +275 -0
- codepulse_cli-0.1.0/tests/test_analyzers.py +130 -0
- codepulse_cli-0.1.0/tests/test_complexity.py +27 -0
- codepulse_cli-0.1.0/tests/test_e2e.py +59 -0
- codepulse_cli-0.1.0/tests/test_git_data.py +13 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: CodePulse risk gate
|
|
2
|
+
|
|
3
|
+
# Scores what each pull request changes against the repo's history and posts
|
|
4
|
+
# the result as a sticky PR comment. Fails the check on HIGH risk.
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
pull-requests: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
risk:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
with:
|
|
19
|
+
fetch-depth: 0 # full history is required to build the risk map
|
|
20
|
+
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.12"
|
|
24
|
+
|
|
25
|
+
- name: Install CodePulse
|
|
26
|
+
run: pip install -e .
|
|
27
|
+
|
|
28
|
+
- name: Build risk comment
|
|
29
|
+
run: |
|
|
30
|
+
codepulse . --changed "origin/${{ github.base_ref }}" \
|
|
31
|
+
--format markdown > comment.md
|
|
32
|
+
cat comment.md
|
|
33
|
+
|
|
34
|
+
- name: Post sticky PR comment
|
|
35
|
+
env:
|
|
36
|
+
GH_TOKEN: ${{ github.token }}
|
|
37
|
+
PR: ${{ github.event.pull_request.number }}
|
|
38
|
+
run: |
|
|
39
|
+
# Update our previous comment if one exists, else create it.
|
|
40
|
+
id=$(gh api "repos/${{ github.repository }}/issues/$PR/comments" \
|
|
41
|
+
--jq '.[] | select(.body | startswith("<!-- codepulse -->")) | .id' \
|
|
42
|
+
| head -1)
|
|
43
|
+
if [ -n "$id" ]; then
|
|
44
|
+
gh api --method PATCH \
|
|
45
|
+
"repos/${{ github.repository }}/issues/comments/$id" \
|
|
46
|
+
-f body="$(cat comment.md)" >/dev/null
|
|
47
|
+
else
|
|
48
|
+
gh pr comment "$PR" --body-file comment.md
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
- name: Enforce risk gate
|
|
52
|
+
run: codepulse . --changed "origin/${{ github.base_ref }}" --fail-on HIGH
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
strategy:
|
|
11
|
+
matrix:
|
|
12
|
+
python-version: ["3.10", "3.12"]
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: ${{ matrix.python-version }}
|
|
18
|
+
- run: pip install -e ".[dev]"
|
|
19
|
+
- run: pytest -q
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mehouelley
|
|
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.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: codepulse-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reveal a codebase's health from its git history — hotspots, bus factor, hidden coupling, and a CI risk gate.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Mehouelley/codepulse
|
|
6
|
+
Project-URL: Repository, https://github.com/Mehouelley/codepulse
|
|
7
|
+
Project-URL: Issues, https://github.com/Mehouelley/codepulse/issues
|
|
8
|
+
Author-email: Mehouelley <fabricemehouelley43@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: code-health,code-quality,devops,git,hotspots,static-analysis,technical-debt
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
24
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# 🩺 CodePulse
|
|
28
|
+
|
|
29
|
+
[](https://github.com/marketplace/actions/codepulse-risk-gate)
|
|
30
|
+
[](https://github.com/Mehouelley/codepulse/actions/workflows/tests.yml)
|
|
31
|
+
[](https://www.python.org/)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
[](pyproject.toml)
|
|
34
|
+
|
|
35
|
+
**Reveal a codebase's health from its git history.**
|
|
36
|
+
|
|
37
|
+
Linters read your code. CodePulse reads your code's *history* — and that reveals
|
|
38
|
+
things no linter can see: where bugs are born, what knowledge lives in one
|
|
39
|
+
person's head, and which files are secretly glued together.
|
|
40
|
+
|
|
41
|
+
Because it works purely from git history, **CodePulse works on any language**,
|
|
42
|
+
with **zero dependencies** beyond git and Python.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Demo
|
|
47
|
+
|
|
48
|
+
<!-- Render docs/demo.gif from demo.tape with `vhs demo.tape`, then:
|
|
49
|
+
 -->
|
|
50
|
+
|
|
51
|
+
```console
|
|
52
|
+
$ codepulse . --badge
|
|
53
|
+
Grade: B (83/100)
|
|
54
|
+
-2 bus factor (9 fichiers à auteur unique)
|
|
55
|
+
-15 pression hotspots en hausse
|
|
56
|
+
-green)
|
|
57
|
+
|
|
58
|
+
$ codepulse . --changed origin/main
|
|
59
|
+
🔴 Release risk: HIGH (100/100)
|
|
60
|
+
56 changed files · 2 high-risk
|
|
61
|
+
[100] crates/ui-egui/src/lib.rs (hotspot majeur)
|
|
62
|
+
[ 70] crates/layout/src/para.rs (zone active, auteur unique)
|
|
63
|
+
👥 Relecteurs suggérés: Valentin Klein (29/37 fichiers), Dmitrii Khlapov (22/37)
|
|
64
|
+
|
|
65
|
+
$ codepulse . --who crates/ui-egui/src/canvas.rs
|
|
66
|
+
crates/ui-egui/src/canvas.rs — 64 commits, 22 auteur(s), dernier changement 2026-10-12
|
|
67
|
+
45% 29 commits Valentin Klein
|
|
68
|
+
11% 7 commits Dmitrii Khlapov
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
> A recordable [`demo.tape`](demo.tape) (for [VHS](https://github.com/charmbracelet/vhs))
|
|
72
|
+
> generates an animated `docs/demo.gif` of the above.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## What it shows
|
|
77
|
+
|
|
78
|
+
| Signal | Question it answers |
|
|
79
|
+
|---|---|
|
|
80
|
+
| 🔥 **Hotspots** | *Where do I refactor first?* Files that are both complex and changed constantly — the birthplace of most bugs. |
|
|
81
|
+
| 👥 **Bus factor** | *What breaks if someone leaves?* Files actively maintained by a single person. |
|
|
82
|
+
| 🔗 **Hidden coupling** | *What really depends on what?* Files that almost always change together, even when the code doesn't show a link. |
|
|
83
|
+
| 🏆 **Contributors** | Who is active across the analyzed window. |
|
|
84
|
+
|
|
85
|
+
The result is a single self-contained **HTML dashboard** you can open anywhere
|
|
86
|
+
or email to your team.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Quick start
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# From a git repository:
|
|
94
|
+
python -m codepulse /path/to/repo --open
|
|
95
|
+
|
|
96
|
+
# Common options:
|
|
97
|
+
python -m codepulse . --since "6 months ago" --top 30 -o health.html
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
> Running from the source checkout? Prefix with `PYTHONPATH=src`, or install it:
|
|
101
|
+
> `pip install -e .` then just run `codepulse`.
|
|
102
|
+
|
|
103
|
+
### Options
|
|
104
|
+
|
|
105
|
+
| Flag | Meaning |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `repo` | Path to a git repository (default: current directory) |
|
|
108
|
+
| `-o, --output` | Output HTML path (default: `codepulse-report.html`) |
|
|
109
|
+
| `--since` | Only commits after a date, e.g. `"6 months ago"` |
|
|
110
|
+
| `--max-count N` | Limit to the N most recent commits |
|
|
111
|
+
| `--top N` | Rows per table (default: 20) |
|
|
112
|
+
| `--open` | Open the report in a browser when done |
|
|
113
|
+
| `--who PATH` | Show who knows a file best (from history) and exit |
|
|
114
|
+
| `--badge` | Print a shields.io health-badge snippet and exit |
|
|
115
|
+
| `--ignore GLOB` | Exclude paths matching a glob (repeatable) |
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Use in CI (the risk gate)
|
|
120
|
+
|
|
121
|
+
CodePulse can score *only the files a change touches* against the repo's risk
|
|
122
|
+
map, then set an exit code so your pipeline can warn or block.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Score what this branch changed vs main; fail the job on HIGH risk.
|
|
126
|
+
python -m codepulse . --changed origin/main --fail-on HIGH
|
|
127
|
+
|
|
128
|
+
# Machine-readable output for dashboards / PR comments:
|
|
129
|
+
python -m codepulse . --changed origin/main --format json
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Output (JSON):
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"level": "HIGH",
|
|
137
|
+
"score": 100,
|
|
138
|
+
"high_risk_count": 1,
|
|
139
|
+
"files": [
|
|
140
|
+
{ "path": "auth.py", "risk": 100, "commits": 6, "loc": 7,
|
|
141
|
+
"reasons": ["hotspot majeur", "auteur unique (Alice)"] }
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### GitHub Action (two lines)
|
|
147
|
+
|
|
148
|
+
Add CodePulse to any repository as a reusable Action — it scores each PR, posts
|
|
149
|
+
a **sticky comment** (updated in place, never spammed), and fails the check on
|
|
150
|
+
`HIGH` risk:
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
name: CodePulse
|
|
154
|
+
on: pull_request
|
|
155
|
+
permissions:
|
|
156
|
+
contents: read
|
|
157
|
+
pull-requests: write
|
|
158
|
+
jobs:
|
|
159
|
+
risk:
|
|
160
|
+
runs-on: ubuntu-latest
|
|
161
|
+
steps:
|
|
162
|
+
- uses: actions/checkout@v4
|
|
163
|
+
with: { fetch-depth: 0 } # full history is required
|
|
164
|
+
- uses: Mehouelley/codepulse@v0.1.0
|
|
165
|
+
with:
|
|
166
|
+
fail-on: HIGH # or MEDIUM / LOW / '' to never fail
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The PR comment looks like:
|
|
170
|
+
|
|
171
|
+
> 🩺 **CodePulse — Risque : 🔴 HIGH (100/100)**
|
|
172
|
+
> 38 fichiers modifiés · 1 à risque élevé
|
|
173
|
+
>
|
|
174
|
+
> 👥 **Relecteurs suggérés** : Valentin Klein (29/37 fichiers), Dmitrii Khlapov (22/37)
|
|
175
|
+
|
|
176
|
+
Prefer a plain workflow with no external action? One ships in
|
|
177
|
+
[`.github/workflows/codepulse.yml`](.github/workflows/codepulse.yml) — copy it in as-is.
|
|
178
|
+
|
|
179
|
+
**Why DevOps cares:** a release is roughly as risky as its riskiest file.
|
|
180
|
+
Touching a well-known hotspot — or a file only one person understands — is
|
|
181
|
+
exactly where change-failure-rate (a DORA metric) comes from. The gate makes
|
|
182
|
+
that risk visible *before* merge, not after an incident.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## How it works
|
|
187
|
+
|
|
188
|
+
1. Parses `git log --numstat` into commits and file changes (no merges).
|
|
189
|
+
2. Accumulates per-file history: commit count, authors, size on disk.
|
|
190
|
+
3. Derives the signals:
|
|
191
|
+
- **Hotspot score** = `commits × complexity`, where complexity estimates
|
|
192
|
+
decision points (branch keywords + boolean operators) — far better than
|
|
193
|
+
raw line count at telling "big but simple" from "dense and risky".
|
|
194
|
+
- **Bus factor risk** = files with ≥3 commits but exactly one author.
|
|
195
|
+
- **Coupling degree** = `commits-together / min(individual commits)`,
|
|
196
|
+
ignoring bulk commits that touch many files at once.
|
|
197
|
+
|
|
198
|
+
Inspired by the "code as a crime scene" approach to software analysis, made
|
|
199
|
+
simple enough to run in one command.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Roadmap
|
|
204
|
+
|
|
205
|
+
- [x] Risk gate: score a change set, JSON output, CI exit code
|
|
206
|
+
- [x] Ignore globs / path filters for generated & data files
|
|
207
|
+
- [x] Post the gate result as a sticky PR comment (GitHub Action)
|
|
208
|
+
- [x] Trend over time (adaptive day/week/month buckets + hotspot-pressure verdict)
|
|
209
|
+
- [x] Complexity-based hotspots (decision-point estimate instead of line count)
|
|
210
|
+
- [x] Suggested PR reviewers from code ownership (in gate text/json/comment)
|
|
211
|
+
- [x] `--who <file>` — who knows this code best
|
|
212
|
+
- [x] `--badge` — shareable A–F health badge for your README
|
|
213
|
+
- [x] Orphaned-code section — complex files nobody touches anymore
|
|
214
|
+
|
|
215
|
+
## Development
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
pip install -e ".[dev]"
|
|
219
|
+
pytest
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The suite covers the complexity estimator, git parsing, every analyzer, and an
|
|
223
|
+
end-to-end run against a throwaway git repo. CI runs it on Python 3.10 and 3.12.
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
MIT
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# 🩺 CodePulse
|
|
2
|
+
|
|
3
|
+
[](https://github.com/marketplace/actions/codepulse-risk-gate)
|
|
4
|
+
[](https://github.com/Mehouelley/codepulse/actions/workflows/tests.yml)
|
|
5
|
+
[](https://www.python.org/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](pyproject.toml)
|
|
8
|
+
|
|
9
|
+
**Reveal a codebase's health from its git history.**
|
|
10
|
+
|
|
11
|
+
Linters read your code. CodePulse reads your code's *history* — and that reveals
|
|
12
|
+
things no linter can see: where bugs are born, what knowledge lives in one
|
|
13
|
+
person's head, and which files are secretly glued together.
|
|
14
|
+
|
|
15
|
+
Because it works purely from git history, **CodePulse works on any language**,
|
|
16
|
+
with **zero dependencies** beyond git and Python.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Demo
|
|
21
|
+
|
|
22
|
+
<!-- Render docs/demo.gif from demo.tape with `vhs demo.tape`, then:
|
|
23
|
+
 -->
|
|
24
|
+
|
|
25
|
+
```console
|
|
26
|
+
$ codepulse . --badge
|
|
27
|
+
Grade: B (83/100)
|
|
28
|
+
-2 bus factor (9 fichiers à auteur unique)
|
|
29
|
+
-15 pression hotspots en hausse
|
|
30
|
+
-green)
|
|
31
|
+
|
|
32
|
+
$ codepulse . --changed origin/main
|
|
33
|
+
🔴 Release risk: HIGH (100/100)
|
|
34
|
+
56 changed files · 2 high-risk
|
|
35
|
+
[100] crates/ui-egui/src/lib.rs (hotspot majeur)
|
|
36
|
+
[ 70] crates/layout/src/para.rs (zone active, auteur unique)
|
|
37
|
+
👥 Relecteurs suggérés: Valentin Klein (29/37 fichiers), Dmitrii Khlapov (22/37)
|
|
38
|
+
|
|
39
|
+
$ codepulse . --who crates/ui-egui/src/canvas.rs
|
|
40
|
+
crates/ui-egui/src/canvas.rs — 64 commits, 22 auteur(s), dernier changement 2026-10-12
|
|
41
|
+
45% 29 commits Valentin Klein
|
|
42
|
+
11% 7 commits Dmitrii Khlapov
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> A recordable [`demo.tape`](demo.tape) (for [VHS](https://github.com/charmbracelet/vhs))
|
|
46
|
+
> generates an animated `docs/demo.gif` of the above.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## What it shows
|
|
51
|
+
|
|
52
|
+
| Signal | Question it answers |
|
|
53
|
+
|---|---|
|
|
54
|
+
| 🔥 **Hotspots** | *Where do I refactor first?* Files that are both complex and changed constantly — the birthplace of most bugs. |
|
|
55
|
+
| 👥 **Bus factor** | *What breaks if someone leaves?* Files actively maintained by a single person. |
|
|
56
|
+
| 🔗 **Hidden coupling** | *What really depends on what?* Files that almost always change together, even when the code doesn't show a link. |
|
|
57
|
+
| 🏆 **Contributors** | Who is active across the analyzed window. |
|
|
58
|
+
|
|
59
|
+
The result is a single self-contained **HTML dashboard** you can open anywhere
|
|
60
|
+
or email to your team.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Quick start
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# From a git repository:
|
|
68
|
+
python -m codepulse /path/to/repo --open
|
|
69
|
+
|
|
70
|
+
# Common options:
|
|
71
|
+
python -m codepulse . --since "6 months ago" --top 30 -o health.html
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
> Running from the source checkout? Prefix with `PYTHONPATH=src`, or install it:
|
|
75
|
+
> `pip install -e .` then just run `codepulse`.
|
|
76
|
+
|
|
77
|
+
### Options
|
|
78
|
+
|
|
79
|
+
| Flag | Meaning |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `repo` | Path to a git repository (default: current directory) |
|
|
82
|
+
| `-o, --output` | Output HTML path (default: `codepulse-report.html`) |
|
|
83
|
+
| `--since` | Only commits after a date, e.g. `"6 months ago"` |
|
|
84
|
+
| `--max-count N` | Limit to the N most recent commits |
|
|
85
|
+
| `--top N` | Rows per table (default: 20) |
|
|
86
|
+
| `--open` | Open the report in a browser when done |
|
|
87
|
+
| `--who PATH` | Show who knows a file best (from history) and exit |
|
|
88
|
+
| `--badge` | Print a shields.io health-badge snippet and exit |
|
|
89
|
+
| `--ignore GLOB` | Exclude paths matching a glob (repeatable) |
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Use in CI (the risk gate)
|
|
94
|
+
|
|
95
|
+
CodePulse can score *only the files a change touches* against the repo's risk
|
|
96
|
+
map, then set an exit code so your pipeline can warn or block.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Score what this branch changed vs main; fail the job on HIGH risk.
|
|
100
|
+
python -m codepulse . --changed origin/main --fail-on HIGH
|
|
101
|
+
|
|
102
|
+
# Machine-readable output for dashboards / PR comments:
|
|
103
|
+
python -m codepulse . --changed origin/main --format json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Output (JSON):
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"level": "HIGH",
|
|
111
|
+
"score": 100,
|
|
112
|
+
"high_risk_count": 1,
|
|
113
|
+
"files": [
|
|
114
|
+
{ "path": "auth.py", "risk": 100, "commits": 6, "loc": 7,
|
|
115
|
+
"reasons": ["hotspot majeur", "auteur unique (Alice)"] }
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### GitHub Action (two lines)
|
|
121
|
+
|
|
122
|
+
Add CodePulse to any repository as a reusable Action — it scores each PR, posts
|
|
123
|
+
a **sticky comment** (updated in place, never spammed), and fails the check on
|
|
124
|
+
`HIGH` risk:
|
|
125
|
+
|
|
126
|
+
```yaml
|
|
127
|
+
name: CodePulse
|
|
128
|
+
on: pull_request
|
|
129
|
+
permissions:
|
|
130
|
+
contents: read
|
|
131
|
+
pull-requests: write
|
|
132
|
+
jobs:
|
|
133
|
+
risk:
|
|
134
|
+
runs-on: ubuntu-latest
|
|
135
|
+
steps:
|
|
136
|
+
- uses: actions/checkout@v4
|
|
137
|
+
with: { fetch-depth: 0 } # full history is required
|
|
138
|
+
- uses: Mehouelley/codepulse@v0.1.0
|
|
139
|
+
with:
|
|
140
|
+
fail-on: HIGH # or MEDIUM / LOW / '' to never fail
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The PR comment looks like:
|
|
144
|
+
|
|
145
|
+
> 🩺 **CodePulse — Risque : 🔴 HIGH (100/100)**
|
|
146
|
+
> 38 fichiers modifiés · 1 à risque élevé
|
|
147
|
+
>
|
|
148
|
+
> 👥 **Relecteurs suggérés** : Valentin Klein (29/37 fichiers), Dmitrii Khlapov (22/37)
|
|
149
|
+
|
|
150
|
+
Prefer a plain workflow with no external action? One ships in
|
|
151
|
+
[`.github/workflows/codepulse.yml`](.github/workflows/codepulse.yml) — copy it in as-is.
|
|
152
|
+
|
|
153
|
+
**Why DevOps cares:** a release is roughly as risky as its riskiest file.
|
|
154
|
+
Touching a well-known hotspot — or a file only one person understands — is
|
|
155
|
+
exactly where change-failure-rate (a DORA metric) comes from. The gate makes
|
|
156
|
+
that risk visible *before* merge, not after an incident.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## How it works
|
|
161
|
+
|
|
162
|
+
1. Parses `git log --numstat` into commits and file changes (no merges).
|
|
163
|
+
2. Accumulates per-file history: commit count, authors, size on disk.
|
|
164
|
+
3. Derives the signals:
|
|
165
|
+
- **Hotspot score** = `commits × complexity`, where complexity estimates
|
|
166
|
+
decision points (branch keywords + boolean operators) — far better than
|
|
167
|
+
raw line count at telling "big but simple" from "dense and risky".
|
|
168
|
+
- **Bus factor risk** = files with ≥3 commits but exactly one author.
|
|
169
|
+
- **Coupling degree** = `commits-together / min(individual commits)`,
|
|
170
|
+
ignoring bulk commits that touch many files at once.
|
|
171
|
+
|
|
172
|
+
Inspired by the "code as a crime scene" approach to software analysis, made
|
|
173
|
+
simple enough to run in one command.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Roadmap
|
|
178
|
+
|
|
179
|
+
- [x] Risk gate: score a change set, JSON output, CI exit code
|
|
180
|
+
- [x] Ignore globs / path filters for generated & data files
|
|
181
|
+
- [x] Post the gate result as a sticky PR comment (GitHub Action)
|
|
182
|
+
- [x] Trend over time (adaptive day/week/month buckets + hotspot-pressure verdict)
|
|
183
|
+
- [x] Complexity-based hotspots (decision-point estimate instead of line count)
|
|
184
|
+
- [x] Suggested PR reviewers from code ownership (in gate text/json/comment)
|
|
185
|
+
- [x] `--who <file>` — who knows this code best
|
|
186
|
+
- [x] `--badge` — shareable A–F health badge for your README
|
|
187
|
+
- [x] Orphaned-code section — complex files nobody touches anymore
|
|
188
|
+
|
|
189
|
+
## Development
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
pip install -e ".[dev]"
|
|
193
|
+
pytest
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The suite covers the complexity estimator, git parsing, every analyzer, and an
|
|
197
|
+
end-to-end run against a throwaway git repo. CI runs it on Python 3.10 and 3.12.
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
name: 'CodePulse Risk Gate'
|
|
2
|
+
description: 'Score pull-request changes against your git history, comment the risk, and gate on it.'
|
|
3
|
+
author: 'Mehouelley'
|
|
4
|
+
|
|
5
|
+
branding:
|
|
6
|
+
icon: 'activity'
|
|
7
|
+
color: 'red'
|
|
8
|
+
|
|
9
|
+
inputs:
|
|
10
|
+
path:
|
|
11
|
+
description: 'Path to the repository to analyze'
|
|
12
|
+
required: false
|
|
13
|
+
default: '.'
|
|
14
|
+
fail-on:
|
|
15
|
+
description: 'Fail the job when risk reaches this level (LOW|MEDIUM|HIGH). Leave empty to never fail.'
|
|
16
|
+
required: false
|
|
17
|
+
default: 'HIGH'
|
|
18
|
+
base-ref:
|
|
19
|
+
description: 'Base ref to diff against. Defaults to the PR base branch.'
|
|
20
|
+
required: false
|
|
21
|
+
default: ''
|
|
22
|
+
comment:
|
|
23
|
+
description: 'Post/update a sticky risk comment on the PR (true|false).'
|
|
24
|
+
required: false
|
|
25
|
+
default: 'true'
|
|
26
|
+
|
|
27
|
+
runs:
|
|
28
|
+
using: 'composite'
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/setup-python@v5
|
|
31
|
+
with:
|
|
32
|
+
python-version: '3.12'
|
|
33
|
+
|
|
34
|
+
- name: Install CodePulse
|
|
35
|
+
shell: bash
|
|
36
|
+
run: pip install "${{ github.action_path }}"
|
|
37
|
+
|
|
38
|
+
- name: Resolve base ref
|
|
39
|
+
id: base
|
|
40
|
+
shell: bash
|
|
41
|
+
run: |
|
|
42
|
+
base="${{ inputs.base-ref }}"
|
|
43
|
+
if [ -z "$base" ]; then base="origin/${{ github.base_ref }}"; fi
|
|
44
|
+
echo "ref=$base" >> "$GITHUB_OUTPUT"
|
|
45
|
+
|
|
46
|
+
- name: Comment risk on PR
|
|
47
|
+
if: ${{ inputs.comment == 'true' && github.event.pull_request.number }}
|
|
48
|
+
shell: bash
|
|
49
|
+
env:
|
|
50
|
+
GH_TOKEN: ${{ github.token }}
|
|
51
|
+
PR: ${{ github.event.pull_request.number }}
|
|
52
|
+
REPO: ${{ github.repository }}
|
|
53
|
+
run: |
|
|
54
|
+
codepulse "${{ inputs.path }}" --changed "${{ steps.base.outputs.ref }}" \
|
|
55
|
+
--format markdown > "$RUNNER_TEMP/codepulse.md"
|
|
56
|
+
cat "$RUNNER_TEMP/codepulse.md"
|
|
57
|
+
id=$(gh api "repos/$REPO/issues/$PR/comments" \
|
|
58
|
+
--jq '.[] | select(.body | startswith("<!-- codepulse -->")) | .id' | head -1)
|
|
59
|
+
if [ -n "$id" ]; then
|
|
60
|
+
gh api --method PATCH "repos/$REPO/issues/comments/$id" \
|
|
61
|
+
-f body="$(cat "$RUNNER_TEMP/codepulse.md")" >/dev/null
|
|
62
|
+
else
|
|
63
|
+
gh pr comment "$PR" --body-file "$RUNNER_TEMP/codepulse.md"
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
- name: Enforce risk gate
|
|
67
|
+
if: ${{ inputs.fail-on != '' }}
|
|
68
|
+
shell: bash
|
|
69
|
+
run: codepulse "${{ inputs.path }}" --changed "${{ steps.base.outputs.ref }}" --fail-on "${{ inputs.fail-on }}"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# CodePulse demo recording.
|
|
2
|
+
#
|
|
3
|
+
# Renders docs/demo.gif. Install VHS (https://github.com/charmbracelet/vhs),
|
|
4
|
+
# then from a checkout of a repo WITH REAL HISTORY run:
|
|
5
|
+
#
|
|
6
|
+
# vhs demo.tape
|
|
7
|
+
#
|
|
8
|
+
# (Run it inside a mature repo so hotspots/reviewers have data to show.)
|
|
9
|
+
|
|
10
|
+
Output docs/demo.gif
|
|
11
|
+
|
|
12
|
+
Set FontSize 16
|
|
13
|
+
Set Width 1120
|
|
14
|
+
Set Height 660
|
|
15
|
+
Set Padding 20
|
|
16
|
+
Set Theme "Dracula"
|
|
17
|
+
|
|
18
|
+
Type "# 1. An A–F health badge for your README" Enter
|
|
19
|
+
Type "codepulse . --badge" Sleep 500ms Enter
|
|
20
|
+
Sleep 3s
|
|
21
|
+
|
|
22
|
+
Type "# 2. Score what a pull request changes" Enter
|
|
23
|
+
Type "codepulse . --changed HEAD~20" Sleep 500ms Enter
|
|
24
|
+
Sleep 4s
|
|
25
|
+
|
|
26
|
+
Type "# 3. Who knows this file best?" Enter
|
|
27
|
+
Type "codepulse . --who src/codepulse/cli.py" Sleep 500ms Enter
|
|
28
|
+
Sleep 4s
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "codepulse-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Reveal a codebase's health from its git history — hotspots, bus factor, hidden coupling, and a CI risk gate."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Mehouelley", email = "fabricemehouelley43@gmail.com" }]
|
|
13
|
+
keywords = ["git", "code-health", "hotspots", "technical-debt", "static-analysis", "devops", "code-quality"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
22
|
+
"Topic :: Software Development :: Version Control :: Git",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/Mehouelley/codepulse"
|
|
27
|
+
Repository = "https://github.com/Mehouelley/codepulse"
|
|
28
|
+
Issues = "https://github.com/Mehouelley/codepulse/issues"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = ["pytest>=7", "build>=1.0", "twine>=5.0"]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
codepulse = "codepulse.cli:main"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["src/codepulse"]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
pythonpath = ["src"]
|
|
41
|
+
testpaths = ["tests"]
|