This document describes the multi-language branch architecture and Gitflow for
please-rules.
Please plugins require a [PluginDefinition] in the repository's root
.plzconfig:
[PluginDefinition]
Name = rustBecause Please allows only one [PluginDefinition] per repository root, hosting
multiple language plugins in a single Git repository creates configuration
collisions if all languages live on the same branch.
To provide clean, dedicated plugins for multiple languages (e.g., Rust, Kotlin,
Python) while keeping all code under the single please-rules repository, we
use a Hub-and-Spoke branch architecture.
[main] (Shared code ONLY: tools/common/, base CI, licenses)
│
┌─────────┴─────────┐
▼ (merge only) ▼ (merge only)
[rust] [kotlin] <-- Dedicated language branches
│ │
┌─────┴─────┐ ┌─────┴─────┐
▼ ▼ ▼ ▼
feat/x feat/y feat/a feat/b <-- Feature branchesmain(The Hub): Contains shared infrastructure, common Go packages (tools/common/), base GitHub Actions workflows, repository linters, and documentation.maincontains zero language-specific build rules or compilers.- Language Branches (The Spokes -
rust,kotlin, etc.): Each language maintains a permanent, long-lived branch. On its branch:- The root
.plzconfigsets[PluginDefinition] Name = <lang>(e.g.rust). - Configuration keys use clean, idiomatic names (e.g.
RustcTool,DefaultEdition) without awkward cross-language prefixes. - Language-specific rules live in
build_defs/<lang>/. - Language-specific tools live in
tools/please_<lang>/. - Language-specific integration tests live in
test/<lang>/.
- The root
-
One-Way Downstream Merges: Commits flow only from
mainto language branches (main$\to$ rust). -
Never Merge Language Branches into
main: Language branches must never be merged back intomain. Merging a language branch intomainwould contaminate other language branches with unrelated rules. -
Merge, Never Rebase Published Branches: Always use
git merge mainto pull updates frommain. Never rebase published language branches (rust,kotlin), as rebasing rewrites commit SHAs, breaking consumer build caches and revision pins.
All language work branches off and merges into the corresponding language branch:
# 1. Create a feature branch off the language branch
git checkout rust
git pull origin rust
git checkout -b feat/rust-edition-2024
# 2. Make changes and test
./pleasew test //test/rust/...
# 3. Commit and push
git commit -m "feat(rust): add edition 2024 support"
git push origin feat/rust-edition-2024
# 4. Open a Pull Request targeting `rust` (NOT `main`)When shared packages (e.g. tools/common/archive) or base CI workflows need
updates:
# 1. Make changes on main
git checkout main
git pull origin main
git checkout -b fix/archive-tar-xz
# ... edit tools/common/... ...
git commit -m "fix(common): support .tar.xz extraction"
git push origin fix/archive-tar-xz
# 2. Open PR targeting and merge into `main`
# 3. Automated Downstream Propagation:
# GitHub Actions (.github/workflows/sync-downstream.yml) automatically merges
# 'main' into all active language branches (rust, kotlin, etc.), tests them,
# and pushes to remote.
# Manual Downstream Propagation (if syncing locally or offline):
git checkout rust
git pull origin rust
git merge main -m "chore: sync common infrastructure from main"
git push origin rust
git checkout kotlin
git pull origin kotlin
git merge main -m "chore: sync common infrastructure from main"
git push origin kotlinBecause Git tags share a global namespace within a repository, release tags must be prefixed with the lowercase language identifier:
<language>-v<semver>Examples:
rust-v0.4.0rust-v0.4.1kotlin-v0.1.0
GitHub Release titles/names MUST be standardized across all languages with the capitalized language name in brackets followed by the version:
[<Language>] v<semver>Examples:
[Rust] v0.4.0[Rust] v0.4.1[Kotlin] v0.1.0
Releases are fully automated via GitHub Actions
(.github/workflows/release.yml), eliminating circular dependency loops for
precompiled binary hashes:
-
Trigger the Release Workflow:
Run the workflow via
workflow_dispatch(in GitHub Actions UI or usinggh):gh workflow run release.yml \ -f language=rust \ -f version=0.4.2
(Pass
-f dry_run=trueto test and build binaries without committing or publishing). -
Automated Release Pipeline: The workflow executes:
- Validation & Smoke Checks: Unit tests and binary
--helpsanity checks. - Cross-Compilation: Generates static binaries for
linux_amd64,linux_arm64,darwin_amd64, anddarwin_arm64. - Checksums & Hashes: Generates
checksums.txtand automatically updatesVERSIONand pins the computed hashes intools/BUILD. - Git Commit & Tag: Commits the release changes to the language branch
(
chore(<lang>): prepare release v<version>) and creates the standardized tag<language>-v<semver>(e.g.rust-v0.4.2). - GitHub Release: Publishes the GitHub Release with the standardized
title
[<Language>] v<semver>and attaches all 4 binaries andchecksums.txt.
- Validation & Smoke Checks: Unit tests and binary
Consumers declare each language plugin by pointing revision to the language's
release tag or branch in plugins/BUILD:
# plugins/BUILD
plugin_repo(
name = "rust",
owner = "wcodesoft",
plugin = "please-rules",
revision = "rust-v0.4.0", # or revision = "rust"
)In the consumer's .plzconfig:
[Plugin "rust"]
Target = //plugins:rust
RustcTool = //third_party/rust:toolchain|rustcIn the consumer's BUILD files:
subinclude("///rust//build_defs:rust")
rust_binary(
name = "my_app",
srcs = ["main.rs"],
)