Skip to content

chore: add conservative community moderation triage - #806

Closed
TonMtt wants to merge 2 commits into
libredb:mainfrom
TonMtt:feat/community-moderation-triage-pr
Closed

TonMtt wants to merge 2 commits into
libredb:mainfrom
TonMtt:feat/community-moderation-triage-pr

Conversation

@TonMtt

@TonMtt TonMtt commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a review-only GitHub Actions workflow for suspicious community activity
  • score a small set of explainable metadata signals and add needs-human-review only when the threshold is reached
  • never block users, close/delete content, execute contributor code, or use repository secrets
  • document the security model, tuning, and a label-only rollout plan

This 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_target only for metadata-only triage on forked PRs. It does not check out or execute pull-request code. Permissions are limited to contents: read, issues: write, and pull-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.

@TonMtt

TonMtt commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

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 needs-human-review; it does not block users, close content, delete comments, or execute contributor code.

Happy to adjust the approach based on the patterns you've been seeing in the repository.

@cevheri cevheri added security Supply-chain, auth, or hardening work github-actions GitHub Actions workflow dependencies labels Sep 11, 2026
@cevheri

cevheri commented Sep 11, 2026

Copy link
Copy Markdown
Member

thanks @TonMtt
I will check it,
this is an interesting aproach 👍

@cevheri
cevheri self-requested a review September 11, 2026 22:50
@cevheri
cevheri marked this pull request as draft September 14, 2026 13:49
@cevheri

cevheri commented Sep 15, 2026

Copy link
Copy Markdown
Member

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 pull_request_target trigger and an externally accessible write token, all for the sake of a detection mechanism that fails to match anything among the 826 items in the actual history.

@cevheri cevheri closed this Sep 15, 2026
@TonMtt

TonMtt commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

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.

@TonMtt

TonMtt commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

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:

  • no pull_request_target
  • no write-capable token
  • no labels or repository mutations
  • no checkout or execution of contributor code
  • no moderation decision

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.

@cevheri

cevheri commented Sep 16, 2026

Copy link
Copy Markdown
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

github-actions GitHub Actions workflow dependencies security Supply-chain, auth, or hardening work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants