# 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`](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 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 . 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). ### Running Tests ```bash 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**: ```bash 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`](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__`; cover both the happy and the error path. ## 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 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. 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. 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](#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 ```mermaid 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-enforced** — `admin` 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: ```bash 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. 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: ```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. ## Report a Bug Open an issue at 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.