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
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ function createGithub(options = {}) {
combinedStatus = { state: 'success', statuses: [] },
comments = [],
graphqlError,
graphqlUnavailable = false,
} = options;
const calls = {
labelAdds: [],
Expand Down Expand Up @@ -113,15 +114,17 @@ function createGithub(options = {}) {
async paginate() {
return comments;
},
async graphql(query, variables) {
__calls: calls,
};
if (!graphqlUnavailable) {
github.graphql = async function graphql(query, variables) {
calls.graphql.push({ query, variables });
if (graphqlError) {
throw graphqlError;
}
return { markPullRequestReadyForReview: { pullRequest: { number: pull?.number || 17, isDraft: false } } };
},
__calls: calls,
};
};
}
return github;
}

Expand Down Expand Up @@ -435,10 +438,55 @@ test('runKeepaliveGate converts checklist-complete draft PRs to ready for review
assert.equal(outputs.reason, '');
assert.equal(github.__calls.graphql.length, 1);
assert.equal(github.__calls.graphql[0].variables.pullRequestId, 'PR_node_17');
const readyMutation = github.__calls.graphql[0].query;
assert.match(readyMutation, /mutation MarkPullRequestReadyForReview\(\$pullRequestId: ID!\) \{/);
assert.equal(
(readyMutation.match(/\{/g) || []).length,
(readyMutation.match(/\}/g) || []).length
);
assert.match(readyMutation, /\n\}$/);
assert.ok(summary.entries.some((entry) => entry.text?.includes('marked ready for review')));
restore();
});

test('runKeepaliveGate distinguishes unavailable GraphQL client from missing PR node id', async () => {
const { core, outputs, summary } = createCore();
const gateStub = async () => createGateResult();
const { runKeepaliveGate, restore } = loadRunnerWithGate(gateStub);

const pr = makePullRequest({
draft: true,
labels: ['agents:keepalive', 'agent:codex'],
body: '- [x] Implementation complete\n- [x] Verified behavior',
});
const github = createGithub({
pull: pr,
graphqlUnavailable: true,
});

await runKeepaliveGate({
core,
github,
context: { repo: { owner: 'octo', repo: 'demo' }, runId: 44 },
env: makeEnv({ KEEPALIVE_MAX_RETRIES: '5' }),
});

assert.equal(outputs.proceed, 'false');
assert.equal(outputs.reason, 'pr-draft-ready-failed');
assert.equal(github.__calls.graphql.length, 0);
assert.ok(
summary.entries.some((entry) =>
entry.text?.includes('GitHub GraphQL client is unavailable')
)
);
assert.ok(
!summary.entries.some((entry) =>
entry.text?.includes('missing GraphQL PR node id')
)
);
restore();
});

test('runKeepaliveGate routes incomplete draft PRs to human', async () => {
const { core, outputs } = createCore();
const gateStub = async () => createGateResult();
Expand Down
26 changes: 14 additions & 12 deletions .github/scripts/keepalive_orchestrator_gate_runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ const PAUSE_LABEL = 'agents:paused';
const NEEDS_HUMAN_LABEL = 'needs-human';
const NEEDS_ATTENTION_LABEL = 'agent:needs-attention';
const DRAFT_DISPOSITION_MARKER = '<!-- keepalive-draft-disposition -->';
const MARK_PULL_REQUEST_READY_FOR_REVIEW_MUTATION = `mutation MarkPullRequestReadyForReview($pullRequestId: ID!) {
markPullRequestReadyForReview(input: {pullRequestId: $pullRequestId}) {
pullRequest {
number
isDraft
}
}
}`;

function normaliseLabelName(label) {
if (!label) {
Expand Down Expand Up @@ -105,23 +113,17 @@ async function addLabelsIfMissing({ github, owner, repo, prNumber, labels, curre

async function markDraftReadyForReview({ github, pr, core, summary }) {
const nodeId = String(pr?.node_id || '').trim();
if (!nodeId || typeof github.graphql !== 'function') {
if (!nodeId) {
summary.addRaw('Draft PR could not be converted automatically: missing GraphQL PR node id.').addEOL();
return false;
}
if (typeof github.graphql !== 'function') {
summary.addRaw('Draft PR could not be converted automatically: GitHub GraphQL client is unavailable.').addEOL();
return false;
}

try {
await github.graphql(
`mutation($pullRequestId: ID!) {
markPullRequestReadyForReview(input: {pullRequestId: $pullRequestId}) {
pullRequest {
number
isDraft
}
}
}`,
{ pullRequestId: nodeId }
);
await github.graphql(MARK_PULL_REQUEST_READY_FOR_REVIEW_MUTATION, { pullRequestId: nodeId });
summary.addRaw('Draft PR had no unchecked checklist items; marked ready for review.').addEOL();
return true;
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ const PAUSE_LABEL = 'agents:paused';
const NEEDS_HUMAN_LABEL = 'needs-human';
const NEEDS_ATTENTION_LABEL = 'agent:needs-attention';
const DRAFT_DISPOSITION_MARKER = '<!-- keepalive-draft-disposition -->';
const MARK_PULL_REQUEST_READY_FOR_REVIEW_MUTATION = `mutation MarkPullRequestReadyForReview($pullRequestId: ID!) {
markPullRequestReadyForReview(input: {pullRequestId: $pullRequestId}) {
pullRequest {
number
isDraft
}
}
}`;

function normaliseLabelName(label) {
if (!label) {
Expand Down Expand Up @@ -105,23 +113,17 @@ async function addLabelsIfMissing({ github, owner, repo, prNumber, labels, curre

async function markDraftReadyForReview({ github, pr, core, summary }) {
const nodeId = String(pr?.node_id || '').trim();
if (!nodeId || typeof github.graphql !== 'function') {
if (!nodeId) {
summary.addRaw('Draft PR could not be converted automatically: missing GraphQL PR node id.').addEOL();
return false;
}
if (typeof github.graphql !== 'function') {
summary.addRaw('Draft PR could not be converted automatically: GitHub GraphQL client is unavailable.').addEOL();
return false;
}

try {
await github.graphql(
`mutation($pullRequestId: ID!) {
markPullRequestReadyForReview(input: {pullRequestId: $pullRequestId}) {
pullRequest {
number
isDraft
}
}
}`,
{ pullRequestId: nodeId }
);
await github.graphql(MARK_PULL_REQUEST_READY_FOR_REVIEW_MUTATION, { pullRequestId: nodeId });
summary.addRaw('Draft PR had no unchecked checklist items; marked ready for review.').addEOL();
return true;
} catch (error) {
Expand Down
Loading