Conversation
|
Hi @cevheri, I opened the PR as requested. I kept the first version intentionally conservative and review-only so we can discuss the signals, threshold, and rollout before considering any stronger moderation action. The workflow only adds Happy to adjust the approach based on the patterns you've been seeing in the repository. |
|
thanks @TonMtt |
|
I am closing this with thanks instead of merging it. While correct from a security model perspective, it is flawed regarding the target. We would be permanently committing the repository to its first |
|
Thanks for taking the time to validate this against the actual repository history. That makes sense. If the current signals don't match any of the 826 historical items, introducing the repository's first pull_request_target + write-capable workflow isn't justified by the benefit it provides. I appreciate the review — this was useful context, especially the distinction between the security model being sound in isolation and the deployment target not justifying it. I'll keep that in mind for future contributions. |
|
I gave this another pass based specifically on your feedback. Instead of trying to preserve the original implementation, I removed the two architectural costs you pointed out:
The idea would be to treat detection as an observation phase first. If it cannot demonstrate useful signal against real repository activity, it should never be promoted into an enforcement mechanism. A minimal version would look like this: name: Community moderation observation
on:
pull_request:
types: [opened, edited, reopened, synchronize]
permissions:
contents: read
jobs:
observe:
runs-on: ubuntu-latest
steps:
- name: Analyze pull request metadata
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
if (!pr) {
core.info("No pull request payload available.");
return;
}
const author = pr.user?.login || "unknown";
const association = pr.author_association || "NONE";
const text = `${pr.title || ""}\n${pr.body || ""}`;
const trusted = new Set([
"OWNER",
"MEMBER",
"COLLABORATOR"
]);
if (trusted.has(association)) {
core.info(
`Trusted participant: ${author} (${association})`
);
return;
}
/*
* Observation only.
*
* These are deliberately simple, explainable signals.
* They are NOT sufficient to classify somebody as a bot.
*
* The purpose is to collect evidence about whether any
* signal has value before considering automation.
*/
const signals = [];
if (
association === "NONE" ||
association === "FIRST_TIME_CONTRIBUTOR" ||
association === "FIRST_TIMER"
) {
signals.push({
type: "new_contributor",
value: association
});
}
const externalLinks =
text.match(
/https?:\/\/(?!github\.com\b)[^\s)>\]]+/gi
) || [];
if (externalLinks.length >= 4) {
signals.push({
type: "external_links",
value: externalLinks.length
});
}
const mentions =
text.match(/@[a-zA-Z0-9-]+/g) || [];
if (mentions.length >= 6) {
signals.push({
type: "mentions",
value: mentions.length
});
}
const repeatedLines = text
.split("\n")
.map(line => line.trim())
.filter(Boolean)
.reduce((acc, line) => {
acc[line] = (acc[line] || 0) + 1;
return acc;
}, {});
const repeatedContent = Object.entries(repeatedLines)
.filter(([, count]) => count >= 3)
.map(([line, count]) => ({
line: line.slice(0, 120),
count
}));
if (repeatedContent.length) {
signals.push({
type: "repeated_content",
value: repeatedContent
});
}
const observation = {
author,
association,
signals,
signalCount: signals.length
};
/*
* Output exists only inside this workflow run.
*
* No labels.
* No comments.
* No blocking.
* No repository writes.
*/
core.info(
JSON.stringify(observation, null, 2)
);
await core.summary
.addHeading("Community moderation observation")
.addRaw(`Author: ${author}\n\n`)
.addRaw(`Association: ${association}\n\n`)
.addRaw(
`Observed signals: ${signals.length}\n\n`
)
.addCodeBlock(
JSON.stringify(signals, null, 2),
"json"
)
.addRaw(
"\nObservation only — no moderation action performed."
)
.write();This changes the goal from "detect and flag suspicious contributors" to "measure whether observable metadata signals are useful at all." The important part of your 826-item observation is still unresolved: without matching the actual historical pattern, even this read-only version may simply have no value. So I wouldn't suggest reopening this PR on the basis of this code. I mainly wanted to check whether this direction addresses the architectural concern correctly: prove signal first with a read-only mechanism, and only discuss automation if the repository's real history demonstrates that it is useful. |
|
I saw once again today that we need like this; another bot account showed up and tried to work on PRs that had already been resolved by others. Thanks for the extra effort. I plan to consider this within the framework of a "bot detector" system we'll be implementing in the coming days. |
Summary
needs-human-reviewonly when the threshold is reachedThis is the proof of concept discussed after #735, where @cevheri asked to move the proposal into a PR for discussion.
Security model
The workflow uses
pull_request_targetonly for metadata-only triage on forked PRs. It does not check out or execute pull-request code. Permissions are limited tocontents: read,issues: write, andpull-requests: write.The default threshold is intentionally conservative and configurable with
MODERATION_REVIEW_THRESHOLD. The final moderation decision always remains with a maintainer.Testing
This change is limited to a GitHub Actions workflow and its rollout documentation. The workflow is designed to remain label-only so its first real validation can be done safely against repository activity while maintainers inspect false positives and misses.