fix(ci): trust maintainer-authored dependency changes

This commit is contained in:
joshavant
2026-08-20 15:08:39 -05:00
parent 97c0455add
commit b52d2f08f5
3 changed files with 89 additions and 16 deletions
+21 -4
View File
@@ -324,7 +324,7 @@ export function renderTrustedDependencyComment({ actor, headSha }) {
"",
"### Dependency graph changes noted",
"",
"This PR includes dependency graph changes. The dependency guard is informational because the PR author is a repository admin or a member of `@openclaw/openclaw-secops`.",
"This PR includes dependency graph changes. The dependency guard is informational because the PR author is a repository admin, a member of `@openclaw/openclaw-secops`, or an OpenClaw organization member with Maintain or Admin repository access.",
"",
`- Current SHA: ${markdownCode(headSha ?? "<head-sha>")}`,
`- Trusted actor: @${sanitizeGuardDisplayValue(actor.login)}`,
@@ -497,12 +497,27 @@ export function dependencyGuardTrustedActorCandidates({ pullRequest, event, curr
/**
* @param {{
* candidates: GuardActorCandidate[],
* pullRequest: { author_association?: string },
* isDependencyApprover: (login: string) => Promise<string | null>,
* getRepositoryRoleName: (login: string) => Promise<string | null>,
* }} options
*/
export async function findTrustedDependencyGuardActor({ candidates, isDependencyApprover }) {
export async function findTrustedDependencyGuardActor({
candidates,
pullRequest,
isDependencyApprover,
getRepositoryRoleName,
}) {
for (const candidate of candidates) {
const role = await isDependencyApprover(candidate.login);
let role = await isDependencyApprover(candidate.login);
if (!role && pullRequest.author_association === "MEMBER") {
// GitHub's MEMBER association excludes outside collaborators. Keep this role path separate
// from override approvers so Maintain authors cannot authorize another contributor's PR.
const repositoryRole = await getRepositoryRoleName(candidate.login);
if (repositoryRole === "maintain" || repositoryRole === "admin") {
role = `OpenClaw organization member with repository ${repositoryRole} role`;
}
}
if (role) {
return {
login: candidate.login,
@@ -787,7 +802,7 @@ async function main() {
return;
}
const { isSecurityMember, isRepositoryAdmin } = createGuardApproverChecks({
const { getRepositoryRoleName, isSecurityMember, isRepositoryAdmin } = createGuardApproverChecks({
api,
owner,
repo,
@@ -820,7 +835,9 @@ async function main() {
}
const trustedActor = await findTrustedDependencyGuardActor({
candidates: dependencyGuardTrustedActorCandidates({ pullRequest, event, currentHeadSha }),
pullRequest,
isDependencyApprover,
getRepositoryRoleName,
});
if (trustedActor) {
if (mode === "detect") {
+11 -10
View File
@@ -157,7 +157,7 @@ export function createGuardApproverChecks({
warn = console.warn,
}) {
const membershipCache = new Map();
const permissionCache = new Map();
const repositoryRoleCache = new Map();
const isSecurityMember = async (login) => {
const normalizedLogin = login.toLowerCase();
if (explicitSecurityApprovers.has(normalizedLogin)) {
@@ -181,27 +181,28 @@ export function createGuardApproverChecks({
return false;
}
};
const isRepositoryAdmin = async (login) => {
const getRepositoryRoleName = async (login) => {
const normalizedLogin = login.toLowerCase();
if (permissionCache.has(normalizedLogin)) {
return permissionCache.get(normalizedLogin);
if (repositoryRoleCache.has(normalizedLogin)) {
return repositoryRoleCache.get(normalizedLogin);
}
try {
const result = await api.request(
`/repos/${owner}/${repo}/collaborators/${encodeURIComponent(login)}/permission`,
);
const allowed = result?.permission === "admin";
permissionCache.set(normalizedLogin, allowed);
return allowed;
const roleName = typeof result?.role_name === "string" ? result.role_name : null;
repositoryRoleCache.set(normalizedLogin, roleName);
return roleName;
} catch (error) {
if (error?.status !== 404) {
warn(`Could not verify repository permission for ${login}: ${error.message}`);
}
permissionCache.set(normalizedLogin, false);
return false;
repositoryRoleCache.set(normalizedLogin, null);
return null;
}
};
return { isSecurityMember, isRepositoryAdmin };
const isRepositoryAdmin = async (login) => (await getRepositoryRoleName(login)) === "admin";
return { getRepositoryRoleName, isSecurityMember, isRepositoryAdmin };
}
function githubErrorBodyTooLarge(maxBytes) {