Require real behavior proof for external PRs (#77622)

* ci: require real behavior proof for external PRs

* fix: tighten real behavior proof heuristics

* fix: reject test-only real behavior proof labels

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
pashpashpash
2026-05-04 21:45:30 -07:00
committed by GitHub
parent d02fbc6116
commit 70f34bf177
10 changed files with 671 additions and 11 deletions

View File

@@ -1,5 +1,13 @@
// Barnacle owns deterministic GitHub triage and auto-response behavior.
import {
MOCK_ONLY_PROOF_LABEL,
NEEDS_REAL_BEHAVIOR_PROOF_LABEL,
PROOF_OVERRIDE_LABEL,
evaluateRealBehaviorProof,
labelsForRealBehaviorProof,
} from "./real-behavior-proof-policy.mjs";
const activePrLimit = 20;
const thirdPartyExtensionMessage =
@@ -134,6 +142,18 @@ export const managedLabelSpecs = {
color: "C5DEF5",
description: "Candidate: PR template appears mostly untouched.",
},
[NEEDS_REAL_BEHAVIOR_PROOF_LABEL]: {
color: "C5DEF5",
description: "Candidate: external PR needs after-fix proof from a real setup.",
},
[MOCK_ONLY_PROOF_LABEL]: {
color: "C5DEF5",
description: "Candidate: PR proof only shows tests, mocks, snapshots, lint, typecheck, or CI.",
},
[PROOF_OVERRIDE_LABEL]: {
color: "C2E0C6",
description: "Maintainer override for the external PR real behavior proof gate.",
},
"triage: dirty-candidate": {
color: "C5DEF5",
description: "Candidate: broad unrelated surfaces; may need splitting or cleanup.",
@@ -154,6 +174,8 @@ export const candidateLabels = {
docsDiscoverability: "triage: docs-discoverability",
testOnlyNoBug: "triage: test-only-no-bug",
refactorOnly: "triage: refactor-only",
needsRealBehaviorProof: NEEDS_REAL_BEHAVIOR_PROOF_LABEL,
mockOnlyProof: MOCK_ONLY_PROOF_LABEL,
dirtyCandidate: "triage: dirty-candidate",
riskyInfra: "triage: risky-infra",
externalPluginCandidate: "triage: external-plugin-candidate",
@@ -196,10 +218,23 @@ const maintainerAuthorLabel = "maintainer";
const privilegedAuthorAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
const privilegedRepositoryRoles = new Set(["admin", "maintain", "write"]);
const candidateLabelValues = Object.values(candidateLabels);
const proofCandidateLabelValues = [NEEDS_REAL_BEHAVIOR_PROOF_LABEL, MOCK_ONLY_PROOF_LABEL];
const noisyPrMessage =
"Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.";
const candidateActionRules = [
{
label: candidateLabels.needsRealBehaviorProof,
close: true,
message:
"Closing this PR because it does not include real behavior proof. Please reopen or resubmit with after-fix evidence from a real OpenClaw setup; terminal screenshots, console output, redacted logs, recordings, linked artifacts, and copied live output count. Unit tests, mocks, snapshots, lint, typechecks, and CI are supplemental only.",
},
{
label: candidateLabels.mockOnlyProof,
close: true,
message:
"Closing this PR because the proof only shows tests, mocks, snapshots, lint, typechecks, or CI. Please reopen or resubmit with after-fix evidence from a real OpenClaw setup; terminal screenshots, console output, redacted logs, recordings, linked artifacts, and copied live output count.",
},
{
label: candidateLabels.dirtyCandidate,
close: true,
@@ -438,6 +473,14 @@ export function classifyPullRequestCandidateLabels(pullRequest, files) {
labelsToAdd.push(candidateLabels.blankTemplate);
}
labelsToAdd.push(
...labelsForRealBehaviorProof(
evaluateRealBehaviorProof({
pullRequest,
}),
),
);
const docsOnly = filenames.every(isMarkdownOrDocsFile);
const docsSignal =
/\b(add|adds|update|updates|fix|fixes|improve|cleanup|clean up|typo|readme|docs?|documentation|translation|translate)\b/i.test(
@@ -718,14 +761,18 @@ async function addMissingLabels(github, context, core, issueNumber, labels, labe
async function applyPullRequestCandidateLabels(github, context, core, pullRequest, labelSet) {
const files = await listPullRequestFiles(github, context, pullRequest);
await addMissingLabels(
github,
context,
core,
pullRequest.number,
classifyPullRequestCandidateLabels(pullRequest, files),
labelSet,
const classifiedLabels = classifyPullRequestCandidateLabels(
{
...pullRequest,
labels: [...labelSet].map((name) => ({ name })),
},
files,
);
const staleProofLabels = proofCandidateLabelValues.filter(
(label) => labelSet.has(label) && !classifiedLabels.includes(label),
);
await removeLabels(github, context, pullRequest.number, staleProofLabels, labelSet);
await addMissingLabels(github, context, core, pullRequest.number, classifiedLabels, labelSet);
}
function isAutomationUser(user, fallbackLogin = "") {
@@ -931,7 +978,9 @@ export async function runBarnacleAutoResponse({ github, context, core = console
const isLabelEvent = context.payload.action === "labeled";
const isPrCandidateEvent =
pullRequest &&
["opened", "edited", "synchronize", "reopened", "labeled"].includes(context.payload.action);
["opened", "edited", "synchronize", "reopened", "labeled", "unlabeled"].includes(
context.payload.action,
);
if (!hasTriggerLabel && !isLabelEvent && !isPrCandidateEvent) {
return;
}