feat: add Brin contributor trust check workflow and use OpenTUI native markdown (#157)
This commit is contained in:
1 parent
ee1ef73e93
commit
8f8641cfcb
2 files changed
+222
-266
No files matched your search
@@ -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,
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user