-
Notifications
You must be signed in to change notification settings - Fork 1
Maintenance actions
What fettle does when you type it, how to read what it prints back, and what
each action actually runs on each distro family.
Every run ends in a Summary, and the mark in front of each line is load-bearing:
| Mark | Means |
|---|---|
✓ |
it happened |
! |
it did not happen, and that may be fine — you declined a prompt, or a tool was absent |
✗ |
it failed |
The distinction between the last two is deliberate and was added because it is genuinely
ambiguous: pacman, apt and dnf all exit non-zero both when you answer "no" at their
prompt and when they genuinely break. With --yes there was no prompt to decline, so a
non-zero exit is a real failure and gets ✗. Without it, fettle says what it knows and no
more.
Exit status: 0 unless something reported a failure (✗), in which case 1. A run you
declined exits 0 — you got what you asked for. This matters for cron and CI: a maintenance
run whose work was blocked no longer looks like a successful one.
"Could not look" is never reported as "clean." If a check could not run — a tool missing,
a repository unreachable, a query that failed — it says so rather than returning the same
output as a healthy system. Several of fettle's worst bugs were exactly that confusion, and
the QA plan in docs/qa/ exists to keep finding them.
Run with no action to execute the configured default set. Every action accepts three interchangeable forms — a short flag, a long flag, or a bare word — and they combine freely:
fettle -c # short flag
fettle --clean # long flag
fettle clean # bare word
fettle -c -u # combine, in the order fettle defines
fettle clean update # identical to the line aboveThe table below lists the short flag and the word; the long flag is the word with -- in
front of it (--clean, --orphans, --config-drift). fettle -h lists all three, in
these same two groups.
· marks the default set — what fettle runs with no arguments (and fettle -a).
Everything else is opt-in.
Anything a distro's backend doesn't support is skipped with a note.
Disabled (superseded) snap revisions are offered on every distro that has snapd, not just Debian — each revision confirmed individually.
| Flag | Action | Arch | Debian | RHEL family |
|---|---|---|---|---|
-c · |
clean |
paccache — drops packages no longer installed, keeps the last 2 versions of the rest ([clean] keep_versions); AUR build dirs (asks first; --yes skips) |
apt-get clean, unused flatpaks |
dnf clean packages — not clean all, so repo metadata survives; unused flatpaks |
-o · |
orphans |
foreign pkgs → ~/.fettle/reports/; remove true orphans (-Qtdq) — the package manager confirms the full transaction, which may exceed what you picked |
obsolete pkgs → ~/.fettle/reports/; orphaned libraries (deborphan, or dpkg reverse-deps where it is gone) + autoremove, same confirmation |
pkgs from no enabled repo (repoquery --extras) → reports; repoquery --unneeded, kernels never offered
|
-u / --upgrade · |
update |
mirrorlist refresh (Manjaro; [updaters.arch] refresh_mirrors), then pacman/pamac, then yay AUR (with review) |
apt/nala — the upgrade is skipped if the package lists could not be refreshed — then flatpak, then snap |
dnf upgrade --refresh, then flatpak/snap; a gpgcheck=0 repo asks once more |
-O |
only-update |
refresh safely — private cache, never pacman -Sy (no partial-upgrade risk) — then report upgradable |
apt-get update --error-on=any + flatpak metadata, then report upgradable |
dnf makecache + report upgradable (check-update; exit 100 means updates exist) |
-r · |
rebuild-check |
checkrebuild (rebuild with -R) + reboot check: warns if the running kernel's modules were replaced |
needrestart — services and the kernel state (KSTA), so a pending reboot is reported |
needs-restarting — reboot hint + services; only exit 0 may mean "no reboot"
|
-y · |
python-rebuild-check (arch)
|
rebuild pkgs stranded on an old /usr/lib/python3.X (skips Python interpreters themselves; flags orphaned dirs) |
— (apt handles transitions) | — (dnf handles transitions) |
-d · |
config-drift |
.pacnew (yours still live) vs .pacorig (yours displaced) and .pacsave
|
.dpkg-dist/.ucf-dist (yours live) vs .dpkg-old/.ucf-old (yours displaced) + dpkg --audit
|
.rpmnew (yours still live) vs .rpmsave/.rpmorig (yours displaced) + dnf check
|
-x · |
auto-updates |
report enabled auto-update timers (known units) | report unattended-upgrades state (apt-config + apt-daily-upgrade.timer) |
dnf-automatic — all four timers, since -install applies updates even with apply_updates = no; warns if the host reboots itself |
| all three also check the timer is actually succeeding — enabled but failing every night is reported, not counted as ON | ||||
-f · |
firmware |
fwupdmgr (shared) — verdict from fwupd's exit code, so a dead daemon reads as UNKNOWN, not "up to date" |
same | same |
-k |
kernel |
mhwd-kernel (running series protected; removal is user-named) |
dpkg -l 'linux-image-*', purge old (running AND newest protected; apt confirms its own transaction; nudges to reboot) |
informational only — dnf enforces installonly_limit itself, so nothing is offered for removal; flags a pending reboot |
-C |
container-update |
pull container images — every installed runtime (docker and podman), asking per image; images built here are never offered ([containers]) |
same | same |
All read-only — none of these changes the system. Only -P runs under -a; the rest
are opt-in. -S is the odd one out and the deepest: it scans firmware, boot and hardware
rather than packages, and elevates itself.
| Flag | Action | Arch | Debian | RHEL family |
|---|---|---|---|---|
-S |
sys-audit |
firmware/boot/hardware security scan — Secure Boot, TPM, microcode, IOMMU, SPI/BIOS, storage firmware; self-elevates | same | same |
-P · |
pkg-audit |
package supply-chain audit → ~/.fettle/reports/
|
apt/flatpak/snap provenance | dnf/yum repo provenance + flatpak/snap/containers/extensions |
-V |
pkg-integrity |
paccheck --sha256sum against pacman's MTREE (falls back to pacman -Qkk) |
debsums against the .md5sums dpkg installed (falls back to dpkg --verify) |
rpm -Va against the rpmdb's file digests |
-A |
aur-audit (arch)
|
AUR health table → ~/.fettle/reports/
|
— | — |
-H |
hardening-audit |
is this system hardened? seven axes — build flags (needs checksec), filesystem, services, kernel, sshd, firewall, TLS certificates → ~/.fettle/reports/
|
same, via dpkg-buildflags baseline |
same, via rpm's %{build_cflags} macros; binaries attributed with rpm -qf
|
-p |
aur-precheck (arch)
|
per-package pre-install check (RPC + IoC); bare = every installed AUR pkg | — | — |
-U |
upgrade-check |
(experimental) AI pre-upgrade safety check; needs ANTHROPIC_API_KEY
|
same | same |
-M |
compromise-check |
is something ALREADY here? boot persistence, loader/kernel integrity, running processes, boot chain. Reports anomalies to investigate, never a fix | same | same |
| — | advisory-check |
installed packages with known CVEs (fix available, or no fix yet) | same | same |
update asks before upgrading (the package manager shows its plan and
prompts); pass --yes to skip the confirmation and run non-interactively.
On Manjaro, fettle -u regenerates /etc/pacman.d/mirrorlist before it upgrades
anything. That is a change to system configuration, so it is worth knowing about — and as
of 0.54.0 it is configurable.
It runs pacman-mirrors -f, which probes mirrors and rewrites the list in speed order.
This is on by default, because a mirror that has fallen behind serves an old package
database and the upgrade then resolves against versions that mirror no longer holds — a
real and recurring cause of failed upgrades, not a theoretical one.
[updaters.arch]
refresh_mirrors = true # default — regenerate the mirrorlist before upgrading
# refresh_mirrors = false # never touch the mirrorlist
# refresh_mirrors = 5 # rank the fastest 5 mirrors onlyConsider setting a number. Bare pacman-mirrors -f is not a moderate default: its
argument is optional and defaults to "no limit", so every upgrade speed-tests every mirror
it knows about. refresh_mirrors = 5 keeps the protection and bounds the cost.
Vanilla Arch and EndeavourOS have no equivalent wired up. pacman-mirrors is
Manjaro-only; when the setting is on and the tool is absent, fettle says so and points at
reflector rather than skipping in silence.
Nothing is done to your mirrorlist there.
No other distribution needs this. Fedora resolves mirrors through metalink, the RHEL family through a mirrorlist service, and Debian/Ubuntu through a CDN or apt's own failover — in each case the server picks, per request, so there is no local file to regenerate.
fettle -O answers "what is waiting for me?" and changes nothing else. It refreshes repo
metadata, then prints the transaction an upgrade would perform — including new
dependencies, not just version bumps — and stops.
It is not a prerequisite for update. Every backend already refreshes as part of
upgrading (pacman -Syuu, apt-get update before the upgrade, dnf upgrade --refresh), so
-O is the standalone look, not a step you must run first.
If the refresh fails, it says so and exits non-zero. A mirror can be down, a key can
expire, a laptop can be on a train. In that case the last-known list is still printed — it
is useful — but marked (from stale metadata), because newly published updates, including
security fixes, would not appear in it. Note that apt-get update exits 0 even when it
reached no repository at all, which is why fettle passes --error-on=any on apt 2.1+.
On Arch and Manjaro the system database is never synced. pacman -Sy without a full
upgrade is the classic partial-upgrade footgun, so the preview is resolved against a private
temporary database (the checkupdates technique) and /var/lib/pacman/sync is left
untouched. It also honours IgnorePkg, so packages you have pinned are not reported.
On Debian and Ubuntu, fettle -u refreshes the package lists and then checks whether that
worked before upgrading anything. If it did not, the upgrade is skipped and the run exits
non-zero:
✗ update: upgrade SKIPPED — the package lists could not be refreshed, and upgrading from
stale lists would report success while installing nothing (run `sudo apt-get update` to
see which repository failed)
This exists because the failure is otherwise invisible. apt-get update exits 0 when it
could not reach a single repository, and full-upgrade then finds nothing to install and
exits 0 as well — so a host that has been cut off from its repositories for months reports a
clean, successful upgrade every night. Measured on Debian 12 with every repository pointed at
an unreachable host, that is exactly what happened.
fettle passes --error-on=any (apt 2.1+, probed rather than assumed) so apt reports the
failure honestly. nala update already exits non-zero unaided and needs no flag.
One unreachable repository is enough to stop the upgrade. This is deliberate — the lists are incomplete, and fettle cannot know whether the repository it could not reach was holding a security update — but it does mean a host carrying a long-dead third-party repository will refuse to upgrade until that repository is fixed or removed. The message names the command to run to find out which one it was.
Extras stop too. flatpak and snap are not updated after a system upgrade that did not complete, and the run says so rather than going quiet.
fettle -r answers the question that matters after an upgrade: is the new code actually
running, or is something still on the old version? An update you have installed but not
activated is not an update. It is in the default action set for that reason.
It reports two different things:
-
A pending reboot. On Debian/Ubuntu from
needrestart's kernel state, on RHEL fromneeds-restarting -r, and on Arch/Manjaro by noticing that the running kernel's module directory has been replaced — at which point that kernel can no longer load any module it has not already loaded, so a USB device plugged in after the upgrade simply will not work. - Services still running old libraries, which need restarting but not a reboot.
It never restarts or reboots anything itself; it tells you and stops. On Arch, -R will
offer to rebuild packages built against since-upgraded libraries.
If the check cannot run, it says so. A missing or failing checkrebuild, empty
needrestart output, or a dnf4 host without yum-utils are all reported as "not
determined" rather than as a clean result — the distinction matters most here, because
"nothing to do" is exactly what a broken check looks like.
Three actions look at AUR packages. The clearest way to tell them apart is when you run them, not what they query — they share most of their queries on purpose.
-P pkg-audit |
-A aur-audit |
-p aur-precheck |
|
|---|---|---|---|
| when | routine, after the fact | after the fact | before an install |
| scope | every ecosystem: AUR, apt, flatpak, snap, containers, editor + shell extensions | AUR only | the package names you give it |
| output | findings, for reading | a census table |
CRIT/WARN lines, for a hook to parse |
| not in the AUR any more | ● | ● | ● |
| orphaned / flagged out-of-date / stale | ● | ● | ● |
| on a known-malicious package list | ● | ● | |
| maintained by a known-malicious account | ● | ● | |
| malicious JS dependency trace | ● | ||
| an IoC feed could not be read | ● | ||
| maintainer changed since last run | ● | ● | |
| votes, reverse dependents, removal candidates | ● | ||
in the default -a set |
● |
Which one do I want?
-
-P— the routine one, and the only one in the default set. Everything below, plus every other install channel on the box, plus the one thing no other view has: it tells you when an IoC feed could not be read, so a quiet result is never mistaken for a clean one. -
-A— "what should I clean up or stop trusting?" The full census: age, votes, maintainer, and the reverse-dependency analysis that finds AUR packages nothing on the system needs any more. Only-Atells you what is safe to remove. -
-p PKG …— the gate, not an audit: it runs before a package is built, and its output is a line contract for the yay hook and for-u's pre-upgrade check. Run it by hand when you are about to install something and want a second opinion.
Bare fettle -p (no package names) points the gate at everything already installed. It
works, and it says so when you run it — but -P reports the same facts and more.
-I/aur-ioc-scanwas retired in v0.73.0. Every check it performed is in-P, across every ecosystem. It had already been dropped from the default set because running both fetched the AUR RPC and the IoC feeds twice and reported each finding twice; retiring the flag finished that.fettle -Inow tells you where the capability went. The feed-coverage reporting that the retired-Iuniquely had — "the scan matched nothing, but the lists were never read" — moved into-Pas part of the retirement, because losing it would have reintroduced the exact bug its QA sweep fixed.
They keep separate maintainer-change baselines. Sharing one meant whichever action ran first consumed the difference and rewrote the file, so a maintainer takeover was reported once and was invisible to the other — the exact signal all three exist to catch.
Package managers leave a file behind whenever an upgrade meets a config file you had
edited — but which file they leave tells you two very different things, and fettle -d
now says which:
| What happened | Arch | Debian | RHEL |
|---|---|---|---|
| New default shipped; your file is still in effect | .pacnew |
.dpkg-dist, .ucf-dist
|
.rpmnew |
| Your file was moved aside — the package's version is in effect now | .pacorig |
.dpkg-old, .ucf-old
|
.rpmsave, .rpmorig
|
| Your file kept after the package was removed | .pacsave |
— | — |
The middle row is the one worth waking up for: a setting you deliberately made has silently stopped applying. Those are reported as warnings and counted separately:
✓ 4 config file(s) to review — 1 where YOUR version is no longer in effect
The scan walks /etc on every distro. On Arch that is deliberate rather than delegating to
pacdiff, which only reports leftovers whose base file still exists — a .pacsave is
created when a package is removed, so it has no base file and pacdiff never mentions it.
pacdiff and rpmconf are still suggested as the tools to merge with.
fettle -o lists packages nothing depends on any more and offers to remove them, one at a
time. Two things worth knowing before you say yes:
The real transaction can be larger than your selection. Removing an orphan also removes dependencies that orphan was the last thing needing — choosing one package can remove several. fettle therefore lets the package manager show and confirm its own transaction, so the full set is a decision point rather than a surprise. Declining there removes nothing.
The count reported is what actually went, measured from the installed set before and after, and it names anything removed beyond what you picked:
✓ 2 package(s) removed (including 1 unused dependency(ies): lua54)
keep_orphans in the config protects packages from ever being offered, and they are named
when held back. On RHEL, kernels are never offered at all — and if the query that identifies
them fails, nothing is offered rather than guessing.
--yesmeans "delete them" here. For automation that is the point; it is also the one action where an unattended run removes software.--dry-runfirst if in doubt.
fettle -c reclaims disk from downloaded package files — the copies your package
manager keeps after installing. It never removes installed software, and it always asks
first (--yes skips the prompt, --dry-run shows what would run and changes nothing).
The summary states what actually happened, measured from the cache directory rather than taken from the package manager's word for it:
✓ caches cleaned — 39.2 MiB reclaimed
✓ caches already clean — nothing to reclaim
✓ would clean caches # --dry-run
Arch / Manjaro / EndeavourOS. The cache is also your offline rollback path: a bad
upgrade is undone with pacman -U /var/cache/pacman/pkg/<older>.pkg.tar.zst, which only
works while that file is still there. So cleaning is split by rollback value —
- cached packages no longer installed at all are removed outright (no rollback value);
- superseded versions of installed packages are trimmed to the last
keep_versions(default 2), so every installed package keeps a working rollback target plus a spare.
A stopped snapd no longer hangs the run. If the snap binary is installed but the
daemon is not answering, fettle says so and moves on:
! snapd is installed but not responding (the service is stopped or wedged) — snaps were
NOT audited; `systemctl start snapd.socket` if you use snaps, or remove the snapd
package if you do not
This matters more than it sounds on Arch and Manjaro, where snapd ships disabled by
default. Install it, never enable the socket, and every snap command blocks forever —
including the read-only inventory clean runs. Before v1.16.0 fettle inherited that hang
outright: -c, -P and -a never returned, --dry-run included, because a read-only
query deliberately bypasses the dry-run gate.
Checking whether /run/snapd.socket exists does not answer the question: the socket
file survives the daemon, so it is present on exactly the host where snap does not work.
fettle asks snap something cheap under a short clock instead, once per run.
It will refuse to clean while a transaction is running. /var/lib/pacman/db.lck is
pacman's transaction lock, and if it is held, some other pacman or AUR helper is mid-write.
fettle names the process holding it and stops:
✗ clean REFUSED — a transaction is running (pacman, pid 4821)
fettle never deletes that file. Until v1.8.0 it did, unconditionally, on every -c —
which is the documented way to corrupt a package database, because removing the lock does
not stop the process holding it, it just lets a second writer in alongside the first. A
stale lock (no process holds it) is reported with the rm command to run, and the clean
proceeds; the deletion stays your decision, because only you know whether the machine
crashed mid-upgrade. If the holder cannot be identified — the lock exists but /proc is
not readable — that is also a refusal, because "I could not tell" is not "it is safe".
Both use paccache from pacman-contrib. Without it, fettle falls back to pacman -Sc, which removes only packages that are no longer installed — correct, less thorough,
no extra dependency. AUR helper build directories (~/.cache/yay, ~/.cache/paru,
~/.cache/pamac, and pamac's /var/tmp build tree) are removed too; this is the only
family where the prompt mentions build directories, because it is the only one that has
any.
pacman -Sccis deliberately not used. With--noconfirmit removes nothing at all (its prompt defaults to No), and answering yes would delete the cached copy of every installed package — destroying offline rollback to reclaim a little more disk.
Debian / Ubuntu / Mint / Pop!_OS. apt-get clean empties /var/cache/apt/archives.
Package lists under /var/lib/apt/lists are untouched, so no apt update is forced
afterwards. apt-get autoclean is not run: clean has already emptied the directory, so
it would have nothing to consider. Unused flatpak runtimes are removed when flatpak is
present.
RHEL / CentOS Stream / Rocky / AlmaLinux / Oracle. dnf clean packages, deliberately
not clean all. Measured on a RHEL 10.1 host, /var/cache/dnf held 796 MB of which
736 MB was .rpm files and 60 MB was repo metadata — clean packages frees the 736 MB,
while clean all would also discard the metadata and force a slow re-download on the very
next dnf command. Note these systems ship keepcache=0, so a large RPM cache usually means
an interrupted transaction, which is exactly when reclaiming it helps. Both dnf
generations are handled: dnf5 caches under /var/cache/libdnf5.
There is no version-retention knob on apt or dnf — neither keeps a version history to trim,
so keep_versions applies to the Arch family only.
Configuration (see the full config example):
[clean]
keep_versions = 2 # Arch family: cached versions kept per INSTALLED package.
# 0 keeps none — frees the most, leaves no offline rollback.Packages you no longer have installed are removed regardless of this setting; retention only ever protects things you could actually roll back to.
Three more flags are shortcuts to subcommands (not part of the action
pipeline): -S → sys-audit --all (security scan), -U → upgrade-check (AI
advisor), -p → aur-precheck (AUR pre-flight; bare = scan all installed). Use
the subcommand form for their own options.
Default set (run when you pass no action, or -a/--all): clean, orphans,
update, rebuild-check, python-rebuild-check, config-drift, auto-updates,
firmware-check, and — last, read-only — pkg-audit (-P), so a full run also
reports where your packages came from and whether any of them matches a
known-compromise feed. -I was retired in v0.73.0: -P already runs all
three of its checks (see Three AUR checks).
Excluded from the default set — request explicitly: -O, -k, -A, -H.
--everything runs every action that is safe start-to-finish without supervision, in an
order chosen so each one describes the state the previous left behind. It works locally
and over fettle remote.
fettle --everything # or: fettle remote myhost --everything --yesIt adds what -a leaves out — pkg-integrity, hardening-audit, aur-audit, and the
two that were previously only reachable as their own subcommands, sys-audit and
advisory-check, plus compromise-check. Actions your distro does not have are filtered out and reported, so
the same command is correct everywhere; there is no per-distro list to maintain.
The order is not alphabetical, and not the order you would type them. clean runs
first, because it frees space before the upgrade needs it — which is the whole point on
a box with a small /var. It costs nothing to do first: clean removes cached packages
that are no longer installed and trims the rest to keep_versions, while the upgrade
downloads new versions that were never in the cache.
update follows, so everything downstream describes the system you will actually be
running rather than the one you booted. The two rebuild checks come next because they
exist to catch what the update just made stale. orphans and config-drift follow
because an upgrade is what creates both — packages left with no dependents, and
.pacnew/.rpmnew files waiting to be merged. pkg-integrity runs after the update or
it would verify packages about to be replaced. And advisory-check runs last, where it
reports what is still unfixed rather than listing everything the update was about to
fix anyway. compromise-check runs after even that — the final
question, and the one an update cannot answer: patching removes a vulnerable
package, it does not remove an implant that is already running.
The remaining audits are genuinely order-independent. pkg-audit and aur-audit are kept
adjacent only because both query the AUR RPC and the second benefits from the first's
TTL-cached fetch.
One caveat if you have set keep_versions = 0: cleaning first then removes every
cached version including the one you are currently running, so an upgrade that goes wrong
leaves you no offline rollback. The default of 2 keeps the running version cached.
Two actions are deliberately absent. kernel can remove your ability to boot — it is
kept out of the default set for that reason and stays out here — and container-update
pulls images over the network. Both are one flag away. --only-update is redundant once
update runs.
Its exit status answers "did the run complete", not "is the machine clean", and that
is a deliberate difference from a single action. Fourteen checks on a real host will
essentially always find something — advisory-check alone reported 142 fix-available on
the machine this was developed on — and a status that is red every single time is one
nobody reads. Findings are in the summary. If you want to gate automation on a specific
condition, run that action on its own: fettle -V still exits non-zero when a packaged
file's contents have changed.
It is not quick. On a workstation with ~4,400 binaries the hardening audit alone is
minutes, and sys-audit runs chipsec. The step counter tells you where it is.
auto-updates (-x) is a read-only, informational report of whether the
system is set up to update itself unattended — on Debian/Ubuntu whether
unattended-upgrades is installed and its apt-daily-upgrade.timer /
APT::Periodic knobs are on; on Arch whether a known auto-updater systemd timer
(e.g. arch-update.timer, pacman-auto-update.timer) is enabled. It states the
fact and offers no opinion; a custom-named Arch timer won't be recognized.
-R / --auto-rebuild turns the -r / -y checks from "list" into "offer to
rebuild". Destructive steps (orphan/kernel removal, disabled-snap pruning) always
prompt per item unless you pass --yes.
When fettle hands off to a package manager (yay/pacman/apt), it brackets that
tool's live output in a labeled banner (──── yay ──── output below is yay's, not fettle's ────) so you can always tell fettle's messages from the tool's.
fettle -u --dry-run resolves and lists every package the upgrade would
install — version upgrades, the new dependencies they pull in, and any
removals — grouped by source, before it prints the commands it would run. It
changes nothing and needs no sudo.
▸ [1/1] Updating packages
14 package(s) would be installed/changed:
official repos (12):
linux 6.12.1-1 -> 6.12.4-1
systemd 257.2-1 -> 257.3-1
+ libfoo 2.0-1 (new dependency)
- obsolete-lib 1.2-3 (remove)
…
AUR (2):
brave-bin 1:1.92.134-1 -> 1:1.92.138-1
would run: pacman -Syuu
…
On Arch this uses checkupdates' trick — a throwaway package DB synced in /tmp
via fakeroot — so the preview reflects fresh mirror data without touching
your system DB or needing root (pacman-contrib + fakeroot recommended; it
degrades to cached data with a note otherwise). On Debian/Ubuntu it's apt's
native apt-get -s dist-upgrade simulation. Pass --no-sync to skip the refresh
and preview against the cached data (faster; may be stale). AUR -git/-devel
packages that rebuild from source may not show a version bump until yay fetches
them — noted in the output.
On the RHEL family the preview is deliberately partial by default, and says so:
dnf's full resolver (dnf upgrade --assumeno) refuses to run without root, and there
is no rootless apt-get -s equivalent, so a plain --dry-run lists upgrades and
reports that new dependencies and removals are missing. --full-preview elevates to
resolve the real transaction — on a live RHEL 10.1 box that is 345 packages against the
337 the rootless query can see. -O already runs as root, so it gets the complete set
without the flag.