Skip to content

feat: validate context length before LLM inference (Issue #1983) #124

feat: validate context length before LLM inference (Issue #1983)

feat: validate context length before LLM inference (Issue #1983) #124

name: PR Merge Guidance
on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
push:
branches:
- main
- develop
workflow_dispatch:
inputs:
pr_number:
description: Pull request number
required: true
type: number
dry_run:
description: Log the comment instead of writing it
required: false
default: true
type: boolean
permissions:
contents: read
issues: write
pull-requests: write
concurrency:
group: pr-merge-guidance-${{ github.event.pull_request.number || github.event.inputs.pr_number || github.ref_name }}
cancel-in-progress: false
jobs:
comment:
if: ${{ github.event_name == 'workflow_dispatch' || vars.PR_MERGE_GUIDANCE_ENABLED == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Post PR merge guidance
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
env:
PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number || '' }}
BASE_BRANCH: ${{ github.event_name == 'push' && github.ref_name || '' }}
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'true' || 'false' }}
MIN_CREATED_AT: ${{ vars.PR_MERGE_GUIDANCE_MIN_CREATED_AT || '2026-04-01T00:00:00Z' }}
COMMENT_MARKER: "### PR merge guidance"
CONFLICT_LABEL: "needs: rebase"
SIGNING_LABEL: "needs: signing"
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const marker = process.env.COMMENT_MARKER;
const conflictLabel = process.env.CONFLICT_LABEL;
const signingLabel = process.env.SIGNING_LABEL;
const requestedPullNumber = Number(process.env.PR_NUMBER);
const baseBranch = process.env.BASE_BRANCH;
const dryRun = process.env.DRY_RUN === "true";
const minCreatedAt = new Date(process.env.MIN_CREATED_AT);
if (requestedPullNumber) {
await processPullRequest(requestedPullNumber);
} else if (baseBranch) {
const openPullRequests = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: "open",
base: baseBranch,
per_page: 100,
});
for (const openPullRequest of openPullRequests) {
try {
await processPullRequest(openPullRequest.number);
} catch (error) {
const errorDetails = error?.stack || error?.message || String(error);
core.warning(`PR #${openPullRequest.number}: failed to process merge guidance: ${errorDetails}`);
}
}
} else {
core.setFailed("Pull request number was not provided.");
return;
}
async function processPullRequest(pull_number) {
const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number,
});
if (pr.state !== "open") {
core.info(`Skipping PR #${pull_number}: state is ${pr.state}.`);
return;
}
if (new Date(pr.created_at) < minCreatedAt) {
if (dryRun) {
core.info(`Would skip PR #${pull_number}: created at ${pr.created_at}, before cutoff ${process.env.MIN_CREATED_AT}.`);
}
return;
}
const existingComment = await findExistingComment(pull_number);
if (pr.draft) {
if (dryRun) {
core.info(`Would delete existing guidance comment and remove labels for draft PR #${pull_number}: ${Boolean(existingComment)}`);
return;
}
await removeLabel(pull_number, conflictLabel);
await removeLabel(pull_number, signingLabel);
await deleteExistingComment(existingComment);
return;
}
const prState = await getPullRequestState(pull_number);
const commits = await github.paginate(github.rest.pulls.listCommits, {
owner,
repo,
pull_number,
per_page: 100,
});
const unverifiedCommits = commits.filter((commit) => {
return !commit.commit?.verification?.verified;
});
const items = [];
const author = prState.author?.login || pr.user?.login;
const authorMention = author ? `@${author} ` : "";
const baseRef = prState.baseRefName || pr.base.ref;
if (prState.mergeable === "CONFLICTING") {
items.push(
`- This branch has merge conflicts with \`${baseRef}\`. Please rebase your branch on the latest \`${baseRef}\`, resolve the conflicts locally, and force-push the updated branch.`
);
}
if (unverifiedCommits.length > 0) {
const exampleShas = unverifiedCommits
.slice(0, 5)
.map((commit) => `\`${commit.sha.slice(0, 7)}\``)
.join(", ");
const extraCount = unverifiedCommits.length > 5 ? ` and ${unverifiedCommits.length - 5} more` : "";
items.push(
`- ${unverifiedCommits.length} commit${unverifiedCommits.length === 1 ? " does" : "s do"} not have a verified signature (${exampleShas}${extraCount}). Please sign the commits and force-push the updated branch.`
);
}
const hasConflict = prState.mergeable === "CONFLICTING";
const mergeableKnown = prState.mergeable !== "UNKNOWN";
const hasUnsigned = unverifiedCommits.length > 0;
if (dryRun) {
core.info(
`Would set labels for PR #${pull_number}: "${conflictLabel}"=${mergeableKnown ? hasConflict : "skipped (mergeable unknown)"}, "${signingLabel}"=${hasUnsigned}.`
);
} else {
if (mergeableKnown) {
await (hasConflict ? addLabel(pull_number, conflictLabel) : removeLabel(pull_number, conflictLabel));
}
await (hasUnsigned ? addLabel(pull_number, signingLabel) : removeLabel(pull_number, signingLabel));
}
if (items.length === 0) {
if (!mergeableKnown) {
core.info(`PR #${pull_number}: no signing issues and mergeable is UNKNOWN; leaving existing guidance comment untouched.`);
return;
}
if (dryRun) {
core.info(`Would delete existing guidance comment for PR #${pull_number}: ${Boolean(existingComment)}`);
return;
}
await deleteExistingComment(existingComment);
return;
}
const body = [
marker,
"",
`${authorMention}thanks for the PR. GitHub is currently blocking merge for one or more repository requirements:`,
"",
...items,
"",
"Relevant guide:",
`- Signed commits: https://github.com/${owner}/${repo}/blob/develop/CONTRIBUTING.md#commit-signing`,
`- Contribution guide: https://github.com/${owner}/${repo}/blob/develop/CONTRIBUTING.md`,
].join("\n");
if (existingComment) {
if (existingComment.body !== body) {
if (dryRun) {
core.info(`Would update guidance comment for PR #${pull_number}:\n${body}`);
return;
}
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existingComment.id,
body,
});
}
} else {
if (dryRun) {
core.info(`Would create guidance comment for PR #${pull_number}:\n${body}`);
return;
}
await github.rest.issues.createComment({
owner,
repo,
issue_number: pull_number,
body,
});
}
}
async function getPullRequestState(pull_number) {
const query = `
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
author {
login
}
baseRefName
mergeable
}
}
}
`;
let result = null;
for (let attempt = 0; attempt < 3; attempt += 1) {
result = await github.graphql(query, {
owner,
repo,
number: pull_number,
});
if (result.repository.pullRequest.mergeable !== "UNKNOWN") {
break;
}
await new Promise((resolve) => setTimeout(resolve, 2000));
}
if (result.repository.pullRequest.mergeable === "UNKNOWN") {
core.warning(`PR #${pull_number}: mergeable state is still UNKNOWN after retries; conflict check skipped.`);
}
return result.repository.pullRequest;
}
async function findExistingComment(pull_number) {
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: pull_number,
per_page: 100,
});
return comments.find((comment) => {
return comment.user?.type === "Bot" && comment.body?.startsWith(marker);
});
}
async function deleteExistingComment(comment) {
if (!comment) {
return;
}
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: comment.id,
});
}
async function addLabel(pull_number, name) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pull_number,
labels: [name],
});
}
async function removeLabel(pull_number, name) {
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: pull_number,
name,
});
} catch (error) {
if (error.status !== 404) {
throw error;
}
}
}