RFC: Automatic Branching Model
Summary
This RFC proposes a branching model and set of GitHub Actions workflows that
automate version management, forward-porting, backporting, and releases for
the Respect/Validation library. The goal is to keep multiple minor versions
maintainable at once with minimal manual effort.
Motivation
The project already maintains long-lived branches for each minor version
(3.0, 3.1, …) alongside main. Today, propagating fixes across versions,
creating releases, and keeping main in sync are all manual tasks. As the
number of maintained versions grows, this becomes error-prone and
time-consuming.
An automated branching model would:
- Let contributors target a single branch without worrying about propagation.
- Let maintainers backport and forward-port with a label click.
- Keep
main automatically in sync with the latest released version.
- Make patch releases a one-click operation.
- Enforce a clear, documented lifecycle for every version branch.
Branching Layout
main ← always mirrors the latest version branch (read-only sync)
│
├── 3.2 ← latest version branch (active development)
├── 3.1 ← previous version (maintained, receives backports)
├── 3.0 ← older version (maintained, receives backports)
└── 2.4 ← legacy version (security fixes only)
Branch roles
| Branch |
Purpose |
Accepts PRs |
Automated |
main |
Mirror of latest version branch |
No |
Yes |
X.Y (latest) |
Active development for next patch |
Yes |
— |
X.Y (older) |
Maintained version, backports only |
Via bot |
Yes |
X.Y (EOL) |
No longer maintained |
No |
— |
Rules
main is read-only. It is force-updated automatically whenever
the latest version branch receives a push.
- All PRs target a version branch, never
main.
- Contributors target the oldest applicable branch. A bug fix that affects
3.0, 3.1, and 3.2 should be opened against 3.0. A new feature
should target the latest version branch.
- Forward-porting is automatic. After a commit lands on an older branch,
the change is cherry-picked upward to every newer maintained branch.
- Backporting is label-driven. When a PR merged into a newer branch also
needs to land on an older one, a maintainer adds a backport:X.Y label
and the bot creates a cherry-pick PR.
- Rebase only. All branches maintain a linear history. PRs are integrated
via "Rebase and merge" on GitHub. No merge commits are created anywhere in
the tree.
- Major version bumps are manual. This model automates minor and patch
version management only. Starting a new major version (e.g. 4.0) is a
rare, high-stakes event that should be handled manually by maintainers,
including branch creation, CI configuration, documentation updates, and
migration guides.
Branch Protection
All version branches and main should have branch protection rules enabled:
| Rule |
Version branches |
main |
| Require PR reviews |
Yes |
No (mirror) |
| Require status checks to pass |
Yes |
No (mirror) |
| Require linear history |
Yes |
Yes |
| Restrict who can push |
Yes |
Yes |
TheRespectPanda bot (PANDA_GITHUB_PAT) must be added to the branch
protection bypass list on every protected branch. The bot needs to push
directly for:
- Forward-port cherry-picks (clean cases that skip PR creation).
- Syncing
main to the latest version branch.
- Creating new version branches.
- Tagging releases.
Without this bypass, every automated push would be blocked by protection
rules, defeating the purpose of the automation.
Workflows
1. Forward-Port Cascade (forward-port.yml)
Trigger: Push to any maintained version branch.
When a commit lands on an older branch (e.g. 3.0), the workflow
cherry-picks the new commits into every newer branch in order:
Cherry-picking (rather than merging) preserves the linear, rebase-only
history that this project requires. If the cherry-pick is clean, the commits
are pushed directly. If there are conflicts, a branch is created and a draft
PR is opened for manual resolution.
name: Forward-Port Cascade
on:
push:
branches: ['[0-9]+.[0-9]+']
jobs:
forward-port:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine newer branches
id: branches
run: # ...
- name: Cherry-pick to newer branches
env:
GH_TOKEN: ${{ secrets.PANDA_GITHUB_PAT }}
run: # ...
2. Backport Bot (backport.yml)
Trigger: A merged PR receives a backport:X.Y label.
Creates a cherry-pick PR targeting the specified older branch. The resulting
PR will be rebased on merge, maintaining linear history.
name: Backport
on:
pull_request:
types: [closed, labeled]
jobs:
backport:
if: >
github.event.pull_request.merged == true &&
contains(join(github.event.pull_request.labels.*.name, ','), 'backport:')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Cherry-pick to target branches
env:
GH_TOKEN: ${{ secrets.PANDA_GITHUB_PAT }}
run: # ...
3. Sync Main (sync-main.yml)
Trigger: Push to the latest version branch.
Keeps main identical to the latest version branch by force-updating it.
Since main is a read-only mirror, this is always safe, no work happens on
main directly.
name: Sync main
on:
push:
branches: ['[0-9]+.[0-9]+']
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Sync main to latest version branch
env:
GH_TOKEN: ${{ secrets.PANDA_GITHUB_PAT }}
run: # ...
4. Release (release.yml, revised)
Trigger: Manual dispatch from a version branch, specifying the bump level.
Replaces the current tag-and-pray approach. A maintainer triggers the workflow
from the target branch, selects patch (or minor for a new version line),
and the workflow:
- Computes the next tag from the branch's existing tags.
- Creates and pushes the tag.
- Generates a changelog from commits since the last tag.
- Publishes a GitHub Release with the changelog.
name: Release
on:
workflow_dispatch:
inputs:
bump:
description: 'Version bump level'
required: true
type: choice
options: [patch, minor]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Compute next version
id: version
run: # ...
- name: Create tag
run: # ...
- name: Generate changelog
id: changelog
run: # ...
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: # ...
5. New Version Branch (new-version.yml)
Trigger: Manual dispatch specifying the new minor version.
This workflow handles minor version branches only (e.g. 3.2, 3.3).
Major version bumps (e.g. 4.0) are intentionally excluded and must be
performed manually (see Rule 7).
When it's time to start a new minor version (e.g. 3.2), this workflow:
- Creates the new branch from the current latest version branch.
- Force-updates the
main mirror to point at the new branch.
- Creates the corresponding
backport:X.Y label for the previous branch.
- Updates the
LAST_MINOR_VERSION repository variable.
name: New Version Branch
on:
workflow_dispatch:
inputs:
version:
description: 'New minor version (e.g. 3.2)'
required: true
type: string
jobs:
create:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Validate input is a minor bump
run: # ...
- name: Create branch and update labels
env:
GH_TOKEN: ${{ secrets.PANDA_GITHUB_PAT }}
run: # ...
6. Regional Updates (update-regionals.yml, revised)
The existing workflow already targets LAST_MINOR_VERSION. Under this model,
that variable is updated automatically by the New Version Branch workflow,
so the regional update workflow needs no structural change, only a switch
from secrets.LAST_MINOR_VERSION to vars.LAST_MINOR_VERSION (repository
variables are the correct primitive for non-secret configuration).
The forward-port cascade will then propagate regional updates from the latest
branch to all newer branches automatically. The sync-main workflow will
update main once the latest branch receives the change.
7. TheRespectPanda Commands (extended)
Add new commands to the existing panda.yml:
| Command |
Action |
@TheRespectPanda backport X.Y |
Adds backport:X.Y label to the PR |
@TheRespectPanda release patch |
Triggers release workflow on the PR branch |
This lets maintainers drive backports and releases from PR comments without
leaving the conversation.
Contribution Flow
Bug fix (affects multiple versions)
1. Contributor opens PR against the oldest affected branch (e.g. 3.0)
2. CI runs on 3.0
3. Maintainer reviews and rebases the PR
4. forward-port.yml cherry-picks 3.0 → 3.1 → 3.2 automatically
5. sync-main.yml force-updates main from 3.2
Bug fix (affects only latest)
1. Contributor opens PR against the latest version branch (e.g. 3.2)
2. CI runs on 3.2
3. Maintainer reviews and rebases the PR
4. sync-main.yml force-updates main
New feature
1. Contributor opens PR against the latest version branch (e.g. 3.2)
2. CI runs on 3.2
3. Maintainer reviews and rebases the PR
4. sync-main.yml force-updates main
Backport after merge
1. A fix is rebased into 3.2
2. Maintainer adds `backport:3.1` label (or comments @TheRespectPanda backport 3.1)
3. backport.yml cherry-picks and opens a PR against 3.1
4. Maintainer reviews and rebases the backport PR
5. After rebase, forward-port cascade runs 3.1 → 3.2 (usually a no-op)
Patch release
1. Maintainer goes to Actions → Release → Run workflow
2. Selects the target branch (e.g. 3.1) and bump level (patch)
3. Workflow tags 3.1.1, generates changelog, publishes GitHub Release
Starting a new minor version
1. Maintainer goes to Actions → New Version Branch → Run workflow
2. Enters "3.3"
3. Workflow creates 3.3 from 3.2, force-updates main, creates backport:3.2 label
Starting a new major version
This is intentionally manual. Maintainers must:
1. Create the new branch (e.g. 4.0) manually
2. Update .github/maintained-versions.json
3. Update branch protection rules to cover the new branch
4. Add TheRespectPanda to the bypass list on the new branch
5. Create backport labels as needed
6. Update CI matrix if the new major version changes PHP requirements
7. Write migration guides and update documentation
Version Lifecycle
Each version branch moves through these phases:
| Phase |
Receives |
Duration |
| Active |
Features + fixes |
Until next minor ships |
| Maintained |
Bug fixes only |
Until next-next minor ships |
| Security |
Security fixes only |
Discretion of maintainers |
| EOL |
Nothing |
Branch archived |
Example timeline:
3.0 Active → Maintained (when 3.1 ships) → Security (when 3.2 ships) → EOL (when 3.3 ships)
3.1 Active → Maintained (when 3.2 ships) → Security (when 3.3 ships) → ...
3.2 Active → ...
To mark a branch as EOL, a maintainer removes it from the maintained set by
updating .github/maintained-versions.json. The forward-port workflow skips
branches not listed in the manifest.
Configuration
All automation relies on a single source of truth for which branches are
currently maintained. This can be a JSON file committed to the repository:
// .github/maintained-versions.json
{
"versions": ["3.0", "3.1", "3.2"],
"latest": "3.2",
"security-only": ["2.4"]
}
All workflows read this file to determine:
- Which branches to forward-port into.
- Which branches accept backport labels.
- Which branch
main mirrors.
- Which branch receives regional updates.
This replaces the current LAST_MINOR_VERSION secret/variable with a
versioned, auditable, reviewable configuration file.
Proposed Changes to CONTRIBUTING.md
The following sections should be added or updated in CONTRIBUTING.md to
reflect the new branching model.
Add: "Choosing a Target Branch" section (after the opening paragraphs)
## Choosing a Target Branch
This project maintains multiple versions simultaneously. Which branch you
target depends on the nature of your change:
- **Bug fixes**: open your PR against the **oldest affected version branch**
(e.g. `3.0` if the bug exists in 3.0, 3.1, and 3.2). Automation will
forward-port the fix to all newer branches after it is merged.
- **New features**: open your PR against the **latest version branch** (check
which branch `main` currently mirrors: that is always the latest).
- **Documentation fixes**: follow the same rule as bug fixes.
Never open a PR against `main` directly. It is a read-only mirror of the
latest version branch and does not accept PRs.
If you are unsure which branch to target, open your PR against the latest
version branch and note in the description which older versions are affected.
A maintainer will help you retarget if needed.
Add: "Backports" section (after "Choosing a Target Branch")
## Backports
If your fix was merged into a newer branch but also needs to land on an older
version, a maintainer will add a `backport:X.Y` label to your PR. This
triggers an automatic cherry-pick. You do not need to open separate PRs for
each version, the automation handles it.
If the automatic cherry-pick fails due to conflicts, a maintainer will either
resolve the conflicts or ask for your help.
Migration Plan
- Create
.github/maintained-versions.json with current branches (3.0,
3.1; latest: 3.1).
- Add
forward-port.yml and backport.yml workflows.
- Add
sync-main.yml workflow.
- Replace
release.yml with the dispatch-based version.
- Add
new-version.yml workflow.
- Update
update-regionals.yml to read from the manifest instead of a
secret.
- Extend
panda.yml with backport and release commands.
- Create
backport:x.x labels.
- Update
CONTRIBUTING.md with the proposed changes above.
- Configure branch protection on all version branches and
main:
- Enable "Require linear history" on all protected branches.
- Add TheRespectPanda to the bypass list on every protected branch.
- Set GitHub merge button to allow only "Rebase and merge" (disable
"Create a merge commit" and "Squash and merge" in repo settings).
Each step can be delivered as an independent PR, merged in order.
Open Questions
-
Auto-push for clean forward-ports. Should clean forward-port
cherry-picks be pushed directly (current proposal), or should they always
open a PR for CI to run first? Direct push is faster but skips CI on the
target branch. Opening a PR every time is safer but noisier. A middle
ground: push directly but trigger CI via a separate workflow_run event,
reverting automatically if it fails.
-
Forward-port for multi-commit pushes. When multiple commits are pushed
at once to an older branch (e.g. a force-push after rebase), the
cherry-pick range before..after may not map cleanly. The workflow may
need to compare trees rather than replaying individual commits.
RFC: Automatic Branching Model
Summary
This RFC proposes a branching model and set of GitHub Actions workflows that
automate version management, forward-porting, backporting, and releases for
the Respect/Validation library. The goal is to keep multiple minor versions
maintainable at once with minimal manual effort.
Motivation
The project already maintains long-lived branches for each minor version
(
3.0,3.1, …) alongsidemain. Today, propagating fixes across versions,creating releases, and keeping
mainin sync are all manual tasks. As thenumber of maintained versions grows, this becomes error-prone and
time-consuming.
An automated branching model would:
mainautomatically in sync with the latest released version.Branching Layout
Branch roles
mainX.Y(latest)X.Y(older)X.Y(EOL)Rules
mainis read-only. It is force-updated automatically wheneverthe latest version branch receives a push.
main.3.0,3.1, and3.2should be opened against3.0. A new featureshould target the latest version branch.
the change is cherry-picked upward to every newer maintained branch.
needs to land on an older one, a maintainer adds a
backport:X.Ylabeland the bot creates a cherry-pick PR.
via "Rebase and merge" on GitHub. No merge commits are created anywhere in
the tree.
version management only. Starting a new major version (e.g.
4.0) is arare, high-stakes event that should be handled manually by maintainers,
including branch creation, CI configuration, documentation updates, and
migration guides.
Branch Protection
All version branches and
mainshould have branch protection rules enabled:mainTheRespectPanda bot (
PANDA_GITHUB_PAT) must be added to the branchprotection bypass list on every protected branch. The bot needs to push
directly for:
mainto the latest version branch.Without this bypass, every automated push would be blocked by protection
rules, defeating the purpose of the automation.
Workflows
1. Forward-Port Cascade (
forward-port.yml)Trigger: Push to any maintained version branch.
When a commit lands on an older branch (e.g.
3.0), the workflowcherry-picks the new commits into every newer branch in order:
Cherry-picking (rather than merging) preserves the linear, rebase-only
history that this project requires. If the cherry-pick is clean, the commits
are pushed directly. If there are conflicts, a branch is created and a draft
PR is opened for manual resolution.
2. Backport Bot (
backport.yml)Trigger: A merged PR receives a
backport:X.Ylabel.Creates a cherry-pick PR targeting the specified older branch. The resulting
PR will be rebased on merge, maintaining linear history.
3. Sync Main (
sync-main.yml)Trigger: Push to the latest version branch.
Keeps
mainidentical to the latest version branch by force-updating it.Since
mainis a read-only mirror, this is always safe, no work happens onmaindirectly.4. Release (
release.yml, revised)Trigger: Manual dispatch from a version branch, specifying the bump level.
Replaces the current tag-and-pray approach. A maintainer triggers the workflow
from the target branch, selects
patch(orminorfor a new version line),and the workflow:
5. New Version Branch (
new-version.yml)Trigger: Manual dispatch specifying the new minor version.
This workflow handles minor version branches only (e.g.
3.2,3.3).Major version bumps (e.g.
4.0) are intentionally excluded and must beperformed manually (see Rule 7).
When it's time to start a new minor version (e.g.
3.2), this workflow:mainmirror to point at the new branch.backport:X.Ylabel for the previous branch.LAST_MINOR_VERSIONrepository variable.6. Regional Updates (
update-regionals.yml, revised)The existing workflow already targets
LAST_MINOR_VERSION. Under this model,that variable is updated automatically by the New Version Branch workflow,
so the regional update workflow needs no structural change, only a switch
from
secrets.LAST_MINOR_VERSIONtovars.LAST_MINOR_VERSION(repositoryvariables are the correct primitive for non-secret configuration).
The forward-port cascade will then propagate regional updates from the latest
branch to all newer branches automatically. The sync-main workflow will
update
mainonce the latest branch receives the change.7. TheRespectPanda Commands (extended)
Add new commands to the existing
panda.yml:@TheRespectPanda backport X.Ybackport:X.Ylabel to the PR@TheRespectPanda release patchThis lets maintainers drive backports and releases from PR comments without
leaving the conversation.
Contribution Flow
Bug fix (affects multiple versions)
Bug fix (affects only latest)
New feature
Backport after merge
Patch release
Starting a new minor version
Starting a new major version
Version Lifecycle
Each version branch moves through these phases:
Example timeline:
To mark a branch as EOL, a maintainer removes it from the maintained set by
updating
.github/maintained-versions.json. The forward-port workflow skipsbranches not listed in the manifest.
Configuration
All automation relies on a single source of truth for which branches are
currently maintained. This can be a JSON file committed to the repository:
All workflows read this file to determine:
mainmirrors.This replaces the current
LAST_MINOR_VERSIONsecret/variable with aversioned, auditable, reviewable configuration file.
Proposed Changes to CONTRIBUTING.md
The following sections should be added or updated in
CONTRIBUTING.mdtoreflect the new branching model.
Add: "Choosing a Target Branch" section (after the opening paragraphs)
Add: "Backports" section (after "Choosing a Target Branch")
Migration Plan
.github/maintained-versions.jsonwith current branches (3.0,3.1; latest:3.1).forward-port.ymlandbackport.ymlworkflows.sync-main.ymlworkflow.release.ymlwith the dispatch-based version.new-version.ymlworkflow.update-regionals.ymlto read from the manifest instead of asecret.
panda.ymlwith backport and release commands.backport:x.xlabels.CONTRIBUTING.mdwith the proposed changes above.main:"Create a merge commit" and "Squash and merge" in repo settings).
Each step can be delivered as an independent PR, merged in order.
Open Questions
Auto-push for clean forward-ports. Should clean forward-port
cherry-picks be pushed directly (current proposal), or should they always
open a PR for CI to run first? Direct push is faster but skips CI on the
target branch. Opening a PR every time is safer but noisier. A middle
ground: push directly but trigger CI via a separate
workflow_runevent,reverting automatically if it fails.
Forward-port for multi-commit pushes. When multiple commits are pushed
at once to an older branch (e.g. a force-push after rebase), the
cherry-pick range
before..aftermay not map cleanly. The workflow mayneed to compare trees rather than replaying individual commits.