Serverless, zero-cost GitOps ETL pipeline on GitHub Actions
Project description
Cronos — Serverless Multi-Source ETL Pipeline
Fully automated, serverless, zero-cost ETL. GitHub Actions runs daily at 00:00 UTC, scrapes every enabled source, validates and deduplicates the results, regenerates statistics and a dashboard, and commits everything back to the repository (GitOps). No servers, no database, no bills.
Live dashboard: https://farajzada.github.io/cronos/
Why Cronos?
- Pluggable sources — add a new site or API by writing one small class
(guide below). Ships with two reference
implementations: HTML scraping (
quotes) and an official JSON API (hackernews). - Idempotent by construction — every source declares a stable dedup key
(natural id or content hash); loads are append-only with O(1)
set()membership checks. Run it 1× or 100×, the dataset is the same. - Corruption cannot ship — an integrity validator (schema, key uniqueness, per-source rules like content-hash verification) gates the GitOps commit.
- Deterministic outputs — stats and dashboard are derived purely from data (no timestamps), so unchanged data means byte-identical files and a silent no-op run.
- A real frontend for free — self-contained dashboard (tabs per source, SVG charts, facet filters, live search, light/dark theme, CSV/JSON export) served by GitHub Pages, regenerated on every data change.
Architecture
┌────────────────┐ cron: 0 0 * * * ┌──────────────────────────────┐
│ GitHub Actions │────────────────────▶│ ubuntu-latest + Python 3.11 │
│ (scheduler) │ │ pip cache (actions/cache) │
└────────────────┘ └──────────────┬───────────────┘
│
┌─────────────────────────────────────────────▼─────────────┐
│ cronos run Source registry → scrape each source │
│ → append-only dedup load (data/<src>.csv)│
│ cronos validate schema + key uniqueness + source rules │
│ cronos stats data/stats.json + Actions job summary │
│ cronos report docs/index.html (self-contained SPA) │
└─────────────────────────────────────────────┬─────────────┘
│ git status --porcelain
┌───────────────▼───────────────┐
│ changed? → bot commit + push │
│ → Pages redeploys │
│ unchanged? → graceful exit 0 │
└───────────────────────────────┘
Bundled sources:
| Source | Type | Dataset | Dedup key |
|---|---|---|---|
quotes |
HTML pages | data/quotes.csv |
SHA-256 of text::author |
hackernews |
JSON API | data/hackernews.csv |
HN item id (first-seen snapshot) |
Quick start
Use as a template (recommended): click Use this template on GitHub to get your own data-collecting repo in one click, then adapt the sources.
Install from PyPI:
pip install cronos-pipeline
cronos all # run → validate → stats → report (in your working dir)
Develop from source:
git clone https://github.com/farajzada/cronos.git && cd cronos
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
cronos all # run → validate → stats → report
cronos sources # list available/enabled sources
python -m http.server -d docs 8000 # view the dashboard locally
Every stage is also runnable as a module (python -m cronos.scraper, etc.) —
that is exactly what the GitHub Actions workflow does.
The second consecutive cronos run appends 0 rows — that is the
idempotency guarantee working, not a bug.
Configuration
Everything is environment-variable driven (see cronos/config.py):
| Variable | Default | Purpose |
|---|---|---|
CRONOS_SOURCES |
quotes,hackernews |
Enabled sources (comma-sep) |
CRONOS_DATA_DIR |
data/ |
Dataset directory |
CRONOS_QUOTES_URL |
https://quotes.toscrape.com/ |
Quotes target |
CRONOS_HN_URL |
Algolia front-page endpoint | Hacker News API |
CRONOS_MAX_PAGES |
50 |
Pagination ceiling |
CRONOS_CONNECT_TIMEOUT |
5.0 |
Connect timeout (s) |
CRONOS_READ_TIMEOUT |
20.0 |
Read timeout (s) |
CRONOS_MAX_RETRIES |
3 |
Attempts per request |
CRONOS_RETRY_BACKOFF |
2.0 |
Backoff base (s, linear) |
CRONOS_POLITENESS_DELAY |
0.5 |
Delay between pages (s) |
Adding your own source
The pipeline is generic over the Source contract — scraper, validator,
metrics and dashboard all adapt automatically:
# cronos/sources/mysite.py
from cronos.sources.base import Source
class MySiteSource(Source):
name = "mysite" # → data/mysite.csv
title = "My Site" # dashboard tab label
fieldnames = ["post_id", "title", "url", "category"]
key_field = "post_id" # stable dedup key
display_columns = [("title", "Title"), ("category", "Category")]
stat_fields = [("category", None)] # aggregated in stats.json
facet_field = "category" # filter chips + bar chart
def scrape(self, client):
payload = client.get_json("https://mysite.example/api/posts")
for post in payload["posts"]:
yield {
"post_id": str(post["id"]),
"title": post["title"],
"url": post["url"],
"category": post["category"],
}
Then register it in cronos/sources/__init__.py and
enable it: CRONOS_SOURCES=quotes,hackernews,mysite. Full checklist in
CONTRIBUTING.md.
Respect the target's robots.txt and terms of service — prefer official
APIs, keep the politeness delay and retry backoff.
Project layout
cronos/
├── .github/workflows/
│ ├── cronos_pipeline.yml # daily ETL: cron + cache + GitOps commit
│ └── ci.yml # ruff lint/format + pytest on every push
├── data/ # append-only datasets + stats.json
├── docs/index.html # self-contained dashboard (GitHub Pages)
├── cronos/
│ ├── cli.py # `cronos` command
│ ├── config.py # env-overridable configuration
│ ├── http_client.py # retries, timeouts, UA rotation
│ ├── storage.py # generic idempotent CSV writer
│ ├── scraper.py / validator.py / metrics.py / report.py
│ └── sources/ # Source contract + implementations
└── tests/ # 37 unit tests, network fully mocked
Operational guarantees
- Idempotent: stable dedup keys; re-runs never duplicate rows.
- Append-only: history is never rewritten.
- Validated: corrupt data fails the run before it can be pushed.
- Fault-isolated: one failing source never blocks the others.
- Deterministic: unchanged data → byte-identical outputs → no commit churn.
- Race-safe & bounded: concurrency group, page ceilings, job timeouts.
- Zero-cost: public repo Actions minutes + GitHub Pages hosting.
Contributing & security
PRs welcome — see CONTRIBUTING.md. New data sources are the most valued contribution. Security reports: SECURITY.md.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cronos_pipeline-1.0.0.tar.gz.
File metadata
- Download URL: cronos_pipeline-1.0.0.tar.gz
- Upload date:
- Size: 28.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f088afbc8c23c67db403cc106149a052282a93b426fef194fb985081383b321c
|
|
| MD5 |
3cf4fa37eab49c420ab3c8b1d384d2a8
|
|
| BLAKE2b-256 |
c610068fcd8940777659208613fbaa01d80813d30546f3ba4abaf24418874d36
|
Provenance
The following attestation bundles were made for cronos_pipeline-1.0.0.tar.gz:
Publisher:
release.yml on farajzada/cronos
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cronos_pipeline-1.0.0.tar.gz -
Subject digest:
f088afbc8c23c67db403cc106149a052282a93b426fef194fb985081383b321c - Sigstore transparency entry: 2195107874
- Sigstore integration time:
-
Permalink:
farajzada/cronos@47abadd57efa4eaec94b37937f5d238c2fdfb0f3 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/farajzada
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@47abadd57efa4eaec94b37937f5d238c2fdfb0f3 -
Trigger Event:
release
-
Statement type:
File details
Details for the file cronos_pipeline-1.0.0-py3-none-any.whl.
File metadata
- Download URL: cronos_pipeline-1.0.0-py3-none-any.whl
- Upload date:
- Size: 25.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6376b7152778d518d33dff24d39fd194c6d942144d0a53573f02d8a8bcdb070b
|
|
| MD5 |
8360f9c09f91cbbce7d5739bd756fe2c
|
|
| BLAKE2b-256 |
1cffdbe716bda4d3d4749eaf1f6bf19ebee272e427e384d1559a398b32c25247
|
Provenance
The following attestation bundles were made for cronos_pipeline-1.0.0-py3-none-any.whl:
Publisher:
release.yml on farajzada/cronos
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cronos_pipeline-1.0.0-py3-none-any.whl -
Subject digest:
6376b7152778d518d33dff24d39fd194c6d942144d0a53573f02d8a8bcdb070b - Sigstore transparency entry: 2195107900
- Sigstore integration time:
-
Permalink:
farajzada/cronos@47abadd57efa4eaec94b37937f5d238c2fdfb0f3 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/farajzada
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@47abadd57efa4eaec94b37937f5d238c2fdfb0f3 -
Trigger Event:
release
-
Statement type: