10 KiB
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.
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
- Fedora:
just— thejustfileis the source of truth for install / run / build / test / uninstall.- Linux, macOS, or FreeBSD.
Quick Start
git clone https://codeberg.org/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 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.
Running Tests
just test
The minitest suite (test/) covers:
frontmatter—+++TOML block parse/serialise round-trips.post/store— the Post model, reading/writing posts and media.markdown— GFM rendering (tables, fenced code).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) viarack-test.
Linting
RuboCop is the source of truth for style (.rubocop.yml). The project enforces
zero offenses:
bundle exec rubocop # check (also run as part of `just build`)
bundle exec rubocop -A # autocorrect
Code Style
- Idiomatic Ruby; let RuboCop guide you. CI fails on any offense.
- Two-space indentation, double quotes, and
# frozen_string_literal: trueat the top of every Ruby file. - Document every class/module with a short comment (
Style/Documentationis 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. - Admin content is authored by trusted users; see
docs/security.mdfor the trust model before touching rendering or uploads.
Testing
- Put tests in
test/*_test.rb, each starting withrequire "test_helper". - Use
Dir.mktmpdirfor 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.
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 Server.build to .configured to avoid a Sinatra 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
- 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, etc.). - Ensure
bundle exec rubocopreports zero offenses 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 # CLI: serve, hash-password, version, help
├── lib/
│ ├── volumen.rb # library entry (requires the core modules)
│ └── 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
│ └── web/
│ ├── views/ # ERB templates (layout, login, list, form, settings)
│ └── public/ # static assets (volumen-logo.svg, volumen-icon.svg)
├── test/ # minitest suite
├── docs/ # architecture, configuration, api-reference, deployment, security
├── .forgejo/workflows/ # test.yml, release.yml (Forgejo Actions)
├── justfile # install, run, dev, lint, fmt, build, test, uninstall
├── volumen.gemspec
├── Gemfile
├── CHANGELOG.md
└── LICENSE
Architecture Overview
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]
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
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.
- Single universal gem — volumen is pure Ruby, so
gem buildproduces one platform-independent gem. Only thepumadependency 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 —
adminmanages users,authormanages posts and their own account;require_admin!gates user management on the server, with last-admin safeguards.
CI
volumen uses Forgejo Actions; both workflows live under .forgejo/workflows/
and run on the codeberg-small runner.
Test — .forgejo/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 |
Run the same locally before opening a PR:
bundle exec rubocop
just test
Release — .forgejo/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.
Requires a FORGEJO_TOKEN secret with permission to create releases.
Cutting a release
- Bump
VERSIONinlib/volumen/version.rb. - 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 gem and publishes the release from the CHANGELOG section.
Report a Bug
Open an issue at https://codeberg.org/petrbalvin/volumen/issues with:
- volumen version (
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.