Skip to content

Commit ec68432

Browse files
committed
feat: codeowners-review-analysis hitting sets
1 parent 76392de commit ec68432

File tree

5 files changed

+596
-20
lines changed

5 files changed

+596
-20
lines changed

actions/codeowners-review-analysis/dist/index.js

Lines changed: 141 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25759,7 +25759,7 @@ function getReviewForStatusFor(codeowner, currentReviewStatus) {
2575925759

2576025760
// actions/codeowners-review-analysis/src/strings.ts
2576125761
var LEGEND = `Legend: ${iconFor(PullRequestReviewStateExt.Approved)} Approved | ${iconFor(PullRequestReviewStateExt.ChangesRequested)} Changes Requested | ${iconFor(PullRequestReviewStateExt.Commented)} Commented | ${iconFor(PullRequestReviewStateExt.Dismissed)} Dismissed | ${iconFor(PullRequestReviewStateExt.Pending)} Pending | ${iconFor("UNKNOWN" /* Unknown */)} Unknown`;
25762-
function formatPendingReviewsMarkdown(entryMap, summaryUrl) {
25762+
function formatPendingReviewsMarkdown(entryMap, summaryUrl, minimumHittingSets) {
2576325763
const lines = [];
2576425764
lines.push("### Codeowners Review Summary");
2576525765
lines.push("");
@@ -25782,13 +25782,18 @@ function formatPendingReviewsMarkdown(entryMap, summaryUrl) {
2578225782
`| ${patternCell} | ${overallIcon} | ${processed.files.length} |${owners.join(", ")} |`
2578325783
);
2578425784
}
25785-
if (summaryUrl) {
25785+
const recommendations = getReviewerRecommendations(minimumHittingSets, 2);
25786+
if (recommendations.length > 0) {
2578625787
lines.push("");
25787-
lines.push(
25788-
`For more details, see the [full review summary](${summaryUrl}).`
25789-
);
25788+
lines.push("### Reviewer Recommendations");
25789+
recommendations.forEach((rec) => {
25790+
lines.push(`- ${rec}`);
25791+
});
2579025792
lines.push("");
2579125793
}
25794+
lines.push("");
25795+
lines.push("---");
25796+
lines.push("");
2579225797
const {
2579325798
runId,
2579425799
repo: { owner, repo }
@@ -25798,10 +25803,26 @@ function formatPendingReviewsMarkdown(entryMap, summaryUrl) {
2579825803
`Refresh analysis with: \`gh run rerun ${runId} -R ${owner}/${repo}\``
2579925804
);
2580025805
}
25806+
if (summaryUrl) {
25807+
lines.push("");
25808+
lines.push(
25809+
`For more details, see the [full review summary](${summaryUrl}).`
25810+
);
25811+
lines.push("");
25812+
}
2580125813
return lines.join("\n");
2580225814
}
25803-
async function formatAllReviewsSummaryByEntry(entryMap) {
25815+
async function formatAllReviewsSummaryByEntry(entryMap, minimumHittingSets) {
2580425816
core6.summary.addHeading("Codeowners Review Details", 2).addRaw(LEGEND).addBreak();
25817+
const recommendations = getReviewerRecommendations(minimumHittingSets, 10);
25818+
if (recommendations.length > 0) {
25819+
core6.summary.addHeading(
25820+
`Reviewer Recommendations (${recommendations.length} of ${minimumHittingSets.size})`,
25821+
3
25822+
);
25823+
core6.summary.addList(recommendations);
25824+
core6.summary.addBreak();
25825+
}
2580525826
const sortedEntries = [...entryMap.entries()].sort(([a, _], [b, __]) => {
2580625827
return a.lineNumber - b.lineNumber;
2580725828
});
@@ -25880,6 +25901,114 @@ async function formatAllReviewsSummaryByEntry(entryMap) {
2588025901
}
2588125902
await core6.summary.addSeparator().write();
2588225903
}
25904+
function getReviewerRecommendations(minimumHittingSets, limit = 3) {
25905+
if (minimumHittingSets.size === 0) {
25906+
return [];
25907+
}
25908+
const setsArray = Array.from(minimumHittingSets);
25909+
const minimumSize = setsArray[0].length;
25910+
const numberOfSetsToSuggest = Math.min(minimumSize, limit, setsArray.length);
25911+
const trimmedSets = setsArray.slice(0, numberOfSetsToSuggest);
25912+
return trimmedSets.map((set2) => `${set2.join(", ")}`);
25913+
}
25914+
25915+
// actions/codeowners-review-analysis/src/hitting-sets.ts
25916+
function calculateAllMinimumHittingSets(reviewSummary) {
25917+
const { superset, subsets } = getSupersetAndSubsets(reviewSummary);
25918+
if (superset.size === 0 || superset.size > 12 || subsets.length === 0) {
25919+
return /* @__PURE__ */ new Set();
25920+
}
25921+
for (let k = 1; k <= superset.size; k++) {
25922+
const validHittingSets = /* @__PURE__ */ new Set();
25923+
for (const combo of combinations(superset, k)) {
25924+
const candidateSet = new Set(combo);
25925+
const hitsAll = subsets.every((subset) => {
25926+
for (const elem of subset) {
25927+
if (candidateSet.has(elem)) {
25928+
return true;
25929+
}
25930+
}
25931+
return false;
25932+
});
25933+
if (hitsAll) {
25934+
validHittingSets.add(combo);
25935+
}
25936+
}
25937+
if (validHittingSets.size > 0) {
25938+
return validHittingSets;
25939+
}
25940+
}
25941+
return /* @__PURE__ */ new Set();
25942+
}
25943+
function combinations(superset, k) {
25944+
const supersetArr = Array.from(superset).sort();
25945+
const results = [];
25946+
function backtrack(start, path) {
25947+
if (path.length === k) {
25948+
results.push([...path]);
25949+
return;
25950+
}
25951+
for (let i = start; i < supersetArr.length; i++) {
25952+
path.push(supersetArr[i]);
25953+
backtrack(i + 1, path);
25954+
path.pop();
25955+
}
25956+
}
25957+
backtrack(0, []);
25958+
return results;
25959+
}
25960+
function getSupersetAndSubsets(reviewSummary) {
25961+
const allPendingOwners = /* @__PURE__ */ new Set();
25962+
const allPendingEntries = [];
25963+
const subsetsSeen = /* @__PURE__ */ new Set();
25964+
for (const [entry, processed] of reviewSummary.entries()) {
25965+
if (processed.overallStatus !== PullRequestReviewStateExt.Approved) {
25966+
entry.owners.forEach((owner) => {
25967+
allPendingOwners.add(owner);
25968+
});
25969+
if (entry.owners.length > 0) {
25970+
addIfUnique(subsetsSeen, allPendingEntries, entry.owners);
25971+
}
25972+
}
25973+
}
25974+
const minimizedSubsets = removeSupersets(allPendingEntries);
25975+
return { superset: allPendingOwners, subsets: minimizedSubsets };
25976+
}
25977+
function addIfUnique(seen, set2, item) {
25978+
const normalized = JSON.stringify([...item].sort());
25979+
if (!seen.has(normalized)) {
25980+
seen.add(normalized);
25981+
set2.push(new Set(item));
25982+
}
25983+
}
25984+
function removeSupersets(sets) {
25985+
const arrs = sets.map((s) => [...s].sort());
25986+
arrs.sort((a, b) => a.length - b.length);
25987+
const keep = [];
25988+
outer: for (const S of arrs) {
25989+
for (const T of keep) {
25990+
if (isSubset(T, S)) {
25991+
continue outer;
25992+
}
25993+
}
25994+
keep.push(S);
25995+
}
25996+
return keep.map((a) => new Set(a));
25997+
}
25998+
function isSubset(A, B) {
25999+
let i = 0, j = 0;
26000+
while (i < A.length && j < B.length) {
26001+
if (A[i] === B[j]) {
26002+
i++;
26003+
j++;
26004+
} else if (A[i] > B[j]) {
26005+
j++;
26006+
} else {
26007+
return false;
26008+
}
26009+
}
26010+
return i === A.length;
26011+
}
2588326012

2588426013
// actions/codeowners-review-analysis/src/run.ts
2588526014
async function run() {
@@ -25932,17 +26061,18 @@ async function run() {
2593226061
);
2593326062
core7.endGroup();
2593426063
core7.startGroup("Create CODEOWNERS Summary");
25935-
const codeownersSummary = createReviewSummaryObjectV2(
26064+
const codeownersSummary = createReviewSummaryObject(
2593626065
currentPRReviewState,
2593726066
codeOwnersEntryToFiles
2593826067
);
25939-
await formatAllReviewsSummaryByEntry(codeownersSummary);
26068+
const minimumHittingSets = calculateAllMinimumHittingSets(codeownersSummary);
26069+
await formatAllReviewsSummaryByEntry(codeownersSummary, minimumHittingSets);
2594026070
const summaryUrl = await getSummaryUrl(octokit, owner, repo);
2594126071
const pendingReviewMarkdown = formatPendingReviewsMarkdown(
2594226072
codeownersSummary,
25943-
summaryUrl
26073+
summaryUrl,
26074+
minimumHittingSets
2594426075
);
25945-
console.log(pendingReviewMarkdown);
2594626076
if (inputs.postComment) {
2594726077
await upsertPRComment(
2594826078
octokit,
@@ -25958,7 +26088,7 @@ async function run() {
2595826088
core7.setFailed(`Action failed: ${error}`);
2595926089
}
2596026090
}
25961-
function createReviewSummaryObjectV2(currentReviewStatus, codeOwnersEntryToFiles) {
26091+
function createReviewSummaryObject(currentReviewStatus, codeOwnersEntryToFiles) {
2596226092
const reviewSummary = /* @__PURE__ */ new Map();
2596326093
for (const [entry, files] of codeOwnersEntryToFiles.entries()) {
2596426094
const ownerReviewStatuses = [];

0 commit comments

Comments
 (0)