Skip to content

feat: add merge-report command that sweeps the branch topology for unpropagated fixes - #39

Merged
jfallows merged 3 commits into
developfrom
claude/gitflow-merge-report-66igkl
Jul 20, 2026
Merged

feat: add merge-report command that sweeps the branch topology for unpropagated fixes#39
jfallows merged 3 commits into
developfrom
claude/gitflow-merge-report-66igkl

Conversation

@jfallows

@jfallows jfallows commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #38.

Adds merge-report, a sibling command to the existing changelog generator, that detects commits on one branch whose patch content has no equivalent on another that should have it. This is the reverse of a "backport": a fix that lands on a maintenance branch but never reaches develop becomes a regression for a customer upgrading past that branch.

Rather than taking an explicit source/targets pair, it discovers the branch topology itself and sweeps it in one pass:

  • lists every branch matching the mainline branch (default develop) or a configured support-branch pattern (default support/(\d+)\.x)
  • derives each branch's sources purely from naming/version convention — mainline's sources are every support branch that exists; support/N.x's sources are every support/M.x with M < N; the lowest surviving support branch has none at all and is trivially clean by definition
  • an explicit target (+ optional sources override) still exists for on-demand narrowing, but the default, parameter-free invocation is always the full sweep

This makes the report self-adjusting — cutting a new support branch from develop needs no config change anywhere — and makes a scheduled run and a manual workflow_dispatch run produce identical results, since neither requires the caller to supply the current topology.

  • Detection: git cherry -v <target> <source> (patch-id comparison), not ancestry diffing — a cherry-picked/re-landed commit under a new sha is correctly recognized as already propagated, avoiding the false-positive flood a plain git log target..source produces.
  • Filtering: reuses the same exclude-labels config and cached, event-sourced Driver entries the changelog command already reads — no second API surface. excludedShas() answers "is this sha's PR/issue labeled to be skipped" independent of enhancement/bug categorization.
  • Human override: a checked-in, sha-keyed .gitflow-changelog-merge-ignore.yml for residual cases the label sweep can't resolve — sha-keyed rather than a commit-message convention, since message conventions aren't consistent enough to filter on reliably.
  • Report shape: one section per discovered branch (including an explicit "nothing to check" / "nothing outstanding" state, not an omission), everything unfiltered within a section, sorted oldest-first by commit age. fail-on-outstanding-after-days (default 14) governs pass/fail only, never what's rendered.
  • Deployment constraint: GitHub's schedule trigger always runs the workflow file on the repo's default branch — no branch-selection equivalent to workflow_dispatch. So this has to be one workflow living on the mainline branch, not duplicated per branch. git cherry doesn't need a branch checked out, only present locally, so a single fetch-depth: 0 checkout plus an explicit git fetch origin '+refs/heads/*:refs/remotes/origin/*' (actions/checkout's default fetch refspec only brings full history for the one ref it checks out, even at fetch-depth: 0) is enough to sweep the whole topology in one job.
  • Shipping: a sibling GitHub Action (merge-report/action.yml + src/merge-report-action.ts, built to merge-report/index.js the same way dist/index.js is) and a merge-report CLI subcommand added to the existing cli.ts without changing its default (no-subcommand) behavior.

Changes

  • src/topology.ts: pure computeTopology() — no git involved, just branch names/versions in, {target, sources}[] out
  • src/git.ts: unmatchedCommits(), commitDate(), listBranches(), resolveRef() (prefers origin/<name>, falls back to the bare name for a plain local clone)
  • src/drivers/github.ts: excludedShas()
  • src/merge-ignore.ts: sha-keyed ignore-list loader
  • src/merge-report.ts: discovers topology, sweeps every (target, source) pair, filters, age-sorts
  • src/render/merge-report.ts: one markdown section per branch
  • src/repo-config.ts + src/merge-report-config.ts: mainline-branch/support-branch-pattern policy (alongside the shared exclude-labels)
  • src/merge-report-cli.ts + src/cli.ts: merge-report subcommand
  • src/merge-report-action.ts: writes output-path, writes a GitHub Actions job summary, fails the run past the age threshold
  • merge-report/action.yml: sibling action manifest
  • package.json, .gitignore, .github/workflows/release.yml: build + release wiring for merge-report/index.js, mirroring dist/
  • README.md: ## merge-report section, including the required git fetch step

Test plan

  • npm run typecheck passes
  • npm test — 195 tests passing across 18 files, including:
    • test/topology.test.ts: computeTopology — mainline gets every support branch, support/N.x gets only lower-numbered peers, lowest branch gets none, numeric (not lexicographic) version sort, self-adjusts when a branch is removed
    • test/git-cherry.test.ts: unmatchedCommits/commitDate (from the original commit) + listBranches/resolveRef — dedupes local vs. origin/-prefixed, prefers origin/<name> when present, falls back to the bare name otherwise
    • test/github-driver.test.ts: excludedShas label-intersection behavior
    • test/merge-ignore.test.ts: sha-keyed ignore-file parsing
    • test/merge-report.test.ts: end-to-end against real git fixtures — genuine gap vs. already-cherry-picked, full-topology sweep (mainline sees every support branch, support/N.x sees only lower peers), lowest branch trivially clean, target alone vs. target+sources override, exclude-label filtering, merge-ignore filtering, oldest-first sort
  • npm run build succeeds for all three entrypoints (dist/cli.js, dist/index.js, merge-report/index.js)
  • Note: npm run lint fails on this branch the same way it fails on develop (missing eslint.config.js, an ESLint 9 migration gap) — confirmed pre-existing, unrelated to this change, left out of scope.

Detects commits on a source branch (e.g. support/1.x) whose patch content
has no equivalent on one or more target branches (develop, or a peer
support/* branch) -- catching the case where a bugfix lands on a
maintenance branch but never reaches develop, which becomes a regression
for a customer upgrading past that branch.

Uses git cherry -v (patch-id comparison) rather than ancestry diffing, so a
cherry-picked/re-landed commit under a new sha is correctly recognized as
already propagated. Filters candidates against the same exclude-labels
config and cached, event-sourced Driver entries the changelog command
already reads (dependencies, wontfix, etc. -- no second API surface), plus
a checked-in, sha-keyed .gitflow-changelog-merge-ignore.yml for residual
human-judgment cases. Reports everything, unfiltered, sorted oldest-first;
fail-on-outstanding-after-days (default 14) governs pass/fail without
touching what's shown.

Ships as a sibling action (merge-report/action.yml) and a `merge-report`
CLI subcommand, sharing git.ts/cache/Driver infrastructure with the
existing changelog generator.

Closes #38.
@jfallows
jfallows force-pushed the claude/gitflow-merge-report-66igkl branch from c5cb975 to a06244e Compare July 20, 2026 20:21
…ce/targets pair

Replace the source/targets shape with auto-discovery: list every branch
matching the mainline branch (default develop) or a configured
support-branch pattern (default support/(\d+)\.x), then derive each
branch's sources purely from naming/version convention -- mainline's
sources are every support branch that exists, support/N.x's sources are
every support/M.x with M < N, and the lowest surviving support branch has
none at all and is trivially clean by definition.

This makes the report self-adjusting: cutting a new support branch from
develop needs no config change anywhere, and a scheduled run and a manual
workflow_dispatch run produce identical results since neither requires the
caller to supply the current topology. An explicit target (+ optional
sources override) still exists for on-demand narrowing.

Adds src/topology.ts (pure computeTopology, no git involved) and
git.ts's listBranches/resolveRef (prefers origin/<name>, falls back to the
bare name for a plain local clone). The renderer now produces one section
per discovered branch, including an explicit "nothing to check"/"nothing
outstanding" state rather than omitting a clean branch.

Per the GitHub Actions constraint that a schedule trigger always runs the
workflow file on the repo's default branch (no branch-selection equivalent
to workflow_dispatch), this only works as one workflow living on the
mainline branch -- README updated with the required
`git fetch origin '+refs/heads/*:refs/remotes/origin/*'` step, since
actions/checkout's default fetch refspec only brings full history for the
one ref it checks out, even at fetch-depth 0.

Updates #38 to match.
@jfallows jfallows changed the title feat: add merge-report command for cross-branch fix propagation feat: add merge-report command that sweeps the branch topology for unpropagated fixes Jul 20, 2026
commitDate's %cI format renders a UTC commit date as trailing "Z" on the
CI runner's git version and "+00:00" locally — both are valid ISO 8601,
so the assertion should accept either instead of assuming one.
@jfallows
jfallows merged commit 9db1f01 into develop Jul 20, 2026
1 check passed
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.

Add a merge-report command: sweep the branch topology for fixes that never propagated

2 participants