Skip to content

Configurable base/target branches, PR stacking, and the branchflow release-train plugin - #9

Open
UnityChaos wants to merge 5 commits into
mainfrom
claude/configurable-branching
Open

Configurable base/target branches, PR stacking, and the branchflow release-train plugin#9
UnityChaos wants to merge 5 commits into
mainfrom
claude/configurable-branching

Conversation

@UnityChaos

Copy link
Copy Markdown
Member

Summary

Abstracts the plugins away from the hardcoded "branch from origin/main, PR to main" model so they work for repos with staging/develop targets and semver update branches, adds PR stacking, and introduces a new branchflow release-train plugin that maintains and releases those branches.

Fully backwards compatible: with no .claude-workflows.json and no new flags, the existing plugins behave byte-for-byte as before.

1. Configurable base/target branches (issue-workflow, deep-review, triage)

  • New per-repo .claude-workflows.json (read from the target repo root) declares the target branch, an optional major/minor/patch update-branch map, feature-branch prefix, and protected branches.
  • A shared bin/_branch-lib.sh resolver (byte-identical per plugin) produces the base/target with precedence --onto > --target > <PLUGIN>_TARGET_BRANCH > --bump/semver:* label > config > main, persisted to <work-dir>/.branch-meta.json so --resume reuses it.
  • New flags: --target (all three), --bump major|minor|patch and --onto <branch|PR#> (issue-workflow).
  • Stacking: --onto cuts from and targets a parent feature branch so dependent PRs stack and land into the update branch once the feature is complete; integrate/review + the inline trivial-integration path follow GitHub's retarget when the parent PR merges.
  • Skills resolve the target once ("Step 0") and never re-type origin/main; gh pr calls are made current-branch-relative so a custom branchPrefix works.
  • The check-git-branch.sh hook now reads protectedBranches from config (falling back to main master production).

Versions: issue-workflow 1.5.0, deep-review 1.6.0, triage 1.1.0.

2. New branchflow release-train plugin (v0.3.0)

A subcommand CLI — the counterpart to the feature plugins (they route PRs into lines; branchflow maintains and releases them). Merge-only and PR-driven, so it never force-pushes and coexists with strict branch protection. claude is a soft dependency, so it runs in CI.

Two modes: semver (major/minor/patch accumulators, invariant major ⊇ minor ⊇ patch) and develop (one line).

  • init — interactive turnkey setup (smart, repo-detected defaults; --yes accepts all): scaffolds the config, creates the accumulator branches, offers a baseline tag, and (with repo admin) sets merge-commit-only + branch protection via the GitHub API. Idempotent.
  • status [--check] — content-based pending/behind, the invariant matrix, versions. --check gates CI.
  • cascade [<level>] — forward-merge a line into the next up (adjacent, chains under the eager Action); conflicts get a Claude-resolved PR.
  • promote <level|bump> — version-bump release PR into main; refuses (exit 3) on a violated invariant unless --force; --auto-merge or waits for manual merge.
  • reconcile — tag the merged release, merge main down into every line.
  • Optional eager-cascade GitHub Action template.

Monorepos: the semver-accumulator model is single-version by nature, so monorepos use develop mode + delegated versioningrelease.command (run by promote) and release.tagCommand (run by reconcile) hand per-package bumps/tags/changelogs to a dedicated tool (changesets/nx/lerna). init auto-detects monorepos and pre-fills the changesets commands. Parallel maintenance lines are out of scope.

Correctness points hardened during testing: content-based containment (git cherry/patch-id, so per-branch reconcile merges don't create false violations), merge-by-PR-number (reconcile fronts several PRs from main), deterministic status --check exit, and a conflict path that aborts cleanly and never leaves the tree half-merged.

Testing

All shell bash -n-clean; the two triplicated files (_branch-lib.sh, check-git-branch.sh) are byte-identical; all JSON manifests valid. The resolver, hook, and branchflow were exercised with local-repo integration tests (bare-origin + stubbed gh), including a full merge-simulating cascade→promote→reconcile lifecycle, delegated versioning, turnkey init --yes, and monorepo auto-detection.

🤖 Generated with Claude Code

UnityChaos and others added 5 commits July 14, 2026 13:00
…and PR stacking

Abstract the hardcoded "branch from origin/main, PR to main" model so the
plugins work for repos using develop/staging targets, semver update
branches (major/minor/patch), and stacked feature branches. Fully
backwards compatible: with no .claude-workflows.json and no new flags,
behavior is byte-for-byte identical to before.

- bin/_branch-lib.sh (triplicated, byte-identical): resolve_branches()
  reads a per-repo .claude-workflows.json and resolves target/base/prefix
  with precedence --onto > --target > <PLUGIN>_TARGET_BRANCH > --bump /
  semver:* label > config targetBranch > main. write/load_branch_meta
  persist the decision to <work-dir>/.branch-meta.json for --resume.
- Orchestrators: new --target (all), --bump/--onto (issue-workflow); swap
  origin/main / --base main / divergence checks for $WF_TARGET/$WF_BASE_REF;
  banner shows target; stacked-parent-merged retarget in the inline
  trivial-integration path.
- Skills: Step 0 target resolution (never re-type origin/main); doctrine
  reframed to "configured target, never a local branch"; stacking-aware
  PR base/body in plan; retarget-follow in review/integrate; gh pr calls
  made current-branch-relative so a custom branchPrefix works.
- Hook: check-git-branch.sh reads protectedBranches from config, falling
  back to main/master/production.
- Docs: root README, CLAUDE.md, and three plugin READMEs document the
  branching models. Versions: issue-workflow 1.5.0, deep-review 1.6.0,
  triage 1.1.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A fourth plugin that maintains and releases the branches the feature
plugins target -- the counterpart to their routing. Manages semver update
branches (major/minor/patch accumulators) or a single develop line:
cascades lower lines up into higher ones (keeping major ⊇ minor ⊇ patch)
and cuts version-bump release PRs into main. Merge-only and PR-driven --
never force-pushes, so it coexists with strict branch protection.

Commands (bin/branchflow, a subcommand CLI):
- init       create the accumulator branches off the release branch
- status     pending/behind (content-based), the invariant matrix, versions;
             --check exits non-zero on a violated invariant (CI gate)
- cascade    forward-merge a line into the next one up, via PRs; adjacent-only
             so it chains cleanly under the eager Action; conflicts get a
             Claude-resolved PR (or a plain PR when claude is unavailable)
- promote    open a version-bump release PR into main; REFUSES (exit 3) if the
             invariant is violated unless --force; --auto-merge lands it when
             the env allows, else waits for manual merge
- reconcile  tag a merged release, then merge main down into every accumulator

Design details:
- Containment is CONTENT-based (git cherry / patch-id), so per-branch reconcile
  merge commits don't create false "ahead"/violation counts.
- Merges target PR numbers, not head branches (reconcile fronts several PRs
  from main; a head-based merge would be ambiguous).
- Version is bumped only in the release PR (tag-based by default; optional
  version file); the changelog skill drafts notes from the commit range.
- claude is a SOFT dependency (only conflict resolution + changelog use it),
  so branchflow runs in CI; templates/branchflow.yml is an optional Action
  that cascades on accumulator pushes and reconciles on main pushes.
- Conflict path aborts any in-progress merge and cleans up, never leaving the
  working tree half-merged.

bin/_branchflow-lib.sh holds the config/version/invariant/merge primitives;
hooks are the byte-identical push-guard shared with the other plugins; the
plugin is registered in the marketplace and documented in its README plus the
root README/CLAUDE.md. Version 0.1.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
init now scaffolds and bootstraps a repo instead of assuming the config
already exists (which its own docstring wrongly claimed it did). It reads
the repo, prompts with smart defaults (--yes accepts all), and is idempotent.

Steps:
- Detect the default branch and a version file (package.json / Cargo.toml /
  pyproject.toml / ...). If no branching model is configured, prompt for
  trains-vs-single, branch names, release branch, tag prefix, version file,
  and changelog; merge the result into .claude-workflows.json (preserving any
  existing keys) and commit it to the release branch so the accumulators
  inherit it.
- Create the accumulator branches off the release branch.
- Offer a baseline version tag when versioning from tags and none exist.
- With repo admin, set merge-commit-only (disable squash & rebase) + enable
  auto-merge, and require PRs on the release + accumulator branches via gh api
  -- the squash-disable being the setting that otherwise silently breaks the
  cascade. Skipped gracefully (with a warning) without admin or a remote.
- Offer to drop in the eager-cascade Action.

New interactive helpers (ask / ask_yn / bf_detect_version_file); version 0.2.0.
Docs updated in the plugin README and root CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename the two branching-model keywords user-facing and internally:
'trains' -> 'semver' (the major/minor/patch accumulator model) and
'single' -> 'develop' (the one-line model, default branch develop). Updates
BF_MODE values, the init prompt/default, all mode comparisons, and the docs
(plugin README + CLAUDE.md). No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The semver-accumulator model is single-version by nature, so it can't
describe a monorepo where packages version independently. Add a delegation
hook so branchflow owns the branch/PR/tag-push mechanics while a dedicated
tool (changesets / nx / lerna / release-please) owns per-package versions.

- New config: release.command (run by promote inside the release branch to
  bump per-package versions + changelogs) and release.tagCommand (run by
  reconcile to create per-package tags).
- promote: when release.command is set, skip the built-in bump/changelog,
  run the command, commit whatever it changed (detected via git status, so
  new files count too), and open the release PR. In develop mode no bump arg
  is needed. Aborts cleanly if the command fails or produces no changes.
- reconcile: when release.tagCommand is set, run it against the merged
  release in a throwaway worktree and push the tags (idempotent), instead of
  the single-tag path.
- status: shows "Versioning: delegated to release.command (per-package)".
- init: detects monorepos (workspaces / .changeset / nx.json / lerna.json),
  defaults to develop mode, and pre-fills the changesets commands.

Parallel maintenance lines (patch 1.x while 2.0 develops) remain out of
scope. Docs: README Example D + config rows; CLAUDE.md. Version 0.3.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant