Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package topgrade for openSUSE:Factory 
checked in at 2025-12-18 18:31:06
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/topgrade (Old)
 and      /work/SRC/openSUSE:Factory/.topgrade.new.1928 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "topgrade"

Thu Dec 18 18:31:06 2025 rev:17 rq:1323326 version:16.7.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/topgrade/topgrade.changes        2025-12-08 
11:56:31.375944310 +0100
+++ /work/SRC/openSUSE:Factory/.topgrade.new.1928/topgrade.changes      
2025-12-18 18:31:59.230962086 +0100
@@ -1,0 +2,22 @@
+Wed Dec 17 10:44:06 UTC 2025 - [email protected]
+
+- Update to version 16.7.0:
+  * chore: release v16.7.0 (#1610)
+  * fix(brew): fix brew casks and incomplete formula update on linux when 
using isolated user (#1611)
+  * feat(containers): add `use_sudo` option (#1618)
+  * chore(deps): lock file maintenance (#1615)
+  * ci(lint_pr): zizmor fixes (#1614)
+  * chore(renovate): move Renovate config (#1613)
+  * chore(deps): update github/codeql-action action to v4.31.8 (#1607)
+  * chore(deps): update github artifact actions (major) (#1609)
+  * fix(vscode): fix parsing of different version format (#1608)
+  * chore(deps): update rust crate shell-words to v1.1.1 (#1604)
+  * chore(deps): update itertools to 0.14.0 (#1601)
+  * feat(sudo): propagate --env to sudo commands (#1588) (#1589)
+  * chore(pre-commit): autoupdate (#1580)
+  * chore(deps): lock file maintenance (#1597)
+  * feat(aqua): run `aqua update --config $AQUA_GLOBAL_CONFIG` instead of 
`aqua update` (#1596)
+  * chore(deps): update github/codeql-action action to v4.31.7 (#1591)
+  * chore(deps): update release-plz/action digest to 487eb7b (#1590)
+
+-------------------------------------------------------------------

Old:
----
  topgrade-16.6.1.tar.zst

New:
----
  topgrade-16.7.0.tar.zst

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ topgrade.spec ++++++
--- /var/tmp/diff_new_pack.8xVJxP/_old  2025-12-18 18:32:14.387598650 +0100
+++ /var/tmp/diff_new_pack.8xVJxP/_new  2025-12-18 18:32:14.391598818 +0100
@@ -17,7 +17,7 @@
 
 
 Name:           topgrade
-Version:        16.6.1
+Version:        16.7.0
 Release:        0
 Summary:        Upgrade all the things
 License:        GPL-3.0-only

++++++ _service ++++++
--- /var/tmp/diff_new_pack.8xVJxP/_old  2025-12-18 18:32:14.439600834 +0100
+++ /var/tmp/diff_new_pack.8xVJxP/_new  2025-12-18 18:32:14.443601001 +0100
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/topgrade-rs/topgrade.git</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="scm">git</param>
-    <param name="revision">v16.6.1</param>
+    <param name="revision">v16.7.0</param>
     <param name="match-tag">*</param>
     <param name="versionrewrite-pattern">v(\d+\.\d+\.\d+)</param>
     <param name="versionrewrite-replacement">\1</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.8xVJxP/_old  2025-12-18 18:32:14.471602178 +0100
+++ /var/tmp/diff_new_pack.8xVJxP/_new  2025-12-18 18:32:14.483602682 +0100
@@ -1,7 +1,7 @@
 <servicedata>
 <service name="tar_scm">
                 <param 
name="url">https://github.com/topgrade-rs/topgrade.git</param>
-              <param 
name="changesrevision">d9d279a35f0de3bef60081e2f3883f64e3e64b3b</param></service><service
 name="tar_scm">
+              <param 
name="changesrevision">db36ce79df4a693d4e7bf666c38220fb8d141d4f</param></service><service
 name="tar_scm">
                 <param 
name="url">https://ghproxy.net/https://github.com/topgrade-rs/topgrade.git</param>
               <param 
name="changesrevision">ef0a0d69bbe0cb08d6c4930ee18b734e03c215fb</param></service></servicedata>
 (No newline at EOF)

++++++ topgrade-16.6.1.tar.zst -> topgrade-16.7.0.tar.zst ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/topgrade-16.6.1/.github/renovate.json5 
new/topgrade-16.7.0/.github/renovate.json5
--- old/topgrade-16.6.1/.github/renovate.json5  1970-01-01 01:00:00.000000000 
+0100
+++ new/topgrade-16.7.0/.github/renovate.json5  2025-12-17 11:07:46.000000000 
+0100
@@ -0,0 +1,10 @@
+{
+    $schema: "https://docs.renovatebot.com/renovate-schema.json";,
+    "extends": [
+        "config:best-practices",
+        ":semanticCommits",
+    ],
+    lockFileMaintenance: {
+        enabled: true,
+    },
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/topgrade-16.6.1/.github/workflows/check_security_vulnerability.yml 
new/topgrade-16.7.0/.github/workflows/check_security_vulnerability.yml
--- old/topgrade-16.6.1/.github/workflows/check_security_vulnerability.yml      
2025-12-06 20:45:42.000000000 +0100
+++ new/topgrade-16.7.0/.github/workflows/check_security_vulnerability.yml      
2025-12-17 11:07:46.000000000 +0100
@@ -32,6 +32,6 @@
         uses: 
microsoft/DevSkim-Action@4b5047945a44163b94642a1cecc0d93a3f428cc6 # v1.0.16
 
       - name: Upload DevSkim scan results to GitHub Security tab
-        uses: 
github/codeql-action/upload-sarif@fe4161a26a8629af62121b670040955b330f9af2 # 
v4.31.6
+        uses: 
github/codeql-action/upload-sarif@1b168cd39490f61582a9beae412bb7057a6b2c4e # 
v4.31.8
         with:
           sarif_file: devskim-results.sarif
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/topgrade-16.6.1/.github/workflows/lint_pr.yml 
new/topgrade-16.7.0/.github/workflows/lint_pr.yml
--- old/topgrade-16.6.1/.github/workflows/lint_pr.yml   2025-12-06 
20:45:42.000000000 +0100
+++ new/topgrade-16.7.0/.github/workflows/lint_pr.yml   2025-12-17 
11:07:46.000000000 +0100
@@ -8,12 +8,18 @@
       - reopened
       - synchronize
 
+permissions: { }
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || 
github.ref }}
+  cancel-in-progress: true
+
 jobs:
   main:
     name: Validate PR title
     runs-on: ubuntu-latest
     permissions:
-      pull-requests: read
+      pull-requests: read # amannn/action-semantic-pull-request
     steps:
       - uses: 
amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # 
v6.1.1
         env:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/topgrade-16.6.1/.github/workflows/release-plz.yml 
new/topgrade-16.7.0/.github/workflows/release-plz.yml
--- old/topgrade-16.6.1/.github/workflows/release-plz.yml       2025-12-06 
20:45:42.000000000 +0100
+++ new/topgrade-16.7.0/.github/workflows/release-plz.yml       2025-12-17 
11:07:46.000000000 +0100
@@ -25,7 +25,7 @@
         uses: dtolnay/rust-toolchain@stable
       - name: Run release-plz
         id: release-plz
-        uses: release-plz/action@1efcf74dfcd6e500990dad806e286899ae384064 # 
v0.5
+        uses: release-plz/action@487eb7b5c085a664d5c5ca05f4159bd9b591182a # 
v0.5
         with:
           command: release
         env:
@@ -60,7 +60,7 @@
       - name: Install Rust toolchain
         uses: dtolnay/rust-toolchain@stable
       - name: Run release-plz
-        uses: release-plz/action@1efcf74dfcd6e500990dad806e286899ae384064 # 
v0.5
+        uses: release-plz/action@487eb7b5c085a664d5c5ca05f4159bd9b591182a # 
v0.5
         with:
           command: release-pr
         env:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/topgrade-16.6.1/.github/workflows/release_to_pypi.yml 
new/topgrade-16.7.0/.github/workflows/release_to_pypi.yml
--- old/topgrade-16.6.1/.github/workflows/release_to_pypi.yml   2025-12-06 
20:45:42.000000000 +0100
+++ new/topgrade-16.7.0/.github/workflows/release_to_pypi.yml   2025-12-17 
11:07:46.000000000 +0100
@@ -26,7 +26,7 @@
           args: --release --out dist
           manylinux: auto
       - name: Upload wheels
-        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 
# v5.0.0
+        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f 
# v6.0.0
         with:
           name: wheels-linux-${{ matrix.target }}
           path: dist
@@ -47,7 +47,7 @@
           target: ${{ matrix.target }}
           args: --release --out dist
       - name: Upload wheels
-        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 
# v5.0.0
+        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f 
# v6.0.0
         with:
           name: wheels-windows-${{ matrix.target }}
           path: dist
@@ -68,7 +68,7 @@
           target: ${{ matrix.target }}
           args: --release --out dist
       - name: Upload wheels
-        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 
# v5.0.0
+        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f 
# v6.0.0
         with:
           name: wheels-macos-${{ matrix.target }}
           path: dist
@@ -86,7 +86,7 @@
           command: sdist
           args: --out dist
       - name: Upload sdist
-        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 
# v5.0.0
+        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f 
# v6.0.0
         with:
           name: wheels-sdist
           path: dist
@@ -105,7 +105,7 @@
       # Used to generate artifact attestation
       attestations: write
     steps:
-      - uses: 
actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
+      - uses: 
actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
 
       - name: Generate artifact attestation
         uses: 
actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # 
v3.0.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/topgrade-16.6.1/.github/workflows/scorecards.yml 
new/topgrade-16.7.0/.github/workflows/scorecards.yml
--- old/topgrade-16.6.1/.github/workflows/scorecards.yml        2025-12-06 
20:45:42.000000000 +0100
+++ new/topgrade-16.7.0/.github/workflows/scorecards.yml        2025-12-17 
11:07:46.000000000 +0100
@@ -63,7 +63,7 @@
       # Upload the results as artifacts (optional). Commenting out will 
disable uploads of run results in SARIF
       # format to the repository Actions tab.
       - name: "Upload artifact"
-        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 
# v5.0.0
+        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f 
# v6.0.0
         with:
           name: SARIF file
           path: results.sarif
@@ -71,6 +71,6 @@
 
       # Upload the results to GitHub's code scanning dashboard.
       - name: "Upload to code-scanning"
-        uses: 
github/codeql-action/upload-sarif@fe4161a26a8629af62121b670040955b330f9af2 # 
v4.31.6
+        uses: 
github/codeql-action/upload-sarif@1b168cd39490f61582a9beae412bb7057a6b2c4e # 
v4.31.8
         with:
           sarif_file: results.sarif
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/topgrade-16.6.1/.pre-commit-config.yaml 
new/topgrade-16.7.0/.pre-commit-config.yaml
--- old/topgrade-16.6.1/.pre-commit-config.yaml 2025-12-06 20:45:42.000000000 
+0100
+++ new/topgrade-16.7.0/.pre-commit-config.yaml 2025-12-17 11:07:46.000000000 
+0100
@@ -1,6 +1,6 @@
 repos:
 - repo: https://github.com/gitleaks/gitleaks
-  rev: v8.29.1
+  rev: v8.30.0
   hooks:
   - id: gitleaks
 
@@ -16,7 +16,7 @@
   - id: trailing-whitespace
 
 - repo: https://github.com/crate-ci/typos
-  rev: v1.39.2
+  rev: v1.40.0
   hooks:
   - id: typos
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/topgrade-16.6.1/CHANGELOG.md 
new/topgrade-16.7.0/CHANGELOG.md
--- old/topgrade-16.6.1/CHANGELOG.md    2025-12-06 20:45:42.000000000 +0100
+++ new/topgrade-16.7.0/CHANGELOG.md    2025-12-17 11:07:46.000000000 +0100
@@ -7,6 +7,33 @@
 
 ## [Unreleased]
 
+## [16.7.0](https://github.com/topgrade-rs/topgrade/compare/v16.6.1...v16.7.0) 
- 2025-12-17
+
+### Added
+
+- *(containers)* add `use_sudo` option 
([#1618](https://github.com/topgrade-rs/topgrade/pull/1618))
+- *(sudo)* propagate --env to sudo commands 
([#1588](https://github.com/topgrade-rs/topgrade/pull/1588)) 
([#1589](https://github.com/topgrade-rs/topgrade/pull/1589))
+- *(aqua)* run `aqua update --config $AQUA_GLOBAL_CONFIG` instead of `aqua 
update` ([#1596](https://github.com/topgrade-rs/topgrade/pull/1596))
+
+### Fixed
+
+- *(brew)* fix brew casks and incomplete formula update on linux when using 
isolated user ([#1611](https://github.com/topgrade-rs/topgrade/pull/1611))
+- *(vscode)* fix parsing of different version format 
([#1608](https://github.com/topgrade-rs/topgrade/pull/1608))
+
+### Other
+
+- *(deps)* lock file maintenance 
([#1615](https://github.com/topgrade-rs/topgrade/pull/1615))
+- *(lint_pr)* zizmor fixes 
([#1614](https://github.com/topgrade-rs/topgrade/pull/1614))
+- *(renovate)* move Renovate config 
([#1613](https://github.com/topgrade-rs/topgrade/pull/1613))
+- *(deps)* update github/codeql-action action to v4.31.8 
([#1607](https://github.com/topgrade-rs/topgrade/pull/1607))
+- *(deps)* update github artifact actions (major) 
([#1609](https://github.com/topgrade-rs/topgrade/pull/1609))
+- *(deps)* update rust crate shell-words to v1.1.1 
([#1604](https://github.com/topgrade-rs/topgrade/pull/1604))
+- *(deps)* update itertools to 0.14.0 
([#1601](https://github.com/topgrade-rs/topgrade/pull/1601))
+- *(pre-commit)* autoupdate 
([#1580](https://github.com/topgrade-rs/topgrade/pull/1580))
+- *(deps)* lock file maintenance 
([#1597](https://github.com/topgrade-rs/topgrade/pull/1597))
+- *(deps)* update github/codeql-action action to v4.31.7 
([#1591](https://github.com/topgrade-rs/topgrade/pull/1591))
+- *(deps)* update release-plz/action digest to 487eb7b 
([#1590](https://github.com/topgrade-rs/topgrade/pull/1590))
+
 ## [16.6.1](https://github.com/topgrade-rs/topgrade/compare/v16.6.0...v16.6.1) 
- 2025-12-06
 
 ### Fixed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/topgrade-16.6.1/Cargo.lock 
new/topgrade-16.7.0/Cargo.lock
--- old/topgrade-16.6.1/Cargo.lock      2025-12-06 20:45:42.000000000 +0100
+++ new/topgrade-16.7.0/Cargo.lock      2025-12-17 11:07:46.000000000 +0100
@@ -350,9 +350,9 @@
 
 [[package]]
 name = "cc"
-version = "1.2.48"
+version = "1.2.49"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a"
+checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
 dependencies = [
  "find-msvc-tools",
  "shlex",
@@ -1239,9 +1239,9 @@
 
 [[package]]
 name = "hyper-util"
-version = "0.1.18"
+version = "0.1.19"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
+checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
 dependencies = [
  "base64",
  "bytes",
@@ -1333,9 +1333,9 @@
 
 [[package]]
 name = "icu_properties"
-version = "2.1.1"
+version = "2.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99"
+checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
 dependencies = [
  "icu_collections",
  "icu_locale_core",
@@ -1347,9 +1347,9 @@
 
 [[package]]
 name = "icu_properties_data"
-version = "2.1.1"
+version = "2.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899"
+checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
 
 [[package]]
 name = "icu_provider"
@@ -1475,6 +1475,15 @@
 ]
 
 [[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
 name = "itoa"
 version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index";
@@ -1511,9 +1520,9 @@
 
 [[package]]
 name = "libc"
-version = "0.2.177"
+version = "0.2.178"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
+checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
 
 [[package]]
 name = "libredox"
@@ -1540,9 +1549,9 @@
 
 [[package]]
 name = "log"
-version = "0.4.28"
+version = "0.4.29"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
 
 [[package]]
 name = "lru-slab"
@@ -1620,9 +1629,9 @@
 
 [[package]]
 name = "mio"
-version = "1.1.0"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
+checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
 dependencies = [
  "libc",
  "wasi",
@@ -1914,7 +1923,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
 dependencies = [
- "toml_edit 0.23.7",
+ "toml_edit 0.23.9",
 ]
 
 [[package]]
@@ -2127,9 +2136,9 @@
 
 [[package]]
 name = "reqwest"
-version = "0.12.24"
+version = "0.12.25"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
+checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a"
 dependencies = [
  "base64",
  "bytes",
@@ -2226,7 +2235,7 @@
  "arc-swap",
  "base62",
  "globwalk",
- "itertools",
+ "itertools 0.11.0",
  "lazy_static",
  "normpath",
  "once_cell",
@@ -2500,9 +2509,9 @@
 
 [[package]]
 name = "shell-words"
-version = "1.1.0"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
+checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
 
 [[package]]
 name = "shellexpand"
@@ -2540,9 +2549,9 @@
 
 [[package]]
 name = "simd-adler32"
-version = "0.3.7"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
 
 [[package]]
 name = "siphasher"
@@ -2941,9 +2950,9 @@
 
 [[package]]
 name = "toml_edit"
-version = "0.23.7"
+version = "0.23.9"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d"
+checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832"
 dependencies = [
  "indexmap",
  "toml_datetime 0.7.3",
@@ -2974,7 +2983,7 @@
 
 [[package]]
 name = "topgrade"
-version = "16.6.1"
+version = "16.7.0"
 dependencies = [
  "base64ct",
  "chrono",
@@ -2992,6 +3001,7 @@
  "ignore",
  "indexmap",
  "is_elevated",
+ "itertools 0.14.0",
  "jetbrains-toolbox-updater",
  "merge",
  "nix",
@@ -3039,9 +3049,9 @@
 
 [[package]]
 name = "tower-http"
-version = "0.6.7"
+version = "0.6.8"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456"
+checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
 dependencies = [
  "bitflags 2.10.0",
  "bytes",
@@ -3236,12 +3246,12 @@
 
 [[package]]
 name = "uuid"
-version = "1.18.1"
+version = "1.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
+checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
 dependencies = [
  "js-sys",
- "serde",
+ "serde_core",
  "wasm-bindgen",
 ]
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/topgrade-16.6.1/Cargo.toml 
new/topgrade-16.7.0/Cargo.toml
--- old/topgrade-16.6.1/Cargo.toml      2025-12-06 20:45:42.000000000 +0100
+++ new/topgrade-16.7.0/Cargo.toml      2025-12-17 11:07:46.000000000 +0100
@@ -6,7 +6,7 @@
 license = "GPL-3.0-or-later"
 repository = "https://github.com/topgrade-rs/topgrade";
 rust-version = "1.84.1"
-version = "16.6.1"
+version = "16.7.0"
 authors = ["Roey Darwish Dror <[email protected]>", "Thomas Schönauer 
<[email protected]>"]
 exclude = ["doc/screenshot.gif", "BREAKINGCHANGES_dev.md"]
 edition = "2021"
@@ -47,6 +47,7 @@
 jetbrains-toolbox-updater = "5.0.0"
 indexmap = { version = "2.9.0", features = ["serde"] }
 serde_json = "1.0.145"
+itertools = "0.14.0"
 # Temporary transitive dependency pins
 ignore = "=0.4.23"
 globset = "=0.4.16"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/topgrade-16.6.1/RELEASE_PROCEDURE.md 
new/topgrade-16.7.0/RELEASE_PROCEDURE.md
--- old/topgrade-16.6.1/RELEASE_PROCEDURE.md    2025-12-06 20:45:42.000000000 
+0100
+++ new/topgrade-16.7.0/RELEASE_PROCEDURE.md    2025-12-17 11:07:46.000000000 
+0100
@@ -4,7 +4,7 @@
 
       > If there are breaking changes, the major version number should be 
increased.
 
-   2. If the major versioin number gets bumped, update 
[SECURITY.md][SECURITY_file_link].
+   2. If the major version number gets bumped, update 
[SECURITY.md][SECURITY_file_link].
 
       [SECURITY_file_link]: 
https://github.com/topgrade-rs/topgrade/blob/main/SECURITY.md
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/topgrade-16.6.1/config.example.toml 
new/topgrade-16.7.0/config.example.toml
--- old/topgrade-16.6.1/config.example.toml     2025-12-06 20:45:42.000000000 
+0100
+++ new/topgrade-16.7.0/config.example.toml     2025-12-17 11:07:46.000000000 
+0100
@@ -362,6 +362,10 @@
 # (default: false)
 # system_prune = false
 
+# Use sudo for updating containers. Necessary for non-rootless installs.
+# (default: false)
+# use_sudo = false
+
 [lensfun]
 # If disabled, Topgrade invokes `lensfun‑update‑data` without root privilege,
 # then the update will be only available to you. Otherwise, `sudo` is required,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/topgrade-16.6.1/renovate.json 
new/topgrade-16.7.0/renovate.json
--- old/topgrade-16.6.1/renovate.json   2025-12-06 20:45:42.000000000 +0100
+++ new/topgrade-16.7.0/renovate.json   1970-01-01 01:00:00.000000000 +0100
@@ -1,10 +0,0 @@
-{
-    "$schema": "https://docs.renovatebot.com/renovate-schema.json";,
-    "extends": [
-        "config:best-practices",
-        ":semanticCommits"
-    ],
-    "lockFileMaintenance": {
-        "enabled": true
-    }
-}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/topgrade-16.6.1/src/config.rs 
new/topgrade-16.7.0/src/config.rs
--- old/topgrade-16.6.1/src/config.rs   2025-12-06 20:45:42.000000000 +0100
+++ new/topgrade-16.7.0/src/config.rs   2025-12-17 11:07:46.000000000 +0100
@@ -8,8 +8,8 @@
 
 use clap::{Parser, ValueEnum};
 use clap_complete::Shell;
-use color_eyre::eyre::Context;
 use color_eyre::eyre::Result;
+use color_eyre::eyre::{Context, OptionExt};
 use etcetera::base_strategy::BaseStrategy;
 use indexmap::IndexMap;
 use merge::Merge;
@@ -61,6 +61,7 @@
     ignored_containers: Option<Vec<String>>,
     runtime: Option<ContainerRuntime>,
     system_prune: Option<bool>,
+    use_sudo: Option<bool>,
 }
 
 #[derive(Deserialize, Default, Debug, Merge)]
@@ -780,8 +781,8 @@
     custom_commands: Vec<String>,
 
     /// Set environment variables
-    #[arg(long = "env", value_name = "NAME=VALUE", num_args = 1..)]
-    env: Vec<String>,
+    #[arg(long = "env", value_name = "NAME=VALUE", value_parser = 
env_args_parser, num_args = 1..)]
+    env: Vec<(String, String)>,
 
     /// Output debug logs. Alias for `--log-filter debug`.
     #[arg(short = 'v', long = "verbose")]
@@ -844,6 +845,13 @@
     pub no_self_update: bool,
 }
 
+fn env_args_parser(arg: &str) -> Result<(String, String)> {
+    let (key, value) = arg
+        .split_once("=")
+        .ok_or_eyre("Environment variable must be in the format NAME=VALUE")?;
+    Ok((key.to_string(), value.to_string()))
+}
+
 impl CommandLineArgs {
     pub fn edit_config(&self) -> bool {
         self.edit_config
@@ -853,7 +861,7 @@
         self.show_config_reference
     }
 
-    pub fn env_variables(&self) -> &Vec<String> {
+    pub fn env_variables(&self) -> &Vec<(String, String)> {
         &self.env
     }
 
@@ -985,6 +993,15 @@
             .unwrap_or(false)
     }
 
+    /// Whether to use sudo for container operations.
+    pub fn containers_use_sudo(&self) -> bool {
+        self.config_file
+            .containers
+            .as_ref()
+            .and_then(|containers| containers.use_sudo)
+            .unwrap_or(false)
+    }
+
     /// Tell whether the specified step should run.
     ///
     /// If the step appears either in the `--disable` command line argument
@@ -1088,6 +1105,11 @@
                 .unwrap_or(false)
     }
 
+    /// List of user-defined environment variables
+    pub fn env_variables(&self) -> &Vec<(String, String)> {
+        self.opt.env_variables()
+    }
+
     /// List of remote hosts to run Topgrade in
     pub fn remote_topgrades(&self) -> Option<&Vec<String>> {
         self.config_file
@@ -1955,4 +1977,14 @@
 
         assert_eq!(order, vec!["z", "y", "x"]);
     }
+
+    #[test]
+    fn test_env_variable_parser() {
+        let mut config = config();
+        config.opt = CommandLineArgs::parse_from(["topgrade", "--env", 
"VAR1=foo", "--env", "VAR2=bar"]);
+        let env_vars = config.env_variables();
+        assert_eq!(env_vars.len(), 2);
+        assert_eq!(env_vars[0], ("VAR1".to_string(), "foo".to_string()));
+        assert_eq!(env_vars[1], ("VAR2".to_string(), "bar".to_string()));
+    }
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/topgrade-16.6.1/src/main.rs 
new/topgrade-16.7.0/src/main.rs
--- old/topgrade-16.6.1/src/main.rs     2025-12-06 20:45:42.000000000 +0100
+++ new/topgrade-16.7.0/src/main.rs     2025-12-17 11:07:46.000000000 +0100
@@ -94,11 +94,8 @@
         return Ok(());
     }
 
-    for env in opt.env_variables() {
-        let mut parts = env.split('=');
-        let var = parts.next().unwrap();
-        let value = parts.next().unwrap();
-        env::set_var(var, value);
+    for (key, value) in opt.env_variables() {
+        env::set_var(key, value);
     }
 
     if opt.edit_config() {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/topgrade-16.6.1/src/steps/containers.rs 
new/topgrade-16.7.0/src/steps/containers.rs
--- old/topgrade-16.6.1/src/steps/containers.rs 2025-12-06 20:45:42.000000000 
+0100
+++ new/topgrade-16.7.0/src/steps/containers.rs 2025-12-17 11:07:46.000000000 
+0100
@@ -2,7 +2,6 @@
 use std::io;
 use std::io::Write;
 use std::path::Path;
-use std::process::Command;
 
 use color_eyre::eyre::Context;
 use color_eyre::eyre::Result;
@@ -65,8 +64,8 @@
 /// "REGISTRY/[PATH/]CONTAINER_NAME:TAG"
 ///
 /// Containers specified in `ignored_containers` will be filtered out.
-fn list_containers(crt: &Path, ignored_containers: Option<&Vec<String>>) -> 
Result<Vec<Container>> {
-    let ignored_containers = ignored_containers.map(|patterns| {
+fn list_containers(ctx: &ExecutionContext, crt: &Path) -> 
Result<Vec<Container>> {
+    let ignored_containers = 
ctx.config().containers_ignored_tags().map(|patterns| {
         patterns
             .iter()
             .map(|pattern| WildMatch::new(pattern))
@@ -77,9 +76,23 @@
         "Querying '{} image ls --format 
\"{{{{.Repository}}}}:{{{{.Tag}}}}/{{{{.ID}}}}\"' for containers",
         crt.display()
     );
-    let output = Command::new(crt)
-        .args(["image", "ls", "--format", "{{.Repository}}:{{.Tag}} {{.ID}}"])
-        .output_checked_utf8()?;
+
+    let sudo = if ctx.config().containers_use_sudo() {
+        Some(ctx.require_sudo()?)
+    } else {
+        None
+    };
+
+    // TODO: This should run even when dry-running. Blocked by #1227.
+    let output = if let Some(sudo) = sudo {
+        sudo.execute(ctx, crt)?
+            .args(["image", "ls", "--format", "{{.Repository}}:{{.Tag}} 
{{.ID}}"])
+            .output_checked_utf8()?
+    } else {
+        ctx.execute(crt)
+            .args(["image", "ls", "--format", "{{.Repository}}:{{.Tag}} 
{{.ID}}"])
+            .output_checked_utf8()?
+    };
 
     let mut retval = vec![];
     for line in output.stdout.lines() {
@@ -124,9 +137,16 @@
             crt.display(),
             image_id
         );
-        let inspect_output = Command::new(crt)
-            .args(["image", "inspect", image_id, "--format", 
"{{.Os}}/{{.Architecture}}"])
-            .output_checked_utf8()?;
+        // TODO: This should run even when dry-running. Blocked by #1227.
+        let inspect_output = if let Some(sudo) = sudo {
+            sudo.execute(ctx, crt)?
+                .args(["image", "inspect", image_id, "--format", 
"{{.Os}}/{{.Architecture}}"])
+                .output_checked_utf8()?
+        } else {
+            ctx.execute(crt)
+                .args(["image", "inspect", image_id, "--format", 
"{{.Os}}/{{.Architecture}}"])
+                .output_checked_utf8()?
+        };
         let mut platform = inspect_output.stdout;
         // truncate the tailing new line character
         platform.truncate(platform.len() - 1);
@@ -146,12 +166,21 @@
 pub fn run_containers(ctx: &ExecutionContext) -> Result<()> {
     // Check what runtime is specified in the config
     let container_runtime = ctx.config().containers_runtime().to_string();
+    let use_sudo = ctx.config().containers_use_sudo();
     let crt = require(container_runtime)?;
     debug!("Using container runtime '{}'", crt.display());
 
     print_separator(t!("Containers"));
 
-    let output = Command::new(&crt).arg("--help").output_checked_with(|_| 
Ok(()))?;
+    let sudo = if use_sudo { Some(ctx.require_sudo()?) } else { None };
+
+    // TODO: This should run even when dry-running. Blocked by #1227.
+    let output = if let Some(sudo) = sudo {
+        sudo.execute(ctx, &crt)?.arg("--help").output_checked_with(|_| Ok(()))?
+    } else {
+        ctx.execute(&crt).arg("--help").output_checked_with(|_| Ok(()))?
+    };
+
     let status_code = output
         .status
         .code()
@@ -185,8 +214,7 @@
         ));
     }
 
-    let containers =
-        list_containers(&crt, 
ctx.config().containers_ignored_tags()).context("Failed to list Docker 
containers")?;
+    let containers = list_containers(ctx, &crt).context("Failed to list Docker 
containers")?;
     debug!("Containers to inspect: {:?}", containers);
 
     for container in &containers {
@@ -197,7 +225,11 @@
             args.push(container.platform.as_str());
         }
 
-        let mut exec = ctx.execute(&crt);
+        let mut exec = if let Some(sudo) = sudo {
+            sudo.execute(ctx, &crt)?
+        } else {
+            ctx.execute(&crt)
+        };
 
         if let Err(e) = exec.args(&args).status_checked() {
             error!("Pulling container '{}' failed: {}", container, e);
@@ -225,14 +257,26 @@
 
     if ctx.config().containers_system_prune() {
         // Run system prune to clean up unused containers, networks, and build 
cache
-        ctx.execute(&crt)
-            .args(["system", "prune", "--force"])
-            .status_checked()?
+        if let Some(sudo) = sudo {
+            sudo.execute(ctx, &crt)?
+                .args(["system", "prune", "--force"])
+                .status_checked()?
+        } else {
+            ctx.execute(&crt)
+                .args(["system", "prune", "--force"])
+                .status_checked()?
+        }
     // Only run `image prune` if we don't run `system prune`
     } else if ctx.config().cleanup() {
         // Remove dangling images
         debug!("Removing dangling images");
-        ctx.execute(&crt).args(["image", "prune", "-f"]).status_checked()?
+        if let Some(sudo) = sudo {
+            sudo.execute(ctx, &crt)?
+                .args(["image", "prune", "-f"])
+                .status_checked()?
+        } else {
+            ctx.execute(&crt).args(["image", "prune", "-f"]).status_checked()?
+        }
     }
 
     Ok(())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/topgrade-16.6.1/src/steps/generic.rs 
new/topgrade-16.7.0/src/steps/generic.rs
--- old/topgrade-16.6.1/src/steps/generic.rs    2025-12-06 20:45:42.000000000 
+0100
+++ new/topgrade-16.7.0/src/steps/generic.rs    2025-12-17 11:07:46.000000000 
+0100
@@ -249,14 +249,17 @@
     let aqua = Aqua::get(ctx)?.aqua_cli()?;
 
     print_separator("Aqua");
-    if ctx.run_type().dry() {
-        println!("{}", t!("Updating aqua ..."));
-        println!("{}", t!("Updating aqua installed cli tools ..."));
-        Ok(())
-    } else {
-        ctx.execute(&aqua).arg("update-aqua").status_checked()?;
-        ctx.execute(&aqua).arg("update").status_checked()
+
+    ctx.execute(&aqua).arg("update-aqua").status_checked()?;
+
+    // Don't run plain `aqua update` because it uses the current directory by 
default.
+    if let Ok(path) = env::var("AQUA_GLOBAL_CONFIG") {
+        ctx.execute(&aqua)
+            .args(["update", "--config", &path])
+            .status_checked()?;
     }
+
+    Ok(())
 }
 
 pub fn run_rustup(ctx: &ExecutionContext) -> Result<()> {
@@ -516,41 +519,54 @@
     let bin = require(bin_name)?;
 
     // VSCode has update command only since 1.86 version ("january 2024" 
update), disable the update for prior versions
-    // Use command `code --version` which returns 3 lines: version, git 
commit, instruction set. We parse only the first one
     //
+    // The output of `code --version` has two possible formats:
+    // 1. 3 lines: version, git commit, instruction set. We parse only the 
first one
+    //    This is confirmed on an install from the apt repository
+    // 2. 1 line: 'bin-name 1.2.3 (commit 123abc)', example: `code-insiders 
1.106.0 (commit 48cdf17f0e856e1daca2ad2747814085a2453df0)`
+    //    See https://github.com/topgrade-rs/topgrade/issues/1605, confirmed 
from Microsoft website
     // This should apply to VSCodium as well.
-    let version: Result<Version> = match Command::new(&bin)
-        .arg("--version")
-        .output_checked_utf8()?
-        .stdout
-        .lines()
-        .next()
-    {
-        Some(item) => {
-            // Insiders versions have "-insider" suffix which we can simply 
ignore.
-            let item = item.trim_end_matches("-insider");
-            // Strip leading zeroes because `semver` does not allow them, but 
VSCodium uses them sometimes.
-            //  This is not the case for VSCode, but just in case, and it 
can't really cause any issues.
-            let item = item
-                .split('.')
-                .map(|s| if s == "0" { "0" } else { s.trim_start_matches('0') 
})
-                .collect::<Vec<_>>()
-                .join(".");
-            Version::parse(&item).map_err(std::convert::Into::into)
-        }
-        None => {
-            return Err(eyre!(output_changed_message!(
+
+    let version_output = 
Command::new(&bin).arg("--version").output_checked_utf8()?;
+
+    debug!(version_output.stdout);
+
+    let line = version_output.stdout.lines().next().ok_or_else(|| {
+        eyre!(output_changed_message!(
+            &format!("{bin_name} --version"),
+            "No first line"
+        ))
+    })?;
+
+    let version_string = if line.starts_with(bin_name) {
+        // Case 2
+        line.split_whitespace().nth(1).ok_or_else(|| {
+            eyre!(output_changed_message!(
                 &format!("{bin_name} --version"),
-                "No first line"
-            )))
-        }
+                format!("No version after '{bin_name}'")
+            ))
+        })?
+    } else {
+        // Case 1
+        // The whole line should be the version
+        // Insiders versions have "-insider" suffix which we can simply ignore.
+        line.trim_end_matches("-insider")
     };
 
-    // Raise any errors in parsing the version
-    //  The benefit of handling VSCodium versions so old that the version 
format is something
-    //  unexpected is outweighed by the benefit of failing fast on new 
breaking versions
-    let version =
-        version.wrap_err_with(|| output_changed_message!(&format!("{bin_name} 
--version"), "Invalid version"))?;
+    // Strip leading zeroes because `semver` does not allow them, but VSCodium 
uses them sometimes.
+    //  This is not the case for VSCode, but just in case, and it can't really 
cause any issues.
+    let version_string = version_string
+        .split('.')
+        .map(|s| if s == "0" { "0" } else { s.trim_start_matches('0') })
+        .collect::<Vec<_>>()
+        .join(".");
+
+    let version = Version::parse(&version_string)
+        // Raise any errors in parsing the version
+        //  The benefit of handling VSCodium versions so old that the version 
format is something
+        //  unexpected is outweighed by the benefit of failing fast on new 
breaking versions
+        .wrap_err_with(|| output_changed_message!(&format!("{bin_name} 
--version"), "Invalid version"))?;
+
     debug!("Detected {name} version as: {version}");
 
     if version < Version::new(1, 86, 0) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/topgrade-16.6.1/src/steps/os/unix.rs 
new/topgrade-16.7.0/src/steps/os/unix.rs
--- old/topgrade-16.6.1/src/steps/os/unix.rs    2025-12-06 20:45:42.000000000 
+0100
+++ new/topgrade-16.7.0/src/steps/os/unix.rs    2025-12-17 11:07:46.000000000 
+0100
@@ -4,8 +4,6 @@
 use etcetera::BaseStrategy;
 use home;
 use ini::Ini;
-#[cfg(target_os = "linux")]
-use nix::unistd::Uid;
 use regex::Regex;
 use rust_i18n::t;
 use semver::Version;
@@ -35,12 +33,124 @@
 use crate::terminal::print_separator;
 use crate::utils::{require, PathExt};
 
+#[cfg(target_os = "linux")]
+fn brew_linux_sudo_uid() -> Option<u32> {
+    let linuxbrew_directory = "/home/linuxbrew/.linuxbrew";
+    if let Ok(metadata) = fs::metadata(linuxbrew_directory) {
+        let owner_id = metadata.uid();
+        let current_id = nix::unistd::Uid::effective();
+        // print debug these two values
+        debug!("linuxbrew_directory owner_id: {owner_id}, current_id: 
{current_id}");
+        return if owner_id == current_id.as_raw() {
+            None // no need for sudo if linuxbrew is owned by the current user
+        } else {
+            Some(owner_id) // otherwise use sudo to run brew as the owner
+        };
+    }
+    None
+}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+fn brew_get_sudo() -> Option<String> {
+    #[cfg(target_os = "linux")]
+    {
+        let sudo_uid = brew_linux_sudo_uid();
+        // if brew is owned by another user, execute "sudo -Hu <uid> brew 
update"
+        if let Some(user_id) = sudo_uid {
+            let uid = nix::unistd::Uid::from_raw(user_id);
+            let user = nix::unistd::User::from_uid(uid)
+                .expect("failed to call getpwuid()")
+                .expect("this user should exist");
+
+            return Some(user.name);
+        }
+    }
+    None
+}
+
 #[cfg(any(target_os = "linux", target_os = "macos"))]
 const INTEL_BREW: &str = "/usr/local/bin/brew";
 
 #[cfg(any(target_os = "linux", target_os = "macos"))]
 const ARM_BREW: &str = "/opt/homebrew/bin/brew";
 
+#[derive(Clone, Debug)]
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+pub struct Brew {
+    variant: BrewVariant,
+    path: PathBuf,
+    sudo: Option<String>,
+}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+impl Brew {
+    fn new(variant: BrewVariant) -> Result<Self> {
+        Ok(Self {
+            variant,
+            path: require(variant.binary_name())?,
+            sudo: brew_get_sudo(),
+        })
+    }
+
+    // TODO: this is suboptimal, hopefully simplify with v17 refactor
+    //  + `impl Clone for Executor`
+    /// Execute a brew command. Uses `arch` to run using the correct
+    /// architecture on macOS if needed, and `sudo -Hu` to run as the
+    /// correct user on Linux if needed.
+    fn execute(&self, ctx: &ExecutionContext) -> Result<Executor> {
+        let mut args = vec![];
+        match self.variant {
+            BrewVariant::MacIntel if cfg!(target_arch = "aarch64") => {
+                args.extend(["arch", "-x86_64"]);
+            }
+            BrewVariant::MacArm if cfg!(target_arch = "x86_64") => {
+                args.extend(["arch", "-arm64e"]);
+            }
+            _ => {}
+        }
+        args.push(
+            self.path
+                .to_str()
+                .ok_or_eyre("brew path contains non-unicode characters")?,
+        );
+        let mut it = args.into_iter();
+
+        // SAFETY: guaranteed to contain at least one element, the path
+        let program = it.next().unwrap();
+        let mut cmd = match &self.sudo {
+            None => ctx.execute(program),
+            Some(user) => {
+                let sudo = ctx.require_sudo()?;
+                sudo.execute_opts(ctx, program, 
SudoExecuteOpts::new().set_home().user(user))?
+            }
+        };
+        cmd.args(it);
+        Ok(cmd)
+    }
+
+    fn step_title(&self) -> String {
+        let both_exists = BrewVariant::both_both_exist();
+        let step_title = match self.variant {
+            BrewVariant::MacArm if both_exists => "Brew (ARM)",
+            BrewVariant::MacIntel if both_exists => "Brew (Intel)",
+            _ => "Brew",
+        };
+
+        match &self.sudo {
+            Some(user) => {
+                format!("{} ({})", step_title, t!("sudo as user '{user}'", 
user = user))
+            }
+            None => step_title.to_string(),
+        }
+    }
+
+    #[cfg(target_os = "macos")]
+    fn is_macos_custom(&self) -> bool {
+        let path = self.path.as_os_str();
+        !(path == INTEL_BREW || path == ARM_BREW)
+    }
+}
+
 #[derive(Copy, Clone, Debug)]
 #[allow(dead_code)]
 #[cfg(any(target_os = "linux", target_os = "macos"))]
@@ -68,57 +178,6 @@
     fn both_both_exist() -> bool {
         Path::new(INTEL_BREW).exists() && Path::new(ARM_BREW).exists()
     }
-
-    pub fn step_title(self) -> &'static str {
-        let both_exists = Self::both_both_exist();
-        match self {
-            BrewVariant::MacArm if both_exists => "Brew (ARM)",
-            BrewVariant::MacIntel if both_exists => "Brew (Intel)",
-            _ => "Brew",
-        }
-    }
-
-    /// Execute an "internal" brew command, i.e. one that should always be run
-    /// even when dry-running. Basically just a wrapper around [`Command::new`]
-    /// that uses `arch` to run using the correct architecture if needed.
-    fn execute_internal(self) -> Command {
-        match self {
-            BrewVariant::MacIntel if cfg!(target_arch = "aarch64") => {
-                let mut command = Command::new("arch");
-                command.arg("-x86_64").arg(self.binary_name());
-                command
-            }
-            BrewVariant::MacArm if cfg!(target_arch = "x86_64") => {
-                let mut command = Command::new("arch");
-                command.arg("-arm64e").arg(self.binary_name());
-                command
-            }
-            _ => Command::new(self.binary_name()),
-        }
-    }
-
-    /// Execute a brew command. Uses `arch` to run using the correct
-    /// architecture on macOS if needed.
-    fn execute(self, ctx: &ExecutionContext) -> Executor {
-        match self {
-            BrewVariant::MacIntel if cfg!(target_arch = "aarch64") => {
-                let mut command = ctx.execute("arch");
-                command.arg("-x86_64").arg(self.binary_name());
-                command
-            }
-            BrewVariant::MacArm if cfg!(target_arch = "x86_64") => {
-                let mut command = ctx.execute("arch");
-                command.arg("-arm64e").arg(self.binary_name());
-                command
-            }
-            _ => ctx.execute(self.binary_name()),
-        }
-    }
-
-    #[cfg(target_os = "macos")]
-    fn is_macos_custom(binary_name: PathBuf) -> bool {
-        !(binary_name.as_os_str() == INTEL_BREW || binary_name.as_os_str() == 
ARM_BREW)
-    }
 }
 
 pub fn run_fisher(ctx: &ExecutionContext) -> Result<()> {
@@ -290,61 +349,25 @@
         .status_checked()
 }
 
-#[cfg(target_os = "linux")]
-pub fn brew_linux_sudo_uid() -> Option<u32> {
-    let linuxbrew_directory = "/home/linuxbrew/.linuxbrew";
-    if let Ok(metadata) = std::fs::metadata(linuxbrew_directory) {
-        let owner_id = metadata.uid();
-        let current_id = Uid::effective();
-        // print debug these two values
-        debug!("linuxbrew_directory owner_id: {}, current_id: {}", owner_id, 
current_id);
-        return if owner_id == current_id.as_raw() {
-            None // no need for sudo if linuxbrew is owned by the current user
-        } else {
-            Some(owner_id) // otherwise use sudo to run brew as the owner
-        };
-    }
-    None
-}
-
 #[cfg(any(target_os = "linux", target_os = "macos"))]
 pub fn run_brew_formula(ctx: &ExecutionContext, variant: BrewVariant) -> 
Result<()> {
-    #[allow(unused_variables)]
-    let binary_name = require(variant.binary_name())?;
+    let brew = Brew::new(variant)?;
 
     #[cfg(target_os = "macos")]
     {
-        if variant.is_path() && !BrewVariant::is_macos_custom(binary_name) {
+        if variant.is_path() && !brew.is_macos_custom() {
             return Err(SkipStep(t!("Not a custom brew for 
macOS").to_string()).into());
         }
     }
 
-    #[cfg(target_os = "linux")]
-    {
-        let sudo_uid = brew_linux_sudo_uid();
-        // if brew is owned by another user, execute "sudo -Hu <uid> brew 
update"
-        if let Some(user_id) = sudo_uid {
-            let uid = nix::unistd::Uid::from_raw(user_id);
-            let user = nix::unistd::User::from_uid(uid)
-                .expect("failed to call getpwuid()")
-                .expect("this user should exist");
-
-            let sudo_as_user = t!("sudo as user '{user}'", user = user.name);
-            print_separator(format!("{} ({})", variant.step_title(), 
sudo_as_user));
-
-            let sudo = ctx.require_sudo()?;
-            sudo.execute_opts(ctx, &binary_name, 
SudoExecuteOpts::new().set_home().user(&user.name))?
-                .current_dir("/tmp") // brew needs a writable current directory
-                .arg("update")
-                .status_checked()?;
-            return Ok(());
-        }
-    }
-    print_separator(variant.step_title());
+    print_separator(brew.step_title());
 
-    variant.execute(ctx).arg("update").status_checked()?;
+    brew.execute(ctx)?.arg("update").status_checked()?;
+    // TODO: this had:
+    //  `.current_dir("/tmp") // brew needs a writable current directory`
+    //  but that only applied when sudo -Hu was used. Is it really needed?
 
-    let mut command = variant.execute(ctx);
+    let mut command = brew.execute(ctx)?;
     command.args(["upgrade", "--formula"]);
 
     if ctx.config().brew_fetch_head() {
@@ -354,11 +377,11 @@
     command.status_checked()?;
 
     if ctx.config().cleanup() {
-        variant.execute(ctx).arg("cleanup").status_checked()?;
+        brew.execute(ctx)?.arg("cleanup").status_checked()?;
     }
 
     if ctx.config().brew_autoremove() {
-        variant.execute(ctx).arg("autoremove").status_checked()?;
+        brew.execute(ctx)?.arg("autoremove").status_checked()?;
     }
 
     Ok(())
@@ -366,17 +389,18 @@
 
 #[cfg(any(target_os = "linux", target_os = "macos"))]
 pub fn run_brew_cask(ctx: &ExecutionContext, variant: BrewVariant) -> 
Result<()> {
-    let binary_name = require(variant.binary_name())?;
+    let brew = Brew::new(variant)?;
 
     #[cfg(target_os = "macos")]
-    if variant.is_path() && !BrewVariant::is_macos_custom(binary_name) {
+    if variant.is_path() && !brew.is_macos_custom() {
         return Err(SkipStep(t!("Not a custom brew for 
macOS").to_string()).into());
     }
 
     #[cfg(target_os = "linux")]
     {
         // Homebrew cask support was added in version 4.5.0
-        let version_output = 
Command::new(&binary_name).arg("--version").output_checked_utf8()?;
+        // TODO: This should run even when dry-running. Blocked by #1227.
+        let version_output = 
brew.execute(ctx)?.arg("--version").output_checked_utf8()?;
 
         let version_line = version_output
             .stdout
@@ -406,10 +430,13 @@
         }
     }
 
-    print_separator(format!("{} - Cask", variant.step_title()));
+    print_separator(format!("{} - Cask", brew.step_title()));
 
-    let cask_upgrade_exists = variant
-        .execute_internal()
+    // TODO: this should run even when dry-running, but that
+    //  functionality was removed to reduce complexity when
+    //  implementing the sudo -Hu stuff. Blocked by #1227.
+    let cask_upgrade_exists = brew
+        .execute(ctx)?
         .args(["--repository", "buo/cask-upgrade"])
         .output_checked_utf8()
         .map(|p| Path::new(p.stdout.trim()).exists())?;
@@ -434,10 +461,10 @@
         }
     }
 
-    variant.execute(ctx).args(&brew_args).status_checked()?;
+    brew.execute(ctx)?.args(&brew_args).status_checked()?;
 
     if ctx.config().cleanup() {
-        variant.execute(ctx).arg("cleanup").status_checked()?;
+        brew.execute(ctx)?.arg("cleanup").status_checked()?;
     }
 
     Ok(())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/topgrade-16.6.1/src/sudo.rs 
new/topgrade-16.7.0/src/sudo.rs
--- old/topgrade-16.6.1/src/sudo.rs     2025-12-06 20:45:42.000000000 +0100
+++ new/topgrade-16.7.0/src/sudo.rs     2025-12-17 11:07:46.000000000 +0100
@@ -1,3 +1,4 @@
+use std::collections::HashSet;
 use std::ffi::OsStr;
 use std::path::Path;
 use std::path::PathBuf;
@@ -8,6 +9,7 @@
 use color_eyre::eyre::eyre;
 use color_eyre::eyre::Context;
 use color_eyre::eyre::Result;
+use itertools::Itertools;
 use rust_i18n::t;
 use serde::Deserialize;
 use strum::Display;
@@ -68,7 +70,7 @@
     /// Preserve all environment variables.
     All,
     /// Preserve only the specified environment variables.
-    Some(&'a [&'a str]),
+    Some(HashSet<&'a str>),
     /// Preserve no environment variables.
     #[default]
     None,
@@ -111,7 +113,7 @@
     /// Preserve only the specified environment variables across the sudo call.
     #[allow(unused)]
     pub fn preserve_env_list(mut self, vars: &'a [&'a str]) -> Self {
-        self.preserve_env = SudoPreserveEnv::Some(vars);
+        self.preserve_env = 
SudoPreserveEnv::Some(vars.iter().copied().collect());
         self
     }
 
@@ -407,7 +409,21 @@
             cmd.arg("-d");
         }
 
-        match opts.preserve_env {
+        let mut preserve_env = opts.preserve_env;
+        // The `--env` arguments are set globally in `main.rs`, but sudo by 
default
+        // does not pass these environment variables through unless explicitly 
told to.
+        // So we add them here to the preserve_env list.
+        let cfg_env_vars = ctx.config().env_variables();
+        if !cfg_env_vars.is_empty() {
+            let cfg_env_var_keys = cfg_env_vars.iter().map(|(key, _value)| 
key.as_str());
+            // merge the user-specified env vars with the opts.preserve_env
+            match preserve_env {
+                SudoPreserveEnv::All => {}
+                SudoPreserveEnv::Some(ref mut env_set) => 
env_set.extend(cfg_env_var_keys),
+                SudoPreserveEnv::None => preserve_env = 
SudoPreserveEnv::Some(cfg_env_var_keys.collect()),
+            }
+        }
+        match preserve_env {
             SudoPreserveEnv::All => match self.kind {
                 SudoKind::Sudo => {
                     cmd.arg("-E");
@@ -426,7 +442,7 @@
             },
             SudoPreserveEnv::Some(vars) => match self.kind {
                 SudoKind::Sudo => {
-                    cmd.arg(format!("--preserve-env={}", vars.join(",")));
+                    cmd.arg(format!("--preserve-env={}", 
vars.iter().join(",")));
                 }
                 SudoKind::Run0 => {
                     for env in vars {
@@ -435,7 +451,7 @@
                 }
                 SudoKind::Please => {
                     cmd.arg("-a");
-                    cmd.arg(vars.join(","));
+                    cmd.arg(vars.iter().join(","));
                 }
                 SudoKind::Doas | SudoKind::WinSudo | SudoKind::Gsudo | 
SudoKind::Pkexec => {
                     return Err(UnsupportedSudo {

++++++ vendor.tar.zst ++++++
/work/SRC/openSUSE:Factory/topgrade/vendor.tar.zst 
/work/SRC/openSUSE:Factory/.topgrade.new.1928/vendor.tar.zst differ: char 7, 
line 1

Reply via email to