commit 1354db6c39a5d907b1947a2fe7a2c412c299ae06 Author: Bryan Joshua Pedini Date: Sat Oct 10 10:37:17 2026 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24d8aeb --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# vendored bats-core / bats-support / bats-assert (fetched by `make vendor`) +tests/vendor/ +# per-distro E2E logs (tests/docker/run-e2e.sh) +tests/docker/logs/ +tests/docker/images-after*.tsv +*.tmp +.bats-tmp/ +# host-specific docker image baseline written by tests/docker/snapshot.sh +tests/docker/images-before.tsv +tests/docker/image-ids-before.txt diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..7850f77 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,5 @@ +# shellcheck configuration used by `make lint` (run from the repository root). +# Follow `source`d files (fakes -> tests/unit/fakes/fakelib.bash, ...), resolved +# relative to the sourcing script. +external-sources=true +source-path=SCRIPTDIR diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9efa6fb --- /dev/null +++ b/LICENSE @@ -0,0 +1,338 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Moe Ghoul, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..35117f1 --- /dev/null +++ b/README.md @@ -0,0 +1,384 @@ +# skeptical-updater + +A distro-agnostic, single-file bash applet for a *manual, conscious* update cycle. +It wraps the native package manager (apt, dnf/yum, pacman/pman, zypper), lists +upgradable packages, lets the user interrogate each one, and only then runs the +native upgrade. Packages the user has explicitly acknowledged are remembered in a +statefile and never questioned again. It is one file with no runtime dependencies +beyond bash >= 4.3 and the distro's package manager, and it never reimplements +package management: every action shells out to the native tool. + +The specification is [docs/DESIGN.md](docs/DESIGN.md); the per-family command +research it is built on lives in [discovery/](discovery/). + +## Install + +```sh +sudo make install # /usr/local/bin/skeptical-updater +sudo make install PREFIX=/usr # /usr/bin/skeptical-updater +make install DESTDIR=/tmp/pkg PREFIX=/usr # staged install, e.g. for packaging +sudo make uninstall +``` + +Or just copy the `skeptical-updater` file somewhere in `PATH` and make it executable. +The statefile directory (`/var/lib/skeptical-updater/`) is created by the applet +on first use, not by `make install`; `make uninstall` leaves it in place. + +Runtime needs: bash >= 4.3, the package manager, coreutils, sed and grep (plus awk, +which the apt and pacman backends use and which those distros always ship). `sudo` +is optional: when the tool needs root and you are not root, it re-runs itself +through `sudo` with the same arguments, or fails with a clear message. + +## Usage + +``` +skeptical-updater [options] [command] + +commands: + (none) interactive review + upgrade + list print the review list and exit (no prompt) + trusted print the statefile + trust PKG... add PKG and its installed dependency closure to the statefile + untrust PKG... remove rows for PKG (and rows whose reason is dep:PKG) + refresh recompute dependency closures for every 'user' root + info PKG one-line description + installed reverse dependencies (as N?) + show PKG full native package information (as N??) + +options: + --backend=NAME force apt|dnf|pacman|zypper (default: detect pacman > apt > dnf/yum > zypper) + --all also list trusted packages (marked with '=') + --no-refresh skip the metadata refresh step + --dry-run print native commands instead of running mutating ones + --state-dir=DIR override /var/lib/skeptical-updater (env: SKEPTICAL_UPDATER_STATE_DIR) + --no-color disable ANSI colours (also honours NO_COLOR and non-tty stdout) + -h, --help this help + -V, --version print the version +``` + +Exit status: `0` ok, `1` usage or backend error, `2` aborted (end of input), +otherwise the exit status of the package manager's upgrade step. + +What needs root: the metadata refresh, the upgrade, a purge, and writing a state +directory you cannot write. `--dry-run`, `list --no-refresh`, `trusted`, `info` and +`show` work unprivileged (as long as the state directory is readable). + +### Example session + +A Debian 12 box with three pending upgrades; `su-dummy-c` is on hold +(`apt-mark hold`) and `su-dummy-a` depends on `su-dummy-b`: + +``` +$ sudo skeptical-updater +Backend: apt (Debian GNU/Linux 12 (bookworm)) +Refreshing package lists... done. +3 upgradable, 0 trusted (hidden, use --all to show), 3 to review: + + [1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo + [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo + [3] su-dummy-c 1.0 -> 2.0 file:/srv/su-repo ./ [held] + + upgrade | N? what is it | N?? full info | N- exclude this run | Nx purge now | N+ trust forever +> 2? +su-dummy-b: dummy package su-dummy-b for skeptical-updater tests + required by (installed): su-dummy-a +> 1+ +trusted su-dummy-a (+1 dependency: su-dummy-b) +> 3- +excluded from this run: su-dummy-c +> l +3 upgradable, 2 trusted (hidden, use --all to show), 1 to review: + +-[3] su-dummy-c 1.0 -> 2.0 file:/srv/su-repo ./ [held] +> +Excluded from this run: su-dummy-c ++ apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 --with-new-pkgs upgrade +... +The following packages have been kept back: + su-dummy-c +The following packages will be upgraded: + su-dummy-a su-dummy-b +... +Upgrade finished. + +$ skeptical-updater trusted +# skeptical-updater trusted packages. Columns: backend package reason added(UTC ISO-8601) +apt su-dummy-a user 2026-10-09T19:24:13Z +apt su-dummy-b dep:su-dummy-a 2026-10-09T19:24:13Z +``` + +Next time `su-dummy-a` and `su-dummy-b` are upgraded silently; only new packages +are questioned. The prompt reads stdin, so the same session can be scripted: +`printf '3-\n\n' | sudo skeptical-updater` (an empty line is what triggers the +upgrade; end of input without one aborts with exit status 2). + +`[held]` marks packages the package manager will not upgrade on its own: apt +holds and "kept back" packages, pacman `IgnorePkg` (`[ignored]` in `pacman -Qu`). +dnf `exclude=`/versionlock and zypper locks hide packages from the native listing, +so they do not appear at all. + +### Command grammar + +One line, whitespace-separated tokens; `N` is the number in brackets. Numbers are +stable for the whole session (rows that leave the list leave a gap). Every token +of a line is validated first: a line with an unknown token or a number that is not +listed prints an error, runs nothing, and the prompt comes back. + +| token | action | +|-------|--------| +| empty line | proceed: run the native upgrade with the current exclusions | +| `N?` | one-line description and the installed packages that depend on it | +| `N??` | full native info (`apt-cache show`, `pacman -Qi` + `-Si`, `dnf info`, `zypper info --requires`) | +| `N-` | exclude from this run (toggle; `N-` again re-includes); shown with a `-` marker | +| `Nx` | purge now, with its now-unneeded dependencies: the purge is simulated first and the prompt lists everything it removes (`Purge su-dummy-a now? This also removes: su-dummy-b [y/N]`); the answer is read from the same stdin; the rows of every removed package leave the list. `x` refuses when other installed packages depend on the target, like `pacman -Rns`; purge the dependents first (`not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first`). Rows with old version `-` (new dependencies, not installed) cannot be purged | +| `N+` | trust forever: write it and its installed dependency closure to the statefile; the row leaves the list. Rows this hides are re-included if they were excluded (a trusted package is upgraded) | +| `*+` / `*-` | `+` / `-` for every listed package (`*-` is an all-or-nothing toggle: when all are excluded it re-includes them) | +| `l` | redraw the list | +| `q` | quit without upgrading (exit 0) | +| `h`, `?` | help | +| end of input | abort without upgrading (exit 2) | + +Several tokens may share a line (`1? 3- 4+`); they run left to right. With `--all` +trusted packages are listed too, marked with `=`. When purges leave nothing to upgrade the +session ends by itself (exit 0). + +Under the rows, `Note:` lines report pending work that is not a row: new dependencies or +replacements only the pacman transaction (`-Sup`) shows, packages dnf installs to obsolete +others, packages a Tumbleweed `dist-upgrade` removes. With no row but such work, the list +says "0 to review, but ... reports pending changes" and `` still runs the upgrade. + +### Dry run + +`--dry-run` prints every mutating command as `+ ` instead of running it: +the upgrade, purges, apt holds/unholds, zypper lock files, the installation of the reviewed +pacman databases and statefile writes. The +metadata refresh still runs when you are root (it is printed when you are not), and +read-only queries run normally, so the list is real: + +``` +$ skeptical-updater --dry-run <<<$'2-\n' +... +Excluded from this run: su-dummy-b ++ apt-mark hold su-dummy-b ++ apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 --with-new-pkgs upgrade ++ apt-mark unhold su-dummy-b +Dry run: nothing was changed. +``` + +## Statefile + +`/var/lib/skeptical-updater/trusted.tsv` (directory override: `--state-dir=DIR` or +`SKEPTICAL_UPDATER_STATE_DIR`). The directory is created `0755` on the first write. + +``` +# skeptical-updater trusted packages. Columns: backend package reason added(UTC ISO-8601) +apt nodejs user 2026-10-09T09:40:12Z +apt libnode115 dep:nodejs 2026-10-09T09:40:12Z +``` + +- Tab-separated, one record per line, sorted and unique on (backend, package); + lines starting with `#` are comments and are preserved (kept at the top). + Malformed lines are kept verbatim with a warning. +- `reason` is `user` for a package you confirmed (`N+` / `trust`), or + `dep:` for a package that was part of the installed dependency closure of + `` when you trusted it. A `user` row is never downgraded to `dep:`. +- A package is trusted when a row with the current backend and its name exists; + the reason does not matter. Rows of other backends are kept untouched. +- Writes are atomic: `trusted.tsv.tmp.` in the same directory, renamed over + the old file. They run under `trusted.lock` (`flock`, or an atomic `mkdir` lock with stale + detection where `flock` is missing): the file is re-read and this session's changes are + replayed onto it, so a `trust` in another terminal during a session is kept. +- A state directory you cannot enter, or a statefile you cannot read, is an error (never + silently "nothing trusted"). +- `untrust PKG` removes the rows for `PKG` and every `dep:PKG` row; `refresh` + recomputes the closure of every `user` root (adds new dependencies, drops rows + that are no longer needed, keeps the dates of the others). + +The other files are recovery aids that only exist while an upgrade runs (or after one was +killed with `kill -9` or a power loss): `holds.added`, the apt hold journal (used under +`holds.lock`), and `zypper-private`, the path of the private zypper configuration. At the start +of every root run, before anything is listed, a left-over journal is replayed (the holds it +recorded are released, a message says which) and a left-over private configuration is removed. + +## How exclusions work + +`N-` exclusions are per run and never persisted: you are asked again next time. +Permanent holds belong to the package manager (`apt-mark hold`, `IgnorePkg`, +`zypper addlock`, dnf `exclude=`), and skeptical-updater never changes them. + +| backend | refresh | upgrade | per-run exclusion | purge | +|---------|---------|---------|-------------------|-------| +| apt | `apt-get update -qq -o APT::Update::Error-Mode=any` | `apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 --with-new-pkgs upgrade` | temporary `apt-mark hold` of the excluded packages that are *not already held*, journalled in `holds.added` before holding; afterwards exactly those are released (also on failure, Ctrl-C, TERM, HUP, QUIT, a second signal during the restore, or a closed stdout; a journal left by `kill -9` is replayed at the start of the next root run). Holds you placed are never touched. `apt-mark` ignores `DPkg::Lock::Timeout`, so it is retried for up to 60 s while dpkg's lock is busy. | plan: `apt-get -s purge --auto-remove PKG` minus what `apt-get -s autoremove` already listed (`--auto-remove` is system-wide), then `apt-get -y purge` of exactly that set: PKG and the dependencies this purge orphans; no `autoremove` | +| pacman / pman | `pacman -Sy --dbpath ` (with `local` linked to the real local database, seeded with the system's `*.db` and `*.db.sig`): listing, `q`, end of input and `--dry-run` never touch `/var/lib/pacman/sync`; errors pacman prints about it are shown | check `pacman -Sup --noconfirm --ignore ... --dbpath ` (on failure: nothing changed, its exit status), install the reviewed `*.db`/`*.db.sig` into `/var/lib/pacman/sync` under `db.lck`, all-or-nothing, never over a newer system database (exit 2), then `pacman -Su --noconfirm` - exactly the reviewed metadata, no second sync; with `--no-refresh` plain `pacman -Su` | `--ignore a,b` | plan: `pacman -Rs --print`; then `pacman -Rns --noconfirm -- ...` | +| dnf / yum | `dnf -q --refresh --setopt=skip_if_unavailable=0 makecache` | `dnf -y --setopt=skip_if_unavailable=0 --setopt=*.metadata_expire=-1 upgrade` (the reviewed metadata; the repo glob is needed because a repo's own `metadata_expire` overrides the main one) | `--exclude=a,b` (adds to `exclude=` from the configuration) | plan: `dnf --assumeno remove PKG`; then `dnf -y remove ...` | +| zypper | `zypper --non-interactive --quiet refresh` | `zypper --non-interactive --no-refresh update` (Leap/SLE) or `dist-upgrade` (Tumbleweed/Slowroll, by `ID=` in os-release) | locks in a *private* `ZYPP_CONF`: the effective configuration (main zypp.conf plus the `zypp.conf.d` drop-ins of `/usr/etc` and `/etc`) with `locksfile.path` pointing at a copy of the effective lock file (also when a drop-in sets it), plus `addlock` of the exclusions; deleted afterwards: the real lock file is never written | plan: `zypper --xmlout remove --dry-run --clean-deps PKG`; then `zypper --non-interactive --no-refresh remove --clean-deps ...` | + +Every package-manager call runs with `LC_ALL=C` (and `DEBIAN_FRONTEND=noninteractive`; +`COLUMNS=0` for pacman), with stdin from `/dev/null` so that it can never eat the +commands you pipe in. The upgradable list comes from `apt-get -s --with-new-pkgs +upgrade` (Inst lines + the "kept back" block), `pacman -Qu` joined with `pacman -Sl`, +`dnf -q check-update` (+ `rpm -qa` for the installed versions) and +`zypper --xmlout list-updates`, or on Tumbleweed (as root) the real plan +`zypper --xmlout --no-refresh dist-upgrade --dry-run`. Reverse dependencies and dependency closures use +`apt-cache rdepends` / the dpkg database, `pacman -Qi`, and plain `rpm` queries on +dnf and zypper systems (see the discovery reports for why). + +## Testing + +Everything runs in Docker; the host only needs GNU make, bash, Docker and (once, +for `make vendor`) curl. Run `make help` for the full list of targets. + +| target | what it does | +|--------|--------------| +| `make vendor` | download the pinned bats-core, bats-support and bats-assert into `tests/vendor/` (once; no network afterwards) | +| `make lint` | shellcheck the applet, the test scripts, fixtures and fakes (`koalaman/shellcheck` image; `discovery/` is reference material and is not linted) | +| `make test-unit` | `tests/unit/*.bats` in a `debian:12` container, against fake package managers | +| `make test-e2e` | `tests/e2e/` inside every distro listed in `tests/docker/images.txt`, against the real package managers; logs go to `tests/docker/logs/