Repository navigation
RFC: Automatic Branching Model #1710
Description
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
mainautomatically 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
mainis 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, and3.2should be opened against3.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 abackport:X.Ylabel
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
mainto 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:
3.0 → 3.1 → 3.2
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
mainmirror to point at the new branch. - Creates the corresponding
backport:X.Ylabel for the previous branch. - Updates the
LAST_MINOR_VERSIONrepository 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
mainmirrors. - 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.jsonwith current branches (3.0,
3.1; latest:3.1). - Add
forward-port.ymlandbackport.ymlworkflows. - Add
sync-main.ymlworkflow. - Replace
release.ymlwith the dispatch-based version. - Add
new-version.ymlworkflow. - Update
update-regionals.ymlto read from the manifest instead of a
secret. - Extend
panda.ymlwith backport and release commands. - Create
backport:x.xlabels. - Update
CONTRIBUTING.mdwith 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 separateworkflow_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 rangebefore..aftermay not map cleanly. The workflow may
need to compare trees rather than replaying individual commits.
Activity
[-]RFC: Branching Model Updates[/-][+]RFC: Automatic Branching Model[/+]alganet commentedon Mar 4, 2026
@Respect/troubleshooters @Respect/core everyone is invited to discuss (that is always true, but even more important for RFCs!)
henriquemoody commentedon Mar 4, 2026
Thanks for putting this together, we've been talking about the branching models for a while now.
If I understood you correctly, the problem this RFC is trying to solve is to reduce the manual labor of dealing with multiple patch versions. Is that really correct?
Personally, I don't think we should keep multiple patch versions at all, and just the last one, unless we determine that we'll have long lived versions and also define how long is that -- which I would try to avoid as well. I'm in favor of adding patches only to the last version, and fix forward.
It's good we're talking about this, though, because I assumed that our policy was:
If we move towards the direction of the RFC, we'll indeed handle multiple PATCH versions, and I'm not in favor of that because even with that automation, maintainability will be harder; even with the cherry-pick there's not guarantee we won't have to do manual work because of conflicts.
Apart from that, I have to say I'm not a big fan of merge commits, but I think in the case that we're integrating changes from other branches, I think it's really fair (and maybe better) that we have that explicit in the git tree, so a merge commit doesn't sound bad to me.
Something I'm not sure is about the
mainbranch being force-pushed. Maybe I missed some part of the RFC, but it seems like we're not using it for anything. If it is for us to create the new MAJOR version, we could just create it from the last MINOR version. It doesn't seem like themainbranch is necessary in the world you describe. It's necessary on how we're operating now (based on the assumption I described before), but not in this RFC.alganet commentedon Mar 4, 2026
The goal is to automate. Handling multiple patch versions is what we already do.
Even if we stick to just the latest minor version, there is still stuff to automate:
That's very unlikely if we follow linear history best practices and semantic versioning. It's likely that
composer.lockwill get some conflicts, but that's also easy to automate.This RFC makes this choice easier. If there's anything of value in keeping multiple versions, the burden is lessened by the automations.
Personally, I also prefer keeping just the latest minor version and instantly EOLing the previous ones.
What do we gain from it? If it's informational only, the tagging should be enough.
In this RFC the main is read-only and a token, useless branch. Stuff breaks when you don't have
masterormain, because many systems assume all repositories will have them. We don't actually need it.Please note that backporting stuff from majors (such as dependency and regional updates) is likely to be necessary.
Right now, 2.x is our most popular version. It will take a while for 3.x to catch on. We changed a lot from 2.x to 3.x and right now most stuff is completely incompatible.
However, I would prefer if next major versions were less drastic, and backporting would be possible to ease migration efforts for our users. Some automations for that might be handy.
The kinds of backports are mostly bugfixes (like we did on DateTime) and regional data.
If we settle on not keeping multiple simultaneous minor branches, I can update the RFC to include only the remaining automations.
henriquemoody commentedon Mar 10, 2026
It's not just informational — it's about traceability that tagging alone can't provide. Consider these two commits:
Same change, but after cherry-picking,
git tag --containsshows them as completely independent. That's a real cost.git cherry-pick -xappends the original hash, but it's metadata in the commit message, not something Git understands structurally.git log --ancestry-path,git merge-base, and similar tools still treat them as unrelated. The "Require linear history" rule in the RFC is what forces cherry-picks over merges, so I think we should be explicit: is linear history worth that cost?I understand, and I accept that 2.x still being the most popular version makes backporting a real need right now, not a hypothetical. My concern isn't the automation itself — it's that the RFC's lifecycle model
(Active → Maintained → Security → EOL) implies we know roughly when each phase transitions. "Maintained until next-next minor ships" only works if we have a predictable shipping cadence, which we don't, and I
don't think we should add one. I think the RFC should be explicit that EOL is at maintainer discretion, and document what criteria maintainers should use to make that call.
alganet commentedon Mar 10, 2026
How would
git tag --containstreat a merge commit? Wouldn't that have a completely different hash too?The "Active -> Maintained -> Security -> EOL" is portrayed as an example timeline. It's non-canonical. Just an example on a hypothetical thing that could happen.
The RFC scope is about automating version management. It doesn't enforce any particular timeline or cadence. I mentioned that *personally, I prefer EOLing the previous minor once the new one goes live, but that's also non-authoratitative (not my call to make alone).
What versions are in which state is manually kept by
maintained-versions.json. No automation to change it, always human and intentional.I want to give us tools to manage versions more easily, not decide how we should keep them.
henriquemoody commentedon Mar 11, 2026
Agreed, and we're aligned on EOLing the previous minor once the new one goes live. If you're fine with that, we can make that decision now.
A merge commit gets a new hash, yes, but the original commits don't, and more importantly, they remain reachable via the merge commit's parent pointers. That's what
git merge-base,git log --ancestry-path, and others rely on. Cherry-pick breaks that: the original commit is unreachable from the target branch, so Git treats the two copies as completely unrelated.The RFC's own "linear history" rule means clean forward-ports could be fast-forward pushes (
git merge --ff) instead of cherry-picks (git cherry-pick). Fast-forward moves the branch pointer without creating any new commits, so hashes are fully preserved, the graph stays connected, andmainnever needs to be force-pushed as it would simply fast-forward like any other branch. The only caveat is that if branches diverge at any point (which will probably often happen), fast-forward won't be possible and you're back to a merge commit.I'm super on board with the rest, though. The
maintained-versions.jsonas the source of truth is clean, and automating releases is long overdue! My only remaining concern is the cherry-pick approach for forward-ports, for the reasons above. I guess, if our next major version changes the code too drastically, we would have to use cherry-picks anyways again, so perhaps the merging strategy could also be included onmaintained-versions.json, but I would rather that we avoid using the cherry-pick strategy whenever we can.alganet commentedon Mar 11, 2026
In practice, it means we will always have merge commits in the history. We have lots of generated content, which almost guarantees that divergence will happen. Maybe I'm missing something here, I'm not super familiar with multi-version merge workflows.
I agree that force-pushing main is not ideal. We actually don't need that if we abandon the idea of a read-only mirror and use it as read-write (for latest maintaned minor).
What can we automate right now that doesn't touch these decisions? I'm honestly not comfortable automating merge stuff (I wouldn't even know how to do it), but I can work on the other aspects and perhaps you can contribute the forward automation part.