github-actions-ingester 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- github_actions_ingester/__init__.py +9 -0
- github_actions_ingester/__main__.py +330 -0
- github_actions_ingester/app_manifest.py +97 -0
- github_actions_ingester/collector.py +338 -0
- github_actions_ingester/config.py +290 -0
- github_actions_ingester/github.py +585 -0
- github_actions_ingester/metrics.py +184 -0
- github_actions_ingester/migrations/0001_initial.sql +140 -0
- github_actions_ingester/ratelimit.py +39 -0
- github_actions_ingester/server.py +115 -0
- github_actions_ingester/store.py +580 -0
- github_actions_ingester/workflow_schedule.py +83 -0
- github_actions_ingester-0.1.0.dist-info/METADATA +522 -0
- github_actions_ingester-0.1.0.dist-info/RECORD +17 -0
- github_actions_ingester-0.1.0.dist-info/WHEEL +4 -0
- github_actions_ingester-0.1.0.dist-info/entry_points.txt +2 -0
- github_actions_ingester-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: github-actions-ingester
|
|
3
|
+
Version: 0.1.0
|
|
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
|
+
Project-URL: Homepage, https://github.com/danielgines/github-actions-ingester
|
|
6
|
+
Project-URL: Repository, https://github.com/danielgines/github-actions-ingester
|
|
7
|
+
Project-URL: Issues, https://github.com/danielgines/github-actions-ingester/issues
|
|
8
|
+
Author: Daniel Gines
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Daniel Gines
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: ci,finops,github,github-actions,grafana,observability,postgres,prometheus
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Intended Audience :: System Administrators
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
40
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
41
|
+
Classifier: Topic :: System :: Monitoring
|
|
42
|
+
Requires-Python: >=3.11
|
|
43
|
+
Requires-Dist: croniter>=3.0
|
|
44
|
+
Requires-Dist: httpx>=0.27
|
|
45
|
+
Requires-Dist: prometheus-client>=0.20
|
|
46
|
+
Requires-Dist: psycopg[binary]>=3.2
|
|
47
|
+
Requires-Dist: pydantic-settings>=2.2
|
|
48
|
+
Requires-Dist: pydantic>=2.6
|
|
49
|
+
Requires-Dist: pyjwt[crypto]>=2.8
|
|
50
|
+
Requires-Dist: pyyaml>=6.0
|
|
51
|
+
Requires-Dist: structlog>=24.1
|
|
52
|
+
Provides-Extra: dev
|
|
53
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
54
|
+
Requires-Dist: pgserver>=0.1.4; (python_version < '3.13') and extra == 'dev'
|
|
55
|
+
Requires-Dist: pre-commit>=3.7; extra == 'dev'
|
|
56
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
57
|
+
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
|
|
58
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
59
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
60
|
+
Requires-Dist: types-croniter>=3.0; extra == 'dev'
|
|
61
|
+
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
|
|
62
|
+
Description-Content-Type: text/markdown
|
|
63
|
+
|
|
64
|
+
# github-actions-ingester
|
|
65
|
+
|
|
66
|
+
[](https://pypi.org/project/github-actions-ingester/)
|
|
67
|
+
[](https://github.com/danielgines/github-actions-ingester/pkgs/container/github-actions-ingester)
|
|
68
|
+
[](https://artifacthub.io/packages/search?repo=github-actions-ingester)
|
|
69
|
+
[](LICENSE)
|
|
70
|
+
|
|
71
|
+
Standalone ingester for **GitHub Actions**: a long-running service that pulls
|
|
72
|
+
repositories, workflows, workflow runs and jobs from the GitHub REST API into
|
|
73
|
+
**PostgreSQL**, and exposes **Prometheus metrics** about the ingestion itself
|
|
74
|
+
and about the liveness of your scheduled workflows.
|
|
75
|
+
|
|
76
|
+
- **PostgreSQL** holds the history. Query it from Grafana with the bundled
|
|
77
|
+
dashboard (a drop-in for the community dashboard
|
|
78
|
+
[24157 "GitHub Actions insights"](https://grafana.com/grafana/dashboards/24157)),
|
|
79
|
+
from SQL for FinOps (minutes per repository, per runner, per event),
|
|
80
|
+
or from anything else that speaks SQL.
|
|
81
|
+
|
|
82
|
+
- **Prometheus** (`/metrics`) answers the questions that need an alert
|
|
83
|
+
rather than a chart: is the ingester alive and current, is the GitHub
|
|
84
|
+
rate limit about to run out, and *did the nightly job actually run
|
|
85
|
+
last night* (per workflow, using the `on.schedule` cron read from the
|
|
86
|
+
workflow file).
|
|
87
|
+
|
|
88
|
+
## Why
|
|
89
|
+
|
|
90
|
+
GitHub keeps 90 days of run history (less on some plans), the UI shows one
|
|
91
|
+
repository at a time, and the API caps a listing at 1 000 runs. Getting
|
|
92
|
+
"how long does CI take across the organization, and how much of it is
|
|
93
|
+
retries" out of that means paging the API on a schedule and keeping the
|
|
94
|
+
rows yourself. Existing solutions are either SaaS, tied to a specific
|
|
95
|
+
GitOps or dashboard product, or need a webhook endpoint reachable from
|
|
96
|
+
GitHub.
|
|
97
|
+
|
|
98
|
+
`github-actions-ingester` is the boring alternative: one container, one
|
|
99
|
+
PostgreSQL schema, one read-only GitHub App. No webhooks, no inbound
|
|
100
|
+
traffic, no state outside the database. Point Grafana's PostgreSQL
|
|
101
|
+
datasource at the schema and the dashboard works.
|
|
102
|
+
|
|
103
|
+
| Use case | Interface |
|
|
104
|
+
|---|---|
|
|
105
|
+
| "Show duration, success rate and queue time per workflow over the last quarter" | PostgreSQL + `examples/grafana-dashboard.json` |
|
|
106
|
+
| "Minutes consumed per repository / runner label this month" | PostgreSQL, one `GROUP BY` on `workflow_jobs` |
|
|
107
|
+
| "Page me when the nightly backup workflow did not run" | Prometheus rule on `gha_scheduled_workflow_last_run_timestamp_seconds` |
|
|
108
|
+
| "Page me when the ingester stopped or the API budget is nearly gone" | Prometheus rules on `gha_ingester_*` |
|
|
109
|
+
|
|
110
|
+
## How it works
|
|
111
|
+
|
|
112
|
+
The ingester runs a **cycle** every `GHA_POLL_INTERVAL_SECONDS` (default 5
|
|
113
|
+
minutes):
|
|
114
|
+
|
|
115
|
+
1. **Inventory** (refreshed every `GHA_REPO_REFRESH_SECONDS`): list the
|
|
116
|
+
repositories in scope (organizations and/or explicit `owner/name`,
|
|
117
|
+
minus exclusion globs and archived repositories) and the workflow
|
|
118
|
+
files of each one. When `GHA_SYNC_SCHEDULES=true` the workflow file is
|
|
119
|
+
read from the default branch and its `on.schedule` cron expressions
|
|
120
|
+
are stored, together with the *expected interval*: the longest gap
|
|
121
|
+
between two consecutive fires with all crons merged (for
|
|
122
|
+
`0 9 * * 1-5` that is 72 h, Friday to Monday, so the alert stays quiet
|
|
123
|
+
over the weekend).
|
|
124
|
+
2. **Runs**: for each repository, list the runs created since the
|
|
125
|
+
repository's cursor minus `GHA_LOOKBACK_MINUTES`. The first cycle of a
|
|
126
|
+
repository goes back `GHA_BACKFILL_DAYS`. Windows that would exceed
|
|
127
|
+
GitHub's 1 000-run cap are split in half recursively, so nothing is
|
|
128
|
+
lost on busy repositories.
|
|
129
|
+
3. **Jobs**: for every run that is new or changed since the last cycle,
|
|
130
|
+
fetch its jobs (`filter=all` to match GitHub billing, or `latest`).
|
|
131
|
+
Runs still open outside the lookback window (long queues, multi-hour
|
|
132
|
+
jobs) are refreshed individually, bounded by `GHA_MAX_OPEN_RUN_REFRESH`.
|
|
133
|
+
4. **Persist**: upserts inside one transaction per repository, then move
|
|
134
|
+
the cursor. `completed_at` of a run is derived from its jobs
|
|
135
|
+
(`MAX(completed_at)`) or from `updated_at` once the run is completed.
|
|
136
|
+
5. **Publish**: gauges for the inventory, counters for what was written,
|
|
137
|
+
the rate-limit headers of the last response, and one series per
|
|
138
|
+
scheduled workflow with its last scheduled run and last conclusion.
|
|
139
|
+
|
|
140
|
+
Pacing is client-side (`GHA_API_RATE_LIMIT_RPS`, default 5) plus the
|
|
141
|
+
server's own signals: `403`/`429` with rate-limit markers honour
|
|
142
|
+
`Retry-After` / `X-RateLimit-Reset`, `5xx` and transport errors back off
|
|
143
|
+
exponentially, and when the primary budget drops under
|
|
144
|
+
`GHA_API_MIN_REMAINING` the cycle waits for the reset instead of spending
|
|
145
|
+
the last requests other tools may need.
|
|
146
|
+
|
|
147
|
+
### Schema lifecycle
|
|
148
|
+
|
|
149
|
+
On every start the ingester connects, creates the schema named by
|
|
150
|
+
`GHA_DATABASE_SCHEMA` if missing, and applies the migrations embedded in
|
|
151
|
+
the package that have not been applied yet (tracked in
|
|
152
|
+
`schema_migrations`, with a checksum). A fresh database is bootstrapped, an
|
|
153
|
+
upgrade applies only what is new, and a start on a current schema does
|
|
154
|
+
nothing. `github-actions-ingester migrate` does the same and exits, for
|
|
155
|
+
init containers or pipelines that want the schema in place before the
|
|
156
|
+
service starts.
|
|
157
|
+
|
|
158
|
+
### Tables and views
|
|
159
|
+
|
|
160
|
+
| Object | Content |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `repositories` | id, owner, name, default branch, archived, first/last seen |
|
|
163
|
+
| `workflows` | one row per workflow file: name, path, state, `schedules[]`, `schedule_interval_seconds` |
|
|
164
|
+
| `workflow_runs` | one row per run: event, status, conclusion, branch, actor, `created_at`, `run_started_at`, `completed_at`, attempt |
|
|
165
|
+
| `workflow_jobs` | one row per job: runner name / group / labels, `started_at`, `completed_at`, steps |
|
|
166
|
+
| `ingest_cursors` | per-repository resume point |
|
|
167
|
+
| `schema_migrations` | applied migrations, with checksum |
|
|
168
|
+
| `minion_repositories`, `minion_workflow_files`, `minion_workflow_runs`, `minion_workflow_jobs` | views with the column contract of dashboard 24157 |
|
|
169
|
+
|
|
170
|
+
Everything lives in the configured schema; the role needs `CREATE` on the
|
|
171
|
+
database for the first start and plain read/write afterwards. Give
|
|
172
|
+
Grafana a separate read-only role.
|
|
173
|
+
|
|
174
|
+
## Quick start
|
|
175
|
+
|
|
176
|
+
### Docker
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
docker run --rm -p 9619:9619 \
|
|
180
|
+
-e GHA_GITHUB_TOKEN=$YOUR_TOKEN \
|
|
181
|
+
-e GHA_ORGS=my-org \
|
|
182
|
+
-e GHA_DATABASE_URL=postgresql://gha:gha@db.example:5432/gha \
|
|
183
|
+
ghcr.io/danielgines/github-actions-ingester:latest
|
|
184
|
+
|
|
185
|
+
curl http://localhost:9619/metrics
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Docker Compose (PostgreSQL + ingester + Grafana with the dashboard)
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
cd examples
|
|
192
|
+
GHA_GITHUB_TOKEN=$YOUR_TOKEN GHA_ORGS=my-org docker compose up -d
|
|
193
|
+
# Grafana on :3000 (admin/admin) → "GitHub Actions insights"
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### pip
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
pip install github-actions-ingester
|
|
200
|
+
GHA_GITHUB_TOKEN=$YOUR_TOKEN GHA_ORGS=my-org \
|
|
201
|
+
GHA_DATABASE_URL=postgresql://gha:gha@localhost:5432/gha \
|
|
202
|
+
github-actions-ingester run
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Helm (Kubernetes)
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
helm install github-actions-ingester \
|
|
209
|
+
oci://ghcr.io/danielgines/charts/github-actions-ingester \
|
|
210
|
+
--namespace github-actions-ingester --create-namespace \
|
|
211
|
+
--set auth.token=$YOUR_TOKEN \
|
|
212
|
+
--set database.url=postgresql://gha:gha@postgres.db.svc:5432/gha \
|
|
213
|
+
--set config.orgs=my-org \
|
|
214
|
+
--set serviceMonitor.enabled=true \
|
|
215
|
+
--set prometheusRule.enabled=true
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
For production, keep credentials out of values: reference existing
|
|
219
|
+
Secrets (fed by External Secrets Operator, CloudNativePG, sealed-secrets,
|
|
220
|
+
whatever you already run):
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
helm install github-actions-ingester \
|
|
224
|
+
oci://ghcr.io/danielgines/charts/github-actions-ingester \
|
|
225
|
+
--set auth.existingSecret=github-actions-ingester-github \
|
|
226
|
+
--set database.existingSecret=gha-db-app \
|
|
227
|
+
--set database.existingSecretKey=uri \
|
|
228
|
+
--set config.orgs=my-org
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Full chart reference in [`helm/github-actions-ingester/README.md`](helm/github-actions-ingester/README.md).
|
|
232
|
+
|
|
233
|
+
## GitHub credential
|
|
234
|
+
|
|
235
|
+
Two shapes are accepted; set exactly one.
|
|
236
|
+
|
|
237
|
+
**GitHub App (recommended).** Least privilege, no human account in the
|
|
238
|
+
loop, tokens rotate hourly on their own. Create the App inside your own
|
|
239
|
+
organization in one click from the manifest page in
|
|
240
|
+
[`examples/github-app/`](examples/github-app/README.md) (GitHub generates
|
|
241
|
+
the private key and hands it only to you), install it on the repositories
|
|
242
|
+
you want, and set:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
GHA_GITHUB_APP_ID=123456
|
|
246
|
+
GHA_GITHUB_APP_PRIVATE_KEY_FILE=/secrets/github-app.pem # or GHA_GITHUB_APP_PRIVATE_KEY inline
|
|
247
|
+
GHA_ORGS=my-org
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Permissions: **Actions: read**, **Metadata: read**, and **Contents: read**
|
|
251
|
+
(only for `on.schedule` discovery; set `GHA_SYNC_SCHEDULES=false` to drop
|
|
252
|
+
it). The installation is discovered automatically.
|
|
253
|
+
|
|
254
|
+
**Personal access token.** Fine for a first look. A fine-grained token
|
|
255
|
+
needs *Actions: read* and *Metadata: read* (plus *Contents: read* for
|
|
256
|
+
schedules) on the repositories in scope; a classic token needs `repo`
|
|
257
|
+
for private repositories.
|
|
258
|
+
|
|
259
|
+
`github-actions-ingester check` validates the credential, lists what it
|
|
260
|
+
can see and prints the remaining rate limit before you commit to a
|
|
261
|
+
backfill.
|
|
262
|
+
|
|
263
|
+
## Configuration
|
|
264
|
+
|
|
265
|
+
All settings are env vars prefixed `GHA_` (a `.env` in the working
|
|
266
|
+
directory is read too; see [`.env.example`](.env.example)).
|
|
267
|
+
|
|
268
|
+
| Variable | Default | Description |
|
|
269
|
+
|---|---|---|
|
|
270
|
+
| `GHA_GITHUB_TOKEN` | `""` | Personal access token. Mutually exclusive with the App settings |
|
|
271
|
+
| `GHA_GITHUB_APP_ID` | `""` | GitHub App ID (or client ID) |
|
|
272
|
+
| `GHA_GITHUB_APP_PRIVATE_KEY` | `""` | PEM private key of the App; literal newlines or `\n` escapes |
|
|
273
|
+
| `GHA_GITHUB_APP_PRIVATE_KEY_FILE` | `""` | Path to the PEM file (wins over the inline value) |
|
|
274
|
+
| `GHA_GITHUB_APP_INSTALLATION_ID` | `""` | Installation to use; auto-discovered when empty |
|
|
275
|
+
| `GHA_GITHUB_API_BASE` | `https://api.github.com` | `https://ghe.example.com/api/v3` for GitHub Enterprise Server |
|
|
276
|
+
| `GHA_ORGS` | `""` | Comma-separated organizations (or user accounts) to ingest |
|
|
277
|
+
| `GHA_REPOS` | `""` | Comma-separated explicit `owner/name`, union with `GHA_ORGS` |
|
|
278
|
+
| `GHA_EXCLUDE_REPOS` | `""` | Comma-separated globs on `owner/name`, case-insensitive |
|
|
279
|
+
| `GHA_INCLUDE_ARCHIVED` | `false` | Also ingest archived repositories |
|
|
280
|
+
| `GHA_DATABASE_URL` | _required_ | libpq URL, e.g. `postgresql://user:pass@host:5432/db?sslmode=require` |
|
|
281
|
+
| `GHA_DATABASE_SCHEMA` | `gha` | Schema holding every table; created on first start |
|
|
282
|
+
| `GHA_DATABASE_CONNECT_TIMEOUT_SECONDS` | `10` | Connection timeout |
|
|
283
|
+
| `GHA_POLL_INTERVAL_SECONDS` | `300` | Seconds between cycles (min 30) |
|
|
284
|
+
| `GHA_BACKFILL_DAYS` | `30` | How far back the first cycle of a repository goes (1..3660) |
|
|
285
|
+
| `GHA_LOOKBACK_MINUTES` | `180` | Window before the cursor re-listed every cycle to catch status changes |
|
|
286
|
+
| `GHA_REPO_REFRESH_SECONDS` | `3600` | Repository / workflow inventory refresh (min 60) |
|
|
287
|
+
| `GHA_MAX_OPEN_RUN_REFRESH` | `200` | Per-cycle cap on `GET /runs/{id}` for stale open runs; 0 disables |
|
|
288
|
+
| `GHA_JOBS_FILTER` | `all` | `all` = jobs from every attempt (matches billing); `latest` = last attempt only |
|
|
289
|
+
| `GHA_SYNC_SCHEDULES` | `true` | Read workflow files for `on.schedule` (needs Contents: read) |
|
|
290
|
+
| `GHA_SCHEDULE_REFRESH_SECONDS` | `21600` | How often workflow files are re-read (min 300) |
|
|
291
|
+
| `GHA_API_RATE_LIMIT_RPS` | `5.0` | Client-side pacing toward the GitHub API |
|
|
292
|
+
| `GHA_API_MIN_REMAINING` | `200` | Pause until reset when the primary budget drops under this |
|
|
293
|
+
| `GHA_API_TIMEOUT_SECONDS` | `30` | Per-request timeout |
|
|
294
|
+
| `GHA_API_MAX_RETRIES` | `4` | Retries on 5xx / transport errors / secondary limits |
|
|
295
|
+
| `GHA_LISTEN_HOST` | `0.0.0.0` | HTTP bind address |
|
|
296
|
+
| `GHA_LISTEN_PORT` | `9619` | HTTP bind port |
|
|
297
|
+
| `GHA_LOG_LEVEL` | `info` | `debug` / `info` / `warning` / `error` |
|
|
298
|
+
| `GHA_LOG_FORMAT` | `json` | `json` (production) / `console` (dev) |
|
|
299
|
+
|
|
300
|
+
### API budget
|
|
301
|
+
|
|
302
|
+
A cycle costs roughly **1 request per repository** (the runs listing;
|
|
303
|
+
more only when the window holds more than 100 runs) **plus 1 per run
|
|
304
|
+
that changed** since the previous cycle (its jobs). The inventory adds
|
|
305
|
+
one request per organization page and one per repository (workflows),
|
|
306
|
+
once an hour by default, and schedule discovery one request per
|
|
307
|
+
workflow file every six hours. With 50 repositories and 500 runs a day,
|
|
308
|
+
that is well under 10 % of the 5 000 req/h an App installation gets.
|
|
309
|
+
The backfill is the expensive part: 30 days of a busy repository can be
|
|
310
|
+
a few thousand requests, spread across cycles by the pacing.
|
|
311
|
+
|
|
312
|
+
## CLI
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
github-actions-ingester run [--once] # the service (default). --once: one cycle and exit
|
|
316
|
+
github-actions-ingester migrate # bootstrap / upgrade the schema and exit
|
|
317
|
+
github-actions-ingester check # validate config, credential, scope and database
|
|
318
|
+
github-actions-ingester app-manifest # print the GitHub App manifest (--org, --redirect-url)
|
|
319
|
+
github-actions-ingester app-convert CODE --key-file github-app.pem
|
|
320
|
+
# exchange a manifest code for App ID + private key
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Exit codes: `0` ok, `1` runtime failure (`check` found a problem, `run
|
|
324
|
+
--once` could not bootstrap), `2` invalid configuration.
|
|
325
|
+
|
|
326
|
+
## Endpoints
|
|
327
|
+
|
|
328
|
+
| Path | Purpose |
|
|
329
|
+
|---|---|
|
|
330
|
+
| `/metrics` | Prometheus text format |
|
|
331
|
+
| `/healthz` | liveness: the HTTP server is up (`200 ok`) |
|
|
332
|
+
| `/readyz` | readiness: `503` until the schema is bootstrapped and the first cycle finished, then `200 ready` |
|
|
333
|
+
| `/` | index |
|
|
334
|
+
|
|
335
|
+
## Metrics
|
|
336
|
+
|
|
337
|
+
Ingester health (`gha_ingester_*`):
|
|
338
|
+
|
|
339
|
+
| Metric | Type | Labels | Description |
|
|
340
|
+
|---|---|---|---|
|
|
341
|
+
| `gha_ingester_build_info` | info | `version` | build information |
|
|
342
|
+
| `gha_ingester_up` | gauge | | 1 when the last cycle finished without a fatal error |
|
|
343
|
+
| `gha_ingester_ready` | gauge | | 1 once the schema is bootstrapped and the first cycle completed |
|
|
344
|
+
| `gha_ingester_cycles_total` | counter | `result` (`ok`/`partial`/`error`) | cycles by outcome |
|
|
345
|
+
| `gha_ingester_cycle_duration_seconds` | histogram | | wall-clock duration of a cycle |
|
|
346
|
+
| `gha_ingester_last_cycle_timestamp_seconds` | gauge | | end of the last cycle, any outcome |
|
|
347
|
+
| `gha_ingester_last_success_timestamp_seconds` | gauge | | end of the last successful cycle |
|
|
348
|
+
| `gha_ingester_errors_total` | counter | `stage` | errors by stage |
|
|
349
|
+
| `gha_ingester_github_requests_total` | counter | `status` | requests to the GitHub API by HTTP status |
|
|
350
|
+
| `gha_ingester_github_rate_limit_remaining` | gauge | | `X-RateLimit-Remaining` of the last response |
|
|
351
|
+
| `gha_ingester_github_rate_limit_limit` | gauge | | `X-RateLimit-Limit` |
|
|
352
|
+
| `gha_ingester_github_rate_limit_reset_timestamp_seconds` | gauge | | when the primary window resets |
|
|
353
|
+
| `gha_ingester_repositories` | gauge | | repositories in scope after exclusions |
|
|
354
|
+
| `gha_ingester_workflows` | gauge | | workflow files known |
|
|
355
|
+
| `gha_ingester_stored_runs` / `_stored_jobs` | gauge | | rows in the database |
|
|
356
|
+
| `gha_ingester_open_runs` | gauge | | stored runs not yet completed |
|
|
357
|
+
| `gha_ingester_runs_upserted_total` / `_jobs_upserted_total` | counter | `repository` | rows written |
|
|
358
|
+
| `gha_ingester_repository_cycle_duration_seconds` | histogram | | time per repository within a cycle |
|
|
359
|
+
|
|
360
|
+
Scheduled-workflow liveness (`gha_scheduled_*`, labels `repository`,
|
|
361
|
+
`workflow`, `workflow_name`):
|
|
362
|
+
|
|
363
|
+
| Metric | Description |
|
|
364
|
+
|---|---|
|
|
365
|
+
| `gha_scheduled_workflows` | active workflows declaring at least one cron |
|
|
366
|
+
| `gha_scheduled_workflow_interval_seconds` | expected interval: longest gap between consecutive fires, all crons merged |
|
|
367
|
+
| `gha_scheduled_workflow_last_run_timestamp_seconds` | most recent run triggered by `schedule` (0 = never observed) |
|
|
368
|
+
| `gha_scheduled_workflow_last_conclusion{conclusion}` | 1 for the conclusion of the last completed scheduled run |
|
|
369
|
+
|
|
370
|
+
## Grafana dashboard
|
|
371
|
+
|
|
372
|
+
[`examples/grafana-dashboard.json`](examples/grafana-dashboard.json) is
|
|
373
|
+
dashboard 24157 "GitHub Actions insights" made provisionable (no
|
|
374
|
+
`__inputs`, fixed uid `github-actions-insights`, datasource picked by the
|
|
375
|
+
`datasource` variable). It queries the `minion_*` views, so it works on
|
|
376
|
+
this ingester's schema unchanged: success rate, duration percentiles,
|
|
377
|
+
queue time, runs by event and conclusion, per-repository rows, and job
|
|
378
|
+
level detail, with an `aggregation` interval variable from 1h to 1M.
|
|
379
|
+
|
|
380
|
+
Provisioning files for both the PostgreSQL datasource and the dashboard
|
|
381
|
+
are in [`examples/grafana-provisioning/`](examples/grafana-provisioning/);
|
|
382
|
+
the Compose stack above mounts them. In Kubernetes, ship the JSON in a
|
|
383
|
+
ConfigMap with the label your Grafana sidecar watches.
|
|
384
|
+
|
|
385
|
+
Every panel is exercised in the test suite against a seeded database
|
|
386
|
+
(all repositories × every aggregation, single-repository rows, display
|
|
387
|
+
toggles), so a schema change that would break the dashboard fails CI.
|
|
388
|
+
|
|
389
|
+
## Sample alert rules
|
|
390
|
+
|
|
391
|
+
The chart renders these when `prometheusRule.enabled=true`; the raw
|
|
392
|
+
manifest is in [`examples/kubernetes/prometheusrule.yaml`](examples/kubernetes/prometheusrule.yaml).
|
|
393
|
+
|
|
394
|
+
```yaml
|
|
395
|
+
- alert: GitHubActionsIngesterDown
|
|
396
|
+
expr: gha_ingester_up == 0
|
|
397
|
+
for: 15m
|
|
398
|
+
- alert: GitHubActionsIngesterStale
|
|
399
|
+
expr: time() - gha_ingester_last_success_timestamp_seconds > 1800
|
|
400
|
+
for: 5m
|
|
401
|
+
- alert: GitHubActionsIngesterRateLimitLow
|
|
402
|
+
expr: gha_ingester_github_rate_limit_remaining < 300
|
|
403
|
+
for: 10m
|
|
404
|
+
- alert: GitHubScheduledWorkflowStopped
|
|
405
|
+
expr: |
|
|
406
|
+
(time() - gha_scheduled_workflow_last_run_timestamp_seconds)
|
|
407
|
+
> 2 * gha_scheduled_workflow_interval_seconds + 1800
|
|
408
|
+
and gha_scheduled_workflow_last_run_timestamp_seconds > 0
|
|
409
|
+
for: 10m
|
|
410
|
+
- alert: GitHubScheduledWorkflowNeverRan
|
|
411
|
+
expr: gha_scheduled_workflow_last_run_timestamp_seconds == 0 and gha_scheduled_workflow_interval_seconds > 0
|
|
412
|
+
for: 6h
|
|
413
|
+
- alert: GitHubScheduledWorkflowFailing
|
|
414
|
+
expr: gha_scheduled_workflow_last_conclusion{conclusion="failure"} == 1
|
|
415
|
+
for: 30m
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Limitations
|
|
419
|
+
|
|
420
|
+
- **Single writer.** One replica per schema. Two ingesters on the same
|
|
421
|
+
schema would double the API spend and fight over the cursors; the chart
|
|
422
|
+
pins `replicaCount: 1` and uses `Recreate`.
|
|
423
|
+
- **No webhooks, so latency is the poll interval.** A run appears at most
|
|
424
|
+
`GHA_POLL_INTERVAL_SECONDS` after it is created and its final status at
|
|
425
|
+
most one interval after it finishes.
|
|
426
|
+
- **History starts at the backfill.** GitHub only serves what it still
|
|
427
|
+
keeps (90 days on most plans); the ingester cannot recover older runs.
|
|
428
|
+
Raising `GHA_BACKFILL_DAYS` later only affects repositories without a
|
|
429
|
+
cursor.
|
|
430
|
+
- **Job-level billing is approximated.** Minutes are computed from
|
|
431
|
+
`started_at`/`completed_at`; GitHub rounds per job and applies OS
|
|
432
|
+
multipliers. Good enough to rank repositories, not to reconcile an
|
|
433
|
+
invoice.
|
|
434
|
+
- **Schedule discovery reads the default branch only.** A cron that exists
|
|
435
|
+
on another branch is not tracked.
|
|
436
|
+
- **Deleted runs stay.** GitHub does not report deletions; a run removed
|
|
437
|
+
from GitHub keeps its last known state in the database.
|
|
438
|
+
- **Requests are sequential.** The client issues one request at a time
|
|
439
|
+
and the jobs endpoint is called once per run, so the first cycle is
|
|
440
|
+
bound by round-trip latency rather than by `GHA_API_RATE_LIMIT_RPS`
|
|
441
|
+
(about 2 requests/s in practice, or roughly 8 minutes per 1000 runs).
|
|
442
|
+
Readiness turns green only after that first cycle, so `helm install
|
|
443
|
+
--wait` can time out on a large backfill while the pod is perfectly
|
|
444
|
+
fine; watch the logs for `cycle.done` instead, or raise `--timeout`.
|
|
445
|
+
|
|
446
|
+
## Development
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
git clone https://github.com/danielgines/github-actions-ingester.git
|
|
450
|
+
cd github-actions-ingester
|
|
451
|
+
|
|
452
|
+
just install # dev deps + pre-commit hooks
|
|
453
|
+
just ci # ruff + mypy + pytest + helm lint + helm template
|
|
454
|
+
just test # pytest only (integration tests start an embedded PostgreSQL)
|
|
455
|
+
just run-once # one cycle against the .env configuration
|
|
456
|
+
just compose-up # local stack: PostgreSQL + ingester + Grafana
|
|
457
|
+
just # all recipes
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Tests need no external database on Python 3.11/3.12: `pgserver` starts a
|
|
461
|
+
throwaway PostgreSQL in a temporary directory. On 3.13 (no `pgserver`
|
|
462
|
+
wheel yet) or to use an existing server, set `GHA_TEST_DATABASE_URL`;
|
|
463
|
+
integration tests are skipped when neither is available. GitHub traffic is mocked with `pytest-httpx`; the
|
|
464
|
+
CLI tests run `migrate`, `check`, `run --once` and the service mode
|
|
465
|
+
end-to-end against the mock API.
|
|
466
|
+
|
|
467
|
+
Pre-commit runs ruff, mypy, yamllint, hadolint, actionlint, gitleaks and
|
|
468
|
+
helm validation on every commit, and the full pytest suite on every push.
|
|
469
|
+
The same checks run in CI as `just ci`.
|
|
470
|
+
|
|
471
|
+
## Releasing
|
|
472
|
+
|
|
473
|
+
`pyproject.toml` is the canonical version source. `__init__.py` and
|
|
474
|
+
`Chart.yaml` (`version` + `appVersion`) are synced from it by
|
|
475
|
+
`scripts/sync-version.py`, enforced as a pre-commit hook.
|
|
476
|
+
|
|
477
|
+
1. Add a `## [X.Y.Z]` section to `CHANGELOG.md`
|
|
478
|
+
2. `just release X.Y.Z` (clean tree on `main`, bumps, syncs, commits
|
|
479
|
+
`chore(release): vX.Y.Z`)
|
|
480
|
+
3. `git push origin main`
|
|
481
|
+
4. CI succeeds → `release.yaml` builds the linux/amd64 + linux/arm64 image
|
|
482
|
+
with SBOM and provenance, pushes
|
|
483
|
+
`ghcr.io/<owner>/github-actions-ingester:X.Y.Z` (+ semver + `latest`),
|
|
484
|
+
Trivy-scans it (fails on unfixed CRITICAL), signs it with cosign
|
|
485
|
+
keyless, pushes the chart to
|
|
486
|
+
`oci://ghcr.io/<owner>/charts/github-actions-ingester`, publishes the
|
|
487
|
+
sdist and wheel to [PyPI](https://pypi.org/project/github-actions-ingester/),
|
|
488
|
+
tags `vX.Y.Z` and creates the GitHub Release.
|
|
489
|
+
|
|
490
|
+
No PAT and no PyPI token: everything happens inside one workflow run, and
|
|
491
|
+
PyPI accepts the upload through [trusted
|
|
492
|
+
publishing](https://docs.pypi.org/trusted-publishers/) (OIDC). The
|
|
493
|
+
one-time setup for a fork is to register the trusted publisher on
|
|
494
|
+
pypi.org with owner `<owner>`, repository `github-actions-ingester`,
|
|
495
|
+
workflow `release.yaml` and environment `pypi`; do it as a pending
|
|
496
|
+
publisher before the first release so the first upload claims the
|
|
497
|
+
project name. The release workflow also accepts `workflow_dispatch` with
|
|
498
|
+
a `version` input and `push: tags ["v*"]`.
|
|
499
|
+
|
|
500
|
+
```bash
|
|
501
|
+
cosign verify ghcr.io/<owner>/github-actions-ingester@<digest> \
|
|
502
|
+
--certificate-identity-regexp 'https://github\.com/<owner>/github-actions-ingester/.*' \
|
|
503
|
+
--certificate-oidc-issuer https://token.actions.githubusercontent.com
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### ArtifactHub indexing
|
|
507
|
+
|
|
508
|
+
ArtifactHub treats each chart as its own repository, so registration is
|
|
509
|
+
per chart with the full OCI URL:
|
|
510
|
+
|
|
511
|
+
1. Sign in at https://artifacthub.io with GitHub
|
|
512
|
+
2. Add a Helm OCI repository pointing to
|
|
513
|
+
`oci://ghcr.io/<owner>/charts/github-actions-ingester`
|
|
514
|
+
3. Copy the assigned `repositoryID` into `helm/artifacthub-repo.yml`
|
|
515
|
+
4. Commit, push, and run `just artifacthub-publish` (or the
|
|
516
|
+
`artifacthub-publish` workflow)
|
|
517
|
+
|
|
518
|
+
Every later chart release is indexed on ArtifactHub's next scrape.
|
|
519
|
+
|
|
520
|
+
## License
|
|
521
|
+
|
|
522
|
+
[MIT](LICENSE) — © 2026 Daniel Gines.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
github_actions_ingester/__init__.py,sha256=TTADWvGAm_J0wyW-3NQ3uoSyzhcWcuYDOczskoZkHrQ,317
|
|
2
|
+
github_actions_ingester/__main__.py,sha256=kW9kvgXtI90DxkN6s87khXHS_kxh9oiOYT-n8MJDLM4,11593
|
|
3
|
+
github_actions_ingester/app_manifest.py,sha256=ExHyJC-zNs_BYBkOtjOaFIZsTBPk455AId_AjW6WWyA,3177
|
|
4
|
+
github_actions_ingester/collector.py,sha256=BhypN2mzMdWe7afkLrHfxgfCFw-OlxoZzf8IUTvZY3c,13130
|
|
5
|
+
github_actions_ingester/config.py,sha256=1OQu8KO-pH6vCF0nyQPvZ1dZJNeM8-xXrCDtW_UpolY,11066
|
|
6
|
+
github_actions_ingester/github.py,sha256=iFbN4fMJ4xxy2sZdMelShVUvf1V7lg_ULLuIViref5o,21578
|
|
7
|
+
github_actions_ingester/metrics.py,sha256=OA4B-wEj2eVcmUMTuoo_zU5RUtwTjgp0mvG_ZP1DZ7I,7081
|
|
8
|
+
github_actions_ingester/ratelimit.py,sha256=PgnswOLrkZeXY4k099r7oU2tWwGyx3qvi9cYTLo6Tis,1298
|
|
9
|
+
github_actions_ingester/server.py,sha256=JGIHdxQ4mDqsqql0miTtEv-CTyzar9fPdEaHXV-Uefs,4114
|
|
10
|
+
github_actions_ingester/store.py,sha256=Q5qaNgHIKLuh88J4nRmDIS_vWc2O43_TTlG24Y0Lmi4,22775
|
|
11
|
+
github_actions_ingester/workflow_schedule.py,sha256=PCmObqYeKtyp6nP6Tdw0ZMXl9AMw1zRdQN0v1Xx-oeI,2656
|
|
12
|
+
github_actions_ingester/migrations/0001_initial.sql,sha256=PB81LItJ0XI7zQU1smIM-WYFKOMtp6SOSFwAQ9paYe8,5449
|
|
13
|
+
github_actions_ingester-0.1.0.dist-info/METADATA,sha256=z3e7G1JLd9Rq68IQULue5eiRoDrNAL2AjEb2OLiAfMU,25870
|
|
14
|
+
github_actions_ingester-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
15
|
+
github_actions_ingester-0.1.0.dist-info/entry_points.txt,sha256=E-0yhSxqnvJvds4wYcm6_RtCBgIsGDrRJMg-xnceKQ0,82
|
|
16
|
+
github_actions_ingester-0.1.0.dist-info/licenses/LICENSE,sha256=70ypun7z1FHfwr1u2UOzrV6FseZYS-td_v52LiOBOx8,1069
|
|
17
|
+
github_actions_ingester-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Gines
|
|
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.
|