MergeGate Merge Outcome¶
Issue2104, plan slice C9a. This change is live for
every repo that MergeGate merges. The release hold (C9b, live for dashecorp/dashe-website only) and the signed
release route (C9c) build on it. For every other repo, when MergeGate decides to merge did not change in C9a. Since
rc#2053, the guard also requires Review-E's approval at the head, in every
repo, and pins the PUT to that head.
Merge() returns a MergeOutcome¶
MergeOutcome is a pure Core value. MergeGate.Merge(repo, pr)
(source) is now internal and returns one:
Path in Merge() |
Outcome | Reason |
HttpStatus |
|---|---|---|---|
LOCATION is set and the multi-location fence refuses the repo's tenant (rig-conductor#2561). Checked first, before the guard: no GitHub call, nothing cleared |
Deferred |
location_fenced: <reason>, e.g. location_fenced: mapped_elsewhere |
– |
No GITHUB_TOKEN |
Failed |
no_token |
– |
| The guard says not ready: a check has not passed, or review-e-bot's latest verdict does not approve the head (rc#2053) | Deferred |
the guard's reason: pending: build, review_not_at_head, review_lookup_failed, or two of them joined by ; |
– |
| GitHub no longer reports the PR open (merged or closed since the trigger; rc#1976) | Deferred |
pr_not_open (no MERGE_GATE_WAITING, nothing cleared) |
– |
| The head moved between the guard's read and the title fetch (rc#2053) | Deferred |
head_moved_during_merge |
– |
The GET /pulls/{pr} title fetch fails |
Failed |
pr_fetch_failed |
GitHub's status |
The squash PUT /pulls/{pr}/merge fails (it sends sha = the verified head, so a head that moved is 409) |
Failed |
merge_put_failed |
GitHub's status, such as 405 or 409 |
| The squash PUT throws (no answer) | Failed |
merge_put_failed |
– (merged: and merging: are freed, as for a refused PUT) |
| GitHub merged | Merged |
– | – (Sha is the squash SHA, or null if GitHub omitted it) |
Held and Deferred("merging") for an approved task PR come from the operator release hold (C9b,
details). Since rc#2053, review_not_at_head and
review_lookup_failed come from the guard for every PR, before the hold runs. Every outcome other than Merged carries
a non-blank reason. A guard defer emits MERGE_GATE_WAITING with the missing conditions (CI, review). It clears only
the fields GitHub showed to be stale: ci-passed when a check has not passed, and review-approved when Review-E's
verdict does not approve the head. Unreadable reviews clear nothing
(table). A failed PUT still deletes the pre-set merged:
key.
MERGE_GATE_MERGED only after a real merge¶
The all-conditions path in SetConditionAndTryMerge used to emit MERGE_GATE_MERGED before it called Merge(),
so a guard defer or a refused PUT still showed "merge gate — all conditions met". It now emits the event only when
Merge() returns Merged, after MERGED and ISSUE_*. The other two callers (no required checks, and the 30 s CI
timeout) never emitted it and still do not, nor does the rc#2425 recheck. The rc#2365 sweep (RetryAsync) runs the all-conditions path and emits it the same way, only after Merged. The event has no projection; it feeds the Discord line and re-render.
An unresolved dashecorp repo coalesces to tenant-0 (B1)¶
BindTenantFromRepoAsync now sets ctx.TenantId = TenantMatch.CoalesceInDefaultOrg(repo, resolved): the resolved
tenant, else tenant-0 for a repo in the dashecorp org (TenantMatch.DefaultGithubOrg), the same coalesce as the GitHub
webhook in Program.cs, whose repos are pre-filtered to that org. Before, a null resolution left the context blank. The
MERGED write after a successful PUT then threw UnattributedTenantException (TenantContextGuard.RequireTenant), and
the merge was never recorded. A resolved tenant binds exactly as before, and a blank answer still fails closed. An
unresolved repo in any other org also stays blank and fails closed (rc#1691): POST /api/merge passes a caller-supplied,
unfiltered repo into SetConditionAndTryMerge, and such a merge must never be recorded under invotek. All four MergeGate scopes
use it (EmitEventAsync, NotifyPendingCiStarted, CompletePendingAfterCi, Merge). DuplicateCloseService has
its own copy of the bind and is unchanged.
The coalesce matches the webhook's only while both resolve the same tenant. MergeGate resolves with the repo alone, while
the webhook also passes the delivery's installation id and org, so an unpinned repo can land in two tenant databases once
another active tenant carries that installation id or shares the org. For the task repo this splits HUMAN_GATE_TRIGGERED
from REVIEW_PASSED. The fix is operational: pin the repo before such a tenant is activated (see
the routes' precondition).
Restoring several conditions at once (rig-conductor#2562)¶
SetConditionsAndTryMerge(repo, pr, conditions) sets every condition, then evaluates once, as SetConditionAndTryMerge
(which now calls it) does after one. The activation reconcile sweep uses it to restore ci-passed and review-approved
from GitHub when a tenant becomes active at a new location (multi-location fencing).
It is still only a trigger: Merge() checks the fence and the guard as for any other caller.
Post-merge bookkeeping is guarded¶
Once GitHub has merged, the MERGED write, the dup-close Task.Run, the main-CI lookup with its pending-done: key,
and ISSUE_IN_DEPLOY or ISSUE_DONE run inside one try/catch. A failure is logged as merged, but post-merge
bookkeeping failed, the outcome stays Merged, and merged: stays set. An unreadable merge response is Merged(null).
Tests¶
MergeOutcomeTests cover the value.
MergeGateTests is the first MergeGate harness: an NSubstitute
IConnectionMultiplexer, a stub HttpMessageHandler, an event store that binds the tenant like the Marten write
session, and one ordered log of GitHub calls and appends. The token comes through an internal constructor, so the
tests never set the process-global GITHUB_TOKEN. Cases:
- merged, guard-deferred, PUT 405, PR fetch 502 and no token, each with its outcome and side effects;
- MERGE_GATE_MERGED after the PUT and after MERGED, and never on a 405 or a defer;
- an unresolved repo records MERGED and ISSUE_DONE under invotek, and a resolved repo keeps its tenant;
- a throwing event store still returns Merged and keeps merged:;
- (rc#2053) every non-approval review shape, the /api/merge path, unreadable and paginated reviews, a head that moved
after the guard, and the pinned PUT.
Watch after deploy¶
Before C9b merges, watch one real MergeGate merge on any repo. Confirm in the Activity tab or /api/events/recent
that it still records MERGED and that MERGE_GATE_MERGED now follows it.