chore: import upstream snapshot with attribution
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
const SUMMARY_MARKER_PREFIX = "<!-- lark-cli-pr-quality-summary";
|
||||
const LEGACY_SUMMARY_MARKER_PREFIXES = [
|
||||
"<!-- lark-cli-semantic-review",
|
||||
];
|
||||
|
||||
function sanitizeMarkdownBody(text) {
|
||||
return String(text || "")
|
||||
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "")
|
||||
.replace(/[\r\n\t]+/g, " ")
|
||||
.replace(/@/g, "@\u200b")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/`/g, "\\`")
|
||||
.replace(/\*/g, "\\*")
|
||||
.replace(/_/g, "\\_")
|
||||
.replace(/#/g, "\\#")
|
||||
.replace(/\|/g, "\\|")
|
||||
.replace(/!/g, "\\!")
|
||||
.replace(/\[/g, "\\[")
|
||||
.replace(/\]/g, "\\]")
|
||||
.replace(/\(/g, "\\(")
|
||||
.replace(/\)/g, "\\)")
|
||||
.replace(/\bhttps:\/\//g, "https[:]//")
|
||||
.replace(/\bhttp:\/\//g, "http[:]//")
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function markdownText(value) {
|
||||
return sanitizeMarkdownBody(String(value || ""));
|
||||
}
|
||||
|
||||
function inlineCodeText(value) {
|
||||
return String(value || "")
|
||||
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "")
|
||||
.replace(/[\r\n\t]+/g, " ")
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function inlineCode(value) {
|
||||
const text = inlineCodeText(value);
|
||||
const runs = text.match(/`+/g) || [];
|
||||
const fence = "`".repeat(runs.reduce((max, run) => Math.max(max, run.length), 0) + 1);
|
||||
const body = text.startsWith("`") || text.endsWith("`") ? ` ${text} ` : text;
|
||||
return fence + body + fence;
|
||||
}
|
||||
|
||||
function summaryMarker(target = {}) {
|
||||
return `${SUMMARY_MARKER_PREFIX} head=${target.headSha || ""} base=${target.baseSha || ""} run=${target.runId || ""} -->`;
|
||||
}
|
||||
|
||||
function parseSummaryMarker(body) {
|
||||
const match = /<!--\s*lark-cli-(?:pr-quality-summary|semantic-review)\s+([^>]*)-->/.exec(String(body || ""));
|
||||
if (!match) {
|
||||
return {};
|
||||
}
|
||||
const metadata = {};
|
||||
for (const part of match[1].trim().split(/\s+/)) {
|
||||
const attr = /^([A-Za-z0-9_-]+)=([^ ]*)$/.exec(part);
|
||||
if (attr) {
|
||||
metadata[attr[1]] = attr[2];
|
||||
}
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
function markerRunNumber(value) {
|
||||
const run = Number(String(value || "").trim());
|
||||
return Number.isInteger(run) && run > 0 ? run : 0;
|
||||
}
|
||||
|
||||
function summaryCommentRunNumber(comment) {
|
||||
return markerRunNumber(parseSummaryMarker(comment?.body).run);
|
||||
}
|
||||
|
||||
function targetRunNumber(target) {
|
||||
return markerRunNumber(target?.runId);
|
||||
}
|
||||
|
||||
function hasNewerSummaryComment(comments, target) {
|
||||
const currentRun = targetRunNumber(target);
|
||||
return qualitySummaryComments(comments)
|
||||
.some((comment) => summaryCommentRunNumber(comment) > currentRun);
|
||||
}
|
||||
|
||||
function isBotComment(comment) {
|
||||
return !!(comment && comment.user && comment.user.type === "Bot");
|
||||
}
|
||||
|
||||
function hasQualitySummaryMarker(body) {
|
||||
const text = String(body || "");
|
||||
return text.includes(SUMMARY_MARKER_PREFIX) ||
|
||||
LEGACY_SUMMARY_MARKER_PREFIXES.some((prefix) => text.includes(prefix));
|
||||
}
|
||||
|
||||
function qualitySummaryComments(comments) {
|
||||
return (Array.isArray(comments) ? comments : [])
|
||||
.filter((comment) => isBotComment(comment) && hasQualitySummaryMarker(comment.body));
|
||||
}
|
||||
|
||||
function findQualitySummaryComment(comments) {
|
||||
return qualitySummaryComments(comments)[0] || null;
|
||||
}
|
||||
|
||||
function finalSummaryBody(target, markdown) {
|
||||
return `${summaryMarker(target)}\n${String(markdown || "")}`.slice(0, 60000);
|
||||
}
|
||||
|
||||
async function listIssueComments(github, context, pr) {
|
||||
return github.paginate(github.rest.issues.listComments, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr,
|
||||
per_page: 100,
|
||||
});
|
||||
}
|
||||
|
||||
async function publishQualitySummary({ github, context, pr, target, markdown, beforeWrite }) {
|
||||
const body = finalSummaryBody(target, markdown);
|
||||
const comments = await listIssueComments(github, context, pr);
|
||||
const summaries = qualitySummaryComments(comments);
|
||||
if (hasNewerSummaryComment(summaries, target)) {
|
||||
return { action: "skipped-newer-summary" };
|
||||
}
|
||||
const existing = summaries[0] || null;
|
||||
if (beforeWrite && !(await beforeWrite(existing ? "update" : "creation"))) {
|
||||
return { action: "skipped" };
|
||||
}
|
||||
for (const duplicate of summaries.slice(1)) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: duplicate.id,
|
||||
});
|
||||
}
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
return { action: "updated", commentId: existing.id, body };
|
||||
}
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr,
|
||||
body,
|
||||
});
|
||||
return { action: "created", body };
|
||||
}
|
||||
|
||||
async function deleteQualitySummaries({ github, context, pr, target, beforeWrite }) {
|
||||
const comments = await listIssueComments(github, context, pr);
|
||||
const existing = qualitySummaryComments(comments);
|
||||
if (hasNewerSummaryComment(existing, target)) {
|
||||
return { deleted: 0, skipped: true };
|
||||
}
|
||||
if (existing.length > 0 && beforeWrite && !(await beforeWrite("delete"))) {
|
||||
return { deleted: 0, skipped: true };
|
||||
}
|
||||
for (const comment of existing) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: comment.id,
|
||||
});
|
||||
}
|
||||
return { deleted: existing.length };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SUMMARY_MARKER_PREFIX,
|
||||
deleteQualitySummaries,
|
||||
finalSummaryBody,
|
||||
findQualitySummaryComment,
|
||||
hasQualitySummaryMarker,
|
||||
inlineCode,
|
||||
listIssueComments,
|
||||
markdownText,
|
||||
publishQualitySummary,
|
||||
qualitySummaryComments,
|
||||
sanitizeMarkdownBody,
|
||||
summaryMarker,
|
||||
};
|
||||
Reference in New Issue
Block a user