Skip to content

[CI/E2E] build anthropic-shim image from checkout #1366

[CI/E2E] build anthropic-shim image from checkout

[CI/E2E] build anthropic-shim image from checkout #1366

name: Check Linked Issue
on:
pull_request:
types: [opened, synchronize, reopened, edited, ready_for_review]
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check-linked-issue:
if: >-
(
github.repository == 'vllm-project/semantic-router'
)
&& !github.event.pull_request.draft
&& github.event.pull_request.user.type != 'Bot'
runs-on: ubuntu-latest
permissions:
pull-requests: read
issues: read
steps:
- name: Verify pull request links an issue
uses: actions/github-script@v7
with:
script: |
// GitHub only populates closingIssuesReferences for closing keywords
// (Closes / Fixes / Resolves). "Related #N" is allowed by this check so
// multi-PR splits can track an issue without auto-closing it on merge.
// See: https://github.com/vllm-project/semantic-router/issues/2725
const RELATED_ISSUE_RE =
/\brelated\b\s*:?\s*(?:https?:\/\/github\.com\/[\w.-]+\/[\w.-]+\/issues\/|[\w.-]+\/[\w.-]+#|#)(\d+)/gi;
function collectRelatedIssueNumbers(text) {
const numbers = new Set();
if (!text) {
return numbers;
}
for (const match of text.matchAll(RELATED_ISSUE_RE)) {
numbers.add(Number(match[1]));
}
return numbers;
}
async function resolveRelatedIssues(issueNumbers) {
const related = [];
for (const number of issueNumbers) {
try {
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: number,
});
// PRs are also "issues" in the GitHub API; require a real issue.
if (issue.pull_request) {
core.warning(
`Related #${number} points at a pull request, not an issue; ignoring.`,
);
continue;
}
related.push({
number: issue.number,
title: issue.title,
url: issue.html_url,
kind: "related",
});
} catch (error) {
const status = error.status || error.statusCode;
if (status === 404) {
core.warning(
`Related #${number} was not found in ${context.repo.owner}/${context.repo.repo}; ignoring.`,
);
continue;
}
throw error;
}
}
return related;
}
const query = `
query ($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
closingIssuesReferences(first: 20) {
nodes {
number
title
url
}
}
}
}
}
`;
const { repository } = await github.graphql(query, {
owner: context.repo.owner,
repo: context.repo.repo,
number: context.payload.pull_request.number,
});
const closingIssues = (
repository.pullRequest.closingIssuesReferences.nodes ?? []
).map((issue) => ({ ...issue, kind: "closing" }));
const pr = context.payload.pull_request;
const relatedNumbers = collectRelatedIssueNumbers(
`${pr.title || ""}\n${pr.body || ""}`,
);
const relatedIssues = await resolveRelatedIssues(relatedNumbers);
const linkedIssues = [...closingIssues, ...relatedIssues];
if (linkedIssues.length === 0) {
core.setFailed(
[
"This pull request is not linked to an issue.",
"",
"Link an issue before review so changes can be tracked correctly:",
"- Closing keyword (auto-closes on merge): Closes #123",
"- Related keyword (tracks without auto-close; use for split PRs): Related #123",
].join("\n"),
);
return;
}
core.info(
`Linked issue(s): ${linkedIssues
.map(
(issue) =>
`#${issue.number} [${issue.kind}] (${issue.url})`,
)
.join(", ")}`,
);