Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ configured sandbox. Before each landing, the daemon merges the base into the PR
head with the quest merge driver and verifies the tested tree, then pushes that
head with a lease and squash-merges it through GitHub. If branch permissions
refuse the push, it fast-forwards the base to the tested squash with a lease and
closes the PR with a linked-commit comment instead. Confirmed conflicts, moved heads, or a tree mismatch eject the
remaining batch. Transient refusals are retried; a racing base rebuilds the queue; already merged PRs stay merged. Failed batches are ejected together, with
closes the PR with a linked-commit comment instead. A confirmed conflict parks only
that PR for the fixer, and a moved head or a prepared tree mismatch ejects only that PR; the
rest of the batch stays queued. Transient refusals are retried; a racing base rebuilds the queue; already merged PRs stay merged. Failed batches are ejected together, with
a log tail in each comment and the full log retained on the host. The optional
credential-free `quest land fix` container makes one mechanical attempt per
maintainer pick. The daemon validates and lease-pushes fixes, records the new
Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -637,11 +637,16 @@ tested squash to the base as a fast-forward, leased on the expected parent. It
closes the PR with a comment linking the landed commit and removes `quest:merge`.
Such a fork PR is closed rather than shown as merged by GitHub. A PR branch lease
failure ejects `head_moved`. A locally prepared or API squash tree mismatch logs
CRITICAL and ejects the remaining batch as `tree_mismatch`.
CRITICAL. A PR whose prepared tree differs ejects alone as `tree_mismatch`; an
already-squashed one stays merged. Either way, the rest of the batch stays queued
and is rebuilt and checked on the new base.

After an API refusal, the daemon re-reads mergeability and briefly retries
pending mergeability or a base refresh. A confirmed conflict parks only that PR for
the fixer and a moved head ejects only that PR; the rest stays queued. A persistent policy refusal ejects as `merge_refused` without the
pending mergeability or a base refresh. It also retries while GitHub still
reports the pin as the head of a PR it just prepared, since GitHub can refuse the
merge for that stale head; only another writer's head ejects `head_moved`.
A confirmed conflict parks only that PR for the fixer and a moved head ejects
only that PR; the rest stays queued. A persistent policy refusal ejects as `merge_refused` without the
fixer. Already-merged PRs are skipped. An unexpected API squash parent means the
base moved: the remainder is rebuilt without ejection.

Expand Down
1 change: 0 additions & 1 deletion quest/m0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ ownership-manifest design:
- [Merge through a host gate](/quest/m0/host-land/README.md) - inline merge picks, small batches landed from the maintainer's host, and a Codex fixer instead of bisection
- [The land fixer handles every mechanical failure](/quest/m0/land-fixer/README.md) - attempts, registry network, triage, flaky tests, local mergeability, protected edits, and fork commits
- [Landing priority](/quest/m0/land-priority.md) - cheap checks wait a bounded time and the land daemon's check runs ahead of agent builds
- [Accurate head_moved](/quest/m0/land-head-moved.md) - the gate's own base-merge push and batch-mate failures are not reported as a moved head
- [Drop emptied Required headings](/quest/m0/required-heading.md) - a clean driver merge that empties a `Required` list passes `quest check`
- [GitHub retries](/quest/m0/github-retry.md) - one 5xx or dropped connection no longer fails a command or land step
- [Current conflict reports](/quest/m0/land-conflicts-refresh.md) - a queued PR's conflict report updates when the queue ahead of it changes
Expand Down
29 changes: 0 additions & 29 deletions quest/m0/land-head-moved.md

This file was deleted.

49 changes: 14 additions & 35 deletions src/cli/land-serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,16 +347,15 @@ async function mergePinned(github: GitHub, path: string, head: string, previous
if (pull.merged === true) return null;
const detail = `GitHub refused ${path}: ${String(result.message)}`;
if (pull.state !== "open") return { reason: "merge_refused", detail };
if (
previous !== head &&
response.status === 409 &&
(object(pull.head).sha === previous || object(pull.head).sha === head)
) {
// Right after the gate's own push, GitHub can still report the pin as the head and refuse the merge
// for it: a 409, or a 405 when the pin conflicts with the base that push merged. Neither is a writer.
const current = object(pull.head).sha;
if (previous !== head && (current === previous || (response.status === 409 && current === head))) {
if (attempt >= 3) throw new Error(`${detail}; prepared head has not propagated`);
await github.pause(500 * 2 ** attempt);
continue;
}
if (object(pull.head).sha !== head) return { reason: "head_moved", detail };
if (current !== head) return { reason: "head_moved", detail };
if (pull.mergeable === false || pull.mergeable_state === "dirty") return { reason: "merge_conflict", detail };
if (pull.mergeable === null || /base branch was modified/i.test(String(result.message))) {
if (attempt >= 3) throw new Error(`${detail}; mergeability did not settle`);
Expand Down Expand Up @@ -647,8 +646,8 @@ export async function serveBatch(
}
if ("reason" in result) {
// Only this PR failed to land. The rest of the batch passed its check and stays queued,
// to be rebuilt on the new base, except after a tree mismatch, which stops them all.
const detail = `${result.detail}; the rest of the batch was not merged`;
// to be rebuilt on the new base.
const detail = `${result.detail}; the rest of the batch stays queued`;
appendFileSync(log, `${detail}\n`);
if (result.reason === "merge_conflict") {
output.push(
Expand All @@ -664,22 +663,11 @@ export async function serveBatch(
);
return output;
}
const remaining = result.reason === "tree_mismatch" ? chain.links.slice(index) : [link];
if (result.reason === "tree_mismatch") deps.warn(`CRITICAL: ${detail}`);
// A prepared tree that differs from the tested one is the gate's own fault, for a maintainer to judge.
const reported = result.reason === "tree_mismatch" ? `CRITICAL: ${detail}` : detail;
if (reported !== detail) deps.warn(reported);
return output.concat(
await stop(
root,
options,
deps,
batch,
remaining.map(({ entry }) =>
ejection(
entry,
result.reason,
excerpt(result.reason === "tree_mismatch" ? `CRITICAL: ${detail}` : detail),
),
),
),
await stop(root, options, deps, batch, [ejection(link.entry, result.reason, excerpt(reported))]),
);
}
const sha = result.sha;
Expand All @@ -697,20 +685,11 @@ export async function serveBatch(
expectedParent = sha;
const tree = string(object(commit.tree).sha);
if (tree === link.tree) continue;
const detail = `CRITICAL: GitHub squash #${link.entry.number} at ${sha} has tree ${tree}, but the tested chain has ${link.tree}. Already merged PRs stay merged; the remaining batch was stopped`;
// The squash is already merged, so there is nothing to eject. The rest of the batch stays queued and
// is rebuilt and checked on the base as it now is, like after a moved base.
const detail = `CRITICAL: GitHub squash #${link.entry.number} at ${sha} has tree ${tree}, but the tested chain has ${link.tree}. It stays merged; the rest of the batch stays queued, to be rebuilt on the new base`;
deps.warn(detail);
appendFileSync(log, `${detail}\n`);
const remaining = chain.links.slice(index + 1);
if (remaining.length)
output.push(
...(await stop(
root,
options,
deps,
batch,
remaining.map(({ entry }) => ejection(entry, "tree_mismatch", excerpt(detail))),
)),
);
return output;
}
return output;
Expand Down
2 changes: 1 addition & 1 deletion src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ export function main(argv: string[], out: Output): number | Promise<number> {
landCommand
.command("serve")
.description(
"Run one daemon per repository. As a batch is filled, park alone for the fixer (eject alone, without one) each PR it reaches whose squash onto the base fails quest check with findings the base lacks, and comment once on each newly queued PR that conflicts with the four PRs landing soonest ahead of it. Check a base PR plus up to floor(log2 n) cleanly merging PRs once through the sandbox, alternating the oldest and the newest queued PR as the base. A failed batch is never bisected: its PRs are parked for the fixer, which blames the ones at fault, or ejected together without a fixer. It halves the next batch's size down to one PR; each landed batch grows it back by one. When the fixer blames none and the base fails its check alone, the queue halts until a moved base passes, and the fixer proposes a fix of the base as a PR. Requires quest on PATH and flock. Sandbox and fixer wrappers MUST run code as a different user from the App key owner (for example in nspawn containers); environment filtering alone cannot isolate /proc or host files. The sandbox receives <checkout> <base-sha>, with the base also in QUEST_LAND_BASE and present in the standalone shallow clone; the optional tracked background fixer receives the log path followed by PR numbers, or none for a base that fails alone. Neither receives credentials. State retains logs, fixer jobs, and one mechanical attempt per maintainer pick across restarts. Fixer results are validated, lease-pushed, recorded in Review, and pinned for requeue, and the comment a result carries for its PR is posted there; invalid results, crashes, and timeouts are handed back. A validated fix whose push GitHub refuses is handed back with its resolution for `quest land apply`. After revalidating the queue, merge the base into each behind-base PR head with quest resolve, verify the tested tree, and lease-push that head before the pinned GitHub squash API merge. A permissions-refused branch push falls back to a leased fast-forward of the tested squash to the base, closing the PR with a linked-commit comment and removing quest:merge. A recorded daemon merge atop the original pin is recognized after restart; other moved heads require maintainer requeueing. Pending mergeability is retried briefly; a confirmed conflict parks only that PR for the fixer and a moved head ejects only that PR, and the rest of the batch stays queued. A refusal that would repeat, such as squash being disabled, ejects as merge_refused without the fixer. An unexpected squash parent rebuilds the queue without ejecting; a tree mismatch logs CRITICAL and ejects the rest as tree_mismatch. Merged PRs drop out of the queue after restart.",
"Run one daemon per repository. As a batch is filled, park alone for the fixer (eject alone, without one) each PR it reaches whose squash onto the base fails quest check with findings the base lacks, and comment once on each newly queued PR that conflicts with the four PRs landing soonest ahead of it. Check a base PR plus up to floor(log2 n) cleanly merging PRs once through the sandbox, alternating the oldest and the newest queued PR as the base. A failed batch is never bisected: its PRs are parked for the fixer, which blames the ones at fault, or ejected together without a fixer. It halves the next batch's size down to one PR; each landed batch grows it back by one. When the fixer blames none and the base fails its check alone, the queue halts until a moved base passes, and the fixer proposes a fix of the base as a PR. Requires quest on PATH and flock. Sandbox and fixer wrappers MUST run code as a different user from the App key owner (for example in nspawn containers); environment filtering alone cannot isolate /proc or host files. The sandbox receives <checkout> <base-sha>, with the base also in QUEST_LAND_BASE and present in the standalone shallow clone; the optional tracked background fixer receives the log path followed by PR numbers, or none for a base that fails alone. Neither receives credentials. State retains logs, fixer jobs, and one mechanical attempt per maintainer pick across restarts. Fixer results are validated, lease-pushed, recorded in Review, and pinned for requeue, and the comment a result carries for its PR is posted there; invalid results, crashes, and timeouts are handed back. A validated fix whose push GitHub refuses is handed back with its resolution for `quest land apply`. After revalidating the queue, merge the base into each behind-base PR head with quest resolve, verify the tested tree, and lease-push that head before the pinned GitHub squash API merge. A permissions-refused branch push falls back to a leased fast-forward of the tested squash to the base, closing the PR with a linked-commit comment and removing quest:merge. A recorded daemon merge atop the original pin is recognized after restart; other moved heads require maintainer requeueing. Pending mergeability, and GitHub still showing the pin after the daemon's own push, are retried briefly; a confirmed conflict parks only that PR for the fixer and a moved head ejects only that PR, and the rest of the batch stays queued. A refusal that would repeat, such as squash being disabled, ejects as merge_refused without the fixer. An unexpected squash parent rebuilds the queue without ejecting; a tree mismatch logs CRITICAL, ejects only a PR whose prepared tree differs as tree_mismatch (an already squashed PR stays merged), and leaves the rest of the batch queued to be rebuilt on the new base. Merged PRs drop out of the queue after restart.",
)
.option("--base <branch>", "Base branch (default: repository default)")
.option("--state <path>", "Private daemon state directory (default: $STATE_DIRECTORY or the XDG state home)")
Expand Down
Loading
Loading