confctl 2.2.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. checksums.yaml +4 -4
  2. data/.git-hooks/pre_commit/nixfmt.rb +13 -0
  3. data/.github/workflows/rspec.yml +64 -0
  4. data/.github/workflows/rubocop.yml +27 -0
  5. data/.github/workflows/tests.yml +139 -0
  6. data/.gitignore +12 -7
  7. data/.overcommit.yml +2 -0
  8. data/.rspec +1 -0
  9. data/.rubocop.yml +8 -1
  10. data/AGENTS.md +42 -0
  11. data/CHANGELOG.md +57 -0
  12. data/Gemfile +3 -2
  13. data/Gemfile.lock +198 -0
  14. data/README.md +166 -58
  15. data/Rakefile +5 -0
  16. data/confctl.gemspec +20 -2
  17. data/docs/carrier.md +3 -3
  18. data/docs/flake-inputs.md +159 -0
  19. data/docs/swpins-to-flakes.md +325 -0
  20. data/example/cluster/module-list.nix +2 -1
  21. data/example/cluster/nixos-machine/config.nix +12 -2
  22. data/example/cluster/nixos-machine/hardware.nix +6 -1
  23. data/example/cluster/vpsadminos-container/config.nix +6 -1
  24. data/example/cluster/vpsadminos-container/module.nix +4 -1
  25. data/example/cluster/vpsadminos-machine/config.nix +6 -1
  26. data/example/cluster/vpsadminos-machine/hardware.nix +6 -1
  27. data/example/cluster/vpsadminos-machine/module.nix +5 -2
  28. data/example/cluster/vpsfreecz-vps/config.nix +6 -1
  29. data/example/cluster/vpsfreecz-vps/module.nix +4 -1
  30. data/example/configs/swpins.nix +8 -3
  31. data/example/environments/base.nix +6 -1
  32. data/example/swpins/core.json +35 -0
  33. data/example-flake/.gitignore +2 -0
  34. data/example-flake/README.md +38 -0
  35. data/example-flake/cluster/cluster.nix +5 -0
  36. data/example-flake/cluster/module-list.nix +4 -0
  37. data/example-flake/cluster/nested/nixos-machine/config.nix +25 -0
  38. data/example-flake/cluster/nested/nixos-machine/hardware.nix +9 -0
  39. data/example-flake/cluster/nested/nixos-machine/module.nix +8 -0
  40. data/example-flake/cluster/nixos-machine/config.nix +25 -0
  41. data/example-flake/cluster/nixos-machine/hardware.nix +9 -0
  42. data/example-flake/cluster/nixos-machine/module.nix +8 -0
  43. data/example-flake/cluster/vpsadminos-container/config.nix +28 -0
  44. data/example-flake/cluster/vpsadminos-container/module.nix +8 -0
  45. data/example-flake/cluster/vpsadminos-machine/config.nix +27 -0
  46. data/example-flake/cluster/vpsadminos-machine/hardware.nix +9 -0
  47. data/example-flake/cluster/vpsadminos-machine/module.nix +8 -0
  48. data/example-flake/cluster/vpsfreecz-vps/config.nix +31 -0
  49. data/example-flake/cluster/vpsfreecz-vps/module.nix +8 -0
  50. data/example-flake/configs/confctl.nix +10 -0
  51. data/example-flake/data/default.nix +5 -0
  52. data/example-flake/data/ssh-keys.nix +7 -0
  53. data/example-flake/environments/base.nix +18 -0
  54. data/example-flake/flake.lock +75 -0
  55. data/example-flake/flake.nix +36 -0
  56. data/example-flake/modules/module-list.nix +13 -0
  57. data/example-flake/shell.nix +11 -0
  58. data/flake.lock +159 -0
  59. data/flake.nix +189 -0
  60. data/gemset.nix +1022 -0
  61. data/lib/confctl/cli/app.rb +145 -0
  62. data/lib/confctl/cli/attr_filters.rb +1 -1
  63. data/lib/confctl/cli/cluster.rb +681 -108
  64. data/lib/confctl/cli/command.rb +24 -2
  65. data/lib/confctl/cli/configuration.rb +171 -106
  66. data/lib/confctl/cli/generation.rb +65 -1
  67. data/lib/confctl/cli/inputs/channels.rb +190 -0
  68. data/lib/confctl/cli/inputs/machines.rb +83 -0
  69. data/lib/confctl/cli/inputs/root.rb +101 -0
  70. data/lib/confctl/cli/inputs.rb +5 -0
  71. data/lib/confctl/cli/log_view.rb +21 -10
  72. data/lib/confctl/cli/migrate/swpins_to_flakes.rb +866 -0
  73. data/lib/confctl/cli/migrate.rb +5 -0
  74. data/lib/confctl/cli/output_formatter.rb +5 -7
  75. data/lib/confctl/cli/swpins/base.rb +9 -0
  76. data/lib/confctl/cli/swpins/channel.rb +2 -5
  77. data/lib/confctl/cli/swpins/cluster.rb +2 -5
  78. data/lib/confctl/cli/swpins/core.rb +2 -5
  79. data/lib/confctl/config_type.rb +7 -0
  80. data/lib/confctl/flake_lock.rb +78 -0
  81. data/lib/confctl/flake_lock_diff.rb +36 -0
  82. data/lib/confctl/generation/build.rb +131 -24
  83. data/lib/confctl/generation/build_list.rb +4 -3
  84. data/lib/confctl/generation/unified.rb +14 -1
  85. data/lib/confctl/git_repo_mirror.rb +2 -2
  86. data/lib/confctl/health_checks/run_command.rb +3 -2
  87. data/lib/confctl/health_checks/systemd/properties.rb +1 -1
  88. data/lib/confctl/health_checks/systemd/property_list.rb +2 -2
  89. data/lib/confctl/inputs/commit_message.rb +125 -0
  90. data/lib/confctl/inputs/git_commit.rb +17 -0
  91. data/lib/confctl/inputs/nix_output_guard.rb +37 -0
  92. data/lib/confctl/inputs/setter.rb +179 -0
  93. data/lib/confctl/inputs/updater.rb +76 -0
  94. data/lib/confctl/inputs.rb +5 -0
  95. data/lib/confctl/inputs_info.rb +50 -0
  96. data/lib/confctl/line_buffer.rb +1 -1
  97. data/lib/confctl/machine.rb +18 -3
  98. data/lib/confctl/machine_control.rb +8 -2
  99. data/lib/confctl/machine_list.rb +2 -2
  100. data/lib/confctl/machine_status.rb +63 -20
  101. data/lib/confctl/nix/args.rb +48 -0
  102. data/lib/confctl/nix.rb +30 -437
  103. data/lib/confctl/nix_build_flake.rb +95 -0
  104. data/lib/confctl/nix_copy.rb +14 -2
  105. data/lib/confctl/nix_flake.rb +449 -0
  106. data/lib/confctl/nix_format.rb +3 -3
  107. data/lib/confctl/nix_legacy.rb +467 -0
  108. data/lib/confctl/swpins/change_set.rb +29 -3
  109. data/lib/confctl/swpins/specs/base.rb +2 -2
  110. data/lib/confctl/ui.rb +19 -0
  111. data/lib/confctl/version.rb +1 -1
  112. data/man/index.html +11 -0
  113. data/man/man8/confctl-options.nix.8.html +113 -0
  114. data/man/man8/confctl.8 +152 -21
  115. data/man/man8/confctl.8.html +355 -0
  116. data/man/man8/confctl.8.md +148 -17
  117. data/man/style.css +301 -0
  118. data/nix/evaluator.nix +94 -69
  119. data/nix/flake/mk-confctl-devshell.nix +85 -0
  120. data/nix/flake/mk-confctl-outputs.nix +522 -0
  121. data/nix/flake/mk-config-devshell.nix +168 -0
  122. data/nix/lib/default.nix +118 -65
  123. data/nix/lib/machine/default.nix +65 -45
  124. data/nix/lib/machine/info.nix +16 -5
  125. data/nix/lib/swpins/eval.nix +42 -29
  126. data/nix/lib/swpins/options.nix +6 -2
  127. data/nix/machines.nix +23 -15
  128. data/nix/modules/cluster/default.nix +104 -41
  129. data/nix/modules/confctl/carrier/base.nix +11 -4
  130. data/nix/modules/confctl/carrier/carrier-env.rb +2 -2
  131. data/nix/modules/confctl/carrier/netboot/build-netboot-server.rb +50 -16
  132. data/nix/modules/confctl/carrier/netboot/nixos.nix +56 -24
  133. data/nix/modules/confctl/configuration-info.nix +17 -0
  134. data/nix/modules/confctl/generations.nix +2 -2
  135. data/nix/modules/confctl/host.nix +13 -0
  136. data/nix/modules/confctl/inputs-info.nix +21 -0
  137. data/nix/modules/confctl/kexec-netboot/default.nix +13 -6
  138. data/nix/modules/confctl/kexec-netboot/kexec-netboot.8.adoc +3 -0
  139. data/nix/modules/confctl/kexec-netboot/kexec-netboot.rb +25 -16
  140. data/nix/modules/confctl/nix.nix +31 -1
  141. data/nix/modules/confctl/swpins.nix +13 -6
  142. data/nix/modules/module-list.nix +4 -2
  143. data/nix/modules/system-list.nix +4 -1
  144. data/nix/package.nix +37 -0
  145. data/shell.nix +39 -8
  146. data/skills/confctl-configuration-update/SKILL.md +228 -0
  147. data/skills/confctl-configuration-update/agents/openai.yaml +4 -0
  148. data/skills/confctl-release/SKILL.md +102 -0
  149. data/skills/confctl-release/agents/openai.yaml +4 -0
  150. data/spec/confctl/cli/cluster_health_check_spec.rb +43 -0
  151. data/spec/confctl/cli/cluster_skip_current_deploy_spec.rb +157 -0
  152. data/spec/confctl/cli/cluster_status_flake_spec.rb +83 -0
  153. data/spec/confctl/cli/inputs_set_output_spec.rb +70 -0
  154. data/spec/confctl/configuration_spec.rb +45 -0
  155. data/spec/confctl/inputs_spec.rb +104 -0
  156. data/spec/confctl/machine_control_spec.rb +112 -0
  157. data/spec/confctl/machine_list_spec.rb +47 -0
  158. data/spec/confctl/nix_flake_spec.rb +41 -0
  159. data/spec/confctl/swpins_spec.rb +106 -0
  160. data/spec/generation/build_modes_spec.rb +132 -0
  161. data/spec/inputs/commit_message_spec.rb +205 -0
  162. data/spec/inputs/nix_cached_fallback_spec.rb +50 -0
  163. data/spec/inputs/nix_output_guard_spec.rb +45 -0
  164. data/spec/spec_helper.rb +12 -0
  165. data/spec/support/cli_helper.rb +57 -0
  166. data/test-runner.sh +7 -0
  167. data/tests/all-tests.nix +30 -0
  168. data/tests/make-test.nix +15 -0
  169. data/tests/runner/extensions/confctl_helpers.rb +238 -0
  170. data/tests/runner/extensions/hostfwd_ports.rb +41 -0
  171. data/tests/suite/auto_rollback.nix +344 -0
  172. data/tests/suite/carrier/deploy.nix +751 -0
  173. data/tests/suite/carrier/netboot.nix +849 -0
  174. data/tests/suite/deploy/base.nix +806 -0
  175. data/tests/suite/deploy/flakes.nix +1 -0
  176. data/tests/suite/deploy/swpins.nix +1 -0
  177. metadata +104 -4
  178. data/nix/modules/confctl/overlays.nix +0 -15
@@ -0,0 +1,228 @@
1
+ ---
2
+ name: confctl-configuration-update
3
+ description: >-
4
+ Upgrade confctl-managed flake configuration repositories from one
5
+ NixOS/nixpkgs release to another, such as 25.11 to 26.05. Use only for NixOS
6
+ release upgrades that require reading target release notes, moving nixpkgs
7
+ release channels, updating release-coupled inputs such as home-manager when
8
+ needed, handling NixOS release deprecations, and validating machines with
9
+ confctl build. Do not use for routine flake input bumps, service updates,
10
+ dependency refreshes, or ordinary staging/production rollouts that do not
11
+ change the NixOS/nixpkgs release.
12
+ ---
13
+
14
+ # confctl NixOS Release Upgrade
15
+
16
+ ## Overview
17
+
18
+ Use this skill only for configuration repositories built by `confctl` when
19
+ upgrading machines from one NixOS/nixpkgs release to another. The goal is not
20
+ only to move release inputs, but to produce a reviewable, deployable release
21
+ port: release notes understood, generated input commits isolated, machines
22
+ built or explicitly blocked, warnings fixed, and compatibility implications
23
+ recorded.
24
+
25
+ Do not use this skill for routine flake input bumps, ordinary service updates,
26
+ dependency refreshes, or staging/production rollouts that keep the same NixOS
27
+ release.
28
+
29
+ ## Rules
30
+
31
+ - Read local repository instructions before editing. Do not assume a specific
32
+ organization, directory layout, branch naming scheme, or tracking-file
33
+ convention.
34
+ - Use the repository's documented development environment. If it provides a
35
+ Nix shell, prefer `nix develop` before running `confctl`.
36
+ - Use `confctl` commands for flake input revision changes. Do not edit
37
+ `flake.lock` manually.
38
+ - Let `confctl` create generated input commits with `--commit`; use
39
+ `--no-editor` in non-interactive runs.
40
+ - Use `--no-changelog` for noisy inputs such as `nixpkgs`, `home-manager`, or
41
+ other large upstreams when they are moved as part of the release upgrade.
42
+ Keep changelogs for smaller controlled repositories when the log is useful.
43
+ - Treat Nix evaluation warnings and deprecation notices as work items. Fix
44
+ them before calling the port done unless the user explicitly accepts a
45
+ documented deferral.
46
+ - Try to build all existing machines. If local secrets, ISO images, private
47
+ paths, or other operator-only inputs block the sweep, record the exact
48
+ target and path, then continue with representative builds.
49
+
50
+ ## Preparation
51
+
52
+ Before changing release inputs, identify and record the upgrade plan in
53
+ whatever place the project uses: an issue, PR description, local notes, or the
54
+ conversation. Include:
55
+
56
+ - source and target NixOS/nixpkgs releases;
57
+ - affected configuration repository and branch;
58
+ - intended channel order, for example shared stable hosts first, then staging,
59
+ then production;
60
+ - compatibility checks for persisted state, database schemas, service APIs,
61
+ generated configs, rollback, and mixed-version operation.
62
+
63
+ Read current release material from official NixOS sources. Do not rely on
64
+ memory for current release notes:
65
+
66
+ - NixOS release announcement;
67
+ - NixOS release notes for the target release;
68
+ - relevant nixpkgs, NixOS module, or service documentation linked from the
69
+ notes.
70
+
71
+ Extract a short checklist of likely issues: removed options, renamed packages
72
+ or aliases, default flips, required option changes, service module removals,
73
+ systemd behavior changes, filesystem changes, compiler/runtime changes, and
74
+ rollback-sensitive state changes.
75
+
76
+ ## Inventory
77
+
78
+ From the configuration repository root:
79
+
80
+ ```shell
81
+ nix develop
82
+ confctl inputs ls
83
+ confctl inputs channel ls
84
+ confctl ls
85
+ rg -n 'nixos-[0-9][0-9]\.[0-9][0-9]|release-[0-9][0-9]\.[0-9][0-9]|nixpkgs' \
86
+ flake.nix cluster configs modules overlays environments packages
87
+ ```
88
+
89
+ If the repository does not use `nix develop`, use its documented shell or run
90
+ the same `confctl` commands with `confctl` available on `PATH`. Omit inventory
91
+ paths that do not exist in the target repository.
92
+
93
+ Map each release-related channel to the machines it affects. Common channel
94
+ roles include:
95
+
96
+ - `nixpkgs`: the nixpkgs input used by machines in a channel;
97
+ - `home-manager`: a release branch that often follows the same nixpkgs
98
+ release;
99
+ - `confctl`: the tool version used by the configuration shell, if the current
100
+ tool cannot evaluate or build the target release;
101
+ - service-specific roles: application, module, package, or operating-system
102
+ inputs consumed by a subset of machines and coupled to the NixOS release;
103
+ - environment channels: names such as `stable`, `staging`, `production`, or
104
+ project-specific equivalents.
105
+
106
+ Decide whether each channel should move now or remain a separate rollout. Do
107
+ not assume every environment channel must move together.
108
+
109
+ ## Updating Inputs
110
+
111
+ Use `confctl` from the configuration repo's normal tool environment. Update
112
+ only inputs that are part of the release upgrade or are required to make the
113
+ target release evaluate. Common patterns:
114
+
115
+ ```shell
116
+ nix develop -c confctl inputs channel update \
117
+ --commit --no-changelog --no-editor stable nixpkgs
118
+
119
+ nix develop -c confctl inputs channel update \
120
+ --commit --no-changelog --no-editor home-manager home-manager
121
+
122
+ nix develop -c confctl inputs channel set \
123
+ --commit --no-changelog --no-editor '{production,staging}' nixpkgs <rev>
124
+
125
+ nix develop -c confctl inputs channel set \
126
+ --commit --no-editor <channel> <role> <rev>
127
+
128
+ # Only when the current confctl input cannot handle the target release:
129
+ nix develop -c confctl inputs update \
130
+ --commit --no-changelog --no-editor confctl
131
+
132
+ # Only when pinning a specific confctl revision for the release upgrade:
133
+ nix develop -c confctl inputs set \
134
+ --commit --no-changelog --no-editor confctl <rev>
135
+ ```
136
+
137
+ Use `channel update` when following the input's configured target release
138
+ branch/ref. Use `channel set` or `inputs set` for an exact revision, especially
139
+ when pinning an unmerged release-port branch or a known branch tip.
140
+
141
+ If a channel selector touches an input that is also used by another channel,
142
+ `confctl` can require `--allow-shared`. Only pass it after confirming the
143
+ shared move is intentional, and record the affected channels wherever the
144
+ project tracks validation.
145
+
146
+ Current `confctl` `set`/`update` commands own `flake.lock` revision changes
147
+ and commit only `flake.lock`. If a release port also needs `flake.nix` URL
148
+ refs changed, such as `github:NixOS/nixpkgs/nixos-25.11` to `nixos-26.05`,
149
+ treat that as a separate configuration edit. Never hand-edit `flake.lock`.
150
+
151
+ After each generated commit:
152
+
153
+ ```shell
154
+ git show --stat --format=fuller HEAD
155
+ nix develop -c confctl inputs channel ls '<affected-channel-pattern>'
156
+ ```
157
+
158
+ Keep generated `confctl` commit messages in their generated form unless
159
+ repository-local rules explicitly say to edit them.
160
+
161
+ ## Build And Warning Loop
162
+
163
+ Start with a focused sample if the release upgrade is broad, then attempt the
164
+ full fleet:
165
+
166
+ ```shell
167
+ nix develop -c confctl build -y '<critical-pattern>'
168
+ nix develop -c confctl build -y
169
+ ```
170
+
171
+ Choose representative targets that cover every release-affected input and
172
+ machine type. Examples include one machine per environment channel, one
173
+ machine per custom nixpkgs input, critical infrastructure hosts, service hosts
174
+ that use release-coupled application inputs, and machines with unusual local
175
+ hardware or filesystem configuration.
176
+
177
+ Watch evaluation output and `confctl` logs for warnings:
178
+
179
+ ```shell
180
+ rg -n -i 'warning:|deprecated|deprecat|renamed|obsolete|removed|will be removed' \
181
+ .confctl/logs
182
+ ```
183
+
184
+ Fix warnings and evaluation failures in small logical commits. Common fixes
185
+ include:
186
+
187
+ - removed NixOS option: stop reading or setting it, or gate the common path;
188
+ - renamed package/alias: use the new package name;
189
+ - required filesystem option: set an explicit `fsType`;
190
+ - stale custom nixpkgs fork: replace the missing behavior locally, then remove
191
+ the unused input;
192
+ - package wrapper regression: add a local overlay/patch and verify the built
193
+ wrapper;
194
+ - local-only build input missing: record the path and target, then validate
195
+ neighboring machines that do not require it.
196
+
197
+ If the full build is blocked, keep reducing to direct targets until every
198
+ category touched by the release upgrade has either built or has a documented
199
+ external blocker. Record generation IDs, log paths, and blocked targets in the
200
+ project's normal validation notes.
201
+
202
+ ## Compatibility Fix Commits
203
+
204
+ Keep release input bumps and functional fixes separate.
205
+
206
+ Recommended ordering:
207
+
208
+ 1. Generated release-input commits.
209
+ 2. Configuration fixes required by evaluation/build failures.
210
+ 3. Warning/deprecation cleanup.
211
+ 4. Removal of obsolete inputs after no machines use them.
212
+ 5. Follow-up service-specific release fixes discovered by targeted builds.
213
+
214
+ For manual commits, use the repository's required commit workflow. Explain what
215
+ broke, why the new release requires the change, and what hosts/services are
216
+ affected. Do not put command transcripts or test logs in commit messages;
217
+ record validation in the project's normal notes or PR text.
218
+
219
+ ## Completion
220
+
221
+ Before finishing:
222
+
223
+ - run repository formatting/hooks required by local rules;
224
+ - run `git status --short --branch`;
225
+ - record commands, validation results, warnings fixed, blockers, commit
226
+ hashes, and cleanup notes in the project-appropriate place;
227
+ - push the branch if requested or if that is the normal project flow;
228
+ - check CI when the repository has branch workflows.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "confctl NixOS release upgrade"
3
+ short_description: "Upgrade NixOS releases with confctl"
4
+ default_prompt: "Use $confctl-configuration-update for a confctl NixOS release upgrade."
@@ -0,0 +1,102 @@
1
+ ---
2
+ name: confctl-release
3
+ description: Prepare and publish confctl version releases, including changelog, dependency metadata, final release commit and verified RubyGems artifact. Use for confctl releases, not releases of configurations managed by confctl.
4
+ ---
5
+
6
+ # confctl releases
7
+
8
+ Read the repository's `AGENTS.md` and applicable workspace instructions first.
9
+ Distinguish release preparation from authorized publication. If the user asks
10
+ to prepare a release and wait, stop before default-branch integration, tagging
11
+ and RubyGems upload. Updating a release candidate does not expand that approval.
12
+
13
+ ## Prepare the changes
14
+
15
+ 1. Confirm the target version and inspect the latest release tag, current remote
16
+ default branch and RubyGems versions. Summarize material operator-visible
17
+ changes from the previous tag to the selected source, checking current code
18
+ and project guides. Record runtime requirements, upgrade steps and explicit
19
+ deprecation decisions. Preserve the dated `CHANGELOG.md` format.
20
+ 2. Finish requested dependency updates and release-procedure changes before
21
+ the release commit. For a full lock refresh, run `bundle update` within the
22
+ existing Gemfile/gemspec constraints. Resolve using the minimum supported
23
+ Ruby from `confctl.gemspec`, not just the development shell's newer Ruby.
24
+ Inspect updates for runtime compatibility; changing declared constraints or
25
+ Ruby support is a separate compatibility decision.
26
+ 3. Regenerate `gemset.nix` with Bundix after lock changes:
27
+
28
+ ```sh
29
+ nix develop -c nix shell --inputs-from . nixpkgs#bundix -c bundix
30
+ nix develop -c nixfmt gemset.nix
31
+ ```
32
+
33
+ `gemset.nix` is generated dependency metadata. Do not maintain it by hand.
34
+ `nix/package.nix` and the flake RSpec check pass `gemdir` to `bundlerEnv`,
35
+ which imports that directory's `gemset.nix`. The development shell instead
36
+ runs Bundler directly, so passing shell tests does not validate the Nix
37
+ package's gemset. Keep a dependency refresh in its own commit.
38
+ 4. Update `lib/confctl/version.rb`, refresh the confctl path-gem version in
39
+ `Gemfile.lock` with Bundler, and regenerate `gemset.nix` with Bundix again.
40
+ Add the changelog entry. Verify all four release identifiers agree:
41
+ changelog, version constant, lockfile's `confctl` entry and gemset's
42
+ `confctl.version`. Other gems can legitimately have the old release's
43
+ version number; do not replace matching numbers globally.
44
+ 5. Commit this release unit with subject `Version <version>` and a concise
45
+ rationale. The release commit must be the final commit, after dependency
46
+ and procedure changes. If preparation needs a correction, consolidate the
47
+ unmerged history so this remains true; preserve published default-branch
48
+ history. Run the declared hooks rather than bypassing them.
49
+
50
+ ## Verify the candidate
51
+
52
+ Run quick metadata, syntax, whitespace and hook checks, then follow the
53
+ applicable independent final-review procedure for the complete committed
54
+ branch. Provide its full commit series, final diff and migration inventory.
55
+ After review, run the repository's RSpec, RuboCop and Nix formatting checks,
56
+ including the minimum supported Ruby when dependencies changed. Build the
57
+ flake's actual `confctl` package as well as the Ruby gem. Verify Nix's installed
58
+ confctl gem metadata matches the release version.
59
+
60
+ The gemspec uses a directory glob when `.git` is not a directory. In a Git
61
+ worktree this can include local `.gems`, `.bin`, caches or the `.git` file.
62
+ Build from a clean `git archive` export of the final commit, reusing the
63
+ provisioned Bundler dependency paths, then run `bundle exec rake build` there.
64
+ The rake build task generates the manual and HTML pages. Inspect the archive's
65
+ file list against committed source plus those generated files. Verify version,
66
+ Ruby requirement, executable and changelog, and retain the artifact's checksum.
67
+ Do not run `rake release` to prepare a candidate: it also tags, pushes and uploads.
68
+
69
+ For a packaged CLI smoke test, extract the gem and load its libraries directly.
70
+ Clear Bundler injection with `Bundler.with_unbundled_env` (or an equivalent
71
+ clean child environment), retain dependency `GEM_HOME`/`GEM_PATH`, and set
72
+ `RUBYLIB` to the extracted `lib`. Assert `ConfCtl.root` and the loaded
73
+ `confctl.rb` come from the extracted tree before running `--help`. In the
74
+ current CLI, GLI's `--version` is unset; inspect `ConfCtl::VERSION` and gemspec
75
+ metadata rather than accepting a version check that only exercises source code.
76
+
77
+ Publish the development branch according to repository/workspace policy and
78
+ check CI for its exact final head. When replacing a published candidate,
79
+ use the expected old head as the force-with-lease condition and handle
80
+ superseded CI under the applicable workspace rules. Present the final head,
81
+ diff, review/check results and artifact checksum for approval.
82
+
83
+ ## Publish after authorization
84
+
85
+ Confirm authorization covers the repository's default branch, release tag and
86
+ RubyGems upload. Recheck the remote default branch and existing tag/version.
87
+ Integrate the approved branch under the repository/workspace Git policy. If
88
+ the source changes, rebuild and verify the artifact and reconcile approval
89
+ scope before publication. Keep the final release commit at the tip.
90
+
91
+ Create an annotated `v<version>` tag at the approved release commit and push
92
+ the authorized branch and tag over SSH. Upload the exact verified gem with
93
+ `gem push <artifact>` using the configured RubyGems authentication; never print
94
+ credentials. Verify that RubyGems reports the expected version, Ruby requirement
95
+ and artifact SHA-256, and that the remote tag resolves to the approved commit.
96
+
97
+ If any publication step fails, inspect which branch, tag and gem writes
98
+ succeeded before retrying. RubyGems versions cannot be overwritten. Do not
99
+ delete a tag, yank a gem, substitute a rebuilt artifact or bump the version
100
+ as an automatic recovery step. Record the partial result and obtain direction
101
+ for any action outside the approved release. Keep the session available for
102
+ follow-up under its lifecycle policy.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "confctl version release"
3
+ short_description: "Prepare and publish verified confctl releases"
4
+ default_prompt: "Use $confctl-release to prepare a confctl version release."
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'confctl'
5
+ require 'confctl/cli'
6
+
7
+ RSpec.describe ConfCtl::Cli::Cluster do
8
+ let(:gopts) { { color: 'never' } }
9
+ let(:opts) { { yes: true } }
10
+ let(:command) { described_class.new(gopts, opts, [nil]) }
11
+
12
+ def machine(managed: true, runnable: true, checks: [])
13
+ instance_double(
14
+ ConfCtl::Machine,
15
+ managed:,
16
+ runnable?: runnable,
17
+ health_checks: checks
18
+ )
19
+ end
20
+
21
+ it 'runs checks only for runnable machines' do
22
+ direct_check = instance_double(ConfCtl::HealthChecks::Base)
23
+ carried_check = instance_double(ConfCtl::HealthChecks::Base)
24
+ direct = machine(checks: [direct_check])
25
+ carried = machine(runnable: false, checks: [carried_check])
26
+ machines = ConfCtl::MachineList.new(
27
+ machines: {
28
+ 'direct' => direct,
29
+ 'carried' => carried
30
+ }
31
+ )
32
+
33
+ allow(command).to receive(:select_machines).with(nil).and_return(machines)
34
+
35
+ expect(command).to receive(:run_health_checks) do |selected, run_checks|
36
+ expect(selected.to_a).to eq([direct])
37
+ expect(run_checks).to eq([direct_check])
38
+ []
39
+ end
40
+
41
+ command.health_check
42
+ end
43
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'confctl'
5
+ require 'confctl/cli'
6
+
7
+ RSpec.describe ConfCtl::Cli::Cluster do
8
+ let(:gopts) { { color: 'never' } }
9
+ let(:opts) { { yes: true } }
10
+ let(:command) { described_class.new(gopts, opts, []) }
11
+ let(:target_toplevel) { '/nix/store/target-system' }
12
+ let(:generation) do
13
+ instance_double(
14
+ ConfCtl::Generation::Build,
15
+ name: '2026-06-04--12-00-00',
16
+ toplevel: target_toplevel
17
+ )
18
+ end
19
+
20
+ def status_with(current_toplevel:, profile_toplevel: nil)
21
+ profile_generation =
22
+ if profile_toplevel
23
+ instance_double(ConfCtl::Generation::Host, toplevel: profile_toplevel)
24
+ end
25
+
26
+ generations =
27
+ if profile_generation
28
+ instance_double(ConfCtl::Generation::HostList, current: profile_generation)
29
+ end
30
+
31
+ instance_double(
32
+ ConfCtl::MachineStatus,
33
+ current_toplevel:,
34
+ generations:
35
+ )
36
+ end
37
+
38
+ def machine_with(carried:)
39
+ instance_double(ConfCtl::Machine, carried?: carried)
40
+ end
41
+
42
+ def already_using_target?(action:, current_toplevel:, profile_toplevel:, carried: false)
43
+ command.send(
44
+ :already_using_target_generation?,
45
+ machine_with(carried:),
46
+ status_with(current_toplevel:, profile_toplevel:),
47
+ generation,
48
+ action
49
+ )
50
+ end
51
+
52
+ it 'skips standalone boot and switch when runtime and profile match' do
53
+ %w[boot switch].each do |action|
54
+ expect(
55
+ already_using_target?(
56
+ action:,
57
+ current_toplevel: target_toplevel,
58
+ profile_toplevel: target_toplevel
59
+ )
60
+ ).to be(true)
61
+ end
62
+ end
63
+
64
+ it 'does not skip standalone boot and switch when the profile is stale' do
65
+ %w[boot switch].each do |action|
66
+ expect(
67
+ already_using_target?(
68
+ action:,
69
+ current_toplevel: target_toplevel,
70
+ profile_toplevel: '/nix/store/old-system'
71
+ )
72
+ ).to be(false)
73
+ end
74
+ end
75
+
76
+ it 'skips standalone test and dry-activate when runtime matches' do
77
+ %w[test dry-activate].each do |action|
78
+ expect(
79
+ already_using_target?(
80
+ action:,
81
+ current_toplevel: target_toplevel,
82
+ profile_toplevel: '/nix/store/old-system'
83
+ )
84
+ ).to be(true)
85
+ end
86
+ end
87
+
88
+ it 'skips carried machines when their carrier-managed profile matches' do
89
+ expect(
90
+ already_using_target?(
91
+ action: 'switch',
92
+ current_toplevel: target_toplevel,
93
+ profile_toplevel: nil,
94
+ carried: true
95
+ )
96
+ ).to be(true)
97
+ end
98
+
99
+ it 'does not skip carried machines when their carrier-managed profile is stale' do
100
+ expect(
101
+ already_using_target?(
102
+ action: 'switch',
103
+ current_toplevel: '/nix/store/old-system',
104
+ profile_toplevel: nil,
105
+ carried: true
106
+ )
107
+ ).to be(false)
108
+ end
109
+
110
+ it 'does not skip when status data is unavailable' do
111
+ expect(
112
+ command.send(
113
+ :already_using_target_generation?,
114
+ machine_with(carried: false),
115
+ status_with(current_toplevel: nil, profile_toplevel: target_toplevel),
116
+ generation,
117
+ 'switch'
118
+ )
119
+ ).to be(false)
120
+ end
121
+
122
+ it 'does not pre-filter copy-only deployments' do
123
+ copy_command = described_class.new(gopts, opts.merge('copy-only' => true), [])
124
+ machine = machine_with(carried: false)
125
+ machines = ConfCtl::MachineList.new(machines: { 'host' => machine })
126
+ host_generations = { 'host' => generation }
127
+
128
+ allow(copy_command).to receive(:deploy_target_statuses).and_raise('unexpected status query')
129
+
130
+ filtered_machines, filtered_generations =
131
+ copy_command.send(:skip_current_deploy_targets, machines, host_generations, 'switch')
132
+
133
+ expect(filtered_machines).to equal(machines)
134
+ expect(filtered_generations).to equal(host_generations)
135
+ end
136
+
137
+ it 'removes skipped hosts from deploy inputs' do
138
+ machine = machine_with(carried: false)
139
+ machines = ConfCtl::MachineList.new(machines: { 'host' => machine })
140
+ host_generations = { 'host' => generation }
141
+
142
+ allow(command).to receive(:deploy_target_statuses).and_return(
143
+ 'host' => status_with(
144
+ current_toplevel: target_toplevel,
145
+ profile_toplevel: target_toplevel
146
+ )
147
+ )
148
+
149
+ expect do
150
+ @filtered_machines, @filtered_generations =
151
+ command.send(:skip_current_deploy_targets, machines, host_generations, 'switch')
152
+ end.to output(/Skipping host: already using target generation 2026-06-04--12-00-00/).to_stdout
153
+
154
+ expect(@filtered_machines).to be_empty
155
+ expect(@filtered_generations).to be_empty
156
+ end
157
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'confctl'
5
+ require 'confctl/cli'
6
+
7
+ RSpec.describe ConfCtl::Cli::Cluster do
8
+ let(:gopts) { { color: 'never' } }
9
+ let(:opts) { { yes: true, generation: 'none' } }
10
+ let(:command) { described_class.new(gopts, opts, [nil]) }
11
+ let(:host) { 'cz.vpsfree/vpsadmin/int.api1' }
12
+ let(:machine) do
13
+ instance_double(
14
+ 'ConfCtl::Machine',
15
+ managed: true,
16
+ target_host: 'int.api1',
17
+ carried?: false
18
+ )
19
+ end
20
+ let(:machines) { ConfCtl::MachineList.new(machines: { host => machine }) }
21
+ let(:status_class) do
22
+ Struct.new(
23
+ :uptime,
24
+ :inputs_info,
25
+ :target_inputs_info,
26
+ :target_toplevel,
27
+ :current_toplevel,
28
+ :generations
29
+ ) do
30
+ def query(**)
31
+ nil
32
+ end
33
+ end
34
+ end
35
+ let(:status) do
36
+ status_class.new(
37
+ 31.7 * 24 * 60 * 60,
38
+ {
39
+ 'nixpkgs' => { 'rev' => '71caefce01234567', 'shortRev' => '71caefce' },
40
+ 'vpsadmin' => { 'rev' => 'cb29516601234567', 'shortRev' => 'cb295166' }
41
+ },
42
+ nil,
43
+ nil,
44
+ nil,
45
+ nil
46
+ )
47
+ end
48
+ let(:nix) { instance_double(ConfCtl::Nix) }
49
+ let(:build_generations) { instance_double(ConfCtl::Generation::BuildList, count: 29) }
50
+
51
+ before do
52
+ allow(command).to receive(:select_machines).with(nil).and_return(machines)
53
+ allow(ConfCtl::MachineStatus).to receive(:new).with(machine).and_return(status)
54
+ allow(ConfCtl::Nix).to receive(:new).and_return(nix)
55
+ allow(nix).to receive(:eval_inputs_info).with(host).and_return(
56
+ 'nixpkgs' => { 'rev' => 'fea3b36789abcdef', 'shortRev' => 'fea3b367' },
57
+ 'vpsadmin' => { 'rev' => 'cb29516601234567', 'shortRev' => 'cb295166' }
58
+ )
59
+ allow(ConfCtl::Generation::BuildList).to receive(:new).with(host).and_return(build_generations)
60
+ end
61
+
62
+ it 'shows deployed input revisions without target arrows' do
63
+ captured_rows = nil
64
+ captured_cols = nil
65
+
66
+ allow(ConfCtl::Cli::OutputFormatter).to receive(:print) do |rows, cols, **|
67
+ captured_rows = rows
68
+ captured_cols = cols
69
+ end
70
+
71
+ command.status_flake
72
+
73
+ expect(captured_cols).to include('nixpkgs', 'vpsadmin')
74
+
75
+ row = captured_rows.fetch(0)
76
+
77
+ expect(row['status'].to_s).to eq('outdated')
78
+ expect(row['nixpkgs'].to_s).to eq('71caefce')
79
+ expect(row['vpsadmin'].to_s).to eq('cb295166')
80
+ expect(row['nixpkgs'].to_s).not_to include('->')
81
+ expect(row['vpsadmin'].to_s).not_to include('->')
82
+ end
83
+ end