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.
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).
Usage
Show inventory
Add / remove packages
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
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.
Import from legacy JSON
Validate
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.
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 (a stale
release-monitoring bug, nothing to push) is reported as up to
date (stale bug). 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.
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.
Builds that shipped before the current releases existed have no
Bodhi record (they were inherited at branching); for those the
branch's dist-git spec is consulted instead, so years-old stale
bugs close too. 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.
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.
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.
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.
License
Licensed under either of
at your option.