Download tests/e2e/mock-llm/scripts/upsert-pr-comment.mjs from SaylorTwift/openhands: direct link, hf CLI and curl.
- Browser
- Download file 7.97 kB
-
https://huggingface.co/SaylorTwift/openhands/resolve/main/tests/e2e/mock-llm/scripts/upsert-pr-comment.mjs
- Command line
-
hf download hf://SaylorTwift/openhands/tests/e2e/mock-llm/scripts/upsert-pr-comment.mjs
-
curl -L -o upsert-pr-comment.mjs https://huggingface.co/SaylorTwift/openhands/resolve/main/tests/e2e/mock-llm/scripts/upsert-pr-comment.mjs
7.97 kB
| import { existsSync, readFileSync, realpathSync } from "node:fs"; | |
| import { isAbsolute, relative, resolve } from "node:path"; | |
| import { pathToFileURL } from "node:url"; | |
| const DEFAULT_MARKER = "<!-- agent-canvas-mock-llm-e2e-report -->"; | |
| const API_ROOT = process.env.GITHUB_API_URL ?? "https://api.github.com"; | |
| function parseArgs(argv) { | |
| const args = {}; | |
| for (let index = 0; index < argv.length; index += 1) { | |
| const arg = argv[index]; | |
| if (!arg.startsWith("--")) { | |
| continue; | |
| } | |
| const [rawKey, inlineValue] = arg.slice(2).split("=", 2); | |
| const key = rawKey.replaceAll("-", "_"); | |
| if (inlineValue !== undefined) { | |
| args[key] = inlineValue; | |
| continue; | |
| } | |
| const next = argv[index + 1]; | |
| if (next && !next.startsWith("--")) { | |
| args[key] = next; | |
| index += 1; | |
| } else { | |
| args[key] = ""; | |
| } | |
| } | |
| return args; | |
| } | |
| function requireValue(name, value) { | |
| if (!value) { | |
| throw new Error(`Missing required value: ${name}`); | |
| } | |
| return value; | |
| } | |
| function assertWithinCwd(label, path) { | |
| const relativePath = relative(realpathSync(resolve(".")), path); | |
| if (relativePath.startsWith("..") || isAbsolute(relativePath)) { | |
| throw new Error(`${label} must be within the current working directory.`); | |
| } | |
| } | |
| function resolveWithinCwd(label, filePath) { | |
| const resolvedPath = resolve(filePath); | |
| if (!existsSync(resolvedPath)) { | |
| throw new Error(`${label} does not exist: ${resolvedPath}`); | |
| } | |
| assertWithinCwd(label, realpathSync(resolvedPath)); | |
| return resolvedPath; | |
| } | |
| function validateRepo(repo) { | |
| if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\/[a-zA-Z0-9._-]+$/.test(repo)) { | |
| throw new Error(`Invalid repo format: ${repo}`); | |
| } | |
| return repo; | |
| } | |
| function validateIssueNumber(issueNumber) { | |
| if (!/^\d+$/.test(String(issueNumber))) { | |
| throw new Error(`Invalid issue number: ${issueNumber}`); | |
| } | |
| return String(issueNumber); | |
| } | |
| function sleep(ms) { | |
| return new Promise((resolvePromise) => { | |
| setTimeout(resolvePromise, ms); | |
| }); | |
| } | |
| function retryDelayMs(attempt, response) { | |
| const retryAfter = response.headers.get("retry-after"); | |
| if (retryAfter && /^\d+$/.test(retryAfter)) { | |
| return Number(retryAfter) * 1000; | |
| } | |
| return 1000 * 2 ** attempt; | |
| } | |
| function parseGitHubPayload(method, path, status, text) { | |
| if (!text) { | |
| return null; | |
| } | |
| try { | |
| return JSON.parse(text); | |
| } catch (error) { | |
| throw new Error( | |
| `GitHub API ${method} ${path} returned invalid JSON with status ${status}: ${text.slice( | |
| 0, | |
| 500, | |
| )}`, | |
| ); | |
| } | |
| } | |
| async function githubRequest(method, path, token, body) { | |
| const retryableStatuses = new Set([429, 502, 503, 504]); | |
| for (let attempt = 0; attempt < 5; attempt += 1) { | |
| let response; | |
| try { | |
| response = await fetch(`${API_ROOT}${path}`, { | |
| method, | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| Accept: "application/vnd.github+json", | |
| "X-GitHub-Api-Version": "2022-11-28", | |
| ...(body === undefined | |
| ? {} | |
| : { "Content-Type": "application/json" }), | |
| }, | |
| body: body === undefined ? undefined : JSON.stringify(body), | |
| }); | |
| } catch (error) { | |
| // Transient network failure (DNS/connect/reset) rejects before any | |
| // HTTP status is available — retry it like a retryable status. | |
| if (attempt < 4) { | |
| await sleep(1000 * 2 ** attempt); | |
| continue; | |
| } | |
| throw error; | |
| } | |
| const text = await response.text(); | |
| if (!response.ok && retryableStatuses.has(response.status) && attempt < 4) { | |
| await sleep(retryDelayMs(attempt, response)); | |
| continue; | |
| } | |
| const payload = parseGitHubPayload(method, path, response.status, text); | |
| if (!response.ok) { | |
| throw new Error( | |
| `GitHub API ${method} ${path} failed with ${response.status}: ${text}`, | |
| ); | |
| } | |
| return payload; | |
| } | |
| throw new Error(`GitHub API ${method} ${path} failed after retries.`); | |
| } | |
| async function listIssueComments(repo, issueNumber, token) { | |
| const comments = []; | |
| for (let page = 1; page <= 10; page += 1) { | |
| const batch = await githubRequest( | |
| "GET", | |
| `/repos/${repo}/issues/${issueNumber}/comments?per_page=100&page=${page}`, | |
| token, | |
| ); | |
| comments.push(...batch); | |
| if (batch.length < 100) { | |
| break; | |
| } | |
| } | |
| return comments; | |
| } | |
| function escapeRegExp(value) { | |
| return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); | |
| } | |
| export function buildCommentBody(body, marker = DEFAULT_MARKER) { | |
| if (body.includes(marker)) { | |
| return body; | |
| } | |
| return `${marker}\n${body}`; | |
| } | |
| export function isMatchingJobComment(comment, { marker, legacyTitle }) { | |
| const body = comment.body ?? ""; | |
| if (body.includes(marker)) { | |
| return true; | |
| } | |
| if (!legacyTitle) { | |
| return false; | |
| } | |
| const user = comment.user ?? {}; | |
| const isBot = user.type === "Bot" || user.login?.endsWith("[bot]"); | |
| if (!isBot) { | |
| return false; | |
| } | |
| const legacyHeading = new RegExp( | |
| `^##\\s+\\S+\\s+${escapeRegExp(legacyTitle)}\\s*$`, | |
| "m", | |
| ); | |
| return legacyHeading.test(body); | |
| } | |
| export function findMatchingJobComments(comments, options) { | |
| return comments.filter((comment) => isMatchingJobComment(comment, options)); | |
| } | |
| export async function upsertJobComment({ | |
| repo, | |
| issueNumber, | |
| token, | |
| body, | |
| marker = DEFAULT_MARKER, | |
| legacyTitle = "", | |
| }) { | |
| const comments = await listIssueComments(repo, issueNumber, token); | |
| const [existing, ...duplicates] = findMatchingJobComments(comments, { | |
| marker, | |
| legacyTitle, | |
| }); | |
| const commentBody = buildCommentBody(body, marker); | |
| // Edit the oldest report in place so the comment keeps its position in the | |
| // thread and does not notify subscribers on every run. Anything beyond it is | |
| // a leftover duplicate from an earlier run. | |
| for (const duplicate of duplicates) { | |
| await githubRequest( | |
| "DELETE", | |
| `/repos/${repo}/issues/comments/${duplicate.id}`, | |
| token, | |
| ); | |
| } | |
| if (existing) { | |
| const updated = await githubRequest( | |
| "PATCH", | |
| `/repos/${repo}/issues/comments/${existing.id}`, | |
| token, | |
| { body: commentBody }, | |
| ); | |
| return { updated: true, deleted: duplicates.length, comment: updated }; | |
| } | |
| const created = await githubRequest( | |
| "POST", | |
| `/repos/${repo}/issues/${issueNumber}/comments`, | |
| token, | |
| { body: commentBody }, | |
| ); | |
| return { updated: false, deleted: duplicates.length, comment: created }; | |
| } | |
| async function main() { | |
| const args = parseArgs(process.argv.slice(2)); | |
| const issueNumber = | |
| args.issue_number ?? | |
| process.env.PR_NUMBER ?? | |
| process.env.ISSUE_NUMBER ?? | |
| ""; | |
| if (!issueNumber) { | |
| console.log("Skipping PR comment because no PR number was provided."); | |
| return; | |
| } | |
| const repo = validateRepo( | |
| requireValue("repo", args.repo ?? process.env.GITHUB_REPOSITORY), | |
| ); | |
| const validatedIssueNumber = validateIssueNumber(issueNumber); | |
| const token = requireValue( | |
| "token", | |
| args.token ?? process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN, | |
| ); | |
| const marker = args.marker ?? DEFAULT_MARKER; | |
| const bodyFile = resolveWithinCwd( | |
| "body-file", | |
| requireValue("body-file", args.body_file), | |
| ); | |
| const body = readFileSync(bodyFile, "utf8"); | |
| const result = await upsertJobComment({ | |
| repo, | |
| issueNumber: validatedIssueNumber, | |
| token, | |
| body, | |
| marker, | |
| legacyTitle: args.legacy_title ?? "", | |
| }); | |
| const action = result.updated ? "Updated" : "Created"; | |
| const cleanup = | |
| result.deleted === 0 | |
| ? "" | |
| : ` Deleted ${result.deleted} duplicate PR comment${ | |
| result.deleted === 1 ? "" : "s" | |
| }.`; | |
| console.log(`${action} PR comment ${result.comment.id}.${cleanup}`); | |
| } | |
| if ( | |
| process.argv[1] && | |
| import.meta.url === pathToFileURL(process.argv[1]).href | |
| ) { | |
| main().catch((error) => { | |
| console.error(error instanceof Error ? error.message : String(error)); | |
| process.exit(1); | |
| }); | |
| } | |