mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-06 04:45:24 +02:00
Support contribution policy label overrides
This commit is contained in:
@@ -3,16 +3,22 @@ const COMMENT_MARKER = "<!-- yaak-contribution-policy -->";
|
|||||||
const MAINTAINER_LOGINS = new Set(["gschier"]);
|
const MAINTAINER_LOGINS = new Set(["gschier"]);
|
||||||
const MAINTAINER_ASSOCIATIONS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
|
const MAINTAINER_ASSOCIATIONS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
|
||||||
const MAINTAINER_PERMISSIONS = new Set(["admin", "maintain", "write"]);
|
const MAINTAINER_PERMISSIONS = new Set(["admin", "maintain", "write"]);
|
||||||
|
const REVIEWER_LOGIN = "gschier";
|
||||||
|
|
||||||
const LARGE_DIFF_CHANGED_FILES = 20;
|
const LARGE_DIFF_CHANGED_FILES = 20;
|
||||||
const LARGE_DIFF_CHANGED_LINES = 800;
|
const LARGE_DIFF_CHANGED_LINES = 800;
|
||||||
const SUMMARY_TITLE_MAX_LENGTH = 80;
|
const SUMMARY_TITLE_MAX_LENGTH = 80;
|
||||||
|
|
||||||
const LABELS = {
|
const LABELS = {
|
||||||
accepted: {
|
inScope: {
|
||||||
name: "contribution: accepted",
|
name: "contribution: in scope",
|
||||||
color: "0E8A16",
|
color: "0E8A16",
|
||||||
description: "Community PR appears to match Yaak's contribution policy.",
|
description: "Community PR appears to be in scope for maintainer review.",
|
||||||
|
},
|
||||||
|
outOfScope: {
|
||||||
|
name: "contribution: out of scope",
|
||||||
|
color: "B60205",
|
||||||
|
description: "Community PR does not match Yaak's contribution policy.",
|
||||||
},
|
},
|
||||||
approvedFeedback: {
|
approvedFeedback: {
|
||||||
name: "contribution: approved feedback",
|
name: "contribution: approved feedback",
|
||||||
@@ -37,7 +43,13 @@ const LABELS = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const MANAGED_LABEL_NAMES = Object.values(LABELS).map((label) => label.name);
|
const LEGACY_MANAGED_LABEL_NAMES = ["contribution: accepted"];
|
||||||
|
const MANAGED_LABEL_NAMES = [
|
||||||
|
...new Set([
|
||||||
|
...Object.values(LABELS).map((label) => label.name),
|
||||||
|
...LEGACY_MANAGED_LABEL_NAMES,
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
const CHECKBOXES = {
|
const CHECKBOXES = {
|
||||||
smallScope: "This PR is a bug fix or small-scope improvement.",
|
smallScope: "This PR is a bug fix or small-scope improvement.",
|
||||||
@@ -112,8 +124,13 @@ function findFeedbackUrl(body) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLabelNames(pr) {
|
||||||
|
return new Set((pr.labels || []).map((label) => label.name));
|
||||||
|
}
|
||||||
|
|
||||||
function analyzePullRequest(pr) {
|
function analyzePullRequest(pr) {
|
||||||
const body = normalizeBody(pr.body);
|
const body = normalizeBody(pr.body);
|
||||||
|
const labelNames = getLabelNames(pr);
|
||||||
const states = Object.fromEntries(
|
const states = Object.fromEntries(
|
||||||
Object.entries(CHECKBOXES).map(([key, label]) => [
|
Object.entries(CHECKBOXES).map(([key, label]) => [
|
||||||
key,
|
key,
|
||||||
@@ -135,6 +152,41 @@ function analyzePullRequest(pr) {
|
|||||||
changedFiles > LARGE_DIFF_CHANGED_FILES ||
|
changedFiles > LARGE_DIFF_CHANGED_FILES ||
|
||||||
totalChangedLines > LARGE_DIFF_CHANGED_LINES;
|
totalChangedLines > LARGE_DIFF_CHANGED_LINES;
|
||||||
|
|
||||||
|
if (labelNames.has(LABELS.outOfScope.name)) {
|
||||||
|
return {
|
||||||
|
blockers: [
|
||||||
|
{
|
||||||
|
label: LABELS.outOfScope.name,
|
||||||
|
message: "Marked out of scope by maintainer label.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
changedFiles,
|
||||||
|
desiredLabels: [LABELS.outOfScope.name],
|
||||||
|
largeDiff,
|
||||||
|
status: "out_of_scope",
|
||||||
|
templateUsed,
|
||||||
|
totalChangedLines,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelNames.has(LABELS.inScope.name)) {
|
||||||
|
const desiredLabels = [LABELS.inScope.name];
|
||||||
|
|
||||||
|
if (largeDiff) {
|
||||||
|
desiredLabels.push(LABELS.largeDiff.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
blockers: [],
|
||||||
|
changedFiles,
|
||||||
|
desiredLabels,
|
||||||
|
largeDiff,
|
||||||
|
status: "in_scope",
|
||||||
|
templateUsed,
|
||||||
|
totalChangedLines,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!templateUsed) {
|
if (!templateUsed) {
|
||||||
blockers.push({
|
blockers.push({
|
||||||
label: LABELS.needsTemplate.name,
|
label: LABELS.needsTemplate.name,
|
||||||
@@ -203,7 +255,7 @@ function analyzePullRequest(pr) {
|
|||||||
desiredLabels.add(
|
desiredLabels.add(
|
||||||
states.approvedFeedback
|
states.approvedFeedback
|
||||||
? LABELS.approvedFeedback.name
|
? LABELS.approvedFeedback.name
|
||||||
: LABELS.accepted.name,
|
: LABELS.inScope.name,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +268,7 @@ function analyzePullRequest(pr) {
|
|||||||
changedFiles,
|
changedFiles,
|
||||||
desiredLabels: [...desiredLabels],
|
desiredLabels: [...desiredLabels],
|
||||||
largeDiff,
|
largeDiff,
|
||||||
|
status: blockers.length === 0 ? "in_scope" : "blocked",
|
||||||
templateUsed,
|
templateUsed,
|
||||||
totalChangedLines,
|
totalChangedLines,
|
||||||
};
|
};
|
||||||
@@ -246,6 +299,38 @@ function buildBlockingComment(analysis) {
|
|||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildInScopeComment() {
|
||||||
|
return [
|
||||||
|
COMMENT_MARKER,
|
||||||
|
"Thanks for the PR. This appears to match Yaak's contribution policy and is awaiting review by @gschier.",
|
||||||
|
"",
|
||||||
|
"This only means the PR is in scope for review. It does not mean the change has been reviewed or accepted for merge.",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOutOfScopeComment() {
|
||||||
|
return [
|
||||||
|
COMMENT_MARKER,
|
||||||
|
"Thanks for the PR. This does not appear to match Yaak's current contribution policy.",
|
||||||
|
"",
|
||||||
|
"Yaak currently accepts community PRs for bug fixes, small-scope improvements, or changes tied to a maintainer-reviewed feedback item from https://yaak.app/feedback.",
|
||||||
|
"",
|
||||||
|
"If this PR is tied to a feedback item where contribution approval was explicitly stated, please link it in the PR description.",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPolicyComment(analysis) {
|
||||||
|
if (analysis.status === "out_of_scope") {
|
||||||
|
return buildOutOfScopeComment();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (analysis.blockers.length > 0) {
|
||||||
|
return buildBlockingComment(analysis);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildInScopeComment();
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(value) {
|
function escapeHtml(value) {
|
||||||
return String(value)
|
return String(value)
|
||||||
.replace(/&/g, "&")
|
.replace(/&/g, "&")
|
||||||
@@ -269,9 +354,9 @@ function escapeTableText(value) {
|
|||||||
|
|
||||||
function summarizeResult({ pr, analysis, skipped, skipReason }) {
|
function summarizeResult({ pr, analysis, skipped, skipReason }) {
|
||||||
const comment =
|
const comment =
|
||||||
analysis?.blockers.length > 0
|
analysis == null
|
||||||
? buildBlockingComment(analysis).replace(COMMENT_MARKER, "").trim()
|
? "None"
|
||||||
: "None";
|
: buildPolicyComment(analysis).replace(COMMENT_MARKER, "").trim();
|
||||||
const summary = {
|
const summary = {
|
||||||
blocked: analysis?.blockers.length > 0,
|
blocked: analysis?.blockers.length > 0,
|
||||||
comment,
|
comment,
|
||||||
@@ -282,7 +367,7 @@ function summarizeResult({ pr, analysis, skipped, skipReason }) {
|
|||||||
: "None",
|
: "None",
|
||||||
number: pr.number,
|
number: pr.number,
|
||||||
prLink: `<a href="${escapeHtml(pr.html_url)}">#${pr.number}</a>`,
|
prLink: `<a href="${escapeHtml(pr.html_url)}">#${pr.number}</a>`,
|
||||||
status: "Accepted",
|
status: "In scope",
|
||||||
title: escapeHtml(truncateTitle(pr.title)),
|
title: escapeHtml(truncateTitle(pr.title)),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -305,13 +390,13 @@ function summarizeResult({ pr, analysis, skipped, skipReason }) {
|
|||||||
analysis.blockers.map((blocker) => blocker.message).join("; "),
|
analysis.blockers.map((blocker) => blocker.message).join("; "),
|
||||||
),
|
),
|
||||||
labels: escapeHtml(summary.labels),
|
labels: escapeHtml(summary.labels),
|
||||||
status: "Blocked",
|
status: analysis.status === "out_of_scope" ? "Out of scope" : "Blocked",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...summary,
|
...summary,
|
||||||
comment: "None",
|
comment: escapeTableText(summary.comment),
|
||||||
labels: escapeHtml(summary.labels),
|
labels: escapeHtml(summary.labels),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -459,6 +544,27 @@ async function deletePolicyComment({ github, owner, repo, issueNumber }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function requestMaintainerReview({ github, owner, repo, pr }) {
|
||||||
|
if (pr.user.login === REVIEWER_LOGIN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await github.rest.pulls.requestReviewers({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pull_number: pr.number,
|
||||||
|
reviewers: [REVIEWER_LOGIN],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status === 422) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function checkPullRequest({
|
async function checkPullRequest({
|
||||||
github,
|
github,
|
||||||
core,
|
core,
|
||||||
@@ -538,7 +644,7 @@ async function checkPullRequest({
|
|||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
issueNumber,
|
issueNumber,
|
||||||
body: buildBlockingComment(analysis),
|
body: buildPolicyComment(analysis),
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
blocked: true,
|
blocked: true,
|
||||||
@@ -548,7 +654,14 @@ async function checkPullRequest({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await deletePolicyComment({ github, owner, repo, issueNumber });
|
await upsertPolicyComment({
|
||||||
|
github,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issueNumber,
|
||||||
|
body: buildPolicyComment(analysis),
|
||||||
|
});
|
||||||
|
await requestMaintainerReview({ github, owner, repo, pr });
|
||||||
core.notice(`Contribution policy check passed for PR #${pr.number}.`);
|
core.notice(`Contribution policy check passed for PR #${pr.number}.`);
|
||||||
return {
|
return {
|
||||||
blocked: false,
|
blocked: false,
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
|
pull_request_target:
|
||||||
|
types: [labeled, unlabeled]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
issues: write
|
issues: write
|
||||||
pull-requests: read
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
@@ -22,6 +24,7 @@ jobs:
|
|||||||
- name: Checkout policy script
|
- name: Checkout policy script
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.base.sha || github.ref }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Check contribution policy
|
- name: Check contribution policy
|
||||||
|
|||||||
Reference in New Issue
Block a user