Reapply "nixos/seerr: use lib.mkStateRevisionOption"

This reverts commit 8820b84c40.
This commit is contained in:
Ryan Hendrickson
2026-08-09 00:44:15 -04:00
parent d3c93fb773
commit 7532d3bfc2
10 changed files with 301 additions and 5 deletions

View File

@@ -0,0 +1,57 @@
# State revision {#sec-state-revision}
NixOS includes a {option}`system.stateVersion` option, used by some modules for a
variety of reasons related to non-backward-compatible changes to software or
the module itself.
Module authors are discouraged from adding new uses of
{option}`system.stateVersion` to their module.
However, when the alternatives are impractical, modules that wish to consume
{option}`system.stateVersion` should instead define their own `stateRevision`
option using `utils.mkStateRevisionOption`.
There should be no uses of `config.system.stateVersion` directly in the module.
(Note the name difference: the {option}`system.stateVersion` option, with a V,
takes a value that looks like "YY.MM".
A `stateRevision` option, with an R, takes a non-negative integer value.)
Modules should also add the value of their `stateRevision` option to
`system.moduleStateRevisions."your.module.stateRevision"`, when the module is
enabled.
This is a purely informative option that exists to help describe the effects of
changing {option}`system.stateVersion`.
Example:
```nix
{
lib,
config,
utils,
...
}:
let
cfg = config.services.whatever;
in
{
options.services.whatever = {
enable = lib.mkEnableOption "whatever, a service that does whatever";
stateRevision = utils.mkStateRevisionOption {
descriptionName = "the whatever service";
migrations = {
"26.05" = "Rename `/var/lib/old_name` to `/var/lib/new_name`.";
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.whatever = {
# ...
serviceConfig.StateDirectory = if cfg.stateRevision < 1 then "old_name" else "new_name";
};
# Important: this is inside the `lib.mkIf cfg.enable`
system.moduleStateRevisions."services.whatever.stateRevision" = cfg.stateRevision;
};
}
```

View File

@@ -220,4 +220,5 @@ importing-modules.section.md
replace-modules.section.md
freeform-modules.section.md
settings-options.section.md
state-revision.section.md
```

View File

@@ -253,6 +253,9 @@
"sec-override-nixos-test": [
"index.html#sec-override-nixos-test"
],
"sec-state-revision": [
"index.html#sec-state-revision"
],
"sec-wireless-declarative": [
"index.html#sec-wireless-declarative"
],

View File

@@ -6,8 +6,10 @@
let
inherit (lib)
all
any
attrNames
concatImapStringsSep
concatMapStringsSep
concatStringsSep
elem
@@ -27,8 +29,11 @@ let
isList
isPath
isString
length
listToAttrs
literalMD
mapAttrs
mkOption
nameValuePair
optionalString
removePrefix
@@ -36,8 +41,10 @@ let
splitString
stringToCharacters
types
versionOlder
;
inherit (lib.lists) findFirstIndex;
inherit (lib.strings) toJSON escapeC;
in
@@ -604,6 +611,123 @@ let
lib.listToAttrs
];
};
/**
Creates a per-module `stateRevision` option that takes an int value, with a
default that is derived from `system.stateVersion`.
# Inputs
`descriptionName`
: A human-friendly name for your module, used for the description of the
created option.
`migrations`
: Attribute set that maps from values of `system.stateVersion`
(representing the breakpoints at which the default value of this option
will change) to Markdown instructions to users for manually migrating
their data to this breakpoint. The migration instructions will be
included in the NixOS documentation for this option. (These instructions
must only contain Markdown inlines, because they will be rendered in a
table. In particular, lists will not render correctly.)
`migrations` will also be exposed as an attribute on the result.
# Examples
:::{.example}
## `lib.options.mkStateRevisionOption` usage example
```nix
exampleModule =
{ lib, config, utils, ... }:
{
options.services.whatever = {
stateRevision = utils.mkStateRevisionOption {
descriptionName = "the whatever service";
migrations = {
"26.05" = "Rename `/var/lib/old_name` to `/var/lib/new_name`.";
"26.11" = "Run the `upgrade_whatever` utility.";
};
};
};
};
}
(pkgs.nixos [
exampleModule
{ system.stateVersion = "25.11"; }
]).config.services.whatever.stateRevision # => 0
(pkgs.nixos [
exampleModule
{ system.stateVersion = "26.05"; }
]).config.services.whatever.stateRevision # => 1
(pkgs.nixos [
exampleModule
{ system.stateVersion = "27.05"; }
]).config.services.whatever.stateRevision # => 2
```
:::
Modules should use this function when they change how data managed by the
module is persisted on the system between NixOS releases.
The default value of the option will be the number of attributes in the
`migrations` parameter with name less than or equal to the value of
`system.stateVersion`.
When using this function, don't forget to add the option's value to
`system.moduleStateRevisions."your.module.stateRevision"` when your module is
enabled.
*/
mkStateRevisionOption =
{
descriptionName,
migrations,
}:
let
versions = attrNames migrations;
maxVal = length versions;
in
assert all (v: builtins.match "[0-9]{2}\\.[0-9]{2}" v != null) versions;
mkOption {
type = types.ints.between 0 maxVal;
description = ''
This option versions the format of state persisted by
${descriptionName}. Its default value depends on the value of
{option}`system.stateVersion`.
Users who wish to increment this option will need to take manual
migration steps to preserve their data. **If you perform these
migrations, rolling back to an older generation will require also
reversing the migrations to the state expected by that generation.**
The migrations needed to advance to each value of this option are as
follows (perform all instructions after the row for the current
`stateRevision`, up to and including the row for the new
`stateRevision`):
| `stateRevision` | Migration instructions |
|-----------------|------------------------|
| 0 | (none) |
${concatImapStringsSep "\n" (
v: sv: "| ${toString v} | ${replaceStrings [ "\n" ] [ " " ] migrations.${sv}} |"
) versions}
Note that you do **not** need to change {option}`system.stateVersion`
in order to update this option. {option}`system.stateVersion` only
determines the default value of this option. Most users should not
change {option}`system.stateVersion` at all.
'';
default = findFirstIndex (versionOlder config.system.stateVersion) maxVal versions;
defaultText = literalMD ''
If {option}`system.stateVersion` is:
${concatImapStringsSep "\n" (v: sv: "* &lt;${sv}: ${toString (v - 1)}") versions}
* otherwise: ${toString maxVal}
'';
}
// {
inherit migrations;
};
};
in
utils

View File

@@ -254,6 +254,30 @@ in
'';
};
moduleStateRevisions = mkOption {
type =
let
baseType = types.attrsOf types.ints.unsigned;
isStateRevisionOption = x: lib.isOption x && x ? migrations;
in
types.addCheck baseType (
attrs:
builtins.all (
attrPath: isStateRevisionOption (lib.attrByPath (lib.splitString "." attrPath) null options)
) (builtins.attrNames attrs)
)
// {
description = "${baseType.description}, in which every attribute name is the path to an option created with mkStateRevisionOption";
};
default = { };
internal = true;
description = ''
NixOS modules should set attributes on this option. Users should leave
it alone. Future tooling may use it to determine the consequences of
updating {option}`system.stateVersion`.
'';
};
configurationRevision = mkOption {
type = types.nullOr types.str;
default = null;

View File

@@ -2,13 +2,14 @@
config,
pkgs,
lib,
utils,
...
}:
let
cfg = config.services.seerr;
# 26.05 introduced a breaking change which is guarded behind stateVersion to avoid
# breaking users.
useNewConfigLocation = lib.versionAtLeast config.system.stateVersion "26.05";
# 26.05 introduced a breaking change which is guarded behind stateRevision to
# avoid breaking users.
useNewConfigLocation = cfg.stateRevision >= 1;
in
{
imports = [
@@ -39,8 +40,23 @@ in
configDir = lib.mkOption {
type = lib.types.path;
default = if useNewConfigLocation then "/var/lib/seerr/" else "/var/lib/jellyseerr/config";
defaultText = lib.literalMD "{file}`/var/lib/seerr` (or {file}`/var/lib/jellyseerr/config` if {option}`services.seerr.stateRevision` < 1)";
description = "Config data directory";
};
stateRevision = utils.mkStateRevisionOption {
descriptionName = "Seerr";
migrations = {
"26.05" = ''
Move {file}`/var/lib/private/jellyseerr/config` to
{file}`/var/lib/private/seerr`, if you have not set
{option}`services.seerr.configDir`. (If you have set
{option}`services.seerr.configDir`, you should also have forced
{option}`systemd.services.seerr.serviceConfig.StateDirectory`, and in
that case `stateRevision` does not affect your configuration.)
'';
};
};
};
config = lib.mkIf cfg.enable {
@@ -81,5 +97,7 @@ in
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ];
};
system.moduleStateRevisions."services.seerr.stateRevision" = cfg.stateRevision;
};
}

View File

@@ -1059,6 +1059,7 @@ in
modularService = pkgs.callPackage ../modules/system/service/systemd/test.nix {
inherit evalSystem;
};
moduleStateRevisions = pkgs.callPackage ./moduleStateRevisions.nix { };
molly-brown = runTest ./molly-brown.nix;
mollysocket = runTest ./mollysocket.nix;
monado = runTest ./monado.nix;
@@ -1842,7 +1843,7 @@ in
userborn-mutable-users = runTest ./userborn-mutable-users.nix;
userborn-static = runTest ./userborn-static.nix;
ustreamer = runTest ./ustreamer.nix;
utils = import ./utils { inherit runTest; };
utils = pkgs.callPackage ./utils { inherit runTest; };
utmp = runTest ./utmp.nix;
uwsgi = runTest ./uwsgi.nix;
v2ray = runTest ./v2ray.nix;

View File

@@ -0,0 +1,25 @@
{
lib,
emptyFile,
nixos,
}:
let
evalModuleStateRevisions =
cfg:
(nixos [
{ system.stateVersion = lib.trivial.release; }
cfg
]).config.system.moduleStateRevisions;
# For modules that follow the <module>.enable, <module>.stateRevision pattern:
testModule =
path:
evalModuleStateRevisions (lib.setAttrByPath path { enable = true; })
? "${builtins.concatStringsSep "." path}.stateRevision";
in
assert evalModuleStateRevisions { } == { };
assert testModule [
"services"
"seerr"
];
emptyFile

View File

@@ -1,5 +1,9 @@
{ runTest }:
{
callPackage,
runTest,
}:
{
genJqSecretsReplacement = runTest ./genJqSecretsReplacement.nix;
mkStateRevisionOption = callPackage ./mkStateRevisionOption.nix { };
}

View File

@@ -0,0 +1,39 @@
{
emptyFile,
nixos,
}:
let
result = nixos (
{ utils, ... }:
{
options = {
stateRevision1 = utils.mkStateRevisionOption {
descriptionName = "...";
migrations = {
"27.05" = "...";
};
};
stateRevision2 = utils.mkStateRevisionOption {
descriptionName = "...";
migrations = {
"26.11" = "...";
};
};
stateRevision3 = utils.mkStateRevisionOption {
descriptionName = "...";
migrations = {
"24.05" = "...";
"27.05" = "...";
};
};
};
config.system.stateVersion = "26.11";
}
);
inherit (result) config options;
in
assert config.stateRevision1 == 0;
assert config.stateRevision2 == 1;
assert config.stateRevision3 == 1;
assert options.stateRevision1.migrations == { "27.05" = "..."; };
emptyFile