Skip to content

linux-updater: background system updates with history and rollback for any major distro - #389

Draft
UmedjonBA wants to merge 5 commits into
noctalia-dev:mainfrom
UmedjonBA:linux-updater-plugin
Draft

linux-updater: background system updates with history and rollback for any major distro#389
UmedjonBA wants to merge 5 commits into
noctalia-dev:mainfrom
UmedjonBA:linux-updater-plugin

Conversation

@UmedjonBA

@UmedjonBA UmedjonBA commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Plugin

  • Id: umedbazarov/linux-updater
  • New plugin
  • Update to an existing plugin (version bumped in plugin.toml)

What it does

A system updater for any major distribution, grown out of arch-updater (relation disclosed below). One click updates the system in the background: polkit asks for the password, the panel shows a live tail of the update log with a progress bar, the run is fully non-interactive and detached (it survives a shell restart — the engine re-attaches to the log). Finished runs land on a history strip: one segment per run, click opens its package list, and where the distribution supports it packages or whole runs can be rolled back (two-click confirmation; the package manager itself refuses any transaction that would break dependencies). An expandable Ignored section unifies the plugin's own ignore list (editable from package rows) with the system's mechanisms (IgnorePkg, apt-mark hold), and the ignore list is honored during updates. When the system is missing a one-time setup piece — the polkit keep-authorization rule so one password covers a run, apt's list-refresh timers — the panel says so and offers a one-click, one-confirmation fix; nothing is changed silently.

The engine is distro-neutral. Each package manager is a backend file (backends/*.luau) declaring commands, parsers and a capability set the UI honors, so a backend without rollback simply shows no rollback buttons. The backend is picked from /etc/os-release (ID, then ID_LIKE — Manjaro and friends resolve to pacman), overridable in settings:

  • pacman (Arch family): full feature set — AUR via paru/yay, [ignored] detection, download size, Arch news, cache-based per-package/per-run rollback with same-run dependencies, --ignore.
  • dnf (Fedora): old→new versions via rpm join, whole-run rollback through dnf history undo (transaction id recorded after each run) plus per-package dnf downgrade while the old version is still in a repo (a cache-only repoquery probe greys the button with the reason otherwise), needs-restarting, --exclude.
  • apt (Debian/Ubuntu/Mint/…): unprivileged check via apt list --upgradable (relies on the apt-daily timers; a self-check offers to enable them), holds applied for the duration of the transaction, per-package/per-run rollback from the apt archive cache (gone entries greyed out — Ubuntu cleans that cache routinely, Debian keeps it), /var/run/reboot-required.
  • zypper (openSUSE): dup on Tumbleweed / up on Leap, locks for the ignore list, zypper needs-rebooting; rollback deliberately off (snapper is the right tool there).
  • xbps (Void): dry-run check, holds via xbps-pkgdb; runit-aware (no systemd calls).
  • PackageKit: generic fallback for anything else; escalation through PackageKit's own polkit policies (no pkexec at all); the plugin ignore list is honored by updating an explicit pending-minus-ignored package list.

Flatpak checking/updating works on every backend, and opt-in extra sources (global npm, cargo via cargo-update, pip [check-only — distribution Pythons are externally managed, PEP 668], RubyGems, Snap, Homebrew; each off by default and silently skipped when its tool is absent) ride along with the check and the update run, honor the ignore list, and roll back per item where their manager has a mechanism (npm/gem/cargo reinstall the recorded version, snap revert, Flatpak pins the full commit recorded at check time). A secondary source that cannot be checked (an unreachable AUR mirror, a registry that is down) fails only itself: the remaining sources are still checked and the panel reports the failed one under the headline. NixOS is intentionally out of scope (nix-monitor already covers it); the backend interface is open for a Gentoo/other contribution. The README carries the full capability matrix and an honest Testing status section.

External dependencies

Declared in dependencies; only the entry matching the distribution is actually needed (the panel reports what is missing): pacman+pacman-contrib, or dnf+rpm, or apt-get+dpkg-query, or zypper+rpm, or xbps-install+xbps-pkgdb, or pkcon; plus POSIX base tools and pkexec; optional paru/yay, flatpak, sudo, xdg-open, less, a terminal emulator; and, only when the matching extra source is enabled, npm/cargo-install-update/pip/gem/snap/brew.

Full accounting: network access equals what the corresponding manual check/upgrade would contact, plus the Arch news feed on the pacman backend. Files are written only to the plugin data directory (update.log, runs.json, ignore.json, news_state.json, run_meta.json, activity_state.json when the opt-in activity graph is on, staged polkit rule + install marker) — with one exception: /etc/polkit-1/rules.d/49-linux-updater-<pm>.rules, written by pkexec install only after the user explicitly clicks the install button and authenticates. Spawned processes per backend are listed in each backends/*.luau header and the README Notes. Package-manager configuration files are never edited; the ignore mechanisms used are the managers' own (hold/lock), applied and released around the transaction.

Testing

  • Arch (pacman backend): fully exercised on a real system — background run including an AUR build (live log, progress, notification, auto re-check), per-package rollback from the cache and roll-forward, ignore/unignore, the polkit rule install button, history strip, engine resume mid-run.

  • dnf/apt/zypper/xbps/PackageKit: command layers exercised in podman containers on the real package managers — including the full upgrade → dnf history undo cycle and the per-package dnf downgrade + probe on Fedora 41, apt hold semantics and the cache rollback (a curl+libcurl pair downgraded in one transaction) on Ubuntu 24.04, and PackageKit's explicit-list update leaving the "ignored" package untouched — with outputs recorded as fixtures; all parsers run against those fixtures in a test harness (luau CLI).

  • Extra sources: full update → rollback cycles with the plugin's exact commands in node:22, ruby:3.3 and rust:1 containers; pip's listing in python:3.12; the Flatpak full-commit downgrade story on Ubuntu 24.04. Snap and Homebrew parsers are from documented formats only (snapd needs systemd, brew a full bootstrap) — stated in the README.

  • Not verified by anyone yet: live polkit dialogs / full UI on non-Arch distributions (containers cannot reproduce a polkit session), the dnf4 output branch, Debian deviations from Ubuntu, snap/brew against live tools. Non-Arch backends are labeled beta in the README; capability flags mean a gap degrades to a missing feature, not a broken system.

  • Tested on Niri

  • Tested on Hyprland

  • Tested on Sway

  • Tested on another compositor:

  • Noctalia version tested against: noctalia-git 5.0.0.r5274.gbf4da239d-1

  • Plugin API level: 9

Screenshots / Videos

In the plugin directory (also embedded in the README): screenshots/panel.png (pending updates with the download estimate and the polkit-rule offer) and screenshots/history.png (clean state with the update-history strip after an update → rollback → roll-forward cycle).

Checklist

  • The directory name matches the part of id after the / in plugin.toml exactly.
  • It ships plugin.toml, README.md, thumbnail.webp, and translations/en.json.
  • README.md follows the
    README template, documents
    every entry id and dependency, and includes exact panel IPC commands and launcher prefixes where applicable.
  • I created thumbnail.webp with the thumbnail generator.
  • version follows semver and is bumped in this PR; plugin_api is the oldest API level this plugin requires.
  • Every non-English translation in this PR uses a locale supported by Noctalia core, and I can read, write, and
    understand that language well enough to review and maintain it (no unreviewed machine/LLM translations).
  • I did not edit catalog.toml; CI generates it.
  • This PR touches exactly one plugin directory.

This PR ships translations/en.json only.

Code review attestation

Plugins run as trusted, unsandboxed Luau in the user's session. Confirm:

  • The code is readable and not obfuscated, minified, or generated.
  • It does not download and execute remote code.
  • Every network call, filesystem write, and spawned process is something the description above accounts for.
  • I have the right to publish this code under the license declared in plugin.toml.

Relation to arch-updater: this plugin generalizes the background-update engine I proposed for yuuto/arch-updater in #384. If #384's feature part is not wanted there, this standalone plugin is the home for it; the bugfix commit of #384 stands on its own either way.

A note on language: the author doesn't speak English and used AI assistance for this text and the review conversation to come. The plugin itself is tested as described above.

🤖 Generated with Claude Code

@UmedjonBA
UmedjonBA force-pushed the linux-updater-plugin branch from 2580fb2 to 93a3972 Compare August 17, 2026 08:45
Multi-distro system updater grown out of arch-updater: background
updates with a live log tail and progress, an update-history strip
with rollback, panel-managed ignore lists, and one-click fixes for
missing system setup. The engine is distro-neutral; per-manager
backends (pacman, dnf, apt, zypper, xbps, PackageKit as the generic
fallback) declare capabilities the UI honors, and the right backend
is picked from /etc/os-release. Command layers of the non-Arch
backends are verified in containers against recorded fixtures; the
plugin is fully exercised on Arch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@UmedjonBA
UmedjonBA force-pushed the linux-updater-plugin branch from 93a3972 to 8f50bb8 Compare August 17, 2026 09:22
@ItsLemmy

Copy link
Copy Markdown
Contributor
  1. blocking

The thumbnail generator was not used.

  1. blocking - linux-updater/backends/apt.luau:99

The background command separates repository refresh, ignore setup, and the upgrade with ;. An apt-get update or apt-mark hold failure
therefore does not stop the upgrade. The same failure-open behavior exists for xbps holds at linux-updater/backends/xbps.luau:72 and zypper
locks at linux-updater/backends/zypper.luau:82.

If hold or lock setup fails, packages the user explicitly ignored can still be upgraded. Gate the upgrade on successful refresh and ignore
setup, while retaining unconditional cleanup after the transaction.

  1. blocking - linux-updater/plugin.toml:9

The dependency metadata does not account for every external command spawned by the plugin. Examples include apt-mark and systemctl at
linux-updater/backends/apt.luau:59 and linux-updater/backends/apt.luau:131, rpm and head at linux-updater/backends/dnf.luau:58 and
linux-updater/backends/dnf.luau:110, pactree and wc at linux-updater/backends/pacman.luau:194 and
linux-updater/backends/pacman.luau:198, xbps-pkgdb at linux-updater/backends/xbps.luau:80, and install, grep, date, less, tee,
head, and rm throughout linux-updater/service.luau.

Repository policy requires external commands to be declared in dependencies and mentioned under README Requirements. Add the appropriate
command or package identifiers and document them.

  1. non-blocking - linux-updater/README.md:180

The Testing status says every backend parser runs against recorded fixtures in CI-able tests, but the PR contains no fixtures or parser
test harness. The repository validation only exercises manifest validation, not these parsers. Include the referenced test artifacts or
revise the testing claim so the advertised coverage is reproducible.

@ItsLemmy
ItsLemmy marked this pull request as draft August 17, 2026 12:35
@Reiling-Jeff

Reiling-Jeff commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Appreciate the transparency about the relation to #384, and generalizing the engine across distros is genuinely impressive work. Alongside the review that already blocked this on the dependency accounting, the fail-open hold/lock ordering, and the testing-status claim. I want to flag a separate point that those fixes won't resolve on their own.

#384 is still an open discussion, not a rejected one. The two structural points I raised there aren't arch-updater-specific, they're about the design of the background-update engine itself:

  • Terminal-driven updates need to stay a first-class, always-available option, not something that only appears after a silent background run has already failed. Some of us need to watch an AUR helper's (or in this case, any package manager's) interactive output as it happens, not just retry after the fact.
  • The stale-log timeout (RUN_STALE_LIMIT_S) can't be applied uniformly to interactive terminal runs - a user legitimately reviewing a diff/prompt for >30 minutes isn't a stuck process, and declaring it "failed" while the real process is still running risks a second concurrent run on the same log file.

Since this PR explicitly reuses that same engine, both concerns carry over directly; across six backends now instead of one. That's not a smaller surface than #384, it's a considerably larger one: every backend (pacman, dnf, apt, zypper, xbps, PackageKit) needs the same scrutiny this repo asks for trusted, unsandboxed code. readable, every side effect accounted for, no silent behavior changes for existing workflows.

So even once the already-flagged blocking items are fixed, I'd rather we settle the direction on #384 before this moves further. If "background-only, terminal demoted to fallback" isn't acceptable for arch-updater, that's the same answer for the shared engine here, it'll need addressing in every backend, not just the pacman one. If you're open to it, let's continue the design discussion on #384 and revisit this once that's resolved, so we're not reviewing the same unresolved design question twice.

UmedjonBA and others added 2 commits August 17, 2026 18:29
Review items:
- Gate apt/zypper/xbps upgrades on successful refresh and hold/lock
  setup (&&, fail closed) while keeping the cleanup unconditional;
  regression-tested in tests/run.sh.
- Declare every spawned external command in plugin.toml dependencies
  and document them under README Requirements.
- Ship the parser fixtures (fixtures/) and the test harness
  (tests/run.sh) the Testing status section refers to, and state their
  CI status precisely.

Also, ported from the arch-updater plugin:
- update_mode setting: background (default) or terminal-window updates
  sharing the same log/progress/history; terminal runs are exempt from
  the stale-log timeout, and their history entries are verified against
  installed versions so declined packages are not recorded.
- Opt-in activity graph of pending-update counts across recent checks.
- Fixes: startup race between run-resume and the auto-check, version
  constraints stripped from rollback dependency lists, flatpak ignore
  filtering and progress counting in both modes, no terminal retry
  offered after a failed rollback, polkit hint hidden in terminal mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@UmedjonBA

Copy link
Copy Markdown
Contributor Author

@Reiling-Jeff Thanks for taking the time on this, and for laying out why the two concerns carry over to every backend rather than just the pacman one.

I'm working on both sides: the blocking items here, and the design points you raised on #384. I agree they shouldn't be settled twice, so the answer that comes out of #384 is the one I'll apply across all backends here.

One practical note: I can't push the update right now. GitHub is having an ongoing incident today (Pull Requests, Actions, Git operations and the API are all degraded — https://www.githubstatus.com), so my pushes keep failing. As soon as it clears I'll send the update.

UmedjonBA and others added 2 commits August 17, 2026 20:07
Their parsers key on localized text (apt's '[upgradable from: ...]',
pkcon's severity words); a non-English locale would count zero updates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Opt-in extra sources beyond the system manager: global npm, cargo
  (via cargo-update), pip (check-only, PEP 668), RubyGems, Snap and
  Homebrew ride along with the check and the update run, honor the
  plugin ignore list, and roll back per item where their manager has a
  mechanism (npm/gem/cargo reinstall the recorded version, snap
  reverts, Flatpak pins the full commit recorded at check time).
- Rollback grew two roads: apt from the archive cache (per package and
  per run, epoch-encoded filenames, probe greys gone entries) and dnf
  per-package downgrade while the old version is in a repo (cache-only
  repoquery probe reports the reason before the attempt).
- The plugin ignore list is now honored on the PackageKit backend too
  (explicit pending-minus-ignored package list).
- A failing secondary source (AUR mirror down, registry unreachable)
  no longer aborts the check: it is reported under the headline and
  the remaining sources are still checked.
- Settings that change what a check would count invalidate the last
  result instead of showing stale numbers; LC_ALL=C pinned wherever
  output is parsed; scoped npm names accepted in the ignore list
  without reaching the native backend's hold path.
- Version 1.0.0: first public release starts at a clean major.
- Fixtures recorded from real containers (node:22, ruby:3.3, rust:1,
  python:3.12, ubuntu:24.04, fedora:41); the README Testing status
  section spells out what was verified where, and that snap/brew
  parsers are from documented formats only.
@UmedjonBA

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review — all four points addressed, plus a feature/fix round ported from my arch-updater plugin.

1 (blocking, fail-open holds/locks): fixed. apt, zypper and xbps now gate the upgrade on the refresh and the hold/lock setup with &&, so a failed hold stops the run before anything is upgraded; the unhold/removelock cleanup stays unconditional (; s=$?; … unhold …; exit $s). Added a regression test (check_gated in tests/run.sh) that fails if a ; ever reappears between the ignore setup and the upgrade.

2 (blocking, dependencies): fixed. plugin.toml dependencies now lists every external command the plugin can spawn across all backends (including apt-mark, systemctl, rpm, dpkg-query, pactree via pacman-contrib, xbps-pkgdb, head, wc, install, less, tee, date, grep, rm), and the README Requirements section documents them grouped by backend, with a note that only one family is needed on any given system.

3 (non-blocking, testing claim): fixed by shipping the artifacts. The recorded fixtures (fixtures/) and the parser test harness (tests/run.sh, luau CLI) are now part of the PR, and the Testing status section states precisely that they are runnable locally and not wired into this repository's CI.

0 (blocking, thumbnail): regenerated with the thumbnail generator; the checklist item in the PR description is now checked.

Also in this push, ported from arch-updater 2.4.0 with multi-backend adaptations:

  • update_mode setting — the default stays the non-interactive background run; a new terminal-window mode opens the upgrade in a terminal where prompts work as usual. Both modes tee into the same log, so the live tail, progress bar and history keep working; terminal runs are exempt from the stale-log timeout, and their history entries are verified against the installed versions afterwards (new optional backend hook installedVersionsCommand: pacman -Q / rpm -q / dpkg-query), so packages the user declined interactively are not recorded as updated.
  • Opt-in activity graph (off by default) of pending-update counts across recent checks.
  • Fixes: the apt and pkcon checks now run under LC_ALL=C (their parsers key on localized text — apt's [upgradable from: …], pkcon's severity words — so a non-English locale would have counted zero updates), a startup race between run-resume and the auto-check, version constraints stripped from rollback dependency lists, the plugin ignore list now also filters Flatpak in terminal mode, Flatpak lines counted in the progress bar, no "Retry in terminal" offered after a failed rollback, and the polkit hint hidden when it does not apply.

And a feature round on top — the version is 1.0.0, since this is the plugin's first public release:

  • Opt-in extra sources: global npm, cargo (via cargo-update), pip (check-only — PEP 668), RubyGems, Snap and Homebrew ride along with the check and the update run, honor the ignore list, and roll back per item where their manager has a mechanism (npm/gem/cargo reinstall the recorded version, snap revert, Flatpak pins the full commit recorded at check time — a short commit prefix turned out to 404 on the server, which is why the check records all 64 characters).
  • Rollback on two more backends: apt per-package/per-run from the archive cache (gone entries greyed out — Ubuntu cleans that cache routinely), dnf per-package downgrade while the old version is in a repo (a cache-only repoquery probe greys the button with the reason first).
  • PackageKit honors the ignore list via an explicit pending-minus-ignored package list.
  • A failing secondary source no longer aborts the check: an unreachable AUR mirror or registry fails only its own slot, reported under the headline; the remaining sources are still checked.
  • Stale-count invalidation: settings that change what a check would count (backend, toggles, ignore list) reset the last result instead of showing numbers the new settings would not produce.
  • Container verification for the new paths (Fedora 41, Ubuntu 24.04, node:22, ruby:3.3, rust:1, python:3.12) with the outputs recorded as fixtures; snap/brew parsers are from documented formats only, stated in the README Testing status.

@UmedjonBA

Copy link
Copy Markdown
Contributor Author

@Reiling-Jeff Both of your structural points are implemented in the update that just landed here, not just promised: update_mode makes the terminal run a first-class mode (every update opens in a terminal window where prompts work as usual — with the log, progress bar and history still working through tee), and terminal runs are exempt from the stale-log timeout for exactly the reason you gave — an interactive prompt held open for an hour is not a stuck process. The launcher also exposes both one-shot actions (update in terminal / update in background), so neither mode is demoted to a fallback. The history entry of a terminal run is verified against the installed versions afterwards, so packages declined at a prompt are not recorded as updated. Happy to continue the direction discussion on #392 — but for this engine the answer is already "both modes, user's choice", applied uniformly across all backends.

@Reiling-Jeff

Copy link
Copy Markdown
Contributor

---Hey @UmedjonBA, appreciate the work here, the multi-distro backend logic looks solid, but I want to flag the direction before this goes further.

When I raised scope concerns on #384/#392, I wasn't saying the background-update/history/rollback work doesn't belong in arch-updater, quite the opposite, we've been iterating on exactly that together and it's close to landing. What I meant was narrower: keep that PR focused on Arch instead of also growing pacman-specific code into a generic abstraction in the same diff.

Spinning up linux-updater as a separate plugin that re-implements the same background/history/rollback engine for pacman, dnf, apt, zypper, xbps, and PackageKit means we now have two plugins to keep in sync, two sets of bugs to fix twice, and users choosing between overlapping tools depending on their distro. That's not what I was asking for, and it undercuts the work we're doing on #392.

If the goal is genuine multi-distro support, I'd much rather we get #392 merged as the Arch-focused baseline, then talk about factoring the shared engine (update runner, history/rollback, ignore management) out into something arch-updater and other distro plugins can both build on, rather than maintaining a parallel fork. Can we hold off on pushing linux-updater forward until we've aligned on that, so we're not duplicating effort?

@UmedjonBA

Copy link
Copy Markdown
Contributor Author

@Reiling-Jeff
Hey, sorry — I didn't get what you meant at first. Sounds good, I'm happy to focus on arch-updater and hold off on linux-updater until we've aligned on factoring out the shared engine.

I really like the Noctalia project and I'm glad to be contributing here.

Are you on the project's Discord, and what's your handle there? GitHub comments aren't the most convenient way to talk — it'd be great to be able to ping you there if I have questions about the project.

@Reiling-Jeff

Copy link
Copy Markdown
Contributor

my discord tag is alreadyclaimed

@UmedjonBA

Copy link
Copy Markdown
Contributor Author

@Reiling-Jeff
Okay, then have a look at #392 whenever you get the time

@Reiling-Jeff

Copy link
Copy Markdown
Contributor

@UmedjonBA
i already did, and i already made a PR, if take a look! after the merge, i am fine with publishing it :D
UmedjonBA#2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants