Skip to content
Open
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
31 changes: 31 additions & 0 deletions pkg/github/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,37 @@ func (ic *InstallationClient) CreateIssueComment(ctx context.Context, repo strin
return comment.GetID(), comment.GetNodeID(), nil
}

// HasIssueCommentWithMarker reports whether any of the PR's most recent
// comments contains the given marker string. Callers use it to make
// at-least-once comment posting idempotent: a hidden HTML marker in the body
// identifies the comment, and a retry that finds the marker skips the
// re-post. Only the newest page of comments is inspected — the markers
// callers search for are posted moments before the search, so an older
// occurrence beyond the page means a duplicate is possible but harmless.
func (ic *InstallationClient) HasIssueCommentWithMarker(ctx context.Context, repo string, pr int, marker string) (bool, error) {
owner, repoName := splitRepo(repo)
comments, err := retryGitHubUnavailableRead(ctx, ic.logger, "list issue comments", []any{"repo", repo, "pr", pr}, func(ctx context.Context) ([]*gh.IssueComment, error) {
list, _, callErr := ic.client.Issues.ListComments(ctx, owner, repoName, pr, &gh.IssueListCommentsOptions{
Sort: new("created"),
Direction: new("desc"),
ListOptions: gh.ListOptions{PerPage: 100},
})
if callErr != nil {
return nil, classifyGitHubAPIError(callErr)
}
return list, nil
})
if err != nil {
return false, fmt.Errorf("list issue comments for %s#%d: %w", repo, pr, err)
}
for _, comment := range comments {
if strings.Contains(comment.GetBody(), marker) {
return true, nil
}
}
return false, nil
}

// GetIssueComment returns the current body of an existing PR/issue comment.
func (ic *InstallationClient) GetIssueComment(ctx context.Context, repo string, commentID int64) (string, error) {
owner, repoName := splitRepo(repo)
Expand Down
15 changes: 15 additions & 0 deletions pkg/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -1520,6 +1520,10 @@ const (
// MergeGateSourceSweep marks a request recorded by the backstop sweep
// over recently completed applies.
MergeGateSourceSweep = "sweep"
// MergeGateSourceReleaseSweep marks a settle request backfilled by the
// sweep over terminal applies whose preflight held sibling checks but
// whose settle was never recorded.
MergeGateSourceReleaseSweep = "release_sweep"
)

// RecordMergeGateRecorded counts durable merge gate requests recorded
Expand Down Expand Up @@ -1602,3 +1606,14 @@ func RecordMergeGateTerminatedStuck(ctx context.Context, terminated int64) {
"Total merge gate requests terminated by the stuck-processing sweep", "{request}",
)
}

// RecordMergeGatePreflightRearmed counts terminally failed preflight renders
// re-armed because their apply is still active. A sustained rate means the
// code host keeps rejecting the hold rendering (outage, auth failure) while
// applies run on stored holds — sibling PRs' visible checks stay stale until
// a render lands, so find the failing render in the merge gate logs.
func RecordMergeGatePreflightRearmed(ctx context.Context, reopened int64) {
addCounterN(ctx, reopened, "schemabot.merge_gate.preflight_renders_rearmed_total",
"Total terminally failed preflight renders re-armed for still-active applies", "{request}",
)
}
11 changes: 11 additions & 0 deletions pkg/webhook/check_runs.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,17 @@ var schemaChangedReplanFailedBlock = checkBlockReason{
message: "The live schema for this database changed after this plan was computed, and SchemaBot could not re-plan the PR against it. Re-run `schemabot plan` (or push a new commit) before this check can pass; see server logs for the re-plan failure.",
}

// applyInFlightBlock is used while an apply is running against a check's
// target: the stored verdict was computed against the pre-apply live schema,
// so a merge must not land on it before the apply finishes. The preflight
// fan-out writes it before the apply's engine work starts, and the apply's
// settle fan-out re-plans the check against the resulting schema, which
// replaces this hold with a live verdict.
var applyInFlightBlock = checkBlockReason{
blockingReason: "apply_in_flight_on_target",
message: "An apply is currently changing this database's live schema, so this check is held until it finishes. SchemaBot then re-plans this PR against the resulting schema and refreshes this check automatically.",
}

// noAllowedConfiguredEnvironmentsBlock is used when schema files changed but
// the server-configured environments for the database do not overlap this
// service's allowed_environments. SchemaBot cannot safely plan the schema
Expand Down
5 changes: 0 additions & 5 deletions pkg/webhook/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,11 +363,6 @@ func NewHandlerWithDispatch(service *api.Service, ghClients github.ClientSet, we
return nil
}

// Wake the merge gate processor as soon as a drive tail records a
// request, so sibling PR checks re-plan without waiting for the next
// poll tick. The durable request row stays the source of truth: a
// kick lost to a pod boundary only costs poll latency.
service.OnMergeGateRecorded = h.KickMergeGate
}

return h
Expand Down
Loading
Loading