workflows/reviewers: First-class team package maintainer review requests

This commit is contained in:
Silvan Mosberger
2025-10-22 17:50:49 +02:00
parent dabc1fab6e
commit e6c822f51f
6 changed files with 94 additions and 24 deletions

View File

@@ -146,19 +146,14 @@ jobs:
- name: Requesting maintainer reviews - name: Requesting maintainer reviews
if: ${{ steps.app-token.outputs.token }} if: ${{ steps.app-token.outputs.token }}
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPOSITORY: ${{ github.repository }} REPOSITORY: ${{ github.repository }}
ORG_ID: ${{ github.repository_owner_id }}
NUMBER: ${{ github.event.number }} NUMBER: ${{ github.event.number }}
AUTHOR: ${{ github.event.pull_request.user.login }} AUTHOR: ${{ github.event.pull_request.user.login }}
# Don't request reviewers on draft PRs # Don't request reviewers on draft PRs
DRY_MODE: ${{ github.event.pull_request.draft && '1' || '' }} DRY_MODE: ${{ github.event.pull_request.draft && '1' || '' }}
run: | run: result/bin/request-maintainers-reviews.sh "$ORG_ID" "$REPOSITORY" "$NUMBER" "$AUTHOR" comparison
# maintainers.json contains GitHub IDs. Look up handles to request reviews from.
# There appears to be no API to request reviews based on GitHub IDs
jq -r 'keys[]' comparison/maintainers.json \
| while read -r id; do gh api /user/"$id" --jq .login; done \
| GH_TOKEN="$APP_GH_TOKEN" result/bin/request-reviewers.sh "$REPOSITORY" "$NUMBER" "$AUTHOR"
- name: Log current API rate limits (app-token) - name: Log current API rate limits (app-token)
if: ${{ steps.app-token.outputs.token }} if: ${{ steps.app-token.outputs.token }}

View File

@@ -179,8 +179,12 @@ runCommand "compare"
jq jq
cmp-stats cmp-stats
]; ];
maintainers = builtins.toJSON maintainers; maintainers = builtins.toJSON maintainers.users;
passAsFile = [ "maintainers" ]; teams = builtins.toJSON maintainers.teams;
passAsFile = [
"maintainers"
"teams"
];
} }
'' ''
mkdir $out mkdir $out
@@ -223,4 +227,5 @@ runCommand "compare"
fi fi
cp "$maintainersPath" "$out/maintainers.json" cp "$maintainersPath" "$out/maintainers.json"
cp "$teamsPath" "$out/teams.json"
'' ''

View File

@@ -51,15 +51,16 @@ let
# updates to .json files. # updates to .json files.
# TODO: Support by-name package sets. # TODO: Support by-name package sets.
filenames = lib.optional (lib.length path == 1) "pkgs/by-name/${sharded (lib.head path)}/"; filenames = lib.optional (lib.length path == 1) "pkgs/by-name/${sharded (lib.head path)}/";
# TODO: Refactor this so we can ping entire teams instead of the individual members. # meta.maintainers also contains all individual team members.
# Note that this will require keeping track of GH team IDs in "maintainers/teams.nix". # We only want to ping individuals if they're added individually as maintainers, not via teams.
maintainers = package.meta.maintainers or [ ]; maintainers = package.meta.nonTeamMaintainers or [ ];
teams = package.meta.teams or [ ];
} }
)) ))
# No need to match up packages without maintainers with their files. # No need to match up packages without maintainers with their files.
# This also filters out attributes where `packge = null`, which is the # This also filters out attributes where `packge = null`, which is the
# case for libintl, for example. # case for libintl, for example.
(lib.filter (pkg: pkg.maintainers != [ ])) (lib.filter (pkg: pkg.maintainers != [ ] || pkg.teams != [ ]))
]; ];
relevantFilenames = relevantFilenames =
@@ -94,20 +95,43 @@ let
attrsWithModifiedFiles = lib.filter (pkg: anyMatchingFiles pkg.filenames) attrsWithFilenames; attrsWithModifiedFiles = lib.filter (pkg: anyMatchingFiles pkg.filenames) attrsWithFilenames;
listToPing = lib.concatMap ( userPings =
pkg: pkg:
map (maintainer: { map (maintainer: {
id = maintainer.githubId; type = "user";
inherit (maintainer) github; user = toString maintainer.githubId;
packageName = pkg.name; packageName = pkg.name;
dueToFiles = pkg.filenames; });
}) pkg.maintainers
teamPings =
pkg: team:
if team ? github then
[
{
type = "team";
team = toString team.githubId;
packageName = pkg.name;
}
]
else
userPings pkg team.members;
maintainersToPing = lib.concatMap (
pkg: userPings pkg pkg.maintainers ++ lib.concatMap (teamPings pkg) pkg.teams
) attrsWithModifiedFiles; ) attrsWithModifiedFiles;
byMaintainer = lib.groupBy (ping: toString ping.id) listToPing; byType = lib.groupBy (ping: ping.type) maintainersToPing;
packagesPerMaintainer = lib.mapAttrs ( byUser = lib.pipe (byType.user or [ ]) [
maintainer: packages: map (pkg: pkg.packageName) packages (lib.groupBy (ping: ping.user))
) byMaintainer; (lib.mapAttrs (_user: lib.map (pkg: pkg.packageName)))
];
byTeam = lib.pipe (byType.team or [ ]) [
(lib.groupBy (ping: ping.team))
(lib.mapAttrs (_team: lib.map (pkg: pkg.packageName)))
];
in in
packagesPerMaintainer {
users = byUser;
teams = byTeam;
}

View File

@@ -17,6 +17,7 @@ stdenvNoCC.mkDerivation {
./get-code-owners.sh ./get-code-owners.sh
./request-reviewers.sh ./request-reviewers.sh
./request-code-owner-reviews.sh ./request-code-owner-reviews.sh
./request-maintainers-reviews.sh
]; ];
}; };
nativeBuildInputs = [ makeWrapper ]; nativeBuildInputs = [ makeWrapper ];
@@ -26,6 +27,7 @@ stdenvNoCC.mkDerivation {
for bin in *.sh; do for bin in *.sh; do
mv "$bin" "$out/bin" mv "$bin" "$out/bin"
wrapProgram "$out/bin/$bin" \ wrapProgram "$out/bin/$bin" \
--set PAGER cat \
--set PATH ${ --set PATH ${
lib.makeBinPath [ lib.makeBinPath [
coreutils coreutils

View File

@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Requests maintainer reviews for a PR
set -euo pipefail
SCRIPT_DIR=$(dirname "$0")
if (( $# < 5 )); then
echo >&2 "Usage: $0 ORG_ID GITHUB_REPO PR_NUMBER AUTHOR COMPARISON_PATH"
exit 1
fi
orgId=$1
baseRepo=$2
prNumber=$3
prAuthor=$4
comparisonPath=$5
org="${baseRepo%/*}"
# maintainers.json/teams.json contains GitHub IDs. Look up handles to request reviews from.
# There appears to be no API to request reviews based on IDs
{
jq -r 'keys[]' "$comparisonPath"/maintainers.json \
| while read -r id; do
if login=$(gh api /user/"$id" --jq .login); then
echo "$login"
else
echo >&2 "Skipping user with id $id: $login"
fi
done
jq -r 'keys[]' "$comparisonPath"/teams.json \
| while read -r id; do
if slug=$(gh api /organizations/"$orgId"/team/"$id" --jq .slug); then
echo "$org/$slug"
else
echo >&2 "Skipping team with id $id: $slug"
fi
done
} | "$SCRIPT_DIR"/request-reviewers.sh "$baseRepo" "$prNumber" "$prAuthor"

View File

@@ -397,6 +397,7 @@ let
]; ];
sourceProvenance = listOf attrs; sourceProvenance = listOf attrs;
maintainers = listOf (attrsOf any); # TODO use the maintainer type from lib/tests/maintainer-module.nix maintainers = listOf (attrsOf any); # TODO use the maintainer type from lib/tests/maintainer-module.nix
nonTeamMaintainers = listOf (attrsOf any); # TODO use the maintainer type from lib/tests/maintainer-module.nix
teams = listOf (attrsOf any); # TODO similar to maintainers, use a teams type teams = listOf (attrsOf any); # TODO similar to maintainers, use a teams type
priority = int; priority = int;
pkgConfigModules = listOf str; pkgConfigModules = listOf str;
@@ -670,6 +671,10 @@ let
maintainers = maintainers =
attrs.meta.maintainers or [ ] ++ concatMap (team: team.members or [ ]) attrs.meta.teams or [ ]; attrs.meta.maintainers or [ ] ++ concatMap (team: team.members or [ ]) attrs.meta.teams or [ ];
# Needed for CI to be able to avoid requesting reviews from individual
# team members
nonTeamMaintainers = attrs.meta.maintainers or [ ];
identifiers = identifiers =
let let
# nix-env writes a warning for each derivation that has null in its meta values, so # nix-env writes a warning for each derivation that has null in its meta values, so