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
7 changes: 4 additions & 3 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ import {
MAX_REVIEW_NAG_COOLDOWN_DAYS,
isProtectedAutomationAuthor,
planAgentMaintenanceActions,
resolveNullableLabel,
type AgentActionPlanInput,
type AgentDispositionLabelSettings,
type PlannedAgentAction,
Expand Down Expand Up @@ -14319,7 +14320,7 @@ async function closeReviewEvasionSelfCloseIfActive(
() => undefined,
);
}
const label = settings.reviewEvasionLabel === null ? null : (settings.reviewEvasionLabel ?? DEFAULT_REVIEW_EVASION_LABEL);
const label = resolveNullableLabel(settings.reviewEvasionLabel, DEFAULT_REVIEW_EVASION_LABEL);
if (label !== null) {
await ensurePullRequestLabel(env, installationId, repoFullName, pr.number, label, { createMissingLabel: true }).catch(() => undefined);
}
Expand Down Expand Up @@ -14468,7 +14469,7 @@ async function closeReviewEvasionDraftConversionIfActive(
() => undefined,
);
}
const label = settings.reviewEvasionLabel === null ? null : (settings.reviewEvasionLabel ?? DEFAULT_REVIEW_EVASION_LABEL);
const label = resolveNullableLabel(settings.reviewEvasionLabel, DEFAULT_REVIEW_EVASION_LABEL);
if (label !== null) {
await ensurePullRequestLabel(env, installationId, repoFullName, pr.number, label, { createMissingLabel: true }).catch(() => undefined);
}
Expand Down Expand Up @@ -14632,7 +14633,7 @@ async function closeRepeatedDraftCyclingIfDetected(
() => undefined,
);
}
const label = settings.reviewEvasionLabel === null ? null : (settings.reviewEvasionLabel ?? DEFAULT_REVIEW_EVASION_LABEL);
const label = resolveNullableLabel(settings.reviewEvasionLabel, DEFAULT_REVIEW_EVASION_LABEL);
if (label !== null) {
await ensurePullRequestLabel(env, installationId, repoFullName, pr.number, label, { createMissingLabel: true }).catch(() => undefined);
}
Expand Down
24 changes: 16 additions & 8 deletions src/settings/agent-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ export const AGENT_LABEL_NEEDS_REVIEW = "manual-review";
// recurring failure mode separately from an ordinary guardrail hold.
export const AGENT_LABEL_MIGRATION_COLLISION = "migration-collision";

// #label-scoping (#4618): every per-repo configurable close/hold label shares this shape -- explicit `null`
// means "act without any label" (an operator opt-out), `undefined` (never configured) falls back to the
// operator-facing default below. Factored out of ~9 independently hand-copied ternaries of the exact same
// idiom across this file and queue/processors.ts. PURE.
export function resolveNullableLabel(configured: string | null | undefined, fallback: string): string | null {
return configured === null ? null : (configured ?? fallback);
}

// Maintainer-managed automation accounts whose PRs are never auto-closed. A recurring accumulator (e.g.
// github-actions[bot] opening automation/readme-refresh) or a dependency PR must not be killed by a duplicate
// or slop heuristic — the maintainer owns its lifecycle. (reviewbot wrongly auto-closed such an accumulator,
Expand Down Expand Up @@ -439,11 +447,11 @@ type ResolvedAgentDispositionLabels = {

function resolveAgentDispositionLabels(settings: AgentDispositionLabelSettings): ResolvedAgentDispositionLabels {
return {
manualReview: settings.manualReviewLabel === null ? null : (settings.manualReviewLabel ?? AGENT_LABEL_NEEDS_REVIEW),
readyToMerge: settings.readyToMergeLabel === null ? null : (settings.readyToMergeLabel ?? AGENT_LABEL_READY),
changesRequested: settings.changesRequestedLabel === null ? null : (settings.changesRequestedLabel ?? AGENT_LABEL_CHANGES),
migrationCollision: settings.migrationCollisionLabel === null ? null : (settings.migrationCollisionLabel ?? AGENT_LABEL_MIGRATION_COLLISION),
pendingClosure: settings.pendingClosureLabel === null ? null : (settings.pendingClosureLabel ?? AGENT_LABEL_PENDING_CLOSURE),
manualReview: resolveNullableLabel(settings.manualReviewLabel, AGENT_LABEL_NEEDS_REVIEW),
readyToMerge: resolveNullableLabel(settings.readyToMergeLabel, AGENT_LABEL_READY),
changesRequested: resolveNullableLabel(settings.changesRequestedLabel, AGENT_LABEL_CHANGES),
migrationCollision: resolveNullableLabel(settings.migrationCollisionLabel, AGENT_LABEL_MIGRATION_COLLISION),
pendingClosure: resolveNullableLabel(settings.pendingClosureLabel, AGENT_LABEL_PENDING_CLOSURE),
};
}

Expand Down Expand Up @@ -630,7 +638,7 @@ export function planAgentMaintenanceActions(input: AgentActionPlanInput): Planne
// #label-scoping: this label is inseparable metadata on the close below, so it rides on `close` autonomy,
// NOT the generic `label` class — a repo can enable close without also opting into the broad label dial.
// Explicit `null` (vs. absent/undefined) means "close without any label."
const label = input.blacklistLabel === null ? null : (input.blacklistLabel ?? DEFAULT_BLACKLIST_LABEL);
const label = resolveNullableLabel(input.blacklistLabel, DEFAULT_BLACKLIST_LABEL);
// Close is pushed BEFORE its coupled label (#label-close-split-brain) so the executor's outcome-correlation
// guard always has the close's outcome already recorded by the time it evaluates the label.
if (acting("close")) {
Expand Down Expand Up @@ -658,7 +666,7 @@ export function planAgentMaintenanceActions(input: AgentActionPlanInput): Planne
if (input.contributorCapMatch?.matched === true && capContributor) {
const { authorLogin, openCount, cap, itemKind, scope } = input.contributorCapMatch;
// #label-scoping: same close-autonomy-gated, null-clearable shape as the blacklist label above.
const label = input.contributorCapLabel === null ? null : (input.contributorCapLabel ?? DEFAULT_CONTRIBUTOR_CAP_LABEL);
const label = resolveNullableLabel(input.contributorCapLabel, DEFAULT_CONTRIBUTOR_CAP_LABEL);
// Close is pushed BEFORE its coupled label (#label-close-split-brain) — see the closeKind doc comment above.
if (acting("close")) {
actions.push({
Expand All @@ -683,7 +691,7 @@ export function planAgentMaintenanceActions(input: AgentActionPlanInput): Planne
if (input.reviewNagMatch?.matched === true && reviewNagContributor) {
const { authorLogin, pingCount, maxPings } = input.reviewNagMatch;
// #label-scoping: same close-autonomy-gated, null-clearable shape as the blacklist label above.
const label = input.reviewNagLabel === null ? null : (input.reviewNagLabel ?? DEFAULT_REVIEW_NAG_LABEL);
const label = resolveNullableLabel(input.reviewNagLabel, DEFAULT_REVIEW_NAG_LABEL);
// Close is pushed BEFORE its coupled label (#label-close-split-brain) — see the closeKind doc comment above.
if (acting("close")) {
actions.push({
Expand Down