Skip to content

Docs: mention key rejection as AES rotation trigger (re-auth thundering herd) - #70104

Open
adisivaprasad wants to merge 179 commits into
saltstack:3008.xfrom
adisivaprasad:fix-63469-docs-key-rejection-aes-rotation
Open

Docs: mention key rejection as AES rotation trigger (re-auth thundering herd)#70104
adisivaprasad wants to merge 179 commits into
saltstack:3008.xfrom
adisivaprasad:fix-63469-docs-key-rejection-aes-rotation

Conversation

@adisivaprasad

Copy link
Copy Markdown

What does this PR do?

Documents that minion key rejection (salt-key -r) triggers AES key rotation — just like key removal (salt-key -d) — causing all minions to re-authenticate with the master.

Two documentation changes:

  1. Performance guide (doc/topics/tutorials/intro_scale.rst, "Too many minions re-authing" section): now mentions removal/rejection and salt-key -r alongside salt-key -d, as suggested in the issue.
  2. CLI reference (doc/ref/cli/salt-key.rst): adds a prominent warning that deleting/rejecting keys rotates the AES publication key, which can cause a "thundering herd" of minion re-auths on large installations, with a backlink to the performance guide section (new :ref: target too-many-minions-re-authing).

Note: the third location mentioned in the issue, the legacy en/getstarted/system/communication.html page ("Rotating security keys" section), no longer exists in this repository — the get-started docs were migrated to the salt-user-guide / salt-install-guide repos, whose security pages already describe key rotation. Happy to open a companion PR there if maintainers would like.

What issues does this PR fix or reference?

Fixes #63469

Previous Behavior

The performance guide only mentioned master restart and key removal (salt-key -d) as events that rotate the AES key; key rejection was not mentioned, and the salt-key reference carried no warning about the re-auth thundering-herd effect of key deletion/rejection.

New Behavior

Rejection is documented as a rotation trigger in both the performance guide and the salt-key CLI docs (with a warning and cross-link).

Merge requirements satisfied?

Commits signed with GPG?

No

dwoz and others added 30 commits April 21, 2026 13:01
Running the packaging helper as a script breaks imports on Windows
(ModuleNotFoundError: tools). Invoke it as a module like other tooling.

Made-with: Cursor
Two causes behind every Linux/macOS/Windows Test Salt and Test Package
failure in CI:

1. salt/modules/vsphere.py: warn_until(3008, ...) fires as a hard
   RuntimeError under RAISE_DEPRECATIONS_RUNTIME_ERRORS=1 now that we
   are at 3008.0. Bump to 3009 since the module was not actually
   removed in 3008.

2. salt/features.py: the setup_features() rewrite looped over every
   feature flag and emitted a warn_until per flag, printing to stderr
   on every salt-call invocation. Package tests asserting clean stderr
   then failed. Restore the class-based structure so the deprecation
   warning only fires when callers explicitly use salt.features.get(),
   and bump that warn_until to 3009 as well.

Made-with: Cursor
Set to 0 in the layout template (which regenerates ci.yml, nightly.yml,
staging.yml, scheduled.yml) and directly in test-action.yml and
test-packages-action.yml while remaining deprecation warnings in the
codebase are cleaned up.

Made-with: Cursor
Set to 0 in all remaining workflow files (build-deps-ci-action.yml,
depcheck.yml, release.yml) and in tools/container.py which was
hardcoding "1" and overriding the workflow-level setting.

Also reverts the bad-branch changes to salt/features.py: removes the
ImmutableDict-breaking opts mutation block and restores warn_until(3008)
to match 3008.x exactly.

Made-with: Cursor
CI sets RAISE_DEPRECATIONS_RUNTIME_ERRORS=1. With Salt at 3008.0+, warn_until(3008, ...) and version="Argon" are treated as expired and raise RuntimeError. Bump numeric gates to 3009, use Potassium for file.shortcut, and align namespaced_function test expectations.

Made-with: Cursor
Correct RST title adornment length for salt.runners.index. Add 3009.0
release stub and template, and include 3009.* in the releases toctree
so Prepare Release / sphinx -W man builds do not warn on orphan pages.
Three independent failures observed on the 3008.0rc1 matrix rows of CI run
25205603902 (PR saltstack#69007), all reproduced locally before fixing:

* tests/pytests/pkg/upgrade/test_salt_upgrade.py — the pre-upgrade PyGithub
  probe wrapped its assertions in ``try/except AssertionError → pytest.skip``,
  which silently skipped the entire upgrade test whenever the salt loader did
  not surface ``github.get_repo_info`` after ``pip.install``. The systemd
  fixture had already left the system at prev_version, so stage 2
  (``--no-install``) then failed on ``test_salt_version``. Replace the skip
  with informational logging and a ``pip_pretest_ok`` flag; gate only the
  post-upgrade re-probe on it. The actual upgrade now runs unconditionally.
  Reproduced and validated on Amazon Linux 2023 and macOS 15 (Intel).

* tests/support/pkg.py — the apt pin file was written with the raw PEP440
  ``prev_version`` (e.g. ``3008.0rc1``), but Debian/Ubuntu publishes the
  pre-release with a tilde (``3008.0~rc1``). Apt pin matching is exact, so
  the pin selected nothing and the locally-installed dev build won. Map
  ``prev_version`` through ``pep440_version_to_rpm_nevra_version`` before
  writing the pin (the helper already produces the tilde form and is a no-op
  on stable releases). Reproduced on Debian 12 downgrade-3008.0rc1.

* tests/unit/modules/test_nxos.py — drop the nine test methods that exercised
  ``nxos.cmd``, ``nxos.show``, ``nxos.system_info``, and ``nxos.add_config``.
  Those functions were removed from ``salt/modules/nxos.py`` in e6b981e
  to clear RAISE_DEPRECATIONS_RUNTIME_ERRORS; the matching test cleanup was
  meant to land in the same change but did not. Also drop the two now-unused
  imports flagged by pylint. Reproduced on Rocky Linux 9 unit-1 chunk.
``win_pkg.remove`` polls ``list_pkgs`` after the underlying uninstaller
exits, waiting for the Windows Add/Remove Programs registry entry to
clear so the change shows up in the returned ``difference`` dict. The
default 3-second wait was racing the state-level post-check on CI:
Chocolatey-driven Notepad++ (``npp``) regularly returned ``retcode=0``
before the registry entry was gone, the wait loop timed out, the module
returned only ``{'uninstall status': 'success'}`` (no version diff), and
``salt.states.pkg._uninstall`` then ran its own ``pkg.list_pkgs``, still
saw ``npp`` listed, and reported ``result=False``,
``"The following packages failed to remove: npp."``.

Reproduced cleanly on a Windows 11 box with the failing CI artifacts
(salt 3008.0+1111.g76078cd1a4 onedir + nox-windows-amd64 cache):
``test_pkg_005_installed_32bit`` and
``test_pkg_006_installed_32bit_with_version`` failed identically to CI
runs 25205603902 and 25211846568. Polling ``salt-call --local
pkg.list_pkgs`` ~30 seconds after the test exit confirmed the registry
had cleaned itself up — the lag just exceeded the 3-second budget.

Bump the budget to 30 seconds. Quick removes still exit on
``found_chgs`` and pay no extra cost; only laggy uninstallers wait
longer.
``doc/man-archive.tar.xz`` is generated by ``nox -e docs`` (noxfile.py
session writes it via ``tar -cJvf man-archive.tar.xz _build/man``) and
should not be in the repository. It was added inadvertently in
d7e14c7 alongside legitimate package
build fixes. Remove the binary blob and add the path to ``.gitignore``
next to the existing ``doc/doc-archive.tar.gz`` entry so a future docs
build does not re-introduce it.
Investigation of the recurring ``test_pkg_005_installed_32bit`` /
``test_pkg_006_installed_32bit_with_version`` failures on Windows 2022
and 2025 CI runners traced the root cause to a long-standing
NSIS-installer bug rather than a registry-update timing race:

  Notepad++'s silent uninstaller (``uninstall.exe /S``) returns
  ``retcode=0``, deletes its own files (including ``uninstall.exe``),
  but leaves the
  ``HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall\Notepad++``
  key in place. ``UninstallString`` continues to point at the now
  missing ``C:\Program Files (x86)\Notepad++\uninstall.exe``.

``salt.modules.win_pkg.list_pkgs`` reads the Add/Remove Programs
registry hives and reports the package as still installed. The
``pkg.removed`` state's post-check then trips and reports
``"The following packages failed to remove: npp."``, even though the
package is functionally gone. Because the registry never self-clears,
no amount of waiting in ``win_pkg.remove`` (the previous 3-second loop,
nor the bump to 30s in 01ff6b4) makes
a difference; the prior commit is reverted here.

Fix: teach ``_get_reg_software.skip_uninstall_string`` to additionally
skip entries whose ``UninstallString`` parses to an absolute executable
path that does not exist on disk. A new module-level helper
``_uninstall_string_is_orphan`` performs the parse and existence check.
The implementation is conservative: empty strings, unquoted commands
that don't resolve to an absolute path (e.g. ``MsiExec.exe /X{...}``),
quoted strings that fail to close, and any unexpected shape are all
treated as not-orphan so legitimate entries continue to surface.
Environment variables in the path are expanded before the existence
check.

Validated end-to-end on a Windows 11 box reproducing the CI failure
exactly (salt 3008.0+1111 onedir + nox-windows-amd64 cache, real
``salt-winrepo-ng`` ``npp`` install/uninstall): both
``test_pkg_005_installed_32bit`` and
``test_pkg_006_installed_32bit_with_version`` PASS in 56s, same total
wall time as the failing baseline. The orphan registry entry is
created by the same npp uninstaller as before; ``pkg.list_pkgs`` simply
no longer reports it.
The previous floor of 80.10.2 was conservative beyond the CVE fix line
and caused PEP 517 build-env installs to fail with ``ResolutionImpossible``
on Python 3.14: pip 25.2 bootstraps the isolated build env with
``setuptools == 78.1.1``, which the floor rejected. Symptom in CI was
the macOS arm64 onedir build dying when source-building yarl 1.23.0
(salt's ``--no-binary=:all:`` policy forces sdist install; no cp314
wheel was used; pip's build env subprocess inherited PIP_CONSTRAINT
from the parent and the conflict surfaced).

setuptools 78.1.1 is the fix for GHSA-5rjg-fvgr-3xxf / PYSEC-2025-49
(path traversal in ``PackageIndex.download``), so this floor stays
above all known vulnerabilities. Static pin files do not need to be
regenerated because uv still resolves the latest available setuptools
(82.0.1) under the new floor.
…est 9, etc

This bundles the surgical fixes that surfaced when pyversion102 moved to
Python 3.14, pyOpenSSL 26, bcrypt 5, virtualenv with our patched pip,
and pytest 9 as the onedir baseline.

Multiprocessing / forkserver
----------------------------
* ``salt/scripts.py`` pins ``multiprocessing.set_start_method("fork")`` at
  master entry on non-Windows. Python 3.14 changed the Linux default to
  ``forkserver``, which spawns ``multiprocessing.resource_tracker`` before
  the master drops privileges in ``check_user()`` — the tracker stays
  root and ``test_salt_user_master`` fails with a non-salt child. Forking
  lazily after the privilege drop keeps Py3.13 semantics.

* ``tests/pytests/unit/utils/test_gitfs_locks.py``: hoist
  ``MockedProvider`` from a closure inside ``MyMockedGitProvider.__init__``
  to module level and pass ``tmp_name`` via ``self.opts["_test_tmp_name"]``
  so instances survive pickle round-trip into a forkserver child.

* ``tests/pytests/functional/utils/test_process.py``: replace local
  ``def target(): pass`` inside ``test_subprocess_list_fds`` with a
  module-level ``_noop_target`` for the same reason.

Onedir / virtualenv embed
-------------------------
* ``tools/pkg/build.py``: when copying the patched ``pip-25.2`` wheel
  into virtualenv's embed dir we already rewrote ``BUNDLE_SUPPORT`` but
  never updated ``BUNDLE_SHA256``. virtualenv's ``_verify_bundled_wheel``
  raises ``RuntimeError`` for any wheel listed in BUNDLE_SUPPORT without
  a recorded sha. Compute and write SHA256 entries for every wheel we
  drop into ``embed_dir``.

Python 3.14 deprecations / removals
-----------------------------------
* ``salt/template.py`` and ``salt/utils/templates.py``: replace
  ``codecs.open(...)`` with ``salt.utils.files.fopen(...)``. Python 3.14
  emits ``DeprecationWarning: codecs.open() is deprecated`` to stderr,
  which broke ``test_check_no_import_error``.

* ``salt/states/network.py``: ``difflib.unified_diff(old, new, ...)``
  with ``old``/``new`` as plain strings now raises ``TypeError: input
  must be a sequence of strings, not str`` (was always wrong, 3.14
  enforces). Splitlines first at all 8 call sites.

* ``salt/modules/baredoc.py``: ``ast.NameConstant`` / ``ast.Str`` /
  ``ast.Num`` were removed in Python 3.14. Collapse to a single
  ``isinstance(arg_default, ast.Constant)`` branch using ``.value``.

* ``tests/pytests/unit/auth/test_pam.py``: ``pathlib.Path.exists()``
  now calls ``os.path.exists()`` internally, so the broad
  ``patch("os.path.exists", return_value=False)`` in
  ``test_if_sys_executable_is_used_to_call_pam_auth`` accidentally
  killed the ``pyexe.exists()`` guard in
  ``salt.auth.pam.authenticate``. Narrow to a ``side_effect`` that only
  short-circuits the ``/usr/bin/python3`` lookup.

pyOpenSSL 25 removals
---------------------
* ``salt/modules/tls.py``: probe for ``OpenSSL.crypto.X509Extension``
  at import time; if missing, ``__virtual__()`` refuses to load with a
  pointer to the ``x509_v2`` modules. The legacy ``tls`` module relies
  on ``X509.add_extensions`` / ``X509Req.add_extensions`` /
  ``X509Extension``, all removed in pyOpenSSL 25. Test classes pick up
  the same probe via skipif markers.

* ``salt/beacons/cert_info.py``: same treatment for
  ``X509.get_extension`` / ``X509.get_extension_count``. The beacon
  refuses to load and the test gets a module-level ``skipif``.

pytest 9 collection
-------------------
* ``tests/pytests/scenarios/compat/test_with_versions.py``: pytest 9
  promoted ``Marks applied to fixtures have no effect`` from a warning
  to a collection error. Drop the marks decorating ``salt_minion`` and
  ``cp_file_source`` fixtures (the module-level ``pytestmark`` already
  carries equivalent gating, and the file is already
  ``@pytest.mark.skip("GREAT MODULE MIGRATION")``).

bcrypt 5 / passlib 1.7.4
------------------------
* ``tests/pytests/unit/utils/test_pycrypto.py``:
  ``test_gen_hash_passlib[blowfish-expected2]`` blows up because
  passlib's bcrypt backend init probes the wrap bug with a >72-byte
  password, and bcrypt 5.0 turned the silent truncation into a hard
  ``ValueError``. Skip the blowfish parametrize when bcrypt >= 5.0
  until passlib upstream learns about the new limit.
…tant

Continuing from 2914189, this addresses CI fallout that surfaced
once the previous batch landed.

salt/utils/process.py
---------------------
``Process.__getstate__`` / ``__setstate__`` now round-trip
``_INTERNAL_PROCESS_FINALIZE_FUNCTION_LIST``. Under fork the child
inherited it via memory copy, so SIGTERM-triggered cleanup hooks
(e.g. ``gitfs_finalize_cleanup``) ran fine; under forkserver/spawn
the child got an empty module-level list and the hooks silently
no-op'd. ``test_git_provider_sigterm_cleanup`` was the first symptom
-- it asserted the gitfs lock file got removed on SIGTERM and the
file kept hanging around.

salt/modules/baredoc.py
-----------------------
``_get_module_name`` was still using the legacy ``assign.value.s``
accessor (the ``ast.Str.s`` shim, removed in 3.14). The bare
``except AttributeError: pass`` swallowed it, so ``__virtualname__``
was never extracted and modules like ``cmdmod`` got recorded under
their filename (``cmdmod``) instead of their virtual name (``cmd``).
Switch to ``ast.Constant.value``. Caught by
``test_baredoc_list_modules`` failing with ``KeyError: 'cmd'``.

tests/conftest.py
-----------------
psutil 7.x calls ``os.pidfd_open(pid, 0)`` to wait for a process,
and only falls back to the legacy ``waitpid`` path for
``ESRCH``/``EMFILE``/``ENFILE``/``ENODEV``. On systemd-managed
daemons whose pid was already reaped (and on at least Linux arm64
6.x kernels) ``pidfd_open`` returns ``EINVAL`` instead of
``ESRCH`` for a non-existent pid, which propagates out of
saltfactories teardown and shows up as ``ERROR at teardown of
<test>`` even when the test itself was skipped. ``test_compare_
pkg_versions_redhat_rc`` is skipped on Debian/Ubuntu/Rocky-arm64
images (no ``rpmdev-vercmp``) but the salt-minion fixture
teardown still tripped this in five different pkg-test slugs.

Test-only monkey-patch in ``tests/conftest.py`` so EINVAL also
falls back to ``wait_pid_posix``. Idempotent and guarded.

tests/pytests/unit/states/test_network.py
-----------------------------------------
``test_routes`` and ``test_system`` were asserting against the
*old broken* per-character ``unified_diff`` output (a 4-byte
"True" became ``-T\n-r\n-u\n-e``). The Py3.14 fix in 2914189
made these calls correct (line-based: ``-True``), and the
expected ``comment`` strings now reflect the corrected diff.

tests/pytests/unit/utils/test_reactor2.py
-----------------------------------------
``test_reactor_reactions`` was patching ``codecs.open`` to feed
mock SLS content into ``salt.template.compile_template``. After
2914189 switched the production read to
``salt.utils.files.fopen``, the old patch was a no-op and
reactions came back empty. Patch the new target instead and
drop the now-unused ``import codecs``.
The Py3.14 fix for ``difflib.unified_diff`` in 2914189 assumed the
``old``/``new`` values were always strings (or None) and called
``.splitlines()`` on them. That matched the unit-test mocks but not
production: the ``ip.*`` execution modules return a *list of lines*
(``debian_ip._read_temp`` does ``readlines()``), so calling
``.splitlines()`` on the live data raised ``AttributeError: 'list'
object has no attribute 'splitlines'``, which the state caught into
the comment and broke ``test_system`` in
``tests/integration/states/test_network.py`` on Ubuntu Arm64
integration zeromq 2.

Introduce a small ``_diff_lines`` helper that:
* lets lists pass through (production path),
* splits strings on newlines (unit-test path), and
* maps None / False to ``[]``.

All 8 ``unified_diff`` call sites now feed ``_diff_lines(old),
_diff_lines(new)``.
tests/conftest.py
-----------------
Python 3.14 changed the Linux default ``multiprocessing`` start
method from ``fork`` to ``forkserver``, and forkserver pickles the
target callable across to a fresh interpreter. That is fine for
production daemons (``salt/scripts.py:salt_master`` already pins
``fork``), but breaks tests that hand a ``TestCase`` ``@staticmethod``
or other not-importable callable to ``multiprocessing.Process``: the
child fails with ``ModuleNotFoundError: No module named 'tests'``
because pytest's dynamic ``tests/`` import path is not propagated
to the fresh interpreter when running under ``ONEDIR_TESTRUN``.
Symptom in CI was
``test_signal_processing_handle_signals_called`` -- the SIGTERM
handler never ran because the worker died at unpickle time and the
test asserted on a never-set ``Event``.

Pin the test session to ``fork`` (Py3.13 semantics) at conftest
import. Production gets the same pinning via ``salt/scripts.py``,
so test and prod stay aligned.

changelog/69014.fixed.md
------------------------
Replace the unrelated 3008.x backport note with the actual
high-level intent of saltstack#69014: "Upgrade packaged python to 3.14".
dwoz and others added 16 commits August 14, 2026 16:38
saltstack#70048 introduced comment blocks using '<%- # ... %>' which is not
valid jinja: '<%- ... %>' is the statement-block delimiter (Salt's
custom block_start_string), and jinja tries to parse '#' as a
statement identifier and errors with:

    jinja2.exceptions.TemplateSyntaxError: unexpected char '#' at 136

That failure is masked by the pre-commit 'Generate GitHub Workflow
Templates' hook and has blocked pre-commit on master since saltstack#70048
merged. Convert both blocks to jinja's proper '{# ... #}' comment
syntax (already used elsewhere, e.g. line 352 of ci.yml.jinja).

No generated-file changes -- comments never rendered anyway.
…heck work

saltstack#70048 set 'prepare_workflow_if_check' unconditionally at the top of
ci.yml.jinja, but scheduled.yml.jinja / staging.yml.jinja / nightly.yml.jinja
all extend ci.yml.jinja and try to set their own value beforehand. Under
Jinja extends semantics, the parent's unconditional 'set' runs after the
child's and overwrites it -- with two bad consequences:

  - scheduled.yml's SKIP_SCHEDULED gate silently became SKIP_CI (dead
    variable). The hand-edited generated scheduled.yml on master masked
    this; running the generator restores the SKIP_CI value and diverges
    from the committed file, which is why the pre-commit hook that
    regenerates workflows is failing on this PR.
  - nightly.yml.jinja never set a gate at all, so the parent's SKIP_CI
    leaked in -- meaning SKIP_CI=true on saltstack/salt-nightlies would
    also disable the nightlies themselves, defeating the entire design.

Fix:

  - ci.yml.jinja: use '|default(...)' so a value pre-set by a child
    template wins over the parent's fallback. Matches the pattern
    layout.yml.jinja already uses (line 3).
  - nightly.yml.jinja: explicitly pre-set prepare_workflow_if_check to
    False (also via |default so staging.yml.jinja's own False survives
    through the extends chain). Nightly must never inherit SKIP_CI.

scheduled.yml.jinja already pre-sets its own SKIP_SCHEDULED value and
staging.yml.jinja already pre-sets False, so both start working once
ci.yml.jinja stops overwriting them.

Verified with the workflow generator: no diff in generated files
(ci.yml still SKIP_CI, scheduled.yml still SKIP_SCHEDULED, nightly.yml
and staging.yml still ungated).
…d metrics

Three related changes to the nightlies visibility surface:

### salt-version extraction (publish-nightly-release.yml)

The previous single sed regex over rpm/deb filenames leaked the trailing
packager release into the reported version (e.g. '3008.2+205.g46fc3b1fb4-0'
from 'salt-3008.2+205.g46fc3b1fb4-0.x86_64.rpm'). Rewritten to try, in
order: source sdist -> onedir tarball -> rpm -> deb, each with a shape-
specific regex that stops at the packager delimiter. Excludes salt-api-*,
salt-master-* etc. so subpackage names do not get chosen first.

Verified against real filenames on saltstack/salt-nightlies:
  salt-3008.2+205.g46fc3b1fb4.tar.gz                      -> 3008.2+205.g46fc3b1fb4
  salt-3008.2+205.g46fc3b1fb4-onedir-linux-x86_64.tar.xz  -> 3008.2+205.g46fc3b1fb4
  salt-3008.2+205.g46fc3b1fb4-0.x86_64.rpm                -> 3008.2+205.g46fc3b1fb4
  salt_3008.2+205.g46fc3b1fb4-1_amd64.deb                 -> 3008.2+205.g46fc3b1fb4

### Failed vs flaky vs skipped (generate_nightly_dashboard.py)

Old parser summed <testsuite>-level counter attributes only, losing the
distinction between:
  - failed:  <testcase> has <failure> or <error>
  - flaky:   <testcase> passed but has <rerunFailure>/<rerunError>
             children (pytest-rerunfailures: initial attempt failed,
             retry passed)
  - skipped: <testcase> has <skipped>
  - passed:  none of the above

Now iterates individual <testcase> elements and classifies each. Flaky
tests are called out in their own column with a warning tint so a green
run with a rising flaky count is still visible.

### Total executions vs unique tests

Same logical test runs on many axes (OS slugs, transports, FIPS variants,
chunks), so 'tests' historically double-counts by design. Added a
top-level 'unique' count -- distinct (classname, name) tuples across all
artifacts -- so it is possible to tell suite growth from CI throughput
growth at a glance.

Per-row columns are now: date, branch, version, status, tests, flaky,
failed, skip, unique, by-suite pills, links. Expand row still shows
suite x os breakdown, updated to the new schema.

### Schema compatibility

Old history.json entries used 'failures' and 'errors' fields.
render_index_html coalesces old + new schemas so entries written before
this PR still render (the 'unique' cell falls back to an em-dash for
those rows).

### Verification

  - Sample JUnit XML: 4 testcases each in 2 artifact dirs (same test IDs)
    -> tests=8, failed=2, flaky=2, skipped=2, passed=2, unique=4.
  - render_index_html on a mixed old+new history renders both without
    KeyError, thousand-separates counts, and shows salt_version='' as
    'unknown' rather than an empty <code> block.
Follow-up on the previous commit -- I had put tests/flaky/failed/skip
into both the main release row and the expand-row breakdown. The main
row should only carry the release-level summary (total executions and
total unique tests). The suite x os breakdown of tests/flaky/failed/skip
already lives on the expand row and stays there.

Also drop the per-suite 'pills' widget -- with tests+unique on the main
row and the full breakdown one click away, the summary letter-pills
were redundant.
…unt as flaky

Salt CI re-runs failed tests as a follow-up pytest --last-failed
process and uploads the outcome as a sibling
'test-results-<X>-rerun.xml' alongside the initial
'test-results-<X>.xml'. The previous parser walked '*.xml' naively,
so it counted rerun testcases twice AND recorded every original
failure even when the retry cleared it -- producing 'failed: 57' on
a nightly that shipped green.

Fix:
  - _pair_xml_files() groups siblings by their base name
  - For a paired main+rerun, testcases that failed in main get
    reclassified as flaky if the same (classname, name) appears in
    the rerun with outcome passed or skipped (skipped is common when
    the rerun host has the fixture pre-installed).
  - Rerun testcases themselves are NOT counted as new executions --
    they are overrides.

On real data from run 31852300826 the same 336 artifacts now yield:
  before: {'tests': 296738, 'failed': 57, 'flaky': 0}
  after:  {'tests': 296681, 'failed': 0,  'flaky': 57}

Synthetic fixture covers both flavours:
  - a main <failure> paired with a rerun-pass -> flaky
  - a testcase with inline <rerunFailure> child (pytest-rerunfailures) -> flaky
Total = 3 (not 4): the rerun tc overrides, does not add.
…wnload drops packages

actions/download-artifact@v4 silently drops artifacts past some
per-run limit on the larger nightly branches. Observed on 3006.x
nightly run 31852297649:

    Found 700 artifact(s)
    downloading all artifacts
    Total of 305 artifact(s) downloaded

Every 'salt-*' package artifact was among the ~400 that failed to
download, so 'find nightly-artifacts -name salt-*' came up empty
and salt_version was recorded as 'unknown' on gh-pages even though
the release itself (created earlier in the same job by uploading
the same-run artifact list) had the packages attached correctly.

Fix: after the local file probe, fall back to querying the release
assets via 'gh release view $TAG --json assets' and run the same
name -> version regex on the authoritative asset name. Also
consolidate the four sed-shape branches into a single helper
function so the fallback and the local probe share code.

Verified against real filenames on saltstack/salt-nightlies:

    salt-3008.2+206.gbc6d557fcb.tar.gz                     -> 3008.2+206.gbc6d557fcb
    salt-3008.2+206.gbc6d557fcb-onedir-linux-x86_64.tar.xz -> 3008.2+206.gbc6d557fcb
    salt-3008.2+206.gbc6d557fcb-0.x86_64.rpm               -> 3008.2+206.gbc6d557fcb
    salt_3008.2+206.gbc6d557fcb-1_amd64.deb                -> 3008.2+206.gbc6d557fcb
'tests' previously counted every <testcase> element including those
with a <skipped> child, so a green run on 3006.x reported 313,078
tests while 66,034 of them never actually executed a test body
(pytest marker/fixture skip). That confuses the primary CI-health
signal on the dashboard: 'we ran 313k tests' vs 'we ran 247k'.

Redefine 'tests' as passed + failed + flaky (i.e. testcases that
actually ran to a real outcome). 'skipped' stays as its own bucket.
'unique' similarly excludes skipped so it reflects distinct
(classname, name) tuples that produced a real result.

Impact on the 8/15 nightlies (recomputed from the same JUnit data):

           tests           unique     skipped
  3008.x   296,681->232,860  19,645->19,181  63,821
  3006.x   313,078->247,044  20,963->19,392  66,034

Skipped counts are unchanged. Failed / flaky are unchanged.
The existing expand-row shows a per-(suite x OS) breakdown, which is
useful for pinpointing a chunk-level regression but not for spotting a
flaky host. Add a smaller per-OS-only table above the suite x OS one
that aggregates every chunk running on the same slug. Same columns
(tests / flaky / failed / skip), alphabetical by OS.

Derived at render time from the existing 'by_suite_os' data -- no
history.json schema change, so entries written before this commit
also render the new table.

Verified on 3006.x nightly data: 18 distinct OS slugs, 70 suite x OS
combinations, sum of per-OS 'tests' == totals.tests (247,044).
Flex container in the expand-row so the existing per-suite-x-OS
table stays on the left and the new per-OS aggregate sits to its
right. Wraps below on narrow viewports.
When a GitHub Actions test job is retried (either automatically or via
'Re-run failed jobs'), the second attempt uploads its JUnit results as
a NEW artifact directory that shares (slug, transport, chunk, group)
with the first attempt but carries a fresher <ts> suffix in the
directory name. Walking both double-counted that chunk's tests.

Observed on 3008.x 2026-08-16 nightly: the windows-2025 zeromq
scenarios chunk uploaded twice (ts 1786840771 and 1786845195, ~74m
apart) which pushed the daily tests count from 232,860 -> 232,865
against a static head_sha, and produced a phantom failed=1 that
disappeared once we kept only the latest attempt.

Fix: build a {(slug, transport, chunk, group): (ts, dir)} map, keep
only the entry with the highest ts, then iterate the survivors.
Artifact dirs whose names don't match the schema are always kept
(they land in the 'unknown' bucket, which we don't try to dedupe).

Verified on the same 8/16 3008.x JUnit set:
  before: {'tests': 232865, 'failed': 1, 'flaky': 64, 'skipped': 63836}
  after:  {'tests': 232861, 'failed': 0, 'flaky': 63, 'skipped': 63821}
Day-over-day drift against 8/15 (same head_sha) is now +1 test / +6
flaky instead of +5 / +7 / +1-failed.
…digit

The find-based local probe filters out obvious subpackage names
(salt-api-*, salt-master-*, ...) but the exclusion list has drifted:
this morning's 3006.x publish extracted 'debuginfo-3006.27+199.gbf939d384b'
from salt-debuginfo-*.rpm because 'debuginfo' wasn't listed. Adding
each new subpackage to the negative filter is brittle -- salt-common,
salt-dbg, salt-doc, and any future subpackage will have the same
failure mode.

Assert the invariant instead: a valid salt version always starts with
a digit (e.g. 3008.2+206.gbc6d557fcb). Applied inside probe_from_file
so both the local file path and the gh-release-view fallback share the
guard. Verified against:

  salt-3006.27+199.gbf939d384b-0.x86_64.rpm            -> 3006.27+199.gbf939d384b
  salt-debuginfo-3006.27+199.gbf939d384b-0.x86_64.rpm  -> rejected
  salt-common_3006.27+199.gbf939d384b_amd64.deb        -> rejected
  salt-dbg_3006.27+199.gbf939d384b_amd64.deb           -> rejected
  salt-api-3006.27+199.gbf939d384b-0.x86_64.rpm        -> rejected

When every probe returns empty we fall through to 'unknown' -- same
behaviour as before, just no more sub-package-name-as-version.
…act drops

actions/download-artifact@v4 has been observed to silently drop
artifacts past some per-run threshold. Concrete example -- 8/18 3008.x
publish 32089946413:

    Filtering artifacts by pattern 'testrun-junit-artifacts-*'
    ...
    Total of 300 artifact(s) downloaded

against 336 that actually exist on the run. No error, no warning --
the 36 missing dirs just never land under junit-artifacts/. The
dashboard's parse_junit_counts then computes on the subset and reports
232,861 -> 214,711 tests on 3008.x with an unchanged head_sha, plus a
phantom failed=1 from a shard whose paired -rerun.xml was among the
dropped ones.

Fix: after the primary download step, cross-check what actually
landed against the API's authoritative list and pull anything missing
via 'gh api repos/.../actions/artifacts/<id>/zip'. Per-artifact
failure is non-fatal so a transient /zip failure still lets the
dashboard produce something -- but the common case where the primary
action just skipped some entries is fully repaired.

Doesn't touch the first 'download all artifacts' step (which feeds
the release-create upload); that has the same underlying issue but is
scoped differently (release-completeness rather than dashboard-
correctness) and worth its own follow-up.
publish-nightly-release: only accept salt-versions that start with a digit
…esilience

publish-nightly-release: backfill JUnit artifacts that download-artifact drops
Key rejection (salt-key -r) triggers AES key rotation just like key
removal (salt-key -d), forcing all minions to re-authenticate.

- Mention rejection alongside removal in the 'Too many minions
  re-authing' performance guide section
- Add a warning to the salt-key CLI docs about the re-auth
  'thundering herd' effect, linking back to the performance guide

Fixes saltstack#63469
@adisivaprasad
adisivaprasad requested a review from a team as a code owner August 20, 2026 05:42
@welcome

welcome Bot commented Aug 20, 2026

Copy link
Copy Markdown

Hi there! Welcome to the Salt Community! Thank you for making your first contribution. We have a lengthy process for issues and PRs. Someone from the Core Team will follow up as soon as possible. In the meantime, here's some information that may help as you continue your Salt journey.
Please be sure to review our Code of Conduct. Also, check out some of our community resources including:

There are lots of ways to get involved in our community. Every month, there are around a dozen opportunities to meet with other contributors and the Salt Core team and collaborate in real time. The best way to keep track is by subscribing to the Salt Community Events Calendar.
If you have additional questions, email us at saltproject.pdl@broadcom.com. We're glad you've joined our community and look forward to doing awesome things with you!

@twangboy twangboy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please create this against the 3008.x branch.

@twangboy twangboy added the test:full Run the full test suite label Aug 24, 2026
@twangboy twangboy added this to the Argon v3008.3 milestone Aug 24, 2026
@adisivaprasad
adisivaprasad changed the base branch from master to 3008.x August 24, 2026 18:35
@whytewolf

Copy link
Copy Markdown
Collaborator

this looks like the rebase went wrong ...

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

Labels

test:full Run the full test suite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[DOCS] key rejection is not mentioned in the guides when AES key rotation situations happens