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 |
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.
| 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 throughapps/service/servicetest. Front ends (the CLI, the REPL, the desktop app),pkg/api,pkg/clientandpkg/socketnever reach the engine; only the service links it (since 2026-10-04: the CLI’s--localruns in a privateblitzd). One Go module (github.com/retail-cortex/blitz). - MR-02 Enforced twice: engine libraries’
visibilitylists only the engine, the service, the CLI’smainpackage (for its tests) and the three packages whose tests use the engine; andbazel 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 inblitzd, 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 existingvisibility, so new engine packages start public and fail the check until given one.
- MR-10 Bazel 9.2 (
.bazelversion; Bazelisk picks it), bzlmod only: rules_go and Gazelle (Go fromgo.mod, dependencies throughgo_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.lockis committed. - MR-11
bazel run //:gazellewrites the Go rules. Directives inBUILD.bazel: the prefix; protos have hand-written rules;apps/desktop/web,apps/vscodeand.bazelbsparen’t Go.bazel run //tools:tidyis the dependency upkeep:go mod tidy -ewith Bazel’s Go (-e: the generated proto package has no files outside the build),bazel mod tidy, Gazelle. Plaingo,pnpmorbufon the host are a last resort;bazel run @rules_go//go -- …is the go command, andbazel run -- @pnpm//:pnpm --dir "$PWD/apps/desktop/web" add --save-exact <package>changes the page’s packages (rules_js’s pnpm, the version inpackage.json). The npm workspace is the repository’s root (pnpm-workspace.yaml: the page andapps/vscode, onepnpm-lock.yaml); after editing apackage.jsonby hand,bazel run -- @pnpm//:pnpm --dir "$PWD" install --lockfile-only. - MR-12 Protos:
//proto/blitz/v1:blitzv1_go_protogenerates one Go package,github.com/retail-cortex/blitz/proto/blitz/v1(blitzv1), with the messages and Connect’s clients and handlers (protoc-gen-connect-gowithpackage_suffix=,//build:connect_go).//apps/desktop/web:api_tsgenerates the page’s TypeScript withprotoc-gen-esfrom the sameproto_library(build/es_proto.bzl, protoc from protobuf’s toolchain, from the sources so comments survive). Nothing generated is committed; plaingo builddoesn’t work. Editors usetools/gopackagesdriver.sh(gopls) and tsconfig’srootDirs(afterbazel build //apps/desktop/web:api_ts). - MR-13 The page:
//apps/desktop/web:pageruns Vite (outside Bazel’s sandbox: Vite resolvesindex.htmlto its real path, which the sandbox links outside itself);:testruns vitest and:typecheckrunstsc;:devserves 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,productiontags (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.bzlinbuild/, 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:requirefor preconditions,assertfor checks) and run table-driven cases as subtests (conventions indocs/content/development/_index.md).bazel test //...runs every Go test, the page’s tests and type check, and the proto lint;--config=raceruns the Go tests with the race detector (tests only). Tests that start commands in the OS sandbox are taggedno-sandbox(macOS’s sandbox can’t nest in Bazel’s);--test_env=RUNSC_PATHlets the gVisor tests findrunsc. 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.shchecks 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), andbazel run //tools:buffor 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 ingo.mod(tool) andMODULE.bazel.
- MR-20
--config=releasestamps:build/workspace_status.shreports the version (git describe, tag withoutv, else the commit; with uncommitted changes,-dirtyand the first 7 hex digits of a SHA-256 ofgit diff HEADand the untracked files, so two builds of one commit differ), and the binaries take it throughx_defs(main.version); unstamped builds saydevand stay cacheable.//build:versionis 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-trayfor the host, each with its own Mach-O UUID and, on macOS, signed ad hoc asdev.blitz.cli,dev.blitz.serviceanddev.blitz.tray(build/macos.bzl; until v0.6.5 also//apps/desktop:blitz-desktopasdev.blitz.desktop). rules_go links with the fixed build IDredacted, from which Go’s linker derives the UUID, and through a temporary file, which names the linker’s signaturea.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(frommacho_uuid_linkopts, a fixed value per program and platform, so builds stay reproducible) sets the UUID;signed_binaryre-signs; until v0.6.5Blitz.appwas signed as a bundle (signed_app), and the Wails app embedded itsInfo.plistin its executable (__TEXT,__info_plist, through acc_librarylink option), so it had a bundle identifier outsideBlitz.apptoo — 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*_unsignedbinaries they come from;blitz_<os>_<arch>andblitzd_<os>_<arch>(pure Go) for darwin/linux × amd64/arm64 and windows/amd64. - MR-22
//release:archives:blitz_<os>_<arch>.tar.gz(.zipfor Windows), each a folderblitz_<os>_<arch>/withblitz,blitzd,blz(a link toblitz; not on Windows), README, LICENSE, NOTICE and THIRD_PARTY_NOTICES (//release:contents_testchecks 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): universalblitz-desktop,blitzandblitzd(lipo), the stampedInfo.plist(darwin/Info.plist.in),iconfile.icnsgenerated fromappicon.png(sips,iconutil; the PNG is rendered fromappicon.svg, the brand’s bolt on the app’s blue), and LICENSE, NOTICE and THIRD_PARTY_NOTICES inContents/Resources(:app_contents_test).sign_macos.shsigns 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,:pacmanand: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-desktoplinking to the app, a launcher, the icon (1024 px and the scalable SVG, inhicolor), and in/usr/share/doc/blitz-desktopDebian’scopyrightfile (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).
- The release archives’ macOS binaries (
blitz_darwin_*, cross-built on Linux) have their own UUIDs but are still signed asa.out:codesignneeds macOS. Building them on the macOS runner, or signing with rcodesign on Linux, would name them (MR-21). - Plain
go build,go testandgo listdon’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 frombazel-bin/apps/desktop/web/src(orbazel clean); the text check (i18n.lint.test.ts) then reads it. Running the tests from their runfiles would avoid it.