Skip to content

chore: update docs and refactor the nix code#1

Closed
0xgleb wants to merge 3 commits into
masterfrom
chore/refactor
Closed

chore: update docs and refactor the nix code#1
0xgleb wants to merge 3 commits into
masterfrom
chore/refactor

Conversation

@0xgleb

@0xgleb 0xgleb commented Mar 19, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added ACME support for SSL certificate management
    • Added HTTP/HTTPS firewall configuration options
    • Added service logging directory support
    • Added pre-commit git hooks (code formatting, linting, and optional Rust support)
  • Documentation

    • Updated module and library function documentation
    • Added example NixOS configurations for minimal, single-service, and full setups
    • Updated roadmap with completed milestones and upcoming integration testing
  • Improvements

    • Enhanced CI and integration testing workflows

@0xgleb 0xgleb self-assigned this Mar 19, 2026
@0xgleb

0xgleb commented Mar 19, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@0xgleb

0xgleb commented Mar 19, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR consolidates terraform/shell helper functions into a new shared lib/shell.nix module, replaces the lib/remote.nix SSH utility with lib/hooks.nix for pre-commit hooks integration, adds a new ACME NixOS module, introduces three example system configurations, and expands CI/integration workflows. The flake now exports nixosConfigurations for examples and simplifies module imports via unified omnix.nixosModules.default.

Changes

Cohort / File(s) Summary
Documentation
ROADMAP.md, README.md
Updated roadmap with completed tasks, new sections for age-based secrets and integration tests; updated README to reflect omnix.nixosModules.default handling all module imports and replaced mkRemote reference with mkGitHooks.
Flake configuration
flake.nix
Extended outputs with nixosConfigurations for three examples, added acme module export, integrated git-hooks input, updated nixosModules.default.imports to include upstream disko/ragenix/acme, changed packages.disko to error on missing default, added checks.git-hooks, formatter, and devShells.default with development tools.
Library shell helpers
lib/shell.nix
New module exporting terraform path constants and reusable shell snippets: parseIdentity, decryptState/encryptState, decryptVars/encryptVars, and resolveIp with proper key/state handling.
Library function refactoring
lib/default.nix, lib/bootstrap.nix, lib/deploy.nix, lib/terraform.nix
Removed system parameter from terraform.nix, extracted shell helpers to lib/shell.nix, updated function signatures to accept open parameter sets, added targetSystem parameter to deploy.nix, refactored bootstrap.nix to use shared shell module.
Library function replacement
lib/hooks.nix (new), lib/remote.nix (removed)
Added pre-commit hooks configuration supporting nixfmt, deadnix, taplo, and optional rustfmt; removed SSH host resolution helper that handled key decryption and Terraform state IP extraction.
NixOS modules
modules/acme.nix (new), modules/base.nix, modules/disko.nix, modules/digitalocean.nix, modules/firewall.nix, modules/services.nix, modules/storage.nix
Added ACME module with email configuration and assertion checks; refactored base.nix to remove stateVersion default; added disko attribute assertion; reformatted digitalocean.nix configuration; added HTTP/HTTPS firewall enable options to firewall.nix; extended services.nix with logDir option, logrotate rules, and improved serviceConfig composition; reformatted storage.nix.
Example configurations
examples/minimal.nix, examples/single-service.nix, examples/full.nix
Added three NixOS system examples: minimal (disko + base), single-service (+ digitalocean + firewall + one service), and full (+ storage + acme + multiple services).
CI workflows
.github/workflows/ci.yaml, .github/workflows/infra.yaml (new), templates/do-service/.github/workflows/ci.yml, templates/do-service/.github/workflows/integration-test.yml (new)
Added top-level ci.yaml for flake checks and example builds; added infra.yaml for full DigitalOcean lifecycle testing; updated template CI to use pinned action versions (@v17, @v8), add job-level permissions, derive HOST_KEY from keys.nix, and support both master/main branches; added integration-test workflow with SSH, Terraform, bootstrap, deploy, and verification steps.
Template configuration
templates/do-service/flake.nix, templates/do-service/os.nix, templates/do-service/keys.nix, templates/do-service/config/secrets.nix, templates/do-service/infra/secrets.nix
Updated template flake to remove redundant module imports and use only omnix.nixosModules.default; added stateVersion to os.nix and removed unused pkgs parameter; reformatted key/role definitions; added instructions to terraform.tfvars.example; enhanced variables.tf with validation for do_token and volume_size_gb.
Service configuration
templates/do-service/services.nix
Updated my-service.dataDir path from "/mnt/data/prod" to "/mnt/data/my-service".
Repository configuration
.coderabbit.yaml
Reworked review settings: set profile to assertive, enabled review_details, adjusted auto_pause_after_reviewed_commits from 10 to 7, added job-level permissions configuration.

Possibly related PRs

  • add examples/ with 3 complexity levels, CI builds each #6: Adds same example system configurations (examples/minimal.nix, examples/single-service.nix, examples/full.nix) and example-building CI workflow infrastructure.
  • nu shell nu life #5: Refactors the same shell helper functions (lib/shell.nix, lib/bootstrap.nix, lib/deploy.nix, lib/terraform.nix) and removes lib/remote.nix, using alternative implementation approach for terraform/deployment helpers.
  • feat: flake-parts #4: Restructures top-level flake outputs including nixosConfigurations, per-system exports (packages.disko, checks, formatter, devShells), and module composition via eachDefaultSystem.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'chore: update docs and refactor the nix code' broadly describes documentation updates and Nix refactoring, which aligns with the primary changes (roadmap/README updates and extensive Nix file refactoring), but is generic and does not capture the specific, substantial scope of changes such as the new module system reorganization, shell helper extraction, or new workflows.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/refactor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ROADMAP.md`:
- Around line 32-33: The roadmap entry mentioning "If secretspec provides a
provider SDK, implement the `Provider` trait alongside the CLI to get structured
secret declarations (`secretspec.toml`) for free." is ambiguous about priority;
update ROADMAP.md by turning that sentence into a separate checklist item and
clearly label it as optional (e.g., "Optional: Implement secretspec `Provider`
trait to emit `secretspec.toml`"), or explicitly state it is required and remove
the conditional phrasing; reference the `Provider` trait, `secretspec` SDK,
`secretspec.toml`, and the CLI in the new line so readers can find the related
implementation targets.
- Line 55: Replace the redundant phrase "CLI interface" in the checklist item "[
] CLI interface: `omnix-age encrypt`, `omnix-age decrypt`, `omnix-age rekey`"
with either "CLI" or the full phrase "Command-line interface" (e.g., "[ ] CLI:
`omnix-age encrypt`, `omnix-age decrypt`, `omnix-age rekey`") to avoid repeating
"interface."
- Around line 35-46: The Mermaid diagram in ROADMAP.md currently enforces a
linear flow (A→B→C→D→E→F→G→H) but several tasks can run in parallel; update the
graph so B[age encrypt/decrypt core] points to both C[keys.nix role parsing] and
D[Host key decryption], have C and D feed into E[CLI: encrypt/decrypt/rekey],
and also add direct edges from B to F[NixOS module] and B to G[deploy-rs
activation], then make E, F, and G converge into H[Integrate into omnix flake];
keep node A[Scaffold Rust crate] → B as-is and adjust labels (E) to reflect CLI
scope so the diagram accurately represents parallelizable work.
- Around line 27-30: The roadmap text embeds a consumer-specific reference
"st0x.liquidity"; remove that name and rewrite the sentence to describe the
generic workflow pattern omnix must support (e.g., committing `.toml.age` files
to git, using a deploy-time decryptor like `rage` to place decrypted files under
`/run/agenix/` using the host key such as `/etc/ssh/ssh_host_ed25519_key`, and
services reading plaintext from tmpfs). Replace the concrete project name with
neutral phrasing like "current pattern" or "existing deployments" and keep the
exact workflow symbols (`.toml.age`, `rage`, `/run/agenix/`,
`/etc/ssh/ssh_host_ed25519_key`, tmpfs) so the requirement remains precise but
infrastructure-agnostic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 06d4a90e-7690-4ae0-8dfa-44963cdb135c

📥 Commits

Reviewing files that changed from the base of the PR and between 6ca2bcd and 113cb19.

📒 Files selected for processing (1)
  • ROADMAP.md
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
*

⚙️ CodeRabbit configuration file

Focus on providing constructive criticism. Whenever you see a suboptimal approach, suggest more idiomatic or robust alternative(s). Flag potential footguns. Suggest FP alternatives to mutable/imperative code. Point out architectural flaws like leaky abstractions, tight coupling, wrong level of abstraction, poor type modeling, over-abstraction, unclear domain boundaries. Code should generally be organized based on business concerns rather than technical aspects - suggest improvements if you find violations. Point out gaps in test coverage but suggest tests that are not too coupled to the implementation and actually test domain invariants and business logic.

Files:

  • ROADMAP.md
**/*.md

⚙️ CodeRabbit configuration file

Focus on the contents of the docs and not on cosmetic things like markdown formatting. We use markdown files for various docs including but not limited to the north star system specification, SPEC.md, the plan for how to get there, ROADMAP.md, guidelines for AI contributors, AGENTS.md, project overview and instructions for human contributors, README.md. Think about the target audience of a document when deciding what comment to leave. For specifications and designs, suggest potential product, architectural, and UI/UX improvements. For plans, suggest changes that would make things more parallelizable and deliverable-focused. For instructions, suggest better rules and guidelines and point out missing instructions. In all cases, flag needless bloat, prefer clear concise writing, and consider the structure of the document and order of the sections

Files:

  • ROADMAP.md
🪛 LanguageTool
ROADMAP.md

[style] ~55-~55: This phrase is redundant (‘I’ stands for ‘interface’). Use simply “CLI”.
Context: ...s identity, same as ragenix today - [ ] CLI interface: omnix-age encrypt, `omnix-age decryp...

(ACRONYM_TAUTOLOGY)

Comment thread ROADMAP.md Outdated
Comment thread ROADMAP.md Outdated
Comment thread ROADMAP.md
Comment thread ROADMAP.md Outdated

0xgleb commented Mar 19, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

♻️ Duplicate comments (1)
ROADMAP.md (1)

55-55: ⚠️ Potential issue | 🟡 Minor

Fix redundant acronym: "CLI interface".

"CLI" already means "Command-Line Interface", so "CLI interface" is redundant.

✍️ Proposed fix
-- [ ] CLI interface: `omnix-age encrypt`, `omnix-age decrypt`, `omnix-age rekey`
+- [ ] CLI: `omnix-age encrypt`, `omnix-age decrypt`, `omnix-age rekey`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ROADMAP.md` at line 55, Change the redundant phrase "CLI interface" to either
"CLI" or "command-line interface" in the ROADMAP entry that lists the commands
(`omnix-age encrypt`, `omnix-age decrypt`, `omnix-age rekey`); update the line
"- [ ] CLI interface: `omnix-age encrypt`, `omnix-age decrypt`, `omnix-age
rekey`" to remove the duplicated word so it reads e.g. "- [ ] CLI: `omnix-age
encrypt`, `omnix-age decrypt`, `omnix-age rekey`".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@flake.nix`:
- Around line 85-99: The dev shell currently hardcodes packages while deriving
shellHook from checks.git-hooks; instead derive the devShell toolchain from the
same check so the shell follows upstream
enabledPackages/extraHooks/rustToolchain logic — update devShells.default to
import the packages and shellHook from checks.git-hooks
(checks.git-hooks.lib.${system}.enabledPackages or equivalent) so the hook
runner and any extraHooks/rustToolchain are included automatically, keep using
shellHook from checks.git-hooks and remove the manual package list
(nixfmt/deadnix/taplo) so toolchain changes in lib/hooks.nix propagate to the
dev shell.

In `@lib/bootstrap.nix`:
- Around line 18-24: The runtimeInputs array is missing the openssh package used
by the wrapper (ssh is invoked later), so update the runtimeInputs declaration
to include pkgs.openssh (e.g., add pkgs.openssh alongside pkgs.rage, pkgs.jq,
pkgs.gnused) so writeShellApplication declares ssh as a runtime dependency; keep
the conditional ragenixPkg handling unchanged.

In `@lib/default.nix`:
- Line 11: The mkGitHooks call passes an unused pkgs parameter even though
hooks.nix only consumes extraHooks and rustToolchain; remove pkgs from the
mkGitHooks invocation and update the caller so it no longer passes { inherit
pkgs; }, or alternatively, if you want to keep pkgs, add explicit usage or a
comment in hooks.nix documenting why pkgs is accepted but unused; target
symbols: mkGitHooks (call site), hooks.nix (module using extraHooks and
rustToolchain), and ensure any callers stop passing pkgs to avoid misleading API
consumers.

In `@lib/hooks.nix`:
- Around line 1-5: The top-level argument set in lib/hooks.nix uses a catch-all
`...` which silently accepts and discards extra args; either remove the `...` or
explicitly accept/document expected extra args (for example add an explicit
`pkgs ? null` parameter) and update the file header comment to state whether
extra arguments are ignored or which ones (like `pkgs`) are supported; locate
the argument list containing `extraHooks ? { }`, `rustToolchain ? null` and the
trailing `...` and either remove `...` or replace it with named parameters and a
short comment explaining behavior.

In `@lib/shell.nix`:
- Around line 37-42: The resolveIp shell block currently uses "jq -r" to extract
droplet_ipv4 into host_ip which returns "null" with exit code 0 when the output
is missing; change the jq invocation in the resolveIp block to use jq -e -r so
jq exits non‑zero on null/missing values and the script fails fast, ensuring
host_ip is never set to an invalid value; update the host_ip extraction line
that references ${tfState} inside resolveIp (the lines surrounding
parseIdentity, decryptState, and host_ip) to use jq -e -r and optionally
propagate the error immediately (i.e., do not suppress the exit) so downstream
SSH attempts in lib/bootstrap.nix and lib/terraform.nix receive a valid IP or
abort.
- Around line 15-18: The decrypted Terraform files are written with
caller-controlled permissions; update decryptState and decryptVars to enforce
restrictive perms by setting a strict umask (e.g., umask 077) before writing or
by immediately changing mode to 600 after writing the plaintext (use tfState and
tfVars symbols to locate the writes). Ensure the umask change or chmod 600 is
applied only around the decrypt write operations so the rest of the script is
unaffected, and keep error handling intact if the permission change fails.

In `@lib/terraform.nix`:
- Around line 10-27: The buildInputs list in lib/terraform.nix is missing the
nix binary required at runtime by the inherited helpers encryptState and
encryptVars (from shell.nix) which call `nix eval`; update the buildInputs array
(the variable named buildInputs) to include pkgs.nix alongside pkgs.terraform,
pkgs.rage, and pkgs.jq so the runtimeInputs used by mkTask/writeShellApplication
provide the `nix` executable for tasks like tfApply, tfDestroy, tfImport and
rekey.

In `@modules/base.nix`:
- Line 63: Replace the imperative activation script at
system.activationScripts.per-service-profiles.text that runs mkdir -p
/nix/var/nix/profiles/per-service with a declarative systemd tmpfiles rule: add
or replace configuration using systemd.tmpfiles.rules (or
systemd.tmpfiles.rules."per-service-profiles") to declare the directory path,
ownership and mode (e.g. d /nix/var/nix/profiles/per-service 0755 root root -)
so the directory is created declaratively with correct permissions instead of
using an activation shell command.

In `@modules/services.nix`:
- Around line 45-70: Replace the verbose conditional merges that use the pattern
"// ( if ... then { ... } else { } )" with lib.optionalAttrs to cleanly include
attributes only when conditions hold: for the block that sets User/Group use
lib.optionalAttrs (cfg.group != null) // { User = cfg.user; Group = cfg.group;
}, for SupplementaryGroups use lib.optionalAttrs (cfg.dynamicUser && cfg.group
!= null) // { SupplementaryGroups = [ cfg.group ]; }, and for ReadWritePaths use
lib.optionalAttrs (svcCfg.dataDir != null) // { ReadWritePaths = [
svcCfg.dataDir ]; } so the attributes are included idiomatically and merged
without the explicit empty-else objects.

In `@modules/storage.nix`:
- Around line 10-13: The option volumeName currently accepts any string which
can produce invalid by-id device paths; add a validation to the lib.mkOption for
volumeName to restrict characters (e.g. only alphanumerics, dot, underscore,
hyphen) by adding a validate field such as validate = name: lib.match
"^[A-Za-z0-9._-]+$" name != null or similar, and provide a clear errorMessage
like "volumeName may only contain letters, digits, '.', '_', '-'"; keep type =
lib.types.str and reference the volumeName option and lib.mkOption in your
change.

In `@README.md`:
- Line 42: Add a short “Migration” note in README describing how to replace
usages of mkRemote with lib.mkGitHooks: state that mkRemote has been renamed to
lib.mkGitHooks, provide a minimal before/after snippet showing the old mkRemote
invocation and the new lib.mkGitHooks invocation (preserving hook list: nixfmt,
deadnix, taplo and optional rustfmt), and mention any argument/name changes or
defaults (e.g., rustfmt is optional) so users can quickly update their code.

In `@templates/do-service/flake.nix`:
- Around line 40-48: The deployConfig is duplicated with inconsistent package
references (top-level uses self.packages.x86_64-linux.default while the
per-system block uses pkgs.hello); fix by extracting the package reference into
a single variable (e.g., package = self.packages.${system}.default or a shared
let-bound pkg variable) and replace both occurrences of
self.packages.x86_64-linux.default and pkgs.hello to reference that single
variable so both the flake-level deployConfig and the per-system wrappers use
the exact same package reference (update deployConfig, the per-system section,
and any uses of pkgs.hello accordingly).

---

Duplicate comments:
In `@ROADMAP.md`:
- Line 55: Change the redundant phrase "CLI interface" to either "CLI" or
"command-line interface" in the ROADMAP entry that lists the commands
(`omnix-age encrypt`, `omnix-age decrypt`, `omnix-age rekey`); update the line
"- [ ] CLI interface: `omnix-age encrypt`, `omnix-age decrypt`, `omnix-age
rekey`" to remove the duplicated word so it reads e.g. "- [ ] CLI: `omnix-age
encrypt`, `omnix-age decrypt`, `omnix-age rekey`".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c264208c-bf58-46d4-91a4-b1e4b2fd453f

📥 Commits

Reviewing files that changed from the base of the PR and between 113cb19 and 5fa8154.

⛔ Files ignored due to path filters (1)
  • flake.lock is excluded by !**/*.lock
📒 Files selected for processing (21)
  • README.md
  • ROADMAP.md
  • flake.nix
  • lib/bootstrap.nix
  • lib/default.nix
  • lib/deploy.nix
  • lib/hooks.nix
  • lib/remote.nix
  • lib/shell.nix
  • lib/terraform.nix
  • modules/base.nix
  • modules/digitalocean.nix
  • modules/disko.nix
  • modules/firewall.nix
  • modules/services.nix
  • modules/storage.nix
  • templates/do-service/config/secrets.nix
  • templates/do-service/flake.nix
  • templates/do-service/infra/secrets.nix
  • templates/do-service/keys.nix
  • templates/do-service/os.nix
💤 Files with no reviewable changes (1)
  • lib/remote.nix
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
*

⚙️ CodeRabbit configuration file

Focus on providing constructive criticism. Whenever you see a suboptimal approach, suggest more idiomatic or robust alternative(s). Flag potential footguns. Suggest FP alternatives to mutable/imperative code. Point out architectural flaws like leaky abstractions, tight coupling, wrong level of abstraction, poor type modeling, over-abstraction, unclear domain boundaries. Code should generally be organized based on business concerns rather than technical aspects - suggest improvements if you find violations. Point out gaps in test coverage but suggest tests that are not too coupled to the implementation and actually test domain invariants and business logic.

Files:

  • README.md
  • flake.nix
  • ROADMAP.md
**/*.md

⚙️ CodeRabbit configuration file

Focus on the contents of the docs and not on cosmetic things like markdown formatting. We use markdown files for various docs including but not limited to the north star system specification, SPEC.md, the plan for how to get there, ROADMAP.md, guidelines for AI contributors, AGENTS.md, project overview and instructions for human contributors, README.md. Think about the target audience of a document when deciding what comment to leave. For specifications and designs, suggest potential product, architectural, and UI/UX improvements. For plans, suggest changes that would make things more parallelizable and deliverable-focused. For instructions, suggest better rules and guidelines and point out missing instructions. In all cases, flag needless bloat, prefer clear concise writing, and consider the structure of the document and order of the sections

Files:

  • README.md
  • ROADMAP.md
🪛 LanguageTool
ROADMAP.md

[style] ~55-~55: This phrase is redundant (‘I’ stands for ‘interface’). Use simply “CLI”.
Context: ...s identity, same as ragenix today - [ ] CLI interface: omnix-age encrypt, `omnix-age decryp...

(ACRONYM_TAUTOLOGY)

🔇 Additional comments (18)
templates/do-service/config/secrets.nix (1)

1-4: Formatting-only refactor looks good.

No functional change introduced; this is a clean readability improvement.

templates/do-service/infra/secrets.nix (1)

1-4: Formatting update is safe.

Behavior and key bindings remain intact.

modules/firewall.nix (1)

3-6: No concerns in this segment.

This is a readability-only refactor.

modules/base.nix (1)

1-11: Formatting refactor is clean and consistent.

No functional regressions detected in these segments.

Also applies to: 46-49, 65-75

modules/disko.nix (1)

3-6: LGTM on this change.

Pure formatting reflow; behavior is unchanged.

modules/storage.nix (1)

3-6: Formatting-only changes are fine.

No functional delta in this block.

modules/digitalocean.nix (1)

1-11: Looks good.

These edits are readability-oriented and preserve behavior.

Also applies to: 33-36, 49-54

README.md (1)

39-39: No changes needed. The remote SSH functionality is actively provided by lib.mkTerraform via the remote wrapper in lib/terraform.nix, and the README description is accurate.

templates/do-service/os.nix (1)

3-6: LGTM!

The multi-line formatting improves readability without changing semantics.

Also applies to: 31-39

templates/do-service/keys.nix (1)

3-5: LGTM!

Formatting improvements to the role mappings. The placeholder keys are appropriately marked as examples.

Also applies to: 9-20

modules/services.nix (1)

8-26: LGTM!

The list-based construction for execStart with builtins.concatStringsSep is a clean approach for handling optional extra arguments.

lib/deploy.nix (1)

3-9: LGTM!

The signature and structural reformatting improves readability. The deploy profile construction logic is sound.

Also applies to: 38-60

ROADMAP.md (1)

74-92: Well-structured integration test flow.

The end-to-end lifecycle (provision → bootstrap → deploy → verify → redeploy → teardown) is clearly documented. The mkIntegrationTest lib function is a good abstraction for consumer extensibility.

lib/default.nix (1)

1-5: LGTM on the signature change.

The spread ... allows forward compatibility. The removal of nixpkgs from explicit parameters is appropriate since it's no longer used directly in lib functions.

templates/do-service/flake.nix (1)

10-17: LGTM on the per-system restructuring.

The use of flake-utils.lib.eachDefaultSystem with the // merge pattern properly separates system-independent outputs (nixosConfigurations, deploy) from per-system outputs (packages, devShells).

Also applies to: 50-112

lib/hooks.nix (2)

30-31: LGTM!

The defaultHooks // extraHooks merge order correctly allows consumers to override defaults with their custom hooks.


8-28: Verify hook structure matches git-hooks framework expectations.

The hook definitions (e.g., nixfmt.enable = true, deadnix = { enable = true; excludes = [...]; }) assume a specific structure expected by the git-hooks framework. The rustfmt hook uses custom fields (entry, files, pass_filenames) which may or may not be recognized.

What is the expected hook configuration structure for nix-community/git-hooks.nix?
lib/bootstrap.nix (1)

61-69: Good guard around rekeying.

Requiring both ragenixPkg and secretsRules avoids emitting a half-configured ragenix invocation.

Comment thread flake.nix
Comment on lines +85 to +99
checks.git-hooks = git-hooks.lib.${system}.run {
inherit hooks;
src = self;
};

formatter = pkgs.nixfmt;

devShells.default = pkgs.mkShell {
inherit (self.checks.${system}.git-hooks) shellHook;
packages = [
pkgs.nixfmt
pkgs.deadnix
pkgs.taplo
];
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Derive the dev-shell toolchain from the hook check.

Right now shellHook comes from checks.git-hooks, but the tool list is hand-maintained. That will drift from lib/hooks.nix:1-31 as soon as extraHooks or rustToolchain is used, and it skips the upstream enabledPackages pattern that brings the hook runner into the dev shell as well. (github.com)

♻️ Suggested refactor
-        devShells.default = pkgs.mkShell {
-          inherit (self.checks.${system}.git-hooks) shellHook;
-          packages = [
-            pkgs.nixfmt
-            pkgs.deadnix
-            pkgs.taplo
-          ];
-        };
+        devShells.default =
+          let
+            inherit (self.checks.${system}.git-hooks) shellHook enabledPackages;
+          in
+          pkgs.mkShell {
+            inherit shellHook;
+            buildInputs = enabledPackages;
+          };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flake.nix` around lines 85 - 99, The dev shell currently hardcodes packages
while deriving shellHook from checks.git-hooks; instead derive the devShell
toolchain from the same check so the shell follows upstream
enabledPackages/extraHooks/rustToolchain logic — update devShells.default to
import the packages and shellHook from checks.git-hooks
(checks.git-hooks.lib.${system}.enabledPackages or equivalent) so the hook
runner and any extraHooks/rustToolchain are included automatically, keep using
shellHook from checks.git-hooks and remove the manual package list
(nixfmt/deadnix/taplo) so toolchain changes in lib/hooks.nix propagate to the
dev shell.

Comment thread lib/bootstrap.nix
Comment thread lib/default.nix
Comment thread lib/hooks.nix
Comment thread lib/shell.nix
Comment thread modules/base.nix

system.activationScripts.per-service-profiles.text =
"mkdir -p /nix/var/nix/profiles/per-service";
system.activationScripts.per-service-profiles.text = "mkdir -p /nix/var/nix/profiles/per-service";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Prefer systemd.tmpfiles over activation shell for static directory creation.

Using an activation shell command here is more imperative than necessary. A tmpfiles rule is more idiomatic and keeps ownership/mode declarative.

♻️ Proposed refactor
-    system.activationScripts.per-service-profiles.text = "mkdir -p /nix/var/nix/profiles/per-service";
+    systemd.tmpfiles.rules = [
+      "d /nix/var/nix/profiles/per-service 0755 root root -"
+    ];

As per coding guidelines, *: "suggest more idiomatic or robust alternative(s)" and "flag potential footguns."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
system.activationScripts.per-service-profiles.text = "mkdir -p /nix/var/nix/profiles/per-service";
systemd.tmpfiles.rules = [
"d /nix/var/nix/profiles/per-service 0755 root root -"
];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/base.nix` at line 63, Replace the imperative activation script at
system.activationScripts.per-service-profiles.text that runs mkdir -p
/nix/var/nix/profiles/per-service with a declarative systemd tmpfiles rule: add
or replace configuration using systemd.tmpfiles.rules (or
systemd.tmpfiles.rules."per-service-profiles") to declare the directory path,
ownership and mode (e.g. d /nix/var/nix/profiles/per-service 0755 root root -)
so the directory is created declaratively with correct permissions instead of
using an activation shell command.

Comment thread modules/services.nix
Comment thread modules/storage.nix
Comment on lines 10 to 13
volumeName = lib.mkOption {
type = lib.types.str;
description =
"DigitalOcean volume name (used in /dev/disk/by-id/scsi-0DO_Volume_<name>)";
description = "DigitalOcean volume name (used in /dev/disk/by-id/scsi-0DO_Volume_<name>)";
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Constrain volumeName to safe characters to avoid invalid device paths.

volumeName is used in a by-id device path, but the option accepts any string. Inputs with spaces or path-like characters can break mounts.

🛡️ Proposed fix
     volumeName = lib.mkOption {
-      type = lib.types.str;
+      type = lib.types.strMatching "^[A-Za-z0-9._-]+$";
       description = "DigitalOcean volume name (used in /dev/disk/by-id/scsi-0DO_Volume_<name>)";
     };

As per coding guidelines, *: "Flag potential footguns" and "suggest more idiomatic or robust alternative(s)."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
volumeName = lib.mkOption {
type = lib.types.str;
description =
"DigitalOcean volume name (used in /dev/disk/by-id/scsi-0DO_Volume_<name>)";
description = "DigitalOcean volume name (used in /dev/disk/by-id/scsi-0DO_Volume_<name>)";
};
volumeName = lib.mkOption {
type = lib.types.strMatching "^[A-Za-z0-9._-]+$";
description = "DigitalOcean volume name (used in /dev/disk/by-id/scsi-0DO_Volume_<name>)";
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/storage.nix` around lines 10 - 13, The option volumeName currently
accepts any string which can produce invalid by-id device paths; add a
validation to the lib.mkOption for volumeName to restrict characters (e.g. only
alphanumerics, dot, underscore, hyphen) by adding a validate field such as
validate = name: lib.match "^[A-Za-z0-9._-]+$" name != null or similar, and
provide a clear errorMessage like "volumeName may only contain letters, digits,
'.', '_', '-'"; keep type = lib.types.str and reference the volumeName option
and lib.mkOption in your change.

Comment thread README.md
Comment thread templates/do-service/flake.nix
coderabbitai[bot]
coderabbitai Bot previously requested changes Mar 19, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
modules/base.nix (1)

26-29: ⚠️ Potential issue | 🟡 Minor

Removing the default for stateVersion is a breaking change.

Consumers relying on the previous default ("24.11") will now receive an evaluation error since stateVersion is a required option without a default. While the template (templates/do-service/os.nix) has been updated, any external users of this module will need to explicitly set omnix.base.stateVersion.

Consider adding an assertion with a helpful error message to guide users:

💡 Suggested assertion
   config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.stateVersion != "";
+        message = "omnix.base.stateVersion must be set explicitly (e.g., \"24.11\")";
+      }
+    ];
+
     services.openssh = {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/base.nix` around lines 26 - 29, The module removed the default for
the option stateVersion, which breaks consumers that relied on "24.11"; restore
a safe failure path by adding an assertion in modules/base.nix guarding
stateVersion: if the option is unset/null/empty, assert with a clear error
message that tells users to set omnix.base.stateVersion (or update to the new
templates/do-service/os.nix) and optionally mention the previous default value
("24.11") as a hint; locate the stateVersion lib.mkOption declaration and add
the assertion immediately after it so evaluation fails with the helpful message
instead of an obscure error.
♻️ Duplicate comments (8)
README.md (1)

42-42: 🛠️ Refactor suggestion | 🟠 Major

Add migration guidance for mkRemotemkGitHooks rename.

Users of the previous lib.mkRemote API will need guidance on how to migrate. Consider adding a brief note (e.g., in a "Migration" or "Changelog" section) explaining that mkRemote has been renamed to mkGitHooks and any signature changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 42, Add a short "Migration" note to the README explaining
that the old API function lib.mkRemote has been renamed to lib.mkGitHooks; state
the exact rename (lib.mkRemote → lib.mkGitHooks), list any signature changes
(parameter names, defaults, or removed/added options) and show the minimal
migration steps users must take (rename calls and update renamed options). Place
this note near the existing API table or in a new "Changelog / Migration"
section so users upgrading can quickly find it and cross-reference the symbols
mkRemote and mkGitHooks.
flake.nix (1)

97-104: 🧹 Nitpick | 🔵 Trivial

Dev shell packages are manually maintained and may drift from hooks.

The shellHook is derived from checks.git-hooks, but the package list (nixfmt, deadnix, taplo) is hardcoded. If lib/hooks.nix is extended with extraHooks or rustToolchain, the dev shell won't include the corresponding tools. Consider using enabledPackages from the git-hooks check output.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flake.nix` around lines 97 - 104, The dev shell currently hardcodes packages
in devShells.default using pkgs.mkShell while the shellHook comes from
self.checks.${system}.git-hooks, which can also emit enabledPackages; replace or
augment the hardcoded package list (pkgs.nixfmt, pkgs.deadnix, pkgs.taplo) with
the enabledPackages exported by self.checks.${system}.git-hooks (e.g., use
enabledPackages from that check and merge it with any unavoidable extras) so
that additions in lib/hooks.nix like extraHooks or rustToolchain automatically
appear in the dev shell; update references in devShells.default to use
self.checks.${system}.git-hooks.enabledPackages together with shellHook.
templates/do-service/flake.nix (1)

35-43: ⚠️ Potential issue | 🟡 Minor

Duplicate deployConfig with inconsistent package references.

Two deployConfig definitions exist:

  • Lines 37-41 (flake-level): uses self.packages.x86_64-linux.default
  • Lines 60-64 (per-system): uses pkgs.hello

While both currently resolve to the same placeholder, they will drift when pkgs.hello is replaced. Consider defining the package once and referencing it consistently.

Also applies to: 60-64

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@templates/do-service/flake.nix` around lines 35 - 43, The two deployConfig
blocks duplicate package selection inconsistently: one uses
self.packages.x86_64-linux.default and the other uses pkgs.hello; unify by
extracting a single package binding (e.g., define a variable like deployPackage
or package = self.packages.x86_64-linux.default) and reference that variable in
both omnix.lib.mkDeploy calls (the deployConfig in the flake-level and the
per-system deployConfig), ensuring both the package field passed into mkDeploy
and any per-system package references use the same identifier.
lib/hooks.nix (1)

1-5: 🧹 Nitpick | 🔵 Trivial

The ... catch-all silently discards pkgs.

Per flake.nix:83, callers pass { inherit pkgs; } but this function ignores it via .... Either document this behavior or remove the catch-all if no extra arguments are expected.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/hooks.nix` around lines 1 - 5, The function parameter list in
lib/hooks.nix currently swallows caller-provided pkgs via the catch-all "..."
which discards { pkgs } from callers; change the parameter list to explicitly
accept pkgs (e.g., replace "..., " with "pkgs" or "pkgs ? null" depending on
whether it should be required) and update any uses in the function to reference
that pkgs symbol (keep extraHooks and rustToolchain as before); this ensures
callers passing { inherit pkgs; } are not ignored and preserves explicit
argument semantics.
lib/bootstrap.nix (1)

18-24: ⚠️ Potential issue | 🟠 Major

Missing pkgs.openssh in runtimeInputs.

The script invokes ssh on lines 36 and 46, but openssh is not declared in runtimeInputs. writeShellApplication requires all runtime tools to be explicitly listed.

   runtimeInputs = [
     pkgs.rage
     pkgs.jq
     pkgs.gnused
+    pkgs.openssh
     nixos-anywhere.packages.${system}.default
   ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/bootstrap.nix` around lines 18 - 24, The runtimeInputs list is missing
pkgs.openssh while the script calls ssh (via writeShellApplication), so add
pkgs.openssh to the runtimeInputs array alongside pkgs.rage, pkgs.jq, and
pkgs.gnused; ensure it is included unconditionally (or follow the same
conditional pattern as ragenixPkg if intended) so ssh is available at runtime
and the writeShellApplication sandbox lists it as a runtime dependency.
lib/terraform.nix (1)

22-27: ⚠️ Potential issue | 🟠 Major

Missing pkgs.nix in buildInputs.

The inherited helpers encryptState and encryptVars from lib/shell.nix execute nix eval at runtime (see shell.nix lines 22-24 and 32-34). Since mkTask uses runtimeInputs = buildInputs, tasks like tfApply, tfDestroy, tfImport, tfRekey, and rekey will fail when invoking encryption helpers.

   buildInputs = [
     pkgs.terraform
     pkgs.rage
     pkgs.jq
+    pkgs.nix
   ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/terraform.nix` around lines 22 - 27, The buildInputs list in
lib/terraform.nix is missing pkgs.nix which causes the runtime helpers
encryptState and encryptVars (from lib/shell.nix) to fail when they run nix
eval; update the buildInputs expression used by mkTask (runtimeInputs =
buildInputs) to include pkgs.nix alongside pkgs.terraform, pkgs.rage, and
pkgs.jq, while preserving the conditional inclusion of ragenixPkg so
tfApply/tfDestroy/tfImport/tfRekey/rekey can invoke the encryption helpers
successfully.
lib/shell.nix (2)

37-43: ⚠️ Potential issue | 🟠 Major

Fail fast when droplet_ipv4 is missing.

jq -r outputs "null" (exit 0) if the path doesn't exist, allowing the script to continue with an invalid IP. Use jq -er to fail immediately. Also, the explicit rm -f on line 42 is redundant since the EXIT trap already handles cleanup.

🔧 Proposed fix
   resolveIp = ''
     ${parseIdentity}
     trap 'rm -f ${tfState}' EXIT
     ${decryptState}
-    host_ip=$(jq -r '.outputs.droplet_ipv4.value' ${tfState})
-    rm -f ${tfState}
+    host_ip=$(jq -er '.outputs.droplet_ipv4.value' ${tfState}) || {
+      echo "Error: Could not resolve droplet_ipv4 from ${tfState}" >&2
+      exit 1
+    }
   '';

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/shell.nix` around lines 37 - 43, In resolveIp, fail fast if droplet_ipv4
is missing by changing the jq call that assigns host_ip from using the tolerant
flag to the strict one (replace the current jq -r usage with jq -er so jq exits
non‑zero on missing/null), and remove the redundant explicit rm -f ${tfState} at
the end since the trap set earlier already cleans up; update references around
parseIdentity, decryptState and tfState accordingly so host_ip is only set when
jq succeeds.

28-30: ⚠️ Potential issue | 🟠 Major

Unconditional decryption fails if age file is missing.

Unlike decryptState, decryptVars doesn't check if ${tfVars}.age exists before attempting decryption. This will fail if the file is missing. Also, same restrictive permissions concern applies here.

🔧 Proposed fix
   decryptVars = ''
-    rage -d -i "$identity" ${tfVars}.age > ${tfVars}
+    if [ -f ${tfVars}.age ]; then
+      rm -f ${tfVars}
+      ( umask 077; rage -d -i "$identity" ${tfVars}.age > ${tfVars} )
+    else
+      echo "Error: ${tfVars}.age not found" >&2
+      exit 1
+    fi
   '';

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/shell.nix` around lines 28 - 30, The decryptVars block unconditionally
runs rage on ${tfVars}.age which fails if the .age file is missing and also
doesn't enforce restrictive permissions; update the decryptVars logic (the
decryptVars heredoc/variable) to first test for the existence of "${tfVars}.age"
and only run rage when it exists, mirroring the pattern used by decryptState,
and after writing the decrypted file set secure permissions (e.g., chmod 600) on
${tfVars} to avoid overly permissive files.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 17-20: The CI workflow currently uses floating refs for
third-party actions; update the `uses` entries for
DeterminateSystems/nix-installer-action and
DeterminateSystems/magic-nix-cache-action to pinned versions (replace
`DeterminateSystems/nix-installer-action@main` with
`DeterminateSystems/nix-installer-action@v21` and
`DeterminateSystems/magic-nix-cache-action@main` with
`DeterminateSystems/magic-nix-cache-action@v13`), or pin them further to
specific commit SHAs for stronger reproducibility; leave `actions/checkout@v4`
as-is unless you also want to pin that to a SHA.

In @.github/workflows/integration-test.yml:
- Around line 101-112: The SSH retry loop's ssh invocation may hang on
interactive password prompts; update the ssh command inside the retry loop (the
until ... ssh ... true 2>/dev/null line) to include the option -o BatchMode=yes
so the client fails immediately on authentication prompts, making the retries
deterministic and preventing hangs; keep the existing -o
StrictHostKeyChecking=no and -o ConnectTimeout=5 options and add BatchMode=yes
alongside the identity file flag (-i ~/.ssh/id_ed25519).
- Around line 59-64: Replace writing the DO_TOKEN plaintext into
infra/terraform.tfvars and instead export it as an environment variable
TF_VAR_do_token (so Terraform picks it up without writing secrets to disk) and
remove the do_token assignment from the here-doc; additionally add a
cleanup-on-failure step that deletes infra/terraform.tfvars if it exists (or
ensure the job's post/finally step removes the workflow directory) to guarantee
no secret file remains on error. Use the variable names do_token,
TF_VAR_do_token and the file infra/terraform.tfvars to locate the changes.

In `@lib/shell.nix`:
- Around line 7-13: The parseIdentity snippet sets identity and unconditionally
runs "shift 2" when $1 is "-i" without checking $2, allowing argument injection
or empty identity; modify parseIdentity to verify that a second argument exists
before assigning identity and shifting (e.g., test [ -n "${2:-}" ]), and if it's
missing, emit an error/usage message and exit or fall back to the default
identity; update the block around the identity assignment and the shift 2 call
to perform that presence check and handle the missing-argument case.
- Around line 32-35: The encryptVars block (encryptVars) should first verify the
source file (${tfVars}) exists and exit/error if missing, then evaluate
${keysFile} roles.infra to collect recipients and validate that the recipient
list is non-empty (similar to encryptState) before invoking rage; if the
recipient list is empty, log/raise a clear error and do not run rage. Update the
encryptVars sequence to perform a file existence check for tfVars, capture the
output of nix eval roles.infra into a variable, assert that variable is
non-empty, and only then call rage to produce ${tfVars}.age, mirroring the
recipient validation used in the encryptState logic.
- Around line 21-26: The encryptState block currently pipes nix eval output into
rage without validating recipients, so if nix eval (resolving ${keysFile}
roles.infra) fails or returns an empty list rage will produce an unreadable
file; update encryptState to capture the output of nix eval into a variable,
check its exit status and that the resulting recipient string is non-empty
before invoking rage (referencing encryptState, ${tfState}, ${keysFile}, and
roles.infra), and if the check fails emit an error and abort the script rather
than running rage with zero recipients.

In `@modules/acme.nix`:
- Around line 16-21: The module currently sets security.acme when cfg.enable is
true but does not guard or document nginx's enableACME interaction; add either
an assertion in the acme module (e.g., in config / top-level check using
cfg.enable and referencing nginx virtualHost enableACME) that fails when an
nginx virtualHost has enableACME = true while omnix.acme (cfg.enable) is false,
or add a clear documentation comment in this module explaining that nginx
virtualHost enableACME requires omnix.acme.enable (cfg.enable) and that
security.acme.acceptTerms and defaults.email must be provided via this module;
reference the existing config block (lib.mkIf cfg.enable { security.acme = {
acceptTerms = true; defaults.email = cfg.email; }; }) and the nginx template
setting enableACME to implement the assertion or docs.
- Around line 10-13: The email option is currently required even when
omnix.acme.enable is false; change the lib.mkOption for email to accept null or
string and provide a default null (e.g., use lib.types.nullOr lib.types.str with
default = null) or add an assertion that checks that email is non-null only when
enable is true (use the enable flag in the assertion). Update the option
definition named email and any related assertion so evaluation succeeds when
enable = false and enforces a real string when enable = true.

In `@modules/services.nix`:
- Around line 136-148: The owner/group fallback logic is duplicated; extract a
shared let binding (e.g., owner and group) above the tmpfiles map and
services.logrotate.settings so both the tmpfiles mapping (map (dir: "d ${dir}
0770 ${owner} ${group} -") over dataDirs ++ logDirs) and the logrotate su
directive ("${if cfg.user != null then cfg.user else "root"} ${if cfg.group !=
null then cfg.group else "root"}") reference the same owner and group variables;
update the tmpfiles code and services.logrotate.settings to use these shared
owner and group bindings (keep the exact names owner and group to minimize
churn) so the fallback logic is defined once and reused.
- Around line 166-171: Add a short explanatory comment above the users.groups
block clarifying the intent of the conditional and branches: that the outer
condition users.groups = lib.mkIf (cfg.user != null && !cfg.dynamicUser ||
cfg.group != null) creates group entries when either an explicit cfg.group is
provided or when a non-dynamic cfg.user should have a matching group, and that
lib.optionalAttrs (cfg.group != null) { ${cfg.group} = { }; } handles the
explicit group case while the ${cfg.user} = { }; branch creates an implicit
user-named group; place the comment immediately above the users.groups
assignment and reference cfg.user, cfg.group, and cfg.dynamicUser so future
maintainers understand the branching.

In `@ROADMAP.md`:
- Around line 111-114: Clarify the mkIntegrationTest entry by specifying its
expected signature and invocation pattern: describe that
mkIntegrationTest(config: {services: ServiceConfig[], keys: KeyConfig, nodeName:
string, ...}) => {run: (flow: LifecycleFlow) => Promise<TestResult>, setup: ()
=> Promise<void>, teardown: () => Promise<void>} and add a one-line usage
example showing how a consumer calls mkIntegrationTest(...) to obtain the
harness and then calls harness.run(myLifecycleFlow). Reference
mkIntegrationTest, LifecycleFlow, TestResult, setup, teardown, and run so
readers know what the function returns and how to invoke it.

In `@templates/do-service/.github/workflows/ci.yml`:
- Around line 50-54: The nix eval for HOST_KEY can fail and abort the workflow;
change the HOST_KEY assignment and append logic so it is defensive: run
HOST_KEY=$(nix eval --raw --file keys.nix keys.host 2>/dev/null || echo "") and
then check that HOST_KEY is non-empty before echoing to ~/.ssh/known_hosts
(similar to the HOST_IP guard); optionally log or fail with a clear message if
HOST_IP is set but HOST_KEY is empty. Ensure references: HOST_IP, HOST_KEY,
keys.nix, and ~/.ssh/known_hosts are used in the updated checks.

---

Outside diff comments:
In `@modules/base.nix`:
- Around line 26-29: The module removed the default for the option stateVersion,
which breaks consumers that relied on "24.11"; restore a safe failure path by
adding an assertion in modules/base.nix guarding stateVersion: if the option is
unset/null/empty, assert with a clear error message that tells users to set
omnix.base.stateVersion (or update to the new templates/do-service/os.nix) and
optionally mention the previous default value ("24.11") as a hint; locate the
stateVersion lib.mkOption declaration and add the assertion immediately after it
so evaluation fails with the helpful message instead of an obscure error.

---

Duplicate comments:
In `@flake.nix`:
- Around line 97-104: The dev shell currently hardcodes packages in
devShells.default using pkgs.mkShell while the shellHook comes from
self.checks.${system}.git-hooks, which can also emit enabledPackages; replace or
augment the hardcoded package list (pkgs.nixfmt, pkgs.deadnix, pkgs.taplo) with
the enabledPackages exported by self.checks.${system}.git-hooks (e.g., use
enabledPackages from that check and merge it with any unavoidable extras) so
that additions in lib/hooks.nix like extraHooks or rustToolchain automatically
appear in the dev shell; update references in devShells.default to use
self.checks.${system}.git-hooks.enabledPackages together with shellHook.

In `@lib/bootstrap.nix`:
- Around line 18-24: The runtimeInputs list is missing pkgs.openssh while the
script calls ssh (via writeShellApplication), so add pkgs.openssh to the
runtimeInputs array alongside pkgs.rage, pkgs.jq, and pkgs.gnused; ensure it is
included unconditionally (or follow the same conditional pattern as ragenixPkg
if intended) so ssh is available at runtime and the writeShellApplication
sandbox lists it as a runtime dependency.

In `@lib/hooks.nix`:
- Around line 1-5: The function parameter list in lib/hooks.nix currently
swallows caller-provided pkgs via the catch-all "..." which discards { pkgs }
from callers; change the parameter list to explicitly accept pkgs (e.g., replace
"..., " with "pkgs" or "pkgs ? null" depending on whether it should be required)
and update any uses in the function to reference that pkgs symbol (keep
extraHooks and rustToolchain as before); this ensures callers passing { inherit
pkgs; } are not ignored and preserves explicit argument semantics.

In `@lib/shell.nix`:
- Around line 37-43: In resolveIp, fail fast if droplet_ipv4 is missing by
changing the jq call that assigns host_ip from using the tolerant flag to the
strict one (replace the current jq -r usage with jq -er so jq exits non‑zero on
missing/null), and remove the redundant explicit rm -f ${tfState} at the end
since the trap set earlier already cleans up; update references around
parseIdentity, decryptState and tfState accordingly so host_ip is only set when
jq succeeds.
- Around line 28-30: The decryptVars block unconditionally runs rage on
${tfVars}.age which fails if the .age file is missing and also doesn't enforce
restrictive permissions; update the decryptVars logic (the decryptVars
heredoc/variable) to first test for the existence of "${tfVars}.age" and only
run rage when it exists, mirroring the pattern used by decryptState, and after
writing the decrypted file set secure permissions (e.g., chmod 600) on ${tfVars}
to avoid overly permissive files.

In `@lib/terraform.nix`:
- Around line 22-27: The buildInputs list in lib/terraform.nix is missing
pkgs.nix which causes the runtime helpers encryptState and encryptVars (from
lib/shell.nix) to fail when they run nix eval; update the buildInputs expression
used by mkTask (runtimeInputs = buildInputs) to include pkgs.nix alongside
pkgs.terraform, pkgs.rage, and pkgs.jq, while preserving the conditional
inclusion of ragenixPkg so tfApply/tfDestroy/tfImport/tfRekey/rekey can invoke
the encryption helpers successfully.

In `@README.md`:
- Line 42: Add a short "Migration" note to the README explaining that the old
API function lib.mkRemote has been renamed to lib.mkGitHooks; state the exact
rename (lib.mkRemote → lib.mkGitHooks), list any signature changes (parameter
names, defaults, or removed/added options) and show the minimal migration steps
users must take (rename calls and update renamed options). Place this note near
the existing API table or in a new "Changelog / Migration" section so users
upgrading can quickly find it and cross-reference the symbols mkRemote and
mkGitHooks.

In `@templates/do-service/flake.nix`:
- Around line 35-43: The two deployConfig blocks duplicate package selection
inconsistently: one uses self.packages.x86_64-linux.default and the other uses
pkgs.hello; unify by extracting a single package binding (e.g., define a
variable like deployPackage or package = self.packages.x86_64-linux.default) and
reference that variable in both omnix.lib.mkDeploy calls (the deployConfig in
the flake-level and the per-system deployConfig), ensuring both the package
field passed into mkDeploy and any per-system package references use the same
identifier.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 41e79b10-0845-43be-a20c-0429c734948a

📥 Commits

Reviewing files that changed from the base of the PR and between 5fa8154 and 4041129.

⛔ Files ignored due to path filters (1)
  • flake.lock is excluded by !**/*.lock
📒 Files selected for processing (28)
  • .github/workflows/ci.yml
  • .github/workflows/integration-test.yml
  • README.md
  • ROADMAP.md
  • flake.nix
  • lib/bootstrap.nix
  • lib/default.nix
  • lib/deploy.nix
  • lib/hooks.nix
  • lib/remote.nix
  • lib/shell.nix
  • lib/terraform.nix
  • modules/acme.nix
  • modules/base.nix
  • modules/digitalocean.nix
  • modules/disko.nix
  • modules/firewall.nix
  • modules/services.nix
  • modules/storage.nix
  • templates/do-service/.github/workflows/ci.yml
  • templates/do-service/config/secrets.nix
  • templates/do-service/flake.nix
  • templates/do-service/infra/secrets.nix
  • templates/do-service/infra/terraform.tfvars.example
  • templates/do-service/infra/variables.tf
  • templates/do-service/keys.nix
  • templates/do-service/os.nix
  • templates/do-service/services.nix
💤 Files with no reviewable changes (1)
  • lib/remote.nix
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Flake check
🧰 Additional context used
📓 Path-based instructions (2)
*

⚙️ CodeRabbit configuration file

Focus on providing constructive criticism. Whenever you see a suboptimal approach, suggest more idiomatic or robust alternative(s). Flag potential footguns. Suggest FP alternatives to mutable/imperative code. Point out architectural flaws like leaky abstractions, tight coupling, wrong level of abstraction, poor type modeling, over-abstraction, unclear domain boundaries. Code should generally be organized based on business concerns rather than technical aspects - suggest improvements if you find violations. Point out gaps in test coverage but suggest tests that are not too coupled to the implementation and actually test domain invariants and business logic.

Files:

  • README.md
  • ROADMAP.md
  • flake.nix
**/*.md

⚙️ CodeRabbit configuration file

Focus on the contents of the docs and not on cosmetic things like markdown formatting. We use markdown files for various docs including but not limited to the north star system specification, SPEC.md, the plan for how to get there, ROADMAP.md, guidelines for AI contributors, AGENTS.md, project overview and instructions for human contributors, README.md. Think about the target audience of a document when deciding what comment to leave. For specifications and designs, suggest potential product, architectural, and UI/UX improvements. For plans, suggest changes that would make things more parallelizable and deliverable-focused. For instructions, suggest better rules and guidelines and point out missing instructions. In all cases, flag needless bloat, prefer clear concise writing, and consider the structure of the document and order of the sections

Files:

  • README.md
  • ROADMAP.md
🔇 Additional comments (42)
modules/storage.nix (2)

10-13: The volumeName validation concern was previously raised.

The past review already flagged that volumeName accepts arbitrary strings which could break device paths. That feedback still applies.


3-6: LGTM!

Formatting refactor to multi-line let ... in block improves readability and aligns with the PR's consistent style.

modules/base.nix (2)

62-62: The activation script vs. tmpfiles concern was previously raised.

The past review already suggested using systemd.tmpfiles.rules instead of system.activationScripts for directory creation. That feedback still applies.


64-73: LGTM!

Multi-line formatting with explicit with pkgs; scope and ++ cfg.extraPackages concatenation is clean and readable.

templates/do-service/infra/terraform.tfvars.example (1)

1-2: LGTM!

Clear instructions guiding users toward the age-encrypted workflow instead of plaintext secrets.

templates/do-service/infra/variables.tf (2)

6-9: LGTM!

Good defensive validation to catch empty API tokens early in the Terraform plan phase rather than at API call time.


35-38: LGTM!

Appropriate minimum constraint aligning with DigitalOcean's block storage requirements.

modules/digitalocean.nix (2)

12-15: LGTM!

Helpful inline documentation clarifying NixOS module import semantics—imports are always evaluated but runtime config is properly gated by lib.mkIf.


37-40: LGTM!

Multi-line list formatting improves readability.

Also applies to: 53-58

lib/deploy.nix (2)

3-10: LGTM!

Good addition of targetSystem parameter with sensible default. This enables cross-architecture deployment scenarios while maintaining backward compatibility.

Also applies to: 13-14


93-94: LGTM!

Correct logic: --remote-build is appropriately added when localSystem differs from the target (non-x86_64-linux), ensuring builds happen on the remote when cross-compiling isn't available locally.

templates/do-service/services.nix (1)

4-4: LGTM!

Service-specific directory name (my-service) is more descriptive than the generic prod and follows a clearer naming convention.

templates/do-service/infra/secrets.nix (1)

1-4: LGTM!

Formatting refactor to multi-line let ... in block aligns with the PR's consistent style.

lib/default.nix (2)

7-7: Verify mkGitHooks signature matches callers.

The rename from mkRemote to mkGitHooks aligns with the new git-hooks functionality. However, as noted in a previous review, callers may be passing unused parameters (e.g., pkgs). Ensure the ./hooks.nix signature matches what callers provide.

#!/bin/bash
# Check how mkGitHooks is called and what parameters hooks.nix expects
echo "=== Callers of mkGitHooks ==="
rg -n "mkGitHooks" --type=nix -A 3

echo ""
echo "=== hooks.nix parameter signature ==="
head -20 lib/hooks.nix

1-1: LGTM!

Using ... for additional attributes makes the interface more flexible for future extensions.

templates/do-service/config/secrets.nix (1)

1-6: LGTM!

The reformatting to multi-line let...in is consistent with the broader Nix formatting patterns in this PR.

modules/disko.nix (1)

17-22: LGTM!

Good defensive assertion—fails early with a clear, actionable error message when the upstream disko module is missing. This prevents confusing downstream errors and guides users to the correct solution.

templates/do-service/.github/workflows/ci.yml (1)

34-34: LGTM!

Good addition to support both master and main as default branch names.

README.md (1)

33-33: LGTM!

Clear documentation that omnix.nixosModules.default bundles upstream dependencies, reducing boilerplate for users.

templates/do-service/keys.nix (1)

1-22: LGTM!

The multi-line formatting for role arrays improves readability. The placeholder keys are clearly marked for replacement.

templates/do-service/os.nix (1)

1-11: LGTM!

Good cleanup: removing unused pkgs parameter and explicitly setting stateVersion = "24.11" aligns with the base module changes that removed the default. The nginx formatting improvements enhance readability.

modules/firewall.nix (1)

3-26: LGTM!

Clean use of lib.optional for conditional port inclusion. The new enableHTTP/enableHTTPS options provide a semantic abstraction over raw port numbers with helpful descriptions (e.g., noting HTTP is needed for ACME challenges).

lib/bootstrap.nix (1)

61-69: LGTM on the conditional rekeying refactor.

The conditional block structure using ${ if ... then '' ... '' else "" } is clear and properly handles the optional ragenix rekeying step.

lib/hooks.nix (1)

7-31: LGTM!

The hook configuration is well-structured: deadnix correctly excludes templates (which may contain intentionally unused bindings for demonstration), and the conditional rustfmt hook cleanly integrates when a Rust toolchain is provided. Using defaultHooks // extraHooks allows callers to override or extend hooks.

lib/terraform.nix (1)

132-143: LGTM on the remote wrapper.

The remote script correctly declares pkgs.openssh in its own runtimeInputs, separate from the Terraform task inputs. This is the right approach since not all tasks need SSH.

templates/do-service/flake.nix (1)

27-30: LGTM on module consolidation.

Using omnix.nixosModules.default instead of importing individual modules simplifies the template. Per the context snippet (flake.nix:46-59), the default module transitively includes all required modules including upstream disko and ragenix.

flake.nix (2)

44-57: LGTM on acme module integration.

The new acme module is properly gated behind lib.mkIf cfg.enable (per context snippet modules/acme.nix:16), so including it in nixosModules.default won't affect consumers who don't opt in.


86-88: Good defensive error handling for disko package.

Using or (throw "...") instead of or null provides a clear failure message when disko isn't available for a given system, rather than silently producing null which could cause confusing downstream errors.

modules/services.nix (2)

45-53: Good refactor to lib.optionalAttrs.

The conditional serviceConfig attributes are now idiomatically expressed. This is cleaner than the previous // (if cond then { ... } else { }) pattern.


110-114: LGTM on the new logDir option.

The logDir option with automatic tmpfiles directory creation and logrotate configuration is a useful addition that reduces boilerplate for service definitions.

lib/shell.nix (1)

15-19: Force restrictive permissions on decrypted state file.

The decrypted state file is written with caller-controlled permissions. Since Terraform state can contain secrets, enforce restrictive permissions.

ROADMAP.md (4)

3-21: LGTM!

Clear documentation of resolved review findings. The checklist format is appropriate for tracking completed work.


23-38: LGTM!

Good documentation of the refactoring status. The completed item aligns with the lib/shell.nix changes in this PR.


40-81: LGTM!

The section addresses all previous review findings: generic workflow description instead of consumer-specific references, parallel paths in the diagram, explicit "Optional:" labeling for secretspec integration, and fixed "CLI interface" redundancy.


115-134: LGTM!

Good separation of future nice-to-haves from completed milestones. The completed section provides useful context about the extraction history.

.github/workflows/integration-test.yml (7)

15-21: LGTM!

Good concurrency configuration. Setting cancel-in-progress: false ensures the cleanup step runs even if a new workflow is queued, preventing orphaned infrastructure.


42-48: LGTM!

Correct SSH key setup with proper permissions and cleanup at the end of the workflow.


94-99: LGTM!

Good practice masking the IP address in workflow logs via ::add-mask::.


136-147: LGTM!

Good approach: fetch the real host key after bootstrap, update keys.nix for future deploys, and refresh known_hosts for strict host key verification in subsequent SSH connections.


149-171: LGTM!

Good verification sequence: SSH connectivity, OS identity, and firewall state. The fallback warning for nftables acknowledges the limitation gracefully.


173-180: LGTM!

Proper cleanup with if: always() ensures infrastructure teardown even on failure, preventing orphaned resources and cloud cost leakage.


114-120: No action needed. The flake configuration name correctly remains my-service.

The sed commands on lines 83-84 modify the content within infra/main.tf and os.nix (e.g., hostnames, resource identifiers), but they do not modify flake.nix. Since projectName = "my-service" is hardcoded in the template's flake.nix (line 21) and not affected by the sed substitutions, the nixosConfigurations.my-service attribute is correctly referenced as .#my-service on line 117.

			> Likely an incorrect or invalid review comment.

Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/integration-test.yml Outdated
Comment on lines +59 to +64
cat > infra/terraform.tfvars <<'VARS'
do_token = "${{ secrets.DO_TOKEN }}"
ssh_key_name = "omnix-integration-test"
region = "${{ inputs.region }}"
droplet_size = "${{ inputs.droplet_size }}"
VARS

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Plaintext secrets in terraform.tfvars.

The DO_TOKEN is written in plaintext to terraform.tfvars. While acceptable for CI (file is ephemeral), ensure the workflow directory is cleaned up on failure. Consider using environment variables with TF_VAR_do_token instead to avoid writing secrets to disk.

🛡️ Alternative using environment variables
+      - name: Set Terraform vars
+        run: |
+          echo "TF_VAR_do_token=${{ secrets.DO_TOKEN }}" >> "$GITHUB_ENV"

       - name: Scaffold test project from template
         run: |
           # ... existing scaffold steps ...
           
-          # Write test terraform vars (encrypted in real usage, plaintext here)
-          cat > infra/terraform.tfvars <<'VARS'
-          do_token = "${{ secrets.DO_TOKEN }}"
-          ssh_key_name = "omnix-integration-test"
-          region = "${{ inputs.region }}"
-          droplet_size = "${{ inputs.droplet_size }}"
-          VARS
+          # Write non-sensitive terraform vars (DO_TOKEN via TF_VAR_do_token env)
+          cat > infra/terraform.tfvars <<VARS
+          ssh_key_name = "omnix-integration-test"
+          region = "${{ inputs.region }}"
+          droplet_size = "${{ inputs.droplet_size }}"
+          VARS
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/integration-test.yml around lines 59 - 64, Replace writing
the DO_TOKEN plaintext into infra/terraform.tfvars and instead export it as an
environment variable TF_VAR_do_token (so Terraform picks it up without writing
secrets to disk) and remove the do_token assignment from the here-doc;
additionally add a cleanup-on-failure step that deletes infra/terraform.tfvars
if it exists (or ensure the job's post/finally step removes the workflow
directory) to guarantee no secret file remains on error. Use the variable names
do_token, TF_VAR_do_token and the file infra/terraform.tfvars to locate the
changes.

Comment on lines +101 to +112
- name: Wait for SSH
run: |
retries=0
until ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
-i ~/.ssh/id_ed25519 "root@$HOST_IP" true 2>/dev/null; do
retries=$((retries + 1))
if [ "$retries" -ge 30 ]; then
echo "SSH not available after 2.5 minutes" >&2
exit 1
fi
sleep 5
done

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding BatchMode=yes to SSH options.

Adding BatchMode=yes prevents the SSH client from hanging on unexpected password prompts or interactive authentication, making the retry loop more robust.

🔧 Proposed fix
       - name: Wait for SSH
         run: |
           retries=0
-          until ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
+          until ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes \
             -i ~/.ssh/id_ed25519 "root@$HOST_IP" true 2>/dev/null; do
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Wait for SSH
run: |
retries=0
until ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
-i ~/.ssh/id_ed25519 "root@$HOST_IP" true 2>/dev/null; do
retries=$((retries + 1))
if [ "$retries" -ge 30 ]; then
echo "SSH not available after 2.5 minutes" >&2
exit 1
fi
sleep 5
done
- name: Wait for SSH
run: |
retries=0
until ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes \
-i ~/.ssh/id_ed25519 "root@$HOST_IP" true 2>/dev/null; do
retries=$((retries + 1))
if [ "$retries" -ge 30 ]; then
echo "SSH not available after 2.5 minutes" >&2
exit 1
fi
sleep 5
done
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/integration-test.yml around lines 101 - 112, The SSH retry
loop's ssh invocation may hang on interactive password prompts; update the ssh
command inside the retry loop (the until ... ssh ... true 2>/dev/null line) to
include the option -o BatchMode=yes so the client fails immediately on
authentication prompts, making the retries deterministic and preventing hangs;
keep the existing -o StrictHostKeyChecking=no and -o ConnectTimeout=5 options
and add BatchMode=yes alongside the identity file flag (-i ~/.ssh/id_ed25519).

Comment thread lib/shell.nix
Comment on lines +7 to +13
parseIdentity = ''
identity=~/.ssh/id_ed25519
if [ "''${1:-}" = "-i" ]; then
identity="$2"
shift 2
fi
'';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

parseIdentity allows arbitrary argument injection.

The shift 2 unconditionally runs after checking only $1, but doesn't validate that $2 actually exists. If called with -i but no second argument, $identity becomes empty and subsequent commands may fail confusingly or use unintended defaults.

🛡️ Proposed fix
   parseIdentity = ''
     identity=~/.ssh/id_ed25519
     if [ "''${1:-}" = "-i" ]; then
+      if [ -z "''${2:-}" ]; then
+        echo "Error: -i requires an identity file argument" >&2
+        exit 1
+      fi
       identity="$2"
       shift 2
     fi
   '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/shell.nix` around lines 7 - 13, The parseIdentity snippet sets identity
and unconditionally runs "shift 2" when $1 is "-i" without checking $2, allowing
argument injection or empty identity; modify parseIdentity to verify that a
second argument exists before assigning identity and shifting (e.g., test [ -n
"${2:-}" ]), and if it's missing, emit an error/usage message and exit or fall
back to the default identity; update the block around the identity assignment
and the shift 2 call to perform that presence check and handle the
missing-argument case.

Comment thread lib/shell.nix
Comment on lines +21 to +26
encryptState = ''
if [ -f ${tfState} ]; then
nix eval --raw --file ${keysFile} roles.infra --apply 'builtins.concatStringsSep "\n"' \
| rage -e -R /dev/stdin -o ${tfState}.age ${tfState}
fi
'';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Silent failure if key resolution fails.

If nix eval fails (e.g., missing roles.infra in keysFile), the pipeline silently continues with no recipients. rage will encrypt to zero recipients, producing a file that cannot be decrypted. Consider validating the key list before encryption.

🛡️ Proposed fix
   encryptState = ''
     if [ -f ${tfState} ]; then
-      nix eval --raw --file ${keysFile} roles.infra --apply 'builtins.concatStringsSep "\n"' \
-        | rage -e -R /dev/stdin -o ${tfState}.age ${tfState}
+      recipients=$(nix eval --raw --file ${keysFile} roles.infra --apply 'builtins.concatStringsSep "\n"')
+      if [ -z "$recipients" ]; then
+        echo "Error: No recipients found in ${keysFile} roles.infra" >&2
+        exit 1
+      fi
+      echo "$recipients" | rage -e -R /dev/stdin -o ${tfState}.age ${tfState}
     fi
   '';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
encryptState = ''
if [ -f ${tfState} ]; then
nix eval --raw --file ${keysFile} roles.infra --apply 'builtins.concatStringsSep "\n"' \
| rage -e -R /dev/stdin -o ${tfState}.age ${tfState}
fi
'';
encryptState = ''
if [ -f ${tfState} ]; then
recipients=$(nix eval --raw --file ${keysFile} roles.infra --apply 'builtins.concatStringsSep "\n"')
if [ -z "$recipients" ]; then
echo "Error: No recipients found in ${keysFile} roles.infra" >&2
exit 1
fi
echo "$recipients" | rage -e -R /dev/stdin -o ${tfState}.age ${tfState}
fi
'';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/shell.nix` around lines 21 - 26, The encryptState block currently pipes
nix eval output into rage without validating recipients, so if nix eval
(resolving ${keysFile} roles.infra) fails or returns an empty list rage will
produce an unreadable file; update encryptState to capture the output of nix
eval into a variable, check its exit status and that the resulting recipient
string is non-empty before invoking rage (referencing encryptState, ${tfState},
${keysFile}, and roles.infra), and if the check fails emit an error and abort
the script rather than running rage with zero recipients.

Comment thread modules/acme.nix
Comment on lines +16 to +21
config = lib.mkIf cfg.enable {
security.acme = {
acceptTerms = true;
defaults.email = cfg.email;
};
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider documenting the relationship with nginx ACME.

The template at templates/do-service/os.nix configures nginx with enableACME = true independently of this module. If a user sets enableACME on an nginx virtualHost without enabling omnix.acme, they'll get NixOS's default ACME behavior (which requires security.acme.acceptTerms and email to be set elsewhere). Consider adding an assertion or documentation clarifying that omnix.acme.enable should be set when using ACME features.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/acme.nix` around lines 16 - 21, The module currently sets
security.acme when cfg.enable is true but does not guard or document nginx's
enableACME interaction; add either an assertion in the acme module (e.g., in
config / top-level check using cfg.enable and referencing nginx virtualHost
enableACME) that fails when an nginx virtualHost has enableACME = true while
omnix.acme (cfg.enable) is false, or add a clear documentation comment in this
module explaining that nginx virtualHost enableACME requires omnix.acme.enable
(cfg.enable) and that security.acme.acceptTerms and defaults.email must be
provided via this module; reference the existing config block (lib.mkIf
cfg.enable { security.acme = { acceptTerms = true; defaults.email = cfg.email;
}; }) and the nginx template setting enableACME to implement the assertion or
docs.

Comment thread modules/services.nix
Comment thread modules/services.nix
Comment thread ROADMAP.md
Comment on lines +111 to +114
- [ ] `mkIntegrationTest` lib function -- let consumers define their own
lifecycle test flows using the same harness, parameterized by their
project-specific config (services, keys, node name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Clarify the mkIntegrationTest deliverable.

The description mentions parameterization by "project-specific config (services, keys, node name)" but doesn't specify what the function returns or how consumers would invoke it. Consider adding expected signature or usage pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ROADMAP.md` around lines 111 - 114, Clarify the mkIntegrationTest entry by
specifying its expected signature and invocation pattern: describe that
mkIntegrationTest(config: {services: ServiceConfig[], keys: KeyConfig, nodeName:
string, ...}) => {run: (flow: LifecycleFlow) => Promise<TestResult>, setup: ()
=> Promise<void>, teardown: () => Promise<void>} and add a one-line usage
example showing how a consumer calls mkIntegrationTest(...) to obtain the
harness and then calls harness.run(myLifecycleFlow). Reference
mkIntegrationTest, LifecycleFlow, TestResult, setup, teardown, and run so
readers know what the function returns and how to invoke it.

Comment thread templates/do-service/.github/workflows/ci.yml
This was referenced Mar 21, 2026
@0xgleb 0xgleb marked this pull request as draft March 21, 2026 07:21
@0xgleb 0xgleb marked this pull request as ready for review March 21, 2026 13:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
modules/firewall.nix (1)

13-27: 🧹 Nitpick | 🔵 Trivial

Add module tests for the new port-toggle invariants.

Please add coverage for combinations of enable, enableHTTP, enableHTTPS, and custom allowedTCPPorts to lock in the expected invariant: SSH always present when enabled, and 80/443 only when toggled.

As per coding guidelines, *: "Point out gaps in test coverage ... test domain invariants and business logic."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/firewall.nix` around lines 13 - 27, Add unit/module tests that assert
the firewall.allowedTCPPorts list produced by this module respects the
invariants: when cfg.enable is true the list always contains 22 (SSH), port 80
is present iff cfg.enableHTTP is true, port 443 is present iff cfg.enableHTTPS
is true, and any entries from cfg.allowedTCPPorts are appended; conversely when
cfg.enable is false the module should not enable networking.firewall. Create
test cases that exercise combinations of enable=true/false with
enableHTTP=true/false, enableHTTPS=true/false and non-empty allowedTCPPorts to
validate the exact resulting list produced by the config expression that sets
networking.firewall.allowedTCPPorts (the expression using [ 22 ] ++ httpPorts ++
httpsPorts ++ cfg.allowedTCPPorts).
templates/do-service/flake.nix (1)

53-58: 🧹 Nitpick | 🔵 Trivial

system parameter is unused by mkTerraform.

Per the function signature in lib/terraform.nix, system is absorbed by ... and not used internally—the function only needs pkgs, keysFile, and optional ragenixPkg/secretsRules. Passing it is harmless but misleading.

♻️ Remove unused parameter
         infraPkgs = omnix.lib.mkTerraform {
-          inherit pkgs system;
+          inherit pkgs;
           keysFile = ./keys.nix;
           ragenixPkg = omnix.inputs.ragenix.packages.${system}.default;
           secretsRules = ./config/secrets.nix;
         };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@templates/do-service/flake.nix` around lines 53 - 58, The call to
omnix.lib.mkTerraform is mistakenly passing the unused parameter system via
"inherit pkgs system"; update the mkTerraform invocation (infraPkgs) to only
pass the required arguments (pkgs, keysFile, and the optional ragenixPkg and
secretsRules) by removing system from the inherit list (i.e., use "inherit
pkgs;") so mkTerraform is not given the unused system parameter while keeping
ragenixPkg = omnix.inputs.ragenix.packages.${system}.default and secretsRules
unchanged.
♻️ Duplicate comments (9)
modules/storage.nix (1)

10-13: ⚠️ Potential issue | 🟠 Major

Constrain volumeName to a safe character set before path interpolation.

volumeName is still an unrestricted string but is used in a device path (Line 24). Invalid characters can break mounts or generate invalid paths.

Proposed fix
     volumeName = lib.mkOption {
-      type = lib.types.str;
+      type = lib.types.strMatching "^[A-Za-z0-9._-]+$";
       description = "DigitalOcean volume name (used in /dev/disk/by-id/scsi-0DO_Volume_<name>)";
     };

Also applies to: 24-24

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/storage.nix` around lines 10 - 13, The option volumeName must be
constrained to a safe character set before it’s used in the device path; update
the volumeName mkOption to include a validate function that rejects any value
not matching a safe regex (e.g. ^[A-Za-z0-9._-]+$) using builtins.match and
throw a clear error message, and keep using the validated volumeName where the
device path is interpolated so only allowed characters can reach that path
construction.
lib/shell.nix (3)

9-11: ⚠️ Potential issue | 🟡 Minor

Validate -i argument before shift 2.

Line 9 checks only $1; if -i is passed without a value, identity handling becomes invalid and error behavior is opaque.

Suggested fix
   parseIdentity = ''
     identity=~/.ssh/id_ed25519
     if [ "''${1:-}" = "-i" ]; then
+      if [ -z "''${2:-}" ]; then
+        echo "Error: -i requires an identity file argument" >&2
+        exit 1
+      fi
       identity="$2"
       shift 2
     fi
   '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/shell.nix` around lines 9 - 11, The script checks for the -i flag then
immediately consumes two args (identity="$2" and shift 2) which breaks if -i is
passed without a value; update the branch that handles the -i flag to validate
that the second positional parameter exists (check "${2:-}" or use -z) before
assigning to the identity variable and calling shift 2, and if the value is
missing print a clear error and exit non‑zero; keep references to the identity
assignment (identity="$2") and the shift 2 call so you modify that exact block.

24-26: ⚠️ Potential issue | 🟠 Major

Guard encryptState against empty/failed recipient resolution.

If nix eval fails or resolves no recipients, the current pipeline can still produce a broken encrypted file.

Suggested fix
   encryptState = ''
     if [ -f ${tfState} ]; then
-      nix eval --raw --file ${keysFile} roles.infra --apply 'builtins.concatStringsSep "\n"' \
-        | rage -e -R /dev/stdin -o ${tfState}.age ${tfState}
+      recipients=$(nix eval --raw --file ${keysFile} roles.infra --apply 'builtins.concatStringsSep "\n"') || {
+        echo "Error: failed to resolve roles.infra from ${keysFile}" >&2
+        exit 1
+      }
+      if [ -z "$recipients" ]; then
+        echo "Error: no recipients found in ${keysFile} roles.infra" >&2
+        exit 1
+      fi
+      echo "$recipients" | rage -e -R /dev/stdin -o ${tfState}.age ${tfState}
     fi
   '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/shell.nix` around lines 24 - 26, The pipeline that resolves recipients
via "nix eval --file ${keysFile} roles.infra ..." must be validated before
attempting to encrypt: capture the output of that command into a variable (used
by encryptState), check that it succeeded and is non-empty, and if it failed or
produced no recipients, print an error and exit non-zero instead of piping to
rage; update the logic around encryptState/tfState to only call rage when the
recipients string is non-empty and to propagate nix eval errors (e.g., check its
exit status or test -n "$recipients"), referencing the existing symbols: nix
eval (roles.infra), ${keysFile}, ${tfState}, and the encrypt step that invokes
rage.

34-37: ⚠️ Potential issue | 🟠 Major

Add source-file and recipient validation in encryptVars.

encryptVars currently skips both ${tfVars} existence checks and recipient validation, so failure modes are silent/unhelpful.

Suggested fix
   encryptVars = ''
-    nix eval --raw --file ${keysFile} roles.infra --apply 'builtins.concatStringsSep "\n"' \
-      | rage -e -R /dev/stdin -o ${tfVars}.age ${tfVars}
+    if [ ! -f ${tfVars} ]; then
+      echo "Error: ${tfVars} not found" >&2
+      exit 1
+    fi
+    recipients=$(nix eval --raw --file ${keysFile} roles.infra --apply 'builtins.concatStringsSep "\n"') || {
+      echo "Error: failed to resolve roles.infra from ${keysFile}" >&2
+      exit 1
+    }
+    if [ -z "$recipients" ]; then
+      echo "Error: no recipients found in ${keysFile} roles.infra" >&2
+      exit 1
+    fi
+    echo "$recipients" | rage -e -R /dev/stdin -o ${tfVars}.age ${tfVars}
   '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/shell.nix` around lines 34 - 37, encryptVars currently pipes data into
rage without validating that the source file (${tfVars}) exists or that
recipients from ${keysFile} are valid; update the encryptVars block to first
check that ${tfVars} exists and is non-empty and that the recipient list parsed
from ${keysFile} (roles.infra) yields at least one valid recipient, failing with
a clear error message via builtins.abort or nix's builtins.trace if checks fail,
then proceed to run the nix eval | rage pipeline only when validations pass;
reference the symbols encryptVars, keysFile, roles.infra and ${tfVars} when
adding these guards so the logic is easy to locate.
modules/acme.nix (1)

10-13: ⚠️ Potential issue | 🟠 Major

Make email optional unless ACME is enabled.

Line 10 currently makes omnix.acme.email mandatory even when omnix.acme.enable = false, which can cause unnecessary evaluation failures for users not using ACME.

Suggested fix
   options.omnix.acme = {
@@
     email = lib.mkOption {
-      type = lib.types.str;
+      type = lib.types.nullOr lib.types.str;
+      default = null;
       description = "Email address for ACME renewal notifications";
     };
   };

   config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.email != null && cfg.email != "";
+        message = "omnix.acme.email must be set when omnix.acme.enable is true";
+      }
+    ];
     security.acme = {
       acceptTerms = true;
       defaults.email = cfg.email;
     };
   };

Also applies to: 16-21

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/acme.nix` around lines 10 - 13, The email option is currently
required even when ACME is disabled; change the omnix.acme.email option to be
nullable and only required when omnix.acme.enable is true by setting its type to
a nullable string (e.g., lib.types.nullOr lib.types.str) and providing a default
null, then add a validation/assertion in the module config that enforces
config.omnix.acme.email is non-null only if config.omnix.acme.enable is true
(apply the same pattern to the other options referenced around lines 16-21).
.github/workflows/ci.yml (1)

18-19: ⚠️ Potential issue | 🟡 Minor

Pin third-party actions to immutable refs.

Using @main can break CI unpredictably and weakens supply-chain reproducibility.

#!/bin/bash
# Read-only verification: detect floating refs for third-party actions
rg -nP 'DeterminateSystems/(nix-installer-action|magic-nix-cache-action)@main' .github/workflows templates/do-service/.github/workflows -g '*.yml'

As per coding guidelines, *: "Flag potential footguns" and suggest robust alternatives.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 18 - 19, The workflow uses floating
refs for third-party actions DeterminateSystems/nix-installer-action@main and
DeterminateSystems/magic-nix-cache-action@main; update both usages to immutable
refs (replace `@main` with a specific release tag or commit SHA for each action)
to ensure CI reproducibility and supply-chain stability, and re-run the provided
verification command (rg -nP
'DeterminateSystems/(nix-installer-action|magic-nix-cache-action)@main' ...) to
confirm no occurrences remain.
templates/do-service/.github/workflows/ci.yml (1)

52-53: ⚠️ Potential issue | 🟡 Minor

Guard HOST_KEY evaluation to avoid hard-failing deploy bootstrap.

nix eval --raw --file keys.nix keys.host is still unguarded; if keys.nix is missing/malformed, the deploy step exits unexpectedly.

🛡️ Suggested defensive fix
-            HOST_KEY=$(nix eval --raw --file keys.nix keys.host)
-            echo "$HOST_IP $HOST_KEY" >> ~/.ssh/known_hosts
+            HOST_KEY=$(nix eval --raw --file keys.nix keys.host 2>/dev/null || echo "")
+            if [ -n "$HOST_KEY" ]; then
+              echo "$HOST_IP $HOST_KEY" >> ~/.ssh/known_hosts
+            else
+              echo "Warning: HOST_IP resolved but keys.host is missing/unreadable" >&2
+            fi
As per coding guidelines, `*`: "Flag potential footguns" and suggest robust alternatives.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@templates/do-service/.github/workflows/ci.yml` around lines 52 - 53, The
HOST_KEY evaluation is unguarded and can hard-fail deployment if `nix eval --raw
--file keys.nix keys.host` fails; change the step to capture the command into a
variable (e.g., HOST_KEY=$(nix eval --raw --file keys.nix keys.host 2>/dev/null)
or using set +e and capture exit code), verify the command succeeded and
HOST_KEY is non-empty before echoing "$HOST_IP $HOST_KEY" >> ~/.ssh/known_hosts,
and on failure emit a warning log and skip adding the host instead of letting
the job exit (use conditional checks on the exit code or empty string and avoid
unhandled failures).
README.md (1)

42-42: ⚠️ Potential issue | 🟠 Major

Add a short migration note for mkRemotemkGitHooks.

The table update is good, but existing users still need a concrete before/after snippet and any argument/default changes to migrate safely.

As per coding guidelines, **/*.md: "point out missing instructions" and "consider the structure of the document and order of the sections."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 42, Add a short "Migration" subsection explaining how to
replace lib.mkRemote usage with lib.mkGitHooks: show a minimal before snippet
using mkRemote and the equivalent after snippet using mkGitHooks, list any
changed or removed arguments and their new names or defaults (e.g., optionX ->
optionY, default behavior changes), and include a note about optional rustfmt
behavior if relevant; place this note near the table entry for lib.mkGitHooks so
users see the concrete before/after example and argument/default changes when
scanning the docs.
lib/default.nix (1)

1-1: 🧹 Nitpick | 🔵 Trivial

Prefer explicit argument sets at this public lib boundary.

Using ... here makes the interface less strict and can mask accidental input drift. Prefer explicit attrs unless passthrough is intentionally required and documented.

♻️ Suggested interface tightening
-{ deploy-rs, nixos-anywhere, ... }:
+{ deploy-rs, nixos-anywhere }:
As per coding guidelines, `*`: "Point out architectural flaws like leaky abstractions."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/default.nix` at line 1, The public function currently accepts an open
argument set "{ deploy-rs, nixos-anywhere, ... }", which weakens the API;
replace the variadic "..." with an explicit list of accepted attributes (e.g.
remove "..." and explicitly declare any optional attrs with defaults) or if
passthrough is required, add a documented single explicit passthrough attr (e.g.
passthrough ? {}) and remove the open ellipsis; update the function signature
where "{ deploy-rs, nixos-anywhere, ... }" appears and document any
intentionally allowed extra keys.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 8-10: The global workflow permissions block currently sets
id-token: write which is unnecessary for the nix flake check and expands
credential scope; change the global permissions to only include contents: read
and remove id-token, then add id-token: write (or the narrower OIDC permission
needed) to the specific job(s) that actually perform OIDC actions (e.g., the job
running nix flake check or any deploy/publish jobs) so only those jobs have the
elevated token scope; update the permissions keys referenced in the workflow
(permissions, id-token, contents) and any relevant job-level permissions blocks
to grant id-token where required.

In @.github/workflows/integration-test.yml:
- Around line 131-134: The teardown step currently silences failures by
appending "|| true" to the run command, which can hide cleanup errors; edit the
Teardown job's run command (the line invoking "nix run .#tfDestroy -- -i
~/.ssh/id_ed25519") to remove "|| true" so failures in tfDestroy propagate and
cause the job to fail (since the job already uses "if: always()"), ensuring
cleanup errors are visible and not ignored.
- Around line 5-13: The declared workflow_dispatch inputs inputs.droplet_size
and inputs.region are never used; either remove those inputs or wire them into
the Terraform steps by exporting them as TF vars before plan/apply. Update the
job steps that run the nix/terraform commands (the steps that perform terraform
plan/apply) to set environment variables TF_VAR_droplet_size and TF_VAR_region
from ${{ inputs.droplet_size }} and ${{ inputs.region }} (or pass them into the
nix command environment) so Terraform sees the values; alternatively, delete the
inputs.droplet_size and inputs.region blocks if you choose not to use them.
- Around line 34-40: Replace the floating branch refs for the DeterminateSystems
actions with immutable references: change the two usages of
DeterminateSystems/nix-installer-action@main and
DeterminateSystems/magic-nix-cache-action@main to pinned commit SHAs or stable
release tags (e.g., DeterminateSystems/nix-installer-action@<commit-sha> and
DeterminateSystems/magic-nix-cache-action@<commit-sha>) so the workflow uses
fixed versions and avoids supply-chain risk when running with id-token: write
and secrets.SSH_KEY.

In `@modules/disko.nix`:
- Around line 17-21: The assertion currently uses builtins.hasAttr "disko"
config inside the assertions block; replace that runtime-config check with an
options namespace test by using the idiomatic guard options ? disko so the
module detects the disko option declarations rather than merged config
values—update the assertion expression (the entry named assertion in the
assertions list) to use options ? disko and keep the existing message text.

In `@modules/services.nix`:
- Around line 48-53: The service unit currently only grants the dynamic service
user access via ReadWritePaths = [ svcCfg.dataDir ] while the module creates
logDir via tmpfiles as root:root, so add svcCfg.logDir to the serviceConfig
ReadWritePaths (i.e. include svcCfg.logDir alongside svcCfg.dataDir in
ReadWritePaths) or instead replace the tmpfiles-managed directory with a
systemd-managed directory by using LogsDirectory=svcCfg.logDir (or the
appropriate systemd helper) so the dynamicUser can access the log directory;
update the serviceConfig block that sets ReadWritePaths and/or switch to
LogsDirectory to ensure permissions for dynamicUser.
- Around line 14-25: Replace the string-joined ExecStart with a list form so
arguments are escaped correctly: instead of building a single string using
builtins.concatStringsSep and svcCfg.extraArgs, set serviceConfig.ExecStart (or
the local execStart variable) to a list like [ path "--config" configFile ] ++
svcCfg.extraArgs so NixOS will escape each element (via
lib.strings.escapeShellArgs) and preserve argument boundaries; apply the same
change to the other identical ExecStart occurrence referenced in the comment.

In `@ROADMAP.md`:
- Around line 3-22: The "PR review findings" list in ROADMAP.md is a growing,
completed-items section; either move these checked items into a new CHANGELOG.md
(create CHANGELOG.md and append the current list under a dated entry, then
replace the list in ROADMAP.md with a short link/note pointing to that
changelog) or collapse them inline using a Markdown collapsible block (wrap the
existing "PR review findings" list inside a <details>...</details> with a
concise summary heading) so ROADMAP.md only shows actionable, forward-looking
items; update ROADMAP.md to reference the new CHANGELOG or the collapsed block
accordingly.
- Around line 132-133: Update the checklist line to reflect current library
exports: replace the outdated mention of mkRemote with the new mkGitHooks and
adjust the phrasing to note that shell helpers were consolidated (lib/remote.nix
removed, helpers moved into shell helper export mkGitHooks). Concretely, change
the checked item that lists "mkTerraform, mkDeploy, mkBootstrap, mkRemote" to
"mkTerraform, mkDeploy, mkBootstrap, mkGitHooks" and, if helpful, add a brief
parenthetical that shell helpers were consolidated into the shell helper export
(mkGitHooks) to avoid future confusion.

In `@templates/do-service/.github/workflows/integration-test.yml`:
- Around line 89-91: The Teardown step ("Teardown") masks failures by appending
"|| true" to the run command; remove the trailing "|| true" from the "nix run
.#tfDestroy -- -i ~/.ssh/id_ed25519 || true" invocation so the step fails on
errors (keep the existing "if: always()" to ensure it still runs), allowing
workflow failure when destroy fails and preventing orphaned droplets.
- Around line 5-13: The workflow defines dispatch inputs droplet_size and region
but never passes them to Terraform; update the CI steps that run tfPlan/tfApply
to export those inputs as environment variables (TF_VAR_droplet_size and
TF_VAR_region) or create a generated tfvars file before calling tfPlan/tfApply
so Terraform receives the UI-supplied values; locate the inputs named
droplet_size and region and wire them into the step that invokes tfPlan/tfApply
(or the job that runs Terraform) by setting TF_VAR_droplet_size and
TF_VAR_region from the workflow inputs or writing a .tfvars file using those
inputs.
- Around line 31-37: Replace the floating refs for the DeterminateSystems
actions with immutable refs: change DeterminateSystems/nix-installer-action@main
and DeterminateSystems/magic-nix-cache-action@main to specific release tags or
commit SHAs (e.g., DeterminateSystems/nix-installer-action@vX.Y.Z or
@<commit-sha>) so the workflow's behavior cannot be altered by upstream
force-pushes; update both action references to use stable tags/SHAs consistent
with how actions/checkout@v4 is pinned.

In `@templates/do-service/infra/variables.tf`:
- Around line 35-38: The validation for variable volume_size_gb allows
fractional GBs; update the validation block for volume_size_gb so it enforces a
whole-number GB >= 1 by changing the condition to check both >= 1 and equality
with its floored value (e.g., use var.volume_size_gb >= 1 && var.volume_size_gb
== floor(var.volume_size_gb)), and update error_message to reflect
"volume_size_gb must be an integer >= 1."; target the validation block for the
variable named volume_size_gb.
- Around line 6-9: The current validation on the variable do_token only checks
length(var.do_token) > 0 and will accept whitespace-only values; update the
variable block for do_token to (1) set nullable = false on the variable and (2)
change the validation condition to use trimmed input e.g.
length(trimspace(var.do_token)) > 0 so whitespace-only strings fail, and update
the error_message accordingly; locate the variable declaration and its
validation block in variables.tf (variable "do_token" / validation { ... }) and
apply these changes.

---

Outside diff comments:
In `@modules/firewall.nix`:
- Around line 13-27: Add unit/module tests that assert the
firewall.allowedTCPPorts list produced by this module respects the invariants:
when cfg.enable is true the list always contains 22 (SSH), port 80 is present
iff cfg.enableHTTP is true, port 443 is present iff cfg.enableHTTPS is true, and
any entries from cfg.allowedTCPPorts are appended; conversely when cfg.enable is
false the module should not enable networking.firewall. Create test cases that
exercise combinations of enable=true/false with enableHTTP=true/false,
enableHTTPS=true/false and non-empty allowedTCPPorts to validate the exact
resulting list produced by the config expression that sets
networking.firewall.allowedTCPPorts (the expression using [ 22 ] ++ httpPorts ++
httpsPorts ++ cfg.allowedTCPPorts).

In `@templates/do-service/flake.nix`:
- Around line 53-58: The call to omnix.lib.mkTerraform is mistakenly passing the
unused parameter system via "inherit pkgs system"; update the mkTerraform
invocation (infraPkgs) to only pass the required arguments (pkgs, keysFile, and
the optional ragenixPkg and secretsRules) by removing system from the inherit
list (i.e., use "inherit pkgs;") so mkTerraform is not given the unused system
parameter while keeping ragenixPkg =
omnix.inputs.ragenix.packages.${system}.default and secretsRules unchanged.

---

Duplicate comments:
In @.github/workflows/ci.yml:
- Around line 18-19: The workflow uses floating refs for third-party actions
DeterminateSystems/nix-installer-action@main and
DeterminateSystems/magic-nix-cache-action@main; update both usages to immutable
refs (replace `@main` with a specific release tag or commit SHA for each action)
to ensure CI reproducibility and supply-chain stability, and re-run the provided
verification command (rg -nP
'DeterminateSystems/(nix-installer-action|magic-nix-cache-action)@main' ...) to
confirm no occurrences remain.

In `@lib/default.nix`:
- Line 1: The public function currently accepts an open argument set "{
deploy-rs, nixos-anywhere, ... }", which weakens the API; replace the variadic
"..." with an explicit list of accepted attributes (e.g. remove "..." and
explicitly declare any optional attrs with defaults) or if passthrough is
required, add a documented single explicit passthrough attr (e.g. passthrough ?
{}) and remove the open ellipsis; update the function signature where "{
deploy-rs, nixos-anywhere, ... }" appears and document any intentionally allowed
extra keys.

In `@lib/shell.nix`:
- Around line 9-11: The script checks for the -i flag then immediately consumes
two args (identity="$2" and shift 2) which breaks if -i is passed without a
value; update the branch that handles the -i flag to validate that the second
positional parameter exists (check "${2:-}" or use -z) before assigning to the
identity variable and calling shift 2, and if the value is missing print a clear
error and exit non‑zero; keep references to the identity assignment
(identity="$2") and the shift 2 call so you modify that exact block.
- Around line 24-26: The pipeline that resolves recipients via "nix eval --file
${keysFile} roles.infra ..." must be validated before attempting to encrypt:
capture the output of that command into a variable (used by encryptState), check
that it succeeded and is non-empty, and if it failed or produced no recipients,
print an error and exit non-zero instead of piping to rage; update the logic
around encryptState/tfState to only call rage when the recipients string is
non-empty and to propagate nix eval errors (e.g., check its exit status or test
-n "$recipients"), referencing the existing symbols: nix eval (roles.infra),
${keysFile}, ${tfState}, and the encrypt step that invokes rage.
- Around line 34-37: encryptVars currently pipes data into rage without
validating that the source file (${tfVars}) exists or that recipients from
${keysFile} are valid; update the encryptVars block to first check that
${tfVars} exists and is non-empty and that the recipient list parsed from
${keysFile} (roles.infra) yields at least one valid recipient, failing with a
clear error message via builtins.abort or nix's builtins.trace if checks fail,
then proceed to run the nix eval | rage pipeline only when validations pass;
reference the symbols encryptVars, keysFile, roles.infra and ${tfVars} when
adding these guards so the logic is easy to locate.

In `@modules/acme.nix`:
- Around line 10-13: The email option is currently required even when ACME is
disabled; change the omnix.acme.email option to be nullable and only required
when omnix.acme.enable is true by setting its type to a nullable string (e.g.,
lib.types.nullOr lib.types.str) and providing a default null, then add a
validation/assertion in the module config that enforces config.omnix.acme.email
is non-null only if config.omnix.acme.enable is true (apply the same pattern to
the other options referenced around lines 16-21).

In `@modules/storage.nix`:
- Around line 10-13: The option volumeName must be constrained to a safe
character set before it’s used in the device path; update the volumeName
mkOption to include a validate function that rejects any value not matching a
safe regex (e.g. ^[A-Za-z0-9._-]+$) using builtins.match and throw a clear error
message, and keep using the validated volumeName where the device path is
interpolated so only allowed characters can reach that path construction.

In `@README.md`:
- Line 42: Add a short "Migration" subsection explaining how to replace
lib.mkRemote usage with lib.mkGitHooks: show a minimal before snippet using
mkRemote and the equivalent after snippet using mkGitHooks, list any changed or
removed arguments and their new names or defaults (e.g., optionX -> optionY,
default behavior changes), and include a note about optional rustfmt behavior if
relevant; place this note near the table entry for lib.mkGitHooks so users see
the concrete before/after example and argument/default changes when scanning the
docs.

In `@templates/do-service/.github/workflows/ci.yml`:
- Around line 52-53: The HOST_KEY evaluation is unguarded and can hard-fail
deployment if `nix eval --raw --file keys.nix keys.host` fails; change the step
to capture the command into a variable (e.g., HOST_KEY=$(nix eval --raw --file
keys.nix keys.host 2>/dev/null) or using set +e and capture exit code), verify
the command succeeded and HOST_KEY is non-empty before echoing "$HOST_IP
$HOST_KEY" >> ~/.ssh/known_hosts, and on failure emit a warning log and skip
adding the host instead of letting the job exit (use conditional checks on the
exit code or empty string and avoid unhandled failures).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9f60369a-a789-4430-9e35-5a52cff3f7a3

📥 Commits

Reviewing files that changed from the base of the PR and between 4041129 and 8b79050.

⛔ Files ignored due to path filters (1)
  • flake.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • .github/workflows/ci.yml
  • .github/workflows/integration-test.yml
  • README.md
  • ROADMAP.md
  • flake.nix
  • lib/bootstrap.nix
  • lib/default.nix
  • lib/deploy.nix
  • lib/hooks.nix
  • lib/remote.nix
  • lib/shell.nix
  • lib/terraform.nix
  • modules/acme.nix
  • modules/base.nix
  • modules/digitalocean.nix
  • modules/disko.nix
  • modules/firewall.nix
  • modules/services.nix
  • modules/storage.nix
  • templates/do-service/.github/workflows/ci.yml
  • templates/do-service/.github/workflows/integration-test.yml
  • templates/do-service/config/secrets.nix
  • templates/do-service/flake.nix
  • templates/do-service/infra/secrets.nix
  • templates/do-service/infra/terraform.tfvars.example
  • templates/do-service/infra/variables.tf
  • templates/do-service/keys.nix
  • templates/do-service/os.nix
  • templates/do-service/services.nix
💤 Files with no reviewable changes (1)
  • lib/remote.nix
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
*

⚙️ CodeRabbit configuration file

Focus on providing constructive criticism. Whenever you see a suboptimal approach, suggest more idiomatic or robust alternative(s). Flag potential footguns. Suggest FP alternatives to mutable/imperative code. Point out architectural flaws like leaky abstractions, tight coupling, wrong level of abstraction, poor type modeling, over-abstraction, unclear domain boundaries. Code should generally be organized based on business concerns rather than technical aspects - suggest improvements if you find violations. Point out gaps in test coverage but suggest tests that are not too coupled to the implementation and actually test domain invariants and business logic

Files:

  • README.md
  • flake.nix
  • ROADMAP.md
**/*.md

⚙️ CodeRabbit configuration file

Focus on the contents of the docs and not on cosmetic things like markdown formatting. We use markdown files for various docs including but not limited to the north star system specification, SPEC.md, the plan for how to get there, ROADMAP.md, guidelines for AI contributors, AGENTS.md, project overview and instructions for human contributors, README.md. Think about the target audience of a document when deciding what comment to leave. For specifications and designs, suggest potential product, architectural, and UI/UX improvements. For plans, suggest changes that would make things more parallelizable and deliverable-focused. For instructions, suggest better rules and guidelines and point out missing instructions. In all cases, flag needless bloat, prefer clear concise writing, and consider the structure of the document and order of the sections

Files:

  • README.md
  • ROADMAP.md
🔇 Additional comments (15)
modules/storage.nix (1)

3-6: Formatting-only let/in refactor looks good.

This improves readability without changing behavior.

templates/do-service/infra/terraform.tfvars.example (1)

1-2: Good guardrail for secret handling in templates.

The new guidance clearly steers users away from creating plaintext tfvars manually and toward the encrypted workflow.

templates/do-service/keys.nix (1)

3-5: Formatting refactor is clean and behavior-preserving.

This keeps the same keys/role mapping while improving readability.

Also applies to: 9-20

templates/do-service/config/secrets.nix (1)

1-4: Looks good — formatting-only normalization.

No behavioral change introduced.

templates/do-service/infra/secrets.nix (1)

1-6: Reformat is safe and preserves secret key mapping semantics.

lib/deploy.nix (1)

9-14: Nice refactor: targetSystem is threaded consistently through deploy activation.

This improves portability while keeping wrapper behavior intact.

Also applies to: 50-61, 65-76, 93-98

templates/do-service/services.nix (1)

4-4: Good template default for per-service data isolation.

Using /mnt/data/my-service is clearer and safer than a generic shared path.

modules/digitalocean.nix (1)

12-16: Good clarification on import/evaluation behavior.

The comment clearly explains module import side effects vs runtime gating and makes integration intent easier to follow.

flake.nix (3)

97-104: Dev shell packages are still manually maintained.

The packages list duplicates what's configured in lib/hooks.nix. If hooks or tools change there, this list won't update automatically.


19-21: LGTM on git-hooks integration.

Clean addition of the git-hooks input with proper nixpkgs follows.


46-59: Clear module layering pattern.

Good approach: importing both the upstream disko.nixosModules.disko for core functionality and the local self.nixosModules.disko wrapper for omnix-specific assertions/configuration. The comment on line 46 clarifies intent.

ROADMAP.md (2)

111-114: mkIntegrationTest deliverable still lacks signature/usage specification.

The description mentions parameterization by "project-specific config" but doesn't specify the function's return type or invocation pattern. Adding a brief expected signature would make this actionable.


40-81: Age-based secret management section is well-structured.

Clear problem statement, explicit workflow requirements, parallelizable dependency graph, and concrete deliverables. The "Optional:" label on secretspec integration addresses prior ambiguity.

templates/do-service/flake.nix (2)

95-105: LGTM on the resolveIp package.

Good extraction into a standalone shell application. The implementation correctly sources the shared helper from infraPkgs.resolveIp and outputs the result. The runtimeInputs properly declares rage and jq dependencies.


27-30: Clean module simplification.

Reducing to just omnix.nixosModules.default plus the local ./os.nix is the right call—the default module already pulls in all upstream and omnix modules (disko, ragenix, and all others per the parent flake's definition).

Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/integration-test.yml
Comment thread .github/workflows/integration-test.yml Outdated
Comment on lines +131 to +134
- name: Teardown
if: always()
working-directory: ${{ env.TEST_DIR }}
run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519 || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't suppress cleanup failures.

if: always() already ensures teardown runs. Keeping || true here means the job can finish green while the droplet is still alive and accruing cost.

Suggested change
-        run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519 || true
+        run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Teardown
if: always()
working-directory: ${{ env.TEST_DIR }}
run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519 || true
- name: Teardown
if: always()
working-directory: ${{ env.TEST_DIR }}
run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/integration-test.yml around lines 131 - 134, The teardown
step currently silences failures by appending "|| true" to the run command,
which can hide cleanup errors; edit the Teardown job's run command (the line
invoking "nix run .#tfDestroy -- -i ~/.ssh/id_ed25519") to remove "|| true" so
failures in tfDestroy propagate and cause the job to fail (since the job already
uses "if: always()"), ensuring cleanup errors are visible and not ignored.

Comment thread modules/disko.nix
Comment on lines +17 to +21
assertions = [
{
assertion = builtins.hasAttr "disko" config;
message = "omnix.disko requires the upstream disko NixOS module. Import disko.nixosModules.disko or use omnix.nixosModules.default which includes it.";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In NixOS modules, what is the recommended way to check if an upstream option namespace exists: builtins.hasAttr "disko" configoroptions ? disko?

💡 Result:

In NixOS modules, the recommended way to check if an upstream option namespace exists is options ? disko. This checks the options argument (the merged option declarations from all modules), which is available early during evaluation and safe to use with the ? operator or lib.hasAttr options "disko". It determines if any module has declared options.disko. In contrast, builtins.hasAttr "disko" config checks the config argument (merged option values), which may not have the attribute even if declared (e.g., if unset or lazy), and can cause infinite recursion if used in certain contexts like lib.optionalAttrs. Official NixOS manual confirms options is provided to modules for option declarations. NixOS Discourse threads recommend options ? foo for detecting option availability across versions or conditionally, avoiding evaluation errors if undeclared.

Citations:


Use options ? disko for the import guard, not builtins.hasAttr "disko" config.

This guard is fragile because it checks merged runtime config values, not option declarations. Use options ? disko to safely detect if the option namespace has been declared, which is the idiomatic approach in NixOS modules for module-import validation.

♻️ Suggested guard pattern
-{ lib, config, ... }:
+{ lib, config, options, ... }:
@@
-    assertions = [
-      {
-        assertion = builtins.hasAttr "disko" config;
-        message = "omnix.disko requires the upstream disko NixOS module. Import disko.nixosModules.disko or use omnix.nixosModules.default which includes it.";
-      }
-    ];
+    assertions = [
+      {
+        assertion = options ? disko;
+        message = "omnix.disko requires the upstream disko NixOS module. Import disko.nixosModules.disko or use omnix.nixosModules.default which includes it.";
+      }
+    ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/disko.nix` around lines 17 - 21, The assertion currently uses
builtins.hasAttr "disko" config inside the assertions block; replace that
runtime-config check with an options namespace test by using the idiomatic guard
options ? disko so the module detects the disko option declarations rather than
merged config values—update the assertion expression (the entry named assertion
in the assertions list) to use options ? disko and keep the existing message
text.

Comment thread templates/do-service/.github/workflows/integration-test.yml
Comment thread templates/do-service/.github/workflows/integration-test.yml Outdated
Comment on lines +89 to +91
- name: Teardown
if: always()
run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519 || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't hide destroy failures.

if: always() already guarantees teardown runs. The trailing || true can leave droplets around while the workflow still reports green.

Suggested change
-        run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519 || true
+        run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Teardown
if: always()
run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519 || true
- name: Teardown
if: always()
run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@templates/do-service/.github/workflows/integration-test.yml` around lines 89
- 91, The Teardown step ("Teardown") masks failures by appending "|| true" to
the run command; remove the trailing "|| true" from the "nix run .#tfDestroy --
-i ~/.ssh/id_ed25519 || true" invocation so the step fails on errors (keep the
existing "if: always()" to ensure it still runs), allowing workflow failure when
destroy fails and preventing orphaned droplets.

Comment thread templates/do-service/infra/variables.tf
Comment thread templates/do-service/infra/variables.tf

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
modules/services.nix (1)

88-91: 🧹 Nitpick | 🔵 Trivial

Consider using lib.types.str for configDir.

The coding guidelines prefer lib.types.str over lib.types.path for values that may come from terraform or other dynamic sources. For consistency with dataDir and logDir (which already use lib.types.str), and to avoid type coercion issues when the config directory path is provided dynamically, consider changing this type.

♻️ Proposed fix
     configDir = lib.mkOption {
-      type = lib.types.path;
+      type = lib.types.str;
       description = "Path to directory containing <service-name>.toml configs";
     };

As per coding guidelines: "Prefer lib.types.str over lib.types.path for values that may come from terraform or other dynamic sources".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/services.nix` around lines 88 - 91, Change the configDir option
definition to use lib.types.str instead of lib.types.path: update the
lib.mkOption for configDir so its type matches dataDir and logDir
(lib.types.str) to avoid path-type coercion when values come from terraform or
other dynamic sources; keep the existing description and option name (configDir)
unchanged.
♻️ Duplicate comments (1)
.github/workflows/ci.yml (1)

16-19: ⚠️ Potential issue | 🟡 Minor

Update action versions to the latest stable releases.

The workflow pins nix-installer-action@v17 and magic-nix-cache-action@v8, but these are outdated. Update to nix-installer-action@v21 and magic-nix-cache-action@v9 for the latest bug fixes and improvements.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 16 - 19, Update the pinned action
versions in the CI workflow: replace DeterminateSystems/nix-installer-action@v17
with DeterminateSystems/nix-installer-action@v21 and
DeterminateSystems/magic-nix-cache-action@v8 with
DeterminateSystems/magic-nix-cache-action@v9 in the steps that currently
reference those actions (lines showing actions/checkout@v4,
DeterminateSystems/nix-installer-action@v17,
DeterminateSystems/magic-nix-cache-action@v8, and the nix flake check step);
ensure no other steps rely on the old versions and run the workflow to verify
nix flake check still passes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/integration-test.yml:
- Around line 71-84: Add an inline comment next to the generated host entry in
the keys.nix content (the "host" key assignment inside the rec { keys = { ... }
} block) stating that the ssh-ed25519 "AAAAC3NzaC1lZDI1NTE5AAAAIPLACEHOLDER"
value is an intentional scaffold placeholder and that the real host key should
be retrieved and replaced post‑provisioning (or via provisioning scripts) for
permanent deployments; keep the placeholder behavior unchanged but make the
intent explicit for future readers.

---

Outside diff comments:
In `@modules/services.nix`:
- Around line 88-91: Change the configDir option definition to use lib.types.str
instead of lib.types.path: update the lib.mkOption for configDir so its type
matches dataDir and logDir (lib.types.str) to avoid path-type coercion when
values come from terraform or other dynamic sources; keep the existing
description and option name (configDir) unchanged.

---

Duplicate comments:
In @.github/workflows/ci.yml:
- Around line 16-19: Update the pinned action versions in the CI workflow:
replace DeterminateSystems/nix-installer-action@v17 with
DeterminateSystems/nix-installer-action@v21 and
DeterminateSystems/magic-nix-cache-action@v8 with
DeterminateSystems/magic-nix-cache-action@v9 in the steps that currently
reference those actions (lines showing actions/checkout@v4,
DeterminateSystems/nix-installer-action@v17,
DeterminateSystems/magic-nix-cache-action@v8, and the nix flake check step);
ensure no other steps rely on the old versions and run the workflow to verify
nix flake check still passes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b7d1f5b9-b2f5-48f4-8e7b-6964b4d5e861

📥 Commits

Reviewing files that changed from the base of the PR and between 8b79050 and 4a91c66.

📒 Files selected for processing (9)
  • .github/workflows/ci.yml
  • .github/workflows/integration-test.yml
  • ROADMAP.md
  • modules/acme.nix
  • modules/services.nix
  • templates/do-service/.github/workflows/ci.yml
  • templates/do-service/.github/workflows/integration-test.yml
  • templates/do-service/flake.nix
  • templates/do-service/infra/variables.tf
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Flake check
🧰 Additional context used
📓 Path-based instructions (3)
**/*.nix

📄 CodeRabbit inference engine (AGENTS.md)

**/*.nix: Use lib.mkOption with proper types for all module options
Every module must be independently usable (no implicit dependencies on other omnix modules)
Options must live under the omnix.* namespace
Use lib.mkIf cfg.enable for conditional configuration
Use lib.mkDefault for overridable defaults
Every configurable value must be a module option with a type and description
Prefer lib.types.str over lib.types.path for values that may come from terraform or other dynamic sources
Group related options under a single omnix.<module> prefix
Functions take an attrset of required parameters
Return an attrset of derivations or values from library functions
Use pkgs.writeShellApplication for shell scripts (not raw writeScript)
Include runtimeInputs in shell scripts and never assume tools are on PATH
All shell scripts must use set -eo pipefail
When changing module option interfaces, preserve the old option with lib.mkRenamedOptionModule or lib.mkRemovedOptionModule with a clear migration message

Files:

  • modules/acme.nix
  • modules/services.nix
  • templates/do-service/flake.nix
*

⚙️ CodeRabbit configuration file

Focus on providing constructive criticism. Whenever you see a suboptimal approach, suggest more idiomatic or robust alternative(s). Flag potential footguns. Suggest FP alternatives to mutable/imperative code. Point out architectural flaws like leaky abstractions, tight coupling, wrong level of abstraction, poor type modeling, over-abstraction, unclear domain boundaries. Code should generally be organized based on business concerns rather than technical aspects - suggest improvements if you find violations. Point out gaps in test coverage but suggest tests that are not too coupled to the implementation and actually test domain invariants and business logic

Files:

  • ROADMAP.md
**/*.md

⚙️ CodeRabbit configuration file

Focus on the contents of the docs and not on cosmetic things like markdown formatting. We use markdown files for various docs including but not limited to the north star system specification, SPEC.md, the plan for how to get there, ROADMAP.md, guidelines for AI contributors, AGENTS.md, project overview and instructions for human contributors, README.md. Think about the target audience of a document when deciding what comment to leave. For specifications and designs, suggest potential product, architectural, and UI/UX improvements. For plans, suggest changes that would make things more parallelizable and deliverable-focused. For instructions, suggest better rules and guidelines and point out missing instructions. In all cases, flag needless bloat, prefer clear concise writing, and consider the structure of the document and order of the sections

Files:

  • ROADMAP.md
🧠 Learnings (14)
📓 Common learnings
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.571Z
Learning: Applies to **/*.nix : Use `pkgs.writeShellApplication` for shell scripts (not raw `writeScript`)
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.571Z
Learning: Applies to **/*.nix : Include `runtimeInputs` in shell scripts and never assume tools are on PATH
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.571Z
Learning: Template projects should pass `nix flake check`
📚 Learning: 2026-03-21T15:47:14.571Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.571Z
Learning: Applies to **/*.nix : Group related options under a single `omnix.<module>` prefix

Applied to files:

  • modules/acme.nix
  • ROADMAP.md
  • modules/services.nix
  • templates/do-service/flake.nix
📚 Learning: 2026-03-21T15:47:14.571Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.571Z
Learning: Applies to **/*.nix : Options must live under the `omnix.*` namespace

Applied to files:

  • modules/acme.nix
  • templates/do-service/flake.nix
📚 Learning: 2026-03-21T15:47:14.571Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.571Z
Learning: Applies to **/*.nix : Every module must be independently usable (no implicit dependencies on other omnix modules)

Applied to files:

  • modules/acme.nix
  • ROADMAP.md
  • templates/do-service/flake.nix
📚 Learning: 2026-03-21T15:47:14.571Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.571Z
Learning: Applies to **/*.nix : Use `lib.mkOption` with proper types for all module options

Applied to files:

  • modules/acme.nix
  • modules/services.nix
📚 Learning: 2026-03-21T15:47:14.571Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.571Z
Learning: Applies to **/*.nix : Every configurable value must be a module option with a type and description

Applied to files:

  • modules/acme.nix
📚 Learning: 2026-03-21T15:47:14.571Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.571Z
Learning: Applies to **/*.nix : When changing module option interfaces, preserve the old option with `lib.mkRenamedOptionModule` or `lib.mkRemovedOptionModule` with a clear migration message

Applied to files:

  • modules/acme.nix
  • ROADMAP.md
  • modules/services.nix
📚 Learning: 2026-03-21T15:47:14.571Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.571Z
Learning: Template projects should pass `nix flake check`

Applied to files:

  • .github/workflows/ci.yml
  • ROADMAP.md
  • templates/do-service/flake.nix
📚 Learning: 2026-03-21T15:47:14.571Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.571Z
Learning: Never suppress Nix evaluation errors or warnings

Applied to files:

  • templates/do-service/.github/workflows/ci.yml
📚 Learning: 2026-03-21T15:47:14.571Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.571Z
Learning: Applies to **/*.nix : Use `lib.mkIf cfg.enable` for conditional configuration

Applied to files:

  • modules/services.nix
📚 Learning: 2026-03-21T15:47:14.571Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.571Z
Learning: Applies to **/*.nix : Functions take an attrset of required parameters

Applied to files:

  • modules/services.nix
  • templates/do-service/flake.nix
📚 Learning: 2026-03-21T15:47:14.571Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.571Z
Learning: Applies to **/*.nix : Include `runtimeInputs` in shell scripts and never assume tools are on PATH

Applied to files:

  • templates/do-service/flake.nix
📚 Learning: 2026-03-21T15:47:14.571Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.571Z
Learning: Applies to **/*.nix : Use `lib.mkDefault` for overridable defaults

Applied to files:

  • templates/do-service/flake.nix
📚 Learning: 2026-03-21T15:47:14.571Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.571Z
Learning: Applies to **/*.nix : Use `pkgs.writeShellApplication` for shell scripts (not raw `writeScript`)

Applied to files:

  • templates/do-service/flake.nix
🔇 Additional comments (25)
modules/acme.nix (3)

1-5: LGTM!

Standard NixOS module header with proper argument destructuring and cfg alias pattern.


7-15: LGTM! Previous feedback addressed.

The email option correctly uses lib.types.nullOr lib.types.str with default = null, allowing the module to evaluate cleanly when enable = false. Both options have proper types and descriptions under the omnix.acme namespace. As per coding guidelines: "Options must live under the omnix.* namespace" and "Every configurable value must be a module option with a type and description."


17-29: LGTM! Well-structured conditional configuration.

The implementation correctly:

  • Uses lib.mkIf cfg.enable for conditional configuration
  • Includes an assertion ensuring email is set when enabled
  • Sets required ACME configuration (acceptTerms, defaults.email)

The module is independently usable with no implicit dependencies on other omnix modules. As per coding guidelines: "Use lib.mkIf cfg.enable for conditional configuration" and "Every module must be independently usable."

modules/services.nix (5)

8-27: LGTM on the function structure.

The curried function signature name: svcCfg: is appropriate for use with lib.mapAttrs on line 132. The internal let bindings are well-organized.

Note: The ExecStart string concatenation concern was previously flagged and remains unaddressed.


147-163: Owner/group duplication in logrotate settings.

Lines 152-154 still compute owner/group inline, duplicating the logic already extracted at lines 142-143 in the tmpfiles block. This was previously flagged.


172-177: Group creation logic comment suggestion still applicable.

The complex conditional (cfg.user != null && !cfg.dynamicUser || cfg.group != null) with nested optionalAttrs handles multiple scenarios. A brief inline comment would help maintainers understand the intent. This was previously suggested.


45-59: LGTM on the serviceConfig refactoring.

The use of lib.optionalAttrs is idiomatic, and logDir is now correctly included in ReadWritePaths alongside dataDir, addressing the previous access concern for dynamic users.


134-145: LGTM on tmpfiles refactoring.

The extraction of owner and group to shared let bindings (lines 142-143) is clean and addresses the previous duplication concern for this section.

templates/do-service/.github/workflows/ci.yml (2)

54-57: Defensive handling for HOST_KEY is implemented but silently fails.

The nix eval for HOST_KEY now has error suppression and an empty check, which prevents workflow abort on malformed keys.nix. However, if HOST_KEY is empty while HOST_IP is set, the workflow continues silently without adding the host to known_hosts, potentially causing SSH verification failures later.

Consider logging a warning when HOST_KEY is empty to aid debugging:

🛡️ Suggested warning
           HOST_KEY=$(nix eval --raw --file keys.nix keys.host 2>/dev/null || echo "")
           if [ -n "$HOST_KEY" ]; then
             echo "$HOST_IP $HOST_KEY" >> ~/.ssh/known_hosts
+          else
+            echo "Warning: Could not read host key from keys.nix" >&2
           fi

35-37: LGTM!

The id-token: write permission is correctly scoped to the deploy job only, following the principle of least privilege. Jobs that don't need OIDC capabilities no longer have elevated permissions.

templates/do-service/.github/workflows/integration-test.yml (2)

92-94: Don't suppress teardown failures — this can leave orphaned resources.

The || true on line 94 masks tfDestroy errors, meaning the workflow can report success while the droplet remains running and accruing cost. Since if: always() already ensures this step runs regardless of prior failures, the error suppression serves no purpose and hides critical cleanup issues.

🔧 Suggested fix
       - name: Teardown
         if: always()
-        run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519 || true
+        run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519

28-30: LGTM!

The workflow inputs are now correctly wired to Terraform via TF_VAR_droplet_size and TF_VAR_region environment variables at the job level. This ensures the UI-provided values are actually used during infrastructure provisioning.

ROADMAP.md (3)

111-113: Clarify the mkIntegrationTest deliverable with expected signature.

The description mentions parameterization by project-specific config but doesn't specify what the function returns or how consumers invoke it. For a roadmap, adding the expected signature or a brief usage example would help contributors understand the scope.


3-21: LGTM!

Good use of a collapsible <details> block to keep the roadmap focused on forward-looking work while preserving the completed PR review findings for reference.


52-64: LGTM!

The Mermaid diagram now correctly shows parallel development paths: the NixOS module (F) and deploy-rs activation (G) can be developed in parallel since they both depend only on the age encrypt/decrypt core (B), not on the CLI. This better reflects the actual dependency structure.

.github/workflows/integration-test.yml (3)

133-136: Don't suppress teardown failures — orphaned droplets accrue cost.

The || true masks tfDestroy errors, allowing the workflow to succeed while leaving the droplet running. Since if: always() already ensures this step executes regardless of prior failures, the error suppression only hides cleanup issues.

🔧 Suggested fix
       - name: Teardown
         if: always()
         working-directory: ${{ env.TEST_DIR }}
-        run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519 || true
+        run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519

100-111: Consider adding BatchMode=yes to the SSH retry loop.

Adding BatchMode=yes prevents the SSH client from hanging on unexpected password prompts or interactive authentication, making the retry loop more deterministic in CI environments.

🔧 Suggested fix
       - name: Wait for SSH
         run: |
           retries=0
-          until ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
+          until ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes \
             -i ~/.ssh/id_ed25519 "root@$HOST_IP" true 2>/dev/null; do

31-33: LGTM!

The workflow dispatch inputs are correctly wired to Terraform via TF_VAR_droplet_size and TF_VAR_region environment variables, ensuring UI-provided values are used during provisioning.

.github/workflows/ci.yml (2)

8-9: LGTM!

The permissions are correctly scoped to contents: read only. This follows the principle of least privilege for a workflow that only needs to read the repository and run nix flake check.


21-39: LGTM!

The examples job correctly validates NixOS configurations using nix eval to check the derivation path exists and nix build --dry-run to verify the configuration can be built. The matrix strategy efficiently tests all example configurations in parallel.

templates/do-service/infra/variables.tf (2)

5-10: LGTM!

The do_token validation now correctly rejects empty and whitespace-only values using trimspace(), and nullable = false ensures the variable must be explicitly set. The error message is clear and actionable.


35-39: LGTM!

The volume_size_gb validation now correctly enforces integer values by comparing against floor(), preventing fractional GB inputs that wouldn't make sense for volume sizing.

templates/do-service/flake.nix (3)

27-30: LGTM!

The module list is correctly simplified to use omnix.nixosModules.default, which aggregates all required modules (disko, ragenix, digitalocean, base, storage, services, firewall, acme) as shown in the root flake.nix. This reduces duplication and ensures the template automatically inherits any new modules added to the default set.


95-105: LGTM!

The resolveIp package correctly uses pkgs.writeShellApplication with explicit runtimeInputs for rage and jq, following the coding guidelines. The package properly wraps the shell snippet from infraPkgs.resolveIp and outputs the resolved host_ip variable. Based on learnings: "Use pkgs.writeShellApplication for shell scripts (not raw writeScript)" and "Include runtimeInputs in shell scripts and never assume tools are on PATH".


33-44: LGTM!

The comment clearly explains why the deploy configuration is hardcoded to x86_64-linux — the remote NixOS host is always x86_64-linux regardless of the local development machine's architecture. This prevents confusion when developers on ARM Macs see the hardcoded system.

Comment on lines +71 to +84
cat > keys.nix <<KEYS
rec {
keys = {
operator = "$PUBKEY";
ci = "$PUBKEY";
host = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPLACEHOLDER";
};
roles = with keys; {
infra = [ operator ci ];
service = [ host operator ];
ssh = [ operator ci ];
};
}
KEYS

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Placeholder host key in generated keys.nix.

The dynamically generated keys.nix uses a placeholder value "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPLACEHOLDER" for the host key. This is acceptable since the actual host doesn't exist during scaffold time, but consider adding a comment explaining this is expected and that the real host key would typically be retrieved post-provisioning for permanent deployments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/integration-test.yml around lines 71 - 84, Add an inline
comment next to the generated host entry in the keys.nix content (the "host" key
assignment inside the rec { keys = { ... } } block) stating that the ssh-ed25519
"AAAAC3NzaC1lZDI1NTE5AAAAIPLACEHOLDER" value is an intentional scaffold
placeholder and that the real host key should be retrieved and replaced
post‑provisioning (or via provisioning scripts) for permanent deployments; keep
the placeholder behavior unchanged but make the intent explicit for future
readers.

@0xgleb 0xgleb marked this pull request as draft March 21, 2026 18:16
@0xgleb 0xgleb marked this pull request as ready for review March 29, 2026 10:14
@0xgleb 0xgleb enabled auto-merge March 29, 2026 10:14
@levsgit levsgit self-requested a review April 1, 2026 07:24

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
modules/base.nix (1)

26-29: ⚠️ Potential issue | 🟠 Major

This is a breaking interface change without a migration path.

Removing the stateVersion default makes previously valid configs fail when omnix.base.enable = true. Keep a compatibility default (or add an explicit migration mechanism/message) before enforcing mandatory configuration.

🛠️ Backward-compatible fix
     stateVersion = lib.mkOption {
       type = lib.types.str;
+      default = "24.11";
       description = "NixOS state version";
     };

As per coding guidelines, When changing module option interfaces, preserve the old option with lib.mkRenamedOptionModule or lib.mkRemovedOptionModule with a clear migration message.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/base.nix` around lines 26 - 29, The change removed the default for
the NixOS module option stateVersion (declared via lib.mkOption), causing a
breaking interface when omnix.base.enable = true; restore backward compatibility
by reintroducing the old option with a compatibility wrapper: either add a
default value for stateVersion again or replace the removed option with
lib.mkRenamedOptionModule / lib.mkRemovedOptionModule (referencing stateVersion
and the module where stateVersion was defined) and provide a clear migration
message explaining the new required configuration and how to set the
stateVersion so existing configs continue to work.
♻️ Duplicate comments (6)
modules/services.nix (2)

14-25: ⚠️ Potential issue | 🟠 Major

Use a list for ExecStart to safely handle arguments with special characters.

The builtins.concatStringsSep " " approach doesn't escape arguments containing whitespace or quotes. If extraArgs contains ["--name" "my service"], systemd will incorrectly split "my service" into two arguments.

Assign ExecStart as a list instead:

-      execStart =
-        if svcCfg.extraArgs == [ ] then
-          "${path} --config ${configFile}"
-        else
-          builtins.concatStringsSep " " (
-            [
-              path
-              "--config"
-              "${configFile}"
-            ]
-            ++ svcCfg.extraArgs
-          );
+      execStart = [ path "--config" configFile ] ++ svcCfg.extraArgs;

NixOS will automatically escape each element when generating the unit file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/services.nix` around lines 14 - 25, The ExecStart currently builds a
shell-escaped string using builtins.concatStringsSep which breaks when elements
in svcCfg.extraArgs contain spaces/quotes; change execStart to be a Nix list so
systemd receives properly escaped argv elements: construct execStart as a list
starting with path, "--config", "${configFile}" and then append svcCfg.extraArgs
(instead of concatenating into a single string), and ensure the empty-extraArgs
case returns a list too (no concatStringsSep or manual string joining).

134-145: 🧹 Nitpick | 🔵 Trivial

Consider extracting shared owner/group bindings to reduce duplication.

The owner/group fallback logic appears in two places:

  • Lines 142-143 for tmpfiles rules
  • Lines 152-153 for logrotate su directive

A single let block scoping both would keep them in sync:

+  config = lib.mkIf (cfg.definitions != { }) (
+    let
+      owner = if cfg.user != null then cfg.user else "root";
+      group = if cfg.group != null then cfg.group else "root";
+    in
+    {
       # ... use owner/group in both tmpfiles and logrotate
+    }
+  );

Also applies to: 147-163

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/services.nix` around lines 134 - 145, The owner/group fallback logic
is duplicated; extract the bindings for owner and group into a single shared let
so both systemd.tmpfiles.rules and the logrotate su directive reuse the same
values. Move the owner = if cfg.user != null then cfg.user else "root" and group
= if cfg.group != null then cfg.group else "root" definitions into a surrounding
let that encloses both the systemd.tmpfiles.rules expression and the logrotate
configuration, then reference those owner and group symbols from
systemd.tmpfiles.rules (the map producing "d ${dir} 0770 ${owner} ${group} -")
and from the logrotate su directive to keep them in sync.
README.md (1)

42-42: ⚠️ Potential issue | 🟡 Minor

Add a short migration note for mkRemotelib.mkGitHooks.

Line 42 documents the new function but not the upgrade path. Add a minimal before/after snippet so existing users can migrate quickly.

As per coding guidelines, **/*.md: “point out missing instructions” and tailor docs to the target audience.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 42, Add a short migration note under the `lib.mkGitHooks`
entry that shows a minimal before/after snippet for users migrating from
`mkRemote` to `lib.mkGitHooks`; mention the original call using `mkRemote` and
the new equivalent using `lib.mkGitHooks`, include any required option name
changes (e.g., hook list, formats) and a one-line instruction that they should
replace `mkRemote(...)` with `lib.mkGitHooks(...)` to make the upgrade path
explicit for existing users.
modules/disko.nix (1)

19-20: ⚠️ Potential issue | 🟠 Major

Use option-namespace detection for the upstream module guard.

Line 19 should check declared options (options ? disko) rather than merged runtime config (builtins.hasAttr "disko" config) to avoid brittle evaluation behavior.

♻️ Proposed fix
-{ lib, config, ... }:
+{ lib, config, options, ... }:
@@
-        assertion = builtins.hasAttr "disko" config;
+        assertion = options ? disko;
#!/bin/bash
set -euo pipefail
# Verify current guard expression and whether options-based guard is already used.
rg -nP --type=nix -C3 'builtins\.hasAttr\s+"disko"\s+config|options\s*\?\s*disko'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/disko.nix` around lines 19 - 20, The module guard currently uses the
runtime check builtins.hasAttr "disko" config in the assertion; replace that
with an options-level detection (options ? disko) so the assertion uses declared
options rather than merged runtime config. Update the assertion attribute (named
assertion) to use options ? disko and keep the existing message unchanged;
ensure the new expression is used where the current builtins.hasAttr "disko"
config appears (e.g., the assertion binding) so the guard is evaluated against
declared options.
lib/bootstrap.nix (1)

18-25: ⚠️ Potential issue | 🟠 Major

Declare all invoked shell tools in runtimeInputs.

Line 49 uses awk and Line 56 uses grep, but neither tool is declared in runtimeInputs. This can break execution in clean environments.

🧩 Proposed fix
   runtimeInputs = [
     pkgs.rage
     pkgs.jq
+    pkgs.gawk
+    pkgs.gnugrep
     pkgs.gnused
     pkgs.openssh
     nixos-anywhere.packages.${system}.default
   ]

Based on learnings, Include runtimeInputs in shell scripts and never assume tools are on PATH.

Also applies to: 49-56

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/bootstrap.nix` around lines 18 - 25, The runtimeInputs list is missing
the shell tools used by the scripts (awk and grep), which breaks clean builds;
update the runtimeInputs array (the existing runtimeInputs variable that
currently includes pkgs.rage, pkgs.jq, pkgs.gnused, pkgs.openssh,
nixos-anywhere.packages.${system}.default, and optional ragenixPkg) to also
include pkgs.gawk and pkgs.gnugrep (or the distro-appropriate awk/grep packages)
so the calls that invoke awk and grep in the shell scripts will be available at
runtime.
.github/workflows/integration-test.yml (1)

100-111: 🧹 Nitpick | 🔵 Trivial

Consider adding BatchMode=yes to SSH options.

Adding BatchMode=yes prevents the SSH client from hanging on unexpected password prompts, making the retry loop fail-fast in CI environments.

Suggested fix
       - name: Wait for SSH
         run: |
           retries=0
-          until ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
+          until ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes \
             -i ~/.ssh/id_ed25519 "root@$HOST_IP" true 2>/dev/null; do
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/integration-test.yml around lines 100 - 111, The "Wait for
SSH" retry loop should add BatchMode=yes to the ssh command to avoid interactive
password prompts; update the ssh invocation used in that step (the until ssh ...
"root@$HOST_IP" true) to include -o BatchMode=yes alongside -o
StrictHostKeyChecking=no and -o ConnectTimeout=5 so the loop fails fast in CI
when a key is missing or authentication would prompt.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.coderabbit.yaml:
- Around line 32-34: The path_instructions entry currently uses path: "*" which
only matches top-level files; change the repository-wide glob in the
.coderabbit.yaml path_instructions block (the path_instructions key and its
path: "*" entry) to use a recursive pattern such as path: "/*" or an empty
string "" so the instructions apply to all nested files; update that path value
accordingly and keep the rest of the path_instructions configuration unchanged.

In @.github/workflows/ci.yml:
- Around line 17-18: The workflow pins third-party GitHub Actions using mutable
tags (actions/checkout@v4, DeterminateSystems/nix-installer-action@v17,
DeterminateSystems/magic-nix-cache-action@v8); replace each tag with the
corresponding full commit SHA for every usage of those actions in the file (both
occurrences), optionally keeping the tag as a trailing comment for readability,
so each uses the immutable ref (full SHA) instead of `@vX` to improve supply-chain
integrity and reproducibility.

In @.github/workflows/integration-test.yml:
- Around line 45-50: The workflow step "Setup SSH identity" uses the wrong
secret name; change the environment variable reference in the echo line from ${{
secrets.SSH_KEY }} to ${{ secrets.SSH_PRIVATE_KEY }} (and update any other
occurrences of secrets.SSH_KEY in this workflow) so it matches the template's
secret naming; ensure the step that writes ~/.ssh/id_ed25519 still uses the
updated secret reference and keep existing chmod/mkdir lines unchanged.

In `@modules/acme.nix`:
- Around line 25-27: security.acme.acceptTerms is hard-set to true which
prevents downstream overrides; change the assignment to use lib.mkDefault so the
default value can be overridden (e.g., replace the direct true with
lib.mkDefault true for security.acme.acceptTerms while keeping defaults.email =
cfg.email unchanged), referencing the security.acme.acceptTerms attribute and
lib.mkDefault helper.

In `@modules/firewall.nix`:
- Around line 13-14: Add an assertion in modules/acme.nix to require
omnix.firewall.enableHTTP when ACME is enabled: inside the existing assertions
attribute, add an assertion that checks cfg.enable implies
config.omnix.firewall.enableHTTP and provide a clear message like "omnix.acme
requires omnix.firewall.enableHTTP = true for HTTP-01 challenges"; reference the
assertions array and the cfg.enable/config.omnix.firewall.enableHTTP symbols so
the module will fail fast if ACME is turned on but firewall HTTP is disabled.

In `@templates/do-service/.github/workflows/ci.yml`:
- Around line 17-18: Replace mutable release tags with immutable commit SHAs for
all third-party GitHub Actions referenced (e.g., actions/checkout@v4,
DeterminateSystems/nix-installer-action@v17,
DeterminateSystems/magic-nix-cache-action@v8 and any other `@v` tags in the
workflow) by looking up the corresponding commit SHA on each action's GitHub
repo and updating the workflow references to use @<commit-sha>; ensure every
occurrence of the mutable tag is updated so no action remains pinned to a
mutable release.

In `@templates/do-service/.github/workflows/integration-test.yml`:
- Around line 61-72: In the "Wait for SSH" workflow step the ssh command can
hang on interactive prompts; update the ssh invocation (the until ssh ... true
loop) to include the option -o BatchMode=yes alongside -o
StrictHostKeyChecking=no and -o ConnectTimeout=5 so SSH exits immediately on
auth prompts and the retry loop remains deterministic in CI.

In `@templates/do-service/os.nix`:
- Line 11: Update the stateVersion value to match the repository's pinned NixOS
release: change the stateVersion declaration (symbol: stateVersion) from "24.11"
to "25.11" so the template aligns with the pinned/examples release and avoids
deployment drift.

---

Outside diff comments:
In `@modules/base.nix`:
- Around line 26-29: The change removed the default for the NixOS module option
stateVersion (declared via lib.mkOption), causing a breaking interface when
omnix.base.enable = true; restore backward compatibility by reintroducing the
old option with a compatibility wrapper: either add a default value for
stateVersion again or replace the removed option with lib.mkRenamedOptionModule
/ lib.mkRemovedOptionModule (referencing stateVersion and the module where
stateVersion was defined) and provide a clear migration message explaining the
new required configuration and how to set the stateVersion so existing configs
continue to work.

---

Duplicate comments:
In @.github/workflows/integration-test.yml:
- Around line 100-111: The "Wait for SSH" retry loop should add BatchMode=yes to
the ssh command to avoid interactive password prompts; update the ssh invocation
used in that step (the until ssh ... "root@$HOST_IP" true) to include -o
BatchMode=yes alongside -o StrictHostKeyChecking=no and -o ConnectTimeout=5 so
the loop fails fast in CI when a key is missing or authentication would prompt.

In `@lib/bootstrap.nix`:
- Around line 18-25: The runtimeInputs list is missing the shell tools used by
the scripts (awk and grep), which breaks clean builds; update the runtimeInputs
array (the existing runtimeInputs variable that currently includes pkgs.rage,
pkgs.jq, pkgs.gnused, pkgs.openssh, nixos-anywhere.packages.${system}.default,
and optional ragenixPkg) to also include pkgs.gawk and pkgs.gnugrep (or the
distro-appropriate awk/grep packages) so the calls that invoke awk and grep in
the shell scripts will be available at runtime.

In `@modules/disko.nix`:
- Around line 19-20: The module guard currently uses the runtime check
builtins.hasAttr "disko" config in the assertion; replace that with an
options-level detection (options ? disko) so the assertion uses declared options
rather than merged runtime config. Update the assertion attribute (named
assertion) to use options ? disko and keep the existing message unchanged;
ensure the new expression is used where the current builtins.hasAttr "disko"
config appears (e.g., the assertion binding) so the guard is evaluated against
declared options.

In `@modules/services.nix`:
- Around line 14-25: The ExecStart currently builds a shell-escaped string using
builtins.concatStringsSep which breaks when elements in svcCfg.extraArgs contain
spaces/quotes; change execStart to be a Nix list so systemd receives properly
escaped argv elements: construct execStart as a list starting with path,
"--config", "${configFile}" and then append svcCfg.extraArgs (instead of
concatenating into a single string), and ensure the empty-extraArgs case returns
a list too (no concatStringsSep or manual string joining).
- Around line 134-145: The owner/group fallback logic is duplicated; extract the
bindings for owner and group into a single shared let so both
systemd.tmpfiles.rules and the logrotate su directive reuse the same values.
Move the owner = if cfg.user != null then cfg.user else "root" and group = if
cfg.group != null then cfg.group else "root" definitions into a surrounding let
that encloses both the systemd.tmpfiles.rules expression and the logrotate
configuration, then reference those owner and group symbols from
systemd.tmpfiles.rules (the map producing "d ${dir} 0770 ${owner} ${group} -")
and from the logrotate su directive to keep them in sync.

In `@README.md`:
- Line 42: Add a short migration note under the `lib.mkGitHooks` entry that
shows a minimal before/after snippet for users migrating from `mkRemote` to
`lib.mkGitHooks`; mention the original call using `mkRemote` and the new
equivalent using `lib.mkGitHooks`, include any required option name changes
(e.g., hook list, formats) and a one-line instruction that they should replace
`mkRemote(...)` with `lib.mkGitHooks(...)` to make the upgrade path explicit for
existing users.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cc07103a-0fc0-4368-9cf2-86ca52ab0a73

📥 Commits

Reviewing files that changed from the base of the PR and between 4a91c66 and 03ba709.

⛔ Files ignored due to path filters (1)
  • flake.lock is excluded by !**/*.lock
📒 Files selected for processing (33)
  • .coderabbit.yaml
  • .github/workflows/ci.yml
  • .github/workflows/integration-test.yml
  • README.md
  • ROADMAP.md
  • examples/full.nix
  • examples/minimal.nix
  • examples/single-service.nix
  • flake.nix
  • lib/bootstrap.nix
  • lib/default.nix
  • lib/deploy.nix
  • lib/hooks.nix
  • lib/remote.nix
  • lib/shell.nix
  • lib/terraform.nix
  • modules/acme.nix
  • modules/base.nix
  • modules/digitalocean.nix
  • modules/disko.nix
  • modules/firewall.nix
  • modules/services.nix
  • modules/storage.nix
  • templates/do-service/.github/workflows/ci.yml
  • templates/do-service/.github/workflows/integration-test.yml
  • templates/do-service/config/secrets.nix
  • templates/do-service/flake.nix
  • templates/do-service/infra/secrets.nix
  • templates/do-service/infra/terraform.tfvars.example
  • templates/do-service/infra/variables.tf
  • templates/do-service/keys.nix
  • templates/do-service/os.nix
  • templates/do-service/services.nix
💤 Files with no reviewable changes (1)
  • lib/remote.nix
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
**/*.nix

📄 CodeRabbit inference engine (AGENTS.md)

**/*.nix: Use lib.mkOption with proper types for all module options
Every module must be independently usable (no implicit dependencies on other omnix modules)
Options must live under the omnix.* namespace
Use lib.mkIf cfg.enable for conditional configuration
Use lib.mkDefault for overridable defaults
Every configurable value must be a module option with a type and description
Prefer lib.types.str over lib.types.path for values that may come from terraform or other dynamic sources
Group related options under a single omnix.<module> prefix
Functions take an attrset of required parameters
Return an attrset of derivations or values from library functions
Use pkgs.writeShellApplication for shell scripts (not raw writeScript)
Include runtimeInputs in shell scripts and never assume tools are on PATH
All shell scripts must use set -eo pipefail
When changing module option interfaces, preserve the old option with lib.mkRenamedOptionModule or lib.mkRemovedOptionModule with a clear migration message

Files:

  • templates/do-service/infra/secrets.nix
  • templates/do-service/services.nix
  • modules/storage.nix
  • templates/do-service/config/secrets.nix
  • modules/digitalocean.nix
  • templates/do-service/keys.nix
  • modules/disko.nix
  • modules/firewall.nix
  • lib/default.nix
  • modules/base.nix
  • templates/do-service/os.nix
  • lib/hooks.nix
  • examples/single-service.nix
  • examples/minimal.nix
  • examples/full.nix
  • modules/acme.nix
  • lib/bootstrap.nix
  • lib/deploy.nix
  • templates/do-service/flake.nix
  • lib/terraform.nix
  • modules/services.nix
  • lib/shell.nix
  • flake.nix
*

⚙️ CodeRabbit configuration file

Focus on providing constructive criticism. Whenever you see a suboptimal approach, suggest more idiomatic or robust alternative(s). Flag potential footguns. Suggest FP alternatives to mutable/imperative code. Point out architectural flaws like leaky abstractions, tight coupling, wrong level of abstraction, poor type modeling, over-abstraction, unclear domain boundaries. Code should generally be organized based on business concerns rather than technical aspects - suggest improvements if you find violations. Point out gaps in test coverage but suggest tests that are not too coupled to the implementation and actually test domain invariants and business logic

Files:

  • README.md
  • ROADMAP.md
  • flake.nix
**/*.md

⚙️ CodeRabbit configuration file

Focus on the contents of the docs and not on cosmetic things like markdown formatting. We use markdown files for various docs including but not limited to the north star system specification, SPEC.md, the plan for how to get there, ROADMAP.md, guidelines for AI contributors, AGENTS.md, project overview and instructions for human contributors, README.md. Think about the target audience of a document when deciding what comment to leave. For specifications and designs, suggest potential product, architectural, and UI/UX improvements. For plans, suggest changes that would make things more parallelizable and deliverable-focused. For instructions, suggest better rules and guidelines and point out missing instructions. In all cases, flag needless bloat, prefer clear concise writing, and consider the structure of the document and order of the sections

Files:

  • README.md
  • ROADMAP.md
🧠 Learnings (17)
📓 Common learnings
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Include `runtimeInputs` in shell scripts and never assume tools are on PATH
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Use `pkgs.writeShellApplication` for shell scripts (not raw `writeScript`)
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Prefer `lib.types.str` over `lib.types.path` for values that may come from terraform or other dynamic sources

Applied to files:

  • templates/do-service/infra/secrets.nix
  • modules/storage.nix
  • modules/disko.nix
  • templates/do-service/os.nix
  • lib/terraform.nix
  • lib/shell.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Functions take an attrset of required parameters

Applied to files:

  • templates/do-service/infra/secrets.nix
  • modules/disko.nix
  • lib/default.nix
  • modules/base.nix
  • lib/hooks.nix
  • lib/bootstrap.nix
  • lib/deploy.nix
  • templates/do-service/flake.nix
  • lib/terraform.nix
  • modules/services.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : When changing module option interfaces, preserve the old option with `lib.mkRenamedOptionModule` or `lib.mkRemovedOptionModule` with a clear migration message

Applied to files:

  • modules/storage.nix
  • modules/digitalocean.nix
  • modules/disko.nix
  • lib/default.nix
  • modules/base.nix
  • templates/do-service/os.nix
  • modules/acme.nix
  • README.md
  • modules/services.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Every configurable value must be a module option with a type and description

Applied to files:

  • modules/storage.nix
  • modules/disko.nix
  • modules/base.nix
  • modules/acme.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Use `lib.mkOption` with proper types for all module options

Applied to files:

  • modules/storage.nix
  • modules/digitalocean.nix
  • modules/disko.nix
  • lib/default.nix
  • modules/base.nix
  • templates/do-service/os.nix
  • lib/hooks.nix
  • modules/acme.nix
  • README.md
  • modules/services.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Group related options under a single `omnix.<module>` prefix

Applied to files:

  • modules/storage.nix
  • modules/digitalocean.nix
  • modules/disko.nix
  • modules/base.nix
  • templates/do-service/os.nix
  • examples/single-service.nix
  • examples/minimal.nix
  • examples/full.nix
  • modules/acme.nix
  • README.md
  • templates/do-service/flake.nix
  • ROADMAP.md
  • modules/services.nix
  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Use `lib.mkIf cfg.enable` for conditional configuration

Applied to files:

  • modules/storage.nix
  • modules/digitalocean.nix
  • modules/disko.nix
  • lib/default.nix
  • modules/base.nix
  • lib/hooks.nix
  • modules/services.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Every module must be independently usable (no implicit dependencies on other omnix modules)

Applied to files:

  • modules/digitalocean.nix
  • modules/disko.nix
  • modules/base.nix
  • templates/do-service/os.nix
  • examples/minimal.nix
  • modules/acme.nix
  • README.md
  • templates/do-service/flake.nix
  • ROADMAP.md
  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Options must live under the `omnix.*` namespace

Applied to files:

  • modules/digitalocean.nix
  • modules/disko.nix
  • modules/base.nix
  • templates/do-service/os.nix
  • modules/acme.nix
  • README.md
  • templates/do-service/flake.nix
  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Template projects should pass `nix flake check`

Applied to files:

  • .github/workflows/ci.yml
  • lib/default.nix
  • modules/base.nix
  • templates/do-service/flake.nix
  • ROADMAP.md
  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Use `lib.mkDefault` for overridable defaults

Applied to files:

  • lib/default.nix
  • modules/base.nix
  • templates/do-service/os.nix
  • lib/hooks.nix
  • examples/full.nix
  • README.md
  • templates/do-service/flake.nix
  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Use `pkgs.writeShellApplication` for shell scripts (not raw `writeScript`)

Applied to files:

  • lib/default.nix
  • modules/base.nix
  • lib/bootstrap.nix
  • lib/deploy.nix
  • templates/do-service/flake.nix
  • lib/terraform.nix
  • lib/shell.nix
  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Include `runtimeInputs` in shell scripts and never assume tools are on PATH

Applied to files:

  • lib/default.nix
  • modules/base.nix
  • lib/hooks.nix
  • lib/bootstrap.nix
  • lib/deploy.nix
  • templates/do-service/flake.nix
  • lib/terraform.nix
  • lib/shell.nix
  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Consumer repos using devenv dev shells require `nix flake check --impure` (devenv needs impure evaluation to resolve the working directory)

Applied to files:

  • lib/terraform.nix
  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : All shell scripts must use `set -eo pipefail`

Applied to files:

  • lib/terraform.nix
  • lib/shell.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Return an attrset of derivations or values from library functions

Applied to files:

  • flake.nix
🔇 Additional comments (39)
.coderabbit.yaml (2)

3-13: Good explicit review behavior defaults.

profile: assertive with explicit review_details, sequence_diagrams, estimate_code_review_effort, and poem toggles is clear and schema-aligned.


19-27: auto_review configuration is valid and consistent.

base_branches: [".*"] and auto_pause_after_reviewed_commits: 7 are correctly structured under reviews.auto_review.

modules/storage.nix (1)

3-6: LGTM on the formatting refactor.

The let/in block reformatting improves readability and aligns with standard Nix style conventions.

modules/firewall.nix (1)

6-7: LGTM on the HTTP/HTTPS port management.

The use of lib.optional to conditionally include ports is idiomatic Nix. The port composition in allowedTCPPorts correctly maintains SSH (22) as always-allowed while making HTTP/HTTPS opt-in.

Also applies to: 26-26

lib/deploy.nix (3)

3-10: LGTM on the targetSystem parameterization.

The new optional targetSystem ? "x86_64-linux" parameter enables cross-architecture deployments while maintaining backward compatibility. Per the context snippets, existing callers rely on the default, which is appropriate since the template targets x86_64-linux droplets.


102-132: Shell scripts correctly use writeShellApplication with runtimeInputs.

All three wrapper scripts (deployNixos, deployService, deployAll) follow the coding guidelines by using pkgs.writeShellApplication with explicit runtimeInputs. The writeShellApplication function automatically enables set -euo pipefail, satisfying the shell safety requirement.

Based on learnings: "Use pkgs.writeShellApplication for shell scripts" and "Include runtimeInputs in shell scripts."


93-94: The --remote-build flag correctly enables remote builds on non-Linux systems, which is appropriate for cross-compilation from macOS or other platforms. Be aware that this shifts the build workload to the target NixOS host; ensure adequate resources are available on the deployment target.

templates/do-service/infra/variables.tf (2)

5-10: LGTM on the do_token validation hardening.

The nullable = false and trimspace() validation correctly prevent empty or whitespace-only tokens, addressing the past review feedback.


35-39: LGTM on the volume_size_gb integer validation.

The floor() equality check ensures only whole-number GB values are accepted, correctly enforcing the integer domain constraint from past review feedback.

modules/services.nix (2)

51-59: LGTM on the ReadWritePaths fix.

The logDir is now correctly included in ReadWritePaths alongside dataDir, resolving the previous issue where dynamicUser services couldn't access their log directories.


116-120: LGTM on the logDir option.

The new option follows conventions with proper type (nullOr str), default (null), and description. It integrates correctly with tmpfiles rules and logrotate configuration.

templates/do-service/services.nix (1)

4-4: LGTM on the dataDir path update.

Using /mnt/data/my-service instead of /mnt/data/prod provides a more service-specific default that avoids potential collisions when multiple services are deployed.

templates/do-service/infra/secrets.nix (1)

1-4: LGTM on the formatting refactor.

The multi-line let/in block improves readability and follows standard Nix formatting conventions.

templates/do-service/infra/terraform.tfvars.example (1)

1-2: LGTM on the security guidance comments.

The instructional comments clearly direct users to use nix run .#tfEditVars for encrypted variable management, preventing accidental plaintext credential storage.

templates/do-service/.github/workflows/ci.yml (1)

54-57: Good hardening on HOST_KEY evaluation before writing known_hosts.

The conditional guard on Lines 54-57 avoids appending invalid entries when key resolution fails.

lib/hooks.nix (1)

1-30: Looks good — clean hook composition and override model.

The conditional rustfmt hook and defaultHooks // extraHooks merge are clear and predictable.

examples/minimal.nix (1)

7-22: Good minimal example composition.

Module set and explicit omnix.base.stateVersion make this example reproducible and clear.

examples/single-service.nix (1)

17-37: LGTM for the single-service example.

The configuration is coherent and uses explicit omnix.* settings across included modules.

examples/full.nix (1)

19-54: Strong end-to-end example configuration.

Good coverage of module interactions (base, storage, firewall, acme, services) with explicit values.

lib/terraform.nix (3)

22-27: Add pkgs.nix to buildInputs for nix eval calls.

The inherited encryptState and encryptVars helpers from lib/shell.nix invoke nix eval at runtime, but buildInputs (used as runtimeInputs in mkTask) doesn't include pkgs.nix. Tasks using these helpers will fail when the nix binary isn't on PATH.

Suggested fix
   buildInputs = [
     pkgs.terraform
     pkgs.rage
     pkgs.jq
+    pkgs.nix
   ]
   ++ (if ragenixPkg != null then [ ragenixPkg ] else [ ]);

Based on learnings: "Include runtimeInputs in shell scripts and never assume tools are on PATH".


64-70: LGTM!

The mkTask helper correctly uses writeShellApplication with runtimeInputs = buildInputs, following the coding guidelines for shell script generation.


132-143: LGTM!

The remote script correctly declares its own runtimeInputs with the specific tools it needs (rage, jq, openssh), separate from the Terraform tasks.

templates/do-service/.github/workflows/integration-test.yml (2)

92-94: Don't suppress destroy failures.

if: always() already ensures teardown runs regardless of prior step outcomes. The trailing || true masks cleanup failures, potentially leaving droplets alive while the workflow reports success.

Suggested fix
       - name: Teardown
         if: always()
-        run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519 || true
+        run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519

28-40: LGTM!

The workflow correctly:

  • Wires inputs.droplet_size and inputs.region to TF_VAR_* environment variables
  • Pins DeterminateSystems actions to stable release tags (@v17, @v8)
lib/shell.nix (4)

7-13: Add validation for -i argument.

parseIdentity shifts 2 positions after checking only $1, but doesn't verify $2 exists. If called with -i but no second argument, $identity becomes empty and subsequent commands fail confusingly.

Proposed fix
   parseIdentity = ''
     identity=~/.ssh/id_ed25519
     if [ "''${1:-}" = "-i" ]; then
+      if [ -z "''${2:-}" ]; then
+        echo "Error: -i requires an identity file argument" >&2
+        exit 1
+      fi
       identity="$2"
       shift 2
     fi
   '';

22-27: Validate recipients before encrypting.

If nix eval fails or roles.infra is empty, rage encrypts to zero recipients, producing a file that cannot be decrypted. The pipeline silently continues, masking the error.

Proposed fix
   encryptState = ''
     if [ -f ${tfState} ]; then
-      nix eval --raw --file ${keysFile} roles.infra --apply 'builtins.concatStringsSep "\n"' \
-        | rage -e -R /dev/stdin -o ${tfState}.age ${tfState}
+      recipients=$(nix eval --raw --file ${keysFile} roles.infra --apply 'builtins.concatStringsSep "\n"')
+      if [ -z "$recipients" ]; then
+        echo "Error: No recipients found in ${keysFile} roles.infra" >&2
+        exit 1
+      fi
+      echo "$recipients" | rage -e -R /dev/stdin -o ${tfState}.age ${tfState}
     fi
   '';

34-37: Add file existence check and recipient validation.

encryptVars has the same silent-failure risk as encryptState, and additionally doesn't verify the source file exists before attempting encryption.

Proposed fix
   encryptVars = ''
+    if [ ! -f ${tfVars} ]; then
+      echo "Error: ${tfVars} not found" >&2
+      exit 1
+    fi
-    nix eval --raw --file ${keysFile} roles.infra --apply 'builtins.concatStringsSep "\n"' \
-      | rage -e -R /dev/stdin -o ${tfVars}.age ${tfVars}
+    recipients=$(nix eval --raw --file ${keysFile} roles.infra --apply 'builtins.concatStringsSep "\n"')
+    if [ -z "$recipients" ]; then
+      echo "Error: No recipients found in ${keysFile} roles.infra" >&2
+      exit 1
+    fi
+    echo "$recipients" | rage -e -R /dev/stdin -o ${tfVars}.age ${tfVars}
   '';

39-45: LGTM!

resolveIp correctly uses jq -e -r to fail fast on missing/null values, and includes a cleanup trap for the decrypted state file.

.github/workflows/integration-test.yml (2)

133-136: Don't suppress destroy failures.

if: always() guarantees the teardown step runs. The trailing || true can mask Terraform destroy failures, leaving infrastructure orphaned while the workflow reports success.

Suggested fix
       - name: Teardown
         if: always()
         working-directory: ${{ env.TEST_DIR }}
-        run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519 || true
+        run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519

31-43: LGTM!

The workflow correctly wires workflow_dispatch inputs to Terraform via TF_VAR_* environment variables, and pins DeterminateSystems actions to stable release tags (@v17, @v8).

ROADMAP.md (3)

111-113: Clarify mkIntegrationTest deliverable.

The description mentions parameterization by "project-specific config" but doesn't specify the expected signature or how consumers would invoke it. Adding a brief usage example would clarify scope.


3-21: LGTM!

PR review findings are now properly collapsed in a <details> section, keeping the roadmap focused on forward-looking work while preserving the historical record.


52-64: LGTM!

The Mermaid diagram now correctly shows parallel paths: B feeds both C/D (for CLI) and F/G (NixOS module/deploy-rs activation) independently, reflecting that these can be developed in parallel.

flake.nix (4)

109-116: Consider deriving dev-shell toolchain from the hook check.

The shellHook comes from checks.git-hooks, but packages are manually maintained. This can drift from hook configuration. The git-hooks.nix library provides enabledPackages that automatically includes the tools configured for hooks.

Suggested refactor
-        devShells.default = pkgs.mkShell {
-          inherit (self.checks.${system}.git-hooks) shellHook;
-          packages = [
-            pkgs.nixfmt
-            pkgs.deadnix
-            pkgs.taplo
-          ];
-        };
+        devShells.default =
+          let
+            inherit (self.checks.${system}.git-hooks) shellHook enabledPackages;
+          in
+          pkgs.mkShell {
+            inherit shellHook;
+            buildInputs = enabledPackages;
+          };

56-69: LGTM!

Good additions:

  • New acme module for ACME/Let's Encrypt support
  • nixosModules.default now bundles upstream disko.nixosModules.disko and ragenix.nixosModules.default, reducing boilerplate for consumers

98-99: LGTM!

Replacing the silent null fallback with an explicit throw provides a clear error message when disko isn't available for the target system, improving debuggability.


102-105: The git-hooks.lib.${system}.run API call is correctly formatted. The mkGitHooks function returns an attribute set with hook names as keys and their configurations as values, which matches the expected signature for git-hooks.nix's run function. The inherit hooks syntax correctly passes this attribute set to the expected hooks parameter.

templates/do-service/flake.nix (2)

95-105: LGTM!

The resolveIp package correctly uses writeShellApplication with appropriate runtimeInputs (rage for decryption, jq for JSON extraction). The shell snippet from infraPkgs.resolveIp sets $host_ip which is then echoed.


27-30: LGTM!

Simplified module imports to just omnix.nixosModules.default (which now includes upstream disko and ragenix) plus ./os.nix. This aligns with the refactored nixosModules.default in the main flake.

Comment thread .coderabbit.yaml
Comment on lines 32 to 34
path_instructions:
- path: "*"
instructions: >-

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In CodeRabbit reviews.path_instructions.pathglob matching, does"*" match only top-level files or all nested files recursively? What is the recommended pattern to target all files in a repository?

💡 Result:

In CodeRabbit reviews.path_instructions.path glob matching, which uses minimatch, the pattern "" matches only top-level files (and directories) in the current directory, not nested files recursively. It matches zero or more characters excluding path separators (/), so it does not cross directory boundaries. The recommended pattern to target all files in a repository recursively is "/" or "". Examples from docs: - "src/controllers/" targets all files under src/controllers recursively. - "tests/" targets all under tests recursively. - "docs//.md" targets all .md files recursively under docs. - "/.ts" for all TypeScript files repo-wide.

Citations:


Use the recommended glob pattern for repository-wide path instructions.

Line 33 uses path: "*", which matches only top-level files, not nested files recursively. For CodeRabbit path instructions that should apply repository-wide, use "/*" or an empty string "" instead (minimatch patterns).

Suggested change
-    - path: "*"
+    - path: "/*"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.coderabbit.yaml around lines 32 - 34, The path_instructions entry currently
uses path: "*" which only matches top-level files; change the repository-wide
glob in the .coderabbit.yaml path_instructions block (the path_instructions key
and its path: "*" entry) to use a recursive pattern such as path: "/*" or an
empty string "" so the instructions apply to all nested files; update that path
value accordingly and keep the rest of the path_instructions configuration
unchanged.

Comment thread .github/workflows/ci.yml
Comment on lines +17 to +18
- uses: DeterminateSystems/nix-installer-action@v17
- uses: DeterminateSystems/magic-nix-cache-action@v8

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Resolve immutable SHAs for the currently used action tags.
for spec in \
  "DeterminateSystems/nix-installer-action v17" \
  "DeterminateSystems/magic-nix-cache-action v8"
do
  repo=$(echo "$spec" | awk '{print $1}')
  tag=$(echo "$spec" | awk '{print $2}')
  sha=$(gh api "repos/${repo}/git/ref/tags/${tag}" --jq '.object.sha')
  echo "${repo}@${tag} -> ${sha}"
done

Repository: data-cartel/omnix

Length of output: 91


🏁 Script executed:

cat -n .github/workflows/ci.yml

Repository: data-cartel/omnix

Length of output: 1369


Pin all third-party actions to commit SHAs, not version tags.

Lines 16, 17, 18, 31, 32, 33 use mutable version tags (@v4, @v17, @v8). For stronger supply-chain integrity and reproducibility, pin each action to its full commit SHA (optionally annotate with the tag in a comment). This applies to:

  • actions/checkout@v4 (lines 16, 31)
  • DeterminateSystems/nix-installer-action@v17 (lines 17, 32)
  • DeterminateSystems/magic-nix-cache-action@v8 (lines 18, 33)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 17 - 18, The workflow pins third-party
GitHub Actions using mutable tags (actions/checkout@v4,
DeterminateSystems/nix-installer-action@v17,
DeterminateSystems/magic-nix-cache-action@v8); replace each tag with the
corresponding full commit SHA for every usage of those actions in the file (both
occurrences), optionally keeping the tag as a trailing comment for readability,
so each uses the immutable ref (full SHA) instead of `@vX` to improve supply-chain
integrity and reproducibility.

Comment on lines +45 to +50
- name: Setup SSH identity
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Secret name differs from template workflow.

This workflow uses secrets.SSH_KEY while templates/do-service/.github/workflows/integration-test.yml uses secrets.SSH_PRIVATE_KEY. This inconsistency could cause confusion when setting up repository secrets.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/integration-test.yml around lines 45 - 50, The workflow
step "Setup SSH identity" uses the wrong secret name; change the environment
variable reference in the echo line from ${{ secrets.SSH_KEY }} to ${{
secrets.SSH_PRIVATE_KEY }} (and update any other occurrences of secrets.SSH_KEY
in this workflow) so it matches the template's secret naming; ensure the step
that writes ~/.ssh/id_ed25519 still uses the updated secret reference and keep
existing chmod/mkdir lines unchanged.

Comment thread modules/acme.nix
Comment on lines +25 to +27
security.acme = {
acceptTerms = true;
defaults.email = cfg.email;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Make ACME terms acceptance overridable.

Line 26 hard-sets security.acme.acceptTerms = true;, which prevents downstream override priority. Use lib.mkDefault here.

♻️ Proposed fix
     security.acme = {
-      acceptTerms = true;
+      acceptTerms = lib.mkDefault true;
       defaults.email = cfg.email;
     };

As per coding guidelines, Use lib.mkDefault for overridable defaults.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
security.acme = {
acceptTerms = true;
defaults.email = cfg.email;
security.acme = {
acceptTerms = lib.mkDefault true;
defaults.email = cfg.email;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/acme.nix` around lines 25 - 27, security.acme.acceptTerms is hard-set
to true which prevents downstream overrides; change the assignment to use
lib.mkDefault so the default value can be overridden (e.g., replace the direct
true with lib.mkDefault true for security.acme.acceptTerms while keeping
defaults.email = cfg.email unchanged), referencing the security.acme.acceptTerms
attribute and lib.mkDefault helper.

Comment thread modules/firewall.nix
Comment on lines +13 to +14
enableHTTP = lib.mkEnableOption "port 80 (HTTP, needed for ACME challenges)";
enableHTTPS = lib.mkEnableOption "port 443 (HTTPS)";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Consider adding an assertion in modules/acme.nix to enforce enableHTTP.

The enableHTTP option correctly opens port 80 for ACME HTTP-01 challenges. However, per modules/acme.nix (context snippet 1), a user can enable ACME while leaving enableHTTP = false, silently breaking certificate validation.

Consider adding an assertion in acme.nix:

assertions = [
  # ... existing assertions
  {
    assertion = !cfg.enable || config.omnix.firewall.enableHTTP;
    message = "omnix.acme requires omnix.firewall.enableHTTP = true for HTTP-01 challenges";
  }
];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/firewall.nix` around lines 13 - 14, Add an assertion in
modules/acme.nix to require omnix.firewall.enableHTTP when ACME is enabled:
inside the existing assertions attribute, add an assertion that checks
cfg.enable implies config.omnix.firewall.enableHTTP and provide a clear message
like "omnix.acme requires omnix.firewall.enableHTTP = true for HTTP-01
challenges"; reference the assertions array and the
cfg.enable/config.omnix.firewall.enableHTTP symbols so the module will fail fast
if ACME is turned on but firewall HTTP is disabled.

Comment on lines +17 to +18
- uses: DeterminateSystems/nix-installer-action@v17
- uses: DeterminateSystems/magic-nix-cache-action@v8

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Resolve SHAs for tags used in the template CI workflow.
for spec in \
  "DeterminateSystems/nix-installer-action v17" \
  "DeterminateSystems/magic-nix-cache-action v8"
do
  repo=$(echo "$spec" | awk '{print $1}')
  tag=$(echo "$spec" | awk '{print $2}')
  sha=$(gh api "repos/${repo}/git/ref/tags/${tag}" --jq '.object.sha')
  echo "${repo}@${tag} -> ${sha}"
done

Repository: data-cartel/omnix

Length of output: 91


🏁 Script executed:

find . -path "*templates/do-service/.github/workflows/ci.yml" -type f

Repository: data-cartel/omnix

Length of output: 108


🏁 Script executed:

cat -n ./templates/do-service/.github/workflows/ci.yml

Repository: data-cartel/omnix

Length of output: 2112


Use immutable SHAs for all third-party GitHub Actions.

This workflow pins GitHub Actions to mutable release tags across all three jobs. Lines 16, 17-18, 25, 26-27, 42, 43-44 use @v tags (actions/checkout@v4, DeterminateSystems/nix-installer-action@v17, DeterminateSystems/magic-nix-cache-action@v8), which can be moved or force-pushed by maintainers. Pin each action to its immutable commit SHA to prevent silent supply-chain drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@templates/do-service/.github/workflows/ci.yml` around lines 17 - 18, Replace
mutable release tags with immutable commit SHAs for all third-party GitHub
Actions referenced (e.g., actions/checkout@v4,
DeterminateSystems/nix-installer-action@v17,
DeterminateSystems/magic-nix-cache-action@v8 and any other `@v` tags in the
workflow) by looking up the corresponding commit SHA on each action's GitHub
repo and updating the workflow references to use @<commit-sha>; ensure every
occurrence of the mutable tag is updated so no action remains pinned to a
mutable release.

Comment on lines +61 to +72
- name: Wait for SSH
run: |
retries=0
until ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
-i ~/.ssh/id_ed25519 "root@$HOST_IP" true 2>/dev/null; do
retries=$((retries + 1))
if [ "$retries" -ge 30 ]; then
echo "SSH not available after 2.5 minutes" >&2
exit 1
fi
sleep 5
done

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding BatchMode=yes to SSH options.

Adding BatchMode=yes prevents SSH from hanging on unexpected interactive prompts, making the retry loop more deterministic in CI.

Suggested fix
       - name: Wait for SSH
         run: |
           retries=0
-          until ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
+          until ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes \
             -i ~/.ssh/id_ed25519 "root@$HOST_IP" true 2>/dev/null; do
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Wait for SSH
run: |
retries=0
until ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
-i ~/.ssh/id_ed25519 "root@$HOST_IP" true 2>/dev/null; do
retries=$((retries + 1))
if [ "$retries" -ge 30 ]; then
echo "SSH not available after 2.5 minutes" >&2
exit 1
fi
sleep 5
done
- name: Wait for SSH
run: |
retries=0
until ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes \
-i ~/.ssh/id_ed25519 "root@$HOST_IP" true 2>/dev/null; do
retries=$((retries + 1))
if [ "$retries" -ge 30 ]; then
echo "SSH not available after 2.5 minutes" >&2
exit 1
fi
sleep 5
done
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@templates/do-service/.github/workflows/integration-test.yml` around lines 61
- 72, In the "Wait for SSH" workflow step the ssh command can hang on
interactive prompts; update the ssh invocation (the until ssh ... true loop) to
include the option -o BatchMode=yes alongside -o StrictHostKeyChecking=no and -o
ConnectTimeout=5 so SSH exits immediately on auth prompts and the retry loop
remains deterministic in CI.

Comment thread templates/do-service/os.nix Outdated
omnix.digitalocean.enable = true;
omnix.base = {
enable = true;
stateVersion = "24.11";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Align template stateVersion with the pinned NixOS release.

Line 11 sets "24.11" while this repository pin/examples are on 25.11, which creates avoidable drift for new deployments.

🔧 Proposed fix
-    stateVersion = "24.11";
+    stateVersion = "25.11";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
stateVersion = "24.11";
stateVersion = "25.11";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@templates/do-service/os.nix` at line 11, Update the stateVersion value to
match the repository's pinned NixOS release: change the stateVersion declaration
(symbol: stateVersion) from "24.11" to "25.11" so the template aligns with the
pinned/examples release and avoids deployment drift.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
flake.nix (1)

124-130: 🧹 Nitpick | 🔵 Trivial

Derive the dev shell from the hook check.

This still hardcodes the tool list while only reusing shellHook. The git-hooks.nix flake example inherits both shellHook and enabledPackages from the check, which keeps the shell aligned with the configured hooks and avoids mirroring that list here by hand. (github.com)

♻️ Suggested refactor
-        devShells.default = pkgs.mkShell {
-          inherit (self.checks.${system}.git-hooks) shellHook;
-          packages = [
-            pkgs.nixfmt
-            pkgs.deadnix
-            pkgs.taplo
-          ];
-        };
+        devShells.default =
+          let
+            inherit (self.checks.${system}.git-hooks) shellHook enabledPackages;
+          in
+          pkgs.mkShell {
+            inherit shellHook;
+            buildInputs = enabledPackages;
+          };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flake.nix` around lines 124 - 130, The dev shell currently hardcodes packages
while only inheriting shellHook from the git-hooks check; change it to derive
both shellHook and enabledPackages from the check so the shell stays in sync
with the hook config: in the devShells.default block inherit
(self.checks.${system}.git-hooks) shellHook enabledPackages and set the shell's
packages to the inherited enabledPackages (rather than listing pkgs.nixfmt,
pkgs.deadnix, pkgs.taplo) so updates to git-hooks enabledPackages automatically
propagate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@flake.nix`:
- Around line 113-115: The current packages.disko uses a hard throw
(disko.packages.${system}.default or (throw ...)) which causes flake evaluation
failures on unsupported systems; replace the throw with a conditional/optional
inclusion so unsupported systems simply omit the attribute. Change the
assignment so you first check for the presence of disko.packages for the current
system (e.g., via lib.hasAttr or by using the fallback
(disko.packages.${system}.default or null)) and then use lib.optionalAttrs or an
if expression to add packages.disko only when that value is non-null; update the
expression that currently references disko.packages.${system}.default and the
surrounding packages map to use this conditional logic (symbols: packages.disko,
disko.packages.${system}.default, flake-utils.lib.eachDefaultSystem).

---

Duplicate comments:
In `@flake.nix`:
- Around line 124-130: The dev shell currently hardcodes packages while only
inheriting shellHook from the git-hooks check; change it to derive both
shellHook and enabledPackages from the check so the shell stays in sync with the
hook config: in the devShells.default block inherit
(self.checks.${system}.git-hooks) shellHook enabledPackages and set the shell's
packages to the inherited enabledPackages (rather than listing pkgs.nixfmt,
pkgs.deadnix, pkgs.taplo) so updates to git-hooks enabledPackages automatically
propagate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0ba23601-453f-486d-ab3e-014dc7bd2d8f

📥 Commits

Reviewing files that changed from the base of the PR and between 03ba709 and 818fa7e.

📒 Files selected for processing (1)
  • flake.nix
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.nix

📄 CodeRabbit inference engine (AGENTS.md)

**/*.nix: Use lib.mkOption with proper types for all module options
Every module must be independently usable (no implicit dependencies on other omnix modules)
Options must live under the omnix.* namespace
Use lib.mkIf cfg.enable for conditional configuration
Use lib.mkDefault for overridable defaults
Every configurable value must be a module option with a type and description
Prefer lib.types.str over lib.types.path for values that may come from terraform or other dynamic sources
Group related options under a single omnix.<module> prefix
Functions take an attrset of required parameters
Return an attrset of derivations or values from library functions
Use pkgs.writeShellApplication for shell scripts (not raw writeScript)
Include runtimeInputs in shell scripts and never assume tools are on PATH
All shell scripts must use set -eo pipefail
When changing module option interfaces, preserve the old option with lib.mkRenamedOptionModule or lib.mkRemovedOptionModule with a clear migration message

Files:

  • flake.nix
*

⚙️ CodeRabbit configuration file

Focus on providing constructive criticism. Whenever you see a suboptimal approach, suggest more idiomatic or robust alternative(s). Flag potential footguns. Suggest FP alternatives to mutable/imperative code. Point out architectural flaws like leaky abstractions, tight coupling, wrong level of abstraction, poor type modeling, over-abstraction, unclear domain boundaries. Code should generally be organized based on business concerns rather than technical aspects - suggest improvements if you find violations. Point out gaps in test coverage but suggest tests that are not too coupled to the implementation and actually test domain invariants and business logic

Files:

  • flake.nix
🧠 Learnings (10)
📓 Common learnings
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Use `pkgs.writeShellApplication` for shell scripts (not raw `writeScript`)
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Include `runtimeInputs` in shell scripts and never assume tools are on PATH
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Template projects should pass `nix flake check`

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Consumer repos using devenv dev shells require `nix flake check --impure` (devenv needs impure evaluation to resolve the working directory)

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Include `runtimeInputs` in shell scripts and never assume tools are on PATH

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Use `pkgs.writeShellApplication` for shell scripts (not raw `writeScript`)

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Every module must be independently usable (no implicit dependencies on other omnix modules)

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Use `lib.mkDefault` for overridable defaults

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Return an attrset of derivations or values from library functions

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Group related options under a single `omnix.<module>` prefix

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Options must live under the `omnix.*` namespace

Applied to files:

  • flake.nix
🔇 Additional comments (2)
flake.nix (2)

37-62: Helpful example surface.

Promoting the examples to first-class nixosConfigurations makes the flake easier to discover and consume from the outside.


71-85: Nice self-contained default module.

Bundling upstream disko/ragenix with the local modules makes nixosModules.default usable as a single import instead of relying on consumers to remember extra upstream modules. As per coding guidelines, "Every module must be independently usable (no implicit dependencies on other omnix modules)".

Comment thread flake.nix
Comment on lines +113 to 115
packages.disko =
disko.packages.${system}.default or (throw "disko package not available for ${system}");
packages.ragenix = ragenix.packages.${system}.default;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rev=$(python - <<'PY'
import json
with open("flake.lock") as f:
    lock = json.load(f)
print(lock["nodes"]["disko"]["locked"]["rev"])
PY
)

for file in flake.nix package.nix default.nix; do
  echo "== ${file} @ ${rev} =="
  curl -Lsf "https://raw.githubusercontent.com/nix-community/disko/${rev}/${file}" \
    | rg -n 'supportedSystems|each(Default)?System|x86_64-darwin|aarch64-darwin|linux|packages|default' || true
  echo
done

Repository: data-cartel/omnix

Length of output: 1295


🏁 Script executed:

sed -n '100,125p' flake.nix

Repository: data-cartel/omnix

Length of output: 883


🏁 Script executed:

rg -A 5 'hasAttrByPath|optionalAttrs' /dev/stdin <<'EOF'
lib.optionalAttrs
pkgs.lib.optionalAttrs
hasAttrByPath
EOF

Repository: data-cartel/omnix

Length of output: 111


🏁 Script executed:

# Check flake-utils.lib.eachDefaultSystem default systems
git ls-files | grep -i flake-utils | head -5 || echo "Checking web source for flake-utils defaults"

Repository: data-cartel/omnix

Length of output: 43


🌐 Web query:

flake-utils lib.eachDefaultSystem default supported systems

💡 Result:

The default supported systems for flake-utils.lib.eachDefaultSystem are ["x86_64-linux", "aarch64-linux", "x86_64-darwin", "aarch64-darwin"]. This list is defined in flake-utils as defaultSystems and can be overridden via the "systems" input.

Citations:


Don't throw from a per-system package slot.

flake-utils.lib.eachDefaultSystem includes x86_64-darwin, but the pinned disko revision only exports packages for x86_64-linux, i686-linux, aarch64-linux, riscv64-linux, and aarch64-darwin. Because nix flake check evaluates packages.<system>.<name> for all systems, this hard throw turns packages.disko into a flake-evaluation failure on Intel macOS instead of just omitting the attribute.

Replace the throw with conditional inclusion:

Suggested fix
-        packages.disko =
-          disko.packages.${system}.default or (throw "disko package not available for ${system}");
-        packages.ragenix = ragenix.packages.${system}.default;
+        packages =
+          {
+            ragenix = ragenix.packages.${system}.default;
+          }
+          // lib.optionalAttrs (disko.packages ? ${system}) {
+            disko = disko.packages.${system}.default;
+          };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
packages.disko =
disko.packages.${system}.default or (throw "disko package not available for ${system}");
packages.ragenix = ragenix.packages.${system}.default;
packages =
{
ragenix = ragenix.packages.${system}.default;
}
// lib.optionalAttrs (disko.packages ? ${system}) {
disko = disko.packages.${system}.default;
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flake.nix` around lines 113 - 115, The current packages.disko uses a hard
throw (disko.packages.${system}.default or (throw ...)) which causes flake
evaluation failures on unsupported systems; replace the throw with a
conditional/optional inclusion so unsupported systems simply omit the attribute.
Change the assignment so you first check for the presence of disko.packages for
the current system (e.g., via lib.hasAttr or by using the fallback
(disko.packages.${system}.default or null)) and then use lib.optionalAttrs or an
if expression to add packages.disko only when that value is non-null; update the
expression that currently references disko.packages.${system}.default and the
surrounding packages map to use this conditional logic (symbols: packages.disko,
disko.packages.${system}.default, flake-utils.lib.eachDefaultSystem).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@flake.nix`:
- Around line 107-110: The flake currently calls lib.mkGitHooks with an empty
attribute set which uses defaults; update the documentation/comments near the
mkGitHooks invocation to note that mkGitHooks supports customization via
extraHooks and rustToolchain (see lib/hooks.nix) so downstream consumers know
they can pass those keys (e.g., extraHooks = ... or rustToolchain = ...) to
customize hooks and Rust toolchain behavior; include a short example in the
comment showing the attribute names and purpose.
- Around line 72-86: The convenience bundle `default` currently imports upstream
modules (e.g., disko.nixosModules.disko and ragenix.nixosModules.default) which
can surprise users who also import those upstream modules directly; add a short
note to the repository documentation (README or SPEC.md) stating that
nixosModules.default includes upstream dependencies and that duplicate imports
are handled by the NixOS module system, and include an example or sentence
advising users to avoid redundant explicit imports when using the convenience
`default` bundle (refer to the `default` attribute and the symbols
disko.nixosModules.disko, ragenix.nixosModules.default, and
nixosModules.default).
- Around line 88-95: The lib import is passing an unused parameter `git-hooks`
into the `lib` function; remove `git-hooks` from the inherit clause in the `lib
= import ./lib { inherit ... }` block (leave `deploy-rs` and `nixos-anywhere`
intact) and verify there are no other references to `git-hooks` in the flake so
the call matches `lib/default.nix`'s actual parameters.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 56e9420e-bd2e-4316-b822-4ca03a2409b2

📥 Commits

Reviewing files that changed from the base of the PR and between 818fa7e and 9b79878.

📒 Files selected for processing (3)
  • .coderabbit.yaml
  • examples/minimal.nix
  • flake.nix
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.nix

📄 CodeRabbit inference engine (AGENTS.md)

**/*.nix: Use lib.mkOption with proper types for all module options
Every module must be independently usable (no implicit dependencies on other omnix modules)
Options must live under the omnix.* namespace
Use lib.mkIf cfg.enable for conditional configuration
Use lib.mkDefault for overridable defaults
Every configurable value must be a module option with a type and description
Prefer lib.types.str over lib.types.path for values that may come from terraform or other dynamic sources
Group related options under a single omnix.<module> prefix
Functions take an attrset of required parameters
Return an attrset of derivations or values from library functions
Use pkgs.writeShellApplication for shell scripts (not raw writeScript)
Include runtimeInputs in shell scripts and never assume tools are on PATH
All shell scripts must use set -eo pipefail
When changing module option interfaces, preserve the old option with lib.mkRenamedOptionModule or lib.mkRemovedOptionModule with a clear migration message

Files:

  • examples/minimal.nix
  • flake.nix
*

⚙️ CodeRabbit configuration file

Focus on providing constructive criticism. Whenever you see a suboptimal approach, suggest more idiomatic or robust alternative(s). Flag potential footguns. Suggest FP alternatives to mutable/imperative code. Point out architectural flaws like leaky abstractions, tight coupling, wrong level of abstraction, poor type modeling, over-abstraction, unclear domain boundaries. Code should generally be organized based on business concerns rather than technical aspects - suggest improvements if you find violations. Point out gaps in test coverage but suggest tests that are not too coupled to the implementation and actually test domain invariants and business logic

Files:

  • flake.nix
🧠 Learnings (10)
📓 Common learnings
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Include `runtimeInputs` in shell scripts and never assume tools are on PATH
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Use `pkgs.writeShellApplication` for shell scripts (not raw `writeScript`)
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Every module must be independently usable (no implicit dependencies on other omnix modules)

Applied to files:

  • examples/minimal.nix
  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Group related options under a single `omnix.<module>` prefix

Applied to files:

  • examples/minimal.nix
  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Template projects should pass `nix flake check`

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Consumer repos using devenv dev shells require `nix flake check --impure` (devenv needs impure evaluation to resolve the working directory)

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Include `runtimeInputs` in shell scripts and never assume tools are on PATH

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Use `pkgs.writeShellApplication` for shell scripts (not raw `writeScript`)

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Use `lib.mkDefault` for overridable defaults

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Return an attrset of derivations or values from library functions

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Options must live under the `omnix.*` namespace

Applied to files:

  • flake.nix
🔇 Additional comments (12)
.coderabbit.yaml (5)

32-34: Use **/* for repository-wide path instructions.

The path: "*" pattern only matches top-level files, not files in subdirectories. For instructions that should apply repository-wide, use **/* instead.


3-9: Documentation comments improve maintainability.

The inline comments explaining configuration options (profile, review_details) are helpful for future maintainers. LGTM.


14-17: Clear explanation of the request changes workflow.

The comment accurately describes the behavior of request_changes_workflow. LGTM.


19-27: Auto-review configuration is valid and well-documented.

  • base_branches: [".*"] correctly enables reviews on all branches using regex.
  • auto_pause_after_reviewed_commits: 7 is a reasonable threshold to reduce noise on active PRs.
  • Comments accurately describe the settings.

44-55: Markdown-specific instructions are well-structured.

The **/*.md pattern correctly uses recursive matching, and the instructions provide clear guidance tailored to different document types (specs, roadmaps, agent guidelines, READMEs). The focus on content over formatting and audience-aware commenting is appropriate.

flake.nix (6)

112-114: Hard throw breaks flake evaluation on unsupported systems.

This was flagged in a previous review. flake-utils.lib.eachDefaultSystem includes x86_64-darwin, but disko may not export packages for all systems. The throw causes nix flake check to fail instead of gracefully omitting the attribute.


123-130: Dev shell packages are manually maintained and may drift from hooks configuration.

This was flagged in a previous review. The packages list (nixfmt, deadnix, taplo) is manually maintained and will diverge from lib/hooks.nix if hooks are added/removed. Consider deriving packages from the git-hooks check's enabledPackages.


19-21: Git-hooks input properly configured.

The input correctly follows nixpkgs to avoid version conflicts. This is the standard pattern for git-hooks.nix integration.


37-61: Clean nixosConfigurations structure.

Each example receives only the flake inputs it requires, following the principle of minimal dependencies. This makes the dependency graph clear and examples easier to reason about.


116-119: Correct git-hooks integration.

The git-hooks.lib.${system}.run pattern with src = self follows the standard git-hooks.nix usage. The hooks configuration from self.lib.mkGitHooks { } correctly produces the attribute set expected by the library.


121-121: Formatter correctly configured.

Using pkgs.nixfmt as the formatter aligns with the nixfmt hook in lib/hooks.nix, ensuring consistent formatting between CI and local development.

examples/minimal.nix (1)

1-21: Well-structured minimal example.

This example correctly demonstrates:

  • Independent module imports (both upstream disko.nixosModules.disko and self.nixosModules.disko)
  • Proper use of omnix.* namespace for configuration
  • Required stateVersion is explicitly set (matching the nixos-25.11 input)
  • Uses default disk device (/dev/vda) which is appropriate for a minimal example

Comment thread flake.nix
Comment on lines +72 to 86
# Convenience: all modules at once (includes upstream disko + ragenix)
default = {
imports = [
disko.nixosModules.disko
ragenix.nixosModules.default
self.nixosModules.disko
self.nixosModules.digitalocean
self.nixosModules.base
self.nixosModules.storage
self.nixosModules.services
self.nixosModules.firewall
self.nixosModules.acme
];
};
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Bundling upstream modules in default is convenient but document the behavior.

Including disko.nixosModules.disko and ragenix.nixosModules.default in the convenience bundle is helpful, but users who also import these upstream modules directly in their configuration may be surprised. The NixOS module system handles duplicate imports gracefully, but this should be documented.

Consider adding a brief note in README or SPEC.md that nixosModules.default includes upstream dependencies.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flake.nix` around lines 72 - 86, The convenience bundle `default` currently
imports upstream modules (e.g., disko.nixosModules.disko and
ragenix.nixosModules.default) which can surprise users who also import those
upstream modules directly; add a short note to the repository documentation
(README or SPEC.md) stating that nixosModules.default includes upstream
dependencies and that duplicate imports are handled by the NixOS module system,
and include an example or sentence advising users to avoid redundant explicit
imports when using the convenience `default` bundle (refer to the `default`
attribute and the symbols disko.nixosModules.disko,
ragenix.nixosModules.default, and nixosModules.default).

Comment thread flake.nix
Comment thread flake.nix
Comment on lines +107 to +110
let
pkgs = import nixpkgs { inherit system; };
hooks = self.lib.mkGitHooks { };
in

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider documenting hook customization.

mkGitHooks accepts extraHooks and rustToolchain parameters per lib/hooks.nix. The current usage with { } uses defaults only. This is correct, but downstream consumers may not know these customization options exist.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flake.nix` around lines 107 - 110, The flake currently calls lib.mkGitHooks
with an empty attribute set which uses defaults; update the
documentation/comments near the mkGitHooks invocation to note that mkGitHooks
supports customization via extraHooks and rustToolchain (see lib/hooks.nix) so
downstream consumers know they can pass those keys (e.g., extraHooks = ... or
rustToolchain = ...) to customize hooks and Rust toolchain behavior; include a
short example in the comment showing the attribute names and purpose.

@0xgleb 0xgleb disabled auto-merge April 3, 2026 04:27
@0xgleb 0xgleb enabled auto-merge April 3, 2026 04:27
@0xgleb 0xgleb added this pull request to the merge queue Apr 3, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Apr 3, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (3)
flake.nix (2)

111-113: ⚠️ Potential issue | 🟠 Major

Don't throw from packages.disko on unsupported systems.

eachDefaultSystem publishes outputs for platforms that the pinned disko input may not support. This fallback turns a missing upstream package into a full flake-evaluation failure instead of simply omitting disko from packages.

♻️ Suggested fix
-        packages.disko =
-          disko.packages.${system}.default or (throw "disko package not available for ${system}");
-        packages.ragenix = ragenix.packages.${system}.default;
+        packages =
+          {
+            ragenix = ragenix.packages.${system}.default;
+          }
+          // pkgs.lib.optionalAttrs (builtins.hasAttr system disko.packages) {
+            disko = disko.packages.${system}.default;
+          };

To verify against the pinned disko revision, the script below prints the revision from flake.lock and the upstream files that declare supported systems. If one of the default flake-utils systems is absent there, the current throw is the blocker on that platform.

#!/bin/bash
set -euo pipefail

disko_rev=$(python - <<'PY'
import json
with open("flake.lock") as f:
    lock = json.load(f)
print(lock["nodes"]["disko"]["locked"]["rev"])
PY
)

echo "Pinned disko revision: ${disko_rev}"
echo
echo "Current package block:"
sed -n '104,113p' flake.nix

echo
for file in flake.nix default.nix package.nix; do
  echo "== ${file} @ ${disko_rev} =="
  curl -Lsf "https://raw.githubusercontent.com/nix-community/disko/${disko_rev}/${file}" \
    | rg -n 'supportedSystems|each(Default)?System|x86_64-linux|aarch64-linux|x86_64-darwin|aarch64-darwin|packages|default' || true
  echo
done
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flake.nix` around lines 111 - 113, The packages.disko entry currently does
"disko.packages.${system}.default or (throw ...)" which causes full flake
evaluation failures on systems that the pinned disko input doesn't support;
change that fallback to a non-fatal value (for example replace the throw with
null or remove the attribute via builtins.getAttr or getAttrDefault) so
unsupported systems simply omit disko from packages when using eachDefaultSystem
— update the expression referencing packages.disko (and keep packages.ragenix
unchanged) to return null/absent instead of throwing.

108-109: 🧹 Nitpick | 🔵 Trivial

Keep the dev-shell tool list sourced from the hook definition.

lib/hooks.nix already owns the default formatter/linter set, but the same tools are hardcoded again here. That will drift the first time extraHooks or rustToolchain changes the hook configuration, so please derive the dev-shell packages from the same source of truth instead of maintaining both lists.

Also applies to: 122-129

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flake.nix` around lines 108 - 109, The dev-shell tool list is hardcoded
instead of being derived from the git hooks configuration; replace the
duplicated tool list with the set produced by the hook definition by assigning
hooks = self.lib.mkGitHooks { ... } (including extraHooks and rustToolchain) and
then reference the resulting hooks' tool list when building the dev-shell.
Concretely, stop re-declaring the formatter/linter packages and consume the
packages exported by the result of self.lib.mkGitHooks (the same `hooks` value
used here and in the repeated block at 122-129) to populate the dev-shell tools
so changes to extraHooks or rustToolchain flow through automatically.
.github/workflows/ci.yaml (1)

15-17: ⚠️ Potential issue | 🟠 Major

Pin every action to an immutable commit SHA.

These uses: refs are still mutable tags, so an upstream retag can change CI behavior without a PR here. Replace them with full commit SHAs and keep the tag in a trailing comment if you want readability.

To resolve the exact immutable refs, the script below prints the commit behind each current tag. The expected result is one full SHA per action; those SHAs should replace the tag refs in both jobs.

#!/bin/bash
set -euo pipefail

for spec in \
  "actions/checkout v4" \
  "DeterminateSystems/nix-installer-action v17" \
  "DeterminateSystems/magic-nix-cache-action v8"
do
  repo=${spec% *}
  tag=${spec#* }
  sha=$(git ls-remote "https://github.com/${repo}.git" "refs/tags/${tag}^{}" | awk '{print $1}')
  if [ -z "$sha" ]; then
    sha=$(git ls-remote "https://github.com/${repo}.git" "refs/tags/${tag}" | awk '{print $1}')
  fi
  printf '%s@%s -> %s\n' "$repo" "$tag" "$sha"
done

echo
echo "Current workflow refs:"
sed -n '12,33p' .github/workflows/ci.yaml

Also applies to: 31-33

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yaml around lines 15 - 17, Replace the mutable action
tags with immutable commit SHAs for each `uses:` entry — specifically change
`actions/checkout@v4`, `DeterminateSystems/nix-installer-action@v17`, and
`DeterminateSystems/magic-nix-cache-action@v8` to the full commit SHA for the
tag (you can use the provided script to lookup the SHAs), and keep the original
tag as a trailing comment for readability; make the same replacements for the
duplicate occurrences of those three `uses:` refs elsewhere in the workflow so
all refs are pinned to immutable commits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In @.github/workflows/ci.yaml:
- Around line 15-17: Replace the mutable action tags with immutable commit SHAs
for each `uses:` entry — specifically change `actions/checkout@v4`,
`DeterminateSystems/nix-installer-action@v17`, and
`DeterminateSystems/magic-nix-cache-action@v8` to the full commit SHA for the
tag (you can use the provided script to lookup the SHAs), and keep the original
tag as a trailing comment for readability; make the same replacements for the
duplicate occurrences of those three `uses:` refs elsewhere in the workflow so
all refs are pinned to immutable commits.

In `@flake.nix`:
- Around line 111-113: The packages.disko entry currently does
"disko.packages.${system}.default or (throw ...)" which causes full flake
evaluation failures on systems that the pinned disko input doesn't support;
change that fallback to a non-fatal value (for example replace the throw with
null or remove the attribute via builtins.getAttr or getAttrDefault) so
unsupported systems simply omit disko from packages when using eachDefaultSystem
— update the expression referencing packages.disko (and keep packages.ragenix
unchanged) to return null/absent instead of throwing.
- Around line 108-109: The dev-shell tool list is hardcoded instead of being
derived from the git hooks configuration; replace the duplicated tool list with
the set produced by the hook definition by assigning hooks = self.lib.mkGitHooks
{ ... } (including extraHooks and rustToolchain) and then reference the
resulting hooks' tool list when building the dev-shell. Concretely, stop
re-declaring the formatter/linter packages and consume the packages exported by
the result of self.lib.mkGitHooks (the same `hooks` value used here and in the
repeated block at 122-129) to populate the dev-shell tools so changes to
extraHooks or rustToolchain flow through automatically.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9940b931-a594-42fd-89ea-defe2c180683

📥 Commits

Reviewing files that changed from the base of the PR and between 9b79878 and 8809465.

📒 Files selected for processing (2)
  • .github/workflows/ci.yaml
  • flake.nix
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.nix

📄 CodeRabbit inference engine (AGENTS.md)

**/*.nix: Use lib.mkOption with proper types for all module options
Every module must be independently usable (no implicit dependencies on other omnix modules)
Options must live under the omnix.* namespace
Use lib.mkIf cfg.enable for conditional configuration
Use lib.mkDefault for overridable defaults
Every configurable value must be a module option with a type and description
Prefer lib.types.str over lib.types.path for values that may come from terraform or other dynamic sources
Group related options under a single omnix.<module> prefix
Functions take an attrset of required parameters
Return an attrset of derivations or values from library functions
Use pkgs.writeShellApplication for shell scripts (not raw writeScript)
Include runtimeInputs in shell scripts and never assume tools are on PATH
All shell scripts must use set -eo pipefail
When changing module option interfaces, preserve the old option with lib.mkRenamedOptionModule or lib.mkRemovedOptionModule with a clear migration message

Files:

  • flake.nix
*

⚙️ CodeRabbit configuration file

Focus on providing constructive criticism. Whenever you see a suboptimal approach, suggest more idiomatic or robust alternative(s). Flag potential footguns. Suggest FP alternatives to mutable/imperative code. Point out architectural flaws like leaky abstractions, tight coupling, wrong level of abstraction, poor type modeling, over-abstraction, unclear domain boundaries. Code should generally be organized based on business concerns rather than technical aspects - suggest improvements if you find violations. Point out gaps in test coverage but suggest tests that are not too coupled to the implementation and actually test domain invariants and business logic

Files:

  • flake.nix
🧠 Learnings (11)
📓 Common learnings
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Use `pkgs.writeShellApplication` for shell scripts (not raw `writeScript`)
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Include `runtimeInputs` in shell scripts and never assume tools are on PATH
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Template projects should pass `nix flake check`

Applied to files:

  • .github/workflows/ci.yaml
  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Consumer repos using devenv dev shells require `nix flake check --impure` (devenv needs impure evaluation to resolve the working directory)

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Include `runtimeInputs` in shell scripts and never assume tools are on PATH

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Use `pkgs.writeShellApplication` for shell scripts (not raw `writeScript`)

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Use `lib.mkDefault` for overridable defaults

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Use `lib.mkOption` with proper types for all module options

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Every module must be independently usable (no implicit dependencies on other omnix modules)

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Return an attrset of derivations or values from library functions

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Group related options under a single `omnix.<module>` prefix

Applied to files:

  • flake.nix
📚 Learning: 2026-03-21T15:47:14.585Z
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Applies to **/*.nix : Options must live under the `omnix.*` namespace

Applied to files:

  • flake.nix
🔇 Additional comments (2)
.github/workflows/ci.yaml (1)

23-28: Good public-surface CI coverage.

Evaluating and dry-running the exported nixosConfigurations.<name>.config.system.build.toplevel keeps this job aligned with the flake interface consumers actually use.

Also applies to: 35-39

flake.nix (1)

37-61: Nice to expose the examples as first-class flake outputs.

These give CI and downstream users stable public entry points to validate instead of reaching into individual files.

@0xgleb 0xgleb enabled auto-merge April 3, 2026 08:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
.github/workflows/infra.yaml (2)

98-102: ⚠️ Potential issue | 🟠 Major

Harden all SSH calls to be non-interactive and time-bounded.

The wait loop still lacks BatchMode=yes, and the later verification SSH commands (Line 120 and Line 124) drop timeout/non-interactive options entirely. This can stall jobs on auth or network edge cases.

Suggested change
       - name: Wait for SSH
         run: |
           retries=0
-          until ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
+          until ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes \
             -i ~/.ssh/id_ed25519 "root@$HOST_IP" true 2>/dev/null; do
             retries=$((retries + 1))
             if [ "$retries" -ge 30 ]; then
               echo "SSH not available after 2.5 minutes" >&2
               exit 1
             fi
             sleep 5
           done

       - name: Verify SSH access
-        run: ssh -i ~/.ssh/id_ed25519 "root@$HOST_IP" uname -a
+        run: ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes \
+          -i ~/.ssh/id_ed25519 "root@$HOST_IP" uname -a

       - name: Verify NixOS
         run: |
-          RESULT=$(ssh -i ~/.ssh/id_ed25519 "root@$HOST_IP" \
+          RESULT=$(ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes \
+            -i ~/.ssh/id_ed25519 "root@$HOST_IP" \
             "grep -c NixOS /etc/os-release")
           if [ "$RESULT" -lt 1 ]; then
             echo "Host is not running NixOS" >&2
             exit 1
           fi

Also applies to: 119-125

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/infra.yaml around lines 98 - 102, The SSH wait loop and
subsequent verification SSH invocations are interactive and risk hanging; update
every ssh invocation (the wait loop command `ssh -o StrictHostKeyChecking=no -o
ConnectTimeout=5 -i ~/.ssh/id_ed25519 "root@$HOST_IP" true` and the later
verification SSH commands) to include `-o BatchMode=yes` and an explicit `-o
ConnectTimeout=<seconds>` (and preserve `-o StrictHostKeyChecking=no` and the
identity `-i ~/.ssh/id_ed25519`) so all SSH calls are non-interactive and
time-bounded; ensure you apply the same set of SSH options to each SSH call in
the workflow.

131-134: ⚠️ Potential issue | 🟠 Major

Do not suppress tfDestroy failures.

Line 134 hides cleanup errors with || true, which can leave resources alive while the job appears successful.

Suggested change
-        run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519 || true
+        run: nix run .#tfDestroy -- -i ~/.ssh/id_ed25519
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/infra.yaml around lines 131 - 134, The Teardown step
currently suppresses tfDestroy failures by appending "|| true" to the run
command; remove that "|| true" so the "nix run .#tfDestroy -- -i
~/.ssh/id_ed25519" command can fail the workflow when destroy fails (leave the
step name "Teardown" and the run invocation "nix run .#tfDestroy" intact so
failures are visible and resources aren't left alive silently).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/infra.yaml:
- Around line 58-59: Replace the hostile TODO comment text ("get rid of this
retarded garbage...") with a neutral, professional note that explains the issue
and suggests a corrective action; for example, change the line to something like
"TODO: refactor this to use workflow variables or other standard approaches to
improve readability and maintainability" (i.e., remove profanity, state the
problem, and recommend using variables/standard patterns).
- Around line 84-90: Add a new workflow step to run a flake validation before
running Terraform: insert a step (e.g., name: "Nix flake check") before the
"Terraform init and apply" step that runs in the same working-directory ${{
env.TEST_DIR }} and executes either `nix flake check` or `nix run .#flakeCheck`
(so the pipeline fails fast on template breakage); ensure this step runs with
the same environment/SSH setup as the subsequent tfInit/tfPlan/tfApply steps and
returns non-zero on failure so provisioning is blocked when the flake check
fails.
- Around line 4-5: The workflow currently triggers on pull_request_review types:
[submitted] which runs for all review submissions; update the job to only
proceed for approved reviews by adding a job-level condition that checks
github.event.review.state == 'approved' (i.e., add if: github.event.review.state
== 'approved' on the job that runs the expensive infra lifecycle). Target the
pull_request_review event and the job(s) that depend on that event so only
reviews with state "approved" execute the full lifecycle.

---

Duplicate comments:
In @.github/workflows/infra.yaml:
- Around line 98-102: The SSH wait loop and subsequent verification SSH
invocations are interactive and risk hanging; update every ssh invocation (the
wait loop command `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -i
~/.ssh/id_ed25519 "root@$HOST_IP" true` and the later verification SSH commands)
to include `-o BatchMode=yes` and an explicit `-o ConnectTimeout=<seconds>` (and
preserve `-o StrictHostKeyChecking=no` and the identity `-i ~/.ssh/id_ed25519`)
so all SSH calls are non-interactive and time-bounded; ensure you apply the same
set of SSH options to each SSH call in the workflow.
- Around line 131-134: The Teardown step currently suppresses tfDestroy failures
by appending "|| true" to the run command; remove that "|| true" so the "nix run
.#tfDestroy -- -i ~/.ssh/id_ed25519" command can fail the workflow when destroy
fails (leave the step name "Teardown" and the run invocation "nix run
.#tfDestroy" intact so failures are visible and resources aren't left alive
silently).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7aee4467-f2c6-402e-8029-81a964298d61

📥 Commits

Reviewing files that changed from the base of the PR and between 8809465 and 3440c5c.

📒 Files selected for processing (1)
  • .github/workflows/infra.yaml
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: data-cartel/omnix PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-21T15:47:14.585Z
Learning: Template projects should pass `nix flake check`

Comment thread .github/workflows/infra.yaml Outdated
Comment on lines +4 to +5
pull_request_review:
types: [submitted]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🌐 Web query:

For GitHub Actions pull_request_reviewevents, what are the possiblegithub.event.review.statevalues, and istypes: [submitted] triggered for both approval and comment-only reviews?

💡 Result:

The possible values for github.event.review.state in GitHub Actions pull_request_review events are: APPROVED, CHANGES_REQUESTED, COMMENTED, DISMISSED, and PENDING. Yes, types: [submitted] is triggered for both approval (APPROVED) and comment-only (COMMENTED) reviews, as "submitted" refers to the pull_request_review event being triggered when a review is submitted (i.e., state changes from PENDING to one of the submitted states: APPROVED, CHANGES_REQUESTED, or COMMENTED). PENDING reviews do not trigger the event, and DISMISSED may trigger on dismissed type.

Citations:


🏁 Script executed:

#!/bin/bash
# Locate and examine the infra.yaml workflow file
fd -i "infra.yaml" -type f

Repository: data-cartel/omnix

Length of output: 231


🏁 Script executed:

#!/bin/bash
# Find infra.yaml workflow file
fd "infra.yaml" .github/workflows/

Repository: data-cartel/omnix

Length of output: 89


🏁 Script executed:

#!/bin/bash
# Read the infra.yaml file
cat -n .github/workflows/infra.yaml

Repository: data-cartel/omnix

Length of output: 5024


🌐 Web query:

GitHub Actions github.event.review.state case sensitivity uppercase or lowercase comparison

💡 Result:

In GitHub Actions, comparisons with github.event.review.state are case-sensitive. The value is lowercase, such as 'approved', 'changes_requested', or 'commented'. Using uppercase like 'APPROVED' will not match. Official GitHub docs show examples using lowercase: if: github.event.review.state == 'approved' (result 7 from first search). A 2023 PR in github/docs corrected documentation from lowercase to uppercase 'APPROVED' because the example didn't work, confirming the actual value is uppercase in some contexts like GraphQL, but for Actions webhook payloads, it's lowercase as per working examples and API consistency (results 1, 6, 7). REST API review states are uppercase (APPROVED, etc., result 8, 19), but webhook payloads for pull_request_review events use lowercase for the review.state field, as evidenced by multiple code examples and the correction needed in docs.

Citations:


Gate infra runs to approved reviews only.

pull_request_review with types: [submitted] runs this expensive full lifecycle on comment-only and change-request reviews as well. Gate the job to approved reviews only:

Suggested change
 jobs:
   infra:
     name: Full lifecycle
+    if: github.event_name != 'pull_request_review' || github.event.review.state == 'approved'
     runs-on: ubuntu-latest
     timeout-minutes: 30
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/infra.yaml around lines 4 - 5, The workflow currently
triggers on pull_request_review types: [submitted] which runs for all review
submissions; update the job to only proceed for approved reviews by adding a
job-level condition that checks github.event.review.state == 'approved' (i.e.,
add if: github.event.review.state == 'approved' on the job that runs the
expensive infra lifecycle). Target the pull_request_review event and the job(s)
that depend on that event so only reviews with state "approved" execute the full
lifecycle.

Comment thread .github/workflows/infra.yaml Outdated
Comment on lines +58 to +59
# TODO: get rid of this retarded garbage and learn the concept of variables
# there is like a thousand ways of doing this and this is by far the dumbest

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Replace hostile TODO wording with neutral guidance.

The current comment text is unprofessional and makes maintenance harder for collaborators.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/infra.yaml around lines 58 - 59, Replace the hostile TODO
comment text ("get rid of this retarded garbage...") with a neutral,
professional note that explains the issue and suggests a corrective action; for
example, change the line to something like "TODO: refactor this to use workflow
variables or other standard approaches to improve readability and
maintainability" (i.e., remove profanity, state the problem, and recommend using
variables/standard patterns).

Comment on lines +84 to +90
- name: Terraform init and apply
working-directory: ${{ env.TEST_DIR }}
run: |
nix run .#tfInit -- -i ~/.ssh/id_ed25519
nix run .#tfPlan -- -i ~/.ssh/id_ed25519
nix run .#tfApply -- -i ~/.ssh/id_ed25519

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Run nix flake check before cloud provisioning.

The workflow goes straight to tfInit/tfPlan/tfApply without validating the generated template flake, so basic template breakage is discovered only after infra actions begin.

Suggested change
       - name: Terraform init and apply
         working-directory: ${{ env.TEST_DIR }}
         run: |
+          nix flake check
           nix run .#tfInit -- -i ~/.ssh/id_ed25519
           nix run .#tfPlan -- -i ~/.ssh/id_ed25519
           nix run .#tfApply -- -i ~/.ssh/id_ed25519

Based on learnings: Template projects should pass nix flake check.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Terraform init and apply
working-directory: ${{ env.TEST_DIR }}
run: |
nix run .#tfInit -- -i ~/.ssh/id_ed25519
nix run .#tfPlan -- -i ~/.ssh/id_ed25519
nix run .#tfApply -- -i ~/.ssh/id_ed25519
- name: Terraform init and apply
working-directory: ${{ env.TEST_DIR }}
run: |
nix flake check
nix run .#tfInit -- -i ~/.ssh/id_ed25519
nix run .#tfPlan -- -i ~/.ssh/id_ed25519
nix run .#tfApply -- -i ~/.ssh/id_ed25519
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/infra.yaml around lines 84 - 90, Add a new workflow step
to run a flake validation before running Terraform: insert a step (e.g., name:
"Nix flake check") before the "Terraform init and apply" step that runs in the
same working-directory ${{ env.TEST_DIR }} and executes either `nix flake check`
or `nix run .#flakeCheck` (so the pipeline fails fast on template breakage);
ensure this step runs with the same environment/SSH setup as the subsequent
tfInit/tfPlan/tfApply steps and returns non-zero on failure so provisioning is
blocked when the flake check fails.

@0xgleb 0xgleb added this pull request to the merge queue Apr 3, 2026
@0xgleb 0xgleb removed this pull request from the merge queue due to a manual request Apr 3, 2026
@0xgleb 0xgleb enabled auto-merge April 3, 2026 09:27
@0xgleb 0xgleb closed this May 9, 2026
auto-merge was automatically disabled May 9, 2026 14:58

Pull request was closed

@0xgleb 0xgleb deleted the chore/refactor branch May 9, 2026 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants