Repository navigation
Design Notes
Language: English · فارسی
How pulsecheck is built, why, and what was learned while making it run on five different places. Useful if you want to contribute or debug.
| Module | Job | Used on |
|---|---|---|
config.py |
Dataclasses for every key, ${ENV} expansion, type checks, message templates |
everywhere |
result.py |
Result of one check, limit() for max_response_ms
|
everywhere |
state.py |
The up/down state machine (step), SQLite Store, JSON-document BlobStore
|
CLI / serverless |
engine.py |
One run: choose due sites, check them in parallel, decide alerts, save state, export telemetry; status()
|
everywhere |
otel.py |
OTLP/HTTP JSON payloads and the push | everywhere |
httpnotify.py |
shoutrrr-style URLs → plain HTTPS requests for 12 services | Vercel, Cloudflare |
checks.py |
HTTP (httpx), TCP, ping, TLS expiry | CLI, Vercel |
notify.py |
Runs shoutrrr (Docker image or binary), verifies URLs, downloads the binary (SHA-256 checked) | CLI, Vercel |
setup.py, cli.py
|
Commands, config discovery, .env, init, cron block, update
|
CLI |
web.py, app.py (repo root) |
Vercel: WSGI app, Upstash Redis state | Vercel |
deploy/cloudflare/ |
build.py (Build Output), src/entry.py (Worker), deploy.sh
|
Cloudflare |
engine.run(cfg, store, check, send, post=...) never touches the network, the disk or a clock it cannot be given. The caller passes:
-
check(site)→Result(CLI: threads running httpx/socket/ping; Cloudflare:fetch), -
send(urls, text)(CLI: shoutrrr; serverless: HTTP senders), -
post(url, headers, body)for OpenTelemetry, - a
store(SQLite file, or one JSON document in Redis / Workers KV).
That is why the same engine runs from cron, GitHub Actions, GitLab CI, Vercel and a Cloudflare Worker, and why most behaviour is tested without any network.
-
SQLite (
Store): every check is a row (kept 400 days) plus the current state per site. Gives exact p95. -
JSON document (
BlobStore): current state plus daily totals per site (checks, ok, latency sum), kept 90 days, so one small document is read and written per run. No p95. Overlapping runs can lose an update (last write wins); cron spacing makes that rare. - Per site the state holds
status, consecutivefails,since,lastcheck time and a smallmeta(when the last alert and the last certificate warning were sent, for reminders).
-
Alerts only on change.
step()is a pure function, unit tested; reminders and certificate warnings are layered on top in the engine. -
Dataclasses, not pydantic. The Cloudflare Worker must run with no third-party packages (the Pyodide package index was unreachable from some networks, and fewer packages means a faster cold start).
config.build()does the type checks and gives errors likeconfig.sites[0].timeout: expected a number. -
YAML only at the edges. The Worker receives JSON made at build time;
ruamel.yamlis imported lazily. -
One shoutrrr, two ways. The CLI uses the real shoutrrr. Where it cannot run (Cloudflare) or may not exist (Vercel) the built-in HTTP senders cover the common services, and Vercel falls back to downloading the real binary (SHA-256 verified against the release's
checksums.txt) into/tmp. -
Secrets stay out of output. Channels are printed as
telegram://***; the Vercel and Cloudflare endpoints compare tokens withhmac.compare_digest; the.envfile is created with mode 600; the Vercel cron endpoint refuses everything whenCRON_SECRETis unset. -
initis idempotent. It never overwrites an existing config, and the cron job lives in one marked block that is replaced, never duplicated.
| Platform | Finding | Consequence |
|---|---|---|
Cloudflare cf CLI |
Beta; its Python dev server package is not published, so it cannot build Python Workers | We write the Build Output ourselves and run cf deploy --prebuilt
|
| Python Worker | Only the main module is importable (others and data files are not mounted) |
build.py embeds the package into one entry.py and registers pulsecheck.* modules at import time |
| Python Worker |
scheduled(self, controller, env, ctx) receives env=None
|
Use self.env
|
| Python Worker | A module-level CONFIG_TEXT = None after the embedded value silently erased it |
The entry uses globals().get(...); a test builds the bundle and runs it |
| Cloudflare ntfy.sh | Returned 429 to the Worker's shared egress | Alert failures are logged and never stop the run |
| Vercel build |
.python-version 3.15 fails (No interpreter found) |
build.env.UV_PYTHON=3.14 in vercel.json
|
| Vercel runtime | Functions ran Python 3.12.14, no ping, no Docker, /tmp writable, outbound HTTPS and running a downloaded binary work |
Download shoutrrr on demand |
| Vercel deploy | A 20 MB binary in the upload made the deploy hang | Never ship binaries; download at run time |
| Upstash on Vercel | The account owner must accept Upstash's terms once in the browser; after that vercel integration add upstash/upstash-kv provisions Redis and injects KV_REST_API_URL / KV_REST_API_TOKEN
|
Documented; tested with a real database |
| Test pitfalls |
pkill -f matched its own command line and killed the shell; an empty __init__.py is dropped from some uploads; a ping to 192.0.2.1 can succeed behind some networks |
Avoided in tests and scripts |
- Automated (28 tests, CI on Python 3.12–3.15): state machine, engine flows (down/up/degraded/reminder/certificate/maintenance/tags/interval), config rules, HTTP senders, OpenTelemetry payloads,
.env, cron block,init, update command choice, shoutrrr download checksum, the Vercel handler against a fake Redis, the Worker and the generated Worker bundle against fake runtime modules. - By hand on real services: the release pipeline (version, tag, GitHub release, PyPI upload,
uv tool install pulsecheckcli); a Cloudflare Worker (deploy, cron run, KV state, webhook delivery, logs) and a Vercel function with Upstash Redis (deploy, auth, config load, state kept between runs,/api/status, running shoutrrr). -
Not tested for real: the Worker's
/statusand/runendpoints;crontabon a real system (a fake was used); Telegram/Slack/etc. delivery from the built-in HTTP senders (only payload shape andshoutrrr verifyof the URLs); PyPI publishing (needs the first release).
PyPI project pulsecheckcli; the commands are pulsecheck and the alias pck. Releases are automatic and driven by Conventional Commits (feat → minor, fix/perf → patch). After CI passes on main, publish.yml runs python-semantic-release: it bumps version in pyproject.toml, updates CHANGELOG.md, tags vX.Y.Z, creates the GitHub release, then builds and uploads to PyPI with a Trusted Publisher (environment pypi, no token). There is no manual step; Run workflow can also force a bump level. The first release, 0.1.0, was published this way on 2026-10-11. See CONTRIBUTING.md in the repository.