Contributing

Workflow

  1. Create a short-lived branch off the main branch.

  2. Make small, atomic changes. Follow Code style and add tests (Testing).

  3. Run task lint and task test. If you touched documentation or docstrings, run task docs too.

  4. Open a pull request with a clear title and description. Review the diff yourself first and keep the pull request to a single purpose.

Change the documentation together with the behaviour it describes, in the same pull request.

Where documentation goes

The site is organised by audience, and each page belongs to exactly one.

Users

Task-oriented pages for readers and reviewers. Short, free of jargon, and illustrated with screenshots.

Operators

Deploying, configuring and running Adriano. Describe consequences (what happens, what to check) rather than mechanisms, and do not mention development workflow.

Developers

Everything for engineers: architecture, decisions, how-tos and the generated references. Implementation detail belongs here.

Do not copy docstrings into narrative pages. Document the behaviour of code in its docstring, which appears in Python API reference, and link to it with :mod:, :class: or :func:. Refer to other pages with :doc: or, for sections, with the page path as prefix, for example :ref:`developer/frontend:Vuetify first`.

Building the docs locally

task docs          # build into docs/_build/html
task docs:serve    # build, then serve on http://localhost:8765

Both use the docs dependency group of the backend project and run Sphinx with -W, so any warning fails the build. The build imports the backend modules for autodoc and writes the OpenAPI document to docs/_generated/openapi.json for the HTTP reference; neither needs a database. docs/_build/ and docs/_generated/ are gitignored.

Writing reStructuredText

  • Wrap lines at 80 columns with one sentence per line, or break long sentences at clauses. Long URLs, code and paths may exceed the limit.

  • Use the heading hierarchy = (title, once), -, ^.

  • Do not put paragraphs in table cells; use definition lists, or .. envvar:: for settings.

  • Give code blocks a language: .. code-block:: sh.

Screenshots

  • Store images in docs/_static/screenshots/. The file INVENTORY.rst there lists the curated set; it is a working list for authors and not a page of the site.

  • Every image has meaningful :alt: text that says what the reader should see, not just “screenshot”.

  • Capture with a dev sign-in on the demo data (task demo and task demo:ui) so that no real identities or data appear.

  • Prefer a crop to the relevant region.

  • Keep the screenshot and the caption consistent with the text, and retake it when the UI changes.

Diagrams

Draw diagrams as code with .. mermaid:: (sphinxcontrib-mermaid).

  • The site-wide theme in docs/conf.py uses the Okabe-Ito colour-blind-safe palette on a white panel. Do not add inline colours unless they come from that palette.

  • Never encode meaning by colour alone. Label every edge and state, and use shapes or text to distinguish kinds of nodes.

  • Keep each diagram small enough to read without zooming, and explain it in the text next to it.

Documentation CI

The workflow .github/workflows/docs.yml builds the site with the same command as task docs on every pull request, so a warning fails the check. On the main branch (master) it also publishes the built site to GitHub Pages. If the check fails, reproduce it locally with task docs.

Releases and images

Continuous integration

Three workflows live in .github/workflows:

testing.yml

The reusable test suite (workflow_call): ruff and pytest against a PostgreSQL 18 service, eslint, vue-tsc, vitest and the production build of the frontend, and the self-tests of the release scripts. It has no event triggers of its own, so it never runs twice for the same event.

image.yml

Owns the triggers (pull requests, pushes to master, v* tags and manual dispatch). It calls testing.yml, builds the image from the root Dockerfile, smoke-tests it and, outside pull requests, pushes it to ghcr.io/exhuma/adriano.

docs.yml

Builds the documentation (see Documentation CI).

What each event publishes:

Event

Image tags

ADRIANO_VERSION

Pull request

none (build and smoke test only)

pr-<number>

Push to master

edge, sha-<short>

edge-<short>

Tag vYYYY.M.D

YYYY.M.D, stable, beta, alpha

YYYY.M.D

Tag vYYYY.M.D-beta.N

YYYY.M.D-beta.N, beta, alpha

YYYY.M.D-beta.N

The version is baked into the image (build argument ADRIANO_VERSION) and reported by /healthz. The image is never tagged latest: CalVer has no ordering to anchor it, so operators pin a full version or follow a channel.

CalVer tags and channels

Releases are driven by tags of the form vYYYY.M.D with an optional pre-release suffix -alpha.N, -beta.N or -rc.N (for example v2026.6.15 or v2026.6.15-alpha.1). Month and day have no leading zero. Every release publishes its immutable full version plus the moving channels it advances. Channels cascade by maturity:

  • -alpha.N moves alpha

  • -beta.N moves beta and alpha

  • -rc.N moves rc

  • no suffix (stable) moves stable, beta and alpha

scripts/derive_channels.sh implements the mapping.

Version-sync guard

On a tag push, scripts/check_version_sync.sh fails the run unless both manifests carry the tag’s version, each in its own canonical form:

  • frontend/package.json keeps the tag verbatim (2026.6.15-alpha.1);

  • backend/pyproject.toml uses the PEP 440 form (2026.6.15a1; -beta.N becomes bN, -rc.N becomes rcN).

The guard runs first, before the tests, and prints an ::error:: line naming the manifest that disagrees. Run it locally with scripts/check_version_sync.sh v2026.6.15, and the self-tests of both scripts with scripts/tests/test_release_scripts.sh.

Cutting a release

  1. Pick the version, for example 2026.6.15 or 2026.6.15-rc.1.

  2. Set version in frontend/package.json (verbatim) and in backend/pyproject.toml (PEP 440 form).

  3. Refresh the lock files so they record the new version: cd backend && uv lock and cd frontend && npm install --package-lock-only.

  4. Verify with scripts/check_version_sync.sh v2026.6.15.

  5. Commit (chore(release): 2026.6.15) and merge to master.

  6. Tag the merge commit and push the tag:

    git tag v2026.6.15
    git push origin v2026.6.15
    
  7. Watch the image workflow; the image and its channel tags appear under the repository’s packages.

A tag whose manifests disagree fails before anything is built or published; fix the manifests, delete the tag and tag again.

Smoke-testing an image locally

CI runs scripts/smoke_test_image.sh on every event. With Docker, curl and python3 available you can run the same checks:

docker build -t adriano:local .
scripts/smoke_test_image.sh adriano:local

The script starts a throwaway PostgreSQL 18 container, starts the image twice (the second start proves the migrations are idempotent), and checks /healthz, /config.js, the security headers, the SPA fallback, the API authentication and error responses, the immutable asset cache and the non-root user. It removes everything it started and prints the container logs when a check fails. Pass a second argument (http://127.0.0.1:9000) when port 8000 is taken.