ci/github-script/labels: set maintainer labels from latest maintainer map (#457243)
This commit is contained in:
2
.github/workflows/eval.yml
vendored
2
.github/workflows/eval.yml
vendored
@@ -202,7 +202,6 @@ jobs:
|
||||
|
||||
- name: Compare against the target branch
|
||||
env:
|
||||
AUTHOR_ID: ${{ github.event.pull_request.user.id }}
|
||||
TARGET_SHA: ${{ inputs.mergedSha }}
|
||||
run: |
|
||||
git -C nixpkgs/trusted diff --name-only "$TARGET_SHA" \
|
||||
@@ -212,7 +211,6 @@ jobs:
|
||||
nix-build nixpkgs/trusted/ci --arg nixpkgs ./nixpkgs/trusted-pinned -A eval.compare \
|
||||
--arg combinedDir ./combined \
|
||||
--arg touchedFilesJson ./touched-files.json \
|
||||
--argstr githubAuthorId "$AUTHOR_ID" \
|
||||
--out-link comparison
|
||||
|
||||
cat comparison/step-summary.md >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
4
.github/workflows/labels.yml
vendored
4
.github/workflows/labels.yml
vendored
@@ -39,10 +39,6 @@ jobs:
|
||||
update:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
if: github.event_name != 'schedule' || github.repository_owner == 'NixOS'
|
||||
env:
|
||||
# TODO: Remove after 2026-03-04, when Node 24 becomes the default.
|
||||
# https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
|
||||
@@ -48,7 +48,6 @@ in
|
||||
{
|
||||
combinedDir,
|
||||
touchedFilesJson,
|
||||
githubAuthorId,
|
||||
}:
|
||||
let
|
||||
# Usually we expect a derivation, but when evaluating in multiple separate steps, we pass
|
||||
@@ -155,22 +154,19 @@ let
|
||||
# Only set this label when no other label with indication for staging has been set.
|
||||
# This avoids confusion whether to target staging or batch this with kernel updates.
|
||||
lib.last (lib.sort lib.lessThan (lib.attrValues rebuildCountByKernel)) <= 500;
|
||||
# Set the "11.by: package-maintainer" label to whether all packages directly
|
||||
# changed are maintained by the PR's author.
|
||||
"11.by: package-maintainer" =
|
||||
maintainers ? ${githubAuthorId}
|
||||
&& lib.all (lib.flip lib.elem maintainers.${githubAuthorId}) (
|
||||
lib.flatten (lib.attrValues maintainers)
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
maintainers = callPackage ./maintainers.nix { } {
|
||||
changedattrs = lib.attrNames (lib.groupBy (a: a.name) changedPackagePlatformAttrs);
|
||||
changedpathsjson = touchedFilesJson;
|
||||
removedattrs = lib.attrNames (lib.groupBy (a: a.name) removedPackagePlatformAttrs);
|
||||
};
|
||||
inherit
|
||||
(callPackage ./maintainers.nix { } {
|
||||
changedattrs = lib.attrNames (lib.groupBy (a: a.name) changedPackagePlatformAttrs);
|
||||
changedpathsjson = touchedFilesJson;
|
||||
removedattrs = lib.attrNames (lib.groupBy (a: a.name) removedPackagePlatformAttrs);
|
||||
})
|
||||
maintainers
|
||||
packages
|
||||
;
|
||||
in
|
||||
runCommand "compare"
|
||||
{
|
||||
@@ -180,7 +176,11 @@ runCommand "compare"
|
||||
cmp-stats
|
||||
];
|
||||
maintainers = builtins.toJSON maintainers;
|
||||
passAsFile = [ "maintainers" ];
|
||||
packages = builtins.toJSON packages;
|
||||
passAsFile = [
|
||||
"maintainers"
|
||||
"packages"
|
||||
];
|
||||
}
|
||||
''
|
||||
mkdir $out
|
||||
@@ -223,4 +223,5 @@ runCommand "compare"
|
||||
fi
|
||||
|
||||
cp "$maintainersPath" "$out/maintainers.json"
|
||||
cp "$packagesPath" "$out/packages.json"
|
||||
''
|
||||
|
||||
@@ -105,9 +105,9 @@ let
|
||||
) attrsWithModifiedFiles;
|
||||
|
||||
byMaintainer = lib.groupBy (ping: toString ping.id) listToPing;
|
||||
|
||||
packagesPerMaintainer = lib.mapAttrs (
|
||||
maintainer: packages: map (pkg: pkg.packageName) packages
|
||||
) byMaintainer;
|
||||
in
|
||||
packagesPerMaintainer
|
||||
{
|
||||
maintainers = lib.mapAttrs (_: lib.catAttrs "packageName") byMaintainer;
|
||||
|
||||
packages = lib.catAttrs "packageName" listToPing;
|
||||
}
|
||||
|
||||
@@ -281,9 +281,6 @@ let
|
||||
# Whether to evaluate on a specific set of systems, by default all are evaluated
|
||||
evalSystems ? if quickTest then [ "x86_64-linux" ] else supportedSystems,
|
||||
baseline,
|
||||
# Which maintainer should be considered the author?
|
||||
# Defaults to nixpkgs-ci which is not a maintainer and skips the check.
|
||||
githubAuthorId ? "nixpkgs-ci",
|
||||
# What files have been touched? Defaults to none; use the expression below to calculate it.
|
||||
# ```
|
||||
# git diff --name-only --merge-base master HEAD \
|
||||
@@ -307,7 +304,7 @@ let
|
||||
};
|
||||
comparisonReport = compare {
|
||||
combinedDir = combine { diffDir = diffs; };
|
||||
inherit touchedFilesJson githubAuthorId;
|
||||
inherit touchedFilesJson;
|
||||
};
|
||||
in
|
||||
comparisonReport;
|
||||
|
||||
@@ -3,9 +3,98 @@ module.exports = async ({ github, context, core, dry }) => {
|
||||
const { DefaultArtifactClient } = require('@actions/artifact')
|
||||
const { readFile, writeFile } = require('node:fs/promises')
|
||||
const withRateLimit = require('./withRateLimit.js')
|
||||
const { classify } = require('../supportedBranches.js')
|
||||
|
||||
const artifactClient = new DefaultArtifactClient()
|
||||
|
||||
async function downloadMaintainerMap(branch) {
|
||||
let run
|
||||
|
||||
const commits = (
|
||||
await github.rest.repos.listCommits({
|
||||
...context.repo,
|
||||
sha: branch,
|
||||
// We look at 10 commits to find a maintainer map, but this is an arbitrary number. The
|
||||
// head commit might not have a map, if the queue was bypassed to merge it. This happens
|
||||
// frequently on staging-esque branches. The branch with the highest chance of getting
|
||||
// 10 consecutive bypassing commits is the stable staging-next branch. Luckily, this
|
||||
// also means that the number of PRs open towards that branch is very low, so falling
|
||||
// back to slightly imprecise maintainer data from master only has a marginal effect.
|
||||
per_page: 10,
|
||||
})
|
||||
).data
|
||||
|
||||
for (const commit of commits) {
|
||||
const run = (
|
||||
await github.rest.actions.listWorkflowRuns({
|
||||
...context.repo,
|
||||
workflow_id: 'merge-group.yml',
|
||||
status: 'success',
|
||||
exclude_pull_requests: true,
|
||||
per_page: 1,
|
||||
head_sha: commit.sha,
|
||||
})
|
||||
).data.workflow_runs[0]
|
||||
if (!run) continue
|
||||
|
||||
const artifact = (
|
||||
await github.rest.actions.listWorkflowRunArtifacts({
|
||||
...context.repo,
|
||||
run_id: run.id,
|
||||
name: 'maintainers',
|
||||
})
|
||||
).data.artifacts[0]
|
||||
if (!artifact) continue
|
||||
|
||||
await artifactClient.downloadArtifact(artifact.id, {
|
||||
findBy: {
|
||||
repositoryName: context.repo.repo,
|
||||
repositoryOwner: context.repo.owner,
|
||||
token: core.getInput('github-token'),
|
||||
},
|
||||
path: path.resolve(path.join('branches', branch)),
|
||||
expectedHash: artifact.digest,
|
||||
})
|
||||
|
||||
return JSON.parse(
|
||||
await readFile(
|
||||
path.resolve(path.join('branches', branch, 'maintainers.json')),
|
||||
'utf-8',
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// We get here when none of the 10 commits we looked at contained a maintainer map.
|
||||
// For the master branch, we don't have any fallback options, so we error out.
|
||||
// For other branches, we select a suitable fallback below.
|
||||
if (branch === 'master') throw new Error('No maintainer map found.')
|
||||
|
||||
const { stable, version } = classify(branch)
|
||||
|
||||
const release = `release-${version}`
|
||||
if (stable && branch !== release) {
|
||||
// Only fallback to the release branch from *other* stable branches.
|
||||
// Explicitly avoids infinite recursion.
|
||||
return await getMaintainerMap(release)
|
||||
} else {
|
||||
// Falling back to master as last resort.
|
||||
// This can either be the case for unstable staging-esque or wip branches,
|
||||
// or for the primary stable branch (release-XX.YY).
|
||||
return await getMaintainerMap('master')
|
||||
}
|
||||
}
|
||||
|
||||
// Simple cache for maintainer maps to avoid downloading the same artifacts
|
||||
// over and over again. Ultimately returns a promise, so the result must be
|
||||
// awaited for.
|
||||
const maintainerMaps = {}
|
||||
function getMaintainerMap(branch) {
|
||||
if (!maintainerMaps[branch]) {
|
||||
maintainerMaps[branch] = downloadMaintainerMap(branch)
|
||||
}
|
||||
return maintainerMaps[branch]
|
||||
}
|
||||
|
||||
async function handlePullRequest({ item, stats }) {
|
||||
const log = (k, v) => core.info(`PR #${item.number} - ${k}: ${v}`)
|
||||
|
||||
@@ -177,21 +266,44 @@ module.exports = async ({ github, context, core, dry }) => {
|
||||
expectedHash: artifact.digest,
|
||||
})
|
||||
|
||||
const maintainers = new Set(
|
||||
Object.keys(
|
||||
JSON.parse(
|
||||
await readFile(`${pull_number}/maintainers.json`, 'utf-8'),
|
||||
),
|
||||
).map((m) => Number.parseInt(m, 10)),
|
||||
)
|
||||
|
||||
const evalLabels = JSON.parse(
|
||||
await readFile(`${pull_number}/changed-paths.json`, 'utf-8'),
|
||||
).labels
|
||||
|
||||
// TODO: Get "changed packages" information from list of changed by-name files
|
||||
// in addition to just the Eval results, to make this work for these packages
|
||||
// when Eval results have expired as well.
|
||||
let packages
|
||||
try {
|
||||
packages = JSON.parse(
|
||||
await readFile(`${pull_number}/packages.json`, 'utf-8'),
|
||||
)
|
||||
} catch (e) {
|
||||
if (e.code !== 'ENOENT') throw e
|
||||
// TODO: Remove this fallback code once all old artifacts without packages.json
|
||||
// have expired. This should be the case in ~ February 2026.
|
||||
packages = Array.from(
|
||||
new Set(
|
||||
Object.values(
|
||||
JSON.parse(
|
||||
await readFile(`${pull_number}/maintainers.json`, 'utf-8'),
|
||||
),
|
||||
).flat(1),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const maintainers = await getMaintainerMap(pull_request.base.ref)
|
||||
|
||||
Object.assign(prLabels, evalLabels, {
|
||||
'12.approved-by: package-maintainer':
|
||||
maintainers.intersection(approvals).size > 0,
|
||||
'11.by: package-maintainer':
|
||||
packages.length &&
|
||||
packages.every((pkg) =>
|
||||
maintainers[pkg].includes(pull_request.user.id),
|
||||
),
|
||||
'12.approved-by: package-maintainer': packages.some((pkg) =>
|
||||
maintainers[pkg].some((m) => approvals.has(m)),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user