Donner 0.5.1
Embeddable browser-grade SVG2 engine
Loading...
Searching...
No Matches
BCR Release Runbook

Status: Active — first BCR release is planned for v0.5.0. 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

  1. Bump module(name = "donner", version = "X.Y.Z") in MODULE.bazel
  2. Run the pre-release checklist below, make sure it's green
  3. Merge the release PR, tag vX.Y.Z, push
  4. .github/workflows/release.yml builds CLI binaries + calls the Publish-to-BCR reusable workflow
  5. The app opens a PR on bazelbuild/bazel-central-registry under modules/donner/X.Y.Z/ from jwmcglynn/bazel-central-registry (a fork)
  6. 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 Experimental; git_override only; revisit post-v0.5

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.bazelmodule(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): 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: 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 references a real bazel-contrib/publish-to-bcr/.github/workflows/publish.yaml@vX.Y.Z tag (Publish-to-BCR does not publish floating major tags; pin exact versions)

Ship

  • Merge release PR → push tag vX.Y.Z → GitHub Release auto-created
  • Watch Actions tab: linux + macos CLI binary jobs run, then publish-to-bcr reusable workflow
  • Watch bazelbuild/bazel-central-registrymodules/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

The Publish-to-BCR reusable GitHub Actions workflow handles the mechanical pieces:

  1. On GitHub Release publish, release.yml invokes bazel-contrib/publish-to-bcr/.github/workflows/publish.yaml@v1.2.0 with tag_name: ${{ github.event.release.tag_name }}, registry_fork: jwmcglynn/bazel-central-registry, and secrets.publish_token = secrets.BCR_PUBLISH_TOKEN. The calling job must also set id-token: write, attestations: write, and contents: write permissions.
  2. The reusable workflow pulls the release tarball, computes the SHA256 integrity, reads .bcr/metadata.template.json + .bcr/source.template.json + .bcr/presubmit.yml, substitutes {VERSION} and {TAG} placeholders, and writes a new modules/donner/X.Y.Z/ entry in the maintainer's BCR fork.
  3. It opens a PR from jwmcglynn/bazel-central-registry to bazelbuild/bazel-central-registry on the maintainer's behalf.

This flow does not use the legacy Publish-to-BCR GitHub App. Upstream marks that app as legacy and says it will be discontinued after June 30, 2026, so new Donner releases should stay on the reusable-workflow path. The only one-time setup for this workflow is a BCR fork plus a Classic PAT (or a machine user PAT) with repo + workflow scopes.

Bump the reusable workflow pin (@v1.2.0) when new releases of Publish-to-BCR land — see the releases page. Prefer exact version tags over floating refs like @v1, because Publish-to-BCR does not publish floating major-version tags.

One-time Publish-to-BCR setup

  1. Fork bazelbuild/bazel-central-registry to jwmcglynn/bazel-central-registry.
  2. Create a Classic PAT with repo + workflow scopes. Add it as BCR_PUBLISH_TOKEN in the jwmcglynn/donner repo secrets. Fine-grained PATs are not currently supported for opening PRs against public repos; see github/roadmap#600.
  3. If Donner releases move to an org-owned or shared-maintainer model, prefer a dedicated machine user PAT over a maintainer's personal token.

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)
Release workflow: publish-to-bcr skipped BCR_PUBLISH_TOKEN secret missing or PAT expired Regenerate PAT, re-save secret
Release workflow: publish-to-bcr fails with "fork not found" Maintainer BCR fork hasn't been created yet Fork bazelbuild/bazel-central-registry to the registry_fork configured in release.yml

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.

  1. Add "@donner//donner/your/new/package/..." (or the specific target) to .bcr/presubmit.yml build_targets.
  2. Re-run the BCR-consumer simulation cquery above to confirm your new library doesn't transitively pull in any non-BCR dep.
  3. 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)