Skip to content

Contributing to buildzr

Thank you for considering contributing to buildzr! Your help is greatly appreciated. Below are some guidelines to help you get started.

Happy Hacking 👩‍💻!

Getting Started

Codespaces

I recommend using GitHub Codespaces for development. Codespaces provides a consistent development environment and simplifies the setup process.

You can quickly set up and start contributing to our project using GitHub Codespaces via GitHub CLI. Follow these steps:

  1. Install the GitHub CLI if you haven’t already. Visit https://cli.github.com/.

  2. Authenticate with your GitHub account by running:

    gh auth login
    
  3. Create a new codespace for this repository using the following command:

    gh codespace create -r amirulmenjeni/buildzr
    
  4. Once the codespace is created, you can connect to it via VS Code or ssh into it:

    gh codespace ssh
    
  5. Inside the workspace, sync the locked Python environment:

    uv sync --all-extras --locked
    

    Run project commands through the environment with uv run, for example:

    uv run pytest --mypy tests
    

The codespace environment also includes useful tools developed by the Structurizr team such as Structurizr CLI, Structurizr Lite, and Structurizr On-Premises to view diagrams defined in Structurizr DSL or in Structurizr JSON. They’re mostly helpful during development to validate the Structurizr JSON generated by buildzr (see Structurizr).

Running Tests

All tests are located in the tests folder. To run the tests, use the following command:

uv run pytest --mypy tests

python -m buildzr.site.check DATA_DIR checks the data/ folder of one built workspace (for example .buildzr/build/shop/data) against the data contract: the JSON schemas in buildzr/site/schema/ plus what they cannot express, such as canonical bytes, containment, counts, frames and ports. It prints OK or each error and exits 1 on errors. buildzr build runs the same check on every workspace it builds and writes a broken workspace when it fails, so the command is for contributors who want to check a data folder on its own. jsonschema, which it needs, is a normal dependency of buildzr.

Testing buildzr Samples

Sometimes it is valuable to manually check the output of the Structurizr workspace generated by buildzr visually. For this reason, you can create buildzr samples in the tests/samples directory.

Here’s an example of a simple workspace to showcase the group feature in Structurizr DSL used for grouping elements together:

from buildzr.dsl import (
    Workspace,
    SoftwareSystem,
    Container,
    Component,
    Group,
    SystemLandscapeView,
    SystemContextView,
    ContainerView,
)

with Workspace("w") as w:
    with Group("Company 1"):
        system_a = SoftwareSystem("A")
        with system_a:
            a1 = Container("a1")
            a2 = Container("a2")

    with Group("Company 2"):
        system_b = SoftwareSystem("B")
        with system_b:
            b1 = Container("b1")
            b2 = Container("b2")
            with b2:
                c1 = Component("c1")

    system_c = SoftwareSystem("C")

    # Define relationships
    system_a >> "Uses" >> system_b
    a1 >> "Uses" >> b1
    system_a >> "Uses" >> system_c

    # Create views
    SystemLandscapeView(
        key='groups-sample',
        description="Groups Sample"
    )

    SystemContextView(
        software_system_selector=system_a,
        key='groups-sample-a',
        description="Groups Sample - Software System A"
    )

    SystemContextView(
        software_system_selector=system_b,
        key='groups-sample-b',
        description="Groups Sample - Software System B"
    )

    ContainerView(
        software_system_selector=system_b,
        key='groups-sample-b2',
        description="Groups Sample - Container B2"
    )

    # Export to JSON
    w.to_json('groups-sample.json')

The example above demonstrates the current buildzr API using context managers (with statements) for a cleaner, more Pythonic syntax. Key features shown:

  • Use with Workspace("name") as w: to create a workspace context
  • Use with Group("name"): to group elements together
  • Use with software_system: or with container: to nest containers and components
  • Define relationships using the >> operator
  • Views and styles are created within the workspace context
  • Export using w.to_json('filename.json')

You can run this code directly or create sample files in the tests/samples directory for testing purposes.

Working with the .json files

What can you do with the generated .json files?

As mentioned, the .json file, if successfully and correctly generated using buildzr, should follow the Structurizr JSON schema. Thus you can make use of existing and mature Structurizr tools such as the Structurizr CLI and Structurizr Lite.

If you use codespaces (highly recommended), these tools are already pre-installed in the devcontainer image.

For example, to export the tests/samples/.tests.samples.groups.json to other format (say, PlantUML):

structurizr.sh \
    export \
    --format plantuml \
    --workspace tests/samples/.tests.samples.groups.json \
    --output tests/samples/.tests.samples.groups.puml

To view the rendering of tests/samples/.tests.samples.groups.json in the browser at http://localhost:8080:

structurizr-lite.sh tests/samples/.tests.samples.groups.json

Using Structurizr On-Premises

Structurizr On-Premises provides a full-featured workspace server with API support, allowing you to publish and manage multiple workspaces. This is useful for testing workspace publishing, API integration, and multi-workspace scenarios.

The codespace includes a helper script to run Structurizr On-Premises via Docker:

# Start Structurizr On-Premises (available at http://localhost:8080)
structurizr-onpremises.sh start

# Check status
structurizr-onpremises.sh status

# View logs
structurizr-onpremises.sh logs

# Stop the server
structurizr-onpremises.sh stop

Default credentials: structurizr / password

Once running, you can publish workspaces using the Structurizr CLI:

# Publish a workspace
structurizr.sh push \
    -id 1 \
    -key YOUR_API_KEY \
    -secret YOUR_API_SECRET \
    -workspace workspace.json \
    -url http://localhost:8080/api

Data is persisted in ~/.structurizr-onpremises/ so your workspaces survive container restarts.

Working on the documentation

The documentation is a MkDocs site. Its pages are the Markdown files under docs/, and mkdocs.yml configures it. The docs workflow builds the site with --strict and deploys it to GitHub Pages on every push to master. The public address is https://buildzr.dev.

Set up the environment

Work in the devcontainer of the repository; it has the same Python, uv and Java tools as CI. Install every extra, which includes the docs extra:

uv sync --all-extras --locked

uv sync --extra docs also installs the documentation tools, but it removes the other extras from the environment, and the tests that need them, such as the PlantUML export tests, fail. uv sync --all-extras --locked brings them back.

The documentation tests build the examples of the pages with the real layout when Node.js 18 or newer is on PATH and the viewer is built ((cd viewer && npm ci && npm run build)). Without them, the tests lay the examples out with a stand-in that draws nothing real; the commands, their output and the Model API answers are still checked.

The documentation build also needs the files of the Run Code button (see Python examples and Run Code). Build them once, and again after you change the viewer or the Python of buildzr; this needs Node.js 22:

(cd viewer && npm ci)
uv run python scripts/build_docs_assets.py

The script builds the viewer and writes everything into docs/runcode/, which Git ignores. Without that folder, mkdocs build and mkdocs serve stop with a message that names this command.

Preview and check your changes

uv run mkdocs serve serves the site at http://127.0.0.1:8000 and reloads it when you save a file.

Before you submit, run the strict build and the documentation tests. The strict build fails on any warning, such as a broken link. The documentation tests check the links between pages and run the examples in the pages.

uv run mkdocs build --strict
uv run pytest tests/docs

Edit the roadmap

The roadmap is the data file roadmap.yml in the repository root; there is no Markdown to edit. The docs build (scripts/mkdocs_hooks.py) validates the file and renders it into the page, and stops with a list of every fault if the file breaks the schema. Move an item between active, planned and launched instead of copying it, and base a launched entry on a release tag or merged pull request. The Structurizr feature tables are a separate page, docs/structurizr-compatibility.md.

Add a page

Create a Markdown file under docs/ and add it to the nav section of mkdocs.yml. A page that is not in nav does not appear in the menu.

Python examples and Run Code

Every python code block of the documentation gets a Run Code button. When a reader presses it, the browser downloads Python (Pyodide, about 6 MB from cdn.jsdelivr.net, once) and a small build of buildzr, runs the example, and draws its workspace in the buildzr viewer under the code. The diagram is built with the same steps as buildzr build, so it looks the same as in buildzr serve. Nothing is downloaded until the first press.

Write each example so that it runs on its own:

  • An example may be a fragment. Run Code adds from buildzr.dsl import * when the example imports nothing from buildzr.dsl, and puts an example without with Workspace(...) as w: inside one.
  • The diagram shows the last Workspace the example creates.
  • An error shows the line number as the page shows it, with the message.

Two comments control the button:

Comment Where Effect
# no-run: <reason> First line of the block No Run Code button. Use it for code that cannot run in a browser, such as an export to PlantUML or code that reads a file, and give the reason (tests/docs/test_snippets.py checks it). The line is hidden.
# focus: <element id> Any line The viewer opens on this element, selected. The id is the viewer’s id, such as shop/database. The line is hidden and not copied.
# focus: view:<key> Any line The viewer opens on the view with this key.

Without # focus:, the viewer opens on a deployment or dynamic view that the example defines; otherwise on the landscape, with the last element created that has children expanded; otherwise on the whole landscape. Add # focus: only when a page is about one element that this rule does not pick.

To check the examples of a page in a real browser, use run_page_blocks of tests/docs/browser.py in a test; tests/docs/test_runcode_browser.py shows how. These tests need Node.js, the Chromium of the viewer tests ((cd viewer && npx playwright install chromium)), the built Run Code files and a connection to cdn.jsdelivr.net; without them they are skipped.

Submitting Changes

As usual, just fork the repo, make changes, and submit PRs!

Releasing

The release workflow publishes releases from master. It never pushes commits to master.

Cut a release

Open a pull request that sets a final version, for example 1.0.1, in buildzr/__about__.py. When the pull request is merged, the workflow builds the viewer and the package, checks them, publishes the package to PyPI and pushes the tag v1.0.1.

Cut a release candidate

Do the same with a version such as 1.1.0rc1. Versions follow PEP 440, and rc is the only pre-release kind that is released. Number the candidates rc1, rc2, and so on; the final 1.1.0 sorts after them. The tag of 1.1.0rc1 is v1.1.0rc1.

PyPI does not offer a release candidate to a plain pip install buildzr. Install it with one of these commands:

  • uvx --from buildzr==1.1.0rc1 buildzr build
  • uv add buildzr==1.1.0rc1
  • pip install buildzr==1.1.0rc1
  • pip install --pre buildzr

Which pushes release

Every push to master runs the workflow. It releases only when the version is final or a release candidate, and its tag does not exist yet. The workflow skips development, alpha, beta and post-release versions, such as 1.0.1.dev0, 1.0.1a1, 1.0.1b1 or 1.0.1.post1. Bumping the version to .dev0 therefore does not trigger a release.

What the workflow checks before it publishes

The viewer (viewer/) is built into the gitignored folder buildzr/site/static/ and ships inside the package. The workflow therefore runs npm ci and npm run build in viewer/ before uv build.

Then .github/scripts/check_wheel.py dist --sdist fails the run, before anything is published, unless both the sdist and the wheel contain the viewer, its copy of the third-party notices, the JSON schemas, the modules of buildzr build and buildzr serve and the layout’s Node runner with elkjs, and the version of the wheel equals buildzr/__about__.py. It also requires THIRD_PARTY_NOTICES.txt in the .dist-info/licenses/ folder of the wheel and at the root of the sdist (license-files in pyproject.toml puts it there). To check a build yourself:

(cd viewer && npm ci && npm run build)
uv build
python .github/scripts/check_wheel.py dist --sdist

Third-party notices

THIRD_PARTY_NOTICES.txt holds the licence text and copyright notice of everything from others that the package ships: the npm packages linked into the viewer bundle, the Inter font and the vendored elkjs. The viewer’s About dialog shows the same file. It is generated; do not edit it. After you change the production dependencies of viewer/package.json, the Inter files or the elkjs bundle, run this from the repository root, then commit the result:

(cd viewer && npm ci)
python scripts/gen_third_party_notices.py

tests/site/test_notices.py fails when a package of the bundle is missing from the file or the file is out of date. Dependencies that are only type declarations (@types/*, csstype) or build tools are not listed, because their code is not shipped. The Java libraries of the PlantUML export are not shipped either; they are downloaded on first use.

The smoke test of the wheel

Pull requests that change the package also run a smoke test in the build workflow. It installs the built wheel into a new virtual environment and, in a temporary project with two model files in workspaces/, runs buildzr build and buildzr serve, then fetches the list page, both workspaces, a Model API answer and the list of workspaces. A second project with one workspace checks that / redirects to it, and a last step builds with uvx --from the wheel. To run it locally, run the three commands above first. The smoke test needs uv, Node.js 18 or newer and curl.

.github/scripts/smoke_wheel.sh dist

The version check on pull requests

When a pull request changes the version, the check_version job of the build workflow checks the version before the merge. It fails on:

  • a version that is not canonical PEP 440, for example 0.0.033;
  • a final version or release candidate that already has a tag;
  • a version that is not higher than the latest v* tag. For example, 1.1.0rc2 is higher than 1.1.0rc1, and 1.1.0 is higher than 1.1.0rc2, but 1.1.0rc3 is not higher than the released 1.1.0.

Otherwise, the job reports whether the merge will release the version to PyPI.

If a release fails

If a release fails, no tag is pushed. Re-run the failed run from the Actions tab before you merge anything else. Otherwise, the next push to master releases that version from the newer commit.

A re-run skips files that are already on PyPI, so a re-run after a failed tag push only pushes the tag. A version that reached PyPI cannot be uploaded again: fix forward with a new version.