258 lines
9.9 KiB
Markdown
258 lines
9.9 KiB
Markdown
# 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
|
|
<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).
|
|
|
|
### 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_<thing>_<scenario>`; 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. 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](#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. 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:
|
|
```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 <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.
|