Skip to content

Work lands often and small, and what is published is master and only master

Work lands often and small, and what is published is master and only master

majordomus://rule/project.land-and-publish@1active processintegrationdeploymentevidence

Rationale

Eight sessions worked on this repository in one afternoon and almost nothing reached the public. The site served version 0.3.1 with no changelog while master carried three releases' worth of change, seven pull requests stood open, and an integration branch cut at 17:52 was stale by 18:00. None of it was a disagreement about the work. Every part of it was a mechanical failure that nobody could see from inside their own branch, and each was measured rather than argued:

  • The conflicts were not conflicts. Two pull requests were marked CONFLICTING on the same ten files, and ten of the ten were merge=derived. Not one was a source file. This repository has scripts/merge-derived and it resolves them; GitHub cannot run a custom merge driver, so the server reports conflicts that exist on nobody's machine. A local merge of master took one of those pull requests to zero conflicts with nothing resolved by hand. A previous integration here went from thirty-three conflicts to zero the same way.
  • The driver is per clone. A worktree that never ran just derive-merge-driver fights those same files locally too, which is why they appeared to fight everyone.
  • The toolchain is per machine. A derive builds the executable, so a clone with no pinned Rust toolchain refuses every prs repair --apply after the merge, naming cargo and nothing else. majordomus doctor checks it (derive-toolchain); install it with rustup before repairing or deriving there.
  • A batch costs more than its conflicts. Every conflicted merge needs a full just derive before the pre-commit hook accepts it, and any source fix made after that derive invalidates it. One session ran three derives for one merge.
  • The public site drifted from the repository. At 17:46 UTC it served a commit from an unmerged branch — the result of site-deploy --any-ref, which documents itself as a preview, pointed at production by two different sessions, this one included.
  • A merge broke the publication and nothing said so. A change to a document's shape that was never rendered locally shipped a page the site generator could not build, the pages run failed, and the public site silently stayed on the previous commit.
  • Green and not-yet-reported looked identical. No validation run on master completed for over four hours while master carried two defects that broke every branch merging it.

Required behaviour

  1. Land often and small. A branch merges to master as soon as it is green, one at a time. An integration branch that batches many is a last resort for a backlog that already exists, is declared as such, and is not the pattern afterwards.
  2. Merge master locally, then push, before asking for review. Run just derive-merge-driver once per worktree. Resolve source conflicts, fix the build, then derive once, then commit — a derive before the last source fix is a derive done twice. A derived file is never resolved by hand: it is regenerated, because a hand-resolved derived file passes the merge and fails derive-check one commit later. The server's view does not change until the local merge is pushed. majordomus prs repair <n> (it also takes the branch name) is that whole gesture as one command of the integrator, in a scratch worktree that never touches the branch's own. It is eligible only when the integrator's classification says the pull request lacks master and its merge conflicts on no path outside the merge=derived set; it refuses and names any authored conflict rather than deciding it. Without --apply it only says what it would do. With it, it holds the integration lease, records the act on the trail first, merges the trunk in where the driver exists, derives, commits and pushes the branch leased on the head it observed, so a branch somebody pushed to meanwhile is refused. It exists because a ten-minute manual gesture performed several times a day is one that gets skipped under pressure, and a branch nobody brings master into is a branch somebody merges without reading.
  3. A mechanical union is wrong across generations. Taking both sides is right for a list that gained entries and wrong when one side is an older generation of the same code: folding one branch that way reintroduced a second match arm built against a previous shape, and seven compile errors with it.
  4. Publish master and only master. scripts/site-deploy --any-ref is a local preview. The commit gh-pages names is an ancestor of master, always.
  5. The merger checks what the public is served. Equality is not the test — a merge landing during a deploy window leaves the published commit an ancestor rather than the head, with nothing wrong. The merger confirms, within a bounded wait, that the site is serving the head they merged, using scripts/pages verify --url … --commit "$(git rev-parse …)" rather than a second comparison written by hand, and never an abbreviated SHA: that comparison is string equality against forty characters, so a short one burns the whole timeout and then reports a mismatch that is not one. A failure names both the commit expected and the commit served.
  6. The deploy belongs to the merge. One owner per merge, so that two sessions do not publish over one another.
  7. A shape change is rendered before it is merged. scripts/site-build && scripts/site-check on the head, before the merge, whenever a change touches a document's shape or the templates that render it. The pages workflow does not re-run those checks, so nothing between the merge and the public catches it.
  8. A merge that changes the public contract carries its version. Landing without the bump is why the published site named a version three releases behind the code.

Failure behaviour

scripts/ci/pages-check, registered as the pages-live gate, decides the published half. Four things, because publication has failed in four ways here: the commit gh-pages names must be an ancestor of master; master must not have moved past it, on a path that can change the site, for longer than the deploy window; the commit the live pages name must be the one gh-pages published — the failure a token-authored push causes, where no workflow starts at all and every check on the tree stays green; and GitHub's own build of gh-pages must not have errored, which is the half of publishing this repository does not own and which no workflow of ours goes red for.

The second of those is clause 5 made mechanical, and it is the one that was missing. Ancestry alone was a one-way check: a publication that is on master is on master however old it is, so on 2026-09-10 the gate said ok all afternoon while the site sat hours behind. The window is what separates the deploy in flight that clause 5 excuses from the deploy that is not coming; it is thirty minutes, sized from a twenty-five-minute Actions queue observed that day, and a commit that cannot change the site owes no publication at all — the paths that can are scripts/pages paths, which is also the publication workflow's own trigger.

It runs in the full plan beside installer-live, never in a path-triggered one: a live-network gate on every push fails collectively when an origin is slow, and a gate that cries wolf is waived.

The rest is decided by review and by the record on the shared server's board. A gate cannot tell that a branch waited for a batch, and inventing one would be a worse cure than the disease.

Verification

test/cases/104_published_site.sh holds scripts/ci/pages-check to what it claims, against fixture repositories rather than the network: a publication from the trunk is accepted, one from a branch that is not on it is refused with the cause and the remedy named, a trunk that has moved past the publication is accepted while the deploy is young and refused once it is older than the window — both over one fixture, so the judgement is the age and not the tree — a deploy commit that does not say where it came from is refused as unauditable rather than guessed at, a measurement the gate could not make is reported as a note and never as an ok, and the gate carries neither a URL nor an identity path of its own — both are read from the declarations that already hold them. The case was proved non-vacuous by removing the refusal from the gate and watching it fail.

The live half is not simulated: a case standing up an HTTP server would be testing its own fixture, and one reaching the real site would fail whenever the network did. --offline is the seam, and the case asserts the seam announces itself rather than skipping silently.

test/cases/97_pages_fast_path.sh holds the publication path itself, including its concurrency: the pages- group serialises the runs and nothing cancels the deploy in flight (cancel-in-progress: false; cancelling it starved publication on 2026-09-12), and the run states whether it pushed gh-pages before it ended, because a cancelled run is not a failure and fourteen of the seventeen cancelled runs of 2026-09-09/10 had already published. The derived-merge-driver behaviour is test/cases/57_derived_merge_driver.sh, and test/cases/911_repair_decides_and_moves_nothing.sh and test/cases/912_repair_acts_under_the_lease_and_the_trail.sh hold majordomus prs repair to clause 2, against a scripted forge and a local bare origin. A pull request conflicting only on derived files is eligible and one conflicting on an authored file is refused with the files named; the default dry run reaches no forge, writes no trail and moves no ref; an applied repair records its attempt before it pushes, holds the lease while it works, pushes a merge of the trunk leased on the observed head and never touches master; and a branch that moved meanwhile is refused and left as its author pushed it. The Rust tests of integration::repair hold the same decisions and the order of the trail without a repository.

Where this comes from

canonical file
.ai/repo/rules/project/land-and-publish.v1.md
identity
project.land-and-publish@1
also served as
MCP resource majordomus://rule/project.land-and-publish@1 · majordomus entity show rule/project-land-and-publish-1 · GET /api/v1/entity?uri=majordomus://rule/project.land-and-publish@1
in the Cockpit
/cockpit/objects/rule/project-land-and-publish-1

This page is generated from the file above by scripts/generate-site-data; editing the page changes nothing, editing the file changes every surface. The relations above are edges of docs/generated/graph.json, which the executable writes from the same index.

Searches the titles and descriptions of every page this site publishes.