feat: add Brin contributor trust check workflow and use OpenTUI native markdown (#157)

This commit is contained in:
Ismail Pelaseyed authored and GitHub committed 2026-03-19 10:24:25 +01:00
1 parent ee1ef73e93
commit 8f8641cfcb
2 files changed
+222 -266

No files matched your search

+184
View File
@@ -0,0 +1,184 @@
name: Contributor Trust Check
on:
pull_request_target:
types: [opened, reopened, synchronize]
permissions:
contents: read
pull-requests: write
jobs:
check:
name: Scan PR author
runs-on: ubuntu-24.04
concurrency:
group: brin-check-${{ github.event.pull_request.number }}
cancel-in-progress: true
steps:
- name: Query Brin API
id: brin
run: |
AUTHOR="${{ github.event.pull_request.user.login }}"
RESPONSE=$(curl -sf "https://api.brin.sh/contributor/${AUTHOR}?details=true&mode=full" || echo '{}')
echo "response<<BRIN_EOF" >> "$GITHUB_OUTPUT"
echo "$RESPONSE" >> "$GITHUB_OUTPUT"
echo "BRIN_EOF" >> "$GITHUB_OUTPUT"
- name: Apply label and comment
uses: actions/github-script@v7
env:
BRIN_RESPONSE: ${{ steps.brin.outputs.response }}
with:
script: |
const marker = "<!-- brin-check -->";
const pr = context.payload.pull_request;
let data;
try {
data = JSON.parse(process.env.BRIN_RESPONSE);
} catch {
core.warning("Failed to parse Brin API response");
return;
}
if (!data.score && data.score !== 0) {
core.warning("Brin API returned no score");
return;
}
const score = data.score;
const verdict = data.verdict ?? "unknown";
const confidence = data.confidence ?? "unknown";
const isSafe = verdict === "safe";
const labels = [
{ name: "contributor:verified", color: "0969da", description: "Contributor passed trust analysis." },
{ name: "contributor:flagged", color: "e16f24", description: "Contributor flagged for review by trust analysis." },
];
for (const label of labels) {
try {
const { data: existing } = await github.rest.issues.getLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: label.name,
});
if (existing.color !== label.color || (existing.description ?? "") !== label.description) {
await github.rest.issues.updateLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: label.name,
color: label.color,
description: label.description,
});
}
} catch (error) {
if (error.status !== 404) throw error;
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: label.name,
color: label.color,
description: label.description,
});
}
}
const nextLabel = isSafe ? "contributor:verified" : "contributor:flagged";
const labelNames = labels.map((l) => l.name);
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
});
const preserved = currentLabels
.map((l) => l.name)
.filter((name) => !labelNames.includes(name));
await github.rest.issues.setLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels: [...preserved, nextLabel],
});
core.info(`PR #${pr.number}: ${verdict} -> ${nextLabel}`);
if (isSafe) {
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
per_page: 100,
});
const existing = comments.find((c) => c.body?.includes(marker));
if (existing) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
});
}
return;
}
const sub = data.sub_scores ?? {};
const fmt = (v) => (v != null ? String(Math.round(v)) : "\u2014");
const verdictEmoji = {
caution: "\u26A0\uFE0F",
suspicious: "\u26D4",
dangerous: "\uD83D\uDEA8",
}[verdict] ?? "\u2753";
let body = `${marker}\n`;
body += `### ${verdictEmoji} Contributor Trust Check \u2014 Review Recommended\n\n`;
body += `This contributor's profile shows patterns that may warrant additional review. `;
body += `This is based on their GitHub activity, not the contents of this PR.\n\n`;
body += `**[${data.name}](https://github.com/${data.name})** \u00b7 Score: **${score}**/100\n\n`;
if (data.threats && data.threats.length > 0) {
body += `#### Why was this flagged?\n\n`;
body += `| Signal | Severity | Detail |\n|--------|----------|--------|\n`;
for (const t of data.threats) {
body += `| ${t.type} | ${t.severity} | ${t.detail} |\n`;
}
body += `\n`;
}
body += `<details>\n<summary>Dimension breakdown</summary>\n\n`;
body += `| Dimension | Score | What it measures |\n|-----------|-------|------------------|\n`;
body += `| Identity | ${fmt(sub.identity)} | Account age, contribution history, GPG keys, org memberships |\n`;
body += `| Behavior | ${fmt(sub.behavior)} | PR patterns, unsolicited contribution ratio, activity cadence |\n`;
body += `| Content | ${fmt(sub.content)} | PR body substance, issue linkage, contribution quality |\n`;
body += `| Graph | ${fmt(sub.graph)} | Cross-repo trust, co-contributor relationships |\n\n`;
body += `</details>\n\n`;
body += `<sub>Analyzed by [Brin](https://brin.sh) \u00b7 [Full profile](${data.url}?details=true)</sub>\n`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
per_page: 100,
});
const existingComment = comments.find((c) => c.body?.includes(marker));
if (existingComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body,
});
}
+38 -266
View File
@@ -1,275 +1,47 @@
import React from "react";
import React, { useMemo } from "react";
import { SyntaxStyle, RGBA } from "@opentui/core";
import type { Theme } from "./theme.js";
/* ── Block types ─────────────────────────────────────────────── */
type Block =
| { kind: "paragraph"; text: string }
| { kind: "heading"; level: number; text: string }
| { kind: "code"; lang: string; code: string }
| { kind: "list"; ordered: boolean; items: string[] }
| { kind: "hr" }
| { kind: "blockquote"; text: string };
/* ── Block parser ────────────────────────────────────────────── */
function parseBlocks(src: string): Block[] {
const lines = src.split("\n");
const blocks: Block[] = [];
let i = 0;
while (i < lines.length) {
const line = lines[i];
// Fenced code block
const fenceMatch = line.match(/^(`{3,}|~{3,})(\S*)/);
if (fenceMatch) {
const fence = fenceMatch[1];
const lang = fenceMatch[2] || "";
const codeLines: string[] = [];
i++;
while (i < lines.length && !lines[i].startsWith(fence)) {
codeLines.push(lines[i]);
i++;
}
i++; // skip closing fence
blocks.push({ kind: "code", lang, code: codeLines.join("\n") });
continue;
}
// Heading
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
if (headingMatch) {
blocks.push({ kind: "heading", level: headingMatch[1].length, text: headingMatch[2] });
i++;
continue;
}
// Horizontal rule
if (/^(\s*[-*_]\s*){3,}$/.test(line)) {
blocks.push({ kind: "hr" });
i++;
continue;
}
// Unordered list
if (/^\s*[-*+]\s+/.test(line)) {
const items: string[] = [];
while (i < lines.length && /^\s*[-*+]\s+/.test(lines[i])) {
items.push(lines[i].replace(/^\s*[-*+]\s+/, ""));
i++;
}
blocks.push({ kind: "list", ordered: false, items });
continue;
}
// Ordered list
if (/^\s*\d+[.)]\s+/.test(line)) {
const items: string[] = [];
while (i < lines.length && /^\s*\d+[.)]\s+/.test(lines[i])) {
items.push(lines[i].replace(/^\s*\d+[.)]\s+/, ""));
i++;
}
blocks.push({ kind: "list", ordered: true, items });
continue;
}
// Blockquote
if (line.startsWith("> ")) {
const bqLines: string[] = [];
while (i < lines.length && lines[i].startsWith("> ")) {
bqLines.push(lines[i].slice(2));
i++;
}
blocks.push({ kind: "blockquote", text: bqLines.join("\n") });
continue;
}
// Empty line — skip
if (line.trim() === "") {
i++;
continue;
}
// Paragraph — collect until empty line or block start
const paraLines: string[] = [];
while (
i < lines.length &&
lines[i].trim() !== "" &&
!lines[i].match(/^(`{3,}|~{3,})/) &&
!lines[i].match(/^#{1,6}\s+/) &&
!lines[i].match(/^(\s*[-*_]\s*){3,}$/) &&
!lines[i].match(/^\s*[-*+]\s+/) &&
!lines[i].match(/^\s*\d+[.)]\s+/) &&
!lines[i].startsWith("> ")
) {
paraLines.push(lines[i]);
i++;
}
if (paraLines.length > 0) {
blocks.push({ kind: "paragraph", text: paraLines.join("\n") });
}
}
return blocks;
function buildSyntaxStyle(t: Theme): SyntaxStyle {
return SyntaxStyle.fromStyles({
default: { fg: RGBA.fromHex(t.text) },
"markup.heading": { fg: RGBA.fromHex(t.mdHeading), bold: true },
"markup.heading.1": { fg: RGBA.fromHex(t.mdHeading), bold: true },
"markup.heading.2": { fg: RGBA.fromHex(t.mdHeading), bold: true },
"markup.heading.3": { fg: RGBA.fromHex(t.mdHeading), bold: true },
"markup.bold": { fg: RGBA.fromHex(t.mdBold), bold: true },
"markup.italic": { fg: RGBA.fromHex(t.mdItalic), italic: true },
"markup.raw": { fg: RGBA.fromHex(t.mdCode) },
"markup.link": { fg: RGBA.fromHex(t.mdLink), underline: true },
"markup.link.label": { fg: RGBA.fromHex(t.mdLinkText) },
"markup.list": { fg: RGBA.fromHex(t.mdListBullet) },
"markup.quote": { fg: RGBA.fromHex(t.mdItalic), italic: true },
"markup.separator": { fg: RGBA.fromHex(t.mdHr) },
code: { fg: RGBA.fromHex(t.mdCodeBlockFg), bg: RGBA.fromHex(t.mdCodeBlockBg) },
});
}
/* ── Inline parser ───────────────────────────────────────────── */
type InlineNode =
| { kind: "text"; text: string }
| { kind: "bold"; text: string }
| { kind: "italic"; text: string }
| { kind: "bolditalic"; text: string }
| { kind: "code"; text: string }
| { kind: "link"; text: string; url: string };
function parseInline(src: string): InlineNode[] {
const nodes: InlineNode[] = [];
// Order matters: bold-italic before bold before italic
const pattern =
/(`[^`]+`)|(\*\*\*[^*]+\*\*\*|___[^_]+___)|( \*\*[^*]+\*\*|__[^_]+__)|(\*[^*]+\*|_[^_]+_)|(\[[^\]]+\]\([^)]+\))/g;
let last = 0;
let match: RegExpExecArray | null;
while ((match = pattern.exec(src)) !== null) {
if (match.index > last) {
nodes.push({ kind: "text", text: src.slice(last, match.index) });
}
const m = match[0];
if (match[1]) {
// inline code
nodes.push({ kind: "code", text: m.slice(1, -1) });
} else if (match[2]) {
// bold italic
nodes.push({ kind: "bolditalic", text: m.slice(3, -3) });
} else if (match[3]) {
// bold
nodes.push({ kind: "bold", text: m.slice(2, -2) });
} else if (match[4]) {
// italic
nodes.push({ kind: "italic", text: m.slice(1, -1) });
} else if (match[5]) {
// link
const linkMatch = m.match(/\[([^\]]+)\]\(([^)]+)\)/);
if (linkMatch) {
nodes.push({ kind: "link", text: linkMatch[1], url: linkMatch[2] });
}
}
last = match.index + m.length;
}
if (last < src.length) {
nodes.push({ kind: "text", text: src.slice(last) });
}
return nodes;
}
/* ── Inline renderer ─────────────────────────────────────────── */
function InlineContent({ text, t }: { text: string; t: Theme }) {
const nodes = parseInline(text);
return (
<text>
{nodes.map((node, i) => {
switch (node.kind) {
case "text":
return <span key={i} style={{ fg: t.text }}>{node.text}</span>;
case "bold":
return <b key={i}><span style={{ fg: t.mdBold }}>{node.text}</span></b>;
case "italic":
return <i key={i}><span style={{ fg: t.mdItalic }}>{node.text}</span></i>;
case "bolditalic":
return <b key={i}><i><span style={{ fg: t.mdBold }}>{node.text}</span></i></b>;
case "code":
return <span key={i} style={{ fg: t.mdCode }}>{`\`${node.text}\``}</span>;
case "link":
return (
<span key={i}>
<span style={{ fg: t.mdLinkText }}>{node.text}</span>
<span style={{ fg: t.textDim }}>{" ("}</span>
<u><span style={{ fg: t.mdLink }}>{node.url}</span></u>
<span style={{ fg: t.textDim }}>{")"}</span>
</span>
);
}
})}
</text>
);
}
/* ── Block renderer ──────────────────────────────────────────── */
function renderBlock(block: Block, key: number, t: Theme): React.ReactNode {
switch (block.kind) {
case "heading":
return (
<box key={key} marginTop={key > 0 ? 1 : 0}>
<text>
<b><span style={{ fg: t.mdHeading }}>{block.text}</span></b>
</text>
</box>
);
case "paragraph":
return (
<box key={key}>
<InlineContent text={block.text} t={t} />
</box>
);
case "code":
return (
<box key={key} marginTop={0} marginBottom={0} backgroundColor={t.mdCodeBlockBg} paddingLeft={1} paddingRight={1}>
<text fg={t.mdCodeBlockFg}>{block.code}</text>
</box>
);
case "list":
return (
<box key={key} flexDirection="column">
{block.items.map((item, i) => (
<box key={i} flexDirection="row">
<text>
<span style={{ fg: t.mdListBullet }}>
{block.ordered ? `${i + 1}. ` : " • "}
</span>
</text>
<InlineContent text={item} t={t} />
</box>
))}
</box>
);
case "blockquote":
return (
<box key={key} paddingLeft={1} borderColor={t.mdItalic} border={["left"]}>
<text>
<i><span style={{ fg: t.mdItalic }}>{block.text}</span></i>
</text>
</box>
);
case "hr":
return (
<box key={key} marginTop={0} marginBottom={0}>
<text fg={t.mdHr}>{"─".repeat(40)}</text>
</box>
);
}
}
/* ── Public component ────────────────────────────────────────── */
const TABLE_OPTIONS = {
widthMode: "full" as const,
columnFitter: "balanced" as const,
wrapMode: "word" as const,
cellPadding: 1,
borders: true,
outerBorder: true,
borderStyle: "rounded" as const,
borderColor: "#333333",
};
export function Markdown({ content, t }: { content: string; t: Theme }) {
const blocks = parseBlocks(content);
const syntaxStyle = useMemo(() => buildSyntaxStyle(t), [t]);
return (
<box flexDirection="column">
{blocks.map((block, i) => renderBlock(block, i, t))}
</box>
<markdown
content={content}
syntaxStyle={syntaxStyle}
conceal={true}
tableOptions={TABLE_OPTIONS}
flexShrink={0}
/>
);
}