♻️ Let create:branch start from an ahead main

`create:finish` leaves the merge local until `create:release` pushes it, so
`main` is routinely ahead of its upstream between a merge and a release. The
branch front door demanded an exact match and refused until it was pushed,
forcing a manual `git push` that the model deliberately keeps out of it.

Reject only a `main` that is behind its upstream — matching `create:finish`,
which already tolerates being ahead — and record why pushing from `finish` was
rejected instead. The push stays with `create:release`, so the merge remains
reviewable locally, and the known issue about the deadlock is gone.

Resolves: backlog task "Resolve the finish/push tension".
This commit is contained in:
tmu committed 2026-09-15 22:14:51 +00:00
1 parent 177d63fa95
commit d646fd1ec4
5 files changed
+21 -18

No files matched your search

+1
View File
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
- require a short summary under `[Unreleased]` in the changelog before a branch is finished - require a short summary under `[Unreleased]` in the changelog before a branch is finished
- let `create:branch` start from a `main` that is ahead of its upstream, so a finished merge no longer blocks the next branch until it is pushed
## [0.1.6] - 2026-09-15 ## [0.1.6] - 2026-09-15
+5 -5
View File
@@ -184,9 +184,10 @@ request workflow on Gitea yet.
- **Branch naming:** `feature/<desc>` / `fix/<desc>` / `chore/<desc>` - **Branch naming:** `feature/<desc>` / `fix/<desc>` / `chore/<desc>`
- **Starting work:** `npm run create:branch -- <prefix>/<desc>`. It refuses, - **Starting work:** `npm run create:branch -- <prefix>/<desc>`. It refuses,
without changing anything, unless the working tree is clean, no without changing anything, unless the working tree is clean, no
merge/rebase/cherry-pick is in progress, `main` matches its upstream, and merge/rebase/cherry-pick is in progress, `main` is not behind its upstream
`npm run test` is green on `main`. The prefix is _your_ call, inferred from the (a local merge not yet pushed is fine — the push belongs to `create:release`),
task; the script validates it rather than guessing it. and `npm run test` is green on `main`. The prefix is _your_ call, inferred from
the task; the script validates it rather than guessing it.
- **Merging:** `npm run create:finish` (on the branch). It re-asserts the same - **Merging:** `npm run create:finish` (on the branch). It re-asserts the same
preconditions, merges `--no-ff`, runs `npm run verify`, and deletes the branch preconditions, merges `--no-ff`, runs `npm run verify`, and deletes the branch
only after the merge is green. The push is left to `create:release`, so the only after the merge is green. The push is left to `create:release`, so the
@@ -196,8 +197,7 @@ request workflow on Gitea yet.
- **Releases are NOT triggered by pushes.** Only the maintainer triggers a - **Releases are NOT triggered by pushes.** Only the maintainer triggers a
release; see [Publishing](#publishing). release; see [Publishing](#publishing).
Full rationale, including the front-door decisions and a known issue about Full rationale, including the front-door decisions:
`main` being ahead of its upstream between a merge and the next push:
[development/workflow.md § Branching model](./development/workflow.md#branching-model). [development/workflow.md § Branching model](./development/workflow.md#branching-model).
## Submitting changes ## Submitting changes
+1 -1
View File
@@ -29,7 +29,7 @@ Documentation:
☐ Validate code fences in Markdown (start with README.md) — compile the TypeScript examples against `src/` so the docs cannot drift from the API ☐ Validate code fences in Markdown (start with README.md) — compile the TypeScript examples against `src/` so the docs cannot drift from the API
Workflow: Workflow:
☐ Resolve the finish/push tension: `create:finish` leaves `main` ahead of its upstream while `create:branch` refuses until `main` matches upstream — decide whether `finish` should push or `branch` should compare only `BEHIND` (see development/workflow.md) ✔ Resolve the finish/push tension: `create:finish` leaves `main` ahead of its upstream while `create:branch` refuses until `main` matches upstream — decide whether `finish` should push or `branch` should compare only `BEHIND` (see development/workflow.md) @done
Maintenance: Maintenance:
☐ Serve CI coverage over a tiny self-hosted webserver (replace the zip artifact) @low ☐ Serve CI coverage over a tiny self-hosted webserver (replace the zip artifact) @low
+7 -7
View File
@@ -26,7 +26,10 @@ plus hand-written `git`.
gate is not paid on an ineligible tree. gate is not paid on an ineligible tree.
- The merge half owns the post-merge `npm run verify`, so a merge cannot land - The merge half owns the post-merge `npm run verify`, so a merge cannot land
unverified. The push stays with `create:release` so the merge is reviewed unverified. The push stays with `create:release` so the merge is reviewed
locally first. locally first; `main` is therefore routinely ahead of its upstream between a
merge and the release that ships it. `create:branch` requires only that `main`
is not _behind_ — matching `create:finish`, which tolerates the local merge and
fast-forwards over a remote one — rather than an exact match.
- Every failure is non-mutating except the baseline test, which runs on `main` - Every failure is non-mutating except the baseline test, which runs on `main`
after switching there: a red `main` restores the branch you started on, and a after switching there: a red `main` restores the branch you started on, and a
merge conflict aborts back to the feature branch rather than stranding a merge conflict aborts back to the feature branch rather than stranding a
@@ -42,12 +45,9 @@ plus hand-written `git`.
can run from a dirty tree. can run from a dirty tree.
- Fast-forward instead of `--no-ff`: `--no-ff` keeps each unit of work visible - Fast-forward instead of `--no-ff`: `--no-ff` keeps each unit of work visible
in `git log`. in `git log`.
- Pushing from `create:finish` to keep `main` level with its upstream: it would
#### Known issue trade the local review the push waits for for a network side effect, and a
failed push would leave the merge landed but unpublished.
- `create:finish` does not push, so `main` is ahead of `origin/main` between a
merge and the next push. `create:branch` requires `main` to match its upstream
and refuses until it is pushed; push `main` before starting the next branch.
## Changelog notes ## Changelog notes
+7 -5
View File
@@ -86,12 +86,14 @@ if [ -n "${UPSTREAM}" ]; then
echo " refusing to branch on a possibly stale '${BASE}'." >&2 echo " refusing to branch on a possibly stale '${BASE}'." >&2
exit 1 exit 1
} }
# Only *behind* is a problem. `create:finish` deliberately leaves the local
# merge on '${BASE}' until `create:release` pushes it, so being ahead is the
# normal state between a merge and the release that ships it; branching from
# those commits is intended. Missing remote commits is not.
BEHIND=$(git rev-list --count "${BASE}..${UPSTREAM}") BEHIND=$(git rev-list --count "${BASE}..${UPSTREAM}")
AHEAD=$(git rev-list --count "${UPSTREAM}..${BASE}") if [ "${BEHIND}" -ne 0 ]; then
if [ "${BEHIND}" -ne 0 ] || [ "${AHEAD}" -ne 0 ]; then echo "error: '${BASE}' is behind '${UPSTREAM}' (by ${BEHIND})." >&2
echo "error: '${BASE}' has diverged from '${UPSTREAM}' (ahead ${AHEAD}, behind ${BEHIND})." >&2 echo " update it: git switch ${BASE} && git pull --ff-only" >&2
[ "${AHEAD}" -ne 0 ] && echo " not yet pushed commits on '${BASE}': push them, or rebase this work onto them." >&2
[ "${BEHIND}" -ne 0 ] && echo " update it: git switch ${BASE} && git pull --ff-only" >&2
exit 1 exit 1
fi fi
else else