Repository navigation
Dev Reproducible Builds
What MowDeck builds from its source files, how two builds of the same commit come out bit for bit identical, how that is checked, and where the limits are.
| Artefact | Built by | Released as |
|---|---|---|
Wheel and sdist of every uv workspace package (libs/*, services/*, tools/*, integrations/*) |
uv build --all-packages (hatchling) |
installed into the service images |
Web app (apps/web/build: the SvelteKit static build, csp.conf, licence notices, the API explorer) |
pnpm --filter web build |
served by the web image |
| Helm chart package (with the NATS and OpenFGA subcharts) | helm package infra/helm/mowdeck |
mowdeck-<version>.tgz on the release, oci://ghcr.io/lukislp/charts/mowdeck
|
Every image the release publishes (13 Python services, matter-server, web, authz-bootstrap) |
docker buildx build with SOURCE_DATE_EPOCH and rewrite-timestamp=true
|
ghcr.io/lukislp/mowdeck/<image> |
The inputs are pinned: uv.lock and pnpm-lock.yaml (installed frozen), the chart's Chart.lock, base images by digest (the Dockerfiles and infra/compose/.env.example), the tool versions in the workflows (uv, Node from .node-version, pnpm from packageManager, Helm) and the GitHub Actions by commit SHA. SOURCE_DATE_EPOCH is the commit time (git log -1 --format=%ct); the release workflow and the check set it.
| Where | What differed | Fix |
|---|---|---|
| Web app | SvelteKit's app version defaults to the build time; it ends up in _app/version.json, 200.html, the service worker and the CSP hash of the inline script |
version.name in apps/web/vite.config.ts is SOURCE_DATE_EPOCH when set (the build time otherwise, as before) |
| Helm chart | file times in the package were the packaging time |
helm package with SOURCE_DATE_EPOCH (Helm 4 uses it for every entry) |
| Python images |
.pyc files recorded the time the sources were unpacked; uv's parallel bytecode workers produced different .pyc bytes for the same source from run to run; uv_cache.json of the workspace packages recorded the install time |
the bytecode is compiled after uv sync in one process in a fixed order with PYTHONHASHSEED=0, hash-based (checked-hash) because SOURCE_DATE_EPOCH is set; uv_cache.json (uv's bookkeeping, not read at runtime) is deleted together with its RECORD line |
camera-relay image |
fontconfig's cache, ldconfig's aux-cache and alternatives.log of the Debian package installation carried the build time |
SOURCE_DATE_EPOCH during the installation, the log and aux-cache removed |
matter-server image |
node_modules/.modules.yaml (pnpm's install state) carried the install time |
removed; Node never reads it |
| All images | file times in the layers, image creation time |
SOURCE_DATE_EPOCH and BuildKit's rewrite-timestamp=true (release workflow and check) |
The Python wheels and sdists needed no change: hatchling writes fixed timestamps and sorted archives.
.github/workflows/reproducibility.yml runs weekly, on demand (gh workflow run reproducibility.yml) and on pull requests that change build inputs (Dockerfiles, lock files, pyproject.toml files, the web build configuration, the chart metadata, the scripts below). It is not a required check.
-
artefacts:
tools/repo-checks/scripts/reproducible_build.shbuilds the Python packages, the web app and the chart into one directory; thengit clean -xdfand the same again;tools/repo-checks/scripts/compare_builds.pycompares the two directories file by file with SHA-256. -
images (one job per released image,
linux/amd64): two builds without cache as OCI layouts with the release's build arguments,SOURCE_DATE_EPOCHandrewrite-timestamp=true, compared the same way. Identical layouts mean identical layer, config and manifest digests.
There is no list of accepted exceptions: any difference fails the run, and the job summary lists the files that differ.
Measured on 2026-11-04 (commit 587e07f0 plus this change, two clean builds each): artefacts 340/340 files identical (52 wheels and sdists of 26 packages plus uv's .gitignore, 286 web files, the chart), images 16/16 identical (linux/amd64, every OCI blob).
On Linux (or in a Linux container) with uv, pnpm (Node per .node-version) and Helm 4:
export SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)"
bash tools/repo-checks/scripts/reproducible_build.sh /tmp/build-a
git clean -xdf
bash tools/repo-checks/scripts/reproducible_build.sh /tmp/build-b
python3 tools/repo-checks/scripts/compare_builds.py /tmp/build-a /tmp/build-bAn image (BuildKit with the docker-container driver, which docker buildx create --use sets up):
for run in a b; do
docker buildx build --no-cache --platform linux/amd64 \
--build-arg SOURCE_DATE_EPOCH="$SOURCE_DATE_EPOCH" \
--output "type=oci,tar=false,rewrite-timestamp=true,dest=/tmp/fleet-$run" \
-f services/fleet/Dockerfile .
done
python3 tools/repo-checks/scripts/compare_builds.py /tmp/fleet-a /tmp/fleet-b-
Chart: check out the release tag, run the chart part of the release workflow (
helm dependency build, thenhelm package infra/helm/mowdeck --version <version> --app-version <version>withSOURCE_DATE_EPOCHof the tag's commit) and compare the.tgzwith the release asset (verified first as in Releases). -
Image: build from the tag with the build arguments and labels of the release workflow's
imagesjob (MOWDECK_VERSION,MOWDECK_SOURCE_URL,MOWDECK_SOURCE_REF=v<version>, theorg.opencontainers.image.*labels) and compare the digest of the platform manifest with the one in the published index (docker buildx imagetools inspect --raw ghcr.io/lukislp/mowdeck/<image>:<version>). Thewebimage additionally containswhats-new.json, which the release writes from the tags before the build (apps/web/build-tools/whats-new.mjs).
-
Same checkout path for the web app. Built from two different directories, the web app differs: Svelte's scoped CSS class names depend on the component paths, and the chunk hashes follow. The Python packages and the chart do not depend on the path. The image build always uses
/repo, and the check builds both times in the same workspace. - Linux. On Windows, the build context and the archives carry different file modes; build on Linux or in a container.
-
Debian packages of
camera-relay.apt-getinstalls what the Debian mirror serves on the day of the build. Two builds on the same day are identical (the check), a rebuild of an old release months later can get newer package versions. Pinning throughsnapshot.debian.orgwould close this. -
Build back end. The workspace packages require
hatchling>=1.32.4; uv resolves the newest matching version at build time, so a much later rebuild may use a newer hatchling. -
Image index and signatures. The released index also carries the SBOM and SLSA provenance attestations, which record the build's own time and invocation, and the Sigstore signatures are new on every run. Compare the platform manifests, not the index digest.
linux/arm64is built from the same Dockerfiles under QEMU and is not part of the check. -
Hash-based bytecode. With
SOURCE_DATE_EPOCHset, Python checks each imported module's.pycagainst a hash of its source instead of its time stamp: a small extra cost at start-up. -
Not covered. The Android APK (
apps/mobile, Gradle, signed with the release keystore) and the iOS build (TestFlight) are not checked.SHA256SUMS,SOURCE-OFFER.md, the SPDX SBOMs and the provenance files of a release are derived from the artefacts above or from the signing run, not from the source alone.
Mirrored from docs/dev/reproducible-builds.md at commit 05e866d6c1e5933a45569176769f20c4e05643a8. Edit the file in the repository, not this wiki page.
Mirrored from the docs/ directory at commit 05e866d6c1e5933a45569176769f20c4e05643a8. Edit the files in the repository, not this wiki.
MowDeck documentation
- Home
- Phases
- Architecture
-
Decisions (ADR)
- Architecture decisions
- ADR 0001: OpenFGA instead of self-built authorization
- ADR 0002: Self-built Identity service in FastAPI instead of Keycloak or Zitadel
- ADR 0003: SvelteKit for the web app
- ADR 0004: Monorepo with a uv workspace
- ADR 0005: AGPL-3.0 for services and apps, Apache-2.0 for libraries
- ADR 0006: PostgreSQL for all persistence, no SQLite
- ADR 0007: NATS JetStream as event bus
- ADR 0008: Traefik as gateway
- ADR 0009: Command and event envelopes, subject naming and de-duplication
- ADR 0010: Python 3.13 for all services and libraries
- ADR 0011: Separate GPL-3.0 package for the PyMammotion adapter
- ADR 0012: Mower control interface and safety rules
- ADR 0013: Device-Gateway registry, sharding and per-mower concurrency
- ADR 0014: Authorization model, tuple ownership and the mowdeck-authz client
- ADR 0015: Tokens, sessions and key rotation
- ADR 0016: Fleet commands and authorization per command kind
- ADR 0017: Telemetry storage and retention
- ADR 0018: Realtime WebSocket protocol
- ADR 0019: Mammotion adapter transports, manual drive and secrets
- ADR 0020: Web app architecture
- ADR 0021: Browser authentication flow
- ADR 0022: Notifications: rules, recipients and delivery
- ADR 0023: Map rendering and projection
- ADR 0024: Scheduler: recurrence, rain pause and command issuing
- ADR 0025: Home Assistant integration as a pure API client
- ADR 0026: Manual drive channel over the Realtime WebSocket
- ADR 0027: Health history and theft alarm
- ADR 0028: Entitlements: plans as data and enforcement
- ADR 0029: Helm chart and release pipeline
- ADR 0030: Mowing patterns as plug-ins and the Planner
- ADR 0031: Billing with a Stripe adapter
- ADR 0032: Live camera over Agora, joined directly by the browser
- ADR 0033: Vision-assisted recovery with human approval
- ADR 0034: Linked cloud accounts and device discovery
- ADR 0035: Server-side camera frames and automatic AI analysis on errors
- ADR 0036: Native app shells with Capacitor, loading the web app from the server
- ADR 0037: Camera panorama calibration in the browser, with the AI supplying points only
- ADR 0038: Dual link per mower and firmware updates
- ADR 0039: Spot mowing by MowDeck-driven paths
- ADR 0040: Link heartbeat for online status
- ADR 0041: Spot runs from the dock: undock, transit and return
- ADR 0042: Drive calibration
- ADR 0043: Turn styles
- ADR 0044: Drive view with camera
- ADR 0045: Camera relay
- ADR 0046: Device settings as capabilities
- ADR 0047: The RTK base station as a Fleet device
- ADR 0048: Maintenance commands
- ADR 0049: Manual driving over the Mammotion cloud
- ADR 0050: Passkeys and the app login hand-off
- ADR 0051: Sign in with GitHub
- ADR 0052: Sign in with Apple
- ADR 0053: Retention and downsampling of the position track
- ADR 0054: Aerial photo under the mower map
- ADR 0055: Deploy maintenance window
- ADR 0056: Camera photo in push notifications for errors and theft alarms
- ADR 0057: Step-up re-authentication for critical actions
- ADR 0058: Theft tracking mode
- ADR 0059: iOS widgets and the mowing Live Activity
- ADR 0060: Apple Watch app
- ADR 0061: Mowing angle rotation
- ADR 0062: Setup tour for new users
- ADR 0063: Matter server (mowers in Apple Home and Google Home)
- ADR 0064: Personal access tokens and the public API documentation
- ADR 0065: Security center and new-login alerts
- ADR 0066: Cloud provider plugins and adapters for more vendors
- ADR 0067: Error code lexicon, provider independent
- ADR 0068: Public demo mode
- ADR 0069: Provider conformance kit and provider package template
- ADR 0070: Virtual zones for mowers without a vendor map
- ADR 0071: Capability matrix generated from the adapters
- ADR 0072: API explorer in the web app, for the public demo
- ADR 0073: Guided tour with coach marks
- ADR 0074: NATS authentication and per-service subject permissions
- ADR 0075: Chat control as an optional assistant add-on
- ADR 0076: Demo sandboxes, one account per visitor
- ADR 0077: Liveness, readiness and routing
- ADR 0078: Batched state ingestion
- ADR 0079: Fast demo sandbox claim and release
-
Development
- Getting started
- Service template
- Shared libraries
- Cloud providers: adding a vendor
- Adding a provider: template and conformance kit
- Adding an error lexicon for a vendor
- Design: identity and authorization
- Design: fleet, telemetry, realtime
- Design: web app
- Web app
- Home Assistant integration
- Camera source bridge protocol
- Security assurance case
- Fuzzing and property-based tests
- CI and pull request scope
- Reproducible builds
- GitHub wiki mirror
-
Operations
- Operating the Compose stack
- Operating MowDeck on Kubernetes (Helm)
- Building the Bluetooth bridge (ESPHome on a XIAO ESP32-S3)
- Releases
- Vendor library updates
- Billing operations
- Telemetry: retention and position downsampling
- iPhone and Android apps
- Passkeys
- Sign in with GitHub
- Aerial photo under the map
- Sign in with Apple
- Theft tracking mode
- Matter server
- Chat control (assistant add-on)
- Source code offer (AGPL-3.0 section 13)
- Public demo
- Load testing
- Spikes
-
Guides
- Guides
- ESP32 bridge: purchase and setup
- Signing in with a passkey
- Signing in with GitHub
- Error codes in plain words
- Aerial photo under the map
- Signing in with Apple
- Confirming critical actions
- Your security page
- iPhone widgets and the mowing Live Activity
- Apple Watch
- The setup tour
- Apple Home and Google Home (Matter)
- Chat control
- Worx Landroid, Kress and LandXcape mowers
- Scripting MowDeck
- Capability matrix
- API reference
- Original plan (German)