Files
volumen/CONTRIBUTING.md
T

9.9 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
  • just — the justfile is 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) via rack-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: 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.
  • Admin content is authored by trusted users; see 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.

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, 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. Ensure bundle exec rubocop reports zero offenses and just test passes.
  4. Add or update tests for your change.
  5. 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.
  6. 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            # 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
│       ├── post.rb        # Post model (metadata + body + rendered HTML)
│       ├── store.rb       # read/write posts and media on disk
│       ├── config.rb      # load/merge config.toml, generate template
│       ├── password.rb    # scrypt hashing/verification (OpenSSL::KDF)
│       ├── users.rb       # users.toml, admin/author roles
│       ├── server.rb      # Sinatra app: public API + admin
│       ├── cli.rb         # command dispatcher
│       └── web/
│           ├── views/     # ERB templates (layout, login, list, form, settings)
│           └── public/    # static assets (volumen-logo.svg)
├── test/                  # minitest suite
├── docs/                  # architecture, configuration, api-reference, deployment, security
├── .forgejo/workflows/    # test.yml, release.yml (Forgejo Actions)
├── justfile               # install, run, dev, 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 .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-enforcedadmin manages users, author manages 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

  1. Bump VERSION in lib/volumen/version.rb.
  2. Add a ## [X.Y.Z] — YYYY-MM-DD section at the top of CHANGELOG.md.
  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 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.