chore: prepare release v0.4.0
Test / test (push) Successful in 1m7s
Release / release (push) Successful in 1m11s

This commit is contained in:
2026-07-26 18:15:29 +02:00
parent 19c6161ae0
commit 3b8ed31f67
61 changed files with 6614 additions and 2704 deletions
+154 -105
View File
@@ -1,16 +1,20 @@
# Contributing
Thank you for considering contributing to **volumen** — a small, file-based
Markdown blog engine written in CRuby, with a built-in admin and a public JSON
API.
Markdown blog engine written in Python, with a built-in admin and a public
JSON API.
## Development Setup
### Requirements
- **Ruby ≥ 3.3** with a C toolchain (Puma compiles a native extension).
- Fedora: `sudo dnf install ruby ruby-devel @development-tools openssl-devel`
- Debian/Ubuntu: `sudo apt install ruby-full build-essential libssl-dev`
- **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).
- [`just`](https://github.com/casey/just) — the `justfile` is the source of
truth for install / run / build / test / uninstall.
- Linux, macOS, or FreeBSD.
@@ -18,11 +22,11 @@ API.
### Quick Start
```bash
git clone https://codeberg.org/petrbalvin/volumen.git
git clone https://sourcedock.dev/petrbalvin/volumen.git
cd volumen
just install # bundle install
just test # bundle exec rake test (minitest)
just build # rubocop (zero warnings) + gem build → pkg/volumen.gem
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
```
@@ -31,7 +35,9 @@ 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`](docs/deployment.md).
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`).
### Running Tests
@@ -39,49 +45,51 @@ template to `/etc/volumen/config.toml`; see [`docs/deployment.md`](docs/deployme
just test
```
The minitest suite (`test/`) covers:
The pytest suite (`tests/`) covers:
- `frontmatter``+++` TOML block parse/serialise round-trips.
- `post` / `store` — the Post model, reading/writing posts and media.
- `markdown` — GFM rendering (tables, fenced code).
- `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 `rack-test`.
- `server` / `admin` — the JSON API and the admin (auth, CSRF, CRUD,
uploads, roles) via `httpx.AsyncClient` against the FastAPI app.
### Linting
RuboCop is the source of truth for style (`.rubocop.yml`). The project enforces
**zero offenses**:
Ruff is the source of truth for style (settings in `pyproject.toml`,
`[tool.ruff]`, `[tool.ruff.lint]`). The project enforces **zero warnings**:
```bash
bundle exec rubocop # check (also run as part of `just build`)
bundle exec rubocop -A # autocorrect
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 Ruby; let RuboCop guide you. CI fails on any offense.
- **Two-space** indentation, **double quotes**, and `# frozen_string_literal: true`
at the top of every Ruby file.
- Document every class/module with a short comment (`Style/Documentation` is on).
- Keep lines ≤ 100 characters (`Layout/LineLength`).
- Prefer guard clauses; return `nil`/objects for expected "not found" cases
rather than raising.
- Keep the dependency set small and deliberate — runtime gems are `sinatra`,
`kramdown` (+ `kramdown-parser-gfm`), `toml-rb`, `puma`, `rackup`. Crypto uses
the standard library (`openssl`, `securerandom`). New runtime dependencies
should be discussed in an issue first.
- 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`](docs/security.md)
for the trust model before touching rendering or uploads.
### Testing
- Put tests in `test/*_test.rb`, each starting with `require "test_helper"`.
- Use `Dir.mktmpdir` for anything touching the filesystem; never assume a clean
working directory.
- Exercise the HTTP layer with `Rack::Test` (`Volumen::Server.configured(...)`).
- Name tests `test_<thing>_<scenario>`; cover both the happy and the error path.
- 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
@@ -101,11 +109,11 @@ We follow [Conventional Commits](https://www.conventionalcommits.org/).
```
feat: add author role with last-admin safeguards
fix: rename Server.build to .configured to avoid a Sinatra clash
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 gem versions to latest
chore: pin Python dependencies to latest
```
- English, lowercase subject, imperative mood (`add`, not `added`).
@@ -118,10 +126,11 @@ chore: pin gem versions to latest
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`, etc.).
4. Ensure `bundle exec rubocop` reports zero offenses and `just test` passes.
`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:
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.
@@ -136,32 +145,32 @@ chore: pin gem versions to latest
```
volumen/
├── exe/
│ └── volumen # CLI: serve, hash-password, version, help
├── lib/
│ ├── volumen.rb # library entry (requires the core modules)
│ └── volumen # thin shim: from volumen.cli import main
├── src/
│ └── volumen/
│ ├── version.rb
│ ├── frontmatter.rb # parse/serialise the +++ TOML frontmatter block
│ ├── markdown.rb # kramdown (GFM) → HTML; <figure> for titled images
│ ├── post.rb # Post model (metadata + body + rendered HTML)
│ ├── store.rb # read/write posts and media on disk (mtime-cached)
│ ├── config.rb # load/merge config.toml, generate template
│ ├── password.rb # scrypt hashing/verification (OpenSSL::KDF)
│ ├── users.rb # users.toml, admin/author roles, per-user profile
│ ├── feed_helpers.rb # RSS / JSON Feed / sitemap generation
│ ├── api_routes.rb # /api/volumen/* (Sinatra routes)
│ ├── admin_routes.rb # /admin/* (Sinatra routes)
│ ├── server.rb # Sinatra app: composes the two route modules
│ ├── cli.rb # command dispatcher
│ ├── __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/
│ ├── views/ # ERB templates (layout, login, list, form, settings)
│ └── public/ # static assets (volumen-logo.svg, volumen-icon.svg)
├── test/ # minitest suite
│ ├── 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
├── .forgejo/workflows/ # test.yml, release.yml (Forgejo Actions)
- `.gitea/workflows/` # test.yml, release.yml, migration-guard.yml (Gitea Actions)
├── justfile # install, run, dev, lint, fmt, build, test, uninstall
├── volumen.gemspec
├── Gemfile
├── pyproject.toml # PEP 621 metadata + dependencies + tool config
├── uv.lock # locked dependency graph
├── config.toml.example # bootstrap config template
├── CHANGELOG.md
└── LICENSE
```
@@ -172,19 +181,24 @@ volumen/
graph TD
Browser[Browser / Vue SPA]
Nginx[nginx reverse proxy]
Server[Volumen::Server -- Sinatra]
Store[Volumen::Store]
Markdown[Volumen::Markdown -- kramdown GFM]
Users[Volumen::Users]
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 --> Server
Server -->|read/write posts| Store
Server -->|render Markdown| Markdown
Server -->|auth + roles| Users
Nginx --> App
App --> ApiR
App --> AdminR
ApiR --> Store
ApiR --> Markdown
AdminR --> Store
AdminR --> Users
Store --> Posts
Users --> UsersFile
```
@@ -192,73 +206,108 @@ graph TD
### 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.
- **Single universal gem** — volumen is pure Ruby, so `gem build` produces one
platform-independent gem. Only the `puma` dependency has a native extension,
compiled at install time on the host.
- **Trusted authors** — kramdown 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!` gates user management on the server,
with last-admin safeguards.
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.
## CI
volumen uses **Forgejo Actions**; both workflows live under `.forgejo/workflows/`
and run on the `codeberg-small` runner.
volumen uses **Gitea Actions**; the workflows live under
`.gitea/workflows/` and run on the `fedora:latest` runner.
### Test — `.forgejo/workflows/test.yml`
### Test — `.gitea/workflows/test.yml`
Triggered on push and pull requests targeting `development`.
| Job | What it does |
|--------|-----------------------------------------------|
| `lint` | `bundle exec rubocop` (must report zero offenses) |
| `test` | `bundle exec rake test` on a Ruby `3.3` / `3.4` matrix |
| 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:
```bash
bundle exec rubocop
just build
just test
```
### Release — `.forgejo/workflows/release.yml`
### Release — `.gitea/workflows/release.yml`
Triggered by pushing a `v*` tag. It verifies the tag matches `Volumen::VERSION`,
runs the quality gate (rubocop + tests), builds `volumen-X.Y.Z.gem`, extracts
the matching `CHANGELOG.md` section, creates a Forgejo release via the REST API,
and attaches the gem. It does **not** publish to RubyGems.
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`.
Requires a `FORGEJO_TOKEN` secret with permission to create releases.
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` in `lib/volumen/version.rb`.
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.
`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).
4. Merge `development` into `main` (the only time `main` changes outside
a tag).
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
```
6. The release workflow builds the gem and publishes the release from the
CHANGELOG section.
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://codeberg.org/petrbalvin/volumen/issues> with:
Open an issue at <https://sourcedock.dev/petrbalvin/volumen/issues>
- volumen version (`volumen version`).
- 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`.
- 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.
For **security issues**, please email **opensource@petrbalvin.org** rather
than opening a public issue.