12 KiB
Contributing
Thank you for considering contributing to volumen — a small, file-based Markdown blog engine written in Python, with a built-in admin and a public JSON API.
Development Setup
Requirements
- Python ≥ 3.14 with the
venvmodule (the venv itself is managed byuv).- Fedora:
sudo dnf install python3.14 python3-virtualenv - Debian/Ubuntu:
sudo apt install python3.14 python3-venv python3-pip
- Fedora:
uv— the package manager. The project pins every dependency (including transitive ones) inuv.lock; install withcurl -LsSf https://astral.sh/uv/install.sh | sh(or via your distro).just— thejustfileis the source of truth for install / run / build / test / uninstall.- Linux, macOS, or FreeBSD.
Quick Start
git clone https://sourcedock.dev/petrbalvin/volumen.git
cd volumen
just install # uv sync
just test # uv run pytest
just build # ruff check + ruff format --check (zero warnings)
just dev # run against ./posts and ./config.toml on :9090
just dev serves the bundled sample posts and a local config.toml. The dev
admin password is admin (the hash lives in ./config.toml). Open
http://localhost:9090/admin/.
For a fresh serve (without --config), the first run writes a commented
template to /etc/volumen/config.toml; see docs/deployment.md
for the full production bootstrap (uv tool install volumen && sudo volumen init).
Running Tests
just test
The pytest suite (tests/) covers:
frontmatter—+++TOML block parse/serialise round-trips.post/store— the Post model, reading/writing posts and media.markdown— GFM-ish rendering (tables, fenced code,<figure>).config— defaults, overrides, template generation.password/users— scrypt hashing, roles, last-admin safeguards.server/admin— the JSON API and the admin (auth, CSRF, CRUD, uploads, roles) viahttpx.AsyncClientagainst the FastAPI app.
Linting
Ruff is the source of truth for style (settings in pyproject.toml,
[tool.ruff], [tool.ruff.lint]). The project enforces zero warnings:
just build # ruff check + ruff format --check
just fmt # ruff format + ruff check --fix (autocorrect)
just build is what CI runs; both steps must exit 0 with no diff in
--check mode.
Code Style
- Idiomatic Python 3.14+; let Ruff guide you. CI fails on any warning.
- 4-space indentation, double quotes, lines
≤ 100characters (set in[tool.ruff]). - Type hints on every public function signature;
from __future__ import annotationsat the top of every module. - Prefer the standard library. New runtime dependencies belong in
pyproject.toml'sdependenciesarray and should be discussed in an issue first. - Prefer
httpxfor HTTP tests; never start a real network server in tests. - Admin content is authored by trusted users; see
docs/security.mdfor the trust model before touching rendering or uploads.
Testing
- Put tests in
tests/test_*.py; usepytestfixtures (seetests/conftest.py) for the temp content directory and the FastAPI app. - Exercise the HTTP layer with
httpx.AsyncClientdriving the FastAPI app viaapp=in theAsyncClientconstructor. - Name tests
test_<thing>_<scenario>; cover both the happy and the error path.
Commit Conventions
We follow Conventional Commits.
| Type | Use |
|---|---|
feat |
New feature |
fix |
Bug fix |
refactor |
Restructuring without behaviour change |
docs |
Documentation |
test |
Adding or updating tests |
chore |
Maintenance, CI, tooling |
perf |
Performance improvement |
style |
Formatting / lint-only changes |
security |
Hardening without a user-visible fix |
feat: add author role with last-admin safeguards
fix: rename create_app to factory to avoid a FastAPI clash
refactor: extract frontmatter parsing into its own module
docs: document the dual-mode editor
test: cover GFM table and fenced code rendering
chore: pin Python dependencies to latest
- English, lowercase subject, imperative mood (
add, notadded). - No trailing period, max 72 characters.
- No version numbers in commit messages — versions belong to tags.
Pull Request Flow
- Create a feature branch from
development. - Make your changes with Conventional-Commits messages.
- Record your changes under the
## [development]section at the top ofCHANGELOG.md, using the Keep a Changelog categories (Added,Changed,Fixed,Removed,Security). - Ensure
just buildreports zero warnings andjust testpasses. - Add or update tests for your change.
- Update documentation if the public API, configuration, or behaviour
changes:
README.md— high-level overview.docs/configuration.md— config keys.docs/api-reference.md— JSON API behaviour.docs/architecture.md— structural changes.docs/security.md— anything touching auth, uploads, or rendering.
- Open a pull request against
development. Releases are cut by mergingdevelopmentintomainand pushing avX.Y.Ztag — see Cutting a release.
Project Structure
volumen/
├── exe/
│ └── volumen # thin shim: from volumen.cli import main
├── src/
│ └── volumen/
│ ├── __init__.py # VERSION + login-attempt limiter
│ ├── cli.py # argparse entry point: serve, hash-password, version
│ ├── config.py # tomllib loader + deep-merge defaults
│ ├── frontmatter.py # parse/serialise the +++ TOML frontmatter block
│ ├── markdown.py # python-markdown + pymdown-extensions
│ ├── post.py # Post model (metadata + body + rendered HTML)
│ ├── store.py # read/write posts and media on disk (mtime-cached)
│ ├── password.py # scrypt hashing/verification (hashlib + secrets)
│ ├── users.py # users.toml + admin/author roles
│ ├── feed_helpers.py # RSS / JSON Feed / sitemap generation
│ ├── api_routes.py # /api/volumen/* (FastAPI router)
│ ├── admin_routes.py # /admin/* + /media/* (FastAPI router)
│ ├── server.py # FastAPI app factory, middleware, dependencies
│ └── web/
│ ├── templates/ # Jinja2 templates (layout, login, list, form, settings)
│ └── static/ # static assets (volumen-logo.svg, volumen-icon.svg)
├── tests/ # pytest suite
├── docs/ # architecture, configuration, api-reference, deployment, security
- `.gitea/workflows/` # test.yml, release.yml, migration-guard.yml (Gitea Actions)
├── justfile # install, run, dev, lint, fmt, build, test, uninstall
├── pyproject.toml # PEP 621 metadata + dependencies + tool config
├── uv.lock # locked dependency graph
├── config.toml.example # bootstrap config template
├── CHANGELOG.md
└── LICENSE
Architecture Overview
graph TD
Browser[Browser / Vue SPA]
Nginx[nginx reverse proxy]
App[FastAPI app -- Uvicorn]
ApiR[api_routes.py]
AdminR[admin_routes.py + CSRF + CSP]
Store[Store]
Markdown[Markdown renderer -- python-markdown]
Users[Users]
Posts[(content_dir/*.md)]
UsersFile[(users.toml)]
Browser -->|GET /api/volumen/*| Nginx
Browser -->|/admin| Nginx
Nginx --> App
App --> ApiR
App --> AdminR
ApiR --> Store
ApiR --> Markdown
AdminR --> Store
AdminR --> Users
Store --> Posts
Users --> UsersFile
Key Design Decisions
- File-based, no database — every post is a
.mdfile with TOML frontmatter; users live in ausers.toml. Everything is plain text, safe to commit and back up. - Headless for the front-end — volumen serves a read-only JSON API and its own server-rendered admin; the public site is a separate consumer.
- Standard library first — TOML parsing uses
tomllib(3.11+), scrypt useshashlib.scrypt, constant-time compares usesecrets.compare_digest. No runtime TOML or crypto dependency is required. - Trusted authors — python-markdown passes raw HTML through on
purpose; content is only as safe as who can log in (see
docs/security.md). - Roles, server-enforced —
adminmanages users,authormanages posts and their own account;require_admin(a FastAPI dependency) gates user management on the server, with last-admin safeguards. - IPv6-first — the default bind address is
::; bind to::1when behind nginx.
CI
volumen uses Gitea Actions; the workflows live under
.gitea/workflows/ and run on the fedora:latest runner.
Test — .gitea/workflows/test.yml
Triggered on push and pull requests targeting development.
| Step | What it does |
|---|---|
| Set up Python and uv | astral-sh/setup-uv with Python 3.14 |
| Install dependencies | uv sync --frozen --group dev |
| Lint | uv run ruff check src/ tests/ |
| Format | uv run ruff format --check src/ tests/ |
| Tests | uv run pytest |
Run the same locally before opening a PR:
just build
just test
Release — .gitea/workflows/release.yml
Triggered by pushing a v* tag. It verifies the tag matches the version
in pyproject.toml, runs the quality gate, builds the wheel and sdist
with python -m build, extracts the matching CHANGELOG.md section,
creates a Gitea release via the REST API, uploads both artefacts, and
publishes them to PyPI via uv publish.
The auto-generated ${{ secrets.GITEA_TOKEN }} is used to create the release
(via the REST API). Only one secret is required: a PYPI_TOKEN
(project-scoped upload token from
https://pypi.org/manage/account/publishing/).
Migration guard — .gitea/workflows/migration-guard.yml
A grep guard that fails the build if any of the Ruby-only strings
(bundle exec, RuboCop, Sinatra, Puma, kramdown, gem build,
Volumen::, Rack::Session::Cookie, OpenSSL::KDF, lib/volumen/)
appear in tracked files other than the explicitly allow-listed
CHANGELOG.md and the guard workflow itself.
Cutting a release
- Bump
versionunder[project]inpyproject.toml. - Rename
## [development]to## [X.Y.Z] — YYYY-MM-DDat the top ofCHANGELOG.md, and add a fresh empty## [development]section above it. - Ensure CI is green on
development. - Merge
developmentintomain(the only timemainchanges outside a tag). - Tag and push:
git tag -a vX.Y.Z -m "volumen vX.Y.Z" git push origin main git push origin vX.Y.Z - The release workflow builds the wheel + sdist, publishes them to
sourcedock.dev Releases and to PyPI (
uv publish --token "$PYPI_TOKEN").
PyPI publishing token
A PyPI API token must be configured in
Settings → Secrets and Variables → Actions of the sourcedock.dev repository
under the name PYPI_TOKEN. Create a project-scoped token at
https://pypi.org/manage/account/publishing/ (scoped to volumen)
rather than a global account token. Rotate the token at least yearly.
The token must have the upload scope for the project.
For local dry-runs and ad-hoc uploads, use the same token via the
UV_PUBLISH_TOKEN env var or uv publish --token "$PYPI_TOKEN". Never
commit it.
Report a Bug
Open an issue at https://sourcedock.dev/petrbalvin/volumen/issues
- volumen version (
uv run volumen version). - Operating system and architecture.
- Exact steps (request or admin action) that reproduce it.
- Expected vs actual behaviour, and the relevant slice of
journalctl -u volumen.
For security issues, please email opensource@petrbalvin.org rather than opening a public issue.