poi-tracker
Package-of-interest tracker for Fedora, EPEL, and CentOS SIGs.
Manages TOML-based inventories of packages that an organization tracks across distributions. Supports exporting to content-resolver YAML and hs-relmon manifest formats.
poi-tracker is the act side of the inventory tooling: it curates the inventory itself (sync, prune, add/remove) and takes credentialed actions on Bugzilla (triaging, closing, and claiming bugs). Its read-only counterpart is sandogasa-pkg-health, which observes the same inventories — periodic, credential-free health checks with persisted, incrementally-refreshed reports. Rule of thumb: anything that writes (to Bugzilla or the inventory) belongs here; anything that produces a report to watch over time belongs in pkg-health.
Installation
Some subcommands shell out to external tools: prune-retired and
sync-distgit --mark-unshipped need
fedrq
(sudo dnf install fedrq); sync-gitlab --mark-unshipped needs
koji configured with the cbs profile (from the
centos-packager package); semver-audit and triage-updates
use koji (sudo dnf install koji) to verify that a version is
actually tagged into a release before calling a bug stale — both
degrade with a warning when it's missing (unverifiable bugs are
left open / reported as up to date).
Usage
Naming inventories: -i, -I, or a workspace
Three ways, from most to least specific. -i FILE (repeatable) names
exactly these inventories and always wins. -I DIR takes every
*.toml in a directory as an inventory, roles unknown — right for the
commands that treat inventories alike (show, find, validate,
export), wrong for the ones where it matters which file is the keeps
and which the packages you own. A workspace file says which is which,
once, so those subcommands need no -i at all; anything typed on the
command line still overrides it.
The workspace file
The maintenance subcommands — kondo, keep, unkeep, deps,
derive, dependents, act, announce — each take a handful of
paths that never change between runs: which inventory is the packages
you own, which ones are the keeps, where the graph is, what branch the
walk runs against. Write them once in a kondo.toml next to the
inventories, and the subcommands read them from there (-w PATH names
another file; ./kondo.toml is found on its own):
= "salimma" # routes kondo and act
= "inventory-salimma-direct.toml" # sync-distgit --no-groups
= "cull.toml" # the standing verdicts
= ["inventory-salimma-essential-retired.toml"]
[[]] # one dependency world
= "fedora"
= ["inventory-salimma-essential.toml", "inventory-salimma-essential-rust.toml"]
= "fedora-build-deps.toml" # deps -o
= "inventory-salimma-essential-deps.toml" # derive -o
= "fedora-build-deps-graph.json"
= "rawhide"
= ["rawhide"]
[[]]
= "hyperscale-el9"
= true # not ours on dist-git
= ["inventory-hyperscale.toml"]
= "inventory-hyperscale-el9-deps.toml" # the walk output is the essential list
= "hyperscale-el9-graph.json" # written by the first deps run
= "hs.el9"
= "stack"
A closure is one set of keeps walked against one repo configuration.
external = true marks keeps that are not yours on dist-git — another
SIG's inventory, another distribution's — whose dependencies you track
all the same: the walk's output counts as essential, and nothing will
offer to cull or demote the keeps themselves. The essential set
kondo uses is every closure's keeps, walk outputs and derived
inventories plus the retired ones (a walk output names packages that
are not yours too, which is harmless: only owned packages can be
candidates). Walk subcommands take the first closure unless
--closure NAME says otherwise; a closure whose graph does not exist
yet is one deps run away from having it. Paths are relative to the
file.
With the file in place, poi-tracker kondo, poi-tracker keep ripgrep
or poi-tracker --closure hyperscale-el9 dependents are complete
commands, and -C DIR runs any of them as if started in the data
directory, the way git -C does. The commands about the packages you
maintain — semver-audit, triage-updates, triage-retired,
prune-retired, show, validate — take the owned inventory as their
-i from it too, so poi-tracker -C ../data semver-audit --non-breaking
is the whole safe-updates question. A flag on the command line still wins, and --no-defaults
ignores the file (and the config file's [defaults]) for one run.
Act on the cull verdicts
The workflow's last verb — walk the standing verdicts and give the packages away, one confirmed action at a time:
Each package is classified fresh (cache-first) and prompted by level:
owner-level offers (y) orphan / (g <user>) give / (u)ncull / (s)kip / (q)uit, admin-level offers removing your own ACL, and ask-level
entries prompt too (uncull is the one action always open). Enter
skips — the safe default for server-side ownership changes — and
there is deliberately no bulk mode. u lifts a verdict you no longer
stand behind: the entry leaves the cull inventory and the package
returns to candidacy on the next kondo run — unless you type
u <inventory>, which also files it into that essential inventory so
it never comes back. Filing this way adds a keep the graph has never
walked, so the prompt says to keep it afterwards to record its
dependencies.
Before any orphan prompt, the package's reverse dependencies are
probed distro-wide through fedrq, across all subpackages, so
feature capabilities (crate(x/feature)) are covered: orphaning
starts the retirement clock, and retirement is what leaves dependents
uninstallable. A package something still requires says so at the
prompt, where skip is already the default — and a hard dependency
makes the orphan reconfirm. Each dependent is resolved to its source
and classified: one that reaches the package only through an optional
foo+feature subpackage is shown as (optional feature) and, if
every dependent is such, flagged likely severable rather than a block
(python-dulwich needs merge3 only via its +merge feature, breezy
outright). The probe aims itself: by
default (--branch auto) each package is probed on the branches it
actually carries per dist-git — rawhide plus its own EPEL branches —
because which branches matter belongs to the package, not to a flag
(python39-* lives on epel8, invisible from rawhide). An explicit
--branch list overrides.
On each success the entry leaves the cull inventory and the package
leaves the --personal inventory, so an interrupted walk loses
nothing already enacted. Requires a dist-git API token (--api-token,
PAGURE_API_TOKEN, or poi-tracker config) and fedrq.
Add a package
Adopt orphaned packages
The action counterpart to
sandogasa-pkg-health's orphaned flag:
walk the inventory, find packages whose dist-git owner is the
orphan sentinel user, show each one's orphaning reason, and take
ownership via the same API as the web UI's "Take" button. An
orphaned package is retired ~6 weeks after orphaning unless
someone adopts it.
Adoption is a per-package commitment, so interactive runs confirm
each package individually (default no) rather than batching one
yes/no over the list; -y adopts every match. --pattern <glob>
scopes the walk. Packages marked retired or unshipped in the
inventory are skipped — dist-git refuses to hand out retired
packages (those need a releng ticket).
Adopting needs a dist-git API token with the
"Modify an existing project" ACL (you must also be in the
packager group): generate one at
https://src.fedoraproject.org/settings/token/new and store it
with poi-tracker config (or pass --api-token / set
PAGURE_API_TOKEN). --dry-run works without a token.
Announce the standing cull verdicts
Render the whole cull inventory as the grouped, announcement-ready report — the artifact the act phase posts to the mailing list before anything runs:
The text report is for humans — the mailing list gets package names
grouped by level, never commands. Enacting the verdicts is act's
job, one confirmed prompt at a time. Levels are re-classified per run (day-fresh cache first; --refresh-acls forces
fresh lookups) rather than read from the cull file, whose reason
text is free-form. kondo's own report covers only the packages
culled in that run, and re-runs skip the already-decided — this is how
the accumulated verdicts of a whole triage get rendered. --json
prints the report machine-readably.
Configure (Bugzilla API key)
Prompts for a Bugzilla API key, validates it with a quick test
search, and saves it to ~/.config/poi-tracker/config.toml.
Lookup order at runtime: --api-key flag → BUGZILLA_API_KEY
env var → config file.
Generate an API key at https://bugzilla.redhat.com/userprefs.cgi?tab=apikey.
Classify keeps by their dependents
The pruning report for shrinking a keep set over time — over the graph
a deps --graph run saved, classify every package of the given
inventories by who needs it:
Reach for it when you want the whole picture — every keep classified —
rather than the changes reconcile reports; the [devel-only] carried
entries are the ones reconcile offers to demote.
Leaves have no dependent in the closure: one shipping real
binaries is presumably kept for its own sake, while a [devel-only]
leaf (every binary ends in -devel — the library shape) is kept for
nobody. Carried packages are needed by other packages of the same
inventories — if such an entry is only tracked as a dependency, stop
curating it: track the dependent and let the dependency walk carry it
in the derived inventory. The edge says whom the graph could carry
a package for, never why the entry exists, so carried lines wear the
same [devel-only] marker: a devel-only carried crate is almost
always dependency-only and safe to bulk-prune, while one shipping
real binaries (a tool whose satellite plugin is the dependent, say)
is likely kept on purpose — decide those per package. Externally needed ones are required only
by closure packages outside the inventories. Packages the graph never
saw are reported as unknown rather than guessed at (a stale graph — or
a stale keep: a package renamed upstream shows up as a leaf shipping
nothing). --json prints the report machine-readably.
Inventory dependencies from other repos
Walk the transitive dependency graph of the inventory's packages against a fedrq branch and repo stack, and collect what they pull from repos of interest — e.g. which EPEL 9 packages a Hyperscale inventory depends on:
$ poi-tracker -C ../data --closure hyperscale-el9 deps # with a workspace file
$ poi-tracker -i hyperscale-packages.toml deps -b hs.el9 -r stack \
-o hyperscale-epel9-deps.toml # without one
1 runtime dependency from [epel] for 2 package(s) on hs.el9:
python-zstandard (epel) — systemd-ukify requires python3dist(zstandard)
wrote hyperscale-epel9-deps.toml
A capability here is RPM's own term for the strings in Provides:
and Requires: — a package name, a versioned expression like
python3dist(zstandard), a soname like libhwloc.so.15()(64bit), or
a file path (rpm -q --provides lists a package's). The walk
repeatedly resolves the capabilities the current packages require
into the packages that provide them, the way dnf would.
Providers whose repo id matches --base-repo (a prefix, default
fedrq-centos-stream-) satisfy a dependency and end the walk there —
the base distro is a given. Every other provider is walked further,
and collected when its repo id is in --from (exact, default
epel). Those defaults describe an EPEL-on-CentOS-Stream stack; for
a Fedora walk there is no base distro beneath the branch, so the
default prefix never matches and the only setting needed is the
branch's own repo id, e.g. -b rawhide --from rawhide. The
output inventory records why each package was pulled in, in its
reason field. The walk also seeds the roots' own BuildRequires
(attributed src:<name>), so a kept application justifies its crate
graph — those edges exist only at build time. Build dependencies
beyond the roots are not walked: the closure keeps the roots working
and the roots rebuildable, not the world. --runtime-only skips the
BuildRequires seeding when the question is the deployment surface
rather than what must stay for the roots to survive.
--fixpoint <inventory> iterates to closure in one run: collected
packages found in that inventory (yours) become roots of their own,
their BuildRequires seeding further rounds until a round adds nothing
— because keeping a package means keeping it rebuildable, and its
test-only BuildRequires appear in no -devel's runtime Requires.
Rounds reuse the walk's seen-sets, so each costs only the genuinely
new capabilities; the manual three-invocation equivalent measured ~90
minutes against rawhide where the in-process fixpoint is the first
walk plus small change. Conflicts with --runtime-only.
Whenever -o writes an inventory, the walk's full dependency graph
lands beside it as <output>-graph.json (--graph moves it) — every
requirer of every capability and every provider, where the report
keeps only first attributions. That is the input unkeep and
dependents answer from in milliseconds instead of a fresh walk, and
the periodic full walk is its refresh; a closure is never written
without it.
The shared walk filters apply: --pattern (glob, CSV or repeated),
--start-from and --end-with restrict which inventory packages are
used as roots — handy for a quick look at one package's pull before
walking a whole inventory.
Requires fedrq, with the branch configured — hs.el9 and hs.el10
come from this repository's configs/fedrq/. Rich (boolean)
dependencies made only of conjunctions (with, and — the shape of
rust-packaging's crate version ranges) resolve by their leaves;
conditional ones (or, if, unless) are skipped with a warning.
File dependencies resolve
and classify normally, but appear under unmatched in --json
output, since tying a path to its provider would need file lists.
Recompute a derived inventory offline (derive)
The derived dependency inventory is a view the graph can rebuild: owned packages the keeps reach, minus the keeps themselves —
reconcile runs this for every closure; on its own it is the quick
"is the derived inventory current?" check, report-only until --apply.
The report is the diff against the file's current content (+/-
lines); --apply replaces its packages wholesale, reason chains
taken from witness edges in the graph. Because the recompute is
idempotent, keep-set edits never need a fresh walk to update the
derived inventory — but the view is only as true as the last full
walk, which remains the periodic calibration against distro drift.
--json prints the report machine-readably.
Export to content-resolver YAML
# Export all workloads (one YAML per workload)
# Export a single workload
# Custom output path (single workload only)
Export to hs-relmon manifest
# Merge multiple inventories into one manifest
# Filter by workload
Existing manifest entries and their per-package overrides are
preserved across re-exports. An archived_builds package gets
archived = true written so hs-relmon can prune its stale CBS
builds; an unshipped package (gone, no builds) is dropped from
the manifest entirely — hs-relmon has nothing to track for it
(the inventory keeps the tombstone). Both are reconciled every
export, so a revived package returns to normal tracking.
Find a package
Import from legacy JSON
Intersect inventories
Keep only the main inventory's packages that also appear in the
--with inventories, optionally merging them into another file. The
motivating case: a deps closure lists everything a keep-set
needs — thousands of packages, most of them other people's — and the
durable fact is its intersection with the packages you maintain.
Entries come from the main (-i) side, so deps' reason chains
survive into the merge target, and the essentials file explains
itself:
-o accumulates like kondo's: existing entries win, new ones are
added sorted.
Start keeping packages (keep)
The add half of incremental maintenance — the mirror of unkeep.
Walk the new keeps' dependencies against the saved graph, so fedrq is
only consulted for capabilities the graph has never seen:
This is the one manual step reconcile cannot replace: reconcile
walks keeps that are new to the graph, so a keep the graph has
already seen — rust-totp-rs before and after its rawhide build grew a
feature — is re-walked with keep, which records the new edges without
re-filing anything.
Known capabilities resolve offline from the graph (their providers'
Requires come from the recorded edges, so the walk cascades through
the known world for free); only the frontier goes to fedrq, and the
summary says how many went each way. The new edges merge into the
graph file, the names are filed into --into (default: the first
-i; a name already kept in any -i inventory is walked but not
re-filed, since the keep set is their union), --deps recomputes
the derived inventory in the same run, and -o brings the closure
inventory (the deps -o file) up to date with the graph the same way —
one command instead of an hour-long walk plus three edits. The new
roots themselves always expand live, every feature subpackage
included: a package the graph met as a mere dependency carries only
the features something requested. Takes the same -b/-r/--from/
--base-repo/--runtime-only as deps. --json prints the report
machine-readably.
Triage cull candidates (kondo)
The set difference between the inventory ("packages I maintain") and
the union of essential inventories — work inventories, and the
dependency inventories deps writes — is the list of packages nothing
justifies. kondo walks that list with the shared review prompt, worded for
what happens here: the finding is "nothing essential needs this
package", so cull (c, or Enter) confirms it as a cull candidate,
essential (e path.toml) files the package into the inventory that
makes it so (the file is created if needed), and skip (s) leaves
it undecided for this run. Filing is immediate, so an interrupted triage loses
nothing already decided.
Confirmed candidates are then classified by your own (direct) dist-git
access, because the level routes the action — owner: orphanable
directly; admin: you can remove your own ACL; commit, collaborator or
ticket: you have to ask. The report groups them accordingly and prints
ready-to-run sandogasa-pkg-acl command lines for the first two
groups, in a form fit for a mailing-list announcement — which comes
first: kondo itself never touches dist-git ACLs. Group-granted
access is deliberately ignored (pair with sync-distgit --no-groups);
a group grant is not yours to walk away from.
Access levels are looked up before the prompt (answers are cached for
a day under ~/.cache/poi-tracker/, since ownership changes on human
timescales — --refresh-acls forces a fresh look), so each candidate
line carries its context — old-toy (commit) — nothing essential needs it
reads differently from (owner).
e path.toml files the package in one line; a bare e asks for the
inventory next. --explain-into PATH sets a default: Enter at that prompt
files the package there, so a pass that sorts many packages into one
inventory is two keystrokes each (e, Enter); an explicit path still
wins.
The cull file records why each package is condemned. The stock
reason is kondo cull candidate (<level>); --reason TEXT replaces
the wording for packages culled this run (the sitting usually has one
shared story — "retiring my GNOME extensions"), and c <note> at the
prompt records this one package's own words instead. The access level
is always appended, entries already in the file keep their reason, and
only names are ever read back — so hand-editing a reason later is
safe. The natural multi-pass flow is one pass per destination, passing
the same file as --essential too — filed packages then never
reappear as candidates. Candidates are computed once, up front, so
filing into an essential inventory mid-run is safe; and when the
--explain-into file does not exist yet, it is tolerated as an empty
essential input rather than failing the load. Any other missing
essential path is still an error — treating a typo as an empty
inventory would make every package it names look cullable.
The shared walk filters (--pattern, --start-from, --end-with)
restrict the candidates, which is how a large triage gets eaten in
sittings: one themed pass at a time (--pattern 'rust-*' --explain-into keep-rust.toml), with --start-from as the resume
point — candidates are sorted, and access lookups run a few at a time
so even hundreds classify in under a minute. -o merges rather than
overwrites, so every pass adds its slice of the verdict to one cull
inventory; packages already present are left untouched — and consulted:
a candidate a previous pass already culled is skipped, not re-asked, so
re-running with the same -o only ever prompts for what is genuinely
undecided. That makes the file itself the undo mechanism: delete an
entry (a mistaken keep, say) and the next run asks about that package
again. The reverse correction is automatic: a culled package that has
since become essential — after a deps run justified it, say
— is rescued from the file and reported, so the verdict never
contradicts the inputs. skip decisions are deliberately not persisted — a skipped
candidate returns on the next run until whatever the analysis missed is
fixed in the essential inputs. Sessions
running at the same time should still write distinct files — the merge
is load-modify-save, so simultaneous finishes can drop each other's
additions — and let a later pass fold them together.
Without a terminal, with --json, or with -y, no prompt fires and
every candidate stays a candidate — which acts on nothing.
Mark packages no longer shipped anywhere
prune-retired finds inventory packages that are no longer
carried on any active branch — the dist-git project is gone
(404), it has no branch on an active release, or it carries a
dead.package marker on every active branch it has. The active
branch set is queried from Bodhi's active releases (plus
rawhide) or overridden with --branch:
By default matches are marked with an unshipped reason in
the inventory rather than deleted: retired packages keep their
ACLs, so a deleted entry would come straight back on the next
sync-distgit run, and the marker is what lets the rest of the
tooling do the right thing. triage-updates and semver-audit
skip unshipped packages; triage-retired still processes them
so their remaining bugs get closed; the sync commands' --prune
preserves them. Markers are refreshed in both directions — a
revived package gets its marker cleared. Pass --remove to
delete the entries outright instead. Packages are checked
concurrently (-j/--jobs, default 8 in-flight dist-git
requests).
An entry with no rpms/ dist-git project (404) is reported as
invalid rather than marked: the entry itself is wrong — a
non-RPM repo (module, container image, tests) imported under its
bare name by an older group sync (e.g. modules/askalono-cli
showing up as askalono-cli), a binary subpackage name
recorded instead of the source package, or a typo. The fix is
editing or removing the entry, which is a human call. A stale
unshipped marker on such an entry is cleared by the next run.
sync-distgit --mark-unshipped runs the same check on the
packages a sync adds, so a fresh inventory starts with its
unshipped markers in place instead of needing a follow-up
prune-retired run. This catches retired packages, which keep
their ACLs and so still appear in sync listings. A package whose
dist-git project was deleted outright never appears in a listing
at all — harmless for a fresh inventory (it simply isn't added),
but if an existing inventory recorded it before the project
vanished, only prune-retired notices the 404.
Reconcile the workspace (reconcile)
The maintenance loop as one command. reconcile reads the workspace
file and, for every closure in it, brings the inventories up to date
with the keeps:
$ poi-tracker reconcile
reconciling workspace kondo.toml
closure fedora:
new keeps walked (2): ripgrep, fd-find
41 capabilities answered from the graph, 3 resolved live
entered the derived inventory (3): rust-buf-min, rust-tree_magic_db, rust-ufmt-write
closure hyperscale-el9 (external):
skipped — hyperscale-el9-deps-graph.json does not exist yet: run `poi-tracker --closure hyperscale-el9 deps` once
12 owned package(s) no essential inventory justifies — triaging with kondo
Per closure: keeps the graph has never walked — not a root, or a root
with no recorded requirements, as a keep filed with u at a prompt is —
are walked (graph first, fedrq only for what it lacks) and merged into
the graph;
the closure inventory and the derived inventory are both recomputed
from the graph — the closure one as what a walk of the keeps collects
(the closure's --from repos, ending at its base-distro prefixes),
less the keeps and, where a derived inventory holds them, the owned
packages — so a package the graph reaches enters whichever file it
belongs in whether or not this run's walk touched it, and a keep or
owned package that strayed into the closure file leaves it. An entry
the graph does not reach at all is kept and listed as such: the graph
is what is incomplete there (a walk records no edge for a provider it
could not attribute), and the next full deps walk settles it. Then
the decisions a program cannot make are asked, one line each, written
as answered so an interrupted run resumes where it stopped:
- a package that newly rides in the derived inventory: Enter lets it
ride,
emakes it essential in its own right (filed into the closure's first keeps inventory, or--into PATH) and it is walked as a keep in the next round; - a keep only devel-only edges carry — the library-only shape your
first prune demoted by hand: Enter keeps it essential,
ddemotes it to the derived inventory.
a takes the default for the rest, q stops asking. External
closures are walked and recomputed but never asked about: their keeps
are not yours to promote or demote. Once every closure has had its
say, the owned packages no essential inventory justifies are looked at
once more before the triage: one that a closure package you do not
own needs — python-zope-testrunner, the test runner of the
python-lazr-config that mailman3 pulls in — can be kept by making
that package essential instead: e files it as a keep in the closure's
first keeps inventory (or --into), e <inventory> in that one — the
same letter and word as at the triage, where e makes the candidate
itself essential. It is walked with its build dependencies, and the
candidate becomes a derived entry instead of a cull. When several
packages need the candidate the first is offered; poi-tracker keep <other> takes a different one. What remains goes through
kondo's cull/essential/skip triage into the cull file, exactly as
poi-tracker kondo would.
--yes takes every default without asking (and leaves the triage
candidates as cull candidates, as kondo's -y does); --dry-run
computes and reports without writing, walking or asking; --json
prints the per-closure report and the triage candidates instead of
triaging. The manual subcommands remain for surgical use — keep for
one package, unkeep to see what a removal frees before doing it,
dependents for the full leaves/carried picture — and deps is the
periodic full walk that refreshes a graph when Fedora has moved.
Remove a package
Audit pending updates by semver impact
semver-audit looks at each maintained package's pending upstream
release notification (the open upstream-release-monitoring@
"X is available" bug) and classifies the version bump against the
version currently packaged in rawhide dist-git, so you can see
which updates are safe to push and which need care:
# All pending updates, grouped by impact
# Just the safe ones for your Rust packages
# Machine-readable
Bumps are classified with Cargo's compatibility rule (the Rust
convention): a change at or before the version's leftmost non-zero
component is breaking. So 1.4 → 1.5 is non-breaking, but
0.4 → 0.5 is breaking (pre-1.0 minor bumps can break), and
0.0.3 → 0.0.4 is breaking too. Versions that aren't plain dotted
integers — pre-releases, dates, git snapshots — are reported as
needs review rather than guessed at. A package whose packaged
version already equals the "available" version is reported as
up to date (stale bug) — but only after verifying (via
koji) that a build with that version is actually in rawhide's
tag chain. A version merely committed to dist-git whose build
sits in a side tag or is still gating is reported as
committed, awaiting release instead: the bug isn't stale,
the update just hasn't shipped. A package that's retired on rawhide (a
dead.package marker — the same signal triage-retired uses) is
reported as retired (update request invalid), since there's no
live package to update; run triage-retired to close those bugs.
--pattern <glob> (comma-separated or repeated, e.g. rust-*)
limits the audit to matching packages, and --non-breaking shows
only the safe updates. The audit makes a Bugzilla search and a
dist-git spec fetch per matching package, so scope it with
--pattern for a large inventory — or use --batch [EMAIL],
which replaces the per-package searches with one Bugzilla
query for all open release-monitoring bugs assigned to or CC'ing
EMAIL (default: the email set via poi-tracker config), matched
against the inventory locally. Batch mode misses bugs where that
email is neither assignee nor CC'd, so it fits inventories of
packages you (co-)maintain or watch.
Show inventory
Sync from dist-git
Create or update an inventory from packages a user or group has access to on Fedora dist-git (Pagure). Re-running merges new packages without overwriting existing entries or annotations.
# All packages for a user
# All packages for a group
# Exclude packages with only group-based access
# Only packages from specific groups
# Exclude specific groups
# Add workload tags to all imported packages
# Remove packages no longer in dist-git results
# Resume an interrupted sync from f*, stop before m*
User syncs query Pagure one name prefix at a time (a*–z*,
0*–9*) by default: an unfiltered per-user project query is too
expensive for Pagure to answer within its gateway timeout, so it
returns HTTP 504. Splitting the query by name prefix keeps each
request small enough to succeed. (See
sandogasa-distgit's development notes
for the details — including why group syncs need no such
workaround.) --pattern switches to a single patterned query
instead, and --no-auto-prefix forces a single unfiltered query. --start-pattern / --end-pattern bound the prefix scan
(e.g. to resume an interrupted sync: start at this prefix / stop
before this prefix) and imply prefix mode, as does
--auto-prefix — which is how a group sync opts into scanning.
If both --auto-prefix and --no-auto-prefix are given, the
last one wins.
Packages where the user has both direct and group-based access are always included, regardless of group filters.
Without --prune, packages in the inventory that are no longer
in the dist-git results are listed as a warning but kept.
Transient network failures are retried with backoff (both 5xx
responses and connection errors). If a fetch still fails, the
progress so far is saved to <output>.partial along with the
failed pattern in <output>.partial.state — re-running the same
command resumes from the failed pattern, and a completed run
replaces <output> and removes both files. Delete the
.partial to start over instead.
For user syncs, --fast replaces the whole prefix scan with one
request against Pagure's owner-alias dump. The trade-off: the
dump only records direct owner/admin/commit maintainers, so
collaborator- and ticket-level grants won't appear (and
--prune --fast would remove them from an inventory the full
scan had populated). It implies --no-groups; --pattern and
--exclude still apply, client-side.
A fast layout that covers nearly everything: keep one --fast
inventory for your own packages plus one group inventory per SIG
you're in (each a single cheap query) —
Together those cover everything except user-level
collaborator/ticket grants, which only the full prefix scan can
see — run one occasionally to true up. The full trade-off
analysis lives in
sandogasa-distgit's development notes.
Sync from GitLab
Create or update an inventory from a CentOS SIG's GitLab RPM group. Presets cover the common groups:
--mark-unshipped cross-checks each project against CBS (CentOS
koji) and records archival state. A project's GitLab repo being
archived means upstream maintenance stopped; what happens
next depends on whether CBS still carries the package:
- archived, no released CBS build → marked
unshipped(a tombstone, skipped by triage/audit like a retired Fedora package). - archived, but release builds remain → marked
archived_builds: it still ships, so it is not skipped, but its lingering builds are a cleanup candidate — the command prints a reminder to runhs-relmonto prune them.
"Released" follows each SIG's lifecycle: Hyperscale ships for
both RHEL N and CentOS Stream Ns, so a release build in
either hyperscaleN-*-release or hyperscaleNs-*-release
counts; Proposed Updates is Stream-only. --centos-release sets
which major releases count (default 9,10). Requires koji with
the cbs profile. Markers are refreshed in both directions on
each run.
Close retired packages' update bugs
When a package gets retired on a dist-git branch (a
dead.package file is committed), any open release-monitoring
bug for that branch is dead weight — there's no live spec to
update. triage-retired walks the inventory, checks dist-git
for retirement, and closes those bugs as CLOSED/CANTFIX:
The --branch flag controls which dist-git branch(es) are
checked (default rawhide); each branch scopes its own Bugzilla
search, so an epel9 retirement closes the
Fedora EPEL/epel9 bug. Pass it more than once (or as a
comma-separated list) to check several branches in one run — a
package retired on some branches but live on others only has
its bugs closed for the branches where it's actually dead:
Note that retirement (a dead.package marker) is distinct from
"never existed": a bug filed against a branch the package was
never built for is not a retirement and is left untouched —
triage-retired only closes bugs on branches where a
dead.package is present.
By default only release-monitoring bugs (filed by the Anitya /
the-new-hotness bot) are closed — those are mechanical and safe
to bulk-close. Pass --all-reporters to instead close every
open bug on the retired branch, including human-filed ones (CVEs,
FTBFS, etc.). Use it deliberately: across a full inventory run it
closes a lot, and a CVE filed only against the retired branch
(with no live-branch counterpart) would be closed as CANTFIX too:
Bugs that are already CLOSED are skipped. Each closure adds a
short comment naming the package and the retired branch.
Interactive runs offer to claim ownership of each closed bug
(set assigned_to to your configured Bugzilla email). Pass
--claim to claim without prompting — under -y this is the
only way to opt in. The email is set via poi-tracker config.
Pass --mark (needs a single -i file; conflicts with
--dry-run) to record the run's findings in each package's
retired_on field — in both directions, so a branch found live
again is removed. semver-audit and triage-updates skip
packages marked retired on rawhide, saving their per-package
queries; re-running triage-retired --mark is how the markers
are refreshed.
Useful flags for big inventories (shared by all
inventory-walking commands — semver-audit, triage-retired,
and triage-updates — and freely combinable):
--pattern <glob>— only process matching packages (comma-separated or repeated; a bare name matches exactly, e.g.--pattern python-django3to check a single package).--start-from <name>— resume from this package onwards in the inventory's iteration order, e.g. to continue an interrupted run.--end-with <name>— stop after this package (inclusive). Combine with--start-fromto scope to a name-range, e.g.--start-from rust-nu-cli --end-with rust-nu-utilsto test the change against everyrust-nu-*package in one shot.--batch [EMAIL]— one Bugzilla query for everything assigned to or CC'ing EMAIL (default: the configured email) instead of one query per retired package per branch; with--all-reportersthe batch query drops the reporter filter too.
Network reads (dist-git probes, Bugzilla searches) retry up to
3 times with exponential backoff, so a transient connection
hiccup against src.fedoraproject.org doesn't abort the whole
inventory.
Triage update bugs
Some packages reliably need attention when a new upstream version
appears — python-django* updates almost always fix CVEs, for
instance. Mark them in the inventory with a priority field (or
a workload-level default_priority), then have poi-tracker
triage the auto-filed release-monitoring bugs by raising their
Bugzilla priority:
For each inventoried package with a resolved priority, this
queries OPEN bugs reported by upstream-release-monitoring@ fedoraproject.org (against Fedora and Fedora EPEL) and
raises any whose priority is unspecified. Bugs already
triaged by a human are left alone.
Per-package priority wins over default_priority; if a
package is in multiple workloads, the highest workload
default applies. Set priority = "unspecified" on a package
to explicitly opt out of a workload default.
Independently of priorities, every open release-monitoring bug is also checked against Bodhi for builds that already carry the advertised version (or newer). When found, the latest addressing build per release is recorded in the bug's Fixed In Version field, and:
- stable in every active release the package has a branch
for → the bug is closed as
ERRATA, with a comment listing the Bodhi updates; - any addressing update still in testing → the bug is moved
to
MODIFIED(a later run closes it once everything is stable); - addressed only in some releases (commonly just rawhide,
since stable branches often intentionally stay behind) → you
are asked before closing.
--close-stalecloses these without asking; under-ythey are skipped unless--close-staleis given.
Bodhi records updates per release, so a release can carry a build
Bodhi has no update for in that release — content inherited at
branching, or answers missing while Bodhi is degraded. For those,
the release's Koji tag chain is consulted (via koji, checking
fXX-updates + fXX and the EPEL equivalents through tag
inheritance), so years-old stale bugs close too — while a version
merely committed to dist-git and built into a side tag or
-candidate/-testing tag correctly stays pending. Without the
koji CLI this check is skipped (with a warning) and such bugs
are simply left open. Genuinely pending bugs are cheap: rawhide is
checked first, and since a stable release may never carry a newer
version than rawhide, a version absent from rawhide skips the
stable-release queries entirely (EPEL bugs, whose branches update
independently, are always checked in full). Pass --skip-stale
to disable the whole check (also restoring the cheaper
priority-only scan), and --pattern <glob> (e.g. rust-*) to
scope the run. --batch [EMAIL] works as in semver-audit: one
Bugzilla query for everything assigned to or CC'ing EMAIL
(default: the configured email) instead of one query per package.
As in triage-retired, interactive runs offer to claim ownership
of the bugs being closed (set assigned_to to your configured
Bugzilla email). Pass --claim to claim without prompting —
under -y this is the only way to opt in. Bugs moved to
MODIFIED keep their assignee: they stay open and belong to
whoever owns the in-flight update. The email is set via
poi-tracker config.
Stop keeping packages (unkeep)
Remove packages from the keep inventories and learn, without re-walking
anything, what else stops being needed — over the dependency graph a
deps --graph run saved:
Use it before dropping a keep to see what the removal frees;
reconcile only notices a keep that is already gone.
The report names each freed package with its former requirers; adding
--apply removes the unkept packages from the -i keep inventories
and the freed ones from the --deps derived inventories, so the next
kondo run offers them all as candidates. An unkept keep the
remaining closure still reaches is not freed — it is moved: provably
still needed, it is filed into the first --deps inventory
immediately, with a reason chain taken from the graph, so pruning
redundant curated entries needs no follow-up walk and leaves no window
where they are essential in no file. Unkeeping a package that was
never a keep and is still reached only warns — culling it would be
rescued right back.
Reachability follows every provider the walk recorded, so nothing is
freed while any alternative chain still holds, and a derived package's
src: build-dependency edges lapse with it, so test-only build
dependencies cascade out the way the fixpoint pulled them in. The
graph is a snapshot: the periodic full deps --graph walk is its
refresh. --json prints the report machine-readably.
Validate
Inventory format
[]
= "hyperscale-packages"
= "CentOS Hyperscale SIG packages"
= "centos-hyperscale"
= ["eln-extras"]
= ["poc", "reason", "team", "task"]
[]
= "hs-packages"
= "Hyperscale SIG workload"
= ["eln-extras"]
[]
= "hs-epel-packages"
= "Hyperscale EPEL workload"
[[]]
= "systemd"
= "Linux Userspace <team@example.com>"
= "Core init system"
= ["systemd-networkd"]
= ["hyperscale"]
= "upstream"
[]
= ["systemd-boot-unsigned"]
= ["systemd-boot-unsigned"]
[[]]
= "fish"
= ["fish"]
= ["hyperscale", "epel"]
= "upstream"
Fields
| Field | Level | Description |
|---|---|---|
name |
inventory/package | Name (required) |
description |
inventory | Human-readable description |
maintainer |
inventory | Maintainer (person or team) |
labels |
inventory | Default labels for content-resolver |
workloads |
inventory | Workload definitions (map) |
workloads |
package | Workload membership (list) |
private_fields |
inventory | Fields stripped on export |
poc |
package | Point of contact |
reason |
package | Reason for tracking |
team |
package | Team responsible |
task |
package | Internal task/ticket |
rpms |
package | Binary RPMs to track |
arch_rpms |
package | Architecture-specific RPMs |
track |
package | hs-relmon tracking branch |
repology_name |
package | Repology name override |
distros |
package | hs-relmon distribution list |
file_issue |
package | File GitLab issues |
priority |
package | Bugzilla priority for triage-updates (unspecified/low/medium/high/urgent) |
retired_on |
package | Dist-git branches where the package is retired; written by triage-retired --mark |
unshipped |
package | Reason the package is no longer shipped on any active branch; written by prune-retired. Skipped by most operations, still processed by triage-retired, preserved by sync --prune |
archived_builds |
package | Reason an archived-upstream package still has CBS builds; written by sync-gitlab --mark-unshipped. Still ships (not skipped); a build-cleanup candidate for hs-relmon |
default_priority |
workload | Default Bugzilla priority for packages in this workload |
Each [inventory.workloads.<key>] section can override name,
description, maintainer, labels, and default_priority
for content-resolver export and triage-updates. Omitted
fields fall back to inventory-level values.
System-wide configuration
Settings are read from /etc/poi-tracker/config.toml first, then
overridden per key by ~/.config/poi-tracker/config.toml, with
command-line flags overriding both. A system file alone is enough — no
per-user file is required — and either may also carry a [defaults]
table pinning flag defaults (see the root DEVELOPMENT.md).
poi-tracker config writes the user file only, with 700 on the
directory and 600 on the file. Nothing writes under /etc: a system
file is admin-authored, holds shared non-secret settings, and is
normally shipped root:root 0644.
Credentials belong in the per-user file, which is 600, or in an
environment variable — a token under /etc is readable by every local
user. For an unattended machine, give the job its own user and its own
600 config.
License
Licensed under either of
at your option.