The pull request as the unit of change, and a changelog built on it #77
Labels
No labels
area:docs
area:identity
area:ops
area:plugin
area:server
channel:community
channel:direct
channel:owned
channel:press
channel:social
e2ee-constrained
gate:at-ga
gate:pre-ga
marketing
parity
relay:absent
relay:planned
relay:requested
relay:supported
risk:additive
risk:contract
risk:none
usability
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Nectenda/nectenda#77
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
Six plugin versions have shipped to a public directory and a user cannot find
out what changed in any of them.
docs/releasing.md:134says "Every pluginrelease bumps
manifest.jsonwith a changelog line";scripts/bump-plugin-version.mjstouchesmanifest.json,versions.jsonandpackage.jsonand writes no changelog. The claim is false, in the same way theREADME's install section was.
Designing the fix surfaced the real gap. Every automated changelog worth copying
is built on a unit of change that carries prose — Kubernetes on the pull
request, GitLab on the commit, Forgejo's own
release-notes-assistanton thepull request. This repository appeared to have none, only a stream of commits.
It does have one, unrecognised: the worktree. One plan, one worktree, one
branch. Four are on
originas this is written. Each is a coherent story with abeginning and an end. It is already the unit of change — it is simply not
addressable.
The decision
One plan → one worktree → one branch → one pull request → fast-forward onto
main.The workflow in
CLAUDE.mddoes not change shape. It gains a merge event.What this fixes beyond the changelog
1. The gate moves in front of the merge.
e2eandpromoteare bothif: github.ref == 'refs/heads/main'today. The comment gives the reasoning —"the two minutes of real Obsidian are spent on what is about to be promoted."
Under pull requests the merge is what is about to be promoted, so the same
reasoning puts the gate on the PR, where failure means "do not merge" rather
than "
mainis already broken and the deploy has been stopped after the fact."2. It costs less CI, not more — measured. Every PR currently triggers CI
twice:
pushandpull_requestfire on the same sha, confirmed on eightdistinct shas; the repo has 317
pushruns against 15pull_requestruns.Narrowing
push:tomainremoves the duplicate. A branch with no PR then getsno CI, which is deliberate — opening a PR is how you ask for it. And
e2emovesfrom once per push to
mainto once per PR: with 41 commits landing in a singleday, consolidating those into one PR is a reduction in real-Obsidian runs.
3.
CLAUDE.md's honour-system rule becomes enforceable. There are zeroprotected branches on this repository. "Never commit to
maindirectly" iscurrently a request, and
CLAUDE.mdrecords a session that broke it — threecommits of in-progress work straight onto the deployment trigger, then a
completion report that never checked CI. That document calls the outcome "luck
rather than method." Branch protection turns it into an impossibility.
What has to change
DefaultMergeStylemergefast-forward-only(AllowFastForwardOnlyis already true) — keeps history linearmainci.ymlon:push:all branches,pull_request:push: branches:[main],pull_request:e2e/promoteref == maine2eon PR and main;promotemain onlyOne thing must be proven before anything depends on it. Of the 15 pull
requests this repository has ever had — all Renovate — not one has been
merged; every one was closed. The review path is exercised, the merge path
has never run here. Specifically unknown: whether Forgejo 16.0.5 records
merged_commit_idunder fast-forward-only merging, which the changelog needsto map a PR to its commits. If it does not, fall back to a
Closes: #NNtraileror to rebase-merge.
The changelog on top
Capture — a release note on the pull request. Kubernetes' mechanism, which
Forgejo's own tool also reads. A
## Changelogblock in the PR body, orNONEwhere there is no user-facing change. A PR carrying several distinct changes uses
a
release-notes/<PR>.mdfile in the branch, one line each —release-notes-assistant's convention, adopted verbatim so that door stays open.Enforced at the PR, not at release, the way Kubernetes uses
do-not-merge/release-note-label-needed. Nothing merges undocumented, so thechangelog cannot silently omit a change.
Routing needs no new taxonomy.
area:plugin,area:server,area:identity,area:docsandarea:opsalready exist and already decide which ledger an entryjoins.
Cut.
scripts/changelog.mjs draft --product plugin --since v0.1.5readsmerged PRs whose merge commit is an ancestor of
HEADand not ofv0.1.5,filters by
area:*, and takesrelease-notes/<PR>.md, else the## Changelogblock, else the PR title — into
## Unreleasedindocs/changelog-plugin.md,every line carrying its PR number. It also reports orphans: commits in range
reachable from no counted PR.
Verified readable without the broken API —
tea api404s on everything here,including
tea api get user, buttea pulls ls --fields index,title,body,labels --output jsonreturns full bodies.Review, then derive. The draft is a file in the worktree; you edit it like
any other file. That is the review window and it needs no bespoke UI.
cutstamps the heading, and one ledger then derives three published copies: the
mirror's
CHANGELOG.md, the GitHub release notes, and/changelogon the site.The LLM is a checker, not an author. The documented risk is that it "can both
omit changes and add ones that don't exist, both breaking the changelog as a
source of truth." A checker cannot invent an entry, and a false positive costs a
glance.
The server
CD does not change.
promotekeeps moving:stableon every greenmain.A server version is a tag on a sha already in production — a name for a point
in time, not a gate in front of one.
State plainly, because it is the opposite of the plugin: the server changelog
is retrospective. Its
## Unreleasedmeans "live but unnamed", not "comingsoon", and the published page must not imply otherwise.
Two ledgers rather than one is forced, not chosen:
build-mirror.mjs'sFORBIDDENscan rejects any staged file containingpackages/server,plan-phaseorpositioning.md, so a combined changelog could not be publishedat all.
Costs and objections
tea pulls createreplaces the merge. It buys the gate running beforemain.where they should be hit.
and applied some other way; under branch protection that path disappears.
release-notes-assistantis not adopted outright, despite being Forgejo'sown tool: it produces one output, defaults to conventional-commit
categorisation we do not use, and has no notion of two products in one
repository. Its two best ideas are taken. Revisit once the workflow is proven.
Children
In dependency order. #78 blocks everything else — nothing should be built on
a merge path that has never run here.
mainci.yml: narrow push to main, move the e2e gate onto the pull requestscripts/changelog.mjs, the two ledgers, and the 0.1.0-0.1.5 backfillCHANGELOG.mdinto the published mirror/changelogonto nectenda.com#82 also deletes
docs/releasing.md:134's false claim that a changelog linealready exists.
Done on 23 September 2026. Every child is closed, and all of the code is on
mainwith CI green and deployed:5f5a0dbwas promoted andeu1serves it.The workflow
merged_commit_id(the head tip) andmerge_base, and REST exposes both. No trailer fallback was needed.mainonly, with no more double runs. e2e is the gate on the PR and runs again onmainfor promote. Since #121, e2e starts alongside build, which cut a PR gate from about 10 min to about 5½.release-notecheck that fails an untouched template.mainis protected: no direct pushes (admins included), three required checks, outdated branches blocked, fast-forward-only by default. Renovate merges through PRs.The changelog
docs/changelog-plugin.mdanddocs/changelog-server.mdare the ledgers.scripts/changelog.mjs draft|cutworks from merged PRs, by commit graph and never by date, and reports orphans afterWORKFLOW_EPOCH=ea78644. The anchor tagsplugin-v0.1.0…0.1.5andserver-v1.0.0are pushed.CHANGELOG.md.--releasetagsplugin-v<ver>here.Found and fixed along the way
concurrencywas never enforced. They are now isolated per run (#118): a port block with a sentinel, a per-checkout TMPDIR, a scopedclean.sh, and Xvfb-nolisten local. That was proven 3-wide in CI and 2-wide on a Mac. The Auto Feature Builder's plan counted this as its phase 5.scripts/ci-capacity.mjsand a clconsole load monitor (a PSI timer). The monitor only logs until a Kuma push URL is configured.paths:-only filters fire on them.e2e.ymlnow excludes tags.Open, and not part of this
docs/sync-limitations.md.draft --assist(LLM-suggested prose) is deferred, as the issue allowed.