diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2dca4587f6f..e58721b678c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,8 @@ on: - 3006.x - 3007.x - 3008.x + - '[0-9][0-9][0-9][0-9].[0-9]*-[0-9]*' + - '[0-9][0-9][0-9][0-9].[0-9]*-patch' - master pull_request: types: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2dbc254bfbb..7ea7600bee0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,11 +12,15 @@ on: DO NOT prefix the version with a "v" (use 3006.0, not v3006.0). For prereleases use the PEP 440 form WITHOUT a hyphen (use 3008.0rc1, not 3008.0-rc1). + For patch releases use the post-release form with a hyphen and number + (use 3008.1-1 for the first patch of 3008.1). The Python sdist/wheel and the GitHub tag/release will use this - string verbatim (e.g. "salt-3008.0rc1.tar.gz" / "v3008.0rc1"). + string verbatim (e.g. "salt-3008.0rc1.tar.gz" / "v3008.0rc1", + "salt-3008.1-1.tar.gz" / "v3008.1-1"). The RPM "Version:" and the Debian changelog stanza substitute "rc" for "~rc" so prereleases sort before the GA version - (e.g. "3008.0~rc1" < "3008.0"). + (e.g. "3008.0~rc1" < "3008.0"). Patch releases set RPM Release: N + so they sort after the base (e.g. "3008.1-1" > "3008.1-0"). skip-salt-pkg-download-test-suite: type: boolean default: false diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 646e5cb5fe5..f6fb8de551b 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -13,7 +13,8 @@ on: required: true description: > The Salt version to set prior to building packages and staging the release. - Good: 3006.0, 3008.0rc1. Bad: v3006.0, 3008.0-rc1, 3008.0~rc1. + Good: 3006.0, 3008.0rc1, 3008.1-1. Bad: v3006.0, 3008.0-rc1, 3008.0~rc1. + For patch releases use the post-release form: 3008.1-1 (first patch of 3008.1). sign-windows-packages: type: boolean default: false diff --git a/.github/workflows/templates/layout.yml.jinja b/.github/workflows/templates/layout.yml.jinja index 9e025ebc610..2fd017321d5 100644 --- a/.github/workflows/templates/layout.yml.jinja +++ b/.github/workflows/templates/layout.yml.jinja @@ -23,6 +23,8 @@ on: - 3006.x - 3007.x - 3008.x + - '[0-9][0-9][0-9][0-9].[0-9]*-[0-9]*' + - '[0-9][0-9][0-9][0-9].[0-9]*-patch' - master pull_request: types: diff --git a/.github/workflows/templates/staging.yml.jinja b/.github/workflows/templates/staging.yml.jinja index 611807c96e0..5c14248c5d7 100644 --- a/.github/workflows/templates/staging.yml.jinja +++ b/.github/workflows/templates/staging.yml.jinja @@ -24,7 +24,8 @@ on: required: true description: > The Salt version to set prior to building packages and staging the release. - Good: 3006.0, 3008.0rc1. Bad: v3006.0, 3008.0-rc1, 3008.0~rc1. + Good: 3006.0, 3008.0rc1, 3008.1-1. Bad: v3006.0, 3008.0-rc1, 3008.0~rc1. + For patch releases use the post-release form: 3008.1-1 (first patch of 3008.1). sign-windows-packages: type: boolean default: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 563c7dfc78a..b7616906c4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Versions are `MAJOR.PATCH`. ### Changed - Upgrade the bundled onedir Python from 3.10.20 to 3.11.15 on the 3006.x branch. Python 3.10 reaches end of security support in October 2026, while Salt 3006.x must ship security fixes through July 2027. Users upgrading from a previous 3006.x package will need to reinstall any Salt extensions installed via `salt-pip` because the onedir `extras-3.10` directory is replaced by `extras-3.11`. [#69526](https://github.com/saltstack/salt/issues/69526) +## 3008.1-1 (2026-07-23) ### Fixed @@ -109,6 +110,7 @@ Versions are `MAJOR.PATCH`. - added conditional X functionality to linux_acl [#62852](https://github.com/saltstack/salt/issues/62852) - Added ``unmask`` parameter to ``pillar.ls``, ``pillar.raw``, ``pillar.ext``, ``pillar.keys``, and ``pillar.obfuscate`` for API consistency with ``pillar.get`` / ``pillar.items`` / ``pillar.item`` / ``pillar.data``. Default masking behavior is unchanged. [#69453](https://github.com/saltstack/salt/issues/69453) - Documented the ``gitcli`` GitFS provider (added in 3008.0) which shells out to the system ``git`` binary, auto-detected after ``pygit2`` and ``gitpython`` and used as a silent fallback when neither Python library is installed. Documented the ``cluster_isolated_filesystem`` master option (added in 3008.0) which lets master clusters run without a shared filesystem; keys, denied keys, ``file_roots`` and ``pillar_roots`` are sync'd in-band over the cluster transport, with ``keys.cache_driver: mmap_key`` as the recommended companion. [#69494](https://github.com/saltstack/salt/issues/69494) +- Deferred OpenTelemetry imports in `salt.utils.tracing` and `salt.utils.metrics` so daemons no longer pay the ~15 MB per-process OTel import cost when `tracing.enabled` / `metrics.enabled` are false (the default). On a stress-tested salt-master container (~15 Python processes) this reclaims ~225 MB per subsystem — restoring the pre-3008.x baseline. [#69855](https://github.com/saltstack/salt/issues/69855) ## 3008.1 (2026-06-11) diff --git a/doc/topics/releases/3008.1.md b/doc/topics/releases/3008.1.md index f79b03a5904..4e93b33bf7c 100644 --- a/doc/topics/releases/3008.1.md +++ b/doc/topics/releases/3008.1.md @@ -18,100 +18,6 @@ This is auto generated. --> ## Changelog -### Changed - -- Changed `salt.returners.redis_return` to enumerate the Redis keyspace [#69037](https://github.com/saltstack/salt/issues/69037) -- with `SCAN` instead of the blocking `KEYS pattern` command in both [#69037](https://github.com/saltstack/salt/issues/69037) -- `get_jids` and `clean_old_jobs`. `KEYS` walks the entire keyspace [#69037](https://github.com/saltstack/salt/issues/69037) -- synchronously and stalls the Redis server for the duration; on a [#69037](https://github.com/saltstack/salt/issues/69037) -- master with hundreds of thousands of jobs this can block all clients [#69037](https://github.com/saltstack/salt/issues/69037) -- of that Redis instance for seconds. `SCAN` is incremental and [#69037](https://github.com/saltstack/salt/issues/69037) -- non-blocking. Order of returned keys is no longer guaranteed (the [#69037](https://github.com/saltstack/salt/issues/69037) -- returner does not rely on order); operators with custom scripts that [#69037](https://github.com/saltstack/salt/issues/69037) -- read `ret:*` or `load:*` directly may see them in a different order. [#69037](https://github.com/saltstack/salt/issues/69037) - - ### Fixed -- Fixed ``win_pkg`` functions ignoring the ``saltenv`` setting in minion configuration. All public functions (``refresh_db``, ``genrepo``, ``install``, ``remove``, ``list_pkgs``, ``latest_version``, ``upgrade_available``, ``list_upgrades``, ``list_available``, ``version``, ``get_repo_data``, ``get_package_info``) now fall back to ``__opts__["saltenv"]`` when ``saltenv`` is not passed explicitly, instead of always defaulting to ``base``. [#38551](https://github.com/saltstack/salt/issues/38551) -- Added ``encoding`` parameter to ``file.replace`` execution module and state to support UTF-16, UTF-32, and other multi-byte encoded files that would otherwise be incorrectly treated as binary. [#52793](https://github.com/saltstack/salt/issues/52793) -- Improved documentation for the `runas` and `password` parameters in `cmd.run`, `cmd.script`, and all `salt.modules.cmdmod` execution functions on Windows. The docs now accurately describe when a password is required: only when the salt-minion is **not** running as SYSTEM or as an elevated Administrator. Removed the inaccurate claim that the target user account must be in the Administrators group. Also changed `cmd.script` to log a warning instead of hard-failing when `runas` is used without a password on Windows, since a password is not always required. [#57951](https://github.com/saltstack/salt/issues/57951) -- Fixed `SSL: DECRYPTION_FAILED_OR_BAD_RECORD_MAC` errors in the VMware cloud driver by reconnecting when a cached vCenter service instance is found to be stale or corrupted (for example when inherited across a fork by salt-cloud's parallel provider queries). [#61983](https://github.com/saltstack/salt/issues/61983) -- Fixed event signature verification failing under ``minion_sign_messages``. The minion was signing the return load before ``salt.channel.client.AsyncReqChannel._package_load`` attached transport metadata (``nonce``, ``ts``, ``tok``, ``id``), so the bytes the master re-serialized to verify did not match what was signed and every signed return was dropped. Signing is now performed inside ``_package_load`` after the metadata is attached, against the same bytes the master verifies. [#68181](https://github.com/saltstack/salt/issues/68181) -- Fixed two distinct bugs in the `salt.engines.redis_sentinel` engine that [#69031](https://github.com/saltstack/salt/issues/69031) -- together prevented it from being usable. `start()` no longer raises [#69031](https://github.com/saltstack/salt/issues/69031) -- `AttributeError: 'dict_values' object has no attribute 'pop'` on Python 3 [#69031](https://github.com/saltstack/salt/issues/69031) -- (the dict.values() result is now wrapped in `list(...)`). `Listener` and [#69031](https://github.com/saltstack/salt/issues/69031) -- `start()` now accept an optional `password` argument and forward it to [#69031](https://github.com/saltstack/salt/issues/69031) -- the redis client, allowing the engine to authenticate against a Sentinel [#69031](https://github.com/saltstack/salt/issues/69031) -- that requires AUTH; the default of `None` keeps existing configurations [#69031](https://github.com/saltstack/salt/issues/69031) -- working unchanged. [#69031](https://github.com/saltstack/salt/issues/69031) -- Fixed `salt.returners.redis_return` silently ignoring the documented [#69032](https://github.com/saltstack/salt/issues/69032) -- `redis.password` configuration option. The returner now reads [#69032](https://github.com/saltstack/salt/issues/69032) -- `redis.password` from config (in both regular and proxy modes) and [#69032](https://github.com/saltstack/salt/issues/69032) -- forwards it to both the single-server `redis.StrictRedis` and the [#69032](https://github.com/saltstack/salt/issues/69032) -- `StrictRedisCluster` constructors. Operators with auth-protected Redis [#69032](https://github.com/saltstack/salt/issues/69032) -- no longer lose every job return to a hidden `NOAUTH Authentication [#69032](https://github.com/saltstack/salt/issues/69032) -- required` failure; deployments without a password are unaffected. [#69032](https://github.com/saltstack/salt/issues/69032) -- Fixed three closely-related bugs in `salt.cache.redis_cache` that [#69033](https://github.com/saltstack/salt/issues/69033) -- together broke hierarchical-bank semantics: [#69033](https://github.com/saltstack/salt/issues/69033) -- `_build_bank_hier` now registers each child bank name in both the [#69033](https://github.com/saltstack/salt/issues/69033) -- parent's `$BANK_` set (consumed by `flush()` tree traversal) and the [#69033](https://github.com/saltstack/salt/issues/69033) -- parent's `$BANKEYS_` set (consumed by `list_()`); `_get_banks_to_remove` [#69033](https://github.com/saltstack/salt/issues/69033) -- now decodes the bytes returned by `smembers` and skips the `"."` [#69033](https://github.com/saltstack/salt/issues/69033) -- placeholder, so recursive `flush()` of a parent bank actually descends [#69033](https://github.com/saltstack/salt/issues/69033) -- into sub-banks instead of corrupting the path; and `flush(bank)` of a [#69033](https://github.com/saltstack/salt/issues/69033) -- sub-bank now removes the flushed bank's own reference from its [#69033](https://github.com/saltstack/salt/issues/69033) -- parent's index sets so `list_(parent)` no longer reports it as [#69033](https://github.com/saltstack/salt/issues/69033) -- present. Together these fixes restore `cache.list("minions")`, [#69033](https://github.com/saltstack/salt/issues/69033) -- `salt-run manage.present` and `salt-run manage.up` for masters [#69033](https://github.com/saltstack/salt/issues/69033) -- configured with `cache: redis`. [#69033](https://github.com/saltstack/salt/issues/69033) -- Fixed `salt.tokens.rediscluster` being unable to retrieve any eauth [#69035](https://github.com/saltstack/salt/issues/69035) -- token. The cluster client was created with `decode_responses=True`, [#69035](https://github.com/saltstack/salt/issues/69035) -- which caused `redis_client.get()` to return `str` and broke [#69035](https://github.com/saltstack/salt/issues/69035) -- `salt.payload.loads` (msgpack rejects `str`); it also caused [#69035](https://github.com/saltstack/salt/issues/69035) -- `redis_client.keys()` to return `str` and broke [#69035](https://github.com/saltstack/salt/issues/69035) -- `[k.decode("utf8") for k in ...]` (`str` has no `.decode`). Both [#69035](https://github.com/saltstack/salt/issues/69035) -- errors were swallowed by broad `except Exception` handlers, so eauth [#69035](https://github.com/saltstack/salt/issues/69035) -- appeared to silently reject every token. `decode_responses=True` is [#69035](https://github.com/saltstack/salt/issues/69035) -- removed; values now round-trip as bytes through msgpack as the rest [#69035](https://github.com/saltstack/salt/issues/69035) -- of the module already expected. [#69035](https://github.com/saltstack/salt/issues/69035) -- Fixed `salt.returners.redis_return` leaking `:` last-jid [#69038](https://github.com/saltstack/salt/issues/69038) -- pointer keys indefinitely. The pointer was written with `pipeline.set` [#69038](https://github.com/saltstack/salt/issues/69038) -- and no `ex=` TTL, so any (minion, fun) pair that stopped running stuck [#69038](https://github.com/saltstack/salt/issues/69038) -- in Redis forever -- O(minions × distinct funcs) keys accumulating over [#69038](https://github.com/saltstack/salt/issues/69038) -- the lifetime of the master. The pointer now expires on the same TTL [#69038](https://github.com/saltstack/salt/issues/69038) -- as the rest of the returner data (`keep_jobs_seconds`). Operators with [#69038](https://github.com/saltstack/salt/issues/69038) -- external scripts reading these keys directly may observe them [#69038](https://github.com/saltstack/salt/issues/69038) -- expiring; the documentation never promised they would not. [#69038](https://github.com/saltstack/salt/issues/69038) -- Fixed `salt.returners.redis_return.get_fun` always returning an [#69039](https://github.com/saltstack/salt/issues/69039) -- empty dict. The function read return data from a `:` [#69039](https://github.com/saltstack/salt/issues/69039) -- key that no other code in the module ever wrote -- a leftover from [#69039](https://github.com/saltstack/salt/issues/69039) -- an older storage schema. It now reads from the canonical [#69039](https://github.com/saltstack/salt/issues/69039) -- `ret:` hash via `HGET ret: `, matching the [#69039](https://github.com/saltstack/salt/issues/69039) -- storage layout that `returner` actually produces and the read [#69039](https://github.com/saltstack/salt/issues/69039) -- pattern that `get_jid` already uses. [#69039](https://github.com/saltstack/salt/issues/69039) -- ``cmd.run`` and friends no longer include the ``env`` and ``stdin`` arguments in the ``CommandExecutionError`` raised when the underlying subprocess fails to start (typically ``ENOENT`` / binary not found). Both fields routinely carry credentials passed in by the caller (``env={"DB_PASSWORD": "..."}``, password piped via ``stdin``), and the error message ends up in master/minion logs and in event-bus return data visible to the API caller. [#69075](https://github.com/saltstack/salt/issues/69075) -- * Relenv 0.22.14 [#69129](https://github.com/saltstack/salt/issues/69129) -- - Update python 3.14 to 3.14.6 [#69129](https://github.com/saltstack/salt/issues/69129) -- - Update sqlite to 3.53.2.0 [#69129](https://github.com/saltstack/salt/issues/69129) -- - Update openssl to 3.5.7 [#69129](https://github.com/saltstack/salt/issues/69129) -- Fix pillar masking leaking ``**********`` into rendered pillar and state values. ``MaskedDict`` / ``MaskedList`` ``__repr__`` / ``__str__`` now consult the ``salt.utils.secret.mask_pillar`` ContextVar, so ``{{ pillar['list_or_dict_value'] }}`` interpolations on the minion return plain values inside a render bracket. Hoist the ``mask_pillar=False`` bracket from ``render_pillar`` to ``compile_pillar`` so ``ext_pillar`` handlers and the rest of the master-side pillar build also run unmasked. [#69160](https://github.com/saltstack/salt/issues/69160) -- Fixed Windows MSI self-upgrade via ``pkg.install`` failing with error 1603. The old product's ``DeleteConfig_DECAC`` custom action was unconditionally deleting ``ROOTDIR\var`` during ``RemoveExistingProducts``, destroying the MSI that ``pkg.install`` had cached to ``ROOTDIR\var\cache`` before launching the upgrade. Users who had ``REMOVE_CONFIG=1`` persisted in the registry (from checking "On uninstall" at install time) hit a worse variant where the entire ``ROOTDIR`` was deleted. The fix checks ``UPGRADINGPRODUCTCODE`` — set by Windows Installer whenever an uninstall is triggered by a major upgrade — and skips all ``ROOTDIR`` deletion during upgrades, matching the behaviour of the NSIS installer which has always preserved ``ROOTDIR`` during upgrades. [#69219](https://github.com/saltstack/salt/issues/69219) -- Fixed `TypeError: string indices must be integers` in the minion when the master returns a bare string error response (e.g. `"bad load"`, `"Some exception handling minion payload"`) for a pillar request. The minion now raises a clean `AuthenticationError` instead of crashing, allowing the caller to retry or fail gracefully. [#69228](https://github.com/saltstack/salt/issues/69228) -- pkg.list_patches in yumpkg.py parses tdnf output on Photon OS [#69229](https://github.com/saltstack/salt/issues/69229) -- Restore Python dependencies in the PyPI sdist by including ``requirements/*.in`` and ``requirements/**/*.lock`` in ``MANIFEST.in``. After the requirements ``.txt`` → ``.in`` rename, the sdist no longer shipped the files that ``setup.py`` reads to populate ``install_requires``, so ``pip install salt`` produced an installation with no dependencies. [#69244](https://github.com/saltstack/salt/issues/69244) -- Fix `salt-cloud` failing to start with `AttributeError: module 'salt' has no attribute 'minion'` by importing `salt.minion` in `salt.cloud`. [#69281](https://github.com/saltstack/salt/issues/69281) -- Ensure multiple masters have their own job/state queues [#69308](https://github.com/saltstack/salt/issues/69308) -- Fixed minion state queue replacing the master-assigned JID on queued state runs, so returns now come back tagged with the JID the master actually published. [#69386](https://github.com/saltstack/salt/issues/69386) -- Made the salt user's home directory and the relenv ``extras-`` directory configurable in the Linux packaging. The DEB preinst scripts now source ``/etc/default/salt-setup`` (and ``/etc/sysconfig/salt-minion-setup`` for cross-distro parity with RPM) before applying the ``SALT_HOME``/``SALT_USER``/``SALT_GROUP``/``SALT_NAME`` defaults, mirroring the long-standing RPM behavior. A new ``SALT_EXTRAS_DIR`` override is honored by both stacks so the extras tree can be relocated outside ``/opt/saltstack/salt`` and its ownership is correctly restored on upgrade. [#69402](https://github.com/saltstack/salt/issues/69402) - - -### Added - -- Added ``dsc_resource`` execution module and state module for invoking individual [#43718](https://github.com/saltstack/salt/issues/43718) -- PowerShell DSC resources directly via ``Invoke-DscResource``, without compiling [#43718](https://github.com/saltstack/salt/issues/43718) -- a MOF file or involving the Local Configuration Manager. The [#43718](https://github.com/saltstack/salt/issues/43718) -- ``dsc_resource.managed`` state provides idiomatic Salt state management for any [#43718](https://github.com/saltstack/salt/issues/43718) -- installed DSC resource module. [#43718](https://github.com/saltstack/salt/issues/43718) -- fix etcdv3 module authentification when using etcd3-py lib [#69202](https://github.com/saltstack/salt/issues/69202) +- Deferred OpenTelemetry imports in `salt.utils.tracing` and `salt.utils.metrics` so daemons no longer pay the ~15 MB per-process OTel import cost when `tracing.enabled` / `metrics.enabled` are false (the default). On a stress-tested salt-master container (~15 Python processes) this reclaims ~225 MB per subsystem — restoring the pre-3008.x baseline. [#69855](https://github.com/saltstack/salt/issues/69855) diff --git a/pkg/debian/changelog b/pkg/debian/changelog index 6db2e2f828a..2d58a99df30 100644 --- a/pkg/debian/changelog +++ b/pkg/debian/changelog @@ -101,6 +101,16 @@ salt (3008.2) stable; urgency=medium -- Salt Project Packaging Wed, 01 Jul 2026 14:44:25 +0000 +salt (3008.1-1) stable; urgency=medium + + + # Fixed + + * Deferred OpenTelemetry imports in `salt.utils.tracing` and `salt.utils.metrics` so daemons no longer pay the ~15 MB per-process OTel import cost when `tracing.enabled` / `metrics.enabled` are false (the default). On a stress-tested salt-master container (~15 Python processes) this reclaims ~225 MB per subsystem — restoring the pre-3008.x baseline. [#69855](https://github.com/saltstack/salt/issues/69855) + + + -- Salt Project Packaging Thu, 23 Jul 2026 19:20:00 +0000 + salt (3008.1) stable; urgency=medium diff --git a/pkg/rpm/salt.spec b/pkg/rpm/salt.spec index 5e4c59e5f7c..4705bbe25e0 100644 --- a/pkg/rpm/salt.spec +++ b/pkg/rpm/salt.spec @@ -1395,6 +1395,11 @@ fi - Migrate Salt documentation to the PyData Sphinx theme. This update modernizes the documentation UI, improves navigation with a persistent sidebar tree, and fixes issues with embedded video playback. [#69185](https://github.com/saltstack/salt/issues/69185) - fix etcdv3 module authentification when using etcd3-py lib [#69202](https://github.com/saltstack/salt/issues/69202) - Added ``lgpo_reg.get_rsop_value`` to query the Resultant Set of Policy (RSoP) for a registry key/value and detect whether it is managed by a Domain Group Policy Object. The ``lgpo_reg`` module functions ``set_value``, ``disable_value``, and ``delete_value`` now log a warning when a Domain GPO is detected for the target value. The ``lgpo_reg`` state functions ``value_present``, ``value_disabled``, and ``value_absent`` append the same warning to the state comment so it is visible in state output. [#69205](https://github.com/saltstack/salt/issues/69205) +* Thu Jul 23 2026 Salt Project Packaging - 3008.1-1 + +# Fixed + +- Deferred OpenTelemetry imports in `salt.utils.tracing` and `salt.utils.metrics` so daemons no longer pay the ~15 MB per-process OTel import cost when `tracing.enabled` / `metrics.enabled` are false (the default). On a stress-tested salt-master container (~15 Python processes) this reclaims ~225 MB per subsystem — restoring the pre-3008.x baseline. [#69855](https://github.com/saltstack/salt/issues/69855) * Thu Jun 11 2026 Salt Project Packaging - 3008.1 diff --git a/pkg/windows/msi/build_pkg.ps1 b/pkg/windows/msi/build_pkg.ps1 index eaa612c0666..9de1422adc8 100644 --- a/pkg/windows/msi/build_pkg.ps1 +++ b/pkg/windows/msi/build_pkg.ps1 @@ -174,17 +174,19 @@ $RUNTIMES | ForEach-Object { #------------------------------------------------------------------------------- Write-Host "Getting internal version: " -NoNewline -[regex]$tagRE = '(?:[^\d]+)?(?[\d]{1,4})(?:\.(?[\d]{1,2}))?(?:\.(?[\d]{0,2}))?' +[regex]$tagRE = '(?:[^\d]+)?(?[\d]{1,4})(?:\.(?[\d]{1,2}))?(?:\.(?[\d]{0,2}))?(?:-(?[\d]{1,2}))?' $tagREM = $tagRE.Match($Version) $major = $tagREM.groups["major"].ToString() $minor = $tagREM.groups["minor"] $bugfix = $tagREM.groups["bugfix"] -if ([string]::IsNullOrEmpty($minor)) {$minor = 0} +$patch = $tagREM.groups["patch"] +if ([string]::IsNullOrEmpty($minor)) {$minor = 0} if ([string]::IsNullOrEmpty($bugfix)) {$bugfix = 0} +if ([string]::IsNullOrEmpty($patch)) {$patch = 0} # Assumption: major is a number $major1 = $major.substring(0, 2) $major2 = $major.substring(2) -$INTERNAL_VERSION = "$major1.$major2.$minor" +$INTERNAL_VERSION = "$major1.$major2.$minor.$patch" Write-Result $INTERNAL_VERSION -ForegroundColor Green #------------------------------------------------------------------------------- diff --git a/salt/version.py b/salt/version.py index 15d10ee5ae4..db7374b13e5 100644 --- a/salt/version.py +++ b/salt/version.py @@ -252,6 +252,7 @@ class SaltStackVersion: "minor", "bugfix", "mbugfix", + "patch", "pre_type", "pre_num", "noc", @@ -265,6 +266,7 @@ class SaltStackVersion: r"(?:\.(?P[\d]{1,2}))?" r"(?:\.(?P[\d]{0,2}))?" r"(?:\.(?P[\d]{0,2}))?" + r"(?:-(?P[\d]{1,2})\b(?!-g?[a-f0-9]))?" r"(?:(?Prc|a|b|alpha|beta|nb)(?P[\d]+))?" r"(?:(?:.*)(?:\+|-)(?P(?:0na|[\d]+|n/a))(?:-|\.)" + git_sha_regex + r")?" ) @@ -287,6 +289,8 @@ def __init__( pre_num=None, noc=0, sha=None, + *, + patch=None, ): if isinstance(major, str): major = int(major) @@ -313,6 +317,11 @@ def __init__( elif isinstance(mbugfix, str): mbugfix = int(mbugfix) + if patch is None: + patch = 0 + elif isinstance(patch, str): + patch = int(patch) if patch else 0 + if pre_type is None: pre_type = "" if pre_num is None: @@ -331,6 +340,7 @@ def __init__( self.minor = minor self.bugfix = bugfix self.mbugfix = mbugfix + self.patch = patch self.pre_type = pre_type self.pre_num = pre_num if self.new_version(major): @@ -365,7 +375,18 @@ def parse(cls, version_string): match = cls.git_describe_regex.match(vstr) if not match: raise ValueError(f"Unable to parse version string: '{version_string}'") - return cls(*match.groups()) + g = match.groupdict() + return cls( + g["major"], + g["minor"], + g["bugfix"], + g["mbugfix"], + g["pre_type"], + g["pre_num"], + g["noc"], + g["sha"], + patch=g["patch"], + ) @classmethod def from_name(cls, name): @@ -462,6 +483,8 @@ def string(self): version_string = f"{self.major}.{self.minor}.{self.bugfix}" if self.mbugfix: version_string += f".{self.mbugfix}" + if self.patch: + version_string += f"-{self.patch}" if self.pre_type: version_string += f"{self.pre_type}{self.pre_num}" if self.noc is not None and self.sha: @@ -537,6 +560,8 @@ def __compare__(self, other, method): # The other side has pre-release information, we don't noc_info[pre_type] = "zzzzz" + if tuple(noc_info) == tuple(other_noc_info): + return method(self.patch or 0, other.patch or 0) return method(tuple(noc_info), tuple(other_noc_info)) def __lt__(self, other): @@ -663,8 +688,8 @@ def __discover_version(saltstack_version): saltstack_version.minor, saltstack_version.bugfix, saltstack_version.mbugfix, - saltstack_version.pre_type, - saltstack_version.pre_num, + pre_type=saltstack_version.pre_type, + pre_num=saltstack_version.pre_num, noc=parsed.noc, sha=parsed.sha, ) diff --git a/tests/pytests/pkg/integration/test_pkg_meta.py b/tests/pytests/pkg/integration/test_pkg_meta.py index 01b5107178c..883aa067c0d 100644 --- a/tests/pytests/pkg/integration/test_pkg_meta.py +++ b/tests/pytests/pkg/integration/test_pkg_meta.py @@ -82,14 +82,21 @@ def package(install_salt, artifact_version, pkg_arch): match the real file from ``install_salt.pkgs`` instead of string-building. """ rpm_re = re.compile( - rf"^salt-\d.*-0\.{re.escape(pkg_arch)}\.rpm$", + rf"^salt-\d.*-\d+\.{re.escape(pkg_arch)}\.rpm$", re.IGNORECASE, ) for pkg_path in install_salt.pkgs: path = pathlib.Path(pkg_path) if rpm_re.match(path.name): return path - name = f"salt-{artifact_version}-0.{pkg_arch}.rpm" + import packaging.version as _pv + + _parsed = _pv.parse(artifact_version) + if _parsed.post is not None: + _base = ".".join(str(p) for p in _parsed.release) + name = f"salt-{_base}-{_parsed.post}.{pkg_arch}.rpm" + else: + name = f"salt-{artifact_version}-0.{pkg_arch}.rpm" return ARTIFACTS_DIR / name diff --git a/tests/pytests/pkg/integration/test_version.py b/tests/pytests/pkg/integration/test_version.py index 9370c9c92dc..5eb059f5658 100644 --- a/tests/pytests/pkg/integration/test_version.py +++ b/tests/pytests/pkg/integration/test_version.py @@ -223,3 +223,38 @@ def test_compare_pkg_versions_redhat_rc(version, install_salt): comp_pkg = pkg.split("~")[0] ret = install_salt.proc.run("rpmdev-vercmp", pkg, comp_pkg) ret.stdout.matcher.fnmatch_lines([f"{pkg} < {comp_pkg}"]) + + +@pytest.mark.skip_unless_on_linux +@pytest.mark.skip_if_binaries_missing("rpmdev-vercmp") +def test_compare_pkg_versions_redhat_patch(version, install_salt): + """ + Test that patch releases (Release: N) sort above the base (Release: 0). + For example, salt-3008.1-1.x86_64.rpm must be greater than salt-3008.1-0.x86_64.rpm. + """ + if install_salt.distro_id not in ( + "almalinux", + "rocky", + "centos", + "redhat", + "amzn", + "fedora", + "photon", + ): + pytest.skip("Only tests rpm packages") + + pkg = [x for x in install_salt.pkgs if "rpm" in x] + if not pkg: + pytest.skip("Not testing rpm packages") + import packaging.version + + parsed = packaging.version.parse(version) + if parsed.post is None: + pytest.skip("Not a patch release") + pkg_name = pkg[0].split("/")[-1] + assert ( + f"-{parsed.post}." in pkg_name + ), f"Expected Release={parsed.post} in package name {pkg_name!r}" + base_pkg = pkg_name.replace(f"-{parsed.post}.", "-0.", 1) + ret = install_salt.proc.run("rpmdev-vercmp", pkg_name, base_pkg) + ret.stdout.matcher.fnmatch_lines([f"{pkg_name} > {base_pkg}"]) diff --git a/tests/pytests/unit/test_version.py b/tests/pytests/unit/test_version.py index 918792a62af..535629b6517 100644 --- a/tests/pytests/unit/test_version.py +++ b/tests/pytests/unit/test_version.py @@ -599,3 +599,32 @@ def test_parsed_version_name(version_str, expected_str, expected_name): assert ver.name == expected_name else: assert ver.name is None + + +@pytest.mark.parametrize( + "version_string,patch,version_str,codename", + [ + ("v3008.1-1", 1, "3008.1-1", "Argon"), + ("v3008.1-2", 2, "3008.1-2", "Argon"), + ("3008.1-1", 1, "3008.1-1", "Argon"), + ], +) +def test_patch_version_parsing(version_string, patch, version_str, codename): + v = SaltStackVersion.parse(version_string) + assert v.patch == patch + assert v.string == version_str + assert v.name == codename + + +@pytest.mark.parametrize( + "higher,lower", + [ + ("v3008.1-2", "v3008.1-1"), + ("v3008.1-1", "v3008.1"), + ("v3008.2", "v3008.1-99"), + ("v3008.1", "v3008.1rc1"), + ], +) +def test_patch_version_ordering(higher, lower): + assert SaltStackVersion.parse(higher) > SaltStackVersion.parse(lower) + assert SaltStackVersion.parse(lower) < SaltStackVersion.parse(higher) diff --git a/tests/support/pkg.py b/tests/support/pkg.py index 570803c9fa7..d9bf965dc50 100644 --- a/tests/support/pkg.py +++ b/tests/support/pkg.py @@ -393,13 +393,27 @@ def _default_artifact_version(self): version = "" artifacts = list(ARTIFACTS_DIR.glob("**/*.*")) for artifact in artifacts: - version = re.search( + m = re.search( r"([0-9].*)(\-[0-9].fc|\-[0-9].el|\+ds|\_all|\_any|\_amd64|\_arm64|\-[0-9].am|(\-[0-9]-[a-z]*-[a-z]*[0-9_]*.|\-[0-9]*.*)(exe|msi|pkg|rpm|deb))", artifact.name, ) - if version: - version = version.groups()[0].replace("_", "-").replace("~", "") - version = version.split("-")[0] + if m: + version = m.groups()[0].replace("_", "-").replace("~", "") + # For RPM-family artifacts the release segment (-N.el, -N.fc, -N.am) + # ends up in group 2, not group 1. Reconstruct "version-release" for + # patch releases (release > 0) so "3008.1-1" is not collapsed to "3008.1". + rpm_rel_m = re.match(r"^-(\d+)\.", m.group(2) or "") + if rpm_rel_m and int(rpm_rel_m.group(1)) > 0: + version = f"{version}-{rpm_rel_m.group(1)}" + else: + # For non-RPM artifacts the patch suffix (-1) may already be + # in group 1 (e.g. "3008.1-1" or "3008.1-1-macos"). Preserve a + # purely-numeric first hyphen segment; strip platform suffixes. + parts = version.split("-") + if len(parts) >= 2 and parts[1].isdigit(): + version = f"{parts[0]}-{parts[1]}" + else: + version = parts[0] break if not version: pytest.fail( @@ -1201,6 +1215,14 @@ def install_previous(self, downgrade=False): "salt-repo-3007-sts", ) self._check_retcode(ret) + # Newer salt.repo files also enable salt-repo-3008-lts; disable it + # so unversioned `yum install salt` stays on the 3007.x STS channel. + self.proc.run( + self.pkg_mngr, + "config-manager", + "--disable", + "salt-repo-3008-lts", + ) elif major_ver >= 3008: # Default ``salt.repo`` enables v3006 LTS only; that stanza excludes # ``*3008*``. Published 3008.x RPMs (including pre-releases) are only @@ -1220,22 +1242,22 @@ def install_previous(self, downgrade=False): "salt-repo-3007-sts", ) self._check_retcode(ret) + # Newer salt.repo files also enable salt-repo-3008-lts; enable it only + # if installing 3008.x, otherwise disable to stay on the correct channel. if "3008" in self.prev_version: - ret = self.proc.run( + self.proc.run( self.pkg_mngr, "config-manager", "--enable", "salt-repo-3008-lts", ) - self._check_retcode(ret) else: - ret = self.proc.run( + self.proc.run( self.pkg_mngr, "config-manager", "--disable", "salt-repo-3008-lts", ) - self._check_retcode(ret) ret = self.proc.run(self.pkg_mngr, "clean", "expire-cache") self._check_retcode(ret) # Unversioned ``yum downgrade`` only moves one step among *all* repo diff --git a/tools/changelog.py b/tools/changelog.py index 2435a19d0a2..d9f634c4a68 100644 --- a/tools/changelog.py +++ b/tools/changelog.py @@ -68,9 +68,11 @@ def _get_pkg_changelog_contents(ctx: Context, version: Version): def _get_salt_version(ctx, next_release=False): - args = [] - if next_release: - args.append("--next-release") + if not next_release: + version_file = REPO_ROOT / "salt" / "_version.txt" + if version_file.exists(): + return Version(version_file.read_text(encoding="utf-8").strip()) + args = ["--next-release"] if next_release else [] ret = ctx.run("python3", "salt/version.py", *args, capture=True, check=False) if ret.returncode: ctx.error(ret.stderr.decode()) @@ -95,18 +97,32 @@ def _get_salt_version(ctx, next_release=False): }, ) def update_rpm(ctx: Context, salt_version: Version, draft: bool = False): + import re as _re + if salt_version is None: salt_version = _get_salt_version(ctx) changes = _get_pkg_changelog_contents(ctx, salt_version) - str_salt_version = str(salt_version).replace("rc", "~rc") + + if salt_version.post is not None: + rpm_version = ".".join(str(p) for p in salt_version.release) + rpm_release = str(salt_version.post) + str_salt_version = f"{rpm_version}-{rpm_release}" + else: + rpm_version = str(salt_version).replace("rc", "~rc") + rpm_release = "0" + str_salt_version = rpm_version + ctx.info(f"Salt version is {str_salt_version}") orig = ctx.run( "sed", - f"s/Version: .*/Version: {str_salt_version}/g", + f"s/Version: .*/Version: {rpm_version}/g", "pkg/rpm/salt.spec", capture=True, check=True, ).stdout.decode() + orig = _re.sub( + r"^Release:.*$", f"Release: {rpm_release}", orig, count=1, flags=_re.MULTILINE + ) dt = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) date = dt.strftime("%a %b %d %Y") header = f"* {date} Salt Project Packaging - {str_salt_version}\n" diff --git a/tools/ci.py b/tools/ci.py index 534cab7c544..08979e27da9 100644 --- a/tools/ci.py +++ b/tools/ci.py @@ -11,6 +11,7 @@ import pathlib import pprint import random +import re import shutil import sys import time @@ -233,6 +234,11 @@ def get_release_changelog_target(ctx: Context, event_name: str): ) release_branches = shared_context["release_branches"] + # Patch release branches look like "3008.1-1" or "3008.1-patch". The + # major prefix (e.g. "3008") is enough to associate them with the correct + # release family; extract it once for the else-branch below. + _patch_branch_re = re.compile(r"refs/heads/(\d{4})\.\d") + release_changelog_target = "next-major-release" if event_name == "pull_request": if gh_event["pull_request"]["base"]["ref"] in release_branches: @@ -242,10 +248,21 @@ def get_release_changelog_target(ctx: Context, event_name: str): if branch_name in release_branches: release_changelog_target = "next-minor-release" else: + ref = gh_event.get("ref", "") for branch_name in release_branches: - if branch_name in gh_event["ref"]: + if branch_name in ref: release_changelog_target = "next-minor-release" break + else: + # Patch release branches (e.g. refs/heads/3008.1-1) share the + # major version with a release branch but differ in the minor part. + m = _patch_branch_re.match(ref) + if m: + major = m.group(1) + for branch_name in release_branches: + if branch_name.startswith(major + "."): + release_changelog_target = "next-minor-release" + break with open(github_output, "a", encoding="utf-8") as wfh: wfh.write(f"release-changelog-target={release_changelog_target}\n") ctx.exit(0) diff --git a/tools/pkg/__init__.py b/tools/pkg/__init__.py index b66a6a78657..ce24bc98750 100644 --- a/tools/pkg/__init__.py +++ b/tools/pkg/__init__.py @@ -181,7 +181,11 @@ def set_salt_version( ctx.info(f"Successfuly wrote {salt_version!r} to 'salt/_version.txt'") version_instance = tools.utils.Version(salt_version) - if release and not version_instance.is_prerelease: + if ( + release + and not version_instance.is_prerelease + and not version_instance.is_postrelease + ): with open( tools.utils.REPO_ROOT / "salt" / "version.py", "r+", encoding="utf-8" ) as rwfh: @@ -421,6 +425,24 @@ def source_tarball(ctx: Context): for pkg in tools.utils.REPO_ROOT.joinpath("dist").iterdir() ] ctx.run("sha256sum", *packages) + # setuptools normalizes "3008.1-1" → "3008.1.post1" per PEP 440. + # Rename back to the hyphenated form so artifact names stay consistent. + version_file = tools.utils.REPO_ROOT / "salt" / "_version.txt" + if version_file.exists(): + import packaging.version as _pv + + raw = version_file.read_text(encoding="utf-8").strip() + parsed = _pv.parse(raw) + if parsed.post is not None: + dist_dir = tools.utils.REPO_ROOT / "dist" + pep440_name = f"salt-{parsed!s}.tar.gz" + hyphen_name = f"salt-{raw}.tar.gz" + src = dist_dir / pep440_name + dst = dist_dir / hyphen_name + if src.exists() and not dst.exists(): + ctx.info(f"Renaming {pep440_name} → {hyphen_name}") + src.rename(dst) + ctx.run("python3", "-m", "twine", "check", "dist/*", check=True) diff --git a/tools/pkg/build.py b/tools/pkg/build.py index 303d1a32101..3a68ca812c7 100644 --- a/tools/pkg/build.py +++ b/tools/pkg/build.py @@ -155,7 +155,15 @@ def debian( ) ctx.run("ln", "-sf", "pkg/debian/", ".") - ctx.run("debuild", *env_args, "-uc", "-us", env=env) + debuild_flags = ["-uc", "-us"] + try: + import packaging.version as _pv + + if _pv.parse(os.environ.get("SALT_VERSION", "")).post is not None: + debuild_flags.insert(0, "-b") + except Exception: + pass + ctx.run("debuild", *env_args, *debuild_flags, env=env) ctx.info("Done")