
























@@ -3,16 +3,29 @@
33// GitHub dependency-change guard: detects dependency files, manages override
44// comments/labels, and can autoscrub lockfile-only PR changes.
55import { appendFile, readFile } from "node:fs/promises";
6-import { readBoundedResponseText } from "../lib/bounded-response.mjs";
6+import {
7+GITHUB_API_REQUEST_TIMEOUT_MS,
8+GITHUB_ERROR_BODY_MAX_BYTES,
9+GITHUB_RESPONSE_BODY_MAX_BYTES,
10+createGitHubApi,
11+createGuardApproverChecks,
12+guardTrustedActorCandidates,
13+readBoundedGitHubErrorText,
14+readBoundedGitHubJson,
15+} from "./guard-shared.mjs";
716817/** Marker used to identify dependency guard comments. */
918export const dependencyChangeMarker = "<!-- openclaw:dependency-guard -->";
1019export const dependencyGraphGuardMarker = "<!-- openclaw:dependency-graph-guard -->";
1120export const dependencyChangedLabel = "dependencies-changed";
1221export const allowDependenciesCommand = "/allow-dependencies-change";
13-export const GITHUB_ERROR_BODY_MAX_BYTES = 64 * 1024;
14-export const GITHUB_RESPONSE_BODY_MAX_BYTES = 4 * 1024 * 1024;
15-export const GITHUB_API_REQUEST_TIMEOUT_MS = 30_000;
22+export {
23+GITHUB_API_REQUEST_TIMEOUT_MS,
24+GITHUB_ERROR_BODY_MAX_BYTES,
25+GITHUB_RESPONSE_BODY_MAX_BYTES,
26+readBoundedGitHubErrorText,
27+readBoundedGitHubJson,
28+};
16291730const maxListedFiles = 25;
1831const autoscrubCommitMessage = "chore: remove dependency lockfile change";
@@ -445,28 +458,7 @@ function renderAutoscrubStatusLines(status) {
445458}
446459447460export function dependencyGuardTrustedActorCandidates({ pullRequest, event, currentHeadSha }) {
448-const eventHeadSha = event?.pull_request?.head?.sha;
449-const eventAfterSha = event?.after;
450-const eventMatchesCurrentHead =
451-Boolean(currentHeadSha) &&
452-(eventHeadSha === currentHeadSha || eventAfterSha === currentHeadSha);
453-if (!eventMatchesCurrentHead) {
454-return [];
455-}
456-const candidates = [];
457-const seen = new Set();
458-for (const [source, login] of [["pull request author", pullRequest?.user?.login]]) {
459-if (typeof login !== "string" || login.length === 0) {
460-continue;
461-}
462-const normalizedLogin = login.toLowerCase();
463-if (seen.has(normalizedLogin)) {
464-continue;
465-}
466-seen.add(normalizedLogin);
467-candidates.push({ login, source });
468-}
469-return candidates;
461+return guardTrustedActorCandidates({ pullRequest, event, currentHeadSha });
470462}
471463472464export async function findTrustedDependencyGuardActor({ candidates, isDependencyApprover }) {
@@ -486,112 +478,12 @@ function renderManifestChangeLine(change) {
486478return `- ${markdownCode(change.path)} changed ${change.fields.map(markdownCode).join(", ")}.`;
487479}
488480489-function githubErrorBodyTooLarge(maxBytes) {
490-return new Error(`GitHub error response body exceeded ${maxBytes} bytes`);
491-}
492-493-function githubResponseBodyTooLarge(maxBytes) {
494-return new Error(`GitHub response body exceeded ${maxBytes} bytes`);
495-}
496-497-export async function readBoundedGitHubErrorText(
498-response,
499-maxBytes = GITHUB_ERROR_BODY_MAX_BYTES,
500-options = {},
501-) {
502-return await readBoundedResponseText(response, "GitHub error", maxBytes, {
503-createTooLargeError: () => githubErrorBodyTooLarge(maxBytes),
504- ...options,
505-});
506-}
507-508-export async function readBoundedGitHubJson(
509-response,
510-maxBytes = GITHUB_RESPONSE_BODY_MAX_BYTES,
511-options = {},
512-) {
513-const text = await readBoundedResponseText(response, "GitHub", maxBytes, {
514-createTooLargeError: () => githubResponseBodyTooLarge(maxBytes),
515- ...options,
516-});
517-return JSON.parse(text);
518-}
519-520-function timeoutError(path, method, timeoutMs) {
521-return new Error(`GitHub API ${method} ${path} exceeded timeout ${timeoutMs}ms`);
522-}
523-524-function combineAbortSignals(signals) {
525-const activeSignals = signals.filter(Boolean);
526-if (activeSignals.length === 0) {
527-return undefined;
528-}
529-if (activeSignals.length === 1) {
530-return activeSignals[0];
531-}
532-return AbortSignal.any(activeSignals);
533-}
534-535481export function githubApi(token, options = {}) {
536-const fetchImpl = options.fetchImpl ?? fetch;
537-const timeoutMs = options.timeoutMs ?? GITHUB_API_REQUEST_TIMEOUT_MS;
538-const responseMaxBodyBytes = options.responseMaxBodyBytes ?? GITHUB_RESPONSE_BODY_MAX_BYTES;
539-const baseHeaders = {
540-accept: "application/vnd.github+json",
541-authorization: `Bearer ${token}`,
542-"user-agent": "openclaw-dependency-guard",
543-"x-github-api-version": "2022-11-28",
544-};
545-const request = async (path, requestOptions = {}) => {
546-const method = requestOptions.method ?? "GET";
547-const timeoutController = new AbortController();
548-let timeout;
549-const timeoutPromise = new Promise((_, reject) => {
550-timeout = setTimeout(() => {
551-timeoutController.abort();
552-reject(timeoutError(path, method, timeoutMs));
553-}, timeoutMs);
554-timeout.unref?.();
555-});
556-const operationPromise = (async () => {
557-const response = await fetchImpl(`https://api.github.com${path}`, {
558- ...requestOptions,
559-signal: combineAbortSignals([requestOptions.signal, timeoutController.signal]),
560-headers: { ...baseHeaders, ...requestOptions.headers },
561-});
562-if (response.status === 204) {
563-return null;
564-}
565-if (!response.ok) {
566-let errorText;
567-try {
568-errorText = await readBoundedGitHubErrorText(response, GITHUB_ERROR_BODY_MAX_BYTES, {
569-signal: timeoutController.signal,
570- timeoutPromise,
571-});
572-} catch (bodyError) {
573-errorText = bodyError instanceof Error ? bodyError.message : String(bodyError);
574-}
575-const error = new Error(`${response.status} ${response.statusText}: ${errorText}`);
576-error.status = response.status;
577-throw error;
578-}
579-return await readBoundedGitHubJson(response, responseMaxBodyBytes, {
580-signal: timeoutController.signal,
581- timeoutPromise,
582-});
583-})();
584-operationPromise.catch(() => {});
585-try {
586-return await Promise.race([operationPromise, timeoutPromise]);
587-} finally {
588-clearTimeout(timeout);
589-}
590-};
482+const api = createGitHubApi(token, { ...options, userAgent: "openclaw-dependency-guard" });
591483return {
592-request,
484+...api,
593485graphql: async (query, variables) => {
594-const result = await request("/graphql", {
486+const result = await api.request("/graphql", {
595487method: "POST",
596488headers: { "content-type": "application/json" },
597489body: JSON.stringify({ query, variables }),
@@ -609,7 +501,7 @@ export function githubApi(token, options = {}) {
609501const items = [];
610502for (let page = 1; ; page += 1) {
611503const separator = path.includes("?") ? "&" : "?";
612-const pageItems = await request(`${path}${separator}per_page=100&page=${page}`);
504+const pageItems = await api.request(`${path}${separator}per_page=100&page=${page}`);
613505items.push(...pageItems);
614506if (pageItems.length < 100) {
615507return items;
@@ -925,51 +817,13 @@ async function main() {
925817return;
926818}
927819928-const membershipCache = new Map();
929-const permissionCache = new Map();
930-const isSecurityMember = async (login) => {
931-const normalizedLogin = login.toLowerCase();
932-if (explicitSecurityApprovers.has(normalizedLogin)) {
933-return true;
934-}
935-if (membershipCache.has(normalizedLogin)) {
936-return membershipCache.get(normalizedLogin);
937-}
938-try {
939-const membership = await api.request(
940-`/orgs/${owner}/teams/${securityTeamSlug}/memberships/${encodeURIComponent(login)}`,
941-);
942-const allowed = membership?.state === "active";
943-membershipCache.set(normalizedLogin, allowed);
944-return allowed;
945-} catch (error) {
946-if (error?.status !== 404) {
947-console.warn(`Could not verify ${login} against ${securityTeamSlug}: ${error.message}`);
948-}
949-membershipCache.set(normalizedLogin, false);
950-return false;
951-}
952-};
953-const isRepositoryAdmin = async (login) => {
954-const normalizedLogin = login.toLowerCase();
955-if (permissionCache.has(normalizedLogin)) {
956-return permissionCache.get(normalizedLogin);
957-}
958-try {
959-const result = await api.request(
960-`/repos/${owner}/${repo}/collaborators/${encodeURIComponent(login)}/permission`,
961-);
962-const allowed = result?.permission === "admin";
963-permissionCache.set(normalizedLogin, allowed);
964-return allowed;
965-} catch (error) {
966-if (error?.status !== 404) {
967-console.warn(`Could not verify repository permission for ${login}: ${error.message}`);
968-}
969-permissionCache.set(normalizedLogin, false);
970-return false;
971-}
972-};
820+const { isSecurityMember, isRepositoryAdmin } = createGuardApproverChecks({
821+ api,
822+ owner,
823+ repo,
824+ securityTeamSlug,
825+ explicitSecurityApprovers,
826+});
973827const isDependencyApprover = async (login) => {
974828if (await isSecurityMember(login)) {
975829return securityTeamSlug;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。