Status: Active — blocked. The first BCR publish attempt, on the v0.5.0 tag (2026-04-16), failed (see the v0.5 retrospective). Root-causing and re-running the publish flow is tracked as a release-blocking item in 0028-v1_0_release.md Phase 1; no successful BCR publish has landed yet. Last updated: 2026-04-08.
This doc is the single source of truth for cutting a new Donner release on the Bazel Central Registry. It's tuned for quick execution, not exhaustive explanation — the "why" lives in the companion docs and PRs linked at the bottom.
TL;DR — the happy path
- Bump module(name = "donner", version = "X.Y.Z") in MODULE.bazel
- Run the pre-release checklist below, make sure it's green
- Merge the release PR, tag vX.Y.Z, push
- .github/workflows/release.yml builds, verifies, and attests the CLI binaries once
- After the release artifacts are green, use the reviewed .bcr/ templates to prepare the BCR PR manually
- Watch BCR presubmit CI on that PR, iterate on .bcr/presubmit.yml if anything fails, ping a BCR maintainer, wait for merge
What's on BCR (and what isn't)
Donner's BCR-published surface is a deliberate subset of the full library. The default build that BCR consumers get is tiny-skia + text-base:
| Feature | On BCR? | How BCR consumers get it |
| SVG parser, CSS, DOM, computed style | ✅ | default, no flags needed |
| Tiny-skia software renderer | ✅ | default backend |
| Text rendering via stb_truetype (text-base) | ✅ | default text tier |
| Filter effects (all 17 primitives) | ✅ | built-in |
| Removed full-Skia backend (legacy) | ❌ | Historical note only; power users previously needed git_override |
| text-full (HarfBuzz + WOFF2) | ❌ | Power users via git_override; also tracked as a future follow-up BCR module |
| Geode (WebGPU + Slug) backend | ❌ | Not BCR-published: its wgpu-native / WebGPU deps are non-BCR (dev_dependency overrides). Geode itself is a supported backend (the editor's default), just not a BCR-consumable config |
The mechanism that keeps the non-BCR features invisible to BCR consumers is the dev_dependency = True module extension at third_party/bazel/non_bcr_deps.bzl. BCR strips dev-only extensions when Donner is consumed as a bazel_dep, so downstream users simply never see the former full-Skia repo, @harfbuzz, @woff2, or @wgpu_native_*.
Every Donner target that references one of those hidden repos must be guarded by target_compatible_with on the relevant config_setting (e.g. //donner/svg/renderer:text_full_enabled, //donner/svg/renderer:renderer_backend_skia, //donner/svg/renderer/geode:geode_enabled). If a BCR consumer's bazel build @donner//... ever tries to resolve one of those repos, the gating is broken — see the checklist below.
Per-release checklist
Do these in order. Each step is either a command to run or a one-line visual check.
Pre-flight
- Working tree on main, clean, up to date
- docs/design_docs/0011-v0_5_release.md (or equivalent release doc) marks all release-blocking phases complete
- RELEASE_NOTES.md drafted for the version being cut
Version bump
- MODULE.bazel → module(name = "donner", version = "X.Y.Z") matches the tag being pushed
- No stale references to the previous version in docs/ or README.md
Dev-config build matrix (local or CI)
- bazel build //... — default config (tiny-skia + text-base)
- Historical note: the removed full-Skia backend used to build in the dev matrix
- bazel build --config=text-full //... — HarfBuzz + WOFF2 text shaping still builds
- bazel test //... on at least the default config green
BCR-consumer simulation (most important)
Simulate what a BCR downstream sees, where the non_bcr_deps dev extension is stripped and the former full-Skia repo / @harfbuzz / @woff2 / @wgpu_native_* do not exist.
- No git_repository / new_git_repository / non-dev *_override in top-level MODULE.bazel (grep for them): sh
grep -nE '^(git_repository|new_git_repository|git_override|archive_override)' MODULE.bazel
(Should return nothing. Dev-only entries live inside use_extension(..., dev_dependency = True) blocks or the extension .bzl file.)
- No non-BCR repo labels reachable from the BCR target allowlist in default config: sh
bazel cquery 'kind("source file", deps( \
//donner/base/... + //donner/css/... + //donner/svg:svg + \
//donner/svg/parser/... + //donner/svg/components/... + \
//donner/svg/core/... + //donner/svg/properties/... + \
//donner/svg/graph/... + //donner/svg/resources/... + \
//donner/svg/renderer:rendering_context + \
//donner/svg/renderer:renderer_tiny_skia))' \
| grep -E '^@(skia|harfbuzz|woff2|wgpu_native|resvg-test-suite|bazel_clang_tidy)'
Must return zero matches. If there are matches, a target in that allowlist has a dep that needs target_compatible_with gating (or a new target was added and isn't on the .bcr/presubmit.yml allowlist).
- .bcr/presubmit.yml build_targets cover every top-level library consumers might reasonably want. Add new libs here when you add them under //donner.
- .bcr/source.template.json strip_prefix matches donner-{VERSION} (GitHub tarball convention).
Scaffolding sanity
- .bcr/config.yml, .bcr/metadata.template.json, .bcr/source.template.json, .bcr/presubmit.yml all valid YAML/JSON
- .github/workflows/release.yml builds from released dependency tags with lockfile generation disabled, verifies both platform manifests, and attests the exact binaries before upload
Ship
- Root-cause the previous BCR attempt before retrying — pull up the last release's BCR PR on bazelbuild/bazel-central-registry (modules/donner/<prev>/), identify exactly why it failed, and confirm the fix is already in the checked-in .bcr/ templates and presubmit.yml targets. Do not retry blind. Cross-check the common-failures table below.
- Merge release PR → push tag vX.Y.Z → GitHub Release auto-created
- Watch Actions tab: linux + macos CLI binary jobs run, then the verified artifact publication job
- Prepare the BCR entry from the reviewed release source and .bcr/ templates only after artifact publication is green
- Watch bazelbuild/bazel-central-registry → modules/donner/X.Y.Z/ for the new PR
- Iterate on BCR presubmit failures via the common-failures table below
- Ping a BCR maintainer in the PR comments when presubmit CI goes green
Publish-to-BCR flow details
BCR publication is intentionally separate from the credentialed release workflow. The release workflow produces and attests immutable CLI artifacts, but it does not rebuild source or invoke a third-party BCR publisher. After those artifacts are green, prepare the registry entry from the reviewed tag and the checked-in .bcr/metadata.template.json, .bcr/source.template.json, and .bcr/presubmit.yml. Verify the source archive digest, then open the BCR pull request from the maintainer fork. This keeps registry publication operator-initiated and prevents a moved tag or workflow rerun from silently substituting source.
One-time Publish-to-BCR setup
- Fork bazelbuild/bazel-central-registry to jwmcglynn/bazel-central-registry.
- Use a dedicated maintainer identity for the fork and BCR pull request when release ownership is shared. Do not place a BCR write token in the release workflow.
Where to watch
Common failures & fixes
Update this section with real-world lessons as they happen.
| Symptom | Cause | Fix |
| BCR PR presubmit: unmapped former full-Skia external dep | A target referenced the former full-Skia repo but wasn't gated by target_compatible_with | Add the appropriate backend gating to the offending target |
| BCR PR presubmit: target not found | New top-level library added under //donner since last release | Add it to .bcr/presubmit.yml build_targets |
| BCR PR presubmit: integrity hash mismatch | GitHub regenerated the source tarball or the tag moved | Re-upload the release tarball verbatim; never force-push tags |
| source.template.json URL 404 | strip_prefix doesn't match GitHub's tarball layout | Confirm pattern is donner-{VERSION} (GitHub uses repo name + version) |
Adding a new top-level library
When you create a new top-level library under //donner/..., the BCR presubmit allowlist won't know about it automatically.
- Add "@donner//donner/your/new/package/..." (or the specific target) to .bcr/presubmit.yml build_targets.
- Re-run the BCR-consumer simulation cquery above to confirm your new library doesn't transitively pull in any non-BCR dep.
- If it does (and that's intentional — e.g. it's text-full-specific), gate the offending target with target_compatible_with on the relevant config_setting, same as text_backend_full and woff2_parser.
Future BCR scope expansion
Things that are deliberately out of scope for the first few BCR releases but may land later:
- text-full on BCR — vendor HarfBuzz + WOFF2 via git subtree (~1–2 days of BUILD.harfbuzz work), or ship a sibling donner-text-full module that layers on top of donner and brings its own HB/WOFF2. Blocked on: deciding whether to own an additional BCR module or vendor.
- Separate tiny-skia-cpp BCR module — it already has its own MODULE.bazel in third_party/tiny-skia-cpp; could be published independently and then consumed as a BCR bazel_dep from Donner. Blocked on: deciding the dev vs publish trade-off.
- Geode / wgpu-native on BCR — wgpu-native is distributed only as a prebuilt binary release (no upstream Bazel rules; no public source build on BCR), so the Geode backend stays git_override-only for the foreseeable future. Revisit post-v1.0 if someone puts up a donner-geode BCR module that pulls the http_archive in itself.
- Skia backend on BCR — not realistic. Skia is a monorepo with a custom build. It will stay git_override-only for the foreseeable future.
References
- Publish-to-BCR — the reusable workflow this runbook drives
- bazelbuild/bazel-central-registry — the BCR repository
- rules_foreign_cc/.bcr/ — reference .bcr/ layout for a C++ library
- docs/design_docs/0011-v0_5_release.md — v0.5 release scope
- third_party/bazel/non_bcr_deps.bzl — the dev-only extension that hides non-BCR deps
- docs/release_checklist.md — generic release checklist (pairs with this BCR-specific runbook)