Files
volumen/CONTRIBUTING.md
petrbalvin 3b8ed31f67
Test / test (push) Successful in 1m7s
Release / release (push) Successful in 1m11s
chore: prepare release v0.4.0
2026-07-26 18:15:29 +02:00

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 venv module (the venv itself is managed by uv).
    • Fedora: sudo dnf install python3.14 python3-virtualenv
    • Debian/Ubuntu: sudo apt install python3.14 python3-venv python3-pip
  • uv — the package manager. The project pins every dependency (including transitive ones) in uv.lock; install with curl -LsSf https://astral.sh/uv/install.sh | sh (or via your distro).
  • just — the justfile is 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) via httpx.AsyncClient against 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 ≤ 100 characters (set in [tool.ruff]).
  • Type hints on every public function signature; from __future__ import annotations at the top of every module.
  • Prefer the standard library. New runtime dependencies belong in pyproject.toml's dependencies array and should be discussed in an issue first.
  • Prefer httpx for HTTP tests; never start a real network server in tests.
  • Admin content is authored by trusted users; see docs/security.md for the trust model before touching rendering or uploads.

Testing

  • Put tests in tests/test_*.py; use pytest fixtures (see tests/conftest.py) for the temp content directory and the FastAPI app.
  • Exercise the HTTP layer with httpx.AsyncClient driving the FastAPI app via app= in the AsyncClient constructor.
  • 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, not added).
  • No trailing period, max 72 characters.
  • No version numbers in commit messages — versions belong to tags.

Pull Request Flow

  1. Create a feature branch from development.
  2. Make your changes with Conventional-Commits messages.
  3. Record your changes under the ## [development] section at the top of CHANGELOG.md, using the Keep a Changelog categories (Added, Changed, Fixed, Removed, Security).
  4. Ensure just build reports zero warnings and just test passes.
  5. Add or update tests for your change.
  6. 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.
  7. Open a pull request against development. Releases are cut by merging development into main and pushing a vX.Y.Z tag — 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 .md file with TOML frontmatter; users live in a users.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 uses hashlib.scrypt, constant-time compares use secrets.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-enforcedadmin manages users, author manages 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 ::1 when 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

  1. Bump version under [project] in pyproject.toml.
  2. Rename ## [development] to ## [X.Y.Z] — YYYY-MM-DD at the top of CHANGELOG.md, and add a fresh empty ## [development] section above it.
  3. Ensure CI is green on development.
  4. Merge development into main (the only time main changes outside a tag).
  5. Tag and push:
    git tag -a vX.Y.Z -m "volumen vX.Y.Z"
    git push origin main
    git push origin vX.Y.Z
    
  6. 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.