Skip to content

Latest commit

 

History

History
219 lines (171 loc) · 9.15 KB

File metadata and controls

219 lines (171 loc) · 9.15 KB

Contributing to cronstable

Thanks for working on cronstable! This document covers local development and, importantly, how releases are cut.

One routing note before anything else: security problems do not go in the issue tracker. SECURITY.md has the private reporting route and what to include.

Signing off your commits (DCO)

cronstable uses the Developer Certificate of Origin (DCO): a lightweight, sign-off-based alternative to a CLA. By signing off, you certify that you wrote the patch, or otherwise have the right to submit it under the project's license (the full text is in the DCO file).

Add a sign-off to each commit with -s:

git commit -s -m "Fix the thing"

That appends a trailer with the name and email from your git config:

Signed-off-by: Your Name <you@example.com>

CI (the dco job) checks that every commit in a pull request carries this trailer. Forgot it? Sign off the whole branch and force-push:

git rebase --signoff origin/develop
git push --force-with-lease

Development setup

cronstable targets Python 3.10+ (3.10, 3.11, 3.12, 3.13 and 3.14 are tested) and runs on Linux, macOS and Windows (the test suite runs on all three in CI, including Windows ARM64).

cronstable uses uv for a fast dev loop (tox also runs through uv via tox-uv, and uv can fetch the 3.10–3.14 interpreters the test matrix needs). With uv installed:

git clone https://github.com/ptweezy/cronstable
cd cronstable
uv venv                                         # create .venv (uv picks a suitable Python)
uv pip install -e ".[dev]"                      # editable install with the dev extra

Prefer stock tooling? The classic path still works unchanged:

python -m venv .venv && . .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -e ".[dev]"                         # or: pip install -r requirements_dev.txt

Note: all OS-specific behavior lives in cronstable/platform.py (default shell, default config location, unix-socket support, and shutdown-signal wiring). The POSIX-only user/group feature imports grp/pwd lazily and is rejected on Windows. mypy is pinned to the linux platform (it type-checks the POSIX API surface; the Windows branches are runtime-guarded), so type-checking is identical on every OS.

Running the checks

Everything CI runs is driven by tox:

tox            # all envs: py310, py311, py312, py313, py314, lint, mypy, bandit
tox -e lint    # ruff check + ruff format --check
tox -e mypy    # mypy
tox -e bandit  # bandit security lint (medium+ severity)
tox -e py      # pytest on the current interpreter

tox.ini declares requires = tox-uv, so tox provisions its environments and installs dependencies with uv automatically (much faster; behavior-identical). Force the legacy virtualenv+pip path with tox --runner virtualenv if you ever need to.

Performance benchmarks

CI benchmarks every commit against the latest release (startup time, schedule math at 100k-job scale, config parsing, state I/O, memory footprint, and more) and, on a release, fails the pipeline if a metric regresses past its declared limit. The release notes then carry a per-metric diff chart. Check your own changes locally with:

python benchmarks/bench.py --quick --json before.json
# make the change
python benchmarks/bench.py --quick --json after.json
python benchmarks/compare.py --baseline before.json --current after.json --md diff.md

To ship an intentional, measured regression, start a pushed commit's subject with [perf:accept] (subjects only, like the [release] marker). The full harness reference, including how to add a benchmark, is in benchmarks/README.md.

Releasing

Releases are automated by the single CI GitHub Actions pipeline (one workflow builds and tests everything on every commit and, on a release, publishes it). Version numbers come from git tags via setuptools_scm; you never edit a version by hand.

Cutting a release

A release happens when any commit in a push to main has a release marker at the start of its subject line (the first line of the commit message):

[release:minor] Add retry backoff to the HTTP reporter

It does not need to be the latest commit in the push, but only subject lines are scanned, and only a marker that begins the subject counts. Prose that mentions a marker in a commit body (or anywhere else in a subject) never triggers or escalates a release.

Valid markers (the bump level is optional; case is ignored):

Marker Bump 1.0.5 →
[release] minor 1.1.0
[release:major] major 2.0.0
[release:minor] minor 1.1.0
[release:patch] patch 1.0.6

If more than one commit in the push carries a marker, the latest such commit wins. (File contents like this document are never scanned; only commit subjects are.)

You can also release manually without a marker: Actions → release → Run workflow, then pick the bump level from the dropdown.

What the pipeline does

The same pipeline runs on every commit and PR; only the publish steps are gated behind the release check (the lone exception is the wiki job, which publishes documentation on every push to develop — see Editing the wiki). On a release it, in order:

  1. decides whether to release and at what level (the strict marker check, which only fires on a push to main or a manual dispatch);
  2. computes the next version from the latest X.Y.Z tag (refusing if that tag already exists);
  3. builds and tests everything in paralleltox (py310–py314, lint, mypy), the wheel + sdist, the self-contained PyInstaller binaries for Linux (amd64, arm64, i686, armv7, armv6, ppc64le, s390x and riscv64, glibc and musl), macOS (arm64 + amd64) and Windows (amd64 + arm64), each smoke-tested with --version, plus a build-only pass over every Docker image — all at the computed version. This whole matrix is the gate: a red anywhere (a failed test, a broken binary, or a broken Dockerfile) means no release;
  4. only once the entire gate is green, publishes the wheel + sdist to PyPI via Trusted Publishing (OIDC): there is no API token to manage or leak;
  5. after a successful publish, creates and pushes the X.Y.Z tag and a GitHub Release with the wheel, sdist, and all the binaries (cronstable-linux-{amd64,arm64,i686,armv7,ppc64le,s390x,riscv64}, their -musl variants plus cronstable-linux-armv6-musl, cronstable-macos-{arm64,amd64}, and cronstable-windows-{amd64,arm64}.exe) plus a single SHA256SUMS attached, then pushes the multi-arch container images and updates the Homebrew tap.

Because no file is committed back to this repo, a release never re-triggers the workflow. (Two jobs do push elsewhere — the Homebrew tap on a release, and the wiki on a develop commit — but both targets are separate repositories and a push to either raises no event here.) Because the tag is created after publishing, a failed publish leaves no orphan tag and a re-run cleanly retries the same version.

Container image

The official image is built and published by the single CI pipeline, from the top-level Dockerfile (and the per-distro docker/Dockerfile.*):

  • On every commit and PR it builds every image without pushing (the docker gate job), across their full published arch sets, so a broken Dockerfile fails CI before a release.
  • On a release, once the whole gate is green, the docker-push job builds and pushes each distro's multi-arch image, tagged <version> and :latest (the Debian base owns the bare tags; variants get a -<distro> suffix), to both ghcr.io/ptweezy/cronstable and docker.io/ptweezy/cronstable. GHCR authenticates with the built-in GITHUB_TOKEN; Docker Hub uses the DOCKERHUB_USERNAME and DOCKERHUB_TOKEN repository secrets (skipped if unset).

Build it locally the same way CI does (the version is read from git, or pass --build-arg VERSION=X.Y.Z):

docker build -t cronstable .
docker run --rm -v "$PWD/example/docker/cronstable.yaml:/etc/cronstable.d/cronstable.yaml:ro" cronstable

Editing the wiki

Edit wiki/ in this repo — not the wiki in the browser. The GitHub wiki is a published copy: every push to develop runs the pipeline's wiki job, which mirrors wiki/*.md onto it (one file per page, named as the page's URL: Web-Dashboard.md/wiki/Web-Dashboard).

The mirror is authoritative, so it deletes: a page created or edited from the wiki's web UI is reverted on the next push to develop. The job prints every add/modify/delete to the run log.

Pages link to each other with bare wiki links — [Installation](Installation) — which only resolve once published. Those links are expected to be dead when browsing wiki/*.md here; that is not a bug.