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
10 changes: 9 additions & 1 deletion .github/scripts/keepalive_gate.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const AGENT_LABEL_PREFIX = 'agent:';
const MAX_RUNS_PREFIX = 'agents:max-runs:';
const SYNC_REQUIRED_LABEL = 'agents:sync-required';
const ACTIVATED_LABEL = 'agents:activated';
const PAUSE_LABEL = 'agents:pause';
Comment on lines 7 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Detect pause label using correct name

The new pause handling checks for agents:pause, but the rest of the keepalive stack uses the agents:paused label (see scripts/keepalive-runner.js line 483 and the paused fixture in tests/workflows/fixtures/keepalive/paused.json). With the name mismatch, PRs already marked agents:paused will not be treated as paused by this gate, so keepalive runs continue even though the pause label is present.

Useful? React with 👍 / 👎.

const DEFAULT_RUN_CAP = 1;
const MIN_RUN_CAP = 1;
const MAX_RUN_CAP = 5;
Expand Down Expand Up @@ -901,6 +902,7 @@ async function evaluateKeepaliveGate({ core, github, context, options = {} }) {
headRef: '',
hasSyncRequiredLabel: false,
hasActivatedLabel: false,
hasPauseLabel: false,
requireHumanActivation: false,
activationComment: null,
gateStatus: { found: false, success: false, status: '', conclusion: '' },
Expand Down Expand Up @@ -934,6 +936,7 @@ async function evaluateKeepaliveGate({ core, github, context, options = {} }) {
headRef: '',
hasSyncRequiredLabel: false,
hasActivatedLabel: false,
hasPauseLabel: false,
requireHumanActivation: false,
activationComment: null,
gateStatus: { found: false, success: false, status: '', conclusion: '' },
Expand All @@ -948,6 +951,7 @@ async function evaluateKeepaliveGate({ core, github, context, options = {} }) {
const labels = Array.isArray(pr?.labels) ? pr.labels : [];
const labelNames = extractLabelNames(labels);
const hasKeepaliveLabel = labelNames.includes(KEEPALIVE_LABEL);
const hasPauseLabel = labelNames.includes(PAUSE_LABEL);
const hasActivatedLabel = labelNames.includes(ACTIVATED_LABEL);
const hasSyncRequiredLabel = labelNames.includes(SYNC_REQUIRED_LABEL);
const agentAliases = extractAgentAliases(labels);
Expand Down Expand Up @@ -1018,7 +1022,10 @@ async function evaluateKeepaliveGate({ core, github, context, options = {} }) {
let reason = 'ok';
let pendingGate = false;

if (hasSyncRequiredLabel) {
if (hasPauseLabel) {
ok = false;
reason = 'keepalive-paused';
} else if (hasSyncRequiredLabel) {
ok = false;
reason = 'sync-required';
} else if (!hasKeepaliveLabel) {
Expand Down Expand Up @@ -1085,6 +1092,7 @@ async function evaluateKeepaliveGate({ core, github, context, options = {} }) {
primaryAgent,
headSha,
headRef,
hasPauseLabel,
lastGreenSha: gateSucceeded ? headSha : '',
hasSyncRequiredLabel,
hasActivatedLabel,
Expand Down
74 changes: 18 additions & 56 deletions .github/scripts/keepalive_orchestrator_gate_runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,17 @@ async function runKeepaliveGate({ core, github, context, env }) {
}
};

if (!preGate.ok) {
addReason(preGate.reason || 'pre-gate-failed');
summary
.addRaw(
`Pre-gate check failed: reason=${preGate.reason || 'unknown'} ok=${preGate.ok ? 'true' : 'false'}`
)
.addEOL();
} else if (preGate.pendingGate) {
summary.addRaw('Gate pending; keepalive will retry once gate concludes.').addEOL();
}

let headSha = '';
if (!pr) {
addReason('missing-pr');
Expand All @@ -183,30 +194,17 @@ async function runKeepaliveGate({ core, github, context, env }) {
.filter(Boolean)
);

if (currentLabels.has('agents:pause')) {
addReason('keepalive-paused');
summary.addRaw('Keepalive paused by agents:pause label.').addEOL();
}

Comment on lines +197 to +201

Copilot AI Dec 21, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pause label check here is redundant with the pre-gate evaluation. The evaluateKeepaliveGate function already checks for the pause label and returns hasPauseLabel in its result (and sets ok=false with reason='keepalive-paused' when the label is present). This reason has already been added at line 167 via addReason(preGate.reason || 'pre-gate-failed'). Consider removing this duplicate check and instead use preGate.hasPauseLabel if needed, or rely on the pre-gate check result that's already being processed at lines 166-175.

Suggested change
if (currentLabels.has('agents:pause')) {
addReason('keepalive-paused');
summary.addRaw('Keepalive paused by agents:pause label.').addEOL();
}

Copilot uses AI. Check for mistakes.
const requiredLabels = ['agents:keepalive'];
if (agentAlias) {
requiredLabels.push(`agent:${agentAlias}`);
}
const missingLabels = requiredLabels.filter((label) => !currentLabels.has(label));

Copilot AI Dec 21, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable missingLabels is computed but never used. Since the auto-labeling logic has been removed, this line should be deleted as well to avoid unnecessary computation and improve code clarity.

Suggested change
const missingLabels = requiredLabels.filter((label) => !currentLabels.has(label));

Copilot uses AI. Check for mistakes.

if (missingLabels.length) {
try {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: missingLabels,
});
summary.addRaw(`Applied keepalive labels to PR #${prNumber}: ${missingLabels.join(', ')}`).addEOL();
for (const label of missingLabels) {
currentLabels.add(label);
}
} catch (labelError) {
const message = labelError instanceof Error ? labelError.message : String(labelError);
summary.addRaw(`Failed to apply keepalive labels to PR #${prNumber}: ${message}`).addEOL();
}
}

const unresolvedLabels = requiredLabels.filter((label) => !currentLabels.has(label));
if (unresolvedLabels.length) {
unresolvedLabels.forEach((label) => addReason(`missing-label:${label}`));
Expand Down Expand Up @@ -304,50 +302,14 @@ async function runKeepaliveGate({ core, github, context, env }) {

}

const currentAssignees = (pr.assignees || []).map((assignee) => assignee?.login).filter(Boolean);
const humanAssignees = (pr.assignees || [])
.filter((assignee) => isAssignable(assignee))
.map((assignee) => assignee.login)
.filter(Boolean);

if (!humanAssignees.length) {
const candidateLogins = [];
const author = pr.user;
if (isAssignable(author)) {
candidateLogins.push(author.login);
}
for (const reviewer of pr.requested_reviewers || []) {
if (isAssignable(reviewer)) {
candidateLogins.push(reviewer.login);
}
}

const uniqueCandidates = [];
const seen = new Set();
for (const login of candidateLogins) {
const normalised = login.toLowerCase();
if (!seen.has(normalised)) {
seen.add(normalised);
uniqueCandidates.push(login);
}
}

if (uniqueCandidates.length) {
try {
await github.rest.issues.addAssignees({
owner,
repo,
issue_number: prNumber,
assignees: uniqueCandidates,
});
summary.addRaw(`Assigned human owners to PR #${prNumber}: ${uniqueCandidates.join(', ')}`).addEOL();
} catch (assignmentError) {
const message = assignmentError instanceof Error ? assignmentError.message : String(assignmentError);
summary.addRaw(`Failed to assign humans to PR #${prNumber}: ${message}`).addEOL();
}
} else {
summary.addRaw(`No human assignees available for PR #${prNumber}; continuing without assignment.`).addEOL();
}
addReason('no-human-assignee');
summary.addRaw(`No human assignees available for PR #${prNumber}; skipping keepalive.`).addEOL();
}
}

Expand Down
Loading