summaryrefslogtreecommitdiff
path: root/docs/handbook/ci/branch-strategy.md
blob: 89535c9f540d530bba60d5e1509b32cc7ef9884c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
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.