mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-21 06:11:31 +08:00
Evaluate issue acceptance using trusted default-branch code and the PR author's repository permissions. Report the native workflow job result, protect enforcement files, and align PR creation and review guidance. Closes #4339 Signed-off-by: Evan Lezar <elezar@nvidia.com>
78 lines
2.8 KiB
JavaScript
78 lines
2.8 KiB
JavaScript
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
module.exports = async function checkIssueAcceptance({ github, context, core }) {
|
|
const { owner, repo } = context.repo;
|
|
const { data: pr } = await github.rest.pulls.get({
|
|
owner, repo, pull_number: context.payload.pull_request.number,
|
|
});
|
|
const author = pr.user.login;
|
|
|
|
async function evaluate() {
|
|
try {
|
|
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
|
owner, repo, username: author,
|
|
});
|
|
// The REST API maps maintain to write and triage to read.
|
|
if (['write', 'maintain', 'admin'].includes(data.permission)) {
|
|
core.info(`${author} has repository write access; acceptance gate passed.`);
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
if (error.status !== 404) throw error;
|
|
}
|
|
|
|
const issues = [];
|
|
let cursor = null;
|
|
do {
|
|
const result = await github.graphql(`
|
|
query($owner: String!, $repo: String!, $number: Int!, $cursor: String) {
|
|
repository(owner: $owner, name: $repo) {
|
|
pullRequest(number: $number) {
|
|
closingIssuesReferences(first: 100, after: $cursor) {
|
|
nodes { number url repository { name owner { login } } }
|
|
pageInfo { hasNextPage endCursor }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`, { owner, repo, number: pr.number, cursor });
|
|
const linked = result.repository.pullRequest.closingIssuesReferences;
|
|
issues.push(...linked.nodes);
|
|
cursor = linked.pageInfo.hasNextPage ? linked.pageInfo.endCursor : null;
|
|
} while (cursor);
|
|
|
|
if (issues.length === 0) {
|
|
return 'Community PRs must link an issue with state:accepted using '
|
|
+ 'Fixes #NNN or Closes #NNN.';
|
|
}
|
|
|
|
const unaccepted = [];
|
|
for (const issue of issues) {
|
|
if (`${issue.repository.owner.login}/${issue.repository.name}`.toLowerCase()
|
|
!== `${owner}/${repo}`.toLowerCase()) {
|
|
return `Acceptance must be recorded in ${owner}/${repo}, not ${issue.url}.`;
|
|
}
|
|
const labels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
|
|
owner: issue.repository.owner.login,
|
|
repo: issue.repository.name,
|
|
issue_number: issue.number,
|
|
per_page: 100,
|
|
});
|
|
if (!labels.some(label => label.name === 'state:accepted')) {
|
|
unaccepted.push(issue.url);
|
|
}
|
|
}
|
|
|
|
if (unaccepted.length) {
|
|
return 'Community PRs require state:accepted on every linked issue. '
|
|
+ `Ask a maintainer to accept: ${unaccepted.join(', ')}. `
|
|
+ 'After acceptance, rerun this workflow.';
|
|
}
|
|
core.info(`All ${issues.length} linked issue(s) have state:accepted.`);
|
|
}
|
|
|
|
const failure = await evaluate();
|
|
if (failure) core.setFailed(failure);
|
|
};
|