fix(ci): identify team members by repository permission (#8822)

* fix(ci): identify team members by repository permission

`author_association` is computed from what the caller can see, so GITHUB_TOKEN
reports a private organization member as CONTRIBUTOR. Only 5 of GreptimeTeam's
members have public membership, so the open-pull-request check skipped almost
everyone it was written for.

Use the repository permission of the author instead, which is
viewer-independent. On error, apply the limit rather than skipping, so a token
that cannot read permissions cannot silently disable the check again.

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

* fix(ci): do not log repository permission levels

Job logs are public. Resolving the author's permission is fine; printing the
level is not.

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
dennis zhuang
2026-08-10 21:14:56 +08:00
committed by GitHub
parent f88226c3ff
commit e2ab11763a
2 changed files with 29 additions and 9 deletions
+29 -8
View File
@@ -9,9 +9,8 @@ import { appendFileSync } from "node:fs";
const MARKER = "<!-- pr-open-limit -->";
// GITHUB_TOKEN can only edit comments authored by the Actions bot itself.
const COMMENT_AUTHOR = "github-actions[bot]";
// Associations that identify an organization member. Outside collaborators and
// community contributors are out of scope.
const MEMBER_ASSOCIATIONS = ["OWNER", "MEMBER"];
// Repository permissions that mark someone as part of the team.
const TEAM_PERMISSIONS = ["admin", "maintain", "write"];
// Keep the comment readable for authors who are far over the limit.
const MAX_LISTED_PRS = 10;
@@ -22,6 +21,29 @@ function summary(text) {
console.log(text);
}
// `author_association` cannot be used here: it is computed from what the caller
// is allowed to see, so GITHUB_TOKEN reports a private organization member as
// CONTRIBUTOR. Repository permission is viewer-independent.
//
// On error this returns true rather than false. A token that cannot read
// permissions would otherwise make the whole check silently pass everyone.
async function isTeamMember(octokit, owner, repo, username) {
try {
const { data } = await octokit.repos.getCollaboratorPermissionLevel({
owner,
repo,
username,
});
// Never log the level itself: job logs are public.
return TEAM_PERMISSIONS.includes(data.permission);
} catch (error) {
console.log(
`Cannot read repository permission for \`${username}\` (HTTP ${error.status}); applying the limit anyway.`
);
return true;
}
}
function buildComment(author, total, limit, openPrs) {
const listed = openPrs
.slice(0, MAX_LISTED_PRS)
@@ -69,7 +91,6 @@ async function upsertComment(octokit, params, body) {
const [owner, repo] = process.env.GITHUB_REPOSITORY.split("/");
const prNumber = Number(process.env.PR_NUMBER);
const author = process.env.PR_AUTHOR;
const association = process.env.PR_AUTHOR_ASSOCIATION;
const rawLimit = Number(process.env.MAX_OPEN_PRS);
const limit = Number.isInteger(rawLimit) && rawLimit >= 0 ? rawLimit : 5;
@@ -78,13 +99,13 @@ async function upsertComment(octokit, params, body) {
return;
}
if (!MEMBER_ASSOCIATIONS.includes(association)) {
summary(`Skipping \`${author}\`: association is \`${association}\`, not an org member.`);
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
if (!(await isTeamMember(octokit, owner, repo, author))) {
summary(`Skipping \`${author}\`: not a team member.`);
return;
}
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const allOpen = await octokit.paginate(octokit.pulls.list, {
owner,
repo,