ci-time-tracker 0.2.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.
- ci_time_tracker-0.2.0/LICENSE +21 -0
- ci_time_tracker-0.2.0/PKG-INFO +561 -0
- ci_time_tracker-0.2.0/README.md +530 -0
- ci_time_tracker-0.2.0/ci_time_tracker/__init__.py +3 -0
- ci_time_tracker-0.2.0/ci_time_tracker/__main__.py +12 -0
- ci_time_tracker-0.2.0/ci_time_tracker/analyzer.py +444 -0
- ci_time_tracker-0.2.0/ci_time_tracker/cache.py +113 -0
- ci_time_tracker-0.2.0/ci_time_tracker/cli.py +664 -0
- ci_time_tracker-0.2.0/ci_time_tracker/config_parser.py +484 -0
- ci_time_tracker-0.2.0/ci_time_tracker/detection.py +234 -0
- ci_time_tracker-0.2.0/ci_time_tracker/gates.py +108 -0
- ci_time_tracker-0.2.0/ci_time_tracker/github_api.py +624 -0
- ci_time_tracker-0.2.0/ci_time_tracker/impact.py +361 -0
- ci_time_tracker-0.2.0/ci_time_tracker/log_parser.py +728 -0
- ci_time_tracker-0.2.0/ci_time_tracker/models.py +277 -0
- ci_time_tracker-0.2.0/ci_time_tracker/pricing.py +202 -0
- ci_time_tracker-0.2.0/ci_time_tracker/reporter.py +577 -0
- ci_time_tracker-0.2.0/ci_time_tracker.egg-info/PKG-INFO +561 -0
- ci_time_tracker-0.2.0/ci_time_tracker.egg-info/SOURCES.txt +34 -0
- ci_time_tracker-0.2.0/ci_time_tracker.egg-info/dependency_links.txt +1 -0
- ci_time_tracker-0.2.0/ci_time_tracker.egg-info/entry_points.txt +2 -0
- ci_time_tracker-0.2.0/ci_time_tracker.egg-info/requires.txt +7 -0
- ci_time_tracker-0.2.0/ci_time_tracker.egg-info/top_level.txt +1 -0
- ci_time_tracker-0.2.0/pyproject.toml +76 -0
- ci_time_tracker-0.2.0/setup.cfg +4 -0
- ci_time_tracker-0.2.0/tests/test_analyzer.py +628 -0
- ci_time_tracker-0.2.0/tests/test_cli.py +576 -0
- ci_time_tracker-0.2.0/tests/test_config_parser.py +566 -0
- ci_time_tracker-0.2.0/tests/test_detection.py +383 -0
- ci_time_tracker-0.2.0/tests/test_gates_markdown.py +253 -0
- ci_time_tracker-0.2.0/tests/test_github_api.py +492 -0
- ci_time_tracker-0.2.0/tests/test_impact.py +582 -0
- ci_time_tracker-0.2.0/tests/test_log_parser.py +448 -0
- ci_time_tracker-0.2.0/tests/test_metrics.py +450 -0
- ci_time_tracker-0.2.0/tests/test_packaging.py +18 -0
- ci_time_tracker-0.2.0/tests/test_reporter.py +554 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ci-time-tracker 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.
|
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ci-time-tracker
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Find where CI time and money go: cost, flaky steps and regressions from GitHub Actions, and a PR comment action
|
|
5
|
+
Author: ci-time-tracker contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/0xWhisp/ci-time-tracker
|
|
8
|
+
Project-URL: Repository, https://github.com/0xWhisp/ci-time-tracker
|
|
9
|
+
Keywords: ci,cd,github-actions,flaky-tests,ci-cost,devops
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: PyYAML>=6.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
28
|
+
Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff>=0.17; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# ci-time-tracker
|
|
33
|
+
|
|
34
|
+
[](https://github.com/0xWhisp/ci-time-tracker/actions/workflows/ci.yml)
|
|
35
|
+
|
|
36
|
+
Find where your CI time and money go. ci-time-tracker reads your GitHub Actions
|
|
37
|
+
history and reports the steps that cost the most, the ones that are flaky, and
|
|
38
|
+
the ones that got slower, with the commit where it started. Its GitHub Action
|
|
39
|
+
comments on each pull request with how it changes CI time and cost.
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- **Config Analysis**: Parse GitHub Actions, GitLab CI, and CircleCI configurations to extract pipeline structure
|
|
44
|
+
- **Log Analysis**: Parse build logs to compute actual step durations and statistics
|
|
45
|
+
- **Flaky Detection**: Flag steps that both failed and passed on the same commit,
|
|
46
|
+
including runs that only passed on a re-run
|
|
47
|
+
- **Regression Detection**: Flag steps whose duration jumped and stayed up, and
|
|
48
|
+
name the commit where it started
|
|
49
|
+
- **Cost and Time**: Rank steps by the time they consume, estimate cost per runner,
|
|
50
|
+
and separate queue time from execution time
|
|
51
|
+
- **Pull Request Comments**: A GitHub Action that tells each pull request how it
|
|
52
|
+
changes CI time and cost, and whether its failures are in known flaky steps
|
|
53
|
+
- **Multiple Output Formats**: Human-readable text or JSON for dashboard integration
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
Requires Python 3.10+. Not yet published on PyPI; install from source:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git clone https://github.com/0xWhisp/ci-time-tracker.git
|
|
61
|
+
cd ci-time-tracker
|
|
62
|
+
pip install -e .
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
### Config Analysis Mode
|
|
68
|
+
|
|
69
|
+
Analyze pipeline configuration files to understand structure and estimated runtimes:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Analyze GitHub Actions workflow
|
|
73
|
+
ci-time-tracker --config .github/workflows/ci.yml
|
|
74
|
+
|
|
75
|
+
# Analyze with runtime estimates
|
|
76
|
+
ci-time-tracker --config .gitlab-ci.yml --estimates estimates.json
|
|
77
|
+
|
|
78
|
+
# Output as JSON
|
|
79
|
+
ci-time-tracker --config .circleci/config.yml --format json
|
|
80
|
+
|
|
81
|
+
# Save output to file
|
|
82
|
+
ci-time-tracker --config .github/workflows/ci.yml --output report.txt
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Estimates File Format** (`estimates.json`):
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"Checkout": 5.0,
|
|
89
|
+
"Build": 120.0,
|
|
90
|
+
"Test": 300.0,
|
|
91
|
+
"Deploy": 60.0
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### GitHub Actions API Mode
|
|
96
|
+
|
|
97
|
+
Analyze recent workflow runs straight from the GitHub Actions API, with no log
|
|
98
|
+
files to collect. Timings come from the API, so nothing depends on parsing
|
|
99
|
+
runner log text:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Last 50 runs of every workflow in the repository
|
|
103
|
+
ci-time-tracker --github acme/web
|
|
104
|
+
|
|
105
|
+
# Last 200 runs of a single workflow on main
|
|
106
|
+
ci-time-tracker --github acme/web --workflow ci.yml --branch main --last 200
|
|
107
|
+
|
|
108
|
+
# JSON for a dashboard
|
|
109
|
+
ci-time-tracker --github acme/web --format json
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Set `GITHUB_TOKEN` (or `GH_TOKEN`) to reach private repositories and to raise the
|
|
113
|
+
rate limit from 60 to 5000 requests per hour:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
export GITHUB_TOKEN=$(gh auth token)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Each run costs one API request for its jobs, so completed runs are cached
|
|
120
|
+
locally and later invocations only fetch what is new. The cache lives in the
|
|
121
|
+
platform cache directory (override with `--cache-path` or the
|
|
122
|
+
`CI_TIME_TRACKER_CACHE` environment variable), and `--no-cache` skips it.
|
|
123
|
+
|
|
124
|
+
Steps are reported as `job / step`, the way the GitHub UI names them, so
|
|
125
|
+
identically named steps in different jobs stay separate.
|
|
126
|
+
|
|
127
|
+
### Log Analysis Mode
|
|
128
|
+
|
|
129
|
+
Analyze build logs to compute actual durations and detect issues:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Analyze single log file
|
|
133
|
+
ci-time-tracker --logs build.log
|
|
134
|
+
|
|
135
|
+
# Analyze directory of logs
|
|
136
|
+
ci-time-tracker --logs logs/
|
|
137
|
+
|
|
138
|
+
# Output to file
|
|
139
|
+
ci-time-tracker --logs build.log --output report.json --format json
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Piping Input
|
|
143
|
+
|
|
144
|
+
Both config and log modes support stdin input using `-`:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Pipe config from stdin
|
|
148
|
+
cat .github/workflows/ci.yml | ci-time-tracker --config -
|
|
149
|
+
|
|
150
|
+
# Pipe log from stdin
|
|
151
|
+
cat build.log | ci-time-tracker --logs -
|
|
152
|
+
|
|
153
|
+
# Chain with other commands
|
|
154
|
+
curl https://example.com/config.yml | ci-time-tracker --config - --format json
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Module Execution
|
|
158
|
+
|
|
159
|
+
You can also run ci-time-tracker as a Python module:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
python -m ci_time_tracker --config .github/workflows/ci.yml
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Output Example
|
|
166
|
+
|
|
167
|
+
### Text Output (Log Mode)
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
==============================================================================================================
|
|
171
|
+
CI Time Tracker Report (log mode)
|
|
172
|
+
==============================================================================================================
|
|
173
|
+
Generated: 2026-09-27 03:43:13
|
|
174
|
+
Mode: log
|
|
175
|
+
|
|
176
|
+
SUMMARY
|
|
177
|
+
--------------------------------------------------------------------------------------------------------------
|
|
178
|
+
build_count: 10
|
|
179
|
+
step_count: 66
|
|
180
|
+
total_execution_seconds: 1h 05m
|
|
181
|
+
job_count: 35
|
|
182
|
+
total_queue_seconds: 2m 42s
|
|
183
|
+
queue_p50_seconds: 4.0s
|
|
184
|
+
queue_p90_seconds: 10.0s
|
|
185
|
+
estimated_cost_usd: $2.81
|
|
186
|
+
priced_jobs: 35
|
|
187
|
+
pricing_source: built-in defaults
|
|
188
|
+
flaky_detection: same-commit
|
|
189
|
+
regression_checked_steps: 12
|
|
190
|
+
regression_min_builds: 10
|
|
191
|
+
note: cost uses private-repository rates and GitHub's per-job rounding; public repos pay nothing for standard runners
|
|
192
|
+
|
|
193
|
+
TOP TIME CONSUMERS
|
|
194
|
+
--------------------------------------------------------------------------------------------------------------
|
|
195
|
+
1. build / Run tests 45m 18s 69.2% 23 runs $1.53
|
|
196
|
+
2. build / Install dependencies 7m 08s 10.9% 23 runs $0.30
|
|
197
|
+
3. build / Set up Python 1m 43s 2.6% 23 runs $0.08
|
|
198
|
+
|
|
199
|
+
ISSUES
|
|
200
|
+
--------------------------------------------------------------------------------------------------------------
|
|
201
|
+
[WARNING] REGRESSION: build / Install dependencies
|
|
202
|
+
Median 10.0s -> 28.0s (+180%, +18.0s)
|
|
203
|
+
Started at commit 3f9c2ab (build 9876543210, 2026-09-14)
|
|
204
|
+
[ERROR] FLAKY: integration / Run e2e tests
|
|
205
|
+
Failed and passed on the same commit in 2 of 17 commits
|
|
206
|
+
Passed on re-run after failing in 2 run(s)
|
|
207
|
+
e.g. 1a2b3c4, 9f8e7d6
|
|
208
|
+
|
|
209
|
+
STEPS
|
|
210
|
+
--------------------------------------------------------------------------------------------------------------
|
|
211
|
+
Step Name Runs Total P50 P90 P95 P99 Cost Flags
|
|
212
|
+
--------------------------------------------------------------------------------------------------------------
|
|
213
|
+
build / Run tests 23 45m 18s 112.00s 178.00s 178.00s 179.00s $1.53
|
|
214
|
+
build / Install dependencies 23 7m 08s 10.00s 28.00s 31.00s 115.00s $0.30 REGR
|
|
215
|
+
build / Set up Python 23 1m 43s 2.00s 9.00s 44.00s 49.00s $0.08
|
|
216
|
+
... and 63 more steps (use --top 0 to show all)
|
|
217
|
+
|
|
218
|
+
==============================================================================================================
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Steps are ordered by the time they consume, because that is what a CI bill is
|
|
222
|
+
made of. `--top N` limits the table (default 25, `0` shows everything). (The
|
|
223
|
+
issues in this example are illustrative; the rest is real output for
|
|
224
|
+
psf/requests.)
|
|
225
|
+
|
|
226
|
+
### JSON Output
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"title": "CI Time Tracker Report (log mode)",
|
|
231
|
+
"mode": "log",
|
|
232
|
+
"generated_at": "2026-09-27T03:43:13",
|
|
233
|
+
"summary": {
|
|
234
|
+
"mode": "log",
|
|
235
|
+
"build_count": 10,
|
|
236
|
+
"step_count": 66,
|
|
237
|
+
"total_execution_seconds": 3918.0,
|
|
238
|
+
"job_count": 35,
|
|
239
|
+
"total_queue_seconds": 162.0,
|
|
240
|
+
"queue_p50_seconds": 4.0,
|
|
241
|
+
"queue_p90_seconds": 10.0,
|
|
242
|
+
"estimated_cost_usd": 2.808,
|
|
243
|
+
"priced_jobs": 35,
|
|
244
|
+
"pricing_source": "built-in defaults"
|
|
245
|
+
},
|
|
246
|
+
"steps": [
|
|
247
|
+
{
|
|
248
|
+
"name": "build / Run tests",
|
|
249
|
+
"execution_count": 23,
|
|
250
|
+
"success_count": 23,
|
|
251
|
+
"failure_count": 0,
|
|
252
|
+
"failure_rate": 0.0,
|
|
253
|
+
"total_duration": 2718.0,
|
|
254
|
+
"mean_duration": 118.17,
|
|
255
|
+
"p50": 112.0,
|
|
256
|
+
"p90": 178.0,
|
|
257
|
+
"p95": 178.0,
|
|
258
|
+
"p99": 179.0,
|
|
259
|
+
"is_regression": false,
|
|
260
|
+
"is_flaky": false,
|
|
261
|
+
"runner": "ubuntu-22.04",
|
|
262
|
+
"estimated_cost_usd": 1.5312,
|
|
263
|
+
"regression": null,
|
|
264
|
+
"flaky_evidence": null
|
|
265
|
+
}
|
|
266
|
+
],
|
|
267
|
+
"issues": [
|
|
268
|
+
{
|
|
269
|
+
"type": "regression",
|
|
270
|
+
"step": "build / Install dependencies",
|
|
271
|
+
"severity": "warning",
|
|
272
|
+
"baseline_median": 10.0,
|
|
273
|
+
"current_median": 28.0,
|
|
274
|
+
"increase_seconds": 18.0,
|
|
275
|
+
"increase_pct": 1.8,
|
|
276
|
+
"baseline_builds": 14,
|
|
277
|
+
"current_builds": 9,
|
|
278
|
+
"started_at": {
|
|
279
|
+
"build_id": "9876543210",
|
|
280
|
+
"head_sha": "3f9c2ab",
|
|
281
|
+
"timestamp": "2026-09-14T10:02:11+00:00"
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
]
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Using It in CI
|
|
289
|
+
|
|
290
|
+
### Run summary in Markdown
|
|
291
|
+
|
|
292
|
+
`--format markdown` renders the report for a workflow run's summary page or a
|
|
293
|
+
PR comment: headline numbers, the issues with their evidence, the top time
|
|
294
|
+
consumers, and the full step table collapsed.
|
|
295
|
+
|
|
296
|
+
```yaml
|
|
297
|
+
- name: CI time report
|
|
298
|
+
run: |
|
|
299
|
+
pip install git+https://github.com/0xWhisp/ci-time-tracker.git
|
|
300
|
+
ci-time-tracker --github ${{ github.repository }} --workflow ci.yml --last 100 --group-matrix --format markdown >> "$GITHUB_STEP_SUMMARY"
|
|
301
|
+
env:
|
|
302
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Output is always UTF-8, including on Windows runners.
|
|
306
|
+
|
|
307
|
+
### Quality gates
|
|
308
|
+
|
|
309
|
+
Reporting an issue does not fail the run. Gates do:
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
# Fail when a step is flaky or has regressed
|
|
313
|
+
ci-time-tracker --github acme/web --fail-on flaky,regression
|
|
314
|
+
|
|
315
|
+
# Fail when the median build takes longer than 10 minutes
|
|
316
|
+
ci-time-tracker --github acme/web --max-duration 600
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
The report is written first, so a failing pipeline still shows why; the reasons
|
|
320
|
+
are also printed to stderr. The median build duration counts each build once,
|
|
321
|
+
leaving out the earlier attempts of re-runs.
|
|
322
|
+
|
|
323
|
+
### Pull request comments (GitHub Action)
|
|
324
|
+
|
|
325
|
+
Once CI finishes on a pull request, the action compares that run with the
|
|
326
|
+
median of recent runs of the same workflow on the default branch and keeps a
|
|
327
|
+
single comment on the pull request up to date:
|
|
328
|
+
|
|
329
|
+
```markdown
|
|
330
|
+
## ⏱️ CI time impact
|
|
331
|
+
|
|
332
|
+
**CI** run 39102310449 on `9f8e7d6` took **8m 12s**: **🔺 +1m 30s (+22%)** compared
|
|
333
|
+
with the median of the last 20 runs on `main`.
|
|
334
|
+
|
|
335
|
+
| | `main` (median) | This run | Change |
|
|
336
|
+
|---|---:|---:|---:|
|
|
337
|
+
| Wall clock | 6m 42s | 8m 12s | 🔺 +1m 30s (+22%) |
|
|
338
|
+
| Compute time | 36m 08s | 41m 10s | ≈ +5m 02s (+14%) |
|
|
339
|
+
| Estimated cost | $0.29 | $0.33 | +$0.04 |
|
|
340
|
+
|
|
341
|
+
### Steps that changed
|
|
342
|
+
|
|
343
|
+
| Step | `main` (median) | This run | Change |
|
|
344
|
+
|---|---:|---:|---:|
|
|
345
|
+
| build / Run tests | 1m 52s | 2m 40s | 🔺 +48.0s (+43%) |
|
|
346
|
+
|
|
347
|
+
### Failures
|
|
348
|
+
|
|
349
|
+
- ❌ `integration / e2e`: ⚠️ known to be flaky (failed and passed on the same
|
|
350
|
+
commit in 2 of 15 commits). A re-run may pass.
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Add it as a workflow of its own, triggered when CI completes:
|
|
354
|
+
|
|
355
|
+
```yaml
|
|
356
|
+
name: PR CI impact
|
|
357
|
+
|
|
358
|
+
on:
|
|
359
|
+
workflow_run:
|
|
360
|
+
workflows: [CI] # the name of your CI workflow
|
|
361
|
+
types: [completed]
|
|
362
|
+
|
|
363
|
+
permissions:
|
|
364
|
+
actions: read
|
|
365
|
+
pull-requests: write
|
|
366
|
+
|
|
367
|
+
jobs:
|
|
368
|
+
comment:
|
|
369
|
+
if: github.event.workflow_run.event == 'pull_request'
|
|
370
|
+
runs-on: ubuntu-latest
|
|
371
|
+
steps:
|
|
372
|
+
- uses: 0xWhisp/ci-time-tracker@main # pin a release tag once published
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
`workflow_run` runs the copy of the workflow on the default branch with the
|
|
376
|
+
repository's own token, so it can comment on pull requests from forks without
|
|
377
|
+
ever running their code. For the same reason, it takes effect once the
|
|
378
|
+
workflow file is on the default branch.
|
|
379
|
+
|
|
380
|
+
| Input | Default | |
|
|
381
|
+
|---|---|---|
|
|
382
|
+
| `baseline-branch` | default branch | Branch to compare with |
|
|
383
|
+
| `baseline-runs` | `20` | Recent baseline runs to compare with |
|
|
384
|
+
| `group-matrix` | `true` | Merge the legs of matrix jobs |
|
|
385
|
+
| `regression-threshold` | `25` | Percentage (and at least 10s) for a step to be listed |
|
|
386
|
+
| `comment` | `true` | Post or update the pull request comment |
|
|
387
|
+
| `fail-on` | | `regression` fails when a step got slower; `flaky` when the run hit a known flaky step |
|
|
388
|
+
| `run-id` | the triggering run | Workflow run to report on |
|
|
389
|
+
| `github-token` | `github.token` | Needs `actions: read` and `pull-requests: write` |
|
|
390
|
+
|
|
391
|
+
The same comparison is available from the command line:
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
ci-time-tracker --github acme/web --run-id 39102310449 --group-matrix
|
|
395
|
+
ci-time-tracker --github acme/web --run-id 39102310449 --comment # post it
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Exit codes
|
|
399
|
+
|
|
400
|
+
| Code | Meaning |
|
|
401
|
+
|------|---------|
|
|
402
|
+
| 0 | Report written, gates passed |
|
|
403
|
+
| 1 | File not found or unreadable, or nothing to analyze |
|
|
404
|
+
| 2 | Invalid configuration, log, estimates or pricing file |
|
|
405
|
+
| 3 | GitHub API error (rate limit, permissions, not found) |
|
|
406
|
+
| 4 | A quality gate failed |
|
|
407
|
+
|
|
408
|
+
## Cost Estimates
|
|
409
|
+
|
|
410
|
+
Costs are estimated from the runner each job ran on, using GitHub's
|
|
411
|
+
[list prices](https://docs.github.com/en/billing/reference/actions-runner-pricing)
|
|
412
|
+
for **private repositories** as of October 2026, with each job rounded up to a
|
|
413
|
+
whole minute. Public repositories pay nothing for standard GitHub-hosted
|
|
414
|
+
runners; larger runners are billed for public repositories too.
|
|
415
|
+
|
|
416
|
+
| Runner | USD per minute |
|
|
417
|
+
|---|---:|
|
|
418
|
+
| Linux 2-core (`ubuntu-*`) | 0.006 |
|
|
419
|
+
| Linux 1-core (`ubuntu-slim`) | 0.002 |
|
|
420
|
+
| Linux 2-core arm64 (`ubuntu-*-arm`) | 0.005 |
|
|
421
|
+
| Windows 2-core, x64 or arm64 (`windows-*`) | 0.010 |
|
|
422
|
+
| macOS 3/4-core (`macos-*`, `macos-*-intel`) | 0.062 |
|
|
423
|
+
| macOS 12-core (`macos-*-large`) | 0.077 |
|
|
424
|
+
| macOS 5-core M2 Pro (`macos-*-xlarge`) | 0.102 |
|
|
425
|
+
|
|
426
|
+
- Versioned labels are recognized by family, so a new image such as
|
|
427
|
+
`ubuntu-26.04` or `macos-26` is priced without an update.
|
|
428
|
+
- Larger runners with custom names are priced from the platform, architecture
|
|
429
|
+
and core count in the label (e.g. `ubuntu-24.04-8-cores`), using GitHub's
|
|
430
|
+
published rates. A core count GitHub does not publish stays unpriced.
|
|
431
|
+
- Self-hosted runners cost `$0.00`.
|
|
432
|
+
- Runners from other providers (Depot, Namespace, ...) and other custom names
|
|
433
|
+
have no known rate. They are listed under `unpriced_runners`, and the report
|
|
434
|
+
says how many jobs the estimate covers ("cost covers only 190 of 790 jobs"),
|
|
435
|
+
so a partial estimate is never shown as the whole cost.
|
|
436
|
+
- Per-step costs are attributed proportionally and do not include the per-job
|
|
437
|
+
rounding, so they add up to slightly less than the job total.
|
|
438
|
+
|
|
439
|
+
Prices change. Override any label, including the families above, with a JSON
|
|
440
|
+
file of label to USD per minute:
|
|
441
|
+
|
|
442
|
+
```bash
|
|
443
|
+
ci-time-tracker --github acme/web --pricing pricing.json
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
```json
|
|
447
|
+
{
|
|
448
|
+
"depot-ubuntu-24.04": 0.004,
|
|
449
|
+
"our-big-runner": 0.032
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
## Matrix Builds
|
|
454
|
+
|
|
455
|
+
GitHub names each leg of a matrix separately, so `Run tests` becomes
|
|
456
|
+
`build (3.12, ubuntu-latest) / Run tests` and its siblings: one step per leg.
|
|
457
|
+
`--group-matrix` merges them, which is usually what you want when asking where
|
|
458
|
+
the time goes:
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
# Without grouping: "build (3.12, windows-latest) / Run tests 2m 59s 4.6%"
|
|
462
|
+
# With grouping: "build / Run tests 45m 18s 69.2%"
|
|
463
|
+
ci-time-tracker --github acme/web --group-matrix
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Grouping mixes runners with different rates, so the cost of a merged step spans
|
|
467
|
+
whatever machines its legs ran on.
|
|
468
|
+
|
|
469
|
+
## Detection Criteria
|
|
470
|
+
|
|
471
|
+
### Flaky Steps
|
|
472
|
+
|
|
473
|
+
A step is **flaky** when it both failed and passed **on the same commit**: the
|
|
474
|
+
code did not change, the outcome did. A failed run that passed when re-run is
|
|
475
|
+
the clearest case, so the earlier attempts of re-run workflows are fetched too.
|
|
476
|
+
|
|
477
|
+
- Outcomes are compared per workflow, step and commit, using each matrix leg's
|
|
478
|
+
own name: a leg that always fails on Windows is broken there, not flaky.
|
|
479
|
+
- A high failure rate alone is **not** evidence. A step that fails because a
|
|
480
|
+
commit broke it, and passes once the fix lands, is doing its job.
|
|
481
|
+
|
|
482
|
+
Logs without commit information (log files) fall back to the failure-rate
|
|
483
|
+
heuristic (`0.05 < failure_rate < 0.95`), which cannot tell flakiness from real
|
|
484
|
+
breakage. The report states which method was used (`flaky_detection`).
|
|
485
|
+
|
|
486
|
+
### Regressions
|
|
487
|
+
|
|
488
|
+
A step has **regressed** when its duration jumped and stayed up:
|
|
489
|
+
|
|
490
|
+
1. Its history (one sample per build, oldest first) is split at the point that
|
|
491
|
+
best separates two stable segments.
|
|
492
|
+
2. The later segment's median must be at least **25%** higher
|
|
493
|
+
(`--regression-threshold`) and at least **10 seconds** higher, so `1s -> 2s`
|
|
494
|
+
steps are not reported.
|
|
495
|
+
3. The most recent 5 builds must still be that slow, so a spike that already
|
|
496
|
+
recovered is not reported.
|
|
497
|
+
|
|
498
|
+
The report names the commit and build where the regression started. A step
|
|
499
|
+
needs at least **10 builds** to be judged; `regression_checked_steps` says how
|
|
500
|
+
many had enough history. With `--group-matrix`, the legs of a build are
|
|
501
|
+
combined into one sample (their median).
|
|
502
|
+
|
|
503
|
+
## Supported CI Providers
|
|
504
|
+
|
|
505
|
+
| Provider | Config Format | Log Format |
|
|
506
|
+
|----------|--------------|------------|
|
|
507
|
+
| GitHub Actions | YAML | Text/JSON |
|
|
508
|
+
| GitLab CI | YAML | Text/JSON |
|
|
509
|
+
| CircleCI | YAML | Text/JSON |
|
|
510
|
+
|
|
511
|
+
## Limitations
|
|
512
|
+
|
|
513
|
+
- Log parsing relies on timestamp patterns; non-standard formats may not parse correctly
|
|
514
|
+
- Flaky detection needs the same commit to have been built more than once
|
|
515
|
+
- Regression detection needs at least 10 builds of a step
|
|
516
|
+
- Estimated durations in config mode require user-provided estimates file
|
|
517
|
+
|
|
518
|
+
## Development
|
|
519
|
+
|
|
520
|
+
```bash
|
|
521
|
+
# Install dev dependencies
|
|
522
|
+
pip install -e ".[dev]"
|
|
523
|
+
|
|
524
|
+
# Run tests
|
|
525
|
+
pytest
|
|
526
|
+
|
|
527
|
+
# Run with coverage
|
|
528
|
+
pytest --cov=ci_time_tracker
|
|
529
|
+
|
|
530
|
+
# Lint (rules are pinned in pyproject.toml)
|
|
531
|
+
ruff check .
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
CI runs the suite on Linux and Windows for Python 3.10 to 3.14, then runs
|
|
535
|
+
the tool on its own CI history: each run's summary page shows the resulting
|
|
536
|
+
Markdown report.
|
|
537
|
+
|
|
538
|
+
### Releasing
|
|
539
|
+
|
|
540
|
+
Releases are published to PyPI by `.github/workflows/release.yml` when a
|
|
541
|
+
version tag is pushed, using PyPI trusted publishing (no token in the repo).
|
|
542
|
+
|
|
543
|
+
One-time setup on PyPI: add a trusted publisher for project `ci-time-tracker`
|
|
544
|
+
with owner `0xWhisp`, repository `ci-time-tracker`, workflow `release.yml`
|
|
545
|
+
and environment `pypi`, and create the `pypi` environment in the repository
|
|
546
|
+
settings.
|
|
547
|
+
|
|
548
|
+
Then, for each release:
|
|
549
|
+
|
|
550
|
+
1. Bump `version` in `pyproject.toml` and `__version__` in
|
|
551
|
+
`ci_time_tracker/__init__.py` (a test checks they agree), and add the
|
|
552
|
+
release to [CHANGELOG.md](CHANGELOG.md).
|
|
553
|
+
2. Tag and push: `git tag v0.2.0 && git push origin v0.2.0`.
|
|
554
|
+
3. Create the GitHub release from the tag, with the changelog entry as its
|
|
555
|
+
notes, and tick "Publish this Action to the GitHub Marketplace".
|
|
556
|
+
|
|
557
|
+
The workflow refuses to publish when the tag and the versions disagree.
|
|
558
|
+
|
|
559
|
+
## License
|
|
560
|
+
|
|
561
|
+
MIT License - see [LICENSE](LICENSE) for details.
|