summaryrefslogtreecommitdiff
path: root/docs/handbook/ci/branch-strategy.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/handbook/ci/branch-strategy.md')
-rw-r--r--docs/handbook/ci/branch-strategy.md388
1 files changed, 388 insertions, 0 deletions
diff --git a/docs/handbook/ci/branch-strategy.md b/docs/handbook/ci/branch-strategy.md
new file mode 100644
index 0000000000..89535c9f54
--- /dev/null
+++ b/docs/handbook/ci/branch-strategy.md
@@ -0,0 +1,388 @@
+# Branch Strategy
+
+## Overview
+
+The Project Tick monorepo uses a structured branch naming convention that enables
+CI scripts to automatically classify branches, determine valid base branches for PRs,
+and decide which checks to run. The classification logic lives in
+`ci/supportedBranches.js`.
+
+---
+
+## Branch Naming Convention
+
+### Format
+
+```
+prefix[-version[-suffix]]
+```
+
+Where:
+- `prefix` — The branch type (e.g., `master`, `release`, `feature`)
+- `version` — Optional semantic version (e.g., `1.0`, `2.5.1`)
+- `suffix` — Optional additional descriptor (e.g., `pre`, `hotfix`)
+
+### Parsing Regex
+
+```javascript
+/(?<prefix>[a-zA-Z-]+?)(-(?<version>\d+\.\d+(?:\.\d+)?)(?:-(?<suffix>.*))?)?$/
+```
+
+This regex extracts three named groups:
+
+| Group | Description | Example: `release-2.5.1-hotfix` |
+|-----------|----------------------------------|---------------------------------|
+| `prefix` | Branch type identifier | `release` |
+| `version` | Semantic version number | `2.5.1` |
+| `suffix` | Additional descriptor | `hotfix` |
+
+### Parse Examples
+
+```javascript
+split('master')
+// { prefix: 'master', version: undefined, suffix: undefined }
+
+split('release-1.0')
+// { prefix: 'release', version: '1.0', suffix: undefined }
+
+split('release-2.5.1')
+// { prefix: 'release', version: '2.5.1', suffix: undefined }
+
+split('staging-1.0')
+// { prefix: 'staging', version: '1.0', suffix: undefined }
+
+split('staging-next-1.0')
+// { prefix: 'staging-next', version: '1.0', suffix: undefined }
+
+split('feature-new-ui')
+// { prefix: 'feature', version: undefined, suffix: undefined }
+// Note: "new-ui" doesn't match version pattern, so prefix consumes it
+
+split('fix-crash-on-start')
+// { prefix: 'fix', version: undefined, suffix: undefined }
+
+split('backport-123-to-release-1.0')
+// { prefix: 'backport', version: undefined, suffix: undefined }
+// Note: "123-to-release-1.0" doesn't start with a version, so no match
+
+split('dependabot-npm')
+// { prefix: 'dependabot', version: undefined, suffix: undefined }
+```
+
+---
+
+## Branch Classification
+
+### Type Configuration
+
+```javascript
+const typeConfig = {
+ master: ['development', 'primary'],
+ release: ['development', 'primary'],
+ staging: ['development', 'secondary'],
+ 'staging-next': ['development', 'secondary'],
+ feature: ['wip'],
+ fix: ['wip'],
+ backport: ['wip'],
+ revert: ['wip'],
+ wip: ['wip'],
+ dependabot: ['wip'],
+}
+```
+
+### Branch Types
+
+| Prefix | Type Tags | Description |
+|----------------|------------------------------|-------------------------------------|
+| `master` | `development`, `primary` | Main development branch |
+| `release` | `development`, `primary` | Release branches (e.g., `release-1.0`) |
+| `staging` | `development`, `secondary` | Pre-release staging |
+| `staging-next` | `development`, `secondary` | Next staging cycle |
+| `feature` | `wip` | Feature development branches |
+| `fix` | `wip` | Bug fix branches |
+| `backport` | `wip` | Backport branches |
+| `revert` | `wip` | Revert branches |
+| `wip` | `wip` | Work-in-progress branches |
+| `dependabot` | `wip` | Automated dependency updates |
+
+Any branch with an unrecognized prefix defaults to type `['wip']`.
+
+### Type Tag Meanings
+
+| Tag | Purpose |
+|--------------|-------------------------------------------------------------|
+| `development` | A long-lived branch that receives PRs |
+| `primary` | The main target for new work (master or release branches) |
+| `secondary` | A staging area — receives from primary, not from WIP directly |
+| `wip` | A short-lived branch created for a specific task |
+
+---
+
+## Order Configuration
+
+Branch ordering determines which branch is preferred when multiple branches are
+equally good candidates as PR base branches:
+
+```javascript
+const orderConfig = {
+ master: 0,
+ release: 1,
+ staging: 2,
+ 'staging-next': 3,
+}
+```
+
+| Branch Prefix | Order | Preference |
+|----------------|-------|------------------------------------------|
+| `master` | 0 | Highest — default target for new work |
+| `release` | 1 | Second — for release-specific changes |
+| `staging` | 2 | Third — staging area |
+| `staging-next` | 3 | Fourth — next staging cycle |
+| All others | `Infinity` | Lowest — not considered as base branches |
+
+If two branches have the same number of commits ahead of a PR head, the one with
+the lower order is preferred. This means `master` is preferred over `release-1.0`
+when both are equally close.
+
+---
+
+## Classification Function
+
+```javascript
+function classify(branch) {
+ const { prefix, version } = split(branch)
+ return {
+ branch,
+ order: orderConfig[prefix] ?? Infinity,
+ stable: version != null,
+ type: typeConfig[prefix] ?? ['wip'],
+ version: version ?? 'dev',
+ }
+}
+```
+
+### Output Fields
+
+| Field | Type | Description |
+|----------|----------|------------------------------------------------------|
+| `branch` | String | The original branch name |
+| `order` | Number | Sort priority (lower = preferred as base) |
+| `stable` | Boolean | `true` if the branch has a version suffix |
+| `type` | Array | Type tags from `typeConfig` |
+| `version` | String | Extracted version number, or `'dev'` if none |
+
+### Classification Examples
+
+```javascript
+classify('master')
+// { branch: 'master', order: 0, stable: false, type: ['development', 'primary'], version: 'dev' }
+
+classify('release-1.0')
+// { branch: 'release-1.0', order: 1, stable: true, type: ['development', 'primary'], version: '1.0' }
+
+classify('release-2.5.1')
+// { branch: 'release-2.5.1', order: 1, stable: true, type: ['development', 'primary'], version: '2.5.1' }
+
+classify('staging-1.0')
+// { branch: 'staging-1.0', order: 2, stable: true, type: ['development', 'secondary'], version: '1.0' }
+
+classify('staging-next-1.0')
+// { branch: 'staging-next-1.0', order: 3, stable: true, type: ['development', 'secondary'], version: '1.0' }
+
+classify('feature-new-ui')
+// { branch: 'feature-new-ui', order: Infinity, stable: false, type: ['wip'], version: 'dev' }
+
+classify('fix-crash-on-start')
+// { branch: 'fix-crash-on-start', order: Infinity, stable: false, type: ['wip'], version: 'dev' }
+
+classify('dependabot-npm')
+// { branch: 'dependabot-npm', order: Infinity, stable: false, type: ['wip'], version: 'dev' }
+
+classify('wip-experiment')
+// { branch: 'wip-experiment', order: Infinity, stable: false, type: ['wip'], version: 'dev' }
+
+classify('random-unknown-branch')
+// { branch: 'random-unknown-branch', order: Infinity, stable: false, type: ['wip'], version: 'dev' }
+```
+
+---
+
+## Branch Flow Model
+
+### Development Flow
+
+```
+┌─────────────────────────────────────────────┐
+│ master │
+│ (primary development, receives all work) │
+└──────────┬──────────────────────┬───────────┘
+ │ fork │ fork
+ ▼ ▼
+┌──────────────────┐ ┌──────────────────────┐
+│ staging-X.Y │ │ release-X.Y │
+│ (secondary, │ │ (primary, │
+│ pre-release) │ │ stable release) │
+└──────────────────┘ └──────────────────────┘
+```
+
+### WIP Branch Flow
+
+```
+ master (or release-X.Y)
+ │
+ ┌────┴────┐
+ │ fork │
+ ▼ │
+ feature-* │
+ fix-* │
+ backport-* │
+ wip-* │
+ │ │
+ └──── PR ─┘
+ (merged back)
+```
+
+### Typical Branch Lifecycle
+
+1. **Create** — Developer creates `feature-my-change` from `master`
+2. **Develop** — Commits follow Conventional Commits format
+3. **PR** — Pull request targets `master` (or the appropriate release branch)
+4. **CI Validation** — `prepare.js` classifies branches, `lint-commits.js` checks messages
+5. **Review** — Code review by owners defined in `ci/OWNERS`
+6. **Merge** — PR is merged into the target branch
+7. **Cleanup** — The WIP branch is deleted
+
+---
+
+## How CI Uses Branch Classification
+
+### Commit Linting Exemptions
+
+PRs between development branches skip commit linting:
+
+```javascript
+if (
+ baseBranchType.includes('development') &&
+ headBranchType.includes('development') &&
+ pr.base.repo.id === pr.head.repo?.id
+) {
+ core.info('This PR is from one development branch to another. Skipping checks.')
+ return
+}
+```
+
+Exempted transitions:
+- `staging` → `master`
+- `staging-next` → `staging`
+- `release-X.Y` → `master`
+
+### Base Branch Suggestion
+
+For WIP branches, `prepare.js` finds the optimal base:
+
+1. Start with `master` as a candidate
+2. Compare commit distances to all `release-*` branches (sorted newest first)
+3. The branch with fewest commits ahead is the best candidate
+4. On ties, lower `order` wins (master > release > staging)
+
+### Release Branch Targeting Warning
+
+When a non-backport/fix/revert branch targets a release branch:
+
+```
+Warning: This PR targets release branch `release-1.0`.
+New features should typically target `master`.
+```
+
+---
+
+## Version Extraction
+
+The `stable` flag and `version` field enable version-aware CI decisions:
+
+| Branch | `stable` | `version` | Interpretation |
+|-------------------|----------|-----------|--------------------------------|
+| `master` | `false` | `'dev'` | Development, no specific version |
+| `release-1.0` | `true` | `'1.0'` | Release 1.0 |
+| `release-2.5.1` | `true` | `'2.5.1'` | Release 2.5.1 |
+| `staging-1.0` | `true` | `'1.0'` | Staging for release 1.0 |
+| `feature-foo` | `false` | `'dev'` | WIP, no version association |
+
+Release branches are sorted by version (descending) when computing base branch
+suggestions, so `release-2.0` is checked before `release-1.0`.
+
+---
+
+## Module Exports
+
+The `supportedBranches.js` module exports two functions:
+
+```javascript
+module.exports = { classify, split }
+```
+
+| Function | Purpose |
+|-----------|----------------------------------------------------------|
+| `classify` | Full classification: type tags, order, stability, version|
+| `split` | Parse branch name into prefix, version, suffix |
+
+These are imported by:
+- `ci/github-script/lint-commits.js` — For commit linting exemptions
+- `ci/github-script/prepare.js` — For branch targeting validation
+
+---
+
+## Self-Testing
+
+When `supportedBranches.js` is run directly (not imported as a module), it executes
+built-in tests:
+
+```bash
+cd ci/
+node supportedBranches.js
+```
+
+Output:
+
+```
+split(branch)
+master { prefix: 'master', version: undefined, suffix: undefined }
+release-1.0 { prefix: 'release', version: '1.0', suffix: undefined }
+release-2.5.1 { prefix: 'release', version: '2.5.1', suffix: undefined }
+staging-1.0 { prefix: 'staging', version: '1.0', suffix: undefined }
+staging-next-1.0 { prefix: 'staging-next', version: '1.0', suffix: undefined }
+feature-new-ui { prefix: 'feature', version: undefined, suffix: undefined }
+fix-crash-on-start { prefix: 'fix', version: undefined, suffix: undefined }
+...
+
+classify(branch)
+master { branch: 'master', order: 0, stable: false, type: ['development', 'primary'], version: 'dev' }
+release-1.0 { branch: 'release-1.0', order: 1, stable: true, type: ['development', 'primary'], version: '1.0' }
+...
+```
+
+---
+
+## Adding New Branch Types
+
+To add a new branch type:
+
+1. Add the prefix and type tags to `typeConfig`:
+
+```javascript
+const typeConfig = {
+ // ... existing entries ...
+ 'hotfix': ['wip'], // or ['development', 'primary'] if it's a long-lived branch
+}
+```
+
+2. If it should be a base branch candidate, add it to `orderConfig`:
+
+```javascript
+const orderConfig = {
+ // ... existing entries ...
+ hotfix: 4, // lower number = higher preference
+}
+```
+
+3. Update the self-tests at the bottom of the file.