github-actions-ingester 0.2.1__tar.gz → 0.2.2__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.
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/CHANGELOG.md +14 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/PKG-INFO +1 -1
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/pyproject.toml +1 -1
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/src/github_actions_ingester/__init__.py +1 -1
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/src/github_actions_ingester/__main__.py +12 -1
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/src/github_actions_ingester/collector.py +22 -7
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/src/github_actions_ingester/github.py +4 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/tests/test_collector.py +31 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/tests/test_github.py +18 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/.gitignore +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/LICENSE +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/README.md +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/src/github_actions_ingester/app_manifest.py +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/src/github_actions_ingester/config.py +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/src/github_actions_ingester/metrics.py +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/src/github_actions_ingester/migrations/0001_initial.sql +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/src/github_actions_ingester/ratelimit.py +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/src/github_actions_ingester/server.py +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/src/github_actions_ingester/store.py +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/src/github_actions_ingester/workflow_schedule.py +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/tests/__init__.py +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/tests/conftest.py +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/tests/helpers.py +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/tests/test_app_manifest.py +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/tests/test_cli.py +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/tests/test_config.py +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/tests/test_dashboard.py +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/tests/test_metrics.py +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/tests/test_ratelimit.py +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/tests/test_server.py +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/tests/test_store.py +0 -0
- {github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/tests/test_workflow_schedule.py +0 -0
|
@@ -4,6 +4,20 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project
|
|
5
5
|
adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.2.2] — 2026-09-05
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- The GitHub rate-limit gauges were refreshed once per repository, so during
|
|
11
|
+
a long first backfill of a busy repository they could lag by twenty
|
|
12
|
+
minutes while the real budget ran out. They now follow every response,
|
|
13
|
+
as the README already said they did.
|
|
14
|
+
- `runs_upserted_total` and `jobs_upserted_total` move as rows are written
|
|
15
|
+
instead of once when the repository finishes.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `jobs.progress` log line every 250 runs during the jobs phase, which was
|
|
19
|
+
silent for the whole duration of a large backfill.
|
|
20
|
+
|
|
7
21
|
## [0.2.1] — 2026-09-05
|
|
8
22
|
|
|
9
23
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: github-actions-ingester
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Standalone GitHub Actions ingester — pulls workflows, runs and jobs from the GitHub API into PostgreSQL for Grafana dashboards, FinOps and alerting. Prometheus /metrics for ingester health and scheduled-workflow liveness.
|
|
5
5
|
Project-URL: Homepage, https://github.com/danielgines/github-actions-ingester
|
|
6
6
|
Project-URL: Repository, https://github.com/danielgines/github-actions-ingester
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "github-actions-ingester"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "Standalone GitHub Actions ingester — pulls workflows, runs and jobs from the GitHub API into PostgreSQL for Grafana dashboards, FinOps and alerting. Prometheus /metrics for ingester health and scheduled-workflow liveness."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -26,7 +26,7 @@ from . import __version__
|
|
|
26
26
|
from .app_manifest import convert_code, manifest_form_url, manifest_json, write_private_key
|
|
27
27
|
from .collector import Collector
|
|
28
28
|
from .config import Settings, load_settings
|
|
29
|
-
from .github import AppAuth, GitHubAPIError, GitHubClient, TokenAuth
|
|
29
|
+
from .github import AppAuth, GitHubAPIError, GitHubClient, RateLimitState, TokenAuth
|
|
30
30
|
from .metrics import Metrics
|
|
31
31
|
from .ratelimit import RateLimiter
|
|
32
32
|
from .server import MetricsServer
|
|
@@ -77,6 +77,16 @@ def _build_client(settings: Settings, metrics: Metrics | None = None) -> GitHubC
|
|
|
77
77
|
if metrics is not None:
|
|
78
78
|
metrics.github_requests_total.labels(status=str(status)).inc()
|
|
79
79
|
|
|
80
|
+
# Refresh the budget gauges on every response, not once per repository:
|
|
81
|
+
# a busy repository keeps the collector inside one ingest_repository()
|
|
82
|
+
# call for many minutes, and an alert on the remaining budget must not
|
|
83
|
+
# wait for it to finish.
|
|
84
|
+
def on_rate_limit(rl: RateLimitState) -> None:
|
|
85
|
+
if metrics is not None:
|
|
86
|
+
metrics.github_rate_limit_remaining.set(rl.remaining)
|
|
87
|
+
metrics.github_rate_limit_limit.set(rl.limit)
|
|
88
|
+
metrics.github_rate_limit_reset_timestamp_seconds.set(rl.reset_at)
|
|
89
|
+
|
|
80
90
|
return GitHubClient(
|
|
81
91
|
auth=auth,
|
|
82
92
|
base_url=settings.github_api_base,
|
|
@@ -85,6 +95,7 @@ def _build_client(settings: Settings, metrics: Metrics | None = None) -> GitHubC
|
|
|
85
95
|
min_remaining=settings.api_min_remaining,
|
|
86
96
|
max_retries=settings.api_max_retries,
|
|
87
97
|
on_request=on_request,
|
|
98
|
+
on_rate_limit=on_rate_limit,
|
|
88
99
|
)
|
|
89
100
|
|
|
90
101
|
|
|
@@ -56,6 +56,7 @@ CONCLUSIONS = (
|
|
|
56
56
|
"stale",
|
|
57
57
|
"startup_failure",
|
|
58
58
|
)
|
|
59
|
+
_JOBS_PROGRESS_EVERY = 250
|
|
59
60
|
_RUN_BATCH = 500
|
|
60
61
|
|
|
61
62
|
|
|
@@ -198,9 +199,10 @@ class Collector:
|
|
|
198
199
|
|
|
199
200
|
written = 0
|
|
200
201
|
for batch in _batched(self._client.list_runs(repo, since, now), _RUN_BATCH):
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
n = self._store.upsert_runs(batch)
|
|
203
|
+
written += n
|
|
204
|
+
if n:
|
|
205
|
+
self._metrics.runs_upserted_total.labels(repository=repo.full_name).inc(n)
|
|
204
206
|
|
|
205
207
|
# Open runs that fell out of the window: refresh them individually.
|
|
206
208
|
stale = self._store.open_runs_before(repo.id, since, s.max_open_run_refresh)
|
|
@@ -213,8 +215,12 @@ class Collector:
|
|
|
213
215
|
raise
|
|
214
216
|
self._store.upsert_runs([run])
|
|
215
217
|
|
|
218
|
+
# One request per run: on a first backfill of a busy repository this
|
|
219
|
+
# loop runs for many minutes, so it reports progress on the way and
|
|
220
|
+
# moves the counters as it goes instead of once at the end.
|
|
216
221
|
jobs_written = 0
|
|
217
|
-
|
|
222
|
+
pending = self._store.runs_needing_jobs(repo.id)
|
|
223
|
+
for done, run_id in enumerate(pending, start=1):
|
|
218
224
|
try:
|
|
219
225
|
jobs = list(self._client.list_jobs(repo, run_id, s.jobs_filter))
|
|
220
226
|
except GitHubAPIError as exc:
|
|
@@ -223,9 +229,18 @@ class Collector:
|
|
|
223
229
|
self._store.upsert_jobs(run_id, [])
|
|
224
230
|
continue
|
|
225
231
|
raise
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
232
|
+
n = self._store.upsert_jobs(run_id, jobs)
|
|
233
|
+
jobs_written += n
|
|
234
|
+
if n:
|
|
235
|
+
self._metrics.jobs_upserted_total.labels(repository=repo.full_name).inc(n)
|
|
236
|
+
if done % _JOBS_PROGRESS_EVERY == 0 and done < len(pending):
|
|
237
|
+
logger.info(
|
|
238
|
+
"jobs.progress",
|
|
239
|
+
repo=repo.full_name,
|
|
240
|
+
runs_done=done,
|
|
241
|
+
runs_total=len(pending),
|
|
242
|
+
jobs=jobs_written,
|
|
243
|
+
)
|
|
229
244
|
|
|
230
245
|
self._store.set_cursor(repo.id, now, written)
|
|
231
246
|
logger.info(
|
|
@@ -338,6 +338,7 @@ class GitHubClient:
|
|
|
338
338
|
min_remaining: int = 200,
|
|
339
339
|
max_retries: int = 4,
|
|
340
340
|
on_request: Callable[[int], None] | None = None,
|
|
341
|
+
on_rate_limit: Callable[[RateLimitState], None] | None = None,
|
|
341
342
|
sleep: Callable[[float], None] = time.sleep,
|
|
342
343
|
clock: Callable[[], float] = time.time,
|
|
343
344
|
) -> None:
|
|
@@ -355,6 +356,7 @@ class GitHubClient:
|
|
|
355
356
|
self._min_remaining = min_remaining
|
|
356
357
|
self._max_retries = max_retries
|
|
357
358
|
self._on_request = on_request
|
|
359
|
+
self._on_rate_limit = on_rate_limit
|
|
358
360
|
self._sleep = sleep
|
|
359
361
|
self._clock = clock
|
|
360
362
|
self.rate_limit = RateLimitState()
|
|
@@ -404,6 +406,8 @@ class GitHubClient:
|
|
|
404
406
|
self._sleep(min(2**attempt, 30))
|
|
405
407
|
continue
|
|
406
408
|
self.rate_limit.update(resp.headers)
|
|
409
|
+
if self._on_rate_limit is not None:
|
|
410
|
+
self._on_rate_limit(self.rate_limit)
|
|
407
411
|
if self._on_request is not None:
|
|
408
412
|
self._on_request(resp.status_code)
|
|
409
413
|
if resp.status_code < 400:
|
|
@@ -195,6 +195,37 @@ def test_first_cycle_backfills_everything(migrated_store: Store, clock: list[dat
|
|
|
195
195
|
assert _gauge(m, "gha_scheduled_workflow_last_conclusion", **labels, conclusion="failure") == 0
|
|
196
196
|
|
|
197
197
|
|
|
198
|
+
def test_jobs_phase_reports_progress_and_moves_counters(
|
|
199
|
+
migrated_store: Store, clock: list[datetime], monkeypatch: pytest.MonkeyPatch
|
|
200
|
+
) -> None:
|
|
201
|
+
"""A long jobs phase logs every N runs and increments the counter per run,
|
|
202
|
+
so a stalled backfill is visible before ingest_repository() returns."""
|
|
203
|
+
import github_actions_ingester.collector as mod
|
|
204
|
+
|
|
205
|
+
gh = FakeGitHub()
|
|
206
|
+
gh.runs[1] = [run(300 + i, 1, 10) for i in range(5)]
|
|
207
|
+
for i in range(5):
|
|
208
|
+
gh.jobs[300 + i] = [job(3000 + i, 300 + i, 1)]
|
|
209
|
+
monkeypatch.setattr(mod, "_JOBS_PROGRESS_EVERY", 2)
|
|
210
|
+
events: list[dict[str, Any]] = []
|
|
211
|
+
seen_counter: list[float | None] = []
|
|
212
|
+
c, m = make(migrated_store, gh, clock)
|
|
213
|
+
|
|
214
|
+
def spy(event: str, **kw: Any) -> None:
|
|
215
|
+
if event == "jobs.progress":
|
|
216
|
+
events.append(kw)
|
|
217
|
+
seen_counter.append(
|
|
218
|
+
_gauge(m, "gha_ingester_jobs_upserted_total", repository="acme/web")
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
monkeypatch.setattr(mod.logger, "info", spy)
|
|
222
|
+
c.run_cycle()
|
|
223
|
+
assert [(e["runs_done"], e["runs_total"]) for e in events] == [(2, 5), (4, 5)]
|
|
224
|
+
# The counter had already moved when the progress line was written.
|
|
225
|
+
assert seen_counter == [2.0, 4.0]
|
|
226
|
+
assert _gauge(m, "gha_ingester_jobs_upserted_total", repository="acme/web") == 5.0
|
|
227
|
+
|
|
228
|
+
|
|
198
229
|
def test_second_cycle_is_incremental(migrated_store: Store, clock: list[datetime]) -> None:
|
|
199
230
|
gh = FakeGitHub()
|
|
200
231
|
c, _ = make(migrated_store, gh, clock)
|
|
@@ -330,6 +330,24 @@ def test_on_request_hook_receives_status(httpx_mock: HTTPXMock) -> None:
|
|
|
330
330
|
assert seen == [404]
|
|
331
331
|
|
|
332
332
|
|
|
333
|
+
def test_on_rate_limit_hook_fires_on_every_response(httpx_mock: HTTPXMock) -> None:
|
|
334
|
+
seen: list[int] = []
|
|
335
|
+
httpx_mock.add_response(
|
|
336
|
+
url=f"{API}/a",
|
|
337
|
+
json={},
|
|
338
|
+
headers={"X-RateLimit-Limit": "5000", "X-RateLimit-Remaining": "4000"},
|
|
339
|
+
)
|
|
340
|
+
httpx_mock.add_response(
|
|
341
|
+
url=f"{API}/b",
|
|
342
|
+
json={},
|
|
343
|
+
headers={"X-RateLimit-Limit": "5000", "X-RateLimit-Remaining": "3999"},
|
|
344
|
+
)
|
|
345
|
+
c = _client(on_rate_limit=lambda rl: seen.append(rl.remaining))
|
|
346
|
+
c.get("/a")
|
|
347
|
+
c.get("/b")
|
|
348
|
+
assert seen == [4000, 3999]
|
|
349
|
+
|
|
350
|
+
|
|
333
351
|
# -- pagination / listings ---------------------------------------------------------
|
|
334
352
|
|
|
335
353
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/src/github_actions_ingester/store.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{github_actions_ingester-0.2.1 → github_actions_ingester-0.2.2}/tests/test_workflow_schedule.py
RENAMED
|
File without changes
|