Skip to main content
Blitz
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage
Edit page

028 · Monorepo

Monorepo layout and the Bazel build (spec_monorepo_028)

Status Implemented (2026-09-27; merged to main the same day)
Source MODULE.bazel, BUILD.bazel, .bazelrc, .bazelversion, .bazelignore, build/, tools/, third_party/, every BUILD.bazel, release/, apps/electron/packaging/, go.mod (tool directives)
Replaces spec_setup_001 §3–5 (layout, toolchain, Make), GoReleaser in spec_release_025
Depends on spec_setup_001

1. Purpose

Blitz is independent apps (the CLI, the service, the desktop app; since, also the tray and the VS Code extension) over shared packages, built, tested and packaged by Bazel. The owner’s decisions (2026-09-27): apps under apps/, shared packages under pkg/ with the engine under pkg/engine/, the service its own binary (blitzd), Bazel builds everything including the desktop page, protos are always generated (never committed), Bazel manages the tools, and Make goes. They reverse the earlier “No Bazel” decision (2026-09-26): the engine had bled into the front ends, and one build over Go, TypeScript, protos and packages keeps it apart.

2. Layout and dependency rules

Path What
apps/cli blitz; apps/cli/internal/tui is the REPL
apps/service blitzd; apps/service/internal/daemon runs it, apps/service/internal/server has the Connect handlers; apps/service/servicetest runs it in other packages’ tests (testonly)
apps/desktop/web The desktop page (until v0.6.5 inside the Wails app, apps/desktop)
apps/electron The desktop app (Electron, since v0.7.0: spec_electron_038); apps/electron/packaging its packages
apps/tray blitz-tray, the service in the system tray, and the Electron app’s host (--serve)
pkg/api The contract between front ends and the engine: Backend and what crosses it
pkg/client, pkg/socket Backend over the service’s API; where the service listens
pkg/engine, pkg/engine/… The engine (engine.Open → Workspace, implementing api.Backend)
pkg/{config,i18n,images,observability,redact,textutil} Shared by every app
pkg/pagehost What the desktop page asks of the program serving it, beyond the service (its status and controls, the window’s preferences, the blitz command’s installation, the OS sandbox): the tray’s /host/, for the Electron app and the Windows tray’s page (since v0.7.0)
proto/blitz/v1 The service API’s protos
build/ Build rules and settings: the Connect compiler for the Go protos, nogo, the version stamp, es_proto.bzl, macos.bzl, workspace_status.sh (pkg_config.bzl, for WebKitGTK, until v0.6.5)
tools/ Commands for working on the repository: check_deps, check_format.sh, buf, tidy, license_headers, third_party_notices.sh (with notices, its Go program), gopackagesdriver.sh
third_party/ Changes to outside code (until v0.6.5 also the Wails patch for Linux)
release/ The CLI release archives
  • MR-01 Apps depend on pkg/ only, never on each other; tests may run the service through apps/service/servicetest. Front ends (the CLI, the REPL, the desktop app), pkg/api, pkg/client and pkg/socket never reach the engine; only the service links it (since 2026-10-04: the CLI’s --local runs in a private blitzd). One Go module (github.com/retail-cortex/blitz).
  • MR-02 Enforced twice: engine libraries’ visibility lists only the engine, the service, the CLI’s main package (for its tests) and the three packages whose tests use the engine; and bazel run //tools:check_deps (CI) queries the build graph — libraries only — and fails when a front end’s (the CLI’s included, since 2026-10-04: every workspace runs in blitzd, spec_cli_020 CLI-14), the contract’s or a shared package’s library reaches the engine, when an app’s library reaches another app, or when an engine library is public. Gazelle keeps an existing visibility, so new engine packages start public and fail the check until given one.

3. Build

  • MR-10 Bazel 9.2 (.bazelversion; Bazelisk picks it), bzlmod only: rules_go and Gazelle (Go from go.mod, dependencies through go_deps), protobuf, rules_buf, aspect_rules_js with rules_nodejs (Node 22, npm packages from the page’s pnpm lockfile), bazel_lib, rules_pkg, rules_shell, apple_support, platforms. MODULE.bazel.lock is committed.
  • MR-11 bazel run //:gazelle writes the Go rules. Directives in BUILD.bazel: the prefix; protos have hand-written rules; apps/desktop/web, apps/vscode and .bazelbsp aren’t Go. bazel run //tools:tidy is the dependency upkeep: go mod tidy -e with Bazel’s Go (-e: the generated proto package has no files outside the build), bazel mod tidy, Gazelle. Plain go, pnpm or buf on the host are a last resort; bazel run @rules_go//go -- … is the go command, and bazel run -- @pnpm//:pnpm --dir "$PWD/apps/desktop/web" add --save-exact <package> changes the page’s packages (rules_js’s pnpm, the version in package.json). The npm workspace is the repository’s root (pnpm-workspace.yaml: the page and apps/vscode, one pnpm-lock.yaml); after editing a package.json by hand, bazel run -- @pnpm//:pnpm --dir "$PWD" install --lockfile-only.
  • MR-12 Protos: //proto/blitz/v1:blitzv1_go_proto generates one Go package, github.com/retail-cortex/blitz/proto/blitz/v1 (blitzv1), with the messages and Connect’s clients and handlers (protoc-gen-connect-go with package_suffix=, //build:connect_go). //apps/desktop/web:api_ts generates the page’s TypeScript with protoc-gen-es from the same proto_library (build/es_proto.bzl, protoc from protobuf’s toolchain, from the sources so comments survive). Nothing generated is committed; plain go build doesn’t work. Editors use tools/gopackagesdriver.sh (gopls) and tsconfig’s rootDirs (after bazel build //apps/desktop/web:api_ts).
  • MR-13 The page: //apps/desktop/web:page runs Vite (outside Bazel’s sandbox: Vite resolves index.html to its real path, which the sandbox links outside itself); :test runs vitest and :typecheck runs tsc; :dev serves it for development. The pnpm workspace file declares that no dependency runs install scripts. The Electron app carries the page built for the host platform, and the Windows tray embeds it (platform_transition_filegroup), whatever platform the binary is for.
  • MR-14 Removed in v0.7.0 (the Wails app; see spec_electron_038). The desktop app with cgo: Wails’s desktop,production tags (a Gazelle directive keeps Wails’s tagged files), -framework UniformTypeIdentifiers (link_darwin.go), macOS 13 minimum (.bazelrc), apple_support’s Xcode toolchains so both Mac architectures build on either Mac. On Linux, WebKitGTK 4.1 (webkit2_41) and GTK’s flags from pkg-config (pkg_config.bzl in build/, a patch to two Wails packages), since rules_go ignores #cgo pkg-config: the system’s libraries, deliberately not hermetic.
  • MR-15 Tests: Go tests assert with Testify (github.com/stretchr/testify, a test-only dependency: require for preconditions, assert for checks) and run table-driven cases as subtests (conventions in docs/content/development/_index.md). bazel test //... runs every Go test, the page’s tests and type check, and the proto lint; --config=race runs the Go tests with the race detector (tests only). Tests that start commands in the OS sandbox are tagged no-sandbox (macOS’s sandbox can’t nest in Bazel’s); --test_env=RUNSC_PATH lets the gVisor tests find runsc. Tests don’t see the shell’s environment (--incompatible_strict_action_env), so API keys never reach them.
  • MR-16 Static analysis runs in every compile of the repository’s Go code (not dependencies or generated code): nogo with //build/analyzers/doccomment (every package and exported declaration documented; tests and generated code exempt), Go vet’s analyzers and staticcheck’s default checks (SA, S, ST but for the style checks staticcheck leaves off, and SA5011, which only works inside staticcheck); //nolint:<check> silences one finding. tools/check_format.sh checks gofmt and that the BUILD files are Gazelle’s. The protos: //proto/blitz/v1:blitzv1_proto_lint (buf STANDARD, and every service, RPC, message and enum commented; descriptor sets keep their source info, .bazelrc, so the comments reach the generated Go), and bazel run //tools:buf for formatting and breaking changes. govulncheck runs on the built binaries (-mode=binary), linked with their symbol table (--strip=never): stripped, it can’t see which packages are linked and counts every advisory in a required module. staticcheck, govulncheck and buf’s version are pinned in go.mod (tool) and MODULE.bazel.

4. Versions and packages

  • MR-20 --config=release stamps: build/workspace_status.sh reports the version (git describe, tag without v, else the commit; with uncommitted changes, -dirty and the first 7 hex digits of a SHA-256 of git diff HEAD and the untracked files, so two builds of one commit differ), and the binaries take it through x_defs (main.version); unstamped builds say dev and stay cacheable. //build:version is the version as a file, for package metadata. The desktop page asks its host for the version (Version()).
  • MR-21 Binaries: //apps/cli:blitz, //apps/service:blitzd, //apps/tray:blitz-tray for the host, each with its own Mach-O UUID and, on macOS, signed ad hoc as dev.blitz.cli, dev.blitz.service and dev.blitz.tray (build/macos.bzl; until v0.6.5 also //apps/desktop:blitz-desktop as dev.blitz.desktop). rules_go links with the fixed build ID redacted, from which Go’s linker derives the UUID, and through a temporary file, which names the linker’s signature a.out: every Go program had one identity, so macOS took them for one program — its log showed the desktop app as an old test binary, and the folder dialog’s service turned it away. -B (from macho_uuid_linkopts, a fixed value per program and platform, so builds stay reproducible) sets the UUID; signed_binary re-signs; until v0.6.5 Blitz.app was signed as a bundle (signed_app), and the Wails app embedded its Info.plist in its executable (__TEXT,__info_plist, through a cc_library link option), so it had a bundle identifier outside Blitz.app too — macOS’s open-panel service dismisses the folder dialog of a host without one (bazel run; the Electron app’s development copy has its own, EL-05); the unsigned *_unsigned binaries they come from; blitz_<os>_<arch> and blitzd_<os>_<arch> (pure Go) for darwin/linux × amd64/arm64 and windows/amd64.
  • MR-22 //release:archives: blitz_<os>_<arch>.tar.gz (.zip for Windows), each a folder blitz_<os>_<arch>/ with blitz, blitzd, blz (a link to blitz; not on Windows), README, LICENSE, NOTICE and THIRD_PARTY_NOTICES (//release:contents_test checks them).
  • MR-23 Removed in v0.7.0 (the Wails app; see spec_electron_038). The macOS app is //apps/electron/packaging:Blitz_<arm64|x64>.app (EL-80). Until then //apps/desktop/packaging:Blitz.app (macOS): universal blitz-desktop, blitz and blitzd (lipo), the stamped Info.plist (darwin/Info.plist.in), iconfile.icns generated from appicon.png (sips, iconutil; the PNG is rendered from appicon.svg, the brand’s bolt on the app’s blue), and LICENSE, NOTICE and THIRD_PARTY_NOTICES in Contents/Resources (:app_contents_test). sign_macos.sh signs a copy (Developer ID and hardened runtime, or ad hoc), makes the disk image, and notarises and staples it: outside Bazel, since it needs Apple’s tools and keys.
  • MR-24 Removed in v0.7.0 (the Wails app; see spec_electron_038). The Linux packages are //apps/electron/packaging:deb, :pacman and :linux_tar (EL-80), with the same layout under /usr. Until then //apps/desktop/packaging:deb (Linux, the machine’s architecture): the three programs in /usr/lib/blitz-desktop, /usr/bin/blitz-desktop linking to the app, a launcher, the icon (1024 px and the scalable SVG, in hicolor), and in /usr/share/doc/blitz-desktop Debian’s copyright file (linux/copyright, pointing at the system’s Apache text), NOTICE and THIRD_PARTY_NOTICES (:deb_contents_test); version from //build:version.
  • MR-25 Reproducible: the release archives are byte for byte the same when built on macOS and on Linux. Output directories are named after the target platform (--experimental_platform_in_output_dir), since generated Go files’ paths end up in the binaries; archives have fixed owners and times (rules_pkg).

5. Known gaps

  • The release archives’ macOS binaries (blitz_darwin_*, cross-built on Linux) have their own UUIDs but are still signed as a.out: codesign needs macOS. Building them on the macOS runner, or signing with rcodesign on Linux, would name them (MR-21).
  • Plain go build, go test and go list don’t work: tools that need them (gopls) use the packages driver.
  • The page’s tests run in its output directory (chdir), where a source file that was deleted stays until removed from bazel-bin/apps/desktop/web/src (or bazel clean); the text check (i18n.lint.test.ts) then reads it. Running the tests from their runfiles would avoid it.