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.
Files changed (36) hide show
  1. ci_time_tracker-0.2.0/LICENSE +21 -0
  2. ci_time_tracker-0.2.0/PKG-INFO +561 -0
  3. ci_time_tracker-0.2.0/README.md +530 -0
  4. ci_time_tracker-0.2.0/ci_time_tracker/__init__.py +3 -0
  5. ci_time_tracker-0.2.0/ci_time_tracker/__main__.py +12 -0
  6. ci_time_tracker-0.2.0/ci_time_tracker/analyzer.py +444 -0
  7. ci_time_tracker-0.2.0/ci_time_tracker/cache.py +113 -0
  8. ci_time_tracker-0.2.0/ci_time_tracker/cli.py +664 -0
  9. ci_time_tracker-0.2.0/ci_time_tracker/config_parser.py +484 -0
  10. ci_time_tracker-0.2.0/ci_time_tracker/detection.py +234 -0
  11. ci_time_tracker-0.2.0/ci_time_tracker/gates.py +108 -0
  12. ci_time_tracker-0.2.0/ci_time_tracker/github_api.py +624 -0
  13. ci_time_tracker-0.2.0/ci_time_tracker/impact.py +361 -0
  14. ci_time_tracker-0.2.0/ci_time_tracker/log_parser.py +728 -0
  15. ci_time_tracker-0.2.0/ci_time_tracker/models.py +277 -0
  16. ci_time_tracker-0.2.0/ci_time_tracker/pricing.py +202 -0
  17. ci_time_tracker-0.2.0/ci_time_tracker/reporter.py +577 -0
  18. ci_time_tracker-0.2.0/ci_time_tracker.egg-info/PKG-INFO +561 -0
  19. ci_time_tracker-0.2.0/ci_time_tracker.egg-info/SOURCES.txt +34 -0
  20. ci_time_tracker-0.2.0/ci_time_tracker.egg-info/dependency_links.txt +1 -0
  21. ci_time_tracker-0.2.0/ci_time_tracker.egg-info/entry_points.txt +2 -0
  22. ci_time_tracker-0.2.0/ci_time_tracker.egg-info/requires.txt +7 -0
  23. ci_time_tracker-0.2.0/ci_time_tracker.egg-info/top_level.txt +1 -0
  24. ci_time_tracker-0.2.0/pyproject.toml +76 -0
  25. ci_time_tracker-0.2.0/setup.cfg +4 -0
  26. ci_time_tracker-0.2.0/tests/test_analyzer.py +628 -0
  27. ci_time_tracker-0.2.0/tests/test_cli.py +576 -0
  28. ci_time_tracker-0.2.0/tests/test_config_parser.py +566 -0
  29. ci_time_tracker-0.2.0/tests/test_detection.py +383 -0
  30. ci_time_tracker-0.2.0/tests/test_gates_markdown.py +253 -0
  31. ci_time_tracker-0.2.0/tests/test_github_api.py +492 -0
  32. ci_time_tracker-0.2.0/tests/test_impact.py +582 -0
  33. ci_time_tracker-0.2.0/tests/test_log_parser.py +448 -0
  34. ci_time_tracker-0.2.0/tests/test_metrics.py +450 -0
  35. ci_time_tracker-0.2.0/tests/test_packaging.py +18 -0
  36. 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
+ [![CI](https://github.com/0xWhisp/ci-time-tracker/actions/workflows/ci.yml/badge.svg)](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.