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:
-
Install the GitHub CLI if you haven’t already. Visit https://cli.github.com/.
-
Authenticate with your GitHub account by running:
-
Create a new codespace for this repository using the following command:
-
Once the codespace is created, you can connect to it via VS Code or
sshinto it: -
Inside the workspace, sync the locked Python environment:
Run project commands through the environment with
uv run, for example:
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:
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:orwith 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:
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 --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:
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.
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 frombuildzr.dsl, and puts an example withoutwith Workspace(...) as w:inside one. - The diagram shows the last
Workspacethe 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 builduv add buildzr==1.1.0rc1pip install buildzr==1.1.0rc1pip 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:
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:
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.
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.0rc2is higher than1.1.0rc1, and1.1.0is higher than1.1.0rc2, but1.1.0rc3is not higher than the released1.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.