ci: replace actions/labeler with an in-tree implementation

actions/labeler applies path based labels by reading the pull request's full
label set and writing it back with setLabels(). It re-reads the labels just
before writing, but the window is still open.

Match the configured patterns in the workflow instead and apply them with
addLabels() and removeLabel(), which only ever touch the labels the step
decided on. The configuration moves to .github/labeler.json so parsing needs
nothing github-script does not already provide, and "*" now means any run of
characters, "/" included. That also makes the patterns say what they looked
like they said: "src/home*" matched nothing below src/home/, so the homed
label was dead.

Crossing the too-many-labels limit in either direction takes the other
outcome's labels back off, so a pull request cannot end up carrying tree-wide
and a handful of subsystem labels at the same time.

The pull_request trigger is there to check a configuration change against the
head copy, so it no longer writes anything: a fork only ever got a read-only
token, and pull_request_target runs for the same pull request, so the two would
fight over the labels. Drop its paths-ignore in turn, because it only kept
pull_request_target away when the pull request changed nothing else, and a
configuration change bundled with other files left nobody doing the writing.
This commit is contained in:
Daan De Meyer authored and Daan De Meyer committed 2026-09-26 16:57:48 +02:00
1 parent 4ed4b428ba
commit 1060fd7472
3 files changed
+250 -340

No files matched your search

+146
View File
@@ -0,0 +1,146 @@
{
"max-labels": 5,
"too-many-labels": "tree-wide",
"labels": {
"analyze": ["src/analyze/*"],
"apparmor": ["*apparmor*"],
"ask-password": ["*ask-password*"],
"backlight": ["*backlight*"],
"battery-check 🪫": ["*battery*"],
"binfmt": ["*binfmt*"],
"bsod": ["*bsod*"],
"btrfs": ["*btrfs*"],
"build-system": ["meson_options.txt", "*meson.build"],
"busctl": ["*busctl*"],
"catalog": ["catalog/*"],
"cgls": ["*cgls*"],
"cgroups": [
"src/basic/cgroup*",
"src/cgroups-agent/*",
"src/core/cgroup*",
"src/shared/cgroup*"
],
"cgtop": ["*cgtop*"],
"ci": [".github/*"],
"coccinelle": ["coccinelle/*"],
"coredump": ["*coredump*"],
"creds": ["src/core/*cred*", "src/creds/*", "src/shared/*cred*", "units/*cred*"],
"cryptsetup": ["src/cryptenroll/*", "src/cryptsetup/*", "units/*crypt*"],
"debug-generator": ["*debug-generator*"],
"delta": ["*delta*"],
"detect-virt": ["*virt*"],
"dissect": ["*dissect*"],
"documentation": ["NEWS", "README*", "docs/*", "man/*"],
"env-generator": ["*environment*generator*"],
"escape": ["src/escape/*"],
"firstboot": ["*firstboot*"],
"fsck": ["*fsck*"],
"fstab-generator": ["*fstab-generator*"],
"gpt-auto": ["*gpt-auto*"],
"growfs": ["*growfs*"],
"hibernate-resume": ["*hibernate-resume*"],
"homed": ["src/home*", "units/*home*"],
"hostname": ["src/hostname/*", "src/shared/*hostname*", "units/*hostname*"],
"hwdb": ["*hwdb*"],
"import": ["src/import/*", "src/shared/*import*", "units/*import*"],
"integritysetup": ["*integrity*"],
"journal": [
"src/journal/*",
"src/libsystemd/sd-journal/*",
"units/systemd-journal-catalog*",
"units/systemd-journal-flush*",
"units/systemd-journald*"
],
"journal-remote": [
"src/journal-remote/*",
"units/systemd-journal-gateway*",
"units/systemd-journal-remote*"
],
"kernel-install": ["*kernel-install*"],
"l10n 🌍": ["po/*"],
"locale": ["*locale*"],
"login": ["src/login/*", "*sd-login*/*"],
"machine": ["src/machine/*", "units/*machine*"],
"meson": ["meson_options.txt", "*meson.build"],
"mkosi": ["*mkosi*"],
"modules-load": ["*modules-load*", "modprobe.d/*"],
"mount": ["src/basic/*mount*", "src/core/*mount*", "src/mount/*", "src/shared/*mount*"],
"mountfsd": ["*mountfsd*"],
"network": [
"src/libsystemd-network/*",
"src/network/*",
"network/*",
"test/networkd-test.py",
"test/test-network*"
],
"notify": ["src/notify/*"],
"nspawn": ["*nspawn*"],
"nsresource": ["*nsresource*"],
"nss-myhostname": ["*nss-myhostname*"],
"portable": ["src/portable/*"],
"pstore": ["*pstore*"],
"random-seed": ["*random-seed*"],
"remount-fs": ["*remount-fs*"],
"repart": ["*repart*"],
"resolve": ["*resolve*"],
"rfkill": ["*rfkill*"],
"rpm": ["src/rpm/*"],
"run": ["src/run/*", "man/systemd-run*"],
"sd-boot/sd-stub/bootctl": [
"src/boot/*",
"src/bootctl/*",
"man/bootctl*",
"man/systemd-boot.xml"
],
"sd-bus": ["*sd-bus*/*"],
"sd-daemon": ["*sd-daemon*/*"],
"sd-device": ["*sd-device*/*"],
"sd-event": ["*sd-event*/*"],
"sd-hwdb": ["*sd-hwdb*/*"],
"sd-id128": ["*sd-id128*/*"],
"sd-netlink": ["*sd-netlink*/*"],
"sd-path": ["*sd-path*/*"],
"sd-resolve": ["*sd-resolve*/*"],
"selinux": ["*selinux*"],
"shell completion": ["shell-completion/*"],
"shell profile": ["profile.d/*"],
"shutdown": ["src/shutdown/*", "units/*shutdown*"],
"sleep": ["src/shared/*sleep*", "src/sleep/*"],
"smack": ["*smack*"],
"socket-proxy": ["*socket-proxy*"],
"ssh-generator": ["*ssh-generator*"],
"storagetm": ["*storagetm*"],
"sulogin": ["*sulogin*"],
"sysctl": ["*sysctl*"],
"sysext": ["*sysext*"],
"systemctl": ["*systemctl*"],
"sysupdate": ["*sysupdate*"],
"sysusers": ["*sysusers*"],
"tests": [
"src/shared/tests.*",
"src/test/*",
"src/fuzz/*",
"test/*",
"*test-*",
"*test_*",
".github/workflows/*"
],
"timedate": ["*timedate*"],
"timesync": ["*timesync*"],
"tmpfiles": ["*tmpfiles*"],
"tpm2": ["*tpm2*", "*tpm-*"],
"udev": ["src/udev/*", "src/libudev/*", "man/*udev*", "rules.d/*"],
"uki": ["*ukify*"],
"units": ["units/*"],
"user-session": ["*user-session*"],
"userdb": ["*userdb*"],
"util-lib": ["src/fundamental/*", "src/basic/*", "src/shared/*"],
"utmp/wtmp": ["*utmp*"],
"varlink": ["src/varlink/*", "src/libsystemd/sd-varlink/*", "src/systemd/sd-varlink*"],
"vconsole": ["*vconsole*"],
"veritysetup": ["*veritysetup*"],
"vmspawn": ["*vmspawn*"],
"volatile": ["*volatile*"],
"xdg-autostart": ["*xdg-autostart-generator*"]
}
}
-330
View File
@@ -1,330 +0,0 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# vi: sw=2 ts=2 et:
changed-files-labels-limit: 5
analyze:
- changed-files:
- any-glob-to-any-file: 'src/analyze/*'
apparmor:
- changed-files:
- any-glob-to-any-file: '**/*apparmor*'
ask-password:
- changed-files:
- any-glob-to-any-file: '**/*ask-password*'
backlight:
- changed-files:
- any-glob-to-any-file: '**/*backlight*'
battery-check 🪫:
- changed-files:
- any-glob-to-any-file: '**/*battery*'
binfmt:
- changed-files:
- any-glob-to-any-file: '**/*binfmt*'
bsod:
- changed-files:
- any-glob-to-any-file: '**/*bsod*'
btrfs:
- changed-files:
- any-glob-to-any-file: '**/*btrfs*'
build-system:
- changed-files:
- any-glob-to-any-file: ['meson_options.txt', '**/meson.build']
busctl:
- changed-files:
- any-glob-to-any-file: '**/*busctl*'
catalog:
- changed-files:
- any-glob-to-any-file: 'catalog/*'
cgls:
- changed-files:
- any-glob-to-any-file: '**/*cgls*'
cgroups:
- changed-files:
- any-glob-to-any-file: ['src/basic/cgroup*', 'src/cgroups-agent/*', 'src/core/cgroup*', 'src/shared/cgroup*']
cgtop:
- changed-files:
- any-glob-to-any-file: '**/*cgtop*'
ci:
- changed-files:
- any-glob-to-any-file: '.github/*'
coccinelle:
- changed-files:
- any-glob-to-any-file: 'coccinelle/*'
coredump:
- changed-files:
- any-glob-to-any-file: '**/*coredump*'
creds:
- changed-files:
- any-glob-to-any-file: ['src/core/*cred*', 'src/creds/*', 'src/shared/*cred*', 'units/*cred*']
cryptsetup:
- changed-files:
- any-glob-to-any-file: ['src/cryptenroll/*', 'src/cryptsetup/*', 'units/*crypt*']
debug-generator:
- changed-files:
- any-glob-to-any-file: '**/*debug-generator*'
delta:
- changed-files:
- any-glob-to-any-file: '**/*delta*'
detect-virt:
- changed-files:
- any-glob-to-any-file: '**/*virt*'
dissect:
- changed-files:
- any-glob-to-any-file: '**/*dissect*'
documentation:
- changed-files:
- any-glob-to-any-file: ['NEWS', 'README*', 'docs/*', 'man/*']
env-generator:
- changed-files:
- any-glob-to-any-file: '**/*environment*generator*'
escape:
- changed-files:
- any-glob-to-any-file: 'src/escape/*'
firstboot:
- changed-files:
- any-glob-to-any-file: '**/*firstboot*'
fsck:
- changed-files:
- any-glob-to-any-file: '**/*fsck*'
fstab-generator:
- changed-files:
- any-glob-to-any-file: '**/*fstab-generator*'
gpt-auto:
- changed-files:
- any-glob-to-any-file: '**/*gpt-auto*'
growfs:
- changed-files:
- any-glob-to-any-file: '**/*growfs*'
hibernate-resume:
- changed-files:
- any-glob-to-any-file: '**/*hibernate-resume*'
homed:
- changed-files:
- any-glob-to-any-file: ['src/home*', 'units/*home*']
hostname:
- changed-files:
- any-glob-to-any-file: ['src/hostname/*', 'src/shared/*hostname*', 'units/*hostname*']
hwdb:
- changed-files:
- any-glob-to-any-file: '**/*hwdb*'
import:
- changed-files:
- any-glob-to-any-file: ['src/import/*', 'src/shared/*import*', 'units/**/*import*']
integritysetup:
- changed-files:
- any-glob-to-any-file: '**/*integrity*'
journal:
- changed-files:
- any-glob-to-any-file: ['src/journal/*', 'src/libsystemd/sd-journal/*', 'units/systemd-journal-catalog*', 'units/systemd-journal-flush*', 'units/systemd-journald*']
journal-remote:
- changed-files:
- any-glob-to-any-file: ['src/journal-remote/*', 'units/systemd-journal-gateway*', 'units/systemd-journal-remote*']
kernel-install:
- changed-files:
- any-glob-to-any-file: '**/*kernel-install*'
l10n 🌍:
- changed-files:
- any-glob-to-any-file: 'po/*'
locale:
- changed-files:
- any-glob-to-any-file: '**/*locale*'
login:
- changed-files:
- any-glob-to-any-file: ['src/login/*', '**/sd-login*/**']
machine:
- changed-files:
- any-glob-to-any-file: ['src/machine/*', 'units/**/*machine*']
meson:
- changed-files:
- any-glob-to-any-file: ['meson_options.txt', '**/meson.build']
mkosi:
- changed-files:
- any-glob-to-any-file: '**/*mkosi*'
modules-load:
- changed-files:
- any-glob-to-any-file: ['**/*modules-load*', 'modprobe.d/*']
mount:
- changed-files:
- any-glob-to-any-file: ['src/basic/*mount*', 'src/core/*mount*', 'src/mount/*', 'src/shared/*mount*']
mountfsd:
- changed-files:
- any-glob-to-any-file: '**/*mountfsd*'
network:
- changed-files:
- any-glob-to-any-file: ['src/libsystemd-network/**/*', 'src/network/**/*', 'network/*', 'test/networkd-test.py', 'test/test-network*']
notify:
- changed-files:
- any-glob-to-any-file: 'src/notify/*'
nspawn:
- changed-files:
- any-glob-to-any-file: '**/*nspawn*'
nsresource:
- changed-files:
- any-glob-to-any-file: '**/*nsresource*'
nss-myhostname:
- changed-files:
- any-glob-to-any-file: '**/*nss-myhostname*'
portable:
- changed-files:
- any-glob-to-any-file: 'src/portable/**/*'
pstore:
- changed-files:
- any-glob-to-any-file: '**/*pstore*'
random-seed:
- changed-files:
- any-glob-to-any-file: '**/*random-seed*'
remount-fs:
- changed-files:
- any-glob-to-any-file: '**/*remount-fs*'
repart:
- changed-files:
- any-glob-to-any-file: '**/*repart*'
resolve:
- changed-files:
- any-glob-to-any-file: '**/*resolve*'
rfkill:
- changed-files:
- any-glob-to-any-file: '**/*rfkill*'
rpm:
- changed-files:
- any-glob-to-any-file: 'src/rpm/*'
run:
- changed-files:
- any-glob-to-any-file: ['src/run/*', 'man/systemd-run*']
sd-boot/sd-stub/bootctl:
- changed-files:
- any-glob-to-any-file: ['src/boot/**/*', 'src/bootctl/*', 'man/bootctl*', 'man/systemd-boot.xml']
sd-bus:
- changed-files:
- any-glob-to-any-file: '**/sd-bus*/**'
sd-daemon:
- changed-files:
- any-glob-to-any-file: '**/sd-daemon*/**'
sd-device:
- changed-files:
- any-glob-to-any-file: '**/sd-device*/**'
sd-event:
- changed-files:
- any-glob-to-any-file: '**/sd-event*/**'
sd-hwdb:
- changed-files:
- any-glob-to-any-file: '**/sd-hwdb*/**'
sd-id128:
- changed-files:
- any-glob-to-any-file: '**/sd-id128*/**'
sd-netlink:
- changed-files:
- any-glob-to-any-file: '**/sd-netlink*/**'
sd-path:
- changed-files:
- any-glob-to-any-file: '**/sd-path*/**'
sd-resolve:
- changed-files:
- any-glob-to-any-file: '**/sd-resolve*/**'
selinux:
- changed-files:
- any-glob-to-any-file: '**/*selinux*'
shell completion:
- changed-files:
- any-glob-to-any-file: 'shell-completion/**'
shell profile:
- changed-files:
- any-glob-to-any-file: 'profile.d/*'
shutdown:
- changed-files:
- any-glob-to-any-file: ['src/shutdown/*', 'units/**/*shutdown*']
sleep:
- changed-files:
- any-glob-to-any-file: ['src/shared/*sleep*', 'src/sleep/*']
smack:
- changed-files:
- any-glob-to-any-file: '**/*smack*'
socket-proxy:
- changed-files:
- any-glob-to-any-file: '**/*socket-proxy*'
ssh-generator:
- changed-files:
- any-glob-to-any-file: '**/*ssh-generator*'
storagetm:
- changed-files:
- any-glob-to-any-file: '**/*storagetm*'
sulogin:
- changed-files:
- any-glob-to-any-file: '**/*sulogin*'
sysctl:
- changed-files:
- any-glob-to-any-file: '**/*sysctl*'
sysext:
- changed-files:
- any-glob-to-any-file: '**/*sysext*'
systemctl:
- changed-files:
- any-glob-to-any-file: '**/*systemctl*'
sysupdate:
- changed-files:
- any-glob-to-any-file: '**/*sysupdate*'
sysusers:
- changed-files:
- any-glob-to-any-file: '**/*sysusers*'
tests:
- changed-files:
- any-glob-to-any-file: [
'src/shared/tests.*',
'src/test/**/*',
'src/fuzz/**/*',
'test/**/*',
'**/test-*',
'**/test_*',
'.github/workflows/*'
]
timedate:
- changed-files:
- any-glob-to-any-file: '**/*timedate*'
timesync:
- changed-files:
- any-glob-to-any-file: '**/*timesync*'
tmpfiles:
- changed-files:
- any-glob-to-any-file: '**/*tmpfiles*'
tpm2:
- changed-files:
- any-glob-to-any-file: ['**/*tpm2*', '**/*tpm-*']
udev:
- changed-files:
- any-glob-to-any-file: ['src/udev/**/*', 'src/libudev/*', 'man/*udev*', 'rules.d/*']
uki:
- changed-files:
- any-glob-to-any-file: '**/ukify*'
units:
- changed-files:
- any-glob-to-any-file: 'units/**/*'
user-session:
- changed-files:
- any-glob-to-any-file: '**/*user-session*'
userdb:
- changed-files:
- any-glob-to-any-file: '**/*userdb*'
util-lib:
- changed-files:
- any-glob-to-any-file: ['src/fundamental/**/*', 'src/basic/**/*', 'src/shared/**/*']
utmp/wtmp:
- changed-files:
- any-glob-to-any-file: '**/*utmp*'
varlink:
- changed-files:
- any-glob-to-any-file: ['src/varlink/*', 'src/libsystemd/sd-varlink/*', 'src/systemd/sd-varlink*']
vconsole:
- changed-files:
- any-glob-to-any-file: '**/*vconsole*'
veritysetup:
- changed-files:
- any-glob-to-any-file: '**/*veritysetup*'
vmspawn:
- changed-files:
- any-glob-to-any-file: '**/*vmspawn*'
volatile:
- changed-files:
- any-glob-to-any-file: '**/*volatile*'
xdg-autostart:
- changed-files:
- any-glob-to-any-file: '**/**xdg-autostart-generator*'
+104 -10
View File
@@ -7,13 +7,10 @@ name: "Pull Request Labeler"
on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review, closed]
paths-ignore:
- '.github/labeler.yml'
- '.github/workflows/labeler.yml'
# Allow testing changes made to the labeler configuration
pull_request:
paths:
- '.github/labeler.yml'
- '.github/labeler.json'
- '.github/workflows/labeler.yml'
issue_comment:
types: [created]
@@ -28,6 +25,13 @@ jobs:
permissions:
pull-requests: write
env:
# The pull_request trigger is only there to check a change to the
# configuration against the head copy, so it never writes: a fork gets a
# read-only token whatever the permissions above say, and pull_request_target
# runs for the same pull request, so the two would fight over the labels.
SELF_TEST: ${{ github.event_name == 'pull_request' }}
steps:
- name: Repository checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
@@ -35,17 +39,107 @@ jobs:
with:
persist-credentials: false
- name: Label PR based on policy in labeler.yml
uses: actions/labeler@c5dadc2a45784a4b6adfcd20fea3465da3a5f904
- name: Label PR based on policy in labeler.json
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
if: startsWith(github.event_name, 'pull_request') && github.base_ref == 'main' && github.event.action != 'closed'
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
configuration-path: .github/labeler.yml
sync-labels: false
script: |
const fs = require("fs");
const CONFIG = ".github/labeler.json";
/* A pattern is a literal path in which "*" stands for any run of
* characters, "/" included. That makes "src/home*" cover everything
* below src/home/ and "*meson.build" every meson.build in the tree,
* which is what the patterns were always meant to say. */
const compile = (glob) =>
new RegExp(`^${glob.split("*").map(p => p.replace(/[.^$+|?()[\]{}\\]/g, "\\$&")).join(".*")}$`);
/* The pull_request trigger checks the branch out so that changes to
* the configuration can be tested. pull_request_target does not, so
* read the base repository's copy through the API instead. */
let raw;
if (context.eventName == "pull_request") {
raw = fs.readFileSync(CONFIG, "utf8");
} else {
const content = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: CONFIG,
});
raw = Buffer.from(content.data.content, "base64").toString();
}
const config = JSON.parse(raw);
const changed = (await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
})).map(f => f.filename);
const matched = Object.entries(config.labels)
.filter(([, globs]) => globs.map(compile).some(re => changed.some(f => re.test(f))))
.map(([label]) => label);
const response = await github.rest.issues.listLabelsOnIssue({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
const present = new Set(response.data.map(l => l.name));
/* A pull request touching half the tree matches so many labels that
* none of them say anything about it, so say that much instead. The
* count is of everything that matched rather than of what is missing,
* so that a rerun on the same pull request decides the same way. */
const tooMany = matched.length > config["max-labels"];
const wanted = tooMany ? [config["too-many-labels"]] : matched;
/* The two outcomes exclude each other, so a push that crosses the limit
* in either direction has to take the other one's labels back off. */
const labels = wanted.filter(l => !present.has(l));
const stale = (tooMany ? matched : [config["too-many-labels"]])
.filter(l => present.has(l));
if (labels.length == 0 && stale.length == 0) {
core.info("Labels are already up to date");
} else if (process.env.SELF_TEST == "true") {
core.info(`Would add ${labels.join(", ") || "nothing"} and remove ` +
`${stale.join(", ") || "nothing"}`);
} else {
if (labels.length > 0) {
/* addLabels() only ever adds, so labels set by another workflow
* between the calls above and this one survive. */
await github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: labels,
});
}
for (const label of stale) {
try {
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: label,
});
} catch (error) {
/* Somebody else removed it first. */
if (error.status !== 404) {
throw error;
}
}
}
}
- name: Set or remove labels based on systemd development workflow
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
if: startsWith(github.event_name, 'pull_request') && github.event.action != 'closed' && !github.event.pull_request.draft
if: startsWith(github.event_name, 'pull_request') && github.event.action != 'closed' && !github.event.pull_request.draft && env.SELF_TEST != 'true'
with:
script: |
/* Never read the full label set and write it back: other workflows