2026-06-21 13:15:06 +02:00
|
|
|
# Contributing
|
|
|
|
|
|
|
|
|
|
Thank you for considering contributing to **volumen** — a small, file-based
|
2026-07-26 18:15:29 +02:00
|
|
|
Markdown blog engine written in Python, with a built-in admin and a public
|
|
|
|
|
JSON API.
|
2026-06-21 13:15:06 +02:00
|
|
|
|
|
|
|
|
## Development Setup
|
|
|
|
|
|
|
|
|
|
### Requirements
|
|
|
|
|
|
2026-07-26 18:15:29 +02:00
|
|
|
- **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`](https://docs.astral.sh/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).
|
2026-06-21 13:15:06 +02:00
|
|
|
- [`just`](https://github.com/casey/just) — the `justfile` is the source of
|
|
|
|
|
truth for install / run / build / test / uninstall.
|
|
|
|
|
- Linux, macOS, or FreeBSD.
|
|
|
|
|
|
|
|
|
|
### Quick Start
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-07-26 18:15:29 +02:00
|
|
|
git clone https://sourcedock.dev/petrbalvin/volumen.git
|
2026-06-21 13:15:06 +02:00
|
|
|
cd volumen
|
2026-07-26 18:15:29 +02:00
|
|
|
just install # uv sync
|
|
|
|
|
just test # uv run pytest
|
|
|
|
|
just build # ruff check + ruff format --check (zero warnings)
|
2026-06-21 13:15:06 +02:00
|
|
|
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
|
2026-07-26 18:15:29 +02:00
|
|
|
template to `/etc/volumen/config.toml`; see [`docs/deployment.md`](docs/deployment.md)
|
|
|
|
|
for the full production bootstrap (`uv tool install volumen && sudo volumen
|
|
|
|
|
init`).
|
2026-06-21 13:15:06 +02:00
|
|
|
|
|
|
|
|
### Running Tests
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
just test
|
|
|
|
|
```
|
|
|
|
|
|
2026-07-26 18:15:29 +02:00
|
|
|
The pytest suite (`tests/`) covers:
|
2026-06-21 13:15:06 +02:00
|
|
|
|
|
|
|
|
- `frontmatter` — `+++` TOML block parse/serialise round-trips.
|
|
|
|
|
- `post` / `store` — the Post model, reading/writing posts and media.
|
2026-07-26 18:15:29 +02:00
|
|
|
- `markdown` — GFM-ish rendering (tables, fenced code, `<figure>`).
|
2026-06-21 13:15:06 +02:00
|
|
|
- `config` — defaults, overrides, template generation.
|
|
|
|
|
- `password` / `users` — scrypt hashing, roles, last-admin safeguards.
|
2026-07-26 18:15:29 +02:00
|
|
|
- `server` / `admin` — the JSON API and the admin (auth, CSRF, CRUD,
|
|
|
|
|
uploads, roles) via `httpx.AsyncClient` against the FastAPI app.
|
2026-06-21 13:15:06 +02:00
|
|
|
|
|
|
|
|
### Linting
|
|
|
|
|
|
2026-07-26 18:15:29 +02:00
|
|
|
Ruff is the source of truth for style (settings in `pyproject.toml`,
|
|
|
|
|
`[tool.ruff]`, `[tool.ruff.lint]`). The project enforces **zero warnings**:
|
2026-06-21 13:15:06 +02:00
|
|
|
|
|
|
|
|
```bash
|
2026-07-26 18:15:29 +02:00
|
|
|
just build # ruff check + ruff format --check
|
|
|
|
|
just fmt # ruff format + ruff check --fix (autocorrect)
|
2026-06-21 13:15:06 +02:00
|
|
|
```
|
|
|
|
|
|
2026-07-26 18:15:29 +02:00
|
|
|
`just build` is what CI runs; both steps must exit 0 with no diff in
|
|
|
|
|
`--check` mode.
|
|
|
|
|
|
2026-06-21 13:15:06 +02:00
|
|
|
## Code Style
|
|
|
|
|
|
2026-07-26 18:15:29 +02:00
|
|
|
- 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.
|
2026-06-21 13:15:06 +02:00
|
|
|
- Admin content is authored by trusted users; see [`docs/security.md`](docs/security.md)
|
|
|
|
|
for the trust model before touching rendering or uploads.
|
|
|
|
|
|
|
|
|
|
### Testing
|
|
|
|
|
|
2026-07-26 18:15:29 +02:00
|
|
|
- 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.
|
2026-06-21 13:15:06 +02:00
|
|
|
|
|
|
|
|
## Commit Conventions
|
|
|
|
|
|
|
|
|
|
We follow [Conventional Commits](https://www.conventionalcommits.org/).
|
|
|
|
|
|
|
|
|
|
| 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
|
2026-07-26 18:15:29 +02:00
|
|
|
fix: rename create_app to factory to avoid a FastAPI clash
|
2026-06-21 13:15:06 +02:00
|
|
|
refactor: extract frontmatter parsing into its own module
|
|
|
|
|
docs: document the dual-mode editor
|
|
|
|
|
test: cover GFM table and fenced code rendering
|
2026-07-26 18:15:29 +02:00
|
|
|
chore: pin Python dependencies to latest
|
2026-06-21 13:15:06 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- 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.
|
2026-06-25 21:37:27 +02:00
|
|
|
3. Record your changes under the `## [development]` section at the top of
|
|
|
|
|
`CHANGELOG.md`, using the Keep a Changelog categories (`Added`, `Changed`,
|
2026-07-26 18:15:29 +02:00
|
|
|
`Fixed`, `Removed`, `Security`).
|
|
|
|
|
4. Ensure `just build` reports zero warnings and `just test` passes.
|
2026-06-25 21:37:27 +02:00
|
|
|
5. Add or update tests for your change.
|
2026-07-26 18:15:29 +02:00
|
|
|
6. Update documentation if the public API, configuration, or behaviour
|
|
|
|
|
changes:
|
2026-06-21 13:15:06 +02:00
|
|
|
- `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.
|
2026-06-25 21:37:27 +02:00
|
|
|
7. Open a pull request against `development`. Releases are cut by merging
|
2026-06-21 13:15:06 +02:00
|
|
|
`development` into `main` and pushing a `vX.Y.Z` tag — see
|
|
|
|
|
[Cutting a release](#cutting-a-release).
|
|
|
|
|
|
|
|
|
|
## Project Structure
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
volumen/
|
|
|
|
|
├── exe/
|
2026-07-26 18:15:29 +02:00
|
|
|
│ └── volumen # thin shim: from volumen.cli import main
|
|
|
|
|
├── src/
|
2026-06-21 13:15:06 +02:00
|
|
|
│ └── volumen/
|
2026-07-26 18:15:29 +02:00
|
|
|
│ ├── __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
|
2026-06-21 13:15:06 +02:00
|
|
|
│ └── web/
|
2026-07-26 18:15:29 +02:00
|
|
|
│ ├── templates/ # Jinja2 templates (layout, login, list, form, settings)
|
|
|
|
|
│ └── static/ # static assets (volumen-logo.svg, volumen-icon.svg)
|
|
|
|
|
├── tests/ # pytest suite
|
2026-06-21 13:15:06 +02:00
|
|
|
├── docs/ # architecture, configuration, api-reference, deployment, security
|
2026-07-26 18:15:29 +02:00
|
|
|
- `.gitea/workflows/` # test.yml, release.yml, migration-guard.yml (Gitea Actions)
|
2026-07-12 14:03:14 +02:00
|
|
|
├── justfile # install, run, dev, lint, fmt, build, test, uninstall
|
2026-07-26 18:15:29 +02:00
|
|
|
├── pyproject.toml # PEP 621 metadata + dependencies + tool config
|
|
|
|
|
├── uv.lock # locked dependency graph
|
|
|
|
|
├── config.toml.example # bootstrap config template
|
2026-06-21 13:15:06 +02:00
|
|
|
├── CHANGELOG.md
|
|
|
|
|
└── LICENSE
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Architecture Overview
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
graph TD
|
|
|
|
|
Browser[Browser / Vue SPA]
|
|
|
|
|
Nginx[nginx reverse proxy]
|
2026-07-26 18:15:29 +02:00
|
|
|
App[FastAPI app -- Uvicorn]
|
|
|
|
|
ApiR[api_routes.py]
|
|
|
|
|
AdminR[admin_routes.py + CSRF + CSP]
|
|
|
|
|
Store[Store]
|
|
|
|
|
Markdown[Markdown renderer -- python-markdown]
|
|
|
|
|
Users[Users]
|
2026-06-21 13:15:06 +02:00
|
|
|
Posts[(content_dir/*.md)]
|
|
|
|
|
UsersFile[(users.toml)]
|
|
|
|
|
|
|
|
|
|
Browser -->|GET /api/volumen/*| Nginx
|
|
|
|
|
Browser -->|/admin| Nginx
|
2026-07-26 18:15:29 +02:00
|
|
|
Nginx --> App
|
|
|
|
|
App --> ApiR
|
|
|
|
|
App --> AdminR
|
|
|
|
|
ApiR --> Store
|
|
|
|
|
ApiR --> Markdown
|
|
|
|
|
AdminR --> Store
|
|
|
|
|
AdminR --> Users
|
2026-06-21 13:15:06 +02:00
|
|
|
Store --> Posts
|
|
|
|
|
Users --> UsersFile
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Key Design Decisions
|
|
|
|
|
|
|
|
|
|
- **File-based, no database** — every post is a `.md` file with TOML
|
2026-07-26 18:15:29 +02:00
|
|
|
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-enforced** — `admin` 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.
|
2026-06-21 13:15:06 +02:00
|
|
|
|
|
|
|
|
## CI
|
|
|
|
|
|
2026-07-26 18:15:29 +02:00
|
|
|
volumen uses **Gitea Actions**; the workflows live under
|
|
|
|
|
`.gitea/workflows/` and run on the `fedora:latest` runner.
|
2026-06-21 13:15:06 +02:00
|
|
|
|
2026-07-26 18:15:29 +02:00
|
|
|
### Test — `.gitea/workflows/test.yml`
|
2026-06-21 13:15:06 +02:00
|
|
|
|
|
|
|
|
Triggered on push and pull requests targeting `development`.
|
|
|
|
|
|
2026-07-26 18:15:29 +02:00
|
|
|
| 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` |
|
2026-06-21 13:15:06 +02:00
|
|
|
|
|
|
|
|
Run the same locally before opening a PR:
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-07-26 18:15:29 +02:00
|
|
|
just build
|
2026-06-21 13:15:06 +02:00
|
|
|
just test
|
|
|
|
|
```
|
|
|
|
|
|
2026-07-26 18:15:29 +02:00
|
|
|
### Release — `.gitea/workflows/release.yml`
|
2026-06-21 13:15:06 +02:00
|
|
|
|
2026-07-26 18:15:29 +02:00
|
|
|
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`.
|
2026-06-21 13:15:06 +02:00
|
|
|
|
2026-07-26 18:15:29 +02:00
|
|
|
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.
|
2026-06-21 13:15:06 +02:00
|
|
|
|
|
|
|
|
### Cutting a release
|
|
|
|
|
|
2026-07-26 18:15:29 +02:00
|
|
|
1. Bump `version` under `[project]` in `pyproject.toml`.
|
2026-06-25 21:37:27 +02:00
|
|
|
2. Rename `## [development]` to `## [X.Y.Z] — YYYY-MM-DD` at the top of
|
2026-07-26 18:15:29 +02:00
|
|
|
`CHANGELOG.md`, and add a fresh empty `## [development]` section
|
|
|
|
|
above it.
|
2026-06-21 13:15:06 +02:00
|
|
|
3. Ensure CI is green on `development`.
|
2026-07-26 18:15:29 +02:00
|
|
|
4. Merge `development` into `main` (the only time `main` changes outside
|
|
|
|
|
a tag).
|
2026-06-21 13:15:06 +02:00
|
|
|
5. Tag and push:
|
|
|
|
|
```bash
|
|
|
|
|
git tag -a vX.Y.Z -m "volumen vX.Y.Z"
|
|
|
|
|
git push origin main
|
|
|
|
|
git push origin vX.Y.Z
|
|
|
|
|
```
|
2026-07-26 18:15:29 +02:00
|
|
|
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.
|
2026-06-21 13:15:06 +02:00
|
|
|
|
|
|
|
|
## Report a Bug
|
|
|
|
|
|
2026-07-26 18:15:29 +02:00
|
|
|
Open an issue at <https://sourcedock.dev/petrbalvin/volumen/issues>
|
2026-06-21 13:15:06 +02:00
|
|
|
|
2026-07-26 18:15:29 +02:00
|
|
|
- volumen version (`uv run volumen version`).
|
2026-06-21 13:15:06 +02:00
|
|
|
- Operating system and architecture.
|
|
|
|
|
- Exact steps (request or admin action) that reproduce it.
|
2026-07-26 18:15:29 +02:00
|
|
|
- Expected vs actual behaviour, and the relevant slice of
|
|
|
|
|
`journalctl -u volumen`.
|
2026-06-21 13:15:06 +02:00
|
|
|
|
2026-07-26 18:15:29 +02:00
|
|
|
For **security issues**, please email **opensource@petrbalvin.org** rather
|
|
|
|
|
than opening a public issue.
|