11 KiB
Contributing to Gotenberg
Gotenberg is a Docker-based API for converting documents to PDF. It is a widely used production dependency. Stability and backward compatibility are paramount. When in doubt about whether a change is breaking, flag it rather than assuming it's safe.
Getting Started
Prerequisites
- Go (see version in
go.mod) - Docker
- Node.js (see version in
.node-version), for Prettier linting - golangci-lint v2+
Build and Run
make build # Build the Docker image
make run # Run a local Gotenberg container
Development Loop
# Write your code, then:
make fmt # Format Go code
make prettify # Format non-Go files (Markdown, YAML, etc.)
make lint # Lint Go code (zero errors permitted)
make lint-prettier # Lint non-Go files
make test-unit # Run unit tests
make build # Build the Docker image (required before integration tests)
make test-integration # Run all integration tests
make telemetry # Start OpenTelemetry collector and OpenObserve
make down # Stop all compose containers
To run only the integration tests relevant to your change:
make test-integration TAGS=health
make test-integration TAGS=chromium-convert-html
make test-integration TAGS="merge,split"
Submitting a Pull Request
For non-trivial changes, outline your approach before writing code. Open an issue or draft PR describing:
- What needs to change and why.
- The proposed solution, with enough detail to implement (files to modify, interface changes, form fields, etc.).
- Which integration test tags will be affected and what new scenarios are needed.
Before opening (or marking ready) a PR, verify:
- Code compiles:
make build - Code is formatted:
make fmtandmake prettify - All linters pass:
make lintandmake lint-prettier - Integration tests pass:
make test-integration(at minimum, the relevant tags) - Unit tests pass:
make test-unit - All exported symbols and new packages have GoDoc comments
- Bruno collection is updated (if routes were added or modified)
Review your changes against the Review Checklist before submitting.
Guidelines
- One thing per PR. Keep features, bug fixes, and refactoring in separate PRs.
- Backward compatibility matters. Do not rename or remove existing CLI flags, environment variables, or API form fields without discussion.
- Integration tests first. When adding a feature or route, start by writing the Gherkin scenario in
test/integration/features/. Seetest/integration/README.mdfor the full reference. - Unit tests when applicable: table-driven tests in
*_test.gofiles using mocks frompkg/gotenberg/mocks.go.
Commit Conventions
If committing, follow the Conventional Commits specification:
<type>(<scope>): <description>
Common types: feat, fix, refactor, test, docs, chore, ci, build. The scope should match the module or area of the change (e.g., chromium, pdfengines, api).
Stage only the files related to the change. Do not use git add -A or git add ..
Core Principles
- Backward compatibility is law. Never modify existing CLI flags, environment variables, or API form fields unless explicitly instructed to perform a breaking change. Flag any breaking change immediately.
- Defensive programming. Assume input is malformed. Handle errors explicitly. Never panic.
- Atomic commits. One feature or fix per PR. Isolate refactoring from feature work.
- Idiomatic Go. Follow "Effective Go" principles. All exported symbols must have GoDoc comments starting with their name.
Project Layout
cmd/gotenberg/ → Entry point only (wiring/startup). No business logic.
pkg/gotenberg/ → Core module system, interfaces, utilities, mocks.
pkg/modules/ → Feature modules (api, chromium, libreoffice, pdfengines, etc.).
pkg/standard/ → Wires all standard modules together via imports.
test/integration/ → Gherkin feature files + Go test infrastructure.
build/ → Dockerfile, fonts, Chromium config.
.bruno/ → Bruno API collection (mirrors every route).
Key interfaces live in pkg/gotenberg/: Module, Provisioner, Validator, Debuggable. Every module implements Descriptor() and self-registers. When adding features, determine if they belong in an existing module or require a new one.
Codebase Navigation
- Start with
pkg/gotenberg/for core interfaces andpkg/modules/for feature implementations. - The integration test infrastructure in
test/integration/scenario/is well-structured. Readscenario.goandcontainers.goto understand the Gherkin step definitions before writing new tests. - Mocks for all major interfaces are in
pkg/gotenberg/mocks.go. Use them for unit tests rather than creating new ones. - Import ordering is enforced: standard library, third-party, then
github.com/gotenberg/gotenberg/v8, separated by blank lines. - When making changes, run only the relevant integration test tag rather than the full suite (40min timeout).
- Telemetry infrastructure lives in
pkg/gotenberg/telemetry.go(global Logger, Tracer, Meter) andpkg/gotenberg/internal/(log handlers, OTEL SDK init). HTTP semantic conventions are inpkg/gotenberg/semconv/.
Makefile: the Only Build Interface
All build and verification tasks go through the Makefile. Do not run go commands directly unless debugging a specific package.
| Command | Purpose | When to use |
|---|---|---|
make build |
Build the Docker image | Before integration tests, or to verify compilation |
make run |
Run Gotenberg container via docker compose |
Manual testing. Flags are configured via Makefile variables and compose.yaml |
make telemetry |
Start OpenTelemetry collector and OpenObserve | When testing telemetry locally |
make down |
Stop all compose containers | After manual testing |
make fmt |
Format Go code (go fix, golangci-lint fmt, go mod tidy) |
Before every commit |
make lint |
Lint Go code (strict .golangci.yml config) |
Before every commit. Zero errors permitted |
make lint-prettier |
Lint non-Go files (Markdown, YAML, etc.) with Prettier | Before every commit |
make prettify |
Format non-Go files (Markdown, YAML, etc.) with Prettier | Before every commit |
make test-unit |
Run unit tests (go test -race ./...) |
After code changes to pkg/ |
make test-integration |
Run integration tests (Gherkin/Godog, 40min timeout) | After any feature or route change |
make godoc |
Serve GoDoc at localhost:6060 |
To verify documentation |
Module System
Gotenberg uses a self-registering module architecture inspired by CaddyServer. Each module:
- Lives in
pkg/modules/<name>/ - Implements the
gotenberg.Moduleinterface (at minimumDescriptor()) - May also implement
gotenberg.Provisioner,gotenberg.Validator, orgotenberg.Debuggable - Self-registers via
init()and is wired throughpkg/standard/
When adding a feature, first determine if it belongs in an existing module. Only create a new module if the feature represents a genuinely separate concern.
Coding Patterns
- Error handling: Always wrap errors with context using
fmt.Errorf("description: %w", err). Never swallow errors silently. - Import ordering: Enforced by
gci: standard library, then third-party, thengithub.com/gotenberg/gotenberg/v8. Three groups separated by blank lines. - Mocks: Comprehensive mock implementations for all major interfaces live in
pkg/gotenberg/mocks.go. Use these for unit tests. - Logging: Use
gotenberg.Logger(mod)to get the module's slog logger duringProvision(). All log calls must be context-aware:logger.DebugContext(ctx, msg),logger.InfoContext(ctx, msg),logger.ErrorContext(ctx, msg). This propagates trace/span IDs into structured logs when OpenTelemetry is active. - Telemetry: External tool calls (Chromium, LibreOffice, PDF engines, webhooks, downloads) must create OTEL spans with
trace.SpanKindClientandsemconv.ServerAddress("toolname"). Usegotenberg.Tracer()andgotenberg.Meter()for traces and metrics respectively. - No business logic in
cmd/: Thecmd/gotenberg/package is strictly for wiring and startup.
Review Checklist
Backward Compatibility
- No existing CLI flags renamed or removed
- No existing environment variables renamed or removed
- No existing API form fields renamed or removed
- No existing HTTP endpoints changed or removed
- No changes to default values that alter existing behavior
- Deprecated flags have both old and new names registered, with
fs.MarkDeprecated()
If any of these are violated, the change must be flagged as a breaking change.
Linting Standards
The .golangci.yml enforces strict rules including: gosec, govet, errcheck, staticcheck, dupl, bodyclose, exhaustive, errname, sloglint, gocritic, and more. Zero linting errors are permitted.
Formatters enforce gci, gofmt, gofumpt, goimports with import ordering:
- Standard library
- Third-party packages
github.com/gotenberg/gotenberg/v8
Three groups separated by blank lines.
Code Quality
- Errors are wrapped with context:
fmt.Errorf("description: %w", err). No swallowed errors. - No business logic in
cmd/. - No panics in production code paths.
- Input is validated defensively.
- New features belong in the correct module (or justify a new one).
Documentation
- Every exported function, type, constant, and variable has a GoDoc comment starting with its name.
- New packages include a
doc.gofile. README.mdis not modified unless explicitly requested.
Scoped Guidelines
Detailed guidelines for specific areas of the codebase:
test/integration/README.md: Integration test framework, Gherkin step reference, available tags, and how to write new tests..bruno/README.md: Bruno API collection structure,.brufile format, conventions, and route update checklist.pkg/modules/pdfengines/README.md: How to add new PDF engine features (Makefile variable and flag).